从手动配置到自然语言生成规则:一个 Android AI 工具的重构实录

<p><img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/71b95ea83dfc4b1695bf05eb549ea226~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgdTNzaGFkb3c=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5NjU5MzI3MiJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1777441461&x-orig-sign=4BpOCiTgsvMGLIgMUyjsHxqmdOA%3D" alt="ChatGPT Image 2026年4月21日 17\_09\_39.png"></p> <h1 id="h1--ai-wi-fi-app-"><a name="先说结论,使用AI 驱动的 Wi-Fi 场景规则生成与执行系统,大幅简化了工具类App的配置使用门槛" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>先说结论,使用AI 驱动的 Wi-Fi 场景规则生成与执行系统,大幅简化了工具类App的配置使用门槛</h1><p>原版配置<br><img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/7876fa55f9b345cda50af9b161742123~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgdTNzaGFkb3c=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5NjU5MzI3MiJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1777441461&x-orig-sign=0vD4mK4IXg6dpiuDNC%2BxidkEXVU%3D"" alt=""></p> <p>AI化后的配置<br><img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/b96c85c5ed9f4602817aadb04d892484~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgdTNzaGFkb3c=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5NjU5MzI3MiJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1777441461&x-orig-sign=x5EiuH5neVX6H2vtsokMB%2FYSdMI%3D" alt=""></p> <h3 id="h3--5-"><a name="值得重点讲的 5 个问题" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>值得重点讲的 5 个问题</h3><ol> <li><strong>规则输入方式怎么从表单升级成自然语言</strong></li><li><strong>模型怎么从“会回答”变成“会产出结构化规则”</strong></li><li><strong>普通聊天和规则生成为什么必须分流</strong></li><li><strong>规则运行为什么要独立成 <code>RuleEngine</code></strong></li><li><strong>规则为什么必须持久化,而不能只存在内存里</strong></li></ol> <p>这 5 个问题决定了项目的系统价值。</p> <h3 id="h3-u53EFu4EE5u5E26u8FC7u7684u95EEu9898"><a name="可以带过的问题" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>可以带过的问题</h3><ul> <li>页面细节和视觉优化</li><li>状态文案怎么命名</li><li>某些小范围重构</li><li>某次 prompt 的措辞细节调整</li></ul> <hr> <h2 id="h2--"><a name="先说结果:项目最后长成了什么样" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>先说结果:项目最后长成了什么样</h2><p>项目最终不是一个单纯的 AI 聊天壳子,而是一个有完整主链路的端侧规则系统。当前代码里已经有:</p> <ul> <li><code>ChatViewModel</code>:负责输入分流、上下文组织、流式请求和结果落地</li><li><code>PromptBuilder</code>:负责规则生成 prompt 和普通聊天能力 prompt</li><li><code>InputClassifier</code>:把输入分成普通聊天和指令聊天</li><li><code>NormalChatContextManager</code> / <code>CommandChatContextManager</code>:分别维护两类上下文</li><li><code>RuleRepo</code>:用 Room 持久化规则</li><li><code>RuleEngine</code>:根据 Wi-Fi 变化读取规则、去重并执行</li><li><code>MainActivity</code>:作为事件入口,负责把用户输入和 Wi-Fi 变化接入系统。</li></ul> <p>从提交历史看,项目在这 7 天里经历了几个明显阶段:先接入 prompt、上下文和 action 解析;再补输入分类、上下文管理类和 RuleEngine;最后补持久化配置、页面优化、代码清理和聊天能力 prompt。</p> <hr> <h2 id="h2--1-"><a name="问题 1:手工配置规则门槛太高,输入方式必须升级" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>问题 1:手工配置规则门槛太高,输入方式必须升级</h2><p>最初的工具思路很直白:用户进入页面,手工选择 Wi-Fi、再手工配置音量、亮度、蓝牙之类的动作。<br>这种方式能用,但门槛很高,尤其是在“我只想说一句:以后在公司 Wi-Fi 下静音”这种场景里,表单输入的体验并不自然。</p> <h3 id="h3-u89E3u51B3u601Du8DEF"><a name="解决思路" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>解决思路</h3><p>把“规则配置入口”从表单升级成自然语言:</p> <ul> <li>用户负责表达意图</li><li>AI 负责把意图翻译成结构化规则</li><li>本地系统负责保存和执行</li></ul> <p>这一步不是简单多了一个聊天窗口,而是把输入方式从“手工配置”改成了“意图配置”。</p> <h3 id="h3-u5173u952Eu5B9Eu73B0"><a name="关键实现" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>关键实现</h3><p>项目里专门做了一个 <code>PromptBuilder.build(...)</code>,它会把:</p> <ul> <li>当前 Wi-Fi</li><li>最近 4 条相关消息</li><li>当前输入</li><li>固定 JSON 输出格式</li><li>示例</li></ul> <p>拼成一条规则生成专用 prompt,让模型只返回类似下面这种结果:</p> <pre><code>{ &quot;trigger&quot;: &quot;公司WiFi&quot;, &quot;operation&quot;: &quot;set_volume&quot;, &quot;params&quot;: { &quot;value&quot;: 0 } }</code></pre><p><code>PromptBuilder</code> 里可以直接看到这套结构:<code>buildRecentHistory()</code> 只取最近 4 条用户/AI 消息;规则 prompt 要求模型返回 <code>trigger / operation / params.value</code> 这样的固定结构。</p> <hr> <h2 id="h2--2-ai-"><a name="问题 2:只有“聊天”不够,必须让 AI 产出可执行的结构化结果" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>问题 2:只有“聊天”不够,必须让 AI 产出可执行的结构化结果</h2><p>很多 AI Demo 到这里就停了:模型能回一句“好的,我已经理解你的需求”。<br>但这对工具系统没有意义。真正的问题是:</p> <blockquote> <p><strong>模型怎么把一句自然语言,稳定变成可以落到本地执行链路里的结构化结果?</strong></p> </blockquote> <h3 id="h3-u89E3u51B3u601Du8DEF"><a name="解决思路" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>解决思路</h3><p>把模型输出目标从“文本回答”改成“JSON 结果”。</p> <h3 id="h3-u5173u952Eu5B9Eu73B0"><a name="关键实现" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>关键实现</h3><p><code>ChatViewModel</code> 的流式请求结束后,不是简单把回复展示到 UI 就结束,而是:</p> <ol> <li>继续持有完整的 <code>currentText</code></li><li>如果这次输入属于 <code>CommandChat</code></li><li>就进入 <code>ActionParser().parseActionDTO(currentText)</code></li><li>校验 DTO</li><li>再转成本地 <code>Action</code></li><li>交给 <code>RuleRepo</code> 保存并执行。</li></ol> <p>这一段很关键,因为它让模型输出真正接入了本地系统,而不是停留在“AI 说了一句正确的话”。</p> <h3 id="h3-u8FD9u4E00u6B65u89E3u51B3u4E86u4EC0u4E48"><a name="这一步解决了什么" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>这一步解决了什么</h3><p>它把项目从:</p> <ul> <li><strong>AI 会聊天</strong></li></ul> <p>推进到了:</p> <ul> <li><strong>AI 能生成规则,并让规则落地</strong></li></ul> <hr> <h2 id="h2--3-prompt-"><a name="问题 3:普通聊天和规则生成混在一起,prompt 很容易被污染" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>问题 3:普通聊天和规则生成混在一起,prompt 很容易被污染</h2><p>项目接入 AI 后,很快遇到一个很典型的问题:</p> <ul> <li>有些输入是普通聊天,比如“你能做什么?”</li><li>有些输入是规则指令,比如“以后在这里把音量调成 0”</li></ul> <p>如果所有输入都走一条链路、吃同一套上下文,结果会很糟:</p> <ul> <li>普通聊天会误触发规则生成</li><li>规则生成会被闲聊历史污染</li><li>prompt 变得越来越不稳定</li></ul> <h3 id="h3-u89E3u51B3u601Du8DEF"><a name="解决思路" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>解决思路</h3><p>先做输入分流,再做上下文分离。</p> <h3 id="h3--1-"><a name="关键实现 1:输入分流" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>关键实现 1:输入分流</h3><p><code>InputClassifier</code> 用一组简单规则把输入分成两类:</p> <ul> <li><code>NormalChat</code></li><li><code>CommandChat</code></li></ul> <p>当前规则是关键词匹配,例如“帮我、自动、连上、设置、音量、打开、关闭、静音、亮度”等会被判成指令输入。</p> <p>在 <code>ChatViewModel</code> 里,所有输入都会先进入统一入口:</p> <pre><code>fun handleUserInput(input: String, wifiSsid: String?) { when (InputClassifier.classify(input)) { InputType.NormalChat -&gt; handleNormalChatInput(input, wifiSsid) InputType.CommandChat -&gt; handleCommandInput(input, wifiSsid) } }</code></pre><p>这一点在当前代码里已经明确落地。</p> <h3 id="h3--2-"><a name="关键实现 2:上下文分离" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>关键实现 2:上下文分离</h3><p>项目没有继续沿用“一个消息列表管所有场景”的方案,而是拆成了两个上下文管理器:</p> <ul> <li><code>NormalChatContextManager</code>:保留最近 10 条普通消息</li><li><code>CommandChatContextManager</code>:保留最近 6 条指令相关消息。</li></ul> <p>更重要的是,AI 回复在流式结束后,也会根据输入类型写回对应上下文,而不是只写到界面消息列表里。<code>appendAssistantToContext(...)</code> 就是在做这件事。</p> <h3 id="h3-u8FD9u4E00u7EC4u6539u52A8u89E3u51B3u4E86u4EC0u4E48"><a name="这一组改动解决了什么" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>这一组改动解决了什么</h3><p>它把系统从“一个大聊天框”变成了两条清晰的业务链:</p> <ul> <li>普通聊天链</li><li>规则生成链</li></ul> <p>这是这个项目工程感开始明显提升的分水岭。</p> <hr> <h2 id="h2--4-"><a name="问题 4:规则不能只在“发送消息时顺手执行”,必须有独立运行引擎" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>问题 4:规则不能只在“发送消息时顺手执行”,必须有独立运行引擎</h2><p>如果规则只是保存在内存里,并且只有用户按下发送按钮时才顺手跑一下,那它本质上还是个半成品。<br>真正需要解决的问题是:</p> <blockquote> <p><strong>当 Wi-Fi 环境变化时,系统能不能独立地读取规则、去重、执行,并把结果反馈回来?</strong></p> </blockquote> <h3 id="h3-u89E3u51B3u601Du8DEF"><a name="解决思路" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>解决思路</h3><p>抽出 <code>RuleEngine</code>,让“规则运行”变成一条独立链路,而不是附着在聊天链路上。</p> <h3 id="h3-u5173u952Eu5B9Eu73B0"><a name="关键实现" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>关键实现</h3><p><code>RuleEngine.run(ssid)</code> 做了几件事:</p> <ol> <li>根据 <code>ssid</code> 从 <code>RuleRepo</code> 读取规则</li><li>和上次执行的 <code>lastActions</code> 比较</li><li>如果 Wi-Fi 和动作都没变化,返回 <code>SkippedDuplicate</code></li><li>如果没匹配规则,返回 <code>NoRule</code></li><li>否则批量执行动作,并返回 <code>Success(actions)</code>。</li></ol> <p>同时,<code>MainActivity</code> 里已经有了独立的环境入口 <code>onWifiMaybeChanged()</code>:</p> <ul> <li>获取当前 <code>ssid</code></li><li>更新界面上的 Wi-Fi 状态</li><li>调用 <code>ruleEngine.run(ssid)</code></li><li>用 <code>updateEnvFromRuleResult(result)</code> 更新环境展示</li><li>再把结果同步给 <code>viewModel.onRuleRunResult(result)</code>。</li></ul> <p>另外,这个入口并不是只在发送消息时调用,它会在页面初始化、<code>onResume()</code>、Wi-Fi 权限回调后都触发。</p> <h3 id="h3-u8FD9u4E00u6B65u7684u610Fu4E49"><a name="这一步的意义" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>这一步的意义</h3><p>项目真正拥有了第二条独立主线:</p> <ul> <li><strong>环境变化 → 规则运行 → 执行反馈</strong></li></ul> <p>而不再只是:</p> <ul> <li><strong>用户输入 → AI 回复</strong></li></ul> <hr> <h2 id="h2--5-"><a name="问题 5:规则只存在内存里,没有实际使用价值" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>问题 5:规则只存在内存里,没有实际使用价值</h2><p>如果规则每次重启应用就丢,那就算模型能生成规则,也只能算实验功能。<br>这个问题在 Day7 才真正补齐:</p> <blockquote> <p><strong>规则必须持久化。</strong></p> </blockquote> <h3 id="h3-u89E3u51B3u601Du8DEF"><a name="解决思路" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>解决思路</h3><p>把 <code>RuleRepo</code> 从内存容器升级成真正的本地存储层。</p> <h3 id="h3-u5173u952Eu5B9Eu73B0"><a name="关键实现" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>关键实现</h3><p>现在 <code>RuleRepo</code> 已经通过 Room 建了一个本地数据库:</p> <ul> <li><code>RuleRepo.init(context)</code> 里使用 <code>Room.databaseBuilder(..., &quot;rule_store.db&quot;)</code></li><li><code>addRule(action)</code> 调 <code>dao.upsert(...)</code></li><li><code>getRules(ssid)</code> 调 <code>dao.getByTrigger(ssid)</code> 再转回 <code>Action</code>。</li></ul> <p><code>MainActivity.onCreate()</code> 里也已经在页面初始化时执行了 <code>RuleRepo.init(applicationContext)</code>。</p> <h3 id="h3-u8FD9u4E00u6B65u7684u610Fu4E49"><a name="这一步的意义" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>这一步的意义</h3><p>项目从“会生成规则”升级成了“规则真的能被保存下来,并在下次启动时继续生效”。</p> <hr> <h2 id="h2--6-app-ai-"><a name="问题 6:用户问“这个 App 能做什么”时,AI 也需要有边界" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>问题 6:用户问“这个 App 能做什么”时,AI 也需要有边界</h2><p>项目加了聊天入口后,很自然会出现一个新问题:</p> <ul> <li>用户会问:“你能做什么?”</li><li>或者:“这个软件到底是干嘛的?”</li></ul> <p>如果没有明确约束,模型很容易回答得过宽,甚至超出项目真实能力边界。</p> <h3 id="h3-u89E3u51B3u601Du8DEF"><a name="解决思路" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>解决思路</h3><p>为普通聊天链路单独加一条能力说明 prompt。</p> <h3 id="h3-u5173u952Eu5B9Eu73B0"><a name="关键实现" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>关键实现</h3><p><code>PromptBuilder</code> 里增加了 <code>buildAppCapabilityPrompt()</code>,专门告诉模型:</p> <ul> <li><p>这是这个 App 的助手</p> </li><li><p>当用户询问能力边界时,要明确回答:</p> <ul> <li>当前可以通过与 AI 聊天设置 Wi-Fi 环境下的手机配置</li><li>支持音量、亮度、蓝牙开关</li><li>并且下次启动时会自动应用这些已保存配置。</li></ul> </li></ul> <p>当前普通聊天上下文里,会 prepend 这条 capability prompt,再拼接普通聊天消息。这样普通聊天不只是在“能说话”,而是开始有了更清楚的产品边界。</p> <hr> <h2 id="h2-u6700u7EC8u67B6u6784u957Fu4EC0u4E48u6837"><a name="最终架构长什么样" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>最终架构长什么样</h2><h3 id="h3-1-"><a name="1)输入链路" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>1)输入链路</h3><pre><code class="language-mermaid">flowchart TD A[用户输入] --&gt; B[MainActivity] B --&gt; C[ChatViewModel.handleUserInput] C --&gt; D[InputClassifier] D --&gt;|NormalChat| E[NormalChatContextManager] D --&gt;|CommandChat| F[CommandChatContextManager] E --&gt; G[普通聊天 Prompt] F --&gt; H[规则生成 Prompt] G --&gt; I[ChatRepo.streamChat] H --&gt; I I --&gt; J[流式文本拼接] J --&gt; K[更新 UI] J --&gt; L{是否为 CommandChat} L --&gt;|否| M[写回普通上下文] L --&gt;|是| N[ActionParser] N --&gt; O[RuleRepo.addRule] O --&gt; P[ActionExecutor.execute] P --&gt; Q[写回指令上下文] </code></pre> <h3 id="h3-2-"><a name="2)环境运行链路" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>2)环境运行链路</h3><pre><code class="language-mermaid">flowchart TD A[onCreate / onResume / Wi-Fi 权限回调] --&gt; B[onWifiMaybeChanged] B --&gt; C[读取当前 SSID] C --&gt; D[RuleEngine.run] D --&gt; E{匹配结果} E --&gt;|NoRule| F[界面提示无匹配规则] E --&gt;|SkippedDuplicate| G[界面提示跳过重复执行] E --&gt;|Success| H[执行 Action 列表] H --&gt; I[更新环境状态与执行结果]</code></pre> <h3 id="h3-3-"><a name="3)模块关系图" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>3)模块关系图</h3><pre><code class="language-mermaid">flowchart LR UI[MainActivity / UI] --&gt; VM[ChatViewModel] VM --&gt; Classifier[InputClassifier] VM --&gt; NCtx[NormalChatContextManager] VM --&gt; CCtx[CommandChatContextManager] VM --&gt; Prompt[PromptBuilder] VM --&gt; Repo[ChatRepo] VM --&gt; Parser[ActionParser] Parser --&gt; RuleRepo[RuleRepo] UI --&gt; RuleEngine[RuleEngine] RuleEngine --&gt; RuleRepo RuleEngine --&gt; Executor[ActionExecutor]</code></pre> <hr> <h2 id="h2-u8FD9u4E2Au9879u76EEu73B0u5728u7684u8FB9u754Cu548Cu4E0Bu4E00u6B65"><a name="这个项目现在的边界和下一步" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>这个项目现在的边界和下一步</h2><p>到当前阶段,这个项目已经具备:</p> <ul> <li>文本输入</li><li>输入分流</li><li>上下文管理</li><li>规则生成</li><li>规则持久化</li><li>环境触发执行</li><li>产品能力边界说明</li></ul> <p>它已经足够作为一个完整的 AI 端侧工程原型来讲。<br>如果继续往下做,比较自然的方向会是:</p> <ul> <li>增加规则编辑、删除和覆盖策略的 UI</li><li>给 <code>InputClassifier</code> 增加更细粒度分类</li><li>把日志和可观测性做得更系统</li><li>再补一层更清晰的消息模式建模</li><li>逐步引入更多系统能力和工具模块</li></ul> <hr> <h2 id="h2-u7ED3u8BED"><a name="结语" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>结语</h2><p>这个项目过程中最重要的变化,不是“多了几个类”,而是系统边界越来越清楚了:</p> <ul> <li>输入不是都一样的</li><li>上下文不是都能混着用的</li><li>模型输出不能只停留在文本</li><li>规则运行不能挂在聊天链路上</li><li>规则如果不持久化,就不是真正的工具能力</li></ul> <p>最后得到的不是一个“AI 聊天 App”,而是一个:</p> <blockquote> <p><strong>AI 驱动的 Wi-Fi 场景规则生成与执行系统</strong></p> </blockquote> <p><a href="https://github.com/u3shadow/ShaverAI.git">完整的代码可以点这里移步我的github查看</a></p>

