• Android 性能优化之黑科技开道(二)


    3. 其它可以黑科技优化的方向

    3.1 核心线程绑定大核

    3.1.1 定义

    核心线程绑定大核的思路也很容易理解,现在的 CPU 都是多核的,大核的频率比小核要高不少,如果我们的核心线程固定运行在大核上,那么应用性能自然会有所提升。

    核心线程指的是 UI 线程、RenderThread 线程,因为它们直观影响用户的感受,或者在具体项目中的其它特定线程,比如语音处理,为了有更快的处理结果,语音线程也是可以列为核心线程的。

    3.1.2 查看设备是否有大小核

    1. 可以通过/sys/devices/system/cpu/目录下的文件获取各个核的频率

    2. 尝试了下正在开发的设备,它没有大小核之分,所有核的频率全都一样,如下:

    3. 当然,我们可以将此判断写到代码中,由我们的 App 智能判断是否需要绑定大小核,并找出来大核线程是哪个,具体代码这里就不贴了,原理同上,需要注意下读取权限问题

    3.1.3 绑定 CPU 核实现

    1. 绑定大核是通过函数 sched_setaffinity 实现的。

    1. extern "C" JNIEXPORT void JNICALL Java_com_zj_android_startup_optimize_StartupNativeLib_bindCore(JNIEnv env, *jobject /* this */, jint thread_id, jint core) {
    2. cpu_set_t mask; // CPU 核的集合
    3. CPU_ZERO(&mask); // 将mask置空
    4. CPU_SET(core, &mask); // 将需要绑定的 cpu 核设置给mask,核为序列0,1,2,3……
    5. if (sched_setaffinity(thread_id, sizeof(mask), &mask) == -1) { // 将线程绑核
    6. LOG ("bind thread %d to core %d fail", thread_id, core);
    7. } else {
    8. LOG ("bind thread %d to core %d success", thread_id, core);
    9. }
    10. }

    2. 如上所示,sched_setaffinity 共有 3 个参数。

    • 参数 1 是线程的 id,如果为 0 则表示主线程。
    • 参数 2 表示 cpu 序列掩码的长度。
    • 参数 3 则表示需要绑定的 cpu 序列的掩码。

    3. 以上是线程绑定大核的核心代码,可以看到我们还需要获取 RenderThread 的 id ,以及 cpu 大核的序列。

    4. 应用中线程的信息记录在 /proc/pid/task 的文件中,通过解析 task 文件就可以获取当前进程的所有线程,而 cpu 大核序列也可以通过解析 /sys/devices/system/cpu 目录实现。

    3.2 GC 抑制

    3.2.1 什么是 GC 抑制

    1. 首先 GC,就是 Java 的垃圾回收,GC 抑制指的是在 App 启动阶段,不让系统做 GC 或者是将 GC 的频繁降低,以提高启动速度

    2. 此技术在 Android10 以上的系统已加入,所以这里讨论的是 在 Android10 以下的系统中添加此功能

    3.2.2 Android10 中的 GC 抑制如何实现的

    1. Java 的垃圾回收机制,在 Android 5.0 之后,ART 取代了 Dalvik,ART 虚拟机在垃圾回收的时候虽然没有像 Dalvik 一样 stop the world,但在启动阶段如果发生垃圾回收,GC 线程同样抢占了不少系统资源。

    2. Google 也注意到启动阶段 GC 对启动速度的影响,并在 Android 10 之后做了一定的优化,详情可见如下提交:https://cs.android.com/android/_/android/platform/art/+/a98a28262f645d100e2dee9587e7822d35ade6f9 

    3. 可以看出,基本思路是在 2s 内提高后台 GC 的阈值,减少启动阶段的 GC 次数,根据 Google 的测试,抑制 GC 后效果如下:

    4. 可以看出,GC 次数明显减少,启动速度也有一定的提升。

    3.2.3 我们的程序是否有必要进行 GC 抑制

    1. 可以通过以下代码获取 gc 的次数与耗时,方便统计 gc 对启动耗时的影响,以评估是否有必要做 GC 抑制

    1. Debug.getRuntimeStat("art.gc.gc-count") // gc 次数
    2. Debug.getRuntimeStat("art.gc.gc-time") // gc 耗时
    3. Debug.getRuntimeStat("art.gc.blocking-gc-count") // 阻塞 gc 次数
    4. Debug.getRuntimeStat("art.gc.blocking-gc-time") // 阻塞 gc 耗时

    在电视项目的首页查看 GC 的情况,结果如下,发现从启动到首页显示出来,GC 次数和时间都是比较高的值:

    2. 另外,我在 profiler 工具中观察到我们的 GC 线程可以更直观的看到,不只是在启动的时候,后续它也会频繁大量的运行,如下:

    3.2.4 GC 抑制实现

    GC 工作的原理

    GC 主要是通过 HeapTaskDaemon 线程实现的,这是一个守护线程,在 Zygote 线程启动后这个线程也就启动了,启动后主要做了以下工作:

    1. 从 HeapTaskDaemon.runInternal()方法开始一步步调用到 native 层的 task_processor.RunAllTasks() 方法。

    2. 当 TaskProcessor 中的 tasks 为空时,会休眠等待,否则会取出第一个 HeapTask 并执行其 Run 方法。

        而 HeapTask 的 Run 方法是一个虚函数,需要子类来实现。

    1. class HeapTask : public SelfDeletingTask {
    2. };
    3. class SelfDeletingTask : public Task {
    4. };
    5. class Task : public Closure {
    6. };
    7. class Closure {
    8. public:
    9. virtual ~Closure() { }
    10. // 定义 Run 虚函数
    11. virtual void Run(Thread* self) = 0;
    12. };

    HeapTask 就是垃圾回收的任务,有多个子类,比如最常见的 ConcurrentGCTask 就是其子类,在 Java 内存达到阈值时就会执行这个 Task,用于执行并发 GC。

    GC 抑制方案:Native 层的 Hook

    在了解了 HeapTaskDaemon 的执行流程之后,我们想到,如果启动时在 ConcurrentGCTask 的 Run 方法执行前休眠一段时间,不就可以实现 GC 抑制了吗?

    而 Run 方法正好是虚函数,虚函数与 Java 中的抽象函数类似,留给子类去扩展实现多态。

    虚函数和外部库函数一样都没法直接执行,需要在表中去查找函数的真实地址,那么我们是不是可以使用类似 PLT Hook 的思路,使用自定义函数的地址替换原有函数地址,实现 Hook 呢?

    答案是肯定的,如上图所示,一个类中如果存在虚函数,那么编译器就会为这个类生成一张虚函数表,并且将虚函数表的地址放在对象实例的首地址的内存中。同一个类的不同实例,共用一张虚函数表的。

    因此我们的主要思路如下:

    1. 启动时将虚函数表中的 Run 函数地址替换为自定义函数地址。

    2. 在自定义函数内部休眠一段时间,抑制 GC。

    休眠完成后将虚函数表中的函数地址替换回来,避免影响后续执行。

    3.3 字节码插桩与性能监控

    3.3.1 性能监控的流程

    基于性能问题,我们可以进行一个性能方面的监控,以达到随时了解情况,随时进行优化的目的。市场上有很多商业化的 APM 平台,比如著名的 NewRelic,还有国内的 听云、OneAPM 等等,还有我们自己也有性能监控平台。这些平台的工作流程如下:

    1. 首先在客户端(Android、iOS、Web 等)采集数据;

    2. 接着将采集到的数据整理上报到服务器;

    3. 服务器接收到数据后建模、存储、挖掘分析,让后将数据可视化,供用户使用。

    其中客户端数据采集时使用字节码插桩比较方便快捷,并且具有较大的通用性

    3.3.2 字节码插桩原理

    字节码插桩的原理就是在 Android 打包的时候,通过 ASM 等框架将 Java 字节码,插入到特定位置上,达到自动加入某些重复代码的目的,也即是 AOP 编程,如下是 Android 打包的流程:

    插桩入口

    在打包过程中,会将所有 class 文件,包括第三方的 class 文件打包成一个或者多个 dex 文件。这其中涉及到两个很关键的环节:

    javac:将 。java 格式的源代码文件编译成 class 文件;

    dex: 将 class 格式的文件打包汇总,组成一个或者多个 dex 文件。

    我们想要对字节码进行修改,只需要在 javac 之后 dex 之前遍历所有的字节码文件,并按照一定的规则过滤修改就好了,这里便是字节码插桩的入口。

    那么我们到底如何介入打包过程,在 class 转换为 dex 文件的时候实现对字节码的修改呢?

    答案是 transform api。Android Gradle Plugin 1.5.0 及以上版本,Google 官方提供了 transform api 作为字节码插桩的入口。我们只需要实现一个自定义的 Gradle Plugin,然后在编译阶段去修改字节码文件即可。

    修改字节码

    找到了插桩入口,接下来就要对字节码进行修改。对于字节码的修改,比较常用的框架有 Javassist 和 ASM。具体的使用就不进行介绍了,有框架使用的话,写字节码还是比较方便的。

    4. 总结

    本篇主要介绍了一些 Android 中实用的黑科技,包括 Hook 技术,线程自定义调整,GC 抑制,字节码插桩等,在电视版智家 App9.0 项目中已经验证了部分技术,还有一些技术正在规划中,后续将会逐步的提升我们的 App 性能。

    最后,讨论一个问题,这些黑科技是"奇淫巧技"吗,还是合理合法的使用呢?

    这里引用一篇文章中的原话:

    国产定制安卓系统一直都在安卓版本号更新之前,领先不只一个身位。

    以至于每次的安卓大版本更新像是在追授国产定制 Android 在 N 年前魔改的功勋,甚至像是在若干个发行版本选一个最好的方案作为整个 Android 生态的标准。

    招安,才是最形象的解释。

    参考:如何评价谷歌刚发布的 AOSP14,在 iOS 和鸿蒙的竞争下,安卓还有哪些第三方开发的系统亮点值得关注?

    国内的 Android 黑科技一直是率先发展的,遍数国内 Android 技术圈走过的路程,从之前的插件化,到双开等,哪一个在当时不算是"奇淫巧技"呢,最后不都成了 Android 官方的标配了么,所以,大胆的探索去吧,能解决我们问题的技术就是好技术。

    5. 参考

    1. 盘点 Android 常用 Hook 技术

    2. 如何优雅关闭 Android 日志输出

    3. Android 中如何 Hook 住 JNI 方法

    4. JNI 函数 Hook 实战

    5. 启动优化中的一些黑科技,了解一下~

    6. Android 性能监控系列一(原理篇)

    7. 如何评价谷歌刚发布的 AOSP14,在 iOS 和鸿蒙的竞争下,安卓还有哪些第三方开发的系统亮点值得关注?

    6. 团队介绍

    三翼鸟数字化技术平台-场景设计交互平台」主要负责设计工具的研发,包括营销设计工具、家电VR设计和展示、水电暖通前置设计能力,研发并沉淀素材库,构建家居家装素材库,集成户型库、全品类产品库、设计方案库、生产工艺模型,打造基于户型和风格的AI设计能力,快速生成算量和报价;同时研发了门店设计师中心和项目中心,包括设计师管理能力和项目经理管理能力。实现了场景全生命周期管理,同时为水,空气,厨房等产业提供商机管理工具,从而实现了以场景贯穿的B端C端全流程系统。

  • 相关阅读:
    【04-提升模型性能:集成学习与超参数优化】
    代码随想录第44天 | ● 1143.最长公共子序列 ● 1035.不相交的线 ● 53. 最大子序和 动态规划
    Windows 搭建 FTP 服务器
    Fabric.js 自定义选框样式
    redis 分布式锁的实现原理
    Springboot之SpringMVC与MyBatis(二)异步迭代商品管理
    PHP中的面向对象编程
    双容水箱液位无线监控模型的设计与实现(MATLAB)
    Android初学 抖音短视频无水印下载APP的实现
    Ai-WB2模组基于TCP的MQTT连接服务器使用示例
  • 原文地址:https://blog.csdn.net/weixin_41559503/article/details/137925061