在这里记录了跟 Apple 相关操作系统中遇到的问题和记录。
ELF
:Linux下的可执行文件格式;PE32/PE32+
:Windows下的可执行文件格式;Mach-o
:Mac和iOS下的可执行文件格式,比如可执行文件、库文件、dsym文件、动态库、动态链接器
- 预处理器:将.c 文件转化成 .i文件,使用的gcc命令是:gcc –E,对应于预处理命令cpp;
- 编译器:将.c/.h文件转换成.s文件,使用的gcc命令是:gcc –S,对应于编译命令 cc –S;
- 汇编器:将.s 文件转化成 .o文件,使用的gcc 命令是:gcc –c,对应于汇编命令是 as;
- 链接器:将.o文件转化成可执行程序,使用的gcc 命令是: gcc,对应于链接命令是 ld;
- 加载器:将可执行程序加载到内存并进行执行,loader和ld-linux.so。
// 该方法默认是添加到 NSDefaultRunLoopMode
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
// 该方法需要手动添加到的NSRunLoop中
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
// 添加方法
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
- NSDefaultRunLoopMode :默认mode,当UIScrollView拖动时会影响。点击事件、普通回调事件、不滑动,是个默认状态、空闲状态,程序启动后,自动被切到这个 mode
- UITrackingRunLoopMode: 界面跟踪mode,用于scrollView跟踪触摸滑动,保证界面滑动时不受其它Mode影响
- UIInitializationRunLoopMode: 启动App时进入的第一个mode,启动完成后就不再使用。程序启动之后,私有,当出现第一个页面时就被切了。
- GSEventReceiveRunLoopMode:Graphic相关事件、接收系统事件内部的mode,通常用不到
- NSRunLoopCommonModes: 占位用的mode,不是真的mode,包含第一个和第二个。
scheduledTimerWithTimeInterval 使用该方法创建出来的timer默认加入到当前线程的RunLoop中,且模式为 NSDefaultRunLoopMode,如果当线程是主线程(UI线程),某些UI事件比如UIScrollView的拖动操作,会将Run Loop 切换成 NSEventTrackingRunLoopMode模式,在这个过程中,创建出来的timer的注册的事件是不会被执行的。需要把创建出来的 timer 使用 NSRunLoopCommonModes 模式添加到 Run Loop中,这个模式等效于 NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合。
iOS开发之NSCoding协议(使用runtime):https://www.jianshu.com/p/b33bdbccfa57。正常对用NSCoding写数据持久化,如果该对象的属性较少直接开干没啥问题,但是如果是一个多属性的对象,那就得写死人了。直接用runtime的特性去遍历出当前对象的所有属性。
property = _var + setter + getter
。self
类似 java
中的 this
和 C++
中的 self
,OC
中 self
同样也具备相同的职能,只不过因为 property
机制的存在,事情并且不简单。
实例变量(成员变量)具有私有性,一般情况下只在类内部使用,为了方便给外界读写这个实例变量,就有了属性(@property
),编译器会自动的(@synthesize var = _var
)为我们生成对应的一个以下划线加属性名命名的实例变量、 setter
和 getter
方法,并且 self.var
会让 var
计数器 + 1,而 _var
不会。需要注意在 var
的 getter
方法中,我们必须要使用 _var
进行访问,如果还是 self.var
则会进入死循环,但是在 setter
中如果还是使用 self.var
则也会触发 setter
。懒加载中也需要用 _var
来访问实例变量。一句话就是,“获取 var
只能 _var
,赋值 var
可以 _var
和 self.var
”。如果我们同时对 var
写了 setter
和 getter
方法,@property
机制将不会生效,我们要自己声明 _var
使用 readonly
关键字修饰后,编译器只会为我们生成 getter
。如果一个属性被 @dynamic
修饰,则编译器不会为其自动生成对应的 setter
和 getter
,如果没有我们没有自行编写 getter
和 setter
,将不会在编译器得到提醒,在程序运行时将会发生 crash
。我们还可以使用 @dynamic
替换到某个类中本来就存在的 property
在 Swift
中没有 @property
类似的机制,属性是否对外部可见通过 private
关键词进行决定,Swift 中的属性分为计算属性和存储属性。计算属性不直接存储值,而是通过 getter
和 setter
方法间接访问其它属性,类似 OC
中的属性。存储属性充当存储值的角色,可直接被外部访问,类似 OC
中的实例变量。
- UIView 和 CALayer 一样都是 UI 操作的对象 :两者都是
NSObject
的子类,发生在 UIView 上的操作本质上也发生在 CALayer 上。 - UIView 是 CALayer 用于交互的对象 :UIView 是 UIResponder 的子类( UIResponder 是 NSObject 的子类),UIView 提供了很多 CALayer 交互上所没有的接口,其主要负责处理用户触发的各种操作。
- CALayer 在图像和动画上渲染性能更好 :正是因为 UIView 有冗余的交互接口,而且相比 CALayer 还有层级之分,而 CALayer 在无须处理交互时进行渲染,可以节省大量时间。
layeroutIfNeeded
: 该方法一旦被调用,主线程会立即强制重新布局。它从当前视图开始,一直到完成所有子视图的布局;layoutSubviews
:该方法用于自定义视图尺寸。系统调用,开发者只能重写该方法,让系统在调整尺寸时能够按照开发者希望的效果进行布局。该方法主要用在屏幕旋转、滑动或触摸界面、修改子视图时被触发。setNeedsLayout
:与layoutSubviews
方法作用类似,不同的是它不会立刻强制视图重新布局,而是在下一个布局周期才会触发更新。该方法主要用于多个视图布局先后更新的场景下。比如,在两个位置不断变化的点之间连一条线,该条线的布局就可以调用setNeedsLayout
方法。
详见:https://stackoverflow.com/questions/31577140/uialertcontroller-is-crashed-ipad
如果能够保证这个 scheme 一定是存在的,可以直接执行:
[[UIApplication sharedApplication] openURL:request.URL
options:@{}
completionHandler:nil];
如果不能保证,以免其它特殊问题(我没遇到过),可以先 canOpenURL
判断一下:
BOOL result = [[UIApplication sharedApplication] canOpenURL:request.URL];
if(result) {
[[UIApplication sharedApplication] openURL:request.URL
options:@{}
completionHandler:nil];
} else {
// 是否需要提示?
}
执行 canOpenURL
方法,记得现在 target
-> Info
-> Custom iOS Target Properties
中添加字段 LSApplicationQueriesSchemes
,类型为 Array
,在 item
中添加对应需要打开的 app scheme 即可。
在 URL Types 中写的 scheme 指的是被其它 app 回调我方 app 时的标识符。
我们经常会使用 [UIView removeFromSuperView]
方法把一个 UIView
及其子类(下文称 ViewA
)从当前视图中进行移除,当我们执行这个操作时,潜意识里是想把这个视图彻底从当前页面上进行移除,但是这个方法只能保证把 ViewA
从当前视图上进行移除,并不能保证该 ViewA
真的被移除”,内存中还是存在 ViewA
。
还需要执行 ViewA = nil
applyAction.setValue(UIColor.lightGray, forKey: "_titleTextColor")
可以使用友盟后台进行“推送测试”,如果是自建了集成友盟推送 SDK 的后台,进行“推送测试”的话,需要把 app 进行构建,通过安装并使用构建出来的 ipa
包才能够收到自建集成了友盟推送 SDK 的后台推送。
注意:测试设备例如 iPhone、iPad 等设备请登陆 Apple ID,否则 app 中集成友盟推送 SDK 将无法对该测试设备进行 device Token
的生成,也即无法推送至友盟中心。
元编程。一件事情通过正常编程去做到是「正常编程」,元编程是通过编程的方法做到其他需要编程的事情,可以理解为「元编程」的本质上在做一个新的 DSL。@property
就是一个「元编程」思路。
当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该现场将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会推出循环。
存在的问题:如果某个线程持有锁的时间过长,就会导致其他等待获取锁的线程进入循环等待,消耗 CPU。
优点:自旋锁不会使线程状态发生切换,其会一直处于用户态,线程一直都是 active 的,不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。非自旋锁在获取不到锁时会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换,因为线程被阻塞后便进入「内核调度状态」,会导致系统在用户态和内核态之间来回切换。
可查看:https://zhuanlan.zhihu.com/p/40729293
首先明确一点,pods 是一个依赖管理工具,能够解析出用在 Podfile
文件中的配置信息,解析配置信息的过程不是特别复杂, 但解析的逻辑代码量一点都不小,考虑了很多的 case,尤其是依赖解析部分,自建了一个依赖图算法完成各个依赖库之间的重复依赖关系。
pods 是基于 ruby
的一个依赖管理工具,虽然 Podfile
写起来给人感觉一点都不像是写代码,而是在写一个描述文档,这是因为 ruby
的方法调用可以不写方法调用时的左右括号,而且也支持 block 的调用(与 OC block 不一致)。解析出依赖库后,会调用一个下载器,下载器会根据当前的执行命令是 pod install
还是 pod update
来决定下载器的入参。详情可见
看了头条的 Router 实现大概流程,在开始的做法是通过 .strings
文件填写好 URL 做好映射,再通过 OC 的反射机制把字符串反射为类,现在虽然已经修改为了不需要引入配置文件,之前在应用初始化的时候就能初始化好了 Router,但实际上的做法还是基于反射。
个人认为 router 的好处在于能够应对多个 type 的跳转。
- 源代码编译
- 静态库链接
- 资源编译、优化、导入
- 配置文件生成
- 签名打包
编译器前端部分主要负责把「编程语言」翻译成「平台无关语言」,编译器后端主要作用是把「平台无关语言」翻译成「平台相关语言」

