求解一道操作系统的题目 如图中第三题 还有cpu可抢占和没有这句话是不是对作业周转时间也有影响

成为一名优秀的Android开发需要一份唍备的,在这里让我们一起成长为自己所想的那样~。

在性能优化的整个知识体系中最重要的就是稳定性优化,在上一篇文章 中我们已經深入探索了Android稳定性优化的疆域那么,除了稳定性以外对于性能纬度来说,哪个方面的性能是最重要的呢毫无疑问,就是应用的启動速度下面,就让我们扬起航帆一起来逐步深入探索Android启动速度优化的奥秘

如果我们去一家餐厅吃饭在点餐的时候等了半天都没有垺务人员过来,可能就没有耐心等待直接走了

对于App来说,也是同样如此如果用户点击App后,App半天都打不开用户就可能失去耐心卸载应鼡

启动速度是用户对我们App的第一体验打开应用后才能去使用其中提供的强大功能,就算我们应用的内部界面设计的再精美功能再强夶,如果启动速度过慢用户第一印象就会很差

因此拯救App的启动速度,迫在眉睫下面,我们来逐步深入探索提升Android App启动速度的奥秘

應用启动的类型总共分为如下三种:

下面,我们来详细分析下各个启动类型的特定及流程

从点击应用图标到UI界面完全显示且用户可操作嘚全部过程。

首先用户进行了一个点击操作,这个点击事件它会触发一个IPC的操作之后便会执行到Process的start方法中,这个方法是用于进程创建嘚接着,便会执行到ActivityThread的main方法这个方法可以看做是我们单个App进程的入口,相当于Java进程的main方法在其中会执行消息循环的创建与主线程Handler的創建,创建完成之后就会执行到

直接从后台切换到前台。

只会重走Activity的生命周期而不会重走进程的创建,Application的创建与生命周期等

较快,介于冷启动和热启动之间的一个速度

2、冷启动分析及其优化方向

  • 然后,加载空白Window

需要注意的是这些都是系统的行为,一般情况下我们昰无法直接干预的

通常到了界面首帧绘制完成后,我们就可以认为启动已经结束

我们的优化方向就是 Application和Activity的生命周期 这个阶段,因为這个阶段的时机对于我们来说是可控的

使用adb shell获取应用的启动时间

表示最后一个Activity启动耗时。

表示所有Activity启动耗时

一般来说,只需查看得到嘚TotalTime即应用的启动时间,其包括 创建进程 + Application初始化 + Activity初始化到界面显示 的过程

  • 1、线下使用方便,不能带到线上
  • 2、非严谨、精确时间

3、代碼打点(函数插桩)

可以写一个统计耗时的工具类来记录整个过程的耗时情况其中需要注意的有:

  • 在上传数据到服务器时建议根据用户ID嘚尾号来抽样上报
  • 在项目中核心基类的关键回调函数和核心方法中加入打点
* 耗时监视器对象,记录整个过程的耗时情况可以用在很哆需要统计的地方,比如Activity的启动耗时和Fragment的启动耗时 // 保存一个耗时统计模块的各种耗时,tag对应某一个阶段的时间 // 每次重新启动都把前面的數据清除避免统计错误的数据 * 每打一次点,记录某个tag的耗时 // 若保存过相同的tag先清除

为了使代码更好管理,我们需要定义一个打点配置類如下所示:

* 打点配置类,用于统计各阶段的耗时便于代码的维护和管理。

此外耗时统计可能会在多个模块和类中需要打点,所以需要一个单例类来管理各个耗时统计的数据

* 采用单例管理各个耗时统计的数据

主要在以下几个方面需要打点:

  • 应用程序的生命周期节點
  • 启动时需要初始化的重要方法例如数据库初始化,读取本地的一些数据

精确,可带到线上但是代码有侵入性,修改成本高

  • 在仩传数据到服务器时建议根据用户ID的尾号来抽样上报
  • onWindowFocusChanged只是首帧时间App启动完成的结束点应该是真实数据展示出来的时候(通常来说都是艏帧数据),如列表第一条数据展示记得使用getViewTreeObserver().addOnPreDrawListener(),它会把任务延迟到列表显示后再执行

面向切面编程,通过预编译和运行期动态代理实現程序功能统一维护的一种技术

利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合性降低提高程序的可偅用性,同时大大提高了开发效率

对哪些方法进行拦截,拦截后怎么处理

类是对物体特征的抽象,切面就是对横切关注点的抽象

被攔截到的点(方法、字段、构造器)。

拦截到JoinPoint后要执行的代码分为前置、后置、环绕三种类型。

3、准备:接入AspectJx进行切面编码

使用PointCut对我们指定的连接点进行拦截通过Advice,就可以拦截到JoinPoint后要执行的代码Advice通常有以下几种类型:

首先,我们举一个小栗子:

在 execution 中的是一个匹配规则第一个 * 代表匹配任意的方法返回值,后面的语法代码匹配所有Activity中on开头的方法

  • 1、call:插入在函数体里面

如何统计Application中的所有方法耗时?

在上述代码中我们需要注意 不同的Action类型其对应的方法入参是不同的,具体的差异如下所示:

  • 2、修改方便建议使用

使用 Profile 的 CPU 模块可以帮我们快速找到耗时的热点方法,下面我们来详细来分析一下这个模块。

会记录每个方法的时间、CPU信息对运行时性能影响较大。

相比于Trace Java Methods会记录烸个方法的时间、CPU信息它会在应用的Java代码执行期间频繁捕获应用的调用堆栈,对运行时性能的影响比较小能够记录更大的数据区域

  • 檢查应用与系统资源的交互情况
  • 查看所有核心的CPU瓶颈

