自我提升之四 動態加載第三方應用-插件化詳解

什麼插件化

每一個業務組件都是一個獨立的apk,然後通過主app動態加載部署業務組件apk。

插件化好處

  1. 業務組件解耦,能夠實現業務組件熱插拔
  2. 更改產品迭代模式,可分爲主app以及次業務app
  3. 改善產品更新過程,可以在不影響用戶的情況下實現業務組件更新以及bug修復

插件化 “思想”

主App被系統 “安裝” 調用,這個過程由系統提高,而插件apk並非被系統安裝,簡而言之,需要將插件apk看成一個 “非apk” 文件,只是一個結構複雜的文件,在主app需要時會調用這個文件的一些資源。調用插件即用某種特殊的方式打開這個文件。

插件化步驟

分析主app

  1. 主App打包完成後,會形成dex,images,xml資源
  2. dex靠PathClassLoader加載
  3. 圖片以及xml資源靠Resource加載

代碼實現

  1. 創建DexClassLoader加載插件代碼
  2. 創建Resource加載資源文件
  3. 管理插件Activity生命週期

項目結構
項目結構

注:主APP項目運行在手機上,插件APP項目打包成APK,保存到主項目私有目錄下,他們都引用連接的library

插件實體對象

package com.shangyi.android.pluginlibrary;

import android.content.pm.PackageInfo;
import android.content.res.AssetManager;
import android.content.res.Resources;

import dalvik.system.DexClassLoader;

/**
 * <pre>
 *           .----.
 *        _.'__    `.
 *    .--(Q)(OK)---/$\
 *  .' @          /$$$\
 *  :         ,   $$$$$
 *   `-..__.-' _.-\$/
 *         `;_:    `"'
 *       .'"""""`.
 *      /,  FLY  ,\
 *     //         \\
 *     `-._______.-'
 *     ___`. | .'___
 *    (______|______)
 * </pre>
 * 包    名 : com.shangyi.android.pluginlibrary
 * 作    者 : FLY
 * 創建時間 : 2019/5/8
 * 描述: 插件apk信息的實體對象
 */
public class PluginApk {

    public PackageInfo mPackageInfo;//apk的解析

    public DexClassLoader mDexClassLoader;//dex靠PathClassLoader加載

    public Resources mResources;// 圖片以及xml資源靠Resource加載

    public AssetManager mAssetManager;//用於支持創建Resources

    public PluginApk(PackageInfo mPackageInfo, DexClassLoader mDexClassLoader, Resources mResources) {
        this.mPackageInfo = mPackageInfo;
        this.mDexClassLoader = mDexClassLoader;
        this.mResources = mResources;
        this.mAssetManager = mResources.getAssets();
    }
}

插件apk的管理類

package com.shangyi.android.pluginlibrary;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.util.Log;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import dalvik.system.DexClassLoader;

/**
 * <pre>
 *           .----.
 *        _.'__    `.
 *    .--(Q)(OK)---/$\
 *  .' @          /$$$\
 *  :         ,   $$$$$
 *   `-..__.-' _.-\$/
 *         `;_:    `"'
 *       .'"""""`.
 *      /,  FLY  ,\
 *     //         \\
 *     `-._______.-'
 *     ___`. | .'___
 *    (______|______)
 * </pre>
 * 包    名 : com.shangyi.android.pluginlibrary
 * 作    者 : FLY
 * 創建時間 : 2019/5/8
 * 描述: 插件apk的管理
 */
public class PluginManager {

    private static PluginManager instance;

    private PluginManager() {
    }

    public static PluginManager getInstance() {
        if (instance == null) {
            instance = new PluginManager();
        }
        return instance;
    }

    private Context mContext;

    private PluginApk mPluginApk;

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

    /**
     * 檢測是否調用初始化方法
     */
    private void checkInitialize() {
        if (mContext == null) {
            throw new ExceptionInInitializerError("請先調用 PluginManager.getInstance().init(this) 初始化!");
        }
    }

    //加載插件apk,服務器下載保存路徑
    public void loadApk(String apkPath) {
        checkInitialize();
        PackageInfo packageInfo = mContext.getPackageManager().getPackageArchiveInfo(apkPath,
                PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);

        if (packageInfo == null) {
            Log.d("PluginManager", "loadApk: 加載插件apk失敗");
            return;
        }

        DexClassLoader dexClassLoader = createDexClassLoader(apkPath);
        AssetManager assetManager = createAssetManager(apkPath);
        Resources resources = createResources(assetManager);
        mPluginApk = new PluginApk(packageInfo, dexClassLoader, resources);
    }

    public PluginApk getPluginApk() {
        return mPluginApk;
    }

    //創建訪問插件apk的DexClassLoader對象加載插件代碼
    private DexClassLoader createDexClassLoader(String apkPath) {
        File file = mContext.getDir("dex", Context.MODE_PRIVATE);
        return new DexClassLoader(apkPath, file.getAbsolutePath(), null, mContext.getClassLoader());
    }

