AndroidAutoSize 框架原理分析与核心问题

AndroidAutoSize 框架原理分析与核心问题

一、框架概述

AndroidAutoSize 是一个基于今日头条屏幕适配方案 的 Android UI 适配框架,核心思想是:通过动态修改 DisplayMetricsdensitydensityDpiscaledDensityxdpi 四个值,欺骗系统布局计算,使所有基于 dp/sp 的尺寸按设计图比例自动缩放

GitHub: github.com/JessYanCodi...


二、整体架构

scss 复制代码
InitProvider (ContentProvider 自动初始化,零侵入)
    │
    ▼
AutoSizeConfig (全局配置单例,保存原始值 + 设计图尺寸)
    │
    ├──▶ ActivityLifecycleCallbacksImpl (监听所有 Activity 生命周期)
    │         │
    │         ▼
    │    WrapperAutoAdaptStrategy (装饰器,触发 onAdaptListener 回调)
    │         │
    │         ▼
    │    DefaultAutoAdaptStrategy (适配决策优先级)
    │         │
    │         ├── ExternalAdaptManager 检查 → 三方库适配
    │         ├── CancelAdapt 接口检查 → 取消适配
    │         ├── CustomAdapt 接口检查 → 自定义参数适配
    │         └── 默认 → 全局配置适配
    │
    ├──▶ AutoSize (核心算法:计算并修改 DisplayMetrics)
    ├──▶ AutoSizeCompat (兼容修复:针对重写 getResources() 的 Activity)
    └──▶ DisplayMetricsInfo (计算结果缓存,SparseArray)

三、核心实现原理(逐层剖析)

3.1 初始化阶段 --- ContentProvider 自动初始化

java 复制代码
// InitProvider.java
public class InitProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        AutoSizeConfig.getInstance().init((Application) getContext().getApplicationContext());
        return true;
    }
}

原理 :利用 Android ContentProvider 在 Application.onCreate() 之前自动创建的机制,实现零侵入自动初始化。用户只需添加依赖,无需手动调用 init。

init() 中做了以下关键事情:

  1. Resources.getSystem().getDisplayMetrics() 获取系统原始 DisplayMetrics 值并保存(用于取消适配时恢复)
  2. 从 AndroidManifest 的 <meta-data> 读取设计图宽高(design_width_in_dpdesign_height_in_dp
  3. 获取设备屏幕实际像素尺寸
  4. 注册 ActivityLifecycleCallbacks 监听所有 Activity 生命周期
  5. 注册 ComponentCallbacks 监听系统配置变化(字体缩放、屏幕旋转等)
  6. 检测 MIUI 系统并反射获取 Resources.mTmpMetrics 字段

3.2 适配触发时机 --- Activity 生命周期拦截

java 复制代码
// ActivityLifecycleCallbacksImpl.java
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    // 注册 Fragment 生命周期回调(如果开启)
    // 执行适配
    mAutoAdaptStrategy.applyAdapt(activity, activity);
}

@Override
public void onActivityStarted(Activity activity) {
    // 再次执行适配(处理从后台恢复等场景)
    mAutoAdaptStrategy.applyAdapt(activity, activity);
}

关键点

  • onActivityCreatedonActivityStarted 两个时机都执行适配,因为 Activity 从后台恢复时可能需要重新适配
  • 通过 Application.ActivityLifecycleCallbacks 实现类似 AOP 的效果,无需继承 BaseActivity

3.3 适配决策 --- 策略模式 + 优先级链

java 复制代码
// DefaultAutoAdaptStrategy.java
public void applyAdapt(Object target, Activity activity) {
    // 优先级1: ExternalAdaptManager (三方库)
    // 优先级2: CancelAdapt 接口 → 取消适配
    // 优先级3: CustomAdapt 接口 → 自定义参数
    // 优先级4: 全局配置
}

决策链

  1. ExternalAdaptManager:为无法修改源码的三方库 Activity 提供外部适配参数
  2. CancelAdapt:标记接口,实现即取消适配(恢复系统原始值)
  3. CustomAdapt :自定义接口,提供 isBaseOnWidth()getSizeInDp() 两个方法
  4. 全局配置:使用 AndroidManifest 中配置的设计图尺寸

3.4 核心算法 --- 修改 DisplayMetrics

