docs: SSL/TLS

This commit is contained in:
LiuBinPeng
2022-05-24 13:00:23 +08:00
parent 7241220c8e
commit 4f11b95363
90 changed files with 4054 additions and 451 deletions

View File

@@ -27,18 +27,17 @@ Mach 异常都在 host 层被 `ux_exception` 转换为相应的 unix 信号,
有2种解决办法
- Ignore the signal globally with the following line of code.(在全局范围内忽略这个信号 。缺点是所有的 `SIGPIPE` 信号都将被忽略)
```objective-c
signal(SIGPIPE, SIG_IGN);
```
- Tell the socket not to send the signal in the first place with the following lines of code (substituting the variable containing your socket in place of `sock`)(告诉 socket 不要发送信号SO_NOSIGPIPE)
```c++
int value = 1;
setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(value));
```
`SO_NOSIGPIPE` 是一个宏定义,跳过去看一下实现
@@ -51,9 +50,9 @@ Mach 异常都在 host 层被 `ux_exception` 转换为相应的 unix 信号,
其中:**EPIPE** 是 socket send 函数可能返回的错误码之一。如果发送数据的话会在 Client 端触发 RST指Client端的 FIN_WAIT_2 状态超时后连接已经销毁的情况导致send操作返回 `EPIPE`errno 32错误并触发 `SIGPIPE` 信号(默认行为是 **Terminate**)。
> What happens if the client ignores the error return from readline and writes more data to the server? This can happen, for example, if the client needs to perform two writes to the server before reading anything back, with the first write eliciting the RST.
>
>
> The rule that applies is: When a process writes to a socket that has received an RST, the SIGPIPE signal is sent to the process. The default action of this signal is to terminate the process, so the process must catch the signal to avoid being involuntarily terminated.
>
>
> If the process either catches the signal and returns from the signal handler, or ignores the signal, the write operation returns EPIPE.
UNP(unix network program) 建议应用根据需要处理 `SIGPIPE`信号,至少不要用系统缺省的处理方式处理这个信号,系统缺省的处理方式是退出进程,这样你的应用就很难查处处理进程为什么退出。对 UNP 感兴趣的可以查看http://www.unpbook.com/unpv13e.tar.gz。
@@ -146,8 +145,10 @@ if (self.socket == NULL) {
```
### 2. 设备无可用空间问题
![设备无可用空间问题](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NoSpaceLeftOnDevice.png)
最早遇到这个问题,直观的判断是某个接口所在的服务器机器,出现了存储问题(因为查了代码是网络回调存在 Error 的时候会调用我们公司基础),因为不是稳定必现,所以也就没怎么重视。直到后来发现线上有商家反馈这个问题最近经常出现。经过排查该问题该问题 `Error Domain=NSPOSIXErrorDomain Code=28 "No space left on device"` 是系统报出来的,开启 Instrucments Network 面板后看到显示 Session 过多。为了将问题复现,定时器去触发“切店”逻辑,切店则会触发首页所需的各个网络请求,则可以复现问题。工程中查找 NSURLSession 创建的代码将问题定位到某几个底层库HOOK 网络监控的能力上。一个是 APM 网络监控,确定 APMM 网路监控 Session 创建是收敛的另一个库是动态域名替换的库之前出现过线上故障。所以思考之下暂时将这个库发布热修代码。之前是采用“悲观策略”99%的概率不会出现故障然后牺牲线上每个网络的性能增加一道流程而且该流程的实现还存在问题。思考之下采用乐观策略假设线上大概率不会出现故障保留2个方法。线上出现故障马上发布热修调用下面的方法。
```
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
return NO;
@@ -158,6 +159,7 @@ if (self.socket == NULL) {
// 代理网络请求
}
```
问题临时解决后,后续动态域名替换的库可以参考 WeexSDK 的实现。见 [WXResourceRequestHandlerDefaultImpl.m](https://github.com/apache/incubator-weex/blob/master/ios/sdk/WeexSDK/Sources/Network/WXResourceRequestHandlerDefaultImpl.m#L37)。WeexSDK 这个代码实现考虑到了多个网络监听对象的问题、且考虑到了 Session 创建多个的问题,是一个合理解法。
```
@@ -174,7 +176,7 @@ if (self.socket == NULL) {
delegateQueue:[NSOperationQueue mainQueue]];
_delegates = [WXThreadSafeMutableDictionary new];
}
NSURLSessionDataTask *task = [_session dataTaskWithRequest:request];
request.taskIdentifier = task;
[_delegates setObject:delegate forKey:task];
@@ -183,7 +185,9 @@ if (self.socket == NULL) {
```
### NSURLProtocol 主意事项
使用 NSURLProtocol 的时候,如果是代理 NSURLSession 的网络请求,则需要重写 protocolClasses 方法。但是在你往给方法设置 protocolClasses 的时候可能全局也有其他 SDK、工具类也做了修改。这样子需要注意不能丢弃别人的也不能丢弃自己的。参考 OHHTTPStubs 在注册 NSURLProtocol 子类的处理
```
+ (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig
{

View File

@@ -6,9 +6,7 @@
## 结构
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/LLVM-segment.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/LLVM-segment.png)
LLVM 由三部分构成:
@@ -18,7 +16,7 @@ LLVM 由三部分构成:
- Backend后端生成目标程序机器码
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/LLVM-Structure.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/LLVM-Structure.png)
正是由于这样的设计,使得 LLVM 具备很多有点:
@@ -35,10 +33,6 @@ LLVM 由三部分构成:
LLVM现在被作为实现各种静态和运行时编译语言的通用基础结构(GCC家族、Java、.NET、Python、Ruby、Scheme、Haskell、D等)
## Clang
[Clang](http://clang.llvm.org/) 是 LLVM 的一个子项目,基于 LLVM 架构的 c/c++/Objective-C 语言的编译器前端
@@ -57,9 +51,7 @@ Clang 相较于 GCC具备下面优点
- 设计清晰简单,容易理解,易于扩展增强
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/LLVM-phase.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/LLVM-phase.png)
### 查看编译过程
@@ -69,12 +61,10 @@ clang -ccc-print-phases main.m
对 main.m 文件
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/clang-phase.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/clang-phase.png)
可以看到经历了输入、预处理、编译、LLVM Backend、汇编、链接、绑定架构7个阶段。
查看 preprocessor (预处理)的结果:`clang -E main.m`。预处理主要做的事情就是头文件导入、宏定义替换等。
词法分析,生成 Token`clang -fmodules -E -Xclang -dump-tokens main.m`
@@ -89,14 +79,10 @@ int main(int argc, const char * argv[]) {
}
```
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/clang-analysize.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/clang-analysize.png)
语法分析生成语法树ASTAbstract Syntax Tree`clang -fmodules -fsyntax-only -Xclang -ast-dump main.m`
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/clang-ast.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/clang-ast.png)
### LLVM IR

View File

@@ -0,0 +1 @@
# 设计模式及其场景

898
Chapter1 - iOS/1.104.md Normal file
View File

@@ -0,0 +1,898 @@
# NSNotification底层原理
> 有人聊起来 NSNotification 可以在不同的线程发和接收吗?对于不知道或者不确定的知识,有必要探究记录下
## NSNotificationCenter
```objectivec
@property (class, readonly, strong) NSNotificationCenter *defaultCenter;
// 添加 Observer
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
// 发送通知
- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
// 移除通知
- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;
// 添加 Observer
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
@end
```
通过官方 API 可以窥探得到,系统内部应该是维护了 NSNotificationName、Observer、selector、object 之间的关系。
直接上 GUNStep 源码探索下
### Observation
```c
typedef struct Obs {
id observer; /* Object to receive message. */
SEL selector; /* Method selector. */
struct Obs *next; /* Next item in linked list. */
int retained; /* Retain count for structure. */
struct NCTbl *link; /* Pointer back to chunk table */
} Observation;
```
结构体存储了 observer、selector 信息。此外可以看出是一个链表结构next指向注册了同一个通知的下一个观察者。
### NCTbl
```c
typedef struct NCTbl {
Observation *wildcard; /* Get ALL messages. */
GSIMapTable nameless; /* Get messages for any name. */
GSIMapTable named; /* Getting named messages only. */
// ...
} NCTable;
```
查看 NCTbl 结构体定义发现其内部存在2张 MapTable。
- named用于保存添加观察者的时候传入 NotificationName 的情况
- nemeless同于保存添加观察者时没有传递 NotificationName 的情况
### named Table
该表用于存储添加观察者时传了 NotificationName 的情况。也就是 Named Table 中NotificationName 作为 key。在使用系统 API 注册观察者的时候还可以传入 object 参数,表示只监听该对象发出的通知,所以还需要一张表存储 object 和 observer 的对应关系object 为 keyobserver 为 value。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/notification-namedTable.png)
- 第一个 MapTable key 为 notificationNamevalue 为另一个 MapTable子 Table
- 子 Table 以 object 为 keyvalue 为链表,存储所有的观察者
- object 为 nil 的时候,系统会根据 nil 自动生成一个 key相当于这个 key 对应的值链表保存的就是当前通知传入了 NotificationName 且没有 object 的所有观察者。
### nameless Table
nameless Table 结构较为简单,因为没有 notificationName所以就一层 MapTable。key 为 objectvalue 为链表,存储所有的观察者。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/Notification-namelessTable.png)
### wildcard
wildcard 也是链表结构。如果在添加 Observer 的时候没有传递 notificationName、object 则会将 Observer 添加到 wildcard 链表中,注册到这里的观察者可以响应所有的系统通知。
### 添加通知
添加通知的所有 API最终都会调用该 API。
```c
- (void)addObserver:(id)observer
selector:(SEL)selector
name:(NSString *)name
object:(id)object {
Observation *list;
Observation *o;
GSIMapTable m;
GSIMapNode n;
// 参数合法性校验
if (observer == nil)
[NSException raise:NSInvalidArgumentException
format:@"Nil observer passed to addObserver ..."];
if (selector == 0)
[NSException raise:NSInvalidArgumentException
format:@"Null selector passed to addObserver ..."];
if ([observer respondsToSelector:selector] == NO)
{
[NSException raise:NSInvalidArgumentException
format:@"[%@-%@] Observer '%@' does not respond to selector '%@'",
NSStringFromClass([self class]), NSStringFromSelector(_cmd),
observer, NSStringFromSelector(selector)];
}
lockNCTable(TABLE);
// 调用 obsNew 方法,创建 observation 对象,持有 SEL、object
o = obsNew(TABLE, selector, observer);
/*
* Record the Observation in one of the linked lists.
*
* NB. It is possible to register an observer for a notification more than
* once - in which case, the observer will receive multiple messages when
* the notification is posted... odd, but the MacOS-X docs specify this.
*/
// notificationName 存在的逻辑
if (name) {
/*
* Locate the map table for this name - create it if not present.
*/
// NAMED 是一个宏定义,表示名为 named 的字典key 为 name从 named 表中获取对应的 mapTable
n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
// 不存在,则创建
if (n == 0) {
m = mapNew(TABLE); // 先从缓存中取,如果没有则新建 mapTable
/*
* As this is the first observation for the given name, we take a
* copy of the name so it cannot be mutated while in the map.
*/
name = [name copyWithZone:NSDefaultMallocZone()];
GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void *)m);
GS_CONSUMED(name)
} else {
// 存在 mapTable 则取出值也就是 named MapTable
m = (GSIMapTable)n->value.ptr;
}
/*
* Add the observation to the list for the correct object.
*/
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
// 从 named MapTable 中以 key 为 object 获取存储着观察者的链表对象。不存在则创建
if (n == 0) {
o->next = ENDOBS;
GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
} else {
list = (Observation *)n->value.ptr;
o->next = list->next;
list->next = o;
}
} else if (object) {
// 走到这里代表 name 为空,但 object 不为空。此时从 nameless MapTable 中以 object 获取链表对象值。
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
// 不存在则创建新的链表,并存入 nameless MapTable
if (n == 0) {
o->next = ENDOBS;
GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
} else {
// 存在,则将新的观察者添加到链表
list = (Observation *)n->value.ptr;
o->next = list->next;
list->next = o;
}
} else {
// name、object 都为空,则将添加的 observation 观察者添加到 WILDCARD 链表
o->next = WILDCARD;
WILDCARD = o;
}
unlockNCTable(TABLE);
}
static Observation *obsNew(NCTable *t, SEL s, id o)
{
Observation *obs;
if (t->freeList == 0)
{
Observation *block;
if (t->chunkIndex == CHUNKSIZE)
{
unsigned size;
t->numChunks++;
size = t->numChunks * sizeof(Observation *);
t->chunks = (Observation **)NSReallocateCollectable(
t->chunks, size, NSScannedOption);
size = CHUNKSIZE * sizeof(Observation);
t->chunks[t->numChunks - 1] = (Observation *)NSAllocateCollectable(size, 0);
t->chunkIndex = 0;
}
block = t->chunks[t->numChunks - 1];
t->freeList = &block[t->chunkIndex];
t->chunkIndex++;
t->freeList->link = 0;
}
obs = t->freeList;
t->freeList = (Observation *)obs->link;
obs->link = (void *)t;
obs->retained = 0;
obs->next = 0;
// 持有 observer 和 selector
obs->selector = s;
obs->observer = o;
return obs;
}
```
通过源码,我们可以发现和设想差不多,得出以下结论:
- 在调用 `(void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;`方法后,系统内部会调用 `obsNew` 方法,创建 Observation 对象(内部结构是一个结构体),内部保存了观察者 Observer、接收通知时会执行的方法 selector
- 如果传递了 notificationName 则会去 Named MapTable 中,以 notificationName 为 key查找对应的 value子 MapTable。如果不存在则创建一个新的 MapTable
- 在子 MapTable 中,以 object 为 key去取对应的 Observation 链表。如果没有 object 则会生成一个默认的 key表示所有的通知都会被监听
- 通过 object 生成的 key 查找 Observation 链表,如果不存在,则创建一个节点,并作为头节点。如果有链表,则将 Observation 插入尾部(链表的基础操作)
- 如果 notificationName 不存在,且 object 不为空,则通过 object 生成的 key查找 Observation 链表,如果没有则创建一个节点,且作为头节点,如果有链表,则插入到尾部
- 如果 notificationName、object 都为空,则直接把创建的 Observation 对象存储在 wildcard 链表结构中
### 发送通知
postNotification 相关的 API 最终都会调用到 `_postAndRelease` 方法,源码如下
```c
- (void) _postAndRelease: (NSNotification*)notification {
Observation *o;
unsigned count;
NSString *name = [notification name];
id object;
GSIMapNode n;
GSIMapTable m;
GSIArrayItem i[64];
GSIArray_t b;
GSIArray a = &b;
// 参数合法性判断
if (name == nil) {
RELEASE(notification);
[NSException raise: NSInvalidArgumentException
format: @"Tried to post a notification with no name."];
}
object = [notification object];
// 创建 Array 来存储 Observation
GSIArrayInitWithZoneAndStaticCapacity(a, _zone, 64, i);
lockNCTable(TABLE);
// 遍历 WILDCARD 链表,将其中的 Observation 对象都添加到 Array 中的 既没有 notificationName又没有 object
for (o = WILDCARD = purgeCollected(WILDCARD); o != ENDOBS; o = o->next) {
GSIArrayAddItem(a, (GSIArrayItem)o);
}
// 拿到 object在 nameless 表中,以 object 为 key查找对应的链表
if (object) {
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
// 链表存在,则遍历链表,将 Observation 对象添加到 Array 中
if (n != 0){
o = purgeCollectedFromMapNode(NAMELESS, n);
while (o != ENDOBS) {
GSIArrayAddItem(a, (GSIArrayItem)o);
o = o->next;
}
}
}
// 如果 NotificationName 存在
if (name) {
//则以 NotificationName 为 key在 Named MapTable 中寻找对应的子 MapTable
n = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
if (n) {
m = (GSIMapTable)n->value.ptr;
} else {
m = 0;
}
if (m != 0) {
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
// 当子 MapTable 存在的时候,以 object 为 key获取对应的链表
if (n != 0) {
o = purgeCollectedFromMapNode(m, n);
// 当链表存在的时候,遍历链表,将其中的 Observation 对象添加到数组
while (o != ENDOBS) {
GSIArrayAddItem(a, (GSIArrayItem)o);
o = o->next;
}
}
if (object != nil) {
// 以 nil 为 object key查找对应的 Observation添加到数组中
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)nil);
if (n != 0) {
o = purgeCollectedFromMapNode(m, n);
while (o != ENDOBS) {
GSIArrayAddItem(a, (GSIArrayItem)o);
o = o->next;
}
}
}
}
}
/* Finished with the table ... we can unlock it,
*/
unlockNCTable(TABLE);
// 最后遍历数组,调用 Observation 结构体中的 selector 方法。
count = GSIArrayCount(a);
while (count-- > 0) {
o = GSIArrayItemAtIndex(a, count).ext;
if (o->next != 0) {
NS_DURING
{
[o->observer performSelector: o->selector
withObject: notification];
}
NS_HANDLER
{
BOOL logged;
/* Try to report the notification along with the exception,
* but if there's a problem with the notification itself,
* we just log the exception.
*/
NS_DURING
NSLog(@"Problem posting %@: %@", notification, localException);
logged = YES;
NS_HANDLER
logged = NO;
NS_ENDHANDLER
if (NO == logged){
NSLog(@"Problem posting notification: %@", localException);
}
}
NS_ENDHANDLER
}
}
lockNCTable(TABLE);
GSIArrayEmpty(a);
unlockNCTable(TABLE);
// 释放 NSNotification 对象
RELEASE(notification);
}
```
通过源码分析,得出以下结论:
- 调用 `-(void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject` 的时候,内部会生成一个 Array 来存储 Observation 信息
- 先将 WILDCARD 中的所有 Observation 添加到 Array 中去通过分析发送通知源码知道WILDCARD 中 Observation 都是没有 Object、NotificationName 的,也就是可以接收所有的通知
- 然后在 NAMELESS MapTable 中,以 Object 为 key获取对应的链表遍历链表将其中的 Observation 添加到数组中
- 在 NAMED MapTable 中,先以 NotificationName 为 key获取对应的子 MapTable
- 在子 MapTable 中根据 object 为 key获取对应的链表。遍历链表将其中的 Observation 对象添加到数组
- 如果 object 不为 nil则以 nil 为 object key查找对应的 Observation添加到 Array 中
- 最后遍历 Array调用 Observation 结构体成员变量 observer 的 selector 方法。以 `performSelector: withObject:` 形式调用的,所以可以看出都是在同一个线程同步执行的
- 释放 NSNootification 对象
### 移除通知
```c
// 遍历 Observation 链表,判断节点的 observer 等于传递来的参数,则删除该节点
static Observation *listPurge(Observation *list, id observer)
{
Observation *tmp;
while (list != ENDOBS && list->observer == observer)
{
tmp = list->next;
list->next = 0;
obsFree(list);
list = tmp;
}
if (list != ENDOBS)
{
tmp = list;
while (tmp->next != ENDOBS)
{
if (tmp->next->observer == observer)
{
Observation *next = tmp->next;
tmp->next = next->next;
next->next = 0;
obsFree(next);
}
else
{
tmp = tmp->next;
}
}
}
return list;
}
- (void) removeObserver: (id)observer
name: (NSString*)name
object: (id)object{
// 参数合法性校验
if (name == nil && object == nil && observer == nil)
return;
/*
* NB. The removal algorithm depends on an implementation characteristic
* of our map tables - while enumerating a table, it is safe to remove
* the entry returned by the enumerator.
*/
lockNCTable(TABLE);
// NotificationName、object 都为 nil则说明被加入到了 WILDCARD 链表中,调用 listPurge 方法,遍历链表,删除节点中 observer 等于换入参数的节点
if (name == nil && object == nil)
{
WILDCARD = listPurge(WILDCARD, observer);
}
// NotficationName 为空
if (name == nil)
{
GSIMapEnumerator_t e0;
GSIMapNode n0;
/*
* First try removing all named items set for this object.
*/
e0 = GSIMapEnumeratorForMap(NAMED);
n0 = GSIMapEnumeratorNextNode(&e0);
// 现在 NAMED MapTable 中遍历所有子 MapTable
while (n0 != 0)
{
GSIMapTable m = (GSIMapTable)n0->value.ptr;
NSString *thisName = (NSString*)n0->key.obj;
n0 = GSIMapEnumeratorNextNode(&e0);
// object 为空,则遍历 MapTable 中的节点
if (object == nil)
{
GSIMapEnumerator_t e1 = GSIMapEnumeratorForMap(m);
GSIMapNode n1 = GSIMapEnumeratorNextNode(&e1);
/*
* Nil object and nil name, so we step through all the maps
* keyed under the current name and remove all the objects
* that match the observer.
*/
while (n1 != 0)
{
GSIMapNode next = GSIMapEnumeratorNextNode(&e1);
// 清空与 observer 相同的节点
purgeMapNode(m, n1, observer);
n1 = next;
}
}
else
// 如果 object 不为空
{
GSIMapNode n1;
// 则以 object 为 key在子 MapTable 中找到链表,清空链表中所有与 observer 相同的节点
n1 = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n1 != 0)
{
purgeMapNode(m, n1, observer);
}
}
/*
* If we removed all the observations keyed under this name, we
* must remove the map table too.
*/
if (m->nodeCount == 0)
{
mapFree(TABLE, m);
GSIMapRemoveKey(NAMED, (GSIMapKey)(id)thisName);
}
}
// 处理 NAMELESS MapTable
if (object == nil)
{
e0 = GSIMapEnumeratorForMap(NAMELESS);
n0 = GSIMapEnumeratorNextNode(&e0);
// object 为空,则遍历链表,删除与 observer 相同的节点
while (n0 != 0)
{
GSIMapNode next = GSIMapEnumeratorNextNode(&e0);
purgeMapNode(NAMELESS, n0, observer);
n0 = next;
}
}
else
{
// 如果 object 不为空,则根据 object 从 NAMELESS MapTable 中以 object 为 key找到对应的链表
n0 = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
if (n0 != 0)
{
// 删除与 Observer 相同的节点
purgeMapNode(NAMELESS, n0, observer);
}
}
}
else
// NotificationName 不为空美好
{
GSIMapTable m;
GSIMapEnumerator_t e0;
GSIMapNode n0;
// 则从 NAMED MapTable 中以 name 为 key获取对应的子 MapTable
n0 = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
if (n0 == 0)
{
unlockNCTable(TABLE);
return; /* Nothing to do. */
}
m = (GSIMapTable)n0->value.ptr;
// object 为空,则在子 MapTable 中删除节点(节点值与 observer 相同)
if (object == nil)
{
e0 = GSIMapEnumeratorForMap(m);
n0 = GSIMapEnumeratorNextNode(&e0);
while (n0 != 0)
{
GSIMapNode next = GSIMapEnumeratorNextNode(&e0);
purgeMapNode(m, n0, observer);
n0 = next;
}
}
else
{
// 如果 object 不为空,则以 object 为 key取出对应的链表然后将链表中与 observer 相同的节点删除
n0 = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n0 != 0)
{
purgeMapNode(m, n0, observer);
}
}
if (m->nodeCount == 0)
{
mapFree(TABLE, m);
GSIMapRemoveKey(NAMED, (GSIMapKey)((id)name));
}
}
unlockNCTable(TABLE);
}
```
查看源码,得出以下结论:
- 调用删除通知观察者最后都会收敛到该API `(void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;`
- 内部会先判断 NotificationName、object 如果都是 nil则移除 WILDCARD 链表中与 observer 相同的节点
- 如果 NotificationName 为 nil
- 先在 NAMED MapTable 中遍历子 MapTable
- 如果 object 为 nil则遍历子 MapTable且删除与 observer 相同的节点
- object 不为 nil则以 object 为 key获取子 MapTable 中对应的链表,然后清空链表中与 observer 相同的节点
- 再处理 NAMLESS MapTable
- 如果 object 为 nil则直接遍历并删除 table 中所有与 observer 相同的节点
- 如果 object 不为 nil则以 object 为 key获取对应的链表遍历并删除 table 中所有与 observer 相同的节点
- 如果 NotificationName 不为 nil则在 NAMED MapTable 中以 NotificationName 为 key获取链表
- 如果 object 为 nil则遍历 MapTable 中所有的节点,清空与 observer 相同的节点
- 如果 object 不为 nil则以 object 为 key获取子 MapTable 中对应的链表,然后清空链表中与 observer 相同的节点
## 如何异步发送通知
这个 case 就需引入 `NSNotificationQueue` 也就是通知队列了。
简单来说 NSNotificationQueue 是 NSNotificationCenter 的缓冲池。当我们调用 `-(void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject` 的这种方式的时候就是同步发送通知。通知会直接发送到 NSNotificationCenter然后 NSNotificationCenter 会直接将其发送给注册了该通知的观察者。
使用 NSNotificationQueue ,则通知不是直接发送给 NSNotificationCenter而是先存储在 NSNotificationQueue 中,然后再由 notification 转发给注册的观察者。
且可以实现合并相同 NSNotificationName 的通知功。NSNotificationQueue 遵循队列先进先出的特性FIFO当一个通知处于对头的时候它会被发送给 NSNotificationCenter然后 NSNotificationCenter 再将该 notiication 转发给注册了该通知的所以监听者。
每一个线程都有一个默认的 NSNotificationQueue该队列与通知中心联系在一起。也可以为一个线程创建多个 NSNotificationQueue。
其所有 API
```objectivec
@interface NSNotificationQueue : NSObject {
@private
id _notificationCenter;
id _asapQueue;
id _asapObs;
id _idleQueue;
id _idleObs;
}
@property (class, readonly, strong) NSNotificationQueue *defaultQueue;
// 创建
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER;
// 添加观察者(入队)
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSRunLoopMode> *)modes;
// 移除观察者(出队)
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
@end
```
其中 postingStyle 是枚举参数
```objectivec
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1,
NSPostASAP = 2,
NSPostNow = 3
};
```
- NSPostWhenIdle代表在空闲时发送 notification 到 NSNotificationCenter。也就是本线程 RunLoop 空闲时即发送通知到通知中心
- NSPostASAPas soon as possible尽可能快。即当前通知或者 timer 回调执行结束就发送通知到通知中心,还是需要依赖 RunLoop
- NSPostNow马上发送
postingStyle 也是枚举
```objectivec
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0,
NSNotificationCoalescingOnName = 1,
NSNotificationCoalescingOnSender = 2
};
```
- NSNotificationNoCoalescing不管是否 NSNotificationName 是否重复,都不合并
- NSNotificationCoalescingOnNameNSNotificationName 相同的多个 NSNotification 会被合并为一个。
- NSNotificationCoalescingOnSender按照发送方如果多个通知发送方相同则保留一个
测试异步发送通知
```objectivec
- (void)mockNotificationQueue
{
//每个进程默认有一个通知队列,默认是没有开启的,底层通过队列实现,队列维护一个调度表
NSNotification *notifi = [NSNotification notificationWithName:@"Notification" object:nil];
NSNotificationQueue *queue = [NSNotificationQueue defaultQueue];
//FIFO
NSLog(@"notifi before");
[queue enqueueNotification:notifi postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName forModes:[NSArray arrayWithObjects:NSDefaultRunLoopMode, nil]];
NSLog(@"notifi after");
NSPort *port = [[NSPort alloc] init];
[[NSRunLoop currentRunLoop] addPort:port forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
NSLog(@"runloop over");
}
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotifi:) name:@"Notification" object:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self mockNotificationQueue];
}
- (void)handleNotifi:(NSNotification *)notification
{
NSLog(@"%@", [NSThread currentThread]);
}
// NSPostWhenIdle、NSPostASAP
2022-05-07 01:18:01.859643+0800 DDD[62783:2383065] notifi before
2022-05-07 01:18:01.859924+0800 DDD[62783:2383065] notifi after
2022-05-07 01:18:01.860887+0800 DDD[62783:2383065] <_NSMainThread: 0x600003530840>{number = 1, name = main}
2022-05-07 01:18:02.005840+0800 DDD[62783:2383065] notifi before
2022-05-07 01:18:02.006072+0800 DDD[62783:2383065] notifi after
2022-05-07 01:18:02.006882+0800 DDD[62783:2383065] <_NSMainThread: 0x600003530840>{number = 1, name = main}
// NSPostNow
2022-05-07 01:35:21.387512+0800 DDD[63186:2401325] notifi before
2022-05-07 01:35:21.387748+0800 DDD[63186:2401325] <_NSMainThread: 0x60000315c880>{number = 1, name = main}
2022-05-07 01:35:21.387917+0800 DDD[63186:2401325] notifi after
2022-05-07 01:35:21.532892+0800 DDD[63186:2401325] notifi before
2022-05-07 01:35:21.533130+0800 DDD[63186:2401325] <_NSMainThread: 0x60000315c880>{number = 1, name = main}
2022-05-07 01:35:21.533292+0800 DDD[63186:2401325] notifi after
```
改变参数发现:
NSPostWhenIdle异步
NSPostASAP 异步
NSPostNow 同步
所以要实现异步发送通知,则必须使用 NSNotificationQueue 相关 API`postingStyle` 参数必须为 NSPostASAP、NSPostWhenIdle不能为 NSPostNow、不能为 NSPostNow、不能为 NSPostNow。
## 通知和 RunLoop 的关系
```objectivec
- (void)notifiWithRunloop
{
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if(activity == kCFRunLoopEntry){
NSLog(@"进入 Runloop");
}else if(activity == kCFRunLoopBeforeWaiting){
NSLog(@"即将进入等待状态");
}else if(activity == kCFRunLoopAfterWaiting){
NSLog(@"结束等待状态");
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
NSNotification *notification1 = [NSNotification notificationWithName:@"notify" object:nil userInfo:@{@"key":@"1"}];
NSNotification *notification2 = [NSNotification notificationWithName:@"notify" object:nil userInfo:@{@"key":@"2"}];
NSNotification *notification3 = [NSNotification notificationWithName:@"notify" object:nil userInfo:@{@"key":@"3"}];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification1 postingStyle:NSPostWhenIdle coalesceMask:NSNotificationNoCoalescing forModes:@[NSDefaultRunLoopMode]];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification2 postingStyle:NSPostASAP coalesceMask:NSNotificationNoCoalescing forModes:@[NSDefaultRunLoopMode]];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification3 postingStyle:NSPostNow coalesceMask:NSNotificationNoCoalescing forModes:@[NSDefaultRunLoopMode]];
NSPort *port = [[NSPort alloc] init];
[[NSRunLoop currentRunLoop] addPort:port forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotifi:) name:@"notify" object:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self notifiWithRunloop];
}
- (void)handleNotifi:(NSNotification *)notification
{
NSLog(@"%@", notification.userInfo);
}
2022-05-07 01:43:51.470047+0800 DDD[63370:2409134] {
key = 3;
}
2022-05-07 01:43:51.470312+0800 DDD[63370:2409134] Runloop
2022-05-07 01:43:51.470522+0800 DDD[63370:2409134] {
key = 2;
}
2022-05-07 01:43:51.470962+0800 DDD[63370:2409134] Runloop
2022-05-07 01:43:51.471223+0800 DDD[63370:2409134]
2022-05-07 01:43:51.471422+0800 DDD[63370:2409134] {
key = 1;
}
2022-05-07 01:43:51.471613+0800 DDD[63370:2409134]
2022-05-07 01:43:51.471759+0800 DDD[63370:2409134]
2022-05-07 01:43:51.479267+0800 DDD[63370:2409134]
2022-05-07 01:43:51.480009+0800 DDD[63370:2409134] Runloop
2022-05-07 01:43:51.480172+0800 DDD[63370:2409134]
2022-05-07 01:43:51.842003+0800 DDD[63370:2409134]
2022-05-07 01:43:51.842938+0800 DDD[63370:2409134]
2022-05-07 01:44:33.109154+0800 DDD[63370:2409134]
```
通过 Demo 和对应的打印可以看出NSNotificationQueue 相关的 API 和 RunLoop 有关系,当 `postingStyle` 参数为 NSPostNow 的时候则说明通知没有进入 RunLoop而是直接立即执行。参数为 NSPostASAP、NSPostWhenIdle 的时候都和 RunLoop 有关NSPostASAP 通知快于 NSPostWhenIdle。
## 通知重定向
```objectivec
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"dispatch thread = %@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TESTNOTIFICATION" object:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"TESTNOTIFICATION" object:nil userInfo:nil];
});
}
- (void)handleNotification:(NSNotification *)notification
{
NSLog(@"receive thread = %@", [NSThread currentThread]);
}
2022-05-07 01:55:21.042542+0800 DDD[63607:2419849] dispatch thread = <_NSMainThread: 0x600001b44840>{number = 1, name = main}
2022-05-07 01:55:21.042835+0800 DDD[63607:2419937] receive thread = <NSThread: 0x600001b1c9c0>{number = 5, name = (null)}
```
虽然我们在主线程中注册了通知的观察者,但在全局队列中 postNotification 并不是在主线程处理的。如果我们想在回调中处理与 UI 相关的操作,需要确保是在主线程中执行回调。
为什么不直接在处理通知事件的地方强制切回主线程?
不推荐。假如子线程发送多个通知,注册多个不同的观察者,那你是否要在每一个通知处理的地方都去切主线程,不够收口
> For example, if an object running in a background thread is listening for notifications from the user interface, such as a window closing, you would like to receive the notifications in the background thread instead of the main thread. In these cases, you must capture the notifications as they are delivered on the default thread and redirect them to the appropriate thread.
这里谈到重定向。一种重定向的实现思路是自定义一个通知队列注意不是NSNotificationQueue 对象而是一个数组让这个队列去维护那些我们需要重定向的Notification。我们仍然是像平常一样去注册一个通知的观察者当 Notification 来了时,判断 postNotification 的线程是不是所期望的线程,如果不是,则将这个 Notification 存储到我们的队列中,并发送一个信号 signal 到期望的线程中来告诉这个线程需要处理一个Notification。指定的线程在收到信号后将 Notification 从队列中移除,并进行处理。
官方 Demo 如下
```objectivec
@interface ViewController () <NSMachPortDelegate>
@property (nonatomic) NSMutableArray *notifications; // 通知队列
@property (nonatomic) NSThread *notificationThread; // 期望线程
@property (nonatomic) NSLock *notificationLock; // 用于对通知队列加锁的锁对象,避免线程冲突
@property (nonatomic) NSMachPort *notificationPort; // 用于向期望线程发送信号的通信端口
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"current thread = %@", [NSThread currentThread]);
// 初始化
self.notifications = [[NSMutableArray alloc] init];
self.notificationLock = [[NSLock alloc] init];
self.notificationThread = [NSThread currentThread];
self.notificationPort = [[NSMachPort alloc] init];
self.notificationPort.delegate = self;
// 往当前线程的run loop添加端口源
// 当Mach消息到达而接收线程的run loop没有运行时则内核会保存这条消息直到下一次进入run loop
[[NSRunLoop currentRunLoop] addPort:self.notificationPort
forMode:(__bridge NSString *)kCFRunLoopCommonModes];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:@"TestNotification" object:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil userInfo:nil];
});
}
- (void)handleMachMessage:(void *)msg {
[self.notificationLock lock];
while ([self.notifications count]) {
NSNotification *notification = [self.notifications objectAtIndex:0];
[self.notifications removeObjectAtIndex:0];
[self.notificationLock unlock];
[self processNotification:notification];
[self.notificationLock lock];
};
[self.notificationLock unlock];
}
- (void)processNotification:(NSNotification *)notification {
if ([NSThread currentThread] != _notificationThread) {
// Forward the notification to the correct thread.
[self.notificationLock lock];
[self.notifications addObject:notification];
[self.notificationLock unlock];
[self.notificationPort sendBeforeDate:[NSDate date]
components:nil
from:nil
reserved:0];
}
else {
// Process the notification here;
NSLog(@"current thread = %@", [NSThread currentThread]);
NSLog(@"process notification");
}
}
@end
```
这种方式先将当前线程存储下来,在收到通知的时候去遍历当前数组(数组代替队列),判断当前线程是不是目标线程,不是则将对头元素移动到对尾。
## FAQ
### 通知的发送是同步还是异步?
通过 postNotification 源码可以看到,通知的发送是同步的,且在同一个线程中
### 页面销毁时,不移除通知会 crash 吗?
通过这段文档,我们可以看出
- 使用 `addObserverForName:object:queue:usingBlock` 必须自己手动移除
- 使用 `addObserver:selector:name:object:` ios9后系统会自动移除
如何自动移除
ios9以后系统使用weak指针修饰observe,当observe被释放后再次发送消息给nil发送不会引起崩溃并且根据描述中提到系统会下次发送通知时移除这些oboserve为nil的观察者
### 多次添加同一个通知的观察者会出现什么问题?多次移除同一个通知会有问题吗?
查看 addObserver 源码会发现,针对同一个 NSNotificationName 进行多次添加,系统并不会过滤,假设有 object则会维护 NAMED MapTablekey 为 NSNotificationNamevalue 为子 MapTable子 MapTable 中 object 为 keyvalue 为 observer。所以多次添加则会造成当 postNotification 的时候会有多次响应。
查看 removeObserver 源码发现,移除都会针对 NSNotificationName 进行操作,从 NAMED MapTable 中,以 NSNofiticationName 为 key获取 value 为子 MapTable 。子 MapTable 根据 object 为 key获取对应的链表然后根据参数 observer 移除链表中所有 observer 都为传递的 observer 的节点。所以多次调用不会存在问题。

134
Chapter1 - iOS/1.105.md Normal file
View File

@@ -0,0 +1,134 @@
# iOS 界面渲染流程
## 渲染机制
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/RenderStructure.png)
iOS 渲染框架可以分为4层顶层是 UIKit包括图形界面的高级 API 和常用的各种 UI 控件。UIKit 下层是 Core Animation不要被名字误解了它不光是处理动画相关也在做图形渲染相关的事情(比如 UIView 的 CALayer 就处于 Core Animation 中)。Core Animation 之下就是由 OpenGL ES 和 CoreGraphics 组成的图形渲染层OpenGL ES 主要操作 GPU 进行图形渲染CoreGraphics 主要操作 CPU 进行图形渲染。上面3层都属于渲染图形软件层再下层就是图形显示硬件层。
iOS 图形界面的显示是一个复杂的流程,一部分数据通过 Core Graphics、Core Image 有 CPU 预处理,最终通过 OpenGL ES 将数据传输给 GPU最终显示到屏幕上。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/RenderPipeline.png)
- Core Animation 提交会话(事务),包括自己和子树(view hierarchy) 的布局状态
- Render Server 解析所提交的子树状态,生成绘制指令
- GPU 执行绘制指令
- 显示器显示渲染后的数据
## Core Animation
![](/Users/lbp/Desktop/Document/assets/APM-CoreAnimationPipeline.png)
可以看到 Core Animation pipeline 由4部分组成Application 层的 Core Animation 部分、Render Server 中的 Core Animation 部分、GPU 渲染、显示器显示。
### Application 层 Core Animation 部分
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/CoreAnimationCommit.png)
- 布局(Layout)`layoutSubviews``addSubview`,这里通常是 CPU、IO 繁忙
- 显示(Display):调用 view 重写的 `drawRect` 方法,或者绘制字符串。这里主要是 CPU 繁忙、消费较多内存。每个 UIView 都有 CALayer同时图层又一个像素存储控件存储视图调用 `setNeedsDisplay` 仅会设置图层为 dirty。当渲染系统准备就绪调用视图的 `display` 方法,同时装配像素存储空间,建立一个 Core Graphics 上下文(CGContextRef),将上下文 push 进上下文堆栈,绘图程序进入对应的内存存储空间。
- 准备(Prepare)图片解码、图片格式转换。GPU 不支持某些图片格式,尽量使用 GPU 能支持的图片格式
- 提交(Commit):打包 layers 并发送给 Render Server递归提交子树的 layers。如果子树层级较多(复杂),则对性能造成影响
### Render Server 中 Core Animation 部分
Render Server 是一个独立的渲染进程,当收到来自 Application 的 (IPC) 事务时,首先解析 layer 层级关系,然后 Decode。最后执行 Draw Calls(执行对应的 OpenGL ES 命令)
### GPU 渲染
- OpenGL ES 的 command buffer 进行定点变换,三角形拼接、光栅话变为 parameter buffer
- parameter buffer 进行像素变化testing、blending 生成 frame buffe
### 显示器显示
视频控制器从 frame buffer 中读取数据显示在显示屏上。
## UIView 绘制流程
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/UIRenderPipeline.png)
- 每个 UIView 都有一个 CALayerlayer 属性都有 contentscontents 其实是一块缓存,叫做 backing store
- 当 UIView 被绘制时CPU 执行 drawRect 方法,通过 context 将数据写入 backing store 中(位图 bitmap)
- 当 backing store 写完后,通过 Render Server 交给 GPU 去渲染,最后显示到屏幕上
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/UIViewRenderPipeline.png)
- 调用 `[UIView setNeedsDisplay]` 方法时,并没有立即执行绘制工作,而是马上调用 `[view.layer setNeedsDisplay]` 方法,给当前 layer 打上标记
- 在当前 RunLoop 快要结束的时候调用 layer 的 display 方法,来进入到当前视图真正的绘制流程
- 在 layer 的 display 方法内部,系统会判断 layer 的 layer.delegate 是否实现了 `displayLayer` 方法
- 如果没有,则执行系统的绘制流程
- 如果实现了,则会进入异步绘制流程
- 最后把绘制完的 backing store 提交给 GPU
### 系统绘制流程
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/iOSRenderProcess.png)
- 首先 CALayer 内部会创建一个 CGContextRef在 drwaRect 方法中,可以通过上下文堆栈取出 context拿到当前视图渲染上下文也就是 backing store
- 然后 layer 会判断是否存在代理,若没有,则调用 CALayer 的 drawInContext
- 如果存在代理,则调用代理方法。然后做当前视图的绘制工作,然后调用 view 的 drawRect 方法
- 最后由 CALayer 上传对应的 backing store(可以理解为位图)提交给 GPU。
### 异步绘制流程
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/iOSAsyncRender.png)
- 如果 layer 有代理对象,且代理对象实现了代理方法,则可以进入异步绘制流程
- 异步绘制流程中主要生成对应的 bitmap。目的是最后一步需要将 bitmap 设置为 layer.contents 的值
- 左侧是主队列,右侧是全局并发队列
- 调用了setNeedsDiaplay 方法后,在当前 Runloop 将要结束的时候,会有系统调用视图所对应 layer 的 display 方法
- 通过在子线程中去做位图的绘制,此时主线程可以去做些其他的工作。在子线程中:主要通过 CGBitmapContextCreate 方法,来创建一个位图的上下文、通过CoreGraphic API绘制 UI、通过 CGBitmapContextCreatImage 方法,根据所绘制的上下文,生成一张 CGImage 图片
- 然后再回到主队列中,提交这个位图,设置给 CALayer 的 contents 属性
## 图片加载库都做了什么事
众所周知iOS应用的渲染模式是完全基于Core Animation和CALayer的macOS上可选另说。因此当一个UIImageView需要把图片呈现到设备的屏幕上时候其实它的Pipeline是这样的
1. 一次Runloop完结 ->
2. Core Animation提交渲染树CA::render::commit ->
3. 遍历所有Layer的contents ->
4. UIImageView的contents是CGImage ->
5. 拷贝CGImage的Bitmap Buffer到SurfaceMetal或者OpenGL ES Texture上 ->
6. SurfaceMetal或者OpenGL ES渲染到硬件管线上
这个流程看起来没有什么问题但是注意Core Animation库自身虽然支持异步线程渲染在macOS上可以手动开启但是UIKit的这套内建的pipeline全部都是发生在主线程的。
因此当一个CGImage是采取了惰性解码通过Image/IO生成出来的那么将会在主线程触发先前提到的惰性解码callback实际上Core Animation的调用触发了一个`CGDataProviderRetainBytePtr`这时候Image/IO的具体解码器会根据先前的图像元信息去分配内存创建Bitmap Buffer这一步骤也发生在主线程。
这个流程带来的问题在于主线程过多的频繁操作会造成渲染帧率的下降。实验可以看出通过原生这一套流程对于一个1000*1000的PNG图片第一次滚动帧率大概会降低5-6帧iPhone 5S上当年有人的测试。后续帧率不受影响因为是惰性解码解码完成后的Bitmap Buffer会复用。
所以,最早不知是哪个团队的人(可能是[FastImageCache](https://github.com/path/FastImageCache)不确定发现并提出了另一种方案通过预先调用获取Bitmap强制Image/IO产生的CGImage解码这样到最终渲染的时候主线程就不会触发任何额外操作带来明显的帧率提升。后面的一系列图片库都互相效仿来解决这个问题。
具体到解决方案上目前主流的方式是通过CGContext开一个额外的画布然后通过`CGContextDrawImage`来画一遍原始的空壳CGImage由于在`CGContextDrawImage`的执行中,会触发到`CGDataProviderRetainBytePtr`因此这时候Image/IO就会立即解码并分配Bitmap内存。得到的产物用来真正产出一个CGImage-based的UIImage交由UIImageView渲染。
## ForceDecode的优缺点
上面解释了ForceDecode具体解决的问题当然这个方案肯定存在一定的问题不然苹果研发团队早已经改变了这套Pipeline流程了
优点:可以提升,图像第一次渲染到屏幕上时候的性能和滚动帧率
缺点提前解码会立即分配Bitmap Buffer的内存增加了内存压力。举例子对于一张大图2048*2048像素32位色来说就会立即分配16MB(2048 * 2048 * 4 Bytes)的内存。
由此可见这是一个拿空间换时间的策略。但是实际上iOS设备早期的内存都是非常有限的UIKit整套渲染机制很多地方采取的都是时间换空间因此最终苹果没有使用这套Pipeline而是依赖于高性能的硬件解码器+其他优化来保证内存开销稳定。当然作为图片库和开发者这就属于仁者见仁的策略了。如大量小图渲染的时候开启Force Decode能明显提升帧率同时内存开销也比较稳定。

256
Chapter1 - iOS/1.106.md Normal file
View File

@@ -0,0 +1,256 @@
# NSUserDefault 底层原理探究
最近看到字节一篇文章 [卡死崩溃监控原理及最佳实践](https://mp.weixin.qq.com/s?__biz=MzI1MzYzMjE0MQ==&mid=2247488080&idx=1&sn=39d0386b97b9ac06c6af1966f48387fc&chksm=e9d0d9b2dea750a4a7d21fd383aefa014d63f0dc79f2e3a13c97ad52bba1578dca8b50d6a40a&scene=21&cur_album_id=1590407423234719749#wechat_redirect) ,里面写到 NSUserDefault 底层实现存在直接或者间接跨进程通信,在主线程同步调用容易卡死。以前只是用过,但是没有仔细研究,看到这里就有必要研究下底层实现啦。
## 回顾
NSUserDefault 不安全。因为数据自动保存在沙盒的 `Libarary/Preferences` 目录下。
数据按照 plist property list格式存储在沙盒中。当攻击者破解 App 就可轻而易举拿到里面的数据(可能有些人会将 token、password、secret 明文存在里面)
另外 App 卸载重装会导致之前存储的数据丢失。这里推荐使用 Keychain。Keychain 是 iOS 提供的安全存储数据的方案,用于存储一些账号、密码等敏感信息。数据也不在沙盒中,即使删除 App重新安装则可以继续从 Keychain 中获取数据。
NSUserDefaults 的原理和 plist 序列化不同。
iOS 上应用必须被沙盒化,各个不同 App 之间的 Defaults Domain 不一样,通常是 Bundle Identifier或者是 App Group 中约定的 Suite Name。当使用 NSUserDefaults 的时候会按照下面 Domain 顺序:
- NSArgumentDomain
- 应用的 Bundle Identifier
- NSGlobalDomain
- 系统语言的标识符
- NSRegistrationDomain
任何应用,通过 NSUserDefaults 访问值都需要经历从上到下搜索各个 Domain 的过程,期间如何某个 Domain 有这个值,就会取出其对应的值。如果全部访问完还是没找到,则返回 undefined result。
## 如何保证多线程安全
通过设置符号断点可以看出, NSUserDefaults 内部在读写时会通过 `os_unfair_lock` 加锁进行多线程安全保护。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/NSUserDfault-lock.png)
## 存储性能如何
查看 GUN 源码
```c
- (BOOL) synchronize
{
BOOL isLocked = NO;
BOOL wasLocked = NO;
BOOL shouldLock = NO;
BOOL defaultsChanged = NO;
BOOL hasLocalChanges = NO;
// 合法性校验
if ([removed count] || [added count] || [modified count])
{
hasLocalChanges = YES;
}
if (YES == hasLocalChanges && NO == [owner _readOnly])
{
shouldLock = YES;
}
if (YES == shouldLock && YES == [owner _lockDefaultsFile: &wasLocked])
{
isLocked = YES;
}
NS_DURING
{
NSFileManager *mgr;
NSMutableDictionary *disk;
// 利用 NSFileManager 读取文件
mgr = [NSFileManager defaultManager];
disk = nil;
if (YES == [mgr isReadableFileAtPath: path])
{
NSData *data;
// 文件存在,则将里面的内容读取出来
data = [NSData dataWithContentsOfFile: path];
if (nil != data)
{
id o;
// 将文件数据利用 NSPropertyListSerialization 序列化为 NSDictionary 信息
o = [NSPropertyListSerialization
propertyListWithData: data
options: NSPropertyListImmutable
format: 0
error: 0];
// 将之前已经持久化好的本地文件数据写入到 disk 可变字典
if ([o isKindOfClass: [NSDictionary class]])
{
disk = AUTORELEASE([o mutableCopy]);
}
}
}
if (nil == disk)
{
disk = [NSMutableDictionary dictionary];
}
loaded = YES;
// 判断是否有新数据
if (NO == [contents isEqual: disk])
{
defaultsChanged = YES;
if (YES == hasLocalChanges)
{
NSEnumerator *e;
NSString *k;
// 从标记为待删除的数据中遍历,删除 disk 可变字典中的数据
e = [removed objectEnumerator];
while (nil != (k = [e nextObject]))
{
[disk removeObjectForKey: k];
}
// 遍历需要添加的数据,添加到 disk 中
e = [added objectEnumerator];
while (nil != (k = [e nextObject]))
{
[disk setObject: [contents objectForKey: k] forKey: k];
}
// 遍历需要修改的数据,添加到 disk 中
e = [modified objectEnumerator];
while (nil != (k = [e nextObject]))
{
[disk setObject: [contents objectForKey: k] forKey: k];
}
}
// 将 disk 数据拷贝到 contents
ASSIGN(contents, disk);
}
if (YES == hasLocalChanges)
{
BOOL written = NO;
if (NO == [owner _readOnly])
{
if (YES == isLocked)
{
// 判断 contents 字典是否有值,没有则给指定路径写入 nil
if (0 == [contents count])
{
/* Remove empty defaults dictionary.
*/
written = writeDictionary(nil, path);
}
else
{
/* Write dictionary to file.
*/
// 判断 contents 字典有值,则将 contents 给指定路径写入
written = writeDictionary(contents, path);
}
}
}
// 写入成功删除内存缓存
if (YES == written)
{
[added removeAllObjects];
[removed removeAllObjects];
[modified removeAllObjects];
}
}
if (YES == isLocked && NO == wasLocked)
{
isLocked = NO;
[owner _unlockDefaultsFile];
}
}
NS_HANDLER
{
fprintf(stderr, "problem synchronising defaults domain '%s': %s\n",
[name UTF8String], [[localException description] UTF8String]);
if (YES == isLocked && NO == wasLocked)
{
[owner _unlockDefaultsFile];
}
}
NS_ENDHANDLER
return defaultsChanged;
}
```
会发现:性能也就那么回事,底层实现通过内存缓存 `contents` 来缓存数据写入文件。
## NSUserDefaults 为什么触发 XPC 通信
通过对代码添加符号断点 `xpc_connection_send_message_with_reply_sync` 可以看到下面的堆栈
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/NSUserDefault-XPC.png)
执行 `[NSUserDefaults standardUserDefaults];` 可以发现是调用了 XPC创建名称为 “com.apple.cfprefsd.daemon” 的 XPC Connection且会发送一个 `xpc_connection_send_message_with_reply_sync` 的消息。
执行 `[defaults setObject:@"杭城小刘" forKey:@"name"];` 会调用 `xpc_connection_send_message_with_reply_sync`,发送一个消息。
通过 Demo 得出结论:
- `NSUserDefaults` 调用 `set...forKey:` 会触发 XPC 通信,调用 `...ForKey:``synchronized` 不会调用 XPC 通信
- 为了提高性能,尽量减少调用 `set...forKey:`
## 异步持久化
XPC 该`xpc_connection_send_message_with_reply_sync` API 因为 XPC 同步通信,所以在主线程容易存在卡死。那么有没有异步调用的能力?
发现2个 API 可以用于异步发送
- xpc_connection_send_message
- xpc_connection_send_message_with_reply
所以想异步持久化,则需要自定义 XPC Connection然后将数据用 xpc_dictionary_create 创造出的 Dictionary 去接,最后调用 `xpc_connection_send_message_with_reply` 去持久化数据
```objectivec
xpc_connection_t conn = xpc_connection_create_mach_service("com.apple.cfprefsd.daemon", NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED);
#pragma mark - 开始构建信息
// (lldb) po $rsi
// <OS_xpc_dictionary: dictionary[0x7fa975908010]: { refcnt = 1, xrefcnt = 1, subtype = 0, count = 8 } <dictionary: 0x7fa975908010> { count = 8, transaction: 0, voucher = 0x0, contents =
// "CFPreferencesHostBundleIdentifier" => <string: 0x7fa9759080d0> { length = 9, contents = "test.demo" }
// "CFPreferencesUser" => <string: 0x7fa975908250> { length = 25, contents = "kCFPreferencesCurrentUser" }
// "CFPreferencesOperation" => <int64: 0x8ccdbf87dd7d7a91>: 1
// "Value" => <string: 0x7fa9759084b0> { length = 16, contents = "酷酷的哀殿2" }
// "Key" => <string: 0x7fa975908430> { length = 3, contents = "key" }
// "CFPreferencesContainer" => <string: 0x7fa9759083a0> { length = 169, contents = "/private/var/mobile/Containers/Data/Application/0C224166-1674-4D36-9CDB-9FCDB633C7E3/" }
// "CFPreferencesCurrentApplicationDomain" => <bool: 0x7fff80002fd0>: true
// "CFPreferencesDomain" => <string: 0x7fa975906ea0> { length = 9, contents = "test.demo" }
// }>
xpc_object_t hello = xpc_dictionary_create(NULL, NULL, 0);
// 注释1test.demo 是 bundleid。测试代码时需要根据需要修改
xpc_dictionary_set_string(hello, "CFPreferencesHostBundleIdentifier", "test.demo");
xpc_dictionary_set_string(hello, "CFPreferencesUser", "kCFPreferencesCurrentUser");
// 注释2存储值
xpc_dictionary_set_int64(hello, "CFPreferencesOperation", 1);
// 注释3存储的内容
xpc_dictionary_set_string(hello, "Value", "this is a test");
xpc_dictionary_set_string(hello, "Key", "key");
// 注释4存储的位置
CFURLRef url = CFCopyHomeDirectoryURL();
const char *container = CFStringGetCStringPtr(CFURLCopyPath(url), kCFStringEncodingASCII);
xpc_dictionary_set_string(hello, "CFPreferencesContainer", container);
xpc_dictionary_set_bool(hello, "CFPreferencesCurrentApplicationDomain", true);
xpc_dictionary_set_string(hello, "CFPreferencesDomain", "test.demo");
xpc_connection_set_event_handler(conn, ^(xpc_object_t object) {
printf("xpc_connection_set_event_handler:收到返回消息: %sn", xpc_copy_description(object));
});
xpc_connection_resume(conn);
#pragma mark - 异步方案一 (没有回应)
// xpc_connection_send_message(conn, hello);
#pragma mark - 异步方案二 (有回应)
xpc_connection_send_message_with_reply(conn, hello, NULL, ^(xpc_object_t _Nonnull object) {
printf("xpc_connection_send_message_with_reply:收到返回消息: %sn", xpc_copy_description(object));
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; // mach_msg_trap
printf("从 NSUserDefaults 读取:%s\n", [defaults stringForKey:@"key"].UTF8String);
});
#pragma mark - 同步方案
// xpc_object_t obj = xpc_connection_send_message_with_reply_sync(conn, hello);
// NSLog(@"xpc_connection_send_message_with_reply_sync:收到返回消息:%s", xpc_copy_description(obj));
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; // mach_msg_trap
printf("从 NSUserDefaults 读取:%s\n", [defaults stringForKey:@"key"].UTF8String);
```

View File

@@ -33,7 +33,7 @@
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/RunLoop-SourceCode.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/RunLoop-SourceCode.png)
## RunLoop API
@@ -138,16 +138,20 @@ RunLoop 在各个 Mode 下做事情,其实就是在处理某个 Mode 中的 So
Source0:
- 屏幕触摸事件处理
- 屏幕触摸事件处理(非基于 Port 的事件对应需要手动触发的事件对应官方文档Input Source 中的 Custom 和 `performSelector:onThread` 事件源。
- `performSelector:onThread:`
- 数组
Source1:
- 基于 Port 的线程间通信
- 基于 Port 的线程间通信,可以主动唤醒 RunLoop
- 系统事件捕捉比如屏幕触摸事件Source1捕捉后派发给 Source0 处理)
- 字典。`{machport : 1}`
Timers
- NSTimer
@@ -388,7 +392,7 @@ CFRelease(obersver);
但是如何直到系统是运行 RunLoop 的哪个函数?给 viewDidLoad 设置断点,在 lldb 模式输入 `bt` 查看堆栈
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/RunLoop-Specific.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/RunLoop-Specific.png)
查看 CF 中 `CFRunLoop.c`源码。方法比较复杂,做了精简摘要
@@ -1497,7 +1501,7 @@ UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示
2. `[[NSRunLoop currentRunLoop] run]` api 换掉。查看系统说明,底层其实就是一个无限循环,循环内部不断调用 `runMode:beforeDate:`。下面也有建议,建议我们想销毁 RunLoop可以替换 API比如设置一个变量标记是否需要结束 RunLoop
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/RunLoop-RunIssue.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/RunLoop-RunIssue.png)
改进代码如下
@@ -1578,6 +1582,17 @@ UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示
}
```
线程保活后如何暂停?
```
[thread cancel];
thread = nil;
// 指针 nil还是被 RunLoop 持有。
// 也不行。CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);
```
主线程为什么
### 线程封装
思考:如何设计一个常驻线程工具类?

