正文
1.插件化开发的核心难点
根据引子中所说,支付宝中各种各样的功能,都是插件形式存在的,那么具体是如何存在?
我们所说的插件
,其实是apk文件,即xxx.apk
;
插件化开发的套路: 外壳app module + 多个插件Module + 插件框架层library module
外壳app 负责整个app的外部架构,并且给插件提供入口组件(比如,用一个
button
作为“余额宝”的入口,点击button
,进入“余额宝” );多个插件Module,负责分开开发各个功能。严格来说,每个功能必须可以单独运行,也必须支持集成到外壳
app
时运行。插件框架层library module, 所有插件化的核心代码,都集中到这里。并且这个
library
要同时被外壳app
和插件module
引用.
文字描述不够直观?
看下图:
插件化开发的代码结构.png
那么现在很清晰了,插件化开发的难点,就是如何让外壳app,启动插件apk中的Activity.
既然给出了demo的代码架构,那就顺便给出github地址了:Demo
2.插件化所需的技术理论基础
可能从上面的代码架构上看,这项技术并不是很复杂,但是也是需要一定的技术基础的,不然出一点小问题,一脸懵逼,无从查起就很尴尬了.
学习插件化开发,首先要了解
1.
Activity
是如何启动的.
在我们自己的Activity
里,开启另一个Activity
,使用startActivity
即可,但是startActivity
之后,系统做了什么?
开始追踪源码(源码追踪基于SDK 28 - 9.0):
我们通常通常启动Activity,一般都是在Activity中 使用startActivity(intent),像下面这样
public class MainActivity extends AppCompatActivity(){ private void xxxx(){ Intent i = new Intent(this,XXXActivity.class); startActivity(i); } }
那么,startActivity到底做了什么,点进去看找到下面的代码:
@Override public void startActivity(Intent intent, @Nullable Bundle options) { if (options != null) { startActivityForResult(intent, -1, options); } else { // Note we want to go through this call for compatibility with // applications that may have overridden the method. startActivityForResult(intent, -1); } }
继续,追踪这两个startActivityForResult,直接到下面的代码:
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) { if (mParent == null) { options = transferSpringboardActivityOptions(options); Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options);//注意看这里,mInstrumentation.execStartActivity if (ar != null) { mMainThread.sendActivityResult( mToken, mEmbeddedID, requestCode, ar.getResultCode(), ar.getResultData()); } if (requestCode >= 0) { ··· mStartedActivity = true; } cancelInputsAndStartExitTransition(options); // TODO Consider clearing/flushing other event sources and events for child windows. } else { if (options != null) { mParent.startActivityFromChild(this, intent, requestCode, options); } else { // Note we want to go through this method for compatibility with // existing applications that may have overridden it. mParent.startActivityFromChild(this, intent, requestCode); } } }
mInstrumentation.execStartActivity
在这里被执行,
然而另一个分支mParent
不为空时,会执行mParent.startActivityFromChild
那么追踪它startActivityFromChild
,
public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) { options = transferSpringboardActivityOptions(options); Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( this, mMainThread.getApplicationThread(), mToken, child, intent, requestCode, options);//然而,这里还是执行了mInstrumentation.execStartActivity if (ar != null) { mMainThread.sendActivityResult( mToken, child.mEmbeddedID, requestCode, ar.getResultCode(), ar.getResultData()); } cancelInputsAndStartExitTransition(options); }
然而,这里还是执行了mInstrumentation.execStartActivity
,
综上所述,startActivity,最终都会执行到mInstrumentation.execStartActivity
,
那么继续跟踪这个execStartActivity:
public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { ...省略一大段... try { intent.migrateExtraStreamToClipData(); intent.prepareToLeaveProcess(who); int result = ActivityManager.getService()//这个不就是大名鼎鼎的AMS么 .startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options); checkStartActivityResult(result, intent); } catch (RemoteException e) { throw new RuntimeException("Failure from system", e); } return null; }
注意这里有一个
ActivityManager.getService()
,其实他就是安卓里大名鼎鼎的AMS(ActivityManagerService
),负责对 Android四大组件(activity
,service
,broadcast
,contentProvider
)的管理,包括启动,生命周期管理等.
Activity里面startActivity的追踪就到这里。
PS:其实,Activity不只是可以在Activity里启动,还可以使用 getApplicationContext().startActivity(),有兴趣的可以去追踪一下,最终结论还是一样,都会执行AMS的startActivity.
结论:
我们通常在自己的XXXActivity里调用startActivity之后,最终会执行AMS的startActivity,从而让启动的那个Activity具有生命周期.
那么,如果只是new 一个 Activity实例,它会不会具有生命周期呢?显而易见了.
2. apk包(其实是压缩包)里的各个文件各自的作用
在androidStudio
里,运行app,或者gradle
执行assemble
命令可以生成apk
文件,那么,我们解压apk
文件之后,它里面的各种内部文件,各自都起到了什么作用呢?
请看下图
一个apk解压之后的内容
这里我们发现了这么几个东西:
classes.dex 工程里面的java源码编译打包而成.
classes.dex文件,包含了这个apk的所有java类,那么我们拿到了这个dex文件,就有能力反射创建其中的类对象.
用AndroidStudio可以看到其内容:image.png
res目录 所有的资源文件
image.png
外壳app 通过资源包,可以拿到包里面的任意资源,当然,前提是,宿主要创建对应资源包的Resources
对象.
resources.arsc res下所有资源的映射
image.png
META-INF app签名的一些东西
AndroidManifest.xml 清单文件
3.核心难点的解决方案
了解了上面的技术基础,那么现在摆出解决方案:
外壳app,作为一个"宿主"。插件apk中的所有东西,无论是classes.dex里的类,还是res资源,都是"宿主"之外的东西,那么宿主要想使用自己身外的类和资源,需要解决3个问题:
1. 取得插件中的Activity的Class
解决方案 ==> 使用DexClassLoader.它是专门加载外部apk的类加载器.
2. 取得插件中的资源
解决方案 ==> 使用hook技术,创建只属于外部插件的Resouces资源管理器.
3. 反射创建了插件中的Activity对象,但是它是没有生命周期的,不能像使用宿主自身的Activity一样拥有完整的生命周期.如果不理解,请回去看 “2.插件化所需的技术理论基础”
解决方案 ==> 使用 代理Activity
作为真正插件Activity的"傀儡".
4.核心代码结构 这是Demo地址
以demo为样板进行细节讲解,下图是demo的项目结构:
外壳app的结构.png
可以看到,外壳app很简单,唯一要说明的就是插件apk,我放置在src/main/assets目录,只是为了演示demo方便.
MyApp.java ,只做了一件事,
PluginManager.getInstance().init(this);
,对PluginManager进行初始化并且赋予上下文.MainActivity.java 只做了两件事,
将asssets里面的apk文件,通过工具类AssetUtil的copyAssetToCache方法,拷贝到了app的缓存目录下,然后使用PluginManager去加载这个apk.
String path = AssetUtil.copyAssetToCache(MainActivity.this, "plugin_module-debug.apk"); PluginManager.getInstance().loadPluginApk(path);
跳转到代理Activity,并且传入真正要跳的目标Activity的name.
// 先跳到代理Activity,由代理Activity展示真正的Activity内容 Intent intent = new Intent(MainActivity.this, ProxyActivity.class); intent.putExtra(PluginApkConst.TAG_CLASS_NAME, PluginManager.getInstance().getPackageInfo().activities[0].name); startActivity(intent);这里涉及到了
PluginManager
类,下一小节详述.
插件module.png
插件module十分简单,它的作用,就是生成插件apk,对它进行编译打包,取得apk文件即可。
两个重点:
插件中的所有Activity,必须都集成来自plugin_lib的
PluginBaseActivity
,只有继承了,才具有插件化特征,能够被外壳app执行startActivity成功跳转.插件内部的Activity跳转,上下文,必须使用
PluginBaseActivity
的proxy
变量.image.png
前面两个module都很简单,那么核心技术在哪里?
插件化框架library.png
插件框架层代码,是插件化开发技术的核心。这个module要同时被外壳app和插件module引用.
其中,3个技术要点:
PluginManager类
它是一个单例,负责读取插件apk的内容,并且创建出专属于插件
的类加载器DexClassLoader
,资源管理器Resources
,以及包信息PackageInfo
并 用public get方法公开出去。
public class PluginManager { //应该是单例模式,因为一个宿主app只需要一个插件管理器对象即可 private PluginManager() { } private volatile static PluginManager instance;//volatile 保证每一次取的instance对象都是最新的 public static PluginManager getInstance() { if (instance == null) { synchronized (PluginManager.class) { if (instance == null) { instance = new PluginManager(); } } } return instance; } private Context mContext;//上下文 private PackageInfo packageInfo;//包信息 private DexClassLoader dexClassLoader;//类加载器 private Resources resources;//资源包 public void init(Context context) { mContext = context.getApplicationContext();//要用application 因为这是单例,直接用Activity对象作为上下文会导致内存泄漏 } /** * 从插件apk中读出我们所需要的信息 * * @param apkPath */ public void loadPluginApk(String apkPath) { //先拿到包信息 packageInfo = mContext.getPackageManager().getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);//只拿Activity if (packageInfo == null) throw new RuntimeException("插件加载失败");//如果apkPath是传的错的,那就拿不到包信息了,下面的代码也就不用执行 //类加载器,DexClassLoader专门负责外部dex的类 File outFile = mContext.getDir("odex", Context.MODE_PRIVATE); dexClassLoader = new DexClassLoader(apkPath, outFile.getAbsolutePath(), null, mContext.getClassLoader()); //创建AssetManager,然后创建Resources try { AssetManager assetManager = AssetManager.class.newInstance(); Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); method.invoke(assetManager, apkPath); resources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration()); } catch (Exception e) { e.printStackTrace(); } } //把这3个玩意公开出去 public PackageInfo getPackageInfo() { return packageInfo; } public DexClassLoader getDexClassLoader() { return dexClassLoader; } public Resources getResources() { return resources; } }
ProxyActivity类
它作为一个代理,一个傀儡,宿主能够通过它,来间接地管理真正插件Activity的生命周期.
那它是如何间接管理真正Activity的生命周期?用类似下面的代码:
public void ProxyActivity extends Activity{ @Override protected void onStart() { iPlugin.onStart();//iPlugin是插件Activity实现的接口,前面用IPlugin将插件Activity对象接收了 super.onStart(); } }
然而,ProxyActivity的onCreate另有玄机
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); realActivityName = getIntent().getStringExtra(PluginApkConst.TAG_CLASS_NAME);//宿主,将真正的跳转意图,放在了这个参数className中, //拿到realActivityName,接下来的工作,自然就是展示出真正的Activity try {// 原则,反射创建RealActivity对象,但是,去拿这个它的class,只能用dexClassLoader Class<?> realActivityClz = PluginManager.getInstance().getDexClassLoader().loadClass(realActivityName); Object obj = realActivityClz.newInstance(); if (obj instanceof IPlugin) {//所有的插件Activity,都必须是IPlugin的实现类 iPlugin = (IPlugin) obj; Bundle bd = new Bundle(); bd.putInt(PluginApkConst.TAG_FROM, IPlugin.FROM_EXTERNAL); iPlugin.attach(this); iPlugin.onCreate(bd);//反射创建的插件Activity的生命周期函数不会被执行,那么,就由ProxyActivity代为执行 } } catch (Exception e) { e.printStackTrace(); } }
前面PluginManager返回了专属于插件
的类加载器DexClassLoader
,资源管理器Resources
,那么这个ProxyActivity真正展示的是插件的Activity内容,就要使用插件自己的类加载器和资源管理器了.
@Override public ClassLoader getClassLoader() { ClassLoader classLoader = PluginManager.getInstance().getDexClassLoader(); return classLoader != null ? classLoader : super.getClassLoader(); } @Override public Resources getResources() { Resources resources = PluginManager.getInstance().getResources(); return resources != null ? resources : super.getResources(); }
注意,前方大坑,
public class ProxyActivity extends Activity{}
,我的ProxyActivity
继承的是android.app.Activity
,而不是android.support.v7.app.AppCompatActivity
,这是因为,AppCompatActivity
会检测上下文context,从而导致空指针.
至于更深层的原因,有兴趣的大佬可以继续挖掘,没兴趣的话直接用android.app.Activity
就完事了.
IPlugin接口和PluginBaseActivity类
插件module中也许不只一个Activity,我们启动插件Activity之后,插件内部如果需要跳转,仍然要遵守插件化
的规则,那就给他们创建一个共同的父类PluginBaseActivity.
IPlugin 接口
/** * 插件Activity的接口规范 */public interface IPlugin { int FROM_INTERNAL = 0;//插件单独测试时的内部跳转 int FROM_EXTERNAL = 1;//宿主执行的跳转逻辑 /** * 给插件Activity指定上下文 * * @param activity */ void attach(Activity activity); // 以下全都是Activity生命周期函数, // 插件Activity本身 在被用作"插件"的时候不具备生命周期,由宿主里面的代理Activity类代为管理 void onCreate(Bundle saveInstanceState); void onStart(); void onResume(); void onRestart(); void onPause(); void onStop(); void onDestroy(); void onActivityResult(int requestCode, int resultCode, Intent data); }
PluginBaseActivity 抽象类
/** * 插件Activity的基类,插件中的所有Activity,都要继承它 */public abstract class PluginBaseActivity extends AppCompatActivity implements IPlugin { private final String TAG = "PluginBaseActivityTag"; protected Activity proxy;//上下文 //这里基本上都在重写原本Activity的函数,因为 要兼容“插件单独测试” 和 "集成到宿主整体测试",所以要进行情况区分 private int from = IPlugin.FROM_INTERNAL;//默认是“插件单独测试” @Override public void attach(Activity proxyActivity) { proxy = proxyActivity; } @Override public void onCreate(Bundle saveInstanceState) { if (saveInstanceState != null) from = saveInstanceState.getInt(PluginApkConst.TAG_FROM); if (from == IPlugin.FROM_INTERNAL) { super.onCreate(saveInstanceState); proxy = this;//如果是从内部跳转,那就将上下文定为自己 } } @Override public void onStart() { if (from == IPlugin.FROM_INTERNAL) { super.onStart(); } else { Log.d(TAG, "宿主启动:onStart()"); } } @Override public void onResume() { if (from == IPlugin.FROM_INTERNAL) { super.onResume(); } else { Log.d(TAG, "宿主启动:onResume()"); } } @Override public void onRestart() { if (from == IPlugin.FROM_INTERNAL) { super.onRestart(); } else { Log.d(TAG, "宿主启动:onRestart()"); } } @Override public void onPause() { if (from == IPlugin.FROM_INTERNAL) { super.onPause(); } else { Log.d(TAG, "宿主启动:onPause()"); } } @Override public void onStop() { if (from == IPlugin.FROM_INTERNAL) { super.onStop(); } else { Log.d(TAG, "宿主启动:onStop()"); } } @Override public void onDestroy() { if (from == IPlugin.FROM_INTERNAL) { super.onDestroy(); } else { Log.d(TAG, "宿主启动:onDestroy()"); } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (from == IPlugin.FROM_INTERNAL) { super.onActivityResult(requestCode, resultCode, data); } else { Log.d(TAG, "宿主启动:onActivityResult()"); } } //下面是几个生命周期之外的重写函数 @Override public void setContentView(int layoutResID) {//设置contentView分情况 if (from == IPlugin.FROM_INTERNAL) { super.setContentView(layoutResID); } else { proxy.setContentView(layoutResID); } } @Override public View findViewById(int id) { if (from == FROM_INTERNAL) { return super.findViewById(id); } else { return proxy.findViewById(id); } } @Override public void startActivity(Intent intent) {//同理 if (from == IPlugin.FROM_INTERNAL) { super.startActivity(intent);//原intent只能用于插件单独运行时 } else { // 如果是集成模式下,插件内的跳转,控制权 仍然是在宿主上下文里面,所以--! // 先跳到代理Activity,由代理Activity展示真正的Activity内容 Intent temp = new Intent(proxy, ProxyActivity.class); temp.putExtra(PluginApkConst.TAG_CLASS_NAME, intent.getComponent().getClassName()); proxy.startActivity(temp);//这里不能跳原来的intent,,必须重新创建 } } }
PluginBaseActivity 抽象类中,3个重点需要特别说明
1. 插件module需要单独测试,也需要 作为插件来集成测试,所以这里IPlugin接口中定义了 FROM_INTERNAL和FROM_EXTERNAL 进行情形区分.
2. 除了IPlugin必须实现的一些生命周期方法之外,最后我还新增了3个方法:
setContentView
,findViewById
,startActivity
设置布局,寻找组件,跳转Activity,也是需要区分 单测还是集成测试的,所以,也要做if/else判定.
并且[看3.]3. 上面说的startActivity,当从外部跳转,也就是宿主来启动插件Activity的时候,也只能跳到ProxyActivity,然后把真正的目标Activity放在参数中.
5.如何使用Demo
image.png
我的demo中已经有了一个插件apk,如上图.
如果你更改了plugin_module
的内容,请重新生成一个apk,放到上图所示位置,文件名必须和外壳app内写的一样.放好之后,运行外壳app即可。
6.最终效果展示
集成测试,由外壳app启动插件Activity
作者:波澜步惊
链接:https://www.jianshu.com/p/3a5cf6ebfafc