欢迎大家来到IT世界,在知识的湖畔探索吧!
前言
汇总了一下众多大佬的性能优化文章,知识点,主要包含:
UI优化/启动优化/崩溃优化/卡顿优化/安全性优化/弱网优化/APP深度优化等等等~
本篇是第二篇:启动优化! [非商业用途,如有侵权,请告知我,我会删除]
强调一下: 性能优化的开发文档跟之前的面试文档一样,想要的跟作者直接要。
二、启动优化
2.1 我们为什么要做启动优化?
用户希望应用能够快速打开。启动时间过长的应用不能满足这个期望,并且可能会令用户失望。轻则鄙视你,重则直接卸载你的应用。
用户不会在乎你的项目是不是过大,里面是不是有很多初始化的逻辑。他只在乎你-慢了。
所以咱们这篇文章有两个目的:
- 启动速度提升(用户眼中的大神就是你)
- 优化代码逻辑和规范(别让自己成为继任者中的XX)
今天咱们就来了解一下应用启动内部机制和启动速度优化。
2.2 启动内部机制
应用有三种启动状态:
- 冷启动;
- 温启动;
- 热启动。
2.2.1 冷启动
冷启动是指应用从头开始:冷启动发生在设备启动后第一次启动应用程序 (Zygote>fork>app) ,或系统关闭应用程序后。
在冷启动开始时,系统有三个任务。 这些任务是:
- 加载和启动应用程序。
- 启动后立即显示应用程序的空白启动页面。
- 创建应用程序进程。
一旦系统创建了应用程序进程,应用程序进程就负责接下来的阶段:
- 创建应用的实体。
- 启动主线程。
- 创建主页面。
- 绘制页面上的View。
- 布局页面。
- 执行首次的绘制。
如下图:
- Displayed Time:初始显示时间
- reportFullyDrawn():完全显示的时间
注意:在创建 Application 和创建 Activity 期间可能会出现性能问题。
创建 Application
当应用程序启动时,空白启动页面保留在屏幕上,直到系统首次完成应用程序的绘制。
如果你重写了Application.onCreate(),系统将调用Application 上的onCreate()方法。之后,应用程序生成主线程,也称为UI线程,并将创建主Activity的任务交给它。
创建Activity
应用进程创建你的Activity后,Activity会执行以下操作:
- 初始化值。
- 调用构造函数。
- 调用 Activity 当前生命周期状态的回调方法,如 Activity.onCreate()。
注意:onCreate() 方法对加载时间的影响最大,因为它执行开销最高的工作:加载UI的布局和渲染,以及初始化Activity运行所需的对象。
2.2.2 热启动
热启动时,系统将应用从后台拉回前台,应用程序的 Activity 在内存中没有被销毁,那么应用程序可以避免重复对象初始化,UI的布局和渲染。
如果 Activity 被销毁则需要重新创建。
和冷启动的区别: 不需要创建 Application。
2.2.3 温启动
温启动介于冷启动和热启动中间吧。例如:
- 用户按返回键退出应用,然后重新启动。进程可能还没有被杀死,但应用必须通过调用onCreate()重新创建 Activity。
- 系统回收了应用的内存,然后用户重新运行应用。应用进程和Activity都需要重新启动。
咱们看看他们共同消耗多长时间。
2.3 查询的启动时间
2.3.1 初始显示时间(Time to initial display)
在 Android 4.4(API 级别 19)及更高版本中,logcat 包含一个输出行,其中包含一个名为 Displayed 的值。 此值表示启动流程和完成在屏幕上绘制相应活动之间经过的时间量。 经过的时间包含以下事件序列:
- 启动进程。
- 初始化对象。
- 创建并初始化Activity。
- 加载布局。
- 第一次绘制你的应用程序。
注意这里查看日志需要如下操作:
报告的日志行类,如下图:
//冷启动I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +1s355ms//温启动(进程被杀死)I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +1s46ms//热启动I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +289msI/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +253ms
欢迎大家来到IT世界,在知识的湖畔探索吧!
图例讲解:
第一个时间,冷启动时间:+1s355ms。
然后我们在后台杀死进程,再次启动应用;
第二个时间,温启动时间:+1s46ms。
这里咱们在后台杀死进程所以:应用进程和Activity需要重新启动。
第三个时间:热启动时间:+289ms 和 +253ms
按返回键,仅退出activity。所以耗时比较短。
当然整体看这个应用开启时间并不长,因为 Demo 的 Application 和 Activity 都没有进行太多的操作。
2.3.2 完全显示时间(Time to full display)
你可以使用 reportFullyDrawn() 方法来测量应用程序启动和所有资源和视图层次结构的完整显示之间经过的时间。在应用程序执行延迟加载的情况下,这可能很有价值。在延迟加载中,应用程序不会阻止窗口的初始绘制,而是异步加载资源并更新视图层次结构。
这里我在Activity.onCreate()中加了个工作线程。并在里面调用reportFullyDrawn() 方法。代码如下:
欢迎大家来到IT世界,在知识的湖畔探索吧!@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(this.getClass().getName(), "onCreate");
setContentView(R.layout.activity_main);
...
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
reportFullyDrawn();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
报告的日志行类,如下图:
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s970msI/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s836msI/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s107msI/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s149ms
图例讲解:
然后你会发现界面出来好一会才打这个日志。看到这里我觉得好多人已经知道怎么去优化启动速度了。
2.4 性能迟缓分析
看到上面的实验其实三种启动情况,受我们影响的方面在于 application 和 activity 。
2.4.1 繁琐的Application 初始化
当你的代码覆盖 Application 对象并在初始化该对象时执行繁重的工作或复杂的逻辑时,启动性能可能会受到影响。 产生的原因包括:
- 应用程序的初始onCreate() 函数。如:执行了不需要立即执行的初始化。
- 应用程序初始化的任何全局单例对象。如:一些不必要的对象。
- 可能发生的任何磁盘I/O、反序列化或紧密循环。
解决方案
无论问题在于不必要的初始化还是磁盘I/O,解决方案都是延迟初始化。换句话说,你应该只初始化立即需要的对象。不要创建全局静态对象,而是转向单例模式,应用程序只在第一次需要时初始化对象。
此外,考虑使用依赖注入框架(如Hilt)
2.4.2 繁琐的Activity初始化
活动创建通常需要大量高开销工作。 通常,有机会优化这项工作以实现性能改进。
产生的原因包括:
- 加载大型或复杂的布局。
- 阻止在磁盘或网络 I/O 上绘制屏幕。
- 加载和解码Bitmap。
- VectorDrawable 对象。
- Activity 初始化任何全局单例对象。
- 所有资源初始化。
解决方案如下。
布局优化
- 通过减少冗余或嵌套布局来扁平化视图层次结构。
- 布局复用(< include/>和 < merge/> )
- 使用ViewStub,不加载在启动期间不需要可见的 UI 部分。
具体内容请看后文布局优化。
代码优化
- 不必要的初始化还是磁盘I/O,延迟初始化
- 资源初始化分类,以便应用程序可以在不同的线程上延迟执行。
- 动态加载资源和Bitmap
具体内容请看后文代码优化。
2.5 阻塞实验
2.5.1 Application 阻塞 2秒, Activity 阻塞 2秒。
SccApp.class
欢迎大家来到IT世界,在知识的湖畔探索吧!public class SccApp extends Application {
@RequiresApi(api = Build.VERSION_CODES.P)
@Override
public void onCreate() {
super.onCreate();
String name = getProcessName();
MLog.e("ProcessName:"+name);
getProcessName("com.scc.demo");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
MainActivity.class
public class MainActivity extends ActivityBase implements View.OnClickListener {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(this.getClass().getName(), "onCreate");
setContentView(R.layout.activity_main);
...
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
reportFullyDrawn();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
报告的日志,如下:
//冷启动
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +5s458ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +8s121ms
//温启动(进程被杀死)
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +5s227ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +7s935ms
//热启动
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +2s304ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +5s189ms
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +2s322ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +5s169ms
将 Appliacation 和 Activity 阻塞的2秒都放在工作线程去操作
这个就是把代码放在如下代码中执行即可,就不全部贴出来了。
new Thread(new Runnable() {
@Override
public void run() {
...
}
}).start();
运行结果如下:
//冷启动
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +1s227ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s957ms
//温启动(进程被杀死)
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +1s83ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s828ms
//热启动
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +324ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s169ms
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +358ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s207ms
2.6 APP 启动黑/白屏
Android 应用启动时,尤其是大型应用, 经常出现几秒钟的黑屏或白屏,黑屏或白屏取决于主界面 Activity 的主题风格。
2.6.1 优雅的解决黑白屛
Android 应用启动时很多大型应用都会有一个广告(图片及视频)页或闪屏页(2-3S)。这并不是开发者想要放上去的,而是为了避免上述启动白屏导致用户体很差。当然你可以珍惜这2-3秒做一个异步加载或者请求。
写到这里。应用启动模式、启动时间、启动速度优化算是完事了。当然后面如果有更好的优化方案还会继续补充。
2.7 启动阶段抑制GC
启动时CG抑制,允许堆一直增长,直到手动或OOM停止GC抑制。(空间换时间)
前提条件
- 1、设备厂商没有加密内存中的Dalvik库文件。
- 2、设备厂商没有改动Google的Dalvik源码。
实现原理
- 1、首先,在源码级别找到抑制GC的修改方法,例如改变跳转分支。
- 2、然后,在二进制代码里找到 A 分支条件跳转的”指令指纹”,以及用于改变分支的二进制代码,假设为 override_A。
- 3、最后,应用启动后扫描内存中的 libdvm.so,根据”指令指纹”定位到修改位置,并使用 override_A 覆盖。
缺点
需要白名单覆盖所有设备,但维护成本高。
2.8 CPU锁频
一个设备的CPU通常都是4核或者8核,但是应用在一般情况下对CPU的利用率并不高,可能只有30%或者50%,如果我们在启动速度暴力拉伸CPU频率,以此提高CPU的利用率,那么,应用的启动速度会提升不少。
在Android系统中,CPU相关的信息存储在/sys/devices/system/cpu目录的文件中,通过对该目录下的特定文件进行写值,实现对CPU频率等状态信息的更改。
缺点
暴力拉伸CPU频率,导致耗电量增加。
CPU工作模式
- performance:最高性能模式,即使系统负载非常低,cpu也在最高频率下运行。
- powersave:省电模式,与performance模式相反,cpu始终在最低频率下运行。
- ondemand:CPU频率跟随系统负载进行变化。
- userspace:可以简单理解为自定义模式,在该模式下可以对频率进行设定。
CPU的工作频率范围
对应的文件有:
- cpuinfo_max_freq
- cpuinfo_min_freq
- scaling_max_freq
- scaling_min_freq
2.9 IO优化
- 1、启动过程不建议出现网络IO。
- 2、为了只解析启动过程中用到的数据,应选择合适的数据结构,如将ArrayMap改造成支持随机读写、延时解析的数据存储结构以替代SharePreference。
这里需要注意的是,需要考虑重度用户的使用场景。
补充加油站:Linux IO知识
1、磁盘高速缓存技术
利用内存中的存储空间来暂存从磁盘中读出的一系列盘块中的信息。因此,磁盘高速缓存在逻辑上属于磁盘,物理上则是驻留在内存中的盘块。
其内存中分为两种形式:
- 在内存中开辟一个单独的存储空间作为磁速缓存,大小固定。
- 把未利用的内存空间作为一个缓沖池,供请求分页系统和磁盘I/O时共享。
2、分页
- 存储器管理的一种技术。
- 可以使电脑的主存使用存储在辅助存储器中的数据。
- 操作系统会将辅助存储器(通常是磁盘)中的数据分区成固定大小的区块,称为“页”(pages)。 当不需要时,将分页由主存(通常是内存)移到辅助存储器;当需要时,再将数据取回,加载主存中。
- 相对于分段,分页允许存储器存储于不连续的区块以维持文件系统的整齐。
- 分页是磁盘和内存间传输数据块的最小单位。
3、高速缓存/缓冲器
- 都是介于高速设备和低速设备之间。
- 高速缓存存放的是低速设备中某些数据的复制数据,而缓冲器则可同时存储高低速设备之间的数据。
- 高速缓存存放的是高速设备经常要访问的数据。
4、linux同步IO:sync、fsync、msync、fdatasync
为什么要使用同步IO?
当数据写入文件时,内核通常先将该数据复制到缓冲区高速缓存或页面缓存中,如果该缓冲区尚未写满,则不会将其排入输入队列,而是等待其写满或内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲排入输出队列,最后等待其到达队首时,才进行实际的IO操作—延迟写。
延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,可能会造成文件更新内容的丢失。为了保证数据一致性,则需使用同步IO。
sync
- sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际磁盘写操作结束再返回。
- 通常称为update的系统守护进程会周期性地(一般每隔30秒)调用sync函数。这就保证了定期冲洗内核的块缓冲区。
fsync
- fsync函数只对文件描述符filedes指定的单一文件起作用,并且等待磁盘IO写结束后再返回。通常应用于需要确保将修改内容立即写到磁盘的应用如数据库。
- 文件的数据和metadata通常存放在硬盘的不同地方,因此fsync至少需要两次IO操作。
msync
如果当前硬盘的平均寻道时间是3-15ms,7200RPM硬盘的平均旋转延迟大约为4ms,因此一次IO操作的耗时大约为10ms。
如果使用内存映射文件的方式进行文件IO(mmap),将文件的page cache直接映射到进程的地址空间,这时需要使用msync系统调用确保修改的内容完全同步到硬盘之上。
fdatasync
- fdatasync函数类似于fsync,但它只影响文件的数据部分。而fsync还会同步更新文件的属性。
- 仅仅只在必要(如文件尺寸需要立即同步)的情况下才会同步metadata,因此可以减少一次IO操作。
日志文件都是追加性的,文件尺寸一致在增大,如何利用好fdatasync减少日志文件的同步开销?
创建每个log文件时先写文件的最后一个page,将log文件扩展为10MB大小,这样便可以使用fdatasync,每写10MB只有一次同步metadata的开销。
2.10 磁盘IO与网络IO
磁盘IO(缓存IO)
标准IO,大多数文件系统默认的IO操作。
- 数据先从磁盘复制到内核空间的缓冲区,然后再从内核空间中的缓冲区复制到应用程序的缓冲区。
- 读操作:操作系统检查内核的缓冲区有没有需要的数据,如果已经有缓存了,那么直接从缓存中返回;否则,从磁盘中返回,再缓存在操作系统的磁盘中。
- 写操作:将数据从用户空间复制到内核空间中的缓冲区中,这时对用户来说写操作就已经完成,至于什么时候写到磁盘中,由操作系统决定,除非显示地调用了sync同步命令。
优点
- 在一定程度上分离了内核空间和用户空间,保护系统本身安全。
- 可以减少磁盘IO的读写次数,从而提高性能。
缺点
DMA方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存中写回到磁盘,而不能在应用程序地址空间和磁盘之间进行数据传输,这样,数据在传输过程中需要在应用程序地址空间(用户空间)和缓存(内核空间)中进行多次数据拷贝操作,这带来的CPU以及内存开销是非常大的。
磁盘IO主要的延时(15000RPM硬盘为例)
机械转动延时(平均2ms)+ 寻址延时(2~3ms)+ 块传输延时(0.1ms左右)=> 平均5ms
网络IO主要延时
服务器响应延时 + 带宽限制 + 网络延时 + 跳转路由延时 + 本地接收延时(一般为几十毫秒到几千毫秒,受环境影响极大)
2.11 PIO与DMA
PIO
很早之前,磁盘和内存之间的数据传输是需要CPU控制的,也就是读取磁盘文件到内存中时,数据会经过CPU存储转发,这种方式称为PIO。
DMA(直接内存访问,Direct Memory Access)
- 可以不经过CPU而直接进行磁盘和内存的数据交换。
- CPU只需要向DMA控制器下达指令,让DMA控制器来处理数据的传送即可。
- DMA控制器通过系统总线来传输数据,传送完毕再通知CPU,这样就在很大程度上降低了CPU占用率,大大节省了系统资源,而它的传输速度与PIO的差异并不明显,而这主要取决于慢速设备的速度。
2.12 直接IO与异步IO
直接IO
应用程序直接访问磁盘数据,而不经过内核缓冲区。以减少从内核缓冲区到用户数据缓存的数据复制。
异步IO
当访问数据的线程发出请求后,线程会接着去处理其它事情,而不是阻塞等待。
2.13 数据重排
Dex文件用到的类和APK里面各种资源文件都比较小,读取频繁,且磁盘地址分布范围比较广。我们可以利用Linux文件IO流程中的page cache机制将它们按照读取顺序重新排列在一起,以减少真实的磁盘IO次数。
2.13.1 类重排
使用Facebook的
ReDex github.com/facebook/re…
的Interdex调整类在Dex中的排列顺序。
2.13.2 资源文件重排
- 1、最佳方案是修改内核源码,实现统计、度量、自动化,其次也可以使用Hook框架进行统计得出资源加载顺序列表。
- 2、最后,调整apk文件列表需要修改7zip源码以支持传入文件列表顺序。
2.14 类加载优化(Dalvik)
2.14.1 类预加载原理
对象第一次创建的时候,JVM首先检查对应的Class对象是否已经加载。如果没有加载,JVM会根据类名查找.class文件,将其Class对象载入。同一个类第二次new的时候就不需要加载类对象,而是直接实例化,创建时间就缩短了。
2.14.2 类加载优化过程
- 在Dalvik VM加载类的时候会有一个类校验过程,它需要校验方法的每一个指令。
- 通过Hook去掉verify步骤 -> 几十ms的优化
- 最大优化场景在于首次安装和覆盖安装时,在Dalvik平台上,一个2MB的Dex正常需要350ms,将classVerifyMode设为VERIFY_MODE_NONE后,只需150ms,节省超过50%时间。
ART比较复杂,Hook需要兼容几个版本。而且在安装时,大部分Dex已经优化好了,去掉ART平台的verify只会对动态加载的Dex带来一些好处。所以暂时不建议在ART平台使用。
2.14.3 延伸:插件化和热修复
它们在设计上都存在大量的Hook和私有API调用,共同的缺点有如下两类问题。
1、稳定性较差
由于厂商的兼容性、安装失败、ART加载时dex2oat失败等原因,还是会有一些代码和资源的异常。Android P推出的non-sdk-interface调用限制,以后适配只会越来越难,成本越来越高。
2、性能问题
用到一些黑科技导致底层Runtime的优化享受不到。如Tinker加载补丁后,启动速度会降低5%~10%。
2.14.4 各项热补丁技术的优缺点
缺点
- 只针对单一客户端版本,随着版本差异变大补丁体积也会变大。
- 不支持所有修改,如AndroidManifest。
- 对代码和资源的更新成功率无法达到100%。
优点
- 降低开发成本,轻量而快速地升级。发布补丁等同于发布版本,也应该完整地执行测试与上线流程。
- 远端调试,只为特定用户发送补丁。
- 数据统计,对同一批用户更换补丁版本,能够更好地进行ABTest,得到更精确的数据。
2.14.5 InstanceRun实现机制
Android官方使用热补丁技术实现InstantRun。
应用构建流程
构建 -> 部署 -> 安装 -> 重启app -> 重启activity
实现目标
尽可能多的剔除不必要的步骤,然后提升必要步骤的速度。
InstantRun构建的三种方式
1、HotSwap
增量构建 -> 改变部署
场景:
适用于多数简单的改变(包括一些方法实现的修改,或者变量值修改)。
2、Warm Swap
增量构建 -> 改变部署 -> activity重启
场景:
一般是修改了resources。
3、Cold Swap
增量构建 -> 改变部署 -> 应用重启 -> activity重启
场景:
涉及结构性变化,如修改了继承规则或方法签名。
首次运行Instant Run,Gradle执行的操作
- 在有Instant Run的环境下:一个新的App Server会被注入到App中,与Bytecode instrumentation协同监控代码的变化。
- 同时会有一个新的Application类,它注入了一个自定义类加载器。同时该Application会启动我们所需的新注入的App Server。于是,AndroidManifest会被修改来确保我们能使用这个新的Application。
- 使用的时候,它会通过决策,合理运用冷温热拔插来协助我们大量地缩短构建程序的时间。
HotSwap原理
Android Studio monitors 运行着Gradle任务来生成增量.dex文件(dex对应着开发中的修改类),AS会提取这些.dex文件发送到App Server,然后部署到App。因为原来版本的类都装载在运行中的程序了,Gradle会解释更新好这些.dex文件,发送到App Server的时候,交给自定义的类加载器来加载.dex文件。 App Server会不断地监听是否需要重写类文件,如果需要,任务会被立马执行,新的更改便能立即被响应。
需要注意的是,此时InstantRun是不能回退的,必须重启应用响应修改。
WarmSwap原理
因为资源文件是在Activity创建时加载,所以必须重启Activity加载资源文件。
注意:AndroidManifest的值是在APK安装的时候被读取的,所以需要触发一个完整的应用构建和部署。
ColdSwap原理
应用部署的时候,会把工程拆分成十个部分,每个部分都拥有自己的.dex文件,然后所有的类会根据包名被分配给相应的.dex文件。当ColdSwap开启时,修改过的类所对应的的.dex文件,会重组生成新的.dex文件,然后再部署到设备上。
注意:应用多进程会被降级为ColdSwap。
2.15 ASM字节码插桩
插桩就是将一段代码插入或者替换原本的代码。 字节码插桩就是在我们的代码编译成字节码(Class)后,在Android下生成dex之前修改Class文件,修改或者增强原有代码逻辑的操作。
除了AspectJ、Javassist框架外,还有一个应用更为广泛的ASM框架同样也是字节码操作框架,Instant Run包括Javassist就是借助ASM来实现各自的功能。
可以这样理解Class字节码与ASM之间的联系,即JSON对于GSON就类似于字节码Class对于Javassist/ASM。
Android ASM自动埋点方案实践
Android 1.5.0版本以后提供了Transform API,允许第三方Plugin在打包dex文件之前的编译过程中操作.class文件,我们做的就是实现Transform进行.class文件遍历拿到所有方法,修改完成后对文件进行替换。
大致的流程如下所示:
1、自动埋点追踪,遍历所有文件更换字节码
AutoTransform -> transform -> inputs.each {TransformInput input -> input.jarInput.each { JarInput jarInput -> … } input.directoryInputs.each { DirectoryInput directoryInput -> … }}
2、Gradle插件实现
PluginEntry -> apply -> def android = project.extensions.getByType(AppExtension)
registerTransform(android) -> AutoTransform transform = new AutoTransform
android.registerTransform(transform)
3、使用ASM进行字节码编写
ASM框架核心类
- ClassReader:读取编译后的.class文件。
- ClassWriter:重新构建编译后的类。
- ClassVisitor:拜访类成员信息。
- AdviceAdapter:实现MethodVisitor接口,拜访方法的信息。
1、visit -> 在ClassVisitor中根据判断是否是实现View$OnClickListener接口的类,只有满足条件的类才会遍历其中的方法进行操作。
2、在MethodVisitor中对该方法进行修改
visitAnnotation -> onMethodEnter -> onMethodExit
3、先在java文件中编写要插入的代码,然后使用ASM插件查看对应的字节码,根据其用ASM提供的Api一一对应地把代码填进来即可。
2.16 Tinker
原理
- 全量替换新的Dex
- 在编译时通过新旧两个Dex生成差异patch.dex。在运行时,将差异patch.dex重新跟原始安装包的旧Dex还原为新的Dex。由于比较耗费时间与内存,放在后台进程:patch中,为了补丁包尽可能小,微信自研了DexDiff算法,它深度利用Dex的格式来减少差异的大小。
DexDiff的粒度是Dex格式的每一项,BsDiff的粒度是文件,AndFix/Qzone的粒度为class。
缺点
- 1、占用Rom体积,1.5倍所修改Dex大小 = Dex.jar + dexopt文件。
- 2、一个额外的合成过程,合成时间长短和额外的内存消耗也会影响最终的成功率。
热补丁方案对比
若不care性能损耗与补丁包大小,Qzone是最简单且成功率最高的方案。
2.17 完善的热补丁系统构建
一、网络通道
负责将补丁包交付给用户,包括特定用户和全量用户。
1、pull通道
在登录/24小时等时机,通过pull方式查询后台是否有对应的补丁包更新。
2、指定版本的push通道
在紧急情况下,我们可以在一个小时内向所有用户下发补丁包更新。
3、指定特定用户的push通道
对特定用户或用户组做远程调试。
二、上线与管理平台
快速上线,管理历史记录,以及监控补丁的运行情况。
2.18 启动优化的常见问题(重要!!)
1、启动优化是怎么做的?
- 1、分析现状、确认问题
- 2、针对性优化(先概括,引导其深入)
- 3、长期保持优化效果
在某一个版本之后呢,我们会发现这个启动速度变得特别慢,同时用户给我们的反馈也越来越多,所以,我们开始考虑对应用的启动速度来进行优化。然后,我们就对启动的代码进行了代码层面的梳理,我们发现应用的启动流程已经非常复杂,接着,我们通过一系列的工具来确认是否在主线程中执行了太多的耗时操作。
我们经过了细查代码之后,发现应用主线程中的任务太多,我们就想了一个方案去针对性地解决,也就是进行异步初始化。(引导=>第2题) 然后,我们还发现了另外一个问题,也可以进行针对性的优化,就是在我们的初始化代码当中有些的优先级并不是那么高,它可以不放在Application的onCreate中执行,而完全可以放在之后延迟执行的,因为我们对这些代码进行了延迟初始化,最后,我们还结合了idealHandler做了一个更优的延迟初始化的方案,利用它可以在主线程的空闲时间进行初始化,以减少启动耗时导致的卡顿现象。做完这些之后,我们的启动速度就变得很快了。
最后,我简单说下我们是怎么长期来保持启动优化的效果的。首先,我们做了我们的启动器,并且结合了我们的CI,在线上加上了很多方面的监控。(引导=> 第4题)
2、是怎么异步的,异步遇到问题没有?
- 1、体现演进过程
- 2、详细介绍启动器
我们最初是采用的普通的一个异步的方案,即new Thread + 设置线程优先级为后台线程的方式在Application的onCreate方法中进行异步初始化,后来,我们使用了线程池、IntentService的方式,但是,在我们应用的演进过程当中,发现代码会变得不够优雅,并且有些场景非常不好处理,比如说多个初始化任务直接的依赖关系,比如说某一个初始化任务需要在某一个特定的生命周期中初始化完成,这些都是使用线程池、IntentService无法实现的。所以说,我们就开始思考一个新的解决方案,它能够完美地解决我们刚刚所遇到的这些问题。
这个方案就是我们目前所使用的启动器,在启动器的概念中,我们将每一个初始化代码抽象成了一个Task,然后,对它们进行了一个排序,根据它们之间的依赖关系排了一个有向无环图,接着,使用一个异步队列进行执行,并且这个异步队列它和CPU的核心数是强烈相关的,它能够最大程度地保证我们的主线程和别的线程都能够执行我们的任务,也就是大家几乎都可以同时完成。
3、启动优化有哪些容易忽略的注意点?
- 1、cpu time与wall time
- 2、注意延迟初始化的优化
- 3、介绍下黑科技
首先,在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、版本迭代导致的启动变慢有好的解决方式吗?
- 启动器
- 结合CI
- 监控完善
这种问题其实我们之前也遇到过,这的确非常难以解决。但是,我们后面对此进行了反复的思考与尝试,终于找到了一个比较好的解决方式。
首先,我们使用了启动器去管理每一个初始化任务,并且启动器中每一个任务的执行都是被其自动进行分配的,也就是说这些自动分配的task我们会尽量保证它会平均分配在我们每一个线程当中的,这和我们普通的异步是不一样的,它可以很好地缓解我们应用的启动变慢。
其次,我们还结合了CI,比如说,我们现在限制了一些类,如Application,如果有人修改了它,我们不会让这部分代码合并到主干分支或者是修改之后会有一些内部的工具如邮件的形式发送到我,然后,我就会和他确认他加的这些代码到底是耗时多少,能否异步初始化,不能异步的话就考虑延迟初始化,如果初始化时间太长,则可以考虑是否能进行懒加载,等用到的时候再去使用等等。
然后,我们会将问题尽可能地暴露在上线之前。同时,我们真正已经到了线上的一个环境下时,我们进行了监控的一个完善,我们不仅是监控了App的整个的启动时间,同时呢,我们也将每一个生命周期都进行了一个监控。比如说Application的onCreate与onAttachBaseContext方法的耗时,以及这两个生命周期之间间隔的时间,我们都进行了一个监控,如果说下一次我们发现了这个启动速度变慢了,我们就可以去查找到底是哪一个环节变慢了,我们会和以前的版本进行对比,对比完成之后呢,我们就可以来找这一段新加的代码。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/29988.html