IBigerBiger的成长之路

插件化实现(一)插件管理器

写在前面的话

对于插件化,这个在2016年相对来说比较热门的话题,确实在中国Android开发的圈子中引起了不小的波澜,插件化其实主要是针对于相对来说比较大的超级App这种性质的应用开发,因为这种超级App一般会面临几个大的问题:

(1)65535方法数问题,当然这个谷歌也是提供了方案,但是这样的解决方式其实还是有问题,第一随着业务的增长,很有可能第二个Dex也不够用了,那么继续分包?这样其实问题会越来越严重,第二Dex更多其实就是代表我们项目的方法数更多,自然编译的速度就会缓慢很多,1~2min的编译时间绝对不是开玩笑的。

(2)随着业务与功能的增长,很有可能出现的情况就是一个大的项目组会分成好几个功能模块的小项目组,那么当新出一个版本则要协调各个功能模块组,确定一个共同满意的上线时间,肯定会出现各个项目组延期导致的整个项目延期的情况,那么对于现在的敏捷开发来说,这样的开发模式明显是不符合的。

(3)App Size过大,对于一个APP来说Size过大其实是一个很严重的问题,这会直接影响到用户是否愿意去下载的问题,当然一个传统的超级App的Size肯定是不言而喻的。

所以插件化就应运而生了,插件化自然就可以解决上面所述的问题了

其实现在已经有不少插件化的库存在了,如下

  • DroidPlugin
    是360手机助手在Android系统上实现了一种新的插件机制

  • Android-Plugin-Framework
    此项目是Android插件开发框架完整源码及示例。用来通过动态加载的方式在宿主程序中运行插件APK。

  • Small
    世界那么大,组件那么小。Small,做最轻巧的跨平台插件化框架。里面有很详细的文档

  • dynamic-load-apk
    Android 使用动态加载框架DL进行插件化开发

  • AndroidDynamicLoader
    Android 动态加载框架,他不是用代理 Activity 的方式实现而是用 Fragment 以及 Schema 的方式实现

  • DynamicAPK
    实现Android App多apk插件化和动态加载,支持资源分包和热修复.携程App的插件化和动态加载框架.

  • ACDD
    非代理Android动态部署框架

  • android-pluginmgr
    不需要插件规范的apk动态加载框架

其中一些插件化库其实已经在一些产品上面经过实践了

虽然插件化看起来确实是对于超级App一种很友好的方式,但是其实也是有一定的问题的,其中最大的问题就是兼容性的问题,先不说谷歌众多的Android版本问题,国内各大厂商的个(heng)性(keng)化(die)的ROM修改,造成很多和源码不一致的地方,这样就需要插件化库要做很多兼容性的工作,同时也要及时处理各种版本ROM更新的问题,所以其实我觉得如果不是有一定插件化开发经验的开发者还是不要轻易使用插件化的库,这样有可能出现了各种问题反馈也得不到及时的回复。

对于插件化开发

当然首先可以确定的是插件化的实现是很复杂且繁琐的过程,我写这系列其实也是一些相对来说我们可以看到的实现,并不会去做特别深入,特别多的研究。

插件化的开发需要开发者对于系统源代码,HOOK相关知识都有一定的要求,所以这系列文章会对于系统相关的源码进行讲解,同时也可以加深对于HOOK相关的知识。

针对于插件化开发,我这里的系列文章主要是实现下面的功能

  • 插件管理器
  • 插件资源获取
  • 插件四大组件的加载

那么这篇就是对于一个插件化库的插件管理器的开发

插件管理器

对于一个插件化库来说插件管理器是承接宿主App与插件App的连接器,通过插件管理器我们要实现对于插件App的添加删除更新等功能。

首先对于一个需要被添加的插件Apk,那么他的存在一定是被我们预先下载到SD卡中,我们当然可以选择就用这个位置的Apk为插件App,但是会存在的问题就是如果这个位置的Apk删除了,那么就无法后面正常的使用插件App中的功能,所以一般情况是要放到一个相对安全的位置。

Application内部存储路径为/data/data/youPackageName/,所有内部存储中保存的文件在用户卸载应用的时候会被删除。所以这个位置存储我们的插件Apk是最好的选择。

存储位置确定了,那么接下来要做的事情就是怎么建立这个Apk与我们宿主之间的联系?

我们知道系统判断一个apk是否安装是根据这个apk的packageName,如果安装了当versionCode不一致的时候则会判断是否需要更新,所以我们也可以根据这个特性来进行Apk与我们宿主之间的联系。所以就是当我们知道了packageName就可以快速的找到这个apk存在的位置。

所以在我们存储这个插件Apk到内部存储路径的时候,可以用packageName与versionCode作为Apk的父文件夹,其路径如下所致

/data/data/youPackageName/plugin/packageName/versionCode/.apk

那么接下来存在的问题就是怎么去获取插件Apk中的packageName与versionCode了

