Android路由的實現

最近在做一個項目,因爲有多個功能模塊,所以遇到了一個困難:當Moudle A依賴Moudle B,Moudle B依賴Moudle C,Moudle C依賴MoudleD,Moudle D爲殼App,但是當我們需要在Moudle B調用Moudle C的時候,跳轉不過去,因爲找不到這個類,因此有了Android路由這個概念的提出,即我們可以在任意一個Moudel調用任意Moudel的Activity以及Services等組件。
瞭解了這個概念的時候,我還是有一點懵,因爲完全沒有一點思路。後來看了兩篇相關的文章:

學習了大神的代碼,我還是打算自己來寫一下!
我的想法是新建一個Moudle,然後所有的項目都依賴於這個Moudle,然後在Mooudle中掃描所有的類,得到類名,最後加載這個類,得到Class< ?>對象,後面我們加載的時候就可以直接調方法得到類了! 代碼如下:



import android.content.Context;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import dalvik.system.DexFile;

/**
 * Created by 魏興 on 2017/5/8.
 */

public class ClassContainer extends AppCompatActivity {
    private static final String TAG = "ClassContainer";
    private static List<String> classesName = new ArrayList<>();
    private static List<Class<?>> classes = new ArrayList<>();

    /**
     * 掃描類,並保存類名稱及對象
     * @param context
     * @param packageNames
     */
    public static void scan(Context context,String[] packageNames) {
        try {
            String str = context.getPackageResourcePath();
            DexFile df = new DexFile(context.getPackageResourcePath());
            Enumeration<String> n=df.entries();
            while(n.hasMoreElements()){
                String className = n.nextElement();
                for (String packageName:packageNames) {
                    if (className.contains(packageName)) {//在當前所有可執行的類裏面查找包含有該包名的所有類
//                        com.example.zhaoshuang.camera.MyVideoView$1
                        if(className.contains("$")){//內存中有多個類,原因未知,此處也排除了靜態內部類
                            int length = className.indexOf("$");
                            className = className.substring(0,length);
                        }
                        classesName.add(className);
                        Class cls = Thread.currentThread().getContextClassLoader().loadClass(className);
                        classes.add(cls);
                        break;
                    }
                }
            }
        } catch (IOException e1) {
            e1.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 獲取類對象
     * @param packageName
     * @return
     */
    public static Class<?> getClass(String packageName){

        try {
            for (int i=0;i<classesName.size();i++){
                String name = classesName.get(i);
                if(name.contains(packageName)){
                    Log.d(TAG, "getClass: "+classes.get(i).getName());
                    return classes.get(i).newInstance().getClass();
//                    return Thread.currentThread().getContextClassLoader().loadClass("com.luckytry.hybrid.myapplication.controller.FormActivity");
                }
            }
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }


}

這裏遇到了幾個坑,第一個就是類名於實際不一樣,比如com.example.zhaoshuang.camera.MyVideoView我只有一個,但是實際加載的時候加載了5次,後來才知道因爲我的MyVideoView類中有多個匿名內部類,但是我在保存類的時候,沒有保存到期望的類。導致後面開啓Activity的時候,出了問題。

com.example.zhaoshuang.camera.MyVideoView$1
com.example.zhaoshuang.camera.MyVideoView$2
com.example.zhaoshuang.camera.MyVideoView$3
com.example.zhaoshuang.camera.MyVideoView$4
com.example.zhaoshuang.camera.MyVideoView$5

後來就在上面的代碼中,修正了匿名內部類,直接保存了普通類。該代碼需要在Applicaition中執行,耗時約800毫秒(保存了200個類,未保存的類包括系統包及依賴庫的包沒有統計)。
然後當我們需要調用組件的時候,直接調用方法即可,代碼如下:

//註冊
@Override
    public void onCreate() {
        // TODO Auto-generated method stub
        super.onCreate();
...
        ClassContainer.scan(this,new String[]{"com.luckytry.luckylibrary.MyAplication",             "com.luckytry.hybrid.myapplication","com.example.zhaoshuang.camera",
                "com.luckytry.luckylibrary"});

    }

//調用
@Override
    public void onClick(View v) {
        int i = v.getId();
       ...
        }else if(i == R.id.rl_iv_sumbit){
            Log.d(TAG, "onClick: 提交照片");

                Intent intent = new Intent(this, ClassContainer.getClass("controller.FormActivity"));
                startActivity(intent);
            finish();
        }
    }

測試了一下,木有問題。後來在優化的過程中又發現新的問題:就是我們的類名是從dex文件中得到的。

/data/app/com.luckytry.hybrid.mainapplication-2.apk

隱患在我們的APK比較大的時候,方法超過了65536個的時候,dex需要分包(後來測試的過程中發現努比亞6.0的機子,以及三星7.0的機子拿到的dex文件都和上面的dex文件不一樣,代碼沒有任何變化,但是dex文件名稱有變化——“/data/app/com.luckytry.hybrid.mainapplication-2/base.apk”,而且這個dex文件沒有class文件),也就是說目前這個方法不確定是否能有效的將全部Activity掃描出來。
這個時候,有兩個辦法解決:

  • 手動將所有的類名保存下來
  • 不再保存類名,當我們需要Class< ?>對象的時候,手動的給類名的全部字符,而不是部分,如下:
//以前
 Intent intent = new Intent(this, ClassContainer.getClass("controller.FormActivity"));
 startActivity(intent);
//現在
Intent intent = new Intent(this, ClassContainer.getClass("com.luckytry.hybrid.myapplication.controller.FormActivity"));
 startActivity(intent);

最後,我發現這個路由Moudle根本木有必要單獨新建,直接將ClassContainer類寫在Moudle A的工具類,甚至工具類都不用,一句代碼就搞定了,越來越簡單了!

Module2Module相比較而言,該項目有一個缺點就是硬編碼,就是說我們將所有的class類名都寫在了代碼中,這一點不應該;優點是代碼非常簡單,維護及擴展的時候,自己比較清楚,不會遇到異常了自己抓瞎。

後續填坑:

上面提到硬編碼,於是我將繼續將代碼做了一些優化,首先第一步是增加一個自定義註解:

/**
 * 路由器註解
 * Created by 魏興 on 2017/5/8.
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAnnptation {
    String name();

}

接下來我們在需要用到的Activity(就是需要任意Moudle均能調用到這個Activity)增加我們剛剛自定義的路由註解,例如:

/**
 * 拍照
 * Created by 魏興 on 2017/4/17.
 */
@ClassAnnptation(name = "CaremaActivity")
public class CaremaActivity extends Activity {}

在完成了註解的添加以後,我們還需要註冊註解,意思就是將所有的註解添加到一個容器內,待後續調用,這個方法與之前的方法參數完全一致,不一樣的只是類名:

/**
     * class容器
     */
    private static Map<String,Class<?>> map = new HashMap<>();
/**
     * 掃描類,並添加到路由器
     * @param context
     * @param packageNames
     */
    public static void scan(Context context, String[] packageNames) {
        try {
            DexFile df = new DexFile(context.getPackageResourcePath());
            Enumeration<String> n=df.entries();
            while(n.hasMoreElements()){
                String className = n.nextElement();
                for (String packageName:packageNames) {
                    if (className.contains(packageName)) {//在當前所有可執行的類裏面查找包含有該包名的所有類
//                        com.example.zhaoshuang.camera.MyVideoView$1
                        if(className.contains("$")){//內存中有多個類,原因未知,此處也排除了靜態內部類
                            int length = className.indexOf("$");
                            className = className.substring(0,length);
                        }

                        Class cls = Thread.currentThread().getContextClassLoader().loadClass(className);
                        addRouter(cls);
                        break;
                    }
                }
            }
        } catch (IOException e1) {
            e1.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

/**
     * 添加到路由器
     * @param cls
     */
    private static void addRouter(Class<?> cls){
        if(cls == null){
            return;
        }
        ClassAnnptation clsa =cls.getAnnotation(ClassAnnptation.class);
        if(clsa!=null){
            if(map.containsValue(cls))
                return;
            String name = clsa.name();
            if(!map.containsKey(name)){
                map.put(name,cls);
            }else{
                Class<?> ocls = map.get(name);
                throw  new IllegalArgumentException(cls.getName()+" 註解的key與"+ocls.getName()+"重複,請重新設置key");
            }

        }
/**
     * 根據key獲取到路由器容器的Class<?>
     * @param key
     * @return
     */
    public static Class<?> getRouter(String key){
        if(map.containsKey(key)){
            return map.get(key);
        }
        return null;
    }
    }

接下來我們獲取Class< ?>對象時就不需要直接傳人完整的類名或者部分包名+類名了,而是直接輸入我們設置的別名,註冊與調用如下:

//註冊註解越早越好,建議放在Application裏面
new Thread(){
            @Override
            public void run() {
                super.run();
                Router.scan(getApplicationContext(),new String[]{"com.luckytry.luckylibrary.MyAplication",
                        "com.luckytry.hybrid.myapplication","com.example.zhaoshuang.camera",
                        "com.luckytry.luckylibrary"});
            }
        }.start();

//調用方法
@Override
public void onClick(View v) {
    v.getId();
    switch (v.getId()) {
        case R.id.action_a://繪製按鈕
            Intent polygon = new Intent(this,Router.getRouter("PolygonActivity"));
            polygon.putExtra(ParameterUtil.belong,0);
            startActivity(polygon);

            break;
          }
}

如此貌似完美了,但是我的想法是將這部分設置爲一個庫,可以供其它項目使用,而且需要實現編譯時註解,這樣就不用手動註解了,但是後來發現一個問題:

Created with Raphaël 2.1.0Moudle AMoudle BMoudle CMoudle D打包APP

我的所有Moudle是隊列的形式排列的A依賴B,B依賴C,C依賴D,D依賴APP,而一般情況下不會有這種情況。所以在這種情況我可以將所有的Class< ?>對象放在底部的Moudle A,這樣所有的Moudle都可以調用到需要被註解的。舉個栗子:本來正常情況下是Moudle A被Moudle B調用,而MoudleA不能調用Moudle B的東西,這個時候Moudle B也需要調用Moudle D裏面的功能,也是不能實現的,需要在打包App裏面建一個處理器,來處理這些調用關係。

看到這裏應該明白我們的設計的路由器的問題了,就是我們的路由有一定的侷限性,不能被所有的項目拿來使用。因此就算我通過編譯時註解或者是運行時註解將Class< ?>對象保存下來了,卻苦於找不到存儲的地方。也許可以通過持久化存儲來解決,嗯,以後有時間再嘗試一下!到這裏大家應該和我一起對這個Android路由有了非常深刻的理解了,今後不管是使用人家的庫或者自己編寫方法來實現,都不再是難題了!
參考文章:

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