0%

Android基于代理的插件化思路分析

前言

正常的App开发流程基本上是这样的:开发功能–>测试—>上线,上线后发现有大bug,紧急修复—->发新版本—->用户更新—–>bug修复。从发现bug到修复bug花了很长时间。我们希望bug的修复是立马生效的,用户无感知就能自动修复bug。当然,Android端的动态修复bug已经有不少框架了,不过我们今天讲的是另一个话题:Android的插件化。Android插件化有很多好处:热插拔、静默升级、bug动态修复、代码解耦等。正是因为如此,才有越来越多的公司选择插件化。

分析

Android插件化有很多的开源框架,基本上都是两种思路:代理和替换系统的一些关键变量,通过替换这些变量,达到欺骗系统的目的(又称Hook)。代理的思路比较简单,就是通过代理类Proxy,把主工程和插件工程的组件连接起来。代理类相当于傀儡,当主工程想要启动插件工程时,实际上会先调用代理类的相应方法,然后再通过代理类调用插件工程的组件,间接达到调用插件工程组件的目的。为何不直接调用插件工程的组件呢?因为通过DexClassLoader加载到内存的Activity等组件只是一个普通的类,没有上下文环境,意味着拿不到Context,意味着没有生命周期。

让Activity有”生命”

得益于Java语言的类加载器可以动态加载类的特性,在Android中加载一个普通的类是很容易的

1
2
3
4
5
DexClassLoader
Class<?> mClassLaunchActivity = (Class<?>)
classLoader.loadClass(mLaunchActivity);
mPluginActivity = (IPluginActivity)
mClassLaunchActivity.newInstance();

通过ClassLoader可以加载一个类,并生成类的实例。但是这个mPluginActivity只是一个普通的类,并没有Activity的生命周期,所以我们需要借助PluginProxyActivity来完成代理工作

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
public class PluginProxyActivity extends Activity {
IPluginActivity mPluginActivity;
String mPluginApkFilePath;
String mLaunchActivity;
private String mPluginName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle bundle = getIntent().getExtras();
if(bundle == null){
return;
}
mPluginName = bundle.getString(PluginUtils.PARAM_PLUGIN_NAME);
mLaunchActivity = bundle.getString(PluginUtils.PARAM_LAUNCH_ACTIVITY);
File pluginFile = PluginUtils.getInstallPath(PluginProxyActivity.this, mPluginName);
if(!pluginFile.exists()){
return;
}
mPluginApkFilePath = pluginFile.getAbsolutePath();
try {
initPlugin();
mPluginActivity.IOnCreate(savedInstanceState);
} catch (Exception e) {
mPluginActivity = null;
e.printStackTrace();
}
}
@Override
protected void onResume() {
super.onResume();
if(mPluginActivity != null){
mPluginActivity.IOnResume();
}
}
@Override
protected void onStart() {
super.onStart();
if(mPluginActivity != null) {
mPluginActivity.IOnStart();
}
}
........ //省略部分代码
private void initPlugin() throws Exception {
PackageInfo packageInfo;
try {
PackageManager pm = getPackageManager();
packageInfo = pm.getPackageArchiveInfo(mPluginApkFilePath, PackageManager.GET_ACTIVITIES);
} catch (Exception e) {
throw e;
}
ClassLoader classLoader = PluginUtils.getOrCreateClassLoaderByPath(this, mPluginName, mPluginApkFilePath);
// get default launchActivity if target Activity is null
if (mLaunchActivity == null || mLaunchActivity.length() == 0) {
if (packageInfo == null || (packageInfo.activities == null) || (packageInfo.activities.length == 0)) {
throw new ClassNotFoundException("Launch Activity not found");
}
mLaunchActivity = packageInfo.activities[0].name;
}
Class<?> mClassLaunchActivity = (Class<?>) classLoader.loadClass(mLaunchActivity);
mPluginActivity = (IPluginActivity) mClassLaunchActivity.newInstance();
mPluginActivity.IInit(mPluginApkFilePath, this, classLoader, packageInfo);
}
protected Class<? extends PluginProxyActivity> getProxyActivity(String pluginActivityName) {
return getClass();
}
@Override
public void startActivityForResult(Intent intent, int requestCode) {
boolean pluginActivity = intent.getBooleanExtra(PluginUtils.PARAM_IS_IN_PLUGIN, false);
if (pluginActivity) {
String launchActivity = null;
ComponentName componentName = intent.getComponent();
if(null != componentName) {
launchActivity = componentName.getClassName();
}
intent.putExtra(PluginUtils.PARAM_IS_IN_PLUGIN, false);
if (launchActivity != null && launchActivity.length() > 0) {
Intent pluginIntent = new Intent(this, getProxyActivity(launchActivity));
pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_NAME, mPluginName);
pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_PATH, mPluginApkFilePath);
pluginIntent.putExtra(PluginUtils.PARAM_LAUNCH_ACTIVITY, launchActivity);
startActivityForResult(pluginIntent, requestCode);
}
} else {
super.startActivityForResult(intent, requestCode);
}
}
}

