<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>KMnO4y_Fish&apos;s Blog</title><description>undefined</description><link>https://ak-ioi.com</link><item><title>好久不见，世界！</title><link>https://ak-ioi.com/blog/announcement/hello-world</link><guid isPermaLink="true">https://ak-ioi.com/blog/announcement/hello-world</guid><description>如果你看到这篇文章，说明我的博客已经成功迁移并发布了。</description><pubDate>Wed, 30 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import F from &apos;@/components/mdx/formatting&apos;&lt;/p&gt;
&lt;p&gt;你好！欢迎来到我的新博客。不出意外的话，之前的大部分文章已经按计划迁移到了这里。&lt;/p&gt;
&lt;h2&gt;来历&lt;/h2&gt;
&lt;p&gt;六年来，我的博客系统一直是基于 Wordpress，因为我看重其易学和便捷的特性——不需要 Git，不需要构建流程，开箱即用，编辑可以直接通过网页面板完成。但是，随着我技术能力的提升，我开始希望添加调整或添加一些排版元素，而在 Wordpress 中，即使正确地加载了自定义插件，调整或添加的过程也难以在一个统一的地方完成——总是要在各种文件（PHP、HTML、CSS）间来回进行修改。此外，Wordpress 还逐渐显现出这些弊端：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Markdown 支持很将就&lt;/strong&gt;：我愈发欣赏 Markdown 等纯文本格式的便利性和统一性，而 Wordpress 就连提供完善的 Markdown 支持都比较困难，网页面板上的编辑体验也难以匹敌本地的代码编辑器；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中心媒体库&lt;/strong&gt;：图片、视频等媒体存储在中心媒体库中，而非与文章放在一起，难以追踪媒体被哪篇文章引用。尽管有插件可以提供这一项功能，但在 Markdown 和传统格式文章共存时，事情就变得更加复杂。“内存泄漏”（不使用的媒体没有及时删除）和“野指针”（清理时误删了正在使用的媒体）事故时有发生；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交互式内容支持较差&lt;/strong&gt;：随着我的前端开发能力提升，我更希望尝试打破单页、静态、图文的写作方式，添加流程化的、灵活的甚至交互式的阅读体验，但这些在 Wordpress 上更是难以优雅地实现；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设计风格混乱&lt;/strong&gt;：插件并非仅仅提供程序功能，还会提供管理界面。这原本正是 Wordpress 的便捷性所在，但设计风格五花八门的插件往往将管理面板弄得混乱不堪；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加载缓慢&lt;/strong&gt;：Wordpress 系统每次访问都要查询数据库生成页面，网页加载慢是难以避免的问题。由于用户登录、评论等机制深度耦合在页面生成过程中，静态缓存也只是治标不治本的手段；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖外部资源&lt;/strong&gt;：各种主题、插件会不可控地引用各种良莠不齐外部资源，难以完全防止引用 &lt;code&gt;googleapis&lt;/code&gt; 的情况（由于众所周知的原因这会导致页面加载迟迟不能完成），并且任何被引用的外部服务爆炸都会导致功能异常（Gravatar 镜像源和 Editor.md 前端资源，说你们呢）。网站无法在没有人工介入的情况下保证长期可靠。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正是这些弊端让我不是很愿意在博客上写文章。我确实写了一些东西（例如学习笔记和信息安全大赛题解），但都不在这个博客上，这里已经好久没有更新了。曾经因为“便捷”被我选中的 Wordpress，早已成了我的负担。&lt;/p&gt;
&lt;p&gt;以往，我对每次写一点东西都要打开代码编辑器大干一通的静态博客非常不屑，但如今，我发现我的大部分写作工作事实上都是在个人电脑上完成，仅有极少勘误和简单的文章会在手机上写作。在搜集相关信息后，我知道了即使静态博客也可以通过自动构建和 CMS 的方式进行网页端编辑，这完全可以满足我的勘误和简单写作需求。再不济，还可以用 GitHub 网页端修改代码然后直接提交嘛。或许，是时候务实一些，搭建一个自动构建的静态博客了。&lt;/p&gt;
&lt;h2&gt;技术框架概览&lt;/h2&gt;
&lt;p&gt;之前我的博客都是在自己控制的云服务器上搭建，因此无论在资源使用、功能还是数据管理权上，都有很大的灵活性和自主性，PHP 应用的开箱即用特性更是让我能随时在网站上添加一些奇思妙想。即使改用了静态博客，我仍然希望有这样的灵活性和自主性，因此更偏好在自己的云服务器上部署网站，而非使用一些便捷且免费的服务。总而言之，现在的博客技术框架包含这些部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;代码托管&lt;/strong&gt;：网站代码，包括所有文章内容，托管于 GitHub。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态站点生成器&lt;/strong&gt;：这个框架要适合写博客，加载速度要快，还要有较多的主题/模板可供选择，还要能够与 &lt;code&gt;npm&lt;/code&gt; 以及各种前端交互式框架原生集成。所以首先排除 Jekyll、Hugo 等不方便集成交互元素的，再排除不适合写博客（主要用来写技术文档）或者第三方主题很少的 Docusaurus、VuePress 和 VitePress 这些，留下的 &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt; 正是符合这些要求的最佳选择。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网站模板&lt;/strong&gt;：我现在的审美已经看厌了大红大紫，所以想要一个无主题色（但可以有强调色）或者主题色很淡的模板。我也不喜欢在文章页面上放一堆与文章无关的侧边栏组件。&lt;a href=&quot;https://astro-pure.js.org/&quot;&gt;Pure&lt;/a&gt; 是淡主题色风格，甚至每篇文章可以自己有主题色，并且侧边栏放的是文章目录，该有的功能也都有。完美！&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态站点构建与托管&lt;/strong&gt;：静态站点构建环境的维护成本较高，因此我选择用 GitHub Actions 在每次主分支发生 push 时构建。免费用户每月有 2000 分钟的构建时长，不太能用完。构建完成后，由 GitHub Actions 将文件通过 &lt;code&gt;rsync&lt;/code&gt; 传输至我的云服务器，之后由 Apache 服务端负责提供页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CMS (暂缓)&lt;/strong&gt;：计划使用 &lt;a href=&quot;https://decapcms.org/&quot;&gt;Decap CMS&lt;/a&gt; 提供 Web 编辑界面。CMS 所做的更改最后仍会以提交的形式反映到 GitHub 仓库中。由于使用 GitHub 网页端就能满足基本勘误需求，暂不折腾。Decap CMS 是几乎纯前端的，只需要与一个运行在后端的通用 GitHub OAuth 服务集成就行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评论收集系统&lt;/strong&gt;：由于 Pure 模板提供 Waline 集成，直接使用 &lt;a href=&quot;https://waline.js.org/&quot;&gt;Waline&lt;/a&gt;。本站的 Waline 实例也部署在自己的云服务器上，使用 MySQL 数据库。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;之后可能（我是说可能）会专门写文章详细解释技术框架和迁移流程。&lt;/p&gt;
&lt;h2&gt;原博客迁移公告&lt;/h2&gt;
&lt;p&gt;~~似曾相识？~~&lt;/p&gt;
&lt;p&gt;随着 Web 技术的更新，本站使用的 Wordpress 框架的弊端日益凸显——容易故障、文章撰写低效、无法轻易克隆、难以添加交互元素、维护成本高是其中显而易见的几个。&lt;/p&gt;
&lt;p&gt;经过仔细的考虑，我们决定将此博客迁移至静态博客系统，新的系统将取代当前的 Wordpress 博客，域名保持不变，当前博客系统&lt;strong&gt;将同时关停&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;迁移的明细见下方。&lt;/p&gt;
&lt;h3&gt;附属应用&lt;/h3&gt;
&lt;p&gt;当前 &lt;code&gt;ak-ioi.com&lt;/code&gt; 下的所有附属应用&lt;strong&gt;已经&lt;/strong&gt;全部迁移并重定向到 &lt;code&gt;apps.ak-ioi.com&lt;/code&gt;。接下来的迁移中它们不会受到影响。&lt;/p&gt;
&lt;h3&gt;文章&lt;/h3&gt;
&lt;p&gt;文章的迁移具体视以下情况而定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;❎ &amp;#x3C;F.Orange&gt;(暂缓)&amp;#x3C;/F.Orange&gt; 部分较新的文章会重写，以提供更好的阅读体验和更新的信息。&lt;br&gt;
现在已经将这些文章标记为“处于待复核状态”。&lt;/li&gt;
&lt;li&gt;✅ 2018 年 6 月的文章将被&lt;strong&gt;丢弃&lt;/strong&gt;（它们来自更早的博客系统且现在已经没有实际价值）。&lt;/li&gt;
&lt;li&gt;✅ 网页工具类的文章&lt;strong&gt;将被删除&lt;/strong&gt;，部分工具后续会以纯前端的形式重新实现并添加。&lt;/li&gt;
&lt;li&gt;✅ 未在&lt;a href=&quot;/archive/&quot;&gt;存档页&lt;/a&gt;中列出，但是公开的文章，将被原样迁移至新的博客系统并且&lt;strong&gt;改为完全公开状态，不再隐藏&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;✅ 隐藏且密码保护的文章将被放入&lt;strong&gt;不公开的存档&lt;/strong&gt;中，不进入新的博客系统。&lt;br&gt;
如果你将来需要这些文章中的内容，建议先保存其快照。如果迁移后仍需要访问，请在 2025 年 6 月 27 日前用任意邮箱发信至 &lt;code&gt;webmaster&lt;/code&gt; at &lt;code&gt;ak-ioi.com&lt;/code&gt;，附上文章密码的明文或者 SHA-256 哈希值，我们将手动提供被存档文章的副本。&lt;/li&gt;
&lt;li&gt;✅ 完全不公开但目前有协作编辑的文章将被放入&lt;strong&gt;不公开的存档&lt;/strong&gt;中，不进入新的博客系统。&lt;br&gt;
如果你目前是协作编辑者，请在 2025 年 6 月 27 日前使用协作账号的注册邮箱发信至 &lt;code&gt;webmaster&lt;/code&gt; at &lt;code&gt;ak-ioi.com&lt;/code&gt;，我们将手动提供被存档文章的副本或访问权。&lt;/li&gt;
&lt;li&gt;✅ 完全不公开且无协作编辑的文章，以及草稿文章，将被&lt;strong&gt;丢弃&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;✅ 除了上述文章之外，所有其他文章将原样迁移至新的博客系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所有被迁移至新系统的文章，其永久链接都会改变。原先的链接处将添加软重定向页面。&lt;/p&gt;
&lt;h3&gt;文章元数据&lt;/h3&gt;
&lt;p&gt;所有原有的分类目录和标签索引页将&lt;strong&gt;不会保留&lt;/strong&gt;。新的系统将重新设计文章分类和索引页面。&lt;/p&gt;
&lt;p&gt;指向原来分类目录/标签索引页的永久链接将完全失效（404），不会有重定向页面。&lt;/p&gt;
&lt;h3&gt;账户与评论&lt;/h3&gt;
&lt;p&gt;所有当前系统上的已注册用户将&lt;strong&gt;不会保留&lt;/strong&gt;。如果你是已注册用户，且需要协助迁出数据，请在 2025 年 6 月 27 日前使用你的注册邮箱发信至 &lt;code&gt;webmaster&lt;/code&gt; at &lt;code&gt;ak-ioi.com&lt;/code&gt;，我们将手动处理。&lt;/p&gt;
&lt;p&gt;所有评论&lt;strong&gt;都不会以评论的形式转移到新的博客&lt;/strong&gt;。具体视以下情况处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 留言板页面的评论将&lt;strong&gt;清空&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;✅ 在迁移中被丢弃的文章，其评论一并&lt;strong&gt;丢弃&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;✅ (没有需要补充的内容) 提供了有用信息的博客和回复将&lt;strong&gt;补充到正文内容中&lt;/strong&gt;，信息贡献者将被提名（只提及评论时留下的昵称，因为邮箱是不应该公开的敏感个人信息）。&lt;/li&gt;
&lt;li&gt;✅ 所有带有提问性质且未得到回答的评论，将在迁移前后的一段时间内统一&lt;strong&gt;通过评论时留下的邮箱&lt;/strong&gt;私信答复，此后评论将被&lt;strong&gt;丢弃&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;✅ 其余评论将被&lt;strong&gt;丢弃&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;新的系统仍会有评论功能，且所有可评论的文章评论区仍会无限期开放。&lt;/p&gt;</content:encoded><h:img src="/_astro/hero.xQ46PfmX.png"/><enclosure url="/_astro/hero.xQ46PfmX.png"/></item><item><title>[Android 模拟器] Android Studio AVD 在 Windows 上出现爆音（已解决）</title><link>https://ak-ioi.com/blog/small-problem/avd-blasting-sounds</link><guid isPermaLink="true">https://ak-ioi.com/blog/small-problem/avd-blasting-sounds</guid><description>这是一个小问题 | 在 Windows 下，Android Studio 官方的模拟器播放声音会出现连续的滴答声或爆破声，持续十几秒后才会逐渐消失。</description><pubDate>Sat, 13 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import F from &apos;@/components/mdx/formatting&apos;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;这是一个小问题。&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;问题描述&lt;/h2&gt;
&lt;p&gt;在 Windows 下，Android Studio 官方的模拟器播放声音会出现连续的滴答声或爆破声，持续十几秒后才会逐渐消失。&lt;/p&gt;
&lt;p&gt;若一段时间不播放声音，稍后再播放还会出现一样的情况。&lt;/p&gt;
&lt;h2&gt;技术赏析&lt;/h2&gt;
&lt;p&gt;这里并不能作出很详细的赏析。&lt;/p&gt;
&lt;p&gt;主要原因是模拟器核心 qemu 为保证音频播放时间的准确性，会每过一定时间根据系统时间调节音频进度。&lt;/p&gt;
&lt;p&gt;这一特性可以通过环境变量关闭。&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;这是一个几乎正确的解决方案。&lt;/p&gt;
&lt;h3&gt;解决方法&lt;/h3&gt;
&lt;p&gt;在运行模拟器的环境下设置环境变量，如下：&lt;/p&gt;
&lt;p&gt;|Var|Val|
|-|-|
|QEMU_DSOUND_LATENCY_MILLIS|3|
|QEMU_AUDIO_TIMER_PERIOD|0|&lt;/p&gt;
&lt;p&gt;你可以将其添加至系统环境变量，也可以在运行模拟器前预执行下面的命令（适用于命令行启动模拟器）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;set QEMU_DSOUND_LATENCY_MILLIS=3 &amp;#x26; set QEMU_AUDIO_TIMER_PERIOD=0

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;[广告]&lt;/strong&gt; 你可以使用 &lt;a href=&quot;https://github.com/yezhiyi9670/avd-launcher&quot;&gt;AVD Launcher&lt;/a&gt; 启动模拟器，它允许你添加自定义环境变量和参数。&lt;/p&gt;
&lt;h3&gt;缺陷&lt;/h3&gt;
&lt;p&gt;暂未验证，但是理论上这可能导致音频播放时间出现微小的误差。&lt;/p&gt;
&lt;p&gt;这样的误差正常情况下难以察觉。当使用场景对时间精度要求极高时（例如音乐游戏），误差&lt;strong&gt;可能&lt;/strong&gt;会带来影响。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[玩机] 一加（OnePlus）手机刷机 Root</title><link>https://ak-ioi.com/blog/play-android/oneplus-flash-root</link><guid isPermaLink="true">https://ak-ioi.com/blog/play-android/oneplus-flash-root</guid><description>OnePlus（一加）设备刷机获取 Root 权限的通法，使用 Magisk 方案。</description><pubDate>Mon, 11 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import F from &apos;@/components/mdx/formatting&apos;&lt;/p&gt;
&lt;p&gt;这篇文章是 &lt;a href=&quot;/blog/play-android/mtk-flash-root&quot;&gt;[玩机] MTK Android 刷机 Root，以 Aigo M2 Pro 为例&lt;/a&gt; 的改写版本，用于说明针对 OnePlus 设备的情况（高通和 MTK 芯片均可）。&lt;/p&gt;
&lt;p&gt;众所周知，OnePlus 是知名的“极客”手机品牌。其 Bootloader 解锁可直接在设置中开启，无须进行注册、申请等手续，折腾起来非常方便。&lt;/p&gt;
&lt;p&gt;本文介绍的是 OnePlus 设备通过刷机获取 Root 权限的通法，使用 Magisk 方案。本教程仅介绍了最基本的部分，没有推荐任何应用和模块，因此仅适用于知道自己 Root 后要做什么的用户。&lt;/p&gt;
&lt;p&gt;操作以 OnePlus 9R 设备为例，所有操作在 Windows 系统下完成。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;时效性复核状态&quot;&gt;
本文的写作时间是 2022 年第三季度，复核或修订发生于 2026-01-10。&lt;br&gt;
请留意文章的时效性。&lt;/p&gt;
&lt;p&gt;2025-09-04 说明 | 从出厂搭载 Android 16 的设备开始，&lt;a href=&quot;https://bbs.oneplus.com/thread/1926504022886318086&quot;&gt;一加将对 Bootloader 解锁施加限制&lt;/a&gt;。尽管目前似乎只是「走申请流程」，但在公告中，该限制的表述相当含糊，表明一加之后完全可能将其变得更加严格，不再支持你自由刷机的权利。请考虑改为购买对解锁和刷机仍有良好支持的设备，可参考 &lt;a href=&quot;https://github.com/melontini/bootloader-unlock-wall-of-shame&quot;&gt;Bootloader 解锁耻辱柱&lt;/a&gt;和 &lt;a href=&quot;https://a.zli.li/&quot;&gt;BL List&lt;/a&gt; 以进行决策。&lt;/p&gt;
&lt;p&gt;2026-01-10 说明 | 从本文写作到现在，Android 设备的分区结构已经发生一些变化，Root 隐藏话题已经变得很复杂，诸如 KernelSU、APatch 等更强大的 Root 方案也出现了。此外，本文未提及一些可能比较重要的保险措施，例如 Root 后立即进行全分区备份。&lt;strong&gt;如果你的设备比较新，或者想尝试一些较新的方案和工具，不建议再参考此文章。&lt;/strong&gt;
&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Tip title=&quot;提示&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在经常有“设备获取 Root 权限不安全”的说法，这种说法过于绝对。&lt;/p&gt;
&lt;p&gt;传统方案 Root 的不安全主要来源于第三方 Recovery 系统以及当时有效数据加密措施的缺失，这允许任何人不经验证直接备份数据和修改系统文件。目前的 Root 方案较为完善，用户获取 Root 权限并不一定不安全，善用 Root 还能保护隐私。但是仍要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于使用不谨慎或喜欢冒险的用户，确实会大大增加隐私泄露、数据丢失、设备损坏的风险；&lt;/li&gt;
&lt;li&gt;若设备遭已获取 Root 权限的恶意软件攻击，则后果也会比一般的恶意软件攻击严重得多；&lt;/li&gt;
&lt;li&gt;Bootloader 已解锁（这是 Root 的先决条件）的设备必须谨慎保管，避免丢失或被他人恶意动手脚。（你可以将 Bootloader 已解锁且加密功能未被人为禁用的设备视为一个相当安全的保险箱，攻击者即使拿到这个保险箱也无法轻易打开，但却可以在保险箱上装一个隐藏摄像头，偷窥你下一次输入的密码甚至你打开保险箱后取出文件的过程）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Magisk 方案是目前的主流方案之一，该方案&lt;strong&gt;不需要&lt;/strong&gt;使用第三方 Recovery 系统，因而设备丢失后，对设备安全造成的影响也较小。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Tip&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;部分应用（主要为安全性要求高的银行应用、政府公共服务应用以及反作弊要求高的游戏应用，但不含主流大厂的实用型应用）会&lt;strong&gt;检测&lt;/strong&gt;设备上是否已获取 Root。若检测到，会采取警告、禁用敏感功能、要求登录验证码、拒绝加载等措施来规避可能的风险，&lt;strong&gt;游戏可能会不加警告而直接封号&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对此，你需要&lt;strong&gt;采取 Root 隐藏措施&lt;/strong&gt;。即便如此，你还需要手动选择隐藏 Root 的对象（如果隐藏方案支持白名单模式则不需要）。因此在安装上述类型软件后，&lt;strong&gt;请务必记住要立即对其开启 Root 隐藏&lt;/strong&gt;，以免造成不必要的麻烦。&lt;/p&gt;
&lt;p&gt;本文会介绍几种简单易用的 Root 隐藏措施。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Danger title=&quot;危险&quot;&gt;&lt;/p&gt;
&lt;p&gt;本文仅供参考。若学习操作，导致设备出现系统损坏、无法开机或正常使用以及数据丢失等问题，机主应当自行承担责任。最坏情况下，可能需要通过“深度刷机”方式或售后刷机修复（然而实际上很难搞到这个地步）。&lt;/p&gt;
&lt;p&gt;为防止操作出现疏漏，请至少完整阅读一遍后再开始操作。建议使用电脑等大屏设备阅读。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Danger&gt;&lt;/p&gt;
&lt;h2&gt;刷机，Root，Magisk，为什么？&lt;/h2&gt;
&lt;h3&gt;Root 意味着什么？&lt;/h3&gt;
&lt;p&gt;Root 意味着&lt;strong&gt;自由&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Root，指的是 root 用户。该用户对设备有&lt;strong&gt;最高的权限&lt;/strong&gt;。获取 Root 权限，即修改系统，使得用户和应用能够以 root 用户的身份执行操作，也就是说，用户获得对设备的最高访问权，真正实现了“&lt;strong&gt;我的设备我做主&lt;/strong&gt;”。&lt;/p&gt;
&lt;p&gt;Root 权限的使用方式主要分为两类。一种是获取系统特权，常见应用如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;利用文件管理器或破解工具自由备份、还原应用的数据和游戏进度。&lt;/li&gt;
&lt;li&gt;利用系统权限禁用/隐藏应用，或禁止应用后台运行。&lt;/li&gt;
&lt;li&gt;禁用，或在当前用户空间下卸载系统应用。&lt;/li&gt;
&lt;li&gt;自由操纵应用的权限，例如禁止自启动。&lt;/li&gt;
&lt;li&gt;使用内存修改器对游戏数据进行破解。&lt;/li&gt;
&lt;li&gt;修改受保护的系统设置，例如无障碍服务。&lt;/li&gt;
&lt;li&gt;修改系统内存，实现更改设备 MAC 地址保护隐私/绕过身份验证等。&lt;/li&gt;
&lt;li&gt;修改隐藏的设置项，例如使状态栏上的时间精确到秒（这个一般不需要 Root 也能做到）。&lt;/li&gt;
&lt;li&gt;执行自动化操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另一种，为&lt;strong&gt;修改&lt;/strong&gt;系统。常见应用如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定制系统的开机动画。&lt;/li&gt;
&lt;li&gt;彻底清除、替换系统应用。&lt;/li&gt;
&lt;li&gt;将应用安装为系统应用。&lt;/li&gt;
&lt;li&gt;修改内核选项。&lt;/li&gt;
&lt;li&gt;修改系统构建信息，以实现修改设备的制造商、机型等信息。&lt;/li&gt;
&lt;li&gt;对系统核心进行破解，以移除应用覆盖安装时的版本和签名检测（对破解游戏比较有用）。&lt;/li&gt;
&lt;li&gt;调校充电速度等行为。&lt;/li&gt;
&lt;li&gt;对硬件超频以压榨性能。&lt;/li&gt;
&lt;li&gt;注入系统进程，以便调整第三方应用的界面、行为。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同样，Root 意味着&lt;strong&gt;风险&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若对恶意软件授予 Root 权限，其将具有非常大的破坏力。&lt;/li&gt;
&lt;li&gt;Root 权限使用时不谨慎，或因为各类天灾人祸，会导致系统损坏与数据丢失。&lt;/li&gt;
&lt;li&gt;系统更新变得麻烦。若原厂镜像未备份或系统被修改，设备将无法直接接受增量更新，&lt;strong&gt;需要刷机才能升级系统&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;我需不需要 Root？&lt;/h3&gt;
&lt;p&gt;是否需要 Root 取决于你需要进行的操作类型。&lt;/p&gt;
&lt;p&gt;如果你的需求在 &lt;code&gt;adb shell&lt;/code&gt; 下，或使用 &lt;code&gt;adb&lt;/code&gt; 授权就能够完成（&lt;strong&gt;系统特权&lt;/strong&gt;型中的大多数均属于此类），则很可能不需要 Root。如今，更加流行一些免 Root 获得系统特权的权宜之计（例如 &lt;a href=&quot;https://shizuku.rikka.app/zh-hans/&quot;&gt;Shizuku API&lt;/a&gt;）。相比于 Root，这种方式的风险小得多，更值得提倡。&lt;/p&gt;
&lt;p&gt;如果你需要&lt;strong&gt;修改系统&lt;/strong&gt;，或从事应用/游戏破解、Android 核心破解等工作，则你需要 Root。&lt;/p&gt;
&lt;h3&gt;为什么是 Magisk？&lt;/h3&gt;
&lt;p&gt;传统的 Root 权限方案是 SuperSU，该方案主要通过修改系统&lt;a href=&quot;/blog/play-android/preintro-bootmode#general-partitions&quot;&gt;分区&lt;/a&gt; &lt;code&gt;/system&lt;/code&gt; 来安装 Root 权限。&lt;/p&gt;
&lt;p&gt;Magisk 是一个较新的方案，主要优势如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;#️⃣ 轻量化修改。相比于 SuperSU，其只修改比系统分区小得多 Boot 分区。因此，刷机过程要快很多，移除 Root 权限并还原原厂镜像容易得多。&lt;/li&gt;
&lt;li&gt;🚫 无系统（Systemless）方式。Magisk 不修改系统分区，使得设备接受后续系统更新要容易很多。&lt;/li&gt;
&lt;li&gt;🧩 模块（Modules）。Magisk 提供许多使用无系统方式修改系统的模块。&lt;/li&gt;
&lt;li&gt;💧 排除（DenyList）。用户可以将既不需要 Root 权限又不需要通过注入代码的方式（LSPosed 等）修改的应用加入&lt;strong&gt;排除列表&lt;/strong&gt;，避免 Magisk 对其进行更改。一般而言，这些软件并不能感受到 Magisk 的存在，这使得&lt;strong&gt;对 Root 权限敏感的游戏、银行等软件仍然能正常使用&lt;/strong&gt;。
一些第三方模块可以在允许 LSPosed 注入的情况下仍然提供 Magisk 隐藏功能。&lt;/li&gt;
&lt;li&gt;⚡ 一次性刷机方案。Magisk 的安装只需要一次刷机就能完成，不使用第三方 Recovery 系统。这使得第三方 Recovery 不支持某一设备的问题得到解决，且对系统的安全性影响比较小。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于较新的设备，Magisk 方案还具有风险极小的特点，具体如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 无设备支持问题。Magisk 具有对 Android 系统的通用支持，而非对某特定机型的支持，无须寻找适合你的机型的版本，更不用担心找错版本。&lt;/li&gt;
&lt;li&gt;🔧 仅对 Boot 分区进行修改。这意味着刷机修改能在数秒内完成，且 Fastboot 模式下，&lt;strong&gt;你始终可以在刷入 Boot 镜像前先进行“试用”，确保刷入后系统不会爆掉&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;🔒 不使用第三方 Recovery。避免覆盖原厂 Recovery，使得&lt;strong&gt;遇到严重问题 Fastboot 无法正常刷机的情况下，也能通过 Recovery 刷原厂镜像还原&lt;/strong&gt;。也使得 Root 对系统安全性的影响降到最低。&lt;/li&gt;
&lt;li&gt;🗑️ 一键卸载。系统更新前，可以一键将被修改的 Boot 镜像恢复原厂，以正常接受增量系统更新（注：OnePlus 设备检测到 Magisk 后，会自动下载全量更新包，故不需要卸载 Magisk 即可进行系统更新）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在也出现了一些更加强大、更加抗检测的 Root 方案，例如 KernelSU 和 APatch，但是它们的安装相比 Magisk 略微复杂，不推荐新手直接尝试（其中 APatch 的基本操作步骤一致，只是多了设置超级密钥的环节）。&lt;/p&gt;
&lt;h3&gt;为什么刷机？&lt;/h3&gt;
&lt;p&gt;如果早些时候（2014 年左右）你接触过 Root 权限的获取，你可能会知道许多“&lt;strong&gt;一键 Root&lt;/strong&gt;”等免刷机的 Root 权限获取软件，有的系统甚至直接允许用户打开 Root 权限。为什么如今获取 Root 如此麻烦？为什么刷机风险这么大，却又说刷机其实是最明智的选择？&lt;/p&gt;
&lt;p&gt;原因之一，是如今“一键 Root”等工具已经不好用了。&lt;/p&gt;
&lt;p&gt;这类软件往往使用&lt;strong&gt;系统漏洞&lt;/strong&gt;进行破解。事实上，这是一个很大的安全性问题，因为不只是一键 Root 工具可以使用漏洞，其他的任何软件都可以，而一旦恶意软件获取 Root 权限，Android 权限管理对其就形同虚设。如今，随着安全漏洞的修复，此类工具自然也就越来越难用了。&lt;/p&gt;
&lt;p&gt;然而，刷机却不利用奇技淫巧，具有&lt;strong&gt;理论可行性&lt;/strong&gt;，因而成为了公认的“通解通法”。&lt;/p&gt;
&lt;p&gt;原因之二，也是最重要的原因，就是“&lt;strong&gt;权力越大，责任越大&lt;/strong&gt;”。我们常说，如果一个设备制造商允许用户刷机并给了教程，那它确实是在支持用户折腾设备；如果设备制造商直接允许在设置中开启 Root 权限，那么它是在给维修点刷业绩。正因如此，我强烈反对使用人工代刷服务来获取 Root 权限。&lt;/p&gt;
&lt;p&gt;获取 Root 权限&lt;strong&gt;并非一劳永逸&lt;/strong&gt;。由于 Root 权限意味着设备最高访问权，一旦后续使用 Root 权限时不谨慎，系统就可能被弄坏。此时，必须使用刷机方式才能修复设备，而使用“一键 Root”工具的人，往往不具备刷机的知识和技能，因此设备只能被送去维修，或者报废。使用刷机方法获取 Root 的过程中，机主锻炼了自身的技能，并已经下载到或者备份好了原厂镜像。只有这样，机主才算是真正拿到了“玩机资格证”。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;$\large{\text{刷机会不会清除数据？}}$&lt;/p&gt;
&lt;p&gt;刷机本身不清除数据。但是，下面几种情况需要清空数据。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用正常 Fastboot 方式解锁 Bootloader，会一并清除数据。（通常，这只需要做一次）&lt;/li&gt;
&lt;li&gt;安装第三方操作系统，通常要自行清空数据，否则系统无法正常工作。&lt;/li&gt;
&lt;li&gt;获取 Root 权限后，不当修改系统数据、禁用系统组件，设备出现无法开机、无限重启的问题，重刷原厂镜像也不能解决时。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;各种各样的刷机事故也会导致需要清除数据。因此，开始前，请&lt;strong&gt;对重要数据做好备份&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h2&gt;我用的是不是 OnePlus 设备？&lt;/h2&gt;
&lt;p&gt;建议自己检查手机后盖或系统信息界面。&lt;/p&gt;
&lt;h2&gt;一定能成功吗？&lt;/h2&gt;
&lt;p&gt;一般是的。但是下面是一些有可能发生的问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用版本不对的 Boot 镜像，系统不能正常启动。&lt;/li&gt;
&lt;li&gt;Magisk 版本对当前 Android 版本不支持，系统不能正常启动。（这种情况发生后可自行撤销更改）&lt;/li&gt;
&lt;li&gt;网上找不到版本合适的全量更新包。（购机前可事先考证。请注意，官网上的更新包肯能不是最新的）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;此设备登录了非你本人的 HeyTap 账户且开启了查找设备&lt;/strong&gt;，这种情况解锁 Bootloader 从而恢复出厂设置后，设备会被锁定，输入原账户的密码才能解锁。（如果你确实是该设备的合法拥有者，可以通过在&lt;strong&gt;系统设置中&lt;/strong&gt;进行恢复出厂设置来一并清除账号绑定）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;h3&gt;如果需要，学好基础知识&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;/blog/play-android/preintro-adb&quot;&gt;[基础预科] ADB、Android 终端、Android 用户权限&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/blog/play-android/preintro-bootmode&quot;&gt;[基础预科] Android 分区、启动模式、Fastboot&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;确认设备如何强制关机&lt;/h3&gt;
&lt;p&gt;开始刷机操作前，请确认自己知道如何强制关机或重启设备，&lt;strong&gt;并且已经尝试成功过&lt;/strong&gt;。常见方法有这些：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拔掉充电器，然后拆除内置电池，将会关机。（适用于电池可拆卸的设备）&lt;/li&gt;
&lt;li&gt;长按电源按钮 10s，强制重启。（适用于较老的设备）&lt;/li&gt;
&lt;li&gt;同时按住电源和音量上按钮，保持 10s，待屏幕熄灭或感受到振动后立即放开，即为强制关机。（适用于较新的设备）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;确认如何从关机状态进入 Fastboot&lt;/h3&gt;
&lt;p&gt;请确认自己知道如何从关机状态下进入 Fastboot 模式，&lt;strong&gt;并且已经尝试成功过&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;有些文章会告诉你可以通过 USB 调试从开机状态下重启进入 Fastboot。&lt;strong&gt;请不要仅仅满足于这么做&lt;/strong&gt;，因为若刷机中途失误，你可能必须直接进入 Fastboot 进行修复，而无法开机，再通过 USB 调试进入。&lt;/p&gt;
&lt;p&gt;~~你看，这篇文章的“准备工作”章节就根本没要求你打开 USB 调试。~~&lt;/p&gt;
&lt;p&gt;对于 OnePlus 设备，具体有两种情况。&lt;/p&gt;
&lt;p&gt;较老设备：&lt;/p&gt;
&lt;p&gt;|操作|按键组合|功能|
|-|-|-|
|关机长按|电源 + 音量上|Recovery 模式|
|关机长按|电源 + 音量下|Fastboot 模式|
|关机长按|电源 + 音量上 + 音量下|无法启动[待考证]|&lt;/p&gt;
&lt;p&gt;较新设备：&lt;/p&gt;
&lt;p&gt;|操作|按键组合|功能|
|-|-|-|
|关机长按|电源 + 音量上|正常启动|
|关机长按|电源 + 音量下|I. 若 Bootloader 未解锁，则 Recovery 模式II. 若 Bootloader 已解锁，则 Fastboot 模式|
|关机长按|电源 + 音量上 + 音量下|Fastboot 模式|&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&apos;注意&apos;&gt;
对于一些更加新的设备，你可能无法成功通过关机状态下按键进入 Fastboot 模式，而只能进入 Recovery 模式。这是正常的——最新的一些设备必须解锁 Bootloader 后才能在关机状态下按键进入 Fastboot 模式，而初次解锁前则只能使用 USB 调试。&lt;/p&gt;
&lt;p&gt;这种情况下，可以先用 USB 调试方法解锁，再尝试按键进入 Fastboot 模式。&lt;/p&gt;
&lt;p&gt;注意，出厂搭载 ColorOS 16 及更高版本的系统&lt;a href=&quot;https://bbs.oneplus.com/thread/1926504022886318086&quot;&gt;需要申请深度测试&lt;/a&gt;，审核通过，然后在审核通过界面点击重启按钮才能进入 Fastboot 模式。你应当在此次进入 Fastboot 模式后立即解锁 Bootloader，之后应当可以正常按键进入 Fastboot 模式。&lt;/p&gt;
&lt;p&gt;如果解锁 Bootloader 后仍然无法按键进入 Fastboot 模式，非常不建议继续——请考虑停止操作并重新锁定 Bootloader。
&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/26bd642a6eccbddd637df0706c793d0b.C3Lb74Pj_Z1j9fMg.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;OnePlus 6T 的 Fastboot 界面实拍。请注意 START 的意思是重启进入正常系统。&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/7fc3b36510f40e67fb14dabfd14c4aa1.DhbNZGPf_fq3eI.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/afb1562ff69786c1ee717fbdde54e6c3.CT-JSMJ0_ZomAmh.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/3cb9d19cf7487cd4ed132b6b7d1e6842.gHAoFpbp_60vfs.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;效果图。此界面可执行几个简单的操作。按音量键选择要进行的操作，电源键确认。&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;注：如果你无法正常退出 Fastboot 模式，请参照强制关机的方法。&lt;/p&gt;
&lt;h3&gt;确认 USB 接触良好&lt;/h3&gt;
&lt;p&gt;接触良好的 USB 接口和数据线是刷机的基础。若刷机过程中连接断开，将产生无法开机的后果（重新刷入受损的分区可以恢复）。&lt;/p&gt;
&lt;p&gt;建议使用 Type-C 接口的 USB 集线器，刷机过程中不要在集线器上插拔设备。&lt;/p&gt;
&lt;p&gt;提示：Fastboot 刷入 Boot 分区，大约需要 2.5s。&lt;/p&gt;
&lt;h3&gt;安装 ADB 环境&lt;/h3&gt;
&lt;p&gt;若电脑上没有 ADB，请先安装。方法参照&lt;a href=&quot;/blog/play-android/preintro-adb#install-adb&quot;&gt;此处&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;你安装的 ADB 中应该会有 &lt;code&gt;fastboot.exe&lt;/code&gt;，否则寄。&lt;/p&gt;
&lt;p&gt;安装后，在命令行中执行命令 &lt;code&gt;fastboot --version&lt;/code&gt; 验证你的安装。&lt;a href=&quot;https://www.bing.com/search?q=Windows+%E5%91%BD%E4%BB%A4%E8%A1%8C&amp;#x26;mkt=zh-CN&quot;&gt;【搜索“Windows 命令行”】&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;允许 OEM 解锁&lt;/h3&gt;
&lt;p&gt;进入开发者选项，打开“允许 OEM 解锁”开关，按提示输入设备锁屏密码，以允许后续解锁 Bootloader。&lt;a href=&quot;https://cn.bing.com/search?q=%E5%BC%80%E5%8F%91%E8%80%85%E9%80%89%E9%A1%B9&amp;#x26;pglt=675&quot;&gt;【搜索“OnePlus 开启开发者选项”】&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;此开关一般位于开发者选项第一屏内。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/d32401a884c5eedb19d42d7c3e24e194.C08H5v4I_1bieov.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/414884ae2ad4c91e68c9320cfa3e3ee8.DD4AUTad_ZU4hWK.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/67aa876265a2f41ce33ae35d7112a8de.-N0Y2au0_toJP7.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;注：&lt;strong&gt;该操作并没有解锁 Bootloader&lt;/strong&gt;，只是允许解锁。&lt;/p&gt;
&lt;h3&gt;关闭系统自动更新&lt;/h3&gt;
&lt;p&gt;系统更新过程会移除已安装的 Magisk。为防止这种意外，应当禁止系统自动更新。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow maxWidth=&apos;520px&apos;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/ba570f0afdc03ea568afab831d37bc3a.Cpch5STZ_Z23iYO3.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/68fb52056d39aa9f4d6450a7ac81b480.BcembGKT_ZK1xuI.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;h3&gt;确认云账户状态&lt;/h3&gt;
&lt;p&gt;如果设备已登录云账户且已经开启“查找设备”，请确保自己知道这个云账户的密码，并在解锁 Bootloader 前退出账户。&lt;/p&gt;
&lt;p&gt;若没有退出，&lt;strong&gt;则再次启动后设备会被锁定，输入云账户密码方可解锁&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;调整心态&lt;/h3&gt;
&lt;p&gt;刷机是有风险的操作。然而，对于 OnePlus 设备和 Magisk 方案，风险实际上极小。请放松心态。&lt;/p&gt;
&lt;p&gt;刷机时，请选择安排不紧张的时间段，保证至少一小时的连续空闲时间，切忌仓促行事，否则容易犯错。&lt;/p&gt;
&lt;p&gt;操作过程中，时刻记住，设备不会被轻易搞坏，即使搞坏了大概率也能自己救。由于采用 Magisk 方案，你可以告诉自己任何更改前，都有机会先“试用”待刷的镜像，观察其能否正常工作（本文章也会让你这么干）。&lt;/p&gt;
&lt;p&gt;本文章会告诉你一些常见的故障如何处理。请看下文每个步骤后的“快速故障排除”表。&lt;/p&gt;
&lt;h2&gt;概览&lt;/h2&gt;
&lt;p&gt;Magisk 要修改 Boot 镜像，然而，在没有 Root 权限时，并没有办法直接提取 Boot 镜像，也无法将修改的镜像直接写入设备。因此，我们必须先从系统更新全量包中提取镜像，交给 Magisk App 进行“修补”，然后自己刷入。&lt;/p&gt;
&lt;p&gt;这只是概览，不要直接对着操作。预计用时假设操作者是新手，但操作顺利。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装 Magisk App &amp;#x3C;F.Muted&gt;5分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;🎇 选择合适的 Magisk 版本 &amp;#x3C;F.Muted&gt;2分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;🔰 下载并安装 App &amp;#x3C;F.Muted&gt;3分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;👀 查看 Ramdisk 是否为“是”，确认是否受支持 &amp;#x3C;F.Muted&gt;0分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;从全量包提取原厂镜像 &amp;#x3C;F.Muted&gt;16分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;💻 下载全量包 &amp;#x3C;F.Muted&gt;15分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;🧵 取出全量包中的 Boot 镜像 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;解锁 Bootloader &amp;#x3C;F.Muted&gt;6分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;⭕ 进入 Fastboot &amp;#x3C;F.Muted&gt;0分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;🔌 安装驱动，调试 Fastboot 连接 &amp;#x3C;F.Muted&gt;2分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;🔓 发起解锁命令并确认 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;⭕ 重启设备，待数据清除完毕并开机一次 &amp;#x3C;F.Muted&gt;2分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;👀 检查解锁状态 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;测试原厂镜像 &amp;#x3C;F.Muted&gt;4分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;⭕ 进入 Fastboot &amp;#x3C;F.Muted&gt;0分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;🪄 传入你提取的原厂镜像进行试用 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;✅ 确认设备工作正常 &amp;#x3C;F.Muted&gt;3分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;修补镜像 &amp;#x3C;F.Muted&gt;6分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;🔰 重新安装 Magisk App &amp;#x3C;F.Muted&gt;2分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;🩹 利用 Magisk App 修补镜像 &amp;#x3C;F.Muted&gt;3分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;⏏️ 导出新镜像 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;试用镜像并确认安装 &amp;#x3C;F.Muted&gt;5分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;⭕ 进入 Fastboot &amp;#x3C;F.Muted&gt;0分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;🪄 传入修补后的镜像进行试用 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;✅ 确认设备工作正常 &amp;#x3C;F.Muted&gt;3分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;⚡ 确认安装 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;⭕ 重启设备 &amp;#x3C;F.Muted&gt;0分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;开机体验 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;✨ 打开需要 Root 权限的应用，或安装模块，开始使用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;共计 43 分钟。&lt;/p&gt;
&lt;p&gt;请注意，接下来请按照下面的指示进行操作，不要中断。教程未要求重启时，请勿自行重启。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Tip title=&quot;提示&quot;&gt;&lt;/p&gt;
&lt;p&gt;选择确认安装前，设备的系统并没有受到更改。&lt;/p&gt;
&lt;p&gt;若要中断 Magisk 的安装，只须断开 USB 连接，重启设备，然后卸载 Magisk App。&lt;/p&gt;
&lt;p&gt;若安装后需要恢复，建议直接使用 Recovery 模式刷入官方全量包。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Tip&gt;&lt;/p&gt;
&lt;h2&gt;安装 Magisk App&lt;/h2&gt;
&lt;h3&gt;选择合适的 Magisk 版本&lt;/h3&gt;
&lt;p&gt;目前主流的 Magisk 版本有原版和 Kitsune Mask。原版 Magisk 的优势在于维护较为积极，稳定版更新较快；Kitsune Mask 的优势在于具有内置的 Root 隐藏功能（隐藏后仍然可以通过代码注入修改应用）以及白名单模式（默认对所有应用隐藏 Root，只有用户指定的应用可以检测到并使用 Root），但是已经停止更新。&lt;/p&gt;
&lt;p&gt;此外，你还可以尝试 &lt;a href=&quot;https://kernelsu.org/&quot;&gt;KernelSU&lt;/a&gt;（及其衍生物 &lt;a href=&quot;https://kernelsu-next.github.io/webpage/&quot;&gt;KernelSU-Next&lt;/a&gt;、&lt;a href=&quot;https://sukisu.org/zh/&quot;&gt;SukiSU-Ultra&lt;/a&gt;）或者 &lt;a href=&quot;https://apatch.dev/&quot;&gt;APatch&lt;/a&gt; 等更加现代的「内核级」方案。它们的 LKM 或内核修补安装方式与 Magisk 的修补方式类似，但对内核有更多特殊要求，且有更大的内核崩溃风险。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/topjohnwu/Magisk&quot;&gt;原版 Magisk&lt;/a&gt;  &lt;a href=&quot;https://github.com/HuskyDG/magisk-files&quot;&gt;Kitsune Mask&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下载到合适的 APK 文件后，将其传输至手机完成安装。&lt;/p&gt;
&lt;h3&gt;查看支持情况&lt;/h3&gt;
&lt;p&gt;打开 Magisk App，观察主页上的参数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/c8349a6fd5a83cf586470fb8497e0826.CYdcjKWj_Z1u6K36.webp&quot; alt=&quot;&quot; title=&quot;[maxw=240]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;若 Ramdisk 一栏为“是”，则你的设备可以安装 Magisk&lt;/strong&gt;。提示：目前没有已知的 OnePlus 设备存在不支持问题，且 Ramdisk 在 Android 11 之后被强制要求，故后续也不会出现类似问题。&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：Ramdisk 显示“否”，能否继续？&lt;/strong&gt;|
|A：不建议继续。这里可能存在误检测情况，但目前已知的误检测只会发生在小米设备上。|&lt;/p&gt;
&lt;h2&gt;从全量包提取原厂镜像&lt;/h2&gt;
&lt;h3&gt;下载全量包&lt;/h3&gt;
&lt;p&gt;先打开系统设置，查看你设备的系统版本号。&lt;/p&gt;
&lt;p&gt;在网络上搜索 &amp;#x3C;你的设备名称&gt; + “官方固件”，找到&lt;strong&gt;对应版本&lt;/strong&gt;或更新版本（尽量与当前版本接近）的固件并下载。下载后，请使用系统更新中的「本地安装」功能将系统覆盖安装或更新到此版本，可靠的方法是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将固件包传输到设备；&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/play-android/preintro-adb#%E5%90%AF%E7%94%A8%E5%BC%80%E5%8F%91%E8%80%85%E9%80%89%E9%A1%B9&quot;&gt;启用开发者选项&lt;/a&gt;；&lt;/li&gt;
&lt;li&gt;开启飞行模式，即断开网络连接；&lt;/li&gt;
&lt;li&gt;在设置中的应用管理中找到系统应用「软件更新」，并清除数据，这样会强制清除之前检测到的新版本信息，确保本地安装可正常进行；&lt;/li&gt;
&lt;li&gt;进入软件更新，点击右上角菜单中的「本地安装」；&lt;/li&gt;
&lt;li&gt;选择固件包安装。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从本地固件包安装之前，系统会自动进行签名校验。如果安装成功，说明你下载的固件包是未经修改的官方版本，且设备正确，&lt;strong&gt;可以放心使用该固件包&lt;/strong&gt;；如果安装不成功，说明固件包已损坏、被修改或者与你的设备不对应，&lt;strong&gt;切勿使用该固件包继续操作&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;注：目前官方网站上版本不全。可以找一找民间的下载。&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：应当去哪里找全量包？&lt;/strong&gt;|
|A：没有固定答案，请自行搜索。&lt;a href=&quot;https://yun.daxiaamu.com/OnePlus_Roms/&quot;&gt;大侠阿木云盘&lt;/a&gt;似乎收录了较多一加设备的全量包，但请确保通过本地安装验证其可靠性。|&lt;/p&gt;
&lt;h3&gt;刷入全量包&lt;/h3&gt;
&lt;p&gt;若你的全量包版本比当前系统版本新，可以先利用“系统更新”应用的本地安装功能，将系统变为该版本。&lt;a href=&quot;https://cn.bing.com/search?q=OnePlus+%E7%B3%BB%E7%BB%9F%E6%9B%B4%E6%96%B0%E6%9C%AC%E5%9C%B0%E5%AE%89%E8%A3%85&quot;&gt;【搜索“OnePlus 系统更新本地安装”】&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;提取 Boot 镜像&lt;/h3&gt;
&lt;p&gt;将全量包作为压缩包打开，从中找出 &lt;code&gt;boot.img&lt;/code&gt; 镜像并解压出来。&lt;/p&gt;
&lt;p&gt;如果全量包中没有 &lt;code&gt;boot.img&lt;/code&gt; 但是有一个很大的 &lt;code&gt;payload.bin&lt;/code&gt; 文件，你需要使用 &lt;a href=&quot;https://github.com/ssut/payload-dumper-go/releases&quot;&gt;payload-dumper-go&lt;/a&gt; 从该文件中提取 &lt;code&gt;boot.img&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&apos;注意&apos;&gt;
如果你的全量包或者 &lt;code&gt;payload.bin&lt;/code&gt; 中包含 &lt;code&gt;init_boot&lt;/code&gt; 分区，请提取该分区（&lt;strong&gt;并且后续也刷入该分区而非 &lt;code&gt;boot&lt;/code&gt; 分区&lt;/strong&gt;）。Magisk 将会需要这个分区。&lt;/p&gt;
&lt;p&gt;如果你使用 KernelSU 或 APatch，请阅读其文档了解需要使用哪个分区。
&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;h2&gt;解锁 Bootloader&lt;/h2&gt;
&lt;h3&gt;连接 Fastboot&lt;/h3&gt;
&lt;p&gt;将设备关机，启动进入 Fastboot 模式，然后使用数据线连接至电脑（操作顺序请勿颠倒）。&lt;/p&gt;
&lt;p&gt;在命令行中执行命令 &lt;code&gt;fastboot devices&lt;/code&gt;，查看设备是否连接并识别。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;C:\Users\[redacted]&gt;fastboot devices
0123456789ABCDEF        fastboot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;$\large{\text{上述命令无输出的解决方法}}$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开 Windows 设备管理器。&lt;a href=&quot;https://cn.bing.com/search?q=Windows+%E8%AE%BE%E5%A4%87%E7%AE%A1%E7%90%86%E5%99%A8&quot;&gt;【搜索“Windows 设备管理器”】&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;找到无法识别的 “Android” 设备，右键，选择“更新驱动程序”。
&lt;img src=&quot;https://ak-ioi.com/_astro/a463fd7bd3b6112ad0e9b5d592ad249d.CcdzsxT7_Pg4ET.webp&quot; alt=&quot;&quot;&gt;&lt;/li&gt;
&lt;li&gt;选择“浏览我的电脑以查找驱动程序”“让我从计算机的可用驱动程序列表中选取”。&lt;/li&gt;
&lt;li&gt;选择 Android Composite ADB Interface。
&lt;img src=&quot;https://ak-ioi.com/_astro/899bb798ba9c71f84aa28197132bc2c1.CaKWcvWb_Z1wQvgr.webp&quot; alt=&quot;&quot; title=&quot;[maxh=400]&quot;&gt;
如果这一步系统找不到驱动程序，则系统存在 ADB 驱动缺失问题。对于 Windows 11 或更高版本，可以检查 Windows 更新的 &lt;code&gt;高级选项 -&gt; 可选更新&lt;/code&gt; 中是否出现了 ADB Interface 驱动，如有请选择安装。其他情况下，请&lt;a href=&quot;https://cn.bing.com/search?q=adb+%E9%A9%B1%E5%8A%A8%E7%BC%BA%E5%A4%B1&quot;&gt;搜索“adb 驱动缺失”&lt;/a&gt;，然后像安装 ADB 驱动一样安装 Fastboot 的驱动。&lt;/li&gt;
&lt;li&gt;完成后，将看到设备为可用状态。
&lt;img src=&quot;https://ak-ioi.com/_astro/10daec1a01797c254f88a9a0b98071c3.Dn0abok6_1uIXWJ.webp&quot; alt=&quot;&quot;&gt;&lt;/li&gt;
&lt;li&gt;重新执行 &lt;code&gt;fastboot devices&lt;/code&gt;，将能够看到设备。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&apos;注意&apos;&gt;
对于一些非常新的设备，你可能无法成功通过关机状态下按键进入 Fastboot 模式，而只能进入 Recovery 模式。这是正常的——最新的一些设备必须解锁 Bootloader 后才能在关机状态下按键进入 Fastboot 模式，而初次解锁前则只能使用 USB 调试。&lt;/p&gt;
&lt;p&gt;这种情况下，可以先&lt;a href=&quot;/blog/play-android/preintro-adb#reboot-device&quot;&gt;用 USB 调试方法进入 Fastboot 模式&lt;/a&gt;解锁，再确保解锁后能够成功按键进入 Fastboot 模式。
&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;若能够看到设备已连接，则说明连接成功&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：始终无法检测到设备。&lt;/strong&gt;|
|A：请按上面说明安装驱动。如果仍然遇到问题，请自行搜索解决。|&lt;/p&gt;
&lt;h3&gt;发送解锁命令&lt;/h3&gt;
&lt;p&gt;&amp;#x3C;F.Danger title=&quot;危险&quot;&gt;&lt;/p&gt;
&lt;p&gt;请注意，&lt;strong&gt;解锁 Bootloader 将清空你的用户数据&lt;/strong&gt;。请对重要内容做好备份。&lt;/p&gt;
&lt;p&gt;解锁后&lt;strong&gt;不建议重新锁定&lt;/strong&gt;。若设备镜像未恢复原厂状态，这会导致无法开机。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Danger&gt;&lt;/p&gt;
&lt;p&gt;连接好 Fastboot 模式下的设备，运行命令 &lt;code&gt;fastboot flashing unlock&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;此时设备上将弹出警告文字，&lt;strong&gt;请仔细阅读全部文字&lt;/strong&gt;（以你设备上显示的文字为准）。读完后，按音量键，使得 UNLOCK THE BOOTLOADER 被选中，然后按电源键确认。完成后，电脑上将显示 OKAY 字样。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow maxWidth=&apos;520px&apos;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/20eea79cdf7c5c3cdf502d624af39e52.CQpAl4eM_Z1ggMfj.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/484fee31bef766186dcc6b0874eccc44.DKy9uUc__Z1DeirM.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;接下来请看 Fastboot 界面上的 DEVICE STATE。请确保显示 unlocked。&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：无法解锁，显示 Permission Denied。&lt;/strong&gt;|
|A：请确保已经在设置中开启“允许 OEM 解锁”。若不是 OnePlus 手机，解锁会更加麻烦，请参考该品牌的官方说明。|
|&lt;strong&gt;Q：解锁是否会对数据安全造成威胁？&lt;/strong&gt;|
|A：一定程度上会（具体表现为设备加密失效，设备丢失后数据更容易被窃取）。请慎重对待。|
|&lt;strong&gt;Q：解锁后开机出现 Orange State 警告。&lt;/strong&gt;|
|A：这是正常现象，用于提醒用户设备处于解锁状态。|
|&lt;strong&gt;Q：如何重新锁定？&lt;/strong&gt;|
|A：&lt;code&gt;fastboot flashing relock&lt;/code&gt;。然而一旦修改过系统，不建议重新锁定。|&lt;/p&gt;
&lt;h3&gt;重启清空数据&lt;/h3&gt;
&lt;p&gt;完成后，运行命令 &lt;code&gt;fastboot reboot&lt;/code&gt;（或在手机上选择操作 START）重启。&lt;/p&gt;
&lt;p&gt;接下来，手机会先重启进入 Recovery 模式以清空数据。完成后，手机将再次重启，这一次重启会花费比往常更多的时间。重启后，请再次完成手机的初次使用设置。&lt;/p&gt;
&lt;p&gt;然后不妨进入开发者选项（你需要重新启用开发者选项）看一眼。此时，&lt;strong&gt;“允许 OEM 解锁”设置的开关应当为开启状态并变为灰色，下方显示字样“引导加载程序已解锁”&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow maxWidth=&apos;520px&apos;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/6b4dd192e6295907dac026becb45afaa._esD4gm2_1gzAdD.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/e5224d4279703e2846b3f79f1fe0c74a.Bscrr4Qe_19tJfO.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;据测试，ColorOS 中一些用于调试代码会通过 Bootloader 解锁状态判断当前设备是否是工程机。如果 Bootloader 已解锁，系统的部分行为可能出现变化，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;耗电量增加；&lt;/li&gt;
&lt;li&gt;~~USB 调试功能有时将&lt;strong&gt;自动授权任意设备对手机进行调试&lt;/strong&gt;，对于用户，这是很大的安全隐患，因此请不要让 USB 调试常开。~~（进一步测试表明该行为可能不是 ColorOS 内置机制导致，但请谨慎）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果由于某些原因，你需要中断 Magisk 的安装（或需要完全卸载并且以后不再使用 Magisk），建议在&lt;strong&gt;确保所有系统镜像都为原厂状态&lt;/strong&gt;（一般可通过刷入系统更新的本地安装功能刷入当前版本全量包进行完全还原）的情况下重新锁定 Bottloader。需要注意，&lt;strong&gt;这一操作也会清空数据&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果你正确完成了 Magisk 的安装，那么你可以利用 Magisk 模块让系统以为 Bootloader 未解锁，稍后将提到这一点。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：开发者选项中的 OEM 解锁开关状态不正常。&lt;/strong&gt;|
|A：这可能不是大问题。请以 Fastboot 界面上的 DEVICE STATE 为准。|
|&lt;strong&gt;Q：重启大概需要多久？&lt;/strong&gt;|
|A：根据系统预编译的程度，30s 至 5min 不等。如果超过 10min，则为不正常状况，请强制重启。|&lt;/p&gt;
&lt;h2&gt;测试原厂镜像&lt;/h2&gt;
&lt;p&gt;在开始修改原厂镜像前，我们应当先试试你拿到的“原厂”Boot 镜像能否正常工作，特别是其版本与你系统版本不一致时。&lt;/p&gt;
&lt;h3&gt;原厂镜像试用启动&lt;/h3&gt;
&lt;p&gt;将设备关机，启动进入 Fastboot 模式，然后使用数据线连接至电脑（操作顺序请勿颠倒）。&lt;/p&gt;
&lt;p&gt;运行命令 &lt;code&gt;fastboot boot &amp;#x3C;boot 镜像文件&gt;&lt;/code&gt;，将会把此镜像传输至手机。手机会暂时地使用这一镜像进行启动，而不会直接将其刷入。你将会看到手机先回到带有“Fastboot Mode”字样的 Logo 界面，然后播放开机动画，进入系统。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;C:\Users\[redacted]&gt;fastboot boot &quot;E:\__my_workspace\BrushMachine\Oneplus 9R\[redacted]\boot-20220710.img&quot;
Sending &apos;boot.img&apos; (98304 KB)                      OKAY [  2.271s]
Booting                                            OKAY [  0.169s]
Finished. Total time: 2.490s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow maxWidth=&apos;520px&apos;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/f8c4e010b19b8856cc157b7ea0f06bf8.BvW-kJyG_Z7xQbM.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/5f9e4877151caaf7f35c91ddf6cb2566.CPuWSIVd_1QXTng.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;注意，开机动画显示异常、开机时间比正常开机长、开机后的卡顿比正常情况长、开机后一段时间不显示锁屏等，&lt;strong&gt;都不是正常现象&lt;/strong&gt;。如果出现这类情况，说明此镜像不能正常工作。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;请注意区分 &lt;code&gt;fastboot boot&lt;/code&gt; 和 &lt;code&gt;fastboot flash boot&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前者是试用启动，只是暂时地用镜像启动设备，而不会修改设备。&lt;/li&gt;
&lt;li&gt;后者是刷入镜像，会将镜像直接写入 Boot 分区。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：错误 Permission Denied。&lt;/strong&gt;|
|A：请再次检查 Bootloader 是否已解锁。|
|&lt;strong&gt;Q：错误 Unknown Command。&lt;/strong&gt;|
|A：你的设备不支持试用启动。如果你头铁，请跳过这一步继续（可能需要同时参考其他教程）。|
|&lt;strong&gt;Q：错误 is not a boot image。&lt;/strong&gt;|
|A：请检查传入的镜像是否正确。|&lt;/p&gt;
&lt;h3&gt;检查是否工作正常&lt;/h3&gt;
&lt;p&gt;请依次检查这些硬件是否正常工作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;显示与触屏&lt;/li&gt;
&lt;li&gt;屏幕指纹（如果有）&lt;/li&gt;
&lt;li&gt;重力/加速度传感器&lt;/li&gt;
&lt;li&gt;摄像头&lt;/li&gt;
&lt;li&gt;麦克风&lt;/li&gt;
&lt;li&gt;WiFi 连接&lt;/li&gt;
&lt;li&gt;移动数据连接&lt;/li&gt;
&lt;li&gt;扬声器&lt;/li&gt;
&lt;li&gt;耳机接口（如果有）&lt;/li&gt;
&lt;li&gt;静音开关（如果有）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请确保通知栏、导航等系统界面功能正常。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果出现任何异常，都请返回重新下载合适的镜像&lt;/strong&gt;，不要继续操作。&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：试用启动期间是否会保存我产生的数据？&lt;/strong&gt;|
|A：会。除 Boot 镜像是临时的以外，试用启动与正常启动没有其他区别。|&lt;/p&gt;
&lt;h2&gt;修补镜像&lt;/h2&gt;
&lt;p&gt;如果上述测试一切正常，你可以选择重启设备，也可以直接在试用状态下继续。现在，使用 Magisk App 修补你的原厂镜像。&lt;/p&gt;
&lt;h3&gt;重新安装 Magisk App&lt;/h3&gt;
&lt;p&gt;由于前面清空过数据，Magisk App 已经无了。请将你下载的 APK 传输到手机重新安装。&lt;/p&gt;
&lt;h3&gt;修补镜像&lt;/h3&gt;
&lt;p&gt;将你的原厂镜像传输到手机中。&lt;/p&gt;
&lt;p&gt;打开 Magisk App，点击“安装”，选择“选择并修补一个文件”，完成镜像修补。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/bf3c6ced542cea9e7273a7a154b8aa4a.DBt8Wejt_1qldQo.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/0f90c3bee3bad3617e471403a167354e.D8qodLh5_ZKeIop.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/b04323b76496a875b5da58535a118c44.C72o5Kl0_2vWMsJ.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/4db68e32a616f5fade75119f72b22625.0OAdCMw0_wpTUJ.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/dd11e4c0a68d476eaa2b3a2e2334f5ba.D3A7vKfS_1TgIMJ.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/86e518c1126a65e1121553835086e2d7.DDpSMp4r_Z2q9Lgn.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;修补后的镜像存在 &lt;code&gt;/sdcard/Download/&lt;/code&gt; 下。请对其进行重命名或移动，然后传输至电脑。&lt;strong&gt;请仍然保管好修补前的镜像，并做好区分以防混淆&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：要不要选择修补 vbmeta？&lt;/strong&gt;|
|A：正常情况下不需要，且开启这个选项可能导致修补后的系统不能正常启动。|&lt;/p&gt;
&lt;h2&gt;试用镜像并确认安装&lt;/h2&gt;
&lt;p&gt;接下来，我们测试修补后的镜像是否正常工作。如果正常工作，接下来可以直接用 Magisk App 确认安装，不需要自行刷入。&lt;/p&gt;
&lt;h3&gt;修补后镜像试用&lt;/h3&gt;
&lt;p&gt;将设备关机，启动进入 Fastboot 模式，然后使用数据线连接至电脑（操作顺序请勿颠倒）。&lt;/p&gt;
&lt;p&gt;运行命令 &lt;code&gt;fastboot boot &amp;#x3C;boot 修补后镜像文件&gt;&lt;/code&gt;，使用修补后镜像启动。此次启动，你的系统将暂时具有 Magisk。&lt;/p&gt;
&lt;h3&gt;检查是否工作正常&lt;/h3&gt;
&lt;p&gt;先参照上文章节，检查设备各种功能是否工作正常。&lt;/p&gt;
&lt;p&gt;完成后，打开 Magisk App，主页上将检测出 Magisk 版本。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;目前发现，部分设备可能出现 &lt;code&gt;fastboot boot&lt;/code&gt; 不能正常试用启动，但刷入后能够启动的情况。如果你出现了修补后镜像不能启动的情况，可以在确认&lt;strong&gt;修补前&lt;/strong&gt;镜像能正常使用的情况下，选择执行 &lt;code&gt;fastboot flash boot &amp;#x3C;修补后文件&gt;&lt;/code&gt; 进行直接刷入。如果直接刷入仍不能正常启动，可用相同命令退回修补前的镜像。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：设备不能正常启动。&lt;/strong&gt;|
|A：这说明你的 Magisk 版本与 Android 版本不兼容，请使用更新的版本。|
|&lt;strong&gt;Q：Magisk 检测不到版本。&lt;/strong&gt;|
|A：这说明你的设备不支持 Magisk。|&lt;/p&gt;
&lt;h3&gt;确认安装&lt;/h3&gt;
&lt;p&gt;确认一切无误后，点击 Magisk App 主页上的“安装”，此时将能够选择“直接安装”选项。选择该选项完成安装即可。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/c30c09813bc0fca2f652002cef084f8d.DWqO9rXC_1TH4yq.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/f6fa03daed5a29993172f2ced989e95d.qRM2OzC2_JqHhO.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/14cecd665dbd8256aff5ad12371479e3.DdLs9YM5_2nuWJY.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;完成后重启，就正式完成了安装。&lt;/p&gt;
&lt;h3&gt;接下来...&lt;/h3&gt;
&lt;p&gt;接下来请看附录部分，提前了解系统更新的方法以及隐藏 Root 的方法。今后大概率会用到。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;刚才提到，ColorOS 中一些用于调试代码会通过 Bootloader 解锁状态判断当前设备是否是工程机，因此解锁 Bootloader 会导致耗电量增加，并可能导致安全隐患。因此，接下来强烈建议开启 Zygisk，然后&lt;a href=&quot;https://github.com/snake-4/Zygisk-Assistant&quot;&gt;安装 Zygisk Assistant 模块&lt;/a&gt;以向系统框架隐藏 Bootloader 已解锁的事实（只需要安装模块，不需要额外设置）。&lt;/p&gt;
&lt;p&gt;要检查隐藏是否成功，请进入开发者选项，确认“允许 OEM 解锁”开关已经恢复到解锁之前的样子（处于打开状态且没有变灰，可以切换）。注意，&lt;strong&gt;不建议关闭这个开关&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;即便成功配置 Zygisk Assistant，也仍然不要保持 USB 调试常开。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;h2&gt;附录：系统更新的方法&lt;/h2&gt;
&lt;p&gt;系统更新后不需要重新解锁，但需要重新刷入 Magisk。（设备支持 A/B 无缝系统更新的除外，见下文）&lt;/p&gt;
&lt;p&gt;对于已安装 Magisk 需要更新系统的设备，可以通过某些手段从官方渠道下载更新包中的分包（大小约为全量包的一半），提取可靠的 &lt;code&gt;boot.img&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;此处关于提取全量包下载网址的说明以 OnePlus 9R 为例，其他设备情况不尽相同。&lt;/p&gt;
&lt;h3&gt;前置要求&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;设备已获取 Root 权限&lt;/li&gt;
&lt;li&gt;OnePlus 系统更新检测到 Magisk，决定下载全量包（系统更新界面此时会显示文件大小是 3GB 以上）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;提示：如果你的系统安装了 Magisk 却没有自动下载全量包（即文件大小小于 3GB），请先选择下载这个更新包，然后安装。更新将会失败，失败后系统将改用全量包。&lt;/p&gt;
&lt;h3&gt;下载更新&lt;/h3&gt;
&lt;p&gt;请先确保关闭“夜间自动安装”。然后下载更新，直至下载完成。先不要选择安装。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/b8c7d6413a65b98566455870c2546746.Uh7h9upo_ZdGM7F.webp&quot; alt=&quot;&quot; title=&quot;[maxw=240]&quot;&gt;&lt;/p&gt;
&lt;h3&gt;查询地址并下载分包&lt;/h3&gt;
&lt;p&gt;使用合适的文件管理器取出 &lt;code&gt;/data/data/com.oppo.ota/databases/ota.db&lt;/code&gt;（或 &lt;code&gt;/data/data/com.oplus.ota/databases/ota.db&lt;/code&gt;），复制到合适位置，然后使用合适的 SQLite 查看器打开。另拷一份到电脑备用。&lt;/p&gt;
&lt;p&gt;找到 &lt;code&gt;system_vendor&lt;/code&gt; 包对应的行，然后找到 &lt;code&gt;url&lt;/code&gt; 列，复制出单元格的内容。这就是包含 &lt;code&gt;boot.img&lt;/code&gt; 的分包的下载地址。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/9cc6623b5dfd40a0cd12f1bf00ce53d5.D7GM5Hna_Z1H8WIz.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/5767c2d94582153360e0e623ca063d8d.efCmo2EB_Z1bKhd9.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/4f3b7d1e7e09c058ddc7cb913bea3b08.DoHrWnqX_1lNzSD.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;用浏览器访问该地址，下载分包，提取出其中的 &lt;code&gt;boot.img&lt;/code&gt;。如果服务器出现 403 Forbidden 错误，请尝试将 User-Agent 设为以下值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;Dalvik/2.1.0 (Linux; U; Android 14; LE2100 Build/UKQ1.230924.001)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在浏览器上可以使用一些 User-Agent 修改插件来修改 User-Agent。&lt;/p&gt;
&lt;h3&gt;镜像修补&lt;/h3&gt;
&lt;p&gt;将 &lt;code&gt;boot.img&lt;/code&gt; 传输至手机进行修补，然后重新传输到电脑。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/bf3c6ced542cea9e7273a7a154b8aa4a.DBt8Wejt_1qldQo.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/0f90c3bee3bad3617e471403a167354e.D8qodLh5_ZKeIop.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/b04323b76496a875b5da58535a118c44.C72o5Kl0_2vWMsJ.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/4db68e32a616f5fade75119f72b22625.0OAdCMw0_wpTUJ.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/dd11e4c0a68d476eaa2b3a2e2334f5ba.D3A7vKfS_1TgIMJ.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/86e518c1126a65e1121553835086e2d7.DDpSMp4r_Z2q9Lgn.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;h3&gt;安装更新&lt;/h3&gt;
&lt;p&gt;在手机上直接安装系统更新，待所有流程完成后，关机。&lt;/p&gt;
&lt;h3&gt;试用并确认&lt;/h3&gt;
&lt;p&gt;打开 Fastboot 模式，试用启动修补后的 &lt;code&gt;boot.img&lt;/code&gt;。参照上文检查设备工作是否正常。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;E:\__my_workspace\BrushMachine\Oneplus 9R\[redacted]&gt;fastboot boot &quot;E:\__my_workspace\BrushMachine\Oneplus 9R\[redacted]\magisk_patched-24308_rqYSN.img&quot;
Sending &apos;boot.img&apos; (98304 KB)                      OKAY [  2.276s]
Booting                                            OKAY [  0.165s]
Finished. Total time: 2.495s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow maxWidth=&apos;520px&apos;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/f8c4e010b19b8856cc157b7ea0f06bf8.BvW-kJyG_Z7xQbM.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/5f9e4877151caaf7f35c91ddf6cb2566.CPuWSIVd_1QXTng.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;若无问题，打开 Magisk App 确认安装。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/3d1271629c542ae32b083471cd0fb11f.ByPspnxh_1c3PR4.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/d2fe78d3913c17fb1dbdbff1b6e137aa.BtLZrJUz_ZEshXK.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/ad47467d81e0946526bc56916373b76b.BcqAzHuz_Z1c8WHH.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;h2&gt;附录：A/B 无缝系统更新&lt;/h2&gt;
&lt;p&gt;此类系统更新可以在后台静默执行。更新完后，重启即可立即体验新版本。&lt;/p&gt;
&lt;p&gt;此类系统更新完成后，请先不要重启。打开 Magisk App，点击“安装”，选择“安装到未使用的槽位（OTA 后）”，然后重启即可使用。&lt;/p&gt;
&lt;h2&gt;附录：隐藏 Magisk 和 Root&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&apos;注意&apos;&gt;
Root 环境有许多种方式可能被检测，下文所述的是基础操作，仅应对一些基础的检测方式，已足以使大部分应用正常工作，但少数比较极端的应用和游戏仍可能出现问题。&lt;/p&gt;
&lt;p&gt;比较完美的 Root 环境隐藏是一个相对复杂的话题，往往需要不止一个工具，并且可能涉及比仅仅安装模块更复杂的操作，也有更大的风险。如果进行基础隐藏操作后，&lt;strong&gt;你的应用可以正常运行，则不必尝试所谓「完美隐藏」&lt;/strong&gt;。如果确实有必要进行更复杂的隐藏，请自行搜索了解相关方法。但是为了你的设备和数据安全，&lt;strong&gt;请不要使用不公开开源的工具，特别避免来路不明或仅在 Telegram 等内部渠道发布的安装包、模块等&lt;/strong&gt;。
&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;Magisk 和 Root 好用好玩之处固然多，然而有的应用会检测用户的 Magisk 与 Root，若发现会警告用户甚至拒绝加载。为防止它们受到影响，需要对 Magisk 和 Root 进行隐藏。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow maxWidth=&apos;520px&apos;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/ff2fa6cc1f2c0eb83eae6b5996f59598.DmhsiTSa_o9VqC.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/f08c7f6a0775e55c9842340b70d0c820.B02Ek22H_2mAAJv.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;h3&gt;使用伪装的 Magisk App&lt;/h3&gt;
&lt;p&gt;尽管检测应用是不正确的做法，但少数应用可能仍然通过检测系统上的 Magisk App 来发现 Magisk（例如“中国工商银行”应用）。这种检测利用的是包名。通过使用 Magisk App 的隐藏功能，可以为 Magisk App 随机生成包名，重新安装。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/157b0a91bc44fc9df8d0a2ccc541190b.XUMfMqPe_axOmv.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/b714556c6b8a2142d88c8ad507a5be78.B74fKtwE_Z2nQr1m.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/454d5afb5712ca871cdf966e5bcc14e1.kPT5DLAW_1e561K.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;伪装应用的名称一般不影响效果。&lt;/p&gt;
&lt;p&gt;请特别注意，禁止应用读取应用列表并&lt;strong&gt;不能&lt;/strong&gt;阻止 Magisk App 被发现，因为这只是禁止应用直接获取应用列表，该应用仍然可以查询某些特定的其他应用是否存在。&lt;/p&gt;
&lt;h3&gt;内置 Magisk 隐藏功能&lt;/h3&gt;
&lt;p&gt;在 Magisk 设置中，可启用 MagiskHide/排除列表 开关。接下来，打开排除列表，选择你&lt;strong&gt;需要对其隐藏&lt;/strong&gt; Magisk 的应用。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/157b0a91bc44fc9df8d0a2ccc541190b.XUMfMqPe_axOmv.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/325e1ddc8563d72177714ac7b4802165.D-caEagu_22wYO1.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/07f8b507162150c9a40852ccd6100458.tfWyIjQ__Z1x2zG2.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;这项功能在原版 Magisk 中称为“排除列表”，而在 Kitsune Mask 中称为 MagiskHide。二者工作原理不同，使用上的主要区别在于，若应用加入排除列表，LSPosed 等代码注入框架也将无法修改这个应用；而 MagiskHide 没有这个限制。&lt;/p&gt;
&lt;p&gt;Kitsune Mask 中的“SuList”功能就是 MagiskHide 白名单模式——只有在 SuList 中的应用可以检测到并调用 Root，其他应用不能感受到 Root 的存在。&lt;/p&gt;
&lt;h3&gt;第三方隐藏工具&lt;/h3&gt;
&lt;p&gt;第三方 Magisk 模块 &lt;a href=&quot;https://github.com/LSPosed/LSPosed.github.io/releases&quot;&gt;Shamiko&lt;/a&gt; 或者 &lt;a href=&quot;https://github.com/snake-4/Zygisk-Assistant&quot;&gt;Zygisk Assistant&lt;/a&gt; 也可以隐藏 Root。更推荐使用后者，因为前者并不开源，且目前可能已经不再积极维护，容易出现兼容性问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你使用原版 Magisk，那么这两个模块（选一个，不要一起用）可以代替“排除列表”功能，解决 LSPosed 等框架无法注入排除列表内应用的限制。&lt;/li&gt;
&lt;li&gt;如果你使用 Kitsune Mask，使用这两个模块（选一个，不要一起用）可以增强隐藏效果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;将下载的 zip 作为 Magisk 模块安装。随后进入 Magisk 设置，&lt;strong&gt;关闭“遵循排除列表”开关&lt;/strong&gt;。仍然需要在排除列表界面中选择你&lt;strong&gt;需要对其隐藏&lt;/strong&gt; Magisk 的应用。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/968d68cd4c0444db093066071e5c6d6f.BjpHAxSY_2kArU.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/75ede6593b46729c4b282fa70e627c53.BHVhtbxG_Z1JdGMr.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/1e0ef97a55ec8c7be7443cc940e9b44f.sapb-EK__Z1KLszF.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;除了对应用隐藏 Root 外，这两个模块还会对系统框架隐藏 Root 以及 Bootloader 解锁状态。&lt;/p&gt;
&lt;h2&gt;附录：救砖常见方法&lt;/h2&gt;
&lt;p&gt;刷机或使用 Root 不当可能造成系统被破坏无法启动，通常表现为无限重启循环。这种情况被称为 Bootloop，在中文中有时也称为「变砖」或「软砖」。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&apos;注意&apos;&gt;
下文所述为 Magisk 的方法。KernelSU 和 APatch 也均有内置的救砖机制，请参考各自的官方文档。
&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;h3&gt;通过触发安全模式禁用 Magisk 模块&lt;/h3&gt;
&lt;p&gt;如果安装某 Magisk 模块后导致无法启动或无限重启，可采用此方法。&lt;/p&gt;
&lt;p&gt;当系统以安全模式启动时，Magisk 会自动禁用所有模块（包括 MagiskHide）。OnePlus 大部分机型安全模式启动的方法是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在关机状态下长按电源键；&lt;/li&gt;
&lt;li&gt;第一屏亮起后，先等待，直到开机动画开始出现，此时立即按住音量减小键不放；&lt;/li&gt;
&lt;li&gt;等待系统启动完成或者出现三次短促的振动，即松开音量键。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;成功启动后应有“安全模式”字样。此时&lt;strong&gt;请勿&lt;/strong&gt;解锁进入桌面，否则会导致桌面布局丢失。请直接在锁屏界面下再重启一次。&lt;/p&gt;
&lt;p&gt;再次重启后系统应当退出安全模式，但是 Magisk 模块仍然会保持禁用。要恢复正常使用，你可能需要这些操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;重新启用 MagiskHide 以及没有问题的 Magisk 模块。&lt;/li&gt;
&lt;li&gt;重新选择输入法，如果你的偏好输入法并非系统默认。&lt;/li&gt;
&lt;li&gt;重新选择默认应用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他可能的 Magisk 模块导致无法启动的预防及恢复方法参见&lt;a href=&quot;/blog/play-android/mtk-flash-root#save-from-bootloop&quot;&gt;这篇文章&lt;/a&gt;，但是较为麻烦，在能够使用安全模式的情况下不推荐。&lt;/p&gt;
&lt;h3&gt;仅触发 Magisk 安全模式&lt;/h3&gt;
&lt;p&gt;进入系统安全模式进行救砖，副作用可能较大（桌面布局丢失、默认应用设置丢失等）。Magisk 还有一种方式可以仅禁用所有模块而不触发系统安全模式的操作方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在关机状态下长按电源键；&lt;/li&gt;
&lt;li&gt;第一屏亮起后，等待 3 秒（如果有 Orange State 警告，等到该警告出现即可），然后按住音量减小键不放；&lt;/li&gt;
&lt;li&gt;开机动画开始出现时立即松开音量减小键，避免触发系统安全模式。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果成功启动，且无“安全模式”字样，说明已通过 Magisk 安全模式救砖而未触发系统安全模式。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Muted&gt;封面图，即 OnePlus 6T Fastboot 实拍图片来自网络，未经明确许可。&amp;#x3C;/F.Muted&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/hero.BFLI8uI1.png"/><enclosure url="/_astro/hero.BFLI8uI1.png"/></item><item><title>[基础预科] Android 应用签名机制概述</title><link>https://ak-ioi.com/blog/play-android/preintro-app-signing</link><guid isPermaLink="true">https://ak-ioi.com/blog/play-android/preintro-app-signing</guid><description>签名机制是 Android 保障用户安全的重要方式。本文粗略讲述其运作原理。</description><pubDate>Fri, 10 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import F from &apos;@/components/mdx/formatting&apos;&lt;/p&gt;
&lt;p&gt;Android 本是一个开放、自由的系统。然而，自由带来权力，也带来更多的责任，普通的移动设备用户显然无法承担这些责任。于是，为了保护安全，Android 引入了签名机制。这一机制使得 Android 系统中大部分的自由都给了开发者，也保障了用户最基本的安全。&lt;/p&gt;
&lt;h2&gt;什么是签名&lt;/h2&gt;
&lt;p&gt;签名，指的是数字签名。&lt;/p&gt;
&lt;p&gt;数字签名的作用与纸质文件的上的签名一样，用于证明某文件确实是指定作者创制的，且具有防修改的特性。其具体内容如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;签名使用非对称加密算法。&lt;/li&gt;
&lt;li&gt;代表一个作者的有两个密钥：私钥和公钥。私钥由作者自行保管，公钥公布于众。&lt;/li&gt;
&lt;li&gt;非对称加密基于数学难题。根据私钥可以轻易生成公钥；公钥包含私钥的信息，但难以逆推出私钥。&lt;/li&gt;
&lt;li&gt;签名时，作者先计算文件的校验值，然后使用私钥对其进行“解密”操作，得到签名数据，随后将签名和公钥一起附在文件中。&lt;/li&gt;
&lt;li&gt;检验签名时，接收者用公钥对签名数据进行“加密”操作，还原成校验值，然后检验其与文件的校验值是否一致。若一致，说明文件确实来自公钥所对应的作者。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一旦文件遭到篡改，文件校验值就会变得与签名数据不符合，这样签名检验无法通过。而第三方没有掌握作者的私钥，因此也无法以作者的名义进行重新签名。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;时效性复核状态&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文章现在处于待复核状态，近期可能出现较大改动。&lt;/strong&gt;&lt;br&gt;
此文章的发布时间是 2022 年第二季度。&lt;br&gt;
请留意内容的时效性。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;讨论 Android 签名机制时，所谓“签名”通常指的是签名者的公钥，不同的文件“签名相同”表明文件来自同一作者。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h2&gt;应用安装时的签名校验&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;下文中“包名”是应用的唯一标识符。系统判断两应用是否为同一应用（是否发生覆盖安装），依据是包名是否相同。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h3&gt;应用安装&lt;/h3&gt;
&lt;p&gt;初次安装应用时，Android 会检测签名是否正确。如果签名不正确或不存在，则不允许安装。&lt;/p&gt;
&lt;p&gt;这时，Android 不会关心签名具体是什么，即不关心具体来自什么开发者。然而，有这些特殊情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有的系统会建立已知常用应用的签名数据库。若用户选择的安装包签名不符，会发出警告。&lt;/li&gt;
&lt;li&gt;大多数涉及敏感数据的应用会有自我签名验证行为，若签名不是来自原厂，则会警告用户或停止工作。然而，理论上这个验证是可以移除的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;应用更新&lt;/h3&gt;
&lt;p&gt;若安装应用包时，该应用已经存在，则需要满足以下条件才能安装：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新应用包签名与已有应用一致。&lt;/li&gt;
&lt;li&gt;新应用包版本不小于已有应用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;应用私有数据&lt;/h2&gt;
&lt;h3&gt;数据的存储位置&lt;/h3&gt;
&lt;p&gt;Android 系统中，应用将其数据存储在三种位置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;私有数据（&lt;code&gt;/data/data/&amp;#x3C;包名&gt;/&lt;/code&gt;）。此处存储的数据无法被其他应用访问。&lt;/li&gt;
&lt;li&gt;外部数据（&lt;code&gt;/sdcard/Android/data/&amp;#x3C;包名&gt;/&lt;/code&gt;）。此处存储的数据可以使用文件管理器查看或备份。申请并获取“存储”权限的应用，可以访问其他应用的外部数据。&lt;/li&gt;
&lt;li&gt;公共存储（&lt;code&gt;/sdcard/&lt;/code&gt;其他位置 或 外置存储器）。应用必须申请并获取“存储”权限，才能向此处写入数据。任何应用都可以访问这些数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中，私有数据和外部数据会在应用卸载时被系统清除。&lt;/p&gt;
&lt;h3&gt;私有数据的安全要求&lt;/h3&gt;
&lt;p&gt;私有数据是神圣之地，是敏感度最高的地方。&lt;/p&gt;
&lt;p&gt;要求任何应用均无法访问其他应用的私有数据；如果应用和系统都不具有“可调试”属性（注：只有开发用模拟器和工程机的系统会具有“可调试”属性），即便&lt;a href=&quot;/blog/play-android/preintro-adb&quot;&gt;使用 USB 调试&lt;/a&gt;，也无法访问应用私有数据。&lt;/p&gt;
&lt;p&gt;还要求处理同一私有数据的应用必须包名相同、签名相同，签名不同的应用绝对不能继承已有的私有数据。&lt;/p&gt;
&lt;p&gt;因此，对安全性要求高的应用（如支付宝）通常将用户令牌信息存储于此。单机游戏为了避免用户直接修改进度数据，也会将数据存储在这里。不难看出，有的安全要求确实是为了保护数据安全，有的则是刻意阻止用户修改，保护单机游戏的机制。&lt;/p&gt;
&lt;h3&gt;私有数据的备份&lt;/h3&gt;
&lt;p&gt;原生 Android 系统中，若应用具有“可备份”属性（大多数应用具有），则可以通过电脑发送备份命令备份私有数据。备份得到的文件可以拆包。然而，备份文件还携带了应用的签名信息和来自系统的签名，系统并不会允许将其恢复至签名不符的应用或另一设备中。&lt;/p&gt;
&lt;p&gt;许多国内定制版系统的数据备份功能所得文件的私有数据部分都会被加密，不允许用户查看和更改。&lt;/p&gt;
&lt;h2&gt;应用更新的安全要求&lt;/h2&gt;
&lt;h3&gt;应用更新时发生什么？&lt;/h3&gt;
&lt;p&gt;应用更新，即覆盖安装。相比于全新安装，应用更新有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装的新应用继承已授予原应用的权限。&lt;/li&gt;
&lt;li&gt;覆盖安装保留应用的私有数据和外部数据，因此新安装的应用将继承对它们的访问权。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;签名机制对私有数据的保护&lt;/h3&gt;
&lt;p&gt;系统对私有数据把守很严。然而，只要使用更新包覆盖掉原有应用，新的应用就可以访问原应用的的私有数据。如果坏人将一个后门程序伪装为更新包，则可以窃取用户数据。为避免这种情况，&lt;strong&gt;系统要求新应用与原应用签名一致，以保证它们来自相同开发者。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;此外，坏人还可能诱导用户用已知存在安全漏洞的旧版本覆盖安装应用，并借此窃取应用数据。因此，&lt;strong&gt;系统还要求新应用版本号不低于旧版本，防止回滚攻击。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;应用共享用户 ID&lt;/h2&gt;
&lt;p&gt;上文为了叙述方便，均未考虑共享用户 ID 的情况。&lt;/p&gt;
&lt;p&gt;应用的 &lt;code&gt;AndroidManifest.xml&lt;/code&gt; 文件中有一个称为“共享用户 ID”的属性 &lt;code&gt;sharedUserId&lt;/code&gt;。多个应用的此属性相同，则它们为“共用户应用”。共用户应用具有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装新应用时，若系统中已存在它的共用户应用，则新应用的签名必须与其一致，否则无法安装。&lt;/li&gt;
&lt;li&gt;共用户应用运行在同一进程中，这可以减少内存消耗。但是它们不会同时崩溃。&lt;/li&gt;
&lt;li&gt;共用户应用的私有数据可以互相访问。它们的私有数据仍然分开存储，并不在一起。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另有两个特殊的用户 ID：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;android.uid.system&lt;/code&gt; 表明应用为系统核心应用。具有此 UID 的应用签名必须与 &lt;code&gt;/system/framework/framework-res.apk&lt;/code&gt; 一致。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android.uid.systemui&lt;/code&gt; 表明应用为“系统界面”软件。具有此 UID 的应用应当有且仅有一个。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;framework-res.apk&lt;/code&gt; 的签名即为系统的“平台签名”。&lt;/p&gt;
&lt;h2&gt;对应用签名机制的态度&lt;/h2&gt;
&lt;p&gt;要自由还是要安全？人们对于这一问题的见解不同，因此对应用签名机制也有了不同的态度。&lt;/p&gt;
&lt;h3&gt;普通用户 - 接受&lt;/h3&gt;
&lt;p&gt;作为保障安全的重要屏障，普通用户接受这一限制。&lt;/p&gt;
&lt;p&gt;要利用好这道屏障，初次安装应用时一定要确保应用来自正规渠道，否则其可能已经携带了非官方的签名。&lt;/p&gt;
&lt;h3&gt;应用修改者 - 规避&lt;/h3&gt;
&lt;p&gt;应用修改者包括自行对应用进行逆向工程并修改的开发者，以及使用应用修改器修改游戏（多为单机小游戏）的用户。由于签名机制会阻碍第三方修改，这类用户会规避签名机制的限制。&lt;/p&gt;
&lt;p&gt;初次安装可能未来需要修改的应用时，他们将会用自己的证书重新签名再安装，以确保签名始终掌握在自己手中。后续新版的游戏或自己制作的修改版只要使用自己的证书再次签名，就能正常覆盖安装。&lt;/p&gt;
&lt;h3&gt;部分高级用户 - 接受并利用&lt;/h3&gt;
&lt;p&gt;高级用户，即常说的“Android 玩家”。此类用户的设备一般具有 Root 权限。&lt;/p&gt;
&lt;p&gt;由于 Root 权限的存在，此类用户可以任意导出、导入和修改应用的私有数据，签名机制已经不会造成过大的不便。正常情况下，他们让签名机制保护自己，特殊情况时，他们则利用文件管理器破例操作私有数据文件，达成自己的目的。&lt;/p&gt;
&lt;h3&gt;部分高级用户 - 全盘否定&lt;/h3&gt;
&lt;p&gt;由于正常 Android 设备都具有应用签名限制，事实上伪造签名进行的后门应用攻击已经少之又少。&lt;/p&gt;
&lt;p&gt;为了方便应用修改与破解的工作，有的高级用户不惜牺牲安全性（一般很少在主力手机上这么干）来换取便利，直接通过修改系统的方法彻底移除签名机制的限制（常称为“Android 核心破解”，幸运破解器等应用在已 Root 设备上会提供这项功能）。由于这样的破解会一并移除签名验证，使得系统认为一切签名有效，他们也可以使用伪造签名的方法绕过应用的自我签名验证，而无须破解自我签名验证再实施修改。&lt;/p&gt;
&lt;h2&gt;系统更新包签名&lt;/h2&gt;
&lt;p&gt;使用原厂 Recovery 系统更新 Android 系统时，为保证新的系统来自原厂，会使用与应用更新相似的签名限制。此处不做赘述。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Muted&gt;封面图像使用图标：&lt;a href=&quot;https://iconscout.com/icons/lock&quot;&gt;Lock Icon&lt;/a&gt; by &lt;a href=&quot;https://iconscout.com/contributors/chamedesign&quot;&gt;Chamestudio&lt;/a&gt;&amp;#x3C;/F.Muted&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/hero.CLfl_cqs.png"/><enclosure url="/_astro/hero.CLfl_cqs.png"/></item><item><title>[深度评测] 关于爱国者 M2 Pro 播放器你想知道的一切</title><link>https://ak-ioi.com/blog/rating/play-aigo-m2-pro</link><guid isPermaLink="true">https://ak-ioi.com/blog/rating/play-aigo-m2-pro</guid><description>爱国者 M2 Pro 是爱国者的旗舰智能播放器产品。本文将深度描述其使用体验。</description><pubDate>Mon, 09 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import F from &apos;@/components/mdx/formatting&apos;&lt;/p&gt;
&lt;p&gt;爱国者 M2 Pro 是爱国者的旗舰智能播放器产品，搭载 Android 系统，具有较强的可扩展性。&lt;/p&gt;
&lt;p&gt;想必外观方面，官方已经宣传得足够充分了，评论也提供了相当充足的信息。因此，本文将深度描述其软件方面的使用体验，并说明优化方法。在本文中，你将不会看到多少实物照片，但会看到大量屏幕截图。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;时效性复核状态&quot;&gt;&lt;/p&gt;
&lt;p&gt;被评测产品的购买日期是2021年12月。产品的具体情况可能随时间发生变化，请谨慎对待。&lt;br&gt;
大部分截图已于2022年6月9日更新。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h2&gt;产品概览&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://item.jd.com/100023613692.html&quot;&gt;参考链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;爱国者 M2 Pro 是爱国者的旗舰智能播放器产品，搭载 Android 系统，具有较强的可扩展性，且能够连接 WiFi，实现在线浏览与下载内容。&lt;/p&gt;
&lt;p&gt;作为特殊的播放器产品，爱国者 M2 Pro 主要适用于以下用户：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;偏好使用独立设备聆听音乐、阅读电子书的用户（这可以免除一些干扰，例如系统提示音打断音乐）。&lt;/li&gt;
&lt;li&gt;想要一个能折腾的 Android 设备的用户。&lt;/li&gt;
&lt;li&gt;不适合使用手机，而需要有一个体验较好的多功能播放器的群体，例如学生。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;产品主要参数见文末。&lt;/p&gt;
&lt;h2&gt;开始之前...&lt;/h2&gt;
&lt;p&gt;本文中会提到一些第三方应用和工具。如果你有兴趣试一试，可以在&lt;a href=&quot;https://wcl.sparkslab.art/index.php?share/folder&amp;#x26;user=4&amp;#x26;sid=Fhs5qSTu&quot;&gt;这里&lt;/a&gt;找到它们。&lt;/p&gt;
&lt;h2&gt;体验概述&lt;/h2&gt;
&lt;p&gt;虽说爱国者比较重视用户体验，但它并不擅长定制 Android 系统，因此，尽管这款播放器有许多值得肯定之处，但也和其他许多小品牌播放器一样，存在些许问题，总体的体验说不上好。&lt;/p&gt;
&lt;p&gt;然而，对于善于折腾的用户，进行一些优化和修改即可改进体验。经过充分的修改，体验可以变得非常好。&lt;/p&gt;
&lt;p&gt;下文的优化操作分为下面四个难度等级：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A级：通过在设备上安装特定应用程序完成操作，或借助电脑通过简单操作完成。&lt;/li&gt;
&lt;li&gt;B级：使用 USB 调试工具 ADB 进行操作。&lt;/li&gt;
&lt;li&gt;C级：需要通过某些方式获取 Root 权限在系统内执行操作（注意，如果你未掌握D级操作，这会非常危险，因为操作错误可能导致设备无法正常启动）。&lt;/li&gt;
&lt;li&gt;D级：会使用 smali 对 dex 程序反汇编，会使用 jadx 反编译得到伪代码，会使用 apktool 进行 APK 修改；会解包与重新打包系统镜像，掌握将修改后系统镜像刷入设备的方法。&lt;/li&gt;
&lt;li&gt;E级：在D级的基础上，由于工作量很大，你可能需要编写自动化程序进行操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请注意，如果你对你的设备进行优化或修改，我不对产生的不良后果负责。如果进行C级及以上的操作，强烈建议备份系统镜像，否则犯了错误将很难修复。&lt;/p&gt;
&lt;p&gt;获取 Root 权限、备份系统及刷入镜像的方法参考&lt;a href=&quot;/blog/play-android/mtk-flash-root&quot;&gt;此处&lt;/a&gt;。请注意此系统上 Magisk 与 Xposed 不兼容。&lt;/p&gt;
&lt;h2&gt;细节内容&lt;/h2&gt;
&lt;h3&gt;外观&lt;/h3&gt;
&lt;p&gt;我向来不是那么在意外观。相信你也在商品的评论区中看到了足够多的外观照片。&lt;/p&gt;
&lt;p&gt;此产品外观较为新颖，但厚度有足足 11mm，重量 160g，略显笨重，不适合运动时使用。&lt;/p&gt;
&lt;h3&gt;初次启动体验&lt;/h3&gt;
&lt;p&gt;长按电源键后，将看到可能有些刺眼的启动界面。几秒后，屏幕变黑。十几秒后，启动界面再次亮起。&lt;/p&gt;
&lt;p&gt;约 1 分钟后，进入锁屏界面。从锁屏界面上可以看出，这是类原生的 Android 5.1 系统。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/15f583ae95f2ccf6c1977f3fc2f345d0.B8CZTgyD_ZDSbIU.webp&quot; alt=&quot;开机界面&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/404530eb71eb452ba6a293911169bb4f.CTvigfJX_Z79LVT.webp&quot; alt=&quot;锁屏&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/fc56e55cacb3bbbae63aa8de920c553c.CF-exMfB_28nadr.webp&quot; alt=&quot;桌面&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;上滑进入桌面。桌面给人的第一感受是粗糙。普通的图标被硬生生地拉扯成了较大的尺寸，内部十分模糊；底部导航按钮歪歪扭扭，令强迫症血压飙升。长按桌面背景，会发现这个桌面甚至连个像样的设置都没有。小部件无法编辑，图标也无法组成文件夹。&lt;/p&gt;
&lt;p&gt;尝试安装第三方桌面，发现系统并不接受第三方桌面。&lt;/p&gt;
&lt;p&gt;重启一次，发现正常开机大约用时 32s，还是可以接受的。&lt;/p&gt;
&lt;p&gt;此系统可以正常使用多任务功能，多个应用来回切换非常方便。在多任务列表上关闭应用后，应用进程将被强行停止（已配置的设备管理器与通知监视器除外）。&lt;/p&gt;
&lt;p&gt;相关优化：&lt;a href=&quot;#opt-navbar&quot;&gt;修改扭曲的导航按钮&lt;/a&gt;  &lt;a href=&quot;#opt-logo&quot;&gt;修改开机界面&lt;/a&gt;  &lt;a href=&quot;#opt-launcher&quot;&gt;允许第三方桌面&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;界面与字体&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/d7b91f8542ab0e2833570d7d3e2f3ef6.DH6_14hv_Z2o8Qvu.webp&quot; alt=&quot;快速设置&quot; title=&quot;[maxh=400]&quot;&gt;&lt;/p&gt;
&lt;p&gt;系统界面使用的是原生 Android 5.1 的界面，不过就现在而言，也谈不上有什么创意了。&lt;/p&gt;
&lt;p&gt;此设备的默认界面尺寸太大，导致屏幕能显示的内容非常有限。通过适当调整，可以使得设备界面尺寸变得合理。（“细节内容”章节的所有截图均在未调整尺寸的情况下截取，“优化体验”章节所有截图均在已调整情况下截取）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/1d91047b668bb06aaf1f69822a7890dd.DT7Vpsty_Z1PX06v.webp&quot; alt=&quot;状态栏&quot; title=&quot;[maxh=30]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/053bb94b4097e2b187bfb37a3737019d.CVz-Tyt__2jWlhY.webp&quot; alt=&quot;字体不匀称&quot; title=&quot;[maxh=40]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/1682b4950ca8efab4259a8229986da4f.CWbdgE5M_LW8wM.webp&quot; alt=&quot;冒号不对齐&quot; title=&quot;[maxh=40]&quot;&gt;&lt;/p&gt;
&lt;p&gt;此外，值得批评的是状态栏和字体。&lt;/p&gt;
&lt;p&gt;原生系统中的包含多种字重的英文字体，而仅包含一套基本的中文字体，因此，中英文字体的观感有时不一致。另外，使用普通字体展示时间时，冒号位置偏下。&lt;/p&gt;
&lt;p&gt;状态栏所有图标集中在右上角。左上角本应该是通知图标，但是，这款播放器里并不存在通知图标，这就浪费了左端空间，且使右上角显得拥挤。&lt;/p&gt;
&lt;p&gt;相关优化：&lt;a href=&quot;#opt-density&quot;&gt;调节界面尺寸&lt;/a&gt;  &lt;a href=&quot;#opt-statusbar&quot;&gt;优化状态栏布局&lt;/a&gt;  &lt;a href=&quot;#opt-font&quot;&gt;修改字体&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;设置&lt;/h3&gt;
&lt;p&gt;这是 Android 系统，设置菜单应该非常有玩头吧... 对，但不完全对。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/9d77b9c2151b1baf371013b8d95e7326.D6KXRvaY_14rBTL.webp&quot; alt=&quot;设置菜单1&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/304be602779bbeeb031670a91ae27dc0.EPlybVrR_dXNXJ.webp&quot; alt=&quot;设置菜单2&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/a3202429f31c8f4359106455d55baa2c.D_pbu3kO_2mllfb.webp&quot; alt=&quot;其他设置&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;可以看到，制造商删除了设置内的不少内容，例如声音与通知、网络共享、无障碍和开发者选项。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/28b32e1a3db38f4770de79fc0e30457d.CmKPAosV_fvPad.webp&quot; alt=&quot;电池设置&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/512e9ce0442873c779adc1708b57d203.3GFk4EcQ_Z1ehTAk.webp&quot; alt=&quot;存储设置&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/7be9f61d038a06f3f5628006c7c1ff54.DaujudV-_ZnJsTw.webp&quot; alt=&quot;定时开关机&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;Android 中许多有意思的设置得到了保留，例如电池和存储统计。此外，还有此播放器的独有功能——定时开关机。
（此截图不代表实际续航情况。预估使用时间较短是因为前几分钟在进行初次启动）&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/5b1524b502b955ca1c47564ecce3fbc0.BnZxlhou_2i793C.webp&quot; alt=&quot;锁屏设置&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/1899486167721c5d7442c552448d8a75.D3aiB0cb_1chJsr.webp&quot; alt=&quot;系统更新&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/4757662379555f69dc51742ad9581195.SVDsnWic_Z1UvUsc.webp&quot; alt=&quot;恢复出厂设置&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;锁屏设置中并没有 PIN 码选项，目前不知道这是为什么。设置中还可以找到一个系统更新界面，然而，即便此系统存在不少应当已知的问题，官方几个月间仍然没有推送更新。&lt;/p&gt;
&lt;p&gt;恢复出厂设置被单独放置在“备份与重置”中。~~《 皇  帝  的  新  备  份 》~~&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/fd06f72e88bc423ce0857e0cffedf467.DlM1c8J2_Z1YXh2G.webp&quot; alt=&quot;设备状态&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/8925065f23bb2bea96531204ea152c71.CRl_gNFs_ZrOu9t.webp&quot; alt=&quot;输入密码&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/ac1ae37f4ac1b0d7dc2508601818919d.uav_VcAm_2mou82.webp&quot; alt=&quot;工厂模式&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;最有意思的当属设备状态界面。连续快速点击多次“Android 版本”，将会弹出棒棒糖，点击棒棒糖多次后再长按，就会打开一个小游戏，这是原生 Android 内置的彩蛋；多次点击“自定义版本”，将打开通往工厂测试模式和工程模式的入口；多次点击“内核版本”，输入密码“hys123”（别问密码哪来的），即可进入开发者选项。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/4329aca7ba3107eba0ca27446263bb9e.BsoqZCwP_2hFFVx.webp&quot; alt=&quot;渐变 EM 背景&quot; title=&quot;[maxh=400]&quot;&gt;&lt;/p&gt;
&lt;p&gt;~~开发者选项中打开“强制 GPU 渲染”，将解锁至尊渐变版工程模式皮肤。这应该是一个奇怪的渲染特性。~~&lt;/p&gt;
&lt;p&gt;大多数被删除的设置其实并非被删除，而是被隐藏了起来。通过“创建快捷方式”等活动选择器应用，即可将其打开。~~上面需要输入密码的开发者选项也可以直接用这种方法打开。~~&lt;/p&gt;
&lt;p&gt;安全设置里面有一个权限管理，但权限管理服务会因为权限管理页面从最近屏幕列表中划出而被强行停止，所以没什么用。&lt;/p&gt;
&lt;p&gt;相关优化：&lt;a href=&quot;#opt-settings&quot;&gt;使用山寨版设置主页&lt;/a&gt;  &lt;a href=&quot;#opt-permission&quot;&gt;修复权限管理模块&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;没有静音模式？&lt;/h3&gt;
&lt;p&gt;这款播放器默认开启了通知提示音、点按提示音和锁屏音效。某些情况下我们不希望有这些音效，但音量键只能调节媒体音量。其实，使用“创建快捷方式”等活动选择器应用，就可以进入声音设置界面，关闭这些提示音。&lt;/p&gt;
&lt;p&gt;~~我在评论区看到有个家伙为了关闭提示音直接就用 Kingroot 获取 Root 了，还上了 GravityBox 开启可下拉音量面板，这才把铃声音量关掉。杀鸡用牛刀了属于是。~~&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/145dff9d0738b4974b3d950e5fe70fa1.TaD0CdFL_14cTWE.webp&quot; alt=&quot;公开处刑&quot;&gt;&lt;/p&gt;
&lt;h3&gt;无线连接&lt;/h3&gt;
&lt;p&gt;这款播放器支持 WiFi 和蓝牙连接。WiFi 支持 2.4GHz 和 5GHz 双频段，蓝牙为 BT4.2。测试发现，WiFi 信号接收能力偏弱，但不影响日常使用。蓝牙对某些设备会出现不兼容情况。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/7d2da7bb655e4884a1212742f982ac84._04WuZLA_Za6SVY.webp&quot; alt=&quot;EM&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/1e29fb212108032f685d8b27b2498957.Ckke4c6E_2gKIrf.webp&quot; alt=&quot;EM WiFi NVRAM&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/6f7f6e80100bcc9197652a4a3e4c95e2.Dju_oK3n_Z2Hopx.webp&quot; alt=&quot;MAC地址已修改&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;值得一提的是，这个设备可以用工程模式（进入方式见上文）修改 WiFi MAC 地址，具体操作请自行百度。至于这有什么用... 那就看你自己的发挥了。&lt;/p&gt;
&lt;h3&gt;文件传输&lt;/h3&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow maxWidth=&apos;520px&apos;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/98838cd54c7dbc12c957363d93db9887.CupfeFkZ_fX7sr.webp&quot; alt=&quot;USB 计算机连接&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/ebafea80cd64fda7feeca2f0e5d961ae.BqQX9o06_1p5ig0.webp&quot; alt=&quot;UMS 模式&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;将设备通过 USB 线连接至电脑，可以像使用传统 MP3 时一样传输文件。设备会默认采用 MTP 媒体设备模式。该模式传输文件不影响播放器使用，但电脑对内部文件也没有完全访问权。&lt;/p&gt;
&lt;p&gt;若存在 TF 卡，则可以选用 UMS 大容量存储模式。启用该模式连接后，需要下拉通知栏，手动点击通知打开 USB 存储设备。此时，TF 卡会被卸载，并作为 U 盘连接到电脑上，如需批量或自动传输文件，这种模式会非常方便。传输完成后，需要手动点击通知关闭 USB 存储，否则 TF 卡将无法正常读取。若出现这种情况，请拔出并重新插入 TF 卡。&lt;/p&gt;
&lt;p&gt;传输速度上，无论使用内置存储还是 TF 卡，速度都能维持在 22MB/s 左右，充分利用了 USB 2.0 的传输能力。&lt;/p&gt;
&lt;h3&gt;音乐播放&lt;/h3&gt;
&lt;p&gt;此播放器自带了旧版本的 APlayer（即桌面上的“音乐”图标）。建议将其更新为新版。 &lt;a href=&quot;https://github.com/rRemix/APlayer&quot;&gt;【APlayer 官方仓库】&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow maxWidth=&apos;520px&apos;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/67ddc28ef9b4606819ad01ae1afe7b21.CvH9yDGG_2au8LN.webp&quot; alt=&quot;所有歌曲&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/78cb8ed9508ac1ff20ae9a321796ed74.DuyRnhit_gxT30.webp&quot; alt=&quot;播放列表&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;歌曲浏览方面，支持查看所有歌曲，或者像在相册中一样查看单个文件夹中歌曲。还支持自行创建播放列表。&lt;/p&gt;
&lt;p&gt;注：播放列表中 TF 卡内歌曲会在 TF 卡卸载时被移除。你需要通过导入导出播放列表的方法来备份它们。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/565bc8d00d7e6860f1b5523f436fb517.DM9JZSwe_ZQLUB8.webp&quot; alt=&quot;播放界面&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/5f5d10cec144e3caa49a25b24ed88094.Dubd3_tz_ZlOHqy.webp&quot; alt=&quot;定时关闭&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/fdf869d4dc5ee0455902da5afdeb89f1.C7sME-ef_1M1wF4.webp&quot; alt=&quot;均衡器&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;播放界面是常规的界面，可以显示内置封面和 LRC 格式的歌词。请不要反复点击左侧切换播放顺序的按钮，这会短时间内创建大量 Toast 而无法消除。&lt;/p&gt;
&lt;p&gt;APlayer 自带了定时关闭功能，该功能仅关闭 APlayer 本身而不关闭设备。定时结束后，还可以选择当前歌曲播放完毕再退出（该选项在调整界面尺寸前不可用，因为默认界面尺寸过大导致此选项无法显示）。&lt;/p&gt;
&lt;p&gt;APlayer 会使用系统内置的均衡器组件。均衡器组件支持最基本的调节。&lt;/p&gt;
&lt;p&gt;关于音质，该播放器支持无损输出，但总体音质水平一般。&lt;/p&gt;
&lt;p&gt;媒体音量的调节范围是 0~15，取值整数。这对于某些敏感的人可能不够精确。&lt;/p&gt;
&lt;p&gt;相关优化：&lt;a href=&quot;#opt-viperfx&quot;&gt;安装 ViPERFX 音效&lt;/a&gt;  &lt;a href=&quot;#opt-mediavolume&quot;&gt;提高媒体音量精度&lt;/a&gt;  &lt;a href=&quot;#opt-toast&quot;&gt;Toast 机制修改&lt;/a&gt;  &lt;a href=&quot;#opt-density&quot;&gt;调节界面尺寸&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;视频播放&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/3f7c9e61e24c6f61fe859502cf305890.Ce1G_rsc_ENOvO.webp&quot; alt=&quot;视频选择&quot; title=&quot;[maxh=320]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/6556a31a4405256817a9b45067aa21b7._8hHIUwL_Z1mUXXz.webp&quot; alt=&quot;视频播放&quot; title=&quot;[maxh=320]&quot;&gt;&lt;/p&gt;
&lt;p&gt;作为 Android 设备，显然没有必要太担心视频格式支持的问题。然而，系统内置的播放器比较鸡肋，并且再次点击屏幕也无法隐藏上面的叠加层，体验极差。&lt;/p&gt;
&lt;p&gt;推荐使用 MX Player 播放器来播放视频。安装 MX Player 后，在视频列表中点击视频，将会自动询问打开方式，此时选择 MX Player 并记住即可。&lt;/p&gt;
&lt;p&gt;如果觉得视频列表不好用，可以直接从文件管理器打开视频。&lt;/p&gt;
&lt;h3&gt;FM 收音机&lt;/h3&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow maxWidth=&apos;520px&apos;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/b880271c45d39ed90d95417d958280bd.zOvVhybe_gM9mS.webp&quot; alt=&quot;请插入耳机&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/3ba8f85202d3c1961bef330bc348adf2.COLcnpm4_1RYTVU.webp&quot; alt=&quot;收音机界面&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;FM 收音机使用的是 Google 系的收音机应用。&lt;/p&gt;
&lt;p&gt;与常规的 MP3 设备相同，这款播放器也需要插入耳机作为天线才能使用 FM 收音机。然而，与众不同的是，这里插入耳机并不代表用耳机播放。点击收音机界面右上角的耳机或扬声器图标，可以切换用于播放的设备。&lt;/p&gt;
&lt;p&gt;此应用支持后台播放与 FM 录音。&lt;/p&gt;
&lt;h3&gt;电子书阅读&lt;/h3&gt;
&lt;p&gt;系统内置电子书软件非常难看，且经常出现乱码等问题，已经不配用“鸡肋”形容了。这里就不作展示了。&lt;/p&gt;
&lt;p&gt;你可以卸载内置阅读器，使用静读天下专业版来代替它。你可以直接从文件管理器打开电子书。&lt;/p&gt;
&lt;h3&gt;文件管理&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/8154c9423a6f12be04a15c91141d462c.BfWRAq0p_Z1cT6j7.webp&quot; alt=&quot;文件管理&quot; title=&quot;[maxh=400]&quot;&gt;&lt;/p&gt;
&lt;p&gt;内置文件管理器只包含最基本的浏览和管理功能，比较无趣。&lt;/p&gt;
&lt;p&gt;取代内置文件管理器的选择有很多，例如 Root Explorer（其应用名称就叫“文件管理器”）和 Solid Explorer。这两款文件管理器还支持在已 Root 设备上管理系统文件和其他应用的数据文件。&lt;/p&gt;
&lt;p&gt;提示：5.1 系统上，Solid Explorer 应使用 2.7.17 版本。后续版本虽然也支持，但界面会抽风，很难使用。&lt;/p&gt;
&lt;h3&gt;词典&lt;/h3&gt;
&lt;p&gt;系统内置词典是有道词典，如果需要离线使用，要提前下载好离线词典包。&lt;/p&gt;
&lt;p&gt;另外，欧路词典在此设备上体验更好。&lt;/p&gt;
&lt;h3&gt;日历与时钟&lt;/h3&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/5e8635f5715104effd565325d213f062.BWKMxRzG_Z9Kh6q.webp&quot; alt=&quot;月视图&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/3925997e948758a22f21d6803902224f.B89OTBuL_FeaUE.webp&quot; alt=&quot;事件视图&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/a577c7864bce4d9e9bf11873c16e42c5.DF3BSY-B_Z20oFw5.webp&quot; alt=&quot;事件明细&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;日历具有常规的事件提醒功能，但不能显示农历日期和节日。&lt;/p&gt;
&lt;p&gt;注意：会议环境下使用，请记得关掉日历提示音。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/fc7e118b91b543926aeaacdd51be9621.Bcx2syIS_ztz5L.webp&quot; alt=&quot;闹钟&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/1faa791acaeeb772c44c7998fb62e185.BHlHF_LV_P0Sid.webp&quot; alt=&quot;闹钟选择&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/a69884da52d68f62928502b66cbc4f10.COqIQuHC_1I5yA4.webp&quot; alt=&quot;计时器&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;时钟采用了原生系统的时钟。这款时钟有着明显的效率至上特点，没有使用常见的密码滚轮，而是使用了钟面时间选择器，只须点击两三下就能设定闹钟时间，不需要反复滑动。计时器界面也只须输入时间，而不需要滑动设定。&lt;/p&gt;
&lt;p&gt;然而，此时钟应用存在问题，可能到时间不会响铃。建议使用 Google 系的新版时钟应用取代。又因此系统下从最近屏幕页面删除应用会将其强行停止，所以我修改出了不会在最近屏幕页面显示的时钟。你可以打开上文的资料链接下载它。&lt;/p&gt;
&lt;p&gt;注意：闹钟与计时器在耳机接入时会同时通过耳机和扬声器输出音频。&lt;/p&gt;
&lt;p&gt;相关优化：&lt;a href=&quot;#opt-clock-1&quot;&gt;使用第三方时钟&lt;/a&gt;  &lt;a href=&quot;#opt-clock-2&quot;&gt;禁用系统自带时钟&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;相册&lt;/h3&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/33b7bf5d3a0c4c08e91122392ec44423.Dv1zxjDE_Z3bzI0.webp&quot; alt=&quot;相册视图&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/f2efd9b03bd5a620bcc4d1487ec65ff7.tAblw4Vc_Z17rOdA.webp&quot; alt=&quot;照片编辑器-1&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/d91d133c7fc06835123a7456793d2d15.DQYhAhrs_ZVH3fU.webp&quot; alt=&quot;照片编辑器-2&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;相册分文件夹展示了内置存储和 TF 卡中的所有图片（被 &lt;code&gt;.nomedia&lt;/code&gt; 排除的除外）。图片查看能够加载完整分辨率的图片，支持双指缩放和拖动，可以浏览大图。&lt;/p&gt;
&lt;p&gt;相册还内置了一个比较强大的图像编辑器。&lt;/p&gt;
&lt;p&gt;注意：图像编辑器保存会覆盖原图。若希望另存为，请点击右上角选择“导出”，然后不加保存地关闭编辑器。&lt;/p&gt;
&lt;h3&gt;第三方应用&lt;/h3&gt;
&lt;p&gt;使用某些较大的第三方应用（如哔哩哔哩）时，卡顿明显。较为现代的应用动画效果容易出现长时间卡顿问题。&lt;/p&gt;
&lt;p&gt;运行部分小游戏，发热和续航会比较成问题。&lt;/p&gt;
&lt;p&gt;相关优化：&lt;a href=&quot;#opt-performance&quot;&gt;应用性能优化&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;外放、屏幕与续航&lt;/h3&gt;
&lt;p&gt;此播放器支持外放，其扬声器最大音量一般，与传统的 MP3 类似。至于音质... 能指望吗？&lt;/p&gt;
&lt;p&gt;屏幕刷新率为 59Hz，但由于响应时间小于对应的 16ms，界面动画会显得比较卡顿。&lt;/p&gt;
&lt;p&gt;屏幕最大亮度非常有限，仅适合在室内使用，最小亮度较高，亦不适合夜间使用。然而，观察不难发现，开机界面的亮度要远大于系统内可调的最大亮度，系统并没有开放所有亮度范围。~~这或许是厂商故意挤牙膏，准备下一代产品来个“高亮度”罢。~~&lt;/p&gt;
&lt;p&gt;夜间使用，不妨使用薄暮微光等应用降低亮度与对比度。&lt;/p&gt;
&lt;p&gt;关于续航，此设备待机时几乎不耗电，因此日常完全可以待机使用。连续播放音乐（屏幕不开启，音量 7）可以播放 13 小时，视频 3 小时，还算是合格的。电池充电 1.2 小时即可充满。&lt;/p&gt;
&lt;p&gt;全新的设备电量计并不准确，显示耗电会明显快于真实情况（这会导致 1% 时还可以用数小时）。经过数次完全充放电后，电量计才能调整到最佳状态。&lt;/p&gt;
&lt;p&gt;相关优化：&lt;a href=&quot;#opt-brightness&quot;&gt;扩大亮度范围&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;系统稳定性&lt;/h3&gt;
&lt;p&gt;使用过程中发现，此系统的稳定性是存在问题的。&lt;/p&gt;
&lt;p&gt;有时熄灭屏幕后，系统会卡死，无法再次按压电源按钮点亮屏幕。后台音乐会继续播放 2 分钟，然后系统自动软重启点亮屏幕。虽然发生概率不高，但是这种问题是非常烦人的，因为一旦出现意味着要等待 2 分钟。&lt;/p&gt;
&lt;p&gt;出现此问题时，可以用细小物体按压侧面的麦克风孔，使设备强制关机。&lt;/p&gt;
&lt;p&gt;关闭锁屏功能，可以大幅降低出现此问题的频率。手动触发方法：① 开启锁屏的情况下，开机完成后立即快速按压电源按钮多次；② 在最近屏幕列表中的应用被划出的瞬间按压电源按钮锁屏。&lt;/p&gt;
&lt;p&gt;此外，我购买到的设备 Webview 支持并不正常，依赖内建浏览器的应用会爆炸。&lt;/p&gt;
&lt;p&gt;相关优化：&lt;a href=&quot;#opt-stability&quot;&gt;修复系统稳定性问题&lt;/a&gt;  &lt;a href=&quot;#opt-webview&quot;&gt;修复 Webview 支持&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;优化体验&lt;/h2&gt;
&lt;p&gt;本节中的系统修改部分仅给出技术分析和修改点，如需实施，请自行学习相关操作。&lt;/p&gt;
&lt;p&gt;再次提醒，下文会提到一些第三方应用和工具。如有兴趣尝试，可以在&lt;a href=&quot;https://wcl.sparkslab.art/index.php?share/folder&amp;#x26;user=4&amp;#x26;sid=Fhs5qSTu&quot;&gt;这里&lt;/a&gt;找到它们。&lt;/p&gt;
&lt;p&gt;对于 B 级以上操作，你可能需要这些基础知识：&lt;a href=&quot;/blog/play-android/preintro-adb&quot;&gt;[基础预科] ADB、Android 终端、Android 用户权限&lt;/a&gt;  &lt;a href=&quot;/blog/play-android/preintro-bootmode&quot;&gt;[基础预科] Android 分区、启动模式、Fastboot&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;进行 C 级以上优化操作后，若有备份，可以删除系统中的无线升级应用，因为非原厂的系统镜像本身无法接受原厂系统更新。&lt;/p&gt;
&lt;p&gt;等级：A&lt;/p&gt;
&lt;p&gt;概述：活动选择器可用于打开隐藏的设置页面。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/6f7543868e5ed58e43e9599b1459f237.CgJu_Vrz_1ft2pV.webp&quot; alt=&quot;查找应用&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/744d4c7220c42532f2f8bd2a3b8c24a7.BOWhiTod_Z1Byv04.webp&quot; alt=&quot;查找活动&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/1a412e8c1c503feff2edaad5c64bc348.6frL6dFV_Z1zd1h6.webp&quot; alt=&quot;打开活动&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;安装“创建快捷方式”等活动选择器应用即可打开隐藏的设置项。&lt;/p&gt;
&lt;p&gt;注：原装的桌面不支持创建活动图标。&lt;/p&gt;
&lt;p&gt;等级：A&lt;/p&gt;
&lt;p&gt;概述：有一个为此播放器特制的设置主页，包含一些隐藏选项的入口。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/3b7dd8109812f95d4e86876d5ff5aefb.8deb0xEi_ZtWCib.webp&quot; alt=&quot;设置-1&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/ac1647bcd4d810f5d080e6a16aa32d91.JFPFyW7e_Z1o5dGi.webp&quot; alt=&quot;设置-2&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/f3bb1ae8b72dfc8313e29e3b8a13faf6.B-HoI80e_DrFxr.webp&quot; alt=&quot;设置-3&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;为方便操作系统设置，我为其定制了一个特殊的设置主页。该设置主页将大多数有用的设置都放了回来，移除了“备份和重置”，并恢复了正常的设置分组。&lt;/p&gt;
&lt;p&gt;这里没有放置权限管理、工程模式和开发者选项的入口，因为它们在其他设置页面里已经有了。&lt;/p&gt;
&lt;p&gt;警告：请不要禁用整个原装设置应用，否则该设置主页将无法工作。若要消除原装设置的图标，请禁用其主要活动。&lt;/p&gt;
&lt;p&gt;等级：A&lt;/p&gt;
&lt;p&gt;概述：第三方时钟可解决闹钟不响的问题。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/c27a78ff833d354c2888db904c7e2333.BcfjGb3Y_Z29nALb.webp&quot; alt=&quot;Google 闹钟&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/5b8c3a339d9d05a85078591e647468bc.BUe2AGy5_Z7zCkG.webp&quot; alt=&quot;Google 世界时钟&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/d3692dc153a6776137a249364c22dc96.CLhiZOLr_Z1nTba3.webp&quot; alt=&quot;Google 计时器&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;使用第三方时钟代替内置时钟，可以解决闹钟不响的问题。&lt;/p&gt;
&lt;p&gt;由于从最近屏幕页面划出应用会强行停止，故我特制了最近屏幕页面上不会显示的 Google 系时钟版本，在上文的链接中可以找到。&lt;/p&gt;
&lt;p&gt;等级：B&lt;/p&gt;
&lt;p&gt;概述：使用 &lt;code&gt;wm density&lt;/code&gt; 命令，可调节 DPI，从而更改界面尺寸。这和更改字体大小不一样。&lt;/p&gt;
&lt;p&gt;将设备通过 ADB 连接至电脑，运行 &lt;code&gt;adb shell&lt;/code&gt; 打开具有 Shell 权限的终端，或在设备上的终端模拟器中运行 &lt;code&gt;su&lt;/code&gt;（需要已 Root 设备），打开具有 Root 权限的终端。&lt;/p&gt;
&lt;p&gt;在终端中执行 &lt;code&gt;wm density 216&lt;/code&gt;，即可调小界面尺寸。调节后，推荐在显示设置中将字体大小改为“大”。&lt;/p&gt;
&lt;p&gt;执行 &lt;code&gt;wm density reset&lt;/code&gt; 恢复。&lt;/p&gt;
&lt;p&gt;调节完成后，重启设备使系统界面响应更改。&lt;/p&gt;
&lt;p&gt;警告：请选择 72~360 之间的值，设置值过大会导致设备无法使用。若不慎打错请立即执行恢复命令。另外，不建议尝试 &lt;code&gt;wm size&lt;/code&gt; 命令。&lt;/p&gt;
&lt;p&gt;等级：B&lt;/p&gt;
&lt;p&gt;概述：使用 &lt;code&gt;pm hide&lt;/code&gt; 命令，可以禁用应用。&lt;/p&gt;
&lt;p&gt;将设备通过 ADB 连接至电脑，运行 &lt;code&gt;adb shell&lt;/code&gt; 打开终端，或在设备上的终端模拟器中运行 &lt;code&gt;su&lt;/code&gt;（需要已 Root 设备），打开具有 Root 权限的终端。&lt;/p&gt;
&lt;p&gt;在终端中执行 &lt;code&gt;pm hide com.android.deskclock&lt;/code&gt; 禁用内置时钟。&lt;/p&gt;
&lt;p&gt;执行 &lt;code&gt;pm unhide com.android.deskclock&lt;/code&gt; 恢复。&lt;/p&gt;
&lt;p&gt;等级：C&lt;/p&gt;
&lt;p&gt;概述：Root 下的无障碍-Daemon软件可以以保活无障碍服务。&lt;/p&gt;
&lt;p&gt;应用被强行停止，无障碍服务会失效。&lt;/p&gt;
&lt;p&gt;对于已 Root 设备，使用无障碍-Daemon应用可以自动重新启用被关闭的无障碍服务。&lt;/p&gt;
&lt;p&gt;等级：C&lt;/p&gt;
&lt;p&gt;概述：系统的字体信息存储于 &lt;code&gt;/system/fonts/&lt;/code&gt; 和 &lt;code&gt;/system/etc/fonts.xml&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;将所需字体文件合并到 &lt;code&gt;/system/fonts/&lt;/code&gt; 文件夹中，然后用修改过的 &lt;code&gt;fonts.xml&lt;/code&gt; 替换 &lt;code&gt;/system/etc/fonts.xml&lt;/code&gt; 文件。替换后重启。&lt;/p&gt;
&lt;p&gt;你可以多找几份 Android 5.1 的 &lt;code&gt;fonts.xml&lt;/code&gt; 来探索如何修改。&lt;/p&gt;
&lt;p&gt;如果你像我一样想要应用 HarmonyOS 字体，那么，上文资料链接中有现成的文件，自取即可。&lt;/p&gt;
&lt;p&gt;由于屏幕分辨率较小，不建议应用奇奇怪怪的手写字体，会影响观感。&lt;/p&gt;
&lt;p&gt;警告：&lt;code&gt;fonts.xml&lt;/code&gt; 格式有误可能导致系统无法启动，此时需要刷入备份镜像来恢复。&lt;/p&gt;
&lt;p&gt;等级：C/D&lt;/p&gt;
&lt;p&gt;概述：Busybox 是 Linux 命令行工具中的瑞士军刀。若使用 Root 应用或 Magisk 模块遇到 &lt;code&gt;sed&lt;/code&gt; 等命令找不到的问题，你需要安装 Busybox。&lt;/p&gt;
&lt;p&gt;首先复制 Busybox 文件。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;点击上文中的资源链接下载 &lt;code&gt;busyboxExtractor.zip&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;busyboxExtractor.zip&lt;/code&gt; 解压到空文件夹下。&lt;/li&gt;
&lt;li&gt;运行 &lt;code&gt;_extract.bat&lt;/code&gt;，等待文件复制完毕。&lt;/li&gt;
&lt;li&gt;删除 &lt;code&gt;_extract.bat&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然后需要将其添加到系统中。一种方法是利用 Root 权限操作。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将上述文件夹传输到设备上。&lt;/li&gt;
&lt;li&gt;用文件管理器将所有文件复制到 &lt;code&gt;/system/bin/&lt;/code&gt; 中，&lt;strong&gt;已存在的文件跳过&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;或者修改系统镜像，这样可以安装到 xbin 文件夹中。这是更规范且更靠谱的方法。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;利用镜像编辑工具将上述文件夹中的所有文件添加到 &lt;code&gt;system.img&lt;/code&gt; 的 &lt;code&gt;xbin&lt;/code&gt; 文件夹下。&lt;/li&gt;
&lt;li&gt;重新打包 &lt;code&gt;system.img&lt;/code&gt; 镜像并刷入设备。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;等级：C
前置要求：&lt;a href=&quot;#opt-busybox&quot;&gt;安装 Busybox&lt;/a&gt;(等级：C/D)&lt;/p&gt;
&lt;p&gt;概述：ViPERFX 是一款需要手动调节（且高度可定制化）的音效软件，包含大量均衡器、音效组件，经恰当调节可以使音质体验大大提升。&lt;/p&gt;
&lt;p&gt;请点击上文资料链接下载 ViPERFX 2.5.0.5 的安装包，并安装至设备。&lt;/p&gt;
&lt;p&gt;打开应用，授予 Root 权限，选择安装驱动，等待完成后重启即可使用。&lt;/p&gt;
&lt;p&gt;提示：安装驱动后可以取消 Root 授权。使用前请在音乐软件中关掉系统自带的均衡器，然后可以将 ViPERFX 设为默认均衡器应用。若系统均衡器不关闭，ViPERFX 将无法正常工作。声音设置中的 AudEnh 和 BesLoudness 音效优化仍能使用，但是没有必要继续使用。&lt;/p&gt;
&lt;p&gt;等级：C&lt;/p&gt;
&lt;p&gt;概述：原本媒体音量调节范围是 0~15 的整数，这对某些敏感的人可能不太够用。通过修改系统配置，可以提高其精度。这并不会改变实际的最大音量。你所要做的，是将系统属性 &lt;code&gt;ro.config.media_vol_steps&lt;/code&gt; 设为你想要的范围值。这有多种实现方法。&lt;/p&gt;
&lt;p&gt;如果能修改系统文件，则可使用修改系统属性文件方法（注意如果改了不该改的东西可能需要刷机恢复）。这会永久生效。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用文本编辑器（多数文件管理器自带）打开 &lt;code&gt;/system/build.prop&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在任意位置添加一行 &lt;code&gt;ro.config.media_vol_steps=31&lt;/code&gt;，将 &lt;code&gt;31&lt;/code&gt; 改为你想要的音量范围值。&lt;/li&gt;
&lt;li&gt;重启设备。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你使用的 Root 方案是 Magisk，可以用 Magisk 内置工具临时修改。重启后失效。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将设备通过 ADB 连接至电脑，运行 &lt;code&gt;adb shell&lt;/code&gt; 打开终端，或打开设备上的终端模拟器。&lt;/li&gt;
&lt;li&gt;运行 &lt;code&gt;su&lt;/code&gt;（需要已 Root 设备），打开具有 Root 权限的终端。&lt;/li&gt;
&lt;li&gt;运行命令 &lt;code&gt;resetprop ro.config.media_vol_steps 31&lt;/code&gt; 以强制更改系统属性，将 &lt;code&gt;31&lt;/code&gt; 改为你想要的音量范围值。&lt;/li&gt;
&lt;li&gt;运行命令 &lt;code&gt;stop;start&lt;/code&gt; 软重启设备（注意 &lt;code&gt;stop&lt;/code&gt; &lt;code&gt;start&lt;/code&gt; 不能分开运行，否则你很可能运行不了 &lt;code&gt;start&lt;/code&gt;，设备会卡死）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你使用的 Root 方案是 Magisk，还可以使用 MagiskHide Props Config 模块永久修改属性，此处不描述方法。&lt;/p&gt;
&lt;p&gt;等级：C&lt;/p&gt;
&lt;p&gt;&lt;em&gt;适用于有外置存储卡且内部存储空间不紧张的用户。这会使应用运行更快更省电，但也会使应用占用更多存储空间，并使应用安装消耗更长时间。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;概述：Android 4 之前，应用中的代码在运行时被直接解释。为了加快运行速度，Android 5~6 会在应用安装或首次开机时对应用内部分代码进行预编译处理并存下来（这种方式称为提前编译）。这是一种空间换时间的措施。此设备上，预编译的程度被设置得很低（这或许是为了节省存储空间），导致应用运行时仍须读取大量原始代码，故性能差。调高预编译级别，即可解决这一问题。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将设备中的 &lt;code&gt;/system/bin/installd&lt;/code&gt; 传输到电脑。&lt;/li&gt;
&lt;li&gt;使用十六进制编辑器打开。文件不大，不必要动用专业编辑器。&lt;/li&gt;
&lt;li&gt;查找 &lt;code&gt;interpret-only&lt;/code&gt; 字符串，将对应位置替换为 &lt;code&gt;everything&lt;/code&gt;、&lt;code&gt;speed&lt;/code&gt; 或 &lt;code&gt;balanced&lt;/code&gt;（运行性能、额外存储占用及应用安装时间递减），多余字节用 &lt;code&gt;00&lt;/code&gt; 补齐。&lt;/li&gt;
&lt;li&gt;将修改后的文件传输至设备，覆盖原有文件。&lt;/li&gt;
&lt;li&gt;立即清空 &lt;code&gt;/data/dalvik-cache/&lt;/code&gt; 文件夹。&lt;/li&gt;
&lt;li&gt;强制重启设备。此次重启与首次启动一样需要较长时间，启动后还需要优化你已经安装的应用，请耐心等待。单个应用优化时间 10 分钟以内，均为正常现象。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;等级：C&lt;/p&gt;
&lt;p&gt;概述：设备的 Webview 支持存在问题，可以通过替换 Webview 组件来修复。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;删除设备上的 &lt;code&gt;/system/app/webview/&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;下载 &lt;code&gt;Android System Webview.zip&lt;/code&gt;，解压，将其中的 &lt;code&gt;webview&lt;/code&gt; 文件夹放置到 &lt;code&gt;/system/app/&lt;/code&gt; 下。&lt;/li&gt;
&lt;li&gt;重启设备体验。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;等级：E&lt;/p&gt;
&lt;p&gt;概述：为了节省编译时间，系统制作时，会将 &lt;code&gt;dex&lt;/code&gt; 可执行文件预编译为针对设备的 &lt;code&gt;odex&lt;/code&gt; 文件，然后，为了节省系统分区空间，会删掉原来的 &lt;code&gt;dex&lt;/code&gt;。然而，这对修改造成了障碍。要用一般方式对系统核心逻辑进行修改，就要将 &lt;code&gt;odex&lt;/code&gt; 还原为 &lt;code&gt;dex&lt;/code&gt;。这就是反 ODEX 化。&lt;/p&gt;
&lt;p&gt;系统核心程序反 ODEX 化后，系统应用必须也反 ODEX 化，否则将无法正常加载。因此，下面步骤将引导你一同解决这两个问题。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;拆解 &lt;code&gt;system.img&lt;/code&gt; 镜像，放入名为 &lt;code&gt;system&lt;/code&gt; 的文件夹中。&lt;/li&gt;
&lt;li&gt;将 oat2dex 和 LollipopBatchDeodexer 程序放置于 &lt;code&gt;system&lt;/code&gt; 同级位置。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;system&lt;/code&gt; 所在文件夹中打开终端。&lt;/li&gt;
&lt;li&gt;运行 &lt;code&gt;oat2dex devfw system/framework&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;上述命令将生成 &lt;code&gt;boot-jar-with-dex&lt;/code&gt; &lt;code&gt;framework-jar-with-dex&lt;/code&gt; 文件夹。将它们的&lt;strong&gt;内容&lt;/strong&gt;合并至 &lt;code&gt;framework-jar-with-dex&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;plugin/&lt;/code&gt; 中除 &lt;code&gt;Signature&lt;/code&gt; 外的所有文件夹移动到 &lt;code&gt;priv-app/&lt;/code&gt; 下。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;vendor/operator/app/&lt;/code&gt; 下的所有&lt;strong&gt;文件夹&lt;/strong&gt;移动到 &lt;code&gt;app/&lt;/code&gt; 下。&lt;/li&gt;
&lt;li&gt;配置好 Java 8 环境，打开 LollipopBatchDeodexer，选择你的 &lt;code&gt;system&lt;/code&gt; 文件夹开始工作。&lt;/li&gt;
&lt;li&gt;完成后，将 6、7 步骤中移动的文件夹移回原来位置。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;framework-jar-with-dex&lt;/code&gt; 中的文件覆盖至 &lt;code&gt;system&lt;/code&gt; 下的 &lt;code&gt;framework/&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;删除 &lt;code&gt;framework/arm/&lt;/code&gt; 文件夹。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来，如果你知道如何正确打包系统镜像，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将 &lt;code&gt;system&lt;/code&gt; 文件夹直接打包为系统镜像 &lt;code&gt;system.img&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;system.img&lt;/code&gt; 刷入你的设备。不需要恢复出厂设置。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你使用的是图形化镜像编辑器，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用编辑器打开原来的 &lt;code&gt;system.img&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;删除镜像中的 &lt;code&gt;framework/arm/&lt;/code&gt; 文件夹和所有 &lt;code&gt;.jar&lt;/code&gt; 文件。&lt;/li&gt;
&lt;li&gt;将上文 &lt;code&gt;framework-jar-with-dex&lt;/code&gt; 中的所有文件添加至镜像中的 &lt;code&gt;framework/&lt;/code&gt; 下。&lt;/li&gt;
&lt;li&gt;删除镜像中 &lt;code&gt;plugin/&lt;/code&gt; &lt;code&gt;app/&lt;/code&gt; &lt;code&gt;priv-app/&lt;/code&gt; &lt;code&gt;vendor/operator/app&lt;/code&gt; 下的所有&lt;strong&gt;文件夹&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;将上述已处理过的 &lt;code&gt;plugin/&lt;/code&gt; &lt;code&gt;app/&lt;/code&gt; &lt;code&gt;priv-app/&lt;/code&gt; &lt;code&gt;vendor/operator/app&lt;/code&gt; 下的所有&lt;strong&gt;文件夹&lt;/strong&gt;逐一添加至对应位置。&lt;/li&gt;
&lt;li&gt;打包镜像。&lt;/li&gt;
&lt;li&gt;将新的镜像 &lt;code&gt;system.img&lt;/code&gt; 刷入你的设备。不需要恢复出厂设置。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;刷入处理过的镜像后，首次开机系统需要在缓存中重建 ODEX 文件，这大概需要 3 分钟。&lt;/p&gt;
&lt;p&gt;等级：D
前置要求：&lt;a href=&quot;#opt-deodex&quot;&gt;系统反 ODEX 化&lt;/a&gt;(等级：E)&lt;/p&gt;
&lt;p&gt;概述：签名验证破解可以允许安装或加载伪造签名的应用，并且会移除安装/更新应用时的签名限制。这会一定程度上造成安全隐患。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/eb9c18479037f74b375e5856909042a2.1FsSh0ia_Z2hrup2.webp&quot; alt=&quot;核心破解&quot; title=&quot;[maxh=400]&quot;&gt;&lt;/p&gt;
&lt;p&gt;此处“签名验证破解”指的是 Lucky*Patcher 应用提供的“Android 核心破解”。无论先前是否应用过破解，反 ODEX 化后都要重新进行破解，且无法以常规步骤完成破解。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选择“禁用 ZIP 签名验证”和“签名验证始终真实”，不选择“仅破解到虚拟机缓存”，应用破解。&lt;/li&gt;
&lt;li&gt;破解完成后，由于 &lt;code&gt;core-junit.jar&lt;/code&gt; 被更改，系统新进程将会崩溃，文件系统进入半损坏状态。&lt;/li&gt;
&lt;li&gt;重启，然后通过文件管理器和文件传输将 &lt;code&gt;/system/framework/core-junit.jar&lt;/code&gt; 传输到电脑。&lt;/li&gt;
&lt;li&gt;手动将其合并至系统镜像内（注意不要拿错系统镜像，要用反 ODEX 化后的那个）。&lt;/li&gt;
&lt;li&gt;将合并后的系统镜像刷入设备。&lt;/li&gt;
&lt;li&gt;启动设备，选择“破解应用管理器”，不选择“仅破解到虚拟机缓存”，应用破解。&lt;/li&gt;
&lt;li&gt;完成后重启即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;若将步骤 3~5 改为仅进行一次重启，则系统分区半损坏，步骤 6 时由于无法写入，将不能破解成功。&lt;/p&gt;
&lt;p&gt;等级：D&lt;/p&gt;
&lt;p&gt;概述：开机界面存在于 &lt;code&gt;logo.bin&lt;/code&gt; 和系统中的 &lt;code&gt;bootanimation.zip&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/58dc7402a79e48a06270e35c4b3337ee.CV1nXQjJ_Z1hrpq2.webp&quot; alt=&quot;LogoBuilder&quot; title=&quot;[maxh=290]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/4cc60d4ffb5d64ef3e1aa64f97239778.g2sFlXMS_2ikCfi.webp&quot; alt=&quot;Create Bootanimation&quot; title=&quot;[maxh=290]&quot;&gt;&lt;/p&gt;
&lt;p&gt;开机界面分为两部分。第一部分是固件 logo，存在于 &lt;code&gt;logo.bin&lt;/code&gt; 中。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从设备提取 &lt;code&gt;logo.bin&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;使用 LogoBuilder 解析 &lt;code&gt;logo.bin&lt;/code&gt;，选择合适的分辨率格式。&lt;/li&gt;
&lt;li&gt;解析后打开工作文件夹，将相应图像替换为相同分辨率的 PNG 图像。&lt;/li&gt;
&lt;li&gt;点击“开始生成”。&lt;/li&gt;
&lt;li&gt;从生成的 &lt;code&gt;update.zip&lt;/code&gt; 中解压出 &lt;code&gt;logo.bin&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;logo.bin&lt;/code&gt; 刷入设备。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第二部分是系统开机动画，在 &lt;code&gt;system.img&lt;/code&gt; 中。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解包当前的 &lt;code&gt;system.img&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;解压 &lt;code&gt;media/bootanimation.zip&lt;/code&gt;，替换相应图像和文件。&lt;/li&gt;
&lt;li&gt;重新打包 &lt;code&gt;bootanimation.zip&lt;/code&gt;，&lt;strong&gt;使用“仅存储”压缩等级&lt;/strong&gt;，然后确保其与原文件结构一致。&lt;/li&gt;
&lt;li&gt;将新的 &lt;code&gt;bootanimation.zip&lt;/code&gt; 合并至 &lt;code&gt;system.img&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;system.img&lt;/code&gt; 刷入设备。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;等级：D
替代操作：&lt;a href=&quot;#opt-systemui&quot;&gt;替换系统界面 APK&lt;/a&gt;(等级：C)&lt;/p&gt;
&lt;p&gt;概述：&lt;code&gt;SystemUI.apk&lt;/code&gt; 中可以找到这三个扭曲的图标，替换即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/b6aa34d14931a2b7157ef766bcad27f9.BvL8_1QL_ZgldF8.webp&quot; alt=&quot;三种不同导航按钮&quot; title=&quot;[maxh=120]&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解包当前的 &lt;code&gt;system.img&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;用普通压缩软件取出 &lt;code&gt;priv-app/SystemUI/SystemUI.apk&lt;/code&gt; 中的 &lt;code&gt;res/drawable/ic_sysbar_*_custom.png&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;替换这些文件。如果你不会制作图标，上文资源链接中有现成的。图像分辨率任意，但为保证按钮尺寸正常，按钮本体应当位于中心 24x24 区域内。&lt;/li&gt;
&lt;li&gt;将替换后的文件用压缩软件合并回 &lt;code&gt;SystemUI.apk&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;将修改后的 &lt;code&gt;SystemUI.apk&lt;/code&gt; 合并至 &lt;code&gt;system.img&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;system.img&lt;/code&gt; 刷入设备。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;等级：D
替代操作：&lt;a href=&quot;#opt-framework&quot;&gt;替换系统资源 APK&lt;/a&gt;(等级：C)&lt;/p&gt;
&lt;p&gt;概述：&lt;code&gt;framework-res.apk&lt;/code&gt; 是系统核心资源包，其中包含亮度的上限与下限信息。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow maxWidth=&apos;520px&apos;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/9995f7c96530a159ec0bbdd931d5984b.UeL2aGgY_ZE11D9.webp&quot; alt=&quot;亮度上限增加&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/1b478b9f9608bc95f4654f99c468f421.BWipFb85_Z2lEwOa.webp&quot; alt=&quot;享受清晰画面&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解包当前的 &lt;code&gt;system.img&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;确保 Apktool 已将你的 &lt;code&gt;framework/framework-res.apk&lt;/code&gt; 作为 Framework 安装。&lt;/li&gt;
&lt;li&gt;用 Apktool 解包 &lt;code&gt;framework/framework-res.apk&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;找到 &lt;code&gt;res/values/integers.xml&lt;/code&gt;，替换亮度值代码。&lt;/li&gt;
&lt;li&gt;重新打包，得到新的 &lt;code&gt;framework-res.apk&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将新的 APK 用压缩软件打开，将其中的 &lt;code&gt;resources.arsc&lt;/code&gt; 直接覆盖至旧的 APK 中。&lt;/li&gt;
&lt;li&gt;将旧的 APK 合并回 &lt;code&gt;system.img&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;system.img&lt;/code&gt; 刷入设备。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;替换代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;    &amp;#x3C;integer name=&quot;config_screenBrightnessSettingMinimum&quot;&gt;10&amp;#x3C;/integer&gt;
    &amp;#x3C;integer name=&quot;config_screenBrightnessSettingMaximum&quot;&gt;102&amp;#x3C;/integer&gt;
    &amp;#x3C;integer name=&quot;config_screenBrightnessSettingDefault&quot;&gt;102&amp;#x3C;/integer&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;    &amp;#x3C;integer name=&quot;config_screenBrightnessSettingMinimum&quot;&gt;1&amp;#x3C;/integer&gt;
    &amp;#x3C;integer name=&quot;config_screenBrightnessSettingMaximum&quot;&gt;255&amp;#x3C;/integer&gt;
    &amp;#x3C;integer name=&quot;config_screenBrightnessSettingDefault&quot;&gt;102&amp;#x3C;/integer&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;提示：若修改亮度最大值超过 255，则亮度调到 255 以上时，系统将维持默认亮度 102，忽略用户设置。&lt;/p&gt;
&lt;p&gt;等级：D
替代操作：&lt;a href=&quot;#opt-permapk&quot;&gt;替换权限管理 APK&lt;/a&gt;(等级：C)&lt;/p&gt;
&lt;p&gt;概述：将权限管理界面从最近屏幕中排除，即可防止其被强行关闭。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解包当前的 &lt;code&gt;system.img&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;确保 Apktool 已将你的 &lt;code&gt;framework/framework-res.apk&lt;/code&gt; 作为 Framework 安装。&lt;/li&gt;
&lt;li&gt;用 Apktool 解包 &lt;code&gt;plugin/PermissionControl/PermissionControl.apk&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;找到 &lt;code&gt;AndroidManifest.xml&lt;/code&gt;，替换代码。&lt;/li&gt;
&lt;li&gt;重新打包，得到新的 &lt;code&gt;PermissionControl.apk&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将新的 APK 用压缩软件打开，将其中的 &lt;code&gt;AndroidManifest.xml&lt;/code&gt; 直接覆盖至旧的 APK 中。&lt;/li&gt;
&lt;li&gt;将旧的 APK 合并回 &lt;code&gt;system.img&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;system.img&lt;/code&gt; 刷入设备。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;替换代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;activity android:configChanges=&quot;keyboardHidden|mcc|mnc|orientation|screenSize&quot; android:label=&quot;@string/manage_permission_app_label&quot; android:launchMode=&quot;singleTask&quot; android:name=&quot;.ui.PermissionControlPageActivity&quot;&gt;

&amp;#x3C;activity android:label=&quot;@string/auto_boot_pref_title&quot; android:launchMode=&quot;singleTask&quot; android:name=&quot;.ui.AutoBootAppManageActivity&quot;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;activity android:configChanges=&quot;keyboardHidden|mcc|mnc|orientation|screenSize&quot; android:excludeFromRecents=&quot;true&quot; android:label=&quot;@string/manage_permission_app_label&quot; android:launchMode=&quot;singleTask&quot; android:name=&quot;.ui.PermissionControlPageActivity&quot;&gt;

&amp;#x3C;activity android:excludeFromRecents=&quot;true&quot; android:label=&quot;@string/auto_boot_pref_title&quot; android:launchMode=&quot;singleTask&quot; android:name=&quot;.ui.AutoBootAppManageActivity&quot;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等级：D
替代操作：&lt;a href=&quot;#opt-systemui&quot;&gt;替换系统界面 APK&lt;/a&gt;(等级：C)&lt;/p&gt;
&lt;p&gt;概述：修改 &lt;code&gt;SystemUI.apk&lt;/code&gt; 中的布局文件，可以将时间改到左上角。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/f56bd53b91c8d9e300a6caeae0281cf3.Dsx9QN2Z_Z1933ut.webp&quot; alt=&quot;优化后的状态栏&quot; title=&quot;[maxh=30]&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解包当前的 &lt;code&gt;system.img&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;确保 Apktool 已将你的 &lt;code&gt;framework/framework-res.apk&lt;/code&gt; 作为 Framework 安装。&lt;/li&gt;
&lt;li&gt;用 Apktool 解包 &lt;code&gt;priv-app/SystemUI/SystemUI.apk&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;找到 &lt;code&gt;res/layout/status_bar.xml&lt;/code&gt;，用上面资源链接内的文件替换，或对照其参考。&lt;/li&gt;
&lt;li&gt;重新打包，得到新的 &lt;code&gt;SystemUI.apk&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将新的 APK 用压缩软件打开，将其中的 &lt;code&gt;res/layout/status_bar.xml&lt;/code&gt; 直接覆盖至旧的 APK 中。&lt;/li&gt;
&lt;li&gt;将旧的 APK 合并回 &lt;code&gt;system.img&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;system.img&lt;/code&gt; 刷入设备。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;等级：D
前置要求：&lt;a href=&quot;#opt-deodex&quot;&gt;系统反 ODEX 化&lt;/a&gt;(等级：E)&lt;/p&gt;
&lt;p&gt;概述：此系统跳转至桌面时，会强制指定包名和活动类名。移除强制指定包名与类名的语句，即可恢复正常的桌面跳转行为。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/a87849e3ec61bdb5573b915c62f6ad54.VMTTlhdq_2gvi7q.webp&quot; alt=&quot;Nova Launcher 旧图像主题&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/e561eaa6605f81a8343b0ec8e2196e81.Bjalztqy_MHpU7.webp&quot; alt=&quot;Nova Launcher Oblatum 主题&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/6fd5da599db82649885588a2436770f7.ze0G_zkk_Z1JluGB.webp&quot; alt=&quot;Square Home 方块式界面&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解包当前的 &lt;code&gt;system.img&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;确保 Apktool 已将你的 &lt;code&gt;framework/framework-res.apk&lt;/code&gt; 作为 Framework 安装。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来，需要修改两部分内容。第一部分是系统界面。这一部分保证在“最近屏幕”页面点击“概览”或“主屏幕”按键时能够正确跳转。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用 Apktool 解包 &lt;code&gt;priv-app/SystemUI/SystemUI.apk&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;smali&lt;/code&gt; 文件夹内全文搜索 &lt;code&gt;com.android.launcher3&lt;/code&gt; &lt;code&gt;com.android.launcher3.Launcher&lt;/code&gt;，删除或注释掉包含这些字符串的行。
（如果你找不到 &lt;code&gt;smali&lt;/code&gt;，那么检查前置要求）&lt;/li&gt;
&lt;li&gt;重新打包，得到新的 &lt;code&gt;SystemUI.apk&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将新的 APK 用压缩软件打开，将其中的 &lt;code&gt;classes.dex&lt;/code&gt; 直接覆盖至旧的 APK 中。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第二部分是核心代码。这一部分保证在其他界面下按“主屏幕”按键能够正确跳转。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用压缩软件提取出 &lt;code&gt;framework/android.policy.jar&lt;/code&gt; 中的 &lt;code&gt;classes.dex&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;baksmali&lt;/code&gt; 反汇编 &lt;code&gt;classes.dex&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在 smali 代码中全文搜索 &lt;code&gt;com.android.launcher3&lt;/code&gt; &lt;code&gt;com.android.launcher3.Launcher&lt;/code&gt;，删除或注释掉包含这些字符串的行。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;smali&lt;/code&gt; 构建新的 &lt;code&gt;classes.dex&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;classes.dex&lt;/code&gt; 覆盖回 &lt;code&gt;android.policy.jar&lt;/code&gt; 中。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将覆盖后的 &lt;code&gt;SystemUI.apk&lt;/code&gt; 和 &lt;code&gt;android.policy.jar&lt;/code&gt; 合并回 &lt;code&gt;system.img&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;system.img&lt;/code&gt; 刷入设备。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;完成后，你需要自行下载安装第三方桌面并启用。这里不做推荐。&lt;/p&gt;
&lt;p&gt;等级：D
前置要求：&lt;a href=&quot;#opt-deodex&quot;&gt;系统反 ODEX 化&lt;/a&gt;(等级：E)&lt;/p&gt;
&lt;p&gt;概述：屏幕关闭时，系统会发出意义不明的 &lt;code&gt;com.hys.SHUTDOWN_PHONE&lt;/code&gt; 广播。实测移除该广播可以修复问题。具体原因不明确。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解包当前的 &lt;code&gt;system.img&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;用压缩软件提取出 &lt;code&gt;framework/services.jar&lt;/code&gt; 中的 &lt;code&gt;classes.dex&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;baksmali&lt;/code&gt; 反汇编 &lt;code&gt;classes.dex&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;全文搜索 &lt;code&gt;com.hys.SHUTDOWN_PHONE&lt;/code&gt;，找到唯一一处引用，然后在其后面几行中搜索 &lt;code&gt;sendBroadcast&lt;/code&gt;，删除或注释掉这一行。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;smali&lt;/code&gt; 构建新的 &lt;code&gt;classes.dex&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;classes.dex&lt;/code&gt; 覆盖回 &lt;code&gt;services.jar&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;将覆盖后的 &lt;code&gt;services.jar&lt;/code&gt; 合并回 &lt;code&gt;system.img&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;system.img&lt;/code&gt; 刷入设备。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-smali&quot;&gt;    .line 3717
    new-instance v0, Landroid/content/Intent;

    const-string v1, &quot;com.hys.SHUTDOWN_PHONE&quot;

    invoke-direct {v0, v1}, Landroid/content/Intent;-&gt;&amp;#x3C;init&gt;(Ljava/lang/String;)V

    .line 3718
    .local v0, &quot;mintent&quot;:Landroid/content/Intent;
    iget-object v1, p0, Lcom/android/server/power/PowerManagerService$BinderService;-&gt;this$0:Lcom/android/server/power/PowerManagerService;

    # getter for: Lcom/android/server/power/PowerManagerService;-&gt;mContext:Landroid/content/Context;
    invoke-static {v1}, Lcom/android/server/power/PowerManagerService;-&gt;access$2400(Lcom/android/server/power/PowerManagerService;)Landroid/content/Context;

    move-result-object v1

-&gt;&gt; invoke-virtual {v1, v0}, Landroid/content/Context;-&gt;sendBroadcast(Landroid/content/Intent;)V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;{/*&lt;/p&gt;
&lt;p&gt;等级：D
前置要求：&lt;a href=&quot;#opt-deodex&quot;&gt;系统反 ODEX 化&lt;/a&gt;(等级：E)
前置知识：对 smali 有一定了解&lt;/p&gt;
&lt;p&gt;~~太复杂了，先放着~~
*/}&lt;/p&gt;
&lt;p&gt;等级：C
等效于：&lt;a href=&quot;#opt-navbar&quot;&gt;修改扭曲的导航按钮&lt;/a&gt;  &lt;a href=&quot;#opt-statusbar&quot;&gt;优化状态栏布局&lt;/a&gt;  &lt;a href=&quot;#opt-launcher&quot;&gt;允许第三方桌面&lt;/a&gt;的一部分&lt;/p&gt;
&lt;p&gt;请使用替换 APK 中的 &lt;code&gt;SystemUI.apk&lt;/code&gt; 替换掉设备上的 &lt;code&gt;/system/priv-app/SystemUI/SystemUI.apk&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;若你的系统没有进行过反 ODEX 化，请删除 &lt;code&gt;/system/priv-app/SystemUI/arm/&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;等级：C
等效于：&lt;a href=&quot;#opt-brightness&quot;&gt;扩大亮度范围&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;请使用替换 APK 中的 &lt;code&gt;framework-res.apk&lt;/code&gt; 替换掉设备上的 &lt;code&gt;/framework/framework-res.apk&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;即使的系统没有进行过反 ODEX 化，也&lt;strong&gt;不要&lt;/strong&gt;删除 &lt;code&gt;/system/priv-app/SystemUI/arm/&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;替代操作：替换权限管理 APK&lt;/h3&gt;
&lt;p&gt;等级：C
等效于：&lt;a href=&quot;#opt-permission&quot;&gt;修复权限管理模块&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;请使用替换 APK 中的 &lt;code&gt;PermissionControl.apk&lt;/code&gt; 替换掉设备上的 &lt;code&gt;/system/plugin/PermissionControl/PermissionControl.apk&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;若你的系统没有进行过反 ODEX 化，请删除 &lt;code&gt;/system/plugin/PermissionControl/arm/&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;主要参数&lt;/h2&gt;
&lt;h3&gt;尺寸与重量&lt;/h3&gt;
&lt;p&gt;|键|值|备注|
|-|-|-|
|尺寸|104x64x11 (mm)|显得较厚|
|质量|约160g|偏重，不很适合运动使用|&lt;/p&gt;
&lt;h3&gt;存储器&lt;/h3&gt;
&lt;p&gt;|键|值|备注|
|-|-|-|
|运行内存|2GB|系统部分优化较好，占用约150MB|
|闪存|14.90GB||
|系统分区|1.75GB||
|有效媒体容量|11.88GB*||
|最大扩展容量|128GB||&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有效媒体容量，是系统内用户数据分区的大小，用于存放用户应用、应用数据和内置存储中的文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;无线连接&lt;/h3&gt;
&lt;p&gt;|键|值|备注|
|-|-|-|
|WLAN|2.4/5GHz||
|蓝牙|BT4.2 音频输出 文件传输||
|FM 收音机|87.5~108.0Hz||&lt;/p&gt;
&lt;h3&gt;芯片&lt;/h3&gt;
&lt;p&gt;|键|值|备注|
|-|-|-|
|平台|MediaTek||
|芯片|MT6592|不适合3D游戏，略有卡顿|&lt;/p&gt;
&lt;h3&gt;显示&lt;/h3&gt;
&lt;p&gt;|键|值|备注|
|-|-|-|
|显示类型|IPS||
|分辨率|480x800||
|像素排列|纵向RGB||
|最大视角|约40°竖直/70°水平||
|最大亮度|约300/450(cd/m²)*|300 亮度仅适合室内使用|
|最小亮度|约50/10(cd/m²)*||
|色温|约9300K||
|色域|未确定||
|色深|8bit (已测试)||
|刷新率|59Hz||
|响应时间|&amp;#x3C;16ms*||
|表面处理|钢化玻璃质量达标||&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统内亮度可调范围约为 50~300。通过修改系统，可将其扩展到 10~450，此时在户外和夜间能较为轻松地使用。&lt;/li&gt;
&lt;li&gt;色深通过色彩分辨测试确认。&lt;/li&gt;
&lt;li&gt;响应时间明显小于刷新率的倒数，可能会造成稳定的卡顿感。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;触控&lt;/h3&gt;
&lt;p&gt;|键|值|备注|
|-|-|-|
|触控类型|5点触控*||
|采样频率|2x59Hz||&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多点触控为任意 5 点，没有区域限制。&lt;/li&gt;
&lt;li&gt;采样总频率 118Hz，均分给各个触摸点；单个触摸点频率最大为 59Hz。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;电池&lt;/h3&gt;
&lt;p&gt;|键|值|备注|
|-|-|-|
|设计容量|1500mAh/5.55Wh*||
|音乐续航|13h||
|视频续航|3h||
|充电时长|约1.2小时*||&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要经过数次完全充放电，电量计才能准确显示电量。&lt;/li&gt;
&lt;li&gt;充电器能提供 5V 1A 的输出。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;外围接口&lt;/h3&gt;
&lt;p&gt;|键|值|备注|
|-|-|-|
|音频输出|2x3.5mm，USB-C||
|数据接口|USB-C||
|扩展存储|TF卡||&lt;/p&gt;
&lt;h3&gt;扬声器与传感器&lt;/h3&gt;
&lt;p&gt;|键|值|备注|
|-|-|-|
|扬声器功率|1W||
|重力传感器|有，3D||&lt;/p&gt;
&lt;h3&gt;操作系统&lt;/h3&gt;
&lt;p&gt;|键|值|备注|
|-|-|-|
|Android 版本|5.1||
|构建日期|20211101||
|开发者选项可用|否*||
|支持多任务处理|是||
|强行停止应用|是*||
|支持第三方桌面|否||
|定时关机可用|是||
|OEM 可解锁|是||
|PreLoader 功能|全||&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开发者选项被隐藏了。你可以使用“创建快捷方式”等应用将其打开。&lt;/li&gt;
&lt;li&gt;应用活动被从“最近屏幕”界面移除时，会强行停止应用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;内置功能&lt;/h3&gt;
&lt;p&gt;|键|值|备注|
|-|-|-|
|音频格式|MP3, WMA, OGG, APE, FLAC, WAV||
|视频格式|RM/RMVB, MP4, AVI, MOV 等，可达1080p||
|电子书格式|TXT||&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内置功能指系统内置应用的功能。第三方应用可提供更多格式或功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;#x3C;F.Muted&gt;封面图来自商品宣传页面，未经明确授权。包含 AI 辅助创作。&amp;#x3C;/F.Muted&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/hero.CD29V3xP.png"/><enclosure url="/_astro/hero.CD29V3xP.png"/></item><item><title>[玩机] MTK MT65xx/MT67xx 刷机 Root，以 Aigo M2 Pro 为例</title><link>https://ak-ioi.com/blog/play-android/mtk-flash-root</link><guid isPermaLink="true">https://ak-ioi.com/blog/play-android/mtk-flash-root</guid><description>MTK 较老的（MT65xx/MT67xx）设备刷机获取 Root 权限的通法，使用 Magisk 方案。</description><pubDate>Thu, 20 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import F from &apos;@/components/mdx/formatting&apos;&lt;/p&gt;
&lt;p&gt;本文介绍的是 MTK 设备刷机获取 Root 权限的通法，使用 Magisk 方案。注意，可能并非所有设备均能成功。&lt;/p&gt;
&lt;p&gt;本文以 Aigo M2 Pro 智能播放器为例，此播放器搭载 Android 5.1。这个例子的特殊性在于，官方并不提供原厂镜像的下载。&lt;/p&gt;
&lt;p&gt;所有涉及电脑的操作在 Windows 系统下完成。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;时效性复核状态&quot;&gt;&lt;/p&gt;
&lt;p&gt;本文的写作时间是 2022 年第一季度。&lt;br&gt;
请留意文章的时效性。&lt;/p&gt;
&lt;p&gt;2026-01-10 说明 | 从本文写作到现在，Android 设备的分区结构已经发生一些变化，Root 隐藏话题已经变得很复杂，诸如 KernelSU、APatch 等更强大的 Root 方案也出现了。此外，本文未提及一些可能比较重要的保险措施，例如 Root 后立即进行全分区备份。&lt;strong&gt;如果你的设备比较新，或者想尝试一些较新的方案和工具，不建议再参考此文章。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Tip title=&quot;提示&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在经常有“设备获取 Root 权限不安全”的说法，这种说法过于绝对。&lt;/p&gt;
&lt;p&gt;传统方案 Root 的不安全主要来源于第三方 Recovery 系统，这允许任何人不经验证直接备份数据和修改系统文件。目前的 Root 方案较为完善，用户获取 Root 权限并不一定不安全，善用 Root 还能保护隐私。但是，对于使用不谨慎或喜欢冒险的用户，确实会大大增加隐私泄露、数据丢失、设备损坏的风险。若设备遭已获取 Root 权限的恶意软件攻击，则后果也会比一般的恶意软件攻击严重得多。&lt;/p&gt;
&lt;p&gt;Magisk 方案是目前最为完善的方案，该方案&lt;strong&gt;不需要&lt;/strong&gt;使用第三方 Recovery 系统，因而设备丢失后，对设备安全造成的影响也较小。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Tip&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;部分应用（主要为安全性要求高的银行应用、政府公共服务应用以及反作弊要求高的游戏应用，但不含主流大厂的实用型应用）会&lt;strong&gt;检测&lt;/strong&gt;设备上是否已获取 Root。若检测到，会采取警告、禁用敏感功能、要求登录验证码、拒绝加载等措施来规避可能的风险，&lt;strong&gt;游戏可能会不加警告而直接封号&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对此，你需要&lt;strong&gt;采取 Root 隐藏措施&lt;/strong&gt;。即便如此，你还需要手动选择隐藏 Root 的对象。因此在安装上述类型软件后，&lt;strong&gt;请务必记住要立即对其开启 Root 隐藏&lt;/strong&gt;，以免造成不必要的麻烦。&lt;/p&gt;
&lt;p&gt;关于一些简单易用的 Root 隐藏措施，可以看&lt;a href=&quot;/blog/play-android/oneplus-flash-root/#hide-magisk&quot;&gt;这篇文章&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Danger title=&quot;危险&quot;&gt;&lt;/p&gt;
&lt;p&gt;注意，本文根据个人经验编写，内容可能并不完全准确。
若学习操作，导致设备出现系统损坏、无法开机或正常使用以及数据丢失等问题，机主应当自行承担责任。&lt;/p&gt;
&lt;p&gt;为防止操作出现疏漏，请至少完整阅读一遍后再开始操作。建议使用电脑等大屏设备阅读。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Danger&gt;&lt;/p&gt;
&lt;h2&gt;刷机，Root，Magisk，为什么？&lt;/h2&gt;
&lt;h3&gt;Root 意味着什么？&lt;/h3&gt;
&lt;p&gt;Root 意味着&lt;strong&gt;自由&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Root，指的是 root 用户。该用户对设备有&lt;strong&gt;最高的权限&lt;/strong&gt;。获取 Root 权限，即修改系统，使得用户和应用能够以 root 用户的身份执行操作，也就是说，用户获得对设备的最高访问权，真正实现了“&lt;strong&gt;我的设备我做主&lt;/strong&gt;”。&lt;/p&gt;
&lt;p&gt;Root 权限的使用方式主要分为两类。一种是获取系统特权，常见应用如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;利用文件管理器或破解工具自由备份、还原应用的数据和游戏进度。&lt;/li&gt;
&lt;li&gt;利用系统权限禁用/隐藏应用，或禁止应用后台运行。&lt;/li&gt;
&lt;li&gt;禁用，或在当前用户空间下卸载系统应用。&lt;/li&gt;
&lt;li&gt;自由操纵应用的权限，例如禁止自启动。&lt;/li&gt;
&lt;li&gt;使用内存修改器对游戏数据进行破解。&lt;/li&gt;
&lt;li&gt;修改受保护的系统设置，例如无障碍服务。&lt;/li&gt;
&lt;li&gt;修改系统内存，实现更改设备 MAC 地址保护隐私/绕过身份验证等。&lt;/li&gt;
&lt;li&gt;修改隐藏的设置项，例如使状态栏上的时间精确到秒（这个一般不需要 Root 也能做到）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另一种，为&lt;strong&gt;修改&lt;/strong&gt;系统。常见应用如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定制系统的开机动画。&lt;/li&gt;
&lt;li&gt;彻底清除、替换系统应用。&lt;/li&gt;
&lt;li&gt;将应用安装为系统应用。&lt;/li&gt;
&lt;li&gt;修改内核选项。&lt;/li&gt;
&lt;li&gt;修改系统构建信息，以实现修改设备的制造商、机型等信息。&lt;/li&gt;
&lt;li&gt;对系统核心进行破解，以移除应用覆盖安装时的版本和签名检测（对破解游戏比较有用）。&lt;/li&gt;
&lt;li&gt;调校充电速度等行为。&lt;/li&gt;
&lt;li&gt;对硬件超频以压榨性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同样，Root 意味着&lt;strong&gt;风险&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若对恶意软件授予 Root 权限，其将具有非常大的破坏力。&lt;/li&gt;
&lt;li&gt;Root 权限使用时不谨慎，或因为各类天灾人祸，会导致系统损坏与数据丢失。&lt;/li&gt;
&lt;li&gt;系统更新变得麻烦。若原厂镜像未备份或系统被修改，设备将无法直接接受增量更新，&lt;strong&gt;需要刷机才能升级系统&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;我需不需要 Root？&lt;/h3&gt;
&lt;p&gt;是否需要 Root 取决于你需要进行的操作类型。&lt;/p&gt;
&lt;p&gt;如果你的需求在 &lt;code&gt;adb shell&lt;/code&gt; 下，或使用 &lt;code&gt;adb&lt;/code&gt; 授权就能够完成（&lt;strong&gt;系统特权&lt;/strong&gt;型中的大多数均属于此类），则很可能不需要 Root。如今，更加流行一些免 Root 获得系统特权的权宜之计（例如 &lt;a href=&quot;https://shizuku.rikka.app/zh-hans/&quot;&gt;Shizuku API&lt;/a&gt;）。相比于 Root，这种方式的风险小得多，更值得提倡。&lt;/p&gt;
&lt;p&gt;如果你需要&lt;strong&gt;修改系统&lt;/strong&gt;，或从事应用/游戏破解、Android 核心破解等工作，则你需要 Root。&lt;/p&gt;
&lt;h3&gt;为什么是 Magisk？&lt;/h3&gt;
&lt;p&gt;传统的 Root 权限方案是 SuperSU，该方案主要通过修改系统&lt;a href=&quot;/blog/play-android/preintro-bootmode#general-partitions&quot;&gt;分区&lt;/a&gt; &lt;code&gt;/system&lt;/code&gt; 来安装 Root 权限。&lt;/p&gt;
&lt;p&gt;Magisk 是一个较新的方案，主要优势如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;#️⃣ 轻量化修改。相比于 SuperSU，其只修改比系统分区小得多 boot 分区。因此，刷机过程要快很多，移除 Root 权限并还原原厂镜像容易得多。&lt;/li&gt;
&lt;li&gt;🚫 无系统（Systemless）方式。Magisk 不修改系统分区，使得设备接受后续系统更新要容易很多。&lt;/li&gt;
&lt;li&gt;🧩 模块（Modules）。Magisk 提供许多使用无系统方式修改系统的模块。&lt;/li&gt;
&lt;li&gt;💧 隐藏（MagiskHide）。Magisk 内置了对其他应用隐藏 Magisk 和 Root 权限的工具，这使得对 Root 权限敏感的游戏、银行等软件仍然能正常使用。&lt;/li&gt;
&lt;li&gt;⚡ 一次性刷机方案。Magisk 的安装只需要一次刷机就能完成，不使用第三方 Recovery 系统。这使得第三方 Recovery 不支持某一设备的问题得到解决，且对系统的安全性影响比较小。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;为什么刷机？&lt;/h3&gt;
&lt;p&gt;如果早些时候（2014 年左右）你接触过 Root 权限的获取，你可能会知道许多“&lt;strong&gt;一键 Root&lt;/strong&gt;”等免刷机的 Root 权限获取软件，有的系统甚至直接允许用户打开 Root 权限。为什么如今获取 Root 如此麻烦？为什么刷机风险这么大，却又说刷机其实是最明智的选择？&lt;/p&gt;
&lt;p&gt;原因之一，是如今“一键 Root”等工具已经不好用了。&lt;/p&gt;
&lt;p&gt;这类软件往往使用&lt;strong&gt;系统漏洞&lt;/strong&gt;进行破解。事实上，这是一个很大的安全性问题，因为不只是一键 Root 工具可以使用漏洞，其他的任何软件都可以，而一旦恶意软件获取 Root 权限，Android 权限管理对其就形同虚设。如今，随着安全漏洞的修复，此类工具自然也就越来越难用了。&lt;/p&gt;
&lt;p&gt;然而，刷机却不利用奇技淫巧，具有&lt;strong&gt;理论可行性&lt;/strong&gt;，因而成为了公认的“通解通法”。&lt;/p&gt;
&lt;p&gt;原因之二，也是最重要的原因，就是“&lt;strong&gt;权力越大，责任越大&lt;/strong&gt;”。我们常说，如果一个设备制造商允许用户刷机并给了教程，那它确实是在支持用户折腾设备；如果设备制造商直接允许在设置中开启 Root 权限，那么它是在给维修点刷业绩。正因如此，我强烈反对使用人工代刷服务来获取 Root 权限。&lt;/p&gt;
&lt;p&gt;获取 Root 权限&lt;strong&gt;并非一劳永逸&lt;/strong&gt;。由于 Root 权限意味着设备最高访问权，一旦后续使用 Root 权限时不谨慎，系统就可能被弄坏。此时，必须使用刷机方式才能修复设备，而使用“一键 Root”工具的人，往往不具备刷机的知识和技能，因此设备只能被送去维修，或者报废。使用刷机方法获取 Root 的过程中，机主锻炼了自身的技能，并已经下载到或者备份好了原厂镜像。只有这样，机主才算是真正拿到了“玩机资格证”。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;$\large{\text{刷机会不会清除数据？}}$&lt;/p&gt;
&lt;p&gt;刷机本身不清除数据。但是，下面几种情况需要清空数据。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用正常 Fastboot 方式解锁 Bootloader，会一并清除数据。（通常，这只需要做一次）&lt;/li&gt;
&lt;li&gt;安装第三方操作系统，通常要自行清空数据，否则系统无法正常工作。&lt;/li&gt;
&lt;li&gt;获取 Root 权限后，不当修改系统数据、禁用系统组件，设备出现无法开机、无限重启的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;各种各样的刷机事故也会导致需要清除数据。因此，开始前，请&lt;strong&gt;对重要数据做好备份&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h2&gt;我用的是不是 MTK 设备？&lt;/h2&gt;
&lt;p&gt;如果你用的设备是常见的手机、平板等，请上网查询资料确认设备的芯片（若看到 MT65xx、MT67xx、天玑等则为 MTK 设备）。&lt;/p&gt;
&lt;p&gt;如果你的设备不常见，尝试在设置中查看系统应用列表。若存在名为 com.mediatek 的应用，则说明是 MTK 设备。&lt;/p&gt;
&lt;h2&gt;一定能成功吗？&lt;/h2&gt;
&lt;p&gt;不一定。&lt;/p&gt;
&lt;p&gt;以下是常见的不能成功的情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设备制造商不允许 Bootloader 解锁*，&lt;strong&gt;且&lt;/strong&gt;系统有启动验证。
（Android 5.1 及以下的设备一定不具有启动验证）&lt;/li&gt;
&lt;li&gt;官方不提供原厂镜像，且锁定了 Preloader。*&lt;/li&gt;
&lt;li&gt;设备没有 Boot Ramdisk，因而不支持正常安装 Magisk。&lt;/li&gt;
&lt;li&gt;官方不提供厂刷包，且设备无法启用 ADB，因而无法获得分区表。&lt;/li&gt;
&lt;li&gt;系统版本不受支持（Magisk 目前支持 5.0~11.0）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然这些情况也不意味着一定不成功。有例外情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;部分 MTK 设备中，Bootloader 解锁可以利用漏洞强制进行（工具为 MTK Client），且这不会清除数据。此处不赘述其方法。&lt;/li&gt;
&lt;li&gt;如果系统没有启动验证，则 Bootloader 可以不解锁。&lt;/li&gt;
&lt;li&gt;部分设备中，Preloader 锁可以使用 MTK Auth Bypass 绕过，此处不赘述其方法。&lt;/li&gt;
&lt;li&gt;没有 Boot Ramdisk 的设备，可以使用第三方魔改过的 Magisk（这种情况下 MagiskHide 将无法使用）。&lt;/li&gt;
&lt;li&gt;无法获得分区表时，可先用 SP Flash Tool 回读（此操作下文会介绍）整个闪存的前 1/4 空间，然后利用十六进制编辑器确认其中 boot 和 recovery 分区的位置，手动编写出不完整的分区表，然后对 boot 镜像安装 Magisk，或利用 boot 分区生成合适的 TWRP（一个第三方 Recovery 系统），然后查看系统文件对设备进行进一步破解。此操作相当复杂，此处不介绍。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;h3&gt;如果需要，学好基础知识&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;/blog/play-android/preintro-adb&quot;&gt;[基础预科] ADB、Android 终端、Android 用户权限&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/blog/play-android/preintro-bootmode&quot;&gt;[基础预科] Android 分区、启动模式、Fastboot&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;确认设备如何强制关机&lt;/h3&gt;
&lt;p&gt;开始刷机操作前，请确认自己知道如何强制关机或重启设备，并且已经尝试成功过。常见方法有这些：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拔掉充电器，然后拆除内置电池，将会关机。（适用于电池可拆卸的设备）&lt;/li&gt;
&lt;li&gt;长按电源按钮 10s，强制重启。（适用于大多数设备）&lt;/li&gt;
&lt;li&gt;同时按住电源按钮和音量下按钮，保持 10s，强制重启。（主要为三星设备）&lt;/li&gt;
&lt;li&gt;同时按住电源按钮和音量上按钮，保持 10s，待屏幕熄灭或感受到振动后立即放开，即为强制关机。（OnePlus）&lt;/li&gt;
&lt;li&gt;同时按住电源按钮和两个音量按钮，待屏幕熄灭后立刻放开，即为强制关机。（适用于大多数设备）&lt;/li&gt;
&lt;li&gt;用细小物体（建议取卡针）戳麦克风孔（或电源按钮附近的一个孔），触发其中的开关。（适用于学习机、媒体播放器、移动数据终端等设备）
提示：不要用缝衣针的尖端戳麦克风孔，以免损坏麦克风。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;确认 USB 接触良好&lt;/h3&gt;
&lt;p&gt;接触良好的 USB 接口和数据线是刷机的基础。若刷机过程中连接断开，将产生无法开机的后果（重新刷入受损的分区可以恢复）。&lt;/p&gt;
&lt;p&gt;建议使用 Type-C 接口的 USB 集线器，刷机过程中不要在集线器上插拔设备。&lt;/p&gt;
&lt;h3&gt;安装 ADB 环境&lt;/h3&gt;
&lt;p&gt;若电脑上没有 ADB，请先安装。方法参照&lt;a href=&quot;/blog/play-android/preintro-adb#install-adb&quot;&gt;此处&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;启用 USB 调试&lt;/h3&gt;
&lt;p&gt;安装 ADB 后，启用设备的 USB 调试，并确认能正常使用，然后对电脑进行“始终允许”的 USB 调试授权。方法参照&lt;a href=&quot;/blog/play-android/preintro-adb#dev-mode&quot;&gt;此处&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Tip title=&quot;提示&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果你手里的设备就是 Aigo M2 pro，那么该设备的开发者选项是被隐藏的，无法通过正常方式打开。你可以安装带有活动选择器的应用将其打开，但是，下面的步骤会更加简单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将设备通过 USB 连接至电脑。&lt;/li&gt;
&lt;li&gt;下拉通知栏，找到选择连接模式的通知，点击打开模式选择界面。
此时若打开多任务界面（导航栏方形按钮），会发现此页标题是“开发者选项”。&lt;/li&gt;
&lt;li&gt;拔掉数据线，模式选择界面将会关闭。&lt;/li&gt;
&lt;li&gt;打开多任务界面，点击刚才的“开发者选项”窗口。窗口将打开失败，系统跳转回桌面。&lt;/li&gt;
&lt;li&gt;再次打开多任务界面，点击“开发者选项”窗口，将会进入开发者选项。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;或者，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开“设备状态”页面。&lt;/li&gt;
&lt;li&gt;快速点击“内核版本”多次。&lt;/li&gt;
&lt;li&gt;弹出密码输入框，输入密码 &lt;code&gt;hys123&lt;/code&gt; 并确定。若这个密码没用，则尝试在终端中执行 &lt;code&gt;getprop ro.custom.developer.pw&lt;/code&gt; 获取密码。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;#x3C;/F.Tip&gt;&lt;/p&gt;
&lt;h3&gt;下载 MTK Droid Tools&lt;/h3&gt;
&lt;p&gt;MTK Droid Tools 用于导出分区表。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://androiddatahost.com/w2ndr&quot;&gt;下载 MTK Droid Tools&lt;/a&gt;（压缩包密码：MTK），并解压到恰当的位置。&lt;/p&gt;
&lt;h3&gt;下载 SP Flash Tool 和驱动&lt;/h3&gt;
&lt;p&gt;SP Flash Tool 用于提取/备份原厂镜像并进行刷机。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://wcl.sparkslab.art/index.php?share/fileDownload&amp;#x26;sid=KEQBAzId&amp;#x26;user=4&amp;#x26;path=%2FAndroidApps%2FMTK%20Drivers.zip&quot;&gt;下载驱动程序&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://wcl.sparkslab.art/index.php?share/fileDownload&amp;#x26;sid=KEQBAzId&amp;#x26;user=4&amp;#x26;path=%2FAndroidApps%2FSP%20Flash%20Tool%20Old.zip&quot;&gt;下载旧版 SP Flash Tool，用于处理 MT65xx 平台&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://wcl.sparkslab.art/index.php?share/fileDownload&amp;#x26;sid=KEQBAzId&amp;#x26;user=4&amp;#x26;path=%2FAndroidApps%2FMTK%20Flash%20Tool%2067xx.zip&quot;&gt;下载新版 SP Flash Tool&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;备份重要数据&lt;/h3&gt;
&lt;p&gt;无论采用何种方案，开始前，请备份好所有能备份的重要数据。数据无价！&lt;/p&gt;
&lt;h3&gt;允许 OEM 解锁&lt;/h3&gt;
&lt;p&gt;进入开发者选项，打开“允许 OEM 解锁”开关，按提示输入设备锁屏密码，以允许后续解锁 Bootloader（虽然可能未必需要）。&lt;/p&gt;
&lt;p&gt;此开关一般位于开发者选项第一屏内。如果没有，说明 Bootloader 可能无法解锁，但是若确认设备没有启动验证，也可以继续操作。&lt;/p&gt;
&lt;h3&gt;调整心态&lt;/h3&gt;
&lt;p&gt;刷机是风险较大的操作，尤其是第一次进行，不熟悉流程的时候。新手如果出现重大失误，很可能一步错，步步错，最后把设备彻底弄坏。&lt;/p&gt;
&lt;p&gt;刷机时，请选择安排不紧张的时间段，保证至少一小时的连续空闲时间，切忌仓促行事。&lt;/p&gt;
&lt;p&gt;开始前，深呼吸，使心情平和。第一次刷机时，如果有条件，请使用低价的备用机练习，不要使用主力设备。&lt;/p&gt;
&lt;p&gt;操作过程中，时刻记住正确的操作决不会置设备于死地。要细心，但也要胆大，不能过度紧张。&lt;/p&gt;
&lt;p&gt;在两个步骤之间，或者在等待的时候，可以吃点东西，去房间外放松休息。&lt;/p&gt;
&lt;h2&gt;概览&lt;/h2&gt;
&lt;p&gt;注意多个方案只需要选一个。&lt;/p&gt;
&lt;p&gt;这只是概览，不要直接对着操作。预计用时假设操作者是新手，但操作顺利。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装 Magisk App &amp;#x3C;F.Muted&gt;3分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;🔰 下载并安装 App &amp;#x3C;F.Muted&gt;3分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;👀 查看 Ramdisk 是否为“是”，确认是否受支持 &amp;#x3C;F.Muted&gt;0分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;获取分区表 &amp;#x3C;F.Muted&gt;5分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;🧷 设备连接至 USB 调试 &amp;#x3C;F.Muted&gt;3分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;🧱 打开 MTK Droid Tools，创建分区表文件 &amp;#x3C;F.Muted&gt;2分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;提取原厂镜像 &amp;#x3C;F.Muted&gt;12分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;⚙️ 安装 MTK 驱动 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;🗒️ 打开 SP Flash Tool，设置好回读分区 &amp;#x3C;F.Muted&gt;2分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;💿 关机后，插入设备开始回读 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;📀 顺便备份好系统分区 &amp;#x3C;F.Muted&gt;8分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;修补镜像 &amp;#x3C;F.Muted&gt;4分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;🩹 利用 Magisk App 修补镜像 &amp;#x3C;F.Muted&gt;4分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;刷入镜像 - SP Flash Tool 方案 &amp;#x3C;F.Muted&gt;2~6分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;👀 判断设备是否有启动验证 &amp;#x3C;F.Muted&gt;0分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;🔓 如果有，解锁 Bootloader &amp;#x3C;F.Muted&gt;0~4分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;🗒️ 打开 SP Flash Tool，选择刷入的镜像 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;⚡ 关机后，插入设备完成刷机 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;🚫 拔掉 USB 线 &amp;#x3C;F.Muted&gt;0分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;刷入镜像 - Fastboot 方案 &amp;#x3C;F.Muted&gt;5分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;🔓 解锁 Bootloader &amp;#x3C;F.Muted&gt;4分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;⚡ 命令刷入新的镜像 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;⭕ 命令重启 &amp;#x3C;F.Muted&gt;0分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;刷入镜像 - Fastboot 方案稳妥版 &amp;#x3C;F.Muted&gt;8分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;🔓 解锁 Bootloader &amp;#x3C;F.Muted&gt;4分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;🪛 用新的镜像测试启动 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;🔰 重新安装 Magisk App &amp;#x3C;F.Muted&gt;2分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;⚡ 使用“直接安装”方式安装 &amp;#x3C;F.Muted&gt;1分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;⭕ 重启设备 &amp;#x3C;F.Muted&gt;0分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;开机体验 &amp;#x3C;F.Muted&gt;2分钟&amp;#x3C;/F.Muted&gt;
&lt;ul&gt;
&lt;li&gt;🔰 重新安装 Magisk App &amp;#x3C;F.Muted&gt;2分钟&amp;#x3C;/F.Muted&gt;&lt;/li&gt;
&lt;li&gt;✨ 打开需要 Root 权限的应用，或安装模块，开始使用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;#x3C;F.Tip title=&quot;提示&quot;&gt;&lt;/p&gt;
&lt;p&gt;到达“刷入镜像”步骤之前，所有操作都没有对设备的系统造成更改。如果这种情况下遇到问题无法继续，需要撤销，请结束所有正在进行的操作，拔下数据线，强制重启设备，然后删除 Magisk App。&lt;/p&gt;
&lt;p&gt;若已到达“刷入镜像”步骤，强烈建议做到底。如果真的出现问题需要撤销，请按照下文“快速故障排除”提示框内的指示进行。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Tip&gt;&lt;/p&gt;
&lt;h2&gt;安装 Magisk App&lt;/h2&gt;
&lt;p&gt;下载 &lt;a href=&quot;https://github.com/topjohnwu/Magisk/releases/tag/v23.0&quot;&gt;Magisk&lt;/a&gt; App，使用 USB 传输、网盘等手段将其复制到 Android 设备上（或直接用 ADB 安装），完成安装。&lt;/p&gt;
&lt;p&gt;安装后，打开应用，&lt;strong&gt;检查 Ramdisk 的状态&lt;/strong&gt;。如果为“是”，那么恭喜，你的设备可以正常安装 Magisk。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/7b3b7ef0d5313ca060cfb9452e040a4a.SvWK5O_9_ZLrTik.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;设备支持 Magisk！&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;部分小米设备的 boot 镜像中没有 Ramdisk，但是启动引导程序实际上接受 Ramdisk。这种情况下，Magisk 会检测为“否”，并且目前没有正确的检测方式。你应当自己试试以确定设备是否受支持。&lt;/p&gt;
&lt;p&gt;如果你的是三星设备，或 A/B 栏为是，建议同时参考一下其他文章再来操作。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：应该使用哪个版本的 Magisk？&lt;/strong&gt;|
|A：你能选择的最低版本取决于你的 Android 系统版本，例如 12 以上的用户只能选择 24.0 及更高版本；要使用 Magisk 的 Zygisk 特性，必须选择 24.0 以上版本或 Magisk Alpha；要使用 Magisk 原版隐藏 Root 的功能，必须选择 23.0 以下版本或 Magisk Alpha。|
|&lt;strong&gt;Q：Ramdisk 显示“否”，能否继续？&lt;/strong&gt;|
|A：不建议继续。然而如果你使用的是小米设备，则应当尝试一下。|
|&lt;strong&gt;Q：在不支持的设备上安装，会怎么样？&lt;/strong&gt;|
|A: 如果系统版本不支持（如 Android 4.4 及以下），则 Magisk 不会允许你安装；如果设备不接受 Ramdisk，则安装后无效，此时建议再次刷机撤销你的更改。|&lt;/p&gt;
&lt;h2&gt;获取分区表&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;不同设备的分区表不同，甚至同样设备不同版本的系统分区表也可能不同。
请亲自完成分区表提取，不要使用网络上的分区表资源！&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;将设备连接到电脑，&lt;strong&gt;确认 USB 调试已开启，且 USB 连接模式不是“仅充电”&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;打开 MTK Droid Tools，等待设备上弹出 ADB 授权提示，点击“允许”。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/50df1505226c0588ceb629d63b6d5712.DkjazJI6_ZyCpEL.webp&quot; alt=&quot;&quot; title=&quot;[maxh=300]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;授权窗口&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;此时观察 MTK Droid Tools 窗口，工具上将逐步显示出设备的详细信息，&lt;strong&gt;左下角方块为蓝色&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/9d463ae771ec391f53150041f3d0bb36.DzUoowSk_7gdJO.webp&quot; alt=&quot;&quot; title=&quot;[maxw=400]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;MTK Droid Tools 窗口&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;不要点击右下角的 ROOT，这是用于在已 Root 设备上申请 Root 权限的。&lt;strong&gt;点击 Blocks Map，在弹出的窗口中点击 Create Scatter File&lt;/strong&gt;，将生成的 txt 文件&lt;strong&gt;保存到一个空文件夹&lt;/strong&gt;（&lt;strong&gt;不要更改文件名&lt;/strong&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/20e329c927c4bac4e5a74b68b8280d7f.Ck_vDGRe_Z2wCanS.webp&quot; alt=&quot;&quot; title=&quot;[maxw=400]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;Blocks Map&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;如果你想知道保存出来的文件大概是啥样，可以&lt;a href=&quot;https://wcl.sparkslab.art/index.php?share/file&amp;#x26;sid=KEQBAzId&amp;#x26;user=4&amp;#x26;path=%2FAndroidApps%2FMT6592_Android_scatter.txt&quot;&gt;查看此样例文件&lt;/a&gt;。注意，不要拿来使用！&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：MTK Droid Tools 无法检测到设备，且设备无反应。&lt;/strong&gt;|
|A：检查通知栏内是否有“已连接到 USB 调试”提示；检查 USB 模式是否为“仅充电”，如果是，请切换；如果问题仍然无法解决，请换一台 Windows 10 及以上的电脑试试。|
|&lt;strong&gt;Q：USB 调试已打开，但通知栏不显示“已连接到 USB 调试”，也无法选择连接模式。&lt;/strong&gt;|
|A：你的数据线可能不支持 USB 数据传输，或者你使用的 USB 接口不对。换一个试试。|
|&lt;strong&gt;Q：设备弹出了授权提示框，但检测不正常，无法创建分区表。&lt;/strong&gt;|
|A：请再次确认设备是否为 MTK 设备。|
|&lt;strong&gt;Q：是否需要自行验视创建的 txt 文件？&lt;/strong&gt;|
|A：不需要。|&lt;/p&gt;
&lt;h2&gt;提取原厂镜像&lt;/h2&gt;
&lt;h3&gt;安装驱动&lt;/h3&gt;
&lt;p&gt;打开 Windows 设备管理器。&lt;a href=&quot;https://cn.bing.com/search?q=Windows+%E8%AE%BE%E5%A4%87%E7%AE%A1%E7%90%86%E5%99%A8&quot;&gt;【搜索“Windows 设备管理器”】&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;将 Android 设备关机，然后通过数据线连接到电脑，&lt;strong&gt;立刻观察设备管理器。此时将短暂地出现“MediaTek MT65xx Preloader”设备&lt;/strong&gt;（即使芯片是 67xx，设备名仍然是 65xx）。立即右键，选择“更新驱动程序”。&lt;strong&gt;选择“浏览我的电脑以查找驱动程序”&lt;/strong&gt;，然后选择先前下载的 MTK 驱动目录，Windows 将会完成驱动安装。&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：这个操作有点困难。&lt;/strong&gt;|
|A：请锻炼手速。另外设备插入时按住音量增大键，似乎设备出现的时间会更长。|
|&lt;strong&gt;Q：没有听到设备插入的提示音，设备直接显示充电界面。&lt;/strong&gt;|
|A：你的数据线可能不支持 USB 数据传输，或者你使用的 USB 接口不对。换一个试试。|
|&lt;strong&gt;Q：能听到设备插入的提示音，但是找不到 MediaTek MT65xx Preloader。&lt;/strong&gt;|
|A：这说明驱动可能已经装好了。|
|&lt;strong&gt;Q：驱动程序目录应该选哪个？&lt;/strong&gt;|
|A：下载上文“下载 SP Flash Tools 和驱动”章节中的驱动压缩包，解压至合适的目录，然后在选择驱动时，直接选择解压到的位置，系统将自动找到驱动。|&lt;/p&gt;
&lt;h3&gt;提取镜像&lt;/h3&gt;
&lt;p&gt;我们需要提取的是设备的 boot.img。另外，建议同时备份 system.img，以备不时之需。&lt;/p&gt;
&lt;p&gt;此时，你应该知道了设备的芯片型号。&lt;strong&gt;选择合适的 SP Flash Tool 打开&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在主界面选择“下载 DA”，选中 SP Flash Tool 目录下的 MTK_AllInOne_DA.bin；选择“配置文件”，&lt;strong&gt;选中上一步中保存的 txt 文件&lt;/strong&gt;。下方将会显示分区表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/0579a9f3e8e75cd0a20d31836bf8f2de.BQVlWyM8_ZaFjGm.webp&quot; alt=&quot;&quot; title=&quot;[maxw=500]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;SP Flash Tool 主界面&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;切到上方“回读”菜单。如果使用的是新版 SP Flash Tool，则此处所有内容已经自动填好，&lt;strong&gt;取消勾选所有项目&lt;/strong&gt;，然后勾选 BOOTIMG（或 boot）和 ANDROID（或 system）分区即可。否则，将需要手动抄写 BOOTIMG 和 ANDROID 分区的信息，方式如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;点击上方“添加”按钮。
&lt;img src=&quot;https://ak-ioi.com/_astro/32f2ca96fda08a321fbb5c15eb8807a6.4WAHild3_Z6c7KE.webp&quot; alt=&quot;&quot; title=&quot;[maxh=48]&quot;&gt;&lt;/li&gt;
&lt;li&gt;打开 txt 文件，找到需要回读的分区信息。
&lt;img src=&quot;https://ak-ioi.com/_astro/cf5697d3bbd97da3fe2fca46bc86673d.BdmacwrR_OSqxw.webp&quot; alt=&quot;&quot; title=&quot;[maxw=300]&quot;&gt;&lt;/li&gt;
&lt;li&gt;双击 SP Flash Tool 下方新增的一行，&lt;strong&gt;选择镜像的保存位置&lt;/strong&gt;。文件名建议和 txt 文件中 filename 一致。&lt;/li&gt;
&lt;li&gt;选择后，弹出的窗口中区域选择 EMMC_USER，&lt;strong&gt;起始地址填写 linear_start_addr，长度填写 partition_size&lt;/strong&gt;。请务必确保没有犯复制错内容、连续粘贴两次等错误。
&lt;img src=&quot;https://ak-ioi.com/_astro/21f2c920dcc6e068be1fb12577f7cf7b.CSpqAdht_1bPrSF.webp&quot; alt=&quot;&quot; title=&quot;[maxw=300]&quot;&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;完成分区选择或抄写后，点击顶部的“回读”按钮，&lt;strong&gt;然后将已关机的设备连接到电脑&lt;/strong&gt;。此时，会听到一次设备接入电脑的提示音。软件几秒后将显示进度条，开始回读。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/8c40eaa139c1a012be17d4ebf6c60bd5.DfefxV0t_Z111soT.webp&quot; alt=&quot;&quot; title=&quot;[maxw=500]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;正在回读&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;这时，可以喝杯水，出去休息一下。（请放心，这一步是没有风险的）&lt;/p&gt;
&lt;p&gt;稍后，回读将结束，&lt;strong&gt;此时会听到设备断开连接的提示音，十几秒后将弹出回读成功的提示框&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;此时 boot.img 和 system.img 镜像应当已经保存到你指定的位置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/c04f68f22657243ae626ba7004781dc9.zJ7RWNmt_Z1GYQvJ.webp&quot; alt=&quot;&quot; title=&quot;[maxw=500]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;回读完毕&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;确认镜像已保存&lt;/strong&gt;后，拔下设备，开机。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;$\large{\text{为什么建议同时备份 system.img？}}$&lt;/p&gt;
&lt;p&gt;备份 system.img 不是必须的。然而，即便 Magisk 本身不修改系统，获取 Root 权限的应用则可能修改系统。以下情况下，你将需要原厂的系统镜像：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统已被修改，但现在需要接受增量系统更新。此时系统镜像必须完全恢复到原厂状态。&lt;/li&gt;
&lt;li&gt;使用 Root 权限过程中由于各种天灾人祸，系统分区被改坏。此时需要重新刷入系统镜像进行还原。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请注意，系统镜像修改后，设备能正常重启不代表系统镜像没有问题。只有恢复出厂设置（清空数据分区）后能正常开机才能完全证明系统镜像能正常使用。常见的情形是，使用幸运破解器破解 Android 核心时选择了“仅破解到虚拟机缓存”，然后破解了系统应用，此时若清除虚拟机缓存，则这些系统应用将不能正常加载。&lt;/p&gt;
&lt;p&gt;使用 Magisk 时，你应当要对修改系统的操作有敏感性。较大规模修改系统时，请一次一备份，并保留所有历史备份，步步为营。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：此操作是否会清空我的数据？&lt;/strong&gt;|
|A：不会。但这时，你应该已经备份过重要数据了。|
|&lt;strong&gt;Q：为什么“你应该知道了设备的芯片型号”？&lt;/strong&gt;|
|A：上一步保存分区表时，默认文件名应当形如 &lt;code&gt;&amp;#x3C;芯片型号&gt;_Android_scatter.txt&lt;/code&gt;。|
|&lt;strong&gt;Q：“回读”是什么意思？&lt;/strong&gt;|
|A：回读是刷机的逆向操作，即将设备中原有的系统镜像传输到电脑。|
|&lt;strong&gt;Q：如何快速找到 txt 文件中的分区？&lt;/strong&gt;|
|A：用记事本打开，按 Ctrl+F，查找分区名。|
|&lt;strong&gt;Q：回读镜像应该保存到哪里？&lt;/strong&gt;|
|A：建议与 txt 文件同一目录。|
|&lt;strong&gt;Q：为什么建议同时备份 system.img？&lt;/strong&gt;|
|A：Root 后，有的操作难免会修改系统。备份系统分区可以使设备在需要接受系统更新时还原原厂，也可以用来在系统被玩坏时恢复。|
|&lt;strong&gt;Q：回读开始时出现奇怪的错误。&lt;/strong&gt;|
|A：可能是这些情况：有的设备关机状态下插入时需要按住一个或两个音量键（设备连接成功后无须继续按住），请上网搜索，或自行尝试所有音量键组合，若成功，接下来刷入镜像时也请使用相同的音量键组合；你所用的 SP Flash Tool 可能和你的芯片不匹配，换另一个版本试试，若成功，接下来刷入镜像时也请使用同一版本；有的设备不支持用正常方法回读，这种情况下请前往官网下载镜像；有的设备设备有 Preloader 锁，请自行搜索 MTK Auth Bypass 尝试绕过，或知难而退，终止操作。|
|&lt;strong&gt;Q：设备插入时 SP Flash Tool 卡住/未响应。&lt;/strong&gt;|
|A：这是正常的，请等几秒。|
|&lt;strong&gt;Q：回读时界面左下角显示的读取速度直线下滑。&lt;/strong&gt;|
|A：这是速度计算机制的问题，请不要惊慌。速度不会降低到 0。|
|&lt;strong&gt;Q：由于 USB 接触不良，回读中道崩殂。&lt;/strong&gt;|
|A：先调整心态，不要惊慌，这不会对设备的系统造成损害。在 SP Flash Tool 上停止回读（如果已经出错就无须再停止），拔下数据线，然后强制重启设备。接下来，再次关机设备。重新尝试前，先排查一下 USB 接触不良的原因。|
|&lt;strong&gt;Q：操作完成后，设备长按电源键无法开机。&lt;/strong&gt;|
|A：正确开机操作：拔掉数据线，长按电源键至少 3s；如果仍然无法开机，可能是 Preloader 卡住，请尝试强制重启。|
|&lt;strong&gt;Q：开机后发现系统时间异常或重置，是不是中了病毒？&lt;/strong&gt;|
|A：这是正常现象，不必惊慌。|&lt;/p&gt;
&lt;h2&gt;修补镜像&lt;/h2&gt;
&lt;p&gt;将获取到的 boot.img 传输至 Android 设备中。&lt;/p&gt;
&lt;p&gt;打开 Magisk App，点击“安装”。不勾选“保留 AVB 2.0/dm-verity”，&lt;strong&gt;安装方式选择“选择并修补一个文件”&lt;/strong&gt;。&lt;strong&gt;选择刚刚传输的 boot.img&lt;/strong&gt;，并确保所选择的文件无误，然后确定。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;Magisk 需要检测目标设备的系统版本和架构以正确完成镜像修补，故请避免在其他设备上修补。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/fb82cb405d5d11053289f54dc8a1455e.Bm1C0YBD_1bstr7.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/cb7d7e469a13bc56071c9187b1fea388.BR5K1qaA_Z1CEciL.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/bc2104d3409b96e3a1c4781148c1491c.Dp7r6ldF_ZvHkE1.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/f6a0a5105368ff0b34f1e49a2377b016.Dv2i4vq7_VVtUR.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/679f1cfd36fe19096029ab7f81aae0a9.3ktxQD6g_Z1yxmse.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/a335ef45b4ffcb229425bb749a7a5c8a.CBe4Nwqp_ZY1wvp.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;打开内置文件管理器，将得到的新文件（位于 &lt;code&gt;/sdcard/Download/&lt;/code&gt; 下）改一个较短的名字，然后传输至电脑。&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：AVB 2.0/dm-verity 是什么？&lt;/strong&gt;|
|A：就是启动验证。注意即使设备没有启动验证，这个选项也会显示。|
|&lt;strong&gt;Q：安装到 Recovery 是否要选？&lt;/strong&gt;|
|A：如果设备可以正常安装，就不要选。|
|&lt;strong&gt;Q：修补操作失败。&lt;/strong&gt;|
|A：请确保传入的镜像确实是从 BOOTIMG 分区提取的。|
|&lt;strong&gt;Q：如何确认传入的镜像是对的？&lt;/strong&gt;|
|A：安装界面中，若出现“Stock boot image detected”，说明原始镜像大概率没问题。|
|&lt;strong&gt;Q：需不需要保留原始的未修补镜像？&lt;/strong&gt;|
|A：一定要！如果你已经删了，请去回收站捞出来；如果回收站也没有，请再提取一次。|&lt;/p&gt;
&lt;h2&gt;刷入镜像&lt;/h2&gt;
&lt;p&gt;刷入镜像有三种方案，仅需要选择其中一个。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SP Flash Tool&lt;/code&gt; 由于前面已经使用过这个工具，你应该已经基本熟悉最基本的用法。且若设备没有启动验证，则这也是最快、最方便的方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Fastboot 直接刷入&lt;/code&gt; 若设备有启动验证，则需要进入 Fastboot 模式解锁。此时这是最方便的方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Fastboot 测试启动&lt;/code&gt; 也需要解锁 Bootloader。这是最稳妥的方法，因为你可以在测试镜像能否工作后再决定是否刷入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：如何判断设备是否有启动验证？&lt;/strong&gt;|
|A：原厂 Recovery 模式下可以判断，此处不介绍。你只须知道，Android 5.1 及以下版本不可能有启动验证，其他情况可一律当作有启动验证处理。|
|&lt;strong&gt;Q：刷入镜像是否会清空我的数据？&lt;/strong&gt;|
|A：解锁 Bootloader 的过程会清空数据，其余刷入操作均不会。但这时，你应该已经备份过重要数据了。|&lt;/p&gt;
&lt;h3&gt;SP Flash Tool 方式&lt;/h3&gt;
&lt;p&gt;自行判断系统是否有启动验证。如果有，参照&lt;a href=&quot;/blog/play-android/preintro-bootmode#fastboot&quot;&gt;这里&lt;/a&gt;重启进入 Fastboot 模式，并解锁 Bootloader。完成后，重启设备。重启设备后检查开发者选项中的“允许 OEM 解锁”开关，如果为开启状态且无法关闭，说明解锁成功，否则说明 SP Flash Tool 方式不能使用，应采用其他方式。&lt;/p&gt;
&lt;p&gt;选择合适的 SP Flash Tool 打开。&lt;/p&gt;
&lt;p&gt;在主界面选择“下载 DA”，选中 SP Flash Tool 目录下的 MTK_AllInOne_DA.bin；&lt;strong&gt;选择“配置文件”，选中上一步中保存的 txt 文件&lt;/strong&gt;，确保文件准确无误。下方将会显示分区表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/0579a9f3e8e75cd0a20d31836bf8f2de.BQVlWyM8_ZaFjGm.webp&quot; alt=&quot;&quot; title=&quot;[maxw=580]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;SP Flash Tool 主界面&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;下方的模式中选择“下载”。&lt;strong&gt;确保所有项目已经取消勾选，然后勾选 BOOTIMG（或 boot）分区&lt;/strong&gt;，点击右侧的“位置”单元格，&lt;strong&gt;选择修补后的镜像&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/423ab5141ebe24cc97fc6e7ddbf35f8c.3zQiv5fV_hPidg.webp&quot; alt=&quot;&quot; title=&quot;[maxw=600]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;镜像已加载&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;仔细核对复选框和镜像位置无误后，拔下 Android 设备，并将其关机。&lt;/p&gt;
&lt;p&gt;点击顶部的“下载”按钮，将关机的设备接入电脑。稍后，软件将开始操作。数秒后，将听到设备断开连接的声音。再过几秒，会弹出下载成功的提示框。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/6e37a2f5c4d08035ea55e5d11f2d8905.DM4PrIdS_1Txqky.webp&quot; alt=&quot;&quot; title=&quot;[maxw=580]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;下载成功&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;此时，拔下设备并开机。&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：“下载”是什么意思？&lt;/strong&gt;|
|A：下载即刷机，指的是将镜像从电脑传输到移动设备并刷入闪存中的过程。|
|&lt;strong&gt;Q：加载的镜像文件名是否一定要是 boot.img？&lt;/strong&gt;|
|A：可以不是。|
|&lt;strong&gt;Q：下载开始时出现奇怪的错误。&lt;/strong&gt;|
|A：你的设备可能有 Preloader 锁。请自行搜索 MTK Auth Bypass 尝试绕过，或知难而退，终止操作。|
|&lt;strong&gt;Q：设备插入时 SP Flash Tool 卡住/未响应。&lt;/strong&gt;|
|A：这是正常的，请等几秒。|
|&lt;strong&gt;Q：下载时界面左下角显示的下载速度直线下滑。&lt;/strong&gt;|
|A：这是速度计算机制的问题，请不要惊慌。速度不会降低到 0。|
|&lt;strong&gt;Q：下载时进度条会滚几次？&lt;/strong&gt;|
|A：下载时通常有三轮进度条，第一轮为红色，为 DA 下载阶段，时间约 1s；第二轮为黄色，为镜像下载阶段，时间约 3s；第三轮为灰色，为校验阶段，时间约 1s。若软件出现卡顿，有可能只能看到最后一轮，这是正常现象。|
|&lt;strong&gt;Q：镜像文件修补后比原来大会怎么样？&lt;/strong&gt;|
|A：这一般不会发生。如果真的这样，SP Flash Tool 会在加载镜像时报错，无须担心覆盖到其他分区上。|
|&lt;strong&gt;Q：由于 USB 接触不良，下载中道崩殂。&lt;/strong&gt;|
|A：先调整心态，不要惊慌。在 SP Flash Tool 上停止下载（如果已经出错就无须再停止），拔下数据线，长按电源键 3s（此时屏幕可能亮也可能不亮）。点击“下载”按钮，然后将设备插入电脑，再强制关机或重启设备，下载将重新开始。|
|&lt;strong&gt;Q：操作完成后，设备长按电源键无法开机。&lt;/strong&gt;|
|A：正确开机操作：拔掉数据线，长按电源键至少 3s；如果仍然无法开机，可能是 Preloader 卡住，请尝试强制重启。|
|&lt;strong&gt;Q：Bootloader 解锁后，正常开机显示 Orange State。&lt;/strong&gt;|
|A：这只是一个警告，纯属正常，可以忽略。|
|&lt;strong&gt;Q：有启动验证的设备上不解锁就刷会怎么样？&lt;/strong&gt;|
|A：设备会无限重启。刷回原镜像可以恢复。|
|&lt;strong&gt;Q：下载完成后，设备启动无限重启。&lt;/strong&gt;|
|A：请确保下载成功完成，如果不是，请重做一遍。如果问题仍然存在，尝试解锁 Bootloader。若 Bootloader 已解锁/无法解锁，或问题仍然存在，则说明系统与 Magisk 不兼容，请重新刷入修补前的镜像撤销更改。|
|&lt;strong&gt;Q：开机后发现系统时间异常或重置，是不是中了病毒？&lt;/strong&gt;|
|A：这是正常现象，不必惊慌。|&lt;/p&gt;
&lt;h3&gt;Fastboot 直接刷入方式&lt;/h3&gt;
&lt;p&gt;参照&lt;a href=&quot;/blog/play-android/preintro-bootmode#fastboot&quot;&gt;这里&lt;/a&gt;重启进入 Fastboot 模式，并解锁 Bootloader。&lt;/p&gt;
&lt;p&gt;然后执行 &lt;code&gt;fastboot flash boot &amp;#x3C;修补后的镜像文件&gt;&lt;/code&gt;，将其刷入。&lt;/p&gt;
&lt;p&gt;刷入成功后，执行 &lt;code&gt;fastboot reboot&lt;/code&gt; 重启。&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：Bootloader 解锁不成功。&lt;/strong&gt;|
|A：请确认设置中的“允许 OEM 解锁”开关已经打开，若确实已经打开，说明 Bootloader 无法解锁。此时请尝试不解锁状态下使用 SP Flash Tool 方案。|
|&lt;strong&gt;Q：刷入时出现错误（boot-magisk_patched.img is not a boot image）。&lt;/strong&gt;|
|A：这表明前面提取的镜像可能不对。请返回检查，不要立即尝试其他刷入方案。|
|&lt;strong&gt;Q：刷入时 USB 接触不良，中道崩殂。&lt;/strong&gt;|
|A：重新插上 USB 数据线，再次执行刷机命令。|
|&lt;strong&gt;Q：Bootloader 解锁后，正常开机显示 Orange State。&lt;/strong&gt;|
|A：这只是一个警告，纯属正常，可以忽略。|
|&lt;strong&gt;Q：刷入后，设备启动无限重启。&lt;/strong&gt;|
|A：这说明系统与 Magisk 不兼容，请重新刷入修补前的镜像撤销更改。|&lt;/p&gt;
&lt;h3&gt;Fastboot 测试启动方式&lt;/h3&gt;
&lt;p&gt;相比之下，这是最稳妥的方式，但是较旧的设备可能不支持测试启动。&lt;/p&gt;
&lt;p&gt;参照&lt;a href=&quot;/blog/play-android/preintro-bootmode#fastboot&quot;&gt;这里&lt;/a&gt;重启进入 Fastboot 模式，并解锁 Bootloader。&lt;/p&gt;
&lt;p&gt;然后执行 &lt;code&gt;fastboot boot &amp;#x3C;修补后的镜像文件&gt;&lt;/code&gt;。稍后，设备将启动进入系统。当前系统已经暂时具有“Magisk 试用版”，重启后将会复原。&lt;/p&gt;
&lt;p&gt;如果桌面上的 Magisk 是安卓机器人图标，且点击后显示“需要下载完整版本的 Magisk”，不要理会。直接将 Magisk 应用的 apk 文件传输到设备中重新安装（如果安装不成功，请卸载桌面上的 Magisk 图标后安装）。&lt;/p&gt;
&lt;p&gt;安装后，打开“Magisk App”，选择“安装”，不选中“保留 AVB 2.0/dm-verity”。此时将多出“直接安装”选项。确认后，Magisk 将永久安装到系统中。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/0a61ef47ce2e2e44d457a8c6375b6adb.Duqk40Vm_Z1w1Qbt.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/cb7d7e469a13bc56071c9187b1fea388.BR5K1qaA_Z1CEciL.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/d535fdc9ff45d0fffa3e94278ce1ea90.DTrn4_ac_ZgEfeH.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;完成后，立即重启设备。&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：Bootloader 解锁不成功。&lt;/strong&gt;|
|A：请确认设置中的“允许 OEM 解锁”开关已经打开，若确实已经打开，说明 Bootloader 无法解锁。此时请尝试不解锁状态下使用 SP Flash Tool 方案。|
|&lt;strong&gt;Q：测试启动时出现错误（boot-magisk_patched.img is not a boot image）。&lt;/strong&gt;|
|A：这表明前面提取的镜像可能不对，请返回检查，不要立即尝试其他刷入方案。|
|&lt;strong&gt;Q：测试启动不成功。&lt;/strong&gt;|
|A：此时说明系统与 Magisk 不兼容，不要尝试其他刷入方案。|
|&lt;strong&gt;Q：Bootloader 解锁后，正常开机显示 Orange State。&lt;/strong&gt;|
|A：这只是一个警告，纯属正常，可以忽略。|
|&lt;strong&gt;Q：启动后 Magisk App 检测不到 Magisk 版本，且无法直接安装。&lt;/strong&gt;|
|A：请确保刷入的是修补后的镜像。|&lt;/p&gt;
&lt;h2&gt;开机体验&lt;/h2&gt;
&lt;p&gt;如果设备成功开机，那么 Magisk 就安装完成了！此时请立即查看桌面上的图标。&lt;/p&gt;
&lt;p&gt;如果桌面上的 Magisk 是安卓机器人图标，且点击后显示“需要下载完整版本的 Magisk”，不要理会。直接将 Magisk 应用的 apk 文件传输到设备中重新安装（如果安装不成功，请卸载桌面上的 Magisk 图标后安装）。&lt;/p&gt;
&lt;p&gt;打开 Magisk App，将会看到 App 成功识别出了 Magisk 版本。屏幕底部会多出浮动工具条，主界面还会有“卸载 Magisk”按钮。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/0a61ef47ce2e2e44d457a8c6375b6adb.Duqk40Vm_Z1w1Qbt.webp&quot; alt=&quot;&quot; title=&quot;[maxw=240]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;Magisk 安装完成&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;现在，就可以安装需要 Root 的应用以及各种 Magisk 模块了。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Tip title=&quot;提示&quot;&gt;&lt;/p&gt;
&lt;p&gt;系统内已安装 Magisk，或 Magisk 应用能够获取 Root 权限时，会提供“直接安装”选项。后续更新 Magisk 可以采用这一方式，不需要连接电脑刷机。&lt;/p&gt;
&lt;p&gt;切勿用该方式降级安装 Magisk，这可能导致严重问题。要降级 Magisk，请先完全卸载，然后用刷机方式重新刷入。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Tip&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;$\large{\text{要不要重新锁定 Bootloader？}}$&lt;/p&gt;
&lt;p&gt;Bootloader 解锁后，设备将被认为是“不安全”状态。&lt;/p&gt;
&lt;p&gt;尽管如此，不建议安装后锁定 Bootloader，如果一定要锁定，请确保锁定前已经禁用启动验证（一般命令 &lt;code&gt;fastboot oem disable-verity&lt;/code&gt;）。否则一旦 boot 或 system 分区遭到修改，设备将无法启动。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;|快速故障排除|
|-|
|&lt;strong&gt;Q：启动后 Magisk App 检测不到 Magisk 版本。&lt;/strong&gt;|
|A：请确保刷入的是修补后的镜像。|&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;$\large{\text{禁用/隐藏/卸载 Magisk App 会怎么样？}}$&lt;/p&gt;
&lt;p&gt;Magisk 核心会正常工作，但无法进行授权和模块管理。&lt;/p&gt;
&lt;p&gt;Magisk 分为核心和 App 两部分。App 的作用是更新、管理与授权。如果 App 被移除或者禁用，Magisk 核心仍能工作，各种模块能正常启动。但是，没有 App 将无法进行 Root 授权，可能导致无法再获取 Root 权限。&lt;/p&gt;
&lt;p&gt;若要完全移除 Magisk，请点击原版 App 中“卸载 Magisk”按钮，选择“完全卸载”。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h2&gt;附：模块安装翻车的急救方法&lt;/h2&gt;
&lt;p&gt;Magisk 模块安装后，系统可能出现无法启动的问题，这称为“翻车”。&lt;/p&gt;
&lt;h3&gt;自动 Bootloop 恢复模块&lt;/h3&gt;
&lt;p&gt;特殊的 Bootloop 恢复模块（例如 &lt;a href=&quot;https://github.com/Magisk-Modules-Alt-Repo/Simple_BootloopSaver&quot;&gt;Bootloop Saver&lt;/a&gt;）可以检测系统在启动过程中的非正常重启。一旦检测到，该模块将禁用所有其他模块来尝试恢复系统。&lt;/p&gt;
&lt;p&gt;注意 Magisk Delta 的 25.2 及以上版本内置了 Bootloop 保护功能，只需要打开即可（26.4 及以上默认打开且不能关闭），不需要安装模块。&lt;/p&gt;
&lt;h3&gt;比较稳妥的安装方式&lt;/h3&gt;
&lt;p&gt;安装模块前，手边准备好电脑，打开 USB 调试，并进行“总是允许”的授权。随后开始模块安装。&lt;/p&gt;
&lt;p&gt;安装后立即重启。若出现“翻车”，请在电脑命令行上执行 &lt;code&gt;adb wait-for-device shell magisk --remove-modules&lt;/code&gt;，然后强制重启设备并马上插入电脑。系统开始初始化时，所有 Magisk 模块都将被移除，这样就可以正常启动了。&lt;/p&gt;
&lt;h3&gt;第三方 Recovery&lt;/h3&gt;
&lt;p&gt;TWRP 等第三方 Recovery 系统具有文件管理功能。删除 &lt;code&gt;/data/adb/modules/&lt;/code&gt; 下的模块文件，即可修复。&lt;/p&gt;
&lt;h3&gt;安全模式&lt;/h3&gt;
&lt;p&gt;将设备启动进入&lt;a href=&quot;/blog/play-android/preintro-bootmode#bootmode&quot;&gt;安全模式&lt;/a&gt;（并不是所有设备都有这一功能），Magisk 将禁用所有模块。&lt;/p&gt;
&lt;h3&gt;安全模式，重刷 Magisk&lt;/h3&gt;
&lt;p&gt;若你的设备只能从开机状态下重启进入安全模式，而不能直接进入，参考此方法。&lt;/p&gt;
&lt;p&gt;使用 SP Flash Tool 刷入未经修补的 boot.img。重启设备，Magisk 将被移除。&lt;/p&gt;
&lt;p&gt;设备正常启动后，在电脑上打开 SP Flash Tool，加载已修补的 boot.img，点击“下载”按钮。&lt;/p&gt;
&lt;p&gt;接下来，长按电源键，然后长按弹出菜单中的“关机”，选择重启进入安全模式。随后立即将设备接入电脑。刷机完成后，设备将自动启动进入安全模式（若没有自动启动，请拔下数据线并手动开机），此时 Magisk 将禁用所有模块。&lt;/p&gt;
&lt;h3&gt;删除模块，重刷 Magisk&lt;/h3&gt;
&lt;p&gt;使用 SP Flash Tool 或 Fastboot 刷入未经修补的 boot.img。重启设备，Magisk 将被移除。&lt;/p&gt;
&lt;p&gt;接下来打开 USB 调试，进入 &lt;code&gt;adb shell&lt;/code&gt;，直接切到 &lt;code&gt;/data/adb/modules/&lt;/code&gt; 目录下并使用命令删除模块文件。&lt;/p&gt;
&lt;p&gt;再次刷入修补后的 boot.img，即可恢复 Magisk。&lt;/p&gt;
&lt;h2&gt;附：系统更新的流程&lt;/h2&gt;
&lt;p&gt;打开 Magisk App，查看主页上的 A/B 参数，即可确认系统更新方式。&lt;/p&gt;
&lt;h3&gt;一般系统更新&lt;/h3&gt;
&lt;p&gt;此类更新要先将所有镜像还原，然后更新，再重新安装 Magisk。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;还原原厂镜像
&lt;ul&gt;
&lt;li&gt;🗑️ 卸载 Magisk：还原原厂镜像&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;重刷原厂系统
&lt;ul&gt;
&lt;li&gt;⚡ 若系统有被修改，重新刷入原厂系统&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;更新系统
&lt;ul&gt;
&lt;li&gt;🔧 安装系统更新&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;提取原厂镜像
&lt;ul&gt;
&lt;li&gt;🗒️ 打开 SP Flash Tool，设置好回读分区&lt;/li&gt;
&lt;li&gt;💿 关机后，插入设备开始回读&lt;/li&gt;
&lt;li&gt;📀 顺便备份好系统分区&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;修补镜像
&lt;ul&gt;
&lt;li&gt;🩹 利用 Magisk App 修补镜像&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;刷入镜像
&lt;ul&gt;
&lt;li&gt;⭕ 启动进入 Fastboot&lt;/li&gt;
&lt;li&gt;⚡ 命令刷入新的镜像&lt;/li&gt;
&lt;li&gt;⭕ 命令重启&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;开机体验
&lt;ul&gt;
&lt;li&gt;🔰 重新安装 Magisk App&lt;/li&gt;
&lt;li&gt;✅ 重新启用已安装的模块&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;A/B 无缝系统更新&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;还原原厂镜像
&lt;ul&gt;
&lt;li&gt;🗑️ 卸载 Magisk：还原原厂镜像&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;重刷原厂系统
&lt;ul&gt;
&lt;li&gt;⚡ 若系统有被修改，重新刷入原厂系统&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;更新系统
&lt;ul&gt;
&lt;li&gt;🔧 安装系统更新，但不要重启&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;安装到新的系统
&lt;ul&gt;
&lt;li&gt;🩹 安装 Magisk 到未使用的槽位&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;重启体验
&lt;ul&gt;
&lt;li&gt;⭕ 重启设备&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;#x3C;F.Muted&gt;封面图来自网络，未经明确授权。&amp;#x3C;/F.Muted&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/hero.CuZalr8M.png"/><enclosure url="/_astro/hero.CuZalr8M.png"/></item><item><title>[基础预科] Android 分区、启动模式、Fastboot</title><link>https://ak-ioi.com/blog/play-android/preintro-bootmode</link><guid isPermaLink="true">https://ak-ioi.com/blog/play-android/preintro-bootmode</guid><description>电脑上， 硬盘有多个分区，启动时有不同的启动选项，在 Android 上亦然。本文是关于 Android 分区、启动模式和 Fastboot 模式使用方法的预科内容。</description><pubDate>Sat, 08 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import F from &apos;@/components/mdx/formatting&apos;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Android 分区&lt;/code&gt; 与电脑硬盘相同，Android 上的存储器同样有多个分区（但是，建议不要在 Android 上尝试“手动切盘”）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;启动模式&lt;/code&gt; 在 Windows 电脑上，我们知道，启动时按 Enter/F2/F12 会打断正常启动，且可以进入另一个世界。在 Android 设备上，同样有非正常启动的模式。Recovery 模式可以刷入官方刷机包修复受损的系统，还可以清除数据；Fastboot 模式在解锁后将允许用户刷入自定义的镜像，实现系统的定制和 Root 权限的获取。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Fastboot&lt;/code&gt; Fastboot 模式是 Bootloader 的一个启动方式。Bootloader 解锁后，可以使用 Fastboot 模式自定义刷机。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;时效性复核状态&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文章现在处于待复核状态，近期可能出现较大改动。&lt;/strong&gt;&lt;br&gt;
此文章的发布时间是 2022 年第一季度。&lt;br&gt;
请留意内容的时效性。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;本文不是教程，你不需要亲自尝试其中的每一个操作。如果你不知道自己在做什么，那就不要做。&lt;br&gt;
若阅读本文并执行相关操作后出现设备损坏、隐私泄露、数据丢失等情况，则机主需要自行承担损失。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;h2&gt;Android 文件系统分区&lt;/h2&gt;
&lt;h3&gt;引入&lt;/h3&gt;
&lt;p&gt;打开系统自带的文件管理器，我们可以看到许多文件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/41b2f32bc929b3380be97a4e1abfbd0d.BWAcpo4H_ZaDJwS.webp&quot; alt=&quot;&quot; title=&quot;[maxw=280]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;文件管理器（就假装是系统自带的那个）&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;其中有应用的数据，有磁盘缓存，有照片录音，有接收的文件，甚至有一些被称为“系统文件”，千万不能删除的东西……&lt;/p&gt;
&lt;p&gt;Android 很好，“毫无保留”地展示了用户的数据，不像苹果，导出照片都要使用专门的工具。这，应该就是系统中的全部文件了吧。&lt;/p&gt;
&lt;p&gt;坐井观天！&lt;/p&gt;
&lt;p&gt;你可曾想过，你安装的应用去了哪里？那些你痛恨而又“不能删除”的预装软件在哪？那些“不能修改”的游戏进度数据在哪？有那么多人想要获取 Root 权限，他们究竟想要修改什么？&lt;/p&gt;
&lt;p&gt;你又可曾注意过，~~百度手机助手~~提醒你文件已下载到 &lt;code&gt;/storage/emulated/0/Downloads/&lt;/code&gt;，而不是 &lt;code&gt;Downloads/&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;其实，打开文件管理器后看到的，根本就不是系统内所有文件，而只是“用户数据”部分，只是“用户数据”部分的冰山一角！&lt;/p&gt;
&lt;p&gt;不妨安装 Root Explorer 文件管理器，一探究竟。&lt;a href=&quot;https://wcl.sparkslab.art/index.php?share/fileDownload&amp;#x26;sid=KEQBAzId&amp;#x26;user=4&amp;#x26;path=%2FAndroidApps%2F%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86%E5%99%A8.ver.4.9.6.build.999496.apk&quot;&gt;【Root Explorer 下载】&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;原来，之前的文件管理器中所看到的目录，只是 &lt;code&gt;/storage/emulated/0&lt;/code&gt;。真正的最高层目录，是系统根目录。还有许多 &lt;code&gt;.&lt;/code&gt; 开头的文件，系统自带的文件管理器是不显示的。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;若设备有 Root 权限，在明确自己要做什么之前，不要将其授予文件管理器。对受保护文件不当的修改会阻止设备正常启动或使用。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow maxWidth=&apos;600px&apos;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/7eab4b3a52fc4c0a06a793058e718d16.Bw78X7An_kvjae.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/19cea75c3fcda4dbca089e5cf8ec0a18.C9Yg710S_ZgP7Vx.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;存储与根目录&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;~~当然，你更有可能看到这个：~~&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/0599960cbf669f63f27e6e522f983d0a.BX6fL8h1_Z1YfOBk.webp&quot; alt=&quot;&quot; title=&quot;[maxw=300]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;Android 7 以上版本，根目录部分内容出现拒绝访问导致打开失败&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;进一步观察不难发现，不同的目录可能有不同的文件系统类型：&lt;code&gt;/system&lt;/code&gt; &lt;code&gt;/vendor&lt;/code&gt; 目录有独立的存储空间；&lt;code&gt;/data&lt;/code&gt; 的存储空间与 &lt;code&gt;/storage/emulated/0&lt;/code&gt; 共享；&lt;code&gt;/sys&lt;/code&gt; 本身就是可读写区域，且没有存储空间限制信息；根目录下大多数地方创建的文件重启后会消失……这意味着，根目录下不仅仅是简单的文件夹和文件，而是有不同的分区。&lt;/p&gt;
&lt;h3&gt;分区与挂载&lt;/h3&gt;
&lt;p&gt;在 Windows 上，我们可以将硬盘分为多个分区。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/18254ff8d0993a3faa4e22f704358191.CpsYu1Ph_Z1X1FvC.webp&quot; alt=&quot;&quot; title=&quot;[maxw=500]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;硬盘分区&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;要访问分区中的文件，有两种方式。一种是指派盘符（新分区产生后，系统会自动完成这一步，除非所有大写字母已经分配完），然后通过盘符访问（如 &lt;code&gt;G:\xxx&lt;/code&gt;），这种方式下，可移动外接设备中文件的路径可能会改变。&lt;/p&gt;
&lt;p&gt;另一种不太常用，是将磁盘分区装入空白 NTFS 文件夹中（如 &lt;code&gt;E:\mnt\可回收垃圾\xxx&lt;/code&gt;）。也就是说，在另一个已分配盘符的分区中新建文件夹，将该文件夹作为访问该磁盘分区的路径。若对可移动设备采取这种分配方式，则其中文件的路径将固定下来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/b32e05036e5f63f90f628ddeb5d9f2f8.Bl5H1EmP_Z1Jgftp.webp&quot; alt=&quot;&quot; title=&quot;[maxw=450]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;固定分配的文件夹路径&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;Android 基于 Linux 内核。在 Linux 内，没有盘符这一概念，只有唯一的根目录 &lt;code&gt;/&lt;/code&gt;。一切分区（包括外接存储设备）均以装入空文件夹的方式装载到某一目录。这一操作称为&lt;strong&gt;挂载&lt;/strong&gt;(mounting)。&lt;/p&gt;
&lt;p&gt;同时，挂载也可以被看成是打开分区的操作，因而有不同的打开方式，常见的有只读（&lt;code&gt;r/o&lt;/code&gt;，不能写入）、读写（&lt;code&gt;r/w&lt;/code&gt;）和装入内存盘（可以写入，断电或重启时丢弃更改）。&lt;/p&gt;
&lt;p&gt;值得一提的是，由于 Linux 中每个设备都有对应的文件路径，被挂载的分区也有自己的路径（如 &lt;code&gt;/dev/block/sda1&lt;/code&gt; &lt;code&gt;/dev/block/mmcblk0p6&lt;/code&gt; &lt;code&gt;/emmc@android&lt;/code&gt;）。这些路径在文件管理器中表现为一个文件。挂载前，无法直接修改其中的数据。&lt;/p&gt;
&lt;h3&gt;常见的分区&lt;/h3&gt;
&lt;p&gt;不同的 Android 设备具有不同的分区表，其具体分区也不太一样。此处列举一些常见的分区。&lt;/p&gt;
&lt;p&gt;|分区名|Scatter 名称|镜像文件名|挂载模式|挂载路径|用途|
|-|-|-|-|-|-|
|boot|BOOTIMG|&lt;code&gt;boot.img&lt;/code&gt;|-|-|启动镜像[注1]|
|kenrel|-|&lt;code&gt;kenrel.img&lt;/code&gt;|-|-|系统内核|
|rootfs|-|&lt;code&gt;rootfs.img&lt;/code&gt;|&lt;code&gt;r/o&lt;/code&gt;|&lt;code&gt;/&lt;/code&gt;|根目录自有的文件|
|ramdisk|-|&lt;code&gt;ramdisk.img&lt;/code&gt;|&lt;code&gt;r/o&lt;/code&gt;|&lt;code&gt;/init&lt;/code&gt;|启动时最先挂载的一批程序文件[注2]|
|system|ANDROID|&lt;code&gt;system.img&lt;/code&gt;|&lt;code&gt;r/o&lt;/code&gt;|&lt;code&gt;/system&lt;/code&gt;|系统分区|
|recovery|RECOVERY|&lt;code&gt;recovery.img&lt;/code&gt;|-|-|恢复模式启动镜像[注3]|
|hboot|UBOOT|&lt;code&gt;lk.bin&lt;/code&gt;|-|-|启动引导程序|
|splash1|LOGO|&lt;code&gt;logo.bin&lt;/code&gt;|-|-|开机第一屏幕|
|nvram|NVRAM|&lt;code&gt;nvram.bin&lt;/code&gt;|&lt;code&gt;RAM&lt;/code&gt;|-|MAC 地址等硬件信息[注4]|
|cache|CACHE|&lt;code&gt;cache.img&lt;/code&gt;|&lt;code&gt;r/w&lt;/code&gt;|&lt;code&gt;/cache&lt;/code&gt;|缓存|
|userdata|USERDATA|&lt;code&gt;data.img&lt;/code&gt;|&lt;code&gt;r/w&lt;/code&gt;|&lt;code&gt;/data&lt;/code&gt;|用户数据|
|-|-|&lt;code&gt;sdcard.img&lt;/code&gt;|&lt;code&gt;r/w&lt;/code&gt;|&lt;code&gt;/sdcard&lt;/code&gt;|内置存储空间[注5]|&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;boot.img&lt;/code&gt; 是 &lt;code&gt;kenrel.img&lt;/code&gt; &lt;code&gt;ramdisk.img&lt;/code&gt; &lt;code&gt;rootfs.img&lt;/code&gt; 打包在一起的产物。较老的机型中，这几个分区是分开的。&lt;/li&gt;
&lt;li&gt;不是每个机型都存在 ramdisk。存在该分区的设备才能正常使用 Magisk。&lt;/li&gt;
&lt;li&gt;recovery 分区和 boot 分区结构相同。&lt;/li&gt;
&lt;li&gt;nvram 分区不一定存在，但开机后 NVRAM 一定存在。&lt;/li&gt;
&lt;li&gt;sdcard 应当是 userdata 的一部分，路径为 &lt;code&gt;/data/media/0&lt;/code&gt;（&lt;code&gt;/sdcard/Android/obb&lt;/code&gt; 例外，对应 &lt;code&gt;/data/media/obb&lt;/code&gt;）。只有极少数模拟器中，内置 sdcard 有独立分区。挂载路径 &lt;code&gt;/sdcard&lt;/code&gt; 为快捷方式，实际路径应该是 &lt;code&gt;/storage/emulated/0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt; &lt;code&gt;/sys&lt;/code&gt; 下的自有内容从 rootfs 分区内读出。对其进行的修改会存储于内存（运行内存）中，故重启后会还原。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;$\large{\text{什么是恢复出厂设置？}}$&lt;/p&gt;
&lt;p&gt;正常情况下，大多数分区保持只读且为原厂状态。所谓恢复出厂设置，只是格式化 userdata 分区清除使用痕迹。&lt;/p&gt;
&lt;p&gt;恢复出厂设置不能还原通过刷机、Root 等手段对 system、boot 分区进行的更改。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h2&gt;启动模式&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;尝试任何非正常启动模式前，请确认自己知道如何强制关机或重启设备，并且已经尝试成功过。常见方法有这些：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拔掉充电器，然后拆除内置电池，将会关机。（适用于电池可拆卸的设备）&lt;/li&gt;
&lt;li&gt;长按电源按钮 10s，强制重启。（适用于大多数设备）&lt;/li&gt;
&lt;li&gt;同时按住电源按钮和音量下按钮，保持 10s，强制重启。（主要为三星设备）&lt;/li&gt;
&lt;li&gt;同时按住电源按钮和两个音量按钮，待屏幕熄灭后立刻放开，即为强制关机。（适用于大多数设备）&lt;/li&gt;
&lt;li&gt;同时按住电源按钮和音量上按钮，待屏幕熄灭后立刻放开，即为强制关机。（适用于一加手机）&lt;/li&gt;
&lt;li&gt;用细小物体（建议取卡针）戳麦克风孔（或电源按钮附近的一个孔），触发其中的开关。（适用于学习机、媒体播放器、移动数据终端等设备）
提示：不要用缝衣针的尖端戳麦克风孔，以免损坏麦克风。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;h3&gt;引入&lt;/h3&gt;
&lt;p&gt;电脑系统炸了，我们会插入启动盘，进入 Windows PE 或便携版 Linux 系统，执行修复。&lt;/p&gt;
&lt;p&gt;要为电脑安装新的系统，我们会插入安装介质，从启动菜单中选择启动选项，安装新的系统。&lt;/p&gt;
&lt;p&gt;在 Android 设备中，同样存在几种非正常启动的启动模式。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Tip title=&quot;提示&quot;&gt;&lt;/p&gt;
&lt;p&gt;本节中的图像可能并非真实截图或照片，仅供参考。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Tip&gt;&lt;/p&gt;
&lt;h3&gt;安全模式&lt;/h3&gt;
&lt;p&gt;若系统仍能进入，但由于装错程序或设置失误导致无法正常使用，则可以使用安全模式恢复。下面是常见情形：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用第三方锁屏软件出现事故。&lt;/li&gt;
&lt;li&gt;设备安装恶意程序后被锁住。&lt;/li&gt;
&lt;li&gt;正常安装软件或更改系统设置后出现无限重启的故障。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;安全模式的常见启动方法有两个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若系统界面仍能使用，则在开机状态下长按电源键，然后长按弹出菜单中的“关机”（8.0 以上系统也可以长按“重新启动”）。系统将弹出安全模式提示。点击“确定”即可重启进入。&lt;/li&gt;
&lt;li&gt;关机状态下长按电源键，待屏幕亮起后，放开电源键，立即按住音量下，直到系统开始初始化（开机动画开始出现）。系统将继续启动进入安全模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据机型不同，安全模式有以下两种不同的效果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有第三方应用被暂时停用。系统设置暂时恢复默认值，壁纸变为黑色，左下角出现“安全模式”字样。&lt;/li&gt;
&lt;li&gt;系统初始化时所有第三方应用停用，应用优化阶段重新启用。第三方应用不能开机自启。壁纸不变，且左下角无“安全模式”字样。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有的设备可能不具备这一功能。&lt;/p&gt;
&lt;p&gt;值得注意的是，若设备上安装了 Magisk，则以安全模式启动时，Magisk 会禁用所有模块（包括 MagiskHide）。若安全模式期间打开过 Magisk 应用，则再次重启后，Magisk 将不会恢复被禁用的模块。&lt;/p&gt;
&lt;h3&gt;Recovery 恢复模式&lt;/h3&gt;
&lt;p&gt;恢复模式，顾名思义，是系统的一个备用启动模式，用于恢复系统。在系统无 A/B 分区（无缝系统更新）的情况下，系统更新也在这一模式下进行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/a08a756c9a22664a0c764b5027708d62.Cw9Jma4L_Z2sFsaR.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;常见的原厂恢复模式&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;通常，原厂的恢复模式具有以下功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从外置 SD 卡或 ADB 传入系统更新包，完成系统更新（系统损坏或 OTA 更新事故时，可以传入全量更新包恢复系统）。
为了确保设备安全，更新包必须是原厂的，经过原厂签名，否则更新无法成功。&lt;/li&gt;
&lt;li&gt;清除缓存。&lt;/li&gt;
&lt;li&gt;清除数据（双清），以恢复出厂设置。如果安全模式无法修复系统问题，则这是保全手机的一个备用方案。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;若恢复出厂设置也无法修复系统的问题，说明 boot/system 等系统分区遭到破坏。此时需要使用恢复模式进行系统更新。&lt;/p&gt;
&lt;p&gt;确实存在部分设备，“清除数据”功能能够同时清除用户数据和锁屏密码。这种情况下锁屏密码只能保护数据，而不能保护设备。而其他设备中，处于锁定状态时，恢复模式中不会显示恢复出厂设置选项。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;常见进入恢复模式的方法如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在开机状态下，&lt;a href=&quot;/blog/play-android/preintro-adb#reboot-device&quot;&gt;使用 ADB 发送命令&lt;/a&gt; &lt;code&gt;adb reboot recovery&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;关机状态下，同时按住电源键和音量增大（有的机型还需要按实体 Home 键），直到屏幕亮起。&lt;/li&gt;
&lt;li&gt;[MediaTek 设备] 关机状态下，同时按住电源键和音量增大，屏幕亮起后，按音量增大键选择 Recovery Mode，然后按音量减小键确认。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以通过刷机等手段为设备刷入第三方恢复模式系统，如 TWRP。这些系统功能非常强大，允许自定义刷机、文件修改、完全备份等操作，同时支持触屏控制，更加人性化。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;不建议在主力机上刷入第三方恢复模式系统。这会使得拿到设备的任何人能够直接导出设备内的数据或进行自定义刷机，彻底破坏设备安全性。&lt;/p&gt;
&lt;p&gt;TWRP 需要专门与某一机型适配后才能使用，故不要借用其他机型的 TWRP 镜像。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;h3&gt;Fastboot 快速启动&lt;/h3&gt;
&lt;p&gt;快速启动，是 Bootloader 即启动引导程序的一个启动方式，故没有对应的启动分区。进入该模式后，移动设备无法继续操作，需要连接电脑，通过电脑命令行完成进一步操作。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;若未收到电脑发出的重启指令，设备将一直处于 Fastboot 模式，不会重启。可以使用强制重启或关机的方法让其退出 Fastboot 模式。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;为了保证安全性，Bootloader 默认为锁定状态，不能进行刷机。有的设备制造商为了阻止或减少刷机，会另外增加解锁码限制，用户需要提供申请得到的解锁码才能解锁。&lt;/p&gt;
&lt;p&gt;如果打算进行刷机（而不是仅仅看看 Fastboot 长啥样），在进入 Fastboot 模式前，请先&lt;a href=&quot;/blog/play-android/preintro-adb#dev-mode&quot;&gt;进入系统设置的开发者选项&lt;/a&gt;，检查是否有“允许 OEM 解锁”选项（该选项通常在第一组内较靠前的位置）。如果有，这表明你的设备不需要解锁码就可以解锁，那么开启该选项，然后按提示输入设备的锁屏密码，随后进入 Fastboot 模式就可以直接解锁；如果没有，表明设备可能需要解锁码。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;打开“允许 OEM 解锁”开关后，系统的数据加密将失效。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;进入 Fastboot 的常见方式如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在开机状态下，&lt;a href=&quot;/blog/play-android/preintro-adb#reboot-device&quot;&gt;使用 ADB 发送命令&lt;/a&gt; &lt;code&gt;adb reboot bootloader&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;关机状态下，同时按住电源键和音量减小（有的机型还需要按实体 Home 键），直到屏幕亮起。&lt;/li&gt;
&lt;li&gt;[MediaTek 设备] 关机状态下，同时按住电源键和音量增大，屏幕亮起后，按音量增大键选择 Fastboot Mode，然后按音量减小键确认。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;三星设备中，Download Mode 取代了 Fastboot。可以使用官方工具 Odin 进行刷机。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;设备成功进入 Fastboot 模式后，将显示 Fastboot 字样。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgRow&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/ddd5ea2499e3a0b5753928868a596d3c.CFowf3A8_1V5Ef1.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/681f0bcb6bd86108217c25819cc1a7e7.C3tHLLFv_13g6Dt.webp&quot; alt=&quot;&quot;&gt;
&lt;img src=&quot;https://ak-ioi.com/_astro/4dc4107942e5600f5af3ccb847ce07c1.CCbS1kf5_zVJIY.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;/F.ImgRow&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;常见的 Fastboot 界面&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;确保已&lt;a href=&quot;/blog/play-android/preintro-adb#install-adb&quot;&gt;安装 Android Platform Tools&lt;/a&gt;。随后执行 &lt;code&gt;fastboot devices&lt;/code&gt; 命令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;C:\Users\[redacted]&gt;fastboot devices
0123456789ABCDEF        fastboot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;$\large{\text{上述命令无输出的解决方法}}$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开 Windows 设备管理器。&lt;a href=&quot;https://cn.bing.com/search?q=Windows+%E8%AE%BE%E5%A4%87%E7%AE%A1%E7%90%86%E5%99%A8&quot;&gt;【搜索“Windows 设备管理器”】&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;找到无法识别的 “Android” 设备，右键，选择“更新驱动程序”。
&lt;img src=&quot;https://ak-ioi.com/_astro/a463fd7bd3b6112ad0e9b5d592ad249d.CcdzsxT7_Pg4ET.webp&quot; alt=&quot;&quot;&gt;&lt;/li&gt;
&lt;li&gt;选择“浏览我的电脑以查找驱动程序”“让我从计算机的可用驱动程序列表中选取”。&lt;/li&gt;
&lt;li&gt;选择 Android Composite ADB Interface。
&lt;img src=&quot;https://ak-ioi.com/_astro/899bb798ba9c71f84aa28197132bc2c1.CaKWcvWb_Z1wQvgr.webp&quot; alt=&quot;&quot; title=&quot;[maxh=400]&quot;&gt;
如果这一步系统找不到驱动程序，则系统存在 ADB 驱动缺失问题。请&lt;a href=&quot;https://cn.bing.com/search?q=adb+%E9%A9%B1%E5%8A%A8%E7%BC%BA%E5%A4%B1&quot;&gt;搜索“adb 驱动缺失”&lt;/a&gt;，然后像安装 ADB 驱动一样安装 Fastboot 的驱动。&lt;/li&gt;
&lt;li&gt;完成后，将看到设备为可用状态。
&lt;img src=&quot;https://ak-ioi.com/_astro/10daec1a01797c254f88a9a0b98071c3.Dn0abok6_1uIXWJ.webp&quot; alt=&quot;&quot;&gt;&lt;/li&gt;
&lt;li&gt;重新执行 &lt;code&gt;fastboot devices&lt;/code&gt;，将能够看到设备。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;随后可以进行刷机操作。&lt;/p&gt;
&lt;h4&gt;Bootloader 解锁&lt;/h4&gt;
&lt;p&gt;&amp;#x3C;F.Danger title=&quot;危险&quot;&gt;&lt;/p&gt;
&lt;p&gt;为防止未经授权的用户直接解锁 Bootloader，然后用刷机方式移除锁屏，获取数据，Bootloader 解锁后，&lt;strong&gt;用户数据将被清空&lt;/strong&gt;。请做好备份，并谨慎操作。&lt;/p&gt;
&lt;p&gt;Bootloader 解锁后，&lt;strong&gt;设备保修可能会失效&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Danger&gt;&lt;/p&gt;
&lt;p&gt;输入命令 &lt;code&gt;fastboot oem unlock&lt;/code&gt;[5.1 及以下] 或 &lt;code&gt;fastboot flashing unlock&lt;/code&gt;[6.0 及以上] 执行解锁。此时，设备上会出现确认界面。请&lt;strong&gt;认真阅读所有文字&lt;/strong&gt;，然后按音量增大键确认，或按音量减小键取消。&lt;/p&gt;
&lt;p&gt;确认后观察电脑上的输出。&lt;code&gt;OKAY&lt;/code&gt; 则为解锁成功。&lt;/p&gt;
&lt;p&gt;完成后，输入命令 &lt;code&gt;fastboot oem lks&lt;/code&gt; 再次确认。若输出状态 &lt;code&gt;lks=0&lt;/code&gt;，则解锁成功。（注意较老设备可能没有 &lt;code&gt;oem lks&lt;/code&gt; 命令，此时请自行跳过这一步）&lt;/p&gt;
&lt;p&gt;若解锁不成功，则设备可能是需要解锁码，请查看制造商官网的指引进行申请，或直接使用官方提供的工具解锁。若官方明确不提供解锁服务，&lt;strong&gt;请知难而退&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一旦解锁成功，再次重启后，将出现数据清空页面。数据清空完成后，设备再次重启，进入系统。&lt;/p&gt;
&lt;p&gt;由于解锁后，未经授权的用户可以随意进行自定义刷机，设备是不安全的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/7b5de4733caa8c5408aaf90d74b3ec0b.kOyCmBmq_Z1eSRYX.webp&quot; alt=&quot;&quot; title=&quot;[maxh=400]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;MTK 设备解锁后开机弹出的警告&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;h4&gt;刷入镜像&lt;/h4&gt;
&lt;p&gt;&amp;#x3C;F.Danger title=&quot;危险&quot;&gt;&lt;/p&gt;
&lt;p&gt;自定义刷机有风险，尤其是刷入非官方镜像时。此处不当操作，甚至可以破坏 Bootloader 本身，导致 Fastboot 模式无法进入。出现这种情况的设备称为“黑砖”（Hardbrick）。&lt;/p&gt;
&lt;p&gt;黑砖设备可以用 EDL（高通）或 Preloader（MTK）进行抢救，但尽量不要弄到这一步。&lt;/p&gt;
&lt;p&gt;刷机前，若可能，强烈建议前往官网下载全量系统更新包。若刷机弄坏系统，可以重新刷入全量包修复。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Danger&gt;&lt;/p&gt;
&lt;p&gt;执行命令 &lt;code&gt;fastboot flash &amp;#x3C;分区名&gt; &amp;#x3C;镜像文件&gt;&lt;/code&gt; 刷入。常见分区名见上文。&lt;/p&gt;
&lt;p&gt;执行命令 &lt;code&gt;fastboot boot &amp;#x3C;镜像文件&gt;&lt;/code&gt;，可以将镜像暂时作为 boot 分区启动，这常用来测试 boot.img 镜像能否正常引导。重启后将会还原原来的 boot 分区。（注意较老设备可能没有这一命令）&lt;/p&gt;
&lt;p&gt;Fastboot 模式不能回读系统中当前的镜像。&lt;/p&gt;
&lt;h4&gt;重新启动&lt;/h4&gt;
&lt;p&gt;执行 &lt;code&gt;fastboot reboot&lt;/code&gt;，设备将立即重新启动。&lt;/p&gt;
&lt;h4&gt;锁定 Bootloader&lt;/h4&gt;
&lt;p&gt;如果只是刷入第三方系统，后续无再次刷机与深度玩机的需求，在测试新系统无问题后，可以锁定 Bootloader，使设备重新变得安全。&lt;/p&gt;
&lt;p&gt;如果新的系统中有 Magisk 等 Root 权限装置，则在锁定前，运行 &lt;code&gt;fastboot oem disable-verity&lt;/code&gt;，关闭启动与系统分区的验证，以免对其修改后，验证失败导致设备无法启动。&lt;/p&gt;
&lt;p&gt;然后运行 &lt;code&gt;fastboot oem lock&lt;/code&gt;。设备将出现确认界面，按音量增大确认。&lt;strong&gt;这同样会清空数据&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;完成后，执行 &lt;code&gt;fastboot reboot&lt;/code&gt;，设备将立即重新启动。&lt;/p&gt;
&lt;h3&gt;EDL 紧急下载&lt;/h3&gt;
&lt;p&gt;此模式为高通芯片的设备所独有。&lt;/p&gt;
&lt;p&gt;设备成为黑砖后，此模式仍然可以使用。&lt;/p&gt;
&lt;p&gt;通过插入特殊的“工程线”，设备将进入 EDL 紧急下载模式（也称高通 9008 模式）。此时，可以用 fastboot 或专门的工具进行刷机，以修复系统。此处不过多介绍相关内容。&lt;/p&gt;
&lt;p&gt;刷机过程中，屏幕全程无显示。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;此模式是“厂刷”模式之一，能操作系统底层数据。正常情况下，请不要使用这一模式刷机。
若迫不得已，请务必谨慎操作。一旦误删设备 IMEI、基带校准等数据，通常只能返厂维修。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;h3&gt;MediaTek Preloader&lt;/h3&gt;
&lt;p&gt;此模式为 MTK 设备所独有。&lt;/p&gt;
&lt;p&gt;将设备在关机状态下插入电脑，从提示音可以发现系统短暂地识别了一个 USB 设备。这就是 Preloader 模式。设备成为黑砖后，此模式仍然可以使用。&lt;/p&gt;
&lt;p&gt;Preloader 是闪存中的一个程序。若这个程序也被破坏，则连接数据线时，设备将改用无法被破坏的 Bootrom 模式启动，两者区别不大。&lt;/p&gt;
&lt;p&gt;SP Flash Tool 可以在设备进入 Preloader 模式后与其交互，进行刷机。此时 Preloader 模式将保持到刷机操作结束。&lt;/p&gt;
&lt;p&gt;该模式下，也&lt;strong&gt;可以回读&lt;/strong&gt;闪存中已存在的镜像。这是最直接的系统镜像备份方法。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;此模式是“厂刷”模式之一，能操作系统底层数据。&lt;strong&gt;如果系统为 5.1 及以下&lt;/strong&gt;，则更推荐用这种方法进行局部刷机，因为这不会清空数据。&lt;br&gt;
若系统为 6.0 及以上，则不解锁 Bootloader 的情况下这样刷机可能导致无法启动，重新刷回原来的镜像即可修复。&lt;br&gt;
使用时请务必谨慎操作。一旦误删设备 IMEI、基带校准等数据而没有备份，通常只能返厂维修。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/hero.CxUhvkh1.png"/><enclosure url="/_astro/hero.CxUhvkh1.png"/></item><item><title>[基础预科] ADB、Android 终端、Android 用户权限</title><link>https://ak-ioi.com/blog/play-android/preintro-adb</link><guid isPermaLink="true">https://ak-ioi.com/blog/play-android/preintro-adb</guid><description>Android 是一个相对开放的系统，有着许多隐藏的可能性等待挖掘。本文是对 ADB 相关内容的预科。</description><pubDate>Sat, 18 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import F from &apos;@/components/mdx/formatting&apos;&lt;/p&gt;
&lt;p&gt;Android 是一个相对开放的系统，有着许多隐藏的可能性等待挖掘。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ADB&lt;/code&gt; ADB 全称 Android Debug Bridge，即安卓调试桥。使用 ADB，可以用电脑进行文件传输、应用安装（无需手机端反复点击确认）、执行终端命令等操作。&lt;a href=&quot;https://developer.android.google.cn/studio/command-line/adb?hl=zh-cn#dpm&quot;&gt;【Android 官方的 ADB 文档】&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Android 终端&lt;/code&gt; Android 基于 Linux 系统的内核，因此继承了 Linux 的终端。使用终端命令，可以进行修改分辨率、禁用/启用应用（包括系统应用）、打开应用隐藏页面等操作。利用终端还可以像在 Linux 上一样进行编程。终端可以使用 ADB 访问，也可以用“终端模拟器”应用访问。&lt;a href=&quot;https://f-droid.org/en/packages/jackpal.androidterm/&quot;&gt;【一款终端模拟器】&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Android 用户权限&lt;/code&gt; Android 终端中有一些操作，例如禁用/启用应用，只能由 ADB 终端执行，而不能由“终端模拟器”应用执行，这是因为每个应用以独立的用户身份运行（尽管这个说法并不严谨），而 ADB 终端以 shell 用户身份运行。Root 权限是对 Linux 系统的最高访问权，但出于安全性考虑，安卓系统中的用户无法直接使用 Root 权限。&lt;/p&gt;
&lt;p&gt;本文将介绍关于 ADB、Android 终端和 Android 用户权限的基础知识。所有 PC 上进行的操作以 Windows 系统为例。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;时效性复核状态&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文章现在处于待复核状态，近期可能出现较大改动。&lt;/strong&gt;&lt;br&gt;
此文章的发布时间是 2021 年第四季度，复核或修订发生于 2021/12/18。&lt;br&gt;
请留意内容的时效性。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;本文不是教程，你不需要亲自尝试其中的每一个操作。如果你不知道自己在做什么，那就不要做。&lt;br&gt;
若阅读本文并执行相关操作后出现设备损坏、隐私泄露、数据丢失等情况，则机主需要自行承担损失。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;h2&gt;ADB 下载与安装&lt;/h2&gt;
&lt;p&gt;ADB 不是 Windows 系统内置软件，需要自行安装才能使用。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Tip title=&quot;提示&quot;&gt;&lt;/p&gt;
&lt;p&gt;若已安装 Android SDK，请使用其附带的 adb，不要另外安装 platform-tools。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Tip&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; 下载正确的 platform-tools。&lt;a href=&quot;https://developer.android.google.cn/studio/releases/platform-tools?hl=zh-cn&quot;&gt;【platform-tools 官方下载】&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; 将压缩包解压至适当的文件夹，并将其设置为 Path。&lt;a href=&quot;https://cn.bing.com/search?q=%E8%AE%BE%E7%BD%AE%E4%B8%BA+Path&quot;&gt;【搜索“设置为 Path”】&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.&lt;/strong&gt; 打开命令行界面，输入 &lt;code&gt;adb version&lt;/code&gt;。若正常显示 ADB 版本，说明安装已经完成。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;Microsoft Windows [版本 10.0.22509.1000]
(c) Microsoft Corporation。保留所有权利。

C:\Users\[redacted]&gt;adb version
Android Debug Bridge version 1.0.41
Version 31.0.3-7562133
Installed as D:\android_sdk\platform-tools\adb.exe

C:\Users\[redacted]&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ADB 连接真机&lt;/h2&gt;
&lt;p&gt;ADB 连接，即 USB 调试是开发专用的功能，Android 中默认不开启。&lt;/p&gt;
&lt;h3&gt;启用开发者选项&lt;/h3&gt;
&lt;p&gt;&amp;#x3C;F.Tip title=&quot;提示&quot;&gt;&lt;/p&gt;
&lt;p&gt;若设置中已经有开发者选项，则无须重复启用。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Tip&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; 在设置中进入“关于设备”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; 连续点击“版本号”一项 7 次。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/edc29017f02f25a0bfa45259314839a6.VyMTDkev_u2UI2.webp&quot; alt=&quot;点击版本号&quot; title=&quot;[maxw=400]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.&lt;/strong&gt; 在设置中找到新增的“开发者选项”，并进入。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/fbbeefb583c2e92b92e388b5947bedaf.B1AAFQeC_GzGU6.webp&quot; alt=&quot;开发者选项的位置&quot; title=&quot;[maxw=400]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;若第 3 步仍然无法在设置中找到“开发者选项”（这一般发生在智能学习机、播放器等设备中），说明设备制造商隐藏了开发者选项。
此时可以安装 Nova Launcher 等带有活动选择列表的工具软件并用其打开开发者选项。&lt;a href=&quot;https://wcl.sparkslab.art/index.php?share/fileDownload&amp;#x26;sid=KEQBAzId&amp;#x26;user=4&amp;#x26;path=%2FAndroidApps%2FNova%20Launcher%206.apk&quot;&gt;【Nova Launcher 下载】&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Nova Launcher 使用提示：打开后完成初步设置进入桌面，长按桌面空白处，选择添加“活动”小部件，在弹出的菜单中选择“设置”-“开发者选项”，然后点击桌面上创建的图标即可进入。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h3&gt;开启 USB 调试&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;4.&lt;/strong&gt; 打开“开发者设置”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5.&lt;/strong&gt; 找到“调试”分类目录，打开 USB 调试。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/26f035b4629d4b5fc116a8e94055a571.BXWhymPL_10aOgw.webp&quot; alt=&quot;USB 调试选项的位置&quot; title=&quot;[maxw=350]&quot;&gt;&lt;/p&gt;
&lt;h3&gt;连接到电脑&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;6.&lt;/strong&gt; 将设备在开机状态下用 USB 数据线连接至电脑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7.&lt;/strong&gt; 若设备使用“仅充电”模式连接（此状态提示会出现在通知栏中），请改用其他模式，然后拔下设备重新插入。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8.&lt;/strong&gt; 打开命令行界面，输入 &lt;code&gt;adb devices&lt;/code&gt;。此时移动设备上将显示 USB 调试授权窗口。点击“确认”。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;C:\Users\[redacted]&gt;adb devices
List of devices attached
0123456789ABCDEF        unauthorized

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/273772f33b4eeba04899a83b422aacde.CpI-Upaf_Z16vSLV.webp&quot; alt=&quot;调试授权窗口&quot; title=&quot;[maxw=300]&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;若调试授权窗口没有出现，请确保电脑上没有安卓模拟器在运行。然后依次运行 &lt;code&gt;adb kill-server&lt;/code&gt; &lt;code&gt;adb start-server&lt;/code&gt;，再次检查授权窗口。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;不要在公用电脑上勾选“一律允许”。ADB 对设备的访问权限较高，被恶意程序利用将导致隐私泄露。
若不慎勾选，可以进入开发者选项，选择“撤销 USB 调试授权”。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;9.&lt;/strong&gt; 再次执行 &lt;code&gt;adb devices&lt;/code&gt;。设备状态显示为 &lt;code&gt;device&lt;/code&gt;，则连接成功。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;C:\Users\yezhiyi9670&gt;adb devices
List of devices attached
0123456789ABCDEF        device

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;若调试授权窗口始终不出现，且 &lt;code&gt;adb devices&lt;/code&gt; 命令不显示任何设备，则可能是 ADB 驱动缺失（少数较旧的电脑会出现这一情况）。&lt;a href=&quot;https://cn.bing.com/search?q=adb+%E9%A9%B1%E5%8A%A8%E7%BC%BA%E5%A4%B1&quot;&gt;【搜索“adb 驱动缺失”】&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h2&gt;ADB 连接模拟器&lt;/h2&gt;
&lt;p&gt;正常情况下，模拟器会使用内置的 adb 程序自动完成连接，连接后使用方法与真机一致。但若模拟器内置 adb 与用户安装的 adb 版本不兼容，则无法进行调试操作。&lt;/p&gt;
&lt;h3&gt;将模拟器的内置 adb 替换为新版&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; 找到自己安装的（或 Android SDK 中的）adb.exe。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; 打开模拟器安装目录，找到其中的 adb.exe。用自己的 adb.exe 将其替换。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;夜神模拟器还需要用自己的 adb.exe 替换安装目录下的 nox_adb.exe。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h2&gt;ADB 常见命令&lt;/h2&gt;
&lt;h3&gt;多设备访问&lt;/h3&gt;
&lt;p&gt;ADB 一次命令只能操作一个设备。若有多个设备连接至 ADB（实际使用时尽量避免这种情况），则执行命令时必须指定设备。&lt;/p&gt;
&lt;p&gt;要指定设备，应当添加 &lt;code&gt;-s &amp;#x3C;设备序列号&gt;&lt;/code&gt; 参数。设备序列号即为 &lt;code&gt;adb devices&lt;/code&gt; 命令中看到的名称。例如，命令 &lt;code&gt;adb install com.android.chrome.apk&lt;/code&gt; 应当改为 &lt;code&gt;adb -s 0123456789ABCDEF install com.android.chrome.apk&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;若无法避免多个设备连接，且需要执行大量命令，则使用 &lt;code&gt;-t &amp;#x3C;transport_id&gt;&lt;/code&gt; 指定设备更加方便。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;adb devices -l&lt;/code&gt; 可以获取设备的 transport_id。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;下文的操作不再考虑多个设备连接至 ADB 的情况。&lt;/p&gt;
&lt;h3&gt;重启设备&lt;/h3&gt;
&lt;p&gt;使用命令 &lt;code&gt;adb reboot&lt;/code&gt; 可以立即重启设备。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;adb reboot&lt;/code&gt; 正常重启。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;adb reboot recovery&lt;/code&gt; 重启进入 recovery，此模式用于进行系统更新，也可用来刷入官方刷机包修复受损的系统。&lt;br&gt;
通常，也可以通过关机状态下长按电源+音量上（若有实体 HOME 键，可能也需要按）进入。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;adb reboot bootloader&lt;/code&gt; 重启进入 fastboot 模式，此模式用于刷入自定义镜像。&lt;br&gt;
通常，也可以通过关机状态下长按电源+音量上（若有实体 HOME 键，可能也需要按）进入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;/blog/play-android/preintro-bootmode#bootmode&quot;&gt;关于启动模式的介绍&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Danger title=&quot;危险&quot;&gt;&lt;/p&gt;
&lt;p&gt;Recovery 中的 Wipe data / Factory reset 选项是恢复出厂设置，慎用！&lt;/p&gt;
&lt;p&gt;解锁 Bootloader 会清空数据，不当使用 fastboot 模式会使设备无法启动或无法正常使用。请确保你知道自己在干什么！&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Danger&gt;&lt;/p&gt;
&lt;h3&gt;访问终端&lt;/h3&gt;
&lt;p&gt;命令 &lt;code&gt;adb shell&lt;/code&gt; 可以以 shell 身份（权限）打开 Android 系统的终端。&lt;code&gt;adb shell &amp;#x3C;命令&gt;&lt;/code&gt; 将立即执行命令并退出终端。&lt;/p&gt;
&lt;h3&gt;安装应用&lt;/h3&gt;
&lt;p&gt;使用命令 &lt;code&gt;adb install &amp;#x3C;apk软件包路径&gt;&lt;/code&gt; 安装软件。大多数设备中，这会直接安装软件，不需要用户确认；少数设备需要用户选择是否允许安装。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;Android 6.0 及以上版本，会使用 Streamed Install 方法，该方法速度较快。早于 Android 6.0 的版本会使用传统 Push Install 方法，这种情况下，apk 文件的名称不能包含非 ASCII 字符（不严谨地说，不能包含中文），否则会引发编码问题无法成功安装。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;h3&gt;卸载应用&lt;/h3&gt;
&lt;p&gt;使用命令 &lt;code&gt;adb uninstall &amp;#x3C;软件包名&gt;&lt;/code&gt; 即可卸载软件。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;adb uninstall -k &amp;#x3C;软件包名&gt;&lt;/code&gt; 会保留数据。若需要重新安装该软件，则新的安装包必须满足覆盖安装条件（应用签名一致，版本相同或更新），否则无法安装。若要清除数据和覆盖安装条件，使用 &lt;code&gt;adb shell pm clear &amp;#x3C;软件包名&gt;&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;Root 模式（仅限模拟器）&lt;/h3&gt;
&lt;p&gt;命令 &lt;code&gt;adb root&lt;/code&gt; 可以使 ADB 命令直接以 root 身份执行，此时可以执行 &lt;code&gt;adb remount&lt;/code&gt; 等操作，且 &lt;code&gt;adb shell&lt;/code&gt; 会默认以 root 身份运行。&lt;code&gt;adb unroot&lt;/code&gt; 可以撤销。&lt;/p&gt;
&lt;p&gt;这是非常不安全的，故生产环境下的设备不能使用，即使设备已经获取 Root 权限。&lt;/p&gt;
&lt;h3&gt;挂载系统分区为可读写（仅限模拟器）&lt;/h3&gt;
&lt;p&gt;系统文件分区默认为只读挂载，无法修改。&lt;/p&gt;
&lt;p&gt;Root 模式下，命令 &lt;code&gt;adb remount&lt;/code&gt; 将暂时挂载 &lt;code&gt;/system&lt;/code&gt; 系统分区为可读写，这样可以修改系统文件。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Danger title=&quot;危险&quot;&gt;&lt;/p&gt;
&lt;p&gt;修改系统文件可能导致系统无法启动或正常使用，这种情况称为设备“变砖”。永远不要做没有十足把握的修改！
对系统文件的修改&lt;strong&gt;不能&lt;/strong&gt;通过恢复出厂设置还原！&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Danger&gt;&lt;/p&gt;
&lt;p&gt;等效的 Android 终端命令为 &lt;code&gt;remount -o rw,remount /system&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;传递文件&lt;/h3&gt;
&lt;p&gt;传入文件：&lt;code&gt;adb push &amp;#x3C;Windows 上的文件或文件夹&gt; &amp;#x3C;Android 上的目标目录&gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;仅传入修改日期更晚的文件：&lt;code&gt;adb push --sync &amp;#x3C;Windows 上的文件或文件夹&gt; &amp;#x3C;Android 上的目标目录&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;取出文件：&lt;code&gt;adb pull &amp;#x3C;Android 上的文件或文件夹&gt; &amp;#x3C;Windows 上的目标目录&gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;注意，Android 内置存储器的路径是 &lt;code&gt;/storage/emulated/0/&lt;/code&gt;（或简写 &lt;code&gt;/sdcard/&lt;/code&gt;）。此命令也能操作内置存储器以外的文件。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;若需要操作的文件存储在内置存储器中，且可以通过 MTP 模式 USB 连接访问，则仍然更提倡使用 MTP 模式传输，因为 MTP 协议有压缩机制，速度比 ADB 文件传输快得多。&lt;a href=&quot;https://cn.bing.com/search?q=MTP+%E6%A8%A1%E5%BC%8F+USB+%E8%BF%9E%E6%8E%A5&quot;&gt;【搜索“MTP 模式 USB 连接”】&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h3&gt;备份还原&lt;/h3&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;此功能已经弃用，未来可能被移除。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;使用命令 &lt;code&gt;adb backup &amp;#x3C;package&gt;&lt;/code&gt; 可以将应用的数据备份到电脑。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;adb restore &amp;#x3C;package&gt;&lt;/code&gt; 可以还原备份。&lt;/p&gt;
&lt;p&gt;注意这两个命令执行后，需要在移动设备上确认。&lt;/p&gt;
&lt;p&gt;应用数据内带有签名验证，因此应用的签名必须一致，数据才能够还原。并不能在破解游戏后通过这种方法恢复进度。&lt;/p&gt;
&lt;h2&gt;使用 Android 终端&lt;/h2&gt;
&lt;h3&gt;进入终端&lt;/h3&gt;
&lt;p&gt;设备已连接 ADB 的情况下，在 Windows 端命令行界面输入 &lt;code&gt;adb shell&lt;/code&gt;，即可进入终端。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;C:\Users\[redacted]&gt;adb shell
shell@m2:/ $
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Android 系统下，可以通过“终端模拟器”应用访问终端。&lt;a href=&quot;https://f-droid.org/en/packages/jackpal.androidterm/&quot;&gt;【一款终端模拟器】&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;u0_a64@m2:/ $

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意，这两种方式进入的终端是相同的，但能执行的操作却不一样。这是因为 &lt;code&gt;adb shell&lt;/code&gt; 进入终端，终端以 shell 身份权限打开；终端模拟器进入终端，终端以应用本身的身份（u0_a64）打开。shell 权限对设备的访问权更高，而应用本身则不能执行大部分常用命令。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;若终端命令提示符中未显示当前用户名，可以输入 &lt;code&gt;whoami&lt;/code&gt; 获取。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h3&gt;基本初等命令&lt;/h3&gt;
&lt;p&gt;使用终端，可以完成基础的文件操作。&lt;a href=&quot;https://cn.bing.com/search?q=Linux+%E7%BB%88%E7%AB%AF%E5%85%A5%E9%97%A8&quot;&gt;【搜索“Linux 终端入门”】&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;|命令|用途|
|-|-|
|&lt;code&gt;cd &amp;#x3C;dir&gt;&lt;/code&gt;|切换到目录|
|&lt;code&gt;exit&lt;/code&gt;|或 &lt;code&gt;Ctrl-D&lt;/code&gt;，退出终端|
|&lt;code&gt;rm &amp;#x3C;-rf&gt; &amp;#x3C;filename&gt;&lt;/code&gt;|删除文件，参数 &lt;code&gt;r&lt;/code&gt; 允许删除目录，参数 &lt;code&gt;f&lt;/code&gt; 强制|
|&lt;code&gt;cat &amp;#x3C;filename&gt;&lt;/code&gt;|输出文本文件的内容（不是文本文件也行，随便）|
|&lt;code&gt;mkdir &amp;#x3C;dirname&gt;&lt;/code&gt;|创建文件夹|
|&lt;code&gt;mv &amp;#x3C;old-path&gt; &amp;#x3C;new-path&gt;&lt;/code&gt;|重命名，或同分区内文件移动|
|&lt;code&gt;cp &amp;#x3C;old-path&gt; &amp;#x3C;new-path&gt;&lt;/code&gt;|复制|
|&lt;code&gt;ls &amp;#x3C;-la&gt; [&amp;#x3C;dir&gt;]&lt;/code&gt;|列出目录内文件，默认为当前目录|
|&lt;code&gt;chmod &amp;#x3C;mode&gt; &amp;#x3C;filename&gt;&lt;/code&gt;|修改文件的&lt;strong&gt;权限&lt;/strong&gt;|
|&lt;code&gt;chown &amp;#x3C;mode&gt; &amp;#x3C;filename&gt;&lt;/code&gt;|修改文件的&lt;strong&gt;所有者&lt;/strong&gt;|&lt;/p&gt;
&lt;p&gt;|组合符号|用途|
|-|-|
|&lt;code&gt;&amp;#x3C;command&gt; &amp;#x26; &amp;#x3C;command&gt;&lt;/code&gt;|同时执行命令|
|&lt;code&gt;&amp;#x3C;command&gt; &amp;#x26;&amp;#x26; &amp;#x3C;command&gt;&lt;/code&gt;|顺序执行直到出现返回值不为 0 为止|
|&lt;code&gt;&amp;#x3C;command&gt; &gt; &amp;#x3C;filename&gt;&lt;/code&gt;|执行结果输出到文件|
|&lt;code&gt;&amp;#x3C;command&gt; &amp;#x3C; &amp;#x3C;filename&gt;&lt;/code&gt;|文件作标准输入|
|&lt;code&gt;&amp;#x3C;command&gt; \| &amp;#x3C;command&gt;&lt;/code&gt;|执行多个命令，无论返回值，并将前一个的输出作为下一个的输入|&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;跨分区移动的方法：复制后删除源文件。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;Android 系统中，文件权限有严格限制。内置 SD 卡中，&lt;code&gt;chmod&lt;/code&gt; 命令不能改变权限。要暂时执行可执行文件，应当将文件放入 &lt;code&gt;/data/local/tmp/&lt;/code&gt; 目录下，修改权限并执行。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h3&gt;软件包管理器 - pm&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pm&lt;/code&gt; 命令用于管理系统上已经安装的软件包。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;包名&lt;/strong&gt;是系统中软件包的唯一标识符。系统设置的“应用”页面一般不会显示包名，可以使用幸运破解器等软件查看包名。&lt;a href=&quot;https://wcl.sparkslab.art/index.php?share/fileDownload&amp;#x26;sid=KEQBAzId&amp;#x26;user=4&amp;#x26;path=%2FAndroidApps%2FLucky%20Patcher.apk&quot;&gt;【幸运破解器 下载】&lt;/a&gt;&amp;#x3C;F.Redacted&gt;幸运破解器 ×    应用工具箱 √&amp;#x3C;/F.Redacted&gt;&lt;/p&gt;
&lt;p&gt;|命令|用途|
|-|-|
|&lt;code&gt;pm list packages&lt;/code&gt;|列出所有软件包名|
|&lt;code&gt;pm list packages -s&lt;/code&gt;|列出系统内置软件包名|
|&lt;code&gt;pm list packages -3&lt;/code&gt;|列出用户安装的软件包名|
|&lt;code&gt;pm install &amp;#x3C;filename&gt;&lt;/code&gt;|安装 APK 文件|
|&lt;code&gt;pm uninstall &amp;#x3C;package&gt;&lt;/code&gt;|卸载软件|
|&lt;code&gt;pm uninstall -k &amp;#x3C;package&gt;&lt;/code&gt;|卸载软件但保留数据，重新安装的软件必须满足覆盖安装条件|
|&lt;code&gt;pm clear &amp;#x3C;package&gt;&lt;/code&gt;|清除软件数据（若软件包已卸载，同时移除覆盖安装条件）|
|&lt;code&gt;pm uninstall --user 0 &amp;#x3C;package&gt;&lt;/code&gt;|为默认用户卸载，可以卸载系统内置软件|
|&lt;code&gt;pm hide &amp;#x3C;package&gt;&lt;/code&gt;|禁用应用[仅 6 及以下]|
|&lt;code&gt;pm disable-user &amp;#x3C;package&gt;&lt;/code&gt;|禁用应用[仅 7 及以上]|
|&lt;code&gt;pm unhide &amp;#x3C;package&gt;&lt;/code&gt;|启用应用[仅 6 及以下]|
|&lt;code&gt;pm enable &amp;#x3C;package&gt;&lt;/code&gt;|启用应用[仅 7 及以上]|
|&lt;code&gt;pm path &amp;#x3C;package&gt;&lt;/code&gt;|获取应用在系统内的 APK 路径|&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注：所谓“系统内置”，不是“系统预装”，而是指不能卸载的系统预装应用。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;应用禁用后，桌面图标将被删除。因机型不同，这些应用在应用列表中可能显示为“已停用”，也可能完全隐藏。&lt;br&gt;
禁用的应用将不能使用，也无法后台运行。&lt;br&gt;
在设置中“重置应用偏好设置”，或覆盖安装被禁用的应用，均能解除禁用。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;获取应用的 APK 路径后，可以直接用 &lt;code&gt;adb pull&lt;/code&gt; 命令取出 APK。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Danger title=&quot;危险&quot;&gt;&lt;/p&gt;
&lt;p&gt;系统内置应用卸载后，同包名应用仍然需要满足覆盖安装条件才能安装（这是因为应用无法从 &lt;code&gt;/system&lt;/code&gt; 分区被删除）。&lt;br&gt;
若未备份原始的 APK 文件，则被卸载的系统应用只能通过恢复出厂设置还原（某些预构建的应用，例如时钟，甚至无法使用原有 APK 还原），请谨慎操作！&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Danger&gt;&lt;/p&gt;
&lt;h3&gt;显示（窗口）管理器 - wm&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;wm&lt;/code&gt; 命令，可以为设备设置自定义分辨率和 DPI（DPI 相当于 Windows 的缩放比例）。&lt;/p&gt;
&lt;p&gt;|命令|用途|
|-|-|
|&lt;code&gt;wm size&lt;/code&gt;|查看当前分辨率|
|&lt;code&gt;wm size &amp;#x3C;width&gt;x&amp;#x3C;height&gt;&lt;/code&gt;|设置分辨率|
|&lt;code&gt;wm size reset&lt;/code&gt;|重置默认分辨率|
|&lt;code&gt;wm density&lt;/code&gt;|查看当前 DPI|
|&lt;code&gt;wm density &amp;#x3C;dpi&gt;&lt;/code&gt;|设置 DPI|
|&lt;code&gt;wm density reset&lt;/code&gt;|重置 DPI|&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;&lt;/p&gt;
&lt;p&gt;Android 7 或更高版本中，可以直接通过设置调节显示项目的大小。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;显示 &gt; 显示大小&lt;/code&gt;，可以选择系统预先设定的值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;系统 &gt; 开发者选项 &gt; 最小宽度&lt;/code&gt;，可以指定自定义值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;分辨率调节容易出现问题，若仅仅需要调整显示项目的大小，仅修改 DPI。&lt;br&gt;
不合理的分辨率或 DPI 设置将导致显示异常，甚至阻止设备正常使用。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;h3&gt;在终端模拟器中获取 shell 权限&lt;/h3&gt;
&lt;p&gt;终端模拟器打开的终端使用的是应用的身份，而非 shell 身份。这导致很多命令（例如 &lt;code&gt;pm&lt;/code&gt; &lt;code&gt;wm&lt;/code&gt;）不能执行。&lt;/p&gt;
&lt;p&gt;若要不借助电脑执行这些命令，则需要找到在终端模拟器中获取 shell 权限的方法。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Tip title=&quot;提示&quot;&gt;&lt;/p&gt;
&lt;p&gt;对于已经获取 root 权限的设备，可以在终端模拟器中输入 &lt;code&gt;su&lt;/code&gt; 直接获得最高访问权，不需要使用其他方法获取 shell 权限。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Tip&gt;&lt;/p&gt;
&lt;p&gt;此处介绍的方法仅有少数设备能成功。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; 打开 USB 调试，断开与电脑的连接，然后重启设备。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; 在终端模拟器中输入 &lt;code&gt;adb shell&lt;/code&gt;。（若系统内没有 ADB，可以尝试使用 &lt;a href=&quot;https://f-droid.org/en/packages/com.termux/&quot;&gt;Termux&lt;/a&gt; 终端模拟器，然后执行 &lt;code&gt;pkg install android-tools&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.&lt;/strong&gt; 系统应当弹出授权窗口。尽快点击“确定”，几秒后，具有 shell 权限的终端将会打开。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/c3d5cf2543cdf00ac910a2a45c5e355d.NQ2_2hBB_1SYq6R.webp&quot; alt=&quot;&quot; title=&quot;[maxw=350]&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Shell、Root 和应用权限&lt;/h2&gt;
&lt;h3&gt;Root 权限&lt;/h3&gt;
&lt;p&gt;Android 基于 Linux 内核，Root 权限就是 Linux 中对系统的最高访问权。为保证基本的安全性，Android 系统不对用户开放这一权限。通过漏洞或刷机获得这一权限的过程称为“Root”。&lt;/p&gt;
&lt;p&gt;获取到 Root 权限的应用几乎无所不能，因此，Root 权限也常常被称为“超级用户”权限（Superuser Access）。按通常的方案获得 Root 权限后，申请 Root 权限的终端命令也因此称为 &lt;code&gt;su&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;通常，获取 Root 权限有三类手段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;漏洞破解。通常，一键 Root 软件以这种方式工作。如今，这种方式比较难成功。&lt;/li&gt;
&lt;li&gt;开启 Root。部分设备制造商会内置 Root 权限，并允许用户自行按需要启用。&lt;/li&gt;
&lt;li&gt;刷机修改。这种方法具有理论可行性，是公认的“通解通法”，也这是目前的主流方法。这种方法需要设备制造商允许刷机。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&quot;警告&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在经常有“设备获取 Root 权限不安全”的说法，这种说法过于绝对。目前的 Root 方案较为完善，用户获取 Root 权限并不一定不安全，善用 Root 还能保护隐私，但是，对于使用不谨慎或喜欢冒险的用户，确实会大大增加隐私泄露、数据丢失、设备损坏的风险。若设备遭已获取 Root 权限的恶意软件攻击，则后果也会比一般的恶意软件攻击严重得多。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;此处不赘述获得 Root 权限的具体方法。&lt;/p&gt;
&lt;h3&gt;Shell 权限&lt;/h3&gt;
&lt;p&gt;使用 ADB 打开的终端具有 Shell 权限。Shell 权限能够操作 Android 中大多数命令，但限制仍然较多，不能修改系统文件，也无法访问应用的数据。&lt;/p&gt;
&lt;p&gt;在具有 shell 权限的终端下，可以执行 &lt;code&gt;run-as &amp;#x3C;package&gt;&lt;/code&gt;（目标应用包必须被指定为“可调试”）。执行后，终端仍然以 shell 身份运行，并且会被额外授予对该应用数据的访问权（还会自动帮你切到应用的数据目录）。这对游戏破解等工作的帮助较大。&lt;/p&gt;
&lt;h3&gt;应用权限&lt;/h3&gt;
&lt;p&gt;系统内应用及其打开的终端将具有应用本身的权限。应用权限受到严格限制（显然受 Android 权限管理限制），无法执行大多数终端命令。但是，应用可以访问自己的数据文件。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Muted&gt;封面图来自网络，未经明确授权。&amp;#x3C;/F.Muted&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/hero.DXYW7Bkl.png"/><enclosure url="/_astro/hero.DXYW7Bkl.png"/></item><item><title>[网络安全] https加密设计思路详解</title><link>https://ak-ioi.com/blog/security/i-think-https</link><guid isPermaLink="true">https://ak-ioi.com/blog/security/i-think-https</guid><description>我很久以前在研究HTTPS，就谈一谈我对HTTPS的理解吧。</description><pubDate>Sun, 27 Oct 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import F from &apos;@/components/mdx/formatting&apos;&lt;/p&gt;
&lt;p&gt;我~~最近~~很久以前在研究HTTPS，就谈一谈我对HTTPS的理解吧。欢迎大佬指错。&lt;/p&gt;
&lt;p&gt;修订：2021年2月6日&lt;/p&gt;
&lt;h2&gt;HTTP与HTTPS&lt;/h2&gt;
&lt;p&gt;http指超文本传输协议，现在我们浏览网站靠的都是这种协议。我们可以用如下的图来表示HTTP建立连接与传输数据的过程。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
participant 客户端
participant 服务器

客户端 -&gt;&gt; 服务器: SYN
服务器 --&gt;&gt; 客户端: SYN ACK
客户端 -&gt;&gt; 服务器: ACK

Note over 客户端, 服务器: 以上操作为建立TCP 连接。

客户端 -&gt;&gt; 服务器: HTTP 请求 1
服务器 --&gt;&gt; 客户端: HTTP 响应 1
客户端 -&gt;&gt; 服务器: HTTP 请求 2
服务器 --&gt;&gt; 客户端: HTTP 响应 2

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTTP是明文传输（其问题见下文）。为了提高安全性，出现了HTTPS（http-over-ssl），即HTTP的加密版本。&lt;/p&gt;
&lt;h2&gt;明文传输的问题&lt;/h2&gt;
&lt;p&gt;明文传输，顾名思义，就是在网络管道中通讯的内容可以被任何中间人读懂或修改。这样有什么危害呢？我们可以举几个现实生活中的例子。&lt;/p&gt;
&lt;p&gt;比如，你的朋友问你信用卡密码等信息，你直接写在纸上，不进行任何加密，然后托别人送给你朋友。结果，路上，送信人被早就想要获取你隐私信息的坏人抓住，那个坏人拿走了送信人的信件，你的密码就让坏人知道了。&lt;/p&gt;
&lt;p&gt;如果你和你朋友在商量国家大事（假设），并且此时坏人行事足够小心，坏人就可以在你们完全不知道的情况下篡改你们之间的通讯，从而使得国家毁灭。甚至更严重一点，如果你的送信人突然起了坏心...&lt;/p&gt;
&lt;p&gt;在网络中，同样地，如果给你提供互联网服务的商家（比如电信）起了坏心，或网络通讯的必经之路（如路由器）被劫持，你将会面临巨大的安全风险。（我的经历可以证实电信起过坏心：通过手机移动数据访问我的某网址时，页面被植入移动套餐相关广告，但改用 HTTPS 后问题不复存在）&lt;/p&gt;
&lt;p&gt;也正是因为这个原因，Google Chrome 和一些主流浏览器才将未加密的HTTP网站标识为“不安全”。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/94c9c51c313643abca00a5d47fe035b9.DdCIkDtI_Z1XOTl1.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;HTTP 网站“不安全”标识&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;同时，Google Chrome 类浏览器会禁止 HTTP 网站使用用户的隐私数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/e5e61ea5c8d23f5a20b4749f429a237d.4p9L61Tj_1UKJ2h.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;HTTP 网站始终被禁止的权限&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;在大概 2018 年 8 月的时候有一个~~谣言~~说法，说 Chrome 可能不出半年就会“封杀”未加密的 HTTP 网站。事实证明，这不是真的，但这足以说明，未加密的网站在互联网中已经难以立足了。因此，是时候了解并启用 HTTPS 了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问题1&lt;/strong&gt;：加密 HTTP 确实可以防止数据被偷看，那可以防止被篡改吗？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;废话&lt;/strong&gt;。传输加密后的数据，相当于在用另一种攻击者看不懂的语言进行交流。既然攻击者都不懂这个语言，他怎么能用这个语言来表达虚假信息呢？&lt;/p&gt;
&lt;p&gt;那么，废话讲完了，我们就可以进入正题——HTTPS 的设计 了。&lt;/p&gt;
&lt;p&gt;为了一步步清晰思路，我们必须先假装自己是 Netscape（发明 HTTPS 的公司）的研究人员，并且正在试图发明 HTTPS。&lt;/p&gt;
&lt;h2&gt;原则：不要小看攻击者&lt;/h2&gt;
&lt;p&gt;攻击者一般利用具有自动性的程序或脚本进行攻击。因此，请不要小看攻击者抓住攻击机会的能力。HTTPS 的设计应当阻止除暴力猜测外的任何破解手段。在我们下面设计 HTTPS 的过程中，我们也不能心存侥幸，小看攻击者。&lt;/p&gt;
&lt;h2&gt;加密HTTP&lt;/h2&gt;
&lt;p&gt;要加密HTTP，我们离不开加密算法。&lt;/p&gt;
&lt;p&gt;加密算法可以分为对称加密和非对称加密。当一串数据被用密钥 $K$ 加密后，使用密钥 $K$ 就能解密出原来的数据，那么该加密体系就是对称加密。简单地说，你有一把钥匙，你的朋友也有一把钥匙，你把信件放进盒子，用你的钥匙锁上，你的朋友收到后用同样的钥匙打开。&lt;/p&gt;
&lt;p&gt;然而，容易想到的对称加密算法也容易破解。生产环境中使用的对称加密体系还必须具有“抗推测性”：即使攻击者截获了一次或若干次通讯的密文，然后准确无误地猜出了原文，也无法准确推出密钥。AES 加密——一种常用的对称加密算法，就具有这样的性质。&lt;/p&gt;
&lt;p&gt;现在，我们已经有了对称加密的理论，于是现在我们可以尝试用对称加密来加密 HTTP。思维敏锐的同学应该很快就会发现问题——在上面的例子中，你和你的朋友都有一把钥匙。我手里的这把和我朋友手上的那个相同的钥匙，是我和朋友某一次见面时，朋友给我的。&lt;/p&gt;
&lt;p&gt;在网络上，网络服务器就相当于你的朋友。然而，由于你访问网站前很可能没有和站长见过面，你的浏览器中就很可能不会有一个&lt;code&gt;(&lt;/code&gt;和服务器上的那个钥匙相同&lt;code&gt;)&lt;/code&gt;的钥匙。要具备相同钥匙，必须进行密钥传递，即“密钥交换”。&lt;/p&gt;
&lt;p&gt;如何传递？显然站长还是不可能线下去找你，因此只能通过网络给。那么，根据当前的想法，我们可以设计出加密通信的过程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
participant 客户端
participant 服务器

客户端 -&gt;&gt; 服务器: SYN
服务器 --&gt;&gt; 客户端: SYN ACK
客户端 -&gt;&gt; 服务器: ACK

Note over 客户端, 服务器: [1] 以上操作为建立TCP 连接。

客户端 -&gt;&gt; 服务器: 请求对称加密密钥
服务器 --&gt;&gt; 客户端: 密钥是 ...

Note over 客户端, 服务器: [2] 此时交换密钥完成

客户端 -&gt;&gt; 服务器: HTTP 请求 1 （对称加密）
服务器 --&gt;&gt; 客户端: HTTP 响应 1 （对称加密）
客户端 -&gt;&gt; 服务器: ...

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;皆大欢喜？No。&lt;/p&gt;
&lt;p&gt;不难发现，直到 [2] 处，加密的连接才被建立。那么，“密钥是 ...”这条消息必须明文传输。但是，根据我们的原则，我们不能小看攻击者抓住机会的能力，而一旦这条消息被截获，那么攻击者就可以截获此后所有的，对称加密的消息，并通过得到的密钥解密它们。这样，刚才设计的加密方式就不存在安全性了。&lt;/p&gt;
&lt;p&gt;那么我们将“密钥是 ...”这条消息也对称加密如何？不行！我们画一下流程图，很快就发现问题了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
participant 客户端
participant 服务器

客户端 -&gt;&gt; 服务器: SYN
服务器 --&gt;&gt; 客户端: SYN ACK
客户端 -&gt;&gt; 服务器: ACK

Note over 客户端, 服务器: [1] 以上操作为建立TCP 连接。

客户端 -&gt;&gt; 服务器: 请求对称加密密钥1
服务器 --&gt;&gt; 客户端: 密钥1是 ...

客户端 -&gt;&gt; 服务器: 请求对称加密密钥2
服务器 --&gt;&gt; 客户端: 密钥2是 ... （对称加密1）

Note over 客户端, 服务器: [2] 此时交换密钥完成

客户端 -&gt;&gt; 服务器: HTTP 请求 1 （对称加密2）
服务器 --&gt;&gt; 客户端: HTTP 响应 1 （对称加密2）
客户端 -&gt;&gt; 服务器: ...

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;~~鲁迅曾经说过，~~ 世间本没有安全信道，密钥交换后，安全信道才得以建立。加密算法的问题在于，必有一次密钥交换要以明文形式进行。因此，在上面的设计中，我们只是无谓地增加了一次 请求/响应 的过程。如果攻击者截获密钥1，他就可以解密出密钥2，进而解密出 HTTP 请求和响应的所有内容。&lt;/p&gt;
&lt;p&gt;看来，仅仅有对称加密是不行的。然而，有对称加密，必有“不对称的加密”。利用“不对称的加密”，或许可以解决这个问题。&lt;/p&gt;
&lt;h2&gt;非对称加密&lt;/h2&gt;
&lt;p&gt;对称加密，是指一个数据，用密钥 $K$ 加密，必定能用密钥 $K$ 解密出相同的数据。&lt;/p&gt;
&lt;p&gt;那么，我们猜测，非对称加密，是指一个数据 $P$，用密钥 $K_e$ 加密得到密文 $C$ 后，就不能再用密钥 $K_e$ 解密出原先的数据，而必须使用另一个密钥 $K_d$。而且，密钥 $K_d$ 和 $K_e$ 之间存在着某些内在联系。&lt;/p&gt;
&lt;p&gt;形式化地说，一个完整的非对称加密体系包含几个函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;私钥生成函数 $\text{Gen}(l,S)$。其中 $S$ 称为“种子”（数据类型无关紧要，依具体实现而定），而 $l$ 是一个正整数（可以有一个固定的取值列表，并非一定要可以任意取值）。输出内容是一个长度为 $l$ 的二进制数（可以有前导 0），称为私钥，即 $K_d = \text{Gen}(l,S)$。
&lt;ul&gt;
&lt;li&gt;这个函数要有关于 $l$ 的多项式级时间复杂度。&lt;/li&gt;
&lt;li&gt;对于相同的 $S$ 和 $l$，该函数的输出一定相同。&lt;/li&gt;
&lt;li&gt;能够用这个函数生成的私钥 $K_d$ 称为&lt;strong&gt;合法私钥&lt;/strong&gt;。$l$ 称为私钥的&lt;strong&gt;长度&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;公钥导出函数 $\text{Pub}(K_d)$。其中 $K_d$ 可以是任何一个&lt;strong&gt;合法私钥&lt;/strong&gt;。输出内容是一个二进制数（长度只与 $K_d$ 的长度有关，可以有前导0），称为公钥，即 $K_e = \text{Pub}(K_d)$。
&lt;ul&gt;
&lt;li&gt;这个函数要有关于 $K_d$的长度 的多项式级时间复杂度。&lt;/li&gt;
&lt;li&gt;对于相同的 $K_d$，该函数的输出一定也相同，反之亦然。&lt;/li&gt;
&lt;li&gt;不存在时间复杂度关于 $K_e$的长度 为多项式的函数 $\text{Rev}(K_e)$，能反推出对应的 $K_d$。也就是说，如果原有的 $K_d$ 足够长，那么已知 $K_e$ 推导出 $K_d$ 是不可行的。&lt;/li&gt;
&lt;li&gt;能够用这个函数生成的公钥 $K_e$ 称为&lt;strong&gt;合法公钥&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;加密算法 $\text{Ec}(P,K_e)$。其中 $P$ 是任意数据（具体实现中可以对长度加以限制），$K_e$ 是一个&lt;strong&gt;合法公钥&lt;/strong&gt;。输出内容是一串数据，称为密文，即 $C = \text{Ec}(P,K_e)$。
&lt;ul&gt;
&lt;li&gt;这个函数要有关于 $K_e$的长度 或 $P$的长度 的多项式级时间复杂度。&lt;/li&gt;
&lt;li&gt;对于相同的 $P$ 和 $K_e$，该函数的输出一定相同。如果 $P$ 不同而 $K_e$ 相同，该函数的输出一定不同。&lt;/li&gt;
&lt;li&gt;不存在时间复杂度关于 $K_e$的长度 或 $C$的长度 为多项式的函数 $\text{RevDc}(C,K_e)$，能解密出原数据 $P$。&lt;/li&gt;
&lt;li&gt;能够用这个函数生成的数据 $C$ 称为&lt;strong&gt;合法密文&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;解密算法 $\text{Dc}(C,K_d)$。其中 $C$ 是&lt;strong&gt;合法密文&lt;/strong&gt;，$K_d$ 是一个&lt;strong&gt;合法私钥&lt;/strong&gt;。输出内容是一串数据。
&lt;ul&gt;
&lt;li&gt;这个函数要有关于 $K_d$的长度 或 $C$的长度 的多项式级时间复杂度。&lt;/li&gt;
&lt;li&gt;该函数必须是加密算法的逆过程，即 $\text{Dc}(\text{Ec}(P,\text{Pub}(K_d)),K_d) = P$ 恒成立。其中 $K_d$ 为&lt;strong&gt;合法私钥&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际上这种算法已经有人发明了。所有的非对称加密算法都基于一个数学难题（以此实现 $\text{Pub}$ 的不可逆性）。例如最常用的算法 RSA，利用的是生成大质数、将两个大质数相乘很容易，将乘积分解却不可行的原理。&lt;/p&gt;
&lt;p&gt;另外，某些非对称加密算法可以用来签名（见下文）。这种算法的加密算法与解密算法还必须具有以下性质：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\text{Dc}(C,K_d)$ 中 $C$ 可以是任意数据（具体实现中可以对长度加以限制）。这一解密的过程被称为“签名”。&lt;/li&gt;
&lt;li&gt;加密算法必须是解密算法的逆过程，即 $\text{Ec}(\text{Dc}(C,K_d),\text{Pub}(K_d)) = C$ 恒成立。其中 $K_d$ 为&lt;strong&gt;合法私钥&lt;/strong&gt;。这一过程被称为“验证”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么现在有能用于签名的非对称加密算法吗？其实 RSA 就是。&lt;/p&gt;
&lt;p&gt;然而，目前的非对称加密算法还有以下缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;速度通常很慢，一般为对称加密的百分之一至千分之一。&lt;/li&gt;
&lt;li&gt;处理的数据（$\text{Ec}$ 中的 $P$，$\text{Dc}$ 中的 $C$）长度不能超过私钥 $K_d$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;尽管存在如此大的缺点，非对称加密可能还是可以拯救刚才那个失败的 HTTP 加密方法的。&lt;/p&gt;
&lt;h2&gt;改进失败的 HTTP 加密设计&lt;/h2&gt;
&lt;p&gt;非对称加密速度太慢，而且处理的数据长度也有限制。显然，如果用它来加密 HTTP 请求 和 HTTP 响应 是不行的。&lt;/p&gt;
&lt;p&gt;那咋办呢？&lt;/p&gt;
&lt;p&gt;回去看一下某个用对称加密保护密钥交换的失败尝试，我们也许就有了灵感（不用回去看，我搬下来了）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
participant 客户端
participant 服务器

客户端 -&gt;&gt; 服务器: SYN
服务器 --&gt;&gt; 客户端: SYN ACK
客户端 -&gt;&gt; 服务器: ACK

Note over 客户端, 服务器: [1] 以上操作为建立TCP 连接。

客户端 -&gt;&gt; 服务器: 请求对称加密密钥1
服务器 --&gt;&gt; 客户端: 密钥1是 ...

客户端 -&gt;&gt; 服务器: 请求对称加密密钥2
服务器 --&gt;&gt; 客户端: 密钥2是 ... （对称加密1）

Note over 客户端, 服务器: [2] 此时交换密钥完成

客户端 -&gt;&gt; 服务器: HTTP 请求 1 （对称加密2）
服务器 --&gt;&gt; 客户端: HTTP 响应 1 （对称加密2）
客户端 -&gt;&gt; 服务器: ...

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们尝试用非对称加密取代上图中的“对称加密1”。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
participant 客户端
participant 服务器

客户端 -&gt;&gt; 服务器: SYN
服务器 --&gt;&gt; 客户端: SYN ACK
客户端 -&gt;&gt; 服务器: ACK

Note over 客户端, 服务器: [1] 以上操作为建立TCP 连接。

客户端 -&gt;&gt; 服务器: 请求公钥
服务器 -&gt;&gt; 服务器: 生成一个私钥，然后导出公钥
服务器 --&gt;&gt; 客户端: 公钥是 ...
客户端 -&gt;&gt; 客户端: 生成一个对称加密密钥
客户端 -&gt;&gt; 服务器: 密钥2是 ... （非对称加密）
服务器 -&gt;&gt; 服务器: 用刚才生成的私钥解密密钥2
服务器 --&gt;&gt; 客户端: 知道了

Note over 客户端, 服务器: [2] 此时交换密钥完成

客户端 -&gt;&gt; 服务器: HTTP 请求 1 （对称加密2）
服务器 --&gt;&gt; 客户端: HTTP 响应 1 （对称加密2）
客户端 -&gt;&gt; 服务器: ...

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，即使攻击者截获了“公钥是 ...”这条消息，也无助于攻击者破解出密钥2。安全性得到了保障。（真的吗？）&lt;/p&gt;
&lt;p&gt;然而，我们很快发现这并不安全。攻击者可以假装自己是真正的“服务器”，然后边和服务器通信，边和客户端通信，并在密钥交换过程中通过发送假公钥破解出密钥2。这就是臭名昭著的“HTTPS 中间人攻击”。具体过程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
participant 客户端
participant 中间人
participant 服务器

客户端 -&gt;&gt; 中间人: SYN
中间人 -&gt;&gt; 服务器: SYN
服务器 --&gt;&gt; 中间人: SYN ACK
中间人 --&gt;&gt; 客户端: SYN ACK
客户端 -&gt;&gt; 中间人: ACK
中间人 -&gt;&gt; 服务器: ACK

Note over 客户端, 服务器: [1] 以上操作为建立TCP 连接。

客户端 -&gt;&gt; 中间人: 请求公钥
中间人 -&gt;&gt; 服务器: 请求公钥
服务器 -&gt;&gt; 服务器: 生成一个私钥，然后导出公钥（真公钥）
服务器 --&gt;&gt; 中间人: 公钥是 ... （真）
中间人 -&gt;&gt; 中间人: 自己生成一个私钥，然后导出公钥（假公钥）
中间人 --&gt;&gt; 客户端: 公钥是 ... （假）
客户端 -&gt;&gt; 客户端: 生成一个对称加密密钥
客户端 -&gt;&gt; 中间人: 密钥2是 ... （假公钥加密）
中间人 -&gt;&gt; 中间人: 用假私钥解密密钥2并记住，然后用真公钥重新加密
中间人 -&gt;&gt; 服务器: 密钥2是 ... （真公钥加密）
服务器 -&gt;&gt; 服务器: 用刚才生成的私钥解密密钥2
服务器 --&gt;&gt; 中间人: 知道了
中间人 --&gt;&gt; 客户端: 知道了

Note over 客户端, 服务器: [2] 此时交换密钥完成

客户端 -&gt;&gt; 中间人: HTTP 请求 1 （对称加密2）
中间人 -&gt;&gt; 中间人: 由于已经掌握密钥2，可以进行暗箱操作（查看或篡改）
中间人 -&gt;&gt; 服务器: HTTP 请求 1 （对称加密2）
服务器 --&gt;&gt; 中间人: HTTP 响应 1 （对称加密2）
中间人 -&gt;&gt; 中间人: 暗箱操作（查看或篡改）
中间人 --&gt;&gt; 客户端: HTTP 响应 1 （对称加密2）
客户端 -&gt;&gt; 中间人: ...

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们将客户端或服务器经历的事件单独提出来，我们会发现，这些事件与没有发生攻击时长得一模一样。客户端和服务器都被蒙骗了：客户端以为中间人就是服务器，服务器以为中间人就是客户端。结果，中间人掌握了所有通信内容。&lt;/p&gt;
&lt;h2&gt;引入证书&lt;/h2&gt;
&lt;p&gt;在数字世界里，一切信息都是可以被复制的。正因如此，中间人假扮成服务器也毫无难度。&lt;/p&gt;
&lt;p&gt;从上面的攻击过程中可以看出，只要要求服务器证明自己就是真正的服务器，问题就可以得到解决。但由于信息的可复制性，我们不能指望服务器“自证清白”，而必须相信权威，让受信任第三方进行认证。一种方法就是要求服务器出示第三方颁发的证书。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/3df671991ee42b138fa7217cd68d9e19.Bj1XEnms_Z24nE1X.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.ImgCap&gt;▲ 浏览器因发现网站证书存在问题而拒绝连接&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;证书由第三方机构颁发，因此证书中需要包含第三方机构的信息；证书用于证明服务器对应某个特定网站，因此必须包含接收证书的网站的信息；证书只能由合法持有证书的服务器使用，因此上述过程中使用的公钥应当与证书捆绑在一起，并且对应私钥仅由服务器持有。&lt;/p&gt;
&lt;p&gt;根据这些要求，我们设计出了第一版证书：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;[Certificate]
subject = example.com,*.example.com   # 表示这是网站example.com和*.example.com的证书
issuer = ca.xxtest.org   # 证书的颁发机构
subject.name = 测试网站
issuer.name = 新新测试证书颁发机构
pk = ...   # 公钥
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有持有私钥的网络节点才是证书的持有者。私钥并不包含在证书中，所以可以放心地将证书向大众展示。&lt;/p&gt;
&lt;p&gt;权威的力量来源于信任。受信任的第三方颁发机构必须遵循严格的程序，在确认证书申请人对目标网站确实有控制权时，才将证书和私钥交给申请人。&lt;/p&gt;
&lt;p&gt;然后，我们看一看前面利用非对称加密的 HTTPS 设计。我们将“请求公钥”地操作操作换成“请求证书”。客户端收到证书后，先验证证书是否有效、是否匹配当且访问的网站。如果证书不对，客户端直接断开连接、终止操作。否则，客户端就使用证书上的公钥继续操作。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
participant 客户端
participant 服务器

客户端 -&gt;&gt; 服务器: SYN
服务器 --&gt;&gt; 客户端: SYN ACK
客户端 -&gt;&gt; 服务器: ACK

Note over 客户端, 服务器: [1] 以上操作为建立TCP 连接。

客户端 -&gt;&gt; 服务器: 请求证书
服务器 --&gt;&gt; 客户端: 证书是 ...
客户端 -&gt;&gt; 客户端: 判断证书是否合法
客户端 -&gt;&gt; 客户端: 生成一个对称加密密钥
客户端 -&gt;&gt; 服务器: 密钥2是 ... （非对称加密）
服务器 -&gt;&gt; 服务器: 用刚才生成的私钥解密密钥2
服务器 --&gt;&gt; 客户端: 知道了

Note over 客户端, 服务器: [2] 此时交换密钥完成

客户端 -&gt;&gt; 服务器: HTTP 请求 1 （对称加密2）
服务器 --&gt;&gt; 客户端: HTTP 响应 1 （对称加密2）
客户端 -&gt;&gt; 服务器: ...

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们再看看中间人攻击。中间人对目标网站并没有控制权，因此无法从第三方机构获得有效的证书；中间人亦不是证书的“合法持有者”，没有证书对应的私钥，不能解密非对称加密的密钥2。攻击无法进行。&lt;/p&gt;
&lt;h2&gt;证书的具体实现&lt;/h2&gt;
&lt;p&gt;看看我们刚才的证书。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;[Certificate]
subject = example.com,*.example.com   # 表示这是网站example.com和*.example.com的证书
issuer = ca.xxtest.org   # 证书的颁发机构
subject.name = 测试网站
issuer.name = 新新测试证书颁发机构
pk = ...   # 公钥
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然，这个证书十分脆弱。它既不能证明自己由“新新测试证书颁发机构”颁发，也不能证明自己没有被篡改过。攻击者截获通信后，想要篡改证书上的公钥，易如反掌。&lt;/p&gt;
&lt;p&gt;而能用于保护证书完整性与可靠性的，正是非对称加密的“签名”功能。&lt;/p&gt;
&lt;p&gt;权威颁发机构要有权威，首先得有档案。我们同样用一张证书表示证书颁发机构的档案（下面这张证书的颁发机构为其本身，称为“自签名证书”。自签名证书代表的证书颁发机构叫“根颁发机构”）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;[Certificate]
subject = ca.xxtest.org
issuer = ca.xxtest.org
subject.name = 新新测试证书颁发机构
issuer.name = 新新测试证书颁发机构
pk = ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么，何谓“权威”？保证按照严格程序颁发证书，经过大众认证，然后被操作系统开发者以补丁形式加入受信任颁发机构库的，即是权威证书颁发机构。&lt;/p&gt;
&lt;p&gt;同样地，颁发机构的证书也有一个公钥，且持有对应私钥者才是证书的合法持有者。&lt;/p&gt;
&lt;p&gt;要证明证书未被篡改，需要利用哈希算法。这类算法可以将任何数据缩短为一段长度固定的“数据摘要”（记作 $h = \text{Hash}(P)$）。显然，不可能保证对于不同的 $P$，$h$ 一定不同。因此用于密码学的“密码哈希函数”还需要满足以下条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;抗修改：一旦 $P$ 改动一点，$h$ 就会截然不同。&lt;/li&gt;
&lt;li&gt;无法逆推：已知 $h$，反推出一个可能的 $P$ 是不可行的。&lt;/li&gt;
&lt;li&gt;非定向抗碰撞：很难找到两个不同的 $P_1, P_2$，使得 $h_1 = h_2$。&lt;/li&gt;
&lt;li&gt;定向抗碰撞：已知 $P_1$，很难找到 $P_2$，使得 $h_1 = h_2$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;提示：“不可行”“很难”表示消耗时间极长，几乎无法实现，而不代表“不可能”。&lt;/p&gt;
&lt;p&gt;我们向证书中添加一个叫“hash”的值。这个值，是证书内其他部分的 SHA-256（其他较强的哈希算法也可以）值 &lt;code&gt;(&lt;/code&gt; 用颁发机构的私钥签名（即解密）后 &lt;code&gt;)&lt;/code&gt; 的结果（此处用到了非对称加密的签名功能）。&lt;/p&gt;
&lt;p&gt;一旦证书遭到篡改，其 hash 值就会改变，因此只需判断证书上的 hash 是否正确，就能判断证书是否被篡改，至此“证书未被篡改”得到证明。而要算出新的 hash 并修改证书，必须知道颁发机构的私钥，因此我们确定只有颁发机构对应证书的合法持有者可以以这个颁发机构的名义签发证书。至此，“证书由权威机构颁发”这件事也得到了证明。&lt;/p&gt;
&lt;p&gt;最后的证书大致这样。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;[Certificate]
subject = ca.xxtest.org
issuer = ca.xxtest.org
subject.name = 新新测试证书颁发机构
issuer.name = 新新测试证书颁发机构
pk = ...   # 颁发机构也有一个公钥。其对应的私钥由颁发机构管理，不外传。
hash.type = SHA-256
hash = ...   # 自签名证书的hash应该用自己对应的私钥解密。
date = 2019/10/27 21:03:00 - 2036/10/26 21:03:00   # 签发日期 - 有效期至
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;[Certificate]
subject = example.com,*.example.com   # 表示这是网站example.com和*.example.com的证书
issuer = ca.xxtest.org   # 证书的颁发机构
subject.name = 测试网站
issuer.name = 新新测试证书颁发机构
pk = ...   # 公钥
hash.type = SHA-256
hash = ...
ocsp = http://ca.xxtest.org/api/ocsp/   # 颁发者向证书内加入的一个网址
date = 2019/10/27 21:04:00 - 2020/10/27 21:04:00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（目前，这是我们设计的初级版证书。实际的网络证书比这个要复杂，并且编码格式不同）&lt;/p&gt;
&lt;p&gt;现在，检查证书是否有效的步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端根据证书中的 &lt;code&gt;issuer&lt;/code&gt;，在操作系统中的可信颁发机构库中找到颁发机构的证书。如果找不到，即认为无效。&lt;/li&gt;
&lt;li&gt;客户端用颁发机构的公钥重新加密证书中的 &lt;code&gt;hash&lt;/code&gt; 参数，并按照 &lt;code&gt;hash.type&lt;/code&gt; 参数指定的算法对证书的其余部分进行哈希。如果两者不匹配，即认为无效。&lt;/li&gt;
&lt;li&gt;客户端检查证书中指定的网址是否与访问的网址匹配。如果不匹配，即认为无效。&lt;/li&gt;
&lt;li&gt;客户端根据系统时间（可见系统时间正确的重要性）判断此证书是否“尚未签发”，或已过期。如果当前不在有效期内，即认为无效。&lt;/li&gt;
&lt;li&gt;客户端使用未加密的 HTTP 协议请求 &lt;code&gt;ocsp&lt;/code&gt; 指定的网址。如果该网址显示当前证书已经被吊销，即认为无效。&lt;/li&gt;
&lt;li&gt;认为证书有效并继续操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样，除非攻击者黑进了证书颁发机构（概率极小），或者服务器使用的证书私钥泄露且忘记吊销（此时站长后果自负），或者私钥泄露并且同时OCSP被劫持（概率极小），攻击者无法进行中间人攻击。&lt;/p&gt;
&lt;p&gt;现在，我们可以给出加密 HTTP 的完整方案了（~~为了体现操作次数的多~~，将DNS也加入了其中（功能是将好记的域名转化成计算机可访问的IP地址））：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
participant DNS
participant 客户端
participant 服务器

客户端 -&gt;&gt; DNS: 查询example.com
DNS --&gt;&gt; 客户端: IP 地址是 19.19.8.10

客户端 -&gt;&gt; 服务器: SYN
服务器 --&gt;&gt; 客户端: SYN ACK
客户端 -&gt;&gt; 服务器: ACK

Note over 客户端, 服务器: [1] 以上操作为建立TCP 连接。

客户端 -&gt;&gt; 服务器: 请求证书
服务器 --&gt;&gt; 客户端: 证书是 ...
客户端 -&gt;&gt; 客户端: 初步判断证书是否合法
客户端 -&gt;&gt; DNS: 查询ca.xxtest.org
DNS --&gt;&gt; 客户端: IP 地址是 19.26.8.17
客户端 -&gt;&gt; OCSP: 查询证书是否吊销
OCSP --&gt;&gt; 客户端: 然而并没有
客户端 -&gt;&gt; 客户端: 生成一个对称加密密钥
客户端 -&gt;&gt; 服务器: 密钥2是 ... （非对称加密）
服务器 -&gt;&gt; 服务器: 用刚才生成的私钥解密密钥2
服务器 --&gt;&gt; 客户端: 知道了

Note over 客户端, 服务器: [2] 此时交换密钥完成

客户端 -&gt;&gt; 服务器: HTTP 请求 1 （对称加密2）
服务器 --&gt;&gt; 客户端: HTTP 响应 1 （对称加密2）
客户端 -&gt;&gt; 服务器: ...

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;在本文中，我们首先尝试了用对称加密算法对 HTTP 进行加密。然后，为了保护密钥交换环节，引入了非对称加密。最终，为了防止中间人攻击，我们设计出了证书和“权威机构”的概念，并利用非对称加密的签名功能证明了证书的有效性。&lt;/p&gt;
&lt;h2&gt;细节：证明控制权&lt;/h2&gt;
&lt;p&gt;我们说过，只有证书申请者证明控制权后，颁发机构才可以对其发放该网站的证书。证明控制权的方法主要有以下几个。大多数情况下，这些方法会同时存在，申请者只要通过其中一个验证即可拿到证书。&lt;/p&gt;
&lt;p&gt;提示：这些自动化的方法只适用于 DV 证书，也就是仅仅证明服务器身份的证书。能同时证明企业身份的 OV 和 EV 证书必须通过人工验证来取得。&lt;/p&gt;
&lt;h3&gt;1 文件证明法&lt;/h3&gt;
&lt;p&gt;证明时，颁发机构给出一文件路径（如 &lt;code&gt;//.well-known/acme-challenge/51a2b8695e5ca9755cdb8445431327c9&lt;/code&gt;）和另一串随机字符串 &lt;code&gt;26148d621ef74844918af182d63976b6&lt;/code&gt;。确认后，颁发机构通过未加密的 HTTP 访问网站上的对应路径。若返回了与另一随机字符串相同的文件，则验证通过。&lt;/p&gt;
&lt;p&gt;该方法考察用户对特定文件夹中内容的控制权。该文件夹通常都位于 &lt;code&gt;//.well-known/&lt;/code&gt; 下。因此建立 Web 服务器后分配权限时，这块区域的权限需要谨慎控制。&lt;/p&gt;
&lt;p&gt;许多 Web 服务器对静态文件请求的响应以文件为依据，故这种方法被称为“文件证明法”。&lt;/p&gt;
&lt;p&gt;优点：文件内容设定后即时生效，验证不会因为无法掌控的缓存而失败。&lt;/p&gt;
&lt;p&gt;缺点：权限控制不当可能导致不该申请证书的人拿到证书；不一定适合所有服务端环境。&lt;/p&gt;
&lt;h3&gt;2 DNS 记录证明法&lt;/h3&gt;
&lt;p&gt;证明时，颁发机构给出一 DNS 记录名称（如 &lt;code&gt;acme-challenge-51a2b8695e5ca975&lt;/code&gt;）和另一串随机字符串 &lt;code&gt;26148d621ef74844918af182d63976b6&lt;/code&gt;。确认后，颁发机构检查该 DNS 记录。若类型为 &lt;code&gt;TXT&lt;/code&gt;（有的机构允许 &lt;code&gt;CNAME&lt;/code&gt;）且内容与随机字符串匹配，则验证通过。&lt;/p&gt;
&lt;p&gt;优点：权限容易控制；操作简单，自动化容易；适合任何服务端。&lt;/p&gt;
&lt;p&gt;缺点：证书申请者无法控制的 DNS 缓存可能导致验证失败，因此修改记录后通常需要等一段时间再确认。&lt;/p&gt;
&lt;h3&gt;3 邮件证明法&lt;/h3&gt;
&lt;p&gt;支持该方法的颁发机构并不多。&lt;/p&gt;
&lt;p&gt;颁发机构向代表统治地位的 &lt;code&gt;admin@example.com&lt;/code&gt; &lt;code&gt;webmaster@example.com&lt;/code&gt; 的邮箱中的其中一个发送链接，点击链接后完成验证。&lt;/p&gt;
&lt;p&gt;优点：操作方便；适合几乎所有服务端。&lt;/p&gt;
&lt;p&gt;缺点：不利于自动化；安全性不高。&lt;/p&gt;
&lt;p&gt;提示：证书颁发机构服务器的通信通道一般受到严格保护，因此即使使用未加密的协议与目标服务器联络，也很难受到劫持。&lt;/p&gt;
&lt;h2&gt;性能比较与个人选择&lt;/h2&gt;
&lt;p&gt;HTTPS 的全过程图就在上面，这就不搬下来了。我们再看看 HTTP 的全过程图：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
participant DNS
participant 客户端
participant 服务器

客户端 -&gt;&gt; DNS: 查询example.com
DNS --&gt;&gt; 客户端: IP 地址是 19.19.8.10

客户端 -&gt;&gt; 服务器: SYN
服务器 --&gt;&gt; 客户端: SYN ACK
客户端 -&gt;&gt; 服务器: ACK

Note over 客户端, 服务器: 以上操作为建立TCP 连接。

客户端 -&gt;&gt; 服务器: HTTP 请求 1
服务器 --&gt;&gt; 客户端: HTTP 响应 1
客户端 -&gt;&gt; 服务器: HTTP 请求 2
服务器 --&gt;&gt; 客户端: HTTP 响应 2

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观察流程发现，实际上除去 &lt;code&gt;ACK&lt;/code&gt; 后，实线箭头和虚线箭头可以一一配对。我们将一个实线箭头和其下面的虚线箭头组成一对，称为一个 RTT。&lt;/p&gt;
&lt;p&gt;HTTP 在开始传输数据之前，经历了两个 RTT。&lt;/p&gt;
&lt;p&gt;HTTPS 在开始传输数据之前，经历了六个 RTT。而用户第一次访问网站时，浏览器默认访问 HTTP 而非 HTTPS，因此还要跳转到 HTTPS，此时就总计有八个 RTT。&lt;/p&gt;
&lt;p&gt;但在开始传输数据之后，两种协议的 请求-响应 过程均只占一个 RTT，只不过 HTTPS 多了一个加密过程（即使使用百兆宽带，这个时间也不到总传输时间的 5%）。&lt;/p&gt;
&lt;p&gt;那么，总体下来，HTTP 速度岂不是吊打 HTTPS？&lt;/p&gt;
&lt;p&gt;$\text{\huge{你错了！}}$&lt;/p&gt;
&lt;p&gt;$\text{都 1202 年了还说 HTTPS 慢}$&lt;/p&gt;
&lt;p&gt;早在 3102 时，大部分主流网站就已经强制使用 HTTPS。然而，绝大多数人都没有感觉到网络变慢了。时至今日，HTTPS 的速度常常超过 HTTP。&lt;/p&gt;
&lt;p&gt;原因主要有以下几个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;HTTPS 本身经过很多优化。在它得到普及之前，它就已经有了可以和 HTTP 媲美的速度。&lt;/li&gt;
&lt;li&gt;自古以来，运营商（或国家）就会收集和审查未加密的网络数据，甚至会试图向其中插入广告。这会大幅度拖慢速度。而 HTTPS 让运营商无计可施。&lt;br&gt;
（网络加密的魅力正是在于：无论你是平民，是高官，是&amp;#x3C;F.Redacted&gt;锟斤拷&amp;#x3C;/F.Redacted&gt;，还是&amp;#x3C;F.Redacted&gt;锟斤拷&amp;#x3C;/F.Redacted&gt;，你都无法查看加密后的数据）&lt;/li&gt;
&lt;li&gt;目前一些较新的网络技术都只适用于 HTTPS，比如最近新出的 HTTP/2（SPDY）和 HTTP/3（QUIC）。这使得 HTTPS 的速度进一步赶超了 HTTP。（实际上很多只适用于 HTTPS 的技术理论上同样可以用于 HTTP，但是如今，浏览器和服务器通常只为 HTTPS 实现了这些技术）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么，如果自己建设网站，是否需要使用 HTTPS 呢？答案显然是肯定的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器的种种限制，使得未加密的网站在互联网中难以立足，而 HTTPS 的绿锁标志允许任何人放心地留下自己的邮箱并设置密码。&lt;/li&gt;
&lt;li&gt;妥善管理的 HTTPS 使得攻击者失去机会，大大降低受攻击的概率。&lt;/li&gt;
&lt;li&gt;隐私窥探与广告植入仍然存在。使用 HTTPS 可以杜绝被这类行为拖慢速度的可能。&lt;/li&gt;
&lt;li&gt;使用 HTTPS 也能保护自己的隐私。&lt;/li&gt;
&lt;li&gt;自 Let&apos;s Encrypt 项目带来清流起，HTTPS 证书已经不再是“证明站长有钱”的工具了。经过合适的调整，免费 HTTPS 证书方案可以做到一劳永逸。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;感谢阅读。如果发现有问题，欢迎留言。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Muted&gt;封面图来自网络，未经明确授权。&amp;#x3C;/F.Muted&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/hero.JLV3hmVF.png"/><enclosure url="/_astro/hero.JLV3hmVF.png"/></item><item><title>[洛谷题解] CF547A Mike and Frog</title><link>https://ak-ioi.com/blog/algo-solution/luogu-cf547a</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-cf547a</guid><description>一道较为暴力的数学问题。思路简单，但是有助于增强代码能力。</description><pubDate>Wed, 23 Oct 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目链接：&lt;a href=&quot;https://www.luogu.org/problemnew/solution/CF547A&quot;&gt;CF547A Mike and Frog&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目大意&lt;/h2&gt;
&lt;p&gt;你有两个数 $H_1$、$H_2$。每次操作，你可以将 $H_1$ 变成 $(H_1 \cdot x_1 + y_1)\ mod\ M$，$H_2$ 变成 $(H_2 \cdot x_2 + y_2)\ mod\ M$ （两个变换会在一次操作时同时进行）。&lt;/p&gt;
&lt;p&gt;问，你至少需要进行多少次操作，才能使得 $H_1 = A_1$ 和 $H_2 = A_2$ 同时满足。&lt;/p&gt;
&lt;p&gt;数据范围：$H_1,x_1,y_1 &amp;#x3C;M$ ，$H_2,x_2,y_2 &amp;#x3C; M$ ，$2 \leq M \leq 10^6$&lt;/p&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;p&gt;注意到本题 $M$ 只有 $10^6$，我么可以对于 $H_1$ 和 $H_2$ 分别暴力模拟。&lt;/p&gt;
&lt;p&gt;对于一个数 $a$，如果不断进行操作，其变化规律应当如下：&lt;/p&gt;
&lt;p&gt;a → b → (c → d → e → f) → (c → d → e → f) → ...&lt;/p&gt;
&lt;p&gt;感性理解一下，我们发现这个数进行一定次数操作后一定会进入一个循环。&lt;/p&gt;
&lt;p&gt;我们称这个数达到第一个 $c$ （如上面的描述）之后就算进入了循环，到达第二个 $c$ 时算“发生循环”。那么，一个数的变化规律可以这么描述：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于任意一个数，其变换 $0$ 次或若干次就会进入一个长度大于 $0$ （废话），且小于等于 $M$ 的循环。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如，上面的描述中，$a$ 在变换两次后就进入了一个长度为 $4$ 的循环。&lt;/p&gt;
&lt;p&gt;我们用 $a^k$ 表示数 $a$ 操作 $k$ 次后的结果，用 $rd_a$ 表示 $a$ 进入循环前的变换次数，用 $r_a$ 表示 $a$ 进入的循环的长度。&lt;/p&gt;
&lt;p&gt;对于 $H_1$，我们先暴力求出一个最小的 $cnt_{H_1}$（其实这样表示不准确，但是为了和$rd$、$r$的表达方式对等所以这么写），使得 $H_1^{cnt_{H_1}} = A_1$。我们还可以暴力求出 $rd_{H_1}$ 和 $r_{H_1}$。&lt;/p&gt;
&lt;p&gt;然后我们很容易知道 $H_1^x = A_1$ 的条件。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 $cnt_{H_1} &amp;#x3C; rd_{H_1}$，则当且仅当正整数 $k$ 满足 $k = cnt_{H_1}$ 时，有 $H_1^k = A_1$&lt;/li&gt;
&lt;li&gt;否则，当且仅当正整数 $k$ 同时满足以下条件时，有 $H_1^k = A_1$
&lt;ul&gt;
&lt;li&gt;$k \geq rd_{H_1}$ （显然）&lt;/li&gt;
&lt;li&gt;$(k - rd_{H_1})\ mod\ r_{H_1} = (cnt_{H_1} - rd_{H_1})$ （即，经过若干次循环以后会回到我们需要的位置）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后我们对于 $H_2$ 也这样计算。随后我们分类讨论如何求得答案$ans$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果两个数的 $cnt$ 均小于 $rd$，那么，$ans$ 必须满足 $ans = cnt_{H_1}$ 和 $ans = cnt_{H_2}$。那么，当且仅当 $cnt_{H_1} = cnt_{H_2}$ 时有解，解是 $cnt_{H_1}$。&lt;/li&gt;
&lt;li&gt;如果只有一个数的 $cnt$ 小于 $rd$，不妨令 $cnt_{H_1} &amp;#x3C; rd_{H_1}$。那么，必定有 $ans = cnt_{H_1}$。那么，当且仅当 $cnt_{H_1} \geq rd_{H_2}$ 且 $(cnt_{H_1} - rd_{H_2})\ mod\ r_{H_2} = (cnt_{H_2} - rd_{H_2})$ 时有解，解是 $cnt_{H_1}$。&lt;/li&gt;
&lt;li&gt;如果没有一个数的 $cnt$ 小于 $rd$，那么，$ans$ 必须满足：
&lt;ul&gt;
&lt;li&gt;$ans \geq rd_{H_1}$ $\texttt{\color{grey}condition 1}$&lt;/li&gt;
&lt;li&gt;$ans \geq rd_{H_2}$ $\texttt{\color{grey}condition 2}$&lt;/li&gt;
&lt;li&gt;$(ans - rd_{H_1})\ mod\ r_{H_1} = (cnt_{H_1} - rd_{H_1})$ $\texttt{\color{grey}condition 3}$&lt;/li&gt;
&lt;li&gt;$(ans - rd_{H_2})\ mod\ r_{H_2} = (cnt_{H_2} - rd_{H_2})$ $\texttt{\color{grey}condition 4}$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么对于第三种情况，我们定义变量 $p$，并赋值为满足条件1和3的最小值。很显然，此时 $p = cnt_{H_1}$。&lt;/p&gt;
&lt;p&gt;随后，我们不停地将 $p$ 的值增加 $r_{H_1}$ （可知这样不会破坏条件1和3），直到其满足条件2。由于 $rd_{H_2}$ 最大只能为 $M-1$，所以不会超时。此时的 $p$ 已经同时满足条件1，2和3。&lt;/p&gt;
&lt;p&gt;最后我们再将 $p$ 的值不停增加 $r_{H_1}$ （可知这样不会破坏条件1，2，3），直到 $p$ 满足条件4（此时答案为 $p$），或者已经增加了超过 $M$ 次（此时无解）。&lt;/p&gt;
&lt;p&gt;综合所有情况，就是正解了。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;注意千万不要把 &lt;code&gt;y1&lt;/code&gt; 定义成全局变量！如果评测机编译器版本较老，这会和 &lt;code&gt;math.h&lt;/code&gt; 冲突，造成错误。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [Accepted]
// oj:     [%uContest]

#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;

// 不开 long long 见祖宗
typedef long long ll;
#define int ll

// 题目中的 M
int mod1;

// 让 h 每次以 h = (h * x + y) % mod1 的形式变换，返回变换到 a 所需的最少步骤数。
// 如果无法变换到 a，返回-1
// * flag: 是否允许在 h==a 时返回0
int findNext(int h,int a,int x,int y,bool flag = false) {
    int cnt = 0;
    if(flag &amp;#x26;&amp;#x26; h==a) return 0;
    do {
        cnt++;
        h = (h * x + y) % mod1;
        // 如果变换超过 M 次还没有找到答案，那么此时肯定已经“发生循环”，不可能还会找到答案。
        if(cnt &gt; mod1) {
            return -1;
        }
    } while(h != a);
    return cnt;
}

int st[1000000];
int st_t = 0;
// 让 h 每次以 h = (h * x + y) % mod1 的形式变换，返回 h 初次“进入循环”时变成的数。
int findCyclicNode(int h,int x,int y) {
    // 可以直接用数组统计是否出现。此处使用一个计数量(st_t)，可以避免清空数组的麻烦
    ++st_t;
    // 此处要先记录访问 h 一次，否则如果 h 本身就在循环内，会出现问题。
    st[h] = st_t;
    // 由于一个数不断变换必定会出现循环，因此while(1)即可。
    while(1) {
        h = (h * x + y) % mod1;
        // 第一个出现超过 1 次的值，一定是初次“进入循环”时的值
        if(st[h] == st_t) return h;
        st[h] = st_t;
    }
}

// 除法上取整（避免double精度问题）
// * 这份代码中没有用到此函数
int divCeil(int x,int y) {
    return x/y + int(bool(x%y));
}

signed main() {
#ifdef OFFLINE_JUDGE
    freopen(&quot;water.in&quot;,&quot;r&quot;,stdin);
    freopen(&quot;water.out&quot;,&quot;w&quot;,stdout);
#endif

    // 不要将 y1 作为全局变量，否则要换个名字。
    int h1,a1,x1,y1;
    int h2,a2,x2,y2;

    ios::sync_with_stdio(false);
    cin&gt;&gt;mod1&gt;&gt;h1&gt;&gt;a1&gt;&gt;x1&gt;&gt;y1&gt;&gt;h2&gt;&gt;a2&gt;&gt;x2&gt;&gt;y2;

    // 求cnt_{H_1}
    int cnt1 = findNext(h1,a1,x1,y1);
    int cnt2 = findNext(h2,a2,x2,y2);

    // H_1 和 H_2 有一个无法到达 A_1 或 A_2。无解。
    if(cnt1 == -1 || cnt2 == -1) {
        cout&amp;#x3C;&amp;#x3C;-1&amp;#x3C;&amp;#x3C;endl;
        exit(0);
    }

    // 先寻找初次进入循环时到达的数字
    int p1 = findCyclicNode(h1,x1,y1);
    int p2 = findCyclicNode(h2,x2,y2);

    // 再求循环长度
    int r1 = findNext(p1,p1,x1,y1);
    int r2 = findNext(p2,p2,x2,y2);

    // 然后求初次进入循环之前的步骤数
    int rd1 = findNext(h1,p1,x1,y1,true);
    int rd2 = findNext(h2,p2,x2,y2,true);

    // cnt - rd 在解法中被频繁使用，创建变量以图方便。
    int d1 = cnt1 - rd1;
    int d2 = cnt2 - rd2;

    // 忘记删除的调试信息（在一般的平台下，使用cerr输出调试信息可避免一部分的“不删调试见祖宗”）
    cerr&amp;#x3C;&amp;#x3C;&quot;1: &quot;&amp;#x3C;&amp;#x3C;r1&amp;#x3C;&amp;#x3C;&apos; &apos;&amp;#x3C;&amp;#x3C;rd1&amp;#x3C;&amp;#x3C;&apos; &apos;&amp;#x3C;&amp;#x3C;d1&amp;#x3C;&amp;#x3C;endl;
    cerr&amp;#x3C;&amp;#x3C;&quot;2: &quot;&amp;#x3C;&amp;#x3C;r2&amp;#x3C;&amp;#x3C;&apos; &apos;&amp;#x3C;&amp;#x3C;rd2&amp;#x3C;&amp;#x3C;&apos; &apos;&amp;#x3C;&amp;#x3C;d2&amp;#x3C;&amp;#x3C;endl;

    // 如果两个数都有 cnt &amp;#x3C; rd （即 cnt - rd &amp;#x3C; 0）
    if(d1 &amp;#x3C; 0 &amp;#x26;&amp;#x26; d2 &amp;#x3C; 0) {
        if(cnt1 == cnt2) {
            cout&amp;#x3C;&amp;#x3C;cnt1&amp;#x3C;&amp;#x3C;endl;
        }
        else {
            cout&amp;#x3C;&amp;#x3C;-1&amp;#x3C;&amp;#x3C;endl;
        }
        exit(0);
    }
    // 如果只有一个数
    else if(d1 &amp;#x3C; 0) {
        if(cnt1 - rd2 &gt;= 0 &amp;#x26;&amp;#x26; (cnt1 - rd2) % r2 == d2) {
            cout&amp;#x3C;&amp;#x3C;cnt1&amp;#x3C;&amp;#x3C;endl;
        }
        else {
            cout&amp;#x3C;&amp;#x3C;-1&amp;#x3C;&amp;#x3C;endl;
        }
        exit(0);
    } else if(d2 &amp;#x3C; 0) {
        if(cnt2 - rd1 &gt;= 0 &amp;#x26;&amp;#x26; (cnt2 - rd1) % r1 == d1) {
            cout&amp;#x3C;&amp;#x3C;cnt2&amp;#x3C;&amp;#x3C;endl;
        }
        else {
            cout&amp;#x3C;&amp;#x3C;-1&amp;#x3C;&amp;#x3C;endl;
        }
        exit(0);
    }

    // 如果没有数满足 cnt &amp;#x3C; rd
    // 先使p满足条件1和3
    int p = rd1 + d1;
    // 然后满足2
    while(p &amp;#x3C; rd2) p += r1;
    // 最后满足4
    for(int i=1;i&amp;#x3C;=r2+1;i++,p+=r1) {
        if((p-rd2) % r2 == d2) {
            cout&amp;#x3C;&amp;#x3C;p&amp;#x3C;&amp;#x3C;endl;
            exit(0);
        }
    }

    // 无解
    cout&amp;#x3C;&amp;#x3C;-1&amp;#x3C;&amp;#x3C;endl;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;评测记录：&lt;a href=&quot;https://www.luogu.org/record/25577582&quot;&gt;R25577582&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[洛谷题解] CF1239C Queue in the Train</title><link>https://ak-ioi.com/blog/algo-solution/luogu-cf1239c</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-cf1239c</guid><description>一道非常有技巧性，使用了事件化处理的模拟题。</description><pubDate>Mon, 21 Oct 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目链接：&lt;a href=&quot;https://www.luogu.org/problem/CF1239C&quot;&gt;CF1239C Queue in the Train&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;题目标题&lt;/h2&gt;
&lt;p&gt;火车上的队列&lt;/p&gt;
&lt;h2&gt;题意简述&lt;/h2&gt;
&lt;p&gt;火车上有一排座位，标号 $1..n$ 。座位上的每个人需要去接开水~~烧方便面（题目这么说的）~~。每个人接开水都一致地需要 $p$ 分钟，而走动的时间可忽略不计。第 $i$ 个人从第 $t_i$ 分钟开始，就会打算去接开水。&lt;/p&gt;
&lt;p&gt;同一时刻只能有一人在接水，因此，自然会出现排队的情况。&lt;/p&gt;
&lt;p&gt;然而人们都不想排队。第 $i$ 个人会关注第 $1$ 到 $i-1$ 个座位（如果这些座位是空的，那么座位上的人肯定是在接水或者排队）。即，第 $i$ 个人会在第 $t_i$ 分钟后的，&lt;code&gt;(&lt;/code&gt;第 $1$ 到第 $i-1$ 个座位都不是空的&lt;code&gt;)&lt;/code&gt;的最早时间离开座位，去接水或者排队。&lt;/p&gt;
&lt;p&gt;另外，还有一个潜规则：如果在同一时刻，有多个人可以离开座位，那么只有坐在编号最小的座位上的人会离开座位，其他的则继续等待。&lt;/p&gt;
&lt;p&gt;现在，你需要计算每个人接完水的时刻。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;p&gt;这题题目中存在很多要维护的东西，比如，正在等待的人的集合、队列中的人的集合（包括正在接水的那个），以及当前时间。如果使用普通的模拟做法，则较为难写。如果将时间离散化，按照时刻处理，同样非常难处理。&lt;/p&gt;
&lt;p&gt;那么，我们可以使用“事件化”的办法进行模拟。&lt;/p&gt;
&lt;p&gt;此处定义“事件”是一个三元组 $(time,type,id)$ 。其中， $time$ 表示事件发生的时间， $type$ 表示事件类型（$0$ 表示接水完成，离开接水的队列；$1$ 表示开始等待）， $id$ 则表示事件的对象（即，受影响的人的编号）。&lt;/p&gt;
&lt;p&gt;一开始，有 $n$ 个事件，即，对于所有 $1 \leq i \leq n$ 的 $(t_i,1,i)$。我们将这 $n$ 个事件放入事件队列。&lt;/p&gt;
&lt;p&gt;我们还需要维护以下内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$wait$ ，表示当前正在等待的人的集合（这些人还在座位上）。维护的数据结构需要支持以下操作（因此选择使用优先队列）：
&lt;ul&gt;
&lt;li&gt;取出最小值。对于任何等待队列中的人可以进入接水队列的情景，一定是最小值优先。&lt;/li&gt;
&lt;li&gt;删除最小值。当一个人进入了接水队列，就不再属于等待队列。&lt;/li&gt;
&lt;li&gt;插入值。当处理到 $type = 1$ 的事件时，对应的人要进入等待队列。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;$q$ ，表示当前接水队列中的人的集合（包括正在排队和正在接水的）。需要支持以下操作（因此选择使用set）：
&lt;ul&gt;
&lt;li&gt;删除指定值。当处理 $type = 0$ 的事件时，需要将指定的值从队列中移除&lt;/li&gt;
&lt;li&gt;取出最小值。如果等待队列中编号最小的人编号比接水队列中的最小编号还要小，这个人才能进入接水队列。&lt;/li&gt;
&lt;li&gt;插入值。如果等待队列中编号最小的人编号比接水队列中的最小编号还要小，这个人就要进入接水队列。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;$currtime$ ，当前正在处理的事件发生的时间。&lt;/li&gt;
&lt;li&gt;$emptytime$ 。在不会再有人进入接水队列的情况下，预计接水队列将会为空的时刻。或者说，最后一个进入接水队列的人完成接水的时刻。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;随后，我们从事件队列中取出最早发生的事件，并将其删除（我们认为，$type = 0$ 的事件总是在 $type = 1$ 的事件之前进行（原因见下文），并且，对于 $type = 1$ 的事件，$id$ 小的先进行）。随后，我们对这个事件进行处理。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 $type = 0$，那么直接从接水队列中将对应的人删除&lt;/li&gt;
&lt;li&gt;如果 $type = 1$，那么将指定的人加入等待队列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在处理完一个事件后，要么是接水队列改变，要么是等待队列改变。这些改变可能会引起当前等待队列中编号最小的人编号比接水队列中的最小编号还要小。那么，这时候，等待队列中编号最小的人就需要进入接水队列。这个人进去之后，等待队列的最小编号必定要大于接水队列的最小编号，因此无需继续判断。&lt;/p&gt;
&lt;p&gt;一个人$t$从等待队列进入接水队列后，显然，他完成接水的时刻是 $max(currtime,emptytime) + p$ 。因此，我们要添加一个事件 $(max(currtime,emptytime) + p,0,t)$ ，然后将 $emptytime$ 更新为 $max(currtime,emptytime) + p$ 。此时，我们就可以进行答案的统计，因为我们知道了编号为$t$的人接水结束的时间。&lt;/p&gt;
&lt;p&gt;因此只要反复处理事件，直到事件队列为空，我们就求得了所有答案。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;问题：为什么要使用上文提到的，对事件的排序方式呢？&lt;/p&gt;
&lt;p&gt;如果 $type = 1$ 的操作先于 $type = 0$ 的执行，你会发现，你愉快地被样例叉掉了。因为，如果先执行 $type = 1$ ，那么在判断等待队列中的人是否能进入接水队列时，原本已经接水完成，要离开接水队列的人却没有即使离开，会导致影响判断。而如果在 $type = 1$ 时，$id$ 大的先执行，则会违反题目中提到的“潜规则”。&lt;/p&gt;
&lt;p&gt;总结：本题中，进入等待队列和离开接水队列的事件有着共同之处，那就是，①必须按照时间顺序交替处理，②处理完毕后，都必须检查等待队列的人能否进入接水队列。另外，我们需要求接水队列的最小值。如果使用队列&lt;code&gt;std::queue&lt;/code&gt;处理，则相当麻烦，而使用事件化的方式处理离开接水队列的事件，则完美地避免了使用队列，方便了最小值的统计。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [Accepted][unofficial]
// oj:     [Codeforces]

#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;

typedef long long ll;
#define int ll

const int MAXN = 100005;
int n,tt;
int t[MAXN];
int ans[MAXN];

// 事件
struct Event {
    int time; // 发生时间
    int type; // 类型
    int id;   // 影响到的人
    Event() {}
    Event(int _time,int _type,int _id) {time=_time;type=_type;id=_id;}
    bool operator&amp;#x3C;(const Event &amp;#x26;other) const {
        if(time != other.time) return time &gt; other.time; // 此处的排序必须反过来，因为优先队列是从大到小排列的。
        if(type != other.type) return type &amp;#x3C; other.type;
        return id &gt; other.id;
    }
};

// 事件队列
priority_queue&amp;#x3C;Event&gt; ev;
// 接水队列
set&amp;#x3C;int&gt; q;
// 等待队列。使用模板 greater&amp;#x3C;int&gt;，以使得编号小的人先被取出。
priority_queue&amp;#x3C;int,vector&amp;#x3C;int&gt;,greater&amp;#x3C;int&gt; &gt; wait;

signed main() {
    ios::sync_with_stdio(false);

    cin&gt;&gt;n&gt;&gt;tt;
    for(int i=1;i&amp;#x3C;=n;i++) {
        cin&gt;&gt;t[i];
        // 创建一开始的n个事件
        ev.push(Event(t[i],true,i));
    }

    int currtime = 0;
    int emptytime = 0;
    while(!ev.empty()) {
        Event c = ev.top(); ev.pop();
        currtime = c.time;
        if(c.type == 0) {
            // cerr&amp;#x3C;&amp;#x3C;currtime&amp;#x3C;&amp;#x3C;&quot;: &quot;&amp;#x3C;&amp;#x3C;c.id&amp;#x3C;&amp;#x3C;&quot; left the queue.&quot;&amp;#x3C;&amp;#x3C;endl;
            q.erase(c.id);
        }
        else {
            // cerr&amp;#x3C;&amp;#x3C;currtime&amp;#x3C;&amp;#x3C;&quot;: &quot;&amp;#x3C;&amp;#x3C;c.id&amp;#x3C;&amp;#x3C;&quot; became waiting.&quot;&amp;#x3C;&amp;#x3C;endl;
            wait.push(c.id);
        }

        // 这是等待队列中的人进入接水队列的条件
        while(!wait.empty() &amp;#x26;&amp;#x26; (q.empty() || wait.top() &amp;#x3C; *q.begin())) {
            int p = wait.top();wait.pop();
            // cerr&amp;#x3C;&amp;#x3C;currtime&amp;#x3C;&amp;#x3C;&quot;: &quot;&amp;#x3C;&amp;#x3C;p&amp;#x3C;&amp;#x3C;&quot; entered the queue.&quot;&amp;#x3C;&amp;#x3C;endl;
            q.insert(p);
            // 计算这个人接水完毕的时刻
            int et = max(currtime, emptytime) + tt;
            // 创建这个人离开接水队列的事件
            ev.push(Event(et,false,p));
            // 更新emptytime
            emptytime = et;
            // 统计答案
            ans[p] = et;
        }
    }

    for(int i=1;i&amp;#x3C;=n;i++) {
        if(i&gt;1) cout&amp;#x3C;&amp;#x3C;&apos; &apos;;
        cout&amp;#x3C;&amp;#x3C;ans[i];
    }
    cout&amp;#x3C;&amp;#x3C;endl;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;评测记录：&lt;a href=&quot;https://www.luogu.org/record/25487378&quot;&gt;R25487378&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[游记] CSP-S 2019 游记 (1)</title><link>https://ak-ioi.com/blog/diary/give-up-csp-2019-1</link><guid isPermaLink="true">https://ak-ioi.com/blog/diary/give-up-csp-2019-1</guid><pubDate>Sun, 20 Oct 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Day -28&lt;/h2&gt;
&lt;p&gt;记载时间：10月20日&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;由于某些原因&lt;/strong&gt;，今天并没有国赛难度的联赛模拟赛。&lt;/p&gt;
&lt;p&gt;上午，我刷了一套据说很水的初赛题，拿了88分。&lt;/p&gt;
&lt;p&gt;下午，教练咕咕，全场颓废（大概已经准备好第二天退役了）。&lt;/p&gt;
&lt;h2&gt;Day -27&lt;/h2&gt;
&lt;p&gt;记载时间：10月20日&lt;/p&gt;
&lt;h2&gt;考前&lt;/h2&gt;
&lt;p&gt;考点非常cxk，离我家有1.2小时的路程。&lt;/p&gt;
&lt;p&gt;不用说，我早上又起不来了。于是就急急忙忙吃完早饭，7:30就出发了。&lt;/p&gt;
&lt;p&gt;到了考点以后我好不容易才找到考试地点，中途还多次以为到了假的考点。到达之后，我遇到了几个同学。祝愿rp++吧。&lt;/p&gt;
&lt;h2&gt;考试时&lt;/h2&gt;
&lt;p&gt;考试顺利地开始了。&lt;/p&gt;
&lt;p&gt;这次试卷果然和之前的NOIp试卷不一样（说好的CSP和NOIp 没  有  关  系），全都是选择题，虽然还是分为常规选择题、阅读理解和完形填空三部分（多选没了）。&lt;/p&gt;
&lt;p&gt;常规选择题部分非常简单，计数问题都可以一定程度上暴力枚举，而且其中还有3道原题。&lt;/p&gt;
&lt;p&gt;阅读理解部分没有问到“一句话解释代码的功能”这类题目，因此都只需感性理解代码即可。有一定难度，但是我都顺利做出了。&lt;/p&gt;
&lt;p&gt;终于到了完形填空部分。第一题是个暴力算法，类似于拓扑排序的板子，非常好做（虽然有几个坑）。&lt;/p&gt;
&lt;p&gt;第二题是个状压题，非常毒瘤。于是我将其他题目都检查一遍之后，就开始刚这道题。&lt;/p&gt;
&lt;p&gt;我的模式是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while(1) {
	rand_ans();
	if(check()) break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果就是持续自闭。直到考试结束后，第三行的&lt;code&gt;break&lt;/code&gt;都没有执行，于是最后一个&lt;code&gt;rand_ans&lt;/code&gt;就被交上去了。&lt;/p&gt;
&lt;h2&gt;考后&lt;/h2&gt;
&lt;p&gt;那个状压题我出考场46秒就会了（然而我不记得选项了），所以我估计自己错了3道，最后估分85.&lt;/p&gt;
&lt;p&gt;后来对了3份答案，发现实际上82.5（阅读理解第二题是个假并查集，我没注意；阅读理解第三题无法读入空字符串，而我认为可以；状压题错了4个）。&lt;/p&gt;
&lt;p&gt;至于分数线，有的人说72，有的人说75，有的人说84，所以我现在也只能等着官方分数线出来了。&lt;/p&gt;
&lt;p&gt;~~（听说上海的机试分数线要98了？）~~&lt;/p&gt;
&lt;h2&gt;Day -15&lt;/h2&gt;
&lt;p&gt;浙江咕了，彻底咕了。&lt;/p&gt;
&lt;p&gt;~~原因：想找到集中在一个地方的1300个机位。~~&lt;/p&gt;
&lt;h2&gt;Day -10&lt;/h2&gt;
&lt;p&gt;机位找到了，成绩出来了。事实证明机位并没有1300个。&lt;/p&gt;
&lt;p&gt;反正我是进复赛了。于是我翻看了一下成绩表，发现我们学校第二名差了1分（分数线72.5，第二名71.5）。真是太惊险了。&lt;/p&gt;
&lt;p&gt;不管那么多了，准备复赛吧。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[洛谷题解] CF1244F Chips</title><link>https://ak-ioi.com/blog/algo-solution/luogu-cf1244f</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-cf1244f</guid><pubDate>Mon, 14 Oct 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目链接：&lt;a href=&quot;https://www.luogu.org/problem/CF1244F&quot;&gt;CF1244F Chips&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;题目描述&lt;/h2&gt;
&lt;p&gt;有$n$个棋子排成环状，标号为$1..n$&lt;/p&gt;
&lt;p&gt;一开始每个棋子都是黑色或白色的。随后有$k$次操作。操作时，棋子变换的规则如下：我们考虑一个棋子本身以及与其相邻的两个棋子（共3个），如果其中白子占多数，那么这个棋子就变成白子，否则这个棋子就变成黑子。注意，对于每个棋子，在确定要变成什么颜色之后，并不会立即改变颜色，而是等到所有棋子确定变成什么颜色后，所有棋子才同时变换颜色。&lt;/p&gt;
&lt;p&gt;对于一个棋子$i$，与其相邻的棋子是$i-1$和$i+1$。特别地，对于棋子$1$，与其相邻的棋子是$2$和$n$；对于棋子$n$，与其相邻的棋子是$1$和$n-1$。&lt;/p&gt;
&lt;p&gt;如图是在$n=6$时进行的一次操作。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/cc3a3695cc74d1bba6edf69f3fb62ea6.B2fnQJ0j_X3zsT.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;给你$n$和初始时每个棋子的颜色，你需要求出在$k$次操作后每个棋子的颜色。&lt;/p&gt;
&lt;h2&gt;输入格式&lt;/h2&gt;
&lt;p&gt;第一行两个整数$n\ (3 \leq n \leq 2\cdot 10^5)$和$k\ (1 \leq k \leq 10^9)$，表示棋子总数与操作次数。&lt;/p&gt;
&lt;p&gt;第二行是一个长度为$n$的，仅由字符$B$和$W$构成的字符串。如果第$i$个字符是$B$，表示第$i$个棋子位黑色，否则为白色。&lt;/p&gt;
&lt;h2&gt;输出格式&lt;/h2&gt;
&lt;p&gt;一行，长度为$n$的，仅由字符$B$和$W$构成的字符串，描述操作完成后每个棋子的颜色。&lt;/p&gt;
&lt;p&gt;注意输出的顺序要和输入数据中的对应。&lt;/p&gt;
&lt;h2&gt;样例解释&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;就是题目描述中举的那个例子&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&quot;WBWBWBW&quot; $\rightarrow$ &quot;WWBWBWW&quot; $\rightarrow$ &quot;WWWBWWW&quot; $\rightarrow$ &quot;WWWWWWW&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&quot;BWBWBW&quot; $\rightarrow$ &quot;WBWBWB&quot; $\rightarrow$ &quot;BWBWBW&quot; $\rightarrow$ &quot;WBWBWB&quot; $\rightarrow$ &quot;BWBWBW&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;提示&lt;/h2&gt;
&lt;p&gt;如果棋子数量是偶数，并且任意两个相邻棋子颜色不同，那么答案只和$k$的奇偶性有关。否则，棋子的状态将呈收敛趋势，最终达到一个“稳定状态”，不再改变。&lt;/p&gt;
&lt;h2&gt;重要的结论与证明&lt;/h2&gt;
&lt;p&gt;不难发现，随着不停地操作，我们可以将棋子的变化规律分为以下两类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果棋子数是偶数，并且任意两个相邻棋子颜色不同，那么答案只和$k$的奇偶性有关。&lt;/li&gt;
&lt;li&gt;否则，棋子的状态将呈收敛趋势，最终达到一个“稳定状态”，不再改变。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;理由？&lt;/p&gt;
&lt;p&gt;对于第一类：显然，假设一开始状态是&lt;code&gt;BWBWBWBW...BWBW&lt;/code&gt;，那么操作偶数次后，状态是 &lt;code&gt;BWBWBWBW...BWBW&lt;/code&gt;，否则状态是 &lt;code&gt;WBWBWBWB...WBWB&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于第二类：&lt;/p&gt;
&lt;p&gt;如果开始时就是“清一色”，那么必然已经是稳定状态。&lt;/p&gt;
&lt;p&gt;否则我们根据颜色对这个棋子环划分“连通块”。比如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/3ac637667c9aa4cd13ed01468d0bf3f1.BvhELRnt_Z1qqS49.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;（其中长度为1的连通块用一个小圆圈表示）&lt;/p&gt;
&lt;p&gt;不难发现，如果一个棋子已经处于长度大于1的连通块中，那么在一次操作后其颜色不会改变，并且将仍然处于一个长度大于1的连通块中。&lt;/p&gt;
&lt;p&gt;并且如果一个长度为1的连通块与一个长度大于1的连通块相邻，那么一次操作后，这个长度为1的连通块中的那个棋子就会变色，而与其相邻的，长度大于1的那个连通块却不会变色。那么，这个棋子就被加入到了与其相邻的连通块中。&lt;/p&gt;
&lt;p&gt;这样，只要图中同时存在长度大于1（这个一定存在，否则就不是我们讨论的情况了）和等于1的连通块，每操作一次，其中长度等于1的连通块数量至少减少1。最终，一定会到达一个不存在长度等于1的连通块的状态。这就是“稳定状态”，到达这个状态后再进行操作，则没有棋子会再改变颜色。&lt;/p&gt;
&lt;h2&gt;解题方法&lt;/h2&gt;
&lt;p&gt;我们将所有长度为1的连通块提出来，构成连通块。为了防止混淆，我们称这种连通块为“相间区间”。&lt;/p&gt;
&lt;p&gt;这句话可能有点晕，但是可以借助图片理解（蓝色表示构成的相间区间）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/2be7af2bc7e0f558c1d8a8ba9d956dd6.IK1FVWyY_m3xWJ.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;根据上面的证明，每次操作时，只有相间区间中的棋子会且一定会改变颜色。相间区间中的棋子改变颜色后，其两端的（指区间中的两端）的棋子将不再属于相间区间。&lt;/p&gt;
&lt;p&gt;设$dist_i$表示第$i$个棋子离其两侧的，长度大于1的连通块的距离中较小的一个。&lt;/p&gt;
&lt;p&gt;从此处开始，用0表示白色，1表示黑色。&lt;/p&gt;
&lt;p&gt;那么，当$k \leq dist_i$时，第$i$个棋子仍然属于间隔区间，其值为&lt;code&gt;val[i] ^ (k&amp;#x26;1)&lt;/code&gt;。否则，这个棋子已经被离其较近的一个长度大于1的连通块给“同化”了（如果两侧的连通块离这个棋子一样近，显然两侧的连通块颜色相同）。&lt;/p&gt;
&lt;h2&gt;实现细节&lt;/h2&gt;
&lt;p&gt;方便起见，先将棋子标号改为$0..n-1$。设&lt;code&gt;val[i]&lt;/code&gt;表示棋子&lt;code&gt;(i%n+n)%n&lt;/code&gt;的颜色。&lt;/p&gt;
&lt;p&gt;首先，特判掉“棋子数是偶数，并且任意两个相邻棋子颜色不同”的情况。&lt;/p&gt;
&lt;p&gt;然后，我们找到任意一个满足&lt;code&gt;val[i] = val[i-1]&lt;/code&gt;的位置。&lt;/p&gt;
&lt;p&gt;从这个位置开始顺时针扫一遍，记录每个相间区间内的棋子离左侧的，长度大于1的连通块的距离，以及其左侧的，长度大于1的连通块的颜色。&lt;/p&gt;
&lt;p&gt;再从这个位置的前一个位置开始逆时针扫一遍，记录每个相间区间内的棋子右侧的，长度大于1的连通块的颜色。另外，判断每个相间区间内的棋子离右侧的，长度大于1的连通块的距离是否小于上一轮记录的距离。如果小于，将距离更新为当前距离的相反数（使用正负两种数，以便判断一个棋子是离左边的连通块更近还是离右边更近）&lt;/p&gt;
&lt;p&gt;其他细节可以见代码。&lt;/p&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [Accepted][unofficial]
// oj:     [Codeforces]

#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;

typedef long long ll;
#define int ll

const int MAXN = 200001;
int n,k;
// 棋子的初始颜色
int arr[MAXN];
// 棋子离左右侧长度大于1的连通块更近的那个距离
int dist[MAXN];
// 棋子左侧长度大于1的连通块的颜色
int lc[MAXN];
// 棋子右侧长度大于1的连通块的颜色
int rc[MAXN];

signed main() {
    ios::sync_with_stdio(false);

    cin&gt;&gt;n&gt;&gt;k;

    // 读入
    for(int i=0;i&amp;#x3C;n;i++) {
        char c;
        cin&gt;&gt;c;
        if(c == &apos;B&apos;) arr[i] = 1;
    }

    // 特判
    bool flag1 = (n%2 == 0);
    if(flag1) for(int i=0;i&amp;#x3C;n;i++) {
        if(arr[0] ^ arr[i] ^ (i&amp;#x26;1)) {flag1 = false;break;}
    }
    if(flag1) {
        cerr&amp;#x3C;&amp;#x3C;&quot;I AK IOI&quot;&amp;#x3C;&amp;#x3C;endl;
        for(int i=0;i&amp;#x3C;n;i++) {
            if(arr[i] ^ (k&amp;#x26;1)) cout&amp;#x3C;&amp;#x3C;&apos;B&apos;;
            else cout&amp;#x3C;&amp;#x3C;&apos;W&apos;;
        }
        cout&amp;#x3C;&amp;#x3C;endl;
        exit(0);
    }

    // 找到开始位置
    int bg = 0;
    for(int i=0;i&amp;#x3C;n;i++) {
        if(i!=0 &amp;#x26;&amp;#x26; arr[i-1] == arr[i]) {bg = i;break;}
    }
    bg += n;

    // 顺时针遍历
    int lastpos = 0;
    int lastcol = 0;
    for(int i=bg;i&amp;#x3C;bg+n;i++) {
        if(arr[(i-1) % n] != arr[i % n] &amp;#x26;&amp;#x26; arr[i % n] != arr[(i+1) % n]) { // 判断是否相间区间
            dist[i % n] = i - lastpos;
            lc[i % n] = lastcol;
        }
        else {lastpos = i;lastcol = arr[i % n];dist[i % n] = 0;}
    }
    bg += n;
    for(int i=bg-1;i&gt;=bg-n;i--) {
        if(arr[(i-1) % n] != arr[i % n] &amp;#x26;&amp;#x26; arr[i % n] != arr[(i+1) % n]) { // 判断是否相间区间
            if(lastpos - i &amp;#x3C; dist[i % n]) {
                dist[i % n] = i - lastpos; // 更新距离
            }
            rc[i % n] = lastcol;
        }
        else {lastpos = i;lastcol = arr[i % n];}
    }

    // 输出
    for(int i=0;i&amp;#x3C;n;i++) {
        if(dist[i] == 0) {
            if(arr[i]) cout&amp;#x3C;&amp;#x3C;&apos;B&apos;;
            else cout&amp;#x3C;&amp;#x3C;&apos;W&apos;;
        }
        else if(dist[i] &gt; 0) {
            if(k &amp;#x3C; dist[i]) {
                if(arr[i] ^ (k&amp;#x26;1)) cout&amp;#x3C;&amp;#x3C;&apos;B&apos;;
                else cout&amp;#x3C;&amp;#x3C;&apos;W&apos;;
            }
            else {
                if(lc[i]) cout&amp;#x3C;&amp;#x3C;&apos;B&apos;;
                else cout&amp;#x3C;&amp;#x3C;&apos;W&apos;;
            }
        }
        else {
            if(k &amp;#x3C; -dist[i]) {
                if(arr[i] ^ (k&amp;#x26;1)) cout&amp;#x3C;&amp;#x3C;&apos;B&apos;;
                else cout&amp;#x3C;&amp;#x3C;&apos;W&apos;;
            }
            else {
                if(rc[i]) cout&amp;#x3C;&amp;#x3C;&apos;B&apos;;
                else cout&amp;#x3C;&amp;#x3C;&apos;W&apos;;
            }
        }
    }
    cout&amp;#x3C;&amp;#x3C;endl;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;评测记录：&lt;a href=&quot;https://www.luogu.org/record/25183207&quot;&gt;R25183207&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[洛谷题解] CF1238D AB-string</title><link>https://ak-ioi.com/blog/algo-solution/luogu-cf1238d</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-cf1238d</guid><pubDate>Fri, 11 Oct 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目链接：&lt;a href=&quot;https://www.luogu.org/problem/CF1238D&quot;&gt;CF1238D AB-string&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;题目描述&lt;/h2&gt;
&lt;p&gt;一个好的字符串$t$（下标从1开始）满足：对于其中任意一个字符，都属于一个或多个长度大于$1$的回文子串。&lt;/p&gt;
&lt;p&gt;回文串是从前向后或从后向前读都一样的字符串。比如A，BAB，ABBA，BAABBBAAB都是回文串，而AB，ABBBAA，BBBA都不是。&lt;/p&gt;
&lt;p&gt;下面是好的字符串的例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;$t$ = AABBB（其中$t_1$和$t_2$属于子串$t_{1..2}$，$t_3$，$t_4$和$t_5$属于子串$t_{3..5}$）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$t$ = ABAA（其中$t_1$，$t_2$和$t_3$属于$t_{1..3}$，$t_4$属于子串$t_{3..4}$）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$t$ = AAAAA（所有字符属于回文子串$t_{1..5}$）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;给你一个长度$n$的字符串$s$，请计算其好的子串的数量。&lt;/p&gt;
&lt;h2&gt;输入格式&lt;/h2&gt;
&lt;p&gt;第一行一个整数$n\ (1 \leq n \leq 3\cdot 10^5)$，表示$s$的长度。&lt;/p&gt;
&lt;p&gt;第二行一个仅由字符&apos;A&apos;和&apos;B&apos;构成的字符串$s$。&lt;/p&gt;
&lt;h2&gt;输出格式&lt;/h2&gt;
&lt;p&gt;一个整数，$s$中好的子串的数量。&lt;/p&gt;
&lt;h2&gt;样例解释&lt;/h2&gt;
&lt;p&gt;样例1：有$s_{1..2}$，$s_{1..4}$，$s_{1..5}$，$s_{3..4}$，$s_{3..5}$，$s_{4..5}$&lt;/p&gt;
&lt;p&gt;样例2：有$s_{1..2}$，$s_{2..3}$，$s_{1..3}$&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;p&gt;注意：字符串只由&apos;A&apos;和&apos;B&apos;构成。字符下标从$1$开始。&lt;/p&gt;
&lt;p&gt;我们考虑一个字符串中不在两端的字符。容易发现，这个字符一定属于一个长度大于$1$的回文子串。设这个字符为$t_i$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 $t_{i-1} =\not t_{i+1}$，那么$t_{i-1}$和$t_{i+1}$中必有一个和$t_i$相等。不妨令$t_i = t_{i+1}$，那么$t_i$就属于$t_{i..i+1}$这个回文子串。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 $t_{i-1} = t_{i+1}$，那么$t_i$属于$t_{i-1..i+1}$这个回文子串。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此只需考虑两端的字符是否属于一个长度大于$1$的回文子串（换句话说，是否同时有一个回文前缀和回文后缀，前缀和后缀包含本身），就可以判断一个字符串是不是好的了。&lt;/p&gt;
&lt;p&gt;接下来我们考虑在字符串$s$上统计。&lt;/p&gt;
&lt;p&gt;假设当前考虑的子串从$s_i$开始。那么，可能存在一个字符$s_j$（$j$尽可能小，$j&gt;i$），使得如果当前子串结束点在$s_j$之后，那么这个字符串就有一个回文前缀。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果存在一个$j$，那么定义$next[i] = j$。&lt;/li&gt;
&lt;li&gt;否则，$next[i] = 0$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;很显然，$s_{i..next[i]}$是以$s_i$开头的，最短的回文子串。&lt;/p&gt;
&lt;p&gt;相应地，我们定义$prev[]$，使$s_{prev[i]..i}$是$s_i$结尾的，最短的回文子串。很显然：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果存在$j$使$next[j]=i$，那么$prev[i] = j$&lt;/li&gt;
&lt;li&gt;否则，$prev[i] = 0$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么如何求$next$数组呢？&lt;/p&gt;
&lt;p&gt;由于字符串中只存在&apos;A&apos;和&apos;B&apos;，$next[i]$等于满足$s_j = s_i$和$j&gt;i$的最小$j$。&lt;/p&gt;
&lt;p&gt;有了$prev$和$next$之后，我们就可以知道符合要求的子串$s_{x..y}$满足下面的条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$x \leq prev[y]$&lt;/li&gt;
&lt;li&gt;$next[x] \leq y$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是我们可以用&lt;a href=&quot;https://www.luogu.org/problem/P3374&quot;&gt;树状数组1&lt;/a&gt;实现统计（树状数组是一种维护序列的$log(n)$数据结构，功能是：单点修改，求前缀和）。细节见代码。&lt;/p&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [Accepted]
// oj:     [luogu]

#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;

typedef long long ll;
#define int ll

const int MAXN = 300002;
// next数组
int nextMatch[MAXN];
// prev数组
int prevMatch[MAXN];
int n;
char s[MAXN];

// 树状数组1
struct WxwAkIoi {
    int data[MAXN];
    static inline int lowbit(int x) {return x&amp;#x26;-x;}
    void update(int p,int v) {
        for(int i=p;i&amp;#x3C;MAXN;i+=lowbit(i)) {
            data[i] += v;
        }
    }
    int query(int p) {
        int ret = 0;
        for(int i=p;i;i-=lowbit(i)) {
            ret += data[i];
        }
        return ret;
    }
}tr;

// 预处理next数组
void pre() {
    int prevA = -1;
    int prevB = -1;
    for(int i=1;i&amp;#x3C;=n;i++) {
        if(s[i] == &apos;A&apos;) {
            if(prevA != -1) nextMatch[prevA] = i;
            prevA = i;
        }
        else if(s[i] == &apos;B&apos;) {
            if(prevB != -1) nextMatch[prevB] = i;
            prevB = i;
        }
        else {
            cerr&amp;#x3C;&amp;#x3C;&quot;DDoSForces AK IOI&quot;;
            exit(28403);
        }
    }
}

signed main() {
    ios::sync_with_stdio(false);
    cin&gt;&gt;n;
    cin&gt;&gt;(s+1);
    n = strlen(s+1);

    pre();

    // 计算prev数组
    for(int i=1;i&amp;#x3C;=n;i++) {
        if(nextMatch[i] != 0) {
            prevMatch[nextMatch[i]] = i;
        }
    }

    // 统计
    int ans = 0;
    for(int i=1;i&amp;#x3C;=n;i++) {
        if(prevMatch[i]) {
            // next[prev[i]] = i，而对于之后的i，有next[prev[i]] &amp;#x3C; new_i。
            // 这就满足了 next[x] &amp;#x3C;= y 的性质
            tr.update(prevMatch[i],1);
        }
        // 求树状数组从1到prev[i]的和。
        // 这就满足了 x &amp;#x3C;= prev[y] 的性质
        ans += tr.query(prevMatch[i]);
    }

    cout&amp;#x3C;&amp;#x3C;ans&amp;#x3C;&amp;#x3C;endl;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;评测记录：&lt;a href=&quot;https://www.luogu.org/record/25031139&quot;&gt;R25031139&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[洛谷题解] CF837F Prefix Sums</title><link>https://ak-ioi.com/blog/algo-solution/luogu-cf837f</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-cf837f</guid><pubDate>Wed, 09 Oct 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目链接：&lt;a href=&quot;https://www.luogu.org/problem/CF837F&quot;&gt;CF837F Prefix Sums&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;题意大意请见题目链接中的翻译。本题解将参照这个翻译进行讲解。&lt;/p&gt;
&lt;p&gt;注意：题目中还有一个限制，即，“数列中必有至少两个元素是正数”。因此，答案不会是无穷大。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;p&gt;本题解中，待求的值可能用$x$表示。&lt;/p&gt;
&lt;p&gt;我们发现，要解决这个问题，只需考虑某个数列$A^x$中的最大值。因为如果最大值没有大于等于$k$，其他值也不会大于等于$k$。&lt;/p&gt;
&lt;p&gt;对于一个数列$A$（其中一定有正数），那么得到的$p(A)$开头一定有一个$0$。这个$0$可以直接忽略，因为它一定不是最大值。&lt;/p&gt;
&lt;p&gt;于是我们可以改造函数$p$，使得$p([1,1,1]) = [1,2,3]$。&lt;/p&gt;
&lt;p&gt;另外，初始数列$A^0$的“前导0”无论如何进行前缀和，其值必然仍然是$0$，并且不对后面的值产生影响。因此可以删去$A^0$中的前导0。（此后的内容都在没有“前导0”的情况下考虑）&lt;/p&gt;
&lt;p&gt;然后我们发现，如果此时$A^0$的长度如果大于等于$10$，那么只需进行很少的几次前缀和就会出现元素大于等于$k$。因此，如果$A^0$长度大于等于$10$，可以直接暴力求解。&lt;/p&gt;
&lt;p&gt;如果$A^0$长度为$2$，那么假设$A^0 = [a,b]$，则有$A^x = [a,a\cdot x + b]$。因此，待求的值可以通过除法得出：$x_{min} = \lceil \frac{k-b}{a} \rceil$&lt;/p&gt;
&lt;p&gt;如果$A^0$的长度介于$2$和$10$之间，答案可能非常大，我们需要更好的做法。我们发现这题的情况有单调性，而且用$k$求$x$很难，而判断$A^x$中是否有元素大于等于$k$则看起来较为容易。（因此可以使用二分答案）&lt;/p&gt;
&lt;p&gt;怎么个容易法呢？由于$A^0$的长度很小，我们可以使用矩阵快速幂。设$A^0$的长度为$n$。定义一个$n \times n$的矩阵$M$，其中对于所有$j \leq i$，$M_{i,j} = 1$，其余情况$M_{i,j} = 0$。&lt;/p&gt;
&lt;p&gt;则$A^x = M^x \cdot A^0$。&lt;/p&gt;
&lt;p&gt;那么二分答案就可以实现了。&lt;/p&gt;
&lt;p&gt;还有一个问题，矩阵快速幂溢出了怎么办？&lt;/p&gt;
&lt;p&gt;由于我们只关心最后的数字是小于$k$还是大于等于$k$，因此类似于取模，我们计算时时时刻刻将结果和$k$取$min$。&lt;/p&gt;
&lt;p&gt;但是在这种情况下乘法运算仍然可能溢出，怎么办呢？~~__int128_t~~ 我们可以使用类似于快速幂的方式，实现一种“龟速乘法”（即，使用加法实现乘法）。具体如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 代码中limit代表k的值
int mxqmul(int a,int b) {
    if(b==0) return 0;
    int r = mxqmul(a,b&gt;&gt;1);
    r = min(r + r, limit);
    if(b&amp;#x26;1) return min(r + a, limit);
    return r;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度分析&lt;/h2&gt;
&lt;p&gt;删除“前导0”，时间复杂度为$O(n)$，然后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当$|A^0|=2$时，仅仅进行一次除法，$O(1)$&lt;/li&gt;
&lt;li&gt;当$|A^0|\geq 10$时，实测在极限数据下答案为$411$。因此复杂度上限为$O(411n)$。计算$411 \cdot 200000 = 82,200,000$，因此不会有问题。&lt;/li&gt;
&lt;li&gt;当$2 \lt |A^0| \lt 10$时，二分答案占有复杂度$log(r)$（r是二分上界），而二分的&lt;code&gt;check&lt;/code&gt;复杂度瓶颈在于矩阵乘法，复杂度上界为$n^2 \cdot log(r)$，另外“龟速乘法”的复杂度上界为$log(long\ long) = 31$，因此复杂度上限为$O(log^2(r)\cdot n^2 \cdot 31)$。如果定二分范围为$[0,64356879284]$，计算&lt;code&gt;Math.log2(64356879284)**2 * 10**2 * 31 == 3996507.528120665&lt;/code&gt;，因此也不会有问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [Accepted]
// oj:     [luogu]
 
#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;
 
typedef long long ll;
#define int ll

const int MAXN = 200001;
int arr[MAXN];
// 题目中的k。因为k一般用作循环变量，所以不作为变量名
int limit;
// A0的长度
int n;

// “龟速乘法”
int mxqmul(int a,int b) {
    if(b==0) return 0;
    int r = mxqmul(a,b&gt;&gt;1);
    r = min(r + r, limit);
    if(b&amp;#x26;1) return min(r + a, limit);
    return r;
}
 
// 矩阵乘法封装类
struct Matrix:vector&amp;#x3C;vector&amp;#x3C;int&gt; &gt; {
 
    Matrix __construct(int l = 0, int w = 0, int v = 0)
    {
        assign(l, vector&amp;#x3C;int&gt;(w, v));
        return *this;
    }
 
    Matrix(int l = 0, int w = 0, int v = 0)
    {
        assign(l, vector&amp;#x3C;int&gt;(w, v));
    }
 
    unsigned sizeL() const {
        return size();
    }
 
    unsigned sizeW() const {
        return empty()?0:(*this)[0].size();
    }
 
    Matrix operator* (const Matrix &amp;#x26;other) const {
        if(sizeW()!=other.sizeL()) {
            return Matrix(0,0,0);
        }
        
        int l=sizeL(),w=other.sizeW(),p=sizeW();
        Matrix ret(l,w,0);
        
        for(int i=0;i&amp;#x3C;l;i++) {
            for(int j=0;j&amp;#x3C;w;j++) {
                for(int k=0;k&amp;#x3C;p;k++) {
                    ret[i][j]+=mxqmul((*this)[i][k],other[k][j]);
                    ret[i][j] = min(ret[i][j],limit);
                }
            }
        }
        return ret;
    }
    Matrix operator*= (const Matrix &amp;#x26;other) {
        *this=(*this)*other;
        return *this;
    }
 
    Matrix pow(int t) {
        if(t==0) {
            Matrix ret(sizeL(),sizeL(),0);
            for(int i=0;i&amp;#x3C;sizeL();i++) {
                ret[i][i]=1;
            }
            return ret;
        }
        if(t==1) {
            return *this;
        }
        if(t==2) {
            return (*this)*(*this);
        }
        Matrix tmp=pow(t/2);
        if(t%2==0) return tmp*tmp;
        else return (*this)*tmp*tmp;
    }
 
    Matrix pow_(int t) {
        *this=pow(t);
        return *this;
    }
 
    void oi_input() {
        for(int i=0;i&amp;#x3C;sizeL();i++) {
            for(int j=0;j&amp;#x3C;sizeW();j++) {
                scanf(&quot;%lld&quot;,&amp;#x26;(*this)[i][j]);
            }
        }
    }
 
    void oi_output() {
        for(int i=0;i&amp;#x3C;sizeL();i++) {
            for(int j=0;j&amp;#x3C;sizeW();j++) {
                printf(&quot;%lld &quot;,(*this)[i][j]);
            }
            printf(&quot;\n&quot;);
        }
    }
};

// 二分的check。如果Ap中存在大于等于limit的元素返回true，否则返回false
bool check(int p) {
    Matrix mt(n,n,0);
    
    for(int i=0;i&amp;#x3C;n;i++) {
        for(int j=0;j&amp;#x3C;=i;j++) {
            mt[i][j] = 1;
        }
    }
    
    Matrix ar(n,1,0);
    for(int i=0;i&amp;#x3C;n;i++) {
        ar[i][0] = arr[i+1];
    }
    
    ar = mt.pow(p) * ar;
    
    for(int i=0;i&amp;#x3C;n;i++) {
        if(ar[i][0] &gt;= limit) return true;
    }
    return false;
}

// 删除前导0，返回删除后数列的长度
int unuqie(int *arr,int len) {
    int ptr = 0;
    for(int i=1;i&amp;#x3C;=len;i++) {
        if(arr[i] != 0 || ptr) {
            arr[++ptr] = arr[i];
        } 
    }
    return ptr;
}

// 功能相当于 (int)ceil((double)a/b)
// 用于A0的长度等于2的情况下。由于double的精度问题，并不能使用强转double并ceil的方法。
int ceilDiv(int a,int b) {
    return a/b + bool(a%b);
}

signed main() {
    scanf(&quot;%lld&quot;,&amp;#x26;n);
    scanf(&quot;%lld&quot;,&amp;#x26;limit);
    for(int i=1;i&amp;#x3C;=n;i++) {
        scanf(&quot;%lld&quot;,&amp;#x26;arr[i]);
    }

    // 去除前导0
    n = unuqie(arr,n);
    
    // 判断 |A0| == 2
    if(n == 2) {
        printf(&quot;%lld\n&quot;,max(0ll, ceilDiv((limit - arr[2]), arr[1])));
        exit(0);
    }
    
    // 判断答案为0的情况（防止出现问题）
    for(int i=1;i&amp;#x3C;=n;i++) {
        if(arr[i] &gt;= limit) {
            printf(&quot;0\n&quot;);
            exit(0);
        }
    }
    
    // 如果 |A0| &gt;= 10，可以暴力迭代完成。
    if(n &gt;= 10) {
        int cnt = 0;
        while(1) {
            cnt++;
            for(int i=1;i&amp;#x3C;=n;i++) {
                arr[i] += arr[i-1];
                if(arr[i] &gt;= limit) {
                    printf(&quot;%lld\n&quot;,cnt);
                    exit(0);
                }
            }
        }
    }
    
    // 否则二分答案
    // 考虑二分边界的方法：
    //   如果check(mid)为true，那么mid值偏大或正好
    //     由于check(mid)为true，最终答案可能是mid，所以 r = mid (如果最终答案不可能是mid，取 r = mid - 1)
    //   如果check(mid)为false，那么mid值偏小
    //     最终答案不可能取mid，所以 l = mid + 1 (如果最终答案可能是mid，取 l = mid)
    // 随后考虑最容易死循环的情况，即 r - l == 1，来确定mid的取值
    //   如果令 mid = l+r - (l+r)/2，则此情况下mid = r：
    //     若check(mid) == true，那么执行 r = mid，即 r = r，此时二分范围没有变小，会造成死循环，所以应当令 mid = (l+r)/2
    int l = 0, r = 64356879284;
    while(l&amp;#x3C;r) {
        int mid = (l+r)/2;
        if(check(mid)) {
            r = mid;
        }
        else {
            l = mid + 1;
        }
    }
    printf(&quot;%lld\n&quot;,l);
}


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;评测记录：&lt;a href=&quot;https://www.luogu.org/record/24947635&quot;&gt;R24947635&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[洛谷题解] CF1033C Permutation Game</title><link>https://ak-ioi.com/blog/algo-solution/luogu-cf1033c</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-cf1033c</guid><pubDate>Sun, 29 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目链接：&lt;a href=&quot;https://www.luogu.org/problem/CF1033C&quot;&gt;CF1033C Permutation Game&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;题目大意请见题目链接中的翻译。本题解中的表述将对照这个翻译。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;先决条件：动态规划板子题（数塔问题） | 有向无环图与拓扑排序&lt;/p&gt;
&lt;p&gt;这是一道博弈论（研究游戏必胜策略等问题的学说）问题。&lt;/p&gt;
&lt;h2&gt;游戏状态&lt;/h2&gt;
&lt;p&gt;显然，游戏局面除了硬币所在的格子之外就没有别的区别了。因此，游戏状态定义为：硬币所在的格子。&lt;/p&gt;
&lt;h2&gt;必胜与必败状态&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;It can be shown that the game is always finite, i.e. there always is a winning strategy for one of the players.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;题目提到，游戏一定是有限的，即，一定有人有必胜策略。因此，如果以某个状态为 &lt;strong&gt;初始状态&lt;/strong&gt; 时，&lt;strong&gt;先手&lt;/strong&gt; 玩家是 &lt;strong&gt;有必胜策略&lt;/strong&gt; 的，那么称这个状态为 &lt;strong&gt;必胜状态&lt;/strong&gt;。否则，因为一定有一个人会有必胜策略，所以这个状态就叫做 &lt;strong&gt;必败状态&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;硬币不能移动的状态是 &lt;strong&gt;必败状态&lt;/strong&gt;，因为先手立刻就输了。&lt;/li&gt;
&lt;li&gt;其他情况下：
&lt;ul&gt;
&lt;li&gt;如果当前状态的下一步 &lt;strong&gt;可以&lt;/strong&gt; 是 &lt;strong&gt;必败状态&lt;/strong&gt;，那么这个状态是 &lt;strong&gt;必胜状态&lt;/strong&gt;，因为，当前状态下，先手有决定权，因此可以将下一个 &lt;strong&gt;必败状态&lt;/strong&gt; 留给后手。&lt;/li&gt;
&lt;li&gt;否则，即当前状态的下一步 &lt;strong&gt;一定&lt;/strong&gt; 是 &lt;strong&gt;必胜状态&lt;/strong&gt;，那么这个状态是 &lt;strong&gt;必败状态&lt;/strong&gt;，因为，当前状态下，先手无论怎么走，都会将一个 &lt;strong&gt;必胜状态&lt;/strong&gt; 留给后手。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;实现方法 / 复杂度分析&lt;/h2&gt;
&lt;p&gt;由于硬币只能从数值小的格子移到数值大的格子，因此如果从 &lt;strong&gt;当前状态&lt;/strong&gt; 向 &lt;strong&gt;下一步能到达的状态&lt;/strong&gt; 连有向边，形成的一定是 &lt;strong&gt;有向无环图&lt;/strong&gt;，其中，方格中的数字就是 &lt;strong&gt;拓扑序&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们按拓扑序从大到小进行DP。这样，DP进行到数值小的格子时，数值大的格子一定已经完成了DP，当前格子的必胜性就可以确定。&lt;/p&gt;
&lt;p&gt;在已知当前格子，枚举当前格子可以到达的状态时，只枚举与当前格子距离为当前格子中数值的格子（这是移动硬币的条件之一）。因为如果枚举$1..n$，时间复杂度会变成$O(n^2)$肯定会TLE&lt;/p&gt;
&lt;p&gt;而使用只枚举倍数的方法，复杂度值是：&lt;/p&gt;
&lt;p&gt;$\sum\limits_{i=1}^{i \leq n} \lfloor \frac{n}{a_i} \rfloor$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Furthermore, there are no pair of indices $i =\not j$ such that $a_i = a_j$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;题目提到格子中的数范围是$1..n$，且两两不同，因此，复杂度值简化为&lt;/p&gt;
&lt;p&gt;$\sum\limits_{i=1}^{i \leq n} \lfloor \frac{n}{i} \rfloor$&lt;/p&gt;
&lt;p&gt;如果你数学学得好，你早就知道，这个值大约是$n\cdot ln(n)$。如果你不知道，你可以用下面的程序验证。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;

int main() {
    int n = 100000;
    int ans = 0;
    for(int i=1;i&amp;#x3C;=n;i++) {
        ans += n/i;
    }
    cout&amp;#x3C;&amp;#x3C;ans&amp;#x3C;&amp;#x3C;endl;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：$1166750$&lt;/p&gt;
&lt;p&gt;因此肯定能过。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;$Talk\ is\ cheap,\ show\ me\ the\ code.$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [Accepted]
// oj:     [luogu]

#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;

const int MAXN = 100001;
// 格子中的值 
int arr[MAXN];
// 值为i的格子编号为pos[i] 
int pos[MAXN];
// 格子是必胜(1)还是必败(0) 
int status[MAXN];

int n;

int main() {
    ios::sync_with_stdio(false);
    
    cin&gt;&gt;n;
    for(int i=1;i&amp;#x3C;=n;i++) cin&gt;&gt;arr[i];
    
    for(int i=1;i&amp;#x3C;=n;i++) {
        pos[arr[i]] = i;
    }
    
    for(int i=n;i&gt;=1;i--) {
        int u = pos[i];
        status[u] = 0;
        int v = u;
        // 只枚举距离是倍数的 
        while(v - arr[u] &gt; 0) v-=arr[u];
        for(;v&amp;#x3C;=n;v+=arr[u]) {
            if(!(arr[u] &amp;#x3C; arr[v])) continue;
            // 发现能到达必败状态，说明这个状态一定是必胜的。 
            if(status[v] == 0) {
                status[u] = 1; break;
            }
        }
    }
    
    // 输出结果 
    for(int i=1;i&amp;#x3C;=n;i++) {
        if(status[i]) {
            cout&amp;#x3C;&amp;#x3C;&apos;A&apos;;
        }
        else {
            cout&amp;#x3C;&amp;#x3C;&apos;B&apos;;
        }
    }
    cout&amp;#x3C;&amp;#x3C;endl;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;评测记录：&lt;a href=&quot;https://www.luogu.org/record/24491666&quot;&gt;R24491666&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[洛谷题解] CF936C Lock Puzzle</title><link>https://ak-ioi.com/blog/algo-solution/luogu-cf936c</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-cf936c</guid><pubDate>Sun, 29 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目链接：&lt;a href=&quot;https://www.luogu.org/problem/CF936C&quot;&gt;CF936C Lock Puzzle&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;题目描述&lt;/h2&gt;
&lt;p&gt;探险家发现了一个保险箱，里面有大量的宝藏。&lt;/p&gt;
&lt;p&gt;保险箱上有一个密码锁，初始时显示的是一个长度为$n$的小写字母字符串$s$。探险家发现，当密码锁上显示的是字符串$t$时，这个密码锁就会打开。&lt;/p&gt;
&lt;p&gt;密码锁显示的字符串可以通过形如&lt;code&gt;shift x&lt;/code&gt;的指令改变。要执行这个指令，探险家需要在$0$到$n$的范围内（包含$0$和$n$）选择一个$x$。此时，设屏幕上显示的字符串$p = \alpha\beta$（其中$\beta$的长度为$x$），那么这个字符串会变为$\beta^{R}\alpha$（$\beta^{R}$表示$\beta$反转后的结果）。&lt;/p&gt;
&lt;p&gt;比如，如果屏幕上当前显示$abcacb$，那么执行&lt;code&gt;shift 4&lt;/code&gt;后屏幕上会显示$bcacab$，因为$\alpha=ab$，$\beta=cacb$，$\beta^{R}=baca$。&lt;/p&gt;
&lt;p&gt;探险家担心如果执行了太多了&lt;code&gt;shift&lt;/code&gt;操作，这个密码锁就会永远锁定。因此，他会给你$n$和字符串$s$和$t$，并且请你给出一个步骤数不大于$6100$的解锁方案。请注意无需最小化步骤数。&lt;/p&gt;
&lt;h2&gt;输入格式&lt;/h2&gt;
&lt;p&gt;第一行一个整数$n$，为字符串$s$和$t$的长度。&lt;/p&gt;
&lt;p&gt;随后两行分别输入小写字母构成的字符串$s$和$t$，表示初始时屏幕显示的字符串以及解锁前需要得到的字符串。&lt;/p&gt;
&lt;h2&gt;输出格式&lt;/h2&gt;
&lt;p&gt;如果不可能打开密码锁，输出&lt;code&gt;-1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;否则，第一行输出一个整数$k\ (0\leq k \leq 6100)$，表示需要的操作数量。第二行输出$k$个空格隔开的整数$x_i\ (0\leq x_i \leq n)$，其中$x_i$代表执行操作$shift\ x_i$。&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;p&gt;$1 \leq n \leq 2000$&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;无解情况&lt;/h2&gt;
&lt;p&gt;显然，只有当$s$构成的可重集合与$t$不同时，问题是无解的。&lt;/p&gt;
&lt;h2&gt;设计递归&lt;/h2&gt;
&lt;p&gt;假设当前屏幕上字符串（此后称为$p$）前缀是$abc$，我们考虑再将两个指定的字符$x$和$y$加进前缀。&lt;/p&gt;
&lt;p&gt;手玩一下，很容易就可以发现一个非常好的做法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;------ Add 2 chars ------
abc....x..... Find x
       ~~~~~~ Step 1
.....xabc....
         ~~~~ Step 2
..y......xabc Find y
   ~~~~~~~~~~ Step 3
cbax........y
            ~ Step 4
ycbax........
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时前缀变成了${\color{red}y}cba{\color{red}x}$。&lt;/p&gt;
&lt;p&gt;整理一下信息，我们发现，若要在$p$的前缀上构造出长度为$len\ (len \geq 2)$的字符串$t_1[0..len-1]$，那么只要先在$p$的前缀上构造出$t_1[1..len-2]^{R}$，然后再令$x=t_1[len-1]$，$y=t_1[0]$，执行上述步骤即可。这样，我们就可以递归解决问题。&lt;/p&gt;
&lt;h2&gt;边界条件&lt;/h2&gt;
&lt;p&gt;不难发现，以上递归不能处理的情况是$len=0$和$len=1$。&lt;/p&gt;
&lt;p&gt;对于$len=0$，无需执行任何操作，直接返回即可。&lt;/p&gt;
&lt;p&gt;对于$len=1$，我们要将$t_1[0]$（下面称之为$x$）字符放到$p$开头。使用下面方法即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;------ Get init char ------
......x..... Find x
       ~~~~~ Step 1
...........x
           ~ Step 2
x...........
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;分析步骤数&lt;/h2&gt;
&lt;p&gt;如果$n$是奇数，那么需要先使用$2$步来将第一个字符放到$p$开头（边界条件），然后随后还要使用$4 \times ((n-1)/2)$步进行递归构造。$n$最大可以取到$1999$，那么步骤数是$4000$，完全没问题。&lt;/p&gt;
&lt;p&gt;如果$n$是偶数，只需要使用$4 \times (n/2)$步进行递归构造。$n$最大可以取到$2000$，步骤数是$4000$，同样没问题。&lt;/p&gt;
&lt;h2&gt;分析复杂度&lt;/h2&gt;
&lt;p&gt;只论递归的话，复杂度$O(n)$。&lt;/p&gt;
&lt;p&gt;如果考虑使用暴力算法进行$shift$操作和字符查找，总时间复杂度为$O(n^2)$。而时限有$2$秒，因此$O(\text{能过})$&lt;/p&gt;
&lt;h2&gt;具体实现&lt;/h2&gt;
&lt;p&gt;可以发现，递归时要求构造为前缀的字符串，要么是$t$的子串，要么是$t$的子串反转。&lt;/p&gt;
&lt;p&gt;设计递归函数时，传入三个参数&lt;code&gt;l,r,d&lt;/code&gt;，其中$d$为$1$时表示是子串，$d$为$-1$时表示子串反转。如果$d=1$，那么子串区间为$[l..r]$，否则为$[r..l]$。&lt;/p&gt;
&lt;p&gt;这种设计比较容易实现。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;$Talk\ is\ cheap,\ show\ me\ the\ code$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [Accepted]
// oj:     [luogu]

#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;

/*

------ Get init char ------
......x.....    int idx = lastIndexOf(x);
       ~~~~~    do_shift(n-idx-1);
...........x
           ~    do_shift(1);
x...........


------ Add 2 chars ------
abc....x.....   int idx = lastIndexOf(x);
       ~~~~~~   do_shift(n-idx);
.....xabc....
         ~~~~   do_shift(idx-len+2);
..y......xabc   idx = indexOf(y);
   ~~~~~~~~~~   do_shift(n-idx-1);
cbax........y
            ~   do_shift(1);
ycbax........

*/

int n;
string s;
string t;
// 存储答案 
vector&amp;#x3C;int&gt; ans;

// 进行shift操作，并记录到答案中 （此处直接在s串上操作） 
void do_shift(int x) {
    ans.push_back(x);
    
    string tmp = s.substr(s.length()-x);
    s = string(x,&apos; &apos;) + s.substr(0,s.length()-x);
    
    for(unsigned i=0;int(i)&amp;#x3C;x;i++) {
        s[x-i-1] = tmp[i];
    }
}

// 检查能否完成任务。如果能，正常返回，否则直接输出-1并结束程序 
void test() {
    int st[26];
    for(int i=0;i&amp;#x3C;26;i++) {
        st[i] = 0;
    }
    for(int i=0;i&amp;#x3C;n;i++) {
        st[s[i]-&apos;a&apos;] ++;
        st[t[i]-&apos;a&apos;] --;
    }
    for(int i=0;i&amp;#x3C;26;i++) {
        if(st[i]) {
            cout&amp;#x3C;&amp;#x3C;-1&amp;#x3C;&amp;#x3C;endl;exit(0);
        }
    }
    return;
}

// 将t的子串或子串反转形式构造为s的前缀（此处直接在s串上操作） 
void buildString(int l,int r,int d) {
    int len = (r-l)*d+1;
    
    // 边界判断 
    if(len==0) return;
    if(len==1) {
        int idx = s.find_last_of(t[l]);
        // 直接忽略掉x==0的shift操作 
        if(n-idx-1 &gt; 0) do_shift(n-idx-1);
        do_shift(1);
        return;
    }
    
    // 递归构造 
    buildString(r-d,l+d,-d);
    
    // 加入2个新的字符 
    char x = t[r];
    char y = t[l];
    
    int idx = s.find_last_of(x);
    do_shift(n-idx);
    if(idx-len+2 &gt; 0) do_shift(idx-len+2);
    idx = s.find(y);
    do_shift(n-idx-1);
    do_shift(1);
    return;
}

// 输出最终答案 
void print() {
    cout&amp;#x3C;&amp;#x3C;ans.size()&amp;#x3C;&amp;#x3C;&apos;\n&apos;;
    for(unsigned i=0;i&amp;#x3C;ans.size();i++) {
        if(i) cout&amp;#x3C;&amp;#x3C;&apos; &apos;;
        cout&amp;#x3C;&amp;#x3C;ans[i];
    }
    cout&amp;#x3C;&amp;#x3C;endl;
}

int main() {
    ios::sync_with_stdio(false);
    cin&gt;&gt;n&gt;&gt;s&gt;&gt;t;
    
    // 测试能否完成 
    test();
    
    // 构造 
    buildString(0,n-1,1);
    
    // 该函数在s!=t时会RE，用于在调试时快速判断是否写挂 
    assert(s==t);
    
    // 输出结果 
    print();
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;评测记录：&lt;a href=&quot;https://www.luogu.org/record/24483982&quot;&gt;R24483982&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[搞事程序] Lemon上的自动AC机</title><link>https://ak-ioi.com/blog/security/lemon-auto-ac</link><guid isPermaLink="true">https://ak-ioi.com/blog/security/lemon-auto-ac</guid><description>可以在Lemon评测软件上自动AC的一份固定代码（不是AC自动机算法，请勿滥用）</description><pubDate>Sun, 29 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;免责声明&lt;/h2&gt;
&lt;p&gt;这份代码是作弊代码，并且只能用于Windows系统下的Lemon评测软件。（亲测时间：2019/09/29）&lt;/p&gt;
&lt;p&gt;UPD(2019/10/09)：在LemonLime（Lemon绿了）评测系统下仍然有效（操作系统为Windows）&lt;/p&gt;
&lt;p&gt;UPD(2025/05/01)：现在LemonLime似乎已经有容器机制了，这是好的。&lt;/p&gt;
&lt;p&gt;使用此代码一般可以做到AC，但是造成的一切后果将由使用者自行承担（包括但不限于：被训斥、被学校竞赛队开除）。&lt;/p&gt;
&lt;p&gt;作者保证不会在基于Lemon的比赛中使用此代码。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/*
    The New Lemon_JudgeHacker
    2019/09/28
    freopen version.
    Does not support Non-ASCII problem titles.
    The program itself will take some time, so don&apos;t use this \
    program for problems that has almost-zero time or memory limits.
*/
#include&amp;#x3C;bits/stdc++.h&gt;
#include&amp;#x3C;windows.h&gt;
#include&amp;#x3C;dirent.h&gt;
using namespace std;

struct Verdict{
    // Judgement Verdict. See below.
    long long ver;
    // Stuff to write to output file (Ignored if VERDICT_AC)
    string txt;
    // Stuff to write to stderr (Always has effect)
    string msg;
    Verdict() {ver=0;}
    Verdict(long long v) {ver=v;txt=&quot;&quot;;msg=&quot;&quot;;}
    Verdict(long long v,string s,string s2) {ver=v;txt=s;msg=s2;}
};
#define VERDICT_AC 0
#define VERDICT_WA 1
#define VERDICT_TLE 2
#define VERDICT_MLE 3
#define VERDICT_OLE 4
#define VERDICT_RE 5
#define VERDICT_USETIME 1000

////// Attention! You need to modify these settings! //////

    // Stringbuf length
    const int BUF_LEN = 1024;

    // Read length per time (32768 is recommended)
    // * Does not affect correctness unless it&apos;s too large or set to 0
    const int READ_LEN = 32768;

    // Problem title
    string PROBLEM = &quot;hello&quot;;

    // Input and output files
    // * Replace arguments are: problem title.
    string INF = &quot;${1}.in&quot;;
    string OUF = &quot;${1}.out&quot;;

    // Input data extension name
    string inext = &quot;in&quot;;

    // Judge the verdict by the input file
    Verdict adjustVerdict(string InputFilename) {
        return Verdict(VERDICT_AC,&quot;&quot;,&quot;&quot;);
        // ----- Following program gives out execution time of O(n log n) -----
        // * The size of testdata in this case is the first int of the input file.
        // ifstream fin(InputFilename.c_str());
        // int sz;
        // fin&gt;&gt;sz;
        // return Verdict(VERDICT_USETIME + (long long)log2(sz) * sz * 35);
    }

    // Error messages
    string E_NO_CONTEST = &quot;Contest could not be found.&quot;;
    string E_NO_DATA_DIR = &quot;The data directory could not be found.&quot;;
    string E_NO_PROB_DIR = &quot;The problem directory could not be found.&quot;;
    string E_FILE_MATCH_FAIL = &quot;The input does not match any input file.&quot;;
    string E_ANS_NOT_FOUNT = &quot;Cannot find answer file.&quot;;
    string E_SUCCESS = &quot;The program exited normally.&quot;;

///////////////////////////////////////////////////////////

// The safe find_last_of function.
// If the char does not exist, then it will return -1;
int findLast(string s,char c) {
    int ret = s.find_last_of(c);
    if(ret &amp;#x3C; 0 || ret &gt;= (int)s.size()) return -1;
    return ret;
}

// Get the basename of a path.
//   example: &apos;C:/test/iakioi.cpp&apos; -&gt; &apos;iakioi&apos;
//   This is used to determine the problem title.
string getBaseName(string path) {
    int idx1 = max(findLast(path,&apos;/&apos;),findLast(path,&apos;\\&apos;));
    if(idx1 &gt;= 0) {
        path = path.substr(idx1+1);
    }
    idx1 = findLast(path,&apos;.&apos;);
    if(idx1 &gt;= 0) {
        path = path.substr(0,idx1);
    }
    return path;
}

// Replaces part of string
// Note that destnation cannot contain original one.
string str_replace(string f,string t,string s) {
    while(1) {
        unsigned idx = s.find(f);
        if(idx &gt;= s.length()) break;
        s = s.replace(idx,f.length(),t);
    }
    return s;
}

// Test if there is such file or directory
bool file_exists(string f) {
    if(!access(f.c_str(),0)) return true;
    return false;
}

// List all files/folders in a folder.
vector&amp;#x3C;string&gt; listFolder(string folder) {
    vector&amp;#x3C;string&gt; v;
    struct dirent *ent = NULL;
    DIR *dir = opendir(folder.c_str());
    if (dir != NULL) {
        while ((ent = readdir (dir)) != NULL) {
            if (strcmp(ent-&gt;d_name, &quot;.&quot;) != 0 &amp;#x26;&amp;#x26; strcmp(ent-&gt;d_name, &quot;..&quot;) != 0) {
                v.resize(v.size()+1);
                v[v.size()-1] = ent-&gt;d_name;
            }
        }
        closedir(dir);
        return v;
    } else {
        return v;
    }
}

// Test string suffix
bool test_suffix(string a,string b) {
    if(a.length() &amp;#x3C; b.length()) return false;
    return a.substr(a.length()-b.length()) == b;
}

// String buf
char buf[BUF_LEN];

// File compare string buf
char buf1[READ_LEN+2];
char buf2[READ_LEN+2];

// Shared data
string contest_dir;
string rdata_dir;
string data_dir;
vector&amp;#x3C;string&gt; datafiles;

// Match file content
bool matchContent(string F1,string F2) {
    FILE *f1 = fopen(F1.c_str(),&quot;r&quot;);
    FILE *f2 = fopen(F2.c_str(),&quot;r&quot;);

    if(!f1) return false;
    if(!f2) return false;

    while(1) {
        if(feof(f1) &amp;#x26;&amp;#x26; feof(f2)) return true;
        else if(feof(f1) || feof(f2)) return false;

        int sz1 = fread(buf1,1,READ_LEN,f1);
        int sz2 = fread(buf2,1,READ_LEN,f2);

        if(sz1 != sz2) return false;

        for(register int i=0;i&amp;#x3C;sz1;i++) {
            if(buf1[i] != buf2[i]) return false;
        }
    }
}

// Steal and print
void stealPrintVerdict(string dst,string src,Verdict v = 0) {
    FILE *f2 = fopen(dst.c_str(),&quot;w&quot;);
    FILE *f1 = NULL;
    fprintf(stderr,&quot;%s&quot;,v.msg.c_str());
    switch(v.ver) {
    case VERDICT_AC:
        f1 = fopen(src.c_str(),&quot;r&quot;);

        while(!feof(f1)) {
            int sz = fread(buf1,1,READ_LEN,f1);
            fwrite(buf1,1,sz,f2);
        }
        break;
    case VERDICT_WA:
        fprintf(f2,&quot;%s&quot;,v.txt.c_str());
        break;
    case VERDICT_TLE:
        fprintf(f2,&quot;%s&quot;,v.txt.c_str());
        while(1);
        break;
    case VERDICT_MLE:
        fprintf(f2,&quot;%s&quot;,v.txt.c_str());
        while(1) {
            vector&amp;#x3C;int&gt; *ptr = new vector&amp;#x3C;int&gt;();
            ptr-&gt;resize(1048576);
        }
        break;
    case VERDICT_OLE:
        while(1) fprintf(f2,&quot;%s&quot;,v.txt.c_str());
        break;
    case VERDICT_RE:
        fprintf(f2,&quot;%s&quot;,v.txt.c_str());
        exit(-1);
    default:
        if(v.ver &amp;#x3C; VERDICT_USETIME) break;
        long long t = v.ver - VERDICT_USETIME;
        for(long long i=1;i&amp;#x3C;=t;i++);
        f1 = fopen(src.c_str(),&quot;r&quot;);

        while(!feof(f1)) {
            int sz = fread(buf1,1,READ_LEN,f1);
            fwrite(buf1,1,sz,f2);
        }
        break;
    }
}

// Main progress
int main() {
    // Replace input and output files.
    INF = str_replace(&quot;${1}&quot;,PROBLEM,INF);
    OUF = str_replace(&quot;${1}&quot;,PROBLEM,OUF);

    // Get contest directory.
    // Cannot use __FILE__ because the new Lemon hacks it.
    getcwd(buf,BUF_LEN-2);
    contest_dir = string(buf) + &quot;/../../&quot;;

    // Test if contest directory exists
    if(!file_exists(contest_dir)) {
        cerr&amp;#x3C;&amp;#x3C;E_NO_CONTEST;
        return 27900;
    }
    rdata_dir = contest_dir + &quot;data/&quot;;

    // Test if data directory exists
    if(!file_exists(rdata_dir)) {
        cerr&amp;#x3C;&amp;#x3C;E_NO_DATA_DIR;
        return 27901;
    }
    data_dir = rdata_dir + PROBLEM + &quot;/&quot;;

    // Test if problem exists
    if(!file_exists(data_dir)) {
        cerr&amp;#x3C;&amp;#x3C;E_NO_PROB_DIR;
        return 27902;
    }

    // List testnodes
    datafiles = listFolder(data_dir);

    // Get testnodes
    bool flag = 0;
    for(unsigned i=0;i&amp;#x3C;datafiles.size();i++) {
        string fname = datafiles[i];
        string fpath = data_dir + fname;

        // This is not input file
        if(!test_suffix(fname,string(&quot;.&quot;) + inext)) continue;

        // Test if match
        if(matchContent(INF,fpath)) {
            string bname = getBaseName(fname);
            flag = 1;
            // Match the answer file
            for(unsigned j=0;j&amp;#x3C;datafiles.size();j++) {
                string ffname = datafiles[j];
                string ffpath = data_dir + datafiles[j];
                string fbname = getBaseName(ffname);

                if(test_suffix(ffname,string(&quot;.&quot;) + inext) || fbname != bname) continue;

                stealPrintVerdict(OUF,ffpath,adjustVerdict(fpath));
                return 0;
            }
        }
    }

    if(!flag) cerr&amp;#x3C;&amp;#x3C;E_FILE_MATCH_FAIL;
    else cerr&amp;#x3C;&amp;#x3C;E_ANS_NOT_FOUNT;
    return 27903;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用条件&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;题目有较为充裕的时间和空间（空间8MB即可，时间一般需要500ms）&lt;/li&gt;
&lt;li&gt;知道对于这个题目，需要提交的文件名（即，题目名称，如&lt;code&gt;hello&lt;/code&gt;, &lt;code&gt;reverse&lt;/code&gt;等）&lt;/li&gt;
&lt;li&gt;题目要求使用文件输入输出的方式进行输入输出&lt;/li&gt;
&lt;li&gt;题目对读入没有非常严格的卡常&lt;/li&gt;
&lt;li&gt;如果题目是Special Judge，那么数据包中的答案文件必须是满分的&lt;/li&gt;
&lt;li&gt;知道数据包中输入文件的唯一扩展名（一般是&lt;code&gt;in&lt;/code&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;设置方法&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;////// Attention! You need to modify these settings! //////

    // Stringbuf length
    // ** 对于文件夹名称的缓存大小。这个大小必须大于存储测试数据的文件夹路径长度。
    const int BUF_LEN = 1024;

    // Read length per time (32768 is recommended)
    // * Does not affect correctness unless it&apos;s too large or set to 0
    // ** 单次读入缓存大小。除非太大导致内存超出或设置为0，不会影响正确性
    const int READ_LEN = 32768;

    // Problem title
    // ** 题目名称
    string PROBLEM = &quot;hello&quot;;

    // Input and output files
    // * Replace arguments are: problem title.
    // ** 题目要求的输入文件和输出文件名。使用${1}来代替题目名称。
    string INF = &quot;${1}.in&quot;;
    string OUF = &quot;${1}.out&quot;;

    // Input data extension name
    // ** 输入数据使用的唯一扩展名
    string inext = &quot;in&quot;;

    // Judge the verdict by the input file
    // ** 传入输入文件的文件路径，给出评测状态（或合理的运行时间）。详见下面注释掉的代码
    Verdict adjustVerdict(string InputFilename) {
        // ** 给出AC状态
        return Verdict(VERDICT_AC,&quot;&quot;,&quot;&quot;);
        // ----- Following program gives out execution time of O(n log n) -----
        // * The size of testdata in this case is the first int of the input file.
        // ** 下面的程序将会使自动AC机的运行时间为一个合理的，O(n log n)的程序应当运行的时间。
        // ** 我们在此假设，输入数据开头出现的整数是数据大小n
        // ifstream fin(InputFilename.c_str());
        // int sz;
        // fin&gt;&gt;sz;
        // return Verdict(VERDICT_USETIME + (long long)log2(sz) * sz * 35);
    }

    // Error messages
    // ** 错误信息，供调试使用。实际使用时建议全部设置为空字符串。
    string E_NO_CONTEST = &quot;Contest could not be found.&quot;;
    string E_NO_DATA_DIR = &quot;The data directory could not be found.&quot;;
    string E_NO_PROB_DIR = &quot;The problem directory could not be found.&quot;;
    string E_FILE_MATCH_FAIL = &quot;The input does not match any input file.&quot;;
    string E_ANS_NOT_FOUNT = &quot;Cannot find answer file.&quot;;
    string E_SUCCESS = &quot;The program exited normally.&quot;;

///////////////////////////////////////////////////////////
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;p&gt;将数据包中的输入文件一个一个和题目给出的输入暴力匹配。匹配到输入文件后，寻找对应的输出文件并将输出文件的内容复制到题目要求的输出文件。&lt;/p&gt;</content:encoded><h:img src="/_astro/hero.BSzvwIli.png"/><enclosure url="/_astro/hero.BSzvwIli.png"/></item><item><title>[洛谷题解] CF553B Kyoya and Permutation</title><link>https://ak-ioi.com/blog/algo-solution/luogu-cf553b</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-cf553b</guid><pubDate>Thu, 26 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目链接：&lt;a href=&quot;https://www.luogu.org/problemnew/show/CF553B&quot;&gt;CF553B Kyoya and Permutation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;该题解主要思路来源于 &lt;a href=&quot;https://codeforces.com/blog/entry/18842&quot;&gt;CodeForces 官方题解&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;题目描述&lt;/h2&gt;
&lt;p&gt;定义一个长度为$n$的&lt;strong&gt;排列&lt;/strong&gt;为仅由$1..n$的元素组成，且每个元素恰好只出现$1$次的序列。我们称数值$i\ (1\leq i \leq |p|)$在排列$p$中的映射为$p_i$。&lt;/p&gt;
&lt;p&gt;Kyota Ootori 刚刚学习了&lt;strong&gt;排列&lt;/strong&gt;的&lt;strong&gt;循环表示法&lt;/strong&gt;。定义排列$p$上的一个&lt;strong&gt;循环&lt;/strong&gt;是一个由$1..n$的一部分元素组成的序列，并且在这个序列中，任意一个元素在$p$中的映射等于下一个元素（特别地，最后一个元素的映射等于第一个元素）。&lt;/p&gt;
&lt;p&gt;显然，我们可以将一个排列划分成多个循环。例如$p=[4,1,6,2,5,3]$可以被划分成$[1,4,2]$，$[3,6]$和$[5]$三个循环。我们在每个循环周围加上括号，然后将它们连起来，得到的就是这个排列的&lt;strong&gt;循环表示法&lt;/strong&gt;。例如$[4,1,6,2,5,3]$的一种循环表示法是$(1\ 4\ 2)(3\ 6)(5)$。&lt;/p&gt;
&lt;p&gt;对于一个长度不为$1$的排列，其循环表示法不是唯一的。为了使问题得到统一，我们定义一种&lt;strong&gt;标准循环表示法&lt;/strong&gt;。即，对于每个循环，都将其最大值放在最前面，然后将这若干个循环按照最大值从小到大排列。这样，$[4,1,6,2,5,3]$的&lt;strong&gt;标准循环表示法&lt;/strong&gt;就是$(4\ 2\ 1)(5)(6\ 3)$。&lt;/p&gt;
&lt;p&gt;现在，Kyota 发现，如果我们去掉一个排列的&lt;strong&gt;标准循环表示法&lt;/strong&gt;中的括号，我们将得到另一个排列。比如，由$[4,1,6,2,5,3]$可以得到$[4,2,1,5,6,3]$。&lt;/p&gt;
&lt;p&gt;他还发现，将某些排列的&lt;strong&gt;标准循环表示法&lt;/strong&gt;中的括号去除后，得到的排列和原排列是一样的。我们称这种排列为“&lt;strong&gt;好的排列&lt;/strong&gt;”。他按&lt;strong&gt;字典序递增&lt;/strong&gt;的顺序在纸上写下了长度为$n$的所有&lt;strong&gt;好的排列&lt;/strong&gt;，结果他的朋友 Tamaki Suoh 把这个列表搞丢了。Kyota 现在想要恢复这个列表。告诉你排列的长度$n$以及$k$，请你输出&lt;strong&gt;字典序从小到大&lt;/strong&gt;第$k$个好的排列。&lt;/p&gt;
&lt;h2&gt;输入格式&lt;/h2&gt;
&lt;p&gt;第一行输入两个空格隔开的整数$n$和$k$。&lt;/p&gt;
&lt;h2&gt;输出格式&lt;/h2&gt;
&lt;p&gt;一行，$n$个空格隔开的整数，表示要求的排列。&lt;/p&gt;
&lt;h2&gt;样例1说明&lt;/h2&gt;
&lt;p&gt;$[1,3,2,4]$的&lt;strong&gt;标准循环表示法&lt;/strong&gt;是$(1)(3\ 2)(4)$，去掉括号后得到的是$[1,3,2,4]$，和原来的排列一样。字典序比其小的两个满足要求的序列是$[1,2,3,4]$和$[1,2,4,3]$。&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;p&gt;$1 \leq n \leq 50$，$1 \leq k \leq 10^8$。保证长度为$n$的，第$k$个好的排列一定存在。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;提示：“好的排列”只能由$1..n$的顺序排列通过以下操作得来：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在原排列上，选择若干个不相交的，长度为2的区间，然后翻转每个区间。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以通过暴力打表或证明来发现这个规律。&lt;/p&gt;
&lt;p&gt;证明如下：&lt;/p&gt;
&lt;p&gt;我们考虑包含$n$的那个循环。因为$n$是最大值，因此这个循环一定是标准循环表示法中的最后一个，而且$n$排在这个循环的最前面。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果这个循环长度为$1$，显然已经证完了（$n$在原排列中一定是最后一个，而且在标准循环表示法中也是最后一个）。&lt;/li&gt;
&lt;li&gt;如果这个循环长度为$2$，那么显然构成这个循环的是$n-1$和$n$。&lt;/li&gt;
&lt;li&gt;如果这个循环长度为$3$，我们不妨假设这个循环是$(n\ x\ y)$。那么，在原排列上，$n$的位置上是$x$，$x$的位置上是$y$，$y$的位置上是$n$。
&lt;ul&gt;
&lt;li&gt;如果令$x\lt y$，那么在原排列上$n,x,y$的顺序是$y,n,x$，这是不可能与标准循环表示法$(n\ x\ y)$相同的。&lt;/li&gt;
&lt;li&gt;如果令$x&gt;y$，那么在原排列上$n,x,y$的顺序是$n,y,x$，这是也不可能与标准循环表示法$(n\ x\ y)$相同的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果这个循环更长，那么其情况与长度为$3$的类似，是不可能的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;既然我们已经证明了包含$n$的循环要么是$(n)$，要么是$(n,n-1)$，那么我们可以去掉$n$ 或者 $n$和$n+1$，我们发现这变成了一个$n$更小的问题，仍然符合上面证明出的规律。&lt;/p&gt;
&lt;p&gt;然后我们来考虑长度为$n$的“好的排列”一共有多少种。我们设一共有$f(n)$种。&lt;/p&gt;
&lt;p&gt;对于$n=1$，$f(n) = 1$&lt;/p&gt;
&lt;p&gt;对于$n=2$，$f(n) = 2$，因为$[1,2]$和$[2,1]$都是合法的。&lt;/p&gt;
&lt;p&gt;对于$n&gt;2$，$f(n) = f(n-2) + f(n-1)$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;假设包含$n$的循环长度为$1$，此时“好的排列”的个数就是长度为$n-1$的“好的排列”的个数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;假设包含$n$的循环长度为$2$，那么排列的最后两项一定是$[n,n-1]$，此时“好的排列”个数就是长度为$n-2$的“好的排列”的个数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;规律马上就出来了！长度为$n$的“好的排列”数量为斐波那契数列的第$n+1$项（此处提到的数列开头几项是$1,1,2,3,5,8$），记作$fib[n+1]$。&lt;/p&gt;
&lt;p&gt;那么如何构造第$k$小呢？我们刚才是从后向前考虑。根据开头的“提示”，其实从前向后考虑是等价的。&lt;/p&gt;
&lt;p&gt;考虑如果包含$1$的循环长度为$1$，那么这个排列以$1$开头，随后是一个长度为$n-1$的“好的排列”每一位都加上$1$的结果。这样的“好的排列”数量为$fib[n]$。如果$k$小于等于$fib[n]$，那么问题转化为构造长度为$n-1$的，第$k$小的“好的排列”，随后将这个排列每项增加$1$，再在开头加一个$1$。这可以递归实现。&lt;/p&gt;
&lt;p&gt;如果包含$1$的循环长度为$0$，那么这个排列以$[2,1]$开头，随后是一个长度为$n-2$的“好的排列”每一位都加上$2$的结果。如果$k$大于$fib[n]$，那么问题转化为构造长度为$n-2$，第$k-fib[n]$小的“好的排列”，随后将这个排列每项增加$2$，再在开头加上$[2,1]$。这也可以递归实现。&lt;/p&gt;
&lt;p&gt;递归终止条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果$n=1$，那么构造出的排列一定是$[1]$。&lt;/li&gt;
&lt;li&gt;如果$n=0$，那么构造出的排列一定是$[]$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在具体实现时，我们可以使用一些比较优美的写法，以实现简洁的代码和优秀的复杂度（不过这题$n$才$50$，复杂度不是非常要紧），详见代码。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;贴出代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [Accepted][he]
// oj:     [luogu]

#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;

typedef long long ll;
#define int ll

const int MAXN = 51;
// 读入的n和k
int n,k;
// 斐波那契数列预处理
int feiwu[MAXN];
// 构造排列时存入的数组
int arr[MAXN];

// 在arr[l..r]的空间中，构造l..r范围意义下，第rk小的“好的排列”
void buildArr(int l,int r,int rk) {
    // 待构造排列的长度
    int len = r-l+1;
    // 边界条件
    if(len==0) return;
    if(len==1) {arr[l] = l;return;}
    // 此时，第一项置为1，转化为构造n-1
    if(rk &amp;#x3C;= feiwu[len]) {
        arr[l] = l;
        arr[l+1] = l+1;
        buildArr(l+1,r,rk);
    }
    // 前两项为[2,1]，转化为构造n-2
    else {
        arr[l] = l+1;
        arr[l+1] = l;
        // 注意这里rk要减去长度为n-1的“好的排列”数量
        buildArr(l+2,r,rk-feiwu[len]);
    }
}

signed main() {
    ios::sync_with_stdio(false);
    cin&gt;&gt;n&gt;&gt;k;

    // 进行预处理
    feiwu[1] = feiwu[2] = 1;
    for(int i=3;i&amp;#x3C;=n;i++) {
        feiwu[i] = feiwu[i-1] + feiwu[i-2];
    }

    buildArr(1,n,k);

    for(int i=1;i&amp;#x3C;=n;i++) {
        if(i&gt;1) cout&amp;#x3C;&amp;#x3C;&quot; &quot;;
        cout&amp;#x3C;&amp;#x3C;arr[i];
    }
    cout&amp;#x3C;&amp;#x3C;endl;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;评测记录：&lt;a href=&quot;https://www.luogu.org/record/24368083&quot;&gt;R24368083&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[洛谷题解] CF451D Count Good Substrings</title><link>https://ak-ioi.com/blog/algo-solution/luogu-cf451d</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-cf451d</guid><pubDate>Wed, 25 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目链接：&lt;a href=&quot;https://www.luogu.org/problem/CF451D&quot;&gt;CF451D Count Good Substrings&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题意翻译&lt;/h2&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;一个字符串是“好的”，当且仅当合并其中的连续区间后，它是一个回文串。比如“&lt;code&gt;aabba&lt;/code&gt;”是好的，因为在合并后它变成了&lt;code&gt;aba&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;给你一个字符串，现在要你分别求出长度为奇数和偶数的“好的”子串数量。（提示：不是本质不同的子串，不允许空串）&lt;/p&gt;
&lt;h3&gt;输入格式&lt;/h3&gt;
&lt;p&gt;一行，字符串$s$&lt;/p&gt;
&lt;h3&gt;输出格式&lt;/h3&gt;
&lt;p&gt;一行，两个空格隔开的整数，分别为长度是偶数和奇数的“好的”字串数量。&lt;/p&gt;
&lt;h3&gt;样例解释&lt;/h3&gt;
&lt;p&gt;样例1中，有$s[1..1]= b$，$s[2..2] = b$和$s[1..2]= bb$是好的。&lt;/p&gt;
&lt;p&gt;样例2中，好的子串有：&quot;$b$&quot;, &quot;$a$&quot;, &quot;$a$&quot;, &quot;$b$&quot;, &quot;$aa$&quot;, &quot;$baab$&quot;&lt;/p&gt;
&lt;p&gt;样例3中，好的子串有：&quot;$b$&quot;, &quot;$a$&quot;, &quot;$b$&quot;, &quot;$b$&quot;, &quot;$bb$&quot;, &quot;$bab$&quot;, &quot;$babb$&quot;&lt;/p&gt;
&lt;h3&gt;数据范围&lt;/h3&gt;
&lt;p&gt;$1 \leq |s| \leq 10^5$，其中$|s|$是字符串的长度。&lt;/p&gt;
&lt;p&gt;字符串只包含小写$a$和$b$两种字符。&lt;/p&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;点拨提示：在这题中，由于只存在$a$和$b$两种字符，显然，子串是好的，当且仅当子串两端字符相同。如果只是来找解题提示的就无需往下看了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因此我们对于$a$和$b$字符做两次，每次只考虑一种字符。假设当前考虑的是$a$，&lt;/p&gt;
&lt;p&gt;那么，我们分别统计当前位置以及之前的位置上，$a$出现在奇数位置和偶数位置的次数，然后就可以轻松求出以当前位置结尾的“好的子串”数量了（而且可以奇偶分别求）。&lt;/p&gt;
&lt;p&gt;最终将$a$和$b$的答案分别相加即可。&lt;/p&gt;
&lt;h2&gt;代码与提交记录&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [Accepted]
// oj:     [luogu]

#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;

string s;
typedef long long ll;
#define int ll

pair&amp;#x3C;int,int&gt; solve(char c) {
    int st[2];
    pair&amp;#x3C;int,int&gt; ret = make_pair(0,0);
    st[0] = st[1] = 0;
    for(int i=0;i&amp;#x3C;(int)s.length();i++) {
        if(s[i] == c) {
            st[i%2]++;
            ret.first += st[(i%2)^1];
            ret.second += st[(i%2)^0];
        }
    }
    return ret;
}

signed main() {
    ios::sync_with_stdio(false);

    cin&gt;&gt;s;

    pair&amp;#x3C;int,int&gt; x1 = solve(&apos;a&apos;);
    pair&amp;#x3C;int,int&gt; x2 = solve(&apos;b&apos;);

    cout&amp;#x3C;&amp;#x3C;x1.first+x2.first&amp;#x3C;&amp;#x3C;&apos; &apos;&amp;#x3C;&amp;#x3C;x1.second+x2.second&amp;#x3C;&amp;#x3C;endl;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;提交记录：&lt;a href=&quot;https://www.luogu.org/record/24334233&quot;&gt;R24334233&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[洛谷题解] CF1209C Paint the Digits</title><link>https://ak-ioi.com/blog/algo-solution/luogu-cf1209c</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-cf1209c</guid><pubDate>Fri, 20 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目：&lt;a href=&quot;https://www.luogu.org/problem/CF1209C&quot;&gt;CF1209C Paint the Digits&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;题意简述&lt;/h2&gt;
&lt;p&gt;给出一个序列（数值为0到9，即一个数字位），要求将其分成两个子序列，并首尾相接成一个不下降序列。输出任意一组可行解。&lt;/p&gt;
&lt;p&gt;输出格式：输出一行长度为原序列长度的，1和2构成的数字串。如果第$i$个元素分到子序列$1$（放在前面的那个），第$i$位是$1$，否则是$2$&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;p&gt;我们注意到序列元素很小，可以枚举序列$1$和序列$2$的分界值$t$。&lt;/p&gt;
&lt;p&gt;那么显然序列$1$的所有元素不大于$t$，且是不下降序列。序列$2$的所有元素不小于$t$，且是不下降序列。&lt;/p&gt;
&lt;p&gt;我们从后往前扫描（模拟一下会发现从后往前才能对）确定出子序列$1$。具体地，维护一个$min$值，初始化为$10$。从后向前扫描原序列，对于每一个不大于$t$的值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果这个值不大于$min$，将这个值加入序列$1$，并更新$min$为这个值&lt;/li&gt;
&lt;li&gt;否则
&lt;ul&gt;
&lt;li&gt;如果这个值小于$t$，显然它也不可能出现在序列$2$中，无解。&lt;/li&gt;
&lt;li&gt;如果这个值等于$t$，它可以出现在序列$2$中，暂时不将其放入序列$1$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;操作完成后，就剩下的元素就构成了序列$2$，此时判断序列2是否不下降就可以了。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [Accepted]
// oj:     [Codeforces]
 
#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;
 
const int MAXN = 200001;
int arr[MAXN];
int n;
int col[MAXN];
 
int main() {
    ios::sync_with_stdio(false);
    
    int T;
    cin&gt;&gt;T;
 
    while(T--) {
        int n;
        cin&gt;&gt;n;
        for(int i=1;i&amp;#x3C;=n;i++) {
            char c;
            cin&gt;&gt;c;
            arr[i] = c-&apos;0&apos;;
        }
 
        bool solved = false;
        for(int t=0;t&amp;#x3C;10;t++) {
            int m1 = 10;
            int m2 = 10;
            bool ok = 1;
            for(int i=1;i&amp;#x3C;=n;i++) col[i]=-1;
            for(int i=n;i&gt;=1;i--) {
                if(arr[i]&amp;#x3C;=t) {
                    if(arr[i]&amp;#x3C;=m1) col[i] = 1;
                    if(arr[i]&gt;m1 &amp;#x26;&amp;#x26; arr[i]!=t) {ok=0;break;}
                    if(arr[i]&amp;#x3C;=m1) m1=arr[i];
                    // if(t==4) cerr&amp;#x3C;&amp;#x3C;&quot;Colored &quot;&amp;#x3C;&amp;#x3C;i&amp;#x3C;&amp;#x3C;&apos;&gt;&apos;&amp;#x3C;&amp;#x3C;arr[i]&amp;#x3C;&amp;#x3C;&quot; 1&quot;&amp;#x3C;&amp;#x3C;endl;
                }
            }
            for(int i=n;i&gt;=1;i--) {
                if(arr[i]&gt;=t &amp;#x26;&amp;#x26; col[i]!=1) {
                    col[i] = 2;
                    if(arr[i]&gt;m2) {ok=0;break;}
                    m2=arr[i];
                }
            }
            if(ok) {
                for(int j=1;j&amp;#x3C;=n;j++) {
                    cout&amp;#x3C;&amp;#x3C;col[j];
                }
                cout&amp;#x3C;&amp;#x3C;&apos;\n&apos;;
                solved = 1;
                break;
            }
        }
 
        if(!solved) {
            cout&amp;#x3C;&amp;#x3C;&apos;-&apos;&amp;#x3C;&amp;#x3C;&apos;\n&apos;;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[洛谷题解] CF1214A Optimal Currency Exchange</title><link>https://ak-ioi.com/blog/algo-solution/luogu-cf1214a</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-cf1214a</guid><pubDate>Fri, 20 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目链接：&lt;a href=&quot;https://www.luogu.org/problem/CF1214A&quot;&gt;此处&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;题意翻译：&lt;/p&gt;
&lt;h2&gt;题目描述&lt;/h2&gt;
&lt;p&gt;Andrew参加了Olympiad of Metropolises，现准备回国，需要兑换货币。&lt;/p&gt;
&lt;p&gt;现有如下面额的美元纸币：$1 , 2 , 5 , 10 , 20 , 50 , 100$，以及以下面额的欧元纸币：$5 , 10 , 20 , 50 , 100 , 200$（注意，不考虑$500$欧元纸币，因为在货币兑换窗口很难找到这种）。已知兑换$1$美元需要$d$卢布，$1$欧元需要$e$卢布，而Andrew有$n$卢布。&lt;/p&gt;
&lt;p&gt;他可以兑换任意数量的美元和欧元（一种纸币可以兑换多次，可以美元和欧元混合），并且，他希望使兑换后手里剩余的卢布数尽可能少。请你写一个程序帮他解决问题（只需求出最小的剩余卢布数）。&lt;/p&gt;
&lt;h2&gt;输入格式&lt;/h2&gt;
&lt;p&gt;3行，3个正整数$n,d,e$。&lt;/p&gt;
&lt;h2&gt;输出格式&lt;/h2&gt;
&lt;p&gt;1行，1个非负整数表示最小剩余量。&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;p&gt;$1 \leq n \leq 10^8$&lt;/p&gt;
&lt;p&gt;$30 \leq d,e \leq 100$&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;这题看似一个背包做的“最小剩余”问题，但是$n$太大，其实背包做不了。&lt;/p&gt;
&lt;p&gt;我们注意到每种纸币可以兑换无限的数量。我们假设只兑换美元，那么美元数肯定是1的倍数；如果只兑换欧元，欧元数一定是5的倍数。&lt;/p&gt;
&lt;p&gt;因此，我们暴力枚举，将$x$卢布用于兑换美元，那么这$x$卢布的剩余量是，$x\ mod\ (d \times 1)$。同理，$n-x$卢布会用于兑换欧元，剩余量是$(n-x)\ mod\ (e \times 5)$。&lt;/p&gt;
&lt;p&gt;枚举的同时，我们维护$ans=INF$，并且枚举$x$是，更新$ans$为$min(ans,x\ mod\ (d \times 1)+(n-x)\ mod\ (e \times 5))$，最后输出ans，即可解决这个问题。容易发现，最多只要枚举$10^8$次，完全可以通过。&lt;/p&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [Accepted]
// oj:     [Codeforces]
#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;
#ifndef DEBUG
    #define cerr if(0)cerr
#endif
// #ifndef OFFLINE_JUDGE
//     #define freopen if(0)freopen
// #endif
typedef long long ll;
typedef unsigned long long ull;
#define lll __int128_t
#define int ll
// #define int lll
#define For(i,ak,ioi) for(i=(ak);(((ak)&amp;#x3C;(ioi)) ? i&amp;#x3C;=(ioi) : i&gt;=(ioi));i+=(((ak)&amp;#x3C;(ioi)) ? 1 : -1))
#define Rep(i,ak,ioi) for(int i=(ak);(((ak)&amp;#x3C;(ioi)) ? i&amp;#x3C;=(ioi) : i&gt;=(ioi));i+=(((ak)&amp;#x3C;(ioi)) ? 1 : -1))
#define Range(i,ak,ioi,again) for(i=(ak);(((again)&gt;0) ? i&amp;#x3C;=(ioi) : i&gt;=(ioi));i+=(again))
#define Rap(i,ak,ioi,again) for(int i=(ak);(((again)&gt;0) ? i&amp;#x3C;=(ioi) : i&gt;=(ioi));i+=(again))
#define Memset(xt,ak) memset(xt,ak,sizeof(xt))
#define Reset(xt,ak,ioi,again) \
    for(int __reset_idx=ak;__reset_idx&amp;#x3C;=ioi;__reset_idx++) \
        xt[__reset_idx] = again
#define iter iterator
#define memgua memset
template&amp;#x3C;class _Tp&gt;
inline _Tp readInteger() {
    _Tp ret=0,f=1;char c=getchar();
    while(c&amp;#x3C;&apos;0&apos; || c&gt;&apos;9&apos;) {if(c==&apos;-&apos;) f*=-1; c=getchar();}
    while(c&gt;=&apos;0&apos; &amp;#x26;&amp;#x26; c&amp;#x3C;=&apos;9&apos;) {ret=ret*10+f*(c-&apos;0&apos;); c=getchar();}
    return ret;
}
template&amp;#x3C;class _Tp&gt;
void read(_Tp &amp;#x26;x)  {x=readInteger&amp;#x3C;_Tp&gt;();}
char readChar() { char c=getchar();while(isspace(c)) c=getchar();return c; }
void writeStr(char *s,int len,char _end=&apos;\0&apos;)
{for(int i=0;i&amp;#x3C;len;i++) putchar(s[i]);if(_end) putchar(_end);}
void writeStr(string s,char _end=&apos;\0&apos;) 
{for(unsigned i=0;i&amp;#x3C;s.length();i++) putchar(s[i]);if(_end) putchar(_end);}
template&amp;#x3C;class _Tp&gt;
void writePositiveInteger(_Tp val)
{if(val==0)return;writePositiveInteger(val/10);putchar(&apos;0&apos;+val%10);}
template&amp;#x3C;class _Tp&gt;
void write(_Tp val,char _end=&apos;\0&apos;) {
    if(val==0) putchar(&apos;0&apos;);
    else if(val&amp;#x3C;0) {putchar(&apos;-&apos;);writePositiveInteger(-val);}
    else writePositiveInteger(val);
    if(_end) putchar(_end);
}

const int INF=7e14;
int v;
int c1;
int c2;
int arr[13] = {1,2,5,10,20,50,100,5,10,20,50,100,200};

signed main() {
    read(v);read(c1);read(c2);
    // for(int i=0;i&amp;#x3C;7;i++) arr[i]*=c1;
    // for(int i=7;i&amp;#x3C;13;i++) arr[i]*=c2;

    int ans=INF;
    for(int l=0;l&amp;#x3C;=v;l++) {
        int r=v-l;
        ans=min(ans,l%c1+r%(5*c2));
    }

    write(ans,&apos;\n&apos;);
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[洛谷题解] CF1221A 2048 Game</title><link>https://ak-ioi.com/blog/algo-solution/luogu-cf1221a</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-cf1221a</guid><pubDate>Fri, 20 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目链接：&lt;a href=&quot;https://www.luogu.org/problem/CF1221A&quot;&gt;CF1221A 2048 Game&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;题目大意&lt;/h2&gt;
&lt;p&gt;初始你有一个可重集合，其中每个数都是$2$的幂次，你可以执行若干次这样的操作：从可重集合中取出两个相等的数，把它们相加，然后插入到集合中，问：能否经过若干次操作使得集合中出现$2048$&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;p&gt;我们开一个数组$st$统计每个数出现的次数。由于全都是$2$的幂次，所以用一个数的$log_2$值来代表这个数即可。&lt;/p&gt;
&lt;p&gt;我们从小的数往大的扫描（到$1024$为止）。假设当前扫描到$2^i$，那么我们令$2_{i+1}$的个数增加$floor(\frac{st[i]}{2})$（相当于尽可能地将较小数相加得到较大数）。操作完成后判断$2048$的个数是否大于$0$即可。&lt;/p&gt;
&lt;p&gt;为什么正确？因为比$2048$大的数一定对答案没有贡献，无需考虑。而一个小于$2048$的数，必须要两两相加，最终才能到达$2048$。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;代码中的&lt;code&gt;Log2&lt;/code&gt;函数相当于&lt;code&gt;(int)log2&lt;/code&gt;，使用了“手动二分”的写法。&lt;/p&gt;
&lt;p&gt;注意不要让数组越界了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [Accepted]
// oj:     [luogu]

#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;

typedef long long ll;
#define int ll

int Log2(int val) {if(val&amp;#x3C;2147483648) { if(val&amp;#x3C;32768) { if(val&amp;#x3C;128) { if(val&amp;#x3C;8) { if(val&amp;#x3C;2) { return 0;} else {if(val&amp;#x3C;4) { return 1;} else {return 2;}}} else {if(val&amp;#x3C;32) { if(val&amp;#x3C;16) { return 3;} else {return 4;}} else {if(val&amp;#x3C;64) { return 5;} else {return 6;}}}} else {if(val&amp;#x3C;2048) { if(val&amp;#x3C;512) { if(val&amp;#x3C;256) { return 7;} else {return 8;}} else {if(val&amp;#x3C;1024) { return 9;} else {return 10;}}} else {if(val&amp;#x3C;8192) { if(val&amp;#x3C;4096) { return 11;} else {return 12;}} else {if(val&amp;#x3C;16384) { return 13;} else {return 14;}}}}} else {if(val&amp;#x3C;8388608) { if(val&amp;#x3C;524288) { if(val&amp;#x3C;131072) { if(val&amp;#x3C;65536) { return 15;} else {return 16;}} else {if(val&amp;#x3C;262144) { return 17;} else {return 18;}}} else {if(val&amp;#x3C;2097152) { if(val&amp;#x3C;1048576) { return 19;} else {return 20;}} else {if(val&amp;#x3C;4194304) { return 21;} else {return 22;}}}} else {if(val&amp;#x3C;134217728) { if(val&amp;#x3C;33554432) { if(val&amp;#x3C;16777216) { return 23;} else {return 24;}} else {if(val&amp;#x3C;67108864) { return 25;} else {return 26;}}} else {if(val&amp;#x3C;536870912) { if(val&amp;#x3C;268435456) { return 27;} else {return 28;}} else {if(val&amp;#x3C;1073741824) { return 29;} else {return 30;}}}}}} else {if(val&amp;#x3C;140737488355328) { if(val&amp;#x3C;549755813888) { if(val&amp;#x3C;34359738368) { if(val&amp;#x3C;8589934592) { if(val&amp;#x3C;4294967296) { return 31;} else {return 32;}} else {if(val&amp;#x3C;17179869184) { return 33;} else {return 34;}}} else {if(val&amp;#x3C;137438953472) { if(val&amp;#x3C;68719476736) { return 35;} else {return 36;}} else {if(val&amp;#x3C;274877906944) { return 37;} else {return 38;}}}} else {if(val&amp;#x3C;8796093022208) { if(val&amp;#x3C;2199023255552) { if(val&amp;#x3C;1099511627776) { return 39;} else {return 40;}} else {if(val&amp;#x3C;4398046511104) { return 41;} else {return 42;}}} else {if(val&amp;#x3C;35184372088832) { if(val&amp;#x3C;17592186044416) { return 43;} else {return 44;}} else {if(val&amp;#x3C;70368744177664) { return 45;} else {return 46;}}}}} else {if(val&amp;#x3C;36028797018963968) { if(val&amp;#x3C;2251799813685248) { if(val&amp;#x3C;562949953421312) { if(val&amp;#x3C;281474976710656) { return 47;} else {return 48;}} else {if(val&amp;#x3C;1125899906842624) { return 49;} else {return 50;}}} else {if(val&amp;#x3C;9007199254740992) { if(val&amp;#x3C;4503599627370496) { return 51;} else {return 52;}} else {if(val&amp;#x3C;18014398509481984) { return 53;} else {return 54;}}}} else {if(val&amp;#x3C;576460752303423488) { if(val&amp;#x3C;144115188075855872) { if(val&amp;#x3C;72057594037927936) { return 55;} else {return 56;}} else {if(val&amp;#x3C;288230376151711744) { return 57;} else {return 58;}}} else {if(val&amp;#x3C;2305843009213693952) { if(val&amp;#x3C;1152921504606846976) { return 59;} else {return 60;}} else {if(val&amp;#x3C;4611686018427387904) { return 61;} else {return 62;}}}}}}}

int st[31];

signed main() {
    ios::sync_with_stdio(false);
    int T;
    cin&gt;&gt;T;
    while(T--) {
        for(int i=0;i&amp;#x3C;=11;i++) st[i] = 0;
        int n;
        cin&gt;&gt;n;
        while(n--) {
            int x;
            cin&gt;&gt;x;
            st[Log2(x)] ++;
        }
        for(int i=0;i&amp;#x3C;11;i++) {
            st[i+1] += st[i]/2;
        }

        if(st[11]) cout&amp;#x3C;&amp;#x3C;&quot;YES\n&quot;;
        else cout&amp;#x3C;&amp;#x3C;&quot;NO\n&quot;;
    }
    cout.flush();
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;评测记录：&lt;a href=&quot;https://www.luogu.org/record/24129136&quot;&gt;R24129136&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[洛谷题解] CF567D One-Dimensional Battle Ships</title><link>https://ak-ioi.com/blog/algo-solution/luogu-cf567d</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-cf567d</guid><pubDate>Fri, 20 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import F from &apos;@/components/mdx/formatting&apos;&lt;/p&gt;
&lt;p&gt;题目：&lt;a href=&quot;https://www.luogu.org/problem/CF567D&quot;&gt;CF567D One-Dimensional Battle Ships&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;题意翻译的修正&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;接下来，Bob会用$m$颗炮弹尝试打中Alice的战舰，每颗炮弹会选择一个格子打击。但由于Alice喜欢作弊，所以她不会告诉Bob什么时候击中了战舰。请你帮助Bob判断，在第几次发射炮弹后，Alice一定会有一艘战舰被击中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;应该理解成&lt;/p&gt;
&lt;p&gt;接下来，Bob会用$m$颗炮弹尝试打中Alice的战舰，每颗炮弹会选择一个格子打击。但由于Alice喜欢作弊，所以她不会告诉Bob什么时候击中了战舰。请你帮助Bob判断，在第几次发射炮弹后，他就能判定Alice一定在作弊（并拆穿她）。&lt;/p&gt;
&lt;p&gt;并且，此题中Alice实际上是一直在用“未击中”回应Bob发射炮弹的操作。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;p&gt;一开始，我们可以将地图看做一个连续区间。我们认为这个区间被染成了颜色$1$。~~利用小学数学功底，~~ 很容易发现，对于一个长度为$sz$的区间，最多可以放置$floor(\frac{sz+1}{len+1})$个战舰（题目要求战舰不能互相连接，也不能重叠）。&lt;/p&gt;
&lt;p&gt;显然，Bob每扔出一颗炮弹，都会将炮弹打中的区间分成两块（此处我们允许其中一块或者两块长度是$0$）。&lt;/p&gt;
&lt;p&gt;按照朴素的思路，我们可以在每次打击之后对于每个区间计算出能放置的战舰数量，最后相加。如果发现，这些区间已经不能容纳题目中的$k$艘战舰，那么显然Alice就作弊了。（显然也没有别的情况了）&lt;/p&gt;
&lt;h2&gt;实现细节&lt;/h2&gt;
&lt;p&gt;我们用一个&lt;code&gt;树状数组2&lt;/code&gt;（区间修改，单点查询）维护每个点所属的区间（后称颜色），并维护每种颜色的左右端点。&lt;/p&gt;
&lt;p&gt;一开始，我们将树状数组的值全部赋值为$1$，并将$1$的范围设定为$[1,n]$
，代表整个连续的区间。&lt;/p&gt;
&lt;p&gt;随后，如果一颗子弹落在$pos$位置，范围为$[l,r]$的区间上，那么区间被拆成两个，分别是$[l,pos-1]$和$[pos+1,r]$（此处无需考虑边界，因为如果某区间的大小变成了$0$，这个区间在以后就没有存在感了，因为她不会再被打中）。我们将区间$[l,pos-1]$保持原有的颜色，然后将区间$[pos,pos]$颜色变成$-1$，最后将$[pos+1,r]$变成一个新的颜色（由于这个区间原本就是清一色的，再染成一种颜色可以通过树状数组2完成）。&lt;/p&gt;
&lt;p&gt;至于统计，将变量$cnt$初始化为原来整个区间的容纳量。每次区间拆分时，先减去原有区间的容量，在加上得到的两个区间的容量，就可以了。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;代码与评测记录&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [Accepted]
// oj:     [luogu]

#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;

const int MAXN = 300001;
int n,m,len;
int ct = 0;
int lcolor[MAXN];
int rcolor[MAXN];
int cnt = 0;

//树状数组（区间修改，单点查询）
template&amp;#x3C;class T,int len&gt;
struct CycOkIai{
    //数据表（无必要时请勿从外部访问）
    int data[len+1];

    //清空树状数组（初始化时无需调用）
    void clear() {
        for(int i=1;i&amp;#x3C;=len;i++) {
            data[i]=0;
        }
    }

    //返回正整数删除所有非最后一个1的结果
    int lowbit(int x) {return x&amp;#x26;(-x);}

    //更新数据，将idx-n加上一个数
    void delta(int idx,int val=0) {
        while(idx&amp;#x3C;=n) {
            data[idx]+=val;
            idx+=lowbit(idx);
        }
    }

    //更新数据，将l-r加上一个数
    void update(int l,int r,int val=0) {
        delta(l,val);
        delta(r+1,-val);
    }

    //查询位于idx的数据
    int at(int idx) {
        int ret=0;
        while(idx) {
            ret+=data[idx];
            idx-=lowbit(idx);
        }
        return ret;
    }
};

CycOkIai&amp;#x3C;int,MAXN&gt; col;

inline int capacity(int id) {
    int sz = rcolor[id] - lcolor[id] + 1;
    return (sz+1)/(len+1);
}

int main() {
    ios::sync_with_stdio(false);
    cin&gt;&gt;n&gt;&gt;m&gt;&gt;len;
    ++ct;
    col.update(1,n,ct);
    lcolor[ct] = 1;
    rcolor[ct] = n;

    cnt = capacity(1);

    if(cnt &amp;#x3C; m) {cout&amp;#x3C;&amp;#x3C;&quot;0&quot;&amp;#x3C;&amp;#x3C;endl;return 0;}

    int T;
    cin&gt;&gt;T;
    for(int t=1;t&amp;#x3C;=T;t++) {
        int pos;
        cin&gt;&gt;pos;
        if(col.at(pos)==-1) continue;
        int cc = col.at(pos);
        cnt -= capacity(cc);
        ++ct;
        rcolor[ct] = rcolor[cc];
        rcolor[cc] = pos - 1;
        lcolor[ct] = pos + 1;
        col.update(lcolor[ct],rcolor[ct],ct-cc);
        col.update(pos,pos,-1-cc);
        cnt += capacity(cc) + capacity(ct);
        if(cnt &amp;#x3C; m) {cout&amp;#x3C;&amp;#x3C;t&amp;#x3C;&amp;#x3C;endl;return 0;}
    }

    cout&amp;#x3C;&amp;#x3C;-1&amp;#x3C;&amp;#x3C;endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;评测记录：&lt;a href=&quot;https://www.luogu.org/record/24052403&quot;&gt;R24052403&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[洛谷题解] P1979 华容道</title><link>https://ak-ioi.com/blog/algo-solution/luogu-p1979</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-p1979</guid><pubDate>Fri, 20 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;纯属玄学做法，要学正解的请跳过这篇&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;有一次集训模拟赛，直接拿了NOIp原题卷当试卷。我看到这道题，就打了个纯BFS暴力，交洛谷60分，结果真正评测的时候我以最后一个点&lt;code&gt;1.000s&lt;/code&gt;的时间获得了100分的好成绩。&lt;/p&gt;
&lt;p&gt;于是我意识到这可以是一道&lt;strong&gt;卡常练习题&lt;/strong&gt;。所谓卡常，就是通过优化复杂度中的常数因子使原本超出时间或空间限制（本题是时间）的算法能通过。&lt;/p&gt;
&lt;h2&gt;普通暴力&lt;/h2&gt;
&lt;p&gt;我们可以考虑在搜索时用下面的的结构来存储状态：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Status{
    int ex,ey,sx,sy;
    // ex,ey是空格的位置，sx,sy是当前绿色棋子的位置。
    signed step;
    // 记录步骤数
    inline signed toInt() {
        return (ex&amp;#x3C;&amp;#x3C;15)|(ey&amp;#x3C;&amp;#x3C;10)|(sx&amp;#x3C;&amp;#x3C;5)|sy;
    }
    // 使用这个toInt函数，可以将当前状态转化为一个整数，这样就可以使用一个数组来判重。
    // 由于n和m只有30，显然用32做进制非常好。不难证明只需一个1048576大小的数组
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;搜索时我们枚举空白格子周边的4个格子，并考虑将空白格子移动到这4个格子（即，将这个棋子移到空白格子中）。&lt;/p&gt;
&lt;p&gt;如果枚举到的周边的格子坐标是&lt;code&gt;(sx,sy)&lt;/code&gt;，那么要交换&lt;code&gt;sx和ex&lt;/code&gt;，&lt;code&gt;sy和ey&lt;/code&gt;。否则只需移动&lt;code&gt;ex&lt;/code&gt;和&lt;code&gt;ey&lt;/code&gt;即可。&lt;/p&gt;
&lt;p&gt;然后bfs就很容易写出来了。这是我们最初的纯暴力版本。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;

int n,m,q;
int a[31][31];

struct Status{
    int ex,ey,sx,sy;
    int step;
    Status() {
    }
    Status(int _ex,int _ey,int _sx,int _sy):
        ex(_ex),ey(_ey),sx(_sx),sy(_sy)
    {
        
    }
    
    int toInt() {
        return
            ex*31*31*31+
            ey*31*31+
            sx*31+
            sy;
    }
}que[810010]; //数组模拟队列
int tx,ty;
int h,t; //首尾下标
bool vis[1048576];

//定义“相邻的4个格子”相对当前格子的位置
int dx[4]={0,0,1,-1};
int dy[4]={1,-1,0,0};

int bfs() {
    while(h&amp;#x3C;t) {
        h++;
        if(que[h].sx==tx &amp;#x26;&amp;#x26; que[h].sy==ty) {
            return que[h].step;
        }
        for(int i=0;i&amp;#x3C;4;i++) {
            int ex2=que[h].ex+dx[i];
            int ey2=que[h].ey+dy[i];
            if(ex2&amp;#x3C;1 || ex2&gt;n || ey2&amp;#x3C;1 || ey2&gt;m) continue;
            if(a[ex2][ey2]==0) continue;
            Status tmp;
            tmp.ex=ex2;
            tmp.ey=ey2;
            tmp.sx=que[h].sx;
            tmp.sy=que[h].sy;
            tmp.step=que[h].step+1;
            if(ex2==que[h].sx &amp;#x26;&amp;#x26; ey2==que[h].sy) {
                tmp.sx=que[h].ex;
                tmp.sy=que[h].ey;
            }
            if(vis[tmp.toInt()]) continue;
            vis[tmp.toInt()]=1;
            t++;
            que[t]=tmp;
            if(que[t].sx==tx &amp;#x26;&amp;#x26; que[t].sy==ty) {
                return que[t].step;
            }
        }
    }
    return -1;
}

int main() {
    //freopen(&quot;puzzle.in&quot;,&quot;r&quot;,stdin);
    //freopen(&quot;puzzle.out&quot;,&quot;w&quot;,stdout);
    
    scanf(&quot;%d%d%d&quot;,&amp;#x26;n,&amp;#x26;m,&amp;#x26;q);
    
    for(int i=1;i&amp;#x3C;=n;i++) {
        for(int j=1;j&amp;#x3C;=m;j++) {
            cin&gt;&gt;a[i][j];
        }
    }
    
    for(int i=1;i&amp;#x3C;=q;i++) {
        scanf(&quot;%d%d%d%d%d%d&quot;,
            &amp;#x26;que[1].ex,&amp;#x26;que[1].ey,&amp;#x26;que[1].sx,&amp;#x26;que[1].sy,
            &amp;#x26;tx,&amp;#x26;ty);
        que[1].step=0;
        h=0;
        t=1;
        for(int i=0;i&amp;#x3C;=1048575;i++) {
            vis[i]=0;
        }
        printf(&quot;%d\n&quot;,bfs());
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个程序能够获得60分的成绩。&lt;/p&gt;
&lt;p&gt;很容易发现，进行一次&lt;code&gt;bfs&lt;/code&gt;的时间复杂度为$O(n^2m^2)$，因为最多有$n^2m^2$种状态，此时最坏时间复杂度为$30^4$，即$810000$。再乘上$q=500$，就是$4,0500,0000$。再加上&lt;code&gt;bfs&lt;/code&gt;有较大的常数，不死就怪。&lt;/p&gt;
&lt;p&gt;但是显然$4,0500,0000$是跑不满的，因此只需优化一下常数，就能获得更好的成绩。&lt;/p&gt;
&lt;h2&gt;进行常数优化&lt;/h2&gt;
&lt;p&gt;仔细观察程序，不难发现，很多地方可以优化常数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;手动O3+Ofast&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;%:pragma GCC optimize(&quot;inline,Ofast&quot;,3)
或
#pragma GCC optimize(&quot;inline,Ofast&quot;,3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一定要加载头文件前面！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;避免初始化清空vis数组&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;显然vis数组很大，清空浪费时间。&lt;/p&gt;
&lt;p&gt;我们考虑维护当前在进行第几次&lt;code&gt;BFS&lt;/code&gt;。如果vis数组中的值等于当前次数，才认为这个状态出现过。同样，标记&lt;code&gt;vis&lt;/code&gt;时标记为当前次数。&lt;/p&gt;
&lt;p&gt;这样就可以不清空vis数组。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用快读快写。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;inline void read(int &amp;#x26;ret) {
    ret=0;
    register char c=getchar();
    while(!isdigit(c)) c=getchar();
    while(isdigit(c)) {ret=ret*10+(c-&apos;0&apos;);c=getchar();}
}

void __write(signed x) {
    if(!x) return;
    __write(x/10);
    putchar(&apos;0&apos;+x%10);
}

inline void write(const signed &amp;#x26;x,char e=&apos;\0&apos;) {
    if(x&gt;0) __write(x);
    else if(x==0) putchar(&apos;0&apos;);
    else {putchar(&apos;-&apos;);__write(-x);}
    if(e) putchar(e);
}

主程序中：
read(n);read(m);read(q);
write(bfs(),&apos;\n&apos;);
等等...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用指针来完成队列操作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;实际上，指针比数组下标访问更快。假设有一个&lt;code&gt;int&lt;/code&gt;数组&lt;code&gt;arr&lt;/code&gt;，那么我们可以定义一个指针&lt;code&gt;int *ptr = arr&lt;/code&gt;，此时&lt;code&gt;ptr&lt;/code&gt;指向&lt;code&gt;arr[0]&lt;/code&gt;，&lt;code&gt;ptr+1&lt;/code&gt;指向&lt;code&gt;arr[1]&lt;/code&gt;，以此类推。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;取值：
cout&amp;#x3C;&amp;#x3C;(*ptr)&amp;#x3C;&amp;#x3C;endl;

赋值：
(*ptr) = 25

假设ptr指向的是Status类型，对其step赋值和取值
ptr-&gt;size = 12
cout&amp;#x3C;&amp;#x3C;(ptr-&gt;size)&amp;#x3C;&amp;#x3C;endl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以将队列的首尾下标变成首尾指针。那么我们仍然可以使用&lt;code&gt;++h&lt;/code&gt;，&lt;code&gt;++t&lt;/code&gt;这种方式进行队列模拟。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;加register, inline&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;inline&lt;/code&gt;可以用在非递归函数的类型前，防止不必要的进栈操作。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;register&lt;/code&gt;可以用在局部变量的类型前，这样可以定义将变量存在寄存器中，加快访问速度。但是寄存器容量小，不宜使用太多register，否则系统会花时间将较早定义的元素扔出寄存器，放入内存，造成负优化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;修改dx, dy数组，防止搜索时单步回退&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们注意到，BFS扩展（转移）部分常数较大&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int dx[4]={0,1,-1,0};
int dy[4]={-1,0,0,1};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样定义的&lt;code&gt;dx,dy&lt;/code&gt;有个性质，即，dx和dy中分别取下标相加等于3的两个元素，相加必等于0。&lt;/p&gt;
&lt;p&gt;我们可以给&lt;code&gt;Status&lt;/code&gt;结构添加一个属性&lt;code&gt;tp&lt;/code&gt;，即，上次使用的&lt;code&gt;dx和dy&lt;/code&gt;中的哪个元素。&lt;/p&gt;
&lt;p&gt;下一次扩展时，若枚举的&lt;code&gt;i&lt;/code&gt;与上次的&lt;code&gt;tp&lt;/code&gt;相加得3，说明这样扩展必将退回到上一个状态。显然这个状态是没用的（判重时会&lt;code&gt;continue&lt;/code&gt;掉）&lt;/p&gt;
&lt;p&gt;对于初始化，只需初始化&lt;code&gt;tp=-1&lt;/code&gt;即可（因为没有任何&lt;code&gt;i&lt;/code&gt;会与&lt;code&gt;-1&lt;/code&gt;相加为3）&lt;/p&gt;
&lt;p&gt;最后的代码长这样&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [status_undefined]
// oj:     [luogu]
%:pragma GCC optimize(&quot;inline,Ofast&quot;,3)
#include&amp;#x3C;stdio.h&gt;
#include&amp;#x3C;ctype.h&gt;
#define int short // short卡常

int n,m,q;
int a[31][31];

struct Status{
    int ex,ey,sx,sy;
    signed step; // 步骤数等地方不可以使用short，可能会爆
    int tp;
    inline signed toInt() { 
        return (ex&amp;#x3C;&amp;#x3C;15)|(ey&amp;#x3C;&amp;#x3C;10)|(sx&amp;#x3C;&amp;#x3C;5)|sy;
    }
}que[810010];
int tx,ty;
Status *h,*t; //用指针
int vis[1048576];
int vis_t=0;

int dx[4]={0,1,-1,0};
int dy[4]={-1,0,0,1};

inline signed bfs() {
    ++vis_t;
    while(h!=t) {
        ++h;
        if(h-&gt;sx==tx &amp;#x26;&amp;#x26; h-&gt;sy==ty) {
            return h-&gt;step;
        }
        for(register int i=0;i&amp;#x3C;4;i++) {
            if(i+h-&gt;tp==3) continue; //相加等于3的优化
            register int ex2=h-&gt;ex+dx[i];
            register int ey2=h-&gt;ey+dy[i];
            if(ex2&amp;#x3C;1 || ex2&gt;n || ey2&amp;#x3C;1 || ey2&gt;m) continue;
            if(!a[ex2][ey2]) continue;
            ++t;
            t-&gt;ex=ex2;
            t-&gt;ey=ey2;
            t-&gt;sx=h-&gt;sx;
            t-&gt;sy=h-&gt;sy;
            if(ex2==h-&gt;sx &amp;#x26;&amp;#x26; ey2==h-&gt;sy) {
                t-&gt;sx=h-&gt;ex;
                t-&gt;sy=h-&gt;ey;
            }
            register signed _t=t-&gt;toInt();
            if(vis[_t]==vis_t) {--t;continue;}
            t-&gt;step=h-&gt;step+1;
            t-&gt;tp=i;
            vis[_t]=vis_t;
            if(t-&gt;sx==tx &amp;#x26;&amp;#x26; t-&gt;sy==ty) {
                return t-&gt;step;
            }
        }
    }
    return -1;
}

inline void read(int &amp;#x26;ret) {
    ret=0;
    register char c=getchar();
    while(!isdigit(c)) c=getchar();
    while(isdigit(c)) {ret=ret*10+(c-&apos;0&apos;);c=getchar();}
}

void __write(signed x) {
    if(!x) return;
    __write(x/10);
    putchar(&apos;0&apos;+x%10);
}

inline void write(const signed &amp;#x26;x,char e=&apos;\0&apos;) {
    if(x&gt;0) __write(x);
    else if(x==0) putchar(&apos;0&apos;);
    else {putchar(&apos;-&apos;);__write(-x);}
    if(e) putchar(e);
}

signed main() {
    read(n);read(m);read(q);
    
    for(int i=1;i&amp;#x3C;=n;i++) {
        register int *tt = a[i];
        for(int j=1;j&amp;#x3C;=m;j++) {
            ++tt;
            read(*tt);
        }
    }
    
    t=que+1;
    t-&gt;tp = -1;
    t-&gt;step = 0; // 无需每次初始化的东西放到循环外
    for(register int i=1;i&amp;#x3C;=q;i++) {
        h=que;
        t=que+1;
        read(t-&gt;ex);read(t-&gt;ey);read(t-&gt;sx);read(t-&gt;sy);
        read(tx);read(ty);
        write(bfs(),&apos;\n&apos;);
    }
}

&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[洛谷题解] CF339C Xenia and Weights</title><link>https://ak-ioi.com/blog/algo-solution/luogu-cf339c</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-cf339c</guid><pubDate>Thu, 19 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目链接：&lt;a href=&quot;https://www.luogu.org/problem/CF339C&quot;&gt;CF339C&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;这题已经有较为完善的题目翻译。&lt;a href=&quot;https://ji-suan-ke.blog.luogu.org/solution-cf339c&quot;&gt;在我之前的一篇题解&lt;/a&gt;思路是暴力搜索（复杂度是正确的），而这篇题解用的是动态规划的思路。&lt;/p&gt;
&lt;h2&gt;题目大意&lt;/h2&gt;
&lt;p&gt;你被允许使用$1..10$中的若干个数。&lt;/p&gt;
&lt;p&gt;一开始有两个数$a,b$，初始值均为$0$。&lt;/p&gt;
&lt;p&gt;你需要对这两个数交替操作（先操作哪个无关紧要），&lt;strong&gt;一共&lt;/strong&gt; $m$次。每次操作你需要将所操作的数加上 &lt;strong&gt;一个&lt;/strong&gt; 你 &lt;strong&gt;被允许使用&lt;/strong&gt; 的数。每次操作需要满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加上的数和你 &lt;strong&gt;上一次&lt;/strong&gt; 操作不同（对于第一次操作，没有限制）&lt;/li&gt;
&lt;li&gt;操作后被操作的数必须 &lt;strong&gt;严格大于&lt;/strong&gt; 另一个数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问你是否可能合法地进行$m$次操作。如果可能，需要构造出一种方法。&lt;/p&gt;
&lt;p&gt;$1 \leq m \leq 1000$&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;由于动态规划的路径记录实际上非常简单，我们暂且不考虑构造出一种方案，只考虑是否有可能。&lt;/p&gt;
&lt;h2&gt;状态设计&lt;/h2&gt;
&lt;p&gt;最容易想到的状态设计是：&lt;/p&gt;
&lt;p&gt;$dp[1..m][1..10\cdot m][1..10\cdot m]$，其中$dp[i][j][k][t]$表示已经操作$i$次，并且此时$a$的值为$j$，$b$的值为$k$，并且上一次操作加上的数是$t$时，是否有可能。&lt;/p&gt;
&lt;p&gt;然而根据题目的数据范围，要开的数组是：$dp[1..1000][1..10000][1..10000][1..10]$，显然不可以。&lt;/p&gt;
&lt;p&gt;我们注意到上面的状态设计中存在极多的冗余信息。我们发现，影响下一步决策的只有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;上一次&lt;/strong&gt; 操作时加上的数（因为这次就不能再用了）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上一次&lt;/strong&gt; 操作后，被操作数减去另一个数所得的值（本次操作加的数必须大于这个）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么只需将这两个作为状态即可。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一维：$0..n$，$i$表示在第$i$次 &lt;strong&gt;操作后&lt;/strong&gt;。&lt;strong&gt;取值范围分析：&lt;/strong&gt; 理论上需要$1..n$，而具体实现时，我们需要增加一个$0$，表示未进行任何操作。这样可以降低编程难度（或避免复制粘贴代码）&lt;/li&gt;
&lt;li&gt;第二维：$0..10$，本次操作加上的数（即，&lt;strong&gt;下次操作&lt;/strong&gt; 不能再用的数）。&lt;strong&gt;取值范围分析：&lt;/strong&gt; 加上的数取值范围是$1..10$，而在没有进行任何操作时，我们认为上次加上的数是$0$，表示没有任何一个数不能在下一次使用。&lt;/li&gt;
&lt;li&gt;第三维：$0..10$，本次操作后，被操作数减去另一个数的值（假如操作的是$a$，那么这个值就是$a-b$，否则是$b-a$）。&lt;strong&gt;下一次操作&lt;/strong&gt; 加上的数必须大于这个。&lt;strong&gt;取值范围分析：&lt;/strong&gt; 每次操作后，这一维的取值范围为$1..9$。特别地，如果第一次操作加上的是$10$，那么这一维能取到$10$。在没有进行任何操作时，我们令这一维为$0$，表示对下一次操作加上的数的最小值没有限制。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;状态描述：$dp[i][cant][gt]$&lt;/p&gt;
&lt;p&gt;我们在$dp$数组中用&lt;code&gt;0&lt;/code&gt;描述“不能做到”，&lt;code&gt;1&lt;/code&gt;描述“可以做到”。&lt;/p&gt;
&lt;p&gt;利用这种状态设计，我们甚至不用关心本次操作的是哪个数了。&lt;/p&gt;
&lt;h2&gt;初始化&lt;/h2&gt;
&lt;p&gt;没有进行任何操作的情况是 &lt;strong&gt;可能做到的&lt;/strong&gt;（显然）。&lt;/p&gt;
&lt;p&gt;没有任何操作时，令$cant = 0$表示下一次（第一次）没有不能取的值，令$gt = 0$表示对下一次加的数的最小大小没有限制。&lt;/p&gt;
&lt;p&gt;对于$i=0$的其他状态，不能做到。&lt;/p&gt;
&lt;p&gt;对于$i&gt;1$的其他状态，都是未知情况。由于$dp$是基于“或”操作的（即，能够转移到当前状态的状态只要有一个可能做到，当前状态就可以做到），因此初始化为$0$。&lt;/p&gt;
&lt;p&gt;初始化：&lt;/p&gt;
&lt;p&gt;$dp[0][0][0] = 0$&lt;/p&gt;
&lt;p&gt;其他情况都是赋值为$0$，如果开的是全局数组则会自动初始化为0，无需手动初始化。&lt;/p&gt;
&lt;h2&gt;状态转移&lt;/h2&gt;
&lt;p&gt;我们用$has[num]$表示你是否允许使用数字$num$。&lt;/p&gt;
&lt;p&gt;考虑状态$dp[i-1][cant][gt]$，那么该状态可以转移到$dp[i][num][num-gt]$，其中$num$满足$has[val]\ &amp;#x26;&amp;#x26;\ num\ =\not\ cant\ &amp;#x26;&amp;#x26;\ num &gt; gt$。&lt;/p&gt;
&lt;p&gt;这非常好理解。至于为什么转移到的是$num-gt$：&lt;/p&gt;
&lt;p&gt;假设第$i-1$操作的数是$a$，那么$gt = a-b$。&lt;/p&gt;
&lt;p&gt;第$i$次操作的数应该是$b$，则有：&lt;/p&gt;
&lt;p&gt;$gt_{new}$&lt;/p&gt;
&lt;p&gt;$= b_{new} - a_{new}$&lt;/p&gt;
&lt;p&gt;$= (b+num)-a$&lt;/p&gt;
&lt;p&gt;$= num + (b-a)$&lt;/p&gt;
&lt;p&gt;$= num - (a-b)$&lt;/p&gt;
&lt;p&gt;$= num - gt$&lt;/p&gt;
&lt;h2&gt;记录路径&lt;/h2&gt;
&lt;p&gt;普通的动态规划是很容易记录路径的。我们在更新一个状态时，可以顺便在这个状态中存储 &lt;strong&gt;转移到这个状态的前一个状态&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;最终$dp$完成时，由于答案并不关心最后一次操作完成后的$cant$和$gt$值，我们需要在$dp[n][0..10][0..10]$任意取出一个可以做到的值（即，$dp[n][0..10][0..10]=1$）。&lt;/p&gt;
&lt;p&gt;然后根据存储的“上一个状态”向前回溯，并重新计算出每次加上的数，放入一个数组。&lt;/p&gt;
&lt;p&gt;由于我们对于一次操作，有$gt_{new} = num - gt$，可以得出：&lt;/p&gt;
&lt;p&gt;如果$dp[i][cant_{new}][gt_{new}]$由$dp[i-1][cant][gt]$转移而来，那么第$i$次操作加上的数$put_i$就是$gt_{new} + gt$&lt;/p&gt;
&lt;p&gt;最后输出答案即可。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;程序与细节注释&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// status: [Accepted]
// oj:     [luogu]

#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;

int has[11]; // 是否允许使用某个数
int put[1001]; // 放入的数，用于最后回溯并输出答案的时候
int n; // 需要操作的次数

struct State {
    // 状态
    bool can;
    // 来历
    int p1;
    int p2;
    int p3;
}dp[1001][11][11];
//DP维度：
// 1. 填充长度
// 2. 不能填充的数字 (0:没有限制)
// 3. 至少填充的数字 (0:没有限制)

int main() {
    ios::sync_with_stdio(false);

    for(int i=1;i&amp;#x3C;=10;i++) {
        char c;
        cin&gt;&gt;c;
        has[i] = c-&apos;0&apos;;
    }

    cin&gt;&gt;n;

    // 初始化
    dp[0][0][0].can = 1;
    // 枚举操作次数
    for(int i=1;i&amp;#x3C;=n;i++) {
        // 针对 上一个状态：上一次加上的数
        for(int cant=0;cant&amp;#x3C;=10;cant++) {
            // 针对 上一个状态：上一次操作后，操作数减去另一个数的值
            for(int gt=0;gt&amp;#x3C;=10;gt++) {
                // 上一个状态 不能做到，不进行转移。
                if(!dp[i-1][cant][gt].can) continue;
                // 枚举大于gt的数用来加
                for(int num=gt+1;num&amp;#x3C;=10;num++) {
                    // 和上一次冲突，或本来就不能取
                    if(!has[num] || num==cant) continue;
                    // 此处使用引用来表示下一次可以转移到的状态，能够简化代码
                    State &amp;#x26;ret = dp[i][num][num-gt];
                    // 上一个状态有解，当前肯定有解
                    ret.can = 1;
                    // 记录状态的“来历”
                    ret.p1 = i-1;
                    ret.p2 = cant;
                    ret.p3 = gt;
                }
            }
        }
    }

    int c1 = 0, c2 = 0, c3 = 0;

    for(int i=0;i&amp;#x3C;=10;i++) {
        for(int j=0;j&amp;#x3C;=10;j++) {
            // 随便找一个有解的状态。
            if(dp[n][i][j].can) {
                c1 = n;
                c2 = i;
                c3 = j;
                break;
            }
        }
    }

    // 有解
    if(c1 || c2 || c3) {
        cout&amp;#x3C;&amp;#x3C;&quot;YES&quot;&amp;#x3C;&amp;#x3C;endl;
        // 向前回溯，存储答案
        while(c1&gt;0) {
            State &amp;#x26;curr = dp[c1][c2][c3];
            int n1 = curr.p1;
            int n2 = curr.p2;
            int n3 = curr.p3;
            // 依据：put[i] = gt_new +gt
            put[c1] = c3 + n3;
            // 向前跳到上一个状态
            c1=n1;c2=n2;c3=n3;
        }
        // 输出答案
        for(int i=1;i&amp;#x3C;=n;i++) {
            if(i&gt;1) cout&amp;#x3C;&amp;#x3C;&apos; &apos;;
            cout&amp;#x3C;&amp;#x3C;put[i];
        }
        cout&amp;#x3C;&amp;#x3C;endl;
    }
    // 无解
    else {
        cout&amp;#x3C;&amp;#x3C;&quot;NO&quot;&amp;#x3C;&amp;#x3C;endl;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;评测记录：&lt;a href=&quot;https://www.luogu.org/record/24108985&quot;&gt;R24108985&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>技术手段下载音频网站内容（初级）</title><link>https://ak-ioi.com/blog/dirty-hacks/fuck-downloads-primary</link><guid isPermaLink="true">https://ak-ioi.com/blog/dirty-hacks/fuck-downloads-primary</guid><description>众所周知，现在很多音频网站以下载客户端并付费作为下载资源的先决条件（某些提供伴奏的网站甚至禁止下载）。本文讲述的奇技淫巧用于“强行”下载这些资源。</description><pubDate>Sun, 30 Jun 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import F from &apos;@/components/mdx/formatting&apos;&lt;/p&gt;
&lt;p&gt;本文方法的先决条件是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在桌面端设备上操作；&lt;/li&gt;
&lt;li&gt;目标平台必须是网站，不是独立客户端应用；&lt;/li&gt;
&lt;li&gt;目标资源只是下载受限，但可以在网站上播放；（当然，不排除遇到“前端开发一天从入门到精通”的神人网站的可能性）&lt;/li&gt;
&lt;li&gt;使用现代的浏览器，例如 Chromium、Firefox 等。老旧的浏览器，如 Internet Explorer，操作方法可能非常不一样。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&apos;警告&apos;&gt;
本文介绍的方法仅应当被用于非商业的教育或测试目的。若跟随本文章指导操作或者滥用通过此手段获得的资源，读者须自担责任。
&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&apos;时效性复核状态&apos;&gt;
复核或修订时间：2020年12月3日
&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h2&gt;理论基础&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;……试图让数字文件无法复制，就如同试图让水失去湿润的特性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;音频或视频文件的在线播放与下载在技术上等同&lt;/h3&gt;
&lt;p&gt;显然，要播放音频或视频，网站必须将相关内容发送给用户，而下载的原理与其完全相同。允许在线播放而禁止下载的行为，本质上是利用用户对技术的无知以对其加以限制。&lt;/p&gt;
&lt;p&gt;没有任何法律规定不得擅自改装浏览器，因此，被改装的浏览器完全可以在在线播放的同时直接提供下载选项。&lt;/p&gt;
&lt;h3&gt;音乐网站上不值得使用分片加载&lt;/h3&gt;
&lt;p&gt;分片加载等技巧用在视频网站上时，能一定程度上增加技术手段下载的难度（但仍然可行），并且改进性能。&lt;/p&gt;
&lt;p&gt;视频网站上的分片一般 2-8 分钟为一片（此大小不会影响视频播放效果），这时能获得最好的性能。然而，歌曲和大多数音乐的大小常常不及一部电影的 1%（此处以 &lt;code&gt;.flac&lt;/code&gt; 无损格式为依据），使用分片加载会增加维护难度，增大网络流量，并完美地损失性能。&lt;/p&gt;
&lt;p&gt;正因如此，主流音乐网站仍使用传统的方式加载音频。&lt;/p&gt;
&lt;h3&gt;音乐网站不值得用奇怪的技巧阻止下载&lt;/h3&gt;
&lt;p&gt;音乐网站用奇怪的技巧阻止下载，通常开销（指性能和程序复杂度）极大而效果不明显。而即使音乐网站真的成功地阻止了技术手段下载，通过录屏的方式（仅录制系统声音）获取音频的难度仍然奇低无比。&lt;/p&gt;
&lt;p&gt;录屏对音频质量的损伤可以忽略（前提是系统音量值足够大，但是可以静音），而傻子都知道，~~看一个四分钟的广告~~ 多享受四分钟的音乐来换取自己喜欢的歌是值得的。&lt;/p&gt;
&lt;h2&gt;初级方法&lt;/h2&gt;
&lt;p&gt;有了上面的思路后，不难发现，只要能使用一种合适的方式分析音频网站的程序，就可以绕过形同虚设的限制，获取通用的、未经加密的音频文件。&lt;/p&gt;
&lt;p&gt;所谓“初级方法”，就是利用已经制作好的网页工具（如浏览器扩展和油猴脚本）或浏览器内置工具对网页进行分析并获取文件的方法。&lt;/p&gt;
&lt;h2&gt;使用预先制作的工具&lt;/h2&gt;
&lt;h3&gt;通用的资源下载扩展程序&lt;/h3&gt;
&lt;p&gt;通用的资源下载扩展可以自动分析页面上的音频、视频等资源，并给出下载链接。你可以在浏览器的扩展程序商店找到这些东西。&lt;/p&gt;
&lt;h3&gt;针对性工具&lt;/h3&gt;
&lt;p&gt;针对性工具是为特定的网站设计的。这些工具通常更加强大，但收到律师函的可能性也更大。&lt;/p&gt;
&lt;p&gt;在 GreasyFork 上可以找到不少这样的针对性脚本。&lt;a href=&quot;https://greasyfork.org/zh-CN/scripts?q=%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90+%E4%B8%8B%E8%BD%BD&quot;&gt;【示例】&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;使用浏览器内置工具&lt;/h2&gt;
&lt;p&gt;然而，上述只是特例。我们需要更加优秀的方法来解决大部分网站的下载问题。&lt;/p&gt;
&lt;p&gt;（当然，通用方法的演示也需要使用一个特例。这里用酷我音乐和QQ音乐演示）&lt;/p&gt;
&lt;h3&gt;演示一（别跳过）&lt;/h3&gt;
&lt;p&gt;举例：十字诀 - 阿悄（酷我音乐）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1&lt;/strong&gt; 打开播放页面 -&gt; &lt;code&gt;http://www.kuwo.cn/play_detail/6877870&lt;/code&gt;（非https链接，请自行复制打开），尝试播放。保证音乐是你想要的（虽然实际上技术手段下载不用花费很久）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2&lt;/strong&gt; 我们尝试用网页上的下载键。我们发现不出我们所料，大多数音乐网站下载都要求客户端。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/hero.Db37DgYl_G1PEu.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;F.ImgCap&gt;酷我音乐要求使用客户端下载&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;注：这里提到了一个【已安装酷我音乐】的按钮。这里利用的是自定义网络协议。将会在进阶的技术手段文章中讲解。当然会不会有进阶文章还不知道。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3&lt;/strong&gt; 在音频最终能播放的页面中按下键盘上的F12（该操作针对Chrome以及大多数主流浏览器）。如果F12不管用，那么可以在菜单中找到“查看器”或“开发者工具”&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/fuck-downloads-primary-2.CCvy4c_T_Z11GFn6.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;F.ImgCap&gt;查看器&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4&lt;/strong&gt; 在开发者工具中切换到Network（网络，有的浏览器已经为开发者工具做了中文版）栏。刷新页面。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5&lt;/strong&gt; 此时应该可以看到一些开发者工具窗口中涌现的网络访问记录。每一次网页从网络上加载资源的操作都会被记载在这里（同时，网页本身的加载也记在了其中。图片见第七步）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6&lt;/strong&gt; 再次操作网页，使音乐播放起来（但是不要关闭开发者工具）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7&lt;/strong&gt; 点击开发者工具表格中的Type（类型）列，将内容以类型排序。以方便查找。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/fuck-downloads-primary-3.DJL7GyRj_Z2smRSI.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;F.ImgCap&gt;进行排序&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8&lt;/strong&gt; 找到类型为media的项目（部分网站中类型可能是xhr，此时需要结合文件名进一步判断）。右键其中一个，点击Open in new tab（在新标签页中打开）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/fuck-downloads-primary-4.CrmgonZj_Zo4k1w.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;F.ImgCap&gt;在新标签页中打开&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;9&lt;/strong&gt; 打开后，有可能浏览器会要求直接下载。此时，要确认文件名后缀是否是音频的后缀（&lt;code&gt;.mp3&lt;/code&gt; &lt;code&gt;.aac&lt;/code&gt; &lt;code&gt;.ogg&lt;/code&gt; &lt;code&gt;.flac&lt;/code&gt; &lt;code&gt;.wav&lt;/code&gt; &lt;code&gt;.m4a&lt;/code&gt; ...；遇到生僻后缀建议网络搜索确定其含义），如果符合要求则确认下载。如果浏览器给出了预览功能，请试听。如果符合要求，按&lt;code&gt;Ctrl+S&lt;/code&gt;进行保存。如果不符合要求，那么请回到开发者工具，尝试其他media或xhr类型的文件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;10&lt;/strong&gt; 下载后，检查下载的文件是否是想要的。&lt;/p&gt;
&lt;h3&gt;演示二&lt;/h3&gt;
&lt;p&gt;QQ音乐的演示主要用于解决一个误区，就是要在哪个页面使用F12&lt;/p&gt;
&lt;p&gt;示例： 蜀绣 - 苏曦汐&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1&lt;/strong&gt; 打开歌曲页面，尝试播放 -&gt; https://y.qq.com/n/yqq/song/004AQQWC4RBEHL.html&lt;/p&gt;
&lt;p&gt;（实际上QQ音乐并不是在歌曲描述页上播放音乐的，这就涉及到两个页面。用来播放的页面是 &lt;code&gt;https://y.qq.com/portal/player.html&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/fuck-downloads-primary-5.C07y_JAt_Z1Gwzr6.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;F.ImgCap&gt;QQ 音乐上的播放器&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2&lt;/strong&gt; 下载按钮就不尝试点了，因为可以猜到结果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3&lt;/strong&gt; 划重点！这里有两个网页，要在哪个上进行F12操作呢？（当然是&lt;code&gt;https://y.qq.com/portal/player.html&lt;/code&gt;，因为这才是音频最终能够播放的页面）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4&lt;/strong&gt; 自觉切换到Network。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5&lt;/strong&gt; 刷新页面。此时播放列表中刚才的歌曲还在（如果没有了，那么请再点击&lt;code&gt;https://y.qq.com/n/yqq/song/0a04AQQWC4RBEHL.html&lt;/code&gt;上的“播放”按钮，此时，能够切换到播放器页面，不造成刷新）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/fuck-downloads-primary-6.CzLzIr4i_ZpIBXX.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;F.ImgCap&gt;检索后的播放页面&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6&lt;/strong&gt; 使要下载的音乐播放起来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7&lt;/strong&gt; 排序&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8&lt;/strong&gt; 找出media。使用Open in new tab的方式打开。可以注意到，这里出现较多的项目。解决方法，就是，只尝试同名文件中的最后一个，从最后一组同名文件开始尝试（原理此处不赘述）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/fuck-downloads-primary-7.DipnpzNa_1dMES2.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;F.ImgCap&gt;大量的 Media 资源&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;9&lt;/strong&gt; 准备保存&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/fuck-downloads-8.DOnAfX9g_1SBXfM.webp&quot; alt=&quot;&quot;&gt;
&amp;#x3C;F.ImgCap&gt;新建下载任务&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;10&lt;/strong&gt; 检验文件是否正确。第8步列表中第一个文件名（C400003mAan...）是不能播放的。尝试第二个（C40003iMUS...），发现是正确的。至此，下载完成。&lt;/p&gt;
&lt;p&gt;目前，大多数主流视频网站都已经分片加载，防止了用这个技术下载内容（当然，对于偏远的视频网站，用音频网站的方法可能是有效的）。但是，根据理论基础，一定会有办法解决下载问题。这个办法将在进阶内容中讲解。&lt;/p&gt;</content:encoded><h:img src="/_astro/hero.Db37DgYl.png"/><enclosure url="/_astro/hero.Db37DgYl.png"/></item><item><title>[洛谷题解] P1078 文化之旅</title><link>https://ak-ioi.com/blog/algo-solution/luogu-p1078</link><guid isPermaLink="true">https://ak-ioi.com/blog/algo-solution/luogu-p1078</guid><description>NOIp 普及组 2012 的假题。可以使用暴搜通过。</description><pubDate>Fri, 16 Nov 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import F from &apos;@/components/mdx/formatting&apos;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;来源：洛谷
编号：P1078
地址：https://www.luogu.org/problemnew/show/P1078
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目描述&lt;/h2&gt;
&lt;p&gt;NOIP普及组 2012 T4&lt;/p&gt;
&lt;p&gt;比较复杂的最短路题目&lt;/p&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;p&gt;听说这道题是错题，数据水得不要不要的？那我可要拿出骗分大法了！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;暴力搜索！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先，可以明确，由于使者太懒，任何国家都可以被视为排斥相同文化的外来人。&lt;/p&gt;
&lt;p&gt;这道题刚开始看起来像是最短路（也确实是）。最短路的算法大家经常谈论的都涉及到点上标签的迭代，但是最短路实际上有种贪心做法，就是 UCS（Uniform cost search, 亦称最短路Label Setting），思想同BFS，只是使用优先队列，总是将离起点最近的节点排在队列的前面，这样，总是可以保证前沿的节点到起点的距离相对一致。&lt;/p&gt;
&lt;p&gt;但是Label Setting充其量就是暴力搜索。我们用结构体&lt;code&gt;_entry&lt;/code&gt;来存储搜索状态，包含到起点的距离、当前所在国家和一个 STL set——已学习的文化（这当然是最暴力的做法）。每当扩展一个在优先队列中的节点时，要检查其相邻节点的文化与已学习文化的set是否相容。&lt;/p&gt;
&lt;p&gt;初始状态，距离为0，当前位置为起点，已学习的文化只有一个——起点的文化。&lt;/p&gt;
&lt;p&gt;当搜索算法找到终点时，返回距离即可。如果队列空了还没找到，显然就返回-1.&lt;/p&gt;
&lt;h2&gt;以下是坑点（很重要）&lt;/h2&gt;
&lt;h3&gt;36分——小于号重载错误&lt;/h3&gt;
&lt;p&gt;第一次提交36分纯属忘记了优先队列会将大的元素排在前面，因此将&lt;code&gt;_entry&lt;/code&gt;的小于号直接定义为离起点距离的小于号（应该改为大于等于，这样距离小的&lt;code&gt;entry&lt;/code&gt;会排在前面）。&lt;/p&gt;
&lt;h3&gt;44分——在找到终点时就结束搜索&lt;/h3&gt;
&lt;p&gt;Label Setting不能在找到终点时就结束搜索。想象有两个节点P, Q，搜索后距离起点的距离分别为33, 34，两个节点均为下一步就能到达终点，P到达终点长度为10，Q为1.&lt;/p&gt;
&lt;p&gt;根据Label Setting的规则，P在Q之前被展开，因此找到终点，返回最短路为43。但是显然从Q到终点的总距离只有35。显然错了。&lt;/p&gt;
&lt;p&gt;正确的做法应该是在终点即将离开优先队列时返回最短路。&lt;/p&gt;
&lt;h3&gt;92分——被10号数据卡到&lt;/h3&gt;
&lt;p&gt;事实证明其他数据都很水，只有10号数据无解而且能诱导搜索算法运行很长一段时间。&lt;/p&gt;
&lt;p&gt;方法：骗分。当出队操作执行超过800次时强制结束搜索，返回-1.&lt;/p&gt;
&lt;h2&gt;AC代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include&amp;#x3C;bits/stdc++.h&gt;
using namespace std;
typedef long long ll;

ll r[101][101]; //文化的排斥关系 r[i][j]
struct _country{
    ll r; //文化
    vector&amp;#x3C;pair&amp;#x3C;ll,ll&gt; &gt; n; //通往其他国家的路线
}c[101];
ll n,m,k,s,t; //如题
struct _entry{
    ll c; //国家
    ll l; //路径长度
    set&amp;#x3C;ll&gt; study; //已学习
    _entry(){
        c=-1;l=0;
    }
    _entry(ll _c,ll _l,ll _i){
        c=_c;l=_l;
        study.insert(_i);
    }
    bool operator&amp;#x3C;(const _entry &amp;#x26;other) const {
        return l&gt;=other.l;
    }
};
priority_queue&amp;#x3C;_entry&gt; que;

ll checkIfError(set&amp;#x3C;ll&gt; studies,ll c) { //检查已学习数组是否与新文化冲突
    for(ll i=1;i&amp;#x3C;=k;i++)
    {
        if(r[c][i] &amp;#x26;&amp;#x26; studies.count(i)) return true;
    }
    return false;
}

ll search() {
    ll elasped=0;
    que.push(_entry(s,0,c[s].r)); //初始
    while(!que.empty()) {
        _country curr=c[que.top().c]; //出发点国家节点
        _entry ent=que.top(); //出发点的Entry信息
        elasped++;
        if(elasped&gt;800) return -1;
        if(ent.c==t) {
            return ent.l;
        }
        que.pop();
        for(ll i=0;i&amp;#x3C;curr.n.size();i++) {
            _entry tmp=ent;
            tmp.l+=curr.n[i].second;
            tmp.c=curr.n[i].first;
            //cout&amp;#x3C;&amp;#x3C;&quot;From &quot;&amp;#x3C;&amp;#x3C;ent.c&amp;#x3C;&amp;#x3C;&quot; to &quot;&amp;#x3C;&amp;#x3C;tmp.c&amp;#x3C;&amp;#x3C;endl;
            if(checkIfError(ent.study,c[tmp.c].r)) continue; //判断冲突
            tmp.study.insert(c[tmp.c].r);
            que.push(tmp);
        }
    }
    return -1;
}

int main()
{
    cin&gt;&gt;n&gt;&gt;k&gt;&gt;m&gt;&gt;s&gt;&gt;t;
    for(ll i=1;i&amp;#x3C;=n;i++) {
        cin&gt;&gt;c[i].r; //输入文化
    }
    for(ll i=1;i&amp;#x3C;=k;i++) {
        for(ll j=1;j&amp;#x3C;=k;j++) {
            cin&gt;&gt;r[i][j];
            if(i==j) r[i][j]=1; //不能学习多次，即所有文化排斥自己
        }
    }
    for(ll i=1;i&amp;#x3C;=m;i++) {
        ll x,y,z;
        cin&gt;&gt;x&gt;&gt;y&gt;&gt;z;
        c[x].n.push_back(make_pair(y,z)); //输入路径
        c[y].n.push_back(make_pair(x,z));
    }
    cout&amp;#x3C;&amp;#x3C;search()&amp;#x3C;&amp;#x3C;endl;;
}

&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>[参赛感受] NOIP2018普及AK记</title><link>https://ak-ioi.com/blog/diary/i-ak-noip-2018</link><guid isPermaLink="true">https://ak-ioi.com/blog/diary/i-ak-noip-2018</guid><description>第一次NOIP... 甚至AK</description><pubDate>Sat, 10 Nov 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import F from &apos;@/components/mdx/formatting&apos;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Note title=&quot;备注&quot;&gt;
本文只是日记，并没有题目和解析。绝对是赛后写的。
&amp;#x3C;/F.Note&gt;&lt;/p&gt;
&lt;h2&gt;赛前&lt;/h2&gt;
&lt;p&gt;听说文件目录要求很严格，我今天早上在家练习断网刷题，用虚拟机写代码时不小心按到重启快捷键...
然后做完提交到OJ，听取WA声一片。
因此我已经对退役做好心理准备了。&lt;/p&gt;
&lt;p&gt;今天下午，我爸爸一定要我骑自行车去比赛场地，结果到了场地就要累死的节奏，还好有时间缓一缓。&lt;/p&gt;
&lt;p&gt;问了一下同学准备得怎么样（杭州观成中学2017年级，最强大的打酱油团），可是由于装蒻是信奥竞赛生的本分，根本问不出来真实情况。好吧，就把心态放轻松点，当观成打酱油团都会爆零吧。&lt;/p&gt;
&lt;p&gt;下午两点，我们就被push到了比赛场地门口的队列中。谁知队列阻塞，连续十五分钟都没pop。于是，我趁着时间充足，&lt;strong&gt;开启手机飞行模式和勿扰模式，全盘静音，然后关掉了手机&lt;/strong&gt;。眼看着内存就要溢出了，队列终于以一秒10个元素的速度开始pop。经过一分钟，我们终于到了考场。&lt;/p&gt;
&lt;p&gt;考场果然比照片中还壮观。这次比赛只有两个考场，一个是普通机房，另外一个就是学军的体育场，容纳了~~600多~~720人。要是能被分到普通机房，那得RP非常高了。座位并没有想象中那么一望无际，但是整整齐齐，每个选手都有一个“隔间”~~，能够极好地防止抄♂袭~~。&lt;/p&gt;
&lt;p&gt;比赛开始前五分钟，监考老师宣读了竞赛规则和注意事项。随后，猝不及防地，随着监考老师一声令下，比赛开始了。&lt;/p&gt;
&lt;h2&gt;比赛时&lt;/h2&gt;
&lt;p&gt;老师在屏幕上公布了试题的解密密码。这个密码真的是难猜啊... 是哪个聪明绝顶的出题人想出来的这种密码？我真想知道一下（我并没有要揍他的意思）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;junior: &amp;#x26;GaiGeKaiFang(40)Nian
hint: 改革开放40年全拼首字母大写，密码由&amp;#x26;开头，数字40用英文括号括起。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我并不是那种爱吃辣椒的人，所以一开始还是虐前两题。打开第一题一看：“标题统计”&lt;/p&gt;
&lt;p&gt;What, 字符串查找？&lt;/p&gt;
&lt;p&gt;接着往下看，竟然是统计标题里有多少字母和数字（卧槽，这么水？）。题目曰：“标题包含大小写字母、数字、空格和换行符”，输入格式中有曰：“一行”，所以&lt;code&gt;getline(cin,s)&lt;/code&gt;估计就行了。&lt;/p&gt;
&lt;p&gt;第二题！&lt;/p&gt;
&lt;p&gt;看完，发现数据范围一脸可暴力的样子。太棒了！于是，我只花了40分钟就干掉了前两题，那时候我心情要多好有多好。&lt;/p&gt;
&lt;p&gt;第三题！&lt;/p&gt;
&lt;p&gt;题目为有N个人要从A地到B地。给定N个人到A地路边等待的时间，和摆渡车从A到B再回来所需的时间（只有一辆，但是容量无穷大）。问如何安排发车时间使所有人等待时间最小。&lt;/p&gt;
&lt;p&gt;我一开始误解为“车一旦回到A地就必须立即出发”，那不是模拟吗？简单！暴力！等到程序写好，才发现不对，并没有那么简单。我一度想要弃疗，结果一看第四题，卧槽，更恶心... 算了，还是先做第三题吧。&lt;/p&gt;
&lt;p&gt;一脸茫然之后我开始瞎写动态规划。我看到路边等待的时间可大至&lt;code&gt;4 × 10^6&lt;/code&gt;，显然用这个做子问题不合适。我将状态改为两个&lt;code&gt;0~N&lt;/code&gt;级别的数据，过了两个小的样例，极限样例数据却自测失败。仔细一想，算法不对。于是改进代码。这下好了，小样例都萎了，更别说大样例。由于要优化并提前DP状态，我又把数组范围改为从1开始，花费不少精力。时间飞速，转眼间，半小时过去了，一小时过去了，90分钟过去了，小样例答案错了又对，对了又错，大样例的答案却似二分查找——不停地逼近正确结果，却永远无法到达。又过了一会儿，我竟大胆地将状态改写为一个&lt;code&gt;1~(N+1)&lt;/code&gt;的数据。我多次想要放弃，但是一看到第四题，想到要重新开始，我又有了那么一点点做第三题的信心和决心。&lt;/p&gt;
&lt;p&gt;监考老师似乎也知道我们要嗝屁了，给我们发面包。然而我并没有心情吃。&lt;/p&gt;
&lt;p&gt;眼看着第四题就要没有时间了，在羞愧、恼怒、惋惜、焦急和迫切想要砸电脑的欲望之中，我突然心血来潮，删掉之前写的核心代码，又调试了好长一段时间。大样例的答案是13490，你可知道，当我终于看到程序输出了13458时，我是何等喜悦！我立刻振奋精神，遍历整个代码，发现有一处漏写，立即加上，程序就在0.51秒的时间内得出了正确答案——13490！&lt;/p&gt;
&lt;p&gt;哇，太棒了！成功了！成功了！1 AK 101 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!&lt;/p&gt;
&lt;p&gt;可是我的程序总还不那么令人放心，因为毕竟耗时0.51秒，就怕奇葩测试数据一出来就挂掉了。毕竟，我的子问题有两个参数，一个&lt;code&gt;1~(N+1)&lt;/code&gt;级别的参数，还有一个时间级参数，范围不明（我并没有理论计算过）。其中还有一层人数级的循环，该循环内还有一级人数级循环，用于累加。那累加的地方来个前缀和如何？这样最坏情况刚好卡到一秒的时限，而事实数据一定没有我推出的理论情况这么坏（因为参数二我认为它是时间级参数，而没有严谨地计算范围，实际应该不是），于是，求出正确答案的时间瞬间变成了0.13秒，翻到卷头表格一看，时限2秒，那肯定可以过了（但是万一最坏情况真的有我想得那么坏肿么办）。&lt;/p&gt;
&lt;p&gt;于是我又仔细检查了一遍第三题代码，发现了一个天大的事情——差点爆零。&lt;code&gt;freopen()&lt;/code&gt;文件读写是&lt;code&gt;cstdio&lt;/code&gt;中的东西，可是我在输出时只引用了&lt;code&gt;iostream&lt;/code&gt;（听说万能引用头文件&lt;code&gt;bits/stdc++.h&lt;/code&gt;有毒，会出现玄学错误，不敢用）。Windows版引用&lt;code&gt;iostream&lt;/code&gt;时会自动引用&lt;code&gt;cstdio&lt;/code&gt;，所以调试没出错，但是到了Linux评测机上，那可就没有禁赛一年就不错了。&lt;/p&gt;
&lt;p&gt;第四题，我认为无可优化，写完暴力就逃（骗分20&lt;/p&gt;
&lt;p&gt;谁知... 比赛结束前1分钟，~~我突然想到一个聪明绝顶地方法，可是已经来不及了（能不能续命，要是续命搞不好就AK了）~~ 该方法后来被我证明是错的。&lt;/p&gt;
&lt;p&gt;比赛结束前一分钟就没有机会再写代码了，因为这时要清理掉选手文件夹中的无关文件，然后坐以待毙。&lt;/p&gt;
&lt;h2&gt;赛后&lt;/h2&gt;
&lt;p&gt;比赛结束后，我整理东西时，突然听到旁边“这次比赛真是太简单了”。难道我要退役了？&lt;/p&gt;
&lt;p&gt;赛后我去如厕，听到学军的大佬说“第一题怕是要萎”。啥？我也萎了？还是学军同学真的基础不扎实？&lt;/p&gt;
&lt;p&gt;后来我和rzmID谈论，第一题的疑虑就消除了。他做出了前两道，第三道0分，第四道估计32。我更加放心了一点。&lt;/p&gt;
&lt;p&gt;晚上，杭二中老师发微信朋友圈，称这是“历史上最难的NOIP试卷，第三题和第四题基本不可兼得”。当我告诉他我做出第三题时，他都觉得难以置信。&lt;/p&gt;
&lt;p&gt;这次一等奖分数线多少？怕是到不了二百五。我终于知道自己可以免于退役了（而且成绩还不错）。&lt;/p&gt;
&lt;h2&gt;坎坷的第四题&lt;/h2&gt;
&lt;p&gt;三天后的晚自习...
Exiler: 你AK了！（来自教练在洛谷用山寨数据进行的评测）&lt;/p&gt;
&lt;p&gt;What？？？&lt;/p&gt;
&lt;p&gt;不是说好的骗分32吗？于是我表示质疑，问家长，家长说第四题正解就是暴力？&lt;/p&gt;
&lt;p&gt;评测机配置真的如此之高？于是第二天我回忆题目...&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/5beea49087232.COzGvQT6_Z1NTPyN.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;事实证明炸不掉。应该我的程序只有极端数据——树是线性时才会爆，那就是96分。哪来的100？&lt;/p&gt;
&lt;p&gt;结果发现一件严重的事情。。。我的程序当一个节点只有左或者只有右时会立即退出。歪打正着？没有办法，反正I AK NOIP.&lt;/p&gt;
&lt;h2&gt;看到成绩&lt;/h2&gt;
&lt;p&gt;I AK ~~IOI~~ NOIP（第四题歪打正着，没错）&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Muted&gt;摘要图片来自洛谷（有魔改），未经明确授权。&amp;#x3C;/F.Muted&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/hero.Brjt1mIC.png"/><enclosure url="/_astro/hero.Brjt1mIC.png"/></item><item><title>[服务器软件] RojExplorer 网盘改进版</title><link>https://ak-ioi.com/blog/publishing/rojexplorer-improved</link><guid isPermaLink="true">https://ak-ioi.com/blog/publishing/rojexplorer-improved</guid><description>强大的自托管网盘、文件分享服务和在线代码编辑器。</description><pubDate>Sun, 26 Aug 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import F from &apos;@/components/mdx/formatting&apos;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;F.Caution title=&apos;警告&apos;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;切 勿 使 用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;更加深入的测试与审计表明，此项目原有代码的安全措施和代码质量有很大问题。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用于防止 XSS 攻击的转义机制非常复杂、不一致并且四处散落。重复转义时有发生，所以注入漏洞应该也不会少。该项目的原作者在使用 JavaScript 层的模板引擎时，甚至「知道自己在做什么」，在全局禁用了默认开启的自动转义，即使该引擎提供了按需插入未经转义的 HTML 的功能。显然，我审计到并且修复过的那些注入漏洞表明他们不知道自己在做什么。&lt;/li&gt;
&lt;li&gt;按照该项目一开始的设计，系统甚至有意允许某些未经过滤 JavaScript 和 HTML 在前端被执行（网页预览和轻应用快捷方式功能）。&lt;/li&gt;
&lt;li&gt;用于生成外链（可以链接到系统上任意能被读取的文件，不仅仅是网盘中所存的文件）的机制依赖于采用不安全随机数生成的密钥和一个甚至没有 padding 的随意手搓的加密算法。&lt;/li&gt;
&lt;li&gt;后端的用户权限机制高度状态化（Stateful）且非常不一致，有很强的局限性。某些比较复杂的功能逻辑中，甚至为了规避这种局限性而有意禁用了整个权限检查机制（当然也是通过一种高度状态化的方式），导致了某些私有文件泄露漏洞。&lt;/li&gt;
&lt;li&gt;代码库中充满了随意拼凑的逻辑，完全没有严谨的测试和缜密的逻辑思维。如果我们知道一个文件名以 &lt;code&gt;.oexe&lt;/code&gt; 后缀结尾，如何移除这个后缀呢？&lt;code&gt;filename.replace(&apos;.oexe&apos;, &apos;&apos;)&lt;/code&gt;！没错，原来的项目里就是这么写的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;KodCloud 官方的 KodExplorer 和 Kodbox 也好不到哪去！尚且不论代码质量是否过关以及各种漏洞是否充分修复（剧透：KodExplorer 废弃前的最后一个版本仍未修复许多我发现了的安全漏洞），它们还采用了令人发指的代码混淆和前端后门（我可以确认至少 2025 年 8 月时仍在采用）剥夺用户的软件自由、隐私和安全，却公然通过使用 GPLv3 许可协议欺骗不较真的用户，使他们相信这是一个自由且开源的软件。凭直觉而言，KodCloud 团队似乎从来都不在意用户的安全。&lt;/p&gt;
&lt;p&gt;RojExplorer Improved 已经在过去的更新中修复软件中的了大量安全漏洞，但继续测试永远都能发现新的。大部分漏洞不算致命，但如果攻击者愿意花精力小心地挖掘，仍能造成敏感信息泄露甚至服务器沦陷。&lt;/p&gt;
&lt;p&gt;简而言之——我不会再维护这个了，并且任何使用这个软件的人都后果自负。并且，如果你不死心，我可以确认我目前放出的最新版本仍然有几个具有实际危害性且未修复的安全漏洞——它们中的一些已经深植于整个软件的架构之中，难以在不对代码进行大规模重构的情况下修复。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;/F.Caution&gt;&lt;/p&gt;
&lt;p&gt;这个 RojExplorer 似乎是 &lt;a href=&quot;https://github.com/kalcaddle/KodExplorer&quot;&gt;KodExplorer&lt;/a&gt; v4.25 的前身或者分支版，但是具体是哪种情况现在已经不太可考（我找到的文件由 CSDN 用户 &lt;a href=&quot;https://my.csdn.net/jingsiyubeigao&quot;&gt;@jingsiyubeigao&lt;/a&gt; 提供，&lt;a href=&quot;https://download.csdn.net/download/jingsiyubeigao/10260500&quot;&gt;原链接在此&lt;/a&gt;）。此处提供的版本支持 KodExplorer 高级版所对应的功能，即支持无限数量的用户、群组，且可以自定义版权，但是不具有文件历史等企业版扩展功能。你现在看到的 RojExplorer Improved 是在 RojExplorer 基础上继续改进得到的。&lt;/p&gt;
&lt;p&gt;KodExplorer 尽管是商业软件，没有提供前端的原始代码，且在 PHP 代码中采用了恶毒的混淆手段和后门程序防止「破解」，但其代码采用 &lt;a href=&quot;https://www.gnu.org/licenses/gpl-3.0.en.html&quot;&gt;GPLv3 许可协议&lt;/a&gt;发布（在 v4.25 版本时就已经使用）。这一许可协议允许用户出于任何目的运行、修改和再发布软件，并且明确允许用户绕过用于「保护版权」的技术措施，因此，无论「破解」高级版功能，还是进一步进行开发，都是合理行使用户的权利，不侵犯版权。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是，无论如何，RojExplorer Improved 不属于 KodExplorer 的公司，没有人有义务稳定地维护该软件。使用此软件造成的任何后果（包括工作延误、数据丢失、服务器遭到入侵等）在法律允许的最大限度上由使用者承担责任。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://wcl.sparkslab.art/index.php?share/folder&amp;#x26;user=4&amp;#x26;sid=ngTypvhX&quot;&gt;【下载链接】&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;升级注意事项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;如果你的系统最初是在 v454h 及之前的版本上部署的，现在建议编辑 &lt;code&gt;data/system/system_setting.php&lt;/code&gt; 文件，将其中的 &lt;code&gt;systemPassword&lt;/code&gt; 替换为一个新生成的随机字符串（至少 64 字符），以确保更好的安全性。&lt;/li&gt;
&lt;li&gt;跨越 v456b 版本，建议清除缓存（按钮位于超级管理员的系统设置页面）。&lt;/li&gt;
&lt;li&gt;跨越 v454g 版本，建议在升级前删除 lightapp 文件夹，因为新的版本对此处进行了清理。&lt;/li&gt;
&lt;li&gt;跨越 v453a 版本，可在&lt;strong&gt;升级后&lt;/strong&gt;删除 &lt;code&gt;config/setting.php&lt;/code&gt;。该文件的内容在新版本中已经移至 &lt;code&gt;system/setting.php&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;图片预览&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/rojimp-pre-1.TLCQrwZ0_UriBa.webp&quot; alt=&quot;screenshot&quot;&gt;
&amp;#x3C;F.ImgCap&gt;符合 Windows 操作习惯的文件管理器。&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/rojimp-pre-2.BBw8vdeM_hwURd.webp&quot; alt=&quot;screenshot&quot;&gt;
&amp;#x3C;F.ImgCap&gt;模仿桌面环境的 Web 界面，工作颓废两不误。借助「轻应用」机制，你可以将几乎任何网页嵌入到你的「桌面环境」！&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/hero.SiFg8sSO_Z8Qe3f.webp&quot; alt=&quot;screenshot&quot;&gt;
&amp;#x3C;F.ImgCap&gt;在线预览音乐、视频甚至交互式的网页文件。甚至可以写代码！&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/rojimp-pre-4.CsJ2euBN_ZOa02m.webp&quot; alt=&quot;screenshot&quot;&gt;
&amp;#x3C;F.ImgCap&gt;创建部门架构并授予管理对部门文件夹的权限。部门的家目录可以自定义，因此你甚至可以用 RojExplorer 管理你服务器上的网页。
注：目前不支持「部门管理员」功能，即不能授予某用户对特定部门下其他用户和部门的管理权。图中的「部门管理员」是空衔。&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/rojimp-pre-5.BKQ3hyre_jCj8u.webp&quot; alt=&quot;screenshot&quot;&gt;
&amp;#x3C;F.ImgCap&gt;轻松共享文件，就像网盘一样。&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/rojimp-pre-6.eAbefro4_1qBDno.webp&quot; alt=&quot;screenshot&quot;&gt;
&amp;#x3C;F.ImgCap&gt;附赠了一些「轻应用」页面，奇思妙想等待你探索！&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ak-ioi.com/_astro/rojimp-pre-7.DVBQfoiE_2qfDvY.webp&quot; alt=&quot;screenshot&quot;&gt;
&amp;#x3C;F.ImgCap&gt;手机端特供文件管理器界面，随时随地皆可使用，现已更加强大！&amp;#x3C;/F.ImgCap&gt;&lt;/p&gt;
&lt;h2&gt;已知问题&lt;/h2&gt;
&lt;p&gt;目前用户/群组所占用存储空间的统计机制并不完善，会产生一些误差。为此，默认配置下，解压文件时会强制重新扫描目录计算大小，这可能会比较耗时。每周还会自动重新计算至多一次。空间被设置为无限的用户和群组，永远不会触发强制重新扫描，但是界面也不会显示已用空间的量。&lt;/p&gt;
&lt;h2&gt;修改说明&lt;/h2&gt;
&lt;p&gt;RojExplorer Improved 相比原来的 RojExplorer 做了很多修复，也加了不少功能。要查看完整的说明文件，请&lt;a href=&quot;https://wcl.sparkslab.art/index.php?user/loginSubmit&amp;#x26;name=guest&amp;#x26;password=guest&quot;&gt;游客登录 WMSDF Cloud&lt;/a&gt; 并进入「公示文件」目录。&lt;/p&gt;
&lt;h2&gt;如何安装&lt;/h2&gt;
&lt;p&gt;注意：至少需要 PHP 7.1 版本。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;下载 zip，解压到一个空文件夹中。&lt;/li&gt;
&lt;li&gt;在服务器文件管理上，进入你用来安装 RojExplorer 的文件夹。&lt;/li&gt;
&lt;li&gt;先创建一个 block.lock 空白文件。&lt;/li&gt;
&lt;li&gt;如果您的服务器支持在线 zip 解压，那么将压缩包上传到文件夹后解压。&lt;br&gt;
如果不支持，请使用支持多线程文件夹上传的 FTP 软件上传（并开启足够多的线程数）。&lt;/li&gt;
&lt;li&gt;上传完成后，打开 .htaccess，找到 &lt;code&gt;RewriteBase&lt;/code&gt; 一行，修改为要安装到的 URL 路径。比如：、
如果安装到 &lt;code&gt;https://example.com/&lt;/code&gt;，那么是 &lt;code&gt;/&lt;/code&gt;；&lt;br&gt;
如果安装到 &lt;code&gt;https://example.org/roj/&lt;/code&gt;，那么是 &lt;code&gt;/roj/&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;从 .htaccess 中删除 IP 屏蔽部分（在文件最前面；这是开发测试部署中的 IP 屏蔽设置，若不删除将导致你无法正常访问）。&lt;/li&gt;
&lt;li&gt;访问你的 URL（看到关于 block.lock 的错误提示就对了）。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;config.init&lt;/code&gt; 和 &lt;code&gt;data.init&lt;/code&gt; 分别重命名为 &lt;code&gt;config&lt;/code&gt; 和 &lt;code&gt;data&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在服务器上删除 block.lock。&lt;/li&gt;
&lt;li&gt;刷新页面，设置管理员密码，点击「忽略并进入」完成设置。默认管理员用户名是 &lt;code&gt;admin&lt;/code&gt;，你可以登录管理员账号后使用用户管理功能修改管理员用户名。&lt;/li&gt;
&lt;li&gt;如果要和其他网页应用对接，请修改 &lt;code&gt;config/setting_user.php&lt;/code&gt;。加入下面的行（见附件1；有则改之，无则加勉），其中横线处输入要对接网站的域名（要加 &lt;code&gt;https://&lt;/code&gt; 或 &lt;code&gt;http://&lt;/code&gt;，多个请用分号隔开）。这些域名将能够以浏览器用户的名义向你的 RojExplorer 发送请求，因此请确认你信任这些网站的行为。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;附件 1：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;//Lightapp Callback 允许的域名。
//请不要设置‘*’,否则用户将会面临CSRF危险。
$GLOBALS[&apos;ACB&apos;][&apos;origins&apos;]=&apos;_______________&apos;;
header(&apos;Access-Control-Allow-Origin: &apos;.$GLOBALS[&apos;ACB&apos;][&apos;origins&apos;]); //只允许特定。
header(&apos;Access-Control-Allow-Methods: GET;POST&apos;);
header(&apos;Access-Control-Allow-Credentials: true&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至于怎么和其他应用对接？我也有点忘了。&lt;/p&gt;
&lt;h2&gt;反馈/技术支持&lt;/h2&gt;
&lt;p&gt;此软件原则上不受支持。如果你发现安全问题，请勿公开报告，请向作者发送私人邮件。&lt;/p&gt;</content:encoded><h:img src="/_astro/hero.SiFg8sSO.png"/><enclosure url="/_astro/hero.SiFg8sSO.png"/></item><item><title>[游记] CSP-S 2019 游记 (2)</title><link>https://ak-ioi.com/blog/diary/give-up-csp-2019-2</link><guid isPermaLink="true">https://ak-ioi.com/blog/diary/give-up-csp-2019-2</guid><pubDate>Fri, 01 Jan 0100 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;写🐎呢，都爆零了。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>