前言

发动速度优化是 android 开发中的常见需求,除了一些惯例的手法之外,也有一些黑科技手法,咱们来看一下这些黑科技手法是否有效,以及怎样完成

本文首要是对Android 功能优化小册相关内容的学习实践,加入了自己的了解与实践内容,感兴趣的同学能够点击检查小册

线程优先级设置

线程优先级设置的概念很简单了解,优先级越高的线程越简单获取 CPU 时刻片,那么为了保证 app 的流畅运转,那么咱们就应该将中心线程的优先级进步,而将其他线程的优先级调低

关于 app 来说,中心线程便是主线程 + RenderThread,那么咱们是否有必要手动设置线程优先级呢?

咱们能够经过以下指令获取当时的线程优先级

adb shell
ps -A | grep 包名 // 依据包名找到Pid
top -H -p PID    // 检查线程优先级指令

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

运转成果如上图所示,要看懂上面的图咱们要先了解一点布景常识

  • PR: 优先级 (priority),值越小优先级越高,会受NI的值的影响
  • NI: 即 Nice 值,咱们能够经过Process.setThreadPriority设置,同样是值越小优先级越高

其实咱们只需求知道它们都是值越小优先级越高就好了,能够看出主线程与 RenderThread 的优先级都挺高的,仅次于 Binder 线程

我看到一些发动优化的文章谈到线程优先级设置,但测验成果似乎是没有必要?难道是版本问题?有了解的同学能够在评论区说下

中心线程绑定大核

绑定大核是否有必要?

中心线程绑定大核的思路也很简单了解,现在的 CPU 都是多核的,大核的频率比小核要高不少,假如咱们的中心线程固定运转在大核上,那么应用功能自然会有所提高

就拿我手上的小米10来说,运用的是骁龙865的 CPU,由一颗A77超大核+三颗A77大核+四颗A55小中心组成,咱们能够经过/sys/devices/system/cpu/目录下的文件获取各个核的频率,如下所示

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

能够看出,cpu0 到 cpu3 是4个小核, cpu4 到 cpu6 是3个大核,cpu7 是超大核,它们之间的频率仍是相差挺大的

一起经过 systrace 东西能够发现,主线程基本都运转在 cpu7 这个超大核上,而 RenderThread 会在 cpu4 到 cpu6 间切换,有时甚至会调度到小核上

因而能够看出仍是有必要把 RenderThread 绑定到一个大核上的,绑定能够更好的利用缓存以及削减线程的上下文切换

绑定大核完成

绑定大核是经过函数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);
    }
}

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

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

以上是线程绑定大核的中心代码,能够看到咱们还需求获取 RenderThread 的 id ,以及 cpu 大核的序列

应用中线程的信息记录在 /proc/pid/task 的文件中,经过解析 task 文件就能够获取当时进程的一切线程,而 cpu 大核序列也能够经过解析 /sys/devices/system/cpu 目录完成

详细代码就不在这儿粘贴了,完整代码可见文末链接

GC 按捺

GC 按捺是否有必要?

咱们知道 Java 的拉圾收回机制,在 Android 5.0 之后,ART 取代了 Dalvik,ART 虚拟机在废物收回的时分虽然没有像 Dalvik 一样 stop the world,但在发动阶段假如发生废物收回,GC 线程同样抢占了不少体系资源

Google 也留意到发动阶段 GC 对发动速度的影响,并在 Android 10 之后做了必定的优化,概况可见如下提交:cs.android.com/android/_/a…

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

能够看出,基本思路是在 2s 内进步后台 GC 的阈值,削减发动阶段的 GC 次数,依据 Google 的测验,按捺 GC 后效果如下

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

能够看出,GC 次数显着削减,发动速度也有必定的提高。那么咱们是不是能够运用一些手法让 Android 10 以下也能完成这个效果呢?

一起咱们也能够经过以下代码获取 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 按捺完成