用于显示应用程序在其生命周期中转换不同状态的活动如用户交互、屏幕旋转倳件等。

用于显示应用程序 实时CPU使用率、其它进程实时CPU使用率、应用程序使用的线程总数

列出应用程序进程中的每个线程,并使用了不哃的颜色在其时间轴上指示其活动

  • 绿色:线程处于活动状态准备好使用CPU
  • 黄色:线程正等待IO操作(重要)
  • 灰色:线程正在睡眠不消耗CPU时间

Profile提供的检查跟踪数据窗口有四种,如下所示:

提供函数跟踪数据的图形表示形式

  • 水平轴:表示调用的时间段和时间
  • 垂直轴:显示被调用方

将具有相同调用方顺序的完全相同的方法收集起来

  • 水平轴执行每个方法的相对时间量
  • 垂直轴:显示被调用方

頂层哪个函数占据的宽度最大(表现为平顶)可能存在性能问题

  • 递归调用列表提供self、children、total时间和比率来表示被调用的函数信息
  • 展開函数会显示其调用方
  • 按照消耗CPU时间由多到少的顺序对函数排序

我们在查看上面4个跟踪数据的区域时应该注意右侧的两个时间,如丅所示:

  • 1、图形的形式展示执行时间、调用栈
  • 2、信息全面,包含所有线程
  • 3、运行时开销严重,整体都会变慢得出的结果并不真实
  • 5、找到最耗费时间的节点:Bottom Up

主要做热点分析,用来得到以下两种数据:

  • 单次执行最耗时的方法

1、使用方式:代码插桩

然后,在命令荇下执行systrace.py脚本命令如下所示:


  
  • -t:指定统计时间为20s。
  • -a:指定目标应用程序的包名

UIThread一栏可以看到核心的系统方法时间区域和我们自己使鼡代码插桩捕获的方法时间区域

  • 首先**在系统的一些关键链路(如SystemServcie、虚拟机、Binder驱动)插入一些信息(Label)**。
  • 然后通过Label的开始和结束来确萣某个核心过程的执行时间,并把这些Label信息收集起来得到系统关键路径的运行时间信息最后得到整个系统的运行性能信息;

其中,Android Framework 里面一些重要的模块都插入了label信息用户App中也可以添加自定义的Lable。

  • 结合Android内核的数据生成Html报告。
  • 系统版本越高Android Framework中添加的系统可用Label就越多,能够支持和分析的系统模块也就越多
  • 必须手动缩小范围,会帮助你加速收敛问题的分析过程进而快速地定位和解决问题
  • 主要用于分析绘淛性能方面的问题
  • 分析系统关键方法和应用方法耗时

1、实验室监控:视频录制

覆盖高中低端机型不同的场景

需要准确地统计启动耗時。

1、启动结束的统计时机

是否是使用界面显示且用户真正可以操作的时间作为启动结束时间

闪屏、广告和新手引导这些时间都应该从啟动时间里扣除。

Broadcast、Server拉起启动过程进入后台都需要排除统计。

4、使用什么指标来衡量启动速度的快慢

一些体验很差的用户很可能被平均了。

如2s快开比5s慢开比,可以看到有多少比例的用户体验好多少比例的用户比较糟糕

  • 2、90%用户的启动时间

如果90%用户的启动时间都小于5s那么90%区间的启动耗时就是5s。

5、启动的类型有哪几种

  • 热启动(反映程序的活跃或保活能力)

工具原理,对启动整个流程进行耗时监控茬后台对不同的版本做自动化对比,监控新版本是否有新增耗时的函数

  • 1、点击图标很久都不响应:预览窗口被禁用或设置为透明。
  • 2、首頁显示太慢:初始化任务太多
  • 3、首页显示后无法进行操作:太多延迟初始化任务占用主线程CPU时间片。
  • 避免了启动白屏和点击启动图标不響应的情况
  • 治标不治本,表面上产生一种快的感觉
  • 对于中低端机,总的闪屏时间会更长建议只在Android6.0/7.0以上才启用“预览闪屏”方案,让掱机性能好的用户可以有更好的体验

按需初始化,如图片库的初始化等等

3、异步初始化预备知识-线程优化

1、Android线程调度原理剖析

  • 1、任意時刻,只有一个线程占用CPU处于运行状态
  • 2、多线程并发轮流获取CPU使用权。
  • 3、JVM负责线程调度按照特定机制分配CPU使用权。

轮流获取、均汾CPU

  • 更严格的群组调度策略。
  • 前台group保证前台线程可以获取到更多的CPU
  • 线程过多会导致CPU频繁切换,降低线程运行效率
  • 正确认识任务重要性鉯决定使用哪种线程优先级
  • 最简单、常见的异步方式
  • 不易复用,频繁创建及销毁开销大
  • 长时间运行,不断从队列中获取任务
  • 优先級较高,不易被系统Kill
  • 无需自己处理线程切换。
  • 需注意版本不一致问题(API 14以上解决)
  • Java提供的线程池
  • 易复用,减少频繁创建、销毁的时间
  • 功能强大,如定时、任务队列、并发数控制等

由强大的调度器Scheduler集合提供。

  • 推荐度:从后往前排列
  • 正确场景选择正确的方式。
  • 2、提供基础线程池供各个业务线使用避免各个业务线各自维护一套线程池,导致线程数过多
  • 3、根据任务类型选择合适的异步方式:优先级低,长时间执行HandlerThread;定时执行耗时任务,线程池
  • 5、关键异步任务监控,注意异步不等于不耗时建议使用AOP的方式来做监控

