ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

改Android Framework层FrameLayout.onMeasure实现第三方App布局定制

改Android Framework层FrameLayout.onMeasure实现第三方App布局定制 1. 项目概述与思路拆解1.1 这个项目到底在做什么先说结论这个项目做的事情是在不改动第三方 App 任何代码的前提下通过修改 Android Framework 层FrameLayout的onMeasure逻辑强行改变某个第三方 App 内部页面的布局尺寸。说白了就是把别人家的 App按照你自己的意愿重新“排版”。我当初接到这个需求的时候第一反应是这玩意儿能合法做吗后来确认了场景——企业内部设备上的定制 ROM预装的是自家签约的第三方应用需求是让它在特定屏幕比例下显示更协调不涉及破解付费、不涉及逆向盗版纯粹是布局适配层面的 Framework 定制。这种需求在政企定制机、收银机、智能屏设备上非常常见属于 ROM 层适配的正当手段。为什么非得动 Framework 层而不是直接在 App 层解决因为第三方 App 的源码你拿不到反编译去改 smali 又是一场噩梦——每升级一个版本就得重新改一遍而且市面上大部分加固壳直接让你连 smali 都看不到。而 Framework 层是所有 App 的“父类”你改了FrameLayout.onMeasure所有用FrameLayout作为根布局的页面都会受到影响根本不需要关心目标 App 的内部实现。这就是这个方案的核心价值用系统级的一处修改替代 App 层的无数次逆向适配。1.2 为什么选中 FrameLayout 作为切入点Android 的视图体系里FrameLayout是最基础的容器之一。市面上绝大多数 App 的 Activity 根布局要么直接是FrameLayout要么是LinearLayout、ConstraintLayout的嵌套结构而这些容器最终都逃不过一个宿命——它们的父容器大概率还是FrameLayoutDecorView的内容区域就是FrameLayout。我统计过几个主流第三方 App 的视图层级打开页面后用hierarchy viewer或者直接dump视图树你会发现一个规律根部一定是FrameLayout中间可能包了各种LinearLayout、RelativeLayout、ConstraintLayout但真正决定整个 Activity 内容区域尺寸的是那个根级FrameLayout的onMeasure测量逻辑。所以只要在 Framework 层的FrameLayout类里做文章就可以同时控制所有第三方页面的测量结果。这就像你在小区门口装了一个闸机不管居民从哪栋楼出来最终都要经过这个闸机——FrameLayout就是那个闸机。当然这里必须说明一个前提onMeasure的执行时机和调用频率非常高每次布局变化都会触发所以在这个方法里做任何修改都需要格外小心性能和递归问题。我在下面的章节里会详细讲怎么避免这些坑。1.3 这个方案适合谁、能解决什么场景我梳理了一下这个思路主要适合这几类人第一类是 ROM 定制工程师。政企定制机、行业终端、收银设备上经常要预装第三方 App如果 App 的 UI 在目标设备上显示变形、显示过小、或者某些区域被遮挡你又拿不到 App 源码Framework 层的布局调整就成为一个可选方案。第二类是系统应用开发者。自己做系统应用时偶尔会遇到某些系统组件的布局不符合预期虽然你可以直接改自己的 App但如果这个组件被多个 App 共用改 Framework 层反而是一劳永逸的做法。第三类是搞逆向分析和安全研究的同学。理解onMeasure这个层面的布局机制有助于你分析 App 的 UI 结构也能帮助理解一些“系统级适配”类功能比如某些厂商的屏幕适配方案、老人模式、单手模式背后的原理——这些功能十有八九都是在 Framework 层动了手脚。不适合谁呢只写普通业务 App、不接触 ROM 开发的同学这个方案对你来说属于“屠龙之术”了解原理即可不建议在生产项目里引入因为改 Framework 意味着你必须维护整个系统的源码分支成本很高。2. 核心细节解析与实操要点2.1 onMeasure 的测量流程你必须先搞懂这些在动刀之前我觉得有必要把FrameLayout.onMeasure的测量流程仔细捋一遍。如果你对onMeasure的机制都不熟悉后面改代码就是盲人摸象。Android 的视图测量遵循一个“由父及子”的递归过程。父容器先根据自身的MeasureSpec测量规格计算出子 View 可用的尺寸范围然后调用子 View 的measure()方法把MeasureSpec传下去。子 View 在onMeasure里根据这个MeasureSpec计算出自己想要的大小再通过setMeasuredDimension()保存结果。等所有子 View 测量完毕父容器再根据所有子 View 的尺寸加上自己的 padding得到自己的最终测量尺寸。FrameLayout.onMeasure的原生实现大致是这个逻辑Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count getChildCount(); int maxHeight 0; int maxWidth 0; int childState 0; for (int i 0; i count; i) { final View child getChildAt(i); if (mMeasureAllChildren || child.getVisibility() ! GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); maxWidth Math.max(maxWidth, child.getMeasuredWidth()); maxHeight Math.max(maxHeight, child.getMeasuredHeight()); childState combineMeasuredStates(childState, child.getMeasuredState()); } } maxWidth getPaddingLeft() getPaddingRight(); maxHeight getPaddingTop() getPaddingBottom(); maxHeight Math.max(maxHeight, getSuggestedMinimumHeight()); maxWidth Math.max(maxWidth, getSuggestedMinimumWidth()); setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState)); }看懂这段代码你就明白了FrameLayout的本质它不关心子 View 怎么排列它只做一件事——找到所有子 View 里最宽的那个和最高的那个然后把它们作为自己的期望尺寸。也就是说FrameLayout的最终尺寸是“最大子 View padding”决定的。明白了这一点定制布局的思路就清晰了如果你想人为改变某个页面的整体宽度或高度你可以在父容器测量完之后、setMeasuredDimension之前强行修改maxWidth/maxHeight的值如果你想改变某个特定区域的显示比例那就需要根据子 View 的身份或者索引对测量结果做条件化的调整。2.2 修改 onMeasure 有哪些可用手段修改FrameLayout.onMeasure具体来说有几种不同的手段各有适用场景手段一直接修改 AOSP 源码里的FrameLayout.java。这是最彻底的方式也是我们这个项目的核心手段。你需要在 AOSP 源码树里找到frameworks/base/core/java/android/widget/FrameLayout.java修改onMeasure方法后单独编译framework模块生成新的framework.jar/boot.oat然后替换到系统镜像里。这种方式影响面最大所有 App 的FrameLayout都会走你修改后的逻辑需要格外谨慎。手段二反射替换FrameLayout的onMeasure回调。你是改不了系统类的但可以通过反射拿到View类的mOnMeasureListener之类的内部钩子其实并没有这个钩子。更常见的做法是在自己的 App 里通过setLayoutParams或者改写ViewTreeObserver来做布局监听但这只能影响自己的 App影响不了第三方 App。手段三使用 Xposed / LSPosed 框架 Hook。如果你只是想验证效果、不想编译整个系统镜像Hook 方案是最快的。核心思路是 HookFrameLayout.onMeasure在方法执行的末尾修改测量的结果。这种方式不需要刷机但需要目标设备有 Root 权限且 Runtime 层要支持 Xposed 框架。对于快速验证来说这是利器但对量产 ROM 来说无法依赖 Xposed。手段四通过Overlay资源或Runtime Resource Overlay(RRO) 做布局覆盖。这个方式可以覆盖layout资源但FrameLayout.onMeasure是 Java 代码逻辑不是资源所以 RRO 在这里派不上用场。只有布局文件中引用的自定义 View 才能通过资源覆盖替换成你自定义的类但第三方 App 里的FrameLayout是硬编码的系统类覆盖不了。所以我们这个项目最终采用的手段是以 AOSP 源码修改为主配合在开发阶段用 Hook 方式做快速验证双轨并行效率最高。2.3 动手前你必须考虑的三件大事在真正改代码之前有三个问题必须想清楚否则后患无穷。第一包名与进程过滤。你改的是系统级FrameLayout一旦生效所有 App 都会受影响。系统 UI、桌面、输入法、拨号盘……全部会被你的新逻辑管到。所以代码里必须有进程或包名的过滤逻辑让修改只对目标第三方 App 生效。我见过有人直接在onMeasure里写了个固定比例缩放结果系统桌面和通知栏全部变形整个 ROM 直接没法用。这个教训非常深刻。第二性能与递归保护。onMeasure在布局阶段会被高频调用一个页面可能触发几十甚至上百次。如果你在onMeasure里做复杂的运算、循环、或者更糟糕的重新触发一次布局请求就会陷入递归死循环直接导致 ANR 甚至系统重启。所以修改逻辑必须尽量轻量最好只做条件判断和简单的数值运算并且加一个递归深度的保护标志。第三兼容性。这个不用说Android 版本从 8.0 到 14.0FrameLayout.onMeasure的原生实现细节有细微差异主要是mMeasureAllChildren这个标志位和combineMeasuredStates的行为在不同版本上有调整。你在 12 上写的补丁到 14 上可能要适配。我自己做的时候就是先在android-12分支上写好补丁然后逐个版本比对源码确认行为一致后才批量提交。这三个问题我用一个表格总结一下问题风险等级核心对策影响范围过大严重包名/进程过滤白名单机制性能与递归严重轻量计算递归保护标志位版本兼容性中等多版本源码比对逐版本验证3. 实操过程与核心环节实现3.1 实验环境准备与前期验证这次实验我用的环境是AOSPandroid-12.0.0_r32分支设备是 Pixel 3 XL编译目标是aosp_crosshatch-userdebug。选 userdebug 是因为它能 root、能 adb root、还能方便的 push 编译产物。如果你的机器配置不够也可以用模拟器但模拟器对framework层改动验证的可靠性不如真机。尤其是涉及布局显示效果的时候模拟器上的分辨率、显示密度和真机差异不小容易误导判断。动手之前先把编译环境准备好确认一次基础编译能通过source build/envsetup.sh lunch aosp_crosshatch-userdebug make -j16第一次全量编译可能需要一两个小时这个时间正好用来做另一件重要的事写一个快速的 Hook 验证脚本。我选的 Hook 工具是 LSPosed因为它的hook能力稳定且支持beforeHookedMethod和afterHookedMethod可以直接修改方法的返回值相关逻辑。Hook 验证代码大致长这样XposedHelpers.findAndHookMethod(FrameLayout.class, onMeasure, int.class, int.class, new XC_MethodHook() { Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { FrameLayout frameLayout (FrameLayout) param.thisObject; // 只处理目标应用的根布局 if (!isTargetApp(frameLayout.getContext())) { return; } int width frameLayout.getMeasuredWidth(); int height frameLayout.getMeasuredHeight(); // 强制修改为占屏宽度的 85%高度保持不变 int screenWidth frameLayout.getResources().getDisplayMetrics().widthPixels; int newWidth (int) (screenWidth * 0.85f); if (width newWidth) { frameLayout.setMeasuredDimension(newWidth, height); } } });注意这里我用的是afterHookedMethod因为onMeasure执行完之后getMeasuredWidth()才能拿到有效的值。setMeasuredDimension()可以在onMeasure之后再次调用但此时测量流程已经进入收尾阶段你后面可能还要手动requestLayout()才能让新的尺寸生效。这个细节在 Hook 验证阶段特别容易踩坑我后面会在问题排查部分细说。3.2 定位要修改的源码位置Hook 验证通过了说明思路可行这时候再去改源码。在 AOSP 源码树里FrameLayout.java位于frameworks/base/core/java/android/widget/FrameLayout.java你需要修改的就是这个文件里的onMeasure方法。但注意光改这一个文件还不够FrameLayout还有一个隐藏的测量细节它内部有个mMeasureAllChildren标志来源于 XML 属性measureAllChildren。这个标志决定是否需要测量所有子 View还是只测量非 GONE 的子 View。我们改的是测量逻辑如果强行修改了测量结果这个标志对最终结果的影响倒是不大但如果你的调整逻辑需要遍历子 View一定要注意 GONE 状态的子 View 不应该参与计算。还有一点FrameLayout在 Android 10 之后引入了LayoutParams里的leftMargin/rightMargin等属性参与测量所以如果你需要精确控制某个子 View 的位置光改onMeasure不够还要配合onLayout。但我们的核心目标是改变整体布局的宽度比例所以onMeasure已经足够了。在动手改源码前我强烈建议你先用git log查看一下这个文件的历史提交确认你所在的 AOSP 分支上是否已经有厂商或社区的修改防止和现有补丁冲突。3.3 核心代码修改包名过滤 尺寸干预下面这段代码是我实际使用的修改方案。我在尽量保证兼容性的前提下加入了两个关键逻辑包名过滤和尺寸干预。Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count getChildCount(); int maxHeight 0; int maxWidth 0; int childState 0; for (int i 0; i count; i) { final View child getChildAt(i); if (mMeasureAllChildren || child.getVisibility() ! GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); maxWidth Math.max(maxWidth, child.getMeasuredWidth()); maxHeight Math.max(maxHeight, child.getMeasuredHeight()); childState combineMeasuredStates(childState, child.getMeasuredState()); } } maxWidth getPaddingLeft() getPaddingRight(); maxHeight getPaddingTop() getPaddingBottom(); maxHeight Math.max(maxHeight, getSuggestedMinimumHeight()); maxWidth Math.max(maxWidth, getSuggestedMinimumWidth()); // 定制逻辑开始 // 1. 通过进程信息过滤只处理目标 App if (isTargetPackage()) { // 2. 干预测量结果将最大宽度限制为理论屏幕宽度的 85% DisplayMetrics dm getContext().getResources().getDisplayMetrics(); int targetMaxWidth (int) (dm.widthPixels * 0.85f); if (maxWidth targetMaxWidth) { maxWidth targetMaxWidth; } // 3. 可选调整垂直方向的尺寸 int targetMaxHeight (int) (dm.heightPixels * 0.9f); if (maxHeight targetMaxHeight) { maxHeight targetMaxHeight; } } // 定制逻辑结束 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState)); }这个方案的核心思路是保持FrameLayout原有的“取最大子 View 尺寸”逻辑不变等它测完所有子 View 得到一个“自然尺寸”之后我再人为地对这个尺寸做一次“上限裁剪”。如果子 View 本身很小就不要干预只有超宽或超高的时候才进行限制。这样能尽量少地影响正常布局。isTargetPackage()的实现我用的方式是通过ActivityThread.currentApplication()获取当前进程的包名private boolean isTargetPackage() { try { Application app ActivityThread.currentApplication(); if (app null) { return false; } String packageName app.getPackageName(); return com.example.thirdparty.equals(packageName); } catch (Throwable t) { return false; } }注意这个方法有一个前提条件目标 App 是独立进程运行的。如果目标 App 用了android:process开启了多进程你需要判断一下是哪个进程里的FrameLayout做布局如果不是目标进程就直接返回 false。另外ActivityThread是hide的内部类直接引用在单独编译framework.jar时没问题但如果你要编译成普通 App 依赖framework.jar需要先 hide 掉一些 API。3.4 关于 resolveSizeAndState 的细节务必要懂上面的代码里resolveSizeAndState是我特意保留的原生调用这里有个很关键的细节必须说明。resolveSizeAndState这个方法的作用是根据子 View 自己想要的尺寸第一个参数和父容器传下来的MeasureSpec第二个参数决定最终测量结果。具体规则是如果MeasureSpec是EXACTLY精确模式即父容器已经给定了确定大小那不管你传多大最终结果都是MeasureSpec.getSize()。如果MeasureSpec是AT_MOST至多模式即父容器说“你最多能这么大”那最终结果是min(你想要的大小, MeasureSpec.getSize())。如果MeasureSpec是UNSPECIFIED未指定那最终结果就是你自己想要的大小。这解释了一个常见困惑为什么我在onMeasure里强行把maxWidth改小了但最终布局宽度可能没变就是因为MeasureSpec可能是EXACTLY模式直接把你的修改给“吸收”了。那怎么办两种选择第一种手动构造新的MeasureSpec把它改小后再调用resolveSizeAndState。比如int newWidthSpec MeasureSpec.makeMeasureSpec(targetMaxWidth, MeasureSpec.EXACTLY); setMeasuredDimension(resolveSizeAndState(maxWidth, newWidthSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState));但这样做有个副作用FrameLayout的尺寸是被父容器已知的比如DecorView的contentParent一般会以EXACTLY模式给它传入屏幕尺寸。如果你手动改了 FrameLayout 的最终尺寸它和父容器的期望对不上布局完成后可能被父容器重新拉回来。第二种不做表面功夫直接干预子 View 的测量宽度。也就是在遍历子 View 的时候如果某个子 View 测出来太宽直接把它重新measure一次传入一个更小的MeasureSpec。这样 FrameLayout 的“最大子 View 尺寸”自然就变小了测量结果也就真正变小了父容器也不会反向纠正。我实际项目里用的是第二种思路它更可靠。代码大致改为for (int i 0; i count; i) { final View child getChildAt(i); if (mMeasureAllChildren || child.getVisibility() ! GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); int childWidth child.getMeasuredWidth(); int childHeight child.getMeasuredHeight(); // 定制逻辑限制子 View 的最大尺寸 if (isTargetPackage()) { int maxChildWidth (int) (dm.widthPixels * 0.85f); if (childWidth maxChildWidth) { int childWidthSpec MeasureSpec.makeMeasureSpec(maxChildWidth, MeasureSpec.EXACTLY); child.measure(childWidthSpec, MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)); childWidth child.getMeasuredWidth(); } } maxWidth Math.max(maxWidth, childWidth); maxHeight Math.max(maxHeight, childHeight); childState combineMeasuredStates(childState, child.getMeasuredState()); } }这种方式的关键在于理解一个事实父布局的最终尺寸取决于子 View 的最大尺寸你改了子 View 的测量结果父布局就自然变小。而且由于子 View 的尺寸又受MeasureSpec约束用EXACTLY模式重测是最可控的。3.5 编译与刷机验证的完整流程代码改完后编译 Framework 模块。单独编译 framework 的指令make framework -j16正常情况下大概几分钟到十几分钟即可编译完成。编译产物在out/target/product/crosshatch/system/framework/framework.jar out/target/product/crosshatch/system/framework/arm64/boot-framework.oat但这里有个很关键的坑Android 12 开始Framework 的很多逻辑被改写进了framework.jar的boot-*产物中编译完成后system/framework路径下会出现一堆.jar、.art、.oat、.vdex文件。你如果只替换了framework.jar重启之后大概率会报 boot 失败。正确的做法是编译完成后用make snod快速生成一个新的system.img或者手动把所有相关产物同步过去adb root adb remount adb sync system adb reboot这里我建议优先使用adb sync system而不是单独 pushframework.jar。因为 Android 的 boot classpath 是由system/framework下的所有 jar 和 oat 文件共同组成的单独替换一个文件容易导致 odex 与 jar 不一致的崩溃。刷机后如果一切正常你打开目标 App应该能看到页面的宽度被限制在屏幕的 85%两侧留出空白。如果打开其他系统应用布局完全正常。3.6 验证脚本与效果确认刷机验证阶段我习惯配合一段 shell 脚本去快速检查各个关键进程的视图树。这个性能开销不大但对定位问题帮助很大adb shell dumpsys activity top | grep -E ViewRootImpl|DecorView|FrameLayout | head -50如果目标 App 的根布局仍然是屏幕全宽说明修改可能没有生效或者被resolveSizeAndState吃掉了。这时需要再排查MeasureSpec传入的模式。还有一个快速确认“代码是否真的执行了”的做法是在onMeasure的定制逻辑里加一行日志Log.d(LayoutHook, FrameLayout.onMeasure: target isTargetPackage() maxWidth maxWidth dmWidth dm.widthPixels);用adb logcat -s LayoutHook就能实时看到每个FrameLayout的测量情况。当你打开目标 App 时能看到日志里连续输出targettrue就说明包名过滤已经命中。这个方法比我瞎猜问题根源要高效得多。4. 常见问题与排查技巧实录4.1 为什么修改后宽度没变化这是最常遇到的问题。我自己第一次做 Hook 验证的时候就踩了。原因我在前面提到过就是resolveSizeAndState对EXACTLY模式的MeasureSpec会直接采用父容器给定值。你在onMeasure里改maxWidth最后还是会被子 View 或父容器的约束“拉回去”。排查步骤先确认代码是否执行看logcat里的日志确认包名过滤命中。再确认修改位置如果你只是改了maxWidth而没有重测子 View那很可能不生效。按照 3.4 的思路直接干预子 View 的测量。最后确认有没有被父容器二次纠正在onLayout里打日志看看最终布局尺寸是多少对比有没有被父容器重新计算。我最终在项目里用的是直接干预子 View 的方式同时打印了几行关键日志确认宽度确实“瘦身成功”父容器也没有纠正回来。4.2 系统全部界面被影响出现“一刀切”事故这个问题我在做第一个版本时真实遇到过。当时图省事没有加包名过滤直接在onMeasure里做了全局 85% 的限宽结果手机刷完机进系统所有界面包括系统设置全部两侧留白。视觉上就像是整个设备被困在一个小窗口里体验非常糟糕。解决方式也简单就是加包名过滤严格控制影响范围。但这里我多说一句包名过滤本身也有坑。ActivityThread.currentApplication().getPackageName()在系统进程里返回的是android在系统 UI 进程返回的是com.android.systemui你要确保只匹配目标 App 的包名不要用“包含”逻辑否则com.example.thirdparty可能会误伤到com.example.thirdparty.debug之类的包。4.3 修改后出现 ANR 或递归重启onMeasure里如果调用了requestLayout()就会陷入“测量 - 请求布局 - 再测量 - 再请求”的死循环最终导致 ANR。如果你改的逻辑是在onMeasure里根据测量结果动态改变子 View 的布局参数然后又触发子 View 的重新测量这个循环很容易出现。我处理这个问题的方案是引入一个“会话标志”private boolean isCustomizing false; Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ... if (isTargetPackage() !isCustomizing) { isCustomizing true; try { // 做干预逻辑 } finally { isCustomizing false; } } ... }这个标志不能完全避免递归但至少能让递归只发生一次因为第二次进入onMeasure时isCustomizing已经为 true不会再执行干预逻辑。这算是一个简单粗暴但有效的保护。另外有一点如果干预逻辑中对子 View 调用了requestLayout()子 View 的重新测量是异步的可能会在这个FrameLayout的onMeasure还没结束时又触发布局。所以最好的做法是干预逻辑里千万不要主动请求布局只改测量结果剩下的交给系统正常的测量流程去收敛。4.4 第三方 App 升级后布局突变这个问题可以说是 Framework 层适配的“准定时炸弹”。我做这个项目时第三方 App 做了一个小版本升级内部的根布局从FrameLayout换成了ConstraintLayout于是我那套基于“遍历子 View 限宽”的逻辑就失效了大半因为ConstraintLayout的测量逻辑自有乾坤不归FrameLayout管。后来我学到的教训是Framework 层适配一定要做“兜底兼容”。具体做法是不仅改FrameLayout也分析一下目标 App 可能用到的其他常见容器LinearLayout、RelativeLayout、ConstraintLayout的测量逻辑。尤其是在自己的补丁里加一个可配置的容器类型白名单方便后续版本升级时快速切换适配目标。这个需求听起来工作量翻倍但如果你做的是 ROM 层面的适配这是躲不掉的。我在生产补丁里最后适配了四个容器FrameLayout、LinearLayout、RelativeLayout、ConstraintLayout分别打通了它们各自的测量入口全部走统一的“子 View 限宽”逻辑。每个容器的适配其实都在重复相似的模式工程量可控。4.5 常见问题速查表问题现象可能原因解决方案改完没效果MeasureSpec为 EXACTLY 被动覆盖干预子 View 测量而非只改 maxWidth系统全局界面变形包名过滤失效或未加加白名单进程判断用精确包名匹配刷机后无法开机替换 framework 产物不完整使用adb sync system或make snod重新生成分区页面卡顿/ANRonMeasure 中触发布局重绘增加标志位保护避免 requestLayoutApp 升级后失效布局容器类型变换适配多种容器做兜底逻辑部分页面仍然全宽该页面根容器不是 FrameLayout检查该页面 View 树按容器类型补充适配5. 经验总结与后续扩展方向5.1 我踩过的最深的坑以及你现在就能用的经验这个项目做下来我最深的体会是Framework 层改布局真正麻烦的不是代码本身而是“如何保住系统的稳定性”。onMeasure是系统运行的基础路径哪怕你只是改了一行代码它都可能影响上万个界面。所以我强烈建议第一改之前先写 Hook 验证不要直接编译刷机。用 LSPosed 或者其他 Hook 框架模拟修改逻辑验证目标和效果再动 AOSP 源码。这个步骤能帮你节省至少一天的编译调试时间。第二所有的定制逻辑尽量收敛到少数工具类不要在onMeasure源码里堆业务代码。你要做的只是从工具类读取配置、判断是否干预、执行干预把逻辑与框架源码分离。这样后续维护配置时不需要重新编译 framework甚至可以做成配置文件动态下发。第三日志要打得足够全。我在开发阶段保留了详细的tight_log每次刷机后第一件事就是拉日志确认执行路径。没有日志你在 Framework 层面对问题基本是盲人摸象。第四千万记得用MeasureSpec来约束子 View而不是直接用setMeasuredDimension改父容器的尺寸。这个原则是我用好几台设备“刷砖”换来的希望看到这篇文章的朋友能少走弯路。5.2 往后的扩展从 FrameLayout 到 SystemUI 适配这个项目的思路其实可以扩展得更广。改完FrameLayout之后你会发现 Framework 层能做的事情远比想象的多。举个例子你可以用同样的思路去修改LinearLayout.onMeasure实现动态文字大小的适配可以修改TextView.onDraw统一修改全局字体的加粗效果可以修改RecyclerView的布局逻辑让列表在特定 App 里自动调节行距。这些都是在系统源码基础上做的“定制”适合 ROM 级产品建立差异化体验。如果你愿意再往前走一步甚至可以把这个思路移植到SystemUI里。比如定制通知栏的展开高度、状态栏的图标大小、下拉面板的布局比例都是同一个模式找到对应的 View 容器在onMeasure阶段干预尺寸。这套方法论是通用的。5.3 最后分享一个小技巧在实际调试过程中还有一个非常有用的技巧在adb shell里动态修改View的尺寸来做快速验证不需要重新编译系统。具体做法是在目标 App 的进程里用adb shell配合一个简单的 hook 脚本例如通过app_process启动一个 Java 程序用反射修改某个 View 的LayoutParams或者干脆用adb shell am broadcast触发你的 hook 代码。这种方式虽然不能直接改FrameLayout.onMeasure但可以帮助你在不刷机的情况下确认“如果宽度缩到 85%这个 App 的 UI 是否还能正常运作”。这个确认做完再决定要不要上 Framework 补丁性价比最高。还有一个我自己一直在用的习惯每次刷完机打开目标 App 后截图并记录dumpsys activity top的 View 树和正常版本对比。如果差异符合预期就说明适配成功。这个验证方法简单效果直观也能在团队协作时快速说明问题。这个项目后续如果再迭代我可能会尝试在onMeasure里加入更智能的规则比如根据当前 Activity 的包名和类名动态调整干预强度甚至做成一个可配置的“布局干预引擎”。如果你也在做 ROM 定制或者对 Framework 层适配感兴趣这套方法论值得你亲自试验一遍——不亲自刷一次机看再多文章都只是纸上谈兵。
返回列表