0%

准备工作

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 参考链接

尝试支付

使用国际信用卡(Master或Visa)务必保证账单地址正确,否则会导致支付失败。

我使用的是招行的Visa信用卡,账单地址可以从招行的掌上生活 App 搜索账单地址可以找到。

无法支付或支付失败

  • 周一到周五,早上9点到下午5点。

  • 登录开发者中心,左侧,Contact Us,选择会员资格与帐户—>计划购买和续订。

  • 填电话号码,苹果客服会打电话给你。

  • 接通10秒左右按1与技术顾问交谈,你就跟客服说,无法支付年费,让他们给解决的方法。

  • 他们一般会让你先多尝试,你要先做一些支付失败的记录。

  • 然后跟他们说,确实无法支付,他们就会给你两种方案其中一种,不一定是哪一种。

  • 第一种是直接信用卡划扣,他们会问你有没有国际信用卡,Master或Visa的。

    然后要你给卡号,过期时间,还有账单地址,给你尝试手动划扣99美金,扣成功的话,就直接成功了。

  • 第二种是让你电汇到苹果公司的账号。
    他们会手动帮你生成一个订单,然后要转账99美金到他们苹果美国的银行账号里。
    然后转账附言上订单号。
    可以使用建行,我是用这个解决的,要有U盾。
    先购汇99美金,大概690+人民币,然后电汇需要手续费20+80,就是100块手续费。
    大概就是800左右了。
    电汇成功后回复邮件告诉他们已经电汇了,就行了,一般隔日账号就通过了。

  • 以上。

链接

  1. 苹果开发者中心联系电话:https://developer.apple.com/contact/phone/cn/

分析工具:clang

1
2
3
4
clang -rewrite-objc test.m

// UIKit
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk xxxxx.m

block 的数据结构定义

对应的结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};

struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};

block 的三种类型

  1. _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
  2. _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
  3. _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。

block对变量的捕获规则:

  1. 静态存储区的变量:例如全局变量、方法中的static变量
    引用,可修改。

  2. block接受的参数
    传值,可修改,和一般函数的参数相同。

  3. 栈变量 (被捕获的上下文变量)
    const,不可修改。 当block被copy后,block会对 id类型的变量产生强引用。
    每次执行block时,捕获到的变量都是最初的值。

  4. 栈变量 (有__block前缀)
    引用,可以修改。如果时id类型则不会被block retain,必须手动处理其内存管理。
    如果该类型是C类型变量,block被copy到heap后,该值也会被挪动到heap

变量的复制

对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的。

对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。

嵌套block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)setUpModel{
XYModel *model = [XYModel new];

__weak typeof(self) weakSelf = self;
model.dataChanged = ^(NSString *title) {
__strong typeof(self) strongSelf = weakSelf;
strongSelf.titleLabel.text = title;

__weak typeof(self) weakSelf2 = strongSelf;
strongSelf.model.dataChanged = ^(NSString *title2) {
__strong typeof(self) strongSelf2 = weakSelf2;
strongSelf2.titleLabel.text = title2;
};
};

self.model = model;
}

这样,就避免的引用循环,总结一下,不管都多少个block嵌套,皆按此法

@weakify, @strongify 使用

  1. weakify(self)展开后是: *weak typeof(self) *weak_self = self;

  2. strongify(self)展开后是:*strong typeof(self) self = *weak_self;

  3. 在block中使用strongify(self);的目的是确保在block作用域内self不会被其它线程释放掉

  4. 以前我们在block中直接使用__weak_self来解除循环引用。这本身没有问题,之所以还要加strongify(self)就是为了避免block中代码执行过程中由于其它线程释放了self导致block内执行的逻辑出现问题。例如:会出现执行前几句代码时访问self还是存在的,但后面的self调用已经变为nil了

  5. 如果是在block外部定义strongify(self)虽然在block中的self还是指向(跳转到定义)这个strongify(self)。但因为方法调用结束后strongify(self)定义的局部self变量被释放了,所以这种做法就回退到了[4]

  6. 由5可知,如果block中有多个嵌套的block异步调用,那么每一个block中都要再定义一个strongify(self);

  7. 虽然在多层嵌套的block中,定义weakify(self)也是可行的。但是不推荐这么做

  8. swift中使用unowned和weak来解决循环引用问题,基本原理同OC。但unowned本质上是__unsafe_unretained即assign,所以使用起来要小心野指针。还是推荐无脑用weak

  9. 不过要达到[3]中的效果,就要在当前closure的作用域内retain下self,只不过有个小麻烦是没法像OC中写的那么自然——不能使用self了。例子如下:

    1
    2
    3
    4
    5
    obj.doSomething {[weak self] in
    if let strong_self = self {
    strong_self.Member_XXX
    }
    }

总结:多层嵌套的block,只需要对最外层block传入的self进行weak化即可。

参考文章

  1. 谈Objective-C block的实现
  2. block没那么难(一):block的实现
  3. block没那么难(二):block和变量的内存管理
  4. block没那么难(三):block和对象的内存管理
  5. 深入研究Block捕获外部变量和__block实现原理
  6. 深入研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用
  7. iOS 中的 block 是如何持有对象的
  8. objc 中的 block
  9. iOS开发之block终极篇
  10. iOS Block用法和实现原理
  11. OC高级编程——深入block,如何捕获变量,如何存储在堆上
  12. A look inside blocks: Episode 1
  13. A look inside blocks: Episode 2
  14. A look inside blocks: Episode 3
  15. 对 Objective-C 中 Block 的追探
  16. LLVM 中 block 实现源码
  17. objective-c-blocks-quiz
  18. Which Clang Warning Is Generating This Message?
  19. iOS 内存泄漏分析