0%

解决 Apple Silicon (M1) 上 LoadError - dlsym(0x7fbb17932d30, Init_ffi_c): symbol not found - /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.3/lib/ffi_c.bundle 问题。

首先通过 file /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.3/lib/ffi_c.bundle 查看这个文件的架构:

1
2
3
/Library/Ruby/Gems/2.6.0/gems/ffi-1.15.3/lib/ffi_c.bundle: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit bundle x86_64] [arm64e:Mach-O 64-bit bundle arm64e]
/Library/Ruby/Gems/2.6.0/gems/ffi-1.15.3/lib/ffi_c.bundle (for architecture x86_64): Mach-O 64-bit bundle x86_64
/Library/Ruby/Gems/2.6.0/gems/ffi-1.15.3/lib/ffi_c.bundle (for architecture arm64e): Mach-O 64-bit bundle arm64e

上面的信息有x86_64和arm64e,虽然包含了arm64e,但是此arm64e不是M1 对应的arm64。也就是说架构是不对的。

那我们接着往下看,先查询下系统ruby的版本 ruby --version

1
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.arm64e-darwin20]

版本和时间都有,2019-04-16 的版本,但是 M1 是 2020 年出来的,不一定适配了新的架构。

那我们就必须得确认当前ruby的真实架构。我们可以通过一段代码获取 arch.rb

1
2
3
4
5
6
7
8
9
require 'rbconfig'

OSVERSION = RbConfig::CONFIG['host_os']
CPU = RbConfig::CONFIG['host_cpu']
ARCH = RbConfig::CONFIG['arch']

puts "OS: #{OSVERSION}"
puts "CPU: #{CPU}"
puts "Arch: #{ARCH}"

执行 ruby arch.rb

1
2
3
OS: darwin20
CPU: x86_64
Arch: universal-darwin20

诡异的一幕出现了,CPU架构却是x86_64而不是arm64,也就是说造成ffi无法运行的原因是ruby版本不支持 arm64。

问题找到了那接下来这个问题就好解决了,安装最新的ruby版本。

可以通过 brew install ruby,也可以通过 rbenv 或者 rvm 来安装。

我使用 brew install ruby 来安装最新的版本。

通过 brew 安装的 ruby 并不会生效,需要添加到环境变量中 echo 'export PATH="/opt/homebrew/opt/ruby/bin:$PATH"' >> ~/.zshrc

为了验证是否有效我们先测试下新版本的架构,先设置当前shell的环境变量 export PATH="/opt/homebrew/opt/ruby/bin:$PATH"

执行 ruby --version

1
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [arm64-darwin20]

执行 ruby arch.rb

1
2
3
OS: darwin20
CPU: arm64
Arch: arm64-darwin20

CPU 架构正确,继续安装 CocoaPods gem install cocoapods

成功!!!

最近在做 iOS 14 的 WebKit API 适配遇到一些问题记录下。

Fatal error: Bug in WebKit: Received neither result or failure.: file WebKit/WebKitSwiftOverlay.swift, line 66

问题现象

1
2
3
webView.evaluateJavaScript("console.log('Hello World')", in: nil, in: .page) { result in
print(result)
}

在 iOS 14.0 的版本中执行以上的代码会产生crash Fatal error: Bug in WebKit: Received neither result or failure.: file WebKit/WebKitSwiftOverlay.swift, line 66 ,但是在最新版 14.5 不会崩溃。

定位问题

在 WebKit 官方代码WebKit/WebKit 中找到了这段产生crash的代码。

1
2
3
4
5
6
7
8
9
10
11
func makeResultHandler<Success, Failure>(_ handler: @escaping (Result<Success, Failure>) -> Void) -> (Success?, Failure?) -> Void {
return { success, failure in
if let success = success {
handler(.success(success))
} else if let failure = failure {
handler(.failure(failure))
} else {
fatalError("Bug in WebKit: Received neither result or failure.")
}
}
}

查看源码可知,当 JavaScript 执行没有返回值,也没有错误的时候就会产生fatalError,比如执行console.log('Hello World')

但是在 WebKit 的 main 分支最新代码中已经没有这段代码了,取而代之的是使用 ObjCBlockConversion.boxingNilAsAnyForCompatibility

为了找到是在哪次commit中修复了这个问题,通过查询WebKitSwiftOverlay.swift文件的git修改记录,找到有这么一次commit,里面记录了这个crash修复的过程,有兴趣的可以去看看。

