Android插件化的兼容性(中):Android P的适配
Android系统的每次版本升级,都会对原有代码进行重构,这就为插件化带来了麻烦。
Android P对插件化的影响,主要体现在两方面,一是它重构了H类中Activity相关的逻辑,另一个是它重构了Instrumentation。
3.1 H类的变身
3.1.1 从Message和Handler说起
对于App开发人员而言,Message和Handler是耳熟能详的两个概念。我们简单回顾一下,一条消息是怎么发送和接收的。
首先,在App启动的时候,会创建ActivityThread,这就是主线程,也叫UI线程。App的入口——main函数,就藏在ActivityThread中,
在main函数中,会创建MainLooper。MainLooper是一个死循环,专门负责接收消息,也就是Message类。
Message类的定义如下,除了耳熟能详的what和obj属性外,还有一个不对App开放的变量target,它是一个Handler:
public final class Message implements Parcelable {
public int what;
public Object obj;
Handler target; //以下省略很多代码哦
}
在App进程中,Application和四大组件的每个生命周期函数,都要和AMS进程进行跨进程通信。
1)App进程把数据传给AMS进程,是通过ActivityManagerNative完成的。
2)AMS进程把数据传给App进程,App进程这边接收数据的是ApplicationThread。
ApplicationThread在接收到数据后,会调用sendMessage方法。这就把消息Message对象发送给了MainLooper这个死循环。
在MainLooper死循环中,处理这个Message对象。怎么处理呢,取出它的target字段,这是一个Handler类型的对象,调用这个Handler对象的dispatchMessage方法。
是时候看一下Handler类的结构了,我简化了它的代码,为的是易于理解:
public class Handler {
final Callback mCallback; public interface Callback {
public boolean handleMessage(Message msg);
} public void handleMessage(Message msg) {
} public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
}
Handler类中有一个mCallback变量,这个变量是插件化技术的核心。书接上文,MainLooper调用了Handler的dispatchMessage方法,这个方法的逻辑是,要么执行mCallback的handleMessage方法,要么执行Handler类自己的handleMessage方法。
Handler类自己的handleMessage方法,是一个空方法,所以我们一般写一个Handler的子类,然后实现这个handleMessage方法。
在Android系统底层,这个Handler类的子类,就是H类,我们在ActivityThread.java中可以找到这个类。H类的handleMessage方法中,定义了所有消息的分发,如下所示:
public final class ActivityThread {
private class H extends Handler {
public static final int LAUNCH_ACTIVITY = 100;
public static final int PAUSE_ACTIVITY = 101;
public static final int PAUSE_ACTIVITY_FINISHING= 102;
public static final int STOP_ACTIVITY_SHOW = 103;
public static final int STOP_ACTIVITY_HIDE = 104;
public static final int SHOW_WINDOW = 105;
public static final int HIDE_WINDOW = 106;
public static final int RESUME_ACTIVITY = 107;
public static final int SEND_RESULT = 108;
public static final int DESTROY_ACTIVITY = 109;
public static final int BIND_APPLICATION = 110;
public static final int EXIT_APPLICATION = 111;
public static final int NEW_INTENT = 112;
public static final int RECEIVER = 113; //以下省略很多代码 public void handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY: {
final ActivityClientRecord r = (ActivityClientRecord) msg.obj; r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
} break; //以下省略很多代码
}
}
}
}
在H类的handleMessage方法中,会根据msg参数的what值,来判断到底是哪种消息,以及相应的执行什么逻辑,比如说,启动Activity。
在H类中,定义了几十种消息,比如说LAUNCH_ACTIVITY的值是100,PAUSE_ACTIVITY的值是101。从100到109,都是给Activity的生命周期函数准备的。从110开始,才是给Application、Service、ContentProvider、BroadcastReceiver使用的。
至此,我们简单回顾了Android系统内部Message的发送和接收流程。其中比较重要的是:
1)Handler类中有一个mCallback变量。
2)H类中定义了各种消息。
3.1.2 Android P之前的插件化解决方案
在Android P(API level 28)之前,我们做插件化,都是Hook掉H类的mCallback对象,拦截这个对象的handleMessage方法。在此之前,我们把插件中的Activity替换为StubActtivty,那么现在,我们拦截到handleMessage方法,再把StubActivity换回为插件中的Activity,代码如下所示:
class MockClass2 implements Handler.Callback { Handler mBase; public MockClass2(Handler base) {
mBase = base;
} @Override
public boolean handleMessage(Message msg) { switch (msg.what) {
// ActivityThread里面 "LAUNCH_ACTIVITY" 这个字段的值是100
// 本来使用反射的方式获取最好, 这里为了简便直接使用硬编码
case 100:
handleLaunchActivity(msg);
break;
} mBase.handleMessage(msg);
return true;
} private void handleLaunchActivity(Message msg) {
// 这里简单起见,直接取出TargetActivity; Object obj = msg.obj; // 把替身恢复成真身
Intent raw = (Intent) RefInvoke.getFieldObject(obj, "intent"); Intent target = raw.getParcelableExtra(AMSHookHelper.EXTRA_TARGET_INTENT);
raw.setComponent(target.getComponent());
}
}
3.1.3 Android P对Activity消息机制的改造
Android系统升级到P,它重构了H类,把100到109这10个用于Activity的消息,都合并为159这个消息,消息名为EXECUTE_TRANSACTION。
为什么要这么修改呢?相信大家面试Android工程师岗位的时候,都会被问及Activity的生命周期图。这其实是一个由Create、Pause、Resume、Stop、Destory、Restart组成的状态机。按照设计模式中状态模式的定义,可以把每个状态都定义成一个类,于是便有了如下的类图:
就拿LaunchActivity来说吧,在Android P之前,是在H类的handleMessage方法的switch分支语句中,编写启动一个Activity的逻辑:
public void handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY: {
final ActivityClientRecord r = (ActivityClientRecord) msg.obj; r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
} break; //以下省略很多代码
}
}
在Android P中,启动Activity的这部分逻辑,被转移到了LaunchActivityItem类的execute方法中。
public class LaunchActivityItem extends ClientTransactionItem { @Override
public void execute(ClientTransactionHandler client, IBinder token,
PendingTransactionActions pendingActions) {
ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
mPendingResults, mPendingNewIntents, mIsForward,
mProfilerInfo, client);
client.handleLaunchActivity(r, pendingActions, null /* customIntent */);
}
}
从架构的角度来说,这次重构的效果很好。使用状态模式,使得Android这部分代码,就是OOP的了。我们把写在H类的handleMessage方法中的switch分支语句,拆分到很多类中,这就符合了五大设计原则中的开闭原则,宁肯有100个类,每个类有100行代码,也不要有一个一万行代码的类。好处是,当我想改动Resume这个状态的业务逻辑时,我只要在ResumeActivityItem类中修改并进行测试就可以了,影响的范围很小。
但是这次重构也有缺点,OOP的缺点就是代码会让人看不懂,因为只有在运行时才知道到底实例化的是哪个类,这让原本清晰的Android Activity消息逻辑变得支离破碎。
按照这个趋势,四大组件之一的Service,它在H类中也有很多消息,也是有很多生命周期函数,Android的下个版本,极有可能把Service也重构为状态模式。
3.1.4 Android P针对于H的Hook
Android P把H类中的100-109这10个消息都删除了,取而代之的是159这个消息,名为EXECUTE_TRANSACTION。
这就导致我们之前的插件化解决方案,在Android P上是不生效的,会因为找不到100这个消息,而不能把StubActiivty换回为插件中的Activity。为此,我们需要拦截159这个消息。拦截后,我们又要面对如何判断当前这个消息到底是Launch,还是Pause或者Resume。
关键在于H类的handleMessage方法的Message参数。这个Message的obj字段,在Message是159的时候,返回的是ClientTransacion类型对象,它内部有一个mActivityCallbacks集合:
public class ClientTransaction implements Parcelable, ObjectPoolItem { private List<ClientTransactionItem> mActivityCallbacks; }
这个mActivityCallbacks集合中,存放的是ClientTransactionItem的各种子类对象,比如LaunchActivityItem、DestoryActivityListItem。我们可以判断这个集合中的值,发现有某个元素是LaunchActivityItem类型的,那么就相当于捕获到了启动Activity的那个消息。
定位到LaunchActivityItem类的对象,它内部有一个mIntent字段,里面存放的就是要启动的Activity名称,目前值是StubActivity。在这里把它替换为真正要启动的插件Activity,代码如下所示:
class MockClass2 implements Handler.Callback { Handler mBase; public MockClass2(Handler base) {
mBase = base;
} @Override
public boolean handleMessage(Message msg) { switch (msg.what) {
// ActivityThread里面 "LAUNCH_ACTIVITY" 这个字段的值是100
// 本来使用反射的方式获取最好, 这里为了简便直接使用硬编码
case 100: //for API 28以下
handleLaunchActivity(msg);
break;
case 159: //for API 28
handleActivity(msg);
break;
} mBase.handleMessage(msg);
return true;
} private void handleActivity(Message msg) {
// 这里简单起见,直接取出TargetActivity;
Object obj = msg.obj; List<Object> mActivityCallbacks = (List<Object>) RefInvoke.getFieldObject(obj, "mActivityCallbacks");
if(mActivityCallbacks.size() > 0) {
String className = "android.app.servertransaction.LaunchActivityItem";
if(mActivityCallbacks.get(0).getClass().getCanonicalName().equals(className)) {
Object object = mActivityCallbacks.get(0);
Intent intent = (Intent) RefInvoke.getFieldObject(object, "mIntent");
Intent target = intent.getParcelableExtra(AMSHookHelper.EXTRA_TARGET_INTENT);
intent.setComponent(target.getComponent());
}
}
}
}
3.2 Instrumentation的变身
在Android P之前,Instrumentation的newActivity方法。逻辑如下:
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}
到了Android P,则改写了Instrumentation类的部分逻辑。它会在newActivity方法中,检查Instrumentation的mThread变量,如果为空,就会抛出一个异常:
public class Instrumentation {
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
String pkg = intent != null && intent.getComponent() != null
? intent.getComponent().getPackageName() : null;
return getFactory(pkg).instantiateActivity(cl, className, intent);
} private AppComponentFactory getFactory(String pkg) {
if (pkg == null) {
Log.e(TAG, "No pkg specified, disabling AppComponentFactory");
return AppComponentFactory.DEFAULT;
}
if (mThread == null) {
Log.e(TAG, "Uninitialized ActivityThread, likely app-created Instrumentation,"
+ " disabling AppComponentFactory", new Throwable());
return AppComponentFactory.DEFAULT;
}
LoadedApk apk = mThread.peekPackageInfo(pkg, true);
// This is in the case of starting up "android".
if (apk == null) apk = mThread.getSystemContext().mPackageInfo;
return apk.getAppFactory();
}
}
我们在本书第5章介绍给一种Hook方案,拦截Instrumentation类的execStartActivity方法,如下所示:
public class HookHelper { public static void attachContext() throws Exception{
// 先获取到当前的ActivityThread对象
Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread"); // 拿到原始的 mInstrumentation字段
Instrumentation mInstrumentation = (Instrumentation) RefInvoke.getFieldObject(currentActivityThread, "mInstrumentation"); // 创建代理对象
Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation); // 偷梁换柱
RefInvoke.setFieldObject(currentActivityThread, "mInstrumentation", evilInstrumentation);
}
} public class EvilInstrumentation extends Instrumentation { private static final String TAG = "EvilInstrumentation"; // ActivityThread中原始的对象, 保存起来
Instrumentation mBase; public EvilInstrumentation(Instrumentation base) {
mBase = base;
} public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) { Log.d(TAG, "XXX到此一游!"); // 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
// 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
Class[] p1 = {Context.class, IBinder.class,
IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class};
Object[] v1 = {who, contextThread, token, target,
intent, requestCode, options}; return (ActivityResult) RefInvoke.invokeInstanceMethod(
mBase, "execStartActivity", p1, v1);
}
}
这段代码,我们把系统原先的Instrumentation替换成EvilInstrumentation,在Android P以下的系统是可以运行的,但是在Android P上就会抛出Uninitialized ActivityThread, likely app-created Instrumentation的异常,显然这是因为EvilInstrumentation的mThread为空导致的。
想要解决这个问题,就必须重写EvilInstrumentation中的newActivity方法,如下所示:
public class EvilInstrumentation extends Instrumentation {
//省略了部分代码 public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException { return mBase.newActivity(cl, className, intent);
}
}
这样编码,即使是EvilInstrumentation,在执行newActivity方法的时候,也会执行原先Instrumentation的newActivity方法,Instrumentation的mThread字段不是null,所以就不会抛出上述的异常信息了。
Android插件化的兼容性(中):Android P的适配的更多相关文章
- Android插件化的兼容性(上):Android O的适配
首先声明,<Android插件化开发指南>这本书所介绍的Android底层是基于Android6.0(API level 23)的,而本书介绍的各种插件化解决方案,以及配套的70多个例子, ...
- Android插件化的兼容性(下):突破Android P中灰黑名单的限制
在Android P系统中,加入了访问私有API接口的限制.
- Android插件化(三)载入插件apk中的Resource资源
Android载入插件apk中的Resource资源 简单介绍 怎样载入未安装apk中的资源文件呢?我们从android.content.res.AssetManager.java的源代码中发现,它有 ...
- 《Android插件化开发指南》勘误
一些常识: 1)全书70个代码例子中,涉及到插件的例子,请先assemble插件的项目,这会在HostApp项目中生成assets目录,并在该目录下plugin1.apk.这样,HostApp才能正常 ...
- Android插件化开发---执行未安装apk中的Service
欢迎各位增加我的Android开发群[257053751] 假设你还不知道什么叫插件化开发.那么你应该先读一读之前写的这篇博客:Android插件化开发,初入殿堂 上一篇博客主要从总体角度分析了一下 ...
- Android插件化(二):使用DexClassLoader动态载入assets中的apk
Android插件化(二):使用DexClassLoader动态载入assets中的apk 简单介绍 上一篇博客讲到.我们能够使用MultiDex.java载入离线的apk文件.须要注意的是,apk中 ...
- 有关Android插件化思考
最近几年移动开发业界兴起了「 插件化技术 」的旋风,各个大厂都推出了自己的插件化框架,各种开源框架都评价自身功能优越性,令人目不暇接.随着公司业务快速发展,项目增多,开发资源却有限,如何能在有限资源内 ...
- Android 插件化和热修复知识梳理
概述 在Android开发中,插件化和热修复的话题越来越多的被大家提及,同时随着技术的迭代,各种框架的发展更新,插件化和热修复的框架似乎已经日趋成熟,许多开发者也把这两项技术运用到实际开发协作和正式的 ...
- 自己动手写Android插件化框架,让老板对你刮目相看
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由达文西发表于云+社区专栏 最近在工作中接触到了Android插件内的开发,发现自己这种技术还缺乏最基本的了解,以至于在一些基本问题上浪 ...
随机推荐
- CentOS7+CDH5.14.0安装全流程记录,图文详解全程实测-总目录
CentOS7+CDH5.14.0安装全流程记录,图文详解全程实测-总目录: 0.Windows 10本机下载Xshell,以方便往Linux主机上上传大文件 1.CentOS7+CDH5.14.0安 ...
- Ubuntu 16.04 安装的那点事
通常,Ubuntu都是与windows共存——安装成双系统的 如果在虚拟机上安装,请参照 https://blog.csdn.net/wyx100/article/details/51582617 U ...
- 使用发射将JavaBean转为Map
import java.lang.reflect.Field; private static Map<String, Object> objectToMap(Object obj) thr ...
- laravel -查询近7月走势图案例
// 获取7月前的时间$time = date('Y-m',strtotime("-0 year -7 month -0 day" ));$where['created_at'] ...
- linux上遇到tomcat报Out of Memory错误,导致jenkins崩溃的问题
今天遇到一个问题,就是JENKINS在同时部署两个前端应用时会出现崩溃的现象. 排查过程如下 查看tomcat-jenkins/bin/hs_err_pid27127.log发现: Out of Me ...
- JavaSE基础知识(5)—面向对象(Object类)
一.包 java.lang包,属于java中的核心包,系统默认导入的,不用手动导入该包中的类:Object.System.String.Integer等 1.包的好处 ①分类管理java文件,查找和管 ...
- server 打开失败
server:An unexpected exception was thrown. 当server服务器遇到这样遇到不能料想的错误导致打开失败的情况下,我们可以找到一个com.genuitec.ec ...
- 4.Redis客户端
4.Redis客户端4.1 客户端通信协议4.2 Java客户端Jedis4.2.1 获取Jedis4.2.2 Jedis的基本使用方法4.2.3 Jedis连接池的使用方法4.2.4 Redis中P ...
- node,Yeoman,Bower,Grunt的简介及安装
作为前端,基本的html,css,js已经不太够用了,所以要学习一些前端自动化工具,来提高我们的生产力 1.NodeJS 先安装NodeJS,直接去官网,下载最新的版本,一定要最新的版本,这样会避免很 ...
- python 12 模块与包
一.不知道什么原则 python文件下面只写方法,所有的可直接执行的代码,都放在条件下 原因.该文件有可能被其他文件调用 二.eval() 将字符串转为相应的数据格式可以想象json转为map 三.模 ...