Instant Run淺析

Instant Run是Android Studio2.0以后新增的一個運行機制,能夠顯著減少你第二次及以后的構建和部署時間。簡單通俗的解釋就是,當你在Android Studio中改了你的代碼,Instant Run可以很快的讓你看到你修改的效果。而在沒有Instant Run之前,你的一個小小的修改,都肯能需要幾十秒甚至更長的等待才能看到修改后的效果。


1.png

而Instant Run 需要更少的時間,只構建修改的部分 → 部署修改的dex或資源 → 熱部署,溫部署,冷部署


2.png

上述說這么多概念,估計大家對Instant Run應該有了大體的認知了。那么它的實現原理是什么呢?其實,在沒有看案例之前,我基本上可以猜測到Instant Run的思路,基于目前比較火的插件化框架,是比較容易理解Instant Run的。但Instant Run畢竟是Google官方的工具,具有很好的借鑒意義。 ##Demo案例 新建一個簡單的android studio項目,新建自己的MyApplication,在AndroidManifest文件中設置:

4.PNG

首先,我們先反編譯一下APK的構成: 使用的工具:d2j-dex2jar 和jd-gui

3.PNG

里面有2個dex文件和一個instant-run.zip文件。首先分別看一下兩個dex文件的源碼:
classes.dex的反編譯之后的源碼:


5.PNG

里面只有一個AppInfo,保存了app的基本信息,主要包含了包名和applicationClass。

classes2.dex反編譯之后的源碼:
6.PNG

我們赫然發現,兩個dex中竟然沒有一句我們自己寫的代碼??那么代碼在哪里呢?你可能猜到,app真正的業務dex在instant-run.zip中。解壓instant-run.zip之后,如下圖所示:


7.PNG

反編譯之后,我們會發現,我們真正的業務代碼都在這里。
另外,我們再decode看一下AndroidManifest文件:

8.PNG

我們發現,我們的application也被替換了,替換成了com.android.tools.fd.runtime.BootstrapApplication
看到這里,那么大體的思路,可以猜到: 1.Instant-Run代碼作為一個宿主程序,將app作為資源dex加載起來,和插件化一個思路 2.那么InstantRun是怎么把業務代碼運行起來的呢? InstantRun啟動app 首先BootstrapApplication分析,按照執行順序,依次分析attachBaseContext和onCreate方法。

attachBaseContext方法:

 protected void attachBaseContext(Context context) {
        if (!AppInfo.usingApkSplits) {
            String apkFile = context.getApplicationInfo().sourceDir;
            long apkModified = apkFile != null ? new File(apkFile)
                    .lastModified() : 0L;
            createResources(apkModified);
            setupClassLoaders(context, context.getCacheDir().getPath(),
                    apkModified);
        }
        createRealApplication();

        super.attachBaseContext(context);
        if (this.realApplication != null) {
            try {
                Method attachBaseContext = ContextWrapper.class
                        .getDeclaredMethod("attachBaseContext",
                                new Class[] { Context.class });

                attachBaseContext.setAccessible(true);
                attachBaseContext.invoke(this.realApplication,
                        new Object[] { context });
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        }
    }

我們依次需要關注的方法有: createResources → setupClassLoaders → createRealApplication → 調用realApplication的attachBaseContext方法

createResources 方法:

private void createResources(long apkModified) {
        FileManager.checkInbox();

        File file = FileManager.getExternalResourceFile();
        this.externalResourcePath = (file != null ? file.getPath() : null);
        if (Log.isLoggable("InstantRun", 2)) {
            Log.v("InstantRun", "Resource override is "
                    + this.externalResourcePath);
        }
        if (file != null) {
            try {
                long resourceModified = file.lastModified();
                if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun", "Resource patch last modified: "
                            + resourceModified);
                    Log.v("InstantRun", "APK last modified: " + apkModified
                            + " "
                            + (apkModified > resourceModified ? ">" : "<")
                            + " resource patch");
                }
                if ((apkModified == 0L) || (resourceModified <= apkModified)) {
                    if (Log.isLoggable("InstantRun", 2)) {
                        Log.v("InstantRun",
                                "Ignoring resource file, older than APK");
                    }
                    this.externalResourcePath = null;
                }
            } catch (Throwable t) {
                Log.e("InstantRun", "Failed to check patch timestamps", t);
            }
        }
    }

該方法主要是判斷資源resource.ap_是否改變,然后保存resource.ap_的路徑到externalResourcePath中

setupClassLoaders 方法:

private static void setupClassLoaders(Context context, String codeCacheDir,
            long apkModified) {
        List<String> dexList = FileManager.getDexList(context, apkModified);

        Class<Server> server = Server.class;
        Class<MonkeyPatcher> patcher = MonkeyPatcher.class;
        if (!dexList.isEmpty()) {
            if (Log.isLoggable("InstantRun", 2)) {
                Log.v("InstantRun", "Bootstrapping class loader with dex list "
                        + join('\n', dexList));
            }
            ClassLoader classLoader = BootstrapApplication.class
                    .getClassLoader();
            String nativeLibraryPath;
            try {
                nativeLibraryPath = (String) classLoader.getClass()
                        .getMethod("getLdLibraryPath", new Class[0])
                        .invoke(classLoader, new Object[0]);
                if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun", "Native library path: "
                            + nativeLibraryPath);
                }
            } catch (Throwable t) {
                Log.e("InstantRun", "Failed to determine native library path "
                        + t.getMessage());
                nativeLibraryPath = FileManager.getNativeLibraryFolder()
                        .getPath();
            }
            IncrementalClassLoader.inject(classLoader, nativeLibraryPath,
                    codeCacheDir, dexList);
        }
    }

繼續看IncrementalClassLoader.inject方法: IncrementalClassLoader的源碼如下:

 public class IncrementalClassLoader extends ClassLoader {
    public static final boolean DEBUG_CLASS_LOADING = false;
    private final DelegateClassLoader delegateClassLoader;

    public IncrementalClassLoader(ClassLoader original,
            String nativeLibraryPath, String codeCacheDir, List<String> dexes) {
        super(original.getParent());

        this.delegateClassLoader = createDelegateClassLoader(nativeLibraryPath,
                codeCacheDir, dexes, original);
    }

    public Class<?> findClass(String className) throws ClassNotFoundException {
        try {
            return this.delegateClassLoader.findClass(className);
        } catch (ClassNotFoundException e) {
            throw e;
        }
    }

    private static class DelegateClassLoader extends BaseDexClassLoader {

        private DelegateClassLoader(String dexPath, File optimizedDirectory,
                String libraryPath, ClassLoader parent) {
            super(dexPath, optimizedDirectory, libraryPath, parent);
        }

        public Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                return super.findClass(name);
            } catch (ClassNotFoundException e) {
                throw e;
            }
        }
    }

    private static DelegateClassLoader createDelegateClassLoader(
            String nativeLibraryPath, String codeCacheDir, List<String> dexes,
            ClassLoader original) {
        String pathBuilder = createDexPath(dexes);
        return new DelegateClassLoader(pathBuilder, new File(codeCacheDir),
                nativeLibraryPath, original);
    }

    private static String createDexPath(List<String> dexes) {
        StringBuilder pathBuilder = new StringBuilder();
        boolean first = true;
        for (String dex : dexes) {
            if (first) {
                first = false;
            } else {
                pathBuilder.append(File.pathSeparator);
            }
            pathBuilder.append(dex);
        }
        if (Log.isLoggable("InstantRun", 2)) {
            Log.v("InstantRun", "Incremental dex path is "
                    + BootstrapApplication.join('\n', dexes));
        }
        return pathBuilder.toString();
    }

    private static void setParent(ClassLoader classLoader, ClassLoader newParent) {
        try {
            Field parent = ClassLoader.class.getDeclaredField("parent");
            parent.setAccessible(true);
            parent.set(classLoader, newParent);
        } catch (IllegalArgumentException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }

    public static ClassLoader inject(ClassLoader classLoader,
            String nativeLibraryPath, String codeCacheDir, List<String> dexes) {
        IncrementalClassLoader incrementalClassLoader = new IncrementalClassLoader(
                classLoader, nativeLibraryPath, codeCacheDir, dexes);

        setParent(classLoader, incrementalClassLoader);

        return incrementalClassLoader;
    }
    }

inject方法是用來設置classloader的父子順序的,使用IncrementalClassLoader來加載dex。由于ClassLoader的雙親委托模式,也就是委托父類加載類,父類中找不到再在本ClassLoader中查找。 調用之后的效果如下圖所示:

classloader.png

我們可以在MyApplication中,用代碼驗證一下

 @Override
    public void onCreate() {
        super.onCreate();
        try{
            Log.d(TAG,"###onCreate in myApplication");
            String classLoaderName = getClassLoader().getClass().getName();
            Log.d(TAG,"###onCreate in myApplication classLoaderName = "+classLoaderName);
            String parentClassLoaderName = getClassLoader().getParent().getClass().getName();
            Log.d(TAG,"###onCreate in myApplication parentClassLoaderName = "+parentClassLoaderName);
            String pParentClassLoaderName = getClassLoader().getParent().getParent().getClass().getName();
            Log.d(TAG,"###onCreate in myApplication pParentClassLoaderName = "+pParentClassLoaderName);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

運行結果:

 06-30 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication classLoaderName = dalvik.system.PathClassLoader
06-30 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication parentClassLoaderName = com.android.tools.fd.runtime.IncrementalClassLoader
06-30 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication pParentClassLoaderName = java.lang.BootClassLoader

由此,我們已經知道了,當前PathClassLoader委托IncrementalClassLoader加載dex。繼續回到BootstrapApplication的attachBaseContext方法繼續分析。

createRealApplication方法:

 private void createRealApplication() {
        if (AppInfo.applicationClass != null) {
            if (Log.isLoggable("InstantRun", 2)) {
                Log.v("InstantRun",
                        "About to create real application of class name = "
                                + AppInfo.applicationClass);
            }
            try {
                Class<? extends Application> realClass = (Class<? extends Application>) Class
                        .forName(AppInfo.applicationClass);
                if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun",
                            "Created delegate app class successfully : "
                                    + realClass + " with class loader "
                                    + realClass.getClassLoader());
                }
                Constructor<? extends Application> constructor = realClass
                        .getConstructor(new Class[0]);
                this.realApplication = ((Application) constructor
                        .newInstance(new Object[0]));
                if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun",
                            "Created real app instance successfully :"
                                    + this.realApplication);
                }
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        } else {
            this.realApplication = new Application();
        }
    }

該方法就是用classes.dex中的AppInfo類的applicationClass常量中保存的app真實的application。由上面的反編譯截圖可以知道,demo中的applicationClass就是mobctrl.net.testinstantrun.MyApplication。通過反射的方式,創建realApplication。
然后接著調用realApplication的attachBaseContext方法 代理realApplication的生命周期,通過反射調用realApplication的attachBaseContext方法,以當前的Context為參數。
attachBaseContext方法執行結束之后,我們繼續往下看,到BootstrapApplication的onCreate方法 。onCreate 源碼如下:

public void onCreate() {
        if (!AppInfo.usingApkSplits) {
            MonkeyPatcher.monkeyPatchApplication(this, this,
                    this.realApplication, this.externalResourcePath);

            MonkeyPatcher.monkeyPatchExistingResources(this,
                    this.externalResourcePath, null);
        } else {
            MonkeyPatcher.monkeyPatchApplication(this, this,
                    this.realApplication, null);
        }
        super.onCreate();
        if (AppInfo.applicationId != null) {
            try {
                boolean foundPackage = false;
                int pid = Process.myPid();
                ActivityManager manager = (ActivityManager) getSystemService("activity");

                List<ActivityManager.RunningAppProcessInfo> processes = manager
                        .getRunningAppProcesses();
                boolean startServer = false;
                if ((processes != null) && (processes.size() > 1)) {
                    for (ActivityManager.RunningAppProcessInfo processInfo : processes) {
                        if (AppInfo.applicationId
                                .equals(processInfo.processName)) {
                            foundPackage = true;
                            if (processInfo.pid == pid) {
                                startServer = true;
                                break;
                            }
                        }
                    }
                    if ((!startServer) && (!foundPackage)) {
                        startServer = true;
                        if (Log.isLoggable("InstantRun", 2)) {
                            Log.v("InstantRun",
                                    "Multiprocess but didn't find process with package: starting server anyway");
                        }
                    }
                } else {
                    startServer = true;
                }
                if (startServer) {
                    Server.create(AppInfo.applicationId, this);
                }
            } catch (Throwable t) {
                if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun", "Failed during multi process check", t);
                }
                Server.create(AppInfo.applicationId, this);
            }
        }
        if (this.realApplication != null) {
            this.realApplication.onCreate();
        }
    }

我們依次需要關注的方法有: monkeyPatchApplication → monkeyPatchExistingResources → Server啟動 → 調用realApplication的onCreate方法。

monkeyPatchApplication : 該方法的目的可以總結為,替換所有當前app的application為realApplication。

