持续创作,加速生长!这是我参加「日新计划 · 10 月更文应战」的第4天,点击检查活动详情
前语
汇总了一下众多大佬的功能优化文章,知识点,首要包含:
本篇是第二篇:发动优化! [非商业用途,如有侵权,请奉告我,我会删除]
强调一下: 功能优化的开发文档跟之前的面试文档相同,想要的跟作者直接要。
二、发动优化
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: +289ms
I/ActivityTaskManager: Displayed com.scc.demo/.actvitiy.MainActivity: +253ms
图例解说:
第一个时刻,冷发动时刻:+1s355ms。
然后咱们在后台杀死进程,再次发动运用;
第二个时刻,温发动时刻:+1s46ms。
这儿咱们在后台杀死进程所以:运用进程和Activity需求从头发动。
第三个时刻:热发动时刻:+289ms 和 +253ms
按回来键,仅退出activity。所以耗时比较短。
当然全体看这个运用敞开时刻并不长,由于 Demo 的 Application 和 Activity 都没有进行太多的操作。
2.3.2 彻底显现时刻(Time to full display)
你能够运用 reportFullyDrawn() 办法来测量运用程序发动和一切资源和视图层次结构的完好显现之间经过的时刻。在运用程序履行推迟加载的状况下,这可能很有价值。在推迟加载中,运用程序不会阻挠窗口的初始制作,而是异步加载资源并更新视图层次结构。
这儿我在Activity.onCreate()中加了个作业线程。并在里边调用reportFullyDrawn() 办法。代码如下:
@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: +3s970ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s836ms
I/ActivityTaskManager: Fully drawn com.scc.demo/.actvitiy.MainActivity: +3s107ms
I/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
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办法的耗时,以及这两个生命周期之间间隔的时刻,咱们都进行了一个监控,假如说下一次咱们发现了这个发动速度变慢了,咱们就能够去查找到底是哪一个环节变慢了,咱们会和以前的版别进行比照,比照完结之后呢,咱们就能够来找这一段新加的代码。