这是整个框架最核心的部分:

java 复制代码
// AutoSize.java --- autoConvertDensity()
// 核心公式:
targetDensity = screenWidth / designWidthInDp          // 以宽度为基准
targetDensity = screenHeight / designHeightInDp        // 以高度为基准

// 字体缩放:
systemFontScale = initScaledDensity / initDensity      // 系统字体缩放比
targetScaledDensity = targetDensity * systemFontScale   // 保持字体缩放一致性

// densityDpi:
targetDensityDpi = (int)(targetDensity * 160)           // 160 是基准值 (1dp = 1px @160dpi)

// xdpi(用于 pt/in/mm 单位):
targetXdpi = screenWidth / subunitsDesignSize

赋值过程setDensity 方法):

java 复制代码
private static void setDensity(Activity activity, ...) {
    // 1. 修改 Activity 级别的 DisplayMetrics
    DisplayMetrics activityMetrics = activity.getResources().getDisplayMetrics();
    activityMetrics.density = targetDensity;
    activityMetrics.densityDpi = targetDensityDpi;
    activityMetrics.scaledDensity = targetScaledDensity;
    activityMetrics.xdpi = targetXdpi;

    // 2. 同时修改 Application 级别的 DisplayMetrics
    //    确保 Dialog、PopupWindow 等也生效
    DisplayMetrics appMetrics = application.getResources().getDisplayMetrics();
    // ... 同上

    // 3. MIUI 兼容:通过反射修改 Resources.mTmpMetrics
    DisplayMetrics miuiMetrics = getMetricsOnMiui(resources);
    // ... 同上
}

为什么同时修改 Activity 和 Application 的 DisplayMetrics?

  • Activity 的 getResources().getDisplayMetrics() 影响该 Activity 内的 View 布局
  • Application 的 getResources().getDisplayMetrics() 影响 Dialog、Toast、PopupWindow 等使用 Application Context 创建的组件

3.5 缓存机制 --- SparseArray

java 复制代码
private static SparseArray<DisplayMetricsInfo> mCache = new SparseArray<>();

// key 的生成:将设计尺寸、屏幕尺寸、基准方向等参数组合成一个 int
int key = Math.round((sizeInDp + subunitsDesignSize + screenSize) * initScaledDensity) & ~MODE_MASK;
key = isBaseOnWidth ? (key | MODE_ON_WIDTH) : (key & ~MODE_ON_WIDTH);
key = isUseDeviceSize ? (key | MODE_DEVICE_SIZE) : (key & ~MODE_DEVICE_SIZE);

目的:避免每次 Activity 创建/恢复时重复计算,用 SparseArray 缓存计算结果。

3.6 五种单位支持

单位 修改字段 原理
dp density, densityDpi 1dp = density px
sp scaledDensity 1sp = scaledDensity px
pt xdpi * 72 1pt = 1/72 inch
in xdpi 1in = xdpi px
mm xdpi * 25.4 1mm = 1/25.4 inch

3.7 MIUI 兼容

MIUI 系统修改了 Android 框架,在 Resources 中增加了 mTmpMetrics 字段。框架通过反射检测并同步修改该字段:

java 复制代码
if ("MiuiResources".equals(resources.getClass().getSimpleName())
    || "XResources".equals(resources.getClass().getSimpleName())) {
    isMiui = true;
    mTmpMetricsField = Resources.class.getDeclaredField("mTmpMetrics");
    mTmpMetricsField.setAccessible(true);
}

3.8 热插拔机制

java 复制代码
// 停止适配
public void stop(Activity activity) {
    application.unregisterActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
    AutoSize.cancelAdapt(activity);  // 恢复系统原始值
    isStop = true;
}

// 重新启动
public void restart() {
    application.registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
    isStop = false;
}

四、一张图总结核心原理

ini 复制代码
设计图 360dp × 640dp
        │
        ▼
设备屏幕 1080px × 1920px
        │
        ▼
targetDensity = 1080 / 360 = 3.0  (系统默认可能是 2.75 或 440dpi)
        │
        ▼
修改 DisplayMetrics.density = 3.0
        │
        ▼
布局时: 100dp × 3.0 = 300px  (在设计图上 100/360 = 27.8% 屏幕宽度)
        │
        ▼
