[{"content":"背景 这个博客从 2013 年底开始，使用 Jekyll 托管在 GitHub Pages 上，经历了几次主题变更。最近一次使用的是 jekyll-theme-chirpy 7.1.1。\n积累了 55 篇文章后，决定重新审视技术栈选型，期望找到一个简单、优雅、有技术范的方案。\n2026 年静态博客生态 在选型之前，整理了当前主流的静态站点生成器（SSG）对比：\nSSG 语言 构建速度 特点 Jekyll Ruby 中等 GitHub Pages 原生支持，生态最成熟 Hugo Go 极快 (\u0026lt;1ms/页) 单二进制，零依赖 Astro JS/TS 快 默认零 JS 输出，Island Architecture 11ty JS 快 灵活，多模板引擎 Zola Rust 极快 单二进制，内置 Sass/语法高亮 为什么选 Hugo + PaperMod 经过评估，最终选择 Hugo + PaperMod：\n构建速度 — 54 篇文章构建时间 \u0026lt;100ms，Jekyll 需要数秒 PaperMod 主题 — 极简优雅，专注内容，暗色模式完美 零 Ruby 依赖 — hugo 单个二进制文件，装完即用 多语言原生支持 — Hugo 内置 i18n 机制，对中英双语友好 活跃维护 — PaperMod 社区活跃，2026 年仍在持续更新 迁移过程 1. 项目结构变化 1 2 3 4 5 6 7 8 # Jekyll 结构 # Hugo 结构 ├── _config.yml ├── hugo.yaml ├── _posts/ ├── content/posts/ ├── _data/ ├── static/ ├── _plugins/ ├── themes/PaperMod/ ├── _tabs/ ├── archetypes/ ├── Gemfile └── layouts/ └── assets/ 2. Front Matter 标准化 Jekyll 时期 front matter 格式不统一，迁移时统一为：\n1 2 3 4 5 6 7 8 9 10 --- title: \u0026#34;文章标题\u0026#34; date: 2026-06-26 description: \u0026#34;简短描述\u0026#34; author: \u0026#34;haoxiqiang\u0026#34; tags: [\u0026#34;tag1\u0026#34;, \u0026#34;tag2\u0026#34;] categories: [\u0026#34;Category\u0026#34;] ShowToc: true TocOpen: false --- 3. 多语言配置 Hugo 的多语言支持通过文件名后缀区分，非常直观：\n1 2 3 content/posts/ ├── my-post.md # 中文（默认语言） └── my-post.en.md # English 在 hugo.yaml 中设置 defaultContentLanguage: zh，英文版本通过 /en/ 前缀访问。\n4. GitHub Actions 部署 利用 peaceiris/actions-hugo 和 GitHub 官方的 Pages 部署 action：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 name: Deploy Hugo site to Pages on: push: branches: [\u0026#34;master\u0026#34;] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: true fetch-depth: 0 - name: Setup Hugo uses: peaceiris/actions-hugo@v3 with: hugo-version: \u0026#39;latest\u0026#39; extended: true - name: Build run: hugo --minify - uses: actions/upload-pages-artifact@v3 with: path: ./public deploy: needs: build runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - uses: actions/deploy-pages@v4 PaperMod 特性 选择 PaperMod 后获得的开箱即用功能：\n深色/浅色模式自动切换 站内搜索（基于 Fuse.js） 代码块一键复制 阅读时间估计 文章目录（TOC） 面包屑导航 RSS 订阅 SEO 优化（Open Graph、Twitter Cards） 响应式设计 归档页面 多语言支持 GitHub Pages 2026 最佳实践 总结一些当前 GitHub Pages 的使用建议：\n部署方式 推荐使用 GitHub Actions，不再使用旧的分支部署模式。Actions 方式的优势：\n不限于 Jekyll，可使用任何 SSG 构建环境完全可控 始终使用最新版本的工具 性能优化 启用 minify 压缩输出 利用 CDN（GitHub Pages 自带 Fastly CDN） 图片使用 WebP 格式 启用 PWA 缓存支持离线访问 自定义域名 仓库根目录放置 CNAME 文件 DNS 配置 CNAME 指向 \u0026lt;username\u0026gt;.github.io GitHub 自动签发 HTTPS 证书 内容管理 Markdown 写作，git push 即部署 使用 archetypes 模板快速创建新文章 利用 draft: true 管理草稿 迁移总结 维度 之前（Jekyll） 之后（Hugo） 构建时间 ~5s \u0026lt;100ms 依赖安装 Ruby + Bundler 单个 binary 主题 Chirpy 7.1.1 PaperMod 多语言 不支持 原生支持 搜索 无 Fuse.js 全文搜索 文章数 55 54（去除 1 个重复） 迁移工作量约半天，主要花在统一 front matter 格式上。结果很满意——构建速度从秒级变成毫秒级，主题简洁优雅，多语言也准备就绪。\n参考资源 Hugo 官方文档 Hugo 多语言配置 PaperMod 主题 GitHub 仓库 PaperMod 主题文档 Hugo GitHub Pages 部署指南 peaceiris/actions-hugo ","permalink":"https://blog.substitute.tech/posts/blog-migration-to-hugo/","summary":"\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003e这个博客从 2013 年底开始，使用 Jekyll 托管在 GitHub Pages 上，经历了几次主题变更。最近一次使用的是 \u003ca href=\"https://github.com/cotes2020/jekyll-theme-chirpy\"\u003ejekyll-theme-chirpy\u003c/a\u003e 7.1.1。\u003c/p\u003e\n\u003cp\u003e积累了 55 篇文章后，决定重新审视技术栈选型，期望找到一个\u003cstrong\u003e简单、优雅、有技术范\u003c/strong\u003e的方案。\u003c/p\u003e\n\u003ch2 id=\"2026-年静态博客生态\"\u003e2026 年静态博客生态\u003c/h2\u003e\n\u003cp\u003e在选型之前，整理了当前主流的静态站点生成器（SSG）对比：\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eSSG\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e语言\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e构建速度\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e特点\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eJekyll\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eRuby\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e中等\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGitHub Pages 原生支持，生态最成熟\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eHugo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e极快 (\u0026lt;1ms/页)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e单二进制，零依赖\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eAstro\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eJS/TS\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e快\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e默认零 JS 输出，Island Architecture\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e11ty\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eJS\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e快\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e灵活，多模板引擎\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eZola\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eRust\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e极快\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e单二进制，内置 Sass/语法高亮\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"为什么选-hugo--papermod\"\u003e为什么选 Hugo + PaperMod\u003c/h3\u003e\n\u003cp\u003e经过评估，最终选择 Hugo + PaperMod：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e构建速度\u003c/strong\u003e — 54 篇文章构建时间 \u0026lt;100ms，Jekyll 需要数秒\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePaperMod 主题\u003c/strong\u003e — 极简优雅，专注内容，暗色模式完美\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e零 Ruby 依赖\u003c/strong\u003e — \u003ccode\u003ehugo\u003c/code\u003e 单个二进制文件，装完即用\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e多语言原生支持\u003c/strong\u003e — Hugo 内置 i18n 机制，对中英双语友好\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e活跃维护\u003c/strong\u003e — PaperMod 社区活跃，2026 年仍在持续更新\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"迁移过程\"\u003e迁移过程\u003c/h2\u003e\n\u003ch3 id=\"1-项目结构变化\"\u003e1. 项目结构变化\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e8\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e# Jekyll 结构                    # Hugo 结构\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── _config.yml                  ├── hugo.yaml\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── _posts/                      ├── content/posts/\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── _data/                       ├── static/\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── _plugins/                    ├── themes/PaperMod/\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── _tabs/                       ├── archetypes/\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e├── Gemfile                      └── layouts/\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e└── assets/\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"2-front-matter-标准化\"\u003e2. Front Matter 标准化\u003c/h3\u003e\n\u003cp\u003eJekyll 时期 front matter 格式不统一，迁移时统一为：\u003c/p\u003e","title":"博客迁移：从 Jekyll Chirpy 到 Hugo PaperMod"},{"content":"AOSP 的构建流程已经比较清晰，大致分为：同步代码、添加对应设备的驱动和内核、构建目标镜像。之前尝试构建 AOSP 来排查一些问题，但 Pixel 3 XL 的官方适配只到 Android 12。最近在 Chromium 开发中需要测试 WebView，因此使用 LineageOS 21 的适配来方便构建。\n前提条件 默认已安装 AOSP 构建环境：\nAOSP 环境准备 Codenames, Tags, and Build Numbers 同步 AOSP 源码 1 2 3 4 mkdir ~/aosp cd ~/aosp repo init --partial-clone -b android-12.0.0_r34 -u https://android.googlesource.com/platform/manifest repo sync -c -j8 获取驱动 在 Codenames, Tags, and Build Numbers 页面搜索 Pixel 3 XL（代号 crosshatch），获取最新 Build ID，示例：SP1A.210812.016.C2 在 Google Drivers 页面下载对应 Build ID 的驱动 1 2 3 4 5 6 7 8 9 mkdir vendor_backup \u0026amp;\u0026amp; cd vendor_backup wget https://dl.google.com/dl/android/aosp/google_devices-crosshatch-sp1a.210812.016.c2-a4e274b7.tgz wget https://dl.google.com/dl/android/aosp/qcom-crosshatch-sp1a.210812.016.c2-00a7f1f3.tgz tar xvf qcom-crosshatch-*.tgz tar xvf google_devices-crosshatch-*.tgz ./extract-google_devices-crosshatch.sh ./extract-qcom-crosshatch.sh mv vendor/ ../ 构建并刷机 参考 Building AOSP 文档：\n1 2 3 4 5 6 7 8 9 10 11 12 13 cd ~/aosp source build/envsetup.sh # 选择 Pixel 3 XL (crosshatch) 构建目标 # user/userdebug/eng 对应不同的构建类型 lunch aosp_crosshatch-userdebug # 开始构建 m # 刷机 export ANDROID_PRODUCT_OUT=\u0026#39;/home/haoxiqiang/workspace/aosp/out/target/product/crosshatch\u0026#39; adb reboot fastboot fastboot flashall -w user、userdebug、eng 三种构建类型的区别参见 官方文档。\n使用 LineageOS 构建 对于需要更高版本 Android 的场景，可以使用 LineageOS 的适配：\n1 2 repo init --partial-clone -u https://github.com/LineageOS/android.git -b lineage-21.0 --git-lfs repo sync -c -j8 具体步骤参见 LineageOS Build Wiki。\n参考资源 AOSP 环境准备 AOSP 构建指南 Build Numbers Google Drivers for Pixel AOSP 源码下载 LineageOS Build Wiki — crosshatch Building Pixel Kernels ","permalink":"https://blog.substitute.tech/posts/aosp-build-for-pixel3/","summary":"\u003cp\u003eAOSP 的构建流程已经比较清晰，大致分为：同步代码、添加对应设备的驱动和内核、构建目标镜像。之前尝试构建 AOSP 来排查一些问题，但 Pixel 3 XL 的官方适配只到 Android 12。最近在 Chromium 开发中需要测试 WebView，因此使用 LineageOS 21 的适配来方便构建。\u003c/p\u003e\n\u003ch2 id=\"前提条件\"\u003e前提条件\u003c/h2\u003e\n\u003cp\u003e默认已安装 AOSP 构建环境：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://source.android.com/docs/setup\"\u003eAOSP 环境准备\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://source.android.com/docs/setup/reference/build-numbers#source-code-tags-and-builds\"\u003eCodenames, Tags, and Build Numbers\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"同步-aosp-源码\"\u003e同步 AOSP 源码\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emkdir ~/aosp\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ecd\u003c/span\u003e ~/aosp\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003erepo init --partial-clone -b android-12.0.0_r34 -u https://android.googlesource.com/platform/manifest\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003erepo sync -c -j8\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"获取驱动\"\u003e获取驱动\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e在 \u003ca href=\"https://source.android.com/docs/setup/reference/build-numbers#source-code-tags-and-builds\"\u003eCodenames, Tags, and Build Numbers\u003c/a\u003e 页面搜索 Pixel 3 XL（代号 \u003ccode\u003ecrosshatch\u003c/code\u003e），获取最新 Build ID，示例：\u003ccode\u003eSP1A.210812.016.C2\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e在 \u003ca href=\"https://developers.google.com/android/drivers\"\u003eGoogle Drivers\u003c/a\u003e 页面下载对应 Build ID 的驱动\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e9\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emkdir vendor_backup \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nb\"\u003ecd\u003c/span\u003e vendor_backup\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ewget https://dl.google.com/dl/android/aosp/google_devices-crosshatch-sp1a.210812.016.c2-a4e274b7.tgz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ewget https://dl.google.com/dl/android/aosp/qcom-crosshatch-sp1a.210812.016.c2-00a7f1f3.tgz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etar xvf qcom-crosshatch-*.tgz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etar xvf google_devices-crosshatch-*.tgz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./extract-google_devices-crosshatch.sh\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./extract-qcom-crosshatch.sh\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emv vendor/ ../\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"构建并刷机\"\u003e构建并刷机\u003c/h2\u003e\n\u003cp\u003e参考 \u003ca href=\"https://source.android.com/docs/setup/build/building\"\u003eBuilding AOSP\u003c/a\u003e 文档：\u003c/p\u003e","title":"Build AOSP for Pixel 3 XL"},{"content":"原本对 RSA 不同加密方式的区别和使用场景还是很清楚的。今天在实现一个 RSA 加密功能时，发现公钥加密结果每次都不一样，这个现象触发了我的盲区。查阅相关资料后，记录一下背后的原理。\n问题现象 以下代码使用 OpenSSL 的 EVP API 进行 RSA 加密，没有显式传入随机值，但每次运行结果都不相同：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 unsigned char *encode_by_rsa(const char *public_key, unsigned const char *input) { int key_len = (int) strlen(public_key); BIO *bio = BIO_new_mem_buf(public_key, key_len); EVP_PKEY *pKey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL); BIO_free_all(bio); EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(pKey, NULL); EVP_PKEY_encrypt_init(ctx); EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING); EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()); EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256()); size_t rsa_len = (int) RSA_LENGTH; unsigned char *encrypted_data = malloc(rsa_len + 1); memset(encrypted_data, 0, rsa_len + 1); if (encrypted_data == NULL) { return NULL; } int input_len = (int) strlen((const char *) input); EVP_PKEY_encrypt(ctx, encrypted_data, \u0026amp;rsa_len, input, input_len); encrypted_data[rsa_len] = \u0026#39;\\0\u0026#39;; EVP_PKEY_CTX_free(ctx); EVP_PKEY_free(pKey); return encrypted_data; } 原因：随机填充 不管是 RSA 私钥签名还是公钥加密，操作中都需要对待处理数据先进行填充，再对填充后的数据进行加密运算。 填充过程中引入了伪随机数，所以即使相同的输入和密钥，每次输出都不同。\n填充方式详解 标准 RSA 操作中，数据长度必须小于密钥长度。在实际应用（如代码中使用的 OAEP 填充）中，填充有以下作用：\n填充方式 规范 特点 PKCS#1 v1.5 RFC 2313 传统填充，历史最久，但存在 Bleichenbacher 填充预言攻击 OAEP PKCS#1 v2.0 (RFC 2437) / v2.1 (RFC 3447) 最优非对称加密填充，推荐用于新应用，引入随机性 PSS PKCS#1 v2.1 用于签名，也是概率性的 OAEP（Optimal Asymmetric Encryption Padding）通过在数据前添加随机掩码，确保了加密结果每次不同。这正是代码中 RSA_PKCS1_OAEP_PADDING 的表现。\nPython Sample 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import base64 from Crypto import Random from Crypto.Cipher import PKCS1_v1_5 from Crypto.PublicKey import RSA random_generator = Random.new().read private_key = RSA.generate(2048) public_key = private_key.publickey() def rsa_encrypt(data): key = RSA.importKey(public_key.exportKey()) cipher = PKCS1_v1_5.new(key) cipher_text = base64.b64encode(cipher.encrypt(data)) return cipher_text def rsa_decode(encrypt_text): key = RSA.importKey(private_key.exportKey()) cipher = PKCS1_v1_5.new(key) text = cipher.decrypt(base64.b64decode(encrypt_text), random_generator) return text def main(): data = \u0026#34;Hello, world!\u0026#34; encrypt_text = rsa_encrypt(data.encode()) print(\u0026#34;encrypt_text\u0026#34;, encrypt_text) decrypt_text = rsa_decode(encrypt_text) print(\u0026#34;decrypt_text\u0026#34;, decrypt_text.decode()) if __name__ == \u0026#34;__main__\u0026#34;: main() 参考资源 RFC 2313 — PKCS #1: RSA Encryption Version 1.5 RFC 2437 — PKCS #1: RSA Cryptography Specifications Version 2.0 RFC 3447 — PKCS #1: RSA Cryptography Specifications Version 2.1 OpenSSL 官方文档 — RSA_public_encrypt OpenSSL 3.x EVP RSA 文档 Wikipedia — Optimal Asymmetric Encryption Padding ","permalink":"https://blog.substitute.tech/posts/openssl-rsa/","summary":"\u003cp\u003e原本对 RSA 不同加密方式的区别和使用场景还是很清楚的。今天在实现一个 RSA 加密功能时，发现公钥加密结果每次都不一样，这个现象触发了我的盲区。查阅相关资料后，记录一下背后的原理。\u003c/p\u003e\n\u003ch2 id=\"问题现象\"\u003e问题现象\u003c/h2\u003e\n\u003cp\u003e以下代码使用 OpenSSL 的 EVP API 进行 RSA 加密，没有显式传入随机值，但每次运行结果都不相同：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003eunsigned\u003c/span\u003e \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nf\"\u003eencode_by_rsa\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003econst\u003c/span\u003e \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003epublic_key\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eunsigned\u003c/span\u003e \u003cspan class=\"k\"\u003econst\u003c/span\u003e \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003einput\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ekey_len\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003estrlen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epublic_key\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eBIO\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003ebio\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eBIO_new_mem_buf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epublic_key\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ekey_len\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eEVP_PKEY\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003epKey\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003ePEM_read_bio_PUBKEY\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebio\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nb\"\u003eNULL\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nb\"\u003eNULL\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nb\"\u003eNULL\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eBIO_free_all\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebio\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eEVP_PKEY_CTX\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003ectx\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003eEVP_PKEY_CTX_new\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epKey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nb\"\u003eNULL\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eEVP_PKEY_encrypt_init\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ectx\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eEVP_PKEY_CTX_set_rsa_padding\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ectx\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eRSA_PKCS1_OAEP_PADDING\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eEVP_PKEY_CTX_set_rsa_oaep_md\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ectx\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nf\"\u003eEVP_sha256\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eEVP_PKEY_CTX_set_rsa_mgf1_md\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ectx\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nf\"\u003eEVP_sha256\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003esize_t\u003c/span\u003e \u003cspan class=\"n\"\u003ersa_len\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003eRSA_LENGTH\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eunsigned\u003c/span\u003e \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003eencrypted_data\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nf\"\u003emalloc\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ersa_len\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003ememset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eencrypted_data\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ersa_len\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eencrypted_data\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"nb\"\u003eNULL\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nb\"\u003eNULL\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003einput_len\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"nf\"\u003estrlen\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"k\"\u003econst\u003c/span\u003e \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003einput\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eEVP_PKEY_encrypt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ectx\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eencrypted_data\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u003c/span\u003e\u003cspan class=\"n\"\u003ersa_len\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003einput\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003einput_len\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eencrypted_data\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ersa_len\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sc\"\u003e\u0026#39;\\0\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eEVP_PKEY_CTX_free\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ectx\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nf\"\u003eEVP_PKEY_free\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epKey\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eencrypted_data\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"原因随机填充\"\u003e原因：随机填充\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e不管是 RSA 私钥签名还是公钥加密，操作中都需要对待处理数据先进行填充，再对填充后的数据进行加密运算。\u003c/strong\u003e 填充过程中引入了伪随机数，所以即使相同的输入和密钥，每次输出都不同。\u003c/p\u003e","title":"OpenSSL RSA 加密的一个认知盲区"},{"content":"最近在处理一个海外应用，打包机原本在上海。由于一些特殊原因需要迁移到海外，顺便记录 Jenkins 的迁移和配置过程。\n前置条件：安装 JDK 11 这里使用了 jenv 工具管理多版本 Java：\n1 2 3 4 5 6 7 8 9 # 安装 jenv git clone https://github.com/jenv/jenv.git ~/.jenv echo \u0026#39;export PATH=\u0026#34;$HOME/.jenv/bin:$PATH\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.bashrc echo \u0026#39;eval \u0026#34;$(jenv init -)\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.bashrc source ~/.bashrc # 重启会话后启用 jenv export 插件 jenv enable-plugin export exec $SHELL -l CentOS 上安装 Jenkins 1 2 3 4 5 6 # 添加 Jenkins 仓库和 GPG 密钥 sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key # 安装 yum install jenkins 配置 systemd 服务 1 2 3 4 sudo vi /etc/systemd/system/jenkins.service sudo systemctl enable /etc/systemd/system/jenkins.service sudo systemctl start jenkins systemctl status jenkins 1 2 3 4 5 6 7 8 9 10 11 12 13 [Unit] Description=jenkins service After=network.target [Service] Type=simple LimitNOFILE=65536 ExecStart=/usr/bin/jenkins User=work Environment=\u0026#34;JENKINS_PORT=8082\u0026#34; [Install] WantedBy=multi-user.target 配置 Android SDK 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 wget https://dl.google.com/android/repository/commandlinetools-linux-8512546_latest.zip unzip commandlinetools-linux-8512546_latest.zip chmod a+x cmdline-tools/ cd cmdline-tools/bin # 使用 sdkmanager 安装所需组件 ./sdkmanager --sdk_root=/home/work/workspace/android_sdk --list ./sdkmanager --install \u0026#39;platforms;android-33\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;build-tools;33.0.0\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;cmdline-tools;7.0\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;build-tools;32.0.0\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;build-tools;31.0.0\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;ndk;25.1.8937393\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;platforms;android-28\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;platforms;android-29\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;platforms;android-30\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;platforms;android-31\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;cmake;3.22.1\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;cmake;3.10.2.4988404\u0026#39; --sdk_root=/home/work/workspace/android_sdk 解决 SSH 密钥权限问题 如果遇到 Permissions 0664 for '/home/work/.ssh/jenkins_id_rsa' are too open 错误：\n1 chmod 600 ~/.ssh 升级 Jenkins 不同版本的升级指南请参考 Jenkins 官方升级文档。\n以下以 Debian/Ubuntu 系统为例（CentOS 可通过 yum update jenkins 升级）：\n1 2 3 4 wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add - sudo sh -c \u0026#39;echo deb http://pkg.jenkins.io/debian-stable binary/ \u0026gt; /etc/apt/sources.list.d/jenkins.list\u0026#39; sudo apt-get update sudo apt-get install jenkins 参考资源 Jenkins 官方安装文档（Linux） Jenkins 升级指南 jenv — Java 版本管理工具 Android 命令行工具下载 Red Hat 发行版安装 Jenkins 指南 ","permalink":"https://blog.substitute.tech/posts/jenkins/","summary":"\u003cp\u003e最近在处理一个海外应用，打包机原本在上海。由于一些特殊原因需要迁移到海外，顺便记录 Jenkins 的迁移和配置过程。\u003c/p\u003e\n\u003ch2 id=\"前置条件安装-jdk-11\"\u003e前置条件：安装 JDK 11\u003c/h2\u003e\n\u003cp\u003e这里使用了 \u003ccode\u003ejenv\u003c/code\u003e 工具管理多版本 Java：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e9\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 安装 jenv\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit clone https://github.com/jenv/jenv.git ~/.jenv\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eecho\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;export PATH=\u0026#34;$HOME/.jenv/bin:$PATH\u0026#34;\u0026#39;\u003c/span\u003e \u0026gt;\u0026gt; ~/.bashrc\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eecho\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;eval \u0026#34;$(jenv init -)\u0026#34;\u0026#39;\u003c/span\u003e \u0026gt;\u0026gt; ~/.bashrc\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003esource\u003c/span\u003e ~/.bashrc\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 重启会话后启用 jenv export 插件\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ejenv enable-plugin \u003cspan class=\"nb\"\u003eexport\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eexec\u003c/span\u003e \u003cspan class=\"nv\"\u003e$SHELL\u003c/span\u003e -l\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"centos-上安装-jenkins\"\u003eCentOS 上安装 Jenkins\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 添加 Jenkins 仓库和 GPG 密钥\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 安装\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eyum install jenkins\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"配置-systemd-服务\"\u003e配置 systemd 服务\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo vi /etc/systemd/system/jenkins.service\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo systemctl \u003cspan class=\"nb\"\u003eenable\u003c/span\u003e /etc/systemd/system/jenkins.service\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo systemctl start jenkins\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esystemctl status jenkins\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-ini\" data-lang=\"ini\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e[Unit]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eDescription\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003ejenkins service\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eAfter\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003enetwork.target\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e[Service]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eType\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003esimple\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eLimitNOFILE\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e65536\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eExecStart\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e/usr/bin/jenkins\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eUser\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003ework\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eEnvironment\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;JENKINS_PORT=8082\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e[Install]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eWantedBy\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003emulti-user.target\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"配置-android-sdk\"\u003e配置 Android SDK\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ewget https://dl.google.com/android/repository/commandlinetools-linux-8512546_latest.zip\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eunzip commandlinetools-linux-8512546_latest.zip\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003echmod a+x cmdline-tools/\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ecd\u003c/span\u003e cmdline-tools/bin\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 使用 sdkmanager 安装所需组件\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk --list\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;platforms;android-33\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;build-tools;33.0.0\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;cmdline-tools;7.0\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;build-tools;32.0.0\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;build-tools;31.0.0\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;ndk;25.1.8937393\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;platforms;android-28\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;platforms;android-29\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;platforms;android-30\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;platforms;android-31\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;cmake;3.22.1\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;cmake;3.10.2.4988404\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"解决-ssh-密钥权限问题\"\u003e解决 SSH 密钥权限问题\u003c/h2\u003e\n\u003cp\u003e如果遇到 \u003ccode\u003ePermissions 0664 for '/home/work/.ssh/jenkins_id_rsa' are too open\u003c/code\u003e 错误：\u003c/p\u003e","title":"Jenkins 配置记录"},{"content":"五一期间重新整理了家里的网络，目标是看 4K 流媒体不卡顿。既然服务器要重新配置，干脆将旧方案迁移到新的 shadowsocks-rust 上。\n系统更新 1 sudo apt update \u0026amp;\u0026amp; sudo apt upgrade 安装并配置 SS 方案一：通过 Cargo 编译安装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 安装 Rust 工具链 curl https://sh.rustup.rs -sSf | sh # 配置 Cargo 环境变量（写入 .profile / .bash_profile 等） # CARGO_HOME 指定 Cargo 安装路径 # target-cpu=native 让 rustc 为目标 CPU 生成优化代码 CARGO_HOME=/root/cargo RUSTFLAGS=\u0026#34;-C target-cpu=native\u0026#34; source .profile # 安装编译依赖 sudo apt install build-essential # 安装 shadowsocks-rust cargo install shadowsocks-rust 方案二：直接下载预编译二进制 1 2 3 4 wget https://github.com/shadowsocks/shadowsocks-rust/releases/download/v1.14.3/shadowsocks-v1.14.3.x86_64-unknown-linux-gnu.tar.xz tar -xf shadowsocks-v1.14.3.x86_64-unknown-linux-gnu.tar.xz cp ssserver /usr/local/bin chmod a+x /usr/local/bin/ssserver 配置文件 1 2 mkdir /etc/shadowsocks vi /etc/shadowsocks/config.json 1 2 3 4 5 6 { \u0026#34;server\u0026#34;: \u0026#34;::\u0026#34;, \u0026#34;server_port\u0026#34;: 8888, \u0026#34;method\u0026#34;: \u0026#34;aes-256-gcm\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;pw\u0026#34; } 测试运行：\n1 ssserver -c /etc/shadowsocks/config.json 配置自启动 1 vi /etc/systemd/system/shadowsocks-server.service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [Unit] Description=shadowsocks-rust service After=network.target [Service] Type=simple ExecStart=ssserver -c /etc/shadowsocks/config.json ExecStop=/usr/bin/killall ssserver Restart=always RestartSec=10 StandardOutput=syslog StandardError=syslog SyslogIdentifier=ssserver User=root Group=root [Install] WantedBy=multi-user.target 1 systemctl enable /etc/systemd/system/shadowsocks-server.service 优化网络延迟与吞吐量（BBR） BBR（Bottleneck Bandwidth and Round-trip propagation time）是 Google 开发的 TCP 拥塞控制算法，通过估算网络瓶颈带宽和最小 RTT 来动态调整发送速率，相比传统的基于丢包的算法，在高延迟、高吞吐场景下表现更好。\n1 2 3 4 5 6 7 8 9 # 检查 BBR 是否已启用 lsmod | grep bbr # 如果未启用，执行以下步骤 modprobe tcp_bbr echo \u0026#34;tcp_bbr\u0026#34; \u0026gt;\u0026gt; /etc/modules-load.d/modules.conf echo \u0026#34;net.core.default_qdisc=fq\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf echo \u0026#34;net.ipv4.tcp_congestion_control=bbr\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf sysctl -p BBR 需要与 fq（Fair Queue）qdisc 配合使用才能发挥最佳效果。\n启动 SS 1 2 3 4 5 6 7 8 9 10 11 sysctl --system systemctl start shadowsocks-server systemctl enable shadowsocks-server # 如有配置变更，重新加载 systemctl daemon-reload systemctl restart shadowsocks-server # 检查状态 systemctl status shadowsocks-server netstat -tunlp x-ui 方案 交流中发现 x-ui 对大多数人来说更省事，尝试记录如下：\n1 2 # 安装 x-ui，访问 http://ip:54321 bash \u0026lt;(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh) 1 2 3 4 # 安装 acme.sh，创建 HTTPS 证书 curl https://get.acme.sh | sh -s email=your@email.com ~/.acme.sh/acme.sh --register-account -m your@email.com ~/.acme.sh/acme.sh --issue -d yourdomain --standalone 参考资源 shadowsocks-rust GitHub 仓库 shadowsocks-rust 官方文档 BBR 拥塞控制算法 — Google BBR GitHub BBR FAQ x-ui 项目 acme.sh ","permalink":"https://blog.substitute.tech/posts/shadowsocks-rust/","summary":"\u003cp\u003e五一期间重新整理了家里的网络，目标是看 4K 流媒体不卡顿。既然服务器要重新配置，干脆将旧方案迁移到新的 shadowsocks-rust 上。\u003c/p\u003e\n\u003ch2 id=\"系统更新\"\u003e系统更新\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo apt update \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e sudo apt upgrade\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"安装并配置-ss\"\u003e安装并配置 SS\u003c/h2\u003e\n\u003ch3 id=\"方案一通过-cargo-编译安装\"\u003e方案一：通过 Cargo 编译安装\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 安装 Rust 工具链\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecurl https://sh.rustup.rs -sSf \u003cspan class=\"p\"\u003e|\u003c/span\u003e sh\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 配置 Cargo 环境变量（写入 .profile / .bash_profile 等）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# CARGO_HOME 指定 Cargo 安装路径\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# target-cpu=native 让 rustc 为目标 CPU 生成优化代码\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003eCARGO_HOME\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e/root/cargo\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003eRUSTFLAGS\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;-C target-cpu=native\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003esource\u003c/span\u003e .profile\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 安装编译依赖\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo apt install build-essential\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 安装 shadowsocks-rust\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecargo install shadowsocks-rust\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"方案二直接下载预编译二进制\"\u003e方案二：直接下载预编译二进制\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ewget https://github.com/shadowsocks/shadowsocks-rust/releases/download/v1.14.3/shadowsocks-v1.14.3.x86_64-unknown-linux-gnu.tar.xz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etar -xf shadowsocks-v1.14.3.x86_64-unknown-linux-gnu.tar.xz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecp ssserver /usr/local/bin\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003echmod a+x /usr/local/bin/ssserver\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"配置文件\"\u003e配置文件\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emkdir /etc/shadowsocks\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evi /etc/shadowsocks/config.json\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;server\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;::\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;server_port\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e8888\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;method\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;aes-256-gcm\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;password\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;pw\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e测试运行：\u003c/p\u003e","title":"ShadowSocks Rust 的配置与优化"},{"content":"在 Android 项目的依赖管理中，通常需要配置多个远程仓库，如 jcenter、jitpack、google() 等。一些大型项目（如\u0026quot;最右\u0026quot;）甚至依赖超过 10 个仓库。当首次初始化项目、依赖发生变化或网络出现问题时，构建过程的排査会变得相当困难。\n很早之前就发现了这个问题，但一直因为懒没有处理。本文记录 Nexus 的搭建与配置过程。\n安装并配置 Nexus 前置条件：JDK 8+。\n1 2 3 4 5 6 7 8 9 10 11 12 # 下载并解压 Nexus mkdir /app \u0026amp;\u0026amp; cd /app wget -O nexus.tar.gz https://download.sonatype.com/nexus/3/latest-unix.tar.gz tar -xvf nexus.tar.gz mv nexus-3* nexus # 创建专用用户 adduser nexus # 修改目录权限 chown -R nexus:nexus /app/nexus chown -R nexus:nexus /app/sonatype-work 配置运行用户：\n1 2 3 vi /app/nexus/bin/nexus.rc # 添加以下内容 run_as_user=\u0026#34;nexus\u0026#34; 如需修改存储路径等，编辑 JVM 参数：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 vi /app/nexus/bin/nexus.vmoptions -Xms2703m -Xmx2703m -XX:MaxDirectMemorySize=2703m -XX:+UnlockDiagnosticVMOptions -XX:+UnsyncloadClass -XX:+LogVMOutput -XX:LogFile=../sonatype-work/nexus3/log/jvm.log -XX:-OmitStackTraceInFastThrow -Djava.net.preferIPv4Stack=true -Dkaraf.home=. -Dkaraf.base=. -Dkaraf.etc=etc/karaf -Djava.util.logging.config.file=etc/karaf/java.util.logging.properties -Dkaraf.data=/nexus/nexus-data -Djava.io.tmpdir=../sonatype-work/nexus3/tmp -Dkaraf.startLocalConsole=false 创建自启动服务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 vi /etc/systemd/system/nexus.service [Unit] Description=nexus service After=network.target [Service] Type=forking LimitNOFILE=65536 User=nexus Group=nexus ExecStart=/app/nexus/bin/nexus start ExecStop=/app/nexus/bin/nexus stop User=nexus Restart=on-abort [Install] WantedBy=multi-user.target 1 2 3 # 启用并启动 chkconfig nexus on systemctl start nexus 初始化配置 打开 http://\u0026lt;ip\u0026gt;:8081 点击右上角登录 获取默认密码：cat /app/sonatype-work/nexus3/admin.password 修改密码后即可使用 1 2 3 # 重启 Nexus systemctl stop nexus systemctl restart nexus 建立代理镜像 Nexus 的仓库类型有三种核心概念：\n类型 说明 group 仓库组，将多个仓库聚合为一个统一入口 hosted 本地托管仓库，用于存储私有制品 proxy 代理仓库，从远程源（如 Maven Central）拉取并缓存 优化的思路是将多个远程仓库通过 Nexus 的 group 聚合为一个入口，客户端只配置一个仓库地址，从而减少依赖检索时的网络请求次数，加快构建速度。\n参考资源 Sonatype Nexus Repository 官方文档 Sonatype Nexus Repository 系统要求 Nexus 仓库类型说明 Nexus 下载页面 Nexus 作为系统服务运行 ","permalink":"https://blog.substitute.tech/posts/nexus/","summary":"\u003cp\u003e在 Android 项目的依赖管理中，通常需要配置多个远程仓库，如 \u003ccode\u003ejcenter\u003c/code\u003e、\u003ccode\u003ejitpack\u003c/code\u003e、\u003ccode\u003egoogle()\u003c/code\u003e 等。一些大型项目（如\u0026quot;最右\u0026quot;）甚至依赖超过 10 个仓库。当首次初始化项目、依赖发生变化或网络出现问题时，构建过程的排査会变得相当困难。\u003c/p\u003e\n\u003cp\u003e很早之前就发现了这个问题，但一直因为懒没有处理。本文记录 Nexus 的搭建与配置过程。\u003c/p\u003e\n\u003ch2 id=\"安装并配置-nexus\"\u003e安装并配置 Nexus\u003c/h2\u003e\n\u003cp\u003e前置条件：JDK 8+。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 下载并解压 Nexus\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emkdir /app \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nb\"\u003ecd\u003c/span\u003e /app\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ewget -O nexus.tar.gz https://download.sonatype.com/nexus/3/latest-unix.tar.gz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etar -xvf nexus.tar.gz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emv nexus-3* nexus\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 创建专用用户\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eadduser nexus\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 修改目录权限\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003echown -R nexus:nexus /app/nexus\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003echown -R nexus:nexus /app/sonatype-work\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e配置运行用户：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evi /app/nexus/bin/nexus.rc\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 添加以下内容\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003erun_as_user\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;nexus\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e如需修改存储路径等，编辑 JVM 参数：\u003c/p\u003e","title":"利用自建 Nexus 仓库优化 Android 构建"},{"content":"最近办公室网络波动影响工作，在 VPS 上重新搭建了一套 Shadowsocks 用来拉取源码。以下步骤适用于大多数 Linux 发行版，已在 Ubuntu 16.04 和 18.04 上测试通过。\n2024 更新说明： 本文使用的 Python 版 shadowsocks 已停止维护。推荐使用 shadowsocks-rust，它是当前官方活跃维护的实现，性能更好且支持现代加密协议。如从零开始搭建，建议直接参考 shadowsocks-rust 官方文档。下文仍保留 Python 版步骤供参考。\n基础环境准备 1 apt update \u0026amp;\u0026amp; apt upgrade -y 安装并配置 Shadowsocks (Python 版) 安装 1 2 apt install python3-pip -y pip3 install https://github.com/shadowsocks/shadowsocks/archive/master.zip 配置文件 1 2 mkdir /etc/shadowsocks vi /etc/shadowsocks/config.json 1 2 3 4 5 6 7 8 9 10 { \u0026#34;server\u0026#34;:\u0026#34;::\u0026#34;, \u0026#34;server_port\u0026#34;:8888, \u0026#34;local_address\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;local_port\u0026#34;:1080, \u0026#34;password\u0026#34;:\u0026#34;your-password\u0026#34;, \u0026#34;timeout\u0026#34;:300, \u0026#34;method\u0026#34;:\u0026#34;aes-256-cfb\u0026#34;, \u0026#34;fast_open\u0026#34;: true } 防火墙配置 1 2 3 4 5 iptables -I INPUT -p tcp --dport 8888 -j ACCEPT iptables -I INPUT -p udp --dport 8888 -j ACCEPT # 如果使用 UFW，则执行： ufw allow 8888 测试运行 1 ssserver -c /etc/shadowsocks/config.json 启用 BBR 加速 BBR (Bottleneck Bandwidth and Round-trip propagation time) 是 Google 开发的 TCP 拥塞控制算法，能显著提升网络吞吐量。\n1 2 3 4 5 6 7 8 9 # 检查是否已加载 BBR lsmod | grep bbr # 如果未加载，执行以下命令： modprobe tcp_bbr echo \u0026#34;tcp_bbr\u0026#34; \u0026gt;\u0026gt; /etc/modules-load.d/modules.conf echo \u0026#34;net.core.default_qdisc=fq\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf echo \u0026#34;net.ipv4.tcp_congestion_control=bbr\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf sysctl -p 配置 systemd 自启动 1 vi /etc/systemd/system/shadowsocks-server.service 1 2 3 4 5 6 7 8 9 10 11 12 [Unit] Description=Shadowsocks Server After=network.target [Service] ExecStartPre=/bin/sh -c \u0026#39;iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8888\u0026#39; ExecStartPre=/bin/sh -c \u0026#39;iptables -t nat -A PREROUTING -p udp --dport 443 -j REDIRECT --to-port 8888\u0026#39; ExecStart=/usr/local/bin/ssserver -c /etc/shadowsocks/config.json Restart=on-abort [Install] WantedBy=multi-user.target 网络参数优化 创建 /etc/sysctl.d/local.conf，进一步优化内核网络参数：\n1 vi /etc/sysctl.d/local.conf 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 # max open files fs.file-max = 51200 # max read/write buffer net.core.rmem_max = 67108864 net.core.wmem_max = 67108864 net.core.rmem_default = 65536 net.core.wmem_default = 65536 # max processor input queue net.core.netdev_max_backlog = 4096 # max backlog net.core.somaxconn = 4096 # resist SYN flood attacks net.ipv4.tcp_syncookies = 1 # reuse timewait sockets when safe net.ipv4.tcp_tw_reuse = 1 # turn off fast timewait sockets recycling net.ipv4.tcp_tw_recycle = 0 # short FIN timeout net.ipv4.tcp_fin_timeout = 30 # short keepalive time net.ipv4.tcp_keepalive_time = 1200 # outbound port range net.ipv4.ip_local_port_range = 10000 65000 # max SYN backlog net.ipv4.tcp_max_syn_backlog = 4096 # max timewait sockets held by system simultaneously net.ipv4.tcp_max_tw_buckets = 5000 # turn on TCP Fast Open on both client and server side net.ipv4.tcp_fastopen = 3 # TCP receive/write buffer (min, default, max) net.ipv4.tcp_rmem = 4096 87380 67108864 net.ipv4.tcp_wmem = 4096 65536 67108864 # turn on path MTU discovery net.ipv4.tcp_mtu_probing = 1 # BBR congestion control net.ipv4.tcp_congestion_control = bbr 启动 Shadowsocks 1 2 3 4 5 6 7 8 9 10 11 sysctl --system systemctl start shadowsocks-server systemctl enable shadowsocks-server # 如有修改，重新加载配置 systemctl daemon-reload systemctl restart shadowsocks-server # 检查状态 systemctl status shadowsocks-server netstat -tunlp 参考资料 Shadowsocks 官方 GitHub (Python) Shadowsocks-rust (推荐替代) Google BBR 拥塞控制算法 TCP BBR 官方文档 - kernel.org ArchLinux Wiki: Shadowsocks ","permalink":"https://blog.substitute.tech/posts/shadowsocks/","summary":"\u003cp\u003e最近办公室网络波动影响工作，在 VPS 上重新搭建了一套 Shadowsocks 用来拉取源码。以下步骤适用于大多数 Linux 发行版，已在 Ubuntu 16.04 和 18.04 上测试通过。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e2024 更新说明：\u003c/strong\u003e 本文使用的 Python 版 \u003ccode\u003eshadowsocks\u003c/code\u003e 已停止维护。推荐使用 \u003ca href=\"https://github.com/shadowsocks/shadowsocks-rust\"\u003eshadowsocks-rust\u003c/a\u003e，它是当前官方活跃维护的实现，性能更好且支持现代加密协议。如从零开始搭建，建议直接参考 \u003ca href=\"https://github.com/shadowsocks/shadowsocks-rust\"\u003eshadowsocks-rust 官方文档\u003c/a\u003e。下文仍保留 Python 版步骤供参考。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"基础环境准备\"\u003e基础环境准备\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eapt update \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e apt upgrade -y\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"安装并配置-shadowsocks-python-版\"\u003e安装并配置 Shadowsocks (Python 版)\u003c/h2\u003e\n\u003ch3 id=\"安装\"\u003e安装\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eapt install python3-pip -y\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003epip3 install https://github.com/shadowsocks/shadowsocks/archive/master.zip\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"配置文件\"\u003e配置文件\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emkdir /etc/shadowsocks\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evi /etc/shadowsocks/config.json\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;server\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;::\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;server_port\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"mi\"\u003e8888\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;local_address\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;127.0.0.1\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;local_port\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"mi\"\u003e1080\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;password\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;your-password\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;timeout\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"mi\"\u003e300\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;method\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;aes-256-cfb\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;fast_open\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"防火墙配置\"\u003e防火墙配置\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eiptables -I INPUT -p tcp --dport \u003cspan class=\"m\"\u003e8888\u003c/span\u003e -j ACCEPT\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eiptables -I INPUT -p udp --dport \u003cspan class=\"m\"\u003e8888\u003c/span\u003e -j ACCEPT\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 如果使用 UFW，则执行：\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eufw allow \u003cspan class=\"m\"\u003e8888\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"测试运行\"\u003e测试运行\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003essserver -c /etc/shadowsocks/config.json\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"启用-bbr-加速\"\u003e启用 BBR 加速\u003c/h2\u003e\n\u003cp\u003eBBR (Bottleneck Bandwidth and Round-trip propagation time) 是 Google 开发的 TCP 拥塞控制算法，能显著提升网络吞吐量。\u003c/p\u003e","title":"Shadowsocks 的配置与优化"},{"content":"三个小问题的记录：DialogFragment 返回键处理、chmod 权限速查、SSL 域名中的下划线问题。\nDialogFragment 返回键处理 DialogFragment 没有直接复写返回键的方法，有两种方式可以实现。\n方式一：在 onCreateDialog 中复写 1 2 3 4 5 6 7 8 9 @Override public Dialog onCreateDialog(Bundle savedInstanceState) { return new Dialog(getActivity(), getTheme()){ @Override public void onBackPressed() { // 在这里处理返回键逻辑 } }; } 方式二：通过 onKeyListener 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public void onResume() { super.onResume(); Dialog dialog = getDialog(); if (dialog != null) { dialog.setOnKeyListener(this); } } @Override public void onPause() { super.onPause(); Dialog dialog = getDialog(); if (dialog != null) { dialog.setOnKeyListener(null); } } 现代方案：OnBackPressedDispatcher（AndroidX） 如果需要更高版本支持，推荐使用 AndroidX 的 OnBackPressedDispatcher。从 Fragment 1.6.1 开始，DialogFragment 默认返回 ComponentDialog，它自带独立的 OnBackPressedDispatcher，可以更优雅地处理返回键：\n1 2 3 4 5 6 7 8 9 10 class MyDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return super.onCreateDialog(savedInstanceState).also { dialog -\u0026gt; val componentDialog = dialog as ComponentDialog componentDialog.onBackPressedDispatcher.addCallback(componentDialog) { // 自定义返回键处理 } } } } 详情可参考 Android Developer: ComponentDialog 和 OnBackPressedDispatcher。\nchmod 权限速查 Linux 文件权限的八进制表示法：\n权限 数字 说明 --- 0 无权限 --x 1 仅执行 -w- 2 仅写入 -wx 3 写入 + 执行 r-- 4 仅读取 r-x 5 读取 + 执行 rw- 6 读取 + 写入 rwx 7 读取 + 写入 + 执行 三个数字分别代表：所有者 / 组用户 / 其他用户。\n常用权限组合 1 2 3 4 5 6 sudo chmod 600 ××× # 仅所有者可读写 sudo chmod 644 ××× # 所有者读写，组用户和他人只读（通用文件权限） sudo chmod 700 ××× # 仅所有者有全部权限（脚本/私钥） sudo chmod 755 ××× # 所有者全部权限，其他人读+执行（可执行文件/目录） sudo chmod 666 ××× # 所有人可读写（不常用） sudo chmod 777 ××× # 所有人全部权限（风险高，慎用） 安全建议： 避免使用 777。配置文件用 600，可执行文件用 755，普通文件用 644。\nSSL 域名中的下划线问题 项目中使用了一些包含 _ 的域名，在 HTTPS 连接时报错 javax.net.ssl.SSLHandshakeException，原因在于主机名中不允许使用下划线。\n规范依据 根据 RFC 952 和 RFC 1123，主机名（hostname）的每个标签只能包含 ASCII 字母、数字和连字符（-），不允许包含下划线（_）。受影响的记录类型包括 A、AAAA、MX、CNAME 等。\n下划线在 DNS 中并非完全禁用 — RFC 2181 Section 11 允许 DNS 标签包含任意二进制字符，但主机名有更严格的限制。RFC 2782 在 SRV 记录中故意引入下划线前缀（如 _sip._tcp.example.com），以避免与主机名冲突。\n总结 主机名（URL、SSL 证书等）：不允许下划线 SRV 记录等服务发现用途：需要下划线前缀 解决方案：去掉域名中的 _，改用连字符或其他合规字符 相关讨论见 StackOverflow: The use of the underscore in host names。\n参考资料 Android Developer: ComponentDialog Android Developer: OnBackPressedDispatcher RFC 952 - Hostname Specifications RFC 1123 - Hostname Requirements RFC 2181 Section 11 - DNS Name Syntax RFC 2782 - SRV Record Specification StackOverflow: The use of the underscore in host names Wikipedia: Hostname - Restrictions ","permalink":"https://blog.substitute.tech/posts/%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86%E4%B8%89/","summary":"\u003cp\u003e三个小问题的记录：DialogFragment 返回键处理、chmod 权限速查、SSL 域名中的下划线问题。\u003c/p\u003e\n\u003ch2 id=\"dialogfragment-返回键处理\"\u003eDialogFragment 返回键处理\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eDialogFragment\u003c/code\u003e 没有直接复写返回键的方法，有两种方式可以实现。\u003c/p\u003e\n\u003ch3 id=\"方式一在-oncreatedialog-中复写\"\u003e方式一：在 onCreateDialog 中复写\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e9\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@Override\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eDialog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eonCreateDialog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eBundle\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003esavedInstanceState\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eDialog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003egetActivity\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003egetTheme\u003c/span\u003e\u003cspan class=\"p\"\u003e()){\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nd\"\u003e@Override\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eonBackPressed\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e            \u003c/span\u003e\u003cspan class=\"c1\"\u003e// 在这里处理返回键逻辑\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"方式二通过-onkeylistener\"\u003e方式二：通过 onKeyListener\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@Override\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eonResume\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003esuper\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eonResume\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eDialog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edialog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003egetDialog\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edialog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e!=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003edialog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esetOnKeyListener\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@Override\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eonPause\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003esuper\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eonPause\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eDialog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edialog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003egetDialog\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edialog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e!=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003edialog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esetOnKeyListener\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"现代方案onbackpresseddispatcherandroidx\"\u003e现代方案：OnBackPressedDispatcher（AndroidX）\u003c/h3\u003e\n\u003cp\u003e如果需要更高版本支持，推荐使用 AndroidX 的 \u003ccode\u003eOnBackPressedDispatcher\u003c/code\u003e。从 Fragment 1.6.1 开始，\u003ccode\u003eDialogFragment\u003c/code\u003e 默认返回 \u003ccode\u003eComponentDialog\u003c/code\u003e，它自带独立的 \u003ccode\u003eOnBackPressedDispatcher\u003c/code\u003e，可以更优雅地处理返回键：\u003c/p\u003e","title":"问题整理三"},{"content":"最近在写一个类似微信的相册功能，需要读取照片和视频，支持多文件夹切换，且速度要比微信快。调研后发现基于 MediaStore 的方案最为合适。以前用得不多，特此记录。\nContentResolver 对 GROUP BY 的特殊处理 ContentResolver.query() 没有提供 groupBy 参数（与 SQLiteQueryBuilder.query() 不同），但可以通过在 selection 参数中嵌入 GROUP BY 来实现类似效果。\n原理是 ContentResolver 会在编译 SQL 时给 selection 自动加上括号包裹，形成 WHERE ( ... )。利用这一点，可以在 selection 中提前闭合括号，然后追加 GROUP BY 子句。\n1 2 3 4 5 6 // 常规写法 — selection 会被包装成 WHERE (mime_type IS NOT NULL) MediaStore.Images.ImageColumns.MIME_TYPE + \u0026#34; IS NOT NULL \u0026#34; // Hack 写法 — 利用闭合括号注入 GROUP BY MediaStore.Images.ImageColumns.MIME_TYPE + \u0026#34; IS NOT NULL \u0026#34; + \u0026#34;) GROUP BY (\u0026#34; + MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME; 生成的 SQL 变为：\n1 WHERE (1=1) AND (mime_type IS NOT NULL) GROUP BY (bucket_display_name) ORDER BY ... 注意： 这种方式在 Android 10（API 29）之后可能失效。系统 MediaProvider 会额外注入 is_pending=0、is_trashed=0、volume_name IN (...) 等条件，可能导致 GROUP BY 被错误地放入 WHERE 子句内部。在 Android 14+ 上该 hack 已确定不可用。\n现代方案：Android 11+ Bundle 参数 从 Android 11（API 30）开始，ContentResolver.query() 支持通过 Bundle 传递结构化查询参数，无需再使用上述 hack。\n1 2 3 4 5 6 7 8 9 10 11 12 Bundle queryArgs = new Bundle(); queryArgs.putStringArray( ContentResolver.QUERY_ARG_GROUP_COLUMNS, new String[]{MediaStore.Images.Media.BUCKET_DISPLAY_NAME} ); Cursor cursor = contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, queryArgs, null ); 推荐使用 QUERY_ARG_GROUP_COLUMNS（结构化参数）而非 QUERY_ARG_SQL_GROUP_BY（原始 SQL），以保证向前兼容。Provider 会通过 Cursor 的 EXTRA_HONORED_ARGS 告知哪些参数已生效。\n补充：Android 10 的临时方案 如果仍需支持 API 29，可以考虑以下方案：\n使用 ContentResolver.query(uri, projection, selection, selectionArgs, sortOrder) 配合原始 SQL — 在 selection 中内联子查询 客户端分组 — 不使用 GROUP BY，全部查询后在内存中手动分组 使用自定义 ContentProvider — 自行控制 SQL 查询逻辑 注意事项 传统的 selection hack 在不同 OEM 和 Android 版本上行为可能不一致，务必充分测试。 如果目标 API 30+，应优先使用 Bundle 方案。 自定义 ContentProvider 时，可直接使用 SQLiteQueryBuilder.query()，它原生支持 groupBy 和 having 参数。 参考资料 Android Developers: Access media files from shared storage Android Developers: ContentResolver StackOverflow: How to query from MEDIA provider with \u0026ldquo;group by\u0026rdquo; option? Android Source: SQLiteQueryBuilder.java Android Source: MediaProvider ","permalink":"https://blog.substitute.tech/posts/android%E7%9A%84mediastore/","summary":"\u003cp\u003e最近在写一个类似微信的相册功能，需要读取照片和视频，支持多文件夹切换，且速度要比微信快。调研后发现基于 \u003ccode\u003eMediaStore\u003c/code\u003e 的方案最为合适。以前用得不多，特此记录。\u003c/p\u003e\n\u003ch2 id=\"contentresolver-对-group-by-的特殊处理\"\u003eContentResolver 对 GROUP BY 的特殊处理\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eContentResolver.query()\u003c/code\u003e 没有提供 \u003ccode\u003egroupBy\u003c/code\u003e 参数（与 \u003ccode\u003eSQLiteQueryBuilder.query()\u003c/code\u003e 不同），但可以通过在 \u003ccode\u003eselection\u003c/code\u003e 参数中嵌入 \u003ccode\u003eGROUP BY\u003c/code\u003e 来实现类似效果。\u003c/p\u003e\n\u003cp\u003e原理是 \u003ccode\u003eContentResolver\u003c/code\u003e 会在编译 SQL 时给 \u003ccode\u003eselection\u003c/code\u003e 自动加上括号包裹，形成 \u003ccode\u003eWHERE ( ... )\u003c/code\u003e。利用这一点，可以在 selection 中提前闭合括号，然后追加 \u003ccode\u003eGROUP BY\u003c/code\u003e 子句。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 常规写法 — selection 会被包装成 WHERE (mime_type IS NOT NULL)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eMediaStore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eImages\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eImageColumns\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eMIME_TYPE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34; IS NOT NULL \u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Hack 写法 — 利用闭合括号注入 GROUP BY\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eMediaStore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eImages\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eImageColumns\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eMIME_TYPE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34; IS NOT NULL \u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;) GROUP BY (\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eMediaStore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eImages\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eImageColumns\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eBUCKET_DISPLAY_NAME\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e生成的 SQL 变为：\u003c/p\u003e","title":"Android 的 MediaStore"},{"content":"使用 HTTPS 建议先阅读 Android 官方 Training: Security with SSL。很多公司已经全站 HTTPS，但有些用法并不正确。这里简单记录一下自己遇到的问题。\n需要提前了解的知识：\n对称加密 非对称加密 证书格式 扩展名 说明 .DER 二进制格式证书，扩展名通常为 .cer 或 .crt .PEM Base64 编码的 X.509v3 证书，以 ---BEGIN... 开头 .CRT / .CER 基本一致；.CRT 更符合微软标准 .key 使用 PKCS #8 算法处理的公钥/私钥文件 使用 OpenSSL 生成自签名证书 生成私钥 1 2 3 4 5 $ openssl genrsa -out key.pem 1024 Generating RSA private key, 1024 bit long modulus ....................++++++ .....................++++++ e is 65537 (0x10001) 创建证书签名请求（CSR） 1 2 3 4 5 6 7 8 9 10 $ openssl req -new -key key.pem -out request.pem You are about to be asked to enter information that will be incorporated into your certificate request. ----- Country Name (2 letter code) [AU]:CN State or Province Name (full name) [Some-State]:Beijing Locality Name (eg, city) []:Beijing Organization Name (eg, company) [Internet Widgits Pty Ltd]:Hxq Common Name (e.g. server FQDN or YOUR name) []:hao Email Address []:haoxiqiang@live.com Common Name 必须与服务器域名匹配，这是 SSL-RFC 的要求。\n生成自签名证书 1 $ openssl x509 -req -days 30 -in request.pem -signkey key.pem -out certificate.pem 从已有服务器获取证书 1 echo | openssl s_client -connect hostname:443 2\u0026gt;\u0026amp;1 | sed -ne \u0026#39;/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p\u0026#39; \u0026gt; hostname.pem Android 中使用证书 Android 通常只能识别 BKS（Bouncy Castle KeyStore） 类型的证书，需要转换。\n转换 PEM 到 BKS 1 2 3 4 5 6 7 8 keytool -importcert -v \\ -trustcacerts \\ -alias 0 \\ -file \u0026lt;(openssl x509 -in hostname.pem) \\ -keystore $CERTSTORE -storetype BKS \\ -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \\ -providerpath bcprov-jdk16-1.46.jar \\ -storepass password 相关工具：\nBouncy Castle — Java 加密库，提供 BKS 支持 Portecle — 图形化证书管理工具 在代码中加载 BKS 证书 1 2 3 4 5 6 7 InputStream inputStream = context.getResources().openRawResource(res); KeyStore keyStore = KeyStore.getInstance(\u0026#34;BKS\u0026#34;); keyStore.load(inputStream, password.toCharArray()); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); SSLContext sslContext = SSLContext.getInstance(\u0026#34;TLS\u0026#34;); sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom()); 如果不想用资源文件，也可以用 String -\u0026gt; InputStream 的方式加载文本形式的证书。\n证书格式转换 1 2 3 4 5 6 7 8 9 10 11 12 # PEM to DER $ openssl x509 -outform der -in certificate.pem -out certificate.der # PEM to P7B $ openssl crl2pkcs7 -nocrl -certfile certificate.cer -out certificate.p7b -certfile CAcert.cer # PEM to PFX $ openssl pkcs12 -export -out certificate.pfx -inkey privateKey.key -in certificate.crt -certfile CAcert.crt # DER to PEM $ openssl x509 -inform der -in certificate.cer -out certificate.pem # P7B to PEM $ openssl pkcs7 -print_certs -in certificate.p7b -out certificate.cer # PFX to PEM $ openssl pkcs12 -in certificate.pfx -out certificate.cer -nodes 查看证书信息 1 2 3 openssl x509 -in cert.pem -text -noout openssl x509 -in cert.cer -text -noout openssl x509 -in cert.crt -text -noout 常见问题 SSLPeerUnverifiedException: Hostname not verified 自签名证书的 Common Name 与服务器域名不匹配会触发此异常。可以通过自定义 HostnameVerifier 解决，但注意不要将 verify() 改为始终返回 true：\n1 2 3 4 5 6 7 8 HostnameVerifier hostnameVerifier = new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier(); return hv.verify(\u0026#34;example.com\u0026#34;, session); } }; 参考：Hostname Not Verified 问题\nReferences Security with HTTPS and SSL | Android Developers HttpsURLConnection | Android Developers Bouncy Castle Portecle | SourceForge OpenSSL genrsa Documentation OpenSSL req Documentation OpenSSL x509 Documentation ","permalink":"https://blog.substitute.tech/posts/https/","summary":"\u003cp\u003e使用 HTTPS 建议先阅读 \u003ca href=\"https://developer.android.com/training/articles/security-ssl.html\"\u003eAndroid 官方 Training: Security with SSL\u003c/a\u003e。很多公司已经全站 HTTPS，但有些用法并不正确。这里简单记录一下自己遇到的问题。\u003c/p\u003e","title":"HTTPS 相关记录"},{"content":"第二篇问题整理，主要涉及 WebView 的内存管理和 Cookie 同步，以及一些其他细节。\nWebView 内存泄漏 不要在 XML 中直接声明 WebView，因为 Activity 销毁后 WebView 仍可能持有 Context 引用，导致内存无法释放。正确的做法：\n使用 ApplicationContext 1 WebView webView = new WebView(getApplicationContext()); 在 Fragment 中正确处理生命周期 1 2 3 4 5 6 @Override public void onDetach() { super.onDetach(); webView.removeAllViews(); webView.destroy(); } 进程管理：AndroidManifest 中的 process 属性 可在清单文件中为不同组件分配独立进程：\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;application android:process=\u0026#34;com.processkill.p1\u0026#34;\u0026gt; \u0026lt;activity android:name=\u0026#34;com.processkill.A\u0026#34; android:process=\u0026#34;com.processkill.p2\u0026#34;\u0026gt; \u0026lt;/activity\u0026gt; \u0026lt;activity android:name=\u0026#34;com.processkill.B\u0026#34; android:process=\u0026#34;com.processkill.p3\u0026#34;\u0026gt; \u0026lt;/activity\u0026gt; \u0026lt;/application\u0026gt; 避免静态 Drawable 导致的内存泄漏 Romain Guy 写过一篇经典文章 Avoid Memory Leaks on Android。虽然现在看来有些过时——从 Android 4.0.1 开始，Drawable.setCallback() 已经改用 WeakReference——但使用 static 关键字持有 Drawable 仍然是不好的实践。\nAndroid 框架内部在设置新背景时也会清理前一个引用：\n1 2 3 4 5 6 7 8 /* * Regardless of whether we\u0026#39;re setting a new background or not, we want * to clear the previous drawable. */ if (mBackground != null) { mBackground.setCallback(null); unscheduleDrawable(mBackground); } WebView URL 参数编码 在向链接追加参数时，需要对参数值进行编码。以前使用 NameValuePair（API 23 起已标记 @deprecated），也可以自己实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 JSONObject json = new JSONObject(); Iterator\u0026lt;String\u0026gt; keys = json.keys(); StringBuilder stringBuilder = new StringBuilder(); try { while (keys.hasNext()) { String key = keys.next(); String value = json.optString(key); if (value != null) { stringBuilder.append(URLEncoder.encode(key, \u0026#34;UTF-8\u0026#34;)) .append(\u0026#34;=\u0026#34;) .append(URLEncoder.encode(value, \u0026#34;UTF-8\u0026#34;)); } } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } 本质与 NameValuePair 相同——后者内部也是两次 toString 操作。\nWebView Cookie 同步问题 使用 OkHttpClient 时需要手动将 Cookie 同步到 WebView 的 CookieManager。抓包发现没有 Cookie，研究文档才发现问题所在。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Uri uri = API.getUri(); HttpUrl httpUrl = new HttpUrl.Builder() .scheme(uri.getScheme()) .host(uri.getHost()) .build(); OkHttpClient okHttpClient = ClientManager.getInstance(); CookieJar cookieJar = okHttpClient.cookieJar(); List\u0026lt;Cookie\u0026gt; cookies = cookieJar.loadForRequest(httpUrl); for (Cookie cookie : cookies) { if (cookie != null) { String cookieString = cookie.name() + \u0026#34;=\u0026#34; + cookie.value() + \u0026#34;; domain=\u0026#34; + cookie.domain(); cookieManager.setCookie(httpUrl.toString(), cookieString); } } 关键点在于 CookieManager.setCookie() 方法的第一个参数接收的是 完整 URL，而不是单纯的 Host：\n1 2 3 4 5 6 7 8 9 10 /** * Sets a cookie for the given URL. Any existing cookie with the same host, * path and name will be replaced with the new cookie. The cookie being set * will be ignored if it is expired. * * @param url the URL for which the cookie is to be set * @param value the cookie as a string, using the format of the \u0026#39;Set-Cookie\u0026#39; * HTTP response header */ public abstract void setCookie(String url, String value); References CookieManager | Android Developers Avoid Memory Leaks on Android - Romain Guy Android WebView Memory Leak - Chromium Code Review ","permalink":"https://blog.substitute.tech/posts/%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86%E4%BA%8C/","summary":"\u003cp\u003e第二篇问题整理，主要涉及 WebView 的内存管理和 Cookie 同步，以及一些其他细节。\u003c/p\u003e","title":"问题整理（二）"},{"content":"最近准备写一个知乎日报客户端，主要是练习和验证 RxJava 在实际项目中的价值。RxJava 在国外社区很火热，目前支持的编程语言包括 Java、JavaScript、C#、Scala、Clojure、C++、Python、Ruby、Kotlin、Swift 等。\nRx（Reactive Extensions）从官方定义来看，是基于观察者模式的事件驱动异步编程框架。它扩展了观察者模式，抽象并简化了线程调度、同步、线程安全、并发数据结构和非阻塞 I/O 等基础操作。\n推荐先阅读扔物线（朱凯）的文章：给 Android 开发者的 RxJava 详解。\nScheduler（调度器） RxJava 提供多种 Scheduler 来控制代码运行在哪个线程上：\nScheduler 用途 说明 Schedulers.immediate() 当前线程 默认 Scheduler，直接在当前线程运行 Schedulers.newThread() 新线程 每次都启用新线程，无复用 Schedulers.io() I/O 操作 读写文件、数据库、网络调用等。使用无上限缓存线程池，可复用空闲线程，比 newThread() 更高效。不要在此执行 CPU 密集型计算 Schedulers.computation() 计算操作 CPU 密集型计算，线程数固定为 CPU 核数。不要在此执行 I/O 操作 AndroidSchedulers.mainThread() Android 主线程 UI 更新专用 通过 subscribeOn() 和 observeOn() 控制线程：\nsubscribeOn()：指定事件产生的线程（即 Observable.OnSubscribe 被激活的线程）。在链中只有第一次调用生效。 observeOn()：指定事件消费的线程（即 Subscriber 接收事件的线程）。可在链中多次调用，每次切换后续操作的线程。 创建 Observable 操作符 说明 Create 通过编程方式调用 Observer 方法创建 Observable Defer 直到有观察者订阅时才创建 Observable，每个订阅者获得独立实例 Empty / Never / Throw 创建特定行为的 Observable：空、永不发射、发射错误 From 将数组或 Iterable 转为 Observable Interval 按固定时间间隔发射递增整数序列 Just 将一个或一组对象转为发射这些对象的 Observable Range 发射指定范围内的连续整数 Repeat 重复发射某个项或序列 Start 发射某个函数的返回值 Timer 延迟一段时间后发射一个项 变换 Observable 操作符 说明 Buffer 定期收集发射物到集合中，再发射该集合 FlatMap 将每个发射项转换为另一个 Observable，然后合并所有结果 GroupBy 按 key 将原始 Observable 分组为多个子 Observable Map 对每个发射项应用函数进行变换 Scan 依次对每个发射项应用函数，并发射每次的中间结果 Window 类似 Buffer，但发射的是 Observable 而非集合 目前正在用 RxAndroid 写一个实际应用。基础用法已掌握，但在真实项目中仍有不少需要摸索的地方，后续继续补充。\nReferences RxJava GitHub Repository ReactiveX 官方文档 RxJava 3.x Javadoc ReactiveX Scheduler 文档 RxAndroid GitHub Repository 给 Android 开发者的 RxJava 详解 - 扔物线 ","permalink":"https://blog.substitute.tech/posts/rxjava/","summary":"\u003cp\u003e最近准备写一个知乎日报客户端，主要是练习和验证 RxJava 在实际项目中的价值。RxJava 在国外社区很火热，目前支持的编程语言包括 Java、JavaScript、C#、Scala、Clojure、C++、Python、Ruby、Kotlin、Swift 等。\u003c/p\u003e","title":"使用 RxJava 写一个应用"},{"content":"读到 Xiaoke\u0026rsquo;s Blog 上的一些问题整理，发现自己很多也遇到过，抽空整理一下。\nFragment 的状态恢复 FragmentActivity 中如果有 Fragment，系统恢复被销毁的 Activity 时会同时恢复所有 FragmentManager 中的 Fragment 列表并添加到当前 Activity。但 Fragment 只是恢复了实例，其内部状态并未自动恢复，需要在 onCreate 或 onRestoreInstanceState 中手动处理。onRestoreInstanceState 在 onStart 之后调用，可根据时机需要选择。\nFragmentActivity 状态恢复源码 1 2 3 4 5 6 7 // onCreate() 中 if (savedInstanceState != null) { Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG); // FragmentManager.restoreAllState 会将之前保存的 // Fragment 重新添加到 FragmentManager 中，并恢复 BackStack mFragments.restoreAllState(p, nc != null ? nc.fragments : null); } FragmentActivity 状态保存源码 1 2 3 4 5 6 7 8 @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Parcelable p = mFragments.saveAllState(); if (p != null) { outState.putParcelable(FRAGMENTS_TAG, p); } } Google 建议 onCreate 中只在 savedInstanceState 为 null 时才创建和初始化 Fragment：\n1 2 3 4 5 6 7 8 if (savedInstanceState == null) { DetailsFragment details = new DetailsFragment(); details.setArguments(getIntent().getExtras()); getFragmentManager() .beginTransaction() .add(android.R.id.content, details) .commit(); } 相关讨论：Failure Delivering Result onActivityResult\nBackground 和 Selector 必须使用真实 Drawable 某些三星和摩托罗拉设备上，如果使用定义在 colors.xml 中的伪 drawable，会显示纯黑色背景。必须使用真实的图片 drawable 或定义好的 shape：\n1 2 \u0026lt;color name=\u0026#34;mail_published_time_color\u0026#34;\u0026gt;#bcbcbc\u0026lt;/color\u0026gt; \u0026lt;drawable name=\u0026#34;ab_bg_black\u0026#34;\u0026gt;#aa191919\u0026lt;/drawable\u0026gt; onActivityResult 中的 Dialog 显示问题 通过 startActivityForResult 调用其他应用后，在 onActivityResult 中需要异步处理并显示对话框。但如果直接在 onActivityResult 中调用 Dialog.show() 会报错——因为 onActivityResult 在 onResume 之前调用，此时 FragmentManager 处于\u0026quot;状态已保存\u0026quot;状态，不允许提交事务。\n类似地，onSaveInstanceState 调用之后也不能进行 FragmentTransaction 的 commit 操作。这两个问题都会影响 DialogFragment 的正常使用。\n正确的做法有两种：\n方法一：在 onPostResume 中显示对话框。 方法二：在 onActivityResult 中设置标志（如 mPendingShowDialog = true），然后在 onResume 中检查并处理。不仅是 Dialog，所有 Fragment 相关的 transaction 和 commit 操作都需要考虑这个时序问题。如果必须在 onSaveInstanceState 之后 commit，应使用 commitAllowingStateLoss。 ListView 显示空白区域问题 ListView 设置 MATCH_PARENT 但内容太少未撑满空间时，ListView 会自动缩小至内容所需高度，空白区域显示默认背景色。解决办法是在主题中添加：\n1 \u0026lt;item name=\u0026#34;android:overScrollFooter\u0026#34;\u0026gt;@android:color/transparent\u0026lt;/item\u0026gt; 或在布局中设置：\n1 android:overScrollFooter=\u0026#34;@android:color/transparent\u0026#34; 参考：Background Color ListView - Stack Overflow\nDrawable Shape 的 Solid 填充问题 \u0026lt;solid\u0026gt; 不设置颜色时，理论默认是透明，但某些三星设备上会显示为黑色。建议始终显式设置：\n1 2 3 4 5 6 7 8 \u0026lt;shape xmlns:android=\u0026#34;http://schemas.android.com/apk/res/android\u0026#34; android:shape=\u0026#34;rectangle\u0026#34;\u0026gt; \u0026lt;solid android:color=\u0026#34;@android:color/transparent\u0026#34;/\u0026gt; \u0026lt;stroke android:width=\u0026#34;1dp\u0026#34; android:color=\u0026#34;@color/soft_white\u0026#34;/\u0026gt; \u0026lt;corners android:radius=\u0026#34;1dp\u0026#34;/\u0026gt; \u0026lt;/shape\u0026gt; WebView 换行问题 Android 4.4 (API 19) 开始，WebView 默认不会自动断行。代码中可尝试设置布局算法：\n1 settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING); TEXT_AUTOSIZING 会根据启发式规则提高段落字体大小，使宽视口布局在概览模式下更易读。不过很多网页仍需要配合 HTML 层级的处理：\n1 \u0026lt;pre style=\u0026#34;word-wrap: break-word; white-space: pre-wrap;\u0026#34;\u0026gt; 使用拨号键盘的 SecretCode 功能 Android 拨号键盘支持特殊的 Secret Code，可启动自定义 Intent。用户输入 *#*#CODE#*#* 时系统会广播 android.provider.Telephony.SECRET_CODE 或（API 29+）android.telephony.action.SECRET_CODE。\n1 2 3 4 5 6 \u0026lt;receiver android:name=\u0026#34;.receiver.DiagnoserReceiver\u0026#34;\u0026gt; \u0026lt;intent-filter\u0026gt; \u0026lt;action android:name=\u0026#34;android.provider.Telephony.SECRET_CODE\u0026#34;/\u0026gt; \u0026lt;data android:scheme=\u0026#34;android_secret_code\u0026#34; android:host=\u0026#34;111222\u0026#34;/\u0026gt; \u0026lt;/intent-filter\u0026gt; \u0026lt;/receiver\u0026gt; 1 2 3 4 5 6 7 8 9 10 public class DiagnoserReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { if (\u0026#34;android.provider.Telephony.SECRET_CODE\u0026#34;.equals(intent.getAction())) { Intent i = new Intent(Intent.ACTION_MAIN); i.setClass(context, Diagnoser.class); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(i); } } } 详情可参考 Create a secret doorway to your app。\n将现有 Git 仓库导入到另一个仓库 推荐使用 Git Subtree Merge 方法：\n1 2 3 4 5 6 7 git remote add rack_remote git@github.com:schacon/rack.git git fetch rack_remote git merge -s ours --no-commit rack_remote/master git read-tree --prefix=rack/ -u rack_remote/master git commit -m \u0026#34;Imported rack as a subtree.\u0026#34; # 后续跟踪上游更新： git pull -s subtree rack_remote master Git 会自动识别子树的根目录，后续合并无需再指定 prefix。\n参考：About Git Subtree Merges\nCardView 的 Ripple 效果（Android Lollipop） AppCompat 支持库中省略了 Ripple 效果。如需查看 Ripple，需使用 Android 5.0+ 版本并在对应设备上测试。AppCompat v7 官方说明：\n\u0026ldquo;Why are there no ripples on pre-Lollipop? A lot of what allows RippleDrawable to run smoothly is Android 5.0\u0026rsquo;s new RenderThread. To optimize for performance on previous versions of Android, we\u0026rsquo;ve left RippleDrawable out for now.\u0026rdquo;\n设置方式：使用 android:foreground（而非 background）指向 selectableItemBackground，避免与 CardView 的阴影和圆角渲染冲突。\n1 2 3 4 5 6 7 8 \u0026lt;android.support.v7.widget.CardView android:layout_width=\u0026#34;match_parent\u0026#34; android:layout_height=\u0026#34;wrap_content\u0026#34; android:layout_gravity=\u0026#34;center\u0026#34; android:foreground=\u0026#34;?android:attr/selectableItemBackground\u0026#34; android:clickable=\u0026#34;true\u0026#34;\u0026gt; ... \u0026lt;/android.support.v7.widget.CardView\u0026gt; Mac OS X 光标移动和文字编辑快捷键 参考 Apple 官方快捷键文档。\n光标移动快捷键：\nControl-F：前进一个字符（Forward） Control-B：后退一个字符（Backward） Control-P：上移一行（Previous） Control-N：下移一行（Next） Control-A：移到行首（Ahead） Control-E：移到行尾（End） 文字操作快捷键：\nControl-H：删除光标前一个字符 Control-D：删除光标后一个字符 Control-K：删除光标到行尾的所有字符 Control-Shift-A：选中光标到行首 Control-Shift-E：选中光标到行尾 切换 Git 版本 Xcode 自带 Git，如果之前安装过其他版本想统一使用 Homebrew 管理的版本，需要做符号链接切换：\n1 2 3 cd /Applications/Xcode.app/Contents/Developer/usr/bin sudo mv ./git ./git-xcode-usr-bin sudo ln -s /usr/local/bin/git ./git References Saving State with Fragments | Android Developers Failure Delivering Result - Stack Overflow WebSettings.LayoutAlgorithm | Android Developers Background Color ListView - Stack Overflow Android Shape Solid Default Color - Stack Overflow Create a Secret Doorway to Your App About Git Subtree Merges | GitHub RippleDrawable | Android Developers Mac Keyboard Shortcuts | Apple Support TelephonyManager.ActionSecretCode | Android Developers ","permalink":"https://blog.substitute.tech/posts/%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86/","summary":"\u003cp\u003e读到 Xiaoke\u0026rsquo;s Blog 上的一些问题整理，发现自己很多也遇到过，抽空整理一下。\u003c/p\u003e","title":"Android 开发中遇到的问题整理"},{"content":"构建项目时发现 shrinkResources 这个属性，用于删除项目中未使用的资源文件。记录一下使用中的问题和配置方法。\n基本配置 shrinkResources 需要配合 minifyEnabled 一起使用，因为资源清理是在 ProGuard/R8 去除无用代码之后进行的：\n1 2 3 4 5 6 7 8 9 10 android { ... buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile(\u0026#39;proguard-android.txt\u0026#39;), \u0026#39;proguard-rules.pro\u0026#39; } } } 查看清理结果 通过 --info 日志查看整体缩减效果：\n1 2 3 4 5 6 ./gradlew clean assembleRelease --info \u0026gt; build-output.txt ... :android:shrinkDebugResources Removed unused resources: Binary resource data reduced from 2570KB to 1711KB: Removed 33% ... 查看具体跳过了哪些文件：\n1 2 3 4 $ ./gradlew clean assembleDebug --info | grep \u0026#34;Skipped unused resource\u0026#34; Skipped unused resource res/anim/abc_fade_in.xml: 396 bytes Skipped unused resource res/anim/abc_fade_out.xml: 396 bytes Skipped unused resource res/anim/abc_slide_in_bottom.xml: 400 bytes 保留或排除特定资源 有些资源通过反射引用（常见于第三方 SDK），会被构建系统误删。现在的版本已经支持通过 keep.xml 配置保留规则。\nStrict Mode 在 res/raw/keep.xml 中设置 shrinkMode=\u0026quot;strict\u0026quot; 启用严格模式：\n1 2 3 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt; \u0026lt;resources xmlns:tools=\u0026#34;http://schemas.android.com/tools\u0026#34; tools:shrinkMode=\u0026#34;strict\u0026#34; /\u0026gt; 保留特定资源 1 2 3 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt; \u0026lt;resources xmlns:tools=\u0026#34;http://schemas.android.com/tools\u0026#34; tools:keep=\u0026#34;@layout/umeng_*, @drawable/umeng_*, @anim/umeng_*\u0026#34; /\u0026gt; 强制删除 1 2 3 4 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt; \u0026lt;resources xmlns:tools=\u0026#34;http://schemas.android.com/tools\u0026#34; tools:shrinkMode=\u0026#34;safe\u0026#34; tools:discard=\u0026#34;@layout/unused2\u0026#34; /\u0026gt; 另外，构建系统也会跳过看起来像被引用的资源 URL，例如 file:///android_res/drawable/ic_plus.png 这类形式。\n按需配置语言和密度 国内很多应用不需要多语言支持，可以通过 resConfigs 保留指定语言或密度资源：\n1 2 3 4 5 6 android { defaultConfig { ... resConfigs \u0026#34;en\u0026#34;, \u0026#34;zh\u0026#34; } } 也可以配置 \u0026quot;nodpi\u0026quot;、\u0026quot;hdpi\u0026quot; 等来限定密度资源。\n总结 建议尝试启用 shrinkResources，即使应用本身比较\u0026quot;干净\u0026quot;、效果不明显，日志信息也能帮你了解项目中哪些资源被使用、哪些可能被误删。\n参考 Customize which resources to keep Enable app optimization ","permalink":"https://blog.substitute.tech/posts/resourceshrinking%E8%B5%84%E6%BA%90%E6%B8%85%E7%90%86/","summary":"\u003cp\u003e构建项目时发现 \u003ccode\u003eshrinkResources\u003c/code\u003e 这个属性，用于删除项目中未使用的资源文件。记录一下使用中的问题和配置方法。\u003c/p\u003e","title":"Android Resource Shrinking 资源清理"},{"content":"很早想写这样一个 Demo。以前实现树形菜单使用 TreeViewList（继承 ListView 的封装），或者用 ExpandableListView 实现多级菜单。后来发现根本不需要自定义控件——直接使用 RecyclerView，只需要控制数据源的展平转换即可。\n核心思路：以递归方式将嵌套数据结构展平为线性列表，通过 notifyItemRangeInserted / notifyItemRangeRemoved 控制展开和收起。\n技术要点 1. 统一数据格式 无论原始数据源是什么格式，都转换为带 children 的树形结构，方便递归处理：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 { \u0026#34;tree\u0026#34;: { \u0026#34;children\u0026#34;: [ { \u0026#34;available\u0026#34;: true, \u0026#34;children\u0026#34;: [], \u0026#34;id\u0026#34;: \u0026#34;548005da36ec3532c4a18391\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;第一轮复习\u0026#34; } ], \u0026#34;id\u0026#34;: \u0026#34;gaozhongshuxue\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;高中数学\u0026#34; } } 2. 递归友好的实体类 实体类需要能存储其全部子节点：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Course { @Expose @SerializedName(\u0026#34;id\u0026#34;) public String id; @Expose @SerializedName(\u0026#34;name\u0026#34;) public String name; public int level; // 层级 public boolean open; // 展开状态 public String parentId; // 父节点标识 public LinkedList\u0026lt;Course\u0026gt; children = new LinkedList\u0026lt;\u0026gt;(); public boolean hasChild() { return children != null \u0026amp;\u0026amp; children.size() \u0026gt; 0; } public void addChildren(LinkedList\u0026lt;Course\u0026gt; children) { this.children.clear(); this.children.addAll(children); } } 3. 递归构建树形数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private void createTree(Course container, JSONArray children, String parentId, int level) throws JSONException { if (children != null) { int size = children.length(); LinkedList\u0026lt;Course\u0026gt; tree = new LinkedList\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; size; i++) { JSONObject item = children.getJSONObject(i); Course course = new Course(); course.id = item.getString(\u0026#34;id\u0026#34;); course.name = item.getString(\u0026#34;name\u0026#34;); course.level = level; course.open = false; course.parentId = parentId; JSONArray subChildren = item.getJSONArray(\u0026#34;children\u0026#34;); createTree(course, subChildren, course.id, level + 1); tree.add(course); if (container == null) { mData.add(course); // 顶层节点直接加入数据源 } } if (container != null) { container.addChildren(tree); } } } 4. 点击展开/收起 每次点击 item 时判断该节点是否有子节点，然后分发展开或收起操作。使用 notifyItemRangeInserted / notifyItemRangeRemoved 实现动画效果。如果不需要动画，可以在 dispatchClick 返回 true 时直接调用 notifyDataSetChanged。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public boolean dispatchClick(LinkedList\u0026lt;Course\u0026gt; container, Course course) { if (container == null || course == null) { return false; } if (course.hasChild()) { int insertPosition = container.indexOf(course) + 1; if (course.open) { size = 0; removeAllChildren(container, course); notifyItemRangeRemoved(insertPosition, size); } else { course.open = true; container.addAll(insertPosition, course.children); notifyItemRangeInserted(insertPosition, course.children.size()); } return true; } return false; } private void removeAllChildren(LinkedList\u0026lt;Course\u0026gt; container, Course course) { course.open = false; int childrenSize = course.children.size(); for (Course child : course.children) { if (child.hasChild() \u0026amp;\u0026amp; child.open) { child.open = false; removeAllChildren(container, child); } } size += childrenSize; container.removeAll(course.children); } 为什么选择 notifyItemRangeInserted/Removed 而非 notifyDataSetChanged：前者保留 RecyclerView 的默认动画效果，用户能直观看到节点展开/收起的过程，体验更流畅。后者会刷新整个列表、丢失动画，且性能开销更大。\n5. 自定义动画（可选） RecyclerView 默认动画可能看不出弹出效果。可以继承 SimpleItemAnimator 自定义：\n1 2 3 4 5 6 7 8 9 @Override public boolean animateAdd(final RecyclerView.ViewHolder holder) { resetAnimation(holder); ViewCompat.setTranslationY(holder.itemView, -(holder.itemView.getMeasuredHeight() / 2)); ViewCompat.setAlpha(holder.itemView, 0); mPendingAdditions.add(holder); return true; } 源码 完整示例代码见 GitHub: Haoxiqiang/TreeView\n参考 RecyclerView SimpleItemAnimator ","permalink":"https://blog.substitute.tech/posts/treeview%E6%A0%91%E5%BD%A2%E8%8F%9C%E5%8D%95/","summary":"\u003cp\u003e很早想写这样一个 Demo。以前实现树形菜单使用 \u003ccode\u003eTreeViewList\u003c/code\u003e（继承 \u003ccode\u003eListView\u003c/code\u003e 的封装），或者用 \u003ccode\u003eExpandableListView\u003c/code\u003e 实现多级菜单。后来发现根本不需要自定义控件——直接使用 \u003ccode\u003eRecyclerView\u003c/code\u003e，只需要控制数据源的展平转换即可。\u003c/p\u003e\n\u003cp\u003e核心思路：以递归方式将嵌套数据结构展平为线性列表，通过 \u003ccode\u003enotifyItemRangeInserted\u003c/code\u003e / \u003ccode\u003enotifyItemRangeRemoved\u003c/code\u003e 控制展开和收起。\u003c/p\u003e","title":"RecyclerView 实现树形菜单"},{"content":"今天有位同事问我一段话是什么意思：\nLorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid veniam harum quas similique quis, quisquam tempore iste. Alias soluta dolorum ratione, dolorem quia corrupti, suscipit eveniet dolor dignissimos voluptas laborum debitis ea fugiat aliquam rem ullam minus a aliquid pariatur!\n第一眼以为是英文，结果没看懂；仔细看还是没看懂。查了一下才发现——这是拉丁文！为什么程序员会写这么一段话呢？\n什么是 Lorem ipsum？ Lorem ipsum 从 15 世纪开始就被广泛用于西方的印刷和设计领域。电脑排版普及后，这段被传统印刷产业使用几百年的无意义文字又再度流行。由于它以 \u0026ldquo;Lorem ipsum\u0026rdquo; 开头，常被用于标题测试，所以一般称为 Lorem ipsum（简称 Lipsum）。\n常见的 Lorem ipsum 段落：\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n一点历史 原先大家以为这段拉丁文只是无意义的组合，目的是让阅读者专注于字型和版式。但根据拉丁学者 Richard McClintock 的研究，Lorem ipsum 实际上源自西塞罗（Cicero）的《善恶之尽》（De finibus bonorum et malorum，公元前 45 年）：\nNeque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit\u0026hellip;\n（无人爱苦，亦无人寻之欲之，乃因其苦……）\n为了减少可读性并使字母出现频率接近现代英语，有些版本用 K、W、Z 等拉丁文中没有的字母替换，或加入 zzril、takimata 等词。\n简单说就是：排版时用来检查样式的占位文字，看起来逼格很高而已。\n参考 Wikipedia: Lorem ipsum Wikipedia: De finibus bonorum et malorum ","permalink":"https://blog.substitute.tech/posts/loremipsum%E4%B9%B1%E6%95%B0%E5%81%87%E6%96%87/","summary":"\u003cp\u003e今天有位同事问我一段话是什么意思：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eLorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid veniam harum quas similique quis, quisquam tempore iste. Alias soluta dolorum ratione, dolorem quia corrupti, suscipit eveniet dolor dignissimos voluptas laborum debitis ea fugiat aliquam rem ullam minus a aliquid pariatur!\u003c/p\u003e\n\u003c/blockquote\u003e","title":"Lorem Ipsum 乱数假文"},{"content":"基础命令不提了，文档很多。记录几个工程中实际用到的操作。\n资源 Git 官方文档 Pro Git 中文版 Codecademy Git 教程 — 在线交互式学习 删除远程分支 从 Git v1.7.0 起，推荐使用 --delete 语法：\n1 2 3 4 5 # 推荐方式（v1.7.0+） $ git push origin --delete \u0026lt;branchName\u0026gt; # 旧语法（推送空引用到目标分支） $ git push origin :\u0026lt;branchName\u0026gt; 详见 git-push 文档。\n回退最近一次提交 1 2 3 4 5 $ git commit ... # (1) 提交 $ git reset --soft HEAD~1 # (2) 撤销提交，保留工作区更改 # \u0026lt;\u0026lt; 编辑文件 \u0026gt;\u0026gt; # (3) 修改文件 $ git add .... # (4) 暂存 $ git commit -c ORIG_HEAD # (5) 复用原提交信息提交 参考 Stack Overflow。\n注意：如果只是想修改提交信息，git commit --amend 更合适。reset 会断开与历史提交的关联，而 amend 只编辑最近一次提交的信息。\n提取某次提交的内容（cherry-pick） 多分支开发时容易改乱分支，手动合并代码很痛苦。直到发现这个命令：\n1 $ git cherry-pick commit-hash 参考 Git 官方文档 - git-push Git 官方文档 - git-reset Git 官方文档 - git-cherry-pick Stack Overflow: How do you undo the last commit? GitHub Docs: Pushing commits to a remote repository ","permalink":"https://blog.substitute.tech/posts/git%E7%9A%84%E4%B8%80%E4%BA%9B%E6%93%8D%E4%BD%9C/","summary":"\u003cp\u003e基础命令不提了，文档很多。记录几个工程中实际用到的操作。\u003c/p\u003e","title":"Git 的一些操作"},{"content":"项目中使用腾讯 Bugly 做崩溃监控。同类工具功能相似，选择 Bugly 主要是因为统计界面友好、品牌可靠。以下整理的 Bugly 技术博客文章，包含开发中遇到的 case 和很有启发性的分析思路。\n文章列表 全系统栈崩溃是什么鬼？（链接可能已失效）— PDF 备份 论 FileDescriptor 泄漏如何导致 Crash（链接可能已失效）— PDF 备份 常见 Android Native 崩溃及错误原因（链接可能已失效）— PDF 备份 Android 堆栈惨遭毁容，精神哥揭露毁容真相（链接可能已失效）— PDF 备份 ANR 到底是什么（链接可能已失效）— PDF 备份 读懂 ANR 的 trace 文件（链接可能已失效）— PDF 备份 UnsatisfiedLinkError 分析（链接可能已失效）— PDF 备份 java.lang.NoSuchMethodError 分析（链接可能已失效）— PDF 备份 参考 Bugly 官方文档 Bugly 专业版 - 崩溃文档 腾讯云终端性能监控 Pro ","permalink":"https://blog.substitute.tech/posts/%E5%A6%82%E4%BD%95%E6%A0%B9%E6%8D%AE%E9%94%99%E8%AF%AF%E6%97%A5%E5%BF%97%E8%A7%A3%E5%86%B3%E9%97%AE%E9%A2%98/","summary":"\u003cp\u003e项目中使用腾讯 Bugly 做崩溃监控。同类工具功能相似，选择 Bugly 主要是因为统计界面友好、品牌可靠。以下整理的 Bugly 技术博客文章，包含开发中遇到的 case 和很有启发性的分析思路。\u003c/p\u003e","title":"如何根据错误日志解决问题 —— Bugly 博客整理"},{"content":"排查内存泄漏问题时，经常需要反射修改字段值。在 Stack Overflow 上看到一个高质量回答，整理如下。\n能否修改 private static final 字段？ 在 SecurityManager 允许的前提下，可以通过 setAccessible 绕过 private 限制，再移除 final 修饰符，从而修改 private static final 字段。\n示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.lang.reflect.*; public class EverythingIsTrue { static void setFinalStatic(Field field, Object newValue) throws Exception { field.setAccessible(true); Field modifiersField = Field.class.getDeclaredField(\u0026#34;modifiers\u0026#34;); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() \u0026amp; ~Modifier.FINAL); field.set(null, newValue); } public static void main(String args[]) throws Exception { setFinalStatic(Boolean.class.getField(\u0026#34;FALSE\u0026#34;), true); System.out.format(\u0026#34;Everything is %s\u0026#34;, false); // \u0026#34;Everything is true\u0026#34; } } 若无 SecurityException 抛出，上述代码输出 \u0026quot;Everything is true\u0026quot;。\n原理 main 中的 true 和 false 被自动装箱为 Boolean.TRUE 和 Boolean.FALSE 通过反射将 Boolean.FALSE 指向 Boolean.TRUE 所引用的对象 此后所有 false 的自动装箱结果都变为 Boolean.TRUE 于是原本为 \u0026ldquo;false\u0026rdquo; 的地方都变成了 \u0026ldquo;true\u0026rdquo; 位运算说明 1 field.getModifiers() \u0026amp; ~Modifier.FINAL field.getModifiers() 获取修饰符位掩码 ~Modifier.FINAL 对 FINAL 位取反 按位与运算，即清除 FINAL 位 注意事项 此行为依赖于 SecurityManager 配置，开启后可能失败 JLS 17.5.3 明确指出：final 字段可以通过反射及其他实现相关手段修改，但语义上仅在对象构造完成且尚未被其他线程可见时才有合理含义 若 final 字段声明时初始化为编译期常量（compile-time constant），反射修改可能无效——编译器已将常量内联到字节码中 JVM 可以对 final 字段的读取进行激进的指令重排优化 JLS 17.5.3：Final fields can be changed via reflection and other implementation dependent means. The only pattern in which this has reasonable semantics is one in which an object is constructed and then the final fields of the object are updated. The object should not be made visible to other threads, nor should the final fields be read, until all updates to the final fields of the object are complete.\n另见 JLS 15.28 Constant Expression：编译期常量（如原始类型的 private static final boolean）的内联特性使得反射修改很可能无效。\n参考 Stack Overflow: Change private static final field using Java reflection JLS 17.5.3: Subsequent Modification of Final Fields JLS 15.28: Constant Expression Stack Overflow: Using reflection to change static final File.separatorChar Stack Overflow: How to limit setAccessible to legitimate uses Wikipedia: Bitwise operation ","permalink":"https://blog.substitute.tech/posts/javareflection/","summary":"\u003cp\u003e排查内存泄漏问题时，经常需要反射修改字段值。在 Stack Overflow 上看到一个高质量回答，整理如下。\u003c/p\u003e","title":"Java Reflection 修改 private static final 字段"},{"content":"项目中使用 Volley 作为网络库，封装过程中遇到几个常见问题，记录如下。以下内容不兼容 Android 2.3 及以下版本。\n添加请求参数 重写 getParams 方法即可：\n1 2 3 4 @Override protected Map\u0026lt;String, String\u0026gt; getParams() { return mParams; } 使用 Cookie HttpURLConnection 原生支持 Cookie 管理，通过 CookieHandler 设置：\n1 2 CookieHandler.setDefault(new CookieManager()); CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL)); 请求时通过 Header 携带 Cookie，响应时解析 set-cookie 头。详见 HttpURLConnection 文档。\nCache.Entry 空指针异常 1 2 3 Attempt to read from field com.android.volley.Cache$Entry com.android.volley.Response.cacheEntry on a null object reference at com.android.volley.NetworkDispatcher.run(NetworkDispatcher.java:126) 原因：重写 parseNetworkResponse 时，第二个参数传入了 null，导致 NetworkDispatcher 读缓存时抛出 NPE。\n修复：返回有效的缓存头信息。\n1 2 3 4 5 @Override protected Response\u0026lt;String\u0026gt; parseNetworkResponse(NetworkResponse response) { // ... return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response)); } statusCode 空指针异常 1 int statusCode = statusLine.getStatusCode(); // NullPointerException Request 构造参数（如 listener）为 null 时可能触发。请确保必要参数非空。\n设置超时时间 1 2 3 4 myRequest.setRetryPolicy(new DefaultRetryPolicy( MY_SOCKET_TIMEOUT_MS, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)); 参考 Volley 官方文档 Volley GitHub 仓库 Volley 自定义请求 HttpURLConnection ","permalink":"https://blog.substitute.tech/posts/volley%E8%87%AA%E5%AE%9A%E4%B9%89%E4%B8%AD%E7%9A%84%E5%87%A0%E4%B8%AA%E9%97%AE%E9%A2%98%E7%9A%84%E5%8E%9F%E5%9B%A0/","summary":"\u003cp\u003e项目中使用 Volley 作为网络库，封装过程中遇到几个常见问题，记录如下。以下内容不兼容 Android 2.3 及以下版本。\u003c/p\u003e","title":"Volley 自定义中的几个问题"},{"content":" 本文基于 Gson 官方用户指南整理而成。相对于其他 JSON 框架，Gson 的性能并不逊色，加上 Google 官方维护的背景，成为 Java/Android 项目中处理 JSON 的首选。以下是对官方文档的全面梳理和备忘。\nGson 是 Google 开发的 Java 库，用于将 Java 对象序列化为 JSON 表示，以及将 JSON 字符串反序列化为 Java 对象。\n性能和可伸缩性 以下数据来自桌面系统（dual opteron, 8GB RAM, 64-bit Ubuntu），可通过 PerformanceTest 复现：\nstrings：超过 25MB 的字符串反序列化没有问题 Large collections：序列化 140 万对象的集合，反序列化 87000 对象的集合 Gson 1.4 将数组的反序列化限制从 80KB 提升到了 11MB 基础用法 基本类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 序列化 Gson gson = new Gson(); gson.toJson(1); ==\u0026gt; prints 1 gson.toJson(\u0026#34;abcd\u0026#34;); ==\u0026gt; prints \u0026#34;abcd\u0026#34; gson.toJson(new Long(10)); ==\u0026gt; prints 10 int[] values = { 1 }; gson.toJson(values); ==\u0026gt; prints [1] // 反序列化 int one = gson.fromJson(\u0026#34;1\u0026#34;, int.class); Integer one = gson.fromJson(\u0026#34;1\u0026#34;, Integer.class); Long one = gson.fromJson(\u0026#34;1\u0026#34;, Long.class); Boolean false = gson.fromJson(\u0026#34;false\u0026#34;, Boolean.class); String str = gson.fromJson(\u0026#34;\\\u0026#34;abc\\\u0026#34;\u0026#34;, String.class); String anotherStr = gson.fromJson(\u0026#34;[\\\u0026#34;abc\\\u0026#34;]\u0026#34;, String.class); 对象示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class BagOfPrimitives { private int value1 = 1; private String value2 = \u0026#34;abc\u0026#34;; private transient int value3 = 3; BagOfPrimitives() { // no-args constructor } } // 序列化 BagOfPrimitives obj = new BagOfPrimitives(); Gson gson = new Gson(); String json = gson.toJson(obj); ==\u0026gt; json is {\u0026#34;value1\u0026#34;:1,\u0026#34;value2\u0026#34;:\u0026#34;abc\u0026#34;} // 反序列化 BagOfPrimitives obj2 = gson.fromJson(json, BagOfPrimitives.class); ==\u0026gt; obj2 is just like obj 要点 推荐使用 private 字段 不需要注解来标记序列化字段，当前类及其所有父类的字段都会默认序列化 transient 字段会被自动忽略 null 处理： 序列化时，null 字段会被跳过 反序列化时，JSON 中缺失的键对应的字段值会被设为 null synthetic（合成）字段不会被序列化 内部类、匿名类和局部类的外部类引用字段会被忽略，不会参与序列化和反序列化 嵌套类 Gson 可以轻松序列化/反序列化静态嵌套类，但无法自动反序列化非静态内部类，因为其无参构造函数需要外部类引用，在反序列化时无法提供。\n1 2 3 4 5 6 7 8 9 10 // NOTE: 这个 class B 默认不会被 Gson 正确序列化 public class A { public String a; class B { public String b; public B() { // No args constructor for B } } } Gson 无法反序列化 {\u0026quot;b\u0026quot;:\u0026quot;abc\u0026quot;} 因为 class B 是内部类。两种解决方案：\n将 B 定义为 static class B 自定义 InstanceCreator： 1 2 3 4 5 6 7 8 9 10 // 可行但不推荐 public class InstanceCreatorForB implements InstanceCreator\u0026lt;A.B\u0026gt; { private final A a; public InstanceCreatorForB(A a) { this.a = a; } public A.B createInstance(Type type) { return a.new B(); } } 数组 1 2 3 4 5 6 7 8 9 10 Gson gson = new Gson(); int[] ints = {1, 2, 3, 4, 5}; String[] strings = {\u0026#34;abc\u0026#34;, \u0026#34;def\u0026#34;, \u0026#34;ghi\u0026#34;}; // 序列化 gson.toJson(ints); ==\u0026gt; prints [1,2,3,4,5] gson.toJson(strings); ==\u0026gt; prints [\u0026#34;abc\u0026#34;, \u0026#34;def\u0026#34;, \u0026#34;ghi\u0026#34;] // 反序列化 int[] ints2 = gson.fromJson(\u0026#34;[1,2,3,4,5]\u0026#34;, int[].class); Gson 也支持任意复杂元素类型的多维数组。\n集合 1 2 3 4 5 6 7 8 9 Gson gson = new Gson(); Collection\u0026lt;Integer\u0026gt; ints = Lists.immutableList(1,2,3,4,5); // 序列化 String json = gson.toJson(ints); ==\u0026gt; json is [1,2,3,4,5] // 反序列化 Type collectionType = new TypeToken\u0026lt;Collection\u0026lt;Integer\u0026gt;\u0026gt;(){}.getType(); Collection\u0026lt;Integer\u0026gt; ints2 = gson.fromJson(json, collectionType); 注意：Java 的泛型擦除使得反序列化时必须通过 TypeToken 指定具体泛型类型。\n集合限制 可以序列化任意类型的集合，但无法自动反序列化（无法确定元素数据类型） 反序列化时，集合必须是具体泛型化的集合 泛型 由于 Java 的类型擦除，直接对泛型对象进行序列化/反序列化会丢失类型信息：\n1 2 3 4 5 6 7 8 class Foo\u0026lt;T\u0026gt; { T value; } Gson gson = new Gson(); Foo\u0026lt;Bar\u0026gt; foo = new Foo\u0026lt;Bar\u0026gt;(); gson.toJson(foo); // 可能无法正确序列化 foo.value gson.fromJson(json, foo.getClass()); // 无法将 foo.value 反序列化为 Bar 通过 TypeToken 解决：\n1 2 3 Type fooType = new TypeToken\u0026lt;Foo\u0026lt;Bar\u0026gt;\u0026gt;() {}.getType(); gson.toJson(foo, fooType); gson.fromJson(json, fooType); 混合类型集合 对于 JSON 数组 ['hello', 5, {name: 'GREETINGS', source: 'guest'}]：\n1 2 3 4 Collection collection = new ArrayList(); collection.add(\u0026#34;hello\u0026#34;); collection.add(5); collection.add(new Event(\u0026#34;GREETINGS\u0026#34;, \u0026#34;guest\u0026#34;)); 序列化很简单，但反序列化需要额外操作。三种方法：\n推荐：使用 Gson 的解析 API（JsonParser）逐个解析元素 为 Collection.class 注册类型适配器 使用 Collection\u0026lt;MyCollectionMemberType\u0026gt; 并注册适配器 使用 JsonParser 的示例：\n1 2 3 4 5 6 Gson gson = new Gson(); JsonParser parser = new JsonParser(); JsonArray array = parser.parse(json).getAsJsonArray(); String message = gson.fromJson(array.get(0), String.class); int number = gson.fromJson(array.get(1), int.class); Event event = gson.fromJson(array.get(2), Event.class); 内置类型支持 Gson 内置了以下类型的序列化/反序列化：\njava.net.URL — 匹配字符串 http://code.google.com/p/google-gson/ java.net.URI — 匹配字符串 /p/google-gson/ Joda-Time 支持 DateTime：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private static class DateTimeTypeConverter implements JsonSerializer\u0026lt;DateTime\u0026gt;, JsonDeserializer\u0026lt;DateTime\u0026gt; { @Override public JsonElement serialize(DateTime src, Type srcType, JsonSerializationContext context) { return new JsonPrimitive(src.toString()); } @Override public DateTime deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { try { return new DateTime(json.getAsString()); } catch (IllegalArgumentException e) { Date date = context.deserialize(json, Date.class); return new DateTime(date); } } } Instant：\n1 2 3 4 5 6 7 8 9 10 11 12 13 private static class InstantTypeConverter implements JsonSerializer\u0026lt;Instant\u0026gt;, JsonDeserializer\u0026lt;Instant\u0026gt; { @Override public JsonElement serialize(Instant src, Type srcType, JsonSerializationContext context) { return new JsonPrimitive(src.getMillis()); } @Override public Instant deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { return new Instant(json.getAsLong()); } } 自定义序列化和反序列化 Gson 通过 GsonBuilder 注册自定义的序列化器：\n1 2 3 4 5 GsonBuilder gson = new GsonBuilder(); gson.registerTypeAdapter(MyType2.class, new MyTypeAdapter()); gson.registerTypeAdapter(MyType.class, new MySerializer()); gson.registerTypeAdapter(MyType.class, new MyDeserializer()); gson.registerTypeAdapter(MyType.class, new MyInstanceCreator()); registerTypeAdapter 会检查适配器是否实现了多个接口，并相应注册。\n自定义序列化器 1 2 3 4 5 6 private class DateTimeSerializer implements JsonSerializer\u0026lt;DateTime\u0026gt; { public JsonElement serialize(DateTime src, Type typeOfSrc, JsonSerializationContext context) { return new JsonPrimitive(src.toString()); } } 自定义反序列化器 1 2 3 4 5 6 private class DateTimeDeserializer implements JsonDeserializer\u0026lt;DateTime\u0026gt; { public DateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { return new DateTime(json.getAsJsonPrimitive().getAsString()); } } 泛型类型的统一处理 如果有一个 Id\u0026lt;T\u0026gt; 类，不同泛型参数的序列化方式相同，可以注册一个处理器统一处理所有 Id 类型：\n1 2 // Gson 支持注册单一处理器处理所有同 raw type 的泛型， // 也支持为特定泛型注册独立处理器 实例构造器（Instance Creator） 反序列化时，Gson 需要创建对象实例。如果类没有无参构造方法，就需要提供 InstanceCreator。\n1 2 3 4 5 private class MoneyInstanceCreator implements InstanceCreator\u0026lt;Money\u0026gt; { public Money createInstance(Type type) { return new Money(\u0026#34;1000000\u0026#34;, CurrencyCode.USD); } } 参数化类型的实例构造 1 2 3 4 5 6 7 8 class MyList\u0026lt;T\u0026gt; extends ArrayList\u0026lt;T\u0026gt; { } class MyListInstanceCreator implements InstanceCreator\u0026lt;MyList\u0026lt;?\u0026gt;\u0026gt; { @SuppressWarnings(\u0026#34;unchecked\u0026#34;) public MyList\u0026lt;?\u0026gt; createInstance(Type type) { return new MyList(); } } 对于需要在构造时获取参数化类型信息的场景：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Id\u0026lt;T\u0026gt; { private final Class\u0026lt;T\u0026gt; classOfId; private final long value; public Id(Class\u0026lt;T\u0026gt; classOfId, long value) { this.classOfId = classOfId; this.value = value; } } class IdInstanceCreator implements InstanceCreator\u0026lt;Id\u0026lt;?\u0026gt;\u0026gt; { public Id\u0026lt;?\u0026gt; createInstance(Type type) { Type[] typeParameters = ((ParameterizedType)type).getActualTypeArguments(); Type idType = typeParameters[0]; return Id.get((Class)idType, 0L); } } 格式化输出 默认 Gson 输出紧凑格式，无空白字符。需要美化输出时：\n1 2 Gson gson = new GsonBuilder().setPrettyPrinting().create(); String jsonOutput = gson.toJson(someObject); NULL 对象处理 默认情况下，Gson 忽略 null 字段。如需序列化 null 值：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Gson gson = new GsonBuilder().serializeNulls().create(); public class Foo { private final String s; private final int i; public Foo() { this(null, 5); } public Foo(String s, int i) { this.s = s; this.i = i; } } Foo foo = new Foo(); String json = gson.toJson(foo); System.out.println(json); // {\u0026#34;s\u0026#34;:null,\u0026#34;i\u0026#34;:5} 版本支持 通过 @Since 注解支持同一对象的多版本控制：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Since(1.1) private final String newerField; @Since(1.0) private final String newField; private final String field; VersionedClass versionedObject = new VersionedClass(); Gson gson = new GsonBuilder().setVersion(1.0).create(); String jsonOutput = gson.toJson(someObject); System.out.println(jsonOutput); // {\u0026#34;newField\u0026#34;:\u0026#34;new\u0026#34;,\u0026#34;field\u0026#34;:\u0026#34;old\u0026#34;} gson = new Gson(); jsonOutput = gson.toJson(someObject); System.out.println(jsonOutput); // {\u0026#34;newerField\u0026#34;:\u0026#34;newer\u0026#34;,\u0026#34;newField\u0026#34;:\u0026#34;new\u0026#34;,\u0026#34;field\u0026#34;:\u0026#34;old\u0026#34;} 字段排除 Java Modifier 排除 默认排除 transient 和 static 字段。如需只排除 static：\n1 2 3 Gson gson = new GsonBuilder() .excludeFieldsWithModifiers(Modifier.STATIC) .create(); 支持多种修饰符组合：\n1 2 3 Gson gson = new GsonBuilder() .excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT, Modifier.VOLATILE) .create(); @Expose 注解 通过 @Expose 注解标记需要暴露的字段，未标记的字段被排除：\n1 2 3 Gson gson = new GsonBuilder() .excludeFieldsWithoutExposeAnnotation() .create(); 自定义排除策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) public @interface Foo { // Field tag only annotation } public class MyExclusionStrategy implements ExclusionStrategy { private final Class\u0026lt;?\u0026gt; typeToSkip; private MyExclusionStrategy(Class\u0026lt;?\u0026gt; typeToSkip) { this.typeToSkip = typeToSkip; } public boolean shouldSkipClass(Class\u0026lt;?\u0026gt; clazz) { return (clazz == typeToSkip); } public boolean shouldSkipField(FieldAttributes f) { return f.getAnnotation(Foo.class) != null; } } Gson gson = new GsonBuilder() .setExclusionStrategies(new MyExclusionStrategy(String.class)) .serializeNulls() .create(); 字段命名 Gson 支持预定义的字段命名策略和 @SerializedName 注解：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private class SomeObject { @SerializedName(\u0026#34;custom_naming\u0026#34;) private final String someField; private final String someOtherField; public SomeObject(String a, String b) { this.someField = a; this.someOtherField = b; } } SomeObject someObject = new SomeObject(\u0026#34;first\u0026#34;, \u0026#34;second\u0026#34;); Gson gson = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE) .create(); String jsonRepresentation = gson.toJson(someObject); System.out.println(jsonRepresentation); // {\u0026#34;custom_naming\u0026#34;:\u0026#34;first\u0026#34;,\u0026#34;SomeOtherField\u0026#34;:\u0026#34;second\u0026#34;} 参考资料 Gson GitHub 仓库 Gson 官方用户指南 Gson API 文档 (Javadoc) Joda-Time ","permalink":"https://blog.substitute.tech/posts/gsonexamples/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本文基于 Gson 官方用户指南整理而成。相对于其他 JSON 框架，Gson 的性能并不逊色，加上 Google 官方维护的背景，成为 Java/Android 项目中处理 JSON 的首选。以下是对官方文档的全面梳理和备忘。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eGson 是 Google 开发的 Java 库，用于将 Java 对象序列化为 JSON 表示，以及将 JSON 字符串反序列化为 Java 对象。\u003c/p\u003e","title":"Gson 使用指南"},{"content":" 时效性说明：本文涉及的镜像地址仅适用于特定时期的版本。清华 TUNA 和中科大 USTC 的 AOSP 镜像地址已多次变更，请以各镜像站官方帮助页为准。\nAOSP 源码体积庞大（约 70GB），通过 VPN 从 Google 官方源下载极其缓慢。国内镜像可以大幅提升下载速度。\n镜像地址替换 将 https://android.googlesource.com/ 全部替换为清华 TUNA 镜像即可：\n1 git://aosp.tuna.tsinghua.edu.cn/android/ 如果已经下载过部分源码，修改 .repo/manifest.xml 中 aosp remote 的 fetch 地址：\n1 2 3 4 5 6 7 \u0026lt;manifest\u0026gt; \u0026lt;remote name=\u0026#34;aosp\u0026#34; - fetch=\u0026#34;https://android.googlesource.com\u0026#34; + fetch=\u0026#34;git://aosp.tuna.tsinghua.edu.cn/android/\u0026#34; review=\u0026#34;android-review.googlesource.com\u0026#34; /\u0026gt; \u0026lt;remote name=\u0026#34;github\u0026#34; 下载 repo 工具 1 2 3 4 5 6 mkdir ~/bin PATH=~/bin:$PATH curl https://storage.googleapis.com/git-repo-downloads/repo \u0026gt; ~/bin/repo ## 如果上述 URL 不可访问，可以用下面的： ## curl https://storage-googleapis.lug.ustc.edu.cn/git-repo-downloads/repo \u0026gt; ~/bin/repo chmod a+x ~/bin/repo 建立工作目录并初始化 1 2 3 4 5 mkdir WORKING_DIRECTORY cd WORKING_DIRECTORY repo init -u git://mirrors.ustc.edu.cn/aosp/platform/manifest ## 如果需要特定版本（例如 Android 5.1.1）： repo init -u git://mirrors.ustc.edu.cn/aosp/platform/manifest -b android-5.1.1_r3 如果无法连接 gerrit.googlesource.com，编辑 ~/bin/repo，修改 REPO_URL 一行：\n1 REPO_URL = \u0026#39;https://gerrit-googlesource.lug.ustc.edu.cn/git-repo\u0026#39; 同步源码 1 repo sync 之后只需重复执行此命令即可同步最新代码。\n已有仓库切换镜像源 如果已从官方同步了 AOSP 仓库，想使用国内镜像，修改 .repo/manifests.git/config：\n将：\n1 url = https://android.googlesource.com/platform/manifest 改为：\n1 url = git://mirrors.ustc.edu.cn/aosp/platform/manifest 此方法也可用于从 TUNA 同步 CyanogenMod 代码。\n参考资料 AOSP 官方网站 清华 TUNA AOSP 镜像帮助页 中科大 USTC AOSP 镜像帮助页 ","permalink":"https://blog.substitute.tech/posts/android%E6%BA%90%E7%A0%81%E4%B8%8B%E8%BD%BD/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e时效性说明\u003c/strong\u003e：本文涉及的镜像地址仅适用于特定时期的版本。清华 TUNA 和中科大 USTC 的 AOSP 镜像地址已多次变更，请以各镜像站官方帮助页为准。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eAOSP 源码体积庞大（约 70GB），通过 VPN 从 Google 官方源下载极其缓慢。国内镜像可以大幅提升下载速度。\u003c/p\u003e","title":"Android 源码下载"},{"content":"使用 Google Messenger 时发现它有设置默认短信应用的功能，于是研究了一下实现方式。\nAndroid 4.4 KitKat 开始，Google 推出了默认短信应用的机制。官方对此的解释是：\nSome of you have built SMS apps using hidden APIs — a practice we discourage because hidden APIs may be changed or removed and new devices are not tested against them for compatibility. So, to provide you with a fully supported set of APIs for building SMS apps.\n简而言之，做得足够好、用户量足够大，Google 就会重视并官方支持。\nAPI 概览 从 Android 4.4 开始引入了两个新 Intent：\nSMS_DELIVER_ACTION — 短信 WAP_PUSH_DELIVER_ACTION — 彩信 设置默认应用 1 2 3 Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT); intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, getPackageName()); startActivity(intent); 完整实现：注册与取消注册 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 // 检查当前是否是默认短信应用 currentPackageName = getPackageName(); String defaultSmsApp = Telephony.Sms.getDefaultSmsPackage(this); if (currentPackageName != null \u0026amp;\u0026amp; !currentPackageName.equals(defaultSmsApp)) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(DEDAULTSMS, defaultSmsApp); editor.apply(); } // 注册为默认 register.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT); intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, getPackageName()); startActivity(intent); } }); // 恢复默认 unregister.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { SharedPreferences sharedPreferences = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()); String defaultSmsApp = sharedPreferences.getString(DEDAULTSMS, null); if (defaultSmsApp != null) { Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT); intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, defaultSmsApp); startActivity(intent); } else { Toast.makeText(getApplicationContext(), \u0026#34;failed unregister\u0026#34;, Toast.LENGTH_SHORT).show(); } } }); 关键逻辑说明 代码的核心思路：\n启动时保存当前默认短信应用的包名到 SharedPreferences 用户点击\u0026quot;注册\u0026quot;时，将自己设为默认 用户点击\u0026quot;取消注册\u0026quot;时，从 SharedPreferences 读取之前的默认应用并恢复 关于 Android Q (10) 的变更： 从 Android 10 开始，Google 引入了 RoleManager API，推荐使用 RoleManager.createRequestRoleIntent(RoleManager.ROLE_SMS) 替代旧的 ACTION_CHANGE_DEFAULT 方式。但上述实现仍兼容大多数版本。\n参考资料 Telephony API 参考文档 Telephony.Sms.Intents - ACTION_CHANGE_DEFAULT Getting Your SMS Apps Ready for KitKat ","permalink":"https://blog.substitute.tech/posts/defaultapp/","summary":"\u003cp\u003e使用 Google Messenger 时发现它有设置默认短信应用的功能，于是研究了一下实现方式。\u003c/p\u003e\n\u003cp\u003eAndroid 4.4 KitKat 开始，Google 推出了默认短信应用的机制。官方对此的解释是：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eSome of you have built SMS apps using hidden APIs — a practice we discourage because hidden APIs may be changed or removed and new devices are not tested against them for compatibility. So, to provide you with a fully supported set of APIs for building SMS apps.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e简而言之，做得足够好、用户量足够大，Google 就会重视并官方支持。\u003c/p\u003e","title":"Make Your App the Default SMS App"},{"content":"阅读 Android 官方文档时发现几个容易忽略的细节，整理如下。\nFragmentTransaction 使用 replace 方法 replace() 会先移除该容器下的所有 Fragment，再添加新的 Fragment。配合 addToBackStack() 可实现返回键恢复上一个 Fragment。\n1 2 3 4 FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragment_container, newFragment); transaction.addToBackStack(null); transaction.commit(); 注意：addToBackStack(null) 将整个事务（而非单个 Fragment）压入回退栈，按返回键时反向执行整个事务的所有操作。\n参考：FragmentTransaction API 文档\nDB 的 BaseColumns 声明 SQLite 内容提供者中，表的内联类应当实现 BaseColumns 接口，自动包含 _ID 和 _COUNT 常量。\n1 2 3 4 5 6 7 8 /* Inner class that defines the table contents */ public static abstract class FeedEntry implements BaseColumns { public static final String TABLE_NAME = \u0026#34;entry\u0026#34;; public static final String COLUMN_NAME_ENTRY_ID = \u0026#34;entryid\u0026#34;; public static final String COLUMN_NAME_TITLE = \u0026#34;title\u0026#34;; public static final String COLUMN_NAME_SUBTITLE = \u0026#34;subtitle\u0026#34;; ... } 参考：BaseColumns API 文档\n启动隐式 Intent 之前的校验 当使用隐式 Intent 启动外部 Activity 时，必须先检查是否有应用能够处理该 Intent，否则会导致 ActivityNotFoundException 崩溃。\n1 2 3 4 5 6 7 8 9 10 PackageManager packageManager = getPackageManager(); List activities = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); boolean isIntentSafe = activities.size() \u0026gt; 0; // 使用 Chooser 时 Intent chooser = Intent.createChooser(intent, title); if (intent.resolveActivity(getPackageManager()) != null) { startActivity(chooser); } 关键区别：\nqueryIntentActivities() 返回所有匹配的 Activity 列表 resolveActivity() 返回最佳的 Activity（或 Chooser） 参考：Intent 和 Intent 过滤器\n提供给第三方应用使用的 Activity 让第三方应用调用自己应用的 Activity，需要在 Manifest 中声明 intent-filter：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;activity android:name=\u0026#34;ShareActivity\u0026#34;\u0026gt; \u0026lt;!-- Handle sending SMS --\u0026gt; \u0026lt;intent-filter\u0026gt; \u0026lt;action android:name=\u0026#34;android.intent.action.SENDTO\u0026#34;/\u0026gt; \u0026lt;category android:name=\u0026#34;android.intent.category.DEFAULT\u0026#34;/\u0026gt; \u0026lt;data android:scheme=\u0026#34;sms\u0026#34; /\u0026gt; \u0026lt;data android:scheme=\u0026#34;smsto\u0026#34; /\u0026gt; \u0026lt;/intent-filter\u0026gt; \u0026lt;!-- Handle sharing text/images --\u0026gt; \u0026lt;intent-filter\u0026gt; \u0026lt;action android:name=\u0026#34;android.intent.action.SEND\u0026#34;/\u0026gt; \u0026lt;category android:name=\u0026#34;android.intent.category.DEFAULT\u0026#34;/\u0026gt; \u0026lt;data android:mimeType=\u0026#34;image/*\u0026#34;/\u0026gt; \u0026lt;data android:mimeType=\u0026#34;text/plain\u0026#34;/\u0026gt; \u0026lt;/intent-filter\u0026gt; \u0026lt;/activity\u0026gt; 接收 Intent 后根据类型做不同处理：\n1 2 3 4 5 6 7 8 9 10 11 12 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = getIntent(); Uri data = intent.getData(); if (intent.getType().indexOf(\u0026#34;image/\u0026#34;) != -1) { // Handle image data } else if (intent.getType().equals(\u0026#34;text/plain\u0026#34;)) { // Handle text } } 返回结果给调用方：\n1 2 3 4 Intent result = new Intent(\u0026#34;com.example.RESULT_ACTION\u0026#34;, Uri.parse(\u0026#34;content://result_uri\u0026#34;)); setResult(Activity.RESULT_OK, result); finish(); Handler 的正确使用姿势 如果通过 new Handler() 在外部类中创建 Handler，其 Looper 取决于创建时的线程。如果不显式指定 Looper，post(Runnable) 是否在主线程执行是不确定的。\n推荐的单例写法：\n1 private static Handler INSTANCE = new Handler(Looper.getMainLooper()); 始终通过 Looper.getMainLooper() 明确指定主线程，避免线程不确定性问题。\n参考：Handler API 文档\nWebView 加载页面时自动请求 favicon 不管页面是否有 favicon，WebView 都会发送以下请求（甚至 iframe 也会触发）：\n1 2 3 \u0026#34;GET /favicon.ico HTTP/1.1\u0026#34; 404 183 \u0026#34;GET /apple-touch-icon-precomposed.png HTTP/1.1\u0026#34; 404 197 \u0026#34;GET /apple-touch-icon.png HTTP/1.1\u0026#34; 404 189 解决方案 使用 data URI 替代真实请求：\n1 \u0026lt;link rel=\u0026#34;icon\u0026#34; href=\u0026#34;data:;base64,iVBORw0KGgo=\u0026#34;\u0026gt; 此方案兼容 Safari、Chrome 和 Firefox。\n技术原理：浏览器在加载页面后会尝试获取站点图标（favicon），即使页面未声明 icon 链接，大多数浏览器仍会默认请求 /favicon.ico。Android WebView 基于 Chromium，同样存在此行为。\n参考：\nChromium Issue 131567 html5-boilerplate Issue 1103 阻止 favicon 请求的相关讨论 ","permalink":"https://blog.substitute.tech/posts/%E6%9C%80%E8%BF%91%E5%8F%91%E7%8E%B0%E7%9A%84%E5%87%A0%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/","summary":"\u003cp\u003e阅读 Android 官方文档时发现几个容易忽略的细节，整理如下。\u003c/p\u003e","title":"最近读文档发现的几个注意点"},{"content":" 历史说明：本文基于 2015 年的 Dart 开发环境编写。当时的 Dart 还以 Dart Editor + Dartium 浏览器为核心工具链。如今的 Dart 已全面转向 Flutter 生态和 Dart SDK 命令行工具链，内容仅供参考。\n您可以直接在 Android 设备上启动和调试 Dart Web 应用，无需预编译为 JavaScript。需要安装 Dart Editor 和 Dart Content Shell。Dart Content Shell 会自动安装到 Android 设备上。\n环境配置 所需硬件和软件：\nAndroid 设备（手机或平板） USB 数据线 电脑和手机上均安装 Chrome 浏览器 第一部分：配置开发环境 Step 1: 配置电脑 下载并安装 Dart Editor：Download Dart Editor（注：该下载页面已不再维护）\nStep 2: 配置 Android 设备 开启 USB 调试。参考：Chrome 远程调试设置\nStep 3: 连接设备 通过 USB 连接设备：连接设备指南\nStep 4: 配置端口转发 除非使用家庭网络，否则通常需要设置端口转发。详情可参考：Android 远程调试\nStep 5: 在 Android 设备上启动应用 在 Dart Editor 中右键点击 HTML 文件，选择 Run in Dartium 或移动端运行选项：\n常见问题：\n如果使用端口转发，确保 Chrome 浏览器正在运行以测试应用 如果 Dart Editor 无法识别设备（显示\u0026quot;No phone or USB development phone found\u0026quot;），尝试拔插 USB 线 如果弹出以下对话框，按提示处理： Step 6: 调试应用 应用在 Android 设备上运行后，可在 Dart Editor 中设置断点进行调试。在 Tools \u0026gt; Debugger 中看到\u0026quot;远程\u0026quot;连接即可开始调试。\n常见问题（FAQ） 哪些应用会下载到 Android 设备？\n首次启动 Dart Editor 会话时，两个应用会被下载到 Android 设备：\nDart Content Shell — 包含 Dart VM 的精简版 Chromium，测试时应用运行在此环境中 连接测试应用 — 用于检测 Web 服务器访问问题 什么是端口转发？为什么需要？\n\u0026ldquo;Pub serve over USB\u0026quot;和\u0026quot;Embedded server over WiFi network\u0026quot;有什么区别？\n在 Manage Launches 对话框中可看到两个选项：\nPub serve over USB：通过 USB 端口转发，推荐在防火墙、公共 WiFi、或设备与电脑不在同一网络时使用 Embedded server over WiFi network：使用 Dart Editor 内嵌服务器，适合家庭无防火墙网络，无需端口转发 第二部分：运行示例代码 启动 Editor 进入解压文件夹，双击 DartEditor。\n获取示例代码 1 git clone https://github.com/dart-lang/one-hour-codelab.git 打开示例项目 Dart Editor 中选择 File \u0026gt; Open Existing Folder，找到 clone 的代码目录。\n运行基础应用 打开 1-blankbadge，右键 piratebadge.html，选择 Run in Dartium。\n更详细的示例说明见：Dart Codelab\n参考资料 Dart 官方文档 Dart 官方网站（已停止维护的历史版本） Chrome 远程调试文档 ","permalink":"https://blog.substitute.tech/posts/dart/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e历史说明\u003c/strong\u003e：本文基于 2015 年的 Dart 开发环境编写。当时的 Dart 还以 Dart Editor + Dartium 浏览器为核心工具链。如今的 Dart 已全面转向 Flutter 生态和 Dart SDK 命令行工具链，内容仅供参考。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e您可以直接在 Android 设备上启动和调试 Dart Web 应用，无需预编译为 JavaScript。需要安装 \u003cstrong\u003eDart Editor\u003c/strong\u003e 和 \u003cstrong\u003eDart Content Shell\u003c/strong\u003e。Dart Content Shell 会自动安装到 Android 设备上。\u003c/p\u003e","title":"Dart Web Apps on Android"},{"content":"缓存算法（Cache Replacement Policies）决定缓存空间满时哪些数据被淘汰，对系统性能有直接影响。以下是常见的缓存淘汰算法概览。\n算法一览 算法 全称 核心思路 Belady Bélády\u0026rsquo;s Algorithm 淘汰未来最久不会使用的项（理论最优，不可实现） LRU Least Recently Used 淘汰最久未访问的项 MRU Most Recently Used 淘汰最近刚访问的项 PLRU Pseudo-LRU LRU 的近似实现，用 bitset 替代链表，减少开销 RR Random Replacement 随机淘汰 SLRU Segmented LRU 将缓存分为 probation 和 protected 两段 2-way 2-Way Set Associative 每个缓存组有两个槽位的集合关联映射 Direct-mapped Direct-Mapped Cache 每个内存地址只能映射到一个固定缓存行 LFU Least-Frequently Used 淘汰访问频率最低的项 LIRS Low Inter-reference Recency Set 区分近期高/低重用距离，性能优于 LRU ARC Adaptive Replacement Cache 自适应地在 LRU 和 LFU 之间平衡 CAR Clock with Adaptive Replacement ARC 的 Clock 近似实现 MQ Multi Queue 多队列分级管理，各有独立的替换策略 Least Recently Used (LRU) LRU 是最常用的缓存淘汰策略：当缓存满且需要插入新项时，淘汰最久未被使用的项。\n实现要点 查询：将访问的 key 移动到链表头部 插入：如果 key 不存在且空间不足，从尾部移除最近最少使用的项，重复直到空间满足 常见实现方式 LinkedHashMap：Java 中 LinkedHashMap 开启 accessOrder 即可实现 LRU Linked List + HashMap：双向链表 + 哈希表实现 O(1) 的读写 LruCache：Android 提供的 LruCache\u0026lt;K, V\u0026gt; 类 优缺点 优点：利用时间局部性，实现简单 缺点：遍历型访问模式（working set 略大于缓存）会导致频繁缓存抖动 参考资料 Wikipedia: Cache Replacement Policies - LRU 懒惰的肥兔 - LRU缓存实现(Java) PDF: LRU缓存实现(Java) ARC (Adaptive Replacement Cache) ARC 由 IBM 研究院提出，自适应地在 LRU（近期性）和 LFU（频率性）之间动态调整，通常能在多种负载下取得比单纯 LRU 更好的命中率。\nWikipedia: Adaptive Replacement Cache 原始论文：ARC: A Self-Tuning, Low Overhead Replacement Cache LIRS (Low Inter-reference Recency Set) LIRS 区分\u0026quot;热\u0026quot;数据和\u0026quot;冷\u0026quot;数据，用重用间隔（inter-reference recency）而非简单的时间顺序来做淘汰决策，在数据库和存储系统中应用广泛。\nWikipedia: LIRS 原始论文: LIRS: An Efficient Low Inter-reference Recency Set 注：本文最初写于学习缓存算法时的笔记整理，内容将持续完善。\n","permalink":"https://blog.substitute.tech/posts/cachealgorithms/","summary":"\u003cp\u003e缓存算法（Cache Replacement Policies）决定缓存空间满时哪些数据被淘汰，对系统性能有直接影响。以下是常见的缓存淘汰算法概览。\u003c/p\u003e","title":"Cache Algorithms"},{"content":"Android 开发中偶尔会遇到一些看似莫名其妙的问题，记录在这里，方便以后查阅。\n1. Android Studio 新建工程直接报编译错误 1 2 Error:Execution failed for task :app:mergeDebugResources. \u0026gt; Crunching Cruncher ic_launcher.png failed, see logs 原因：早期 Android Studio 版本中，资源目录结构不完整时可能触发此问题。\n解决方法：创建一个 drawable-hdpi 或 drawable-xhdpi 文件夹即可。\n2. 魅蓝 Note 无法连接 ADB（或其它 USB 连接问题） 当所有常规条件都正常时，手机仍然无法连接电脑，可能需要添加 USB Vendor ID。\nmacOS 下的解决方法：\n通过系统信息或 system_profiler SPUSBDataType 找到设备的 Vendor ID 编辑 ~/.android/adb_usb.ini，在末尾添加 0x2a45（魅族厂商 ID） 重启 ADB 服务并重新连接： 1 2 adb kill-server adb start-server 详细参考：Android 官方 ADB 文档\n3. 应用签名冲突无法安装 1 Package com.yuexue.tifenapp signatures do not match the previously installed version; ignoring! 原因：手机上已安装的版本与新安装的版本签名不一致。这在调试与正式签名切换时经常出现。\n解决方法：先卸载冲突版本：\n1 adb uninstall com.yuexue.tifenapp 关键要点：\nadb -e 指定模拟器设备，不带设备参数则会匹配唯一连接设备 应用在谷歌 Play 中的包名卸载需要用 adb shell pm uninstall -k --user 0 \u0026lt;pkg\u0026gt;（需 root） ","permalink":"https://blog.substitute.tech/posts/recent-strangeproblem/","summary":"\u003cp\u003eAndroid 开发中偶尔会遇到一些看似莫名其妙的问题，记录在这里，方便以后查阅。\u003c/p\u003e","title":"最近遇到的一些奇怪的问题"},{"content":"Android 提供了多种数据持久化方案,不同的方案适用于不同的场景。选择合适的存储方式对于应用的性能、安全性和用户体验至关重要。\n存储方案概览 存储方式 适用场景 数据私有 卸载时移除 SharedPreferences 键值对简单数据 是 是 Internal Storage 应用私有文件 是 是 External Storage 可分享的文件 否 仅 getExternalFilesDir() SQLite Databases 结构化数据 是 是 Network Connection 服务端数据 — 否 Content Providers 跨应用数据共享 取决于实现 取决于实现 SharedPreferences 用于存储 key-value 形式的简单数据,存储在 XML 文件中。直接支持基本数据类型和 String。\n写值 commit() 会同步写入磁盘,而 apply() 直接刷新内存然后异步写入磁盘——推荐优先使用 apply():\n1 2 3 SharedPreferences.Editor editor = settings.edit(); editor.putBoolean(\u0026#34;silentMode\u0026#34;, mSilentMode); editor.apply(); // 使用 apply 替代 commit 读取 直接调用 getXX(key, defaultValue):\n1 2 boolean silentMode = settings.getBoolean(\u0026#34;silentMode\u0026#34;, false); String name = settings.getString(\u0026#34;name\u0026#34;, \u0026#34;\u0026#34;); 注意:2019 年 Google 推出了 Jetpack DataStore,使用 Kotlin Coroutines 和 Flow 实现了类型安全的异步存储,是 SharedPreferences 的现代替代方案。\nInternal Storage \u0026amp;\u0026amp; External Storage 区别 Internal Storage (内部存储)\n始终可用 默认仅本应用可访问 用户卸载应用时数据一并移除 适合存储不希望被其他应用读取的私有数据 External Storage (外部存储)\n不保证始终可用(如作为 USB 存储时可能被移除) 文件权限公开,可能被其他应用读取 仅 getExternalFilesDir() 创建的文件会在卸载时删除 适合需要分享或用户通过电脑访问的文件 Internal Storage 操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 方法1: 使用 getFilesDir() File file = new File(context.getFilesDir(), filename); // 方法2: 使用 openFileOutput() String filename = \u0026#34;myfile\u0026#34;; FileOutputStream outputStream; try { outputStream = openFileOutput(filename, Context.MODE_PRIVATE); outputStream.write(\u0026#34;Hello world!\u0026#34;.getBytes()); outputStream.close(); } catch (Exception e) { e.printStackTrace(); } // 方法3: 缓存文件 public File getTempFile(Context context, String url) { File file; try { String fileName = Uri.parse(url).getLastPathSegment(); file = File.createTempFile(fileName, null, context.getCacheDir()); } catch (IOException e) { // Error while creating file } return file; } External Storage 操作 操作前需要先判断存储状态:\n1 2 3 4 5 6 7 8 9 10 11 12 // 检查外部存储是否可读写 public boolean isExternalStorageWritable() { String state = Environment.getExternalStorageState(); return Environment.MEDIA_MOUNTED.equals(state); } // 检查外部存储至少可读 public boolean isExternalStorageReadable() { String state = Environment.getExternalStorageState(); return Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state); } 查询可用空间 API 8+ 可使用 File.getFreeSpace() 和 File.getTotalSpace() 查询存储空间。在写入前判断空间大小可以避免空间不足。但系统不能保证你一定能够使用 getFreeSpace() 返回的大小——如果所需空间远小于剩余空间,或者系统使用率未达 90%,一般是安全的;否则应当谨慎写入。\n实际上,你并不需要在保存文件前一定检查可用空间。另一种做法是直接尝试写入,捕获 IOException。这在不确定所需空间大小时尤其有用(例如将 PNG 转为 JPEG 后文件大小无法预知)。\nSQLite Databases Android 内置 SQLite 数据库引擎,适合存储结构化数据。推荐通过 SQLiteOpenHelper 管理数据库的创建和版本迁移。\n现代 Android 开发推荐使用 Room 持久化库,它在 SQLite 基础上提供了编译时验证、自动迁移、协程支持等便利功能。\nNetwork Connection 通过网络连接将数据存储到远程服务器,适用于需要跨设备访问或云端同步的场景。通常配合 Retrofit、OkHttp 等网络库使用。\nContent Providers ContentProvider 是 Android 组件级别的数据共享机制,允许跨应用访问和操作数据。它封装了底层存储(可以是 SQLite、文件、网络等),通过 URI 接口对外暴露数据。\n参考 Data and file storage overview — Android Developers Shared storage overview — Android Developers Jetpack DataStore — Android Developers Room Persistence Library — Android Developers Content Provider Basics — Android Developers ","permalink":"https://blog.substitute.tech/posts/androiddatastorage/","summary":"\u003cp\u003eAndroid 提供了多种数据持久化方案,不同的方案适用于不同的场景。选择合适的存储方式对于应用的性能、安全性和用户体验至关重要。\u003c/p\u003e","title":"Android Data Storage"},{"content":"App Widget 是 Android 中的一种微型应用视图,可以嵌入到其他应用(如桌面 Home Screen)中,并支持周期性更新。最常见的例子就是桌面 Widget,比如天气小部件、音乐播放器控件等。\n基本架构 一个 App Widget 由以下组件构成:\nAppWidgetProviderInfo:定义 Widget 的元数据(布局、更新频率、尺寸等),在 XML 中声明 AppWidgetProvider:继承自 BroadcastReceiver,处理 Widget 的生命周期事件 View Layout:Widget 的界面布局,使用 RemoteViews 构建 关键生命周期方法 方法 触发时机 onUpdate() 到达更新周期时触发,是唯一必须实现的方法 onAppWidgetOptionsChanged() Widget 尺寸变化时触发 onEnabled(Context) 第一个 Widget 实例被创建时触发 onDisabled(Context) 最后一个 Widget 实例被移除时触发 onDeleted(Context, int[]) Widget 实例被删除时触发 创建步骤 1. 定义 Widget 元信息 res/xml/example_appwidget_info.xml:\n1 2 3 4 5 6 7 \u0026lt;appwidget-provider xmlns:android=\u0026#34;http://schemas.android.com/apk/res/android\u0026#34; android:minWidth=\u0026#34;250dp\u0026#34; android:minHeight=\u0026#34;40dp\u0026#34; android:updatePeriodMillis=\u0026#34;86400000\u0026#34; android:initialLayout=\u0026#34;@layout/example_appwidget\u0026#34; android:resizeMode=\u0026#34;horizontal|vertical\u0026#34; android:widgetCategory=\u0026#34;home_screen\u0026#34; /\u0026gt; 2. 创建布局 Widget 布局使用 RemoteViews,支持的控件有限(主要是 FrameLayout、LinearLayout、RelativeLayout、GridLayout、Button、TextView、ImageView 等):\nres/layout/example_appwidget.xml:\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;LinearLayout xmlns:android=\u0026#34;http://schemas.android.com/apk/res/android\u0026#34; android:layout_width=\u0026#34;match_parent\u0026#34; android:layout_height=\u0026#34;match_parent\u0026#34; android:orientation=\u0026#34;horizontal\u0026#34;\u0026gt; \u0026lt;TextView android:id=\u0026#34;@+id/example_text\u0026#34; android:layout_width=\u0026#34;0dp\u0026#34; android:layout_height=\u0026#34;match_parent\u0026#34; android:layout_weight=\u0026#34;1\u0026#34; android:text=\u0026#34;Hello Widget!\u0026#34; /\u0026gt; \u0026lt;/LinearLayout\u0026gt; 3. 实现 Provider 1 2 3 4 5 6 7 8 9 10 11 public class ExampleAppWidgetProvider extends AppWidgetProvider { public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { for (int appWidgetId : appWidgetIds) { RemoteViews views = new RemoteViews( context.getPackageName(), R.layout.example_appwidget); views.setTextViewText(R.id.example_text, \u0026#34;Updated!\u0026#34;); appWidgetManager.updateAppWidget(appWidgetId, views); } } } 4. 在 AndroidManifest 中声明 1 2 3 4 5 6 7 8 \u0026lt;receiver android:name=\u0026#34;.ExampleAppWidgetProvider\u0026#34;\u0026gt; \u0026lt;intent-filter\u0026gt; \u0026lt;action android:name=\u0026#34;android.appwidget.action.APPWIDGET_UPDATE\u0026#34; /\u0026gt; \u0026lt;/intent-filter\u0026gt; \u0026lt;meta-data android:name=\u0026#34;android.appwidget.provider\u0026#34; android:resource=\u0026#34;@xml/example_appwidget_info\u0026#34; /\u0026gt; \u0026lt;/receiver\u0026gt; 局限性 Widget 不能使用自定义 View 或 View 子类——只能使用 RemoteViews 支持的控件 Widget 的更新频率由系统管理,updatePeriodMillis 最小值为 30 分钟 使用 ListView 或 GridView 时需要配合 RemoteViewsService 提供数据 参考 Create a simple widget — Android Developers App Widgets Overview — Android Developers AppWidgetProvider API Reference App Widget Design Guidelines Jetpack Glance (Compose-based Widgets) ","permalink":"https://blog.substitute.tech/posts/androidappwidgets/","summary":"\u003cp\u003eApp Widget 是 Android 中的一种微型应用视图,可以嵌入到其他应用(如桌面 Home Screen)中,并支持周期性更新。最常见的例子就是桌面 Widget,比如天气小部件、音乐播放器控件等。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"appwidget\" loading=\"lazy\" src=\"/images/appwidget.png\"\u003e\u003c/p\u003e","title":"Android App Widgets"},{"content":" 注意:本文写于 2015 年,部分工具链和版本信息已过时,仅供历史参考。当前 Android 开发请直接使用 Android Studio 和 Android Developers 官方文档。\n给他人总结的一份 Android 开发环境配置清单(未提及版本号的均使用最新版本):\n工具下载 Google Android Developer (官网): https://developer.android.com/sdk/index.html(链接可能已失效,请访问新版入口) AndroidDevTools (国内镜像,方便无法翻墙的用户): http://www.androiddevtools.cn/(链接可能已失效) 必需工具 JDK:建议安装 1.7+,搜索安装即可。(注:2019 年起 Android 已要求 JDK 11+) Gradle:Android 构建系统的核心。建议下载离线 Gradle 发行版,可在 IDE 中配置本地路径以加速构建 Android SDK Tools:最新版 SDK 通常已自带代理配置;如果下载失败,可使用国内镜像 Android Studio:使用最新版本即可 其他工具:根据实际需要自行选择 注意事项 现在(2015 年)非常不建议使用 Eclipse。单从生产效率上讲,Android Studio 优势非常大。\n这个判断至今依然成立——Android Studio 作为官方 IDE,在代码编辑、调试、性能分析、构建管理等方面远超 Eclipse ADT。\n参考 Android Studio 官方下载 Android SDK 文档 Gradle 官方下载 ","permalink":"https://blog.substitute.tech/posts/androiddeveloperrequirements/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e注意\u003c/strong\u003e:本文写于 2015 年,部分工具链和版本信息已过时,仅供历史参考。当前 Android 开发请直接使用 \u003ca href=\"https://developer.android.com/studio\"\u003eAndroid Studio\u003c/a\u003e 和 \u003ca href=\"https://developer.android.com/\"\u003eAndroid Developers 官方文档\u003c/a\u003e。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"Android Developer Requirements"},{"content":"设计模式(Design Patterns)是面向对象设计中针对常见问题的可复用解决方案。1994 年由 GoF (Gang of Four: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides) 在 Design Patterns: Elements of Reusable Object-Oriented Software 一书中系统总结,共 23 种经典模式。\n本文整理自 smallnest 的博客文章,以及 StackOverflow 上关于 GoF 设计模式在 Java API 中应用实例的讨论。\n查看PDF概览图\n设计模式不是代码库,而是解决问题的\u0026quot;思路框架\u0026quot;。理解模式的关键在于知道 \u0026ldquo;何时使用\u0026rdquo; 而非 \u0026ldquo;如何实现\u0026rdquo;。\n创建型模式 (Creational Patterns) 创建型模式抽象了对象的实例化过程,使系统与如何创建、组合和表示对象相独立。\n抽象工厂 (Abstract Factory) 识别方式:创建方法返回工厂本身,该工厂可进一步用于创建其他抽象/接口类型。\nJava API 示例:\njavax.xml.parsers.DocumentBuilderFactory#newInstance() javax.xml.transform.TransformerFactory#newInstance() javax.xml.xpath.XPathFactory#newInstance() 建造者模式 (Builder) 识别方式:创建方法返回实例本身,支持链式调用。\nJava API 示例:\njava.lang.StringBuilder#append() (非线程安全) java.lang.StringBuffer#append() (线程安全) java.nio.ByteBuffer#put() (以及 CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer) javax.swing.GroupLayout.Group#addComponent() 所有实现了 java.lang.Appendable 的类 工厂方法 (Factory Method) 识别方式:创建方法返回某个抽象/接口类型的实现(非工厂本身)。\nJava API 示例:\njava.util.Calendar#getInstance() java.util.ResourceBundle#getBundle() java.text.NumberFormat#getInstance() java.nio.charset.Charset#forName() java.net.URLStreamHandlerFactory#createURLStreamHandler(String) (每个协议返回单例) 原型模式 (Prototype) 识别方式:创建方法返回自身的一个新实例,属性与原实例相同。\nJava API 示例:\njava.lang.Object#clone() (类需实现 java.lang.Cloneable) 单例模式 (Singleton) 识别方式:创建方法每次返回同一个实例。\nJava API 示例:\njava.lang.Runtime#getRuntime() java.awt.Desktop#getDesktop() 结构型模式 (Structural Patterns) 结构型模式关注类和对象的组合方式,以形成更大的结构。\n适配器模式 (Adapter) 识别方式:创建方法接收一个不同抽象/接口类型的实例,返回包装/覆盖了该实例的当前抽象/接口类型的实现。\nJava API 示例:\njava.util.Arrays#asList() java.io.InputStreamReader(InputStream) (返回 Reader) java.io.OutputStreamWriter(OutputStream) (返回 Writer) javax.xml.bind.annotation.adapters.XmlAdapter#marshal() 和 #unmarshal() 桥接模式 (Bridge) 识别方式:创建方法接收一个不同抽象/接口类型的实例,返回使用/委托该实例的当前抽象/接口类型的实现。\nJava API 示例:\nJDBC、JNDI、JCE 等 API 是桥接模式的典型应用。——尚未找到直接的 JDK 类示例。java.util.Collections#newSetFromMap() 和 singletonXXX() 方法较为接近。\n组合模式 (Composite) 识别方式:行为方法接收相同抽象/接口类型的实例以形成树形结构。\nJava API 示例:\njava.awt.Container#add(Component) (Swing 中随处可见) javax.faces.component.UIComponent#getChildren() (JSF 中随处可见) 装饰器模式 (Decorator) 识别方式:创建方法接收相同抽象/接口类型的实例并添加额外行为。\nJava API 示例:\njava.io.InputStream、OutputStream、Reader、Writer 的所有子类均有接收同类型实例的构造方法 java.util.Collections 的 checkedXXX()、synchronizedXXX()、unmodifiableXXX() 方法 javax.servlet.http.HttpServletRequestWrapper 和 HttpServletResponseWrapper 外观模式 (Facade) 识别方式:行为方法内部使用多个不同的独立抽象/接口类型的实例。\nJava API 示例:\njavax.faces.context.FacesContext — 内部使用 LifeCycle、ViewHandler、NavigationHandler 等多个抽象类型 javax.faces.context.ExternalContext — 内部使用 ServletContext、HttpSession、HttpServletRequest 等 享元模式 (Flyweight) 识别方式:创建方法返回缓存实例,类似\u0026quot;多例\u0026quot;模式。\nJava API 示例:\njava.lang.Integer#valueOf(int) (Boolean、Byte、Character、Short、Long 同理) 字符串常量池 代理模式 (Proxy) 识别方式:创建方法返回给定抽象/接口类型的实现,该实现内部委托/使用另一个实现。\nJava API 示例:\njava.lang.reflect.Proxy java.rmi.* 全部 API 行为型模式 (Behavioral Patterns) 行为型模式关注对象之间的责任分配和算法交互。\n责任链模式 (Chain of Responsibility) 识别方式:行为方法(间接)在队列中调用另一个同类型实现的相同方法。\nJava API 示例:\njava.util.logging.Logger#log() javax.servlet.Filter#doFilter() 命令模式 (Command) 识别方式:抽象/接口类型中的行为方法,调用在创建时被封装到命令实现中的另一个类型的方法。\nJava API 示例:\n所有实现了 java.lang.Runnable 的类 所有实现了 javax.swing.Action 的类 解释器模式 (Interpreter) 识别方式:行为方法返回与给定实例/类型结构不同的实例/类型。\nJava API 示例:\njava.util.Pattern java.text.Normalizer java.text.Format 的所有子类 javax.el.ELResolver 的所有子类 迭代器模式 (Iterator) 识别方式:行为方法按顺序从队列中返回另一种类型的实例。\nJava API 示例:\n所有实现了 java.util.Iterator 的类 所有实现了 java.util.Enumeration 的类 中介者模式 (Mediator) 识别方式:行为方法接收一个(通常使用命令模式封装的)抽象/接口类型的实例,并委托/使用该实例。\nJava API 示例:\njava.util.Timer (所有 scheduleXXX() 方法) java.util.concurrent.Executor#execute() java.util.concurrent.ExecutorService (invokeXXX() 和 submit() 方法) java.util.concurrent.ScheduledExecutorService (所有 scheduleXXX() 方法) java.lang.reflect.Method#invoke() 备忘录模式 (Memento) 识别方式:行为方法内部改变整个实例的状态。\nJava API 示例:\njava.util.Date (setter 方法改变内部 long 值) 所有实现了 java.io.Serializable 的类 所有实现了 javax.faces.component.StateHolder 的类 观察者模式 (Observer / Publish-Subscribe) 识别方式:行为方法根据自身状态调用另一个抽象/接口类型实例的方法。\nJava API 示例:\njava.util.Observer / java.util.Observable (实际使用较少) 所有实现了 java.util.EventListener 的类 javax.servlet.http.HttpSessionBindingListener javax.servlet.http.HttpSessionAttributeListener javax.faces.event.PhaseListener 状态模式 (State) 识别方式:行为方法根据实例的状态改变其行为,且状态可被外部控制。\nJava API 示例:\njavax.faces.lifecycle.LifeCycle#execute() (行为取决于当前 JSF 生命周期阶段) 策略模式 (Strategy) 识别方式:抽象/接口类型中的行为方法调用了另一抽象/接口类型实例的方法,该实例作为参数传入策略实现。\nJava API 示例:\njava.util.Comparator#compare() (被 Collections#sort() 调用) javax.servlet.http.HttpServlet 的 service() 及所有 doXXX() 方法 javax.servlet.Filter#doFilter() 模板方法模式 (Template Method) 识别方式:抽象类型中已经定义了\u0026quot;默认\u0026quot;行为的行为方法。\nJava API 示例:\njava.io.InputStream、OutputStream、Reader、Writer 的所有非抽象方法 java.util.AbstractList、AbstractSet、AbstractMap 的所有非抽象方法 javax.servlet.http.HttpServlet 的所有 doXXX() 方法默认返回 HTTP 405 错误 访问者模式 (Visitor) 识别方式:两个不同的抽象/接口类型各自定义了接收对方类型的方法,一方调用另一方的方法执行策略。\nJava API 示例:\njavax.lang.model.element.AnnotationValue 和 AnnotationValueVisitor javax.lang.model.element.Element 和 ElementVisitor javax.lang.model.type.TypeMirror 和 TypeVisitor 参考 原文: smallnest/设计模式速查表 Design Patterns: Elements of Reusable Object-Oriented Software — GoF 著 CMU 课程: \u0026ldquo;23 Patterns in 80 Minutes\u0026rdquo; by Josh Bloch GoF 设计模式 Java 示例 — Alibaba Cloud Java Design Patterns — Wikipedia ","permalink":"https://blog.substitute.tech/posts/designpatterns/","summary":"\u003cp\u003e设计模式(Design Patterns)是面向对象设计中针对常见问题的可复用解决方案。1994 年由 GoF (Gang of Four: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides) 在 \u003cem\u003eDesign Patterns: Elements of Reusable Object-Oriented Software\u003c/em\u003e 一书中系统总结,共 23 种经典模式。\u003c/p\u003e\n\u003cp\u003e本文整理自 \u003ca href=\"https://github.com/smallnest\"\u003esmallnest\u003c/a\u003e 的博客文章,以及 StackOverflow 上关于 GoF 设计模式在 Java API 中应用实例的讨论。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"javagofdesignpatterns\" loading=\"lazy\" src=\"/images/javagofdesignpatterns.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"../source/pdf/design-pattern-scard.pdf\"\u003e查看PDF概览图\u003c/a\u003e\u003c/p\u003e","title":"Design Patterns"},{"content":"Chrome DevTools 支持远程调试 Android 设备上的网页和 WebView 应用。这一功能对于移动端前端开发和混合应用开发来说非常实用,可以让我们使用桌面 Chrome 的完整开发者工具来调试移动端页面。\n前置要求 开发机上安装 Chrome 32+ USB 数据线连接 Android 设备 浏览器调试: Android 4.0+ 且安装了 Chrome for Android WebView 调试: Android 4.4+ (KitKat) 且 WebView 已配置为可调试模式 操作步骤 1. 连接设备 打开设备的开发者模式(设置 \u0026gt; 关于手机 \u0026gt; 连续点击版本号),确保 USB 调试 已启用,然后用数据线连接手机。\n2. 打开 Chrome 检查页面 在桌面 Chrome 地址栏输入 chrome://inspect,确保 Discover USB devices 已勾选。设备首次连接时可能会弹出授权确认,点击确定即可。\n3. 检查浏览器标签页 此时 Chrome 会列出设备上已打开的浏览器标签页,点击 inspect 即可打开 DevTools 调试界面。\n4. 调试 WebView 应用 如果想调试自己的应用中的 WebView,需要在代码中添加:\n1 2 3 if (Build.VERSION.SDK_INT \u0026gt;= Build.VERSION_CODES.KITKAT) { WebView.setWebContentsDebuggingEnabled(true); } 生产环境中建议仅在 debug 构建中启用,或通过编译开关控制。\n5. 实时显示设备屏幕 点击 DevTools 中的 Screencast 图标,可以实时查看设备屏幕,并在桌面上直接操作。\n调试技巧 按 F5 (Mac 上为 Cmd+R) 从 DevTools 刷新远程页面 使设备使用 蜂窝网络连接,在 Network 面板中查看真实移动网络下的网络瀑布图 使用 Timeline 面板 分析渲染和 CPU 性能——移动设备的硬件通常远慢于开发机 如果开发服务器无法从设备访问,使用端口转发(Port Forwarding)或虚拟主机映射(Virtual Host Mapping) 端口转发 手机通常无法直接访问开发服务器(它们可能处于不同的网络,或开发机处于受限的企业网络中)。Chrome 的端口转发功能通过在手机上创建一个监听 TCP 端口,将流量通过 USB 转发到开发机的指定端口,从而解决这个问题。\n设置步骤:\n打开 chrome://inspect 点击 Port Forwarding 在 Device port 中填写设备要监听的端口(默认 8080) 在 Host 中填写开发服务器的 IP 和端口(端口必须在 1024-65535 之间) 勾选 Enable port forwarding 点击 Done 虚拟主机映射 如果需要使用自定义域名访问本地开发服务器:\n在开发机上安装代理软件(如 Charles Proxy 或 Squid),运行代理并记下端口号 在 chrome://inspect 中设置端口转发,Device port 填写 9000,Host 填写 localhost:代理端口 在 Android 设备的 Wi-Fi 设置中手动配置代理为 localhost:9000 常见问题 设备未显示在 chrome://inspect 页面\nWindows 用户需确认安装了正确的 USB 驱动,参见 OEM USB Drivers 确保设备直接连接到电脑(不要经过 USB Hub) 确认已启用 USB 调试,并在设备上同意 USB 调试授权 桌面 Chrome 版本需高于设备上的 Chrome for Android 版本,可尝试 Chrome Canary 如果仍然不行,在设备上进入 设置 \u0026gt; 开发者选项 \u0026gt; 撤销 USB 调试授权,重新连接 浏览器标签页未显示\n在设备上打开 Chrome 并加载要调试的网页,然后刷新 chrome://inspect WebView 未显示\n确认 WebView 调试已启用(setWebContentsDebuggingEnabled(true)) 在设备上打开包含 WebView 的应用,然后刷新 chrome://inspect 无法访问开发服务器\n尝试启用端口转发或设置虚拟主机映射 如果上述方法都无效,可以使用 adb 工具的传统方案作为备选。\n参考 Remote debugging WebViews — Chrome DevTools Debugging Web Apps — Android Developers OEM USB Drivers — Android Developers Chrome DevTools Remote Debugging ","permalink":"https://blog.substitute.tech/posts/remotedebuggingonandroidwithchrome/","summary":"\u003cp\u003eChrome DevTools 支持远程调试 Android 设备上的网页和 WebView 应用。这一功能对于移动端前端开发和混合应用开发来说非常实用,可以让我们使用桌面 Chrome 的完整开发者工具来调试移动端页面。\u003c/p\u003e","title":"Remote Debugging on Android with Chrome"},{"content":"贝塞尔曲线(Bézier Curve)被广泛应用于计算机图形学中,用于为平滑曲线建立模型。它以法国工程师 Pierre Bézier (雷诺汽车公司) 和 Paul de Casteljau (雪铁龙汽车公司) 命名,两人在 1960 年代独立开发了这一曲线表示方法。\n数学定义 贝塞尔曲线由给定控制点 P0、P1、\u0026hellip;、Pn 的向量函数 B(t) 追踪,参数 t 的取值范围为 [0, 1]。通用的 n 阶贝塞尔曲线公式为:\n$$\\mathbf{B}(t) = \\sum_{i=0}^{n} \\binom{n}{i} t^{i} (1-t)^{n-i} \\mathbf{P}_i \\quad , \\quad t \\in [0, 1]$$\n常用阶数 一阶(线性)贝塞尔曲线 两个控制点 P0、P1,就是两点之间的一条直线:\n$$\\mathbf{B}(t)=\\mathbf{P}_0 + (\\mathbf{P}_1-\\mathbf{P}_0)t=(1-t)\\mathbf{P}_0 + t\\mathbf{P}_1 \\mbox{ , } t \\in [0,1]$$\n1 2 3 private float getLinearBezier(float t, float p0, float p1) { return (1 - t) * p0 + t * p1; } 二阶(二次)贝塞尔曲线 三个控制点 P0、P1、P2:\n$$\\mathbf{B}(t) = (1 - t)^{2}\\mathbf{P}_0 + 2t(1 - t)\\mathbf{P}_1 + t^{2}\\mathbf{P}_2 \\mbox{ , } t \\in [0,1]$$\n三阶(三次)贝塞尔曲线 四个控制点 P0、P1、P2、P3,也是实际应用中最常见的:\n$$\\mathbf{B}(t) = (1 - t)^3\\mathbf{P}_0 + 3t(1 - t)^2\\mathbf{P}_1 + 3t^2(1 - t)\\mathbf{P}_2 + t^3\\mathbf{P}_3 \\quad , \\quad t \\in [0, 1]$$\n关键性质 端点插值:曲线始终经过第一个和最后一个控制点: B(0) = P0, B(1) = Pn 切线性质:曲线在起点处与 P0-\u0026gt;P1 相切,在终点处与 Pn-1-\u0026gt;Pn 相切 凸包性质:整个曲线位于其控制点的凸包(Convex Hull)之内 仿射不变性:对控制点做仿射变换(平移、旋转、缩放)后再生成曲线,等价于对曲线本身做相同变换 细分性:一条贝塞尔曲线可以被分割为两条同阶的贝塞尔曲线(de Casteljau 算法) 在 Android 中的应用 在 Android 中,贝塞尔曲线常用于:\n自定义插值器:通过贝塞尔曲线定义动画的变化速率 路径动画:使用 Path 类的 quadTo()(二次)和 cubicTo()(三次)方法创建曲线路径 手势轨迹:通过 GestureOverlayView 或自定义 View 绘制触摸轨迹 矢量图形:在 VectorDrawable 中定义曲线形状 1 2 3 4 5 6 7 // Android Path 中使用三次贝塞尔曲线 Path path = new Path(); path.moveTo(startX, startY); path.cubicTo(control1X, control1Y, control2X, control2Y, endX, endY); // 使用二次贝塞尔曲线 path.quadTo(controlX, controlY, endX, endY); 参考 Bézier Curve — Wolfram MathWorld Bézier curve — Wikipedia Path API Reference — Android Developers VectorDrawable — Android Developers de Casteljau\u0026rsquo;s algorithm — Wikipedia ","permalink":"https://blog.substitute.tech/posts/beziercurvepractice/","summary":"\u003cp\u003e贝塞尔曲线(Bézier Curve)被广泛应用于计算机图形学中,用于为平滑曲线建立模型。它以法国工程师 Pierre Bézier (雷诺汽车公司) 和 Paul de Casteljau (雪铁龙汽车公司) 命名,两人在 1960 年代独立开发了这一曲线表示方法。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Bézier_3_big\" loading=\"lazy\" src=\"/images/Be%CC%81zier_3_big.svg.png\"\u003e\u003cimg alt=\"Bézier_3_big_gif\" loading=\"lazy\" src=\"/images/Be%CC%81zier_3_big.gif\"\u003e\u003c/p\u003e","title":"Bezier Curve Practice"},{"content":"Property Animation 是 Android 提供的一套非常强大的动画框架,可以在运行时动态改变任意 View (可见或不可见) 的属性.相比传统的 View 动画(补间动画),属性动画真正改变了对象的属性,而不仅仅是绘制效果.\n为什么需要属性动画? Android 3.0 (API 11) 之前的 View 动画(Animation) 存在一个根本缺陷:它只改变 View 的绘制位置,而不改变 View 本身的实际属性。比如把一个 Button 通过平移动画移到屏幕右侧,点击原来的位置依然有效——因为 View 的 click 事件区域并未移动。属性动画则直接改变对象的属性值(如 x、y、alpha),从根本上解决了这个问题。\n核心 API 属性动画的核心类位于 android.animation 包下:\nObjectAnimator 最常用的属性动画类,直接对指定对象的指定属性进行动画:\n1 2 3 ObjectAnimator anim = ObjectAnimator.ofFloat(foo, \u0026#34;alpha\u0026#34;, 0f, 1f); anim.setDuration(1000); anim.start(); ValueAnimator 更底层的动画类,通过监听值的变化手动更新属性:\n1 2 3 4 5 6 7 8 9 10 11 ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = (float) animation.getAnimatedValue(); // 手动应用 value view.setAlpha(value); } }); animator.setDuration(300); animator.start(); AnimatorSet 用于编排多个动画的播放顺序:\n1 2 3 4 AnimatorSet set = new AnimatorSet(); set.play(anim1).before(anim2); set.play(anim2).with(anim3); set.start(); 可配置属性 属性 说明 默认值 Duration 动画持续时长 300ms TimeInterpolator 时间插值器,控制动画变化速率 AccelerateDecelerateInterpolator RepeatCount 重复次数 0 (不重复) RepeatMode 重复模式: RESTART 或 REVERSE RESTART StartDelay 启动延迟 0 FrameRefreshDelay 帧刷新延迟(通常无需修改) 10ms 注意:帧刷新延迟的最终结果不仅取决于设定的值,还受到当前系统性能和资源占用的影响。\n插值器 (Interpolator) 插值器决定了动画的变化速率。Android 内置了多种插值器:\nLinearInterpolator — 匀速 AccelerateDecelerateInterpolator — 先加速后减速 AccelerateInterpolator — 加速 DecelerateInterpolator — 减速 BounceInterpolator — 回弹效果 AnticipateOvershootInterpolator — 先回退再超出目标 你也可以实现 TimeInterpolator 接口来自定义插值器。\nViewPropertyAnimator 如果只需要对一个 View 的多个属性做简单动画,ViewPropertyAnimator 提供了更简洁的链式调用:\n1 2 3 4 5 view.animate() .alpha(0.5f) .translationX(200f) .setDuration(1000) .start(); 动画监听 通过 AnimatorListener 或简化版 AnimatorListenerAdapter 监听动画状态:\n1 2 3 4 5 6 anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { // 动画结束后的处理 } }); 参考 Property Animation Overview — Android Developers Animation Resources — Android Developers ObjectAnimator API Reference ValueAnimator API Reference AnimatorSet API Reference ViewPropertyAnimator API Reference ","permalink":"https://blog.substitute.tech/posts/androidpropertyanimation/","summary":"\u003cp\u003e\u003ccode\u003eProperty Animation\u003c/code\u003e 是 Android 提供的一套非常强大的动画框架,可以在运行时动态改变任意 View (可见或不可见) 的属性.相比传统的 View 动画(补间动画),属性动画真正改变了对象的属性,而不仅仅是绘制效果.\u003c/p\u003e","title":"Android Property Animation"},{"content":"最近排查一个 BUG 时遇到一个奇怪的问题：android.database.sqlite.SQLiteFullException: database or disk is full (code 13)。查阅了很多资料也没能完全弄清楚根因，这里把整理的资料分享出来，希望遇到类似问题的朋友共同探讨。\n最开始怀疑是磁盘空间满了，但从上图信息来看应该不是。然后猜测是 SQLite 存在大小限制。Stack Overflow 上有一个经典的讨论：maximum-number-of-rows-in-a-sqlite-table：\nIn July 2011 the sqlite3 limits page was updated to define the practical limits\u0026hellip;The theoretical maximum number of rows in a table is 2^64 (about 1.8e+19). This limit is unreachable since the maximum database size of 14 terabytes will be reached first. A 14 terabytes database can hold no more than approximately 1e+13 rows\u0026hellip;So with a max database size of 14 terabytes you\u0026rsquo;d be lucky to get ~1 Trillion rows since if you actually had a useful table with data in it the number of rows would be constrained by the size of the data.\n论行数、论体积，我这边的情况都排不上号。接着怀疑是平台相关，查了 Comparison of relational database management systems 也不对。于是逐一排查官方文档 Limits In SQLite，以下是整理出的关键限制清单。\nSQLite 关键限制汇总 字符串 / BLOB 长度 最大支持 2^31 - 1 字节，即 2147483647 字节（约 2 GB）。\n列数 默认最多 2000 列，可在编译时调整为 32767。\nSQL 语句长度 默认限制 1000000 字节，可上调至 1073741824 字节（1 GB）。\nJOIN 中的表数 最多支持 64 个表的连接查询。\n表达式树深度 默认限制 1000，设为 0 则不强制限制。\n函数参数 默认最多 100 个参数。\n复合 SELECT 的项数 默认值 500。\nLIKE / GLOB 模式长度 默认 50000 字节。官方特别标注了时间复杂度为 O(N^2)，N 为字符总数。\nSQL 语句中的宿主参数 默认 999 个。\n触发器递归深度 从 v3.6.18 开始支持，v3.7.0 起默认值为 1000。\n附加的数据库数 默认最多附加 10 个数据库，最大不超过 125 个。\n数据库文件页数 一般设置为 1073741823，最大为 2147483646。配合最大页大小 65536 字节，最大数据库体积约为 140 TB（新版 SQLite 的理论上限已达 281 TB）。\n表中的行数 理论上限为 2^64（约 1.8e+19），实际上在此之前会先达到文件大小上限。\n数据库最大体积 每个数据库由一个或多个\u0026quot;页\u0026quot;组成。页大小可以是 512 到 65536 字节之间（2 的幂）。数据库文件最大为 2147483646 页。在最大页大小 65536 字节下，最大数据库体积约 140 TB（或 128 TiB）。注：自 SQLite 3.45.0 起，页数上限提升至 2^32 - 2，对应最大体积约 281 TB。\n排查方向 从以上限制来看，SQLite 自身的上限远高于实际场景，所以方向需要调整。以下两个方向值得关注。\n数据库体积与 VACUUM 如果你删除了大量数据但数据库文件大小没有减小——这不是 BUG。SQLite 将被释放的空间放入内部的\u0026quot;空闲列表\u0026quot;（free-list），下一次插入数据时优先复用，但不会返还给操作系统。\n如果希望缩小数据库文件，可以执行 VACUUM 命令：\n1 VACUUM; VACUUM 会从头重建数据库，生成一个空闲列表为空的最小体积文件。但需要注意，VACUUM 执行时需要最多 2 倍于原文件的临时磁盘空间，且耗时较长。\n作为替代方案，可以启用自动清理模式：\n1 PRAGMA auto_vacuum = FULL; -- 1: fully auto-vacuum 需要权衡的是，auto_vacuum 虽然自动回收空闲页，但可能导致更严重的碎片化，且不会像 VACUUM 那样压缩未完全使用的页。\n数据库最大容量限制（Android 平台） 如果数据库来自第三方或底层框架，可能显式限制了最大体积。可以通过以下方式检查：\n1 2 SQLiteDatabase db = getWritableDatabase(); long maxSize = db.getMaximumSize(); // 获取当前最大容量 确认限制后，可以酌情调用 setMaximumSize() 放宽限制，或做兼容处理。\n相关讨论：Bugly SQLiteFullException（链接可能已失效）\n如果你有类似经验或知道其他可能的原因，请多指教。\n参考资料 Limits In SQLite (sqlite.org) VACUUM Command (sqlite.org) Maximum number of rows in a SQLite table (Stack Overflow) Comparison of relational database management systems (Wikipedia) SQLiteFullException (Android Developers) Performance characteristics of SQLite with very large database files (Stack Overflow) ","permalink":"https://blog.substitute.tech/posts/limitsinsqlite/","summary":"\u003cp\u003e最近排查一个 BUG 时遇到一个奇怪的问题：\u003ccode\u003eandroid.database.sqlite.SQLiteFullException: database or disk is full (code 13)\u003c/code\u003e。查阅了很多资料也没能完全弄清楚根因，这里把整理的资料分享出来，希望遇到类似问题的朋友共同探讨。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"limitsinsqlite01\" loading=\"lazy\" src=\"/images/limitsinsqlite01.png\"\u003e\u003c/p\u003e","title":"SQLite 限制详解：从一次 SQLiteFullException 说起"},{"content":"本文记录了 Go 语言入门过程中最基础的四个环节：编写第一个程序、创建工具库、编写单元测试，以及使用远程包。内容基于 Go 1.x 初期的 GOPATH 模式编写。\n注意：自 Go 1.11 起引入了模块（Modules）机制，Go 1.16 开始默认启用模块模式，GOPATH 模式已被取代。现在开发新项目应当使用 go mod init 创建模块，而非手动设置 GOPATH。下文示例保留原始风格以作参考。\n第一个程序 安装 Go 的 pkg 包后，需要配置环境变量。建议在用户目录下创建一个 go 文件夹用于开发：\n1 2 export GOPATH=$HOME/go export PATH=$PATH:$GOPATH/bin 文档中虽未强制要求这些步骤，但如果不这样做，排查问题会非常痛苦。按照 Go 的约定创建目录结构，方便后续提交代码。在 $GOPATH/src 下创建路径 info.haoxiqiang/first/hello，然后在其中创建 hello.go：\n1 2 3 4 5 6 7 package main import \u0026#34;fmt\u0026#34; func main() { fmt.Printf(\u0026#34;hello, go\\n\u0026#34;) } 要能直接运行，必须保证至少存在一个包名为 main 的包。有三种运行方式：\n直接运行：go run hello.go 编译安装（省略 src 路径）：go install info.haoxiqiang/first/hello 在源码目录直接执行 go install 运行成功不会有任何报错信息，同时在 $GOPATH/bin 中会生成可执行文件。可以进入 bin 目录执行 ./hello，也可以在任意位置执行 $GOPATH/bin/hello。\n工具库 在实际项目中，经常会用到工具类或第三方库。下面展示基本的 Go library 用法。创建目录和源文件：\n1 2 mkdir -p $GOPATH/src/info.haoxiqiang/tool/stringutil vi util1.go 1 2 3 4 5 6 7 8 9 package stringutil func Reverse(s string) string { r := []rune(s) for i, j := 0, len(s)-1; i \u0026lt; len(s)/2; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] } return string(r) } 需要注意，包名就是引用时使用的名称。调用方式如下：\n1 2 3 4 5 6 7 8 9 10 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;info.haoxiqiang/tool/stringutil\u0026#34; ) func main() { fmt.Printf(stringutil.Reverse(\u0026#34;123456789\\n\u0026#34;)) } 单元测试 Go 自带一个轻量级的测试框架，使用起来非常方便。测试文件建议命名为 xxx_test.go 的格式：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package stringutil import \u0026#34;testing\u0026#34; func TestReverse(t *testing.T) { cases := []struct { in, want string }{ {\u0026#34;Hello, world\u0026#34;, \u0026#34;dlrow ,olleH\u0026#34;}, {\u0026#34;Hello, 世界\u0026#34;, \u0026#34;界世 ,olleH\u0026#34;}, {\u0026#34;\u0026#34;, \u0026#34;\u0026#34;}, } for _, c := range cases { got := Reverse(c.in) if got != c.want { t.Errorf(\u0026#34;Reverse(%q) == %q, want %q\u0026#34;, c.in, got, c.want) } } } 运行测试：\n1 2 go test info.haoxiqiang/tool/stringutil ok info.haoxiqiang/tool/stringutil\t0.005s 这种表格驱动测试（table-driven test）是 Go 社区广泛采用的风格，通过定义结构体切片来覆盖多个测试用例，既简洁又易于扩展。\n远程包 Go 原生支持远程包。可以直接获取官方示例：\n1 go get github.com/golang/example/hello 这会在本地 $GOPATH/src 下创建对应的目录结构和文件。执行 $GOPATH/bin/hello 就能看到输出 Hello, Go examples!。\n同样可以在 import 中直接引用远程包，go get 会自动抓取、构建和安装：\n1 2 3 4 import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/golang/example/stringutil\u0026#34; ) 1 2 3 go get info.haoxiqiang/first/hello go install info.haoxiqiang/first/hello bin/hello 源码：https://github.com/Haoxiqiang/go-practise\n参考资料 How to Write Go Code (golang.org) Effective Go (golang.org) A Tour of Go (tour.golang.org) Go Testing Package (pkg.go.dev) Go Modules Reference (go.dev) Go Wiki: GOPATH (go.googlesource.com) Migrating to Go Modules (go.dev) ","permalink":"https://blog.substitute.tech/posts/go1/","summary":"\u003cp\u003e本文记录了 Go 语言入门过程中最基础的四个环节：编写第一个程序、创建工具库、编写单元测试，以及使用远程包。内容基于 Go 1.x 初期的 GOPATH 模式编写。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e注意\u003c/strong\u003e：自 Go 1.11 起引入了模块（Modules）机制，Go 1.16 开始默认启用模块模式，GOPATH 模式已被取代。现在开发新项目应当使用 \u003ccode\u003ego mod init\u003c/code\u003e 创建模块，而非手动设置 GOPATH。下文示例保留原始风格以作参考。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"Go 入门：第一个程序与基础实践"},{"content":"通知系统是 Android 平台上用户与应用交互的重要通道——它能在应用不处于前台时告知用户重要事件，如来消息或日历提醒。Notification 本身在 Android 4.1 (Jelly Bean) 经历过一次重大升级，后续在 5.0 (Lollipop) 又有诸多细节改进。从 4.1 开始，Android 支持在通知底部附加操作按钮，用户无需打开应用即可直接执行常见任务，配合滑出清除，使通知抽屉的体验更加顺滑。\n注意：本文基于 Android 4.1—5.0 时代的 API 编写。自 Android 8.0 (API 26) 起，所有通知必须归属到通知渠道（Notification Channel）；Android 13 (API 33) 起需要运行时权限 POST_NOTIFICATIONS。下文代码示例使用 NotificationCompat 以保证对低版本的兼容性，在不同设备上效果可能略有差异。\n基础用法 所有示例均通过 android.support.v4.app.NotificationCompat 实现。创建一个最基本的通知只需要几行代码：\n1 2 3 4 5 6 7 8 9 10 public static void showNotification(Context context, int mNotificationId) { NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context) .setSmallIcon(R.mipmap.ic_launcher) .setContentTitle(\u0026#34;SimpleNotification\u0026#34;) .setContentText(\u0026#34;Hello World! This is the first notification.\u0026#34;); NotificationManager mNotifyMgr = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); mNotifyMgr.notify(mNotificationId, mBuilder.build()); } 点击行为与 Activity 导航 如果希望用户点击通知后跳转到应用内的某个页面，需要为通知设置一个 PendingIntent：\n1 2 3 4 5 6 Intent resultIntent = new Intent(context, ResultActivity.class); // 因为是通知触发的\u0026#34;特殊\u0026#34;Activity，无需构建人工返回栈 PendingIntent resultPendingIntent = PendingIntent.getActivity( context, 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT); // ... mBuilder.setContentIntent(resultPendingIntent); 这里有一个容易忽略的细节：android:excludeFromRecents 可以控制 Activity 是否出现在最近任务列表中。\n1 2 3 4 \u0026lt;activity android:name=\u0026#34;.ResultActivity\u0026#34; android:launchMode=\u0026#34;singleTask\u0026#34; android:taskAffinity=\u0026#34;\u0026#34; android:excludeFromRecents=\u0026#34;false\u0026#34;/\u0026gt; 通知所指向的页面通常分为两种场景。\n常规 Activity（带返回栈） 适用于通知启动的是应用工作流中的某个环节，用户应当能够按返回键回到上一级页面。使用 TaskStackBuilder 构建完整的返回栈：\n1 2 3 4 5 6 7 8 9 Intent resultIntent = new Intent(context, ParentActivity.class); TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); // 添加返回栈 stackBuilder.addParentStack(ParentActivity.class); // 将 Intent 放入栈顶 stackBuilder.addNextIntent(resultIntent); // 获取包含完整返回栈的 PendingIntent PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); 特定 Activity（无返回栈） 适用于用户只能从通知进入的页面，相当于通知的扩展，展示通知本身难以容纳的信息：\n1 2 3 4 5 Intent notifyIntent = new Intent(); notifyIntent.setComponent(new ComponentName(context, NewTaskActivity.class)); notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent notifyPendingIntent = PendingIntent.getActivity( context, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT); 更新与取消通知 避免每次都生成全新的通知。应当尽量更新已有的通知——要么修改内容，要么增加信息。要实现可更新的通知，发布时需通过 NotificationManager.notify(ID, notification) 指定一个唯一 ID。更新时只需修改或重建 NotificationCompat.Builder 对象，然后以相同的 ID 再次发布：\n1 mBuilder.setNumber(20); // 通知数字将显示为 20 通知的消失由以下几种情况控制：\n用户手动清除单条通知，或点击\u0026quot;清除所有\u0026quot;（前提是通知可被清除） 创建时调用了 setAutoCancel() 且用户点击了该通知 调用了 NotificationManager.cancel(ID)，这也会移除正在进行的通知 调用了 NotificationManager.cancelAll()，移除所有此前发布的通知 通知样式 Android 4.1 引入了大视图（Big Views），让通知能够展示更多内容。\nBigTextStyle 展示大段文字：\n1 2 3 4 5 6 .setStyle(new NotificationCompat.BigTextStyle() .setBigContentTitle(\u0026#34;BigContentTitle\u0026#34;) .setSummaryText(\u0026#34;SummaryText\u0026#34;) .bigText(\u0026#34;I\u0026#39;m a big text message\u0026#34;)) .addAction(R.mipmap.ic_stat_dismiss, \u0026#34;dismiss\u0026#34;, notifyPendingIntent) .addAction(R.mipmap.ic_stat_snooze, \u0026#34;snooze\u0026#34;, notifyPendingIntent); BigPictureStyle 展示大图：\n1 2 3 4 .setStyle(new NotificationCompat.BigPictureStyle() .setBigContentTitle(\u0026#34;BigContentTitle\u0026#34;) .setSummaryText(\u0026#34;SummaryText\u0026#34;) .bigPicture(bitmapDrawable.getBitmap())); InboxStyle 展示多条消息列表：\n1 2 3 4 5 6 7 .setStyle(new NotificationCompat.InboxStyle() .setBigContentTitle(\u0026#34;BigContentTitle\u0026#34;) .setSummaryText(\u0026#34;SummaryText\u0026#34;) .addLine(\u0026#34;aaaaaaaaaaaaaaaaa\u0026#34;) .addLine(\u0026#34;bbbbbbbbbbbbbbbbb\u0026#34;) .addLine(\u0026#34;ccccccccccccccccc\u0026#34;) .addLine(\u0026#34;ddddddddddddddddd\u0026#34;)); 进度条通知 通知可以包含进度条。如果能够估算操作总时长和当前进度，使用 determinate 模式显示百分比进度；否则使用 indeterminate 模式显示连续动画进度。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 在后台线程中执行耗时操作 new Thread( new Runnable() { @Override public void run() { int incr; for (incr = 0; incr \u0026lt;= 100; incr += 5) { mBuilder.setProgress(100, incr, false); mNotifyManager.notify(mNotificationId, mBuilder.build()); try { Thread.sleep(5 * 1000); } catch (InterruptedException e) { Log.d(\u0026#34;showNotificationWithDeterminate\u0026#34;, \u0026#34;sleep failure\u0026#34;); } } mBuilder.setContentText(\u0026#34;Download complete\u0026#34;) .setProgress(0, 0, false); // 移除进度条 mNotifyManager.notify(mNotificationId, mBuilder.build()); } }).start(); // indeterminate 模式 // .setProgress(0, 0, true); 参考资料 Android Notification Overview (developer.android.com) Create a Notification (developer.android.com) Start an Activity from a Notification (developer.android.com) NotificationCompat API Reference (developer.android.com) NotificationCompat.Builder API Reference (developer.android.com) TaskStackBuilder API Reference (developer.android.com) Notifications Design Guide (developer.android.com) ","permalink":"https://blog.substitute.tech/posts/androidnotification/","summary":"\u003cp\u003e通知系统是 Android 平台上用户与应用交互的重要通道——它能在应用不处于前台时告知用户重要事件，如来消息或日历提醒。\u003ccode\u003eNotification\u003c/code\u003e 本身在 Android 4.1 (Jelly Bean) 经历过一次重大升级，后续在 5.0 (Lollipop) 又有诸多细节改进。从 4.1 开始，Android 支持在通知底部附加操作按钮，用户无需打开应用即可直接执行常见任务，配合滑出清除，使通知抽屉的体验更加顺滑。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e注意\u003c/strong\u003e：本文基于 Android 4.1—5.0 时代的 API 编写。自 Android 8.0 (API 26) 起，所有通知必须归属到通知渠道（Notification Channel）；Android 13 (API 33) 起需要运行时权限 \u003ccode\u003ePOST_NOTIFICATIONS\u003c/code\u003e。下文代码示例使用 \u003ccode\u003eNotificationCompat\u003c/code\u003e 以保证对低版本的兼容性，在不同设备上效果可能略有差异。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cimg alt=\"notification01\" loading=\"lazy\" src=\"/images/notification01.jpg\"\u003e\u003c/p\u003e","title":"Android 通知机制详解"},{"content":" 注：此文写于 2015 年，文中部分链接可能已失效。资源推荐反映了当时的生态，许多内容已有更新版本，但核心的学习路径和方法论仍然有参考价值。\n前言 谁无年少时。对于一门技术而言，大家的起点都是 Hello World。让人困惑的是，一年下来有些人的技术提升是 100%，有些人是 20%，有些人或许悲催地没有什么变化。是智商的原因吗？在我看来，可能是学习方法不对。\n前几天结婚休假，我一直在写博客。找资料的过程中发现，单用百度效率太低，至少有一半的时间浪费在了无用页面上。我准备花几个晚上的时间整理收集的资料，尽可能保证在国内可以访问。以下列举初中级开发者的资料整合，至于高级——我大约还没有什么发言权。\nAndroid 初学者 对于初级开发者，我更推荐先买一本基础书，泛泛浏览 Android 的基础知识点。市面上各种 Android 入门书其实都差不多，图灵的质量还算不错。上次图灵妹子 @图灵教育 送的这本 《Android 编程权威指南》，我自己买的 《Android 4 高级编程》，随便一本都行，放桌上没事翻翻。如果你英语尚可且能翻墙，直接看 Android API Guides 更好——大部分图书其实就是对这部分内容的翻译和扩展。\n编程新手一定要多动手。我当初学习时把 eoe 实例区（链接可能已失效）的代码从头写了一遍——当然，这是夸张说法，隐约记得当时写了几百个例子。\n当你能够写一些简单应用后，可以试着从别人的代码中读起。我当初就是从 对一个开源的课程表修复大量 Bug（链接可能已失效）开始的，一点点阅读，一点点修改，这样会有成就感，不容易放弃编程。\n然后，你会慢慢发现能读懂大部分代码了。这时需要了解 Android 的设计规范：\nMaterial Design 中文翻译版（首次加载可能较慢，因为页面引用了 Google Fonts） 开始：Android 基本特征及界面的标准命名 风格：设计原则，对屏幕适配很有帮助——Icon 做多大、间距做多大、不同分辨率图片放哪个文件夹 模式：Android 各种元素的使用场景，比如通知适合什么场景使用 控件：基本控件的使用方式，如进度条和活动指示器作为耗时操作的信号 另一个重要的入门资源是 Google 官方培训课程的中文版：Android Training Course in Chinese\nAndroid 中级开发者 到这个阶段，写一个一般的应用对你来说就像喝水一样，需要的只是时间，不再是技术。你可能熟练使用了大部分 Android API，会数据库操作、懂网络请求、能写自定义 View。这时可以通过博客来提升自己。\nCSDN 博主推荐 博主 地址 特点 Hongyang blog.csdn.net/lmj623565791 在慕课网开课，写过很多实战教程 郭霖 blog.csdn.net/guolin_blog 《第一行代码》作者 任玉刚 blog.csdn.net/singwhatiwanna Apk 动态加载框架，百度手机卫士团队 Mr.Simple blog.csdn.net/bboyfeiyu 源码分析系列，HTTP 框架教程 AigeStudio blog.csdn.net/aigestudio 人称\u0026quot;爱哥\u0026quot;，自定义 View 系列深度好文 Android_Tutor blog.csdn.net/android_tutor 早期教程很详细 源码与示例 23CODE（链接可能已失效）—— 精彩示例，不定期更新 APKBUS（链接可能已失效）—— 类似 iOS 的 code4app，示例集合 修炼源码（链接可能已失效）—— 个人站，精彩示例 godcoder（链接可能已失效）—— 精彩示例 其他资源 IBM DeveloperWorks Java 社区 —— 我在这里学会了 HashMap\u0026hellip; stormzhang —— eoe 会员，当时在讲 Android Studio 的系列教程 Google Android 官方培训课程中文版 (GitHub) 提问 Stack Overflow —— 程序员的神器。简单问题在中文社区搜索即可，搞不定的请到 Stack Overflow Android 高级开发者 到了这个阶段——好吧，我竟然没有太多可写的，逼格要 Low 掉了。\nGit：新时代的版本管理工具，互联网公司面试必修课 Git Cheat Sheet 中文版 Git 完全版教程（链接可能已失效） GitHub：面试敲门砖，热爱开源必备 GitHub Cheat Sheet 陈皓 - 酷壳 —— 很多有意思的技术话题和思考 罗升阳 —— Android 源码分析深度较大，适合对原理有兴趣的同学 码农周刊分类整理 —— 按语言和技术分类 Android 开源项目汇总 —— 来自 Trinea 的整理 《程序员编程艺术：面试和算法心得》 Awesome Android UI —— Android UI/UX 库精选 装备建议 我建议非 .NET 开发的程序员最好使用 Mac，可以关注一下 池建强的 Mac 教程。Mac 的最大优势：\n原生命令行环境 各种工具类软件齐全 环境配置简单，可以顺手写写 iOS 关键是电力——9 小时的续航（2015 年标准） 对于 IDE，2015 年大部分公司还在用 Eclipse + JDK 1.6。我个人更推荐 Android Studio + JDK 1.8——这是趋势。如今 Android Studio 已成为官方标准，Eclipse 已不再推荐使用。\n备注 以上资源均在发布时经过验证。转发时无需注明出处，我发这个帖子的目的就是帮助每个 Android 开发者。整理了差不多一个下午，希望对你有所帮助。\n另外，希望搬运工们尊重以上博主的劳动成果，自己努力之后哪怕是分享一个简单的 Activity，那也有自己的骄傲。谢谢。\n参考文献 Android Developer Guide Material Design Guidelines Google Android Training Course in Chinese Awesome Android UI ","permalink":"https://blog.substitute.tech/posts/android%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99%E6%8C%87%E5%8D%97/","summary":"\u003cblockquote\u003e\n\u003cp\u003e注：此文写于 2015 年，文中部分链接可能已失效。资源推荐反映了当时的生态，许多内容已有更新版本，但核心的学习路径和方法论仍然有参考价值。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e谁无年少时。对于一门技术而言，大家的起点都是 Hello World。让人困惑的是，一年下来有些人的技术提升是 100%，有些人是 20%，有些人或许悲催地没有什么变化。是智商的原因吗？在我看来，可能是学习方法不对。\u003c/p\u003e\n\u003cp\u003e前几天结婚休假，我一直在写博客。找资料的过程中发现，单用百度效率太低，至少有一半的时间浪费在了无用页面上。我准备花几个晚上的时间整理收集的资料，尽可能保证在国内可以访问。以下列举初中级开发者的资料整合，至于高级——我大约还没有什么发言权。\u003c/p\u003e","title":"Android 学习资料指南"},{"content":"注：此文写于 2015 年，部分镜像地址可能已变更或失效。当前国内推荐使用清华大学 TUNA、中科大 USTC 等镜像站，或直接通过 Android Studio SDK Manager 下载。截至 2025 年，本文提到的部分高校镜像可能已停止服务。\n此篇为国内网络环境下的 Android 开发者准备，涵盖以下内容：\nAndroid SDK 更新 Android Studio 下载 AOSP 源码下载 Android NDK 下载 国内镜像站 国内高校和云厂商提供了 Android 镜像，速度远快于直连 Google 服务器。\nSDK Manager 代理设置（历史方案） 2015 年前后，通过 SDK Manager 设置代理可加速下载：\n镜像地址：http://ubuntu.buct.edu.cn/ 或 http://ubuntu.buct.cn/ 端口：默认 80 NDK 下载（历史方案） NDK 镜像地址 —— 更新及时，与官方发布保持同步。\n当前推荐方案（2025 年） 如今国内已有更稳定的 Android 镜像方案：\nAOSP 源码镜像 镜像源 地址 说明 清华大学 TUNA https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest 持续维护，支持每月打包下载 中国科学技术大学 USTC https://mirrors.ustc.edu.cn/aosp/platform/manifest 从 TUNA 同步，长期维护 南方科技大学 SUSTech https://mirrors.sustech.edu.cn/AOSP/ 相对较新 首次同步 AOSP 源码建议使用 TUNA 的每月打包文件（约 80G）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 下载 repo 工具（使用清华镜像） curl https://mirrors.tuna.tsinghua.edu.cn/git/git-repo -o ~/bin/repo chmod +x ~/bin/repo # 下载初始化包（首次使用推荐） wget https://mirrors.tuna.tsinghua.edu.cn/aosp-monthly/aosp-latest.tar tar xf aosp-latest.tar cd aosp repo sync -j4 # 或直接初始化指定版本 repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b android-14.0.0_r33 repo sync -j4 SDK 下载 Android Studio 的 SDK Manager 目前在国内环境下基本可用，内置了腾讯 CDN 加速。如果仍需镜像，可参考：\n腾讯 SDK 镜像：https://ac.qq.com/sdk/ 阿里云镜像：mirrors.aliyun.com 相关资源 Android Studio 官方下载 Android NDK 官方下载 AOSP 源代码 清华大学 TUNA AOSP 镜像帮助页 中国科学技术大学 USTC AOSP 镜像帮助页 ","permalink":"https://blog.substitute.tech/posts/%E8%BD%BB%E6%9D%BE%E6%90%9E%E5%AE%9Aandroid%E7%9A%84%E7%8E%AF%E5%A2%83%E6%BA%90%E7%A0%81%E5%B7%A5%E5%85%B7/","summary":"\u003cp\u003e\u003cstrong\u003e注：此文写于 2015 年，部分镜像地址可能已变更或失效。当前国内推荐使用清华大学 TUNA、中科大 USTC 等镜像站，或直接通过 Android Studio SDK Manager 下载。截至 2025 年，本文提到的部分高校镜像可能已停止服务。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e此篇为国内网络环境下的 Android 开发者准备，涵盖以下内容：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAndroid SDK 更新\u003c/li\u003e\n\u003cli\u003eAndroid Studio 下载\u003c/li\u003e\n\u003cli\u003eAOSP 源码下载\u003c/li\u003e\n\u003cli\u003eAndroid NDK 下载\u003c/li\u003e\n\u003c/ul\u003e","title":"轻松搞定 Android 环境、源码与工具（国内镜像方案）"},{"content":" 1 2 android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment make sure class name exists, is public, and has an empty constructor that is public 以前偶尔碰到这个错误，量不大没在意。最近突然暴增，仔细研究了一番，整理出几个原因和解决办法。\n崩溃原因 Fragment 在系统恢复状态时需要通过反射重新创建实例。以下情况会导致反射失败：\nFragment 是非 static 的内部类 —— Java 内部类会持有对外部类的隐式引用，编译器生成带外部类参数的构造方法，导致没有空构造方法 通过构造方法传递参数 —— 系统恢复时只会调用无参构造器，自定义的有参构造不会被调用 没有声明 public 的空构造方法 —— 即使没有定义任何构造方法，也要确保编译器生成的默认构造是 public 的（包可见性在某些情况下不够） 复现场景 最容易触发的方式是让 Activity 经历配置变化：旋转屏幕（portrait \u0026lt;-\u0026gt; landscape）、接打电话后切回应用等。在这些场景下系统会销毁并重建 Activity 及其关联的 Fragment，此时会对每个 Fragment 调用 Fragment.instantiate()，如果找不到 public 无参构造就会抛出 InstantiationException。\n解决方案 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 保证有 public 的空构造方法 public class CourseFragment extends BaseThemeFragment { public CourseFragment() { super(); } } // 需要初始化参数的，通过 setArguments 传递 public static PageFragment newInstance(int currentResId) { PageFragment fragment = new PageFragment(); Bundle args = new Bundle(); args.putInt(CURRENT_RES, currentResId); fragment.setArguments(args); return fragment; } 关键原则：永远通过 setArguments(Bundle) 传递 Fragment 初始化参数，不要使用自定义构造方法。Fragment 被系统重建时会自动恢复 Arguments。\n为什么一定要写 public 空构造 即使编译器会生成默认构造，也建议显式声明 public。Fragment.instantiate() 内部通过 Class.newInstance() 反射创建实例，该方法要求构造方法是 public 且类加载器允许访问。内部类（非 static）的隐式构造带有外部类引用参数，不满足 Class.newInstance() 的要求。\n补充：FragmentFactory 方案（AndroidX） 如果你的项目使用了 AndroidX，可以通过 FragmentFactory 自定义 Fragment 的实例化逻辑，不再强制要求空构造：\n1 2 3 4 5 6 7 8 9 10 supportFragmentManager.fragmentFactory = new FragmentFactory() { @NonNull @Override public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) { return switch (className) { case \u0026#34;com.example.MyFragment\u0026#34; -\u0026gt; new MyFragment(\u0026#34;custom arg\u0026#34;); default -\u0026gt; super.instantiate(classLoader, className); }; } }; 但在大多数情况下，newInstance() 静态工厂模式仍然是最简单、最推荐的做法。\n参考文献 Fragment 官方文档 — \u0026ldquo;All subclasses of Fragment must include a public no-argument constructor\u0026rdquo; Fragment.InstantiationException 文档 FragmentFactory API 参考 Do fragments really need an empty constructor? Fragment InstantiationException — no empty constructor ","permalink":"https://blog.substitute.tech/posts/android-fragmentinstantiationexception/","summary":"\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-gdscript3\" data-lang=\"gdscript3\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eandroid\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esupport\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ev4\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFragment\u003c/span\u003e\u003cspan class=\"o\"\u003e$\u003c/span\u003e\u003cspan class=\"n\"\u003eInstantiationException\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eUnable\u003c/span\u003e \u003cspan class=\"n\"\u003eto\u003c/span\u003e \u003cspan class=\"n\"\u003einstantiate\u003c/span\u003e \u003cspan class=\"n\"\u003efragment\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emake\u003c/span\u003e \u003cspan class=\"n\"\u003esure\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"n\"\u003eexists\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eis\u003c/span\u003e \u003cspan class=\"n\"\u003epublic\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"ow\"\u003eand\u003c/span\u003e \u003cspan class=\"n\"\u003ehas\u003c/span\u003e \u003cspan class=\"n\"\u003ean\u003c/span\u003e \u003cspan class=\"n\"\u003eempty\u003c/span\u003e \u003cspan class=\"n\"\u003econstructor\u003c/span\u003e \u003cspan class=\"n\"\u003ethat\u003c/span\u003e \u003cspan class=\"n\"\u003eis\u003c/span\u003e \u003cspan class=\"n\"\u003epublic\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e以前偶尔碰到这个错误，量不大没在意。最近突然暴增，仔细研究了一番，整理出几个原因和解决办法。\u003c/p\u003e","title":"Android Fragment$InstantiationException 分析与解决"},{"content":"HashMap 是 Java 面试中高频出现的题目，能有效考察候选人对数据结构和工程实现的理解。这篇文章梳理其核心设计思路与实现细节。\n存储结构 HashMap 底层是一个 table 数组，数组中每个元素是一个 Node\u0026lt;K,V\u0026gt;（JDK 7 中名为 HashMapEntry）。当 put 一个键值对时，先计算 key 的 hashCode，再通过 hash \u0026amp; (table.length - 1) 定位到数组下标。如果多个 key 的哈希值落到同一个下标（哈希碰撞），则以链表形式挂载在该位置。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Override public V put(K key, V value) { if (key == null) { return putValueForNullKey(value); } int hash = secondaryHash(key); HashMapEntry\u0026lt;K, V\u0026gt;[] tab = table; int index = hash \u0026amp; (tab.length - 1); for (HashMapEntry\u0026lt;K, V\u0026gt; e = tab[index]; e != null; e = e.next) { if (e.hash == hash \u0026amp;\u0026amp; key.equals(e.key)) { preModify(e); V oldValue = e.value; e.value = value; return oldValue; } } // No entry for (non-null) key is present; create one modCount++; if (size++ \u0026gt; threshold) { tab = doubleCapacity(); index = hash \u0026amp; (tab.length - 1); } addNewEntry(key, value, hash, index); return null; } void addNewEntry(K key, V value, int hash, int index) { table[index] = new HashMapEntry\u0026lt;K, V\u0026gt;(key, value, hash, table[index]); } 上述代码来自 Android AOSP 对 JDK 7 HashMap 的实现。核心逻辑很清晰：遍历链表查找已有 key，找到则覆盖值，未找到则新增节点。\n哈希函数 key.hashCode() 的结果还要经过一次扰动计算，目的是让高位信息也能参与低位索引的计算，降低碰撞概率。\nAndroid AOSP / JDK 7 实现 采用多次异或移位进行扰动：\n1 2 3 int hash = key.hashCode(); hash ^= (hash \u0026gt;\u0026gt;\u0026gt; 20) ^ (hash \u0026gt;\u0026gt;\u0026gt; 12); hash ^= (hash \u0026gt;\u0026gt;\u0026gt; 7) ^ (hash \u0026gt;\u0026gt;\u0026gt; 4); JDK 8 实现 简化成一次扰动：(h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16)。原因是红黑树的引入降低了链表的查找开销，不再需要复杂的扰动函数。\n1 2 3 4 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16); } 下标计算：hash \u0026amp; (length - 1) HashMap 的数组长度始终是 2 的 n 次方，这是有意为之的设计。length - 1 的二进制全是低位 1，hash \u0026amp; (length - 1) 等价于 hash % length，但位运算比取模快得多，且结果始终落在 [0, length-1] 范围内。\nh \u0026amp; (length - 1) 示例（length=16，即 n-1 = 1111）：\nhash (二进制) \u0026amp; length-1 (1111) = 结果 8 (0100) \u0026amp; 15 (1111) = 0100 (8) 9 (0101) \u0026amp; 15 (1111) = 0101 (9) 16 (10000) \u0026amp; 15 (1111) = 0000 (0) 17 (10001) \u0026amp; 15 (1111) = 0001 (1) 如果 length 不是 2 的幂（如 15），length - 1 是 1110，最后一位始终为 0，导致奇数下标永远不被使用，浪费空间且增加碰撞。\n扩容机制 触发条件 当元素个数超过 capacity * loadFactor 时触发扩容。默认 loadFactor = 0.75，默认容量 16，首个阈值是 16 * 0.75 = 12。\n为什么负载因子选 0.75？这是时间与空间的权衡。负载因子越大（如 1.0），空间利用率高但碰撞概率大，查找效率低；负载因子越小（如 0.5），查找快但浪费空间。0.75 是 JDK 经过长期测试选取的折中值。\n扩容过程 容量翻倍（2 * 16 = 32），然后重新计算每个元素在新数组中的位置。这个 rehash 操作是 HashMap 中开销最大的地方。\nJDK 7 在 rehash 时采用头插法（transfer() 方法），并发场景下容易形成环形链表导致死循环。JDK 8 改为尾插法，解决了这一问题。\n预分配建议 如果已知要存放 1000 个元素，new HashMap(1000) 会将容量设为 1024（最近的 2 的幂）。但阈值 1024 * 0.75 = 768 \u0026lt; 1000，所以会在插入过程中触发扩容。更合理的是 new HashMap(2048)，使 capacity * 0.75 \u0026gt; 1000，避免 resize。\nJDK 8+ 的主要改进 JDK 8 对 HashMap 做了重大重构，核心变化如下：\n改进项 JDK 7 JDK 8 节点命名 HashMapEntry Node，另有 TreeNode 子类 碰撞处理 仅链表（O(n)） 链表 + 红黑树（O(log n)） 树化阈值 无 TREEIFY_THRESHOLD = 8 哈希函数 4 次扰动 1 次扰动 插入方式 头插法 尾插法 初始化时机 构造时分配数组 首次 put 时懒加载 红黑树转换条件 当链表长度超过 TREEIFY_THRESHOLD = 8 且 数组长度超过 MIN_TREEIFY_CAPACITY = 64 时，链表转为红黑树，最差查找从 O(n) 降到 O(log n)。如果数组长度不足 64，即使链表很长也先进行扩容而非树化。\n树化阈值选 8 的原因：在理想随机哈希码且负载因子 0.75 的情况下，桶中节点数量服从泊松分布，链表长度达到 8 的概率约为 0.00000006（千万分之六），因此 8 是一个足够保守的阈值。\n为什么降级阈值是 6 UNTREEIFY_THRESHOLD = 6，与树化阈值 8 保留 2 的缓冲区间，避免节点在阈值附近震荡时频繁转换。\n线程安全问题 HashMap 不是线程安全的。并发 put 可能导致环形链表（JDK 7 的头插法在 rehash 时触发）、数据丢失等问题。多线程场景应使用：\nConcurrentHashMap：分段锁（JDK 7）或 CAS + synchronized（JDK 8） Collections.synchronizedMap()：简单包装，性能较低 HashTable：全表锁，已不推荐 参考文献 OpenJDK HashMap.java 源码 (JDK 8) JDK 17 HashMap 文档 How Java HashMaps Work - Internal Mechanics Explained (freeCodeCamp) Android AOSP libcore HashMap 源码 HashMap collisions and how JDK handles it (Dev.to) ","permalink":"https://blog.substitute.tech/posts/javahashmap%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/","summary":"\u003cp\u003eHashMap 是 Java 面试中高频出现的题目，能有效考察候选人对数据结构和工程实现的理解。这篇文章梳理其核心设计思路与实现细节。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"hashmap01\" loading=\"lazy\" src=\"/images/hashmap01.jpg\"\u003e\u003c/p\u003e","title":"Java HashMap 的实现原理"},{"content":"box-shadow 是 CSS3 中非常灵活的属性，用好它可以在纯 CSS 的情况下实现丰富的视觉效果。下面整理了几类常用技巧。\n定向阴影 通过调整偏移量和扩散半径，可以控制阴影只出现在特定方向。\nTop Right Bottom Left 1 2 3 4 5 6 7 8 9 10 11 12 .drop-shadow.top { box-shadow: 0 -4px 2px -2px rgba(0,0,0,0.4); } .drop-shadow.right { box-shadow: 4px 0 2px -2px rgba(0,0,0,0.4); } .drop-shadow.bottom { box-shadow: 0 4px 2px -2px rgba(0,0,0,0.4); } .drop-shadow.left { box-shadow: -4px 0 2px -2px rgba(0,0,0,0.4); } 关键点：使用负的 spread-radius（第四参数）抵消阴影在非目标方向上的扩散，配合偏移量实现单边阴影。\n强调效果 通过无偏移的阴影和高扩散半径可模拟发光或边框效果。\nDark Light Inset Border 1 2 3 4 5 6 7 8 9 10 11 12 .emphasize-dark { box-shadow: 0 0 5px 2px rgba(0,0,0,.35); } .emphasize-light { box-shadow: 0 0 0 10px rgba(255,255,255,.25); } .emphasize-inset { box-shadow: inset 0 0 7px 4px rgba(255,255,255,.5); } .emphasize-border { box-shadow: inset 0 0 0 7px rgba(255,255,255,.5); } Dark/Light：通过模糊半径+扩散半径组合制造光晕 Inset：利用 inset 关键字实现内阴影，模拟内凹效果 Border：spread-radius 等于 7px、无模糊时，inset 阴影看起来像一个内边框 渐变叠加 使用 background-image 的渐变配合透明度的 box-shadow，实现更为柔和的层次感。\nLight Linear Dark Linear Light Radial Dark Radial 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .gradient-light-linear { background-image: linear-gradient( rgba(255,255,255,.5), rgba(255,255,255,0)); } .gradient-dark-linear { background-image: linear-gradient( rgba(0,0,0,.25), rgba(0,0,0,0)); } .gradient-light-radial { background-image: radial-gradient( center 0, circle farthest-corner, rgba(255,255,255,0.4), rgba(255,255,255,0)); } .gradient-dark-radial { background-image: radial-gradient( center 0, circle farthest-corner, rgba(0,0,0,0.15), rgba(0,0,0,0)); } 注意：这里已移除厂商前缀，现代浏览器均支持标准 linear-gradient / radial-gradient 写法。\n圆角阴影 圆角与阴影配合使用，可获得更自然的卡片效果。\nLight Heavy Full Barrel 1 2 3 4 5 6 7 8 9 10 11 12 .light-rounded { border-radius: 3px; } .heavy-rounded { border-radius: 8px; } .full-rounded { border-radius: 50%; } .barrel-rounded { border-radius: 20px/60px; } border-radius 支持使用 / 分别指定水平和垂直半径，barrel-rounded 展示了这种椭圆圆角的效果。\n浮雕阴影 结合 inset 和常规阴影，可以模拟按钮或卡片的浮雕效果。\nLight Heavy 1 2 3 4 5 6 7 8 9 10 11 .embossed-light { border: 1px solid rgba(0,0,0,0.05); box-shadow: inset 0 1px 0 rgba(255,255,255,0.7); } .embossed-heavy { border: 1px solid rgba(0,0,0,0.05); box-shadow: inset 0 2px 3px rgba(255,255,255,0.3), inset 0 -2px 3px rgba(0,0,0,0.3), 0 1px 1px rgba(255,255,255,0.9); } 原理：通过 inset 高亮上边缘、暗化下边缘，配合外部投影，模拟出凸起或凹陷的立体感。\n参考 box-shadow | MDN Basic Ready to Use CSS Styles | Tympanus Box-shadow generator | MDN ","permalink":"https://blog.substitute.tech/posts/css-box-shadows/","summary":"\u003cp\u003e\u003ccode\u003ebox-shadow\u003c/code\u003e 是 CSS3 中非常灵活的属性，用好它可以在纯 CSS 的情况下实现丰富的视觉效果。下面整理了几类常用技巧。\u003c/p\u003e","title":"CSS Box Shadows 实用技巧"},{"content":"最近写一个图片上传功能时，在某些手机上遇到了运行时异常：\n1 2 3 4 5 6 7 8 9 java.io.FileNotFoundException: /mnt/sdcard/Android/data/com.xxxxxx.android/files/xxxx open failed: EBUSY (Device or resource busy) at libcore.io.IoBridge.open(IoBridge.java:406) at java.io.FileOutputStream.\u0026lt;init\u0026gt;(FileOutputStream.java:88) ... Caused by: libcore.io.ErrnoException: open failed: EBUSY (Device or resource busy) at libcore.io.Posix.open(Native Method) at libcore.io.BlockGuardOs.open(BlockGuardOs.java:110) ... 原因分析 这个 EBUSY 错误与 Android 文件系统（特别是 FAT32）的行为有关。常见场景：删除文件后立即重新创建同名文件，此时文件虽然已被删除，但文件系统的 dentry cache 或文件锁尚未释放，导致新文件创建失败。\n解决方案 最简单的解决办法：删除文件或目录之前先重命名。\n1 2 3 4 final File to = new File( file.getAbsolutePath() + System.currentTimeMillis()); file.renameTo(to); to.delete(); 原理：renameTo() 改变了文件名在文件系统中的引用，原文件名被释放，新的临时文件名承载了实际数据块。此时再删除临时文件，就不会因为原文件名被占用而失败。\n补充说明 EBUSY 通常出现在以下场景：\n多个进程引用了同一个文件 文件已被删除，但引用未被释放 外部 SD 卡上的 FAT32 文件系统（此类问题在该系统上尤为常见） 参考 StackOverflow: open failed EBUSY ","permalink":"https://blog.substitute.tech/posts/android-ebusy-exception/","summary":"\u003cp\u003e最近写一个图片上传功能时，在某些手机上遇到了运行时异常：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e9\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ejava\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eio\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eFileNotFoundException\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003emnt\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003esdcard\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003eAndroid\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003ecom\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003exxxxxx\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eandroid\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003efiles\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003exxxx\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eopen\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003efailed\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eEBUSY\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDevice\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eor\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eresource\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ebusy\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eat\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003elibcore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eio\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eIoBridge\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eopen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIoBridge\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ejava\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003e406\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eat\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ejava\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eio\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eFileOutputStream\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003einit\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eFileOutputStream\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ejava\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003e88\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e...\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eCaused\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eby\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003elibcore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eio\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eErrnoException\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eopen\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003efailed\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eEBUSY\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDevice\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eor\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eresource\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ebusy\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eat\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003elibcore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eio\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ePosix\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eopen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eNative\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eMethod\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eat\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003elibcore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eio\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eBlockGuardOs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eopen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eBlockGuardOs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ejava\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003e110\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e...\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e","title":"Android EBUSY Exception"},{"content":"前几天看到 stormzhang 的腾讯面试题，拿来做一遍，记录下自己的解答思路。\n面试题涉及的主要领域：\nAndroid 基础（进程、Activity、Handler） 屏幕适配（dp、dpi、资源目录） 多线程同步 系统设计（断点续传、网络请求架构） 面试题清单：\n如何画出一个印章的图案 如何实现一个字体的描边与阴影效果 同一个应用程序的不同 Activity 可以运行在不同的进程中么？如果可以，举例说明 Java 中的线程同步有哪几种方式，举例说明 说说对 Handler、Looper 以及 HandlerThread 的理解 dp、dip、dpi、px、sp 是什么意思以及它们的换算公式？layout-sw600dp、layout-h600dp 分别代表什么意思 写出 Activity 的几种启动方式，并简单说说自己的理解或者使用场景 如何设计一个文件的断点续传系统 一个关于 xml 的布局问题，大概意思就是如何让两个 TextView 在一个 RelativeLayout 水平居中显示 设计一个从网络请求数据、图片并加载到列表的系统，画出客户端架构并简单分析 3. 同一个应用程序的不同 Activity 可以运行在不同的进程中吗？ 可以。一般情况下，同一个应用程序的 Activity 都运行在同一个进程中。但如果 Activity 配置了 android:process 属性，它就会运行在独立的进程中。\nandroid:process 的命名规则：\n以 : 开头（如 :first.process）：表示私有进程，进程名前缀为应用包名 以小写字母开头（如 com.example.shared）：表示全局进程，允许其他应用组件也在此进程中运行 1 2 3 4 5 6 7 \u0026lt;application android:icon=\u0026#34;@drawable/icon\u0026#34; android:label=\u0026#34;@string/app_name\u0026#34;\u0026gt; \u0026lt;activity android:name=\u0026#34;.MainActivity\u0026#34; android:process=\u0026#34;:first.process\u0026#34; /\u0026gt; \u0026lt;activity android:name=\u0026#34;.SubActivity\u0026#34; android:process=\u0026#34;:second.process\u0026#34; /\u0026gt; \u0026lt;/application\u0026gt; 两个 Activity 虽然属于同一应用且在同一任务中，却运行在不同的进程中——这正是 Android 任务管理器的强大之处。它让我们可以将相对独立的模块放入独立的进程，降低模块耦合，同时不必关心跨进程通信的细节。具体实现涉及 ActivityRecord 和 ProcessRecord 的调度，由 ActivityManagerService 统一管理。\n参考：Android 应用程序在新的进程中启动新的 Activity 的方法和过程分析 官方文档：android:process | Android Developers\n6. dp、dip、dpi、px、sp 的含义与换算 单位 含义 说明 px 像素点 屏幕物理像素 in 英寸 物理尺寸 mm 毫米 物理尺寸 pt 磅 1/72 英寸 dp / dip 密度无关像素 160dpi 屏幕下 1dp = 1px sp 缩放无关像素 同 dp，但会跟随用户字体大小偏好缩放 dp 与 px 的换算：\n1 2 3 4 5 6 7 8 9 10 11 public static int dip2px(Context context, float dipValue) { final float scale = context.getResources() .getDisplayMetrics().density; return (int)(dipValue * scale + 0.5f); } public static int px2dip(Context context, float pxValue) { final float scale = context.getResources() .getDisplayMetrics().density; return (int)(pxValue / scale + 0.5f); } 建议：文本使用 sp，其他使用 dp。\n资源目录的限制条件 swdp（如 layout-sw600dp）：最小宽度（smallest width），取屏幕宽高中的较小值。不会随屏幕方向变化，固定不变。 wdp（如 layout-w600dp）：当前屏幕宽度，随横竖屏切换变化。 hdp（如 layout-h600dp）：当前屏幕高度，随横竖屏切换变化。官方文档建议尽量少用，因为纵向滚动导致高度变化频繁，不像宽度那样稳定。 参考 Tencent Interview - stormzhang android:process | Android Developers Android 应用程序在新的进程中启动新的 Activity 的方法和过程分析 - CSDN Android 屏幕适配小技巧 - CSDN Processes and threads overview | Android Developers ","permalink":"https://blog.substitute.tech/posts/tencent-interview/","summary":"\u003cp\u003e前几天看到 \u003ca href=\"http://stormzhang.com/android/other/2014/05/03/tencent-interview/\"\u003estormzhang 的腾讯面试题\u003c/a\u003e，拿来做一遍，记录下自己的解答思路。\u003c/p\u003e\n\u003cp\u003e面试题涉及的主要领域：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAndroid 基础（进程、Activity、Handler）\u003c/li\u003e\n\u003cli\u003e屏幕适配（dp、dpi、资源目录）\u003c/li\u003e\n\u003cli\u003e多线程同步\u003c/li\u003e\n\u003cli\u003e系统设计（断点续传、网络请求架构）\u003c/li\u003e\n\u003c/ul\u003e","title":"Tencent Interview"},{"content":"什么是 Lambda 表达式？ Lambda 表达式是 Java 8 引入的核心特性，让代码更加简洁。先看一个最直观的例子：\n1 2 3 4 5 6 7 8 9 10 Runnable runnable1 = new Runnable() { @Override public void run() { System.out.println(\u0026#34;runnable1 start!!!\u0026#34;); } }; Runnable runnable2 = () -\u0026gt; System.out.println(\u0026#34;runnable2 start!!!\u0026#34;); runnable1.run(); runnable2.run(); 两段代码完全等价，但 Lambda 版本只有一行。基本形式为 () -\u0026gt; expression 或 () -\u0026gt; { statements; }。\n有参数无返回值 1 2 3 4 5 6 7 8 9 JButton testButton = new JButton(\u0026#34;Test Button\u0026#34;); testButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent ae) { System.out.println(\u0026#34;Click Detected by Anon Class\u0026#34;); } }); testButton.addActionListener(e -\u0026gt; System.out .println(\u0026#34;Click Detected by Lambda Listner\u0026#34;)); 单参数时可省略括号，形式为 e -\u0026gt; expression。\n有参数有返回值 1 2 3 4 5 6 7 8 9 10 11 12 List\u0026lt;Person\u0026gt; personList = Person.createShortList(); // 匿名内部类 Collections.sort(personList, new Comparator\u0026lt;Person\u0026gt;() { public int compare(Person p1, Person p2) { return p1.getSurName().compareTo(p2.getSurName()); } }); // Lambda 版本 Collections.sort(personList, (Person p1, Person p2) -\u0026gt; p1.getSurName().compareTo(p2.getSurName())); 多参数带返回值的 Lambda 可写成 (p1, p2) -\u0026gt; { return expression; }。当类型可推断时，参数类型可以省略。\n@FunctionalInterface 与函数式接口 如果一个接口中只有一个抽象方法，即使不加 @FunctionalInterface 注解，Java 8 也会将其视为函数式接口。加上注解可以明确意图，并在违反约定时让编译器报错。\n常见的函数式接口包括 Runnable、Comparator、ActionListener 等。\nJava 8 内置的函数式接口 Java 8 在 java.util.function 包下提供了多种标准函数式接口：\n接口 描述 Predicate\u0026lt;T\u0026gt; 接收参数 T，返回 boolean Consumer\u0026lt;T\u0026gt; 接收参数 T，无返回值 Function\u0026lt;T, R\u0026gt; 接收 T，返回 R Supplier\u0026lt;T\u0026gt; 不接受参数，返回 T（工厂模式） UnaryOperator\u0026lt;T\u0026gt; 接收 T，返回 T（一元操作） BinaryOperator\u0026lt;T\u0026gt; 接收两个 T，返回 T（二元操作） 集合操作与 Stream 结合 Stream API 可以写出非常简洁的数据处理代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 List\u0026lt;Person\u0026gt; pl = Person.createShortList(); // forEach pl.forEach(p -\u0026gt; p.printWesternName()); pl.forEach(Person::printEasternName); pl.forEach(p -\u0026gt; { System.out.println(p.printCustom( r -\u0026gt; \u0026#34;Name: \u0026#34; + r.getGivenName())); }); // filter + forEach pl.stream().filter(p -\u0026gt; p.getAge() \u0026gt; 16) .forEach(Person::printWesternName); // filter + collect（生成新列表） Predicate\u0026lt;Person\u0026gt; allDraftees = p -\u0026gt; p.getAge() \u0026gt;= 18 \u0026amp;\u0026amp; p.getAge() \u0026lt;= 25 \u0026amp;\u0026amp; p.getGender() == Gender.MALE; List\u0026lt;Person\u0026gt; pilotList = pl .stream() .filter(allDraftees) .collect(Collectors.toList()); // mapToInt + sum Predicate\u0026lt;Person\u0026gt; allPilots = p -\u0026gt; p.getAge() \u0026gt;= 23 \u0026amp;\u0026amp; p.getAge() \u0026lt;= 65; long totalAge = pl .stream() .filter(allPilots) .mapToInt(p -\u0026gt; p.getAge()) .sum(); // parallelStream + average OptionalDouble averageAge = pl .parallelStream() .filter(allPilots) .mapToDouble(p -\u0026gt; p.getAge()) .average(); 泛型混合使用 下面这个例子展示了如何将 Predicate、Function 和 Consumer 组合为一个通用的处理管道。可读性随着层级增加而下降，但理解了每个接口的职责后还是可以看懂的：\n1 2 3 4 5 6 7 8 9 10 11 12 public static \u0026lt;X, Y\u0026gt; void processElements( Iterable\u0026lt;X\u0026gt; source, Predicate\u0026lt;X\u0026gt; tester, Function\u0026lt;X, Y\u0026gt; mapper, Consumer\u0026lt;Y\u0026gt; block) { for (X p : source) { if (tester.test(p)) { Y data = mapper.apply(p); block.accept(data); } } } 参考 Lambda Expressions (Oracle Official Tutorial) Lambda-QuickStart (Oracle OBE) java.util.function Package (Java SE 8) Project Lambda (OpenJDK) Lambda Expressions (JLS 15.27) ","permalink":"https://blog.substitute.tech/posts/java-8-%E7%9A%84-lambda/","summary":"\u003ch2 id=\"什么是-lambda-表达式\"\u003e什么是 Lambda 表达式？\u003c/h2\u003e\n\u003cp\u003eLambda 表达式是 Java 8 引入的核心特性，让代码更加简洁。先看一个最直观的例子：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eRunnable\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003erunnable1\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eRunnable\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@Override\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;runnable1 start!!!\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eRunnable\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003erunnable2\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;runnable2 start!!!\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003erunnable1\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003erunnable2\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e两段代码完全等价，但 Lambda 版本只有一行。基本形式为 \u003ccode\u003e() -\u0026gt; expression\u003c/code\u003e 或 \u003ccode\u003e() -\u0026gt; { statements; }\u003c/code\u003e。\u003c/p\u003e","title":"Java 8 的 Lambda 表达式"},{"content":"自从换了 Nexus 设备，就不太想用 USB 连电脑了。Android 支持通过 WiFi 进行 ADB 调试，完全可以摆脱数据线的束缚。\n前提条件 手机和电脑（Mac）在同一个局域网 手机开启「开发者模式」和「USB 调试」 电脑装有 ADB 工具 传统方式（Android 10 及以下） 先用 USB 连接手机，执行以下命令切换到 TCP/IP 模式：\n1 2 3 4 5 6 7 8 9 10 11 12 # 确认 adb 运行在 USB 模式 $ adb usb restarting in USB mode # 查看设备是否已连接 $ adb devices List of devices attached ######## device # 将 adb 切换到 TCP/IP 模式，端口 5555 $ adb tcpip 5555 restarting in TCP mode port: 5555 然后查看手机的 IP 地址（设置 \u0026gt; 关于手机 \u0026gt; 状态 \u0026gt; IP 地址），通过无线连接：\n1 2 3 4 5 6 7 8 # 连接设备 $ adb connect 你的IP地址 connected to #.#.#.#:5555 # 拔掉 USB，确认设备仍在 $ adb devices List of devices attached #.#.#.#:5555 device 到此就可以愉快地无线调试了。\n故障排查 如果连接断开或失败，检查以下几点：\n确认手机和电脑在同一个网段 重启 ADB 服务，重新执行一遍流程： 1 2 3 4 $ adb kill-server $ adb start-server $ adb tcpip 5555 $ adb connect 你的IP地址 如果仍然不行，插回 USB 重新走一遍完整流程。\nAndroid 11+ 原生无线调试 Android 11 及以上版本已支持无线调试功能，无需 USB 初始连接：\n开启「开发者选项」和「无线调试」 选择「配对码配对」 在电脑上执行： 1 2 3 $ adb pair IP地址:配对端口 # 输入配对码 $ adb connect IP地址:连接端口 此方式通过 TLS 加密通信，安全性比传统 adb tcpip 方式更高。\n参考 Run apps on a hardware device | Android Developers Android Debug Bridge (adb) | Android Developers ADB over Wi-Fi and Ethernet | ChromeOS Developers ADB Wifi Architecture | AOSP ","permalink":"https://blog.substitute.tech/posts/android%E4%BD%BF%E7%94%A8wireless%E8%B0%83%E8%AF%95/","summary":"\u003cp\u003e自从换了 Nexus 设备，就不太想用 USB 连电脑了。Android 支持通过 WiFi 进行 ADB 调试，完全可以摆脱数据线的束缚。\u003c/p\u003e","title":"Android 使用 Wireless 调试"},{"content":"SQLite 是一个轻量级的关系型数据库引擎，Android 内置对其支持，非常适合本地数据持久化。无需额外配置，通过 android.database.sqlite 包下的 API 即可使用。\nSQLiteOpenHelper 的基本用法 创建和打开数据库只需继承 SQLiteOpenHelper。构造方法中指定数据库名和版本号，系统会自动判断：如果数据库已存在则打开，否则创建新库并回调 onCreate。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class DBHelper extends SQLiteOpenHelper { private static final String TAG = \u0026#34;DBHelper\u0026#34;; private static final String DATABASE_NAME = \u0026#34;contacts.db\u0026#34;; private static final int DATABASE_VERSION = 1; public DBHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } /** * Called when the database is created for the first time. */ @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { Log.i(TAG, \u0026#34;[\u0026#34; + DATABASE_NAME + \u0026#34; v.\u0026#34; + DATABASE_VERSION + \u0026#34;]\u0026#34;); // TODO: Create tables here } /** * Called when the DATABASE_VERSION is increased. */ @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { Log.i(TAG, \u0026#34;Upgrading database[\u0026#34; + DATABASE_NAME + \u0026#34; v.\u0026#34; + newVersion + \u0026#34;]\u0026#34;); } } 需要注意的是，数据库直到首次调用 getWritableDatabase() 或 getReadableDatabase() 时才会真正创建或打开。onUpgrade 在版本号递增时触发，应在此处执行表结构变更逻辑。\n单例模式与线程安全 SQLiteOpenHelper 的子类会返回同一个 SQLiteDatabase 实例。这意味着在任何线程调用 close() 都会关闭应用中所有的数据库实例。因此需要格外注意打开和关闭的时机。\n建议的实践：\n使用单例模式管理 SQLiteOpenHelper 实例 避免在多处频繁开关数据库连接 除非数据量特别大，否则尽量将数据合并到一个数据库中，避免多个 SQLiteOpenHelper 实例带来的混乱 数据库文件的存储位置与安全性 数据库文件对应用来说是私有的，默认存储在 /data/data/(packageName)/database/ 路径下。但需要注意：\n数据库文件没有加密，Root 过的设备上任何人都可以直接读取 如需在非 Root 设备上导出数据库文件，需要在应用中先将其复制到公共目录 敏感数据建议使用 EncryptedDatabase 或 SQLCipher 等方案加密 参考 SQLiteOpenHelper | Android Developers Save data using SQLite | Android Developers SQLite 官方文档 Using the SQLite Database on Android and SQLiteOpenHelper ","permalink":"https://blog.substitute.tech/posts/androids-sqlite/","summary":"\u003cp\u003eSQLite 是一个轻量级的关系型数据库引擎，Android 内置对其支持，非常适合本地数据持久化。无需额外配置，通过 \u003ccode\u003eandroid.database.sqlite\u003c/code\u003e 包下的 API 即可使用。\u003c/p\u003e","title":"Android's SQLite"},{"content":"Palette 是 Android Support Library（现已迁移至 AndroidX palette 库）提供的取色工具，可以从 Bitmap 中自动提取一组代表色。在 Material Design 中，从图片提取色彩并应用到 UI 元素是常见的设计手法，Palette 让这一过程变得非常简单。\n支持的颜色类型 Palette 提供六种色调类型，覆盖从鲜艳到暗淡的不同风格：\n色调类型 说明 Vibrant 鲜艳 Vibrant Dark 鲜艳暗色 Vibrant Light 鲜艳亮色 Muted 暗淡 Muted Dark 暗淡暗色 Muted Light 暗淡亮色 每种色调对应一个 Palette.Swatch 对象，包含 RGB 值、HSL 值、像素占比（population），以及适合在该色块上展示的标题和正文文本颜色。\n基本用法 Palette 支持同步和异步两种生成方式：\n1 2 3 4 5 6 7 8 9 // 同步生成（需在后台线程执行） public static Palette generate (Bitmap bitmap); public static Palette generate (Bitmap bitmap, int numColors); // 异步生成（自动在后台线程执行） public static AsyncTask\u0026lt;Bitmap, Void, Palette\u0026gt; generateAsync ( Bitmap bitmap, Palette.PaletteAsyncListener listener); public static AsyncTask\u0026lt;Bitmap, Void, Palette\u0026gt; generateAsync ( Bitmap bitmap, int numColors, Palette.PaletteAsyncListener listener); 同步方法 generate 速度很快，约几十毫秒。异步方式则通过 AsyncTask 在后台线程执行，结果通过回调返回。\n关于 numColors 参数 numColors 控制提取的颜色数量，取值取决于图片类型：\n风景照推荐 12-16，人物肖像建议 24-32。\n数量越少生成速度越快，越多则色彩越精细。如果未指定，默认值为 16。\n示例代码 以下示例从 Bitmap 提取颜色并应用到 ActionBar 和一组 View 上：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 Bitmap bitmap = BitmapFactory.decodeResource( getResources(), R.drawable.strictdroid); Palette.generateAsync(bitmap, new PaletteAsyncListener() { @Override public void onGenerated(Palette palette) { getSupportActionBar().setBackgroundDrawable( new ColorDrawable( palette.getVibrantSwatch().getRgb())); for (int i = 0; i \u0026lt; viewList.size(); i++) { View view = viewList.get(i); switch (i) { case 0: view.setBackgroundColor( palette.getDarkMutedColor(Color.BLACK)); break; case 1: view.setBackgroundColor( palette.getDarkVibrantColor(Color.BLACK)); break; case 2: view.setBackgroundColor( palette.getLightMutedColor(Color.BLACK)); break; case 3: view.setBackgroundColor( palette.getLightVibrantColor(Color.BLACK)); break; case 4: view.setBackgroundColor( palette.getMutedColor(Color.BLACK)); break; case 5: view.setBackgroundColor( palette.getVibrantColor(Color.BLACK)); break; } } } }); 每种 getXxxColor() 方法都接受一个默认颜色参数，当该色调类型在图片中不存在时使用。\n迁移至 AndroidX 原始 v7 Palette 库已迁移至 AndroidX，新依赖为：\n1 implementation(\u0026#34;androidx.palette:palette:1.0.0\u0026#34;) API 用法基本一致。Builder 模式推荐使用 Palette.from(bitmap).generate()，支持更多自定义选项，如 maximumColorCount()、addFilter()、resizeBitmapArea() 等。\n参考 Select colors with the Palette API | Android Developers Palette API Reference | Android Developers Palette.Builder API Reference | Android Developers 使用 Palette API 选择颜色 | Android 开发者 示例源码：PaletteDemo ","permalink":"https://blog.substitute.tech/posts/androids-palette/","summary":"\u003cp\u003ePalette 是 Android Support Library（现已迁移至 AndroidX \u003ccode\u003epalette\u003c/code\u003e 库）提供的取色工具，可以从 Bitmap 中自动提取一组代表色。在 Material Design 中，从图片提取色彩并应用到 UI 元素是常见的设计手法，Palette 让这一过程变得非常简单。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"palette01\" loading=\"lazy\" src=\"/images/palette01.png\"\u003e\u003c/p\u003e","title":"Android's Palette"},{"content":"视差滚动（Parallax Scrolling）是一种常见的 UI 效果：滚动时不同层次的元素以不同速度移动，营造立体感。\n实现原理 核心思路是监听 RecyclerView 的滚动事件，根据滚动距离动态调整 Header 的位移，同时裁剪超出部分以保证布局正确。\n1. 监听滚动并移动 Header 通过 setOnScrollListener 获取滚动距离，然后对 Header 施加 translationY 偏移：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 RecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (mHeader != null) { RecyclerView.ViewHolder holder = RecyclerView.findViewHolderForPosition(0); if (holder != null) { int startTop = holder.itemView.getTop(); float ofCalculated = startTop * SCROLL_MULTIPLIER; ViewCompat.setTranslationY(mHeader, -ofCalculated); // ... } } } }); 这里的关键在于 SCROLL_MULTIPLIER 控制视差速度——值小于 1 时 Header 移动比内容慢，实现分层效果。\n2. 裁剪溢出部分 由于 Header 虽然视觉上移动了，但实际占位没有变化，需要裁剪掉溢出的部分：\n1 2 3 4 5 6 7 8 9 10 11 12 13 @Override protected void dispatchDraw(Canvas canvas) { Log.i(\u0026#34;dispatchDraw\u0026#34;, \u0026#34;mOffset=\u0026#34; + mOffset + \u0026#34; ;getLeft()=\u0026#34; + getLeft() + \u0026#34; ;getRight()=\u0026#34; + getRight() + \u0026#34; ;getTop()=\u0026#34; + getTop() + \u0026#34; ;getBottom()=\u0026#34; + getBottom()); canvas.clipRect(new Rect(getLeft(), getTop(), getRight(), getBottom() + mOffset)); super.dispatchDraw(canvas); } public void setClipY(int offset) { mOffset = offset; invalidate(); } 通过重写 dispatchDraw，在绘制前裁剪 Canvas，确保 Header 超出容器范围的部分被隐藏。\n3. 计算滚动进度 当前滚动进度可通过 startTop / mHeader.getHeight() 计算，用于驱动其他联动效果（如透明度渐变动画）。\n参考 源码 AndroidRecyclerViewDemo android-parallax-recyclerview RecyclerView - Android Developers ","permalink":"https://blog.substitute.tech/posts/androids-recyclerview2-%E8%A7%86%E5%B7%AE%E6%BB%9A%E5%8A%A8/","summary":"\u003cp\u003e视差滚动（Parallax Scrolling）是一种常见的 UI 效果：滚动时不同层次的元素以不同速度移动，营造立体感。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"parallaxrecycler\" loading=\"lazy\" src=\"/images/parallaxrecycler.gif\"\u003e\u003c/p\u003e","title":"Android 的 RecyclerView（二）：视差滚动"},{"content":"RecyclerView 是一个比 ListView 更灵活的滚动列表控件。官方文档指出，它能高效维护数量有限的滚动数据集合，当 View 需要与用户行为和网络数据交互时，推荐使用 RecyclerView。\nRecyclerView 的核心优势 RecyclerView 简化了 View 的显示和数据处理，主要体现在：\n布局定位 — 通过 LayoutManager 管理 Item 的位置 Item 动画 — 内置增删动画，支持自定义 基本使用 使用 RecyclerView 必须指定一个布局管理器（LayoutManager）和一个适配器（继承 RecyclerView.Adapter）。LayoutManager 负责确定 Item 的位置信息、复用与回收，避免不必要的性能开销（如 findViewById）。\n内置的布局管理器：\n布局管理器 效果 LinearLayoutManager 垂直或水平滚动的列表 GridLayoutManager 网格布局 StaggeredGridLayoutManager 交错网格布局 动画 RecyclerView 默认启用添加和删除的动画。如需自定义动画，可扩展 RecyclerView.ItemAnimator 类，并通过 RecyclerView.setItemAnimator() 设置。\n参考示例：RecyclerViewItemAnimators\n点击事件 RecyclerView 没有类似 ListView 的 onItemClickListener。原因是原来的 onItemClickListener 容易让人误解——RecyclerView 并没有严格的行或列概念，因此推荐使用每个 Item View 自身的点击事件。\n更多讨论见：Why doesn\u0026rsquo;t RecyclerView have onItemClickListener()?\nLayoutManager 详解 LinearLayoutManager 默认效果类似 ListView，额外提供以下配置：\n1 2 // 方向与逆序排列 LinearLayoutManager(Context context, int orientation, boolean reverseLayout) orientation 可选 HORIZONTAL 或 VERTICAL，reverseLayout 表示是否逆序排列。\n1 mLayoutManager.setStackFromEnd(true); setStackFromEnd(true) 表示从底部开始显示，但在数据集改变时不起作用。\nGridLayoutManager 1 2 3 mLayoutManager = new GridLayoutManager(this, 2, GridLayoutManager.VERTICAL, false); mRecyclerView.setLayoutManager(mLayoutManager); dataSet.addAll(Arrays.asList(LETTERS)); GridLayoutManager 有两种构造方式：\nGridLayoutManager(Context context, int spanCount) — 默认垂直布局，spanCount 控制列数 GridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) — 同 LinearLayoutManager 可指定方向 参考 源码 AndroidRecyclerViewDemo Create dynamic lists with RecyclerView - Android Developers RecyclerView API Reference ","permalink":"https://blog.substitute.tech/posts/androids-recyclerview/","summary":"\u003cp\u003eRecyclerView 是一个比 ListView 更灵活的滚动列表控件。官方文档指出，它能高效维护数量有限的滚动数据集合，当 View 需要与用户行为和网络数据交互时，推荐使用 RecyclerView。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"RecyclerView01\" loading=\"lazy\" src=\"/images/RecyclerView01.png\"\u003e\u003c/p\u003e","title":"Android 的 RecyclerView"},{"content":"Canvas 的裁剪（Clip）用于限定绘制区域：只有裁剪区域范围内的内容才会被显示出来。\nRegion.Op 裁剪模式 Canvas 支持多种裁剪区域的组合操作，通过 Region.Op 枚举指定：\n枚举值 说明 DIFFERENCE(0) 最终区域为 region1 与 region2 不同的区域 INTERSECT(1) 最终区域为 region1 与 region2 相交的区域 UNION(2) 最终区域为 region1 与 region2 组合的区域 XOR(3) 最终区域为 region1 与 region2 相交之外的区域 REVERSE_DIFFERENCE(4) 最终区域为 region2 与 region1 不同的区域 REPLACE(5) 最终区域为 region2 的区域 完整示例 以下代码展示了各种裁剪模式的实际效果：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 private void drawScene(Canvas canvas) { canvas.clipRect(0, 0, 100 * factor, 100 * factor); canvas.drawColor(Color.WHITE); mPaint.setColor(Color.RED); canvas.drawLine(0, 0, 100 * factor, 100 * factor, mPaint); mPaint.setColor(Color.GREEN); canvas.drawCircle(30 * factor, 70 * factor, 30 * factor, mPaint); mPaint.setColor(Color.BLUE); canvas.drawText(\u0026#34;Clipping\u0026#34;, 100 * factor, 30 * factor, mPaint); } @Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.GRAY); canvas.save(); canvas.translate(10 * factor, 10 * factor); drawScene(canvas); canvas.restore(); canvas.save(); canvas.translate(160 * factor, 10 * factor); // 剪切外圈 canvas.clipRect(10 * factor, 10 * factor, 90 * factor, 90 * factor); // 中间挖空，保留 region1 与 region2 不同的区域 canvas.clipRect(30 * factor, 30 * factor, 70 * factor, 70 * factor, Region.Op.DIFFERENCE); drawScene(canvas); canvas.restore(); canvas.save(); canvas.translate(10 * factor, 160 * factor); mPath.reset(); // canvas.clipPath(mPath); // makes the clip empty mPath.addCircle(50 * factor, 50 * factor, 50 * factor, Path.Direction.CCW); canvas.clipPath(mPath, Region.Op.REPLACE); drawScene(canvas); canvas.restore(); canvas.save(); canvas.translate(160 * factor, 160 * factor); canvas.clipRect(0, 0, 60 * factor, 60 * factor); canvas.clipRect(40 * factor, 40 * factor, 100 * factor, 100 * factor, Region.Op.UNION); drawScene(canvas); canvas.restore(); canvas.save(); canvas.translate(10 * factor, 310 * factor); canvas.clipRect(0, 0, 60 * factor, 60 * factor); canvas.clipRect(40 * factor, 40 * factor, 100 * factor, 100 * factor, Region.Op.XOR); drawScene(canvas); canvas.restore(); canvas.save(); canvas.translate(160 * factor, 310 * factor); canvas.clipRect(0, 0, 60 * factor, 60 * factor); canvas.clipRect(40 * factor, 40 * factor, 100 * factor, 100 * factor, Region.Op.REVERSE_DIFFERENCE); drawScene(canvas); canvas.restore(); } 使用 canvas.save() 和 canvas.restore() 可以保存和恢复 Canvas 状态，确保每次裁剪只影响当前绘制区域。\n参考 roamer\u0026rsquo; blog - Canvas Clip 源码 Blog02 Canvas - Android Developers ","permalink":"https://blog.substitute.tech/posts/android-canvas2/","summary":"\u003cp\u003eCanvas 的裁剪（Clip）用于限定绘制区域：只有裁剪区域范围内的内容才会被显示出来。\u003c/p\u003e","title":"Android 的 Canvas（二）：裁剪"},{"content":"View 能将内容显示出来，本质上是\u0026quot;画\u0026quot;出来的——在画板上使用画笔绘制。这里的画布是 Canvas，画笔是 Paint。通过 onDraw 方法获取到的 Canvas 内容可以直接反映到 View 上。\n获取 Canvas 对象 有三种方式获取 Canvas 实例：\n重写 View.onDraw 方法 — Canvas 对象由系统作为参数传入 直接构造 — Canvas c = new Canvas(Bitmap) 或构造后调用 setBitmap Surface 锁定 — 调用 SurfaceHolder.lockCanvas() Canvas 变换方法 Canvas 提供了一系列位置变换方法，用于实现平移、旋转、缩放等效果：\nrotate — 旋转 scale — 缩放 translate — 平移 skew — 倾斜 Canvas 图层（Layer）机制 以下内容引用自 roamer\u0026rsquo; blog\nCanvas 在一般情况下可看作一张画布，所有绘图操作（如 drawBitmap、drawCircle）都发生在这张画布上。画布还定义了 Matrix、颜色等属性。\n如果需要实现相对复杂的绘图操作（如多层动画、地图图层叠加），Canvas 提供了图层（Layer）支持。缺省情况下只有一个图层 Layer。可以按层次绘图，使用 saveLayerXXX 创建中间层、restore 恢复。\n图层按\u0026quot;栈结构\u0026quot;管理：\n引用自 roamer\u0026rsquo; blog\n创建新的 Layer 入栈使用 saveLayer、savaLayerAlpha；出栈使用 restore、restoreToCount。Layer 入栈后，后续的 DrawXXX 操作都在这个 Layer 上进行；Layer 退栈时，将本层图像\u0026quot;绘制\u0026quot;到上层或 Canvas 上。复制到 Canvas 时可指定透明度。\n完整绘制示例 下面是一个综合示例，展示了 Canvas 支持的各种绘制操作：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 public class CanvasView extends View { private Paint arcPaint; private RectF rectF; private Shader mShader; private Path mPath; private Bitmap bitmap; public CanvasView(Context context) { super(context); } public CanvasView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG); arcPaint.setColor(Color.BLUE); rectF = new RectF(260, 70, 290, 115); // 设置渐变色 mShader = new LinearGradient(0, 0, 100, 100, new int[] {Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.LTGRAY}, null, Shader.TileMode.REPEAT); mPath = new Path(); mPath.reset(); bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 填充背景 canvas.drawARGB(255, 0, 180, 255); canvas.drawColor(Color.RED); // TODO PorterDuff.Mode 待深入了解 canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); canvas.drawRGB(255, 255, 255); // 创建画笔 arcPaint.setColor(Color.RED); canvas.drawText(\u0026#34;画圆：\u0026#34;, 10, 50, arcPaint); canvas.drawCircle(100, 45, 10, arcPaint); arcPaint.setAntiAlias(true); canvas.drawCircle(150, 45, 20, arcPaint); canvas.drawText(\u0026#34;画线及弧线：\u0026#34;, 10, 100, arcPaint); arcPaint.setColor(Color.GREEN); canvas.drawLine(160, 90, 210, 90, arcPaint); canvas.drawLine(210, 75, 270, 55, arcPaint); arcPaint.setStyle(Paint.Style.STROKE); canvas.drawCircle(300, 100, 50, arcPaint); canvas.drawArc(rectF, 180, 180, false, arcPaint); rectF.set(310, 70, 340, 115); canvas.drawArc(rectF, 180, 180, false, arcPaint); rectF.set(290, 110, 310, 125); canvas.drawArc(rectF, 0, 180, false, arcPaint); canvas.drawText(\u0026#34;画矩形：\u0026#34;, 50, 150, arcPaint); arcPaint.setColor(Color.GRAY); arcPaint.setStyle(Paint.Style.FILL); canvas.drawRect(160, 135, 210, 185, arcPaint); canvas.drawRect(215, 150, 260, 210, arcPaint); canvas.drawText(\u0026#34;画扇形和椭圆：\u0026#34;, 10, 250, arcPaint); arcPaint.setShader(mShader); rectF.set(150, 190, 290, 310); canvas.drawArc(rectF, 200, 130, true, arcPaint); rectF.set(300, 200, 420, 300); canvas.drawOval(rectF, arcPaint); canvas.drawText(\u0026#34;画三角形：\u0026#34;, 10, 380, arcPaint); mPath.moveTo(100, 330); mPath.lineTo(150, 430); mPath.lineTo(180, 350); mPath.close(); canvas.drawPath(mPath, arcPaint); // 六边形 arcPaint.reset(); arcPaint.setColor(Color.LTGRAY); arcPaint.setStyle(Paint.Style.STROKE); mPath.reset(); mPath.moveTo(280, 400); mPath.lineTo(300, 400); mPath.lineTo(310, 410); mPath.lineTo(300, 420); mPath.lineTo(280, 420); mPath.lineTo(270, 410); mPath.close(); canvas.drawPath(mPath, arcPaint); // 圆角矩形 arcPaint.setStyle(Paint.Style.FILL); arcPaint.setColor(Color.LTGRAY); arcPaint.setAntiAlias(true); canvas.drawText(\u0026#34;画圆角矩形：\u0026#34;, 10, 450, arcPaint); rectF.set(180, 430, 300, 470); canvas.drawRoundRect(rectF, 20, 15, arcPaint); // 贝塞尔曲线 canvas.drawText(\u0026#34;画贝塞尔曲线：\u0026#34;, 10, 310, arcPaint); arcPaint.reset(); arcPaint.setStyle(Paint.Style.STROKE); arcPaint.setColor(Color.GREEN); mPath.reset(); mPath.moveTo(180, 310); mPath.quadTo(250, 250, 200, 350); canvas.drawPath(mPath, arcPaint); // 画点 arcPaint.setStyle(Paint.Style.FILL); canvas.drawText(\u0026#34;画点：\u0026#34;, 10, 520, arcPaint); canvas.drawPoint(60, 520, arcPaint); canvas.drawPoints(new float[] {60, 550, 65, 560, 70, 570}, arcPaint); // 画图片 canvas.drawBitmap(bitmap, 350, 350, arcPaint); } } 参考 roamer\u0026rsquo; blog - Canvas 详解 源码 Canvas(1) Canvas - Android Developers Paint - Android Developers Create a custom drawing - Android Developers ","permalink":"https://blog.substitute.tech/posts/android-canvas1/","summary":"\u003cp\u003eView 能将内容显示出来，本质上是\u0026quot;画\u0026quot;出来的——在画板上使用画笔绘制。这里的画布是 \u003ccode\u003eCanvas\u003c/code\u003e，画笔是 \u003ccode\u003ePaint\u003c/code\u003e。通过 \u003ccode\u003eonDraw\u003c/code\u003e 方法获取到的 Canvas 内容可以直接反映到 View 上。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Canvas01\" loading=\"lazy\" src=\"/images/android_canvas01.png\"\u003e\u003c/p\u003e","title":"Android 的 Canvas（一）"},{"content":"在 ViewGroup 为子 View 测量时，会通过 MeasureSpec 指定测量模式。理解这三种模式是自定义 View 布局的基础。\n测量模式详解 模式 含义 触发条件 EXACTLY 精确值 子 View 宽高为精确值或 match_parent AT_MOST 最大值限制 子 View 宽高为 wrap_content UNSPECIFIED 无限制 常见于 AdapterView、ScrollView 的子 View 高度 EXACTLY ViewGroup 为子 View 指定了精确的尺寸。当子 View 的 layout_width 或 layout_height 设置为具体数值或 match_parent 时，父容器会传递此模式。子 View 必须在给定大小内完成绘制。\nAT_MOST 子 View 被限制在一个最大值内。当设置为 wrap_content 时，父容器会传递此模式。子 View 需要根据自身内容计算尺寸，但不能超过父容器给的上限。\nUNSPECIFIED 不做任何限制。父容器对子 View 没有任何大小约束，子 View 可以按需取任意尺寸。常见于可滚动的容器（如 ScrollView、ListView）中。\n参考 源码 BlogCode01 Creating Custom Views - Android Developers View.MeasureSpec - Android Developers ","permalink":"https://blog.substitute.tech/posts/customview2/","summary":"","title":"Android 的自定义 View（二）：测量模式"},{"content":"刚开始编写自定义 View 时，难免不知道如何下手。一般说来有两种实现方式：\n从零开始：继承 View，通过计算和绘制实现所需的外观。 扩展现有 View：在已有控件基础上增加子 View，或重写方法改变原有逻辑。 继承 View 实现自定义控件 先看一个继承 View 的最简示例：\n1 2 3 4 5 6 7 8 9 10 public class SimpleView extends View { public SimpleView(Context context) { super(context); } public SimpleView(Context context, AttributeSet attrs) { super(context, attrs); } } 如果不需要 XML 配置属性，AttributeSet 构造方法可以不写——但这样一来此 View 也不能在 XML 中使用。\n自定义属性 AttributeSet 将 XML 属性解析为数组供代码使用。属性类型需要事先声明，通常定义在 res/values/attrs.xml 中：\n1 2 3 4 5 6 7 \u0026lt;declare-styleable name=\u0026#34;SimpleView\u0026#34;\u0026gt; \u0026lt;attr name=\u0026#34;showContent\u0026#34; format=\u0026#34;boolean\u0026#34; /\u0026gt; \u0026lt;attr name=\u0026#34;showPosition\u0026#34; format=\u0026#34;enum\u0026#34;\u0026gt; \u0026lt;enum name=\u0026#34;left\u0026#34; value=\u0026#34;0\u0026#34; /\u0026gt; \u0026lt;enum name=\u0026#34;right\u0026#34; value=\u0026#34;1\u0026#34; /\u0026gt; \u0026lt;/attr\u0026gt; \u0026lt;/declare-styleable\u0026gt; 支持的数据类型：boolean、string、dimension、enum、fraction、reference、color，也可用 | 指定多种类型。\n在 XML 中使用自定义命名空间 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;RelativeLayout xmlns:android=\u0026#34;http://schemas.android.com/apk/res/android\u0026#34; xmlns:tools=\u0026#34;http://schemas.android.com/tools\u0026#34; xmlns:hxq=\u0026#34;http://schemas.android.com/apk/res/blog.haoxiqiang\u0026#34; android:layout_width=\u0026#34;match_parent\u0026#34; android:layout_height=\u0026#34;match_parent\u0026#34; tools:context=\u0026#34;blog.haoxiqiang.MainActivity\u0026#34; \u0026gt; \u0026lt;blog.haoxiqiang.SimpleView android:layout_width=\u0026#34;wrap_content\u0026#34; android:layout_height=\u0026#34;wrap_content\u0026#34; hxq:showContent=\u0026#34;true\u0026#34; hxq:showPosition=\u0026#34;right\u0026#34; android:text=\u0026#34;@string/hello_world\u0026#34; /\u0026gt; \u0026lt;/RelativeLayout\u0026gt; 命名空间写法：http://schemas.android.com/apk/res/[your package name] 或 http://schemas.android.com/apk/res/auto。\n完整示例：带圆和文字的 View 下面实现一个自定义 View，绘制一个蓝色圆形并配上文字。showContent 控制文字显示，showPosition 控制在屏幕左半部分还是右半部分绘制。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package blog.haoxiqiang; public class SimpleView extends View { // attr private boolean showContent = false; private int position = 0; // param private Paint mCirclePaint; private Paint mTextPaint; private final int radius = 100; private int width = 0; private boolean initLayout = false; public SimpleView(Context context) { super(context); init(context); } public SimpleView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SimpleView); if (typedArray != null) { showContent = typedArray.getBoolean(R.styleable.SimpleView_showContent, false); position = typedArray.getInt(R.styleable.SimpleView_showPosition, 0); } typedArray.recycle(); init(context); } private void init(Context context) { mCirclePaint = new Paint(); mCirclePaint.setColor(Color.BLUE); mCirclePaint.setStrokeWidth(8); mTextPaint = new Paint(); mTextPaint.setColor(Color.RED); mTextPaint.setStrokeWidth(5); mTextPaint.setTextSize(35.0f); this.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (!initLayout) { width = SimpleView.this.getWidth(); Log.i(\u0026#34;onGlobalLayout\u0026#34;, \u0026#34;initLayout:\u0026#34; + width); SimpleView.this.invalidate(); initLayout = true; } } }); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int x = position == 0 ? radius : width - 2 * radius; x = x \u0026lt; 0 ? 0 : x; canvas.drawCircle(x, 100, radius, mCirclePaint); if (showContent) { canvas.drawText(\u0026#34;SimpleView\u0026#34;, x, 100, mTextPaint); } } } 下一篇将讨论 onMeasure 的工作原理。\n参考 源码 BlogCode01 Creating Custom Views - Android Developers Create custom view components - Android Developers ","permalink":"https://blog.substitute.tech/posts/customview1/","summary":"\u003cp\u003e刚开始编写自定义 View 时，难免不知道如何下手。一般说来有两种实现方式：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e从零开始\u003c/strong\u003e：继承 \u003ccode\u003eView\u003c/code\u003e，通过计算和绘制实现所需的外观。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e扩展现有 View\u003c/strong\u003e：在已有控件基础上增加子 View，或重写方法改变原有逻辑。\u003c/li\u003e\n\u003c/ol\u003e","title":"Android 的自定义 View（一）"},{"content":"这是本站的第一篇博客。当时参考了许多 Jekyll 模板，借鉴 Pure 设计了一套自己的模板，托管在 GitHub 上。\n源码：haoxiqiang-template\nJekyll 简介 Jekyll 是一个静态站点生成器，将 Markdown、Liquid 模板等纯文本转换为完整的静态网站，无需数据库支持。结合 GitHub Pages 可免费托管博客。\n功能支持 代码高亮 Jekyll 内置基于 Rouge 的代码高亮：\n1 2 3 4 5 def print_hi(name) puts \u0026#34;Hi, #{name}\u0026#34; end print_hi(\u0026#39;Tom\u0026#39;) #=\u0026gt; prints \u0026#39;Hi, Tom\u0026#39; LaTeX 公式 页面声明 latex: true 后可使用 MathJax 渲染数学公式：\n$$ \\begin{aligned} \\dot{x} \u0026amp;= \\sigma(y-x) \\ \\dot{y} \u0026amp;= \\rho x - y - xz \\ \\dot{z} \u0026amp;= -\\beta z + xy \\end{aligned} $$\n$$ a^2 + b^2 = c^2 $$\nWhen $a \\ne 0$, there are two solutions to $ax^2 + bx + c = 0$.\n预览分割线 使用 `\n` 标记文章摘要与全文的分割点。\n技术依赖 工具 说明 版本 Jekyll 静态博客生成器 2.5.2 MathJax LaTeX 公式渲染 2.4.0 Duoshuo 多说评论（链接可能已失效） 1.0.0 Disqus 评论系统 1.0.0 haoxiqiang-template 博客模板 1.0.0 参考 Jekyll 官方文档 MathJax 官方文档 GitHub Pages ","permalink":"https://blog.substitute.tech/posts/welcome/","summary":"\u003cp\u003e这是本站的第一篇博客。当时参考了许多 Jekyll 模板，借鉴 \u003ccode\u003ePure\u003c/code\u003e 设计了一套自己的模板，托管在 GitHub 上。\u003c/p\u003e\n\u003cp\u003e源码：\u003ca href=\"https://github.com/Haoxiqiang/haoxiqiang-template\"\u003ehaoxiqiang-template\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"jekyll-简介\"\u003eJekyll 简介\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://jekyllrb.com/\"\u003eJekyll\u003c/a\u003e 是一个静态站点生成器，将 Markdown、Liquid 模板等纯文本转换为完整的静态网站，无需数据库支持。结合 \u003ca href=\"https://pages.github.com/\"\u003eGitHub Pages\u003c/a\u003e 可免费托管博客。\u003c/p\u003e\n\u003ch2 id=\"功能支持\"\u003e功能支持\u003c/h2\u003e\n\u003ch3 id=\"代码高亮\"\u003e代码高亮\u003c/h3\u003e\n\u003cp\u003eJekyll 内置基于 Rouge 的代码高亮：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-ruby\" data-lang=\"ruby\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eprint_hi\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nb\"\u003eputs\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Hi, \u003c/span\u003e\u003cspan class=\"si\"\u003e#{\u003c/span\u003e\u003cspan class=\"nb\"\u003ename\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eprint_hi\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Tom\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e#=\u0026gt; prints \u0026#39;Hi, Tom\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"latex-公式\"\u003eLaTeX 公式\u003c/h3\u003e\n\u003cp\u003e页面声明 \u003ccode\u003elatex: true\u003c/code\u003e 后可使用 MathJax 渲染数学公式：\u003c/p\u003e\n\u003cp\u003e$$\n\\begin{aligned} \\dot{x} \u0026amp;= \\sigma(y-x) \\\n\\dot{y} \u0026amp;= \\rho x - y - xz \\\n\\dot{z} \u0026amp;= -\\beta z + xy \\end{aligned}\n$$\u003c/p\u003e\n\u003cp\u003e$$\na^2 + b^2 = c^2\n$$\u003c/p\u003e\n\u003cp\u003eWhen $a \\ne 0$, there are two solutions to $ax^2 + bx + c = 0$.\u003c/p\u003e\n\u003ch3 id=\"预览分割线\"\u003e预览分割线\u003c/h3\u003e\n\u003cp\u003e使用 `\u003c/p\u003e","title":"Jekyll 搭建 Blog"},{"content":"关于我 我是郝锡强，一个技术方向比较杂的工程师。\n主要涉及的领域：\nChromium — 浏览器内核相关开发 Android — 应用层和系统层开发 Server — 后端服务与基础设施 联系方式 GitHub: haoxiqiang Email: haoxiqiang@live.com 关于这个博客 这个博客记录我在技术实践中的一些笔记和思考。使用 Hugo 构建，主题为 PaperMod，托管在 GitHub Pages。\n","permalink":"https://blog.substitute.tech/about/","summary":"about","title":"关于"}]