什麼插件化
每一個業務組件都是一個獨立的apk,然後通過主app動態加載部署業務組件apk。
插件化好處
- 業務組件解耦,能夠實現業務組件熱插拔
- 更改產品迭代模式,可分爲主app以及次業務app
- 改善產品更新過程,可以在不影響用戶的情況下實現業務組件更新以及bug修復
插件化 “思想”
主App被系統 “安裝” 調用,這個過程由系統提高,而插件apk並非被系統安裝,簡而言之,需要將插件apk看成一個 “非apk” 文件,只是一個結構複雜的文件,在主app需要時會調用這個文件的一些資源。調用插件即用某種特殊的方式打開這個文件。
插件化步驟
分析主app
- 主App打包完成後,會形成dex,images,xml資源
- dex靠PathClassLoader加載
- 圖片以及xml資源靠Resource加載
代碼實現
- 創建DexClassLoader加載插件代碼
- 創建Resource加載資源文件
- 管理插件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>