插樁式插件化跳轉

插樁式插件化跳轉

  1. 什麼是插件化

    所謂插件化 就是將app分爲一個宿主apk和多個插件apk,由宿主apk啓動插件apk,在宿主apk中實現不安裝插件apk也可完成之間活動跳轉,發送廣播,啓動服務等。舉個例子:比如 支付寶內接了海量的應用,如 ofo小黃車,餓了麼等等,首先我們知道他們不可能是在一個項目裏完成開發,海量的應用,維護起來簡直是個噩夢。支付寶就是一個宿主app,內接的海量應用就是插件apk,當我們在支付寶內點擊餓了麼圖標時,就是先從服務器上下載下餓了麼的apk,然後由支付寶調用餓了麼插件apk的首頁,完成跳轉到另外一個apk。

  2. 插件化分爲幾種
    插件化 原理上可分爲:插樁式 和 Hook式

  3. 插樁式原理

    • 由宿主apk訂製一套規則,宿主和插件共同遵守,只有按規則開發才能接入到宿主apk內
    • 宿主context語法
    • 瞞天過海式代理跳轉

下面 一個個解讀:
這裏寫圖片描述

1.宿主app訂製的規則是什麼,按什麼規則開發:

項目中有兩個app,一個支付寶app(宿主app),另外一個爲餓了麼app(插件app),還有一個制定的標準lib,兩個app都需要依賴於這個標準,此lib中有三個接口,分別爲Acticity,廣播,服務 定製的標準接口,以activity的標準爲例
這裏寫圖片描述

public interface PayInterfaceActivity {

    /**
     * 生命週期的方法
     */
    public void attach(Activity proxyActivity);
    public void onCreate(Bundle savedInstanceState);
    public void onStart();
    public void onResume();
    public void onPause();
    public void onStop();
    public void onDestroy();
    public void onSaveInstanceState(Bundle outState);
    public boolean onTouchEvent(MotionEvent event);
    public void onBackPressed();
}

兩個apk會通過這些接口來進行調用或傳值,以實現接口的規則進行開發,後面會有詳細代碼說明。

  1. 宿主context語法是什麼?
    此時你會不會想到 既然我是從一個宿主app內跳轉到另外一個沒有安裝的插件apk時,那插件內部還能否繼續調用自身的Context上下文?答案肯定是不能的,因爲插件apk都沒有進行安裝,也沒有運行,肯定是拿不到自身上下文的,那麼插件app中要用到上下文怎麼辦?答案是用宿主傳過來的Context。
public class BaseActivity  extends Activity implements PayInterfaceActivity {
//這是插件的基類  在插件的基類中實現訂製標準的接口  在attach中將從宿主app內傳過來的上下文進行賦值
//然後重寫那些用到上下文的方法,將上下文改爲宿主的上下文對象,此即爲Context語法
    protected  Activity that;
    @Override
    public void attach(Activity proxyActivity) {
        this.that = proxyActivity;
    }


    @Override
    public void setContentView(View view) {
        if (that != null) {
            that.setContentView(view);
        }else {
            super.setContentView(view);
        }
    }

    @Override
    public void setContentView(int layoutResID) {
        that.setContentView(layoutResID);
    }

    @Override
    public ComponentName startService(Intent service) {
        Intent m = new Intent();
        m.putExtra("serviceName", service.getComponent().getClassName());
        return that.startService(m);
    }

    @Override
    public View findViewById(int id) {
        return that.findViewById(id);
    }

    @Override
    public Intent getIntent() {
        if(that!=null){
            return that.getIntent();
        }
        return super.getIntent();
    }
    @Override
    public ClassLoader getClassLoader() {
        return that.getClassLoader();
    }

    @Override
    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {

        return that.registerReceiver(receiver, filter);
    }

    @Override
    public void sendBroadcast(Intent intent) {
        that.sendBroadcast(intent);
    }

    @Override
    public void startActivity(Intent intent) {
//        ProxyActivity --->className
        Intent m = new Intent();
        m.putExtra("className", intent.getComponent().getClassName());
        that.startActivity(m);
    }

    @NonNull
    @Override
    public LayoutInflater getLayoutInflater() {
        return that.getLayoutInflater();
    }

    @Override
    public ApplicationInfo getApplicationInfo() {
        return that.getApplicationInfo();
    }


    @Override
    public Window getWindow() {
        return that.getWindow();
    }


    @Override
    public WindowManager getWindowManager() {
        return that.getWindowManager();
    }




    @Override
    public void onCreate(Bundle savedInstanceState) {

    }

    @Override
    public void onStart() {

    }

    @Override
    public void onResume() {

    }

    @Override
    public void onPause() {

    }

    @Override
    public void onStop() {

    }

    @Override
    public void onDestroy() {

    }

    @Override
    public void onSaveInstanceState(Bundle outState) {

    }
}
  1. 重頭戲:瞞天過海式代理跳轉

3.1 首先在宿主app內將插件的app加載到宿主app的目錄中去,在跳轉前一定要先完成加載,宿主apk可放在服務器上,在點擊跳轉前從服務器下載apk到本地。

