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

news2024/12/24 3:04:18

1前言

启动速度优化是 android 开发中的常见需求,除了一些常规的手段之外,也有一些黑科技手段,我们来看一下这些黑科技手段是否有效,以及如何实现

本文主要是对Android 性能优化小册相关内容的学习实践,加入了自己的理解与实践内容,感兴趣的同学可以点击查看小册。

2线程优先级设置

线程优先级设置的概念很容易理解,优先级越高的线程越容易获取 CPU 时间片,那么为了保证 app 的流畅运行,那么我们就应该将核心线程的优先级提高,而将其他线程的优先级调低。对于 app 来说,核心线程就是主线程 + RenderThread,那么我们是否有必要手动设置线程优先级呢?我们可以通过以下命令获取当前的线程优先级。

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

运行结果如上图所示,要看懂上面的图我们要先了解一点背景知识。

  • PR: 优先级 (priority),值越小优先级越高,会受NI的值的影响。
  • NI: 即 Nice 值,我们可以通过Process.setThreadPriority设置,同样是值越小优先级越高。

其实我们只需要知道它们都是值越小优先级越高就好了,可以看出主线程与 RenderThread 的优先级都挺高的,仅次于 Binder 线程。

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

3核心线程绑定大核

绑定大核是否有必要?

核心线程绑定大核的思路也很容易理解,现在的 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 目录实现。

具体代码就不在这里粘贴了,完整代码可见文末链接。

4GC 抑制

GC 抑制是否有必要?

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

Google 也注意到启动阶段 GC 对启动速度的影响,并在 Android 10 之后做了一定的优化,详情可见如下提交:

可以看出,基本思路是在 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方法。

而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。

方案设计

在了解了 HeapTaskDaemon 的执行流程之后,我们想到,如果启动时在ConcurrentGCTask的Run方法执行前休眠一段时间,不就可以实现 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 函数中休眠一段时间,休眠结束后还原虚函数表,避免影响后续任务。

5总结

在性能优化中除了一些常规手段外,也经常有一些黑科技手段,本文主要介绍了启动优化中的线程优先级设置,核心线程绑定大核,GC 抑制等手段, 讲解了一下这些黑科技手段是否有效,以及具体是怎么实现的,希望对你有所帮助。

为了帮助到大家更好的全面清晰的掌握好性能优化,准备了相关的核心笔记(还该底层逻辑):https://qr18.cn/FVlo89

性能优化核心笔记:https://qr18.cn/FVlo89

启动优化

内存优化

UI优化

网络优化

Bitmap优化与图片压缩优化https://qr18.cn/FVlo89

多线程并发优化与数据传输效率优化

体积包优化

《Android 性能监控框架》:https://qr18.cn/FVlo89

《Android Framework学习手册》:https://qr18.cn/AQpN4J

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/745942.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

腾讯云服务器CVM实例族区别如何选择?

腾讯云服务器CVM有多种实例族&#xff0c;如标准型S6、标准型S5、SA3实例、高IO型、内存、计算型及GPU型实例等&#xff0c;如何选择云服务器CVM实例规格呢&#xff1f;腾讯云服务器网建议根据实际使用场景选择云服务器CVM规格&#xff0c;例如Web网站应用可以选择标准型S5或S6…

linux 读取文件,并输出含空格的每一行

[devusercdp-node12 ~]$ cat test.sh 1234 aaaa 2345 bbbb 3456 cccc 需求 输出三行 分别是 aaaa bbbb cccc 咋一看很简单&#xff0c;实则里面有很多小问题。 rescat test.sh 这个时候思考下 echo $res和 echo "$res"有啥区别 快速的写出for循环 结果 for i in …

Web3 开发指南:使用 NFTScan NFT API 构建一个 NFT 链上追踪器

对于大多数 Web3 团队来说&#xff0c;构建一个完整的链上 NFT 数据追踪系统是一项具有挑战性的任务&#xff0c;构建一个 NFT 链上追踪器更是如此。涉及到处理区块链上的智能合约和交易数据&#xff0c;并将其与外部数据源进行整合和分析工作量是十分巨大的&#xff1a; 区块链…

图像分类——ResNet

目录 残差块ResNet模型手写数字识别 残差块 左图残差块实现如下 import tensorflow as tf from tensorflow.keras import layers,activations#残差块 class Residul(tf.keras.Model):def __init__(self,num_channels,use_1x1convFalse,strides1):super(Residul,self).__init_…

2023年最值得关注的APP开发工具

随着越来越多的开发人员转向移动应用程序开发&#xff0c;行业和企业都在寻找最好的工具来帮助他们。随着市场的变化&#xff0c;客户越来越注重质量和开发效率。 无论您是要创建一个全新的 APP&#xff0c;还是要寻找一种可快速部署的工具来满足您的需求&#xff0c;这篇文章…

RT1176 将代码放到RAM上运行

为了实现IAP固件升级&#xff0c;需要擦写Flash。如果代码在Nor Flash上运行&#xff0c;同时擦写该Flash&#xff0c;代码就会乱了无法正常执行。所以我们要先将代码放到RAM上运行。 但又不能像官方SDK Demo那样将代码直接烧录到RAM上或SDRAM上&#xff0c;否则掉电后代码丢失…

CSS样式表的学习

CSS样式&#xff0c;在前面学习HTML的结构时&#xff0c;也讲过一点。大概知道CSS样式如何使用。现在&#xff0c;系统的讲述一下CSS的用法&#xff0c;以及关于CSS样式的一些结构。 参考文章&#xff1a;CSS 背景图像重复 (w3schools.com) 一、CSS介绍 CSS 代表 层叠样式表…

