Android 4.x 系统虚拟内存治理方案

本文介绍了针对Android4.x系统中的Webview组件和无用字体库的内存优化方法,通过分析`/proc/self/smap`和深入研究`libwebviewchromium.so`,作者展示了如何利用`dlclose`和`structsoinfo`中的`ref_count`来释放内存,并详细描述了针对不同版本Android的字体加载机制,最终通过hookfopen和open函数来阻止不必要的字体库加载,节省系统资源。
摘要由CSDN通过智能技术生成

业界治理虚拟内存的方案大多数是针对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;
}

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题
图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值