Android dex动态加载(Kotlin版)

前言

环境

  1. 语言–Kotlin
  2. JDK11
  3. SDK33
  4. AndroidStudio版本
Android Studio Dolphin | 2021.3.1 Patch 1
Build #AI-213.7172.25.2113.9123335, built on September 30, 2022

概述

  1. libaray项目打包成jar
  2. jar通过dx/d8命令行工具转为dex.jar
  3. dex.jar放到assets目录下
  4. App启动读取assets中的dex.jar复制到App可访问的文件夹中(建议内部存储的沙盒中,不受权限限制)
  5. 实例化DexClassLoader加载dex获取ClassLoader对象
  6. 通过ClassLoader.loadClass方法,获取想要执行的类
  7. 可以通过接口实现or反射实现,获取类的内部属性或者执行类的内部方法
ClassLoader

与JVM不同,Dalvik的虚拟机不能用ClassCload直接加载.dex,Android从ClassLoader派生出了两个类:DexClassLoader和PathClassLoader;
而这两个类就是我们加载dex文件的关键,这两者的区别是:

1.DexClassLoader:可以加载jar/apk/dex,可以从SD卡中加载未安装的apk;
2.PathClassLoader:要传入系统中apk的存放Path,所以只能加载已经安装的apk文件。

因此我们动态加载dex的核心类是DexClassLoader

实践

打包jar

1.创建一个library项目,命名为dexlib
2.添加如下代码:

引入Glide是为了实验第三方库依赖的可行性。

    //Glide4.x
    implementation 'com.github.bumptech.glide:glide:4.13.1'
    implementation 'com.github.bumptech.glide:okhttp3-integration:4.13.1'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.13.1'
class DexWork {
    
    

    fun showNavToast(context: Context, text: String) {
    
    
        Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
    }


    fun loadImage(imageView: ImageView, url: String) {
    
    
        Glide.with(imageView.context).load(url).into(imageView)
    }


    fun getClassName(): String {
    
    
        return this.javaClass.canonicalName
    }
}
3.build.gradle中添加打包jar代码,并sync now
//根据Library名称生存jar包到build目录下
//可根据自己需求更改
task againMakeJar(type: Copy) {
    //Library名称
    def name = project.name
    //删除之前的旧jar包
    delete 'libs/' + name + '.jar'
    //从这个目录下取出默认jar包,不同版本目录均不一样,根据自己项目在build中找classes.jar所在目录
    from('build/intermediates/compile_library_classes_jar/release/')
    into('libs/') //将jar包输出到指定目录下
    include('classes.jar')
    rename('classes.jar', name + '.jar') //自定义jar包的名字
}
againMakeJar.dependsOn(build)
4.gradle中双击againMakeJar,在dexlib/libs中生成dexlib.jar

生成dex.jar

1.在dexlib/libs下打开命令行
2.参考Android 使用dx/d8将jar转换为dex,运行对应的命令行
3.等几秒生成dexlib_dex.jar
4.dexlib_dex.jar是一个包含dex等jar

app项目

1.dexlib_dex.jar放到app项目等assets目录下,并在build.gradle中添加Glide依赖。
2.启动执行如下代码,将dexlib_dex.jar复制到内部存储到沙盒中去。
private val dexName = "dexlib_dex.jar"
Utils.copyDex(this, dexName)
    /**
     * 复制dex到沙盒中
     */
    fun copyDex(context: Context, dexName: String) {
    
    
        val cacheFile = File(context.filesDir, "dex")
        if (!cacheFile.exists()) {
    
    
            cacheFile.mkdirs()
        }
        val internalPath = cacheFile.absolutePath + File.separator + dexName
        val desFile = File(internalPath)
        if (!desFile.exists()) {
    
    
            desFile.createNewFile()
        }
        var `in`: InputStream? = null
        var out: OutputStream? = null
        try {
    
    
            `in` = context.applicationContext.assets.open(dexName)
            out = FileOutputStream(desFile.absolutePath)
            val bytes = ByteArray(1024)
            var len = 0
            while (`in`.read(bytes).also {
    
     len = it } != -1) out.write(bytes, 0, len)
            out.flush()
        } catch (e: IOException) {
    
    
            e.printStackTrace()
        } finally {
    
    
            try {
    
    
                `in`?.close()
                out?.close()
            } catch (e: IOException) {
    
    
                e.printStackTrace()
            }
        }
    }
3.执行如下方法,加载dex,获取到DexClassLoader对象。
Utils.loader = Utils.loadDexClass(this, dexName)
    /**
     * 加载dex
     */
    fun loadDexClass(context: Context, dexName: String): DexClassLoader? {
    
    
        try {
    
    
            //下面开始加载dex class
            //1.待加载的dex文件路径,如果是外存路径,一定要加上读外存文件的权限,
            //2.解压后的dex存放位置,此位置一定要是可读写且仅该应用可读写
            //3.指向包含本地库(so)的文件夹路径,可以设为null
            //4.父级类加载器,一般可以通过Context.getClassLoader获取到,也可以通过ClassLoader.getSystemClassLoader()取到。
            val cacheFile = File(context.filesDir, "dex")
            val internalPath = cacheFile.absolutePath + File.separator + dexName
            return DexClassLoader(internalPath, cacheFile.absolutePath, null, context.classLoader)
        } catch (e: Exception) {
    
    
            e.printStackTrace()
        }
        return null
    }
4.根据完整类名获取DexWork类,反射代码执行DexWork类中的方法,完整Activity代码如下:

Tips:接口实现的方式相较于反射实现起来更简单,可参考:https://blog.csdn.net/wy353208214/article/details/50859422

class NormalActivity : AppCompatActivity() {
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_show)

        /**
         * 非静态类反射,kt代码跟java代码反射调用完全一致
         * invoke 第一个参数传入类实例
         */
        val cla = Utils.loader?.loadClass("com.demon.dexlib.DexWork")

        cla?.run {
    
    
            val className = getMethod("getClassName").invoke(newInstance()) as String
            findViewById<TextView>(R.id.text).text = className

            findViewById<Button>(R.id.btn1).setOnClickListener {
    
    
                getMethod("showNavToast", Context::class.java, String::class.java).invoke(newInstance(), this@NormalActivity, className)
            }

            val img = findViewById<ImageView>(R.id.iv)
            findViewById<Button>(R.id.btn2).setOnClickListener {
    
    
                getMethod("loadImage", ImageView::class.java, String::class.java).invoke(
                    newInstance(), img,
                    "https://idemon.oss-cn-guangzhou.aliyuncs.com/D.png"
                )
            }
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="原生toast"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />



    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Load Image"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn1" />



    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="Hello World!"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn2" />


    <ImageView
        android:id="@+id/iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/text" />
</androidx.constraintlayout.widget.ConstraintLayout>
5.运行app项目,发行可以正常DexWork中的代码,图片也可以正常加载(注意网络权限)。
6.dex动态加载实践完成。也可知:app项目同时引入library中的第三库依赖,可以解决jar无法打包第三方库代码的问题。

效果截图

在这里插入图片描述

源码

https://github.com/DeMonDemoSpace/DexDynamicLoad

参考

https://blog.csdn.net/wy353208214/article/details/50859422

猜你喜欢

转载自blog.csdn.net/DeMonliuhui/article/details/128255887