[Android][Pluginization] Simple implementation of plug-inization: start unregistered Activity

Preface

This article introduces a solution to start an Activity in Android that is not registered in AndroidManifest. Mainly need to master the following knowledge points:

  1. reflection

  2. Class loading

  3. Activity startup process

  4. Resource loading process

Start an unregistered activity in the app

Activities need to be registered in AndroidManifest by default, and unregistered applications cannot be started. When AMS starts the application, it will check whether it has been registered. Therefore, if you want to start an unregistered Activity, you need to replace the Intent that starts the application with the registered Activity before the Activity, so you can create a new Activity for placeholder. After the detection is passed, the unregistered activity that needs to be started is replaced before the activity is actually started.

Get the Hook point to replace the Intent

After calling the startActivity method, the remote method of AMS will be called in the execStartActivity method of Instrumentation for processing. Android 6.0 and below and Android 6.0 and above, the method of calling AMS in execStartActivity is different, so compatibility processing is required.

6.0

public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        //...
        int result = ActivityManagerNative.getDefault()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
        //...
    }

Get AMS by calling ActivityManagerNative.getDefault().

8.0

public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        IApplicationThread whoThread = (IApplicationThread) contextThread;
        //...
        int result = ActivityManager.getService()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
        //...
    }

Get AMS by calling ActivityManager.getService().

Replace Intent

public static void hookAMS() {
        try {
            Field singletonField;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                singletonField = getField(Class.forName("android.app.ActivityManager"), "IActivityManagerSingleton");
            } else {
                singletonField = getField(Class.forName("android.app.ActivityManagerNative"), "gDefault");
            }
            Object singleton = singletonField.get(null);

            Field mInstanceField = getField(Class.forName("android.util.Singleton"), "mInstance");
            final Object mInstance = mInstanceField.get(singleton);

            final Object proxyInstance = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{Class.forName("android.app.IActivityManager")}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    if ("startActivity".equals(method.getName())) {
                        int index = 0;
                        for (int i = 0; i < args.length; i++) {
                            if (args[i] instanceof Intent) {
                                index = i;
                                break;
                            }
                        }
                        Intent intent = (Intent) args[index];

                        Intent proxyIntent = new Intent(intent);
                        //占位的Activity
                        proxyIntent.setClassName("com.wangyz.plugindemo", "com.wangyz.plugindemo.ProxyActivity");
                        proxyIntent.putExtra("target_intent", intent);

                        args[index] = proxyIntent;
                    }
                    return method.invoke(mInstance, args);
                }
            });
            mInstanceField.set(singleton, proxyInstance);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Get the Hook point for restoring the Intent

Android8.0 and below

The message that starts the Activity will be called back to the dispatchMessage method of mH in the ActivityThread. You can set a callBack to mH in the handleMessage of the callBack, and then replace it with the Intent to be started, and then return false to let the handleMessage continue processing.

public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

Android8.0 and below, in the handleMessage method in the mH of ActivityThread, the LAUNCH_ACTIVITY type of message will be processed, and the handleLaunchActivity method is called here to start the Activity.

case LAUNCH_ACTIVITY: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    handleLaunchActivity(r, null);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                } break;

Android9.0

As in 8.0, set the callBack and then modify the Intent.

In the handleMessage method in the mH of ActivityThread, the message of type EXECUTE_TRANSACTION will be processed, and the TransactionExecutor.execute method is called here

case EXECUTE_TRANSACTION:
                    final ClientTransaction transaction = (ClientTransaction) msg.obj;
                    mTransactionExecutor.execute(transaction);
                    if (isSystem()) {
                        // Client transactions inside system process are recycled on the client side
                        // instead of ClientLifecycleManager to avoid being cleared before this
                        // message is handled.
                        transaction.recycle();
                    }
                    // TODO(lifecycler): Recycle locally scheduled transactions.
                    break;

The execute method will call executeCallbacks

public void execute(ClientTransaction transaction) {
        final IBinder token = transaction.getActivityToken();
        log("Start resolving transaction for client: " + mTransactionHandler + ", token: " + token);

        executeCallbacks(transaction);

        executeLifecycleState(transaction);
        mPendingActions.clear();
        log("End resolving transaction");
    }