public static void monkeyPatchApplication(Context context,
            Application bootstrap, Application realApplication,
            String externalResourceFile) {
        try {
            Class<?> activityThread = Class
                    .forName("android.app.ActivityThread");
            Object currentActivityThread = getActivityThread(context,
                    activityThread);

            Field mInitialApplication = activityThread
                    .getDeclaredField("mInitialApplication");
            mInitialApplication.setAccessible(true);
            Application initialApplication = (Application) mInitialApplication
                    .get(currentActivityThread);
            if ((realApplication != null) && (initialApplication == bootstrap)) {
                mInitialApplication.set(currentActivityThread, realApplication);
            }
            if (realApplication != null) {
                Field mAllApplications = activityThread
                        .getDeclaredField("mAllApplications");
                mAllApplications.setAccessible(true);
                List<Application> allApplications = (List<Application>) mAllApplications
                        .get(currentActivityThread);
                for (int i = 0; i < allApplications.size(); i++) {
                    if (allApplications.get(i) == bootstrap) {
                        allApplications.set(i, realApplication);
                    }
                }
            }
            Class<?> loadedApkClass;
            try {
                loadedApkClass = Class.forName("android.app.LoadedApk");
            } catch (ClassNotFoundException e) {
                loadedApkClass = Class
                        .forName("android.app.ActivityThread$PackageInfo");
            }
            Field mApplication = loadedApkClass
                    .getDeclaredField("mApplication");
            mApplication.setAccessible(true);
            Field mResDir = loadedApkClass.getDeclaredField("mResDir");
            mResDir.setAccessible(true);

            Field mLoadedApk = null;
            try {
                mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
            } catch (NoSuchFieldException e) {
            }
            for (String fieldName : new String[] { "mPackages",
                    "mResourcePackages" }) {
                Field field = activityThread.getDeclaredField(fieldName);
                field.setAccessible(true);
                Object value = field.get(currentActivityThread);
                for (Map.Entry<String, WeakReference<?>> entry : ((Map<String, WeakReference<?>>) value)
                        .entrySet()) {
                    Object loadedApk = ((WeakReference) entry.getValue()).get();
                    if (loadedApk != null) {
                        if (mApplication.get(loadedApk) == bootstrap) {
                            if (realApplication != null) {
                                mApplication.set(loadedApk, realApplication);
                            }
                            if (externalResourceFile != null) {
                                mResDir.set(loadedApk, externalResourceFile);
                            }
                            if ((realApplication != null)
                                    && (mLoadedApk != null)) {
                                mLoadedApk.set(realApplication, loadedApk);
                            }
                        }
                    }
                }
            }
        } catch (Throwable e) {
            throw new IllegalStateException(e);
        }
    }

具體做的事情可以總結為:
1.替換ActivityThread的mInitialApplication為realApplication 。
2.替換mAllApplications 中所有的Application為realApplication。
3.替換ActivityThread的mPackages,mResourcePackages中的mLoaderApk中的application為realApplication。