https://github.com/WebKit/WebKit/commit/534def4b8414c5ca1bf3712272ad24eaf271b134#diff-93ac6a04946f8372bfaec900fdcab57ef95932e9f30f45e7115a9ea807b82e6c

问题已经找到,那就需要确定是在哪个版本的 iOS 中修复了这个问题。

首先需要找到 iOS 版本对应的 WebKit 版本。

在 Wikipedia 上有维护 Safari version history Safiri 版本和对应的 WebKit 版本,但是遗憾的是最新版本的 iOS 14 还没有该记录。

那接下来如何找到 WebKit 版本呢?

需要分为两部分,首先先确定Xcode里集成的 iOS 编译库,再确定老版本的 iOS,老版本的iOS可以从Xcode 的 Components 下载对应版本的Simulator。

以 Xcode 12.5 为例。在 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/ 路径下找到 iOS.simruntime,再找到 WebKit Contents/Resources/RuntimeRoot/System/Library/Frameworks/WebKit.framework/WebKit

完整路径为:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/WebKit.framework

通过 otool -L 命令找到对应的WebKit 版本。

1
2
3
4
$ otool -L WebKit | grep WebKit                                      [23:42:54]
WebKit:
/System/Library/Frameworks/WebKit.framework/WebKit (compatibility version 1.0.0, current version 611.1.21)
/System/Library/PrivateFrameworks/WebKitLegacy.framework/WebKitLegacy (compatibility version 1.0.0, current version 611.1.21, reexport)

其中 611.1.21 就是对应的 WebKit 的版本。

Xcode 通过 Components 下载的 Simulator 版本路径在 /Library/Developer/CoreSimulator/Profiles/Runtimes 下,用同样的方式确定 iOS 版本的 WebKit 版本。

iOS 版本 WebKit 版本
iOS 14.5 611.1.21
iOS 14.4 610.4.3
iOS 14.3 610.3.7
iOS 14.2 610.2.11

最后在 WebKit/Webkit 上确认对应的修复版本,最终确认修复的版本为 iOS 14.3。

总结

evaluateJavaScript 方法做兼容,不能直接使用 #available(iOS 14.0, *) 适配。

1
2
3
4
5
6
7
8
9
if #available(iOS 14.3, *) {
webView.evaluateJavaScript("console.log('Hello World')", in: nil, in: .page) { result in
print(result)
}
} else {
webView.evaluateJavaScript("console.log('Hello World')") { value, error in
print(value)
}
}

准备工作

Mac OS

Safari 开启调试模式

依次选择 偏好设置 > 高级 > 在菜单栏中显示“开发”菜单

image-20200908154443157

iOS

Safari 开启调试模式

要远程调试 iOS Safari ,必须启用 Web 检查 功能,打开 iPhone 依次进入 设置 > Safari > 高级 > Web 检查 > 启用

IMG_19800580398B-1

开发调试

启动 Web Inspector

  1. iPhone 使用 Safari 浏览器打开要调试的页面,或者 App 里打开要调试的页面
  2. Mac 打开 Safari 浏览器调试(菜单栏 > 开发 > iPhone 设备名 -> 选择调试页面)
  3. 在弹出的 Safari Developer Tools 中调试

调试菜单

img

Resources

这个菜单用来显示当前网页中加载的资源,比如 HTML、JS、CSS、图片、字体等资源文件,并且可以对 JS 代码添加断点来调试代码。

断点

Inspector 中的断点调试和 Xocde 的大同小异。

格式化代码

web 页面中的 JS、CSS、HTML 文件大多数都经过了压缩处理,以前 inspector 并不支持 HTML,这次可以格式化 HTML 文件了:
img

Local overrides

如果你想调试某个文件的时候,通常把改动好的代码推动服务端,然后通过浏览器访问,查看效果,整个过程可能会耗费很长时间。Local overrides 提供了一种能力,可以替换当前页面所加载的文件,这样只需要修改本地文件即可,当页面加载的时候会直接使用本地的文件,达到快速调试的作用。更多内容。
img

Bootstrap Script

Bootstrap Script 也叫引导程序,通常是程序执行时第一个要执行的文件,在 Inspector 中可以创建一个这样的文件用来作为调试工具使用,比如替换某个函数的实现,给某个函数增加特殊的调试语句。在调试的时候,很多 JS 函数都经过了压缩处理,可通过这种方式把压缩的函数替换成未被压缩的函数,方便调试。
更多内容

Timelines

Timelines 用来分享各种功能的加载时长。

