惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
The Exploit Database - CXSecurity.com
J
Java Code Geeks
H
Help Net Security
B
Blog RSS Feed
G
Google Developers Blog
博客园 - 司徒正美
MongoDB | Blog
MongoDB | Blog
量子位
博客园 - 三生石上(FineUI控件)
The Cloudflare Blog
P
Proofpoint News Feed
小众软件
小众软件
人人都是产品经理
人人都是产品经理
云风的 BLOG
云风的 BLOG
V
V2EX
月光博客
月光博客
C
Check Point Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
A
Arctic Wolf
Help Net Security
Help Net Security
Schneier on Security
Schneier on Security
D
DataBreaches.Net
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园_首页
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Palo Alto Networks Blog
T
Tenable Blog
L
LangChain Blog
Attack and Defense Labs
Attack and Defense Labs
Google DeepMind News
Google DeepMind News
N
News and Events Feed by Topic
Forbes - Security
Forbes - Security
F
Fortinet All Blogs
Recent Announcements
Recent Announcements
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
大猫的无限游戏
大猫的无限游戏
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Y
Y Combinator Blog
WordPress大学
WordPress大学
Stack Overflow Blog
Stack Overflow Blog
V
Visual Studio Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Engineering at Meta
Engineering at Meta
NISL@THU
NISL@THU
GbyAI
GbyAI
博客园 - Franky
S
Secure Thoughts
有赞技术团队
有赞技术团队
PCI Perspectives
PCI Perspectives
U
Unit 42

暗无天日

读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 读:Immutability 不是万能药,它是一种权衡 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日 读:Anvil——把 Emacs 变成 AI 的工具服务器 读:Emacs 代码折叠终极指南 - 暗无天日 读:Clojure 搭车客指南 - 暗无天日 git推送失败后恢复仓库损坏的完整记录 - 暗无天日 多智能体系统的两个有效模式——以及对 Claude Code 用户的启示 - 暗无天日 用 Org Babel 写 Literate 博文:扩展执行 + 定制导出 proced:Emacs 内置的进程查看器 - 暗无天日 从 proced 定制中学到的 Elisp 模式 读:让 Emacs proced 在 macOS 上显示 CPU 和内存 异步编程的函数着色税 - 暗无天日 链式调用的代价:JavaScript 和 Clojure 的共同教训 - 暗无天日 hyperfine:命令行基准测试工具 - 暗无天日 管道中的变量去哪了?——子 shell 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日 程序员愿意为 AI 写文档,却不愿为同事写 - 暗无天日 mktemp: Shell 脚本中临时文件的安全陷阱与最佳实践 - 暗无天日 WSL9x —— 在 Windows 9x 里跑 Linux 内核 6.19 用 ox.el 做你想做的事 —— org-export 高级编程指南 读:Hot-wiring the Lisp Machine —— 用纯 Elisp 构建零依赖的 Org 静态站点生成器 Elisp 性能优化的六个实战教训 - 暗无天日 fcitx5 下 Emacs 无法切换输入法的排查 - 暗无天日 ERT 测试交互命令的三种方式 - 暗无天日 SEM Assistant: 当 Elisp 守护进程遇上 LLM 用 dmsg 给 Elisp 加上结构化调试日志 用 org-habit 追踪非每日习惯 - 暗无天日 Clojure X-Men:当编程语言特性变成超能力 - 暗无天日 TIL: 用 diff-hl 在 fringe 中显示 git 变更 读:llm-test —— 用 LLM agent 驱动 Emacs 测试 TIL: AI 时代的橡皮鸭调试 - 暗无天日 fcitx 启动后键盘输入卡顿的排查 - 暗无天日 TIL: 早期网页的图片热区导航 - 暗无天日 读 Seeing the Whole System 用 Emacs 自动生成每周链接推荐 - 暗无天日 读:ASCII control characters in my terminal 读 What to learn - 暗无天日 Lisp 的括号之痛——一个愚人节玩笑揭开的老伤疤 - 暗无天日 一本书该"线性读"还是"并行读" - 暗无天日 读 How to Monetize a Blog:一篇伪装成变现指南的讽刺文 Python Mock 第三方依赖的四种策略 - 暗无天日 Emacs Lisp 热重载实用指南 - 暗无天日 Prot 的 Emacs 配置哲学 - 暗无天日 TIL: 从直播对谈中学到的三个 Emacs 技巧 - 暗无天日 TIL: 自动使用项目虚拟环境的 Python - 暗无天日 TIL: 让 Help buffer 自动获得焦点 一条命令让本地开发用上 HTTPS —— slim 工具介绍 用 fsck 检查和修复 Linux 文件系统 排查Linux进程"卡死"实战:从strace到gdb全流程 - 暗无天日 PostgreSQL 索引:从基础到你可能不知道的高级用法 - 暗无天日 用 .pdbrc 自定义 Python 调试器 ANSI 转义码的标准化现状 - 暗无天日 终端程序的潜规则 - 暗无天日 PARA Org-mode 测试配置 - 暗无天日 AI越强越辣鸡?控制论说这是必然的 - 暗无天日 AI 越强越需要你盯着——反馈循环实操指南 - 暗无天日 你的AI代理正在偷你的密钥——四种你没想到的泄露通道 - 暗无天日 LLM 在 DevOps 中的三种角色 - 暗无天日 写作风格的反建议 - 暗无天日 反驳本质复杂性——Dan Luu 论为什么《没有银弹》错了 - 暗无天日 文件充满了危险——Dan Luu 谈文件系统的可靠性陷阱 - 暗无天日 AI 时代的 PARA 方法:用 Org-mode 和 AI 打造个人知识管理系统 Linux 数据去重学习笔记 - 暗无天日 创建跨平台 ZIP 文件的隐藏陷阱:Extra Field - 暗无天日 X11 Forwarding 排障指南 - 暗无天日 IP欺骗端口扫描:当别人冒充你去扫描别人 - 暗无天日 Linux 输入栈全景解析:从硬件按键到屏幕响应 - 暗无天日 Unix 系统中那些被埋没的配置开关——以 FontConfig 为例 - 暗无天日 在Linux上限制儿童使用电脑 - 暗无天日 GIF不仅仅是一种图片格式——用GIF流做些奇怪的事 - 暗无天日 Leiningen 学习笔记:Clojure 项目构建与管理从入门到实战配置 - 暗无天日 Google SRE Book 读书笔记 - 暗无天日 yes 管道 head 发生了什么 - 暗无天日 为什么 nohup 在 crontab 中不起作用 Bash中的Indirection与Nameref - 暗无天日 Linux PAM 简介 - 暗无天日 从Linux ISO文件启动计算机 - 暗无天日 用 Bash 打造一个Screen Locker 用GitHub Actions自动构建EGO博客 - 暗无天日 blocking I/O 的作用 - 暗无天日 mobileog 手机端同步提示Error:2 No such file 的解决方法 回收 WSL2 VHDX 文件占用空间 使用 org-mode columnview 生成任务列表 - 暗无天日 Emacs 作为 MPD 客户端 - 暗无天日 移动文件路径却不破坏org file link的方法 - 暗无天日 如何合理的导出help link 成HTML - 暗无天日 笑话理解之Biology - 暗无天日
MobileOrg Android:从 API 17 迁移到 API 34 的实战记录
2026-05-10 · via 暗无天日

本文迁移全程借助 Claude Code 完成,我本人没有手动编写任何代码。整个过程是:真机测试发现问题 → 将 logcat 或现象描述给 Claude Code → Claude Code 定位原因并生成修复代码 → 推送 GitHub Actions 构建 → 再测试验证。包括本文本身,也是借助 Claude Code 生成的。

由于我不是 Android 开发者,对文中涉及的技术细节无法保证完全准确,请读者自行判断。

MobileOrg Android 是一个 Org-mode 的 Android 客户端,fork 自已经停止维护的 matburt/mobileorg-android。原始代码写于 2013 年左右,targetSdk 为 17(Android 4.2),使用 ActionBarSherlock、Support Library v4 等早已废弃的库。

Google Play 要求 targetSdk 33+,所以必须迁移。本文记录了整个迁移过程中的踩坑和修复经验。

迁移时间线

整个迁移分三个阶段,共 50+ 个 commit:

第一阶段:构建系统升级

原始项目使用 ADT 构建,没有 Gradle 支持。首先需要让项目能编译:

  1. 创建 Gradle 构建配置(AGP 3.0.1 + Gradle 4.1)
  2. 解决 ActionBarSherlock 兼容性(禁用 AAPT2)
  3. 解决 support-v4 版本冲突(降级到 25.4.0)

第二阶段:AndroidX 迁移

  1. 替换 ActionBarSherlock 为 AppCompat
  2. 执行 AndroidX 迁移(Refactor → Migrate to AndroidX)
  3. 升级到现代工具链(AGP 8.2.2 + Gradle 8.5 + JDK 17 + compileSdk 34)

第三阶段:运行时兼容性修复

这是最耗时的阶段。代码能编译,但在现代 Android 上各种崩溃和功能异常。下面详细记录。

踩过的坑

坑 1:ActionBarSherlock → AppCompat 的菜单图标消失

症状:菜单图标在 ActionBar 上不显示

AppCompat 使用自己的 namespace 解析 showAsAction 属性。如果用 =android:showAsAction=,AppCompat 会静默忽略,图标不显示但不报错。

<item android:showAsAction="ifRoom" />


<item app:showAsAction="ifRoom"
    xmlns:app="http://schemas.android.com/apk/res-auto" />

**影响范围**:项目中 8 个菜单 XML 文件全部需要修改。只修了一个文件时,其他界面(capture 保存按钮、节点操作按钮等)的图标依然消失。

教训:迁移框架后要全局搜索同类问题,不要只修一个文件就以为搞定了。

坑 2:NotificationChannel 导致前台服务崩溃

症状:CannotPostForegroundServiceNotificationException: Bad notification for startForeground

Android 8.0(API 26)引入了 NotificationChannel。Android 13(API 33)进一步强化——如果 notification 引用的 channel 不存在,直接抛异常崩溃。

原始代码通过反射创建 channel(因为当时 compileSdk 是 23),但反射异常被静默吞掉了。Channel 没创建成功,notification 引用了一个不存在的 channel,崩溃。

try {
    Class<?> channelClass = Class.forName("android.app.NotificationChannel");
    } catch (Exception e) {
    }

NotificationChannel channel = new NotificationChannel(channelId, name,
    NotificationManager.IMPORTANCE_LOW);
nm.createNotificationChannel(channel);

同时,所有 NotificationCompat.Builder 构造函数都必须传 CHANNEL_ID:

new NotificationCompat.Builder(context)

new NotificationCompat.Builder(context, CHANNEL_ID)

**影响范围**:6 处 Builder 调用点,涉及 SyncService、TimeclockService、SynchronizerNotification、SynchronizerNotificationCompat。

坑 3:危险权限的运行时检查

症状:CalendarSyncService 启动时 SecurityException 崩溃

Android 6.0(API 23)引入了运行时权限。=READ_CALENDAR= 和 WRITE_CALENDAR 是危险权限,光在 Manifest 声明不够,必须运行时请求。

if (!hasCalendarPermission()) {
    stopSelf();      return;
}

**影响范围**:CalendarSyncService 的 onCreate()onStartCommand() 都需要加权限检查。

坑 4:Service early return 导致 onDestroy NPE

症状:修复权限检查后,CalendarSyncService.onDestroy() 抛 NullPointerException

修复坑 3 加了 early return 后,=sharedPreferences= 没有被初始化,但 Android 仍然会调用 onDestroy()=,里面直接调了 =unregisterOnSharedPreferenceChangeListener(sharedPreferences...) 导致 NPE。

@Override
public void onDestroy() {
    if (sharedPreferences != null) {
        sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
    }
    super.onDestroy();
}

教训:在 onCreate() / onStartCommand() 中加 early return(比如权限检查失败后 stopSelf()=)时,=onDestroy() 仍会被 Android 调用。所有在 onDestroy() 中使用的字段都必须做空检查。

坑 5:主线程网络操作

症状:SSH Connection failed: android.os.NetworkOnMainThreadException

Android 3.0 开始禁止主线程网络操作。但 SyncService.getSynchronizer() 在主线程调用 new SSHSynchronizer()=,而 SSHSynchronizer 的构造函数调了 =connect() 做网络连接。

public SSHSynchronizer(Context context) {
        this.connect();  }