在任何设备上: 100dp 始终占屏幕宽度的 27.8%

五、10 个核心问题

Q1:为什么选择修改 DisplayMetrics.density 而不是其他适配方案(如约束布局、百分比布局、smallestWidth 限定符)?

本质问题:修改 density 方案的核心权衡是什么?

修改 density 是一种运行时全局劫持方案,其优势在于:

  • 零成本接入:不需要修改任何现有布局代码,所有 dp/sp 值自动生效
  • 覆盖范围广:Activity、Fragment、Dialog、自定义 View 全部自动适配
  • 设计稿 1:1 还原:设计师给的 dp 值直接使用,无需换算

但代价也很明显:

  • 全局副作用:修改的是进程级的 DisplayMetrics,影响范围不可控
  • 第三方库兼容风险:第三方库的布局也会被影响,可能变形
  • 调试困难:布局实际像素值与代码中写的 dp 值不一致,增加理解成本

Q2:同时修改 Activity 和 Application 级别的 DisplayMetrics,在多 Activity 场景下会产生什么竞态问题?

本质问题:全局状态的并发安全性。

源码中 setDensity 方法同时修改了:

java 复制代码
activity.getResources().getDisplayMetrics()   // Activity 级别
application.getResources().getDisplayMetrics() // Application 级别

问题场景

  • Activity A 以宽度为基准适配(density = 3.0)
  • Activity B 以高度为基准适配(density = 2.5)
  • 当 Activity A 启动 Activity B 时,Application 级别的 DisplayMetrics 会被覆盖为 B 的值
  • 如果 A 中有使用 Application Context 创建的 Dialog,此时 Dialog 的密度值已经变了

更严重的问题onActivityCreatedonActivityStarted 都会执行适配,但没有看到任何同步机制(synchronized 或 CAS),在快速切换 Activity 时可能出现竞态。


Q3:onActivityCreatedonActivityStarted 都执行适配,为什么需要两个时机?是否存在重复执行的性能浪费?

本质问题:适配时机的精确性 vs 性能开销。

源码中两个时机都调用了 mAutoAdaptStrategy.applyAdapt(activity, activity)

  • onActivityCreated:确保 setContentView() 之前完成适配
  • onActivityStarted:处理 Activity 从后台恢复的场景

问题

  • 正常启动流程中,created → started连续触发两次适配,第二次完全是浪费
  • 框架没有记录上次适配的参数,无法判断是否需要重新适配
  • 如果 CustomAdapt 的 getSizeInDp() 每次返回不同值(比如根据运行时状态动态计算),两次执行可能导致不一致

Q4:缓存 key 的生成算法是否存在哈希碰撞风险?

本质问题:缓存正确性。

java 复制代码
int key = Math.round((sizeInDp + subunitsDesignSize + screenSize) * initScaledDensity) & ~MODE_MASK;
key = isBaseOnWidth ? (key | MODE_ON_WIDTH) : (key & ~MODE_ON_WIDTH);
key = isUseDeviceSize ? (key | MODE_DEVICE_SIZE) : (key & ~MODE_DEVICE_SIZE);

问题

  • key 由 sizeInDp + subunitsDesignSize + screenSize求和生成,而非组合
  • 例如:sizeInDp=300, subunits=100, screen=1080sizeInDp=200, subunits=200, screen=1080 的求和都是 1480,会产生相同的 key
  • 虽然实际使用中 subunitsDesignSize 通常等于 sizeInDp(当未配置副单位时),碰撞概率较低,但这是一个潜在的 bug

Q5:getMetaData() 在子线程中读取 AndroidManifest,是否存在初始化时序问题?

本质问题:异步初始化与同步使用的矛盾。

java 复制代码
private void getMetaData(final Context context) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 读取 AndroidManifest meta-data
            mDesignWidthInDp = (int) applicationInfo.metaData.get(KEY_DESIGN_WIDTH_IN_DP);
            mDesignHeightInDp = (int) applicationInfo.metaData.get(KEY_DESIGN_HEIGHT_IN_DP);
        }
    }).start();
}

问题

  • init() 方法中先设置了默认值(360×640),然后异步读取 Manifest
  • 如果第一个 Activity 启动时 Manifest 还没读完,会使用默认值 360×640 进行适配
  • 适配完成后 Manifest 读取完毕,mDesignWidthInDp 被更新,但已经适配的 Activity 不会重新适配
  • 这在低配设备上尤其明显,源码注释也承认了这个问题

