Android dex dynamic loading (Kotlin version)

foreword

environment

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

overview

  1. The libaray project is packaged into jar
  2. jar is converted to dex.jar through the dx/d8 command line tool
  3. Put dex.jar in the assets directory
  4. App startup reads the dex.jar in assets and copies it to a folder accessible to the App (it is recommended to be in the sandbox of internal storage, without permission restrictions)
  5. Instantiate DexClassLoader to load dex to get ClassLoader object
  6. Get the class you want to execute through the ClassLoader.loadClass method
  7. It can be realized through interface or reflection, get the internal properties of the class or execute the internal method of the class
ClassLoader

Unlike JVM, Dalvik's virtual machine cannot directly load .dex with ClassCload. Android derives two classes from ClassLoader: DexClassLoader and PathClassLoader;
and these two classes are the key to our loading dex files. The difference between the two is:

1.DexClassLoader: jar/apk/dex can be loaded, and uninstalled apk can be loaded from the SD card;
2.PathClassLoader: the apk storage path in the system must be passed in, so only installed apk files can be loaded.

So the core class we dynamically load dex is DexClassLoader

practice

packaging jar

1. Create a library project named dexlib
2. Add the following code:

Glide is introduced to experiment with the feasibility of third-party library dependencies.

    //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. Add the packaging jar code in build.gradle, andsync 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. Double-click in gradle to generate dexlib.jaragainMakeJar in dexlib/libs

Generate dex.jar

1. Open the command line under dexlib/libs
2. Refer to Android to use dx/d8 to convert jar to dex , and run the corresponding command line
3. Wait a few seconds to generatedexlib_dex.jar
4.dexlib_dex.jar is a jar containing dex, etc.

app project

1. dexlib_dex.jarPut it in the assets directory such as the app project, and add Glide dependencies in build.gradle.
2. Start to execute the following code, and dexlib_dex.jarcopy it to the internal storage to the sandbox.
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. Execute the following method to load dex and get the DexClassLoader object.
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. Obtain the DexWork class according to the complete class name, and execute the methods in the DexWork class through the reflection code. The complete Activity code is as follows:

Tips: The interface implementation method is simpler than reflection implementation, please refer to: 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. Run the app project, publish the code in DexWork normally, and the pictures can also be loaded normally (note the network permissions).
6. The dex dynamic loading practice is completed. It can also be seen that the app project also introduces the third-party library dependency in the library, which can solve the problem that the jar cannot package the third-party library code.

Effect screenshot

insert image description here

source code

https://github.com/DeMonDemoSpace/DexDynamicLoad

reference

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

Guess you like

Origin blog.csdn.net/DeMonliuhui/article/details/128255887