Sotrage

storage 用来显示缓存的数据,比如 Local Storage、Session Storage、Indexed DataBase。

Layers

Layers 主要用来显示页面的绘制、布局。
img

Console

console 就是打印日志的地方,也可以执行 JavaScript 代码。Console 的界面如下:

img

第一次使用 Gitalk 时,之前的文章的评论都需要初始化一下,如果文章多的话,挺麻烦的。不过,有些博客有提供接口获取博客上所有文章的相关信息,那其实就可以通过脚本来完成之前文章的评论初始化。下面是一些已经写好的脚本,可以直接使用或参考。

Gitalk 官方的 WiKi 里记录的方法年久失修,已经不能使用,我重新整理了一份。

获得权限

在使用该脚本之前首先要在 GitHub 创建一个新的 Personal access tokens,选择 Generate new token 后,在当前的页面中为 Token 添加所有 Repo 的权限。

自动化脚本

安装脚本依赖库

1
$ gem install faraday activesupport sitemap-parser nokogiri

使用 sitemap 文件

找到博客对应的 sitemap 文件,例如 https://chaosky.tech/sitemap.xml。

使用脚本

在任意目录创建 comment.rb,将下面的代码粘贴到文件中:

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
require 'open-uri'
require 'faraday'
require 'active_support'
require 'active_support/core_ext'
require 'sitemap-parser'
require 'digest'
require 'nokogiri'

username = "chaoskyx" # GitHub 用户名
token = "xxxxxx" # GitHub Token
repo_name = "chaoskyx.github.io" # 存放 issues
sitemap_url = "https://chaosky.tech/sitemap.xml" # sitemap
kind = "Gitalk"

sitemap = SitemapParser.new sitemap_url
urls = sitemap.to_a

conn = Faraday.new(:url => "https://api.github.com/repos/#{username}/#{repo_name}/issues") do |conn|
conn.basic_auth(username, token)
conn.adapter Faraday.default_adapter
end

urls.each_with_index do |url, index|
id = Digest::MD5.hexdigest URI(url).path
response = conn.get do |req|
req.params["labels"] = [kind, id].join(',')
req.headers['Content-Type'] = 'application/json'
end
response_hash = JSON.load(response.body)

if response_hash.count == 0
document = Nokogiri::HTML(open(url))
title = document.xpath("//head/title/text()").to_s
desc = document.xpath("//head/meta[@name='description']/@content").to_s
body = url + "\n\n" + desc
puts title
response = conn.post do |req|
req.body = { body: body, labels: [kind, id], title: title }.to_json
end
puts response.body
end
sleep 15 if index % 20 == 0
end

在这里有 5 个配置项,分别是 GitHub 用户名、在上一步获得的 Token、存放 issues 的仓库、sitemap 的地址以及最后你在博客中使用了哪个评论插件,不同的插件拥有标签,可以选择 “Gitalk” 或者 “gitment”。

运行脚本

1
$ ruby comment.rb

参考链接

  1. https://github.com/gitalk/gitalk/wiki/评论初始化
  2. https://draveness.me/git-comments-initialize/

LLVM Module

A module is a single unit of code distribution—a framework or application that is built and shipped as a single unit and that can be imported by another module with Swift’s import keyword.

Each build target (such as an app bundle or framework) in Xcode is treated as a separate module in Swift. If you group together aspects of your app’s code as a stand-alone framework—perhaps to encapsulate and reuse that code across multiple applications—then everything you define within that framework will be part of a separate module when it’s imported and used within an app, or when it’s used within another framework.

As the docs indicate, the module is an application or a framework (library). If you create a project with classes A and B, they are part of the same module. Any other class in the same project can inherit from those classes. If you however import that project to another project, classes from that another project won’t be able to subclass A nor B. For that you would have to add open indicator before their declarations.

Basically, if you work on a single app then you are working in one single module and unless declared as private or fileprivate, the classes can subclass each other.

Module

Module 是一种集成库的方式,在 Module 出现之前,开发者需要在引入库文件的同时引入需要使用的头文件,以保证编译的正常进行。但是每次引入库的时候都要导入一堆文件,看起来并不优雅。Module 和 Framework 的出现让开发者极大程度上告别了这些不优雅的工作。简单说就是用树形的结构化描述来取代以往的平坦式 #include, 例如传统的 #include <stdio.h> 现在变成了 import std.io