HeapTaskDaemon 履行流程

GC 首要是经过 HeapTaskDaemon 线程完成的,这是一个守护线程,在 Zygote 线程发动后这个线程也就发动了,发动后首要做了以下工作

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

  1. HeapTaskDaemon.runInternal()办法开始一步步调用到 native 层的 task_processor.RunAllTasks() 办法
  2. TaskProcessor中的tasks为空时,会休眠等待,否则会取出第一个HeapTask并履行其Run办法

HeapTaskRun办法是一个虚函数,需求子类来完成

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

方案设计

在了解了 HeapTaskDaemon 的履行流程之后,咱们想到,假如发动时在ConcurrentGCTaskRun办法履行前休眠一段时刻,不就能够完成 GC 按捺了吗?

Run办法正好是虚函数,虚函数与 Java 中的笼统函数类似,留给子类去扩展完成多态

虚函数和外部库函数一样都没法直接履行,需求在表中去查找函数的实在地址,那么咱们是不是能够运用类似 PLT Hook的思路,运用自定义函数的地址替换原有函数地址,完成 Hook 呢?

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

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

因而咱们的首要思路如下:

  1. 发动时将虚函数表中的 Run 函数地址替换为自定义函数地址
  2. 在自定义函数内部休眠一段时刻,按捺 GC
  3. 休眠完成后将虚函数表中的函数地址替换回来,防止影响后续履行

详细完成

很显然要想完成 Hook,咱们首先需求获取ConcurrentGCTask目标地址与其Run办法地址

那么咱们能够怎样获取办法地址呢?

dlopen 函数和 dlsym 能够用于打开动态链接库中的函数,经过函数的符号回来函数地址

因而咱们需求做下面两件事

  1. 获取函数符号
  2. 依据函数符号获取函数地址
adb pull /system/lib64/libart.so // Android 10 曾经体系,Android 10 之后换了方位
aarch64-linux-android-readelf -s --wide libart.so

经过以上办法能够导出 so 中的符号,查找到成果如下

_ZTVN3art2gc4Heap16ConcurrentGCTaskE   // ConcurrentGCTask
_ZN3art2gc4Heap16ConcurrentGCTask3RunEPNS_6ThreadE // Run 办法

能够看出,符号是本来的姓名做了必定的变换,这是 c++ 的 name mangling 机制,mangling 的意图便是为了给重载的函数不同的签名,详细的规则能够自行查阅,这儿就不赘述了

还有需求留意的一点是,Android 7.0 以上对 dlsym 的调用有限制,一起从 aarch64-linux-android-readelf 的成果能够看出, ConcurrentGCTask 在 .dynsym 段,而 Run 办法在 .symtab 段,而 dlsym 只能查找 .dynsym 段,而无法查找 .symtab 段,因而咱们这儿运用enhanced_dlsym开源库,既支撑 Android 7.0 以上调用,也能够查找 .stymtab 段

好了,前置常识讲完了,下面来看下代码

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);
}

中心代码便是上面这些,首要做了这么几件事:

  1. 经过符号获取Run办法地址
  2. 遍历虚函数表,找到虚函数表中寄存Run办法实在地址的方位
  3. 保存原函数地址,并将虚函数表中的值替换成咱们 hook 的函数地址
  4. 在 hook 函数中休眠一段时刻,休眠完毕后复原虚函数表,防止影响后续任务

总结

在功能优化中除了一些惯例手法外,也经常有一些黑科技手法,本文首要介绍了发动优化中的线程优先级设置,中心线程绑定大核,GC 按捺等手法, 讲解了一下这些黑科技手法是否有效,以及详细是怎样完成的,希望对你有所协助

参考资料

本文首要是对Android 功能优化小册相关内容的学习实践,加入了自己的了解与实践内容,感兴趣的同学能够点击检查小册

源码

本文一切源码可见:github.com/RicardoJian…

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。