获取packageName与versionCode代码如下

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
public static PluginInfo parse(File apk) throws Exception{
PluginInfo pluginInfo = new PluginInfo();
ZipFile zipFile = new ZipFile(apk,ZipFile.OPEN_READ);
ZipEntry manifestXmlEntry = zipFile.getEntry(ManifestReader.DEFAULT_XML);
String manifestXml = ManifestReader.getManifestXMLFromAPK(zipFile, manifestXmlEntry);
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
XmlPullParser parser = factory.newPullParser();
parser.setInput(new StringReader(manifestXml));
int eventType = parser.getEventType();
do {
switch (eventType) {
case XmlPullParser.START_DOCUMENT:
break;
case XmlPullParser.START_TAG: {
String tag = parser.getName();
if ("manifest".equals(tag)) {
String namespace = parser.getNamespace("android");
pluginInfo.setPackageName(parser.getAttributeValue(null, "package"));
pluginInfo.setVersionCode(parser.getAttributeValue(namespace, "versionCode"));
pluginInfo.setVersionName(parser.getAttributeValue(namespace, "versionName"));
} else if ("meta-data".equals(tag)) {
} else if ("exported-fragment".equals(tag)) {
} else if ("exported-service".equals(tag)) {
} else if ("uses-library".equals(tag)) {
} else if ("application".equals(tag)) {
} else if ("activity".equals(tag)) {
} else if ("receiver".equals(tag)) {
} else if ("service".equals(tag)) {
} else if ("provider".equals(tag)) {
}
break;
}
case XmlPullParser.END_TAG: {
break;
}
}
eventType = parser.next();
} while (eventType != XmlPullParser.END_DOCUMENT);
return pluginInfo;
}

当然除了上述的方式以外还有采用android系统方法的方式,由于这里暂时只需要packageName与versionCode,那么用系统方法的方式在后面再说。

所以我们解析出Apk相关信息后就可以通过这些信息去判断是否已经安装过这个插件Apk了,那么接下来看一下判断插件是否安装的方法

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
/**
* 安装插件
*
* @String filePath
*/
public void installPlugin(String filePath) throws Exception {
File file = new File(filePath);
if (!file.isFile() || !file.exists() || file.length() == 0){
throw new FileNotFoundException(filePath);
}
PluginInfo pluginInfo = ManifestUtil.parse(file);
switch (checkInstallType(pluginInfo)){
case TYPE_UNINSTALLED:
PluginInstallManager.install(file,pluginInfo);
break;
case TYPE_INSTALLED:
throw new Exception("Plugin has been already installed");
case TYPE_UPDATE:
break;
case TYPE_OLDERVERSION:
throw new Exception("Plugin version is too old");
}
}
/**
* 判断插件安装状态
*
* @param pluginInfo
* @return
*/
private INSTALL_TYPE checkInstallType(PluginInfo pluginInfo){
if (FileUtil.getPatchTempDirectory(mContext).exists()){
if (FileUtil.getPatchNameDirectory(mContext,pluginInfo).exists()){
if (FileUtil.getPatchNameVersonDirectory(mContext,pluginInfo).exists()){
return INSTALL_TYPE.TYPE_INSTALLED;
}else {
File file = FileUtil.getChildDirectory(FileUtil.getPatchNameDirectory(mContext,pluginInfo))[0];
if (file.exists()){
Long oldVersion = Long.parseLong(file.getName());
Long newVersion = Long.parseLong(pluginInfo.getVersionCode());
if (newVersion > oldVersion){
return INSTALL_TYPE.TYPE_UPDATE;
}else {
return INSTALL_TYPE.TYPE_OLDERVERSION;
}
}else {
return INSTALL_TYPE.TYPE_UNINSTALLED;
}
}
}else {
return INSTALL_TYPE.TYPE_UNINSTALLED;
}
}else {
return INSTALL_TYPE.TYPE_UNINSTALLED;
}
}

这里其实就是先判断插件路径是否存在,不存在返回未安装,存在情况判断需要安装apk的包名路径是否存在,不存在返回未安装,如果存在通过需要安装apk的版本号与存在路径包名版本号进行对比,相同则返回已安装,不同则返回需要更新或者版本太老。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 安装插件
*
* @param apkFile
* @param pluginInfo
* @throws Exception
*/
public static void install(File apkFile,PluginInfo pluginInfo) throws Exception {
File targetFile = new File(FileUtil.getPatchNameVersonDirectory(mContext,pluginInfo),DEFAULT_PLUGIN_NAME);
if (targetFile.exists()){
throw new Exception("Plugin has been already installed");
}
if (apkFile.renameTo(targetFile))
return;
FileUtil.copyFile(apkFile,targetFile);
}

安装部分代码如下,其实就是根据包名与版本号创建指定路径的文件将需要安装的apk拷贝到指定的位置

当然为了后面的需求,这里还提供了一些其他的方法,根据包名去获取文件,获取Apk相关的信息
如下

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
/**
* 获取指定插件的插件信息
*
* @param packageName
* @return
*/
public static PluginInfo getTargetPluginInfo(String packageName) throws Exception {
File targetFile = getTargetPluginFile(packageName);
PluginInfo pluginInfo = ManifestUtil.parse(targetFile);
return pluginInfo;
}
/**
* 获取指定插件的文件
*
* @param packageName
* @return
*/
public static File getTargetPluginFile(String packageName){
File[] pluginversion =FileUtil.getChildDirectory(new File(FileUtil.getPatchTempDirectory(mContext),packageName));
if (pluginversion == null || pluginversion.length < 0)
return null;
File pluginFile = new File(pluginversion[0],DEFAULT_PLUGIN_NAME);
if (!pluginFile.exists())
return null;
return pluginFile;
}

那么到这里基本上简单的插件管理器就实现了,当然后续会有更多的需求的话,根据相关需求更新就好了

写在后面的话

对于插件化来说插件管理器只是一个基础的作用,和插件化其实并没有特别多的关系,那么下一篇文章,就要开启插件化学习的真正篇章了,peace~~~