public SSHSynchronizer(Context context) {
    }

后台线程的 getRemoteFile() 已经有重连逻辑,会自动在正确的线程上建立连接。

坑 6:隐式广播不送达

症状:同步后变更计数不刷新,同步动画不显示,但同步本身是成功的

这是最隐蔽的一个问题。=OrgUtils.announceSyncDone()= 通过 sendBroadcast() 发送隐式广播通知 UI 刷新。但 targetSdk 34 的设备上,隐式广播可能不被 RECEIVER_NOT_EXPORTED 的 receiver 接收。

Intent intent = new Intent(Synchronizer.SYNC_UPDATE);
context.sendBroadcast(intent);

Intent intent = new Intent(Synchronizer.SYNC_UPDATE);
intent.setPackage(context.getPackageName());  context.sendBroadcast(intent);

这个问题的影响面很大:变更计数不刷新、同步动画不显示、编辑后列表不更新——全部因为广播没送达。但不会报任何错误,就是功能静默失效。

教训:targetSdk 升级后,最危险的不是崩溃,而是功能静默失效。崩溃至少有日志,静默失效只能靠用户反馈。

坑 7:Preference 的隐式 Intent 解析错误

症状:点击 Setup Wizard 直接退回到大纲界面

XML Preference 中用隐式 intent 打开 Activity:

<Preference android:title="Setup Wizard">
    <intent android:action="com.matburt.mobileorg.Settings.SETUP_WIZARD" />
</Preference>


<Preference android:title="Setup Wizard">
    <intent
        android:targetPackage="com.matburt.mobileorg"
        android:targetClass="com.matburt.mobileorg.Gui.Wizard.WizardActivity" />
</Preference>

坑 8:动画在未挂载的 View 上启动

症状:同步图标的旋转动画不显示

startAnimation() 再 =setActionView()=,此时 view 还没挂载到 window,动画被静默忽略。

refreshView.startAnimation(rotate);  synchronizerMenuItem.setActionView(refreshView);

synchronizerMenuItem.setActionView(refreshView);
refreshView.post(() -> refreshView.startAnimation(rotate));

坑 9:Intent.getAction() 返回 null 导致 NPE

症状:Setup Wizard 完成后,OutlineActivity 崩溃 NullPointerException

之前修改 Wizard 完成后的导航逻辑,使用 FLAG_ACTIVITY_SINGLE_TOP 回到已有的 OutlineActivity:

Intent outlineIntent = new Intent(context, OutlineActivity.class);
outlineIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
context.startActivity(outlineIntent);

由于 FLAG_ACTIVITY_SINGLE_TOP=,Android 不会创建新实例,而是调用已有实例的 =onNewIntent()=。但这个 Intent 没有设 action,所以 =getAction() 返回 null:

if (intent.getAction().equals(SYNC_FAILED)) { ... }

if (SYNC_FAILED.equals(intent.getAction())) { ... }

教训:=Intent.getAction()= 可以为 null。使用 FLAG_ACTIVITY_SINGLE_TOP 导航回已有 Activity 时,=onNewIntent()= 收到的 Intent 可能没有 action。养成 CONSTANT.equals(variable) 的习惯可以避免此类 NPE。

经验总结

1. 静默失效比崩溃更可怕

Android 兼容性的最大敌人不是 FATAL EXCEPTION=,而是功能静默消失。=android:showAsAction 被忽略、隐式广播不送达、动画不播放——这些都不会出现在 logcat 里,只能靠人工测试发现。

2. 全局搜索同类问题

修了一个文件的 showAsAction 后,必须全局搜索所有同类问题。Android 迁移中的问题往往是同一模式在多处重复。我们用 grep -r "android:showAsAction" 一次性找到了 7 个文件。

3. 生命周期配对调用

onCreate() 中加 early return 时,必须检查 onDestroy() 是否使用了会被跳过的字段。Android 保证 onDestroy()stopSelf() 后仍被调用。

4. 编译通过≠运行正确

compileSdk 升级后,很多废弃 API 仍然编译通过但运行时行为改变。需要逐个在目标设备上测试。

5. 用 CI 构建而非本地构建

全程通过 GitHub Actions 构建 APK,在真机上测试。本地环境和 CI 环境的差异可能导致遗漏问题。