当前位置: 首页 > news >正文

Android启动优化实践

作者:95分技术

启动优化是Android优化老生常谈的问题了。众所周知,android的启动是指用户从点击 icon 到看到首帧可交互的流程。

而启动流程 粗略的可以分为以下几个阶段

  1. fork创建出一个新的进程
  2. 创建初始化Application类、创建四大组件等 走Application.onCreate()
  3. 创建launchActivity 走完onCreate、onStart、onResume生命周期

往深往细里面钻研 这里可以有非常多的‘黑科技’能操作,mutilDex优化,message调度优化,json预热之类的方案非常多。

本文只解决一个点,针对Application.onCreate()做优化。

一、技术背景

随着业务的发展堆叠,application中初始化方法越来越臃肿。代码全都堆在一起,例如:

initARouter(app)
initAutoSize()
initFlipper()
initNetworkConfig()
launch()
configXXX()
ServiceManager.init(app)
initJVerification(app)
initHotFix(app)
initAPM(app)
initBugly(app)
....省略大量初始化代码

当大量的初始化方法这样累加在一起必然会导致启动变慢。这是第一个问题:启动变慢

随着项目的组件化逐步进行,这里就存在了一个新问题。为了业务解耦,每个业务模块需要不同的功能,例如商品模块需要分享,物流定位模块需要地图等。但是这些功能并非全部业务组件都用到的东西,放到主工程Application不合适。这是第二个问题:业务上的解耦

所以我们需要一个启动时,简单、高效的初始化组件的方法,这也是为什么设计这套startup的原因。

二、算法基础

要解决启动变慢的问题,主要有两个思路,延迟加载和异步加载。当然,大部分库都是需要在进入首页之前初始化完成的,否则会产生一些异常。所以我们这里首先解决如何去异步加载的问题。

2.1 : 有向无环图

  • DAG,有向无环图,能够管理任务之间的依赖关系,并调度这些任务,似乎能够满足本节开始的诉求,那么我们先了解下这种数据结构。

  • 顶点:在DAG中,每个节点(sdk1/sdk2/sdk3…)都是一个顶点;

  • :连接每个节点的连接线;

  • 入度:每个节点依赖的节点数,形象来说就是有几根线连接到该节点,例如sdk2的入度是1,sdk5的入度是2。

  • 我们从图中可以看出,是有方向的,但是没有路径再次回到起点,因此就叫做有向无环图

  • 2.2 : 拓扑排序

  • 拓扑排序用于对节点的依赖关系进行排序,主要分为两种:DFS(深度优先遍历)(这也是我们的方案)、BFS(广度优先遍历),如果了解二叉树的话,对于这两种算法应该比较熟悉。

  • 我们就拿这张图来演示,拓扑排序算法的流程:

  • 1:首先找到图中,入度为0的顶点,那么这张图中入度为0的顶点就是task1,然后删除

2:删除之后,再次找到入度为0的顶点,这个时候有两个入度为0的顶点,task2和task3,所以拓扑排序的结果不是唯一的!

3:依次递归,直到删除全部入度为0的顶点,完成拓扑排序

三、技术方案

3.1 : 接口设计

要把我们启动任务拆分为若干个小task去调度启动,首先设计我们的task基类。

interface ITask : ITaskCallBack {/**
* 任务name
*/
val taskName: String/**
* 任务是否完成
*/
val isCompleted: Boolean/**
* 是否要block启动
*/
val needAwait: Boolean/**
* 任务初始化进程
*/
val process: RunProcess/**
* 任务是否可用
*/
val enable: Boolean/**
* 是否在主线程执行
*/
val runOnMainThread: Boolean/**
* 是否需要同意隐私协议后再执行
*/
val needPrivateAgree: Boolean/**
* 依赖的task
*/
fun dependsTaskList(): List<String>/**
* 任务被执行的时候回调
*/
fun run(application: Application)}

并且提供实现Task.class