主要好处有:

  1. 语义上完整描述了一个框架的作用
  2. 提高编译时可扩展性,只编译或 include 一次。避免头文件多次引用,只解析一次头文件甚至不需要解析(类似预编译头文件)
  3. 减少碎片化,每个 module 只处理一次,环境的变化不会导致不一致
  4. 对工具友好,工具(语言编译器)可以获取更多关于 module 的信息,比如链接库,比如语言是 C++ 还是 C

modulemap 文件

module.map 文件就是对一个框架,一个库的所有头文件的结构化描述。通过这个描述,桥接了新语言特性和老的头文件。默认文件名是 module.modulemap,modulemap 其实是为了兼容老标准,不过现在 Xcode 里的还都是这个文件名,相信以后会改成新名字。

文件的内容以 Module Map Language 描述,大概语法如下:

1
2
3
4
5
6
7
8
9
10
11
module MyLib {
explicit module A {
header "A.h"
export *
}

explicit module B {
header "B.h"
export *
}
}

类似上面的语法,描述了 MyLib、MyLib.A、MyLib.B 这样的模块结构。

官方文档中有更多相关内容,可以描述框架,描述系统头文件,控制导出的范围,描述依赖关系,链接参数等等。这里不多叙述,举个 libcurl 的例子:

1
2
3
4
5
module curl [system] [extern_c] {
header "/usr/include/curl/curl.h"
link "curl"
export *
}

将此 modulemap 文件放入任意文件夹,通过 Xcode 选项或者命令行参数,添加路径到 import search path (swift 的 -I 参数)。 然后就可以在 Swift 代码里直接通过 import curl 导入所有的接口函数、结构体、常量等。

Xcode 选项位于 Build Settings 下面的 Swift Compiler - Search Paths 。添加路劲即可。

每个Module中必须包涵一个umbrella头文件,这个文件用来import所有这个Module下的文件。

大致关系为:import module -> import umbrella header -> other header

使用 Module 库的调用方式:

项目类型 OC库(GDTPackage) Swift库(GDTPackage)
OC 项目 #import <GDTPackage/GDTPackage.h> #import <GDTPackage-Swift.h>
Swift 项目 import GDTPackage import GDTPackage

GDTPackage.h 其实就是 umbrella header/master header

CocoaPods 自定义 Module

我们以桥接 GDTMobSDK 为例。

创建 GDTPackage 库

通过 CocoaPods 提供的命令行创建库:

1
$ pod lib create GDTPackage

创建 module.modulemap 和 BridgeHeader.h

在项目中新建 module.modulemapBridgeHeader.h,将它们放在同一个文件夹下 GDTPackage/Module

module.modulemap 代码如下:

1
2
3
4
module GDTPackageBridge {
header "BridgeHeader.h"
export *
}

BridgeHeader.h 代码如下:

1
2
3
4
5
6
#import <GDTMobSDK/GDTMobBannerView.h>
#import <GDTMobSDK/GDTRewardVideoAd.h>
#import <GDTMobSDK/GDTNativeExpressAd.h>
#import <GDTMobSDK/GDTNativeExpressAdView.h>
#import <GDTMobSDK/GDTMobInterstitial.h>
#import <GDTMobSDK/GDTSplashAd.h>

GDTPackage.podspec 部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
...
s.static_framework = true
s.source_files = 'GDTPackage/Classes/**/*'

s.preserve_paths = ['GDTPackage/Module/module.modulemap', 'GDTPackage/Module/BridgeHeader.h']
s.pod_target_xcconfig = {
# 路径根据实际情况进行引用,必须保证路径是正确的
'SWIFT_INCLUDE_PATHS' => ['$(PODS_ROOT)/GDTPackage/Module', '$(PODS_TARGET_SRCROOT)/GDTPackage/Module']
}

s.dependency 'GDTMobSDK'
...

代码中引用 GDTPackageBridge

1
2
3
4
5
6
7
import GDTPackageBridge

class GDTPackage {
func test() {
GDTSplashAd.init()
}
}

注意事项

  1. 如果已经在 preserve_paths 添加了 modulemapheader,可以不用在 source_files 里再加一遍,如果要在 source_files 里加也可以,记得指定 public_header_files。如果没有指定,你自己创建的 modulemap 也会当做 public 处理。这样 lint 的时候会报 Include of non-modular header inside framework module

  2. lint 时遇到 Include of non-modular header inside framework module 错误,可以在后面添加 --use-libraries。虽然能验证和上传通过,但是其他项目引用的时候还是会有问题。

  3. user_target_xcconfig 是针对所有 Pod 的,可能和其他 Pod 存在冲突。pod_target_xcconfig 是针对当前 Pod 的。