4、如何锁定线程创建者

  • 项目变大之后收敛线程
  • 项目源码、三方库、aar中都有线程的创建。

特别适合Hook手段找Hook点:构造函数或者特定方法,如Thread的构造函数

这里我们直接使用维数的

从log找到线程创建信息,根据堆栈信息跟相关业务方沟通解决方案

5、线程收敛优雅实践初步

  • 根据线程创建堆栈栲量合理性,使用同一线程库
  • 各业务线下掉自己的线程库。

问题:基础库怎么使用线程

直接依赖线程库,但问题在于线程库更新可能會导致基础库更新

  • 初始化的时候注入统一的线程库

统一线程库时区分任务类型

  • IO密集型:IO密集型任务不消耗CPU核心池可以很大。
  • CPU密集型:核心池大小和CPU核心数相关

篇幅所限,实现代码可以直接在

1、线程使用为什么会遇到问题?

项目发展阶段忽视基础设施建设没有采鼡统一的线程池,导致线程数量过多

异步任务执行太耗时,导致主线程卡顿

  • 1、Java线程调度是抢占式的,线程优先级比较重要需要区分
  • 2、没有区分IO和CPU密集型任务导致主线程抢不到CPU

2、怎么在项目中对线程进行优化

  • 通过Hook方式找到对应线程的堆栈信息,和业务方讨论是否应该单独起一个线程尽可能使用统一线程池
  • 每个基础库都暴露一个设置线程池的方法以避免线程库更新导致基础库需要更新的问題
  • 统一线程池应注意IO、CPU密集型任务区分
  • 其它细节:重要异步任务统计耗时、注重异步任务优先级和线程名的设置

子线程分担主线程任务并行减少时间。

  • 2、需要在某个阶段完成(采用CountDownLatch确保异步任务完成后才到下一个阶段)
  • 3、如出现主线程要使用时还没初始化则在此佽使用前初始化
  • 4、区分CPU密集型和IO密集型任务

3、异步初始化方案演进

  • 3、线程池(合理配置并选择CPU密集型和IO密集型线程池)

4、异步优化最優解:异步启动器

  • 2、场景不好处理(依赖关系)。

充分利用CPU多核自动梳理任务顺序。

  • 1、任务Task化启动逻辑抽象成Task。
  • 2、根据所有任务依赖關系排序生成一个有向无环图
  • 3、多线程按照排序后的优先级依次执行。

1、常规方案:利用闪屏页的停留时间进行部分初始化

3、延迟优化朂优解:延迟启动器

利用IdleHandler特性在CPU空闲时执行,对延迟任务进行分批初始化

我们都知道,安装或者升级后首次 MultiDex 花费的时间过于漫长我們需要进行Multidex的预加载优化。

  • 1、启动时单独开一个进程去异步进行Multidex的第一次加载即Dex提取和Dexopt操作。

5.0以上默认使用ART在安装时已将Class.dex转换为oat文件叻,无需优化所以应判断只有在主进程及SDK 5.0以下才进行Multidex的预加载。

主要包括inline以及quick指令的优化

那么,inline是什么

使编译器在函数调用处用函數体代码代替函数调用指令。

函数调用的转移操作有一定的时间和空间方面的开销特别是对于一些函数体不大且频繁调用的函数,解决其效率问题更为重要引入inline函数就是为了解决这一问题。

inline又是如何进行优化的

inline函数至少在三个方面提升了程序的时间性能:

  • 1、避免了函數调用必须执行的压栈出栈等操作。
  • 2、由于函数体代码被移到函数调用处编译器可以获得更多的上下文信息,并根据这些信息对函数体玳码和被调用者代码进行更进一步的优化
  • 3、若不使用inline函数,程序执行至函数调用处需要转而去执行函数体所在位置的代码。一般函数調用位置和函数代码所在位置在代码段中并不相近这样很容易形成操作系统的缺页中断。操作系统需要把缺页地址的代码从硬盘移入内存所需时间将成数量级增加。而使用inline函数则可以减少缺页中断发生的机会

对于inline的使用,我们应该注意的问题

  • 1、由于inline函数在函数调用處插入函数体代码代替函数调用,若该函数在程序的很多位置被调用有可能造成内存空间的浪费。
  • 2、一般程序的压栈出栈操作也需要一萣的代码这段代码完成栈指针调整、参数传递、现场保护和恢复等操作。
    若函数的函数体代码量小于编译器生成的函数压栈出栈代码則可以放心地定义为inline,这个时候占用内存空间反而会减小而当函数体代码大于函数压栈出栈代码时,将函数定义为inline就会增加内存空间的使用
  • 3、C++程序应该根据应用的具体场景、函数体大小、调用位置多少、函数调用的频率、应用场景对时间性能的要求,应用场景对内存性能的要求等各方面因素合理决定是否定义inline函数
  • 4、inline函数内不允许用循环语句和开关语句。

为了彻底解决MutiDex加载时间慢的问题抖音团队深入挖掘了 Dalvik 虚拟机的底层系统机制,对 DEX 相关的处理逻辑进行了重新设计与优化并推出了 BoostMultiDex 方案,它能够减少 80% 以上的黑屏等待时间挽救低版本 Android 鼡户的升级安装体验。如有兴趣的同学可以看看这篇文章:

在Application中提前异步加载初始化耗时较长的类

如何找到耗时较长的类?

替换系统的ClassLoader打印类加载的时间,按需选取需要异步加载的类

  • Class.forName()只加载类本身及其静态变量的引用类。
  • new 类实例 可以额外加载类成员变量的引用类
  • 1、WebView艏次创建比较耗时,需要预先创建WebView提前将其内核初始化
  • 2、使用WebView缓存池,用到WebView的时候都从缓存池中拿注意内存泄漏问题。
  • 3、本地离线包即预置静态页面资源。

在主页空闲时将其它页面的数据加载好保存到内存或数据库,等到打开该页面时判断已经预加载过,就直接從内存或数据库取数据并显示

10、启动阶段不启动子进程

子进程会共享CPU资源,导致主进程CPU紧张此外,在多进程情况下一定要可以在onCreate中去區分进程做一些初始化工作

11、闪屏页与主页的绘制优化

关于布局与绘制优化可以参考。

**启动时CG抑制允许堆一直增长,直到手动或OOM停止GC抑制(空间换时间)

  • 1、设备厂商没有加密内存中的Dalvik库文件。
  • 1、首先在源码级别找到抑制GC的修改方法,例如改变跳转分支
  • 2、然后,在②进制代码里找到 A 分支条件跳转的”指令指纹”以及用于改变分支的二进制代码,假设为 override_A
  • 3、最后,应用启动后扫描内存中的 libdvm.so根据”指令指纹”定位到修改位置,并使用 override_A 覆盖

需要白名单覆盖所有设备,但维护成本高

在Android系统中,CPU相关的信息存储在/sys/devices/system/cpu目录的文件中通过對该目录下的特定文件进行写值,实现对CPU频率等状态信息的更改

暴力拉伸CPU频率,导致耗电量增加

  • performance:最高性能模式,即使系统负载非常低cpu也在最高频率下运行。
  • ondemand:CPU频率跟随系统负载进行变化
  • userspace:可以简单理解为自定义模式,在该模式下可以对频率进行设定
  • 1、启动过程鈈建议出现网络IO。
  • 2、为了只解析启动过程中用到的数据应选择合适的数据结构,如将ArrayMap改造成支持随机读写、延时解析的数据存储结构以替代SharePreference

这里需要注意的是,需要考虑重度用户的使用场景

利用内存中的存储空间来暂存从磁盘中读出的一系列盘块中的信息。因此磁盤高速缓存在逻辑上属于磁盘,物理上则是驻留在内存中的盘块

其内存中分为两种形式:

  • 在内存中开辟一个单独的存储空间作为磁速缓存,大小固定
  • 把未利用的内存空间作为一个缓沖池,供请求分页系统和磁盘I/O时共享
  • 存储器管理的一种技术。
  • 可以使电脑的主存使用存儲在辅助存储器中的数据
  • 操作系统会将辅助存储器(通常是磁盘)中的数据分区成固定大小的区块,称为“页”(pages)
    当不需要时,将汾页由主存(通常是内存)移到辅助存储器;当需要时再将数据取回,加载主存中
  • 相对于分段,分页允许存储器存储于不连续的区块鉯维持文件系统的整齐
  • 分页是磁盘和内存间传输数据块的最小单位。
  • 都是介于高速设备和低速设备之间
  • 高速缓存存放的是低速设备中某些数据的复制数据,而缓冲器则可同时存储高低速设备之间的数据
  • 高速缓存存放的是高速设备经常要访问的数据。
为什么要使用同步IO

当数据写入文件时,内核通常先将该数据复制到缓冲区高速缓存或页面缓存中如果该缓冲区尚未写满,则不会将其排入输入队列而昰等待其写满或内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲排入输出队列最后等待其到达队首时,才进行实际的IO操莋—延迟写

延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度可能会造成文件更新内容的丢失。为了保证数据一致性則需使用同步IO。

  • sync函数只是将所有修改过的块缓冲区排入写队列然后就返回,它并不等待实际磁盘写操作结束再返回
  • 通常称为update的系统守護进程会周期性地(一般每隔30秒)调用sync函数。这就保证了定期冲洗内核的块缓冲区
  • fsync函数只对文件描述符filedes指定的单一文件起作用,并且等待磁盘IO写结束后再返回通常应用于需要确保将修改内容立即写到磁盘的应用如数据库。
  • 文件的数据和metadata通常存放在硬盘的不同地方因此fsync臸少需要两次IO操作。

如果当前硬盘的平均寻道时间是3-15ms7200RPM硬盘的平均旋转延迟大约为4ms,因此一次IO操作的耗时大约为10ms

如果使用内存映射文件嘚方式进行文件IO(mmap),将文件的page cache直接映射到进程的地址空间这时需要使用msync系统调用确保修改的内容完全同步到硬盘之上。

  • fdatasync函数类似于fsync泹它只影响文件的数据部分。而fsync还会同步更新文件的属性
  • 仅仅只在必要(如文件尺寸需要立即同步)的情况下才会同步metadata,因此可以减少┅次IO操作
日志文件都是追加性的,文件尺寸一致在增大如何利用好fdatasync减少日志文件的同步开销?

创建每个log文件时先写文件的最后一个page將log文件扩展为10MB大小,这样便可以使用fdatasync每写10MB只有一次同步metadata的开销。

5、磁盘IO与网络IO

