zoukankan      html  css  js  c++  java
  • 聊聊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的预加载。

  • 相关阅读:
    vivo 全球商城:从 0 到 1 代销业务的融合之路
    mysql 批量kill掉运行中的进程id
    启用php-fpm状态功能 --php-fpm调优也有
    PHP-php-fpm配置优化
    Linux-cpu分析-vmstat
    关于overflow:hidden的作用(溢出隐藏、清除浮动、解决外边距塌陷等等)
    curl 发送POST请求
    python redis-rdb工具 分析redis工具
    tupdump
    spring cloud gateway security oauth2
  • 原文地址:https://www.cnblogs.com/gloryhope/p/10727813.html
Copyright © 2011-2022 走看看