Android-動態加載插件化的兩種實現方式(一):反射

縱觀整個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打包,也算是一次學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章