Q6:字体缩放的处理策略(scaledDensity)是否真正解决了字体大小一致性问题?

本质问题:字体适配的数学正确性。

java 复制代码
systemFontScale = initScaledDensity / initDensity;
targetScaledDensity = targetDensity * systemFontScale;

分析

  • initScaledDensity / initDensity 计算的是系统字体缩放比例(用户在设置中调整的)
  • targetDensity * systemFontScale 让 sp 值在适配后的设备上保持与系统字体设置一致的比例

问题

  • 如果用户系统字体设置为"特大",systemFontScale 可能是 1.3
  • 在设计图上 14sp 的字体,适配后变成 14 * targetDensity * 1.3 px
  • 但设计师设计时通常不考虑系统字体缩放,这会导致文字溢出布局
  • 框架提供了 isExcludeFontScaleprivateFontScale 来应对,但默认是跟随系统,大多数场景下会导致布局错乱

Q7:AutoSizeCompat 的存在说明了什么设计缺陷?为什么不能在 AutoSize 中统一解决?

本质问题:框架对 Android 资源加载机制的理解边界。

AutoSizeCompatAutoSize 逻辑几乎完全相同,但参数是 Resources 而非 Activity。使用场景是:

java 复制代码
// 某些 Activity 重写了 getResources()
@Override
public Resources getResources() {
    return super.getResources(); // 或者返回自定义 Resources
}

问题

  • 如果 Activity 重写了 getResources() 返回不同的 Resources 实例,AutoSize 修改的 DisplayMetrics 就不是实际使用的那个
  • 这说明框架的适配是基于对象引用修改,而非基于 ClassLoader 或 Resources 体系的拦截
  • 根本原因是 Android 的 Resources 体系可能有多个 DisplayMetrics 实例,框架无法保证全部覆盖
  • WebView、RecyclerView 等内部可能有自己的 Resources 缓存,无法被框架覆盖

Q8:策略模式(AutoAdaptStrategy)+ 装饰器(WrapperAutoAdaptStrategy)的设计是否过度工程化?

本质问题:设计复杂度与实际需求的匹配度。

scss 复制代码
AutoAdaptStrategy (接口)
    └── WrapperAutoAdaptStrategy (装饰器,加 onAdaptListener)
            └── DefaultAutoAdaptStrategy (默认实现)

分析

  • WrapperAutoAdaptStrategy 唯一的作用是在 applyAdapt 前后调用 onAdaptListener 的回调
  • 完全可以在 DefaultAutoAdaptStrategy 内部直接调用 listener,不需要装饰器模式
  • AutoAdaptStrategy 接口允许用户完全替换适配逻辑,但实际使用中几乎没人会这么做
  • 这种设计增加了理解成本,对 90% 的使用者来说是过度工程化

Q9:在多进程场景下,AutoSizeConfig 单例 + 静态缓存 mCache 会有什么问题?

本质问题:Android 多进程内存隔离。

java 复制代码
private static volatile AutoSizeConfig sInstance;  // 单例
private static SparseArray<DisplayMetricsInfo> mCache = new SparseArray<>();  // 静态缓存

问题

  • Android 每个进程有独立的 JVM 实例,静态变量不跨进程共享
  • 如果 App 有多个进程(如 push 服务进程、webview 进程),每个进程的 AutoSizeConfig 是独立初始化的
  • InitProvider 会在每个进程中执行,但 init() 方法有 mInitDensity == -1 的防重入检查,所以每个进程都会独立初始化
  • 框架提供了 initCompatMultiProcess() 方法,通过 ContentProvider query 触发初始化,但这只是解决了初始化问题,缓存仍然不共享

Q10:这种"修改全局 DisplayMetrics"的方案,在 Android 新版本(如折叠屏、多窗口、多密度显示)下面临的根本性挑战是什么?

本质问题:方案的生命周期和未来兼容性。

