Android-动态加载插件化的两种实现方式(一):反射

来源:互联网 发布:数据库系统包括 编辑:程序博客网 时间:2024/05/16 04:08

纵观整个Android体系的发展,常规应用开发中,很少使用到动态加载和热修复等插件化技术,但是在一些比较大的应用中我们可以察觉到他的存在。例如:支付宝、QQ、微信、去哪儿APP等都内嵌了很多“插件”来扩张延伸更多功能。未来插件化是否会成为主流有待考证,但不可否认的是功能高度集成化对于经常使用的APP的用户可以省去很多繁琐的操作,并且从人机交互方面考虑也更为合理。插件化的优势其实很好理解,简单的说就是可以通过一个APP打开另一个或几个没有安装在手机上的APP,这样一来,用户就不需要在手机上安装太多应用,在一个应用上就可以获取到自己想要的功能和信息。当然现在的手机内存做得越来越大,这一方面解决了用户多应用的问题。但是功能集成化依然有优势。例如腾讯现在很火的“微信小程序”,可以理解为一种特殊的插件化产物。很有趣微信小程序正试图抢占它“宿主”APP的地位,至于结果如何,还得拭目以待。
以上皆属瞎扯,下面开始切入正题:本文章共分为上下两篇,主要分析通过反射和接口两种方式来实现动态加载插件。本文章主要参考两位CSDN上两位知名大神任玉刚和尼古拉斯-赵四,两位大神的博客文章对我学习动态加载有很大的帮助,如下正是结合我自身的工作需要来分享自己的学习体会。第一篇通过反射实现,第二篇通过反射实现,都会以简单的案例来分析实现,也涉及到一些ant打包等知识,仅供分享。
任玉刚大神在自己的博客中也提到动态加载插件化主要有两个需要解决的复杂问题:资源的访问和Android四大组件的生命周期管理。这样正是Android区别于Java的核心所在,Android四大组件的生命周期概念问题使整个过程变得有些繁琐。至于资源访问那一块,目前我项目中的解决方案是布局全部通过动态布局生成(这也正是我之前写动态布局这篇文章的原因)和图片资源通过转化字符串调用。当然大神也用另外的方式突破了这一块的内容,感兴趣的可以访问任大神的博客进行学习。
动态加载反射篇,本篇主要通过两个项目来演示 Host(宿主)、LibPlugin(插件),涉及项目内容和主要目的是对Android核心四大组件的动态加载演示,所以更多讲述的是反射实现四大组件加载的内容。
LibPlugin:
LibPluginActivity.java

public class LibPluginActivity {    public void onCreate(Activity activity, Class<?> mActivityClass, Class<?> mServiceClass, Class<?> mReceiverClass){    body...;    };    public void onStart(){    body...;    };    public void onResume(){    body...;    };    public void onStop(){    body...;    };    public void onDestroy(){    body...;    };    public boolean onKeyDown(int keyCode, KeyEvent event){    body...;    };    public boolean onTouchEvent(MotionEvent event){    body...;    };}

LibPluginReceiver.java

public class LibPluginReceiver{    public void onReceive(Context context, Intent intent, Class<?> mActivityClass, Class<?> mServiceClass,            Class<?> mReceiverClass){    body...;    };} 

LibPluginService.java

public interface InterFloatServices {    public void onCreate(Context context, Class<?> mActivityClass, Class<?> mServiceClass, Class<?> mReceiverClass){    body...;    };    public int onStartCommand(Intent intent, int flags, int startId){    body...;    };    public void onDestroy(){    body...;    };}

上面是Libplugin的内容,当然实际还有很多逻辑代码但这里只给出三个Android组件核心生命周期代码。接下来使用ant打.apk包,当然可以不使用ant,视情况而定。我这里使用的是ant。具体的代码这里不给出,网上也有很多的相关资料,不过对于Android的这几个组件是不能够混淆的,但是可以改包名和类名,可以针对性做些处理。
这里写图片描述
打包完成后生成LibPlugin.apk,接下来在Host宿主中启动LibPlugin。
步骤如下:
1,在JRE环境下使用Java对LibPlugin.apk进行字符串转化处理

