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?