- 基于 LLVM 的编译器前端
- 基于 LLVM 的 C 语言编译工具集,兼容 GCC
clang -fsyntax-only -fmodules -Xclang -dump-tokens text.m
,命令执行效果:
clang -fsyntax-only -fmodules -Xclang -ast-dump
iOS 上没有「内存交换机制」,应用的性能不会逐步下滑,只会直接撞上南墙。,所以脏页面在 Apple 的移动设备上成本更高。无论使用多少。脏页面永远不会写入磁盘,所以 IOS 必须更积极地交换干净页面(可执行代码、映射文件)或终止进程。
发生「内存警告」时,其会在主线程上进行传递,如果 app 在主线程进入了忙碌状态时接收到了「内存警告」,那么 app 会被标记为无响应状态,然后就会被移除。
将内存分配操作移到后台也不行,当主线程在前台尝试处理「内存警告」时,后台线程还在大块大块的分配内存,此时 app 同样也会被移除。
在进行分配大量内存的后台线程即将进行工作时,先向主线程利用 performSelector:onMainThread
(将 waitUnitDone
标志位设置为 YES
) 发送一条 ping
消息。如果主线程在处理「内存警告」,那么这条语句会阻塞后台线程中的内存分配操作,并允许在恢复操作之前将内存释放掉。
栈是计算机自动管理,有单独的存储区域。堆需要手动管理。细节看这个
如果是多组件并行开发的情况下,先在 Podfile 中修改对应的组件 :path
为本地路径,开发完毕后,本地 pod
组件库更新,再修改 Podfile
为原先的 Pod
地址。
内核相当于是控制计算机硬件资源的「软件」,用户态里包括了 shell
等一系列应用程序,当应用程序需要调用系统硬件资源时,就跑到了内核态中。
因为这相当于涉及到了两个「软件」的切换使用,所以从用户态切换到内核态时,需要保持用户态下的当前软件环境中的各种信息,再去调用内核态中的各种资源,也就是系统调用。详情可见这篇文章
只不过添加了诸如名称、所有者、访问权限及访问日期之类的元数据。
- 比
NSFileManager
更好用,提供了文档同步,异步性能优化等问题 - 需要被继承,实现方法。不能直接使用
- http://swiftcafe.io/2015/11/14/uidocument/
- https://developer.apple.com/documentation/uikit/uidocument
[UIApplication sharedApplication].applicationIconBadgeNumber
- 纯函数是函数式编程的概念,必须遵守以下一些约束。
- 不得改写参数
- 不能调用系统 I/O 的API
- 不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不一样的结果
- 如何防止
UIView
的刷新不在主线程之类的问题- 那就写个分类!帮它移动到主线程,但我觉得这种问题应该被暴露出来而不是被放过
- 如何做「对象找不到」方法时的防 crash
runtime
里在找不到方法时有三次几乎可以触发保护resolveInstanceMethod。一般做
crash 防护时不会去碰这个方法,因为这个方法很有可能是本类在做一些动态插入的事情。forwardingTargetForSelector
。一般在这个方法中做处理,通过方法交换,把本应该是 A 类处理,但 A 类没有相关处理的方法转移到另外一个 B 类中,通过在 B 类中替换 A 类的方法,可以做记录 crash 发生的参数等。forwardingInvocation
。一般也不在这个方法中做处理,因为需要自行创建NSInvocation
对象来做对象替换,并调用对应方法。
由于 WKWebView
在独立进程里执行网络请求。一旦注册http(s) scheme后,网络请求将从 Network Process发送到 App Process,这样 NSURLProtocol
才能拦截网络请求。在 webkit2 的设计里使用 MessageQueue
进行进程之间的通信,Network Process 会将请求 encode 成一个 Message,然后通过 IPC 发送给 App Process。
出于性能的原因,encode 的时候 HTTPBody
和 HTTPBodyStream
这两个字段被丢弃掉了。
因此,如果通过 registerSchemeForCustomProtocol
注册了http(s) scheme, 那么由 WKWebView 发起的所有http(s)请求都会通过 IPC 传给主进程 NSURLProtocol 处理,导致 post 请求 body
被清空。
#!/bin/sh
read -p "Please enter the Module name: " module
script_dir=$(dirname $0)
dev_path="${script_dir}/../../Development Pods/"
git_path="template.git"
module_name="${module}"
cd "${dev_path}"
pod lib create --template-url=${git_path} ${module_name}
比如 A 库依赖 AFNetworking
3.0,B 依赖 AFNetworking
2.0,此时 B 要引入 A 组件库,按照以往的昨晚是会出现重名库错误。
解决办法可以把 A 库达成二进制库,只暴露出 A 库应该对外暴露的方法,或者勇另外一个笨方法,手动把 A 库依赖的 AFNetworking
里方法都加上 A 库的前缀,但这种做法只能在不麻烦的情况下使用啦。
- UIView 修改 CALayer 动画时,实际上做的隐式事物,在下一个 RunLoop 中进行提交,显示事物会被立即提交
- [CATransction commit]。不管当前 RunLoop 进行剩下多少耗时操作动画都会被理解提交
- 把暴力操作移交到子线程去做
pod setup
很智障,会把 github 上的所有的托管的 repo 都拉到本地,所以造成了第一次巨慢...
第一次执行 pod install
时生成,除非 Podfile
有修改或执行 pod update
否则不会改变
该目录下存放对应库的头文件软链接
Podfile.lock
的拷贝文件
存放工程 build 时所依赖的一些文件
-
每一个 Pod 都会有
.xccoinfig
,.pch
,dummy.m
(类的空实现) -
debug.xcconfig
和release.xcconfig
对应主工程里的Configurations
的xcconfig
-
resource.sh
用来编译 storyboard 等资源文件或者 copyxcassets
等资源文件 -
Frameworks.sh
用于实现 framework 类型第三方库的链接
-
首次使用
pod update
和pod install
没有任何区别 -
pod install
会参考Podfile.lock
和Podfile
,在生成Podfile.lock
之后,除非Podfile
有所改动,否则运行pod install
结果依然不变 -
Pods/ 文件夹不需要添加到
.gitignore
里。Podfile.lock
应该加入项目版本控制中,可以保证所有人在执行pod install
后所依赖的三方库版本完全一致。

