small插件化框架源码分析
简介
small是一款轻量级插件化方案,核心逻辑主要是ActivityLuncher、ApkBundleLuncher、WebBundleLuncher
流程
1.BundleLauncher加载流程 预加载Small.preSetUp(this);->onCreate
初始化插件 Small.setUp(Context context, OnCompleteListener listener)Bundle.loadLaunchableBundles(listener);Bundle.loadBundles(context);Bundle.setupLaunchers(context);
protected static void setupLaunchers(Context context) {
if (sBundleLaunchers == null) return;
for (BundleLauncher launcher : sBundleLaunchers) {
launcher.setUp(context);
}
}
Bundle.loadBundles(manifest.bundles);
private static void loadBundles(List<Bundle> bundles) {
sPreloadBundles = bundles;
// Prepare bundle
for (Bundle bundle : bundles) {
bundle.prepareForLaunch();
}
// Handle I/O
if (sIOActions != null) {
ExecutorService executor = Executors.newFixedThreadPool(sIOActions.size());
for (Runnable action : sIOActions) {
executor.execute(action);
}
executor.shutdown();
try {
if (!executor.awaitTermination(LOADING_TIMEOUT_MINUTES, TimeUnit.MINUTES)) {
throw new RuntimeException("Failed to load bundles! (TIMEOUT > "
+ LOADING_TIMEOUT_MINUTES + "minutes)");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
sIOActions = null;
}
// Wait for the things to be done on UI thread before `postSetUp`,
// as on 7.0+ we should wait a WebView been initialized. (#347)
while (sRunningUIActionCount != 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Notify `postSetUp' to all launchers
for (BundleLauncher launcher : sBundleLaunchers) {
launcher.postSetUp();
}
// Wait for the things to be done on UI thread after `postSetUp`,
// like creating a bundle application.
while (sRunningUIActionCount != 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Free all unused temporary variables
for (Bundle bundle : bundles) {
if (bundle.parser != null) {
bundle.parser.close();
bundle.parser = null;
}
bundle.mBuiltinFile = null;
bundle.mExtractPath = null;
}
}
调用prepareForLaunch ,postSetUp
释放未使用插件 bundle.parser.close(); 加载完成时打开插件的控件 Small.openUri 若是https,http,file,或插件没有加载,紧接着调用Bundle.getLaunchableBundle(uri) ->bundle.prepareForLaunch();->resolveBundle(this)->preloadBundle(bundle) -loadBundle(bundle);->launchBundle
2.ActivityLauncher,ApkLaunchBundle,WebBundleLaunch主要是用来解析插件中清单文件得到ActivityInfo,把原始intent信息保存到bundle中ApkLaunchBundle主要使用反射ActivityThread.mInstrumentation,使用动态代理拦截startActivity
ActivityLuncher
<!--保存真正的activity到bundle,绕过检测后在activitythread中替换-->
public void prelaunchBundle(Bundle bundle) {
super.prelaunchBundle(bundle);
Intent intent = new Intent();
bundle.setIntent(intent);
// Intent extras - class
String activityName = bundle.getActivityName();
if (!sActivityClasses.contains(activityName)) {
if (activityName.endsWith("Activity")) {
throw new ActivityNotFoundException("Unable to find explicit activity class " +
"{ " + activityName + " }");
}
String tempActivityName = activityName + "Activity";
if (!sActivityClasses.contains(tempActivityName)) {
throw new ActivityNotFoundException("Unable to find explicit activity class " +
"{ " + activityName + "(Activity) }");
}
activityName = tempActivityName;
}
intent.setComponent(new ComponentName(Small.getContext(), activityName));
// Intent extras - params
String query = bundle.getQuery();
if (query != null) {
intent.putExtra(Small.KEY_QUERY, '?'+query);
}
}
ApkBundleLuncher
hook ActivityThread替换Instrumentation和mH,绕过检查后,替换真正的Intent
加载资源
- File.loadDex 优化dex
- 反射addAsertPaths,传入资源路径
- ReflectAccelerator.expandDexPathList(cl, dexPaths, dexFiles);合并dex
- ReflectAccelerator.mergeResources(app, sActivityThread, paths);合并资源
- ReflectAccelerator.expandNativeLibraryDirectories(cl, libPathList);合并动态库
资源冲突
为保证整合在一起的程序资源id不冲突,对组件包分配 [0x03, 0x7e] 之间的package id,直接合并资源路径,不会出现资源冲突
public class ApkBundleLauncher extends SoBundleLauncher {
private static final String PACKAGE_NAME = ApkBundleLauncher.class.getPackage().getName();
private static final String STUB_ACTIVITY_PREFIX = PACKAGE_NAME + ".A";
private static final String STUB_ACTIVITY_TRANSLUCENT = STUB_ACTIVITY_PREFIX + '1';
private static final String TAG = "ApkBundleLauncher";
private static final String FD_STORAGE = "storage";
private static final String FILE_DEX = "bundle.dex";
private static final String STUB_QUEUE_RESTORE_KEY = "small.stubQueue";
private static class LoadedApk {
public String packageName;
public File packagePath;
public String applicationName;
public String path;
public DexFile dexFile;
public File optDexFile;
public File libraryPath;
public boolean nonResources; /** no resources.arsc */
}
private static ConcurrentHashMap<String, LoadedApk> sLoadedApks;
private static ConcurrentHashMap<String, ActivityInfo> sLoadedActivities;
private static ConcurrentHashMap<String, List<IntentFilter>> sLoadedIntentFilters;
private static Instrumentation sHostInstrumentation;
private static InstrumentationWrapper sBundleInstrumentation;
private static ActivityThreadHandlerCallback sActivityThreadHandlerCallback;
private static final char REDIRECT_FLAG = '>';
private static Object sActivityThread;
private static List<ProviderInfo> sProviders;
private static List<ProviderInfo> mLazyInitProviders;
/**
* 替换ActivityThread中mH 的callback成员变量
*/
private static class ActivityThreadHandlerCallback implements Handler.Callback {
private static final int LAUNCH_ACTIVITY = 100;
private static final int CREATE_SERVICE = 114;
private static final int CONFIGURATION_CHANGED = 118;
private static final int ACTIVITY_CONFIGURATION_CHANGED = 125;
private static final int EXECUTE_TRANSACTION = 159; // since Android P
private Configuration mApplicationConfig;
interface ActivityInfoReplacer {
void replace(ActivityInfo info);
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY:
redirectActivity(msg);
break;
case EXECUTE_TRANSACTION:
redirectActivityForP(msg);
break;
case CREATE_SERVICE:
ensureServiceClassesLoadable(msg);
break;
case CONFIGURATION_CHANGED:
recordConfigChanges(msg);
break;
case ACTIVITY_CONFIGURATION_CHANGED:
return relaunchActivityIfNeeded(msg);
default:
break;
}
return false;
}
private void redirectActivityForP(Message msg) {
if (Build.VERSION.SDK_INT >= 28) {
// Following APIs cannot be called again since android 9.0.
return;
}
Object/*android.app.servertransaction.ClientTransaction*/ t = msg.obj;
List callbacks = ReflectAccelerator.getLaunchActivityItems(t);
if (callbacks == null) return;
for (final Object/*LaunchActivityItem*/ item : callbacks) {
Intent intent = ReflectAccelerator.getIntentOfLaunchActivityItem(item);
tryReplaceActivityInfo(intent, new ActivityInfoReplacer() {
@Override
public void replace(ActivityInfo targetInfo) {
ReflectAccelerator.setActivityInfoToLaunchActivityItem(item, targetInfo);
}
});
}
}
private void redirectActivity(Message msg) {
final Object/*ActivityClientRecord*/ r = msg.obj;
Intent intent = ReflectAccelerator.getIntent(r);
tryReplaceActivityInfo(intent, new ActivityInfoReplacer() {
@Override
public void replace(ActivityInfo targetInfo) {
ReflectAccelerator.setActivityInfo(r, targetInfo);
}
});
}
static void tryReplaceActivityInfo(Intent intent, ActivityInfoReplacer replacer) {
if (intent == null) return;
String targetClass = unwrapIntent(intent);
boolean hasSetUp = Small.hasSetUp();
if (targetClass == null) {
// The activity was register in the host.
if (hasSetUp) return; // nothing to do
if (intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
// The launcher activity will setup Small.
return;
}
// Launching an activity in remote process. Set up Small for it.
Small.setUpOnDemand();
return;
}
if (!hasSetUp) {
// Restarting an activity after application recreated,
// maybe upgrading or somehow the application was killed in background.
Small.setUp();
}
// Replace with the REAL activityInfo
ActivityInfo targetInfo = sLoadedActivities.get(targetClass);
replacer.replace(targetInfo);
// Ensure the merged application-scope resource has been cached so that
// the incoming activity can attach to it without creating a new(unmerged) one.
ReflectAccelerator.ensureCacheResources();
}
private void ensureServiceClassesLoadable(Message msg) {
Object/*ActivityThread$CreateServiceData*/ data = msg.obj;
ServiceInfo info = ReflectAccelerator.getServiceInfo(data);
if (info == null) return;
String appProcessName = Small.getContext().getApplicationInfo().processName;
if (!appProcessName.equals(info.processName)) {
// Cause Small is only setup in current application process, if a service is specified
// with a different process('android:process=xx'), then we should also setup Small for
// that process so that the service classes can be successfully loaded.
Small.setUpOnDemand();
} else {
// The application might be started up by a background service
if (Small.isFirstSetUp()) {
Log.e(TAG, "Starting service before Small has setup, this might block the main thread!");
}
Small.setUpOnDemand();
}
}
private void recordConfigChanges(Message msg) {
mApplicationConfig = (Configuration) msg.obj;
}
private boolean relaunchActivityIfNeeded(Message msg) {
try {
Field f = sActivityThread.getClass().getDeclaredField("mActivities");
f.setAccessible(true);
Map mActivities = (Map) f.get(sActivityThread);
Object /*ActivityThread$ActivityConfigChangeData*/ data = msg.obj;
Object token;
if (data instanceof IBinder) {
token = data;
} else {
f = data.getClass().getDeclaredField("activityToken");
f.setAccessible(true);
token = f.get(data);
}
Object /*ActivityClientRecord*/ r = mActivities.get(token);
Intent intent = ReflectAccelerator.getIntent(r);
String bundleActivityName = unwrapIntent(intent);
if (bundleActivityName == null) {
return false;
}
f = r.getClass().getDeclaredField("activity");
f.setAccessible(true);
Activity activity = (Activity) f.get(r);
f = Activity.class.getDeclaredField("mCurrentConfig");
f.setAccessible(true);
Configuration activityConfig = (Configuration) f.get(activity);
if (mApplicationConfig == null) {
// The application config is not ready yet.
// This may be called on Android 7.0 multi-window-mode.
return false;
}
// Calculate the changes
int configDiff = activityConfig.diff(mApplicationConfig);
if (configDiff == 0) {
return false;
}
// Check if the activity can handle the changes
ActivityInfo bundleActivityInfo = sLoadedActivities.get(bundleActivityName);
if ((configDiff & (~bundleActivityInfo.configChanges)) == 0) {
return false;
}
// The activity isn't handling the change, relaunch it.
return ReflectAccelerator.relaunchActivity(activity, sActivityThread, token);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
/**
* Class for redirect activity from Stub(AndroidManifest.xml) to Real(Plugin)
*/
protected static class InstrumentationWrapper extends Instrumentation
implements InstrumentationInternal {
private Instrumentation mBase;
private static final int STUB_ACTIVITIES_COUNT = 4;
public InstrumentationWrapper(Instrumentation base) {
mBase = base;
}
/** @Override V21+
* Wrap activity from REAL to STUB */
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, android.os.Bundle options) {
wrapIntent(intent);
ensureInjectMessageHandler(sActivityThread);
return ReflectAccelerator.execStartActivity(mBase,
who, contextThread, token, target, intent, requestCode, options);
}
/** @Override V20-
* Wrap activity from REAL to STUB */
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode) {
wrapIntent(intent);
ensureInjectMessageHandler(sActivityThread);
return ReflectAccelerator.execStartActivity(mBase,
who, contextThread, token, target, intent, requestCode);
}
@Override
public Activity newActivity(ClassLoader cl, final String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
final String[] targetClassName = {className};
if (Build.VERSION.SDK_INT >= 28) {
ActivityThreadHandlerCallback.tryReplaceActivityInfo(intent, new ActivityThreadHandlerCallback.ActivityInfoReplacer() {
@Override
public void replace(ActivityInfo info) {
targetClassName[0] = info.targetActivity; // Redirect to the plugin activity
}
});
}
return mBase.newActivity(cl, targetClassName[0], intent);
}
@Override
/** Prepare resources for REAL */
public void callActivityOnCreate(Activity activity, android.os.Bundle icicle) {
do {
if (sLoadedActivities == null) break;
ActivityInfo ai = sLoadedActivities.get(activity.getClass().getName());
if (ai == null) break;
applyActivityInfo(activity, ai);
} while (false);
// Reset activity instrumentation if it was modified by some other applications #245
if (sBundleInstrumentation != null) {
try {
Field f = Activity.class.getDeclaredField("mInstrumentation");
f.setAccessible(true);
Object instrumentation = f.get(activity);
if (instrumentation != sBundleInstrumentation) {
f.set(activity, sBundleInstrumentation);
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
sHostInstrumentation.callActivityOnCreate(activity, icicle);
}
@Override
public void callActivityOnSaveInstanceState(Activity activity, android.os.Bundle outState) {
sHostInstrumentation.callActivityOnSaveInstanceState(activity, outState);
if (mStubQueue != null) {
outState.putCharSequenceArray(STUB_QUEUE_RESTORE_KEY, mStubQueue);
}
}
@Override
public void callActivityOnRestoreInstanceState(Activity activity, android.os.Bundle savedInstanceState) {
sHostInstrumentation.callActivityOnRestoreInstanceState(activity, savedInstanceState);
if (mStubQueue == null) {
mStubQueue = savedInstanceState.getStringArray(STUB_QUEUE_RESTORE_KEY);
}
}
@Override
public void callActivityOnStop(Activity activity) {
sHostInstrumentation.callActivityOnStop(activity);
if (!Small.isUpgrading()) return;
// If is upgrading, we are going to kill self while application turn into background,
// and while we are back to foreground, all the things(code & layout) will be reload.
// Don't worry about the data missing in current activity, you can do all the backups
// with your activity's `onSaveInstanceState' and `onRestoreInstanceState'.
// Get all the processes of device (1)
ActivityManager am = (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningAppProcessInfo> processes = am.getRunningAppProcesses();
if (processes == null) return;
// Gather all the processes of current application (2)
// Above 5.1.1, this may be equals to (1), on the safe side, we also
// filter the processes with current package name.
String pkg = activity.getApplicationContext().getPackageName();
final List<RunningAppProcessInfo> currentAppProcesses = new ArrayList<>(processes.size());
for (RunningAppProcessInfo p : processes) {
if (p.pkgList == null) continue;
boolean match = false;
int N = p.pkgList.length;
for (int i = 0; i < N; i++) {
if (p.pkgList[i].equals(pkg)) {
match = true;
break;
}
}
if (!match) continue;
currentAppProcesses.add(p);
}
if (currentAppProcesses.isEmpty()) return;
// The top process of current application processes.
RunningAppProcessInfo currentProcess = currentAppProcesses.get(0);
if (currentProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND) return;
// Seems should delay some time to ensure the activity can be successfully
// restarted after the application restart.
// FIXME: remove following thread if you find the better place to `killProcess'
new Thread() {
@Override
public void run() {
try {
sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (RunningAppProcessInfo p : currentAppProcesses) {
android.os.Process.killProcess(p.pid);
}
}
}.start();
}
@Override
public void callActivityOnDestroy(Activity activity) {
do {
if (sLoadedActivities == null) break;
String realClazz = activity.getClass().getName();
ActivityInfo ai = sLoadedActivities.get(realClazz);
if (ai == null) break;
inqueueStubActivity(ai, realClazz);
} while (false);
sHostInstrumentation.callActivityOnDestroy(activity);
}
@Override
public boolean onException(Object obj, Throwable e) {
if (e.getClass().equals(ClassNotFoundException.class)) {
if (sProviders == null) return super.onException(obj, e);
boolean errorOnInstallProvider = false;
StackTraceElement[] stacks = e.getStackTrace();
for (StackTraceElement st : stacks) {
if (st.getMethodName().equals("installProvider")) {
errorOnInstallProvider = true;
break;
}
}
if (errorOnInstallProvider) {
// We'll reinstall this content provider later, so just ignores it!!!
// FIXME: any better way to get the class name?
String msg = e.getMessage();
final String prefix = "Didn't find class \"";
if (msg.startsWith(prefix)) {
String providerClazz = msg.substring(prefix.length());
providerClazz = providerClazz.substring(0, providerClazz.indexOf("\""));
for (ProviderInfo info : sProviders) {
if (info.name.equals(providerClazz)) {
if (mLazyInitProviders == null) {
mLazyInitProviders = new ArrayList<ProviderInfo>();
}
mLazyInitProviders.add(info);
break;
}
}
}
return true;
}
} else if (HealthManager.fixException(obj, e)) {
return true;
}
return super.onException(obj, e);
}
private void wrapIntent(Intent intent) {
ComponentName component = intent.getComponent();
String realClazz;
if (component == null) {
// Try to resolve the implicit action which has registered in host.
component = intent.resolveActivity(Small.getContext().getPackageManager());
if (component != null) {
// A system or host action, nothing to be done.
return;
}
// Try to resolve the implicit action which has registered in bundles.
realClazz = resolveActivity(intent);
if (realClazz == null) {
// Cannot resolved, nothing to be done.
return;
}
} else {
realClazz = component.getClassName();
if (realClazz.startsWith(STUB_ACTIVITY_PREFIX)) {
// Re-wrap to ensure the launch mode works.
realClazz = unwrapIntent(intent);
}
}
if (sLoadedActivities == null) return;
ActivityInfo ai = sLoadedActivities.get(realClazz);
if (ai == null) return;
// Carry the real(plugin) class for incoming `newActivity' method.
intent.addCategory(REDIRECT_FLAG + realClazz);
String stubClazz = dequeueStubActivity(ai, realClazz);
intent.setComponent(new ComponentName(Small.getContext(), stubClazz));
}
private String resolveActivity(Intent intent) {
if (sLoadedIntentFilters == null) return null;
Iterator<Map.Entry<String, List<IntentFilter>>> it =
sLoadedIntentFilters.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, List<IntentFilter>> entry = it.next();
List<IntentFilter> filters = entry.getValue();
for (IntentFilter filter : filters) {
if (filter.hasAction(Intent.ACTION_VIEW)) {
// TODO: match uri
}
if (filter.hasCategory(Intent.CATEGORY_DEFAULT)) {
// custom action
if (filter.hasAction(intent.getAction())) {
// hit
return entry.getKey();
}
}
}
}
return null;
}
private String[] mStubQueue;
/** Get an usable stub activity clazz from real activity */
private String dequeueStubActivity(ActivityInfo ai, String realActivityClazz) {
if (ai.launchMode == ActivityInfo.LAUNCH_MULTIPLE) {
// In standard mode, the stub activity is reusable.
// Cause the `windowIsTranslucent' attribute cannot be dynamically set,
// We should choose the STUB activity with translucent or not here.
Resources.Theme theme = Small.getContext().getResources().newTheme();
theme.applyStyle(ai.getThemeResource(), true);
TypedArray sa = theme.obtainStyledAttributes(
new int[] { android.R.attr.windowIsTranslucent });
boolean translucent = sa.getBoolean(0, false);
sa.recycle();
return translucent ? STUB_ACTIVITY_TRANSLUCENT : STUB_ACTIVITY_PREFIX;
}
int availableId = -1;
int stubId = -1;
int countForMode = STUB_ACTIVITIES_COUNT;
int countForAll = countForMode * 3; // 3=[singleTop, singleTask, singleInstance]
if (mStubQueue == null) {
// Lazy init
mStubQueue = new String[countForAll];
}
int offset = (ai.launchMode - 1) * countForMode;
for (int i = 0; i < countForMode; i++) {
String usedActivityClazz = mStubQueue[i + offset];
if (usedActivityClazz == null) {
if (availableId == -1) availableId = i;
} else if (usedActivityClazz.equals(realActivityClazz)) {
stubId = i;
}
}
if (stubId != -1) {
availableId = stubId;
} else if (availableId != -1) {
mStubQueue[availableId + offset] = realActivityClazz;
} else {
// TODO:
Log.e(TAG, "Launch mode " + ai.launchMode + " is full");
}
return STUB_ACTIVITY_PREFIX + ai.launchMode + availableId;
}
/** Unbind the stub activity from real activity */
private void inqueueStubActivity(ActivityInfo ai, String realActivityClazz) {
if (ai.launchMode == ActivityInfo.LAUNCH_MULTIPLE) return;
if (mStubQueue == null) return;
int countForMode = STUB_ACTIVITIES_COUNT;
int offset = (ai.launchMode - 1) * countForMode;
for (int i = 0; i < countForMode; i++) {
String stubClazz = mStubQueue[i + offset];
if (stubClazz != null && stubClazz.equals(realActivityClazz)) {
mStubQueue[i + offset] = null;
break;
}
}
}
private void setStubQueue(String mode, String realActivityClazz) {
int launchMode = mode.charAt(0) - '0';
int stubIndex = mode.charAt(1) - '0';
int offset = (launchMode - 1) * STUB_ACTIVITIES_COUNT + stubIndex;
if (mStubQueue == null) {
mStubQueue = new String[STUB_ACTIVITIES_COUNT * 3];
}
mStubQueue[offset] = realActivityClazz;
}
}
private static void ensureInjectMessageHandler(Object thread) {
try {
Field f = thread.getClass().getDeclaredField("mH");
f.setAccessible(true);
Handler ah = (Handler) f.get(thread);
f = Handler.class.getDeclaredField("mCallback");
f.setAccessible(true);
boolean needsInject = false;
if (sActivityThreadHandlerCallback == null) {
needsInject = true;
} else {
Object callback = f.get(ah);
if (callback != sActivityThreadHandlerCallback) {
needsInject = true;
}
}
if (needsInject) {
// Inject message handler
sActivityThreadHandlerCallback = new ActivityThreadHandlerCallback();
f.set(ah, sActivityThreadHandlerCallback);
}
} catch (Exception e) {
throw new RuntimeException("Failed to replace message handler for thread: " + thread);
}
}
public static void wrapIntent(Intent intent) {
sBundleInstrumentation.wrapIntent(intent);
}
private static String unwrapIntent(Intent intent) {
Set<String> categories = intent.getCategories();
if (categories == null) return null;
// Get plugin activity class name from categories
Iterator<String> it = categories.iterator();
while (it.hasNext()) {
String category = it.next();
if (category.charAt(0) == REDIRECT_FLAG) {
return category.substring(1);
}
}
return null;
}
/**
* A context wrapper that redirect some host environments to plugin
*/
private static final class BundleApplicationContext extends ContextWrapper {
private LoadedApk mApk;
public BundleApplicationContext(Context base, LoadedApk apk) {
super(base);
mApk = apk;
}
@Override
public String getPackageName() {
return mApk.packageName;
}
@Override
public String getPackageResourcePath() {
return mApk.path;
}
@Override
public ApplicationInfo getApplicationInfo() {
ApplicationInfo ai = super.getApplicationInfo();
// TODO: Read meta-data in bundles and merge to the host one
// ai.metaData.putAll();
return ai;
}
}
@Override
public void onCreate(Application app) {
super.onCreate(app);
Object/*ActivityThread*/ thread;
List<ProviderInfo> providers;
Instrumentation base;
ApkBundleLauncher.InstrumentationWrapper wrapper;
Field f;
// Get activity thread
thread = ReflectAccelerator.getActivityThread(app);
// Replace instrumentation
try {
f = thread.getClass().getDeclaredField("mInstrumentation");
f.setAccessible(true);
base = (Instrumentation) f.get(thread);
wrapper = new ApkBundleLauncher.InstrumentationWrapper(base);
f.set(thread, wrapper);
} catch (Exception e) {
throw new RuntimeException("Failed to replace instrumentation for thread: " + thread);
}
// Inject message handler
ensureInjectMessageHandler(thread);
// Get providers
try {
f = thread.getClass().getDeclaredField("mBoundApplication");
f.setAccessible(true);
Object/*AppBindData*/ data = f.get(thread);
f = data.getClass().getDeclaredField("providers");
f.setAccessible(true);
providers = (List<ProviderInfo>) f.get(data);
} catch (Exception e) {
throw new RuntimeException("Failed to get providers from thread: " + thread);
}
sActivityThread = thread;
sProviders = providers;
sHostInstrumentation = base;
sBundleInstrumentation = wrapper;
}
@Override
public void setUp(Context context) {
super.setUp(context);
Field f;
// AOP for pending intent
try {
f = TaskStackBuilder.class.getDeclaredField("IMPL");
f.setAccessible(true);
final Object impl = f.get(TaskStackBuilder.class);
InvocationHandler aop = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Intent[] intents = (Intent[]) args[1];
for (Intent intent : intents) {
sBundleInstrumentation.wrapIntent(intent);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
}
return method.invoke(impl, args);
}
};
Object newImpl = Proxy.newProxyInstance(context.getClassLoader(), impl.getClass().getInterfaces(), aop);
f.set(TaskStackBuilder.class, newImpl);
} catch (Exception ignored) {
Log.e(TAG, "Failed to hook TaskStackBuilder. \n" +
"Please manually call `Small.wrapIntent` to ensure the notification intent can be opened. \n" +
"See https://github.com/wequick/Small/issues/547 for details.");
}
}
@Override
public void postSetUp() {
super.postSetUp();
if (sLoadedApks == null) {
Log.e(TAG, "Could not find any APK bundles!");
return;
}
Collection<LoadedApk> apks = sLoadedApks.values();
// Merge all the resources in bundles and replace the host one
final Application app = Small.getContext();
String[] paths = new String[apks.size() + 1];
paths[0] = app.getPackageResourcePath(); // add host asset path
int i = 1;
for (LoadedApk apk : apks) {
if (apk.nonResources) continue; // ignores the empty entry to fix #62
paths[i++] = apk.path; // add plugin asset path
}
if (i != paths.length) {
paths = Arrays.copyOf(paths, i);
}
ReflectAccelerator.mergeResources(app, sActivityThread, paths);
// Merge all the dex into host's class loader
ClassLoader cl = app.getClassLoader();
i = 0;
int N = apks.size();
String[] dexPaths = new String[N];
DexFile[] dexFiles = new DexFile[N];
for (LoadedApk apk : apks) {
dexPaths[i] = apk.path;
dexFiles[i] = apk.dexFile;
if (Small.getBundleUpgraded(apk.packageName)) {
// If upgraded, delete the opt dex file for recreating
if (apk.optDexFile.exists()) apk.optDexFile.delete();
Small.setBundleUpgraded(apk.packageName, false);
}
i++;
}
ReflectAccelerator.expandDexPathList(cl, dexPaths, dexFiles);
// Expand the native library directories for host class loader if plugin has any JNIs. (#79)
List<File> libPathList = new ArrayList<File>();
for (LoadedApk apk : apks) {
if (apk.libraryPath != null) {
libPathList.add(apk.libraryPath);
}
}
if (libPathList.size() > 0) {
ReflectAccelerator.expandNativeLibraryDirectories(cl, libPathList);
}
// Trigger all the bundle application `onCreate' event
for (final LoadedApk apk : apks) {
String bundleApplicationName = apk.applicationName;
if (bundleApplicationName == null) continue;
try {
final Class applicationClass = Class.forName(bundleApplicationName);
Bundle.postUI(new Runnable() {
@Override
public void run() {
try {
BundleApplicationContext appContext = new BundleApplicationContext(app, apk);
Application bundleApplication = Instrumentation.newApplication(
applicationClass, appContext);
sHostInstrumentation.callApplicationOnCreate(bundleApplication);
} catch (Exception e) {
e.printStackTrace();
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
// Lazy init content providers
if (mLazyInitProviders != null) {
try {
Method m = sActivityThread.getClass().getDeclaredMethod(
"installContentProviders", Context.class, List.class);
m.setAccessible(true);
m.invoke(sActivityThread, app, mLazyInitProviders);
} catch (Exception e) {
throw new RuntimeException("Failed to lazy init content providers: " + mLazyInitProviders);
}
}
// Free temporary variables
sLoadedApks = null;
sProviders = null;
}
@Override
protected String[] getSupportingTypes() {
return new String[] {"app", "lib"};
}
@Override
public File getExtractPath(Bundle bundle) {
Context context = Small.getContext();
File packagePath = context.getFileStreamPath(FD_STORAGE);
return new File(packagePath, bundle.getPackageName());
}
@Override
public File getExtractFile(Bundle bundle, String entryName) {
if (!entryName.endsWith(".so")) return null;
return new File(bundle.getExtractPath(), entryName);
}
@Override
public void loadBundle(Bundle bundle) {
String packageName = bundle.getPackageName();
BundleParser parser = bundle.getParser();
parser.collectActivities();
PackageInfo pluginInfo = parser.getPackageInfo();
// Load the bundle
String apkPath = parser.getSourcePath();
if (sLoadedApks == null) sLoadedApks = new ConcurrentHashMap<String, LoadedApk>();
LoadedApk apk = sLoadedApks.get(packageName);
if (apk == null) {
apk = new LoadedApk();
apk.packageName = packageName;
apk.path = apkPath;
apk.nonResources = parser.isNonResources();
if (pluginInfo.applicationInfo != null) {
apk.applicationName = pluginInfo.applicationInfo.className;
}
apk.packagePath = bundle.getExtractPath();
apk.optDexFile = new File(apk.packagePath, FILE_DEX);
// Load dex
final LoadedApk fApk = apk;
Bundle.postIO(new Runnable() {
@Override
public void run() {
try {
fApk.dexFile = DexFile.loadDex(fApk.path, fApk.optDexFile.getPath(), 0);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
// Extract native libraries with specify ABI
String libDir = parser.getLibraryDirectory();
if (libDir != null) {
apk.libraryPath = new File(apk.packagePath, libDir);
}
sLoadedApks.put(packageName, apk);
}
if (pluginInfo.activities == null) {
return;
}
// Record activities for intent redirection
if (sLoadedActivities == null) sLoadedActivities = new ConcurrentHashMap<String, ActivityInfo>();
for (ActivityInfo ai : pluginInfo.activities) {
sLoadedActivities.put(ai.name, ai);
}
// Record intent-filters for implicit action
ConcurrentHashMap<String, List<IntentFilter>> filters = parser.getIntentFilters();
if (filters != null) {
if (sLoadedIntentFilters == null) {
sLoadedIntentFilters = new ConcurrentHashMap<String, List<IntentFilter>>();
}
sLoadedIntentFilters.putAll(filters);
}
// Set entrance activity
bundle.setEntrance(parser.getDefaultActivityName());
}
@Override
public void prelaunchBundle(Bundle bundle) {
super.prelaunchBundle(bundle);
Intent intent = new Intent();
bundle.setIntent(intent);
/*
* 根据pkg和uri确定具体包,拼接成mainactivity
*/
// Intent extras - class
String activityName = bundle.getActivityName();
if (!ActivityLauncher.containsActivity(activityName)) {
if (sLoadedActivities == null) {
throw new ActivityNotFoundException("Unable to find explicit activity class " +
"{ " + activityName + " }");
}
if (!sLoadedActivities.containsKey(activityName)) {
if (activityName.endsWith("Activity")) {
throw new ActivityNotFoundException("Unable to find explicit activity class " +
"{ " + activityName + " }");
}
String tempActivityName = activityName + "Activity";
if (!sLoadedActivities.containsKey(tempActivityName)) {
throw new ActivityNotFoundException("Unable to find explicit activity class " +
"{ " + activityName + "(Activity) }");
}
activityName = tempActivityName;
}
}
intent.setComponent(new ComponentName(Small.getContext(), activityName));
// Intent extras - params
String query = bundle.getQuery();
if (query != null) {
intent.putExtra(Small.KEY_QUERY, '?'+query);
}
}
@Override
public void launchBundle(Bundle bundle, Context context) {
prelaunchBundle(bundle);
super.launchBundle(bundle, context);
}
@Override
public <T> T createObject(Bundle bundle, Context context, String type) {
if (type.startsWith("fragment")) {
if (!(context instanceof Activity)) {
return null; // context should be an activity which can be add resources asset path
}
String packageName = bundle.getPackageName();
if (packageName == null) return null;
String fname = bundle.getPath();
if (fname == null || fname.equals("")) {
fname = packageName + ".MainFragment"; // default
} else {
char c = fname.charAt(0);
if (c == '.') {
fname = packageName + fname;
} else if (c >= 'A' && c <= 'Z') {
fname = packageName + "." + fname;
} else {
// TODO: check the full quality fragment class name
}
}
if (type.endsWith("v4")) {
return (T) android.support.v4.app.Fragment.instantiate(context, fname);
}
return (T) android.app.Fragment.instantiate(context, fname);
}
return super.createObject(bundle, context, type);
}
/**
* Apply plugin activity info with plugin's AndroidManifest.xml
* @param activity
* @param ai
*/
private static void applyActivityInfo(Activity activity, ActivityInfo ai) {
// Apply theme (9.0 only)
if (Build.VERSION.SDK_INT >= 28) {
ReflectAccelerator.resetResourcesAndTheme(activity, ai.getThemeResource());
}
// Apply window attributes
Window window = activity.getWindow();
window.setSoftInputMode(ai.softInputMode);
activity.setRequestedOrientation(ai.screenOrientation);
}
}