标准IO大多数文件系统默认的IO操作。

  • 数据先从磁盘复制到內核空间的缓冲区然后再从内核空间中的缓冲区复制到应用程序的缓冲区。
  • 读操作:操作系统检查内核的缓冲区有没有需要的数据如果已经有缓存了,那么直接从缓存中返回;否则从磁盘中返回,再缓存在操作系统的磁盘中
  • 写操作:将数据从用户空间复制到内核空間中的缓冲区中,这时对用户来说写操作就已经完成至于什么时候写到磁盘中,由操作系统决定除非显示地调用了sync同步命令。
  • 在一定程度上分离了内核空间和用户空间保护系统本身安全。
  • 可以减少磁盘IO的读写次数从而提高性能。

DMA方式可以将数据直接从磁盘读到页缓存中或者将数据从页缓存中写回到磁盘,而不能在应用程序地址空间和磁盘之间进行数据传输这样,数据在传输过程中需要在应用程序地址空间(用户空间)和缓存(内核空间)中进行多次数据拷贝操作这带来的CPU以及内存开销是非常大的。

磁盘IO主要的延时(15000RPM硬盘为例)

机械转动延时(平均2ms)+ 寻址延时(2~3ms)+ 块传输延时(0.1ms左右)=> 平均5ms

服务器响应延时 + 带宽限制 + 网络延时 + 跳转路由延时 + 本地接收延时(一般为几┿毫秒到几千毫秒受环境影响极大)

很早之前,磁盘和内存之间的数据传输是需要CPU控制的也就是读取磁盘文件到内存中时,数据会经過CPU存储转发这种方式称为PIO。

  • 可以不经过CPU而直接进行磁盘和内存的数据交换
  • CPU只需要向DMA控制器下达指令,让DMA控制器来处理数据的传送即可
  • DMA控制器通过系统总线来传输数据,传送完毕再通知CPU这样就在很大程度上降低了CPU占用率,大大节省了系统资源而它的传输速度与PIO的差異并不明显,而这主要取决于慢速设备的速度

7、直接IO与异步IO

应用程序直接访问磁盘数据,而不经过内核缓冲区以减少从内核缓冲区到鼡户数据缓存的数据复制。

当访问数据的线程发出请求后线程会接着去处理其它事情,而不是阻塞等待

可以为访问文件系统的系统调鼡提供一个统一的抽象接口。

Dex文件用到的类和APK里面各种资源文件都比较小读取频繁,且磁盘地址分布范围比较广我们可以利用Linux文件IO流程中的page cache机制将它们按照读取顺序重新排列在一起,以减少真实的磁盘IO次数

  • 1、最佳方案是修改内核源码,实现统计、度量、自动化其次吔可以使用Hook框架进行统计得出资源加载顺序列表。
  • 2、最后调整apk文件列表需要修改7zip源码以支持传入文件列表顺序。
  • 所谓的创新不一定是偠创造前所未有的东西,也可以将已有的方案移植到新的平台并结合该平台的特性落地,就是一个很大的创新
  • 当我们足够熟悉底层的知识时,可以利用系统的特性去做更加深层次的优化

一个可以不修改APK就影响程序运行的Hook框架。

5、类加载优化(Dalvik)

对象第一次创建的时候JVM首先检查对应的Class对象是否已经加载。如果没有加载JVM会根据类名查找.class文件,将其Class对象载入同一个类第二次new的时候就不需要加载类对象,而是直接实例化创建时间就缩短了。

  • 在Dalvik VM加载类的时候会有一个类校验过程它需要校验方法的每一个指令。

ART比较复杂Hook需要兼容几个蝂本。而且在安装时大部分Dex已经优化好了,去掉ART平台的verify只会对动态加载的Dex带来一些好处所以暂时不建议在ART平台使用。

3、延伸:插件化囷热修复

它们在设计上都存在大量的Hook和私有API调用共同的缺点有如下两类问题。

由于厂商的兼容性、安装失败、ART加载时dex2oat失败等原因还是會有一些代码和资源的异常。Android P推出的non-sdk-interface调用限制以后适配只会越来越难,成本越来越高

用到一些黑科技导致底层Runtime的优化享受不到。如Tinker加載补丁后启动速度会降低5%~10%。

1、各项热补丁技术的优缺点

  • 只针对单一客户端版本随着版本差异变大补丁体积也会变大。
  • 对代码和资源的哽新成功率无法达到100%
  • 降低开发成本,轻量而快速地升级发布补丁等同于发布版本,也应该完整地执行测试与上线流程
  • 远端调试,只為特定用户发送补丁
  • 数据统计,对同一批用户更换补丁版本能够更好地进行ABTest,得到更精确的数据

尽可能多的剔除不必要的步骤,然後提升必要步骤的速度

增量构建 -> 改变部署

适用于多数简单的改变(包括一些方法实现的修改,或者变量值修改)

涉及结构性变化,如修改了继承规则或方法签名

  • 同时会有一个新的Application类,它注入了一个自定义类加载器同时该Application会启动我们所需的新注入的App Server。于是AndroidManifest会被修改來确保我们能使用这个新的Application。
  • 使用的时候它会通过决策,合理运用冷温热拔插来协助我们大量地缩短构建程序的时间

运行着Gradle任务来生荿增量.dex文件(dex对应着开发中的修改类),AS会提取这些.dex文件发送到App Server然后部署到App。因为原来版本的类都装载在运行中的程序了Gradle会解释更新恏这些.dex文件,发送到App Server的时候交给自定义的类加载器来加载.dex文件。
App Server会不断地监听是否需要重写类文件如果需要,任务会被立马执行新嘚更改便能立即被响应。

需要注意的是此时InstantRun是不能回退的,必须重启应用响应修改

因为资源文件是在Activity创建时加载,所以必须重启Activity加载資源文件

注意:AndroidManifest的值是在APK安装的时候被读取的,所以需要触发一个完整的应用构建和部署

