Skip to content

Commit 8cfb514

Browse files
committed
fix(DexFactory): register injected proxy dex with a single class loader
Injecting a generated proxy into the app's PathClassLoader by opening it through a temporary DexClassLoader and splicing its dex element left the same DexFile claimed by two class loaders. ART rejects this unconditionally ("Attempt to register dex file ... with multiple class loaders"), but the second registration only materializes on non-debuggable builds, so release apps crashed on the first runtime-generated proxy while debug builds worked. Use BaseDexClassLoader.addDexPath (API 24+) on the target class loader instead. The dex element is built through the loader's own DexPathList with that loader as defining context, so the DexFile is registered exactly once. On API < 24, or if injection fails, fall back to the isolated DexClassLoader path (pre-#1951 behavior) instead of failing the subsequent loadClass with ClassNotFoundException. Adds tests covering the original FragmentFactory scenario: proxies generated at runtime (hidden from the static binding generator) must be resolvable via Class.forName through the app's class loader. Fixes #1962 Refs #1951
1 parent 9b45990 commit 8cfb514

3 files changed

Lines changed: 114 additions & 38 deletions

File tree

test-app/app/src/main/assets/app/mainpage.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ require("./tests/java-array-test");
5050
require("./tests/field-access-test");
5151
require("./tests/byte-buffer-test");
5252
require("./tests/dex-interface-implementation");
53+
require("./tests/testClassForNameDiscovery");
5354
require("./tests/testInterfaceImplementation");
5455
require("./tests/testRuntimeImplementedAPIs");
5556
require("./tests/testsInstanceOfOperator");
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
describe("Tests Class.forName discovery of runtime generated classes", function () {
2+
3+
// Android framework components (e.g. FragmentFactory) resolve classes with
4+
// Class.forName(className, false, context.getClassLoader()). Runtime generated
5+
// proxies must be discoverable through the app's class loader, otherwise
6+
// framework lookups crash with ClassNotFoundException (see issue #1962 / PR #1951).
7+
//
8+
// The extend calls below are built dynamically so the static binding generator
9+
// cannot pre-generate the proxies and DexFactory.resolveClass takes the runtime
10+
// generation + parent class loader injection path.
11+
var ext = "ex" + "tend";
12+
13+
// dex injection into the parent class loader requires BaseDexClassLoader.addDexPath (API 24+)
14+
var supportsInjection = android.os.Build.VERSION.SDK_INT >= 24;
15+
// the same class loader the framework uses, e.g. in FragmentFactory.loadFragmentClass
16+
var appClassLoader = com.tns.Runtime.getCurrentRuntime().getContext().getClassLoader();
17+
18+
it("When_extending_a_class_at_runtime_it_should_be_discoverable_through_the_app_class_loader", function () {
19+
if (!supportsInjection) {
20+
return;
21+
}
22+
23+
var MyObject = java.lang.Object[ext]("ClassForNameDiscoveryObject", {
24+
toString: function () {
25+
return "discoverable";
26+
}
27+
});
28+
29+
var instance = new MyObject();
30+
var className = instance.getClass().getName();
31+
32+
var found = java.lang.Class.forName(className, false, appClassLoader);
33+
34+
expect(found.getName()).toBe(className);
35+
expect(found.equals(instance.getClass())).toBe(true);
36+
});
37+
38+
it("When_implementing_an_interface_at_runtime_it_should_be_discoverable_through_the_app_class_loader", function () {
39+
if (!supportsInjection) {
40+
return;
41+
}
42+
43+
var MyRunnable = java.lang.Runnable[ext]("ClassForNameDiscoveryRunnable", {
44+
run: function () {
45+
}
46+
});
47+
48+
var instance = new MyRunnable();
49+
var className = instance.getClass().getName();
50+
51+
var found = java.lang.Class.forName(className, false, appClassLoader);
52+
53+
expect(found.getName()).toBe(className);
54+
expect(found.equals(instance.getClass())).toBe(true);
55+
});
56+
57+
it("When_a_runtime_generated_class_is_instantiated_through_reflection_it_should_dispatch_to_the_JS_implementation", function () {
58+
if (!supportsInjection) {
59+
return;
60+
}
61+
62+
var MyObject = java.lang.Object[ext]("ClassForNameDiscoveryInstantiated", {
63+
toString: function () {
64+
return "created via reflection";
65+
}
66+
});
67+
68+
// make sure the implementation object is registered before Java constructs an instance
69+
var instance = new MyObject();
70+
var className = instance.getClass().getName();
71+
72+
// FragmentFactory resolves the class by name and instantiates it through reflection
73+
var found = java.lang.Class.forName(className, false, appClassLoader);
74+
var created = found.getDeclaredConstructor().newInstance();
75+
76+
expect(created.toString()).toBe("created via reflection");
77+
});
78+
});

test-app/runtime/src/main/java/com/tns/DexFactory.java

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.tns;
22

3+
import android.os.Build;
34
import android.util.Log;
45

56
import com.tns.bindings.AnnotationDescriptor;
@@ -18,8 +19,7 @@
1819
import java.io.InputStreamReader;
1920
import java.io.InvalidClassException;
2021
import java.io.OutputStreamWriter;
21-
import java.lang.reflect.Array;
22-
import java.lang.reflect.Field;
22+
import java.lang.reflect.Method;
2323
import java.util.HashMap;
2424
import java.util.HashSet;
2525
import java.util.zip.ZipEntry;
@@ -166,13 +166,19 @@ public Class<?> resolveClass(String baseClassName, String name, String className
166166
}
167167
jarFile.setReadOnly();
168168

169-
Class<?> result;
169+
Class<?> result = null;
170170
String classNameToLoad = isInterface ? fullClassName : desiredDexClassName;
171171

172-
if (injectIntoParentClassLoader && classLoader instanceof BaseDexClassLoader) {
173-
injectDexIntoClassLoader((BaseDexClassLoader) classLoader, jarFilePath);
174-
result = classLoader.loadClass(classNameToLoad);
175-
} else {
172+
if (injectIntoParentClassLoader && classLoader instanceof BaseDexClassLoader
173+
&& injectDexIntoClassLoader((BaseDexClassLoader) classLoader, jarFilePath)) {
174+
try {
175+
result = classLoader.loadClass(classNameToLoad);
176+
} catch (ClassNotFoundException e) {
177+
// fall through to the isolated DexClassLoader below
178+
}
179+
}
180+
181+
if (result == null) {
176182
DexClassLoader dexClassLoader = new DexClassLoader(jarFilePath, this.odexDir.getAbsolutePath(), null, classLoader);
177183
result = dexClassLoader.loadClass(classNameToLoad);
178184
}
@@ -389,40 +395,31 @@ private String getCachedProxyThumb(File proxyDir) {
389395
* (e.g. FragmentFactory) use Class.forName() to instantiate classes by name, but
390396
* NativeScript's dynamically-generated classes normally live in isolated DexClassLoaders
391397
* that Class.forName() doesn't search.
398+
*
399+
* The jar must be added through the target class loader's own DexPathList so that
400+
* the resulting DexFile has the PathClassLoader as its only owner. Opening the jar
401+
* through a separate DexClassLoader first and splicing its dex element would leave
402+
* the same DexFile claimed by two loaders, which ART rejects on non-debuggable
403+
* builds with "Attempt to register dex file ... with multiple class loaders".
404+
*
405+
* @return true if the jar was injected and the class can be loaded through the
406+
* target class loader, false if the caller should fall back to an
407+
* isolated DexClassLoader.
392408
*/
393-
private void injectDexIntoClassLoader(BaseDexClassLoader targetClassLoader, String jarFilePath) {
394-
try {
395-
// Create a temporary DexClassLoader to produce the optimized dex
396-
DexClassLoader tempLoader = new DexClassLoader(jarFilePath, this.odexDir.getAbsolutePath(), null, targetClassLoader);
397-
398-
// Get pathList from both classloaders
399-
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
400-
pathListField.setAccessible(true);
401-
402-
Object targetPathList = pathListField.get(targetClassLoader);
403-
Object sourcePathList = pathListField.get(tempLoader);
404-
405-
// Get dexElements from both pathLists
406-
Field dexElementsField = targetPathList.getClass().getDeclaredField("dexElements");
407-
dexElementsField.setAccessible(true);
408-
409-
Object targetElements = dexElementsField.get(targetPathList);
410-
Object sourceElements = dexElementsField.get(sourcePathList);
411-
412-
int targetLen = Array.getLength(targetElements);
413-
int sourceLen = Array.getLength(sourceElements);
414-
415-
// Create merged array: target + source
416-
Object merged = Array.newInstance(targetElements.getClass().getComponentType(), targetLen + sourceLen);
417-
System.arraycopy(targetElements, 0, merged, 0, targetLen);
418-
System.arraycopy(sourceElements, 0, merged, targetLen, sourceLen);
409+
private boolean injectDexIntoClassLoader(BaseDexClassLoader targetClassLoader, String jarFilePath) {
410+
// BaseDexClassLoader.addDexPath exists since API 24
411+
if (Build.VERSION.SDK_INT < 24) {
412+
return false;
413+
}
419414

420-
dexElementsField.set(targetPathList, merged);
415+
try {
416+
Method addDexPath = BaseDexClassLoader.class.getDeclaredMethod("addDexPath", String.class);
417+
addDexPath.setAccessible(true);
418+
addDexPath.invoke(targetClassLoader, jarFilePath);
419+
return true;
421420
} catch (Exception e) {
422-
if (logger.isEnabled()) {
423-
logger.write("Failed to inject dex into parent classloader: " + e.getMessage());
424-
}
425-
// Non-fatal: class will still be loadable via the ClassStorageService fallback
421+
Log.w("JS", "Failed to inject dex into parent classloader: " + e);
422+
return false;
426423
}
427424
}
428425
}

0 commit comments

Comments
 (0)