monkeyPatchExistingResources: 替換所有當前app的mAssets為newAssetManager。

 public static void monkeyPatchExistingResources(Context context,
            String externalResourceFile, Collection<Activity> activities) {
        if (externalResourceFile == null) {
            return;
        }
        try {
            AssetManager newAssetManager = (AssetManager) AssetManager.class
                    .getConstructor(new Class[0]).newInstance(new Object[0]);
            Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
                    "addAssetPath", new Class[] { String.class });
            mAddAssetPath.setAccessible(true);
            if (((Integer) mAddAssetPath.invoke(newAssetManager,
                    new Object[] { externalResourceFile })).intValue() == 0) {
                throw new IllegalStateException(
                        "Could not create new AssetManager");
            }
            Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod(
                    "ensureStringBlocks", new Class[0]);
            mEnsureStringBlocks.setAccessible(true);
            mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
            if (activities != null) {
                for (Activity activity : activities) {
                    Resources resources = activity.getResources();
                    try {
                        Field mAssets = Resources.class
                                .getDeclaredField("mAssets");
                        mAssets.setAccessible(true);
                        mAssets.set(resources, newAssetManager);
                    } catch (Throwable ignore) {
                        Field mResourcesImpl = Resources.class
                                .getDeclaredField("mResourcesImpl");
                        mResourcesImpl.setAccessible(true);
                        Object resourceImpl = mResourcesImpl.get(resources);
                        Field implAssets = resourceImpl.getClass()
                                .getDeclaredField("mAssets");
                        implAssets.setAccessible(true);
                        implAssets.set(resourceImpl, newAssetManager);
                    }
                    Resources.Theme theme = activity.getTheme();
                    try {
                        try {
                            Field ma = Resources.Theme.class
                                    .getDeclaredField("mAssets");
                            ma.setAccessible(true);
                            ma.set(theme, newAssetManager);
                        } catch (NoSuchFieldException ignore) {
                            Field themeField = Resources.Theme.class
                                    .getDeclaredField("mThemeImpl");
                            themeField.setAccessible(true);
                            Object impl = themeField.get(theme);
                            Field ma = impl.getClass().getDeclaredField(
                                    "mAssets");
                            ma.setAccessible(true);
                            ma.set(impl, newAssetManager);
                        }
                        Field mt = ContextThemeWrapper.class
                                .getDeclaredField("mTheme");
                        mt.setAccessible(true);
                        mt.set(activity, null);
                        Method mtm = ContextThemeWrapper.class
                                .getDeclaredMethod("initializeTheme",
                                        new Class[0]);
                        mtm.setAccessible(true);
                        mtm.invoke(activity, new Object[0]);

                        Method mCreateTheme = AssetManager.class
                                .getDeclaredMethod("createTheme", new Class[0]);
                        mCreateTheme.setAccessible(true);
                        Object internalTheme = mCreateTheme.invoke(
                                newAssetManager, new Object[0]);
                        Field mTheme = Resources.Theme.class
                                .getDeclaredField("mTheme");
                        mTheme.setAccessible(true);
                        mTheme.set(theme, internalTheme);
                    } catch (Throwable e) {
                        Log.e("InstantRun",
                                "Failed to update existing theme for activity "
                                        + activity, e);
                    }
                    pruneResourceCaches(resources);
                }
            }
            Collection<WeakReference<Resources>> references;
            if (Build.VERSION.SDK_INT >= 19) {
                Class<?> resourcesManagerClass = Class
                        .forName("android.app.ResourcesManager");
                Method mGetInstance = resourcesManagerClass.getDeclaredMethod(
                        "getInstance", new Class[0]);
                mGetInstance.setAccessible(true);
                Object resourcesManager = mGetInstance.invoke(null,
                        new Object[0]);
                try {
                    Field fMActiveResources = resourcesManagerClass
                            .getDeclaredField("mActiveResources");
                    fMActiveResources.setAccessible(true);

                    ArrayMap<?, WeakReference<Resources>> arrayMap = (ArrayMap) fMActiveResources
                            .get(resourcesManager);
                    references = arrayMap.values();
                } catch (NoSuchFieldException ignore) {
                    Field mResourceReferences = resourcesManagerClass
                            .getDeclaredField("mResourceReferences");
                    mResourceReferences.setAccessible(true);

                    references = (Collection) mResourceReferences
                            .get(resourcesManager);
                }
            } else {
                Class<?> activityThread = Class
                        .forName("android.app.ActivityThread");
                Field fMActiveResources = activityThread
                        .getDeclaredField("mActiveResources");
                fMActiveResources.setAccessible(true);
                Object thread = getActivityThread(context, activityThread);

                HashMap<?, WeakReference<Resources>> map = (HashMap) fMActiveResources
                        .get(thread);

                references = map.values();
            }
            for (WeakReference<Resources> wr : references) {
                Resources resources = (Resources) wr.get();
                if (resources != null) {
                    try {
                        Field mAssets = Resources.class
                                .getDeclaredField("mAssets");
                        mAssets.setAccessible(true);
                        mAssets.set(resources, newAssetManager);
                    } catch (Throwable ignore) {
                        Field mResourcesImpl = Resources.class
                                .getDeclaredField("mResourcesImpl");
                        mResourcesImpl.setAccessible(true);
                        Object resourceImpl = mResourcesImpl.get(resources);
                        Field implAssets = resourceImpl.getClass()
                                .getDeclaredField("mAssets");
                        implAssets.setAccessible(true);
                        implAssets.set(resourceImpl, newAssetManager);
                    }
                    resources.updateConfiguration(resources.getConfiguration(),
                            resources.getDisplayMetrics());
                }
            }
        } catch (Throwable e) {
            throw new IllegalStateException(e);
        }
    }

改方法的目的總結為:
1.如果resource.ap_文件有改變,那么新建一個AssetManager對象newAssetManager,然后用newAssetManager對象替換所有當前Resource、Resource.Theme的mAssets成員變量。
2.如果當前的已經有Activity啟動了,還需要替換所有Activity中mAssets成員變量。

server啟動:判斷Server是否已經啟動,如果沒有啟動,則啟動Server

調用realApplication的onCreate方法: 代理realApplication的生命周期。