View File

@@ -325,22 +325,23 @@ dispatch_group_notify(group, queue, ^{
QA优先级反转是什么
线程本质上就是 CPU 高速切换,看上去是同时在做多个线程内的事情。也就是时间片轮转调度算法(进程、线程)。同时不同线程的优先级不一样
线程本质上就是 CPU 高速切换,看上去是同时在做多个线程内的事情。操作系统会使用基于优先级抢占式调度算法。高优先级的线程始终在低优先级线程前执行。
比如存在 thread1 的优先级最高、thread2 优先级普通。CPU 在调度到 thread2 的时候会加,加锁后 CPU 调度到 thread1在尝试给 thread1 加锁的时候发现锁被占用,所以此时在 thread1 里面自旋等待锁。thread2 等待 CPU 调度过来,但是因为 thread1 优先级比较高,所以 CPU 优先执行 thread1可是 thread1 里等待锁,锁此时被 thread2 占用。存在死锁
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/Thread_priority.jpg)
```
thread2 {
加锁
    做事情
    解锁
}
thread1 {
加锁(等待锁) while(未解锁)
    做事情
    解锁
}
```
线程 A 在 T1 时刻拿到锁,并处理数据。
线程 C 在 T2 时刻被唤醒,但是此时锁被线程 A 使用,所以线程 C 放弃 CPU 进入阻塞状态,而线程 A 继续占据 CPU执行任务
目前来看一切正常。
但是在 T3 时刻,线程 B 被唤醒,由于优先级比较高,所以会立即抢占 CPU此时线程 A 被迫进入 READY 状态等待。
T4 时刻,线程 B 放弃 CPU此时线程 A 优先级10是唯一处于 READY 状态的线程,所以再次占据 CPU 去执行任务,在 T5 时刻释放锁。
在 T5 时刻,线程 A 解锁瞬间,线程 C 立即获取锁并在优先级20上等待 CPU因为优先级比较高所以系统会立刻调度线程 C 的任务执行。此时线程 A 进入 READY 状态。
线程 B 从 T3 到 T4 这个时间段占据 CPU 资源的行为叫做优先级反转。一个优先级 15 的线程B通过压制优线级10的线程 A而事实上导致高优先级线程 C 无法正确得到 CPU。这段时间是不可控的因为线程 B 可以长时间占据 CPU即使轮转时间片到时线程 A 和 B 都处于可执行态但是因为B的优先级高它依然可以占据 CPU其结果就是高优先级线程 C 可能长时间无法得到 CPU。
上面的代码改进下
@@ -436,13 +437,13 @@ int cursorr = 1;
假如对存钱过程,忘记解锁怎么办?产生死锁,如下
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Thread-deadlock-unfaillock.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/Thread-deadlock-unfaillock.png)
添加 cursor 标记死锁是发生在 `saveMoney` 方法执行的第几次。发现是第二次。因为第一次锁没有任何使用方,所以加锁成功,当第二次加锁的时候发现锁没有释放,所以产生死锁。
这时候使用尝试加锁 API `os_unfair_lock_trylock` 即可成功如下
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Thread-deadlock-unfairTrylock.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/Thread-deadlock-unfairTrylock.png)
### pthread_mutex
@@ -515,8 +516,6 @@ int cursor = 0;
互斥锁提供 API 实现该功能。只需要在互斥锁初始化地方将属性修改为 `PTHREAD_MUTEX_RECURSIVE`。即 `pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);`在**同一个线程中可以多次获取同一把锁。并且不会死锁**。
### 互斥条件锁 pthread_cond_t
初始化互斥锁条件 `pthread_cond_init(&_condition, NULL);`
@@ -596,10 +595,6 @@ int cursor = 0;
- add 方法内加完元素会调用 `pthread_cond_signal` 来激活等待该条件的线程
### 从汇编角度分析 os_unfair_lock 属于什么锁(教你如何用汇编分析源码)
属于互斥锁。自旋锁是指在等锁的时候通过类似 while 循环的代码,让线程忙碌等到锁的到来。
@@ -637,21 +632,21 @@ sistep instruction简写为 stepisi。当你在 Xcode 汇编面板看
第一步:当第二次调用 saveMoney 方法,开启汇编调试
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/OSSpinLock-Assemble2.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/OSSpinLock-Assemble2.png)
看到可疑方法 `OSSpinLockLock`给它加断点看到第10行高亮了。lldb 模式输入 c敲回车。次数输入 si 即可进入 `OSSpinLockLock`  方法内部调试
第二步:继续输入 si敲回车
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/OSSpinLock-Assemble3.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/OSSpinLock-Assemble3.png)
第三步:看到可疑方法 `_OSSpinLockLockSlow`给它加断点lldb 输入 C。此时断点到这一行了继续输入 si。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/OSSpinLock-Assemble4.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/OSSpinLock-Assemble4.png)
第四步:在 `OSSpinLockLockSlow` 方法内部调试,不断输入 si。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/OSSpinLockAssemble1.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/OSSpinLockAssemble1.png)
发现不断 si 最终一直会在第6行到第19行之间执行。懂汇编的会发现这其实是一个 while 循环。便可以证明自旋锁 OSSpinLock 在等锁的时候,底层实现是执行 while 循环,忙等,太浪费性能了。
@@ -665,10 +660,6 @@ sistep instruction简写为 stepisi。当你在 Xcode 汇编面板看
同样的步骤研究 `pthread_mutex_t` 会发现最后也是调用 `syscall` 做到线程休眠,不像自旋锁一样,在底层实现是 while 循环一样忙等,浪费资源。
### NSLock、NSRecursiveLock
NSLock 是对 mutex 普通锁pthread_mutex_t的封装
@@ -679,7 +670,7 @@ NSRecursiveLock 是对 mutex 递归锁pthread_mutex_t ,且 attr 为 `PTHREA
```objectivec
+ (void) initialize{
static BOOL beenHere = NO;
static BOOL beenHere = NO;
if (beenHere == NO){
beenHere = YES;
/* Initialise attributes for the different types of mutex.
@@ -735,7 +726,7 @@ NSRecursiveLock 是对 mutex 递归锁pthread_mutex_t ,且 attr 为 `PTHREA
- (id) init{
if (nil != (self = [super init])) {
if (0 != pthread_mutex_init(&_mutex, &attr_recursive)){
DESTROY(self);
DESTROY(self);
}
}
return self;
@@ -749,7 +740,7 @@ pthread_mutexattr_init(&attr_recursive);
pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE);
```
NSRecursiveLock 不能在多线程下递归调用。@synchronized 可以在多线程下递归调用。底层原因是 TLS 有关。
### NSCondition
@@ -814,7 +805,7 @@ Demo
@end
```
存在 `虚假唤醒` 的问题。则可以将后续的 if 判断换为 while。比如某一时刻发送了一次 signal然后可能有多个线程收到唤醒的信号则可能还是会存在问题。所以 if 换为 while。
### NSCondtionLock
@@ -856,20 +847,12 @@ Demo
通过 NSCondtionLock 可以控制线程的执行顺序。
### dispatch_queue
使用 GCD 的串行队列,也是可以实现线程同步。
线程同步的本质就是多线程的任务是顺序执行
### dispatch_semaphore
semaphore 叫做”信号量”
@@ -912,16 +895,35 @@ semaphore 叫做”信号量”
`dispatch_semaphore_signal` 函数的本质:让信号量的值 + 1
所以如何让线程同步?设置信号量的值=1即可。保证同一时间只有一个线程任务在执行
NSCache 扩容策略。x86 3/4arm7/8.
有趣的实验:
```objectivec
self.semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
```
上面的代码会 crash。因为创建出来信号量为1但是经过 dispatch_semaphore_wait 之后信号量变为0底层会调用到 `_dispatch_semaphore_dispose`。内部会做判断,就是原始的信号量
```objectivec
void _dispatch_semaphore_dispose(dispatch_object_t dou,
DISPATCH_UNUSED bool *allow_free){
dispatch_semaphore_t dsema = dou._dsema;
if (dsema->dsema_value < dsema->dsema_orig) {
DISPATCH_CLIENT_CRASH(dsema->dsema_orig - dsema->dsema_value,
"Semaphore object deallocated while in use");
}
_dispatch_sema4_dispose(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
}
```
### @synchronized
@synchronized 可递归重入的原理分析/线程缓存空间
```objectivec
@interface ViewController ()
@property (nonatomic, assign) NSInteger money;
@@ -972,7 +974,7 @@ semaphore 叫做”信号量”
为了探究下实现,开启汇编调试
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/synchronized-asemble.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/synchronized-asemble.png)
可以查看 objc4 的源码,查找 `objc_sync_enter`
@@ -1088,10 +1090,6 @@ class recursive_mutex_tt : nocopy_t {
另外 `recursive_mutex_tt` 在初始化的时候传入 `PTHREAD_RECURSIVE_MUTEX_INITIALIZER`,看起来也支持递归。所以 @synchronized 是一个递归互斥锁的封装。
封装
有的时候我们需要在方法内部创建 semaphore ,则可以创建宏
@@ -1107,11 +1105,8 @@ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
#define SemaphoreEnd \
dispatch_semaphore_signal(semaphore);
```
### 自旋锁、互斥锁对比
什么情况使用自旋锁比较划算?
@@ -1136,11 +1131,7 @@ dispatch_semaphore_signal(semaphore);
- 临界区竞争非常激烈
## atomic
### atomic
`atomic` 用于保证属性 setter、getter 的原子性操作,相当于在 getter 和 setter 内部加了线程同步的锁。
@@ -1155,13 +1146,13 @@ id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
@@ -1182,7 +1173,7 @@ static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t o
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
id *slot = (id*) ((char*)self offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
@@ -1213,6 +1204,13 @@ void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL ato
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
void lock() {
    lockdebug_mutex_lock(this);
// <rdar://problem/50384154>
uint32_t opts = OS_UNFAIR_LOCK_DATA_SYNCHRONIZATION | OS_UNFAIR_LOCK_ADAPTIVE_SPIN;
os_unfair_lock_lock_with_options_inline(&mLock, (os_unfair_lock_options_t)opts);
}
```
可以看到设置属性的时候会判断是不是 atomic
@@ -1223,15 +1221,38 @@ void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL ato
它并不能保证使用属性的过程是线程安全的。
QA为什么在 iOS 上几乎没有使用?
因为属性 getter、setter 使用太高频,另外 atomic 内部实现是自旋锁,自旋锁是忙等,所以太耗费性能了。
#### atomic 并不能保证使用属性的过程是线程安全的?
```objectivec
@property (atomic,copy) NSString *name;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(self){
for (int i = 0; i<100; i++) {
self.name = @"杭城小刘";
NSLog(@"线程1 %@",self.name);
}
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(self){
for (int i = 0; i<100; i++) {
self.name = @"魅影";
NSLog(@"线程2 %@",self.name);
}
}
});
```
预期:线程 A 打印出来一定是杭城小刘,线程 B 打印出来是魅影。但事实上可能存在乱序。
atomic 是原子属性,它内部实现是针对属性的 setter、getter 进行加锁(早期实现是自旋旋,因为存在问题,后续替换为了 os_unfair_lock。但是事实上在进行多线程编程的时候我们针对数据的操作并不是修改指针本身思考 NSString 的 getter、setter而是操作类似 NSArray、NSDictionary 这样的 case。比如 `@property(atomic, strong)NSMutableArray *hobbies;` 如果在多线程情况下进行处理,一边生产者添加数据,一边消费者消费数据,则会产生内存问题。
所以多线程并发编程来说,推荐使用锁是一个合理的方案。此外自旋锁不推荐使用,互斥锁中 pthread_mutex 等性能高一些的锁推荐使用。
## 读写安全
@@ -1245,8 +1266,6 @@ QA为什么在 iOS 上几乎没有使用?
- dispatch_barrier_async异步栅栏调用
### pthread_rwlock
初始化
@@ -1268,8 +1287,6 @@ pthread_rwlock_init(&_lock, NULL)
销毁 `pthread_rwlock_destroy(&_lock);`
```objectivec
@interface ViewController ()
@property (nonatomic, assign) NSInteger money;
@@ -1324,8 +1341,6 @@ pthread_rwlock_init(&_lock, NULL)
2022-04-09 22:25:25.045020+0800 DDD[13652:333135] read 200
```
### dispatch_barrier_async
```objectivec
@@ -1346,8 +1361,6 @@ dispatch_barrier_async(self.queue, ^{
- 这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
- 如果传入的是一个串行或是一个全局的并发队列那这个函数便等同于dispatch_async函数的效果
上 Demo
```objectivec
@@ -1402,4 +1415,50 @@ dispatch_barrier_async(self.queue, ^{
2022-04-09 22:37:26.477247+0800 DDD[14019:343937] read 200
2022-04-09 22:37:26.477267+0800 DDD[14019:343938] read 200
2022-04-09 22:37:26.477269+0800 DDD[14019:343933] read 200
```
```
## 其他常见的多线程编程模式
### Promise
Promise 在多线程解决方案中比较常见,比如在前端中 Promise 就是一个标准解决方案。同样的iOS 界也有三方开发者写的 PromiseKit。也有对应的 AFNetworking Promise 版本。
Promise 解决了什么问题?
- 在需要多个操作的时候我们可能会设置多个回调参数嵌套导致代码很长也就是传说中的“回调地狱”Callback Hell
- 丧失了 return 特性
Promise 就是一个对象,用来传递异步操作的消息。代表了某个未来才会知道结果的事件(也就是异步操作),并且这个事件提供统一的 API可以供进一步处理
对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中,又称 Incomplete)、Resolved(已完成,又称 Fulfilled)和 Rejected (已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。
一旦状态改变就不会再变任何时候都可以得到这个结果。Promise对象的状态改变只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生状态就凝固了不会再变了会一直保持这个结果。就算改变已经发生了你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
```objectivec
APIClient.fetchData(...).then().onFailure();
```
### Pipeline
将一个任务分解为若干个阶段(Stage),前阶段的输出为下阶段的输入,各个阶段由不同的工
作者线程负责执行。
各个任务的各个阶段是并行(Parallel)处理的。
具体任务的处理是串行的,即完成一个任务要依次执行各个阶段,但从整体任务上看,不同任务的各个阶段的执行是并行的。
### Master-Slave
将一个任务分解为若干个语义等同的子任务,并由专门的工作者线程来并行执行这些子任务,既 提高计算效率,又实现了信息隐藏。
比如 Jekins
### Serial Thread Confinement
如果并发任务的执行涉及某个非线程安全对象,而很多时候我们又不希望因此而引入锁。
通过将多个并发的任务存入队列实现任务的串行化,并为这些串行化任务创建唯一的工作者线程进行处理。
比如 FMDB 的设计,内部就是一个串行队列。

View File

@@ -1,4 +1,4 @@
# 内存问题研
# iOS 内存原理探
## 定时器内存泄漏
@@ -10,7 +10,7 @@ NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval
栈、堆、BSS、数据段、代码段
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/iOS-MemoryLayout.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/iOS-MemoryLayout.png)
stack又称作堆栈用来存储程序的局部变量但不包括static声明的变量static修饰的数据存放于数据段中。除此之外在函数被调用时栈用来传递参数和返回值。栈内存地址越来越少
@@ -74,10 +74,12 @@ obj: 0x6000012780e0
height: 0x7ff7b83fdbb8
age: 0x7ff7b83fdbbc
```
```
NSObject *obj = [[NSObject alloc] init];
NSLog(@"%p %p %@", obj, &obj, obj);
```
分别打印 obj指针指向的堆上的内存地址、obj 指针在栈上的地址、obj 内容
## Tagged Pointer
@@ -145,7 +147,7 @@ Demo1
运行该代码会 Crash报错信息如下
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/TaggedPointerCrash.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/TaggedPointerCrash.png)
说明:一开始的报错信息只说坏内存访问,但是并没有显示具体的方法调用堆。想知道具体 Crash 原因还是需要看看堆栈比较方便。输入 bt 查看最后是由于 `objc_release` 方法造成 crash。
@@ -157,8 +159,9 @@ Demo1
-(void)setName:(NSString *)name
{
if (_name!=name) {
[name retain];
[_name release];
_name = [name retain];
_name = name;
}
}
```
@@ -495,9 +498,41 @@ NSLog(@"%@", p3);
当用 `__unsafe_unretained` 修饰后,虽然释放了,但是内存还没回收,这时候去使用很容易出错。
## dealloc
## dealloc 是如何工作的?
查看 objc4 源码
在 MRC 时代,写完代码都需要显示在 dealloc 方法中做一些内存回收之类的工作。对象析构时将内部对象先 release 掉,非 OC 对象(比如定时器、c 对象、CF 对象等) 也需要回收内存,最后调用 `[super dealloc]` 继续将父类对象做析构。
```objectivec
- (void)dealloc {
CFRelease(XX);
self.timer = nil;
[super dealloc];
}
```
但在 ARC 时代dealloc 中一般只需要写一些非 OC 对象的内存释放工作,比如 CFRelease()
带来2个问题
- 类中的实例变量在哪释放?
- 当前类中没有显示调用 `[super dealloc]` ,父类的析构如何触发?
### LLVM 文档对 dealloc 的描述
[LLVM ARC 文档对 dealloc 描述](https://clang.llvm.org/docs/AutomaticReferenceCounting.html#dealloc) 如下
> A class may provide a method definition for an instance method named `dealloc`. This method will be called after the final `release` of the object but before it is deallocated or any of its instance variables are destroyed. The superclasss implementation of `dealloc` will be called automatically when the method returns.
>
> The instance variables for an ARC-compiled class will be destroyed at some point after control enters the `dealloc` method for the root class of the class. The ordering of the destruction of instance variables is unspecified, both within a single class and between subclasses and superclasses.
根据描述可以看到 dealloc 方法在最后一次 release 方法调用后触发但实例变量ivars 还未释放,父类的 dealloc 方法将会在子类 dealloc 方法返回后自动调用。
ARC 模式下,对象的实例变量会在根类 [NSObject dealloc] 中释放,但是释放的顺序是不一定的。
也就是说会自动调用 `[super dealloc]`,那到底如何实现的,探究下。
### 查看 objc4 源码
```c
- (void)dealloc {
@@ -619,6 +654,299 @@ weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
}
```
可以清楚看到在 `objc_destructInstance` 方法中调用了3个核心方法
- object_cxxDestruct(obj) 清除成员变量
- object_remove_assocations(obj):去除该对象相关的关联属性(Category 添加的)
- obj->clearDeallocating():清空引用技术表和弱引用表,将 weak 引用设置为 nil
继续看看 object_cxxDestruct 方法内部细节。
### 神秘的 cxx_destruct
`object_cxxDestruct` 方法最终会调用到 `object_cxxDestructFromClass`
```c
void object_cxxDestruct(id obj) {
if (_objc_isTaggedPointerOrNil(obj)) return;
object_cxxDestructFromClass(obj, obj->ISA());
}
static void object_cxxDestructFromClass(id obj, Class cls) {
void (*dtor)(id);
// Call cls's dtor first, then superclasses's dtors.
for ( ; cls; cls = cls->getSuperclass()) {
if (!cls->hasCxxDtor()) return;
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
// 调用
if (dtor != (void(*)(id))_objc_msgForward_impcache) {
if (PrintCxxCtors) {
_objc_inform("CXX: calling C++ destructors for class %s",
cls->nameForLogging());
}
(*dtor)(obj);
}
}
}
```
做的事情就是遍历,不断寻找父类中 `SEL_cxx_destruct`这个 selector找到函数实现并调用。
```c
void sel_init(size_t selrefCount){
#if SUPPORT_PREOPT
if (PrintPreopt) {
_objc_inform("PREOPTIMIZATION: using dyld selector opt");
}
#endif
namedSelectors.init((unsigned)selrefCount);
// Register selectors used by libobjc
mutex_locker_t lock(selLock)
SEL_cxx_construct = sel_registerNameNoLock(".cxx_construct", NO);
SEL_cxx_destruct = sel_registerNameNoLock(".cxx_destruct", NO);
}
```
继续翻阅源码发现 `SEL_cxx_destruct` 其实就是 `.cxx_destruct`。在 《Effective Objective-C 2.0》中说明:
> When the compiler saw that an object contained C++ objects, it would generate a method called .cxx_destruct. ARC piggybacks on this method and emits the required cleanup code within it.
也就是说,当编译器看到 C++ 对象的时候,它将会生成 `.cxx_destruct` 析构方法,但是 ARC 借用这个方法,并在其中插入了代码以实现自动内存释放的功能。
### 探究啥时候生成 .cxx_destruct 方法
```objectivec
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
//
- (void)viewDidLoad {
[super viewDidLoad];
{
NSLog(@"comes");
Person *p = [[Person alloc] init];
p.name = @"杭城小刘";
NSLog(@"gone");
}
}
```
在 gone 处加断点,利用 runtime 查看类中的方法信息
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/cxx_destructDemo1.png)
发现存在 `.cxx_destruct` 方法。
我们一开要研究的是 ivars 啥时候释放,所以控制变量,将属性改为成员对象
```objectivec
@interface Person : NSObject
{
@public
NSString *name;
}
@end
{
NSLog(@"comes");
Person *p = [[Person alloc] init];
p->name = @"杭城小刘";
NSLog(@"gone");
}
```
也有 `.cxx_destruct` 方法
将成员变量换为基本数据类型
```objectivec
@interface Person : NSObject
{
@public
int age;
}
@end
```
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/cxx_destructdemo3.png)
Tips@property 会自动生成成员变量,另外类后面加 `{}` 在内部也可以加成员变量,假如成员变量是对象类型,比如 NSString则叫实例变量。
得出结论:
- 只有 ARC 模式下才有 `.cxx_destruct` 方法
- 类拥有实例变量的时候(`{}` 或者 `@property`) 才有 `.cxx_destruct`,父类成员对象的实例变量不会让子类拥有该方法
使用 watchpoint 观察内存释放时机
在 gone 的地方加断点,输入 `watchpoint set variable p->_name`,则会将 `_name` 实例变量加入 watchpoint当变量被修改时会触发断点可以看出从某个值变为 0x0也就是 nil。此时边上调用堆栈显示在 `objc_storestrong` 方法中,被设置为 nil.
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/cxx_destructDemo2.png)
### 深入 .cxx_destruct
简单梳理下,在 ARC 模式下,类拥有实例变量的时候会在 `.cxx_destruct` 方法内调用 `objc_storeStrong` 去释放的内存。
我们也知道 `.cxx_destruct` 是编译器生成的代码。去查询资料 `.cxx_destruct site:clang.llvm.org`
在 clang 的 doxygen 文档中 [CodeGenModule 模块源码](https://clang.llvm.org/doxygen/CodeGenModule_8cpp_source.html)发现了相关逻辑。在 5907 行代码
```c
void CodeGenModule::EmitObjCIvarInitializations(ObjCImplementationDecl *D) {
// We might need a .cxx_destruct even if we don't have any ivar initializers.
if (needsDestructMethod(D)) {
IdentifierInfo *II = &getContext().Idents.get(".cxx_destruct");
Selector cxxSelector = getContext().Selectors.getSelector(0, &II);
ObjCMethodDecl *DTORMethod = ObjCMethodDecl::Create(
getContext(), D->getLocation(), D->getLocation(), cxxSelector,
getContext().VoidTy, nullptr, D,
/*isInstance=*/true, /*isVariadic=*/false,
/*isPropertyAccessor=*/true, /*isSynthesizedAccessorStub=*/false,
/*isImplicitlyDeclared=*/true,
/*isDefined=*/false, ObjCMethodDecl::Required);
D->addInstanceMethod(DTORMethod);
CodeGenFunction(*this).GenerateObjCCtorDtorMethod(D, DTORMethod, false);
D->setHasDestructors(true);
}
// If the implementation doesn't have any ivar initializers, we don't need
// a .cxx_construct.
if (D->getNumIvarInitializers() == 0 ||
AllTrivialInitializers(*this, D))
return;
IdentifierInfo *II = &getContext().Idents.get(".cxx_construct");
Selector cxxSelector = getContext().Selectors.getSelector(0, &II);
// The constructor returns 'self'.
ObjCMethodDecl *CTORMethod = ObjCMethodDecl::Create(
getContext(), D->getLocation(), D->getLocation(), cxxSelector,
getContext().getObjCIdType(), nullptr, D, /*isInstance=*/true,
/*isVariadic=*/false,
/*isPropertyAccessor=*/true, /*isSynthesizedAccessorStub=*/false,
/*isImplicitlyDeclared=*/true,
/*isDefined=*/false, ObjCMethodDecl::Required);
D->addInstanceMethod(CTORMethod);
CodeGenFunction(*this).GenerateObjCCtorDtorMethod(D, CTORMethod, true);
D->setHasNonZeroConstructors(true);
}
```
源码大概做的事情就是:获取 `.cxx_destructor` 的 selector创建 Method然后将新创建的 Method 插入到 class 方法列表中。调用 `GenerateObjCCtorDtorMethod` 方法,才创建这个方法的实现。查看 GenerateObjCCtorDtorMethod 的实现。在 https://clang.llvm.org/doxygen/CGObjC_8cpp_source.html 的1626行处。
```c
static void emitCXXDestructMethod(CodeGenFunction &CGF,
ObjCImplementationDecl *impl) {
CodeGenFunction::RunCleanupsScope scope(CGF);
llvm::Value *self = CGF.LoadObjCSelf();
const ObjCInterfaceDecl *iface = impl->getClassInterface();
for (const ObjCIvarDecl *ivar = iface->all_declared_ivar_begin();
ivar; ivar = ivar->getNextIvar()) {
QualType type = ivar->getType();
// Check whether the ivar is a destructible type.
QualType::DestructionKind dtorKind = type.isDestructedType();
if (!dtorKind) continue;
CodeGenFunction::Destroyer *destroyer = nullptr;
// Use a call to objc_storeStrong to destroy strong ivars, for the
// general benefit of the tools.
if (dtorKind == QualType::DK_objc_strong_lifetime) {
destroyer = destroyARCStrongWithStore;
// Otherwise use the default for the destruction kind.
} else {
destroyer = CGF.getDestroyer(dtorKind);
}
CleanupKind cleanupKind = CGF.getCleanupKind(dtorKind);
CGF.EHStack.pushCleanup<DestroyIvar>(cleanupKind, self, ivar, destroyer,
cleanupKind & EHCleanup);
}
assert(scope.requiresCleanups() && "nothing to do in .cxx_destruct?");
}
```
可以看到:遍历了当前对象的所有实例变量,调用 `objc_storeStrong`,从 clang 文档上可以看出
```c
id objc_storeStrong(id *object, id value) {
value = [value retain];
id oldValue = *object;
*object = value;
[oldValue release];
return value;
}
```
`.cxx_destruct` 方法内部会对所有的实例变量调用 `objc_storeStrong(&ivar, null)` ,实例变量就会 release 。
### 自动调用 [super dealloc] 的原理
同理CodeGen 也会做自动调用 `[super dealloc]` 的事情。https://clang.llvm.org/doxygen/CGObjC_8cpp_source.html第751行 `StartObjCMethod` 方法。
```c
751 void CodeGenFunction::StartObjCMethod(const ObjCMethodDecl *OMD,
752 const ObjCContainerDecl *CD) {
// ...
789 // In ARC, certain methods get an extra cleanup.
790 if (CGM.getLangOpts().ObjCAutoRefCount &&
791 OMD->isInstanceMethod() &&
792 OMD->getSelector().isUnarySelector()) {
793 const IdentifierInfo *ident =
794 OMD->getSelector().getIdentifierInfoForSlot(0);
795 if (ident->isStr("dealloc"))
796 EHStack.pushCleanup<FinishARCDealloc>(getARCCleanupKind());
797 }
798 }
```
可以看到在调用到 dealloc 方法时,插入了代码,实现如下
```c
struct FinishARCDealloc : EHScopeStack::Cleanup {
void Emit(CodeGenFunction &CGF, Flags flags) override {
const ObjCMethodDecl *method = cast<ObjCMethodDecl>(CGF.CurCodeDecl);
const ObjCImplDecl *impl = cast<ObjCImplDecl>(method->getDeclContext());
const ObjCInterfaceDecl *iface = impl->getClassInterface();
if (!iface->getSuperClass()) return;
bool isCategory = isa<ObjCCategoryImplDecl>(impl);
// Call [super dealloc] if we have a superclass.
llvm::Value *self = CGF.LoadObjCSelf();
CallArgList args;
CGF.CGM.getObjCRuntime().GenerateMessageSendSuper(CGF, ReturnValueSlot(),
CGF.getContext().VoidTy,
method->getSelector(),
iface,
isCategory,
self,
/*is class msg*/ false,
args,
method);
}
};
```
代码大概就是向父类转发 dealloc 的调用实现,内部自动调用 [super dealloc] 方法。
总结下:
- ARC 模式下,实例变量由编译器插入 `.cxx_destruct` 方法自动释放
- ARC 模式下 `[super dealloc]` 由 llvm 编译器自动插入(CodeGen)
## 自动释放池底层原理探索
上 Demo
@@ -705,7 +1033,7 @@ class AutoreleasePoolPage {
- 每个 AutoreleasePoolPage 对象占用 4096 字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放 autorelease 对象的地址
- 所有的 AutoreleasePoolPage 对象通过**双向链表**的形式连接在一起。child 指向下一个对象parent 指向上一个对象
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/autoreleasepool.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/autoreleasepool.png)
```objectivec
id * begin() {
@@ -761,7 +1089,7 @@ int main(int argc, const char * argv[]) {
main 方法内部3个 autoreleasepool 底层怎么样工作的?
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/AutoreleasePoolMoreItem.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/AutoreleasePoolMoreItem.png)
3个@auto releasepool 系统遇到第一个的时候底层就是初始化一个结构体 `__AtAutoreleasePool`,结构体构造方法内部调用 `AutoreleasePoolPage::push` 方法,系统给 AutoreleasePoolPage 真正保存 autorelease 对象的地方存储进一个 `POOL_BOUNDARY` 对象,然后储存 P1、P2 对象地址,遇到第二个则继续初始化结构体,调用 push 方法,存储一个` POOL_BOUNDARY` 对象,继续保存 P3遇到第三个则继续初始化结构体调用 push 方法,存储一个 `POOL_BOUNDARY` 对象,继续保存 P4。
@@ -1535,7 +1863,7 @@ iOS 在主线程的 Runloop 中注册了2个 Observer
结合 RunLoop 运行图
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/RunLoop-SourceCode.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/RunLoop-SourceCode.png)
- 01 通知 Observer 进入 Loop 会调用 `objc_autoreleasePoolPush`

