view src/main/java/org/icedrobot/daneel/loader/DaneelClassLoader.java @ 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 e47fc744d6b4
children cf71c6ab82fc
line wrap: on
line source

/*
 * Daneel - Dalvik to Java bytecode compiler
 * Copyright (C) 2011  IcedRobot team
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * This file is subject to the "Classpath" exception:
 *
 * Linking this library statically or dynamically with other modules is
 * making a combined work based on this library.  Thus, the terms and
 * conditions of the GNU General Public License cover the whole
 * combination.
 *
 * As a special exception, the copyright holders of this library give you
 * permission to link this library with independent modules to produce an
 * executable, regardless of the license terms of these independent
 * modules, and to copy and distribute the resulting executable under terms
 * of your choice, provided that you also meet, for each linked independent
 * module, the terms and conditions of the license of that module.  An
 * independent module is a module which is not derived from or based on
 * this library.  If you modify this library, you may extend this exception
 * to your version of the library, but you are not obligated to do so.  If
 * you do not wish to do so, delete this exception statement from your
 * version.
 */

package org.icedrobot.daneel.loader;

import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.zip.ZipEntry;

import org.icedrobot.daneel.dex.DexFile;
import org.icedrobot.daneel.rewriter.DexRewriter;

/**
 * A class loader capable of loading classes compiled for the Dalvik VM. The
 * classes are loaded by transforming them into the Java VM class file format
 * and then defining them in the host VM itself.
 */
public class DaneelClassLoader extends ClassLoader {
    private final File[] files;

    /** 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
     * around so that this class loader can be used as a system class loader.
     * For details take a look at the {@link ClassLoader#getSystemClassLoader()}
     * 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) throws IOException {
        this(parent, defaultFiles());
    }

    /**
     * Constructs a new class loader for a given set of files from which to load
     * classes and resources.
     * 
     * @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) throws IOException {
        super(parent);
        this.files = files.clone();
    }

    @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);
                }
            }
        }

        // Iterate over all dex files containing classes.
        for (DexFile dexFile : dexFiles) {
            try {
                byte[] bytecode = DexRewriter.rewrite(name, dexFile);
                return defineClass(name, bytecode, 0, bytecode.length);
            } catch (ClassNotFoundException e) {
                // Ignore and skip to the next file.
            }
        }

        // Unable to find class definition for given class name.
        throw new ClassNotFoundException(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) {
            ZipEntry entry = apk.getEntry(name);
            if (entry != null)
                return apk.getJarURL(entry);
        }

        // Unable to find resource for given resource name.
        return null;
    }

    /**
     * Helper method to get the default set of files to load from.
     */
    private static File[] defaultFiles() {
        String path = System.getProperty("daneel.class.path");
        if (path == null || path.isEmpty())
            return new File[0];
        String[] paths = path.split(File.pathSeparator);
        File[] files = new File[paths.length];
        for (int i = 0; i < paths.length; i++)
            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()]);
    }
}