日期:
来源:鸿洋收集编辑:程序员江同学
本文主要是对Android 性能优化小册相关内容的学习实践,加入了自己的理解与实践内容,感兴趣的同学可以点击查看小册。
adb shell
ps -A | grep 包名 // 根据包名找到Pid
top -H -p PID // 查看线程优先级命令
PR: 优先级 (priority),值越小优先级越高,会受NI的值的影响。 NI: 即 Nice 值,我们可以通过Process.setThreadPriority设置,同样是值越小优先级越高。
我看到一些启动优化的文章谈到线程优先级设置,但测试结果似乎是没有必要?难道是版本问题?有了解的同学可以在评论区说下。
绑定大核是否有必要?
cas:/sys/devices/system/cpu $ cat cpu0/cpufreq/cpuinfo_max_freq
1804800
cas:/sys/devices/system/cpu $ cat cpu1/cpufreq/cpuinfo_max_freq
1804800
cas:/sys/devices/system/cpu $ cat cpu2/cpufreq/cpuinfo_max_freq
1804800
cas:/sys/devices/system/cpu $ cat cpu3/cpufreq/cpuinfo_max_freq
1804800
cas:/sys/devices/system/cpu $ cat cpu4/cpufreq/cpuinfo_max_freq
2419200
cas:/sys/devices/system/cpu $ cat cpu5/cpufreq/cpuinfo_max_freq
2419200
cas:/sys/devices/system/cpu $ cat cpu6/cpufreq/cpuinfo_max_freq
2419200
cas:/sys/devices/system/cpu $ cat cpu7/cpufreq/cpuinfo_max_freq
2841600
绑定大核实现
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);
}
}
参数 1 是线程的 id,如果为 0 则表示主线程。 参数 2 表示 cpu 序列掩码的长度。 参数 3 则表示需要绑定的 cpu 序列的掩码。
具体代码就不在这里粘贴了,完整代码可见文末链接。
GC 抑制是否有必要?
Google 也注意到启动阶段 GC 对启动速度的影响,并在 Android 10 之后做了一定的优化,详情可见如下提交:
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 抑制实现
HeapTaskDaemon 执行流程
从HeapTaskDaemon.runInternal()方法开始一步步调用到 native 层的 task_processor.RunAllTasks() 方法。 当TaskProcessor中的tasks为空时,会休眠等待,否则会取出第一个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;
};
方案设计
启动时将虚函数表中的 Run 函数地址替换为自定义函数地址。 在自定义函数内部休眠一段时间,抑制 GC。 休眠完成后将虚函数表中的函数地址替换回来,避免影响后续执行。
具体实现
获取函数符号。 根据函数符号获取函数地址。
adb pull /system/lib64/libart.so // Android 10 以前系统,Android 10 之后换了位置
aarch64-linux-android-readelf -s --wide libart.so
_ZTVN3art2gc4Heap16ConcurrentGCTaskE // ConcurrentGCTask
_ZN3art2gc4Heap16ConcurrentGCTask3RunEPNS_6ThreadE // Run 方法
void delayGC() {
//以RTLD_NOW模式打开动态库libart.so,拿到句柄,RTLD_NOW即解析出每个未定义变量的地址
void *handle = enhanced_dlopen("/system/lib64/libart.so", RTLD_NOW);
//通过符号拿到ConcurrentGCTask对象地址
void *taskAddress = enhanced_dlsym(handle, "_ZTVN3art2gc4Heap16ConcurrentGCTaskE");
//通过符号拿到run方法
void *runAddress = enhanced_dlsym(handle, "_ZN3art2gc4Heap16ConcurrentGCTask3RunEPNS_6ThreadE");
//由于 ConcurrentGCTask 只有五个虚函数,所以我们只需要查询前五个地址即可。
for (size_t i = 0; i < 5; i++) {
//对象头地址中的内容存放的就是虚函数表的地址,所以这里是指针的指针,即是虚函数表地址,拿到虚函数表地址后,转换成数组,并遍历获取值
void *vfunc = ((void **) taskAddress)[i];
// 如果虚函数表中的值是前面拿到的 Run 函数的地址,那么就找到了Run函数在虚函数表中的地址
if (vfunc == runAddress) {
//这里需要注意的是,这里 +i 操作拿到的是地址,而不是值,因为这里的值是 Run 函数的真实地址
mSlot = (void **) taskAddress + i;
}
}
// 保存原有函数
originFun = *mSlot;
// 将虚函数表中的值替换成我们hook函数的地址
replaceFunc(mSlot, (void *) &hookRun);
}
//我们的 hook 函数
void hookRun(void *thread) {
//休眠3秒
sleep(3);
//将虚函数表中的值还原成原函数,避免每次执行run函数时都会执行hook的方法
replaceFunc(mSlot, originFun);
//执行原来的Run方法
((void (*)(void *)) originFun)(thread);
}
通过符号获取Run方法地址。 遍历虚函数表,找到虚函数表中存放Run方法真实地址的位置。 保存原函数地址,并将虚函数表中的值替换成我们 hook 的函数地址。 在 hook 函数中休眠一段时间,休眠结束后还原虚函数表,避免影响后续任务。
参考资料
源码
本文所有源码可见:
https://github.com/RicardoJiang/android-performance
明天早上赶火车,周五我就不推文了哈,提前祝大家五一快乐!
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!