2

I'm working on an android app and I'm trying to plugin-like feature which would allow loading additional dex files to extend app functionality. I've figured out how to load additional dex files extending PathClassLoader with few small changes to allow other modules to communicate. The issue is that when the dex files are loaded into the app for the first time when the app is running all works perfectly, then if I decide to disable this module so the classloader is unloaded the app continues to work properly for a few seconds then it throws exception (still continue working properly) and then again few seconds/minutes later (sometimes it takes even 5 minutes) the app crashes with native stack trace. If I decide to load the module which I disabled previously again it will only increase a chance to crash.

This is what happens few seconds module classloader is unloaded:

12-27 01:57:10.839 E/System: Uncaught exception thrown by finalizer
12-27 01:57:10.840 E/System: java.lang.AssertionError: Failed to close dex file in finalizer.
                                 at dalvik.system.DexFile.finalize(DexFile.java:336)
                                 at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:250)
                                 at java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:237)
                                 at java.lang.Daemons$Daemon.run(Daemons.java:103)
                                 at java.lang.Thread.run(Thread.java:764)
12-27 01:57:10.840 E/System: Uncaught exception thrown by finalizer
12-27 01:57:10.840 E/System: java.lang.AssertionError: Failed to close dex file in finalizer.
                                 at dalvik.system.DexFile.finalize(DexFile.java:336)
                                 at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:250)
                                 at java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:237)
                                 at java.lang.Daemons$Daemon.run(Daemons.java:103)
                                 at java.lang.Thread.run(Thread.java:764)
12-27 01:57:10.841 E/System: Uncaught exception thrown by finalizer
12-27 01:57:10.841 E/System: java.lang.AssertionError: Failed to close dex file in finalizer.
                                 at dalvik.system.DexFile.finalize(DexFile.java:336)
                                 at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:250)
                                 at java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:237)
                                 at java.lang.Daemons$Daemon.run(Daemons.java:103)
                                 at java.lang.Thread.run(Thread.java:764)

Then few seconds or minutes later there is native crash:

12-27 01:57:15.409 A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
12-27 01:57:15.409 A/DEBUG: Build fingerprint: 'Sony/G8341/G8341:8.0.0/47.1.A.8.49/3744219090:user/release-keys'
12-27 01:57:15.409 A/DEBUG: Revision: '0'
12-27 01:57:15.409 A/DEBUG: ABI: 'arm64'
12-27 01:57:15.409 A/DEBUG: pid: 17551, tid: 17697, name: Profile Saver  >>> com.rowl.plugdj 
12-27 01:57:15.409 A/DEBUG: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x657a6983
12-27 01:57:15.409 A/DEBUG:     x0   00000000657a6973  x1   0000007bb0739460  x2   0000007bb0600000  x3   0000000000000002
12-27 01:57:15.409 A/DEBUG:     x4   0000000000000139  x5   0000007bb073947f  x6   2f6d65747379732f  x7   726f77656d617266
12-27 01:57:15.409 A/DEBUG:     x8   0000000013954588  x9   aec629c3012f5dc2  x10  0000000000000139  x11  000000000000004c
12-27 01:57:15.409 A/DEBUG:     x12  656d6172662f6b72  x13  72616a2e6b726f77  x14  000000000000000d  x15  aaaaaaaaaaaaaaab
12-27 01:57:15.409 A/DEBUG:     x16  0000007bca457cc8  x17  0000007bca3f5f60  x18  0000007bc9c07eb0  x19  0000007baecba270
12-27 01:57:15.409 A/DEBUG:     x20  0000007ba6ab68c0  x21  0000007baecba588  x22  0000007ba6ab4298  x23  0000007baa692b18
12-27 01:57:15.409 A/DEBUG:     x24  0000000000000000  x25  0000000000002710  x26  00000000977434b0  x27  0000000097746934
12-27 01:57:15.409 A/DEBUG:     x28  0000007bc99b6b70  x29  0000007baecba1d0  x30  0000007bc96cf67c
12-27 01:57:15.409 A/DEBUG:     sp   0000007baecba0c0  pc   0000007bc96cf634  pstate 0000000080000000
12-27 01:57:15.411 A/DEBUG: backtrace:
12-27 01:57:15.411 A/DEBUG:     #00 pc 00000000002fe634  /system/lib64/libart.so (_ZN3art3jit12JitCodeCache18GetProfiledMethodsERKNSt3__13setINS2_12basic_stringIcNS2_11char_traitsIcEENS2_9allocatorIcEEEENS2_4lessIS9_EENS7_IS9_EEEERNS2_6vectorINS_17ProfileMethodInfoENS7_ISH_EEEE+228)
12-27 01:57:15.411 A/DEBUG:     #01 pc 000000000030ac08  /system/lib64/libart.so (_ZN3art12ProfileSaver20ProcessProfilingInfoEbPt+1452)
12-27 01:57:15.411 A/DEBUG:     #02 pc 00000000003099ac  /system/lib64/libart.so (_ZN3art12ProfileSaver3RunEv+704)
12-27 01:57:15.411 A/DEBUG:     #03 pc 000000000030b7b8  /system/lib64/libart.so (_ZN3art12ProfileSaver21RunProfileSaverThreadEPv+88)
12-27 01:57:15.411 A/DEBUG:     #04 pc 00000000000667d0  /system/lib64/libc.so (_ZL15__pthread_startPv+36)
12-27 01:57:15.411 A/DEBUG:     #05 pc 000000000001f2a4  /system/lib64/libc.so (__start_thread+68)

