1、介绍

我们知道PathClassLoader是一个应用的默认加载器(而且他只能加载data/app/xxx.apk的文件),但是我们加载插件一般使用DexClassLoader加载器,所以这里就有问题了,其实如果对于开始的时候,每个人都会认为很简单,很容易想到使用DexClassLoader来加载Activity获取到class对象,在使用Intent启动

 

2、替换LoadApk里面的mClassLoader

我们知道我们可以将我们使用的DexClassLoader加载器绑定到系统加载Activity的类加载器上就可以了,这个是我们的思路。也是最重要的突破点。下面我们就来通过源码看看如何找到加载Activity的类加载器。加载Activity的时候,有一个很重要的类:LoadedApk.Java,这个类是负责加载一个Apk程序的,我们可以看一下他的源码:

Android插件化开发之运行未安装apk的activity_插件化

我们知道内部有个mClassLoader成员变量,我们只需要获取它就可以了,因为它不是静态的,所以我们需要先获取LoadApk这个类的对象,我们再去

看看ActivityThread.java这个类

Android插件化开发之运行未安装apk的activity_android_02

我们可以发现ActivityThread里面有个静态的成员变量sCurrentActivityThread,然后还有一个ArrayMap存放Apk包名和LoadedApk映射关系的数据结构,我们通过反射来获取mClassLoader对象。

如果对ActivityThread.java这个类不熟悉的可以看我这篇博客,(Android插件化开发之AMS与应用程序(客户端ActivityThread、Instrumentation、Activity)通信模型分析)非常重要,是app程序的入口处。

 

3、实现具体代码

     1)我们先需要一个测试apk,然后把这个测试的test.apk,放到手机sdcard里面去,关键代码如下
package com.example.testapkdemo;

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Toast;

public class MainActivity extends ActionBarActivity {
	
	public static View parentView;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		if (parentView == null) {
			setContentView(R.layout.activity_main);
		} else {
			setContentView(parentView);
		}
		findViewById(R.id.button).setOnClickListener(new OnClickListener(){
			@Override
			public void onClick(View v) {
				Toast.makeText(MainActivity.this, "我是来自插件", Toast.LENGTH_SHORT).show();
			}
		});
	}
	
	public void setView(View view) {
		this.parentView = view;
	}
}

 
效果图如下:
Android插件化开发之运行未安装apk的activity_Android_03
 
 
 

接下来是我宿主代码:

ReflectHelper.java  这个是的我的反射帮助类

 

package com.example.dexclassloaderactivity;


import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * 反射辅助函数
 * @author
 *
 */
public class ReflectHelper {
	public static final Class<?>[] PARAM_TYPE_VOID = new Class<?>[]{};