挑战

  1. 折叠屏 :屏幕尺寸在运行时动态变化,onConfigurationChanged 中虽然会更新 mScreenWidth/mScreenHeight,但已经创建的 View 不会重新布局

  2. 多窗口模式 :Activity 可能只占屏幕的一部分,此时 getScreenSize() 返回的是全屏尺寸而非窗口尺寸,适配比例会错误

  3. 多密度显示:Android 10+ 支持外接显示器,不同显示器可能有不同的 density,全局修改 density 无法区分

  4. Compose:Jetpack Compose 不使用 DisplayMetrics 进行布局计算,此方案对 Compose 完全无效

  5. WebView 内的 H5 页面 :WebView 使用自己的 density 设置(WebSettings.setUseWideViewPort 等),与 Android View 体系的 density 修改无关


六、核心考题(层层递进,彻底掌握)

基础原理层(知其然)

考题 1:核心公式填空

AndroidAutoSize 的核心适配公式是什么?请补全以下代码:

java 复制代码
// 以宽度为基准
targetDensity = ______ / ______;
// 以高度为基准  
targetDensity = ______ / ______;
// densityDpi 的计算
targetDensityDpi = (int)(targetDensity * ______);

答案与解析

java 复制代码
// 以宽度为基准
targetDensity = screenWidth / designWidthInDp;
// 以高度为基准  
targetDensity = screenHeight / designHeightInDp;
// densityDpi 的计算
targetDensityDpi = (int)(targetDensity * 160);

为什么乘以 160?

Android 定义 160dpi 为基准密度(mdpi),此时 1dp = 1px。densityDpi 是系统用于资源选择(如 drawable-mdpi/hdpi)的密度值,与 density 的关系是:densityDpi = density * 160


考题 2:初始化机制

框架如何实现"零侵入"自动初始化?请说明关键类和机制。
答案与解析

关键类InitProvider(继承 ContentProvider)

机制

  1. Android 的 ContentProvider 会在 Application.onCreate() 之前自动创建
  2. InitProvider.onCreate() 中调用 AutoSizeConfig.getInstance().init(application)
  3. 用户只需添加依赖,无需手动调用初始化代码

源码

java 复制代码
public class InitProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        AutoSizeConfig.getInstance()
            .init((Application) getContext().getApplicationContext());
        return true;
    }
}

考题 3:适配触发时机

框架在 Activity 的哪些生命周期回调中执行适配?为什么要选这些时机?
答案与解析

两个时机

  1. onActivityCreated():确保 setContentView() 之前完成适配
  2. onActivityStarted():处理 Activity 从后台恢复的场景

源码

java 复制代码
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    mAutoAdaptStrategy.applyAdapt(activity, activity);
}

@Override
public void onActivityStarted(Activity activity) {
    mAutoAdaptStrategy.applyAdapt(activity, activity);
}

注意:这会导致正常启动时连续执行两次适配,存在性能浪费。


源码实现层(知其所以然)

考题 4:DisplayMetrics 修改范围

为什么 setDensity() 方法要同时修改 Activity 和 Application 级别的 DisplayMetrics?
答案与解析

java 复制代码
private static void setDensity(Activity activity, ...) {
    // 1. Activity 级别:影响该 Activity 内的 View 布局
    DisplayMetrics activityMetrics = activity.getResources().getDisplayMetrics();
    setDensity(activityMetrics, ...);
    
    // 2. Application 级别:影响 Dialog、PopupWindow、Toast 等
    //    这些组件使用 Application Context 创建
    DisplayMetrics appMetrics = application.getResources().getDisplayMetrics();
    setDensity(appMetrics, ...);
}

如果只修改 Activity 级别:Dialog、Toast 等使用 Application Context 的组件不会适配,显示比例错误。


考题 5:缓存 key 的设计缺陷

以下缓存 key 生成代码存在什么潜在问题?

java 复制代码
int key = Math.round((sizeInDp + subunitsDesignSize + screenSize) 
    * initScaledDensity) & ~MODE_MASK;

答案与解析

问题:哈希碰撞风险

key 由三个参数求和生成,而非组合:

  • (sizeInDp=300, subunits=100, screen=1080) → 求和 = 1480
  • (sizeInDp=200, subunits=200, screen=1080) → 求和 = 1480

两个不同的参数组合产生相同的 key,导致缓存命中错误的结果。

正确做法 :应该使用类似 Objects.hash(sizeInDp, subunitsDesignSize, screenSize) 的组合方式。


考题 6:异步初始化的时序问题

