那片树海

用心做一件工艺品


  • 首页

  • 分类

  • 归档

  • 标签

  • 关于

Xcode 26符号解析异常修复

发表于 2026-04-17 | 更新于: 2026-09-17 | 分类于 技术文章

最近遇到一个问题,在平台上有iOS App符号解析异常,但是其他App都正常

起初以为是该App代码和配置修改的问题,排查后发现是Xcode 26的兼容问题

原因

Xcode 26编译后产生的符号文件是DWARF 5格式的,平台上使用的 buglySymboliOS.jar 包只支持到 DWARF 4格式,所以就导致了解析异常

那为什么有的App能正常解析,有的App不能呢

这是因为我们采用的是组件化开发,组件是提前编译成二进制的,部分编译比较早的二进制不是使用Xcode 26编译的,在链接的时候这部分组件排到了前面

而 buglySymboliOS.jar 这个包会对 dSYM 中 DWARF 信息的第一个 CU(Compilation Unit)判断版本,如果是 DWARF 5,不支持,会直接失败;如果是 DWARF 4 及以下版本,会继续进行解析,但中间遇到 DWARF 5 的 CU 仍会解析失败

这就导致 DWARF 5 格式的 CU 排在最前面的 App 会整体解析失败,而第一个 CU 恰好是 DWARF 4 的 App 则能部分正常解析

修复方案

解析服务是一个 Java 后端服务,通过 buglySymboliOS.jar 进行解析。因为 buglySymboliOS.jar 没有后续版本升级支撑,且它是通过 Java 手动解析符号文件的,所以我需要在兼容平台现有格式逻辑的前提下替换解析实现。

最终方案是编写一个新的 jar 包,内置 llvm-nm 和 llvm-dwarfdump 工具,在调用时将内部工具释放到 Linux 服务器目录,然后使用这两个工具解析符号文件,解析后再转换为和 buglySymboliOS.jar 相同的输出格式,这样既解决了 DWARF 5 的兼容问题,又不影响平台现有逻辑。

源代码

也可以直接下载 release 包使用

libtinfo.so.5 兼容性问题

为了最大化兼容,我使用了 LLVM 18.1.8 版本,但是这个版本需要 libtinfo.so.5,而我们的服务器上安装的是 libtinfo.so.6。

为了解决这个兼容问题,我将服务器上的 libtinfo.so.6 创建了一个 libtinfo.so.5 的软链到解析工具目录,因为 libtinfo.so.6 是兼容 5 的,所以就解决了这个问题。

其他方案

  1. 通过配置将 Xcode 26 打包的符号文件修改为 DWARF 4 — 可行,但属于降级方案,不是长期可靠方案
  2. 重写一个手动解析 jar 包 — 工作量大,DWARF 5 规范复杂,维护成本高
  3. 使用 mac 环境解析 — 需要和 Jenkins 联动,过程复杂

核心网页指标-INP

发表于 2025-11-14 | 更新于: 2026-05-15 | 分类于 技术文章

什么是 INP

今天听组内同学分享,了解到一个前端概念 INP(Interaction to Next Paint),它用来衡量互动性,为了提供良好的用户体验,网页的 INP 应不超过 200 毫秒。

INP 指标通过观测网页在用户访问期间发生的所有点击、点按和键盘互动的延迟时间,评估网页对用户互动的总体响应情况。最终 INP 值是观测到的最长互动时间,离群值会被忽略。

常见的 INP 优化手段

INP 的通常优化手段是将大任务拆解成小任务,然后通过 setTimeout 调用,保障用户输入的响应。比如一个 200ms 的任务,将它拆分成 4 个 50ms 的任务来执行。

1
2
3
4
5
6
7
8
// 原来的重任务
doHeavyWork();

// 拆小之后
setTimeout(task1, 0);
setTimeout(task2, 0);
setTimeout(task3, 0);
setTimeout(task4, 0);

疑问

到这里移动端的同学是不是突然有些疑问

  1. JavaScript 是单线程的,拆分成小任务,并不能缩短整体的执行时间,这个优化有用吗
  2. 每个任务 50ms,它执行完成之后,下一个任务又会被执行,这中间的间隙能响应用户的操作吗
  3. 两个任务中间的窗口期这么短,会有用户命中吗

解答

问题 1

JavaScript 引擎是单线程,拆分成小任务,并不能缩短时长,甚至因为调度的问题,执行时长还会增加,但这类优化的目标不是“加速”,而是让用户的操作在执行任务的间隙能更快被响应。

问题 2

