饿了么热修复开源解决方案 Amigo 0.4.4 源码解读

1,467 阅读4分钟
原文链接: www.diycode.cc

本文由iceGeneral原创,首发于diycode.cc,非商业转载请注明作者和原文链接。

热修复的框架有很多,Amigo 的实现应该算是比较优雅的,其代码阅读难度不会太大,它不仅可以用于热修复,也可以用于版本更新。至于集成和使用方法,可以去看 Github 上的 wiki,就不赘诉了。 本文主要分为5部分:概述、释放APK、替换修复(包括替换 Service 的过程)、替换 Activity 的过程、局限
一、概述
拿到新的APK,怎么用? --> 拷贝其dex、so、优化dex
上面那些步骤太耗时,怎么办? --> 第一种选择:直接在后台开个线程慢慢处理,下次打开App就能生效了。第二个选择:等不及想立即生效,强制重启App,然后在Application的onCreate里,在新进程里打开一个loading的Activity,在这个Activity里开个线程去处理,处理完毕后退出这个进程
还要关注的问题有:支持替换Application吗?支持res更新吗?如何支持新增Service?如何支持新增Ac(如果TargetActivity不是新增的,就不会进行替换)tivity?如何支持新增Receiver?支持新增Provider吗?

二、释放APK

让补丁包生效的方法,看 wiki 上的说法

补丁包生效有两种方式可以选择:
稍后生效补丁包
如果不想立即生效而是用户第二次打开App 时才打入补丁包,第二次打开时就会自动生效。可以通过这个方法
Amigo.workLater(context, apkFile);
立即生效补丁包
如果想要补丁包立即生效,调用以下两个方法之一,App 会立即重启,并且打入补丁包。
Amigo.work(context, apkFile);

1、Amigo.workLater

public static void workLater(Context context, File patchFile) {
    //检查patchFile是否存在、是否有读权限、是否有新增permission、签名是否和原APK一致
    checkPatchApk(context, patchFile);
    String patchChecksum = CrcUtils.getCrc(patchFile);
    if (!PatchApks.getInstance(context).exists(patchChecksum)) {
        //将patchFile拷贝到/data/data/{packageName}/files/amigo/{checksum}/patch.apk
        copyFile(patchFile, PatchApks.getInstance(context).patchFile(patchChecksum));
    }
    //见3
    AmigoService.start(context, patchChecksum, true);
}

2、Amigo.work

public static void work(Context context, File patchFile) {
    checkPatchApk(context, patchFile);
    String patchChecksum = CrcUtils.getCrc(patchFile);
    if (!PatchApks.getInstance(context).exists(patchChecksum)) {
        copyFile(patchFile, PatchApks.getInstance(context).patchFile(patchChecksum));
    }
    //跨进程SharedPreferences,WORKING_PATCH_APK_CHECKSUM有值表示存在补丁
    context.getSharedPreferences(SP_NAME, MODE_MULTI_PROCESS)
            .edit()
            .putString(Amigo.WORKING_PATCH_APK_CHECKSUM, patchChecksum)
            .commit();
    AmigoService.start(context, patchChecksum, false);
    //杀死自身,当前是App的默认进程(主进程)
    System.exit(0);
    Process.killProcess(Process.myPid());
}

3、AmigoService

private Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case WHAT:
                Context context = AmigoService.this;
                if (!isMainProcessRunning(context)) {
                    Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
                    launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
                    context.startActivity(launchIntent);
                    Log.e(TAG, "start launchIntent");
                    stopSelf();
                    System.exit(0);
                    Process.killProcess(Process.myPid());
                    return;
                }
                if (count++ < RETRY_TIMES) {
                    sendMessageDelayed(Message.obtain(msg), DELAY);
                }
                break;
            default:
                break;
        }
    }
};

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    if (intent != null) {
        String checksum = intent.getStringExtra(APK_CHECKSUM);
        boolean workLater = intent.getBooleanExtra(WORK_LATER, false);
        if (workLater) {
            ApkReleaser.getInstance(this).release(checksum);
        } else {
            handler.sendMessage(handler.obtainMessage(WHAT, checksum));
        }
    }
    return super.onStartCommand(intent, flags, startId);
}

public static void start(Context context, String checksum, boolean workLater) {
    Intent intent = new Intent(context, AmigoService.class);
    intent.putExtra(WORK_LATER, workLater)
            .putExtra(APK_CHECKSUM, checksum);
    context.startService(intent);
}

AmigoService是运行在另一个进程里的(amigo进程),看它的AndroidManifest.xml配置

如果是workLater,则执行ApkReleaser.getInstance(this).release(checksum)(见6)
如果是work,则等待主进程结束,然后再startActivity(launcher),这样实现了App重启
workLater有执行release,而work却没有,所以我们猜测,重启后在Application的onCreate里会执行release

4、AndroidManifest.xml
集成了Amigo的App运行后进入的是Amigo,而不是我们自己的Application
我们写的AndroidManifest.xml

