业界治理虚拟内存的方案大多数是针对Android 5及以上机型,如:阿里巴巴开源Patrons方案。针对Android 4.x的治理方案少之又少,故此专门针对Android 4.x 系统虚拟内存治理提上了日程。
开释webview组件
仔细研究/proc/self/smap,因为APP应用没有使用到webview组件,占用18M的libwebviewchromium.so便映入眼帘。
71fc2000-7317a000 r-xp 00000000 b3:19 903 /system/lib/libwebviewchromium.so
Size: 18144 kB
Rss: 0 kB
Pss: 0 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 0 kB
Anonymous: 0 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Locked: 0 kB
那如何开释这部分虚拟内存内?看到so,自然而言想到dl家族的dlclose函数。
#include <dlfcn.h>
int
dlclose(void* handle);
然而dl家族函数存在一个特性就是引用计数,dlclose调用一次引用计数减1,直到引用计数减到0时才会真正的卸载so,因此只有正确的读取到引用计数,才能完全卸载do。
那我们只能到Anroid源码中寻找答案了。
struct soinfo {
public:
char name[SOINFO_NAME_LEN];
const Elf32_Phdr* phdr;
size_t phnum;
Elf32_Addr entry;
Elf32_Addr base;
unsigned size;
uint32_t unused1; // DO NOT USE, maintained for compatibility.
Elf32_Dyn* dynamic;
uint32_t unused2; // DO NOT USE, maintained for compatibility
uint32_t unused3; // DO NOT USE, maintained for compatibility
soinfo* next;
unsigned flags;
const char* strtab;
Elf32_Sym* symtab;
size_t nbucket;
size_t nchain;
unsigned* bucket;
unsigned* chain;
unsigned* plt_got;
Elf32_Rel* plt_rel;
size_t plt_rel_count;
Elf32_Rel* rel;
size_t rel_count;
linker_function_t* preinit_array;
size_t preinit_array_count;
linker_function_t* init_array;
size_t init_array_count;
linker_function_t* fini_array;
size_t fini_array_count;
linker_function_t init_func;
linker_function_t fini_func;
#if defined(ANDROID_ARM_LINKER)
// ARM EABI section used for stack unwinding.
unsigned* ARM_exidx;
size_t ARM_exidx_count;
#elif defined(ANDROID_MIPS_LINKER)
unsigned mips_symtabno;
unsigned mips_local_gotno;
unsigned mips_gotsym;
#endif
size_t ref_count;
link_map_t link_map;
bool constructors_called;
// When you read a virtual address from the ELF file, add this
// value to get the corresponding address in the process' address space.
Elf32_Addr load_bias;
bool has_text_relocations;
bool has_DT_SYMBOLIC;
void CallConstructors();
void CallDestructors();
void CallPreInitConstructors();
private:
void CallArray(const char* array_name, linker_function_t* functions, size_t count, bool reverse);
void CallFunction(const char* function_name, linker_function_t function);
};
对比4.1、4.2、4.3、4.4的 struct soinfo 定义可知,Google针对ref_count进行了位置对齐,即在4.x系列机型上,offsetof(struct soinfo, ref_count)为固定值(256),根据固定偏移,即可读到引用计数数值,即可知道调用dlclose的次数,废话不多说,直接上代码验证:
#ifdef __arm__
#define SO_INFO_REFCOUNT_OFFSET 256
#else
#define SO_INFO_REFCOUNT_OFFSET 248
#endif
void Android4_webview(bool k)
{
std::vector<std::string> libs;
libs.reserve(2);
if (k)
{
libs.emplace_back("/system/lib/libwebviewchromium_plat_support.so");
libs.emplace_back("/system/lib/libwebviewchromium.so");
}
else
{
libs.emplace_back("/system/lib/libwebcore.so");
libs.emplace_back("/system/lib/libchromium_net.so");
}
for (auto lib : libs)
{
void* handle = dlopen(lib.data(), RTLD_LAZY);
if (handle != nullptr) {
while(1)
{
unsigned refcount = *((unsigned*) ((unsigned) handle + SO_INFO_REFCOUNT_OFFSET));
if (refcount > 0)
{
LOG("----- library : %s refcount : %d", lib.data(), refcount);
dlclose(handle);
}
else
{
break;
}
}
}
}
}
再次读取/proc/self/smap可以看到zygote引入的libwebviewchromium.so被我们完全释放了,而且只要我们保证不做任何webview操作的,AUTO不会出现任何异常,perfect!!!
开释中英文外其他字体库
仔细研究/proc/self/smap,可以看到加载了好多其他语种字体库,汇总下来mmap了大概2M左右虚拟内存,像尼泊尔语、泰语这些语种对AUTO没什么意义,只是白白占用了系统虚拟内存,然后观看zygote smap可以推断出除Roboto-Regular.ttf外其他字体库都是在apk中加载的,这样就给了我们拦截其他字体库加载的机会。
cat /proc/30550/smaps | grep ttf
71cdd000-71cfa000 r--p 00000000 b3:19 537 /system/fonts/Roboto-Regular.ttf
758d3000-758f0000 r--p 00000000 b3:19 532 /system/fonts/Roboto-Bold.ttf
758f0000-7590c000 r--p 00000000 b3:19 476 /system/fonts/DroidNaskhUI-Regular.ttf
7590c000-75944000 r--p 00000000 b3:19 480 /system/fonts/DroidSansEthiopic-Regular.ttf
75944000-7594c000 r--p 00000000 b3:19 483 /system/fonts/DroidSansHebrew-Bold.ttf
7594c000-75951000 r--p 00000000 b3:19 528 /system/fonts/NotoSansThaiUI-Bold.ttf
75951000-75955000 r--p 00000000 b3:19 479 /system/fonts/DroidSansArmenian.ttf
75955000-7595b000 r--p 00000000 b3:19 482 /system/fonts/DroidSansGeorgian.ttf
7595b000-75979000 r--p 00000000 b3:19 499 /system/fonts/NotoSansDevanagariUI-Bold.ttf
75979000-75983000 r--p 00000000 b3:19 520 /system/fonts/NotoSansTamilUI-Bold.ttf
75983000-75991000 r--p 00000000 b3:19 515 /system/fonts/NotoSansMalayalamUI-Bold.ttf
75991000-759ab000 r--p 00000000 b3:19 495 /system/fonts/NotoSansBengaliUI-Bold.ttf
759ab000-759c7000 r--p 00000000 b3:19 524 /system/fonts/NotoSansTeluguUI-Bold.ttf
759c7000-759da000 r--p 00000000 b3:19 503 /system/fonts/NotoSansKannadaUI-Bold.ttf
759da000-759e5000 r--p 00000000 b3:19 507 /system/fonts/NotoSansKhmerUI-Bold.ttf
759e5000-759ee000 r--p 00000000 b3:19 511 /system/fonts/NotoSansLaoUI-Bold.ttf
759ee000-75b4b000 r--p 00000000 b3:19 491 /system/fonts/NanumGothic.ttf
75b4b000-75bb7000 r--p 00000000 b3:19 531 /system/fonts/Padauk-bookbold.ttf
75bb9000-75bba000 r--p 00000000 b3:19 517 /system/fonts/NotoSansSymbols-Regular.ttf
75bba000-75bbb000 r--p 00000000 b3:19 473 /system/fonts/AndroidEmoji.ttf
75bbc000-75ee9000 r--p 00000000 b3:19 492 /system/fonts/NotoColorEmoji.ttf
75eea000-7633c000 r--p 00000000 b3:19 481 /system/fonts/DroidSansFallback.ttf
76342000-7634a000 r--p 00000000 b3:19 484 /system/fonts/DroidSansHebrew-Regular.ttf
7634b000-76351000 r--p 00000000 b3:19 529 /system/fonts/NotoSansThaiUI-Regular.ttf
76352000-76371000 r--p 00000000 b3:19 500 /system/fonts/NotoSansDevanagariUI-Regular.ttf
76372000-7637b000 r--p 00000000 b3:19 521 /system/fonts/NotoSansTamilUI-Regular.ttf
7637b000-76389000 r--p 00000000 b3:19 516 /system/fonts/NotoSansMalayalamUI-Regular.ttf
7638a000-763a5000 r--p 00000000 b3:19 496 /system/fonts/NotoSansBengaliUI-Regular.ttf
763a6000-763c2000 r--p 00000000 b3:19 525 /system/fonts/NotoSansTeluguUI-Regular.ttf
763c3000-763d7000 r--p 00000000 b3:19 504 /system/fonts/NotoSansKannadaUI-Regular.ttf
763d8000-763e3000 r--p 00000000 b3:19 508 /system/fonts/NotoSansKhmerUI-Regular.ttf
763e3000-763ec000 r--p 00000000 b3:19 512 /system/fonts/NotoSansLaoUI-Regular.ttf
763ed000-76462000 r--p 00000000 b3:19 530 /system/fonts/Padauk-book.ttf
追踪Android源码,推断出libskia.so进行具体的字体库的加载和内存映射。
nm -D libskia.so | grep mmap
0024b6e8 T _Z8sk_fmmapP6SkFILEPj
0024b664 T _Z9sk_fdmmapiPj
U mmap@LIBC
nm -D libskia.so | grep open
0024b970 T _Z8sk_fopenPKc12SkFILE_Flags
00186f94 T _ZNK10SkTypeface10openStreamEPi
0024df40 T _ZNK19SkTypeface_FreeType7Scanner8openFaceEP8SkStreamiP13FT_StreamRec_
U fdopen@LIBC
U fopen@LIBC
U open@LIBC
U opendir@LIBC
U ucnv_open_55
到底是通过fopen还是open打开ttf文件:
(1)阅读skia源码
(2)hook mmap然后进行栈回溯可以精准推断
(3)直接hook fopen或者open,打印filename
最终结论:Android 4.4通过fopem方式打开ttf文件,Android 4.3通过open方式打开ttf文件,代理函数发现是中英文之外的其他语种字体直接返回(错误)即可,最终阻断了无用字体库的加载,节省了系统虚拟内存。
FILE* fopen_proxy(const char* filename, const char* mode)
{
if (filename == nullptr)
{
return nullptr;
}
LOG("fopen_proxy begin");
LOG("fopen_proxy filename : %s", filename);
FILE* ret = nullptr;
if (strstr(filename, ".ttf") != nullptr)
{
if (strstr(filename, "Roboto-Regular.ttf") != nullptr
|| strstr(filename, "Roboto-Bold.ttf") != nullptr
|| strstr(filename, "DroidSansFallback.ttf") != nullptr)
{
PLTHOOK_CALL_ORIG_FUNC_SAFE(fopen_proxy, ret, filename, mode);
}
}
else
{
PLTHOOK_CALL_ORIG_FUNC_SAFE(fopen_proxy, ret, filename, mode);
}
LOG("fopen_proxy end");
return ret;
}