插件化so库加载原理及实现

系统加载 so 库的工作流程

当我们调用当调用 System#loadLibrary("xxx" ) 后,Android Framework 都干了些了啥?


static {
    System.loadLibrary("ymm_log");
}

在看下System类的实现:

public static void loadLibrary(String libname) {
    Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}

synchronized void loadLibrary0(ClassLoader loader, String libname) {
    if (libname.indexOf((int)File.separatorChar) != -1) {
        throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
    }
    String libraryName = libname;
    if (loader != null) {
        String filename = loader.findLibrary(libraryName);
        if (filename == null) {
            // It's not necessarily true that the ClassLoader used
            // System.mapLibraryName, but the default setup does, and it's
            // misleading to say we didn't find "libMyLibrary.so" when we
            // actually searched for "liblibMyLibrary.so.so".
            throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                           System.mapLibraryName(libraryName) + "\"");
        }
        String error = doLoad(filename, loader);
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
        return;
    }

    String filename = System.mapLibraryName(libraryName);
    List<String> candidates = new ArrayList<String>();
    String lastError = null;
    for (String directory : getLibPaths()) {
        String candidate = directory + filename;
        candidates.add(candidate);

        if (IoUtils.canOpenReadOnly(candidate)) {
            String error = doLoad(candidate, loader);
            if (error == null) {
                return; // We successfully loaded the library. Job done.
            }
            lastError = error;
        }
    }

    if (lastError != null) {
        throw new UnsatisfiedLinkError(lastError);
    }
    throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}

 private String doLoad(String name, ClassLoader loader) {
        // Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH,
        // which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH.

        // The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load
        // libraries with no dependencies just fine, but an app that has multiple libraries that
        // depend on each other needed to load them in most-dependent-first order.

        // We added API to Android's dynamic linker so we can update the library path used for
        // the currently-running process. We pull the desired path out of the ClassLoader here
        // and pass it to nativeLoad so that it can call the private dynamic linker API.

        // We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the
        // beginning because multiple apks can run in the same process and third party code can
        // use its own BaseDexClassLoader.

        // We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any
        // dlopen(3) calls made from a .so's JNI_OnLoad to work too.

        // So, find out what the native library search path is for the ClassLoader in question...
        String librarySearchPath = null;
        if (loader != null && loader instanceof BaseDexClassLoader) {
            BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
            librarySearchPath = dexClassLoader.getLdLibraryPath();
        }
        // nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless
        // of how many ClassLoaders are in the system, but dalvik doesn't support synchronized
        // internal natives.
        synchronized (this) {
            return nativeLoad(name, loader, librarySearchPath);
        }
    }

获取so文件名的方式,就是从classLoader中获取,最终加载时通过本地方法nativeLoad实现

 String filename = loader.findLibrary(libraryName)

其实现在BaseDexClassLoader

@Override    
public String findLibrary(String name) {
  return pathList.findLibrary(name);   
 }

方案分析:

1. JNI 代码内置方案

代码隔离方案比较适合新增的 Native 模块,一开始就奔着动态化、延迟加载的方向去。

2. 插件化方案

单独把 so 文件单独打包进插件包,JNI 代码保留在宿主代码内部

由于 nativeLibraryDirectories 的具体实现是一个 ArrayList 实例,其元素读写操作自身是不保证线程安全的

急需解决的问题

1. 安全性问题

所有可执行代码在拷贝安装到安全路径(比如 Android 的 data/data 内部路径)之前,都有被劫持或者破坏的风险。

最好的做法是每次加载 so 库之前都对其做一次安全性校验

最简单的方式是记录 so 文件的 MD5 或者 CRC 等 Hash 信息(粒度可以是每个单独的 so 文件,或者一批 so 文件的压缩包)

如果本地下载目录中的 so 文件总数目,少于预定义在集合里 so 文件数目,说明不完整

2. 版本控制问题