getMetaData() 方法在什么线程执行?可能引发什么问题?
答案与解析

java 复制代码
private void getMetaData(final Context context) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 异步读取 AndroidManifest
            mDesignWidthInDp = ...;
            mDesignHeightInDp = ...;
        }
    }).start();
}

问题

  1. init() 先设置默认值 360×640
  2. 然后异步读取 Manifest 中的真实设计图尺寸
  3. 如果第一个 Activity 启动时 Manifest 还没读完,会用默认值适配
  4. 适配完成后即使 Manifest 读取完毕,已适配的 Activity 不会重新适配

源码注释也承认了这个问题

建议使用者在低配设备上主动在 Application#onCreate 中调用 setDesignWidthInDp 替代以使用 AndroidManifest 配置设计图尺寸的方式


考题 7:MIUI 兼容的实现

框架如何兼容 MIUI 系统?为什么要这样做?
答案与解析

检测 MIUI

java 复制代码
if ("MiuiResources".equals(application.getResources().getClass().getSimpleName())
    || "XResources".equals(application.getResources().getClass().getSimpleName())) {
    isMiui = true;
    mTmpMetricsField = Resources.class.getDeclaredField("mTmpMetrics");
    mTmpMetricsField.setAccessible(true);
}

兼容方式

java 复制代码
private static DisplayMetrics getMetricsOnMiui(Resources resources) {
    if (AutoSizeConfig.getInstance().isMiui() 
        && AutoSizeConfig.getInstance().getTmpMetricsField() != null) {
        return (DisplayMetrics) mTmpMetricsField.get(resources);
    }
    return null;
}

原因 :MIUI 修改了 Android 框架,在 Resources 中增加了 mTmpMetrics 字段。如果不同步修改该字段,在 MIUI7 + Android 5.1.1 上适配会失效。


设计权衡层(深入理解)

考题 8:AutoSizeCompat 的存在意义

AutoSizeCompatAutoSize 有什么区别?它的存在说明了什么设计缺陷?
答案与解析

区别

AutoSize AutoSizeCompat
参数 Activity Resources
使用场景 正常 Activity 重写 getResources() 的 Activity

使用示例

java 复制代码
public class MainActivity extends Activity {
    @Override
    public Resources getResources() {
        // 返回自定义 Resources
        AutoSizeCompat.autoConvertDensityOfGlobal(super.getResources());
        return super.getResources();
    }
}

说明的设计缺陷 : 框架基于对象引用修改 DisplayMetrics,无法保证覆盖 Android Resources 体系中的所有实例。如果 Activity 重写 getResources() 返回不同的 Resources 实例,AutoSize 修改的就不是实际使用的那个。

这暴露了框架的根本局限:无法拦截 Resources 的创建,只能事后修补。


考题 9:字体缩放策略的取舍

框架如何处理系统字体缩放?默认策略有什么隐患?
答案与解析

计算公式

java 复制代码
systemFontScale = initScaledDensity / initDensity;
targetScaledDensity = targetDensity * systemFontScale;

逻辑

  • initScaledDensity / initDensity = 系统字体缩放比例(用户在设置中调整的)
  • 保持 sp 值在适配后的设备上与系统字体设置一致的比例

隐患

  • 用户设"特大"字体时,systemFontScale 可能是 1.3
  • 设计图上 14sp 的字体,适配后变成 14 * targetDensity * 1.3 px
  • 设计师设计时通常不考虑系统字体缩放,这会导致文字溢出布局

应对选项

  • isExcludeFontScale = true:屏蔽系统字体缩放
  • privateFontScale:APP 独立控制字体缩放

但默认是跟随系统,大多数场景下会导致布局错乱。


考题 10:策略模式是否过度设计

AutoAdaptStrategy + WrapperAutoAdaptStrategy + DefaultAutoAdaptStrategy 三层结构是否必要?
答案与解析

结构

scss 复制代码
AutoAdaptStrategy (接口)
    └── WrapperAutoAdaptStrategy (装饰器)
            └── DefaultAutoAdaptStrategy (默认实现)

WrapperAutoAdaptStrategy 的唯一作用

