聊聊Android优化这个巨坑

一.启动类型

冷启动

指进程死亡的情况下,从点击应用图标到UI界面完全显示且用户可操作的全部过程。

大致流程:
Click Event -> IPC -> Process.start -> ActivityThread -> bindApplication -> LifeCycle -> ViewRootImpl

用户点击桌面图标,这个点击事件它会触发一个IPC的操作,之后便会执行到Process的start方法中,这个方法是用于进程创建的,接着,便会执行到ActivityThread的main方法,这个方法可以看做是我们单个App进程的入口,相当于Java进程的main方法,在其中会执行消息循环的创建与主线程Handler的创建,创建完成之后,就会执行到 bindApplication 方法,在这里使用了反射去创建 Application以及调用了 Application相关的生命周期,Application结束之后,便会执行Activity的生命周期,在Activity生命周期结束之后,最后,就会执行到 ViewRootImpl,这时才会进行真正的一个页面的绘制。

热启动

即进程存活情况下,点击桌面图标,应用从后台切换到前台

二.如何检测启动耗时

1.查看Logcat

在Android Studio Logcat中过滤关键字“Displayed”,可以看到对应的冷启动耗时日志。

2.adb shell  

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

// 其中的AppstartActivity全路径可以省略前面的packageName
adb shell am start -W [packageName]/[AppstartActivity全路径]

  •  

     

执行后会得到三个时间:ThisTime、TotalTime和WaitTime,详情如下:
ThisTime
表示最后一个Activity启动耗时。
TotalTime
表示所有Activity启动耗时。
WaitTime
表示AMS启动Activity的总耗时。
一般来说,只需查看得到的TotalTime,即应用的启动时间,其包括 创建进程 + Application初始化 + Activity初始化到界面显示 的过程。
特点:

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

3.AOP(Aspect Oriented Programming) 打点

具体AOP可以自行上网查找文章
下面以统计统计Application中的所有方法耗时为例子

@Aspect
public class ApplicationAop {

    @Around("call (* com.json.chao.application.BaseApplication.**(..))")
    public void getTime(ProceedingJoinPoint joinPoint) {
    Signature signature = joinPoint.getSignature();
    String name = signature.toShortString();
    long time = System.currentTimeMillis();
    try {
        joinPoint.proceed();
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
    Log.i(TAG, name + " cost" +     (System.currentTimeMillis() - time));
    }
}

 

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

当Action为Before、After时,方法入参为JoinPoint。
当Action为Around时,方法入参为ProceedingPoint。

Around和Before、After的最大区别:
ProceedingPoint不同于JoinPoint,其提供了proceed方法执行目标方法。

4.使用TraceView
这个的使用参考 《Android性能优化系列之App启动优化

三.启动优化进阶方法

启动优化一些常用的方法参考《Android性能优化系列之App启动优化》,这里不再赘述,这里讲一些进阶的方法

1.定制一套APP启动框架

常见的启动优化,我们会将一些sdk或者模块的初始化进行并发的进行,但这些工作之间可能存在前后依赖的关系,所以我们又需要想办法保证他们执行顺序的正确性,所以需要通过启动框架,为各个任务建立依赖关系,最终构成一个有向无环图。对于可以并发的任务,会通过线程池最大程度提升启动速度。

目前开源的启动框架有:
阿里的alpha:https://github.com/alibaba/alpha
美团的AppInit: https://github.com/laohong/AppInit
具体原理,感兴趣的可以check源码来看

2.I/O 优化

SharedPreference 在初始化的时候还是要全部数据一起解析。如果它的数据量超过
1000 条,启动过程解析时间可能就超过 100 毫秒。如果只解析启动过程用到的数据项则会很大程度减少解析时间,启动过程适合使用随机读写的数据结构。

解决方式:可以将 ArrayMap 改造成支持随机读写、延时解析的数据存储方式。具体实现后续将出文章讲解。

3.数据重排

Linux 文件 I/O 流程
在这里插入图片描述
Linux 文件系统从磁盘读文件的时候,会以 block 为单位去磁盘读取,一般 block 大小是4KB。也就是说一次磁盘读写大小至少是 4KB,然后会把 4KB 数据放到页缓存 Page Cache 中。如果下次读取文件数据已经在页缓存中,那就不会发生真实的磁盘 I/O,而是直接从页缓存中读取,大大提升了读的速度。所以上面的例子,我们虽然读了 1000 次,但事实上只会发生一次磁盘 I/O,其他的数据都会在页缓存中得到。
Dex 文件用的到的类和安装包 APK 里面各种资源文件一般都比较小,但是读取非常频繁。我们可以利用系统这个机制将它们按照读取顺序重新排列,减少真实的磁盘 I/O 次数。

类重排

启动过程类加载顺序可以通过复写 ClassLoader 得到。

class GetClassLoader extends PathClassLoader 
{ 
public Class<?> findClass(String name) { // 将 name 记录到文件 writeToFile(name,"coldstart_classes.txt");
 return super.findClass(name); 
 }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

具体实现可以参考 ReDex 的Interdex,调整类在 Dex 中的排列顺序,可以利用 010 Editor 查看修改后的效果。

资源文件重排

修改 Kernel 源码,单独编译一个特殊的 ROM。这样做的目的
有三个:
1)统计。统计应用启动过程加载了安装包中哪些资源文件,比如 assets、drawable、layout 等。跟类重排一样,我们可以得到一个资源加载的顺序列表。
2)度量。在完成资源顺序重排后,我们需要确定是否真正生效。比如有哪些资源文件加载了,它是发生真实的磁盘 I/O,还是命中了 Page Cache。
3)自动化。任何代码提交都有可能改变启动过程中类和资源的加载顺序,如果完全依靠人工手动处理,这个事情很难持续下去。通过定制 ROM 的一些埋点和配合的工具,我们可以将它们放到自动化流程当中。