至此,我們的app就啟動起來了。下一步就要分析,Server啟動之后,到底是如何進行熱部署、溫部署和冷部署了。Server負責的熱部署、溫部署和冷部署 首先重點關注一下Server的內部類SocketServerReplyThread:

 private class SocketServerReplyThread extends Thread {
        private final LocalSocket mSocket;

        SocketServerReplyThread(LocalSocket socket) {
            this.mSocket = socket;
        }

        public void run() {
            try {
                DataInputStream input = new DataInputStream(
                        this.mSocket.getInputStream());
                DataOutputStream output = new DataOutputStream(
                        this.mSocket.getOutputStream());
                try {
                    handle(input, output);
                } finally {
                    try {
                        input.close();
                    } catch (IOException ignore) {
                    }
                    try {
                        output.close();
                    } catch (IOException ignore) {
                    }
                }
                return;
            } catch (IOException e) {
                if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun", "Fatal error receiving messages", e);
                }
            }
        }

        private void handle(DataInputStream input, DataOutputStream output)
                throws IOException {
            long magic = input.readLong();
            if (magic != 890269988L) {
                Log.w("InstantRun",
                        "Unrecognized header format " + Long.toHexString(magic));

                return;
            }
            int version = input.readInt();

            output.writeInt(4);
            if (version != 4) {
                Log.w("InstantRun",
                        "Mismatched protocol versions; app is using version 4 and tool is using version "
                                + version);
            } else {
                int message;
                for (;;) {
                    message = input.readInt();
                    switch (message) {
                    case 7:
                        if (Log.isLoggable("InstantRun", 2)) {
                            Log.v("InstantRun", "Received EOF from the IDE");
                        }
                        return;
                    case 2:
                        boolean active = Restarter
                                .getForegroundActivity(Server.this.mApplication) != null;
                        output.writeBoolean(active);
                        if (Log.isLoggable("InstantRun", 2)) {
                            Log.v("InstantRun",
                                    "Received Ping message from the IDE; returned active = "
                                            + active);
                        }
                        break;
                    case 3:
                        String path = input.readUTF();
                        long size = FileManager.getFileSize(path);
                        output.writeLong(size);
                        if (Log.isLoggable("InstantRun", 2)) {
                            Log.v("InstantRun", "Received path-exists(" + path
                                    + ") from the " + "IDE; returned size="
                                    + size);
                        }
                        break;
                    case 4:
                        long begin = System.currentTimeMillis();
                        path = input.readUTF();
                        byte[] checksum = FileManager.getCheckSum(path);
                        if (checksum != null) {
                            output.writeInt(checksum.length);
                            output.write(checksum);
                            if (Log.isLoggable("InstantRun", 2)) {
                                long end = System.currentTimeMillis();
                                String hash = new BigInteger(1, checksum)
                                        .toString(16);
                                Log.v("InstantRun", "Received checksum(" + path
                                        + ") from the " + "IDE: took "
                                        + (end - begin) + "ms to compute "
                                        + hash);
                            }
                        } else {
                            output.writeInt(0);
                            if (Log.isLoggable("InstantRun", 2)) {
                                Log.v("InstantRun", "Received checksum(" + path
                                        + ") from the "
                                        + "IDE: returning <null>");
                            }
                        }
                        break;
                    case 5:
                        if (!authenticate(input)) {
                            return;
                        }
                        Activity activity = Restarter
                                .getForegroundActivity(Server.this.mApplication);
                        if (activity != null) {
                            if (Log.isLoggable("InstantRun", 2)) {
                                Log.v("InstantRun",
                                        "Restarting activity per user request");
                            }
                            Restarter.restartActivityOnUiThread(activity);
                        }
                        break;
                    case 1:
                        if (!authenticate(input)) {
                            return;
                        }
                        List<ApplicationPatch> changes = ApplicationPatch
                                .read(input);
                        if (changes != null) {
                            boolean hasResources = Server.hasResources(changes);
                            int updateMode = input.readInt();
                            updateMode = Server.this.handlePatches(changes,
                                    hasResources, updateMode);

                            boolean showToast = input.readBoolean();

                            output.writeBoolean(true);

                            Server.this.restart(updateMode, hasResources,
                                    showToast);
                        }
                        break;
                    case 6:
                        String text = input.readUTF();
                        Activity foreground = Restarter
                                .getForegroundActivity(Server.this.mApplication);
                        if (foreground != null) {
                            Restarter.showToast(foreground, text);
                        } else if (Log.isLoggable("InstantRun", 2)) {
                            Log.v("InstantRun",
                                    "Couldn't show toast (no activity) : "
                                            + text);
                        }
                        break;
                    }
                }

            }
        }

socket開啟后,開始讀取數據,當讀到1時,獲取代碼變化的ApplicationPatch列表,然后調用handlePatches來處理代碼的變化。

private int handlePatches(List<ApplicationPatch> changes,
            boolean hasResources, int updateMode) {
        if (hasResources) {
            FileManager.startUpdate();
        }
        for (ApplicationPatch change : changes) {
            String path = change.getPath();
            if (path.endsWith(".dex")) {
                handleColdSwapPatch(change);

                boolean canHotSwap = false;
                for (ApplicationPatch c : changes) {
                    if (c.getPath().equals("classes.dex.3")) {
                        canHotSwap = true;
                        break;
                    }
                }
                if (!canHotSwap) {
                    updateMode = 3;
                }
            } else if (path.equals("classes.dex.3")) {
                updateMode = handleHotSwapPatch(updateMode, change);
            } else if (isResourcePath(path)) {
                updateMode = handleResourcePatch(updateMode, change, path);
            }
        }
        if (hasResources) {
            FileManager.finishUpdate(true);
        }
        return updateMode;
    }

1.如果后綴為“.dex”,冷部署處理handleColdSwapPatch
2.如果后綴為“classes.dex.3”,熱部署處理handleHotSwapPatch
3.其他情況,溫部署,處理資源handleResourcePatch

