Android徒手打造一个超精简的插件加载工具(创建Context)

来源:互联网 发布:rime输入法 mac 编辑:程序博客网 时间:2024/06/14 20:58

最近插件化,热修复又火了一阵,插件化和热修复基本实现原理都是靠ClassLoader,自己在业余之下也凑了一下热闹,呵呵,造一造自行车的轮子。
首先实现插件化,肯定就是要动态的访问里面的方法和资源了,其实对于已经安装的APK可以通过Context.createPackageContext创建Contxt,然后通过反射调用方法和获取资源,但是没有安装的APK,Context 就没有那么容易创建了。
创建一个没安装的APK的Context,其实只要把一个旧的Context包裹一下覆写一些方法就行了,但有一些细节需要注意。

1)ClassLoader创建

a.首先创建一个ClassLoader,方法如下

  /**     *     * @param context 上下文     * @param src  apk路径     * @param internalPath 解压classes.dex的位置(不会重复解压)     * @return ClassLoader     */    private static ClassLoader buildClassLoader(Context context, String src,String internalPath) {        return new DexClassLoader(src, internalPath, internalPath + "/" + "lib", context.getClassLoader());    }

b. 以上代码中,如果有jni的so文件,需要拷贝对应ABI的so文件到internalPath/lib/目录下。
so 从APK对应ABI下解压文件的解压方法如下

/**     * copy suitable so files to destination  dir     *     * @param apkPath apk 's path     * @param cpuAbi  cpu abi     * @param dstDir  destination  dir     */    private static void unZipSpecialJniLib(String apkPath, String cpuAbi, String dstDir) {        FileInputStream fileInputStream = null;        ZipInputStream zipInputStream = null;        try {            fileInputStream = new FileInputStream(apkPath);            zipInputStream = new ZipInputStream(fileInputStream);            ZipEntry zipEntry = null;            String mark = "lib/" + cpuAbi + File.separatorChar;            byte[] buffer = new byte[1024 * 5];            while ((zipEntry = zipInputStream.getNextEntry()) != null) {                String zipEntryName = zipEntry.getName();                if (zipEntryName.startsWith(mark) && !zipEntry.isDirectory()) {                    String entryName = zipEntry.getName();                    String zipFileName = entryName.substring(entryName.indexOf(mark) + mark.length());                    String dstFiePath = dstDir + "/lib/" + zipFileName;                    unZipItem(zipInputStream, buffer, dstFiePath);                }            }        } catch (Exception e) {            e.printStackTrace();        } finally {            try {                if (fileInputStream != null) fileInputStream.close();            } catch (Exception e) {                e.printStackTrace();            }            try {                if (zipInputStream != null) zipInputStream.close();            } catch (Exception e) {                e.printStackTrace();            }        }    }    private static void unZipItem(ZipInputStream zipInputStream, byte[] buffer, String dstFiePath) {        File dstFile = new File(dstFiePath);        if (!dstFile.getParentFile().exists()) {            boolean r = dstFile.getParentFile().mkdirs();            if (!r) throw new RuntimeException("  create folder failed,during unzip "+dstFiePath);        }        if (dstFile.exists()) {            boolean r = dstFile.delete();            if (!r) throw new RuntimeException(" create folder failed,during unzip"+dstFiePath);        }        int count;        FileOutputStream fileOutputStream = null;        try {            fileOutputStream = new FileOutputStream(dstFile);            while ((count = zipInputStream.read(buffer)) > 0) {                fileOutputStream.write(buffer, 0, count);            }            fileOutputStream.flush();        } catch (Exception e) {            e.printStackTrace();        } finally {            try {                if (fileOutputStream != null)                    fileOutputStream.close();            } catch (IOException e) {                e.printStackTrace();            }        }    }

以上的cpu ABI 可以通过 android.os.Build.CPU_ABI,得到。
至此就Classloader创建完毕。现在可以通过反射去调用外部APK中的方法了 。值得注意的是 Classloader 是优先从系统以及当前应用的dex中找类。

2)AssetManager的创建