应用部署的时候,会把工程拆分成十个部分每个部分都拥有自己的.dex文件,然后所有的类会根据包名被分配给相应的.dex文件当ColdSwap开启时,修改过的类所对应的的.dex文件会重组生成新的.dex攵件,然后再部署到设备上

注意:应用多进程会被降级为ColdSwap。

manifest文件合并、打包和res一起被AAPT合并到APK中,同时项目代码被编译成字节码然后轉换成.dex文件,也被合并到APK中

Android打包流程回顾,最后对于release签名apk需要进行zipalign优化它是指什么?
各种类型的数据按照一定的规则在内存空间上排列这就是对齐。

内存对齐的优势在于能够以空间换时间减少数据存取指令周期,提升程序运行时的速度

编译器内存字节对齐的原则昰什么?
  • 1、数据类型的自身对齐值就是其长度(64位 OS)
  • 2、结构体或类的自身对齐值就是成员中自身对齐值最大的那个。需要起始地址必须昰其相应有效对齐值的整数并要求结构体的大小也为该结构体有效对齐值的整数倍。
手动执行Align优化

检查当前APK是否已经执行过Align优化:

  • 4:代表对齐为4个字节

它是一个基于Android Dex分包方案。它将多个dex文件放入到app的classloader中但是android dex拆包方案中的类是没有重复的,如果classes.dex和classes1.dex中有重复的类当用到這个重复的类时,系统会选择哪个类进行加载呢

一个ClassLoader可以包含多个dex文件,每个dex文件是一个Elements多个dex文件排列成有序的dexElements,当找类的时候会按顺序遍历dex文件,然后从当前遍历的dex文件中找类如果找到则返回,如果找不到从下一个dex文件继续查找

所以,如果在不同的dex中有相同的類存在那么会优先选择排在前面的dex文件的类。

Qzone热补丁方案就是把有问题的类打包到一个dex(patch.dex)中去然后把这个dex插入到Elements的最前面。

1、当其咜dex文件中的类引用了patch.dex中的类时会出现校验错误。拆分dex的很多类都不是在同一个dex内的怎么没有问题?

因为这个校验有个前提当引用类被打上了CLASS_ISPREVERIFIED标志,那么就会进行dex的校验

  • 在dex转换成odex(dexopt过程)时,当apk在安装的时候apk中的classes.dex会被虚拟机(dexopt)优化成odex文件,然后才会拿去执行
  • 虚擬机在启动的时候,会有许多的启动参数其中一项就是verify选项,当verify选项被打开时doVerify变量为true,那么就会执行dvmVerifyClass进行类的校验如果校验成功,這个类会被打上CLASS_ISPREVERIFIED标志
具体的校验过程是怎么样的?

如果以上方法中直接引用到的类(第一层级关系不会进行递归搜索)和clazz都在同一个dexΦ的话,那么这个类就会被打上CLASS_ISPREVERIFED标志

为了解决补丁方案中遇到的问题,所以必须从这些方法中入手防止类被打上CLASS_ISPREVERIFIED标志。空间的方案是往所有类的构造函数里面插入一段代码:

其中AntilazyLoad类会被打包成单独的hack.dex这样当安装apk的时候,classes.dex中的类都会引用一个在不同dex中的AntilazyLoad类这样就防止類被打上了CLASS_ISPREVERIFILED标志,只要没被打上这个标志的类都可以进行打补丁操作

  • 1、在应用启动进行加载时,AntilazyLoad类所在的dex包必须先加载进来不然AntilazyLoad类会被标记为不存在,即使后续加载了hack.dex包那么它也是不存在的。

为什么要选择构造函数

因为他不增加方法数,一个类即使没有显示的构造函数也有一个隐式的默认构造函数。

如何更高效地插入上述代码

可以使用ASM/javaassist库在编译期间将相应的字节码插入Class文件中。

Art采用了新的方式插桩对代码的执行效率没有影响。但是补丁中的类出现修改类变量或者方法可能会导致出现内存地址错乱的情况。

dex2oat时fast*已经将类能确定嘚各个地址写死如果运行时补丁包的地址出现改变,原始类去调用时就会出现地址错乱

将其父类以及调用类的所有类都加入到补丁包Φ。

虚拟机在安装期间为类打上CLASS_ISPREVERIFIED标志是为了什么

由于现在很多App都使用了MultiDex分包方案,这导致了很多类都没有被打上这个标志所以此时禁鼡所有类打上CLASS_ISPREVERIFIED标志对性能的影响不是很大。

如何有效地生成补丁包
  • 1、在正式版本发布的时候,会生成一份缓存文件里面记录了所有class文件的MD5值,还有一份mapping混淆文件
  • 2、在后续的版本中使用-applaymapping选项,应用正式版本的mapping文件然后计算编译完成的class文件的MD5和正式版本进行比较,把不楿同的class文件打包成补丁包

在补丁包大小与性能损耗上有一定的局限性。

插桩就是将一段代码插入或者替换原本的代码
字节码插桩就是茬我们的代码编译成字节码(Class)后,在Android下生成dex之前修改Class文件修改或者增强原有代码逻辑的操作。

除了AspectJ、Javassist框架外还有一个应用更为广泛嘚ASM框架同样也是字节码操作框架,Instant Run包括Javassist就是借助ASM来实现各自的功能

可以这里理解Class字节码与ASM的联系:

Android 1.5.0版本以后提供了Transform API,允许第三方Plugin在打包dex攵件之前的编译过程中操作.class文件我们做的就是实现Transform进行.class文件遍历拿到所有方法,修改完成后对文件进行替换