2026/4/22
阅读更多

学习版游戏自制云存档同步的方法探索以及总结

<p>作为一名资深足球游戏玩家,最近重燃对《实况足球2021》的热情,搞了个带大补的版本,确实爽,却在多设备存档同步问题上掉进了坑。通过两天”痛并快乐着”的折腾,最终找到完美解决方案,现整理给喝油们参考。</p> <p>■ 硬件环境</p> <p>ROG Ally掌机(外出便携)+ MSI gp76笔记本(家用主力)构成了我的双屏足球战场。这两个Win11平台都装载了打了大补的PES2021赛季更新版。</p> <p><img src="https://imgheybox.max-c.com/bbs/2025/08/06/1150852b14ece474b2219386a1cef519/thumb.jpeg?imageMogr2/format/webp/quality/50/auto-orient/ignore-error/1" alt=""><br>我的爱机们<br>■ 痛点溯源</p> <p>游戏存档默认存储在C:\Users\用户名\Documents\KONAMI\eFootball PES 2021 SEASON UPDATE\数字ID\文件夹。由于学习版缺乏云存档功能,导致:</p> <ol> <li><p>双设备存档各自为战</p> </li><li><p>手动复制存在覆盖风险</p> </li><li><p>游戏进度无法延续</p> </li></ol> <p>■ 试错全记录(前5个踩坑方案)</p> <p>方案0 - 正版授权(沉没成本)</p> <p>各平台密钥均价$800+,瞬间击穿钱包防御。</p> <p><img src="https://imgheybox.max-c.com/bbs/2025/08/06/994a99ca853a9f62f3a9bd8dacbfad11/thumb.jpeg?imageMogr2/format/webp/quality/50/auto-orient/ignore-error/1" alt=""><br>太贵了<br>方案1 - 手工搬运(原始主义)</p> <p>直接复制save000001。bin等系列文件。耗时费力不说,曾在机场手滑覆盖2小时比赛记录,血泪教训。</p> <p>方案2 - 黑盒云盘(伪云方案)</p> <p>宣称自动同步,实测:</p> <ul> <li><p>仅支持5个文件</p> </li><li><p>与黑盒语音服务耦合</p> </li></ul> <p>不太好用,放弃</p> <p>方案3 - 微存档(氪金劝退)</p> <p>专业存档管理器的PUA套路:</p> <p>免费版可本地备份</p> <p>云同步需月付,感觉有点不靠谱,暂不考虑</p> <p><img src="https://imgheybox.max-c.com/bbs/2025/08/06/860a3e7e50d1944c8fb3ec90ebabc2a7/thumb.png?imageMogr2/format/webp/quality/50/auto-orient/ignore-error/1" alt=""><br>云存档要给钱<br>方案4 - GitHub私仓(极客陷阱)</p> <p>.git仓库看似优雅却暗藏杀机:</p> <ul> <li><p>需手动pull/push操作</p> </li><li><p>无代理时上传龟速(25MB存档需5min)</p> </li><li><p>冲突解决机制缺失</p> </li></ul> <p>■ 破局方案:百度网盘+符号链接</p> <p>最终选择百度网盘超级会员方案,实现原理:</p> <p><img src="https://imgheybox.max-c.com/bbs/2025/08/06/75078d264d51fec8276c19bd96d15315/thumb.png?imageMogr2/format/webp/quality/50/auto-orient/ignore-error/1" alt=""><br>百度网盘带的同步功能<br>📁 系统架构</p> <p>本地存档 → 符号链接 → 百度同步目录 → 云端实时同步</p> <p>🖥关于符号连接</p> <p><img src="https://imgheybox.max-c.com/bbs/2025/08/06/47db9d8e8140976d5b177733ff2c1389/thumb.jpeg?imageMogr2/format/webp/quality/50/auto-orient/ignore-error/1" alt=""><br>ai是这样说的<br>⚙ 实施步骤(Win系统通用)</p> <ol> <li>定位存档路径:</li></ol> <p>C:\Users\用户名\Documents\KONAMI\eFootball PES 2021 SEASON UPDATE</p> <ol start="2"> <li>创建云同步目录(建议路径):</li></ol> <p>D:\BaiduSync\PES2021_SAVE</p> <ol start="3"> <li>迁移存档文件:</li></ol> <p>剪切原存档文件夹内容至云目录</p> <ol start="4"> <li>管理员模式启动CMD执行:</li></ol> <p>mklink /J “C:…\数字ID” “D:\BaiduSync\PES2021_SAVE”</p> <ol start="5"> <li>配置百度网盘:</li></ol> <p>设置→高级设置→文件夹自动备份→添加云目录</p> <p>🔧 注意要点</p> <ul> <li><p>需先卸载原有存档文件再创建软链</p> </li><li><p>关闭客户端”完成同步弹窗提示”(否则游戏过程频繁弹窗)</p> </li></ul> <p>🔄 同步效果实测</p> <ul> <li><p>ROG Ally到笔记本端延迟&lt;20秒</p> </li><li><p>修改时间戳完全同步</p> </li></ul> <p><img src="https://imgheybox.max-c.com/bbs/2025/08/06/b21990aba8de7a233ff21c571206c5ab/thumb.jpeg?imageMogr2/format/webp/quality/50/auto-orient/ignore-error/1" alt=""><br>电脑上的存档进度<br><img src="https://imgheybox.max-c.com/bbs/2025/08/06/4fd07cad4f09c06a67656212db53aae6/thumb.jpeg?imageMogr2/format/webp/quality/50/auto-orient/ignore-error/1" alt=""><br>ally 同步过来啦</p> <p>经过测试,累计完成34场跨设备联赛对战,存档同步成功率达 90%,有时候百度云会下载失败,多点两下就好了。此方案不仅适用于实况足球,同样可拓展到FIFA、FM等需多设备存档同步的体育类游戏。</p> <p>后记:这次折腾最值得的投资不是网盘会员,而是掌握mklink这个命令。当软链接贯通物理路径和云存储的瞬间,颇有种开任意门连接数字世界的奇妙快感。</p>

2025/8/19
阅读更多

鸿蒙next中web组件和navigation的一个bug以及处理方案

<h2 id="h2-u95EEu9898u73B0u8C61"><a name="问题现象" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>问题现象</h2><p>在这种情况下,把controller从上层传递到最底层的web页面,用它来打开网页</p> <p>刚开始一切正常,但是需要执行一些js的时候,就会出现controller未绑定的提示</p> <p>特别是在连续打开好几个页面之后,从最顶部页面返回,在中间的页面就会出现该问题</p> <p>例如a-b-c-d,此时从d返回c,再在c页面使用controller执行js方法,就会出现controller未绑定到webview的提示</p> <p>但是此时,controller肯定是绑定过的,出现该问题是不合理的。</p> <h2 id="h2-u73B0u8C61u62A5u9519"><a name="现象报错" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>现象报错</h2><p><img src="https://u3blog.xyz/zb_users/upload/2024/08/202408301612303128494.png" alt=""></p> <h2 id="h2-u7B80u8981u4EE3u7801"><a name="简要代码" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>简要代码</h2><p>homepage</p> <pre><code> -HomeNavigation </code></pre><p>homenavigation</p> <pre><code> -Navigation -stack -mywebview(在这里new webviewcontroller()) </code></pre><p>mywebview</p> <pre><code> -mywebcore(传递controller) </code></pre><p>mywebcore</p> <pre><code> -web(最终的使用controller) -button1 打开新页面 -button2 返回上一页并执行一段自定义js </code></pre><p>现在我不断的点击button1使用navigation打开多个新页面,再不断点击返回,即可出现问题</p> <h2 id="h2-u6392u67E5u6B65u9AA4"><a name="排查步骤" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>排查步骤</h2><p>1.首先排查是否在往navigation中添加和移除页面的过程中,是否发生了对象的销毁,经过对比对象id号,发现id未变化,说明对象不存在销毁的问题</p> <p>2.排查该报错出现的原因,经过网络搜索,发现未绑定问题只会出现在new了controller但是还未传递给web组件的时候,但是我们已经打开了网页,所以肯定是绑定过的</p> <p>3.经过1,2的排查,我们可以推断,这是一个系统级的bug,所以我们需要特殊管理controller</p> <h2 id="h2-u5C1Du8BD5u89E3u51B3"><a name="尝试解决" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>尝试解决</h2><p>新建一个全局的map,当有新页面打开的时候,把对应的controller放入该map,当页面销毁时,把controller从中移除</p> <p>代码改造,经过如下改造,我们已经对controller进行了单独的管理</p> <h2 id="h2-u89E3u51B3u4EE3u7801"><a name="解决代码" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>解决代码</h2><pre><code>import web_webview from &#39;@ohos.web.webview&#39;; import { HashMap } from &#39;@kit.ArkTS&#39;; export let controllers:HashMap&lt;string,web_webview.WebviewController&gt; = new HashMap&lt;string,web_webview.WebviewController&gt;()</code></pre><h2 id="h2-u6548u679Cu6D4Bu8BD5"><a name="效果测试" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>效果测试</h2><p>改造之后,再看我们的项目运行情况,发现每次都能顺利的执行了,不再会出现未绑定的情况</p> <h2 id="h2-u7ED3u8BBA"><a name="结论" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>结论</h2><p>目前将webviewcontroller作为页面跳转参数传递,在navigation中存在bug,会导致对象的绑定状态混乱,令其无法成功调用方法。</p> <p>我们需要单独使用全局的管理策略,这样可以暂时规避controller状态混乱的情况</p> <h2 id="h2-u95EEu9898u672Cu8D28"><a name="问题本质" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>问题本质</h2><p>该问题的本质还是鸿蒙next系统在传递controller的过程中,该对象的绑定状态会丢失,判定为一个系统bug,需要等后续修复。</p> <p><strong>本文正在参加华为鸿蒙有奖征文征文活动</strong></p>

2024/8/30
阅读更多

ChatGPT是怎么帮我写ios代码的

<h2 id="h2-u80CCu666F"><a name="背景" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>背景</h2><p>博主只会安卓开发,最近使用chatGPT3.5把一个安卓项目转换成了ios项目并成功运行实现功能</p> <h2 id="h2-u5E94u7528u7684u4E3Bu8981u529Fu80FD"><a name="应用的主要功能" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>应用的主要功能</h2><p>这个安卓应用主要是拿来看比赛回放的,有时候太迟的足球比赛不想熬夜,但是又不想被剧透看全场回放,就写了个这个应用,这次讨论的重点并不是应用本身,如果大家有兴趣可以评论区留言,我把应用发出来</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402021631018035453.png" alt=""></p> <p>应用的主要页面有下面几个,1.登录爱奇艺账号页面,2.球队列表页面,3.播放页面</p> <p>其中播放页面最复杂,要实现以下几个功能</p> <ol> <li>根据选择的球队,加载搜索比赛结果页面,并把回放和广告列表隐藏,把比赛列表文字放大</li><li>点击了比赛列表,跳转到播放页面</li><li>等待播放页面加载完成,自动点击全屏元素按钮</li><li>在全屏开始播放之后,加载左右两个快放按钮,长按调用网页上的快速播放元素,松开恢复正常速播放<h2 id="h2-u5F00u59CBu8F6Cu6362"><a name="开始转换" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>开始转换</h2><h3 id="h3-u6587u4EF6u7ED3u6784u5DEEu5F02"><a name="文件结构差异" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>文件结构差异</h3>Android的页面主要由两个部分组成</li><li>XML布局文件,决定了页面上有哪些元素,以及如何摆放,实现布局部分</li><li>Activity文件,决定了页面上元素的各种行为,实现逻辑部分</li></ol> <p>但是在ios上,并不是这样,chatgpt告诉我,转换出来的东西是一个继承自<strong>UIViewController</strong>的类,里面同时包含了上面的两个部分内容</p> <h3 id="h3-u8981u8F6Cu6362u54EAu4E9Bu529Fu80FD"><a name="要转换哪些功能" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>要转换哪些功能</h3><p>应用主要功能就是操作webview加载页面,然后通过调用js的方法来操作页面上的元素,所以webview的加载和js的加载就是这次转换的重点</p> <p>问题有两个</p> <ol> <li>怎么用webview加载内容</li><li>怎么操作webview中的内容<h3 id="h3--gpt-"><a name="我和gpt的对话" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>我和gpt的对话</h3>由于gpt没法自动合并xml和activity的代码,所以我只有让它先转xml,得到布局的controller内容<br>列表页面</li></ol> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402021638041898414.png" alt=""></p> <p>比如这个布局</p> <pre><code class="language-html"> &lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot; xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot; xmlns:tools=&quot;http://schemas.android.com/tools&quot; android:layout_width=&quot;match_parent&quot; android:layout_height=&quot;match_parent&quot; tools:context=&quot;.PlayerActivity&quot;&gt; &lt;FrameLayout android:visibility=&quot;gone&quot; android:id=&quot;@+id/mFlVideoContainer&quot; android:layout_width=&quot;match_parent&quot; android:layout_height=&quot;match_parent&quot;&gt; &lt;FrameLayout android:id=&quot;@+id/mFlVideoContainer1&quot; android:layout_width=&quot;match_parent&quot; android:layout_height=&quot;match_parent&quot;/&gt; &lt;LinearLayout android:orientation=&quot;horizontal&quot; android:id=&quot;@+id/ll_container&quot; android:layout_marginBottom=&quot;50dp&quot; android:layout_width=&quot;match_parent&quot; android:layout_height=&quot;match_parent&quot;&gt; &lt;Button android:layout_weight=&quot;1&quot; android:layout_width=&quot;0dp&quot; android:background=&quot;#00000000&quot; android:id=&quot;@+id/back_bt&quot; android:layout_height=&quot;match_parent&quot;/&gt; &lt;Button android:layout_weight=&quot;3&quot; android:layout_width=&quot;0dp&quot; android:visibility=&quot;invisible&quot; android:enabled=&quot;false&quot; android:background=&quot;#00000000&quot; android:id=&quot;@+id/pause_bt&quot; android:layout_height=&quot;match_parent&quot;/&gt; &lt;Button android:layout_weight=&quot;1&quot; android:layout_width=&quot;0dp&quot; android:background=&quot;#00000000&quot; android:id=&quot;@+id/go_bt&quot; android:layout_height=&quot;match_parent&quot;/&gt; &lt;/LinearLayout&gt; &lt;/FrameLayout&gt; &lt;WebView android:layout_width=&quot;match_parent&quot; android:layout_height=&quot;match_parent&quot; android:id=&quot;@+id/webview&quot; app:layout_constraintBottom_toBottomOf=&quot;parent&quot; app:layout_constraintEnd_toEndOf=&quot;parent&quot; app:layout_constraintStart_toStartOf=&quot;parent&quot; app:layout_constraintTop_toTopOf=&quot;parent&quot; /&gt; &lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;</code></pre> <p>让它给我转成ios版本,转出来是下面这样的</p> <pre><code class="language-swift">import UIKit import WebKit class PlayerViewController: UIViewController { // UIView elements let videoContainer: UIView = { let view = UIView() view.isHidden = true return view }() let videoContainer1: UIView = { let view = UIView() return view }() let backButton: UIButton = { let button = UIButton() button.backgroundColor = UIColor.clear return button }() let pauseButton: UIButton = { let button = UIButton() button.isHidden = true button.isEnabled = false button.backgroundColor = UIColor.clear return button }() let goButton: UIButton = { let button = UIButton() button.backgroundColor = UIColor.clear return button }() let webView: WKWebView = { let webView = WKWebView() return webView }() override func viewDidLoad() { super.viewDidLoad() // Set up constraints for each element videoContainer.translatesAutoresizingMaskIntoConstraints = false videoContainer1.translatesAutoresizingMaskIntoConstraints = false backButton.translatesAutoresizingMaskIntoConstraints = false pauseButton.translatesAutoresizingMaskIntoConstraints = false goButton.translatesAutoresizingMaskIntoConstraints = false webView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(videoContainer) view.addSubview(webView) NSLayoutConstraint.activate([ videoContainer.topAnchor.constraint(equalTo: view.topAnchor), videoContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor), videoContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor), videoContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor), videoContainer1.topAnchor.constraint(equalTo: videoContainer.topAnchor), videoContainer1.leadingAnchor.constraint(equalTo: videoContainer.leadingAnchor), videoContainer1.trailingAnchor.constraint(equalTo: videoContainer.trailingAnchor), videoContainer1.bottomAnchor.constraint(equalTo: videoContainer.bottomAnchor), backButton.topAnchor.constraint(equalTo: videoContainer.bottomAnchor), backButton.leadingAnchor.constraint(equalTo: view.leadingAnchor), backButton.bottomAnchor.constraint(equalTo: view.bottomAnchor), backButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.25), pauseButton.topAnchor.constraint(equalTo: videoContainer.bottomAnchor), pauseButton.leadingAnchor.constraint(equalTo: backButton.trailingAnchor), pauseButton.bottomAnchor.constraint(equalTo: view.bottomAnchor), pauseButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5), goButton.topAnchor.constraint(equalTo: videoContainer.bottomAnchor), goButton.leadingAnchor.constraint(equalTo: pauseButton.trailingAnchor), goButton.bottomAnchor.constraint(equalTo: view.bottomAnchor), goButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.25), webView.topAnchor.constraint(equalTo: view.topAnchor), webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), webView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } } </code></pre> <h2 id="h2-u9047u5230u7684u5751"><a name="遇到的坑" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>遇到的坑</h2><h3 id="h3-gpt-"><a name="gpt生成的代码并不是直接可以用的" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>gpt生成的代码并不是直接可以用的</h3><p>gpt生成出来的代码看似正确,但是有时候运行就会发现缺少内容,比如有的变量没有定义就使用了,你得追问它,才会告诉你</p> <p>虽然告诉你了,但是大概率会重新生成一份代码,这又是一个坑,这个重新生成的代码又有概率和第一次完全不同</p> <p>这坑需要有一定的编程能力,自己阅读下gpt给的代码,才可以实现想要的功能,不然就会被它坑</p> <h3 id="h3--gpt-"><a name="对于稍微复杂一点的布局,gpt给出的方案并不总能成功实现" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>对于稍微复杂一点的布局,gpt给出的方案并不总能成功实现</h3><p>由于给出的ios界面是用代码实现的,给出的安卓布局是xml的,有些时候ios并没有安卓的布局控件,gpt给出的方案运行起来就会出现错位等情况,这时候也需要人工调试</p> <h3 id="h3--gpt-"><a name="对于项目配置方面,gpt并不能给出精准的解答" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>对于项目配置方面,gpt并不能给出精准的解答</h3><p>这点主要体现在其中一个功能上,从ios应用中拉起爱奇异体育并播放,chatgpt给出的方案不管怎么设置都无法实现,因为它理解成了我需要从其他应用跳转到自己的ios应用</p> <p>其实这个问题在网上一搜就能处理,这也是一个坑,不能全部相信gpt的话</p> <h3 id="h3-ios-webview-"><a name="ios和安卓webview的差异导致的功能实现不同" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>ios和安卓webview的差异导致的功能实现不同</h3><p>由于爱奇艺的限制,使用ios的wkwebview无法进行播放,在安卓却可以播放,所以需要调整功能,不能直接在应用内播放,需要走scheme的方式跳转爱奇艺app做播放</p> <p>对于wkwebview无法播放的问题,我问了gpt很久,每次它也非常配合,给出各种各样的方案,花了很多时间,但是都无效</p> <p>这个问题也需要自己想一想,用自带的浏览器打开一下页面,把agent换成pc端,发现也无法播放,所以确认是wkwebview的问题,而不是实现方案的问题</p> <h2 id="h2-u603Bu7ED3"><a name="总结" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>总结</h2><ul> <li><p>在使用gpt的时候,也不能无脑相信它,有时候它明明不会或者不懂,也会给出答案,这种答案看起来没问题,其实是错误的,会把方向带偏,需要特别留意</p> </li><li><p>使用gpt可以让我这样一个完全不懂ios的开发人员在短时间内完成简单的开发工作,放在以前我至少需要学习基础的ios开发知识,学习完了之后,我还需要在开发遇到问题的时候一个点一个点的搜,大概率是没有相同问题的,需要自己从各种各样的结果中提炼</p> </li><li><p>现在gpt帮我完成了这个步骤,让这次开发变得轻松了很多</p> </li><li><p>gpt对开发人员的帮助确实是非常大,粗略估计,这次的小应用开发,为了节省了60%以上的时间,相信随着技术的发展,这个工具会越来越便利,这次使用的是免费的gpt3.5都如此强大,收费的4.0真是不敢想象</p> </li></ul>

2024/2/2
阅读更多

Android项目cicd流程总结(使用jenkins)

<h2 id="h2--cicd-"><a name="没有cicd之前我们都是怎么做的" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>没有cicd之前我们都是怎么做的</h2><p>相信做安卓开发的都做过这些事</p> <ol> <li>手动运行单元测试,根据报错改代码</li><li>检查代码风格,根据报错改代码</li><li>构建apk包,发给测试,有时候还得打很多个</li><li>接收测试的反馈,改bug,</li><li>重复之前的步骤</li><li>把apk放到ftp或者其他地方去发布</li></ol> <p>是不是想到这一套流程,头都大了,虽然每一步都不难,但是连起来都手工操作就很繁琐</p> <p>像这些手动流程固定的事,我们完全就可以交给机器来做,让我们有更多时间做点别的事,没错,这就是今天要说的cicd</p> <h2 id="h2--cicd-cicd-"><a name="什么是cicd,以及在cicd过程中包含了哪些步骤" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>什么是cicd,以及在cicd过程中包含了哪些步骤</h2><p>一般来说,安卓开发的CI/CD流程包括以下几个阶段:代码提交、代码检查、编译构建、单元测试集成测试、部署发布、用户反馈。</p> <ol> <li>在代码提交阶段,开发者将自己的代码推送到远程仓库,例如Git或SVN,并触发CI/CD工具或平台例如Jenkins或Travis CI等</li><li>在代码检查阶段,CI/CD工具或平台会对代码进行静态分析和风格检查,例如使用SonarQube或Checkstyle等。</li><li>在编译构建阶段,CI/CD工具或平台会使用Gradle或Maven等工具对代码进行编译和打包,生成APK文件</li><li>在单元测试阶段,CI/CD工具或平台会使用JUnit或Espresso等框架对代码进行单元测试,并生成测试报告。</li><li>在集成测试阶段,CI/CD工具或平台会使用Appium或Selenium等框架对应用进行集成测试,并生成测试报告</li><li>在部署发布阶段,CI/CD工具或平台会将APK文件上传到内部服务器或外部平台,例如蒲公英或Google Play等,并通知相关人员。</li><li>在用户反馈阶段,开发者可以通过Bugly或Firebase等工具收集用户的反馈和错误信息,并根据需要进行修复和更新</li></ol> <p>通过以上几个步骤,我们可以把以前的app构建流程从手动变为自动,而且可以通过不断以非常低的成本的重复这个过程,提高我们的项目质量,这就是cicd带给我们的自信</p> <p>今天我们来通过jenkins来实现上面的几个步骤</p> <h2 id="h2--jenkins"><a name="安装配置jenkins" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>安装配置jenkins</h2><p>本文讨论的主要是在windows环境下安装jenkins</p> <ol> <li>从jenkins官网下载对应的安装包即可</li><li>安装过程很简单但是需要提供一个账号,就像下图显示的界面,这个账号需要有权限<br><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402230959024092488.png" alt=""><br>打开开始菜单,搜索<code>本地安全策略</code>,选择<code>本地策略</code>、<code>用户权限分配</code>,在右侧的策略中找到<code>作为服务登录</code>,双击打开。点击<code>添加用户或组</code>,在输入框中填入你的账户的名字,单击<code>检查名称</code>,如果加上了下划线,则说明没有问题,如果输入的用户不存在,则会跳出来一个<code>找不到名称</code>的对话框。<br><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402230959315130881.png" alt=""></li></ol> <p>这里需要注意一点,windows家庭版默认是没有本地安全策略的,需要用一些技巧把它开启,如下:</p> <pre><code>1. 在桌面上单击右键,选择“新建”-&gt;“文本文档”。 2. 将文本文档重命名为“OpenLocalSecurityPolicy.bat”。 3. 右键单击“OpenLocalSecurityPolicy.bat”,选择“编辑”。 4. 将以下命令复制并粘贴到文本编辑器中: @echo off pushd &quot;%SystemRoot%\system32&quot; findstr /c:&quot;[SR] Cannot repair member file&quot; %windir%\logs\cbs\cbs.log &gt;%userprofile%\Desktop\sfcdetails.txt start ms-settings:windowsdefender start ms-settings:windowsupdate start ms-settings:windowsupdate-history start ms-settings:windowsupdate-options start ms-settings:storagesense start ms-settings:storagesense-diagnostics start ms-settings:storagesense-configurecleanup start ms-settings:storagesense-changehowwesave start ms-settings:storagesense-runstoragecleanupnow start ms-settings:storagesense-storageusage start ms-settings:storagesense-changestoragesavelocations start ms-settings:backup start ms-settings:backup-advancedsettings start ms-settings:backup-addalocaldriveornetworklocation start ms-settings:backup-managebackups start ms-settings:backup-moreoptions start ms-settings:dateandtime start ms-settings:regionlanguage start ms-settings:regionlanguage-languagepacks start ms-settings:regionlanguage-speech start ms-settings:regionlanguage-keyboards start ms-settings:regionlanguage-morespeechservicesonline start ms-settings:speech start ms-settings:speech-microphoneprivacysettings 5. 保存并关闭文本编辑器。 6. 双击“OpenLocalSecurityPolicy.bat”文件,以打开本地安全策略。</code></pre><ol start="3"> <li>修改默认根地址到其他盘符</li></ol> <p>默认情况下,jenkins的主目录都是在c盘,如果这样,我们使用中产生的数据都是在c盘,用过windows的都知道,数据放在c盘是很危险也是很让人不爽的一件事,我们可以通过修改jenkins的主目录方法来把数据放到其他盘</p> <p> 在成功安装了jenkins并解锁之后,我们可以配置环境变量JENKINS_HOME,地址就是我们想改的目录,</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402230959507353784.png" alt=""><br> 然后修改jenkins.xml</p> <pre><code> &lt;env name=&quot;JENKINS_HOME&quot; value=&quot;%LocalAppData%\Jenkins.jenkins&quot;/&gt;</code></pre><p>改为</p> <pre><code> &lt;env name=&quot;JENKINS_HOME&quot; value=&quot;E:\jenkins&quot;/&gt;</code></pre><ol start="4"> <li>配置常用的插件<br>在第一次启动jenkins的时候,会让你选择安装哪些插件,这时候直接选择推荐的插件就好,包含了一些常用插件,比如git等等,如下图</li></ol> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231000064448499.png" alt=""></p> <h2 id="h2--android-"><a name="配置针对于android的环境" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>配置针对于android的环境</h2><ol> <li><p>android sdk 见下图<br><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231000377101337.png" alt=""></p> </li><li><p>gradle – Dashboard -&gt; Manage Jenkins -&gt; Global Tool Configuration中的Gradle配置gradle路径即可</p> </li><li><p>jdk – Dashboard -&gt; Manage Jenkins -&gt; Global Tool Configuration中的JDK配置jdk路径即可</p> </li><li><p>git – Dashboard -&gt; Manage Jenkins -&gt; Global Tool Configuration中的Git installations配置git路径即可</p> <h2 id="h2--android-job-"><a name="配置Android的具体job信息" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>配置Android的具体job信息</h2><p>新建一个freestyle的item,在里面做以下几步:</p> </li><li><p>配置git仓库地址以及构建分支</p> </li><li><p>设置构建触发器(定时构建) – 找到构建触发器,勾选build periodically,在编辑框里按照规则设置构建时间,在某天的某个时段自动构建,比如45 9-15/2 * * 1-5,虽然可以提交一次就构建一次,但是不建议这么做。构建表达式的规则见下图,可以根据自己的需要写表达式。<br><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231000511535244.png" alt=""></p> </li><li><p>添加构建步骤,打包出apk,如下图在build step中触发<br><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231001082739227.png" alt=""></p> </li><li><p>配置构建后步骤,自动把apk包上传到ftp或者其他地方</p> <p> 在Jenkins的项目中,选择“构建”-&gt;“增加构建步骤”-&gt;“执行shell”或“执行Windows批处理命令”<br>一个上传ftp的例子</p> <pre><code>ftp -n &lt;&lt;EOF open ftp.example.com user username password cd /remote/directory put /local/file bye EOF</code></pre></li></ol> <h2 id="h2-u914Du7F6Eu90AEu4EF6u901Au77E5"><a name="配置邮件通知" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>配置邮件通知</h2><p>在构建完成之后,特别是失败的时候,我们希望收到一封邮件告诉我们构建失败了,快去处理,我们可以通过以下步骤来实现</p> <ol> <li>在Jenkins中安装Email Extension Plugin插件,可以在插件管理中搜索并安装。</li><li>在Jenkins的系统管理中,配置邮件服务器的地址,用户名,密码和端口。如果使用的是QQ邮箱或者163邮箱,还需要获取邮箱授权码并使用授权码作为密码。</li><li>在Jenkins的项目中,选择“构建后操作”-&gt;“增加构建后操作”-&gt;“Editable Email Notification”。</li><li>在邮件通知的配置中,填写收件人,抄送人,邮件主题,邮件内容等信息。可以使用一些变量来自定义邮件内容,例如BUILDSTATUS表示构建状态,BUILDS​TATUS表示构建状态, {BUILD_URL}表示构建链接等。</li></ol> <p>这里特别要注意的是,上面的配置地址和授权码需要在job的设置里面进行,在全局配置有可能发不出邮件</p> <h2 id="h2-u914Du7F6Eu5355u5143u6D4Bu8BD5u548Cu4EE3u7801u68C0u67E5"><a name="配置单元测试和代码检查" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>配置单元测试和代码检查</h2><p>我们还需要在运行前执行代码lint检查和单元测试,也需要配插件,插件名字是JUnit和Warnings Next Generation</p> <ol> <li>参考上面 配置Android的具体job信息 中的配置,添加lint和单元测试的任务</li><li>配置单元测试插件和lint插件,主要指定报告文件的位置,见下图</li></ol> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231001324315128.png" alt=""></p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231001449615058.png" alt=""></p> <ol start="3"> <li>把单元测试的结果放到邮件的附件中去,配置见下图,也可以放些别的东西</li></ol> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231001589775822.png" alt=""></p> <h2 id="h2--docker-"><a name="一劳永逸,使用docker把上面的配置做到随时随地使用" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>一劳永逸,使用docker把上面的配置做到随时随地使用</h2><p>上面的步骤完成之后,我们就能自动构建,上传apk什么的了,但是每次换台机器我们都得再配一次,想下就很累,这时候我们就可以用docker,创建一个容器,把上面这些操作放在容器里面,在新环境里面拉个镜像,创建容器跑起来,就ok啦,关于怎么用docker,就需要大家自己去搜索学习了</p> <p>最后放张图吧,jenkins真好用啊<br><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231002171779904.png" alt=""></p>