View File

@@ -243,7 +243,7 @@ dispatch_semaphore_t semaphore_;
说明:直接 `performSelector` 存在警告,可以告诉编译器忽略警告。可以在 Xcode 点开警告,查看详情,复制 `[]` 里面的字符串去忽略警告
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/ignoreXcodewarning.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/ignoreXcodewarning.png)

View File

@@ -281,7 +281,7 @@ KVC 之后会触发 KVO。为什么探究下 `setValueForKey`
整个流程如下
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/KVC-process.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/KVC-process.png)
```
@implementation Person
@@ -315,6 +315,6 @@ valueForKey 原理
- 都没找到则抛出异常
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/KVC-get-process.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/KVC-get-process.png)

View File

@@ -957,7 +957,7 @@ c 数组指针 `array()->lists + addedCount` 可以代表其中的位置。
过程如下
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-categoryattachLists.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-categoryattachLists.png)
QA
@@ -1716,7 +1716,7 @@ Category 是在运行阶段,才会将数据合并到类信息中。
查看分类在 Runtime 加载类信息时候的调用原理可以知道分类中的类方法、对象方法都会被加载原始类的前面去initialize 是类方法)如下图:
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-categoryattachLists.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-categoryattachLists.png)

View File

@@ -1,48 +1,59 @@
# 开发效率提升利器
> 软件的生命周期贯穿产品的开发,测试,生产,用户使用,版本升级和后期维护等过程,只有易读,易维护的软件代码才具有生命力。
## 一、思路
最近重构项目组件,看到项目中存在一些命名和方法分块方面存在一些问题,结合平时经验和 [Apple官方代码规范](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CodingGuidelines/CodingGuidelines.html) 在此整理出 iOS 工程规范。提出第一个版本,如果后期觉得有不完善的地方,继续提出来不断完善,文档在此记录的目的就是为了大家的代码可读性较好,后来的人或者团队里面的其他人看到代码可以不会因为代码风格和可读性上面造成较大时间的开销。
先梳理出规范,然后使用一些脚本的方式,提高大家使用的便捷性与效率,最后开发一些协作脚本。
## 二、一些原则
1. 长的,描述性的方法和变量命名是好的。不要使用简写,除非是一些大家都知道的场景比如 VIP。不要使用 bgView推荐使用 backgroundView
2. 见名知意。含义清楚,做好不加注释代码自我表述能力强。(前提是代码足够规范)
3. 不要过分追求技巧,降低代码可读性
4. 删除没必要的代码。比如我们新建一个控制器,里面会有一些不会用到的代码,或者注释起来的代码,如果这些代码不需要,那就删除它,留着偷懒吗?下次需要自己手写
5. 在方法内部不要重复计算某个值,适当的情况下可以将计算结果缓存起来
6. 尽量减少单例的使用。
7. 提供一个统一的数据管理入口,不管是 MVC、MVVM、MVP 模块内提供一个统一的数据管理入口会使得代码变得更容易管理和维护。
8. 除了 .m 文件中方法,其他的地方"{"不需要另起一行。
```Objective-c
```Objective-c
- (void)getGooodsList
{
{
...
}
}
- (void)doHomework
{
{
if (self.hungry) {
return;
}
if (self.thirsty) {
return;
}
if (self.tired) {
return;
}
papapa.then.over;
}
```
}
```
```
### 变量
@@ -58,80 +69,87 @@
### 条件表达式
1. 当有条件过多、过长的时候需要换行,为了代码看起来整齐些
```
//good
if (condition1() &&
```
//good
if (condition1() &&
condition2() &&
condition3() &&
condition4()) {
// Do something
}
//bad
if (condition1() && condition2() && condition3() && condition4() { // Do something }
```
// Do something
}
//bad
if (condition1() && condition2() && condition3() && condition4() { // Do something }
```
2. 在一个代码块里面有个可能的情况时善于使用 `return` 来结束异常的情况。
```
```
- (void)doHomework
{
{
if (self.hungry) {
return;
}
if (self.thirsty) {
return;
}
if (self.tired) {
return;
}
papapa.then.over;
}
```
}
```
3. 每个分支的实现都必须使用 {} 包含。
```
// bad
if (self.hungry) self.eat()
// good
if (self.hungry) {
```
// bad
if (self.hungry) self.eat()
// good
if (self.hungry) {
self.eat()
}
```
}
```
4. 条件判断的时候应该是变量在左,条件在右。 if ( currentCursor == 2 ) { //... }
5. switch 语句后面的每个分支都需要用大括号括起来。
6. switch 语句后面的 default 分支必须存在,除非是在对枚举进行 switch。
```
switch (menuType) {
case menuTypeLeft: {
```
switch (menuType) {
case menuTypeLeft: {
...
break;
}
case menuTypeRight: {
case menuTypeRight: {
...
break;
}
case menuTypeTop: {
}
case menuTypeTop: {
...
break;
}
case menuTypeBottom: {
}
case menuTypeBottom: {
...
break;
}
}
```
}
}
```
### 类名
1. 大写驼峰式命名。每个单词首字母大写。比如「申请记录控制器」ApplyRecordsViewController
2. 每个类型的命名以该类型结尾。
- ViewController使用 `ViewController` 结尾。例子ApplyRecordsViewController
- View使用 `View` 结尾。例子分界线boundaryView
- NSArray使用 `s` 结尾。比如商品分类数据源。categories
- UITableViewCell使用 `Cell` 结尾。比如 MyProfileCell
- Protocol使用 `Delegate` 或者 `Datasource` 结尾。比如 XQScanViewDelegate
- Tool工具类
- 代理类Delegate
- Service 类Service
- ViewController使用 `ViewController` 结尾。例子ApplyRecordsViewController
- View使用 `View` 结尾。例子分界线boundaryView
- NSArray使用 `s` 结尾。比如商品分类数据源。categories
- UITableViewCell使用 `Cell` 结尾。比如 MyProfileCell
- Protocol使用 `Delegate` 或者 `Datasource` 结尾。比如 XQScanViewDelegate
- Tool工具类
- 代理类Delegate
- Service 类Service
### 类的注释
@@ -140,6 +158,7 @@ switch (menuType) {
### 枚举
枚举的命名和类的命名相近。
```
typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
UIControlContentVerticalAlignmentCenter = 0,
@@ -176,6 +195,7 @@ typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
单例适合全局管理状态或者事件的场景。一旦创建,对象的指针保存在静态区,单例对象在堆内存中分配的内存空间只有程序销毁的时候才会释放。基于这种特点,那么我们类似 UIApplication 对象,需要全局访问唯一一个对象的情况才适合单例,或者访问频次较高的情况。我们的功能模块的生命周期肯定小于 App 的生命周期,如果多个单例对象的话,势必 App 的开销会很大,糟糕的情况系统会杀死 App。如果觉得非要用单例比较好那么注意需要在合适的场合 tearDown 掉。
单例的使用场景概括如下:
- 控制资源的使用,通过线程同步来控制资源的并发访问。
- 控制实例的产生,以达到节约资源的目的。
- 控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。
@@ -188,7 +208,7 @@ typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
//because has rewrited allocWithZone use NULL avoid endless loop lol.
_sharedInstance = [[super allocWithZone:NULL] init];
});
return _sharedInstance;
}
@@ -216,18 +236,13 @@ typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
{
return self;
}
```
### 私有变量
推荐以 `_` 开头,写在 .m 文件中。例如 NSString * _somePrivateVariable
### 代理方法
### 代理方法
1. 类的实例必须作为方法的参数之一。
2. 对于一些连续的状态的,可以加一些 will将要、did已经
@@ -239,8 +254,6 @@ typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;
```
### 方法
1. 方法与方法之间间隔一行
@@ -268,28 +281,28 @@ typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
period:(NSInteger)second
score:(NSInteger)score;
```
11. 方法如果有多个参数的情况下需要注意是否需要介词和连词。很多时候在不知道如何抉择测时候思考下苹果的一些 API 的方法命名。
```objective-c
//good
```objective-c
//good
- (instancetype)initWithAge:(NSInteger)age name:(NSString *)name;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
//bad
- (instancetype)initWithAge:(NSInteger)age andName:(NSString *)name;
- (void)tableView:(UITableView *)tableView :(NSIndexPath *)indexPath;
```
```
12. `.m` 文件中的私有方法需要在顶部进行声明
13. 方法组之间也有个顺序问题。
- 在文件最顶部实现属性的声明、私有方法的声明(很多人省去这一步,问题不大,但是蛮多第三方的库都写了,看起来还是会很方便,建议书写)。
- 在生命周期的方法里面,比如 viewDidLoad 里面只做界面的添加,而不是做界面的初始化,所有的 view 初始化建议放在 getter 里面去做。往往 view 的初始化的代码长度会比较长、且一般会有多个 view 所以 getter 和 setter 一般建议放在最下面,这样子顶部就可以很清楚的看到代码的主要逻辑。
- 所有button、gestureRecognizer 的响应事件都放在这个区域里面,不要到处乱放。
文件基本上就是
```objective-c
//___FILEHEADER___
@@ -320,7 +333,7 @@ typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
}
- (void)viewDidLoad
@@ -332,13 +345,13 @@ typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
- (void)viewWillDisappear:(BOOL)animated
{
[super viewDidAppear:animated];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidAppear:animated];
}
#ifdef DEBUG
@@ -366,8 +379,6 @@ typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
@end
```
### 图片资源
1. 单个文件的命名
@@ -377,8 +388,6 @@ typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
2. 资源的文件夹命名
最好也参考 App 按照功能模块建立对应的实体文件夹目录,最后到对应的目录下添加相应的资源文件。
### 注释
1. 对于类的注释写在当前类文件的顶部
@@ -391,11 +400,11 @@ typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
采用 A.B.C 三位数字命名比如1.0.2,当有更新的情况下按照下面的依据
| 版本号 | 右说明对齐标题 | 示例 |
| :------:| :------: | :------: |
| **A**.b.c | 属于重大内容的更新 | 1.0.2 -> 2.0.0 |
| 版本号 | 右说明对齐标题 | 示例 |
|:---------:|:----------:|:--------------:|
| **A**.b.c | 属于重大内容的更新 | 1.0.2 -> 2.0.0 |
| a.**B**.c | 属于小部分内容的更新 | 1.0.2 -> 1.1.1 |
| a.b.**C** | 属于补丁更新 | 1.0.2 -> 1.0.3 |
| a.b.**C** | 属于补丁更新 | 1.0.2 -> 1.0.3 |
### 改进
@@ -406,74 +415,64 @@ typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
Xcode 代码块的存放地址:`~/Library/Developer/Xcode/UserData/CodeSnippets`
Xcode 文件模版的存放地址:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File Templates/
### 意义
1. 为了个人或者团队开发者的代码更加规范。Property的书写的时候的空格、线程修饰词、内存修饰词的先后顺序
2. 提供大量可用的代码块,提高开发效率。比如在 Xcode 里面敲 UITableView_init 便可以自动懒加载创建一个 UITabelView 对象,你只需要设置在指定的位置写相应的参数
3. 通过一些代码块提高代码规范、避免一些bug。比如曾看到过 block 属性用 strong 修饰的代码,造成内存泄漏。举个例子你在 Xcode 中输入 **Property_delegate** 就会出来 `@property (nonatomic, weak) id<<#delegate#>> delegate;`,你输入 **Property_block** 就会出来 `@property (nonatomic, copy) <#returnType#> (^<#Block#>)(<#parType#>);`
## 三、 代码块的改造
我们可以将属性、控制器生命周期方法、单例构造一个对象的方法、代理方法、block、GCD、UITableView 懒加载、UITableViewCell 注册、UITableView 代理方法的实现、UICollectionVIew 懒加载、UICollectionVIewCell 注册、UICollectionView 的代理方法实现等等组织为 codesnippets
### 思考
- 封装好 codesnippets 之后团队除了你编写这个项目的人如何使用?如何知道是否有这个代码块?
方案:先在团队内召开代码规范会议,大家都统一知道这个事情在。之后大家共同维护 codesnippets。用法见下
方案:先在团队内召开代码规范会议,大家都统一知道这个事情在。之后大家共同维护 codesnippets。用法见下
属性:通过 **Property_类型** 开头,回车键自动补全。比如 Strong 类型,编写代码通过 Property_Strong 回车键自动补全成如下格式
```objective-c
@property (nonatomic, strong) <#Class#> *<#object#>;
```
```objective-c
@property (nonatomic, strong) <#Class#> *<#object#>;
```
方法:以 **Method_关键词** 回车键确认,自动补全。比如 Method_UIScrollViewDelegate 回车键自动补全成 如下格式
```objective-c
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
}
```
```objective-c
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
}
```
各种常见的 Mark以 **Mark_关键词** 回车确认,自动补全。比如 Method_MethodsGroup 回车键自动补全成 如下格式
```objective-c
#pragma mark - life cycle
#pragma mark - public Method
#pragma mark - private method
#pragma mark - event response
#pragma mark - UITableViewDelegate
#pragma mark - UITableViewDataSource
#pragma mark - getters and setters
```
```objective-c
#pragma mark - life cycle
#pragma mark - public Method
#pragma mark - private method
#pragma mark - event response
#pragma mark - UITableViewDelegate
#pragma mark - UITableViewDataSource
#pragma mark - getters and setters
```
- 封装好 codesnippets 之后团队内如何统一?想到一个方案,可以将团队内的 codesnippets 共享到 git团队内的其他成员再从云端拉取同步。这样的话团队内的每个成员都可以使用最新的 codesnippets 来编码。
编写 shell 脚本。几个关键步骤:
1. 给系统文件夹授权
2. 在脚本所在文件夹新建存放代码块的文件夹
3. 将系统文件夹下面的代码块复制到步骤2创建的文件夹下面
4. 将当前的所有文件提交到 Git 仓库
## 四、文件模版的改造
我们观察系统文件模版的特点,和在 Xcode 新建文件模版对应。
![Xcode file template存放地址](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/20190304_filetemplates.png)
所以我们新建 Custom 文件夹,将系统 Source 文件夹下面的 Cocoa Touch Class.xctemplate 复制到 Custom 文件夹下。重命名为我们需要的名字我这里以“Power”为例
![自定义文件模版示例](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/20190319-filetemplateSelf.png)
@@ -492,28 +491,22 @@ Xcode 文件模版的存放地址:/Applications/Xcode.app/Contents/Developer/P
![plist注意点](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/20190319-filetemplate5.png)
思考:
- 如何使用
商量好一个标识“Power”。比如我新建了单例、控制器、Model、UIView、UITableViewCell、UICollectionViewCell6个模版都以为 Power 开头。
![模版用法](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/20190319-filetemplate6.png)
- 如何共享
以 shell 脚本为工具。使用脚本将 git 云端的代码模版同步到本地 Xcode 文件夹对应的位置就可以使用了。关键步骤:
1. git clone 代码到脚本所在文件夹
2. 进入存放 codesnippets 的文件夹将内容复制到系统存放 codesnippets 的地方
3. 进入存放 file template 的文件夹将内容复制到系统存放 file template 的地方
## 六、使用
### 1. Xcode 中开发
@@ -542,8 +535,6 @@ Xcode 文件模版的存放地址:/Applications/Xcode.app/Contents/Developer/P
- 一些常用的代码块。敲 **Thread_** 等自动联想,选中后敲回车自动补全。
### 2. Code Snippet 同步
你可能是代码块的创建者,也可能是使用方,使用的时候直接先给脚本赋权
@@ -560,7 +551,6 @@ chmod +x ./uploadMySnippets.sh // 为脚本设置可执行权限
./uploadMySnippets.sh //将本地的代码块和文件模版同步到云端
```
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2021-1008-CodeSnippetMethodGroup.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2021-1008-CodeSnippetProperty.png)

View File

@@ -1,4 +1,4 @@
# App 启动时间优化
# App 启动时间优化与二进制重排
## 启动分类
@@ -7,8 +7,6 @@
所以我们聊启动时间优化,通常是指冷启动。此外启动慢,也就是看到主页面的过程很慢,都是发生在主线程上的。
为了量化启动时间,要么自定义 APM 监控。要么利用 Xcode 提供的启动时间统计。通过添加环境变量可以打印出APP的启动时间分析Edit scheme -> Run -> Arguments
- `DYLD_PRINT_STATISTICS` 设置为1
@@ -39,11 +37,9 @@ main () {
}
```
## 第一阶段:进程创建到 main 函数执行dyld、runtime
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/AppLaunchingTime.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/AppLaunchingTime.png)
这一阶段主要包括 dyld 和 Runtime 、main 函数的工作。
@@ -77,17 +73,17 @@ int __mac_execve(proc_t p, struct __mac_execve_args *uap, int32_t *retval) {
struct uthread *uthread; // 线程
task_t new_task = NULL; // Mach Task
...
context.vc_thread = current_thread();
context.vc_ucred = kauth_cred_proc_ref(p);
// 分配大块内存,不用堆栈是因为 Mach-O 结构很大。
MALLOC(bufp, char *, (sizeof(*imgp) + sizeof(*vap) + sizeof(*origvap)), M_TEMP, M_WAITOK | M_ZERO);
imgp = (struct image_params *) bufp;
// 初始化 imgp 结构里的公共数据
...
uthread = get_bsdthread_info(current_thread());
if (uthread->uu_flag & UT_VFORK) {
imgp->ip_flags |= IMGPF_VFORK_EXEC;
@@ -104,22 +100,22 @@ int __mac_execve(proc_t p, struct __mac_execve_args *uap, int32_t *retval) {
new_task = get_threadtask(imgp->ip_new_thread);
context.vc_thread = imgp->ip_new_thread;
}
// 加载解析 Mach-O
error = exec_activate_image(imgp);
if (imgp->ip_new_thread != NULL) {
new_task = get_threadtask(imgp->ip_new_thread);
}
if (!error && !in_vfexec) {
p = proc_exec_switch_task(p, current_task(), new_task, imgp->ip_new_thread);
should_release_proc_ref = TRUE;
}
kauth_cred_unref(&context.vc_ucred);
if (!error) {
task_bank_init(get_threadtask(imgp->ip_new_thread));
proc_transend(p, 0);
@@ -139,10 +135,9 @@ int __mac_execve(proc_t p, struct __mac_execve_args *uap, int32_t *retval) {
}
```
Mach-O 文件非常大,系统为 Mach-O 分配一块大内存 imgp接下来会初始化 imgp 里的公共数据。内存处理完__mac_execve 会通过 fork_create_child 函数 for 出一个新的进程,然后通过 `exec_activate_image` 函数解析加载 Mach-O 文件到内存 imgp 中。最后调用 `task_set_main_thread_qos` 设置新 fork 出来进程的主线程。
Mach-O 文件非常大,系统为 Mach-O 分配一块大内存 imgp接下来会初始化 imgp 里的公共数据。内存处理完__mac_execve 会通过 `fork_create_child` 函数 fork 出一个新的进程,然后通过 `exec_activate_image` 函数解析加载 Mach-O 文件到内存 imgp 中。最后调用 `task_set_main_thread_qos` 设置新 fork 出来进程的主线程。
```c
struct execsw {
int (*ex_imgact)(struct image_params *);
const char *ex_name;
@@ -160,9 +155,7 @@ struct execsw {
dyld 入口函数为 `_dyld_start`dyld 属于用户态进程,不在 xnu 中,具体实现可以查看 [dyld/dyldStartup.s at master · opensource-apple/dyld · GitHub](https://github.com/opensource-apple/dyld/blob/master/src/dyldStartup.s)`_dyld_start` 会加载 App 动态库,处理完成后会返回 App 的入口地址。然后执行 App 的 main 函数。
dylddynamic link editorApple的动态链接器可以用来装载 Mach-O 文件(可执行文件、动态库等)
dylddynamic link editorApple 的动态链接器,可以用来装载 Mach-O 文件(可执行文件、动态库等)
启动 APP 时dyld 所做的事情有
@@ -210,8 +203,6 @@ void _objc_init(void){
到此为止,可执行文件和动态库中所有的符号(ClassProtocolSelectorIMP…)都已经按格式成功加载到内存中,被 Runtime 所管理
## 第二阶段main 函数到 didFinishLaunchingWithOptions
APP的启动由 dyld 主导将可执行文件加载到内存顺便加载所有依赖的动态库。并由Runtime 负责加载成 objc 定义的结构。所有初始化工作结束后dyld 就会调用 main 函数
@@ -220,8 +211,6 @@ APP的启动由 dyld 主导,将可执行文件加载到内存,顺便加载
AppDelegate 的`application:didFinishLaunchingWithOptions:` 方法。此阶段主要是各种 SDK 的注册、初始化、APM 监控系统的启动、热修复 SDK 的设置、App 初始化数据的加载、IO 等等。
## 第三阶段:`didFinishLaunchingWithOptions` 到业务首页加载完成
这部分主要是首页渲染相关逻辑,不同的场景,首页的定义可能不一样。比如未登录的时候首页就是登陆页、否则就是某个主页
@@ -230,8 +219,6 @@ AppDelegate 的`application:didFinishLaunchingWithOptions:` 方法。此阶段
- 渲染数据的计算
## 启动优化
### 第一阶段
@@ -254,8 +241,6 @@ AppDelegate 的`application:didFinishLaunchingWithOptions:` 方法。此阶段
- 控制 C++ 的全局变量的数据
### 第二阶段
- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在 `application:didFinishLaunchingWithOptions` 方法中
@@ -268,16 +253,10 @@ AppDelegate 中 `application:didFinishLaunchingWithOptions` 函数阶段一般
- 比如二方、三方 SDK 的注册、初始化。这其中有些 SDK 比如 APM 是有必要放在很早的阶段。有些则不需要,比如日志 SDK 的初始化
### 第三阶段
很多时候,需要梳理出那些功能是首屏渲染所需要的初始化功能,那些是非首页需要的功能,按照业务场景梳理并治理。
QA
静态库、动态库?
@@ -298,51 +277,61 @@ QA
Framework 只是打包方式,动态、静态库都可以支持。甚至可以存放资源文件。
## 二进制重排
### 虚拟内存、物理内存、内存分页
早期计算机内部都是物理内存。物理内存一个问题就是安全问题,通过地址访问到别的应用程序的数据。进程如果能直接访问物理内存无疑是很不安全的,(诞生了解决方案:虚拟内存)所以操作系统在物理内存的上又建立了一层虚拟内存。
应用程序的可执行文件是存储在磁盘上的,启动应用,则需要将可执行文件加载进内存,早期计算机是没有虚拟内存的,一旦加载就会全部加载到内存中,并且进程都是按照顺序排列的,这样存在两个问题:
一个应用程序在使用的时候并不是全部使用的,往往是使用了一个应用程序的某个或者某几个功能。
- 上一个进程只需要加一些地址就能访问到下一个进程,安全性很低
所以早期工程师的第一个解决方案是将一个应用程序分块,按需加载功能模块的内存地址数据
- 当前软件越来越大,开启多个软件时会直接全部加载到内存中,导致内存不够用。而且使用软件的时候大多数情况只使用部分功能。如果软件一打开就全部加载到内存中,会造成内存的严重浪费
虚拟内存是间接访问了内存条
基于上述2个问题诞生了虚拟内存技术
内存分页iOS 一页就是16KB。
物理内存如果满了,则将最活跃的内存数据,覆盖掉,最不活跃的内存数据;
ASLR为了安全问题诞生。
### 内存缺页异常
自己的代码中有 NSLog 代码,可是 NSLog 函数的代码实现在 Foundation 动态库中
每个进程在创建加载时会被分配一个大小大概为12倍真实地内存的连续虚拟地址空间让当前软件认为自己拥有一块很大内存空间。实际上是把磁盘的一小部分作为假想内存来使用
App 启动则用 dyld 去加载库,共享缓存库
CPU 不直接和物理内存打交道,而是通过 MMUMemory Manage Unit内存管理单元MMU 是一种硬件电路,速度很快,主要工作是内存管理,地址转换是功能之一
虚拟地址:偏移是编译后就能确定的
每个进程都会有自己的页表 `Page Table` 页表存储了进程中虚拟地址到物理地址的映射关系所以就相当于地图。MMU 收到 CPU 的虚拟地址之后就开始查询页表,确定是否存在映射以及读写权限是否正常
内存缺页异常:在使用中,访问虚拟内存的一个 page 而对应的物理内存缺不存在(没有被加载到物理内存中),则发生缺页异常。影响耗时,在几毫秒之内
iOS 程序在进行加载时,会根据一 page 大小16kb 将程序分割为多页启动时部分的页加载进真实内存部分页还在磁盘中中间的调度记录在一张表Page Table这个表用来调度磁盘和内存两者之间的数据交换
什么时候发生大量的缺页异常?一个应用程序刚启动的时候。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/iOSPageInPageOut.png)
启动时所需要的代码分布在第一页、第二页、第三页...第200页。这样的情况下启动时间会影响较大所以解决思路就是将应用程序启动刻所需要的代码二进制优化一下统一放到某几页这样就可以避免内存缺页异常则优化了 App 启动时间
如上图App 运行时执行某个任务时会先访问虚拟页表如果页表的标记为1则说明该页面数据已经存在于内存中可以直接访问。如果页表为0则说明数据未在物理内存中这时候系统会阻塞进程叫做缺页中断page fault进程会从用户态切换到内核态并将缺页中断交给内核的 page Fault Handler 处理。等将对应的 page 从磁盘加载到内存之后再进行访问,这个过程叫做 page in
dylib loading time
因为磁盘访问速度较慢,所以 page in 比较还是,而且 iOS 不仅仅是将数据加载到内存中,还要多这页做签名认证,所以 iOS 耗时更长
rebase/binding time 修正符号是由于 ASLR 导致。binding time 是动态库去 bind lazy table、non-lazy table 所占用的时间。rebase 地址偏移ASLR。 rebase 的时间如何缩小Mach-O 文件大小变小。 binding time 变小则需要动态库变小。2者优化手段冲突
等到程序运行时用到了才去内存中寻找虚拟地址对应的页帧,找不到才进行分配,这就是内存的惰性(延时)分配机制。
Objc setup timeSwift 这部分占优势
initializer timeload 方法耗时。
slowest intializers
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/PageFault.png)
libS
libMain
为了提高效率和方便管理对虚拟内存和物理内存进行分页Page管。进程在访问虚拟内存的一个 page 而对应的物理内存却不存在(没有被加载到物理内存中),则会触发一次缺页异常(缺页中断),然后分配物理内存,有需要的话会从磁盘 mmap 读入数据。
启动时所需要的代码分布在 VM 的第一页、第二页、第三页...,这样的情况下启动时间会影响较大,所以解决思路就是将应用程序启动刻所需要的代码(二进制优化一下),统一放到某几页,这样就可以避免内存缺页异常,则优化了 App 启动时间。
二进制重排提升 App 启动速度是通过「解决内存缺页异常」(内存缺页会有几毫秒的耗时)来提速的。
一个 App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理放到某一页或者某几页」虚拟内存中的页。Xcode 工程允许开发者指定 「Order File」可以「按照文件中的方法顺序去加载」可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 Order File、Write Link Map Files 参数)。
### 如何获取启动阶段的 Page Fault 次数
Instrucments 中的 System Trace 可以查看详细信息。
### 如何验证重排是否成功
查看 LinkMap。发现方法展示顺序是按照写代码的顺序展示的。
@@ -350,6 +339,14 @@ libMain
### 有没有办法将 App 启动需要的方法集中收拢?
其实二进制重排 Apple 自己本身就在用,查看 `objc4` 源码的时候就发现了身影
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/Objc-OrderFile.png)
Xcode 使用的链接器为 `ld`。 ld 有个参数 `-order_file` 。order_file 中的符号会按照顺序排列在对应 section 的开始。
Xcode 的 Build Setting GUI 面板也支持配置。
1. 在 Xcode 的 Build Settings 中设置 **Order File**Write Link Map Files 设置为 YES进行观察
2. 如果你给 Xcode 工程根目录下指定一个 order 文件,比如 `refine.order`,则 Xcode 会按照指定的文件顺序进行二进制数据重排。分析 App 启动阶段,将优先需要加载的函数、方法,集中合并,利用 Order File减小缺页异常从而减小启动时间。
@@ -380,3 +377,23 @@ initialize
load 方法会在类被加载到 runtime 的时候调用。且父类的 load 方法比子类先执行。load 方法只会执行1次。
initialize 方法会在第一次收到消息的时候调用。父类的 initialize 方法比子类先执行。假如有 Person 类,还有一个子类 children。子类第一次收到消息的时候会先调用父类的 initialize然后调用子类的 initialize如果子类没有实现 initialize 那么父类的 initialize 会执行多次。
总结:
启动优化思路主要是先监控发现具体的启动时间和启动阶段对应的各个任务,有了具体数据,才可以谈优化。
- 删除启动项
- 如果不能删除,则延迟启动项。启动结束后找合适的时机预热
- 不能延迟的可以使用并发,多线程优势
- 启动阶段必须的任务,如果不适合并发,则利用技术手段将代码加速
## 参考
- [# 抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%](https://mp.weixin.qq.com/s/Drmmx5JtjG3UtTFksL6Q8Q)
- [iOS 启动优化+监控实践](https://www.jianshu.com/p/17f00a237284)

View File

@@ -257,12 +257,12 @@ sizeof 是运算符。
`objc_getClass()` 如果传递 instance 实例对象,返回 class 类对象;传递 Class 类对象,返回 meta-class 元类对象;传递 meta-class 元对象,则返回 NSObject(基类)的 meta-class 对象
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/objc-isa.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/objc-isa.png)
instance 的 isa 指向 Class。当调用方法时通过 instance 的 isa 找到 Class最后找到对象方法的实现进行调用
class 的 isa 指向 meta-class。当调用类方法的时通过 class 的 isa 找到 meta-class最后找到类方法进行调用。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/objc-superclass.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/objc-superclass.png)
当 Student 实例对象调用 Person 的对象方法时,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,在类对象的对象方法列表中找到方法实现并调用。
当 Stduent 实例对象调用 init 方法时候,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,找到 Person Claas 的 superClass 到 NSObject 类对象,在 NSObject 类对象的方法列表中找到 `init` 方法并调用。

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,58 @@
> 做很多需求或者是技术细节验证的时候会用到 Runtime 技术,用了挺久的了,本文就写一些场景和源码分析相关的文章。
## 动态语言
Runtime 是实现 OC 语言动态的 API。
静态语言:在编译阶段确定了变量数据类型、函数地址等,无法动态修改。
动态语言:只有在运行的时候才可以决定变量属于什么类型、方法真正的地址,
对象 `objc_object` 存了isa、成员变量的值
类 objc_class: superclass、成员变量、实例变量
```objectivec
@interface Person : NSObject
{
NSString *_name;
}
@property (nonatomic, strong) NSString *hobby;
@end
malloc_size((__bridge const void *)(p)) // 24 isa占8字节 + _name 指针占8字节 + hobby 指针占8字节 = 24
class_getInstanceSize(p.class) // 32 ,系统内存对齐
```
为什么内存对齐以空间换时间。系统以16字节对齐。
x /6gx p.class
类对象有且仅有1个。
p.class
class_getClass("Person")
[Person class]
p/x (class_data_bits_t *)地址
## class_rw_t、class_ro_t 区别?
class_ro_t 在编译时期生成的class_rw_t 是在运行时期生成的。
拷贝带来的问题?当开发者通过 runtime第一次 动态修改类的信息的时候Apple 会生成 rwe。搜索 class_rw_ext_t
## 有类对象、为什么设计元类对象
复用消息机制。比如 `[Person new]`
元类对象: isa、元类方法、
`objc_msgSend` 设计初衷就是为了消息发送很快。假如没有元类,则类方法也存储在类对象的方法信息中,则可能需要加额外的字段来标记某个方法是类方法还是对象方法。遍历或者寻找会比较慢。所以引入元类(单一职责),设计元类的目的就是为了提高 `objc_msgSend` 的效率。
## isa 本质
在 arm64 架构之前isa 就是一个普通的指针,存储着 Class或Meta-Class 对象的内存地址。
@@ -89,7 +141,7 @@ isa 在 arm64 之后必须通过 `ISA_MASK` 去查询 class类对象、元类
`0x0000000ffffffff8ULL` 用程序员模式打开计算器
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/objc-isa-mask.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/objc-isa-mask.png)
其中,结构体中的数据存放大体是下面的结构:
@@ -291,7 +343,7 @@ struct class_ro_t {
具体关系整理如下图
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-class.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-class.png)
说明:
@@ -299,7 +351,7 @@ struct class_ro_t {
为什么不是二维数组因为Array 中的子 Array长度不一致且不能补空
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-class-rw-t.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-class-rw-t.png)
```c
static void remethodizeClass(Class cls)
@@ -384,7 +436,7 @@ struct class_ro_t {
- `class_ro_t` 里面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,包含了类的(原始信息)初始内容
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-class-ro-t.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-class-ro-t.png)
## Method_t
@@ -422,7 +474,7 @@ typedef struct objc_selector *SEL;
iOS 中提供了一个叫做 `@encode` 的指令,可以将具体的类型表示成字符串编码
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-method-encoding.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-method-encoding.png)
```objectivec
- (int)calcuate:(int)age heigith:(float)height;
@@ -774,7 +826,7 @@ NSLog(@"%s %p", bucket._key, bucket._imp);
// personSay 0xbec8
```
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-method-find.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-method-find.png)
原理就是根据类对象结构体找到 cache 结构体cache 结构体内部的 `_buckets` 是一个方法散列表,查看源代码,根据散列表的哈希寻找策略 `(key & mask)` 找到哈希索引,然后找到方法对象 bucket其中寻找方法索引的 key 就是 方法 selector。
@@ -1359,7 +1411,7 @@ for 循环不断查找,找当前类的父类,直到当前类为 nil。
上面的流程是整个 `objc_msgSend` 的消息发送阶段的整个流程。可以用下图表示
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-objc_msgSend-messageSend.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-objc_msgSend-messageSend.png)
### 动态方法解析阶段
@@ -1494,7 +1546,7 @@ SEL_resolveClassMethod, sel);`
完整流程如下
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-objc_msgSend-ResolveMethod.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-objc_msgSend-ResolveMethod.png)
上 Demo
@@ -1634,7 +1686,7 @@ void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
为什么是 `__forwarding__` 方法。我们可以根据 Xcode 崩溃窥探一二
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-forwardingFailed.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-forwardingFailed.png)
```c
int __forwarding__(void *frameStackPointer, int isStret) {
@@ -1678,7 +1730,7 @@ int __forwarding__(void *frameStackPointer, int isStret) {
完整流程如下
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-forwarding.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-forwarding.png)
上 Demo
@@ -1899,7 +1951,7 @@ objc_msgSendSuper(arg, sel_registerName("class"))
我们对 iOS 项目`[super viewDidLoad]` 下符号断点,发现`objc_msgSendSuper2`
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-super.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-super.png)
查看 objc4 源代码发现是一段汇编实现。
@@ -2133,7 +2185,7 @@ void test () {
方法内的变量存储在栈上,堆向上增长,栈向下增长。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-isa-demo.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-isa-demo.png)
3.**实例对象的本质就是一个结构体存储所有成员变量isa 是一个特殊成员变量,其他的成员变量,这里就是 _name`sayHi` 方法内部的 self 就是 obj找成员变量的本质就是找内存地址的过程此时就是偏移8个字节**
@@ -2182,7 +2234,7 @@ objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));
所以此时的“前一个局部变量” 也就是结构体 `objc_super` 类型的 arg。arg 是一个结构体,结构体第一个成员变量就是 self所以“前一个局部变量” 也就是 selfViewController
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-super-isa-demo.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-super-isa-demo.png)
## 应用场景
@@ -2216,7 +2268,7 @@ Person *p = [Person new];
object_setClass(p, [Student class]);
```
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-changeisa-demo.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-changeisa-demo.png)
3.动态创建类
@@ -2243,7 +2295,7 @@ void createClass (void) {
}
```
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/runtime-dynamicCreateClass-demo.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/runtime-dynamicCreateClass-demo.png)
runtime 中 copy、create 等出来的内存,不使用的时候需要手动释放`objc_disposeClassPair(newClass>)`

View File

@@ -1,7 +1,5 @@
# fishhook 原理
## 先看看怎么用
经常会遇到 hook oc 方法,但是遇到像 NSLog、objc_msgSend 等方法的时候 OC Runtime 就不满足了,有了 fishhook 神器hook “c 函数”已不是难题。
@@ -41,8 +39,6 @@ struct rebinding {
};
```
## 原理窥探
我们知道 NSLog 的函数实现在 Foundation 库中,而我们开发自己写的其他函数则在自身可执行文件中,也就是 Mach-O。
@@ -63,14 +59,10 @@ struct rebinding {
但也带来了坏处因为都是程序每次装载的时候进行重新链接。有解决方案叫做延迟绑定Lazy binding可使得动态链接对性能的影响减的最小。据估算动态链接相比静态链接存在大约5%的性能损失,但换来程序在空间上的节省和程序构建和升级的灵活性,是值得的。
地址无关代码PIC
装载时重定位是解决动态模块中有绝对地址引用的方案之一。但其存在一个很大缺点,是指令部分无法在多个进程间共享,这样就是失去动态链接节省内存这一优势。我们还需要一种更优雅的方案,希望程序模块中的共享指令部分在装载时不需要因为装载地址改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这个方案就是 地址无关代码Position Independent CodePIC 技术。
写的业务代码里面假如某一行调用了 `NSLog`,那么在编译阶段,使用 NSLog 只是 IDE 提供了功能,让你可以看到声明而已。编译后的可执行文件,还是不知道 NSLog 的具体函数地址。这个底层是如何工作的呢?
在工程编译阶段,所产生的 Mach-O 可执行文件中会预留出一段空间,这个空间被叫做符号表,存放在 `_DATA` 数据段中,且数据段是可读可写的。
@@ -83,10 +75,6 @@ struct rebinding {
它指向了一个表(类似一个应用程序的外部函数名,函数真正实现地址),去这个表里面找地址,这个表叫做**符号表**。
当真正去调用 NSLog 函数的时候才去这个符号表中去寻找函数地址,去调用实现。
调用外部函数(在内部找不到方法实现)的时候,在 Mach-O 的数据段生成一个区域,叫做符号表。符号表的 key 就是方法名,比如 NSLog。
@@ -104,10 +92,15 @@ PIC 技术。
fishhook 做的事情就是将系统的符号表,将符号表中的特定符号对应的地址,修改为自定义的函数地址。起到了 hook 作用。也就是说外部的 c 函数,在 iOS 中的调用属于**动态调用**。
知道了 fishhook 的工作原理,我们就知道 fishhook 是 hook 不了自己写的 c 函数了。因为自定义函数是没有通过符号去寻找函数真正地址的这个过程。而系统库是通过符号去绑定真实地址的。可以通过 `rebind_symbols` 这个名称得以印证。
## 纠错
之前同事问了个问题fishhook为什么能hook系统库的c方法不能hook c++
1. FishHook 的原理是 ASRL + Lazy Symbol Table。系统库 NSLog 地址不确定的,会随机偏移,当 DYLD 加载后根据 offset 动态计算(也就是 rebinding、rebase
2. Data 段可读可写NSLog 位于 Data 段,自定义函数位于 Text 段只读。所以C/C++ FishHook 可以hook 系统库/动态库共享缓存这些符号
3. 知道机制后也就可以说:自定义符号是在 Text 段Read Only所以不能被 FishHook hook。另外系统库很多都是 c 实现。要是某个库是 C++ 实现,也可以 hook
4.
总结版FishHook 基于 ASRL + Lazy Symbol Table 运行,另外能不能 hook 要看代码是落在 Data 段(RW) 还是 TextRO

View File

@@ -6,7 +6,7 @@ block 本质上就是一个 oc 对象,也有 isa 指针
block 是封装了函数调用和函数调用环境的 OC 对象
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block-structure.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/block-structure.png)
```objectivec
int age = 27;
@@ -247,7 +247,7 @@ NSLog(@"%@", [[block class] superclass]); // NSBlock
NSLog(@"%@", [[[block class] superclass] superclass]); // NSObjec
```
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block-memorylayout.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/block-memorylayout.png)
代码存放在 text 段static 修饰的数据存放在 data 区,程序员手动申请的内存存放在堆,局部变量存放在栈区
@@ -598,7 +598,7 @@ static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
可以看到 `__block int age = 27;` 变为了 `__Block_byref_age_0 age` 结构体。block 内部的函数在修改 age 的时候其实就是通过 `__main_block_impl_0` 结构体的 age 找到 `__Block_byref_age_0`,然后访问 `__Block_byref_age_0` 中的成员变量 `__forwarding` 访问成员变量 age并修改值。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block-forwarding.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/block-forwarding.png)
`__block` 修饰基本数据类型和对象,对于生成的结构体也不一样。
@@ -692,7 +692,7 @@ int main(int argc, const char * argv[]) {
我们将断点设置到 NSLog 这里,打印出自定义结构体 `__main_block_impl_0` 中的 age 。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Block-variableAddress.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/Block-variableAddress.png)
```c
// 0x0000000105231f70
@@ -736,7 +736,7 @@ block 内部对变量的值修改其实就是对 block 内部自定义结构体
## `__forwarding` 的设计
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block_forwarding.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/block_forwarding.png)
当block在栈中时`__Block_byref_age_0`结构体内的`__forwarding`指针指向结构体自己。
@@ -750,7 +750,7 @@ block 内部对变量的值修改其实就是对 block 内部自定义结构体
`__block ` 修饰符修饰的对象在内存中如下
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block-object-memoery.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/block-object-memoery.png)
```c
@@ -871,7 +871,7 @@ p.block();
`__unsafe_retained` 因为不安全所以不推荐,`__block` 因为使用繁琐,且必须等到调用 block 才会释放内存所以不推荐。ARC 下最佳用 `__weak`
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block_object_cycle.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/block_object_cycle.png)
### MRC 下

View File

@@ -109,7 +109,7 @@ MH_DSYM存储着二进制文件符号。`.dSYM/Contents/Resource/DWARF/xx`
Xcode 中也可以查看 Mach-O 文件类型
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOFileType.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/MachOFileType.png)
### Universal Binary
@@ -137,7 +137,7 @@ Xcode 中也可以查看 Mach-O 文件类型
## Mach-O 结构
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Mach-OStructure.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/Mach-OStructure.png)
一个 Mach-O 文件包含3块
@@ -149,13 +149,13 @@ Xcode 中也可以查看 Mach-O 文件类型
可以用 [GitHub - fangshufeng/MachOView: 分析Macho必备工具](https://github.com/fangshufeng/MachOView) 和系统自带的 atool 查看 Mach-O 信息
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/otoolhelp.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/otoolhelp.png)
用 MachOView 查看 DDD Mach-O 文件
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOPageZero.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/MachOPageZero.png)
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOText.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/MachOText.png)
可以看到在 Mach-O 文件上,`__PAGEZERO``VM Size` 有值,但是 File Size 为0也就是说 `__PAGEZERO` 在 Mach-O 中不占据内存,但是程序运行起来之后,会占据虚拟内存。所以代码段在 Mach-O 中 File Offset 为0如果前面的 `__PAGEZERO` 的 File size 有值,这里的 File Offset 就不为0
@@ -179,9 +179,9 @@ Xcode 中也可以查看 Mach-O 文件类型
也可以使用 `size -l -m -x DDD` 指令来查看 Mach-O 的内存分布
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOInsepect.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/MachOInsepect.png)
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/ASLRDemo.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/ASLRDemo.png)
我们会发现根据 Mach-O 文件中的信息File Size、File Offset、VM Address、VM Size 可以判断出 `__TEXT` 段内函数信息,这样子不够安全
@@ -189,7 +189,7 @@ Xcode 中也可以查看 Mach-O 文件类型
Address Space Layout Randomization地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术通过堆、栈、共享库映射等线性区布局的随机化通过增加攻击者预测目的地址的难度防止攻击者直接定位攻击代码的位置达到阻止溢出攻击目的的一种技术。在 iOS 4.3 引入。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/ASLROffset.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/ASLROffset.png)
- LC_SEGMENT (__TEXT) 的 VM Address 0x10005000

View File

@@ -4,7 +4,6 @@
网传:如果在老设备上,使用最新的 iOS 系统苹果会自动降频CPU 频率),从而让你的 iPhone 看上去很卡,让你主动去购买新的设备。
其实,苹果在 iOS 13 的时候,在内核中加入了一个新的性能衡量指标`wakeup`。CPU 频率和设备电池有关系。看看 ARM 架构中对于 CPU 功耗问题的描述:
> Many ARM systems are mobile devices and powered by batteries. In such systems, optimization of power use, and total energy use, is a key design constraint. Programmers often spend significant amounts of time trying to save battery life in such systems.

View File

@@ -64,7 +64,7 @@
* [58、Swift每个版本迁移的总结](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.58.md)
* [59、iOS零散知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.59.md)
* [60、App瘦身之道](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.60.md)
* [61、App启动时间优化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.61.md)
* [61、App 启动时间优化与二进制重排](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.61.md)
* [62、OCLint实现Code Review](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.62.md)
* [63、苹果官方开源资料](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.63.md)
* [64、组件化、模块化、插件、子应用、框架、库理解](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.64.md)
@@ -105,4 +105,8 @@
* [99、客户端质量把控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.99.md)
* [100、iOS 端底层网络错误](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.100.md)
* [101、离屏渲染](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.101.md)
* [102、LLVM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.102.md)
* [102、LLVM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.102.md)
* [103、设计模式及其场景](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.103.md)
* [104、NSNotification底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.104.md)
* [105、iOS 界面渲染流程](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.105.md)
* [106、NSUserDefault 底层原理探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.106.md)