广告拦截模块开发避坑指南
环境背景
一、Kotlin 协程兼容性问题
现象
编译报错:Suspension functions can be called only within coroutine body
原因
Kotlin 编译器版本较低,对 AlertDialog 按钮回调的 lambda 类型识别不准确。即使在 lifecycleScope.launch 内部,某些嵌套 lambda 也不会被自动识别为挂起上下文。
解决方案
- 用显式接口替代 lambda:
// ❌ 可能报错 .setPositiveButton("确定") { _, _ -> withContext(Dispatchers.IO) { ... } // 挂起函数调用 } // ✅ 稳妥写法 val listener = android.content.DialogInterface.OnClickListener { _, _ -> lifecycleScope.launch { withContext(Dispatchers.IO) { ... } } } .setPositiveButton("确定", listener)
- 用 Handler 替代 withContext(Dispatchers.Main):
在非挂起上下文中需要更新 UI 时,用 Handler(Looper.getMainLooper()).post { … } 代替 withContext(Dispatchers.Main)。 - 将挂起操作封装成普通函数:
// ✅ 普通函数,内部启动协程 private fun doAsyncWork() { lifecycleScope.launch { withContext(Dispatchers.IO) { ... } } }
二、# 号过滤规则导致的 CSS 规则丢失
现象
· CSS 隐藏规则(##.ad-banner)无法通过快速添加、导入、订阅更新写入
· 在规则列表里看不到已添加的 CSS 规则
原因
多处代码使用 !it.startsWith("#") 过滤注释行,但 CSS 规则以 ## 开头,被误判为注释直接丢弃。
解决方案
将所有 !it.startsWith("#") 改为允许 ## 开头的判断:// ❌ 错误 .filter { it.isNotEmpty() && !it.startsWith("#") } // ✅ 正确 .filter { line -> line.isNotEmpty() && (!line.startsWith("#") || line.startsWith("##")) }
需要修改的文件
· AdBlocker.kt — loadRulesFromFile、loadCssRulesFromFile
· AdBlockUpdateWorker.kt — 规则过滤
· AdBlockActivity.kt — showBatchAddBottomSheet、importRulesFromContent
三、动态注入 JS/CSS 影响自身页面
现象
· 首页滑动失效
· 元素探测工具用不了
原因
广告拦截的静态 CSS 和动态 JS 对所有页面生效,包括自己的本地首页(file:///android_asset/…)。
解决方案
在注入前加 URL 判断,只对在线网页生效:if (url != null && (url.startsWith("http://") || url.startsWith("https://"))) { // 注入 CSS/JS }
四、URL 匹配失败导致删除/同步异常
现象
· 取消收藏后状态不刷新
· 云端同步后本地数据不一致
原因
URL 末尾斜杠差异(https://xxx.com vs https://xxx.com/)、编码差异等导致字符串精确匹配失败。
解决方案
所有 URL 比较统一使用标准化处理:fun normalizeUrl(url: String) = url.trimEnd('/').lowercase() // 比较时 list.removeAll { normalizeUrl(it.url) == normalizeUrl(item.url) }
需要修改的文件
· FavoriteManager.kt — add、remove、isFavorite
· FavoriteRepository.kt — remove
· FavoriteDialogHelper.kt — existingFavorite 的查找
五、云端数据截断问题
现象
· 备份后恢复报错:Unterminated array
· 规则数量多时同步失败
原因
- 旧方案用 API 传 JSON,受 PHP/WordPress 表单大小限制
- JSON 中特殊字符未转义导致数据损坏
解决方案
改为逐条存储(每条规则一条数据库记录),避免大 JSON 传输:// 服务端:逐条存储 foreach ($rules as $rule) { add_user_meta($user_id, 'meow_adblock_rule', $rule); }
六、检测失效链接误判
现象
很多能正常访问的网站被判定为失效。
原因
只用 HEAD 请求检测,部分网站拒绝 HEAD 请求,直接判为失效。
解决方案
双重验证:HEAD 失败后再用 GET 请求(只请求 1 字节)确认一次:// 1. HEAD 请求 val headOk = try { ... } catch { false } if (headOk) return false // HEAD 成功,肯定没失效 // 2. GET 请求确认 val getOk = try { request.header("Range", "bytes=0-0").get() ... } catch { false } // 两次都失败才算失效 return !getOk
- 功能隔离:新增拦截功能时,先用 URL 判断限制生效范围,避免影响自身页面
- 编译测试:每改完一个文件就编译一次,不要攒到最后
- 兼容性优先:在 AndroidIDE 环境下,尽量用显式接口、Handler、普通函数,避免依赖 Kotlin 高级语法
- URL 处理:统一使用标准化函数处理 URL 比较,避免大小写、斜杠、编码差异
- 数据存储:大数据量用逐条存储代替大 JSON,避免截断
以后遇到类似问题,对照这篇文章快速定位即可。



Comments NOTHING