public void executeCallbacks(ClientTransaction transaction) {
        final List<ClientTransactionItem> callbacks = transaction.getCallbacks();
        if (callbacks == null) {
            // No callbacks to execute, return early.
            return;
        }
        log("Resolving callbacks");

        final IBinder token = transaction.getActivityToken();
        ActivityClientRecord r = mTransactionHandler.getActivityClient(token);

        // In case when post-execution state of the last callback matches the final state requested
        // for the activity in this transaction, we won't do the last transition here and do it when
        // moving to final state instead (because it may contain additional parameters from server).
        final ActivityLifecycleItem finalStateRequest = transaction.getLifecycleStateRequest();
        final int finalState = finalStateRequest != null ? finalStateRequest.getTargetState()
                : UNDEFINED;
        // Index of the last callback that requests some post-execution state.
        final int lastCallbackRequestingState = lastCallbackRequestingState(transaction);

        final int size = callbacks.size();
        for (int i = 0; i < size; ++i) {
            final ClientTransactionItem item = callbacks.get(i);
            log("Resolving callback: " + item);
            final int postExecutionState = item.getPostExecutionState();
            final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
                    item.getPostExecutionState());
            if (closestPreExecutionState != UNDEFINED) {
                cycleToPath(r, closestPreExecutionState);
            }

            item.execute(mTransactionHandler, token, mPendingActions);
            item.postExecute(mTransactionHandler, token, mPendingActions);
            if (r == null) {
                // Launch activity request will create an activity record.
                r = mTransactionHandler.getActivityClient(token);
            }

            if (postExecutionState != UNDEFINED && r != null) {
                // Skip the very last transition and perform it by explicit state request instead.
                final boolean shouldExcludeLastTransition =
                        i == lastCallbackRequestingState && finalState == postExecutionState;
                cycleToPath(r, postExecutionState, shouldExcludeLastTransition);
            }
        }
    }

This method will call the execute method of ClientTransactionItem. ClientTransactionItem is added in realStartActivityLocked in ActivityStackSupervisor

final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,
            boolean andResume, boolean checkConfig) throws RemoteException {
                // Create activity launch transaction.
                final ClientTransaction clientTransaction = ClientTransaction.obtain(app.thread,
                        r.appToken);
                clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
                        System.identityHashCode(r), r.info,
                        // TODO: Have this take the merged configuration instead of separate global
                        // and override configs.
                        mergedConfiguration.getGlobalConfiguration(),
                        mergedConfiguration.getOverrideConfiguration(), r.compat,
                        r.launchedFromPackage, task.voiceInteractor, app.repProcState, r.icicle,
                        r.persistentState, results, newIntents, mService.isNextTransitionForward(),
                        profilerInfo));
            }

Therefore, the specific class corresponding to ClientTransactionItem is LaunchActivityItem, which corresponds to the execute method

@Override
    public void execute(ClientTransactionHandler client, IBinder token,
            PendingTransactionActions pendingActions) {
        Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
        ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
                mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
                mPendingResults, mPendingNewIntents, mIsForward,
                mProfilerInfo, client);
        client.handleLaunchActivity(r, pendingActions, null /* customIntent */);
        Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
    }

In its method, the handleLaunchActivity of ClientTransactionHandler is called, and ClientTransactionHandler is defined in ActivityThread

private final TransactionExecutor mTransactionExecutor = new TransactionExecutor(this);

ActivityThread inherits ClientTransactionHandler, then it will implement handleLaunchActivity. Finally start Activity in this method

public final class ActivityThread extends ClientTransactionHandler {
    @Override
    public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) {

            }
}

Restore Intent

