前言
由于近期项目中要用到插件,所以特地去翻找资料学习了一番,现在在这里分享我所学到的东西给大家,有什么错误的希望能给我指出来,文章有点长,希望大家能认真读完。 近些年来,插件化可谓是特别的火热,就拿支付宝美团等软件来说,都是使用这个技术来支撑他们的产品。但是什么是插件化呢,插件化到底有什么好处呢? 插件化也就是运行的APP(宿主APP)去加载插件APP(没有安装的APP),这就是所谓的插件化开发。
插件化到底运行在什么场景下呢?其实插件化使用的场景有很多,这里就比如下图的支付宝或者美团等APP,点击某个相应的item,就会跳转到相应的页面当中,其实这个页面是插件apk中的页面,但是它到底怎么做到的呢?怎么做到不安装apk而加载插件中的页面呢?
下面我们就来探索探索不用安装插件apk是怎么去加载里面的Activity、Service、BroadCastReceiver等这些组件的。本篇文章所提的是占位式(插桩式)插件化。 由于插件apk是没有安装的,也就是插件apk没有组件的一些环境,比如context上下文对象之类的,如果要用到这些环境就必须依赖宿主的环境运行。所以我们就要宿主跟插件之间定义一个标准。用来传递宿主中的环境给插件。
加载插件中的Activity
第一步:
首先我们先定义一个标准,让插件实现我们的标准来传递宿主APP的环境,下图是定义Activity类的标准,下面我们从加载插件中的Activity开始讲起。
首先我们要在插件中实现刚才我们定义的标准,由于插件都需要宿主APP的环境,所以我们就定义一个基类来实现该标准,然后让我们的插件的Activity来继承该基类,该Activity就具有了宿主的环境了。如图所示。
第二步:
build工程,得到插件apk,命名为 plugin.apk 并把它放到我们的sd目录下。让宿主APP来加载插件。 首先我们要加载插件apk中的类,就需要用到DexClassLoader这个类,下面是用该类来加载插件apk的方法。
File file = new File(path);
File pluginDir = context.getDir("plugin", Context.MODE_PRIVATE);
//加载插件的class
dexClassLoader = new DexClassLoader(path, pluginDir.getAbsolutePath(), null, context.getClassLoader());
参数说明: path:插件所存放的目录(plugin.apk存放的目录) pluginDir.getAbsolutePath():插件apk解析后dex文件所存放的路径 null:该参数是so库存放的路径,由于插件里没有so库,所以为null。 context.getClassLoader():类加载器
获取到了DexClassLoader的对象,我们就可以拿到插件中的类了,接下来我们要获取插件apk中的资源对象,也就是 Resources对象。由于插件apk没有宿主的环境,也就无法使用 context.getResources()的方式来得到一个Resources对象,我们只能new 一个对象出来。
从上面代码中可以看出,我们可以看出创建一个 Resources对象需要传递一个 AssetManager、DisplayMetrics、Configuration对象,而这些对象我们怎么可以获取到呢?其实后两个参数很容易获取到。后面的两个参数我们可以直接用宿主的Resources对象里面的属性就可以了,如下
Resources appResources = context.getResources();
DisplayMetrics displayMetrics = appResources.getDisplayMetrics();
Configuration configuration = appResources.getConfiguration();
现在我们比较头疼的问题是如何获取AssetManager这个类的对象?其实我们可以利用反射的方式来获取一个AssetManager的对象,如下
AssetManager pluginAssetManager = AssetManager.class.newInstance();
阅读源码发现,AssetManager类里面要更改资源路径就必须要调用 addAssetPath 方法。从这里得出我们可以反射拿到这个方法,然后传入插件apk的资源路径即可。
AssetManager pluginAssetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(pluginAssetManager, path);
通过上述代码可以获取到我们的插件apk的 AssetManager对象了,拥有该对象,我们就可以获取到 Resources对象了,总体代码如下:
//加载插件的资源文件
//1、获取插件的AssetManager
AssetManager pluginAssetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(pluginAssetManager, path);
//2、获取宿主的Resources
Resources appResources = context.getResources();
//实例化插件的Resources
pluginResource = new Resources(pluginAssetManager, appResources.getDisplayMetrics(), appResources.getConfiguration());
拿到了DexClassLoader对象和Resources对象我们就可以加载插件apk中的activity等组件了。要跳转到插件中的activity,我们首先要获取到插件找那个的Activity,通过 PackageManager.getPackageArchiveInfo()我们可以得到一个 PackageInfo对象,而这个对象里面就有我们需要的Activity数组,如下图:
获取插件Activity数组我们就可以拿到Activity对象,并且可以进行跳转了:
//获取插件包的Activity
PackageManager packageManager = getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
//获取在manifest文件中注册的第一个activity
ActivityInfo activity = packageArchiveInfo.activities[0];
Intent intent = new Intent(this, ProxyActivity.class);
intent.putExtra("className", activity.name);
通过以上代码显示,我们先是跳转到了一个ProxyActivity,然后把插件的Activity类名传递过去,为什么不直接跳转到插件的Activity呢?原因是因为插件的Activity没有在我们的宿主的manifest文件中进行注册,如果直接跳转就会发生崩溃,所以我们这里先跳转到一个代理的Activity,因为该Activity是宿主里面的并且在manifest文件中进行注册了,所以我们用它来进行模拟Activity入栈出栈的操作,当我们的代理Activity回调onCreate() 的时候,我们可以获取到传递过来的className来反射得到我们的插件中的Activity对象,由于我们插件中的Activity是实现了上面所述的标准,我们可以通过标准来调用插件Activity中的生命周期。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//真正的加载插件里面的Activity
String className = getIntent().getStringExtra("className");
try {
Class<?> pluginActivity1Clazz = getClassLoader().loadClass(className);
Constructor<?> constructor = pluginActivity1Clazz.getConstructor(new Class[]{});
pluginActivity1 = (IActivityInterface) constructor.newInstance(new Object[]{});
pluginActivity1.insertAppContext(this);
Bundle bundle = new Bundle();
bundle.putString("value", "我是宿主传递过来的字符串");
pluginActivity1.onCreate(bundle);
} catch (Exception e) {
e.printStackTrace();
}
}
当执行完上述代码后我们就可以加载出插件的Activity了
插件内跳转Activity
跳转成功后,我们就要实现插件中的Activity跳转到插件中的另一个Activity了。大家都知道跳转Activity的话我们都要执行startActivity() 方法,而查看源码而知,startActivity() 里调用了 this.startActivity(intent, null);这里的 this表示着上下文对象,但是我们的插件APP没有上下文对象的环境,如果执行了 startActivity() 方法肯定会发生崩溃的,那么怎样才能避免崩溃,并且成功跳转呢。其实非常简单,我们只要使用宿主APP的环境就可以来实现该功能了,只不过是稍微有些麻烦。我们需要重写 startActivity() 方法,并且传递我们需要跳转的Activity的全类名给宿主,且跳转的方法交给我们的宿主来进行就可以了,然后我们重写宿主APP的代理Activity 的startActivity() 方法用来接收插件APP传递过来的全类名,最后执行宿主APP的代理Activity 的startActivity() 方法即可。如下:
另外提示一点,我们插件的Activity 所有使用的都是宿主的环境,比如 setContentView() 、findViewById() 等方法
至此,我们动态加载插件APP的Activity已经讲完了,至于动态加载Service与动态加载广播的方法跟加载Activity的方法类似,这里不做讲解
注册插件内的静态广播
要加载插件中的静态广播,我们先来提问一个问题。我们APP中的静态广播到底是什么时候被注册的呢? 其实在手机开机的时候,手机里面所有的已经安装APP,系统会再次进行安装一遍,这也就能说明Android手机开机的时候为什么会那么慢。等到安装完成后,会马上扫描 /data/app/ 目录,然后逐个去解析该目录下的所有 apk里面的 Manifest.xml 文件, 如果里面有静态广播后,就会自动注册。 我们现在来研究系统是如何去解析 apk文件里面的组件信息的。我们知道解析apk 文件会用到 PackageManagerService 这个类,所以我们来查看这个类的源码。
跟踪源码发现,执行到 packageParser.parsePackage() 这个方法的时候会返回一个 PackageParser.Package 对象,我们进去查看一下 PackageParser 这个类的源码,发现 Package 是其里面的静态内部类。
以下是代码的实现:
Class<?> mPackageParserClass = Class.forName("android.content.pm.PackageParser");
Object mPackageParser = mPackageParserClass.newInstance();
Method parsePackageMethod = mPackageParserClass.getMethod("parsePackage", File.class, int.class);
Object mPackage = parsePackageMethod.invoke(mPackageParser, file, PackageManager.GET_ACTIVITIES);
//2、获取Package类下的 public final ArrayList<Activity> receivers = new ArrayList<Activity>(0); 广播集合
Field mReceiversField = mPackage.getClass().getDeclaredField("receivers");
//本质上是 ArrayList<Activity> receivers
ArrayList<Object> receivers = (ArrayList<Object>) mReceiversField.get(mPackage);
第二步:
在第一步我们获取到了 receivers 这个广播集合,有了这个集合我们就可以拿到里面的广播来进行注册了。我们先来看看先跟到这个 List
我们可以发现里面有个ActivityInfo 的属性。我们继续跟这个类和他的父类的源码,结果发现,name 这个属性刚好就是 android:name="com.xxx.xxx" 对应里面的包名跟类名,我们需要获取到这个值来 new 一个广播对象出来。
我们现在想办法得到上述 Activity 里面的 info这个属性这样我们就可以获得 name的值了。继续阅读 PackageParser 的源码,我们发现 通过这个方法我们可以得到一个 ActivityInfo 对象,所以我们就来执行这个方法。执行该方法我们需要传递四个参数,第一个Activity类型我们已经有了,第二个参数我们可以直接传个0,第三个我们可以反射来获取一个类的实例。如
Class<?> mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Object mPackageUserState = mPackageUserStateClass.newInstance();
现在就剩下最后一个参数了,就是我们的userId,到底怎么获取这个参数值呢?其实我们可以从 android.os.UserHandle 这个类入手,分析该类得出的结果 通过图中的方法可以获取一个userId;所以我们就以反射来获取该userId; 详细代码如下:
//先获取到 ActivityInfo类
Class<?> mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Object mPackageUserState = mPackageUserStateClass.newInstance();
Method generateActivityInfoMethod = mPackageParserClass.getMethod("generateActivityInfo", mActivity.getClass(),
int.class, mPackageUserStateClass, int.class);
//获取userId
Class<?> mUserHandleClass = Class.forName("android.os.UserHandle");
//public static @UserIdInt int getCallingUserId()
int userId = (int) mUserHandleClass.getMethod("getCallingUserId").invoke(null);
//执行此方法 由于是静态方法 所以不用传对象
ActivityInfo activityInfo = (ActivityInfo) generateActivityInfoMethod.invoke(null, mActivity, 0, mPackageUserState, userId);
以上我们就可以获取到一个ActivityInfo 对象了。
第三步: 我们知道注册广播的时候还要添加一个IntentFliter,但是这个应该从哪里取到呢? 其实我们刚才分析的 Activity 那个类说起 ,它继承与 Component 在跟进这个类去看,发现里面有个 intents 的集合,查看泛型继承的 IntentInfo,就是我们想要的 IntentFliter,所以我们要得到这个集合,然后进行遍历就可以得到 IntentFliter,然后我们就可以注册广播了。 所以注册广播的代码如下:
//3、遍历所有的静态广播
//Activity 该Activity 不是四大组件里面的activity,而是一个Java bean对象,用来封装清单文件中的activity和receiver
for (Object mActivity : receivers) {
//4、获取该广播的全类名 即 <receiver android:name=".PluginStaticReceiver"> android:name属性后面的值
// /**
// * Public name of this item. From the "android:name" attribute.
// */
// public String name;
// public static final ActivityInfo generateActivityInfo(Activity a, int flags,
// PackageUserState state, int userId)
//先获取到 ActivityInfo类
Class<?> mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Object mPackageUserState = mPackageUserStateClass.newInstance();
Method generateActivityInfoMethod = mPackageParserClass.getMethod("generateActivityInfo", mActivity.getClass(),
int.class, mPackageUserStateClass, int.class);
//获取userId
Class<?> mUserHandleClass = Class.forName("android.os.UserHandle");
//public static @UserIdInt int getCallingUserId()
int userId = (int) mUserHandleClass.getMethod("getCallingUserId").invoke(null);
//执行此方法 由于是静态方法 所以不用传对象
ActivityInfo activityInfo = (ActivityInfo) generateActivityInfoMethod.invoke(null, mActivity, 0, mPackageUserState, userId);
String receiverClassName = activityInfo.name;
Class<?> receiverClass = getClassLoader().loadClass(receiverClassName);
BroadcastReceiver receiver = (BroadcastReceiver) receiverClass.newInstance();
//5、获取 intent-filter public final ArrayList<II> intents;这个是intent-filter的集合
//静态内部类反射要用 $+类名
//getField(String name)只能获取public的字段,包括父类的;
//而getDeclaredField(String name)只能获取自己声明的各种字段,包括public,protected,private。
Class<?> mComponentClass = Class.forName("android.content.pm.PackageParser$Component");
Field intentsField = mActivity.getClass().getField("intents");
ArrayList<IntentFilter> intents = (ArrayList<IntentFilter>) intentsField.get(mActivity);
for (IntentFilter intentFilter : intents) {
//6、注册广播
context.registerReceiver(receiver, intentFilter);
}
}
到这里我们已经完成了对插件中的静态广播进行注册
下面附上Github的项目地址,给需要的小伙伴进行参考: