前言
手把手講解系列文章,是我寫給各位看官,也是寫給我自己的。
文章可能過分詳細,但是這是為了幫助到盡量多的人,畢竟工作5,6年,不能老吸血,也到了回饋開源的時候.
這個系列的文章:
1、用通俗易懂的講解方式,講解一門技術的實用價值
2、詳細書寫源碼的追蹤,源碼截圖,繪制類的結構圖,盡量詳細地解釋原理的探索過程
3、提供Github 的 可運行的Demo工程,但是我所提供代碼,更多是提供思路,拋磚引玉,請酌情cv
4、集合整理原理探索過程中的一些坑,或者demo的運行過程中的注意事項
5、用gif圖,最直觀地展示demo運行效果如果覺得細節太細,直接跳過看結論即可。
本人能力有限,如若發現描述不當之處,歡迎留言批評指正。
學到老活到老,路漫漫其修遠兮。與眾君共勉 !
引子
用過微信支付寶的人應該都知道,其中有很多功能,都是可以靈活配置的,比如,支付寶里面,可以用“淘票票”來買電影票,還可以購買火車票飛機票,,甚至還有“餓了么”外賣,如果細細去數的話,這些雜七雜八的功能,加起來可能有幾十上百個了。
試想,這么多小功能,代碼都寫在支付寶里面,支付寶會不會爆炸 o(╯□╰)o !那支付寶的開發者如何做到這些功能的集成呢?
插件化開發。Demo地址
支付寶 app本身更像是一個 “ 空殼 ”,里面可以搭載很多小功能,這些小功能都是以"插件"的形式存在,支持小功能的靈活配置,用戶不想要某個功能,可以不顯示出來。
插件化開發是當下大型app必備的一項技術,不可不學。
鳴謝
感謝 大佬 "瀟湘夜雨" 提供的Demo
感謝 享學課堂alvin老師 的的熱心幫助
正文大綱
1.插件化開發的核心難點
2.插件化所需的技術理論基礎
3.核心難點的解決方案
4.核心代碼結構
5.如何使用Demo
6.最終效果展示
正文
1.插件化開發的核心難點
根據引子中所說,支付寶中各種各樣的功能,都是插件形式存在的,那么具體是如何存在?
我們所說的插件
,其實是apk文件,即xxx.apk
;
插件化開發的套路: 外殼app module + 多個插件Module + 插件框架層library module
- 外殼app 負責整個app的外部架構,并且給插件提供入口組件(比如,用一個
button
作為“余額寶”的入口,點擊button
,進入“余額寶” );- 多個插件Module,負責分開開發各個功能。嚴格來說,每個功能必須可以單獨運行,也必須支持集成到外殼
app
時運行。- 插件框架層library module, 所有插件化的核心代碼,都集中到這里。并且這個
library
要同時被外殼app
和插件module
引用.
文字描述不夠直觀?
看下圖:
那么現在很清晰了,插件化開發的難點,就是如何讓外殼app,啟動插件apk中的Activity.
既然給出了demo的代碼架構,那就順便給出github地址了:Demo
2.插件化所需的技術理論基礎
可能從上面的代碼架構上看,這項技術并不是很復雜,但是也是需要一定的技術基礎的,不然出一點小問題,一臉懵逼,無從查起就很尷尬了.
學習插件化開發,首先要了解
1.
Activity
是如何啟動的.
在我們自己的Activity
里,開啟另一個Activity
,使用startActivity
即可,但是startActivity
之后,系統做了什么?
開始追蹤源碼(源碼追蹤基于SDK 28 - 9.0):
我們通常通常啟動Activity,一般都是在Activity中 使用startActivity(intent),像下面這樣
public class MainActivity extends AppCompatActivity(){
private void xxxx(){
Intent i = new Intent(this,XXXActivity.class);
startActivity(i);
}
}
那么,startActivity到底做了什么,點進去看找到下面的代碼:
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
// Note we want to go through this call for compatibility with
// applications that may have overridden the method.
startActivityForResult(intent, -1);
}
}
繼續,追蹤這兩個startActivityForResult,直接到下面的代碼:
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);//注意看這里,mInstrumentation.execStartActivity
if (ar != null) {
mMainThread.sendActivityResult(
mToken, mEmbeddedID, requestCode, ar.getResultCode(),
ar.getResultData());
}
if (requestCode >= 0) {
···
mStartedActivity = true;
}
cancelInputsAndStartExitTransition(options);
// TODO Consider clearing/flushing other event sources and events for child windows.
} else {
if (options != null) {
mParent.startActivityFromChild(this, intent, requestCode, options);
} else {
// Note we want to go through this method for compatibility with
// existing applications that may have overridden it.
mParent.startActivityFromChild(this, intent, requestCode);
}
}
}
mInstrumentation.execStartActivity
在這里被執行,
然而另一個分支mParent
不為空時,會執行mParent.startActivityFromChild
那么追蹤它startActivityFromChild
,
public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent,
int requestCode, @Nullable Bundle options) {
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, child,
intent, requestCode, options);//然而,這里還是執行了mInstrumentation.execStartActivity
if (ar != null) {
mMainThread.sendActivityResult(
mToken, child.mEmbeddedID, requestCode,
ar.getResultCode(), ar.getResultData());
}
cancelInputsAndStartExitTransition(options);
}
然而,這里還是執行了mInstrumentation.execStartActivity
,
綜上所述,startActivity,最終都會執行到mInstrumentation.execStartActivity
,
那么繼續跟蹤這個execStartActivity:
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
...省略一大段...
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
int result = ActivityManager.getService()//這個不就是大名鼎鼎的AMS么
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
注意這里有一個
ActivityManager.getService()
,其實他就是安卓里大名鼎鼎的AMS(ActivityManagerService
),負責對 Android四大組件(activity
,service
,broadcast
,contentProvider
)的管理,包括啟動,生命周期管理等.
Activity里面startActivity的追蹤就到這里。
PS:其實,Activity不只是可以在Activity里啟動,還可以使用 getApplicationContext().startActivity(),有興趣的可以去追蹤一下,最終結論還是一樣,都會執行AMS的startActivity.
結論:
我們通常在自己的XXXActivity里調用startActivity之后,最終會執行AMS的startActivity,從而讓啟動的那個Activity具有生命周期.
那么,如果只是new 一個 Activity實例,它會不會具有生命周期呢?顯而易見了.
2. apk包(其實是壓縮包)里的各個文件各自的作用
在androidStudio
里,運行app,或者gradle
執行assemble
命令可以生成apk
文件,那么,我們解壓apk
文件之后,它里面的各種內部文件,各自都起到了什么作用呢?
請看下圖
一個apk解壓之后的內容
這里我們發現了這么幾個東西:
classes.dex 工程里面的java源碼編譯打包而成.
classes.dex文件,包含了這個apk的所有java類,那么我們拿到了這個dex文件,就有能力反射創建其中的類對象.
用AndroidStudio可以看到其內容:
image.pngres目錄 所有的資源文件
image.png
外殼app 通過資源包,可以拿到包里面的任意資源,當然,前提是,宿主要創建對應資源包的Resources
對象.resources.arsc res下所有資源的映射
image.pngMETA-INF app簽名的一些東西
AndroidManifest.xml 清單文件
3.核心難點的解決方案
了解了上面的技術基礎,那么現在擺出解決方案:
外殼app,作為一個"宿主"。插件apk中的所有東西,無論是classes.dex里的類,還是res資源,都是"宿主"之外的東西,那么宿主要想使用自己身外的類和資源,需要解決3個問題:
1. 取得插件中的Activity的Class
解決方案 ==> 使用DexClassLoader.它是專門加載外部apk的類加載器.
2. 取得插件中的資源
解決方案 ==> 使用hook技術,創建只屬于外部插件的Resouces資源管理器.
3. 反射創建了插件中的Activity對象,但是它是沒有生命周期的,不能像使用宿主自身的Activity一樣擁有完整的生命周期.如果不理解,請回去看 “2.插件化所需的技術理論基礎”
解決方案 ==> 使用 代理Activity
作為真正插件Activity的"傀儡".
4.核心代碼結構 這是Demo地址
以demo為樣板進行細節講解,下圖是demo的項目結構:
外殼app的結構.png
可以看到,外殼app很簡單,唯一要說明的就是插件apk,我放置在src/main/assets目錄,只是為了演示demo方便.
- MyApp.java ,只做了一件事,
PluginManager.getInstance().init(this);
,對PluginManager進行初始化并且賦予上下文.- MainActivity.java 只做了兩件事,
- 將asssets里面的apk文件,通過工具類AssetUtil的copyAssetToCache方法,拷貝到了app的緩存目錄下,然后使用PluginManager去加載這個apk.
String path = AssetUtil.copyAssetToCache(MainActivity.this, "plugin_module-debug.apk"); PluginManager.getInstance().loadPluginApk(path);
- 跳轉到代理Activity,并且傳入真正要跳的目標Activity的name.
// 先跳到代理Activity,由代理Activity展示真正的Activity內容 Intent intent = new Intent(MainActivity.this, ProxyActivity.class); intent.putExtra(PluginApkConst.TAG_CLASS_NAME, PluginManager.getInstance().getPackageInfo().activities[0].name); startActivity(intent);
這里涉及到了
PluginManager
類,下一小節詳述.
插件module.png
插件module十分簡單,它的作用,就是生成插件apk,對它進行編譯打包,取得apk文件即可。
兩個重點:
- 插件中的所有Activity,必須都集成來自plugin_lib的
PluginBaseActivity
,只有繼承了,才具有插件化特征,能夠被外殼app執行startActivity成功跳轉.- 插件內部的Activity跳轉,上下文,必須使用
PluginBaseActivity
的proxy
變量.
image.png
前面兩個module都很簡單,那么核心技術在哪里?
插件化框架library.png
插件框架層代碼,是插件化開發技術的核心。這個module要同時被外殼app和插件module引用.其中,3個技術要點:
- PluginManager類
它是一個單例,負責讀取插件apk的內容,并且創建出專屬于插件
的類加載器DexClassLoader
,資源管理器Resources
,以及包信息PackageInfo
并 用public get方法公開出去。
public class PluginManager {
//應該是單例模式,因為一個宿主app只需要一個插件管理器對象即可
private PluginManager() {
}
private volatile static PluginManager instance;//volatile 保證每一次取的instance對象都是最新的
public static PluginManager getInstance() {
if (instance == null) {
synchronized (PluginManager.class) {
if (instance == null) {
instance = new PluginManager();
}
}
}
return instance;
}
private Context mContext;//上下文
private PackageInfo packageInfo;//包信息
private DexClassLoader dexClassLoader;//類加載器
private Resources resources;//資源包
public void init(Context context) {
mContext = context.getApplicationContext();//要用application 因為這是單例,直接用Activity對象作為上下文會導致內存泄漏
}
/**
* 從插件apk中讀出我們所需要的信息
*
* @param apkPath
*/
public void loadPluginApk(String apkPath) {
//先拿到包信息
packageInfo = mContext.getPackageManager().getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);//只拿Activity
if (packageInfo == null)
throw new RuntimeException("插件加載失敗");//如果apkPath是傳的錯的,那就拿不到包信息了,下面的代碼也就不用執行
//類加載器,DexClassLoader專門負責外部dex的類
File outFile = mContext.getDir("odex", Context.MODE_PRIVATE);
dexClassLoader = new DexClassLoader(apkPath, outFile.getAbsolutePath(), null, mContext.getClassLoader());
//創建AssetManager,然后創建Resources
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
method.invoke(assetManager, apkPath);
resources = new Resources(assetManager,
mContext.getResources().getDisplayMetrics(),
mContext.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
}
//把這3個玩意公開出去
public PackageInfo getPackageInfo() {
return packageInfo;
}
public DexClassLoader getDexClassLoader() {
return dexClassLoader;
}
public Resources getResources() {
return resources;
}
}
- ProxyActivity類
它作為一個代理,一個傀儡,宿主能夠通過它,來間接地管理真正插件Activity的生命周期.
那它是如何間接管理真正Activity的生命周期?用類似下面的代碼:
public void ProxyActivity extends Activity{
@Override
protected void onStart() {
iPlugin.onStart();//iPlugin是插件Activity實現的接口,前面用IPlugin將插件Activity對象接收了
super.onStart();
}
}
然而,ProxyActivity的onCreate另有玄機
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
realActivityName = getIntent().getStringExtra(PluginApkConst.TAG_CLASS_NAME);//宿主,將真正的跳轉意圖,放在了這個參數className中,
//拿到realActivityName,接下來的工作,自然就是展示出真正的Activity
try {// 原則,反射創建RealActivity對象,但是,去拿這個它的class,只能用dexClassLoader
Class<?> realActivityClz = PluginManager.getInstance().getDexClassLoader().loadClass(realActivityName);
Object obj = realActivityClz.newInstance();
if (obj instanceof IPlugin) {//所有的插件Activity,都必須是IPlugin的實現類
iPlugin = (IPlugin) obj;
Bundle bd = new Bundle();
bd.putInt(PluginApkConst.TAG_FROM, IPlugin.FROM_EXTERNAL);
iPlugin.attach(this);
iPlugin.onCreate(bd);//反射創建的插件Activity的生命周期函數不會被執行,那么,就由ProxyActivity代為執行
}
} catch (Exception e) {
e.printStackTrace();
}
}
前面PluginManager返回了專屬于插件
的類加載器DexClassLoader
,資源管理器Resources
,那么這個ProxyActivity真正展示的是插件的Activity內容,就要使用插件自己的類加載器和資源管理器了.
@Override
public ClassLoader getClassLoader() {
ClassLoader classLoader = PluginManager.getInstance().getDexClassLoader();
return classLoader != null ? classLoader : super.getClassLoader();
}
@Override
public Resources getResources() {
Resources resources = PluginManager.getInstance().getResources();
return resources != null ? resources : super.getResources();
}
注意,前方大坑,
public class ProxyActivity extends Activity{}
,我的ProxyActivity
繼承的是android.app.Activity
,而不是android.support.v7.app.AppCompatActivity
,這是因為,AppCompatActivity
會檢測上下文context,從而導致空指針.
至于更深層的原因,有興趣的大佬可以繼續挖掘,沒興趣的話直接用android.app.Activity
就完事了.
- IPlugin接口和PluginBaseActivity類
插件module中也許不只一個Activity,我們啟動插件Activity之后,插件內部如果需要跳轉,仍然要遵守插件化
的規則,那就給他們創建一個共同的父類PluginBaseActivity.
IPlugin 接口
/**
* 插件Activity的接口規范
*/
public interface IPlugin {
int FROM_INTERNAL = 0;//插件單獨測試時的內部跳轉
int FROM_EXTERNAL = 1;//宿主執行的跳轉邏輯
/**
* 給插件Activity指定上下文
*
* @param activity
*/
void attach(Activity activity);
// 以下全都是Activity生命周期函數,
// 插件Activity本身 在被用作"插件"的時候不具備生命周期,由宿主里面的代理Activity類代為管理
void onCreate(Bundle saveInstanceState);
void onStart();
void onResume();
void onRestart();
void onPause();
void onStop();
void onDestroy();
void onActivityResult(int requestCode, int resultCode, Intent data);
}
PluginBaseActivity 抽象類
/**
* 插件Activity的基類,插件中的所有Activity,都要繼承它
*/
public abstract class PluginBaseActivity extends AppCompatActivity implements IPlugin {
private final String TAG = "PluginBaseActivityTag";
protected Activity proxy;//上下文
//這里基本上都在重寫原本Activity的函數,因為 要兼容“插件單獨測試” 和 "集成到宿主整體測試",所以要進行情況區分
private int from = IPlugin.FROM_INTERNAL;//默認是“插件單獨測試”
@Override
public void attach(Activity proxyActivity) {
proxy = proxyActivity;
}
@Override
public void onCreate(Bundle saveInstanceState) {
if (saveInstanceState != null)
from = saveInstanceState.getInt(PluginApkConst.TAG_FROM);
if (from == IPlugin.FROM_INTERNAL) {
super.onCreate(saveInstanceState);
proxy = this;//如果是從內部跳轉,那就將上下文定為自己
}
}
@Override
public void onStart() {
if (from == IPlugin.FROM_INTERNAL) {
super.onStart();
} else {
Log.d(TAG, "宿主啟動:onStart()");
}
}
@Override
public void onResume() {
if (from == IPlugin.FROM_INTERNAL) {
super.onResume();
} else {
Log.d(TAG, "宿主啟動:onResume()");
}
}
@Override
public void onRestart() {
if (from == IPlugin.FROM_INTERNAL) {
super.onRestart();
} else {
Log.d(TAG, "宿主啟動:onRestart()");
}
}
@Override
public void onPause() {
if (from == IPlugin.FROM_INTERNAL) {
super.onPause();
} else {
Log.d(TAG, "宿主啟動:onPause()");
}
}
@Override
public void onStop() {
if (from == IPlugin.FROM_INTERNAL) {
super.onStop();
} else {
Log.d(TAG, "宿主啟動:onStop()");
}
}
@Override
public void onDestroy() {
if (from == IPlugin.FROM_INTERNAL) {
super.onDestroy();
} else {
Log.d(TAG, "宿主啟動:onDestroy()");
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (from == IPlugin.FROM_INTERNAL) {
super.onActivityResult(requestCode, resultCode, data);
} else {
Log.d(TAG, "宿主啟動:onActivityResult()");
}
}
//下面是幾個生命周期之外的重寫函數
@Override
public void setContentView(int layoutResID) {//設置contentView分情況
if (from == IPlugin.FROM_INTERNAL) {
super.setContentView(layoutResID);
} else {
proxy.setContentView(layoutResID);
}
}
@Override
public View findViewById(int id) {
if (from == FROM_INTERNAL) {
return super.findViewById(id);
} else {
return proxy.findViewById(id);
}
}
@Override
public void startActivity(Intent intent) {//同理
if (from == IPlugin.FROM_INTERNAL) {
super.startActivity(intent);//原intent只能用于插件單獨運行時
} else {
// 如果是集成模式下,插件內的跳轉,控制權 仍然是在宿主上下文里面,所以--!
// 先跳到代理Activity,由代理Activity展示真正的Activity內容
Intent temp = new Intent(proxy, ProxyActivity.class);
temp.putExtra(PluginApkConst.TAG_CLASS_NAME, intent.getComponent().getClassName());
proxy.startActivity(temp);//這里不能跳原來的intent,,必須重新創建
}
}
}
PluginBaseActivity 抽象類中,3個重點需要特別說明
1. 插件module需要單獨測試,也需要 作為插件來集成測試,所以這里IPlugin接口中定義了 FROM_INTERNAL和FROM_EXTERNAL 進行情形區分.
2. 除了IPlugin必須實現的一些生命周期方法之外,最后我還新增了3個方法:
setContentView
,findViewById
,startActivity
設置布局,尋找組件,跳轉Activity,也是需要區分 單測還是集成測試的,所以,也要做if/else判定.
并且[看3.]3. 上面說的startActivity,當從外部跳轉,也就是宿主來啟動插件Activity的時候,也只能跳到ProxyActivity,然后把真正的目標Activity放在參數中.
5.如何使用Demo
我的demo中已經有了一個插件apk,如上圖.
如果你更改了
plugin_module
的內容,請重新生成一個apk,放到上圖所示位置,文件名必須和外殼app內寫的一樣.放好之后,運行外殼app即可。
6.最終效果展示
集成測試,由外殼app啟動插件Activity
插件化集成測試.gif
插件單獨測試
插件單獨測試.gif
結語
可能有人說,目前自己公司的app還用不著
插件化
這種重量級的技術,如果這樣說的人,真的是這么想,那么可能永遠只能做小app了。做技術如果不想做大做強,那和咸魚有什么區別,o(╯□╰)o
另有問題需要探討,或者發現錯誤,歡迎給我留言!
技術之路漫漫長,碼字不易,希望看到的客官點個好評,謝謝支持.
Demo地址