public static void main(String[] args) throws Exception     {        File file=new File("D:/adSDKworkspace/LibPlugin.apk");        byte[] by = new byte[(int) file.length()];        try {            InputStream is = new FileInputStream(file);            ByteArrayOutputStream bytestream = new ByteArrayOutputStream();            byte[] bb = new byte[2048];            int ch;            ch = is.read(bb);            while (ch != -1) {                bytestream.write(bb, 0, ch);                ch = is.read(bb);                //System.out.println("ch : " + ch);            }            by = bytestream.toByteArray();            is.close();        }        catch(Exception e)        {            e.printStackTrace();        }        byte[] lib=Kode.en_b(by);        String li=Base64Util.encode(lib, 0, lib.length);        //System.out.println("  //"+li.substring(0,10)+";"+li.length()+";"+li.substring(li.length()-10));        li=str1+li;        System.out.println("    public static String b[]={");        int len=getOneRandom(10240, 20480);        for(int i=0;i<li.length();i=i+len,len=getOneRandom(10240, 20480))        {            if(i+len>li.length())                len=li.length()-i;            System.out.println("    \""+li.substring(i,i+len)+"\",");        }        System.out.println("    };" );        System.out.print("  //");        for(int i:k)            System.out.print("0x"+Integer.toHexString(i)+",");        System.out.println();        System.out.println("}");    }

2,处理结果Constant.java
这里写图片描述
3,创建一个工具类Utils.java,构造一个方法,该方法传入(LibPlugin的完成类名)类名参数,返回该类

public static Class<?> getClasses(Context context, String className) {        byte[] by = null;        String filePath = context.getFilesDir() + File.separator + "bjls";        File filedir = new File(filePath);        if (!filedir.exists()) {            filedir.mkdir();        }        if (str == null || by == null) {            int[] cl = getName();            StringBuffer lib = new StringBuffer();            for (String s : Constants.b)                lib.append(s);            by = Base64.decode(lib.toString().substring(cl[cl.length - 1]), Base64.DEFAULT);        }        }        Class<?> mClass = clas.get(className);        if (mClass == null) {            String jarFilePath = filePath + File.separator + System.currentTimeMillis() + ".jar";            String dexFilePath = filePath + File.separator + System.currentTimeMillis() + ".dex" ;            byte[] byte2 = desDecrypt(by, k);            try {                BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(jarFilePath));                output.write(byte2);                output.flush();                output.close();                Class<?> classLoader = Class.forName("dalvik.system.DexClassLoader");                if (classLoader == null) {                    return null;                }                Constructor<?> constructor = classLoader                        .getConstructor(new Class<?>[] { String.class, String.class, String.class, ClassLoader.class });                Object instance = constructor.newInstance(                        new Object[] { jarFilePath, context.getFilesDir().getPath(), null, context.getClassLoader() });                if (instance == null) {                    return null;                }                Method method = classLoader.getMethod("loadClass", new Class<?>[] { String.class });                if (method == null) {                    return null;                }                mClass = (Class<?>) invoke(instance, method, className);                clas.put(className, mClass);            } catch (Exception e) {                e.printStackTrace();            }            File file = new File(jarFilePath);            if (file.exists()) {                file.delete();            }            file = new File(dexFilePath);            if (file.exists()) {                file.delete();            }        }        return mClass;    }    /**     * 执行方法     *      * @param method     * @param receiver     * @param args     * @return     */    public static Object invoke(Object receiver, Method method, Object... args) {        if (method == null || receiver == null) {            return null;        }        try {            return method.invoke(receiver, args);        } catch (Exception e) {            e.printStackTrace();        }        return null;    }    /**     * <将加密的密文字节数组转化为明文字节数组>     */    public static byte[] desDecrypt(byte[] src, byte[] password) {        try {            // DES算法要求有一个可信任的随机数源            SecureRandom random = new SecureRandom();            // 创建一个DESKeySpec对象            DESKeySpec desKey = new DESKeySpec(password);            // 创建一个密匙工厂            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(crypt_str);            // 将DESKeySpec对象转换成SecretKey对象            SecretKey securekey = keyFactory.generateSecret(desKey);            // Cipher对象实际完成解密操作            Cipher cipher = Cipher.getInstance(crypt_str);            // 用密匙初始化Cipher对象            cipher.init(Cipher.DECRYPT_MODE, securekey, random);            // 真正开始解密操作            return cipher.doFinal(src);        } catch (Exception e) {            e.printStackTrace();        }        return null;    }

简述下上面的过程,首先将被转化成字符串的LibPlugin.apk的内容再次转为byte字节,然后通过DES解密在指定路径下生成.jar文件。接下来就是动态加载的核心内容。这一步分网上有很多案例,具体学习推荐尼古拉斯-赵四的这篇博客,就是Android中使用DexClassLoad来达到动态加载目的这个关键api。不过我这里使用的是通过反射获取,所以看起来会有点困难。最后,通过调用反射方法,返回获取到LibPlugin中的Android三个组件生命周期的Java Class。

4,接下来就是在Host中创建Android必须的组件生命周期环境来调用LiaPlugin中的各个类方法。
MyActivity.java

public class MyActivity extends Activity {    /** 方法顺序 */    private final int METHOD_NUM_ONCREATE = 5;    private final int METHOD_NUM_ONSTART = 9;    private final int METHOD_NUM_ONRESUME = 8;    private final int METHOD_NUM_ONSTOP = 10;    private final int METHOD_NUM_ONDESTROY = 6;    private final int METHOD_NUM_ONKEYDOWN = 7;    private final int METHOD_NUM_ONTOUCHEVENT = 11;    private Object activity;    private Method[] activityMethods;    /** class 顺序 */    private final int CLASS_NUM_ACTIVITY = 5;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        Class<?> activityClass = Utils.getClasses(this, CLASS_NUM_ACTIVITY, null);        if (activityClass == null)            return;        activity = classes.newInstance();        activityMethods = classes.getMethods();        Utils.invoke(activity, activityMethods[METHOD_NUM_ONCREATE ], context, MyActivity.class, MyService.class,                    MyReceiver.class);    }    @Override    protected void onStart() {        super.onStart();        Utils.invoke(activity, activityMethods[METHOD_NUM_ONSTART ]);    }    @Override    protected void onResume() {        super.onResume();         Utils.invoke(activity, activityMethods[METHOD_NUM_ONRESUME]);    }    @Override    protected void onStop() {        super.onStop();        Utils.invoke(activity, activityMethods[METHOD_NUM_ONSTOP ]);    }    @Override    protected void onDestroy() {        super.onDestroy();        Utils.invoke(activity, activityMethods[METHOD_NUM_ONDESTROY]);    }    @Override    public boolean onKeyDown(int keyCode, KeyEvent event) {        return ((Boolean) Utils.invoke(activity, activityMethods[METHOD_NUM_ONKEYDOWN], keyCode, event)).booleanValue();    }    @Override    public boolean onTouchEvent(MotionEvent event) {    return ((Boolean) Utils.invoke(activity, activityMethods[METHOD_NUM_ONTOUCHEVENT], event)).booleanValue();    }}

MyReceiver.java

public class MyReceiver extends BroadcastReceiver {    /** 方法顺序 */    private final int METHOD_NUM_ONRECEIVER = 5;    /** class顺序 */    private final int CLASS_NUM_RECEIVER = 6;    @Override    public void onReceive(Context context, Intent intent) {        Class<?> classes = Utils.getClasses(context, CLASS_NUM_RECEIVER, null);        if (classes == null)            return;        Object receiver = classes.newInstance();        Method[] declaredMethods = classes.getMethods();        Utils.invoke(receiver, declaredMethods[METHOD_NUM_ONRECEIVER ], context, intent, MyActivity.class,                    MyService.class, MyReceiver.class);    }}

MyService.java

public class MyService extends Service {    /** 方法顺序 */    private final int METHOD_NUM_ONCREATE = 6;    private final int METHOD_NUM_ONSTARTCOMMAND = 8;    private final int METHOD_NUM_ONDESTROY = 7;    private Object service;    private Method[] serviceMethods;    /** class顺序 */    private final int CLASS_SERVICE = 7;    @Override    public IBinder onBind(Intent intent) {        return null;    }    @Override    public void onCreate() {        super.onCreate();        Class<?> servieclass = Utils.getClasses(this, CLASS_SERVICE, null);        if (servieclass == null)            return;        service = classes.newInstance();        serviceMethods = classes.getMethods();        Utils.invoke(service, serviceMethods[METHOD_NUM_ONCREATE ], context, MyActivity.class, MyService.class, MyReceiver.class);    }    @Override    public int onStartCommand(Intent intent, int flags, int startId) {    Utils.invoke(service, serviceMethods[METHOD_NUM_ONSTARTCOMMAND ], intent, flags, startId);    }    @Override    public void onDestroy() {        super.onDestroy();    Utils.invoke(service, serviceMethods[METHOD_NUM_ONDESTROY ]);        }}

上述基本上就是大致的一个实现过程,当然LibPlugin中所涉及到的权限相关要添加到宿主中,才能运行。
其实大家可能留意到一个问题就是项目中存在多个activity和fragment的时候一个跳转的处理问题,这部分我并没有做详细测试,大家感兴趣可以认真研究任玉刚大神的DL插件,对此有很深的讲解。
这一篇是通过反射来实现动态加载插件化,但是从性能方面考虑,反射不是很理想。下一篇将会以接口的方式来实现。使用接口的方式,对我来说最大的挑战就是打包的过程控制,因为作为插件需要考虑宿主会修改包名,然而如果是接口,那么在整个打包过程中,要保证这一部分代码保持原样。其实主要还是没有完全掌握ant打包,也算是一次学习。

0 0