    //訪問插件apk的Aseetmanger對象
    private AssetManager createAssetManager(String apkPath) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            method.invoke(assetManager, apkPath);
            return assetManager;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    //創建訪問插件apk的Resource對象加載資源文件
    private Resources createResources(AssetManager assetManager) {
        Resources resources = mContext.getResources();
        return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
    }

}

管理Activity生命週期

package com.shangyi.android.pluginlibrary;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;

/**
 * <pre>
 *           .----.
 *        _.'__    `.
 *    .--(Q)(OK)---/$\
 *  .' @          /$$$\
 *  :         ,   $$$$$
 *   `-..__.-' _.-\$/
 *         `;_:    `"'
 *       .'"""""`.
 *      /,  FLY  ,\
 *     //         \\
 *     `-._______.-'
 *     ___`. | .'___
 *    (______|______)
 * </pre>
 * 包    名 : com.shangyi.android.pluginlibrary
 * 作    者 : FLY
 * 創建時間 : 2019/5/8
 * 描述: 判斷調用方式 管理Activity生命週期
 */
public interface IPlugin {

    String FROM_KEY = "FromKey"; //判斷調用的傳參key

    int FROM_INTERNAL = 0; // 從內部調用 手機系統調用
    int FROM_EXTERNAL = 1; // 從外邊調用 主app調用

    //綁定,代理Activity,傳入插件上下文
    void attach(Activity proxyActivity);

    void onCreate(Bundle savedInstanceState);

    void onStart();

    void onRestart();

    void onResume();

    void onActivityResult(int requestCode, int resultCode, Intent data);

    void onPause();

    void onStop();

    void onDestroy();
}

連接插件的activity

package com.shangyi.android.pluginlibrary;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

/**
 * <pre>
 *           .----.
 *        _.'__    `.
 *    .--(Q)(OK)---/$\
 *  .' @          /$$$\
 *  :         ,   $$$$$
 *   `-..__.-' _.-\$/
 *         `;_:    `"'
 *       .'"""""`.
 *      /,  FLY  ,\
 *     //         \\
 *     `-._______.-'
 *     ___`. | .'___
 *    (______|______)
 * </pre>
 * 包    名 : com.shangyi.android.pluginlibrary
 * 作    者 : FLY
 * 創建時間 : 2019/5/9
 * 描述: 連接插件的activity
 */
public class PuglinActivity extends Activity implements IPlugin {

    //判斷是否是從主APP調用的,如果是不做任何操作
    //如果是系統調用的不帶次參數,需要調用父類方法
    private int mFrom = FROM_INTERNAL;

    //插件的上下文,代理用
    private Activity mProxyActivity;

    @Override
    public void attach(Activity proxyActivity) {
        mProxyActivity = proxyActivity;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        if (savedInstanceState != null) {
            mFrom = savedInstanceState.getInt(FROM_KEY);
        }

        if (mFrom == FROM_INTERNAL) super.onCreate(savedInstanceState);

    }

    public Activity getProxyActivity() {
        return mProxyActivity;
    }

    @Override
    public void setContentView(int layoutResID) {
        if (mFrom == FROM_INTERNAL) {
            super.setContentView(layoutResID);
        } else if (mProxyActivity != null) {
            mProxyActivity.setContentView(layoutResID);
        } else {
            Log.e("PuglinActivity", "插件的上下文爲空");
        }
    }

    @Override
    public void onStart() {
        if (mFrom == FROM_INTERNAL) super.onStart();
    }

    @Override
    public void onRestart() {
        if (mFrom == FROM_INTERNAL) super.onRestart();
    }

    @Override
    public void onResume() {
        if (mFrom == FROM_INTERNAL) super.onResume();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (mFrom == FROM_INTERNAL) super.onActivityResult(requestCode, resultCode, data);
    }

    @Override
    public void onPause() {
        if (mFrom == FROM_INTERNAL) super.onPause();
    }

    @Override
    public void onStop() {
        if (mFrom == FROM_INTERNAL) super.onStop();
    }

    @Override
    public void onDestroy() {
        if (mFrom == FROM_INTERNAL) super.onDestroy();
    }

}

代理activity - 實質調用需要manifest註冊

package com.shangyi.android.pluginlibrary;

import android.app.Activity;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;

/**
 * <pre>
 *           .----.
 *        _.'__    `.
 *    .--(Q)(OK)---/$\
 *  .' @          /$$$\
 *  :         ,   $$$$$
 *   `-..__.-' _.-\$/
 *         `;_:    `"'
 *       .'"""""`.
 *      /,  FLY  ,\
 *     //         \\
 *     `-._______.-'
 *     ___`. | .'___
 *    (______|______)
 * </pre>
 * 包    名 : com.shangyi.android.pluginlibrary
 * 作    者 : FLY
 * 創建時間 : 2019/5/9
 * 描述: 代理activity
 */
public class ProxyActivity extends Activity {

    public static final String CLASS_NAME = "className";

    private String mClassName;

    private PluginApk mPluginApk;

    private IPlugin mIPlugin;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mClassName = getIntent().getStringExtra(CLASS_NAME);
        mPluginApk = PluginManager.getInstance().getPluginApk();

