diff --git a/Chapter1 - iOS/1.100.md b/Chapter1 - iOS/1.100.md index 885f4e1..5fbc2e8 100644 --- a/Chapter1 - iOS/1.100.md +++ b/Chapter1 - iOS/1.100.md @@ -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 { diff --git a/Chapter1 - iOS/1.102.md b/Chapter1 - iOS/1.102.md index 0157ba9..bad0257 100644 --- a/Chapter1 - iOS/1.102.md +++ b/Chapter1 - iOS/1.102.md @@ -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) 语法分析,生成语法树(AST,Abstract 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 - - diff --git a/Chapter1 - iOS/1.103.md b/Chapter1 - iOS/1.103.md index e69de29..28ec154 100644 --- a/Chapter1 - iOS/1.103.md +++ b/Chapter1 - iOS/1.103.md @@ -0,0 +1 @@ +# 设计模式及其场景 diff --git a/Chapter1 - iOS/1.104.md b/Chapter1 - iOS/1.104.md new file mode 100644 index 0000000..67a524a --- /dev/null +++ b/Chapter1 - iOS/1.104.md @@ -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 )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 为 key,observer 为 value。 + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/notification-namedTable.png) + +- 第一个 MapTable key 为 notificationName,value 为另一个 MapTable(子 Table) + +- 子 Table 以 object 为 key,value 为链表,存储所有的观察者 + +- object 为 nil 的时候,系统会根据 nil 自动生成一个 key,相当于这个 key 对应的值链表保存的就是当前通知传入了 NotificationName 且没有 object 的所有观察者。 + +### nameless Table + +nameless Table 结构较为简单,因为没有 notificationName,所以就一层 MapTable。key 为 object,value 为链表,存储所有的观察者。 + +![](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 *)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 空闲时即发送通知到通知中心 + +- NSPostASAP:as soon as possible,尽可能快。即当前通知或者 timer 回调执行结束就发送通知到通知中心,还是需要依赖 RunLoop + +- NSPostNow:马上发送 + +postingStyle 也是枚举 + +```objectivec +typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) { + NSNotificationNoCoalescing = 0, + NSNotificationCoalescingOnName = 1, + NSNotificationCoalescingOnSender = 2 +}; +``` + +- NSNotificationNoCoalescing:不管是否 NSNotificationName 是否重复,都不合并 + +- NSNotificationCoalescingOnName:NSNotificationName 相同的多个 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 *)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 *)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 = {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 () +@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 MapTable,key 为 NSNotificationName,value 为子 MapTable,子 MapTable 中 object 为 key,value 为 observer。所以多次添加则会造成当 postNotification 的时候会有多次响应。 + +查看 removeObserver 源码发现,移除都会针对 NSNotificationName 进行操作,从 NAMED MapTable 中,以 NSNofiticationName 为 key,获取 value 为子 MapTable 。子 MapTable 根据 object 为 key,获取对应的链表,然后根据参数 observer 移除链表中所有 observer 都为传递的 observer 的节点。所以多次调用不会存在问题。 diff --git a/Chapter1 - iOS/1.105.md b/Chapter1 - iOS/1.105.md new file mode 100644 index 0000000..e7a3db0 --- /dev/null +++ b/Chapter1 - iOS/1.105.md @@ -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 都有一个 CALayer,layer 属性都有 contents,contents 其实是一块缓存,叫做 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到Surface(Metal或者OpenGL ES Texture)上 -> +6. Surface(Metal或者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能明显提升帧率,同时内存开销也比较稳定。 diff --git a/Chapter1 - iOS/1.106.md b/Chapter1 - iOS/1.106.md new file mode 100644 index 0000000..72bfeb0 --- /dev/null +++ b/Chapter1 - iOS/1.106.md @@ -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 +// { count = 8, transaction: 0, voucher = 0x0, contents = +// "CFPreferencesHostBundleIdentifier" => { length = 9, contents = "test.demo" } +// "CFPreferencesUser" => { length = 25, contents = "kCFPreferencesCurrentUser" } +// "CFPreferencesOperation" => : 1 +// "Value" => { length = 16, contents = "ÈÖ∑ÈÖ∑ÁöÑÂìÄÊÆø2" } +// "Key" => { length = 3, contents = "key" } +// "CFPreferencesContainer" => { length = 169, contents = "/private/var/mobile/Containers/Data/Application/0C224166-1674-4D36-9CDB-9FCDB633C7E3/" } +// "CFPreferencesCurrentApplicationDomain" => : true +// "CFPreferencesDomain" => { length = 9, contents = "test.demo" } +// }> + + xpc_object_t hello = xpc_dictionary_create(NULL, NULL, 0); + + // 注释1:test.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); +``` diff --git a/Chapter1 - iOS/1.38.md b/Chapter1 - iOS/1.38.md index 952001b..eccba7d 100644 --- a/Chapter1 - iOS/1.38.md +++ b/Chapter1 - iOS/1.38.md @@ -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); +``` + +主线程为什么 + ### 线程封装 思考:如何设计一个常驻线程工具类? diff --git a/Chapter1 - iOS/1.39.md b/Chapter1 - iOS/1.39.md index f95e4f2..ff579b7 100644 --- a/Chapter1 - iOS/1.39.md +++ b/Chapter1 - iOS/1.39.md @@ -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 @@ si:step instruction,简写为 stepi,si。当你在 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 @@ si:step instruction,简写为 stepi,si。当你在 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/4,arm7/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); + // + 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 -``` \ No newline at end of file +``` + +## 其他常见的多线程编程模式 + +### 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 的设计,内部就是一个串行队列。 \ No newline at end of file diff --git a/Chapter1 - iOS/1.40.md b/Chapter1 - iOS/1.40.md index 09a375c..c40a3d5 100644 --- a/Chapter1 - iOS/1.40.md +++ b/Chapter1 - iOS/1.40.md @@ -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 superclass’s 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(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(getARCCleanupKind()); + 797 } + 798 } +``` + +可以看到在调用到 dealloc 方法时,插入了代码,实现如下 + +```c +struct FinishARCDealloc : EHScopeStack::Cleanup { + void Emit(CodeGenFunction &CGF, Flags flags) override { + const ObjCMethodDecl *method = cast(CGF.CurCodeDecl); + + const ObjCImplDecl *impl = cast(method->getDeclContext()); + const ObjCInterfaceDecl *iface = impl->getClassInterface(); + if (!iface->getSuperClass()) return; + + bool isCategory = isa(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` diff --git a/Chapter1 - iOS/1.45.md b/Chapter1 - iOS/1.45.md index 97121a2..218b6c4 100644 --- a/Chapter1 - iOS/1.45.md +++ b/Chapter1 - iOS/1.45.md @@ -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) diff --git a/Chapter1 - iOS/1.46.md b/Chapter1 - iOS/1.46.md index 076daa0..4d1ec61 100644 --- a/Chapter1 - iOS/1.46.md +++ b/Chapter1 - iOS/1.46.md @@ -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) diff --git a/Chapter1 - iOS/1.48.md b/Chapter1 - iOS/1.48.md index 4e3a30f..79bbc12 100644 --- a/Chapter1 - iOS/1.48.md +++ b/Chapter1 - iOS/1.48.md @@ -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) diff --git a/Chapter1 - iOS/1.52.md b/Chapter1 - iOS/1.52.md index 8c4b344..7de21b7 100644 --- a/Chapter1 - iOS/1.52.md +++ b/Chapter1 - iOS/1.52.md @@ -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) diff --git a/Chapter1 - iOS/1.61.md b/Chapter1 - iOS/1.61.md index 2283ff6..3aa2e0f 100644 --- a/Chapter1 - iOS/1.61.md +++ b/Chapter1 - iOS/1.61.md @@ -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 函数。 - - -dyld(dynamic link editor),Apple的动态链接器,可以用来装载 Mach-O 文件(可执行文件、动态库等) +dyld(dynamic link editor),Apple 的动态链接器,可以用来装载 Mach-O 文件(可执行文件、动态库等) 启动 APP 时,dyld 所做的事情有 @@ -210,8 +203,6 @@ void _objc_init(void){ 到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 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 动态库中。 +每个进程在创建加载时,会被分配一个大小大概为1~2倍真实地内存的连续虚拟地址空间,让当前软件认为自己拥有一块很大内存空间。实际上是把磁盘的一小部分作为假想内存来使用。 -App 启动则用 dyld 去加载库,共享缓存库。 +CPU 不直接和物理内存打交道,而是通过 MMU(Memory 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 time:Swift 这部分占优势 -initializer time:load 方法耗时。 -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) diff --git a/Chapter1 - iOS/1.7.md b/Chapter1 - iOS/1.7.md index e8df216..7d2b22a 100644 --- a/Chapter1 - iOS/1.7.md +++ b/Chapter1 - iOS/1.7.md @@ -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` 方法并调用。 diff --git a/Chapter1 - iOS/1.74.md b/Chapter1 - iOS/1.74.md index f81c212..5ba3e7a 100644 --- a/Chapter1 - iOS/1.74.md +++ b/Chapter1 - iOS/1.74.md @@ -545,17 +545,9 @@ curNode.costTime > (parentNode.costTime * 1.1/n) && curNode.costTime > parentNod 方法调用的深度约定为100,现在方法调用层级不会那么深,如果真有那么多节点都是很小的子节点,排查意义不大,所以阈值没必要设置太大。 -### 7. 功能设计小思考 - -业务团队经常有关心某个页面或者某个关键业务流程卡顿情况的诉求,比如门店经营团队的同学很关心商品加购到开单整个链路的卡顿情况。基于这个背景,可以参考 iOS 系统留给开发者标记某个场景的功能(需要搭配 Instrucments 使用)使用需要导入 `#include ` 文件。 - -所以 APM 也提供了类似的能力,`-(void)setPhase:(NSString *)phase` - -这样情况下,业务方可以针对特定的业务流程做性能统计分析 - ## 二、 App 启动时间监控 -### 1. App 启动时间的监控 +### 1. 简易版App 启动时间的监控 应用启动时间是影响用户体验的重要因素之一,所以我们需要量化去衡量一个 App 的启动速度到底有多快。启动分为冷启动和热启动。 @@ -677,6 +669,84 @@ Main 阶段 其实难点是如何拿到启动时刻所调用的所用方法?代码可能是 Swift、block、c、OC,所以 hook 肯定不行、fishhook 也不行,用 clang 插桩可以满足需求。 +启动优化具体可以查看这篇 [App 启动时间优化与二进制重排](./1.61.md) + +### 4. 精确版启动时间监控 + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/AppStarupPipeline.png) + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/APMStartup.png) + +进程创建:通过 sysctl 可以拿到 + +第一个 +load 时刻:通过 `AAA` 为前缀给 Pod 命名,则可以让 +load 第一个被执行,从而记录时间 + +didFinishLaunching:通过 UIApplicationDidFinishLaunchingNotification 拿到 + +首屏渲染时间怎么拿?能获取到 `CA::Transaction::commit()` 时刻即可。 + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/CATransactionCommit.png) + +对 UI 进行修改后会在主线程休眠前触发 `CA::Transaction::commit()`,其实就是打包 Render Tree,通过 IPC 发送给 Render Server。 + +对 View 的各个方法加断点,查看和 Core Animation Commit 的时间顺序: + +在`CA::Transaction::commit()` 会依次执行以下步骤: + +- setNeedsDisplay + +- Layout 布局:调用 `layout` 等与布局相关的 API + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/CoreAnimationPipeline1.png) + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/CoreAnimationPipeline2.png) + +- Display 绘制:调用 `drawRect` 等与绘制相关方法 + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/CoreAnimationPipeline3.png) + +- Prepare:图片解码 + +- Commit:递归打包 Render Tree,通过 IPC 计数发送给 Render Server + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/CoreAnimationPipeline4.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/CoreAnimationPipeline5.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/CoreAnimationPipeline6.png) + +断点调试完,`[BSXPCServiceConnectionProxy invokeMethod:onTarget:withMessage:forConnection:]` 底层调用的是 send 方法。send 方法调用 `BSXPCServiceConnectionMessageReply send` 方法。 + +我们 hook `BSXPCServiceConnectionMessageReply` 的 send 便可监控启动耗时监控。 + +```objectivec + Class aClass = NSClassFromString(@"BSXPCServiceConnectionMessageReply"); + Class class = aClass; + SEL originalSelector = NSSelectorFromString(@"send"); + SEL swizzledSelector = @selector(mockSend); + + Method originalMethod = class_getInstanceMethod(aClass, originalSelector); + Method swizzledMethod = class_getInstanceMethod([KKMonitor class], swizzledSelector); + + BOOL didAddMethod = + class_addMethod(class, + originalSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod)); + if (didAddMethod) { + class_replaceMethod(class, + swizzledSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod)); + } else { + method_exchangeImplementations(originalMethod, swizzledMethod); + } +} + +- (void)mockSend +{ + // 时间计算 +} +``` + ## 三、 CPU 使用率监控 ### 1. CPU 架构 @@ -787,7 +857,7 @@ iOS 不支持交换空间?不只是 iOS 不支持交换空间,大多数手 内存(RAM)与 CPU 一样都是系统中最稀少的资源,也很容易发生竞争,应用内存与性能直接相关。iOS 没有交换空间作为备选资源,所以内存资源尤为重要。 -什么是 OOM?是 out-of-memory 的缩写,字面意思是超过了内存限制。分为 FOOM(Foreground Out Of Memory,应用在前台运行的过程中崩溃。用户在使用的过程中产生的,这样的崩溃会使得活跃用户流失,业务上是非常不愿意看到的)和 BOOM(Background Out Of Memory,应用在后台运行的过程崩溃)。它是由 iOS 的 `Jetsam` 机制造成的一种非主流 Crash,它不能通过 Signal 这种监控方案所捕获。 +什么是 OOM?是 out-of-memory 的缩写,字面意思是超过了内存限制。分为 FOOM(Foreground Out Of Memory)应用在前台运行的过程中崩溃。用户在使用的过程中产生的,这样的崩溃会使得活跃用户流失,业务上是非常不愿意看到的和 BOOM(Background Out Of Memory)应用在后台运行的过程崩溃。它是由 iOS 的 `Jetsam` 机制造成的一种非主流 Crash,它不能通过 Signal 这种监控方案所捕获。 什么是 Jetsam 机制?Jetsam 机制可以理解为系统为了控制内存资源过度使用而采用的一种管理机制。Jetsam 机制是运行在一个独立的进程中,每个进程都有一个内存阈值,一旦超过这个内存阈值,Jetsam 会立即杀掉这个进程。 @@ -1630,18 +1700,18 @@ for 循环打印出每个进程(也就是 App)的 pid、Priority、User Data /* * The jetsam no frills kill call * Return: 0 on success - * error code on failure (EINVAL...) + * error code on failure (EINVAL...) */ static int jetsam_do_kill(proc_t p, int jetsam_flags, os_reason_t jetsam_reason) { - int error = 0; - error = exit_with_reason(p, W_EXITCODE(0, SIGKILL), (int *)NULL, FALSE, FALSE, jetsam_flags, jetsam_reason); - return error; + int error = 0; + error = exit_with_reason(p, W_EXITCODE(0, SIGKILL), (int *)NULL, FALSE, FALSE, jetsam_flags, jetsam_reason); + return error; } ``` FacekBook 提出排除法监控 OOM。 -![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Facebook-OOM.jpeg) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/Facebook-OOM.jpeg) - App 更新了版本 @@ -1665,10 +1735,6 @@ FacekBook 提出排除法监控 OOM。 - 应用 exit 主动退出 - - - - OOM 导致 crash 前,app 一定会收到低内存警告吗? 做 2 组对比实验: @@ -1979,7 +2045,7 @@ for (NSInteger index = 0; index < 10000000; index++) { 其他的开发习惯就不一一描述了,良好的开发习惯和代码意识是需要平时注意修炼的。 -### 7. 现状及其改进 +### 7. 在使用了一波业界优秀的的内存监控工具后发现了一些问题,比如 `MLeaksFinder`、`OOMDetector`、`FBRetainCycleDetector`等都有一些问题。比如 `MLeaksFinder` 因为单纯通过 VC 的 push、pop 等检测内存泄露的情况,会存在误报的情况。`FBRetainCycleDetector` 则因为对象深度优先遍历,会有一些性能问题,影响 App 性能。`OOMDetector` 因为没有合适的触发时机。 @@ -1995,7 +2061,791 @@ for (NSInteger index = 0; index < 10000000; index++) { 全部的讲解可以看[这里](<(https://mp.weixin.qq.com/s/4-4M9E8NziAgshlwB7Sc6g)>)。对于 Memory Graph 的实现细节感兴趣的可以看这篇[文章](https://juejin.cn/post/6895583288451465230) -## 五、 App 网络监控 +## 五、野指针/内存泄漏 + +### 1. 概念定义 + +1.内存泄漏会导致 OOM,那么什么是内存泄漏? + +定义:程序中已经动态分配的堆内存,由于某些原因,导致程序无法释放或者未释放内存,造成系统内存的浪费,导致程序的运行速度减慢甚至是系统奔溃等严重后果。 + +一般来说导致内存泄漏的原因:对象没有释放、循环引用(无法释放) + +2.什么是野指针? + +C 语言中:声明一个指针变量,但是没有初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,指向一个随机的内存空间。所以指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存 + +OC 语言:指针指向的内存对象已经被释放或回收了,但是指针没有置为 nil,还是指向已经回收的内存空间。 + +3.什么是空指针? + +空指针就是没有存储任何内存地址(也就是没有指向任何对象的指针),即 nil、Nil、NULL、NSNULL、0 + +- nil:OC 中的对象的空指针 + +- Nil:OC 中类的空指针 + +- NULL:C 类型的空指针 + +- NSNull:数值类的空对象 + +4.内存回收的本质 + +申请一块内存空间,其本质是向系统申请一块别人不再使用的内存空间,即已经回收的内存空间 + +释放一块内存空间,其本质是这个内存空间不再使用,可以由系统分配给别的对象使用。此时内存空间虽然回收了,但是原本的数据依赖的是存在的,可以理解为垃圾数据。 + +5.什么是僵尸对象? + +僵尸对象就是指一个 OC 对象释放后所占用的内存还没被复写(重新分配给其他对象)前被称为僵尸对象。此时僵尸对象内存很不稳定,内存随时可能被系统分配给其他对象所使用。所以此时僵尸对象不应该访问和使用(调用对象的方法等) + +6.为什么 OC 野指针 Crash 很多? + +App 上线前经过自测、UI 自动化、精准测试、高铁回归包测试等,但是野指针具有随机性,所以很多野指针偷偷跑到线上了。 + +野指针随机性表现为: + +- 出错分支比较难进,执行不到出错的 case,所以能做的就是提高测试覆盖率 + +- 即使跑进有问题的分支,但是野指针指向的地址并不一定会导致 crash。因为野指针本质是一个指向已删除的对象或受限内存区域的指针。这里 OC 野指针指的是 OC 对象释放后但指针未置空导致的野指针。dealloc 执行后只是告诉系统这篇内存我不用,但是系统并没有让这块内存不能访问。 + +### 2. Zombie Objects + +Zoom Object 是 Xcode 提供的一种用来检测内存问题的对象(EXC_BAD_ACCESS),它可以捕获任何尝试访问坏内存的调用。 + +如果给僵尸对象发送消息,那么系统会在运行期间奔溃并在 Xcode 输出错误日志,通过日志定位到野指针对象调用的方法和类名。 + +当野指针指向的僵尸对象所占用的内存还未被复写(未分配),此时是可以访问的 + +当野指针指向的僵尸对象所占用的内存被分配后,此时访问会奔溃,比如 objc_msgSend 失败、SIGBUS、SIGFPE、SIGILL 等异常。 + +### 3. 探索 Xcode 如何实现僵尸对象检测的原理 + +Xcode 默认设置不检查指针指向的对象是否是僵尸对象。为什么这么设计?因为开启会影响开发效率,所以线上就会报错。 + +线下阶段做内存优化时,有些僵尸对象访问是不会报错的,可以在 Xcode 中开启。路径为:Edit Scheme - Diagnostics - Memory Management - Zombie Objects + +Demo:MRC + Xcode 开启 Zombie Object + +```objectivec +- (void)viewDidLoad { + [super viewDidLoad]; + Person *person = [[Person alloc] init]; + printClassInfo(person); + [person release]; + printClassInfo(person); +    [person description]; +} +2022-05-14 01:35:54.521783+0800 DDD[79672:3001452] self:Person - superClass:NSObject +2022-05-14 01:35:54.522115+0800 DDD[79672:3001452] self:_NSZombie_Person - superClass:nil +2022-05-14 01:35:54.523292+0800 DDD[79672:3001452] *** -[Person description]: message sent to deallocated instance 0x6000024f1030 +``` + +(前提是开启了 Zombie Objects)可以看到系统在回收对象时,不是真正的回收,而是先将其转为僵尸对象,僵尸对象所在内存无法被重用,所以让不稳定复现的内存奔溃变为稳定崩溃(更好的复现问题)。 + +开启僵尸对象检测后,系统会修改对象 isa 指针,指向新生成的僵尸类,僵尸类可以响应所有的消息,但表现打印格式为 `*** -[类 sel]: message sent to deallocated instance 地址` 的日志,然后结束进程。 + +神奇,为什么打印出 `_NSZombie_Person` 。 + +```objectivec +// Replaced by NSZombies +- (void)dealloc { + _objc_rootDealloc(self); +} +``` + +objc 源码中 dealloc 方法注释说了 dealloc 的实现会被僵尸对象所替换。 + +开启 Instrucments 分析查看得到,调用了 `__dealloc_zombie` 方法。底层到底做了什么?加个符号断点查看下 + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/XcodeZombieDetect.png) + +通过符号名称大概可以才到系统会调用 dealloc 方法时,类似 KVO,派生出一个新的僵尸类类,将当前类的 isa 指针指向僵尸类。 + +查看 Runtime 源码 + +```objectivec +/*********************************************************************** +* object_dispose +* fixme +* Locking: none +**********************************************************************/ +id object_dispose(id obj) { + if (!obj) return nil; + objc_destructInstance(obj); + free(obj); + return nil; +} +/*********************************************************************** +* objc_destructInstance +* Destroys an instance without freeing memory. +* Calls C++ destructors. +* Calls ARC ivar cleanup. +* Removes associative references. +* Returns `obj`. Does nothing if `obj` is nil. +**********************************************************************/ +void *objc_destructInstance(id obj) { + if (obj) { + // Read all of the flags at once for performance. + bool cxx = obj->hasCxxDtor(); + bool assoc = obj->hasAssociatedObjects(); + + // This order is important. + if (cxx) object_cxxDestruct(obj); + if (assoc) _object_remove_assocations(obj, /*deallocating*/true); + obj->clearDeallocating(); + } + return obj; +} +``` + +dealloc 方法最终调用到 object_dispose,但是如果开启 Zombie Object 检测则不会执行 free。其中 objc_destructInstance 方法会清理对象的关联对象、c++ 的析构函数、对象的 ivar 清理。 + +另一方面,从 GUN 源码中窥探下 (NSObject.m 文件) + +```objectivec +- (void) dealloc{ + NSDeallocateObject(self); +} + +inline void NSDeallocateObject(id anObject){ + Class aClass = object_getClass(anObject); + if ((anObject != nil) && !class_isMetaClass(aClass)) { +#ifndef OBJC_CAP_ARC + obj o = &((obj)anObject)[-1]; + NSZone *z = NSZoneFromPointer(o); +#endif + /* Call the default finalizer to handle C++ destructors. + */ + // c++ 对象调用默认的析构函数 + (*finalize_imp)(anObject, finalize_sel); + + AREM(aClass, (id)anObject); + // Xcode 环境变量。调试僵尸对象,对象 -dealloc 时并没有真正释放,而是将 isa 指向NSZombie 类,从而在向它发消息时能打印出相关信息 + if (NSZombieEnabled == YES){ +#ifdef OBJC_CAP_ARC + if (0 != zombieMap){ + pthread_mutex_lock(&allocationLock); + if (0 != zombieMap) { + NSMapInsert(zombieMap, (void*)anObject, (void*)aClass); + } + pthread_mutex_unlock(&allocationLock); + } + // Xcode 环境变量。上面变量开启时对象内存不会释放,同时开启这个会释放僵尸对象 + if (NSDeallocateZombies == YES) { + object_dispose(anObject); + } else { + // 设置 isa 指针 + object_setClass(anObject, zombieClass); + } +#else + // 调用设置僵尸对象方法 + GSMakeZombie(anObject, aClass); + if (NSDeallocateZombies == YES) { + NSZoneFree(z, o); + } +#endif + } + else { +#ifdef OBJC_CAP_ARC + // ARC: runtime 释放对象方法 + object_dispose(anObject); +#else + // MRC:设置对象 isa + object_setClass((id)anObject, (Class)(void*)0xdeadface); + NSZoneFree(z, o); +#endif + } + } + return; +} + + +@class NSZombie; +static Class zombieClass = Nil; +static NSMapTable *zombieMap = 0; + +#ifndef OBJC_CAP_ARC +static void GSMakeZombie(NSObject *o, Class c) { + // 将 dealloc 对象的 isa 修改为 zombieClass。zombieClass 在 NSObject 的 initialize 方法中初始化 + object_setClass(o, zombieClass); + if (0 != zombieMap) { + pthread_mutex_lock(&allocationLock); + if (0 != zombieMap) { + NSMapInsert(zombieMap, (void*)o, (void*)c); + } + pthread_mutex_unlock(&allocationLock); + } +} +#endif +// 后续针对 Zombie Objects 对象的任何消息都会经过 runtime 调用到 GSLogZombie,内部会打印日志 +static void GSLogZombie(id o, SEL sel){ + Class c = 0; + if (0 != zombieMap) { + pthread_mutex_lock(&allocationLock); + if (0 != zombieMap) { + c = NSMapGet(zombieMap, (void*)o); + } + pthread_mutex_unlock(&allocationLock); + } + if (c == 0) { + NSLog(@"*** -[??? %@]: message sent to deallocated instance %p", + NSStringFromSelector(sel), o); + } else { + // 按照固定格式打印日志 + NSLog(@"*** -[%@ %@]: message sent to deallocated instance %p", + c, NSStringFromSelector(sel), o); + } + // 最后根据环境变量判断,调用系统底层 abort 来奔溃 + if (GSPrivateEnvironmentFlag("CRASH_ON_ZOMBIE", NO) == YES) { + abort(); + } +} + +@implementation NSZombie +- (Class) class{ + return object_getClass(self); +} +- (Class) originalClass{ + Class c = Nil; + if (0 != zombieMap) { + pthread_mutex_lock(&allocationLock); + if (0 != zombieMap) { + c = NSMapGet(zombieMap, (void*)self); + } + pthread_mutex_unlock(&allocationLock); + } + return c; +} +// 针对僵尸对象的任何方法调用都会走 runtime forwarding 进行调用 GSLogZombie +- (void) forwardInvocation: (NSInvocation*)anInvocation { + NSUInteger size = [[anInvocation methodSignature] methodReturnLength]; + unsigned char v[size]; + memset(v, '\0', size); + GSLogZombie(self, [anInvocation selector]); + [anInvocation setReturnValue: (void*)v]; + return; +} +- (NSMethodSignature*) methodSignatureForSelector: (SEL)aSelector { + Class c; + if (0 == aSelector) { + return nil; + } + pthread_mutex_lock(&allocationLock); + c = zombieMap ? NSMapGet(zombieMap, (void*)self) : Nil; + pthread_mutex_unlock(&allocationLock); + return [c instanceMethodSignatureForSelector: aSelector]; +} +@end + +// OSObject ++ (void) initialize { + if (self == [NSObject class]) { + // ... + /* Determine zombie management flags and set up a map to store + * information about zombie objects. + */ + NSZombieEnabled = GSPrivateEnvironmentFlag("NSZombieEnabled", NO); + NSDeallocateZombies = GSPrivateEnvironmentFlag("NSDeallocateZombies", NO); + zombieMap = NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks, + NSNonOwnedPointerMapValueCallBacks, 0); + + /* We need to cache the zombie class. + * We can't call +class because NSZombie doesn't have that method. + * We can't use NSClassFromString() because that would use an NSString + * object, and that class hasn't been initialized yet ... + */ + zombieClass = objc_lookUpClass("NSZombie"); + } + return; +} +``` + +可以从 GUN 中看到调用对象 dealloc 方法时,内部实现通过调用 `GSMakeZombie` 方法,将类的 isa 指向为 NSZombie 类,也就是 zombieClass,其中 zombieClass 在 NSObject `initialize` 方法中初始化。后续针对僵尸对象的所有方法调用,都会走 Runtime forwarding 这个机制,内部会调用 `GSLogZombie` 方法,方法会按照 `NSLog(@"*** -[%@ %@]: message sent to deallocated instance %p", c, NSStringFromSelector(sel), o);` 格式打印日志,最后根据环境变量判断,调用系统底层 `abort` 来奔溃 + +### 4. Malloc Scribble + +申请内存 alloc 时在内存上填 `0xAA`,释放内存 dealloc 时在内存上填 `0x55`。此种方案也会让内存泄漏偶现的 crash 变为必现。 + +### 4. 野指针监控工具 + +##### 类 Zombie Object 方案 + +可以模拟系统工作原理,做到类似的野指针监控。 + +其中的僵尸对象检测做了这么几件事: + +- 开启僵尸对象时,不回收真正内存,而是将其 isa 变为僵尸对象,僵尸对象在内存中无法复用,所以偶现的 crash 变为必现 crash + +- 僵尸类可以响应任何消息,是通过 Runtime forawrding 实现的。表现为先打印一条日志,按照 `NSLog(@"*** -[%@ %@]: message sent to deallocated instance %p", c, NSStringFromSelector(sel), o);` 格式打印日志。随后调用系统的 `abort()` 奔溃。 + +- 奔溃的时候应该将调用堆栈等案发信息保存下来,走 APM 问题跟进流程 + +```objectivec +// ZombiePoxy +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ZombieProxy : NSProxy +@property (nonatomic, assign) Class originClass; +@end + +NS_ASSUME_NONNULL_END + +#import "ZombieProxy.h" +#define MockZombieObjectsDetector [self mockSystemBehaviorOfZombieObjects: _cmd] + +@implementation ZombieProxy + +- (BOOL)respondsToSelector: (SEL)aSelector +{ + return [self.originClass instancesRespondToSelector:aSelector]; +} + +- (NSMethodSignature *)methodSignatureForSelector: (SEL)sel +{ + return [self.originClass instanceMethodSignatureForSelector:sel]; +} + +- (void)forwardInvocation: (NSInvocation *)invocation +{ + [self mockSystemBehaviorOfZombieObjects:invocation.selector]; +} + + +- (Class)class +{ + MockZombieObjectsDetector; + return nil; +} + +- (BOOL)isEqual:(id)object +{ + MockZombieObjectsDetector; + return NO; +} + +- (NSUInteger)hash +{ + MockZombieObjectsDetector; + return 0; +} + +- (id)self +{ + MockZombieObjectsDetector; + return nil; +} + +- (BOOL)isKindOfClass:(Class)aClass +{ + MockZombieObjectsDetector; + return NO; +} + +- (BOOL)isMemberOfClass:(Class)aClass +{ + MockZombieObjectsDetector; + return NO; +} + +- (BOOL)conformsToProtocol:(Protocol *)aProtocol +{ + MockZombieObjectsDetector; + return NO; +} + +- (BOOL)isProxy +{ + MockZombieObjectsDetector; + return NO; +} + +- (void)dealloc +{ + MockZombieObjectsDetector; + [super dealloc]; +} + +- (NSZone *)zone +{ + MockZombieObjectsDetector; + return nil; +} + +- (NSString *)description +{ + MockZombieObjectsDetector; + return nil; +} + +#pragma mark - Private +- (void)mockSystemBehaviorOfZombieObjects:(SEL)selector +{ + NSString *msg = [NSString stringWithFormat:@"(-[%@ %@]) was sent to a zombie object at address: %p", NSStringFromClass(self.originClass), NSStringFromSelector(selector), self]; + NSLog(@"%@", msg); + NSLog(@"堆栈:\n%@",[NSThread callStackSymbols]); + abort(); +} +@end + +// ZombieSniffer.h +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ZombieSniffer : NSObject + +/*! + * @method installSniffer + * 启动zombie检测 + */ ++ (void)installSniffer; + +/*! + * @method uninstallSnifier + * 停止zombie检测 + */ ++ (void)uninstallSnifier; + +/*! + * @method appendIgnoreClass + * 添加白名单类 + */ ++ (void)appendIgnoreClass: (Class)cls; + +@end + +NS_ASSUME_NONNULL_END + + +#import "ZombieSniffer.h" +#import +#import "ZombieProxy.h" + +typedef void (*MIDeallocPointer) (id objc); +//野指针探测器是否开启 +static BOOL _enabled = NO; +//根类 +static NSArray *_rootClasses = nil; +//用于存储被释放的对象 +static NSDictionary *_rootClassDeallocImps = nil; + +//白名单 +static inline NSMutableSet *__mi_sniffer_white_lists() +{ + //创建白名单集合 + static NSMutableSet *mi_sniffer_white_lists; + //单例初始化白名单集合 + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + mi_sniffer_white_lists = [[NSMutableSet alloc] init]; + }); + return mi_sniffer_white_lists; +} + +static inline void __mi_dealloc(__unsafe_unretained id obj) +{ + //获取对象的类 + Class currentCls = [obj class]; + Class rootCls = currentCls; + + //获取非NSObject和NSProxy的类 + while (rootCls != [NSObject class] && rootCls != [NSProxy class]) { + //获取rootCls的父类,并赋值 + rootCls = class_getSuperclass(rootCls); + } + //获取类名 + NSString *clsName = NSStringFromClass(rootCls); + //根据类名获取dealloc的imp指针 + MIDeallocPointer deallocImp = NULL; + [[_rootClassDeallocImps objectForKey:clsName] getValue:&deallocImp]; + + if (deallocImp != NULL) { + deallocImp(obj); + } +} + +static inline IMP __mi_swizzleMethodWithBlock(Method method, void *block) +{ + IMP blockImp = imp_implementationWithBlock((__bridge id _Nonnull)(block)); + return method_setImplementation(method, blockImp); +} + +@implementation ZombieSniffer + + ++ (void)initialize +{ + _rootClasses = [@[[NSObject class], [NSProxy class]] retain]; +} + +#pragma mark - public ++ (void)installSniffer +{ + @synchronized (self) { + if (!_enabled) { + [self _swizzleDealloc]; + _enabled = YES; + } + } +} + ++ (void)uninstallSnifier +{ + @synchronized (self) { + if (_enabled) { + [self _unswizzleDealloc]; + _enabled = NO; + } + } +} + ++ (void)appendIgnoreClass:(Class)cls +{ + @synchronized (self) { + NSMutableSet *whiteList = __mi_sniffer_white_lists(); + NSString *clsName = NSStringFromClass(cls); + [clsName retain]; + [whiteList addObject:clsName]; + } +} + +#pragma mark - private ++ (void)_swizzleDealloc +{ + static void *swizzledDeallocBlock = NULL; + //定义block,作为方法的IMP + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + swizzledDeallocBlock = (__bridge void *)[^void(id obj) { + //获取对象的类 + Class currentClass = [obj class]; + //获取类名 + NSString *clsName = NSStringFromClass(currentClass); + //判断该类是否在白名单类 + if ([__mi_sniffer_white_lists() containsObject: clsName]) { + //如果在白名单内,则直接释放对象 + __mi_dealloc(obj); + } else { + NSValue *objVal = [NSValue valueWithBytes: &obj objCType: @encode(typeof(obj))]; + //为obj设置指定的类 + object_setClass(obj, [ZombieProxy class]); + //保留对象原本的类 + ((ZombieProxy *)obj).originClass = currentClass; + + //设置在30s后调用dealloc将存储的对象释放,避免内存空间的增大 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + __unsafe_unretained id deallocObj = nil; + //获取需要dealloc的对象 + [objVal getValue: &deallocObj]; + //设置对象的类为原本的类 + object_setClass(deallocObj, currentClass); + //释放 + __mi_dealloc(deallocObj); + }); + } + } copy]; + }); + + //交换了根类NSObject和NSProxy的dealloc方法为originalDeallocImp + NSMutableDictionary *deallocImps = [NSMutableDictionary dictionary]; + //遍历根类 + for (Class rootClass in _rootClasses) { + //获取指定类中dealloc方法 + Method oriMethod = class_getInstanceMethod([rootClass class], NSSelectorFromString(@"dealloc")); + //hook - 交换dealloc方法的IMP实现 + IMP originalDeallocImp = __mi_swizzleMethodWithBlock(oriMethod, swizzledDeallocBlock); + //设置IMP的具体实现 + [deallocImps setObject: [NSValue valueWithBytes: &originalDeallocImp objCType: @encode(typeof(IMP))] forKey: NSStringFromClass(rootClass)]; + } + //_rootClassDeallocImps字典存储交换后的IMP实现 + _rootClassDeallocImps = [deallocImps copy]; +} + ++ (void)_unswizzleDealloc +{ + //还原dealloc交换的IMP + [_rootClasses enumerateObjectsUsingBlock:^(Class rootClass, NSUInteger idx, BOOL * _Nonnull stop) { + IMP originDeallocImp = NULL; + //获取根类类名 + NSString *clsName = NSStringFromClass(rootClass); + //获取hook后的dealloc实现 + [[_rootClassDeallocImps objectForKey:clsName] getValue:&originDeallocImp]; + + NSParameterAssert(originDeallocImp); + //获取原本的dealloc实现 + Method oriMethod = class_getInstanceMethod([rootClass class], NSSelectorFromString(@"dealloc")); + //还原dealloc的实现 + method_setImplementation(oriMethod, originDeallocImp); + }]; + //释放 + [_rootClassDeallocImps release]; + _rootClassDeallocImps = nil; +} + +@end + +2022-05-14 12:29:06.879970+0800 DDD[86210:3208547] self:Person - superClass:NSObject +2022-05-14 12:29:06.880239+0800 DDD[86210:3208547] self:ZombieProxy - superClass:NSProxy +2022-05-14 12:29:06.880451+0800 DDD[86210:3208547] (-[Person description]) was sent to a zombie object at address: 0x6000013c03e0 +2022-05-14 12:29:06.885690+0800 DDD[86210:3208547] 堆栈: +( + 0 DDD 0x00000001010968d3 -[ZombieProxy mockSystemBehaviorOfZombieObjects:] + 163 + 1 DDD 0x0000000101096825 -[ZombieProxy description] + 37 + 2 DDD 0x0000000101092fe4 -[ViewController viewDidLoad] + 132 + 3 UIKitCore 0x00000001061a29f0 -[UIViewController _sendViewDidLoadWithAppearanceProxyObjectTaggingEnabled] + 88 + 4 UIKitCore 0x00000001061a73f3 -[UIViewController loadViewIfRequired] + 1193 + 5 UIKitCore 0x00000001060d077a -[UINavigationController _updateScrollViewFromViewController:toViewController:] + 162 + 6 UIKitCore 0x00000001060d0a7d -[UINavigationController _startTransition:fromViewController:toViewController:] + 162 + 7 UIKitCore 0x00000001060d1aa3 -[UINavigationController _startDeferredTransitionIfNeeded:] + 863 + 8 UIKitCore 0 +``` + +注意:ZombieProxy、ZombieSinffer 2个类要在 Build Phases 设置为非 ARC,加 `-fno-objc-arc` + +可以看到可以实现野指针的稳定复现,并打印调用信息和堆栈。真实环境这里一般是案发现场数据的保存,走数据上报策略,APM 问题状态跟进流程。 + +##### 类 Malloc Scribble 方案 + +主要步骤: + +- fishhook hook c 函数 free 方法为 safeFree + +- safeFree 内对将要释放的对象内存填充 `0x55`,使该块内存不能继续访问,从而后续发消息就必现 crash + +- 为了防止 `0x55` 这块内存被系统重用,使“必现 crash” 这个目的达不到,在 safeFree 内部不释放内存(也就是不调用之前的 free 方法,代码表现为 safeFree 内不调用 safeFree) + +- 因为不释放,为了防止系统内存消耗过快,需要在保留的内存大于某个临界值的时候,释放一部分,防止出现 OOM。同时收到系统内存警告,也要释放一部分内存 + +- 奔溃的时候应该将调用堆栈等案发信息保存下来,走 APM 问题跟进流程 + +关键代码实现 + +```objectivec +// ZombieObjectDetector +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ZombieObjectDetector : NSObject + +@end + +NS_ASSUME_NONNULL_END + + +#import "ZombieObjectDetector.h" +#import +#include +#include +#import "queue.h" +#import "fishhook.h" +#import "ZombieProxy.h" + +#define MAX_STEAL_MEM_SIZE 1024*1024*100 // 监控对象的临界值,防止系统因为大内存造成 OOM +#define MAX_STEAL_MEM_NUM 1024*1024*10 // 最多保留这么多个指针,再多就释放一部分 +#define BATCH_FREE_NUM 100// 每次释放的时候释放指针数量 + +static Class mockIsa; +static size_t DetectObjectSize; + +static void(* orig_free)(void *p); +static CFMutableSetRef registeredClasses = nil; +struct DSQueue* _unfreeQueue = NULL;//用来保存自己偷偷保留的内存:1这个队列要线程安全或者自己加锁;2这个队列内部应该尽量少申请和释放堆内存。 +int unfreeSize = 0;//用来记录我们偷偷保存的内存的大小 + + +@implementation ZombieObjectDetector + +#pragma mark - life cycle ++ (void)load +{ +#ifdef DEBUG + loadCatchProxyClass(); + init_safe_free(); +#endif +} + + +#pragma mark -------------------------- Public Methods +//系统内存警告的时候调用这个函数释放一些内存 +void freeMemoryWhenMemoryWarning(size_t freeNum) +{ +#ifdef DEBUG + size_t count = ds_queue_length(_unfreeQueue); + freeNum= freeNum > count ? count:freeNum; + for (int i=0; i MAX_STEAL_MEM_NUM*0.9 || unfreeSize>MAX_STEAL_MEM_SIZE) { + freeMemoryWhenMemoryWarning(BATCH_FREE_NUM); + } else { + size_t memSiziee = malloc_size(p); + if (memSiziee > DetectObjectSize) {//有足够的空间才覆盖 + id obj=(id)p; + Class origClass= object_getClass(obj); + // 判断是不是objc对象 + char *type = @encode(typeof(obj)); + if (strcmp("@", type) == 0 && + CFSetContainsValue(registeredClasses, origClass)) { + memset(obj, 0x55, memSiziee); + memcpy(obj, &mockIsa, sizeof(void*)); //更改 isa + object_setClass(obj, [ZombieProxy class]); + ((ZombieProxy *)obj).originClass = origClass; + __sync_fetch_and_add(&unfreeSize, (int)memSiziee);// 多线程下int的原子加操作,多线程对全局变量进行自加,不用理线程锁了 + ds_queue_put(_unfreeQueue, p); + }else{ + orig_free(p); + } + }else{ + orig_free(p); + } + } +} + +void loadCatchProxyClass(void) +{ + registeredClasses = CFSetCreateMutable(NULL, 0, NULL); + unsigned int count = 0; + Class *classes = objc_copyClassList(&count); + for (unsigned int i = 0; i < count; i++) { + CFSetAddValue(registeredClasses, (__bridge const void *)(classes[i])); + } + free(classes); + classes = NULL; + mockIsa = objc_getClass("ZombieProxy"); + DetectObjectSize = class_getInstanceSize(mockIsa); +} + + +bool init_safe_free(void) +{ + _unfreeQueue = ds_queue_create(MAX_STEAL_MEM_NUM); + orig_free = (void(*)(void*))dlsym(RTLD_DEFAULT, "free"); + rebind_symbols((struct rebinding[]){{"free", (void*)safeFree}}, 1); + return true; +} +@end +``` + +注意:ZombieProxy、ZombieObjectDetector 2个类要在 Build Phases 设置为非 ARC,加 `-fno-objc-arc` + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/ZombieObjectsDetector.png) + +## 六、 App 网络监控 移动网络环境一直很复杂,WIFI、2G、3G、4G、5G 等,用户使用 App 的过程中可能在这几种类型之间切换,这也是移动网络和传统网络间的一个区别,被称为「Connection Migration」。此外还存在 DNS 解析缓慢、失败率高、运营商劫持等问题。用户在使用 App 时因为某些原因导致体验很差,要想针对网络情况进行改善,必须有清晰的监控手段。 @@ -2851,13 +3701,13 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式 dispatch_once(&onceToken, ^{ _sharedInstance = [NetworkDelegateProxy alloc]; - + }); - + return _sharedInstance; - + } - + #pragma mark - public Method - (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate { @@ -3647,7 +4497,7 @@ typedef CFHTTPMessageRef (*APMURLResponseFetchHTTPResponse)(CFURLRef response); } ``` -## 六、 电量消耗 +## 七、 电量消耗 移动设备上电量一直是比较敏感的问题,如果用户在某款 App 的时候发现耗电量严重、手机发热严重,那么用户很大可能会马上卸载这款 App。所以需要在开发阶段关心耗电量问题。 @@ -3747,7 +4597,7 @@ NSCache 的使用可以查看 SDWebImage 这个图片加载框架。在图片读 `- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;` 方法用来设置缓存条件。所以我们写磁盘、内存的文件操作时可以借鉴该策略,以优化耗电量。 -## 七、 Crash 监控 +## 八、 Crash 监控 ### 1. 异常相关知识回顾 @@ -5405,15 +6255,89 @@ export type ErrorUtilsT = typeof ErrorUtils; 所以 RN 的异常可以使用 `global.ErrorUtils` 来设置错误处理。举个例子 -``` -global.ErrorUtils.setGlobalHandler(e => { - // e.name e.message e.stack -}, true); +```js + +const defaultHandler = ErrorUtils.getGlobalHandler && ErrorUtils.getGlobalHandler(); + +ErrorUtils.setGlobalHandler( (error: Error, isFatal?: boolean) => { + + // + console.log( + `Global Error Handled: ${JSON.stringify( + { + isFatal, + errorName: error.name, + errorMessage: error.message, + componentStack: error.componentStack, + errorStack: error.stack, + }, + null, + 2, + )}`, + ); + + defaultHandler(error, isFatal); +}); ``` -###### 2.7.3.2 组件问题 -其实对于 RN 的 crash 处理还有个需要注意的就是 **React Error Boundaries**。[详细资料](https://zh-hans.reactjs.org/docs/error-boundaries.html) + +红屏产生的原因是 ErrorUtils.setGlobalHandler 捕获全局错误后,调用 LogBox 来显示红屏。红屏报错逻辑涉及 RN 框架源码中2个文件,分别为 [setUpErrorHandling.js](https://github.com/facebook/react-native/blob/8bd3edec88148d0ab1f225d2119435681fbbba33/Libraries/Core/setUpErrorHandling.js#L32) 和 [ExceptionsManager.js](https://github.com/facebook/react-native/blob/b633cc130533f0731b2577123282c4530e4f0abe/Libraries/Core/ExceptionsManager.js#L98-L103),关键代码如下 + +```js +ErrorUtils.setGlobalHandler( (error: Error, isFatal?: boolean) => { + if (__DEV__) { + const LogBox = require('../LogBox/LogBox'); + LogBox.addException({ + message: error.message, + name: error.name, + componentStack: error.componentStack, + stack: error.stack, + isFatal + }); + } +}); +``` + +###### 2.7.3.2 Promise 错误 + +普通的 js 错误,可以通过 try catch 捕获,但是 Promise 错误是不能被 try catch 捕获的。 + +RN 提供了2种 Promise 捕获机制:一种是新架构的 Hermes 引擎提供的捕获机制、另一种是老架构(非 Hermes)引擎提供的捕获机制。分别在 [polyfillPromise.js](https://github.com/facebook/react-native/blob/35800962c16a33eb8e9ff1adfd428cf00bb670d3/Libraries/Core/polyfillPromise.js#L29-L36)、[Promise.js](https://github.com/facebook/react-native/blob/8bd3edec88148d0ab1f225d2119435681fbbba33/Libraries/Promise.js#L18-L22)、[promiseRejectionTrackingOptions.js](https://github.com/facebook/react-native/blob/8bd3edec88148d0ab1f225d2119435681fbbba33/Libraries/promiseRejectionTrackingOptions.js) 中。 + +```js +const defualtRejectionTrackingOptions = { + allRejections: true, + onUnhandled: (id: string, error: Error) => {}, + onHandled : (id: string) => {} +} + +if (global?.HermesInternal?.hasPromise?.()) { + if (__DEV__) { + global.HermesInternal?.enablePromiseRejectionTracker?.( + defualtRejectionTrackingOptions, + ); + } +} else { + if (__DEV__) { + require('promise/setimmediate/rejection-tracking').enable( + defualtRejectionTrackingOptions, + ); + } +} +``` + +定义配置项 `defualtRejectionTrackingOptions`,其中的 onHnhandled 回调函数,该回调函数主要来处理未被 catch 的 Promise 错误。然后通过 `global?.HermesInternal?.hasPromise` 来判断 RN 是否采用 Hermes 引擎。如果采用 Hermes 引擎,则使用 `enablePromiseRejectionTracker` 方法来捕获未被 catch 的 Promise 错误。如果不是 Hermes 引擎则使用第三方 Promise 库中的 `rejection-tracking` 文件中的 enable 方法来捕获未被 catch 的 Promise 错误。 + + + +###### 2.7.3.3 组件 render 错误 + +其实对于 RN 的异常监控中 Javascript 错误和未捕获的 Promise 错误外,还有一类需要就是 React/React Native 的 render 错误。 + +在类组件中,render 报错指的是类的 render 方法执行报错;在函数组件中,render 报错指的就是函数本身执行报错了。 + +render 错误在本地就是红屏,线上可能没有反应或者白屏。如何监控?RN 提供了**React Error Boundaries** 专门用来捕获组件的 render 错误。[详细资料](https://zh-hans.reactjs.org/docs/error-boundaries.html) > 过去,组件内的 JavaScript 错误会导致 React 的内部状态被破坏,并且在下一次渲染时 [产生](https://github.com/facebook/react/issues/4026) [可能无法追踪的](https://github.com/facebook/react/issues/6895) [错误](https://github.com/facebook/react/issues/8579)。这些错误基本上是由较早的其他代码(非 React 组件代码)错误引起的,但 React 并没有提供一种在组件中优雅处理这些错误的方式,也无法从错误中恢复。 > @@ -6397,7 +7321,7 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化 ![符号化服务架构图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-SymbolicateServerArch.png) -## 八、Weex、Flutter 异常监控 +## 九、Weex、Flutter 异常监控 Weex 由于历史原因,不做性能监控了,现有业务代码继续跑着,只业务问题和稳定性问题。 @@ -6697,7 +7621,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) { #### 2. Flutter 异常监控 -## 九、子线程 UI 监控 +## 十、子线程 UI 监控 ### 1. 背景介绍 @@ -6731,9 +7655,9 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) { 对 [dlopen](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/dlopen.3.html)、[dlsym](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/dlsym.3.html) 陌生的小伙伴可以直接看 Apple 官方文档,这里不做展开。 -具体可以参考这个 [Demo](https://github.com/FantasticLBP/MainThreadChecker) +具体可以参考这个 [saDemo](https://github.com/FantasticLBP/MainThreadChecker) -## 十、页面渲染时长统计 +## 十一、页面渲染时长统计 当我们的产品经理、TL、领导或者任何关心我们产品质量的某个角色问你,你们的 App 看上去好像比较卡,很难用,这时候我们心里一阵空虚,卡吗? @@ -6783,7 +7707,17 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) { 1. 8060 会有特例,比如8060满足了,且此时主线程空了。因为某个 ImageView 在子线程上根据 URL 异步请求资源,之后会再次触发渲染。所以这个情况下,方案还是存在问题的 -## 十一、 APM 小结 +## 十二、单点追踪 + +或者某个 Crash 根据案发堆栈能发现并解决问题,或许某个卡顿/ANR 可以根据堆栈找到问题,或许不能。也有一些 OOM 问题,提供的内存分配信息比较难复现或者定位问题。又或者某些线上性能问题比较难直接从某一方面发现问题,这时候往往就需要单点追踪能力了,根据用户某个时间段、设备等信息,将相关的性能数据、用户行为等数据聚合起来,结合分析 + +业务团队经常有关心某个页面或者某个关键业务流程卡顿情况的诉求,比如门店经营团队的同学很关心商品加购到开单整个链路的卡顿情况。那如何实现呢?基于这个背景,可以参考 iOS 系统留给开发者标记某个场景的功能(需要搭配 Instrucments 使用)使用需要导入 `#include signpost.h>` 文件。 + +所以 APM 也提供了类似的能力,`-(void)setPhase:(NSString *)phase`。数据被打标之后,mPaaS 会去查找和检索。 + +这样情况下,业务方可以针对特定的业务流程做性能统计分析。 + +## 十三、 APM 小结 1. 通常来说各个端的监控能力是不太一致的,技术实现细节也不统一。所以在技术方案评审的时候需要将监控能力对齐统一。每个能力在各个端的数据字段必须对齐(字段个数、名称、数据类型和精度),因为 APM 本身是一个闭环,监控了之后需符号化解析、数据整理,进行产品化开发、最后需要监控大盘展示等 diff --git a/Chapter1 - iOS/1.82.md b/Chapter1 - iOS/1.82.md index 1844d91..1021380 100644 --- a/Chapter1 - iOS/1.82.md +++ b/Chapter1 - iOS/1.82.md @@ -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,所以“前一个局部变量” 也就是 self(ViewController) -![](/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>)` diff --git a/Chapter1 - iOS/1.88.md b/Chapter1 - iOS/1.88.md index ec68b18..a6e1e41 100644 --- a/Chapter1 - iOS/1.88.md +++ b/Chapter1 - iOS/1.88.md @@ -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 Code)PIC 技术。 - - 写的业务代码里面假如某一行调用了 `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) 还是 Text(RO) diff --git a/Chapter1 - iOS/1.89.md b/Chapter1 - iOS/1.89.md index acbca8e..57bd838 100644 --- a/Chapter1 - iOS/1.89.md +++ b/Chapter1 - iOS/1.89.md @@ -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 下 diff --git a/Chapter1 - iOS/1.91.md b/Chapter1 - iOS/1.91.md index e52a28b..334c5db 100644 --- a/Chapter1 - iOS/1.91.md +++ b/Chapter1 - iOS/1.91.md @@ -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 diff --git a/Chapter1 - iOS/1.94.md b/Chapter1 - iOS/1.94.md index add94b2..51d79cd 100644 --- a/Chapter1 - iOS/1.94.md +++ b/Chapter1 - iOS/1.94.md @@ -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. diff --git a/Chapter1 - iOS/chapter1.md b/Chapter1 - iOS/chapter1.md index e9a4ae6..f2bc838 100644 --- a/Chapter1 - iOS/chapter1.md +++ b/Chapter1 - iOS/chapter1.md @@ -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) \ No newline at end of file + * [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) \ No newline at end of file diff --git a/Chapter2 - Web FrontEnd/2.32.md b/Chapter2 - Web FrontEnd/2.32.md index 8173b26..94766e4 100644 --- a/Chapter2 - Web FrontEnd/2.32.md +++ b/Chapter2 - Web FrontEnd/2.32.md @@ -1,6 +1,4 @@ -# React核心技术剖析 - - +# React核心技术剖析 ## 虚拟 Dom @@ -17,19 +15,14 @@ React 性能高效的一个原因就是 Virtual Dom 的应用和 diff 之后的 - https://www.infoq.cn/article/AiQMbjI0oXZ1UrueiBze - - ## Diff 算法 diff 算法大体上做的事情就是拿到前后2个状态的 Virtual Dom ,然后按照同层级节点去比较,发现当前的节点有差异,则不向下进行比较,直接将当前节点重新渲染。 - - ## JSX 的原理 JSX 做的事情是为了告诉 React 样式模版是什么。本质上来说 JSX 就是 `React.createElement` 的可读性更强的版本。`React.createElement` 接收三个参数。参数1:标签类型;参数2:属性;参数3:子元素。 - ```Javascript render () { const { content } = this.props @@ -42,14 +35,10 @@ render () { } ``` - - ## 生命周期 ![React生命周期](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-17-ReactLifecycle.PNG) - - ## 状态管理 Redux 设计上是对 Flux 的改进,增加了 reducer。Flux 就不再介绍了。解决了各个组件之间数据传递的复杂问题。先看看 Redux 进行状态管理的一个流程吧。 @@ -67,51 +56,55 @@ Redux 设计上是对 Flux 的改进,增加了 reducer。Flux 就不再介绍 ### 开发经验 - Redux 中每次创建 action 都需要设置 type,type 为字符串,所以很容易写错,且各个组件都直接用字符串的方式创建 action 的 type 会比较分散,字符串拼写错误造成的 bug 难以排查,所以需要一个地方集中统一处理 action type。思路为在 `src/store` 文件夹下面创建 actionTypes.js 文件夹,创建全部大写的变量,然后导出。 -
+ +
展开示例代码 - - ```javascript - export const CHANGE_INPUT_VALUE = 'change_input_value'; - export const ADD_TODO_ITEM = 'add_todo_item'; - export const DELETE_TODO_ITEM = 'delete_todo_item'; - export const INIT_TODO_DATA = 'init_todo_data'; - export const GET_INIT_LIST = 'get_init_list' - ``` -
+ + ```javascript + export const CHANGE_INPUT_VALUE = 'change_input_value'; + export const ADD_TODO_ITEM = 'add_todo_item'; + export const DELETE_TODO_ITEM = 'delete_todo_item'; + export const INIT_TODO_DATA = 'init_todo_data'; + export const GET_INIT_LIST = 'get_init_list' + ``` + +
- Redux 使用的时候全局工程会创建很多 action,所以和上面的思想一样,需要集中统一处理,符合“收口原则”、“单一原则”。做法就是在 `src/store` 目录下创建一个 actionCreators.js 文件。然后在里面引入 actionType.js,根据业务导出几个产生 action 的函数。 -
- 展开示例代码 - - ```javascript - import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM } from './actionTypes' - - export const getInputChangeAction = (value) => { - return { - type: CHANGE_INPUT_VALUE, - value - } - }; - - export const getAddTodoItemAction = () => ({ - type: ADD_TODO_ITEM - }); - - export const getDeleteTodoItemAction = (value) => ({ - type: DELETE_TODO_ITEM, - value - }); - ``` -
-- store 发现 action 提交的数据是函数类型的时候,会自动执行函数 +
+ 展开示例代码 + + ```javascript + import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM } from './actionTypes' + + export const getInputChangeAction = (value) => { + return { + type: CHANGE_INPUT_VALUE, + value + } + }; + + export const getAddTodoItemAction = () => ({ + type: ADD_TODO_ITEM + }); + + export const getDeleteTodoItemAction = (value) => ({ + type: DELETE_TODO_ITEM, + value + }); + ``` + +
+ +- store 发现 action 提交的数据是函数类型的时候,会自动执行函数 ### 核心思想 - 单一数据源:整个应用的 state 被存储在一棵 Object tree 中,并且这个 object tree 只存在于唯一一个 store 中 - State 是只读的:唯一改变 state 的方式就是触发 action,action 是描述已发生时间的普通对象 - 使用纯函数来执行修改:为了描述 action 如何改变 state tree,你需要根据业务编写 reducer - + ### Redux-thunk Redux-thunk 是 redux 里面常用的一个中间件。中间件?针对谁和谁的中间?对 action 和 store 的中间件。本来 action 只可以返回一个对象,灵活性较低,但是采用了 redux-thunk 之后,action 不仅可以传递对象,还可以传递函数。 action 通过 dispatch 传递给 store。 dispatch 判断 action 的类型,如果是对象则直接传递;如果是函数则直接执行。 @@ -119,16 +112,17 @@ Redux-thunk 是 redux 里面常用的一个中间件。中间件?针对谁和 ![不使用redux-thunk时action返回函数报错](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-24-reduxThunk.png) - 异步函数不应该放在组件的生命周期函数里面。复杂的业务逻辑和异步函数适合拆分。目前主流的解决方案有2种中间件:redux-thunk、redux-saga。采用不同的策略 + - redux-thunk:将异步任务分离到 action 中。 - redux-saga:将异步任务拆分到单独的文件中,而不是 action 里面。 - 相比较而言,redux-saga 比 redux-thunk 功能更加强大,提供的有用的功能更多。 + 相比较而言,redux-saga 比 redux-thunk 功能更加强大,提供的有用的功能更多。 - react-redux:网上经常说的 react-redux 里面既有 UI 组件、也有容器组件。connect 方法将一个 UI 组件(傻瓜组件) 和 store、dispatch 联合在一起后,connect 函数的返回结果就是一个容器组件 + ```javascript export default connect(mapStateToProps, mapDispatchToProps)(TodoListReactRedux) ``` - ## 组件的写法 ```javascript @@ -142,20 +136,20 @@ class Welcome extends React.Component { return

hello, {this.props.name}

; } } - ``` -### - ## 开发tips - 不要直接操作 state,只能通过 setState 操作数据;props 是只读的 - setState 的时候如果依赖之前的 state 数据,那么 setState 第一个参数可以更改为函数方式,这个函数有2个参数 + ```javascript setState((state, props) => ({ count: state.count + props.increment })); ``` + - 路由设置的时候,我们经常会设置路径。每个路径匹配到具体的页面资源会呈现出来。但是在一开始的时候会遇到疑问,为什么我在浏览器里面输入了 “/home”。但是出来的内容还是 “/” 皮配到的页面,后来知道了还可以设置 **exact** 属性可以精确控制。 + ```javascript @@ -169,10 +163,11 @@ class Welcome extends React.Component { ``` + - 在 React 的开发中,有2个名字会很熟悉:傻瓜组件、容器组件。假如一个 TodoList 的 UI 部分和逻辑处理部分都在 一个 TodoList 组件里面进行解决,那么代码将会冗余且不易测试,为了解决此问题,我们常常会将 UI 部分单独抽离出去,只负责显示出 UI,这种组件叫做傻瓜组件(UI组件)。页面需要的数据或者点击事件的处理函数都通过 **props** 的形式由父组件传递下来。父组件在渲染的时候只负责逻辑的展示,在自身的 render 函数里面调用之前分离出去的傻瓜组件(UI组件)。为了保证代码的健壮性和安全性,UI 组件需要的数据和函数都通过 props 传递,且加一个 propTypes 安全校验。 - 无状态组件:当一个组件只有 render 函数的时候,这样的组件被叫做**无状态组件**。做法就是将 `class TodoList extends React.Dom` 修改成一个函数。函数形式的无状态组件效率比较高。因为类形式的组件,会有生命周期等函数,效率会低一些。 - + ```javascript const TodoListUI = (props) => { return
props.name
; @@ -180,6 +175,7 @@ class Welcome extends React.Component { ``` - export default 在一个模块里面只可以存在一个,使用的时候不需要 `{}`;export 可以存在多个,使用的时候需要使用 `{}` + ```javascript export const person = { name: 'lbp', @@ -192,15 +188,17 @@ class Welcome extends React.Component { import { person, testing } from './store' ``` - ## React 和 Vue 的对比 - React 是单向数据流,数据是不可变的。Vue 是双向数据流,数据是可以变的。什么意思?看下面的例子 Vue.js + ```javascript ``` + React.js + ```javascript constructor(props) { super(props); @@ -209,7 +207,7 @@ class Welcome extends React.Component { this.handleInputChange = this.handleInputChange.bind(this) store.subscribe(this.handleStateChange); } - +