关于 Android中的插件化开发,dex分包,热修复(Tinker)的思考(一)
2017-03-17 16:56
555 查看
插件化开发
优点:一. 来可以将自己的应用分拆,某些功能可以在插件中实现,用到时再进行下载,而且不用安装. 如果有新功能的添加,不需要更新应用,只要预留插件管理,我们就可以通过添加插件的方式,动态更新自己的应用,该功能需要改进或扩展,更新插件即可,无需频繁安装或卸载(容易造成用户反感).
二. 对应同系应用,正常的引流方式只能引导用户进行新应用的下载和安装,如果使用插件化开发,则无需安装应用,关闭插件功能也十分方便,省去应用安装和卸载的过程,可以实现无缝引流.
三.避开65536个方法限制,模块化开发便于维护。
基本实现原理:
1.使用dexClassLoader加载未安装的APK。
2.代理模式之通过代理的Activity执行APK中的Activity,加载对应生命周期。
3.资源管理,通过反射调用AssetManager中的addAssetPath()方法获取插件中的Resource.
插件化开发简单的说就是DexClassLoader在程序A里面动态装载程序B中的类,并且来调用B程序中的方法.
插件化开发库:
https://github.com/Qihoo360/DroidPlugin/blob/master/readme_cn.md
https://github.com/houkx/android-pluginmgr
https://github.com/singwhatiwanna/dynamic-load-apk
dex分包
当一个app的功能越来越复杂,代码量越来越多,也许有一天便会突然遇到下列现象:生成的apk在2.3以前的机器无法安装,提示INSTALL_FAILED_DEXOPT
方法数量过多,编译时出错,提示:
java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536
出现这种问题的原因是:
Android2.3及以前版本用来执行dexopt(用于优化dex文件)的内存只分配了5M
一个dex文件最多只支持65536个方法。
针对上述问题,也出现了诸多解决方案,使用的最多的是插件化,即将一些独立的功能做成一个单独的apk,当打开的时候使用DexClassLoader动态加载,然后使用反射机制来调用插件中的类和方法。这固然是一种解决问题的方案:但这种方案存在着以下两个问题:
插件化只适合一些比较独立的模块;
必须通过反射机制去调用插件的类和方法,因此,必须搭配一套插件框架来配合使用;
由于上述问题的存在,通过不断研究,便有了dex分包的解决方案。简单来说,其原理是
将编译好的class文件拆分打包成多个dex,绕过dex方法数量的限制以及安装时的检查,在运行时再动态用DexClassLoader加载第二个dex文件中。
目前谷歌提供了multidex分包技术。后续分析
热修复(tinker)
Tinker是什么Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。
原理:
一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。
理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类
那么这样的话,我们可以在这个dexElements中去做一些事情,比如,在这个数组的第一个元素放置我们的patch.jar,里面包含修复过的类,这样的话,当遍历findClass的时候,我们修复的类就会被查找到,从而替代有bug的类。
说到这,你应该已经明白了,SO!原来热修复原理这么简单。
看到这里是不是发现插件化开发,dex分包,热修复都用到了DexClassLoader。那么DexClassLoader又是什么?
DexClassLoader
构造函数:DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
dexPath:被解压的apk路径,不能为空。
optimizedDirectory:解压后的.dex文件的存储路径,不能为空。这个路径强烈建议使用应用程序的私有路径,不要放到sdcard上,否则代码容易被注入攻击。
libraryPath:os库的存放路径,可以为空,若有os库,必须填写。
parent:父亲加载器,一般为ClassLoader.getSystemClassLoader()。
如果大家对于插件化有所了解,肯定对这个类不陌生,插件化一般就是提供一个apk(插件)文件,然后在程序中load该apk,那么如何加载apk中的类呢?其实就是通过这个DexClassLoader,具体的代码我们后面有描述。
ok,到这里,大家只需要明白,Android使用PathClassLoader作为其类加载器,DexClassLoader可以从.jar和.apk类型的文件内部加载classes.dex文件就好了。
由上图可以看到,两个叶子节点的类都继承BaseDexClassLoader中,而具体的类加载逻辑也在此类中:
@Override protected Class<?> findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); Class c = pathList.findClass(name, suppressedExceptions); if (c == null) { ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList); for (Throwable t : suppressedExceptions) { cnfe.addSuppressed(t); } throw cnfe; } return c; }
由上述函数可知,当我们需要加载一个class时,实际是从pathList中去需要的,查阅源码,发现pathList是DexPathList类的一个实例。ok,接着去分析DexPathList类中的findClass函数,
DexPathList:
public Class findClass(String name, List<Throwable> suppressed) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz != null) { return clazz; } } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; }
#DexFile public Class loadClassBinaryName(String name, ClassLoader loader) { return defineClass(name, loader, mCookie); } private native static Class defineClass(String name, ClassLoader loader, int cookie);
上述函数的大致逻辑为:遍历一个装在dex文件(每个dex文件实际上是一个DexFile对象)的数组(Element数组,Element是一个内部类),然后依次去加载所需要的class文件,直到找到为止。
看到这里,dex分包后再注入的解决方案也就浮出水面,假如我们将第二个dex文件放入Element数组中,那么在加载第二个dex包中的类时,应该可以直接找到。
那么在我们自定义的BaseApplication的onCreate中,我们执行注入操作:
public String inject(String libPath) { boolean hasBaseDexClassLoader = true; try { Class.forName("dalvik.system.BaseDexClassLoader"); } catch (ClassNotFoundException e) { hasBaseDexClassLoader = false; } if (hasBaseDexClassLoader) { PathClassLoader pathClassLoader = (PathClassLoader)sApplication.getClassLoader(); DexClassLoader dexClassLoader = new DexClassLoader(libPath, sApplication.getDir("dex", 0).getAbsolutePath(), libPath, sApplication.getClassLoader()); try { Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader))); Object pathList = getPathList(pathClassLoader); setField(pathList, pathList.getClass(), "dexElements", dexElements); return "SUCCESS"; } catch (Throwable e) { e.printStackTrace(); return android.util.Log.getStackTraceString(e); } } return "SUCCESS"; }
这是注入的关键函数,分析一下这个函数:
参数libPath是第二个dex包的文件信息(包含完整路径,我们当初将其打包到了assets目录下),然后将其使用DexClassLoader来加载(这里为什么必须使用DexClassLoader加载,回顾以上的使用场景),然后通过反射获取PathClassLoader中的DexPathList中的Element数组(已加载了第一个dex包,由系统加载),以及DexClassLoader中的DexPathList中的Element数组(刚将第二个dex包加载进去),将两个Element数组合并之后,再将其赋值给PathClassLoader的Element数组,到此,注入完毕。
一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。
相关文章推荐
- 关于 Android中的插件化开发,dex分包,热修复(Tinker)的思考(二)
- Android关于分包方案、插件化动态加载APK或DEX 以及热补丁资料总结
- 关于android开发中的一点思考
- Android dex分包方案以及热补丁修复
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- 关于Android apk 插件化开发
- Android插件化,热修复,模块化开发
- 基于Dex分包方案---热修复、热更新、插件化
- Android插件化开发之DexClassLoader动态加载dex、jar小Demo
- Android热修复之dex多分包架构设计
- Android 热修复 Tinker 源码分析之DexDiff / DexPatch
- Android中关于修复bug的思考
- Android开发中关于获取当前Activity的一些思考
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- Android热补丁动态修复技术(一)dex分包原理
- Android 热修复方案Tinker(三) Dex补丁加载
- Android开发—— 热修复Tinker源码浅析
- Android 热修复方案Tinker(三) Dex补丁加载
- 微信 Tinker 负责人张绍文关于 Android 热修复直播分享记录