参考链接

  1. Modules - Clang 12 documentation

想在项目中使用静态库功能,需要在 Podspec 显示指定 s.static_framework = true,对于多个 Pod 的项目来说,一个个改起来太麻烦了,也不现实。但是 CocoaPods 是 Ruby 写的,我们可以通过 patch CocoaPods 来实现在只写几行代码的情况下,把所有 pod 变成 Static Framework。

通过分析 CocoaPods 的源代码发现,CocoaPods 会通过 Pod -> Installer -> Analyzer -> determine_build_type 这个方法来决定每个 podspec 的 build type,我们可以通过 patch 这个方法来改写。

在 Podfile 的同级目录创建 patch_static_framework.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module Pod
class Installer
class Analyzer
def determine_build_type(spec, target_definition_build_type)
if target_definition_build_type.framework?
# 过滤掉只能动态库方式的framework,或者不确定的framework
dynamic_frameworks = ['xxxxx']
if !dynamic_frameworks.include?(spec.root.name)
return BuildType.static_framework
end
root_spec = spec.root
root_spec.static_framework ? BuildType.static_framework : target_definition_build_type
else
BuildType.static_library
end
end
end
end
end

在 Podfile 的最上面,引入该文件

1
require_relative 'patch_static_framework'

这样 patch 就会在 pod install 的时候生效,我们就不需要改每个 Pod 的 Podspec 就可以实现每个 Pod 都是 static_framework。

越狱软件

越狱软件统计(更新日期:2020-05-24)

越狱软件 最新版本 支持设备 支持版本 源码
unc0ver 5.0.0 A7 ~ A13 iOS 11.0 ~ 13.5 https://github.com/pwn20wndstuff/Undecimus/
checkra1n 0.10.2 beta iPhone 5s ~ iPhone X iOS 12.3 ~ 未释放
Chimera 1.4.0 所有设备 iOS 12 ~ 12.2 and 12.4,tvOS 12 ~ 12.2 and 12.4 https://github.com/coolstar/Chimera13
Electra 1.3.2 所有设备 iOS 11.0 – 11.4.1 https://github.com/coolstar/electra
Meridian v0.9-007 Pre-Release 所有64位设备 iOS 10 ~ 10.3.3 https://github.com/PsychoTea/MeridianJB
Doubleh3lix RC8 所有64位A7 ~ A9 iOS 10.x
yalu102 beta7 所有64位设备除了iPhone7 iOS 10.x https://github.com/kpwn/yalu102

越狱工具

  • Cydia Impactor
    1. Note: This method requires an Apple developer account.
    2. Download Cydia Impactor for the applicable OS.
    3. Extract the application file, and open it.
    4. Connect your iOS device.
    5. Download the latest version of unc0ver from above.
    6. Drag the IPA file into the Impactor window.
    7. Enter your Apple ID and password (requires developer account). (Note: If you are using two factor authentication, generate an app specific password, and use that here.)
    8. On your iOS device, open Settings → General → Device Management and tap on your Apple ID.
    9. Trust unc0ver.
    10. Open unc0ver and jailbreak!
  • AltStore
    1. Download AltStore. Use the link for your operating system.
    2. Unzip and move AltStore to your Applications folder.
    3. Launch the AltStore application.
    4. Click on the AltStore icon in the Menu Bar, and then click on the Install Mail Plug-in option.
    5. Open the Mail app, and click on Mail → Preferences in the menu bar.
    6. Open the General tab in mail preferences, click Manage Plug-ins, check AltPlugin, and apply and restart Mail.
    7. Connect your iOS device via USB.
    8. Click AltStore in the menu bar, then go to Install AltStore → (Your iOS Device)
    9. Login with your Apple ID when prompted and click install.
    10. On your iOS device, open Settings → General → Device Management and tap on your Apple ID.
    11. Trust AltStore.
    12. Tap the “Open in AltStore” button located above.
    13. AltStore will now install the app. Wait until it finishes.
    14. Open unc0ver and jailbreak!
  • iOS App Signer
    1. Install Xcode, open it, and agree to the license agreement.
    2. Plug in your iOS device and select it as the build target.
    3. Open Xcode and create a new iOS Application.
    4. Type a name and identifier.
    5. Xcode will complain about the lack of a provisioning profile. Click fix issue.
    6. Sign into an Apple ID when prompted.
    7. Download iOS App Signer
    8. Download the latest version of unc0ver from above.
    9. Open iOS App Signer.
    10. Select the ipa you just downloaded as an input file.
    11. Click start.
    12. Return to Xcode. Go to the menu bar. Click Window → Devices.
    13. Find your device, click the plus, and select the file created by iOS App Signer.
    14. Open unc0ver on your device and jailbreak!

