天天看点

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

最近插件化,热修复又火了一阵,插件化和热修复基本实现原理都是靠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[ * ];
            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)) > ) {
                fileOutputStream.write(buffer, , 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遇见过,这里尚未验证).