提前将三方库打成静态库,存到公司内部仓库,每次 pod install
直接通过内网拉去静态库,build 时省去 Pod 的编译。
-
直接把需要用到的东西都同步到一个新的 Pod 库里
-
利用
cocoapods
的特性。在pod install
时,利用pre_install
和post_install
两个钩子函数,如下图所示:
Foundation 框架中的所有集合类在默认情况下都执行浅拷贝,也就是说,只拷贝容器本身,而不复制其中的数据。这样做的原因在于容器内的对象未必都能拷贝,而且调用者也未必想在拷贝容器的时一并拷贝其中的每个对象。
NSSet
方法有对应的初始化方法直接执行深拷贝initWithSet:copyItems
- OC 中没有专门定义深拷贝的协议,具体执行由每个类自行决定。
- 不要假定遵循了
NSCopying
协议的对象都会执行深拷贝,在绝大多数情况下执行的都是浅拷贝。除非文档中说明了其深拷贝时基于
- 再加一层密
将一系列代码封装为动态库,并在其中放入描述其接口的头文件,这就叫框架。iOS 上的第三方框架几乎都是静态库,因为 Apple 不允许 iOS 应用程序中包含动态库,但 Apple 自身的系统框架都是动态库。
Cocoapods 的 bundle_resources
和 resources
可以并存,分别制定资源的加载方式,如果不通过 bundle 的方式去做,用 imageWithName
的方法去做即可。因为用之前的 resources
方法会导致 pod 库中的资源合并到主工程里,多个 pod 库之间都这么做的话,那同名资源会出现异常。
使用 bundle_resources
方式引入资源时,如果原先是 resources
方式引入的资源,各个 pod 库里的资源会被合并到主工程中,所以其它 pod 库可以通过「资源合并」的方式间接引用到其它 pod 里的资源,改用 bundle_resources
时,其它 pod 库原先引用的地方都要改为对应的 bundle 里的资源,如果在全局替换搜索时,很有可能会因为 pod 库饮用时是二进制包导致搜索不到,还有些没有暴露 .m 文件。
解决思路:
- 各个 pod 库所属的各自资源都加上自己的前缀。
- 使用
resource_bundles
的方式。+ (NSBundle *)mainBundle { static NSBundle *bundle = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSString *bundlePath = [[NSBundle bundleForClass:TTVideoBusinessBundle.class].resourcePath stringByAppendingPathComponent:@"TTVideoBusinessResource.bundle"]; bundle = [NSBundle bundleWithPath:bundlePath]; if (!bundle) { NSAssert(!bundle, @"未正确加载 TTVideoBusinessResource.bundle"); } }); return bundle; }
+ (UIImage *)ttv_bundleImageName:(NSString *)imageName { return [UIImage imageNamed:imageName inBundle:[TTVideoBusinessBundle mainBundle] compatibleWithTraitCollection:nil]; }
本质上只是加个个标记,在下一个 runloop 布局。
在子线程中手动起一个 runloop,直接 run,停不下来,之后的内容执行不到。
- 基于
cassowary
算法 cassowary
还有 JS、Java 和 C++ 等库的实现- Auto Layout 不只有布局算法,还包含了布局在运行时的生命周期等,这一套布局引擎系统叫
Layout Engine
- 每个视图在获取到自己的布局之前,
Layout Engine
会将试图、约束、优先级、固定大小通过计算转换成最终的大小和位置。 - 在
Layout Engine
里,每当约束发送变化,就会触发Deffered Layout Pass
,完成后进入监听约束变化的状态。 - 监听到约束变化时,即进入下一轮
RunLoop
循环中。 - 添加、删除视图时会触发约束变化,设置约束或优先级也会触发约束变化
Layout Engine
在约束变化时,会重新计算布局。获取到布局后调用superView.setNeedLayout()
然后进入Defferred Layout Pass
。Defferred Layout Pass
的主要作用是做容错处理。如果视图在更新约束时没有确定或缺失布局声明的话,会先在这里做容错处理。- 接下来
Layout Engine
会从上到下调用layoutSubview()
,通过cassowary
算法计算各个子视图的位置,计算完成后,再从子视图的 frame 从Layout Engine
里 copy 出去。 - 与 frame 布局多了一个通过
cassowary
布局计算的过程。
- 每个视图在获取到自己的布局之前,
就是完成变量、函数符号和其地址绑定这样的任务。
可以理解为是把分离的源代码文件合并在一起,解决调用其它文件中的方法问题。
可以使用不同的 podspec
,引用不同的宏定义,通过宏去执行不同的逻辑。
- 在程序开始运行时通过
dyld
动态加载。需要在编译时进行链接,链接时会做标记,绑定的地址在加载后再决定。 - 显式运行时链接,在运行时通过动态链接器提供的 API
dlopen
和dlsym
来动态加载。这种方式,在编译时是不需要参与链接的。但不允许上线,但可通过其加速调试。
并只生成所需的线程执行处理。当处理结束,应当执行的处理数减少,XNU 内核会结束不再需要的线程。
- New 一个 target
- copy 一个 target
- copy 只会多一些配置文件,可以在这些配置文件中读取一些特定的字段进行操作。
保活指的是 app 在后台不会因为资源不足而被系统结束进程。
- 地理位置更新,打点range:涉及到后台获取地理位置。
- 录音。Plan A。
- 持续下载文件。带宽问题。
- 静默loop播放音乐。
静默播放音乐可能最优。 优点:无权限,无带宽,性能损耗低,无影响。 缺点:某些情况下play也会失效。比如和平精英的语音转文字场景。(目前还没有有效方案)。
如果是同一个 groupID 的 app 集合,可以通过共享文件夹的方式随意存取文件,但一个公司/集团不可能只有一个开发者账号,不同的开发者账号不同的 appID,如何做到互相传递大文件呢?
直接用剪贴板,针对被传递出去的文件做加密,剪贴板传资源不费事,但对传输出去的文件做加密比较费事。