通过版本控制流程,我们可以在服务端禁用这个版本的 so 插件,从而使客户端进入“so 插件不可用”的逻辑,而不至于执行有问题的代码。

3. abi 兼容性判断

检查 so 插件包里的 so 库 abi 信息是否与宿主目前运行时的 abi 一致。

直接指定你 so 下载的路径,通过反射获取 android.os.SystemProperties 私有方法 get ro.product.cpu.abi 可以动态获取 CPU 架构

    /**
     * 获取设备的cpu架构类型
     */
    public static String getCpuArchType() {
        if (!TextUtils.isEmpty(cpuArchType)) {
            return cpuArchType;
        }
        try {
            Class<?> clazz = Class.forName("android.os.SystemProperties");
            Method get = clazz.getDeclaredMethod("get", new Class[]{String.class});
            cpuArchType = (String) get.invoke(clazz, new Object[]{"ro.product.cpu.abi"});
        } catch (Exception e) {
        }

        try {
            if (TextUtils.isEmpty(cpuArchType)) {
                cpuArchType = Build.CPU_ABI;//获取不到,重新获取,可能不准确?
            }
        } catch (Exception e) {
        }
        if (TextUtils.isEmpty(cpuArchType)) {
            cpuArchType = "armeabi-v7a";
        }
        cpuArchType = cpuArchType.toLowerCase();
        return cpuArchType;
    }

4. System#load 加载代码侵入问题

通过 System#loadLibrary("xxx" ) 加载 so 库, Android Framework 会遍历当前上下文的 ClassLoader 实例里的 nativeLibraryDirectories 数组,在数组里所有的文件路径下查找文件名为 libxxx.so 的文件,所以我们的解决思路就是在安装好 so 插件之后,将其所在的内部安全路径注入到这个 nativeLibraryDirectories 数组里,即可实现通过 System#loadLibrary 加载,代码如下:

第一步: 通过反射,注入 so 文件注入到 nativeLibraryDirectories 路径

    private static final class V14 {
        private static void install(ClassLoader classLoader, File folder) throws Throwable {
           // 反射宿主 APK 的 ClassLoader 的 pathList成员变量
            Field pathListField = MkReflectUtil.findField(classLoader, "pathList");
            // 获取这个成员变量 在 宿主 APK 的 ClassLoader 对象的取值
            Object dexPathList = pathListField.get(classLoader);
            // 将被加载的 被加载的 so 实例存储到 dexPathList
            MkReflectUtil.expandArray(dexPathList, "nativeLibraryDirectories", new File[]{folder});
        }
    }

 private static final class V23 {
        private static void install(ClassLoader classLoader, File folder) throws Throwable {
            Field pathListField = MkReflectUtil.findField(classLoader, "pathList");
            Object dexPathList = pathListField.get(classLoader);

            Field nativeLibraryDirectories = MkReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
            List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);

            //去重
            if (libDirs == null) {
                libDirs = new ArrayList<>(2);
            }
            final Iterator<File> libDirIt = libDirs.iterator();
            while (libDirIt.hasNext()) {
                final File libDir = libDirIt.next();
                if (folder.equals(libDir)) {
                    libDirIt.remove();
                    Log.d(TAG, "dq libDirIt.remove() " + folder.getAbsolutePath());
                    break;
                }
            }

            libDirs.add(0, folder);
            Field systemNativeLibraryDirectories =
                    MkReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
            List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);

            //判空
            if (systemLibDirs == null) {
                systemLibDirs = new ArrayList<>(2);
            }
            //Log.d(TAG, "dq systemLibDirs,size=" + systemLibDirs.size());

            // 获得Element[] 数组
            Method makePathElements = MkReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class, List.class);
            ArrayList<IOException> suppressedExceptions = new ArrayList<>();
            libDirs.addAll(systemLibDirs);
           // 输出调用对象,插件APK所在目录,插件APK的全路径,和用于存储IO异常的List,获得Element[] 返回
            Object[] elements = (Object[]) makePathElements.invoke(dexPathList, libDirs, null, suppressedExceptions);
            Field nativeLibraryPathElements = MkReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
            nativeLibraryPathElements.setAccessible(true);
            nativeLibraryPathElements.set(dexPathList, elements);
        }
    }

 private static final class V25 {
        private static void install(ClassLoader classLoader, File folder) throws Throwable {
            Field pathListField = MkReflectUtil.findField(classLoader, "pathList");
            Object dexPathList = pathListField.get(classLoader);
            Field nativeLibraryDirectories = MkReflectUtil.findField(dexPathList, "nativeLibraryDirectories");

            List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
            //去重
            if (libDirs == null) {
                libDirs = new ArrayList<>(2);
            }
            final Iterator<File> libDirIt = libDirs.iterator();
            while (libDirIt.hasNext()) {
                final File libDir = libDirIt.next();
                if (folder.equals(libDir)) {
                    libDirIt.remove();
                    Log.d(TAG, "dq libDirIt.remove()" + folder.getAbsolutePath());
                    break;
                }
            }

            libDirs.add(0, folder);
            //system/lib
            Field systemNativeLibraryDirectories = MkReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
            List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);

            //判空
            if (systemLibDirs == null) {
                systemLibDirs = new ArrayList<>(2);
            }
            //Log.d(TAG, "dq systemLibDirs,size=" + systemLibDirs.size());

            Method makePathElements = MkReflectUtil.findMethod(dexPathList, "makePathElements", List.class);
            libDirs.addAll(systemLibDirs);

            Object[] elements = (Object[]) makePathElements.invoke(dexPathList, libDirs);
            Field nativeLibraryPathElements = MkReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
            nativeLibraryPathElements.setAccessible(true);
            nativeLibraryPathElements.set(dexPathList, elements);
        }
    }