2023/4/21
阅读更多

Android使用Logger开发App本地日志记录功能

<h2 id="h2-u524Du8A00"><a name="前言" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>前言</h2><p>记录日志是一个App常用的功能,很多软件都会有,它能帮助开发者快速确定问题位置,可以说是App维护的基础。</p> <h2 id="h2-u9879u76EEu73B0u72B6"><a name="项目现状" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>项目现状</h2><p>之前我们的项目没有记录日志到本地的功能,每次排查问题都非常的费劲,需要本地能复现才能解决问题,但是有些问题并不是我们本地能复现的,比如客户环境在内网,无法访问,或者只有特定手机出现问题,这时候我们就束手无策了。</p> <h2 id="h2-u5F00u53D1u76EEu7684"><a name="开发目的" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>开发目的</h2><p>为了更好的手机问题信息,并且让用户简单的去获取日志,发给开发者,我们急需记录日志到本地的功能,并且提供开关给客户,让客户方便的进行操作。</p> <h2 id="h2-u63A2u7D22u8FC7u7A0B"><a name="探索过程" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>探索过程</h2><p>经过搜索对比,最终选取了Logger日志库作为开发的基础,这个库比较知名,出了问题也有很多资料,而且使用简单,功能全面,甚至提供了基础的记录日志到本地的能力,应该能满足我们的需求。</p> <p>这个库的地址是<a href="https://github.com/orhanobut/logger">https://github.com/orhanobut/logger</a></p> <p>但是这个库有一些小问题需要我们通过开发去补全。</p> <ul> <li>记录的目录需要改变,默认的目录都在同一个,不然有其他的app使用了Logger记录日志功能,就会混在一起<br>默认的情况是这样的<br><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231006563048760.png" alt=""><br>需要变成这样</li></ul> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231007085226542.png" alt=""></p> <ul> <li>记录的文件名字需要改变,标记日期,否则用户不知道哪个日志是最新的,不好发过来<br>莫默认情况是这样的,我们想加上日志标识,让用户看得懂</li></ul> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231007204262926.png" alt=""><br>变成这个样子</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231007354169398.png" alt=""></p> <ul> <li>需要有一个开关来控制是否开启记录,并且在一定时间之后自动停止记录,并删除超期的日志文件,这一步主要是为了app的安全性,否则通过日志会泄露一些重要的信息<br>比如这样<br><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231007594475338.png" alt=""></li><li>为了单独分析网络请求情况,我们还需要把网络请求信息单独放到一个文件里面,也就是一个logger同时往两个文件输出日志<br><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231008126922476.png" alt=""><h2 id="h2-u5177u4F53u6B65u9AA4"><a name="具体步骤" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>具体步骤</h2></li><li>集成logger,并按照官方文档实现记录日志到本地功能<br> 这一步很简单,照着官方文档做就行了</li><li>修改logger的TextDiskLogStrategy和TextFormatStrategy文件,实现日志的分文件、路径存放<br> 这一步需要把logger的源码进行一些改造,具体来说分为两个部分<br> 1.TextDiskLogStrategy – 继承FormatStrategy,在内部可以通过log方法给日志信息加一些附加信息,比如时间戳或者换行,并且定义一个handlerthread并获取handler,方便子线程使用它来往文件里写日志</li></ul> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231008255464985.png" alt=""><br> 2.TextFormatStrategy–继承自LogStrategy,使用handler进行日志的写入工作,通过what类型的不同,写入到不同的目录<br>通过tag不同,发送不同的message<br><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231008363199501.png" alt=""><br>内部还有个处理的handler,用来接收并真正执行日志文件的写入,根据不同类型写入不同文件,并且对日志使用加密算法加密,保证客户不能直接读取文件内容,确保安全性。至于如何加密,就可以根据实际情况使用不同的算法进行操作。</p> <ul> <li>写一个工具类,通过开关来控制是否进行日志的输出操作,并定时关闭开关</li></ul> <ol> <li>每次app启动的时候,扫描日志目录,判断文件创建时间,如果超过三天,就删除</li><li>使用一个shareprefrence的值来保存是否开启调试模式,并记录开启时间,每次应用启动的时候检测这两个值,并比较,如果超过时间就把debug变量还原,这样就不会继续输出日志了<h2 id="h2-u5173u4E8Eu5B89u5168u6027u7684u8003u91CF"><a name="关于安全性的考量" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>关于安全性的考量</h2>由于日志里面记录的信息有可能涉及一些敏感的东西,我们必须做好防护,上面说到的三天之后删除,24小时自动关闭记录,加密日志都是为了防止用户敏感信息泄露出去。</li></ol> <p>这个功能只能在用户知情的情况下,主动开启,并且只能使用有限的时间。</p> <h2 id="h2-u56DEu987Eu603Bu7ED3"><a name="回顾总结" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>回顾总结</h2><p>这个功能开发起来并不是非常困难,但是结合产品的过程可以引发一些思考</p> <ol> <li>为了获取信息的便捷性,这个功能不可或缺,否则很多问题光靠开发复现费时费力</li><li>为了记录的清晰,要把网络请求和logcat日志分开记录</li><li>为了用户信息的安全,需要设置开关和自动关闭功能,并且给日志加密</li></ol> <p>以前做一个功能的时候,考虑的可能就是实现这个功能,记录日志这个功能其实非常简单</p> <p>但是要考虑到安全性和易用性,做起来还是有很多的工作,写代码的过程往往就是简单功能,复杂实现</p> <p>为了堵住所有可能的口子,往往需要对一个功能有更全面的思考,开始思考这些问题,也是编程经验提升的一种象征。</p>

2022/10/12
阅读更多

一张启动图引发的思考--探索.9图原理和应用场景

