I wrote a piece before: My first contact with Amigo, the hot update solution for Android, mainly describing the access and use of Amigo.

Recently read Amigo source code, and looked at other online Amigo source articles, they are aimed at the code and the latest Amigo code has a way out, so for the latest code, a shallow analysis.

The last update to Amigo was 8 months ago. I recently updated Android Studio3.0, Gradle version 3.1.1. But Amigo uses Gradle version 2.3.1. An error is reported if the project is still using gradle3.x. So I updated the gradle version of the Amigo plugin (because gradle3.0 has some changes compared to 2.0, so I also modified some of the Amigo plugin code), if you have old iron using Amigo and gradle3.x, please ask me for the code.

Amigo has two main parts, which you can see on Github, amigo-lib and buildSrc.

Amigo – lib for:

dependencies {
    ...
    compile 'me. Ele: amigo - lib: 0.6.7'
}
Copy the code

BuildSrc plugin:

dependencies {
        ......
        classpath 'me. Ele: amigo: 0.6.8'
}
Copy the code
apply plugin: 'me.ele.amigo'
Copy the code

The plugin modifies androidmanifest.xml, replaces the project’s original Application with amigo.java, and stores the name of the original Application in a class named ACD. Androidmanifest.xml uses the original application as an Activity.

Again, amigo-lib, which is the focus of hot updates. Let’s start with the update entry:

 button.setOnClickListener {
            var file = File(Environment.getExternalStorageDirectory().path + File.separator + "test.apk")
            if(file.exists()){
                Amigo.workLater(this, file) {
                    if(it){
                        toast("Update successful!")
                        val intent = packageManager.getLaunchIntentForPackage(packageName)
                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        startActivity(intent)
                        android.os.Process.killProcess(android.os.Process.myPid())
                    }
                }
            }
        }
Copy the code

With a new APK already in place, call amigo.worklater () to start the update.

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); }}Copy the code

The first step

The second step

The release of Dex. In AmigoService. StartReleaseDex mainly launched AmigoService (). In amigoservice.java it calls:

private synchronized void handleReleaseDex(Intent intent) {
        String checksum = intent.getStringExtra(EXTRA_APK_CHECKSUM);
        if (apkReleaser == null) {
            apkReleaser = new ApkReleaser(getApplicationContext());
        }
        apkReleaser.release(checksum, msgHandler);
    }
Copy the code

The specific Dex release is in the release() of ApkReleaser:

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; }}); }Copy the code

Here are three more steps: 1. Release dex; 2. Release so in lib; 3. Optimize dex.

In DexExtractor(context, checksum).extractdexFiles () is the specific dex release process:

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

        returnperformExtractions(PatchApks.getInstance(context).patchFile(checksum), AmigoDirs.getInstance(context).dexDir(checksum));  }// Unpack the dex from patchApk into 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); }}}Copy the code

Here’s the extract() method:

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");
        // You can reuse the old dex
        if (reused) {
            return;
        }
        // If it cannot be reused, copy it
        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 (intlength = 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(); }}Copy the code

Here we see the process of copying the TMP file and changing its name. However, there is an operation reusePreExistedODex() before copying. In this method, it is determined whether the dex of the currently running App is consistent with the dex of the update package. If a link is made consistently, Link the dex file of the current APP to the directory dexDir:/data/data/{package_name}/files/amigo/{checksum}/dexes. If they are inconsistent, copy them again.

After releasing dex, release so file next. NativeLibraryHelperCompat. CopyNativeBinaries () here is the judgment, according to the different system version (for example, is a 64 – bit 32-bit system), calls the different copy method. The specific article is detailed.

Then it’s time to optimize dex. final boolean dexOptimized = Build.VERSION.SDK_INT >= 21 ? DexOptimizationOnArt (checksum) : dexOptimizationOnDalvik(checksum) Here different methods are called for the system version.

The third step

Save the flag and restart:

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);
        }
    }
Copy the code

This is the end of the release phase.

The following is read resource after restart. The code is in Amigo.java, and take a look at attachApplication(). In attachApplication(), a series of decisions are made to determine whether the released files need to be accessed (for example, whether the files need to be updated). AttachPatchApk (workingChecksum) is used to retrieve the files.

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 newLoadPatchApkException(e); }}Copy the code

There are two place setAPKClassLoader (AmigoClassLoader newInstance (this, checksum)); setApkResource(checksum); Set the ClassLoader and load the resource respectively.

AmigoClassLoader (); ClassLoader ();

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());
    }
Copy the code

From the above code, we can see that AmigoClassLoader uses the directory of resources we released earlier, namely dex, libs, etc.

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);
    }
Copy the code

ReplaceAssetManager (Context, newAssetManager) hooks something that other assetManagers use.

And I’m going to set shouldHookAmAndPm to true. AttachApplication () is complete. Then go 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();
    }
Copy the code

Here’s one:

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();
    }
Copy the code

First determine if there is a new Activity; if there is, the HookmInstrumentation is required. InstallHookFactory () replaces ClassLoader, dynamically registers Receiver, installs ContentProvider.

Before the last call attachOriginalApplication (), the Application of replacement back, then go the normal process.


【 2018/10/11 】

More recently, Android O has been adapted. If you need it, take it -> Github.