JavaScript 的 Event Loop 循环,执行完任务会执行其他的,这时候可以执行用户的操作,如果这个用户的操作耗时也比较长,整体 Loop 的执行也会加长,导致下一个 50ms 的任务更靠后执行

问题 3

会的,在这 50ms 执行期间,用户的操作仍能被接收,但是不会被处理,而是延后处理,所以这个优化会保证用户的操作尽快响应,减少卡顿感。

对于在第一个 50ms 用户做的操作,

  • 假如没有优化,要到 200ms 之后才会执行
  • 假如进行了优化,会在第一个 50ms 任务执行完成后执行,因为这个时候进入了下一个 Event Loop 周期,可以处理积压的用户操作了

新的问题 4 - 为什么使用定时器就可以保证每个任务之间有间隔

因为浏览器的定时器是一个单独的线程,setTimeout(task1, 0) 的任务并不会立即执行,而是把任务放入宏任务队列,等主线程空闲时才取出来执行。

浏览器规定,同一宏任务结束后,主线程会先处理 微任务队列(Promise、MutationObserver),再去处理下一个宏任务(下一个 setTimeout)。所以即使设置 0ms,也会有一个事件循环的调度间隙,因为主线程在任务之间会检查是否有用户事件或渲染需要处理。

新的问题 5 - 一次性加进去了 4 个定时任务,不会一次性全部取出执行吗

setTimeout 任务是宏任务,每次事件循环只取一个宏任务执行,即使宏任务队列里有 4 个 setTimeout,也不会一次性全部执行。事件循环每次只取队列头部的一个任务执行。

task1 执行完后,浏览器会:

  • 处理微任务队列(Promise.then 等)
  • 更新渲染(如果需要)
  • 处理用户事件(点击/输入)
  • 然后才执行 task2,依次类推

扩展 - 浏览器的其他线程

  • 主线程:执行 JS、解析 HTML、构建 DOM 树、计算样式、布局、绘制。几乎所有前端逻辑都在这里执行。
  • 合成线程:负责将多个图层合成,决定哪些区域需要重绘。加速滚动与动画渲染,使页面更流畅。
  • Worker 线程:运行在后台的 JS 线程,处理计算密集或网络请求任务,不阻塞主线程。
  • 定时器线程:负责 setTimeout / setInterval 的计时实现。
  • 事件线:管理用户输入事件(鼠标、键盘、触摸),并将其派发给主线程。
  • 动画线程: 用于协调 requestAnimationFrame 与浏览器渲染节奏。

扩展 - 和移动端的区别

  • 移动端是利用多线程,把耗时操作丢到其他线程处理,保证主线程的流畅度
  • 前端也有类似的操作,如使用 Web Worker

扩展 - 为什么是 50ms

这个数字并不是固定标准,而是经验值,因为浏览器通常以 60 帧每秒(16.7ms 每帧)刷新,如果一个任务执行时间太长,就会阻塞渲染导致掉帧,将任务控制在 50ms 以内,能有效保证交互和动画的流畅性。

扩展 - 其他优化手段

除了拆分任务外,INP 也还有其他的一些优化手段,比如说把复杂耗时的计算丢到 Web Worker 当中去处理,这是一个单独的线程,不会阻塞 JavaScript 引擎线程。

其他还有一些更细节的优化,大家也可以自己去搜索,此处就不再赘述了。

参考文章:

  • https://developers.google.com/search/blog/2023/05/introducing-inp?hl=zh-cn
  • https://web.dev/articles/vitals?hl=zh-cn
  • https://web.dev/articles/inp?hl=zh-cn
  • https://web.dev/articles/optimize-inp?hl=zh-cn

使用expect自动化你的shell操作

发表于 2024-05-11 | 更新于: 2026-09-11 | 分类于 技术文章

经常使用跳板机维护远程服务的同学可能会有这么一个苦恼,就是每次连接都需要一连串交互式操作才能连上目标机器,遇到电脑息屏或者退出终端还得再来一遍

比如维护和查询数据库,可能就需要以下操作:

  1. ssh 到远程机器
  2. 输入令牌
  3. 输入密码
  4. 查看机器列表
  5. 选择特定机器
  6. 连接特定数据库客户端
  7. 输入用户名
  8. 输入密码
  9. 选择对应数据库

完成以上 9 个步骤,我们才可以真正开始操作数据库。这个过程如果每天重复多次,实在让人头疼,而 expect 可以帮我们把这些步骤一键搞定

Expect 是什么

Expect 本质上是一个自动化交互工具,最经典的用法就是:

启动一个程序 → 等待程序输出特定内容 → 自动输入 → 再等待 → 再输入。

比如我们手工 ssh 登录一台机器,过程是这样的:

1
2
3
$ ssh root@192.168.1.100        # 发起连接
root@192.168.1.100's password: # 等待输入密码
123456 # 手动输入密码并回车

用 Expect 可以把这个过程完全自动化:

1
2
3
4
5
6
#!/usr/bin/expect

spawn ssh root@192.168.1.100 # 启动 ssh
expect "password:" # 等待出现 password:
send "123456\r" # 自动输入密码并回车
interact # 交还控制权给用户

Expect 的四个核心概念

spawn - 启动程序

1
spawn ssh root@192.168.1.100

相当于在终端执行 ssh root@192.168.1.100,但 Expect 会接管这个进程的输入输出:

1
2
3
4
5
Expect
│
├── 启动 ssh
├── 读取 ssh 输出
└── 给 ssh 发送输入

expect - 等待特定输出

1
expect "password:"

Expect 会持续读取程序输出,直到匹配到 password: 才继续执行下一条命令。比如 ssh 输出了:

1
2
Connecting to 192.168.1.100...
root@192.168.1.100's password:

匹配到 password: 后,流程才往下走。

send - 发送输入

1
send "123456\r"

向程序发送 123456 并回车(\r 相当于按下 Enter)。

interact - 交回人工控制

1
interact

自动操作完成后,把终端交还给用户,你可以继续手动操作。

串起来看

1
2
3
4
spawn ssh root@192.168.1.100
expect "password:"
send "123456\r"
interact

整个执行流程:启动 ssh → 等待 password: → 自动输入密码 → 进入服务器 → 交还给用户。这就是最经典的 Expect 用法。

示例

开头提到的数据库连接场景,9 个步骤手动操作一遍要好一会儿,用 Expect 写成脚本后,一条命令直接搞定:

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
#!/usr/bin/expect

# ---- 配置项 ----
set jump_host "user@jump-server.example.com"
set ssh_pass "my_ssh_password"
set server_id "3"
set db_host "db-server"
set db_port "3306"
set db_user "db_admin"
set db_pass "db_password"
set db_name "my_database"

# 1. ssh 到跳板机
spawn ssh $jump_host

# 2. 输入令牌(令牌是动态的,需要用户手动输入)
expect "token:"
stty -echo
send_user "请输入令牌: "
expect_user -re "(.*)\n"
send "$expect_out(1,string)\r"
stty echo

# 3. 输入密码
expect "password:"
send "$ssh_pass\r"

# 4-5. 等待机器列表,选择目标机器
expect "Please select a server:"
send "$server_id\r"

# 6. 连接数据库客户端
expect "$"
send "mysql -h $db_host -P $db_port\r"

# 7. 输入数据库用户名
expect "Enter user:"
send "$db_user\r"

# 8. 输入数据库密码
expect "Enter password:"
send "$db_pass\r"

# 9. 选择对应数据库
expect "mysql>"
send "use $db_name;\r"

# 交还控制权,开始手动操作
interact

保存为 connect_db.sh,加上执行权限后直接运行即可:

1
2
chmod +x connect_db.sh
./connect_db.sh

脚本会自动完成前面的 9 个步骤,最后通过 interact 把终端交还给你,直接进入数据库开始操作。

说明

示例的第 2 步需要用户手动输入令牌。如果本地安装了 TOTP 令牌生成工具(如 oathtool),也可以在脚本中直接调用来生成,实现完全自动化。

一行命令执行系列任务

发表于 2022-07-04 | 更新于: 2026-05-11 | 分类于 技术文章

日常开发过程中,安装命令行工具过程有时候比较繁琐,使用脚本时需要将脚本仓库克隆下来,我们怎么能够简化这些过程呢,homebrew 的安装过程提供了一种解决方案,就是直接通过 bash 执行一个远端的 sh 文件

1
2
# homebrew安装命令
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

实现方式

/bin/bash -c 表示命令会从后面紧跟的字符串读取,如果字符串后面还有内容,则作为参数。(-c: cmd)

curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh 表示请求 install.sh 的源内容

这样就可以读取远端的一个脚本,执行脚本了,在脚本中我们可以做很多自动化的工作,使用者可以在不用下载或者安装的情况执行这些动作

GitLab 实现

企业内部一般都使用的是 GitLab 托管代码,并且有权限控制

GitLab 官方推荐使用如下方式访问,文档地址

1
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fmodels%2Fkey%2Erb?ref=master"

your_access_token 在 GitLab 仓库的 setting 选项当中可以生成,而且仅展示一次,请注意保存,这个用来做身份校验。

13083 是你仓库的 Project ID,这个一般在 Project overview 就可以看到。