蚂蚁集团开源可信隐私计算框架「隐语」:开放、通用

7 月 4 日,蚂蚁集团宣布面向全球开发者正式开源可信隐私计算框架 “隐语”。 隐语是蚂蚁集团历时 6 年自主研发,以安全、开放为核心设计理念打造的可信隐私计算技术框架,涵盖了当前几乎所有主流隐私计算技术。 据介绍,隐语内置 MPC、TEE、同态等多种密态计算虚拟设备,提…

【Java实现AES加解密】

Java实现AES加解密 这篇文章主要介绍了使用Java实现加密之AES加解密,AES为最常见的对称加密算法,对称加密算法也就是加密和解密用相同的密钥,需要的朋友可以参考下在Vue中&#xff0c;Vue加密&#xff1a;Java解密&#xff1a; 这篇文章主要介绍了使用Java实现加密之AES加解密,…

Linux开发工具【vim】

Linux开发工具之【vim】 文章目录&#xff1a; Linux开发工具之【vim】1. Linux软件包管理器yum1.1 查看软件1.2. 下载软件1.3 卸载软件 2. vim编辑器的使用2.1 vim常用模式2.2 vim基本操作2.3 vim命令模式命令集2.3.1 移动光标2.3.2 删除文字2.3.3 复制文本内容2.3.4 替换文本…

设计模式——组合模式

组合模式 定义 组合模式&#xff08;Composite Pattern&#xff09;又称为合成模式、部分-整体模式&#xff08;Part-Whole&#xff09;&#xff0c;主要用来描述部分与整体的关系。 定义&#xff1a;将对象组合成树形结构以表示“部分-整体”的层次结构&#xff0c;使用户对…

【C#】并行编程实战:同步原语(2)

在第4章中讨论了并行编程的潜在问题&#xff0c;其中之一就是同步开销。当将工作分解为多个工作项并由任务处理时&#xff0c;就需要同步每个线程的结果。线程局部存储和分区局部存储&#xff0c;某种程度上可以解决同步问题。但是&#xff0c;当数据共享时&#xff0c;就需要用…

电商项目“商品分类浏览”如何测试?附详细思维导图

电商项目无论是工作中&#xff0c;还是面试中&#xff0c;都是一个高频出现的词。面试官非常热衷提问关于电商项目的问题。例如商品分类怎么测试&#xff1f;购物车怎么测试&#xff1f;订单怎么测试&#xff1f;优惠券怎么测试&#xff1f;支付怎么测试&#xff1f;等等 今天…

【Spring Cloud系列】- Ribbon详解与实战

【Spring Cloud系列】- Ribbon详解与实战 文章目录 【Spring Cloud系列】- Ribbon详解与实战一、什么是Ribbon二、Spring Cloud中Ribbon应用Ribbon使用步骤如下 三、Ribbon负载均衡策略设置3.1 全局策略设置3.2 基于注解的针对单个服务的 Ribbon 负载均衡策略3.2.1 注解方式3.2…

『赠书活动 | 第十五期』《Java核心技术·卷II》

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; 『赠书活动 &#xff5c; 第十五期』 本期书籍&#xff1a;《Java核心技术卷II》 公众号赠书&#xff1a;第六期 参与方式&#xff1a;关注公众号&#xff1a;低调而…

折叠屏手机的屏幕,华为Mate X3给出了一份“内外兼修”的解决方案

说起折叠屏手机&#xff0c;屏幕一直都是这个领域的重头戏&#xff0c;很多人都对折叠屏手机有一种刻板印象&#xff0c;那就是脆弱。但是&#xff0c;3月份华为最新推出的Mate X3可以说是非常的亮眼&#xff0c;在内外屏幕、水滴铰链、影像系统等多个核心部件的全方位提升&…

MKPFMSEG字段增强后 关于 BAPI_GOODSMVT_CREATE 的extensionin的增强参数带入。

首先在MKPF 的增强表结构 BAPI_TE_XMKPF 增强字段 如果是行项目上的 BAPI_TE_XMSEG 然后在实际赋值的时候 前面14位数是 10位物料凭证号 4位 年度 CLEAR: ls_extensionin.ls_extensionin-STRUCTURE BAPI_TE_XMKPF.ls_extensionin-valuepart114(50) ls_…

告诉你有哪些音频转换成mp3免费软件

曾经有一个名叫小华的音乐爱好者&#xff0c;他对于收集和欣赏各种类型的音频文件情有独钟。然而&#xff0c;他在构建自己的音乐库时遇到了一个困扰&#xff1a;不同设备和平台支持的音频格式千差万别&#xff0c;这让他无法顺利地播放和分享自己喜爱的音乐。幸运的是&#xf…

GTK列表显示文本和图片

使用GtkTreeView控件显示包含文本和图片的列表&#xff0c;GtkTreeView/GtkListStore或者GtkTreeView/GtkTreeModel使用的是MVC设计理念。 关于MVC: M层: model 数据模型层(处理数据的增删改查) 提供数据 V层: Views 视图层 (数据展示) 渲染数据 C层: controller 控制层(处理业…

【UI框架-uView】Input输入框如何使用前后槽?

【UI框架-uView】Input输入框如何使用前后槽&#xff1f; 官方链接如下&#xff1a;Input输入框 - 前后槽 前槽如下&#xff1a; 后槽如下&#xff1a; 可以看到&#xff0c;前后槽的灵活使用&#xff0c;不仅方便我们的布局&#xff0c;还可以在input中实现复杂的应用。 注意…