handleColdSwapPatch冷部署:

 private static void handleColdSwapPatch(ApplicationPatch patch) {
        if (patch.path.startsWith("slice-")) {
            File file = FileManager.writeDexShard(patch.getBytes(), patch.path);
            if (Log.isLoggable("InstantRun", 2)) {
                Log.v("InstantRun", "Received dex shard " + file);
            }
        }
    }

把dex文件寫到私有目錄,等待整個app重啟,重啟之后,使用前面提到的IncrementalClassLoader加載dex即可。

handleHotSwapPatch熱部署:

private int handleHotSwapPatch(int updateMode, ApplicationPatch patch) {
        if (Log.isLoggable("InstantRun", 2)) {
            Log.v("InstantRun", "Received incremental code patch");
        }
        try {
            String dexFile = FileManager.writeTempDexFile(patch.getBytes());
            if (dexFile == null) {
                Log.e("InstantRun", "No file to write the code to");
                return updateMode;
            }
            if (Log.isLoggable("InstantRun", 2)) {
                Log.v("InstantRun", "Reading live code from " + dexFile);
            }
            String nativeLibraryPath = FileManager.getNativeLibraryFolder()
                    .getPath();
            DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
                    this.mApplication.getCacheDir().getPath(),
                    nativeLibraryPath, getClass().getClassLoader());

            Class<?> aClass = Class.forName(
                    "com.android.tools.fd.runtime.AppPatchesLoaderImpl", true,
                    dexClassLoader);
            try {
                if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun", "Got the patcher class " + aClass);
                }
                PatchesLoader loader = (PatchesLoader) aClass.newInstance();
                if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun", "Got the patcher instance " + loader);
                }
                String[] getPatchedClasses = (String[]) aClass
                        .getDeclaredMethod("getPatchedClasses", new Class[0])
                        .invoke(loader, new Object[0]);
                if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun", "Got the list of classes ");
                    for (String getPatchedClass : getPatchedClasses) {
                        Log.v("InstantRun", "class " + getPatchedClass);
                    }
                }
                if (!loader.load()) {
                    updateMode = 3;
                }
            } catch (Exception e) {
                Log.e("InstantRun", "Couldn't apply code changes", e);
                e.printStackTrace();
                updateMode = 3;
            }
        } catch (Throwable e) {
            Log.e("InstantRun", "Couldn't apply code changes", e);
            updateMode = 3;
        }
        return updateMode;
    }

將patch的dex文件寫入到臨時目錄,然后使用DexClassLoader去加載dex。然后反射調用AppPatchesLoaderImpl類的load方法,需要說明的是,AppPatchesLoaderImpl繼承自抽象類AbstractPatchesLoaderImpl,并實現了抽象方法:getPatchedClasses
如下是AbstractPatchesLoaderImpl抽象類的源碼,注意看load方法:

 public abstract class AbstractPatchesLoaderImpl implements PatchesLoader {
    public abstract String[] getPatchedClasses();

    public boolean load() {
        try {
            for (String className : getPatchedClasses()) {
                ClassLoader cl = getClass().getClassLoader();
                Class<?> aClass = cl.loadClass(className + "$override");
                Object o = aClass.newInstance();
                Class<?> originalClass = cl.loadClass(className);
                Field changeField = originalClass.getDeclaredField("$change");

                changeField.setAccessible(true);

                Object previous = changeField.get(null);
                if (previous != null) {
                    Field isObsolete = previous.getClass().getDeclaredField(
                            "$obsolete");
                    if (isObsolete != null) {
                        isObsolete.set(null, Boolean.valueOf(true));
                    }
                }
                changeField.set(null, o);
                if ((Log.logging != null)
                        && (Log.logging.isLoggable(Level.FINE))) {
                    Log.logging.log(Level.FINE, String.format("patched %s",
                            new Object[] { className }));
                }
            }
        } catch (Exception e) {
            if (Log.logging != null) {
                Log.logging.log(Level.SEVERE, String.format(
                        "Exception while patching %s",
                        new Object[] { "foo.bar" }), e);
            }
            return false;
        }
        return true;
    }
    }

由此,我們大概理清楚了InstantRun熱部署的原理: 在第一次構建apk時,在每一個類中注入了一個$change的成員變量,它實現了IncrementalChange接口,并在每一個方法中,插入了一段類似的邏輯。

IncrementalChange localIncrementalChange = $change;
        if (localIncrementalChange != null) {
            localIncrementalChange.access$dispatch(
                    "onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
                            ... });
            return;
    }

就是當$change不為空的時候,執行IncrementalChange中的方法。

handleResourcePatch方法:

  private static int handleResourcePatch(int updateMode,
            ApplicationPatch patch, String path) {
        if (Log.isLoggable("InstantRun", 2)) {
            Log.v("InstantRun", "Received resource changes (" + path + ")");
        }
        FileManager.writeAaptResources(path, patch.getBytes());

        updateMode = Math.max(updateMode, 2);
        return updateMode;
    }

