修复技术干货 | 深度理解Android InstantRun原理(一)

   2016-09-08 0
核心提示:简单介绍一下Instant Run,它是Android Studio2.0以后新增的一个运行机制,能够显著减少你第二次及以后的构建和部署时间。简单通俗的解释就是,当你在Android Studio中改了你的代码,Instant Run可以很快的让你看到你修改的效果。而在没有Instant Run之前,你

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

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

修复技术干货 | 深度理解Android InstantRun原理(一)

构建整个apk → 部署app → app重启 → 重启Activity

而Instant Run则需要更少的时间。

Instant Run编译和部署流程

修复技术干货 | 深度理解Android InstantRun原理(一)

只构建修改的部分 → 部署修改的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文件中设置:

修复技术干货 | 深度理解Android InstantRun原理(一)

首先,我们先反编译一下APK的构成:

使用的工具:d2j-dex2jar 和jd-gui

修复技术干货 | 深度理解Android InstantRun原理(一)

里面有2个dex文件和一个instant-run.zip文件。首先分别看一下两个dex文件的源码:

classes.dex的反编译之后的源码:

修复技术干货 | 深度理解Android InstantRun原理(一)

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

classes2.dex反编译之后的源码:

修复技术干货 | 深度理解Android InstantRun原理(一)

我们赫然发现,两个dex中竟然没有一句我们自己写的代码??那么代码在哪里呢?你可能猜到,app真正的业务dex在instant-run.zip中。解压instant-run.zip之后,如下图所示:

修复技术干货 | 深度理解Android InstantRun原理(一)

反编译之后,我们会发现,我们真正的业务代码都在这里。

另外,我们再decode看一下AndroidManifest文件

修复技术干货 | 深度理解Android InstantRun原理(一)

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

我们依次需要关注的方法有:

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);
            }
        }
    }

该方法主要是判断资源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<Mon
keyPatcher> patcher = Mon
keyPatcher.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中查找。

调用之后的效果如下图所示:

修复技术干货 | 深度理解Android InstantRun原理(一)

我们可以在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.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.BootClassLoader

由此,我们已经知道了,当前PathClassLoader委托IncrementalClassLoader加载dex。【未完待续】

 
标签: 安卓开发
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • 安卓中通知功能的具体实现
    安卓中通知功能的具体实现
    通知[Notification]是Android中比较有特色的功能,当某个应用程序希望给用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知实现。使用通知的步骤1、需要一个NotificationManager来获得NotificationManager manager = (NotificationManager
    02-05 安卓开发
  • Android view系统分析-setContentView
    Android view系统分析-setContentView
    第一天上班,列了一下今年要学习的东西。主要就是深入学习Android相关的系统源代码,夯实基础。对于学习Android系统源代码,也没什么大概,就从我们平常使用最基础的东西学起,也就是从view这个切入点开始学习Android的源码,在没分析源码之前,我们有的时候
    02-05 安卓开发
  • 如何进行网络视频截图/获取视频的缩略图
    如何进行网络视频截图/获取视频的缩略图
    小编导读:获取视频的缩略图,截图正在播放的视频某一帧,是在音视频开发中,常遇到的问题。本文是主要用于点播中截图视频,同时还可以获取点播视频的缩略图进行显示,留下一个问题,如下图所示, 如果要获取直播中节目视频缩略图,该怎么做呢?(ps:直播是直
  • Android NDK 层发起 HTTP 请求的问题及解决
    Android NDK 层发起 HTTP 请求的问题及解决
    前言新的一年,大家新年快乐~~鸡年大吉!本次给大家带来何老师的最新文章~虽然何老师还在过节,但依然放心不下广大开发者,在此佳节还未结束之际,给大家带来最新的技术分享~ 事件的起因不说了,总之是需要实现一个 NDK 层的网络请求。为了多端适用,还是选择
  • Android插件化(六): OpenAtlasの改写aapt以防止资源ID冲突
    Android插件化(六): OpenAtlasの改写aapt以防
    引言Android应用程序的编译中,负责资源打包的是aapt,如果不对打包后的资源ID进行控制,就会导致插件中的资源ID冲突。所以,我们需要改写aapt的源码,以达到通过某种方式传递资源ID的Package ID,通过aapt打包时获取到这个Package ID并且应用才插件资源的命名
    02-05 安卓开发
  • Android架构(一)MVP架构在Android中的实践
    Android架构(一)MVP架构在Android中的实践
    为什么要重视程序的架构设计 对程序进行架构设计的原因,归根结底是为了 提高生产力 。通过设计是程序模块化,做到模块内部的 高聚合 和模块之间的 低耦合 (如依赖注入就是低耦合的集中体现)。 这样做的好处是使得程序开发过程中,开发人员主需要专注于一点,
    02-05 安卓开发
  • 安卓逆向系列教程 4.2 分析锁机软件
    安卓逆向系列教程 4.2 分析锁机软件
    安卓逆向系列教程 4.2 分析锁机软件 作者: 飞龙 这个教程中我们要分析一个锁机软件。像这种软件都比较简单,完全可以顺着入口看下去,但我这里还是用关键点来定位。首先这个软件的截图是这样,进入这个界面之后,除非退出模拟器,否则没办法回到桌面。上面那
    02-05 安卓开发
  • Android插件化(二):OpenAtlas插件安装过程分析
    Android插件化(二):OpenAtlas插件安装过程分析
    在前一篇博客 Android插件化(一):OpenAtlas架构以及实现原理概要 中,我们对应Android插件化存在的问题,实现原理,以及目前的实现方案进行了简单的叙述。从这篇开始,我们要深入到OpenAtlas的源码中进行插件安装过程的分析。 插件的安装分为3种:宿主启动时立
    02-05 安卓开发
  • [译] Android API 指南
    [译] Android API 指南
    众所周知,Android开发者有中文网站了,API 指南一眼看去最左侧的菜单都是中文,然而点进去内容还是很多是英文,并没有全部翻译,我这里整理了API 指南的目录,便于查看,如果之前还没有通读,现在可以好好看一遍。注意,如果标题带有英文,说明官方还没有翻
  • 使用FileProvider解决file:// URI引起的FileUriExposedException
    使用FileProvider解决file:// URI引起的FileUri
    问题以下是一段简单的代码,它调用系统的相机app来拍摄照片:void takePhoto(String cameraPhotoPath) {File cameraPhoto = new File(cameraPhotoPath);Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);takePhotoIntent.putExtra(Medi
    02-05 安卓开发
点击排行