-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
support multidex feature in plugin application for the ROM below LOLLIPOP #264
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
非常感谢Osan兄的给力提交!
MultiDex的支持本身会比较繁琐,能写出这么多核心代码着实不易。这里有几个我认为要修改的点,可能辛苦兄弟多看下了。
@@ -53,4 +54,12 @@ dependencies { | |||
provided 'com.android.support:support-v4:25.2.0' | |||
} | |||
|
|||
publishing { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
我们经过试验,publishing组和bintray是有冲突的,建议先注释掉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议先回滚此改动(指publishing块)。我这边会提上去“可用的”玩法
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
已回滚
|
||
try { | ||
// get dexElements of main dex | ||
Class<?> clz = Class.forName("dalvik.system.BaseDexClassLoader"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
因为方法内涉及到Hook点了(DexPathList),所以从原则上可尽量去掉对此的引用,否则一旦DexPathList做了修改(例如4.4以下的个别ROM做了什么改动),则可能会导致加载失败的问题。
例如,能不能采用new出多个DexClassLoader,并加到List中,然后在使用时依次做findClass的处理呢?
因为DexPathList在findClass阶段,本质上也就是去轮询读取DexFile里的findClass,在性能上确实是OK的。
以下是“伪代码”,具体用法欢迎QQ私信联系
// 初始化
DexClassLoader[] dexes = ...
// 使用(以下为伪代码)
for (DexClassLoader d : dexes) {
Class clz = cl.findClass();
if (clz == null) {
continue;
}
...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
多个DexClassLoader依次loadclass,目前做过多种解决思路尝试,最终有会遇到2个问题:
1.主DexClassLoader和其他DexClassLoader无双亲委派关系时,会在跨dex加载2个类A和B,并这2个类关系为内部类接口实现,且A类和B类分别位于不同dex时,dalvik无法resloving interface关系。除了这个特殊情况,其他情况已验证均可正常加载,demo1 multidex时已测试pass,但是demo2 multidex测试时,发现这个特殊情况会失败。
2.主DexClassLoader和其他DexClassLoader有双亲委派关系时,会在跨dex加载其他dex的类时,遇到Class ref in pre-verified class resolved to unexpected implementation异常。
以上2个问题尝试过一些办法去解决,暂时未成。
目前在方法上加注了@deprecated,限定为仅5.0以下api,且可能废弃。
后续继续尝试是否有其他解决方案。
* @param optimizedDirectory | ||
* @param parent | ||
*/ | ||
private void installExtraDexes(String dexPath, String optimizedDirectory, ClassLoader parent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
因为这个方法主要是针对4.4及以下,建议将方法名改为:installMultiDexesBeforeLollipop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
已更名
allElements.add(mainElements); | ||
|
||
// get paths of dex | ||
List<File> dexFiles = getDexFiles(dexPath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
可以先做下DexFile的判断,如果只有一个Dex,则和过去完全一样,否则才走后面的逻辑,这样尽可能减少影响面。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
已判断
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
因为还要考虑到和之前版本的兼容问题,故建议这么判断:
List<File> dexFiles = getDexFiles(dexPath);
if (dexFiles != null) {
if (dexFiles.size() > 1) {
// 不止一处Dex,证明适用于MultiDex的情况,做后面的MultiDex支持工作
...
return true;
} else if (dexFiles.size() == 1) {
// 只有一个Dex,按原来逻辑处理
...
return false;
}
}
// 其它异常情况,抛出错误日志,然后直接返回
return false;
List<File> files = null; | ||
try { | ||
zipFile = new ZipFile(dexPath); | ||
files = traverseZipFile(dexPath.substring(0, dexPath.lastIndexOf("/")), zipFile); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这块儿可能有个问题:目前所有的Dex都放在app_p_od下(可以看下手机中存的dex文件即可了解)。如果“直接释放”,则一旦有超过2个及以上的MultiDex插件,则可能会出现classes2.dex覆盖的问题。
建议创建目录,说白了就是dexPath的文件名后面加个_md后缀,这样可做有效区分。
同时还应注意,需要在PluginManagerServer和PluginInfo中,对这种情况做“兼容处理”,否则一旦插件卸载,则可能导致目录无法被清除。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
已针对多dex多插件场景作了覆盖;已针对卸载升级等做了"兼容处理"
对了,我刚才突然想到一点,也建议改下,关于Sample上的。 目前Demo1上的东西有些多,而MultiDex本身相对独立,建议挪到Demo2中进行展示,这样避免“头重脚轻”,而且顺带着可以演示下从Demo1进入带MultiDex的Demo2的真实效果 |
另:MultiDex test case 已移至demo2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
感谢Osan!现在代码比以前更完善了。
还有几个细节,辛苦再看下哈。
files = traverseZipFile(dexPath.substring(0, dexPath.lastIndexOf("/")), zipFile); | ||
} catch (IOException e) { | ||
|
||
String installedFileName = dexPath.substring(dexPath.lastIndexOf(File.separator) + 1).replace(".jar", ""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这块儿其实有个更好的方法能拿到APK的路径,那就是利用PluginInfo.getApkFile() 方法。
而PluginInfo的获取可以这么做:
String pn = RePlugin.fetchPluginNameByClassLoader(this);
PluginInfo pi = RePlugin.getPluginInfo(pn);
...
(上面只是最简单的流程代码,具体还得做下缓存、判空等处理)
PS:这块儿当时我在设计时,确实没想到需要用到PluginInfo(觉得用不到)。早知道当时就通过构造函数,直接让外面传进来得了 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
经过测试,这里通过自身的classloader获取不到插件名。
因此这里我重构了构造函数,增加了PluginInfo参数,不想用我之前那种妥协的轮询查插件信息方式。
* | ||
* @return 优化前Dex所在目录的File对象 | ||
*/ | ||
public File getUnoptDexParentDir() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
目前该方法的内容为:无论是否支持MultiDex,其获取的都是添加了“_md”后的目录,然而,非MultiDex插件又不会用到这个方法。所以在方法名上,叫“getUnoptDex...”则会让人误以为,无论是否支持MultiDex都获取的是统一的目录。
建议将方法名改为:getMultiDexParentDir,或者直接将此方法放入PluginDexClassLoader中(毕竟只有一处在用)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里包括后面您提到的关于multidex,main dex,extra dex的revivew点,我做了一次整体的调整。
1.这里获取的dex更确切的说是extra dex,即除main dex以外的dex,因此叫getExtraDexParentDir更为确切,包括后续相关方法定义,均沿用此思路。
2.dex放置目录上,区别为_ed目录和_od目录,分别用于放置优化前的extra dex(因为main dex由父dexclassloader直接去释放到优化后的目录,不需要RP框架释放,因此定义为extra dex目录)和优化后的optimized dex(包括main odex 和 extra odex)。这里从定义的层面不再去特别关心是否为multidex场景,从而避免了在非multidex场景时目录名仍包含_"m"d的误解。
3.在获取extra dex列表时,如果不存在extra dex,则不会去创建extra dex的_ed目录。
|
||
File subDir = new File(dir + File.separator + makeInstalledFileName() + Constant.LOCAL_PLUGIN_MULTI_ODEX_SUB_DIR); | ||
if (!subDir.exists()) { | ||
subDir.mkdir(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
理由同getUnoptDexParentDir
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
已重构
|
||
File subDir = new File(dir + File.separator + makeInstalledFileName() + Constant.LOCAL_PLUGIN_MULTI_DEX_SUB_DIR); | ||
if (!subDir.exists()) { | ||
subDir.mkdir(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
通常这类get方法不建议再做一次mkdir操作,容易和方法名所表示的含义冲突。一般叫“getXXX”方法都是从内存中获取记录(可以做简单运算),而从网络、文件中获取内容大部分用fetch。而会“创建目录”的情况下,叫“make”更合适。
建议将mkdir的操作放在方法外。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
已重构,采用的类似context.getDir方式,单独重构了一个getDexDir方法出来,Retrieve dir, or creating if needed
allElements.add(mainElements); | ||
|
||
// get paths of dex | ||
List<File> dexFiles = getDexFiles(dexPath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
因为还要考虑到和之前版本的兼容问题,故建议这么判断:
List<File> dexFiles = getDexFiles(dexPath);
if (dexFiles != null) {
if (dexFiles.size() > 1) {
// 不止一处Dex,证明适用于MultiDex的情况,做后面的MultiDex支持工作
...
return true;
} else if (dexFiles.size() == 1) {
// 只有一个Dex,按原来逻辑处理
...
return false;
}
}
// 其它异常情况,抛出错误日志,然后直接返回
return false;
// 必须使用宿主的Context对象,防止出现“目录定位到插件内”的问题 | ||
Context context = RePluginInternal.getAppContext(); | ||
if (isPnPlugin()) { | ||
return context.getDir(Constant.LOCAL_PLUGIN_ODEX_SUB_DIR, 0); | ||
dir = context.getDir(Constant.LOCAL_PLUGIN_ODEX_SUB_DIR, 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议可以联合上面的方法,再抽取一个共用方法出来(但为private,不能让外界访问)。只单纯的获取Dex目录即可(也即,和早期的getDexParentFile一样)。比如可以叫 getPureDexParentDir() 之类的(先临时这么叫,反正将来随时可以改)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
已重构
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
我看了下,这部分代码会将P-N(一种旧的卫士插件管理方案)的目录给修改了(原来是”app_plugins_v3_odex“,现在是”app_plugins_v3_odex_ed/od),可能会导致出现旧的P-n插件会重复释放odex。总体而言,我觉得这块儿的改动还是过大了。
我的想法是,尽可能减少对过去代码的入侵,要确保在 5.0以上,或者没有做MultiDex的插件,仍走过去一样的逻辑,不受影响。只有在判断是需要做MultiDex后,再创建相应的目录。
因为和这部分代码不是很搭,所以我把具体描述写到了外面的,可参考下。
|
||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { | ||
|
||
File subDir = new File(dir + File.separator + makeInstalledFileName() + Constant.LOCAL_PLUGIN_MULTI_ODEX_SUB_DIR); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这块儿写的非常赞,先判断Android 5.0以上,然后再判断是否有带MultiDex字样的目录,有就返回新的,没有就 返回旧的👍
* @param zipFile | ||
* @return the File list of the extra dexes | ||
*/ | ||
private static List<File> traverseZipFile(String dir, ZipFile zipFile) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
如刚才私信沟通,建议改成“traverseExtraDex”,毕竟里面是对classes.dex做了排除的,怕会有误会
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
所见略同:),已重构。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
非常感谢,还有些细节调整
// 必须使用宿主的Context对象,防止出现“目录定位到插件内”的问题 | ||
Context context = RePluginInternal.getAppContext(); | ||
if (isPnPlugin()) { | ||
return context.getDir(Constant.LOCAL_PLUGIN_ODEX_SUB_DIR, 0); | ||
dir = context.getDir(Constant.LOCAL_PLUGIN_ODEX_SUB_DIR, 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
我看了下,这部分代码会将P-N(一种旧的卫士插件管理方案)的目录给修改了(原来是”app_plugins_v3_odex“,现在是”app_plugins_v3_odex_ed/od),可能会导致出现旧的P-n插件会重复释放odex。总体而言,我觉得这块儿的改动还是过大了。
我的想法是,尽可能减少对过去代码的入侵,要确保在 5.0以上,或者没有做MultiDex的插件,仍走过去一样的逻辑,不受影响。只有在判断是需要做MultiDex后,再创建相应的目录。
因为和这部分代码不是很搭,所以我把具体描述写到了外面的,可参考下。
我看了下,最新的提交会将P-N(一种旧的卫士插件管理方案)的目录给修改了(原来是”app_plugins_v3_odex“,现在是”app_plugins_v3_odex_ed/od),可能会导致出现旧的P-n插件会重复释放odex。总体而言,我觉得这块儿的改动还是过大了。 我的想法是,尽可能减少对过去代码的入侵,要确保在 5.0以上,或者没有做MultiDex的插件,仍走过去一样的逻辑,不受影响。只有在判断是需要做MultiDex后,再创建相应的目录。 再具体一些,如果这个插件“不需要支持”MultiDex,则其Dex的目录为:
等于说,和过去完全一样,保持不变。 如果需要支持MultiDex,则为:
若老插件不支持MultiDex,但新插件又支持,则:
反之亦然。 |
从架构设计角度来看,虽然项目迭代在所难免,但很多时候,越少的侵入性修改,也就意味着越能够减少不稳定因素。 关于这次MultiDex上,我觉得核心点是针对“Android 5.0以下,且该插件是MultiDex”的去做处理,所以我们应该专注于针对这块儿来做支持。 如咱俩私信沟通,建议: 关于PluginInfo:
在PluginDexClassLoader里:
注意原来的像isDexExtracted这类方法,原来只判断了getDexFile的情况,现在还应再判断下getMultiDexDir。当然这个MultiDexDir在注释里也写下“仅适用于4.4及以下”,避免未来有歧义 仅供参考~ 另外给个大大的👍 ! |
另:附dex场景测试case结果
|
这么详细的测试场景描述,真的是太用心了。辛苦 @wangfuda ! 明天我再看下,OK后就Merge到Dev上。 👍 |
移除了move方法内对extra dex的move操作,缘由:
另实测,在同版本升级时,因为此刻新插件尚未释放,导致PluginManagerServer.move方法内,有两处代码会报异常:
|
如果“待更新插件”(无论是否为同版本覆盖)在做了install之后,用其“返回值”来作为RePlugin.preload的参数并被调用,那么这个新插件的Dex等会被释放出来,这时当进程重启后,其updateNow方法会将之前preload出来的dex等移动到正确的位置。 所以对Dex、MultiDex的Move我认为是有必要的。
发现的很好,这确实是一个改进点,虽然对最终结果无影响(毕竟Dex不在的话,NativeLib也一定不会在),但老是出来个Error也挺讨厌的。辛苦改下~ |
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
最后阶段了,还有几个小问题,辛苦修改下。
} | ||
|
||
// get dexElements of extra dex (need to load dex first) | ||
String optimizedDirectory = pi.getExtraOdexDir().getAbsolutePath(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里应该在循环外获取,毕竟每次获取的值是一样的。
String optimizedDirectory = pi.getExtraOdexDir().getAbsolutePath(); | ||
DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath(), optimizedDirectory, optimizedDirectory, parent); | ||
// delete extra dex, after optimized | ||
FileUtils.forceDelete(pi.getExtraDexDir()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
如果有三个及以上的Dex,直接删除ExtraDir会不会导致第三个Dex无法被优化?
从流程上看:
- 第一个Dex在前面已经被释放了,不再赘述
- 第二个Dex在上面那行被释放了,然后在这里删除了整个目录,进入下一步
- 第三个Dex在new DexClassLoader时,发现由于之前已把目录全删掉了,所以无法再找到这个Dex,出现问题
已修改为全部dex优化释放后,再清理extra dex目录。 另:附dex场景测试case结果
测试项
背景条件
测试步骤
测试结果
|
这份PR可以作为范本。尤其是你写的测试场景的验证,非常的全面!辛苦 @wangfuda ! 我看了下,这次改完后应该没什么问题了,这就Merge。 |
Summary
增加特性支持:
1.增加RP框架对运行在android 5.0以下版本的插件(含多dex)的multidex支持
2.demo1中增加multidex support特性测试用例,默认关闭
Description
1.在android 5.0以下,动态解压插件应用并加载除主dex以外的所有dex。解压后并未删除这些dex,空间与时间的权衡,不希望删除文件带来性能损耗和时间上的增加,虽然只是一点点(还请评估,如果希望避免空间浪费,可以增加删除解压后dex文件逻辑)
2.在android 5.0及5.0以上,不做处理。
3.已针对内置插件和外置插件在android 5.0以上及4.4.2设备上做过多次压力测试,测试用例pass。
4.对宿主lib库的gradle做了修改,增加便于发布lib库至mavenLocal的代码,便于本地调试。