2. 缓存初始化常见 ANR 场景分析

缓存初始化阶段是 Android 应用中触发 ANR 的高发区域。当主线程(UI 线程)在应用启动或组件初始化时,被迫等待耗时的缓存加载操作完成,系统便会弹出“应用无响应”对话框。本章深入分析三种最常见的 ANR 诱因:同步加载大文件、网络请求阻塞、数据库查询超时,并揭示其在主线程中的执行痕迹。

2.1 场景一:同步加载大文件

应用启动时,若直接在 onCreate() 或 Application.onCreate() 中调用 BitmapFactory.decodeFile()、FileInputStream.read() 或 ObjectInputStream 加载大型缓存文件到内存,主线程会被 I/O 操作长时间挂起。一个 10MB 的图片文件在低端设备上解码可能超过 800ms,直接触发 ANR 阈值(通常为 5s)。

问题代码(反例):
// 主线程中同步加载大缓存文件,极易导致ANR
public class MainApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 危险:在主线程执行同步文件I/O
        File cacheFile = new File(getCacheDir(), "big_cache.dat");
        byte[] data = new byte[(int) cacheFile.length()];
        FileInputStream fis = new FileInputStream(cacheFile);
        fis.read(data);  // 可能阻塞数秒
        fis.close();
        CacheManager.init(data);
    }
}

2.2 场景二:网络请求阻塞

部分旧版 SDK 或未优化的代码会在主线程发起 HttpURLConnection 或 OkHttp 同步调用以获取远程缓存配置。当网络状态不佳(弱网、无网或 DNS 解析超时)时,请求可能等待数秒甚至抛出超时异常。主线程在此期间无法处理任何触摸事件或绘制请求。

典型示例: 在主线程调用 URL.openConnection().getInputStream() 获取远程缓存元数据。Android 平台在 API 9 之后已明确禁止在主线程进行网络请求,但仍有部分遗留代码通过反射或隐式线程绕过限制,造成 ANR。
// 在主线程同步获取远程缓存配置(禁止模式)
URL url = new URL("https://example.com/cache/config.json");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(10000); // 即使设置了超时,主线程依然被阻塞10秒
InputStream in = conn.getInputStream();
// 解析JSON并初始化缓存
parseAndInitCache(in);

2.3 场景三:数据库查询超时

缓存数据存储在 SQLite 或 Room 数据库中时,若初始化期间在主线程执行全表扫描、复杂联表查询或大批量插入,会导致 ANR。特别是当数据库文件被其他进程占用、索引缺失或数据量超过 10 万行时,单次查询可能超过 2 秒。

实际案例: 某社交应用在 ContentProvider.onCreate() 中直接调用 db.query() 获取好友列表缓存,由于表中有 50 万行数据且未建索引,查询耗时 3.2 秒,叠加其他初始化时间,总耗时 5.8 秒,最终触发 ANR。

2.4 主线程 ANR 痕迹特征对比

以下表格总结三种场景在 ANR 日志 /data/anr/traces.txt 中的典型特征,帮助开发者快速定位问题根源:

场景类型主线程调用栈特征关键方法ANR 日志常见标记平均阻塞时间
同步加载大文件java.io 包下的 read / decode 方法FileInputStream.read(), BitmapFactory.decodeStream()“waiting on file I/O”1s – 5s
网络请求阻塞libcore 或 okhttp 的 Socket 读写SocketInputStream.read(), HttpURLConnection.getInputStream()“blocked on network”5s – 30s
数据库查询超时android.database.sqlite 下的 query / insertSQLiteQuery.fillWindow(), Cursor.getCount()“waiting for SQLite lock”500ms – 4s

2.5 场景根源 SVG 架构图

下面的 SVG 流程图展示了上述三种 ANR 场景在主线程中的阻塞链路,以及系统 Watchdog 检测的触发点。

缓存初始化同步阻塞导致 ANR 的三种链路 Android 主线程 同步加载大文件 FileInputStream read() / decode() 网络请求阻塞 HttpURLConnection getInputStream() 数据库查询超时 SQLite / Room query() / insert() 主线程输入事件队列堵塞 > 5 秒 系统 Watchdog 触发 ANR 解决方案:使用异步任务(Thread / AsyncTask / Kotlin Coroutine)将缓存初始化移出主线程

2.6 如何确认是否属于此类 ANR

  • 查看 /data/anr/traces.txt: 搜索主线程 (main) 的调用栈,若出现 FileInputStream.read、SQLiteConnection.executeQuery 或 OkHttp 相关方法,即可判定为缓存初始化阻塞。
  • 使用 StrictMode: 在开发版本中开启 StrictMode.setThreadPolicy(),系统会在主线程检测磁盘读、网络访问时立即抛警告(不触发 ANR,但可暴露问题)。
  • TraceView / Perfetto: 抓取启动阶段 CPU 和线程状态,观察主线程是否长时间处于 Runnable 状态且调用栈指向 I/O 或网络操作。
// 开启 StrictMode 在主线程检测 IO 操作(仅在调试环境使用)
if (BuildConfig.DEBUG) {
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
            .detectDiskReads()
            .detectDiskWrites()
            .detectNetwork()
            .penaltyLog()
            .build());
}

2.7 性能优化提示

核心原则: 任何可能超过 16ms 的操作都不应出现在主线程。缓存初始化应使用 Executors.newSingleThreadExecutor()、HandlerThread 或 kotlinx.coroutines.Dispatchers.IO 异步执行,并通过回调 / LiveData / Flow 向 UI 层同步状态。