|
1 | 1 | package com.tns; |
2 | 2 |
|
| 3 | +import android.os.Build; |
3 | 4 | import android.util.Log; |
4 | 5 |
|
5 | 6 | import com.tns.bindings.AnnotationDescriptor; |
|
20 | 21 | import java.io.OutputStreamWriter; |
21 | 22 | import java.lang.reflect.Array; |
22 | 23 | import java.lang.reflect.Field; |
| 24 | +import java.lang.reflect.Method; |
| 25 | +import java.util.ArrayList; |
23 | 26 | import java.util.HashMap; |
24 | 27 | import java.util.HashSet; |
| 28 | +import java.util.List; |
25 | 29 | import java.util.zip.ZipEntry; |
26 | 30 | import java.util.zip.ZipOutputStream; |
27 | 31 |
|
@@ -166,13 +170,19 @@ public Class<?> resolveClass(String baseClassName, String name, String className |
166 | 170 | } |
167 | 171 | jarFile.setReadOnly(); |
168 | 172 |
|
169 | | - Class<?> result; |
| 173 | + Class<?> result = null; |
170 | 174 | String classNameToLoad = isInterface ? fullClassName : desiredDexClassName; |
171 | 175 |
|
172 | | - if (injectIntoParentClassLoader && classLoader instanceof BaseDexClassLoader) { |
173 | | - injectDexIntoClassLoader((BaseDexClassLoader) classLoader, jarFilePath); |
174 | | - result = classLoader.loadClass(classNameToLoad); |
175 | | - } else { |
| 176 | + if (injectIntoParentClassLoader && classLoader instanceof BaseDexClassLoader |
| 177 | + && injectDexIntoClassLoader((BaseDexClassLoader) classLoader, jarFilePath)) { |
| 178 | + try { |
| 179 | + result = classLoader.loadClass(classNameToLoad); |
| 180 | + } catch (ClassNotFoundException e) { |
| 181 | + // fall through to the isolated DexClassLoader below |
| 182 | + } |
| 183 | + } |
| 184 | + |
| 185 | + if (result == null) { |
176 | 186 | DexClassLoader dexClassLoader = new DexClassLoader(jarFilePath, this.odexDir.getAbsolutePath(), null, classLoader); |
177 | 187 | result = dexClassLoader.loadClass(classNameToLoad); |
178 | 188 | } |
@@ -389,40 +399,74 @@ private String getCachedProxyThumb(File proxyDir) { |
389 | 399 | * (e.g. FragmentFactory) use Class.forName() to instantiate classes by name, but |
390 | 400 | * NativeScript's dynamically-generated classes normally live in isolated DexClassLoaders |
391 | 401 | * that Class.forName() doesn't search. |
| 402 | + * |
| 403 | + * The jar must be added through the target class loader's own DexPathList so that |
| 404 | + * the resulting DexFile has the PathClassLoader as its only owner. Opening the jar |
| 405 | + * through a separate DexClassLoader first and splicing its dex element would leave |
| 406 | + * the same DexFile claimed by two loaders, which ART rejects on non-debuggable |
| 407 | + * builds with "Attempt to register dex file ... with multiple class loaders". |
| 408 | + * |
| 409 | + * @return true if the jar was injected and the class can be loaded through the |
| 410 | + * target class loader, false if the caller should fall back to an |
| 411 | + * isolated DexClassLoader. |
392 | 412 | */ |
393 | | - private void injectDexIntoClassLoader(BaseDexClassLoader targetClassLoader, String jarFilePath) { |
| 413 | + private boolean injectDexIntoClassLoader(BaseDexClassLoader targetClassLoader, String jarFilePath) { |
394 | 414 | 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); |
| 415 | + if (Build.VERSION.SDK_INT >= 24) { |
| 416 | + // BaseDexClassLoader.addDexPath exists since API 24 |
| 417 | + Method addDexPath = BaseDexClassLoader.class.getDeclaredMethod("addDexPath", String.class); |
| 418 | + addDexPath.setAccessible(true); |
| 419 | + addDexPath.invoke(targetClassLoader, jarFilePath); |
| 420 | + } else { |
| 421 | + appendDexElements(targetClassLoader, jarFilePath); |
| 422 | + } |
| 423 | + return true; |
| 424 | + } catch (Exception e) { |
| 425 | + Log.w("JS", "Failed to inject dex into parent classloader: " + e); |
| 426 | + return false; |
| 427 | + } |
| 428 | + } |
404 | 429 |
|
405 | | - // Get dexElements from both pathLists |
406 | | - Field dexElementsField = targetPathList.getClass().getDeclaredField("dexElements"); |
407 | | - dexElementsField.setAccessible(true); |
| 430 | + /** |
| 431 | + * Pre API 24 equivalent of BaseDexClassLoader.addDexPath: builds the dex elements |
| 432 | + * through DexPathList's static factory methods (so no temporary class loader is |
| 433 | + * involved) and splices them into the target loader's dexElements array. This is |
| 434 | + * the same technique MultiDex used on these OS versions. |
| 435 | + */ |
| 436 | + private void appendDexElements(BaseDexClassLoader targetClassLoader, String jarFilePath) throws Exception { |
| 437 | + Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList"); |
| 438 | + pathListField.setAccessible(true); |
| 439 | + Object pathList = pathListField.get(targetClassLoader); |
| 440 | + |
| 441 | + ArrayList<File> files = new ArrayList<File>(); |
| 442 | + files.add(new File(jarFilePath)); |
| 443 | + ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); |
| 444 | + |
| 445 | + Object newElements; |
| 446 | + if (Build.VERSION.SDK_INT >= 23) { |
| 447 | + Method makePathElements = pathList.getClass().getDeclaredMethod("makePathElements", List.class, File.class, List.class); |
| 448 | + makePathElements.setAccessible(true); |
| 449 | + newElements = makePathElements.invoke(null, files, this.odexDir, suppressedExceptions); |
| 450 | + } else { |
| 451 | + Method makeDexElements = pathList.getClass().getDeclaredMethod("makeDexElements", ArrayList.class, File.class, ArrayList.class); |
| 452 | + makeDexElements.setAccessible(true); |
| 453 | + newElements = makeDexElements.invoke(null, files, this.odexDir, suppressedExceptions); |
| 454 | + } |
408 | 455 |
|
409 | | - Object targetElements = dexElementsField.get(targetPathList); |
410 | | - Object sourceElements = dexElementsField.get(sourcePathList); |
| 456 | + if (!suppressedExceptions.isEmpty()) { |
| 457 | + throw suppressedExceptions.get(0); |
| 458 | + } |
411 | 459 |
|
412 | | - int targetLen = Array.getLength(targetElements); |
413 | | - int sourceLen = Array.getLength(sourceElements); |
| 460 | + Field dexElementsField = pathList.getClass().getDeclaredField("dexElements"); |
| 461 | + dexElementsField.setAccessible(true); |
| 462 | + Object oldElements = dexElementsField.get(pathList); |
414 | 463 |
|
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); |
| 464 | + int oldLen = Array.getLength(oldElements); |
| 465 | + int newLen = Array.getLength(newElements); |
| 466 | + Object merged = Array.newInstance(oldElements.getClass().getComponentType(), oldLen + newLen); |
| 467 | + System.arraycopy(oldElements, 0, merged, 0, oldLen); |
| 468 | + System.arraycopy(newElements, 0, merged, oldLen, newLen); |
419 | 469 |
|
420 | | - dexElementsField.set(targetPathList, merged); |
421 | | - } 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 |
426 | | - } |
| 470 | + dexElementsField.set(pathList, merged); |
427 | 471 | } |
428 | 472 | } |
0 commit comments