duozuoshi

深度理解Android InstantRun原理以及源码分析

0
阅读(1593)

Instant Run官方介绍


简单介绍一下Instant Run,它是Android Studio2.0以后新增的一个运行机制,能够显著减少你第二次及以后的构建和部署时间。简单通俗的解释就是,当你在Android Studio中改了你的代码,Instant Run可以很快的让你看到你修改的效果。而在没有Instant Run之前,你的一个小小的修改,都肯能需要几十秒甚至更长的等待才能看到修改后的效果。

统的代码修改及编译部署流程

1

构建整个apk → 部署app → app重启 → 重启Activity
而Instant Run则需要更少的时间。

Instant Run编译和部署流程

2
只构建修改的部分 → 部署修改的dex或资源 → 热部署,温部署,冷部署

热部署

Incremental code changes are applied and reflected in the app without needing to relaunch the app or even restart the current Activity. Can be used for most simple changes within method implementations. 
方法内的简单修改,无需重启app和Activity

温部署

The Activity needs to be restarted before changes can be seen and used. Typically required for changes to resources. 
app无需重启,但是activity需要重启,比如资源的修改。

冷部署

The app is restarted (but still not reinstalled). Required for any structural changes such as to inheritance or method signatures. 
app需要重启,比如继承关系的改变或方法的签名变化等。

上述说这么多概念,估计大家对Instant Run应该有了大体的认知了。那么它的实现原理是什么呢?其实,在没有看案例之前,我基本上可以猜测到Instant Run的思路,基于目前比较火的插件化框架,是比较容易理解Instant Run的。但Instant Run毕竟是Google官方的工具,具有很好的借鉴意义。

Demo案例

新建一个简单的android studio项目,新建自己的MyApplication,在AndroidManifest文件中设置:

4
首先,我们先反编译一下APK的构成: 
使用的工具:d2j-dex2jar 和jd-gui
3
里面有2个dex文件和一个instant-run.zip文件。首先分别看一下两个dex文件的源码:
classes.dex的反编译之后的源码:
5
里面只有一个AppInfo,保存了app的基本信息,主要包含了包名和applicationClass。
classes2.dex反编译之后的源码:
6
我们赫然发现,两个dex中竟然没有一句我们自己写的代码??那么代码在哪里呢?你可能猜到,app真正的业务dex在instant-run.zip中。解压instant-run.zip之后,如下图所示:
7
反编译之后,我们会发现,我们真正的业务代码都在这里。
另外,我们再decode看一下AndroidManifest文件
8
//TODO 
我们发现,我们的application也被替换了,替换成了com.android.tools.fd.runtime.BootstrapApplication

看到这里,那么大体的思路,可以猜到: 
1.Instant-Run代码作为一个宿主程序,将app作为资源dex加载起来,和插件化一个思路 
2.那么InstantRun是怎么把业务代码运行起来的呢?

InstantRun启动app

首先BootstrapApplication分析,按照执行顺序,依次分析attachBaseContext和onCreate方法。

1.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);
            }
        }
    }
    ...1234567891011121314151617181920212223242526272829

我们依次需要关注的方法有: 
createResources → setupClassLoaders → createRealApplication → 调用realApplication的attachBaseContext方法

1.1.createResources

首先看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);
            }
        }
    }123456789101112131415161718192021222324252627282930313233

该方法主要是判断资源resource.ap_是否改变,然后保存resource.ap_的路径到externalResourcePath中

1.2.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);
        }
    }12345678910111213141516171819202122232425262728293031323334

继续看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;
    }
    }123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687

inject方法是用来设置classloader的父子顺序的,使用IncrementalClassLoader来加载dex。由于ClassLoader的双亲委托模式,也就是委托父类加载类,父类中找不到再在本ClassLoader中查找。 
调用之后的效果如下图所示:
classloader
我们可以在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();
        }
    }12345678910111213141516

运行结果:

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

由此,我们已经知道了,当前PathClassLoader委托IncrementalClassLoader加载dex。继续回到BootstrapApplication的attachBaseContext方法继续分析。

1.3.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();
        }
    }123456789101112131415161718192021222324252627282930313233

该方法就是用classes.dex中的AppInfo类的applicationClass常量中保存的app真实的application。由上面的反编译截图可以知道,demo中的applicationClass就是mobctrl.net.testinstantrun.MyApplication。通过反射的方式,创建真是的realApplication。

1.4.调用realApplication的attachBaseContext方法

代理realApplication的生命周期,通过反射调用realApplication的attachBaseContext方法,以当前的Context为参数。
attachBaseContext方法执行结束之后,我们继续往下看,到BootstrapApplication的onCreate方法

2.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) {                    &nbsp