前言

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

分析

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

让Activity有"生命"

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

DexClassLoader
Class<?> mClassLaunchActivity = (Class<?>)
classLoader.loadClass(mLaunchActivity);
mPluginActivity = (IPluginActivity)
mClassLaunchActivity.newInstance();

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


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中。

 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,其主要代码如下


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的生命周期

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方法即可。代码如下

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

延伸阅读

资源加载和activity生命周期管理

基于Proxy思想的Android插件框架

Android基于代理的插件化思路分析的更多相关文章

  1. android 基于dex的插件化开发

    安卓里边可以用DexClassLoader实现动态加载dex文件,通过访问dex文件访问dex中封装的方法,如果dex文件本身还调用了native方法,也就间接实现了runtime调用native方法 ...

  2. 基于AOP的插件化(扩展)方案

    在项目迭代开发中经常会遇到对已有功能的改造需求,尽管我们可能已经预留了扩展点,并且尝试通过接口或扩展类完成此类任务.可是,仍然有很多难以预料的场景无法通过上述方式解决.修改原有代码当然能够做到,但是这 ...

  3. Android架构设计之插件化、组件化

    如今移动app市场已经是百花齐放,其中有不乏有很多大型公司.巨型公司都是通过app创业发展起来的:app类型更加丰富,有电子商务.有视频.有社交.有工具等等,基本上涵盖了各行各业每个角落,为了更加具有 ...

  4. 基于Fragment的插件化

    --<摘自android插件化开发指南> 1.有些项目,整个app只有一个Activity,切换页面全靠Fragment,盛行过一时,但有点极端 2.Activity切换fragment页 ...

  5. Android 模块化/热修复/插件化 框架选用

    概念汇总 动态加载:在程序运行的时候,加载一些程序自身原本不存在的文件并运行这些文件里的代码逻辑.动态加载是热修复与插件化实现的基础. 热修复:修改部分代码,不用重新发包,在用户不知情的情况下,给ap ...

  6. Android热修复、插件化、组件化

    模块化:项目按照独立的模块进行划分 组件化:将项目按照单一的组件来进行划分结构 项目组件化的重要环节在于,将项目按照模块来进行拆分,拆分成一个个业务module和其他支撑module(lib),各个业 ...

  7. Android 插件化和热修复知识梳理

    概述 在Android开发中,插件化和热修复的话题越来越多的被大家提及,同时随着技术的迭代,各种框架的发展更新,插件化和热修复的框架似乎已经日趋成熟,许多开发者也把这两项技术运用到实际开发协作和正式的 ...

  8. 《Android插件化开发指南》面世

    本书在京东购买地址:https://item.jd.com/31178047689.html 本书Q群:389329264 (一)这是一本什么书 如果只把本书当作纯粹介绍Android插件化技术的书籍 ...

  9. 又一开源项目爆火于GitHub,Android高级插件化强化实战

    一.插件化起源 插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主. 想必大家都知道,在 Android 系统中,应用是以 Ap ...

随机推荐

  1. resharper激活

    1.解压后点击64位系统的IntelliJIDEALicenseServer_windows_amd64.exe      32位点击IntelliJIDEALicenseServer_windows ...

  2. stack,heap的区别

    一个由C/C++编译的程序占用的内存分为以下几个部分    1.栈区(stack)—   由编译器自动分配释放   ,存放函数的参数值,局部变量的值等.其    操作方式类似于数据结构中的栈.    ...

  3. 深入解析VueJs中的V-bind指令

    v-bind 主要用于属性绑定,比方你的class属性,style属性,value属性,href属性等等,只要是属性,就可以用v-bind指令进行绑定.这次主要介绍了VueJs中的V-bind指令,需 ...

  4. Manacher算法学习 【马拉车】

    好久没写算法学习博客了 比较懒,一直在刷水题 今天学一个用于回文串计算问题manacher算法[马拉车] 回文串 回文串:指的是以字符串中心为轴,两边字符关于该轴对称的字符串 ——例如abaaba 最 ...

  5. JavaScript—获取本地时间以12小时制显示

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  6. topcoder(BinaryCode)

    Problem Statement      Let's say you have a binary string such as the following: 011100011 One way t ...

  7. TCP面试题之三次握手过程

    TCP简介: 1.面向连接的.可靠的.基于字节流的传输层的通信协议: 2.将应用层的数据流分割成报文段并发送给目标节点的TCP层: 3.数据包都有序号,对方收到则发送ACK确认,未收到则重传: 4.使 ...

  8. #!/bin/sh简介

    第一次学shell编程,看的文章中说shell程序必须以"#!/bin/sh"开始,也就认为是这样了,虽然知道在shell中以"#"开始的语句都是注释,但也从没 ...

  9. display:inline、block、inline-block的区别 摘】

    display:block就是将元素显示为块级元素. block元素的特点是: 总是在新行上开始: 高度,行高以及顶和底边距都可控制: 宽度缺省是它的容器的100%,除非设定一个宽度 <div& ...

  10. appium+python自动化24-滑动方法封装(swipe)【转载】

    swipe介绍 1.查看源码语法,起点和终点四个坐标参数,duration是滑动屏幕持续的时间,时间越短速度越快.默认为None可不填,一般设置500-1000毫秒比较合适. swipe(self, ...