Cydia源&常用软件

BigBoss

源:http://apt.thebigboss.org/repofiles/cydia/

  • OpenSSH
  • FLEXing
  • LookinLoader
  • LocationFakerX
  • AnyWhere!–虚拟定位

Binger

源:https://apt.bingner.com/

  • Class Dump

Frida

源:https://build.frida.re/

  • Frida

Chariz

源:https://repo.chariz.com/

  • Cephei
  • NewTerm
  • QuitAll

TIGI Software

源:https://tigisoftware.com/cydia/

  • Apps Manager
  • Filza File Manager

Matchstic

源:https://repo.incendo.ws/

  • ReProvision

雷锋源

源:https://apt.abcydia.com/

  • AppStore++ 应用降级
  • iCleaner Pro
  • NetControl 联网控制
  • Shadow 屏蔽越狱检测
  • eSim+ 双卡增强
  • FlyJB 屏蔽越狱检测
  • NtSpeed 悬浮网速
  • Filza File 文件管理器
  • CacheClearerXI 缓存清理
  • Snapper 2 智能截图
  • CarBridge 汽车互联
  • AudioRecorder XS 通话录音

fastlane is the easiest way to automate beta deployments and releases for your iOS and Android apps. 🚀 It handles all tedious tasks, like generating screenshots, dealing with code signing, and releasing your application.

fastlane 是自动化Beta部署和发布iOS和Android应用程序最简单方法。它可以处理所有繁琐的任务,例如生成屏幕截图,处理代码签名以及发布应用程序。

Fastlane 安装

安装 Xcode command line tools

1
$ xcode-select --install

安装 Homebrew

1
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

安装 RVM

1
2
$ curl -sSL https://get.rvm.io | bash -s stable --auto-dotfiles
$ source ~/.rvm/scripts/rvm

修改 RVM 的 Ruby 安装源到 Ruby China 的 Ruby 镜像服务器,这样能提高安装速度。

1
$ echo "ruby_url=https://cache.ruby-china.org/pub/ruby" > ~/.rvm/user/db

安装Ruby 2.6.5

1
2
$ rvm install 2.6.5
$ rvm use 2.6.5 --default

更新 RubyGems 镜像

1
2
3
4
5
$ gem sources --add https://gems.ruby-china.org/ --remove https://rubygems.org/
$ gem sources -l
https://gems.ruby-china.org
# 确保只有 gems.ruby-china.org
bundle config mirror.https://rubygems.org https://gems.ruby-china.org

安装 CocoaPods 和 Fastlane

1
2
3
$ gem install cocoapods
$ gem install fastlane -NV
$ gem install bundle
阅读全文 »

Swift 的函数作为一等公民,可以赋值给变量,柯里化,也可以作为参数传递(如果将函数作为参数传递给闭包,只要类型匹配,就可以将函数引用代替内联闭包)。我们可以将函数当作带有名称的特殊闭包,但是使用的时候需要当心。

0x01 问题

最近遇到一个在 Swift 中将函数作为参数传递给闭包时,导致循环引用的场景。

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
class ClassA {

var commandHandler: () -> Void = { }

init() {
print("init ClassA")
}

deinit {
print("deinit ClassA")
}

func handle(commandHandler: @escaping () -> Void) {
self.commandHandler = commandHandler
}
}

class ClassB {
let a: ClassA
init(a: ClassA) {
print("init ClassB")
self.a = a
a.handle(commandHandler: self.commandAction)
}

deinit {
print("deinit ClassB")
}

func commandAction() {

}
}

实例化ClassB,这个时候就会产生循环引用导致内存泄漏。

0x02 实例函数是柯里化类函数

在Swift中,实例函数只是柯里化类函数,该类函数将实例作为第一个参数,并隐式地使第一个参数作为self可供函数体使用。 因此,以下两个是等价的:

1
2
3
let numbers = [1, 2, 3, 4, 5, 6, 7, 8]
numbers.contains(3) //true
Array.contains(numbers)(3) //true

而且,这些也是等价的:

1
2
3
let handler1 = self.commandAction
let handler2 = self.dynamicType.commandAction(self)
let handler3 = { [unowned self] in self.commandAction() }

0x03 可以通过泛型函数来管理内存

如果我们要从上面的 handler2 中获取 self.dynamicType.commandAction,但是没有参数 (self)作为参数传递给了包装函数以便引用 self,我们改怎么办呢? 我们可以通过unowend来引用,并将 unowned 实例引用传递给类函数获取一个实例函数,而且不会导致循环引用。

1
2
3
4
5
6
7
func unown<T: AnyObject, V>(_ instance: T, _ classFunction: @escaping (T) -> (() -> V)) -> () -> V {
return { [unowned instance] in classFunction(instance)() }
}

func unown<T: AnyObject, U, V>(_ instance: T, _ classFunction: @escaping (T) -> ((U) -> V)) -> (U) -> V {
return { [unowned instance] in classFunction(instance)($0) }
}

这样的话,我们就可以通过以下方式来获取实例方法的引用,而且我们不会强引用self

1
let handler4 = unown(self, self.dynamicType.commandAction)

缺点是,函数每增加一个参数,我们就需要写一个泛型函数来管理内存。而且,由于使用的是unowned管理内存,如果使用不当会导致野指针访问导致崩溃。

参考链接

接触新项目后,发现没有改代码的情况下,每次编译基本上编译时间都在一分钟左右。就有了一个想法去解决这个问题,断断续续花了三天时间解决,解决过程中,学习到很多,记录下来。

0x01 发现问题

开启编译耗时显示

打开终端执行以下命令并重启Xcode:

1
$ defaults write com.apple.dt.Xcode ShowBuildOperationDuration -bool YES

编译 Build

WX20200418-182140@2x

编译时长 56.3 s,其中耗时比较长的过程为以下:

  • Compile asset catalogs:23.5 s
  • [CP]Embed Pods Frameworks:7.4 s
  • [CP] Copy Pods Resources:17.6 s

0x02 分析&解决问题

开始尝试优化 Xcode 编译速度

发现编译耗时集中在上面三个过程中,一开始主要关注于 Xcode 本身编译提升,看了很多关于提升 Xcode 编译速度的文章,比如这篇文章:https://elliotsomething.github.io/2018/05/23/XCodeBuild/

编译时长优化 Find Implicit Dependencies

对所编译项目的Scheme进行配置 Product > Scheme > Edit Scheme > Build Build Opitions选项中,去掉Find Implicit Dependencies。

编译线程数优化

1
2
3
4
$ defaults write com.apple.dt.xcodebuild PBXNumberOfParallelBuildSubtasks `sysctl -n hw.ncpu`
$ defaults write com.apple.dt.xcodebuild IDEBuildOperationMaxNumberOfConcurrentCompileTasks `sysctl -n hw.ncpu`
$ defaults write com.apple.dt.Xcode PBXNumberOfParallelBuildSubtasks `sysctl -n hw.ncpu`
$ defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks `sysctl -n hw.ncpu`

其后的数字为指定的编译线程数。Xcode默认使用与CPU核数相同的线程来进行编译,但由于编译过程中的IO操作往往比CPU运算要多,因此适当的提升线程数可以在一定程度上加快编译速度。

然后做完以上尝试后,优化了4s。😭

远远没有达到优化的目的。

寻找另外的解决方向

从 Xcode 的本身优化不能有任何的提升后,那问题只能出在工程本身,再次分析编译过程的时长发现和 Assets.xcassets 和 Pods 关系很大。先从 CocoaPods 开始分析 Podfile,发现工程的 Podfile 有如下代码:

install! ‘cocoapods’, disable_input_output_paths: true

去掉以后运行 pod install,出现编译出现错误:

error: Multiple commands produce ‘/xxxxx/xxxxx/Assets.car’:

1) Target ‘xxxx’ (project ‘xxx’) has compile command with input ‘/xxxx/xxxx/Assets.xcassets’

2) That command depends on command in Target ‘xxx’ (project ‘xxx’): script phase “[CP] Copy Pods Resources”

在 CocoaPods 上找到了这样一个 issue https://github.com/CocoaPods/CocoaPods/issues/8122,里面提到主工程里 Assets.xcassets 和 Pods 里有同名的 Assets.xcassets,在 Xcode 10 之前进行编译是不会有问题的,Xcode 只是生成 Warning,但是在 Xcode 10 之后使用了 New Build System 会生成 Errror,提示重复生成 Assets.car。

