上篇介绍了如何从dex中加载类,这篇尝试了一下从apk中加载资源,用的同样是DexClassLoader。同样还是那个kotlin项目,简单的尝试从另一个apk的drawable中加载一张图片,个人感觉还是挺麻烦的。
先准备另一个项目
新建另一个项目,这个项目只在drawable下放了一张名为ssm的图片。然后生成一个debug的apk包。将这个apk拷贝到本项目的assets下(只是为了方便,也可以从远程获取这个apk)。
我们的目标就是通过这个debug的apk来加载这张图片,这里我将这个apk的名字改为了plugin1.apk。
简单的代码
这里不得不说一下,kotlin对java的兼容做的真的不错,这里的反射本来还有点担心该怎么做,后来发现比较容易的就实现了。只不过语法上有一些小小的不同。在Java中通过类名.class就可以访问该类的Class对象,而在Kotlin中则需要类名::class.java,可以访问到java的Class对象。
fun dynamicLoadApk() {
val pm = packageManager
// 在应用安装目录下创建一个名为plugin的文件夹目录
// 我进了程序目录看了一下,叫app_plugin
val optDir = getDir("plugin", Context.MODE_PRIVATE)
// 生成输出文件
val desFile = File(optDir.path + File.separator + "plugin1.apk")
println(desFile.path)
println(desFile.absolutePath)
// 如果不存在,将assets下的plugin1.apk复制到输出文件中
if (!desFile.exists()) {
desFile.createNewFile()
copyFiles("plugin1.apk", desFile)
}
// plugin1.apk 获取包名
val pkInfo = pm.getPackageArchiveInfo(desFile.path,
PackageManager.GET_ACTIVITIES)
val packageName = pkInfo.applicationInfo.packageName
// 访问AssetManager的Class对象,生成AssetManager实例对象
val assetManager = AssetManager::class.java.newInstance()
// 通过反射拿到隐藏方法
val addAssetPath = assetManager.javaClass.getMethod("addAssetPath", String::class.java)
val loader = DexClassLoader(optDir.path + File.separator +
"plugin1.apk", optDir.path, null,
ClassLoader.getSystemClassLoader())
addAssetPath.invoke(assetManager, desFile.path)
val superRes = resources
val mResources = Resources(assetManager, superRes.displayMetrics, superRes.configuration)
val clz = loader.loadClass("$packageName.R\$drawable")
val field = clz.getDeclaredField("ssm")
val resId = field.getInt(R.id::class)
println("resId: $resId")
val iv_img = findViewById(R.id.iv_img) as ImageView
iv_img.setImageDrawable(mResources.getDrawable(resId))
}
效果图:
的确可以加载成功。但是到这,这两篇只能算是hello world,比起普通的资源加载,更令人向往的是启动各个未安装的apk中的Activity和各种Service。不过比起到现在为止的直接反射暴力新建对象,Activity作为系统的组件,需要系统来初始化,来调用各个生命周期方法。后续就是就是要关注一下如何调用这些资源了。