Here is the classloader which I'm using

public class ExtensionClassLoader extends PathClassLoader {

    private final Map<String, Class<?>> classes = new HashMap();
    private final ExtensionManager extensionManager;

    public ExtensionClassLoader(ExtensionManager extensionManager, String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, librarySearchPath, parent);
        this.extensionManager = extensionManager;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if (name.startsWith("com.rowl.") || name.startsWith("dj.plug.")) {
            throw new ClassNotFoundException(name);
        }
        PlugDJ.d("Find Class: " + name);
        Class clazz = classes.get(name);
        if(clazz == null){
            clazz = extensionManager.getClass(name);

            if(clazz == null){
                clazz = super.findClass(name);
                if(clazz != null){
                    extensionManager.addClass(name, clazz);
                }
            }
            if(clazz != null){
                classes.put(name, clazz);
            }
        }

        return clazz;
    }

    @Override
    public String findLibrary(String name) {
        return super.findLibrary(name);
    }

    public Set<String> getClasses(){
        return classes.keySet();
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
    }
}

Additionally here is the class which loads these modules:

class ExtensionManager(plugDJInstance: PlugDJ) : Manager(plugDJInstance) {

    private val classes = HashMap<String, Class<*>>()
    private val extensionLoaders = HashMap<String, ExtensionClassLoader>()

    private val loadedExtensions = ArrayList<PDJExtension>()
    private val availableExtensions = ArrayList<PDJExtensionInfo>()
    val plugAPI = PlugAPIImpl(pdjInstance as PlugDJAndroid)
    private val context by lazy { (pdjInstance as PlugDJAndroid).context }
    private val extensionSettings by lazy { context.getSharedPreferences("ext", Context.MODE_PRIVATE)}
    private val whitelistedExtensions by lazy { extensionSettings.getStringSet("whitelistedExtensions", HashSet<String>()) }

    fun addClass(name: String, clazz: Class<*>){
        if(!classes.containsKey(name)){
            classes.put(name, clazz)
        }
    }

    fun removeClass(name: String){
        classes.remove(name)
    }

    fun getClass(name: String): Class<*>?{
        return classes[name]
    }

    private fun loadApk(dir: File, dexOutputDir: File, parent: ClassLoader): ExtensionClassLoader{
        val files = StringBuilder()
        dir.listFiles().filter { it.extension == "apk" }.forEach {
            files.append(it.absolutePath).append(":")
        }
        files.deleteCharAt(files.length - 1)
        return ExtensionClassLoader(this, files.toString(), dexOutputDir.absolutePath.toString(), parent)
    }

    fun isExtensionWhitelisted(extInfo: PDJExtensionInfo): Boolean{
        return whitelistedExtensions.contains(extInfo.appPackage)
    }

    @SuppressLint("ApplySharedPref")
    fun addExtensionToWhitelist(extInfo: PDJExtensionInfo){
        whitelistedExtensions.add(extInfo.appPackage)
        extensionSettings.edit().putStringSet("whitelistedExtensions", whitelistedExtensions).commit()
        d("Added " + extInfo.appPackage + " to whitelist")
    }

    @SuppressLint("ApplySharedPref")
    fun removeExtensionFromWhitelist(extInfo: PDJExtensionInfo){
        whitelistedExtensions.remove(extInfo.appPackage)
        extensionSettings.edit().putStringSet("whitelistedExtensions", if(whitelistedExtensions.isEmpty()) null else whitelistedExtensions).commit()
        d("Checking... " + whitelistedExtensions)

        d("Removed " + extInfo.appPackage + " from whitelist")
    }

    fun discoverExtensions() {
        try{
            whitelistedExtensions.forEach {
                d("Whitelisted extension: $it")
            }
            val installedApps = context.packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
            for(app in installedApps){
                if(app.sourceDir.contains("system")) continue
                if(app.metaData != null && app.metaData.containsKey("pdjExtensionName")){
                    val extName = app.metaData.getString("pdjExtensionName")
                    val extMainClass = app.metaData.getString("pdjExtensionMainClass")
                    val sourceDir = File(app.sourceDir).parentFile
                    PlugDJ.i("Found extension: " + extName)
                    var extInfo = getAvailableExtensions().find { it.appPackage == app.packageName }
                    d(extInfo?.sourceDirectory.toString() )
                    d(sourceDir.toString() )
                    if(extInfo != null){
                        PlugDJ.d("Removing " + app.packageName)
                        availableExtensions.remove(extInfo)
                    }
                    extInfo = PDJExtensionInfoImpl(extName, sourceDir, app.packageName, extMainClass, "", "", null, null)
                    availableExtensions.add(extInfo)
                }
            }
        }catch (e: Exception){
            e.printStackTrace()
        }
    }