每次启动新的Activity的时候,都会调用startActivityForResult,在此方法中,进行了Intent的替换,启动的新Activity还是会跳到ProxyActivity中。

1
2
3
4
5
Intent pluginIntent = new Intent(this, getProxyActivity(launchActivity));
pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_NAME, mPluginName);
pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_PATH, mPluginApkFilePath);
pluginIntent.putExtra(PluginUtils.PARAM_LAUNCH_ACTIVITY, launchActivity);
startActivityForResult(pluginIntent, requestCode);

所有的插件工程都需要继承于BasePluginActivity,其主要代码如下

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
public class BasePluginActivity extends Activity implements IPluginActivity {
private boolean mIsRunInPlugin;
private ClassLoader mDexClassLoader;
private Activity mOutActivity;
private String mApkFilePath;
private PackageInfo mPackageInfo;
private PluginContext mContext;
private View mContentView;
private Activity mActivity;
private boolean mFinished;
@Override
protected void onCreate(Bundle savedInstanceState) {
if (mIsRunInPlugin) {
mActivity = mOutActivity;
} else {
super.onCreate(savedInstanceState);
mActivity = this;
}
}
@Override
public void setContentView(int layoutResID) {
if (mIsRunInPlugin) {
mContentView = LayoutInflater.from(mContext).inflate(layoutResID, null);
mActivity.setContentView(mContentView);
} else {
super.setContentView(layoutResID);
}
}
@Override
public void setContentView(View view) {
if (mIsRunInPlugin) {
mContentView = view;
mActivity.setContentView(mContentView);
} else {
super.setContentView(view);
}
}
@Override
public View findViewById(int id) {
if (mIsRunInPlugin && mContentView != null) {
View v = mContentView.findViewById(id);
if (null == v) {
v = super.findViewById(id);
}
return v;
} else {
return super.findViewById(id);
}
}
@Override
public void IOnCreate(Bundle savedInstanceState) {
onCreate(savedInstanceState);
}
@Override
public void IOnResume() {
onResume();
}
@Override
public void IOnStart() {
onStart();
}
@Override
public void IOnPause() {
onPause();
}
@Override
public void IOnStop() {
onStop();
}
@Override
public void IOnDestroy() {
onDestroy();
}
@Override
public void IOnRestart() {
onRestart();
}
@Override
public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo) {
mIsRunInPlugin = true;
mDexClassLoader = classLoader;
mOutActivity = context;
mApkFilePath = path;
mPackageInfo = packageInfo;
mContext = new PluginContext(context, 0, mApkFilePath, mDexClassLoader);
attachBaseContext(mContext);
}
@Override
protected void onResume() {
if (mIsRunInPlugin) {
return;
}
super.onResume();
}
@Override
protected void onPause() {
if (mIsRunInPlugin) {
return;
}
super.onPause();
}
@Override
protected void onStart() {
if (mIsRunInPlugin) {
return;
}
super.onStart();
}
@Override
protected void onRestart() {
if (mIsRunInPlugin) {
return;
}
super.onRestart();
}
@Override
protected void onStop() {
if (mIsRunInPlugin) {
return;
}
super.onStop();
}
@Override
protected void onDestroy() {
if (mIsRunInPlugin) {
mDexClassLoader = null;
return;
}
super.onDestroy();
}
@Override
public void finish() {
if (mIsRunInPlugin) {
int resultCode = Activity.RESULT_CANCELED;
Intent data = null;
synchronized (this) {
Field field;
try {
field = Activity.class.getDeclaredField("mResultCode");
field.setAccessible(true);
resultCode = (Integer) field.get(this);
field = Activity.class.getDeclaredField("mResultData");
field.setAccessible(true);
data = (Intent) field.get(this);
} catch (Exception e) {
}
}
mOutActivity.setResult(resultCode, data);
mOutActivity.finish();
mFinished = true;
} else {
super.finish();
}
}
@Override
public boolean isFinishing() {
if (mIsRunInPlugin) {
return mFinished;
} else {
return super.isFinishing();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (mIsRunInPlugin) {
return;
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
public LayoutInflater getLayoutInflater() {
if (mContext != null) {
return LayoutInflater.from(mContext);
} else {
return LayoutInflater.from(mActivity);
}
}
@Override
public WindowManager getWindowManager() {
if (mIsRunInPlugin) {
return mOutActivity.getWindowManager();
} else {
return super.getWindowManager();
}
}
@Override
public void startActivityForResult(Intent intent, int requestCode) {
if (mIsRunInPlugin) {
intent.putExtra(PluginUtils.PARAM_IS_IN_PLUGIN, true);
mActivity.startActivityForResult(intent, requestCode);
} else {
super.startActivityForResult(intent, requestCode);
}
}
}

在BasePluginActivity中,覆写了很多父类Activity的方法,用来判断当前Activity是独立运行还是作为插件运行,如果是在插件中运行,则是调用插件中设置进来的代理类ProxyActivity(mActivity)的相应方法。同时使用IPluginActivity来模拟Activity的生命周期

1
2
3
4
5
6
7
8
9
10
public interface IPluginActivity {
public void IOnCreate(Bundle savedInstanceState);
public void IOnResume();
public void IOnStart();
public void IOnPause();
public void IOnStop();
public void IOnDestroy();
public void IOnRestart();
public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo);
}

至此Activity已经有生命周期了!但是还有个问题,插件的资源如何获取?

插件资源的获取

这里就不卖关子了。直接通过反射调用AssetManager的addAssetPath方法把资源加载到Resource对象中,即可获取插件中的资源。为了后续方便,我们直接继承ContextWrapper类自己实现getAssets和getResources方法即可。代码如下

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
class PluginContext extends ContextWrapper {
private AssetManager mAsset;
private Resources mResources;
private Theme mTheme;
private int mThemeResId;
private ClassLoader mClassLoader;
private Context mOutContext;
private AssetManager getSelfAssets(String apkPath) {
AssetManager instance = null;
try {
instance = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(instance, apkPath);
} catch (Throwable e) {
e.printStackTrace();
}
return instance;
}
private Resources getSelfRes(Context ctx, AssetManager selfAsset) {
DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();
Configuration con = ctx.getResources().getConfiguration();
return new Resources(selfAsset, metrics, con);
}
private Theme getSelfTheme(Resources selfResources) {
Theme theme = selfResources.newTheme();
mThemeResId = getInnerRIdValue("com.android.internal.R.style.Theme");
theme.applyStyle(mThemeResId, true);
return theme;
}


private int getInnerRIdValue(String rStrnig) {
int value = -1;
try {
int rindex = rStrnig.indexOf(".R.");
String Rpath = rStrnig.substring(0, rindex + 2);
int fieldIndex = rStrnig.lastIndexOf(".");
String fieldName = rStrnig.substring(fieldIndex + 1, rStrnig.length());
rStrnig = rStrnig.substring(0, fieldIndex);
String type = rStrnig.substring(rStrnig.lastIndexOf(".") + 1, rStrnig.length());
String className = Rpath + "$" + type;
Class<?> cls = Class.forName(className);
value = cls.getDeclaredField(fieldName).getInt(null);
} catch (Throwable e) {
e.printStackTrace();
}
return value;
}
public PluginContext(Context base, int themeres, String apkPath, ClassLoader classLoader) {
super(base, themeres);
mClassLoader = classLoader;
mAsset = getSelfAssets(apkPath);
mResources = getSelfRes(base, mAsset);
mTheme = getSelfTheme(mResources);
mOutContext = base;
}
@Override
public Resources getResources() {
return mResources;
}
@Override
public AssetManager getAssets() {
return mAsset;
}
@Override
public Theme getTheme() {
return mTheme;
}
}

至此,插件化的两大难题已经解决。现在插件中的Activity可以启动了!用同样的思路可以完成其他组件的代码编写。

源码下载

PluginDemo

延伸阅读