	public static Object invokeStaticMethod(String className, String methodName, Class<?>[] paramTypes, Object...params) {
		try {
			Class<?> clazz = Class.forName(className);
			Method method = clazz.getMethod(methodName, paramTypes);
			method.setAccessible(true);
			return method.invoke(null, params);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	
	public static Object invokeMethod(String className, String methodName, Class<?>[] paramTypes, Object obj, Object...params) {
		try {
			Class<?> clazz = Class.forName(className);
			Method method = clazz.getMethod(methodName, paramTypes);
			method.setAccessible(true);
			return method.invoke(obj, params);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	public static Object getStaticField(String className, String fieldName) {
		try {
			Class<?> clazz = Class.forName(className);
			Field field = clazz.getDeclaredField(fieldName);
			field.setAccessible(true);
			return field.get(null);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	
	public static Object getField(String className, String fieldName, Object obj) {
		try {
			Class<?> clazz = Class.forName(className);
			Field field = clazz.getDeclaredField(fieldName);
			field.setAccessible(true);
			return field.get(obj);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	
	public static void setStaticField(String className, String fieldName, Object value) {
		try {
			Class<?> clazz = Class.forName(className);
			Field field = clazz.getDeclaredField(fieldName);
			field.setAccessible(true);
			field.set(null, value);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public static void setField(String className, String fieldName, Object obj, Object value) {
		try {
			Class<?> clazz = Class.forName(className);
			Field field = clazz.getDeclaredField(fieldName);
			field.setAccessible(true);
			field.set(obj, value);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static Object createInstance(String className, Class<?>[] paramTypes, Object...params) {
		Object object = null;
		try {
			Class<?> cls = Class.forName(className);
			Constructor<?> constructor = cls.getConstructor(paramTypes);
			constructor.setAccessible(true);
			object = constructor.newInstance(params);
		}catch (Exception e) {
			e.printStackTrace();
		}
		return object;
	}

    /**
     * Locates a given field anywhere in the class inheritance hierarchy.
     *
     * @param instance an object to search the field into.
     * @param name field name
     * @return a field object
     * @throws NoSuchFieldException if the field cannot be located
     */
    public static Field findField(Object instance, String name) throws NoSuchFieldException {
        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
            try {
                Field field = clazz.getDeclaredField(name);


                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }

                return field;
            } catch (NoSuchFieldException e) {
                // ignore and search next
            }
        }

        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
    }

	/**
	 * Locates a given field anywhere in the class inheritance hierarchy.
	 *
	 * @param  cls to search the field into.
	 * @param name field name
	 * @return a field object
	 * @throws NoSuchFieldException if the field cannot be located
	 */
	public static Field findField2(Class<?> cls, String name) throws NoSuchFieldException {
		Class<?> clazz = null;
		for (clazz = cls; clazz != null; clazz = clazz.getSuperclass()) {
			try {
				Field field = clazz.getDeclaredField(name);


				if (!field.isAccessible()) {
					field.setAccessible(true);
				}

				return field;
			} catch (NoSuchFieldException e) {
				// ignore and search next
			}
		}

		throw new NoSuchFieldException("Field " + name + " not found in " + clazz);
	}

    /**
     * Locates a given method anywhere in the class inheritance hierarchy.
     *
     * @param instance an object to search the method into.
     * @param name method name
     * @param parameterTypes method parameter types
     * @return a method object
     * @throws NoSuchMethodException if the method cannot be located
     */
    public static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
            throws NoSuchMethodException {
        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
            try {
                Method method = clazz.getDeclaredMethod(name, parameterTypes);


                if (!method.isAccessible()) {
                    method.setAccessible(true);
                }

                return method;
            } catch (NoSuchMethodException e) {
                // ignore and search next
            }
        }

        throw new NoSuchMethodException("Method " + name + " with parameters " +
                Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
    }
}


MyApplication.java 这个类用实现替换mClassLoader

 

 

package com.example.dexclassloaderactivity;

import java.io.File;
import java.lang.ref.WeakReference;

import dalvik.system.DexClassLoader;
import android.annotation.SuppressLint;
import android.app.Application;
import android.os.Environment;
import android.util.ArrayMap;
import android.util.Log;

public class MyApplication extends Application{
	
	public static final String TAG = "MyApplication";
	public static final String AppName = "test.apk";
	public static int i = 0;
	
	public static DexClassLoader mClassLoader;
	
	@Override
	public void onCreate() {
		Log.d(TAG, "替换之前系统的classLoader");
		showClassLoader();
		try {
			String cachePath = this.getCacheDir().getAbsolutePath();
			String apkPath = /*Environment.getExternalStorageState() + File.separator*/"/sdcard/"+ AppName;
			mClassLoader = new DexClassLoader(apkPath, cachePath,cachePath, getClassLoader()); 
			loadApkClassLoader(mClassLoader);
		} catch (Exception e) {
			e.printStackTrace();
		}
		Log.d(TAG, "替换之后系统的classLoader");
		showClassLoader();
	}
	

	@SuppressLint("NewApi")
	public void loadApkClassLoader(DexClassLoader loader) {
		try {
			Object currentActivityThread  = ReflectHelper.invokeMethod("android.app.ActivityThread", "currentActivityThread", new Class[] {},new Object[] {});
			String packageName = this.getPackageName();
			ArrayMap mpackages = (ArrayMap) ReflectHelper.getField("android.app.ActivityThread", "mPackages", currentActivityThread);
			WeakReference wr= (WeakReference)mpackages.get(packageName);
			Log.e(TAG, "mClassLoader:" + wr.get());  
			ReflectHelper.setField("android.app.LoadedApk", "mClassLoader", wr.get(), loader);
			Log.e(TAG, "load:" + loader);  
		} catch (Exception e) {
			Log.e(TAG, "load apk classloader error:" + Log.getStackTraceString(e));  
		}
	}
	
	
	/**
	 * 打印系统的classLoader
	 */
	public void showClassLoader() {
		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());
                i++;
            }
        }
	}
}

然后就是MainActivity.java文件,里面包含了下面另外一种方式,打开activity,所以我把函数  inject(DexClassLoader loader)先注释掉

 

 

package com.example.dexclassloaderactivity;

import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;

import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

public class MainActivity extends ActionBarActivity{

	public static final String TAG = "MainActivity";
	public static final String AppName = "test.apk";
	public static DexClassLoader mDexClassLoader = null;
	public static final String APPName = "test.apk";
	public TextView mText;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
	    findViewById(R.id.text2).setOnClickListener(new OnClickListener(){
			@Override
			public void onClick(View v) {
				try {
//					 inject(MyApplication.mClassLoader);
					 String apkPath = Environment.getExternalStorageDirectory().toString() + File.separator + APPName;
					 Class clazz = MyApplication.mClassLoader.loadClass("com.example.testapkdemo.MainActivity");
					 Intent intent = new Intent(MainActivity.this, clazz);
					 startActivity(intent);
					 finish();
				} catch (Exception e) {
					Log.e(TAG, "name:" + Log.getStackTraceString(e));
				}
			}
	    });
	}
	
	
	private void inject(DexClassLoader loader){  
	  
		PathClassLoader pathLoader = (PathClassLoader) getClassLoader();  
	    try {  
	        Object dexElements = combineArray(  
	                getDexElements(getPathList(pathLoader)),  
	                getDexElements(getPathList(loader)));  
	        Object pathList = getPathList(pathLoader);  
	        setField(pathList, pathList.getClass(), "dexElements", dexElements);  
	    } catch (IllegalArgumentException e) {  
	        e.printStackTrace();  
	    } catch (NoSuchFieldException e) {  
	        e.printStackTrace();  
	    } catch (IllegalAccessException e) {  
	        e.printStackTrace();  
	    } catch (ClassNotFoundException e) {  
	        e.printStackTrace();  
	    }  
	}  
	  
	private static Object getPathList(Object baseDexClassLoader)  
	        throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {  
	    ClassLoader bc = (ClassLoader)baseDexClassLoader;  
	    return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");  
	}  
	  
	private static Object getField(Object obj, Class<?> cl, String field)  
	        throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {  
	    Field localField = cl.getDeclaredField(field);  
	    localField.setAccessible(true);  
	    return localField.get(obj);  
	}  
	  
	private static Object getDexElements(Object paramObject)  
	        throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {  
	    return getField(paramObject, paramObject.getClass(), "dexElements");  
	}  
	private static void setField(Object obj, Class<?> cl, String field,  
	        Object value) throws NoSuchFieldException,  
	        IllegalArgumentException, IllegalAccessException {  
	  
	    Field localField = cl.getDeclaredField(field);  
	    localField.setAccessible(true);  
	    localField.set(obj, value);  
	}  
	  
	private static Object combineArray(Object arrayLhs, Object arrayRhs) {  
	    Class<?> localClass = arrayLhs.getClass().getComponentType();  
	    int i = Array.getLength(arrayLhs);  
	    int j = i + Array.getLength(arrayRhs);  
	    Object result = Array.newInstance(localClass, j);  
	    for (int k = 0; k < j; ++k) {  
	        if (k < i) {  
	            Array.set(result, k, Array.get(arrayLhs, k));  
	        } else {  
	            Array.set(result, k, Array.get(arrayRhs, k - i));  
	        }  
	    }   
	    return result;  
	}    
}
	
	
	


 

 

这里一定要注意,我们犯了3个错,

1、test.apk的路径写错了,下次写文件路径的时候,我们应该需要加上File file = new File(path); 用file.exist()函数来判断是否存在

2、从sdcard卡里面读取test.apk,没加上权限,   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

3、写了Application,忘了在AndroidManifest.xml文件里面声明。

我们还需要注意要加上开启activity 在AndroidManifest.xml里面注册,切记,希望下次不要再次犯错。

AndroidManifest.xml文件如下:

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.dexclassloaderactivity"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="11"
        android:targetSdkVersion="21" />
    
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

    <application
        android:name="com.example.dexclassloaderactivity.MyApplication"
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <activity   
     			android:name="com.example.testapkdemo.MainActivity">  
        </activity> 
    </application>

</manifest>


运行结果如下:

 

Android插件化开发之运行未安装apk的activity_android_04

 

然后这里又出现了插件资源和宿主资源冲突问题,我们后面再来研究。

 

4、合并PathClassLoader和DexClassLoader中的dexElements数组 

我们首先来看一下PathClassLoader和DexClassLoader类加载器的父类BaseDexClassloader的源码:

(这里需要注意的是PathClassLoader和DexClassLoader类的父加载器是BaseClassLoader,他们的父类是BaseDexClassLoader)

Android插件化开发之运行未安装apk的activity_Android_05

这里有一个DexPathList对象,在来看一下DexPathList.java源码:

Android插件化开发之运行未安装apk的activity_activity_06

Elements数组,我们看到这个变量他是专门存放加载的dex文件的路径的,系统默认的类加载器是PathClassLoader,本身一个程序加载之后会释放一个dex出来,这时候会将dex路径放到里面,当然DexClassLoader也是一样的,那么我们会想到,我们是否可以将DexClassLoader中的dexElements和PathClassLoader中的dexElements进行合并,然后在设置给PathClassLoader中呢?这也是一个思路。我们来看代码:

 

/**
 * 以下是一种方式实现的
 * @param loader
 */
private void inject(DexClassLoader loader){
	PathClassLoader pathLoader = (PathClassLoader) getClassLoader();

	try {
		Object dexElements = combineArray(
				getDexElements(getPathList(pathLoader)),
				getDexElements(getPathList(loader)));
		Object pathList = getPathList(pathLoader);
		setField(pathList, pathList.getClass(), "dexElements", dexElements);
	} catch (IllegalArgumentException e) {
		e.printStackTrace();
	} catch (NoSuchFieldException e) {
		e.printStackTrace();
	} catch (IllegalAccessException e) {
		e.printStackTrace();
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	}
}

private static Object getPathList(Object baseDexClassLoader)
		throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
	ClassLoader bc = (ClassLoader)baseDexClassLoader;
	return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}

private static Object getField(Object obj, Class<?> cl, String field)
		throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
	Field localField = cl.getDeclaredField(field);
	localField.setAccessible(true);
	return localField.get(obj);
}

private static Object getDexElements(Object paramObject)
		throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
	return getField(paramObject, paramObject.getClass(), "dexElements");
}
private static void setField(Object obj, Class<?> cl, String field,
		Object value) throws NoSuchFieldException,
		IllegalArgumentException, IllegalAccessException {

	Field localField = cl.getDeclaredField(field);
	localField.setAccessible(true);
	localField.set(obj, value);
}

private static Object combineArray(Object arrayLhs, Object arrayRhs) {
	Class<?> localClass = arrayLhs.getClass().getComponentType();
	int i = Array.getLength(arrayLhs);
	int j = i + Array.getLength(arrayRhs);
	Object result = Array.newInstance(localClass, j);
	for (int k = 0; k < j; ++k) {
		if (k < i) {
			Array.set(result, k, Array.get(arrayLhs, k));
		} else {
			Array.set(result, k, Array.get(arrayRhs, k - i));
		}
	}
	return result;
}

然后运行的时候把MyApplication.java文件里面的函数loadApkClassLoader(mClassLoader);注释掉,然后把MainActivity.java文件里面的inject(MyApplication.mClassLoader)不要注释,运行效果一样。

 

 

总结:

我们在使用反射机制来动态加载Activity的时候,有两个思路:

1>、替换LoadApk类中的mClassLoader变量的值,将我们动态加载类DexClassLoader设置为mClassLoader的值

2>、合并系统默认加载器PathClassLoader和动态加载器DexClassLoader中的dexElements数组

这两个的思路原理都是一样的:就是让我们动态加载进来的Activity能够具备正常的启动流程和生命周期。

我们还没解决资源冲突问题,后面再解决,有点复杂。