app%2Fmodels%2Fkey%2Erb 是 app/models/key.rb 的 urlencode,是文件路径的 urlencode。在命令中 files 后面的部分要使用文件在仓库的路径的 urlencode 形式,这样才能保证这部分是一个整体,不会在 url 解析时被误解

所以一个完整的执行远程 GitLab 文件脚本的命令如下:

1
/bin/bash -c "$(curl --header "PRIVATE-TOKEN: <your_access_token>" "https://<your_gitlab_domain>/api/v4/projects/<your_project_id>/repository/files/<your_file_path_urlencode>?ref=master)"

fastlane移除苹果两步验证

发表于 2022-03-11 | 更新于: 2026-05-11 | 分类于 技术文章

苹果的 AppleID 现在都开启了两步验证,在登录新设备或者进行一些操作时都需要提供二次验证的验证码(已登录设备、绑定手机号、绑定邮箱接收),这个功能提高了 AppleID 的安全性,但也给自动化打包带来了一些困扰,我们的打包过程会被中断,下面就简单介绍两种可以在 fastlane 中移除苹果两步验证的方案。

参考文档:http://docs.fastlane.tools/getting-started/ios/authentication/

使用应用特殊密码

点击链接 appleid.apple.com/account/manage 进入 AppleID 管理页面,点击 App 专用密码,然后生成。(此密码仅展示一次,请注意保存)

将专用密码配置在 fastlane 的环境变量当中即可

1
ENV["FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD"] = "xxxx-xxxx-xxxx-xxxx"

此种方式仅支持 upload_to_app_store、upload_to_testflight 等上传二进制动作,其他的与 App Store Connect 交互的动作不支持。

使用 App Store Connect API key(推荐)

点击 https://appstoreconnect.apple.com/access/api 进入 App Store Connect 用户与访问下面的 Keys 页面,生成一个 API key(请确保自己有权限使用这个功能,如果看不到这个 tab,也可以让账号持有者帮你创建),获得 key_id,issuer_id 并下载 API Key 的 p8 文件(此文件仅能下载一次,请注意保存)

在调用动作之前,向 Apple 请求获得 api_key,并使用即可

1
2
3
4
5
6
7
8
9
10
11
lane :release do
api_key = app_store_connect_api_key(
key_id: "XXXXXXXXX",
issuer_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
key_filepath: "./AuthKey_XXXXXXXXX.p8",
duration: 1200, # optional (maximum 1200)
in_house: false # optional but may be required if using match/sigh
)

pilot(api_key: api_key)
end

本人更推荐自动将 api_key 自动写入上下文的这种写法,fastlane 的标准动作都会去上下文当中取 api_key,写法上更方便一点

1
2
3
4
5
6
7
8
9
10
11
12
lane :release do
app_store_connect_api_key(
key_id: "XXXXXXXXX",
issuer_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
key_filepath: "./AuthKey_XXXXXXXXX.p8",
duration: 1200, # optional (maximum 1200)
in_house: false # optional but may be required if using match/sigh
)

# Automatically loads Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY]
pilot
end

Jenkins使用指南

发表于 2022-02-24 | 更新于: 2026-05-11 | 分类于 技术文章

Jenkins 官方文档:https://www.jenkins.io/doc/

安装、启动和更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装最新LTS版本(LTS:Long-term support,长期支持)
brew install jenkins-lts

# 安装特定的LTS版本
brew install jenkins-lts@YOUR_VERSION

# 启动Jenkins服务
brew services start jenkins-lts

# 重启服务
brew services restart jenkins-lts

# 更新版本
brew upgrade jenkins-lts

启动服务之后,可以用浏览器打开 http://localhost:8080 ,然后根据提示完成安装即可。

局域网内无法通过 ip 访问的问题

使用 homebrew 安装 Jenkins 之后会将 httpListenAddress 默认设置为 127.0.0.1,导致只能本机通过 localhost 访问,局域网内其他机器无法通过 ip 访问。要修复此问题,需要将如下文件当中的 httpListenAddress 配置修改为本机 ip 或者 0.0.0.0,然后重启 Jenkins 即可。

1
2
3
// 注:如果你安装的是非LTS版本,则文件名为homebrew.mxcl.jenkins.plist
~/Library/LaunchAgents/homebrew.mxcl.jenkins-lts.plist
/usr/local/opt/jenkins/homebrew.mxcl.jenkins-lts.plist

强制删除插件

在某些情况下,安装插件异常,插件既不能正常安装,也不能通过 Jenkins 提供的页面操作删除,此时可以到 Jenkins 存储插件的目录,将文件删除重启即可。

插件目录:~/.jenkins/plugins

