changeset 92:eafd801c8fa0

Refactor DaneelClassLoader to load DEX file lazily when a class is requested instead in the constructor. This patch also loads APK lazily when getResource is called.
author forax
date Mon, 28 Mar 2011 00:45:02 +0200
parents 2711ac37fdfb
children 52fb46737ce6
files src/main/java/org/icedrobot/daneel/dex/DexFile.java src/main/java/org/icedrobot/daneel/loader/ApkFile.java src/main/java/org/icedrobot/daneel/loader/DaneelClassLoader.java
diffstat 3 files changed, 139 insertions(+), 53 deletions(-) [+]
line wrap: on
line diff
--- a/src/main/java/org/icedrobot/daneel/dex/DexFile.java	Mon Mar 28 00:29:55 2011 +0200
+++ b/src/main/java/org/icedrobot/daneel/dex/DexFile.java	Mon Mar 28 00:45:02 2011 +0200
@@ -39,10 +39,13 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.nio.channels.Channels;
 import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
@@ -58,16 +61,53 @@
     /** Constant used to indicate that an index value is absent. */
     public static final int NO_INDEX = 0xffffffff;
 
-    public static DexFile parse(ByteBuffer buf) {
-        return new DexFile(buf);
+    
+    /**
+     * Create a DEX file from a buffer of bytes.
+     * 
+     * @param buffer a buffer of bytes containing classes encoded using the DEX format.
+     * @return a DEX buffer
+     */
+    public static DexFile parse(ByteBuffer buffer) {
+        return new DexFile(buffer);
     }
 
+    /**
+     * Create a DEX file from a file by reading its content.
+     * The file should be encoded using the DEX file format.
+     *  
+     * @param file a file.
+     * @return a DEX file
+     * @throws IOException if the file can't be read.
+     */
     public static DexFile parse(File file) throws IOException {
         ByteBuffer buffer = (new RandomAccessFile(file, "r")).getChannel().map(
                 FileChannel.MapMode.READ_ONLY, 0, file.length());
         return new DexFile(buffer);
     }
 
+    /**
+     * Create a DEX file from an input stream and a size.
+     * The input stream should be encoded using the DEX file format.
+     *  
+     * @param inputStream an input stream
+     * @param size the size of the content of the input stream.
+     * @return a DEX file
+     * @throws IOException if the content of the input stream can't be read.
+     */
+    public static DexFile parse(InputStream inputStream, long size) throws IOException {
+        if (size > Integer.MAX_VALUE)
+            throw new IllegalArgumentException("Oversized DEX file detected.");
+        
+        ByteBuffer buffer = ByteBuffer.allocate((int)size);
+        ReadableByteChannel channel = Channels.newChannel(inputStream);
+        while (buffer.hasRemaining()) {
+            channel.read(buffer);
+        }
+        buffer.clear();
+        return new DexFile(buffer);
+    }
+    
     public static DexFile parse(byte[] bytes) {
         return new DexFile(ByteBuffer.wrap(bytes));
     }
--- a/src/main/java/org/icedrobot/daneel/loader/ApkFile.java	Mon Mar 28 00:29:55 2011 +0200
+++ b/src/main/java/org/icedrobot/daneel/loader/ApkFile.java	Mon Mar 28 00:45:02 2011 +0200
@@ -41,8 +41,6 @@
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.nio.ByteBuffer;
-import java.nio.channels.Channels;
 import java.util.jar.JarFile;
 import java.util.zip.ZipEntry;
 
@@ -83,17 +81,14 @@
      * Returns the DEX file containing all the classes of this APK file.
      * @return The contained DEX file object.
      * @throws IOException In case of an error while accessing the file.
+     * @throws IllegalStateException if the APK doesn't contains any classes.
      */
-    public DexFile getClasses() throws IOException {
+    public DexFile getDexFile() throws IOException {
         ZipEntry entry = getEntry("classes.dex");
         if (entry == null)
-            throw new DaneelException("The APK doesn't contain classes.");
-        if (entry.getSize() > Integer.MAX_VALUE)
-            throw new DaneelException("Oversized DEX file detected.");
-        ByteBuffer buffer = ByteBuffer.allocate((int) entry.getSize());
-        Channels.newChannel(getInputStream(entry)).read(buffer);
-        buffer.rewind();
-        return DexFile.parse(buffer);
+            throw new IllegalStateException("The APK doesn't contain classes.");
+        
+        return DexFile.parse(getInputStream(entry), entry.getSize());
     }
 
     /**
--- a/src/main/java/org/icedrobot/daneel/loader/DaneelClassLoader.java	Mon Mar 28 00:29:55 2011 +0200
+++ b/src/main/java/org/icedrobot/daneel/loader/DaneelClassLoader.java	Mon Mar 28 00:45:02 2011 +0200
@@ -38,13 +38,12 @@
 package org.icedrobot.daneel.loader;
 
 import java.io.File;
+import java.io.IOError;
 import java.io.IOException;
 import java.net.URL;
-import java.util.LinkedList;
-import java.util.List;
+import java.util.ArrayList;
 import java.util.zip.ZipEntry;
 
-import org.icedrobot.daneel.DaneelException;
 import org.icedrobot.daneel.dex.DexFile;
 import org.icedrobot.daneel.rewriter.DexRewriter;
 
@@ -54,12 +53,19 @@
  * and then defining them in the host VM itself.
  */
 public class DaneelClassLoader extends ClassLoader {
+    private final File[] files;
 
-    /** The list of all files containing classes. */
-    private final DexFile[] classFiles;
-
-    /** The list of all files containing resources. */
-    private final ApkFile[] resourceFiles;
+    /** The list of all files containing DEX classes (lazy initialized). */
+    private DexFile[] dexFiles;
+    
+    /** Lock used to lazy initialize dexFiles */
+    private final Object dexLock = new Object();
+    
+    /** The list of all files containing resources (lazy initialized). */
+    private ApkFile[] resourceFiles;
+    
+    /** Lock used to lazy initialize resourceFiles */
+    private final Object resourcesLock = new Object();
 
     /**
      * Constructs a new class loader. Keep a constructor with such a signature
@@ -68,8 +74,9 @@
      * documentation.
      * 
      * @param parent The parent class loader for delegation.
+     * @throws IOException throws if an I/O error occurs while reading a DEX files
      */
-    public DaneelClassLoader(ClassLoader parent) {
+    public DaneelClassLoader(ClassLoader parent) throws IOException {
         this(parent, defaultFiles());
     }
 
@@ -79,43 +86,37 @@
      * 
      * @param parent The parent class loader for delegation.
      * @param files The set of files from which to load.
+     * @throws IOException throws if an I/O error occurs while reading a DEX files
      */
-    public DaneelClassLoader(ClassLoader parent, File[] files) {
+    public DaneelClassLoader(ClassLoader parent, File... files) throws IOException {
         super(parent);
+        this.files = files.clone();
+    }
 
-        // Split the given files into APK and DEX files.
-        List<DexFile> dexs = new LinkedList<DexFile>();
-        List<ApkFile> apks = new LinkedList<ApkFile>();
-        for (File file : files) {
-            try {
-                if (file.getName().endsWith(".apk")) {
-                    ApkFile apk = new ApkFile(file);
-                    dexs.add(apk.getClasses());
-                    apks.add(apk);
-                } else {
-                    DexFile dex = DexFile.parse(file);
-                    dexs.add(dex);
+    @Override
+    protected Class<?> findClass(String name) throws ClassNotFoundException {
+        System.out.printf("Trying to find class '%s' ...\n", name);
+        
+        DexFile[] dexFiles;
+        synchronized(dexLock) {
+            dexFiles = this.dexFiles;
+            if (dexFiles == null) {
+                // avoid an infinite loop if loadDexFiles() requires to load a class
+                // that isn't available in parent classloaders
+                this.dexFiles = new DexFile[0];
+                
+                try {
+                    this.dexFiles = dexFiles = loadDexFiles(files);
+                } catch (IOException e) {
+                    throw new IOError(e);
                 }
-            } catch (IOException e) {
-                throw new DaneelException("Unable to setup class loader.", e);
             }
         }
 
-        classFiles = dexs.toArray(new DexFile[dexs.size()]);
-        resourceFiles = apks.toArray(new ApkFile[apks.size()]);
-    }
-
-    /**
-     * Method which tries to find a class for the given name.
-     */
-    @Override
-    protected Class<?> findClass(String name) throws ClassNotFoundException {
-        System.out.printf("Trying to find class '%s' ...\n", name);
-
-        // Iterate over all files containing classes.
-        for (DexFile dex : classFiles) {
+        // Iterate over all dex files containing classes.
+        for (DexFile dexFile : dexFiles) {
             try {
-                byte[] bytecode = DexRewriter.rewrite(name, dex);
+                byte[] bytecode = DexRewriter.rewrite(name, dexFile);
                 return defineClass(name, bytecode, 0, bytecode.length);
             } catch (ClassNotFoundException e) {
                 // Ignore and skip to the next file.
@@ -126,12 +127,24 @@
         throw new ClassNotFoundException(name);
     }
 
-    /**
-     * Method which tries to find a resource for the given name.
-     */
     @Override
     protected URL findResource(String name) {
         System.out.printf("Trying to find resource '%s' ...\n", name);
+        
+        ApkFile[] resourceFiles;
+        synchronized(resourcesLock) {
+            resourceFiles = this.resourceFiles;
+            if (resourceFiles == null) {
+                // avoid an infinite loop if findResourceFiles() calls
+                // findResource() on the current classloader
+                this.resourceFiles = new ApkFile[0];
+                try {
+                    this.resourceFiles = resourceFiles = findResourceFiles(files);
+                } catch (IOException e) {
+                    throw new IOError(e);
+                }
+            }
+        }
 
         // Iterate over all files containing resources.
         for (ApkFile apk : resourceFiles) {
@@ -157,4 +170,42 @@
             files[i] = new File(paths[i]);
         return files;
     }
+    
+    /** 
+     * Filter an array of files to find all DEX files. 
+     * @param files an array of files.
+     * @return an array of DEX files.
+     * @throws IOException
+     */
+    private static DexFile[] loadDexFiles(File[] files) throws IOException {
+        ArrayList<DexFile> dexs = new ArrayList<DexFile>(files.length);
+        for (File file : files) {
+            DexFile dexFile;
+            if (file.getName().endsWith(".apk")) {
+                ApkFile apk = new ApkFile(file);
+                dexFile = apk.getDexFile();
+            } else {
+                dexFile = DexFile.parse(file);
+            }
+            dexs.add(dexFile);
+        }
+        return dexs.toArray(new DexFile[dexs.size()]);
+    }
+    
+    /** 
+     * Filter an array of files to find all APK files. 
+     * @param files an array of files.
+     * @return an array of APK files.
+     * @throws IOException
+     */
+    private static ApkFile[] findResourceFiles(File[] files) throws IOException {
+        ArrayList<ApkFile> apks = new ArrayList<ApkFile>(files.length);
+        for (File file : files) {
+            if (file.getName().endsWith(".apk")) {
+                ApkFile apk = new ApkFile(file);
+                apks.add(apk);
+            }
+        }
+        return apks.toArray(new ApkFile[apks.size()]);
+    }
 }