与Android热更新方案Amigo的亲密接触

以前写过一片:与Android热更新方案Amigo的初次接触,主要是记叙了Amigo的接入和使用。java

最近读了一下Amigo的源码,并且看了看网上其余讲Amigo源码的文章,它们所针对的代码都和最新的Amigo代码有所出路,因此针对最新的代码,浅浅的分析一下。android

Amigo的最近更新已是8个月以前了。我最近更新了Android Studio3.0,gradle版本3.1.1。可是Amigo使用的gradle版本2.3.1。若是项目仍是要使用gradle3.x版本的话,会报错。因此我更新了Amigo插件使用的gradle版本(由于的gradle3.0比起2.0有一些修改,因此也修改了部分Amigo插件代码),若是有使用Amigo的老铁同时用的是gradle3.x版本的话,能够找我要代码。git

Amigo主要有两个部分,在Github上能够看到,amigo-lib和buildSrc。github

amigo-lib对应:bash

dependencies {
    ...
    compile 'me.ele:amigo-lib:0.6.7'
}
复制代码

buildSrc对应插件:app

dependencies {
        ......
        classpath 'me.ele:amigo:0.6.8'
}
复制代码
apply plugin: 'me.ele.amigo'
复制代码

先说插件,这个插件的做用就是修改AndroidManifest.xml,将项目本来的Application替换称Amigo.java,而且将原来的 application 的 name 保存在了一个名为acd的类中。AndroidManifest.xml 中将原来的 application 作为一个 Activity。ide

再说amigo-lib,这个是热更新的重点。咱们从更新的入口开始提及:post

button.setOnClickListener {
            var file = File(Environment.getExternalStorageDirectory().path + File.separator + "test.apk")
            if(file.exists()){
                Amigo.workLater(this, file) {
                    if(it){
                        toast("更新成功!")
                        val intent = packageManager.getLaunchIntentForPackage(packageName)
                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        startActivity(intent)
                        android.os.Process.killProcess(android.os.Process.myPid())
                    }
                }
            }
        }
复制代码

本地已经有了一个新的APK,直接调用Amigo.workLater()开始更新。gradle

private static void workLater(Context context, File patchFile, boolean checkSignature, WorkLaterCallback callback) {
        String patchChecksum = PatchChecker.checkPatchAndCopy(context, patchFile, checkSignature);
        if (checkWithWorkingPatch(context, patchChecksum)) return;
        if (patchChecksum == null) {
            Log.e(TAG, "#workLater: empty checksum");
            return;
        }

        if (callback != null) {
            AmigoService.startReleaseDex(context, patchChecksum, callback);
        } else {
            AmigoService.startReleaseDex(context, patchChecksum);
        }
    }
复制代码

第一步

将新的安装包拷贝到了/data/data/{package_name}/files/amigo/{checksum}/patch.apk。checksum是根据APK算出的,每一个APK都不一样,能够理解为APK的id。固然,在拷贝以前作了一些校验,主要是之前拷贝过吗?是否是如今正在运行的版本?

第二步

释放Dex。在 AmigoService.startReleaseDex()中主要是启动了AmigoService。在AmigoService.java中会调用:优化

private synchronized void handleReleaseDex(Intent intent) {
        String checksum = intent.getStringExtra(EXTRA_APK_CHECKSUM);
        if (apkReleaser == null) {
            apkReleaser = new ApkReleaser(getApplicationContext());
        }
        apkReleaser.release(checksum, msgHandler);
    }
复制代码

具体的释放Dex在ApkReleaser的release()中:

public void release(final String checksum, final Handler msgHandler) {
        if (isReleasing) {
            Log.w(TAG, "release : been busy now, skip release " + checksum);
            return;
        }

        Log.d(TAG, "release: start release " + checksum);
        try {
            this.amigoDirs = AmigoDirs.getInstance(context);
            this.patchApks = PatchApks.getInstance(context);
        } catch (Exception e) {
            Log.e(TAG,
                    "release: unable to create amigo dir and patch apk dir, abort release dex files",
                    e);
            handleDexOptFailure(checksum, msgHandler);
            return;
        }
        isReleasing = true;
        service.submit(new Runnable() {
            @Override
            public void run() {
                if (!new DexExtractor(context, checksum).extractDexFiles()) {
                    Log.e(TAG, "releasing dex failed");
                    handleDexOptFailure(checksum, msgHandler);
                    isReleasing = false;
                    FileUtils.removeFile(amigoDirs.dexDir(checksum), false);
                    return;
                }

                // todo
                // just create a link point to /data/app/{package_name}/libs
                // if none of the native libs are changed
                int errorCode;
                if ((errorCode =
                        NativeLibraryHelperCompat.copyNativeBinaries(patchApks.patchFile(checksum),
                                amigoDirs.libDir(checksum))) < 0) {
                    Log.e(TAG, "coping native binaries failed, errorCode = " + errorCode);
                    handleDexOptFailure(checksum, msgHandler);
                    FileUtils.removeFile(amigoDirs.dexDir(checksum), false);
                    FileUtils.removeFile(amigoDirs.libDir(checksum), false);
                    isReleasing = false;
                    return;
                }

                final boolean dexOptimized = Build.VERSION.SDK_INT >= 21 ? dexOptimizationOnArt(checksum)
                        : dexOptimizationOnDalvik(checksum);
                if (dexOptimized) {
                    Log.e(TAG, "optimize dex succeed");
                    handleDexOptSuccess(checksum, msgHandler);
                    isReleasing = false;
                    return;
                }

                Log.e(TAG, "optimize dex failed");
                FileUtils.removeFile(amigoDirs.dexDir(checksum), false);
                FileUtils.removeFile(amigoDirs.libDir(checksum), false);
                FileUtils.removeFile(amigoDirs.dexOptDir(checksum), false);
                handleDexOptFailure(checksum, msgHandler);
                isReleasing = false;
            }
        });
    }
