Shadow解决Activity等组件生命周期的方法解析

明确问题

每个Android插件框架要解决的首要问题都是Activity的生命周期问题。Activity代表了Service等其他需要注册的组件。不同是插件框架解决这个问题的前提也不完全一样。而我们的业务要求比较苛刻,再加上Android 9.0的非公开API限制,所以由这些前提要求:

  1. 插件代码也要能正常编译安装运行。
  2. 插件代码都是现有业务代码,不能因为接入插件框架而需要修改代码(即需要插件框架无代码侵入性)。
  3. 在宿主的AndroidManifest.xml中只能注册有限数量(大约10个)的组件。宿主AndroidManifest过大会使宿主安装变慢,跨进程通信出错。
  4. 不能使用非公开API。

大方向的选择

其实我们早就在用一款也是基于代理组件转调插件组件的插件框架了。只不过这款插件框架用到了大量反射使用私有API,眼看着是不可能再Android 9.0上继续使用了。我们也调研了外界口碑最好的RePlugin。所以大概就这两种方向,一是用代理Activity作为壳子注册在宿主中真正运行起来,然后让它持有插件Activity,想办法在收到系统的生命周期方法调用时转调插件Activity的对应生命周期方法。二是Hack修改宿主PathClassLoader,让它能在收到系统查询AndroidManifest中注册的Activity的类时返回插件的Activity类。

方法二就是RePlugin的关键技术。它利用了JVM的特性。我也不太肯定这算不算是bug,总之ClassLoader的loadClass方法返回的实际类可以和它被要求加载的类名字不一样。举个例子,宿主的AndroidManifest.xml注册一个Activity名叫A,插件里有一个Activity名叫B。宿主代码或者apk中最终是没有A这个类的,只有在AndroidManifest中注册的一个名字而已。当想要加载插件Activity B时,就发出一个启动Activity A的Intent。系统收到这个Intent后会检查宿主安装的AndroidManifest信息,从中确定A是哪个apk安装的,就会找到宿主的PathClassLoader。然后系统就会试图从PathClassLoader中加载A这个类,然后作为Activity类型的对象使用(这很正常)。所以如果我们把宿主的PathClassLoader给Hack了,控制它的加载逻辑,让它收到这个加载调用时实际返回的是插件Activity B的类。由于B也真的是Activity的子类,所以系统拿回去当作Activity类型使用没有任何问题。这里再扩展一下,如果类C继承自类A,在加载C时也会去加载A,如果这时拿B当A返回的话,C收到B之后是会发现B的名字不是A而出错的。关于RePlugin这段关键技术的实现,当时调研时就发现实现的有些麻烦了。RePlugin选择复制一个PathClassLoader,然后替换系统持有的PathClassLoader。所以复制PathClassLoader需要反射使用PathClassLoader的私有API,拿出来它里面的数据,替换系统持有的PathClassLoader也要反射修改私有API。我们当时已经实现了“全动态插件框架”,其中代理壳子Activity的动态化使用的方法也能解决这个问题,我们的选择是在宿主PathClassLoader上给它加一个parent ClassLoader。因为PathClassLoader也是一个有正常“双亲委派”逻辑的ClassLoader,它加载什么类都会先问自己parent ClassLoader先加载。所以我们加上去的这个parent ClassLoader也能完成RePlugin想要做的事。不过我们用它的目的是不希望壳子Activity打包在宿主占用宿主很多方法数,还不能更新。这一点以后可能再单独讲。

RePlugin的这种方案还有一点非常不适合我们的业务,就是宿主AndroidManifest中注册的“坑位”Activity,就是上面举例的Activity A,是不能同时供多个插件Activity使用的。就是我不能在宿主AndroidManifest中注册一个Activity A,然后让它同时支持插件Activity B和C。这是因为ClassLoader在loadClass的时候,收到的参数只有一个A的类名,我们没有办法传递更多信息,让ClassLoader能在这个loadClass的调用中区分出应该返回B还是应该返回C。所以这种方案需要在宿主中注册大量Activity,这对于我们的宿主来说是不可接受的。而方法一是用代理Activity持有插件Activity转调的方案,就可以在启动代理Activity时通过Intent传递很多参数,代理Activity通过Intent中的参数就能决定该构造一个B还是一个C。这就使得这种方案下壳子是可复用的。

还有一点就是我们在旧框架上就已经设计了“全动态插件框架”,所以基于方法一的方向上开发新插件框架,我们可以不修改宿主的任何代码,不跟宿主版本就能更新插件框架。关于这一点,后续文章再解析。

所以我们探索的方向就这样确定在方法一这个方向上了。

旧框架为什么要用反射、要用私有API?

我们的旧框架就是代理Activity转调插件Activity的方案,市面上还有很多插件框架也是这种方案。大家只是在实现转调的手段上不一样。这里就不深入分析它们都是怎么实现的了,总之就是一个目的,让插件Activity能收到生命周期回调。我们的旧框架和其他一些框架有一种这样的方案,就是让壳子Activity直接反射调用插件Activity对应的生命周期方法。这样做要解决一个额外的问题。

Activity被系统构造出实例之后,并不是直接调用onCreate方法的。首先会调用它的attach方法。attach方法实际上就是Activity的初始化方法,系统通过这个方法向Activity注入一些私有变量,比如Window、ActivityThread等等。插件Activity由于是我们壳子Activity自己new出来的,所以系统不会调用插件Activity的attach方法初始化它。Activity如果没有初始化就被调用了onCreate会有什么问题呢?我们前面说了我们的一个前提是插件Activity要求也要能正常编译安装运行,所以插件Activity的onCreate方法里一定写了super.onCreate()调用。我们还要求对插件代码无侵入性,所以也不能在这个调用外面包一层“if (不是插件模式)"。那么在插件环境下,这个super.onCreate()就一定会执行。Activity基类的onCreate方法就会使用那些应该初始化过的私有变量,但是现在它们没有初始化。所以这一类插件框架方案就要解决这个问题。所以要么是反射调用attach方法,传入从壳子Activity拿到的私有变量,比如说反射出壳子Activity的ActivityThread对象,传给插件Activity的attach方法。要么就干脆直接用反射枚举读写壳子Activity和插件Activity的私有变量,把它们写成一样的完成这个初始化。所以这就是为什么旧框架需要使用反射和私有API。

Shadow如何解决的插件Activity生命周期问题

实际上通过前面的分析,我们发现其实根本不需要插件Activity执行super.onCreate()方法。明确了这个方案原本的目的,就是在宿主中注册并启动一个壳子Activity,这个壳子Activity什么都不自己做,想办法让插件Activity的各个生命周期方法实现代码成为壳子Activity的各个生命周期实现方法的代码。因此我们根本不需要插件Activity是一个系统Activity的子类。我们只是因为需要插件Activity还能正常安装运行,才导致它是一个真正的系统Activity子类的。

我们也知道如果不要求对插件代码无侵入性,也不要求插件能独立安装运行,实际上是可以把让插件Activity不用继承系统Activity了,就简单继承一个普通类就行了。这个普通类上定义一些跟系统Activity类一样的生命周期方法,实现成空实现,然后这些生命周期方法可以设置成public的,这样壳子Activity以这个普通类类型持有插件Activity就可以直接调用插件Activity的生命周期方法了。这样实现既不用反射也不用私有API。

而我们实际上是不需要插件的apk能独立安装运行的,我们希望插件能独立安装运行的本质目的是节省人力,不要维护两套代码。所以看起来,这里只要引入AOP手段,通过AOP编程修改插件Activity的父类,把插件Activity的父类从系统Activity改成我们想要的普通类就行了。

我们先不说这个AOP手段怎么实现,因为问题还没彻底搞清。我们是真的想让A持有B,A收到什么调用就转调B的什么调用吗?这是我们的真正目的吗?不是的。单纯的只是让插件Activity中的super.onCreate()调用失效,并不完美。因为这跟插件Activity正常安装运行时还有点不一样。现在插件Activity的onCreate方法的代码就相当于是壳子Activity的onCreate方法的代码的一部分了。比如:

class ShadowActivity {
    public void onCreate(Bundle savedInstanceState) {
    }
}

class PluginActivity extends ShadowActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        System.out.println("Hello World!");
    }
}

class ContainerActivity extends Activity {
    ShadowActivity pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        pluginActivity.onCreate(savedInstanceState);
    }
}

上面的ShadowActivity就是我们前面说的普通类。仔细看一下是不是就相当于ContainerActivity原本就实现成这样:

class ContainerActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        System.out.println("Hello World!");
    }
} 

只要PluginActivity是动态加载的,就相当于ContainerActivity的实现是动态的。但是如果原本PluginActivity的代码是这样的呢?

class PluginActivity extends ShadowActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        savedInstanceState.clear();
        super.onCreate(savedInstanceState);
    }
}

显然这种代码在正常安装运行时和插件环境运行时就不一样了。因为变成了:

class ContainerActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        savedInstanceState.clear();
    }
}

这说明我们要的不是插件Activity的super.onCreate()调用不执行,我们是希望插件Activity的super.onCreate()能够直接指挥壳子Activity什么时候调用super.onCreate()。而且再想想,这是不是继承关系很像?假如PluginActivity是继承自ContainerActivity的,运行时系统调用的是PluginActivity的实例,那么PluginActivity的super.onCreate()就会直接指导ContainerActivity什么时候调用super.onCreate()了。所以我们在这里的真正需求是如何把原本的继承关系用持有关系实现了。所以Shadow是这样实现的:

class ShadowActivity {
    ContainerActivity containerActivity;
    public void onCreate(Bundle savedInstanceState) {
        containerActivity.superOnCreate(savedInstanceState);
    }
}

class PluginActivity extends ShadowActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        savedInstanceState.clear();
        super.onCreate(savedInstanceState);
    }
}

class ContainerActivity extends Activity {
    ShadowActivity pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        pluginActivity.onCreate(savedInstanceState);
    }

    public void superOnCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}

仔细思考下现在的调用结果,是不是PluginActivity在正常安装运行和插件环境下运行时行为就一致了?

好了,现在就可以考虑怎么实施AOP手段将PluginActivity的父类从系统Activity改为ShadowActivity了。Shadow最终使用的是字节码编辑手段。字节码编辑可以通过Android官方的构建过程中的Transform API来实现,所以这个手段也是利用公开API实现的。关于字节码编辑的细节以后的文章再讲。总之,通过字节码编辑,我们可以在不修改源码的情况下,将插件Activity的父类从系统Activity改为ShadowActivity。从而达到同一份源码应用不同编译选项生成不同的apk,一个能正常安装运行,一个能在插件环境运行。

其实字节码编辑并不是达到我们的目的的唯一手段。最初我并没有一上来就用字节码编辑实现,因为字节码编辑在构建工程上要使用更多技巧,这些技巧后面也会分享。想达到这个AOP目的最简单的方法是利用Java语言的基本特性。Java类在编译时是没有链接过程的,在Java类的字节码中只记录了依赖的其他类的名字,在运行的时候才会去问ClassLoader查找这些类的具体实现。由于插件的ClassLoader就是我们自行创建的,所以我们完全可以在实现插件的ClassLoader时将它设计成不遵循“双亲委派”机制的ClassLoader。在插件Activity查找它的父类系统Activity时,我们就给它返回一个假的系统Activity类。把ShadowActivity当做系统Activity返回给它就可以了。这个代码实现就比字节码编辑简单太多了。可惜的是,Android的Java虚拟机并不是一个标准的JVM。对于系统内置的类,比如系统Activity来说,它会直接使用预编译好的Native实现。在有些手机上Debug模式没有这一特性,上述方案就能正常运行。在Release模式下就会使用预编译的Native实现,导致JVM崩溃。我想写Android JVM虚拟机的人就没想到会有人返回一个假的系统类吧,所以它甚至没有抛出异常,出现了野指针。

后续的开发也利用了字节码编辑做了更多事情,那些事情是不能用ClassLoader技巧实现的。关于那些事情也在未来分享吧。

总结

所以,这篇文章讲的东西可以说就是Shadow实现零反射、无非公开API调用的关键了。怎么样?你们觉得是不是真的是一层窗户纸,一捅就破了?解决问题的关键不是最后的代码,而是最初的思想。“任何软件工程遇到的问题都可以通过增加一个中间层来解决”,Shadow在解决这个问题上加入的ShadowActivity就是这个中间层。通过这个中间层让Android系统看不到插件,也让插件看不到Android系统。这样就不会打破Android系统的限制。反观RePlugin的方案,在大方向上就将未安装的Activity交给了系统,因此后面必然不得不继续用Hack手段解决系统看到了未安装的Activity的问题。比如系统为插件Activity初始化的Context是以宿主的apk初始化的,插件框架就不得不再去Hack修复。所以大方向的选择很重要。

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