Android插件化启动Activity

支付宝APP本身更像是一个”空壳”,里面可以搭载很多小功能,这些小功能都是以”插件”的形式存在,支持小功能的灵活配置,用户不想要某个功能,可以不显示出来。插件化开发是当下大型APP必备的一项技术。

插件化开发的核心难点

根据引子中所说,支付宝中各种各样的功能,都是插件形式存在的,那么具体是如何存在?
我们所说的插件,其实是apk文件,即xxx.apk
插件化开发的套路: 外壳app module + 多个插件plugin module + 插件框架层library module

  • 外壳app 负责整个app的外部架构,并且给插件提供入口组件(比如,用一个button作为”余额宝”的入口,点击button,进入”余额宝”)
  • 多个插件plugin module,负责分开开发各个功能。严格来说,每个功能必须可以单独运行,也必须支持集成到外壳app时运行
  • 插件框架层library module,所有插件化的核心代码,都集中到这里。并且这个library要同时被外壳app和插件module引用

插件化所需的技术理论基础

学习插件化开发,首先要了解

Activity是如何启动的

在Activity里,开启另一个Activity,使用startActivity即可,但是startActivity之后,系统做了什么?

开始追踪源码(源码追踪基于SDK 28 - 9.0):

1
2
3
4
5
6
public class MainActivity extends AppCompatActivity() {
private void xxxx() {
Intent i = new Intent(this, XXXActivity.class);
startActivity(i);
}
}

那么,startActivity到底做了什么,点进去看找到下面的代码:

1
2
3
4
5
6
7
8
9
10
@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,直接到下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

结论:我们通常在自己的Activity里调用startActivity之后,最终会执行AMS的startActivity,从而让启动的那个Activity具有生命周期。那么如果只是new一个Activity实例,它会不会具有生命周期呢?显而易见了

apk包(其实是压缩包)里的各个文件各自的作用

在Android Studio里,运行app,或者gradle执行assemble命令可以生成apk文件,那么我们解压apk文件之后,它里面的各种内部文件,各自都起到了什么作用呢?

classes.dex

classes.dex文件,工程里面的Java源码编译打包而成,包含了这个apk的所有Java类,我们拿到这个dex文件,就有能力反射创建其中的类对象

res目录

所有的资源文件,外壳app通过资源包,可以拿到包里面的任意资源,当然前提是宿主要创建对应资源包的Resources对象

resources.arsc

res下所有资源的映射

META-INF

app签名的一些东西

AndroidManifest.xml

清单文件

核心难点的解决方案

外壳app,作为一个”宿主”。插件apk中的所有东西,无论是classes.dex里的类,还是res资源,都是”宿主”之外的东西,那么宿主要想使用自己身外的类和资源,需要解决3个问题:

取得插件中的Activity的Class

解决方案:使用DexClassLoader它是专门加载外部apk的类加载器

取得插件中的资源

解决方案:使用hook技术,创建只属于外部插件的Resouces资源管理器

代理Activity生命周期

反射创建了插件中的Activity对象,但是它是没有生命周期的,不能像使用宿主自身的Activity一样拥有完整的生命周期

解决方案:使用代理Activity作为真正插件Activity的”傀儡”

核心代码结构

app module

外壳app很简单,唯一要说明的就是插件apk,放置在src/main/assets目录,只是为了演示demo方便

MyApp.java,只做了一件事,PluginManager.getInstance().init(this);,对PluginManager进行初始化并且赋予上下文

MainActivity.java只做了两件事:

(1)将assets里面的apk文件,通过工具类AssetUtilcopyAssetToCache方法,拷贝到了app的缓存目录下,然后使用PluginManager去加载这个apk:

1
2
String path = AssetUtil.copyAssetToCache(MainActivity.this, "plugin_module-debug.apk");
PluginManager.getInstance().loadPluginApk(path);

(2)跳转到代理Activity,并且传入真正要跳的目标Activity的name.

1
2
3
4
5
// 先跳到代理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);

plugin module

插件module十分简单,它的作用,就是生成插件apk,对它进行编译打包,取得apk文件即可。

两个重点:

  • 插件中的所有Activity,必须都集成来自plugin_libPluginBaseActivity,只有继承了,才具有插件化特征,能够被外壳app执行startActivity成功跳转
  • 插件内部的Activity跳转,上下文,必须使用PluginBaseActivityproxy变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 插件的第一个Activity
*/
public class MainActivity extends PluginBaseActivity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(proxy, Main2Activity.class);
startActivity(intent);
}
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 插件的第二个Activity
*/
public class Main2Activity extends PluginBaseActivity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
}

}

plugin library module

前面两个module都很简单,那么核心技术在哪里?答案就是插件框架层代码,是插件化开发技术的核心。这个module要同时被外壳app和插件module引用。其中,3个技术要点:

PluginManager类

它是一个单例,负责读取插件apk的内容,并且创建出专属于插件的类加载器DexClassLoader,资源管理器Resources,以及包信息PackageInfo并用public get方法公开出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;

import java.io.File;
import java.lang.reflect.Method;

import dalvik.system.DexClassLoader;

/**
* 插件apk的管理类
* <p>
* loadPluginApk(String path);
* init(Context context);
* getPackageInfo();
* getDexClassLoader();
* getResources();
*/
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;
}

/**
* 既然无论是宿主启动插件的Activity,还是插件内部的跳转都要使用ProxyActivity作为代理
* 何不写一个公共方法以供调用呢?
*
* @param context
* @param realActivityClassName
*/
public void gotoActivity(Context context, String realActivityClassName) {
Intent intent = new Intent(context, ProxyActivity.class);
intent.putExtra(PluginApkConst.TAG_CLASS_NAME, realActivityClassName);
context.startActivity(intent);
}
}
1
2
3
4
public class PluginApkConst {
public final static String TAG_CLASS_NAME = "className";
public static final String TAG_FROM = "from";
}

ProxyActivity类

它作为一个代理,一个傀儡,宿主能够通过它,来间接地管理真正插件Activity的生命周期。那它是如何间接管理真正Activity的生命周期?用类似下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import android.app.Activity;
import android.content.res.Resources;
import android.os.Bundle;

/**
* 代理Activity
* 作用:接收来自宿主的跳转意图,并且拿到其中的参数
* 这里只能继承Activity,而不是AppCompatActivity,否则会报“空指针”
* 原因是,AppCompatActivity会调用上下文,你问为啥?不知道啊,问谷歌大佬
*/
public class ProxyActivity extends Activity {

private String realActivityName; //既然我只是个代理,那么自然有真正的Activity

private IPlugin iPlugin;

// 由ProxyActivity代为管理真正Activity的生命周期
@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();
}
}

@Override
protected void onStart() {
iPlugin.onStart();
super.onStart();
}

@Override
protected void onResume() {
iPlugin.onResume();
super.onResume();
}

@Override
protected void onRestart() {
iPlugin.onRestart();
super.onRestart();
}

@Override
protected void onPause() {
iPlugin.onPause();
super.onPause();
}

@Override
protected void onStop() {
iPlugin.onStop();
super.onStop();
}

@Override
protected void onDestroy() {
iPlugin.onDestroy();
super.onDestroy();
}

//然后,下面2个方法必须重写,因为插件中使用的是外部的类和资源,所以必须用对应的DexClassLoader
@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();
}
}

前面PluginManager返回了专属于插件的类加载器DexClassLoader,资源管理器Resources,那么这个ProxyActivity真正展示的是插件的Activity内容,就要使用插件自己的类加载器和资源管理器

public class ProxyActivity extends Activity {},文中ProxyActivity继承的是android.app.Activity,而不是android.support.v7.app.AppCompatActivity,这是因为,AppCompatActivity会检测上下文context,从而导致空指针。至于更深层的原因,有兴趣的大佬可以继续挖掘,没兴趣的话直接用android.app.Activity即可

IPlugin接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;

/**
* 插件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抽象类

插件module中也许不只一个Activity,我们启动插件Activity之后,插件内部如果需要跳转,仍然要遵守插件化的规则,那就给他们创建一个共同的父类PluginBaseActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

/**
* 插件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内容
PluginManager.getInstance().gotoActivity(proxy, intent.getComponent().getClassName());
}
}
}

PluginBaseActivity抽象类中,3个重点需要特别说明

  1. 插件module需要单独测试,也需要作为插件来集成测试,所以这里IPlugin接口中定义了FROM_INTERNALFROM_EXTERNAL进行情形区分

  2. 除了IPlugin必须实现的一些生命周期方法之外,最后还新增了3个方法:setContentViewfindViewByIdstartActivity,设置布局,寻找组件,跳转Activity,也是需要区分单测还是集成测试的,所以,也要做if/else判定

  3. 上面说的startActivity,当从外部跳转,也就是宿主来启动插件Activity的时候,也只能跳到ProxyActivity,然后把真正的目标Activity放在参数中

  4. plugin library module下的AndroidManifest.xml注册

1
2
3
4
5
6
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cn.appblog.plugin_lib" >
<application>
<activity android:name=".ProxyActivity" />
</application>
</manifest>

如何使用Demo

更改plugin_module的内容,重新生成一个apk,放到app/src/main/assets目录,文件名必须和外壳app内写的一样,运行外壳app即可。

最终效果展示

集成测试,由外壳app启动插件Activity

集成测试

插件单独测试

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2020 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :