【Android 插件化】Hook 插件化框架 ( 从源码角度分析加载资源流程 | Hook 点选择 | 资源冲突解决方案 )

2023-03-29 15:24:55 浏览数 (1)

Android 插件化系列文章目录

【Android 插件化】插件化简介 ( 组件化与插件化 )

【Android 插件化】插件化原理 ( JVM 内存数据 | 类加载流程 )

【Android 插件化】插件化原理 ( 类加载器 )

【Android 插件化】“ 插桩式 “ 插件化框架 ( 原理与实现思路 )

【Android 插件化】“ 插桩式 “ 插件化框架 ( 类加载器创建 | 资源加载 )

【Android 插件化】“ 插桩式 “ 插件化框架 ( 注入上下文的使用 )

【Android 插件化】“ 插桩式 “ 插件化框架 ( 获取插件入口 Activity 组件 | 加载插件 Resources 资源 )

【Android 插件化】“ 插桩式 “ 插件化框架 ( 运行应用 | 代码整理 )

【Android 插件化】Hook 插件化框架 ( Hook 技术 | 代理模式 | 静态代理 | 动态代理 )

【Android 插件化】Hook 插件化框架 ( Hook 实现思路 | Hook 按钮点击事件 )

【Android 插件化】Hook 插件化框架 ( Hook Activity 启动过程 | 静态代理 )

【Android 插件化】Hook 插件化框架 ( 从 Hook 应用角度分析 Activity 启动流程 一 | Activity 进程相关源码 )

【Android 插件化】Hook 插件化框架 ( 从 Hook 应用角度分析 Activity 启动流程 二 | AMS 进程相关源码 | 主进程相关源码 )

【Android 插件化】Hook 插件化框架 ( hook 插件化原理 | 插件包管理 )

【Android 插件化】Hook 插件化框架 ( 通过反射获取 “插件包“ 中的 Element[] dexElements )

【Android 插件化】Hook 插件化框架 ( 通过反射获取 “宿主“ 应用中的 Element[] dexElements )

【Android 插件化】Hook 插件化框架 ( 合并 “插件包“ 与 “宿主“ 中的 Element[] dexElements | 设置合并后的 Element[] 数组 )

【Android 插件化】Hook 插件化框架 ( 创建插件应用 | 拷贝插件 APK | 初始化插件包 | 测试插件 DEX 字节码 )

【Android 插件化】Hook 插件化框架 ( Hook Activity 启动流程 | Hook 点分析 )

【Android 插件化】Hook 插件化框架 ( Hook Activity 启动流程 | 反射获取 IActivityManager 对象 )

【Android 插件化】Hook 插件化框架 ( Hook Activity 启动流程 | AMS 启动前使用动态代理替换掉插件 Activity 类 )

【Android 插件化】Hook 插件化框架 ( Hook Activity 启动流程 | 主线程创建 Activity 实例之前使用插件 Activity 类替换占位的组件 )

【Android 插件化】Hook 插件化框架 ( 反射工具类 | 反射常用操作整理 )

【Android 插件化】Hook 插件化框架 ( 插件包资源加载 )

【Android 插件化】Hook 插件化框架 ( 从源码角度分析加载资源流程 | Hook 点选择 | 资源冲突解决方案 )


文章目录

  • Android 插件化系列文章目录
  • 前言
  • 一、从源码角度分析加载资源流程
    • 1、ActivityThread 入口
    • 2、LaunchActivityItem
    • 3、ActivityThread.performLaunchActivity
    • 4、ContextImpl
  • 二、Hook 点选择
  • 三、资源冲突解决方案
  • 四、博客资源

前言

在之前的博客 【Android 插件化】Hook 插件化框架 ( 插件包资源加载 ) 中 , 实现了从插件包中获取资源 ;

但是这种方法对代码的侵入性较大 , 使用这种方式开发 , 插件应用 和 宿主应用 , 都需要对 Resources 进行特别处理 , 如重写 Activity 和 Application 的 public Resources getResources() 方法 ;

最好是使用 Hook 方式加载资源文件 , 实现插件包代码 0 侵入 , 开发插件应用 与 开发普通应用 , 基本一致 ;

一、从源码角度分析加载资源流程


在插件包中的 Activity , 如果加载 R.layout.activity_main , 拿到的是 “宿主” 应用中的资源 , 无法拿到插件包中的资源 ;

1、ActivityThread 入口

在 【Android 插件化】Hook 插件化框架 ( Hook Activity 启动流程 | 主线程创建 Activity 实例之前使用插件 Activity 类替换占位的组件 ) 博客中 , 分析了 Activity 在主线程启动前的一些操作 ;

在 ActivityThread 中的 mH 处理 EXECUTE_TRANSACTION 信号时 , 其中 ClientTransaction 中有 LaunchActivityItem ;

代码语言:javascript复制
public final class ActivityThread extends ClientTransactionHandler {
    final H mH = new H();

    /** Reference to singleton {@link ActivityThread} */
    private static volatile ActivityThread sCurrentActivityThread;

    class H extends Handler {
        public static final int EXECUTE_TRANSACTION = 159;
        public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: "   codeToString(msg.what));
            switch (msg.what) {
                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;
            }
        }
	}
}

源码路径 : /frameworks/base/core/java/android/app/ActivityThread.java

2、LaunchActivityItem

LaunchActivityItem 中的 execute 方法中的 ClientTransactionHandler client 参数 就是 ActivityThread ;

代码语言:javascript复制
public class LaunchActivityItem extends ClientTransactionItem {
    @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);
    }
}

源码路径 : /frameworks/base/core/java/android/app/servertransaction/LaunchActivityItem.java

3、ActivityThread.performLaunchActivity

在 LaunchActivityItem 的 execute 方法中 , 调用 client.handleLaunchActivity 方法就是执行的 ActivityThread 的 handleLaunchActivity 方法 ;

在 ActivityThread 中的 handleLaunchActivity 方法中 , 调用了 performLaunchActivity 方法 ;

在 ActivityThread 中的 performLaunchActivity 方法中 , 调用

代码语言:javascript复制
ContextImpl appContext = createBaseContextForActivity(r);

方法 , 创建了 ContextImpl 对象 ;

代码语言:javascript复制
public final class ActivityThread extends ClientTransactionHandler {
    private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
        final int displayId;
        try {
            displayId = ActivityManager.getService().getActivityDisplayId(r.token);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }

        ContextImpl appContext = ContextImpl.createActivityContext(
                this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);

        final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
        // For debugging purposes, if the activity's package name contains the value of
        // the "debug.use-second-display" system property as a substring, then show
        // its content on a secondary display if there is one.
        String pkgName = SystemProperties.get("debug.second-display.pkg");
        if (pkgName != null && !pkgName.isEmpty()
                && r.packageInfo.mPackageName.contains(pkgName)) {
            for (int id : dm.getDisplayIds()) {
                if (id != Display.DEFAULT_DISPLAY) {
                    Display display =
                            dm.getCompatibleDisplay(id, appContext.getResources());
                    appContext = (ContextImpl) appContext.createDisplayContext(display);
                    break;
                }
            }
        }
        return appContext;
    }
}

源码路径 : /frameworks/base/core/java/android/app/ActivityThread.java

4、ContextImpl

在 ActivityThread 中调用了

代码语言:javascript复制
        ContextImpl appContext = ContextImpl.createActivityContext(
                this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);

方法 , 创建了 ContextImpl ;

下面分析 ContextImpl 的 createActivityContext 方法 ;

代码语言:javascript复制
class ContextImpl extends Context {
    static ContextImpl createActivityContext(ActivityThread mainThread,
            LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
            Configuration overrideConfiguration) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");

        String[] splitDirs = packageInfo.getSplitResDirs();
        ClassLoader classLoader = packageInfo.getClassLoader();

        if (packageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "SplitDependencies");
            try {
                classLoader = packageInfo.getSplitClassLoader(activityInfo.splitName);
                splitDirs = packageInfo.getSplitPaths(activityInfo.splitName);
            } catch (NameNotFoundException e) {
                // Nothing above us can handle a NameNotFoundException, better crash.
                throw new RuntimeException(e);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            }
        }

        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,
                activityToken, null, 0, classLoader);

        // Clamp display ID to DEFAULT_DISPLAY if it is INVALID_DISPLAY.
        displayId = (displayId != Display.INVALID_DISPLAY) ? displayId : Display.DEFAULT_DISPLAY;

        final CompatibilityInfo compatInfo = (displayId == Display.DEFAULT_DISPLAY)
                ? packageInfo.getCompatibilityInfo()
                : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;

        final ResourcesManager resourcesManager = ResourcesManager.getInstance();

        // Create the base resources for which all configuration contexts for this Activity
        // will be rebased upon.
        context.setResources(resourcesManager.createBaseActivityResources(activityToken,
                packageInfo.getResDir(),
                splitDirs,
                packageInfo.getOverlayDirs(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                classLoader));
        context.mDisplay = resourcesManager.getAdjustedDisplay(displayId,
                context.getResources());
        return context;
    }
}

源码路径 : /frameworks/base/core/java/android/app/ContextImpl.java

在上述方法中 , 就有创建 Resources 资源的方法 :

代码语言:javascript复制
        context.setResources(resourcesManager.createBaseActivityResources(activityToken,
                packageInfo.getResDir(),
                splitDirs,
                packageInfo.getOverlayDirs(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                classLoader));

二、Hook 点选择


修改应用 Resources 的 Hook 点有很多 , 通过改变 Activity 的 Resources , 甚至修改 ActivityThread 中创建 Resources 的流程 都可以实现 ;

在插件包中 , 使用了 AppCompatActivity , 可以直接替换 AppCompatActivity 中的 private Resources mResources 成员 ;

代码语言:javascript复制
public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
        TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {

    private static final String DELEGATE_TAG = "androidx:appcompat";

    private AppCompatDelegate mDelegate;
    private Resources mResources;
}

只要可以拿到 AppCompatActivity 实例 , 就可以通过反射 , 替换掉 private Resources mResources 成员 ;

在 Instrumentation 中 , 调用了 newActivity 创建 Activity 实例 ;

代码语言:javascript复制
    public Activity newActivity(Class<?> clazz, Context context, 
            IBinder token, Application application, Intent intent, ActivityInfo info, 
            CharSequence title, Activity parent, String id,
            Object lastNonConfigurationInstance) throws InstantiationException, 
            IllegalAccessException {
        Activity activity = (Activity)clazz.newInstance();
        ActivityThread aThread = null;
        // Activity.attach expects a non-null Application Object.
        if (application == null) {
            application = new Application();
        }
        activity.attach(context, aThread, this, token, 0 /* ident */, application, intent,
                info, title, parent, id,
                (Activity.NonConfigurationInstances)lastNonConfigurationInstance,
                new Configuration(), null /* referrer */, null /* voiceInteractor */,
                null /* window */, null /* activityConfigCallback */);
        return activity;
    }

源码路径 : /frameworks/base/core/java/android/app/Instrumentation.java

在 ActivityThread 中 , 有 Instrumentation mInstrumentation 成员变量 , ActivityThread 本身就是单例 , 通过获取其静态成员 private static volatile ActivityThread sCurrentActivityThread , 就可以获取到 ActivityThread ;

代码语言:javascript复制
    /** Reference to singleton {@link ActivityThread} */
    private static volatile ActivityThread sCurrentActivityThread;
    Instrumentation mInstrumentation;

三、资源冲突解决方案


资源的 ID 在 AAPT 编译资源阶段就确定了 ;

固定类型的资源 , 编号是从一定的编号段开始的 , 如 layout 布局资源 , 第一个布局资源总是 2131361820 ;

不同类型的资源 , 布局 , 字符串 , 数值 , 主题 , 图片 , drawable 等 , 都有对应的资源编号范围 , 同时也要兼容系统的资源编号范围 ;

如果宿主应用启动 , 加载第一个布局资源 , 那么编号是 2131361820 ;

如果插件应用启动 , 加载第一个布局资源 , 那么编号也是 2131361820 ;

这样就出现了资源冲突 ;

使用不同的 AssetManager 加载不同的资源 , 可以解决资源冲突问题 ;

四、博客资源

博客资源 :

  • GitHub : https://github.com/han1202012/Plugin_Hook

0 人点赞