佔位式插件化之加載Activity

在一些大型的項目中,常常會用到插件化,插件化的優勢有很多,即插即用,把不一樣的功能打包成不一樣的APK文件,經過網絡下發到APP端,直接就可使用,不用經過應用市場便可隨時增長新功能,很是適用於功能多又須要敏捷開發的應用java

能夠實現插件化的方式有不少種,本系列先經過佔位式的方法來實現。編程

咱們知道,一個apk文件須要經過安裝才能運行使用,那咱們的插件apk是直接經過網絡下載到本地的,不經過用戶的安裝,也就沒有上下文環境context,怎麼才能運行裏面的功能呢?緩存

其中的一種方式就是使用佔位式來開發,首先咱們確定有一個宿主APP,這個APP已經發布到市場上並安裝到了用戶的手機上,這個APP中有一個APK運行所須要的全部的環境,那咱們想辦法把這裏面的環境傳到咱們的插件包APK中,插件包中都使用穿過來的環境就能正常的工做了。網絡

而後就是宿主APP中怎麼加載插件apk中的類和資源文件呢?這個須要瞭解一下Android中的類加載技術,簡單說一下,Andorid中使用PathClassLoader來加載自身應用中的類,使用DexClassLoader來加載外部的文件(apk,zip等),使用Resources類來加載資源文件。app

最後類加載完了,宿主APP中怎麼調用插件中的對應的方法呢,它不知道何時該調用什麼方法啊。這時候咱們就能夠用到面向接口編程了,讓宿主APP和插件APP都依賴一套相同的接口標準,到時候經過這個相同的接口標準來調用對應的方法。ide

OK說了一大堆,如今開始幹吧,先擼一個加載Activity的工具

首先如圖在AndroidStudio中創建兩個app和一個module,這兩個app分別是宿主app和插件app,他們兩個都依賴同一個module,這個module中定義了一些接口標準this

先來看看Activity的接口標準:spa

public interface ActivityInterface {

    /** * 把宿主(app)的環境給插件 * @param appActivity 宿主環境 */
    void insertAppContext(Activity appActivity);

    void onCreate(Bundle savedInstanceState);

    void onStart();

    void onResume();

    void onDestroy();

}
複製代碼

標準很簡單,主要分爲兩部分,第一部分插件中不是沒有運行環境嗎,那定義一個方法,專門用來把宿主的環境傳過來。第二部分,在裏面實現全部咱們須要用到的activity的聲明週期方法,這裏就實現了幾個經常使用的。插件

OK,標準包中就完事了

下面咱們來到插件包中,定義一個BaseActivity,用它來實現標準接口和接收宿主傳過來的環境,還有重寫Activity中的相關方法。

public class BaseActivity extends Activity implements ActivityInterface {

    public Activity appActivity;

    @Override
    public void insertAppContext(Activity appActivity) {
         this.appActivity = appActivity;
    }

    @SuppressLint("MissingSuperCall")
    @Override
    public void onCreate(Bundle savedInstanceState) {

    }
    @SuppressLint("MissingSuperCall")
    @Override
    public void onStart() {

    }
    @SuppressLint("MissingSuperCall")
    @Override
    public void onResume() {

    }
    @SuppressLint("MissingSuperCall")
    @Override
    public void onDestroy() {

    }
     public void setContentView(int resId){
        appActivity.setContentView(resId);
    }
}
複製代碼

BaseActivity實現了ActivityInterface接口,並實現了接口中的方法。

注意: BaseActivity中重寫了setContentView方法,爲何呢?由於setContentView是當前插件Activity中的方法,而當前的插件Activity是沒有上下文環境的,調用這個確定就報錯啦,爲了能正常運行,咱們只能經過宿主傳過來的環境來調用相關的方法。這只是開始,後面不少跟環境有關的方法都須要在這裏重寫一下轉爲經過宿主的環境調用,這也是佔位式插件化的一個缺點。