1、自动埋点追踪,遍历所囿文件更换字节码


  

3、使用ASM进行字节码编写

3、先在java文件中编写要插入的代码然后使用ASM插件查看对应的字节码,根据其用ASM提供的Api一一对应地紦代码填进来即可

关于编译插桩的知识,笔者后面会有一系列的文章进行深入讲解具体的文章目录可以在。

  • 在编译时通过新旧两个Dex生荿差异patch.dex在运行时,将差异patch.dex重新跟原始安装包的旧Dex还原为新的Dex由于比较耗费时间与内存,放在后台进程:patch中为了补丁包尽可能小,微信洎研了DexDiff算法它深度利用Dex的格式来减少差异的大小。
  • 2、一个额外的合成过程合成时间长短和额外的内存消耗也会影响最终的成功率。

若鈈care性能损耗与补丁包大小Qzone是最简单且成功率最高的方案。

7、完善的热补丁系统构建

负责将补丁包交付给用户包括特定用户和全量用户。

在登录/24小时等时机通过pull方式查询后台是否有对应的补丁包更新。

2、指定版本的push通道

在紧急情况下我们可以在一个小时内向所有用户丅发补丁包更新。

3、指定特定用户的push通道

对特定用户或用户组做远程调试

快速上线,管理历史记录以及监控补丁的运行情况。

构建了App與系统(ROM)之间可靠的通信框架让系统知道App的需求。

一种优化资源调度的技术

让应用程序与系统资源实现实时”双向对话”。当来自應用和游戏程序的不同场景和用户行为被Hyper Boost识别后手机会智能地匹配到合理的系统资源,让手机SoC的CPU、GPU、ISP、DSP提供的运算资源更加合理地利用从而让用户使用手机更加流畅。

1、启动优化是怎么做的

  • 1、分析现状、确认问题
  • 2、针对性优化(先概括,引导其深入)

在某一个版本之後呢我们会发现这个启动速度变得特别慢,同时用户给我们的反馈也越来越多所以,我们开始考虑对应用的启动速度来进行优化然後,我们就对启动的代码进行了代码层面的梳理我们发现应用的启动流程已经非常复杂,接着我们通过一系列的工具来确认是否在主線程中执行了太多的耗时操作。

我们经过了细查代码之后发现应用主线程中的任务太多,我们就想了一个方案去针对性地解决也就是進行异步初始化。(引导=>第2题) 然后我们还发现了另外一个问题,也可以进行针对性的优化就是在我们的初始化代码当中有些的优先級并不是那么高,它可以不放在Application的onCreate中执行而完全可以放在之后延迟执行的,因为我们对这些代码进行了延迟初始化最后,我们还结合叻idealHandler做了一个更优的延迟初始化的方案利用它可以在主线程的空闲时间进行初始化,以减少启动耗时导致的卡顿现象做完这些之后,我們的启动速度就变得很快了

最后,我简单说下我们是怎么长期来保持启动优化的效果的首先,我们做了我们的启动器并且结合了我們的CI,在线上加上了很多方面的监控(引导=> 第4题)

2、是怎么异步的,异步遇到问题没有

我们最初是采用的普通的一个异步的方案,即new Thread + 設置线程优先级为后台线程的方式在Application的onCreate方法中进行异步初始化后来,我们使用了线程池、IntentService的方式但是,在我们应用的演进过程当中發现代码会变得不够优雅,并且有些场景非常不好处理比如说多个初始化任务直接的依赖关系,比如说某一个初始化任务需要在某一个特定的生命周期中初始化完成这些都是使用线程池、IntentService无法实现的。所以说我们就开始思考一个新的解决方案,它能够完美地解决我们剛刚所遇到的这些问题

这个方案就是我们目前所使用的启动器,在启动器的概念中我们将每一个初始化代码抽象成了一个Task,然后对咜们进行了一个排序,根据它们之间的依赖关系排了一个有向无环图接着,使用一个异步队列进行执行并且这个异步队列它和CPU的核心數是强烈相关的,它能够最大程度地保证我们的主线程和别的线程都能够执行我们的任务也就是大家几乎都可以同时完成。

3、启动优化囿哪些容易忽略的注意点

  • 2、注意延迟初始化的优化

首先,在CPU Profiler和Systrace中有两个很重要的指标即cpu time与wall time,我们必须清楚cpu time与wall time之间的区别wall time指的是代码執行的时间,而cpu time指的是代码消耗CPU的时间锁冲突会造成两者时间差距过大。我们需要以cpu time来作为我们优化的一个方向

其次,我们不仅只追求启动速度上的一个提升也需要注意延迟初始化的一个优化,对于延迟初始化通常的做法是在界面显示之后才去进行加载,但是如果此时界面需要进行滑动等与用户交互的一系列操作就会有很严重的卡顿现象,因此我们使用了idealHandler来实现cpu空闲时间来执行耗时任务这极大哋提升了用户的体验,避免了因启动耗时任务而导致的页面卡顿现象

最后,对于启动优化还有一些黑科技,首先就是我们采用了类預先加载的方式,我们在MultiDex.install方法之后起了一个线程然后用Class.forName的方式来预先触发类的加载,然后当我们这个类真正被使用的时候就不用再进荇类加载的过程了。同时我们再看Systrace图的时候,有一部分手机其实并没有给我们应用去跑满cpu比如说它有8核,但是却只给了我们4核等这些凊况然后,有些应用对此做了一些黑科技它会将cpu的核心数以及cpu的频率在启动的时候去进行一个暴力的提升。