xml version="1.0" encoding="utf-8"?>


    
    

    
        
            
                

                
            
        
    

其实编译后的AndroidManifest.xml并不是这样的,编译后的是这个文件app/build/intermediates/manifests/full/debug/AndroidManifest.xml

xml version="1.0" encoding="UTF-8"?>

  
  
  
  
    
      
        
        
      
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
     //注意这个
  

看到application节点android:name="me.ele.amigo.Amigo",显然是Amigo的plugin修改了AndroidManifest.xml

注意最后一个节点,原来的 Application 变成一个 activity 节点,Amigo 官方对此的解释是

Amigo Plugin 做了很 hack 的一步,就是在 AndroidManifest.xml 中将原来的 application 做为一个 Activity 。我们知道 MultiDex 分包的规则中,一定会将 Activity 放到主 dex 中,Amigo Plugin 为了保证原来的 application 被替换后仍然在主 dex 中,就做了这个十分 hack 的一步。机智的少年。

5、Amigo.onCreate --> release

public class Amigo extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        try {
            ...
            String workingPatchApkChecksum = sharedPref.getString(WORKING_PATCH_APK_CHECKSUM, "");
            try {
                //如果App当前的version_code大于上一次存储在sp里的version_code,说明App升级过
                if (checkUpgrade()) {
                    throw new RuntimeException("Host app has upgrade");
                }
                // /data/data/{packageName}/files/amigo/{workingPatchApkChecksum}/patch.apk是否存在
                //workLater()和work()都会把APK拷贝到这里
                if (TextUtils.isEmpty(workingPatchApkChecksum)
                        || !patchApks.exists(workingPatchApkChecksum)) {
                    throw new RuntimeException("Patch apk doesn't exists");
                }
            } catch (RuntimeException e) {
                e.printStackTrace();
                if (ProcessUtils.isMainProcess(this)) {
                    // clear is a dangerous operation, only need to be operated by main process
                    doClear(this);
                }
                runOriginalApplication();
                return;
            }
            //如果是amigo进程
            // ensure load dex process always run host apk not patch apk
            if (ProcessUtils.isLoadDexProcess(this)) {
                runOriginalApplication();
                return;
            }
            //isPatchApkFirstRun()在这里扯犊子,workingPatchApkChecksum是从sp取出来的,结果还去判断跟sp里的是不是不一样,那结果肯定是false
            if (!ProcessUtils.isMainProcess(this) && isPatchApkFirstRun(workingPatchApkChecksum)) {
                runOriginalApplication();
                return;
            }

            // only release loaded apk in the main process
            runPatchApk(workingPatchApkChecksum);
        } catch (LoadPatchApkException e) {
            e.printStackTrace();
            loadPatchError = LoadPatchError.record(LoadPatchError.LOAD_ERR, e);
            try {
                runOriginalApplication();
            } catch (Throwable e2) {
                throw new RuntimeException(e2);
            }
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

private boolean isPatchApkFirstRun(String checksum) {
    return !sharedPref.getString(WORKING_PATCH_APK_CHECKSUM, "").equals(checksum);
}

5.1 Amigo.onCreate --> runPatchApk --> release

private void runPatchApk(String checksum) throws LoadPatchApkException {
    try {
        if (isPatchApkFirstRun(checksum) || !AmigoDirs.getInstance(this).isOptedDexExists(checksum)) {
            // TODO This is workaround for now, refactor in future.
            sharedPref.edit().remove(checksum).commit();
            releasePatchApk(checksum);
        } else {
            checkDexAndSoChecksum(checksum);
        }
        ...   //后续操作,见7
    } catch (Exception e) {
        throw new LoadPatchApkException(e);
    }
}

isPatchApkFirstRun(checksum) 扯犊子,肯定等于false
AmigoDirs.getInstance(this).isOptedDexExists(checksum) 是判断 /data/data/{package_name}/code_cache/{checksum}/amigo-dexes/ 目录底下是否存在文件
Amigo.work()的话,这个目录底下肯定还没有东西(因为work()到现在只是重启了App而已,并还没有做什么),Amigo.workLater()执行后,这个目录里应该是有东西的(release见6)

5.2 Amigo.onCreate --> runPatchApk --> releasePatchApk --> release

private void releasePatchApk(String checksum) throws Exception {
    //clear previous working dir
    clearWithoutPatchApk(checksum);

    //start a new process to handle time-tense operation
    ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getPackageName(), GET_META_DATA);
    String layoutName = appInfo.metaData.getString("amigo_layout");
    String themeName = appInfo.metaData.getString("amigo_theme");
    int layoutId = 0;
    int themeId = 0;
    if (!TextUtils.isEmpty(layoutName)) {
        layoutId = (int) readStaticField(Class.forName(getPackageName() + ".R$layout"), layoutName);
    }
    if (!TextUtils.isEmpty(themeName)) {
        themeId = (int) readStaticField(Class.forName(getPackageName() + ".R$style"), themeName);
    }
    ApkReleaser.getInstance(this).work(checksum, layoutId, themeId);
}

