├── .gitignore
├── LICENSE
├── README.md
└── Tips
├── DNS污染方案调研
├── iOS防DNS污染方案调研---Cookie业务场景.md
├── iOS防DNS污染方案调研---SNI业务场景.md
└── iOS防DNS污染方案调研---WebView业务场景.md
├── HTTP状态码汇总.m
├── HTTP状态码汇总.md
├── Heap-Stack Dance
├── Heap-Stack Dance.md
└── weak-strong-drance-demo
│ ├── weak-strong-drance-demo.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── weak-strong-drance-demo
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── Foo.h
│ ├── Foo.m
│ ├── Info.plist
│ ├── ViewController.h
│ ├── ViewController.m
│ └── main.m
├── iOS常见耗电量检测方案调研
└── iOS常见耗电量检测方案调研.md
├── 基于Websocket的IM即时通讯技术
├── IM 即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md
├── 技术实现细节.md
├── 有一种 Block 叫 Callback,有一种 Callback 做 CompletionHandler.md
└── 防 DNS 污染方案.md
├── 大话Socket.md
└── 避免使用GCD-Global队列创建Runloop常驻线程
├── CYLGCDRunloopDemo
├── CYLGCDRunloopDemo.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ └── contents.xcworkspacedata
├── CYLGCDRunloopDemo
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── Icon-40.png
│ │ │ ├── Icon-40@2x.png
│ │ │ ├── Icon-40@3x.png
│ │ │ ├── Icon-60@2x.png
│ │ │ ├── Icon-60@3x.png
│ │ │ ├── Icon-72.png
│ │ │ ├── Icon-72@2x.png
│ │ │ ├── Icon-76.png
│ │ │ ├── Icon-76@2x.png
│ │ │ ├── Icon-83.5@2x.png
│ │ │ ├── Icon-Small-50.png
│ │ │ ├── Icon-Small-50@2x.png
│ │ │ ├── Icon-Small.png
│ │ │ ├── Icon-Small@2x.png
│ │ │ ├── Icon-Small@3x.png
│ │ │ ├── Icon.png
│ │ │ └── Icon@2x.png
│ │ ├── Contents.json
│ │ └── LaunchImage.launchimage
│ │ │ ├── Contents.json
│ │ │ ├── Default-568h@2x.png
│ │ │ ├── Default.png
│ │ │ ├── Default@2x.png
│ │ │ ├── Default~ipad.png
│ │ │ ├── Default~ipad@2x.png
│ │ │ ├── Default~ipad~landscape.png
│ │ │ ├── Default~ipad~landscape@2x.png
│ │ │ ├── Default~ipad~landscape~nostatusbar.png
│ │ │ ├── Default~ipad~landscape~nostatusbar@2x.png
│ │ │ ├── Default~ipad~nostatusbar.png
│ │ │ ├── Default~ipad~nostatusbar@2x.png
│ │ │ ├── iPhone6-Plus-landscape@3x.png
│ │ │ ├── iPhone6-Plus-portrait@3x.png
│ │ │ └── iPhone6-portrait@2x.png
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Foo.h
│ ├── Foo.m
│ ├── Info.plist
│ ├── ViewController.h
│ ├── ViewController.m
│ └── main.m
├── CYLGCDRunloopDemoTests
│ ├── CYLGCDRunloopDemoTests.m
│ └── Info.plist
├── CYLGCDRunloopDemoUITests
│ ├── CYLGCDRunloopDemoUITests.m
│ └── Info.plist
└── Podfile
└── 避免使用GCD-Global队列创建Runloop常驻线程.md
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata
19 |
20 | ## Other
21 | *.xccheckout
22 | *.moved-aside
23 | *.xcuserstate
24 | *.xcscmblueprint
25 |
26 | ## Obj-C/Swift specific
27 | *.hmap
28 | *.ipa
29 |
30 | # CocoaPods
31 | #
32 | # We recommend against adding the Pods directory to your .gitignore. However
33 | # you should judge for yourself, the pros and cons are mentioned at:
34 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
35 | #
36 | # Pods/
37 |
38 | # Carthage
39 | #
40 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
41 | # Carthage/Checkouts
42 |
43 | Carthage/Build
44 |
45 | # fastlane
46 | #
47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
48 | # screenshots whenever they are needed.
49 | # For more information about the recommended setup visit:
50 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
51 |
52 | fastlane/report.xml
53 | fastlane/screenshots
54 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 微博@iOS程序犭袁
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # iOSDevelopmentTips
2 |
3 |
4 | --------------------------------------------
5 |
6 |
7 |
8 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | --------------------------------------------
42 |
43 |
44 |
45 |
46 |
47 | 1. [《iOS9适配系列教程》](https://github.com/ChenYilong/iOS9AdaptationTips)
48 | 2. [《iOS10适配系列教程》]( https://github.com/ChenYilong/iOS10AdaptationTips )
49 | 3. [《iOS11适配系列教程》](https://github.com/ChenYilong/iOS11AdaptationTips)
50 | 4. [《iOS面试题集锦》](https://github.com/ChenYilong/iOSInterviewQuestions)
51 | 5. [《Parse源码学习》]( https://github.com/ChenYilong/ParseSourceCodeStudy )
52 | 6. [《HTTP状态码汇总》](https://github.com/ChenYilong/iOSBlog/issues/3)
53 | 7. [《大话Socket》](https://github.com/ChenYilong/iOSBlog/issues/5)
54 | 8. [《IM 即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角)》]( https://github.com/ChenYilong/iOSBlog/issues/6)
55 | 9. [《基于WebRTC的视频聊天技术在iOS端的实现》](https://github.com/ChenYilong/WebRTC)
56 | 10. [《使用 Heap-Stack Dance 替代 Weak-Strong Dance,优雅避开循环引用》](https://github.com/ChenYilong/iOSBlog/issues/4)
57 | 11. [《避免使用 GCD Global 队列创建 Runloop 常驻线程》](https://github.com/ChenYilong/iOSBlog/issues/9)
58 | 12. [《iOS 常见耗电量检测方案调研》]( https://github.com/ChenYilong/iOSBlog/issues/10 )
59 |
60 | ----------
61 |
62 |
63 | Posted by [微博@iOS程序犭袁](http://weibo.com/luohanchenyilong/)
64 | 原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | [Creative Commons BY-NC-ND 3.0](http://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Tips/DNS污染方案调研/iOS防DNS污染方案调研---Cookie业务场景.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - [iOS 防 DNS 污染方案调研 --- Cookie 业务场景](#ios-%E9%98%B2-dns-%E6%B1%A1%E6%9F%93%E6%96%B9%E6%A1%88%E8%B0%83%E7%A0%94-----cookie-%E4%B8%9A%E5%8A%A1%E5%9C%BA%E6%99%AF)
5 | - [概述](#%E6%A6%82%E8%BF%B0)
6 | - [WKWebView 使用 NSURLProtocol 拦截请求无法获取 Cookie 信息](#wkwebview-%E4%BD%BF%E7%94%A8-nsurlprotocol-%E6%8B%A6%E6%88%AA%E8%AF%B7%E6%B1%82%E6%97%A0%E6%B3%95%E8%8E%B7%E5%8F%96-cookie-%E4%BF%A1%E6%81%AF)
7 | - [利用 iOS11 API WKHTTPCookieStore 解决 WKWebView 首次请求不携带 Cookie 的问题](#%E5%88%A9%E7%94%A8-ios11-api-wkhttpcookiestore-%E8%A7%A3%E5%86%B3-wkwebview-%E9%A6%96%E6%AC%A1%E8%AF%B7%E6%B1%82%E4%B8%8D%E6%90%BA%E5%B8%A6-cookie-%E7%9A%84%E9%97%AE%E9%A2%98)
8 | - [利用 iOS11 之前的 API 解决 WKWebView 首次请求不携带 Cookie 的问题](#%E5%88%A9%E7%94%A8-ios11-%E4%B9%8B%E5%89%8D%E7%9A%84-api-%E8%A7%A3%E5%86%B3-wkwebview-%E9%A6%96%E6%AC%A1%E8%AF%B7%E6%B1%82%E4%B8%8D%E6%90%BA%E5%B8%A6-cookie-%E7%9A%84%E9%97%AE%E9%A2%98)
9 | - [Cookie包含动态 IP 导致登陆失效问题](#cookie%E5%8C%85%E5%90%AB%E5%8A%A8%E6%80%81-ip-%E5%AF%BC%E8%87%B4%E7%99%BB%E9%99%86%E5%A4%B1%E6%95%88%E9%97%AE%E9%A2%98)
10 |
11 |
12 |
13 | # iOS 防 DNS 污染方案调研 --- Cookie 业务场景
14 |
15 | ## 概述
16 |
17 | 本文将讨论下类似这样的问题:
18 |
19 | - WKWebView 对于 Cookie 的管理一直是它的短板,那么 iOS11 是否有改进,如果有,如果利用这样的改进?
20 | - 采用 IP 直连方案后,服务端返回的 Cookie 里的 Domain 字段也会使用 IP 。如果 IP 是动态的,就有可能导致一些问题:由于许多 H5 业务都依赖于 Cookie 作登录态校验,而 WKWebView 上请求不会自动携带 Cookie。
21 |
22 |
23 | ## WKWebView 使用 NSURLProtocol 拦截请求无法获取 Cookie 信息
24 |
25 | iOS11推出了新的 API `WKHTTPCookieStore` 可以用来拦截 WKWebView 的 Cookie 信息
26 |
27 | 用法示例如下:
28 |
29 | ```Objective-C
30 | WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
31 | //get cookies
32 | [cookieStroe getAllCookies:^(NSArray * _Nonnull cookies) {
33 | NSLog(@"All cookies %@",cookies);
34 | }];
35 |
36 | //set cookie
37 | NSMutableDictionary *dict = [NSMutableDictionary dictionary];
38 | dict[NSHTTPCookieName] = @"userid";
39 | dict[NSHTTPCookieValue] = @"123";
40 | dict[NSHTTPCookieDomain] = @"xxxx.com";
41 | dict[NSHTTPCookiePath] = @"/";
42 |
43 | NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:dict];
44 | [cookieStroe setCookie:cookie completionHandler:^{
45 | NSLog(@"set cookie");
46 | }];
47 |
48 | //delete cookie
49 | [cookieStroe deleteCookie:cookie completionHandler:^{
50 | NSLog(@"delete cookie");
51 | }];
52 | ```
53 |
54 |
55 | ### 利用 iOS11 API WKHTTPCookieStore 解决 WKWebView 首次请求不携带 Cookie 的问题
56 |
57 |
58 | 问题说明:由于许多 H5 业务都依赖于 Cookie 作登录态校验,而 WKWebView 上请求不会自动携带 Cookie。比如,如果你在Native层面做了登陆操作,获取了Cookie信息,也使用 NSHTTPCookieStorage 存到了本地,但是使用 WKWebView 打开对应网页时,网页依然处于未登陆状态。如果是登陆也在 WebView 里做的,就不会有这个问题。
59 |
60 | iOS11 的 API 可以解决该问题,只要是存在 WKHTTPCookieStore 里的 cookie,WKWebView 每次请求都会携带,存在 NSHTTPCookieStorage 的cookie,并不会每次都携带。于是会发生首次 WKWebView 请求不携带 Cookie 的问题。
61 |
62 | 解决方法:
63 |
64 | 在执行 `-[WKWebView loadReques:]` 前将 `NSHTTPCookieStorage` 中的内容复制到 `WKHTTPCookieStore` 中。示例代码如下:
65 |
66 | ```Objective-C
67 | [self copyNSHTTPCookieStorageToWKHTTPCookieStoreWithCompletionHandler:^{
68 | NSURL *url = [NSURL URLWithString:@"https://www.v2ex.com"];
69 | NSURLRequest *request = [NSURLRequest requestWithURL:url];
70 | [_webView loadRequest:request];
71 | }];
72 | ```
73 |
74 | ```Objective-C
75 | - (void)copyNSHTTPCookieStorageToWKHTTPCookieStoreWithCompletionHandler:(nullable void (^)())theCompletionHandler; {
76 | NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
77 | WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
78 | if (cookies.count == 0) {
79 | !theCompletionHandler ?: theCompletionHandler();
80 | return;
81 | }
82 | for (NSHTTPCookie *cookie in cookies) {
83 | [cookieStroe setCookie:cookie completionHandler:^{
84 | if ([[cookies lastObject] isEqual:cookie]) {
85 | !theCompletionHandler ?: theCompletionHandler();
86 | return;
87 | }
88 | }];
89 | }
90 | }
91 | ```
92 |
93 | 这个是 iOS11 的API,针对iOS11之前的系统,需要另外处理。
94 |
95 | ## 利用 iOS11 之前的 API 解决 WKWebView 首次请求不携带 Cookie 的问题
96 |
97 | 通过让所有 WKWebView 共享同一个 WKProcessPool 实例,可以实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie)数据。不过 WKWebView WKProcessPool 实例在 app 杀进程重启后会被重置,导致 WKProcessPool 中的 Cookie、session Cookie 数据丢失,目前也无法实现 WKProcessPool 实例本地化保存。可以采取 cookie 放入 Header 的方法来做。
98 |
99 | ```Objective-C
100 | WKWebView * webView = [WKWebView new];
101 | NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://xxx.com/login"]];
102 | [request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"];
103 | [webView loadRequest:request];
104 | ```
105 |
106 | 其中对于 `skey=skeyValue` 这个cookie值的获取,也可以统一通过domain获取,获取的方法,可以参照下面的工具类:
107 |
108 | ```Objective-C
109 | HTTPDNSCookieManager.h
110 |
111 | #ifndef HTTPDNSCookieManager_h
112 | #define HTTPDNSCookieManager_h
113 |
114 | // URL匹配Cookie规则
115 | typedef BOOL (^HTTPDNSCookieFilter)(NSHTTPCookie *, NSURL *);
116 |
117 | @interface HTTPDNSCookieManager : NSObject
118 |
119 | + (instancetype)sharedInstance;
120 |
121 | /**
122 | 指定URL匹配Cookie策略
123 |
124 | @param filter 匹配器
125 | */
126 | - (void)setCookieFilter:(HTTPDNSCookieFilter)filter;
127 |
128 | /**
129 | 处理HTTP Reponse携带的Cookie并存储
130 |
131 | @param headerFields HTTP Header Fields
132 | @param URL 根据匹配策略获取查找URL关联的Cookie
133 | @return 返回添加到存储的Cookie
134 | */
135 | - (NSArray *)handleHeaderFields:(NSDictionary *)headerFields forURL:(NSURL *)URL;
136 |
137 | /**
138 | 匹配本地Cookie存储,获取对应URL的request cookie字符串
139 |
140 | @param URL 根据匹配策略指定查找URL关联的Cookie
141 | @return 返回对应URL的request Cookie字符串
142 | */
143 | - (NSString *)getRequestCookieHeaderForURL:(NSURL *)URL;
144 |
145 | /**
146 | 删除存储cookie
147 |
148 | @param URL 根据匹配策略查找URL关联的cookie
149 | @return 返回成功删除cookie数
150 | */
151 | - (NSInteger)deleteCookieForURL:(NSURL *)URL;
152 |
153 | @end
154 |
155 | #endif /* HTTPDNSCookieManager_h */
156 |
157 | HTTPDNSCookieManager.m
158 | #import
159 | #import "HTTPDNSCookieManager.h"
160 |
161 | @implementation HTTPDNSCookieManager
162 | {
163 | HTTPDNSCookieFilter cookieFilter;
164 | }
165 |
166 | - (instancetype)init {
167 | if (self = [super init]) {
168 | /**
169 | 此处设置的Cookie和URL匹配策略比较简单,检查URL.host是否包含Cookie的domain字段
170 | 通过调用setCookieFilter接口设定Cookie匹配策略,
171 | 比如可以设定Cookie的domain字段和URL.host的后缀匹配 | URL是否符合Cookie的path设定
172 | 细节匹配规则可参考RFC 2965 3.3节
173 | */
174 | cookieFilter = ^BOOL(NSHTTPCookie *cookie, NSURL *URL) {
175 | if ([URL.host containsString:cookie.domain]) {
176 | return YES;
177 | }
178 | return NO;
179 | };
180 | }
181 | return self;
182 | }
183 |
184 | + (instancetype)sharedInstance {
185 | static id singletonInstance = nil;
186 | static dispatch_once_t onceToken;
187 | dispatch_once(&onceToken, ^{
188 | if (!singletonInstance) {
189 | singletonInstance = [[super allocWithZone:NULL] init];
190 | }
191 | });
192 | return singletonInstance;
193 | }
194 |
195 | + (id)allocWithZone:(struct _NSZone *)zone {
196 | return [self sharedInstance];
197 | }
198 |
199 | - (id)copyWithZone:(struct _NSZone *)zone {
200 | return self;
201 | }
202 |
203 | - (void)setCookieFilter:(HTTPDNSCookieFilter)filter {
204 | if (filter != nil) {
205 | cookieFilter = filter;
206 | }
207 | }
208 |
209 | - (NSArray *)handleHeaderFields:(NSDictionary *)headerFields forURL:(NSURL *)URL {
210 | NSArray *cookieArray = [NSHTTPCookie cookiesWithResponseHeaderFields:headerFields forURL:URL];
211 | if (cookieArray != nil) {
212 | NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
213 | for (NSHTTPCookie *cookie in cookieArray) {
214 | if (cookieFilter(cookie, URL)) {
215 | NSLog(@"Add a cookie: %@", cookie);
216 | [cookieStorage setCookie:cookie];
217 | }
218 | }
219 | }
220 | return cookieArray;
221 | }
222 |
223 | - (NSString *)getRequestCookieHeaderForURL:(NSURL *)URL {
224 | NSArray *cookieArray = [self searchAppropriateCookies:URL];
225 | if (cookieArray != nil && cookieArray.count > 0) {
226 | NSDictionary *cookieDic = [NSHTTPCookie requestHeaderFieldsWithCookies:cookieArray];
227 | if ([cookieDic objectForKey:@"Cookie"]) {
228 | return cookieDic[@"Cookie"];
229 | }
230 | }
231 | return nil;
232 | }
233 |
234 | - (NSArray *)searchAppropriateCookies:(NSURL *)URL {
235 | NSMutableArray *cookieArray = [NSMutableArray array];
236 | NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
237 | for (NSHTTPCookie *cookie in [cookieStorage cookies]) {
238 | if (cookieFilter(cookie, URL)) {
239 | NSLog(@"Search an appropriate cookie: %@", cookie);
240 | [cookieArray addObject:cookie];
241 | }
242 | }
243 | return cookieArray;
244 | }
245 |
246 | - (NSInteger)deleteCookieForURL:(NSURL *)URL {
247 | int delCount = 0;
248 | NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
249 | for (NSHTTPCookie *cookie in [cookieStorage cookies]) {
250 | if (cookieFilter(cookie, URL)) {
251 | NSLog(@"Delete a cookie: %@", cookie);
252 | [cookieStorage deleteCookie:cookie];
253 | delCount++;
254 | }
255 | }
256 | return delCount;
257 | }
258 |
259 | @end
260 | ```
261 |
262 | 使用方法示例:
263 |
264 | 发送请求
265 |
266 | ```Objective-C
267 | WKWebView * webView = [WKWebView new];
268 | NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://xxx.com/login"]];
269 | NSString *value = [[HTTPDNSCookieManager sharedInstance] getRequestCookieHeaderForURL:url];
270 | [request setValue:value forHTTPHeaderField:@"Cookie"];
271 | [webView loadRequest:request];
272 | ```
273 |
274 | 接收处理请求:
275 |
276 | ```Objective-C
277 | NSURLSessionTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
278 | if (!error) {
279 | NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
280 | // 解析 HTTP Response Header,存储cookie
281 | [[HTTPDNSCookieManager sharedInstance] handleHeaderFields:[httpResponse allHeaderFields] forURL:url];
282 | }
283 | }];
284 | [task resume];
285 | ```
286 |
287 |
288 | 通过 `document.cookie` 设置 Cookie 解决后续页面(同域)Ajax、iframe 请求的 Cookie 问题;
289 |
290 | ```Objective-C
291 | WKUserContentController* userContentController = [WKUserContentController new];
292 | WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie = 'skey=skeyValue';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
293 | [userContentController addUserScript:cookieScript];
294 | ```
295 |
296 | ## Cookie包含动态 IP 导致登陆失效问题
297 |
298 |
299 | 关于COOKIE失效的问题,假如客户端登录 session 存在 COOKIE,此时这个域名配置了多个IP,使用域名访问会读对应域名的COOKIE,使用IP访问则去读对应IP的COOKIE,假如前后两次使用同一个域名配置的不同IP访问,会导致COOKIE的登录session失效,
300 |
301 | 如果APP里面的webview页面需要用到系统COOKIE存的登录session,之前APP所有本地网络请求使用域名访问,是可以共用COOKIE的登录session的,但现在本地网络请求使用httpdns后改用IP访问,导致还使用域名访问的webview读不到系统COOKIE存的登录session了(系统COOKIE对应IP了)。IP直连后,服务端返回Cookie包含动态 IP 导致登陆失效。
302 |
303 | 使用IP访问后,服务端返回的cookie也是IP。导致可能使用对应的域名访问,无法使用本地cookie,或者使用隶属于同一个域名的不同IP去访问,cookie也对不上,导致登陆失效,是吧。
304 |
305 | 我这边的思路是这样的,
306 | - 应该得干预cookie的存储,基于域名。
307 | - 根源上,api域名返回单IP
308 |
309 | 第二种思路将失去DNS调度特性,故不考虑。第一种思路更为可行。
310 |
311 | 当每次服务端返回cookie后,在存储前都进行下改造,使用域名替换下IP。
312 | 之后虽然每次网络请求都是使用IP访问,但是host我们都手动改为了域名,这样本地的cookie也是能对得上的。
313 |
314 | 代码演示:
315 |
316 | 在网络请求成功后,或者加载网页成功后,主动将本地的 domain 字段为 IP 的 Cookie 替换 IP 为 host 域名地址。
317 |
318 | ```Objective-C
319 | - (void)updateWKHTTPCookieStoreDomainFromIP:(NSString *)IP toHost:(NSString *)host {
320 | WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
321 | [cookieStroe getAllCookies:^(NSArray * _Nonnull cookies) {
322 | [[cookies copy] enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull cookie, NSUInteger idx, BOOL * _Nonnull stop) {
323 | if ([cookie.domain isEqualToString:IP]) {
324 | NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:cookie.properties];
325 | dict[NSHTTPCookieDomain] = host;
326 | NSHTTPCookie *newCookie = [NSHTTPCookie cookieWithProperties:[dict copy]];
327 | [cookieStroe setCookie:newCookie completionHandler:^{
328 | [self logCookies];
329 | //FIXME: `-[WKHTTPCookieStore deleteCookie:]` 在 iOS11-beta3 中依然有bug,不会执行。(后续正式版修复后,再更新该注视。)
330 | [cookieStroe deleteCookie:cookie
331 | completionHandler:^{
332 | [self logCookies];
333 | }];
334 | }];
335 | }
336 | }];
337 | }];
338 | }
339 | ```
340 |
341 |
342 | iOS11中也提供了对应的 API 供我们来处理替换 Cookie 的时机,那就是下面的API:
343 |
344 | ```Objective-C
345 | @protocol WKHTTPCookieStoreObserver
346 | @optional
347 | - (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore;
348 | @end
349 | ```
350 |
351 |
352 | ```Objective-C
353 | //WKHTTPCookieStore
354 | /*! @abstract Adds a WKHTTPCookieStoreObserver object with the cookie store.
355 | @param observer The observer object to add.
356 | @discussion The observer is not retained by the receiver. It is your responsibility
357 | to unregister the observer before it becomes invalid.
358 | */
359 | - (void)addObserver:(id)observer;
360 |
361 | /*! @abstract Removes a WKHTTPCookieStoreObserver object from the cookie store.
362 | @param observer The observer to remove.
363 | */
364 | - (void)removeObserver:(id)observer;
365 | ```
366 |
367 | 用法如下:
368 |
369 | ```Objective-C
370 | @interface WebViewController ()
371 | - (void)viewDidLoad {
372 | [super viewDidLoad];
373 | [NSURLProtocol registerClass:[WebViewURLProtocol class]];
374 | NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
375 | [cookieStorage setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways];
376 | WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
377 | [cookieStroe addObserver:self];
378 |
379 | [self.view addSubview:self.webView];
380 | //... ...
381 | }
382 |
383 | #pragma mark -
384 | #pragma mark - WKHTTPCookieStoreObserver Delegate Method
385 |
386 | - (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore {
387 | [self updateWKHTTPCookieStoreDomainFromIP:CYLIP toHost:CYLHOST];
388 | }
389 | ```
390 |
391 | `-updateWKHTTPCookieStoreDomainFromIP` 方法的实现,在上文已经给出。
392 |
393 | //TODO: 这个思路正在完善中... ...
394 |
395 | **相关的文章:**
396 |
397 | - [《WKWebView 那些坑》]( https://zhuanlan.zhihu.com/p/24990222 )
398 | - [《HTTPDNS域名解析场景下如何使用Cookie?》]( https://yq.aliyun.com/articles/64356 )
399 |
400 |
--------------------------------------------------------------------------------
/Tips/DNS污染方案调研/iOS防DNS污染方案调研---SNI业务场景.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - [iOS 防 DNS 污染方案调研--- SNI 业务场景](#ios-%E9%98%B2-dns-%E6%B1%A1%E6%9F%93%E6%96%B9%E6%A1%88%E8%B0%83%E7%A0%94----sni-%E4%B8%9A%E5%8A%A1%E5%9C%BA%E6%99%AF)
5 | - [概述](#%E6%A6%82%E8%BF%B0)
6 | - [基于 CFNetWork 有性能瓶颈](#%E5%9F%BA%E4%BA%8E-cfnetwork-%E6%9C%89%E6%80%A7%E8%83%BD%E7%93%B6%E9%A2%88)
7 | - [调研性能瓶颈的原因](#%E8%B0%83%E7%A0%94%E6%80%A7%E8%83%BD%E7%93%B6%E9%A2%88%E7%9A%84%E5%8E%9F%E5%9B%A0)
8 | - [调研性能瓶颈的方法](#%E8%B0%83%E7%A0%94%E6%80%A7%E8%83%BD%E7%93%B6%E9%A2%88%E7%9A%84%E6%96%B9%E6%B3%95)
9 | - [能瓶颈原因](#%E8%83%BD%E7%93%B6%E9%A2%88%E5%8E%9F%E5%9B%A0)
10 | - [Body 放入 Header 导致请求超时](#body-%E6%94%BE%E5%85%A5-header-%E5%AF%BC%E8%87%B4%E8%AF%B7%E6%B1%82%E8%B6%85%E6%97%B6)
11 | - [换用其他提供了SNI字段配置接口的更底层网络库](#%E6%8D%A2%E7%94%A8%E5%85%B6%E4%BB%96%E6%8F%90%E4%BE%9B%E4%BA%86sni%E5%AD%97%E6%AE%B5%E9%85%8D%E7%BD%AE%E6%8E%A5%E5%8F%A3%E7%9A%84%E6%9B%B4%E5%BA%95%E5%B1%82%E7%BD%91%E7%BB%9C%E5%BA%93)
12 | - [iOS CURL 库](#ios-curl-%E5%BA%93)
13 | - [走过的弯路](#%E8%B5%B0%E8%BF%87%E7%9A%84%E5%BC%AF%E8%B7%AF)
14 | - [误以为 iOS11 新 API 可以直接拦截 DNS 解析过程](#%E8%AF%AF%E4%BB%A5%E4%B8%BA-ios11-%E6%96%B0-api-%E5%8F%AF%E4%BB%A5%E7%9B%B4%E6%8E%A5%E6%8B%A6%E6%88%AA-dns-%E8%A7%A3%E6%9E%90%E8%BF%87%E7%A8%8B)
15 | - [参考链接:](#%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5)
16 |
17 |
18 |
19 | # iOS 防 DNS 污染方案调研--- SNI 业务场景
20 |
21 | ## 概述
22 |
23 | SNI(单IP多HTTPS证书)场景下,iOS上层网络库 `NSURLConnection/NSURLSession` 没有提供接口进行 `SNI 字段` 配置,因此需要 Socket 层级的底层网络库例如 `CFNetwork`,来实现 `IP 直连网络请求`适配方案。而基于 CFNetwork 的解决方案需要开发者考虑数据的收发、重定向、解码、缓存等问题(CFNetwork是非常底层的网络实现)。
24 |
25 | 针对 SNI 场景的方案, Socket 层级的底层网络库,大致有两种:
26 |
27 | - 基于 CFNetWork ,hook 证书校验步骤。
28 | - 基于原生支持设置 SNI 字段的更底层的库,比如 libcurl。
29 |
30 |
31 | 下面将目前面临的一些挑战,以及应对策略介绍一下:
32 |
33 | ## 基于 CFNetWork 有性能瓶颈
34 |
35 | 方案:
36 |
37 | 1. 调研性能瓶颈的原因
38 | 2. 换用其他提供了SNI字段配置接口的更底层网络库。
39 |
40 | ### 调研性能瓶颈的原因
41 |
42 | 在使用 CFNetWork 实现了基本的SNI解决方案后,虽然问题解决了,但是遇到了性能瓶颈,对比 `NSURLConnection/NSURLSession` ,打开流到结束流时间明显更长。介绍下对比性能时的调研方法:
43 |
44 | /*one more thing*/
45 |
46 |
53 |
54 | #### 调研性能瓶颈的方法
55 |
56 |
57 | 可以使用下面的方法,做一个简单的打点,将流开始和流结束记录下。
58 |
59 | 记录的数据如下:
60 |
61 | key | from | to | vule
62 | -------|------|-------|------
63 | 请求的序列号 | 开始时间戳 | 结束时间戳 | 耗时
64 |
65 |
66 | ```Objective-C
67 | #import
68 |
69 | @interface CYLRequestTimeMonitor : NSObject
70 |
71 | + (NSString *)requestBeginTimeKeyWithID:(NSUInteger)ID;
72 | + (NSString *)requestEndTimeKeyWithID:(NSUInteger)ID;
73 | + (NSString *)requestSpentTimeKeyWithID:(NSUInteger)ID;
74 | + (NSString *)getKey:(NSString *)key ID:(NSUInteger)ID;
75 | + (NSUInteger)timeFromKey:(NSString *)key;
76 | + (NSUInteger)frontRequetNumber;
77 | + (NSUInteger)changeToNextRequetNumber;
78 | + (void)setCurrentTimeForKey:(NSString *)key taskID:(NSUInteger)taskID time:(NSTimeInterval *)time;
79 | + (void)setTime:(NSUInteger)time key:(NSString *)key taskID:(NSUInteger)taskID;
80 |
81 | + (void)setBeginTimeForTaskID:(NSUInteger)taskID;
82 | + (void)setEndTimeForTaskID:(NSUInteger)taskID;
83 | + (void)setSpentTimeForKey:(NSString *)key endTime:(NSUInteger)endTime taskID:(NSUInteger)taskID;
84 |
85 | @end
86 | ```
87 |
88 |
89 | ```Objective-C
90 | #import "CYLRequestTimeMonitor.h"
91 |
92 | @implementation CYLRequestTimeMonitor
93 |
94 | static NSString *const CYLRequestFrontNumber = @"CYLRequestFrontNumber";
95 | static NSString *const CYLRequestBeginTime = @"CYLRequestBeginTime";
96 | static NSString *const CYLRequestEndTime = @"CYLRequestEndTime";
97 | static NSString *const CYLRequestSpentTime = @"CYLRequestSpentTime";
98 |
99 | + (NSString *)requestBeginTimeKeyWithID:(NSUInteger)ID {
100 | return [self getKey:CYLRequestBeginTime ID:ID];
101 | }
102 |
103 | + (NSString *)requestEndTimeKeyWithID:(NSUInteger)ID {
104 | return [self getKey:CYLRequestEndTime ID:ID];
105 | }
106 |
107 | + (NSString *)requestSpentTimeKeyWithID:(NSUInteger)ID {
108 | return [self getKey:CYLRequestSpentTime ID:ID];
109 | }
110 |
111 | + (NSString *)getKey:(NSString *)key ID:(NSUInteger)ID {
112 | NSString *timeKeyWithID = [NSString stringWithFormat:@"%@-%@", @(ID), key];
113 | return timeKeyWithID;
114 | }
115 |
116 | + (NSUInteger)timeFromKey:(NSString *)key {
117 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
118 | NSUInteger time = [defaults integerForKey:key];
119 | return time ?: 0;
120 | }
121 |
122 | + (NSUInteger)frontRequetNumber {
123 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
124 | NSUInteger frontNumber = [defaults integerForKey:CYLRequestFrontNumber];
125 | return frontNumber ?: 0;
126 | }
127 |
128 | + (NSUInteger)changeToNextRequetNumber {
129 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
130 | NSUInteger nextNumber = ([self frontRequetNumber]+ 1);
131 | [defaults setInteger:nextNumber forKey:CYLRequestFrontNumber];
132 | [defaults synchronize];
133 | return nextNumber;
134 | }
135 |
136 | + (void)setCurrentTimeForKey:(NSString *)key taskID:(NSUInteger)taskID time:(NSTimeInterval *)time {
137 | NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970]*1000;
138 | *time = currentTime;
139 | [self setTime:currentTime key:key taskID:taskID];
140 | }
141 |
142 | + (void)setTime:(NSUInteger)time key:(NSString *)key taskID:(NSUInteger)taskID {
143 | NSString *keyWithID = [self getKey:key ID:taskID];
144 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
145 | [defaults setInteger:time forKey:keyWithID];
146 | [defaults synchronize];
147 | }
148 |
149 | + (void)setBeginTimeForTaskID:(NSUInteger)taskID {
150 | NSTimeInterval begin;
151 | [self setCurrentTimeForKey:CYLRequestBeginTime taskID:taskID time:&begin];
152 | }
153 |
154 | + (void)setEndTimeForTaskID:(NSUInteger)taskID {
155 | NSTimeInterval endTime = 0;
156 | [self setCurrentTimeForKey:CYLRequestEndTime taskID:taskID time:&endTime];
157 | [self setSpentTimeForKey:CYLRequestSpentTime endTime:endTime taskID:taskID];
158 | }
159 |
160 | + (void)setSpentTimeForKey:(NSString *)key endTime:(NSUInteger)endTime taskID:(NSUInteger)taskID {
161 | NSString *beginTimeString = [self requestBeginTimeKeyWithID:taskID];
162 | NSUInteger beginTime = [self timeFromKey:beginTimeString];
163 | NSUInteger spentTime = endTime - beginTime;
164 | [self setTime:spentTime key:CYLRequestSpentTime taskID:taskID];
165 | }
166 |
167 | @end
168 |
169 | ```
170 |
171 | NSURLConnection 的打点位置如下:
172 |
173 | ```Objective-C
174 | 这里普通的做法就是继承NSURLProtocol 这个类写一个子类,然后在子类中实现NSURLConnectionDelegate 的那五个代理方法。
175 |
176 | - (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response
177 | // 这个方法里可以做计时的开始
178 |
179 | - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
180 | // 这里可以得到返回包的总大小
181 |
182 | - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
183 | // 这里将每次的data累加起来,可以做加载进度圆环之类的
184 |
185 | - (void)connectionDidFinishLoading:(NSURLConnection *)connection
186 | // 这里作为结束的时间
187 |
188 | - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
189 | // 错误的收集
190 | ```
191 |
192 | NSURLSession 类似。
193 |
194 | 然后在自定义CFNetwork的下面两个方法中打点:流开始和流结束,命名大致如:`-startLoading`、`-didReceiveRedirection`。
195 |
196 | 发送相同的网络请求,然后通过对比两个的时间来观察性能。
197 |
198 | ### 能瓶颈原因
199 |
200 | #### Body 放入 Header 导致请求超时
201 |
202 | 使用 NSURLProtocol 拦截 NSURLSession 请求丢失 body,故有以下几种解决方法:
203 |
204 |
205 | 方案如下:
206 |
207 | 1. 换用 NSURLConnection
208 | 2. 将 body 放进 Header 中
209 | 3. 使用 HTTPBodyStream 获取 body,并赋值到 body 中
210 |
211 | 其中换用 NSURLConnection 这种方法,不用多少了,终究会被淘汰。不考虑。
212 |
213 | 其中body放header的方法,2M以下没问题,超过2M会导致请求延迟,超过 10M 就直接 Request timeout。而且无法解决 Body 为二进制数据的问题,因为Header里都是文本数据,
214 |
215 | 另一种方法是使用 HTTPBodyStream 获取 body,并赋值到 body 中,具体的代码如下,可以解决上面提到的问题:
216 |
217 | ```Objective-C
218 | //
219 | // NSURLRequest+CYLNSURLProtocolExtension.h
220 | //
221 | //
222 | // Created by ElonChan on 28/07/2017.
223 | // Copyright © 2017 ChenYilong. All rights reserved.
224 | //
225 |
226 | #import
227 |
228 | @interface NSURLRequest (CYLNSURLProtocolExtension)
229 |
230 | - (NSMutableURLRequest *)cyl_getPostRequestIncludeBody;
231 |
232 | @end
233 |
234 |
235 | @interface NSMutableURLRequest (CYLNSURLProtocolExtension)
236 |
237 | - (void)cyl_handlePostRequestBody;
238 |
239 | @end
240 |
241 |
242 | //
243 | // NSURLRequest+CYLNSURLProtocolExtension.h
244 | //
245 | //
246 | // Created by ElonChan on 28/07/2017.
247 | // Copyright © 2017 ChenYilong. All rights reserved.
248 | //
249 |
250 | #import "NSURLRequest+CYLNSURLProtocolExtension.h"
251 |
252 | @implementation NSURLRequest (CYLNSURLProtocolExtension)
253 |
254 | - (NSMutableURLRequest *)cyl_getPostRequestIncludeBody {
255 | NSMutableURLRequest * req = [self mutableCopy];
256 | if ([self.HTTPMethod isEqualToString:@"POST"]) {
257 | if (!self.HTTPBody) {
258 | uint8_t d[1024] = {0};
259 | NSInputStream *stream = self.HTTPBodyStream;
260 | NSMutableData *data = [[NSMutableData alloc] init];
261 | [stream open];
262 | while ([stream hasBytesAvailable]) {
263 | NSInteger len = [stream read:d maxLength:1024];
264 | if (len > 0 && stream.streamError == nil) {
265 | [data appendBytes:(void *)d length:len];
266 | }
267 | }
268 | req.HTTPBody = [data copy];
269 | [stream close];
270 | }
271 | }
272 | return req;
273 | }
274 |
275 | @end
276 |
277 | @implementation NSMutableURLRequest (CYLNSURLProtocolExtension)
278 |
279 | - (void)cyl_handlePostRequestBody {
280 | if ([self.HTTPMethod isEqualToString:@"POST"]) {
281 | if (!self.HTTPBody) {
282 | uint8_t d[1024] = {0};
283 | NSInputStream *stream = self.HTTPBodyStream;
284 | NSMutableData *data = [[NSMutableData alloc] init];
285 | [stream open];
286 | while ([stream hasBytesAvailable]) {
287 | NSInteger len = [stream read:d maxLength:1024];
288 | if (len > 0 && stream.streamError == nil) {
289 | [data appendBytes:(void *)d length:len];
290 | }
291 | }
292 | self.HTTPBody = [data copy];
293 | [stream close];
294 | }
295 | }
296 | }
297 |
298 | @end
299 |
300 | ```
301 |
302 | 使用方法:
303 |
304 | 在用于拦截请求的 NSURLProtocol 的子类中实现方法 `-[NSURLProtocol startLoading]`,并处理 `request` 对象。
305 |
306 |
307 | ```Objective-C
308 |
309 | /**
310 | * 开始加载,在该方法中,加载一个请求
311 | */
312 | - (void)startLoading {
313 | NSMutableURLRequest *request = [self.request mutableCopy];
314 | [request cyl_handlePostRequestBody];
315 | // 表示该请求已经被处理,防止无限循环
316 | [NSURLProtocol setProperty:@(YES) forKey:CYL_NSURLPROTOCOL_REQUESTED_FLAG_KEY inRequest:request];
317 | curRequest = request;
318 | [self startRequest];
319 | }
320 | ```
321 |
322 | 注意在拦截 `NSURLSession` 请求时,需要将用于拦截请求的 NSURLProtocol 的子类添加到 `NSURLSessionConfiguration` 中,用法如下:
323 |
324 | ```Objective-C
325 | NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
326 | NSArray *protocolArray = @[ [CYLURLProtocol class] ];
327 | configuration.protocolClasses = protocolArray;
328 | NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
329 | ```
330 |
331 | ## 换用其他提供了SNI字段配置接口的更底层网络库
332 |
333 | 如果使用第三方网络库:curl, 中有一个 `-resolve` 方法可以实现使用指定 ip 访问 https 网站,iOS 中集成 curl 库,参考 [curl文档](https://curl.haxx.se/libcurl/c/CURLOPT_RESOLVE.html) ;
334 |
335 | 另外有一点也可以注意下,它也是支持 IPv6 环境的,只需要你在 build 时添加上 `--enable-ipv6` 即可。
336 |
337 | curl 支持指定 SNI 字段,设置 SNI 时我们需要构造的参数形如: `{HTTPS域名}:443:{IP地址}`
338 |
339 | 假设你要访问. www.example.org ,若IP为 127.0.0.1 ,那么通过这个方式来调用来设置 SNI 即可:
340 |
341 | > curl --resolve 'www.example.org:443:127.0.0.1'
342 |
343 | ### iOS CURL 库
344 |
345 | 使用[libcurl](https://curl.haxx.se/libcurl/c/) 来解决,`curl` 中有一个 `-resolve` 方法可以实现使用指定ip访问https网站。
346 |
347 | 在iOS实现中,代码如下
348 |
349 | ```Objective-C
350 | //{HTTPS域名}:443:{IP地址}
351 | NSString *curlHost = ...;
352 | _hosts_list = curl_slist_append(_hosts_list, curlHost.UTF8String);
353 | curl_easy_setopt(_curl, CURLOPT_RESOLVE, _hosts_list);
354 | ```
355 |
356 | 其中 `curlHost` 形如:
357 |
358 | `{HTTPS域名}:443:{IP地址}`
359 |
360 | `_hosts_list` 是结构体类型`hosts_list`,可以设置多个IP与Host之间的映射关系。`curl_easy_setopt`方法中传入`CURLOPT_RESOLVE` 将该映射设置到 HTTPS 请求中。
361 |
362 | 这样就可以达到设置SNI的目的。
363 |
364 | 我在这里写了一个 Demo:[CYLCURLNetworking](https://github.com/ChenYilong/CYLCURLNetworking),里面包含了编译好的支持 IPv6 的 libcurl 包,演示了下如何通过curl来进行类似NSURLSession。
365 |
366 | ## 走过的弯路
367 |
368 | ## 误以为 iOS11 新 API 可以直接拦截 DNS 解析过程
369 |
370 | 参考:[NEDNSProxyProvider:DNS based on HTTP supported in iOS11](https://github.com/ChenYilong/iOS11AdaptationTips/issues/12)
371 |
372 | ## 参考链接:
373 |
374 | - [Apple - Communicating with HTTP Servers](https://developer.apple.com/library/content/documentation/Networking/Conceptual/CFNetwork/CFHTTPTasks/CFHTTPTasks.html?spm=5176.doc30143.2.3.5016q8)
375 | - [Apple - HTTPS Server Trust Evaluation - Server Name Failures ](https://developer.apple.com/library/content/technotes/tn2232/_index.html?spm=5176.doc30143.2.4.5016q8#//apple_ref/doc/uid/DTS40012884-CH1-SECSERVERNAME)
376 | - [Apple - HTTPS Server Trust Evaluation - Trusting One Specific Certificate ](https://developer.apple.com/library/content/technotes/tn2232/_index.html?spm=5176.doc30143.2.5.5016q8#//apple_ref/doc/uid/DTS40012884-CH1-SECCUSTOMCERT)
377 | - [《HTTPDNS > 最佳实践 > HTTPS(含SNI)业务场景“IP直连”方案说明
378 | HTTPS(含SNI)业务场景“IP直连”方案说明》]( https://help.aliyun.com/document_detail/30143.html?spm=5176.doc30141.6.591.A8B1d3 )
379 | - [《在 curl 中使用指定 ip 来进行请求 https》]( https://blog.mozcp.com/curl-request-https-specify-ip/ )
380 | - [支持SNI与WebView的 alicloud-ios-demo](https://github.com/Dave1991/alicloud-ios-demo)
381 |
382 |
--------------------------------------------------------------------------------
/Tips/DNS污染方案调研/iOS防DNS污染方案调研---WebView业务场景.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - [iOS 防 DNS 污染方案调研--- WebView 业务场景](#ios-%E9%98%B2-dns-%E6%B1%A1%E6%9F%93%E6%96%B9%E6%A1%88%E8%B0%83%E7%A0%94----webview-%E4%B8%9A%E5%8A%A1%E5%9C%BA%E6%99%AF)
5 | - [概述](#%E6%A6%82%E8%BF%B0)
6 | - [面临的问题](#%E9%9D%A2%E4%B8%B4%E7%9A%84%E9%97%AE%E9%A2%98)
7 | - [WKWebView 无法使用 NSURLProtocol 拦截请求](#wkwebview-%E6%97%A0%E6%B3%95%E4%BD%BF%E7%94%A8-nsurlprotocol-%E6%8B%A6%E6%88%AA%E8%AF%B7%E6%B1%82)
8 | - [使用 NSURLProtocol 拦截 NSURLSession 请求丢失 body](#%E4%BD%BF%E7%94%A8-nsurlprotocol-%E6%8B%A6%E6%88%AA-nsurlsession-%E8%AF%B7%E6%B1%82%E4%B8%A2%E5%A4%B1-body)
9 | - [302重定向问题](#302%E9%87%8D%E5%AE%9A%E5%90%91%E9%97%AE%E9%A2%98)
10 | - [Cookie相关问题](#cookie%E7%9B%B8%E5%85%B3%E9%97%AE%E9%A2%98)
11 | - [参考链接](#%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5)
12 | - [走过的弯路](#%E8%B5%B0%E8%BF%87%E7%9A%84%E5%BC%AF%E8%B7%AF)
13 | - [误以为 iOS11 新 API 可以原生拦截 WKWebView 的 HTTP/HTTPS 网络请求](#%E8%AF%AF%E4%BB%A5%E4%B8%BA-ios11-%E6%96%B0-api-%E5%8F%AF%E4%BB%A5%E5%8E%9F%E7%94%9F%E6%8B%A6%E6%88%AA-wkwebview-%E7%9A%84-httphttps-%E7%BD%91%E7%BB%9C%E8%AF%B7%E6%B1%82)
14 |
15 |
16 |
17 | # iOS 防 DNS 污染方案调研--- WebView 业务场景
18 |
19 | ## 概述
20 |
21 | 为什么 WebView 需要特别适配SNI?
22 |
23 |
24 | 因为形如 http://1.1.1.1/a/b.com 在 WebView 中是无法正常访问的,也是需要修改HOST,所以需要使用 NSURLProtocol 来 hook 网络请求,而且 HTTPS+SNI 场景是非常场景的。
25 |
26 | WebView的IP直连方案,基本的思路是接管网络请求,随之就会面临到一些重定向、cookie等问题。下面对这些问题做下记录、总结。
27 |
28 | ## 面临的问题
29 |
30 | ### WKWebView 无法使用 NSURLProtocol 拦截请求
31 |
32 | 方案如下:
33 |
34 |
35 | 1. 换用 UIWebView
36 | 2. 使用私有API进行注册拦截
37 |
38 | 换用 UIWebView 方案不做赘述,说明下使用私有API进行注册拦截的方法:
39 |
40 | ```Objective-C
41 | //注册自己的protocol
42 | [NSURLProtocol registerClass:[CustomProtocol class]];
43 |
44 | //创建WKWebview
45 | WKWebViewConfiguration * config = [[WKWebViewConfiguration alloc] init];
46 | WKWebView * wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height) configuration:config];
47 | [wkWebView loadRequest:webViewReq];
48 | [self.view addSubview:wkWebView];
49 |
50 | //注册scheme
51 | Class cls = NSClassFromString(@"WKBrowsingContextController");
52 | SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
53 | if ([cls respondsToSelector:sel]) {
54 | // 通过http和https的请求,同理可通过其他的Scheme 但是要满足ULR Loading System
55 | [cls performSelector:sel withObject:@"http"];
56 | [cls performSelector:sel withObject:@"https"];
57 | }
58 |
59 | ```
60 | 注意避免执行太晚,如果在 `- (void)viewDidLoad` 中注册,可能会因为注册太晚,引发问题。建议在`+load`方法中执行。
61 |
62 | 然后同样会遇到 [《iOS SNI 场景下的防 DNS 污染方案调研》](https://github.com/ChenYilong/iOSBlog/issues/12) 里提到的各种 NSURLProtocol 相关的问题,可以参照里面的方法解决。
63 |
64 | ### 使用 NSURLProtocol 拦截 NSURLSession 请求丢失 body
65 |
66 | 方案如下:
67 |
68 | 1. 换用 NSURLConnection
69 | 2. 将 body 放进 Header 中
70 | 3. 使用 HTTPBodyStream 获取 body,并赋值到 body 中
71 |
72 | ```Objective-C
73 | //处理POST请求相关POST 用HTTPBodyStream来处理BODY体
74 | - (void)handlePostRequestBody {
75 | if ([self.HTTPMethod isEqualToString:@"POST"]) {
76 | if (!self.HTTPBody) {
77 | uint8_t d[1024] = {0};
78 | NSInputStream *stream = self.HTTPBodyStream;
79 | NSMutableData *data = [[NSMutableData alloc] init];
80 | [stream open];
81 | while ([stream hasBytesAvailable]) {
82 | NSInteger len = [stream read:d maxLength:1024];
83 | if (len > 0 && stream.streamError == nil) {
84 | [data appendBytes:(void *)d length:len];
85 | }
86 | }
87 | self.HTTPBody = [data copy];
88 | [stream close];
89 | }
90 | }
91 | }
92 |
93 | ```
94 |
95 | 使用 `-[WKWebView loadRequest]` 同样会遇到该问题,按照同样的方法修改。
96 |
97 |
98 |
99 | ## 302重定向问题
100 |
101 | 上面提到的 Cookie 方案无法解决302请求的 Cookie 问题,比如,第一个请求是 http://www.a.com ,我们通过在 request header 里带上 Cookie 解决该请求的 Cookie 问题,接着页面302跳转到 http://www.b.com ,这个时候 http://www.b.com 这个请求就可能因为没有携带 cookie 而无法访问。当然,由于每一次页面跳转前都会调用回调函数:
102 |
103 | ```Objective-C
104 | - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
105 | ```
106 |
107 | 可以在该回调函数里拦截302请求,copy request,在 request header 中带上 cookie 并重新 loadRequest。不过这种方法依然解决不了页面 iframe 跨域请求的 Cookie 问题,毕竟-[WKWebView loadRequest:]只适合加载 mainFrame 请求。
108 |
109 |
110 | ## Cookie相关问题
111 |
112 | 单独成篇: [《防 DNS 污染方案调研---iOS HTTPS(含SNI) 业务场景(四)-- Cookie 场景》]( https://github.com/ChenYilong/iOSBlog/issues/14 )
113 |
114 |
115 | ## 参考链接
116 |
117 |
118 | **相关的库:**
119 |
120 | - [GitHub:WebViewProxy](https://github.com/marcuswestin/WebViewProxy)
121 | - [GitHub:NSURLProtocol-WebKitSupport](https://github.com/Yeatse/NSURLProtocol-WebKitSupport)
122 | - [GitHub:happy-dns-objc](https://github.com/qiniu/happy-dns-objc)
123 | - [Chrome For iOS ](https://chromium.googlesource.com/chromium/src.git/+/master/ios/)
124 |
125 | **相关的文章:**
126 |
127 | - [《NSURLProtocol对WKWebView的处理》]( http://www.jianshu.com/p/8f5e1082f5e0 )
128 | - [《可能是最全的iOS端HttpDns集成方案》]( http://www.jianshu.com/p/cd4c1bf1fd5f )
129 | - [《WKWebView 那些坑》]( https://zhuanlan.zhihu.com/p/24990222 )
130 |
131 |
132 | **可以参考的Demo:**
133 |
134 | - [支持SNI与WebView的 alicloud-ios-demo](https://github.com/Dave1991/alicloud-ios-demo)
135 | - [HybirdWKWebVIew](https://github.com/LiuShuoyu/HybirdWKWebVIew/)
136 | - [《WWDC 2017-WKWebView 新功能》]( https://zhuanlan.zhihu.com/p/27914128 )
137 |
138 | ## 走过的弯路
139 |
140 | ## 误以为 iOS11 新 API 可以原生拦截 WKWebView 的 HTTP/HTTPS 网络请求
141 |
142 | 参考:[Deal With WKWebView DNS pollution problem in iOS11](https://github.com/ChenYilong/iOS11AdaptationTips/issues/16)
143 |
144 |
--------------------------------------------------------------------------------
/Tips/HTTP状态码汇总.m:
--------------------------------------------------------------------------------
1 | 1xx消息
2 |
3 | 这一类型的状态码,代表请求已被接受,需要继续处理。这类响应是临时响应,只包含状态行和某些可选的响应头信息,并以空行结束。由于HTTP/1.0协议中没有定义任何1xx状态码,所以除非在某些试验条件下,服务器禁止向此类客户端发送1xx响应。 这些状态码代表的响应都是信息性的,标示客户应该采取的其他行动。
4 |
5 | 100 Continue
6 | 客户端应当继续发送请求。这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。服务器必须在请求完成后向客户端发送一个最终响应。
7 | 101 Switching Protocols
8 | 服务器已经理解了客户端的请求,并将通过Upgrade消息头通知客户端采用不同的协议来完成这个请求。在发送完这个响应最后的空行后,服务器将会切换到在Upgrade消息头中定义的那些协议。: 只有在切换新的协议更有好处的时候才应该采取类似措施。例如,切换到新的HTTP版本比旧版本更有优势,或者切换到一个实时且同步的协议以传送利用此类特性的资源。
9 | 102 Processing
10 | 由WebDAV(RFC 2518)扩展的状态码,代表处理将被继续执行。
11 |
12 | 2xx成功
13 |
14 | 这一类型的状态码,代表请求已成功被服务器接收、理解、并接受。
15 |
16 | 200 OK
17 | 请求已成功,请求所希望的响应头或数据体将随此响应返回。
18 | 201 Created
19 | 请求已经被实现,而且有一个新的资源已经依据请求的需要而创建,且其URI已经随Location头信息返回。假如需要的资源无法及时创建的话,应当返回'202 Accepted'。
20 | 202 Accepted
21 | 服务器已接受请求,但尚未处理。正如它可能被拒绝一样,最终该请求可能会也可能不会被执行。在异步操作的场合下,没有比发送这个状态码更方便的做法了。:返回202状态码的响应的目的是允许服务器接受其他过程的请求(例如某个每天只执行一次的基于批处理的操作),而不必让客户端一直保持与服务器的连接直到批处理操作全部完成。在接受请求处理并返回202状态码的响应应当在返回的实体中包含一些指示处理当前状态的信息,以及指向处理状态监视器或状态预测的指针,以便用户能够估计操作是否已经完成。
22 | 203 Non-Authoritative Information
23 | 服务器已成功处理了请求,但返回的实体头部元信息不是在原始服务器上有效的确定集合,而是来自本地或者第三方的拷贝。当前的信息可能是原始版本的子集或者超集。例如,包含资源的元数据可能导致原始服务器知道元信息的超集。使用此状态码不是必须的,而且只有在响应不使用此状态码便会返回200 OK的情况下才是合适的。
24 | 204 No Content
25 | 服务器成功处理了请求,但不需要返回任何实体内容,并且希望返回更新了的元信息。响应可能通过实体头部的形式,返回新的或更新后的元信息。如果存在这些头部信息,则应当与所请求的变量相呼应。
26 | 如果客户端是浏览器的话,那么用户浏览器应保留发送了该请求的页面,而不产生任何文档视图上的变化,即使按照规范新的或更新后的元信息应当被应用到用户浏览器活动视图中的文档。
27 | 由于204响应被禁止包含任何消息体,因此它始终以消息头后的第一个空行结尾。
28 | 205 Reset Content
29 | 服务器成功处理了请求,且没有返回任何内容。但是与204响应不同,返回此状态码的响应要求请求者重置文档视图。该响应主要是被用于接受用户输入后,立即重置表单,以便用户能够轻松地开始另一次输入。
30 | 与204响应一样,该响应也被禁止包含任何消息体,且以消息头后的第一个空行结束。
31 | 206 Partial Content
32 | 服务器已经成功处理了部分GET请求。类似于FlashGet或者迅雷这类的HTTP 下载工具都是使用此类响应实现断点续传或者将一个大文档分解为多个下载段同时下载。
33 | 该请求必须包含Range头信息来指示客户端希望得到的内容范围,并且可能包含If-Range来作为请求条件。
34 | 响应必须包含如下的头部域:
35 |
36 | Content-Range用以指示本次响应中返回的内容的范围;如果是Content-Type为multipart/byteranges的多段下载,则每一multipart段中都应包含Content-Range域用以指示本段的内容范围。假如响应中包含Content-Length,那么它的数值必须匹配它返回的内容范围的真实字节数。
37 | Date
38 | ETag和/或Content-Location,假如同样的请求本应该返回200响应。
39 | Expires, Cache-Control,和/或Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。
40 |
41 | 假如本响应请求使用了If-Range强缓存验证,那么本次响应不应该包含其他实体头;假如本响应的请求使用了If-Range弱缓存验证,那么本次响应禁止包含其他实体头;这避免了缓存的实体内容和更新了的实体头信息之间的不一致。否则,本响应就应当包含所有本应该返回200响应中应当返回的所有实体头部域。
42 | 假如ETag或Last-Modified头部不能精确匹配的话,则客户端缓存应禁止将206响应返回的内容与之前任何缓存过的内容组合在一起。
43 | 任何不支持Range以及Content-Range头的缓存都禁止缓存206响应返回的内容。
44 | 207 Multi-Status
45 | 由WebDAV(RFC 2518)扩展的状态码,代表之后的消息体将是一个XML消息,并且可能依照之前子请求数量的不同,包含一系列独立的响应代码。
46 |
47 | 3xx重定向
48 |
49 | 这类状态码代表需要客户端采取进一步的操作才能完成请求。通常,这些状态码用来重定向,后续的请求地址(重定向目标)在本次响应的Location域中指明。
50 |
51 | 当且仅当后续的请求所使用的方法是GET或者HEAD时,用户浏览器才可以在没有用户介入的情况下自动提交所需要的后续请求。客户端应当自动监测无限循环重定向(例如:A→B→C→……→A或A→A),因为这会导致服务器和客户端大量不必要的资源消耗。按照HTTP/1.0版规范的建议,浏览器不应自动访问超过5次的重定向。
52 |
53 | 300 Multiple Choices
54 | 被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向。
55 | 除非这是一个HEAD请求,否则该响应应当包括一个资源特性及地址的列表的实体,以便用户或浏览器从中选择最合适的重定向地址。这个实体的格式由Content-Type定义的格式所决定。浏览器可能根据响应的格式以及浏览器自身能力,自动作出最合适的选择。当然,RFC 2616规范并没有规定这样的自动选择该如何进行。
56 | 如果服务器本身已经有了首选的回馈选择,那么在Location中应当指明这个回馈的URI;浏览器可能会将这个Location值作为自动重定向的地址。此外,除非额外指定,否则这个响应也是可缓存的。
57 | 301 Moved Permanently
58 | 被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个URI之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。除非额外指定,否则这个响应也是可缓存的。
59 | 新的永久性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。
60 | 如果这不是一个GET或者HEAD请求,因此浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。
61 | 注意:对于某些使用HTTP/1.0协议的浏览器,当它们发送的POST请求得到了一个301响应的话,接下来的重定向请求将会变成GET方式。
62 | 302 Found
63 | 请求的资源现在临时从不同的URI响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。
64 | 新的临时性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。
65 | 如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。
66 | 注意:虽然RFC 1945和RFC 2068规范不允许客户端在重定向时改变请求的方法,但是很多现存的浏览器将302响应视作为303响应,并且使用GET方式访问在Location中规定的URI,而无视原先请求的方法。状态码303和307被添加了进来,用以明确服务器期待客户端进行何种反应。
67 | 303 See Other
68 | 对应当前请求的响应可以在另一个URI上被找到,而且客户端应当采用GET的方式访问那个资源。这个方法的存在主要是为了允许由脚本激活的POST请求输出重定向到一个新的资源。这个新的URI不是原始资源的替代引用。同时,303响应禁止被缓存。当然,第二个请求(重定向)可能被缓存。
69 | 新的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。
70 | 注意:许多HTTP/1.1版以前的浏览器不能正确理解303状态。如果需要考虑与这些浏览器之间的互动,302状态码应该可以胜任,因为大多数的浏览器处理302响应时的方式恰恰就是上述规范要求客户端处理303响应时应当做的。
71 | 304 Not Modified
72 | 如果客户端发送了一个带条件的GET请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个状态码。304响应禁止包含消息体,因此始终以消息头后的第一个空行结尾。
73 | 该响应必须包含以下的头信息:
74 |
75 | Date,除非这个服务器没有时钟。假如没有时钟的服务器也遵守这些规则,那么代理服务器以及客户端可以自行将Date字段添加到接收到的响应头中去(正如RFC 2068中规定的一样),缓存机制将会正常工作。
76 | ETag和/或Content-Location,假如同样的请求本应返回200响应。
77 | Expires, Cache-Control,和/或Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。
78 |
79 | 假如本响应请求使用了强缓存验证,那么本次响应不应该包含其他实体头;否则(例如,某个带条件的GET请求使用了弱缓存验证),本次响应禁止包含其他实体头;这避免了缓存了的实体内容和更新了的实体头信息之间的不一致。
80 | 假如某个304响应指明了当前某个实体没有缓存,那么缓存系统必须忽视这个响应,并且重复发送不包含限制条件的请求。
81 | 假如接收到一个要求更新某个缓存条目的304响应,那么缓存系统必须更新整个条目以反映所有在响应中被更新的字段的值。
82 | 305 Use Proxy
83 | 被请求的资源必须通过指定的代理才能被访问。Location域中将给出指定的代理所在的URI信息,接收者需要重复发送一个单独的请求,通过这个代理才能访问相应资源。只有原始服务器才能创建305响应。
84 | 注意:RFC 2068中没有明确305响应是为了重定向一个单独的请求,而且只能被原始服务器创建。忽视这些限制可能导致严重的安全后果。
85 | 306 Switch Proxy
86 | 在最新版的规范中,306状态码已经不再被使用。
87 | 307 Temporary Redirect
88 | 请求的资源现在临时从不同的URI响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。
89 | 新的临时性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。因为部分浏览器不能识别307响应,因此需要添加上述必要信息以便用户能够理解并向新的URI发出访问请求。
90 | 如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。
91 |
92 | 4xx客户端错误
93 |
94 | 这类的状态码代表了客户端看起来可能发生了错误,妨碍了服务器的处理。除非响应的是一个HEAD请求,否则服务器就应该返回一个解释当前错误状况的实体,以及这是临时的还是永久性的状况。这些状态码适用于任何请求方法。浏览器应当向用户显示任何包含在此类错误响应中的实体内容。
95 |
96 | 如果错误发生时客户端正在传送数据,那么使用TCP的服务器实现应当仔细确保在关闭客户端与服务器之间的连接之前,客户端已经收到了包含错误信息的数据包。如果客户端在收到错误信息后继续向服务器发送数据,服务器的TCP栈将向客户端发送一个重置数据包,以清除该客户端所有还未识别的输入缓冲,以免这些数据被服务器上的应用程序读取并干扰后者。
97 |
98 | 400 Bad Request
99 | 由于包含语法错误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求。
100 | 401 Unauthorized
101 | 当前请求需要用户验证。该响应必须包含一个适用于被请求资源的WWW-Authenticate信息头用以询问用户信息。客户端可以重复提交一个包含恰当的Authorization头信息的请求。如果当前请求已经包含了Authorization证书,那么401响应代表着服务器验证已经拒绝了那些证书。如果401响应包含了与前一个响应相同的身份验证询问,且浏览器已经至少尝试了一次验证,那么浏览器应当向用户展示响应中包含的实体信息,因为这个实体信息中可能包含了相关诊断信息。参见RFC 2617。
102 | 402 Payment Required
103 | 该状态码是为了将来可能的需求而预留的。
104 | 403 Forbidden
105 | 服务器已经理解请求,但是拒绝执行它。与401响应不同的是,身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交。如果这不是一个HEAD请求,而且服务器希望能够讲清楚为何请求不能被执行,那么就应该在实体内描述拒绝的原因。当然服务器也可以返回一个404响应,假如它不希望让客户端获得任何信息。
106 | 404 Not Found
107 | 请求失败,请求所希望得到的资源未被在服务器上发现。没有信息能够告诉用户这个状况到底是暂时的还是永久的。假如服务器知道情况的话,应当使用410状态码来告知旧资源因为某些内部的配置机制问题,已经永久的不可用,而且没有任何可以跳转的地址。404这个状态码被广泛应用于当服务器不想揭示到底为何请求被拒绝或者没有其他适合的响应可用的情况下。
108 | 405 Method Not Allowed
109 | 请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个Allow头信息用以表示出当前资源能够接受的请求方法的列表。
110 | 鉴于PUT,DELETE方法会对服务器上的资源进行写操作,因而绝大部分的网页服务器都不支持或者在默认配置下不允许上述请求方法,对于此类请求均会返回405错误。
111 | 406 Not Acceptable
112 | 请求的资源的内容特性无法满足请求头中的条件,因而无法生成响应实体。
113 | 除非这是一个HEAD请求,否则该响应就应当返回一个包含可以让用户或者浏览器从中选择最合适的实体特性以及地址列表的实体。实体的格式由Content-Type头中定义的媒体类型决定。浏览器可以根据格式及自身能力自行作出最佳选择。但是,规范中并没有定义任何作出此类自动选择的标准。
114 | 407 Proxy Authentication Required
115 | 与401响应类似,只不过客户端必须在代理服务器上进行身份验证。代理服务器必须返回一个Proxy-Authenticate用以进行身份询问。客户端可以返回一个Proxy-Authorization信息头用以验证。参见RFC 2617。
116 | 408 Request Timeout
117 | 请求超时。客户端没有在服务器预备等待的时间内完成一个请求的发送。客户端可以随时再次提交这一请求而无需进行任何更改。
118 | 409 Conflict
119 | 由于和被请求的资源的当前状态之间存在冲突,请求无法完成。这个代码只允许用在这样的情况下才能被使用:用户被认为能够解决冲突,并且会重新提交新的请求。该响应应当包含足够的信息以便用户发现冲突的源头。
120 | 冲突通常发生于对PUT请求的处理中。例如,在采用版本检查的环境下,某次PUT提交的对特定资源的修改请求所附带的版本信息与之前的某个(第三方)请求向冲突,那么此时服务器就应该返回一个409错误,告知用户请求无法完成。此时,响应实体中很可能会包含两个冲突版本之间的差异比较,以便用户重新提交归并以后的新版本。
121 | 410 Gone
122 | 被请求的资源在服务器上已经不再可用,而且没有任何已知的转发地址。这样的状况应当被认为是永久性的。如果可能,拥有链接编辑功能的客户端应当在获得用户许可后删除所有指向这个地址的引用。如果服务器不知道或者无法确定这个状况是否是永久的,那么就应该使用404状态码。除非额外说明,否则这个响应是可缓存的。
123 | 410响应的目的主要是帮助网站管理员维护网站,通知用户该资源已经不再可用,并且服务器拥有者希望所有指向这个资源的远端连接也被删除。这类事件在限时、增值服务中很普遍。同样,410响应也被用于通知客户端在当前服务器站点上,原本属于某个个人的资源已经不再可用。当然,是否需要把所有永久不可用的资源标记为'410 Gone',以及是否需要保持此标记多长时间,完全取决于服务器拥有者。
124 | 411 Length Required
125 | 服务器拒绝在没有定义Content-Length头的情况下接受请求。在添加了表明请求消息体长度的有效Content-Length头之后,客户端可以再次提交该请求。
126 | 412 Precondition Failed
127 | 服务器在验证在请求的头字段中给出先决条件时,没能满足其中的一个或多个。这个状态码允许客户端在获取资源时在请求的元信息(请求头字段数据)中设置先决条件,以此避免该请求方法被应用到其希望的内容以外的资源上。
128 | 413 Request Entity Too Large
129 | 服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。此种情况下,服务器可以关闭连接以免客户端继续发送此请求。
130 | 如果这个状况是临时的,服务器应当返回一个Retry-After的响应头,以告知客户端可以在多少时间以后重新尝试。
131 | 414 Request-URI Too Long
132 | 请求的URI长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。这比较少见,通常的情况包括:
133 |
134 | 本应使用POST方法的表单提交变成了GET方法,导致查询字符串(Query String)过长。
135 | 重定向URI“黑洞”,例如每次重定向把旧的URI作为新的URI的一部分,导致在若干次重定向后URI超长。
136 | 客户端正在尝试利用某些服务器中存在的安全漏洞攻击服务器。这类服务器使用固定长度的缓冲读取或操作请求的URI,当GET后的参数超过某个数值后,可能会产生缓冲区溢出,导致任意代码被执行[1]。没有此类漏洞的服务器,应当返回414状态码。
137 |
138 | 415 Unsupported Media Type
139 | 对于当前请求的方法和所请求的资源,请求中提交的实体并不是服务器中所支持的格式,因此请求被拒绝。
140 | 416 Requested Range Not Satisfiable
141 | 如果请求中包含了Range请求头,并且Range中指定的任何数据范围都与当前资源的可用范围不重合,同时请求中又没有定义If-Range请求头,那么服务器就应当返回416状态码。
142 | 假如Range使用的是字节范围,那么这种情况就是指请求指定的所有数据范围的首字节位置都超过了当前资源的长度。服务器也应当在返回416状态码的同时,包含一个Content-Range实体头,用以指明当前资源的长度。这个响应也被禁止使用multipart/byteranges作为其Content-Type。
143 | 417 Expectation Failed
144 | 在请求头Expect中指定的预期内容无法被服务器满足,或者这个服务器是一个代理服务器,它有明显的证据证明在当前路由的下一个节点上,Expect的内容无法被满足。
145 | 418 I'm a teapot
146 | 本操作码是在1998年作为IETF的传统愚人节笑话, 在RFC 2324 超文本咖啡壶控制协议中定义的,并不需要在真实的HTTP服务器中定义。
147 | 421 There are too many connections from your internet address
148 | 从当前客户端所在的IP地址到服务器的连接数超过了服务器许可的最大范围。通常,这里的IP地址指的是从服务器上看到的客户端地址(比如用户的网关或者代理服务器地址)。在这种情况下,连接数的计算可能涉及到不止一个终端用户。
149 | 422 Unprocessable Entity
150 | 请求格式正确,但是由于含有语义错误,无法响应。(RFC 4918 WebDAV)
151 | 423 Locked
152 | 当前资源被锁定。(RFC 4918 WebDAV)
153 | 424 Failed Dependency
154 | 由于之前的某个请求发生的错误,导致当前请求失败,例如PROPPATCH。(RFC 4918 WebDAV)
155 | 425 Unordered Collection
156 | 在WebDav Advanced Collections草案中定义,但是未出现在《WebDAV顺序集协议》(RFC 3658)中。
157 | 426 Upgrade Required
158 | 客户端应当切换到TLS/1.0。(RFC 2817)
159 | 449 Retry With
160 | 由微软扩展,代表请求应当在执行完适当的操作后进行重试。
161 |
162 | 5xx服务器错误
163 |
164 | 这类状态码代表了服务器在处理请求的过程中有错误或者异常状态发生,也有可能是服务器意识到以当前的软硬件资源无法完成对请求的处理。除非这是一个HEAD请求,否则服务器应当包含一个解释当前错误状态以及这个状况是临时的还是永久的解释信息实体。浏览器应当向用户展示任何在当前响应中被包含的实体。
165 |
166 | 这些状态码适用于任何响应方法。
167 |
168 | 500 Internal Server Error
169 | 服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。一般来说,这个问题都会在服务器的程序码出错时出现。
170 | 501 Not Implemented
171 | 服务器不支持当前请求所需要的某个功能。当服务器无法识别请求的方法,并且无法支持其对任何资源的请求。
172 | 502 Bad Gateway
173 | 作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。
174 | 503 Service Unavailable
175 | 由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。如果能够预计延迟时间,那么响应中可以包含一个Retry-After头用以标明这个延迟时间。如果没有给出这个Retry-After信息,那么客户端应当以处理500响应的方式处理它。
176 | 504 Gateway Timeout
177 | 作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP、FTP、LDAP)或者辅助服务器(例如DNS)收到响应。
178 | 注意:某些代理服务器在DNS查询超时时会返回400或者500错误
179 | 505 HTTP Version Not Supported
180 | 服务器不支持,或者拒绝支持在请求中使用的HTTP版本。这暗示着服务器不能或不愿使用与客户端相同的版本。响应中应当包含一个描述了为何版本不被支持以及服务器支持哪些协议的实体。
181 | 506 Variant Also Negotiates
182 | 由《透明内容协商协议》(RFC 2295)扩展,代表服务器存在内部配置错误:被请求的协商变元资源被配置为在透明内容协商中使用自己,因此在一个协商处理中不是一个合适的重点。
183 | 507 Insufficient Storage
184 | 服务器无法存储完成请求所必须的内容。这个状况被认为是临时的。WebDAV(RFC 4918)
185 | 509 Bandwidth Limit Exceeded
186 | 服务器达到带宽限制。这不是一个官方的状态码,但是仍被广泛使用。
187 | 510 Not Extended
188 | 获取资源所需要的策略并没有没满足。
189 |
--------------------------------------------------------------------------------
/Tips/HTTP状态码汇总.md:
--------------------------------------------------------------------------------
1 | #HTTP状态码汇总
2 |
3 |
4 | 编号 | 名称 | 解释
5 | -------------|-------------|-------------
6 | 1️⃣❌❌| 🔴🔴🔴| 1xx消息
这一类型的状态码,代表请求已被接受,需要继续处理。这类响应是临时响应,只包含状态行和某些可选的响应头信息,并以空行结束。由于HTTP/1.0协议中没有定义任何1xx状态码,所以除非在某些试验条件下,服务器禁止向此类客户端发送1xx响应。 这些状态码代表的响应都是信息性的,标示客户应该采取的其他行动。
7 | 100 | Continue | 客户端应当继续发送请求。这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。服务器必须在请求完成后向客户端发送一个最终响应。
8 | 101 | Switching Protocols | 服务器已经理解了客户端的请求,并将通过Upgrade消息头通知客户端采用不同的协议来完成这个请求。在发送完这个响应最后的空行后,服务器将会切换到在Upgrade消息头中定义的那些协议。: 只有在切换新的协议更有好处的时候才应该采取类似措施。例如,切换到新的HTTP版本比旧版本更有优势,或者切换到一个实时且同步的协议以传送利用此类特性的资源。
9 | 102 | Processing | 由WebDAV(RFC 2518)扩展的状态码,代表处理将被继续执行。
10 | 2️⃣❌❌|🔴🔴🔴|2xx成功 这一类型的状态码,代表请求已成功被服务器接收、理解、并接受。
11 | 200 | OK | 请求已成功,请求所希望的响应头或数据体将随此响应返回。
12 | 201 | Created | 请求已经被实现,而且有一个新的资源已经依据请求的需要而创建,且其URI已经随Location头信息返回。假如需要的资源无法及时创建的话,应当返回'202 Accepted'。
13 | 202 | Accepted | 服务器已接受请求,但尚未处理。正如它可能被拒绝一样,最终该请求可能会也可能不会被执行。在异步操作的场合下,没有比发送这个状态码更方便的做法了。:返回202状态码的响应的目的是允许服务器接受其他过程的请求(例如某个每天只执行一次的基于批处理的操作),而不必让客户端一直保持与服务器的连接直到批处理操作全部完成。在接受请求处理并返回202状态码的响应应当在返回的实体中包含一些指示处理当前状态的信息,以及指向处理状态监视器或状态预测的指针,以便用户能够估计操作是否已经完成。
14 | 203 | Non-Authoritative Information | 服务器已成功处理了请求,但返回的实体头部元信息不是在原始服务器上有效的确定集合,而是来自本地或者第三方的拷贝。当前的信息可能是原始版本的子集或者超集。例如,包含资源的元数据可能导致原始服务器知道元信息的超集。使用此状态码不是必须的,而且只有在响应不使用此状态码便会返回200 OK的情况下才是合适的。
15 | 204 | No Content | 服务器成功处理了请求,但不需要返回任何实体内容,并且希望返回更新了的元信息。响应可能通过实体头部的形式,返回新的或更新后的元信息。如果存在这些头部信息,则应当与所请求的变量相呼应。 如果客户端是浏览器的话,那么用户浏览器应保留发送了该请求的页面,而不产生任何文档视图上的变化,即使按照规范新的或更新后的元信息应当被应用到用户浏览器活动视图中的文档。 由于204响应被禁止包含任何消息体,因此它始终以消息头后的第一个空行结尾。
16 | 205 | Reset Content | 服务器成功处理了请求,且没有返回任何内容。但是与204响应不同,返回此状态码的响应要求请求者重置文档视图。该响应主要是被用于接受用户输入后,立即重置表单,以便用户能够轻松地开始另一次输入。 与204响应一样,该响应也被禁止包含任何消息体,且以消息头后的第一个空行结束。
17 | 206 | Partial Content | 服务器已经成功处理了部分GET请求。类似于FlashGet或者迅雷这类的HTTP 下载工具都是使用此类响应实现断点续传或者将一个大文档分解为多个下载段同时下载。 该请求必须包含Range头信息来指示客户端希望得到的内容范围,并且可能包含If-Range来作为请求条件。 响应必须包含如下的头部域: Content-Range用以指示本次响应中返回的内容的范围;如果是Content-Type为multipart/byteranges的多段下载,则每一multipart段中都应包含Content-Range域用以指示本段的内容范围。假如响应中包含Content-Length,那么它的数值必须匹配它返回的内容范围的真实字节数。 Date ETag和/或Content-Location,假如同样的请求本应该返回200响应。 Expires, Cache-Control,和/或Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。 假如本响应请求使用了If-Range强缓存验证,那么本次响应不应该包含其他实体头;假如本响应的请求使用了If-Range弱缓存验证,那么本次响应禁止包含其他实体头;这避免了缓存的实体内容和更新了的实体头信息之间的不一致。否则,本响应就应当包含所有本应该返回200响应中应当返回的所有实体头部域。 假如ETag或Last-Modified头部不能精确匹配的话,则客户端缓存应禁止将206响应返回的内容与之前任何缓存过的内容组合在一起。 任何不支持Range以及Content-Range头的缓存都禁止缓存206响应返回的内容。
18 | 207 | Multi-Status | 由WebDAV(RFC 2518)扩展的状态码,代表之后的消息体将是一个XML消息,并且可能依照之前子请求数量的不同,包含一系列独立的响应代码。
19 | 3️⃣❌❌|🔴🔴🔴| 3xx重定向 这类状态码代表需要客户端采取进一步的操作才能完成请求。通常,这些状态码用来重定向,后续的请求地址(重定向目标)在本次响应的Location域中指明。当且仅当后续的请求所使用的方法是GET或者HEAD时,用户浏览器才可以在没有用户介入的情况下自动提交所需要的后续请求。客户端应当自动监测无限循环重定向(例如:A→B→C→……→A或A→A),因为这会导致服务器和客户端大量不必要的资源消耗。按照HTTP/1.0版规范的建议,浏览器不应自动访问超过5次的重定向。
20 | 300 | Multiple Choices | 被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向。 除非这是一个HEAD请求,否则该响应应当包括一个资源特性及地址的列表的实体,以便用户或浏览器从中选择最合适的重定向地址。这个实体的格式由Content-Type定义的格式所决定。浏览器可能根据响应的格式以及浏览器自身能力,自动作出最合适的选择。当然,RFC 2616规范并没有规定这样的自动选择该如何进行。 如果服务器本身已经有了首选的回馈选择,那么在Location中应当指明这个回馈的URI;浏览器可能会将这个Location值作为自动重定向的地址。此外,除非额外指定,否则这个响应也是可缓存的。
21 | 301 | Moved Permanently | 被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个URI之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。除非额外指定,否则这个响应也是可缓存的。 新的永久性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。 如果这不是一个GET或者HEAD请求,因此浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。 注意:对于某些使用HTTP/1.0协议的浏览器,当它们发送的POST请求得到了一个301响应的话,接下来的重定向请求将会变成GET方式。
22 | 302 | Found | 请求的资源现在临时从不同的URI响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。 新的临时性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。 如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。 注意:虽然RFC 1945和RFC 2068规范不允许客户端在重定向时改变请求的方法,但是很多现存的浏览器将302响应视作为303响应,并且使用GET方式访问在Location中规定的URI,而无视原先请求的方法。状态码303和307被添加了进来,用以明确服务器期待客户端进行何种反应。
23 | 303 | See Other | 对应当前请求的响应可以在另一个URI上被找到,而且客户端应当采用GET的方式访问那个资源。这个方法的存在主要是为了允许由脚本激活的POST请求输出重定向到一个新的资源。这个新的URI不是原始资源的替代引用。同时,303响应禁止被缓存。当然,第二个请求(重定向)可能被缓存。 新的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。 注意:许多HTTP/1.1版以前的浏览器不能正确理解303状态。如果需要考虑与这些浏览器之间的互动,302状态码应该可以胜任,因为大多数的浏览器处理302响应时的方式恰恰就是上述规范要求客户端处理303响应时应当做的。
24 | 304 | Not Modified | 如果客户端发送了一个带条件的GET请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个状态码。304响应禁止包含消息体,因此始终以消息头后的第一个空行结尾。 该响应必须包含以下的头信息: Date,除非这个服务器没有时钟。假如没有时钟的服务器也遵守这些规则,那么代理服务器以及客户端可以自行将Date字段添加到接收到的响应头中去(正如RFC 2068中规定的一样),缓存机制将会正常工作。 ETag和/或Content-Location,假如同样的请求本应返回200响应。 Expires, Cache-Control,和/或Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。 假如本响应请求使用了强缓存验证,那么本次响应不应该包含其他实体头;否则(例如,某个带条件的GET请求使用了弱缓存验证),本次响应禁止包含其他实体头;这避免了缓存了的实体内容和更新了的实体头信息之间的不一致。 假如某个304响应指明了当前某个实体没有缓存,那么缓存系统必须忽视这个响应,并且重复发送不包含限制条件的请求。 假如接收到一个要求更新某个缓存条目的304响应,那么缓存系统必须更新整个条目以反映所有在响应中被更新的字段的值。
25 | 305 | Use Proxy | 被请求的资源必须通过指定的代理才能被访问。Location域中将给出指定的代理所在的URI信息,接收者需要重复发送一个单独的请求,通过这个代理才能访问相应资源。只有原始服务器才能创建305响应。 注意:RFC 2068中没有明确305响应是为了重定向一个单独的请求,而且只能被原始服务器创建。忽视这些限制可能导致严重的安全后果。
26 | 306 | Switch Proxy | 在最新版的规范中,306状态码已经不再被使用。
27 | 307 | Temporary Redirect | 请求的资源现在临时从不同的URI响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。 新的临时性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。因为部分浏览器不能识别307响应,因此需要添加上述必要信息以便用户能够理解并向新的URI发出访问请求。 如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。
28 | 4️⃣❌❌|🔴🔴🔴|4xx客户端错误这类的状态码代表了客户端看起来可能发生了错误,妨碍了服务器的处理。除非响应的是一个HEAD请求,否则服务器就应该返回一个解释当前错误状况的实体,以及这是临时的还是永久性的状况。这些状态码适用于任何请求方法。浏览器应当向用户显示任何包含在此类错误响应中的实体内容。如果错误发生时客户端正在传送数据,那么使用TCP的服务器实现应当仔细确保在关闭客户端与服务器之间的连接之前,客户端已经收到了包含错误信息的数据包。如果客户端在收到错误信息后继续向服务器发送数据,服务器的TCP栈将向客户端发送一个重置数据包,以清除该客户端所有还未识别的输入缓冲,以免这些数据被服务器上的应用程序读取并干扰后者。
29 | 400 | Bad Request | 由于包含语法错误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求。
30 | 401 | Unauthorized | 当前请求需要用户验证。该响应必须包含一个适用于被请求资源的WWW-Authenticate信息头用以询问用户信息。客户端可以重复提交一个包含恰当的Authorization头信息的请求。如果当前请求已经包含了Authorization证书,那么401响应代表着服务器验证已经拒绝了那些证书。如果401响应包含了与前一个响应相同的身份验证询问,且浏览器已经至少尝试了一次验证,那么浏览器应当向用户展示响应中包含的实体信息,因为这个实体信息中可能包含了相关诊断信息。参见RFC 2617。
31 | 402 | Payment Required | 该状态码是为了将来可能的需求而预留的。
32 | 403 | Forbidden | 服务器已经理解请求,但是拒绝执行它。与401响应不同的是,身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交。如果这不是一个HEAD请求,而且服务器希望能够讲清楚为何请求不能被执行,那么就应该在实体内描述拒绝的原因。当然服务器也可以返回一个404响应,假如它不希望让客户端获得任何信息。
33 | 404 | Not Found | 请求失败,请求所希望得到的资源未被在服务器上发现。没有信息能够告诉用户这个状况到底是暂时的还是永久的。假如服务器知道情况的话,应当使用410状态码来告知旧资源因为某些内部的配置机制问题,已经永久的不可用,而且没有任何可以跳转的地址。404这个状态码被广泛应用于当服务器不想揭示到底为何请求被拒绝或者没有其他适合的响应可用的情况下。
34 | 405 | Method Not Allowed | 请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个Allow头信息用以表示出当前资源能够接受的请求方法的列表。 鉴于PUT,DELETE方法会对服务器上的资源进行写操作,因而绝大部分的网页服务器都不支持或者在默认配置下不允许上述请求方法,对于此类请求均会返回405错误。
35 | 406 | Not Acceptable | 请求的资源的内容特性无法满足请求头中的条件,因而无法生成响应实体。 除非这是一个HEAD请求,否则该响应就应当返回一个包含可以让用户或者浏览器从中选择最合适的实体特性以及地址列表的实体。实体的格式由Content-Type头中定义的媒体类型决定。浏览器可以根据格式及自身能力自行作出最佳选择。但是,规范中并没有定义任何作出此类自动选择的标准。
36 | 407 | Proxy Authentication Required | 与401响应类似,只不过客户端必须在代理服务器上进行身份验证。代理服务器必须返回一个Proxy-Authenticate用以进行身份询问。客户端可以返回一个Proxy-Authorization信息头用以验证。参见RFC 2617。
37 | 408 | Request Timeout | 请求超时。客户端没有在服务器预备等待的时间内完成一个请求的发送。客户端可以随时再次提交这一请求而无需进行任何更改。
38 | 409 | Conflict | 由于和被请求的资源的当前状态之间存在冲突,请求无法完成。这个代码只允许用在这样的情况下才能被使用:用户被认为能够解决冲突,并且会重新提交新的请求。该响应应当包含足够的信息以便用户发现冲突的源头。 冲突通常发生于对PUT请求的处理中。例如,在采用版本检查的环境下,某次PUT提交的对特定资源的修改请求所附带的版本信息与之前的某个(第三方)请求向冲突,那么此时服务器就应该返回一个409错误,告知用户请求无法完成。此时,响应实体中很可能会包含两个冲突版本之间的差异比较,以便用户重新提交归并以后的新版本。
39 | 410 | Gone | 被请求的资源在服务器上已经不再可用,而且没有任何已知的转发地址。这样的状况应当被认为是永久性的。如果可能,拥有链接编辑功能的客户端应当在获得用户许可后删除所有指向这个地址的引用。如果服务器不知道或者无法确定这个状况是否是永久的,那么就应该使用404状态码。除非额外说明,否则这个响应是可缓存的。 410响应的目的主要是帮助网站管理员维护网站,通知用户该资源已经不再可用,并且服务器拥有者希望所有指向这个资源的远端连接也被删除。这类事件在限时、增值服务中很普遍。同样,410响应也被用于通知客户端在当前服务器站点上,原本属于某个个人的资源已经不再可用。当然,是否需要把所有永久不可用的资源标记为'410 Gone',以及是否需要保持此标记多长时间,完全取决于服务器拥有者。
40 | 411 | Length Required | 服务器拒绝在没有定义Content-Length头的情况下接受请求。在添加了表明请求消息体长度的有效Content-Length头之后,客户端可以再次提交该请求。
41 | 412 | Precondition Failed | 服务器在验证在请求的头字段中给出先决条件时,没能满足其中的一个或多个。这个状态码允许客户端在获取资源时在请求的元信息(请求头字段数据)中设置先决条件,以此避免该请求方法被应用到其希望的内容以外的资源上。
42 | 413 | Request Entity Too Large | 服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。此种情况下,服务器可以关闭连接以免客户端继续发送此请求。 如果这个状况是临时的,服务器应当返回一个Retry-After的响应头,以告知客户端可以在多少时间以后重新尝试。
43 | 414 | Request-URI Too Long | 请求的URI长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。这比较少见,通常的情况包括: 本应使用POST方法的表单提交变成了GET方法,导致查询字符串(Query String)过长。 重定向URI“黑洞”,例如每次重定向把旧的URI作为新的URI的一部分,导致在若干次重定向后URI超长。 客户端正在尝试利用某些服务器中存在的安全漏洞攻击服务器。这类服务器使用固定长度的缓冲读取或操作请求的URI,当GET后的参数超过某个数值后,可能会产生缓冲区溢出,导致任意代码被执行[1]。没有此类漏洞的服务器,应当返回414状态码。
44 | 415 | Unsupported Media Type | 对于当前请求的方法和所请求的资源,请求中提交的实体并不是服务器中所支持的格式,因此请求被拒绝。
45 | 416 | Requested Range Not Satisfiable | 如果请求中包含了Range请求头,并且Range中指定的任何数据范围都与当前资源的可用范围不重合,同时请求中又没有定义If-Range请求头,那么服务器就应当返回416状态码。 假如Range使用的是字节范围,那么这种情况就是指请求指定的所有数据范围的首字节位置都超过了当前资源的长度。服务器也应当在返回416状态码的同时,包含一个Content-Range实体头,用以指明当前资源的长度。这个响应也被禁止使用multipart/byteranges作为其Content-Type。
46 | 417 | Expectation Failed | 在请求头Expect中指定的预期内容无法被服务器满足,或者这个服务器是一个代理服务器,它有明显的证据证明在当前路由的下一个节点上,Expect的内容无法被满足。
47 | 418 | I'm a teapot | 本操作码是在1998年作为IETF的传统愚人节笑话, 在RFC 2324 超文本咖啡壶控制协议中定义的,并不需要在真实的HTTP服务器中定义。
48 | 421 | There are too many connections from your internet address | 从当前客户端所在的IP地址到服务器的连接数超过了服务器许可的最大范围。通常,这里的IP地址指的是从服务器上看到的客户端地址(比如用户的网关或者代理服务器地址)。在这种情况下,连接数的计算可能涉及到不止一个终端用户。
49 | 422 | Unprocessable Entity | 请求格式正确,但是由于含有语义错误,无法响应。(RFC 4918 WebDAV)
50 | 423 | Locked | 当前资源被锁定。(RFC 4918 WebDAV)
51 | 424 | Failed Dependency | 由于之前的某个请求发生的错误,导致当前请求失败,例如PROPPATCH。(RFC 4918 WebDAV)
52 | 425 | Unordered Collection | 在WebDav Advanced Collections草案中定义,但是未出现在《WebDAV顺序集协议》(RFC 3658)中。
53 | 426 | Upgrade Required | 客户端应当切换到TLS/1.0。(RFC 2817)
54 | 449 | Retry With | 由微软扩展,代表请求应当在执行完适当的操作后进行重试。
55 | 5️⃣❌❌|🔴🔴🔴|5xx服务器错误 这类状态码代表了服务器在处理请求的过程中有错误或者异常状态发生,也有可能是服务器意识到以当前的软硬件资源无法完成对请求的处理。除非这是一个HEAD请求,否则服务器应当包含一个解释当前错误状态以及这个状况是临时的还是永久的解释信息实体。浏览器应当向用户展示任何在当前响应中被包含的实体。这些状态码适用于任何响应方法。
56 | 500 | Internal Server Error | 服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。一般来说,这个问题都会在服务器的程序码出错时出现。
57 | 501 | Not Implemented | 服务器不支持当前请求所需要的某个功能。当服务器无法识别请求的方法,并且无法支持其对任何资源的请求。
58 | 502 | Bad Gateway | 作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。
59 | 503 | Service Unavailable | 由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。如果能够预计延迟时间,那么响应中可以包含一个Retry-After头用以标明这个延迟时间。如果没有给出这个Retry-After信息,那么客户端应当以处理500响应的方式处理它。
60 | 504 | Gateway Timeout | 作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP、FTP、LDAP)或者辅助服务器(例如DNS)收到响应。 注意:某些代理服务器在DNS查询超时时会返回400或者500错误
61 | 505 | HTTP Version Not Supported | 服务器不支持,或者拒绝支持在请求中使用的HTTP版本。这暗示着服务器不能或不愿使用与客户端相同的版本。响应中应当包含一个描述了为何版本不被支持以及服务器支持哪些协议的实体。
62 | 506 | Variant Also Negotiates | 由《透明内容协商协议》(RFC 2295)扩展,代表服务器存在内部配置错误:被请求的协商变元资源被配置为在透明内容协商中使用自己,因此在一个协商处理中不是一个合适的重点。
63 | 507 | Insufficient Storage | 服务器无法存储完成请求所必须的内容。这个状况被认为是临时的。WebDAV(RFC 4918)
64 | 509 | Bandwidth Limit Exceeded | 服务器达到带宽限制。这不是一个官方的状态码,但是仍被广泛使用。
65 | 510 | Not Extended | 获取资源所需要的策略并没有没满足。
66 |
--------------------------------------------------------------------------------
/Tips/Heap-Stack Dance/Heap-Stack Dance.md:
--------------------------------------------------------------------------------
1 | 【使用 Heap-Stack Dance 替代 Weak-Strong Dance,优雅避开循环引用】Weak-Strong Dance这一最佳实践的原理已经被讲烂了,开发者对该写法已经烂熟于心。有相当一部分开发者是不理解 Weak-Strong Dance 的原理,但却写得很溜,即使没必要加 `strongSelf` 的场景下也会添加上 `strongSelf`。没错,这样做,总是没错。
2 |
3 | 有没有想过从API层面简化一下?
4 |
5 | 介绍下我的做法:
6 |
7 | 为 block 多加一个参数,也就是 self 所属类型的参数,那么在 block 内部,该参数就会和 `strongSelf` 的效果一致。同时你也可以不写 `weakSelf`,直接使用使用该参数(作用等同于直接使用 `strongSelf` )。这样就达到了:“多加一个参数,省掉两行代码”的效果。原理就是利用了“参数”的特性:参数是存放在栈中的(或寄存器中),系统负责回收,开发者无需关心。因为解决问题的思路是:将 block 会捕获变量到堆上的问题,化解为了:变量会被分配到栈(或寄存器中)上,所以我把种做法起名叫 Heap-Stack Dance 。
8 |
9 | 具体用法示例如下:
10 | (详见仓库中的[Demo---文件夹叫做:weak-strong-drance-demo](https://github.com/ChenYilong/iOSBlog/tree/master/Tips/Heap-Stack%20Dance) )
11 |
12 |
13 | ```Objective-C
14 |
15 | #import "Foo.h"
16 |
17 | typedef void (^Completion)(Foo *foo);
18 |
19 | @interface Foo ()
20 |
21 | @property (nonatomic, copy) Completion completion1;
22 | @property (nonatomic, copy) Completion completion2;
23 |
24 | @end
25 |
26 | @implementation Foo
27 |
28 | - (instancetype)init {
29 | if (!(self = [super init])) {
30 | return nil;
31 | }
32 | __weak typeof(self) weakSelf = self;
33 | self.completion1 = ^(Foo *foo) {
34 | NSLog(@"completion1");
35 | };
36 | self.completion2 = ^(Foo *foo) {
37 | __strong typeof(self) strongSelf = weakSelf;
38 | NSLog(@"completion2");
39 | NSUInteger delaySeconds = 2;
40 | dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delaySeconds * NSEC_PER_SEC));
41 | dispatch_after(when, dispatch_get_main_queue(), ^{
42 | NSLog(@"两秒钟后");
43 | foo.completion1(foo);//foo等价于strongSelf
44 | });
45 | };
46 | self.completion2(self);
47 | return self;
48 | }
49 |
50 | - (void)dealloc {
51 | NSLog(@"dealloc");
52 | }
53 |
54 | @end
55 |
56 |
57 | @implementation ViewController
58 |
59 | - (void)viewDidLoad {
60 | [super viewDidLoad];
61 | __autoreleasing Foo *foo = [Foo new];
62 | }
63 |
64 | @end
65 | ```
66 |
67 | 打印如下:
68 |
69 | ```Objective-C
70 | completion2
71 | 两秒钟后
72 | completion1
73 | dealloc
74 |
75 | ```
76 |
77 | 举一个实际开发中的例子:
78 |
79 | 如果我们为UIViewController添加了一个属性,叫做viewDidLoadBlock,让用户来进行一些UI设置。
80 |
81 | 具体的做法如下:
82 |
83 |
84 | ```Objective-C
85 | @property (nonatomic, copy) CYLViewDidLoadBlock viewDidLoadBlock;
86 |
87 | - (void)viewDidLoad {
88 | [super viewDidLoad];
89 | //...
90 | !self.viewDidLoadBlock ?: self.viewDidLoadBlock(self);
91 | }
92 | ```
93 |
94 | 那么可以想象block中必然是要使用到viewController本身,为了避免循环引用,之前我们不得不这样做:
95 |
96 | 简化前:
97 |
98 | ```Objective-C
99 | __weak typeof(controller) weakController = conversationController;
100 | [conversationController setViewDidLoadBlock:^{
101 | [weakController.navigationItem setTitle:@"XXX"];
102 | }];
103 | ```
104 |
105 | 如果借助这种做法,简化后:
106 |
107 | ```Objective-C
108 | [conversationViewController setViewDidLoadBlock:^(LCCKBaseViewController *viewController) {
109 | viewController.navigationItem.title = @"XXX";
110 | }];
111 | ```
112 |
113 | 这种可能优势不太明显,毕竟编译器都能看出来,会报警告。但如果遇到了那种很难看出会造成循环引用的情景下,优势就显现出来了。
114 | 尤其是在公开的 API 中,无法获知 `block` 是否被 `self` 持有的,如果在 `block` 中加增一个 `self` 类型的参数,因为 `block` 内部已经提供了 `weakSelf` 或者是 `strongSelf` 的替代者,那么调用者就可以在不使用 Weak-Strong Dance 的情况下避免循环引用。
115 |
116 | 下面这个语句,编译器不会报警告,你能看出来有循环应用吗?
117 |
118 | 比如我们为 `UIViewController` 添加了一个方法,这个方法主要作用就是配置下 `navigationBar` 右上角的 `item` 样式以及点击事件:
119 |
120 | ```Objective-C
121 | [aConversationController configureBarButtonItemStyle:LCCKBarButtonItemStyleGroupProfile
122 | action:^(__kindof LCCKBaseViewController *viewController, UIBarButtonItem *sender, UIEvent *event) { [aConversationController.navigationController pushViewController:[UIViewController new] animated:YES];
123 | }];
124 | ```
125 |
126 | 实际上你必须点击进去看一下该 API 的实现,你才能发现原来 `aConversationController` 持有了 `action` 这个 `block`,而在这种用法中 `block` 又持有了 `aConversationController` ,所以这种情况是有循环引用的。
127 |
128 | 可以看下上述方法的具体的实现:
129 |
130 | ```Objective-C
131 | - (void)configureBarButtonItemStyle:(LCCKBarButtonItemStyle)style action:(LCCKBarButtonItemActionBlock)action {
132 | NSString *icon;
133 | switch (style) {
134 | case LCCKBarButtonItemStyleSetting: {
135 | icon = @"barbuttonicon_set";
136 | break;
137 | }
138 | case LCCKBarButtonItemStyleMore: {
139 | icon = @"barbuttonicon_more";
140 | break;
141 | }
142 | case LCCKBarButtonItemStyleAdd: {
143 | icon = @"barbuttonicon_add";
144 | break;
145 | }
146 | case LCCKBarButtonItemStyleAddFriends:
147 | icon = @"barbuttonicon_addfriends";
148 | break;
149 | case LCCKBarButtonItemStyleSingleProfile:
150 | icon = @"barbuttonicon_InfoSingle";
151 | break;
152 | case LCCKBarButtonItemStyleGroupProfile:
153 | icon = @"barbuttonicon_InfoMulti";
154 | break;
155 | case LCCKBarButtonItemStyleShare:
156 | icon = @"barbuttonicon_Operate";
157 | break;
158 | }
159 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage lcck_imageNamed:icon bundleName:@"BarButtonIcon" bundleForClass:[self class]] style:UIBarButtonItemStylePlain target:self action:@selector(clickedBarButtonItemAction:event:)];
160 | self.barButtonItemAction = action;
161 | }
162 |
163 | - (void)clickedBarButtonItemAction:(UIBarButtonItem *)sender event:(UIEvent *)event {
164 | if (self.barButtonItemAction) {
165 | self.barButtonItemAction(self, sender, event);
166 | }
167 | }
168 | ```
169 |
170 |
171 | > 必须让调用者理解了内部实现,才能用得好的API,不是一个好的API设计。
172 |
173 | 能不能在API层面就避免?增加一个self类型的参数就好了:
174 |
175 | ```Objective-C
176 | [aConversationController configureBarButtonItemStyle:LCCKBarButtonItemStyleGroupProfile
177 | action:^(__kindof LCCKBaseViewController *viewController, UIBarButtonItem *sender, UIEvent *event) {
178 | [viewController.navigationController pushViewController:[UIViewController new] animated:YES];
179 | }];
180 |
181 | ```
182 |
183 | 各位如果觉得好用,可以到你的项目中使用 `Heap-Stack Dance` 替代 `Weak-Strong Dance`,重构一些代码。
184 |
185 |
186 |
187 | 这里还有另外一种方法来证明 self 做参数传进 block 不会被 Block 捕获:
188 |
189 | 用 clang 对 Foo.m 文件转成c/c++代码:
190 |
191 | > clang -rewrite-objc Foo.m -Wno-deprecated-declarations -fobjc-arc
192 |
193 |
194 | 比如如下代码:
195 |
196 | ```Objective-C
197 | int tmpTarget;
198 | self.completion1 = ^(Foo *foo) {
199 | tmpTarget;
200 | NSLog(@"completion1");
201 | };
202 | self.completion1(self);
203 | ```
204 |
205 | 可以看到 Block 只会对传入的 `tmpTarget` 引用,`self` 不会捕获:
206 |
207 |
208 | ```C
209 | struct __Foo__init_block_impl_0 {
210 | struct __block_impl impl;
211 | struct __Foo__init_block_desc_0* Desc;
212 | int tmpTarget;
213 | __Foo__init_block_impl_0(void *fp, struct __Foo__init_block_desc_0 *desc, int _tmpTarget, int flags=0) : tmpTarget(_tmpTarget) {
214 | impl.isa = &_NSConcreteStackBlock;
215 | impl.Flags = flags;
216 | impl.FuncPtr = fp;
217 | Desc = desc;
218 | }
219 | };
220 | ```
221 |
222 |
223 | 而如果是如下代码 self 就会被捕获:
224 |
225 |
226 | ```Objective-C
227 | int tmpTarget;
228 | self.completion1 = ^(Foo *foo) {
229 | tmpTarget;
230 | _b;
231 | NSLog(@"completion1");
232 | };
233 | self.completion1(self);
234 | ```
235 |
236 |
237 | ```Objective-C
238 | struct __Foo__init_block_impl_0 {
239 | struct __block_impl impl;
240 | struct __Foo__init_block_desc_0* Desc;
241 | int tmpTarget;
242 | Foo *__strong self;
243 | __Foo__init_block_impl_0(void *fp, struct __Foo__init_block_desc_0 *desc, int _tmpTarget, Foo *__strong _self, int flags=0) : tmpTarget(_tmpTarget), self(_self) {
244 | impl.isa = &_NSConcreteStackBlock;
245 | impl.Flags = flags;
246 | impl.FuncPtr = fp;
247 | Desc = desc;
248 | }
249 | };
250 | ```
251 |
252 | ----------
253 |
254 | QQ交流群:515295083
255 |
256 | Posted by [微博@iOS程序犭袁](http://weibo.com/luohanchenyilong/)
257 | 原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | [Creative Commons BY-NC-ND 3.0](http://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
258 |
259 |
260 |
261 |
--------------------------------------------------------------------------------
/Tips/Heap-Stack Dance/weak-strong-drance-demo/weak-strong-drance-demo.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 9AB072FB1E099BDF00A7B4F8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 9AB072FA1E099BDF00A7B4F8 /* main.m */; };
11 | 9AB072FE1E099BDF00A7B4F8 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 9AB072FD1E099BDF00A7B4F8 /* AppDelegate.m */; };
12 | 9AB073011E099BDF00A7B4F8 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9AB073001E099BDF00A7B4F8 /* ViewController.m */; };
13 | 9AB073041E099BDF00A7B4F8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9AB073021E099BDF00A7B4F8 /* Main.storyboard */; };
14 | 9AB073061E099BDF00A7B4F8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9AB073051E099BDF00A7B4F8 /* Assets.xcassets */; };
15 | 9AB073091E099BDF00A7B4F8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9AB073071E099BDF00A7B4F8 /* LaunchScreen.storyboard */; };
16 | 9AB073121E099C4F00A7B4F8 /* Foo.m in Sources */ = {isa = PBXBuildFile; fileRef = 9AB073111E099C4F00A7B4F8 /* Foo.m */; };
17 | /* End PBXBuildFile section */
18 |
19 | /* Begin PBXFileReference section */
20 | 9AB072F61E099BDF00A7B4F8 /* weak-strong-drance-demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "weak-strong-drance-demo.app"; sourceTree = BUILT_PRODUCTS_DIR; };
21 | 9AB072FA1E099BDF00A7B4F8 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
22 | 9AB072FC1E099BDF00A7B4F8 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
23 | 9AB072FD1E099BDF00A7B4F8 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
24 | 9AB072FF1E099BDF00A7B4F8 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; };
25 | 9AB073001E099BDF00A7B4F8 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; };
26 | 9AB073031E099BDF00A7B4F8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
27 | 9AB073051E099BDF00A7B4F8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
28 | 9AB073081E099BDF00A7B4F8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
29 | 9AB0730A1E099BDF00A7B4F8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
30 | 9AB073101E099C4F00A7B4F8 /* Foo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Foo.h; sourceTree = ""; };
31 | 9AB073111E099C4F00A7B4F8 /* Foo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Foo.m; sourceTree = ""; };
32 | /* End PBXFileReference section */
33 |
34 | /* Begin PBXFrameworksBuildPhase section */
35 | 9AB072F31E099BDF00A7B4F8 /* Frameworks */ = {
36 | isa = PBXFrameworksBuildPhase;
37 | buildActionMask = 2147483647;
38 | files = (
39 | );
40 | runOnlyForDeploymentPostprocessing = 0;
41 | };
42 | /* End PBXFrameworksBuildPhase section */
43 |
44 | /* Begin PBXGroup section */
45 | 9AB072ED1E099BDF00A7B4F8 = {
46 | isa = PBXGroup;
47 | children = (
48 | 9AB072F81E099BDF00A7B4F8 /* weak-strong-drance-demo */,
49 | 9AB072F71E099BDF00A7B4F8 /* Products */,
50 | );
51 | sourceTree = "";
52 | };
53 | 9AB072F71E099BDF00A7B4F8 /* Products */ = {
54 | isa = PBXGroup;
55 | children = (
56 | 9AB072F61E099BDF00A7B4F8 /* weak-strong-drance-demo.app */,
57 | );
58 | name = Products;
59 | sourceTree = "";
60 | };
61 | 9AB072F81E099BDF00A7B4F8 /* weak-strong-drance-demo */ = {
62 | isa = PBXGroup;
63 | children = (
64 | 9AB072FC1E099BDF00A7B4F8 /* AppDelegate.h */,
65 | 9AB072FD1E099BDF00A7B4F8 /* AppDelegate.m */,
66 | 9AB072FF1E099BDF00A7B4F8 /* ViewController.h */,
67 | 9AB073001E099BDF00A7B4F8 /* ViewController.m */,
68 | 9AB073101E099C4F00A7B4F8 /* Foo.h */,
69 | 9AB073111E099C4F00A7B4F8 /* Foo.m */,
70 | 9AB073021E099BDF00A7B4F8 /* Main.storyboard */,
71 | 9AB073051E099BDF00A7B4F8 /* Assets.xcassets */,
72 | 9AB073071E099BDF00A7B4F8 /* LaunchScreen.storyboard */,
73 | 9AB0730A1E099BDF00A7B4F8 /* Info.plist */,
74 | 9AB072F91E099BDF00A7B4F8 /* Supporting Files */,
75 | );
76 | path = "weak-strong-drance-demo";
77 | sourceTree = "";
78 | };
79 | 9AB072F91E099BDF00A7B4F8 /* Supporting Files */ = {
80 | isa = PBXGroup;
81 | children = (
82 | 9AB072FA1E099BDF00A7B4F8 /* main.m */,
83 | );
84 | name = "Supporting Files";
85 | sourceTree = "";
86 | };
87 | /* End PBXGroup section */
88 |
89 | /* Begin PBXNativeTarget section */
90 | 9AB072F51E099BDF00A7B4F8 /* weak-strong-drance-demo */ = {
91 | isa = PBXNativeTarget;
92 | buildConfigurationList = 9AB0730D1E099BE000A7B4F8 /* Build configuration list for PBXNativeTarget "weak-strong-drance-demo" */;
93 | buildPhases = (
94 | 9AB072F21E099BDF00A7B4F8 /* Sources */,
95 | 9AB072F31E099BDF00A7B4F8 /* Frameworks */,
96 | 9AB072F41E099BDF00A7B4F8 /* Resources */,
97 | );
98 | buildRules = (
99 | );
100 | dependencies = (
101 | );
102 | name = "weak-strong-drance-demo";
103 | productName = "weak-strong-drance-demo";
104 | productReference = 9AB072F61E099BDF00A7B4F8 /* weak-strong-drance-demo.app */;
105 | productType = "com.apple.product-type.application";
106 | };
107 | /* End PBXNativeTarget section */
108 |
109 | /* Begin PBXProject section */
110 | 9AB072EE1E099BDF00A7B4F8 /* Project object */ = {
111 | isa = PBXProject;
112 | attributes = {
113 | LastUpgradeCheck = 0730;
114 | ORGANIZATIONNAME = ElonChan;
115 | TargetAttributes = {
116 | 9AB072F51E099BDF00A7B4F8 = {
117 | CreatedOnToolsVersion = 7.3;
118 | };
119 | };
120 | };
121 | buildConfigurationList = 9AB072F11E099BDF00A7B4F8 /* Build configuration list for PBXProject "weak-strong-drance-demo" */;
122 | compatibilityVersion = "Xcode 3.2";
123 | developmentRegion = English;
124 | hasScannedForEncodings = 0;
125 | knownRegions = (
126 | en,
127 | Base,
128 | );
129 | mainGroup = 9AB072ED1E099BDF00A7B4F8;
130 | productRefGroup = 9AB072F71E099BDF00A7B4F8 /* Products */;
131 | projectDirPath = "";
132 | projectRoot = "";
133 | targets = (
134 | 9AB072F51E099BDF00A7B4F8 /* weak-strong-drance-demo */,
135 | );
136 | };
137 | /* End PBXProject section */
138 |
139 | /* Begin PBXResourcesBuildPhase section */
140 | 9AB072F41E099BDF00A7B4F8 /* Resources */ = {
141 | isa = PBXResourcesBuildPhase;
142 | buildActionMask = 2147483647;
143 | files = (
144 | 9AB073091E099BDF00A7B4F8 /* LaunchScreen.storyboard in Resources */,
145 | 9AB073061E099BDF00A7B4F8 /* Assets.xcassets in Resources */,
146 | 9AB073041E099BDF00A7B4F8 /* Main.storyboard in Resources */,
147 | );
148 | runOnlyForDeploymentPostprocessing = 0;
149 | };
150 | /* End PBXResourcesBuildPhase section */
151 |
152 | /* Begin PBXSourcesBuildPhase section */
153 | 9AB072F21E099BDF00A7B4F8 /* Sources */ = {
154 | isa = PBXSourcesBuildPhase;
155 | buildActionMask = 2147483647;
156 | files = (
157 | 9AB073121E099C4F00A7B4F8 /* Foo.m in Sources */,
158 | 9AB073011E099BDF00A7B4F8 /* ViewController.m in Sources */,
159 | 9AB072FE1E099BDF00A7B4F8 /* AppDelegate.m in Sources */,
160 | 9AB072FB1E099BDF00A7B4F8 /* main.m in Sources */,
161 | );
162 | runOnlyForDeploymentPostprocessing = 0;
163 | };
164 | /* End PBXSourcesBuildPhase section */
165 |
166 | /* Begin PBXVariantGroup section */
167 | 9AB073021E099BDF00A7B4F8 /* Main.storyboard */ = {
168 | isa = PBXVariantGroup;
169 | children = (
170 | 9AB073031E099BDF00A7B4F8 /* Base */,
171 | );
172 | name = Main.storyboard;
173 | sourceTree = "";
174 | };
175 | 9AB073071E099BDF00A7B4F8 /* LaunchScreen.storyboard */ = {
176 | isa = PBXVariantGroup;
177 | children = (
178 | 9AB073081E099BDF00A7B4F8 /* Base */,
179 | );
180 | name = LaunchScreen.storyboard;
181 | sourceTree = "";
182 | };
183 | /* End PBXVariantGroup section */
184 |
185 | /* Begin XCBuildConfiguration section */
186 | 9AB0730B1E099BDF00A7B4F8 /* Debug */ = {
187 | isa = XCBuildConfiguration;
188 | buildSettings = {
189 | ALWAYS_SEARCH_USER_PATHS = NO;
190 | CLANG_ANALYZER_NONNULL = YES;
191 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
192 | CLANG_CXX_LIBRARY = "libc++";
193 | CLANG_ENABLE_MODULES = YES;
194 | CLANG_ENABLE_OBJC_ARC = YES;
195 | CLANG_WARN_BOOL_CONVERSION = YES;
196 | CLANG_WARN_CONSTANT_CONVERSION = YES;
197 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
198 | CLANG_WARN_EMPTY_BODY = YES;
199 | CLANG_WARN_ENUM_CONVERSION = YES;
200 | CLANG_WARN_INT_CONVERSION = YES;
201 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
202 | CLANG_WARN_UNREACHABLE_CODE = YES;
203 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
204 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
205 | COPY_PHASE_STRIP = NO;
206 | DEBUG_INFORMATION_FORMAT = dwarf;
207 | ENABLE_STRICT_OBJC_MSGSEND = YES;
208 | ENABLE_TESTABILITY = YES;
209 | GCC_C_LANGUAGE_STANDARD = gnu99;
210 | GCC_DYNAMIC_NO_PIC = NO;
211 | GCC_NO_COMMON_BLOCKS = YES;
212 | GCC_OPTIMIZATION_LEVEL = 0;
213 | GCC_PREPROCESSOR_DEFINITIONS = (
214 | "DEBUG=1",
215 | "$(inherited)",
216 | );
217 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
218 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
219 | GCC_WARN_UNDECLARED_SELECTOR = YES;
220 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
221 | GCC_WARN_UNUSED_FUNCTION = YES;
222 | GCC_WARN_UNUSED_VARIABLE = YES;
223 | IPHONEOS_DEPLOYMENT_TARGET = 9.3;
224 | MTL_ENABLE_DEBUG_INFO = YES;
225 | ONLY_ACTIVE_ARCH = YES;
226 | SDKROOT = iphoneos;
227 | TARGETED_DEVICE_FAMILY = "1,2";
228 | };
229 | name = Debug;
230 | };
231 | 9AB0730C1E099BDF00A7B4F8 /* Release */ = {
232 | isa = XCBuildConfiguration;
233 | buildSettings = {
234 | ALWAYS_SEARCH_USER_PATHS = NO;
235 | CLANG_ANALYZER_NONNULL = YES;
236 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
237 | CLANG_CXX_LIBRARY = "libc++";
238 | CLANG_ENABLE_MODULES = YES;
239 | CLANG_ENABLE_OBJC_ARC = YES;
240 | CLANG_WARN_BOOL_CONVERSION = YES;
241 | CLANG_WARN_CONSTANT_CONVERSION = YES;
242 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
243 | CLANG_WARN_EMPTY_BODY = YES;
244 | CLANG_WARN_ENUM_CONVERSION = YES;
245 | CLANG_WARN_INT_CONVERSION = YES;
246 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
247 | CLANG_WARN_UNREACHABLE_CODE = YES;
248 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
249 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
250 | COPY_PHASE_STRIP = NO;
251 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
252 | ENABLE_NS_ASSERTIONS = NO;
253 | ENABLE_STRICT_OBJC_MSGSEND = YES;
254 | GCC_C_LANGUAGE_STANDARD = gnu99;
255 | GCC_NO_COMMON_BLOCKS = YES;
256 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
257 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
258 | GCC_WARN_UNDECLARED_SELECTOR = YES;
259 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
260 | GCC_WARN_UNUSED_FUNCTION = YES;
261 | GCC_WARN_UNUSED_VARIABLE = YES;
262 | IPHONEOS_DEPLOYMENT_TARGET = 9.3;
263 | MTL_ENABLE_DEBUG_INFO = NO;
264 | SDKROOT = iphoneos;
265 | TARGETED_DEVICE_FAMILY = "1,2";
266 | VALIDATE_PRODUCT = YES;
267 | };
268 | name = Release;
269 | };
270 | 9AB0730E1E099BE000A7B4F8 /* Debug */ = {
271 | isa = XCBuildConfiguration;
272 | buildSettings = {
273 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
274 | INFOPLIST_FILE = "weak-strong-drance-demo/Info.plist";
275 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
276 | PRODUCT_BUNDLE_IDENTIFIER = "ElonChan.weak-strong-drance-demo";
277 | PRODUCT_NAME = "$(TARGET_NAME)";
278 | };
279 | name = Debug;
280 | };
281 | 9AB0730F1E099BE000A7B4F8 /* Release */ = {
282 | isa = XCBuildConfiguration;
283 | buildSettings = {
284 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
285 | INFOPLIST_FILE = "weak-strong-drance-demo/Info.plist";
286 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
287 | PRODUCT_BUNDLE_IDENTIFIER = "ElonChan.weak-strong-drance-demo";
288 | PRODUCT_NAME = "$(TARGET_NAME)";
289 | };
290 | name = Release;
291 | };
292 | /* End XCBuildConfiguration section */
293 |
294 | /* Begin XCConfigurationList section */
295 | 9AB072F11E099BDF00A7B4F8 /* Build configuration list for PBXProject "weak-strong-drance-demo" */ = {
296 | isa = XCConfigurationList;
297 | buildConfigurations = (
298 | 9AB0730B1E099BDF00A7B4F8 /* Debug */,
299 | 9AB0730C1E099BDF00A7B4F8 /* Release */,
300 | );
301 | defaultConfigurationIsVisible = 0;
302 | defaultConfigurationName = Release;
303 | };
304 | 9AB0730D1E099BE000A7B4F8 /* Build configuration list for PBXNativeTarget "weak-strong-drance-demo" */ = {
305 | isa = XCConfigurationList;
306 | buildConfigurations = (
307 | 9AB0730E1E099BE000A7B4F8 /* Debug */,
308 | 9AB0730F1E099BE000A7B4F8 /* Release */,
309 | );
310 | defaultConfigurationIsVisible = 0;
311 | };
312 | /* End XCConfigurationList section */
313 | };
314 | rootObject = 9AB072EE1E099BDF00A7B4F8 /* Project object */;
315 | }
316 |
--------------------------------------------------------------------------------
/Tips/Heap-Stack Dance/weak-strong-drance-demo/weak-strong-drance-demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tips/Heap-Stack Dance/weak-strong-drance-demo/weak-strong-drance-demo/AppDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.h
3 | // weak-strong-drance-demo
4 | //
5 | // Created by 陈宜龙 on 12/21/16.
6 | // Copyright © 2016 ElonChan. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface AppDelegate : UIResponder
12 |
13 | @property (strong, nonatomic) UIWindow *window;
14 |
15 |
16 | @end
17 |
18 |
--------------------------------------------------------------------------------
/Tips/Heap-Stack Dance/weak-strong-drance-demo/weak-strong-drance-demo/AppDelegate.m:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.m
3 | // weak-strong-drance-demo
4 | //
5 | // Created by 陈宜龙 on 12/21/16.
6 | // Copyright © 2016 ElonChan. All rights reserved.
7 | //
8 |
9 | #import "AppDelegate.h"
10 |
11 | @interface AppDelegate ()
12 |
13 | @end
14 |
15 | @implementation AppDelegate
16 |
17 |
18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
19 | // Override point for customization after application launch.
20 | return YES;
21 | }
22 |
23 | - (void)applicationWillResignActive:(UIApplication *)application {
24 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
25 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
26 | }
27 |
28 | - (void)applicationDidEnterBackground:(UIApplication *)application {
29 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
30 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
31 | }
32 |
33 | - (void)applicationWillEnterForeground:(UIApplication *)application {
34 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
35 | }
36 |
37 | - (void)applicationDidBecomeActive:(UIApplication *)application {
38 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
39 | }
40 |
41 | - (void)applicationWillTerminate:(UIApplication *)application {
42 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
43 | }
44 |
45 | @end
46 |
--------------------------------------------------------------------------------
/Tips/Heap-Stack Dance/weak-strong-drance-demo/weak-strong-drance-demo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "ipad",
35 | "size" : "29x29",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "ipad",
40 | "size" : "29x29",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "40x40",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "40x40",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "76x76",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "76x76",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "83.5x83.5",
66 | "scale" : "2x"
67 | }
68 | ],
69 | "info" : {
70 | "version" : 1,
71 | "author" : "xcode"
72 | }
73 | }
--------------------------------------------------------------------------------
/Tips/Heap-Stack Dance/weak-strong-drance-demo/weak-strong-drance-demo/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Tips/Heap-Stack Dance/weak-strong-drance-demo/weak-strong-drance-demo/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Tips/Heap-Stack Dance/weak-strong-drance-demo/weak-strong-drance-demo/Foo.h:
--------------------------------------------------------------------------------
1 | //
2 | // Foo.h
3 | // weak-strong-drance-demo
4 | //
5 | // Created by 陈宜龙 on 12/21/16.
6 | // Copyright © 2016 ElonChan. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface Foo : NSObject
12 |
13 | @end
14 |
--------------------------------------------------------------------------------
/Tips/Heap-Stack Dance/weak-strong-drance-demo/weak-strong-drance-demo/Foo.m:
--------------------------------------------------------------------------------
1 | //
2 | // Foo.m
3 | // weak-strong-drance-demo
4 | //
5 | // Created by 陈宜龙 on 12/21/16.
6 | // Copyright © 2016 ElonChan. All rights reserved.
7 | //
8 |
9 | #import "Foo.h"
10 |
11 | typedef void (^Completion)(Foo *foo);
12 |
13 | @interface Foo ()
14 |
15 | @property (nonatomic, copy) Completion completion1;
16 | @property (nonatomic, copy) Completion completion2;
17 |
18 | @end
19 |
20 | @implementation Foo
21 |
22 | - (instancetype)init {
23 | if (!(self = [super init])) {
24 | return nil;
25 | }
26 | __weak typeof(self) weakSelf = self;
27 | self.completion1 = ^(Foo *foo) {
28 | NSLog(@"completion1");
29 | };
30 | self.completion2 = ^(Foo *foo) {
31 | __strong typeof(self) strongSelf = weakSelf;
32 | NSLog(@"completion2");
33 | NSUInteger delaySeconds = 2;
34 | dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delaySeconds * NSEC_PER_SEC));
35 | dispatch_after(when, dispatch_get_main_queue(), ^{
36 | NSLog(@"两秒钟后");
37 | foo.completion1(foo);//foo等价于strongSelf
38 | });
39 | };
40 | self.completion2(self);
41 | return self;
42 | }
43 |
44 | - (void)dealloc {
45 | NSLog(@"dealloc");
46 | }
47 |
48 | @end
49 |
--------------------------------------------------------------------------------
/Tips/Heap-Stack Dance/weak-strong-drance-demo/weak-strong-drance-demo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationLandscapeLeft
37 | UIInterfaceOrientationLandscapeRight
38 |
39 | UISupportedInterfaceOrientations~ipad
40 |
41 | UIInterfaceOrientationPortrait
42 | UIInterfaceOrientationPortraitUpsideDown
43 | UIInterfaceOrientationLandscapeLeft
44 | UIInterfaceOrientationLandscapeRight
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/Tips/Heap-Stack Dance/weak-strong-drance-demo/weak-strong-drance-demo/ViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.h
3 | // weak-strong-drance-demo
4 | //
5 | // Created by 陈宜龙 on 12/21/16.
6 | // Copyright © 2016 ElonChan. All rights reserved.
7 | //
8 |
9 | #import
10 | #import "Foo.h"
11 |
12 | @interface ViewController : UIViewController
13 |
14 | @end
15 |
16 |
--------------------------------------------------------------------------------
/Tips/Heap-Stack Dance/weak-strong-drance-demo/weak-strong-drance-demo/ViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.m
3 | // weak-strong-drance-demo
4 | //
5 | // Created by 陈宜龙 on 12/21/16.
6 | // Copyright © 2016 ElonChan. All rights reserved.
7 | //
8 |
9 | #import "ViewController.h"
10 |
11 | @interface ViewController ()
12 |
13 | @end
14 |
15 | @implementation ViewController
16 |
17 | - (void)viewDidLoad {
18 | [super viewDidLoad];
19 | __autoreleasing Foo *foo = [Foo new];
20 | // Do any additional setup after loading the view, typically from a nib.
21 | }
22 |
23 | - (void)didReceiveMemoryWarning {
24 | [super didReceiveMemoryWarning];
25 | // Dispose of any resources that can be recreated.
26 | }
27 |
28 | @end
29 |
--------------------------------------------------------------------------------
/Tips/Heap-Stack Dance/weak-strong-drance-demo/weak-strong-drance-demo/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // weak-strong-drance-demo
4 | //
5 | // Created by 陈宜龙 on 12/21/16.
6 | // Copyright © 2016 ElonChan. All rights reserved.
7 | //
8 |
9 | #import
10 | #import "AppDelegate.h"
11 |
12 | int main(int argc, char * argv[]) {
13 | @autoreleasepool {
14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Tips/iOS常见耗电量检测方案调研/iOS常见耗电量检测方案调研.md:
--------------------------------------------------------------------------------
1 | # iOS 常见耗电量检测方案调研
2 |
3 |
4 | 本文对应 Demo 以及 Markdown 文件在 [ GitHub 仓库中]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/iOS常见耗电量检测方案调研/iOS常见耗电量检测方案调研.md ),文中的错误可以提 PR 到这个文件,我会及时更改。
5 |
6 |
7 |
8 |
9 | - [前言](#%E5%89%8D%E8%A8%80)
10 | - [系统接口](#%E7%B3%BB%E7%BB%9F%E6%8E%A5%E5%8F%A3)
11 | - [测试平台](#%E6%B5%8B%E8%AF%95%E5%B9%B3%E5%8F%B0)
12 | - [常用的电量测试方法:](#%E5%B8%B8%E7%94%A8%E7%9A%84%E7%94%B5%E9%87%8F%E6%B5%8B%E8%AF%95%E6%96%B9%E6%B3%95)
13 | - [软件工具检测](#%E8%BD%AF%E4%BB%B6%E5%B7%A5%E5%85%B7%E6%A3%80%E6%B5%8B)
14 | - [iOS电量测试方法](#ios%E7%94%B5%E9%87%8F%E6%B5%8B%E8%AF%95%E6%96%B9%E6%B3%95)
15 | - [1.iOS 设置选项 ->开发者选项->logging ->start recording](#1ios-%E8%AE%BE%E7%BD%AE%E9%80%89%E9%A1%B9--%E5%BC%80%E5%8F%91%E8%80%85%E9%80%89%E9%A1%B9logging-start-recording)
16 | - [2.进行需要测试电量的场景操作后进入开发者选项点击stop recording](#2%E8%BF%9B%E8%A1%8C%E9%9C%80%E8%A6%81%E6%B5%8B%E8%AF%95%E7%94%B5%E9%87%8F%E7%9A%84%E5%9C%BA%E6%99%AF%E6%93%8D%E4%BD%9C%E5%90%8E%E8%BF%9B%E5%85%A5%E5%BC%80%E5%8F%91%E8%80%85%E9%80%89%E9%A1%B9%E7%82%B9%E5%87%BBstop-recording)
17 | - [3.将iOS设备和Mac连接](#3%E5%B0%86ios%E8%AE%BE%E5%A4%87%E5%92%8Cmac%E8%BF%9E%E6%8E%A5)
18 | - [4.打开Instrument,选择Energy Diagnostics](#4%E6%89%93%E5%BC%80instrument%E9%80%89%E6%8B%A9energy-diagnostics)
19 | - [5.选择 File > Import Logged Data from Device](#5%E9%80%89%E6%8B%A9-file--import-logged-data-from-device)
20 | - [6.保存的数据以时间轴输出到Instrument面板](#6%E4%BF%9D%E5%AD%98%E7%9A%84%E6%95%B0%E6%8D%AE%E4%BB%A5%E6%97%B6%E9%97%B4%E8%BD%B4%E8%BE%93%E5%87%BA%E5%88%B0instrument%E9%9D%A2%E6%9D%BF)
21 | - [其他](#%E5%85%B6%E4%BB%96)
22 | - [硬件检测](#%E7%A1%AC%E4%BB%B6%E6%A3%80%E6%B5%8B)
23 |
24 |
25 |
26 |
27 | ## 前言
28 |
29 | 如果我们想看下我们的 APP 或 SDK 是否耗电,需要给一些数据来展示,所以就对常见的电量测试方案做了一下调研。
30 |
31 | 影响 iOS 电量的因素,几个典型的耗电场景如下:
32 |
33 | 1. 定位,尤其是调用GPS定位
34 | 2. 网络传输,尤其是非Wifi环境
35 | 3. cpu频率
36 | 4. 内存调度频度
37 | 5. 后台运行
38 |
39 |
40 | ## 系统接口
41 |
42 | iOS 10 系统内置的 Setting 里可以查看各个 App 的电池消耗。
43 |
44 | 
45 |
46 | 系统接口,能获取到整体的电池利用率,以及充电状态。代码演示如下:
47 |
48 | ```Objective-C
49 | //#import
50 | UIDevice *device = [UIDevice currentDevice];
51 | device.batteryMonitoringEnabled = YES;
52 | //UIDevice返回的batteryLevel的范围在0到1之间。
53 | NSUInteger batteryLevel = device.batteryLevel * 100;
54 | //获取充电状态
55 | UIDeviceBatteryState state = device.batteryState;
56 | if (state == UIDeviceBatteryStateCharging || state == UIDeviceBatteryStateFull) {
57 | //正在充电和电池已满
58 | }
59 | ```
60 |
61 | 这些均不符合我们的检测需求,不能检测固定某一时间段内的电池精准消耗。
62 |
63 |
64 | ## 测试平台
65 |
66 | **阿里云移动测试[MQC](http://mqc.yunos.com)**
67 |
68 | [MQC](http://mqc.yunos.com) 调研,结论:没有iOS性能测试,无法提供耗电量指标。
69 |
70 | 解释 | 截图
71 | -------------|-------------
72 | 安卓有性能测试项目|  |
73 | 安卓的性能测试项目 | |
74 | iOS没有性能测试,无法提供耗电量指标| 
75 |
76 |
77 | 百度移动云测试中心 MTC 同样没有 iOS 的性能测试。
78 |
79 | 其他测试平台类似。
80 |
81 | ## 常用的电量测试方法:
82 |
83 | 1. 硬件测试
84 | 2. 软件工具检测
85 |
86 |
87 |
88 | ## 软件工具检测
89 |
90 | 下面介绍通过软件 Instrument 来进行耗电检测。
91 |
92 |
93 |
94 | ## iOS电量测试方法
95 |
96 | #### 1.iOS 设置选项 ->开发者选项->logging ->start recording
97 |
98 | 
99 |
100 | #### 2.进行需要测试电量的场景操作后进入开发者选项点击stop recording
101 | #### 3.将iOS设备和Mac连接
102 | #### 4.打开Instrument,选择Energy Diagnostics
103 | #### 5.选择 File > Import Logged Data from Device
104 |
105 | 
106 |
107 |
108 | #### 6.保存的数据以时间轴输出到Instrument面板
109 | 
110 |
111 | #### 其他
112 |
113 | - 测试过程中要断开 iOS设备和电脑、电源的连接
114 | - 电量使用level为0-20,1/20:表示运行该app,电池生命会有20个小时;20/20:表示运行该app,电池电量仅有1小时的生命
115 | - 数据不能导出计算,只能手动计算平均值
116 |
117 |
118 | ## 硬件检测
119 |
120 | 通过硬件 [PowerMonitor]( https://www.msoon.com/LabEquipment/PowerMonitor/ ) 可以精准地获得应用的电量消耗。
121 |
122 | 步骤如下:
123 |
124 | 1. 拆开iOS设备的外壳,找到电池后面的电源针脚。
125 | 2. 连接电源监控器的设备针脚
126 | 3. 运行应用
127 | 4. 测量电量消耗
128 |
129 | 下图展示了与iPhone的电池针脚连接的电源监控器工具。
130 |
131 | 
132 |
133 | 可以参考:[**Using Monsoon Power Monitor with iPhone 5s**]( https://www.bottleofcode.com/2015/07/12/using-monsoon-power-monitor-with-iphone-5s/)。
134 |
135 | - 可以精准地获得应用的电量消耗。
136 | - 设备价格 $771.00 USD
137 | - 需要拆解手机
138 |
139 |
140 | 这样看来,只有 Instrument 的方案更适合,大家有什么方案的话,也可以贴在下面。
141 |
142 |
--------------------------------------------------------------------------------
/Tips/基于Websocket的IM即时通讯技术/IM 即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md:
--------------------------------------------------------------------------------
1 | # IM 即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角)
2 |
3 | - 文章备份地址: [《IM 即时通讯技术在多应用场景下的技术实现,以及性能调优( iOS 视角)(附 PPT 与 2 个半小时视频)》]( https://www.jianshu.com/p/8cd908148f9e ) (防止图床挂了)
4 | - 演讲视频(上下两部,时长将近2个半小时)以及 PPT 下载:链接: https://pan.baidu.com/s/1FfhxcRImvwL7w38ZXnnzaw 密码: hb1y
5 | - 油管在线观看 [《IM 即时通讯技术在多应用场景下的技术实现,以及性能调优( iOS 视角)(附 PPT 与 2 个半小时视频)》]( https://youtu.be/yIOlzzA_dRQ "")
6 |
7 |
8 | 2016年9月份[我](https://github.com/ChenYilong)参加了 MDCC2016(中国移动开发者大会),
9 |
10 | 
11 |
12 | 在 MDCC2016 上我做了关于 IM 相关分享,会上因为有50分钟的时间限制 ,所以有很多东西都没有展开,这篇是演讲稿的博文版本,比会上讲得更为详细。有些演讲时一笔带过的部分,在文中就可以展开讲讲。
13 |
14 | 
15 |
16 | 注:
17 |
18 | - 本文中所涉及到的所有 iOS 端相关代码,均已100%开源(不存在 framework ),便于学习参考。
19 | - 本文侧重移动端的设计与实现,会展开讲,服务端仅仅属于概述,不展开。
20 | - 为大家在设计或改造优化 IM 模块时,提供一些参考。
21 |
22 | 我现在任职于 [LeanCloud(原名 `AVOS` )](https://leancloud.cn/?source=T6M35E4H) 。LeanCloud 是国内较早提供 IM 服务的 Paas 厂商,提供 IM 相关的 SDK 供开发者使用,现在采纳我们 IM 方案的 APP 有:知乎Live、掌上链家、懂球帝等等,在 IM 方面也积累了一些经验,这次就在这篇博文分享下。
23 |
24 | 
25 |
26 | ## IM系列文章
27 |
28 | IM 系列文章分为下面这几篇:
29 |
30 | - [《IM 即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角)》](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md) (本文)
31 | - [《技术实现细节》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/技术实现细节.md )
32 | - [《有一种 Block 叫 Callback,有一种 Callback 做 CompletionHandler》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/有一种%20Block%20叫%20Callback,有一种%20Callback%20做%20CompletionHandler.md )
33 | - [《防 DNS 污染方案》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/防%20DNS%20污染方案.md )
34 |
35 | 本文是第一篇。
36 |
37 | ## 提纲
38 |
39 | 1. [应用场景](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#应用场景)
40 | 1. [IM 发展史](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#im-发展史)
41 | 2. [大家都在使用什么技术](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#大家都在使用什么技术)
42 | 3. [社交场景](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#社交场景)
43 | 4. [直播场景](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#直播场景)
44 | 5. [数据自动更新场景](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#数据自动更新场景)
45 | 6. [电梯场景(假在线状态处理)](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#电梯场景假在线状态处理)
46 | 2. [技术实现细节](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#技术实现细节)
47 | 1. [基于 WebSocket 的 IM 系统](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#基于-websocket-的-im-系统)
48 | 2. [更多](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#更多)
49 |
50 | 3. [性能调优 -- 针对移动网络特点的性能调优](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#性能调优----针对移动网络特点的性能调优)
51 | 1. [极简协议,传输协议 Protobuf](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#极简协议传输协议-protobuf)
52 | 2. [在安全上需要做哪些事情?](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#在安全上需要做哪些事情)
53 | 1. [防止 DNS 污染](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#防止-dns-污染)
54 | 2. [账户安全](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#账户安全)
55 | 3. [重连机制](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#重连机制)
56 | 4. [使用 HTTP/2 减少不必要的网络连接](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#使用-http2-减少不必要的网络连接)
57 | 5. [设置合理的超时时间](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#设置合理的超时时间)
58 | 6. [图片视频等文件上传](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#图片视频等文件上传)
59 | 7. [使用缓存:基于 Hash 的本地缓存校验](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md#使用缓存基于-hash-的本地缓存校验)
60 |
61 | ### 大规模即时通讯技术上的难点
62 |
63 | 思考几个问题:
64 |
65 | - 如何在移动网络环境下优化电量,流量,及长连接的健壮性?现在移动网络有2G、3G、4G各种制式,并且随时可能切换和中断,移动网络优化可以说是面向移动服务的共同问题。
66 | - 如何确保IM系统的整体安全?因为用户的消息是个人隐私,因此要从多个层面来保证IM系统的安全性。
67 | - 如何降低开发者集成门槛?
68 | - 如何应对新的iOS生态下的政策以及结合新技术:比如HTTP/2、IPv6、新的APNs协议等。
69 |
70 | ## 应用场景
71 |
72 | 一个 IM 服务最大的价值在于什么?
73 |
74 | 可复用的长连接。一切高实时性的场景,都适合使用IM来做。
75 |
76 | 比如:
77 |
78 | - 视频会议、聊天、私信
79 | - 弹幕、抽奖
80 | - 互动游戏
81 | - 协同编辑
82 | - 股票基金实时报价、体育实况更新、
83 | - 基于位置的应用:Uber、滴滴司机位置
84 | - 在线教育
85 | - 智能家居
86 |
87 | 下文会挑一些典型的场景进行介绍,并涉及到技术细节。
88 |
89 | ### IM 发展史
90 |
91 | 基本的发展历程是:轮询、长轮询、长连接。
92 |
93 | 挑一些代表性的技术做下介绍:
94 |
95 | **一般的网络请求:一问一答**
96 |
97 | 
98 |
99 | **轮询:频繁的一问一答**
100 |
101 | 
102 |
103 | **长轮询:耐心地一问一答**
104 |
105 | 
106 |
107 |
108 | 一种轮询方式是否为长轮询,是根据服务端的处理方式来决定的,与客户端没有关系。
109 |
110 | 短轮询很容易理解,那么什么叫长轮询?与短轮询有什么区别。
111 |
112 | 举个例子:
113 |
114 | 比如中秋节我们要做一个秒杀月饼的页面,要求我们要实时地展示剩余的月饼数量,也就是库存量。这时候如果要求你只能用短轮询或长轮询去做,怎么做呢?
115 |
116 | 
117 |
118 | 长轮询和短轮询最大的区别是,短轮询去服务端查询的时候,不管服务端有没有变化,服务器就立即返回结果了。而长轮询则不是,在长轮询中,服务器如果检测到库存量没有变化的话,将会把当前请求挂起一段时间(这个时间也叫作超时时间,一般是几十秒)。在这个时间里,服务器会去检测库存量有没有变化,检测到变化就立即返回,否则就一直等到超时为止,这就是区别。
119 |
120 | (实际开发中不会使用长短轮询来做这种需求,这里仅仅是为了说明两者区别而做的一个例子。)
121 |
122 | 长轮询曾被 Facebook 早起版本采纳,示意图如下:
123 |
124 | 
125 |
126 | **HTML5 WebSocket: 双向**
127 |
128 | 
129 |
130 | 参考: [What are Long-Polling, Websockets, Server-Sent Events (SSE) and Comet?](http://stackoverflow.com/a/12855533/3395008)
131 |
132 |
133 | 我们可以看到,发展历史是这样:从长短轮询到长连接,使用 WebSocket 来替代 HTTP。
134 |
135 | 其中长短轮询与长短连接的区别主要有:
136 |
137 | 1. 概念范畴不同:长短轮询是应用层概念、长短连接是传输层概念
138 | 2. 协商方式不同:一个 TCP 连接是否为长连接,是通过设置 HTTP 的 Connection Header 来决定的,而且是需要两边都设置才有效。而一种轮询方式是否为长轮询,是根据服务端的处理方式来决定的,与客户端没有关系。
139 | 3. 实现方式不同:连接的长短是通过协议来规定和实现的。而轮询的长短,是服务器通过编程的方式手动挂起请求来实现的。
140 |
141 | **在移动端上长连接是趋势。**
142 |
143 | 其最大的特点是节省 Header。
144 |
145 | **轮询与 WebSocket 所花费的Header流量对比**:
146 |
147 | 让我们来作一个测试:
148 |
149 | 假设 Header 是871字节,
150 |
151 | 我们以相同的频率 10W/s 去做网络请求, 对比下轮询与 WebSocket 所花费的 Header 流量:
152 |
153 | Header 包括请求和响应头信息。
154 |
155 | 出于兼容性考虑,一般建立 WebSocket 连接也采用 HTTP 请求的方式,那么从这个角度讲:无论请求如何频繁,都只需要一个 Header。
156 |
157 | 并且 Websocket 的数据传输是 frame 形式传输的,帧传输更加高效,对比轮询的2个 Header,这里只有一个 Header 和一个 frame。
158 |
159 | 而 Websocket 的frame 仅仅用2个字节就代替了轮询的871字节!
160 |
161 | 
162 |
163 | 相同的每秒客户端轮询的次数,当次数高达 10W/s 的高频率次数的时候,Polling 轮询需要消耗665Mbps,而 WebSocket 仅仅只花费了1.526Mbps,将近435倍!!
164 |
165 | 数据参考:
166 |
167 | 1. [HTML5 WebSocket: A Quantum Leap in Scalability for the Web](https://www.websocket.org/quantum.html)
168 | 2. [《微信,QQ这类IM app怎么做——谈谈Websocket》]( http://www.jianshu.com/p/bcefda55bce4 )
169 |
170 | 下面探讨下长连接实现方式里的协议选择:
171 |
172 | ### 大家都在使用什么技术
173 |
174 | 最近做了两个 IM 相关的问卷,累计产生了900多条的投票数据:
175 |
176 | 1. [《你项目中使用什么协议实现了 IM 即时通讯》]( http://vote.weibo.com/poll/137494424)
177 | 2. [《IM 即时通讯中你会选用什么数据传输格式?》](http://vote.weibo.com/poll/137505291)
178 |
179 | 注:本次投票是发布在[微博@iOS程序犭袁](http://weibo.com/luohanchenyilong) ,鉴于微博关注机制,本数据只能反映出 IM 技术在 iOS 领域的使用情况,并不能反映出整个IT行业的情况。
180 |
181 | 下文会对这个投票结果进行下分析。
182 |
183 | 
184 |
185 | 
186 |
187 | 投票结果 [《你项目中使用什么协议实现了 IM 即时通讯》]( http://vote.weibo.com/poll/137494424)
188 |
189 | **协议如何选择?**
190 |
191 | IM 协议选择原则一般是:易于拓展,方便覆盖各种业务逻辑,同时又比较节约流量。后一点的需求在移动端 IM 上尤其重要。常见的协议有:XMPP、SIP、MQTT、私有协议。
192 |
193 | 我们这里只关注前三名,
194 |
195 | 名称 | 优点 | 缺点
196 | -------------|-------------|-------------
197 | XMPP | 优点:协议开源,可拓展性强,在各个端(包括服务器)有各种语言的实现,开发者接入方便; | 缺点:缺点也是不少,XML表现力弱、有太多冗余信息、流量大,实际使用时有大量天坑。
198 | MQTT | 优点:协议简单,流量少;订阅+推送模式,非常适合Uber、滴滴的小车轨迹的移动。 | 缺点:它并不是一个专门为 IM 设计的协议,多使用于推送。IM 情景要复杂得多,pub、sub,比如:加入对话、创建对话等等事件。
199 | 私有协议 | 市面上几乎所有主流IM APP都是是使用私有协议,一个被良好设计的私有协议优点非常明显。优点:高效,节约流量(一般使用二进制协议),安全性高,难以破解;| 缺点:在开发初期没有现有样列可以参考,对于设计者的要求比较高。
200 |
201 | 一个好的协议需要满足如下条件:高效,简洁,可读性好,节约流量,易于拓展,同时又能够匹配当前团队的技术堆栈。基于如上原则,我们可以得出: 如果团队小,团队技术在 IM 上积累不够可以考虑使用 XMPP 或者 MQTT+HTTP 短连接的实现。反之可以考虑自己设计和实现私有协议,这里建议团队有计划地迁移到私有协议上。
202 |
203 | 这里特别提一下排名第二的 WebSocket ,区别于上面的聊天协议,这是一个传输通讯协议,那为什么会有这么多人在即时通讯领域运用了这一协议?除了上文说的长连接特性外,这个协议 web 原生支持,有很多第三方语言实现,可以搭配 XMPP、MQTT 等多种聊天协议进行使用,被广泛地应用于即时通讯领。
204 |
205 | #### 社交场景
206 |
207 | 最大的特点在于:模式成熟,界面类似。
208 |
209 | 我们专门为社交场景开发的开源组件:ChatKit-OC,star数,1000+。
210 |
211 | ChatKit-OC 在协议选择上使用的是 WebSocket 搭配私有聊天协议的方式,在数据传输上选择的是 Protobuf 搭配 JSON 的方式。
212 |
213 | 项目地址:[ChatKit-OC]( https://github.com/leancloud/ChatKit-OC )
214 |
215 | 
216 |
217 | 下文会专门介绍下技术实现细节。
218 |
219 | #### 直播场景
220 |
221 | 一个演示如何为直播集成 IM 的开源直播 Demo:
222 |
223 | 项目地址:[LiveKit-iOS](https://github.com/leancloud/LeanCloudLiveKit-iOS)
224 |
225 | (这个库,我最近也在优化,打算做成 Lib,支持下 CocoaPods 。希望能帮助大家快速集成直播模块。有兴趣的也欢迎参与进来提 PR)
226 |
227 | LiveKit 相较社交场景的特点:
228 |
229 | - 无人数限制的聊天室
230 | - 自定义消息
231 | - 打赏机制的服务端配合
232 |
233 | 有人可能有这样的疑问:
234 |
235 | 
236 |
237 | (叫我Elon(读:一龙)就好了)
238 |
239 | 那么可以看下 Demo 的实现:我们可以看到里面的弹幕、礼物、点赞出心这些都是 IM 系统里的自定义消息。
240 |
241 | 
242 |
243 | 
244 |
245 | 
246 |
247 | #### 数据自动更新场景
248 |
249 | - 打车应用场景(Uber、滴滴等 APP 首页的移动小车)
250 | - 朋友圈状态的实施更新,朋友圈自己发送的消息无需刷新,自动更新
251 |
252 | 这些场景比聊天要简单许多,仅仅涉及到监听对象的订阅、取消订阅。
253 | 正如上文所提到的,使用 MQTT 实现最为经济。用社交类、直播类的思路来做,也可以实现,但略显冗余。
254 |
255 | #### 电梯场景(假在线状态处理)
256 |
257 | iOS端的假在线的状态,有两种方案:
258 |
259 | - 双向ping pong机制
260 | - iOS端只走APNs
261 |
262 | **双向 ping-pong 机制**:
263 |
264 | Message 在发送后,在服务端维护一个表,一段时间内,比如15秒内没有收到 ack,就认为应用处于离线状态,先将用户踢下线,然后转而进行推送。这里如果出现,重复推送,客户端要负责去重。将 Message 消息相当于服务端发送的 Ping 消息,APP 的 ack 作为 pong。
265 |
266 | 
267 |
268 | **使用 APNs 来作聊天**
269 |
270 | 优缺点:
271 |
272 | 优点:
273 |
274 | - 解决了,iOS端假在线的问题。
275 |
276 | 缺点:(APNs的缺点)
277 |
278 | - 无法保证消息的及时性。
279 | - 让服务端负载过重
280 |
281 | APNs不保证消息的到达率,消息会被折叠:
282 |
283 | 你可能见过这种推送消息:
284 |
285 | 
286 |
287 | 这中间发生了什么?
288 |
289 | 当 APNs 向你发送了4条推送,但是你的设备网络状况不好,在 APNs 那里下线了,这时 APNs 到你的手机的链路上有4条任务堆积,APNs 的处理方式是,只保留最后一条消息推送给你,然后告知你推送数。那么其他三条消息呢?会被APNs丢弃。
290 |
291 | 有一些 App 的 IM 功能没有维持长连接,是完全通过推送来实现的,通常情况下,这些 App 也已经考虑到了这种丢推送的情况,这些 App 的做法都是,每次收到推送之后,然后向自己的服务器查询当前用户的未读消息。但是 APNs 也同样无法保证这四条推送能至少有一条到达你的 App。
292 |
293 | 为什么这么设计?APNs的存储-转发能力太弱,大量的消息存储和转发将消耗 Apple 服务器的资源,可能是出于存储成本考虑,也可能是因为 Apple 转发能力太弱。总之结果就是 APNs 从来不保证消息的达到率。并且设备上线之后也不会向服务器上传信息。
294 |
295 | 现在我们可以保证消息一定能推送到 APNs 那里,但是 APNs 不保证帮我们把消息投递给用户。
296 |
297 | 即使搭配了这样的策略:每次收到推送就拉历史记录的消息,一旦消息被 APNs 丢弃,这条消息可能会在几天之后受到了新推送后才被查询到。
298 |
299 | 让服务端负载过重:
300 |
301 | APNs 的实现原理决定了:必须每次收到消息后,拉取历史消息。这意味着你无法控制 APP 请求服务端的频率,同一时间十万、百万的请求量都是可能的,这带来的负载以及风险,有时甚至会比轮询还要大。
302 |
303 | 参考:[《基于HTTP2的全新APNs协议》](https://github.com/ChenYilong/iOS9AdaptationTips/blob/master/基于HTTP2的全新APNs协议/基于HTTP2的全新APNs协议.md)
304 |
305 | 结论:如果面向的目标用户对消息的及时性并不敏感,可以采用这种方案。比如社交场景。(对消息较为敏感的APP则并不适合,比如:专门为情侣间使用的APP。。。)
306 |
307 | ### 技术实现细节
308 |
309 | ###基于 WebSocket 的 IM 系统
310 |
311 | **WebSocket简介**
312 |
313 | WebSocket 是 HTML5 开始提供的一种浏览器与服务器间进行全双工通讯的网络技术。 WebSocket 通信协定于2011年被 IETF 定为标准 RFC 6455,WebSocket API 被 W3C 定为标准。
314 |
315 | 在 WebSocket API 中,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
316 |
317 | 只从 RFC 发布的时间看来,WebSocket要晚很多,HTTP 1.1是1999年,WebSocket 则是12年之后了。WebSocket 协议的开篇就说,本协议的目的是为了解决基于浏览器的程序需要拉取资源时必须发起多个HTTP请求和长时间的轮训的问题而创建的。可以达到支持 iOS,Android,Web 三端同步的特性。
318 |
319 | ### 更多
320 |
321 | **技术实现细节的部分较长,单独成篇。**: [《技术实现细节》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/技术实现细节.md )
322 |
323 |
324 | 下面是文章的第二部分:
325 |
326 | ### 性能调优 -- 针对移动网络特点的性能调优
327 |
328 | #### 极简协议,传输协议 Protobuf
329 |
330 | 目录如下:
331 |
332 | 1. 极简协议,传输协议 Protobuf
333 | 2. 在安全上做了哪些事情?
334 | 1. 防止 DNS 污染
335 | 2. 账户安全
336 | 3. 重连机制
337 | 4. 使用 HTTP/2 减少不必要的网络连接
338 | 5. 设置合理的超时时间
339 | 6. 图片视频等文件上传
340 | 7. 使用缓存:基于 Hash 的本地缓存校验
341 |
342 |
343 | 首先让我们来看下:
344 |
345 | **IM 即时通讯中你会选用什么数据传输格式?**
346 |
347 | 之前做的调研数据如下:
348 |
349 | 
350 |
351 | 
352 |
353 | [《IM 即时通讯中你会选用什么数据传输格式?》](http://vote.weibo.com/poll/137505291)
354 |
355 | 注:本次投票是发布在[微博@iOS程序犭袁](http://weibo.com/luohanchenyilong) ,鉴于微博关注机制,本数据只能反映出 IM 技术在 iOS 领域的使用情况,并不能反映出整个IT行业的情况。
356 |
357 | 排名前三的分别的JSON 、ProtocolBuffer、XML;
358 |
359 | 这里重点推荐下 ProtocolBuffer:
360 |
361 | 该协议已经在业内有很多应用,并且效果显著:
362 |
363 | **使用 ProtocolBuffer 减少 Payload**
364 |
365 | - 滴滴打车40%;
366 | - 携程之前分享过,说是采用新的Protocol Buffer数据格式+Gzip压缩后的Payload大小降低了15%-45%。数据序列化耗时下降了80%-90%。
367 |
368 | 采用高效安全的私有协议,支持长连接的复用,稳定省电省流量
369 |
370 | 1. 【高效】提高网络请求成功率,消息体越大,失败几率随之增加。
371 | 2. 【省流量】流量消耗极少,省流量。一条消息数据用Protobuf序列化后的大小是 JSON 的1/10、XML格式的1/20、是二进制序列化的1/10。同 XML 相比, Protobuf 性能优势明显。它以高效的二进制方式存储,比 XML 小 3 到 10 倍,快 20 到 100 倍。
372 | 3. 【省电】省电
373 | 4. 【高效心跳包】同时心跳包协议对IM的电量和流量影响很大,对心跳包协议上进行了极简设计:仅 1 Byte 。
374 | 5. 【易于使用】开发人员通过按照一定的语法定义结构化的消息格式,然后送给命令行工具,工具将自动生成相关的类,可以支持java、c++、python、Objective-C等语言环境。通过将这些类包含在项目中,可以很轻松的调用相关方法来完成业务消息的序列化与反序列化工作。语言支持:原生支持c++、java、python、Objective-C等多达10余种语言。 2015-08-27 Protocol Buffers v3.0.0-beta-1中发布了Objective-C(Alpha)版本, 2016-07-28 3.0 Protocol Buffers v3.0.0正式版发布,正式支持 Objective-C。
375 | 6. 【可靠】微信和手机 QQ 这样的主流 IM 应用也早已在使用它(采用的是改造过的Protobuf协议)
376 |
377 | 
378 |
379 | 如何测试验证 Protobuf 的高性能?
380 |
381 | 对数据分别操作100次,1000次,10000次和100000次进行了测试,
382 |
383 | 纵坐标是完成时间,单位是毫秒,
384 |
385 | 反序列化 | 序列化 | 字节长度
386 | -------------|-------------|-------------
387 | | |
388 |
389 | [数据来源](http://www.cnblogs.com/beyondbit/p/4778264.html)。
390 |
391 | 
392 |
393 | 数据来自:项目 [thrift-protobuf-compare]( https://github.com/eishay/jvm-serializers/wiki ),测试项为 Total Time,也就是 指一个对象操作的整个时间,包括创建对象,将对象序列化为内存中的字节序列,然后再反序列化的整个过程。从测试结果可以看到 Protobuf 的成绩很好.
394 |
395 | 缺点:
396 |
397 | 可能会造成 APP 的包体积增大,通过 Google 提供的脚本生成的 Model,会非常“庞大”,Model 一多,包体积也就会跟着变大。
398 |
399 | 如果 Model 过多,可能导致 APP 打包后的体积骤增,但 IM 服务所使用的 Model 非常少,比如在 ChatKit-OC 中只用到了一个 Protobuf 的 Model:Message对象,对包体积的影响微乎其微。
400 |
401 | 在使用过程中要合理地权衡包体积以及传输效率的问题,据说去哪儿网,就曾经为了减少包体积,进而减少了 Protobuf 的使用。
402 |
403 | #### 在安全上需要做哪些事情?
404 |
405 | ##### 防止 DNS 污染
406 |
407 | **文章较长,单独成篇。**: [《防 DNS 污染方案.md》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/防%20DNS%20污染方案.md )
408 |
409 | ##### 账户安全
410 |
411 | IM 服务账号密码一旦泄露,危害更加严峻。尤其是对于消息可以漫游的类型。比如:
412 | 
413 |
414 | 介绍下我们是如何做到,即使是我们的服务器被攻破,你的用户系统依然不会受到影响:
415 |
416 | 1. 帐号安全:
417 |
418 | 无侵入的权限控制:
419 | 与用户的用户帐号体系完全隔离,只需要提供一个ID就可以通信,接入方可以对该 ID 进行 MD5 加密后再进行传输和存储,保证开发者用户数据的私密性及安全。
420 |
421 | 2. 签名机制
422 |
423 | 对关键操作,支持第三方服务器鉴权,保护你的信息安全。
424 |
425 | 
426 |
427 | 参考: [《实时通信服务总览-权限和认证》](https://leancloud.cn/docs/realtime_v2.html#权限和认证 )
428 |
429 | 3. 单点登录
430 |
431 | 让 APP 支持单点登录,能有限减少盗号造成的安全问题。在 ChatKit-OC 中,我们就默认开启了单点登录功能,以此来提升 APP 的安全性。
432 |
433 | #### 重连机制
434 |
435 | - 精简心跳包,保证一个心跳包大小在10字节之内;
436 | - 减少心跳次数:心跳包只在空闲时发送;从收到的最后一个指令包进行心跳包周期计时而不是固定时间。
437 | - 重连冷却
438 | 2的指数级增长2、4、8,消息往来也算作心跳。类似于 iPhone 密码的 错误机制,冷却单位是5分钟,依次是5分钟后、10分钟后、15分钟后,10次输错,清除数据。
439 |
440 | 当然,这样灵活的策略也同样决定了,只能在 APP 层进行心跳ping。
441 |
442 | 
443 |
444 | 这里有必要提一下重连机制的必要性,我们知道 TCP 也有保活机制,但这个与我们在这里讨论的“心跳保活”机制是有区别的。
445 |
446 | TCP 保活(TCP KeepAlive 机制)和心跳保活区别:
447 |
448 | TCP保活 | 心跳保活
449 | -------------|-------------
450 | 在定时时间到后,一般是 7200 s,发送相应的 KeepAlive 探针。,失败后重试 10 次,每次超时时间 75 s。(详情请参见《TCP/IP详解》中第23章) | 通常可以设置为3-5分钟发出 Ping
451 | 检测**连接**的死活(对应于下图中的1) | 检测通讯**双方**的存活状态(对应于下图中的2)
452 |
453 | 保活,究竟保的是谁?
454 |
455 | 
456 |
457 | 比如:考虑一种情况,某台服务器因为某些原因导致负载超高,CPU 100%,无法响应任何业务请求,但是使用 TCP 探针则仍旧能够确定连接状态,这就是典型的连接活着但业务提供方已死的状态,对客户端而言,这时的最好选择就是断线后重新连接其他服务器,而不是一直认为当前服务器是可用状态,一直向当前服务器发送些必然会失败的请求。
458 |
459 | #### 使用 HTTP/2 减少不必要的网络连接
460 |
461 | 大多数的移动网络(3G)并不允许一个给定 IP 地址超过两个的并发 HTTP 请求,既当你有两个针对同一个地址的连接时,再发起的第三个连接总是会超时。而2G网络下这个限定为1个。同一时间发起过多的网络请求不仅不会起到加速的效果,反而有副作用。
462 |
463 | 另一方面,由于网络连接很是费时,保持和共享某一条连接就是一个不错的选择:比如短时间内多次的HTTP请求。
464 |
465 | 使用 HTTP/2 就可以达到这样的目的。
466 |
467 | > HTTP/2 是 HTTP 协议发布后的首个更新,于2015年2月17日被批准。它采用了一系列优化技术来整体提升 HTTP 协议的传输性能,如异步连接复用、头压缩等等,可谓是当前互联网应用开发中,网络层次架构优化的首选方案之一。
468 |
469 | HTTP/2 也以高复用著称,而且如果我们要使用 HTTP/2,那么在网络库的选择上必然要使用 NSURLSession。所以 AFN2.x 也需要升级到AFN3.x.
470 |
471 | #### 设置合理的超时时间
472 |
473 | 过短的超时容易导致连接超时的事情频频发生,甚至一直无法连接,而过长的超时则会带来等待时间过长,体验差的问题。就目前来看,对于普通的TCP连接30秒是个不错的超时值,而Http请求可以按照重要性和当前网络情况动态调整超时,尽量将超时控制在一个合理的数值内,以提高单位时间内网络的利用率。
474 |
475 | #### 图片视频等文件上传
476 |
477 | 图片格式优化在业界已有成熟的方案,例如 Facebook 使用的 WebP 图片格式,已经被国内众多 App 使用。
478 |
479 | 分片上传、断点续传、秒传技术、
480 |
481 | - 文件分块上传:因为移动网络丢包严重,将文件分块上传可以使得一个分组包含合理数量的TCP包,使得重试概率下降,重试代价变小,更容易上传到服务器;
482 | - 提供文件秒传的方式:服务器根据MD5、SHA进行文件去重;
483 | - 支持断点续传。
484 | - 上传失败,合理的重连,比如3次。
485 |
486 | #### 使用缓存:基于 Hash 的本地缓存校验
487 |
488 | 微信是不用考虑消息同步问题,因为微信是不存储历史记录的,卸载重装消息记录就会丢失。
489 |
490 | 所以我们可以采用一个类似 E-Tag、Last-Modified 的本地消息缓存校验机制,具体做法就是,当我们想加载最近10条的聊天记录时,先将本地缓存的最近10条做一个 hash 值,将 hash 值发送给服务端,服务端将服务端的最近十条做一个 hash ,如果一致就返回304。最理想的情况是服务端一直返回304,一直加载本地记录。这样做的好处:
491 |
492 | - 消息同步
493 | - 节省流量
494 |
495 | ## IM系列文章
496 |
497 | IM 系列文章分为下面这几篇:
498 |
499 | - [《IM 即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角)》](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md) (本文)
500 | - [《技术实现细节》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/技术实现细节.md )
501 | - [《有一种 Block 叫 Callback,有一种 Callback 做 CompletionHandler》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/有一种%20Block%20叫%20Callback,有一种%20Callback%20做%20CompletionHandler.md )
502 | - [《防 DNS 污染方案》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/防%20DNS%20污染方案.md )
503 |
504 | 本文是第一篇。
505 |
506 |
507 | ----------
508 |
509 | Posted by [微博@iOS程序犭袁](http://weibo.com/luohanchenyilong/)
510 | 原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | [Creative Commons BY-NC-ND 3.0](http://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
511 |
512 |
--------------------------------------------------------------------------------
/Tips/基于Websocket的IM即时通讯技术/技术实现细节.md:
--------------------------------------------------------------------------------
1 | #技术实现细节
2 |
3 | ## IM系列文章
4 |
5 | IM系列文章分为下面这几篇:
6 |
7 | - [《IM 即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角)》](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md)
8 | - [《技术实现细节》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/技术实现细节.md ) (本文)
9 | - [《有一种 Block 叫 Callback,有一种 Callback 做 CompletionHandler》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/有一种%20Block%20叫%20Callback,有一种%20Callback%20做%20CompletionHandler.md )
10 | - [《防 DNS 污染方案》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/防%20DNS%20污染方案.md )
11 |
12 |
13 | 本文是第二篇。
14 |
15 | 本文将以开源项目 ChatKit-OC 为例进行介绍:
16 |
17 | ## 正文
18 |
19 | ChatKit-OC 我们专门为社交场景开发的开源组件:ChatKit-OC,star数,1000+。
20 |
21 | 项目地址:[ChatKit-OC]( https://github.com/leancloud/ChatKit-OC )
22 |
23 | 
24 |
25 | 下文会专门介绍下技术实现细节。
26 |
27 | 先看下现在 IM 领域的轮子有什么问题:
28 |
29 | - Demo 太多,是时候该来一款 Lib 了;
30 | - 闭源的太多,是时候来一款开源的了;
31 | - 部分开源的太多,是时候来一款 100% 开源的了(iOS 端)
32 | - 手撕 Frame 的太多,是时候来一 AutoLayout 款了;
33 | - 自定义能力太弱的太多,是时候来一款可高度自定义的了;
34 |
35 | [ChatKit-OC]( https://github.com/leancloud/ChatKit-OC ) 正是为解决这些问题做的,它的特点如下:
36 |
37 | - 集成方法简单,但可扩展性好。
38 | - iOS 端代码完全开源,你能看到完整的建立 Socket 连接,以及维持心跳的所有步骤。
39 | - 原生语言开发,利于调试,并非采用C++库。
40 | - Masonry 布局
41 | - 友好的 API 设计
42 | - 接地气
43 | - 支持 CocoaPods
44 | - 不需要改源码,不需要设 Delegate
45 | - 不需要在代码里调整聊天气泡位置
46 |
47 | 效果展示:
48 |
49 | 
50 |
51 | 
52 |
53 | 
54 |
55 | 
56 |
57 | 
58 |
59 | 最近联系人 | 语音消息,根据语音长度调整宽度 | 图片消息,尺寸自适应
60 | -------------|-------------|-------------|-------------
61 | | | 
62 |
63 | 地理位置消息| 失败消息本地缓存,可重发 |上传图片,进度条提示
64 | -------------|-------------|-------------
65 |  | | 
66 |
67 | 图片消息支持多图联播,支持多种分享 |文本消息支持图文混排| 文本消息支持双击全屏展示
68 | -------------|-------------|------------
69 |  |  | 
70 |
71 | ## 紧凑的API设计:门面模式
72 |
73 | 
74 |
75 | 使用门面模式,使接入方仅仅需要与一个类打交道,也就使得 API 更加紧凑。
76 |
77 | **极简参数:**
78 |
79 | 像 UIAlertView 一样,你只需要使用 title 去 init 后,然后直接调用 show 就能达到预期的效果。采用了类似的 API 设计,
80 |
81 | **隐藏细节:**
82 |
83 | 设计 API时尽量隐藏细节,尽量提供默认实现。
84 |
85 | 就像 SDWebImage 被使用最多的功能就是 TableView 图片的加载,
86 |
87 | 只需要传入占位图和图片 URL 两个参数,就能完成复杂的异步加载展示。
88 |
89 | ## 无侵入的用户系统接入
90 |
91 | 需求:与用户的用户帐号体系完全隔离,只需要提供一个 ID 就可以通信,接入方可以对该 ID 进行 MD5 加密后再进行传输和存储,保证开发者用户数据的私密性及安全。
92 |
93 | 基本的流程示意:
94 |
95 | 
96 |
97 | 正如图中所示,ChatKit 在需要展现用户图像等信息到 UI 上时,会拿着 ID 去回调一个 Block,这个 Block 需要将对应的用户头像等信息 callback 给 ChatKit,进而展现在 UI 上。
98 |
99 | 其中最关键的部分是,ChatKit 提供的这个 Block,这个 Block 需要用户自己实现。对这个 Block 的具体实现感兴趣的话,可以看下这篇:
100 |
101 | [《有一种 Block 叫 Callback,有一种 Callback 做 CompletionHandler》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/有一种%20Block%20叫%20Callback,有一种%20Callback%20做%20CompletionHandler.md )
102 |
103 | ## 面向 ID 编程
104 |
105 | 1. 底层协议:Client -- ClientID
106 | 2. 上层UI:Conversation -- ConversationID
107 |
108 | ClientId:
109 |
110 | WebSocket 通信是 Session 对 Session 的通信,我们将概念简化为 Id 对 Id 的通信,这样一来,两个 APP 只需要 ID 就可以聊起来,这个 ID 我们取名叫 ClientId。
111 |
112 | 在 ChatKit-OC 的 UI 实现中,体现在对话页面的初始化,你只需要传递一个id,对话id,即可。你同样可以使用ConversationID也就是对话ID来初始化。这样简洁的 API 设计,做法类似 UIAlertView 的初始化只需要传一个 title 就可以。
113 |
114 | Conversation(对话)这个概念,我们在使用其他的聊天协议时,比如XMPP,会有单聊和群聊之分,我们在 ChatKit-OC 中,将这个概念简化,不区分单聊群聊。这样一来,你在初始化时就会节省一个概念,而且可以达到只需要一个 ID 就能初始化一个对话页面的目的。在 ChatKit-OC 中我们通过对话中的人数来群分单聊群聊,当然你也可以为对话添加额外的字段,来准确地标记。每个 Conversation 对象都有一个 Attribute 属性,可以自定义字段。
115 |
116 | ### 可维护性
117 |
118 | 传统方式设置 View 坐标,或 颜色的问题:
119 |
120 | 1. 直接写坐标可维护性太差
121 | 2. 定义成宏,无法满足组件化后的自定义需求
122 | 3. 失去动态更新能力
123 |
124 | 为了解决以上问题,ChatKit 使用了 UI 配置文件的方式来提高可维护性:
125 |
126 | 将配置文件以 JSON 文件,或 plist 文件的方式,我采用的是 plist 文件形式,放置在 bundle 中,让 bundle 文件可以自定义,图片等多媒体资源也就同样可以自定义了。如果在App运行中覆盖对应路径,也就能达到动态更新应用主题的目的。
127 |
128 | - 易于维护
129 | - 利于动态更新
130 | - 安卓和iOS公用一套 UI 配置
131 |
132 | 下图是 ChatKit 中部分可自定义项:
133 |
134 | 
135 | 
136 |
137 | 做法类似:
138 |
139 | - 安卓开发中的 style xml
140 | - 网页开发中的 css
141 | - 微信团队做法类似。淘宝的一些app也采用了。
142 | 微信团队就是如此,软件开发团队从来不负责改坐标、颜色等 UI 参数,这些都是 UI 设计 团队去做。也是采用配置文件。淘宝的聚划算团队也使用了类似的策略,只不过是使用了 JSON 文件的形式。
143 |
144 | ## 可拓展性
145 |
146 | 使用 CocoaPods 集成,在 Demo 层面能实现红包这样大粒度的自定义业务模块,
147 |
148 | 效果图如下所示:
149 |
150 | 
151 |
152 | 需要几个 API:
153 |
154 | 12个接口:
155 |
156 | 自定义项 | 公开API | 备注
157 | -------------|-------------|-------------
158 | 自定义消息 | 2 | `-registerSubclass`、 `+classMediaType`
159 | 自定义Cell | 4 | `-registerCustomMessageCell`、`+classMediaType`、`-setup`、 `-configureCellWithData:`
160 | 自定义插件 | 6 | `-registerCustomInputViewPlugin`、`+classPluginType`、`-pluginIconImage`、`-pluginTitle`、`-pluginDidClicked`、`sendCustomMessageHandler`
161 |
162 | 效果图:
163 |
164 | - | - | -
165 | -------------|-------------|-------------
166 |  |  | 
167 |  |  | 
168 |
169 | 如何做到在Demo层面进行这样的拓展?
170 | 最关键的是:插件映射机制:
171 |
172 | 比如下图中输入框底部的这些可以点击的插件,ChatKit-OC 提供了一个可变字典,能让用户操作,添加元素。在需要展示 时会去该字典中拿View,因此只要提前进行了映射,就可以加载上。
173 |
174 | 
175 |
176 | 这种映射机制应用在了:
177 |
178 | - 自定义cell类型
179 | - 自定义Message类型
180 | - 自定义输入框底部插件
181 |
182 | ### 封装程度高
183 |
184 | 现状,现在社区中很多轮子,大多数自定义消息的做法,是在“自定义字符串”,对话双方传输的是字符串,接收后需要自己去做序列化、反序列化。
185 |
186 | ChatKit-OC 则封装程度更高,发送接收消息时,已经将所有的自定义消息,序列化和反序列化好。你只需要操作 Model 就可以。
187 |
188 |
189 | ## IM系列文章
190 |
191 | IM系列文章分为下面这几篇:
192 |
193 | - [《IM 即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角)》](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md)
194 | - [《技术实现细节》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/技术实现细节.md ) (本文)
195 | - [《有一种 Block 叫 Callback,有一种 Callback 做 CompletionHandler》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/有一种%20Block%20叫%20Callback,有一种%20Callback%20做%20CompletionHandler.md )
196 | - [《防 DNS 污染方案》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/防%20DNS%20污染方案.md )
197 |
198 |
199 | 本文是第二篇。
200 |
201 | ----------
202 |
203 | Posted by [微博@iOS程序犭袁](http://weibo.com/luohanchenyilong/)
204 | 原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | [Creative Commons BY-NC-ND 3.0](http://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
205 |
--------------------------------------------------------------------------------
/Tips/基于Websocket的IM即时通讯技术/有一种 Block 叫 Callback,有一种 Callback 做 CompletionHandler.md:
--------------------------------------------------------------------------------
1 | # 有一种 Block 叫 Callback,有一种 Callback 叫 CompletionHandler
2 |
3 | ## IM系列文章
4 |
5 | IM系列文章分为下面这几篇:
6 |
7 | - [《IM 即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角)》](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md)
8 | - [《技术实现细节》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/技术实现细节.md )
9 | - [《有一种 Block 叫 Callback,有一种 Callback 叫 CompletionHandler》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/有一种%20Block%20叫%20Callback,有一种%20Callback%20做%20CompletionHandler.md ) (本文)
10 | - [《防 DNS 污染方案》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/防%20DNS%20污染方案.md )
11 |
12 |
13 | 本文是第三篇。
14 |
15 | ## 正文
16 |
17 | 本文会以下面这种情况为切入点来探讨 Block 的种类和用法:
18 |
19 | > 集成 ChatKit 时,如何导入 APP 已有的用户系统,
20 |
21 | 我们作为开发者去集成一个 Lib (也可以叫轮子、SDK、下文统一叫 Lib)时,我们会发现我们遇到的 `Block`, 按照功能的角度划分,其实可以分为这几种:
22 |
23 | - Lib 通知开发者,**Lib**操作已经完成。一般命名为 Callback
24 | - 开发者通知 Lib,**开发者**的操作已经完成。一般可以命名为 CompletionHandler。
25 |
26 | 这两处的区别: 前者是 “Block 的执行”,后者是 “Block 的填充”。
27 |
28 | `Callback vs CompletionHandler` 命名与功能的差别,Apple 也没有明确的编码规范指出过,只不过如果按照“执行与填充”的功能划分的话,`callback` 与 `completionHandler` 的命名可以区分开来对待。同时也方便调用者理解 block 的功能。但总体来说,Apple 官方的命名中,“Block 填充“这个功能一般都会命名为 “completionHandler”,“Block 执行”这个功能大多命名为了“callback” ,也有少部分命名为了 “completionHandler”。
29 |
30 | 比如:
31 |
32 | NSURLSession 中,下面的函数将 “callback” 命名为了 “completionHandler”:
33 |
34 |
35 | ```Objective-C
36 | - (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
37 |
38 | ```
39 |
40 |
41 | 我们常常见到 `CompletionHandler` 被用到了第一种场景,而第一种场景“Block 执行”命名为 Callback 则更合适。
42 |
43 | > 不是所有 Block 都适合叫做 CompletionHandler
44 |
45 | 一般情况下,CompletionHandler 的设计往往考虑到多线程操作,于是,你就完全可以异步操作,然后在线程结束时执行该 CompletionHandler,下文的例子中会讲述下 `CompletionHandler` 方式在多线程场景下的一些优势。
46 |
47 | ## CompletionHandler + Delegate 组合
48 |
49 | 在 iOS10 中新增加的 `UserNotificaitons` 中大量使用了这种 Block,比如:
50 |
51 | ```Objective-C
52 | - (void)userNotificationCenter:(UNUserNotificationCenter *)center
53 | didReceiveNotificationResponse:(UNNotificationResponse *)response
54 | withCompletionHandler:(void (^)(void))completionHandler;
55 | ```
56 |
57 | [文档](https://developer.apple.com/reference/usernotifications/unusernotificationcenterdelegate/1649501-usernotificationcenter?language=objc) 对 completionHandler 的注释是这样的:
58 |
59 | ```Objective-C
60 | The block to execute when you have finished processing the user’s response. You must execute this block from your method and should call it as quickly as possible. The block has no return value or parameters.
61 | ```
62 |
63 | 同样在这里也有应用:
64 |
65 | ```Objective-C
66 | - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
67 | didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
68 | completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler;
69 | ```
70 |
71 |
72 |
73 | 还有另外一个也非常普遍的例子(Delegate 方式使用URLSession 时候必不可少的 4个代理函数之一 )
74 |
75 |
76 | ```Objective-C
77 | - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
78 | didReceiveResponse:(NSURLResponse *)response
79 | completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;
80 | ```
81 |
82 |
83 | 在代理方法实现代码里面,若是不执行 completionHandler(NSURLSessionResponseAllow) 话,http请求就终止了。
84 |
85 | ## CompletionHandler + Block 组合
86 |
87 | 函数中将函数作为参数或者返回值,就叫做高阶函数。
88 |
89 | 按照这种定义,Block 中将 Block 作为参数,这也就是高阶函数。
90 |
91 | 结合实际的应用场景来看一个例子:
92 |
93 | 如果有这样一个需求:
94 |
95 | 以我之前的一个 IM 项目 ChatKit-OC (开源的,下面简称 ChatKit)为例,当你的应用想要集成一个 IM 服务时,可能这时候,你的 APP 已经上架了,已经有自己的注册、登录等流程了。用 ChatKit 进行聊天很简单,只需要给 ChatKit 一个 id 就够了。聊天是正常了,但是双方只能看到一个id,这样体验很不好。但是如何展示头像、昵称呢?于是就设计了这样一个接口,`-setFetchProfilesBlock:` 。
96 |
97 | 这是上层(APP)提供用户信息的 Block,由于 ChatKit 并不关心业务逻辑信息,比如用户昵称,用户头像等。用户可以通过 ChatKit 单例向 ChatKit 注入一个用户信息内容提供 Block,通过这个用户信息提供 Block,ChatKit 才能够正确的进行业务逻辑数据的绘制。
98 |
99 | 示意图如下:
100 |
101 | 
102 |
103 | 具体实现如下:
104 |
105 | 方法定义如下:
106 |
107 | ```Objective-C
108 | /*!
109 | * @brief The block to execute with the users' information for the userIds. Always execute this block at some point when fetching profiles completes on main thread. Specify users' information how you want ChatKit to show.
110 | * @attention If you fetch users fails, you should reture nil, meanwhile, give the error reason.
111 | */
112 | typedef void(^LCCKFetchProfilesCompletionHandler)(NSArray> *users, NSError *error);
113 |
114 | /*!
115 | * @brief When LeanCloudChatKit wants to fetch profiles, this block will be invoked.
116 | * @param userIds User ids
117 | * @param completionHandler The block to execute with the users' information for the userIds. Always execute this block at some point during your implementation of this method on main thread. Specify users' information how you want ChatKit to show.
118 | */
119 | typedef void(^LCCKFetchProfilesBlock)(NSArray *userIds, LCCKFetchProfilesCompletionHandler completionHandler);
120 |
121 | @property (nonatomic, copy) LCCKFetchProfilesBlock fetchProfilesBlock;
122 |
123 | /*!
124 | * @brief Add the ablitity to fetch profiles.
125 | * @attention You must get peer information by peer id with a synchronous implementation.
126 | * If implemeted, this block will be invoked automatically by LeanCloudChatKit for fetching peer profile.
127 | */
128 | - (void)setFetchProfilesBlock:(LCCKFetchProfilesBlock)fetchProfilesBlock;
129 | ```
130 |
131 |
132 | 用法如下所示:
133 |
134 |
135 | ```Objective-C
136 | #warning 注意:setFetchProfilesBlock 方法必须实现,如果不实现,ChatKit将无法显示用户头像、用户昵称。以下方法循环模拟了通过 userIds 同步查询 users 信息的过程,这里需要替换为 App 的 API 同步查询
137 | [[LCChatKit sharedInstance] setFetchProfilesBlock:^(NSArray *userIds,
138 | LCCKFetchProfilesCompletionHandler completionHandler) {
139 | if (userIds.count == 0) {
140 | NSInteger code = 0;
141 | NSString *errorReasonText = @"User ids is nil";
142 | NSDictionary *errorInfo = @{
143 | @"code":@(code),
144 | NSLocalizedDescriptionKey : errorReasonText,
145 | };
146 | NSError *error = [NSError errorWithDomain:NSStringFromClass([self class])
147 | code:code
148 | userInfo:errorInfo];
149 |
150 | !completionHandler ?: completionHandler(nil, error);
151 | return;
152 | }
153 |
154 | NSMutableArray *users = [NSMutableArray arrayWithCapacity:userIds.count];
155 | #warning 注意:以下方法循环模拟了通过 userIds 同步查询 users 信息的过程,这里需要替换为 App 的 API 同步查询
156 |
157 | [userIds enumerateObjectsUsingBlock:^(NSString *_Nonnull clientId, NSUInteger idx,
158 | BOOL *_Nonnull stop) {
159 | NSPredicate *predicate = [NSPredicate predicateWithFormat:@"peerId like %@", clientId];
160 | //这里的LCCKContactProfiles,LCCKProfileKeyPeerId都为事先的宏定义,
161 | NSArray *searchedUsers = [LCCKContactProfiles filteredArrayUsingPredicate:predicate];
162 | if (searchedUsers.count > 0) {
163 | NSDictionary *user = searchedUsers[0];
164 | NSURL *avatarURL = [NSURL URLWithString:user[LCCKProfileKeyAvatarURL]];
165 | LCCKUser *user_ = [LCCKUser userWithUserId:user[LCCKProfileKeyPeerId]
166 | name:user[LCCKProfileKeyName]
167 | avatarURL:avatarURL
168 | clientId:clientId];
169 | [users addObject:user_];
170 | } else {
171 | //注意:如果网络请求失败,请至少提供 ClientId!
172 | LCCKUser *user_ = [LCCKUser userWithClientId:clientId];
173 | [users addObject:user_];
174 | }
175 | }];
176 | // 模拟网络延时,3秒
177 | // sleep(3);
178 |
179 | #warning 重要:completionHandler 这个 Bock 必须执行,需要在你**获取到用户信息结束**后,将信息传给该Block!
180 | !completionHandler ?: completionHandler([users copy], nil);
181 | }];
182 | ```
183 |
184 |
185 | 对于以上 Fetch 方法的这种应用场景,其实用方法的返回值也可以实现,但是与 CompletionHandler 相比,无法自由切换线程是个弊端。
186 |
187 |
188 | ## IM系列文章
189 |
190 | IM系列文章分为下面这几篇:
191 |
192 | - [《IM 即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角)》](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md)
193 | - [《技术实现细节》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/技术实现细节.md )
194 | - [《有一种 Block 叫 Callback,有一种 Callback 叫 CompletionHandler》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/有一种%20Block%20叫%20Callback,有一种%20Callback%20做%20CompletionHandler.md ) (本文)
195 | - [《防 DNS 污染方案》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/防%20DNS%20污染方案.md )
196 |
197 |
198 | 本文是第三篇。
199 |
200 | ----------
201 |
202 | Posted by [微博@iOS程序犭袁](http://weibo.com/luohanchenyilong/)
203 | 原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | [Creative Commons BY-NC-ND 3.0](http://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
204 |
205 |
206 |
--------------------------------------------------------------------------------
/Tips/基于Websocket的IM即时通讯技术/防 DNS 污染方案.md:
--------------------------------------------------------------------------------
1 | ### 防 DNS 污染方案
2 |
3 | ## IM系列文章
4 |
5 | IM系列文章分为下面这几篇:
6 |
7 | - [《IM 即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角)》](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md)
8 | - [《技术实现细节》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/技术实现细节.md )
9 | - [《有一种 Block 叫 Callback,有一种 Callback 做 CompletionHandler》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/有一种%20Block%20叫%20Callback,有一种%20Callback%20做%20CompletionHandler.md )
10 | - [《防 DNS 污染方案》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/防%20DNS%20污染方案.md ) (本文)
11 |
12 | 本文是第四篇。
13 |
14 | ## 正文
15 |
16 | DNS出问题的概率其实比大家感觉的要大,首先是DNS被劫持或者失效,2015年初业内比较知名的就有 Apple 内部 DNS 问题导致 App Store、iTunes Connect 账户无法登录;京东因为 CDN 域名付费问题导致服务停摆。
17 |
18 | 另一个常见问题就是 DNS 解析慢或者失败,例如国内中国运营商网络的 DNS 就很慢,一次 DNS 查询的耗时甚至都能赶上一次连接的耗时,尤其 2G 网络情况下,DNS 解析失败是很常见的。因此如果直接使用 DNS,对于首次网络服务请求耗时和整体服务成功率都有非常大的影响。
19 |
20 | DNS 劫持、污染一般是针对递归 DNS 服务器的 DNS 劫持攻击,
21 |
22 | > DNS 系统中有两种服务角色:递归 DNS 和授权 DNS。本质上来说,授权 DNS 控制网站的解析;递归 DNS 只起缓存的作用。所以跟广大站长关系比较大的是授权 DNS,也就是在域名注册商处填写的 DNS 地址。而网民使用的则是递归 DNS。
23 | 见:https://support.dnspod.cn/Kb/showarticle/tsid/186/
24 |
25 | 现实中的问题:
26 |
27 | - DNS解析时间过长
28 | 像 iOS 系统一般是24小时之后会过期,还有进入飞行模式再切回来,开关机,重置网络设置等也会导致DNS cache的清除。所以一般情况下用户在第二天打开你的app都会经历一次完整的DNS解析请求,网络情况差的时候会明显增加应用请求的总耗时。
29 |
30 | - DNS劫持,不可以被信任的运营商,不可以被信任的 DNS 解析服务。
31 |
32 | DNS 在设计之初是基于 UDP 的,显然这样的设计不能满足当今社会的准确性的需求,于是涌现了如 DNSPod 这样的基于 HTTP 的 DNS 解析服务。但是当时为什么这样设计,实际也很好理解,UDP 效率高,一来一回网络上传输的只有两个包,而 HTTP则需要三次握手三个包,再一拆包,就需要四个包。这是受限于当时整个社会的带宽水平较低,而现在没人会感激 UDP 所节省的流量,所有人都在诟病DNS污染问题。
33 |
34 | 图为360向大家们示范什么是 DNS 劫持:
35 |
36 | 
37 |
38 | 运营商 DNS 劫持问题中,中国移动最为严重,
39 |
40 | 某些地区的中国移动还有个简单粗爆的域名检查系统,包含 av 字样的域名一率返回错误的 IP,
41 |
42 | LeanCloud 之前叫做 AVOSCLoud,域名是:https://cn.avoscloud.com,嗯,我们很受伤。
43 | 后来我们改名了,域名也切换到了 api.leancloud.cn ,我们用户的 DNS 问题已经大大的减少了。
44 |
45 | 鬼知道我们经历了什么。
46 |
47 | 虽然这个事件并不典型,但也足以说明,这个一个不可靠的服务,你无法掌控它的拦截规则。
48 |
49 | 而且黑产与各地运营商的一些“合作”也会导致 DNS 劫持。
50 |
51 | 原有的解决方法:简单粗暴投诉运营商。
52 |
53 | #### 传统的解决方法:投诉
54 |
55 | 诊断方法步骤:
56 |
57 | * iOS 用户推荐 iNetTools
58 | * Android 用户推荐 LanDroid
59 |
60 | ping 响应时间,100(单位默认为 ms)以下都是可以接受的,高于 100 ms 会感到缓慢
61 |
62 | 移动环境下,向中国移动打 10086 电话投诉,告之受影响的域名及 DNS 服务器的 IP,才能解决问题。
63 | 如果是在无线网络情况下, DNS 异常,则请通过路由器的 DHCP 设置,将默认的 DNS 修改为正常的 DNS(推荐 114.114.114.114),并重启路由器即可。
64 |
65 | 投诉到中国移动后 48 小时问题仍未解决的话,依据中国相关法律法规规定,可以向工信部申诉,网址是 http://www.chinatcc.gov.cn:8080/cms/shensus/ ,这里最好是以邮件的方式申诉,将具体细节和截图写在邮件里发送给 accept@chinatcc.gov.cn,工信部的相关同学最早会在第 2 天回电话并催促中国移动。
66 |
67 | 申诉邮件的内容需要包括两个部分:
68 | 一是申诉者的姓名、身份证号码、通信地址、邮编、联系电话、申诉涉及到的电话号码、电子邮件、申诉日期
69 | 二是被申诉企业名称、申诉内容(详情)、是否向企业申诉过(一定要先向企业投诉,无效后工信部才能受理,直接找工信部的不受理),最后要承诺「我承诺申诉信息真实有的」
70 |
71 | 这样显然不是长久之计,下面就介绍下如何用技术手段去解决:
72 |
73 | #### IP 直连在IPv6 环境下的可行性
74 |
75 | 首先:**所有防 DNS 方案都是基于IP直连的方案**,那么就要首先介绍 IP 直连这个方案的可行性。
76 |
77 | 从2016年6月1日起,iOS 应用必须支持 IPv6,否则审核将被拒。IPv6 规则出来后,网上有一种言论称:IP 直连不可行。
78 |
79 | 其实是 IP 直连,在 IPv6 环境下也是可行的,下面做下说明:
80 |
81 | IP或域名在到达服务器前,经历了两个步骤往往会被我们所忽略:
82 |
83 | 
84 |
85 | 如果你拿一个 IPv4 的 IP 或域名进行请求,在 IPv6-Only 环境下,有两个机制可以保证最终能够到达 Server 地址。
86 |
87 | 第一个机制是绿色部分,指的是 iOS系统级别的 IPv4 兼容方案,只要你使用了 `NSURLSession` 或 `CFNetwork`, 那么 iOS 系统会将帮你把它转为 IPv6 地址。
88 |
89 | > NSURLSession and CFNetwork automatically synthesize IPv6 addresses from IPv4 literals locally on devices operating on DNS64/NAT64 networks.(如果当前网络是 IPv6 网络,那么会在iOS系统层面转换成 IPv6.)
90 |
91 | 第二个机制是 DNS 服务的兼容方案,可以是运营商提供的服务,也可以是第三方 DNS 解析机构比如 DNSPod。如果 DNS 解析出来的域名是 IPv4 地址,也会转为 IPv6 兼容的地址。DNS64/NAT64 起到的作用就是将网关的出口地址进行转换,映射到 IPv4 地址上,保证路由能寻址到 IPv4 的地址。
92 |
93 | 综上所述,IPv6 政策的应对方案可以有下面几种:
94 |
95 | 1. 使用高层API,比如 `NSURLSession` and `CFNetwork`。
96 | 2. 升级服务器,让服务端支持 IPv6。在 APP 中替换 IPv4 的地址。
97 | 3. 如果你的 APP 需要使用了更底层的 API 连接到仅支持 IPv4 的服务器,且不使用 DNS 域名解析,请在APP端使用 `getaddrinfo` 处理 IPv4 地址串( `getaddrinfo` 可通过传入一个IPv4或IPv6地址,得到一个 sockaddr 结构链表)。如果当前的网络接口不支持 IPv4,仅支持 IPv6,NAT64和DNS64,这样做可以得到一个合成的IPv6地址。
98 |
99 | 就目前国内的情况来看,据大部分的服务端器是不支持IPv6的,最后一种方法更加适用。这样一来,服务端完全不用做更改,在服务端看来,客户端是能够正常连接到 IPv4 的地址的。
100 |
101 | 参考:[《iOS支持IPv6 DNS64/NAT64网络》]( http://www.pchou.info/ios/2016/06/05/ios-supporting-ipv6.html )
102 |
103 | #### 在 HTTPS 业务场景下的防 DNS 污染方案
104 |
105 | 防止 DNS 污染的方式有多种:
106 |
107 | 实现方式大致有两种:
108 |
109 | 方案一:HTTP 场景 IP 直连
110 |
111 | 通过IP直接访问网站,可以解决 DNS 劫持问题。如果是 HTTP 请求,使用 ip 地址直接访问接口,配合 header 中 Host 字段带上原来的域名信息即可;
112 |
113 | 方案二:客户端维护一个 IP 列表
114 |
115 | - 无效映射淘汰机制
116 | - 使用IP列表避免DNS解析失败或者劫持 (电信、移动、联通,域名异步地去获取)IP地址,请求成功就+1、失败就-1,然后得到优先级列表
117 | - 根据网络延迟选择服务端IP
118 |
119 | 参考: [《iOS网络请求优化之DNS映射》]( http://www.jianshu.com/p/ad038ea54310 )。
120 |
121 | 方案三:使用基于 HTTP 的 DNS 解析方案
122 |
123 | 对于服务器IP经常变的情况,可能需要使用第三方服务,比如DNSPod、httpDNS。
124 |
125 | 默认的 DNS 是基于 UDP,改用 HTTP 协议进行域名解析,代替现有基于 UDP 的 DNS 协议,域名解析请求直接发送到指定的第三方 DNS 解析服务器,从而绕过运营商的 Local DNS,能够避免 Local DNS 造成的域名劫持问题和调度不精准问题。
126 |
127 | 绕过运营商直接连可以信任的第三方服务。
128 |
129 | 
130 |
131 | 那如果这些第三方解析商服务也挂掉了呢?这里有一个折中的方案,你可以两个服务都使用,其中一个作为失败重试的备选项,首选和备选的优先级可以调整。
132 |
133 | 参考:
134 |
135 | 1. [《DNSPod接入指南》]( https://www.dnspod.cn/httpdns/guide )
136 | 2. [《腾讯云DNSPod域名解析全面支持IPv6-only》]( http://www.qcloud.com/blog/?p=1234 )
137 |
138 | #### 实现时的问题
139 |
140 | 发送 HTTPS 请求首先要进行 SSL/TLS 握手,握手过程大致如下:
141 |
142 | 1. 客户端发起握手请求,携带随机数、支持算法列表等参数。
143 | 2. 服务端收到请求,选择合适的算法,下发公钥证书和随机数。
144 | 3. 客户端对服务端证书进行校验,并发送随机数信息,该信息使用公钥加密。
145 | 4. 服务端通过私钥获取随机数信息。
146 |
147 | 
148 |
149 | 最后,双方根据以上交互的信息生成session ticket,用作该连接后续数据传输的加密密钥。
150 | 上述过程中,和我们的方案有关的是第3步,客户端需要验证服务端下发的证书,验证过程有以下两个要点:
151 |
152 | 客户端用本地保存的根证书解开证书链,确认服务端下发的证书是由可信任的机构颁发的。
153 | 客户端需要检查证书的domain域和扩展域,看是否包含本次请求的host。
154 | 如果上述两点都校验通过,就证明当前的服务端是可信任的,否则就是不可信任,应当中断当前连接。
155 |
156 | 当客户端使用基于HTTP的第三方解析服务解析域名时,请求URL中的host会被替换成解析出来的IP,所以在证书验证的第2步,会出现domain不匹配的情况,导致SSL/TLS握手不成功。
157 |
158 | 解决方案:
159 |
160 | https 请求,需要 `Overriding TLS Chain Validation Correctly`;
161 |
162 | 如果使用第三方网络库:curl, 中有一个 `-resolve` 方法可以实现使用指定 ip 访问 https 网站,iOS 中集成 curl 库,参考 [curl文档](https://curl.haxx.se/libcurl/c/CURLOPT_RESOLVE.html) ;它也是支持 IPv6 环境的,只需要你在 build 时添加上 `--enable-ipv6` 即可。
163 |
164 | 如果使用AFN,则需要重写AFN里的一些方法,
165 |
166 | 具体步骤是:hook 住 SSL 握手方法,也就是上图中的第2步,对应于下面的方法:
167 |
168 | ```Objective-C
169 | /*
170 | * NSURLSession
171 | */
172 | - (void)connection:(NSURLConnection *)connectionwillSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
173 | ```
174 |
175 |
176 | ```Objective-C
177 | /*
178 | * NSURLSession
179 | */
180 | - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
181 | didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
182 | completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler
183 |
184 | ```
185 |
186 | 然后将IP直接替换成原来的域名,再执行证书验证。
187 |
188 | 具体参考:
189 |
190 | 1. [《如何使用ip直接访问https网站?》]( https://segmentfault.com/a/1190000004359232?utm_source=Weibo )
191 | 2. [《HTTPS业务场景解决方案》]( https://help.aliyun.com/document_detail/30143.html )
192 | 3. [Supporting IPv6 DNS64/NAT64 Networks](https://developer.apple.com/library/content/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/UnderstandingandPreparingfortheIPv6Transition/UnderstandingandPreparingfortheIPv6Transition.html#//apple_ref/doc/uid/TP40010220-CH213-SW1)
193 |
194 | ## IM系列文章
195 |
196 | IM系列文章分为下面这几篇:
197 |
198 | - [《IM 即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角)》](https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/IM%20即时通讯技术在多应用场景下的技术实现,以及性能调优(iOS视角).md)
199 | - [《技术实现细节》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/技术实现细节.md )
200 | - [《有一种 Block 叫 Callback,有一种 Callback 做 CompletionHandler》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/有一种%20Block%20叫%20Callback,有一种%20Callback%20做%20CompletionHandler.md )
201 | - [《防 DNS 污染方案》]( https://github.com/ChenYilong/iOSBlog/blob/master/Tips/基于Websocket的IM即时通讯技术/防%20DNS%20污染方案.md ) (本文)
202 |
203 | 本文是第四篇。
204 |
205 |
206 |
207 | ----------
208 |
209 | Posted by [微博@iOS程序犭袁](http://weibo.com/luohanchenyilong/)
210 | 原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | [Creative Commons BY-NC-ND 3.0](http://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
211 |
212 |
213 |
--------------------------------------------------------------------------------
/Tips/大话Socket.md:
--------------------------------------------------------------------------------
1 | # 大话Socket
2 |
3 |
4 | 要了解Socket首先要了解 TCP,他们两个的关系可以说是:
5 |
6 | Socket 是抽象出来的使用 TCP/UDP 的概念模型,屏蔽掉了晦涩的底层协议的实现,是一个接口。
7 |
8 |
9 | 最近看到了一张如此详细的 TCP 三次握手和四次挥手,打印一张放工位!摘自《图解网络硬件》249页 图5-11 《TCP的三次握手》
10 |
11 | 
12 |
13 | 
15 |
16 |
17 | 所谓的`X、X+1`、`Y、Y+1`
18 | 对应于`你收到了没、我收到了`、`你收到'我收到'没、我收到了不用回了`,为什么用`+1`表示呢?那是因为前两个指的是一个人,后两个指的是一个人。
19 | 四组是三个连接,每个连接的序号依次是X、Y、Z。
20 |
21 |
22 | TCP的连接过程就像两个人的对话:
23 |
24 | 想象一下,每次这俩儿人聊天,都要像下面这样一来一回三次,接下来他们才能【好好聊天了。。。】真是有点“作”。。。
25 |
26 | 我是客户端,树懒是服务端,演示三次握手、数据传输步骤
27 |
28 |
29 | 
30 |
31 |
32 | 其实有个问题,为什么连接的时候是三次握手,关闭的时候却是四次挥手?
33 |
34 | 因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
35 |
36 |
37 |
38 | 而这一设计,主要是因为“服务器不是你想关就能关”。。。
39 |
40 |
41 |
42 | 比如说两个热恋中的人正在QQ上发送一个传mp4格式的文件,
43 |
44 | A说,我要下QQ了,
45 |
46 | B说:我知道了,你下吧。
47 |
48 | A说,那我关了,(想关)
49 |
50 | 但是当A尝试关闭QQ的时候,QQ弹窗说“正在传输文件,传输完成后自动关闭QQ?”
51 |
52 | 这时候A对B说,呀,正在传东西,等传完了,我就关吧。(不能关)
53 |
54 | B说:行。既然关不掉,不行再聊会儿呗?
55 |
56 | A:聊吧。。。传完了啊,下了啊(传输结束了--能关)
57 |
58 | B:下吧。我也下了。。。
59 |
60 | 就是多了一个Finish报文。
61 |
62 |
63 | 或者简单点表示是这样的:
64 |
65 | 
66 |
67 | 图片演示了四次挥手,与三次握手相比,只多了一个被动方确认自身任务Finish的动作。
68 |
69 |
70 | 
71 |
72 |
73 | 总结下相关的函数:
74 |
75 | 创建套接字
76 |
77 | Socket(af,type,protocol)
78 |
79 | 建立地址和套接字的联系
80 |
81 | bind(sockid, local addr, addrlen)
82 | 服务器端侦听客户端的请求
83 |
84 | listen( Sockid ,quenlen)
85 | 建立服务器/客户端的连接 (面向连接TCP)
86 |
87 | 客户端请求连接
88 |
89 | Connect(sockid, destaddr, addrlen)
90 | 服务器端等待从编号为Sockid的Socket上接收客户连接请求
91 |
92 | newsockid=accept(Sockid,Clientaddr, paddrlen)
93 | 发送/接收数据
94 |
95 | 面向连接:
96 |
97 | send(sockid, buff, bufflen)
98 | recv( )
99 | 面向无连接:
100 |
101 | sendto(sockid,buff,…,addrlen)
102 | recvfrom( )
103 | 释放套接字
104 |
105 | close(sockid)
106 |
107 |
108 | 至于为什么是三次握手,而不是二次握手,还有四次握手,可以看看下面的类比:
109 |
110 | 三次握手 | 二次握手 | 四次握手
111 | -------------|-------------|-------------
112 | |  |
113 |
114 | 参考:http://zhihu.com/question/24853633/answer/114872771
115 |
116 | 三次握手的对话,也挺像这个的,哈哈:
117 |
118 | A: How are you?
119 | B: I'm fine, thanks,and you?
120 | A: I'm fine too.
121 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/AppDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.h
3 | // CYLGCDRunloopDemo
4 | //
5 | // Created by chenyilong on 2017/6/7.
6 | // Copyright © 2017年 Elon Chan. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface AppDelegate : UIResponder
12 |
13 | @property (strong, nonatomic) UIWindow *window;
14 |
15 |
16 | @end
17 |
18 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/AppDelegate.m:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.m
3 | // CYLGCDRunloopDemo
4 | //
5 | // Created by chenyilong on 2017/6/7.
6 | // Copyright © 2017年 Elon Chan. All rights reserved.
7 | //
8 |
9 | #import "AppDelegate.h"
10 | #import "Foo.h"
11 |
12 | @interface AppDelegate ()
13 |
14 | @end
15 |
16 | @implementation AppDelegate
17 |
18 |
19 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
20 | // Override point for customization after application launch.
21 |
22 | // NSOperationQueue *asyncOperationQueue = [[NSOperationQueue alloc] init];
23 | // [asyncOperationQueue setMaxConcurrentOperationCount:300];
24 | // for (int i = 0; i < 300 ; i++) {
25 | // NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
26 | // NSString *currentThreadName = [NSString stringWithFormat:@"🔴类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), @""];
27 | // [[NSThread currentThread] setName:@"didFinishLaunchingWithOptions"];
28 | //// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
29 | //// NSLog(@"🔴类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), @"");
30 | //// });
31 | ////NSLog(@"🔴类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), @"");
32 | //
33 | //
34 | //
35 | //
36 | // NSThread *networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(ayncThread:) object:@(i)];
37 | // [networkRequestThread start];
38 | // NSLog(@"🔴类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), @(i));
39 | //
40 | //
41 | //// [[Foo new] test];
42 | //// [[Foo new] begin];
43 | // }];
44 | //
45 | //
46 | //
47 | // [asyncOperationQueue addOperation:operation];
48 | // }
49 |
50 | return YES;
51 | }
52 |
53 | //- (void)ayncThread:(id)i {
54 | // NSLog(@"💚类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), i);
55 | //
56 | //}
57 | - (void)applicationWillResignActive:(UIApplication *)application {
58 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
59 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
60 | }
61 |
62 |
63 | - (void)applicationDidEnterBackground:(UIApplication *)application {
64 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
65 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
66 | }
67 |
68 |
69 | - (void)applicationWillEnterForeground:(UIApplication *)application {
70 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
71 | }
72 |
73 |
74 | - (void)applicationDidBecomeActive:(UIApplication *)application {
75 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
76 | }
77 |
78 |
79 | - (void)applicationWillTerminate:(UIApplication *)application {
80 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
81 | }
82 |
83 |
84 | @end
85 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "size" : "29x29",
15 | "idiom" : "iphone",
16 | "filename" : "Icon-Small.png",
17 | "scale" : "1x"
18 | },
19 | {
20 | "size" : "29x29",
21 | "idiom" : "iphone",
22 | "filename" : "Icon-Small@2x.png",
23 | "scale" : "2x"
24 | },
25 | {
26 | "size" : "29x29",
27 | "idiom" : "iphone",
28 | "filename" : "Icon-Small@3x.png",
29 | "scale" : "3x"
30 | },
31 | {
32 | "size" : "40x40",
33 | "idiom" : "iphone",
34 | "filename" : "Icon-40@2x.png",
35 | "scale" : "2x"
36 | },
37 | {
38 | "size" : "40x40",
39 | "idiom" : "iphone",
40 | "filename" : "Icon-40@3x.png",
41 | "scale" : "3x"
42 | },
43 | {
44 | "size" : "57x57",
45 | "idiom" : "iphone",
46 | "filename" : "Icon.png",
47 | "scale" : "1x"
48 | },
49 | {
50 | "size" : "57x57",
51 | "idiom" : "iphone",
52 | "filename" : "Icon@2x.png",
53 | "scale" : "2x"
54 | },
55 | {
56 | "size" : "60x60",
57 | "idiom" : "iphone",
58 | "filename" : "Icon-60@2x.png",
59 | "scale" : "2x"
60 | },
61 | {
62 | "size" : "60x60",
63 | "idiom" : "iphone",
64 | "filename" : "Icon-60@3x.png",
65 | "scale" : "3x"
66 | },
67 | {
68 | "idiom" : "ipad",
69 | "size" : "20x20",
70 | "scale" : "1x"
71 | },
72 | {
73 | "idiom" : "ipad",
74 | "size" : "20x20",
75 | "scale" : "2x"
76 | },
77 | {
78 | "size" : "29x29",
79 | "idiom" : "ipad",
80 | "filename" : "Icon-Small.png",
81 | "scale" : "1x"
82 | },
83 | {
84 | "size" : "29x29",
85 | "idiom" : "ipad",
86 | "filename" : "Icon-Small@2x.png",
87 | "scale" : "2x"
88 | },
89 | {
90 | "size" : "40x40",
91 | "idiom" : "ipad",
92 | "filename" : "Icon-40.png",
93 | "scale" : "1x"
94 | },
95 | {
96 | "size" : "40x40",
97 | "idiom" : "ipad",
98 | "filename" : "Icon-40@2x.png",
99 | "scale" : "2x"
100 | },
101 | {
102 | "size" : "50x50",
103 | "idiom" : "ipad",
104 | "filename" : "Icon-Small-50.png",
105 | "scale" : "1x"
106 | },
107 | {
108 | "size" : "50x50",
109 | "idiom" : "ipad",
110 | "filename" : "Icon-Small-50@2x.png",
111 | "scale" : "2x"
112 | },
113 | {
114 | "size" : "72x72",
115 | "idiom" : "ipad",
116 | "filename" : "Icon-72.png",
117 | "scale" : "1x"
118 | },
119 | {
120 | "size" : "72x72",
121 | "idiom" : "ipad",
122 | "filename" : "Icon-72@2x.png",
123 | "scale" : "2x"
124 | },
125 | {
126 | "size" : "76x76",
127 | "idiom" : "ipad",
128 | "filename" : "Icon-76.png",
129 | "scale" : "1x"
130 | },
131 | {
132 | "size" : "76x76",
133 | "idiom" : "ipad",
134 | "filename" : "Icon-76@2x.png",
135 | "scale" : "2x"
136 | },
137 | {
138 | "size" : "83.5x83.5",
139 | "idiom" : "ipad",
140 | "filename" : "Icon-83.5@2x.png",
141 | "scale" : "2x"
142 | }
143 | ],
144 | "info" : {
145 | "version" : 1,
146 | "author" : "xcode"
147 | }
148 | }
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-40.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-72.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-76.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/AppIcon.appiconset/Icon@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "extent" : "full-screen",
5 | "idiom" : "iphone",
6 | "filename" : "Default.png",
7 | "orientation" : "portrait",
8 | "scale" : "1x"
9 | },
10 | {
11 | "extent" : "full-screen",
12 | "idiom" : "iphone",
13 | "filename" : "Default@2x.png",
14 | "minimum-system-version" : "7.0",
15 | "orientation" : "portrait",
16 | "scale" : "2x"
17 | },
18 | {
19 | "extent" : "full-screen",
20 | "idiom" : "iphone",
21 | "subtype" : "retina4",
22 | "filename" : "Default-568h@2x.png",
23 | "orientation" : "portrait",
24 | "scale" : "2x"
25 | },
26 | {
27 | "extent" : "full-screen",
28 | "idiom" : "iphone",
29 | "subtype" : "retina4",
30 | "filename" : "Default-568h@2x.png",
31 | "minimum-system-version" : "7.0",
32 | "orientation" : "portrait",
33 | "scale" : "2x"
34 | },
35 | {
36 | "extent" : "full-screen",
37 | "idiom" : "iphone",
38 | "filename" : "Default@2x.png",
39 | "orientation" : "portrait",
40 | "scale" : "2x"
41 | },
42 | {
43 | "extent" : "to-status-bar",
44 | "idiom" : "ipad",
45 | "filename" : "Default~ipad.png",
46 | "orientation" : "portrait",
47 | "scale" : "1x"
48 | },
49 | {
50 | "extent" : "to-status-bar",
51 | "idiom" : "ipad",
52 | "filename" : "Default~ipad@2x.png",
53 | "orientation" : "portrait",
54 | "scale" : "2x"
55 | },
56 | {
57 | "extent" : "to-status-bar",
58 | "idiom" : "ipad",
59 | "filename" : "Default~ipad~landscape.png",
60 | "orientation" : "landscape",
61 | "scale" : "1x"
62 | },
63 | {
64 | "extent" : "to-status-bar",
65 | "idiom" : "ipad",
66 | "filename" : "Default~ipad~landscape@2x.png",
67 | "orientation" : "landscape",
68 | "scale" : "2x"
69 | },
70 | {
71 | "extent" : "full-screen",
72 | "idiom" : "ipad",
73 | "filename" : "Default~ipad~nostatusbar.png",
74 | "minimum-system-version" : "7.0",
75 | "orientation" : "portrait",
76 | "scale" : "1x"
77 | },
78 | {
79 | "extent" : "full-screen",
80 | "idiom" : "ipad",
81 | "filename" : "Default~ipad~nostatusbar.png",
82 | "orientation" : "portrait",
83 | "scale" : "1x"
84 | },
85 | {
86 | "extent" : "full-screen",
87 | "idiom" : "ipad",
88 | "filename" : "Default~ipad~nostatusbar@2x.png",
89 | "minimum-system-version" : "7.0",
90 | "orientation" : "portrait",
91 | "scale" : "2x"
92 | },
93 | {
94 | "extent" : "full-screen",
95 | "idiom" : "ipad",
96 | "filename" : "Default~ipad~nostatusbar@2x.png",
97 | "orientation" : "portrait",
98 | "scale" : "2x"
99 | },
100 | {
101 | "extent" : "full-screen",
102 | "idiom" : "ipad",
103 | "filename" : "Default~ipad~landscape~nostatusbar.png",
104 | "minimum-system-version" : "7.0",
105 | "orientation" : "landscape",
106 | "scale" : "1x"
107 | },
108 | {
109 | "extent" : "full-screen",
110 | "idiom" : "ipad",
111 | "filename" : "Default~ipad~landscape~nostatusbar.png",
112 | "orientation" : "landscape",
113 | "scale" : "1x"
114 | },
115 | {
116 | "extent" : "full-screen",
117 | "idiom" : "ipad",
118 | "filename" : "Default~ipad~landscape~nostatusbar@2x.png",
119 | "minimum-system-version" : "7.0",
120 | "orientation" : "landscape",
121 | "scale" : "2x"
122 | },
123 | {
124 | "extent" : "full-screen",
125 | "idiom" : "ipad",
126 | "filename" : "Default~ipad~landscape~nostatusbar@2x.png",
127 | "orientation" : "landscape",
128 | "scale" : "2x"
129 | },
130 | {
131 | "extent" : "full-screen",
132 | "idiom" : "iphone",
133 | "subtype" : "736h",
134 | "filename" : "iPhone6-Plus-portrait@3x.png",
135 | "minimum-system-version" : "8.0",
136 | "orientation" : "portrait",
137 | "scale" : "3x"
138 | },
139 | {
140 | "extent" : "full-screen",
141 | "idiom" : "iphone",
142 | "subtype" : "736h",
143 | "filename" : "iPhone6-Plus-landscape@3x.png",
144 | "minimum-system-version" : "8.0",
145 | "orientation" : "landscape",
146 | "scale" : "3x"
147 | },
148 | {
149 | "extent" : "full-screen",
150 | "idiom" : "iphone",
151 | "subtype" : "667h",
152 | "filename" : "iPhone6-portrait@2x.png",
153 | "minimum-system-version" : "8.0",
154 | "orientation" : "portrait",
155 | "scale" : "2x"
156 | }
157 | ],
158 | "info" : {
159 | "version" : 1,
160 | "author" : "xcode"
161 | }
162 | }
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad~landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad~landscape.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad~landscape@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad~landscape@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad~landscape~nostatusbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad~landscape~nostatusbar.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad~landscape~nostatusbar@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad~landscape~nostatusbar@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad~nostatusbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad~nostatusbar.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad~nostatusbar@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/Default~ipad~nostatusbar@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/iPhone6-Plus-landscape@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/iPhone6-Plus-landscape@3x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/iPhone6-Plus-portrait@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/iPhone6-Plus-portrait@3x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/iPhone6-portrait@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChenYilong/iOSBlog/883eee2d93b61c5604fa7baf9b671c83b041da03/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Assets.xcassets/LaunchImage.launchimage/iPhone6-portrait@2x.png
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Foo.h:
--------------------------------------------------------------------------------
1 | //
2 | // Foo.h
3 | // CYLGCDRunloopDemo
4 | //
5 | // Created by chenyilong on 2017/6/7.
6 | // Copyright © 2017年 Elon Chan. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface Foo : NSObject
12 | - (id)test;
13 | @end
14 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Foo.m:
--------------------------------------------------------------------------------
1 |
2 | //
3 | // Foo.m
4 | // CYLGCDRunloopDemo
5 | //
6 | // Created by chenyilong on 2017/6/7.
7 | // Copyright © 2017年 Elon Chan. All rights reserved.
8 | //
9 |
10 | #import "Foo.h"
11 |
12 | @interface Foo() {
13 | NSRunLoop *_runloop;
14 | NSTimer *_timeoutTimer;
15 | NSTimeInterval _timeoutInterval;
16 | dispatch_semaphore_t _sem;
17 | }
18 | @end
19 |
20 | @implementation Foo
21 |
22 | - (instancetype)init {
23 | if (!(self = [super init])) {
24 | return nil;
25 | }
26 | _timeoutInterval = 1 ;
27 | _sem = dispatch_semaphore_create(0);
28 | // Do any additional setup after loading the view, typically from a nib.
29 | return self;
30 | }
31 |
32 | - (id)test {
33 | // 第一种方式:
34 | // NSThread *networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint0:) object:nil];
35 | // [networkRequestThread start];
36 | //第二种方式:
37 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
38 | [self networkRequestThreadEntryPoint0:nil];
39 | });
40 | dispatch_semaphore_wait(_sem, DISPATCH_TIME_FOREVER);
41 | NSLog(@"🔴类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), @"");
42 | return @(YES);
43 | }
44 |
45 | - (void)networkRequestThreadEntryPoint0:(id)__unused object {
46 | @autoreleasepool {
47 | NSLog(@"🔴类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), [NSThread currentThread]);
48 | [[NSThread currentThread] setName:@"CYLTest"];
49 | _runloop = [NSRunLoop currentRunLoop];
50 | [_runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
51 |
52 | NSLog(@"🔴类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), @"");
53 | _timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(stopLoop) userInfo:nil repeats:NO];
54 | [_runloop addTimer:_timeoutTimer forMode:NSRunLoopCommonModes];
55 | [_runloop run];//在实际开发中最好使用这种方式来确保能runloop退出,做双重的保障[runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:(timeoutInterval+5)]];
56 | NSLog(@"🔴类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), [NSThread currentThread]);
57 | }
58 | }
59 |
60 | - (void)stopLoop {
61 | NSLog(@"🔴类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), @"stop loop");
62 | CFRunLoopStop([_runloop getCFRunLoop]);
63 | dispatch_semaphore_signal(_sem);
64 | }
65 |
66 | - (void)dealloc {
67 | NSLog(@"🔴类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), @"");
68 | }
69 |
70 | @end
71 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIMainStoryboardFile
24 | Main
25 | UIRequiredDeviceCapabilities
26 |
27 | armv7
28 |
29 | UISupportedInterfaceOrientations
30 |
31 | UIInterfaceOrientationPortrait
32 | UIInterfaceOrientationLandscapeLeft
33 | UIInterfaceOrientationLandscapeRight
34 |
35 | UISupportedInterfaceOrientations~ipad
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationPortraitUpsideDown
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/ViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.h
3 | // CYLGCDRunloopDemo
4 | //
5 | // Created by chenyilong on 2017/6/7.
6 | // Copyright © 2017年 Elon Chan. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface ViewController : UIViewController
12 |
13 |
14 | @end
15 |
16 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/ViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.m
3 | // CYLGCDRunloopDemo
4 | //
5 | // Created by chenyilong on 2017/6/7.
6 | // Copyright © 2017年 Elon Chan. All rights reserved.
7 | //
8 |
9 | #import "ViewController.h"
10 | #import "Foo.h"
11 |
12 | @implementation ViewController
13 |
14 | - (void)viewDidLoad {
15 | [super viewDidLoad];
16 | // NSOperationQueue *asyncOperationQueue = [[NSOperationQueue alloc] init];
17 | // [asyncOperationQueue setMaxConcurrentOperationCount:300];
18 | // for (int i = 0; i < 300 ; i++) {
19 | // NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
20 | // NSString *currentThreadName = [NSString stringWithFormat:@"🔴类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), @""];
21 | // [[NSThread currentThread] setName:@"didFinishLaunchingWithOptions"];
22 | // [[Foo new] test];
23 | // }];
24 | // [asyncOperationQueue addOperation:operation];
25 | // }
26 | for (int i = 0; i < 300 ; i++) {
27 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
28 | [[Foo new] test];
29 | NSLog(@"🔴类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), @"");
30 | });
31 | }
32 | }
33 | @end
34 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemo/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // CYLGCDRunloopDemo
4 | //
5 | // Created by chenyilong on 2017/6/7.
6 | // Copyright © 2017年 Elon Chan. All rights reserved.
7 | //
8 |
9 | #import
10 | #import "AppDelegate.h"
11 |
12 | int main(int argc, char * argv[]) {
13 | @autoreleasepool {
14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemoTests/CYLGCDRunloopDemoTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // CYLGCDRunloopDemoTests.m
3 | // CYLGCDRunloopDemoTests
4 | //
5 | // Created by chenyilong on 2017/6/7.
6 | // Copyright © 2017年 Elon Chan. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface CYLGCDRunloopDemoTests : XCTestCase
12 |
13 | @end
14 |
15 | @implementation CYLGCDRunloopDemoTests
16 |
17 | - (void)setUp {
18 | [super setUp];
19 | // Put setup code here. This method is called before the invocation of each test method in the class.
20 | }
21 |
22 | - (void)tearDown {
23 | // Put teardown code here. This method is called after the invocation of each test method in the class.
24 | [super tearDown];
25 | }
26 |
27 | - (void)testExample {
28 | // This is an example of a functional test case.
29 | // Use XCTAssert and related functions to verify your tests produce the correct results.
30 | }
31 |
32 | - (void)testPerformanceExample {
33 | // This is an example of a performance test case.
34 | [self measureBlock:^{
35 | // Put the code you want to measure the time of here.
36 | }];
37 | }
38 |
39 | @end
40 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemoTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemoUITests/CYLGCDRunloopDemoUITests.m:
--------------------------------------------------------------------------------
1 | //
2 | // CYLGCDRunloopDemoUITests.m
3 | // CYLGCDRunloopDemoUITests
4 | //
5 | // Created by chenyilong on 2017/6/7.
6 | // Copyright © 2017年 Elon Chan. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface CYLGCDRunloopDemoUITests : XCTestCase
12 |
13 | @end
14 |
15 | @implementation CYLGCDRunloopDemoUITests
16 |
17 | - (void)setUp {
18 | [super setUp];
19 |
20 | // Put setup code here. This method is called before the invocation of each test method in the class.
21 |
22 | // In UI tests it is usually best to stop immediately when a failure occurs.
23 | self.continueAfterFailure = NO;
24 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
25 | [[[XCUIApplication alloc] init] launch];
26 |
27 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
28 | }
29 |
30 | - (void)tearDown {
31 | // Put teardown code here. This method is called after the invocation of each test method in the class.
32 | [super tearDown];
33 | }
34 |
35 | - (void)testExample {
36 | // Use recording to get started writing UI tests.
37 | // Use XCTAssert and related functions to verify your tests produce the correct results.
38 | }
39 |
40 | @end
41 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/CYLGCDRunloopDemoUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/CYLGCDRunloopDemo/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment the next line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | # 指定 Master 仓库和阿里云仓库
5 | source 'https://github.com/CocoaPods/Specs.git'
6 | source 'https://github.com/aliyun/aliyun-specs.git'
7 |
8 |
9 | target 'CYLGCDRunloopDemo' do
10 | # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
11 | # use_frameworks!
12 |
13 | pod 'AlicloudFeedback', '~> 3.0.1'
14 |
15 | # Pods for CYLGCDRunloopDemo
16 |
17 | target 'CYLGCDRunloopDemoTests' do
18 | inherit! :search_paths
19 | # Pods for testing
20 | end
21 |
22 | target 'CYLGCDRunloopDemoUITests' do
23 | inherit! :search_paths
24 | # Pods for testing
25 | end
26 |
27 | end
28 |
--------------------------------------------------------------------------------
/Tips/避免使用GCD-Global队列创建Runloop常驻线程/避免使用GCD-Global队列创建Runloop常驻线程.md:
--------------------------------------------------------------------------------
1 | # 避免使用 GCD Global队列创建Runloop常驻线程
2 |
3 | 本文对应 Demo 以及 Markdown 文件在[仓库中](https://github.com/ChenYilong/iOSBlog/tree/master/Tips/避免使用GCD-Global队列创建Runloop常驻线程),文中的错误可以提 PR 到这个文件,我会及时更改。
4 |
5 | ## 目录
6 |
7 |
8 |
9 |
10 | - [避免使用 GCD Global队列创建Runloop常驻线程](#%E9%81%BF%E5%85%8D%E4%BD%BF%E7%94%A8-gcd-global%E9%98%9F%E5%88%97%E5%88%9B%E5%BB%BArunloop%E5%B8%B8%E9%A9%BB%E7%BA%BF%E7%A8%8B)
11 | - [GCD Global队列创建线程进行耗时操作的风险](#gcd-global%E9%98%9F%E5%88%97%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B%E8%BF%9B%E8%A1%8C%E8%80%97%E6%97%B6%E6%93%8D%E4%BD%9C%E7%9A%84%E9%A3%8E%E9%99%A9)
12 | - [避免使用 GCD Global 队列创建 Runloop 常驻线程](#%E9%81%BF%E5%85%8D%E4%BD%BF%E7%94%A8-gcd-global-%E9%98%9F%E5%88%97%E5%88%9B%E5%BB%BA-runloop-%E5%B8%B8%E9%A9%BB%E7%BA%BF%E7%A8%8B)
13 | - [单一 Runloop 常驻线程](#%E5%8D%95%E4%B8%80-runloop-%E5%B8%B8%E9%A9%BB%E7%BA%BF%E7%A8%8B)
14 | - [多个 Runloop 常驻线程](#%E5%A4%9A%E4%B8%AA-runloop-%E5%B8%B8%E9%A9%BB%E7%BA%BF%E7%A8%8B)
15 |
16 |
17 |
18 |
19 | ## GCD Global队列创建线程进行耗时操作的风险
20 |
21 | 先思考下如下几个问题:
22 |
23 | - 新建线程的方式有哪些?各自的优缺点是什么?
24 | - dispatch_async 函数分发到全局队列一定会新建线程执行任务么?
25 | - 如果全局队列对应的线程池如果满了,后续的派发的任务会怎么处置?有什么风险?
26 |
27 | 答案大致是这样的:dispatch_async 函数分发到全局队列不一定会新建线程执行任务,全局队列底层有一个的线程池,如果线程池满了,那么后续的任务会被 block 住,等待前面的任务执行完成,才会继续执行。如果线程池中的线程长时间不结束,后续堆积的任务会越来越多,此时就会存在 APP crash的风险。
28 |
29 | 比如:
30 |
31 |
32 | ```objective-c
33 | - (void)dispatchTest1 {
34 | for (NSInteger i = 0; i< 10000 ; i++) {
35 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
36 | [self dispatchTask:i];
37 | });
38 | }
39 | }
40 |
41 | - (void)dispatchTask:(NSInteger)index {
42 | //模拟耗时操作,比如DB,网络,文件读写等等
43 | sleep(30);
44 | NSLog(@"----:%ld",index);
45 | }
46 | ```
47 |
48 | 以上逻辑用真机测试会有卡死的几率,并非每次都会发生,但多尝试几次就会复现,伴随前后台切换,crash几率增大。
49 |
50 |
51 |
52 | 下面做一下分析:
53 |
54 | 参看 GCD 源码我们可以看到全局队列的相关源码如下:
55 |
56 | ``` c
57 | DISPATCH_NOINLINE
58 | static void
59 | _dispatch_queue_wakeup_global_slow(dispatch_queue_t dq, unsigned int n)
60 | {
61 | dispatch_root_queue_context_t qc = dq->do_ctxt;
62 | uint32_t i = n;
63 | int r;
64 |
65 | _dispatch_debug_root_queue(dq, __func__);
66 | dispatch_once_f(&_dispatch_root_queues_pred, NULL,
67 | _dispatch_root_queues_init);
68 |
69 | #if HAVE_PTHREAD_WORKQUEUES
70 | #if DISPATCH_USE_PTHREAD_POOL
71 | if (qc->dgq_kworkqueue != (void*)(~0ul))
72 | #endif
73 | {
74 | _dispatch_root_queue_debug("requesting new worker thread for global "
75 | "queue: %p", dq);
76 | #if DISPATCH_USE_LEGACY_WORKQUEUE_FALLBACK
77 | if (qc->dgq_kworkqueue) {
78 | pthread_workitem_handle_t wh;
79 | unsigned int gen_cnt;
80 | do {
81 | r = pthread_workqueue_additem_np(qc->dgq_kworkqueue,
82 | _dispatch_worker_thread4, dq, &wh, &gen_cnt);
83 | (void)dispatch_assume_zero(r);
84 | } while (--i);
85 | return;
86 | }
87 | #endif // DISPATCH_USE_LEGACY_WORKQUEUE_FALLBACK
88 | #if HAVE_PTHREAD_WORKQUEUE_SETDISPATCH_NP
89 | if (!dq->dq_priority) {
90 | r = pthread_workqueue_addthreads_np(qc->dgq_wq_priority,
91 | qc->dgq_wq_options, (int)i);
92 | (void)dispatch_assume_zero(r);
93 | return;
94 | }
95 | #endif
96 | #if HAVE_PTHREAD_WORKQUEUE_QOS
97 | r = _pthread_workqueue_addthreads((int)i, dq->dq_priority);
98 | (void)dispatch_assume_zero(r);
99 | #endif
100 | return;
101 | }
102 | #endif // HAVE_PTHREAD_WORKQUEUES
103 | #if DISPATCH_USE_PTHREAD_POOL
104 | dispatch_pthread_root_queue_context_t pqc = qc->dgq_ctxt;
105 | if (fastpath(pqc->dpq_thread_mediator.do_vtable)) {
106 | while (dispatch_semaphore_signal(&pqc->dpq_thread_mediator)) {
107 | if (!--i) {
108 | return;
109 | }
110 | }
111 | }
112 | uint32_t j, t_count;
113 | // seq_cst with atomic store to tail
114 | t_count = dispatch_atomic_load2o(qc, dgq_thread_pool_size, seq_cst);
115 | do {
116 | if (!t_count) {
117 | _dispatch_root_queue_debug("pthread pool is full for root queue: "
118 | "%p", dq);
119 | return;
120 | }
121 | j = i > t_count ? t_count : i;
122 | } while (!dispatch_atomic_cmpxchgvw2o(qc, dgq_thread_pool_size, t_count,
123 | t_count - j, &t_count, acquire));
124 |
125 | pthread_attr_t *attr = &pqc->dpq_thread_attr;
126 | pthread_t tid, *pthr = &tid;
127 | #if DISPATCH_ENABLE_PTHREAD_ROOT_QUEUES
128 | if (slowpath(dq == &_dispatch_mgr_root_queue)) {
129 | pthr = _dispatch_mgr_root_queue_init();
130 | }
131 | #endif
132 | do {
133 | _dispatch_retain(dq);
134 | while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {
135 | if (r != EAGAIN) {
136 | (void)dispatch_assume_zero(r);
137 | }
138 | _dispatch_temporary_resource_shortage();
139 | }
140 | } while (--j);
141 | #endif // DISPATCH_USE_PTHREAD_POOL
142 | }
143 |
144 | ```
145 |
146 |
147 | 对于执行的任务来说,所执行的线程具体是哪个线程,则是通过 GCD 的线程池(Thread Pool)来进行调度,正如[Concurrent Programming: APIs and Challenges](https://www.objc.io/issues/2-concurrency/concurrency-apis-and-pitfalls/)文章里给的示意图所示:
148 |
149 |
150 | 
151 |
152 | 上面贴的源码,我们关注如下的部分:
153 |
154 | 其中有一个用来记录线程池大小的字段 `dgq_thread_pool_size`。这个字段标记着GCD线程池的大小。摘录上面源码的一部分:
155 |
156 | ```c
157 | uint32_t j, t_count;
158 | // seq_cst with atomic store to tail
159 | t_count = dispatch_atomic_load2o(qc, dgq_thread_pool_size, seq_cst);
160 | do {
161 | if (!t_count) {
162 | _dispatch_root_queue_debug("pthread pool is full for root queue: "
163 | "%p", dq);
164 | return;
165 | }
166 | j = i > t_count ? t_count : i;
167 | } while (!dispatch_atomic_cmpxchgvw2o(qc, dgq_thread_pool_size, t_count,
168 | t_count - j, &t_count, acquire));
169 |
170 | ```
171 |
172 |
173 | 从源码中我们可以对应到[官方文档 :Getting the Global Concurrent Dispatch Queues](https://developer.apple.com/library/content/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW5)里的说法:
174 |
175 | > A concurrent dispatch queue is useful when you have multiple tasks that can run in parallel. A concurrent queue is still a queue in that it dequeues tasks in a first-in, first-out order; however, a concurrent queue may dequeue additional tasks before any previous tasks finish. The actual number of tasks executed by a concurrent queue at any given moment is variable and can change dynamically as conditions in your application change. Many factors affect the number of tasks executed by the concurrent queues, including the number of available cores, the amount of work being done by other processes, and the number and priority of tasks in other serial dispatch queues.
176 |
177 | 也就是说:
178 |
179 | 全局队列的底层是一个线程池,向全局队列中提交的 block,都会被放到这个线程池中执行,如果线程池已满,后续再提交 block 就不会再重新创建线程。这就是为什么 Demo 会造成卡顿甚至冻屏的原因。
180 |
181 |
182 |
183 | ## 避免使用 GCD Global 队列创建 Runloop 常驻线程
184 |
185 | 在做网路请求时我们常常创建一个 Runloop 常驻线程用来接收、响应后续的服务端回执,比如NSURLConnection、AFNetworking等等,我们可以称这种线程为 Runloop 常驻线程。
186 |
187 | 正如上文所述,用 GCD Global 队列创建线程进行耗时操作是存在风险的。那么我们可以试想下,如果这个耗时操作变成了 runloop 常驻线程,会是什么结果?下面做一下分析:
188 |
189 | 先介绍下 Runloop 常驻线程的原理,在开发中一般有两种用法:
190 |
191 | - 单一 Runloop 常驻线程:在 APP 的生命周期中开启了唯一的常驻线程来进行网络请求,常用于网络库,或者有维持长连接需求的库,比如: AFNetworking 、 [SocketRocket](https://github.com/facebook/SocketRocket)。
192 | - 多个 Runloop 常驻线程:每进行一次网络请求就开启一条 Runloop 常驻线程,这条线程的生命周期的起点是网络请求开始,终点是网络请求结束,或者网络请求超时。
193 |
194 |
195 | ### 单一 Runloop 常驻线程
196 | 先说第一种用法:
197 |
198 | 以 AFNetworking 为例,[AFURLConnectionOperation](https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking%2FAFURLConnectionOperation.m) 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:
199 |
200 | ```objective-c
201 | + (void)networkRequestThreadEntryPoint:(id)__unused object {
202 | @autoreleasepool {
203 | [[NSThread currentThread] setName:@"AFNetworking"];
204 | NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
205 | [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
206 | [runLoop run];
207 | }
208 | }
209 |
210 | + (NSThread *)networkRequestThread {
211 | static NSThread *_networkRequestThread = nil;
212 | static dispatch_once_t oncePredicate;
213 | dispatch_once(&oncePredicate, ^{
214 | _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
215 | [_networkRequestThread start];
216 | });
217 | return _networkRequestThread;
218 | }
219 | ```
220 |
221 | ### 多个 Runloop 常驻线程
222 |
223 | 第二种用法,我写了一个小 Demo 来模拟这种场景,
224 |
225 | 我们模拟了一个场景:假设所有的网络请求全部超时,或者服务端根本不响应,然后网络库超时检测机制的做法:
226 |
227 | ```objective-c
228 | #import "Foo.h"
229 |
230 | @interface Foo() {
231 | NSRunLoop *_runloop;
232 | NSTimer *_timeoutTimer;
233 | NSTimeInterval _timeoutInterval;
234 | dispatch_semaphore_t _sem;
235 | }
236 | @end
237 |
238 | @implementation Foo
239 |
240 | - (instancetype)init {
241 | if (!(self = [super init])) {
242 | return nil;
243 | }
244 | _timeoutInterval = 1 ;
245 | _sem = dispatch_semaphore_create(0);
246 | // Do any additional setup after loading the view, typically from a nib.
247 | return self;
248 | }
249 |
250 | - (id)test {
251 | // 第一种方式:
252 | // NSThread *networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint0:) object:nil];
253 | // [networkRequestThread start];
254 | //第二种方式:
255 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
256 | [self networkRequestThreadEntryPoint0:nil];
257 | });
258 | dispatch_semaphore_wait(_sem, DISPATCH_TIME_FOREVER);
259 | return @(YES);
260 | }
261 |
262 | - (void)networkRequestThreadEntryPoint0:(id)__unused object {
263 | @autoreleasepool {
264 | [[NSThread currentThread] setName:@"CYLTest"];
265 | _runloop = [NSRunLoop currentRunLoop];
266 | [_runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
267 | _timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(stopLoop) userInfo:nil repeats:NO];
268 | [_runloop addTimer:_timeoutTimer forMode:NSRunLoopCommonModes];
269 | [_runloop run];//在实际开发中最好使用这种方式来确保能runloop退出,做双重的保障[runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:(timeoutInterval+5)]];
270 | }
271 | }
272 |
273 | - (void)stopLoop {
274 | CFRunLoopStop([_runloop getCFRunLoop]);
275 | dispatch_semaphore_signal(_sem);
276 | }
277 |
278 | @end
279 | ```
280 |
281 |
282 | 如果
283 |
284 | ```objective-c
285 | for (int i = 0; i < 300 ; i++) {
286 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
287 | [[Foo new] test];
288 | NSLog(@"🔴类名与方法名:%@(在第%@行),描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), @"");
289 | });
290 | }
291 | ```
292 |
293 | 以上逻辑用真机测试会有卡死的几率,并非每次都会发生,但多尝试几次就会复现,伴随前后台切换,crash几率增大。
294 |
295 | 其中我们采用了 GCD 全局队列的方式来创建常驻线程,因为在创建时可能已经出现了全局队列的线程池满了的情况,所以 GCD 派发的任务,无法执行,而且我们把超时检测的逻辑放进了这个任务中,所以导致的情况就是,有很多任务的超时检测功能失效了。此时就只能依赖于服务端响应来结束该任务(服务端响应能结束该任务的逻辑在 Demo 中未给出),但是如果再加之服务端不响应,那么任务就永远不会结束。后续的网络请求也会就此 block 住,造成 crash。
296 |
297 | 如果我们把 GCD 全局队列换成 NSThread 的方式,那么就可以保证每次都会创建新的线程。
298 |
299 |
300 | 注意:文章中只演示的是超时 cancel runloop 的操作,实际项目中一定有其他主动 cancel runloop 的操作,就比如网络请求成功或失败后需要进行cancel操作。代码中没有展示网络请求成功或失败后的 cancel 操作。
301 |
302 |
303 | Demo 的这种模拟可能比较极端,但是如果你维护的是一个像 AFNetworking 这样的一个网络库,你会放心把创建常驻线程这样的操作交给 GCD 全局队列吗?因为整个 APP 是在共享一个全局队列的线程池,那么如果 APP 把线程池沾满了,甚至线程池长时间占满且不结束,那么 AFNetworking 就自然不能再执行任务了,所以我们看到,即使是只会创建一条常驻线程, AFNetworking 依然采用了 NSThread 的方式而非 GCD 全局队列这种方式。
304 |
305 | 注释:以下方法存在于老版本[AFN 2.x](https://github.com/AFNetworking/AFNetworking/tree/2.x) 中。
306 |
307 |
308 | ```objective-c
309 | + (void)networkRequestThreadEntryPoint:(id)__unused object {
310 | @autoreleasepool {
311 | [[NSThread currentThread] setName:@"AFNetworking"];
312 | NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
313 | [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
314 | [runLoop run];
315 | }
316 | }
317 |
318 | + (NSThread *)networkRequestThread {
319 | static NSThread *_networkRequestThread = nil;
320 | static dispatch_once_t oncePredicate;
321 | dispatch_once(&oncePredicate, ^{
322 | _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
323 | [_networkRequestThread start];
324 | });
325 | return _networkRequestThread;
326 | }
327 | ```
328 |
329 | 正如你所看到的,没有任何一个库会用 GCD 全局队列来创建常驻线程,而你也应该
330 |
331 | > 避免使用 GCD Global 队列来创建 Runloop 常驻线程。
332 |
333 |
334 |
335 |
336 |
337 |
--------------------------------------------------------------------------------