复制代码

这里又分了3小步:1.释放dex;2.释放lib中的so;3.优化dex。

DexExtractor(context, checksum).extractDexFiles()中是具体的释放dex过程:

public boolean extractDexFiles() {
        if (Build.VERSION.SDK_INT >= 21) {
            return true; // art supports multi-dex natively
        }

        return performExtractions(PatchApks.getInstance(context).patchFile(checksum),
                AmigoDirs.getInstance(context).dexDir(checksum));
    }

    //把patchApk(新包)中的dex解压到dexDir:/data/data/{package_name}/files/amigo/{checksum}/dexes
private boolean performExtractions(File patchApk, File dexDir) {
        ZipFile apk = null;
        try {
            apk = new ZipFile(patchApk);
            int dexNum = 0;
            ZipEntry dexFile = apk.getEntry("classes.dex");
            for (; dexFile != null; dexFile = apk.getEntry("classes" + dexNum + ".dex")) {
                String fileName = dexFile.getName().replace("dex", "zip");
                File extractedFile = new File(dexDir, fileName);
                extract(apk, dexFile, extractedFile);
                verifyZipFile(extractedFile);
                if (dexNum == 0) ++dexNum;
                ++dexNum;
            }
            return dexNum > 0;
        } catch (IOException ioe) {
            ioe.printStackTrace();
            return false;
        } finally {
            try {
                apk.close();
            } catch (IOException var16) {
                Log.w("DexExtractor", "Failed to close resource", var16);
            }
        }
    }
复制代码

这里有个extract()方法,进去看一下:

private void extract(ZipFile patchApk, ZipEntry dexFile, File extractTo) throws IOException {
        boolean reused = reusePreExistedODex(patchApk, dexFile);
        Log.d(TAG, "extracted: "
                + dexFile.getName() + " success ? "
                + reused
                + ", by reusing pre-existed secondary dex");
        //能够若是复用旧dex
        if (reused) {
            return;
        }
        //不能复用,就执行拷贝
        InputStream in = null;
        File tmp = null;
        ZipOutputStream out = null;
        try {
            in = patchApk.getInputStream(dexFile);
            tmp = File.createTempFile(extractTo.getName(), ".tmp", extractTo.getParentFile());
            try {
                out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
                ZipEntry classesDex = new ZipEntry("classes.dex");
                classesDex.setTime(dexFile.getTime());
                out.putNextEntry(classesDex);
                if (buffer == null) {
                    buffer = new byte[16384];
                }
                for (int length = in.read(buffer); length != -1; length = in.read(buffer)) {
                    out.write(buffer, 0, length);
                }
            } finally {
                if (out != null) {
                    out.closeEntry();
                    out.close();
                }
            }

            if (!tmp.renameTo(extractTo)) {
                throw new IOException("Failed to rename \""
                        + tmp.getAbsolutePath()
                        + "\" to \""
                        + extractTo.getAbsolutePath()
                        + "\"");
            }
        } finally {
            closeSilently(in);
            if (tmp != null) tmp.delete();
        }
    }
复制代码

在这里咱们看到了具体的拷贝过程,先是拷贝到了tmp文件,而后改的名。可是在拷贝以前有一个操做reusePreExistedODex(),在这个方法中判断了当前运行的App的dex与更新包的dex是否一致,若是一致作了一个link,把当前APP的dex文件link到了咱们须要拷贝的目录dexDir:/data/data/{package_name}/files/amigo/{checksum}/dexes,若是不一致再作拷贝操做。

到此dex释放完毕,下一步释放so文件。NativeLibraryHelperCompat.copyNativeBinaries()这里就是判断,根据不一样的系统版本(好比是64位还32位系统),调用了不一样拷贝方法。具体的这篇文章有详细说。

而后就是优化dex了。final boolean dexOptimized = Build.VERSION.SDK_INT >= 21 ? dexOptimizationOnArt(checksum) : dexOptimizationOnDalvik(checksum)这里针对系统版本调用了不一样的方法。

第三步

保存标志,而后重启:

private void handleDexOptSuccess(String checksum, Handler msgHandler) {
        saveDexAndSoChecksum(checksum);
        PatchInfoUtil.updateDexFileOptStatus(context, checksum, true);
        PatchInfoUtil.setWorkingChecksum(context, checksum);
        if (msgHandler != null) {
            msgHandler.sendEmptyMessage(AmigoService.MSG_ID_DEX_OPT_SUCCESS);
        }
    }
复制代码

到此释放阶段结束。

下面是重启后,读取资源。代码在Amigo.java中,先看看attachApplication(). 在attachApplication()中先作了一系列判断,主要是判断是否须要读取释放后的文件(好比是否有更新文件,是否须要更新),咱们直接进入读取文件的地方attachPatchApk(workingChecksum);

private void attachPatchApk(String checksum) throws LoadPatchApkException {
        try {
            if (isPatchApkFirstRun(checksum)
                    || !AmigoDirs.getInstance(this).isOptedDexExists(checksum)) {
                PatchInfoUtil.updateDexFileOptStatus(this, checksum, false);
                releasePatchApk(checksum);
            } else {
                PatchChecker.checkDexAndSo(this, checksum);
            }

            setAPKClassLoader(AmigoClassLoader.newInstance(this, checksum));
            setApkResource(checksum);
            revertBitFlag |= getClassLoader() instanceof AmigoClassLoader ? 1 : 0;
            attachPatchedApplication(checksum);
            PatchCleaner.clearOldPatches(this, checksum);
            shouldHookAmAndPm = true;
            Log.i(TAG, "#attachPatchApk: success");
        } catch (Exception e) {
            throw new LoadPatchApkException(e);
        }
    }
复制代码

这里有两个地方setAPKClassLoader(AmigoClassLoader.newInstance(this, checksum)); setApkResource(checksum); 分别是设置 ClassLoader 和加载资源。

先说ClassLoader,这里Hook了一个ClassLoader,使用了本身的AmigoClassLoader。

public static AmigoClassLoader newInstance(Context context, String checksum) {
        return new AmigoClassLoader(PatchApks.getInstance(context).patchPath(checksum),
                getDexPath(context, checksum),
                AmigoDirs.getInstance(context).dexOptDir(checksum).getAbsolutePath(),
                getLibraryPath(context, checksum),
                AmigoClassLoader.class.getClassLoader().getParent());
    }
复制代码

经过上面的代码能够看出AmigoClassLoader使用了咱们以前释放的资源的目录,也就是dex,libs等。

private void setApkResource(String checksum) throws Exception {
        PatchResourceLoader.loadPatchResources(this, checksum);
        Log.i(TAG, "hook Resources success");
    }
static void loadPatchResources(Context context, String checksum) throws Exception {
        AssetManager newAssetManager = AssetManager.class.newInstance();
        invokeMethod(newAssetManager, "addAssetPath", PatchApks.getInstance(context).patchPath(checksum));
        invokeMethod(newAssetManager, "ensureStringBlocks");
        replaceAssetManager(context, newAssetManager);
    }
复制代码

replaceAssetManager(context, newAssetManager)中Hook了一些其余AssetManager用到的地方。

而后把shouldHookAmAndPm设成了true。 到这里attachApplication()执行完成。而后走onCreate()

public void onCreate() {
        super.onCreate();
        try {
            setAPKApplication(realApplication);
        } catch (Exception e) {
            // should not happen, if it does happen, we just let it die
            throw new RuntimeException(e);
        }
        if(shouldHookAmAndPm) {
            try {
                installAndHook();
            } catch (Exception e) {
                try {
                    clear(this);
                    attachOriginalApplication();
                } catch (Exception e1) {
                    throw new RuntimeException(e1);
                }
            }
        }
        realApplication.onCreate();
    }
复制代码

这里有个:

private void installAndHook() throws Exception {
        boolean gotNewActivity = ActivityFinder.newActivityExistsInPatch(this);
        if (gotNewActivity) {
            setApkInstrumentation();
            revertBitFlag |= 1 << 1;
            setApkHandlerCallback();
            revertBitFlag |= 1 << 2;
        } else {
            Log.d(TAG, "installAndHook: there is no any new activity, skip hooking " +
                    "instrumentation & mH's callback");
        }
        installHookFactory();
        dynamicRegisterNewReceivers();
        installPatchContentProviders();
    }
复制代码

首先判断是否有新的Activity,若是有,就须要HookmInstrumentationinstallHookFactory()中替换了ClassLoader,动态注册了Receiver,安装ContentProvider。

最后调用attachOriginalApplication(),把以前的Application替换回来,而后走正常的流程。


【2018/10/11】

最近,适配了Android O。有须要的,拿去-> github

相关文章
相关标签/搜索