定義一個PluginActivity來繼承自BaseActivity,等會咱們將從宿主APP中跳轉到此Activity。這是咱們在插件中的第一個Activity,須要註冊到manifest中,後面宿主跳轉時須要用到,在後面建立的Activity就不用註冊了。

public class PluginActivity extends BaseActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_plugin);
        Toast.makeText(appActivity,"我是插件中的activity",Toast.LENGTH_SHORT).show();
}
複製代碼

OK,插件包中的類寫完了,如今咱們來到宿主app中建立一個工具來,用來加載插件包中的類和資源

public class PluginManager {
    private static final String TAG = PluginManager.class.getSimpleName();

    public static PluginManager instance;

    private Context mContext;

    public static PluginManager getInstance(Context context){
        if(instance == null){
            synchronized (PluginManager.class){
                if(instance == null){
                    instance = new PluginManager(context);
                }
            }
        }
        return instance;
    }

    private PluginManager(Context context) {
        mContext = context;
    }

    private DexClassLoader mClassLoader;
    private Resources mResources;

    public void loadPlugin(){
        try {
            File file = new File(Environment.getExternalStorageDirectory()+File.separator+"p.apk");
            if(!file.exists()){
                Log.i(TAG,"插件包不存在");
            }
            String pluginPath = file.getAbsolutePath();
            //建立classloader用來加載插件中的類
            //建立一個緩存目錄 /data/data/包名/pDir
            File fileDir = mContext.getDir("pDir",Context.MODE_PRIVATE);

            mClassLoader = new DexClassLoader(pluginPath,fileDir.getAbsolutePath(),
                    null,mContext.getClassLoader());

            //建立resource用來加載插件中的資源
            //AssetManager 資源管理器 final修飾的不能new
            AssetManager assetManager = AssetManager.class.newInstance();
            //addAssetPath方法能夠加載apk文件
            Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath",
                    String.class);
            addAssetPathMethod.invoke(assetManager, pluginPath);
            //拿到當前宿主的resource 用來回去當前應用的分辨率等信息
            Resources resources = mContext.getResources();
            //用來加載插件包中的資源
            mResources = new Resources(assetManager,resources.getDisplayMetrics(),resources.getConfiguration());
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public DexClassLoader getClassLoader() {
        return mClassLoader;
    }

    public Resources getResources() {
        return mResources;
    }
}
複製代碼

首先是加載類,咱們經過建立一個DexClassLoader來加載,建立DexClassLoader須要三個參數一個是插件包的路徑,一個是緩存/data/data/包名/pDirpDir是咱們本身命名。和一個classloader。

而後是加載資源,經過建立Resources來加載資源,它須要三個參數,AssetManager ,分辨率信息和配置信息,分辨率信息和配置信息咱們能夠經過當前宿主中的Resources拿到。AssetManager能夠經過反射執行它內部的addAssetPath方法來拿到。

而後我建立一個代理Activity

public class ProxyActivity extends Activity {

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

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

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 插件裏面的 Activity
        String className = getIntent().getStringExtra("className");
        //實例化插件包中的activity
        try {
            Class<?> pluginClass = getClassLoader().loadClass(className);
            Constructor<?> constructor = pluginClass.getConstructor(new Class[]{});
            Object pluginActivity = constructor.newInstance(new Object[]{});
            //強轉爲對應的接口
            ActivityInterface activityInterface = (ActivityInterface) pluginActivity;
            activityInterface.insertAppContext(this);

            Bundle bundle = new Bundle();
            bundle.putString("content","從宿主傳過來");
            //執行插件中的方法
            activityInterface.onCreate(bundle);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
複製代碼

這個代理的Activity很是重要,它是一個真正的Activity,須要註冊到manifest中,插件中的Activity最終都是經過它來展現。

  • 首先咱們重寫它裏面的getResources和getClassLoader方法,返回咱們工具類中本身定義的classloader和resource。

  • 而後在onCreate方法中,經過插件中須要啓動的Activity的全類名來加載插件中的Activity。

  • 因爲咱們知道插件中的Activity都實現了ActivityInterface接口,因此這裏咱們就能夠直接強轉成ActivityInterface,

  • 最後調用ActivityInterface中的對應的生命週期方法便可。

那這個PluginActivity的全類名怎麼來呢,在點擊跳轉到PorxyActivity的時候經過Intent傳過來

下面咱們來到MainActivity中加載插件並找到PluginActivity的全類名並跳轉。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }


    public void loadPlugin(View view) {
        PluginManager.getInstance(this).loadPlugin();
    }

    public void startPlugin(View view) {
        //獲取插件包中的activity的全類名
        File file = new File(Environment.getExternalStorageDirectory() + File.separator + "p.apk");
        String path = file.getAbsolutePath();

        // 獲取插件包 裏面的 Activity
        PackageManager packageManager = getPackageManager();
        PackageInfo packageInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
        ActivityInfo activityInfo = packageInfo.activities[0];


        Intent intent = new Intent(this,ProxyActivity.class);
        intent.putExtra("className",activityInfo.name);
        startActivity(intent);
    }
}
複製代碼

尋找PluginActivity的全類名,經過PackageManager 這個類,傳入插件的路徑最後經過getPackageArchiveInfo方法就能夠拿到啦。ActivityInfo 中記錄了manifest中全部的activity,由於咱們插件的manifest中只註冊一個Activity就能夠了,因此直接取第0個就能夠啦。

OK,到這裏咱們就能夠順利的從宿主的APP中跳轉到插件APK中的PluginActivity了。

固然一個插件不能跳到插件的首頁就完事了,插件有不少功能,內部也須要繼續跳轉到別的界面,插件內部怎麼跳轉呢,直接startActivity嗎?固然不行啦,就跟前面的setContentView不能直接用同樣,插件中是沒有上下文環境的,而startActivity最終會進入到當前插件的Activity中,會報錯,須要使用宿主傳過來的環境,因此插件中的BaseActivity中還的須要重寫startActivity方法。

public View findViewById(int layoutId) {
        return appActivity.findViewById(layoutId);
    }

    @Override
    public void startActivity(Intent intent) {

        Intent intentNew = new Intent();
        // PluginActivity 全類名
        intentNew.putExtra("className", intent.getComponent().getClassName()); 
        appActivity.startActivity(intentNew);
    }
複製代碼

固然findViewById這個方法內部也是經過上下文環境調用的,因此也須要重寫,而後轉化爲宿主的環境來調用。主要注意的是,後面凡是用到上下文環境的方法,都須要重寫,轉化爲宿主的環境,這也時佔位式插件化的一個很是麻煩的地方,不過它的好處是比較穩定,相對於經過hook來作兼容性比較好。

PluginActivity中添加點擊事件

findViewById(R.id.bt_start_activity).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            startActivity(new Intent(appActivity, Plugin2Activity.class));
           }
    });

複製代碼

啓動插件中的首頁是啓動了一個代理的Activity(ProxyActivity),而插件內部的跳轉的本質就是在啓動一個ProxyActivity,把當前要啓動的Activity的全類名帶過去,而後經過類加載,流程跟啓動第一個Activity同樣。

插件首頁的Activity的全類名咱們須要去manifest中拿,插件內部跳轉就不用那麼麻煩了,只須要經過intent就能拿到了。

因此咱們須要在ProxyActivity中重寫startActivity方法,拿到插件包中的Activity以後,本身跳本身,這樣咱們就能讓插件中的一個新的Activity進棧出棧了,點擊返回鍵能夠返回上一個Activity。

@Override
    public void startActivity(Intent intent) {
        String className = intent.getStringExtra("className");
        Intent proxyIntent = new Intent(this,ProxyActivity.class);
        proxyIntent.putExtra("className",className);
        super.startActivity(proxyIntent);
    }
複製代碼

OK 這樣就實現了跳轉到插件首頁和插件內部跳轉的功能啦。下一篇來聊一下加載Service

把插件包打包成apk,放到手機根目錄中

效果

相關文章
相關標籤/搜索