java 复制代码
public void applyAdapt(Object target, Activity activity) {
    onAdaptListener.onAdaptBefore(target, activity);  // 前置回调
    mAutoAdaptStrategy.applyAdapt(target, activity);   // 实际适配
    onAdaptListener.onAdaptAfter(target, activity);    // 后置回调
}

问题

  • 装饰器模式增加了不必要的抽象层
  • 完全可以在 DefaultAutoAdaptStrategy 内部直接调用 listener
  • 允许用户替换整个适配策略的接口,实际使用中几乎没人会这么做

结论:对 90% 的使用者来说是过度工程化,增加了理解成本。


边界与局限层(高阶思考)

考题 11:多进程场景的问题

如果 App 有多个进程(如 push 服务进程),AutoSizeConfig 单例 + 静态缓存 mCache 会有什么问题?
答案与解析

问题

  1. Android 每个进程有独立的 JVM 实例,静态变量不跨进程共享
  2. InitProvider 会在每个进程中执行,init() 的防重入检查 mInitDensity == -1 在每个进程都成立
  3. 每个进程的 AutoSizeConfigmCache 都是独立实例

框架的应对

java 复制代码
public static void initCompatMultiProcess(Context context) {
    context.getContentResolver().query(
        Uri.parse("content://" + context.getPackageName() + ".autosize-init-provider"),
        null, null, null, null);
}

通过 ContentProvider query 触发初始化,但缓存仍然不共享,每个进程独立计算。


考题 12:新场景下的根本性挑战

在折叠屏、多窗口、Jetpack Compose 等新场景下,此方案面临什么根本性挑战?
答案与解析

场景 挑战
折叠屏 屏幕尺寸运行时动态变化,onConfigurationChanged 会更新 mScreenWidth/mScreenHeight,但已创建的 View 不会重新布局
多窗口 Activity 可能只占屏幕一部分,getScreenSize() 返回的是全屏尺寸而非窗口尺寸,适配比例错误
多密度显示 Android 10+ 支持外接显示器,不同显示器有不同 density,全局修改 density 无法区分
Jetpack Compose Compose 不使用 DisplayMetrics 进行布局计算,此方案完全无效
WebView WebView 使用独立的 density 设置(WebSettings),与 Android View 体系的 density 修改无关

根本问题 :此方案是全局静态修改,无法适应 Android 正在向"动态、多窗口、多密度"发展的趋势。


考题 13:为什么需要同时修改 Configuration.screenWidthDp?

除了 DisplayMetrics,框架还修改了 Configuration.screenWidthDp/screenHeightDp,这是为什么?
答案与解析

java 复制代码
private static void setScreenSizeDp(Activity activity, int screenWidthDp, int screenHeightDp) {
    Configuration activityConfiguration = activity.getResources().getConfiguration();
    activityConfiguration.screenWidthDp = screenWidthDp;
    activityConfiguration.screenHeightDp = screenHeightDp;
    
    Configuration appConfiguration = application.getResources().getConfiguration();
    setScreenSizeDp(appConfiguration, screenWidthDp, screenHeightDp);
}

原因

  • screenWidthDp 是 Configuration 中的字段,用于 sw<N>dp 资源限定符选择
  • 如果不修改,系统可能根据原始屏幕尺寸选择错误的资源文件
  • 例如:设计图 360dp,设备 1080px/3.0=360dp,修改后 screenWidthDp=360,确保选择 sw360dp 的资源

注意 :这仅在 isSupportScreenSizeDP() 开启时生效。


综合应用层(实战检验)

考题 14:线上问题排查

用户反馈:在小米手机上,从页面 A(宽度适配,设计图 360dp)跳转到页面 B(高度适配,设计图 640dp)后,返回页面 A 时布局错乱。请分析可能的原因和解决方案。
答案与解析

原因分析

  1. Application 级 DisplayMetrics 被覆盖

    • 页面 B 适配时将 application.getResources().getDisplayMetrics().density 改为基于高度的值
    • 返回页面 A 时,虽然 A 重新执行了适配,但如果 A 中有使用 Application Context 创建的 Dialog/PopupWindow,它们的密度值已经是 B 的
  2. MIUI 系统的 mTmpMetrics 未正确恢复

    • 如果页面 B 实现了 CancelAdapt,调用 cancelAdapt() 恢复的是初始化时的系统值
    • 但页面 A 期望的是基于宽度适配的值,两者冲突

