Android 动态加载机制基础-ClassLoader

2016-05-19  本文已影响1169人  shuixingge

本文仅为学习笔记;不是原创文章

动态加载的关键问题
ClassLoader机制
ClassLoader概念:Java代码都是写在Class里面的,程序运行在虚拟机上时,虚拟机需要把需要的Class加载进来才能创建实例对象并工作,而完成这一个加载工作的角色就是ClassLoader。
ClassLoader分类:

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ClassLoader classLoader = getClassLoader();
        if (classLoader != null){
            Log.i(TAG, "[onCreate] classLoader " + i + " : " + classLoader.toString());
            while (classLoader.getParent()!=null){
                classLoader = classLoader.getParent();
                Log.i(TAG,"[onCreate] classLoader " + i + " : " + classLoader.toString());
            }
        }
    }

输出结果为

[onCreate] classLoader 1 : dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/me.kaede.anroidclassloadersample-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
 
[onCreate] classLoader 2 : java.lang.BootClassLoader@14af4e32

2个Classloader实例:
一个是BootClassLoader(系统启动的时候创建的);
另一个是PathClassLoader (应用启动时创建的,用于加载“/data/app/me.kaede.anroidclassloadersample-1/base.apk”里面的类)。

创建ClassLoader: 需要使用一个现有的ClassLoader实例作为新创建的实例的Parent;复合ClassLoader的双亲委派机制(Parent-Delegation Model)的特点

 /*
     * constructor for the BootClassLoader which needs parent to be null.
     */
    ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
        if (parentLoader == null && !nullAllowed) {
            throw new NullPointerException("parentLoader == null && !nullAllowed");
        }
        parent = parentLoader;
    }

ClassLoader双亲代理模型加载类的特点和作用
JVM中ClassLoader通过defineClass方法加载jar里面的Class,而Android中这个方法被弃用了。

@Deprecated
    protected final Class<?> defineClass(byte[] classRep, int offset, int length)
            throws ClassFormatError {
        throw new UnsupportedOperationException("can't load this type of class file");
    }

取而代之的是loadClass方法

public Class<?> loadClass(String className) throws ClassNotFoundException {
        return loadClass(className, false);
    }
 
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);
 
        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }
 
            if (clazz == null) {
                try {
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }
 
        return clazz;
    }

1 会先查询当前ClassLoader实例是否加载过此类,有就返回;
2 如果没有。查询Parent是否已经加载过此类,如果已经加载过,就直接返回Parent加载的类;
3 如果继承路线上的ClassLoader都没有加载,才由Child执行类的加载工作;
双亲委派模型优势:
共享功能 : 一些Framework层级的类一旦被顶层的ClassLoader加载过就缓存在内存里面,以后任何地方用到都不需要重新加载。
隔离功能:不同继承路线上的ClassLoader加载的类肯定不是同一个类,这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况;一些系统层级的类会在系统初始化的时候被加载,比如java.lang.String,如果在一个应用里面能够简单地用自定义的String类把这个系统的String类给替换掉,那将会有严重的安全问题。

ClassLoader 隔离问题:
JVM 及 Dalvik 对类唯一的识别是 ClassLoader id + PackageName + ClassName,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的。并且如果这两个”类”不是由一个 ClassLoader 加载,是无法将一个类的示例强转为另外一个类的,这就是 ClassLoader 隔离。 如 Android 中碰到如下异常;
同一个Class = 相同的 ClassName + PackageName + ClassLoader;

Java.lang.ClassCastException: android.support.v4.view.ViewPager can not be cast to android.support.v4.view.ViewPager

通过instance.getClass().getClassLoader()来判断加载类的ClassLoader是否一致;

DexClassLoader 和 PathClassLoader:(extends BaseDexClassLoader)
DexClassLoader :可以加载文件系统上的jar、dex、apk;可以从SD卡中加载未安装的apk
PathClassLoader :可以加载/data/app目录下的apk,这也意味着,它只能加载已经安装的apk;
URLClassLoader :可以加载java中的jar,但是由于dalvik不能直接识别jar,所以此方法在Android中无法使用;

** DexClassLoader和PathClassLoader的区别在于PathClassLoader的optimizedDirectory指定为空**

// DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}
 
// PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
     
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

BaseDexClassLoader

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);

DexPathList

public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        ……
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
    }
 
    private static Element[] makeDexElements(ArrayList<File> files,
            File optimizedDirectory) {
        ArrayList<Element> elements = new ArrayList<Element>();
        for (File file : files) {
            ZipFile zip = null;
            DexFile dex = null;
            String name = file.getName();
            if (name.endsWith(DEX_SUFFIX)) {
                dex = loadDexFile(file, optimizedDirectory);
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
                zip = new ZipFile(file);
            } 
            ……
            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, zip, dex));
            }
        }
        return elements.toArray(new Element[elements.size()]);
    }
     
    private static DexFile loadDexFile(File file, File optimizedDirectory)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0);
        }
    }
     
    /**
     * Converts a dex/jar file path and an output directory to an
     * output file path for an associated optimized dex file.
     */
    private static String optimizedPathFor(File path,
            File optimizedDirectory) {
        String fileName = path.getName();
        if (!fileName.endsWith(DEX_SUFFIX)) {
            int lastDot = fileName.lastIndexOf(".");
            if (lastDot < 0) {
                fileName += DEX_SUFFIX;
            } else {
                StringBuilder sb = new StringBuilder(lastDot + 4);
                sb.append(fileName, 0, lastDot);
                sb.append(DEX_SUFFIX);
                fileName = sb.toString();
            }
        }
        File result = new File(optimizedDirectory, fileName);
        return result.getPath();
    }

optimizedDirectory是用来缓存我们需要加载的dex文件的,并创建一个DexFile对象,如果它为null,那么会直接使用dex文件原有的路径来创建DexFile;
DexClassLoader可以指定自己的optimizedDirectory,所以它可以加载外部的dex,因为这个dex会被复制到内部路径的optimizedDirectory;而PathClassLoader没有optimizedDirectory,所以它只能加载内部的dex;

加载类的过程:
**第一步: **会先查询当前ClassLoader实例是否加载过此类,有就返回;
**第二步: **如果没有;查询Parent是否已经加载过此类,如果已经加载过,就直接返回Parent加载的类;


public Class<?> loadClass(String className) throws ClassNotFoundException {
        return loadClass(className, false);
    }
 
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);
        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }
 
            if (clazz == null) {
                try {
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }
        return clazz;
    }

BaseDexClassLoader.findClass()

@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

调用了DexPathList的findClass:遍历了之前所有的DexFile实例,其实也就是遍历了所有加载过的dex文件,再调用loadClassBinaryName方法一个个尝试能不能加载想要的类

DexPathList.findClass()

public Class findClass(String name) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        return null;
    }

DexFile.loadClassBinaryName()

public Class loadClassBinaryName(String name, ClassLoader loader) {
        return defineClass(name, loader, mCookie);
    }
    private native static Class defineClass(String name, ClassLoader loader, int cookie);

动态加载的几个关键问题
资源访问:无法找到某某id所对应的资源
因为将apk加载到宿主程序中去执行,就无法通过宿主程序的Context去取到apk中的资源,比如图片、文本等,这是很好理解的,因为apk已经不存在上下文了,它执行时所采用的上下文是宿主程序的上下文,用别人的Context是无法得到自己的资源的;
解决方案一:插件中的资源在宿主程序中也预置一份;
缺点:增加了宿主apk的大小;在这种模式下,每次发布一个插件都需要将资源复制到宿主程序中,这意味着每发布一个插件都要更新一下宿主程序;
解决方案二:将插件中的资源解压出来,然后通过文件流去读取资源;
缺点:实际操作起来还是有很大难度的。首先不同资源有不同的文件流格式,比如图片、XML等,其次针对不同设备加载的资源可能是不一样的,如何选择合适的资源也是一个需要解决的问题;
实际解决方案:
Activity中有一个叫mBase的成员变量,它的类型就是ContextImpl。注意到Context中有如下两个抽象方法,看起来是和资源有关的,实际上Context就是通过它们来获取资源的。这两个抽象方法的真正实现在ContextImpl中;

/** Return an AssetManager instance for your application's package. */
    public abstract AssetManager getAssets();

    /** Return a Resources instance for your application's package. */
    public abstract Resources getResources();

具体实现

 protected void loadResources() {
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        addAssetPath.invoke(assetManager, mDexPath);
        mAssetManager = assetManager;
    } catch (Exception e) {
        e.printStackTrace();
    }
    Resources superRes = super.getResources();
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),
            superRes.getConfiguration());
    mTheme = mResources.newTheme();
    mTheme.setTo(super.getTheme());
}

加载资源的方法是通过反射,通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources对象中,由于addAssetPath是隐藏API我们无法直接调用,所以只能通过反射。
addAssetPath();

@hide
    public final int addAssetPath(String path) {

    synchronized (this) {

        int res = addAssetPathNative(path);

        makeStringBlocks(mStringBlocks);

        return res;

    }

}

** Activity生命周期的管理:**
反射方式和接口方式。
反射的方式很好理解,首先通过Java的反射去获取Activity的各种生命周期方法,比如onCreate、onStart、onResume等,然后在代理Activity中去调用插件Activity对应的生命周期方法即可;
缺点:一方面是反射代码写起来比较复杂,另一方面是过多使用反射会有一定的性能开销。

反射方式

@Override

protected void onResume() {

    super.onResume();

    Method onResume = mActivityLifecircleMethods.get("onResume");

    if (onResume != null) {

        try {

            onResume.invoke(mRemoteActivity, new Object[] { });

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

}


@Override

protected void onPause() {

    Method onPause = mActivityLifecircleMethods.get("onPause");

    if (onPause != null) {

        try {

            onPause.invoke(mRemoteActivity, new Object[] { });

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

    super.onPause();

}

接口方式

public interface DLPlugin {

    public void onStart();

    public void onRestart();

    public void onActivityResult(int requestCode, int resultCode, Intent

    data);

    public void onResume();

    public void onPause();

    public void onStop();

    public void onDestroy();

    public void onCreate(Bundle savedInstanceState);

    public void setProxy(Activity proxyActivity, String dexPath);

    public void onSaveInstanceState(Bundle outState);

    public void onNewIntent(Intent intent);

    public void onRestoreInstanceState(Bundle savedInstanceState);

    public boolean onTouchEvent(MotionEvent event);

    public boolean onKeyUp(int keyCode, KeyEvent event);

    public void onWindowAttributesChanged(LayoutParams params);

    public void onWindowFocusChanged(boolean hasFocus);

    public void onBackPressed();

…

}

代理Activity中只需要按如下方式即可调用插件Activity的生命周期方法,这就完成了插件Activity的生命周期的管理;插件Activity需要实现DLPlugin接口;

@Override

protected void onStart() {

    mRemoteActivity.onStart();

    super.onStart();

}


@Override

protected void onRestart() {

    mRemoteActivity.onRestart();

    super.onRestart();

}


@Override

protected void onResume() {

    mRemoteActivity.onResume();

    super.onResume();

}

插件ClassLoader的管理
为了更好地对多插件进行支持,需要合理地去管理各个插件的DexClassLoader,这样同一个插件就可以采用同一个ClassLoader去加载类,从而避免了多个ClassLoader加载同一个类时所引发的类型转换错误;通过将不同插件的ClassLoader存储在一个HashMap中,这样就可以保证不同插件中的类彼此互不干扰;

public class DLClassLoader extends DexClassLoader {

    private static final String TAG = "DLClassLoader";


    private static final HashMap<String, DLClassLoader> mPluginClassLoaders

    = new HashMap<String, DLClassLoader>();


    protected DLClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {

        super(dexPath, optimizedDirectory, libraryPath, parent);

    }


    /**

     * return a available classloader which belongs to different apk

     */

    public static DLClassLoader getClassLoader(String dexPath, Context

    context, ClassLoader parentLoader) {

        DLClassLoader dLClassLoader = mPluginClassLoaders.get(dexPath);

        if (dLClassLoader != null)

            return dLClassLoader;


        File dexOutputDir = context.getDir("dex", Context.MODE_PRIVATE);

        final String dexOutputPath = dexOutputDir.getAbsolutePath();

        dLClassLoader = new DLClassLoader(dexPath, dexOutputPath, null,

        parentLoader);

        mPluginClassLoaders.put(dexPath, dLClassLoader);


        return dLClassLoader;

    }

}

DexClassLoader补充:

DexClassLoader
DexClassLoader构造函数
DexClassLoader (String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)

dexPath: 包含资源和class文件的apk/jar;
optimizedDirectory: dex文件的存储路径;
libraryPath:native library的位置;
parent: 父ClassLoader;

上一篇下一篇

猜你喜欢

热点阅读