一看就会,为 AI 编程 Agent 撸一个 MCP 服务

通过 MCP (Model Context Protocol) 让 AI 助手直接调用 ADB 命令操作 Android 设备,实现日志查看、应用安装、性能分析等自动化操作。 MCP 协议说明 MCP 是 Anthropic 推出的开放协议,用于连接 AI 助手与外部工具。MCP Server 将特定工具包装成标准化接口,让 AI 能够理解和调用。 架构如下: 1 Claude Desktop/API → MCP Server → ADB Commands → Android Device 实现步骤 初始化项目 1 2 3 4 mkdir adb_mcp cd adb_mcp npm init -y npm install @modelcontextprotocol/sdk 配置 package.json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { "name": "adb-mcp-server", "version": "1.0.0", "description": "MCP Server for Android Debug Bridge (ADB) operations", "main": "adb-mcp-server.js", "bin": { "adb-mcp-server": "./adb-mcp-server.js" }, "scripts": { "start": "node adb-mcp-server.js" }, "keywords": ["mcp", "adb", "android", "debug"], "author": "", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0" } } 核心代码实现 创建 adb-mcp-server.js: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 #!/usr/bin/env node const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js'); const { exec } = require('child_process'); const { promisify } = require('util'); const execPromise = promisify(exec); // 创建 MCP Server 实例 const server = new Server( { name: 'adb-server', version: '1.0.0' }, { capabilities: { tools: {} } } ); // 定义工具列表 const tools = [ { name: 'adb-logcat', description: 'Get Android device logs', inputSchema: { type: 'object', properties: { lines: { type: 'number', description: 'Number of log lines to retrieve', default: 100 } } } }, { name: 'adb-devices', description: 'List connected Android devices', inputSchema: { type: 'object', properties: {} } }, { name: 'adb-install', description: 'Install APK on connected device', inputSchema: { type: 'object', properties: { apkPath: { type: 'string', description: 'Path to the APK file to install' } }, required: ['apkPath'] } }, { name: 'adb-app-info', description: 'Get app package information', inputSchema: { type: 'object', properties: { packageName: { type: 'string', description: 'Android package name (e.g., com.example.app)' } }, required: ['packageName'] } }, { name: 'adb-meminfo', description: 'Get app memory usage information', inputSchema: { type: 'object', properties: { packageName: { type: 'string', description: 'Android package name (e.g., com.example.app)' } }, required: ['packageName'] } }, { name: 'adb-clear-data', description: 'Clear app data and cache', inputSchema: { type: 'object', properties: { packageName: { type: 'string', description: 'Android package name (e.g., com.example.app)' } }, required: ['packageName'] } } ]; // 处理工具列表请求 server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); // 处理工具执行请求 server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { let result; switch (name) { case 'adb-logcat': { const lines = args.lines || 100; const { stdout } = await execPromise(`adb logcat -d -t ${lines}`); result = stdout; break; } case 'adb-devices': { const { stdout } = await execPromise('adb devices -l'); result = stdout; break; } case 'adb-install': { if (!args.apkPath) { throw new Error('apkPath is required'); } const { stdout } = await execPromise(`adb install -r "${args.apkPath}"`); result = stdout; break; } case 'adb-app-info': { if (!args.packageName) { throw new Error('packageName is required'); } const { stdout } = await execPromise(`adb shell dumpsys package ${args.packageName}`); result = stdout; break; } case 'adb-meminfo': { if (!args.packageName) { throw new Error('packageName is required'); } const { stdout } = await execPromise(`adb shell dumpsys meminfo ${args.packageName}`); result = stdout; break; } case 'adb-clear-data': { if (!args.packageName) { throw new Error('packageName is required'); } const { stdout } = await execPromise(`adb shell pm clear ${args.packageName}`); result = stdout; break; } default: throw new Error(`Unknown tool: ${name}`); } return { content: [ { type: 'text', text: result } ] }; } catch (error) { return { content: [ { type: 'text', text: `Error executing ${name}: ${error.message}` } ], isError: true }; } }); // 启动服务器 async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('ADB MCP Server running on stdio'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); }); 赋予执行权限: 1 chmod +x adb-mcp-server.js 关键实现说明 Server 初始化 1 2 3 4 const server = new Server( { name: 'adb-server', version: '1.0.0' }, { capabilities: { tools: {} } } ); 声明服务器名称、版本和支持的能力类型。 工具定义 每个工具包含三个部分: name: 唯一标识符 description: 功能描述,AI 根据此判断调用时机 inputSchema: JSON Schema 格式的参数定义 示例: 1 2 3 4 5 6 7 8 9 10 11 { name: 'adb-install', description: 'Install APK on connected device', inputSchema: { type: 'object', properties: { apkPath: { type: 'string', description: 'Path to the APK file' } }, required: ['apkPath'] } } 请求处理 MCP 定义两种请求类型: ListToolsRequest - 列出所有可用工具: 1 2 3 server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); CallToolRequest - 执行具体工具: 1 2 3 4 server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // 根据 name 执行相应 ADB 命令 }); 命令执行 使用 child_process 执行 ADB 命令: 1 const { stdout } = await execPromise(`adb devices -l`); 配置方式 Claude Desktop 配置 编辑 ~/Library/Application Support/Claude/claude_desktop_config.json: 1 2 3 4 5 6 7 8 { "mcpServers": { "adb": { "command": "node", "args": ["/Users/你的用户名/Documents/self_host/adb_mcp/adb-mcp-server.js"] } } } 配置完成后重启 Claude Desktop。 Claude Code CLI 配置 使用 Claude Code 命令行工具添加 MCP Server: 1 claude -p mcp add adb node /Users/xxxx/Documents/sxxx/adb_mcp/adb-mcp-server.js 参数说明: adb: MCP Server 名称 node: 运行命令 最后是 adb-mcp-server.js 的完整路径 Gemini CLI 配置 编辑 Gemini CLI 配置文件 ~/.gemini/mcp_config.json: 1 2 3 4 5 6 7 8 { "mcpServers": { "adb": { "command": "node", "args": ["/Users/xxxx/Documents/sxxx/adb_mcp/adb-mcp-server.js"] } } } 注:Gemini CLI 的 MCP 配置可能因版本而异,建议以官方文档为准。 Copilot CLI 配置 编辑 Copilot CLI 配置文件 ~/.github-copilot/mcp_servers.json: 1 2 3 4 5 6 { "adb": { "command": "node", "args": ["/Users/xxxx/Documents/sxxx/adb_mcp/adb-mcp-server.js"] } } 注:Copilot CLI 的 MCP 配置可能因版本而异,建议以官方文档为准。 使用示例 在 Claude 中直接使用自然语言: 1 查看连接的 Android 设备 1 获取最近 50 行 logcat 日志 1 查看 com.android.chrome 的内存使用情况 1 清除 com.example.app 的数据 Claude 会自动调用对应的 MCP 工具执行操作。 应用场景 日志分析 传统方式: 1 2 adb logcat -d > log.txt # 手动搜索错误信息 使用 MCP: 1 获取最近的 logcat 日志,找出所有 ERROR 级别信息 Claude 自动执行并分析结果。 性能监控 传统方式: 1 2 adb shell dumpsys meminfo com.example.app # 手动分析输出数据 使用 MCP: 1 查看 com.example.app 的内存使用,分析是否有内存泄漏 批量操作 1 列出所有连接的设备,在每个设备上安装 /path/to/app.apk Claude 自动处理设备列表和批量安装。 扩展功能 添加截图 1 2 3 4 5 6 7 8 9 10 11 { name: 'adb-screenshot', description: 'Take a screenshot', inputSchema: { type: 'object', properties: { savePath: { type: 'string', description: 'Path to save screenshot' } }, required: ['savePath'] } } 执行命令: 1 await execPromise(`adb exec-out screencap -p > ${args.savePath}`); 添加性能监控 1 2 3 4 5 { name: 'adb-top', description: 'Get CPU usage', inputSchema: { type: 'object', properties: {} } } 添加文件传输 1 2 3 4 5 6 7 8 9 10 11 12 { name: 'adb-push', description: 'Push file to device', inputSchema: { type: 'object', properties: { localPath: { type: 'string' }, remotePath: { type: 'string' } }, required: ['localPath', 'remotePath'] } } 注意事项 权限检查 确保 ADB 已添加到系统 PATH: 1 2 which adb adb version 设备授权 使用前确认设备已连接并授权: 1 adb devices 如果显示 unauthorized,需在设备上确认 USB 调试授权。 错误处理 生产环境应添加: 详细日志记录 设备断开连接处理 命令超时机制 安全性 避免在不信任的环境使用 注意 APK 路径注入风险 考虑添加命令白名单 参考资源 MCP 官方文档 MCP SDK GitHub ADB 官方文档

2025/10/15
articleCard.readMore

同样是 Sonnet 4.5,为何 CLI 工具差距这么大

最近使用 Claude Code CLI 和 GitHub Copilot CLI 时发现,虽然两者都使用 Claude Sonnet 4.5 模型,但 Claude Code 明显更智能。本文记录性能差异的技术原因。 核心问题 同模型不等于同性能。Claude Sonnet 4.5 原生支持 200K tokens 上下文和 Extended Thinking,但 Copilot CLI 通过中间层大幅限制了这些能力。 Copilot CLI 的三大限制 1. 上下文窗口严重缩水 Claude Sonnet 4.5 原生能力: 标准:200K tokens 长上下文版本:1M tokens Copilot CLI 实际限制: 约 8K tokens 上下文窗口 官方未明确公布,但用户实测约为此值 实际影响: 1 2 3 4 5 6 7 8 9 10 11 # 场景:分析涉及 10 个文件的代码库 # Claude Code CLI (200K 上下文) # ✓ 可同时加载多个相关文件 # ✓ 保持完整的代码关系理解 # ✓ 前后一致的分析结果 # Copilot CLI (8K 上下文) # ✗ 只能保持 1-2 个文件 # ✗ 频繁遗忘先前分析内容 # ✗ 需要反复重新读取 8K tokens 约等于 6000 英文单词 或 1500 行代码。Copilot CLI 的小窗口导致频繁的上下文切换和信息丢失。 2. Extended Thinking 功能完全缺失 什么是 Extended Thinking: 允许模型进行深度推理,配置 1K-64K tokens 的”思考预算”,在复杂任务中显著提升表现。 Claude Code CLI 配置示例: 1 2 3 4 5 6 7 { "model": "claude-sonnet-4-5-20250929", "thinking": { "type": "enabled", "budget_tokens": 10000 } } Copilot CLI: 完全不支持此配置,无法启用 Extended Thinking。这是两者智能表现差异的关键原因。 3. 资源配额与超时策略 Claude Code CLI: 按 token 计费($3/百万输入,$15/百万输出) 允许长时间运行 支持检查点功能,可保存进度 Copilot CLI: Premium Request 配额制(Pro 300 次/月) 隐性”思考预算”限制 超时中断机制 比喻: Claude Code 设计用于 跑马拉松(长时间、多步骤任务),Copilot CLI 只能 跑百米(快速交互)。 架构差异 Claude Code CLI:直接访问 1 用户 → Anthropic API → Claude Sonnet 4.5 特点: 完整的 200K tokens 上下文 支持 Extended Thinking 完全参数控制 并行工具调用 无功能限制 代价: 使用 grep-only 检索(无语义索引) 大量顺序工具调用 速度慢 4-5 倍 Copilot CLI:中间层架构 1 用户 → GitHub 编排层 → Anthropic API → Claude Sonnet 4.5 中间层作用: 模型路由和切换 成本控制和配额管理 上下文窗口限制(8K) 屏蔽高级功能(Extended Thinking) GitHub 生态集成 优点: 多模型选择 GitHub 深度集成 相对稳定 代价: 模型能力被”阉割” 上下文限制严重 复杂任务表现差 实测性能对比 速度差异 重构 React 前端任务(约 15 个文件): Claude Code CLI: 18 分 20 秒 Claude Chat 手动: 4 分 30 秒 Copilot CLI: 约 90 分钟(18 分 × 5) 用户反馈:Copilot CLI 比 Claude Code 慢 5 倍以上。 大文件处理 Claude Code 限制: 1 2 # 单文件读取限制 25K tokens Error: File content (28375 tokens) exceeds maximum allowed tokens (25000) 需要使用 offset 和 limit 分块读取,导致反复工具调用。 Copilot CLI 问题: 8K tokens 窗口导致频繁分块 1000 行文件经常卡顿 有时卡死 30 分钟后超时 为什么 Claude Code “更智能” 1. 全局视野 vs 局部视野 Claude Code: 200K tokens 上下文窗口 可同时保持多个文件内容 理解代码全局关系 Copilot CLI: 8K tokens 上下文窗口 只能保持少量文件 频繁遗忘先前内容 2. 深度思考 vs 快速响应 Claude Code: Extended Thinking 允许模型”想”得更久 可以进行复杂推理 适合多步骤任务 Copilot CLI: 无 Extended Thinking 思考预算受限 倾向快速给出结果 3. 马拉松 vs 百米 Claude Code: 设计用于长时间运行 可处理复杂、多步骤任务 允许大量 token 消耗 Copilot CLI: 为快速交互优化 超出一定时限就中断 控制成本和资源 稳定性问题 Copilot CLI GitHub 社区反馈: Claude Sonnet 4 在约 5 个提示后停止 频繁 “I’m sorry but there was an error” 上下文突然丢失 官方承认是”已知的服务器端问题”。 Claude Code 使用 31% 配额时被提前限制 陷入”无限压缩循环” 读取大文件时崩溃 优化建议 Claude Code 使用语义索引: 1 2 # 安装 MCP 服务器(如 Serena MCP) # 替代低效 grep 检索 主动管理上下文: 1 2 /clear # 清理上下文 /compact # 压缩上下文 维护 CLAUDE.md: 项目规范 禁止目录 常用命令 Copilot CLI 监控配额: 1 /usage # 查看使用情况 按任务选模型: 复杂任务:Claude Sonnet 4.5 简单任务:Haiku 避免大任务接近限制时启动 总结 两者差异的根本原因: 维度 Claude Code CLI Copilot CLI 上下文窗口 200K tokens ~8K tokens Extended Thinking ✓ 支持 ✗ 不支持 资源策略 马拉松 百米 架构 直接访问 中间层限制 适用场景 复杂重构 快速迭代 Claude Code 提供完整模型能力但速度慢,像让模型”看全局、想得久”。 Copilot CLI 功能受限但集成好,像让模型”看局部、快速答”。 用户反馈的”8K tokens 限制”并非误解,而是 Copilot CLI 的真实约束。这个限制加上 Extended Thinking 缺失,是智能表现差异的核心原因。 实际使用中,许多开发者两者并用:Claude Code 处理复杂任务,Copilot CLI 处理快速交互。

2025/10/13
articleCard.readMore

定位 Android 权限声明来源

开发中经常需要排查某个权限是由哪个依赖库引入的,本文记录通过 Gradle daemon 日志快速定位权限来源的方法。 查询方法 使用以下命令在 Gradle daemon 日志中搜索权限声明: 1 grep -n -C 2 "android.permission.INTERNET" --include="*.out.log" -R ~/.gradle/daemon/ . 参数说明 -n:显示行号 -C 2:显示匹配行前后各 2 行上下文(关于 grep 上下文参数的详细用法见这篇文章) --include="*.out.log":只搜索 .out.log 文件 -R:递归搜索目录 ~/.gradle/daemon/:Gradle daemon 日志目录 daemon 日志文件说明 什么是 daemon*.out.log Gradle daemon 是 Gradle 构建系统的后台进程,用于加速构建过程。daemon-*.out.log 文件记录了 daemon 进程的详细输出信息,包括: 依赖解析过程:库的下载、合并信息 Manifest 合并日志:权限、组件的合并来源 构建任务执行:编译、打包等任务的详细输出 错误堆栈信息:构建失败时的完整日志 文件位置 1 2 3 4 5 6 7 ~/.gradle/daemon/ ├── 5.4.1/ │ ├── daemon-77407.out.log │ └── daemon-77408.out.log ├── 7.0.2/ │ └── daemon-88901.out.log └── ... 每个 Gradle 版本对应一个目录,每次 daemon 启动会生成新的日志文件,文件名中的数字为进程 ID。 结果分析 查询结果示例 1 2 /daemon/5.4.1/daemon-77407.out.log:132336:Merging uses-permission#android.permission.INTERNET with lower [net.butterflytv.utils:rtmp-client:3.0.1] AndroidManifest.xml:11:5-67 信息解读 从日志可以看出: 权限名称:android.permission.INTERNET 来源依赖:net.butterflytv.utils:rtmp-client:3.0.1 声明位置:该依赖的 AndroidManifest.xml 第 11 行 日志文件:daemon-77407.out.log 第 132336 行 处理方式 使用 <uses-permission tools:node="remove"> 在主 Manifest 中显式移除。 注意事项 daemon 日志会随着构建次数增多而变大,定期清理 ~/.gradle/daemon/ 目录 不同 Gradle 版本的日志格式可能略有差异 查询结果可能包含多个匹配项,需根据依赖关系判断实际来源 延伸阅读 Gradle Daemon 官方文档 Android Manifest 合并机制

2025/10/12
articleCard.readMore

Android 升级 targetSDK 35 解决 namespace 问题

升级 Android targetSDK 至 35 并使用 Gradle 8.0+ 后,遇到了第三方库 namespace 配置问题。 错误信息 1 2 3 4 Execution failed for task ':react-native-inappbrowser:processDebugManifest'. > A failure occurred while executing com.android.build.gradle.tasks.ProcessLibraryManifest$ProcessLibWorkAction > Setting the namespace via the package attribute in the source AndroidManifest.xml is no longer supported. Recommendation: remove package="com.proyecto26.inappbrowser" from the source AndroidManifest.xml. 或者类似错误: 1 2 3 4 5 Namespace not specified. Please specify a namespace in the module's build.gradle file like so: android { namespace 'com.example.namespace' } 原因分析 Android Gradle Plugin 8.0+ 不再支持在 AndroidManifest.xml 中通过 package 属性设置 namespace,要求在 build.gradle 中显式声明。升级 targetSDK 至 35 需要使用 Gradle 8.0+,但很多第三方库(如 react-native-inappbrowser、appcenter-analytics 等)尚未更新配置,导致构建失败。 解决方案 在项目根目录的 android/build.gradle 文件中添加以下代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 allprojects { repositories { google() mavenCentral() // 如果使用 Detox 测试框架,添加此配置 maven { url("$rootDir/../node_modules/detox/Detox-android") } } subprojects { afterEvaluate { project -> if (project.hasProperty('android')) { project.android { // 自动设置 namespace if (namespace == null || namespace.isEmpty()) { def defaultNamespace = project.group.toString().replace('.', '_') namespace = defaultNamespace } // 启用 buildConfig buildFeatures { buildConfig = true } } // 自动修复 namespace 和清理 AndroidManifest.xml project.tasks.register("fixManifestsAndNamespace") { doLast { // 1. 从 AndroidManifest.xml 提取 package 并添加到 build.gradle def buildGradleFile = file("${project.projectDir}/build.gradle") if (buildGradleFile.exists()) { def buildGradleContent = buildGradleFile.getText('UTF-8') def manifestFile = file("${project.projectDir}/src/main/AndroidManifest.xml") if (manifestFile.exists()) { def manifestContent = manifestFile.getText('UTF-8') def packageName = manifestContent.find(/package="([^"]+)"/) { match, p -> p } if (packageName && !buildGradleContent.contains("namespace")) { println "Setting namespace in ${buildGradleFile}" buildGradleContent = buildGradleContent.replaceFirst( /android\s*\{/, "android {\n namespace '${packageName}'" ) buildGradleFile.write(buildGradleContent, 'UTF-8') } } } // 2. 移除 AndroidManifest.xml 中的 package 属性 def manifests = fileTree(dir: project.projectDir, includes: ['**/AndroidManifest.xml']) manifests.each { File manifestFile -> def manifestContent = manifestFile.getText('UTF-8') if (manifestContent.contains('package=')) { println "Removing package attribute from ${manifestFile}" manifestContent = manifestContent.replaceAll(/package="[^"]*"/, '') manifestFile.write(manifestContent, 'UTF-8') } } } } // 在构建前自动执行修复 project.tasks.matching { it.name.startsWith("preBuild") }.all { dependsOn project.tasks.named("fixManifestsAndNamespace") } } } } } 说明 工作原理 此方案包含三个层次的处理: 自动设置 namespace:如果子项目未配置 namespace,自动使用 project.group 并将点号替换为下划线作为 namespace 启用 buildConfig:自动为所有子项目启用 buildConfig 特性 自动迁移配置: 从 AndroidManifest.xml 中提取 package 属性 将其写入对应的 build.gradle 作为 namespace 移除 AndroidManifest.xml 中的 package 属性 这个 task 在每次构建前(preBuild)自动执行,确保所有第三方库都符合 Gradle 8.0+ 的要求。 适用场景 React Native 项目升级 targetSDK 35 Flutter 项目升级 targetSDK 35 使用 Detox 测试框架的项目 原生 Android 项目使用旧版第三方库 任何遇到 “namespace not specified” 或 “package attribute not supported” 错误的场景 注意事项 此方案会自动修改第三方库的 build.gradle 和 AndroidManifest.xml 文件 修改仅在 node_modules 中生效,不影响源码仓库 建议在 CI/CD 中首次构建后检查修改是否正确 如果某些库已经声明了 namespace,不会被覆盖 验证 执行以下命令重新构建项目: 1 2 3 cd android ./gradlew clean ./gradlew assembleDebug 或在 React Native 项目中: 1 npx react-native run-android 构建过程中会看到类似输出: 1 2 Setting namespace in /path/to/project/android/react-native-inappbrowser/build.gradle Removing package attribute from /path/to/project/android/react-native-inappbrowser/src/main/AndroidManifest.xml 与简化方案对比 如果只需要为缺少 namespace 的库自动设置默认值,可以使用简化版: 1 2 3 4 5 6 7 8 9 10 11 subprojects { afterEvaluate { project -> if (project.hasProperty('android')) { project.android { if (namespace == null || namespace.isEmpty()) { namespace project.group.toString().replace('.', '_') } } } } } 简化方案不会修改任何文件,仅在内存中设置 namespace,但可能无法解决所有第三方库的问题。 参考 GitHub Issue #451 Stack Overflow - namespace not specified Detox Project Setup

2025/10/4
articleCard.readMore

解决 Android Studio 关闭后终端 flutter run 进程自动结束的问题

在 Flutter 开发过程中,很多开发者遇到一个困扰的问题:当使用终端运行 flutter run 命令进行开发时,一旦关闭 Android Studio 或 IntelliJ IDEA,终端中的 flutter run 进程就会自动结束,导致应用停止运行。本文将详细分析这个问题的原因并提供解决方案。 问题现象 典型场景 在终端中执行 flutter run 启动 Flutter 应用 同时打开 Android Studio 进行代码编辑 关闭 Android Studio 或 IntelliJ IDEA 终端中的 flutter run 进程自动结束,应用停止运行 影响范围 通过终端启动的 flutter run 进程 相关的热重载功能失效 调试连接中断 需要重新启动应用才能继续开发 问题原因分析 根本原因 当 Android Studio 启动时,它会自动管理 ADB(Android Debug Bridge)服务器的生命周期。默认情况下,IDE 会: 启动自己的 ADB 服务器实例 接管现有的调试连接 在退出时终止所有相关的调试进程 这种设计导致即使是通过终端独立启动的 flutter run 进程,也会因为 ADB 服务器的关闭而被迫结束。 进程依赖关系 1 终端 flutter run → ADB 连接 → Android Studio 管理的 ADB 服务器 当 Android Studio 关闭时,它管理的 ADB 服务器也会关闭,进而导致所有依赖该 ADB 连接的进程(包括终端的 flutter run)都被终止。 解决方案 配置外部 ADB 服务器管理 最有效的解决方案是让 Android Studio 使用外部手动管理的 ADB 服务器,而不是自己管理一个实例: 配置步骤 打开 Android Studio 设置(Preferences/Settings) 导航到 Build, Execution, Deployment → Debugger 找到 Android Debug Bridge (adb) 部分 在 Adb Server Lifecycle Management 中选择 Use existing manually managed server 设置 Existing ADB server port 为 5037(默认端口) 关键配置说明 Use existing manually managed server: 告诉 Android Studio 不要自己管理 ADB 服务器,而是使用外部已存在的服务器 Existing ADB server port: 指定外部 ADB 服务器的端口(通常为 5037) 这样配置后,Android Studio 不会在启动时接管 ADB 服务器,也不会在关闭时终止它,从而保证终端运行的进程不受影响。 验证配置是否生效 配置完成后,可以通过以下步骤验证: 在终端启动 flutter run 打开 Android Studio 关闭 Android Studio 检查终端中的 flutter run 是否依然运行 如果 flutter run 进程没有被终止,说明配置成功。 总结 通过配置 Android Studio 使用外部手动管理的 ADB 服务器,可以有效解决 IDE 关闭后终端 flutter run 进程自动结束的问题。这种方法的优势在于: 进程独立性:终端和 IDE 的调试进程相互独立 开发效率:无需频繁重启应用 资源优化:避免不必要的进程重启 稳定性:减少因 IDE 操作导致的调试中断 推荐所有 Flutter 开发者采用这种配置方式,特别是那些习惯在终端中运行 flutter run 的开发者。

2025/9/28
articleCard.readMore

使用 grep 查找关键字并显示上下文行

背景 排查日志时,常需要定位关键字并带上一两行上下文确认语义。grep 内建的上下文选项可以直接满足需求,不必再手动 sed -n '19,21p'。 快速示例 假设想在 app.log 中找出包含 Fatal error 的行,并且同时看到上一行与下一行: 1 grep -n -C 1 "Fatal error" app.log -n 会显示行号,便于定位。 -C 1 等价于 --context=1,表示向前向后各多带 1 行。想多看几行时调整数字即可。 输出中,命中的行以冒号分隔行号与内容,上下文行则以短横线 - 连接,快速区分重点。 控制上下文范围 grep 提供三个粒度化参数: -C <N>:两侧各 N 行,是最常用的形式。 -B <N>:只带前 N 行(Before)。 -A <N>:只带后 N 行(After)。 例如只关心关键字后面的调用栈,可使用: 1 grep -n -A 4 "NullPointerException" stacktrace.txt 再配合 -m 1(匹配一次后退出)可以缩短复杂日志的搜索时间。 与常见参数组合 -i:忽略大小写,处理大小写不一致的告警信息很方便。 -E:启用扩展正则,可直接写 grep -E "(WARN|ERROR)"。 --color=auto:高亮命中关键字,在终端阅读更直观。 将这些参数组合成 Shell 函数,后续排查直接调用。例如在 ~/.bashrc 中定义: 1 2 3 4 gctx() { local keyword="$1" file="$2" lines="${3:-1}" grep -n --color=always -C "$lines" "$keyword" "$file" } 执行 gctx "timeout" service.log 2,即可得到行号、关键字高亮、上下文行的结果。 小结 -C/-A/-B 是获取上下文的核心选项,记住数字表示行数即可。 搭配 -n、--color、-m 等参数可以提升排查效率。 如果命中结果过多,将命令与 less -R 或 fzf 管道组合,能够在终端中进行二次筛选,让排查体验更顺滑。

2025/6/24
articleCard.readMore

Android 开发中的三个常见构建错误及解决方案

最近在 Android 项目开发中遇到了几个构建错误,以下是解决方案,供遇到同样问题的开发者参考。 1. META-INF 文件冲突 错误信息 1 2 3 4 5 FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':app:mergeDebugJavaResource'. > A failure occurred while executing com.android.build.gradle.internal.tasks.MergeJavaResWorkAction > 2 files found with path 'META-INF/versions/9/OSGI-INF/MANIFEST.MF' from inputs: 解决方案 在 app/build.gradle 中添加以下配置: 1 2 3 4 5 6 7 android { packagingOptions { resources { excludes += "META-INF/versions/9/OSGI-INF/MANIFEST.MF" } } } 说明 此错误通常由多个依赖包含相同的 META-INF 文件引起,通过 excludes 排除重复文件即可解决。 2. TensorFlow Lite 库冲突 错误信息 1 2 3 Caused by: java.lang.RuntimeException: Duplicate class org.tensorflow.lite.DataType found in modules jetified-litert-api-1.0.1-runtime (com.google.ai.edge.litert:litert-api:1.0.1) and jetified-tensorflow-lite-api-2.12.0-runtime (org.tensorflow:tensorflow-lite-api:2.12.0) 解决方案 在 app/build.gradle 中添加依赖替换规则: 1 2 3 4 5 configurations.all { resolutionStrategy.dependencySubstitution { substitute module("org.tensorflow:tensorflow-lite") with module("com.google.ai.edge.litert:litert:1.0.1") } } 说明 Google 将 TensorFlow Lite 迁移到新包名 com.google.ai.edge.litert,若项目同时包含新旧包名,会导致类冲突。通过依赖替换强制使用新包解决。 3. Jetifier 与 BouncyCastle 兼容性问题 错误信息 1 2 3 4 Caused by: java.lang.RuntimeException: Failed to transform '/Users/xxxxx/.gradle/caches/modules-2/files-2.1/org.bouncycastle/bcprov-jdk18on/1.78/619aafb92dc0b4c6c c4cf86c487ca48ee2d67a8e/bcprov-jdk18on-1.78.jar' using Jetifier. Reason: IllegalArgumentException, message: Unsupported class file major version 65. 解决方案 在项目根目录的 android/gradle.properties 文件中添加: 1 android.jetifier.ignorelist=bcprov-jdk18on-1.78.jar,bcutil-jdk18on-1.78.jar 说明 BouncyCastle 1.78 版本使用 Java 21 编译(class file major version 65),而 Jetifier 不支持此版本字节码。将相关 jar 包加入 Jetifier 忽略列表可避免转换错误。 总结 以上三个问题是 Android 构建中常见的依赖冲突问题,解决思路包括: 排除重复文件 替换冲突依赖 跳过不兼容的处理 遇到类似问题时,仔细分析错误信息,通常能找到相应解决方案。

2025/6/23
articleCard.readMore

使用 flock 解决 Git `unable to read tree` 问题

背景 在 CI/CD 环境下,团队常遇到以下错误: 1 fatal: unable to read tree <SHA> 这通常是多个进程或脚本并发操作同一个 Git 仓库,导致元数据损坏或锁冲突。Git 并非为高并发本地操作设计,因此需要解决并发问题。 问题复现 在自动化脚本中,例如: 1 2 git fetch origin git checkout some-branch 如果多个任务同时执行,可能导致锁冲突或元数据损坏。 解决思路 通过加锁机制,让所有 Git 操作串行执行。flock 是一个简单高效的工具,专为这种场景设计。 flock 安装 Linux 大多数 Linux 发行版自带 flock(属于 util-linux 套件)。如果没有,可按以下方式安装: Debian/Ubuntu: 1 2 sudo apt-get update sudo apt-get install util-linux CentOS/RHEL: 1 sudo yum install util-linux Arch: 1 sudo pacman -S util-linux 安装后即可使用 flock 命令。 macOS macOS 默认不包含 flock,但可通过 Homebrew 安装兼容版本: 1 brew install flock 安装的是 Ben Noordhuis 的 flock,语法与 Linux 版本基本一致。 提示:在 CI 服务(如 GitHub Actions)中,可在步骤中提前安装 flock。 flock 用法 flock 用于在 shell 脚本中对文件加锁: 1 flock <lockfile> <command> 建议将锁文件放在 .git 目录下,避免污染业务代码目录。 实战例子 假设有一个 deploy.sh 脚本: 1 2 3 4 #!/bin/bash git fetch origin git checkout some-branch # ...more commands... 加锁后修改为: 1 2 3 4 5 6 7 8 #!/bin/bash LOCK_FILE="/path/to/your/repo/.git/deploy.lock" flock -n "$LOCK_FILE" bash <<'EOF' git fetch origin git checkout some-branch # ...more commands... EOF 或者直接锁定整个脚本: 1 flock -n /path/to/your/repo/.git/deploy.lock ./deploy.sh -n:表示拿不到锁时立即退出(可选)。 建议将锁文件放在 .git 目录下。 总结 避免并发操作同一个 Git 仓库! 使用 flock 使 Git 操作串行,防止元数据损坏。 Linux 下直接使用,macOS 通过 Homebrew 安装 flock。 锁粒度可适当放宽,确保安全优先。 本地自动化操作 Git 时,flock 是必备工具,简单高效! 如有问题,请在评论区讨论。

2025/6/15
articleCard.readMore

Could not create task ':generateDebugRFile' 问题小记

前段时间,处理一个比较旧的 flutter plugin,涉及到 Android 的部分,一顿修改后,发现无法 gradle sync 成功。 报错如下, 1 2 3 4 5 6 7 8 9 10 Could not create task ':generateDebugRFile'. Cannot use @TaskAction annotation on method IncrementalTask.taskAction$gradle_core() because interface org.gradle.api.tasks.incremental.IncrementalTaskInputs is not a valid parameter to an action method. * Try: > Run with --debug option to get more log output. > Run with --scan to get full insights. * Exception is: com.intellij.openapi.externalSystem.model.ExternalSystemException: Could not create task ':generateDebugRFile'. Cannot use @TaskAction annotation on method IncrementalTask.taskAction$gradle_core() because interface org.gradle.api.tasks.incremental.IncrementalTaskInputs is not a valid parameter to an action method. 根据分析上面的错误信息,判定与 gradle 有关,和修改的 kotlin 代码无关。 经过一些简短尝试,最终确定是 gradle 版本不匹配的问题(主要由这一句推断 because interface org.gradle.api.tasks.incremental.IncrementalTaskInputs is not a valid parameter to an action method.)。 原因与解法 原因为 Android Gradle Plugin 与 gradle 不匹配。 可以修改 gradle plugin 版本,也可以修改 gradle 版本。 修改 AGP 版本 classpath ‘com.android.tools.build:gradle:7.1.2’ // The Android Gradle plugin. 修改 gradle 版本 修改gradle/wrapper/gradle-wrapper.properties distributionUrl=https://services.gradle.org/distributions/gradle-8.0-all.zip 修改成(或者对应的gradle 版本) distributionUrl=https://services.gradle.org/distributions/gradle-7.4-all.zip 如何确定 AGP 与 gradle 对应关系 查询,请访问 这里 https://developer.android.com/build/releases/gradle-plugin?#updating-gradle

2024/11/3
articleCard.readMore

Android 模拟器实现 hosts 修改

有时候我们需要使用 Android 模拟器来 绑定一下 hosts 来实现功能的开发与验证,刚好最近遇到了这样的需求,处理完成,简单记录一下。 替换m1 实现(针对 苹果 M1 芯片才需要处理) 下载这个文件 https://github.com/google/android-emulator-m1-preview/releases/download/0.2/emulator-darwin-aarch64-0.2-engine-only.zip 解压,然后将 emulator 和 emulator-check 替换掉这里面的文件 ~/Library/Android/sdk/tools/ (原有的可以备份为 xxx_backup) 查看 avd_id 1 2 3 4 5 6 7 ~/Library/Android/sdk/tools/emulator -list-avds Pixel6ProAPI33 Pixel_3a_API_33_arm64-v8a Pixel_6_API_22 Pixel_6_API_28 Pixel_6_Pro_API_23 Pixel_6_Pro_API_30_X86 启动 avd,可写入状态 1 ~/Library/Android/sdk/tools/emulator -avd Pixel_3a_API_33_arm64-v8a -writable-system 新起终端tab 执行 adb root adb remount adb push your_hosts_on_mac /etc/hosts 验证ping 假设上面的 hosts 我们新增了 127.0.0.1 baidu.com 1 2 3 4 5 6 adb shell ping baidu.com PING baidu.com (127.0.0.1) 56(84) bytes of data. 64 bytes from baidu.com (127.0.0.1): icmp_seq=1 ttl=64 time=1.55 ms 64 bytes from baidu.com (127.0.0.1): icmp_seq=2 ttl=64 time=0.180 ms 注意: hosts 修改建议在 mac 上进行处理,然后使用adb push your_hosts_on_mac /etc/hosts 替换手机内的hosts。手机内置的 vi 很弱,可能无法编辑。 以上。

2023/3/12
articleCard.readMore

Vs Code 快速实现 重写 方法

作为一个从 Android Studio/IntelliJ 切到 VS code 的开发者,一开始会遇到各种不适应的情况。 比如快捷键不一样,使用习惯不一样等。 这里将简单记录一下 个人遇到的一些痛点,比如如何重写方法。 在 Android Studio/ IntelliJ 中,使用起来很简单,比如弹出这个菜单,选择 Override Methods 即可,实现重写 initState 方法 但是切到 Vs Code 后,发现找不到快捷键,后来经过一些摸索,还是找到了 如何快速实现方法重写的方法。 如下图,只需要输入待重写的方法的首字母,即可弹出提示。 VS Code 的方式显得会更加的简单。(后来才发现同样的方式 在 Android Studio/Intellij 也支持,Orz)

2023/2/12
articleCard.readMore

Merge(Pull) Request 推荐的标签列表

一个Merge Request 的 阶段 代码添加或修改,需要进行review 代码review结束,需要修改 重复步骤1和步骤2,直到达到可以合并的标准 角色 MR submitter 负责提交Merge Request,并针对review做修改 MR reviewer 负责review Merge Request,提出MR中存在的问题,该角色可以对应多个人 MR dispatcher 负责分发MR,修改或增加MR reviewer MR terminator 最终负责MR结果走向的人,比如合并或者关闭 注意 上述角色至少需要两个人 因权限问题, MR reviewer 可能无权限合并该MR 有哪些标签 MR:Needs Review(MR:需要Review) 当MR创建或者进行了更新,需要人员Review时,MR submitter 设置该标签 如果MR对应的内容不需要跟版,不需要现在合并的,不要增加该Lable 如果一个MR,当前的label不包含MR:Needs Review,MR reviewer 则不会review MR:Reviewed With Comments(MR:需要修改) MR reviewer 进行了review,并提出了一些评论来记录发现的问题和疑问 MR reviewer 移除 MR:Needs Review 并添加标签 MR:Reviewed With Comments MR submitter 根据提出的问题和疑问进行修改或回答,当修改完毕后,移除标签 MR:Reviewed With Comments,并设置MR:Needs Review Good to Merge(可以合并) 经过上面的来回操作,在某一点,MR达到了一个可以合并的时候,这时候需要移除前面的标签,设置成Good to Merge 设置这个标签,需要由MR reviewer 操作,而不是MR submitter 设置完这个标签后,MR不需要再更新 因MR reviewer不一定有merge权限,这一标签还是有必要的 Do Not Merge(请勿合并) 禁止合并标签,该MR可以被 review,但是不要合并进来. 适用于未来的需求,目前尚不需要加入到主分支 辅助标签,更加明确表明不需要合并当前MR 待验收 该功能没有开始进行验收(测试,UI,UE,产品,数据等) 如果当前MR包含待验收,通常不会被合并 验收中 该功能正在处于验收中(测试,UI,UE,产品,数据等) 如果当前MR包含该标签,通常不会被合并 验收通过 该功能已经通过验收(测试,UI,UE,产品,数据等) 如果当前MR包含验收通过,可以进行合并 BugFix 仅用于修复线上版本的崩溃提交时,使用 可能不上线 有些MR已经完成,但不确定什么版本引入,需要增加该标签 技术需求 非产品迭代需求 References https://engineering.invisionapp.com/post/pr-labels/

2022/11/27
articleCard.readMore

中国特惠!多平台广告屏蔽专家 AdGuard 买断仅需 119 元起

网络垃圾广告令人深恶痛绝,它不仅浪费流量、拖慢设备性能,还存在诸多安全隐患!所以很多用户选用 AdGuard 来对付这种「数字牛皮癣」。 点击[合作伙伴]专属优惠链接,前往数码荔枝下单 AdGuard 终生版最低仅需 119 元,新注册用户再享受立减 5 元优惠! 全方位广告拦截 为了应对无孔不入的垃圾广告,大家也许已经尝试过不少工具。 目前大多数的广告拦截工具仅支持电脑网页端,同时因为技术问题,往往需要逐个浏览器单独配置,最后效果还很一般 (比如面对反屏蔽脚本时无能为力)。 AdGuard 不仅内置防反屏蔽功能,同时支持 Win、Mac、安卓、iOS 四大主流系统,而且一经启用,无须额外配置即可在系统内生效! AdGuard 还可以通过屏蔽垃圾信息,净化网页和软件,让页面更快完成载入,有效节省带宽流量和系统性能。 以视频播放前的贴片广告为例,这类可恶的广告,短则几十秒,长则数分钟!非常浪费时间。 在安装 AdGuard 并启动后,用户再通过浏览器浏览视频,就能享受如同 B 站一般,无广告直接播放的畅快体验 (屏蔽功能不支持国内部分网站和 App)。 以搜索同一个常见疾病为例,在没有拦截广告时,搜索结果通常会置顶多条广告信息。在开启拦截后,搜索结果会清爽得多。 赶快点击[合作伙伴]专属优惠链接购买 AdGuard 畅享无广告的清爽体验,终生版仅需 119 元起! 隐私与保护 无论是大人还是小孩,如今每天接触互联网的时间越来越多。怎样保护个人隐私,怎样让孩子时刻远离危险网站,正困扰着无数人。 除了过滤广告,AdGuard 还提供了「隐形模式」等功能,可以帮助用户远离成千上万的在线跟踪器,保护用户的数据免受网络分析: 它能通过隐藏搜索记录、User-Agent 以及阻止网站通过 Cookie、IP 地址等信息追踪到你,实现对个人信息的有效保护。 AdGuard 还内置了 DNS 保护、危险网站警告、家长控制等功能来为用户保驾护航。当用户即将进入钓鱼、欺诈或恶意网站时,AdGuard 会向用户发出警告。 利用家长控制功能,简单操作就能建立黑白名单,设定允许和禁止访问的网站,配合阻止下载可执行文件的功能,切实避免小朋友接触到有害的网站 / 文件。 实用扩展功能 AdGuard 一大特色在于它提供了强大的可扩展性,通过快速迭代且持续不断的「进化」来切实保护你的网络安全: AdGuard 助手:网页悬浮球帮你管理过滤选项; 弹窗拦截器:拦截烦人的网页弹窗广告; Web of Trust:检测网页的信誉度来判断是否安全; AdGuard Extra:应对常规广告拦截规则无效的复杂情况…… AdGuard 是一款少见的,能同时在广告拦截和网络安全上有着亮眼表现的工具,它在扩展功能上的持续开发更显示出了十足的诚意。 如果你需要一款支持多系统 + 全局广告拦截 + 个人隐私保护 + 儿童安全浏览 + 丰富扩展功能的工具,可以选择的大概只有 AdGuard 了! 它是超级聪明的广告屏蔽专家,更是值得信赖的安全防护墙! 马上点击[合作伙伴]专属优惠链接,AdGuard 终生版最低仅需 119 元!新注册用户再享受立减 5 元优惠,心动不如行动,马上入手吧!

2022/11/27
articleCard.readMore

使用 FVM 解决 flutter 3 无法添加 uploader 问题

Flutter 3 之后,移除了 添加 uploader 的功能,这使得一些使用unpub 的用户很是苦恼,所以想要继续使用命令添加 uploader, 需要切回 flutter 2 才可以。 这里简单介绍一个很便捷的方式来处理上面的问题,就是使用 fvm 来指定 flutter 2 来执行 uploader 添加。 安装 FVM 1 2 brew tap leoafarias/fvm brew install fvm 注: 如果没有安装homebrew, 需要安装一下。 使用 如下内容保存为脚本 addUnpubUploader.sh 1 2 #!/bin/bash fvm spawn 2.10.3 packages pub uploader add $1 --verbose 执行脚本 1 bash addUnpubUploader.sh aaa@gmail.com Referrence https://fvm.app/

2022/11/20
articleCard.readMore

AAPT2 aapt2-7.2.2-7984345-osx Daemon #5: Idle daemon unexpectedly exit. This should not happen 问题解决

CI 构建机,一直有概率出现构建失败的情况,查看了日志,得到了这样的相关错误信息 1 2 3 4 5 6 7 8 9 AAPT2 aapt2-7.2.2-7984345-osx Daemon #7: Idle daemon unexpectedly exit. This should not happen. 15:32:49 [ ] AAPT2 aapt2-7.2.2-7984345-osx Daemon #8: Idle daemon unexpectedly exit. This should not happen. 15:32:49 [ ] AAPT2 aapt2-7.2.2-7984345-osx Daemon #0: shutdown 15:32:49 [ +98 ms] AAPT2 aapt2-7.2.2-7984345-osx Daemon #6: Idle daemon unexpectedly exit. This should not happen. 15:32:49 [ ] AAPT2 aapt2-7.2.2-7984345-osx Daemon #4: Idle daemon unexpectedly exit. This should not happen. 15:32:49 [ ] AAPT2 aapt2-7.2.2-7984345-osx Daemon #3: Idle daemon unexpectedly exit. This should not happen. 15:32:49 [ ] AAPT2 aapt2-7.2.2-7984345-osx Daemon #5: Idle daemon unexpectedly exit. This should not happen. 15:32:49 [ ] AAPT2 aapt2-7.2.2-7984345-osx Daemon #1: Idle daemon unexpectedly exit. This should not happen. 15:32:49 [ +499 ms] The message received from the daemon indicates that the daemon has disappeared. 解决方法 看日志感觉是 Gradle 守护进程的问题 想要既保持 Gralde 守护进程,又要解决这个问题,需要更多的时间 比较简单的方式就是 禁用 Gradle 守护进程。 命令参数传递 适用于能够直接使用gradlew 也适用于不想全局应用配置的情况 1 ./gradlew --no-daemon assembleXXX gradle.properties 适用于 无法直接配置 --no-daemon 的情况,比如 flutter 执行 Android 构建。 适用于全局配置 1 2 ## 修改这个文件 ~/.gradle/gradle.properties 如果没有,直接创建即可 org.gradle.daemon=false 注意: 这里配置完成,最好执行一下./gradlew --stop 确保不适用已有的守护进程。

2022/11/14
articleCard.readMore

git clone 使用代理,实现百倍加速

有时候我们对 github 的仓库进行 clone 的时候,会发现很慢,甚至是龟速,很不够效率。好在有一个简单且快捷的方法来倍速提升clone 效率。 我们通过检索 git 的帮助文档发现有这样的描述 If you just want to use proxy on a specified repository, don’t need on other repositories. The preferable way is the -c, –config <key=value> option when you git clone a repository. e.g. 实践起来 1 git clone https://github.com/flutter/flutter.git --config "http.proxy=192.168.1.6:1611" 上面的例子 通过 --config "http.proxy=192.168.1.6:1611" 设置代理 其中 192.168.1.6:1611 是代理的地址,需要自己搭建或者可用的 上面的配置好,再次执行,基本上可以得到百倍的提效。

2022/10/26
articleCard.readMore

Flutter 处理 Error Setter not found AsciiChar 问题

当我们进行了 flutter 升级后,有时候运行程序会发现无法编译,出现下面这样的错误 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [ ] [ +2 ms] ../../../../.pub-cache/hosted/pub.flutter-io.cn/cached_network_image-3.2.0/lib/src/image_provider/multi_image_stream_completer.dart:152:22: Warning: Operand of null-aware operation '?.' has type 'SchedulerBinding' which excludes null. [ ] [ ] - 'SchedulerBinding' is from 'package:flutter/src/scheduler/binding.dart' ('../../../../code/flutter_3/packages/flutter/lib/src/scheduler/binding.dart'). [ ] [ ] SchedulerBinding.instance?.scheduleFrameCallback(_handleAppFrame); [ ] [ ] ^ [ +402 ms] [ +414 ms] ../../../../.pub-cache/hosted/pub.flutter-io.cn/win32-2.3.1/lib/src/structs.g.dart:554:31: Error: Member not found: 'UnicodeChar'. [ ] [ ] int get UnicodeChar => Char.UnicodeChar; [ ] [ ] ^^^^^^^^^^^ [ ] [ ] ../../../../.pub-cache/hosted/pub.flutter-io.cn/win32-2.3.1/lib/src/structs.g.dart:555:38: Error: Setter not found: 'UnicodeChar'. [ ] [ ] set UnicodeChar(int value) => Char.UnicodeChar = value; [ ] [ ] ^^^^^^^^^^^ [ ] [ ] ../../../../.pub-cache/hosted/pub.flutter-io.cn/win32-2.3.1/lib/src/structs.g.dart:557:29: Error: Member not found: 'AsciiChar'. [ ] [ ] int get AsciiChar => Char.AsciiChar; [ ] [ ] ^^^^^^^^^ [ ] [ ] ../../../../.pub-cache/hosted/pub.flutter-io.cn/win32-2.3.1/lib/src/structs.g.dart:558:36: Error: Setter not found: 'AsciiChar'. [ ] [ ] set AsciiChar(int value) => Char.AsciiChar = value; [ ] [ ] ^^^^^^^^^ 对于这种问题,解决起来也比较简单,执行 1 flutter pub upgrade

2022/10/11
articleCard.readMore

iSlide 插件买两年送 180 天,每月不到 6 元钱

是不是每次在制作 PPT 时,都丝毫没有头绪?各种模板东拼西凑没有章法?你和 PPT 高手之间,其实就只差一个 iSlide。 iSlide 是一款口碑优秀的 PPT 插件,海量在线资源库,超 30 万专业模板、素材一键插入 PPT,更有一键排版等 38 种辅助功能,真正实现「让 PPT 设计简单起来」。 ​iSlide 目前正在限时优惠中,前往数码荔枝买两年送 180 天,买一年送 90 天,每月不到 6 元钱,新用户首单还能立减 5 元,非常值的价格别错过! 赶快点击[合作伙伴]专属优惠链接收下这款人见人爱的 PPT 神器吧!订阅时长可叠加,趁现在便宜快买买买~ 8 大资源库,轻松找素材 iSlide 内置 300,000+ PPT 模板,来自专业 PPT 模板设计团队,有原创正版保障,快速检索一键插入 PPT 文档。 8 大资源库为你所用,丰富的各类图标 / 图片 / 插图,可在原位置直接编辑替换,做 PPT 告别四处找素材,一个插件整合你要用的所有资源,轻松高效制作精美 PPT! 智能图表实现数据可视化 iSlide 智能图表改变传统图表的单一样式,拥有当下流行视觉化元素和风格,样式新颖更具设计感。 无需耗费大量时间精力,各种参数化调节,iSlide 让图表制作更简单直观,通俗易懂。 买两年送 180 天,买一年送 90 天,时长可无限叠加,长期需要的用户可多续费几年,每月不到 6 块钱,赶快点击购买吧! 智能对齐排版,专治强迫症 iSlide 为大家提供了丰富的设计工具,涵盖对齐、大小调整、参考线布局、内容选择、矢量、剪贴板、吸附、旋转多种常用操作。告别徒手拖动排版,一键完成各个区域对齐超方便,让幻灯片排版更规范简单。 无论是创建新文档,还是修改旧文档,都能帮你从繁琐的传统编辑中解脱,提升设计效率,呈现专业。 更细致的色彩优化 iSlide 将全球知名公司的色彩搭配方案共享上传,可以在「色彩库」中浏览并一键应用于当前的 PPT 文档,即便不懂设计,也能呈现专业。 更多辅助功能 自己做的 PPT,其实是东一页西一页复制来的?iSlide「一键优化」功能非常适合你,只需轻点一下就能统一 PPT 字体 / 段落。 还有图形矩阵 / 环形布局、图片统一裁剪、补间动画、PPT 瘦身压缩等多项功能,让你的 PPT 设计更简单。 现在点击[合作伙伴]专属优惠链接购买 iSlide,买两年送 180 天,买一年送 90 天,每月不到 6 元钱,助你变身幻灯片高手!前往数码荔枝购买的新用户,首单立减 5 元! 只有一个 iSlide 插件,还缺少 Office 中的 PowerPoint 软件?前往数码荔枝找拼车,官方牵头安全享受 Office 365 订阅,Excel、PowerPoint、Word 三件套安排上,还有 1TB OneDrive 放心用,点击立即上车!

2022/10/11
articleCard.readMore

修复 Flutter 项目中 xxx.kt: (19, 8): Redeclaration: xxxManager

在我们日常构建 Android app 包时,多少会遇到这样的问题 1 2 3 4 5 6 [ ] e: /Users/xxxxxxxxx/.pub-cache/hosted/unpub.xxxxx.com/gogogo-1.0.3/android/src/main/kotlin/com/example/gogogo/xxxManager.kt: (19, 8): Redeclaration: xxxManager [ ] e: /Users/xxxxxxxxx/.pub-cache/hosted/unpub.xxxxx.com/gogogo-1.0.3/android/src/main/kotlin/com/example/gogogo/xxxManager.kt: (79, 12): Redeclaration: gogogoResult [ ] e: /Users/xxxxxxxxx/.pub-cache/hosted/unpub.xxxxx.com/gogogo-1.0.3/android/src/main/kotlin/com/example/gogogo/xxxManager.kt: (82, 12): Redeclaration: gogogoListResult [ ] e: /Users/xxxxxxxxx/.pub-cache/hosted/unpub.xxxxx.com/gogogo-1.0.4/android/src/main/kotlin/com/example/gogogo/xxxManager.kt: (21, 8): Redeclaration: xxxManager [ ] e: /Users/xxxxxxxxx/.pub-cache/hosted/unpub.xxxxx.com/gogogo-1.0.4/android/src/main/kotlin/com/example/gogogo/xxxManager.kt: (94, 12): Redeclaration: gogogoResult [ ] e: /Users/xxxxxxxxx/.pub-cache/hosted/unpub.xxxxx.com/gogogo-1.0.4/android/src/main/kotlin/com/example/gogogo/xxxManager.kt: (97, 12): Redeclaration: gogogoListResult 问题的原因 因为 Kotlin 增量编译的bug。当你关闭增量编译时,该问题不会出现。 常用的解决办法 flutter clean 但是这个方法可能会比较重,因为它会清理掉一些多余的缓存,影响构建速度 更加轻量的解决办法 该方法只删除对应的pub 缓存,不删除其他的内容。 1 2 #!/bin/bash find ~/.pub-cache/hosted -name "$1-*" -type d -maxdepth 2 | xargs rm -rfv 保存上述命令为脚本。 比如我们出问题的 pub 名称为 gogogo 我们只需要清除它的版本缓存就可以了。 1 2 3 4 5 6 7 8 removePubCache.sh gogogo /Users/xxxxxxxxx/.pub-cache/hosted/unpub.xxxxx.com/gogogo-1.0.4/LICENSE /Users/xxxxxxxxx/.pub-cache/hosted/unpub.xxxxx.com/gogogo-1.0.4/test/gogogo_test.dart /Users/xxxxxxxxx/.pub-cache/hosted/unpub.xxxxx.com/gogogo-1.0.4/test /Users/xxxxxxxxx/.pub-cache/hosted/unpub.xxxxx.com/gogogo-1.0.4/CHANGELOG.md /Users/xxxxxxxxx/.pub-cache/hosted/unpub.xxxxx.com/gogogo-1.0.4/example/test/widget_test.dart /Users/xxxxxxxxx/.pub-cache/hosted/unpub.xxxxx.com/gogogo-1.0.4/example/test 然后执行一下 flutter pub get --verbose即可(需要)。

2022/10/4
articleCard.readMore

Mac 平台知名视频下载工具 Downie 4 买断仅需 79 元

Downie 4 是一款 Mac 上备受好评的视频下载利器,支持 Youtube、B 站、优酷、爱奇艺、腾讯视频 等 1000+ 国内外视频流媒体网站。 点击[合作伙伴]专属优惠链接,下单 Downie 4 到手价只需 79 元,新注册用户再享立减 5 元优惠! 支持 1000+ 视频网站 从网站扒视频,最烦恼的就是工具不万能。部分站点轻松下载,另外的网站却不可用,大大拖慢效率…… 在 Downie 4 下,这个问题几乎不存在。软件支持超过 1000 个视频站点,从 YouTube、TED、Vimeo,到爱奇艺、优酷、Bilibili、网易云音乐……基本上只要你用过的网站,它都能下载。 多种方式高速下载 用 Downie 下载视频很简单,软件支持多种下载方式。无需充会员也不用排队,速度毫不含糊。 Downie 4 的下载速度对比上一版提升了 6 倍之多。 拖拽链接下载 拖拽视频页面链接至 Dock 中或者菜单栏的 Downie 图标上,软件会自动创建下载任务。 粘贴链接下载 复制视频网页链接,在软件中粘贴即可自动解析网站,开启视频下载。 浏览器插件下载 打开视频页面,点击 Downie 的浏览器插件图标,就可以一键在软件内创建视频下载任务。 关键字搜索下载 直接使用 Downie 的搜索功能,查找关键词有关的视频进行下载。 自定义下载功能 除了简单的视频下载功能,Downie 还能够自定义下载选项,无论是下载 MP4 格式的视频,或者只需要视频的背景声音,软件都能实现。同时用户可自定义后期处理脚本,对下载的视频进行个性化处理。 Downie 4 对于不支持的网站还可以手动提取视频内容,完成下载后会保存设置偏好,便于下次使用。同时 Downie 4 支持在菜单栏中直接控制,操作更加简单易用。 Downie 4 带来众多出色功能,点击[合作伙伴]专属优惠链接,仅需 79 元,新注册用户再享立减 5 元优惠! Downie 下载视频的后处理 收藏达人,希望用统一的编码来保存 Downie 下载的内容? 创作达人,希望将 Downie 下载的视频转换为更适合剪辑软件处理的编码? 电影达人,希望将 Downie 下载的软件转成更适合移动设备的编码在旅途上观看? 试试与 Downie 同样优秀的 Permute 吧! Permute 和 Downie 出自同一家开发商,它是一款图片音视频格式转换软件,只需简单设置就能无缝对接 Downie,将 Downie 下载的分段视频合并为长视频。常用操作与 Downie 一样简单,拖拽倒入素材,一键即可转换。 点击[合作伙伴]专属优惠链接仅需 69 元即可带 Permute 回家。

2022/9/18
articleCard.readMore