在早期的支付宝android客户端中,也有插件化的功能。大概的做法就是,自定义所有的UI控件,再通过XML文件,仿安卓原生XML的布局文件来搭建布局,再通过自定义的表达式解析器,利用JAVA的反射特性来给具体的控件添加不同的功能。这样也达到了插件化。
之前写过一篇文章,说的是支付宝的插件化。其实这篇文章很老了,现在的支付宝早已不是这种做法。最近几天忙里偷闲,反编译了一下支付宝的插件化。
在下资历不高,简单分享一下,大牛看到也不要喷我,在下也是在探索学习中,欢迎交流!
工具:
工欲善其事,必先利其器。因为平时拆包少,对某些好工具也了解不多,基本用了手工的方法来处理的。大家可以用什么APK改之理之类的工具。
apktool
:这个大家都知道,反编译利器,我下的是apktool_2.0.0b9版本dex2jar
:不是必须,但看smali代码太累,用这个工具好受一些jd-gui
:不解释Replace Studio
:文本搜索工具,可以搜索某文件夹下的文件是否有某文本,我一直用这个,不知道大家有没有其它好工具推荐。notepad++
:如果你用记事本也可以android环境
:这个必须,你看完它的代码了,你起码得自己写的试试吧
简单拆包分析
先copy一份apk出来,改后缀名为zip,直接解压,先瞧瞧里面的内容,发现在/lib/armeabi/下的so文件相当的多,有蹊跷!
出于习惯,立马就拿notepad++打开了,结果发现,在文本的最前边是PK
开头的两个字符,哈哈,这绝对的是一个zip文件,我们都知道apk其实就是一个zip,而且,真正的so文件应该是以ELF
开头的。随便找一个打开,发现了APK的结构:
该上工具了,一步到位。对支付宝主APK进行拆包,使用apktool d xxx.apk
命令,直接拆成smali,再使用dex2jar
命令拆成jar包,再保存成java文件。
打开AndroidManifest.xml
文件,找到application
com.alipay.mobile.quinox.LauncherApplication
用jd-gui打开jar包,找到该类,查看onCreate方法,找到这段代码
代码中反射的mPackageInfo
其实就是有名的LoadedApk
类。
使用Replace Studio
在生成的java文件目录下搜索pathClassloader
我发现在com.alipay.mobile.quinox.classloader下有两个类继承自该类。
通过对smali代码的注入log日志的跟踪,JAVA文件和smali文件相互对照(因为不是所有的class都能反编译回来),我大概整理了一些逻辑与类的结构。
安卓动态加载原理
支付宝把一个一个插件称为bundle,在application
的onCreate方法中,反射mPackageInfo
中的mClassloader
字段,该属性是一个pathClassloade,将其替换成自己的PathClassloader(这段代码在dex2jar后的代码中看不到,我是直接读的smali代码)。
在自定义的PathClassloader中处理,如果是自身dex中的类,则用原pathClassloader加载,如果是bundle,则用bundle的dexfile.loadClass
来加载
BundleClassloader
:继承自classloader,用于加载具体的某个插件,包含一个DexFile文件引用,重写了loadClass方法,通过调用dexFile.loadClass(“className” , classLoader);HostClasloader
:继承自PathClassloader,包含一个系统的pathClassloader,也就是加载apk本身的pathClassloaderBootstrapClassloader
:继承自PathClassloader
,该类中包含一个map集合,保存着一个一个的BundleClassloader
,同时包含一个HostClasloader,该类就是自定义的pathClassloader,通过反射将原来的mClassloader替换成该类。OriginClassLoader
:继承自classloader,也就是上图中new的c,ClassLoader.class的parent
成了该对象。重写了findClass方法,调用了对android原生的类和APK中的类加载做了分发处理,APK中的类调用BootstrapClassloader
的loadClass
方法返回。
关于资源
使用反射创建一个AssetManager对象, 使用getDeclaredMethod
后调用addAssetPath
方法,先用getApplicationInfo().sourceDir
做参数调用该方法,再用bundle的路径调用该方法,这样就能整合到一起。
1 2 3 4 5 6 7 8 | //先反射创建一个AssetManager对象 //反射得到addAssetPath //调用addAssetPath并把当前APK的路径传进去,也就是sourceDir //调用addAssetPath并把子包的APK路径传进去 //使用下面的代码创建出Resources //用反射替换掉 mPackageInfo 的mResources字段 Resources rs = getResources(); new Resources(assetManager , rs.getDisplayMetrics(), rs.getConfiguration()) |
使用反射替换掉 mPackageInfo
的mResources
字段
代码
我只写了一下动态加载activity的代码,具体的资源我没有加载,小伙伴们可以自己试试。
activity我没有添加,大家可以添加到主工程下,也可以添加的被加载的工程下,不过一定要记得在AndroidManifest.xml
里注册
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 | public class MyApplication extends Application { private Field field; @Override public void onCreate() { super.onCreate(); Context context = getBaseContext(); Field localField1; try { localField1 = context.getClass().getDeclaredField("mPackageInfo"); localField1.setAccessible(true); Object mPackageInfo = localField1.get(context); field = mPackageInfo.getClass().getDeclaredField("mClassLoader"); field.setAccessible(true); Object mClassLoader = field.get(mPackageInfo); ClassLoader loader = new MyPathClassLoader(this, this.getApplicationInfo().sourceDir, (PathClassLoader) mClassLoader); field.set(mPackageInfo, loader); } catch (Exception e) { } } } |
下面是我的Classloader 比较粗糙
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 | public class MyPathClassLoader extends PathClassLoader { private ClassLoader mClassLoader; private Context context; public MyPathClassLoader(Context context,String dexPath, PathClassLoader mClassLoader) { super(dexPath, mClassLoader); this.mClassLoader = mClassLoader; this.context = context; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { File file = new File("/data/data/com.example.test/lib/libtest.so"); Class clazz = null; try { clazz = mClassLoader.loadClass(name); } catch (Exception e) { } if (clazz != null) { return clazz; } try { DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(), context .getDir("dex", 0).getAbsolutePath() + "/libtest.so", 0); return dexFile.loadClass(name, ClassLoader.getSystemClassLoader()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return super.findClass(name); } } |
其它
支付宝之前的做法利用自定义表达式解析器,对JAVA程序员的学习成本相当高,后来支付宝就换成了该方法。
优点:
- 使用同一个Context,还是同一个应用,相当灵活
- 能解决打包时候method ID not in [0, 0xffff]: 65536的问题
- 使用反射少
另外,该方法的缺点:
- 用到的组件必须在manifest.xml中声明,我们并没有突破manifest的验证
- 使用了反射私有API,尽管反射使用的不多。
- 资源文件处理,如果bundle中的id和主工程下的id冲突了就悲剧了。支付宝自己修改了aapt的源码,把资源
0x7f010001
前面的7f改了。所有应用的生成id都是7f打头的,该方法不修改aapt办不到,会给你自动改回来。一般我们也可以通过public.xml下指定id
说明:
同是阿里系的淘宝网有一套框架叫atlas,该框架是一套重量级框架,完全突破了manifest的封锁,不同的bundle使用的不同的context。
后记
其实支付宝也突破了manifest文件,采用的是代理的模式,注册一个CommonActivity,在各生命周期的方法中调用targetActivity的方法。再利用反射将CommonActivity中的变量赋值到插件中targetActivity中(用遍历就能满足),此方法有个缺陷就是,在插件中的activity中,要慎用this关键字,必要用的时候,得用其它方法取CommonActivity对象。
Android中文API(140) —— DexFile
前言
本章内容dalvik.system.DexFile章节,版本为Android 4.0 r1,翻译来自:"阿年",欢迎访问他的博客:"http://blog.csdn.net/mtding",再次感谢"阿年" !期待你一起参与翻译Android的相关资料,联系我over140@gmail.com。
声明
欢迎转载,但请保留文章原始出处:)
Android中文翻译组:http://androidbox.sinaapp.com/
DexFile
译者署名:阿年
译者链接:http://blog.csdn.net/mtding
版本:Android 4.0 r1
结构
继承关系
public final class DexFile extends Object
java.lang.Object
dalvik.system.DexFile
类概述
操作DEX文件。这个类原理上和ZipFile相似。主要在类装载器里被使用。
注意,我们不直接打开和读取DEX文件。它们被虚拟机以只读方式映射到内存了。
构造函数
public DexFile (File file)
通过指定的File对象打开DEX文件。指定的文件通常是一个ZIP/JAR文件,里面包含一个”classes.dex”。虚拟机将在目录/data/dalvik-cache下生成对应的文件名字并打开它,如果系统权限允许的话会首先创建或更新它。不要传目录/data/dalvik-cache下的文件名给它,因为这个文件被认为处于初始状态(DEX被优化之前)。
参数
File 引用实际DEX文件的File对象
异常
IOException 发生I/O异常,例如文件不存在或者没有权限访问。
public DexFile (String fileName)
打开指定文件名的DEX文件。指定的文件通常是一个ZIP/JAR文件,里面包含一个”classes.dex”。虚拟机将在目录/data/dalvik-cache下生成对应的文件名字并打开它,如果系统权限允许的话会首先创建或更新它。不要传目录/data/dalvik-cache下的文件名给它,因为这个文件被认为处于初始状态(DEX被优化之前)。
参数
fileName DEX文件名。
异常
IOException 发生I/O异常,例如文件不存在或者没有权限访问。
公共方法
public void close ()
关闭DEX文件。
有可能无法释放任何资源。如果来自DEX文件的类还存活着的话,DEX文件不能被取消映射。
异常
IOException 在关闭文件的过程中可能发生I/O异常,一般不会发生。
public Enumeration<String> entries ()
枚举DEX文件里面的类名。
返回值
DEX文件所包含类名的枚举,类名的类型是一般内部格式(像java/lang/String)。
public String getName ()
获取(已打开)DEX文件名。
返回值
文件名
public static boolean isDexOptNeeded (String fileName)
如果虚拟机认为apk/jar文件已经过期返回true,并且应该再次通过”dexopt”传递。(译者注:dexopt是apk优化工具)
参数
fileName 被检查apk/jar文件的绝对路径名。
返回值
如果应该调用dexopt处理文件返回true;否则false。
异常
FileNotFoundException 文件不可读、不是一个文件或者文件不存在。
IOException fileName不是有效的apk/jar文件,或者在解析文件时出现问题。
NullPointerException fileName是空的。
StaleDexCacheError 优化过的DEX文件已过期且位于只读分区。
public Class loadClass (String name, ClassLoader loader)
装载一个类。返回成功装载的类,失败返回空。
如果在类装载器之外调用它,往往不会得到你想要的结果,这时请使用forName(String)。
该方法不会在找不到类的时候抛出ClassNotFoundException异常,因为每次在我们看到的第一个DEX文件里找不到类就粗暴地抛出异常是不合理的。
参数
name 类名,应该是一个"java/lang/String"
loader 试图装载类的类装载器(大多数情况下就是该方法的调用者)
返回值
类名对应的对象,装载失败时返回空。
public static DexFile loadDex (String sourcePathName, String outputPathName, int flags)
打开一个DEX文件,并提供一个文件来保存优化过的DEX数据。如果优化过的格式已存在并且是最新的,就直接使用它。如果不是,虚拟机将试图重新创建一个。该方法主要用于应用希望在通常的应用安装机制之外下载和执行DEX文件。不能在应用里直接调用该方法,而应该通过一个类装载器例如dalvik.system.DexClassLoader.
参数
sourcePathName 包含”classes.dex”的Jar或者APK文件。(将来可能会扩展支持"raw DEX"。)
outputPathName 保存优化过的DEX数据的文件。
flags 打开可选功能(目前什么也没定义)
返回值
一个新的,或者先前已经打开的DexFile。
异常
IOException 无法打开输入或输出文件。
受保护方法
protected void finalize ()
类结束时调用。确保DEX文件被关闭。
异常
IOException 关闭文件时发生I/O异常,一般不会发生。