用了一周多,做了一个Android动态加载的小玩具DCommand。支持下载APK,获取其中的资源、执行代码、启动Activity(这个是抄的,非常粗糙)。
最开始只是觉得动态加载逻辑代码很有用,如果MVP模式使用合理的话,对于大部分的逻辑更新、线上bug修复直接使用动态下发APK,更新P端的逻辑即可。后来越来越复杂,最后基本所有方面都可以动态使用,如果再深入开发的话,做个MVP框架也是可以的(当然最好迁移到RxJava上,这种网络文件操作很多的东西,用响应式编程还是很赞的)。

整体结构

理想情况下,结构是分层的:
- Apk管理,ApkConfigManager,负责下载Apk、验证安全并读取其中的配置。维护Apk相关数据库
- classloader管理,ClassManager,负责创建对应某个Apk的DexClassLoader
- resources/asset管理,ResourceManager,负责创建对应某个Apk的ResourceFetcher。维护resource相关数据库
- interface管理,CommandManager,负责加载某个interface在Apk中对应的实现。维护interface相关数据库
- component管理,ComponentManager,负责启动指定类名的Android Component(暂时只有Activity)。维护component相关数据库
- resource获取,ResourceFetcher,负责构建Resources对象,并获取其中的指定资源
- App,动态加载的使用者

流程

以获取interface的实现为例:
1. 调用CommandManager.getImplement
2. CommandManager调用ClassManager.loadClass
3. ClassManager调用ApkConfigManager.getApkInfoAndFileById
4. ApkConfigManager下载Config.json并解析
5. ApkConfigManager使用Apk的id找到url,下载Apk
6. ApkConfigManager校验Apk签名
7. ApkConfigManager通知有新Apk
8. CommandManager得到通知,更新数据库
9. ApkConfigManager通过回调,将Apk文件返回给ClassManager
10. ClassManager使用返回的文件构造ClassFetcher,通过回调返回给CommandManager
11. CommandManager使用数据库中的映射关系找到interface的实现,反射构建实现对象

细节

名词 解释
Apk 动态加载的目标,从网络上下载的Apk文件
宿主 使用动态加载的程序

1. Apk配置和更新

使用id唯一表示Apk,不使用版本,使用时间戳来标记Apk新旧。时间戳是配置文件的生成时间点,如果Apk是在配置文件之后下载的,肯定是最新版本。使用Apk版本会要求多次读文件内容,效率有问题。

2. Apk校验

校验分为两部分:Apk签名完整性的校验,Apk签名和宿主签名的校验。

  • Apk签名校验:
    • Apk签名过程:解释0,解释1。总结起来就是:先对所有文件进行digest放到MANIFEST.MF中;再对MANIFEST.MF的每一行加密,放到CERT.SF中;最后把公钥信息放到CERT.RSA中。
    • 校验过程:入口在PackageParser中,基本是签名的逆过程,但是写的实在是乱,没看懂。真正调用签名校验的地方是PackageParser.collectCertificates。
      真正使用不需要这么复杂。正常情况下,使用PackageManager.getPackageArchieveInfo并传入GET_SIGNATURES就可以了。当然也可以直接反射。
  • 签名交叉校验
    其实就是读入宿主和Apk的签名,对比是不是相同的公钥。
//读入证书X509Certificate cert = (X509Certificate) certFactory                        .generateCertificate(new ByteArrayInputStream(signature.toByteArray()));//公钥字符串cert.getPublicKey().toString();

公钥本身在公钥字符串中,在字符串modulus=和,publicExponent之间。

3. AndroidManifest的解析

Android中Xml是预处理过的,所以不能随随便便就读出来了。一个还算准确的图,一个通过源码分析的解释。特别推荐一下后一个,他通过看ResType.h这些aapt相关的类,非常准确的还原了二进制的结构。
manifest文件二进制片段示例中对应关系如下(二进制是big-endian的):

        /*            <manifest versionCode="1"            0201// type            1000// header size            8800 0000// size            0200 0000// line number            ffff ffff// comment            ffff ffff// ns            0c00 0000// name            1400// start            1400// size            0500// count            0000// id            0000// class            0000// style            0700 0000// ns            0000 0000// name            ffff ffff// raw value            0800// size            00// 00            10// type            0100 0000//data        */

