*本篇文章已授權微信公衆号 guolin_blog (郭霖)獨家釋出
最近把Activity啟動流程整體看了一遍,估摸着弄個啥來鞏固下,發現插件化正好是這塊技術的實踐,而說道插件化其實有好幾種實作方式,這裡我用的是hook的方式實作,主要目的呢是為了對activity啟動流程有個整體的認識,當然了本篇的插件化也隻是一個demo版本并沒有任何相容适配,重在流程和原理的了解。
概述
插件化顧名思義,就是将一個APK拆成多個,當需要的時候下載下傳對應插件APK加載的技術。本文demo中除了下載下傳是通過adb指令,其他都是模拟真實環境的,這裡先理下流程。
- 将插件工程打包為APK,然後通過adb push指令發送到宿主APK目錄(模拟下載下傳流程)。
- 利用ClassLoader加載插件APK中的類檔案。
- hook Activity啟動流程中部分類,利用占坑Activity幫助PluginActivity繞過AMS驗證,在真正啟動的時候又替換回PluginActivity。
- 建立插件Apk的Resources對象,完成插件資源的加載。
對整體流程有個大概認識後,下面将結合源碼和Demo來詳細講解,本文貼出的源碼基于API27。
初始化插件APK類檔案
既然插件APK是通過網絡下載下傳下來的,那麼APK中的類檔案就需要我們自己加載了,這裡我們要用到DexClassLoader去加載插件APK中的類檔案,然後将DexClassLoader中的Element數組和宿主應用的PathClassLoader的Element數組合并再設定回PathClassLoader,完成插件APK中類的加載。對ClassLoader不太熟悉的可以看下我另篇Android ClassLoader淺析
public class InjectUtil {
private static final String TAG = "InjectUtil";
private static final String CLASS_BASE_DEX_CLASSLOADER = "dalvik.system.BaseDexClassLoader";
private static final String CLASS_DEX_PATH_LIST = "dalvik.system.DexPathList";
private static final String FIELD_PATH_LIST = "pathList";
private static final String FIELD_DEX_ELEMENTS = "dexElements";
public static void inject(Context context, ClassLoader origin) throws Exception {
File pluginFile = context.getExternalFilesDir("plugin");// /storage/emulated/0/Android/data/$packageName/files/plugin
if (pluginFile == null || !pluginFile.exists() || pluginFile.listFiles().length == 0) {
Log.i(TAG, "插件檔案不存在");
return;
}
pluginFile = pluginFile.listFiles()[0];//擷取插件apk檔案
File optimizeFile = context.getFileStreamPath("plugin");// /data/data/$packageName/files/plugin
if (!optimizeFile.exists()) {
optimizeFile.mkdirs();
}
DexClassLoader pluginClassLoader = new DexClassLoader(pluginFile.getAbsolutePath(), optimizeFile.getAbsolutePath(), null, origin);
Object pluginDexPathList = FieldUtil.getField(Class.forName(CLASS_BASE_DEX_CLASSLOADER), pluginClassLoader, FIELD_PATH_LIST);
Object pluginElements = FieldUtil.getField(Class.forName(CLASS_DEX_PATH_LIST), pluginDexPathList, FIELD_DEX_ELEMENTS);//拿到插件Elements
Object originDexPathList = FieldUtil.getField(Class.forName(CLASS_BASE_DEX_CLASSLOADER), origin, FIELD_PATH_LIST);
Object originElements = FieldUtil.getField(Class.forName(CLASS_DEX_PATH_LIST), originDexPathList, FIELD_DEX_ELEMENTS);//拿到Path的Elements
Object array = combineArray(originElements, pluginElements);//合并數組
FieldUtil.setField(Class.forName(CLASS_DEX_PATH_LIST), originDexPathList, FIELD_DEX_ELEMENTS, array);//設定回PathClassLoader
Log.i(TAG, "插件檔案加載成功");
}
private static Object combineArray(Object pathElements, Object dexElements) {//合并數組
Class<?> componentType = pathElements.getClass().getComponentType();
int i = Array.getLength(pathElements);
int j = Array.getLength(dexElements);
int k = i + j;
Object result = Array.newInstance(componentType, k);
System.arraycopy(dexElements, 0, result, 0, j);
System.arraycopy(pathElements, 0, result, j, i);
return result;
}
}
這裡我們約定将插件APK放在/storage/emulated/0/Android/data/$packageName/files/plugin目錄,然後為了盡早加載是以在Application中執行加載邏輯。
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
InjectUtil.inject(this, getClassLoader());//加載插件Apk的類檔案
} catch (Exception e) {
e.printStackTrace();
}
}
}
Hook啟動流程
在說之前我們得先了解下Activity的啟動流程。
上圖抽象的給出了Acticity的啟動過程。在應用程式程序中的Activity向AMS請求建立Activity(步驟1),AMS會對這個Activty的生命周期棧進行管理,校驗Activity等等。如果Activity滿足AMS的校驗,AMS就會請求應用程式程序中的ActivityThread去建立并啟動Activity。
那麼在上一步我們已經将插件Apk的類檔案加載進來了,但是我們并不能通過startActivity的方式去啟動PluginActivity,因為PluginActivity并沒有在AndroidManifest中注冊過不了AMS的驗證,既然這樣我們換一個思路。
- 在宿主項目中提前弄一個SubActivity占坑,在啟動PluginActivity的時候替換為啟動這個SubActivity繞過驗證。
- 在AMS處理完相應驗證通知我們ActivityThread建立Activty的時候在替換為PluginActivity。
占坑SubActivity非常簡單
public class SubActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
然後在AndroidManifest注冊好即可
對于startActivity()最終都會調到ActivityManagerService的startActivity()方法。
ActivityManager.getService()//擷取AMS
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
那麼我們可以通過動态代理hook ActivityManagerService,然後在startActivity()的時候将PluginActivity替換為SubActivity,不過對于ActivityManagerService的擷取不同版本方式有所不同。
在Android7.0以下會調用ActivityManagerNative的getDefault方法擷取,如下所示。
static public IActivityManager getDefault() {
return gDefault.get();
}
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");//擷取ams
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);//拿到ams代理對象
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}
};
getDefault()傳回的是IActivityManager,而gDefault是一個單例對象Singleton并且是靜态的是非常容易用反射擷取。
Android8.0會調用ActivityManager的getService方法擷取,如下所示。
public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);//拿到ams
final IActivityManager am = IActivityManager.Stub.asInterface(b);//拿到ams代理對象
return am;
}
};
傳回一個IActivityManager,而IActivityManagerSingleton是一個單例對象Singleton并且是靜态非常容易擷取。
在看下上面提到的Singleton等會hook會用到
public abstract class Singleton<T> {
private T mInstance;
protected abstract T create();
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}
到這裡會發現其實傳回的都是AMS的接口IActivityManager,那麼我們隻要能通過反射拿到,然後通過動态代理去Hook這個接口在啟動的時候把PluginActivity替換為SubActivity即可繞過AMS的驗證。
public class IActivityManagerProxy implements InvocationHandler {//動态代理
private final Object am;
public IActivityManagerProxy(Object am) {//傳入代理的AMS對象
this.am = am;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {//startActivity方法
Intent oldIntent = null;
int i = 0;
for (; i < args.length - 1; i++) {//擷取startActivity Intent參數
if (args[i] instanceof Intent) {
oldIntent = (Intent) args[i];
break;
}
}
Intent newIntent = new Intent();//建立新的Intent
newIntent.setClassName("rocketly.demo", "rocketly.demo.SubActivity");//啟動目标SubActivity
newIntent.putExtra(HookHelper.TRANSFER_INTENT, oldIntent);//保留原始intent
args[i] = newIntent;//把插件Intent替換為占坑Intent
}
return method.invoke(am, args);
}
}
動态代理寫好後,我們還需要通過反射去hook住原始AMS。因為會用到反射弄了一個簡單的工具類
public class FieldUtil {
public static Object getField(Class clazz, Object target, String name) throws Exception {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field.get(target);
}
public static Field getField(Class clazz, String name) throws Exception {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field;
}
public static void setField(Class clazz, Object target, String name, Object value) throws Exception {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
field.set(target, value);
}
}
接下來是hook代碼
public class HookHelper {
public static final String TRANSFER_INTENT = "transfer_intent";
public static void hookAMS() throws Exception {
Object singleton = null;
if (Build.VERSION.SDK_INT >= 26) {//大于等于8.0
Class<?> clazz = Class.forName("android.app.ActivityManager");
singleton = FieldUtil.getField(clazz, null, "IActivityManagerSingleton");//拿到靜态字段
} else {//8.0以下
Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
singleton = FieldUtil.getField(activityManagerNativeClazz, null, "gDefault");//拿到靜态字段
}
Class<?> singleClazz = Class.forName("android.util.Singleton");
Method getMethod = singleClazz.getMethod("get");
Object iActivityManager = getMethod.invoke(singleton);//拿到AMS
Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{iActivityManagerClazz}, new IActivityManagerProxy(iActivityManager));//生成動态代理
FieldUtil.setField(singleClazz, singleton, "mInstance", proxy);//将代理後的對象設定回去
}
}
接下來我們需要在Application去執行hook
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
InjectUtil.inject(this, getClassLoader());//加載插件Apk的類檔案
HookHelper.hookAMS();//hookAMS
} catch (Exception e) {
e.printStackTrace();
}
}
}
那麼這裡我們已經實作了第一步
在宿主項目中提前弄一個SubActivity占坑,在啟動PluginActivity的時候替換為啟動這個SubActivity繞過驗證。
接下來我們在看如何在收到AMS建立Activity的通知時替換回PluginActivity。
AMS建立Activity的通知會先發送到ApplicationThread,然後ApplicationThread會通過Handler去執行對應邏輯。
private class ApplicationThread extends IApplicationThread.Stub {
@Override
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
int procState, Bundle state, PersistableBundle persistentState,
List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {//收到AMS啟動Activity事件
ActivityClientRecord r = new ActivityClientRecord();
r.intent = intent;//給r賦上要啟動的intent
...//省略很多r屬性初始化
sendMessage(H.LAUNCH_ACTIVITY, r);//發送r到Handler
}
private void sendMessage(int what, Object obj) {
sendMessage(what, obj, 0, 0, false);
}
private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
Message msg = Message.obtain();
msg.what = what;
msg.obj = obj;
msg.arg1 = arg1;
msg.arg2 = arg2;
if (async) {
msg.setAsynchronous(true);
}
mH.sendMessage(msg);//發送到mH
}
}
private class H extends Handler {
public static final int LAUNCH_ACTIVITY = 100;
public void handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY: {
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");//執行啟動activity
} break;
}
}
}
既然是通過sendMessage()方式通知Handler去執行對應的方法,那麼在調用handleMessage()之前會通過dispatchMessage()分發事件。
public class Handler {
final Callback mCallback;
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
public interface Callback {
public boolean handleMessage(Message msg);
}
}
可以發現一個很好的hook點就是mCallback這個接口,可以讓我們在handleMessage方法之前将ActivityClientRecord中的SubActivity Intent替換回PluginActivity Intent。
public class HCallback implements Handler.Callback {//實作Callback接口
public static final int LAUNCH_ACTIVITY = 100;
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY://啟動事件
Object obj = msg.obj;
try {
Intent intent = (Intent) FieldUtil.getField(obj.getClass(), obj, "intent");//拿到ActivityClientRecord的intent字段
Intent targetIntent = intent.getParcelableExtra(HookHelper.TRANSFER_INTENT);//拿到我們要啟動PluginActivity的Intent
intent.setComponent(targetIntent.getComponent());//替換為啟動PluginActivity
} catch (Exception e) {
e.printStackTrace();
}
break;
}
return false;
}
}
接下來就是我們需要将這個Callback設定給Handler,而剛剛說的Handler是ActivityThread的成員變量mH,ActivityThread執行個體則可以通過他的靜态字段sCurrentActivityThread擷取。
public final class ActivityThread {
private static volatile ActivityThread sCurrentActivityThread;
final H mH = new H();
}
然後我們通過反射給mH設定Callback
public class HookHelper {
...//省略前面的hookAMS()方法
public static void hookH() throws Exception {
Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");
Object activityThread = FieldUtil.getField(activityThreadClazz, null, "sCurrentActivityThread");//拿到activityThread
Object mH = FieldUtil.getField(activityThreadClazz, activityThread, "mH");//拿到mH
FieldUtil.setField(Handler.class, mH, "mCallback", new HCallback());//給mH設定callback
}
}
依舊是在Application初始化這段hook邏輯
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
InjectUtil.inject(this, getClassLoader());//加載插件Apk的類檔案
HookHelper.hookAMS();
HookHelper.hookH();
} catch (Exception e) {
e.printStackTrace();
}
}
}
到這裡完成了我們上面說的第二步,并且成功啟動了PluginActivity
在AMS處理完相應驗證通知我們ActivityThread建立Activty的時候在替換為PluginActivity。
不過這裡肯定會有人問啟動是啟動了但是沒有生命周期,對于AMS那邊他隻知道我們啟動的是SubActivity,那麼接下來我們解釋生命周期如何處理。
插件Activity生命周期
其實不用做任何處理就已經有生命周期了,那麼我們看看是為何。
先回顧下啟動的流程
- AMS通知ApplicationThread啟動Activity
- ApplicationThread發送事件到Handler
- Handler調用handleLaunchActivity去執行啟動邏輯
- 然後在handleLaunchActivity方法中建立對應的Activity
對你會發現Activity是在應用程序建立的,AMS是沒有該Activity的引用的,那麼AMS必須得有一個唯一辨別來辨別該Activity,然後應用程序存儲這個辨別和Activity的對應關系,這樣當AMS通知應用程序生命周期事件的時候隻需要告訴應用程序需要執行該事件的Activity辨別就可以了,然後應用程序通過辨別找到Activity具體執行即可。
那我們先看下建立Activity的時候是如何存儲這個關系的。
private class ApplicationThread extends IApplicationThread.Stub {
@Override
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
int procState, Bundle state, PersistableBundle persistentState,
List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {//AMS通知啟動Activity
ActivityClientRecord r = new ActivityClientRecord();
r.token = token;//這個token正是Activity的唯一标示
...//省略很多r屬性初始化
sendMessage(H.LAUNCH_ACTIVITY, r);//發送到Handler
}
}
private class H extends Handler {
public void handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY: {//啟動Activity事件
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");//執行啟動的方法
} break;
}
}
}
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
Activity a = performLaunchActivity(r, customIntent);//真正執行啟動的方法
}
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ComponentName component = r.intent.getComponent();//拿到intent中的元件
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);//通過反射建立Activity
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
r.activity = activity;//将建立的好的Activity存儲在ActivityClientRecord對象中
mActivities.put(r.token, r);//然後用一個Map存儲token和ActivityClientRecord的對應關系
return activity;
}
final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();//存儲對應關系的Map
從代碼中可以看出,在建立Activity之後将Activity存儲到了ActivityClientRecord對象中,然後用AMS傳來的token作為鍵ActivityClientRecord作為值存儲到Map中。
而在ActivityThread中執行生命周期的方法一般命名為
perform$事件名Activity()
,那麼直接看該方法
public final ActivityClientRecord performResumeActivity(IBinder token,
boolean clearHide, String reason) {
ActivityClientRecord r = mActivities.get(token);//通過AMS token拿到ActivityClientRecord
r.activity.performResume();//執行Resume事件
}
private ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
int configChanges, boolean getNonConfigInstance) {
ActivityClientRecord r = mActivities.get(token);//通過AMS token拿到ActivityClientRecord
mInstrumentation.callActivityOnDestroy(r.activity);//執行Destroy事件
}
随便找了兩個執行生命周期事件的方法,都是通過AMS的token找到ActivityClientRecord然後拿到裡面的Activity執行生命周期方法。
那麼在分析下為啥,建立的PluginActivity會有生命呢,因為我們是在Handler将StubActivity替換為PluginActivity,然後在performLaunchActivity方法中,會将PluginActivity建立并且添加到ActivityClientRecord然後用AMS傳來的token作為鍵ActivityClientRecord作為值存儲到Map中,那麼在接下來的生命周期方法AMS是通過token來通知應用程序執行生命周期方法,而這個token所對應的Activity就是PluginActivity,是以PluginActivity就有了生命。
初始化插件資源
前面我們已經完成了PluginActivity的啟動和生命周期事件,但是PluginActivity沒法setContentView()這種方式通過id去操作布局,因為凡是通過id去擷取資源的方式都是通過Resource去擷取的,但是宿主APK并不知道插件APK的存在,是以宿主Resource也沒法加載插件APK的資源。
那麼這裡我們可以給插件APK建立一個Resources,然後插件APK中都通過這個Resource去擷取資源。這裡看下Resources構造方法
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
有三個參數
- AssetManager真正加載資源的(根據插件APK路徑建立AssetManager加載資源)
- DisplayMetrics顯示配置(直接用宿主的Resources的配置即可)
- Configuration配置(直接用宿主的Resources的配置即可)
接下來看AssetManager如何建立
public final class AssetManager implements AutoCloseable {
public AssetManager() {
synchronized (this) {
if (DEBUG_REFS) {
mNumRefs = 0;
incRefsLocked(this.hashCode());
}
init(false);
if (localLOGV) Log.v(TAG, "New asset manager: " + this);
ensureSystemAssets();
}
}
/**
* Add an additional set of assets to the asset manager. This can be
* either a directory or ZIP file. Not for use by applications. Returns
* the cookie of the added asset, or 0 on failure.
* {@hide}
*/
public final int addAssetPath(String path) {//傳入需要加載資源的路徑
return addAssetPathInternal(path, false);
}
}
直接通過空參構造方法建立,然後調用addAssetPath()去加載對應路徑的資源。
接下來我們在Application中建立插件的Resources,之是以在這裡建立也是有原因的,友善插件APK中擷取到這個Resources。
public class MyApplication extends Application {
private Resources pluginResource;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
InjectUtil.inject(this, getClassLoader());//加載插件Apk的類檔案
HookHelper.hookAMS();
HookHelper.hookH();
initPluginResource();
} catch (Exception e) {
e.printStackTrace();
}
}
private void initPluginResource() throws Exception {
Class<AssetManager> clazz = AssetManager.class;
AssetManager assetManager = clazz.newInstance();//建立AssetManager
Method method = clazz.getMethod("addAssetPath", String.class);//拿到addAssetPath方法
method.invoke(assetManager, getExternalFilesDir("plugin").listFiles()[0].getAbsolutePath());//調用addAssetPath傳入插件APk路徑
pluginResource = new Resources(assetManager, getResources().getDisplayMetrics(), getResources().getConfiguration());//生成插件Resource
}
@Override
public Resources getResources() {
return pluginResource == null ? super.getResources() : pluginResource;
}
}
這裡我們解釋下為啥插件Resources在Application初始化插件APK友善擷取,因為插件APK中的四大元件實際都是在宿主APK建立的,那麼他們拿到的Application實際上都是宿主的,是以他們隻需要通過
getApplication().getResources()
就可以非常友善的拿到插件Resource。
插件工程
插件工程比較簡單,就是一個Activity,不過有點需要注意的是重寫了
getResources()
方法,因為我們需要通過插件Resources才能用id去操作資源檔案。
public class PluginActivity extends Activity {//這裡需要注意繼承的是Activity不是AppCompatActivity,因為AppCompatActivity做了很多檢查用它的話還需要多hook幾個類,而我們主要是流程和原理的掌握就沒有進行适配了。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_plugin);
}
@Override
public Resources getResources() {//重寫getResources()是因為對于activity中通過id擷取資源的Resources都是通過該方法擷取
return getApplication() != null && getApplication().getResources() != null ? getApplication().getResources() : super.getResources();//拿到插件Resources
}
}
測試流程
測試流程這裡說明下
- 将插件項目打包成APK
- 然後通過adb指令
将APK推到記憶體卡中adb push <local> <remote>
- 宿主應用加載插件APK,能顯示插件Activity布局即為成功