获取AndroidManifest.xml里的amigo_layout和amigo_theme

public class ApkReleaser {
    ...
    public void work(String checksum, int layoutId, int themeId) {
        if (!ProcessUtils.isLoadDexProcess(context)) {
            if (!isDexOptDone(checksum)) {
                waitDexOptDone(checksum, layoutId, themeId);
            }
        }
    }

    private boolean isDexOptDone(String checksum) {
        return context.getSharedPreferences(SP_NAME, Context.MODE_MULTI_PROCESS)
                .getBoolean(checksum, false);
    }

    private void waitDexOptDone(String checksum, int layoutId, int themeId) {
        //打开ApkReleaseActivity
        new Launcher(context).checksum(checksum).layoutId(layoutId).themeId(themeId).launch();
        while (!isDexOptDone(checksum)) {
            try {
                Thread.sleep(SLEEP_DURATION);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    ...
}

打开ApkReleaseActivity并传入amigo_layout和amigo_theme(ApkReleaseActivity内部操作见5.3 )
ApkReleaseActivity内部操作完毕,才会更新sp checksum的值,这个while才会退出循环
所以Amigo.onCreate --> runPatchApk --> releasePatchApk会一直阻塞在ApkReleaser.getInstance(this).work(checksum, layoutId, themeId),直到ApkReleaseActivity内部操作完毕

5.3 ApkReleaseActivity

public class ApkReleaseActivity extends Activity {

    static final String LAYOUT_ID = "layout_id";
    static final String THEME_ID = "theme_id";
    static final String PATCH_CHECKSUM = "patch_checksum";

    private int layoutId;
    private int themeId;
    private String checksum;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        overridePendingTransition(0, 0);
        layoutId = getIntent().getIntExtra(LAYOUT_ID, 0);
        themeId = getIntent().getIntExtra(THEME_ID, 0);
        checksum = getIntent().getStringExtra(PATCH_CHECKSUM);
        if (TextUtils.isEmpty(checksum)) {
            throw new RuntimeException("patch apk checksum must not be empty");
        }
        if (themeId != 0) {
            setTheme(themeId);
        }
        if (layoutId != 0) {
            setContentView(layoutId);
        }

        ApkReleaser.getInstance(this).release(checksum);
    }

    @Override
    public void onBackPressed() {
        //do nothing
    }
}

看到onBackPressed里啥也不处理,所以按回退键不会退出ApkReleaseActivity,而且ApkReleaseActivity是运行在amigo进程

主进程还被阻塞在onCreate呢,因为ApkReleaseActivity处在栈顶,所以输入事件都会进入ApkReleaseActivity,并不进入主进程,所以主进程并不会ANR,而ApkReleaseActivity里的耗时操作(release)是放在子线程,所以也不会ANR

6、ApkReleaser.release
从上面分析得知,Amigo.workLater()和Amigo.work()最终都会调用ApkReleaser.getInstance(this).release(checksum)

public void release(final String checksum) {
    if (isReleasing) {
        return;
    }
    //加入线程池
    service.submit(new Runnable() {
        @Override
        public void run() {
            isReleasing = true;
            //释放Dex到/data/data/{package_name}/files/amigo/{checksum}/dexes
            DexReleaser.releaseDexes(patchApks.patchFile(checksum), amigoDirs.dexDir(checksum));
            //拷贝so文件到/data/data/{package_name}/files/amigo/{checksum}/libs,见6.1
            NativeLibraryHelperCompat.copyNativeBinaries(patchApks.patchFile(checksum), amigoDirs.libDir(checksum));
            //优化dex文件,见6.2
            dexOptimization(checksum);
        }
    });
}

6.1 NativeLibraryHelperCompat,拷贝so文件

public static final int copyNativeBinaries(File apkFile, File sharedLibraryDir) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        return copyNativeBinariesAfterL(apkFile, sharedLibraryDir);
    } else {
        return copyNativeBinariesBeforeL(apkFile, sharedLibraryDir);
    }
}

VERSION.SDK_INT<21< strong="">