private void loadPlugin() {
        File filesDir = this.getDir("plugin", Context.MODE_PRIVATE);
        String name = "pluginb.apk"; //插件名稱
        String filePath = new File(filesDir, name).getAbsolutePath();
        File file = new File(filePath);
        if (file.exists()) {
            file.delete();
        }
        InputStream is = null;
        FileOutputStream os = null;
        try {
            Log.i(TAG, "加載插件 " + new File(Environment.getExternalStorageDirectory(), name).getAbsolutePath());
            is = new FileInputStream(new File(Environment.getExternalStorageDirectory(), name));
            os = new FileOutputStream(filePath);
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            File f = new File(filePath);
            if (f.exists()) {
                Toast.makeText(this, "dex overwrite", Toast.LENGTH_SHORT).show();
            }
            PluginManager.getInstance().loadPath(this);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            try {
                os.close();
                is.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

3.2 跳轉到插件的MainActivity

    public void click(View view) {

        Intent intent = new Intent(this, ProxyActivity.class);
        intent.putExtra("className",        PluginManager.getInstance().getPackageInfo().activities[0].name);
        startActivity(intent);
    }

咦,咋一看,這也不對啊,你這是騙我啊,說好得跳轉到MainActivity,怎麼一言不合就跳轉到本地的ProxyActivity中去了,別慌,首先看名字,ProxyActivity 代理activity的意思,作爲跳轉插件apk的代理app,一定要在xml文件中註冊,否則 會報錯,一會看ProxyActivity中具體做了什麼,先來看看 intent.putExtra中以className爲鍵,以PluginManager.getInstance().getPackageInfo().activities[0].name)爲值,這個值傳了什麼,首先先看最重要的類PluginManager。

public class PluginManager {
    private PackageInfo packageInfo;
    private Resources resources;
    private Context context;
    private DexClassLoader dexClassLoader;//首先理解DexClassLoader和PathClassLoader的區別
                                          //簡要來說DexClassLoader加載任意路徑的類,PathclassLoader只能加載本地自己應用的類
    private static final PluginManager ourInstance = new PluginManager();

    public static PluginManager getInstance() {
        return ourInstance;
    }

    private PluginManager() {
    }

    public void setContext(Context context) {
        this.context = context;
    }

    public PackageInfo getPackageInfo() {
        return packageInfo;
    }

    public void loadPath(Context context) {
       //找到插件apk的文件 用PackageManager管理器獲取PackageInfo,PackageInfo內有我們想要的插件apk的包名,類名等信息。
        File filesDir = context.getDir("plugin", Context.MODE_PRIVATE);
        String name = "pluginb.apk";
        String path = new File(filesDir, name).getAbsolutePath();

        PackageManager packageManager=context.getPackageManager();
          packageInfo=packageManager.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES);


   //根據插件apk的路徑,加載路徑,緩存路徑,和本地類加載器 生成一個插件的類加載器,通過類加載器加載插件中的類
        File dexOutFile = context.getDir("dex", Context.MODE_PRIVATE);
        dexClassLoader = new DexClassLoader(path, dexOutFile.getAbsolutePath()
                , null, context.getClassLoader());

        try {
        //通過反射獲取插件中resources 資源,包括佈局文件等
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath=AssetManager.class.getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, path);
            resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
        } catch ( Exception e) {
            e.printStackTrace();
        }
     }


    public Resources getResources() {
        return resources;
    }

    public DexClassLoader getDexClassLoader() {
        return dexClassLoader;
    }
}

回過頭來再繼續看

  intent.putExtra("className", PluginManager.getInstance().getPackageInfo().activities[0].name);

packageInfo.activities[0].name返回的就是插件的第一個activity的全路徑,即com.test.eleme.MainActivity,將類全路徑傳到代理ProxyActivity中去,用ProxyActivity來欺騙AMS,通過AMS檢查,接着看最後一個關鍵點,代理類ProxyActivity中做了什麼:

public class ProxyActivity  extends Activity {
    private String className;
    PayInterfaceActivity payInterfaceActivity;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState  ) {
        super.onCreate(savedInstanceState );
        //1.獲取到從上一個活動傳進來的啓動插件的類全名
        className = getIntent().getStringExtra("className");

        try {
           //2.用類加載器創建出MainActivity的類
            Class activityClass = getClassLoader().loadClass(className);
            Constructor constructor = activityClass.getConstructor(new Class[]{});
            Object instance= constructor.newInstance(new Object[]{});
            //到此 instance已經是MainActivity的類對象了
            //將MainActivity強轉成訂製的activity標準
            payInterfaceActivity = (PayInterfaceActivity) instance;
            //將上下文對象傳入到插件中去,
            payInterfaceActivity.attach(this);
            //此處可以對插件Activity進行傳值
            Bundle bundle = new Bundle();
            //依次調用插件activity生命週期的方法 完成啓動
            payInterfaceActivity.onCreate(bundle);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void startActivity(Intent intent) {
        String className1=intent.getStringExtra("className");
        Intent intent1 = new Intent(this, ProxyActivity.class);
        intent1.putExtra("className", className1);
        super.startActivity(intent1);
    }

    @Override
    public ClassLoader getClassLoader() {
        return PluginManager.getInstance().getDexClassLoader();
    }


    @Override
    public Resources getResources() {
        return PluginManager.getInstance().getResources();
    }


    @Override
    protected void onStart() {
        super.onStart();
        payInterfaceActivity.onStart();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        payInterfaceActivity.onDestroy();
    }

    @Override
    protected void onPause() {
        super.onPause();
        payInterfaceActivity.onPause();
    }
}

總結:到此,插樁式插件化Activity跳轉已經完成,首先雙方需要共同遵守一套標準的接口,由接口進行傳值和調用,用類加載器實現加載插件中的類,用反射的方法調用插件中的資源,插件中因爲沒有自己的上下文對象,所以凡是用到Context上下文對象時,都要調用從宿主app中傳進來的Context對象。

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