说明:此文章完全参照《Android热修复升级探索——SO库修复方案》https://yq.aliyun.com/articles/217377,整理做个笔记。

Android开发过程中,经常会引入so文件,因为so具有安全,高效,方便移植的特点。有些时候,我们需要在不发版的情况下,更新so文件,因此就有了这篇文章啦。下面主要讲讲动态更新so的实践…

SO的注册方式

动态更新SO,首先需要了解JNI的两个注册方式

  • 静态注册
#include <jni.h>
#include <string>
	
//静态注册
extern "C"
JNIEXPORT jstring JNICALL
Java_com_sankuai_ndk_demo_NdkManager_stringFromJNI(JNIEnv *env, jclass type) 	{
    std::string hello = "我是补丁so,动态加载即时生效";
    return env->NewStringUTF(hello.c_str());
}
	
  • 动态注册
#include <jni.h>
#include <string>
	
//动态注册
jint test(JNIEnv *env, jclass clazz) {
    return 10086;
}
	
JNINativeMethod nativeMethods[] = {"test", "()I", (void *) test};
	
#define JNIREG_CLASS "com/sankuai/ndk/demo/NdkManager"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    printf("old JNI_OnLoad");
	
    JNIEnv* env = NULL;
    //判断虚拟机状态是否有问题
    if(vm->GetEnv((void**)&env,JNI_VERSION_1_6)!= JNI_OK){
        return -1;
    }
	
    jclass clz = env->FindClass(JNIREG_CLASS);
    if (env->RegisterNatives(clz, nativeMethods, sizeof(nativeMethods)/ sizeof(nativeMethods[0])) != JNI_OK) {
        return JNI_ERR;
    }
	
    return JNI_VERSION_1_6;
}
  • Android代码
package com.sankuai.ndk.demo;

public class NdkManager {
    static {
        System.loadLibrary("native-lib");
    }
    /**
     * A native method that is implemented by the 'native-lib' native 
     * library, which is packaged with this application.
     */
    public static native String stringFromJNI();
	
    public static native int test();
	
}

Android加载SO的两种方式

Java Api提供以下两个接口加载一个so库。这两种方式最后都调用了nativeLoad(name, loader, librarySearchPath)这个方法,参数name:so库在磁盘中的完整路径名。

System.loadLibrary(String libName):传进去的参数:so库名称, 表示的so库文件,位于apk压缩文件中的libs目录,最后复制到apk安装目录下。

System.load(String pathName):传进去的参数:so库在磁盘中的完整路径, 加载一个自定义外部so库文件 。

java.lang.Runtime类里面:

private String doLoad(String name, ClassLoader loader) {
    // ...
    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库热部署实时生效可行性分析

  • jni动态注册

分析SO的加载原理,动态注册的native方法,每次JNI_OnLoad方法都会重新完成一次映射。所以是否只要先加载原来的so库,,然后再加载补丁so库,就能完成Java层native方法到native层patch后的新方法映射,这样就完成动态注册native方法的patch实时修复。实际操作发现:

ART

能正常工作。

Dalvik

Dalvik下存在bug,Dalvik下根据so的文件名称去查找句柄,发现加载过,直接返回so的句柄。因此热修复so的时候,只需要在so文件后面加个随机数就行..

  • jni静态注册

静态注册有两个难点:

(1)首先需要解注册静态注册的native方法, 这个也是难点, 因为我们很难知道so库中哪几个静态注册的native方法发生了变更。

(2)假设就算我们知道如果静态注册的native方法需要解注册, 重新load补丁so库也有可能被修复也有可能不被修复。

上面对补丁so进行了第二次加载, 那么肯定是多消耗了一次本地内存, 如果补丁so库够大, 补丁so够多,那么JNI层的OOM也不是没可能。

so库冷部署重启生效实现方案

  • 接口调用替换

SOPatchManager.loadLibrary接口加载so库的时候优先尝试去加载sdk指定目录下的补丁so, 加载策略如下:

如果存在则加载补丁so库而不会去加载安装apk安装目录下的so库。

如果不存在补丁so, 那么调用System.loadLibrary去加载安装apk目录下的so库。

优点:不需要对不同sdk版本进行兼容, 因为所有的sdk版本都有System.loadLibrary这个接口。

缺点: 调用方需要替换掉System默认加载so库接口为sdk提供的接口, 如果是已经编译混淆好的三方库的so库需要patch, 那么是很难做到接口的替换。

虽然这种方案实现简单, 同时不需要对不同sdk版本区分处理,但是有一定的局限性没法修复三方包的so库同时需要强制侵入接入方接口调用。

  • 反射注入

sdk<23 我们可以采取类似类修复反射注入方式, 只要把我们的补丁so库的路径插入到nativeLibraryDirectories数组的最前面就能够达到加载so库的时候是补丁so库而不是原来so库的目录, 从而达到修复的目的。

sdk23以上findLibrary实现已经发生了变化, 如上所示, 那么我们只需要把补丁so库的完整路径作为参数构建一个Element对象, 然后再插入到nativeLibraryPathElements数组的最前面就好了。

如果正确复制补丁so库

查找出对应的CpuABI,然后复制对应的so。

private String[] getPrimaryCpuAbi() {
    String[] cpuAbi = null;
    try {
	
        PackageManager pm = getPackageManager();
        if (null != pm) {
            ApplicationInfo applicationInfo = pm.getApplicationInfo(getPackageName(), 0);
	
            if (null != applicationInfo) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    Field field = ApplicationInfo.class.getField("primaryCpuAbi");
                    field.setAccessible(true);
                    String abi = (String) field.get(applicationInfo);
	
                    cpuAbi = new String[]{abi};
                } else {
                    cpuAbi = new String[]{Build.CPU_ABI, Build.CPU_ABI2};
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return cpuAbi;
}
	

参考资料

Android热修复升级探索——SO库修复方案:https://yq.aliyun.com/articles/217377


分享到