注入 so 路径的逻辑如下:

  1. APK 的 ClassLoader 的 pathList 的成员变量,
  2. pathList 实际上是 SoPathList, 类的实例 的内部 成员变量 List 实例
  3. 这个 List 存储的是 被加载的 so 文件实例
    /**
     * 1. 通过反射拿到dexElements的取值
     * 2. 将 findField 方法获取到的 object[] 插入到数组的最前面。
     * 3. 被插入的 object[] 数组就是外部修复包存储路径集合编译后形成的队列
     *    即外部修复包的资源和 .class 队列
     * @param instance 宿主 APK 的 ClassLoader实例的成员变量 pathList(DexPathList类似)
     * @param fieldName 需要被反射和替换的 DexPathList 类对象的成员变量 "dexElements", 用于存储 .dex 加载对象dex
     * @param extraElements 被加载的插件 apk 的 .dex实例列表
     */
    public static void expandFieldArray(Object instance, String fieldName, Object[] extraElements)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        // 1 通过反射获取 classLoader 实例的成员变量 pathList(DexPathList类的实例)的成员变量dexElements
        Field jlrField = findField(instance, fieldName);
        // 2 获取当前dexElements 这个成员变量在classLoader 实例的成员变量 pathList(DexPathList类的实例)中的取值
        Object[] original = (Object[]) jlrField.get(instance);
        // 3 新建一个数组,这个数组用来容纳 宿主 apk .dex 文件加载出来的elements[] 和 插件apk .dex 文件加载出来的 elements[]
        Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length);
      // 4 先把插件 apk 中获取的elements[] 以及 dexFileArr复制到数组里面,方便我们动态加载
        System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
       // 5 再把apk所有的 dexElements 成员变量取值复制到数组里面
        System.arraycopy(original, 0, combined, extraElements.length, original.length);
         // 6 覆盖 dexElements 成员变量取值
        jlrField.set(instance, combined);
    }

参考

App极限瘦身: 动态下发so
插件化so库加载原理及实现

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容