对于大部分基础信息,PackageManager.getPackageArchieveInfo就足够了。我这里用到的,PackageManager.getPackageArchieveInfo没有的,只有读取meta-data的功能。这个答案里的代码是有问题的,需要修改,具体可见Manifest类。

4. DexClassLoader

构造函数中的optimizedDirectory就是dex文件解压后的位置,第一次还是比较慢的。

5. 泛型

  • return泛型时,Java会自动类型匹配。但是用回调代替return后,自动类型匹配不管用。可以用一个比较鬼的办法:
public interface Listener{    <T> T onXXX(T obj);}调用时:Foo ret = listener.onXXX(obj);

此时obj会被匹配成Foo类的对象。

6. 动态Activity

启动动态Activity问题在于系统不识别动态Activity。基本就是用fragment或者proxyActivity来绕过。关于ProxyActivity。
使用proxyActivity分为两部分:
- 宿主提供ProxyActivity,系统实际识别的就是这个Activity。ProxyActivity将所有系统的回调事件路由给DynamicActivity,基本就是所有onXXX函数
- Apk提供DynamicActivity,所有真正的逻辑都在这里。DynamicActivity将所有调用系统的函数都委托给ProxyActivity来做,比如startActivity、getXXX等。
里面有一个坑,在super.onCreate之前,getIntent是返回不了有效数据的。

后来发现,其实还是有很多牛逼的解决方法更优雅的解决这个问题的。总结一下探索新方法的思路:
1. 仔细看一遍Activity的启动流程
2. 寻找里面非native、非IPC的与Activity相关的部分(存在于同一个虚拟机实例中,可以通过反射替换成自己的实现):

  • Activity.mInstrumentation
  • PackageInfo
  • ActivityInfo

7. Resources

  • 资源是靠id标记的,id的最高8位是包名,正常生成的Apk,id都是7f打头的
  • 资源id的type字段并不是固定的,是在aapt生成时遇到什么新资源就加一生成的
  • 资源的维度简直就不能理解,老罗的解读
  • 资源读取时,就是顺序的找,找到了就返回了。所以当宿主和Apk的资源同时存在时(不修改aapt一定会有重复id),一定会出bug。所以不能反射修改Activity的mResources,这样会出错。只能显式的分开用这两种资源
  • 调用某Apk中的资源代码:
            assetManager = AssetManager.class.newInstance();            ReflectUtils.invokeMethod(assetManager, "addAssetPath", new Class[]{String.class}, new Object[]{apkFile.getAbsolutePath()});            mResources = new Resources(assetManager, metrics, configuration);

这个mResources就是可以用的,包括id和真正资源

更多相关文章

  1. [Android]如何做一个崩溃率少于千分之三噶应用app(32)-Android(安
  2. 如何理解Gradle?Grade在Android的构建过程中有什么作用?动态修改An
  3. Android手机软件汉化教程---第五课 打包签名
  4. android动态显示图片
  5. android fragment动态显示隐藏
  6. Android动态权限列表
  7. Android(安卓)6.0 动态申请权限无效问题
  8. Android反编译工具及其使用
  9. android 客制化系统签名文件

随机推荐

  1. Android客户端与服务端通信乱码问题的解
  2. [置顶] 我的Android进阶之旅------>andro
  3. Retrofit简单使用(Kotlin)
  4. 转:Android 学习笔记3(控件)
  5. android中开机自动运行程序
  6. Android自带的TabLayout实现滑动翻页效果
  7. 【android】解决在图库中缩略图与实际图
  8. Android架构组件WorkManager详解
  9. android 实现屏幕截图
  10. 04.Android的数据存储操作