abstract class Task(override val taskName: String) : ITask {private var completed: AtomicBoolean = AtomicBoolean(false)override val isCompleted: Booleanget() = completed.get()/ ** * 默认运行在主进程 */   override val process: RunProcessget() = RunProcess.MAIN/ ** * 默认阻塞启动 */   override val needAwait: Boolean = true/ ** * 默认运行 */   override val enable: Boolean = true/ ** * 默认运行在子线程 */   override val runOnMainThread: Boolean = false/ ** * 默认需要同意隐私协议后初始化 */   override val needPrivateAgree: Boolean = true/ ** * 用来在前置任务完成之前阻塞当前task */   private val countDownLatch: CountDownLatch by lazy {  CountDownLatch(dependsTaskList().size)} override fun dependsTaskList() = emptyList< String>()override fun runProcessName(): List<String> = emptyList( )/ ** * 当前任务开始等待 直至依赖项全部完成再开始执行 */   internal fun await() {if (dependsTaskList().isNotEmpty( ))countDownLatch.await()}/ ** * 通知某个依赖项完成 */   internal fun countdown() {if (dependsTaskList().isNotEmpty( ))countDownLatch.countDown()}override fun onAdd() {}@CallSuperoverride fun onStart() {completed.set(false)}@CallSuperoverride fun onFinish() {completed.set(true)}override fun toString(): String {return "$taskName(enable=$enable, runOnMainThread=$runOnMainThread, needPrivateAgree=$needPrivateAgree ,dependsTaskList=${dependsTaskList()})"}}

我们提供实现Task类去定义启动任务,注意定义各种参数。

启动配置

 Startup.debug(BuildConfig.DEBUG).privateAgreeCondition { Storage.APP_FIRST_PRIVATE_DIALOG }.start(app)

3.2 : 线程管理设计

首先,我们的任务分为两种模式,运行在主线程和运行在子线程。

  • 既然不能保证每个任务都在主线程中执行,那么就需要对任务做配置
interface ITask {/**
* 是否在主线程执行
*/
val runOnMainThread: Boolean
}

3.2.1 : 线程池

既然要在子线程初始化一些任务,那么我们必须维护一个线程池。

CPU密集型也是指计算密集型,大部分时间用来做计算逻辑判断等CPU动作的程序称为CPU密集型任务。该类型的任务需要进行大量的计算,主要消耗CPU资源。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。占据CPU的时间片过多的话会影响性能,所以这里控制了最大 并发 ,防止主线程的时间片减少

IO密集型任务指任务需要执行大量的IO操作,涉及到网络、磁盘IO操作,对CPU消耗较少。有好多任务其实占用的CPU time非常少,所以使用缓存线程池,基本上来者不拒

这里我们选用的是CPU****密集型任务的线程池。

threadList.forEach {
if (it.isCompleted) {setNotifyChildren(it)} else {threadPoolExecutor.execute(TaskRunnable(application, task = it))}
}
mainList.forEach {
if (it.isCompleted) {setNotifyChildren(it)} else {TaskRunnable(application, task = it).run()}
} 

3.2.2 : 任务分发

有一些task是依赖于别的task的,需要在其他task初始化完成后,才能初始化自己。比如预取任务必须要在网络初始化完成后再执行。

而往往这些任务可能是运行在不同的线程里的,那就有一个大问题,任务之间的执行顺序,或者说分发。比如sdk4是耗时任务,可以放在子线程中执行,但是又依赖sdk2的初始化,这种情况下,我们其实不能保证每个任务都是在主线程中执行的,需要等待某个线程执行完成之后,再执行下个线程,我们先看一个简单的问题:假如有两个线程 AB ,A线程需要三步完成,当执行到第二步的时候,开始执行B线程,这种情况下该怎么处理?

答案是 CountDownLatch。

相信大家对CountDownLatch并不陌生。它的原理就是会阻塞当前并等待所有的线程都执行完成之后,再执行下一个任务。

  • 我们先看task的配置
interface ITask : ITaskCallBack {/**
* 依赖的task
*/
fun dependsTaskList(): List<String>
}

dependsTaskList表示该task要等待这些task初始化完成后再完成,string是依赖task的taskName。通过字符串解耦。

这里简单看一下启动的流程,只看一些关键代码:

step1
private fun executeTasks(application: Application, list: List<Task>) {//。。。//这里是子线程任务
threadList.forEach {threadPoolExecutor.execute(TaskRunnable(application, task = it))}//这里是主线程任务
mainList.forEach {TaskRunnable(application, task = it).run()}
}step2
class TaskRunnable(private val application: Application,private val task: Task
) : Runnable {override fun run() {//  前置任务没有执行完毕的话,等待,执行完毕的话,往下走task.await()//......// 执行任务task.run(application)//.......// 通知子任务,当前任务执行完毕了,相应的计数器要减一。Startup.notify(task)}
}step3
class Task{/*** 用来在前置任务完成之前阻塞当前task*/private val countDownLatch: CountDownLatch by lazy {CountDownLatch(dependsTaskList().size)}/*** 当前任务开始等待 直至依赖项全部完成再开始执行*/internal fun await() {if (dependsTaskList().isNotEmpty())countDownLatch.await()}/*** 通知某个依赖项完成*/internal fun countdown() {if (dependsTaskList().isNotEmpty())countDownLatch.countDown()}
}

当我们Startup启动的时候,首先会对所有的task实例进行拓扑排序,那些被其他Task所依赖且自身不依赖于其他Task的Task必然会先进队列执行,这里保证了我们的task不会被互相阻塞。

同时,我们有一个childrenMap,key是所有被其他task所依赖的task,value是所有依赖于key的task的list。这个map是当被依赖的task执行完成的用于唤醒被阻塞的task。

当我们的task被执行的时候,首先我们会执行Task的await()。如果该task存在依赖task,会阻塞。直到所有的依赖task都执行完毕。而我们是怎么去判断依赖的task都执行完毕的呢? 这里就用到了上面说的childrenMap了。

当每个task执行结束的时候,我们会调用Startup的setNotifyChildren方法,然后去childrenMap中去查找依赖于此task的其他task,调用其conutdown方法。使其计数器countDownLatch减1,而countDownLatch的count就是其依赖的task的size。当其每个依赖的task都执行完发出notifyChildren信号后,阻塞放开,开始执行。

同时上面也说了,经过拓扑排序后,被依赖的task一定先进队列,这样也避免了cpu线程池中被阻塞的线程塞满的情况,也就是互相阻塞,一直等待的情况。

3.2.3 : 提前释放

application初始化中的场景非常复杂,这里存在一种场景,我们的application不需要等待某个task执行完后再结束。也就是某些必要task执行完了,不等待其他task执行完,直接进入页面。

流程如图

这里在task中也有配置

interface ITask{/*** 是否要block启动*/val needAwait: Boolean
}

当然 这个task一定要是运行在子线程的啊。一个任务不能即运行在主线程又不阻塞主线程。

这里需要注意,当你的task的needAwait为false且runOnMainThread为true的时候,会直接报错, 太扯了。

  • 而具体实现看代码
private fun executeTasks(application: Application, list: List<Task>) {if (list.isEmpty()) throw StartupException("tasks不能为空")taskMap.clear()taskChildMap.clear()val sortResult = TaskSortUtil.getSortResult(list, taskMap, taskChildMap)sortResult.forEach {
if (it.runOnMainThread) {mainList.add(it)} else {threadList.add(it)}}countDownLatch = CountDownLatch(1)executeMonitor.recordProjectStart()listeners.forEach {
it.onProjectStart()}
threadList.forEach {
if (it.isCompleted) {notifyChildren(it)} else {threadPoolExecutor.execute(TaskRunnable(application, task = it))}}
mainList.forEach {
if (it.isCompleted) {notifyChildren(it)} else {TaskRunnable(application, task = it).run()}}
countDownLatch?.await()
}internal fun notifyChildren(task: Task) {taskChildMap[task.taskName]?.forEach {
taskMap[it.taskName]?.countdown()}
if (task.needAwait) {finishTask.incrementAndGet()}val taskSize = if (isPrivateAgree) {totalAwaitTaskSize.get()} else {noPrivateTask.sumBy { if (it.needAwait) 1 else 0 }
}if (finishTask.get() == taskSize) {countDownLatch?.countDown()executeMonitor.recordProjectFinish()onGetMonitorRecordCallback?.onGetProjectExecuteTime(executeMonitor.projectCostTime)onGetMonitorRecordCallback?.onGetTaskExecuteRecord(executeMonitor.executeTimeMap)listeners.forEach {
it.onProjectFinish()}
}
}

原理很简单,启动的时候会开启一个countLatch去阻塞住主线程,并当所有需要阻塞主线程的任务完成后放开,并视为启动结束。

3.3 : 业务模块自动注册

伴随着项目的逐步组件化,各个模块之间充分解耦。当我们在各个module去定义好自己的初始化task后,存在一个严重的问题。我们需要在主application里面去感知收集到这些task,并且对之进行拓扑排序。

当然,我们可以去一一依赖并手动创建new出来这些task并add到我们的容器里,但是这样有一些严重的耦合问题,而且会导致一些重复依赖bug。并且这样极不优雅且代码侵入性极强,当task一多,我们要手写几十行的addTask代码,很不优雅😄 。

AutoRegister很好很强大,大家想了解的可以去github上阅读源码,简单直白来说就是五个字 字节码插桩

使用autoRegister方法 ,自定义了一个AutoRegister接口

interface AutoRegister

然后将我们自定义的启动task去实现AutoRegister接口,即可完成自动注册。

  • 3.4 : 进程管理设计

  • 不同启动任务运行的进程可能不一致,这里是通过task的process字段控制。

interface ITask : ITaskCallBack {/**
* 初始化进程
*/
val process: RunProcess
}

sealed class RunProcess(val processNames: List<String>) {abstract fun check(application: Application, processName: String?): Boolean//仅主进程初始化object MAIN : RunProcess(emptyList( )) {override fun check(application: Application, processName: String?): Boolean {return application.packageName == processName}}//所有进程都初始化object ALL : RunProcess(emptyList( )) {override fun check(application: Application, processName: String?): Boolean {return true}}//非进程初始化object OTHER : RunProcess(emptyList( )) {override fun check(application: Application, processName: String?): Boolean {return application.packageName ! = processName}}//指定进程初始化class SPECIAL(processNames: List<String>) : RunProcess(processNames) {override fun check(application: Application, processName: String?): Boolean {return processName in processNames}}
}

顾名思义 启动进程mode有四种,仅主进程初始化,仅非主进程初始化,所有进程都初始化,仅特定进程初始化。

当引入进程概念的时候又新增了一个问题,当前task和依赖的task不在同一个进程初始化,可能会导致异常。这里在自动注册的时候已经判断好了,如果进程有异常会主动抛异常,大家定义task的时候注意就好了。

3.5 : 非自动任务的处理

当前app大都有隐私合规的需求,当我们初次冷启动app的时候不能一股脑全部初始化,有些task需要用户同意了隐私协议后才能初始化。

为了解决隐私合规的问题,在task中我们提供了配置项

interface ITask {/ ** * 是否需要同意隐私协议后再执行 */   val needPrivateAgree: Boolean
}

Startup类中同时也提供了两个方法

object Startup {/*** 判断当前是否同意隐私协议*  @param  condition 返回是否同意隐私协议*/fun privateAgreeCondition(condition: () -> Boolean) = apply {privateCondition = condition}/*** 当用户同意隐私协议后 调用方法进行下一步sdk初始化*/fun notifyPrivateAgree(application: Application) {val currentTaskList = noPrivateTask + needPrivateTasksexecuteTasks(application, currentTaskList)}
}

其中 privateAgreeCondition是配置方法,我们需要在调用start方法之前配置好,当启动时会根据privateCondition的返回值去决定是否去启动那些需要同意协议后才能初始化的task

notifyPrivateAgree是当用户同意协议后去手动调用,去继续初始化下一步需要同意协议的task

四 、上线效果与总结

在app内部新增启动分析页面 把启动过程中的任务和耗时做了一个简单可视化页面,启动流程一目了然。

同时在数据平台观察最新的上报数据

可以看到启动过程中 Application的onCreate方法耗时下降接近一倍,大幅提升用户启动时的体验,同时方案设计也保留了充分的拓展性,后续新增启动项时也可以快速高效的接入这套框架,保证启动效果不劣化。

启动优化一直都是Android性能优化中最重要的一环,简称APP的门面担当。在app上线后,与用户接触的第一个功能就是APP的启动,它的启动时长直接就决定了用户后续是否会继续使用。在APP性能优化中除了启动优化很重要以外,其他的优化技术也很重要,像内存优化、卡顿优化、网络优化、安全优化……等等

为了帮助到大家更好的全面清晰的掌握好性能优化,准备了相关的学习路线以及核心笔记(还该底层逻辑):https://qr18.cn/FVlo89 大家可以进行参考学习:

性能优化核心笔记:https://qr18.cn/FVlo89

启动优化

内存优化

UI优化

网络优化

Bitmap优化与图片压缩优化https://qr18.cn/FVlo89

多线程并发优化与数据传输效率优化


体积包优化

《Android 性能监控框架》:https://qr18.cn/FVlo89

《Android Framework学习手册》:https://qr18.cn/AQpN4J

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战

相关文章:

Android启动优化实践

作者&#xff1a;95分技术 启动优化是Android优化老生常谈的问题了。众所周知&#xff0c;android的启动是指用户从点击 icon 到看到首帧可交互的流程。 而启动流程 粗略的可以分为以下几个阶段 fork创建出一个新的进程创建初始化Application类、创建四大组件等 走Applicatio…...

ROS:通信机制实操

目录 ROS&#xff1a;通信机制一、话题发布实操1.1需求1.2分析1.3实现流程1.4实现代码1.4.1C版1.4.2Python版 1.5执行 二、话题订阅实操2.1需求2.2分析2.3流程2.4实现代码2.4.1启动无辜GUI与键盘控制节点2.4.2C版 ROS&#xff1a;通信机制 一、话题发布实操 1.1需求 编码实现…...

C/C++内存管理(内存分布、动态内存分配、动态内存分配与释放、内存泄漏等)

喵~ 内存之5大区&#xff08;栈区、堆区、静态区、常量区、代码区&#xff09;C/C中各自的内存分配操作符内存泄露?内存泄漏检测方法 内存之5大区&#xff08;栈区、堆区、静态区、常量区、代码区&#xff09; 1、栈区&#xff08;stack&#xff09;&#xff1a;由编译器自动分…...

【云原生】软件架构的演进以及各个架构的优缺点

文章目录 1. 什么是软件架构?2. 单机架构3. 应用数据分离架构4. 应用服务集群架构5. 读写分离架构6. 冷热分离架构7.垂直分库架构8. 微服务架构9. 容器编排架构10. 小结 1. 什么是软件架构? 软件架构是指在设计和构建软件系统时&#xff0c;对系统的组织结构、组件、模块、接…...

力扣刷题笔记——二叉树

首先定义二叉树节点的结构体 struct TreeNode{TreeNode* left;TreeNode* right;int val;TreeNode():val(0),left(nullptr),right(nullptr){}TreeNode(int val):val(val),left(nullptr),right(nullptr){}TreeNode(int val,TreeNode* l,TreeNode* R):val(val),left(l),right(R){…...

【华为OD机试】工号不够用了怎么办?(python, java, c++, js)

工号不够用了怎么办? 前言:本专栏将持续更新华为OD机试题目,并进行详细的分析与解答,包含完整的代码实现,希望可以帮助到正在努力的你。关于OD机试流程、面经、面试指导等,如有任何疑问,欢迎联系我,wechat:steven_moda;email:nansun0903@163.com;备注:CSDN。 题目…...

【leetcode】198. 打家劫舍

你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯入&#xff0c;系统会自动报警。 给定一个代表每个房屋存放金额的非…...

【react全家桶学习】react的 (新/旧) 生命周期(重点)

目录 生命周期&#xff08;旧&#xff09; 挂载时的生命周期 constructor&#xff08;props&#xff09; componentWillMount&#xff08;&#xff09;-------------新生命周期已替换 render&#xff08;&#xff09; componentDidMount&#xff08;&#xff09;--- 组件…...

Gradio私网和公网的使用

Gradio私网问题 如果部署的服务器只有私有地址&#xff0c;那么无法直接从外部网络中的其他计算机访问该服务器和其中运行的 Gradio 应用程序。在这种情况下&#xff0c;你可以考虑使用端口转发技术&#xff0c;将服务器的私有地址映射到一定的公开地址上&#xff0c;从而可以…...

ant design vue 配置菜单外部打开

实现如下 菜单配置 前端项目地址&#xff1a;http://localhost:3000 菜单路径&#xff1a;dataCenter/HealthData 打开方式&#xff1a;外部 在项目中src-->config-->router.config.js文件 将需要再外部打开的菜单地址进行如下配置 菜单地址&#xff1a;/dataCenter/Hea…...

YOLOv5/v7 添加注意力机制,30多种模块分析⑦,CCN模块,GAMAttention模块

目录 一、注意力机制介绍1、什么是注意力机制&#xff1f;2、注意力机制的分类3、注意力机制的核心 二、CCN模块1、CCN模块的原理2、实验结果3、应用示例 三、GAMAttention模块1、GAMAttention模块的原理2、实验结果3、应用示例 大家好&#xff0c;我是哪吒。 &#x1f3c6;本…...

IDEA下Logback.xml自动提示功能配置

首先打开logback的配置文件&#xff0c;在configuration标签中加入xsd的配置 <configuration xmlns"http://ch.qos.logback/xml/ns/logback"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://ch.qos.logback/xml…...

CUDA编程模型系列八(原子操作 / 规约 / 向量元素求和)

本系列视频目的是帮助开发者们一步步地学会利用CUDA编程模型加速GPU应用, 我们的口号是: 让GPU飞起来 本期我介绍了cuda 当中规约算法的一种情况, 也是小何尚职业生涯中的第一道面试题, 计算数组中所有元素的和. CUDA编程模型系列八(原子操作 / 规约 / 向量元素求和) #include…...

go语言系列基础教程总结(4)

1、goroutine和channel 每执行一次go func()就创建一个 goroutine&#xff0c;包含要执行的函数和上下文信息。 goroutine 是Go程序并发的执行体&#xff0c;channel是它们之间的沟通连接通道。 var ch1 chan int. //声明一个整型的通道 2、channel 常用操作 //定义一个…...

网络基础一:网络协议初识与网络传输基本流程

目录 网络协议认识“协议”网络协议初识协议分层OSI七层模型&#xff08;理论模型&#xff09;TCP/IP五层(或四层)模型&#xff08;工程实现模型&#xff09; 网络中的地址管理MAC地址IP地址 网络传输基本流程路由的本质 数据包封装和分用网络协议需要解决的问题 网络协议 计算…...

Mysql找出执行慢的SQL【慢查询日志使用与分析】

分析慢SQL的步骤 慢查询的开启并捕获&#xff1a;开启慢查询日志&#xff0c;设置阈值&#xff0c;比如超过5秒钟的就是慢SQL&#xff0c;至少跑1天&#xff0c;看看生产的慢SQL情况&#xff0c;并将它抓取出来explain 慢SQL分析show Profile。&#xff08;比explain还要详细…...

设计模式3:单例模式:JMM与volatile和synchronized的关系

本文目录 JMM简介Java 内部内存模型(The Internal Java Memory Model)硬件内存架构(Hardware Memory Architecture)弥合 Java 内存模型和硬件内存架构之间的差距(Bridging The Gap Between The Java Memory Model And The Hardware Memory Architecture)1.共享对象的可见性2.竞…...

一个简单的OPC UA/ModbusTCP 网关(Python)

使用我前面几篇博文的内容&#xff0c;能够使用Python编写一个最简单的OPC UA /ModbusTCP网关。 从这个程序可以看出&#xff1a; 应用OPC UA 并不难&#xff0c;现在我们就可以应用到工程应用中&#xff0c;甚至DIY项目也可以。不必采用复杂的工具软件。使用Python 来构建工…...

线性代数行列式的几何含义

行列式可以看做是一系列列向量的排列&#xff0c;并且每个列向量的分量可以理解为其对应标准正交基下的坐标。 行列式有非常直观的几何意义&#xff0c;例如&#xff1a; 二维行列式按列向量排列依次是 a \mathbf{a} a和 b \mathbf{b} b&#xff0c;可以表示 a \mathbf{a} a和…...

python用flask将视频显示在网页上

注意我们的return返回值必须是以下之一&#xff0c;否则会报错 from flask import Flask, render_template, Response import cv2app Flask(__name__)app.route(/) def index():return render_template(index.html)def gen(camera):while True:success, image camera.read(…...

国防科技大学计算机基础课程笔记02信息编码

1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制&#xff0c;因此这个了16进制的数据既可以翻译成为这个机器码&#xff0c;也可以翻译成为这个国标码&#xff0c;所以这个时候很容易会出现这个歧义的情况&#xff1b; 因此&#xff0c;我们的这个国…...

C++初阶-list的底层

目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...

进程地址空间(比特课总结)

一、进程地址空间 1. 环境变量 1 &#xff09;⽤户级环境变量与系统级环境变量 全局属性&#xff1a;环境变量具有全局属性&#xff0c;会被⼦进程继承。例如当bash启动⼦进程时&#xff0c;环 境变量会⾃动传递给⼦进程。 本地变量限制&#xff1a;本地变量只在当前进程(ba…...

C++使用 new 来创建动态数组

问题&#xff1a; 不能使用变量定义数组大小 原因&#xff1a; 这是因为数组在内存中是连续存储的&#xff0c;编译器需要在编译阶段就确定数组的大小&#xff0c;以便正确地分配内存空间。如果允许使用变量来定义数组的大小&#xff0c;那么编译器就无法在编译时确定数组的大…...

【从零学习JVM|第三篇】类的生命周期(高频面试题)

前言&#xff1a; 在Java编程中&#xff0c;类的生命周期是指类从被加载到内存中开始&#xff0c;到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。本文会深入探寻类的生命周期&#xff0c;让读者对此有深刻印象。 目录 ​…...

Linux 中如何提取压缩文件 ?

Linux 是一种流行的开源操作系统&#xff0c;它提供了许多工具来管理、压缩和解压缩文件。压缩文件有助于节省存储空间&#xff0c;使数据传输更快。本指南将向您展示如何在 Linux 中提取不同类型的压缩文件。 1. Unpacking ZIP Files ZIP 文件是非常常见的&#xff0c;要在 …...

JavaScript 数据类型详解

JavaScript 数据类型详解 JavaScript 数据类型分为 原始类型&#xff08;Primitive&#xff09; 和 对象类型&#xff08;Object&#xff09; 两大类&#xff0c;共 8 种&#xff08;ES11&#xff09;&#xff1a; 一、原始类型&#xff08;7种&#xff09; 1. undefined 定…...

省略号和可变参数模板

本文主要介绍如何展开可变参数的参数包 1.C语言的va_list展开可变参数 #include <iostream> #include <cstdarg>void printNumbers(int count, ...) {// 声明va_list类型的变量va_list args;// 使用va_start将可变参数写入变量argsva_start(args, count);for (in…...

FFmpeg:Windows系统小白安装及其使用

一、安装 1.访问官网 Download FFmpeg 2.点击版本目录 3.选择版本点击安装 注意这里选择的是【release buids】&#xff0c;注意左上角标题 例如我安装在目录 F:\FFmpeg 4.解压 5.添加环境变量 把你解压后的bin目录&#xff08;即exe所在文件夹&#xff09;加入系统变量…...

mac:大模型系列测试

0 MAC 前几天经过学生优惠以及国补17K入手了mac studio,然后这两天亲自测试其模型行运用能力如何&#xff0c;是否支持微调、推理速度等能力。下面进入正文。 1 mac 与 unsloth 按照下面的进行安装以及测试&#xff0c;是可以跑通文章里面的代码。训练速度也是很快的。 注意…...