4、版本迭代导致的启动变慢有好的解决方式吗

这种问题其实我们之前也遇到过,这的确非常难以解决但是,我们后面对此进行了反复的思考与尝试终于找到叻一个比较好的解决方式。

首先我们使用了启动器去管理每一个初始化任务,并且启动器中每一个任务的执行都是被其自动进行分配的也就是说这些自动分配的task我们会尽量保证它会平均分配在我们每一个线程当中的,这和我们普通的异步是不一样的它可以很好地缓解峩们应用的启动变慢。

其次我们还结合了CI,比如说我们现在限制了一些类,如Application如果有人修改了它,我们不会让这部分代码合并到主幹分支或者是修改之后会有一些内部的工具如邮件的形式发送到我然后,我就会和他确认他加的这些代码到底是耗时多少能否异步初始化,不能异步的话就考虑延迟初始化如果初始化时间太长,则可以考虑是否能进行懒加载等用到的时候再去使用等等。

然后我们會将问题尽可能地暴露在上线之前。同时我们真正已经到了线上的一个环境下时,我们进行了监控的一个完善我们不仅是监控了App的整個的启动时间,同时呢我们也将每一个生命周期都进行了一个监控。比如说Application的onCreate与onAttachBaseContext方法的耗时以及这两个生命周期之间间隔的时间,我們都进行了一个监控如果说下一次我们发现了这个启动速度变慢了,我们就可以去查找到底是哪一个环节变慢了我们会和以前的版本進行对比,对比完成之后呢我们就可以来找这一段新加的代码。

  • wall time(代码执行时间)与cpu time(代码消耗CPU时间)锁冲突会造成这两者时间差距過大
  • 线上监控多阶段时间(App、Activity、生命周期间隔时间)
  • 收敛启动代码修改权限。
  • 结合CI修改启动代码需要Review通知

至此,探索Android启动速度优化嘚旅途也应该告一段落了如果你耐心读到最后的话,会发现要想极致地提升App的性能需要有一定的技术广度,如我们引入了始于后端的AOP編程来实现无侵入式的函数插桩也需要有一定的深度,从前面的探索之旅来看我们先后涉及了Framework层、Native层、Dalvik虚拟机、甚至是Linux IO和文件系统相關的原理。因此我想说,Android开发并不简单即使是App层面的性能优化这一知识体系,也是需要我们不断地加深自身知识的深度和广度

ps:在攵章的黑科技部分涉及到了许多基础架构研发领域的知识,这部分无法理解
的同学不要灰心先了解即可,笔者之后的文章都会一一详细講解

12、《Android应用性能优化最佳实践》

欢迎关注我的微信:bcce5360

微信群由于人太多,所以无法生成二维码麻烦大家想进微信群的朋友们,加我微信拉你进群

很感谢您阅读这篇文章,希望您能将它分享给您的朋友或技术群这对我意义重大。

希望我们能成为朋友在 、上一起分享知识。

版权声明:本文为博主原创文章遵循 版权协议,转载请附上原文出处链接和本声明

进程是程序在一个数据集上的运行过程。理解三个关键字:
程序:静止的概念比洳 .exe 可执行文件
运行:进程是程序的一次执行过程
数据集:进程为程序的执行分配各种资源,并进行调度

进程由三部分组成:程序、数据、PCB
PCB:进程控制块通过包括

  • 进程表示符:用于唯一标识一个进程
  • 处理机状态:由处理机(比如ARM)的各种寄存器的内容组成
  • 进程调度信息:和進程调度有关的信息
  • 进程控制信息: 包括程序和数据的地址、进程同步和通信机制……
  • 当系统创建完成一个进程并分配资源后,进程就置於就绪状态
  • 调度程序分配时间片后进程由就绪状态转为执行状态

时间片:进程从创建到终止内使用的CPU时间由内核负责记录。内核为每个進程设置定时器当某一进程使用一定数量的时间片后,内核通过时钟中断让其它进程获得CPU控制权单处理器同一时间只能有一个进程处於执行状态

  • 当时间片用完,进程回到就绪状态等待调度程序再次分配时间片
  • 当执行状态的进程由于某事件受阻,比如等待外部信号进程变为阻塞状态,当外部事件满足条件回到就绪状态
  • 当进程执行结束或强行结束时,产生进程终止事件
  • 进程控制通过原语,包括创建原语撤销原语、阻塞原语、唤醒原语

原语:执行某些特定功能不可被中断的程序段

VIP专享文档是百度文库认证用户/机構上传的专业性文档文库VIP用户或购买VIP专享文档下载特权礼包的其他会员用户可用VIP专享文档下载特权免费下载VIP专享文档。只要带有以下“VIP專享文档”标识的文档便是该类文档

VIP免费文档是特定的一类共享文档,会员用户可以免费随意获取非会员用户需要消耗下载券/积分获取。只要带有以下“VIP免费文档”标识的文档便是该类文档

VIP专享8折文档是特定的一类付费文档,会员用户可以通过设定价的8折获取非会員用户需要原价获取。只要带有以下“VIP专享8折优惠”标识的文档便是该类文档

付费文档是百度文库认证用户/机构上传的专业性文档,需偠文库用户支付人民币获取具体价格由上传人自由设定。只要带有以下“付费文档”标识的文档便是该类文档

共享文档是百度文库用戶免费上传的可与其他用户免费共享的文档,具体共享方式由上传人自由设定只要带有以下“共享文档”标识的文档便是该类文档。

我要回帖

 

随机推荐