<h2 id="h2-u5F15u5B50"><a name="引子" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>引子</h2><p>小u啊,我们应用启动的时候有一段白屏,不雅观,你给整个启动图上去,给,这里是资源图片</p> <p>换好了</p> <p>嗯,不错不错,咦,这个小米fold怎么显示了两个logo?</p> <p>啊?这。。。我来看看</p> <p>是这样的,activity启动图和启动背景图标一起显示了,但是启动图片又不适配fold这种狭长的屏幕,而且下半部分由于没有背景,是透明的,所以就显示了两个图标,一个启动图标,一个启动图图片图标,请看示意图</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231011585529349.png" alt=""></p> <p>那怎么解决呢?</p> <p>有几个办法,1.让启动图拉伸,覆盖下部分区域,但是会有形变,不够优雅,2.给imageview设置一个白色背景,让它不透明,3.使用android studio的。9图片制作工具,让空白部分拉伸,内容不拉伸,非常优雅。</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231012094820968.png" alt=""></p> <p>好,那你先试试.9图片吧</p> <h2 id="h2--9-"><a name=".9简介" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>.9简介</h2><p>我知道看这篇文章的朋友肯定对.9图或多或少有点了解,就简单介绍下就行<br>.9图可以通过设置来让图片某一部分拉伸 其他部分不拉伸 还可以控制图片里面显示内容的区域<br>这就是.9图的简介 够简了吧 如果想看更详细的说明可以移步谷歌官方的说明 大体上也是这意思</p> <p>这个是官方的文档<a href="https://developer.android.com/studio/write/draw9patch?hl=zh-cn">https://developer.android.com/studio/write/draw9patch?hl=zh-cn</a></p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231012253081132.png" alt=""></p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231012381597629.png" alt=""></p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231012474663033.png" alt=""></p> <p>这是.9图在三种拉伸情况下的例子,纵向,横向,横纵双向。</p> <h2 id="h2--9-"><a name=".9图制作" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>.9图制作</h2><p>网上很多文章说最简单是用as内置工具来做 但是网上文章好多都是很早的 都是老版本工具 让我们来看看这工具现在啥样</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231013534311290.png" alt=""><br>好像也不是很简单啊 这些东西做什么用的第一次看确实会很蒙,这里介绍下这个工具</p> <ul> <li><p><strong>Zoom</strong>:调整图形在绘制区域中的缩放级别。</p> </li><li><p><strong>Patch scale</strong>:调整图像在预览区域中的比例。</p> </li><li><p><strong>Show lock</strong>:当鼠标悬停在图形的不可绘制区域上时以直观方式呈现。</p> </li><li><p><strong>Show patches</strong>:预览绘制区域中的可拉伸图块(粉色为可拉伸图块),如上面的图 2 所示。</p> </li><li><p><strong>Show content</strong>:突出显示预览图像中的内容区域(紫色为允许绘制内容的区域),如图 2 所示。</p> </li><li><p><strong>Show bad patches</strong>:在拉伸时可能会在图形中产生伪影的图块区域周围添加红色边框,如图 2 所示。如果您消除所有不良图块,已拉伸图像的视觉连贯性将得以保持。<br>怎么生成.9图不是重点,网上文章不要太多,最主要就是绘制左边和上面的黑线,左边黑线控制的是哪里可以被上下拉伸,上面黑线控制哪里可以被水平拉伸,可以有多条,右边和下面的黑线控制了哪里可以放内容,不在这个区域的内容不会被显示。</p> </li></ul> <p>当然 还有很多办法生成.9图 单独工具或者在线工具都行 看个人喜好了。</p> <h2 id="h2--9-"><a name=".9图原理" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>.9图原理</h2><p>上面的介绍都很大众化 那么为啥.9图这么神奇呢?它是什么原理呢,这个好像没什么人说过,这里也简单阐述下。</p> <p>主要是四条黑线 分为两组<br>左边和上面的黑线 负责判定图片哪个部分可以被拉伸<br>右边和下面的黑线 负责确定图片内部展示内容的区域 比如这个图是个聊天气泡 内容是一堆文字</p> <p>大概就是下面这个图的样子 分成了九个区域</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231014086520717.png" alt=""></p> <p>我们把他们编号成1-9,这几个区域对应情况如下</p> <ol> <li>1 3 7 9 号区域,不在两条黑线区域内,不会被拉伸</li><li>4 6 号区域,只在左侧黑线范围内,可以被上下拉伸</li><li>2 8 号区域,只在上面黑线区域内,会被左右拉伸</li><li>5号区域,同时在上和左区域内,会被上下左右拉伸</li></ol> <p>回到刚才的问题 为啥处理之后就能控制拉伸和内容了?</p> <p>首先我们发现处理之后的图片后缀还是原来的 证明没有变成其他格式 但是名字里加上了.9</p> <p>那猜测是不是在文件里加上了一些额外信息 用.9作为标识 图片系统处理拉伸的时候就去读这些信息</p> <p>那么 到底加了什么信息呢 又是怎么判断和使用的呢 下面一一道来</p> <h3 id="h3--"><a name="首先 我们给图片加了什么信息" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>首先 我们给图片加了什么信息</h3><p>我们看看官方怎么描述.9图的</p> <blockquote> <p><code>NinePatchDrawable</code> 图形是一种可拉伸的位图,可用作视图的背景。Android 会自动调整图形的大小以适应视图的内容。NinePatch 图形是标准 PNG 图片,包含一个额外的 1 像素边框。必须使用 <code>9.png</code> 扩展名将其保存在项目的 <code>res/drawable/</code> 目录下。</p> </blockquote> <p>可以看到,.9图本质上还是png图片,但是加了1像素边框,且名字里加了个.9。</p> <p>让我们来看看什么是png图片,以及它的数据构成</p> <blockquote> <p> The PNG format provides a portable, legally unencumbered, well-compressed, well-specified standard for lossless bitmapped image files.</p> </blockquote> <blockquote> <p> A PNG file consists of a PNG signature followed by a series of chunks. This chapter defines the signature and the basic properties of chunks.</p> </blockquote> <p>png的签名块后面跟了两个数据块critical chunk和ancillary chunks,其中critical chunk包含关键数据,也是每个图片必须有的,ancillary chunks包含一些辅助信息,png如果不识别这些辅助块,可以忽略它打到向下兼容的目的。</p> <p>签名块就是一个八个字节的十六进制码,用来标识图片。</p> <p>重点来看看数据块的布局<br>名称 | 字节数 | 说明 |<br>| ———————– | —- | ——————————— |<br>| 长度(Length) | 4字节 | 指定数据块中数据区域的长度,长度不可超过(231-1)个字节 |<br>| 数据块类型码(Chunk Type Code) | 4字节 | 数据块类型码由ASCII字母(A-Z和a-z)组成的”数据块符号” |<br>| 数据块数据(Chunk Data) | 可变长度 | 存储数据块类型码指定的数据 |<br>| 循环冗余检测(CRC) | 4字节 | 存储用来检测是否文件传输有误的循环冗余码</p> <p>看到这个可以猜测,我们添加的黑色边框就是往辅助块里面加了内容,在展示的时候识别添加的信息,达到控制哪些地方伸缩的目的。</p> <p>让我们看看一张图片被弄成.9之后加了些什么内容。</p> <p>原图数据如下</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231021096032167.png" alt=""></p> <p>.9数据如下</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231021219499314.png" alt=""></p> <p>可以看到,变成.9图片之后,多了5个IDAT块,而且参数里面的长宽都增加了2像素,而且图片大小也增加了不少从80kb增加到了180kb,我们可以猜测到,这几个块里面记录的数据就是我们生成.9图时画的那几条线生成的了。</p> <h3 id="h3--"><a name="其次 这些信息在图片发生拉伸时怎么被识别和使用的" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>其次 这些信息在图片发生拉伸时怎么被识别和使用的</h3><p>这就涉及到android怎么加载一张图片的问题了,当然这些操作都是native层进行的,经过代码跟踪,我们发现有这样一个类 ** NinePatchPeeker.cpp**</p> <p>我们来看看这个类里面的readChunk方法干了些什么</p> <pre><code class="language-js">bool NinePatchPeeker::readChunk(const char tag[], const void* data, size_t length) { if (!strcmp(&quot;npTc&quot;, tag) &amp;&amp; length &gt;= sizeof(Res_png_9patch)) { Res_png_9patch* patch = (Res_png_9patch*) data; size_t patchSize = patch-&gt;serializedSize(); if (length != patchSize) { return false; } // You have to copy the data because it is owned by the png reader Res_png_9patch* patchNew = (Res_png_9patch*) malloc(patchSize); memcpy(patchNew, patch, patchSize); Res_png_9patch::deserialize(patchNew); patchNew-&gt;fileToDevice(); free(mPatch); mPatch = patchNew; mPatchSize = patchSize; } else if (!strcmp(&quot;npLb&quot;, tag) &amp;&amp; length == sizeof(int32_t) * 4) { mHasInsets = true; memcpy(&amp;mOpticalInsets, data, sizeof(int32_t) * 4); } else if (!strcmp(&quot;npOl&quot;, tag) &amp;&amp; length == 24) { // 4 int32_ts, 1 float, 1 int32_t sized byte mHasInsets = true; memcpy(&amp;mOutlineInsets, data, sizeof(int32_t) * 4); mOutlineRadius = ((const float*)data)[4]; mOutlineAlpha = ((const int32_t*)data)[5] &amp; 0xff; } return true; }</code></pre> <p>我们看到这里处理了<code>npTc</code>、<code>npLb</code>、<code>npOl</code>三个数据块,当判断有<code>npTc</code>这个数据块的时候,系统就认为这是.9图片,就会进行下一步处理。</p> <h3 id="h3--code-nptc-code-"><a name="那<code>npTc</code>这个数据又是从哪来的呢?" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>那<code>npTc</code>这个数据又是从哪来的呢?</h3><p>从上面内容我们知道已经添加了一些额外信息,我们发现官方的说明里有一句,要把.9图放在src/drawable目录下,这是因为在编译的时候,aapt会在发现图片名字符合.9图规则的时候,把四周的黑色边框提取出来,放在<code>npTc</code>数据块里面。</p> <p>接下来我们看看结构体<code>Res_png_9patch</code>里面有什么。</p> <pre><code class="language-js">/** * This chunk specifies how to split an image into segments for * scaling. * * There are J horizontal and K vertical segments. These segments divide * the image into J*K regions as follows (where J=4 and K=3): * * F0 S0 F1 S1 * +-----+----+------+-------+ * S2| 0 | 1 | 2 | 3 | * +-----+----+------+-------+ * | | | | | * | | | | | * F2| 4 | 5 | 6 | 7 | * | | | | | * | | | | | * +-----+----+------+-------+ * S3| 8 | 9 | 10 | 11 | * +-----+----+------+-------+ * * Each horizontal and vertical segment is considered to by either * stretchable (marked by the Sx labels) or fixed (marked by the Fy * labels), in the horizontal or vertical axis, respectively. In the * above example, the first is horizontal segment (F0) is fixed, the * next is stretchable and then they continue to alternate. Note that * the segment list for each axis can begin or end with a stretchable * or fixed segment. * * ... * * The colors array contains hints for each of the regions. They are * ordered according left-to-right and top-to-bottom as indicated above. * For each segment that is a solid color the array entry will contain * that color value; otherwise it will contain NO_COLOR. Segments that * are completely transparent will always have the value TRANSPARENT_COLOR. * * The PNG chunk type is &quot;npTc&quot;. */ struct alignas(uintptr_t) Res_png_9patch { int8_t wasDeserialized; uint8_t numXDivs, numYDivs, numColors; uint32_t xDivsOffset, yDivsOffset, colorsOffset; int32_t paddingLeft, paddingRight, paddingTop, paddingBottom; enum { // The 9 patch segment is not a solid color. NO_COLOR = 0x00000001, // The 9 patch segment is completely transparent. TRANSPARENT_COLOR = 0x00000000 }; ... inline int32_t* getXDivs() const { return reinterpret_cast&lt;int32_t*&gt;(reinterpret_cast&lt;uintptr_t&gt;(this) + xDivsOffset); } inline int32_t* getYDivs() const { return reinterpret_cast&lt;int32_t*&gt;(reinterpret_cast&lt;uintptr_t&gt;(this) + yDivsOffset); } inline uint32_t* getColors() const { return reinterpret_cast&lt;uint32_t*&gt;(reinterpret_cast&lt;uintptr_t&gt;(this) + colorsOffset); } }</code></pre> <p>从注释我们看出,一张图片被分成了很多个部分,s开头的代表可以拉伸的,f开头的代表不能拉伸,还有一些颜色的数据,他们共同构成了这个结构体,x,y轴上这些可以拉伸和不可拉伸的部分分别放在<code>xdiv</code>和<code>ydivs</code>数组里,同时内容的显示区域也由几个<code>padding</code>来存放,到这里,就把.9图的额外信息都给读出来了。</p> <p>既然已经获得了这些额外的信息,那么绘制的时候,系统就可以根据这些信息判断怎么拉伸一张.9图了,绘制调用了<code>NinePatchDrawable</code>,最终也会走到native层去处理这些数据。</p> <p>到此为止,制作.9和解析.9的过程就分析完毕了,一张.9图背后居然这么复杂,还是挺让人意外的,果然是学无止境啊。</p> <h2 id="h2--9-"><a name=".9图应用以及一个小坑" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>.9图应用以及一个小坑</h2><p>看完了怎么生成和它的原理 该我们去使用它了 回到开头 用我们新做的酷炫.9图解决问题吧!<br>代码张这样</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231021411068535.png" alt=""><br>效果张这样<br><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231022134758145.png" alt=""></p> <p>为什么我们的.9图没有按照预期的拉伸,而是把内容也给拉伸了?因为.9图的特性问题,<strong>它只支持拉伸</strong>,如果一个图片本身就比imageview大了,那就不会去拉伸,所以我们的.9图本身太大了,也就不存在拉伸的说法了,那肯定是无效的。<br>那我们再来尝试把图片尺寸改小,这下可以拉伸了吧,来看看效果。</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231023091761509.png" alt=""></p> <p>可以看到,下面图标被遮挡住了,<strong>但是内容也太小了吧</strong>,<strong>所以用.9图来做splash screen背景不是个好主意</strong>。</p> <p>那么,到底哪些场景可以用它呢?总结一下就是,一张图片,角落不能拉伸,其他部分可以随着内容的增多随便拉伸的情况,像这样。</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231023551647288.png" alt=""></p> <p>简单来说,就是聊天气泡一类的,如果一张图片的主体是内容要放缩的话,是不适合的,因为.9图只会拉伸特定部分,主体部分会维持原大小。</p> <p>比如我们上面做背景图这种情况。</p> <h2 id="h2-u603Bu7ED3"><a name="总结" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>总结</h2><ol> <li>.9图并不适合作为全屏展示页面,要不太大了没有拉伸效果,要不内容太小了不美观</li><li>.9图适合做一些纯色拉伸不形变的背景,比如聊天气泡和按钮背景这种</li><li>在android中,.9图得放到sr/drawable目录才行,系统才会去生成对应数据块,才可以被解析,否则就要自己去生成</li><li>普通图片由于没有额外的数据,是不能达到.9图这样的效果的</li></ol>