public static void hookHandler() {
        try {
            Field sCurrentActivityThreadThread = getField(Class.forName("android.app.ActivityThread"), "sCurrentActivityThread");
            Object activityThread = sCurrentActivityThreadThread.get(null);

            Field mHField = getField(Class.forName("android.app.ActivityThread"), "mH");
            Object mH = mHField.get(activityThread);

            Field mCallbackField = getField(Class.forName("android.os.Handler"), "mCallback");
            mCallbackField.set(mH, new Handler.Callback() {
                @Override
                public boolean handleMessage(Message msg) {
                    switch (msg.what) {
                        case 100: {
                            try {
                                Field intentField = getField(msg.obj.getClass(), "intent");
                                Intent proxyIntent = (Intent) intentField.get(msg.obj);
                                Intent targetIntent = proxyIntent.getParcelableExtra("target_intent");
                                if (targetIntent != null) {
//                                    proxyIntent.setComponent(targetIntent.getComponent());
                                    intentField.set(msg.obj, targetIntent);
                                }
                            } catch (Exception e) {
                                e.printStackTrace();
                            }

                        }
                        break;
                        case 159: {
                            try {
                                Field mActivityCallbacksField = getField(msg.obj.getClass(), "mActivityCallbacks");
                                List mActivityCallbacks = (List) mActivityCallbacksField.get(msg.obj);
                                for (int i = 0; i < mActivityCallbacks.size(); i++) {
                                    if (mActivityCallbacks.get(i).getClass().getName()
                                            .equals("android.app.servertransaction.LaunchActivityItem")) {
                                        Object launchActivityItem = mActivityCallbacks.get(i);

                                        Field mIntentField = getField(launchActivityItem.getClass(), "mIntent");
                                        Intent intent = (Intent) mIntentField.get(launchActivityItem);
                                        // 获取插件的
                                        Intent proxyIntent = intent.getParcelableExtra("target_intent");
                                        //替换
                                        if (proxyIntent != null) {
                                            mIntentField.set(launchActivityItem, proxyIntent);
                                        }
                                    }
                                }
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                        break;
                        default:
                            break;
                    }
                    return false;
                }
            });

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Hook when the Application is created

@Override
    public void onCreate() {
        super.onCreate();
        //一般是从服务器下载回来,然后复制到应用的私有目录下,这里演示从sdcard复制到data目录下,6.0及以上需要申请动态权限。复制应该放在非UI线程上做,这里简化操作,放在UI线程上操作。
        String pluginPath = getDir("plugin", Context.MODE_PRIVATE).getAbsolutePath();
        pluginPath = pluginPath + "/plugin.apk";
        if (!new File(pluginPath).exists()) {
            FileUtil.copyFile(PLUGIN_PATH, pluginPath);
        }
        HookUtil.loadPlugin(this, pluginPath);
        HookUtil.hookAMS();
        HookUtil.hookHandler();
    }

At this point, you can enable unregistered activities in the same application.

Start the Activity in the plug-in app

Starting an activity in a non-same application requires a few more steps than starting an activity in the same application. Since it is not in an application, the APK of the plug-in needs to be loaded first, and then the Intent needs to be replaced with the placeholder Intent before the AMS detection. After the detection, before the Activity is started, it is replaced with the Intent that needs to start the Activity. In addition, since the plug-in is dynamically loaded, the problem of resource loading also needs to be solved.

Load plugin

Load plugins mainly use class loader

public static void loadPlugin(Context context, String dexPath) {

        //判断dex是否存在
        File dex = new File(dexPath);
        if (!dex.exists()) {
            return;
        }

        try {
            //获取自己的dexElements
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();

            Field pathListField = getField(pathClassLoader.getClass(), "pathList");
            Object pathListObject = pathListField.get(pathClassLoader);

            Field dexElementsField = getField(pathListObject.getClass(), "dexElements");
            Object[] dexElementsObject = (Object[]) dexElementsField.get(pathListObject);

            //获取dex中的dexElements
            File odex = context.getDir("odex", Context.MODE_PRIVATE);
            DexClassLoader dexClassLoader = new DexClassLoader(dexPath, odex.getAbsolutePath(), null, pathClassLoader);

            Field pluginPathListField = getField(dexClassLoader.getClass(), "pathList");
            Object pluginPathListObject = pluginPathListField.get(dexClassLoader);

            Field pluginDexElementsField = getField(pluginPathListObject.getClass(), "dexElements");
            Object[] pluginDexElementsObject = (Object[]) pluginDexElementsField.get(pluginPathListObject);

            Class<?> elementClazz = dexElementsObject.getClass().getComponentType();
            Object newDexElements = Array.newInstance(elementClazz, pluginDexElementsObject.length + dexElementsObject.length);
            System.arraycopy(pluginDexElementsObject, 0, newDexElements, 0, pluginDexElementsObject.length);
            System.arraycopy(dexElementsObject, 0, newDexElements, pluginDexElementsObject.length, dexElementsObject.length);

            //设置
            dexElementsField.set(pathListObject, newDexElements);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

Replace Intent

This process is the same as the situation in the application, so I won’t repeat it

Load resources

Load resources mainly use the addAssetPath method of AssetManager, which is loaded through reflection

 private static Resources loadResource(Context context) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPathField = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            addAssetPathField.setAccessible(true);
            addAssetPathField.invoke(assetManager, PATH);
            Resources resources = context.getResources();
            return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

Guess you like

Origin blog.csdn.net/xfb1989/article/details/110671095