docs: SSL/TLS
@@ -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. 设备无可用空间问题
|
||||
|
||||

|
||||
最早遇到这个问题,直观的判断是某个接口所在的服务器机器,出现了存储问题(因为查了代码是网络回调存在 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
|
||||
{
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
|
||||
## 结构
|
||||
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
LLVM 由三部分构成:
|
||||
|
||||
@@ -18,7 +16,7 @@ LLVM 由三部分构成:
|
||||
|
||||
- Backend(后端):生成目标程序(机器码)
|
||||
|
||||

|
||||

|
||||
|
||||
正是由于这样的设计,使得 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,具备下面优点:
|
||||
|
||||
- 设计清晰简单,容易理解,易于扩展增强
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
### 查看编译过程
|
||||
|
||||
@@ -69,12 +61,10 @@ clang -ccc-print-phases main.m
|
||||
|
||||
对 main.m 文件
|
||||
|
||||

|
||||

|
||||
|
||||
可以看到经历了:输入、预处理、编译、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[]) {
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
语法分析,生成语法树(AST,Abstract Syntax Tree):`clang -fmodules -fsyntax-only -Xclang -ast-dump main.m`
|
||||
|
||||

|
||||

|
||||
|
||||
### LLVM IR
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# 设计模式及其场景
|
||||
|
||||
898
Chapter1 - iOS/1.104.md
Normal file
@@ -0,0 +1,898 @@
|
||||
# NSNotification底层原理
|
||||
|
||||
> 有人聊起来 NSNotification 可以在不同的线程发和接收吗?对于不知道或者不确定的知识,有必要探究记录下
|
||||
|
||||
## NSNotificationCenter
|
||||
|
||||
```objectivec
|
||||
@property (class, readonly, strong) NSNotificationCenter *defaultCenter;
|
||||
// 添加 Observer
|
||||
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
|
||||
// 发送通知
|
||||
- (void)postNotification:(NSNotification *)notification;
|
||||
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
|
||||
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
|
||||
// 移除通知
|
||||
- (void)removeObserver:(id)observer;
|
||||
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;
|
||||
// 添加 Observer
|
||||
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
|
||||
@end
|
||||
```
|
||||
|
||||
通过官方 API 可以窥探得到,系统内部应该是维护了 NSNotificationName、Observer、selector、object 之间的关系。
|
||||
|
||||
直接上 GUNStep 源码探索下
|
||||
|
||||
### Observation
|
||||
|
||||
```c
|
||||
typedef struct Obs {
|
||||
id observer; /* Object to receive message. */
|
||||
SEL selector; /* Method selector. */
|
||||
struct Obs *next; /* Next item in linked list. */
|
||||
int retained; /* Retain count for structure. */
|
||||
struct NCTbl *link; /* Pointer back to chunk table */
|
||||
} Observation;
|
||||
```
|
||||
|
||||
结构体存储了 observer、selector 信息。此外可以看出,是一个链表结构(next),指向注册了同一个通知的下一个观察者。
|
||||
|
||||
### NCTbl
|
||||
|
||||
```c
|
||||
typedef struct NCTbl {
|
||||
Observation *wildcard; /* Get ALL messages. */
|
||||
GSIMapTable nameless; /* Get messages for any name. */
|
||||
GSIMapTable named; /* Getting named messages only. */
|
||||
// ...
|
||||
} NCTable;
|
||||
```
|
||||
|
||||
查看 NCTbl 结构体定义,发现其内部存在2张 MapTable。
|
||||
|
||||
- named:用于保存添加观察者的时候传入 NotificationName 的情况
|
||||
|
||||
- nemeless:同于保存添加观察者时没有传递 NotificationName 的情况
|
||||
|
||||
### named Table
|
||||
|
||||
该表用于存储添加观察者时传了 NotificationName 的情况。也就是 Named Table 中,NotificationName 作为 key。在使用系统 API 注册观察者的时候还可以传入 object 参数,表示只监听该对象发出的通知,所以还需要一张表存储 object 和 observer 的对应关系,object 为 key,observer 为 value。
|
||||
|
||||

|
||||
|
||||
- 第一个 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 为链表,存储所有的观察者。
|
||||
|
||||

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

|
||||
|
||||
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,最终显示到屏幕上。
|
||||
|
||||

|
||||
|
||||
- Core Animation 提交会话(事务),包括自己和子树(view hierarchy) 的布局状态
|
||||
|
||||
- Render Server 解析所提交的子树状态,生成绘制指令
|
||||
|
||||
- GPU 执行绘制指令
|
||||
|
||||
- 显示器显示渲染后的数据
|
||||
|
||||
## Core Animation
|
||||
|
||||

|
||||
|
||||
可以看到 Core Animation pipeline 由4部分组成:Application 层的 Core Animation 部分、Render Server 中的 Core Animation 部分、GPU 渲染、显示器显示。
|
||||
|
||||
### Application 层 Core Animation 部分
|
||||
|
||||

|
||||
|
||||
- 布局(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 绘制流程
|
||||
|
||||

|
||||
|
||||
- 每个 UIView 都有一个 CALayer,layer 属性都有 contents,contents 其实是一块缓存,叫做 backing store
|
||||
|
||||
- 当 UIView 被绘制时,CPU 执行 drawRect 方法,通过 context 将数据写入 backing store 中(位图 bitmap)
|
||||
|
||||
- 当 backing store 写完后,通过 Render Server 交给 GPU 去渲染,最后显示到屏幕上
|
||||
|
||||

|
||||
|
||||
- 调用 `[UIView setNeedsDisplay]` 方法时,并没有立即执行绘制工作,而是马上调用 `[view.layer setNeedsDisplay]` 方法,给当前 layer 打上标记
|
||||
|
||||
- 在当前 RunLoop 快要结束的时候调用 layer 的 display 方法,来进入到当前视图真正的绘制流程
|
||||
|
||||
- 在 layer 的 display 方法内部,系统会判断 layer 的 layer.delegate 是否实现了 `displayLayer` 方法
|
||||
|
||||
- 如果没有,则执行系统的绘制流程
|
||||
|
||||
- 如果实现了,则会进入异步绘制流程
|
||||
|
||||
- 最后把绘制完的 backing store 提交给 GPU
|
||||
|
||||
### 系统绘制流程
|
||||
|
||||

|
||||
|
||||
- 首先 CALayer 内部会创建一个 CGContextRef,在 drwaRect 方法中,可以通过上下文堆栈取出 context,拿到当前视图渲染上下文也就是 backing store
|
||||
|
||||
- 然后 layer 会判断是否存在代理,若没有,则调用 CALayer 的 drawInContext
|
||||
|
||||
- 如果存在代理,则调用代理方法。然后做当前视图的绘制工作,然后调用 view 的 drawRect 方法
|
||||
|
||||
- 最后由 CALayer 上传对应的 backing store(可以理解为位图)提交给 GPU。
|
||||
|
||||
### 异步绘制流程
|
||||
|
||||

|
||||
|
||||
- 如果 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能明显提升帧率,同时内存开销也比较稳定。
|
||||
256
Chapter1 - iOS/1.106.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# NSUserDefault 底层原理探究
|
||||
|
||||
最近看到字节一篇文章 [卡死崩溃监控原理及最佳实践](https://mp.weixin.qq.com/s?__biz=MzI1MzYzMjE0MQ==&mid=2247488080&idx=1&sn=39d0386b97b9ac06c6af1966f48387fc&chksm=e9d0d9b2dea750a4a7d21fd383aefa014d63f0dc79f2e3a13c97ad52bba1578dca8b50d6a40a&scene=21&cur_album_id=1590407423234719749#wechat_redirect) ,里面写到 NSUserDefault 底层实现存在直接或者间接跨进程通信,在主线程同步调用容易卡死。以前只是用过,但是没有仔细研究,看到这里就有必要研究下底层实现啦。
|
||||
|
||||
## 回顾
|
||||
|
||||
NSUserDefault 不安全。因为数据自动保存在沙盒的 `Libarary/Preferences` 目录下。
|
||||
|
||||
数据按照 plist (property list)格式存储在沙盒中。当攻击者破解 App 就可轻而易举拿到里面的数据(可能有些人会将 token、password、secret 明文存在里面)
|
||||
|
||||
另外 App 卸载重装会导致之前存储的数据丢失。这里推荐使用 Keychain。Keychain 是 iOS 提供的安全存储数据的方案,用于存储一些账号、密码等敏感信息。数据也不在沙盒中,即使删除 App,重新安装则可以继续从 Keychain 中获取数据。
|
||||
|
||||
NSUserDefaults 的原理和 plist 序列化不同。
|
||||
|
||||
iOS 上应用必须被沙盒化,各个不同 App 之间的 Defaults Domain 不一样,通常是 Bundle Identifier,或者是 App Group 中约定的 Suite Name。当使用 NSUserDefaults 的时候会按照下面 Domain 顺序:
|
||||
|
||||
- NSArgumentDomain
|
||||
|
||||
- 应用的 Bundle Identifier
|
||||
|
||||
- NSGlobalDomain
|
||||
|
||||
- 系统语言的标识符
|
||||
|
||||
- NSRegistrationDomain
|
||||
|
||||
任何应用,通过 NSUserDefaults 访问值都需要经历从上到下搜索各个 Domain 的过程,期间如何某个 Domain 有这个值,就会取出其对应的值。如果全部访问完还是没找到,则返回 undefined result。
|
||||
|
||||
## 如何保证多线程安全
|
||||
|
||||
通过设置符号断点可以看出, NSUserDefaults 内部在读写时会通过 `os_unfair_lock` 加锁进行多线程安全保护。
|
||||
|
||||

|
||||
|
||||
## 存储性能如何
|
||||
|
||||
查看 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` 可以看到下面的堆栈
|
||||
|
||||

|
||||
|
||||
执行 `[NSUserDefaults standardUserDefaults];` 可以发现是调用了 XPC,创建名称为 “com.apple.cfprefsd.daemon” 的 XPC Connection,且会发送一个 `xpc_connection_send_message_with_reply_sync` 的消息。
|
||||
|
||||
执行 `[defaults setObject:@"杭城小刘" forKey:@"name"];` 会调用 `xpc_connection_send_message_with_reply_sync`,发送一个消息。
|
||||
|
||||
通过 Demo 得出结论:
|
||||
|
||||
- `NSUserDefaults` 调用 `set...forKey:`, 会触发 XPC 通信,调用 `...ForKey:` 、`synchronized` 不会调用 XPC 通信
|
||||
|
||||
- 为了提高性能,尽量减少调用 `set...forKey:`
|
||||
|
||||
## 异步持久化
|
||||
|
||||
XPC 该`xpc_connection_send_message_with_reply_sync` API 因为 XPC 同步通信,所以在主线程容易存在卡死。那么有没有异步调用的能力?
|
||||
|
||||
发现2个 API 可以用于异步发送
|
||||
|
||||
- xpc_connection_send_message
|
||||
|
||||
- xpc_connection_send_message_with_reply
|
||||
|
||||
所以想异步持久化,则需要自定义 XPC Connection,然后将数据用 xpc_dictionary_create 创造出的 Dictionary 去接,最后调用 `xpc_connection_send_message_with_reply` 去持久化数据
|
||||
|
||||
```objectivec
|
||||
xpc_connection_t conn = xpc_connection_create_mach_service("com.apple.cfprefsd.daemon", NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED);
|
||||
#pragma mark - 开始构建信息
|
||||
// (lldb) po $rsi
|
||||
// <OS_xpc_dictionary: dictionary[0x7fa975908010]: { refcnt = 1, xrefcnt = 1, subtype = 0, count = 8 } <dictionary: 0x7fa975908010> { count = 8, transaction: 0, voucher = 0x0, contents =
|
||||
// "CFPreferencesHostBundleIdentifier" => <string: 0x7fa9759080d0> { length = 9, contents = "test.demo" }
|
||||
// "CFPreferencesUser" => <string: 0x7fa975908250> { length = 25, contents = "kCFPreferencesCurrentUser" }
|
||||
// "CFPreferencesOperation" => <int64: 0x8ccdbf87dd7d7a91>: 1
|
||||
// "Value" => <string: 0x7fa9759084b0> { length = 16, contents = "酷酷的哀殿2" }
|
||||
// "Key" => <string: 0x7fa975908430> { length = 3, contents = "key" }
|
||||
// "CFPreferencesContainer" => <string: 0x7fa9759083a0> { length = 169, contents = "/private/var/mobile/Containers/Data/Application/0C224166-1674-4D36-9CDB-9FCDB633C7E3/" }
|
||||
// "CFPreferencesCurrentApplicationDomain" => <bool: 0x7fff80002fd0>: true
|
||||
// "CFPreferencesDomain" => <string: 0x7fa975906ea0> { length = 9, contents = "test.demo" }
|
||||
// }>
|
||||
|
||||
xpc_object_t hello = xpc_dictionary_create(NULL, NULL, 0);
|
||||
|
||||
// 注释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);
|
||||
```
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
和
|
||||
|
||||

|
||||

|
||||
|
||||
## 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` 查看堆栈
|
||||
|
||||

|
||||

|
||||
|
||||
查看 CF 中 `CFRunLoop.c`源码。方法比较复杂,做了精简摘要
|
||||
|
||||
@@ -1497,7 +1501,7 @@ UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示
|
||||
|
||||
2. `[[NSRunLoop currentRunLoop] run]` api 换掉。查看系统说明,底层其实就是一个无限循环,循环内部不断调用 `runMode:beforeDate:`。下面也有建议,建议我们想销毁 RunLoop,可以替换 API,比如设置一个变量,标记是否需要结束 RunLoop
|
||||
|
||||

|
||||

|
||||
|
||||
改进代码如下
|
||||
|
||||
@@ -1578,6 +1582,17 @@ UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示
|
||||
}
|
||||
```
|
||||
|
||||
线程保活后如何暂停?
|
||||
|
||||
```
|
||||
[thread cancel];
|
||||
thread = nil;
|
||||
// 指针 nil,还是被 RunLoop 持有。
|
||||
// 也不行。CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);
|
||||
```
|
||||
|
||||
主线程为什么
|
||||
|
||||
### 线程封装
|
||||
|
||||
思考:如何设计一个常驻线程工具类?
|
||||
|
||||
@@ -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 占用。存在死锁
|
||||

|
||||
|
||||
```
|
||||
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;
|
||||
|
||||
假如对存钱过程,忘记解锁怎么办?产生死锁,如下
|
||||
|
||||

|
||||

|
||||
|
||||
添加 cursor 标记死锁是发生在 `saveMoney` 方法执行的第几次。发现是第二次。因为第一次锁没有任何使用方,所以加锁成功,当第二次加锁的时候发现锁没有释放,所以产生死锁。
|
||||
|
||||
这时候使用尝试加锁 API `os_unfair_lock_trylock` 即可成功如下
|
||||
|
||||

|
||||

|
||||
|
||||
### 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 方法,开启汇编调试
|
||||
|
||||

|
||||

|
||||
|
||||
看到可疑方法 `OSSpinLockLock`,给它加断点,看到第10行高亮了。lldb 模式输入 c,敲回车。次数输入 si 即可进入 `OSSpinLockLock` 方法内部调试
|
||||
|
||||
第二步:继续输入 si,敲回车
|
||||
|
||||

|
||||

|
||||
|
||||
第三步:看到可疑方法 `_OSSpinLockLockSlow`,给它加断点,lldb 输入 C。此时断点到这一行了,继续输入 si。
|
||||
|
||||

|
||||

|
||||
|
||||
第四步:在 `OSSpinLockLockSlow` 方法内部调试,不断输入 si。
|
||||
|
||||

|
||||

|
||||
|
||||
发现不断 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 叫做”信号量”
|
||||
|
||||
为了探究下实现,开启汇编调试
|
||||
|
||||

|
||||

|
||||
|
||||
可以查看 objc4 的源码,查找 `objc_sync_enter`
|
||||
|
||||
@@ -1088,10 +1090,6 @@ class recursive_mutex_tt : nocopy_t {
|
||||
|
||||
另外 `recursive_mutex_tt` 在初始化的时候传入 `PTHREAD_RECURSIVE_MUTEX_INITIALIZER`,看起来也支持递归。所以 @synchronized 是一个递归互斥锁的封装。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
封装
|
||||
|
||||
有的时候我们需要在方法内部创建 semaphore ,则可以创建宏
|
||||
@@ -1107,11 +1105,8 @@ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
||||
|
||||
#define SemaphoreEnd \
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 自旋锁、互斥锁对比
|
||||
|
||||
什么情况使用自旋锁比较划算?
|
||||
@@ -1136,11 +1131,7 @@ dispatch_semaphore_signal(semaphore);
|
||||
|
||||
- 临界区竞争非常激烈
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## atomic
|
||||
### atomic
|
||||
|
||||
`atomic` 用于保证属性 setter、getter 的原子性操作,相当于在 getter 和 setter 内部加了线程同步的锁。
|
||||
|
||||
@@ -1155,13 +1146,13 @@ id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
|
||||
// Retain release world
|
||||
id *slot = (id*) ((char*)self + offset);
|
||||
if (!atomic) return *slot;
|
||||
|
||||
|
||||
// Atomic retain release world
|
||||
spinlock_t& slotlock = PropertyLocks[slot];
|
||||
slotlock.lock();
|
||||
id value = objc_retain(*slot);
|
||||
slotlock.unlock();
|
||||
|
||||
|
||||
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
|
||||
return objc_autoreleaseReturnValue(value);
|
||||
}
|
||||
@@ -1182,7 +1173,7 @@ static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t o
|
||||
}
|
||||
|
||||
id oldValue;
|
||||
id *slot = (id*) ((char*)self + offset);
|
||||
id *slot = (id*) ((char*)self offset);
|
||||
|
||||
if (copy) {
|
||||
newValue = [newValue copyWithZone:nil];
|
||||
@@ -1213,6 +1204,13 @@ void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL ato
|
||||
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
|
||||
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
|
||||
}
|
||||
|
||||
void lock() {
|
||||
lockdebug_mutex_lock(this);
|
||||
// <rdar://problem/50384154>
|
||||
uint32_t opts = OS_UNFAIR_LOCK_DATA_SYNCHRONIZATION | OS_UNFAIR_LOCK_ADAPTIVE_SPIN;
|
||||
os_unfair_lock_lock_with_options_inline(&mLock, (os_unfair_lock_options_t)opts);
|
||||
}
|
||||
```
|
||||
|
||||
可以看到设置属性的时候会判断是不是 atomic
|
||||
@@ -1223,15 +1221,38 @@ void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL ato
|
||||
|
||||
它并不能保证使用属性的过程是线程安全的。
|
||||
|
||||
|
||||
|
||||
QA:为什么在 iOS 上几乎没有使用?
|
||||
|
||||
因为属性 getter、setter 使用太高频,另外 atomic 内部实现是自旋锁,自旋锁是忙等,所以太耗费性能了。
|
||||
|
||||
#### atomic 并不能保证使用属性的过程是线程安全的?
|
||||
|
||||
```objectivec
|
||||
@property (atomic,copy) NSString *name;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
@synchronized(self){
|
||||
for (int i = 0; i<100; i++) {
|
||||
self.name = @"杭城小刘";
|
||||
NSLog(@"线程1 : %@",self.name);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
@synchronized(self){
|
||||
for (int i = 0; i<100; i++) {
|
||||
self.name = @"魅影";
|
||||
NSLog(@"线程2 : %@",self.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
预期:线程 A 打印出来一定是杭城小刘,线程 B 打印出来是魅影。但事实上可能存在乱序。
|
||||
|
||||
atomic 是原子属性,它内部实现是针对属性的 setter、getter 进行加锁(早期实现是自旋旋,因为存在问题,后续替换为了 os_unfair_lock)。但是事实上在进行多线程编程的时候,我们针对数据的操作并不是修改指针本身(思考 NSString 的 getter、setter),而是操作类似 NSArray、NSDictionary 这样的 case。比如 `@property(atomic, strong)NSMutableArray *hobbies;` 如果在多线程情况下进行处理,一边生产者添加数据,一边消费者消费数据,则会产生内存问题。
|
||||
|
||||
所以多线程并发编程来说,推荐使用锁是一个合理的方案。此外自旋锁不推荐使用,互斥锁中 pthread_mutex 等性能高一些的锁推荐使用。
|
||||
|
||||
## 读写安全
|
||||
|
||||
@@ -1245,8 +1266,6 @@ QA:为什么在 iOS 上几乎没有使用?
|
||||
|
||||
- dispatch_barrier_async:异步栅栏调用
|
||||
|
||||
|
||||
|
||||
### pthread_rwlock
|
||||
|
||||
初始化
|
||||
@@ -1268,8 +1287,6 @@ pthread_rwlock_init(&_lock, NULL)
|
||||
|
||||
销毁 `pthread_rwlock_destroy(&_lock);`
|
||||
|
||||
|
||||
|
||||
```objectivec
|
||||
@interface ViewController ()
|
||||
@property (nonatomic, assign) NSInteger money;
|
||||
@@ -1324,8 +1341,6 @@ pthread_rwlock_init(&_lock, NULL)
|
||||
2022-04-09 22:25:25.045020+0800 DDD[13652:333135] read 200
|
||||
```
|
||||
|
||||
|
||||
|
||||
### dispatch_barrier_async
|
||||
|
||||
```objectivec
|
||||
@@ -1346,8 +1361,6 @@ dispatch_barrier_async(self.queue, ^{
|
||||
- 这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
|
||||
- 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果
|
||||
|
||||
|
||||
|
||||
上 Demo
|
||||
|
||||
```objectivec
|
||||
@@ -1402,4 +1415,50 @@ dispatch_barrier_async(self.queue, ^{
|
||||
2022-04-09 22:37:26.477247+0800 DDD[14019:343937] read 200
|
||||
2022-04-09 22:37:26.477267+0800 DDD[14019:343938] read 200
|
||||
2022-04-09 22:37:26.477269+0800 DDD[14019:343933] read 200
|
||||
```
|
||||
```
|
||||
|
||||
## 其他常见的多线程编程模式
|
||||
|
||||
### Promise
|
||||
|
||||
Promise 在多线程解决方案中比较常见,比如在前端中 Promise 就是一个标准解决方案。同样的,iOS 界也有三方开发者写的 PromiseKit。也有对应的 AFNetworking Promise 版本。
|
||||
|
||||
Promise 解决了什么问题?
|
||||
|
||||
- 在需要多个操作的时候,我们可能会设置多个回调参数嵌套,导致代码很长,也就是传说中的“回调地狱”(Callback Hell)
|
||||
|
||||
- 丧失了 return 特性
|
||||
|
||||
Promise 就是一个对象,用来传递异步操作的消息。代表了某个未来才会知道结果的事件(也就是异步操作),并且这个事件提供统一的 API,可以供进一步处理
|
||||
|
||||
对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中,又称 Incomplete)、Resolved(已完成,又称 Fulfilled)和 Rejected (已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。
|
||||
|
||||
一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
|
||||
|
||||
```objectivec
|
||||
APIClient.fetchData(...).then().onFailure();
|
||||
```
|
||||
|
||||
### Pipeline
|
||||
|
||||
将一个任务分解为若干个阶段(Stage),前阶段的输出为下阶段的输入,各个阶段由不同的工
|
||||
|
||||
作者线程负责执行。
|
||||
|
||||
各个任务的各个阶段是并行(Parallel)处理的。
|
||||
|
||||
具体任务的处理是串行的,即完成一个任务要依次执行各个阶段,但从整体任务上看,不同任务的各个阶段的执行是并行的。
|
||||
|
||||
### Master-Slave
|
||||
|
||||
将一个任务分解为若干个语义等同的子任务,并由专门的工作者线程来并行执行这些子任务,既 提高计算效率,又实现了信息隐藏。
|
||||
|
||||
比如 Jekins
|
||||
|
||||
### Serial Thread Confinement
|
||||
|
||||
如果并发任务的执行涉及某个非线程安全对象,而很多时候我们又不希望因此而引入锁。
|
||||
|
||||
通过将多个并发的任务存入队列实现任务的串行化,并为这些串行化任务创建唯一的工作者线程进行处理。
|
||||
|
||||
比如 FMDB 的设计,内部就是一个串行队列。
|
||||
@@ -1,4 +1,4 @@
|
||||
# 内存问题研究
|
||||
# iOS 内存原理探究
|
||||
|
||||
## 定时器内存泄漏
|
||||
|
||||
@@ -10,7 +10,7 @@ NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval
|
||||
|
||||
栈、堆、BSS、数据段、代码段
|
||||
|
||||

|
||||

|
||||
|
||||
栈(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,报错信息如下
|
||||
|
||||

|
||||

|
||||
|
||||
说明:一开始的报错信息只说坏内存访问,但是并没有显示具体的方法调用堆。想知道具体 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 查看类中的方法信息
|
||||
|
||||

|
||||
|
||||
发现存在 `.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
|
||||
```
|
||||
|
||||

|
||||
|
||||
Tips:@property 会自动生成成员变量,另外类后面加 `{}` 在内部也可以加成员变量,假如成员变量是对象类型,比如 NSString,则叫实例变量。
|
||||
|
||||
得出结论:
|
||||
|
||||
- 只有 ARC 模式下才有 `.cxx_destruct` 方法
|
||||
|
||||
- 类拥有实例变量的时候(`{}` 或者 `@property`) 才有 `.cxx_destruct`,父类成员对象的实例变量不会让子类拥有该方法
|
||||
|
||||
使用 watchpoint 观察内存释放时机
|
||||
|
||||
在 gone 的地方加断点,输入 `watchpoint set variable p->_name`,则会将 `_name` 实例变量加入 watchpoint,当变量被修改时会触发断点,可以看出从某个值变为 0x0,也就是 nil。此时边上调用堆栈显示在 `objc_storestrong` 方法中,被设置为 nil.
|
||||
|
||||

|
||||
|
||||
### 深入 .cxx_destruct
|
||||
|
||||
简单梳理下,在 ARC 模式下,类拥有实例变量的时候会在 `.cxx_destruct` 方法内调用 `objc_storeStrong` 去释放的内存。
|
||||
|
||||
我们也知道 `.cxx_destruct` 是编译器生成的代码。去查询资料 `.cxx_destruct site:clang.llvm.org`
|
||||
|
||||
在 clang 的 doxygen 文档中 [CodeGenModule 模块源码](https://clang.llvm.org/doxygen/CodeGenModule_8cpp_source.html)发现了相关逻辑。在 5907 行代码
|
||||
|
||||
```c
|
||||
void CodeGenModule::EmitObjCIvarInitializations(ObjCImplementationDecl *D) {
|
||||
// We might need a .cxx_destruct even if we don't have any ivar initializers.
|
||||
if (needsDestructMethod(D)) {
|
||||
IdentifierInfo *II = &getContext().Idents.get(".cxx_destruct");
|
||||
Selector cxxSelector = getContext().Selectors.getSelector(0, &II);
|
||||
ObjCMethodDecl *DTORMethod = ObjCMethodDecl::Create(
|
||||
getContext(), D->getLocation(), D->getLocation(), cxxSelector,
|
||||
getContext().VoidTy, nullptr, D,
|
||||
/*isInstance=*/true, /*isVariadic=*/false,
|
||||
/*isPropertyAccessor=*/true, /*isSynthesizedAccessorStub=*/false,
|
||||
/*isImplicitlyDeclared=*/true,
|
||||
/*isDefined=*/false, ObjCMethodDecl::Required);
|
||||
D->addInstanceMethod(DTORMethod);
|
||||
CodeGenFunction(*this).GenerateObjCCtorDtorMethod(D, DTORMethod, false);
|
||||
D->setHasDestructors(true);
|
||||
}
|
||||
|
||||
// If the implementation doesn't have any ivar initializers, we don't need
|
||||
// a .cxx_construct.
|
||||
if (D->getNumIvarInitializers() == 0 ||
|
||||
AllTrivialInitializers(*this, D))
|
||||
return;
|
||||
|
||||
IdentifierInfo *II = &getContext().Idents.get(".cxx_construct");
|
||||
Selector cxxSelector = getContext().Selectors.getSelector(0, &II);
|
||||
// The constructor returns 'self'.
|
||||
ObjCMethodDecl *CTORMethod = ObjCMethodDecl::Create(
|
||||
getContext(), D->getLocation(), D->getLocation(), cxxSelector,
|
||||
getContext().getObjCIdType(), nullptr, D, /*isInstance=*/true,
|
||||
/*isVariadic=*/false,
|
||||
/*isPropertyAccessor=*/true, /*isSynthesizedAccessorStub=*/false,
|
||||
/*isImplicitlyDeclared=*/true,
|
||||
/*isDefined=*/false, ObjCMethodDecl::Required);
|
||||
D->addInstanceMethod(CTORMethod);
|
||||
CodeGenFunction(*this).GenerateObjCCtorDtorMethod(D, CTORMethod, true);
|
||||
D->setHasNonZeroConstructors(true);
|
||||
}
|
||||
```
|
||||
|
||||
源码大概做的事情就是:获取 `.cxx_destructor` 的 selector,创建 Method,然后将新创建的 Method 插入到 class 方法列表中。调用 `GenerateObjCCtorDtorMethod` 方法,才创建这个方法的实现。查看 GenerateObjCCtorDtorMethod 的实现。在 https://clang.llvm.org/doxygen/CGObjC_8cpp_source.html 的1626行处。
|
||||
|
||||
```c
|
||||
static void emitCXXDestructMethod(CodeGenFunction &CGF,
|
||||
ObjCImplementationDecl *impl) {
|
||||
CodeGenFunction::RunCleanupsScope scope(CGF);
|
||||
|
||||
llvm::Value *self = CGF.LoadObjCSelf();
|
||||
|
||||
const ObjCInterfaceDecl *iface = impl->getClassInterface();
|
||||
for (const ObjCIvarDecl *ivar = iface->all_declared_ivar_begin();
|
||||
ivar; ivar = ivar->getNextIvar()) {
|
||||
QualType type = ivar->getType();
|
||||
|
||||
// Check whether the ivar is a destructible type.
|
||||
QualType::DestructionKind dtorKind = type.isDestructedType();
|
||||
if (!dtorKind) continue;
|
||||
|
||||
CodeGenFunction::Destroyer *destroyer = nullptr;
|
||||
|
||||
// Use a call to objc_storeStrong to destroy strong ivars, for the
|
||||
// general benefit of the tools.
|
||||
if (dtorKind == QualType::DK_objc_strong_lifetime) {
|
||||
destroyer = destroyARCStrongWithStore;
|
||||
|
||||
// Otherwise use the default for the destruction kind.
|
||||
} else {
|
||||
destroyer = CGF.getDestroyer(dtorKind);
|
||||
}
|
||||
|
||||
CleanupKind cleanupKind = CGF.getCleanupKind(dtorKind);
|
||||
|
||||
CGF.EHStack.pushCleanup<DestroyIvar>(cleanupKind, self, ivar, destroyer,
|
||||
cleanupKind & EHCleanup);
|
||||
}
|
||||
|
||||
assert(scope.requiresCleanups() && "nothing to do in .cxx_destruct?");
|
||||
}
|
||||
```
|
||||
|
||||
可以看到:遍历了当前对象的所有实例变量,调用 `objc_storeStrong`,从 clang 文档上可以看出
|
||||
|
||||
```c
|
||||
id objc_storeStrong(id *object, id value) {
|
||||
value = [value retain];
|
||||
id oldValue = *object;
|
||||
*object = value;
|
||||
[oldValue release];
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
在 `.cxx_destruct` 方法内部会对所有的实例变量调用 `objc_storeStrong(&ivar, null)` ,实例变量就会 release 。
|
||||
|
||||
### 自动调用 [super dealloc] 的原理
|
||||
|
||||
同理,CodeGen 也会做自动调用 `[super dealloc]` 的事情。https://clang.llvm.org/doxygen/CGObjC_8cpp_source.html,第751行 `StartObjCMethod` 方法。
|
||||
|
||||
```c
|
||||
751 void CodeGenFunction::StartObjCMethod(const ObjCMethodDecl *OMD,
|
||||
752 const ObjCContainerDecl *CD) {
|
||||
// ...
|
||||
789 // In ARC, certain methods get an extra cleanup.
|
||||
790 if (CGM.getLangOpts().ObjCAutoRefCount &&
|
||||
791 OMD->isInstanceMethod() &&
|
||||
792 OMD->getSelector().isUnarySelector()) {
|
||||
793 const IdentifierInfo *ident =
|
||||
794 OMD->getSelector().getIdentifierInfoForSlot(0);
|
||||
795 if (ident->isStr("dealloc"))
|
||||
796 EHStack.pushCleanup<FinishARCDealloc>(getARCCleanupKind());
|
||||
797 }
|
||||
798 }
|
||||
```
|
||||
|
||||
可以看到在调用到 dealloc 方法时,插入了代码,实现如下
|
||||
|
||||
```c
|
||||
struct FinishARCDealloc : EHScopeStack::Cleanup {
|
||||
void Emit(CodeGenFunction &CGF, Flags flags) override {
|
||||
const ObjCMethodDecl *method = cast<ObjCMethodDecl>(CGF.CurCodeDecl);
|
||||
|
||||
const ObjCImplDecl *impl = cast<ObjCImplDecl>(method->getDeclContext());
|
||||
const ObjCInterfaceDecl *iface = impl->getClassInterface();
|
||||
if (!iface->getSuperClass()) return;
|
||||
|
||||
bool isCategory = isa<ObjCCategoryImplDecl>(impl);
|
||||
|
||||
// Call [super dealloc] if we have a superclass.
|
||||
llvm::Value *self = CGF.LoadObjCSelf();
|
||||
|
||||
CallArgList args;
|
||||
CGF.CGM.getObjCRuntime().GenerateMessageSendSuper(CGF, ReturnValueSlot(),
|
||||
CGF.getContext().VoidTy,
|
||||
method->getSelector(),
|
||||
iface,
|
||||
isCategory,
|
||||
self,
|
||||
/*is class msg*/ false,
|
||||
args,
|
||||
method);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
代码大概就是向父类转发 dealloc 的调用实现,内部自动调用 [super dealloc] 方法。
|
||||
|
||||
总结下:
|
||||
|
||||
- ARC 模式下,实例变量由编译器插入 `.cxx_destruct` 方法自动释放
|
||||
|
||||
- ARC 模式下 `[super dealloc]` 由 llvm 编译器自动插入(CodeGen)
|
||||
|
||||
## 自动释放池底层原理探索
|
||||
|
||||
上 Demo
|
||||
@@ -705,7 +1033,7 @@ class AutoreleasePoolPage {
|
||||
- 每个 AutoreleasePoolPage 对象占用 4096 字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放 autorelease 对象的地址
|
||||
- 所有的 AutoreleasePoolPage 对象通过**双向链表**的形式连接在一起。child 指向下一个对象,parent 指向上一个对象
|
||||
|
||||

|
||||

|
||||
|
||||
```objectivec
|
||||
id * begin() {
|
||||
@@ -761,7 +1089,7 @@ int main(int argc, const char * argv[]) {
|
||||
|
||||
main 方法内部3个 autoreleasepool 底层怎么样工作的?
|
||||
|
||||

|
||||

|
||||
|
||||
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 运行图
|
||||
|
||||

|
||||

|
||||
|
||||
- 01 通知 Observer 进入 Loop 会调用 `objc_autoreleasePoolPush`
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ dispatch_semaphore_t semaphore_;
|
||||
|
||||
说明:直接 `performSelector` 存在警告,可以告诉编译器忽略警告。可以在 Xcode 点开警告,查看详情,复制 `[]` 里面的字符串去忽略警告
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ KVC 之后会触发 KVO。为什么?探究下 `setValueForKey`
|
||||
|
||||
整个流程如下
|
||||
|
||||

|
||||

|
||||
|
||||
```
|
||||
@implementation Person
|
||||
@@ -315,6 +315,6 @@ valueForKey 原理
|
||||
|
||||
- 都没找到则抛出异常
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -957,7 +957,7 @@ c 数组指针 `array()->lists + addedCount` 可以代表其中的位置。
|
||||
|
||||
过程如下
|
||||
|
||||

|
||||

|
||||
|
||||
QA:
|
||||
|
||||
@@ -1716,7 +1716,7 @@ Category 是在运行阶段,才会将数据合并到类信息中。
|
||||
|
||||
查看分类在 Runtime 加载类信息时候的调用原理可以知道,分类中的类方法、对象方法都会被加载原始类的前面去(initialize 是类方法)如下图:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 新建文件模版对应。
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
所以我们新建 Custom 文件夹,将系统 Source 文件夹下面的 Cocoa Touch Class.xctemplate 复制到 Custom 文件夹下。重命名为我们需要的名字,我这里以“Power”为例
|
||||
|
||||

|
||||
@@ -492,28 +491,22 @@ Xcode 文件模版的存放地址:/Applications/Xcode.app/Contents/Developer/P
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
思考:
|
||||
|
||||
- 如何使用
|
||||
|
||||
|
||||
商量好一个标识(“Power”)。比如我新建了单例、控制器、Model、UIView、UITableViewCell、UICollectionViewCell6个模版,都以为 Power 开头。
|
||||
|
||||
|
||||

|
||||
|
||||
- 如何共享
|
||||
|
||||
|
||||
以 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 //将本地的代码块和文件模版同步到云端
|
||||
```
|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
@@ -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)
|
||||
|
||||

|
||||

|
||||
|
||||
这一阶段主要包括 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),这个表用来调度磁盘和内存两者之间的数据交换。
|
||||
|
||||
什么时候发生大量的缺页异常?一个应用程序刚启动的时候。
|
||||

|
||||
|
||||
启动时所需要的代码分布在第一页、第二页、第三页...第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:
|
||||

|
||||
|
||||
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` 源码的时候就发现了身影
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
||||
@@ -257,12 +257,12 @@ sizeof 是运算符。
|
||||
|
||||
`objc_getClass()` 如果传递 instance 实例对象,返回 class 类对象;传递 Class 类对象,返回 meta-class 元类对象;传递 meta-class 元对象,则返回 NSObject(基类)的 meta-class 对象
|
||||
|
||||

|
||||

|
||||
|
||||
instance 的 isa 指向 Class。当调用方法时,通过 instance 的 isa 找到 Class,最后找到对象方法的实现进行调用
|
||||
class 的 isa 指向 meta-class。当调用类方法的时,通过 class 的 isa 找到 meta-class,最后找到类方法进行调用。
|
||||
|
||||

|
||||

|
||||
|
||||
当 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` 方法并调用。
|
||||
|
||||
@@ -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` 用程序员模式打开计算器
|
||||
|
||||

|
||||

|
||||
|
||||
其中,结构体中的数据存放大体是下面的结构:
|
||||
|
||||
@@ -291,7 +343,7 @@ struct class_ro_t {
|
||||
|
||||
具体关系整理如下图
|
||||
|
||||

|
||||

|
||||
|
||||
说明:
|
||||
|
||||
@@ -299,7 +351,7 @@ struct class_ro_t {
|
||||
|
||||
为什么不是二维数组?因为Array 中的子 Array长度不一致,且不能补空
|
||||
|
||||

|
||||

|
||||
|
||||
```c
|
||||
static void remethodizeClass(Class cls)
|
||||
@@ -384,7 +436,7 @@ struct class_ro_t {
|
||||
|
||||
- `class_ro_t` 里面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,包含了类的(原始信息)初始内容
|
||||
|
||||

|
||||

|
||||
|
||||
## Method_t
|
||||
|
||||
@@ -422,7 +474,7 @@ typedef struct objc_selector *SEL;
|
||||
|
||||
iOS 中提供了一个叫做 `@encode` 的指令,可以将具体的类型表示成字符串编码
|
||||
|
||||

|
||||

|
||||
|
||||
```objectivec
|
||||
- (int)calcuate:(int)age heigith:(float)height;
|
||||
@@ -774,7 +826,7 @@ NSLog(@"%s %p", bucket._key, bucket._imp);
|
||||
// personSay 0xbec8
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
原理就是根据类对象结构体找到 cache 结构体,cache 结构体内部的 `_buckets` 是一个方法散列表,查看源代码,根据散列表的哈希寻找策略 `(key & mask)` 找到哈希索引,然后找到方法对象 bucket,其中寻找方法索引的 key 就是 方法 selector。
|
||||
|
||||
@@ -1359,7 +1411,7 @@ for 循环不断查找,找当前类的父类,直到当前类为 nil。
|
||||
|
||||
上面的流程是整个 `objc_msgSend` 的消息发送阶段的整个流程。可以用下图表示
|
||||
|
||||

|
||||

|
||||
|
||||
### 动态方法解析阶段
|
||||
|
||||
@@ -1494,7 +1546,7 @@ SEL_resolveClassMethod, sel);`
|
||||
|
||||
完整流程如下
|
||||
|
||||

|
||||

|
||||
|
||||
上 Demo
|
||||
|
||||
@@ -1634,7 +1686,7 @@ void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
|
||||
|
||||
为什么是 `__forwarding__` 方法。我们可以根据 Xcode 崩溃窥探一二
|
||||
|
||||

|
||||

|
||||
|
||||
```c
|
||||
int __forwarding__(void *frameStackPointer, int isStret) {
|
||||
@@ -1678,7 +1730,7 @@ int __forwarding__(void *frameStackPointer, int isStret) {
|
||||
|
||||
完整流程如下
|
||||
|
||||

|
||||

|
||||
|
||||
上 Demo
|
||||
|
||||
@@ -1899,7 +1951,7 @@ objc_msgSendSuper(arg, sel_registerName("class"))
|
||||
|
||||
我们对 iOS 项目`[super viewDidLoad]` 下符号断点,发现`objc_msgSendSuper2`
|
||||
|
||||

|
||||

|
||||
|
||||
查看 objc4 源代码发现是一段汇编实现。
|
||||
|
||||
@@ -2133,7 +2185,7 @@ void test () {
|
||||
|
||||
方法内的变量存储在栈上,堆向上增长,栈向下增长。
|
||||
|
||||

|
||||

|
||||
|
||||
3.**实例对象的本质就是一个结构体,存储所有成员变量(isa 是一个特殊成员变量,其他的成员变量,这里就是 _name),`sayHi` 方法内部的 self 就是 obj,找成员变量的本质就是找内存地址的过程(此时就是偏移8个字节)**
|
||||
|
||||
@@ -2182,7 +2234,7 @@ objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));
|
||||
|
||||
所以此时的“前一个局部变量” 也就是结构体 `objc_super` 类型的 arg。arg 是一个结构体,结构体第一个成员变量就是 self,所以“前一个局部变量” 也就是 self(ViewController)
|
||||
|
||||

|
||||

|
||||
|
||||
## 应用场景
|
||||
|
||||
@@ -2216,7 +2268,7 @@ Person *p = [Person new];
|
||||
object_setClass(p, [Student class]);
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
3.动态创建类
|
||||
|
||||
@@ -2243,7 +2295,7 @@ void createClass (void) {
|
||||
}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
runtime 中 copy、create 等出来的内存,不使用的时候需要手动释放`objc_disposeClassPair(newClass>)`
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -6,7 +6,7 @@ block 本质上就是一个 oc 对象,也有 isa 指针
|
||||
|
||||
block 是封装了函数调用和函数调用环境的 OC 对象
|
||||
|
||||

|
||||

|
||||
|
||||
```objectivec
|
||||
int age = 27;
|
||||
@@ -247,7 +247,7 @@ NSLog(@"%@", [[block class] superclass]); // NSBlock
|
||||
NSLog(@"%@", [[[block class] superclass] superclass]); // NSObjec
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
代码存放在 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,并修改值。
|
||||
|
||||

|
||||

|
||||
|
||||
`__block` 修饰基本数据类型和对象,对于生成的结构体也不一样。
|
||||
|
||||
@@ -692,7 +692,7 @@ int main(int argc, const char * argv[]) {
|
||||
|
||||
我们将断点设置到 NSLog 这里,打印出自定义结构体 `__main_block_impl_0` 中的 age 。
|
||||
|
||||

|
||||

|
||||
|
||||
```c
|
||||
// 0x0000000105231f70
|
||||
@@ -736,7 +736,7 @@ block 内部对变量的值修改其实就是对 block 内部自定义结构体
|
||||
|
||||
## `__forwarding` 的设计
|
||||
|
||||

|
||||

|
||||
|
||||
当block在栈中时,`__Block_byref_age_0`结构体内的`__forwarding`指针指向结构体自己。
|
||||
|
||||
@@ -750,7 +750,7 @@ block 内部对变量的值修改其实就是对 block 内部自定义结构体
|
||||
|
||||
被 `__block ` 修饰符修饰的对象在内存中如下
|
||||
|
||||

|
||||

|
||||
|
||||
```c
|
||||
|
||||
@@ -871,7 +871,7 @@ p.block();
|
||||
|
||||
`__unsafe_retained` 因为不安全所以不推荐,`__block` 因为使用繁琐,且必须等到调用 block 才会释放内存,所以不推荐。ARC 下最佳用 `__weak`
|
||||
|
||||

|
||||

|
||||
|
||||
### MRC 下
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ MH_DSYM:存储着二进制文件符号。`.dSYM/Contents/Resource/DWARF/xx`
|
||||
|
||||
Xcode 中也可以查看 Mach-O 文件类型
|
||||
|
||||

|
||||

|
||||
|
||||
### Universal Binary
|
||||
|
||||
@@ -137,7 +137,7 @@ Xcode 中也可以查看 Mach-O 文件类型
|
||||
|
||||
## Mach-O 结构
|
||||
|
||||

|
||||

|
||||
|
||||
一个 Mach-O 文件包含3块
|
||||
|
||||
@@ -149,13 +149,13 @@ Xcode 中也可以查看 Mach-O 文件类型
|
||||
|
||||
可以用 [GitHub - fangshufeng/MachOView: 分析Macho必备工具](https://github.com/fangshufeng/MachOView) 和系统自带的 atool 查看 Mach-O 信息
|
||||
|
||||

|
||||

|
||||
|
||||
用 MachOView 查看 DDD Mach-O 文件
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
可以看到在 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 的内存分布
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
我们会发现根据 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 引入。
|
||||
|
||||

|
||||

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

|
||||
|
||||
|
||||
|
||||
## 状态管理
|
||||
|
||||
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 文件夹,创建全部大写的变量,然后导出。
|
||||
<details>
|
||||
|
||||
<details>
|
||||
<summary>展开示例代码</summary>
|
||||
|
||||
```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'
|
||||
```
|
||||
</details>
|
||||
|
||||
```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'
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
- Redux 使用的时候全局工程会创建很多 action,所以和上面的思想一样,需要集中统一处理,符合“收口原则”、“单一原则”。做法就是在 `src/store` 目录下创建一个 actionCreators.js 文件。然后在里面引入 actionType.js,根据业务导出几个产生 action 的函数。
|
||||
<details>
|
||||
<summary>展开示例代码</summary>
|
||||
|
||||
```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
|
||||
});
|
||||
```
|
||||
</details>
|
||||
- store 发现 action 提交的数据是函数类型的时候,会自动执行函数
|
||||
|
||||
<details>
|
||||
<summary>展开示例代码</summary>
|
||||
|
||||
```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
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
- 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 里面常用的一个中间件。中间件?针对谁和
|
||||

|
||||
|
||||
- 异步函数不应该放在组件的生命周期函数里面。复杂的业务逻辑和异步函数适合拆分。目前主流的解决方案有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 <h2>hello, {this.props.name}</h2>;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
###
|
||||
|
||||
## 开发tips
|
||||
|
||||
- 不要直接操作 state,只能通过 setState 操作数据;props 是只读的
|
||||
|
||||
- setState 的时候如果依赖之前的 state 数据,那么 setState 第一个参数可以更改为函数方式,这个函数有2个参数
|
||||
|
||||
```javascript
|
||||
setState((state, props) => ({ count: state.count + props.increment }));
|
||||
```
|
||||
|
||||
- 路由设置的时候,我们经常会设置路径。每个路径匹配到具体的页面资源会呈现出来。但是在一开始的时候会遇到疑问,为什么我在浏览器里面输入了 “/home”。但是出来的内容还是 “/” 皮配到的页面,后来知道了还可以设置 **exact** 属性可以精确控制。
|
||||
|
||||
```javascript
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
@@ -169,10 +163,11 @@ class Welcome extends React.Component {
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
```
|
||||
|
||||
- 在 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 <div>props.name</div>;
|
||||
@@ -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
|
||||
<input type="text" maxlength="11" autocomplete="off" class="form-control input-lg input-flat input-flat-user" placeholder="请输入手机号码" name="resetmobile" v-validate="'required|resetmobile'" v-model="resetmobile">
|
||||
```
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
<input placeholder="things to do..."
|
||||
style={ { width: 300, marginRight: 20 } }
|
||||
value={props.inputValue}
|
||||
@@ -231,6 +229,7 @@ class Welcome extends React.Component {
|
||||
this.setState(store.getState());
|
||||
}
|
||||
```
|
||||
|
||||
可以看出来,Vue 通过简单的一个内置命令 `v-model` 将 model 和 view 双向绑定了起来,数据是双向的。model 改变 view 自动刷新;用户在 input 写了文字,model 的值也自动改变。React 先设置一个 input 组件,监听用户的输入事件(onChange),然后在 onChange 里面拿到当前输入框里面的数据,然后你可以直接 setState 去操作数据,setState 后才会触发 render 函数,dom 才会跟着更新。
|
||||
|
||||
- React 比 Vue 更加适合构建大型项目。
|
||||
@@ -238,5 +237,4 @@ class Welcome extends React.Component {
|
||||
|
||||
- Vue 设计思想:How easy it can be。React:How corrct it can be 和 all in js(css写法也在用 js 控制,比如 styled-component)
|
||||
|
||||
|
||||
在 React、React Native、Vue、Weex、Flutter 等声明式开发思想的框架下,UI = F(state)。一个状态唯一对应一个 UI(但一个 UI 不一定对应一个 state),关心 state 即可
|
||||
@@ -1,16 +1,203 @@
|
||||
# HTTP 请求头 Range 信息
|
||||
# HTTP/HTTPS 细节探索
|
||||
|
||||
## TCP/UDP
|
||||
|
||||
## HTTP 缓存控制
|
||||
|
||||
缓存(Cache)是计算机领域里的一个重要概念,是优化系统性能的利器。
|
||||
|
||||
由于链路漫长,网络时延不可控,浏览器使用 HTTP 获取资源的成本较高。所以,非常有必要把“来之不易”的数据缓存起来,下次再请求的时候尽可能地复用。这样,就可以避免多次请求 - 应答的通信成本,节约网络带宽,也可以加快响应速度。
|
||||
|
||||
试想一下,如果有几十 K 甚至几十 M 的数据,不是从网络而是从本地磁盘获取,那将是多么大的一笔节省,免去多少等待的时间。
|
||||
|
||||
实际上,HTTP 传输的每一个环节基本上都会有缓存,非常复杂。
|
||||
|
||||
基于“请求 - 应答”模式的特点,可以大致分为客户端缓存和服务器端缓存。
|
||||
|
||||
### 服务器的缓存控制
|
||||
|
||||
一个缓存控制的标准流程是:
|
||||
|
||||
- 浏览器第一次发送请求,向服务器获取资源;
|
||||
|
||||
- 服务器响应请求,返回资源,同时标记资源的有效期;服务器标记资源有效期使用的头字段是 `Cache-Control`,里面的值 `max-age=30` 就是资源的有效时间,相当于告诉浏览器,“这个页面只能缓存 30 秒,之后就算是过期,不能用”
|
||||
|
||||
- 浏览器缓存资源,等待下次复用
|
||||
|
||||
- 后续浏览器的每次请求都会携带缓存相关的头信息(比如 if-not-match、if-last-modified等)
|
||||
|
||||
- 浏览器会根据服务端的资源情况,判断客户端的缓存有没有更新或者变化,来继续在响应头中将缓存信息返回,如果没有过期则返回类似 “304 Not Modified” 之类的信息。
|
||||
|
||||
QA: 浏览器直接缓存数据就好了,为什么需要加有效期?
|
||||
|
||||
服务端上的资源可能会变,比如网关层生成的商品 DB,客户端缓存后每次 App 启动都需要去向服务端问有没有新的数据。可能服务端晚上10点定时生成新的商品数据。所以需要加缓存有效期。
|
||||
|
||||
Cache-Control 字段里的 max-age 指的是**生存时间**。时间的起点是响应报文的创建时间(Date 字段,也就是离开服务器的时刻),而不是客户端收到报文的时刻,也就是说包含了在链路上传输过程中的所有节点停留时间。比如 “max-age=5”,也就是缓存有效期为5s,恰好网络比较糟糕,等客户端收到报文已经过了4s,那么缓存在客户端1s有效,之后就失效了。
|
||||
|
||||
Cache-Control 中比较常用的就是 max-age,此外还有几个
|
||||
|
||||
- no-store:不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面
|
||||
|
||||
- no-cache:它的字面含义容易与 no-store 搞混,实际的意思并不是不允许缓存,而是可以缓存,但在使用之前必须要去服务器验证是否过期,是否有最新的版本
|
||||
|
||||
- must-revalidate:又是一个和 no-cache 相似的词,它的意思是如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。
|
||||
|
||||
- public:可以被所有用户缓存(多用户共享),包括终端和 CDN 等中间代理服务
|
||||
|
||||
- private:只能被终端浏览器缓存(而且是私有缓存),不允许中继缓存服务器进行缓存
|
||||
|
||||
### 客户端的缓存控制
|
||||
|
||||
有没有注意到这样一个现象:每次浏览器强制刷新,页面上的内容可能会变。之前返回的数据明明说缓存3600s。为什么?
|
||||
|
||||
其实不止服务器可以设置 `Cache-Control` ,浏览器也可以设置。也就是说:请求 - 应答的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略。
|
||||
|
||||
点击刷新,浏览器会在请求头加“Cache-Control:max-age=0”。代表着浏览器告诉服务器,我需要一个最新的数据。
|
||||
|
||||
点击强制刷新,浏览器会在请求头将 `If-Modified-Since`、`If-None-Match` 清空。所以服务器会返回最新的数据(当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的相匹配的时候,服务器端才会返回所请求的资源,响应码为 200)
|
||||
|
||||
将网页点击前进、后退会发现“ Status Code:200 from disk cache”
|
||||
|
||||

|
||||
|
||||
### 条件请求
|
||||
|
||||
早期浏览器可以用2个连续请求完整缓存验证:第一个 HEAD,获取资源的修改时间等信息,然后与本地缓存数据相比较,如果没有修改则使用缓存,节省网络流量,否则就发第二个 GET 请求,获取最新的资源数据。
|
||||
|
||||
但是2个请求网络成本太高,所以 HTTP 协议就定义了一系列 `If` 开头的“条件请求”字段,专门用来检查验证资源是否过期,把两个请求才能完成的工作合并在一个请求里做。而且,验证的责任也交给服务器,浏览器只需“坐享其成”。
|
||||
|
||||
条件请求共5个头字段,常用的是 `If-Modified-Since` + `Last-modified` 和 `If-None-Match` + `ETag`这两个(需要搭配使用)。需要在第一次响应报文预先提供 `Last-modified` 和 `ETag`,然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。如果服务端资源没有更新,则返回 “304 Not Modified”,表示缓存有效, 浏览器只需要更新缓存日期便可继续使用本地缓存
|
||||
|
||||

|
||||
|
||||
Last Modified 代表资源的最后修改时间
|
||||
|
||||
ETag 即 Entity Tag,代表资源的唯一标识。主要用来解决修改时间无法准确区分文件变化的问题。使用 ETag 就可以精确地识别资源的变动情况,让浏览器能够更有效地利用缓存。
|
||||
|
||||
- 一个文件在一秒内修改了多次,但因为修改时间是秒级,所以这一秒内的新版本无法区分
|
||||
|
||||
- 一个文件定期更新,但有时会是同样的内容,实际上没有变化,用修改时间就会误以为发生了变化,传送给浏览器就会浪费带宽
|
||||
|
||||
- 强 ETag 要求资源在字节级别必须完全相符,弱 ETag 在值前有个 `W/` 标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)
|
||||
|
||||
## HTTP 代理服务
|
||||
|
||||
引入 HTTP 代理后,原来简单的双方通信就变复杂了一些,加入了一个或者多个中间人,但整体上来看,还是一个有顺序关系的链条,而且链条里相邻的两个角色仍然是简单的一对一通信,不会出现越级的情况。代理在 HTTP 协议里对它并没有什么特别的描述,它就是在客户端和服务器原本的通信链路中插入的一个中间环节,也是一台服务器,但提供的是“代理服务”。
|
||||
|
||||
所谓的“代理服务”就是指服务本身不生产内容,而是处于中间位置转发上下游的请求和响应,具有双重身份:面向下游的用户时,表现为服务器,代表源服务器响应客户端的请求;而面向上游的源服务器时,又表现为客户端,代表客户端发送请求。
|
||||
|
||||
代理最基本的一个功能是负载均衡。因为在面向客户端时屏蔽了源服务器,客户端看到的只是代理服务器,源服务器究竟有多少台、是哪些 IP 地址都不知道。于是代理服务器就可以掌握请求分发的“大权”,决定由后面的哪台服务器来响应请求。
|
||||
|
||||
代理中著名的负载均衡算法,比如轮询、一致性哈希等等,这些算法的目标都是尽量把外部的流量合理地分散到多台源服务器,提高系统的整体资源利用率和性能。
|
||||
|
||||
在负载均衡的同时,代理服务还可以执行更多的功能,比如:
|
||||
|
||||
- 健康检查:使用“心跳”等机制监控后端服务器,发现有故障就及时“踢出”集群,保证服务高可用
|
||||
|
||||
- 安全防护:保护被代理的后端服务器,限制 IP 地址或流量,抵御网络攻击和过载
|
||||
|
||||
- 加密卸载:对外网使用 SSL/TLS 加密通信认证,而在安全的内网不加密,消除加解密成本
|
||||
|
||||
- 数据过滤:拦截上下行的数据,任意指定策略修改请求或者响应
|
||||
|
||||
- 内容缓存:暂存、复用服务器响应
|
||||
|
||||
### 代理头字段
|
||||
|
||||
代理体现在头信息上就是字段 `Via`,是一个通用字段,请求头或响应头里都可以出现。每当报文经过一个代理节点,代理服务器就会把自身的信息追加到字段的末尾,就像是经手人盖了一个章。如果通信链路中有很多中间代理,就会在 Via 里形成一个链表,这样就可以知道报文究竟走过了多少个环节才到达了目的地。
|
||||
|
||||

|
||||
|
||||
`X-Forwarded-For` 的字面意思是“为谁而转发”,形式上和“Via”差不多,也是每经过一个代理节点就会在字段里追加一个信息。但“Via”追加的是代理主机名(或者域名),而“X-Forwarded-For”追加的是请求方的 IP 地址。所以,在字段里最左边的 IP 地址就是客户端的地址
|
||||
|
||||
`X-Real-IP` 是另一种获取客户端真实 IP 的手段,它的作用很简单,就是记录客户端 IP 地址,没有中间的代理信息,相当于是“X-Forwarded-For”的简化版。如果客户端和源服务器之间只有一个代理,那么这两个字段的值就是相同的。
|
||||
|
||||
### 代理协议
|
||||
|
||||
有 v1 和 v2 两个版本,v1 和 HTTP 差不多,也是明文,而 v2 是二进制格式。
|
||||
|
||||
v1 规定,它在 HTTP 报文前增加了一行 ASCII 码文本,相当于又多了一个头。这一行文本其实非常简单,开头必须是“PROXY”五个大写字母,然后是“TCP4”或者“TCP6”,表示客户端的 IP 地址类型,再后面是请求方地址、应答方地址、请求方端口号、应答方端口号,最后用一个回车换行(\r\n)结束。
|
||||
|
||||
```shell
|
||||
PROXY TCP4 1.1.1.1 2.2.2.2 55555 80\r\n
|
||||
GET / HTTP/1.1\r\n
|
||||
Host: www.xxx.com\r\n
|
||||
\r\n
|
||||
```
|
||||
|
||||
总结:
|
||||
|
||||
- HTTP 代理就是客户端和服务器通信链路中的一个中间环节,为两端提供“代理服务”
|
||||
|
||||
- 代理处于中间层,为 HTTP 处理增加了更多的灵活性,可以实现负载均衡、安全防护、数据过滤等功能
|
||||
|
||||
- 代理服务器需要使用字段“Via”标记自己的身份,多个代理会形成一个列表
|
||||
|
||||
- 如果想要知道客户端的真实 IP 地址,可以使用字段“X-Forwarded-For”和“X-Real-IP”
|
||||
|
||||
- 专门的“代理协议”可以在不改动原始报文的情况下传递客户端的真实 IP
|
||||
|
||||
## 性能优化之 HTTP 缓存代理
|
||||
|
||||
客户端(浏览器)上的缓存控制,它能够减少响应时间、节约带宽,提升客户端的用户体验。
|
||||
|
||||
HTTP 传输链路上,不只是客户端有缓存,服务器上的缓存也是非常有价值的,可以让请求不必走完整个后续处理流程,"就近"获得响应结果。特别是对于那些“读多写少”的数据,例如突发热点新闻、爆款商品的详情页,一秒钟内可能有成千上万次的请求。即使仅仅缓存数秒钟,也能够把巨大的访问流量挡在外面,让 RPS(request per second)降低好几个数量级,减轻应用服务器的并发压力,对性能的改善是非常显著的。
|
||||
|
||||

|
||||
|
||||
代理服务器没有缓存的时候:代理服务器每次直接转发来自客户端的报文给服务端,转发服务端的报文给客户端,中间不会存储任何数据,只有基础的中转功能。
|
||||
|
||||
有了缓存后:
|
||||
|
||||
- 把报文转发给客户端
|
||||
|
||||
- 把报文存储到缓存中
|
||||
|
||||
这样下次有相同的请求,代理服务器就可以直接返回 304 或者缓存数据,不必再从源服务器那里获取一次数据,降低了客户端等待时间、节约了源服务器的带宽
|
||||
|
||||
### 源服务器的缓存控制
|
||||
|
||||
客户端的缓存只是用户自己使用,而代理的缓存可能会为非常多的客户端提供服务。所以,需要对它的缓存再多一些限制条件。
|
||||
|
||||
额外增加2个新属性:
|
||||
|
||||
- private:表示缓存只能在客户端保存,是用户“私有”的,不能放在代理上与别人共享。比如你访问购物网站,返回的响应报文里用 "Set-Cookie" 添加了 token,这就属于私人数据,不能存在代理上
|
||||
|
||||
- public:表示缓存完全开放,谁都可以存,谁都可以用
|
||||
|
||||
关于缓存失效后的验证有一些新的属性:
|
||||
|
||||
- proxy-revalidate:只要求代理的缓存过期后必须验证,客户端不必回源,只验证到代理
|
||||
|
||||
- s-maxage:s即 share 的意思,表示限定在代理上能够存多久,而客户端仍然使用 `max-age`
|
||||
|
||||
- no-transform:代理有时候会对缓存下来的数据做一些优化,比如把图片生成 png、webp 等几种格式,方便今后的请求处理,`no-transform` 则禁止这样做,不允许对资源做任何处理
|
||||
|
||||
下图是完整的服务器端缓存控制策略,可以同时控制客户端和代理
|
||||
|
||||

|
||||
|
||||
### 客户端的缓存控制
|
||||
|
||||

|
||||
|
||||
- max-stale:如果代理上的缓存过期了也可以接受,但不能过期太多,超过 x 秒也会不要
|
||||
|
||||
- min-fresh:缓存必须有效,而且必须在 x 秒后依然有效
|
||||
|
||||
## HTTP 请求头 Range 信息
|
||||
|
||||
请求资源的部分内容,单位是 byte(字节),从0开始。
|
||||
如果请求头携带了 Range 信息,也就是分批下载,这时候服务器会返回 206 Partial Content 的状态码及说明。
|
||||
|
||||
如果服务器不支持分批下载,那么会返回整个资源的大小以及状态码为200。
|
||||
|
||||
|
||||
## Range 请求头
|
||||
### Range 请求头
|
||||
|
||||
`Range: bytes=start-end`
|
||||
|
||||
例如:
|
||||
|
||||
```
|
||||
Range: bytes=10- //:从第10个字节开始到最后一个字节的数据
|
||||
Range:bytes=20-39 //:从第20个字节到第39个字节之间的数据
|
||||
@@ -18,31 +205,776 @@ Range:bytes=20-39 //:从第20个字节到第39个字节之间的数据
|
||||
|
||||
注意:整个表示 [start, end] 是前闭后闭的,也就是包含请求头的 start 和 end。所以下次请求应该是 [end+1, nextEnd]。
|
||||
|
||||
|
||||
## Content-Range 响应头
|
||||
### Content-Range 响应头
|
||||
|
||||
`Content-Range:bytes 0-10/3000`
|
||||
表示服务器返回了前(0-10)个字节的数据,总共3000字节的数据。
|
||||
|
||||
### Content-Type 数据类型
|
||||
|
||||
## Content-Type 数据类型
|
||||
`Content-Type:image/png` 表示资源类型是 png 格式的图片
|
||||
|
||||
## Content-Length 资源的长度
|
||||
### Content-Length 资源的长度
|
||||
|
||||
`Content-Length:11` 表示服务器响应了11个字节的数据
|
||||
|
||||
## Last-Modified
|
||||
### Last-Modified
|
||||
|
||||
`Last-Modified:Tue, 30 Jun 2018 03:12:48 GMT` 表示资源最近被修改的时间,如果分批下载的时候发现 Last-Modified 被修改了,那么需要重新下载
|
||||
|
||||
## ETag
|
||||
### ETag
|
||||
|
||||
`ETag: W/"3103-1435633968000"` 表示资源版本的标示符。通常是消息摘要(类似MD5)。分段下载时需要注意,或者缓存控制也需要注意。如果是分布式缓存系统,需要确保每台计算机的 ETag 计算规则的一致性,缓存的过期需要结合 ETag 和 Last-Modified 共同决定。
|
||||
|
||||
## 分段下载
|
||||
### 分段下载
|
||||
|
||||
利用 HTTP 的头信息的上述几个特点,我们可以充分利用多线程的能力。
|
||||
|
||||
- 先发送一个 HEAD 方法的请求,知道总文件大小(Content-Length 就是总字节大小)
|
||||
- 多线程下载(线程1:Range:bytes=0-100,线程2:Range:bytes=100-200,...)
|
||||
- 多线程下载(线程1:Range:bytes=0-100,线程2:Range:bytes=100-200,...)
|
||||
|
||||
## HTTPS 安全
|
||||
|
||||
HTTP 的缺点之一就是:明文 + 不安全。为此诞生了 HTTPS 协议。
|
||||
|
||||
比如代理服务,它作为 HTTP 通信的中间人,在数据上下行的时候可以添加或删除部分头字段,也可以使用黑白名单过滤 body 里的关键字,甚至直接发送虚假的请求、响应,而浏览器和源服务器都没有办法判断报文的真伪。这是很糟糕的
|
||||
|
||||
如何定义安全:
|
||||
|
||||
- 机密性:指对数据保密,只能由可信的人访问,对其他人是不可见的“秘密”,简单来说就是不能让不相关的人看到不该看的东西
|
||||
|
||||
- 完整性:也叫一致性,数据在传输过程中没有被篡改,不多也不少,“完完整整”地保持着原状
|
||||
|
||||
- 身份认证:指确认对方的真实身份,也就是“证明你真的是你”,保证消息只能发送给可信的人
|
||||
|
||||
- 不可否认:不能否认已经发生过的行为
|
||||
|
||||
HTTPS 其实是一个“非常简单”的协议,RFC 只有短短的 7 页 描述,里面规定了新的协议名`https`,默认端口号 `443`,至于其他的什么请求 - 应答模式、报文结构、请求方法、URI、头字段、连接管理等等都完全沿用 HTTP,没有任何新的东西。也就是说,除了协议名 `http` 和端口号 `80` 这两点不同,HTTPS 协议在语法、语义上和 HTTP 完全一样,有窃电也一样(但缺点不包含明文、不安全)
|
||||
|
||||
HTTPS 的核心在于 `s` ,也就是把 HTTP 下层传输协议由 TCP/IP 换为了 SSL/TLS,从 "HTTP Over TCP/IP" 变为 "HTTP Over SSL/TLS"。让 HTTP 运行在了安全的 SSL/TLS 协议上,收发报文不再使用 Socket API,而是调用专门的安全接口。
|
||||
|
||||

|
||||
|
||||
SSL(Secure Sockets Layer),安全套接字层,在 OSI 模型中第五层会话层,由网景公司在1994年发明,有 V2、V3 版本,V1 因为有严重缺陷从未公开过。SSL 发展到 V3 被证明是一个非常好的安全通信协议,于是互联网工程组 IEFT 在1999年更名为 TLS(Transport Layer Security)传输层安全,正式标准化,版本号从1.0计数,所以 TLS1.0 也就是 SSL V3.1/
|
||||
|
||||
TLS 由记录协议、握手协议、警告协议、变更密码规范协议、扩展协议等几个子协议组成,综合使用了对称加密、非对称加密、身份认证等许多密码学前沿技术
|
||||
|
||||
客户端和服务器在使用 TLS 建立链接时需要选择一组恰当的加密算法来实现安全通信,这组算法叫“密码套件”(Cipher Suite,加密套件)
|
||||
|
||||
TLS 的密码套件命名规范,格式固定:密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法。
|
||||
|
||||
`ECDHE-RSA-AES256-GCM-SHA384` 代表握手时使用 ECDHE 算法作为密钥交换算法,用 RSA 签名和身份认证,握手后通信使用 AES 对称加密方法,密钥长度256位,分组模式为 GCM,摘要算法 SHA38 用于消息认证和产生随机数。
|
||||
|
||||
TLS 中有很多对称加密算法可以选择,比如 RC4、DES、3DES、AES、ChaCha20 等,但前三种算法都被认为是不安全的,通常都禁止使用,目前常用的只有 AES 和 ChaCha20
|
||||
|
||||
- AES:Advanced Encryption Standard,高级加密标准”,密钥长度可以是 128、192 或 256。是 DES 算法的替代者,安全强度很高,性能也很好,而且有的硬件还会做特殊优化,所以非常流行,是应用最广泛的对称加密算法
|
||||
|
||||
- ChaCha20:Google 设计的另一种加密算法,密钥长度固定为 256 位,纯软件运行性能要超过 AES,曾经在移动客户端上比较流行,但 ARMv8 之后也加入了 AES 硬件优化,所以现在不再具有明显的优势,但仍然算得上是一个不错的算法
|
||||
|
||||
### 加密分组模式
|
||||
|
||||
对称算法还有一个“分组模式”的概念,它可以让算法用固定长度的密钥加密任意长度的明文。明文的长度不固定,而密钥一次只能处理特定长度的一块数据,这就需要进行迭代,以便将一段很长的明文全部加密,而迭代的方法就是分组模式。
|
||||
|
||||
比如 `AES128-GCM`,意思是密钥长度为 128 位的 AES 算法,使用的分组模式是 GCM;`ChaCha20-Poly1305` 的意思是 ChaCha20 算法,使用的分组模式是 Poly1305。
|
||||
|
||||
### 对称加密、非对称加密
|
||||
|
||||
对称加密实现了机密性,但有一个很大的问题:如何将密钥安全地传输给对方,也就是"密钥交换"。诞生了非对称加密。拥有2个密钥:公钥、私钥。
|
||||
|
||||
公钥可以公开给任何人使用,私钥必须秘密保存。
|
||||
|
||||
私钥、公钥都是“单向”的。也就是公钥加密后只能私钥解密,私钥加密后只能公钥解密。
|
||||
|
||||
非对称加密可以解决“密钥交换”的问题。网站秘密保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。非对称加密也叫“公钥加密算法”的原理。
|
||||
|
||||
RSA 是非对称加密中最著名的一个,可以说是非对称加密的代名词。“由已知加密密钥推导出解密密钥在计算上是不可行的”密码体制。
|
||||
|
||||
RSA 公开密钥密码体制的原理是:根据数论,寻求两个大素数比较简单,而将它们的乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。
|
||||
|
||||
10年前 RSA 密钥推荐长度为1024位,但随着计算机运算能力的提高,1024位已经不够安全了,目前需要2048位。
|
||||
|
||||
ECC(Elliptic Curve Cryptography) 是非对称加密里的“后起之秀”,它基于“椭圆曲线离散对数”的数学难题,使用特定的曲线方程和基点生成公钥和私钥,子算法 ECDHE 用于密钥交换,ECDSA 用于数字签名。
|
||||
|
||||
### 混合加密
|
||||
|
||||
非对称的优点也是缺点:因为非对称加密密钥“密钥交换”的问题,但是是基于复杂的数学难题实现的,所以运算速度比较慢。下面是网上看到的测试速度
|
||||
|
||||
```shell
|
||||
aes_128_cbc enc/dec 1000 times : 0.97ms, 13.11MB/s
|
||||
rsa_1024 enc/dec 1000 times : 138.59ms, 93.80KB/s
|
||||
rsa_1024/aes ratio = 143.17
|
||||
rsa_2048 enc/dec 1000 times : 840.35ms, 15.47KB/s
|
||||
rsa_2048/aes ratio = 868.13
|
||||
```
|
||||
|
||||
TLS 将2者结合起来使用混合加密手段(取长补短)兼顾安全和性能。
|
||||
|
||||
- 通信刚开始使用非对称加密算法,比如 RSA、ECDHE,解决密钥交换的问题
|
||||
|
||||
- 然后用随机数产生对称算法使用的“会话密钥”,再用公钥加密。因为会话密钥很短,通常有16/32字节,所以慢一点无所谓。
|
||||
|
||||
- 对方拿到密文后用私钥解密,取出会话密钥。这样,双方就实现了对称密钥的安全交换,后续就不再使用非对称加密,全都使用对称加密。
|
||||
|
||||
总结:
|
||||
|
||||
- 加密算法的核心思想是“把一个小秘密(密钥)转化为一个大秘密(密文消息)”,守住了小秘密,也就守住了大秘密
|
||||
|
||||
- 对称加密只使用一个密钥,运算速度快,密钥必须保密,无法做到安全的密钥交换,常用的有 AES 和 ChaCha20
|
||||
|
||||
- 非对称加密使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,解决了密钥交换问题但速度慢,常用的有 RSA 和 ECC
|
||||
|
||||
- 把对称加密和非对称加密结合起来就得到了“又好又快”的混合加密,也就是 TLS 里使用的加密方式
|
||||
|
||||
### 数字签名与证书
|
||||
|
||||
虽然采用混合加密,黑客因为没有密钥,就无法破解得到秘文。但是中间人可以伪造身份发布公钥,客户端拿到假的公要钥,混合加密就失效了,以为是和某个网站进行通信,其实是黑客,这样手机号等敏感信息都泄漏了。
|
||||
|
||||
所以,在机密性的基础上还需要添加完整性、身份认证等特性,才可以实现真正的安全。
|
||||
|
||||
#### 摘要算法
|
||||
|
||||
Digest Algorithm 实现完整性校验的手段就是**摘要算法**,也就是常说的散列函数、哈希函数。
|
||||
|
||||
摘要算法近似地理解成一种特殊的压缩算法,可以任意长度的数据“压缩”成固定长度且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。
|
||||
|
||||
换一个角度,也可以把摘要算法理解成特殊的“单向”加密算法,它只有算法,没有密钥,加密后的数据无法解密,不能从摘要逆推出原文。
|
||||
|
||||
摘要算法要对输入具有“单向性”和“雪崩效应”。对于输入数据的些许改变就会产生很大的变化,所以被用来 TLS 生成伪随机数(PRF,pseudo random function)
|
||||
|
||||
大家都听过 md5、SHA-1,他们是常见的摘要算法,但目前已经不够安全,在 TLS 中被禁止使用了。TLS 推荐 SHA-2。
|
||||
|
||||
SHA-2 其实是一系列摘要算法的统称,包含6中,常用的是 SHA224、SHA256、SHA384,分别生成28字节、32字节、48字节的摘要。
|
||||
|
||||
#### 完整性
|
||||
|
||||
摘要算法的作用就是保证“数字摘要”和原文是完全等价的。所以只需要在原文后附加上摘要,就能保证数据的完整性(还不是 TLS 的最终实现,继续向下分析)
|
||||
|
||||
比如原始信息是“我叫杭城小刘,喜欢乒乓球”,然后根据 SHA-2 计算出摘要 xxx,发送的时候就将“我叫杭城小刘,喜欢乒乓球”和摘要一起发送给服务器,服务器收到消息,然后重新计算一下摘要,将传输过来的摘要和自己计算的摘要进行比较,如果一致,则说明消息是可信的,没有被中间人修改。
|
||||
|
||||
够了吗?还不够,**摘要算法不具备“机密性”**,明文传输的时候黑客可以将消息修改后,重新生成一份摘要,一起发送给服务器,此时服务器就以为还是没有中间人攻击。
|
||||
|
||||
知道问题症结所在,也比较好解,真正的完整性必须建立在“机密性”基础上。使用混合加密系统中用会话密钥加密消息和摘要,这样中间人无法得知明文。这个过程叫做“哈希消息认证码”(HMAC)
|
||||
|
||||

|
||||
|
||||
#### 数字签名
|
||||
|
||||
加密算法 + 摘要算法,通信过程已经比较安全了,但是还是存在不足,比如通信的发送方和接收方。黑客可以伪装成客户端向服务器请求隐私数据,也可以伪装为服务器,获取你的隐私数据。
|
||||
|
||||
如何标记你就是你?可以利用非对称加密中的私钥。使用私钥,再加上摘要算法,就可以实现“数字签名”,同时保证“身份认证”和“不可否认”。
|
||||
|
||||
数字签名其实就是把公钥、私钥反过来使用。发送报文时,发送方用一个哈希函数从报文文本中生成报文摘要,然后用发送方的私钥对这个摘要进行加密,这个加密后的摘要将作为报文的数字签名和报文一起发送给接收方。接收方首先使用与发送方一样的哈希函数从接收到的原始报文中计算出报文摘要,接着再用公钥对报文附加的数字签名进行解密,如果2个摘要相同,则接收方认为该报文是发送方发送的。
|
||||
|
||||
非对称加密效率太低,所以私钥只加密原报文的摘要,这样运算量小,速度也快。得到的数字签名也很小,方便传输和保管。
|
||||
|
||||

|
||||
|
||||
这个过程被叫做“签名”、“验签”。
|
||||
|
||||
#### 数字证书和 CA
|
||||
|
||||
目前为止已经实现了安全的四大特性:机密性、完整性、身份认证、不可否认。够了吗?还不够,还存在“公钥信任”的问题。因为任何人都可以发布公钥,目前还缺少防止黑客伪造公钥的手段。
|
||||
|
||||
可能你会想到利用混合加密去实现?一环套一环,存在“鸡生蛋还是蛋生鸡“的问题。逻辑上死锁。
|
||||
|
||||
iOS 打破循环引用就可以使用 NSProxy 类的解决方案,这里也一样,引入一个具有权威性的第三方机构,也就是 CA(Certificate Authority)证书认证机构。来负责给各个公钥签名。
|
||||
|
||||
CA 对公钥的签名认证也是有格式的,不只是把公钥绑定在持有者身份上就完事了,还要包含序列号、用途、颁发者、有效时间等等,把这些打成一个包再签名,完整地证明公钥关联的各种信息,形成“数字证书”(Certificate)。知名的 CA 全世界就那么几家,比如 DigiCert、VeriSign、Entrust、Let’s Encrypt 等,它们签发的证书分 DV、OV、EV 三种,区别在于可信程度。DV 是最低的,只是域名级别的可信,背后是谁不知道。EV 是最高的,经过了法律和审计的严格核查,可以证明网站拥有者的身份(在浏览器地址栏会显示出公司的名字,例如 Apple、GitHub 的网站)
|
||||
|
||||
CA 如何自证?
|
||||
|
||||
信任链。小一点的 CA 可以让大 CA 签名认证,但链条的最后,也就是 Root CA,就只能自己证明自己了,这个就叫“自签名证书”(Self-Signed Certificate)或者“根证书”(Root Certificate)。你必须相信,否则整个证书信任链就走不下去了。
|
||||
|
||||

|
||||
|
||||
浏览器内置各大 CA 的根证书,上网的时候只要服务器发过来它的证书,就可以验证证书里的签名,顺着证书链(Certificate Chain)一层层地验证,直到找到根证书,就能够确定证书是可信的,从而里面的公钥也是可信的。
|
||||
|
||||
#### 证书体系的弱点
|
||||
|
||||
证书体系(PKI,Public Key Infrastructure)虽然是目前整个网络世界的安全基础设施,但绝对的安全是不存在的,它也有弱点,还是关键的“信任”二字。
|
||||
|
||||
- 如果 CA 失误或者被欺骗,签发了错误的证书,虽然证书是真的,可它代表的网站却是假的。解决方案:开发出了 CRL(证书吊销列表,Certificate revocation list)和 OCSP在线证书状态协议,Online Certificate Status Protocol),及时废止有问题的证书
|
||||
|
||||
- 更危险的是,CA 被黑客攻陷,或者 CA 有恶意,因为它(即根证书)是信任的源头,整个信任链里的所有证书也就都不可信了
|
||||
|
||||
### TLS 1.2 连接过程
|
||||
|
||||
#### HTTPS 建立连接
|
||||
|
||||
在地址栏输入了以 HTTPS 开头的 URI,会发生什么事情?
|
||||
|
||||
浏览器首先要从 URI 里提取出协议名和域名。因为协议名是“https”,所以浏览器就知道了端口号是默认的 443,它再用 DNS 解析域名,得到目标的 IP 地址,然后就可以使用三次握手与网站建立 TCP 连接了。在 HTTP 协议里,建立连接后,浏览器会立即发送请求报文。但现在是 HTTPS 协议,它需要再用另外一个“握手”过程,在 TCP 上建立安全连接,之后才是收发 HTTP 报文。
|
||||
|
||||
#### TLS 协议的组成
|
||||
|
||||
TLS 包含多个子协议,每个协议都有各自的职责,比较常用的协议有:
|
||||
|
||||
- 记录协议:Record Protocol,规定了 TLS 收发数据的基本单位:记录(record)。它有点像是 TCP 里的 segment,所有的其他子协议都需要通过记录协议发出。但多个记录数据可以在一个 TCP 包里一次性发出,也并不需要像 TCP 那样返回 ACK
|
||||
|
||||
- 警报协议:Alert Protocol,的职责是向对方发出警报信息,有点像是 HTTP 协议里的状态码。比如,protocol_version 就是不支持旧版本,bad_certificate 就是证书有问题,收到警报后另一方可以选择继续,也可以立即终止连接
|
||||
|
||||
- 握手协议:Handshake Protocol,是 TLS 里最复杂的子协议,要比 TCP 的 SYN/ACK 复杂的多,浏览器和服务器会在握手过程中协商 TLS 版本号、随机数、密码套件等信息,然后交换证书和密钥参数,最终双方协商得到会话密钥,用于后续的混合加密系统
|
||||
|
||||
- 变更密码规范协议:Change Cipher Spec Protocol,它非常简单,就是一个“通知”,告诉对方,后续的数据都将使用加密保护。那么反过来,在它之前,数据都是明文的
|
||||
|
||||
关于记录可以用下图解释
|
||||
|
||||

|
||||
|
||||
可以看出:
|
||||
|
||||
- 每一个框都可以看成一条记录(最顶上绿色框整体就是一个记录)
|
||||
|
||||
- 多个记录组合成一个 TCP 包发送(右侧最顶上3条记录,当作一个 TCP 包发送)
|
||||
|
||||
- TLS 最多经历2次消息往返(4个消息)就可以完成握手
|
||||
|
||||
#### ECDHE 握手过程(TLS核心)
|
||||
|
||||
下图是 TLS 完整的流程
|
||||
|
||||

|
||||
|
||||
上 Demo,以 Mac 上 wireshark 抓取 `https://github.com` 为例。
|
||||
|
||||
第一步:在 TCP 3次握手建立连接后,客户端发送一个 “Client Hello“ 的消息,表示开始和服务器沟通。包含客户端的 TLS 版本号、支持的密码套件、随机数,这些信息用于后续生成会话密钥。
|
||||
|
||||

|
||||
|
||||
其实,这些信息的作用都是**协商**,客户端告诉浏览器我这边的 TLS 协议是什么版本,我本地支持的加密套件都有哪些,你后续从我支持的列表中选一个。
|
||||
|
||||
第二步:服务端收到 “Client Hello” 的消息后,会返回一个 “Server Hello” 的消息。核对版本号,同时也会生成一个随机数,然后从客户端的加密套件中选择一个作为本次通信使用的密码套件。
|
||||
|
||||

|
||||
|
||||
可以看到此时,服务端选择了 `Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)`这个加密套件。服务端返回给客户端的这些信息也就代表“TLS 版本号对上了,你这边给的加密套件很多,我选了一个最合适的,椭圆曲线 + RSA + AES + SHA384。另外我给了你一个随机数,你需要保存后续使用”
|
||||
|
||||
QA:为什么看到 Client Hello、Server Hello 之间还有一个 TCP 包?
|
||||
|
||||
TLS 是建立在 TCP 的上层协议,因此要先按照 TCP 的规则来,也就是发出去的包,会收到相应的 ACK 包。如果对应的某个 TCP 数据包里面装的内容凑巧是 TLS 协议,那么 wireshark 就会把他展示成 TLS
|
||||
|
||||
注意这里不是 TCP Fast Open,TFO 是用来加速连续 TCP 连接的数据交互 TCP 拓展协议。原理如下:TCP 三次握手的过程中,当用户首次访问 Server 时,发送 SYN 包,Server 根据用户 IP 生成 Cookie(已加密),并与 SYN-ACK 一同发回 Client;当 Client 随后重连时,在SYN 包携带 TCP Cookie;如果 Server 校验合法,则在用户回复 ACK 前就可以直接发送数据;否则按照正常三次握手进行。
|
||||
|
||||
|
||||
|
||||
|
||||
第三步:服务器会把证书也发送给客户端(Server Certificate),见下图第一个大红框中的内容。
|
||||
|
||||
同时服务器选择了 ECDHE 算法,所以在发送了服务器证书后马上发送“Server Key Exchange”消息。里面是椭圆曲线的公钥(Server Params),用来实现密钥的交换算法,再加上自己的私钥签名认证(用私钥对椭圆曲线的 public key 做了签名认证生成了 Signature)
|
||||
|
||||

|
||||
|
||||
意味着,服务器告诉客户端,我这边选择的加密套件有点复杂,所以再给你一个算法的参数,和随机数一样,先保存后续使用。为了保证我就是我,我给参数 public key 做了签名。
|
||||
|
||||
第四步:服务端发送“Server Hello Done”消息。告诉客户端,我的基础信息就是这些,打招呼阶段结束
|
||||
|
||||

|
||||
|
||||
至此,第一个消息往返就结束了(2个TCP包),客户端和服务端通过**明文**共享了:Client Random、Server Random、Server Params。
|
||||
|
||||
同时,客户端拿着服务端的证书,他还不信任,他需要去找 CA,开始证书的逐级验证,确认证书的真实性,再用证书的公钥验证签名,就确认了服务器的身份。
|
||||
|
||||
第五步:客户端按照加密套件的要求,也生成了一个椭圆曲线的公钥(Client Params),用“Client Key Exchange”消息发送给服务器
|
||||
|
||||

|
||||
|
||||
至此,客户端和服务器都拿到了密钥交换算法的2个参数(Client Params、Server Params),然后用 ECDHE 算法计算出一个随机数,叫“Pre-Master”,也叫做“预主密钥”。
|
||||
|
||||
现在客户端和服务器都拥有3个随机数:Client Random、Server Random、Pre-Master,用这3个数,就可以生成用于加密会话的主密钥(Master Secret)。因为黑客拿不到 Pre-Master,所以也不会得到 Master Secret。
|
||||
|
||||
有了主密钥和会话密钥,握手就要结束了。此时客户端发送 “Change Cipher Spec”,再发送一个 “Encrypted Handshake Message” 的消息。把之前所有发送的数据做个摘要,再加密,让服务器做个验证。
|
||||
|
||||
也就是告诉服务器“后续都采用对称加密算法进行通信了,用的就是协商过的 AES,但是你还需要测试下能否正常解密”
|
||||
|
||||
QA:那里看出是 AES?
|
||||
|
||||
图上可以看到 “Change Cipher Spec”这里 `Content Type:Change Cipher Spec(20)`。
|
||||
|
||||
20对应16进制14。翻到最顶上看客户端的加密套件列表,`Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)`.
|
||||
|
||||
QA:为什么必须是3个随机数?
|
||||
|
||||
TLS 的设计者认为不能信任客户端和服务器伪随机数的可靠性,为了保证真正的“完全随机”、“不可预测”,将3个随机数混合起来,结果会更加随机,大大增加黑客破解成本,更加难以预测。
|
||||
|
||||
```shell
|
||||
master_secret = PRF(pre_master_secret, "master secret",
|
||||
ClientHello.random + ServerHello.random)
|
||||
```
|
||||
|
||||
这里的“PRF”就是伪随机数函数,它基于密码套件里的最后一个参数,比如这次的 SHA384,通过摘要算法来再一次强化 “Master Secret” 的随机性。主密钥有 48 字节,但它也不是最终用于通信的会话密钥,还会再用 PRF 扩展出更多的密钥,比如客户端发送用的会话密钥(client_write_key)、服务器发送用的会话密钥(server_write_key)等等,避免只用一个密钥带来的安全隐患。
|
||||
|
||||
第六步:服务器发送“Change Cipher Spec” 和 “Encrypted Handshake Message” 消息,双方都解密 OK,握手正式结束。后续请求就收发被加密的 HTTP 数据了。
|
||||
|
||||

|
||||
|
||||
#### RSA 握手
|
||||
|
||||
上述是主流的 TLS 握手过程,较传统握手过程稍微复杂些。
|
||||
|
||||
- 使用 ECDHE 实现密钥交换,而不是 RSA,所以会在服务端发送 “Server Key Exchange” 消息
|
||||
|
||||
- 使用 ECDHE,客户端可以不用等服务器发送 “Encrypted Handshake Message” 确认握手完成,立即发出 HTTP 报文,省去了一个消息往返的时间,叫做 “TLS False Start”,抢跑。不等建立连接完全建立就提前发送应用数据,提高传输效率
|
||||
|
||||
#### 双向认证
|
||||
|
||||
上述讲了“单向认证”握手过程,只认证了服务器的身份,而没有认证客户端的身份。因为单向认证通过后已经建立了安全通信,用账号、密码等简单的手段就能够确认用户的真实身份。
|
||||
|
||||
但为了防止账号、密码被盗,有的时候(比如网上银行)还会使用 U 盾给用户颁发客户端证书,实现“双向认证”,这样会更加安全。
|
||||
|
||||
双向认证的流程也没有太多变化,只是在 “Server Hello Done” 之后,“Client Key Exchange” 之前,客户端要发送 “Client Certificate” 消息,服务器收到后也把证书链走一遍,验证客户端的身份。
|
||||
|
||||
总结:
|
||||
|
||||
- HTTPS 协议会先与服务器执行 TCP 握手,然后执行 TLS 握手,才能建立安全连接
|
||||
|
||||
- 握手的目标是安全地交换对称密钥,需要三个随机数,第三个随机数“Pre-Master”必须加密传输,绝对不能让黑客破解
|
||||
|
||||
- “Hello” 消息交换随机数,“Key Exchange” 消息交换 “Pre-Master”
|
||||
|
||||
- “Change Cipher Spec” 之前传输的都是明文,之后都是对称密钥加密的密文
|
||||
|
||||
### TLS 1.3 最新特性
|
||||
|
||||
上述研究的是10年前的 TLS1.2,性能存在问题,2018年 TLS1.3 亮相:兼容、安全、性能是最大亮点。
|
||||
|
||||
#### 最大化兼容性
|
||||
|
||||
由于 TLS1.1、1.2 等协议已经出现了多年,很多应用软件、中间代理只认老的记录协议格式,更新改造很困难。早期试验发现,变更了记录头字段里的版本号,也就是由 0x303(TLS1.2)改为 0x304(TLS1.3),大量的代理服务器、网关都无法正确处理,最终导致 TLS 握手失败。
|
||||
|
||||
为了保证这些被广泛部署的“老设备”能够继续使用,避免新协议带来的“冲击”,TLS1.3 不得不做出妥协,保持现有的记录格式不变,通过“伪装”来实现兼容,使得 TLS1.3 看上去“像是”TLS1.2。
|
||||
|
||||
如何实现?通过**拓展协议**(Extentsion Protocol),在记录末尾处增加一系列的拓展字段来增加新的功能。老版本 TLS 不认识则可以直接忽略,所以达到了“向后兼容”。只要是 TLS1.3协议,握手的 “Hello” 小西湖都必须携带 “supported_versions” 拓展。
|
||||
|
||||
```shell
|
||||
Handshake Protocol: Client Hello
|
||||
Version: TLS 1.2 (0x0303)
|
||||
Extension: supported_versions (len=11)
|
||||
Supported Version: TLS 1.3 (0x0304)
|
||||
Supported Version: TLS 1.2 (0x0303)
|
||||
```
|
||||
|
||||
Tips:TLS1.3 利用拓展协议增加了很多特性:`supported_groups`、`key_share`、`signature_algorithms`、`server_name`
|
||||
|
||||
#### 强化安全
|
||||
|
||||
TLS1.2 应用多年,期间发现了很多加密算法的不足和漏洞,所以 TLS1.3 就在协议里进行了修改。
|
||||
|
||||
- 伪随机数函数由 PRF 升级为 HKDF(HMAC-based Extract-and-Expand Key Derivation Function)
|
||||
|
||||
- 明确禁止在记录协议里使用压缩
|
||||
|
||||
- 废除了 RC4、DES 对称加密算法
|
||||
|
||||
- 废除了 ECB、CBC 等传统分组模式
|
||||
|
||||
- 废除了 MD5、SHA1、SHA-224 摘要算法
|
||||
|
||||
- 废除了 RSA、DH 密钥交换算法和许多命名曲线
|
||||
|
||||
TLS1.3 里只保留了 AES、ChaCha20 对称加密算法;分组模式只能用 AEAD 的 GCM、CCM 和 Poly1305;摘要算法只能用 SHA256、SHA384;密钥交换算法只有 ECDHE 和 DHE;椭圆曲线也被“砍”到只剩 P-256 和 x25519 等 5 种。
|
||||
|
||||
算法精简后带来了一个意料之中的好处:原来众多的算法、参数组合导致密码套件非常复杂,难以选择,而现在的 TLS1.3 里只有 5 个套件,无论是客户端还是服务器都不会再犯“选择困难症”了。
|
||||
|
||||
QA:为什么废除 RSA、DH?
|
||||
|
||||
假设黑客获取了大量的密文,如果加密系统使用服务器证书里的 RSA 做密钥交换,一旦私钥泄漏或者被超级计算机破解,那么黑客就可以使用私钥解密处之前所有的报文“Pre-Master”,再计算出会话密钥,破解出积累的一堆密文。
|
||||
|
||||
而 ECDHE 算法在每次握手时都会生成一对临时的公钥和私钥,每次通信的密钥对都是不同的,也就是“一次一密”,即使黑客花大力气破解了这一次的会话密钥,也只是这次通信被攻击,之前的历史消息不会受到影响,仍然是安全的。所以现在主流的服务器和浏览器在握手阶段都已经不再使用 RSA,改用 ECDHE,而 TLS1.3 在协议里明确废除 RSA 和 DH 则在标准层面保证了“前向安全”。
|
||||
|
||||
#### 提升性能
|
||||
|
||||
HTTPS 建立连接时除了要做 TCP 握手,还要做 TLS 握手,在 1.2 中会多花两个消息往返(2-RTT),可能导致几十毫秒甚至上百毫秒的延迟,在移动网络中延迟还会更严重
|
||||
|
||||
现在因为密码套件大幅度简化,也就没有必要再像以前那样走复杂的协商流程了。TLS1.3 压缩了以前的“Hello”协商过程,删除了“Key Exchange”消息,把握手时间减少到了“1-RTT”,效率提高了一倍。
|
||||
|
||||
做法:利用了扩展。客户端在 “Client Hello” 消息里直接用 “supported_groups” 带上支持的曲线,比如 P-256、x25519,用“key_share”带上曲线对应的客户端公钥参数,用 “signature_algorithms” 带上签名算法。服务器收到后在这些扩展里选定一个曲线和参数,再用 “key_share” 扩展返回服务器这边的公钥参数,就实现了双方的密钥交换,后面的流程就和 1.2 基本一样了。
|
||||
|
||||

|
||||
|
||||
### HTTPS 加速
|
||||
|
||||
大家都知道 “HTTPS 连接很慢”,但 why?
|
||||
|
||||
HTTPS 连接大致上可以划分为两个部分:建立连接时的非对称加密握手 + 握手后的对称加密报文传输。由于目前流行的 AES、ChaCha20 性能都很好,还有硬件优化,报文传输的性能损耗可以说是非常地小,小到几乎可以忽略不计了。所以 “TTPS 连接慢”指的就是刚开始建立连接的那段时间。
|
||||
|
||||
在 TCP 建连之后,正式数据传输之前,HTTPS 比 HTTP 增加了一个 TLS 握手的步骤,这个步骤最长可以花费两个消息往返,也就是 2-RTT。而且在握手消息的网络耗时之外,还会有其他的一些“隐形”消耗,比如:
|
||||
|
||||
- 产生用于密钥交换的临时公私钥对(ECDHE)
|
||||
|
||||
- 验证证书时访问 CA 获取 CRL 或者 OCSP
|
||||
|
||||
- 非对称加密解密处理“Pre-Master”
|
||||
|
||||
在最差的情况下,也就是不做任何的优化措施,HTTPS 建立连接可能会比 HTTP 慢上几百毫秒甚至几秒,这其中既有网络耗时,也有计算耗时,就会让人觉得“打开一个 HTTPS 网站好慢啊”
|
||||
|
||||
下图是存在改进空间的地方。
|
||||
|
||||

|
||||
|
||||
#### 硬件优化
|
||||
|
||||
计算机中优化分为软件层面和硬件层面。HTTPS 也是一样,它的特点是计算密集型,所以可以
|
||||
|
||||
- 选择更快的 CPU,最好还内建 AES 优化,这样可以加速握手、加速传输
|
||||
|
||||
- 选择 ”SSL 加速卡“,加解密时调用它的 API,让专门硬件来做非对称加密,分担 CPU 的计算压力。但加速卡存在缺点,比如升级慢、支持算法优先,不能灵活定制解决方案
|
||||
|
||||
- SSL 加速服务器,用专门的服务器集群来彻底“卸载 TLS 握手时的加解密计算”,性能也比单纯的加速卡好得多。加速服务器通信必须异步,否则就会阻塞,没有加速效果了
|
||||
|
||||
#### 软件优化
|
||||
|
||||
分为软件升级和协议优化。其中软件升级比较好理解,就是把正在使用的软件升级为最新版本。比如把 Nginx、OpenSSL、Linux 都升级为最新版本,这个也很好理解,就是 iOS 开发一样,我们的 App 要是用来自 iOS 系统库、也要使用来自 github 的三方库、公司内部的二方库,升级一般都是解决问题和性能优化,所以享受系统升级带来的红利。可能需要运维配合做一些措施,但是优化效果来说 ROI 最佳。
|
||||
|
||||
#### 协议优化
|
||||
|
||||
TLS1.2 握手需要消费2-RTT,TLS1.3 只需要1-RTT,如果可能,升级 TLS 到1.3。如果不能升级到 TLS1.3,那么可以将握手使用的密钥交换协议改为椭圆曲线的 ECDHE 算法,不仅运算速度快,安全性高,还支持 “False Start”,能够把握手的消息往返由2-RTT减少为1-RTT,达到与 TLS1.3一样的效果。
|
||||
|
||||
椭圆曲线也要选高性能的曲线,最好是 `x25519`,其次选择 `P-256`。
|
||||
|
||||
对称加密选择 `AES_128_GCM`,其次是 `AES_256_GCM`
|
||||
|
||||
Nginx 可以用 `ssl_ciphers`、 `ssl_ecdh_curve` 等指令配置服务器使用的密码套件和椭圆曲线,把优先使用的放在前面。
|
||||
|
||||
#### 证书优化
|
||||
|
||||
除了密钥交换,握手过程中的证书验证也是一个比较耗时的操作,服务器需要把自己的证书链全发给客户端,然后客户端接收后再逐一验证。
|
||||
|
||||
存在两个优化点:证书传输、证书验证。
|
||||
|
||||
服务器的证书可以选择椭圆曲线(ECDSA)证书而不是 RSA 证书,因为 224 位的 ECC 相当于 2048 位的 RSA,所以椭圆曲线证书的“个头”要比 RSA 小很多,即能够节约带宽也能减少客户端的运算量,可谓“一举两得”。
|
||||
|
||||
客户端的证书验证其实是个很复杂的操作,除了要公钥解密验证多个证书签名外,因为证书还有可能会被撤销失效,客户端有时还会再去访问 CA,下载 CRL 或者 OCSP 数据,这又会产生 DNS 查询、建立连接、收发数据等一系列网络通信,增加好几个 RTT。
|
||||
|
||||
CRL(Certificate revocation list,证书吊销列表)由 CA 定期发布,里面是所有被撤销信任的证书序号,查询这个列表就可以知道证书是否有效。但 CRL 因为是“定期”发布,就有“时间窗口”的安全隐患,而且随着吊销证书的增多,列表会越来越大,一个 CRL 经常会上 MB。想象一下,每次需要预先下载几 M 的“无用数据”才能连接网站,实用性实在是太低了。
|
||||
|
||||
所以,现在 CRL 基本上不用了,取而代之的是 OCSP(在线证书状态协议,Online Certificate Status Protocol),向 CA 发送查询请求,让 CA 返回证书的有效状态。但 OCSP 也要多出一次网络请求的消耗,而且还依赖于 CA 服务器,如果 CA 服务器很忙,那响应延迟也是等不起的。
|
||||
|
||||
于是又出来了一个“补丁”,叫“OCSP Stapling”(OCSP 装订),它可以让服务器预先访问 CA 获取 OCSP 响应,然后在握手时随着证书一起发给客户端,免去了客户端连接 CA 服务器查询的时间。
|
||||
|
||||
#### 会话复用
|
||||
|
||||
HTTPS 建立握手过程中:TCP 3次握手、TLS 一次握手。TLS 握手的核心就是为了计算出 Master Secret,而每次建立连接都要重新计算一次,这个太奢侈了,如果可以将主密钥缓存重用,就省去了握手和计算成本。这叫做“会话复用” TLS Session Resumption, 和 HTTP Cache 一样,可以可以 HTTPS 性能而被浏览器和服务器广泛使用。
|
||||
|
||||
会话复用分为2种。
|
||||
|
||||
第一种是 Session ID,也就是客户端和服务端首次连接后各自保存一个会话 ID,内存里存储主密钥和其他信息。当客户端再次连接时,会发送一个 ID,服务器在内存中查找,找到就直接用主密钥恢复会话状态,跳过证书验证和密钥交换,只有一个消息往返就可以建立安全通信。
|
||||
|
||||
Session ID 存在缺点,服务器必须保存每个客户端会话数据,对于大型服务来说存储成本太大,加重服务器负担。
|
||||
|
||||
第二种是 Session Ticket 方案。类似 HTTP Cookie,存储责任从服务器放到了客户端,服务器加密会话信息用 “New Session Ticket” 消息发送给客户端,让客户端保存。重连的时候客户端使用拓展 “session_ticket” 发送 “Ticket”,服务器解密后验证有效期,就可以恢复会话,开始加密通信。
|
||||
|
||||
Session Ticket 方案需要使用一个固定密钥文件(ticket_key)来加密 Ticket,为了防止密钥破解,保证“前向安全”,密钥文件需要定时更换,比如一小时或者一天。
|
||||
|
||||
#### 预共享密钥
|
||||
|
||||
`False Start` 、`Session ID`、 `Session Ticket`等方式只能实现 1-RTT,而 TLS1.3 更进一步实现了 0-RTT,原理和 `Session Ticket` 差不多,但在发送 Ticket 的同时会带上应用数据(Early Data),免去了 1.2 里的服务器确认步骤,这种方式叫 `Pre-shared Key`,简称为“PSK”。
|
||||
|
||||

|
||||
|
||||
PSK 存在缺点,为了追求效率而降低一些安全性,容易收到“重放攻击”,黑客截获 PSK 后,原封不动的向服务器发出去。解决办法是只允许安全的 HTTP 请求方法,如 HEAD/GET,在消息中增加时间戳、nonce 验证。
|
||||
|
||||
总结:
|
||||
|
||||
- 可以有多种硬件和软件手段减少网络耗时和计算耗时,让 HTTPS 变得和 HTTP 一样快,最可行的是软件优化
|
||||
|
||||
- 应当尽量使用 ECDHE 椭圆曲线密码套件,节约带宽和计算量,还能实现“False Start”
|
||||
|
||||
- 服务器端应当开启“OCSP Stapling”功能,避免客户端访问 CA 去验证证书
|
||||
|
||||
- 会话复用的效果类似 Cache,前提是客户端必须之前成功建立连接,后面就可以用 `Session ID`、 `Session Ticket`等凭据跳过密钥交换、证书验证等步骤,直接开始加密通信
|
||||
|
||||
## HTTP/2 特性
|
||||
|
||||
HTTP/1 的缺点是性能问题和安全性问题。安全性问题通过 HTTP Over SSL/TLS 也就是 HTTPS 解决了。但是在数据传输方面还是很差的。HTTP/2 解决的就是解决性能问题。
|
||||
|
||||
### 兼容 HTTP/1.1
|
||||
|
||||
TLS从1.2 升级的时候踩坑了(使用 TLS1.2 无法握手了),所以 HTTP/2 把 HTTP 拆分为“语义”和“语法”。语义”层不做改动,与 HTTP/1 完全一致(即 RFC7231),比如请求方法、URI、状态码、头字段等概念都保留不变,基于 HTTP 的上层应用也不需要做任何修改,可以无缝转换到 HTTP/2。
|
||||
|
||||
HTTP/2 真正改变的是语法层
|
||||
|
||||
### 连接前言
|
||||
|
||||
TLS 握手成功之后,客户端必须要发送一个“连接前言”(connection preface),用来确认建立 HTTP/2 连接。连接前言是标准的 HTTP/1 请求报文,使用纯文本的 ASCII 码格式,请求方法是特别注册的一个关键字 `PRI`,全文只有 24 个字节
|
||||
|
||||
```shell
|
||||
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
|
||||
```
|
||||
|
||||
但在 wireshark 抓包中,HTTP/2 连接前言被称为 “Magic“。
|
||||
|
||||

|
||||
|
||||
### 头部压缩
|
||||
|
||||
HTTP/1 使用头部字段“Content-Encoding” 指定 Body 的编码方式,比如使用 gzip 压缩来节约贷款,但报文的头部 Header 被忽视了。
|
||||
|
||||
报文 Header 会携带:User Agent、Cookie、Accept、Server 等许多固定格式的头字段,多达成百上千字节,但 Body 通常只有几十字节(如GET请求、204/301/304响应),成千上万的请求响应报文中很多字段都是重复的,严重浪费,长尾效应导致大量带宽消耗在这些冗余度很高的数据上。
|
||||
|
||||
所以 HTTP/2 把“头部压缩”当作性能改进的一个发力点,采用压缩策略。专门研发了 “HPACK” 算法,HPACK 算法是一个有状态的算法,在客户端和服务器建立“字典”,用索引号表示重复的字符串,还采用哈夫曼编码来压缩整数和字符串,可达到50%~90%的高压缩率。
|
||||
|
||||
为了方便管理和压缩,HTTP/2 废除了原有的起始行概念,把起始行里面的请求方法、URI、状态码等统一转换成了头字段的形式,并且给这些“不是头字段的头字段”起了个特别的名字“伪头字段”(pseudo-header fields)。而起始行里的版本号和错误原因短语因为没什么大用,顺便也给废除了。为了与“真头字段”区分开来,这些“伪头字段”会在名字前加一个“:”,比如“:authority” “:method” “:status”,分别表示的是域名、请求方法和状态码。
|
||||
|
||||
现在 HTTP 报文头就简单了,全都是 `Key-Value` 形式的字段,于是 HTTP/2 就为一些最常用的头字段定义了一个只读的“静态表”(Static Table)。
|
||||
|
||||

|
||||
|
||||
完整的静态表可以查看[RFC 7541 - HPACK: Header Compression for HTTP/2](https://httpwg.org/specs/rfc7541.html#static.table.definition)
|
||||
|
||||
假设使用了自定义字段怎么办?动态表(Dynamic Table) 添加在静态表后面,结构相同,在编码的时候随时更新。
|
||||
|
||||

|
||||
|
||||
在 HTTP/2 连接上发送的报文越来越多,客户端、服务器的“字典”也会越来越丰富,最终每次的头部字段都会变成一两个字节的代码,原来上千字节的头用几十个字节就可以表示了,压缩效果比 gzip 要好得多。
|
||||
|
||||
### 二进制格式
|
||||
|
||||
HTTP/2 以前采用纯文本格式的报文(ASCII 码),但 HTTP/2 向 TCP/IP 协议靠拢,采用二进制格式。
|
||||
|
||||
虽对人不友好,但却方便了计算机的解析。原来使用纯文本的时候容易出现多义性,比如大小写、空白字符、回车换行、多字少字等等,程序在处理时必须用复杂的状态机,效率低,还麻烦。而二进制只有0和1,可以严格规定字段大小、顺序、标志位等格式,“对就是对,错就是错”,解析起来没有歧义,实现简单,而且体积小、速度快,做到“内部提效”。
|
||||
|
||||
把 TCP 协议的部分特性挪到应用层,将原来的 “Header + Body” 消息打散到为多个小片的二进制“帧 Frame”。用 `HEADERS` 帧存放头数据,`DATA` 帧存放实体数据。
|
||||
|
||||
这种策略有点像 `Chunked` 分块编码的方式,化整为零,但 HTTP/2 数据分帧后 “Header + Body” 的报文结构就没了,协议看到的是一个个碎片。
|
||||
|
||||

|
||||
|
||||
HTTP/2 的帧结构有点类似 TCP 的段或者 TLS 里的记录,但报头很小,只有 9 字节,非常地节省(可以对比一下 TCP 头,它最少是 20 个字节)。
|
||||
|
||||

|
||||
|
||||
帧开头是 3 个字节的长度(但不包括头的 9 个字节),默认上限是 2^14,最大是 2^24,也就是说 HTTP/2 的帧通常不超过 16K,最大是 16M。
|
||||
|
||||
长度后面的一个字节是帧类型,大致可以分成数据帧和控制帧两类,HEADERS 帧和 DATA 帧属于数据帧,存放的是 HTTP 报文,而 SETTINGS、PING、PRIORITY 等则是用来管理流的控制帧。
|
||||
|
||||
HTTP/2 总共定义了 10 种类型的帧,但一个字节可以表示最多 256 种,所以也允许在标准之外定义其他类型实现功能扩展。这就有点像 TLS 里扩展协议的意思了,比如 Google 的 gRPC 就利用了这个特点,定义了几种自用的新帧类型。
|
||||
|
||||
第 5 个字节是非常重要的帧标志信息,可以保存 8 个标志位,携带简单的控制信息。常用的标志位有 `END_HEADERS` 表示头数据结束,相当于 HTTP/1 里头后的空行(“\r\n”),`END_STREAM` 表示单方向数据发送结束(即 EOS,End of Stream),相当于 HTTP/1 里 Chunked 分块结束标志(“0\r\n\r\n”)。
|
||||
|
||||
报文头里最后 4 个字节是流标识符,也就是帧所属的“流”,接收方使用它就可以从乱序的帧里识别出具有相同流 ID 的帧序列,按顺序组装起来就实现了虚拟的“流”。流标识符虽然有 4 个字节,但最高位被保留不用,所以只有 31 位可以使用,也就是说,流标识符的上限是 2^31,大约是 21 亿。
|
||||
|
||||
### 流
|
||||
|
||||
碎片化的消息到底目的地如何组装?HTTP/2 为此定义了一个“流”(Stream)的概念,它是二进制帧的双向传输序列,**同一个消息往返的帧会分配一个唯一的流 ID**。你可以把它想象成是一个虚拟的“数据流”,在里面流动的是一串有先后顺序的数据帧,这些数据帧按照次序组装起来就是 HTTP/1 里的请求报文和响应报文。
|
||||
|
||||
因为“流”是虚拟的,实际上并不存在,所以 HTTP/2 就可以在一个 TCP 连接上用“流”同时发送多个“碎片化”的消息,这就是常说的 **`多路复用`(Multiplexing)多个往返通信都复用一个连接来处理**
|
||||
|
||||
从“流”的层面上看,消息是一些有序的“帧”序列,而在“连接”的层面上看,消息却是乱序收发的“帧”。多个请求 / 响应之间没有了顺序关系,不需要排队等待,也就不会再出现“队头阻塞”问题,降低了延迟,大幅度提高了连接的利用率。
|
||||
|
||||

|
||||
|
||||
HTTP/1 中请求-响应报文来回一次是一次 HTTP 通信,HTTP/2中一个流也做了类似的事情。
|
||||
|
||||
HTTP/2流的特点:
|
||||
|
||||
- 流是可并发的,一个 HTTP/2 连接上可以同时发出多个流传输数据,也就是并发多请求,实现“多路复用”
|
||||
|
||||
- 客户端和服务器都可以创建流,双方互不干扰
|
||||
|
||||
- 流是双向的,一个流里面客户端和服务器都可以发送或接收数据帧,也就是一个“请求 - 应答”来回
|
||||
|
||||
- 流之间没有固定关系,彼此独立,但流内部的帧是有严格顺序的
|
||||
|
||||
- 流可以设置优先级,让服务器优先处理,比如先传 HTML/CSS,后传图片,优化用户体验
|
||||
|
||||
- 流 ID 不能重用,只能顺序递增,客户端发起的 ID 是奇数,服务器端发起的 ID 是偶数
|
||||
|
||||
- 在流上发送 `RST_STREAM` 帧可以随时终止流,取消接收或发送
|
||||
|
||||
- 第 0 号流比较特殊,不能关闭,也不能发送数据帧,只能发送控制帧,用于流量控制
|
||||
|
||||

|
||||
|
||||
可以看到:
|
||||
|
||||
- HTTP/2 在一个连接上使用多个流收发数据,那么它本身默认就会是长连接,所以不需要 `Connection` 头字段(keepalive 或 close)
|
||||
|
||||
- HTTP/1 里只能断开 TCP 连接重新“三次握手”,成本很高,而在 HTTP/2 里就可以简单地发送一个 `RST_STREAM` 中断流,而长连接会继续保持
|
||||
|
||||
- 客户端和服务器两端都可以创建流,而流 ID 有奇数偶数和上限的区分,所以大多数的流 ID 都会是奇数,而且客户端在一个连接里最多只能发出 2^30,也就是 10 亿个请求
|
||||
|
||||
### 流状态转换
|
||||
|
||||

|
||||
|
||||
上图对应到标准的 HTTP 请求应答。
|
||||
|
||||
- 开始的时候流都是“空闲”(idle)状态,也就是“不存在”,可以理解成是待分配的“号段资源”
|
||||
|
||||
- 当客户端发送 HEADERS 帧后,有了流 ID,流就进入了“打开”状态,两端都可以收发数据
|
||||
|
||||
- 客户端发送一个带 `END_STREAM` 标志位的帧,流就进入了“半关闭”状态。这个“半关闭”状态很重要,意味着客户端的请求数据已经发送完了,需要接受响应数据,而服务器端也知道请求数据接收完毕,之后就要内部处理,再发送响应数据
|
||||
|
||||
- 响应数据发完了之后,也要带上“END_STREAM”标志位,表示数据发送完毕,这样流两端就都进入了“关闭”状态,流就结束了
|
||||
|
||||
- 流 ID 不能重用,所以流的生命周期就是 HTTP/1 里的一次完整的“请求 - 应答”,流关闭就是一次通信结束
|
||||
|
||||
- 下一次再发请求就要开一个新流(而不是新连接),流 ID 不断增加,直到到达上限,发送 `GOAWAY` 帧开一个新的 TCP 连接,流 ID 就又可以重头计数
|
||||
|
||||
### Server Push
|
||||
|
||||
HTTP/2 还在一定程度上改变了传统的“请求 - 应答”工作模式,服务器不再是完全被动地响应请求,也可以新建“流”主动向客户端发送消息。比如,在浏览器刚请求 HTML 的时候就提前把可能会用到的 JS、CSS 文件发给客户端,减少等待的延迟,这被称为“服务器推送”(Server Push,也叫 Cache Push)。
|
||||
|
||||
### 强化安全
|
||||
|
||||
出于兼容的考虑,HTTP/2 延续了 HTTP/1 的“明文”特点,可以像以前一样使用明文传输数据,不强制使用加密通信,不过格式还是二进制,只是不需要解密。
|
||||
|
||||
但由于 HTTPS 已经是大势所趋,而且主流的浏览器 Chrome、Firefox 等都公开宣布只支持加密的 HTTP/2,所以“事实上”的 HTTP/2 是加密的。也就是说,互联网上通常所能见到的 HTTP/2 都是使用“https”协议名,跑在 TLS 上面。
|
||||
|
||||
为了区分“加密”和“明文”这两个不同的版本,HTTP/2 协议定义了两个字符串标识符:
|
||||
|
||||
- `h2`表示加密的 HTTP/2
|
||||
|
||||
- `h2c` 表示明文的 HTTP/2,多出的那个字母“c”的意思是“clear text”
|
||||
|
||||
在 HTTP/2 标准制定的时候(2015 年)已经发现了很多 SSL/TLS 的弱点,而新的 TLS1.3 还未发布,所以加密版本的 HTTP/2 在安全方面做了强化,要求下层的通信协议必须是 TLS1.2 以上,还要支持前向安全和 SNI,并且把几百个弱密码套件列入了“黑名单”,比如 DES、RC4、CBC、SHA-1 都不能在 HTTP/2 里使用,相当于底层用的是“TLS1.25”。
|
||||
|
||||
### 协议栈
|
||||
|
||||
HTTP/2 是建立在 HPack + Stream + TLS1.2 之上的。
|
||||
|
||||

|
||||
|
||||
QA:明文形式的 HTTP/2(h2c)有什么好处,应该如何使用呢?
|
||||
|
||||
h2c使用明文传输,速度更快,不需要TLS握手,按需选择
|
||||
|
||||
QA:应该怎样理解 HTTP/2 里的“流”,为什么它是“虚拟”的?
|
||||
|
||||
HTTP/2 中数据不再像 “Header + Body” 的数据包格式,将多个请求分成不同的流,每个流中切成不同的帧(Frame),包括 `HEADER Frame`、`DATA Frame` 。发送的时候按帧发送,为了保证帧到达目的地址后可以组装还原为真实数据,给每个帧添加了一个 `流 ID`,标记该帧属于哪个流,服务端收到按照流 ID 进行组装拼接。从传输角度看流是不存在的,因为传输的时候就是一个个的帧,所以是虚拟的。
|
||||
|
||||
QA:对比一下 HTTP/2 与 HTTP/1、HTTPS 的相同点和不同点吗?
|
||||
|
||||
相同点:
|
||||
|
||||
- 都是基于TCP和TLS的,url格式都是相同的
|
||||
|
||||
- 都是基于header+body的形式。都是请求-应答模型
|
||||
|
||||
不同点:
|
||||
|
||||
- 使用了HPACK进行头部压缩
|
||||
|
||||
- HTTP/2 参考 TCP/IP 使用的是二进制的方式进行传输
|
||||
|
||||
- 将多个请求切分成帧发送,实现了多路复用
|
||||
|
||||
- 服务器可以主动向客户端推送消息。充分利用了 TCP 的全双工通道
|
||||
|
||||
总结:
|
||||
|
||||
- HTTP 协议取消了小版本号,所以 HTTP/2 的正式名字不是 2.0
|
||||
|
||||
- HTTP/2 在“语义”上兼容 HTTP/1,保留了请求方法、URI 等传统概念
|
||||
|
||||
- HTTP/2 使用 “HPACK” 算法压缩头部信息,消除冗余数据节约带宽
|
||||
|
||||
- HTTP/2 的消息不再是“Header+Body”的形式,而是分散为多个二进制“帧”
|
||||
|
||||
- HTTP/2 使用虚拟的“流”传输消息,解决了困扰多年的“队头阻塞”问题
|
||||
|
||||
- 在一个 HTTP/2 连接上可以并发多个流,也就是多个“请求 - 响应”报文,这就是“多路复用”,提高连接的利用率
|
||||
|
||||
- HTTP/2 也增强了安全性,要求至少是 TLS1.2,而且禁用了很多不安全的密码套件
|
||||
|
||||
- HTTP/2 必须先发送一个“连接前言”字符串,然后才能建立正式连接
|
||||
|
||||
- HTTP/2 废除了起始行,统一使用头字段,在两端维护字段 `Key-Value`的索引表,使用“HPACK”算法压缩头部
|
||||
|
||||
- HTTP/2 把报文切分为多种类型的二进制帧,报头里最重要的字段是流标识符,标记帧属于哪个流
|
||||
|
||||
- 流是 HTTP/2 虚拟的概念,是帧的双向传输序列,相当于 HTTP/1 里的一次“请求 - 应答”
|
||||
|
||||
## HTTP/3
|
||||
|
||||
HTTP/2 通过头部压缩、二进制分帧、虚拟的“流”、多路复用,性能方面比 HTTP/1 有了很大的提升,基本上解决了“队头阻塞”。
|
||||
|
||||
为什么叫基本上?HTTP/2 虽然使用帧、流、多路复用,看上去解决了“队头阻塞”,但这些手段都是在应用层里,而在下层,传输还是需要依靠 TCP 协议,还是会发生“队头阻塞”。因为HTTP/2 把多个“请求 - 响应”分解成流,交给 TCP 后,TCP 会再拆成更小的包依次发送(其实在 TCP 里应该叫 segment,也就是“段”),在网络良好的情况下,包可以很快送达目的地。但如果网络质量比较差,就有可能丢包。而 TCP 为了保证可靠传输,有个特别的 "**丢包重传**”机制,丢失的包必须要等待重新传输确认,其他的包即使已经收到了,也只能放在缓冲区里,上层的应用拿不出来,还是存在队头阻塞。
|
||||
|
||||
比如客户端用 TCP 发送了三个包,但服务器所在的操作系统只收到了后两个包,第一个包丢了。那么内核里的 TCP 协议栈就只能把已经收到的包暂存起来,“停下”等着客户端重传那个丢失的包,这样就又出现了“队头阻塞”。
|
||||
|
||||
队头阻塞是 TCP 的基本特征,所以上层的 HTTP 再优化也解决不了。
|
||||
|
||||
HTTP Over QUIC 就是 HTTP3,完美解决队头阻塞问题。
|
||||
|
||||
### QUIC 协议
|
||||
|
||||

|
||||
|
||||
可以看到 HTTP3 对比 HTTP2 将 TCP 换为 UDP,因为 UDP 无序,包之间没有依赖关系,所以从根本上解决了“队头阻塞”问题
|
||||
|
||||
QUIC 选择 UDP,在之上将 TCP 那一套连接管理、拥塞窗口、流量控制搬来,集大成者。
|
||||
|
||||
### QUIC 特点
|
||||
|
||||
- 快:QUIC 基于 UDP,而 UDP 是“无连接”的,根本就不需要“握手”和“挥手”,所以天生就要比 TCP 快。
|
||||
|
||||
- 可靠传输:TCP 在 IP 的基础上实现了可靠传输一样,QUIC 也基于 UDP 实现了可靠传输,保证数据一定能够抵达目的地。它还引入了类似 HTTP/2 的“流”和“多路复用”,单个“流”是有序的,可能会因为丢包而阻塞,但其他“流”不会受到影响
|
||||
|
||||
- 安全:为了防止网络上的中间设备(Middle Box)识别协议的细节,QUIC 全面采用加密通信,可以很好地抵御窜改和“协议僵化”(ossification)
|
||||
|
||||
- 0-RTT/1-RTT:TLS1.3 已经在去年(2018)正式发布,所以 QUIC 就直接应用了 TLS1.3,顺便也就获得了 0-RTT、1-RTT 连接的好处
|
||||
|
||||
QUIC 并不是建立在 TLS 之上,而是内部“包含”了 TLS。它使用自己的帧“接管”了 TLS 里的“记录”,握手消息、警报消息都不使用 TLS 记录,直接封装成 QUIC 的帧发送,省掉了一次开销。
|
||||
|
||||
### QUIC 结构
|
||||
|
||||
QUIC 的基本数据传输单位是包(packet)和帧(frame),一个包由多个帧组成,包面向的是“连接”,帧面向的是“流”。
|
||||
|
||||
QUIC 使用不透明的 “**连接 ID**” 来标记通信的两个端点,客户端和服务器可以自行选择一组 ID 来标记自己,这样就解除了 TCP 里连接对“IP 地址 + 端口”(即常说的四元组)的强绑定,支持“连接迁移”(Connection Migration)
|
||||
|
||||

|
||||
|
||||
比如从外面玩回到家,手机会自动由 4G 切换到 WiFi。这时 IP 地址会发生变化,TCP 就必须重新建立连接。而 QUIC 连接里的两端连接 ID 不会变,所以连接在“逻辑上”没有中断,它就可以在新的 IP 地址上继续使用之前的连接,消除重连的成本,实现连接的无缝迁移。
|
||||
|
||||
QUIC 的帧里有多种类型,PING、ACK 等帧用于管理连接,而 STREAM 帧专门用来实现流。
|
||||
|
||||
QUIC 里的流与 HTTP/2 的流非常相似,也是帧的序列。但 HTTP/2 里的流都是双向的,而 QUIC 则分为双向流和单向流。
|
||||
|
||||

|
||||
|
||||
QUIC 帧普遍采用变长编码,最少只要 1 个字节,最多有 8 个字节。流 ID 的最大可用位数是 62,数量上比 HTTP/2 的 2^31 大大增加。
|
||||
|
||||
流 ID 还保留了最低两位用作标志,第 1 位标记流的发起者,0 表示客户端,1 表示服务器;第 2 位标记流的方向,0 表示双向流,1 表示单向流。所以 QUIC 流 ID 的奇偶性质和 HTTP/2 刚好相反,客户端的 ID 是偶数,从 0 开始计数。
|
||||
|
||||
### HTTP3 协议
|
||||
|
||||
因为 QUIC 本身就已经支持了加密、流和多路复用,所以 HTTP/3 的工作减轻了很多,把流控制都交给 QUIC 去做。调用的不再是 TLS 的安全接口,也不是 Socket API,而是专门的 QUIC 函数。不过这个“QUIC 函数”还没有形成标准,必须要绑定到某一个具体的实现库。
|
||||
|
||||
HTTP/3 里仍然使用流来发送“请求 - 响应”,但它自身不需要像 HTTP/2 那样再去定义流,而是直接使用 QUIC 的流,相当于做了一个“概念映射”。
|
||||
|
||||
HTTP/3 里的“双向流”可以完全对应到 HTTP/2 的流,而“单向流”在 HTTP/3 里用来实现控制和推送,近似地对应 HTTP/2 的 0 号流。由于流管理被“下放”到了 QUIC,所以 HTTP/3 里帧的结构也变简单了。帧头只有两个字段:类型和长度,而且同样都采用变长编码,最小只需要两个字节。
|
||||
|
||||

|
||||
|
||||
HTTP/3 里的帧仍然分成数据帧和控制帧两类,HEADERS 帧和 DATA 帧传输数据,但其他一些帧因为在下层的 QUIC 里有了替代,所以在 HTTP/3 里就都消失了,比如 RST_STREAM、WINDOW_UPDATE、PING 等。
|
||||
|
||||
头部压缩算法在 HTTP/3 里升级成了“QPACK”,使用方式上也做了改变。虽然也分成静态表和动态表,但在流上发送 HEADERS 帧时不能更新字段,只能引用,索引表的更新需要在专门的单向流上发送指令来管理,解决了 HPACK 的“队头阻塞”问题。
|
||||
|
||||
另外,QPACK 的字典也做了优化,静态表由之前的 61 个增加到了 98 个,而且序号从 0 开始,也就是说“:authority”的编号是 0。
|
||||
|
||||
### 服务发现
|
||||
|
||||
HTTP/3 没有指定默认的端口号,也就是说不一定非要在 UDP 的 80 或者 443 上提供 HTTP/3 服务。
|
||||
|
||||
这就要用到 HTTP/2 里的“扩展帧”了。浏览器需要先用 HTTP/2 协议连接服务器,然后服务器可以在启动 HTTP/2 连接后发送一个 `Alt-Svc` 帧,包含一个 `h3=host:port` 的字符串,告诉浏览器在另一个端点上提供等价的 HTTP/3 服务。浏览器收到 `Alt-Svc` 帧,会使用 QUIC 异步连接指定的端口,如果连接成功,就会断开 HTTP/2 连接,改用新的 HTTP/3 收发数据。
|
||||
|
||||
总结:
|
||||
|
||||
- HTTP/3 基于 QUIC 协议,完全解决了“队头阻塞”问题,弱网环境下的表现会优于 HTTP/2
|
||||
|
||||
- QUIC 是一个新的传输层协议,建立在 UDP 之上,实现了可靠传输
|
||||
|
||||
- QUIC 内含了 TLS1.3,只能加密通信,支持 0-RTT 快速建连
|
||||
|
||||
- QUIC 的连接使用“不透明”的连接 ID,不绑定在“IP 地址 + 端口”上,支持“连接迁移”
|
||||
|
||||
- QUIC 的流与 HTTP/2 的流很相似,但分为双向流和单向流
|
||||
|
||||
- HTTP/3 没有指定默认端口号,需要用 HTTP/2 的扩展帧 `Alt-Svc` 来发现。
|
||||
|
||||
## Socket/WebSocket
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [HTTP/2 协议-HPACK(HTTP2 头部压缩算法)原理介绍_爱因诗贤的博客-CSDN博客_hpack算法](https://blog.csdn.net/qq_38937634/article/details/111410191)
|
||||
@@ -1,8 +1,7 @@
|
||||
# 第五部分
|
||||
|
||||
第五部分主要记录在计算机网络知识
|
||||
|
||||
* [1、HTTP请求头Range](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.1.md)
|
||||
* [1、HTTP/HTTPS 细节探索](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.1.md)
|
||||
* [2、认识HTTP、TCP、UDP](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.2.md)
|
||||
* [3、你知道字节序吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.3.md)
|
||||
* [4、自定义报头协议](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.4.md)
|
||||
|
||||
@@ -63,7 +63,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,6 +105,10 @@
|
||||
* [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)
|
||||
* [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)
|
||||
* [Chapter2 - Web FrontEnd](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/chapter2.md)
|
||||
* [1、-last-child与-last-of-type你只是会用,有研究过区别吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.1.md)
|
||||
* [2、正则表达式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.2.md)
|
||||
@@ -168,7 +172,7 @@
|
||||
|
||||
|
||||
* [Chapter5 - Network](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/chapter5.md)
|
||||
* [1、HTTP请求头Range](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.1.md)
|
||||
* [1、HTTP/HTTPS 细节探索](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.1.md)
|
||||
* [2、认识HTTP、TCP、UDP](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.2.md)
|
||||
* [3、你知道字节序吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.3.md)
|
||||
* [4、自定义报头协议](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter5%20-%20Network/5.4.md)
|
||||
|
||||
BIN
assets/.DS_Store
vendored
BIN
assets/APMStartup.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
assets/AppStarupPipeline.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
assets/CATransactionCommit.png
Normal file
|
After Width: | Height: | Size: 866 KiB |
BIN
assets/CacheComunicate.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
assets/CacheControlFromDisk.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
assets/CacheControlPipeline.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
assets/CacheProxyServer.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
assets/ClientCacheControl.png
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
assets/CoreAnimationCommit.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
assets/CoreAnimationPipeline1.png
Normal file
|
After Width: | Height: | Size: 532 KiB |
BIN
assets/CoreAnimationPipeline2.png
Normal file
|
After Width: | Height: | Size: 508 KiB |
BIN
assets/CoreAnimationPipeline3.png
Normal file
|
After Width: | Height: | Size: 557 KiB |
BIN
assets/CoreAnimationPipeline4.png
Normal file
|
After Width: | Height: | Size: 704 KiB |
BIN
assets/CoreAnimationPipeline5.png
Normal file
|
After Width: | Height: | Size: 639 KiB |
BIN
assets/CoreAnimationPipeline6.png
Normal file
|
After Width: | Height: | Size: 697 KiB |
BIN
assets/HTTP2BinaryData.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
assets/HTTP2BinaryFrameStructure.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
assets/HTTP2ConnectionPreface.png
Normal file
|
After Width: | Height: | Size: 897 KiB |
BIN
assets/HTTP2DymaicTable.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
assets/HTTP2ProtocolStack.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
assets/HTTP2StaticTable.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
assets/HTTP2StatusChange.png
Normal file
|
After Width: | Height: | Size: 361 KiB |
BIN
assets/HTTP2Stram.png
Normal file
|
After Width: | Height: | Size: 323 KiB |
BIN
assets/HTTP2StreamExample.png
Normal file
|
After Width: | Height: | Size: 334 KiB |
BIN
assets/HTTP3Frame.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
assets/HTTP3Protocol.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
assets/HTTPProxyVia.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
assets/HTTPSCAChain.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
assets/HTTPSIntegrity.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
assets/HTTPSOverSSL.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
assets/HTTPSSign.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
assets/NSUserDefault-XPC.png
Normal file
|
After Width: | Height: | Size: 1003 KiB |
BIN
assets/NSUserDfault-lock.png
Normal file
|
After Width: | Height: | Size: 243 KiB |
BIN
assets/Notification-namelessTable.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
assets/Objc-OrderFile.png
Normal file
|
After Width: | Height: | Size: 210 KiB |
BIN
assets/PageFault.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
assets/QUICPacket.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
assets/QUICStream.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
assets/RenderPipeline.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
assets/RenderStructure.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
assets/TLS1.3PSK.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
assets/TLS1.3Pipeline.png
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
assets/TLSFullPipeline.png
Normal file
|
After Width: | Height: | Size: 473 KiB |
BIN
assets/TLSHandShake1.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/TLSHandShake2.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
assets/TLSHandShake3.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/TLSHandShake4.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/TLSHandShake5.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/TLSHandShake6.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/TLSRecords.png
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
assets/TLSSpeedDisadvantage.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
assets/Thread_priority.jpg
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
assets/UIRenderPipeline.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
assets/UIViewRenderPipeline.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
assets/XcodeZombieDetect.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/ZombieObjectsDetector.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/cxx_destructDemo1.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
assets/cxx_destructDemo2.png
Normal file
|
After Width: | Height: | Size: 726 KiB |
BIN
assets/cxx_destructdemo3.png
Normal file
|
After Width: | Height: | Size: 344 KiB |
BIN
assets/iOSAsyncRender.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
assets/iOSPageInPageOut.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
assets/iOSRenderProcess.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
assets/notification-namedTable.png
Normal file
|
After Width: | Height: | Size: 85 KiB |