浅谈Instan Run中的热替换

(本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发)

前言:

自从Android Studio 2.0发布以来,相信广大的攻城狮朋友们都已经用上了Instant Run这个新特性,还没用上的朋友们,赶紧去Google官网了解一下吧 https://developer.android.com/studio/run/index.html#instant-run

Instant Run主要分为三种方式来加载app:

Hot Swap:
这是最令人激动的方式,它可以在不重启Activity的情况下实现代码的替换,简直是逆天啊!但是热替换的条件很苛刻,只能是在简单的修改了代码的情况下,AS才会采用这种方式。

Warm Swap:
暖替换,是对热替换的让步,它会重启你所修改的Activity,但是不会重启App。如果在项目中修改了资源,AS会自动选择这种方式。

Cold Swap:
如果你改变了代码的结构,如继承和改变了方法名,那么AS也只能无奈的选择冷替换了,它会重启整个App。

探索:

接下来让我们来探索一下神奇的Hot Swap。

这是一个很简单的Activity,就用它来窥探Instant Run吧。

public class MainActivity extends AppCompatActivity {
    private Button mBtnTest;
    private int mNum;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBtnTest = (Button) findViewById(R.id.btn_test);
        setListener();
    }

    private void setListener() {
        mBtnTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mNum++;
                Log.e("InstantRun", "Num: " + mNum);
        });
    }
}

点一下按钮,打出如下log:

08-11 16:51:16.730 4125-4125/com.wangxiandeng.instantruntest E/InstantRun: Num: 1

再点一下:

08-11 16:54:36.300 4125-4125/com.wangxiandeng.instantruntest E/InstantRun: Num: 2

你们看到现在,是不是心想,你特么在逗我么?别急,接着往下走。

把代码修改为:

Log.e("InstantRun", "Num: " + mNum*2);

点击Instan Run 闪电按钮️,Activty没有重启,这时候再点击按钮

08-11 17:02:15.340 14022-14022/com.wangxiandeng.instantruntest E/InstantRun: Num: 6

可见,mNum的值在Hot Swap时并没有重置,而是保持了之前的值:2,也就是说,Activity的所有生命周期方法并没有重走一遍,但是现在log打印出来为6,所以代码确实被替换了,那Hot Swap究竟是怎么做到这一点的呢,让我们来揭开它的神秘面纱。

原理:

Instant Run其实类似于这两年很火的Hotfix,根据Instant Run的思想,甚至可以自己去鼓捣出一个Hotfix库。

Hot Swap 看起来很高大上,其实玩的就是狸猫换太子的把戏。在app的第一次编译阶段,它利用transform 在我们的每一个类里注入了一个变量:$change,这是一个IncrementalChange类型的变量。各位看官想必又要骂我了:你说注入就注入了啊?

那我们回到刚才那个Activity,证明它被注入了$change字段。

现在修改onClick中的代码如下:

            Class clazz = MainActivity.class;
            try {
                Field changeField = clazz.getDeclaredField("$change");
                changeField.setAccessible(true);
                Object changeValue = changeField.get(this);
                Class changeClass = changeValue.getClass();
            } catch (Exception e) {
                e.printStackTrace();
            } 

再次点击Activity中按钮,log打印为:

08-11 17:25:15.830 3311-3311/com.wangxiandeng.instantruntest E/InstantRun: Class: class com.wangxiandeng.instantruntest.MainActivity$override 

事实证明,Activity中确实有$change这个变量,细心的读者还会发现,这个$change 变量的运行类型为
com.wangxiandeng.instantruntest.MainActivity$override

这里的MainActivity$override其实就是狸猫,也就是我们经常说的补丁,它实现了IncrementalChange接口,并且重写了MainActivity中的所有方法。我们在onClick中再加一句代码

printMethods(changeClass);

printMethods会打印出MainActivity$override中的所有方法。

public static void printMethods(Class cl) {
    Method[] methods = cl.getDeclaredMethods();
    for (Method m : methods) {
        Class retType = m.getReturnType();
        String name = m.getName();
        System.out.print("  ");
        String modifiers = Modifier.toString(m.getModifiers());
        if (modifiers.length() > 0) {
            System.out.print(modifiers + " ");
        }
        System.out.print(retType.getName() + " " + name + "(");
        Class[] paramTypes = m.getParameterTypes();
        for (int j = 0; j < paramTypes.length; j++) {
            System.out.print(paramTypes[j].getName());
            if (j < paramTypes.length - 1) {
                System.out.print(", ");
            }
        }
        System.out.println(");");
    }
}

点击Activity中按钮,打印出方法如下:

public transient java.lang.Object access$dispatch(java.lang.String, [Ljava.lang.Object;);
    
public static java.lang.Object init$args([Lcom.wangxiandeng.instantruntest.MainActivity;, [Ljava.lang.Object;);

public static void init$body(com.wangxiandeng.instantruntest.MainActivity, [Ljava.lang.Object;);

public static void onCreate(com.wangxiandeng.instantruntest.MainActivity, android.os.Bundle);

public static void printMethods(java.lang.Class);

public static void setListener(com.wangxiandeng.instantruntest.MainActivity);

从Log中可以看出,MainActivity$override中包含了MainActivity中所有的方法,包括onCreate(), printMethods(), setListener()。

看到这里,聪明的读者应该已经猜测出Instan Run的原理了,其实也就是和代理差不多,MainActivity在执行方法时,会先判断它的代理($change)是否为空,如果不为空,就执行代理里的方法。这样当我们修改了某个类方法里的代码,AS会自动的创建一个该类的代理(xx$override),并将代理赋值给该类的$chang字段,这样我们的修改在不重启Activity的情况下也能生效了。

代理类是通过access$dispatch()方法来进行函数分发的,传入的参数为所要执行方法的签名和参数,access$dispatch()会根据方法签名的hashcode寻找到目标方法,并传入参数执行。接下来我们再来试验一下。

在MainActivity中再添加一个方法:

private void sayHello(String text) {
    Log.e("InstantRun", text);
}

接着在onClick try块中再添加两行代码,通过反射MainActivity$override 中的access$dispatch()方法,实现调用补丁中的sayHello()。

Method dispatchMethod = changeClass.getDeclaredMethod("access$dispatch", new Class[]{String.class, Object[].class});
dispatchMethod.invoke(changeValue, "sayHello.(Ljava/lang/String;)V", new Object[]{MainActivity.this, "Hello World!"});

在第二行代码中,我们将sayHello()的方法签名以及一个“Hello World!”字符串传入给access$dispatch方法,接下来看看能不能成功的调用sayHello()。

08-11 17:25:15.840 3311-3311/com.wangxiandeng.instantruntest E/InstantRun: Hello World!

Log中成功的打印出了Hello World!

到这里,大家应该对Instan Run Hot Swap的来龙去脉有所了解了,那么补丁文件又是怎么加载进来的呢?
当我们修改代码,并点击运行按钮时,AS会创建一个AppPatchesLoaderImpl,该类中记录了哪些类被修改了,然后通过scoket,将补丁文件和AppPatchesLoaderImpl发送到设备,调用设备的
handleHotSwapPatch()方法。

private int handleHotSwapPatch(int updateMode, @NonNull ApplicationPatch patch) {
   try {
       String dexFile = FileManager.writeTempDexFile(patch.getBytes());
       String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
       DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
               mApplication.getCacheDir().getPath(), nativeLibraryPath,
               getClass().getClassLoader());
       // we should transform this process with an interface/impl
       Class<?> aClass = Class.forName(
               "com.android.tools.fd.runtime.AppPatchesLoaderImpl", true, dexClassLoader);
       try {
           PatchesLoader loader = (PatchesLoader) aClass.newInstance();
           String[] getPatchedClasses = (String[]) aClass
                   .getDeclaredMethod("getPatchedClasses").invoke(loader);
           if (!loader.load()) {
               updateMode = UPDATE_MODE_COLD_SWAP;
           }
       } catch (Exception e) {
           updateMode = UPDATE_MODE_COLD_SWAP;
       }
   } catch (Throwable e) {
       updateMode = UPDATE_MODE_COLD_SWAP;
   }
   return updateMode;
 }

该方法首先新建了一个ClassLoader,将补丁记录类AppPatchesLoaderImpl加载进来,然后调用AppPatchesLoaderImpl的load方法,load()方法中会遍历并记载所有的补丁类,并反射原有类的$change变量,赋值以补丁类。

想深入了解补丁加载的同学,可以看一看w4lle's Notes的文章《从Instant run谈Android替换Application和动态加载机制》

总结:

至此为止,Instan Run中的Hot Swap基本流程已经讲完了,总的来说就是代理,有点类似支付宝的Andfix,不过Andfix是从jni层去修改方法指针,本质其实都是替换掉目标方法,运行补丁方法。

(转载请注明ID:半栈工程师,欢迎访问个人博客https://halfstackdeveloper.github.io/)

欢迎关注我的知乎专栏:https://zhuanlan.zhihu.com/halfstack

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 如何编译运行app 我们要编译运行一个AS工程,只需在AndroidStudio上点击几下按钮就行了。Instan...
    EsonJack阅读 2,006评论 0 5
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,567评论 25 707
  • 什么是Instant Run? 我们都知道,Android Studio功能非常强大,在各个功能性方面都要优于Ec...
    GB_speak阅读 734评论 0 3
  • 我的生活,穿梭于两座城,周末一座城,工作日出差一座城,时间在指尖划过,却没有带来任何起色,曾想何时可以结束穿梭,...
    兔十六阅读 209评论 0 0
  • 学习讲《迈向富足》 学习《美乐家三大基本要素之公司、产品》 感受:每天都要学习,在学习中能找到所有问题的答案,而且...
    周曼Melaleuca阅读 140评论 0 0