通过反射创建
代码如下

 private static AssetManager buildAssetManager(String apkPluginPath) {        Class<?> assetManagerClass = AssetManager.class;        AssetManager assetManager = null;        try {            assetManager = (AssetManager) assetManagerClass.newInstance();            Method addPathMethod = assetManagerClass.getMethod("addAssetPath", String.class);            addPathMethod.setAccessible(true);            addPathMethod.invoke(assetManager, apkPluginPath);        } catch (Exception e) {            e.printStackTrace();        }        return assetManager;    }

3)ContextWrapper资源替换(重点)

看过开源dynamic-load-apk 源码的应该知道前两步,但是dynamic-load-apk的加载资源的话,一次只能加载一个,原理是每次切换到某一个插件的时候,就把相应的资源替换掉。如果每个插件new一个ContextWrapper 会怎样呢。实际上在加载资源还是会有问题,而问题的根源就是LayoutInflate中的它:

  public static LayoutInflater from(Context context) {        LayoutInflater LayoutInflater =                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);        if (LayoutInflater == null) {            throw new AssertionError("LayoutInflater not found.");        }        return LayoutInflater;    }

和ContextImp中的它

   registerService(LAYOUT_INFLATER_SERVICE, new ServiceFetcher() {                public Object createService(ContextImpl ctx) {                    return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext());                }});

解释一下,建立ContextWrapper替换资源后,虽然资源已经被替换了,但是
LayoutInflater却是用的旧的Context创建的LayoutInflater,因此需要创建一个新的LayoutInflater;创建方法如下

   Class<?> clz = classLoader.loadClass("com.android.internal.policy.PolicyManager");            Method createMethod = clz.getDeclaredMethod("makeNewLayoutInflater", Context.class);            mLayoutInflater = createMethod.invoke(null, this);

4)最终ContextWrapper

package jx.cy.csh.PluginContextLoader;import android.content.Context;import android.content.ContextWrapper;import android.content.res.AssetManager;import android.content.res.Resources;import java.lang.reflect.Method;/** * Created by biluo on 2016/8/25. */public class PluginContext extends ContextWrapper {    private AssetManager mAssetManager;    private ClassLoader mClassLoader;    private Resources mResources;    private Object mLayoutInflater;    public PluginContext(Context base, AssetManager assetManager , ClassLoader classLoader) {        super(base);        mClassLoader = classLoader;        mAssetManager = assetManager;        mResources = new Resources(assetManager, base.getResources().getDisplayMetrics(), base.getResources().getConfiguration());        createNewLayoutInflater(classLoader);    }    private void createNewLayoutInflater(ClassLoader classLoader) {        try {            Class<?> clz = classLoader.loadClass("com.android.internal.policy.PolicyManager");            Method createMethod = clz.getDeclaredMethod("makeNewLayoutInflater", Context.class);            mLayoutInflater = createMethod.invoke(null, this);        } catch (Exception e) {            e.printStackTrace();        }    }    @Override    public Resources getResources() {        return mResources;    }    @Override    public ClassLoader getClassLoader() {        return mClassLoader;    }    @Override    public AssetManager getAssets() {        return mAssetManager;    }    @Override    public Object getSystemService(String name) {        if (name != null && name.equals(LAYOUT_INFLATER_SERVICE)) {            return mLayoutInflater;        }        return super.getSystemService(name);    }}

5)其他

a 插件入口实例

package jx.cy.csh.pluginsample;import android.content.Context;import android.view.LayoutInflater;import android.view.View;/** * Created by biluo on 2016/8/30. */public class Main {    private View mView;    static {        System.loadLibrary("jnitest");    }    public void main(Context context) {        mView = LayoutInflater.from(context).inflate(R.layout.main, null);    }    public  native int plus(int a,int b);    public View getView() {        return mView;    }}

说明: layout里面可以使用自定义控件,因为解析xml是用的新的Context。父控件和子控件如果有相同包名的自定义控件(包括jar)并且写在XML里面,可能会出现强制转换异常(之前使用Context.createPackage遇见过,这里尚未验证).

1 0
原创粉丝点击