將資源的patch寫入到私有目錄,等到restart之后生效. restart 根據不同的InstantRun的updateMode模式,進行重啟,使上述的3種部署模式生效!

private void restart(int updateMode, boolean incrementalResources,
            boolean toast) {
        if (Log.isLoggable("InstantRun", 2)) {
            Log.v("InstantRun", "Finished loading changes; update mode ="
                    + updateMode);
        }
        if ((updateMode == 0) || (updateMode == 1)) {
            if (Log.isLoggable("InstantRun", 2)) {
                Log.v("InstantRun", "Applying incremental code without restart");
            }
            if (toast) {
                Activity foreground = Restarter
                        .getForegroundActivity(this.mApplication);
                if (foreground != null) {
                    Restarter.showToast(foreground,
                            "Applied code changes without activity restart");
                } else if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun",
                            "Couldn't show toast: no activity found");
                }
            }
            return;
        }
        List<Activity> activities = Restarter.getActivities(this.mApplication,
                false);
        if ((incrementalResources) && (updateMode == 2)) {
            File file = FileManager.getExternalResourceFile();
            if (Log.isLoggable("InstantRun", 2)) {
                Log.v("InstantRun", "About to update resource file=" + file
                        + ", activities=" + activities);
            }
            if (file != null) {
                String resources = file.getPath();
                MonkeyPatcher.monkeyPatchApplication(this.mApplication, null,
                        null, resources);
                MonkeyPatcher.monkeyPatchExistingResources(this.mApplication,
                        resources, activities);
            } else {
                Log.e("InstantRun", "No resource file found to apply");
                updateMode = 3;
            }
        }
        Activity activity = Restarter.getForegroundActivity(this.mApplication);
        if (updateMode == 2) {
            if (activity != null) {
                if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun", "Restarting activity only!");
                }
                boolean handledRestart = false;
                try {
                    Method method = activity.getClass().getMethod(
                            "onHandleCodeChange", new Class[] { Long.TYPE });
                    Object result = method.invoke(activity,
                            new Object[] { Long.valueOf(0L) });
                    if (Log.isLoggable("InstantRun", 2)) {
                        Log.v("InstantRun", "Activity " + activity
                                + " provided manual restart method; return "
                                + result);
                    }
                    if (Boolean.TRUE.equals(result)) {
                        handledRestart = true;
                        if (toast) {
                            Restarter.showToast(activity, "Applied changes");
                        }
                    }
                } catch (Throwable ignore) {
                }
                if (!handledRestart) {
                    if (toast) {
                        Restarter.showToast(activity,
                                "Applied changes, restarted activity");
                    }
                    Restarter.restartActivityOnUiThread(activity);
                }
                return;
            }
            if (Log.isLoggable("InstantRun", 2)) {
                Log.v("InstantRun",
                        "No activity found, falling through to do a full app restart");
            }
            updateMode = 3;
        }
        if (updateMode != 3) {
            if (Log.isLoggable("InstantRun", 6)) {
                Log.e("InstantRun", "Unexpected update mode: " + updateMode);
            }
            return;
        }
        if (Log.isLoggable("InstantRun", 2)) {
            Log.v("InstantRun",
                    "Waiting for app to be killed and restarted by the IDE...");
        }
    }

總體總結 總結起來,做了一下幾件事:
第一次編譯apk:
1.把Instant-Run.jar和instant-Run-bootstrap.jar打包到主dex中
2.替換AndroidManifest.xml中的application配置
3.使用asm工具,在每個類中添加$change,在每個方法前加邏輯
4.把源代碼編譯成dex,然后存放到壓縮包instant-run.zip中

app運行期:
1.獲取更改后資源resource.ap_的路徑
2.設置ClassLoader。setupClassLoader:
使用IncrementalClassLoader加載apk的代碼,將原有的BootClassLoader → PathClassLoader改為BootClassLoader → IncrementalClassLoader → PathClassLoader繼承關系。
3.createRealApplication:創建apk真實的application
4.monkeyPatchApplication
反射替換ActivityThread中的各種Application成員變量
5.monkeyPatchExistingResource
反射替換所有存在的AssetManager對象
6.調用realApplication的onCreate方法
7.啟動Server,Socket接收patch列表

有代碼修改時:
1.生成對應的$override類
2.生成AppPatchesLoaderImpl類,記錄修改的類列表
3.打包成patch,通過socket傳遞給app
4.app的server接收到patch之后,分別按照handleColdSwapPatch、handleHotSwapPatch、handleResourcePatch等待對patch進行處理
5.restart使patch生效。

參考文章:
https://github.com/nuptboyzhb/AndroidInstantRun

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容