    fun getAvailableExtensions(): List<PDJExtensionInfo>{
        return Collections.unmodifiableList(ArrayList(availableExtensions))
    }

    fun getLoadedExtensions(): List<PDJExtension>{
        return Collections.unmodifiableList(ArrayList(loadedExtensions))
    }

    fun loadExtension(extInfo: PDJExtensionInfo): PDJExtension{
        var cl = context.classLoader
        val dexOutputDir = context.getDir("dex", Context.MODE_PRIVATE)
        val extcl = loadApk(extInfo.sourceDirectory, dexOutputDir, cl)
        extensionLoaders[extInfo.name] = extcl
        val mainClass = extcl.loadClass(extInfo.mainClass)
        val apiField = mainClass.superclass.getDeclaredField("api")
        val clField = mainClass.superclass.getDeclaredField("classLoader")
        val extField = mainClass.superclass.getDeclaredField("extensionInfo")
        val extension = mainClass.newInstance() as PDJExtension
        apiField.apply {
            isAccessible = true
            set(extension, plugAPI)
            isAccessible = false
        }
        clField.apply {
            isAccessible = true
            set(extension, mainClass.classLoader)
            isAccessible = false
        }
        extField.apply {
            isAccessible = true
            set(extension, extInfo)
            isAccessible = false
        }
        loadedExtensions.add(extension)
        return extension
    }

    fun getLoadedExtension(appPackage: String): PDJExtension? {
        return getLoadedExtensions().find { it.extensionInfo.appPackage == appPackage }
    }

    fun enableExtension(ext: PDJExtension){
        ext.onEnable()
        ext::class.java.superclass.getDeclaredField("isEnabled").apply {
            isAccessible = true
            set(ext, true)
            isAccessible = false
        }

    }

    fun disableExtension(ext: PDJExtension){
        try{
            plugAPI.removeEventListeners(ext)
            ext.onDisable()
        }catch (e: Exception){
            e.printStackTrace()
        }

        ext::class.java.superclass.getDeclaredField("isEnabled").apply {
            isAccessible = true
            set(ext, false)
            isAccessible = false
        }


    }

    fun unloadExtension(ext: PDJExtension){
        d("Unloading extension...")
        ext::class.java.superclass.getDeclaredField("api").apply {
            isAccessible = true
            set(ext, null)
            isAccessible = false
        }
        val cl = ext.classLoader as ExtensionClassLoader
        loadedExtensions.remove(ext)
        for(className in cl.classes){
            removeClass(className)
        }
        extensionLoaders.remove(ext.extensionInfo.name)
        d("Extension unloaded.")
        System.runFinalization()
        System.gc()
    }

}

Additionally, here is the full log https://pastebin.com/Jhpk4Wpc

Any idea what causes it?

  • 1
    I've found what was the issue, maybe someone will find it useful. The issue here is that if you decide the no longer use a classloader then you would normally null it so it gets garbage collected. Right? Wrong, if your loaded code interacted with UI in any way then unloading it will cause a native crash. The only way to solve it that I found is to keep a reference to the classloader even if you're done with it. If you have a similar issue feel free to message me. – Patryk Rogalski Jun 27 '18 at 19:18
  • hello, did you ever manage to solve this? I've been having the same issue for quite a while now it is driving me crazy since I couldn't find the exact cause for this. If you could possible provide an answer for your own question that would be wonderful ! – Amr El Aswar Aug 21 '19 at 20:35
  • I also noticed the native crash only occurs on some android versions but not all – Amr El Aswar Aug 21 '19 at 20:35
  • @Amroelaswar I never fully solved it but keeping the reference to that classloader so it's never garbage collected solved it. It's not perfect but in my case, that was enough. If you can provide me with more details about how you use it etc. then I might be able to help you. – Patryk Rogalski Aug 21 '19 at 22:20
  • I am using this the same way as you, allowing for plugins to be added to the app, the plugins weren't interacting with the UI however. The issue is it was really hard to pinpoint the exact cause of this since it doesn't happen on all devices. Apparently this is the only solution. I was looking through some solutions and came across a library for dynamic dex loading developed by Tencent and their solution to the problem was same as yours so I guess this is the only solution. – Amr El Aswar Aug 21 '19 at 22:31
  • here is a link if you are interested in taking a look : https://git.anw.cn/gitee/tinker/blob/6c099f8bd0075852d4702d72cbd8afd9ce242614/tinker-android/tinker-android-loader/src/main/java/com/tencent/tinker/loader/AndroidNClassLoader.java if you look on line 80 they are adding all the classloaders to an arraylist to avoid this issue and it is specified in a comment as well – Amr El Aswar Aug 21 '19 at 22:31

1 Answers1

0

Make sure you are not deleting the dex file.

Pankaj Kumar
  • 21
  • 2
  • 5