Android插件化入门
插件化是什么
用通俗易懂的话就是,它就像我们的U盘,可以机时的插在个电脑。不单单的U盘,还有显示器,显卡和CPU等电脑配件都是可以插入主板提供的接口,这些电脑组件通过主板提示的接口组合在一起就可以组成一部具有完整功能的电脑。
即然电脑可以以这种形式进行组装,哪我们android程序是不是也可以这样?答案是肯定的,我们的各个独立的功能模块都可以打包成apk,让宿主程序把apk加载进来,再运行里面的各个activity,service等
插件化的分类
插件化在技术难度上可以为分两种:独立插件化和非独立插件
-
非独立插件是宿主程序与插件发开约定好插件开发规则,插件开发者要遵循这个规划进行开发,这种方式要求的技术难度相对来说低很多,但是增加了开发者的调用成本。类似比较成熟的解新局面方案有Small,需要说明的是small支持Android和iOS两个平台
-
独立插件完全支持android的兼容四大组件的大部份的属性,这种方式对于开发者可以说是完全透明,是十分完善的插件化解决方案。但是偏写这类框架需要对android底层的代码非常熟悉,要对种种api进行hook处理,所以技术要求也非常的高。类似比较成熟的方案有DroidPlugin等等
插件化优缺点
-
优点
-
模块间的解耦
-
解除单个dex方法65535的限制
-
动态更新,使我们的运营更加的灵活
-
-
缺点
-
增加了程序开发的复杂度
-
技术门槛更高
-
非独立插件原理
因为我们下载的apk里面activity没有在宿入的manifest.xml注册,如果我们直接调用startActivity方法,就会报activity没有注册的异常。我们可以在宿主activity中先注册一个代理Activity,然后通过宿主activity去调用插件里面的activity的方法。
哪我们怎样去加载我们插件apk里面的类呢?下面让我们了解下java里面几个类加载器:
-
DexClassLoader :可以加载文件系统上的jar、dex、apk
-
PathClassLoader :可以加载/data/app目录下的apk,这也意味着,它只能加载已经安装的apk
-
URLClassLoader :可以加载java中的jar,但是由于dalvik不能直接识别jar,所以此方法在android中无法使用,尽管还有这个类
由上面的解释可以了解到我们可以通过DexClassLoader把插件apk里面的类加载出来让我们使用。
非独立插件实现
首先我们先摸拟apk下载的流程,把assets目录下面的apk下载下来,存放在缓存目录中,但是下载下来我们还不能直接使用,我们还要对apk包进行以下处理
解密我们下载下来的apk包
校验apk的签名是否正确
校验apk包所需的权限是否在主包中都包函
File mApkPath = this.getDir(ProxyActivity.APK_PATH, MODE_PRIVATE);
try {
String mApkName = "myapplication-release-unsigned.apk";
InputStream inputStream = getResources().getAssets().open(mApkName);
byte[] datas = FileUtil.readFromInputStream(inputStream);
String fullPath = mApkPath.getAbsolutePath() + File.separator + mApkName;
FileUtil.writeByteToFile(datas, new File(fullPath));
} catch (IOException e) {
e.printStackTrace();
}
然后我们就启动插件,但是由于非独立插件开发的时,宿主程序对插件模块是没有用引的,显示启动启件的方式显然是行不通的,所以只能通过隐式调用,具体调用如下。
Intent intent = new Intent(this, ProxyActivity.class);
intent.putExtra(ProxyActivity.APK_FILE_PATH, "myapplication-release-unsigned.apk");
intent.putExtra(ProxyActivity.ACTIVITY_NAME, "com.sundar.myapplication.LoginActivity");
startActivity(intent);
细心的读者或者己经发现,上面即然提到我们宿主程对插件是没用引用的,那为什么intent创建时我们会带上ProxyActivity的类呢?
其实我们是通过代理Activity来对插件apk进行加载,然后通过宿主ProxyActivity来实现插件的生命周期调用,下面给出实现代码
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
try {
Intent intent = getIntent();
mApkName = intent.getStringExtra(APK_FILE_PATH);
mActivityName = intent.getStringExtra(ACTIVITY_NAME);
} catch (Exception ignore) {
//
}
// 创建相关路径
createFile();
// 把当前的apk放到资源查找目录中
mCustomAssetManager = new CustomAssetManager();
mCustomAssetManager.addAssetPath(fullPath);
// 创建classLoader加载类
// dex解压释放后的目录
File dexOutputDir = getDir(DEX_OP, 0);
// apk存放的路径
String fullPath = mApkPath.getAbsolutePath() + File.separator + mApkName;
// 定义DexClassLoader
// 第一个参数:是dex压缩文件的路径
// 第二个参数:是dex解压缩后存放的目录
// 第三个参数:是C/C++依赖的本地库文件目录,可以为null
// 第四个参数:是上一级的类加载器
mDexClassLoader = new DexClassLoader(fullPath, dexOutputDir.getAbsolutePath(), mSoPath.getAbsolutePath(), getClassLoader());
// 通过代理的方法去生成Activity类
try {
Class<PluginActivity> pluginActivityClass = (Class<PluginActivity>) mDexClassLoader.loadClass(mActivityName);
mPluginActivity = pluginActivityClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
// 加载类错误,需要显示错误信息给用户
// 此类是插件activity
if (mPluginActivity == null) {
return;
}
//进行必要的初始化
mPluginActivity.setmBaseActivity(this);
// 实现对插件activity方法进行调用
mPluginActivity.onCreate(savedInstanceState);
}
下面是创建插件Activity所需要的AssetsAssetManager用于加载插件资源的
public CustomAssetManager() {
try {
this.mAssetManager = AssetManager.class.newInstance();
this.mAddedAssetsPath = new HashMap<>();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 添加apk的资源路径放到AssetManager里面
*/
public void addAssetPath(String apkPath) {
if (mAssetManager == null) {
return;
}
//先判断Map里面有没有己经添加的资源
if (mAddedAssetsPath.containsKey(apkPath)) {
return;
}
try {
AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(
mAssetManager, apkPath);
mAddedAssetsPath.put(apkPath, apkPath);
} catch (Exception e) {
e.printStackTrace();
}
}
下面是我们插件Activity类的主要方法,大家或许会觉得好寄为什么要重写activity的方法呢?
答案是因为我们如果不重写直接设置就会报找不到相应的资源的异常。那我们是不是必须要重写这个方法呢?其实不是,我们可以把我们的插件apk添加到宿主apk的assetsManager里面,但是这里还会涉到到资源冲突的问题,具体解决资源冲突的方式可以自行百度
@Override
public void setContentView(@LayoutRes int layoutResID) {
Resources resources = mBaseActivity.getmCustomAssetManager().getBundleResource(mBaseActivity);
XmlResourceParser xmlResourceParser = resources.getLayout(layoutResID);
View view = LayoutInflater.from(mBaseActivity).inflate(xmlResourceParser, null);
mBaseActivity.setContentView(view);
}
下面就是我们的效果图
6037478-09ae3fe1efed2f4d.jpg 472BD804-6650-49DB-9036-45D844A50881.png非独立插件实现总结
以上就是非独立插件的实现过程,但是上面只是做了实现的原理,在做demo过程中,我遇到几个坑
-
因为android是通过AssetsManager去加载资源的,此时如果在配置文件中使用资源id去引用资源,系统则会抛出找不到资源的异常,而我们现在只能自己创建AssetsManager去获取apk包的资源
-
从上面的代码可以知道,插件的Activity的生成周期的调用只能通过代理Activity方法去调用
-
插件模块开发增加难度,插件开发者必须要尊守开发的规则
后续要做的事情
是否可以参考动态换肤的机制来实现对插件的资源进行加载,这样我们就可以直接使用配置文件里的资源,具体可以参考http://www.jianshu.com/p/af7c0585dd5b
网友评论