事实上如果仅仅为了统计,我们也可以使用 Hook 的方式。下面是利用 Frida 实现获得Android 资源加载顺序的方法

resourceImpl.loadXmlResourceParser.implementation=function(a,b,c,d){ 
	send('file:'+a)
 	return this.loadXmlResourceParser(a,b,c,d) 
 }
 resourceImpl.loadDrawableForCookie.implementation=function(a,b,c,d,e){ 
 	send("file:"+a)
 	return this.loadDrawableForCookie(a,b,c,d,e) 
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

调整安装包文件排列需要修改 7zip 源码实现支持传入文件列表顺序,同样最后可以利用010 Editor 查看修改后的效果。

类的加载

在加载类的过程有一个 verify class 的步骤,它需要校验方法的每一个指令,是一个比较耗时的操作。
在这里插入图片描述
我们可以通过 Hook 来去掉 verify 这个步骤,这对启动速度有几十毫秒的优化。其实最大的优化场景在于首次和覆盖安装时。以 Dalvik 平台为例,一个 2MB 的 Dex
正常需要 350 毫秒,将 classVerifyMode 设为 VERIFY_MODE_NONE 后,只需要 150毫秒,节省超过 50% 的时间。

但是 ART 平台要复杂很多,Hook 需要兼容几个版本。而且在安装时大部分 Dex 已经优化好了,去掉 ART 平台的 verify 只会对动态加载的 Dex 带来一些好处。Atlas 中的dalvik_hack可以通过下面的方法去掉 verify,但是当前没有支持 ART 平台。

这个黑科技可以大大降低首次启动的速度,代价是对后续运行会产生轻微的影响。同时也要考虑兼容性问题,暂时不建议在 ART 平台使用。

最后附上redex地址:https://github.com/facebook/redex

启动阶段抑制GC

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

1、设备厂商没有加密内存中的Dalvik库文件。
2、设备厂商没有改动Google的Dalvik源码。

实现原理
1、首先,在源码级别找到抑制GC的修改方法,例如改变跳转分支。
2、然后,在二进制代码里找到 A 分支条件跳转的"指令指纹",以及用于改变分支的二进制代码,假设为 override_A。
3、最后,应用启动后扫描内存中的 libdvm.so,根据"指令指纹"定位到修改位置,并使用 override_A 覆盖。

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

5.0 以下Multidex预加载优化

安装或者升级后首次 MultiDex 花费的时间过于漫长,我们需要进行Multidex的预加载优化。
优化步骤
1、启动时单独开一个进程去异步进行Multidex的第一次加载,即Dex提取和Dexopt操作。
2、此时,主进程Application进入while循环,不断检测Multidex操作是否完成。
3、执行到Multidex时,则已经发现提取并优化好了Dex,直接执行。MultiDex执行完之后主进程Application继续执行ContentProvider初始化和Application的onCreate方法。

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

posted @ 2019-04-18 09:55  GLORY-HOPE  阅读(807)  评论(0编辑  收藏  举报