private static int copyNativeBinariesBeforeL(File apkFile, File sharedLibraryDir) {
    try {
        Object[] args = new Object[2];
        args[0] = apkFile;
        args[1] = sharedLibraryDir;
        return (int) MethodUtils.invokeStaticMethod(nativeLibraryHelperClass(), "copyNativeBinariesIfNeededLI", args);
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

    return -1;
}

private static final Class nativeLibraryHelperClass() throws ClassNotFoundException {
    return Class.forName("com.android.internal.content.NativeLibraryHelper");
}

相当于

com.android.internal.content.NativeLibraryHelper.copyNativeBinariesIfNeededLI(apkFile, sharedLibraryDir)

VERSION.SDK_INT>=21
不提供方法 copyNativeBinariesIfNeededLI(file, file),换成 findSupportedAbi() 和 copyNativeBinaries()

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static int copyNativeBinariesAfterL(File apkFile, File sharedLibraryDir) {
    try {
        Object handleInstance = MethodUtils.invokeStaticMethod(handleClass(), "create", apkFile);
        if (handleInstance == null) {
            return -1;
        }

        String abi = null;

        if (isVM64()) {
            if (Build.SUPPORTED_64_BIT_ABIS.length > 0) {
                Set abis = getAbisFromApk(apkFile.getAbsolutePath());
                if (abis == null || abis.isEmpty()) {
                    return 0;
                }
                int abiIndex = (int) MethodUtils.invokeStaticMethod(nativeLibraryHelperClass(), "findSupportedAbi", handleInstance, Build.SUPPORTED_64_BIT_ABIS);
                if (abiIndex >= 0) {
                    abi = Build.SUPPORTED_64_BIT_ABIS[abiIndex];
                }
            }
        } else {
            if (Build.SUPPORTED_32_BIT_ABIS.length > 0) {
                Set abis = getAbisFromApk(apkFile.getAbsolutePath());
                if (abis == null || abis.isEmpty()) {
                    return 0;
                }
                int abiIndex = (int) MethodUtils.invokeStaticMethod(nativeLibraryHelperClass(), "findSupportedAbi", handleInstance, Build.SUPPORTED_32_BIT_ABIS);
                if (abiIndex >= 0) {
                    abi = Build.SUPPORTED_32_BIT_ABIS[abiIndex];
                }
            }
        }

        if (abi == null) {
            return -1;
        }

        Object[] args = new Object[3];
        args[0] = handleInstance;
        args[1] = sharedLibraryDir;
        args[2] = abi;
        return (int) MethodUtils.invokeStaticMethod(nativeLibraryHelperClass(), "copyNativeBinaries", args);
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

    return -1;
}

private static final Class handleClass() throws ClassNotFoundException {
    return Class.forName("com.android.internal.content.NativeLibraryHelper$Handle");
}

相当于

Object handleInstance = com.android.internal.content.NativeLibraryHelper$Handle.create(apkFile)
String abi = null;
if (isVM64()) {
    int abiIndex = com.android.internal.content.NativeLibraryHelper.findSupportedAbi(handleInstance, Build.SUPPORTED_64_BIT_ABIS);
    abi = Build.SUPPORTED_64_BIT_ABIS[abiIndex];
} else {
    int abiIndex = com.android.internal.content.NativeLibraryHelper.findSupportedAbi(handleInstance, Build.SUPPORTED_32_BIT_ABIS);
    abi = Build.SUPPORTED_32_BIT_ABIS[abiIndex];
}
com.android.internal.content.NativeLibraryHelper.copyNativeBinaries(handleInstance, sharedLibraryDir, abi);

6.2 优化dex文件

private void dexOptimization(final String checksum) {
    // /data/data/{package_name}/files/amigo/{checksum}/dexes/classes*.dex
    File[] validDexes = amigoDirs.dexDir(checksum).listFiles(new FileFilter() {
        @Override
        public boolean accept(File pathname) {
            return pathname.getName().endsWith(".dex");
        }
    });

    final CountDownLatch countDownLatch = new CountDownLatch(validDexes.length);

    for (final File dex : validDexes) {
        service.submit(new Runnable() {
            @Override
            public void run() {
                long startTime = System.currentTimeMillis();
                // /data/data/{package_name}/code_cache/{checksum}/amigo-dexes/classes*.dex
                String optimizedPath = optimizedPathFor(dex, amigoDirs.dexOptDir(checksum));
                DexFile dexFile = null;
                try {
                    dexFile = DexFile.loadDex(dex.getPath(), optimizedPath, 0);
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (dexFile != null) {
                        try {
                            dexFile.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
                countDownLatch.countDown();
            }
        });
    }

    try {
        countDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    handler.sendMessage(handler.obtainMessage(WHAT_DEX_OPT_DONE, checksum));
}

多个classes*.dex就开启多个线程进行DexFile.loadDex(sourcePathName, outputPathName, flags),源码里对于这个方法的注释

打开一个DEX文件,并提供一个文件来保存优化过的DEX数据。如果优化过的格式已存在并且是最新的,就直接使用它。如果不是,虚拟机将试图重新创建一个。该方法主要用于应用希望在通常的应用安装机制之外下载和执行DEX文件。不能在应用里直接调用该方法,而应该通过一个类装载器例如dalvik.system.DexClassLoader.
参数
sourcePathName:包含”classes.dex”的Jar或者APK文件(将来可能会扩展支持"raw DEX")
outputPathName:保存优化过的DEX数据的文件
flags:打开可选功能(目前没定义)
返回值
一个新的,或者先前已经打开的DexFile

所有classes*.dex都优化完毕就调用

handler.sendMessage(handler.obtainMessage(WHAT_DEX_OPT_DONE, checksum));

static final int DELAY_FINISH_TIME = 4000;

private Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case WHAT_DEX_OPT_DONE:
                isReleasing = false;
                String checksum = (String) msg.obj;
                //更新 sp checksum 的值,更新成功后,主进程就退出 while 循环,继续往下执行了
                doneDexOpt(checksum);
                // dex 和 so 的全路径名做 key,checksum 做 value 存储到 sp,下次使用时用来和真实文件做校验,检查文件是否被修改过
                saveDexAndSoChecksum(checksum);
                context.getSharedPreferences(SP_NAME, Context.MODE_MULTI_PROCESS)
                        .edit()
                        .putString(Amigo.WORKING_PATCH_APK_CHECKSUM, checksum)
                        .commit();
                //等待4秒,等待肯定是因为sp真正写入到文件需要时间,只是强行等待4秒未免不太友好,应该有提升空间
                handler.sendEmptyMessageDelayed(WHAT_FINISH, DELAY_FINISH_TIME);
                break;
            case WHAT_FINISH:
                System.exit(0);
                Process.killProcess(Process.myPid());
                break;
            default:
                break;
        }
    }
};

private void doneDexOpt(String checksum) {
    context.getSharedPreferences(SP_NAME, Context.MODE_MULTI_PROCESS)
            .edit()
            .putBoolean(checksum, true)
            .commit();
}

最后就是退出进程,这个进程当然是amigo进程,ApkReleaseActivity当然也就消失了

三、替换修复

private void runPatchApk(String checksum) throws LoadPatchApkException {
    try {
        if (isPatchApkFirstRun(checksum) || !AmigoDirs.getInstance(this).isOptedDexExists(checksum)) {
            // TODO This is workaround for now, refactor in future.
            sharedPref.edit().remove(checksum).commit();
            //一直阻塞直到释放APK完毕
            releasePatchApk(checksum);
        } else {
            //检查文件名是否和sp里存储的一致
            checkDexAndSoChecksum(checksum);
        }
        //1、替换 ClassLoader
        // .../classes.dex:.../classes2.dex:... 这样传入ClassLoader就能一次性全部加载
        String dexPathes = getDexPath(checksum);
        AmigoClassLoader amigoClassLoader = new AmigoClassLoader(dexPathes,
                AmigoDirs.getInstance(this).dexOptDir(checksum),
                AmigoDirs.getInstance(this).libDir(checksum).getAbsolutePath(),
                getRootClassLoader());
        setAPKClassLoader(amigoClassLoader);
        patchedClassLoader = amigoClassLoader;

        //2、替换 AssetManager
        AssetManager assetManager = AssetManager.class.newInstance();
        Method addAssetPath = getMatchedMethod(AssetManager.class, "addAssetPath", String.class);
        addAssetPath.setAccessible(true);
        addAssetPath.invoke(assetManager, patchApks.patchPath(checksum));
        setAPKResources(assetManager);

        //3、替换 Instrumentation
        setApkInstrumentation();

        //4、替换 H
        setApkHandler();

        //5、注册广播
        dynamicRegisterReceivers(amigoClassLoader);

        sharedPref.edit().putString(WORKING_PATCH_APK_CHECKSUM, checksum).commit();
        clearOldPatches(checksum);

        //6、IActivityManagerNative Hook
        installHook(amigoClassLoader);

        //7、运行原始的 Application
        runPatchedApplication();
    } catch (Exception e) {
        throw new LoadPatchApkException(e);
    }
}

1、替换 ClassLoader

public class AmigoClassLoader extends BaseDexClassLoader {

    public AmigoClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, libraryPath, parent);
    }

}

看看 BaseDexClassLoader

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    /**
     * Constructs an instance.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     * should be written; may be {@code null}
     * @param libraryPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    ...

dexpath 为 jar 或 apk 文件目录,多个的话用冒号分隔
optimizedDirectory 为优化 dex 缓存目录
libraryPath 为包含 native lib 的目录路径
parent 为父类加载器

只要 new AmigoClassLoader(...) 就加载了全部 classes.dex,接下来就是替换

private void setAPKClassLoader(ClassLoader classLoader) throws Exception {
    writeField(getLoadedApk(), "mClassLoader", classLoader);
}

private static Object getLoadedApk() throws Exception {
    Map> mPackages = (Map>) readField(instance(), "mPackages", true);
    for (String s : mPackages.keySet()) {
        WeakReference wr = mPackages.get(s);
        if (wr != null && wr.get() != null) {
            return wr.get();
        }
    }
    return null;
}

先看 instance() 是什么

public class ActivityThreadCompat {

    private static Object sActivityThread;

    private static Class sClass;

    public synchronized static final Object instance() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        if (sActivityThread == null) {
            sActivityThread = MethodUtils.invokeStaticMethod(clazz(), "currentActivityThread");
        }
        return sActivityThread;
    }

    public static final Class clazz() throws ClassNotFoundException {
        if (sClass == null) {
            sClass = Class.forName("android.app.ActivityThread");
        }
        return sClass;
    }
}

所以 instance() = ActivityThread.currentActivityThread()

要知道 ActivityThread 是什么,有什么作用,得去了解 App 启动过程、系统如何管理 Activity 的生命周期

public final class ActivityThread {

    private static ActivityThread sCurrentActivityThread;

    public static ActivityThread currentActivityThread() {
        return sCurrentActivityThread;
    }

    private void attach(boolean system) {
        sCurrentActivityThread = this;
        ...
    }

    final ArrayMap> mPackages
        = new ArrayMap>();

getLoadedApk() 最终得到的就是LoadedApk的一个实例
所以 setAPKClassLoader() 做的事情就是

final ArrayMap> mPackages
        = ActivityThread.currentActivityThread().mPackages;

LoadedApk loadedApk = null;
for (String s : mPackages.keySet()) {
    WeakReference wr = mPackages.get(s);
    if (wr != null && wr.get() != null) {
        loadedApk = wr.get();
        break;
    }
}
loadedApk.mClassLoader = amigoClassLoader;

2、替换 AssetManager
同样都是用反射实现,这里都翻译一下,把反射改为正常写法

AssetManager assetManager = new AssetManager();
assetManager.addAssetPath("/data/data/{package_name}/files/amigo/{checksum}/patch.apk");
assetManager.ensureStringBlocks();
Collection> references;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    if (ResourcesManager.getInstance().mActiveResources != null) {
        ArrayMap, WeakReference> arrayMap = ResourcesManager.getInstance().mActiveResources;
        references = arrayMap.values();
    } else {
        references = ResourcesManager.getInstance().mResourceReferences;
    }
} else {
    HashMap, WeakReference> map = ActivityThread.currentActivityThread().mActiveResources;
    references = map.values();
}

for (WeakReference wr : references) {
    Resources resources = wr.get();
    if (resources == null) continue;
    try {
        resources.mAssets = assetManager; //可能反射调用出错
    } catch (Throwable ignore) {
        resources.mResourcesImpl.mAssets = assetManager;
    }
    resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    for (WeakReference wr : references) {
        Resources resources = wr.get();
        if (resources == null) continue;
        // Clear all the pools
        while (resources.mTypedArrayPool.acquire() != null) ;
    }
}

3、替换 Instrumentation

Instrumentation oldInstrumentation = ActivityThread.currentActivityThread().mInstrumentation;
AmigoInstrumentation amigoInstrumentation = new AmigoInstrumentation(oldInstrumentation);
ActivityThread.currentActivityThread().mInstrumentation = amigoInstrumentation;

4、替换 H

Handler handler = ActivityThread.currentActivityThread().mH;
Callback mCallback = handler.mCallback;
AmigoCallback amigoCallback = new AmigoCallback(Amigo.this, amigoClassLoader, callback);
ActivityThread.currentActivityThread().mH = amigoCallback;

5、注册广播

这里不介绍如何找出新增的Receiver,只是需要注意的是:要使用 amigoClassLoader 来加载类

for (IntentFilter filter : filters) {
    BroadcastReceiver receiver = (BroadcastReceiver) classLoader.loadClass(addedReceiver.name).newInstance();
    context.registerReceiver(receiver, filter);
}

新增的静态注册的 receiver,这里就在 Amigo 这个 Application 的 onCreate() 里动态注册

6、IActivityManagerNative Hook
先说这个步骤的作用:用来拦截替换 Service

简单说下 startService 的过程
client
ContextImpl -->
ActivityManagerNative.ActivityManagerProxy.startService() -->
server
ActivityManagerService --> ... ->
client
ActivityThread.ApplicationThread.scheduleCreateService() -->
ActivityThread.H.sendMessage() -->
ActivityThread.H.handleMessage() -->
ActivityThread.handleCreateService() -->
Service.onCreate() --> return -->
server
ActivityManagerService -->
client
ActivityThread.ApplicationThread.scheduleServiceArgs() -->
ActivityThread.H.sendMessage() -->
ActivityThread.H.handleMessage() -->
ActivityThread.handleServiceArgs() -->
Service.onStart() & Service.onStartCommand()

private void installHook(AmigoClassLoader amigoClassLoader) throws Exception {
    Class hookFactoryClazz = amigoClassLoader.loadClass(HookFactory.class.getName());
    MethodUtils.invokeStaticMethod(hookFactoryClazz, "install", this, amigoClassLoader);
}

即 HookFactory.install(Amigo.this, amigoClassLoader);

public class HookFactory {

    private static List mHookList = new ArrayList<>(1);

    public static void install(Context context, ClassLoader cl) {
        installHook(new IActivityManagerHook(context), cl);
    }

    private static void installHook(Hook hook, ClassLoader cl) {
        try {
            hook.onInstall(cl);
            synchronized (mHookList) {
                mHookList.add(hook);
            }
        } catch (Throwable throwable) {
        }
    }

}

相当于

new IActivityManagerHook(Amigo.this).onInstall(amigoClassLoader);

代码比较多,主要就是动态代理 ActivityManagerNative,用来拦截 Service,关键类 IActivityManagerHookHandle

public class IActivityManagerHookHandle extends BaseHookHandle {

    private static final String TAG = IActivityManagerHookHandle.class.getSimpleName();

    public IActivityManagerHookHandle(Context context) {
        super(context);
    }

    @Override
    protected void init() {
        sHookedMethodHandlers.put("startService", new startService(context));
        sHookedMethodHandlers.put("stopService", new stopService(context));
        sHookedMethodHandlers.put("stopServiceToken", new stopServiceToken(context));
        sHookedMethodHandlers.put("bindService", new bindService(context));
        sHookedMethodHandlers.put("unbindService", new unbindService(context));
        sHookedMethodHandlers.put("unbindFinished", new unbindFinished(context));
        sHookedMethodHandlers.put("peekService", new peekService(context));
    }
... 

framework 的 client 向 server 发起 binder 请求时,代理拦截,在 beforeInvoke() 将 TargetService 替换成 .stub.ServiceStub,并把原来的 intent 存入 Extras(如果TargetService不是新增的,就不会进行替换)

newIntent.putExtra(AmigoInstrumentation.EXTRA_TARGET_INTENT, intent);

server 处理完回到 client,进入 ServiceStub 的生命周期方法

public class ServiceStub extends AbstractServiceStub {

}

AbstractServiceStub 实现了 Service 大部分的方法,方法内根据

Intent targetIntent = intent.getParcelableExtra(EXTRA_TARGET_INTENT);

拿到原来的 Service,再调用它的生命周期方法,比如 onStart()

[AbstractServiceStub.java]

private static ServiceManager mCreator = ServiceManager.getDefault();

@Override
public void onStart(Intent intent, int startId) {
    try {
        if (intent != null) {
            if (intent.getBooleanExtra("ActionKillSelf", false)) {
                startKillSelf();
                if (!ServiceManager.getDefault().hasServiceRunning()) {
                    stopSelf(startId);
                    boolean stopService = getApplication().stopService(intent);
                } else {
                }
            } else {
                mCreator.onStart(this, intent, 0, startId);
            }
        }
    } catch (Throwable e) {
        handleException(e);
    }
    super.onStart(intent, startId);
}

[ServiceManager.java]

public int onStart(Context context, Intent intent, int flags, int startId) throws Exception {
    Intent targetIntent = intent.getParcelableExtra(EXTRA_TARGET_INTENT);
    if (targetIntent != null) {
        ServiceInfo targetInfo = ServiceFinder.resolveServiceInfo(context, targetIntent);
        if (targetInfo != null) {
            Service service = mNameService.get(targetInfo.name);
            if (service == null) {
                handleCreateServiceOne(targetInfo);
            }
            return handleOnStartOne(context, targetIntent, flags);
        }
    }
    return -1;
}

private int handleOnStartOne(Context context, Intent intent, int flags) throws Exception {
    ServiceInfo info = ServiceFinder.resolveServiceInfo(context, intent);
    if (info != null) {
        Service service = mNameService.get(info.name);
        if (service != null) {
            ClassLoader classLoader = getClassLoader(context);
            intent.setExtrasClassLoader(classLoader);
            Object token = findTokenByService(service);
            Integer integer = mServiceTaskIds.get(token);
            if (integer == null) {
                integer = -1;
            }
            int startId = integer + 1;
            mServiceTaskIds.put(token, startId);
            int res = service.onStartCommand(intent, flags, startId);
            QueuedWorkCompat.waitToFinish();
            return res;
        }
    }
    return -1;
}

最终还是调用 service.onStartCommand(intent, flags, startId),这个 service 就是 TargetService,在 handleCreateServiceOne() 里面初始化的

private void handleCreateServiceOne(ServiceInfo info) throws Exception {
    Object activityThread = ActivityThreadCompat.instance();
    IBinder fakeToken = new MyFakeIBinder();
    Class CreateServiceData = Class.forName(ActivityThreadCompat.clazz().getName() + "$CreateServiceData");
    Constructor init = CreateServiceData.getDeclaredConstructor();
    if (!init.isAccessible()) {
        init.setAccessible(true);
    }
    Object data = init.newInstance();

    FieldUtils.writeField(data, "token", fakeToken);
    FieldUtils.writeField(data, "info", info);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        FieldUtils.writeField(data, "compatInfo", CompatibilityInfoCompat.DEFAULT_COMPATIBILITY_INFO());
    }

    MethodUtils.invokeMethod(activityThread, "handleCreateService", data);

    Object mService = FieldUtils.readField(activityThread, "mServices");
    Service service = (Service) MethodUtils.invokeMethod(mService, "get", fakeToken);
    MethodUtils.invokeMethod(mService, "remove", fakeToken);

    mTokenServices.put(fakeToken, service);
    mNameService.put(info.name, service);
}

相当于

CreateServiceData data = new CreateServiceData();
data.token = fakeToken;
data.info = info;
data.compatInfo = ...
activityThread.handleCreateService(data);
Service service = activityThread.mServices.get(fakeToken);
activityThread.mServices.remove(fakeToken);
mNameService.put(info.name, service);

7、运行原始的 Application

private void runOriginalApplication() throws Exception {
    setAPKClassLoader(originalClassLoader);
    Class acd = originalClassLoader.loadClass("me.ele.amigo.acd");
    String applicationName = (String) readStaticField(acd, "n");
    Application application =
            (Application) originalClassLoader.loadClass(applicationName).newInstance();
    Method attach = getMatchedMethod(Application.class, "attach", Context.class);
    attach.setAccessible(true);
    attach.invoke(application, getBaseContext());
    setAPKApplication(application);
    application.onCreate();
}

把原始的 Application 的全类名保存在 me.ele.amigo.acd.n,取出来初始化,并调用 application.attach(getBaseContext()) 和 application.onCreate() 到这里就修复完毕了,并把控制权还给原始的 Application

8、小结
Application 是直接使用新的,上个版本的 Application 完全无视,还有在 Amigo 的 onCreate() 里动态注册 AndroidManifest.xml 里新增的 receiver,还有使用了新的 AssetManager,所以 res 也完全没问题

四、替换 Activity 的过程

简单说下 startActivity 的过程
client
ContextImpl --> Instrumentation.execStartActivity() -->
server
ActivityManagerService -->...-->
client
ActivityThread.ApplicationThread.scheduleLaunchActivity() -->
ActivityThread.H.sendMessage() -->
ActivityThread.H.handleMessage() -->
ActivityThread.handleLaunchActivity() -->
Instrumentation.callActivityOnCreate() -->
Activity.onCreate() & Activity.onStart()

1、startActivity

Intent targetIntent = new Intent(context, TargetActivity.class);
context.startActivity(targetIntent);

2、[ContextImpl.java]

mMainThread.getInstrumentation().execStartActivity(
            getOuterContext(), mMainThread.getApplicationThread(), null,
            (Activity) null, intent, -1, options);

activityThread 的 mInstrumentation 已经被替换成 AmigoInstrumentation

3、[AmigoInstrumentation.java]

public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {

    //根据 launchMode 和 activity name 选出一个合适的 ActivityStub
    //从上面 AndroidManifest.xml 看到新增注册了很多个 Activity,有:.stub.ActivityStub$StandardStub, .stub.ActivityStub$SingleInstanceStub8, .stub.ActivityStub$SingleTopStub8...)
    //比如是标准启动模式,那么TargetActivity.class就被替换成.stub.ActivityStub$StandardStub.class(如果TargetActivity不是新增的,就不会进行替换)
    //这样做是因为没有在 AndroidManifest.xml 注册的 Activity,是没办法通过 framework 服务端的校验,所以得替换成已注册的 Activity
    intent = wrapIntent(who, intent);

    return oldInstrumentation.execStartActivity(...);

}

服务端处理完毕,通知 activityThread.ApplicationThread.scheduleLaunchActivity()
--> activityThread.mH.sendMessage()
activityThread.mH 也已被替换成 AmigoCallback

4、[AmigoCallback.java]

    @Override
    public boolean handleMessage(Message msg) {

        if (msg.what == LAUNCH_ACTIVITY) {
            //把.stub.ActivityStub$StandardStub.class 替换回 TargetActivity.class,然后再调用 mCallback.handleMessage(msg)
            // mCallback 就是原本的 activityThread.mH
            return handleLaunchActivity(msg);
        }
        if (mCallback != null) {
            return mCallback.handleMessage(msg);
        }
        return false;
    }

--> activityThread.handleLaunchActivity()
--> mInstrumentation.callActivityOnCreate()

5、[AmigoInstrumentation.java]

@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
    try {
        Intent targetIntent = activity.getIntent();
        if (targetIntent != null) {
            ActivityInfo targetInfo = targetIntent.getParcelableExtra(EXTRA_TARGET_INFO);
            if (targetInfo != null) {
                activity.setRequestedOrientation(targetInfo.screenOrientation);

                ComponentName componentName = new ComponentName(activity, getDelegateActivityName(activity, activity.getClass().getName()));
                FieldUtils.writeField(activity, "mComponent", componentName);

                Class stubClazz = (Class) targetIntent.getSerializableExtra(EXTRA_STUB_NAME);
                if (stubClazz != null)
                    ActivityStub.onActivityCreated(stubClazz, activity, "");
            }
        }
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }

    if (oldInstrumentation != null) {
        oldInstrumentation.callActivityOnCreate(activity, icicle);
    } else {
        super.callActivityOnCreate(activity, icicle);
    }

}

五、局限
上面的分析都没看到注册 provider,所以不支持新增 provider(不修改全类名都支持修复,dex都全盘替换了)
也没有 notification 和 widget 的相关处理,所以也不支持这2个相关控件的修改
另外的限制就是:launcher activity 的全类名暂时不支持修改、新增 permission
除了官方 wiki 说的这些局限外,IActivityManagerHookHandle.java 里面有这样一句话

TODO: we may need to support new added services in multi-process app

欢迎关注我的博客