注:需同时删除插件文件夹和.jpi 文件

远程触发

GitLab 场景推荐使用 构建触发器 -> Build when a change is pushed to GitLab 触发器(此触发器需要 Jenkins 的 GitLab 插件,默认安装,如果没有,请手动安装),该远程触发器可以获取到 GitLab 触发仓库传递过来的参数,包含仓库地址,分支,tag,推送人等信息,很实用,参数详情可以参考 gitlab-plugin 中的 Defined variables 部分。

远程调用或者脚本触发场景推荐使用 构建触发器 -> 触发远程构建,使用此触发器,需要设置一个 token,拼在 webhook url 上即可。如下是一个 curl 调用范例.

1
curl JENKINS_URL/job/app/job/user-release/build?token=sh123

如果需要传递参数,请使用如下方式,使用使用参数时,需要将参数配置在 参数化构建过程 当中。

1
curl JENKINS_URL/job/app/job/user-release/buildWithParameters?token=sh123&username=yzq

构建触发器 -> Generic Webhook Trigger 用作不传递参数的普通触发器也是一个选择,但是此触发器的参数支持较弱,仅支持 form 方式的参数提交,不太方便扩展,不太推荐。

远程触发 403 或 404 报错,提示权限问题

此问题是远程触发鉴权失败导致,需要在触发链接上添加身份信息,此处需要 Jenkins 用户的用户 ID 和 Token。用户 ID 为登录的用户名,Token 可以在 Dashboard -> 用户列表 -> 某个用户 -> 设置 -> API Token 处生成,生成后仅展示一次,注意保存。获取到用户 ID 和 Token 之后按照如下方式调用即可

1
curl https://userId:token@myjenkins.com/job/app/job/user-release/build?token=sh123

Swift混编踩坑指南

发表于 2021-09-02 | 更新于: 2026-05-11 | 分类于 技术文章

第一次在 OC 工程当中添加 Swift 文件或者在 Swift 工程当中添加 OC 文件时,Xcode 会提示你创建一个名称为{projectname}-Bridging-Header.h 的文件,点击 Create Bridging Header 创建。

Swift 调用 OC

在 {projectname}-Bridging-Header.h 导入需要调用的 OC 代码的头文件,即可在 Swift 代码中直接引用对应的类和变量

1
2
3
4
5
6
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//

#import "SHOCUtil.h"
#import "SHTest.h"
阅读全文 »

切换各种源

发表于 2020-04-28 | 更新于: 2026-05-11

我们在使用一些工具的时候因为他们的服务或者资源托管在国外的服务器上,会导致我们请求或者下载的速度非常慢,这是一种非常不好的体验,切换相关镜像源使用国内的服务可以极大改善这种情况。

Homebrew

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 替换brew.git:
cd "$(brew --repo)"
git remote set-url origin https://mirrors.ustc.edu.cn/brew.git

# 替换homebrew-core.git:
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-core.git

# 重置brew.git:
cd "$(brew --repo)"
git remote set-url origin https://github.com/Homebrew/brew.git

# 重置homebrew-core.git:
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://github.com/Homebrew/homebrew-core.git
阅读全文 »

macOS 10.15.4下Sketch等三方软件无法打开

发表于 2020-04-28 | 更新于: 2026-05-11

电脑更新 macOS Catalina 10.15.4 之后,原来安装的 Sketch、CleanMyMac 等第三方软件打不开了,直接崩溃

解决方案:打开终端,执行如下命令

1
2
xattr -cr /Applications/xxx.app
codesign --force --deep --sign - /Applications/xxx.app

YCode工具集

发表于 2019-09-10 | 更新于: 2026-05-11

最近在新的团队中各个团队的不同的代码风格对协作造成了较大的影响,然后就想除了人工方式能不能用工具的方式来使大家的代码风格统一,于是就写了 YCode 这个工具集。

YCode 目前只提供代码段功能,后续会加入代码模板功能,如果大家有什么想法或者想要的功能,也可以给我提 issue 或者发邮件,我会慢慢完善的。如果你觉得 YCode 确实对你的工作有所帮助,能点个 star 或者推荐给其他人就更好了,多谢大家。

YCode 地址
代码段仓库地址

用法

Xcode 代码段

安装之后就可以使用已经写好的代码段了,比如你在 Xcode 当中输入 yLazyUILabel,就可以得到现成的一整段代码,具体提供的片段可以在代码段仓库地址或者 Xcode 当中查看。

12…7
树海

树海

61 日志
1 分类
21 标签
RSS
GitHub Weibo
© 2026 树海
由 Hexo 强力驱动
|
主题 — NexT.Muse