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

文摘   科技   2024-04-18 17:11   山东  

2024年 第08篇


其他可以黑科技优化的方向


  • 核心线程绑定大核

  • GC 抑制

  • 字节码插桩与性能监控

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



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



| 3.1 核心线程绑定大核

「 定义 」

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

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

「 查看设备是否有大小核 」

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

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

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


「 绑定 CPU 核实现 」

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

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

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

    1. 参数 1 是线程的 id,如果为 0 则表示主线程。

    2. 参数 2 表示 cpu 序列掩码的长度。

    3. 参数 3 则表示需要绑定的 cpu 序列的掩码。

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

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


| 3.2 GC 抑制

「 什么是 GC 抑制 」

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

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

「 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 次数明显减少,启动速度也有一定的提升。


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

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

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

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

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

「 GC 抑制实现 」

GC 工作的原理

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

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

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

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

class HeapTask :public SelfDeletingTask {
};

class SelfDeletingTask :public Task {
};

class Task :public Closure {
};

class Closure {
public:
virtual ~Closure() { }
// 定义 Run 虚函数
virtual void Run(Thread* self) = 0;
};

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

GC 抑制方案:Native 层的 Hook

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

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

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

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

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

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

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

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

| 3.3 字节码插桩与性能监控

「 性能监控的流程 」

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

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

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

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

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

「 字节码插桩原理 」

字节码插桩的原理就是在 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。具体的使用就不进行介绍了,有框架使用的话,写字节码还是比较方便的。


04 总结



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

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

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

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

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

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

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

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



05 参考


  1. 盘点 Android 常用 Hook 技术

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

  3. Android 中如何 Hook 住 JNI 方法

  4. JNI 函数 Hook 实战

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

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

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



06 团队介绍


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


    _________________ END__________________

三翼鸟数字化科技
三翼鸟数字化技术团队官方订阅号,提供技术前沿洞察、技术实践分享、最佳实践整合、技术规范发布、团队文化输出。