2022/10/8
阅读更多

跟我一起玩Paging3

<h2 id="h2--paging3"><a name="什么是Paging3" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>什么是Paging3</h2><blockquote> <p>Paging 库可帮助您加载和显示来自本地存储或网络中更大的数据集中的数据页面。此方法可让您的应用更高效地利用网络带宽和系统资源。Paging 库的组件旨在契合推荐的 Android 应用架构,流畅集成其他 Jetpack 组件,并提供一流的 Kotlin 支持。<br>以上来自安卓官网,简单点来说,就是帮你处理列表分页加载工作的一个官方框架,从名字也能看出来。而且已经到达了第三个版本,paging3。</p> </blockquote> <h2 id="h2--paging3"><a name="如何使用Paging3" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>如何使用Paging3</h2><p>使用paging3总共有以下几个步骤</p> <ol> <li>定义PagingSource – 需要在这里处理如何获取数据和如何处理页面的号码</li><li>设置 PagingData 流 – 需要把pagingsource的实例和一些其他参数传递给构造器,比如每次加载的条目数量等等</li><li>定义 RecyclerView 适配器 – 需要继承PagingDataAdapter并提供DiffUtil.ItemCallback的实例</li><li>在界面中显示分页数据 – 在页面中绑定adapter<br>通过以上几个步骤我们就可以使用paging3了,如果想看具体的步骤,可以移步<a href="https://developer.android.com/topic/libraries/architecture/paging/v3-paged-data">官网</a>,我们今天不用沉溺于具体的细节当中<h2 id="h2-paging3-"><a name="Paging3有哪些方便的地方" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>Paging3有哪些方便的地方</h2>毫无疑问,使用paging3会带给我们一些开发效率上的收益,回想一下我们以前是如何处理分页列表的,检测列表是否到达底部,使用自己持有的页数去做网络请求,把数据添加到原有的集合,更新列表。</li></ol> <p>是不是简单的回想一下就感觉到不少坑了呢,没错,我们自己做列表分页加载不仅繁琐无趣,还容易出问题。</p> <p>这些工作交给paging3之后,我们只需要关注逻辑部分就可以了,如何获取数据,想以怎样的形式展示数据,这才是我们需要关注的焦点。</p> <p>总结一下paging3的优点,替代了大量的琐碎分页加载代码,让逻辑和页面的耦合度降低,由官方的工程师保证了加载的正确性和效率。</p> <h2 id="h2-paging3-"><a name="Paging3有哪些不方便的地方" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>Paging3有哪些不方便的地方</h2><p>相信看了上面的优点之后大家都迫不及待的想去把以前的老代码干掉,换成这种看起来方便又省事的工具了。</p> <p>但是在换之前,我这里先提几条使用paging3容易遇到的坑,帮助大家综合考量是不是真的要去使用这个工具框架。</p> <ol> <li>对数据源有一定要求,有时候会提示indexoutofbounds,这种情况需要检查你提供的数据源,而不是adapter</li><li>使用paging3官方教程上需要继承PagingDataAdapter,这个问题可以通过装饰模式来解决,具体可以参考<a href="https://juejin.cn/post/6936889079393058847">这篇文章</a></li><li>paging3的pagingdataadapter虽然提供了refresh方法,但是调用之后数据源并没有获得通知,需要手动调用一次notifyDataSetChanged再去调用refresh才能让列表刷新之后继续触发分页加载,这是一个已知的bug,希望后期得到解决</li><li>paging3的列表中由于写法的原因,对于多种不同item的展示存在一定困难性,如果现在已经有多item逻辑的话,修改起来比较麻烦<h2 id="h2-paging3-"><a name="Paging3的原理分析" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>Paging3的原理分析</h2>关于paging3的原理,网上的文章已经很多了,相信大家也不想看到我再重复长篇大论,这里放一张官方架构图给大家</li></ol> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231026169812944.png" alt=""><br>简单的总结一下原理就是,数据源把数据放入pager,pager使用flow(kotlin协程提供的对标rxjava的工具)将数据传递到adapter并处理和维护页数,adapter里面再使用DiffUtil.ItemCallback结合上自身的一些api对新来的数据做比对,再进行数据的处理,最后反应到页面上。</p>

2022/10/8
阅读更多

Android中使用ASPECTJ进行用户操作路径跟踪与日志搜集

<h2 id="h2-u7F16u5199u521Du8877"><a name="编写初衷" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>编写初衷</h2><p>在Android App开发中,出现了bug和崩溃测试们就会提着手机上门,然后开发一顿操作,bug消失了,测试们又只有进行大量的操作来复现。</p> <p>这样的情况想必大家都遇到过,更极端的是线上出现了bug,虽然可以设置崩溃日志上传来收集崩溃日志,但是用户是怎么操作的,我们也只能靠猜</p> <p>为什么不能有一个工具,记录下最近打开了什么界面,点击了哪些按钮,并且记录到本地,方便开发们查看呢?于是笔者有了写这个工具的想法。</p> <p>我们来缕一缕:我们的工具要实现的几个功能</p> <ol> <li>记录activity的打开和关闭情况</li><li>记录fragment的打开和关闭情况</li><li>记录控件的点击情况</li><li>把这些日志存放到本地,方便上传和查阅</li></ol> <h2 id="h2-u7528u4EC0u4E48u6280u672Fu5B9Eu73B0"><a name="用什么技术实现" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>用什么技术实现</h2><p>有了目标,我们就可以考虑怎样实现了,大概有以下几种方式实现操作收集</p> <p>这种技术有个名称,叫做埋点,因为我们就像埋地雷一样在指定位置设置代码,当触发的时候打日志并收集</p> <ol> <li>人工手动埋点</li><li>使用特殊控件,原理与1类似,不过工作转移给了写控件的人</li><li>使用aop框架进行自动化埋点,比如aspectj或者asm</li><li>AccessibilityService配合ContentDescription进行埋点</li></ol> <p>选择哪项技术呢,我们逐项分析</p> <ol> <li>虽然最简单,但是工作量大,容错错埋漏埋,放弃</li><li>工作量巨大,使用者学习成本高,放弃</li><li>使用简单,通用,考虑使用</li><li>需要开发时添加ContentDescription并且某些机型无法开启AccessibilityService相关设置,有局限性</li></ol> <p>综合考虑之后,我们选择了使用aop的方式埋点,我们现在有两个选择,aspectj或者asm,但是有于asm过于灵活学习和使用均有一定门槛,所以我们选择简单易用的aspectj</p> <h2 id="h2-u57FAu672Cu601Du8DEF"><a name="基本思路" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>基本思路</h2><p>通过对aspectj的学习,我们发现可以对onclick方法以及生命周期方法进行切点设置,至于什么是切点,将会在后续部分解释</p> <p>日志方面,我们采用了开源项目ZLog进行记录,该框架实现了日志写到文件,以及对文件的数量和大小管理功能,比较方便<br><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231029093316222.png" alt=""></p> <p>对于最后一项,由于使用itembinding等框架的时候会遇到捕获不到具体类的情况,所以按需要进行捕捉</p> <h2 id="h2--aspectj"><a name="关于Aspectj" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>关于Aspectj</h2><p>这个工具最关键的还是怎样使用aspectj去做埋点,关于aspectj的使用网上有很多例子,这里就不赘述了,下面贴出一个简单的使用文件,大家配合注解看一下,如果还是不太明白建议先去搜一些基本使用的帖子</p> <pre><code>@Aspect//标注这个类是一个aspectj需要处理的类 class AspectJTest { @Pointcut(&quot;execution(void _internalCallbackOnClick(..))&quot;)//切点,检测返回值为void的_internalCallbackOnClick方法 fun onBindingClick() { } @Around(&quot;onBindingClick()&quot;)//在合适的时机对切点进行处理 @Throws(Throwable::class) fun onClickMethodBinding(joinPoint: ProceedingJoinPoint) { val args = joinPoint.args//获取方法参数 if (args.size &gt;= 1 &amp;&amp; args[1] is View) { val view = args[1] as View//获取view val id = view.id //处理该view或者打印日志 } joinPoint.proceed() //执行原来的代码 } }</code></pre><h2 id="h2-u9700u8981u89E3u51B3u7684u95EEu9898"><a name="需要解决的问题" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>需要解决的问题</h2><p>虽然看起来aspectj用起来很简单,但还是有一些问题需要我们处理</p> <ol> <li>怎样获取点击事件的控件id和它所在的类名</li><li>怎么获取在list中的点击事件,并获取它的位置信息<br>关于第一点,我们普通使用setOnClick设置是可以拿到的,但是当使用databinding等技术的时候情况就比较复杂了,我们如果使用以下代码,就会获取到生成类里的一个onclick文件,根本不知道哪个页面的控件被点击了<pre><code><a href="https://github.com/Around" title="&#64;Around" class="at-link">@Around</a>(&quot;onClick()&quot;) <a href="https://github.com/Throws" title="&#64;Throws" class="at-link">@Throws</a>(Throwable::class) fun onClickMethodAround(joinPoint: ProceedingJoinPoint) { //获取点击事件view对象及名称,可以对不同按钮的点击事件进行统计 val target = joinPoint.target var className = &quot;&quot; if (target != null) { className = target.javaClass.name if (className.contains(&quot;$&quot;)) { className = className.split(&quot;\\$&quot;).toTypedArray()[0] } if (className.contains(&quot;_ViewBinding&quot;)) { className = className.split(&quot;_ViewBinding&quot;).toTypedArray()[0] } }//看似可以获取,但是实际使用itembinding的时候只能拿到生成的onclick文件名,没有其他信息 joinPoint.proceed() //执行原来的代码 }</code></pre></li></ol> <p>要处理这个问题,我们需要监听生成类中的点击事件,在笔者这里,这个事件方法名叫做_internalCallbackOnClick,我们添加一个对它的切点就可以了</p> <p>对于问题2,我们可以通过获取view的父view,并判断它的类型,再通过强转来解决,代码如下</p> <pre><code> val view = args[0] as View var index = -1 if (view.parent is RecyclerView) { index = (view.parent as RecyclerView).getChildPosition(view) }</code></pre><h2 id="h2-u8F93u51FA"><a name="输出" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>输出</h2><p>当完成了埋点之后,我们就可以把日志输出到文件里了,这里我使用了一个叫ZLog的库,不过由于有一些年头了,我直接复制了文件到项目中。如果大家有兴趣可以去看看<a href="https://github.com/0xZhangKe/ZLog">ZLog代码仓库</a>并点个star</p> <h2 id="h2-u603Bu7ED3u4E0Eu5B8Cu5168u4EE3u7801"><a name="总结与完全代码" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>总结与完全代码</h2><p>解决了上面的问题,我们就可以检测到想要的信息并保存了,以后测试来找我们的时候排查bug又多了一点点线索,线上用户报bug的时候也不用胡乱猜测了,是不是感觉很有用呢?(可能并没有</p> <p>如果想看完全的代码,可以到github上的<a href="https://github.com/u3shadow/AspectTest">这里</a>来看看如果能顺便点个star就再好不过了,如果有任何问题,也可以留言讨论</p>