issue 里提到了4种解决方案:

方案1:https://github.com/CocoaPods/CocoaPods/issues/8122#issuecomment-424169508

1
install! 'cocoapods', :disable_input_output_paths => true

这个方案会导致每次编译时长增加3x倍多。这也刚好是我们工程采用的方式。

方案2:https://github.com/CocoaPods/CocoaPods/issues/8122#issuecomment-424265887

使用 Legacy Build System 而不是 Xcode 11 的 New Build System

方案3:在 Podfile 中添加如下代码

1
2
3
4
5
6
7
8
9
10
project_path = '[YOUR_PROJ_NAME].xcodeproj'
project = Xcodeproj::Project.open(project_path)
project.targets.each do |target|
build_phase = target.build_phases.find { |bp| bp.display_name == '[CP] Copy Pods Resources' }

assets_path = '${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Assets.car'
if build_phase.present? && build_phase.input_paths.include?(assets_path) == false
build_phase.input_paths.push(assets_path)
end
end

这种方案在 CocoaPods 1.8.0 之前可以的,但是在 1.8.0 之后 Input Files 变成了 xcfilelist,就无法直接使用了。

方案4:https://github.com/CocoaPods/CocoaPods/issues/8122#issuecomment-531726302

主要代码是在 [CP] Copy Pods ResourcesInput Files 或者 Input File Lists 中添加。

1
$ {TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Assets.car

尝试了以上4种解决方案,只有方案4 符合预期。

0x03 解决方案

使用这个 cocoapods 插件:https://github.com/dreampiggy/cocoapods-xcode-patch

使用 BundlerGemfile 添加这个插件:

1
2
3
4
source "https://rubygems.org"

gem 'cocoapods'
gem 'cocoapods-xcode-patch', :git => 'https://github.com/dreampiggy/cocoapods-xcode-patch.git'

使用 bundle exec pod install 替代 pod install 来加载这个插件。

0x04 原因分析

出现这个问题根本原因是因为 CocoaPods 有两种资源管理方式 resource_bundlesresources

以下简单介绍下这两种资源管理方式:

resource_bundles(官方推荐)

This attribute allows to define the name and the file of the resource bundles which should be built for the Pod. They are specified as a hash where the keys represent the name of the bundles and the values the file patterns that they should include.

For building the Pod as a static library, we strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute.

The names of the bundles should at least include the name of the Pod to minimise the chance of name collisions.

To provide different resources per platform namespaced bundles must be used.

Examples:

1
spec.ios.resource_bundle = { 'MapBox' => 'MapView/Map/Resources/*.png' }
1
2
3
4
spec.resource_bundles = {
'MapBox' => ['MapView/Map/Resources/*.png'],
'MapBoxOtherResources' => ['MapView/Map/OtherResources/*.png']
}

resources

A list of resources that should be copied into the target bundle.

For building the Pod as a static library, we strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute. Moreover, resources specified with this attribute are copied directly to the client target and therefore they are not optimised by Xcode.

Examples:

1
spec.resource = 'Resources/HockeySDK.bundle'
1
spec.resources = ['Images/*.png', 'Sounds/*']

由于组件化的原因,我们的某个组件采用了Assets.xcassets 和 Storyboard 需要拷贝到主工程中进行引用,Pod 库只能以 resources 的方式引用资源。经过这次优化编译速度有了很大提升。

0x05 后续:Pods 文件更改没有更新

优化了 Xcode 编译后,出现另外一个问题:更改 Pods 库后,Pods 库已编译但主工程没有使用最新的frameworks,导致动态链接的时候找不到对应的符号而产生崩溃。

导致这个问题的原因是 Build Phases 中的 [CP] Embed Pods Frameworks 不是每次都执行,猜测可能是 Xcode 11 的 New Build System 做了优化,导致脚本没有执行。最终想了个办法来解决这个问题,追加命令来执行脚本 find "${PODS_ROOT}" -type f -name *frameworks.sh -exec bash -c "touch \"{}\"" \;,使得脚本每次能执行更新frameworks。

因为 [CP] Embed Pods Frameworks的脚本是由 CocoaPods 进行修改的,所有我将上面的命令通过hook的方式来追加,具体使用方法可以查看 https://github.com/chaoskyx/cocoapods-xcode-patch

编译时间也有所增加,在工程中测试大概增加了20s左右,还有优化的空间,后续如果想到更好的解决办法再更新。

0x06 参考链接