昨天再次翻看 ARouter 时,留意到了作者 zhi1ong 的又一开源项目 Patrons。
被 README 中的一段描述惊呆:大型 32 位 Android 应用稳定性提升 50% 的“黑科技”,于是压抑不住内心的好奇心去学习一波这稳定性提升 50% 的黑科技,虽然手头的项目已不再支持 32 位,短期内用不上 Partons ,但毕竟是 ARouter 的作者出品,同时也收归到了阿里的开源库中,其中肯定少不了干货。
原文的描述相对专业且简洁,读起来有些费力,本文试图以一个新手司机的角度去理解这个 Patrons 用的什么方法?解决了什么问题?
背景
Android 中莫名的 Native Crash 问题实在让人头痛,正如《阿里开源 Patrons: 大型 32 位 Android 应用稳定性提升 50% 的“和科技”》所述,Native Crash 中的 TOP 问题均可以通过解决 Native虚拟内存不足 这个问题来缓解。
所以 Patrons 是通过处理 Native 虚拟内存不足来降低 Native Crash 率的。
方案
针对虚拟内存不足问题,文中给出了两种解决方案:
方案一:替换回收效率更高的内存分配器,加快 Native 虚拟内存回收。
zhi1ong 在尝试方案一的过程中发现主要会面临如下两个问题:
- 替换内存分配器需要Hook多个函数;
- 新旧内存分配器在分配内存时,存在重复分配同一块内存的冲突,Hook方案只能通过添加回收逻辑去保证新内存分配器分配的内存的可靠性,却无法保证内置内存分配器对内存的破化。
方案二:减少虚拟机的虚拟内存(虚拟机中的堆、栈空间都在此区域内),将虚拟机腾出的虚拟内存空间供 Native 使用。
Patrons
Patrons 采用的就是方案二的思路。
原文中有这样一张图片,简明介绍了 Patron 的核心思路。zhi1ong 大佬确实厉害,感受一下大佬的基础功!!
这张图要是竖着来的,估计会更清晰了:),课本里大家都学过 32 位系统中进程寻址空间是 2^32=4GB,低地址的 1GB 留给操作系统,剩下的3GB留给作为用户空间。
对于一个开启了 largeHeap 的应用进程来说,如图:
1、剩下的3GB里,最低位的1GB分配给 JVM 虚拟机;
2、紧接着的300MB为显存空间;
3、然后是非显存的其他设备文件或资源空间;
4、最剩下的虚拟内存空间均是 Native 可用的虚拟内存空间 (图中示意有200M虚拟空间已使用,最高位附近的栈空间应该是没有画出来);
Patrons 通过 Hook 调用内存分配中RegionSpace类的ClampGrowthLimit方法来降低虚拟机的虚拟内存空间,回收(Steal Space)虚拟空间并交给内存分配器管理供 Native 使用,缓解 Native 虚拟内存不足问题。
至于怎么Hook的可能需要对 ELF文件的编译、链接和加载有比较扎实的理解,这里推荐一本经典的计算机书籍《程序员的自我修养--链接、装载与库》
Patrons 项目的代码极少,有兴趣的同学可以自行去了解。
可行性
为什么通过 Hook RegionSpace类的ClampGrowthLimit方法就能解决 Native 虚拟内存不足的问题呢?
《快速缓解 32 位 Android 环境下虚拟内存地址空间不足的“黑科技”》在源码层面对 Patrons 做了更多的解释。 在阅读这篇文章之前那,建议先阅读 《Android GC 简史》。
上图中的 Unmapped Page 就是 Patrons 中的 Steal Space 。
我们需要带着几个问题去进一步了解方案的可行性:
问题一:重新分配内存是否会破化运行中的虚拟机?
这就需要了解一个这个比较关键的函数ClampGrowthLimit:
RegionSpace会先将内存资源划分为一个个的固定大小(由下面函数中的kRegionSize指定,默认为 1M)的内存块,每个内存块用一个Region对象表示;
下面是
void RegionSpace::ClampGrowthLimit(size_t new_capacity) {
MutexLock mu(Thread::Current(), region_lock_);
//上限越界检测
CHECK_LE(new_capacity, NonGrowthLimitCapacity());
//新的region总数
size_t new_num_regions = new_capacity / kRegionSize;
//non_free_region_index_limit_ 正在使用的region数量
if (non_free_region_index_limit_ > new_num_regions) {
LOG(WARNING) << "Couldn't clamp region space as there are regions in use beyond growth limit.";
return;
}
// 后续逻辑会将Unmapped Page释放
...
}
上述讲到的1GB虚拟机的虚拟内存也就是这里的 new_capacity,现在要做的就是在某个时机,重新传入一个比1GB小的数值即可。
如果重新分配时,新的Region总数小于已使用的Region数量,则会直返回,这样避免了重新分配对已使用内存的破坏,所以不会破会运行中的虚拟机。
问题二:Native申请内存时,怎样能识别低地址中可用 Region,并且不破化原有数据的
这个问题《快速缓解 32 位 Android 环境下虚拟内存地址空间不足的“黑科技”》已讲得足够清楚,非空闲的Region不会被重新使用,每次查找空闲 Region的时候都是从第一个Region开始,
关于ART内存,墙裂推荐读本《深入理解Android Java虚拟机ART》
总结
Patrons 是通过牺牲 ART 的虚拟内存来缓解 Native 虚拟地址不足问题,有点拆东墙补西墙的意思了,所以在使用时还是需要确实一下自己在做什么。在 64 位的 Android 系统的寻址空间足够大,因此没必要这么做了,如果再强行操作,缩小 ART 的虚拟内存带来的问题可能更大。
虽然和目前的所在业务没啥关系,但这并不是拒绝学习的借口~
推荐阅读
《阿里开源 Patrons: 大型 32 位 Android 应用稳定性提升 50% 的“和科技”》