2022/10/8
阅读更多

彻底摆脱数据线——远程ADB调试小工具开发过程记录

<h2 id="h2-u524Du6392u63D0u793A"><a name="前排提示" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>前排提示</h2><p>本文中所描述工具只在ROOT过的设备上有效,如果不感兴趣的朋友可以点赞后退出了,也可以去github给我点个星星,源码地址在<a href="https://github.com/u3shadow/RemoteADB">这里</a></p> <h2 id="h2-u5199u5728u5F00u59CBu524D"><a name="写在开始前" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>写在开始前</h2><p>每次重启测试机都要连接usb才能开始远程adb调试,真麻烦,能不能弄一个软件点一下就能开始远程调试呢?</p> <p>如果对什么是adb远程调试不熟悉的朋友,可以搜索下adb tcpip 5555这条命令</p> <p>为了解决每次重启都要连接一次电脑的问题,经过了大量的网络搜索,终于解决了这个问题。</p> <p>本文记录了从提出问题到解决问题的全过程。</p> <h2 id="h2-u63D0u95EEu73AFu8282"><a name="提问环节" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>提问环节</h2><ul> <li><p>1.为什么要做这个工具?</p> <p> adb提供的远程调试工具不香吗,为什么还要没事找事折腾半天来做这个工具</p> </li><li><p>2.怎么实现不连接Usb启动adb的远程连接模式?</p> <p> 通过往常的经验来看,各种实现远程调试的插件都需要连接一次usb,看起来不用usb好像不太现实,但是经过调研,确实是可以的</p> </li><li><p>3.找到方法了之后,执行哪些命令才能达到我们的目的?</p> <p> 看似简单的几行命令,到了Android手机上怎么就老跑不通呢</p> <p> ** 带着上面这几个问题,我们开始今天的探索吧**</p> <h2 id="h2-u4E3Au4EC0u4E48"><a name="为什么" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>为什么</h2><p>博主有一台很棒的测试机,它叫小6,小6有很多优点,速度快,体积小,操作简单。<br>既然小6如此优秀,为什么还会有这篇博客呢,其实小6有一个致命问题,<strong>电池不耐用</strong>,毕竟已经退役成测试机了嘛。</p> <p>每次博主要远程调试的时候,小6都需要重新连一次电脑,对于一个电脑只有两个usb口的博主来说,这个过程非常的痛苦,特别是小6的电池又不耐用,每天晚上都会自动关机睡一觉。</p> <p>所以博主我想,能不能把连电脑这一个步骤转移到手机上执行呢,看着命令也挺简单的,只有一行</p> <p>感觉我上我也行嘛,说做就做,马上来试试</p> <pre><code>adb tcpip 5555</code></pre><h2 id="h2-adb-tcpip-5555-"><a name="adb tcpip 5555 做了些什么" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>adb tcpip 5555 做了些什么</h2><p>这是一条神奇的命令,手机开启调试模式,连上电脑之后,运行它,我们就可以用ip连接手机,不用数据线调试程序了</p> </li></ul> <p>经过一番查找,我们发现,这条命令发送了几条命令给手机</p> <ul> <li>1.设置service.adb.tcp.port为5555</li><li>2.重启手机上的adb</li></ul> <p>搜到这里,博主激动了起来,看起来好像很简单啊,我在手机上也这么操作不就行了,马上去找找怎么在手机上执行命令行</p> <h2 id="h2--"><a name="在手机上运行它们!" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>在手机上运行它们!</h2><p>Android上有很多命令行工具,博主随便找了一个,叫做Termux,运行了上面两条命令</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231035334291110.png" alt=""></p> <p> 报错了,not found 是什么鬼?经过一番搜索,原来是没有root权限</p> <p>** 那我们只有去root手机了**</p> <p>我们来看看root之后再运行怎么样</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231036351358846.png" alt=""></p> <p>在获取root权限之后,报错消失了,太好了!</p> <p>我们继续运行接下来两个命令</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231036477942633.png" alt=""></p> <p>运行成功了,我们现在就可以在电脑上用下面的命令来连接手机了</p> <pre><code>adb connect 手机的ip:5555 </code></pre><h2 id="h2-u5728u81EAu5DF1u7684u4EE3u7801u91CCu8FD0u884Cu547Du4EE4"><a name="在自己的代码里运行命令" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>在自己的代码里运行命令</h2><p>目前为止,一切都很顺利,我们不想用第三方的app,自己写一个程序,点一下,玩一年,不用数据线一整年</p> <p>怎么在代码里运行shell命令呢?</p> <p>经过又一番搜索,博主找到了几种方法,最终选定了下面这种,su可以替换成其他命令</p> <pre><code> Runtime.getRuntime().exec(&quot;su&quot;)</code></pre><p>让我们行动起来,运行三行命令吧<br><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231036586312556.png" alt=""></p> <p>好像没有作用,尴尬</p> <h2 id="h2-u5BF9u6BD4u4E09u65B9u5E94u7528u548Cu81EAu5DF1u7684u547Du4EE4"><a name="对比三方应用和自己的命令" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>对比三方应用和自己的命令</h2><p>通过对比,我们发现第三方的客户端的用户好像不太一样啊,经过搜索,我们发现是它调用了一下/system/bin/sh</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231037079908086.png" alt=""></p> <p>我们也得给自己的代码加上才行</p> <p>我们看看改完之后的命令</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231037177247150.png" alt=""></p> <p>运行检测一下,成功啦!</p> <h2 id="h2-u4F18u5316u4E00u4E0Bu4F7Fu7528u4F53u9A8C"><a name="优化一下使用体验" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>优化一下使用体验</h2><p>现在我们实现了基本的功能,但是不太友好,对于没有root的用户我们要提醒他,你不能用这个玩意,对于已经root的用户,操作成功了我们要提示他现在的ip是什么,用什么命令连接</p> <p>这个很简单,弄完就像下面这样,其中的ip我们要去获取手机现在的ip地址</p> <p><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231037297899249.png" alt=""><img src="https://u3blog.xyz/zb_users/upload/2024/02/202402231037399036056.png" alt=""></p> <h2 id="h2-u653Eu51FAu4EE3u7801"><a name="放出代码" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>放出代码</h2><p>前面比比了那么多,代码拿出来看看啊,别急,下面就是完整的代码</p> <pre><code>class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val wm = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager val ip: String = Formatter.formatIpAddress(wm.connectionInfo.ipAddress) AlertDialog.Builder(this).setMessage(&quot;该程序仅工作在拥有root权限设备上,如果没有root,请连接usb并使用下列命令进行远程调试\nadb tcpip 5555\nadb connect $ip:5555&quot;).setPositiveButton( &quot;确认&quot; ) { _, _ -&gt; val commands = arrayListOf( &quot;/system/bin/sh&quot;, &quot;setprop service.adb.tcp.port 5555&quot;, &quot;stop adbd&quot;, &quot;start adbd&quot; ) try { RunAsRoot(commands) } catch (e: IOException) { e.printStackTrace() } }.setCancelable(false).show() } private fun RunAsRoot(cmds: ArrayList&lt;String&gt;) { val p = Runtime.getRuntime().exec(&quot;su&quot;) val os = DataOutputStream(p.outputStream) for (tmpCmd in cmds) { os.writeBytes( &quot;&quot;&quot; $tmpCmd &quot;&quot;&quot;.trimIndent() ) } os.writeBytes(&quot;exit\n&quot;) os.flush() val wm = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager val ip: String = Formatter.formatIpAddress(wm.connectionInfo.ipAddress) AlertDialog.Builder(this).setMessage(&quot;设置完成,请在同一局域网下使用以下命令连接设备\nadb connect $ip:5555&quot;).setPositiveButton( &quot;确认&quot; ) { _, _ -&gt; finish() }.setCancelable(false).show() } }</code></pre><p><strong>相信看完上面的介绍,代码应该不难看懂,如果不想自己写,可以去<a href="https://github.com/u3shadow/RemoteADB">这里</a>找到源码,直接复制粘贴一把梭</strong></p> <h2 id="h2-u95EEu9898u56DEu987E"><a name="问题回顾" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>问题回顾</h2><p>让我们来看看最开始的几个问题,总结一下</p> <ul> <li><p>1.为什么要做这个工具?</p> <p> 我们的测试机可以重启后不连接usb启动远程调试模式啦!</p> </li><li><p>2.怎么实现不连接Usb启动adb的远程连接模式?</p> <p> 在代码里运行命令就可以了,用Runtime.getRuntime().exec(你的命令)</p> </li><li><p>3.找到方法了之后,执行哪些命令才能达到我们的目的?</p> <p> 下面这些</p> <pre><code> su /system/bin/sh setprop service.adb.tcp.port 5555 stop adbd start adbd</code></pre></li></ul> <h2 id="h2-u5199u5728u6700u540E"><a name="写在最后" class="reference-link" href="#"></a><span class="header-link octicon octicon-link"></span>写在最后</h2><p>终极问题,能不能不用root实现这套操作?不能,即使设置端口成功了,重启手机上adb的操作也需要su权限,所以不root是不行的,遗憾</p> <p>那么大家下次再见</p>

2022/10/8
阅读更多