<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>lark</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://larkzhh.xyz/</id>
  <link href="https://larkzhh.xyz/" rel="alternate"/>
  <link href="https://larkzhh.xyz/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, lark</rights>
  <title>Lark's Space</title>
  <updated>2026-11-01T16:00:00.000Z</updated>
  <entry>
    <author>
      <name>lark</name>
    </author>
    <category term="Android" scheme="https://larkzhh.xyz/categories/Android/"/>
    <category term="Android" scheme="https://larkzhh.xyz/tags/Android/"/>
    <category term="移动端" scheme="https://larkzhh.xyz/tags/%E7%A7%BB%E5%8A%A8%E7%AB%AF/"/>
    <content>
      <![CDATA[<p><code>KeyStore</code> 其实并不是 Android 独有，它是 <strong>JCA</strong>（Java 密码学架构）里的密钥库门面类，实现由 Provider 通过 <code>KeyStoreSpi</code> 提供。在 Android 上，系统注册了一个名为 <code>AndroidKeyStore</code> 的 Provider，把 <code>KeyStoreSpi</code> 接到了硬件密钥库上。它支持 <code>AndroidKeyStore</code>、<code>AndroidCAStore</code>、<code>BKS</code>、<code>PKCS12</code> 这几种 type。</p><h2 id="开局常量配置"><a href="#开局常量配置" class="headerlink" title="开局常量配置"></a>开局常量配置</h2><p>可以声明配置密钥别名、指定加密算法组合和初始化向量的长度（IV）。</p><ul><li>密钥名要具有唯一性，最好带个版本号，方便以后处理些东西。</li></ul><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">const</span> <span class="keyword">val</span> KEY_ALIAS = <span class="string">&quot;lime_token_key_v1&quot;</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">const</span> <span class="keyword">val</span> TRANSFORMATION = <span class="string">&quot;AES/GCM/NoPadding&quot;</span><span class="comment">// 算法字符串</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">const</span> <span class="keyword">val</span> IV_LENGTH = <span class="number">12</span><span class="comment">// GCM 规定的标准 IV 长度</span></span><br></pre></td></tr></table></figure><p>算法字符串是要按照 <code>算法 / 模式 / 填充</code> 的格式来配置，通常是 <code>&quot;AES/GCM/NoPadding&quot;</code>，<code>AES</code> 是一种对称加密算法，<code>GCM</code> 是工作模式，<code>NoPadding</code> 是无填充。GCM 是一种认证加密模式，不仅可以保证数据的机密性，还能保证数据的完整性（也就是防篡改）。</p><blockquote><p>IV 的作用是<strong>用同样的明文和同一把密钥，让每次加密出来的密文都不一样。</strong></p></blockquote><h2 id="打开密钥库并生成密钥"><a href="#打开密钥库并生成密钥" class="headerlink" title="打开密钥库并生成密钥"></a>打开密钥库并生成密钥</h2><h3 id="向系统索要一个密钥库对象"><a href="#向系统索要一个密钥库对象" class="headerlink" title="向系统索要一个密钥库对象"></a>向系统索要一个密钥库对象</h3><p>需要先指定密钥库类型，这里选 <code>&quot;AndroidKeyStore&quot;</code> 就行，数据由系统硬件保管，不用密码，所以适合用来加密本地数据、存令牌。但这时它还没有和系统的密钥服务连上，硬件密钥库又不需要从文件读数据、也不需要密码，所以打开密钥库时参数传 <code>null</code>  即可。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> ks = KeyStore.getInstance(<span class="string">&quot;AndroidKeyStore&quot;</span>).apply &#123; load(<span class="literal">null</span>) &#125;</span><br></pre></td></tr></table></figure><h3 id="在密钥库里生成密钥"><a href="#在密钥库里生成密钥" class="headerlink" title="在密钥库里生成密钥"></a>在密钥库里生成密钥</h3><p>要一个密钥生成器，填上你要的算法和提供者。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, <span class="string">&quot;AndroidKeyStore&quot;</span>)</span><br></pre></td></tr></table></figure><p>用 <code>KeyGenParameterSpec</code> 说明用途和规格，</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">generator.<span class="keyword">init</span>(</span><br><span class="line">    KeyGenParameterSpec.Builder(</span><br><span class="line">        KEY_ALIAS, <span class="comment">// 密钥名</span></span><br><span class="line">        PURPOSE_ENCRYPT or PURPOSE_DECRYPT<span class="comment">// 用途：允许加密和解密</span></span><br><span class="line">    )</span><br><span class="line">        .setBlockModes(BLOCK_MODE_GCM)<span class="comment">// 加密模式</span></span><br><span class="line">        .setEncryptionPaddings(ENCRYPTION_PADDING_NONE)<span class="comment">// 填充操作</span></span><br><span class="line">        .setKeySize(<span class="number">256</span>)<span class="comment">// 密钥长度，默认 128</span></span><br><span class="line">        .build()</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>生成 key 引用</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> generator.generateKey()</span><br></pre></td></tr></table></figure><p>附一张表供自查：</p><table><thead><tr><th>Provider（提供者）</th><th>它提供的 KeyStore 类型</th></tr></thead><tbody><tr><td><code>AndroidKeyStore</code></td><td><code>AndroidKeyStore</code></td></tr><tr><td><code>AndroidOpenSSL</code>（Conscrypt）</td><td><code>AndroidCAStore</code></td></tr><tr><td><code>BC</code>（BouncyCastle）</td><td><code>BKS</code>、<code>PKCS12</code>（还有 <code>BouncyCastle</code> 等别名）</td></tr></tbody></table><h2 id="加密"><a href="#加密" class="headerlink" title="加密"></a>加密</h2><p>创建 Cipher，获取加密工具。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> cipher = Cipher.getInstance(TRANSFORMATION)<span class="comment">// 算法 / 模式 / 填充</span></span><br></pre></td></tr></table></figure><p>初始化，指定密钥与选择加密操作。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cipher.<span class="keyword">init</span>(Cipher.ENCRYPT_MODE, SecretKey)<span class="comment">// 加密这把密钥</span></span><br></pre></td></tr></table></figure><p>取出 IV ，并执行加密。</p><p>对了，还要注意 IV 是在 <code>init()</code> 那一刻由系统生成的，切记要在 doFinal 前取出来，不然的话加密结束 <code>Cipher</code> 对象的状态会被重置。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> iv = cipher.iv</span><br><span class="line"><span class="keyword">val</span> encrypted = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))</span><br></pre></td></tr></table></figure><p>最后是组装，把 IV 和密文拼接，再转成 Base64 字符串</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Base64.encodeToString(iv + encrypted, Base64.NO_WRAP)</span><br></pre></td></tr></table></figure><p>这样加密之后就是这个形式啦。</p><p><code>Base64( [IV 12 字节] [密文] [校验标签 16 字节] )</code></p><h2 id="解密"><a href="#解密" class="headerlink" title="解密"></a>解密</h2><p>解密当然是加密的逆操作，和加密时反着来。</p><p>把 Base64 字符串还原成明文，把存进去的字符串变回字节数组。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> raw = Base64.decode(encoded, Base64.NO_WRAP)</span><br></pre></td></tr></table></figure><p>解铃还须系铃人，按约定的格式拆开后，再有请 <code>Cipher</code>  😄</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> iv = raw.copyOfRange(<span class="number">0</span>, IV_LENGTH)</span><br><span class="line"><span class="keyword">val</span> body = raw.copyOfRange(IV_LENGTH, raw.size)</span><br><span class="line"><span class="keyword">val</span> cipher = Cipher.getInstance(TRANSFORMATION)</span><br></pre></td></tr></table></figure><p>解密时要用原来的 IV ，需手动传，用 <code> GCMParameterSpec</code> 装 <strong>GCM 加解密需要的两个参数</strong> 。即校验标签长度和 IV。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cipher.<span class="keyword">init</span>(Cipher.DECRYPT_MODE, SecretKey, GCMParameterSpec(<span class="number">128</span>, iv))</span><br></pre></td></tr></table></figure><p>没啥可说的，依旧逆操作，然后你就解密成功了！</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">String(cipher.doFinal(body), Charsets.UTF_8)</span><br></pre></td></tr></table></figure><h2 id="附"><a href="#附" class="headerlink" title="附"></a>附</h2><p>此由 AI 生成 👇，未经核实研究，请辩证看待。</p><table><thead><tr><th>场景</th><th>用什么</th><th>密钥存哪</th></tr></thead><tbody><tr><td><strong>加密本地小数据</strong>（token、配置、缓存）——你这个项目</td><td><code>AES/GCM/NoPadding</code></td><td><code>AndroidKeyStore</code>（硬件，不可导出）</td></tr><tr><td>加密<strong>大文件</strong></td><td>同样 AES&#x2F;GCM，但走流式或分段</td><td><code>AndroidKeyStore</code></td></tr><tr><td>用户设了密码，要用密码加密数据</td><td><code>AES/GCM</code> + <code>PBKDF2</code> 从密码派生密钥</td><td>不需要密钥库，密钥每次从密码算出来</td></tr><tr><td>要<strong>跨设备&#x2F;跨平台</strong>解密（换手机还能解、后端也能解）</td><td><code>AES/GCM</code></td><td><strong>不能用 <code>AndroidKeyStore</code></strong>，密钥必须能导出或由密码派生</td></tr><tr><td>和<strong>后端交换</strong>加密数据</td><td><code>AES/GCM</code> 加密数据 + <code>RSA/OAEP</code>（或 ECDH）传密钥</td><td>后端公钥 + 本地随机密钥</td></tr><tr><td><strong>网络传输</strong></td><td>不要自己加密</td><td>交给 HTTPS&#x2F;TLS</td></tr><tr><td><strong>存密码</strong>（要能验证，不能还原）</td><td><code>PBKDF2WithHmacSHA256</code> + 随机 salt</td><td>不适用（不可逆）</td></tr><tr><td><strong>校验数据有没有被改</strong>（不保密）</td><td><code>HmacSHA256</code>（需要防伪造）或 <code>SHA-256</code>（只防意外损坏）</td><td>HMAC 的密钥放 <code>AndroidKeyStore</code></td></tr><tr><td><strong>签名&#x2F;身份验证</strong></td><td><code>Signature.getInstance(&quot;SHA256withECDSA&quot;)</code> 或 <code>SHA256withRSA</code></td><td><code>AndroidKeyStore</code>（<code>KeyPairGenerator</code>）</td></tr><tr><td><strong>必须解锁手机才能用</strong></td><td>同上 + <code>setUserAuthenticationRequired(true)</code></td><td><code>AndroidKeyStore</code></td></tr></tbody></table>]]>
    </content>
    <id>https://larkzhh.xyz/2026/11/02/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/Android/Android-Keystore/</id>
    <link href="https://larkzhh.xyz/2026/11/02/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/Android/Android-Keystore/"/>
    <published>2026-11-01T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p><code>KeyStore</code> 其实并不是 Android 独有，它是 <strong>JCA</strong>（Java 密码学架构）里的密钥库门面类，实现由 Provider 通过 <code>KeyStoreSpi</code> 提供。在 Android]]>
    </summary>
    <title>Android 安全存储——Android Keystore</title>
    <updated>2026-11-01T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>lark</name>
    </author>
    <category term="杂谈" scheme="https://larkzhh.xyz/categories/%E6%9D%82%E8%B0%88/"/>
    <category term="Android" scheme="https://larkzhh.xyz/tags/Android/"/>
    <content>
      <![CDATA[<h2 id="绪论"><a href="#绪论" class="headerlink" title="绪论"></a>绪论</h2><p>APP 开发者往往着眼于 <strong>UI 界面的搭建与功能实现</strong>，这很正常。毕竟一个 APP 的亮点与特色是什么，吸引用户与否，实用创新的功能和简洁美观的 UI 往往占大头。但近几年来，AI 正在把“搭建”这件事变得越来越廉价。去年这时我还在 CSDN 里搜索某功能逻辑怎么写；而现在写好提示词后，丢给 AI，几分钟便能生成一套像模像样的页面，连对应功能的实现也一并给了。</p><p>但在<strong>性能优化</strong>方面，很遗憾，我没打算昧着良心安慰你说 AI 做不到。目前，Agent 能跑构建、查日志、扒源码，只要你给的权限足够，让它在模拟器上跑一遍关键用户路径生成 <code>.prof</code> 文件，或是在真机上连续冷启动测平均启动时长等，这些只要写好提示词，或配好 Skill，都能实现。</p><p>当然，这一切都建立在你得知道有这些东西的前提上，AI 默认你没问，就不会为你主动考虑得面面俱到。而在短期主义的驱使下，绝大多数人也确实不会问，毕竟代码能跑就行，需求没提关我啥事。AI 时代下，<strong>知识面的宽度很大程度上影响产出质量的下限</strong>（深度也很重要），在你什么都不了解的情况下，就很容易变成 AI 给什么就验收什么。而模型输出靠的是统计学上最常见的写法，并不是针对某个具体约束的最优解，性能优化往往不在典型写法的射程之内。</p><p>此外，<strong>性能问题大多不会报错，也不影响功能的执行</strong>。掉帧、解码慢、主线程阻塞，这些在日志里可能只是一行不起眼的 warning；它们并不会让 APP 崩溃，但会一点点拉低应用的整体体验。</p><p>APP 性能优化不是一个三言两语就能讲完的话题，它主要涵盖以下部分：</p><ul><li><p><strong>启动速度优化</strong>：Application 初始化、SplashScreen 优化、Baseline Profiles 与 R8 编译期优化等。</p></li><li><p><strong>包体积优化</strong>：R8 代码混淆与压缩、资源裁剪、图片格式优化、ABI 分包与裁剪等。</p></li><li><p><strong>UI 渲染优化</strong>：XML 侧布局层级扁平化、Compose 侧智能重组与阶段跳过、列表渲染优化、硬件加速与动画优化等。</p></li><li><p><strong>内存管理优化</strong>：内存泄漏检测、缓存与资源管理、SparseArray 替代 HashMap 等。</p></li><li><p><strong>多线程与异步优化</strong>：主线程耗时操作剥离、Kotlin 协程调度、线程池复用等。</p></li><li><p><strong>网络与电量优化</strong>：网络请求合并、HTTP 缓存策略、数据压缩、连接池复用、后台任务管理等。</p></li><li><p><strong>性能监控与工具</strong>：Android Studio Profiler、Systrace &#x2F; Perfetto、LeakCanary 等分析工具。</p></li></ul>]]>
    </content>
    <id>https://larkzhh.xyz/2026/09/17/%E6%9D%82%E8%B0%88/%E6%88%91%E4%BB%AC%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/</id>
    <link href="https://larkzhh.xyz/2026/09/17/%E6%9D%82%E8%B0%88/%E6%88%91%E4%BB%AC%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    <published>2026-09-16T16:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="绪论"><a href="#绪论" class="headerlink" title="绪论"></a>绪论</h2><p>APP 开发者往往着眼于 <strong>UI 界面的搭建与功能实现</strong>，这很正常。毕竟一个 APP]]>
    </summary>
    <title>我们为什么要性能优化</title>
    <updated>2026-09-16T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>lark</name>
    </author>
    <category term="杂谈" scheme="https://larkzhh.xyz/categories/%E6%9D%82%E8%B0%88/"/>
    <category term="余庆" scheme="https://larkzhh.xyz/tags/%E4%BD%99%E5%BA%86/"/>
    <content>
      <![CDATA[<blockquote><p>积善之家，必有余庆。</p></blockquote><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p><strong>《余庆集》</strong>原名本非如此，在这里我也没打算分享它的真名，至于为什么现在叫这个名字，因为余庆只是我的笔名罢了。</p><p>关于《余庆集》由来的记忆我早已模糊，也不必深究，依稀记得是在我初中时期就已经存在了。时至今日，20 岁的我再次点开它时，竟有一种莫名的割裂感，几年前的我是我吗？</p><p>《余庆集》主要分为<strong>长生、虚无、风骚、破陈（尘）、余庆</strong>。</p><p>这里主要陈列余庆篇里的内容，有些部分因为某些原因选择糜烂在心里或做删改。</p><h2 id="余庆篇"><a href="#余庆篇" class="headerlink" title="余庆篇"></a>余庆篇</h2><blockquote><p>我讽喻过，我豪放过，我抒情过，我悼亡过，我怀古过，我山水过，我婉约过，我闺怨过，我羁旅过，我朦胧过… 过去的一切已逝去，将来又虚无缥缈……</p></blockquote><p>因为我对<strong>平仄与韵律</strong>研究的不深，这些姑且算古体诗吧。</p><h3 id="晚山行"><a href="#晚山行" class="headerlink" title="晚山行"></a>晚山行</h3><p>嶙峋乱石三六壁，晚山幽径寒草迷。 </p><p>古道悲魂凄凉诉，绕树残鸦不肯栖。</p><p> 我近十八不得意，清贫身披飞鹑衣。 </p><p>神憎鬼厌无人济，惟有孤影不相离。</p><p> 西风萧兮枝摇曳，星汉淡兮月渐移。 </p><p>忽逢远处有人家，烟袅烛明门半掩。 </p><p>主人怜我囊如洗，邀我共饮流水边。 </p><p>狗睡池鱼寂无声，梦中故园何时还？ </p><p>精怪鸠聚仙雀喧，妖言魅泣自难眠。 </p><p>长夜众咻何由彻？鸡啼晓破万邪湮。</p><hr><h3 id="无题"><a href="#无题" class="headerlink" title="无题"></a>无题</h3><p>独上东墙千万言，西楼凭栏叹流年。 </p><p>长卿七弦求凤凰，李郑竹笛歌纸鸢。 </p><p>月明宵静姮不寐，绛河鹊桥星连天。 </p><p>此情可待成追忆，只是现时已茫然。 </p><ul><li>东墙：战国时期，楚国著名诗人宋玉的东面邻居家有一个长得非常美丽的女儿，她爱慕宋玉的才能，每天登上墙头窥视宋玉。整整三年时间，宋玉没有看出她的心思而与她交往。 </li><li>长卿：司马相如，字长卿。 </li><li>七弦：古琴有七弦。琴七根弦分别是宫、商、角、征、羽、文、武。这七根弦的名称源于古代的音乐理论，即”五音”或”六律”，分别为”宫、商、角、征、羽”。后来，周文王和周武王分别加上了一根文弦和武弦，构成了现在的七弦。</li><li>求凤凰：指《凤求凰》相传司马相如为求爱卓文君所作名赋。 </li><li>李郑：据记载：“五代李郑于宫中作纸鸢，引线乘风为戏，后于鸢首以竹为笛，使风入竹，声如筝鸣，故名风筝。 </li><li>姮：嫦娥。 </li><li>鹊桥：传说乌鹊为牛郎织女搭桥 </li><li>此情…：化用李商隐《锦瑟》“此情可待成追忆，只是当时已惘然”。</li></ul><hr><h3 id="君悦"><a href="#君悦" class="headerlink" title="君悦"></a>君悦</h3><p>朱伞青瓦巷，灯寒玉雨凉，花泪与湿绢，一杯共赏月昏黄。        </p><p>高烛照红妆，清辉宵梦长，等君又一年，倩倩幽影入空床。 </p><ul><li>君悦：2023年，5月20日作。 </li><li>玉雨：梨花。</li></ul><h3 id="注孤笙"><a href="#注孤笙" class="headerlink" title="注孤笙"></a>注孤笙</h3><p>枕泪痕，独守空床。菱花残妆，为谁而伤？双鲤焚至半张。闺阁戚戚声，闻孤笙，舞清风。       </p><p> 想当年，为君割爱。多少清辞，道尽情痴，而今恍如隔世。青楼一梦中，两情通？觅无踪。 </p><ul><li>注孤笙：2022年，11月19日作。 </li><li>菱花：镜子。 </li><li>双鲤：代指书信。 </li><li>青楼：青漆涂饰的豪华精致的楼房，指豪门贵族的高档住宅。因为古代普通百姓住的是茅草屋或土坯房，只有豪门贵族才有资格用青漆装饰房屋。</li></ul><hr><h3 id="思归"><a href="#思归" class="headerlink" title="思归"></a>思归</h3><p>思归思归,啼血子规。身陷囹圄,雨雪霏霏。思归思归,孤影残杯。休沐廿日,心如死灰。思归思归,流年似水。长路漫漫,踽踽独归。</p><hr><p>补充中…</p>]]>
    </content>
    <id>https://larkzhh.xyz/2026/09/17/%E6%9D%82%E8%B0%88/%E4%BD%99%E5%BA%86%E9%9B%86/%E4%BD%99%E5%BA%86/</id>
    <link href="https://larkzhh.xyz/2026/09/17/%E6%9D%82%E8%B0%88/%E4%BD%99%E5%BA%86%E9%9B%86/%E4%BD%99%E5%BA%86/"/>
    <published>2026-09-16T16:00:00.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>积善之家，必有余庆。</p>
</blockquote>
<h2 id="前言"><a href="#前言" class="headerlink"]]>
    </summary>
    <title>余庆集·余庆</title>
    <updated>2026-09-16T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>lark</name>
    </author>
    <category term="计算机" scheme="https://larkzhh.xyz/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/"/>
    <category term="声学" scheme="https://larkzhh.xyz/tags/%E5%A3%B0%E5%AD%A6/"/>
    <category term="音频编码" scheme="https://larkzhh.xyz/tags/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81/"/>
    <content>
      <![CDATA[<h2 id="声学与数字信号处理"><a href="#声学与数字信号处理" class="headerlink" title="声学与数字信号处理"></a>声学与数字信号处理</h2><p>将声音由模拟信号转化成数字信号需要<strong>采样、量化、编码</strong>。</p><p><strong>PCM</strong> 脉冲编码调制，就是这三个步骤的统称。转化后的数据就是没有压缩过的原始音频数据。经由此过程产生的、没有任何处理的二进制数字序列也可以称作 <strong>PCM</strong> 或 PCM音频数据流。</p><p>那模拟声音信号是怎么来的呢？答案是麦克风设备采集。我们说话时声波在空气中传播，麦克风内部的振膜被声波推动产生振动，进而转换成连续变化的微弱电压信号。</p><p>PCM 过程是在 ADC（模数转换器）上执行的。ADC 是声卡内部的一个核心元件。</p><ul><li><p><strong>采样</strong> 简单来说，就是把连续变化的模拟信号每隔一定时间取个值，也就是进行离散化的切割，并获取采样点的模拟值。可想而知，点越多越密集，越能近似表示原来的波形，声音保真度越高，质量越好；但记录数据越多，存储容量越高。</p><p>每秒采样声音的次数称作<strong>采样频率</strong>，单位是 Hz 。</p></li><li><p><strong>量化</strong>  将采样点的值分级量化，变换到最接近的数字，也就是用有限个数近似表示原来连续变化的值。分级越多，采样点取值更精确，声音质量越好；但同理二进制的位数更多，存储容量越大。</p><p>量化值的二进制位数称作<strong>量化位数</strong>，也叫<strong>位深</strong></p></li><li><p><strong>编码</strong> 将这些整数转换成二进制码。</p></li></ul><p>有了这些数字信号，我们就能够保存、传输或者压缩了。</p><h2 id="声道"><a href="#声道" class="headerlink" title="声道"></a>声道</h2><p>简单来说，就是声音在录制或播放时，相互独立的音频信号通道。通道越多，声音的空间方位感和立体感就越强。单声道最常见，只有一路信号，没有方向感；立体声（双声道）有左右两路独立信号，能让人分辨出声音的方向；多声道（如5.1声道）则通过多个发音点，给人声音包围环绕的体验。</p><h2 id="压缩算法"><a href="#压缩算法" class="headerlink" title="压缩算法"></a>压缩算法</h2><h3 id="有损压缩"><a href="#有损压缩" class="headerlink" title="有损压缩"></a>有损压缩</h3><p>通过利用心理声学模型，剔除人耳不易察觉的声音信息，达到<strong>减少文件体积</strong>。常见算法有 MP3、AAC、Opus 等。</p><p>AAC 高级音频编码，是一种<strong>有损音频压缩算法</strong>。它的目的是取代 MP3，在相同码率下，它比 MP3质量更优，文件更小。它的文件格式通常是 .m4a、.aac 。</p><h3 id="无损压缩"><a href="#无损压缩" class="headerlink" title="无损压缩"></a>无损压缩</h3><p>就是通过预测编码、熵编码等技术寻找数据里的重复规律，用更短的代码去替换重复的内容，达到减少文件体积，同时保证解压后的数据和原始数据一模一样，不丢失任何细节。常见的有 FLAC、ALAC、APE 等。</p><h2 id="音频文件格式"><a href="#音频文件格式" class="headerlink" title="音频文件格式"></a>音频文件格式</h2><h3 id="纯格式"><a href="#纯格式" class="headerlink" title="纯格式"></a>纯格式</h3><ul><li><strong>WAV</strong>：微软的标准外壳。里面通常直接装未经压缩的原始 PCM 数据，音质最完美，但体积巨大。</li><li><strong>AIFF</strong>：苹果的标准外壳。相当于 Mac 系统里的 WAV 。</li><li><strong>M4A</strong>：苹果推广的纯音频外壳。里面可以装 AAC 算法压缩的数据，也可以装 ALAC 无损算法的数据。</li></ul><h3 id="算法与格式“同名”的格式"><a href="#算法与格式“同名”的格式" class="headerlink" title="算法与格式“同名”的格式"></a>算法与格式“同名”的格式</h3><ul><li><strong>.mp3 文件</strong>：里面装 MP3 算法压缩出来的数据。</li><li><strong>.flac 文件</strong>：里面装的也是 FLAC 算法压缩出来的数据。</li><li><strong>.aac 文件</strong>：里面装的也是 AAC 算法压缩出来的数据。</li></ul><h3 id="特殊用途格式"><a href="#特殊用途格式" class="headerlink" title="特殊用途格式"></a>特殊用途格式</h3><ul><li><strong>MIDI (.mid)</strong>：不是声音文件，里面只有一张乐谱指令，告诉设备什么时候、用什么乐器、弹哪个音，里面没有声音波形数据。</li><li><strong>DSD</strong>：专为 SACD 光盘设计的超高品质格式，采用 1bit 的极高频率采样。</li></ul><h2 id="比特率"><a href="#比特率" class="headerlink" title="比特率"></a>比特率</h2><p>又叫码率，就是每秒钟传输或处理的二进制数据量，单位是 bps。</p><p>音频压缩后每秒声音数据多大用比特率衡量，常用单位 kbps，比特率越高，保留的声音细节越多，音质越好，但文件也越大。</p><p>对于人声（如语音消息、语音识别、播客等），<strong>64 kbps</strong> 是一个非常经典的标准。在这个码率下，人声听起来非常清晰，且文件体积小，适合网络传输；如果是像听歌这种，则需要更高。</p><h2 id="奈奎斯特定理"><a href="#奈奎斯特定理" class="headerlink" title="奈奎斯特定理"></a>奈奎斯特定理</h2><p>没研究原理，不懂。</p><p>也叫采样定理，结论主要是<strong>采样频率必须大于或等于信号最高频率的两倍</strong>，才能不失真地还原原来的声音。</p><p>eg: 人耳听觉的上限频率约为 20kHz，根据奈奎斯特定理，采样率至少需要 40kHz。因此，标准 CD 的采样率为 44.1kHz 。</p>]]>
    </content>
    <id>https://larkzhh.xyz/2026/09/14/%E8%AE%A1%E7%AE%97%E6%9C%BA/%E4%B8%80%E4%BA%9B%E5%A3%B0%E5%AD%A6%E7%BC%96%E7%A0%81%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/</id>
    <link href="https://larkzhh.xyz/2026/09/14/%E8%AE%A1%E7%AE%97%E6%9C%BA/%E4%B8%80%E4%BA%9B%E5%A3%B0%E5%AD%A6%E7%BC%96%E7%A0%81%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/"/>
    <published>2026-09-13T16:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="声学与数字信号处理"><a href="#声学与数字信号处理" class="headerlink"]]>
    </summary>
    <title>一些声学编码学习记录</title>
    <updated>2026-09-17T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>lark</name>
    </author>
    <category term="Android" scheme="https://larkzhh.xyz/categories/Android/"/>
    <category term="Android" scheme="https://larkzhh.xyz/tags/Android/"/>
    <category term="移动端" scheme="https://larkzhh.xyz/tags/%E7%A7%BB%E5%8A%A8%E7%AB%AF/"/>
    <category term="TTS" scheme="https://larkzhh.xyz/tags/TTS/"/>
    <content>
      <![CDATA[<p>对的，就是 TTS，文字朗读</p><p>安卓平台提供 TTS 相关功能的 API，对于基本的文字转语音需求还是足够的，当然朗读的音色似乎很人机，要实现更丰富的语音还是调用云端 TTS 相关 API 吧。</p><h2 id="初始化-TextToSpeech"><a href="#初始化-TextToSpeech" class="headerlink" title="初始化 TextToSpeech"></a>初始化 TextToSpeech</h2><p>初始化 <code>TextToSpeech</code> ，因为初始化过程是异步的，需要在回调中检查初始化状态是否成功，等引擎加载成功后再进一步配置、播报。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">tts = TextToSpeech(context.applicationContext) &#123; status -&gt;</span><br><span class="line"><span class="keyword">if</span> (status == TextToSpeech.SUCCESS) &#123;</span><br><span class="line">        configure(tts)<span class="comment">// 配置</span></span><br><span class="line">        startSpeaking(messageId, content)<span class="comment">// 朗读</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h2><p>配置的话，可以进行一系列配置，太晚了先睡觉吧。</p>]]>
    </content>
    <id>https://larkzhh.xyz/2026/09/14/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/Android/%E8%AE%A9%E6%96%87%E5%AD%97%E8%AF%B4%E8%AF%9D/</id>
    <link href="https://larkzhh.xyz/2026/09/14/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/Android/%E8%AE%A9%E6%96%87%E5%AD%97%E8%AF%B4%E8%AF%9D/"/>
    <published>2026-09-13T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>对的，就是 TTS，文字朗读</p>
<p>安卓平台提供 TTS 相关功能的 API，对于基本的文字转语音需求还是足够的，当然朗读的音色似乎很人机，要实现更丰富的语音还是调用云端 TTS 相关 API 吧。</p>
<h2 id="初始化-TextToSpeech"><a]]>
    </summary>
    <title>让文字说话</title>
    <updated>2026-09-13T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>lark</name>
    </author>
    <id>https://larkzhh.xyz/2026/09/13/%E6%9D%82%E8%B0%88/%E6%97%A0/</id>
    <link href="https://larkzhh.xyz/2026/09/13/%E6%9D%82%E8%B0%88/%E6%97%A0/"/>
    <published>2026-09-13T15:49:37.174Z</published>
    <updated>2026-09-13T15:49:37.174Z</updated>
  </entry>
  <entry>
    <author>
      <name>lark</name>
    </author>
    <category term="Android" scheme="https://larkzhh.xyz/categories/Android/"/>
    <category term="Android" scheme="https://larkzhh.xyz/tags/Android/"/>
    <category term="移动端" scheme="https://larkzhh.xyz/tags/%E7%A7%BB%E5%8A%A8%E7%AB%AF/"/>
    <content>
      <![CDATA[<p>在 Android 项目开发中，统一管理插件版本和第三方库依赖十分重要，Gradle 提供的 <strong>Version Catalog (TOML)</strong> 方便我们通过<code>libs.versions.toml</code>文件<strong>集中管理所有依赖和插件的版本</strong>。</p><p>TOML 文件在项目根目录的 <code>gradle/libs.versions.toml</code> 里，如果文件视图中选择 <code>Android</code> 的话也很容易在 Gradle Scripts 里找到。</p><h1 id="古老硬编码写法"><a href="#古老硬编码写法" class="headerlink" title="古老硬编码写法"></a>古老硬编码写法</h1><p><em>喂，这样明明很方便的好吧</em></p><p>在早期的 Gradle 配置中，我们通常在 <code>build.gradle.kts</code> 文件中直接声明插件和依赖库。不过现在新版的 Android Studio 刚创建项目后应该也看不见它的身影了。在一些教程里为了方便，依赖引用声明常常直接导入。</p><p>——历史的博物馆——</p><p>根项目的 <code>build.gradle.kts</code></p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">plugins &#123; </span><br><span class="line">    id(<span class="string">&quot;com.android.application&quot;</span>) version <span class="string">&quot;9.2.1&quot;</span> apply <span class="literal">false</span>  </span><br><span class="line">    id(<span class="string">&quot;org.jetbrains.kotlin.plugin.compose&quot;</span>) version <span class="string">&quot;2.2.10&quot;</span> apply <span class="literal">false</span> </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>模块内应用与依赖库引用 <code>build.gradle.kts</code></p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">plugins &#123;  </span><br><span class="line">   id(<span class="string">&quot;com.android.application&quot;</span>) </span><br><span class="line">   id(<span class="string">&quot;org.jetbrains.kotlin.plugin.compose&quot;</span>)  </span><br><span class="line">&#125;</span><br><span class="line">dependencies&#123;</span><br><span class="line">    implementation(<span class="string">&quot;androidx.activity:activity-compose:1.13.0&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="使用TOML统一管理"><a href="#使用TOML统一管理" class="headerlink" title="使用TOML统一管理"></a>使用TOML统一管理</h1><p>TOML 文件最常用的三个表是<code>[versions]</code> 、<code>[libraries]</code> 、 <code>[plugins]</code> 。</p><p> 分别用来定义版本号、声明具体的第三方库、管理 Gradle 插件。</p><h2 id="插件管理"><a href="#插件管理" class="headerlink" title="插件管理"></a>插件管理</h2><p>对于插件，我们可以直接在 <code>[plugins]</code> 中定义插件：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[plugins]</span></span><br><span class="line"><span class="attr">android</span> = <span class="string">&quot;com.android.application:9.2.1&quot;</span></span><br></pre></td></tr></table></figure><p>然后我们就可以 <code>build.gradle.kts</code> 中通过使用 <code>alias</code> 引用 TOML 中的插件啦。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">alias(libs.plugins.android.application) apply <span class="literal">false</span></span><br></pre></td></tr></table></figure><p>当然，为了分离版本信息，<strong>方便集中统一版本</strong>，我们可以使用<strong>内联表</strong>的方式，包裹键值对，用 <code>version.ref</code> 引用 <code>version</code> 表里的版本变量。</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[versions]</span></span><br><span class="line"><span class="attr">agp</span> = <span class="string">&quot;9.2.1&quot;</span></span><br><span class="line"><span class="section">[plugins]</span></span><br><span class="line"><span class="attr">android-application</span> = &#123; id = <span class="string">&quot;com.android.application&quot;</span>, version.ref = <span class="string">&quot;agp&quot;</span> &#125;</span><br></pre></td></tr></table></figure><p>这样之后，版本号被集中管理起来，就便于统一升级了。</p><h2 id="依赖管理"><a href="#依赖管理" class="headerlink" title="依赖管理"></a>依赖管理</h2><p>依赖同理，如下：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[versions]</span></span><br><span class="line"><span class="attr">activityCompose</span> = <span class="string">&quot;1.13.0&quot;</span></span><br><span class="line"><span class="section">[libraries]</span></span><br><span class="line"><span class="attr">androidx-activity-compose</span> = &#123; group = <span class="string">&quot;androidx.activity&quot;</span>, name = <span class="string">&quot;activity-compose&quot;</span>, version.ref = <span class="string">&quot;activityCompose&quot;</span> &#125;</span><br><span class="line">dependencies &#123;</span><br><span class="line">        implementation(libs.androidx.activity.compose)</span><br><span class="line">        //在模块中引用时，Gradle会自动将TOML中的连字符（-）转换为Kotlin中的点（.）</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为了进一步简化依赖引用，我们还可以在 <code>toml</code> 文件里定义一个数组用来装依赖变量名，将多个相关的依赖组合在一起。</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[bundles]</span></span><br><span class="line"><span class="attr">androidx</span> = [<span class="string">&quot;androidx-core-ktx&quot;</span>,<span class="string">&quot;androidx-lifecycle-runtime-ktx&quot;</span>]</span><br><span class="line"><span class="comment">#你在[libraries]里定义的变量</span></span><br></pre></td></tr></table></figure><p>然后在代码里就能一次性引入：</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">implementation(libs.bundles.androidx)</span><br></pre></td></tr></table></figure><h2 id="使用BOM进行版本对齐"><a href="#使用BOM进行版本对齐" class="headerlink" title="使用BOM进行版本对齐"></a>使用BOM进行版本对齐</h2><p><code>BOM</code> 即 <code>Bill Of Materials</code> 对于像 Jetpack Compose 这样的大型库集合， Google 提供了 BOM 来统一管理版本。它提供一个<strong>清单版本</strong>，以实现库的版本的<strong>自动对齐</strong>。你只需引入 BOM ，就不用手动指定每个库的版本。 在 <code>libs.versions.toml</code> 中定义 BOM ：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[versions]</span></span><br><span class="line"><span class="attr">composeBom</span> = <span class="string">&quot;2026.02.01&quot;</span></span><br><span class="line"><span class="section">[libraries]</span></span><br><span class="line"><span class="attr">androidx-compose-bom</span> = &#123; group = <span class="string">&quot;androidx.compose&quot;</span>, name = <span class="string">&quot;compose-bom&quot;</span>, version.ref = <span class="string">&quot;composeBom&quot;</span> &#125;</span><br></pre></td></tr></table></figure><p>在<code>build.gradle.kts</code>中应用 BOM</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">implementation(platform(libs.androidx.compose.bom))</span><br></pre></td></tr></table></figure><p>当然，如果需要在<code>debug</code>或<code>androidTest</code>中也使用 BOM ，可以把 BOM 单独抽出来存个变量</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> composeBom = platform(libs.androidx.compose.bom)</span><br><span class="line">implementation(composeBom)</span><br><span class="line">androidTestImplementation(composeBom)</span><br><span class="line">debugImplementation(composeBom)</span><br></pre></td></tr></table></figure><p>哦对了，不要忘了 <strong>Sync now</strong> ( ∩_∩ ) </p>]]>
    </content>
    <id>https://larkzhh.xyz/2026/07/01/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/Android/TOM%E4%BE%9D%E8%B5%96%E7%AE%A1%E7%90%86/</id>
    <link href="https://larkzhh.xyz/2026/07/01/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/Android/TOM%E4%BE%9D%E8%B5%96%E7%AE%A1%E7%90%86/"/>
    <published>2026-06-30T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>在 Android 项目开发中，统一管理插件版本和第三方库依赖十分重要，Gradle 提供的 <strong>Version Catalog (TOML)</strong>]]>
    </summary>
    <title>TOML依赖管理</title>
    <updated>2026-06-30T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>lark</name>
    </author>
    <category term="HarmonyOS" scheme="https://larkzhh.xyz/categories/HarmonyOS/"/>
    <category term="HarmonyOS" scheme="https://larkzhh.xyz/tags/HarmonyOS/"/>
    <category term="移动端" scheme="https://larkzhh.xyz/tags/%E7%A7%BB%E5%8A%A8%E7%AB%AF/"/>
    <category term="ArkTS" scheme="https://larkzhh.xyz/tags/ArkTS/"/>
    <content>
      <![CDATA[<h2 id="ArkTS-入门"><a href="#ArkTS-入门" class="headerlink" title="ArkTS 入门"></a>ArkTS 入门</h2><h3 id="ArkTS-简介"><a href="#ArkTS-简介" class="headerlink" title="ArkTS 简介"></a>ArkTS 简介</h3><p>ArkTS 是 HarmonyOS APP 应用开发的默认开发语言，在 TypeScript 生态基础上做了进一步扩展和规范定义，强化了开发期的静态检查和分析，提升了程序执行的稳定性和性能。ArkTS 同时也支持与 TS&#x2F;JS 高效互操作。</p><p>它与 TS 的区别主要有：</p><ul><li>类型更加严格，强制静态类型，禁用了 any 类型，而且强制显式标注</li><li>通过 ArkCompiler（方舟编译器）AOT 编译为方舟字节码或机器码，在方舟运行时中执行。</li><li>删减了部分动态特性，如 <code>with</code> 语句、运行时类型反射等。</li><li>扩展了并发模型，除了 <code>Promise</code> 外，特有 <code>TaskPool</code>与 <code>Worker</code>。</li></ul><p>在网上很容易看到类似这种图来表示  JS , TS , Arkts 关系的，但由于 ArtTS 的限制和拓展，严格来说 ArkTS 并非 TS 的超集，因为它并非像 TS 之于 JS 那样基本上是只增加不减少，而是在 TS 语法之上做了减法（如上面所说的砍掉动态特性），在平台能力上进行拓展（增加了鸿蒙专用 的API）。</p><p><img src="/img/arkts.png" alt="ArkTS"></p><p>下次更新将在后文追加……</p>]]>
    </content>
    <id>https://larkzhh.xyz/2026/06/30/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/HarmonyOS/ArkTS%EF%BC%88%E4%B8%80%EF%BC%89/</id>
    <link href="https://larkzhh.xyz/2026/06/30/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/HarmonyOS/ArkTS%EF%BC%88%E4%B8%80%EF%BC%89/"/>
    <published>2026-06-29T16:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="ArkTS-入门"><a href="#ArkTS-入门" class="headerlink" title="ArkTS 入门"></a>ArkTS 入门</h2><h3 id="ArkTS-简介"><a href="#ArkTS-简介"]]>
    </summary>
    <title>ArkTS</title>
    <updated>2026-06-29T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>lark</name>
    </author>
    <category term="Android" scheme="https://larkzhh.xyz/categories/Android/"/>
    <category term="Android" scheme="https://larkzhh.xyz/tags/Android/"/>
    <category term="移动端" scheme="https://larkzhh.xyz/tags/%E7%A7%BB%E5%8A%A8%E7%AB%AF/"/>
    <content>
      <![CDATA[<p><code>SharedPreferences</code> 想必都知道吧，是所有安卓人学数据持久化的入门必修课，它可以通过简单的<strong>键值对</strong> (Key-Value) 方式来轻松存储和获取各种基本数据类型，在开发中常用于存储简单的应用配置、登录状态和用户偏好等等。</p><p>但它也有许多令人诟病的，比如无论 <code>edit</code> 还是 <code>commit</code> 都有可能引发 ANR ，容易阻塞线程，类型不安全，数据一致性难保等等。</p><p><em>反正我目前是还没遇到过</em></p><p><em>所以都 2026 了还有人在用 <strong>SP</strong> 吗？</em></p><blockquote><p>官方：Welcome Jetpack DataStore, now in alpha - a new and improved data storage solution aimed at replacing SharedPreferences.</p></blockquote><p>？？？？？？</p><h2 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h2><p>DataStore 是个什么❓</p><p><em><del>依旧美美照搬</del></em></p><blockquote><p>Jetpack DataStore 是一种数据存储解决方案，让您可以使用<a href="https://developers.google.cn/protocol-buffers?hl=zh-cn">协议缓冲区</a>存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。</p></blockquote><h2 id="DataStore-的分类"><a href="#DataStore-的分类" class="headerlink" title="DataStore 的分类"></a>DataStore 的分类</h2><p>DataStore 分为<strong>两种</strong>，如下：</p><ul><li><strong><code>Preferences DataStore</code></strong> : 嗯… 和 SP 类似，就把它当成 SP 改良版吧，它可以用于存储简单的<strong>键值对</strong> (Key-Value)，不需要预定义架构，也不确保类型安全。</li><li><strong><code>Proto DataStore</code></strong>  : 用于存储<strong>类型化对象</strong>，用于存储自定义的强类型对象，使用<code>ProtoBuf</code> 来定义架构，确保<strong>类型安全</strong>(本文暂不对它展开介绍)。</li></ul><h3 id="优势在我："><a href="#优势在我：" class="headerlink" title="优势在我："></a>优势在我：</h3><ul><li>因为是基于<strong>协程</strong>的，所以<strong>完全异步</strong>，主线程安全</li><li><code>DataStore</code> 使用 <code>Flow</code> 暴露数据，支持<strong>响应式</strong></li><li><strong>事务性</strong>更新，原子操作</li><li>支持 <code>try-catch</code></li><li>……</li></ul><h2 id="添加依赖"><a href="#添加依赖" class="headerlink" title="添加依赖"></a>添加依赖</h2><p>这个不必多说，在 <code>build.gradle</code> 引入依赖。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">implementation(<span class="string">&quot;androidx.datastore:datastore-preferences:1.2.1&quot;</span>)</span><br></pre></td></tr></table></figure><h2 id="创建DataStore"><a href="#创建DataStore" class="headerlink" title="创建DataStore"></a>创建DataStore</h2><p>可以通过<strong>扩展函数</strong>和<strong>属性委托</strong> <code>preferencesDataStore</code> 来创建  <code>DataStore&lt;Preferences&gt;</code> 实例。把它放在文件顶层，以保证<strong>全局单例</strong>。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> Context.dataStore: DataStore&lt;Preferences&gt; <span class="keyword">by</span> preferencesDataStore(name = <span class="string">&quot;XXX&quot;</span>)</span><br></pre></td></tr></table></figure><h2 id="定义Keys"><a href="#定义Keys" class="headerlink" title="定义Keys"></a>定义Keys</h2><p>与 SP 不同直接传字符串不一样，DataStore 需要使用专门的函数来定义 Key 。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">object</span> PrefsKeys &#123;</span><br><span class="line">     <span class="keyword">val</span> DARK_MODE = booleanPreferencesKey(<span class="string">&quot;dark_mode&quot;</span>)</span><br><span class="line">     <span class="keyword">val</span> USERNAME = stringPreferencesKey(<span class="string">&quot;username&quot;</span>)</span><br><span class="line">     <span class="keyword">val</span> AGE = intPreferencesKey(<span class="string">&quot;age&quot;</span>) &#125;</span><br></pre></td></tr></table></figure><h2 id="读取数据"><a href="#读取数据" class="headerlink" title="读取数据"></a>读取数据</h2><p>定义好 <code>key</code> 后，就能用 <code>DataStore.data</code> 读取数据啦，它会返回一个  <code>Flow&lt;T&gt;</code>  对象，数据变化时会重新发出。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> userNameFlow: Flow&lt;String&gt; = context.dataStore.<span class="keyword">data</span>.map &#123; preferences -&gt;</span><br><span class="line">    preferences[USER_NAME_KEY] ?: <span class="string">&quot;访客&quot;</span>   </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="写入数据"><a href="#写入数据" class="headerlink" title="写入数据"></a>写入数据</h2><p>写入时使用的是  <code>DataStore.edit</code> 它是 <del><em>上吊</em></del> 挂起 的，所以必须在<strong>协程作用域</strong>中执行。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">saveUserName</span><span class="params">( name: <span class="type">String</span>)</span></span> &#123;</span><br><span class="line">    context.dataStore.edit &#123; settings -&gt;</span><br><span class="line">    settings[PrefsKeys.USERNAME] = name settings</span><br><span class="line">    &#125; </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="迁移"><a href="#迁移" class="headerlink" title="迁移"></a>迁移</h2><p>如果老项目需要迁移，构建时传入 <code>SharedPreferencesMigration</code> 即可。</p><p><strong>温馨提示：新旧请key名保持一致</strong></p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> Context.dataStore <span class="keyword">by</span> preferencesDataStore(</span><br><span class="line">    name = <span class="string">&quot;new&quot;</span>, <span class="comment">// DataStore 名字</span></span><br><span class="line">    produceMigrations = &#123; context -&gt;</span><br><span class="line">        listOf(SharedPreferencesMigration(context,<span class="string">&quot;old&quot;</span>))<span class="comment">//旧的 SP 名字</span></span><br><span class="line">    &#125;  </span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>……</p><p><em>嗯…好麻烦，还是回去继续接着开心使用SharedPreferences了！</em></p><h2 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h2><p>额，说的再多不如直接上代码运行（以下代码由 AI 生成并人工微调，可运行）</p><h3 id="DataStoreManager"><a href="#DataStoreManager" class="headerlink" title="DataStoreManager"></a>DataStoreManager</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> android.content.Context  </span><br><span class="line"><span class="keyword">import</span> androidx.datastore.core.DataStore  </span><br><span class="line"><span class="keyword">import</span> androidx.datastore.preferences.core.Preferences  </span><br><span class="line"><span class="keyword">import</span> androidx.datastore.preferences.core.edit  </span><br><span class="line"><span class="keyword">import</span> androidx.datastore.preferences.core.intPreferencesKey  </span><br><span class="line"><span class="keyword">import</span> androidx.datastore.preferences.core.stringPreferencesKey  </span><br><span class="line"><span class="keyword">import</span> androidx.datastore.preferences.preferencesDataStore  </span><br><span class="line"><span class="keyword">import</span> kotlinx.coroutines.flow.Flow  </span><br><span class="line"><span class="keyword">import</span> kotlinx.coroutines.flow.map  </span><br><span class="line">  </span><br><span class="line"><span class="keyword">val</span> Context.dataStore: DataStore&lt;Preferences&gt; <span class="keyword">by</span> preferencesDataStore(name = <span class="string">&quot;user_prefs&quot;</span>)  </span><br><span class="line"><span class="keyword">class</span> <span class="title class_">DataStoreManager</span>(<span class="keyword">private</span> <span class="keyword">val</span> context: Context) &#123;</span><br><span class="line">    <span class="comment">//定义 Keys    companion object &#123;</span></span><br><span class="line">        <span class="keyword">val</span> USER_NAME_KEY = stringPreferencesKey(<span class="string">&quot;user_name&quot;</span>)</span><br><span class="line">        <span class="keyword">val</span> CLICK_COUNT_KEY = intPreferencesKey(<span class="string">&quot;click_count&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//读取数据</span></span><br><span class="line">    <span class="keyword">val</span> userNameFlow: Flow&lt;String&gt; = context.dataStore.<span class="keyword">data</span>.map &#123; preferences -&gt;</span><br><span class="line">        preferences[USER_NAME_KEY] ?: <span class="string">&quot;访客&quot;</span> <span class="comment">//默认值为访客</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">val</span> clickCountFlow: Flow&lt;<span class="built_in">Int</span>&gt; = context.dataStore.<span class="keyword">data</span>.map &#123; preferences -&gt;</span><br><span class="line">        preferences[CLICK_COUNT_KEY] ?: <span class="number">0</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 写入数据</span></span><br><span class="line">    <span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">saveUserName</span><span class="params">(name: <span class="type">String</span>)</span></span> &#123;</span><br><span class="line">        context.dataStore.edit &#123; preferences -&gt;</span><br><span class="line">            preferences[USER_NAME_KEY] = name</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">incrementClickCount</span><span class="params">()</span></span> &#123;</span><br><span class="line">        context.dataStore.edit &#123; preferences -&gt;</span><br><span class="line">            <span class="keyword">val</span> currentCounterValue = preferences[CLICK_COUNT_KEY] ?: <span class="number">0</span></span><br><span class="line">            preferences[CLICK_COUNT_KEY] = currentCounterValue + <span class="number">1</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="ViewModel"><a href="#ViewModel" class="headerlink" title="ViewModel"></a>ViewModel</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> android.app.Application  </span><br><span class="line"><span class="keyword">import</span> androidx.lifecycle.AndroidViewModel  </span><br><span class="line"><span class="keyword">import</span> androidx.lifecycle.viewModelScope  </span><br><span class="line"><span class="keyword">import</span> kotlinx.coroutines.launch  </span><br><span class="line">  </span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MainViewModel</span>(application: Application) : AndroidViewModel(application) &#123;  </span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">val</span> dataStoreManager = DataStoreManager(application)</span><br><span class="line">    <span class="keyword">val</span> userNameFlow = dataStoreManager.userNameFlow</span><br><span class="line">    <span class="keyword">val</span> clickCountFlow = dataStoreManager.clickCountFlow</span><br><span class="line">    <span class="comment">//保存名字</span></span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">saveName</span><span class="params">(name: <span class="type">String</span>)</span></span> &#123;</span><br><span class="line">        viewModelScope.launch &#123;</span><br><span class="line">            dataStoreManager.saveUserName(name)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//增加次数</span></span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">incrementCount</span><span class="params">()</span></span> &#123;</span><br><span class="line">        viewModelScope.launch &#123;</span><br><span class="line">            dataStoreManager.incrementClickCount()</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Activity"><a href="#Activity" class="headerlink" title="Activity"></a>Activity</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> android.os.Bundle</span><br><span class="line"><span class="keyword">import</span> androidx.appcompat.app.AppCompatActivity</span><br><span class="line"><span class="keyword">import</span> android.widget.Button</span><br><span class="line"><span class="keyword">import</span> android.widget.EditText</span><br><span class="line"><span class="keyword">import</span> android.widget.TextView</span><br><span class="line"><span class="keyword">import</span> androidx.activity.viewModels</span><br><span class="line"><span class="keyword">import</span> androidx.lifecycle.Lifecycle</span><br><span class="line"><span class="keyword">import</span> androidx.lifecycle.lifecycleScope</span><br><span class="line"><span class="keyword">import</span> androidx.lifecycle.repeatOnLifecycle</span><br><span class="line"><span class="keyword">import</span> kotlinx.coroutines.launch</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MainActivity</span> : <span class="type">AppCompatActivity</span>() &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">val</span> viewModel: MainViewModel <span class="keyword">by</span> viewModels()</span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line">        setContentView(R.layout.activity_main)</span><br><span class="line">        <span class="comment">//都2026了还用findViewById？</span></span><br><span class="line">        <span class="keyword">val</span> tvGreeting = findViewById&lt;TextView&gt;(R.id.tvGreeting)</span><br><span class="line">        <span class="keyword">val</span> tvCounter = findViewById&lt;TextView&gt;(R.id.tvCounter)</span><br><span class="line">        <span class="keyword">val</span> etName = findViewById&lt;EditText&gt;(R.id.etName)</span><br><span class="line">        <span class="keyword">val</span> btnSaveName = findViewById&lt;Button&gt;(R.id.btnSaveName)</span><br><span class="line">        <span class="keyword">val</span> btnIncrement = findViewById&lt;Button&gt;(R.id.btnIncrement)  </span><br><span class="line"></span><br><span class="line">        btnSaveName.setOnClickListener &#123;</span><br><span class="line">            <span class="keyword">val</span> name = etName.text.toString()</span><br><span class="line">            <span class="keyword">if</span> (name.isNotBlank()) &#123;</span><br><span class="line">                viewModel.saveName(name)</span><br><span class="line">                etName.text.clear()</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;  </span><br><span class="line"></span><br><span class="line">        btnIncrement.setOnClickListener &#123;</span><br><span class="line">            viewModel.incrementCount()</span><br><span class="line">        &#125;  </span><br><span class="line"></span><br><span class="line">        <span class="comment">//监听DataStore数据的变化，自动更新 UI</span></span><br><span class="line">        lifecycleScope.launch &#123;</span><br><span class="line">    <span class="comment">//节省资源罢了</span></span><br><span class="line">            repeatOnLifecycle(Lifecycle.State.STARTED) &#123;</span><br><span class="line">        <span class="comment">// 监听名字变化</span></span><br><span class="line">            launch &#123;</span><br><span class="line">            viewModel.userNameFlow.collect &#123; name -&gt;</span><br><span class="line">                tvGreeting.text = <span class="string">&quot;你好，<span class="variable">$name</span>！&quot;</span>  </span><br><span class="line">            &#125;  </span><br><span class="line">        &#125;        <span class="comment">//监听点击次数变化  </span></span><br><span class="line">            launch &#123;  </span><br><span class="line">                viewModel.clickCountFlow.collect &#123; count -&gt;  </span><br><span class="line">                tvCounter.text = <span class="string">&quot;点击次数: <span class="variable">$count</span>&quot;</span></span><br><span class="line">            &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="XML"><a href="#XML" class="headerlink" title="XML"></a>XML</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;utf-8&quot;</span>?&gt;</span>  </span><br><span class="line"><span class="tag">&lt;<span class="name">LinearLayout</span> <span class="attr">xmlns:android</span>=<span class="string">&quot;http://schemas.android.com/apk/res/android&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:layout_width</span>=<span class="string">&quot;match_parent&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:layout_height</span>=<span class="string">&quot;match_parent&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:orientation</span>=<span class="string">&quot;vertical&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:padding</span>=<span class="string">&quot;24dp&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">android:fitsSystemWindows</span>=<span class="string">&quot;true&quot;</span>)&gt;</span>  </span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">TextView</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:id</span>=<span class="string">&quot;@+id/tvGreeting&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:layout_width</span>=<span class="string">&quot;wrap_content&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:layout_height</span>=<span class="string">&quot;wrap_content&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:text</span>=<span class="string">&quot;你好，访客！&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:textSize</span>=<span class="string">&quot;24sp&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:textStyle</span>=<span class="string">&quot;bold&quot;</span> /&gt;</span>  </span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">TextView</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:id</span>=<span class="string">&quot;@+id/tvCounter&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:layout_width</span>=<span class="string">&quot;wrap_content&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:layout_height</span>=<span class="string">&quot;wrap_content&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:text</span>=<span class="string">&quot;点击次数: 0&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:textSize</span>=<span class="string">&quot;18sp&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:layout_marginTop</span>=<span class="string">&quot;8dp&quot;</span> /&gt;</span>  </span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">EditText</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:id</span>=<span class="string">&quot;@+id/etName&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:layout_width</span>=<span class="string">&quot;match_parent&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:layout_height</span>=<span class="string">&quot;wrap_content&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:hint</span>=<span class="string">&quot;输入你的名字&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:layout_marginTop</span>=<span class="string">&quot;32dp&quot;</span> /&gt;</span>  </span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">Button</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:id</span>=<span class="string">&quot;@+id/btnSaveName&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:layout_width</span>=<span class="string">&quot;match_parent&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:layout_height</span>=<span class="string">&quot;wrap_content&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:text</span>=<span class="string">&quot;保存名字&quot;</span> /&gt;</span>  </span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">Button</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:id</span>=<span class="string">&quot;@+id/btnIncrement&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:layout_width</span>=<span class="string">&quot;match_parent&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:layout_height</span>=<span class="string">&quot;wrap_content&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:text</span>=<span class="string">&quot;增加点击次数&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">android:layout_marginTop</span>=<span class="string">&quot;16dp&quot;</span> /&gt;</span>  </span><br><span class="line">  </span><br><span class="line"><span class="tag">&lt;/<span class="name">LinearLayout</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="附："><a href="#附：" class="headerlink" title="附："></a>附：</h2><h3 id="官方对比图"><a href="#官方对比图" class="headerlink" title="官方对比图"></a>官方对比图</h3><p><img src="https://ccn80b5bgw86.feishu.cn/space/api/box/stream/download/asynccode/?code=NjYwM2E5MTU2MzMxNzM1MTUzOTRhMDM4OTBiOThiZTBfajNYbjBBYkFTU2s2Y29SWGIxV1cwSEg4NVM3UE5UOVZfVG9rZW46RWxPMWJ0cGhlb0ZFQ3F4V2pzb2MxMnhmbmFjXzE3ODI4MTY5MTc6MTc4MjgyMDUxN19WNA&add_watermark=true&scene_type=CCM" alt="img"></p><h3 id="相关网址"><a href="#相关网址" class="headerlink" title="相关网址"></a>相关网址</h3><p><a href="https://developer.android.google.cn/topic/libraries/architecture/datastore?hl=zh-cn#multiprocess">官方介绍DataStore</a></p>]]>
    </content>
    <id>https://larkzhh.xyz/2026/06/30/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/Android/DataStore/</id>
    <link href="https://larkzhh.xyz/2026/06/30/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/Android/DataStore/"/>
    <published>2026-06-29T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p><code>SharedPreferences</code> 想必都知道吧，是所有安卓人学数据持久化的入门必修课，它可以通过简单的<strong>键值对</strong> (Key-Value)]]>
    </summary>
    <title>新的键值对存储方案 DataStore</title>
    <updated>2026-06-29T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>lark</name>
    </author>
    <category term="Android" scheme="https://larkzhh.xyz/categories/Android/"/>
    <category term="Android" scheme="https://larkzhh.xyz/tags/Android/"/>
    <category term="移动端" scheme="https://larkzhh.xyz/tags/%E7%A7%BB%E5%8A%A8%E7%AB%AF/"/>
    <content>
      <![CDATA[<p>在 APP 开发中很多效果往往都需要动画的配合来实现。 Android 提供了多种动画类型，如<strong>属性动画</strong>和<strong>视图动画</strong>，其中视图动画又包括帧动画和补间动画。</p><p>属性动画说实话没什么高大上的，它的原理也很简单粗暴，无非就是通过不断调用 <code>setXxx</code> 方法修改对象的 <strong>属性值</strong>（比如 <code>translationX</code> 、<code>alpha</code> 等等），并配合重绘，以达到动画的效果。</p><h1 id="ViewPropertyAnimator"><a href="#ViewPropertyAnimator" class="headerlink" title="ViewPropertyAnimator"></a><code>ViewPropertyAnimator</code></h1><p>想让一个 View 动起来，可以用 <code>ViewPropertyAnimator</code> 简单实现。通过 <code>View.animate()</code> 可以返回一个 <code>ViewPropertyAnimator</code> 对象，拿到它之后就可以 <em>为所欲为</em> 操作 View 了 。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> ViewPropertyAnimator <span class="title function_">animate</span><span class="params">()</span> &#123;  </span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;Stub!&quot;</span>);  </span><br><span class="line">&#125;</span><br><span class="line">view.animate().translationX(<span class="number">500</span>);<span class="comment">//让 view 水平移动500</span></span><br></pre></td></tr></table></figure><h2 id="基本用法"><a href="#基本用法" class="headerlink" title="基本用法"></a>基本用法</h2><p>因为属性动画是通过不断操作 View 的属性来实现效果的，所以 <code>ViewPropertyAnimator</code> 的许多方法和 View 的设置属性的相关方法基本相对应。</p><table><thead><tr><th><code>ViewPropertyAnimator</code> 方法</th><th>功能</th><th>View 中方法</th></tr></thead><tbody><tr><td>translationX() translationXBy()</td><td>设置 x 轴偏移</td><td>setTranslationX()</td></tr><tr><td>translationY() translationYBy()</td><td>设置 y 轴偏移</td><td>setTranslationY()</td></tr><tr><td>translationZ() translationZBy()</td><td>设置 z 轴偏移</td><td>setTranslationZ()</td></tr><tr><td>x() xBy()</td><td>修改view 的X坐标</td><td>setX()</td></tr><tr><td>……懒得写了，和上面大差不差</td><td>……</td><td>……</td></tr><tr><td>rotation() rotationBy()</td><td>旋转 View ，正值顺时针</td><td>setRotation()</td></tr><tr><td>rotationX() rotationXBy()</td><td>绕x轴旋转</td><td>setRotationX()</td></tr><tr><td>rotationY() rotationYBy()</td><td>绕Y轴旋转</td><td>setRotationY()</td></tr><tr><td>scaleX() scaleXBy()</td><td>设置横向放缩</td><td>setScaleX()</td></tr><tr><td>scaleY() scaleYBy()</td><td>设置纵向放缩</td><td>setScaleY()</td></tr><tr><td>alpha() alphaBy()</td><td>设置透明度</td><td>setAlpha()</td></tr></tbody></table><p>说明：</p><ol><li>这里方法参数大部分是 <code>Float</code> ，所以就放空了……</li><li>带 <code>By</code> 的话表示 <strong>相对变化</strong>（在当前状态基础上增加或减少），比如 <code>translationXBy(50)</code> 表示用动画把 <code>View</code> 的 <code>translationX</code> 值渐变地增加 <code>50</code>。</li><li>顺带提一嘴 Anroid 的 View 系统使用的是<strong>左手坐标系</strong>。后面如果有写一些关于自定义 View 方面的想法再细谈 View 吧。</li></ol><table><thead><tr><th>方法</th><th>功能</th></tr></thead><tbody><tr><td>setDuration()</td><td>设置动画时长</td></tr><tr><td>setInterpolator()</td><td>设置插值器</td></tr><tr><td>start()</td><td>开始动画</td></tr><tr><td>cancel()</td><td>取消动画</td></tr><tr><td>setStartDelay()</td><td>设置延迟开始时间</td></tr></tbody></table><h2 id="插值器"><a href="#插值器" class="headerlink" title="插值器"></a>插值器</h2><p>我们可以通过 <code>setInterpolator()</code> 来设置动画的<strong>速度模型</strong>。 如果想简单体验各种插值器对平移、旋转、缩放、透明度变化的效果，可以访问这个网站玩一玩，一般常用的插值器都有：</p><p> <strong><a href="https://inloop.github.io/interpolator">Android 动画插值器的在线可视化调试和预览工具</a></strong>。 对于没有指定插值器的动画，默认使用的是 <code>AccelerateDecelerateInterpolator</code> ，实现的效果和它名字差不多，就是先加速再减速。 如果想自定义速度模型的话 *超级大神啊&#x2F;(ㄒoㄒ)&#x2F;*~~ ，可以用 <code>PathInterpolator</code> ，传个<code>Path</code> 就行了。</p><h2 id="监听器"><a href="#监听器" class="headerlink" title="监听器"></a>监听器</h2><p>有时候我们想在动画执行的合适时机做点什么，可以使用监听器。</p><p>是啊，做点什么啊？</p><h3 id="1-setListener-Animator-AnimatorListener-listener"><a href="#1-setListener-Animator-AnimatorListener-listener" class="headerlink" title="1.setListener(Animator.AnimatorListener listener)"></a><code>1.setListener(Animator.AnimatorListener listener)</code></h3><p>这个方法需要你去实现一个 <code>Animator.AnimatorListener</code> 接口，但是 —— 这货 <em>特么</em> 要重写四个回调方法……</p><ul><li><code>onAnimationStart</code> 动画开始执行时调用</li><li><code>onAnimationEnd</code> 动画结束时调用</li><li><code>onAnimationCancel</code> 动画被通过 <code>cancel()</code> 取消时调用</li><li><code>onAnimationRepeat</code> 重复执行时调用</li></ul><p>（ps: <code>ViewPropertyAnimator</code> 不支持重复，所以这个方法对它无效）</p><p>当然如果只想监听结束回调啥的，不想写一堆空方法，用 <code>AnimatorListenerAdapter</code> 就行，它是一个适配器类，已经对所有方法提供了空实现，这样我们就可以按需重写方法啦。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">view.animate() </span><br><span class="line">    .translationX(<span class="number">500f</span>) </span><br><span class="line">    .setListener(<span class="keyword">object</span> : AnimatorListenerAdapter() &#123; </span><br><span class="line">        <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onAnimationEnd</span><span class="params">(animation: <span class="type">Animator</span>&#123; </span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="comment">// 动画结束后执行的逻辑，如跳转页面或执行下一个动画等等</span></span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="type">Log</span>.<span class="type">d</span>(&quot;<span class="type">Animation</span>&quot;, &quot;动画结束&quot;) </span></span></span><br><span class="line"><span class="params"><span class="function">    &#125; </span></span></span><br><span class="line"><span class="params"><span class="function">&#125;)</span></span></span><br></pre></td></tr></table></figure><h3 id="2-setUpdateListener-ValueAnimator-AnimatorUpdateListener-listener"><a href="#2-setUpdateListener-ValueAnimator-AnimatorUpdateListener-listener" class="headerlink" title="2.setUpdateListener(ValueAnimator.AnimatorUpdateListener listener)"></a><code>2.setUpdateListener(ValueAnimator.AnimatorUpdateListener listener)</code></h3><p>它要重写 <code>onAnimationUpdate(ValueAnimator animation)</code> 当动画的属性更新时这个方法被调用。 此外我们可以通过 <code>animation.getAnimatedValue()</code> 拿到当前动画计算出的属性值。</p><h3 id="3-withStartAction-Runnable-runnable"><a href="#3-withStartAction-Runnable-runnable" class="headerlink" title="3.withStartAction(Runnable runnable)"></a><code>3.withStartAction(Runnable runnable)</code></h3><p>设置一个在动画开始时执行的 <code>Runnable</code></p><h3 id="4-withEndAction-Runnable-runnable"><a href="#4-withEndAction-Runnable-runnable" class="headerlink" title="4.withEndAction(Runnable runnable)"></a><code>4.withEndAction(Runnable runnable)</code></h3><p>设置一个在动画结束时执行的 <code>Runnable</code>，它在动画<strong>正常结束</strong>时才会被调用，而在动画被<code>cancel()</code> 的话是不会被执行的。</p><p>这俩是<strong>一次性</strong>的，动画执行用完就扔了，之后再重用 <code>ViewPropertyAnimator</code> 来做别的动画，用它们设置的回调也不会再被调用。而 <code>setListener()</code> 所设置的 <code>AnimatorListener</code> 是<strong>持续有效</strong>的，当动画重复执行时，回调总会被调用。</p><h1 id="ObjectAnimator"><a href="#ObjectAnimator" class="headerlink" title="ObjectAnimator"></a><code>ObjectAnimator</code></h1><p>最后提一嘴 <code>ObjectAnimator</code> ，它的动画功能更强大，可以实现更多复杂的动画。而且它<strong>不依赖</strong> <strong><code>View</code></strong> ，可以作用于任何对象。 当然，这个对象必须拥有对应属性的 <code>setter</code> 方法（如果有<strong>初始值</strong>还需要 <code>getter</code> 方法）。 码字结束，有空再写吧~</p>]]>
    </content>
    <id>https://larkzhh.xyz/2026/06/30/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/Android/%E5%B1%9E%E6%80%A7%E5%8A%A8%E7%94%BB/</id>
    <link href="https://larkzhh.xyz/2026/06/30/%E7%A7%BB%E5%8A%A8%E5%BC%80%E5%8F%91/Android/%E5%B1%9E%E6%80%A7%E5%8A%A8%E7%94%BB/"/>
    <published>2026-06-29T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>在 APP 开发中很多效果往往都需要动画的配合来实现。 Android]]>
    </summary>
    <title>属性动画 让 APP 动起来</title>
    <updated>2026-06-29T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>lark</name>
    </author>
    <category term="MISC" scheme="https://larkzhh.xyz/categories/MISC/"/>
    <category term="公告" scheme="https://larkzhh.xyz/tags/%E5%85%AC%E5%91%8A/"/>
    <content>
      <![CDATA[<h2 id="博客已迁移"><a href="#博客已迁移" class="headerlink" title="博客已迁移"></a>博客已迁移</h2><p>旧博客的所有内容已迁移至本站，原站点将不再更新，后续所有东西我会慢慢搬过来。</p><h2 id="为什么迁移"><a href="#为什么迁移" class="headerlink" title="为什么迁移"></a>为什么迁移</h2><p>因为之前的 GitHub 号被封了影响到了托管的站点，在我有一天心血来潮想传点东西的时候发现的……也不必去考古，~~ 因为其实我也就传了一篇帖子在网上，其他基本在本地 ~~ 虽然后面与伟大且高效的 GitHub 官方进行了一番交谈解除了限制（原因竟是有个仓库名写了 f**k ？？？），但事已至此，新号也创建了，网站也备案了……就让他成为过去吧。ಥ_ಥ</p><h2 id="迁移说明"><a href="#迁移说明" class="headerlink" title="迁移说明"></a>迁移说明</h2><ul><li><strong>文章</strong>：会慢慢上传的，也许吧，因为我个人喜欢把东西存本地。</li><li><strong>评论区</strong>：本站暂不开启评论功能。</li><li><strong>主题</strong>: 主题从 <strong>Butterfly</strong> 换成了 <strong>Anzhiyu</strong>，但我感觉花里胡哨的，自由度也不是很大，后面看情况再考虑换不换吧。此外我不建议你打开白天模式，因为会出现一些 UI 配色问题。</li></ul><p>感谢你一直以来的关注</p>]]>
    </content>
    <id>https://larkzhh.xyz/2026/06/29/%E5%8D%9A%E5%AE%A2/%E5%8D%9A%E5%AE%A2%E8%BF%81%E7%A7%BB%E5%85%AC%E5%91%8A/</id>
    <link href="https://larkzhh.xyz/2026/06/29/%E5%8D%9A%E5%AE%A2/%E5%8D%9A%E5%AE%A2%E8%BF%81%E7%A7%BB%E5%85%AC%E5%91%8A/"/>
    <published>2026-06-28T16:00:00.000Z</published>
    <summary>旧博客已迁移至此。</summary>
    <title>公告</title>
    <updated>2026-06-30T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>lark</name>
    </author>
    <category term="MISC" scheme="https://larkzhh.xyz/categories/MISC/"/>
    <category term="Hexo" scheme="https://larkzhh.xyz/tags/Hexo/"/>
    <category term="博客" scheme="https://larkzhh.xyz/tags/%E5%8D%9A%E5%AE%A2/"/>
    <content>
      <![CDATA[<p>Welcome to <a href="https://hexo.io/">Hexo</a>! This is your very first post. Check <a href="https://hexo.io/docs/">documentation</a> for more info. If you get any problems when using Hexo, you can find the answer in <a href="https://hexo.io/docs/troubleshooting.html">troubleshooting</a> or you can ask me on <a href="https://github.com/hexojs/hexo/issues">GitHub</a>.</p><h2 id="Quick-Start"><a href="#Quick-Start" class="headerlink" title="Quick Start"></a>Quick Start</h2><h3 id="Create-a-new-post"><a href="#Create-a-new-post" class="headerlink" title="Create a new post"></a>Create a new post</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo new <span class="string">&quot;My New Post&quot;</span></span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/writing.html">Writing</a></p><h3 id="Run-server"><a href="#Run-server" class="headerlink" title="Run server"></a>Run server</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo server</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/server.html">Server</a></p><h3 id="Generate-static-files"><a href="#Generate-static-files" class="headerlink" title="Generate static files"></a>Generate static files</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo generate</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/generating.html">Generating</a></p><h3 id="Deploy-to-remote-sites"><a href="#Deploy-to-remote-sites" class="headerlink" title="Deploy to remote sites"></a>Deploy to remote sites</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo deploy</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/one-command-deployment.html">Deployment</a></p>]]>
    </content>
    <id>https://larkzhh.xyz/2026/06/28/%E5%8D%9A%E5%AE%A2/hello-world/</id>
    <link href="https://larkzhh.xyz/2026/06/28/%E5%8D%9A%E5%AE%A2/hello-world/"/>
    <published>2026-06-27T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>Welcome to <a href="https://hexo.io/">Hexo</a>! This is your very first post. Check <a href="https://hexo.io/docs/">documentation</a>]]>
    </summary>
    <title>Hello World</title>
    <updated>2026-09-09T05:02:36.033Z</updated>
  </entry>
</feed>