解决方案

  1. 统一适配基准:全局统一使用宽度或高度适配,不要混用
  2. 避免使用 Application Context 创建 UI 组件:改用 Activity Context
  3. 检查 CustomAdapt 实现 :确保 getSizeInDp() 返回正确值,不要返回 0 导致使用全局配置

考题 15:框架选型决策

你的团队要开发一个新项目,目标 API 21+,使用 Jetpack Compose 作为主要 UI 框架。你会选择 AndroidAutoSize 吗?为什么?
答案与解析

不应该选择 AndroidAutoSize

原因

  1. Compose 完全不兼容

    • Compose 使用自己的布局系统,不读取 DisplayMetrics.density
    • AutoSize 对 Compose 完全无效
  2. Compose 有原生适配方案

    • BoxWithConstraints:根据容器尺寸自适应
    • WindowSizeClass:响应式布局(Google 推荐方案)
    • Modifier.fillMaxWidth(fraction = 0.5f):百分比布局
  3. 技术债务

    • AutoSize 是"hack 系统"的方案,随着 Android 版本迭代风险增加
    • Compose 是 Google 官方主推方向,应该使用其原生适配能力

推荐方案

  • 使用 Compose 的响应式布局 API
  • 参考 Material Design 3 的自适应布局规范
  • 使用 WindowSizeClass 实现跨设备适配(手机/平板/折叠屏)

七、知识图谱总结

bash 复制代码
AndroidAutoSize
├── 核心原理
│   └── 修改 DisplayMetrics.density/densityDpi/scaledDensity/xdpi
├── 初始化机制
│   └── ContentProvider 自动初始化(零侵入)
├── 适配触发
│   ├── ActivityLifecycleCallbacks 监听生命周期
│   └── onActivityCreated + onActivityStarted 双重触发
├── 适配策略
│   ├── ExternalAdaptManager(三方库)
│   ├── CancelAdapt(取消适配)
│   ├── CustomAdapt(自定义参数)
│   └── 全局配置(默认)
├── 关键问题
│   ├── 缓存 key 求和导致哈希碰撞
│   ├── 异步读取 Manifest 的时序问题
│   ├── Activity + Application 双级修改的竞态
│   ├── 字体缩放默认跟随系统的隐患
│   └── MIUI 兼容的反射 hack
└── 根本局限
    ├── 全局静态修改无法适应多窗口/折叠屏
    ├── 对 Compose 完全无效
    └── 多进程下缓存不共享

八、总结

AndroidAutoSize 的核心原理可以一句话概括:

在 Activity 创建时,根据 设计图尺寸 / 设备屏幕尺寸 的比例,动态修改 DisplayMetrics.density 等值,使系统在布局计算时自动按比例缩放所有 dp/sp 尺寸。

这是一种简单粗暴但有效的方案,优点是接入成本极低、覆盖范围广,缺点是全局副作用大、对特殊场景(多窗口、折叠屏、Compose)兼容性差。随着 Android 生态的发展,这种"hack DisplayMetrics"的方案正在逐渐被更现代的方案(如 Compose 自适应布局、响应式设计)所替代。

相关推荐
fengci.4 小时前
CTF+随机困难题目
android·开发语言·前端·学习·php
Le_ee5 小时前
SWPUCTF 2025 秋季新生赛wp2
android
pengyu6 小时前
【Kotlin 协程修仙录 · 金丹境 · 初阶】 | 并发艺术:async/await 与并发组合的优雅之道
android·kotlin
沐言人生7 小时前
ReactNative 源码分析3——ReactActivity之初始化RN应用
android·react native
YaBingSec7 小时前
网络安全靶场WP:Grafana 任意文件读取漏洞(CVE-2021-43798)
android·笔记·安全·web安全·ssh·grafana
YF02118 小时前
彻底解决Android非SDK接口绕过限制的深度实践
android·google·app
IVEN_8 小时前
Gradle 依赖下载 403 Forbidden 修复:全局镜像配置实战
android·后端
恋猫de小郭8 小时前
Flutter 3.44 发布前夕,官方宣布 SwiftPM 将完全取代 CocoaPods
android·前端·flutter
黄林晴8 小时前
重磅发布!KMP 双端订阅支付彻底封神,一套代码搞定 iOS+Android
android·kotlin