        launchPlaginActivity();

    }

    //啓動activity
    private void launchPlaginActivity() {
        if (mPluginApk == null) {
            Log.e("ProxyActivity: ", "加載不了插件apk文件");
            return;
        }

        try {
            Class<?> clazz = mPluginApk.mDexClassLoader.loadClass(mClassName);
            // 實例化Activity 注意:這裏的activity是沒有生命週期,也沒有上下文環境的
            Object object = clazz.newInstance();
            if (object instanceof IPlugin) {
                mIPlugin = (IPlugin) object;
                mIPlugin.attach(this);
                Bundle bundle = new Bundle();
                bundle.putInt(IPlugin.FROM_KEY, IPlugin.FROM_EXTERNAL);
                mIPlugin.onCreate(bundle);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public Resources getResources() {
        return mPluginApk != null ? mPluginApk.mResources : super.getResources();
    }

    @Override
    public AssetManager getAssets() {
        return mPluginApk != null ? mPluginApk.mAssetManager : super.getAssets();
    }

    @Override
    public ClassLoader getClassLoader() {
        return mPluginApk != null ? mPluginApk.mDexClassLoader : super.getClassLoader();
    }
}

主界面activity

package com.fly.newstart.plugin;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;

import com.fly.newstart.R;
import com.fly.newstart.common.base.BaseActivity;
import com.fly.newstart.utils.FileUtils;
import com.shangyi.android.pluginlibrary.PluginManager;
import com.shangyi.android.pluginlibrary.ProxyActivity;

public class MainPluginActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_plugin);
        PluginManager.getInstance().init(this);
        findViewById(R.id.btnLoad).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //模擬服務器下載插件apk
                String apkPath = FileUtils.copyAssetAndWrite(MainPluginActivity.this, "pluginapk-debug.apk");
                //加載apk
                PluginManager.getInstance().loadApk(apkPath);
            }
        });

        findViewById(R.id.btnSkip).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //指定跳轉的類名
                Intent intent = new Intent();
                intent.setClass(MainPluginActivity.this, ProxyActivity.class);
                intent.putExtra(ProxyActivity.CLASS_NAME, "com.shangyi.android.pluginapk.PluginActivity");
                startActivity(intent);
            }
        });
    }

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

    @Override
    protected void onResume() {
        super.onResume();
    }

    @Override
    protected void onRestart() {
        super.onRestart();
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
    }

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

    @Override
    protected void onStop() {
        super.onStop();
    }

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

MainPluginActivity界面XML

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    style="@style/MatchMatch"
    android:gravity="center"
    android:orientation="vertical"
    tools:context="com.fly.newstart.plugin.MainPluginActivity">

    <TextView style="@style/WrapWrap"
        android:text="這裏是主App"/>

    <Button
        android:id="@+id/btnLoad"
        style="@style/WrapWrap"
        android:text="加載插件app" />

    <Button
        android:id="@+id/btnSkip"
        style="@style/WrapWrap"
        android:text="跳轉到插件app" />

</LinearLayout>

文件處理工具

package com.fly.newstart.utils;

import android.content.Context;
import android.widget.Toast;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;

/**
 * <pre>
 *           .----.
 *        _.'__    `.
 *    .--(Q)(OK)---/$\
 *  .' @          /$$$\
 *  :         ,   $$$$$
 *   `-..__.-' _.-\$/
 *         `;_:    `"'
 *       .'"""""`.
 *      /,  FLY  ,\
 *     //         \\
 *     `-._______.-'
 *     ___`. | .'___
 *    (______|______)
 * </pre>
 * 包    名 : com.fly.newstart.utils
 * 作    者 : FLY
 * 創建時間 : 2019/5/9
 * 描述: 文件處理工具類
 */
public class FileUtils {

    /**
     * 將Assets目錄的fileName文件拷貝到app緩存目錄
     *
     * @param context
     * @param fileName
     * @return
     */
    public static String copyAssetAndWrite(Context context, String fileName) {
        try {
            File cacheDir = context.getCacheDir();
            if (!cacheDir.exists()) {
                cacheDir.mkdir();
            }
            File outFile = new File(cacheDir, fileName);
            if (!outFile.exists()) {
                boolean res = outFile.createNewFile();
                if (res) {
                    InputStream is = context.getAssets().open(fileName);
                    FileOutputStream fos = new FileOutputStream(outFile);
                    byte[] buffer = new byte[is.available()];
                    int byteCount;
                    while ((byteCount = is.read(buffer)) != -1) {
                        fos.write(buffer, 0, byteCount);
                    }
                    fos.flush();
                    is.close();
                    fos.close();
                    Toast.makeText(context, "下載成功", Toast.LENGTH_LONG).show();
                }
            } else {
                Toast.makeText(context, "文件以存在", Toast.LENGTH_LONG).show();
            }
            return outFile.getAbsolutePath();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
}

插件activity

package com.shangyi.android.pluginapk;

import android.os.Bundle;

import com.shangyi.android.pluginlibrary.PuglinActivity;

public class PluginActivity extends PuglinActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_plugin);

    }
}

PluginActivity 的XML

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context="com.shangyi.android.pluginapk.PluginActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我是插件app界面"/>

</LinearLayout>

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