mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 04:17:17 +00:00
docs: 批量博文
This commit is contained in:
92
Chapter1 - iOS/1.1.md
Normal file
92
Chapter1 - iOS/1.1.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# 工程大小优化之图片资源
|
||||
|
||||
> 一点点iOS项目本身功能较多,导致应用体积也比较大。一个Xcode工程下图片资源占用了很大的空间,且如果有些App需要一键换肤功能,呵呵,不知道得做多少图片。每套图片还需要设置1x@,2x@,3x@等
|
||||
|
||||
## 简介
|
||||
|
||||
IconFont技术起源于Web领域的Web Font技术。随着时间的推移,网页设计越来越漂亮。但是电脑预装的字体远远无法满足设计者的要求,于是Web Font技术诞生了。一个英文字库并不大,通过网络下载字体,完成网页的显示。有了Web Font技术,大大提升了设计师的发挥空间。
|
||||
|
||||
网页设计中图标需要适配多个分辨率,每个图标需要占用一次网络请求。于是有人想到了用Web Font的方法来解决这两个问题,就是IconFont技术。将矢量的图标做成字体,一次网络请求就够了,可以保真缩放。解决这个问题的另一个方式是图片拼合的Sprite图。
|
||||
|
||||
Web领域使用IconFont类似的技术已经多年,当我在15年接触BootStrap的时候Font Awesome技术大行其道。最近IconFont技术在iOS图片资源方面得以应用,最近有点时间自己研究整理了一番,在此记录学习点滴。
|
||||
|
||||
## 优点
|
||||
|
||||
* 减小体积,字体文件比图片要小
|
||||
* 图标保真缩放,解决2x/3x乃至将来的nx图问题
|
||||
* 方便更改颜色大小,图片复用
|
||||
|
||||
## 缺点
|
||||
|
||||
* 只适用于
|
||||
`纯色icon`
|
||||
* 使用unicode字符难以理解
|
||||
* 需要维护字体库
|
||||
|
||||
网上说了一大堆如何制作IconFont的方法,在此不做讨论。
|
||||
|
||||
## 我们说说怎么用
|
||||
|
||||
1. 首先选取一些有丰富资源的网站,我使用阿里的IconFont多年,其他的没去研究,所以此处直接使用阿里的产品。地址:[http://www.iconfont.cn/plus](http://www.iconfont.cn/plus)
|
||||
|
||||
2. 打开网站在线挑选好合适的图标加入购物车,如图
|
||||

|
||||
|
||||
1. 选择好之后在购物车查看,然后点击下载代码
|
||||
|
||||
2. 打开下载好的文件,其机构如下,我们在iOS项目开发过程中使用unicode的形式使用IconFont,所以打开demo\_unicode.html
|
||||
|
||||

|
||||
|
||||
|
||||
**注意:** 创建 UIFont 使用的是字体名,而不是文件名;文本值为 8 位的 Unicode 字符,我们可以打开 demo.html 查找每个图标所对应的 HTML 实体 Unicode 码,比如: "店" 对应的 HTML 实体 Unicode 码为:0x3439 转换后为:\U00003439 就是将 0x 替换为 \U 中间用 0 填补满长度为 8 个字符
|
||||
|
||||
# Xcode中使用IconFont
|
||||
|
||||
初步尝试使用
|
||||
|
||||
1. 首先看看如何简单实用IconFont
|
||||
2. 首先将下载好的文件夹中的 **iconfont.ttf** 加入到Xcode工程中,确保加入成功在Build检查
|
||||
|
||||

|
||||
|
||||
1. 怎么用?
|
||||
|
||||
```Objective-c
|
||||
|
||||
NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:@"\U0000e696 \U0000e6ab \U0000e6ac \U0000e6ae"];
|
||||
[attributedStr addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 1)];
|
||||
[attributedStr addAttribute:NSForegroundColorAttributeName value:[UIColor orangeColor] range:NSMakeRange(3, 1)];
|
||||
[attributedStr addAttribute:NSForegroundColorAttributeName value:[UIColor blackColor] range:NSMakeRange(9, 1)];
|
||||
self.label.attributedText = attributedStr;
|
||||
[self.view addSubview:self.label];
|
||||
|
||||
pragma mark - getter and setter
|
||||
-(UILabel *)label{
|
||||
if (!_label) {
|
||||
_label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, BoundWidth-200, 40)];
|
||||
_label.font = [UIFont fontWithName:@"iconfont" size:24];
|
||||
_label.textColor = [UIColor purpleColor];
|
||||
}
|
||||
return _label;
|
||||
}
|
||||
```
|
||||
|
||||
#### 做进一步封装,实用更加方便
|
||||
|
||||
利用IconFont生成1个UIImage只需要 LBPIconFontmake(par1, par2, par3),par1:iconfont的unicode值;par2:图片大小;par3:图片的颜色值。其中,LBPIconFontmake是一个宏,#define LBPIconFontmake(text,size,color) [[LBPFontInfo alloc] initWithText:text withSize:size andColor:color]。
|
||||
|
||||
```Objective-c
|
||||
self.latestImageView.image = [UIImage iconWithInfo:LBPIconFontmake(@"\U0000e6ac", 60, @"000066") ];
|
||||
```
|
||||

|
||||
|
||||
1. LBPFontInfo来封装字体信息
|
||||
2. UIColor+picker根据十六进制字符串来设置颜色
|
||||
3. LBPIconFont向系统中注册IconFont字体库,并使用
|
||||
4. UIImage+LBPIconFont封装一个使用IconFont的Image分类
|
||||
|
||||
|
||||
# [Demo地址](https://github.com/FantasticLBP/IconFont_Demo)
|
||||
|
||||
|
||||
206
Chapter1 - iOS/1.10.md
Normal file
206
Chapter1 - iOS/1.10.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# UIWebView加载网页内容
|
||||
|
||||
可以通过本地文件、url等方式。
|
||||
|
||||
```objective-c
|
||||
NSString *htmlPath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
|
||||
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:htmlPath]];
|
||||
[self.webView loadRequest:request];
|
||||
```
|
||||
|
||||
## Native调用JavaScript
|
||||
|
||||
Native调用JS是通过UIWebView的stringByEvaluatingJavaScriptFromString 方法实现的,该方法返回js脚本的执行结果。
|
||||
|
||||
```objective-c
|
||||
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];
|
||||
```
|
||||
|
||||
实际上就是调用了网页的Window下的一个对象。如果我们需要让native端调用js方法,那么这个js方法必须在window下可以访问到。
|
||||
|
||||
|
||||
## JavaScript调用Native
|
||||
|
||||
反过来,JavaScript调用Native,并没有现成的API可以调用,而是间接地通过一些其它手段来实现。UIWebView有个代理方法:在UIWebView内发起的任何网络请求都可以通过delegate函数在Native层得到通知。由此思路,我们就可以在UIWebView内发起一个自定义的网络请求,通常是这样的格式:**jsbridge://methodName?param1=value1¶m2=value2...**
|
||||
|
||||
在UIWebView的delegate函数中,我们判断请求的scheme,如果request.URL.scheme是jsbridge,那么就不进行网页内容的加载,而是去执行相应的方法。方法名称就是request.URL.host。参数可以通过request.URL.query得到。
|
||||
|
||||
问题来了??
|
||||
|
||||
发起这样1个网络请求有2种方式。1:location.href .2:iframe。通过location.href有个问题,就是如果js多次调用原生的方法也就是location.href的值多次变化,Native端只能接受到最后一次请求,前面的请求会被忽略掉。
|
||||
|
||||
使用ifrmae方式,以调用Native端的方法。
|
||||
|
||||
```javascript
|
||||
var iFrame;
|
||||
iFrame = document.createElement("iframe");
|
||||
iFrame.style.height = "1px";
|
||||
iFrame.style.width = "1px";
|
||||
iFrame.style.display = "none";
|
||||
iFrame.src = url;
|
||||
document.body.appendChild(iFrame);
|
||||
setTimeout(function(){
|
||||
iFrame.remove();
|
||||
},100);
|
||||
```
|
||||
|
||||
举个🌰:
|
||||
|
||||
需求:
|
||||
|
||||
原生端提供一个UIWebView,加载一个网页内容。还有1个按钮,按钮点击一下网页增加一段段落文本。网页上有2个输入框,用户输入数字,点击按钮,js将用户输入的参数告诉native端,native去执行加法,计算完成后将结果返回给js
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf8">
|
||||
<script language="javascript">
|
||||
function loadURL(url) {
|
||||
var iFrame;
|
||||
iFrame = document.createElement("iframe");
|
||||
iFrame.style.height = "1px";
|
||||
iFrame.style.width = "1px";
|
||||
iFrame.style.display = "none";
|
||||
iFrame.src = url;
|
||||
document.body.appendChild(iFrame);
|
||||
setTimeout(function () {
|
||||
iFrame.remove();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
|
||||
function receiveValue(value) {
|
||||
alert("从原生拿到加法结果为:" + value);
|
||||
}
|
||||
|
||||
function check() {
|
||||
var par1 = document.getElementById("par1").value;
|
||||
var par2 = document.getElementById("par2").value;
|
||||
loadURL("JSBridge://plus?par1=" + par1 + "&par2=" + par2);
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<input type="text" placeholder="请输入数字" id="par1" /> +
|
||||
<input type="text" placeholder="请输入数字" id="par2" />
|
||||
<input type="button" value="=" onclick="check()" />
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```objective-c
|
||||
-(void)addContentToWebView{
|
||||
NSString *jsString = @" var pNode = document.createElement(\"p\"); pNode.innerText = \"我是由原生代码调用js后将一段文件添加到html上,也就是注入\";document.body.appendChild(pNode);";
|
||||
[self.webView stringByEvaluatingJavaScriptFromString:jsString];
|
||||
}
|
||||
|
||||
|
||||
-(NSInteger)plusparm:(NSInteger)par1 parm2:(NSInteger)par2{
|
||||
return par1 + par2;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -- UIWebViewDelegate
|
||||
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
|
||||
NSURL *url = request.URL;
|
||||
NSString *scheme = url.scheme;
|
||||
NSString *method = url.host;
|
||||
NSString *parms = url.query;
|
||||
NSArray *pars = [parms componentsSeparatedByString:@"&"];
|
||||
NSInteger par1 = [[pars[0] substringFromIndex:5] integerValue];
|
||||
NSInteger par2 = [[pars[1] substringFromIndex:5] integerValue];
|
||||
if ([scheme isEqualToString:@"jsbridge"]) {
|
||||
//发现scheme是JSBridge,那么就是自定义的URLscheme,不去加载网页内容而拦截去处理事件。
|
||||
|
||||
if ([method isEqualToString:@"plus"]) {
|
||||
NSInteger result = [self plusparm:par1 parm2:par2];
|
||||
[self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"receiveValue(%@);",@(result)]];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Android 端如何与 JS 通信(2种方法)
|
||||
|
||||
- webview.loadUrl()
|
||||
- Webview.evaluateJavascript()
|
||||
|
||||
> 2者区别:
|
||||
>
|
||||
> 1. loadUrl() 会刷新页面,evaluateJavascript() 则不会刷新页面,效率高
|
||||
> 2. loadUrl() 得不到 JS 的返回值;evaluateJavascrip() 则可以获取返回值
|
||||
> 3. evaluateJavascrip() 在 Android 4.4 之后才可以使用
|
||||
|
||||
注意:Android 可以直接调用 JS 的 alert() 方法是因为 alert 方法直接挂载在 window 对象上。但是 Native 与 JS 可能不止一个方法、多个方法多个属性去访问,这样都直接挂载在 window 对象上不是明智之举。因为后期维护很不方便。所以我们在 Native 和 JS 之间会设置一个桥接对象,像一个中间层一样,让2端互调。
|
||||
|
||||
Android 需要在页面加载完,也就是 webview 的 onPageFinished 方法中写调用逻辑,否则不会执行
|
||||
|
||||
```java
|
||||
webView.loadUrl("javascript:callJsFunction('soloname')")
|
||||
webView.evaluateJavascript("javascript:callJsFunction('soloname')"
|
||||
```
|
||||
|
||||
|
||||
### JS 如何与 Android 通信
|
||||
|
||||
- 通过 Webview 的 addJavascriptInterface() 进行对象映射
|
||||
- 通过 WebviewClient 的 shouldOverrideUrlLoading() 方法回调拦截 Url
|
||||
- 通过 webChromeClient 的 onJsAlert()、onJSPrompt() 方法回调拦截 JS 对话框 alert()、confirm()、prompt() 等消息
|
||||
|
||||
第一种最简洁,但是在 Android 4.2 以下存在漏洞。
|
||||
|
||||
实验:Android webview 上跑一个网页,点击网页的按钮,让 Native 弹出一个字符串。
|
||||
|
||||
```vue
|
||||
methods: {
|
||||
showAndroidToast() {
|
||||
$App.showToast("哈哈,我是js调用的")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
public class JsJavaBridge {
|
||||
|
||||
private Activity activity;
|
||||
private WebView webView;
|
||||
|
||||
public JsJavaBridge(Activity activity, WebView webView) {
|
||||
this.activity = activity;
|
||||
this.webView = webView;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void onFinishActivity() {
|
||||
activity.finish();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void showToast(String msg) {
|
||||
ToastUtils.show(msg);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
然后通过 webview 设置 Android 类与 JS 代码的映射
|
||||
|
||||
```
|
||||
webView.addJavascriptInterface(new JsJavaBridge(this, tbsWebView), "$App");
|
||||
```
|
||||
|
||||
这里将类 JsJavaBridge 在 JS 中映射为了 $App,所以在 Vue 中可以这样调用 `$App.showToast("哈哈,我是js调用的")`。
|
||||
|
||||
|
||||
|
||||
## 同步和异步问题
|
||||
|
||||
js调用native是通过在一个网页上插入一个iframe,这个iframe插入完了就完了,执行的结果需要native另外调用stringByEvaluatingJavaScriptString 方法通知js。这明显是1个异步的调用。而stringByEvaluatingJavaScriptString方法会返回执行js脚本的结果。本质上是一个同步调用
|
||||
|
||||
所以js call native是异步,native call js是同步。
|
||||
51
Chapter1 - iOS/1.11.md
Normal file
51
Chapter1 - iOS/1.11.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# iOS中的事件
|
||||
|
||||
|
||||
|
||||
* 用户在使用App的时候会产生各种事件
|
||||
* 触摸事件、重力加速计事件、远程遥控事件
|
||||
* 只有继承自UIResponder才可以响应事件
|
||||
* UIView、UIApplication、UIViewController都可以响应事件
|
||||
* ## UIResponder
|
||||
* UIResponder内部提供了一些方法处理事件
|
||||
|
||||
```
|
||||
//触摸事件
|
||||
-(void)touchBegan:(NSSet *)touches withEvent:(UIEvent *)event;
|
||||
-(void)touchMoved:(NSSet *)touches withEvent:(UIEvent *)event;
|
||||
-(void)touchEnded:(NSSet *)touches withEvent:(UIEvent *)event;
|
||||
-(void)touchCanceled:(NSSet *)touches withEvent:(UIEvent *)event;
|
||||
|
||||
//加速计事件
|
||||
-(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
|
||||
-(void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
|
||||
-(void)motionCanceled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
|
||||
|
||||
//远程控制事件
|
||||
-(void)remoteControlReceivedWithEvent:(UIEvent *)event;
|
||||
```
|
||||
|
||||
# 事件的产生和传递
|
||||
|
||||
* 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中去
|
||||
* UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常先分发事件给应用程序的主窗口(keyWindow)
|
||||
* 主窗口会在视图层次结构中寻找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程中最重要的一步。
|
||||
|
||||
|
||||
|
||||
找到合适的视图控件后,就会调用视图控件的touch方法来做具体的事件处理逻辑
|
||||
|
||||
|
||||
|
||||
## UIView不接收事件的3种情况
|
||||
|
||||
1. 不接收用户交互。view.userInteractionEnabled = NO
|
||||
2. 隐藏。view.hidden = YES
|
||||
3. 透明度很低。view.alpha = 0.0 ~ 0.01
|
||||
|
||||
|
||||
|
||||
注意:UIImageView的userInteractionEnabled默认为NO,因此UIImageView及其它上面的子控件默认是不能接受触摸事件的。
|
||||
|
||||
|
||||
|
||||
224
Chapter1 - iOS/1.12.md
Normal file
224
Chapter1 - iOS/1.12.md
Normal file
@@ -0,0 +1,224 @@
|
||||
|
||||
# NSFileManager
|
||||
|
||||
> 想操作文件,该去了解下NSFileManager
|
||||
|
||||
注意://小窍门:打印数组或者字典,里面包含中文,直接用%@打印会看不到中文,可用for遍历访问
|
||||
|
||||
* 单例方法得到文件管理者对象
|
||||
|
||||
```
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
```
|
||||
|
||||
* 判断是否存在指定的文件
|
||||
|
||||
```
|
||||
#define LogBool(value) NSLog(@"%@",value==YES?@"YES":@"NO");
|
||||
|
||||
NSString *filepath = @"/Users/geek/Desktop/data.plist";
|
||||
BOOL res = [fileManager fileExistsAtPath:filepath];
|
||||
LogBool(res)
|
||||
```
|
||||
|
||||
* 根据给出的文件路径判断是否存在文件,且判断路径是文件还是文件夹
|
||||
|
||||
```
|
||||
NSString *filepath1 = @"/Users/geek/Desktop/data.plist";
|
||||
BOOL isDirectory = NO;
|
||||
BOOL isExist = [fileManager fileExistsAtPath:filepath1 isDirectory:&isDirectory];
|
||||
if (isExist) {
|
||||
NSLog(@"文件存在");
|
||||
if (isDirectory) {
|
||||
NSLog(@"文件夹路径");
|
||||
}else{
|
||||
NSLog(@"文件路径");
|
||||
}
|
||||
}else{
|
||||
NSLog(@"给定的路径不存在");
|
||||
}
|
||||
```
|
||||
|
||||
* 判断文件或者文件夹是否可以读取
|
||||
|
||||
```
|
||||
//这是一个系统文件(不可读)
|
||||
NSString *filePath2 = @"/.DocumentRevisions-V100 ";
|
||||
BOOL isReadable = [fileManager isReadableFileAtPath:filePath2];
|
||||
if (isReadable) {
|
||||
NSLog(@"文件可读取");
|
||||
} else {
|
||||
NSLog(@"文件不可读取");
|
||||
}
|
||||
```
|
||||
|
||||
* 判断文件是否可以写入
|
||||
|
||||
```
|
||||
//系统文件不可写入
|
||||
BOOL isWriteAble = [fileManager isWritableFileAtPath:filePath2];
|
||||
if (isWriteAble) {
|
||||
NSLog(@"文件可写入");
|
||||
} else {
|
||||
NSLog(@"文件不可写入");
|
||||
}
|
||||
```
|
||||
|
||||
* 判断文件是否可以删除
|
||||
|
||||
```
|
||||
//系统文件不可删除
|
||||
BOOL isDeleteAble = [fileManager isDeletableFileAtPath:filePath2];
|
||||
if (isDeleteAble) {
|
||||
NSLog(@"文件可以删除");
|
||||
} else {
|
||||
NSLog(@"文件不可删除");
|
||||
}
|
||||
```
|
||||
|
||||
* 获取文件信息
|
||||

|
||||
|
||||
```
|
||||
NSError *error = nil;
|
||||
NSDictionary *fileInfo = [fileManager attributesOfItemAtPath:filepath1 error:&error];
|
||||
// NSLog(@"文件信息:%@,错误信息:%@",fileInfo,error);
|
||||
NSLog(@"文件大小:%@",fileInfo[NSFileSize]);
|
||||
```
|
||||
|
||||
* 获取指定目录下的所有目录(列出所有的文件和文件夹)
|
||||
|
||||
```
|
||||
NSString *filePath3 = @"/Users/geek/desktop";
|
||||
NSArray *subs = [fileManager subpathsAtPath:filePath3];
|
||||
NSLog(@"Desktop目录下所有的所有文件和文件夹");
|
||||
//小窍门:打印数组或者字典,里面包含中文,直接用%@打印会看不到中文,可用for遍历访问
|
||||
for (NSString *item in subs) {
|
||||
NSLog(@"%@",item);
|
||||
}
|
||||
```
|
||||
|
||||
* 获取指定目录下的子目录和文件(不包含子孙)
|
||||
|
||||
```
|
||||
NSError *erroe = nil;
|
||||
NSArray *children = [fileManager contentsOfDirectoryAtPath:filePath3 error:&erroe];
|
||||
NSLog(@"Desktop目录下的文件和文件夹");
|
||||
for (NSString *item in children) {
|
||||
NSLog(@"%@",item);
|
||||
}
|
||||
```
|
||||
|
||||
* 在指定目录创建文件
|
||||
|
||||
```
|
||||
NSString *filePath1 = @"/Users/geek/Desktop/data.text";
|
||||
NSData *data = [@"我要学好OC" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
BOOL createFile = [fileManager createFileAtPath:filePath1 contents:data attributes:nil];
|
||||
if (createFile) {
|
||||
NSLog(@"文件创建成功");
|
||||
} else {
|
||||
NSLog(@"文件创建失败");
|
||||
}
|
||||
```
|
||||
|
||||
* 在指定目录创建文件夹(参数说明:withIntermediateDirectories后的参数为Bool代表。YES:一路创建;NO:不会做一路创建)
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
设置一路创建为NO,如果文件夹不存在则停止创建文件
|
||||
|
||||
```
|
||||
NSString *filePath2 = @"/Users/geek/Desktop/海贼王";
|
||||
NSError *error = nil;
|
||||
BOOL createDirectory = [fileManager createDirectoryAtPath:filePath2 withIntermediateDirectories:NO attributes:nil error:&error];
|
||||
if (createDirectory) {
|
||||
NSLog(@"文件夹创建成功");
|
||||
} else {
|
||||
NSLog(@"文件夹创建失败,原因:%@",error);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//一路创建失败(文件夹不存在就不创建)
|
||||
NSString *filePath3 = @"/Users/geek/Desktop/海贼王";
|
||||
BOOL createDirectory1 = [fileManager createDirectoryAtPath:filePath3 withIntermediateDirectories:NO attributes:nil error:&error];
|
||||
if (createDirectory1) {
|
||||
NSLog(@"文件夹创建成功");
|
||||
} else {
|
||||
NSLog(@"文件夹创建失败,原因:%@",error);
|
||||
}
|
||||
```
|
||||
|
||||
* 复制文件
|
||||
|
||||
```
|
||||
NSString *filePath4 = @"/Users/geek/Desktop/动漫";
|
||||
|
||||
BOOL copyRes = [fileManager copyItemAtPath:filePath3 toPath:filePath4 error:nil];
|
||||
if (copyRes) {
|
||||
NSLog(@"文件复制成功");
|
||||
} else {
|
||||
NSLog(@"文件复制失败");
|
||||
}
|
||||
```
|
||||
|
||||
* 移动文件
|
||||
|
||||
```
|
||||
NSString *filePath5 = @"/Users/geek/Downloads/动漫";
|
||||
BOOL moveRes = [fileManager moveItemAtPath:filePath3 toPath:filePath5 error:nil];
|
||||
if (moveRes) {
|
||||
NSLog(@"文件移动成功");
|
||||
} else {
|
||||
NSLog(@"文件移动失败");
|
||||
}
|
||||
```
|
||||
|
||||
* 可以给文件重命名
|
||||
|
||||
```
|
||||
//可以给文件重命名
|
||||
NSString *filePath6 = @"/Users/geek/Downloads/卡通";
|
||||
[fileManager moveItemAtPath:filePath5 toPath:filePath6 error:nil];
|
||||
```
|
||||
|
||||
* 删除文件
|
||||
|
||||
```
|
||||
BOOL deleteRes = [fileManager removeItemAtPath:filePath6 error:nil];
|
||||
if (deleteRes) {
|
||||
NSLog(@"文件删除成功");
|
||||
} else {
|
||||
NSLog(@"文件删除失败");
|
||||
}
|
||||
```
|
||||
|
||||
# NSFileManager小病毒
|
||||
```
|
||||
//单例方法得到文件管理者对象
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSString *filePath = @"/Users/geek/desktop/delete/";
|
||||
while (1) {
|
||||
//判断该文件路径是否存在
|
||||
BOOL exist = [fileManager fileExistsAtPath:filePath];
|
||||
if (exist) {
|
||||
//找出该路径下的所有文件
|
||||
NSArray *subs = [fileManager contentsOfDirectoryAtPath:filePath error:nil];
|
||||
if (subs.count > 0) {
|
||||
for (int i=0; i<subs.count; i++) {
|
||||
NSString *fullFileStr = [NSString stringWithFormat:@"%@%@",filePath,subs[i]];
|
||||
//判断文件是否可删除
|
||||
BOOL canDelete = [fileManager isDeletableFileAtPath:fullFileStr];
|
||||
if (canDelete) {
|
||||
[fileManager removeItemAtPath:fullFileStr error:nil];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//5秒钟为周期,开始不断扫描文件并删除
|
||||
[NSThread sleepForTimeInterval:5];
|
||||
}
|
||||
```
|
||||
226
Chapter1 - iOS/1.13.md
Normal file
226
Chapter1 - iOS/1.13.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# UINavagationController重写push和pop方法
|
||||
|
||||
> 有个需求就是在App的Tab的首页需要显示浮动着的交互动画的机器人,该机器人具有机器学习的特点,因此可以不断的与用户交互,怎么样实现只浮动在App的5个tab首页,当点击跳转不是首页的时候不需要显示
|
||||
|
||||
因为5个tab上是5个自定义的导航控制器,所以我们可以监听导航控制器的push和pop事件,并且在push和pop的事件中判断当前控制器的字控制器的数量来判断窗口上的机器人是否需要显示,其实这里要说的就是如何监听push和pop事件。
|
||||
|
||||
```
|
||||
/**
|
||||
* 重写这个方法的目的:为了拦截整个push过程,拿到所有push进来的子控制器
|
||||
*
|
||||
* @param viewController 当前push进来的子控制器
|
||||
*/
|
||||
-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
|
||||
{
|
||||
// if (viewController != 栈底控制器) {
|
||||
if (self.viewControllers.count > 0) {
|
||||
|
||||
for (UIView *view in [UIApplication sharedApplication].keyWindow.subviews) {
|
||||
if ([view isKindOfClass:[XLRobotImageView class]]) {
|
||||
if (self.viewControllers.count > 0) {
|
||||
self.robotView = (XLRobotImageView *)view;
|
||||
[view removeFromSuperview];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 当push这个子控制器时, 隐藏底部的工具条
|
||||
viewController.hidesBottomBarWhenPushed = YES;
|
||||
|
||||
UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
backButton.frame = CGRectMake(0, 0, 44, 44);
|
||||
[backButton setImage:[UIImage imageNamed:@"backArror"] forState:UIControlStateNormal];
|
||||
[backButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
|
||||
backButton.adjustsImageWhenHighlighted = NO;
|
||||
backButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
|
||||
|
||||
backButton.titleLabel.font = [UIFont systemFontOfSize:16];
|
||||
|
||||
[backButton addTarget:self action:@selector(back) forControlEvents:UIControlEventTouchUpInside];
|
||||
[backButton setImageEdgeInsets:UIEdgeInsetsMake(0, 5 * BoundWidth/375, 0, 0)];
|
||||
viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backButton];
|
||||
}
|
||||
|
||||
// 将viewController压入栈中
|
||||
[super pushViewController:viewController animated:animated];
|
||||
}
|
||||
|
||||
|
||||
-(UIViewController *)popViewControllerAnimated:(BOOL)animated{
|
||||
//在5个tab的首页需要显示
|
||||
NSArray *vcs = self.viewControllers;
|
||||
UIViewController *topVC = vcs[vcs.count - 2];
|
||||
if (self.viewControllers.count >= 2) {
|
||||
if ([topVC isKindOfClass:[MZPregnancyHomeController class]] ||
|
||||
[topVC isKindOfClass:[HLSettingViewController class]] ||
|
||||
[topVC isKindOfClass:[BBXEditViewController class]] ||
|
||||
[topVC isKindOfClass:[HLFriendTopicController class]] ||
|
||||
[topVC isKindOfClass:[MZBookViewController class]]
|
||||
) {
|
||||
[[UIApplication sharedApplication].keyWindow addSubview:self.robotView];
|
||||
}
|
||||
}
|
||||
return [super popViewControllerAnimated:animated];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**敲黑板,注意啦**
|
||||
|
||||
因为我做的一个全局的机器人只需要浮动在App的5个模块的首页,所以当页面进入第二层的时候就需要隐藏机器人,当App的顶层控制器是最外层的首页的时候再显示机器人,用导航控制器的push和pop监听就可以实现这个需求,但是遇到的一个问题就是当App从首页进入到第二层页面,用于手动右滑且滑到一半停止,这样子页面还是停留在第二层但是此时也会触发pop方法上面的代码就有点问题
|
||||
|
||||
因此想办法需要监听导航控制器里面每个控制器的出现事件,找到一个方法 **- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated;** 恰好满足需求,以前没用过记录下来
|
||||
|
||||
```
|
||||
-(void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated{
|
||||
self.interactivePopGestureRecognizer.enabled = [self.viewControllers count] > 1 ;
|
||||
|
||||
if ([viewController isKindOfClass:[MZPregnancyHomeController class]] ||
|
||||
[viewController isKindOfClass:[HLSettingViewController class]] ||
|
||||
[viewController isKindOfClass:[BBXEditViewController class]] ||
|
||||
[viewController isKindOfClass:[HLFriendTopicController class]] ||
|
||||
[viewController isKindOfClass:[MZBookViewController class]]
|
||||
) {
|
||||
[[UIApplication sharedApplication].keyWindow addSubview:self.robotView];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```
|
||||
#
|
||||
|
||||
|
||||
@interface HLNavigationController ()<UIGestureRecognizerDelegate>
|
||||
|
||||
@property (nonatomic, strong) XLRobotImageView *robotView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HLNavigationController
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
[[UINavigationBar appearance] setTitleTextAttributes:
|
||||
[NSDictionary dictionaryWithObjectsAndKeys:[UIColor whiteColor], NSForegroundColorAttributeName, [UIFont fontWithName:@"Lato-Regular" size:18], NSFontAttributeName, nil]];
|
||||
|
||||
[[UINavigationBar appearance] setTranslucent:NO];
|
||||
|
||||
NSMutableDictionary *testAttr = [NSMutableDictionary dictionary];
|
||||
testAttr[NSForegroundColorAttributeName] = [UIColor whiteColor];
|
||||
testAttr[NSFontAttributeName] = [UIFont systemFontOfSize:18];
|
||||
|
||||
[[UINavigationBar appearance] setTitleTextAttributes:testAttr];
|
||||
|
||||
testAttr = [NSMutableDictionary dictionary];
|
||||
testAttr[NSForegroundColorAttributeName] = [UIColor whiteColor];
|
||||
|
||||
[[UIBarButtonItem appearance] setTitleTextAttributes:testAttr forState:UIControlStateNormal];
|
||||
|
||||
|
||||
[[UINavigationBar appearance] setTintColor:[UIColor whiteColor]];
|
||||
|
||||
[[UINavigationBar appearance] setBarStyle:UIBarStyleBlack];
|
||||
|
||||
[[UINavigationBar appearance] setBackgroundImage:[[UIImage alloc] init] forBarMetrics:UIBarMetricsDefault];
|
||||
[[UINavigationBar appearance] setShadowImage:[[UIImage alloc] init]];
|
||||
|
||||
}
|
||||
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[[UINavigationBar appearance] setBarTintColor:GlobalMainColor];
|
||||
|
||||
// 设置pop手势的代理
|
||||
self.interactivePopGestureRecognizer.delegate = self;
|
||||
self.delegate = self;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写这个方法的目的:为了拦截整个push过程,拿到所有push进来的子控制器
|
||||
*
|
||||
* @param viewController 当前push进来的子控制器
|
||||
*/
|
||||
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
|
||||
{
|
||||
// if (viewController != 栈底控制器) {
|
||||
if (self.viewControllers.count > 0) {
|
||||
|
||||
for (UIView *view in [UIApplication sharedApplication].keyWindow.subviews) {
|
||||
if ([view isKindOfClass:[XLRobotImageView class]]) {
|
||||
if (self.viewControllers.count > 0) {
|
||||
self.robotView = (XLRobotImageView *)view;
|
||||
[view removeFromSuperview];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 当push这个子控制器时, 隐藏底部的工具条
|
||||
viewController.hidesBottomBarWhenPushed = YES;
|
||||
|
||||
UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
backButton.frame = CGRectMake(0, 0, 44, 44);
|
||||
[backButton setImage:[UIImage imageNamed:@"backArror"] forState:UIControlStateNormal];
|
||||
[backButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
|
||||
backButton.adjustsImageWhenHighlighted = NO;
|
||||
backButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
|
||||
|
||||
backButton.titleLabel.font = [UIFont systemFontOfSize:16];
|
||||
|
||||
[backButton addTarget:self action:@selector(back) forControlEvents:UIControlEventTouchUpInside];
|
||||
[backButton setImageEdgeInsets:UIEdgeInsetsMake(0, 5 * BoundWidth/375, 0, 0)];
|
||||
viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backButton];
|
||||
}
|
||||
|
||||
// 将viewController压入栈中
|
||||
[super pushViewController:viewController animated:animated];
|
||||
}
|
||||
|
||||
|
||||
- (void)back{
|
||||
[self popViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
#pragma mark - <UIGestureRecognizerDelegate>
|
||||
/**
|
||||
* 这个代理方法的作用:决定pop手势是否有效
|
||||
*
|
||||
* @return YES:手势有效, NO:手势无效
|
||||
*/
|
||||
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if (self.disableGesture) {
|
||||
return NO;
|
||||
}
|
||||
return self.viewControllers.count > 1;
|
||||
}
|
||||
|
||||
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated{
|
||||
self.interactivePopGestureRecognizer.enabled = [self.viewControllers count] > 1 ;
|
||||
|
||||
if ([viewController isKindOfClass:[MZPregnancyHomeController class]] ||
|
||||
[viewController isKindOfClass:[HLSettingViewController class]] ||
|
||||
[viewController isKindOfClass:[BBXEditViewController class]] ||
|
||||
[viewController isKindOfClass:[HLFriendTopicController class]] ||
|
||||
[viewController isKindOfClass:[MZBookViewController class]]
|
||||
) {
|
||||
[[UIApplication sharedApplication].keyWindow addSubview:self.robotView];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
144
Chapter1 - iOS/1.14.md
Normal file
144
Chapter1 - iOS/1.14.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# 自定义URL Schemes
|
||||
|
||||
1、引言
|
||||
|
||||
URL Schemes 应用在 iOS 上已经很久了。对于使用者来说,在沙盒机制下的 iOS 中,如果想做到一定程度上的自动化就不可避免地要用到 URL Schemes。但因为 URL Schemes 的使用方式不像传统 iOS 使用者接触到的图形界面那样可以直观地点来点去,造成了对它有兴趣的人(尤其是对英文有恐惧的人)一定程度上理解的困难。
|
||||
|
||||
|
||||
2、简介苹果的沙盒机制
|
||||
|
||||
|
||||
苹果选择沙盒来保障用户的隐私和安全,但沙盒也阻碍了应用间合理的信息共享,于是有了 URL Schemes 这个解决办法。
|
||||
|
||||
一般来说,我们使用的智能设备上有许多我们的个人信息。比如:联系方式、银行卡/信用卡信息、支付宝/Paypal/各大商城的账户密码、照片甚至行程与位置信息等。
|
||||
|
||||
如果说,你设备上的每一个应用,不管是官方的还是你从任何商城安装的应用都可以随意地获取这些信息,那么你轻则收到骚扰信息和邮件、重则后果不堪设想。如何让这些信息不被其它应用随意使用,或者说,如何让这些信息仅在设备所有者本人知情并允许的情况下被使用,是所有智能设备与操作系统所要在乎的核心安全问题。
|
||||
|
||||
在 iOS 这个操作系统中,针对这个问题,苹果使用了名为「沙盒」的机制:应用只能访问它声明可能访问的资源。一切提交到 App Store 的应用都必须遵守这个机制。
|
||||
|
||||
在安全方面沙盒是个很好的解决办法,但是有些矫枉过正。敏感的个人信息我们不愿意透露,却不代表所有的信息我们都不想与其它应用共享。
|
||||
|
||||
比如说我们要一次性地(没错,只按一次)把多个事件放到日历中,这些事件包含日期时间以及持续时间等信息,如果 App 之间信息不能沟通,就无法做到这点。(在下文中的 x-callback-URL 的部分会详述整个过程)
|
||||
|
||||
类似于一次性添加多个日历事件这样的,我们在使用智能设备的过程中会遇到很多不必要的重复的步骤。大多数人对这些重复的步骤是不自觉的,就像当自己电脑里有一批文件需要批量重命名的时候,他们机械地重复着重命名的过程。但是当我们掌握了这些设备运行的模式,或者有了一些工具,我们就能将这些重复的步骤全部节省下来。在 iOS 上,我们可以利用的工具就是 URL Schemes。
|
||||
|
||||
|
||||
3、URL Schemes 是什么
|
||||
|
||||
Custom URL scheme 的好处就是,你可以在其它程序中通过这个url打开应用程序。如A应用程序注册了一个url scheme:myApp, 那么就在mobile浏览器中就可以通过<href=’myApp://’>打开你的应用程序A。
|
||||
|
||||
对比网页url就比较好理解url scheme。给出一个url “http://bxu2359670321.my3w.com/view/login.php”,它的格式:protocol :// hostname[:port] / path / [;parameters][?query]#fragment。
|
||||
因此这个url的protocol就是http。对比URL Scheme,给出例子“weixin://dl/moments“,前面的weixin:就代表微信的scheme。你可以完全按照理解一个网页的 URL ——也就是它的网址——的方式来理解一个 iOS 应用的 URL。即Scheme是**://**之前的那段字符
|
||||
|
||||
|
||||
###注意###
|
||||
|
||||
1、所有的网页都有url;但未必所有的应用都有自己的 URL Schemes,更不是每个应用的每个功能都有相应的 URL Schemes
|
||||
|
||||
2、一个网址只对应一个网页,但并非每个 URL Schemes 都只对应一款应用。这点是因为苹果没有对 URL Schemes 有不允许重复的硬性要求
|
||||
|
||||
3、一般网页的 URL 比较好预测,而 iOS 上的 URL Schemes 因为没有统一标准,所以非常难猜,通过猜来获取 iOS 应用的 URL Schemes 是不现实的。(我推荐将Bundle identifier反转)
|
||||
|
||||
|
||||
###上干货###
|
||||
|
||||
1、注册自定义 URL Scheme
|
||||
|
||||
|
||||
1)注册自定义 URL Scheme 的第一步是创建 URL Scheme — 在 Xcode Project Navigator 中找到并点击工程 info.plist 文件。当该文件显示在右边窗口,在列表上点击鼠标右键,选择 Add Row:
|
||||
|
||||

|
||||
|
||||
2)点击左边剪头打开列表,可以看到 Item 0,一个字典实体。展开 Item 0,可以看到 URL Identifier,一个字符串对象。该字符串是你自定义的 URL scheme 的名字。建议采用反转Bundle idenmtifier的方法保证该名字的唯一性
|
||||

|
||||
|
||||
|
||||
3)点击 Item 0 新增一行,从下拉列表中选择 URL Schemes,敲击键盘回车键完成插入。(注意 URL Schemes 是一个数组,允许应用定义多个 URL schemes。)展开该数据并点击 Item 0。你将在这里定义自定义 URL scheme 的名字。只需要名字,不要在后面追加 ://
|
||||
|
||||

|
||||
|
||||
2、拿浏览器做简单验证
|
||||
|
||||
在地址栏中熟入自定的url scheme。此时必须保证该浏览器所在设备上已经安装了具有自定义url scheme的App。
|
||||

|
||||
|
||||
|
||||
3、新建Xcode工程,做个App试试看,这里我就放一个Button,点击打开url
|
||||
代码。
|
||||
|
||||
|
||||
```
|
||||
- (IBAction)open:(id)sender {
|
||||
NSString *url = @"zhunaer://?name=lbp&age=22";
|
||||
if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:url]]) {
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]];
|
||||
}else{
|
||||
NSLog(@"打不开");
|
||||
}
|
||||
}
|
||||
```
|
||||
结果打不开,为什么?
|
||||
因为在新建App的plist中没加query schemes
|
||||
|
||||
|
||||
```
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>zhunaer</string>
|
||||
</array>
|
||||
```
|
||||
|
||||
4、如果需要在2个App之间传值,怎么办?可以用URL Scheme解决。
|
||||
|
||||
在被打开的App的Appdelegate.m中实现-(BOOL)application:(UIApplication *)application openURL:(nonnull NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nonnull id)annotation;
|
||||
|
||||
|
||||
|
||||
```
|
||||
-(BOOL)application:(UIApplication *)application openURL:(nonnull NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nonnull id)annotation{
|
||||
NSLog(@"calling application bundle id: %@",sourceApplication);
|
||||
NSLog(@"url shceme:%@",[url scheme]);
|
||||
NSLog(@"参数:%@",[url query]);
|
||||
if ([sourceApplication isEqualToString:@"com.geek.test1"]) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
在需要打开第三方App的点击事件处的url处后面加上参数,类似NSString *url = @"zhunaer://?name=lbp&age=22";
|
||||
|
||||
注意:在URL Scheme后加?然后跟网页的url的参数一样写法。
|
||||
|
||||
5、如何判断是指定App打开,或者某些App不让打开我们的App?
|
||||
做了实验。
|
||||
|
||||
A:在需要打开第三方App的工程中将Bundle identifier改为“com.geek.test2”,其余不变
|
||||
|
||||
B:在被打开的App的AppDelegate.m中
|
||||
|
||||
|
||||
```
|
||||
-(BOOL)application:(UIApplication *)application openURL:(nonnull NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nonnull id)annotation{
|
||||
NSLog(@"calling application bundle id: %@",sourceApplication);
|
||||
NSLog(@"url shceme:%@",[url scheme]);
|
||||
NSLog(@"参数:%@",[url query]);
|
||||
if ([sourceApplication isEqualToString:@"com.geek.test1"]) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
```
|
||||

|
||||
|
||||
###实验结果###
|
||||
|
||||
依旧可以打开App,即使断点走入Return NO
|
||||
|
||||
结论:如果你想阻止其它应用调用你的应用,**创建一个与众不同的 URL scheme**。尽管这不能保证你的应用不会被调用,但至少大大降低了这种可能性。
|
||||
|
||||
|
||||
参考:https://sspai.com/post/31500#01
|
||||
54
Chapter1 - iOS/1.15.md
Normal file
54
Chapter1 - iOS/1.15.md
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
# URL Schemes 的发展
|
||||
|
||||
|
||||
|
||||
URL Schemes 的发展过程可以说就是 iOS 效率工具类 App 的发展过程。
|
||||
|
||||
起初的苹果建立的 Apple URL Schemes 只是用于自用,里面只有邮件、电话、iTunes 搜索、Youtube 视频等一些内置服务的 URL。
|
||||
|
||||
个人认为 URL Schemes 第一次大火是在 2011 年末(如有异议欢迎指正),那个时期也是越狱的鼎盛时期,那个时期越狱后大家都会装的一个插件是 SBSettings[1]。越狱的人都知道每当新系统发布的时候,等待新系统的越狱发布是最撩人的,而这段时期那些「不越狱就能做到某种越狱功能」的应用经常一时间风头无两。
|
||||
|
||||
2011年 iOS 5 发布带来了通知中心,没过多久,出现了一大批使用 iOS 系统设置的 URL Schemes 的 App 神奇地完成了接近 SBSettings 的功能——它们可以让我们从通知中心直接跳转到某些 App 的特定界面,比如 Twitter 的发推界面。它们甚至还可以直接跳转到系统设置里的 Wi-Fi 选项。在这一批 App 中,就有如今效率软件霸主之一 Launch Center Pro 的前身——Launch Center。
|
||||
|
||||
|
||||
|
||||
## 基本 URL Schemes
|
||||
|
||||
基本 URL Schemes 的能力虽然简单有限,但使用情境却是最普遍的。
|
||||
|
||||
我所谓的基本 URL Schemes,是指一个 URL 的 Schemes 部分,比如上文提到的微信的 weixin:。这个部分的唯一功能,就是打开相应应用,而不能够跳转到任何功能。
|
||||
|
||||
绝大多数所谓支持 URL Schemes 的应用,一般都是只有这么一个部分,它一般是这个应用的名称,比如 OmniFocus 这款应用,它的基本 URL Schemes 就是 Omnifocus:。如果应用的主名称是个中文名的话,它的 URL Schemes 也许会是应用名的拼音,比如 墨客 这款应用,它的基本 URL Schemes 是 moke:。
|
||||
|
||||
但,我前面提过了网页 URL 和 iOS 应用的 URL 的三个重要区别,其中第三项,就是 iOS 上的 URL Schemes 并不规范,一个应用的 URL 可以是各种各样的:
|
||||
<ul>
|
||||
<li>Coursera 的 URL 是:coursera-mobile:</li>
|
||||
<li>Duet 这款游戏的 URL 是:x-kumo-duet:</li>
|
||||
<li>Monument 这款游戏的 URL 是:fb690517270143345:</li>
|
||||
<li>Feedly 的 URL 是:fb129765800430121:</li>
|
||||
<li>扇贝新闻的 URL 是:wx95962d02b9c3e2f7:</li>
|
||||
</ul>
|
||||
|
||||
它们目前并没有统一的规则,所以猜测一个应用的意义并不太大,你可以试试,但不要过于指望这种方式。如何查找一个应用的基本 URL Schemes,只要那个应用支持 URL Schemes 就能找到。
|
||||
|
||||
|
||||
步骤
|
||||
<ul>
|
||||
<li>首先,在 iTunes 找到你想用 URL 打开的 App,右键选择在文件夹中显示:</li>
|
||||
<li>然后解压该文件:</li>
|
||||
<li>解压完毕后,在解压出的文件夹中,找到 .app 文件:</li>
|
||||
<li>然后选择显示包内容:</li>
|
||||
<li>找到 info.plist 这个文件,用你电脑里能打开它的 App 打开它(Xcode没得说)。</li>
|
||||
<li>然后查找 URLSchemes:</li>
|
||||
<li>在 CFBundleURLSchemes 下的那两行就是该 App 的基本 URL Schemes 了。</li>
|
||||
</ul>
|
||||
|
||||
## 复杂 URL Schemes
|
||||
|
||||
|
||||
参考链接:[URL Scheme](https://sspai.com/post/31500#fnref:2)
|
||||
|
||||
|
||||
|
||||
|
||||
10
Chapter1 - iOS/1.16.md
Normal file
10
Chapter1 - iOS/1.16.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Swift、OC混编
|
||||
|
||||
```
|
||||
1、在oc文件中使用swift文件。
|
||||
选中项目TARGETS->Building Settings->搜索“Objective-C Genereated Interface Header Name”对应的名字。
|
||||
在oc文件中需要使用swift的地方,头文件导入上一步对应的名字。
|
||||
```
|
||||
|
||||
|
||||
|
||||
10
Chapter1 - iOS/1.17.md
Normal file
10
Chapter1 - iOS/1.17.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# 对于不可调节高度的UI控件进行改变frame
|
||||
|
||||
* 对于不能调节高度的控件比如 UISlider、UISwitch、UIProgressView 等控件的宽高可以用 \(仿射变化\)transform 属性控制高度。
|
||||
|
||||
```
|
||||
myswitch.transform = CGAffineTransformMakeScale(1,5);
|
||||
```
|
||||
|
||||
|
||||
|
||||
193
Chapter1 - iOS/1.18.md
Normal file
193
Chapter1 - iOS/1.18.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# 简单的 Model 与 JSON 相互转换
|
||||
|
||||
```
|
||||
// JSON:
|
||||
{
|
||||
"uid":123456,
|
||||
"name":"Harry",
|
||||
"created":"1965-07-31T00:00:00+0000"
|
||||
}
|
||||
|
||||
// Model:
|
||||
@interface User : NSObject
|
||||
@property UInt64 uid;
|
||||
@property NSString *name;
|
||||
@property NSDate *created;
|
||||
@end
|
||||
@implementation User
|
||||
@end
|
||||
|
||||
// 将 JSON (NSData,NSString,NSDictionary) 转换为 Model:
|
||||
User *user = [User yy_modelWithJSON:json];
|
||||
// 将 Model 转换为 JSON 对象:
|
||||
NSDictionary *json = [user yy_modelToJSONObject];
|
||||
|
||||
```
|
||||
|
||||
|
||||
### Model 属性名和 JSON 中的 Key 不相同
|
||||
|
||||
```
|
||||
// JSON:
|
||||
{
|
||||
"n":"Harry Pottery",
|
||||
"p": 256,
|
||||
"ext" : {
|
||||
"desc" : "A book written by J.K.Rowing."
|
||||
},
|
||||
"ID" : 100010
|
||||
}
|
||||
|
||||
// Model:
|
||||
@interface Book : NSObject
|
||||
@property NSString *name;
|
||||
@property NSInteger page;
|
||||
@property NSString *desc;
|
||||
@property NSString *bookID;
|
||||
@end
|
||||
@implementation Book
|
||||
//返回一个 Dict,将 Model 属性名对映射到 JSON 的 Key。
|
||||
+ (NSDictionary *)modelCustomPropertyMapper {
|
||||
return @{@"name" : @"n",
|
||||
@"page" : @"p",
|
||||
@"desc" : @"ext.desc",
|
||||
@"bookID" : @[@"id",@"ID",@"book_id"]};
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
你可以把一个或一组 json key (key path) 映射到一个或多个属性。如果一个属性没有映射关系,那默认会使用相同属性名作为映射。
|
||||
|
||||
在 json->model 的过程中:如果一个属性对应了多个 json key,那么转换过程会按顺序查找,并使用第一个不为空的值。
|
||||
|
||||
在 model->json 的过程中:如果一个属性对应了多个 json key (key path),那么转换过程仅会处理第一个 json key (key path);如果多个属性对应了同一个 json key,则转换过过程会使用其中任意一个不为空的值。
|
||||
|
||||
|
||||
### Model 包含其他 Model
|
||||
```
|
||||
// JSON
|
||||
{
|
||||
"author":{
|
||||
"name":"J.K.Rowling",
|
||||
"birthday":"1965-07-31T00:00:00+0000"
|
||||
},
|
||||
"name":"Harry Potter",
|
||||
"pages":256
|
||||
}
|
||||
|
||||
// Model: 什么都不用做,转换会自动完成
|
||||
@interface Author : NSObject
|
||||
@property NSString *name;
|
||||
@property NSDate *birthday;
|
||||
@end
|
||||
@implementation Author
|
||||
@end
|
||||
|
||||
@interface Book : NSObject
|
||||
@property NSString *name;
|
||||
@property NSUInteger pages;
|
||||
@property Author *author; //Book 包含 Author 属性
|
||||
@end
|
||||
@implementation Book
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
### 容器类属性
|
||||
```
|
||||
@class Shadow, Border, Attachment;
|
||||
|
||||
@interface Attributes
|
||||
@property NSString *name;
|
||||
@property NSArray *shadows; //Array<Shadow>
|
||||
@property NSSet *borders; //Set<Border>
|
||||
@property NSMutableDictionary *attachments; //Dict<NSString,Attachment>
|
||||
@end
|
||||
|
||||
@implementation Attributes
|
||||
// 返回容器类中的所需要存放的数据类型 (以 Class 或 Class Name 的形式)。
|
||||
+ (NSDictionary *)modelContainerPropertyGenericClass {
|
||||
return @{@"shadows" : [Shadow class],
|
||||
@"borders" : Border.class,
|
||||
@"attachments" : @"Attachment" };
|
||||
}
|
||||
@end
|
||||
|
||||
```
|
||||
|
||||
### 黑名单与白名单
|
||||
```
|
||||
@interface User
|
||||
@property NSString *name;
|
||||
@property NSUInteger age;
|
||||
@end
|
||||
|
||||
@implementation Attributes
|
||||
// 如果实现了该方法,则处理过程中会忽略该列表内的所有属性
|
||||
+ (NSArray *)modelPropertyBlacklist {
|
||||
return @[@"test1", @"test2"];
|
||||
}
|
||||
// 如果实现了该方法,则处理过程中不会处理该列表外的属性。
|
||||
+ (NSArray *)modelPropertyWhitelist {
|
||||
return @[@"name"];
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
### 数据校验与自定义转换
|
||||
|
||||
```
|
||||
// JSON:
|
||||
{
|
||||
"name":"Harry",
|
||||
"timestamp" : 1445534567
|
||||
}
|
||||
|
||||
// Model:
|
||||
@interface User
|
||||
@property NSString *name;
|
||||
@property NSDate *createdAt;
|
||||
@end
|
||||
|
||||
@implementation User
|
||||
// 当 JSON 转为 Model 完成后,该方法会被调用。
|
||||
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。
|
||||
// 你也可以在这里做一些自动转换不能完成的工作。
|
||||
- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic {
|
||||
NSNumber *timestamp = dic[@"timestamp"];
|
||||
if (![timestamp isKindOfClass:[NSNumber class]]) return NO;
|
||||
_createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue];
|
||||
return YES;
|
||||
}
|
||||
|
||||
// 当 Model 转为 JSON 完成后,该方法会被调用。
|
||||
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。
|
||||
// 你也可以在这里做一些自动转换不能完成的工作。
|
||||
- (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic {
|
||||
if (!_createdAt) return NO;
|
||||
dic[@"timestamp"] = @(n.timeIntervalSince1970);
|
||||
return YES;
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
### Coding/Copying/hash/equal/description
|
||||
|
||||
```
|
||||
@interface YYShadow :NSObject <NSCoding, NSCopying>
|
||||
@property (nonatomic, copy) NSString *name;
|
||||
@property (nonatomic, assign) CGSize size;
|
||||
@end
|
||||
|
||||
@implementation YYShadow
|
||||
// 直接添加以下代码即可自动完成
|
||||
- (void)encodeWithCoder:(NSCoder *)aCoder { [self yy_modelEncodeWithCoder:aCoder]; }
|
||||
- (id)initWithCoder:(NSCoder *)aDecoder { self = [super init]; return [self yy_modelInitWithCoder:aDecoder]; }
|
||||
- (id)copyWithZone:(NSZone *)zone { return [self yy_modelCopy]; }
|
||||
- (NSUInteger)hash { return [self yy_modelHash]; }
|
||||
- (BOOL)isEqual:(id)object { return [self yy_modelIsEqual:object]; }
|
||||
- (NSString *)description { return [self yy_modelDescription]; }
|
||||
@end
|
||||
|
||||
```
|
||||
56
Chapter1 - iOS/1.19.md
Normal file
56
Chapter1 - iOS/1.19.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 实现波浪动画
|
||||
|
||||
波浪的形状绘制在 CAShapeLayer 上。通过 CADisplayLink 与屏幕刷新频率同步,每次刷新都绘制新的波浪,并改变小船的位置和角度。另外,水和天空的颜色是渐变的,由 CAGradientLayer 实现,其中,显示水的 CAGradientLayer 需要有波浪形状的 CAShapeLayer 的遮罩\(mask\)。
|
||||
|
||||
### CAShapeLayer
|
||||
|
||||
CAShapeLayer 的属性 path \(CGPath\)就是图层要显示的形状。把波浪的形状绘制出来,赋值给此属性即可。
|
||||
|
||||
### CADisplayLink
|
||||
|
||||
创建 CADisplayLink,相应的 target 实现屏幕刷新时要调用的方法。把 CADisplayLink 加入 RunLoop 中。通过 isPaused 属性控制 CADisplayLink 是否暂停\(target 是否调用方法\)
|
||||
|
||||
```
|
||||
private var waveLink: CADisplayLink?waveLink = CADisplayLink(target: self, selector: #selector(waveLinkRefresh))
|
||||
waveLink?.isPaused = true
|
||||
waveLink?.add(to: .current, forMode: .defaultRunLoopMode)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 绘制波浪
|
||||
|
||||
波浪的形状关键是正弦函数曲线
|
||||
|
||||
```
|
||||
y = A*sin(x+B)
|
||||
```
|
||||
|
||||
参数 A 决定了波浪的高度;参数 B 决定了波浪在 x 轴的位置。
|
||||
|
||||
用一个属性 currentPhase 表示参数 B。每次屏幕刷新的时候用 currentPhase 绘制,然后更新此属性,加上一个固定的数。这样波浪就会朝左或右匀速移动。
|
||||
|
||||
为了使波浪高度逐渐变化,用一个属性表示参数 A,然后每次绘制后更新此属性,加上一个固定的数,直到波浪高度达到目标值。
|
||||
|
||||
### 小船的位置和旋转角度
|
||||
|
||||
已知小船 x 轴坐标,通过正弦函数可以直接计算出小船的 y 轴坐标。此外,小船需要随着波浪旋转,旋转至船底与波浪表面相切。这就要对正弦函数进行求导
|
||||
|
||||
```
|
||||
y' = A * cos(x + B)
|
||||
```
|
||||
|
||||
用以上式子计算出小船所在位置的 y',表示正弦函数在此处的切线斜率,几何意义是切线与 x 轴的夹角的正切值。反正切即可求出切线与 x 轴的夹角,也就是小船需要旋转的角度
|
||||
|
||||
```
|
||||
angle = atan(y')
|
||||
```
|
||||
|
||||
用以上旋转角度,改变小船视图\(UIView\)的 transform,调用 CGAffineTransformRotate 方法,实现小船的旋转。
|
||||
|
||||
### CAGradientLayer
|
||||
|
||||
CAGradientLayer 默认的颜色渐变方向是由上至下。给 colors 属性赋值一个包含 CGColor 的数组,则图层颜色由上至下,从数组第一个值经中间值渐变至最后一个值。
|
||||
|
||||
显示水的 CAGradientLayer 需要呈现波浪形状,需要 CAShapeLayer 的遮罩。把绘制好波浪形状的 CAShapeLayer 赋值给 CAGradientLayer 的 mask 属性即可。
|
||||
|
||||
172
Chapter1 - iOS/1.2.md
Normal file
172
Chapter1 - iOS/1.2.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# 看透构造方法
|
||||
|
||||
## 构造方法
|
||||
|
||||
* new 方法的内部就是先调用 alloc 方法,再调用 init 方法
|
||||
* alloc 方法:那个类接受 alloc 消息,那么该方法返回该接受类的对象,并把对象返回
|
||||
* init 方法:是1个对象方法,作用:初始化对象
|
||||
* 创建对象的步骤:先使用 alloc 创建1个对象,再使用 init 初始化这个对象,才可以使用这个对象
|
||||
* 使用1个未被初始化的对象是很危险的
|
||||
* init 方法:作用:初始化对象,为对象赋初始值,叫做构造方法
|
||||
|
||||
## 重写init构造方法
|
||||
* 如果想创建出来的对象的属性值不是默认的初始化值,则需要重写 init 方法
|
||||
* 重写 init 方法的规范:
|
||||
* 必须要先调用父类的 init 方法(因为当前类初始化就是通过\`\[父类init\]\`去初始化),然后将返回值赋值给self
|
||||
* 调用 init 方法有可能会失败,如果失败直接返回nil
|
||||
* 判断父类是否初始化成功。如果 `self != nil`,则代表初始化成功
|
||||
* 如果初始化成功就去初始化当前对象的属性
|
||||
* 最后返回 self
|
||||
|
||||
#### 解惑:
|
||||
|
||||
1. 为什么要调用父类的 init 方法?
|
||||
1. 当前类有 isa 指针,当前类的 isa 指针赋值是通过父类的 init 方法赋值的。
|
||||
2. 需要保证当前对象的父类属性同时被初始化
|
||||
2. 重写 init 方法的规范:
|
||||
|
||||
```
|
||||
-(instancetype)init{
|
||||
if (self = [super init]) {
|
||||
//todo:自定义属性的初始化
|
||||
}
|
||||
return self;
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
//Person
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface Person : NSObject
|
||||
@property NSString* name;
|
||||
@property int age;
|
||||
|
||||
-(void)sayHi;
|
||||
|
||||
@end
|
||||
|
||||
#import "Person.h"
|
||||
@implementation Person
|
||||
|
||||
-(void)sayHi{
|
||||
NSLog(@"Hi");
|
||||
}
|
||||
|
||||
-(instancetype)init{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.name = @"杭城小刘";
|
||||
self.age = 22;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@end
|
||||
|
||||
|
||||
//测试
|
||||
|
||||
Person *p1 = [[Person alloc] init]; //p1.name = "杭城小刘",p1.age =22;
|
||||
Person *p2 = [Person new]; //p2.name = "杭城小刘",p2.age =22;
|
||||
```
|
||||
|
||||
如果2个类的关系为组合关系,且它的一个属性是另一个类的对象,那么当该类初始化的时候默认它的属性为 nil,那么如何初始化?
|
||||
|
||||
```
|
||||
-(instancetype)init{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.name = @"lbp";
|
||||
self.age = 22;
|
||||
self.pig = [[Pig alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
//测试
|
||||
Person *p1 = [[Person alloc] init]; //p1.dog != nil
|
||||
```
|
||||
|
||||
## 自定义构造方法
|
||||
|
||||
* 现状:虽然每次双肩的对象的属性值不是默认的,但是每次初始化的对象的值都是一样的。
|
||||
|
||||
* 需求:每次实例化的对象的属性值由调用者决定
|
||||
|
||||
* 解决办法:自定义构造方法
|
||||
|
||||
* 自定义构造方法规范:
|
||||
|
||||
* 自定义构造方法的返回值为 instancetype
|
||||
|
||||
* 方法的命名必须以 initWith 开头
|
||||
|
||||
* 方法的实现类似 init 的实现
|
||||
|
||||
**注意:此时不能使用 new 来调用。(因为 new 的实现是先 alloc 再 init ,默认 init 的实现是给属性赋默认值)**
|
||||
|
||||
```
|
||||
-(instancetype)initWithName:(NSString *)name andAge:(int)age{
|
||||
if (self = [super init]) {
|
||||
self.name = name;
|
||||
self.age = age;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
//Person
|
||||
#import <Foundation/Foundation.h>
|
||||
@interface Person : NSObject
|
||||
@property NSString* name;
|
||||
@property int age;
|
||||
|
||||
-(instancetype)initWithName:(NSString *)name andAge:(int)age;
|
||||
@end
|
||||
|
||||
#import "Person.h"
|
||||
@implementation Person
|
||||
|
||||
-(instancetype)init{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.name = @"lbp";
|
||||
self.age = 22;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
//不能在构造方法之外给self赋值
|
||||
//编译器认为只有以initWith开头的方法是构造方法
|
||||
|
||||
-(instancetype)initWithName:(NSString *)name andAge:(int)age{
|
||||
if (self = [super init]) {
|
||||
self.name = name;
|
||||
self.age = age;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
//测试
|
||||
Person *p1 = [[Person alloc] init];
|
||||
Person *p2 = [Person new];
|
||||
Person *p3 = [[Person alloc] initWithName:@"杭城小刘2号" andAge:23];
|
||||
```
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
关于“自定义构造方法必须以 initWith 开头”做个实验
|
||||
|
||||

|
||||
|
||||
报错信息很明显:不能在构造方法之外给 self 赋值
|
||||
|
||||
因为,编译器认为只有以 initWith 开头的方法是构造方法
|
||||
88
Chapter1 - iOS/1.20.md
Normal file
88
Chapter1 - iOS/1.20.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Block探究
|
||||
|
||||
|
||||
### 1、Block作为函数参数可以应用到函数式编程
|
||||
```
|
||||
self.prepare.play(@"女人");
|
||||
- (ViewController *(^)(NSString *))play{
|
||||
NSLog(@"即将吃喝玩乐");
|
||||
ViewController *(^block)(NSString *) = ^ViewController *(NSString *fun){
|
||||
NSLog(@"接下来玩%@,好不好?",fun);
|
||||
return self;
|
||||
};
|
||||
return block;
|
||||
}
|
||||
|
||||
- (ViewController *)prepare{
|
||||
NSLog(@"我们先好好休息一下。😂\n");
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
###2、Block作为函数的返回值可以作为链式编程
|
||||
|
||||
|
||||
```
|
||||
[self blockAsFunctionalProgramming];
|
||||
|
||||
- (void)blockAsFunctionalProgramming{
|
||||
[self reprepare:^{
|
||||
NSLog(@"接下来玩女人,好不好?😊");
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)reprepare:(void(^)(void))replay{
|
||||
NSLog(@"我们先好好休息一下。😂\n");
|
||||
replay();
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
###3、Block 访问、修改外部变量
|
||||
|
||||
* 打开 Terminal.app,编写一段c代码
|
||||
|
||||
```
|
||||
#include "stdio.h"
|
||||
|
||||
int main(){
|
||||
|
||||
printf("Coming\n");
|
||||
__block int a = 10;
|
||||
|
||||
printf("开始->%p %d\n",&a,a);
|
||||
|
||||
void(^block)(int a) = ^void(int a){
|
||||
a += 10;
|
||||
printf("中间->%p %d\n",&a,a);
|
||||
};
|
||||
block(a);
|
||||
printf("结束->%p %d\n",&a,a);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
* 之后用 **gcc** 编译一下。在同层目录下得到一个 **a.out** 的可执行文件。
|
||||
|
||||
```
|
||||
gcc index.c
|
||||
|
||||
```
|
||||
|
||||
* 之后用 **clang** 编译成 C++ 文件,可以看到系统底层是如何处理 block 外部的变量、以及如何在 block 里面处理变量的。
|
||||
|
||||
```
|
||||
clang -rewrite-objc index.c
|
||||
```
|
||||

|
||||
|
||||
|
||||
###4、Block 经常造成循环引用
|
||||
* 如果 block 作为函数参数的话,且这个函数是在对象的层级,那么可能会造成循环应用。 self -> func -> block -> self.
|
||||
此时需要在 block 里面访问 self 的时候将 self 修饰为 __weak
|
||||
80
Chapter1 - iOS/1.21.md
Normal file
80
Chapter1 - iOS/1.21.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# 禅与 Objective-C 编程艺术
|
||||
|
||||
## 警告和错误
|
||||
|
||||
* 警告
|
||||
```
|
||||
#warning Dude, don't compare floating point numbers like this!
|
||||
```
|
||||
|
||||
* 错误
|
||||
```
|
||||
#warning Dude, don't compare floating point numbers like this!
|
||||
```
|
||||
|
||||
* 让编译器忽略忽略你这段代码的警告
|
||||
|
||||
大多数 iOS 开发者平时并没有和很多编译器选项打交道。一些选项是对控制严格检查(或者不检查)你的代码或者错误的。有时候,你想要用 pragma 直接产生一个异常,临时打断编译器的行为。
|
||||
|
||||
当你使用ARC的时候,编译器帮你插入了内存管理相关的调用。但是这样可能产生一些烦人的事情。比如你使用 NSSelectorFromString 来动态地产生一个 selector 调用的时候,ARC不知道这个方法是哪个并且不知道应该用那种内存管理方法,你会被提示 performSelector may cause a leak because its selector is unknown(执行 selector 可能导致泄漏,因为这个 selector 是未知的).
|
||||
|
||||
如果你知道你的代码不会导致内存泄露,你可以通过加入这些代码忽略这些警告
|
||||
|
||||
```
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
|
||||
[myObj performSelector:mySelector withObject:name];
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
```
|
||||
|
||||
|
||||
* 忽略没用使用变量的编译警告
|
||||
|
||||
```
|
||||
#pragma used(foo)
|
||||
```
|
||||
|
||||
```
|
||||
- (NSInteger)giveMeFive
|
||||
{
|
||||
NSString *foo;
|
||||
#pragma unused (foo)
|
||||
return 5;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
* 善用代码块
|
||||
|
||||
一个 GCC 非常模糊的特性、以及 Clang 也有的特性:代码块如果在闭合的括号内,会返回最后语句的值。
|
||||
|
||||
```
|
||||
self. = ({
|
||||
NSString *urlString = [NSString stringWithFormat:@"%@/%@", baseURLString, endpoint];
|
||||
[NSURL URLWithString:urlString];
|
||||
});
|
||||
```
|
||||
|
||||
这个特性非常适合组织小块的代码,给代码阅读者一个重要的入口且减小相关干扰,能让读者聚焦于关键的变量和函数中,此外这个方法有个优点:变量在代码块的区域内有效,可以减小对其他作用域的命名污染。
|
||||
|
||||
|
||||
* 方法参数断言
|
||||
|
||||
你的方法可能需要一些参数来满足特定的条件(比如不能为 nil),在这种情况下最好使用 **NSParameterAssert()** 来断言条件是否成立
|
||||
|
||||
括号内的条件为 false 的时候则断言抛出异常
|
||||
|
||||
```
|
||||
NSParameterAssert(message.length > 0);
|
||||
```
|
||||
|
||||
```
|
||||
[self testAssert:nil]; //*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: message.length > 0'
|
||||
- (void)testAssert:(NSString *)message{
|
||||
NSParameterAssert(!(message.length < 1));
|
||||
NSLog(@"%@",message);
|
||||
}
|
||||
|
||||
```
|
||||
35
Chapter1 - iOS/1.22.md
Normal file
35
Chapter1 - iOS/1.22.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# 修改 UITextField 的 placeholder样式
|
||||
|
||||
> 对于 UITextField 的 placeholder 私有属性来说 Apple 不允许我们直接修改,但是按照经验我们有2种方式可以实现自定义 placeholder 的样式
|
||||
|
||||
|
||||
### 1、利用 KVC 对 UITextField 的私有属性修改
|
||||
|
||||
```
|
||||
|
||||
[self.invitecodeTextfield setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
|
||||
[self.invitecodeTextfield setValue:[UIFont systemFontOfSize:35] forKeyPath:@"_placeholderLabel.font"];
|
||||
|
||||
```
|
||||
|
||||
|
||||
### 2、利用 Apple 提供的 API 进行修改
|
||||
|
||||
UITextField 有个属性 attributedPlaceholder,利用它我们可以修改 placeholder 的样式
|
||||
|
||||
|
||||
```
|
||||
|
||||
self.invitecodeTextfield.attributedPlaceholder = [LBPHightedAttributedString setAllText:@"我要Testing" andSpcifiStr:@"Testing" withColor:[UIColor redColor] specifiStrFont:[UIFont systemFontOfSize:17]];
|
||||
|
||||
```
|
||||
|
||||
|
||||
其中 **LBPHightedAttributedString** 是我封装的一个关于 NSMutableAttributedString 的工具,可以对一个指定的字符串内部的字符串进行全局查找并高亮设置的小工具,具体可以查看地址
|
||||
|
||||
|
||||
[LBPHightedAttributedString](https://github.com/FantasticLBP/BlogDemos/tree/master/LBPAttributedStringTools/LBPHightedAttributedString "LBPHightedAttributedString")
|
||||
|
||||
|
||||
|
||||
|
||||
27
Chapter1 - iOS/1.23.md
Normal file
27
Chapter1 - iOS/1.23.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# UIScrollView 拖拽滑动时收起键盘
|
||||
|
||||
|
||||
> 当一个页面的 UIScrollView/UITableView 上有输入框时,为了较好的体验,就是当滑动的时候需要回收键盘
|
||||
|
||||
* 最开始的做法是设置 UIScrollView 的代理位当前控制器,监听 scrollViewWillBeginDragging 方法,找到 keyWindow 并且 endEditing
|
||||
|
||||
|
||||
```
|
||||
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
|
||||
[[UIApplication sharedApplication].keyWindow endEditing:YES];
|
||||
}
|
||||
```
|
||||
|
||||
* 之后偶然有幸看到一个 UIScrollView 的属性"keyboardDismissModel"。实现上述需求只需要一行代码
|
||||
|
||||
```
|
||||
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
|
||||
```
|
||||
|
||||
* keyboardDismissMode 有3个枚举值
|
||||
|
||||
* UIScrollViewKeyboardDismissModeNone:默认值,也就是拖拽时对于键盘没有任何影响。
|
||||
|
||||
* UIScrollViewKeyboardDismissModeOnDrag:(dismisses the keyboard when a drag begins)当刚拖拽的时候就会回收键盘
|
||||
|
||||
* UIScrollViewKeyboardDismissModeInteractive:(the keyboard follows the dragging touch off screen, and may be pulled upward again to cancel the dismiss)当向下滑动的时候键盘会跟随手势一起下滑,当向上滑动的时候键盘也会跟随手势向上滑动而出现。
|
||||
86
Chapter1 - iOS/1.24.md
Normal file
86
Chapter1 - iOS/1.24.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# NSRange 设计之美
|
||||
|
||||
|
||||
|
||||
> typedef struct _NSRange {
|
||||
NSUInteger location;
|
||||
NSUInteger length;
|
||||
} NSRange;
|
||||
|
||||
1、看到官方文档的源代码就知道 NSRange 是个结构体,但是如果是你设计一个这样的数据类型你会怎么办??
|
||||
|
||||
设计成结构体,然后有些属性怎么办?比如为了开发者方便,让你设计出一个办法,让开发者可以很快知道这个结构体的上限是什么?
|
||||
|
||||
苹果就很机智,设计了一个内联函数
|
||||
|
||||
```
|
||||
NS_INLINE NSUInteger NSMaxRange(NSRange range) {
|
||||
return (range.location + range.length);
|
||||
}
|
||||
```
|
||||
|
||||
2、什么是内联函数?
|
||||
|
||||
```
|
||||
NS_INLINE 返回值类型 函数名(参数列表) {
|
||||
//函数实现
|
||||
//return ;
|
||||
}
|
||||
```
|
||||
|
||||
3、内联函数的应用
|
||||
|
||||
比如自定义一个弹窗
|
||||
|
||||
```
|
||||
NS_INLINE void tipWithMessage(NSString *message){
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
UIAlertView *alerView = [[UIAlertView alloc] initWithTitle:@"提示" message:message delegate:nil cancelButtonTitle:nil otherButtonTitles:nil, nil];
|
||||
|
||||
[alerView show];
|
||||
|
||||
[alerView performSelector:@selector(dismissWithClickedButtonIndex:animated:) withObject:@[@0, @1] afterDelay:0.9];
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
4、内联函数的注意事项
|
||||
|
||||
内联函数是以代码膨胀为代价, 仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数内代码的时间相比于函数调用的开销大,那么效率收获会很小。
|
||||
此外每一处调用内联函数的地方都会复制一遍代码,所以会使得程序的体积变大,消耗更多的代码段区域。
|
||||
所以下面的情况不适合使用内联函数:
|
||||
|
||||
* 如果函数体内的代码比较长,使用内联将导致内存消耗比较大
|
||||
* 如果函数体内出现循环,那么执行函数体内代码的时间要比函数的调用开销大
|
||||
|
||||
|
||||
5、FOUNDATION_EXPORT
|
||||
|
||||
查看 NSRange 的代码还会发现一个关键词 **FOUNDATION_EXPORT**,它可以用作定义常量。
|
||||
|
||||
|
||||
FOUNDATION_EXPORT 和 #define 都可用来定义常量。
|
||||
用法
|
||||
|
||||
```
|
||||
//.h
|
||||
FOUNDATION_EXPORT NSString *const NickName;
|
||||
|
||||
//.m
|
||||
NSString *const NickName = @"杭城小刘";
|
||||
```
|
||||
|
||||
那么它和 **#define** 有何区别?
|
||||
|
||||
FOUNDATION_EXPORT 在检测字符串的值是否相等的时候效率更高
|
||||
使用** NickName == MyName** 来判断,而 #define 是用 **[NickName isEqualToString:MyName]** 来判断。
|
||||
|
||||
|
||||
* 本质上 FOUNDATION_EXPORT 是比较指针的自己
|
||||
* \#define 是比较每个字符串是否相等
|
||||
210
Chapter1 - iOS/1.25.md
Normal file
210
Chapter1 - iOS/1.25.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# 复制层(CAReplicatorLayer)
|
||||
|
||||
> 对于下面的效果大家是否有实现思路?
|
||||
>
|
||||
> 有些人可能要说:老夫撸起袖子,敲键盘就是干,不需要手势交互,那么直接用5个**CALayer**,处理不同的位置以及定时器、透明度等等,貌似很简单。
|
||||
>
|
||||
> 不不不,今天要带出来的主题是 **CAReplicatorLayer**
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 1、CAReplicatorLayer
|
||||
|
||||
> /* The replicator layer creates a specified number of copies of its
|
||||
>
|
||||
> - sublayers, each copy potentially having geometric, temporal and
|
||||
> - color transformations applied to it.
|
||||
>
|
||||
> *
|
||||
>
|
||||
> - Note: the CALayer -hitTest: method currently only tests the first
|
||||
> - instance of z replicator layer's sublayers. This may change in the
|
||||
> - future. */
|
||||
|
||||
官方给出的意思就不翻译了,使用场景大致是一个形状、特性差不多的 layer,我们不需要重复创建,可以利用它来实现复制多个 layer ,然后通过 CAReplicatorLayer 的一些属性实现我们的需求。
|
||||
|
||||
|
||||
|
||||
上述效果的代码
|
||||
|
||||
```objective-c
|
||||
//创建复制层,因为我们做的多个音量柱变化的动画都是一样的,所以创建了一个复制层,这个复制层可以对里面的 sublayer 进行复制,所以我们不需要重复创建了
|
||||
|
||||
CAReplicatorLayer *replicatorrLayer = [CAReplicatorLayer layer];
|
||||
replicatorrLayer.frame = CGRectMake(0, 0, self.contentView.frame.size.width, self.contentView.frame.size.height);
|
||||
replicatorrLayer.backgroundColor = [UIColor blackColor].CGColor;
|
||||
self.replicatorrLayer = replicatorrLayer;
|
||||
[self.contentView.layer addSublayer:replicatorrLayer];
|
||||
|
||||
|
||||
//创建音量震动条
|
||||
CALayer *layer = [CALayer layer];
|
||||
layer.backgroundColor = [UIColor whiteColor].CGColor;
|
||||
CGFloat width = 30;
|
||||
CGFloat height = 100;
|
||||
layer.bounds = CGRectMake(0, self.contentView.frame.size.height - height, width, height);
|
||||
layer.anchorPoint = CGPointMake(0, 1);
|
||||
layer.position = CGPointMake(0, self.contentView.frame.size.height);
|
||||
[self.contentView.layer addSublayer:layer];
|
||||
|
||||
//创建音量震动动画
|
||||
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.scale.y"];
|
||||
animation.toValue = @0;
|
||||
animation.duration = 1;
|
||||
animation.repeatCount = MAXFLOAT;
|
||||
animation.autoreverses = YES;
|
||||
[layer addAnimation:animation forKey:nil];
|
||||
|
||||
|
||||
[replicatorrLayer addSublayer:layer];
|
||||
|
||||
//* The number of copies to create, including the source object.
|
||||
replicatorrLayer.instanceCount = 6; //复制 sublayer 的个数,包括创建的第一个sublayer 在内的个数
|
||||
replicatorrLayer.instanceDelay = 0.4; //设置动画延迟执行的时间
|
||||
replicatorrLayer.instanceAlphaOffset = -0.15; //设置透明度递减
|
||||
replicatorrLayer.instanceTransform = CATransform3DMakeTranslation(50, 0, 0);
|
||||
```
|
||||
[源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/复制层应用1-音量柱动画)
|
||||
|
||||
|
||||
|
||||
|
||||
## 例子1
|
||||
|
||||

|
||||
|
||||
这里比较简单了,关键代码
|
||||
|
||||
```objective-c
|
||||
CAReplicatorLayer *replicatorLayer = (CAReplicatorLayer *)self.view.layer;
|
||||
replicatorLayer.instanceCount = 2;
|
||||
replicatorLayer.instanceTransform = CATransform3DMakeRotation(M_PI, 1, 0, 0);
|
||||
replicatorLayer.instanceRedOffset -= 0.1;
|
||||
replicatorLayer.instanceGreenOffset -= 0.1;
|
||||
replicatorLayer.instanceBlueOffset -= 0.1;
|
||||
replicatorLayer.instanceAlphaOffset -= 0.3;
|
||||
```
|
||||
|
||||
- 需要说明是这里我用 storyboard 处理的,因为已经拉好了控件,所以我们没办法将图片直接加到复制层上去。间接做法是将 UIViewController 的 view 的 layer 类型改变为 复制层
|
||||
|
||||
```
|
||||
//该方法返回 UIView 的层
|
||||
//改写 UIView 的层:重写 layerClass 方法
|
||||
+ (Class)layerClass{
|
||||
return [CAReplicatorLayer class];
|
||||
}
|
||||
```
|
||||
[源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/复制层应用2-倒影效果)
|
||||
|
||||
|
||||
## 例子2
|
||||
|
||||

|
||||
|
||||
|
||||
需求分析:
|
||||
|
||||
- 先画图。也就是添加一个滑动手势并监听它。然后强制绘图(self setNeedsDisplay)
|
||||
|
||||
- 添加一个 layer 到 self.layer 上
|
||||
|
||||
- 改变当前 view 的 layer 类型。
|
||||
|
||||
```
|
||||
+ (Class)layerClass{
|
||||
return [CAReplicatorLayer class];
|
||||
}
|
||||
```
|
||||
|
||||
- 设置 CAReplicatorLayer 的 instanceCount 和 instanceDelay 属性
|
||||
|
||||
- 添加了小点,并为小点设置关键帧动画。
|
||||
|
||||
- 重置功能实现靠的是清除 path 上面的 points ,并移除 小点上面的动画
|
||||
|
||||
```
|
||||
#import "ViewControllerView.h"
|
||||
|
||||
@interface ViewControllerView()
|
||||
|
||||
@property (nonatomic, strong) UIBezierPath *path;
|
||||
@property (nonatomic, weak) CALayer *dotLayer;
|
||||
@end
|
||||
|
||||
@implementation ViewControllerView
|
||||
|
||||
+ (Class)layerClass{
|
||||
return [CAReplicatorLayer class];
|
||||
}
|
||||
|
||||
- (void)awakeFromNib{
|
||||
[super awakeFromNib];
|
||||
|
||||
UIPanGestureRecognizer *tapGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(draw:)];
|
||||
[self addGestureRecognizer:tapGesture];
|
||||
self.path = [UIBezierPath bezierPath];
|
||||
|
||||
CALayer *layer = [CALayer layer];
|
||||
layer.frame = CGRectMake(-UIScreen.mainScreen.bounds.size.width, 0, 15, 15);
|
||||
layer.backgroundColor = [UIColor orangeColor].CGColor;
|
||||
layer.cornerRadius = 7.5;
|
||||
self.dotLayer = layer;
|
||||
[self.layer addSublayer:layer];
|
||||
|
||||
CAReplicatorLayer *replicatorLayer = (CAReplicatorLayer *)self.layer;
|
||||
replicatorLayer.instanceCount = 20;
|
||||
replicatorLayer.instanceDelay = 0.25;
|
||||
}
|
||||
|
||||
|
||||
- (void)draw:(UIPanGestureRecognizer *)tap{
|
||||
CGPoint currentPoint = [tap locationInView:self];
|
||||
if (tap.state == UIGestureRecognizerStateBegan) {
|
||||
[self.path moveToPoint:currentPoint];
|
||||
}
|
||||
else if(tap.state == UIGestureRecognizerStateChanged){
|
||||
[self.path addLineToPoint:currentPoint];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)startAnimation{
|
||||
//要实现动画围绕着给定的形状执行,那么需要关键帧动画(类比于Flash概念中的关键帧动画,只需要给定指定的关键帧,其余的帧系统会创建出来。)。关键帧动画的 path 和 values 是互斥的,也就是说如果设置了 values 还设置了 path 那么 path 属性会覆盖 values 属性。
|
||||
|
||||
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
|
||||
animation.keyPath = @"position";
|
||||
animation.path = self.path.CGPath;
|
||||
animation.duration = 5;
|
||||
animation.repeatCount = MAXFLOAT;
|
||||
[self.dotLayer addAnimation:animation forKey:nil];
|
||||
}
|
||||
|
||||
- (void)redraw{
|
||||
//清空路径:移除 path 上面所有的点,然后重绘
|
||||
[self.path removeAllPoints];
|
||||
[self setNeedsDisplay];
|
||||
//移除动画
|
||||
[self.dotLayer removeAllAnimations];
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect{
|
||||
[self.path stroke];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
[源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/复制层应用3-粒子闪烁效果)
|
||||
|
||||
### CALayer 层的动画有2个概念非常重要:AnchorPoint 和 position
|
||||
|
||||
- postion 用来确定 layer 层在父层中的位置
|
||||
|
||||
- anchorPoint 用来确定 layer 身上哪个点会在 position 所指的位置。
|
||||
|
||||
|
||||
|
||||
172
Chapter1 - iOS/1.26.md
Normal file
172
Chapter1 - iOS/1.26.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# CAShapeLayer
|
||||
|
||||
> 一言以蔽之:CAShapeLayer 可以根据贝塞尔曲线描绘出的路径而生成对应的图形
|
||||
|
||||
|
||||
|
||||
## 综合例子
|
||||
|
||||
- 效果图
|
||||
|
||||

|
||||
|
||||
|
||||
- 关键技术点剖析
|
||||
|
||||
- 分析 QQ 粘性动画的关键点就是当手势拖动时候2个圆之间那个形状怎么绘制
|
||||
|
||||
答案:将2个圆的某一时刻之间形成的形状用数学抽象来计算。
|
||||

|
||||
|
||||
|
||||
- 拖动到超过某个范围的时候怎么执行爆炸动画
|
||||
|
||||
UIImageView 可以执行帧动画,类似于 Flash 效果
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 关键代码
|
||||
|
||||
```
|
||||
- (void)pan:(UIPanGestureRecognizer *)pan{
|
||||
//当前移动的偏移量
|
||||
CGPoint transP = [pan translationInView:self];
|
||||
//改变红点的位置
|
||||
//transform并没有修改自身的 center(center 是 layer 的position),只是修改了 frame
|
||||
NSLog(@"偏移量:%@",NSStringFromCGPoint(transP));
|
||||
CGPoint center = self.center;
|
||||
center.x += transP.x;
|
||||
center.y += transP.y;
|
||||
self.center = center;
|
||||
|
||||
//self.transform = CGAffineTransformTranslate(self.transform, transP.x, transP.y);
|
||||
//手势复位:设置坐标原点位上次的坐标
|
||||
[pan setTranslation:CGPointZero inView:self];
|
||||
|
||||
CGFloat distance = [self distanceWith:self.smallCircle bigCircle:self];
|
||||
NSLog(@"%f",distance);
|
||||
|
||||
|
||||
CGFloat smallCircleRadius = self.bounds.size.width * 0.5;
|
||||
smallCircleRadius = smallCircleRadius - distance/10;
|
||||
|
||||
if (smallCircleRadius < 3) {
|
||||
smallCircleRadius = 3;
|
||||
}
|
||||
self.smallCircle.bounds = CGRectMake(0, 0, smallCircleRadius*2, smallCircleRadius*2);
|
||||
self.smallCircle.layer.cornerRadius = smallCircleRadius;
|
||||
|
||||
if (self.smallCircle.hidden == NO) {
|
||||
//返回一个不规则的路径
|
||||
UIBezierPath *path = [self drawTracertWithSmallCircle:self.smallCircle bigCircle:self];
|
||||
//将形状转换为一个形状图层
|
||||
self.shapeLayer.path = path.CGPath;//根据路径生成形状
|
||||
}
|
||||
//创建形状图层
|
||||
[self.superview.layer insertSublayer:self.shapeLayer atIndex:0];
|
||||
|
||||
if (distance > 60) {
|
||||
self.smallCircle.hidden = YES;
|
||||
[self.shapeLayer removeFromSuperlayer];
|
||||
}
|
||||
|
||||
if (pan.state == UIGestureRecognizerStateEnded) {
|
||||
//结束手势
|
||||
if (distance < 60) {
|
||||
[self.shapeLayer removeFromSuperlayer];
|
||||
self.center = self.smallCircle.center;
|
||||
self.smallCircle.hidden = NO;
|
||||
}
|
||||
else{
|
||||
//手势拖拽超过60则播放一个动画
|
||||
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.bounds];
|
||||
|
||||
NSMutableArray *images = [NSMutableArray array];
|
||||
|
||||
for (int i=0; i<8; i++) {
|
||||
NSString *imageName = [NSString stringWithFormat:@"%d",i+1];
|
||||
UIImage *image = [UIImage imageNamed:imageName];
|
||||
[images addObject:image];
|
||||
}
|
||||
imageView.animationImages = images;
|
||||
[imageView setAnimationDuration:1];
|
||||
[imageView startAnimating];
|
||||
[self addSubview:imageView];
|
||||
//动画结束移除本身
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self removeFromSuperview];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (CGFloat )distanceWith:(UIView *)smallCircle bigCircle:(UIView *)bigScirle{
|
||||
CGFloat offsetX = bigScirle.frame.origin.x - smallCircle.frame.origin.x;
|
||||
CGFloat offsetY = bigScirle.frame.origin.y - smallCircle.frame.origin.y;
|
||||
return sqrt( pow(offsetX, 2) + pow(offsetY, 2));
|
||||
}
|
||||
|
||||
//将2个圆运行的变化轨迹用代码模拟
|
||||
- (UIBezierPath *)drawTracertWithSmallCircle:(UIView *)smallCircle bigCircle:(UIView *)bigCircle{
|
||||
|
||||
CGFloat X1 = smallCircle.center.x;
|
||||
CGFloat X2 = bigCircle.center.x;
|
||||
CGFloat Y1 = smallCircle.center.y;
|
||||
CGFloat Y2 = bigCircle.center.y;
|
||||
|
||||
CGFloat r1 = smallCircle.bounds.size.width/2;
|
||||
CGFloat r2 = bigCircle.bounds.size.width/2;
|
||||
|
||||
CGFloat d = [self distanceWith:smallCircle bigCircle:bigCircle];
|
||||
//Ø 代表角度
|
||||
CGFloat SinØ = (X2 - X1)/d;
|
||||
CGFloat CosØ = (Y2 - Y1)/d;
|
||||
|
||||
CGPoint pointA = CGPointMake(X1 - r1*CosØ, Y1 + r1*SinØ);
|
||||
|
||||
CGPoint pointB = CGPointMake(X1 + r1*CosØ, Y1 - r1*SinØ);
|
||||
|
||||
CGPoint pointC = CGPointMake(X2 + r2*CosØ, Y2 - r2*SinØ);
|
||||
|
||||
CGPoint pointD = CGPointMake(X2 - r2*CosØ, Y2 + r2*SinØ);
|
||||
|
||||
CGPoint pointO = CGPointMake(X1 + SinØ *d/2, Y1 + CosØ*d/2);
|
||||
|
||||
CGPoint pointP = CGPointMake(X1 + SinØ *d/2,Y1 + CosØ*d/2 );
|
||||
|
||||
//描述路径
|
||||
UIBezierPath *path = [UIBezierPath bezierPath];
|
||||
|
||||
//AB
|
||||
[path moveToPoint:pointA];
|
||||
[path addLineToPoint:pointB];
|
||||
|
||||
//BC(曲线)
|
||||
[path addQuadCurveToPoint:pointC controlPoint:pointP];
|
||||
|
||||
//CD
|
||||
[path addLineToPoint:pointD];
|
||||
|
||||
//DA(曲线)
|
||||
[path addQuadCurveToPoint:pointA controlPoint:pointO];
|
||||
|
||||
return path;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
完整的代码,[Github地址](https://github.com/FantasticLBP/BlogDemos/tree/master/QQ粘性动画)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
65
Chapter1 - iOS/1.27.md
Normal file
65
Chapter1 - iOS/1.27.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# 仿微博弹簧动画
|
||||
|
||||
> 老玩微博,最近在研究动画,周末抽空写了个发微博的动画
|
||||
|
||||
|
||||
|
||||
# 实现步骤
|
||||
|
||||
- 首先模打出一个控制器
|
||||
- 这个控制器用来显示多个按钮。(按钮是图文上下排列的,所以我们需要自定义按钮的布局样式)
|
||||
- 动画思路:先在界面添加好几个 UIButton,之后给每个 button 添加**y**方向的平移动画 -> 设置一个定时器,每次执行的时候依次取出按钮,将按钮添加一个弹簧动画(**usingSpringWithDamping **)将形变动画恢复原位
|
||||
- 给按钮添加2种事件(按下的事件、点击后抬起的事件)
|
||||
|
||||
## 关键代码
|
||||
|
||||
```
|
||||
//开始时让所有按钮都移动到最底部
|
||||
btn.transform = CGAffineTransformMakeTranslation(0, self.view.bounds.size.height);
|
||||
|
||||
//添加定时器
|
||||
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(update) userInfo:nil repeats:YES];
|
||||
|
||||
- (void)update{
|
||||
if (self.btnIndex == self.btnArray.count) {
|
||||
[self.timer invalidate];
|
||||
return ;
|
||||
}
|
||||
|
||||
VerticalStyleButton *button = self.btnArray[self.btnIndex];
|
||||
//弹簧动画
|
||||
[UIView animateWithDuration:0.3 delay:0.2 usingSpringWithDamping:0.8 initialSpringVelocity:0 options:UIViewAnimationOptionCurveLinear animations:^{
|
||||
button.transform = CGAffineTransformIdentity;
|
||||
} completion:^(BOOL finished) {
|
||||
|
||||
}];
|
||||
self.btnIndex++;
|
||||
}
|
||||
|
||||
- (void)btnClick:(UIButton *)button{
|
||||
[UIView animateWithDuration:0.25 animations:^{
|
||||
button.transform = CGAffineTransformMakeScale(1.2, 1.2);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)btnClick1:(UIButton *)button{
|
||||
[UIView animateWithDuration:0.25 animations:^{
|
||||
button.alpha = 0;
|
||||
button.transform = CGAffineTransformMakeScale(2, 2);
|
||||
}];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
# 效果图
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/微博发帖动画)
|
||||
|
||||
39
Chapter1 - iOS/1.28.md
Normal file
39
Chapter1 - iOS/1.28.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# UILabel 给关键字模糊匹配并高亮
|
||||
|
||||
> 有些情况就是需要查找某个字符串并高亮,但有些需求就是需要全局模糊查找,找到符合的字符串并高亮。造了个小轮子
|
||||
|
||||
###效果图
|
||||

|
||||
|
||||
```
|
||||
#pragma mark -- 设置在一个文本中所有特殊字符的特殊颜色
|
||||
+ (NSMutableAttributedString *)setAllText:(NSString *)allStr andSpcifiStr:(NSString *)keyWords withColor:(UIColor *)color specifiStrFont:(UIFont *)font{
|
||||
NSMutableAttributedString *mutableAttributedStr = [[NSMutableAttributedString alloc] initWithString:allStr];
|
||||
if (color == nil) {
|
||||
color = [UIColor redColor];
|
||||
}
|
||||
if (font == nil) {
|
||||
font = [UIFont systemFontOfSize:17];
|
||||
}
|
||||
|
||||
|
||||
for (NSInteger j=0; j<=keyWords.length-1; j++) {
|
||||
|
||||
NSRange searchRange = NSMakeRange(0, [allStr length]);
|
||||
NSRange range;
|
||||
NSString *singleStr = [keyWords substringWithRange:NSMakeRange(j, 1)];
|
||||
while
|
||||
((range = [allStr rangeOfString:singleStr options:NSLiteralSearch range:searchRange]).location != NSNotFound) {
|
||||
//改变多次搜索时searchRange的位置
|
||||
searchRange = NSMakeRange(NSMaxRange(range), [allStr length] - NSMaxRange(range));
|
||||
//设置富文本
|
||||
[mutableAttributedStr addAttribute:NSForegroundColorAttributeName value:color range:range];
|
||||
[mutableAttributedStr addAttribute:NSFontAttributeName value:font range:range];
|
||||
}
|
||||
}
|
||||
return mutableAttributedStr;
|
||||
}
|
||||
```
|
||||
|
||||
[源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/LBPAttributedStringTools)
|
||||
155
Chapter1 - iOS/1.29.md
Normal file
155
Chapter1 - iOS/1.29.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# JavascriptCore
|
||||
|
||||
|
||||
|
||||
1、JSCore 是基于 webkit 以 C/C++ 实现的一个 js 包装,让 js 和 Native 交互变得更加简单。
|
||||
|
||||
- JScontext
|
||||
JSContext 代表一个 JavaScript 的执行环境的一个实例。所有JavaScript执行都是在上下文内进行。JSContext还用于管理对象的生命周期内 JavaScript 的虚拟机
|
||||
- JSValue
|
||||
JSValue 是用来接收 JSContext 执行后的返回结果。JSValue 可以是 JS 的任意类型(变量、对象、函数...)
|
||||
- JSManagedValue
|
||||
JSManagedValue 是对 JSValue 的封装,可以解决 JS 和 OC 之间循环引用的问题。JSManagedValue 最常用的用法就是安全的从内存堆区里面引用 JSValue 对象.如果 JSValue 存储在内存的堆区的方式是不正确的,很容易造成循环引用,然后导致 JSContext 对象不能正确的释放掉.
|
||||
- JSExport
|
||||
是一个协议,用来将 Native 对象暴露给 JS,这个对象可以指向给自身和别的对象。
|
||||
- JSVirtualMachine
|
||||
管理 JS 对象空间和所需的资源
|
||||
|
||||
2、Native 调用 JS
|
||||
|
||||
- 加载 JS 代码
|
||||
(JSValue *)evaluateScript:(NSString *)script;
|
||||
|
||||
- 调用 JS 方法
|
||||
JSvalue *callBack = self.context[@"sayHi"];
|
||||
[callback callWithArguments:@[@"杭城小刘"]];
|
||||
|
||||
|
||||
3、JS 调用 Native
|
||||
|
||||
- 通过 Block 实现。然后在 JS 中直接调用方法即可。需要注意的是在 Block 内部不要直接使用外部定义的 JScontext 对象或 JSValue ,应该作为参数传递进来,或者通过 + (JSContext *)currentContext; 来获取。否则会造成循环引用、内存无法被正确回收
|
||||
self.context[@"showMessage"] = ^(NSString *message){
|
||||
UIAlertController *alertCtr = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];
|
||||
UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil];
|
||||
[alertCtr addAction:cancel];
|
||||
//注意:方法是在子线程中执行的,需要跟新UI的话,需要切入主线程。
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[weakSealf presentViewController:alertCtr animated:YES completion:nil];
|
||||
});
|
||||
};
|
||||
- 通过 JSExport 协议实现; JS 需要通过 OC 中注入的对象来调方法,那么方法需要在协议中声明,并且在注入的对象中实现;在 webview 加载完成的时候注入实现协议的 Native 对象
|
||||
//声明协议
|
||||
|
||||
@proptocol JSInject<JSExport>
|
||||
- (void)showMessage:(NSString *)message;
|
||||
@end
|
||||
|
||||
//实现相应的协议
|
||||
|
||||
- (void)showMessage:(NSString *)message{
|
||||
UIAlertController *alertCtr = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];
|
||||
UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil];
|
||||
[alertCtr addAction:cancel];
|
||||
//注意:方法是在子线程中执行的,需要跟新UI的话,需要切入主线程。
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[weakSealf presentViewController:alertCtr animated:YES completion:nil];
|
||||
});
|
||||
}
|
||||
|
||||
//注入
|
||||
|
||||
- (void)webViewDidFinishLoad:(UIWebView *)webView
|
||||
{
|
||||
//从webview上获取相应的JSContext。
|
||||
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
|
||||
|
||||
//注入JS需要的“OC”对象
|
||||
self.context[@"Bridge"] = [JSInject new];
|
||||
}
|
||||
|
||||
|
||||
举个🌰
|
||||
|
||||
JS call Native
|
||||
|
||||
//对外要暴露的 native 对象(其中挂载了一些属性和方法)
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <JavaScriptCore/JavaScriptCore.h>
|
||||
|
||||
@protocol PersonInjectExport<JSExport>
|
||||
|
||||
@property (nonatomic, strong) NSString *name;
|
||||
|
||||
@property (nonatomic, strong) NSString *hobby;
|
||||
|
||||
- (id)sayHi;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface PersonInject : NSObject<PersonInjectExport>
|
||||
|
||||
@property (nonatomic, strong) NSString *name;
|
||||
|
||||
@property (nonatomic, strong) NSString *hobby;
|
||||
|
||||
- (id)sayHi;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
// viewcontroller
|
||||
|
||||
- (void)webViewDidFinishLoad:(UIWebView *)webView{
|
||||
self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
|
||||
PersonInject *person = [[PersonInject alloc] init];
|
||||
person.name = @"杭城小刘";
|
||||
person.hobby = @"Coding、Movie、Music、Table tennis、Fit";
|
||||
self.jsContext[@"lbp"] = person;
|
||||
}
|
||||
|
||||
//JS
|
||||
<body>
|
||||
|
||||
嗨。大家好我是
|
||||
<p id="name">***</p>
|
||||
|
||||
<button id="show">那你做个自我介绍吧</button>
|
||||
|
||||
</body>
|
||||
<script>
|
||||
var u = navigator.userAgent
|
||||
var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1
|
||||
var isiOS = u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/) //ios终端
|
||||
|
||||
document.getElementById("show").onclick = function () {
|
||||
if (isiOS) {
|
||||
document.getElementById("name").innerHTML = lbp.name;
|
||||
setTimeout(() => {
|
||||
alert(lbp.sayHi());
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
Native call JS
|
||||
|
||||
//Native
|
||||
- (void)callJS{
|
||||
JSValue *functionName = self.jsContext[@"sum"];
|
||||
NSInteger sum = [[functionName callWithArguments:@[@"2",@"18"]] toInt32];;
|
||||
|
||||
UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"来自JS 的计算" message:[NSString stringWithFormat:@"%zd",sum] preferredStyle:UIAlertControllerStyleAlert];
|
||||
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil];
|
||||
[alertVC addAction:okAction];
|
||||
[self presentViewController:alertVC animated:YES completion:nil];
|
||||
}
|
||||
|
||||
//JS
|
||||
function sum(a ,b){
|
||||
return parseInt(a) + parseInt(b);
|
||||
}
|
||||
|
||||
|
||||
99
Chapter1 - iOS/1.3.md
Normal file
99
Chapter1 - iOS/1.3.md
Normal file
@@ -0,0 +1,99 @@
|
||||
|
||||
|
||||
# loadView
|
||||
|
||||
1. 作用:加载控制器的view
|
||||
|
||||
2. 何时调用:当控制器的view第一次使用的时候就会调用
|
||||
|
||||
3. 使用场景:只要想自定义控制器的view就调用此方法
|
||||
|
||||
访问控制器的View就相当于调用控制器中的view get方法
|
||||
|
||||
```
|
||||
|
||||
-(UIView *)view{
|
||||
if(_view == nil){
|
||||
[self loadView];
|
||||
[self viewDidload];
|
||||
|
||||
}
|
||||
return _view;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
# 控制器加载view的流程
|
||||

|
||||
|
||||
|
||||
* 控制器的init方法底层会调用initWithNibName方法
|
||||
|
||||
MyViewController *vc = [[MyViewController alloc] init];
|
||||
|
||||
注意点:
|
||||
|
||||
* 系统做判断的前提提条件:没有指定nibName;没有自定义loadView方法;控制器以...Controller命名
|
||||
|
||||
* 判断原则:
|
||||
|
||||
* 1、判断下有没有指定nibName,如果指定了就去加载nib
|
||||
|
||||
* 2、判断有没有跟控制器同名的xib,但是xib的名称不带Controller的xib,如果有就去加载
|
||||
|
||||
* 3、如果第二步没有指定,就判断有没有跟控制器类名同名的xib,如果有就去加载
|
||||
|
||||
* 4、如果没有任何xib描述控制器的view,就不加载xib
|
||||
|
||||
## MyViewController加载view的处理
|
||||
|
||||
* 判断有没有指定xibName,如果有就去加载指定的xib
|
||||
|
||||
* 判断有没有跟控制器类名同名的xib,但是名字不带controller
|
||||
|
||||
* 判断有没有跟控制器类名同名的xib,有就去加载
|
||||
|
||||
* 直接创建一个空的xib
|
||||
|
||||
例子
|
||||
|
||||
```
|
||||
//在Appdelegate中
|
||||
ViewController *vc = [[ViewController alloc] init];
|
||||
vc.view.backgroundColkor = [UIColor redColor];
|
||||
self.window.rootViewController = vc;
|
||||
[pself.window makeKeyAndVisable];
|
||||
|
||||
//ViewController
|
||||
-(UIView *)view{
|
||||
if(!_view){
|
||||
[self loadView];
|
||||
[self viewDidLoad];
|
||||
}
|
||||
}
|
||||
|
||||
-(void)loadView{
|
||||
UIView*view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
||||
view.backgroundColor = [UIColor greenColor]; self.view = view;
|
||||
|
||||
}
|
||||
|
||||
-(void)viewDidload{
|
||||
[super viewDidload];
|
||||
self.view.backgroundColor = [UIColor brownColor];
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 请问此时界面颜色是什么?
|
||||
|
||||
可能很多人会回到绿色。其实答案是 红色
|
||||
|
||||
why?在AppDelegate中vc.view.backgroundColor就是调用vc的view的getter方法,在getter方法内部判断_view是否存在,不存在则新建一个UIView,新建view是通过[self loadView]方法创建,创建成功直接调用viewdidload方法;存在则直接返回,所以界面先是绿色,再是棕色最后是红色
|
||||
|
||||
#### 来一个官方解释
|
||||
|
||||

|
||||
|
||||
|
||||
83
Chapter1 - iOS/1.30.md
Normal file
83
Chapter1 - iOS/1.30.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Xcode 小技巧
|
||||
|
||||
1. 快速打开:**Command+Shift+O** 。这个命令可以开启一个小窗格用来快速搜索浏览文件、类、算法以及函数等,且支持模糊搜索,这个命令可以说是日常开发中最常用的一个命令了。
|
||||
|
||||
2. 显示项目导航器:**Command+Shift+J**。使用快速打开命令跳转到对应文件后,如果需要在左侧显示出该文件在项目中的目录结构,只需要键入这个命令,非常方便
|
||||
|
||||
3. 显示编辑历史。如果一行代码写的很好或者很糟糕,不需要专门跑到 diff 工具去查看代码历史。在该行代码处右击,选择**Show Last Change For Line**
|
||||
|
||||
4. 跳转到方法。在使用类或者结构时,我们经常需要快速的跳转到类的某个特定方法。通过快捷键**control+6**再输入方法的头几个字母就可以非常方便的做到这点。
|
||||
|
||||
5. 范围编辑。多光标是个很棒的并且每个高级的编辑器都该有的特训过,快捷键为**Command+Control+E**。将光标移动刀需要编辑的符号,输入快捷键,然后就可以在当前页面全局编辑了。
|
||||
|
||||
6. Xcode 设置代码只在 Debug 下起效的几种方式
|
||||
在日常开发中 Xcode 在 Debug 模式下写很多测试代码,或者引入一些第三方测试用的 .a 和 .framework 动态库,也会通过 CocoaPods 引入一些第三方测试工具或者库;但是不希望这些库在**Release**正式包中被引入,如何做到呢?
|
||||
|
||||
* .h/.m 文件中的测试代码
|
||||
|
||||
Xcode 在 Debug 模式下定义了宏 DEBUG=1 ,所以我们可以在代码中把相关的测试代码写在预编译处理命令 **\#ifdef DEBUG... \#endif** 中间即可,如图所示
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
* 测试用的 .a 和 .framework
|
||||
|
||||
对于拖拽到工程中的 .a .framework 静态库,可以在 **target->Build Settings->Search Paths**这2个选项,分别设置 **Library Search Paths**和**Framework Search Paths**这2个选项。如果我们需要在测试的时候会用到,那么我们可以将 **Debug** 对应的值留下,删掉**Release** 对应的值。这样我们打包 Release 包的时候就不会包含不需要的包。
|
||||
|
||||

|
||||
|
||||
* CocoPods 引入的库
|
||||
对于 CocoPods 方式引入的库,在配置的时候就可以处理掉,比如下面的方式
|
||||
```
|
||||
platform: iOS, '8.0'
|
||||
...
|
||||
pod 'PonyDebugger', :configurations => ['Debug']
|
||||
```
|
||||
|
||||
7. App Store Connect 经常在上架的时候需要开发人员判断是否满足出口合规的证明,每次写都很麻烦,所以可以在工程里面的 plist 里面进行设置。
|
||||
```
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
```
|
||||
|
||||
8. 让 Xcode 折叠代码
|
||||
在 VS Code 或者其他 IDE 里面都具有代码折叠的功能,Xcode 也支持代码折叠功能,但是默认没有开启。所以我们需要做的就是打开代码折叠功能。步骤:打开 Xcode - Preference - Text Editing - 在「Show」模块下面勾选「Code folding ribbon」。这样 Xcode 就具备代码折叠的功能了。
|
||||
快捷键:
|
||||
- command + option + 左右方向键 : 折叠或展开鼠标光标所在位置的代码
|
||||
- command + option + shift + 左右方向键:折叠或展开当前页面全部的方法(函数)
|
||||
|
||||
|
||||
9. 几种设置废弃 Api 的方法
|
||||
|
||||
- __deprecated
|
||||
|
||||
- NS_UNAVAILABLE。`- (instancetype)init NS_UNAVAILABLE;`
|
||||
|
||||
- #define MJRefreshDeprecated(instead) NS_DEPRECATED(2_0, 2_0, 2_0, 2_0, instead)
|
||||
```
|
||||
MJRefreshDeprecated("请使用automaticallyChangeAlpha属性");
|
||||
```
|
||||
|
||||
- DEPRECATED_ATTRIBUTE
|
||||
```
|
||||
@property (nonatomic, strong, readonly) UILabel *dateLabel DEPRECATED_ATTRIBUTE;
|
||||
```
|
||||
|
||||
- DEPRECATED_MSG_ATTRIBUTE
|
||||
```
|
||||
@property (nonatomic, assign) NSStringEncoding stringEncoding DEPRECATED_MSG_ATTRIBUTE("The string encoding is never used. AFHTTPResponseSerializer only validates status codes and content types but does not try to decode the received data in any way.");
|
||||
```
|
||||
|
||||
- @property(nullable, nonatomic, strong) IBOutlet NSLayoutConstraint *IQLayoutGuideConstraint __attribute__((deprecated("Due to change in core-logic of handling distance between textField and keyboard distance, this layout contraint tweak is no longer needed and things will just work out of the box regardless of constraint pinned with safeArea/layoutGuide/superview.")));
|
||||
|
||||
- + (CLLocationDistance)getCurrentLocationDistanceFilter __deprecated_msg("废弃方法(空实现),使用distanceFilter属性替换");
|
||||
|
||||
- + (NSString *)getWeiboAppSupportMaxSDKVersion __attribute__((deprecated));
|
||||
|
||||
- #pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
result = [self sizeWithFont:font constrainedToSize:size lineBreakMode:lineBreakMode];
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
|
||||
30
Chapter1 - iOS/1.31.md
Normal file
30
Chapter1 - iOS/1.31.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# 终端效率
|
||||
|
||||
|
||||
## tree
|
||||
|
||||
如果要在终端查看当前目录的层级结构,不妨了解下**tree**。它可以以树状的形式展示当前的目录结构。
|
||||
|
||||
安装:
|
||||
在终端输入**brew install tree**
|
||||
|
||||
使用:
|
||||
|
||||
在当前目录下,显示树状目录结构。**tree -L 2 -d**。其中 -L 表示遍历的深度,-d 表示只显示目录。
|
||||
|
||||
|
||||
## 合并写法
|
||||
|
||||
之前在终端操作的时候都是老老实实一行行的写代码,最近发现可以合并起来写。比如
|
||||
|
||||
```
|
||||
//写法一
|
||||
cd Desktop
|
||||
mkdir awesome-project
|
||||
cd awesome-project
|
||||
//写法二
|
||||
cd Desktop && mkdir awesome-project && cd awesome-project
|
||||
```
|
||||
|
||||
|
||||
|
||||
303
Chapter1 - iOS/1.32.md
Normal file
303
Chapter1 - iOS/1.32.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# 终极截屏
|
||||
|
||||
- **-(void)snapshotForView:(__kindof UIView *)view;**
|
||||
|
||||
今天新学到这种写法,__kindof 是苹果声明的一个特性,是 Xcode7 出现的新特性。
|
||||
|
||||
- 假如我们想声明一个方法,这个方法的参数必须是一个 UIView 类型的对象,那么我们应该可以写成下面这个样子
|
||||
|
||||
```
|
||||
-(void)snapshotForView:(UIView *)view;
|
||||
```
|
||||
|
||||
|
||||
|
||||
- 那么我们想声明一个方法,这个方法的参数必须是 UIView 及其 UIView 的子类,那么前一种写法就满足不了我们的需求了,这时候引入了 __kindof 方法
|
||||
|
||||
```
|
||||
-(void)snapshotForView:(__kindof UIView *)view;
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**UIWebView 截图**
|
||||
|
||||
对 UIWebView 截图比较简单,renderInContext 这个方法相信大家都不会陌生,这个方法是 CALayer 的一个实例方法,可以用来对大部分 View 进行截图。我们知道,UIWebView 承载内容的其实是作为其子 View 的 UIScrollView,所以对 UIWebView 截图应该对其 scrollView 进行截图。具体的截图方法如下:
|
||||
|
||||
```
|
||||
- (void)snapshotForScrollView:(UIScrollView *)scrollView
|
||||
{
|
||||
// 1. 记录当前 scrollView 的偏移和位置
|
||||
CGPoint currentOffset = scrollView.contentOffset;
|
||||
CGRect currentFrame = scrollView.frame;
|
||||
|
||||
scrollView.contentOffset = CGPointZero;
|
||||
// 2. 将 scrollView 展开为其实际内容的大小
|
||||
scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height);
|
||||
|
||||
// 3. 第三个参数设置为 0 表示设置为屏幕的默认缩放因子
|
||||
UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, YES, 0);
|
||||
[scrollView.layer renderInContext:UIGraphicsGetCurrentContext()];
|
||||
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
// 4. 重新设置 scrollView 的偏移和位置,还原现场
|
||||
scrollView.contentOffset = currentOffset;
|
||||
scrollView.frame = currentFrame;
|
||||
}
|
||||
```
|
||||
|
||||
**WKWebView 截图**
|
||||
|
||||
虽然 WKWebView 里也有 scrollView,但是直接对这个 scrollView 截图得到的是一片空白的,具体原因不明。一番 Google 之后可以看到好些人提到 drawViewHierarchyInRect 方法, 可以看到这个方法是 iOS 7.0 开始引入的。官方文档中描述为:
|
||||
|
||||
> Renders a snapshot of the complete view hierarchy as visible onscreen into the current context.
|
||||
|
||||
注意其中的 **visible onscreen**,也就是将屏幕中可见部分渲染到上下文中,这也解释了为什么对 WKWebView 中的 scrollView 展开为实际内容大小,再调用 drawViewHierarchyInRect 方法总是得到一张不完整的截图(只有屏幕可见区域被正确截到,其他区域为空白)。
|
||||
|
||||
不过,这样倒是给我们提供了一个思路,可以将 WKWebView 按屏幕高度裁成 n 页,然后将 WKWebView 一页一页的往上推,每推一页就调用一次 drawViewHierarchyInRect 将当前屏幕的截图渲染到上下文中,最后调用 UIGraphicsGetImageFromCurrentImageContext 从上下文中获取的图片即为完整截图。
|
||||
|
||||
核心代码如下:
|
||||
|
||||
```
|
||||
- (void)snapshotForWKWebView:(WKWebView *)webView
|
||||
{
|
||||
// 1
|
||||
UIView *snapshotView = [webView snapshotViewAfterScreenUpdates:YES];
|
||||
[webView.superview addSubview:snapshotView];
|
||||
|
||||
// 2
|
||||
CGPoint currentOffset = webView.scrollView.contentOffset;
|
||||
...
|
||||
|
||||
// 3
|
||||
UIView *containerView = [[UIView alloc] initWithFrame:webView.bounds];
|
||||
[webView removeFromSuperview];
|
||||
[containerView addSubview:webView];
|
||||
|
||||
// 4
|
||||
CGSize totalSize = webView.scrollView.contentSize;
|
||||
NSInteger page = ceil(totalSize.height / containerView.bounds.size.height);
|
||||
|
||||
webView.scrollView.contentOffset = CGPointZero;
|
||||
webView.frame = CGRectMake(0, 0, containerView.bounds.size.width, webView.scrollView.contentSize.height);
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(totalSize, YES, UIScreen.mainScreen.scale);
|
||||
[self drawContentPage:0 maxIndex:page completion:^{
|
||||
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
// 8
|
||||
[webView removeFromSuperview];
|
||||
...
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)drawContentPage(NSInteger)index maxIndex:(NSInteger)maxIndex completion:(dispatch_block_t)completion
|
||||
{
|
||||
// 5
|
||||
CGRect splitFrame = CGRectMake(0, index * CGRectGetHeight(containerView.bounds), containerView.bounds.size.width, containerView.frame.size.height);
|
||||
CGRect myFrame = webView.frame;
|
||||
myFrame.origin.y = -(index * containerView.frame.size.height);
|
||||
webView.frame = myFrame;
|
||||
|
||||
// 6
|
||||
[targetView drawViewHierarchyInRect:splitFrame afterScreenUpdates:YES];
|
||||
|
||||
// 7
|
||||
if (index < maxIndex) {
|
||||
[self drawContentPage:index + 1 maxIndex:maxIndex completion:completion];
|
||||
} else {
|
||||
completion();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
代码注意项如下(对应代码注释中的序号):
|
||||
|
||||
1. 为了截图时对 frame 进行操作不会出现闪屏等现象,我们需要盖一个“假”的 webView 到现在的位置上,并将真正的 webView “摘下来”。调用 snapshotViewAfterScreenUpdates 即可得到这样一个“假”的 webView
|
||||
|
||||
|
||||
|
||||
2. 保存真正的 webView 的偏移、位置等信息,以便截图完成之后“还原现场”
|
||||
|
||||
3. 用一个新的视图承载“真正的” webView,这个视图也是绘图所用到的上下文
|
||||
|
||||
4. 将 webView 按照实际内容高度和屏幕高度分成 page 页
|
||||
|
||||
5. 得到每一页的实际位置,并将 webView 往上推到该位置
|
||||
|
||||
6. 调用 drawViewHierarchyInRect 将当前位置的 webView 渲染到上下文中
|
||||
|
||||
7. 如果还未到达最后一页,则递归调用 drawViewHierarchyInRect 方法进行渲染;如果已经渲染完了全部页,则回调通知截图完成
|
||||
|
||||
8. 调用 UIGraphicsGetImageFromCurrentImageContext 方法从当前上下文中获取到完整截图,将第 2 步中保存的信息重新赋予到 webView 上,“还原现场”
|
||||
|
||||
注意:我们的截图方法中有对 webView 的 frame 进行操作,如果其他地方如果有对 frame 进行操作的话,是会影响我们截图的。所以在截图时应该禁用掉其他地方对 frame 的改变,就像这样:
|
||||
|
||||
```
|
||||
- (void)layoutWebView
|
||||
{
|
||||
if (!_isCapturing) {
|
||||
self.wkWebView.frame = [self frameForWebView];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
#import <UIKit/UIKit.h>
|
||||
@class PPSnapshotHandler;
|
||||
|
||||
@protocol PPSnapshotHandlerDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
|
||||
- (void)snapshotHandler:(PPSnapshotHandler *)snapshotHandler didFinish:(UIImage *)captureImage forView:(UIView *)view;
|
||||
|
||||
@end
|
||||
|
||||
@interface PPSnapshotHandler : NSObject
|
||||
|
||||
+ (instancetype)defaultHandler;
|
||||
|
||||
@property (nonatomic, weak) id<PPSnapshotHandlerDelegate> delegate;
|
||||
|
||||
- (void)snapshotForView:(__kindof UIView *)view;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#import "PPSnapshotHandler.h"
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
#define DELAY_TIME_DRAW 0.1
|
||||
|
||||
@interface PPSnapshotHandler () {
|
||||
BOOL _isCapturing;
|
||||
UIView *_captureView;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation PPSnapshotHandler
|
||||
|
||||
+ (instancetype)defaultHandler
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
static PPSnapshotHandler *defaultHandler = nil;
|
||||
dispatch_once(&onceToken, ^{
|
||||
defaultHandler = [[PPSnapshotHandler alloc] init];
|
||||
});
|
||||
return defaultHandler;
|
||||
}
|
||||
|
||||
#pragma mark - public method
|
||||
|
||||
- (void)snapshotForView:(__kindof UIView *)view
|
||||
{
|
||||
if (!view || _isCapturing) {
|
||||
return;
|
||||
}
|
||||
|
||||
_captureView = view;
|
||||
|
||||
if ([view isKindOfClass:[UIScrollView class]]) {
|
||||
[self snapshotForScrollView:(UIScrollView *)view];
|
||||
} else if ([view isKindOfClass:[UIWebView class]]) {
|
||||
UIWebView *webView = (UIWebView *)view;
|
||||
[self snapshotForScrollView:webView.scrollView];
|
||||
} else if ([view isKindOfClass:[WKWebView class]]) {
|
||||
[self snapshotForWKWebView:(WKWebView *)view];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - WKWebView
|
||||
|
||||
- (void)snapshotForWKWebView:(WKWebView *)webView
|
||||
{
|
||||
UIView *snapshotView = [webView snapshotViewAfterScreenUpdates:YES];
|
||||
snapshotView.frame = webView.frame;
|
||||
[webView.superview addSubview:snapshotView];
|
||||
|
||||
CGPoint currentOffset = webView.scrollView.contentOffset;
|
||||
CGRect currentFrame = webView.frame;
|
||||
UIView *currentSuperView = webView.superview;
|
||||
NSUInteger currentIndex = [webView.superview.subviews indexOfObject:webView];
|
||||
|
||||
UIView *containerView = [[UIView alloc] initWithFrame:webView.bounds];
|
||||
[webView removeFromSuperview];
|
||||
[containerView addSubview:webView];
|
||||
|
||||
CGSize totalSize = webView.scrollView.contentSize;
|
||||
NSInteger page = ceil(totalSize.height / containerView.bounds.size.height);
|
||||
|
||||
webView.scrollView.contentOffset = CGPointZero;
|
||||
webView.frame = CGRectMake(0, 0, containerView.bounds.size.width, webView.scrollView.contentSize.height);
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(totalSize, YES, UIScreen.mainScreen.scale);
|
||||
[self drawContentPage:containerView webView:webView index:0 maxIndex:page completion:^{
|
||||
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
[webView removeFromSuperview];
|
||||
[currentSuperView insertSubview:webView atIndex:currentIndex];
|
||||
webView.frame = currentFrame;
|
||||
webView.scrollView.contentOffset = currentOffset;
|
||||
|
||||
[snapshotView removeFromSuperview];
|
||||
|
||||
self->_isCapturing = NO;
|
||||
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(snapshotHandler:didFinish:forView:)]) {
|
||||
[self.delegate snapshotHandler:self didFinish:snapshotImage forView:self->_captureView];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)drawContentPage:(UIView *)targetView webView:(WKWebView *)webView index:(NSInteger)index maxIndex:(NSInteger)maxIndex completion:(dispatch_block_t)completion
|
||||
{
|
||||
CGRect splitFrame = CGRectMake(0, index * CGRectGetHeight(targetView.bounds), targetView.bounds.size.width, targetView.frame.size.height);
|
||||
CGRect myFrame = webView.frame;
|
||||
myFrame.origin.y = -(index * targetView.frame.size.height);
|
||||
webView.frame = myFrame;
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(DELAY_TIME_DRAW * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[targetView drawViewHierarchyInRect:splitFrame afterScreenUpdates:YES];
|
||||
|
||||
if (index < maxIndex) {
|
||||
[self drawContentPage:targetView webView:webView index:index + 1 maxIndex:maxIndex completion:completion];
|
||||
} else {
|
||||
completion();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollView
|
||||
|
||||
- (void)snapshotForScrollView:(UIScrollView *)scrollView
|
||||
{
|
||||
CGPoint currentOffset = scrollView.contentOffset;
|
||||
CGRect currentFrame = scrollView.frame;
|
||||
|
||||
scrollView.contentOffset = CGPointZero;
|
||||
scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height);
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, YES, UIScreen.mainScreen.scale);
|
||||
[scrollView.layer renderInContext:UIGraphicsGetCurrentContext()];
|
||||
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
scrollView.contentOffset = currentOffset;
|
||||
scrollView.frame = currentFrame;
|
||||
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(snapshotHandler:didFinish:forView:)]) {
|
||||
[self.delegate snapshotHandler:self didFinish:snapshotImage forView:_captureView];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
|
||||
28
Chapter1 - iOS/1.33.md
Normal file
28
Chapter1 - iOS/1.33.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# 推送
|
||||
|
||||
> 1、现在 App 开发推送功能,一般都是接入极光推送,那么为什么极光推送就可以实现推送呢?
|
||||
2、极光推送做了哪些事情?与 APNS 怎么交互的?
|
||||
带着这2个问题来看看推送吧
|
||||
|
||||
## 一、推送原理
|
||||

|
||||
|
||||
(这张图转载于网络)
|
||||
|
||||
|
||||
|
||||
说说推送的步骤:
|
||||
1、你的 App 需要推送服务,要向苹果的 APNS 注册推送功能
|
||||
2、当苹果 APNS 推送服务器收到你的注册请求后会返回给你一串 device token
|
||||
3、当应用收到 device token 后,需要将 device token 传给自己的应用服务器(自己公司的服务端)
|
||||
4、当你需要为你的应用推送消息的时候,自己的应用服务器会将消息,以及 device token 打包发送给苹果的 APNS。
|
||||
5、APNS 再将消息推送给你的 手机 App
|
||||
|
||||
|
||||
## 不接入极光推送的话,自己怎么做推送功能
|
||||
|
||||
参考这篇文章:[自己做推送](https://blog.csdn.net/shenjie12345678/article/details/41120637)
|
||||
|
||||
## 所以极光推送逗帮我们做了什么?
|
||||
|
||||
简化了获取 device token 的步骤,我们将申请号的证书上传到极光服务器,程序运行接入极光 SDK ,手机获取 device token, 然后将 device token 上传给极光推送服务器,。
|
||||
55
Chapter1 - iOS/1.34.md
Normal file
55
Chapter1 - iOS/1.34.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# App 评分
|
||||
|
||||
> 经常有这样的需求-引导用户在合适的时机对 App 做出好评。本文就尝试谈一谈这一块的一些知识
|
||||
|
||||
1. 评分的方式
|
||||
可以跳出应用对 App 进行评分,也可以在应用内进行评分(>= iOS 10.3)。
|
||||
|
||||
2. 跳出 App 评分
|
||||
利用系统方法打开 URL(跳到 App store 后跳转到自己 App 的评价页面)
|
||||
```
|
||||
NSString *urlString = [NSString stringWithFormat:@"itms-apps://itunes.apple.com/app/id%@?action=write-review", @"你的App ID"];
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString]];
|
||||
```
|
||||
|
||||
3. 应用内评分
|
||||
iOS 10.3 之后系统为我们评分这个需求引入 **StoreKit**。利用它,我们可以很方便地在应用内对 App 进行快速评分,而不用跳出去。
|
||||
+ 在 App 内部打开 App store并跳转到App 评价页面
|
||||
```
|
||||
#import <StoreKit/StoreKit.h>
|
||||
SKStoreProductViewController *storeVC = [[SKStoreProductViewController alloc] init];
|
||||
storeVC.delegate = self;
|
||||
[storeVC loadProductWithParameters:@{SKStoreProductParameterITunesItemIdentifier:@"1401834682"} completionBlock:^(BOOL result, NSError * _Nullable error) {
|
||||
if (error) {
|
||||
|
||||
}else{
|
||||
[self presentViewController:storeVC animated:YES completion:nil];
|
||||
}
|
||||
}];
|
||||
```
|
||||
+ 在 App 内弹出评分对话框,用户星级评分后可以继续输入文字
|
||||
```
|
||||
if (@available(iOS 10.3, *)) {
|
||||
if([SKStoreReviewController respondsToSelector:@selector(requestReview)]){
|
||||
[SKStoreReviewController requestReview];
|
||||
else{
|
||||
NSString *urlString = [NSString stringWithFormat:@"itms-apps://itunes.apple.com/app/id%@?action=write-review", @"你的App ID"];
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString]];
|
||||
}
|
||||
}else {
|
||||
// Fallback on earlier versions
|
||||
NSString *urlString = [NSString stringWithFormat:@"itms-apps://itunes.apple.com/app/id%@?action=write-review", @"你的App ID"];
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString]];
|
||||
}
|
||||
```
|
||||
|
||||
4. 注意时机哦
|
||||
我们的目的是能得到用户的正反馈,如果在用户刚使用APP时就弹出评分框,可能会给某些用户带来反感,因此,选择一个合适的时机弹出评分很重要,不然适得其反。
|
||||
今天在使用爱奇艺的时候发现他们的弹出场景是这样的。我因为要出门所以下载了一部电影。在会员模式下高速缓存成功后(我很满意)弹出评分按钮。
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
163
Chapter1 - iOS/1.35.md
Normal file
163
Chapter1 - iOS/1.35.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# 一些布局小知识
|
||||
|
||||
1. LaunchScreen 会根据设备大小设置屏幕的显示范围;LaunchImage 则根据提供的启动图片设置App的可见范围
|
||||
2. UITextView 可以设置显示范围
|
||||
textView.textContainerInset = UIEdgeInsetsMake(40, 0, 0, 0);
|
||||
3. UITextView 可以设置像 Word 一样文字环绕在图片四周的效果。其中用到的属性就是exclusionPaths
|
||||
// Default value : empty array An array of UIBezierPath representing the exclusion paths inside the receiver's bounding rect.
|
||||
@property (copy, NS_NONATOMIC_IOSONLY) NSArray<UIBezierPath *> *exclusionPaths NS_AVAILABLE(10_11, 7_0);
|
||||
NSString *str = @“xxx”;//xxx为文字内容
|
||||
textView = [[UITextView alloc] initWithFrame:CGRectMake(10, 20, self.view.frame.size.width-20, self.view.frame.size.height-30)];
|
||||
textView.text = str;
|
||||
[self.view addSubview:textView];
|
||||
|
||||
imageView = [[UIImageView alloc] initWithFrame:CGRectMake(140, 280, 160, 100)]; imageView.backgroundColor = [UIColor orangeColor];
|
||||
imageView.image = [UIImage imageNamed:@"mao.jpg"];
|
||||
[self.view addSubview:imageView];
|
||||
textView.textContainer.exclusionPaths = @[[self translatedBezierPath]];
|
||||
|
||||
- (UIBezierPath *)translatedBezierPath{
|
||||
CGRect imageRect = [textView convertRect:imageView.frame fromView:self.view];
|
||||
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(imageRect.origin.x+5, imageRect.origin.y, imageRect.size.width-5, imageRect.size.height-5)];
|
||||
return bezierPath;
|
||||
}
|
||||
1. 引申学习 CoreText
|
||||
2. 在读很多第方库的时候,经常会看到2个关键词:“IB_DESIGNABLE”和”IBInspectable“。如果你想让你纯代码写的 View 具有可以在 StoryBoard 和 xib 文件可预览,就要在自定义的 UIView 头文件加上 IB_DESIGNABLE
|
||||
3. 如果想让你自定义的 View 的参数可以在 xib 或者 storyboard 上 Attributes inspector 栏目中被看到且可以被修改,那么你需要在每个 property 前面加上 IBInspectable
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
IB_DESIGNABLE
|
||||
@interface DashView : UIView
|
||||
|
||||
@property (nonatomic, copy) void(^TimerBlock)(NSInteger);
|
||||
|
||||
@property (nonatomic, strong) IBInspectable UIColor *color;
|
||||
|
||||
//跃动数字刷新
|
||||
- (void)refreshJumpNOFromNO:(NSString *)startNO toNO:(NSString *)toNO andTime:(NSString *)time;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
4.UITabBarController 设置图片不能过大,不然不能显示
|
||||
|
||||
5.设置导航控制器的 NavigationBar 的 BackgroundImage 且 使用了 UIBarMetericsDefault 会导航控制器的子控制器的 view 的高度会减小 64。只有设置为 UIBarMetricsDefault 的时候给 NavigationBar 设置背景图片才会显示。UIBarMetricsCompact 意味着导航条是透明的
|
||||
|
||||
[self.navigationBar setBackgroundImage:[UIImage imageNamed:@"Report_customreport"] forBarMetrics:UIBarMetricsDefault];
|
||||
|
||||
1. 在 iOS 6及之前的系统上默认都是 NO,在 iOS 7及其以后都是默认为 YES。效果表现为顶部的 NavigationBar 都是有透明度的效果
|
||||
@property(nonatomic,assign,getter=isTranslucent) BOOL translucent NS_AVAILABLE_IOS(3_0) UI_APPEARANCE_SELECTOR; // Default is NO on iOS 6 and earlier. Always YES if barStyle is set to UIBarStyleBlackTranslucent
|
||||
translucent 设置为 YES ,则布局 view 从屏幕的左上角开始计算,如果设置为 NO,那么布局从 NavigationBar 的下面开始布局。
|
||||
2. 总结:需要让导航控制器里面的控制器的 view 从导航栏以下开始布局,有2种方法可以实现。
|
||||
- 设置导航控制器 setTranslucent = NO
|
||||
- 给导航控制器的 NavigationBar 设置背景图片,且 BarMetrics 需要设置为 UIBarMetricsDefault
|
||||
3. + (void)load 和 + (void)initialize 的使用分析
|
||||
- initialize:第一次使用这个类或者它的子类的时候调用
|
||||
- load :这个方法在类加载的时候调用一次。
|
||||
//window 下有一个导航控制器,导航控制器的根控制器是 ViewController ,点击屏幕跳转到 SubViewController(继承自 ViewController)
|
||||
|
||||
//ViewController
|
||||
+ (void)initialize{
|
||||
NSLog(@"%s",__func__);
|
||||
}
|
||||
+ (void)load{
|
||||
NSLog(@"%s",__func__);
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.title = @"test";
|
||||
self.view.backgroundColor = [UIColor whiteColor];
|
||||
}
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
|
||||
SubViewController *vc = [[SubViewController alloc] init];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
//SubViewController
|
||||
+ (void)initialize{
|
||||
[super initialize];
|
||||
NSLog(@"%s",__func__);
|
||||
}
|
||||
|
||||
//这个方法在类加载的时候调用一次。
|
||||
+ (void)load{
|
||||
NSLog(@"%s",__func__);
|
||||
}
|
||||
|
||||
2018-07-19 11:26:04.621740+0800 Test[14617:1049502] +[ViewController load]
|
||||
2018-07-19 11:26:04.622463+0800 Test[14617:1049502] +[SubViewController load]
|
||||
2018-07-19 11:26:04.743541+0800 Test[14617:1049502] +[ViewController initialize]
|
||||
2018-07-19 11:26:07.648425+0800 Test[14617:1049502] +[ViewController initialize]
|
||||
2018-07-19 11:26:07.648610+0800 Test[14617:1049502] +[SubViewController initialize]
|
||||
|
||||
|
||||
结果分析来看,类都被加载了(调用了 load 方法,其中页面显示的是 ViewController 所以它的 initialize 被调用,点击屏幕跳转到 SubViewController,所以 SubViewController 的 initialize 方法会被调用,在调用的时候调用了 super 关键字,调用父类的 initialize 方法)
|
||||
4. UIAppearance appearanceWhenContainedInInstancesOfClasses : 这个方法可以控制让自定义的导航控制器的 appearance 只修改自己需要修改的样式,不至于对于全部的导航控制器的 navigationBar 全部修改。
|
||||
5. UIImage 与 UIImageRenderingMode
|
||||
在 iOS 系统中经常会用到 UIImage 来渲染一些控件,比如 UITabBar 和 UIBarButtonItem
|
||||
在日常开发的时候我们可以为 UITabBar 设置 items 属性。其中可以指定 UITabBar 的 image 和 selectedImage。此时你可以提供2张图片,比如下面的代码
|
||||
[homeBar setImage:[UIImage imageNamed:@"Tab_home"]];
|
||||
[homeBar setSelectedImage:[UIImage imageNamed:@"Tab_home_selected"]];
|
||||
你也可以按照下面的写法
|
||||
[homeBar setImage:[UIImage imageNamed:@"Tab_home"]];
|
||||
[homeBar setSelectedImage:[UIImage imageNamed:@"Tab_home"]];
|
||||
这是因为系统会渲染。如果不为 UIImage 设置渲染模式,系统会在合适的地方根据上下文渲染,比如在这个地方的 UITabBar 就会根据上下文渲染出选中的效果。我们不必要必须设置选中的颜色。
|
||||
当然你也可以指定 UIImage 的渲染模式。下面看看官方文档讲的 UIImage 的渲染模式
|
||||
// Create a version of this image with the specified rendering mode. By default, images have a rendering mode of UIImageRenderingModeAutomatic.
|
||||
- (UIImage *)imageWithRenderingMode:(UIImageRenderingMode)renderingMode NS_AVAILABLE_IOS(7_0);
|
||||
@property(nonatomic, readonly) UIImageRenderingMode renderingMode NS_AVAILABLE_IOS(7_0);
|
||||
|
||||
|
||||
|
||||
/* Images are created with UIImageRenderingModeAutomatic by default. An image with this mode is interpreted as a template image or an original image based on the context in which it is rendered. For example, navigation bars, tab bars, toolbars, and segmented controls automatically treat their foreground images as templates, while image views and web views treat their images as originals. You can use UIImageRenderingModeAlwaysTemplate to force your image to always be rendered as a template or UIImageRenderingModeAlwaysOriginal to force your image to always be rendered as an original.
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, UIImageRenderingMode) {
|
||||
UIImageRenderingModeAutomatic, // Use the default rendering mode for the context where the image is used
|
||||
|
||||
UIImageRenderingModeAlwaysOriginal, // Always draw the original image, without treating it as a template
|
||||
UIImageRenderingModeAlwaysTemplate, // Always draw the image as a template image, ignoring its color information
|
||||
} NS_ENUM_AVAILABLE_IOS(7_0);
|
||||
UIImage 的渲染模式共有3种值可以选择
|
||||
- UIImageRenderingModeAutomatic:根据所使用的环境和绘图上下文自动调整渲染模式
|
||||
- UIImageRenderingModeAlwaysOriginal:始终绘制图片原始状态,不使用 tintColor
|
||||
- UIImageRenderingModeAlwaysTemplate:始终根据tintColor绘制图片,不管图片本身的颜色状态
|
||||
|
||||
6.下面举个例子。在 UITabBarController 设置 tabBar
|
||||
|
||||
[homeBar setImage:[UIImage imageNamed:@"Tab_home"]];
|
||||
[homeBar setSelectedImage:[UIImage imageNamed:@"Tab_home_selected"]];
|
||||
|
||||
|
||||
从 iconfont 网站上面随便选择1个彩色 icon 用来做对比实验
|
||||
|
||||

|
||||
|
||||
|
||||
- 实验1
|
||||
|
||||
[homeBar setImage:[UIImage imageNamed:@"Tab_home"]];
|
||||
[homeBar setSelectedImage:[[UIImage imageNamed:@"Tab_home_selected"] imageWithRenderingMode:UIImageRenderingModeAutomatic]];
|
||||
|
||||

|
||||
|
||||
|
||||
- 实验2
|
||||
|
||||
[homeBar setImage:[UIImage imageNamed:@"Tab_home"]];
|
||||
[homeBar setSelectedImage:[[UIImage imageNamed:@"Tab_home_selected"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]];
|
||||
|
||||

|
||||
|
||||
|
||||
- 实验3
|
||||
[homeBar setImage:[UIImage imageNamed:@"Tab_home"]];
|
||||
[homeBar setSelectedImage:[[UIImage imageNamed:@"Tab_home_selected"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
|
||||
|
||||

|
||||
|
||||
结论:对于 UIImage 来说如果不指定渲染模式的话则默认使用**UIImageRenderingModeAutomatic**,则会根据渲染的环境和上下文进行渲染。如果指定了模式,则根据具体的模式开启渲染。**UIImageRenderingModeAlwaysOriginal:**则绘制图片的原始信息,不使用**tintColor**。**UIImageRenderingModeAlwaysTemplate:**则始终根据**tintColor**绘制图片,忽略图片本身的信息。
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||

|
||||
72
Chapter1 - iOS/1.36.md
Normal file
72
Chapter1 - iOS/1.36.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# iOS 数值计算精度丢失问题
|
||||
|
||||
> 在 iOS 中经常会计算金额和价格,我们有时会定义数据类型为 double 或者 float,这样在做过一些运算后会发现精度丢失了,这显然不是我们想要的结果。今日偶然间看到一篇技术博文,为了记忆,顺道解决我的这个问题,所以记录了下来。
|
||||
|
||||
|
||||
|
||||
### 存在的问题(精度丢失)
|
||||
|
||||
```objective-c
|
||||
float a = 0.01;
|
||||
int b = 99999999;
|
||||
double c = 0.0;
|
||||
c = a*b;
|
||||
NSLog(@"c-%f",c); // c-1000000.000000
|
||||
NSLog(@"c-%.2f",c); // c-1000000.00
|
||||
```
|
||||
|
||||
通过上面的代码我们看到简单的数学运算后对于数据类型不合理的数值进行过运算后精度丢失了,这如果是在我们的 App 中,用户看到自己的金额不正确,那还不吓一跳??
|
||||
|
||||
接下来看看如何做简单的改进
|
||||
|
||||
```objective-c
|
||||
NSString *aString = [NSString stringWithFormat:@"%f",a];
|
||||
NSString *bString = [NSString stringWithFormat:@"%.2f",(double)b];
|
||||
c = [aString doubleValue]*[bString doubleValue];
|
||||
NSLog(@"%f",c); //999999.990000
|
||||
NSLog(@"%.2f",c); //999999.99
|
||||
```
|
||||
|
||||
这样虽然可以达成目的,但是计算的过程比较麻烦,并不是我们想要的解决方案。通过查阅资料得知苹果推出了一个类,专门解决数据计算的精度问题NSDecimalNumber 。
|
||||
|
||||
|
||||
|
||||
### NSDecimalNumber 为数据精度应用而生
|
||||
|
||||
|
||||
|
||||
NSDecimalNumber 是 NSNumber 的子类,专门负责精度计算。提供了完善的初始化方案,对于头疼的精度计算问题(金额)它提供了便利的解决方案(加、减、乘、除、次方运算并且可以给计算出的结果设置明显的精度方案(四舍五入、取上、取下等等))。NSDecimalNumberHandler 可以对计算出的结果做一些策略,比如舍入的模式、数据溢出、除0等异常情况的处理规则。
|
||||
|
||||
我们来说说上面的问题吧,引入了 NSDecimalNumber,解决上面的问题就不费吹灰之力了。
|
||||
|
||||
```objective-c
|
||||
NSString *decimalNumberMutiplyWithString(NSString *multiplierValue, NSString *multiplicandvalue){
|
||||
NSDecimalNumber *multiplierNumber = [NSDecimalNumber decimalNumberWithString:multiplierValue];
|
||||
|
||||
NSDecimalNumber *multiplicandNumber = [NSDecimalNumber decimalNumberWithString:multiplicandvalue];
|
||||
NSDecimalNumber *result = [multiplierNumber decimalNumberByMultiplyingBy:multiplicandNumber];
|
||||
return [result stringValue];
|
||||
}
|
||||
|
||||
NSDecimalNumber *price = [NSDecimalNumber decimalNumberWithMantissa:18992 exponent:-3 isNegative:NO];
|
||||
NSDecimalNumber *price2 = [NSDecimalNumber decimalNumberWithString:@"18.992"];
|
||||
NSLog(@"price:%@",[price stringValue]); //999999.990000
|
||||
NSLog(@"price2:%@",[price2 stringValue]); //999999.99
|
||||
|
||||
|
||||
//设置计算的精度(小数点位数、舍入规则)
|
||||
NSDecimalNumberHandler *roundPlain = [NSDecimalNumberHandler
|
||||
decimalNumberHandlerWithRoundingMode:NSRoundPlain
|
||||
scale:1
|
||||
raiseOnExactness:NO
|
||||
raiseOnOverflow:NO
|
||||
raiseOnUnderflow:NO
|
||||
raiseOnDivideByZero:YES];
|
||||
|
||||
NSDecimalNumber *resultDecimal = [multiplierNumber decimalNumberByMultiplyingBy:multiplicandNumber withBehavior:roundPlain];
|
||||
```
|
||||
|
||||
### 参考文章
|
||||
|
||||
[文章1](https://www.jianshu.com/p/25d24a184016)、[文章2](https://www.jianshu.com/p/ea4da259a062)
|
||||
|
||||
34
Chapter1 - iOS/1.37.md
Normal file
34
Chapter1 - iOS/1.37.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# 数组、集合、字典与 hash、isEqual 方法的关联
|
||||
|
||||
1. NSArray 允许重复添加元素,添加元素的时候不查重,所以不会调用上面2个方法。在移出元素的时候会依次遍历数组内的元素,每个元素调用 **isEqual** 方法(remove 方法传入的元素作为参数),所有返回真值的元素都会被移除。在字典中不涉及 hash 方法。
|
||||
|
||||
2. NSSet 不允许重复添加元素,所以在添加新元素的时候,该元素的 **hash** 方法会被调用,若集合中不存在与此元素 hash 相同的元素,则它会被直接加入集合,不调用 **isEqual** 方法;若存在,则依次调用该集合每个元素的 **isEqual** 方法,返回真值则判等,不加入,处理结合,若返回 false, 则判定集合内不存在该元素,将其加入。
|
||||
|
||||
3. 集合中移除元素时,首先调用它的 **hash方法**,若集合中存在与其 **hash值** 相等的元素,则调用该元素的 **isEqual方法**,若返回真值则判断,进行移除;若不存在,则会依次调用集合中每个元素的 **isEqual方法**,只要找到一个返回真值的元素,就进行移除,并结束整个过程(所有这样会有其它满足 isEqual 方法但却漏掉未被删除的元素)。调用 contains 方法时类似
|
||||
|
||||
4. 因此如果自定义对象被加入到集合或作为字典的 key 时,需要同时重写 isEqual 方法和 hash 方法,这样,若集合存在某元素,则调用它的 contains 和 remove 方法时,可以在 O(1) 完成查询,否则需要 O(n) 完成。
|
||||
|
||||
5. 需要注意的是,NSDictionary 的 key、value 都说对象类型即可,但是被设为 key 的对象需要遵循 NSCopying 协议。
|
||||
|
||||
6. hash 方法出现的目的是:当我们从数组中查找元素时,需要依次遍历数组中的元素,时间复杂度为 O(n),为了解决效率问题,引入了 Hash Table 方法。当添加元素的时候,为每个元素设置了 hash 值,这样当下次查找的时候就直接通过 hash 值找到对应的位置,时间复杂度为 O(1)。设计一个合理的 hash 算法的指标是对于每个参数,其返回的 hash 值唯一。
|
||||
|
||||
7. 查阅资料可知,一个推荐的自定义对象的 **hash** 算法是将关键属性的 hash 值,按照位或运算
|
||||
|
||||
```
|
||||
@interface Person : NSObject
|
||||
@property (nonatomic, strong) NSString *name;
|
||||
@property (nonatomic, strong) NSDate *birthday;
|
||||
@end
|
||||
|
||||
|
||||
- (NSUInteger)hash{
|
||||
return [self.name hash] ^ [self.birthday hash];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
8. NSObject 类中的 equal 方法的判断是包括内存地址的,也就是说,NSObject 若想判断2个对象相等,那么这2个对象的内存地址必须相等
|
||||
|
||||
|
||||
|
||||
942
Chapter1 - iOS/1.38.md
Normal file
942
Chapter1 - iOS/1.38.md
Normal file
@@ -0,0 +1,942 @@
|
||||
# RunLoop 对象
|
||||
|
||||
先附上一张总结的非常棒的RunLoop图
|
||||
|
||||

|
||||
|
||||
iOS 中有2套 API 可以访问和使用 RunLoop。分别是
|
||||
|
||||
- Foundation:NSRunLoop
|
||||
|
||||
- CoreFoundation:CFRunLoopRef
|
||||
|
||||
```
|
||||
//Foundation
|
||||
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
|
||||
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
|
||||
|
||||
//Core Foundation
|
||||
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
|
||||
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
|
||||
```
|
||||
|
||||
|
||||
NSRunLoop 是对 CFRunLoopRef 的一层 OC 包装,所以要了解 RunLoop 的内部结果,就需要了解 [CFRunLoopRef](https://legacy.gitbook.com/book/fantasticlbp/knowledge-kit/edit#)。
|
||||
|
||||
- 每条线程都有与之一一对应的 RunLoop 对象
|
||||
- 主线程的 RunLoop 已经自动创建好了,子线程的 RunLoop 需要主动创建
|
||||
- RunLoop 在第一次获取时创建,在线程结束时消失
|
||||
|
||||
### RunLoop 相关的5个类
|
||||
|
||||
- CFRunLoopRef
|
||||
- CFRunLoopModeRef
|
||||
- CFRunLoopSourceRef
|
||||
- CFRunLoopTimerRef
|
||||
- CFRunLoopObserverRef
|
||||
|
||||
|
||||
### CFRunLoopModeRef 代表 RunLoop 的运行模式
|
||||
|
||||
- 一个 RunLoop 包含若干个 Mode,每个 Mode 包含若干个 Source/Timer/Observer
|
||||
- 每次 RunLoop 启动,只能指定一个 Mode,这个 Mode 被叫做 CurrentMode
|
||||
- 如果需要切换 Mode,只能退出 RunLoop,则以一个 Mode 进入
|
||||
- 这样做的目的是为了分隔开不同组的 Source/Timer/Observer 互不影响
|
||||
|
||||
系统默认注册了5个Mode
|
||||
|
||||
- kCFRunLoopDefaultMode:App 的默认 Mode,通常主线程是在这个 Mode 下运行
|
||||
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
|
||||
- UIInitializationRunLoopMode: 在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用
|
||||
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
|
||||
- kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
|
||||
|
||||
|
||||
## CFRunLoopSourceRef 事件源(输入源)
|
||||
|
||||
- 早期的分法:
|
||||
- Ported-Based Source
|
||||
- Custom Input Source
|
||||
- Cocoa Perform Selector Source
|
||||
- 现在的分法
|
||||
- Source0:非基于 port 的,用户主动触发的事件
|
||||
- Source1: 基于 port的,通过内核在线程间相互发送消息
|
||||
|
||||
|
||||
## CFRunLoopTimerRef 是基于时间的触发器
|
||||
|
||||
- 基本上说就是 NSTimer,它会收到 RunLoopMode 的影响
|
||||
- GCD 的 timer 不受 RunLoopMode 的影响
|
||||
|
||||
## - CFRunLoopObserverRef 观察者,监听 RunLoop 状态的变化
|
||||
|
||||
```objective-c
|
||||
/* Run Loop Observer Activities */
|
||||
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
|
||||
kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop
|
||||
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 NSTimer
|
||||
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
|
||||
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
|
||||
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
|
||||
kCFRunLoopExit = (1UL << 7), // 退出 RunLoop
|
||||
kCFRunLoopAllActivities = 0x0FFFFFFFU
|
||||
};
|
||||
```
|
||||
|
||||
添加 Observer
|
||||
|
||||
```objective-c
|
||||
//1、获得当前线程下的 RunLoop
|
||||
CFRunLoopRef runloop = CFRunLoopGetCurrent();
|
||||
//2、为 RunLoop 创建观察者
|
||||
CFRunLoopObserverRef obersver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
|
||||
|
||||
|
||||
});
|
||||
//3、为当前的 RunLoop 添加观察者
|
||||
CFRunLoopAddObserver(runloop, obersver, kCFRunLoopDefaultMode);
|
||||
//4、在 CoreFoundation 框架中, create、copy、retain 过的对象都必须在最后 release
|
||||
CFRelease(obersver);
|
||||
```
|
||||
|
||||
|
||||
|
||||
## NSTimer 经常会不准确,原因是什么?
|
||||
|
||||
NSTimer 在创建的时候经常会指派到特定的 NSRunLoopMode 中去,举个例子,默认创建的NSTimer 是被添加到 NSRunLoopDefaultMode 中去,当你的页面上有 UIScrollView 或者子类的时如果被拖动了,当前 RunLoop 的 NSRunloopMode 会从 NSDefaultRunLoopMode 转变为 UITrackingRunLoopMode 。遇到这种情况你需要精确的 NSTimer 的话,在创建好 NSTimer 之后,设置 RunLoopMod 为 NSRunLoopCommonModes
|
||||
|
||||
```objective-c
|
||||
NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(show) userInfo:nil repeats:YES];
|
||||
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
|
||||
```
|
||||
|
||||
NSTimer 会受 NSRunLoopMode 影响,GCD 的 timer 则不会。
|
||||
|
||||
```objective-c
|
||||
#import "ViewController.h"
|
||||
|
||||
@interface ViewController ()
|
||||
@property (nonatomic, strong) dispatch_source_t timer;
|
||||
@end
|
||||
|
||||
|
||||
@implementation ViewController
|
||||
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
|
||||
/*
|
||||
只在默认状态下执行的 NSTimer
|
||||
[NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
|
||||
NSLog(@"我在执行了");
|
||||
}];
|
||||
*/
|
||||
|
||||
/*
|
||||
指定 NSRunLoopMode 的 NSTimer
|
||||
NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(show) userInfo:nil repeats:YES];
|
||||
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
|
||||
*/
|
||||
|
||||
/*
|
||||
GCD 的单位是 纳秒.
|
||||
使用 GCD 创建的 timer 正常创建后不会执行,因为创建后设置了指定的时间后触发,所以当代码运行到最后一行的时候,Timer 还没执行,就被销毁了。所以我们必须设置一个属性去保存它。
|
||||
*/
|
||||
//1、创建队列
|
||||
dispatch_queue_t queue = dispatch_get_main_queue();
|
||||
//2、创建 timer
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
|
||||
self.timer = timer;
|
||||
//3、设置 timer 的参数:精准度、时间间隔
|
||||
//第三个参数为 GCD timer 的精准度
|
||||
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
|
||||
//4、为 Timer 设置任务
|
||||
dispatch_source_set_event_handler(timer, ^{
|
||||
NSLog(@"%@",[NSRunLoop currentRunLoop]);
|
||||
});
|
||||
//5、执行任务
|
||||
dispatch_resume(timer);
|
||||
}
|
||||
|
||||
- (void)show{
|
||||
NSLog(@"shw-%@",[NSThread currentThread]);
|
||||
NSLog(@"%@",[NSRunLoop currentRunLoop]);
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
## 监听 RunLoop
|
||||
|
||||
```objective-c
|
||||
//给 RunLoop 添加监听者
|
||||
- (void)testRunLoopObserver{
|
||||
|
||||
//创建监听者
|
||||
// CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopAllActivities, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>)
|
||||
/*
|
||||
创建监听对象
|
||||
参数1:分配内存空间
|
||||
参数2:要监听的状态 kCFRunLoopAllActivities :所有状态
|
||||
参数3:是否要持续监听
|
||||
参数4:优先级
|
||||
参数5:回调
|
||||
*/
|
||||
CFRunLoopObserverRef oberver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
|
||||
switch (activity) {
|
||||
case kCFRunLoopEntry:
|
||||
NSLog(@"RunLoop 闪亮登场");
|
||||
break;
|
||||
case kCFRunLoopBeforeTimers:
|
||||
NSLog(@"RunLoop 大哥要处理 Timer 了");
|
||||
break;
|
||||
case kCFRunLoopBeforeSources:
|
||||
//Source 有2种。Source0:非基于 port 的,用户主动触发的事件。Source1:基于 port,通过内核和其它线程互相发送消息
|
||||
NSLog(@"RunLoop 大哥要处理 Source 了");
|
||||
break;
|
||||
case kCFRunLoopBeforeWaiting:
|
||||
NSLog(@"RunLoop 大哥没事干要睡觉了");
|
||||
break;
|
||||
case kCFRunLoopAfterWaiting:
|
||||
NSLog(@"");
|
||||
NSLog(@"RunLoop 大哥终于等到有缘人了,要醒来开始干活了");
|
||||
break;
|
||||
case kCFRunLoopExit:
|
||||
NSLog(@"RunLoop 大哥要退出离开了");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
/*
|
||||
参数1:要监听哪个RunLoop
|
||||
参数2:监听者
|
||||
参数3:要监听 RunLoop 在哪种运行模式下的状态
|
||||
*/
|
||||
CFRunLoopAddObserver(CFRunLoopGetCurrent(), oberver, kCFRunLoopDefaultMode);
|
||||
//CoreFoundation 的内存管理:凡是带有 Create、Copy、Retain等字眼的函数创建出来的对象都需要在最后调用 release
|
||||
CFRelease(oberver);
|
||||
[NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(wakeupRunLoop) userInfo:nil repeats:YES];
|
||||
}
|
||||
|
||||
|
||||
//等到 RunLoop 休眠后,5秒钟叫醒 RunLoop
|
||||
- (void)wakeupRunLoop{
|
||||
NSLog(@"%s",__func__);
|
||||
}
|
||||
/*
|
||||
2018-08-01 11:23:49.401626+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:49.401950+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:49.402326+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:49.402509+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:49.402721+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:49.402855+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:49.403080+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
|
||||
2018-08-01 11:23:49.459238+0800 RunLoop[38148:1994974]
|
||||
2018-08-01 11:23:49.459512+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
|
||||
2018-08-01 11:23:49.459740+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:49.459932+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:49.460431+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:49.460607+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:49.460775+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
|
||||
2018-08-01 11:23:49.880631+0800 RunLoop[38148:1994974]
|
||||
2018-08-01 11:23:49.880867+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
|
||||
2018-08-01 11:23:49.881530+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:49.881699+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:49.881870+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
|
||||
2018-08-01 11:23:54.402263+0800 RunLoop[38148:1994974]
|
||||
2018-08-01 11:23:54.402562+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
|
||||
2018-08-01 11:23:54.402773+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop]
|
||||
2018-08-01 11:23:54.403081+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:54.403245+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:54.403476+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
|
||||
2018-08-01 11:23:59.402151+0800 RunLoop[38148:1994974]
|
||||
2018-08-01 11:23:59.402511+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
|
||||
2018-08-01 11:23:59.402687+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop]
|
||||
2018-08-01 11:23:59.402913+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:59.403037+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:59.403156+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
|
||||
*/
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
上个实验是在主线程对 RunLoop 进行的监听,但是由于是主线程是由系统创建的,所以系统也创建了对应的主 RunLoop,所以我们看不到 RunLoop 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟
|
||||
|
||||
```objective-c
|
||||
- (void)testRunLoopObserverOnSubThread{
|
||||
|
||||
//创建并发队列
|
||||
dispatch_queue_t queue = dispatch_queue_create("com.lbp.testRunLoopOnSubThread", DISPATCH_QUEUE_CONCURRENT);
|
||||
//开启子线程
|
||||
dispatch_async(queue, ^{
|
||||
|
||||
//1、获得当前线程下的 RunLoop
|
||||
CFRunLoopRef runloop = CFRunLoopGetCurrent();
|
||||
|
||||
|
||||
//2、为 RunLoop 创建观察者
|
||||
CFRunLoopObserverRef obersver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
|
||||
switch (activity) {
|
||||
case kCFRunLoopEntry:
|
||||
NSLog(@"RunLoop 闪亮登场");
|
||||
break;
|
||||
case kCFRunLoopBeforeTimers:
|
||||
NSLog(@"RunLoop 大哥要处理 Timer 了");
|
||||
break;
|
||||
case kCFRunLoopBeforeSources:
|
||||
//Source 有2种。Source0:非基于 port 的,用户主动触发的事件。Source1:基于 port,通过内核和其它线程互相发送消息
|
||||
NSLog(@"RunLoop 大哥要处理 Source 了");
|
||||
break;
|
||||
case kCFRunLoopBeforeWaiting:
|
||||
NSLog(@"RunLoop 大哥没事干要睡觉了");
|
||||
break;
|
||||
case kCFRunLoopAfterWaiting:
|
||||
NSLog(@"");
|
||||
NSLog(@"RunLoop 大哥终于等到有缘人了,要醒来开始干活了");
|
||||
break;
|
||||
case kCFRunLoopExit:
|
||||
NSLog(@"RunLoop 大哥要退出离开了");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
//为了运行 RunLoop 必须触发事件
|
||||
[NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(wakeUpRunLoopOnSubThread) userInfo:nil repeats:NO];
|
||||
//3、为当前的 RunLoop 添加观察者
|
||||
CFRunLoopAddObserver(runloop, obersver, kCFRunLoopDefaultMode);
|
||||
//4、在 CoreFoundation 框架中, create、copy、retain 过的对象都必须在最后 release
|
||||
CFRelease(obersver);
|
||||
//5、在非主线程创建的 RunLoop 必须触发运行
|
||||
[[NSRunLoop currentRunLoop] run];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
- (void)wakeUpRunLoopOnSubThread{
|
||||
NSLog(@"%s",__func__);
|
||||
}
|
||||
/*
|
||||
2018-08-01 14:23:06.453282+0800 RunLoop[2376:115968] RunLoop 闪亮登场
|
||||
2018-08-01 14:23:06.453608+0800 RunLoop[2376:115968] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 14:23:06.453781+0800 RunLoop[2376:115968] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 14:23:06.453982+0800 RunLoop[2376:115968] RunLoop 大哥没事干要睡觉了
|
||||
2018-08-01 14:23:08.458237+0800 RunLoop[2376:115968]
|
||||
2018-08-01 14:23:08.458658+0800 RunLoop[2376:115968] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
|
||||
2018-08-01 14:23:08.458894+0800 RunLoop[2376:115968] -[ViewController wakeUpRunLoopOnSubThread]
|
||||
2018-08-01 14:23:08.459082+0800 RunLoop[2376:115968] RunLoop 大哥要退出离开了
|
||||
*/
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## RunLoop 内部运行原理
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- 图上左上角的 Input source 是早期 RunLoop 的分法,现在分法为:Source0 和 Source1。
|
||||
- Source0:非基于 port 的,用户主动触发的事件。
|
||||
- Source1:基于 port,通过内核和其它线程互相发送消息
|
||||
- RunLoop 我们不能自己手动创建,而是可以通过 [NSRunLoop currentRunLoop] 方法获取,类似于懒加载。系统底层的做法是在全局维护了一个字典,字典的 key 和 value 分别是当前的线程和线程对应的 RunLoop,如果新开辟的线程没有对应的 RunLoop,系统则为其创建 RunLoop,并将其写入字典(线程、为其创建的 RunLoop)
|
||||
|
||||
## RunLoopMode 的概念
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 底层实现
|
||||
|
||||
|
||||
|
||||
内部就是 do-while 的循环,在这个循环内部不断处理各种任务(Timer、Source、Observer)
|
||||
|
||||
我们来看看苹果官方开源的 [CFRunLoop.c 文件](https://legacy.gitbook.com/book/fantasticlbp/knowledge-kit/edit#)。看几个关键函数的实现猜测下 RunLoop 的内部原理
|
||||
|
||||
以下的代码都有注释说明
|
||||
|
||||
**__CFRunLoopModeIsEmpty**
|
||||
|
||||
此函数的作用就是判断这个 Mode 下面有没有 source0、source1、timer,只要存在就说明当前 Mode 不是空的,同时看看这个 Mode 是不是属于当前的 RunLoop
|
||||
|
||||
|
||||
|
||||
```objective-c
|
||||
// expects rl and rlm locked
|
||||
static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) {
|
||||
CHECK_FOR_FORK();
|
||||
if (NULL == rlm) return true;
|
||||
#if DEPLOYMENT_TARGET_WINDOWS
|
||||
if (0 != rlm->_msgQMask) return false;
|
||||
#endif
|
||||
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
|
||||
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) return false; // represents the libdispatch main queue
|
||||
// 判断时候有没有_sources0
|
||||
if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false;
|
||||
// 判断时候有没有_sources1
|
||||
if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false;
|
||||
// 判断时候有没有_timers
|
||||
if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false;
|
||||
|
||||
|
||||
struct _block_item *item = rl->_blocks_head;
|
||||
while (item) {
|
||||
struct _block_item *curr = item;
|
||||
item = item->_next;
|
||||
Boolean doit = false;
|
||||
if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) {
|
||||
doit = CFEqual(curr->_mode, rlm->_name) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name));
|
||||
} else {
|
||||
doit = CFSetContainsValue((CFSetRef)curr->_mode, rlm->_name) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name));
|
||||
}
|
||||
if (doit) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
**CFRunLoopRun、CFRunLoopRunInMode**
|
||||
|
||||
1、2个函数的作用分别是让 RunLoop 跑在 KCFRunLoopDefaultMode 下和特定的 Mode 下
|
||||
|
||||
2、2个函数本质上都是调用 CFRunLoopRunSpecific
|
||||
|
||||
```objective-c
|
||||
// 用DefaultMode启动
|
||||
void CFRunLoopRun(void) { /* DOES CALLOUT */
|
||||
int32_t result;
|
||||
do {
|
||||
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
|
||||
CHECK_FOR_FORK();
|
||||
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
|
||||
}
|
||||
|
||||
|
||||
// 用指定的Mode启动,允许设置RunLoop超时时间
|
||||
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
|
||||
CHECK_FOR_FORK();
|
||||
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
**CFRunLoopRunSpecific**
|
||||
|
||||
参数1: RunLoop 对象。参数2:运行 Mode 名称。参数3:超时时间。参数4:主_CFRunLoopRun 会用到
|
||||
|
||||
```objective-c
|
||||
// RunLoop的实现
|
||||
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
|
||||
CHECK_FOR_FORK();
|
||||
if (__CFRunLoopIsDeallocating(rl))
|
||||
return kCFRunLoopRunFinished;
|
||||
__CFRunLoopLock(rl);
|
||||
|
||||
|
||||
// 根据modeName找到对应mode
|
||||
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
|
||||
|
||||
|
||||
// 判断mode里没有source/timer, 没有直接返回。
|
||||
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
|
||||
Boolean did = false;
|
||||
if (currentMode)
|
||||
__CFRunLoopModeUnlock(currentMode);
|
||||
__CFRunLoopUnlock(rl);
|
||||
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
|
||||
}
|
||||
|
||||
|
||||
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
|
||||
|
||||
|
||||
CFRunLoopModeRef previousMode = rl->_currentMode;
|
||||
rl->_currentMode = currentMode;
|
||||
int32_t result = kCFRunLoopRunFinished;
|
||||
|
||||
|
||||
if (currentMode->_observerMask & kCFRunLoopEntry )
|
||||
// 1. 通知 Observers: RunLoop 即将进入 loop
|
||||
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
|
||||
// 进入loop
|
||||
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
|
||||
|
||||
|
||||
if (currentMode->_observerMask & kCFRunLoopExit )
|
||||
// 10.通知 Observers: RunLoop 即将退出。
|
||||
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
|
||||
|
||||
|
||||
__CFRunLoopModeUnlock(currentMode);
|
||||
__CFRunLoopPopPerRunData(rl, previousPerRun);
|
||||
rl->_currentMode = previousMode;
|
||||
__CFRunLoopUnlock(rl);
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
**__CFRunLoopDoObserver**
|
||||
|
||||
|
||||
|
||||
调用 Observer 回调
|
||||
|
||||
联想给 RunLoop 添加观察者,监听 RunLoop 状态。
|
||||
|
||||
```objective-c
|
||||
static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) { /* DOES CALLOUT */
|
||||
CHECK_FOR_FORK();
|
||||
|
||||
|
||||
CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) : 0;
|
||||
if (cnt < 1) return;
|
||||
|
||||
|
||||
/* Fire the observers */
|
||||
STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024) ? cnt : 1);
|
||||
CFRunLoopObserverRef *collectedObservers = (cnt <= 1024) ? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef));
|
||||
CFIndex obs_cnt = 0;
|
||||
//遍历 rlm-> _observers,将元素放到 collectedObservers 数组中
|
||||
for (CFIndex idx = 0; idx < cnt; idx++) {
|
||||
CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);
|
||||
if (0 != (rlo->_activities & activity) && __CFIsValid(rlo) && !__CFRunLoopObserverIsFiring(rlo)) {
|
||||
collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo);
|
||||
}
|
||||
}
|
||||
__CFRunLoopModeUnlock(rlm);
|
||||
__CFRunLoopUnlock(rl);
|
||||
for (CFIndex idx = 0; idx < obs_cnt; idx++) {
|
||||
CFRunLoopObserverRef rlo = collectedObservers[idx];
|
||||
__CFRunLoopObserverLock(rlo);
|
||||
if (__CFIsValid(rlo)) {
|
||||
Boolean doInvalidate = !__CFRunLoopObserverRepeats(rlo);
|
||||
__CFRunLoopObserverSetFiring(rlo);
|
||||
__CFRunLoopObserverUnlock(rlo);
|
||||
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, activity, rlo->_context.info);
|
||||
if (doInvalidate) {
|
||||
CFRunLoopObserverInvalidate(rlo);
|
||||
}
|
||||
__CFRunLoopObserverUnsetFiring(rlo);
|
||||
} else {
|
||||
__CFRunLoopObserverUnlock(rlo);
|
||||
}
|
||||
CFRelease(rlo);
|
||||
}
|
||||
__CFRunLoopLock(rl);
|
||||
__CFRunLoopModeLock(rlm);
|
||||
|
||||
|
||||
if (collectedObservers != buffer) free(collectedObservers);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
**__CFRunLoopRun**
|
||||
|
||||
```objective-c
|
||||
/* rl, rlm are locked on entrance and exit */
|
||||
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
|
||||
|
||||
|
||||
uint64_t startTSR = mach_absolute_time();
|
||||
|
||||
|
||||
if (__CFRunLoopIsStopped(rl)) {
|
||||
__CFRunLoopUnsetStopped(rl);
|
||||
return kCFRunLoopRunStopped;
|
||||
} else if (rlm->_stopped) {
|
||||
rlm->_stopped = false;
|
||||
return kCFRunLoopRunStopped;
|
||||
}
|
||||
|
||||
|
||||
mach_port_name_t dispatchPort = MACH_PORT_NULL;
|
||||
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
|
||||
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
|
||||
|
||||
|
||||
#if USE_DISPATCH_SOURCE_FOR_TIMERS
|
||||
mach_port_name_t modeQueuePort = MACH_PORT_NULL;
|
||||
if (rlm->_queue) {
|
||||
modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
|
||||
if (!modeQueuePort) {
|
||||
CRASH("Unable to get port for run loop mode queue (%d)", -1);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
dispatch_source_t timeout_timer = NULL;
|
||||
struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
|
||||
if (seconds <= 0.0) { // instant timeout
|
||||
seconds = 0.0;
|
||||
timeout_context->termTSR = 0ULL;
|
||||
} else if (seconds <= TIMER_INTERVAL_LIMIT) {
|
||||
dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
|
||||
timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
|
||||
dispatch_retain(timeout_timer);
|
||||
timeout_context->ds = timeout_timer;
|
||||
timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
|
||||
timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
|
||||
dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
|
||||
dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
|
||||
dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
|
||||
uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
|
||||
dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
|
||||
dispatch_resume(timeout_timer);
|
||||
} else {
|
||||
// 设置RunLoop超时时间
|
||||
seconds = 9999999999.0;
|
||||
timeout_context->termTSR = UINT64_MAX;
|
||||
}
|
||||
|
||||
|
||||
Boolean didDispatchPortLastTime = true;
|
||||
int32_t retVal = 0;
|
||||
do {
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED;
|
||||
voucher_t voucherCopy = NULL;
|
||||
#endif
|
||||
uint8_t msg_buffer[3 * 1024];
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
mach_msg_header_t *msg = NULL;
|
||||
mach_port_t livePort = MACH_PORT_NULL;
|
||||
#elif DEPLOYMENT_TARGET_WINDOWS
|
||||
HANDLE livePort = NULL;
|
||||
Boolean windowsMessageReceived = false;
|
||||
#endif
|
||||
__CFPortSet waitSet = rlm->_portSet;
|
||||
|
||||
|
||||
__CFRunLoopUnsetIgnoreWakeUps(rl);
|
||||
|
||||
|
||||
if (rlm->_observerMask & kCFRunLoopBeforeTimers)
|
||||
// 2. 通知 Observers: RunLoop 即将触发 Timer 回调
|
||||
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
|
||||
if (rlm->_observerMask & kCFRunLoopBeforeSources)
|
||||
// 3. 通知 Observers: RunLoop 即将触发 Source 回调
|
||||
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
|
||||
// 执行被加入的block
|
||||
__CFRunLoopDoBlocks(rl, rlm);
|
||||
|
||||
|
||||
// 4. RunLoop 触发 Source0 (非port) 回调
|
||||
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
|
||||
if (sourceHandledThisLoop) {
|
||||
// 执行被加入的block
|
||||
__CFRunLoopDoBlocks(rl, rlm);
|
||||
}
|
||||
|
||||
|
||||
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
|
||||
|
||||
|
||||
// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息
|
||||
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
msg = (mach_msg_header_t *)msg_buffer;
|
||||
|
||||
|
||||
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
|
||||
goto handle_msg;
|
||||
}
|
||||
#elif DEPLOYMENT_TARGET_WINDOWS
|
||||
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
|
||||
goto handle_msg;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
didDispatchPortLastTime = false;
|
||||
|
||||
|
||||
// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)
|
||||
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
|
||||
__CFRunLoopSetSleeping(rl);
|
||||
// do not do any user callouts after this point (after notifying of sleeping)
|
||||
|
||||
|
||||
// Must push the local-to-this-activation ports in on every loop
|
||||
// iteration, as this mode could be run re-entrantly and we don't
|
||||
// want these ports to get serviced.
|
||||
|
||||
|
||||
__CFPortSetInsert(dispatchPort, waitSet);
|
||||
|
||||
|
||||
__CFRunLoopModeUnlock(rlm);
|
||||
__CFRunLoopUnlock(rl);
|
||||
|
||||
|
||||
CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
|
||||
|
||||
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
#if USE_DISPATCH_SOURCE_FOR_TIMERS
|
||||
do {
|
||||
if (kCFUseCollectableAllocator) {
|
||||
// objc_clear_stack(0);
|
||||
// <rdar://problem/16393959>
|
||||
memset(msg_buffer, 0, sizeof(msg_buffer));
|
||||
}
|
||||
msg = (mach_msg_header_t *)msg_buffer;
|
||||
|
||||
|
||||
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
|
||||
|
||||
|
||||
if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
|
||||
// Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
|
||||
while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
|
||||
if (rlm->_timerFired) {
|
||||
// Leave livePort as the queue port, and service timers below
|
||||
rlm->_timerFired = false;
|
||||
break;
|
||||
} else {
|
||||
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
|
||||
}
|
||||
} else {
|
||||
// Go ahead and leave the inner loop.
|
||||
break;
|
||||
}
|
||||
} while (1);
|
||||
#else
|
||||
if (kCFUseCollectableAllocator) {
|
||||
// objc_clear_stack(0);
|
||||
// <rdar://problem/16393959>
|
||||
memset(msg_buffer, 0, sizeof(msg_buffer));
|
||||
}
|
||||
msg = (mach_msg_header_t *)msg_buffer;
|
||||
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
|
||||
#elif DEPLOYMENT_TARGET_WINDOWS
|
||||
// Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
|
||||
__CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
|
||||
#endif
|
||||
|
||||
|
||||
__CFRunLoopLock(rl);
|
||||
__CFRunLoopModeLock(rlm);
|
||||
|
||||
|
||||
rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
|
||||
|
||||
|
||||
// Must remove the local-to-this-activation ports in on every loop
|
||||
// iteration, as this mode could be run re-entrantly and we don't
|
||||
// want these ports to get serviced. Also, we don't want them left
|
||||
// in there if this function returns.
|
||||
|
||||
|
||||
__CFPortSetRemove(dispatchPort, waitSet);
|
||||
|
||||
|
||||
__CFRunLoopSetIgnoreWakeUps(rl);
|
||||
|
||||
|
||||
// user callouts now OK again
|
||||
__CFRunLoopUnsetSleeping(rl);
|
||||
|
||||
|
||||
// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了
|
||||
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
|
||||
// 处理消息
|
||||
handle_msg:;
|
||||
__CFRunLoopSetIgnoreWakeUps(rl);
|
||||
|
||||
|
||||
#if DEPLOYMENT_TARGET_WINDOWS
|
||||
if (windowsMessageReceived) {
|
||||
// These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
|
||||
__CFRunLoopModeUnlock(rlm);
|
||||
__CFRunLoopUnlock(rl);
|
||||
|
||||
|
||||
if (rlm->_msgPump) {
|
||||
rlm->_msgPump();
|
||||
} else {
|
||||
MSG msg;
|
||||
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
__CFRunLoopLock(rl);
|
||||
__CFRunLoopModeLock(rlm);
|
||||
sourceHandledThisLoop = true;
|
||||
|
||||
|
||||
// To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
|
||||
// Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
|
||||
// NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
|
||||
__CFRunLoopSetSleeping(rl);
|
||||
__CFRunLoopModeUnlock(rlm);
|
||||
__CFRunLoopUnlock(rl);
|
||||
|
||||
|
||||
__CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);
|
||||
|
||||
|
||||
__CFRunLoopLock(rl);
|
||||
__CFRunLoopModeLock(rlm);
|
||||
__CFRunLoopUnsetSleeping(rl);
|
||||
// If we have a new live port then it will be handled below as normal
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
if (MACH_PORT_NULL == livePort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_NOTHING();
|
||||
// handle nothing
|
||||
} else if (livePort == rl->_wakeUpPort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
|
||||
// do nothing on Mac OS
|
||||
#if DEPLOYMENT_TARGET_WINDOWS
|
||||
// Always reset the wake up port, or risk spinning forever
|
||||
ResetEvent(rl->_wakeUpPort);
|
||||
#endif
|
||||
}
|
||||
#if USE_DISPATCH_SOURCE_FOR_TIMERS
|
||||
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_TIMER();
|
||||
|
||||
|
||||
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
|
||||
// Re-arm the next timer, because we apparently fired early
|
||||
__CFArmNextTimerInMode(rlm, rl);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if USE_MK_TIMER_TOO
|
||||
// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调
|
||||
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_TIMER();
|
||||
// On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
|
||||
// In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
|
||||
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
|
||||
// Re-arm the next timer
|
||||
__CFArmNextTimerInMode(rlm, rl);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// 9.2 如果有dispatch到main_queue的block,执行block
|
||||
else if (livePort == dispatchPort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
|
||||
__CFRunLoopModeUnlock(rlm);
|
||||
__CFRunLoopUnlock(rl);
|
||||
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
|
||||
#if DEPLOYMENT_TARGET_WINDOWS
|
||||
void *msg = 0;
|
||||
#endif
|
||||
/**/
|
||||
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
|
||||
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
|
||||
__CFRunLoopLock(rl);
|
||||
__CFRunLoopModeLock(rlm);
|
||||
sourceHandledThisLoop = true;
|
||||
didDispatchPortLastTime = true;
|
||||
}
|
||||
// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
|
||||
else {
|
||||
CFRUNLOOP_WAKEUP_FOR_SOURCE();
|
||||
|
||||
|
||||
// If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again.
|
||||
voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);
|
||||
|
||||
|
||||
/**/
|
||||
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
|
||||
if (rls) {
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
mach_msg_header_t *reply = NULL;
|
||||
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
|
||||
if (NULL != reply) {
|
||||
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
|
||||
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
|
||||
}
|
||||
#elif DEPLOYMENT_TARGET_WINDOWS
|
||||
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
// Restore the previous voucher
|
||||
_CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
|
||||
|
||||
|
||||
}
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
|
||||
#endif
|
||||
// 执行加入到Loop的block
|
||||
__CFRunLoopDoBlocks(rl, rlm);
|
||||
|
||||
|
||||
|
||||
|
||||
if (sourceHandledThisLoop && stopAfterHandle) {
|
||||
// 进入loop时参数说处理完事件就返回
|
||||
retVal = kCFRunLoopRunHandledSource;
|
||||
} else if (timeout_context->termTSR < mach_absolute_time()) {
|
||||
// 超出传入参数标记的超时时间了
|
||||
retVal = kCFRunLoopRunTimedOut;
|
||||
} else if (__CFRunLoopIsStopped(rl)) {
|
||||
__CFRunLoopUnsetStopped(rl);
|
||||
// 被外部调用者强制停止了
|
||||
retVal = kCFRunLoopRunStopped;
|
||||
} else if (rlm->_stopped) {
|
||||
rlm->_stopped = false;
|
||||
retVal = kCFRunLoopRunStopped;
|
||||
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
|
||||
// source/timer一个都没有
|
||||
retVal = kCFRunLoopRunFinished;
|
||||
}
|
||||
|
||||
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
voucher_mach_msg_revert(voucherState);
|
||||
os_release(voucherCopy);
|
||||
#endif
|
||||
// 如果没超时,mode里没空,loop也没被停止,那继续loop
|
||||
} while (0 == retVal);
|
||||
|
||||
|
||||
if (timeout_timer) {
|
||||
dispatch_source_cancel(timeout_timer);
|
||||
dispatch_release(timeout_timer);
|
||||
} else {
|
||||
free(timeout_context);
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
```
|
||||
|
||||
770
Chapter1 - iOS/1.39.md
Normal file
770
Chapter1 - iOS/1.39.md
Normal file
@@ -0,0 +1,770 @@
|
||||
# 监听 RunLoop
|
||||
```Objective-C
|
||||
//给 RunLoop 添加监听者
|
||||
- (void)testRunLoopObserver{
|
||||
|
||||
//创建监听者
|
||||
// CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopAllActivities, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>)
|
||||
/*
|
||||
创建监听对象
|
||||
参数1:分配内存空间
|
||||
参数2:要监听的状态 kCFRunLoopAllActivities :所有状态
|
||||
参数3:是否要持续监听
|
||||
参数4:优先级
|
||||
参数5:回调
|
||||
*/
|
||||
CFRunLoopObserverRef oberver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
|
||||
switch (activity) {
|
||||
case kCFRunLoopEntry:
|
||||
NSLog(@"RunLoop 闪亮登场");
|
||||
break;
|
||||
case kCFRunLoopBeforeTimers:
|
||||
NSLog(@"RunLoop 大哥要处理 Timer 了");
|
||||
break;
|
||||
case kCFRunLoopBeforeSources:
|
||||
//Source 有2种。Source0:非基于 port 的,用户主动触发的事件。Source1:基于 port,通过内核和其它线程互相发送消息
|
||||
NSLog(@"RunLoop 大哥要处理 Source 了");
|
||||
break;
|
||||
case kCFRunLoopBeforeWaiting:
|
||||
NSLog(@"RunLoop 大哥没事干要睡觉了");
|
||||
break;
|
||||
case kCFRunLoopAfterWaiting:
|
||||
NSLog(@"");
|
||||
NSLog(@"RunLoop 大哥终于等到有缘人了,要醒来开始干活了");
|
||||
break;
|
||||
case kCFRunLoopExit:
|
||||
NSLog(@"RunLoop 大哥要退出离开了");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
/*
|
||||
参数1:要监听哪个RunLoop
|
||||
参数2:监听者
|
||||
参数3:要监听 RunLoop 在哪种运行模式下的状态
|
||||
*/
|
||||
CFRunLoopAddObserver(CFRunLoopGetCurrent(), oberver, kCFRunLoopDefaultMode);
|
||||
//CoreFoundation 的内存管理:凡是带有 Create、Copy、Retain等字眼的函数创建出来的对象都需要在最后调用 release
|
||||
CFRelease(oberver);
|
||||
[NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(wakeupRunLoop) userInfo:nil repeats:YES];
|
||||
}
|
||||
|
||||
|
||||
//等到 RunLoop 休眠后,5秒钟叫醒 RunLoop
|
||||
- (void)wakeupRunLoop{
|
||||
NSLog(@"%s",__func__);
|
||||
}
|
||||
/*
|
||||
2018-08-01 11:23:49.401626+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:49.401950+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:49.402326+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:49.402509+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:49.402721+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:49.402855+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:49.403080+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
|
||||
2018-08-01 11:23:49.459238+0800 RunLoop[38148:1994974]
|
||||
2018-08-01 11:23:49.459512+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
|
||||
2018-08-01 11:23:49.459740+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:49.459932+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:49.460431+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:49.460607+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:49.460775+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
|
||||
2018-08-01 11:23:49.880631+0800 RunLoop[38148:1994974]
|
||||
2018-08-01 11:23:49.880867+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
|
||||
2018-08-01 11:23:49.881530+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:49.881699+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:49.881870+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
|
||||
2018-08-01 11:23:54.402263+0800 RunLoop[38148:1994974]
|
||||
2018-08-01 11:23:54.402562+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
|
||||
2018-08-01 11:23:54.402773+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop]
|
||||
2018-08-01 11:23:54.403081+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:54.403245+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:54.403476+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
|
||||
2018-08-01 11:23:59.402151+0800 RunLoop[38148:1994974]
|
||||
2018-08-01 11:23:59.402511+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
|
||||
2018-08-01 11:23:59.402687+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop]
|
||||
2018-08-01 11:23:59.402913+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 11:23:59.403037+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 11:23:59.403156+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
上个实验是在主线程对 RunLoop 进行的监听,但是由于是主线程是由系统创建的,所以系统也创建了对应的主 RunLoop,所以我们看不到 RunLoop 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟
|
||||
|
||||
|
||||
|
||||
```objective-c
|
||||
- (void)testRunLoopObserverOnSubThread{
|
||||
|
||||
//创建并发队列
|
||||
dispatch_queue_t queue = dispatch_queue_create("com.lbp.testRunLoopOnSubThread", DISPATCH_QUEUE_CONCURRENT);
|
||||
//开启子线程
|
||||
dispatch_async(queue, ^{
|
||||
|
||||
//1、获得当前线程下的 RunLoop
|
||||
CFRunLoopRef runloop = CFRunLoopGetCurrent();
|
||||
|
||||
|
||||
//2、为 RunLoop 创建观察者
|
||||
CFRunLoopObserverRef obersver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
|
||||
switch (activity) {
|
||||
case kCFRunLoopEntry:
|
||||
NSLog(@"RunLoop 闪亮登场");
|
||||
break;
|
||||
case kCFRunLoopBeforeTimers:
|
||||
NSLog(@"RunLoop 大哥要处理 Timer 了");
|
||||
break;
|
||||
case kCFRunLoopBeforeSources:
|
||||
//Source 有2种。Source0:非基于 port 的,用户主动触发的事件。Source1:基于 port,通过内核和其它线程互相发送消息
|
||||
NSLog(@"RunLoop 大哥要处理 Source 了");
|
||||
break;
|
||||
case kCFRunLoopBeforeWaiting:
|
||||
NSLog(@"RunLoop 大哥没事干要睡觉了");
|
||||
break;
|
||||
case kCFRunLoopAfterWaiting:
|
||||
NSLog(@"");
|
||||
NSLog(@"RunLoop 大哥终于等到有缘人了,要醒来开始干活了");
|
||||
break;
|
||||
case kCFRunLoopExit:
|
||||
NSLog(@"RunLoop 大哥要退出离开了");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
//为了运行 RunLoop 必须触发事件
|
||||
[NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(wakeUpRunLoopOnSubThread) userInfo:nil repeats:NO];
|
||||
//3、为当前的 RunLoop 添加观察者
|
||||
CFRunLoopAddObserver(runloop, obersver, kCFRunLoopDefaultMode);
|
||||
//4、在 CoreFoundation 框架中, create、copy、retain 过的对象都必须在最后 release
|
||||
CFRelease(obersver);
|
||||
//5、在非主线程创建的 RunLoop 必须触发运行
|
||||
[[NSRunLoop currentRunLoop] run];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
- (void)wakeUpRunLoopOnSubThread{
|
||||
NSLog(@"%s",__func__);
|
||||
}
|
||||
/*
|
||||
2018-08-01 14:23:06.453282+0800 RunLoop[2376:115968] RunLoop 闪亮登场
|
||||
2018-08-01 14:23:06.453608+0800 RunLoop[2376:115968] RunLoop 大哥要处理 Timer 了
|
||||
2018-08-01 14:23:06.453781+0800 RunLoop[2376:115968] RunLoop 大哥要处理 Source 了
|
||||
2018-08-01 14:23:06.453982+0800 RunLoop[2376:115968] RunLoop 大哥没事干要睡觉了
|
||||
2018-08-01 14:23:08.458237+0800 RunLoop[2376:115968]
|
||||
2018-08-01 14:23:08.458658+0800 RunLoop[2376:115968] RunLoop 大哥终于等到有缘人了,要醒来开始干活了
|
||||
2018-08-01 14:23:08.458894+0800 RunLoop[2376:115968] -[ViewController wakeUpRunLoopOnSubThread]
|
||||
2018-08-01 14:23:08.459082+0800 RunLoop[2376:115968] RunLoop 大哥要退出离开了
|
||||
*/
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## RunLoop 内部运行原理
|
||||
|
||||

|
||||
|
||||
- 图上左上角的 Input source 是早期 RunLoop 的分法,现在分法为:Source0 和 Source1。
|
||||
- Source0:非基于 port 的,用户主动触发的事件。
|
||||
- Source1:基于 port,通过内核和其它线程互相发送消息
|
||||
- RunLoop 我们不能自己手动创建,而是可以通过 [NSRunLoop currentRunLoop] 方法获取,类似于懒加载。系统底层的做法是在全局维护了一个字典,字典的 key 和 value 分别是当前的线程和线程对应的 RunLoop,如果新开辟的线程没有对应的 RunLoop,系统则为其创建 RunLoop,并将其写入字典(线程、为其创建的 RunLoop)
|
||||
|
||||
|
||||
运行流程图
|
||||
|
||||

|
||||
|
||||
运行流程说明
|
||||
|
||||

|
||||
|
||||
## RunLoopMode 的概念
|
||||

|
||||
|
||||
|
||||
|
||||
## 底层实现
|
||||
内部就是 do-while 的循环,在这个循环内部不断处理各种任务(Timer、Source、Observer)
|
||||
|
||||
我们来看看苹果官方开源的 [CFRunLoop.c 文件](https://legacy.gitbook.com/book/fantasticlbp/knowledge-kit/edit#)。看几个关键函数的实现猜测下 RunLoop 的内部原理
|
||||
|
||||
以下的代码都有注释说明
|
||||
|
||||
**__CFRunLoopModeIsEmpty**
|
||||
|
||||
此函数的作用就是判断这个 Mode 下面有没有 source0、source1、timer,只要存在就说明当前 Mode 不是空的,同时看看这个 Mode 是不是属于当前的 RunLoop
|
||||
|
||||
|
||||
|
||||
```objective-c
|
||||
// expects rl and rlm locked
|
||||
static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) {
|
||||
CHECK_FOR_FORK();
|
||||
if (NULL == rlm) return true;
|
||||
#if DEPLOYMENT_TARGET_WINDOWS
|
||||
if (0 != rlm->_msgQMask) return false;
|
||||
#endif
|
||||
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
|
||||
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) return false; // represents the libdispatch main queue
|
||||
// 判断时候有没有_sources0
|
||||
if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false;
|
||||
// 判断时候有没有_sources1
|
||||
if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false;
|
||||
// 判断时候有没有_timers
|
||||
if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false;
|
||||
|
||||
|
||||
struct _block_item *item = rl->_blocks_head;
|
||||
while (item) {
|
||||
struct _block_item *curr = item;
|
||||
item = item->_next;
|
||||
Boolean doit = false;
|
||||
if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) {
|
||||
doit = CFEqual(curr->_mode, rlm->_name) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name));
|
||||
} else {
|
||||
doit = CFSetContainsValue((CFSetRef)curr->_mode, rlm->_name) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name));
|
||||
}
|
||||
if (doit) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
**CFRunLoopRun、CFRunLoopRunInMode**
|
||||
|
||||
1、2个函数的作用分别是让 RunLoop 跑在 KCFRunLoopDefaultMode 下和特定的 Mode 下
|
||||
|
||||
2、2个函数本质上都是调用 CFRunLoopRunSpecific
|
||||
|
||||
```objective-c
|
||||
// 用DefaultMode启动
|
||||
void CFRunLoopRun(void) { /* DOES CALLOUT */
|
||||
int32_t result;
|
||||
do {
|
||||
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
|
||||
CHECK_FOR_FORK();
|
||||
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
|
||||
}
|
||||
|
||||
|
||||
// 用指定的Mode启动,允许设置RunLoop超时时间
|
||||
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
|
||||
CHECK_FOR_FORK();
|
||||
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
**CFRunLoopRunSpecific**
|
||||
|
||||
参数1: RunLoop 对象。参数2:运行 Mode 名称。参数3:超时时间。参数4:主_CFRunLoopRun 会用到
|
||||
|
||||
```objective-c
|
||||
// RunLoop的实现
|
||||
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
|
||||
CHECK_FOR_FORK();
|
||||
if (__CFRunLoopIsDeallocating(rl))
|
||||
return kCFRunLoopRunFinished;
|
||||
__CFRunLoopLock(rl);
|
||||
|
||||
|
||||
// 根据modeName找到对应mode
|
||||
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
|
||||
|
||||
|
||||
// 判断mode里没有source/timer, 没有直接返回。
|
||||
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
|
||||
Boolean did = false;
|
||||
if (currentMode)
|
||||
__CFRunLoopModeUnlock(currentMode);
|
||||
__CFRunLoopUnlock(rl);
|
||||
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
|
||||
}
|
||||
|
||||
|
||||
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
|
||||
|
||||
|
||||
CFRunLoopModeRef previousMode = rl->_currentMode;
|
||||
rl->_currentMode = currentMode;
|
||||
int32_t result = kCFRunLoopRunFinished;
|
||||
|
||||
|
||||
if (currentMode->_observerMask & kCFRunLoopEntry )
|
||||
// 1. 通知 Observers: RunLoop 即将进入 loop
|
||||
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
|
||||
// 进入loop
|
||||
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
|
||||
|
||||
|
||||
if (currentMode->_observerMask & kCFRunLoopExit )
|
||||
// 10.通知 Observers: RunLoop 即将退出。
|
||||
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
|
||||
|
||||
|
||||
__CFRunLoopModeUnlock(currentMode);
|
||||
__CFRunLoopPopPerRunData(rl, previousPerRun);
|
||||
rl->_currentMode = previousMode;
|
||||
__CFRunLoopUnlock(rl);
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
**__CFRunLoopDoObserver**
|
||||
|
||||
|
||||
|
||||
调用 Observer 回调
|
||||
|
||||
联想给 RunLoop 添加观察者,监听 RunLoop 状态。
|
||||
|
||||
```objective-c
|
||||
static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) { /* DOES CALLOUT */
|
||||
CHECK_FOR_FORK();
|
||||
|
||||
|
||||
CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) : 0;
|
||||
if (cnt < 1) return;
|
||||
|
||||
|
||||
/* Fire the observers */
|
||||
STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024) ? cnt : 1);
|
||||
CFRunLoopObserverRef *collectedObservers = (cnt <= 1024) ? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef));
|
||||
CFIndex obs_cnt = 0;
|
||||
//遍历 rlm-> _observers,将元素放到 collectedObservers 数组中
|
||||
for (CFIndex idx = 0; idx < cnt; idx++) {
|
||||
CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);
|
||||
if (0 != (rlo->_activities & activity) && __CFIsValid(rlo) && !__CFRunLoopObserverIsFiring(rlo)) {
|
||||
collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo);
|
||||
}
|
||||
}
|
||||
__CFRunLoopModeUnlock(rlm);
|
||||
__CFRunLoopUnlock(rl);
|
||||
for (CFIndex idx = 0; idx < obs_cnt; idx++) {
|
||||
CFRunLoopObserverRef rlo = collectedObservers[idx];
|
||||
__CFRunLoopObserverLock(rlo);
|
||||
if (__CFIsValid(rlo)) {
|
||||
Boolean doInvalidate = !__CFRunLoopObserverRepeats(rlo);
|
||||
__CFRunLoopObserverSetFiring(rlo);
|
||||
__CFRunLoopObserverUnlock(rlo);
|
||||
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, activity, rlo->_context.info);
|
||||
if (doInvalidate) {
|
||||
CFRunLoopObserverInvalidate(rlo);
|
||||
}
|
||||
__CFRunLoopObserverUnsetFiring(rlo);
|
||||
} else {
|
||||
__CFRunLoopObserverUnlock(rlo);
|
||||
}
|
||||
CFRelease(rlo);
|
||||
}
|
||||
__CFRunLoopLock(rl);
|
||||
__CFRunLoopModeLock(rlm);
|
||||
|
||||
|
||||
if (collectedObservers != buffer) free(collectedObservers);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
**__CFRunLoopRun**
|
||||
|
||||
```objective-c
|
||||
/* rl, rlm are locked on entrance and exit */
|
||||
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
|
||||
|
||||
|
||||
uint64_t startTSR = mach_absolute_time();
|
||||
|
||||
|
||||
if (__CFRunLoopIsStopped(rl)) {
|
||||
__CFRunLoopUnsetStopped(rl);
|
||||
return kCFRunLoopRunStopped;
|
||||
} else if (rlm->_stopped) {
|
||||
rlm->_stopped = false;
|
||||
return kCFRunLoopRunStopped;
|
||||
}
|
||||
|
||||
|
||||
mach_port_name_t dispatchPort = MACH_PORT_NULL;
|
||||
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
|
||||
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
|
||||
|
||||
|
||||
#if USE_DISPATCH_SOURCE_FOR_TIMERS
|
||||
mach_port_name_t modeQueuePort = MACH_PORT_NULL;
|
||||
if (rlm->_queue) {
|
||||
modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
|
||||
if (!modeQueuePort) {
|
||||
CRASH("Unable to get port for run loop mode queue (%d)", -1);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
dispatch_source_t timeout_timer = NULL;
|
||||
struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
|
||||
if (seconds <= 0.0) { // instant timeout
|
||||
seconds = 0.0;
|
||||
timeout_context->termTSR = 0ULL;
|
||||
} else if (seconds <= TIMER_INTERVAL_LIMIT) {
|
||||
dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
|
||||
timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
|
||||
dispatch_retain(timeout_timer);
|
||||
timeout_context->ds = timeout_timer;
|
||||
timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
|
||||
timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
|
||||
dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
|
||||
dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
|
||||
dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
|
||||
uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
|
||||
dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
|
||||
dispatch_resume(timeout_timer);
|
||||
} else {
|
||||
// 设置RunLoop超时时间
|
||||
seconds = 9999999999.0;
|
||||
timeout_context->termTSR = UINT64_MAX;
|
||||
}
|
||||
|
||||
|
||||
Boolean didDispatchPortLastTime = true;
|
||||
int32_t retVal = 0;
|
||||
do {
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED;
|
||||
voucher_t voucherCopy = NULL;
|
||||
#endif
|
||||
uint8_t msg_buffer[3 * 1024];
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
mach_msg_header_t *msg = NULL;
|
||||
mach_port_t livePort = MACH_PORT_NULL;
|
||||
#elif DEPLOYMENT_TARGET_WINDOWS
|
||||
HANDLE livePort = NULL;
|
||||
Boolean windowsMessageReceived = false;
|
||||
#endif
|
||||
__CFPortSet waitSet = rlm->_portSet;
|
||||
|
||||
|
||||
__CFRunLoopUnsetIgnoreWakeUps(rl);
|
||||
|
||||
|
||||
if (rlm->_observerMask & kCFRunLoopBeforeTimers)
|
||||
// 2. 通知 Observers: RunLoop 即将触发 Timer 回调
|
||||
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
|
||||
if (rlm->_observerMask & kCFRunLoopBeforeSources)
|
||||
// 3. 通知 Observers: RunLoop 即将触发 Source 回调
|
||||
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
|
||||
// 执行被加入的block
|
||||
__CFRunLoopDoBlocks(rl, rlm);
|
||||
|
||||
|
||||
// 4. RunLoop 触发 Source0 (非port) 回调
|
||||
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
|
||||
if (sourceHandledThisLoop) {
|
||||
// 执行被加入的block
|
||||
__CFRunLoopDoBlocks(rl, rlm);
|
||||
}
|
||||
|
||||
|
||||
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
|
||||
|
||||
|
||||
// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息
|
||||
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
msg = (mach_msg_header_t *)msg_buffer;
|
||||
|
||||
|
||||
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
|
||||
goto handle_msg;
|
||||
}
|
||||
#elif DEPLOYMENT_TARGET_WINDOWS
|
||||
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
|
||||
goto handle_msg;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
didDispatchPortLastTime = false;
|
||||
|
||||
|
||||
// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)
|
||||
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
|
||||
__CFRunLoopSetSleeping(rl);
|
||||
// do not do any user callouts after this point (after notifying of sleeping)
|
||||
|
||||
|
||||
// Must push the local-to-this-activation ports in on every loop
|
||||
// iteration, as this mode could be run re-entrantly and we don't
|
||||
// want these ports to get serviced.
|
||||
|
||||
|
||||
__CFPortSetInsert(dispatchPort, waitSet);
|
||||
|
||||
|
||||
__CFRunLoopModeUnlock(rlm);
|
||||
__CFRunLoopUnlock(rl);
|
||||
|
||||
|
||||
CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
|
||||
|
||||
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
#if USE_DISPATCH_SOURCE_FOR_TIMERS
|
||||
do {
|
||||
if (kCFUseCollectableAllocator) {
|
||||
// objc_clear_stack(0);
|
||||
// <rdar://problem/16393959>
|
||||
memset(msg_buffer, 0, sizeof(msg_buffer));
|
||||
}
|
||||
msg = (mach_msg_header_t *)msg_buffer;
|
||||
|
||||
|
||||
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
|
||||
|
||||
|
||||
if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
|
||||
// Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
|
||||
while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
|
||||
if (rlm->_timerFired) {
|
||||
// Leave livePort as the queue port, and service timers below
|
||||
rlm->_timerFired = false;
|
||||
break;
|
||||
} else {
|
||||
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
|
||||
}
|
||||
} else {
|
||||
// Go ahead and leave the inner loop.
|
||||
break;
|
||||
}
|
||||
} while (1);
|
||||
#else
|
||||
if (kCFUseCollectableAllocator) {
|
||||
// objc_clear_stack(0);
|
||||
// <rdar://problem/16393959>
|
||||
memset(msg_buffer, 0, sizeof(msg_buffer));
|
||||
}
|
||||
msg = (mach_msg_header_t *)msg_buffer;
|
||||
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
|
||||
#elif DEPLOYMENT_TARGET_WINDOWS
|
||||
// Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
|
||||
__CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
|
||||
#endif
|
||||
|
||||
|
||||
__CFRunLoopLock(rl);
|
||||
__CFRunLoopModeLock(rlm);
|
||||
|
||||
|
||||
rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
|
||||
|
||||
|
||||
// Must remove the local-to-this-activation ports in on every loop
|
||||
// iteration, as this mode could be run re-entrantly and we don't
|
||||
// want these ports to get serviced. Also, we don't want them left
|
||||
// in there if this function returns.
|
||||
|
||||
|
||||
__CFPortSetRemove(dispatchPort, waitSet);
|
||||
|
||||
|
||||
__CFRunLoopSetIgnoreWakeUps(rl);
|
||||
|
||||
|
||||
// user callouts now OK again
|
||||
__CFRunLoopUnsetSleeping(rl);
|
||||
|
||||
|
||||
// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了
|
||||
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
|
||||
// 处理消息
|
||||
handle_msg:;
|
||||
__CFRunLoopSetIgnoreWakeUps(rl);
|
||||
|
||||
|
||||
#if DEPLOYMENT_TARGET_WINDOWS
|
||||
if (windowsMessageReceived) {
|
||||
// These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
|
||||
__CFRunLoopModeUnlock(rlm);
|
||||
__CFRunLoopUnlock(rl);
|
||||
|
||||
|
||||
if (rlm->_msgPump) {
|
||||
rlm->_msgPump();
|
||||
} else {
|
||||
MSG msg;
|
||||
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
__CFRunLoopLock(rl);
|
||||
__CFRunLoopModeLock(rlm);
|
||||
sourceHandledThisLoop = true;
|
||||
|
||||
|
||||
// To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
|
||||
// Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
|
||||
// NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
|
||||
__CFRunLoopSetSleeping(rl);
|
||||
__CFRunLoopModeUnlock(rlm);
|
||||
__CFRunLoopUnlock(rl);
|
||||
|
||||
|
||||
__CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);
|
||||
|
||||
|
||||
__CFRunLoopLock(rl);
|
||||
__CFRunLoopModeLock(rlm);
|
||||
__CFRunLoopUnsetSleeping(rl);
|
||||
// If we have a new live port then it will be handled below as normal
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
if (MACH_PORT_NULL == livePort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_NOTHING();
|
||||
// handle nothing
|
||||
} else if (livePort == rl->_wakeUpPort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
|
||||
// do nothing on Mac OS
|
||||
#if DEPLOYMENT_TARGET_WINDOWS
|
||||
// Always reset the wake up port, or risk spinning forever
|
||||
ResetEvent(rl->_wakeUpPort);
|
||||
#endif
|
||||
}
|
||||
#if USE_DISPATCH_SOURCE_FOR_TIMERS
|
||||
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_TIMER();
|
||||
|
||||
|
||||
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
|
||||
// Re-arm the next timer, because we apparently fired early
|
||||
__CFArmNextTimerInMode(rlm, rl);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if USE_MK_TIMER_TOO
|
||||
// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调
|
||||
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_TIMER();
|
||||
// On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
|
||||
// In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
|
||||
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
|
||||
// Re-arm the next timer
|
||||
__CFArmNextTimerInMode(rlm, rl);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// 9.2 如果有dispatch到main_queue的block,执行block
|
||||
else if (livePort == dispatchPort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
|
||||
__CFRunLoopModeUnlock(rlm);
|
||||
__CFRunLoopUnlock(rl);
|
||||
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
|
||||
#if DEPLOYMENT_TARGET_WINDOWS
|
||||
void *msg = 0;
|
||||
#endif
|
||||
/**/
|
||||
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
|
||||
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
|
||||
__CFRunLoopLock(rl);
|
||||
__CFRunLoopModeLock(rlm);
|
||||
sourceHandledThisLoop = true;
|
||||
didDispatchPortLastTime = true;
|
||||
}
|
||||
// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
|
||||
else {
|
||||
CFRUNLOOP_WAKEUP_FOR_SOURCE();
|
||||
|
||||
|
||||
// If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again.
|
||||
voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);
|
||||
|
||||
|
||||
/**/
|
||||
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
|
||||
if (rls) {
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
mach_msg_header_t *reply = NULL;
|
||||
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
|
||||
if (NULL != reply) {
|
||||
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
|
||||
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
|
||||
}
|
||||
#elif DEPLOYMENT_TARGET_WINDOWS
|
||||
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
// Restore the previous voucher
|
||||
_CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
|
||||
|
||||
|
||||
}
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
|
||||
#endif
|
||||
// 执行加入到Loop的block
|
||||
__CFRunLoopDoBlocks(rl, rlm);
|
||||
|
||||
if (sourceHandledThisLoop && stopAfterHandle) {
|
||||
// 进入loop时参数说处理完事件就返回
|
||||
retVal = kCFRunLoopRunHandledSource;
|
||||
} else if (timeout_context->termTSR < mach_absolute_time()) {
|
||||
// 超出传入参数标记的超时时间了
|
||||
retVal = kCFRunLoopRunTimedOut;
|
||||
} else if (__CFRunLoopIsStopped(rl)) {
|
||||
__CFRunLoopUnsetStopped(rl);
|
||||
// 被外部调用者强制停止了
|
||||
retVal = kCFRunLoopRunStopped;
|
||||
} else if (rlm->_stopped) {
|
||||
rlm->_stopped = false;
|
||||
retVal = kCFRunLoopRunStopped;
|
||||
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
|
||||
// source/timer一个都没有
|
||||
retVal = kCFRunLoopRunFinished;
|
||||
}
|
||||
|
||||
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
voucher_mach_msg_revert(voucherState);
|
||||
os_release(voucherCopy);
|
||||
#endif
|
||||
// 如果没超时,mode里没空,loop也没被停止,那继续loop
|
||||
} while (0 == retVal);
|
||||
|
||||
|
||||
if (timeout_timer) {
|
||||
dispatch_source_cancel(timeout_timer);
|
||||
dispatch_release(timeout_timer);
|
||||
} else {
|
||||
free(timeout_context);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
```
|
||||
|
||||
17
Chapter1 - iOS/1.4.md
Normal file
17
Chapter1 - iOS/1.4.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# 如何优雅地调试手机网页
|
||||
> 在web开发的过程中,抓包、调试页面样式、查看请求头是很常用的技巧。其实在iOS开发中,这些技巧也能用(无论是模拟器还是真机),不过我们需要用到mac自带的浏览器Safari。所以,本文将讲解如何使用Safari对iOS程序中的webview进行调试。
|
||||
|
||||
* 1、打开真机(模拟器)的开发者模式
|
||||
【设置】-> 【Safari】 -> 【高级】 -> 【Web检查器】打开
|
||||

|
||||
|
||||
* 2、打开MBP上的Safari的开发者模式:
|
||||
【Safari】->【偏好设置】->【高级】-> 【在菜单栏中显示“开发”菜单】勾选。
|
||||
|
||||
* 3、调试你的WebView页面。
|
||||
|
||||
* 4、在MBP的Safari选项中的开发,看到手机,右击可以看到正在调试的WebView的url
|
||||

|
||||
|
||||
* 5、在弹出的这个框里面可以查看网页源代码以及可以调试样样式、查看localStorage、sessionStorage、Cookie的值等等,给原生端调试带来很大方便,不过这样前端调试更加方便啊,谷歌的模拟器不能完全模真实环境下的iphone使用效果啊。
|
||||

|
||||
143
Chapter1 - iOS/1.40.md
Normal file
143
Chapter1 - iOS/1.40.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# RunLoop 的应用
|
||||
|
||||
> 了解了 RunLoop 的底层原理以及特点后我们有必要想一想它可以应用在什么地方?现在归纳下常见的应用场景
|
||||
|
||||
## NSTimer
|
||||
|
||||
我们常常写的 **NSTimer** 都是在的默认的运行状态下执行的(**NSDefaultRunLoopMode**)。所以我们会经常遇到 NSTimer 执行不准去的问题。因为 RunLoop 是有不同的运行状态的,当我们 UI 滚动的时候从 **NSDefaultRunLoopMode** 切换到 **UITrackingRunLoopMode**,所以添加到 **NSDefaultRunLoopMode** 状态下的事件是不会执行的,为了达到定时器准确的目的有2种方法。方法一:必须根据具体需求给 NSTimer 指定具体的 **CFRunLoopModeRef**。方法二:利用 GCD 的 timer 不会受 **NSDefaultRunLoopMode** 影响的特点。
|
||||
|
||||
```objective-c
|
||||
//默认状态下的 NSTimer
|
||||
[NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
|
||||
NSLog(@"我在执行了");
|
||||
}];
|
||||
```
|
||||
|
||||
```objective-c
|
||||
//方法1:给 NSTimer 指定运行状态
|
||||
NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(show) userInfo:nil repeats:YES];
|
||||
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
|
||||
```
|
||||
|
||||
```objective-c
|
||||
//方法2:GCD 的单位是纳秒。使用 GCD 创建的 timer 正常创建后不会执行,因为创建后设置了指定的时间后触发,所以当代码运行到最后一行的时候,Timer 还没执行,就被销毁了。所以我们必须设置一个属性去保存它。
|
||||
|
||||
//1、创建队列
|
||||
dispatch_queue_t queue = dispatch_get_main_queue();
|
||||
//2、创建 timer
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
|
||||
// self.timer = timer;
|
||||
//3、设置 timer 的参数:精准度、时间间隔
|
||||
//第三个参数为 GCD timer 的精准度
|
||||
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
|
||||
//4、为 Timer 设置任务
|
||||
dispatch_source_set_event_handler(timer, ^{
|
||||
NSLog(@"%@",[NSRunLoop currentRunLoop]);
|
||||
});
|
||||
//5、执行任务
|
||||
dispatch_resume(timer);
|
||||
```
|
||||
|
||||
## ImageView显示\(PerformSelector\)
|
||||
|
||||
UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示,通常需要根据网络去下载图片。所以如果用户快速滚动列表的时候,如果立马下载并显示图片的话,势必会对 UI 的刷新产生影响,直观的表现就是会卡顿,**FPS** 达不到60。
|
||||
|
||||
利用 RunLoop 可以实现这个效果,就是给下载并显示图片的方法指定 **NSRunLoopMode**。
|
||||
|
||||
```objective-c
|
||||
- (IBAction)clickLoadIMage:(id)sender {
|
||||
//[self.imageview performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"test"] afterDelay:2];
|
||||
[self performSelector:@selector(downloadAndShowImage) withObject:nil afterDelay:2 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];
|
||||
}
|
||||
|
||||
- (void)downloadAndShowImage{
|
||||
self.imageview.image = [UIImage imageNamed:@"test"];
|
||||
}
|
||||
```
|
||||
|
||||
## 常驻线程
|
||||
|
||||
我们需要常驻线程,那么就需要线程不要销毁,那么一些做法比如设置任务死循环,那么线程就不会销毁;将当前线程强引用不要销毁等都存在问题。最好的方法是为当前线程设置合理的 RunLoop
|
||||
|
||||
```objective-c
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
|
||||
LBPThread *thred = [[LBPThread alloc] initWithTarget:self selector:@selector(showThreadLife) object:nil];
|
||||
self.lbpThread = thred;
|
||||
[thred start];
|
||||
}
|
||||
|
||||
- (void)showThreadLife{
|
||||
NSLog(@"---show----");
|
||||
}
|
||||
|
||||
- (IBAction)clickLoadIMage:(id)sender {
|
||||
NSLog(@"%s",__func__);
|
||||
[self performSelector:@selector(contactWithTwoThread) onThread:self.lbpThread withObject:nil waitUntilDone:YES];
|
||||
}
|
||||
|
||||
- (void)contactWithTwoThread{
|
||||
NSLog(@"----contactWithTwoThread----");
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
我们都知道 RunLoop 存在必须要有一个 Timer 或者 Source。
|
||||
|
||||
```objective-c
|
||||
//方法1:添加 Port 的 Source。Sourcr1
|
||||
- (void)showThreadLife{
|
||||
NSLog(@"---show----");
|
||||
//子线程的 RunLoop 是需要自己手动创建并添加;RunLoop 如果不要销毁那么至少存在一个 Timer 或 Source
|
||||
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
|
||||
[[NSRunLoop currentRunLoop] run];
|
||||
}
|
||||
//方法2
|
||||
- (void)showThreadLife{
|
||||
NSLog(@"---show----");
|
||||
//子线程的 RunLoop 是需要自己手动创建并添加;RunLoop 如果不要销毁那么至少存在一个 Timer 或 Source
|
||||
|
||||
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(test) userInfo:nil repeats:YES];
|
||||
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
|
||||
[[NSRunLoop currentRunLoop] run];
|
||||
}
|
||||
```
|
||||
|
||||
注意:添加 Observer 是没有效果的。
|
||||
|
||||
## 自动释放池
|
||||
|
||||
自动释放池什么时候创建和释放
|
||||
|
||||
创建时间:第一次进入 RunLoop 的时候
|
||||
|
||||
释放时间:RunLoop 退出的时候
|
||||
|
||||
其他情况:当 RunLoop 将要休眠的时候释放,然后创建一个新的
|
||||
|
||||
**\_wrapRunLoopWithAutoreleasePoolHandler** **0x1**
|
||||
|
||||
**\_wrapRunLoopWithAutoreleasePoolHandler** **0xa0**
|
||||
|
||||
0x1 和 0xa0 是十六进制的数,对应十进制为1和160。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
参考文章:
|
||||
|
||||
http://www.cocoachina.com/ios/20180515/23380.html
|
||||
|
||||
http://www.cocoachina.com/ios/20170417/19075.html
|
||||
|
||||
https://www.jianshu.com/p/4c38d16a29f1
|
||||
|
||||
https://www.cnblogs.com/kenshincui/p/6823841.html
|
||||
|
||||
https://juejin.im/entry/599c13bc6fb9a0248926a77d
|
||||
|
||||
https://blog.ibireme.com/2015/05/18/runloop/
|
||||
|
||||
|
||||
|
||||
154
Chapter1 - iOS/1.41.md
Normal file
154
Chapter1 - iOS/1.41.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# iOS应用启动性能优化资料汇总
|
||||
|
||||
[WWDC](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#wwdc)
|
||||
|
||||
[文章](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E6%96%87%E7%AB%A0)
|
||||
|
||||
[工具](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E5%B7%A5%E5%85%B7)
|
||||
|
||||
[代码](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E4%BB%A3%E7%A0%81)
|
||||
|
||||
[偏门古董](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E5%81%8F%E9%97%A8%E5%8F%A4%E8%91%A3)
|
||||
|
||||
[书籍](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E4%B9%A6%E7%B1%8D)
|
||||
|
||||
[总结](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E6%80%BB%E7%BB%93)
|
||||
|
||||
发现好资料就整理到这里,_随时更新,最后一次更新2018年8月6日_
|
||||
|
||||
# WWDC
|
||||
|
||||
1. Optimizing App Startup Time
|
||||
|
||||
必看官方资料,从底层到上层[https://developer.apple.com/videos/play/wwdc2016/406/](https://developer.apple.com/videos/play/wwdc2016/406/)
|
||||
|
||||
2. App Startup Time: Past, Present, and Future
|
||||
|
||||
dyld层面的优化[https://developer.apple.com/videos/play/wwdc2017/413/](https://developer.apple.com/videos/play/wwdc2017/413/)
|
||||
|
||||
3. Optimizing I/O for Performance and Battery Life
|
||||
|
||||
IO是启动性能的重要影响部分[https://developer.apple.com/videos/play/wwdc2016/719/](https://developer.apple.com/videos/play/wwdc2016/719/)
|
||||
|
||||
4. Practical Approaches to Great App Performance
|
||||
|
||||
现场一步一步解决性能问题[https://developer.apple.com/videos/play/wwdc2018/407/](https://developer.apple.com/videos/play/wwdc2018/407/)
|
||||
|
||||
5. Using Time Profiler in Instruments
|
||||
|
||||
TimeProfiler是必备好帮手[https://developer.apple.com/videos/play/wwdc2016/418/](https://developer.apple.com/videos/play/wwdc2016/418/)
|
||||
|
||||
6. High Performance Auto Layout
|
||||
|
||||
App首页如果是AutoLayout的,那么以后看来不是问题了[https://developer.apple.com/videos/play/wwdc2018/220/](https://developer.apple.com/videos/play/wwdc2018/220/)
|
||||
|
||||
7. Core Image: Performance, Prototyping, and Python
|
||||
|
||||
首页当然也有大量的图片,了解Core Image[https://developer.apple.com/videos/play/wwdc2018/719/](https://developer.apple.com/videos/play/wwdc2018/719/)
|
||||
|
||||
# 文章
|
||||
|
||||
**以下文章仅仅是收集,各家之谈,不要全信,也不要反对,各有道理,学习思路即可。**
|
||||
|
||||
1. 即刻技术团队:iOS app 启动速度研究实践
|
||||
|
||||
地址[https://zhuanlan.zhihu.com/p/38183046?from=1086193010&wm=3333\_2001&weiboauthoruid=1690182120](https://zhuanlan.zhihu.com/p/38183046?from=1086193010&wm=3333_2001&weiboauthoruid=1690182120)学习思路。
|
||||
|
||||
2. iOS Dynamic Framework 对App启动时间影响实测
|
||||
|
||||
[https://www.jianshu.com/p/3263009e9228](https://www.jianshu.com/p/3263009e9228)动态库的测试。可知:启动过程中尽量不要加载动态库了。
|
||||
|
||||
3. Optimizing Facebook for iOS start time
|
||||
|
||||
[https://code.fb.com/ios/optimizing-facebook-for-ios-start-time/](https://code.fb.com/ios/optimizing-facebook-for-ios-start-time/)Facebook的思路。虽然Facebook的启动很慢。
|
||||
|
||||
4. Bugly: iOS App 启动性能优化
|
||||
|
||||
[https://mp.weixin.qq.com/s/Kf3EbDIUuf0aWVT-UCEmbA](https://mp.weixin.qq.com/s/Kf3EbDIUuf0aWVT-UCEmbA)这篇文章最后透露了一个很给力的思路。强烈推荐仔细看文章最后。
|
||||
|
||||
5. 今日头条iOS客户端启动速度优化
|
||||
|
||||
[https://techblog.toutiao.com/2017/01/17/iosspeed/](https://techblog.toutiao.com/2017/01/17/iosspeed/)文章开头的信息很多,但减少代码量,貌似很难行得通。
|
||||
|
||||
6. 如何精确度量 iOS App 的启动时间
|
||||
|
||||
[https://www.jianshu.com/p/c14987eee107](https://www.jianshu.com/p/c14987eee107)文章的思路可参考。
|
||||
|
||||
7. 优化 App 的启动时间
|
||||
|
||||
[http://yulingtianxia.com/blog/2016/10/30/Optimizing-App-Startup-Time/](http://yulingtianxia.com/blog/2016/10/30/Optimizing-App-Startup-Time/)主要是对WWDC的笔记,但仍然很给力。南萧玉,北子棋。这篇文章就是南萧玉所作。
|
||||
|
||||
8. 手淘iOS性能优化探索
|
||||
|
||||
[https://github.com/izhangxb/GMTC/blob/master/%E5%85%A8%E7%90%83%E7%A7%BB%E5%8A%A8%E6%8A%80%E6%9C%AF%E5%A4%A7%E4%BC%9AGMTC%202017%20PPT/%E6%89%8B%E6%B7%98iOS%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E6%8E%A2%E7%B4%A2%20.pdf](https://github.com/izhangxb/GMTC/blob/master/%E5%85%A8%E7%90%83%E7%A7%BB%E5%8A%A8%E6%8A%80%E6%9C%AF%E5%A4%A7%E4%BC%9AGMTC%202017%20PPT/%E6%89%8B%E6%B7%98iOS%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E6%8E%A2%E7%B4%A2%20.pdf)这是GMTC 2017手机淘宝专家的技术分享,可以参考。
|
||||
|
||||
9. iOS应用启动性能优化\(1\)-premain
|
||||
|
||||
[https://everettjf.github.io/2018/05/26/ios-app-launch-performance-part1/](https://everettjf.github.io/2018/05/26/ios-app-launch-performance-part1/)仅仅是pre-main阶段的思路。作者说有后续的文章,但很久没动静了,不知道在搞什么。
|
||||
|
||||
10. 一种 hook objective c +load 的方法
|
||||
|
||||
[https://everettjf.github.io/2017/01/06/a-method-of-hook-objective-c-load/](https://everettjf.github.io/2017/01/06/a-method-of-hook-objective-c-load/)这篇文章的hook比较麻烦,其实还可以参考上面的一篇文章[https://www.jianshu.com/p/c14987eee107](https://www.jianshu.com/p/c14987eee107),这里有批量hook +load的代码。(未来我也有计划会把这些相关代码整理到一个repo中)
|
||||
|
||||
11. 一种 hook C++ static initializers 的方法
|
||||
|
||||
[https://everettjf.github.io/2017/02/06/a-method-of-hook-static-initializers/](https://everettjf.github.io/2017/02/06/a-method-of-hook-static-initializers/)这篇文章的hook方法,有较大的可能是我首创,强烈推荐。手淘的分享中也提了这个方法。
|
||||
|
||||
12. 一种延迟 premain code 的方法
|
||||
|
||||
[https://everettjf.github.io/2017/03/06/a-method-of-delay-premain-code/](https://everettjf.github.io/2017/03/06/a-method-of-delay-premain-code/)通过学习Facebook的App中特有的section(参考文章[https://everettjf.github.io/2016/08/20/facebook-explore-section-fbinjectable/](https://everettjf.github.io/2016/08/20/facebook-explore-section-fbinjectable/)),发现的一种思路。
|
||||
|
||||
# 工具
|
||||
|
||||
1. TimeProfiler
|
||||
|
||||
都知道是啥。
|
||||
|
||||
2. AppleTrace
|
||||
|
||||
[https://github.com/everettjf/AppleTrace](https://github.com/everettjf/AppleTrace)使用 HookZz hook了objc\_msgSend,会有较大性能损耗,但可根据相对比例来知道大概的耗时占比。另外也手动定义开始结尾生成chrome tracing。
|
||||
|
||||
3. DTrace
|
||||
|
||||
只能用于模拟器。使用方法可参考这本书:Advanced Apple Debugging & Reverse Engineering[https://store.raywenderlich.com/products/advanced-apple-debugging-and-reverse-engineering](https://store.raywenderlich.com/products/advanced-apple-debugging-and-reverse-engineering)
|
||||
|
||||
4. Xcode 环境变量
|
||||
|
||||
DYLD\_PRINT\_STATISTIC 及其他类似环境变量[https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/LoggingDynamicLoaderEvents.html](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/LoggingDynamicLoaderEvents.html)
|
||||
|
||||
# 代码
|
||||
|
||||
1. FastImageCache
|
||||
|
||||
[https://github.com/path/FastImageCache](https://github.com/path/FastImageCache)优化图片加载的速度。空间换时间。
|
||||
|
||||
# 偏门古董
|
||||
|
||||
1. Code Size Performance Guidelines
|
||||
|
||||
[https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/MachOOverview.html](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/MachOOverview.html)页面最下面提出的思路很好,但文章是gcc时代的了。有没有clang时代对应的呢。 Improving Locality of Reference[https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/ImprovingLocality.html\#//apple\_ref/doc/uid/20001862-CJBJFIDD](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/ImprovingLocality.html#//apple_ref/doc/uid/20001862-CJBJFIDD)
|
||||
|
||||
# 书籍
|
||||
|
||||
1. Pro iOS Apps Performance Optimization
|
||||
|
||||
貌似比较古老,仅参考。
|
||||
|
||||
2. iOS and macOS Performance Tuning
|
||||
|
||||
很细致,我正在看。有中文翻译版。
|
||||
|
||||
3. High Performance iOS Apps
|
||||
|
||||
有中文翻译版。
|
||||
|
||||
# 总结
|
||||
|
||||
上面的文章我都看过,或者至少是正在看,总结下来,辅助大家优化启动性能。
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
转载于:https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/
|
||||
|
||||
84
Chapter1 - iOS/1.42.md
Normal file
84
Chapter1 - iOS/1.42.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# App 数据安全篇
|
||||
|
||||
> 之前的研究了 web 站点的数据安全,同时也用[文章](https://github.com/FantasticLBP/Anti-WebSpider)记录下来分享给大家。接着又研究了下 App 的安全,同样写文章记录下来
|
||||
|
||||
|
||||
|
||||
## 现状
|
||||
|
||||
目前 App 的安全比较低,体现在哪?很多人在想用了 HTTPS 不是就很安全吗?其实并不是,专业的抓包工具还是可以抓 HTTPS 包。根据接口规律,做自动化请求接口,将数据保存窃取是我们不想看到的结果。所以如果只用了 HTTPS 还是不安全。
|
||||
|
||||
所以需要实现的安全表现在:1. App 数据防止抓包;2. 防止中间人攻击;3. 下下策。即使抓包成功拿到的数据也是密文。如果想解密,是不可逆的。
|
||||
|
||||
|
||||
|
||||
## 解决方案
|
||||
|
||||
1. App 数据防止抓包
|
||||
|
||||
原理:抓包工具工作原理见[文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/第四部分%20开发杂谈/4.10.md)
|
||||
|
||||

|
||||
|
||||
**验证证书的真伪**其实一般来说这个过程应该是安全的,因为一般的证书都是由操作系统来管理。所以只要操作系统没有证书链验证等方面的 bug 是没有什么问题的,但是为了抓包其实我们是在操作系统中导入了中间人的 CA,这样中间人下发的公钥证书就可以被认为是合法的,可以通过验证的(中间人既承担了颁发证书,又承担了验证证书,通过验证)。
|
||||
|
||||
|
||||
|
||||
措施: 客户端为了解决这个问题,最好的方式其实就是内嵌证书,比对一下这个证书到底是不是自己真正的“服务端”发来的,而不是中间被替换了。
|
||||
|
||||
- 跟服务端人员拿到 https 证书,导入 Xcode 工程项目中
|
||||
|
||||
- AFNetworking设置以下代码
|
||||
|
||||
```objective-c
|
||||
AFSecurityPolicy * policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
|
||||
manager.securityPolicy = policy;
|
||||
```
|
||||
|
||||
AF的安全策略会自动的在bundle里面查找公钥证书,建立https的时候进行比对。不一样直接就失败了。
|
||||
|
||||
AFNetworking 的 AFSSLPinningMode 的三个级别
|
||||
|
||||
AFSSLPinningModeNone: (默认级别),客户端无条件信任任何下发的公钥证书
|
||||
|
||||
AFSSLPinningModePublicKey: 客户端本地去验证服务端下发的公钥证书的 public keys部分。如果正确才通过
|
||||
|
||||
AFSSLPinningModeCertificate: 客户端本地去验证服务端下发的公钥证书的所有部分。如果正确才通过
|
||||
|
||||
这样做了之后,就可以即使手机上安装了抓包工具的CA,抓包工具也不能抓到包了。因为你的客户端在验证“服务端”下发的公钥证书的真伪的时候就不会通过“中间人”下发的公钥证书,也就不会建立起来https的连接了。
|
||||
|
||||
2. 防止中间人攻击
|
||||
|
||||
即使我们的 App 被大神逆向了(iOS + 网络精通),抓到网络请求,然后原封不动去向服务器发起请求,但是服务端做了防重放,也是很安全的。所以防重放机制是 Server 端的安全措施
|
||||
|
||||
[防重放](https://www.cnblogs.com/yjf512/p/6590890.html)
|
||||
|
||||
3. 密文,反向解密不可逆
|
||||
|
||||
采用 RSA 非对称加密算法。
|
||||
|
||||
- iOS 端和 Server 端各生成自己的公钥和私钥
|
||||
|
||||
使用 **openssl** 生成所需秘钥
|
||||
|
||||
- iOS 端生成的公钥和私钥定义为 **iOSPublicKey、iOSPrivateKey**,Server 端生成的公钥私钥定义为**ServerPublicKey、ServerPrivateKey**。将 **iOSPublicKey ** 给 Server 端使用,让它用 **iOSPublicKey ** 加密数据传给 iOS 端,iOS 端用 **iOSPrivateKey** 解密;Server 端将 **ServerPublicKey** 给 iOS 端,iOS 端用 **ServerPublicKey** 加密数据后上传给 Server 端,Server 端利用 **ServerPrivateKey** 去解密,这样就实现了数据传输过程中的加密与解密
|
||||
|
||||
|
||||
|
||||
## 资料
|
||||
|
||||
[RSA算法原理(一)](http://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html)
|
||||
|
||||
[RSA算法原理(二)](http://www.ruanyifeng.com/blog/2013/07/rsa_algorithm_part_two.htmll)
|
||||
|
||||
[素数](https://zh.wikipedia.org/zh-cn/互質)
|
||||
|
||||
[防重放](https://www.cnblogs.com/yjf512/p/6590890.html)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
16
Chapter1 - iOS/1.43.md
Normal file
16
Chapter1 - iOS/1.43.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 调试方面的骚操作
|
||||
|
||||
1. 在日常开发中我们经常会封装某个功能模块然后暴露某个方法给外部。但是很多时候调用我们封装功能的人可能会不按照约定的方法传递参数。所以我们会使用断言。但是在线上的时候如果使用了断言,那么程序肯定会 **Crash** ,Xcode 提供了一个小功能可以解决这个问题。
|
||||
|
||||
`NS_BLOCK_ASSERTIONS `: 表明在 Release 状态下过滤 NSAssert,只需要这一个条件就可以过滤掉 NSAssert。
|
||||
方法:在 “Build Settings” 下搜索 **Preprocessor Macros** ,然后在 Release 下面添加 NS_BLOCK_ASSERTIONS
|
||||
|
||||

|
||||
|
||||
|
||||
### BreakPoint
|
||||
|
||||
#### 分类
|
||||
Breakpoint 分为 Normal Breakpoint、Exception Breakpoint、OpenGL ES Error Breakpoint、Symbolic Breakpoint、Test Failure breakpoint、WatchPoint。可以按照具体的情景使用不同类型的 Breakpoint ,解决问题为根本
|
||||
|
||||
|
||||
767
Chapter1 - iOS/1.44..md
Normal file
767
Chapter1 - iOS/1.44..md
Normal file
@@ -0,0 +1,767 @@
|
||||
# Hybrid 设计与实现
|
||||
|
||||
随着移动浪潮的兴起,各种 App 层出不穷,极速发展的业务拓展提升了团队对开发效率的要求,这个时候纯粹使用 Native 开发技术成本难免会更高一点。而 H5 的低成本、高效率、跨平台等特性马上被利用起来了,形成一种新的开发模式: Hybrid App
|
||||
|
||||
作为一种混合开发的模式,Hybrid App 底层依赖于 Native 提供的容器(Webview),上层使用各种前端技术完成业务开发(现在三足鼎立的 Vue、React、Angular),底层透明化、上层多样化。这种场景非常有利于前端介入,非常适合业务的快速迭代。于是 Hybrid 火了。
|
||||
|
||||
大道理谁都懂,但是按照我知道的情况,还是有非常多的人和公司在 Hybrid 这一块并没有做的很好,所以我将我的经验做一个总结,希望可以帮助广大开发者的技术选型有所帮助
|
||||
|
||||
|
||||
## Hybrid 的一个现状
|
||||
|
||||
可能早期都是 PC 端的网页开发,随着移动互联网的发展,iOS、Android 智能手机的普及,非常多的业务和场景都从 PC 端转移到移动端。开始有前端开发者为移动端开发网页。这样子早期资源打包到 Native App 中会造成应用包体积的增大。越来越多的业务开始用 H5 尝试,这样子难免会需要一个需要访问 Native 功能的地方,这样子可能早期就是懂点前端技术的 Native 开发者自己封装或者暴露 Native 能力给 JS 端,等业务较多的时候者样子很明显不现实,就需要专门的 Hybrid 团队做这个事情;量大了,就需要规矩,就需要规范。
|
||||
|
||||
总结:
|
||||
1. Hybrid 开发效率高、跨平台、低成本
|
||||
2. Hybrid 从业务上讲,没有版本问题,有 Bug 可以及时修复
|
||||
|
||||
Hybrid 在大量应用的时候就需要一定的规范,那么本文将讨论一个 Hybrid 的设计知识。
|
||||
- Hybrid 、Native、前端各自的工作是什么
|
||||
- Hybrid 交互接口如何设计
|
||||
- Hybrid 的 Header 如何设计
|
||||
- Hybrid 的如何设计目录结构以及增量机制如何实现
|
||||
- 资源缓存策略,白屏问题...
|
||||
|
||||
|
||||
## Native 与前端分工
|
||||
在做 Hybird 架构设计之前我们需要分清 Native 与前端的界限。首先 Native 提供的是宿主环境,要合理利用 Native 提供的能力,要实现通用的 Hybrid 架构,站在大前端的视觉,我觉得需要考虑以下核心设计问题。
|
||||
|
||||
### 交互设计
|
||||
|
||||
Hybrid 架构设计的第一要考虑的问题就是如何设计前端与 Native 的交互,如果这块设计不好会对后续的开发、前端框架的维护造成深远影响。并且这种影响是不可逆、积重难返。所以前期需要前端与 Native 好好配合、提供通用的接口。比如
|
||||
|
||||
1. Native UI 组件、Header 组件、消息类组件
|
||||
2. 通讯录、系统、设备信息读取接口
|
||||
3. H5 与 Native 的互相跳转。比如 H5 如何跳转到一个 Native 页面,H5 如何新开 Webview 并做动画跳转到另一个 H5 页面
|
||||
|
||||
### 账号信息设计
|
||||
|
||||
账号系统是重要且无法避免的,Native 需要设计良好安全的身份验证机制,保证这块对业务开发者足够透明,打通账户体系
|
||||
|
||||
### Hybrid 开发调试
|
||||
|
||||
功能设计、编码完并不是真正结束,Native 与前端需要商量出一套可开发调试的模型,不然很多业务开发的工作难以继续。
|
||||
|
||||
[iOS调试技巧](https://www.jianshu.com/p/f430caa81fa8)
|
||||
|
||||
Android 调试技巧:
|
||||
1. App 中开启 Webview 调试(WebView.setWebContentsDebuggingEnabled(true); )
|
||||
2. chrome 浏览器输入 chrome://inspect/#devices 访问可以调试的 webview 列表
|
||||
3. 需要翻墙的环境
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## Hybrid 交互设计
|
||||
|
||||
Hybrid 交互无非是 Native 调用 H5 页面JS 方法,或者 H5 页面通过 JS 调 Native 提供的接口。2者通信的桥梁是 Webview。
|
||||
业界主流的通信方法:1.桥接对象(时机问题,不太主张这种方式);2.自定义 Url scheme
|
||||
|
||||

|
||||
|
||||
App 自身定义了 url scheme,将自定义的 url 注册到调度中心,例如
|
||||
weixin:// 可以打开微信。
|
||||
|
||||
关于 Url scheme 如果不太清楚可以看看 [这篇文章](https://www.jianshu.com/p/253479ccc83a)
|
||||
|
||||
### JS to Native
|
||||
|
||||
Native 在每个版本都会提供一些 Api,前端会有一个对应的框架团队对其封装,释放业务接口。举例
|
||||
|
||||
```
|
||||
SDGHybrid.http.get() // 向业务服务器拿数据
|
||||
SDGHybrid.http.post() // 向业务服务器提交数据
|
||||
SDGHybrid.http.sign() // 计算签名
|
||||
SDGHybrid.http.getUA() // 获取UserAgent
|
||||
```
|
||||
|
||||
```
|
||||
SDGHybridReady(function(arg){
|
||||
SDGHybrid.http.post({
|
||||
url: arg.baseurl + '/feedback',
|
||||
params:{
|
||||
title: '点菜很慢',
|
||||
content: '服务差'
|
||||
},
|
||||
success: (data) => {
|
||||
renderUI(data);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log(err);
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
前端框架定义了一个全局变量 SDGHybrid 作为 Native 与前端交互的桥梁,前端可以通过这个对象获得访问 Native 的能力
|
||||
|
||||
|
||||
### Api 交互
|
||||
|
||||
调用 Native Api 接口的方式和使用传统的 Ajax 调用服务器,或者 Native 的网络请求提供的接口相似
|
||||

|
||||
|
||||
所以我们需要封装的就是模拟创建一个类似 Ajax 模型的 Native 请求。
|
||||
|
||||

|
||||
|
||||
|
||||
### 格式约定
|
||||
交互的第一步是设计数据格式。这里分为请求数据格式与响应数据格式,参考 Ajax 模型:
|
||||
|
||||
```
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "test.json",
|
||||
data: {username:$("#username").val(), content:$("#content").val()},
|
||||
dataType: "json",
|
||||
success: function(data){
|
||||
renderUI(data);
|
||||
}
|
||||
});
|
||||
```
|
||||
```
|
||||
$.ajax(options) => XMLHTTPRequest
|
||||
type(默认值:GET),HTTP请求方法(GET|POST|DELETE|...)
|
||||
url(默认值:当前url),请求的url地址
|
||||
data(默认值:'') 请求中的数据如果是字符串则不变,如果为Object,则需要转换为String,含有中文则会encodeURI
|
||||
```
|
||||
|
||||
所以 Hybrid 中的请求模型为:
|
||||
```
|
||||
requestHybrid({
|
||||
// H5 请求由 Native 完成
|
||||
tagname: 'NativeRequest',
|
||||
// 请求参数
|
||||
param: requestObject,
|
||||
// 结果的回调
|
||||
callback: function (data) {
|
||||
renderUI(data);
|
||||
}
|
||||
});
|
||||
```
|
||||
这个方法会形成一个 URL,比如:
|
||||
`SDGHybrid://NativeRequest?t=1545840397616&callback=Hybrid_1545840397616¶m=%7B%22url%22%3A%22https%3A%2F%2Fwww.datacubr.com%2FApi%2FSearchInfo%2FgetLawsInfo%22%2C%22params%22%3A%7B%22key%22%3A%22%22%2C%22page%22%3A1%2C%22encryption%22%3A1%7D%2C%22Hybrid_Request_Method%22%3A0%7D`
|
||||
|
||||
Native 的 webview 环境可以监控内部任何的资源请求,判断如果是 SDGHybrid 则分发事件,处理结束可能会携带参数,参数需要先 urldecode 然后将结果数据通过 Webview 获取 window 对象中的 callback(Hybrid_时间戳)
|
||||
|
||||
数据返回的格式和普通的接口返回格式类似
|
||||
```
|
||||
{
|
||||
errno: 1,
|
||||
message: 'App版本过低,请升级App版本',
|
||||
data: {}
|
||||
}
|
||||
```
|
||||
这里注意:真实数据在 data 节点中。如果 errno 不为0,则需要提示 message。
|
||||
|
||||
|
||||
简易版本代码实现。
|
||||
|
||||
```
|
||||
//通用的 Hybrid call Native
|
||||
window.SDGbrHybrid = window.SDGbrHybrid || {};
|
||||
var loadURL = function (url) {
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.style.display = "none";
|
||||
iframe.style.width = '1px';
|
||||
iframe.style.height = '1px';
|
||||
iframe.src = url;
|
||||
document.body.appendChild(iframe);
|
||||
setTimeout(function () {
|
||||
iframe.remove();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
var _getHybridUrl = function (params) {
|
||||
var paramStr = '', url = 'SDGHybrid://';
|
||||
url += params.tagname + "?t=" + new Date().getTime();
|
||||
if (params.callback) {
|
||||
url += "&callback=" + params.callback;
|
||||
delete params.callback;
|
||||
}
|
||||
|
||||
if (params.param) {
|
||||
paramStr = typeof params.param == "object" ? JSON.stringify(params.param) : params.param;
|
||||
url += "¶m=" + encodeURIComponent(paramStr);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
|
||||
var requestHybrid = function (params) {
|
||||
//生成随机函数
|
||||
var tt = (new Date().getTime());
|
||||
var t = "Hybrid_" + tt;
|
||||
var tmpFn;
|
||||
|
||||
if (params.callback) {
|
||||
tmpFn = params.callback;
|
||||
params.callback = t;
|
||||
window.SDGHybrid[t] = function (data) {
|
||||
tmpFn(data);
|
||||
delete window.SDGHybrid[t];
|
||||
}
|
||||
}
|
||||
loadURL(_getHybridUrl(params));
|
||||
};
|
||||
|
||||
//获取版本信息,约定APP的navigator.userAgent版本包含版本信息:scheme/xx.xx.xx
|
||||
var getHybridInfo = function () {
|
||||
var platform_version = {};
|
||||
var na = navigator.userAgent;
|
||||
var info = na.match(/scheme\/\d\.\d\.\d/);
|
||||
|
||||
if (info && info[0]) {
|
||||
info = info[0].split('/');
|
||||
if (info && info.length == 2) {
|
||||
platform_version.platform = info[0];
|
||||
platform_version.version = info[1];
|
||||
}
|
||||
}
|
||||
return platform_version;
|
||||
};
|
||||
```
|
||||
Native 对于 H5 来说有个 Webview 容器,框架&&底层不太关心 H5 的业务实现,所以真实业务中 Native 调用 H5 场景较少。
|
||||
|
||||
上面的网络访问 Native 代码(iOS为例)
|
||||
|
||||
```
|
||||
typedef NS_ENUM(NSInteger){
|
||||
Hybrid_Request_Method_Post = 0,
|
||||
Hybrid_Request_Method_Get = 1
|
||||
} Hybrid_Request_Method;
|
||||
|
||||
@interface RequestModel : NSObject
|
||||
|
||||
@property (nonatomic, strong) NSString *url;
|
||||
@property (nonatomic, assign) Hybrid_Request_Method Hybrid_Request_Method;
|
||||
@property (nonatomic, strong) NSDictionary *params;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface HybridRequest : NSObject
|
||||
|
||||
|
||||
+ (void)requestWithNative:(RequestModel *)requestModel hybridRequestSuccess:(void (^)(id responseObject))success hybridRequestfail:(void (^)(void))fail;
|
||||
|
||||
+ (void)requestWithNative:(RequestModel *)requestModel hybridRequestSuccess:(void (^)(id responseObject))success hybridRequestfail:(void (^)(void))fail{
|
||||
//处理请求不全的情况
|
||||
NSAssert(requestModel || success || fail, @"Something goes wrong");
|
||||
|
||||
NSString *url = requestModel.url;
|
||||
NSDictionary *params = requestModel.params;
|
||||
if (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Get) {
|
||||
[AFNetPackage getJSONWithUrl:url parameters:params success:^(id responseObject) {
|
||||
success(responseObject);
|
||||
} fail:^{
|
||||
fail();
|
||||
}];
|
||||
}
|
||||
else if (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Post) {
|
||||
[AFNetPackage postJSONWithUrl:url parameters:params success:^(id responseObject) {
|
||||
success(responseObject);
|
||||
} fail:^{
|
||||
fail();
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 常用交互 Api
|
||||
|
||||
良好的交互设计是第一步,在真实业务开发中有一些 Api 一定会由应用场景。
|
||||
|
||||
### 跳转
|
||||
跳转是 Hybrid 必用的 Api 之一,对前端来说有以下情况:
|
||||
- 页面内跳转,与 Hybrid 无关
|
||||
- H5 跳转 Native 界面
|
||||
- H5 新开 Webview 跳转 H5 页面,一般动画切换页面
|
||||
如果使用动画,按照业务来说分为前进、后退。forward & backword,规定如下,首先是 H5 跳 Native 某个页面
|
||||
|
||||
```
|
||||
//H5跳Native页面
|
||||
//=>SDGHybrid://forward?t=1446297487682¶m=%7B%22topage%22%3A%22home%22%2C%22type%22%3A%22h2n%22%2C%22data2%22%3A2%7D
|
||||
requestHybrid({
|
||||
tagname: 'forward',
|
||||
param: {
|
||||
// 要去到的页面
|
||||
topage: 'home',
|
||||
// 跳转方式,H5跳Native
|
||||
type: 'native',
|
||||
// 其它参数
|
||||
data2: 2
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
H5 页面要去 Native 某个页面
|
||||
|
||||
```
|
||||
//=>SDGHybrid://forward?t=1446297653344¶m=%7B%22topage%22%253A%22Goods%252Fdetail%20%20%22%252C%22type%22%253A%22h2n%22%252C%22id%22%253A20151031%7D
|
||||
requestHybrid({
|
||||
tagname: 'forward',
|
||||
param: {
|
||||
// 要去到的页面
|
||||
topage: 'Goods/detail',
|
||||
// 跳转方式,H5跳Native
|
||||
type: 'native',
|
||||
// 其它参数
|
||||
id: 20151031
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
H5 新开 Webview 的方式去跳转 H5
|
||||
|
||||
```
|
||||
requestHybrid({
|
||||
tagname: 'forward',
|
||||
param: {
|
||||
// 要去到的页面,首先找到goods频道,然后定位到detail模块
|
||||
topage: 'goods/detail ',
|
||||
//跳转方式,H5新开Webview跳转,最后装载H5页面
|
||||
type: 'webview',
|
||||
//其它参数
|
||||
id: 20151031
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
back 与 forward 一致,可能会有 animatetype 参数决定页面切换的时候的动画效果。真实使用的时候可能会全局封装方法去忽略 tagname 细节。
|
||||
|
||||
## Header 组件的设计
|
||||
|
||||
Native 每次改动都比较“慢”,所以类似 Header 就很需要。
|
||||
1. 主流容器都是这么做的,比如微信、手机百度、携程
|
||||
2. 没有 Header 一旦出现网络错误或者白屏,App 将陷入假死状态
|
||||
|
||||
PS: Native 打开 H5,如果 300ms 没有响应则需要 loading 组件,避免白屏
|
||||
因为 H5 App 本身就有 Header 组件,站在前端框架层来说,需要确保业务代码是一致的,所有的差异需要在框架层做到透明化,简单来说 Header 的设计需要遵循:
|
||||
- H5 Header 组件与 Native 提供的 Header 组件使用调用层接口一致
|
||||
- 前端框架层根据环境判断选择应该使用 H5 的 Header 组件抑或 Native 的 Header 组件
|
||||
|
||||
一般来说 Header 组件需要完成以下功能:
|
||||
|
||||
1. Header 左侧与右侧可配置,显示为文字或者图标(这里要求 Header 实现主流图标,并且也可由业务控制图标),并需要控制其点击回调
|
||||
|
||||
2. Header 的 title 可设置为单标题或者主标题、子标题类型,并且可配置 lefticon 与 righticon(icon居中)
|
||||
|
||||
3. 满足一些特殊配置,比如标签类 Header
|
||||
|
||||
所以,站在前端业务方来说,Header 的使用方式为(其中 tagname 是不允许重复的):
|
||||
|
||||
```
|
||||
//Native以及前端框架会对特殊tagname的标识做默认回调,如果未注册callback,或者点击回调callback无返回则执行默认方法
|
||||
// back前端默认执行History.back,如果不可后退则回到指定URL,Native如果检测到不可后退则返回Naive大首页
|
||||
// home前端默认返回指定URL,Native默认返回大首页
|
||||
this.header.set({
|
||||
left: [
|
||||
{
|
||||
//如果出现value字段,则默认不使用icon
|
||||
tagname: 'back',
|
||||
value: '回退',
|
||||
//如果设置了lefticon或者righticon,则显示icon
|
||||
//native会提供常用图标icon映射,如果找不到,便会去当前业务频道专用目录获取图标
|
||||
lefticon: 'back',
|
||||
callback: function () { }
|
||||
}
|
||||
],
|
||||
right: [
|
||||
{
|
||||
//默认icon为tagname,这里为icon
|
||||
tagname: 'search',
|
||||
callback: function () { }
|
||||
},
|
||||
//自定义图标
|
||||
{
|
||||
tagname: 'me',
|
||||
//会去hotel频道存储静态header图标资源目录搜寻该图标,没有便使用默认图标
|
||||
icon: 'hotel/me.png',
|
||||
callback: function () { }
|
||||
}
|
||||
],
|
||||
title: 'title',
|
||||
//显示主标题,子标题的场景
|
||||
title: ['title', 'subtitle'],
|
||||
//定制化title
|
||||
title: {
|
||||
value: 'title',
|
||||
//标题右边图标
|
||||
righticon: 'down', //也可以设置lefticon
|
||||
//标题类型,默认为空,设置的话需要特殊处理
|
||||
//type: 'tabs',
|
||||
//点击标题时的回调,默认为空
|
||||
callback: function () { }
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
因为 Header 左边一般来说只有一个按钮,所以其对象可以使用这种形式:
|
||||
|
||||
```
|
||||
this.header.set({
|
||||
back: function () { },
|
||||
title: ''
|
||||
});
|
||||
//语法糖=>
|
||||
this.header.set({
|
||||
left: [{
|
||||
tagname: 'back',
|
||||
callback: function(){}
|
||||
}],
|
||||
title: '',
|
||||
});
|
||||
```
|
||||
|
||||
为完成 Native 端的实现,这里会新增两个接口,向 Native 注册事件,以及注销事件:
|
||||
|
||||
```
|
||||
var registerHybridCallback = function (ns, name, callback) {
|
||||
if(!window.Hybrid[ns]) window.Hybrid[ns] = {};
|
||||
window.Hybrid[ns][name] = callback;
|
||||
};
|
||||
|
||||
var unRegisterHybridCallback = function (ns) {
|
||||
if(!window.Hybrid[ns]) return;
|
||||
delete window.Hybrid[ns];
|
||||
};
|
||||
```
|
||||
|
||||
Native Header 组件实现:
|
||||
|
||||
```
|
||||
define([], function () {
|
||||
'use strict';
|
||||
|
||||
return _.inherit({
|
||||
|
||||
propertys: function () {
|
||||
|
||||
this.left = [];
|
||||
this.right = [];
|
||||
this.title = {};
|
||||
this.view = null;
|
||||
|
||||
this.hybridEventFlag = 'Header_Event';
|
||||
|
||||
},
|
||||
|
||||
//全部更新
|
||||
set: function (opts) {
|
||||
if (!opts) return;
|
||||
|
||||
var left = [];
|
||||
var right = [];
|
||||
var title = {};
|
||||
var tmp = {};
|
||||
|
||||
//语法糖适配
|
||||
if (opts.back) {
|
||||
tmp = { tagname: 'back' };
|
||||
if (typeof opts.back == 'string') tmp.value = opts.back;
|
||||
else if (typeof opts.back == 'function') tmp.callback = opts.back;
|
||||
else if (typeof opts.back == 'object') _.extend(tmp, opts.back);
|
||||
left.push(tmp);
|
||||
} else {
|
||||
if (opts.left) left = opts.left;
|
||||
}
|
||||
|
||||
//右边按钮必须保持数据一致性
|
||||
if (typeof opts.right == 'object' && opts.right.length) right = opts.right
|
||||
|
||||
if (typeof opts.title == 'string') {
|
||||
title.title = opts.title;
|
||||
} else if (_.isArray(opts.title) && opts.title.length > 1) {
|
||||
title.title = opts.title[0];
|
||||
title.subtitle = opts.title[1];
|
||||
} else if (typeof opts.title == 'object') {
|
||||
_.extend(title, opts.title);
|
||||
}
|
||||
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
this.title = title;
|
||||
this.view = opts.view;
|
||||
|
||||
this.registerEvents();
|
||||
|
||||
_.requestHybrid({
|
||||
tagname: 'updateheader',
|
||||
param: {
|
||||
left: this.left,
|
||||
right: this.right,
|
||||
title: this.title
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
//注册事件,将事件存于本地
|
||||
registerEvents: function () {
|
||||
_.unRegisterHybridCallback(this.hybridEventFlag);
|
||||
this._addEvent(this.left);
|
||||
this._addEvent(this.right);
|
||||
this._addEvent(this.title);
|
||||
},
|
||||
|
||||
_addEvent: function (data) {
|
||||
if (!_.isArray(data)) data = [data];
|
||||
var i, len, tmp, fn, tagname;
|
||||
var t = 'header_' + (new Date().getTime());
|
||||
|
||||
for (i = 0, len = data.length; i < len; i++) {
|
||||
tmp = data[i];
|
||||
tagname = tmp.tagname || '';
|
||||
if (tmp.callback) {
|
||||
fn = $.proxy(tmp.callback, this.view);
|
||||
tmp.callback = t;
|
||||
_.registerHeaderCallback(this.hybridEventFlag, t + '_' + tagname, fn);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//显示header
|
||||
show: function () {
|
||||
_.requestHybrid({
|
||||
tagname: 'showheader'
|
||||
});
|
||||
},
|
||||
|
||||
//隐藏header
|
||||
hide: function () {
|
||||
_.requestHybrid({
|
||||
tagname: 'hideheader',
|
||||
param: {
|
||||
animate: true
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
//只更新title,不重置事件,不对header其它地方造成变化,仅仅最简单的header能如此操作
|
||||
update: function (title) {
|
||||
_.requestHybrid({
|
||||
tagname: 'updateheadertitle',
|
||||
param: {
|
||||
title: 'aaaaa'
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.propertys();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
## 请求类
|
||||
|
||||
虽然 get 类请求可以用 jsonp 方式绕过跨域问题,但是 post 请求是一个拦路虎。为了安全性问题服务器会设置 cors 仅仅针对几个域名,Hybrid 内嵌静态资源可能是通过本地 file 的方式读取,所以 cors 就行不通了。另外一个问题是防止爬虫获取数据,由于 Native 针对网络做了安全性设置(鉴权、防抓包等),所以 H5 的网络请求由 Native 完成。可能有些人说 H5 的网络请求让 Native 走就安全了吗?我可以继续爬取你的 Dom 节点啊。这个是针对反爬虫的手段一。想知道更多的反爬虫策略可以看看我这篇文章 [Web反爬虫方案](https://github.com/FantasticLBP/Anti-WebSpider)
|
||||
|
||||

|
||||
|
||||
这个使用场景和 Header 组件一致,前端框架层必须做到对业务透明化,业务事实上不必关心这个网络请求到底是由 Native 还是浏览器发出。
|
||||
|
||||
```
|
||||
HybridGet = function (url, param, callback) {
|
||||
|
||||
};
|
||||
HybridPost = function (url, param, callback) {
|
||||
|
||||
};
|
||||
```
|
||||
|
||||
真实的业务场景,会将之封装到数据请求模块,在底层做适配,在H5站点下使用ajax请求,在Native内嵌时使用代理发出,与Native的约定为
|
||||
|
||||
```
|
||||
requestHybrid({
|
||||
tagname: 'NativeRequest',
|
||||
param: {
|
||||
url: arg.Api + "SearchInfo/getLawsInfo",
|
||||
params: requestparams,
|
||||
Hybrid_Request_Method: 0,
|
||||
encryption: 1
|
||||
},
|
||||
callback: function (data) {
|
||||
renderUI(data);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 常用 NativeUI 组件
|
||||
|
||||
一般情况 Native 通常会提供常用的 UI,比如 加载层loading、消息框toast
|
||||
|
||||
```
|
||||
var HybridUI = {};
|
||||
HybridUI.showLoading();
|
||||
//=>
|
||||
requestHybrid({
|
||||
tagname: 'showLoading'
|
||||
});
|
||||
|
||||
HybridUI.showToast({
|
||||
title: '111',
|
||||
//几秒后自动关闭提示框,-1需要点击才会关闭
|
||||
hidesec: 3,
|
||||
//弹出层关闭时的回调
|
||||
callback: function () { }
|
||||
});
|
||||
//=>
|
||||
requestHybrid({
|
||||
tagname: 'showToast',
|
||||
param: {
|
||||
title: '111',
|
||||
hidesec: 3,
|
||||
callback: function () { }
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Native UI与前端UI不容易打通,所以在真实业务开发过程中,一般只会使用几个关键的Native UI。
|
||||
|
||||
|
||||
|
||||
## 账号系统的设计
|
||||
|
||||
Webview 中跑的网页,账号登录与否由是否携带密钥 cookie 决定(不能保证密钥的有效性)。因为 Native 不关注业务实现,所以每次载入都有可能是登录成功跳转回来的结果,所以每次载入都需要关注密钥 cookie 变化,以做到登录态数据的一致性。
|
||||
|
||||
|
||||
|
||||
- 使用 Native 代理做请求接口,如果没有登录则 Native 层唤起登录页
|
||||
- 直连方式使用 ajax 请求接口,如果没登录则在底层唤起登录页(H5)
|
||||
|
||||
```javascript
|
||||
/*
|
||||
无论成功与否皆会关闭登录框
|
||||
参数包括:
|
||||
success 登录成功的回调
|
||||
error 登录失败的回调
|
||||
url 如果没有设置success,或者success执行后没有返回true,则默认跳往此url
|
||||
*/
|
||||
HybridUI.Login = function (opts) {
|
||||
//...
|
||||
};
|
||||
//=>
|
||||
requestHybrid({
|
||||
tagname: 'login',
|
||||
param: {
|
||||
success: function () { },
|
||||
error: function () { },
|
||||
url: '...'
|
||||
}
|
||||
});
|
||||
//与登录接口一致,参数一致
|
||||
HybridUI.logout = function () {
|
||||
//...
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
|
||||
在设计 Hybrid 层的时候,接口要做到对于处于 Hybrid 环境中的代码乐意通过接口获取 Native 端存储的用户账号信息;对于处于传统的网页环境,可以通过接口获取线上的账号信息,然后将非敏感的信息存储到 LocalStorage 中,然后每次页面加载从 LocalStorage 读取数据到内存中(比如 Vue.js 框架中的 Vuex,React.js 中的 Redux)
|
||||
|
||||
|
||||
|
||||
## Hybrid 资源管理
|
||||
|
||||
|
||||
|
||||
Hybrid 的资源需要 `增量更新` 需要拆分方便,所以一个 Hybrid 资源结构类似于下面的样子
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
假设有2个业务线:商城、购物车
|
||||
|
||||
|
||||
|
||||
```tex
|
||||
WebApp
|
||||
│- Mall
|
||||
│- Cart
|
||||
│ index.html //业务入口html资源,如果不是单页应用会有多个入口
|
||||
│ │ main.js //业务所有js资源打包
|
||||
│ │
|
||||
│ └─static //静态样式资源
|
||||
│ ├─css
|
||||
│ ├─hybrid //存储业务定制化类Native Header图标
|
||||
│ └─images
|
||||
├─libs
|
||||
│ libs.js //框架所有js资源打包
|
||||
│
|
||||
└─static
|
||||
├─css
|
||||
└─images
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 增量更新
|
||||
|
||||
|
||||
|
||||
每次业务开发完毕后都需要在打包分发平台进行部署上线,之后会生成一个版本号。
|
||||
|
||||
|
||||
|
||||
| Channel | Version | md5 |
|
||||
| ------- | ------- | ----------- |
|
||||
| Mall | 1.0.1 | 12233000ww |
|
||||
| Cart | 1.1.2 | 28211122wt2 |
|
||||
|
||||
|
||||
|
||||
当 Native App 启动的时候会从服务端请求一个接口,接口的返回一个 json 串,内容是 App 所包含的各个 H5 业务线的版本号和 md5 信息。
|
||||
|
||||
拿到 json 后和 App 本地保存的版本信息作比较,发现变动了则去请求相应的接口,接口返回 md5 对应的文件。Native 拿到后完成解压替换。
|
||||
|
||||
全部替换完毕后将这次接口请求到的资源版本号信息保存替换到 Native 本地。
|
||||
|
||||
|
||||
|
||||
因为是每个资源有版本号,所以如果线上的某个版本存在问题,那么可以根据相应的稳定的版本号回滚到稳定的版本。
|
||||
|
||||
|
||||
|
||||
|
||||
## 一些零散的解决方案
|
||||
|
||||
1. 静态直出
|
||||
|
||||
“直出”这个概念对前端同学来说,并不陌生。为了优化首屏体验,大部分主流的页面都会在服务器端拉取首屏数据后通过 NodeJs 进行渲染,然后生成一个包含了首屏数据的 Html 文件,这样子展示首屏的时候,就可以解决内容转菊花的问题了。
|
||||
当然这种页面“直出”的方式也会带来一个问题,服务器需要拉取首屏数据,意味着服务端处理耗时增加。
|
||||
不过因为现在 Html 都会发布到 CDN 上,WebView 直接从 CDN 上面获取,这块耗时没有对用户造成影响。
|
||||
手 Q 里面有一套自动化的构建系统 Vnues,当产品经理修改数据发布后,可以一键启动构建任务,Vnues 系统就会自动同步最新的代码和数据,然后生成新的含首屏 Html,并发布到 CDN 上面去。
|
||||
|
||||
我们可以做一个类似的事情,自动同步最新的代码和数据,然后生成新的含首屏 Html,并发布到 CDN 上面去
|
||||
|
||||
|
||||
2. 离线预推
|
||||
|
||||
页面发布到 CDN 上面去后,那么 WebView 需要发起网络请求去拉取。当用户在弱网络或者网速比较差的环境下,这个加载时间会很长。于是我们通过离线预推的方式,把页面的资源提前拉取到本地,当用户加载资源的时候,相当于从本地加载,即使没有网络,也能展示首屏页面。这个也就是大家熟悉的离线包。
|
||||
手 Q 使用 7Z 生成离线包, 同时离线包服务器将新的离线包跟业务对应的历史离线包进行 BsDiff 做二进制差分,生成增量包,进一步降低下载离线包时的带宽成本,下载所消耗的流量从一个完整的离线包(253KB)降低为一个增量包(3KB)。
|
||||
|
||||
|
||||
https://mp.weixin.qq.com/s?__biz=MzUxMzcxMzE5Ng==&mid=2247488218&idx=1&sn=21afe07eb642162111ee210e4a040db2&chksm=f951a799ce262e8f6c1f5bb85e84c2db49ae4ca0acb6df40d9c172fc0baaba58937cf9f0afe4&scene=27#wechat_redirect
|
||||
|
||||
|
||||
|
||||
3. 拦截加载
|
||||
|
||||
事实上,在高度定制的 wap 页面场景下,我们对于 webview 中可能出现的页面类型会进行严格控制。可以通过内容的控制,避免 wap 页中出现外部页面的跳转,也可以通过 webview 的对应代理方法,禁掉我们不希望出现的跳转类型,或者同时使用,双重保护来确保当前 webview 容器中只会出现我们定制过的内容。既然 wap 页的类型是有限的,自然想到,同类型页面大都由前端采用模板生成,页面所使用的 html、css、js 的资源很可能是同一份,或者是有限的几份,把它们直接随客户端打包在本地也就变得可行。加载对应的 url 时,直接 load 本地的资源。
|
||||
对于 webview 中的网络请求,其实也可以交由客户端接管,比如在你所采用的 Hybrid 框架中,为前端注册一个发起网络请求的接口。wap 页中的所有网络请求,都通过这个接口来发送。这样客户端可以做的事情就非常多了,举个例子,NSURLProtocol 无法拦截 WKWebview 发起的网络请求,采用 Hybrid 方式交由客户端来发送,便可以实现对应的拦截。
|
||||
基于上面的方案,我们的 wap 页的完整展示流程是这样:客户端在 webview 中加载某个 url,判断符合规则,load 本地的模板 html,该页面的内部实现是通过客户端提供的网络请求接口,发起获取具体页面内容的网络请求,获得填充的数据从而完成展示。
|
||||
|
||||
|
||||
NSURLProtocol能够让你去重新定义苹果的URL加载系统(URL Loading System)的行为,URL Loading System里有许多类用于处理URL请求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等。当URL Loading System使用NSURLRequest去获取资源的时候,它会创建一个NSURLProtocol子类的实例,你不应该直接实例化一个NSURLProtocol,NSURLProtocol看起来像是一个协议,但其实这是一个类,而且必须使用该类的子类,并且需要被注册。
|
||||
|
||||
|
||||
171
Chapter1 - iOS/1.45.md
Normal file
171
Chapter1 - iOS/1.45.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# NSTimer 中的内存泄露
|
||||
|
||||
- GCD 的 timer
|
||||
- NSProxy
|
||||
- 采用 Block 的形式为 NSTimer 增加分类
|
||||
|
||||
|
||||
|
||||
```objective-c
|
||||
@interface ViewController()
|
||||
@property (nonatomic, strong) NSTimer *timer;
|
||||
@end
|
||||
|
||||
@implementation ViewController
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1
|
||||
target:self
|
||||
selector:@selector(p_doSomeThing)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
|
||||
}
|
||||
|
||||
- (void)p_doSomeThing {
|
||||
// doSomeThing
|
||||
}
|
||||
|
||||
- (void)p_stopDoSomeThing {
|
||||
[self.timer invalidate];
|
||||
self.timer = nil;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self.timer invalidate];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
上面的代码主要是利用定时器重复执行 p_doSomeThing 方法,在合适的时候调用 p_stopDoSomeThing 方法使定时器失效。
|
||||
|
||||
能看出问题吗?在开始讨论上面代码问题之前,需要对 NSTimer 做一点说明。NSTimer 的 `scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:` 方法的最后一个参数为 YES 时,NSTimer 会保留目标对象,等到自身失效才释放目标对象。执行完任务后,一次性的定时器会自动失效;重复性的定时器,需要主动调用 invalidate 方法才会失效。
|
||||
|
||||
当前的 VC 和 定时器互相引用,造成循环引用。
|
||||
|
||||
如果能在合适的时机打破循环引用就不会有问题了
|
||||
|
||||
1. 控制器不再强引用定时器
|
||||
2. 定时器不再保留当前的控制器
|
||||
|
||||
```objective-c
|
||||
//.h文件
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface NSTimer (UnRetain)
|
||||
+ (NSTimer *)lbp_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval
|
||||
repeats:(BOOL)repeats
|
||||
block:(void(^)(NSTimer *timer))block;
|
||||
@end
|
||||
|
||||
//.m文件
|
||||
#import "NSTimer+SGLUnRetain.h"
|
||||
|
||||
@implementation NSTimer (SGLUnRetain)
|
||||
|
||||
+ (NSTimer *)lbp_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{
|
||||
|
||||
return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(lbp_blcokInvoke:) userInfo:[block copy] repeats:repeats];
|
||||
}
|
||||
|
||||
+ (void)lbp_blcokInvoke:(NSTimer *)timer {
|
||||
|
||||
void (^block)(NSTimer *timer) = timer.userInfo;
|
||||
|
||||
if (block) {
|
||||
block(timer);
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
//控制器.m
|
||||
|
||||
#import "ViewController.h"
|
||||
#import "NSTimer+UnRetain.h"
|
||||
|
||||
//定义了一个__weak的self_weak_变量
|
||||
#define weakifySelf \
|
||||
__weak __typeof(&*self)weakSelf = self;
|
||||
|
||||
//局域定义了一个__strong的self指针指向self_weak
|
||||
#define strongifySelf \
|
||||
__strong __typeof(&*weakSelf)self = weakSelf;
|
||||
|
||||
@interface ViewController ()
|
||||
|
||||
@property(nonatomic, strong) NSTimer *timer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ViewController
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
__block NSInteger i = 0;
|
||||
weakifySelf
|
||||
self.timer = [NSTimer lbp_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) {
|
||||
strongifySelf
|
||||
[self p_doSomething];
|
||||
NSLog(@"----------------");
|
||||
if (i++ > 10) {
|
||||
[timer invalidate];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)p_doSomething {
|
||||
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
// 务必在当前线程调用invalidate方法,使得Runloop释放对timer的强引用(具体请参阅官方文档)
|
||||
[self.timer invalidate];
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
上面的方法之所以能解决内存泄漏的问题,关键在于把保留转移到了定时器的类对象身上,这样就避免了实例对象被保留。
|
||||
|
||||
当我们谈到循环引用时,其实是指实例对象间的引用关系。类对象在 App 杀死时才会释放,在实际开发中几乎不用关注类对象的内存管理。下面的代码摘自苹果开源的 NSObject.mm 文件,从中可以看出,对于类对象,并不需要像实例对象那样进行内存管理。
|
||||
|
||||
```objective-c
|
||||
+ (id)retain {
|
||||
return (id)self;
|
||||
}
|
||||
|
||||
// Replaced by ObjectAlloc
|
||||
- (id)retain {
|
||||
return ((id)self)->rootRetain();
|
||||
}
|
||||
|
||||
+ (oneway void)release {
|
||||
}
|
||||
|
||||
// Replaced by ObjectAlloc
|
||||
- (oneway void)release {
|
||||
((id)self)->rootRelease();
|
||||
}
|
||||
|
||||
+ (id)autorelease {
|
||||
return (id)self;
|
||||
}
|
||||
|
||||
// Replaced by ObjectAlloc
|
||||
- (id)autorelease {
|
||||
return ((id)self)->rootAutorelease();
|
||||
}
|
||||
|
||||
+ (NSUInteger)retainCount {
|
||||
return ULONG_MAX;
|
||||
}
|
||||
|
||||
- (NSUInteger)retainCount {
|
||||
return ((id)self)->rootRetainCount();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
iOS 10 中,定时器 api 增加了 block 方法,实现原理与此类似,这里采用分类为 NSTimer 增加 block 参数的方法,最终的行为一致
|
||||
259
Chapter1 - iOS/1.46.md
Normal file
259
Chapter1 - iOS/1.46.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# KVC && KVO
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 字典快速赋值
|
||||
|
||||
KVC 可以将字典里面和 model 同名的 property 进行快速赋值 **setValuesForKeysWithDictionary**
|
||||
|
||||
```objective-c
|
||||
//前提:model 中的各个 property 必须和 NSDictionary 中的属性一致
|
||||
- (instancetype)initWithDic:(NSDictionary *)dic{
|
||||
BannerModel *model = [BannerModel new];
|
||||
[model setValuesForKeysWithDictionary:dic];
|
||||
return model;
|
||||
}
|
||||
```
|
||||
|
||||
但是这里会有2种特殊情况。
|
||||
|
||||
- 情况一:在 model 里面有 property 但是在 NSDictionary 里面没有这个值
|
||||
|
||||
运行上面的代码,代码不崩溃,只不过在输出值的时候输出了 null
|
||||
|
||||
- 情况二:在 NSDictionary 中存在某个值,但是在 model 里面没有值
|
||||
|
||||
运行后编译成功,但是代码奔溃掉。原因是 KVC 。所以我们只需要实现这么一个方法。甚至不需要写函数体部分
|
||||
|
||||
```
|
||||
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
- 情况三:如果 Dictionary 和 Model 中的 property 不同名
|
||||
|
||||
我们照样可以利用 **setValue:forUndefinedKey:** 去处理
|
||||
|
||||
|
||||
```objective-c
|
||||
//model
|
||||
@property (nonatomic,copy)NSString *name;
|
||||
@property (nonatomic,copy)NSString *sex;
|
||||
@property (nonatomic,copy) NSString* age;
|
||||
//NSDictionary
|
||||
NSDictionary *dic = @{@"username":@"张三",@"sex":@"男",@"id":@"22"};
|
||||
|
||||
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
|
||||
if([key isEqualToString:@"id"]){
|
||||
self.age=value;
|
||||
}
|
||||
if([key isEqualToString:@"username"]){
|
||||
self.name=value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 情况四:如果我们观察对象的属性是数组,我们经常会观察不到变化,因为 KVO 是观察 setter 方法。我们可以用 `mutableArrayValueForKeyPath` 进行属性的操作
|
||||
|
||||
```
|
||||
NSMutableArray *hobbies = [_person mutableArrayValueForKeyPath:@"hobbies"];
|
||||
[hobbies addObject:@"Web"];
|
||||
```
|
||||
|
||||
- 情况五: 注册依赖键.
|
||||
|
||||
KVO 可以观察属性的二级属性对象的所有属性变化。说人话就是“假如 Person 类有个 Dog 类,Dog 类有 name、fur、weight 等属性,我们给 Person 的 Dog 属性观察,假如 Dog 的任何属性变化是,Person 的观察者对象都可以拿到当前的变化值。我们只需要在 Person 中写下面的方法即可”
|
||||
|
||||
```
|
||||
[self.person addObserver:self
|
||||
forKeyPath:NSStringFromSelector(@selector(dog))
|
||||
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
|
||||
context:ContextMark];
|
||||
|
||||
self.person.dog.name = @"啸天犬";
|
||||
self.person.dog.weight = 50;
|
||||
|
||||
|
||||
// Person.m
|
||||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
|
||||
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
|
||||
|
||||
if ([key isEqualToString:@"dog"]) {
|
||||
NSArray *affectingKeys = @[@"name", @"fur", @"weight"];
|
||||
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
|
||||
}
|
||||
return keyPaths;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## KVO 的本质
|
||||
|
||||
kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础。
|
||||
|
||||
### 几个基本的知识点
|
||||
|
||||
1. KVO 观察者和属性被观察的对象之间不是强引用的关系
|
||||
|
||||
2. KVO 的触发分为`自动触发模式`和`手动触发模式`2种。通常我们使用的都是自动通知,注册观察者之后,当条件触发的时候会自动调用`-(void)observeValueForKeyPath...`
|
||||
|
||||
如果需要实现手动通知,我们需要使用下面的方法
|
||||
|
||||
```
|
||||
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
|
||||
return NO;
|
||||
}
|
||||
```
|
||||
3. 若类有实例变量 NSString *_foo, 调用 setValue:forKey: 是以 foo 还是 _foo 作为 key ?
|
||||
|
||||
都可以
|
||||
|
||||
4. KVC 的 keyPath 中的集合运算符如何使用
|
||||
|
||||
- 必须用在 **集合对象** 或者 **普通对象的集合属性** 上
|
||||
|
||||
-简单的集合运算符有 @avg、@count、@max、@min、@sum
|
||||
|
||||
5. KVO 和 KVC 的 keyPath 一定是属性吗?
|
||||
可以是成员变量
|
||||
|
||||
|
||||
|
||||
|
||||
### 实现机制
|
||||
|
||||
> Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
|
||||
|
||||
Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间类,而不是原来真正的类,
|
||||
|
||||
通过对被观察的对象断点调试发现 Person 类在执行过 addObserveValueForKeyPath... 方法后 isa 改变了。NSKVONotifying_Person。
|
||||
|
||||
- KVO 是基于 Runtime 机制实现的
|
||||
|
||||
- 当某个类的属性第一次被观察的时候,系统会在运行期动态的创建该类的一个派生类。在派生类中重写任何被观察属性的 setter 方法。派生类在真正实现`通知机制`
|
||||
|
||||
- 如果当前类为 Person,则生成的派生类名称为 `NSKVONotifying_Person`
|
||||
|
||||
- 每个类对象中都有一个 `isa指针` 指向当前类,当一个类对象第一次被观察的时候,系统会偷偷将 isa 指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是当前派生类的 `setter` 方法
|
||||
|
||||
- 键值观察通知依赖于 NSObject 的两个方法:`willChangeValueForKey:、didChangeValueForKey:` 。在一个被观察属性改变之前,调用 `willChangeValueForKey:` 记录旧的值。在属性值改变之后调用 `didChangeValueForKey:`,从而 `observeValueForKey:ofObject:change:context:` 也会被调用。
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
为什么要选择是继承的子类而不是分类呢?
|
||||
子类在继承父类对象,子类对象调用调方法的时候先看看当前子类中是否有方法实现,如果不存在方法则通过 isa 指针顺着继承链向上找到父类中是否有方法实现,如果父类种也不存在方法实现,则继续向上找...直到找到 NSObject 类为止,系统会抛出几次机会给程序员补救,如果未做处理则奔溃
|
||||
|
||||
关于分类与子类的关系可以看看我之前的 [文章](1.50.md).
|
||||
|
||||
|
||||
### 模拟实现系统的 KVO
|
||||
|
||||
1. 创建被观察对象的子类
|
||||
2. 重写观察对象属性的 set 方法,同时调用 `willChangeValueForKey、didChangeValueForKey`
|
||||
3. 外界改变 isa 指针(class方法重写)
|
||||
|
||||
我们用自己的类模拟系统的 KVO。
|
||||
```
|
||||
//NSObject+LBPKVO.h
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSObject (LBPKVO)
|
||||
|
||||
- (void)lbpKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
//NSObject+LBPKVO.m
|
||||
#import "NSObject+LBPKVO.h"
|
||||
#import <objc/message.h>
|
||||
|
||||
@implementation NSObject (LBPKVO)
|
||||
|
||||
|
||||
- (void)lbpKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
|
||||
//生成自定义的名称
|
||||
NSString *className = NSStringFromClass(self.class);
|
||||
NSString *currentClassName = [@"LBPKVONotifying_" stringByAppendingString:className];
|
||||
//1. runtime 生成类
|
||||
Class myclass = objc_allocateClassPair(self.class, [currentClassName UTF8String], 0);
|
||||
// 生成后不能马上使用,必须先注册
|
||||
objc_registerClassPair(myclass);
|
||||
|
||||
//2. 重写 setter 方法
|
||||
class_addMethod(myclass,@selector(setName:) , (IMP)setName, "v@:@");
|
||||
//3. 修改 isa
|
||||
object_setClass(self, myclass);
|
||||
|
||||
//4. 将观察者保存到当前对象里面
|
||||
objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_ASSIGN);
|
||||
|
||||
//5. 将传递的上下文绑定到当前对象里面
|
||||
objc_setAssociatedObject(self, "context", (__bridge id _Nullable)(context), OBJC_ASSOCIATION_RETAIN);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
void setName (id self, SEL _cmd, NSString *name) {
|
||||
NSLog(@"come here");
|
||||
//先切换到当前类的父类,然后发送消息 setName,然后切换当前子类
|
||||
//1. 切换到父类
|
||||
Class class = [self class];
|
||||
object_setClass(self, class_getSuperclass(class));
|
||||
//2. 调用父类的 setName 方法
|
||||
objc_msgSend(self, @selector(setName:), name);
|
||||
|
||||
//3. 调用观察
|
||||
id observer = objc_getAssociatedObject(self, "observer");
|
||||
id context = objc_getAssociatedObject(self, "context");
|
||||
if (observer) {
|
||||
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), @"name", self, @{@"new": name, @"kind": @1 } , context);
|
||||
}
|
||||
//4. 改回子类
|
||||
object_setClass(self, class);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
//ViewController.m
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
_person = [[Person alloc] init];
|
||||
_person.name = @"杭城小刘";
|
||||
_person.age = 23;
|
||||
_person.hobbies = [@[@"iOS"] mutableCopy];
|
||||
NSDictionary *context = @{@"name": @"成吉思汗", @"hobby" : @"弯弓射大雕"};
|
||||
[_person lbpKVO_addObserver:self forKeyPath:@"hobbies" options:(NSKeyValueObservingOptionNew) context:(__bridge void * _Nullable)(context)];
|
||||
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
|
||||
_person.name = @"刘斌鹏";
|
||||
NSMutableArray *hobbies = [_person mutableArrayValueForKeyPath:@"hobbies"];
|
||||
[hobbies addObject:@"Web"];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
|
||||
NSLog(@"%@",change);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### KVO 的缺陷
|
||||
|
||||
KVO 虽然很强大,你只能重写 `-observeValueForKeyPath:ofObject:change:context: ` 来获得通知,想要提供自定义的 selector ,不行;想要传入一个 block,没门儿。感觉如果加入 block 就更棒了。
|
||||
|
||||
### KVO 的改装
|
||||
|
||||
看到官方的做法并不是很方便使用,我们看到无数的优秀框架都支持 block 特性,比如 AFNetworking ,所以我们可以将系统的 KVO 改装成支持 block。
|
||||
|
||||
|
||||
|
||||
|
||||
128
Chapter1 - iOS/1.47.md
Normal file
128
Chapter1 - iOS/1.47.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# 金融 App 金额格式化
|
||||
|
||||
> 在一些金融类的 App 中,对于表示金额类的字符串,通常需要进行格式化后再显示出来。例如:
|
||||
>
|
||||
> 0 显示为:0.00
|
||||
>
|
||||
> 123 显示为:123.00
|
||||
>
|
||||
> 123.456 显示为:123.46
|
||||
>
|
||||
> 102000 显示为:102,000.00
|
||||
>
|
||||
> 10204500 显示为:10,204,500.00
|
||||
>
|
||||
> 它的规则为:个位数起每隔三位数字添加一个逗号,同时保留两位小数,也称为“千分位格式”。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 方法一
|
||||
|
||||
首先根据小数点 `.` 将传入的字符串分割为两部分,整数部分和小数部分(如果没有小数点,则补 `.00`,如果有多个小数点则报金额格式错误)。对于小数部分,只取前两位;然后对整数部分字符串进行遍历,从右到左,每三位数前插入一个逗号 `,`,最后再把两部分拼接起来,代码大致如图 1 和图 2 所示。
|
||||
|
||||
|
||||
|
||||
```
|
||||
- (void)method1{
|
||||
NSArray *temps = @[@"0",@"123",@"123.456",@"102000",@"10204500"];
|
||||
self.pricelabel.text = [self moneyFromat:temps[arc4random()%5]];
|
||||
}
|
||||
|
||||
- (NSString *)moneyFromat:(NSString *)money{
|
||||
if (!money || money.length == 0) {
|
||||
return money;
|
||||
}
|
||||
|
||||
BOOL hasPoint = NO;
|
||||
if ([money rangeOfString:@"."].length > 0) {
|
||||
hasPoint = YES;
|
||||
}
|
||||
|
||||
|
||||
NSMutableString *pointMoney = [NSMutableString stringWithString:money];
|
||||
if (hasPoint == NO) {
|
||||
[pointMoney appendFormat:@".00"];
|
||||
}
|
||||
|
||||
|
||||
NSArray *moneys = [pointMoney componentsSeparatedByString:@"."];
|
||||
if (moneys.count > 2) {
|
||||
return pointMoney;
|
||||
}
|
||||
else if (moneys.count == 1) {
|
||||
return [NSString stringWithFormat:@"%@.00",moneys[0]];
|
||||
}
|
||||
else {
|
||||
//整数部分:每隔3位插入一个“,”
|
||||
NSString *frontMoney = [self stringFromToThreeBit:moneys[0]];
|
||||
if ([frontMoney isEqualToString:@""]) {
|
||||
frontMoney = @"0";
|
||||
}
|
||||
//拼接整数部分和消暑部分
|
||||
NSString *backMoney = moneys[1];
|
||||
if (backMoney.length == 1) {
|
||||
return [NSString stringWithFormat:@"%@.%@",frontMoney,backMoney];
|
||||
}
|
||||
else if (backMoney.length > 2) {
|
||||
return [NSString stringWithFormat:@"%@.%@",frontMoney,[backMoney substringToIndex:2]];
|
||||
}
|
||||
else {
|
||||
return [NSString stringWithFormat:@"%@.%@",frontMoney,backMoney];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)stringFromToThreeBit:(NSString *)string{
|
||||
NSString *tempString = [string stringByReplacingOccurrencesOfString:@"," withString:@""];
|
||||
NSMutableString *mutableString = [NSMutableString stringWithString:tempString];
|
||||
NSInteger n = 2;
|
||||
if (mutableString.length > 3) {
|
||||
for (NSUInteger i = mutableString.length - 3; i > 0; i--) {
|
||||
n++;
|
||||
|
||||
if (n == 3) {
|
||||
[mutableString insertString:@"," atIndex:i];
|
||||
n = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mutableString;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 方法二
|
||||
|
||||
其实,苹果提供了 NSNumberFormatter 用来处理 NSString 和 NSNumber 之间的转化,可以满足基本的数字形式的格式化。我们通过设置 NSNumberFormatter 的 `numberStyle` 和 `positiveFormat` 属性,即可实现上述功能,非常简洁。
|
||||
|
||||
|
||||
|
||||
```objective-c
|
||||
- (void)method2{
|
||||
NSArray *temps = @[@"0",@"123",@"123.456",@"102000",@"10204500"];
|
||||
self.pricelabel.text = [self formatDecimalNumber:temps[arc4random()%5]];
|
||||
}
|
||||
|
||||
- (NSString *)formatDecimalNumber:(NSString *)string{
|
||||
if (!string || string.length == 0) {
|
||||
return string;
|
||||
}
|
||||
NSNumber *number = @([string doubleValue]);
|
||||
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
|
||||
formatter.numberStyle = kCFNumberFormatterDecimalStyle;
|
||||
formatter.positiveFormat = @"###,##0.00";
|
||||
formatter.positiveFormat = @"###,##0.00";
|
||||
NSString *amountString = [formatter stringFromNumber:number];
|
||||
return amountString;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### [参考资料](https://www.jianshu.com/p/817029422a72)
|
||||
|
||||
|
||||
|
||||
215
Chapter1 - iOS/1.48.md
Normal file
215
Chapter1 - iOS/1.48.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# 类别(Category)、拓展(Extension)
|
||||
|
||||
> 很多人都知道类别、分类的用法,但是对于一些细节就不是很清楚了,本文主要梳理下这3个概念的细节
|
||||
|
||||
## 类别(Category)
|
||||
|
||||
### 文件特征
|
||||
- 类别文件有2个,分别为 .h 和 .m
|
||||
- 命名为: “类名+类别名.h”和“类名+类别名.m”
|
||||
|
||||
### 文件内容格式
|
||||
.h 文件格式
|
||||
```
|
||||
#import "类名.h"
|
||||
|
||||
@interface 类名 (类别名)
|
||||
// 在此处声明方法
|
||||
@end
|
||||
```
|
||||
|
||||
.m 文件格式
|
||||
```
|
||||
#import "类名+类别名.h"
|
||||
|
||||
@implementation 类名 (类别名)
|
||||
// 在此处实现声明的方法
|
||||
@end
|
||||
```
|
||||
|
||||
### 类别的作用
|
||||
|
||||
拓展当前类,为类添加方法
|
||||
|
||||
### 类别的局限性
|
||||
- 无法向现有的类添加实例变量(编译器报“instance variables may not be placed in categories”)。Category 一般只为类提供方法的拓展,不提供属性的拓展。但是利用 Runtime 可以在 Category 中添加属性
|
||||
|
||||
- 方法名称冲突的情况下,如果 Category 中的方法与当前类的方法名称重名,Category 具有更高的优先级,类别中的方法将完全取代现有类中的方法(调用方法的时候不会去调用现有类里面的方法实现)。
|
||||
|
||||
- 当现有类具有多个 Category 的时候,如果每个 Category 都有同名的方法,那么在调用方法的时候肯定不会调用现有类的方法实现。系统根据编译顺序决定调用哪个 Category 下的方法实现。(可以在 Targets -> Build phases -> Compile Sources 下给多个 Category 更换顺序看看到底在执行哪个方法)
|
||||
|
||||
|
||||
### Category 的使用和注意
|
||||
1. Category 中的方法如果和现有类方法一致,工程中任何调用当前类的方法的时候都会去调用 Category 里面的方法(比如:UIViewCtroller、UITableView这些)的方法时要慎重。因为用Category重写类中的方法会对子类造成很大的影响。比如:用Category 重写了 UIViewCtroller 的方法 A,那么如果你在工程中用到的所有继承自 UIViewCtroller 的子类,去调用方法 A 时,执行的都是 Category 中重写的方法 A,如果不幸的是,你写的方法 A 有 Bug,那么会造成整个工程中调用该方法的所有 UIViewCtroller 子类的不正常。除非你在子类中重写了父类的方法 A,这样子类调用方法 A 时是调用的自己重写的方法 A,消除了父类 Category 中重写方法对自己的影响
|
||||
|
||||
2. Category拓展方法按照有没有重写当前类中的方法,分为未重写的拓展方法和重写拓展方法。且类引用自己的 Category 时,只能在 .m 文件中引用(.h 文件引用自己的类别会报错)。子类引用父类的 Category 在 .h 或 .m 都可以。如果类调用 Category 中重写的方法,不用引入 Category 头文件,系统会自动调用 Category 中的重写方法
|
||||
|
||||
3. Category 中如果重写了 A 类从父类继承来的某方法,不会影响与 A 同层级的 B 类
|
||||
|
||||
4. 子类会不会继承父类的 Category: Category 中重写的方法会对子类造成影响,但是子类不会继承非重写的方法(现有类中没有的方法)。但是在子类中引入父类 Category 的声明文件后,子类就会继承 Category 的非重写方法。继承的表现是:当子类的方法和父类 Category 中的方法名完全相同,那么子类里的方法会覆盖掉父类 Category,相当于子类重写了继承自父类的方法
|
||||
|
||||
5. Category 的作用是向下有效的。即只会影响到该类的所有子类。比如 A 类和 B 类是继承自 Super 类的2个子类,当给 A 类添加一个 Category sayHello 方法,仅有A 类的子类才可以使用 sayHello 方法
|
||||
|
||||
## 拓展(Extension)
|
||||
|
||||
### 文件特征
|
||||
- 只存在一个文件
|
||||
- 命名方式:“类名_拓展名.h”
|
||||
```
|
||||
#import "类名.h"
|
||||
|
||||
@interface 类名 ()
|
||||
// 在此添加私有成员变量、属性、声明方法
|
||||
@end
|
||||
```
|
||||
|
||||
### 拓展的作用
|
||||
1. 为类增加额外的属性、成员变量、方法声明
|
||||
|
||||
2. 一般将类拓展直接写到当前类的 .m 文件中。不单独创建
|
||||
|
||||
3. 一般私有的属性和方法写到类拓展中
|
||||
|
||||
4. 和 Category 类似,但是小括号里面没有拓展的名字
|
||||
|
||||
|
||||
### 拓展的局限性
|
||||
1. Extension 中添加的属性、成员变量、方法属于私有(只可以在本类的 .m 文件中访问、调用。在其他类里面是无法访问的,同时子类也是无法继承的)。假如我们有这样一个需求,一个属性对外是只读的,对内是可以读写的,那么我们可以通过 Extension 实现。
|
||||
|
||||
2. 通常 Extension 都写在 .m 文件中,不会单独建立一个 Extension 文件。而且 Extension 必须写到 @implementation 上方,否则编译报错
|
||||
|
||||
3. 类拓展定义的方法和属性必须在类的实现文件中实现。如果单独定义类扩展的文件并且只定义属性的话,也需要将类实现文件中包含进类扩展文件,否则会找不到属性的 setter 和 getter 方法。
|
||||
|
||||
```
|
||||
//Web.h
|
||||
#import "Person.h"
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@interface Web : Person
|
||||
@end
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
//Web.m
|
||||
#import "Web.h"
|
||||
#import "Web+H5.h"
|
||||
@interface Web ()
|
||||
@property (nonatomic, strong) NSString *skillStacks;
|
||||
@end
|
||||
@implementation Web
|
||||
- (void)test {
|
||||
self.skills = @"iOS && Web && Node && Hybrid";
|
||||
self.skillStacks = @"iOS && Web && Node && Hybrid";
|
||||
}
|
||||
- (void)show {
|
||||
NSLog(@"%@",self.skillStacks);
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
## 总结
|
||||
1. Category 只能拓充方法,不能拓展属性和成员变量(包含成员变量会报错。属性虽然不可以直接拓展,利用 Runtime 可以实现)
|
||||
|
||||
2. 如果 Category 中声明了1个属性,那么 Category 只会生成 setter 和 getter 的声明,不会有实现
|
||||
|
||||
3. Extension 也被成为匿名的 Category
|
||||
|
||||
4. 分类的方法本质是追加在当前类方法列表后,所以分类的方法会覆盖当前类的方法。
|
||||
|
||||
|
||||
##「小插曲」:为 Category 实现属性的 Setter 和 Getter
|
||||
```
|
||||
#import "Person.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface Person (Student)
|
||||
|
||||
|
||||
/**< 学号*/
|
||||
@property (nonatomic, strong) NSString *studyNumber;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
#import "Person+Student.h"
|
||||
#import <objc/message.h>
|
||||
|
||||
@implementation Person (Student)
|
||||
|
||||
- (void)sayHi {
|
||||
NSLog(@"大家好,我叫%@,我今年%zd岁了",self.name,self.age);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* 传统的做法是在 setter 里面这样写
|
||||
_studyNumber = studyNumber;
|
||||
ARC 自动管理内存
|
||||
MRC
|
||||
[_studyNumber release];
|
||||
[studyNumber retain];
|
||||
_studyNumber = studyNumber;
|
||||
|
||||
但是在 Category里面不会生成对应的实例变量,因此我们可以利用 Runtime 为我们的 category 关联属性的值
|
||||
setter:objc_setAssociatedObject(self, @selector(firstView), firstView, OBJC_ASSOCIATION_RETAIN);
|
||||
getter:objc_getAssociatedObject(self, @selector(firstView));
|
||||
}
|
||||
*/
|
||||
- (void)setStudyNumber:(NSString *)studyNumber {
|
||||
objc_setAssociatedObject(self, @selector(studyNumber), studyNumber
|
||||
, OBJC_ASSOCIATION_RETAIN);
|
||||
}
|
||||
|
||||
//@selector(studyNumber)
|
||||
- (NSString *)studyNumber {
|
||||
return objc_getAssociatedObject(self, @selector(studyNumber));
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
说明: `objc_setAssociatedObject` 的第二个参数是`const void * _Nonnull key` 所以可以用 "studyNumber" 或者利用 `@selector()` 的特性返回的数据类型也满足,所以示例代码选用第二种方式
|
||||
|
||||
给分类添加属性的时候,为了避免多人开发对于属性添加造成的覆盖,我们需要为属性起一个独特的名字。比如我们的工程是组件化、模块化开展的工程,那么我们可以为属性命名的时候在前面添加当前模块的前缀。
|
||||
|
||||
比如我们在 Login-Register-Module 模块为 NSURL 的 Category 添加一个 title 的属性的时候,可以这样命名 LR_Title。请查看下面的代码
|
||||
|
||||
```Objective-c
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSURL (Title)
|
||||
|
||||
@property (nonatomic, copy) NSString *LR_title;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
#import "NSURL+Title.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@implementation NSURL (Title)
|
||||
|
||||
- (void)setLR_title:(NSString *)LR_title
|
||||
{
|
||||
objc_setAssociatedObject(self, @selector(LR_title), LR_title
|
||||
, OBJC_ASSOCIATION_RETAIN);
|
||||
}
|
||||
|
||||
- (NSString *)LR_title
|
||||
{
|
||||
return objc_getAssociatedObject(self, @selector(LR_title));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
163
Chapter1 - iOS/1.49.md
Normal file
163
Chapter1 - iOS/1.49.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# MVC、MVP、MVVM
|
||||
|
||||
## MVC
|
||||
|
||||
MVC 模式下,软件被划分为视图(View:用户界面)、控制器(Controller:业务逻辑)、模型(Model:数据保存)
|
||||
|
||||

|
||||
|
||||
1. 用户操作 View,在 View 上面的事件都将被传递到 Controller 处理
|
||||
2. Controller 处理事件、请求网络,操作 Model 更新状态
|
||||
3. Model 将更新后的数据发送到 View,用户得到反馈
|
||||
|
||||
所有的通信都是单向的。
|
||||
|
||||
|
||||
## MVP
|
||||
|
||||
MVP 模式将 Controller 改名为 Presenter,通信改变了通信方向
|
||||
|
||||

|
||||
|
||||
1. 各部分之间的通信都是双向的
|
||||
2. Model 与 View 不发生联系,都通过 Presenter 传递
|
||||
3. View 层非常薄。不部署任何业务逻辑,称为“被动视图(Passive View)”,即没有任何主动性,而 Presenter 非常厚,所有的逻辑都部署在这层
|
||||
|
||||
## MVVM
|
||||
|
||||
MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。
|
||||
|
||||

|
||||
|
||||
区别在于:采用双向绑定的模式。View 的改变,自动反应在 ViewModel 上。 ViewModel 的改变会发应在 View 上。
|
||||
|
||||
|
||||
MVC 等到业务逻辑很复杂的时候被称为 Massive View Controller (重量级视图控制器)。这样的 Controller 后期维护看着阅读成本很高、不易于测试、维护。当时写这个代码的人离职后,几千行规模的代码在后期添加新功能或者修改 Bug 都是一件折磨人的事情。
|
||||
|
||||

|
||||
|
||||
看到 View 和 ViewController 是不同的技术组件,但是日常开发中它们总是成对的存在,为什么不考虑将他们的连接正规化呢?
|
||||
|
||||

|
||||
|
||||
典型的 MVC 存在弊端就是 Controller 层非常复杂,很多逻辑都在里面,包括一些不是逻辑的“表示逻辑”(presentation logic)。用 MVVM 术语来说就是将那些 Model 数据转换为 View 可以呈现的东西。例如将一个 NSDate 格式化为一个“2018-11-17” 这样的 NSString。
|
||||
|
||||
|
||||
上图中缺少一个环节,一个专门用来处理所有的表示逻辑。称为 “ViewModel”。位于 ViewController 与 Model 之间。
|
||||
|
||||

|
||||
|
||||
MVVM 就是 MVC 的增强版,将展示层逻辑单独拎出来,即 ViewModel。iOS 使用 MVVM 可以降低 ViewController 的复杂性并使得表示逻辑易于测试。
|
||||
|
||||
|
||||
- MVVM 兼容当下的 MVC 机构
|
||||
- MVVM 增加应用的可测试性
|
||||
- MVVM 配合一个绑定机制效果最好
|
||||
|
||||
## 一个简单的例子
|
||||
PersonModel
|
||||
```
|
||||
@interface Person : NSObject
|
||||
|
||||
- (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate;
|
||||
|
||||
@property (nonatomic, readonly) NSString *salutation;
|
||||
@property (nonatomic, readonly) NSString *firstName;
|
||||
@property (nonatomic, readonly) NSString *lastName;
|
||||
@property (nonatomic, readonly) NSDate *birthdate;
|
||||
|
||||
@end
|
||||
```
|
||||
PersonViewController
|
||||
```
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
if (self.model.salutation.length > 0) {
|
||||
self.nameLabel.text = [NSString stringWithFormat:@"%@ %@ %@", self.model.salutation, self.model.firstName, self.model.lastName];
|
||||
} else {
|
||||
self.nameLabel.text = [NSString stringWithFormat:@"%@ %@", self.model.firstName, self.model.lastName];
|
||||
}
|
||||
|
||||
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||||
[dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
|
||||
self.birthdateLabel.text = [dateFormatter stringFromDate:model.birthdate];
|
||||
}
|
||||
```
|
||||
上面是标准的 MVC。现在我们考虑用 ViewModel 增加改进下
|
||||
|
||||
PersonViewModel
|
||||
```
|
||||
@interface PersonViewModel : NSObject
|
||||
|
||||
- (instancetype)initWithPerson:(Person *)person;
|
||||
|
||||
@property (nonatomic, readonly) Person *person;
|
||||
|
||||
@property (nonatomic, readonly) NSString *nameText;
|
||||
@property (nonatomic, readonly) NSString *birthdateText;
|
||||
- (instancetype)initWithPerson:(Person *)person;
|
||||
@end
|
||||
|
||||
|
||||
@implementation PersonViewModel
|
||||
|
||||
- (instancetype)initWithPerson:(Person *)person {
|
||||
self = [super init];
|
||||
if (!self) return nil;
|
||||
|
||||
_person = person;
|
||||
if (person.salutation.length > 0) {
|
||||
_nameText = [NSString stringWithFormat:@"%@ %@ %@", self.person.salutation, self.person.firstName, self.person.lastName];
|
||||
} else {
|
||||
_nameText = [NSString stringWithFormat:@"%@ %@", self.person.firstName, self.person.lastName];
|
||||
}
|
||||
|
||||
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||||
[dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
|
||||
_birthdateText = [dateFormatter stringFromDate:person.birthdate];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
此时,我们的 ViewController 会很轻量
|
||||
```
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.nameLabel.text = self.viewModel.nameText;
|
||||
self.birthdateLabel.text = self.viewModel.birthdateText;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
可测试?View Controller 是出了名的难测试。因为传动的 VC 很重,在 MVVM 的世界里,代码各司其职,测试 View Controller 变得较为容易
|
||||
```
|
||||
SpecBegin(Person)
|
||||
NSString *salutation = @"Dr.";
|
||||
NSString *firstName = @"first";
|
||||
NSString *lastName = @"last";
|
||||
NSDate *birthdate = [NSDate dateWithTimeIntervalSince1970:0];
|
||||
|
||||
it (@"should use the salutation available. ", ^{
|
||||
Person *person = [[Person alloc] initWithSalutation:salutation firstName:firstName lastName:lastName birthdate:birthdate];
|
||||
PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
|
||||
expect(viewModel.nameText).to.equal(@"Dr. first last");
|
||||
});
|
||||
|
||||
it (@"should not use an unavailable salutation. ", ^{
|
||||
Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate];
|
||||
PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
|
||||
expect(viewModel.nameText).to.equal(@"first last");
|
||||
});
|
||||
|
||||
it (@"should use the correct date format. ", ^{
|
||||
Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate];
|
||||
PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
|
||||
expect(viewModel.birthdateText).to.equal(@"Thursday January 1, 1970");
|
||||
});
|
||||
SpecEnd
|
||||
```
|
||||
|
||||
70
Chapter1 - iOS/1.5.md
Normal file
70
Chapter1 - iOS/1.5.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# 事件响应者链
|
||||
|
||||
|
||||
实验1:
|
||||
|
||||
定义 BaseView,在里面实现方法touchBegan,监听当前哪个类调用了该方法。
|
||||
|
||||
在控制器的界面上加5个颜色不同的view,每个view自定义view去实现,因此在不同的view上的手势就可以由不同的view拦截到。
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
```
|
||||
//BaseView
|
||||
#import "BaseView.h"
|
||||
|
||||
@implementation BaseView
|
||||
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
|
||||
NSLog(@"%@",[self class]);
|
||||
}
|
||||
```
|
||||
|
||||
结果:点击不同的View打印出不同的类名。
|
||||
|
||||
结论:
|
||||
|
||||
* 触摸事件是从父控件传递到子控件的。
|
||||
* 点击了绿色(图上的2级)的view:UIApplication-> UIWindow -> UIViewController的view -> 绿色的view
|
||||
* 点击了蓝色(图上的3级)的view:UIApplication-> UIWindow -> UIViewController的view -> 红棕色的view -> 蓝色的view
|
||||
* 点击了黄色(图上的4级)的view:UIApplication -> UIWindow -> UIViewController的view -> 红棕色的view -> 蓝色的view -> 黄色的view
|
||||
|
||||
注意:如果父控件不能接收触摸事件,那么这个父控件的子控件也不能接收触摸事件
|
||||
|
||||
#### 如何找到最合适的控件来接收触摸事件?
|
||||
|
||||
* 自己能否接收触摸事件?
|
||||
* 触摸点是否在自己身上?
|
||||
* 从后往前遍历子控件,重复前面2个步骤
|
||||
* 如果没有符合条件的子控件,那么就自己最适合处理
|
||||
|
||||
|
||||
# 事件响应原理
|
||||
|
||||
产生的touch方法的默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者处理。
|
||||
|
||||
#### 响应者链条
|
||||
|
||||

|
||||
|
||||
#### 事件传递的完整过程
|
||||
|
||||
1. 先将事件对象由上往下传递(父控件传递给子控件),找到最合适的控件来处理
|
||||
2. 调用最合适控件的touch方法
|
||||
3. 如果调用了\[super touches...\]方法就会将事件顺着响应者链条向上传递,传递给上一个响应者
|
||||
4. 接着就会调用上一个响应者的touches...方法
|
||||
|
||||
#### 事件响应者
|
||||
|
||||
##### 如何判断该控件的上一个响应者?
|
||||
|
||||
1. 如果当前这个view是控制器的view,那么上一个响应者就是控制器
|
||||
2. 如果当前这个view不是控制器的view,那么上一个响应者就是父控件。
|
||||
|
||||
事件传递给UIApplication后如果不处理的话,该事件会销毁掉。
|
||||
|
||||
控制器view上的子控件的touch...方法如果子控件不处理那么都会顺着响应者链条向上传递给上一层响应者对象,比如可以交给控制器处理。
|
||||
|
||||
|
||||
|
||||
25
Chapter1 - iOS/1.50.md
Normal file
25
Chapter1 - iOS/1.50.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 动态库和静态库
|
||||
|
||||
通常,我们的 Xcode 工程会依赖一些第三方库,包括:.a 静态库(Static Library)和 .framework 动态库(Dynamic Library)。
|
||||
|
||||
不过简单地把 .framework 后缀的文件称为“动态库”并不严谨,因为在 iOS/macOS 开发中,framework 又分为 “静态 framework” 和 “动态 framework”,区别如下:
|
||||
|
||||
* 静态 framework:可以理解为是 .a 静态文件 + .h 公共头文件 + 资源文件 的集合,本质上与 .a 静态库是一致的;
|
||||
|
||||
* 动态 framework:即真正意义上的动态库,一般包括动态二进制文件、头文件和资源文件等。
|
||||
|
||||
对于一个 Static Library 工程,其编译产物为 .a 静态二进制文件 + 公共 .h 头文件;
|
||||
|
||||
对于一个 Framework 工程,其编译的最终产物是动态库还是静态库,我们可以通过在 Build Settings -> Linking -> Mach-O Type 中进行选择设置其值为 Dynamic Library 或者 Static Library。
|
||||
|
||||
此外,我们知道,对于一个 Mach-O 二进制文件,不管是 static 还是 dynamic,一般都包含了几种不同的处理器架构(Architectures),例如:i386, x86_64, armv7, armv7s, arm64 等。
|
||||
|
||||
Xcode 在编译链接时,对于静态库和动态库的处理方式是不同的。
|
||||
|
||||
对于静态库,在链接时(Linking Time),Xcode 会自动筛选出静态库中的不同 architecture 合并到对应处理器架构的主可执行二进制文件中;而在打包归档(Archive)时,Xcode 会自动忽略掉静态库中未用到的 architecture,例如会移除掉 i386, x86_64 等 Mac 上模拟器专用的架构。
|
||||
|
||||
而对于动态库,在编译打包时,Xcode 会直接拷贝整个动态 framework 文件到最终的 .ipa 包中,只有在 App 真正启动运行时,才会进行动态链接。但是苹果是不允许最终上传到 App Store Connect 后台的 .ipa 文件包含 i386, x86_64 等模拟器架构的,会报 Invalid 错误,所以对于工程中的动态 framework,我们在打 Release 正式包时,一般会通过执行命令或者脚本的方式移除掉这些 Invalid Architectures。
|
||||
|
||||
最后,如何在 Xcode 工程中添加这些静态/动态库呢?
|
||||
|
||||
对于 “.a 静态库” 和 “静态 framework” ,直接拖拽到工程中,并勾选 Copy if needed 选项即可,无需其他设置。
|
||||
205
Chapter1 - iOS/1.51.md
Normal file
205
Chapter1 - iOS/1.51.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# Pod
|
||||
|
||||
## 1. 组件的地址
|
||||
|
||||
我们在做组件化的时候经常将一些业务模块封装打包,做成 pod 管理的形式,然后当在开发的时候需要修改一些模块化的代码。
|
||||
|
||||
当维护好组件的时候我们可能在一个新的工程设置好 podfile 引入组件,但是有可能需要继续修改组件的源代码,代码需要可编辑。所以我们可能需要将 Podfile 中的 pod 源修改为本地。
|
||||
然后安装 pod install 后就可以看到在项目文件里面有可编辑的组件代码
|
||||
|
||||
```
|
||||
pod 'GoodsCategoryModule', :path => '../GoodsCategoryModule'
|
||||
#pod 'GoodsCategoryModule',:git => 'git@gitlab.xxx.com:forntend_ios/GoodsCategoryModule.git',:branch => 'release/Appstore'
|
||||
```
|
||||
|
||||
注意点:
|
||||
1. 我们本地的组件源代码需要和 pod 文件中的代码工程文件名称一致
|
||||
2. 注释掉远端仓库的地址
|
||||
path 后面是相对路径
|
||||
|
||||
## 2. 组件库使用 pch 头文件
|
||||
|
||||
```Objective-c
|
||||
///一个区分开发和线上环境的Log。NSLog的本质是调用对象方法的 description 方法,所以线上代码使用NSLog会造成性能和安全问题
|
||||
#ifdef DEBUG
|
||||
#define SafeLog(...) NSLog(__VA_ARGS__)
|
||||
#else
|
||||
#define SafeLog(...)
|
||||
#endif
|
||||
```
|
||||
|
||||
所以我们需要对各个组件库里面的 **NSLog** 进行改造,变成 **SafeLog**。没做特殊处理,当你在 pod 库的 pch 文件写好代码,发现主工程执行 pod install 之后,看到之前写的 SafeLog 代码不见了。所以我们需要指定 pch 文件。
|
||||
操作:
|
||||
- 新建 **PrefixHeader.h** 头文件
|
||||
- 在当前 pod 库的 ***.podspec 文件中写如下代码
|
||||
```Ruby
|
||||
s.prefix_header_contents = '#import "PrefixHeader.h"'
|
||||
```
|
||||
说明:该代码的作用相当于将 PrefixHeader.h 文件写入到当前 pod 库 XQTriggerKit-prefix.pch 文件的最后一行。相当于
|
||||
```Objective-c
|
||||
#ifdef __OBJC__
|
||||
#import <UIKit/UIKit.h>
|
||||
#else
|
||||
#ifndef FOUNDATION_EXPORT
|
||||
#if defined(__cplusplus)
|
||||
#define FOUNDATION_EXPORT extern "C"
|
||||
#else
|
||||
#define FOUNDATION_EXPORT extern
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#import "PrefixHeader.h"
|
||||
```
|
||||
|
||||
缺点:项目存在多个 pod 组件库。主工程修改 pch 还好,但是每个 pod 库都去新建 PrefixHeader.h 文件和 podspec 文件添加一行代码。工作量非常大。
|
||||
|
||||
改进方案:对 NSLog 进行 Hook。
|
||||
|
||||
步骤:
|
||||
- 引入 fishhook 库
|
||||
- 新建 SafeLog.h 和 SafeLog.m 文件
|
||||
- 在 SafeLog.m 文件的 load 方法中对 NSLog 进行 hook,判断是 Debug 环境就打印。代码如下
|
||||
|
||||
```Objective-c
|
||||
#import "SafeLog.h"
|
||||
#import "fishhook.h"
|
||||
|
||||
// orig_NSLog是原有方法被替换后 把原来的实现方法放到另一个地址中
|
||||
// new_NSLog就是替换后的方法了
|
||||
static void (*orig_NSLog)(NSString *format, ...);
|
||||
|
||||
@implementation SafeLog
|
||||
|
||||
void(new_NSLog)(NSString *format, ...)
|
||||
{
|
||||
va_list args;
|
||||
if (format) {
|
||||
va_start(args, format);
|
||||
NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
|
||||
#ifdef DEBUG
|
||||
orig_NSLog(@"%@", message);
|
||||
#endif
|
||||
va_end(args);
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)load
|
||||
{
|
||||
rebind_symbols((struct rebinding[1]){ {"NSLog", new_NSLog, (void *)&orig_NSLog} }, 1);
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
## 3. CocoPods 指定版本号带 ~> 与不带的区别
|
||||
|
||||
- 带 ~> 是指库版本号的一个范围。大于等于指定的版本号,小于高一位的版本号
|
||||
- 不带 ~> 是指固定的版本号
|
||||
|
||||
```
|
||||
pod 'aaa', '~> 0.1.2' // 大于等于 0.1.2 且小于 0.2
|
||||
pod 'bbb', '1.1' 版本号指定为 1.1
|
||||
|
||||
```
|
||||
|
||||
## 4. 查看当前 Pod 库被何处引用
|
||||
|
||||
出于某种原因经常会需要查看当前 Pod 库被何处引用了(你需要修改组件库A,然后A修改完之后,可能需要在依赖组件库A的地方去修改版本号。这时候就需要查询了,例子可能不优雅,但是确实有需要查询引用的情况),有轮子
|
||||
|
||||
- 先下载所需要的库
|
||||
```Shell
|
||||
gem install reversepoddependency
|
||||
```
|
||||
- 利用脚本在我们的组件库 repo 中去查询
|
||||
```
|
||||
specbackwarddependency /Users/liubinpeng/.cocoapods/repos/51xianqu-xq_specs(本地CocoPod repo地址) xq_baaidumapkit(组件库名称)
|
||||
```
|
||||
|
||||
鉴于这个命令比较长,且本人使用的是 iTerm2+Zsh,所以将命令写入到 .zshrc 文件中, source 编译链接过可以快速使用。例如 `repoanalysis xq_baaidumapkit`
|
||||
```Shell
|
||||
alias repoanalysis='specbackwarddependency /Users/liubinpeng/.cocoapods/repos/51xianqu-xq_specs'
|
||||
```
|
||||
|
||||

|
||||
|
||||
具体的操作步骤可以参考我的这个[文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/第六部分%20开发杂谈/6.11.md)
|
||||
|
||||
|
||||
## 5. lint 的时候安装一些神奇的依赖
|
||||
|
||||
|
||||
在对 App 做应用包瘦身的时候发现了一些问题。某个组件库 lint 的时候通过终端的信息,发现安装了一些不是 podspec 里面指定的依赖仓库。百思不得其解,同事说可能是之前的某个版本依赖了这些项目。有了这个思路就好办事了。
|
||||
|
||||

|
||||
|
||||
- 打开本地的 `/Users/liubinpeng/.cocoapods/repos` 文件夹。查看本地的私有 repo 的管理的所有的项目,找到出问题的 repo,进去删掉有问题的 tag。
|
||||
- 出问题的 repo 将远端的有问题的 tag 也删掉
|
||||
- 删除远端的 repo 仓库的有问题的 tag。
|
||||
|
||||
## 6. lint 失败 - 报错为 `error: invalid task ('StripNIB...`
|
||||
```Shell
|
||||
Build system information
|
||||
error: invalid task ('StripNIB /Users/liubinpeng/Library/Developer/Xcode/DerivedData/App-fgbnpsgtrtstroctiqnanvyrfwyr/Build/Products/Release-iphonesimulator/XQLoginModule/SDGMemberCardBindViewController.nib') with mutable output but no other virtual output node (in target 'XQLoginModule')
|
||||
```
|
||||
原因为 xib 和图片资源都属于资源文件,不可以放在源文件(Classes)中,需要放在 Assets 中。如果放到 Classes 文件夹中 lint 会报错。
|
||||
|
||||
## 7. 一台电脑安装了最新版本的,出问题删除最新版 Xcode,下载旧版本 Xcode,pod install 失败
|
||||
|
||||
```Shell
|
||||
You need at least git version 1.8.5 to use CocoaPods
|
||||
```
|
||||
- 可能是cocoapods安装成功了,但是链接Xcode的版本过低,所以需要更新Xcode
|
||||
- 电脑安装了多个版本的Xcode,就需要修改链接Xcode路径,改成链接电脑比较高版本的Xcode。
|
||||
```Shell
|
||||
sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer
|
||||
```
|
||||
## 8. 一台电脑安装了最新版本的,出问题删除最新版 Xcode,下载旧版本 Xcode,打开工程 Xib 报错
|
||||
|
||||
- sudo killall -9 com.apple.CoreSimulator.CoreSimulatorService
|
||||
- xcrun simctl erase all
|
||||
|
||||
|
||||
## 9. 开发电脑上安装了旧版本 Cocopods,Mac 系统升级后, pod lint 失败
|
||||
|
||||
解决方案: 将 **fourflusher** 仓库中上的 [find.rb]((https://raw.githubusercontent.com/CocoaPods/fourflusher/master/lib/fourflusher/find.rb)) 文件中的 ruby 脚本复制到本地的 fourflusher 下.
|
||||
|
||||
查找本地 fourflusher 文件夹所在位置.
|
||||
```Shell
|
||||
gem which fourflusher
|
||||
```
|
||||
我的电脑 find.rb 文件所在位置.
|
||||
`/Library/Ruby/Gems/2.6.0/gems/fourflusher-2.3.1/lib/fourflusher/find.rb`
|
||||
|
||||
注意: 文件保存需要权限,所以加 sudo
|
||||
|
||||
## 10. pod lint 产生的信息太多,一屏显示不全,但是出错之后我们可能需要去查看 error 信息,上下翻页不方便
|
||||
|
||||
解决方案: 利用脚本 ` >1.log 2>&1` 将当前的 pod lint 产生的信息写入文件.
|
||||
|
||||
完整代码
|
||||
|
||||
```Ruby
|
||||
pod lib lint --sources=****,**** --allow-warnings --verbose --use-libraries >1.log 2>&1
|
||||
```
|
||||
|
||||
|
||||
## 11. pod 库每次修改代码,主工程必须 clean 再安装才可以看到新改动的代码
|
||||
|
||||
解决方案:
|
||||
```ruby
|
||||
install! 'cocoapods', :disable_input_output_paths => true
|
||||
```
|
||||
|
||||
## 12. pod 库太多,每次构建编译都很耗费时间
|
||||
|
||||
```ruby
|
||||
install! 'cocoapods', generate_multiple_pod_projects: true
|
||||
```
|
||||
|
||||
## 13. 卸载旧版本 cocoapods 安装新的
|
||||
|
||||
```shell
|
||||
sudo gem uninstall cocoapods-core cocoapods cocoapods-deintegrate cocoapods-downloader cocoapods-plugins cocoapods-search cocoapods-stats cocoapods-trunk cocoapods-try coderay colored2 concurrent-ruby cocoapods-clean
|
||||
sudo gem install cocoapods
|
||||
```
|
||||
541
Chapter1 - iOS/1.52.md
Normal file
541
Chapter1 - iOS/1.52.md
Normal file
@@ -0,0 +1,541 @@
|
||||
# 如何打造团队的代码风格统一以及开发效率的提升
|
||||
|
||||
|
||||
> 最近重构项目组件,看到项目中存在一些命名和方法分块方面存在一些问题,结合平时经验和 [Apple官方代码规范](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CodingGuidelines/CodingGuidelines.html) 在此整理出 iOS 工程规范。提出第一个版本,如果后期觉得有不完善的地方,继续提出来不断完善,文档在此记录的目的就是为了大家的代码可读性较好,后来的人或者团队里面的其他人看到代码可以不会因为代码风格和可读性上面造成较大时间的开销。
|
||||
|
||||
软件的生命周期贯穿产品的开发,测试,生产,用户使用,版本升级和后期维护等过程,只有易读,易维护的软件代码才具有生命力。
|
||||
|
||||
|
||||
## 一些原则
|
||||
|
||||
1. 长的,描述性的方法和变量命名是好的。不要使用简写,除非是一些大家都知道的场景比如 VIP。不要使用 bgView,推荐使用 backgroundView
|
||||
2. 见名知意。含义清楚,做好不加注释代码自我表述能力强。(前提是代码足够规范)
|
||||
3. 不要过分追求技巧,降低代码可读性
|
||||
4. 删除没必要的代码。比如我们新建一个控制器,里面会有一些不会用到的代码,或者注释起来的代码,如果这些代码不需要,那就删除它,留着偷懒吗?下次需要自己手写
|
||||
5. 在方法内部不要重复计算某个值,适当的情况下可以将计算结果缓存起来
|
||||
6. 尽量减少单例的使用。
|
||||
7. 提供一个统一的数据管理入口,不管是 MVC、MVVM、MVP 模块内提供一个统一的数据管理入口会使得代码变得更容易管理和维护。
|
||||
8. 除了 .m 文件中方法,其他的地方"{"不需要另起一行。
|
||||
```Objective-c
|
||||
- (void)getGooodsList
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
- (void)doHomework
|
||||
{
|
||||
if (self.hungry) {
|
||||
return;
|
||||
}
|
||||
if (self.thirsty) {
|
||||
return;
|
||||
}
|
||||
if (self.tired) {
|
||||
return;
|
||||
}
|
||||
papapa.then.over;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 变量
|
||||
|
||||
1. 一个变量最好只有一个作用,切勿为了节省代码行数,觉得一个变量可以做多个用途。(单一原则)
|
||||
2. 方法内部如果有局部变量,那么局部变量应该靠近在使用的地方,而不是全部在顶部声明全部的局部变量。
|
||||
|
||||
|
||||
## 运算符
|
||||
|
||||
1. 1元运算符和变量之间不需要空格。例如:++n
|
||||
2. 2元运算符与变量之间需要空格隔开。例如: containerWidth = 0.3 * Screen_Width
|
||||
3. 当有多个运算符的时候需要使用括号来明确正确的顺序,可读性较好。例如: 2 << (1 + 2 * 3 - 4)
|
||||
|
||||
|
||||
## 条件表达式
|
||||
|
||||
1. 当有条件过多、过长的时候需要换行,为了代码看起来整齐些
|
||||
```
|
||||
//good
|
||||
if (condition1() &&
|
||||
condition2() &&
|
||||
condition3() &&
|
||||
condition4()) {
|
||||
// Do something
|
||||
}
|
||||
//bad
|
||||
if (condition1() && condition2() && condition3() && condition4()) { // Do something }
|
||||
```
|
||||
2. 在一个代码块里面有个可能的情况时善于使用 `return` 来结束异常的情况。
|
||||
```
|
||||
- (void)doHomework
|
||||
{
|
||||
if (self.hungry) {
|
||||
return;
|
||||
}
|
||||
if (self.thirsty) {
|
||||
return;
|
||||
}
|
||||
if (self.tired) {
|
||||
return;
|
||||
}
|
||||
papapa.then.over;
|
||||
}
|
||||
```
|
||||
3. 每个分支的实现都必须使用 {} 包含。
|
||||
```
|
||||
// bad
|
||||
if (self.hungry) self.eat()
|
||||
// good
|
||||
if (self.hungry) {
|
||||
self.eat()
|
||||
}
|
||||
```
|
||||
4. 条件判断的时候应该是变量在左,条件在右。 if ( currentCursor == 2 ) { //... }
|
||||
5. switch 语句后面的每个分支都需要用大括号括起来。
|
||||
6. switch 语句后面的 default 分支必须存在,除非是在对枚举进行 switch。
|
||||
```
|
||||
switch (menuType) {
|
||||
case menuTypeLeft: {
|
||||
...
|
||||
break;
|
||||
}
|
||||
case menuTypeRight: {
|
||||
...
|
||||
break;
|
||||
}
|
||||
case menuTypeTop: {
|
||||
...
|
||||
break;
|
||||
}
|
||||
case menuTypeBottom: {
|
||||
...
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 类名
|
||||
|
||||
1. 大写驼峰式命名。每个单词首字母大写。比如「申请记录控制器」ApplyRecordsViewController
|
||||
2. 每个类型的命名以该类型结尾。
|
||||
- ViewController:使用 `ViewController` 结尾。例子:ApplyRecordsViewController
|
||||
- View:使用 `View` 结尾。例子:分界线:boundaryView
|
||||
- NSArray:使用 `s` 结尾。比如商品分类数据源。categories
|
||||
- UITableViewCell:使用 `Cell` 结尾。比如 MyProfileCell
|
||||
- Protocol:使用 `Delegate` 或者 `Datasource` 结尾。比如 XQScanViewDelegate
|
||||
- Tool:工具类
|
||||
- 代理类:Delegate
|
||||
- Service 类:Service
|
||||
|
||||
|
||||
## 类的注释
|
||||
|
||||
有时候我们需要为我们创建的类设置一些注释。我们可以在类的下面添加。
|
||||
|
||||
|
||||
## 枚举
|
||||
|
||||
枚举的命名和类的命名相近。
|
||||
```
|
||||
typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
|
||||
UIControlContentVerticalAlignmentCenter = 0,
|
||||
UIControlContentVerticalAlignmentTop = 1,
|
||||
UIControlContentVerticalAlignmentBottom = 2,
|
||||
UIControlContentVerticalAlignmentFill = 3,
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## 宏
|
||||
|
||||
1. 全部大写,单词与单词之间用 `_` 连接。
|
||||
2. 以 `K` 开头。后面遵循大写驼峰命名。「不带参数」
|
||||
|
||||
```
|
||||
#define HOME_PAGE_DID_SCROLL @"com.xq.home.page.tableview.did.scroll"
|
||||
#define KHomePageDidScroll @"com.xq.home.page.tableview.did.scroll"
|
||||
```
|
||||
|
||||
|
||||
## 属性
|
||||
|
||||
书写规则,基本上就是 `@property 之后空一格,括号,里面的 线程修饰词、内存修饰词、读写修饰词,空一格 类 对象名称`
|
||||
根据不同的场景选择合适的修饰符。
|
||||
|
||||
```
|
||||
@property (nonatomic, strong) UITableView *tableView;
|
||||
@property (nonatomic, assign, readonly) BOOL loading;
|
||||
@property (nonatomic, weak) id<#delegate#> delegate;
|
||||
@property (nonatomic, copy) <#returnType#> (^<#Block#>)(<#parType#>);
|
||||
```
|
||||
|
||||
|
||||
## 单例
|
||||
|
||||
单例适合全局管理状态或者事件的场景。一旦创建,对象的指针保存在静态区,单例对象在堆内存中分配的内存空间只有程序销毁的时候才会释放。基于这种特点,那么我们类似 UIApplication 对象,需要全局访问唯一一个对象的情况才适合单例,或者访问频次较高的情况。我们的功能模块的生命周期肯定小于 App 的生命周期,如果多个单例对象的话,势必 App 的开销会很大,糟糕的情况系统会杀死 App。如果觉得非要用单例比较好,那么注意需要在合适的场合 tearDown 掉。
|
||||
|
||||
单例的使用场景概括如下:
|
||||
- 控制资源的使用,通过线程同步来控制资源的并发访问。
|
||||
- 控制实例的产生,以达到节约资源的目的。
|
||||
- 控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。
|
||||
|
||||
```objective-c
|
||||
+ (instancetype)sharedInstance
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
//because has rewrited allocWithZone use NULL avoid endless loop lol.
|
||||
_sharedInstance = [[super allocWithZone:NULL] init];
|
||||
});
|
||||
|
||||
return _sharedInstance;
|
||||
}
|
||||
|
||||
+ (id)allocWithZone:(struct _NSZone *)zone
|
||||
{
|
||||
return [TestNSObject sharedInstance];
|
||||
}
|
||||
|
||||
+ (instancetype)alloc
|
||||
{
|
||||
return [TestNSObject sharedInstance];
|
||||
}
|
||||
|
||||
- (id)copy
|
||||
{
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)mutableCopy
|
||||
{
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(struct _NSZone *)zone
|
||||
{
|
||||
return self;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 私有变量
|
||||
|
||||
推荐以 `_` 开头,写在 .m 文件中。例如 NSString * _somePrivateVariable
|
||||
|
||||
|
||||
|
||||
## 代理方法
|
||||
|
||||
1. 类的实例必须作为方法的参数之一。
|
||||
2. 对于一些连续的状态的,可以加一些 will(将要)、did(已经)
|
||||
3. 以类的名称开头
|
||||
|
||||
```objective-c
|
||||
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;
|
||||
```
|
||||
|
||||
|
||||
## 方法
|
||||
|
||||
1. 方法与方法之间间隔一行
|
||||
2. 大量的方法尽量要以组的形式放在一起,比如生命周期函数、公有方法、私有方法、setter && getter、代理方法..
|
||||
3. 方法最后面的括号需要另起一行。遵循 Apple 的规范
|
||||
4. 对于其他场景的括号,括号不需要单独换行。比如 if 后面的括号。
|
||||
5. 如果方法参数过多过长,建议多行书写。用冒号进行对齐。
|
||||
6. 一个方法内的代码最好保持在50行以内,一般经验来看如果一个方法里面的代码行数过多,代码的阅读体验就很差(别问为什么,做过重构代码行数很长的人都有类似的心情)
|
||||
7. 一个函数只做一个事情,做到单一原则。所有的类、方法设计好后就可以类似搭积木一样实现一个系统。
|
||||
8. 对于有返回值的函数,且函数内有分支情况。确保每个分支都有返回值。
|
||||
9. 函数如果有多个参数,外部传入的参数需要检验参数的非空、数据类型的合法性,参数错误做一些措施:立即返回、断言。
|
||||
10. 多个函数如果有逻辑重复的代码,建议将重复的部分抽取出来,成为独立的函数进行调用
|
||||
|
||||
```objective-c
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
<#statements#>
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)doHomework:(NSString *)name
|
||||
period:(NSInteger)second
|
||||
score:(NSInteger)score;
|
||||
```
|
||||
11. 方法如果有多个参数的情况下需要注意是否需要介词和连词。很多时候在不知道如何抉择测时候思考下苹果的一些 API 的方法命名。
|
||||
```objective-c
|
||||
//good
|
||||
- (instancetype)initWithAge:(NSInteger)age name:(NSString *)name;
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
|
||||
|
||||
|
||||
//bad
|
||||
- (instancetype)initWithAge:(NSInteger)age andName:(NSString *)name;
|
||||
|
||||
- (void)tableView:(UITableView *)tableView :(NSIndexPath *)indexPath;
|
||||
```
|
||||
12. `.m` 文件中的私有方法需要在顶部进行声明
|
||||
13. 方法组之间也有个顺序问题。
|
||||
- 在文件最顶部实现属性的声明、私有方法的声明(很多人省去这一步,问题不大,但是蛮多第三方的库都写了,看起来还是会很方便,建议书写)。
|
||||
- 在生命周期的方法里面,比如 viewDidLoad 里面只做界面的添加,而不是做界面的初始化,所有的 view 初始化建议放在 getter 里面去做。往往 view 的初始化的代码长度会比较长、且一般会有多个 view 所以 getter 和 setter 一般建议放在最下面,这样子顶部就可以很清楚的看到代码的主要逻辑。
|
||||
- 所有button、gestureRecognizer 的响应事件都放在这个区域里面,不要到处乱放。
|
||||
|
||||
|
||||
|
||||
文件基本上就是
|
||||
```objective-c
|
||||
//___FILEHEADER___
|
||||
|
||||
#import "___FILEBASENAME___.h"
|
||||
/*ViewController*/
|
||||
|
||||
/*View&&Util*/
|
||||
|
||||
/*model*/
|
||||
|
||||
/*NetWork InterFace*/
|
||||
|
||||
/*Vender*/
|
||||
|
||||
@interface ___FILEBASENAMEASIDENTIFIER___ ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation ___FILEBASENAMEASIDENTIFIER___
|
||||
|
||||
|
||||
#pragma mark - life cycle
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
self.title = <#value#>;
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
- (void)dealloc
|
||||
{
|
||||
NSLog(@"%s",__func__);
|
||||
}
|
||||
#endif
|
||||
|
||||
#pragma mark - public Method
|
||||
|
||||
#pragma mark - private method
|
||||
|
||||
#pragma mark - event response
|
||||
|
||||
|
||||
|
||||
#pragma mark - UITableViewDelegate
|
||||
|
||||
#pragma mark - UITableViewDataSource
|
||||
//...(多个代理方法依次往下写)
|
||||
|
||||
#pragma mark - getters and setters
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
## 图片资源
|
||||
|
||||
1. 单个文件的命名
|
||||
文件资源的命名也需要一定的规范,形式为:功能模块名_类别_功能_状态@nx.png
|
||||
Setting_Button_search_selected@2x.png、Setting_Button_search_selected@3x.png
|
||||
Setting_Button_search_unselected@2x.png、Setting_Button_search_unselected@3x.png
|
||||
2. 资源的文件夹命名
|
||||
最好也参考 App 按照功能模块建立对应的实体文件夹目录,最后到对应的目录下添加相应的资源文件。
|
||||
|
||||
|
||||
## 注释
|
||||
|
||||
1. 对于类的注释写在当前类文件的顶部
|
||||
2. 对于属性的注释需要写在属性后面的地方。 //**<userId*/
|
||||
3. 对于 .h 文件中方法的注释,一律按快捷键 `command+option+/`。三个快捷键解决。按需在旁边对方法进行说明解释、返回值、参数的说明和解释
|
||||
4. 对于 .m 文件中的方法的注释,在方法的旁边添加 `//`。
|
||||
5. 注释符和注释内容需要间隔一个空格。 例如: // fetch goods list
|
||||
|
||||
|
||||
## 版本规范
|
||||
|
||||
采用 A.B.C 三位数字命名,比如:1.0.2,当有更新的情况下按照下面的依据
|
||||
|
||||
| 版本号 | 右说明对齐标题 | 示例 |
|
||||
| :------:| :------: | :------: |
|
||||
| **A**.b.c | 属于重大内容的更新 | 1.0.2 -> 2.0.0 |
|
||||
| a.**B**.c | 属于小部分内容的更新 | 1.0.2 -> 1.1.1 |
|
||||
| a.b.**C** | 属于补丁更新 | 1.0.2 -> 1.0.3 |
|
||||
|
||||
|
||||
## 改进
|
||||
|
||||
我们知道了平时在使用 Xcode 开发的过程中使用的系统提供的代码块所在的地址和新建控制器、模型、view等的文件模版的存放文件夹地址后,我们就可以设想下我们是否可以定制自己团队风格的控制器模版、是否可以打造和维护自己团队的高频使用的代码块?
|
||||
|
||||
答案是可以的。
|
||||
|
||||
Xcode 代码块的存放地址:`~/Library/Developer/Xcode/UserData/CodeSnippets`
|
||||
Xcode 文件模版的存放地址:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File Templates/
|
||||
|
||||
|
||||
## 意义
|
||||
|
||||
1. 为了个人或者团队开发者的代码更加规范。Property的书写的时候的空格、线程修饰词、内存修饰词的先后顺序
|
||||
2. 提供大量可用的代码块,提高开发效率。比如在 Xcode 里面敲 UITableView_init 便可以自动懒加载创建一个 UITabelView 对象,你只需要设置在指定的位置写相应的参数
|
||||
3. 通过一些代码块提高代码规范、避免一些bug。比如曾看到过 block 属性用 strong 修饰的代码,造成内存泄漏。举个例子你在 Xcode 中输入 **Property_delegate** 就会出来 `@property (nonatomic, weak) id<<#delegate#>> delegate;`,你输入 **Property_block** 就会出来 `@property (nonatomic, copy) <#returnType#> (^<#Block#>)(<#parType#>);`
|
||||
|
||||
|
||||
## 代码块的改造
|
||||
|
||||
我们可以将属性、控制器生命周期方法、单例构造一个对象的方法、代理方法、block、GCD、UITableView 懒加载、UITableViewCell 注册、UITableView 代理方法的实现、UICollectionVIew 懒加载、UICollectionVIewCell 注册、UICollectionView 的代理方法实现等等组织为 codesnippets
|
||||
|
||||
|
||||
### 思考
|
||||
|
||||
- 封装好 codesnippets 之后团队除了你编写这个项目的人如何使用?如何知道是否有这个代码块?
|
||||
|
||||
方案:先在团队内召开代码规范会议,大家都统一知道这个事情在。之后大家共同维护 codesnippets。用法见下
|
||||
|
||||
|
||||
|
||||
属性:通过 **Property_类型** 开头,回车键自动补全。比如 Strong 类型,编写代码通过 Property_Strong 回车键自动补全成如下格式
|
||||
|
||||
```objective-c
|
||||
@property (nonatomic, strong) <#Class#> *<#object#>;
|
||||
```
|
||||
|
||||
方法:以 **Method_关键词** 回车键确认,自动补全。比如 Method_UIScrollViewDelegate 回车键自动补全成 如下格式
|
||||
|
||||
```objective-c
|
||||
#pragma mark - UIScrollViewDelegate
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
各种常见的 Mark:以 **Mark_关键词** 回车确认,自动补全。比如 Method_MethodsGroup 回车键自动补全成 如下格式
|
||||
|
||||
```objective-c
|
||||
#pragma mark - life cycle
|
||||
#pragma mark - public Method
|
||||
#pragma mark - private method
|
||||
#pragma mark - event response
|
||||
#pragma mark - UITableViewDelegate
|
||||
#pragma mark - UITableViewDataSource
|
||||
#pragma mark - getters and setters
|
||||
```
|
||||
|
||||
- 封装好 codesnippets 之后团队内如何统一?想到一个方案,可以将团队内的 codesnippets 共享到 git,团队内的其他成员再从云端拉取同步。这样的话团队内的每个成员都可以使用最新的 codesnippets 来编码。
|
||||
|
||||
编写 shell 脚本。几个关键步骤:
|
||||
|
||||
1. 给系统文件夹授权
|
||||
2. 在脚本所在文件夹新建存放代码块的文件夹
|
||||
3. 将系统文件夹下面的代码块复制到步骤2创建的文件夹下面
|
||||
4. 将当前的所有文件提交到 Git 仓库
|
||||
|
||||
|
||||
## 文件模版的改造
|
||||
|
||||
我们观察系统文件模版的特点,和在 Xcode 新建文件模版对应。
|
||||
|
||||

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

|
||||
|
||||
进入 PowerViewController.xctemplate/PowerViewControllerObjective-C
|
||||
|
||||
修改 `___FILEBASENAME___.h` 和 `___FILEBASENAME___.m` 文件内容
|
||||
|
||||

|
||||
|
||||
在替换 .h 文件内容的时候后面改为 UIViewController,不然其他开发者新建文件模版的时候出现的不是 UIViewController 而是我们的 PowerViewController
|
||||
|
||||

|
||||
|
||||
修改 TemplateInfo.plist
|
||||
|
||||

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

|
||||
|
||||
- 如何共享
|
||||
|
||||
以 shell 脚本为工具。使用脚本将 git 云端的代码模版同步到本地 Xcode 文件夹对应的位置就可以使用了。关键步骤:
|
||||
|
||||
1. git clone 代码到脚本所在文件夹
|
||||
2. 进入存放 codesnippets 的文件夹将内容复制到系统存放 codesnippets 的地方
|
||||
3. 进入存放 file template 的文件夹将内容复制到系统存放 file template 的地方
|
||||
|
||||
|
||||
## 内容及其如何使用
|
||||
|
||||
1. Property 属性。敲 **Property_** 自动联想,光标移动选中后敲回车自动补全
|
||||
2. Mark 标识。 敲 **Mark_** 自动联想,会展示各种常用的 Mark,光标移动选中后敲回车自动补全
|
||||
3. Method 方法。敲 **Method_** 自动联想,会展示各种常用的 Method,光标移动选中后敲回车自动补全
|
||||
4. GCD。敲 **GCD_** 自动联想,会展示各种常用的 GCD,光标移动选中后敲回车自动补全
|
||||
5. 常用 UI 控件的懒加载。敲 **_init** 自动联想,展示常用的 UI 控件的懒加载,光标移动选中后敲回车自动补全
|
||||
6. Delegate。敲 **Delegate_** 自动联想,会展示各种常用的 Delegate,光标移动选中后敲回车自动补全
|
||||
7. Notification。敲 **NSNotification_** 自动联想,会展示各种常用的 NSNotification 的代码块,比如发送通知、添加观察者、移除观察者、观察者方法的实现等等,光标移动选中后敲回车自动补全
|
||||
8. Protocol。敲 **Protocol_** 自动联想,会展示各种常用的 Protocol 的代码块,光标移动选中后敲回车自动补全
|
||||
9. 内存修饰代码块
|
||||
10. 工程常用 TODO、FIXME、Mark。敲 **Mark_** 自动联想,会展示各种常用的 Mark 的代码块,光标移动选中后敲回车自动补全
|
||||
11. 内存修饰代码块。敲 **Memory_** 自动联想,会展示各种常用的内存修饰的代码块,光标移动选中后敲回车自动补全
|
||||
12. 一些常用的代码块。敲 **Thread_** 等自动联想,选中后敲回车自动补全。
|
||||
|
||||
|
||||
## 使用
|
||||
|
||||
```shell
|
||||
chmod +x ./syncSnippets.sh // 为脚本设置可执行权限
|
||||
chmod +x ./uploadMySnippets.sh // 为脚本设置可执行权限
|
||||
./syncSnippets.sh // 同步git云端代码块和文件模版到本地
|
||||
./uploadMySnippets.sh //将本地的代码块和文件模版同步到云端
|
||||
```
|
||||
|
||||
## PS
|
||||
|
||||
**不断完善中。大家有好用或者不错的代码块或者文件模版希望参与到这个项目中来,为我们开发效率的提升添砖加瓦、贡献力量**
|
||||
|
||||
目前新建了大概58个代码段和6个类文件模版(UIViewController控制器带有方法组、模型、线程安全的单例模版、带有布局方法的UIView模版、UITableViewCell、UICollectionViewCell模版)
|
||||
|
||||
shell 脚本基本有每个函数和关键步骤的代码注释,想学习 shell 的人可以看看代码。[代码传送门](https://github.com/FantasticLBP/codesnippets)
|
||||
17
Chapter1 - iOS/1.53.md
Normal file
17
Chapter1 - iOS/1.53.md
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
# 数据持久化方案
|
||||
|
||||
## 功能
|
||||
主要将一些网络获取下来的数据保存到 App 本地,增强用户体验、减小网络请求的次数。
|
||||
|
||||
|
||||
| 方案 | 存储量 | 数据类型 | 数据载体 | 特点 | 场景 | 缺点 |
|
||||
|:-:| :-: | :-: | :-: | :-:| :-:| :-: |
|
||||
| plist | 适合存储小数据量、并且属于一类的列表类的数据 | 基本数据类型、不支持存储自定义的对象 | 直接在plist文件上操作(可见) | 量小、不常变动| 省市列表、职场工作分类| 所有数据都存放在 root dictionary 里,每次都需要将整个 dictiona 读取出来访问所需的对象。数据较大的时候很费时间和空间 |
|
||||
| 归档 | 存储数据量较大的数据对象 | 遵循NSCoding、NSCopying协议的对象| 必须依赖NSKeyedArchieve、NSUnKeyedArchieve的类方法或者对象方法进行存储 | 存储较为麻烦,需要实现对应的协议。归档需要实现`-(void)encodeWithCoder:(NSCoder*)aCoder;`解码需要实现`-(id)initWithCoder:(NSCoder*)aDecoder;`。除了遵循NSCoding协议外,对象要实现复制,需要实现`-(id)copyWithZone:(NSZone *)zone;` | 存储一些较小的数据 | 无法存储较大的数据和高效的查找 |
|
||||
| NSUserDefault(偏好设置) | 适合存储数据量较少,有时也会存储一些比如状态开关的标准性值 | 存储OC所有的数据类型| 实例对象、基本数据 | 利用| App应用程序的配置信息、如版本号、app名称、用户权限、标志键值等| 无法存储自定义的对象 |
|
||||
| 沙盒存储 | 存储较大量数据量的数据 | 基本存储 NSData 类型的数据| 文件 |依赖NSFileManager进行文件的写入和读取,过程较为复杂。Application:存放程序源代码,上架前经过数字签名,上架后不可修改。Documents:保存App运行时生成的需要持久化的数据,iTunes同步设备会同步该目录。例如将游戏数据保存到该目录下。tmp:保存App运行时产生的临时数据,程序结束将文件从该目录删除。iTunes同步设备不会同步该目录。Library/Caches:保存应用运行时生成的需要持久化的数据,iTunes同步设备时不会同步该目录。一般体积大、不需要备份的非重要数据,比如网络数据的缓存。Library/Preference:保存应用的偏好设置,比如OS的设置应用会在该目录下查找用户的设置信息。iTunes同步设备会同步该目录 | 图片、音视频。比如SDWebImage的文件缓存| 缓存太多,文件体积会非常大 |
|
||||
| 数据库 | 存储大型数据量的数据 | OC所有的数据类型| 数据库文件 | 数据的增删改查,较为强大的数据库批量处理指令,SQL| 几乎每个大型App都有自己的数据库,比如微信、微博,为了较好的用户体验在每个小细节都有数据库技术| 需要新建数据库、建立连接、处理数据、关闭数据库连接。也不支持自定义的对象存储 |
|
||||
|
||||
|
||||
## 2个概念:Relation、Object
|
||||
74
Chapter1 - iOS/1.54.md
Normal file
74
Chapter1 - iOS/1.54.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# 自定义工程中的头文件信息
|
||||
|
||||
> 我们打开 Xcode 工程的时候新建的文件顶部的信息非常的少且不是我们需要展示信息,看到很多的 GitHub 项目的顶部的头信息还是非常的花哨,所以在此记录如何写自定义模版的文章。
|
||||
|
||||
## 现状
|
||||
|
||||
```
|
||||
//
|
||||
// MASLayoutConstraint.h
|
||||
// Masonry
|
||||
//
|
||||
// Created by Jonas Budelmann on 3/08/13.
|
||||
// Copyright (c) 2013 Jonas Budelmann. All rights reserved.
|
||||
//
|
||||
```
|
||||
|
||||
## 目标
|
||||
|
||||
```
|
||||
//
|
||||
// SDGFasterEncoder.h
|
||||
// XQ_Persistance
|
||||
//
|
||||
// Author: @杭城小刘
|
||||
// Github: https://github.com/FantasticLBP
|
||||
// E-mail: wsbglbp@outlook.com
|
||||
//
|
||||
// Created by 杭城小刘 on 2019/1/23
|
||||
//
|
||||
```
|
||||
|
||||
## 动手实现
|
||||
|
||||
我们利用 Xcode9 新特性,自定义文本宏,来实现上述的需求。
|
||||
|
||||
|
||||
### 步骤
|
||||
|
||||
1. 创建 .plist 文件
|
||||
2. 添加宏名称:FILEHEADER
|
||||
3. 添加宏对应的值,即自定义的注释格式
|
||||
4. 将文件放置于起作用的文件目录下
|
||||
- 选中项目的 **.xcodeproj 文件
|
||||
- 显示包内容
|
||||
- 进入 xcshareddata 文件夹
|
||||
- 将之前完成的 IDETemplateMacros.plist 复制到xcshareddata 下面和 xcschemes 的同级目录
|
||||
|
||||
- 打开 XQ_Persistance.xcworkspace
|
||||
- 显示包内容
|
||||
- 进入 xcuserdata 文件夹
|
||||
- 将 IDETemplateMacros.plist 复制进去,生效
|
||||
|
||||
### 模版
|
||||
|
||||
```
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>FILEHEADER</key>
|
||||
<string>
|
||||
// ___FILENAME___
|
||||
// ___PACKAGENAME___
|
||||
//
|
||||
// Author: @杭城小刘
|
||||
// Github: https://github.com/FantasticLBP
|
||||
// E-mail: wsbglbp@outlook.com
|
||||
//
|
||||
// Created by 杭城小刘 on ___DATE___
|
||||
//
|
||||
</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
766
Chapter1 - iOS/1.55.md
Normal file
766
Chapter1 - iOS/1.55.md
Normal file
@@ -0,0 +1,766 @@
|
||||
# 无痕埋点的设计与实现
|
||||
|
||||
|
||||
在移动互联网时代,对于每个公司、企业来说,用户的行为数据非常重要。重要到什么程度,用户在这个页面停留多久、点击了什么按钮、浏览了什么内容、什么手机、什么网络环境、App什么版本等都需要清清楚楚。一些大厂的蛮多业务成果都是基于用户操作行为进行推荐后二次转换。另一方面是以日志的作用帮助开发者分析线上问题的一种辅助手段。
|
||||
|
||||
那么有了上述的诉求,那么技术人员如何满足这些需求?引出来了一个技术点-“埋点”
|
||||
|
||||
|
||||
|
||||
## 0x01. 埋点手段
|
||||
|
||||
业界中对于代码埋点主要有3种主流的方案:代码手动埋点、可视化埋点、无痕埋点。简单说说这几种埋点方案。
|
||||
|
||||
- 代码手动埋点:根据业务需求(运营、产品、开发多个角度出发)在需要埋点地方手动调用埋点接口,上传埋点数据。
|
||||
- 可视化埋点:通过可视化配置工具完成采集节点,在前端自动解析配置并上报埋点数据,从而实现可视化“无痕埋点”
|
||||
- 无痕埋点:通过技术手段,完成对用户行为数据无差别的统计上传的工作。后期数据分析处理的时候通过技术手段筛选出合适的数据进行统计分析。
|
||||
|
||||
|
||||
|
||||
## 0x02. 技术选型
|
||||
|
||||
|
||||
|
||||
### 1. 代码手动埋点
|
||||
|
||||
该方案情况下,如果需要埋点,则需要在工程代码中,写埋点相关代码。因为侵入了业务代码,对业务代码产生了污染,显而易见的缺点是**埋点的成本较高**、且违背了**单一原则**。
|
||||
|
||||
例1:假如你需要知道用户在点击“购买按钮”时的相关信息(手机型号、App版本、页面路径、停留时间、动作等等),那么就需要在按钮的点击事件里面去写埋点统计的代码。这样明显的弊端就是在之前业务逻辑的代码上面又多出了埋点的代码。由于埋点代码分散、埋点的工作量很大、代码维护成本较高、后期重构很头痛。
|
||||
|
||||
例2:假如 App 采用了 Hybrid 架构,当 App 的第一版本发布的时候 H5 的关键业务逻辑统计是由 Native 定义好关键逻辑(比如H5调起了Native的分享功能,那么存在一个分享的埋点事件)的桥接。假如某天增加了一个扫一扫功能,未定义扫一扫的埋点桥接,那么 H5 页面变动的时候,Native 埋点代码不去更新的话,变动的 H5 的业务就未被精确统计。
|
||||
|
||||
优点:产品、运营工作量少,对照业务映射表就可以还原出相关业务场景、数据精细无须大量的加工和处理
|
||||
|
||||
缺点:开发工作量大、前期需要和运营、产品指定的好业务标识,以便产品和运营进行数据统计分析
|
||||
|
||||
|
||||
|
||||
### 2. 可视化埋点
|
||||
|
||||
**可视化埋点的出现,是为解决代码埋点流程复杂、成本高、新开发的页面(H5、或者服务端下发的 json 去生成相应页面)不能及时拥有埋点能力**
|
||||
|
||||
前端在「埋点编辑模式」下,以“可视化”的方式去配置、绑定关键业务模块的路径到前端可以唯一确定到view的xpath过程。
|
||||
|
||||
用户每次操作的控件,都生成一个 **xpath** 字符串,然后通过接口将 xpath 字符串(view在前端系统中的唯一定位。以 iOS 为例,App名称、控制器名称、一层层view、同类型view的序号:“GoodCell.21.RetailTableView.GoodsViewController.*baoApp”)到真正的业务模块(“宝App-商城控制器-分销商品列表-第21个商品被点击了”)的映射关系上传到服务端。xpath 具体是什么在下文会有介绍。
|
||||
|
||||
之后操作 App 就生成对应的 xpath 和埋点数据(开发者通过技术手段将从服务端获取的关键数据塞到前端的 UI 控件上。 iOS 端为例, UIView 的 accessibilityIdentifier 属性可以设置我们从服务端获取的埋点数据)上传到服务端。
|
||||
|
||||
优点:数据量相对准确、后期数据分析成本低
|
||||
|
||||
缺点:前期控件的唯一识别、定位都需要额外开发;可视化平台的开发成本较高;对于额外需求的分析可能会比较困难
|
||||
|
||||
|
||||
|
||||
### 3. 无痕埋点
|
||||
|
||||
通过技术手段无差别地记录用户在前端页面上的行为。可以正确的获取 PV、UV、IP、Action、Time 等信息。
|
||||
|
||||
缺点:前期开发统计基础信息的技术产品成本较高、后期数据分析数据量很大、分析成本较高(大量数据传统的关系型数据库压力大)
|
||||
|
||||
优点:开发人员工作量小、数据全面、无遗漏、产品和运营按需分析、支持动态页面的统计分析
|
||||
|
||||
|
||||
|
||||
### 4. 如何选择
|
||||
|
||||
结合上述优缺点,我们选择了**无痕埋点+可视化埋点结合**的技术方案。
|
||||
|
||||
怎么说呢?对于关键的业务开发结束上线后、通过可视化方案(类似于一个界面,想想看 Dreamwaver,你在界面上拖拖控件,简单编辑下就可以生成对应的 HTML 代码)点击一下绑定对应关系到服务端。
|
||||
|
||||
那么这个对应关系是什么?我们需要唯一定位一个前端元素,那么想到的办法就是不管 Native 和 Web 前端,控件或者元素来说就是一个树形层级,DOM tree 或者 UI tree,所以我们通过技术手段定位到这个元素,以 Native iOS 为例子,假如点击商品详情页的加入购物车按钮会根据 UI 层级结构生成一个唯一标识 “addCartButton.GoodsViewController.GoodsView.*BaoApp” 。但是用户在使用 App 的时候,上传的是这串东西的 MD5到服务端。
|
||||
|
||||
这么做有2个原因:服务端数据库存储这串很长的东西不是很好;埋点数据被劫持的话直接看到明文不太好。所以 MD5 再上传。
|
||||
|
||||
|
||||
|
||||
## 0x03. 操刀就干
|
||||
|
||||
一言以蔽之就是:**AOP -> Event Collector -> Event Cache -> Data Upload**
|
||||
- AOP:通过 runtime hook 的能力做到提供合适的时机去生成点击事件的数据
|
||||
- Event Collector:将步骤1产生的数据统一收集(一般是一个内存存储的数据结构)
|
||||
- Event Cache:mmap,当内存中的数据达到一定的阀值,或者应用程序的生命周期切换的时候将内存中的数据同步到缓存中(数据库、磁盘、文件)
|
||||
- Data Uploader:制定一定的策略,当达到触发条件的时候再去上传数据(App达到阀值,生命周期的切换等);App从前台进入后台的时候去上传数据(后台线程保活策略);数据上传格式的选择(zip压缩文件、protoBuf)
|
||||
|
||||
### 1. 数据的收集
|
||||
|
||||
实现方案由以下几个关键指标:
|
||||
|
||||
- 现有代码改动少、尽量不要侵入业务代码去实现拦截系统事件
|
||||
- 全量收集
|
||||
- 如何唯一标识一个控件元素
|
||||
|
||||
|
||||
|
||||
### 2. 不侵入业务代码拦截系统事件
|
||||
|
||||
以 iOS 为例。我们会想到 **AOP(Aspect Oriented Programming)**面向切面编程思想。动态地在函数调用前后插入相应的代码,在 Objective-C 中我们可以利用 Runtime 特性,用 **Method Swizzling** 来 hook 相应的函数
|
||||
|
||||
为了给所有类方便地 hook,我们可以给 NSObject 添加个 Category,名字叫做 NSObject+MethodSwizzling
|
||||
|
||||
```objective-c
|
||||
#pragma mark - public Method
|
||||
+ (void)lbp_swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
|
||||
{
|
||||
class_swizzleInstanceMethod(self, originalSelector, swizzledSelector);
|
||||
}
|
||||
|
||||
+ (void)lbp_swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
|
||||
{
|
||||
//类方法实际上是储存在类对象的类(即元类)中,即类方法相当于元类的实例方法,所以只需要把元类传入,其他逻辑和交互实例方法一样。
|
||||
Class class2 = object_getClass(self);
|
||||
class_swizzleInstanceMethod(class2, originalSelector, swizzledSelector);
|
||||
}
|
||||
|
||||
#pragma mark - private method
|
||||
|
||||
void class_swizzleInstanceMethod(Class class, SEL originalSEL, SEL replacementSEL)
|
||||
{
|
||||
/*
|
||||
Class class = [self class];
|
||||
//原有方法
|
||||
Method originalMethod = class_getInstanceMethod(class, originalSelector);
|
||||
//替换原有方法的新方法
|
||||
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
|
||||
//先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况
|
||||
BOOL didAddMethod = class_addMethod(class,originalSelector,
|
||||
method_getImplementation(swizzledMethod),
|
||||
method_getTypeEncoding(swizzledMethod));
|
||||
if (didAddMethod) {//添加成功:表明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
|
||||
class_replaceMethod(class,swizzledSelector,
|
||||
method_getImplementation(originalMethod),
|
||||
method_getTypeEncoding(originalMethod));
|
||||
} else {//添加失败:表明源SEL已经有IMP,直接将两个SEL的IMP交换即可
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod);
|
||||
}
|
||||
*/
|
||||
|
||||
Method originMethod = class_getInstanceMethod(class, originalSEL);
|
||||
Method replaceMethod = class_getInstanceMethod(class, replacementSEL);
|
||||
|
||||
if(class_addMethod(class, originalSEL, method_getImplementation(replaceMethod),method_getTypeEncoding(replaceMethod))) {
|
||||
class_replaceMethod(class,replacementSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
|
||||
} else {
|
||||
method_exchangeImplementations(originMethod, replaceMethod);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 3. 全量收集
|
||||
|
||||
我们会想到 hook AppDelegate 代理方法、UIViewController 生命周期方法、按钮点击事件、手势事件、各种系统控件的点击回调方法、应用状态切换等等。
|
||||
|
||||
| 动作 | 事件 |
|
||||
| :-----------------------------------------------------: | :-----------------------------------------: |
|
||||
| App 状态的切换 | 给 Appdelegate 添加分类,hook 生命周期 |
|
||||
| UIViewController 生命周期函数 | 给 UIViewController 添加分类,hook 生命周期 |
|
||||
| UIButton 等的点击 | UIButton 添加分类,hook 点击事件 |
|
||||
| UICollectionView、UITableView 等的 | 在对应的 Cell 添加分类,hook 点击事件 |
|
||||
| 手势事件 UITapGestureRecognizer、UIControl、UIResponder | 相应系统事件 |
|
||||
|
||||
|
||||
|
||||
以统计页面的打开时间和统计页面的打开、关闭的需求为例,我们对 UIViewController 进行 hook
|
||||
|
||||
|
||||
|
||||
```objective-c
|
||||
static char *lbp_viewController_open_time = "lbp_viewController_open_time";
|
||||
static char *lbp_viewController_close_time = "lbp_viewController_close_time";
|
||||
|
||||
@implementation UIViewController (lbpka)
|
||||
|
||||
// load 方法里面添加 dispatch_once 是为了防止手动调用 load 方法。
|
||||
+ (void)load
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
@autoreleasepool {
|
||||
[[self class] lbp_swizzleMethod:@selector(viewWillAppear:) swizzledSelector:@selector(lbp_viewWillAppear:)];
|
||||
[[self class] lbp_swizzleMethod:@selector(viewWillDisappear:) swizzledSelector:@selector(lbp_viewWillDisappear:)];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - add prop
|
||||
|
||||
- (void)setOpenTime:(NSDate *)openTime
|
||||
{
|
||||
objc_setAssociatedObject(self,&lbp_viewController_open_time, openTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
- (NSDate *)getOpenTime
|
||||
{
|
||||
return objc_getAssociatedObject(self, &lbp_viewController_open_time);
|
||||
}
|
||||
|
||||
- (void)setCloseTime:(NSDate *)closeTime
|
||||
{
|
||||
objc_setAssociatedObject(self,&lbp_viewController_close_time, closeTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
- (NSDate *)getCloseTime
|
||||
{
|
||||
return objc_getAssociatedObject(self, &lbp_viewController_close_time);
|
||||
}
|
||||
|
||||
- (void)lbp_viewWillAppear:(BOOL)animated
|
||||
{
|
||||
NSString *className = NSStringFromClass([self class]);
|
||||
NSString *refer = [NSString string];
|
||||
//TODO:TODO 是否只埋本地有url的page
|
||||
if ([self getPageUrl:className]) {
|
||||
//设置打开时间
|
||||
[self setOpenTime:[NSDate dateWithTimeIntervalSinceNow:0]];
|
||||
if (self.navigationController) {
|
||||
if (self.navigationController.viewControllers.count >=2) {
|
||||
//获取当前vc 栈中 上一个VC
|
||||
UIViewController *referVC = self.navigationController.viewControllers[self.navigationController.viewControllers.count-2];
|
||||
refer = [self getPageUrl:NSStringFromClass([referVC class])];
|
||||
}
|
||||
}
|
||||
if (!refer || refer.length == 0) {
|
||||
refer = @"unknown";
|
||||
}
|
||||
[UserTrackDataCenter openPage:[self getPageUrl:className] fromPage:refer];
|
||||
}
|
||||
|
||||
[self lbp_viewWillAppear:animated];
|
||||
}
|
||||
|
||||
- (void)lbp_viewWillDisappear:(BOOL)animated
|
||||
{
|
||||
NSString *className = NSStringFromClass([self class]);
|
||||
if ([self getPageUrl:className]) {
|
||||
[self setCloseTime:[NSDate dateWithTimeIntervalSinceNow:0]];
|
||||
[UserTrackDataCenter leavePage:[self getPageUrl:className] spendTime:[self p_calculationTimeSpend]];
|
||||
}
|
||||
[self lbp_viewWillDisappear:animated];
|
||||
}
|
||||
|
||||
#pragma mark - private method
|
||||
|
||||
- (NSString *)p_calculationTimeSpend
|
||||
{
|
||||
|
||||
if (![self getOpenTime] || ![self getCloseTime]) {
|
||||
return @"unknown";
|
||||
}
|
||||
NSTimeInterval aTimer = [[self getCloseTime] timeIntervalSinceDate:[self getOpenTime]];
|
||||
|
||||
int hour = (int)(aTimer/3600);
|
||||
|
||||
int minute = (int)(aTimer - hour*3600)/60;
|
||||
|
||||
int second = aTimer - hour*3600 - minute*60;
|
||||
|
||||
return [NSString stringWithFormat:@"%d",second];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 4. 如何唯一标识一个控件元素
|
||||
|
||||
**xpath** 是移动端定义可操作区域的唯一标识。既然想通过一个字符串标识前端系统中可操作的控件,那么 xpath 需要2个指标:
|
||||
|
||||
- 唯一性:在同一系统中不存在不同控件有着相同的 xpath
|
||||
- 稳定性:不同版本的系统中,在页面结构没有变动的情况下,不同版本的相同页面,相同的控件的 xpath 需要保持一致。
|
||||
|
||||
我们想到 Naive、H5 页面等系统渲染的时候都是以树形结构去绘制和渲染,所以我们以当前的 View 到系统的根元素之间的所有关键点(UIViewController、UIView、UIView容器(UITableView、UICollectionView等)、UIButton...)串联起来这样就唯一定位了控件元素。
|
||||
|
||||
|
||||
|
||||
为了精确定位元素节点,参看下图
|
||||
|
||||
假设一个 UIView 中有三个子 view,先后顺序是:label、button1、button2,那么深度依次为: 0、1、2。假如用户做了某些操作将 label1 从父 view 中被移除了。此时 UIView 只有 2 个子view:button1、button2,而且深度变为了:0、1。
|
||||
|
||||

|
||||
|
||||
可以看出仅仅由于其中某个子 view 的改变,却导致其它子 view 的深度都发生了变化。因此,在设计的时候需要注意,在新增/移除某一 view 时,尽量减少对已有 view 的深度的影响,调整了对节点的深度的计算方式:采用当前 view 位于其父 view 中的所有 **与当前 view 同类型** 子view 中的索引值。
|
||||
|
||||
我们再看一下上面的这个例子,最初 label、button1、button2 的深度依次是:0、0、1。在 label 被移除后,button1、button2 的深度依次为:0、1。可以看出,在这个例子中,label 的移除并未对 button1、button2 的深度造成影响,这种调整后的计算方式在一定程度上增强了 xpath 的抗干扰性。
|
||||
|
||||
另外,调整后的深度的计算方式是依赖于各节点的类型的,因此,此时必须要将各节点的名称放到 `viewPath` 中,而不再是仅仅为了增加可读性。
|
||||
|
||||
|
||||
|
||||
在标识控件元素的层级时,需要知道「当前 view 位于其父 view 中的所有 **与当前 view 同类型** 子view 中的索引值」。参看上图,如果不是同类型的话,则唯一性得不到保证。
|
||||
|
||||
|
||||
|
||||
### 5. 同类型的 view 的唯一定位问题
|
||||
|
||||
有个问题,比如我们点击的元素是 UITableViewCell,那么它虽然可以定位到类似于这个标示 xxApp.GoodsViewController.GoodsTableView.GoodsCell,同类型的 Cell 有多个,所以单凭借这个字符串是没有办法定位具体的那个 Cell 被点击了。
|
||||
|
||||
当然有解决方案啦。
|
||||
|
||||
- 找出当前元素在父层同类型元素中的索引。根据当前的元素遍历当前元素的父级元素的子元素,如果出现相同的元素,则需要判断当前元素是所在层级的第几个元素
|
||||
|
||||
对当前的控件元素的父视图的全部子视图进行遍历,如果存在和当前的控件元素同类型的控件,那么需要判断当前控件元素在同类型控件元素中的所处的位置,那么则可以唯一定位。举例:GoodsCell-3.GoodsTableView.GoodsViewController.xxApp
|
||||
|
||||
|
||||
|
||||
```objective-c
|
||||
//UIResponder分类
|
||||
- (NSString *)lbp_identifierKa
|
||||
{
|
||||
// if (self.xq_identifier_ka == nil) {
|
||||
if ([self isKindOfClass:[UIView class]]) {
|
||||
UIView *view = (id)self;
|
||||
NSString *sameViewTreeNode = [view obtainSameSuperViewSameClassViewTreeIndexPath];
|
||||
NSMutableString *str = [NSMutableString string];
|
||||
//特殊的 加减购 因为带有spm但是要区分加减 需要带TreeNode
|
||||
NSString *className = [NSString stringWithUTF8String:object_getClassName(view)];
|
||||
if (!view.accessibilityIdentifier || [className isEqualToString:@"lbpButton"]) {
|
||||
[str appendString:sameViewTreeNode];
|
||||
[str appendString:@","];
|
||||
}
|
||||
while (view.nextResponder) {
|
||||
[str appendFormat:@"%@,", NSStringFromClass(view.class)];
|
||||
if ([view.class isSubclassOfClass:[UIViewController class]]) {
|
||||
break;
|
||||
}
|
||||
view = (id)view.nextResponder;
|
||||
}
|
||||
self.xq_identifier_ka = [self md5String:[NSString stringWithFormat:@"%@",str]];
|
||||
// self.xq_identifier_ka = [NSString stringWithFormat:@"%@",str];
|
||||
}
|
||||
// }
|
||||
return self.xq_identifier_ka;
|
||||
}
|
||||
|
||||
// UIView 分类
|
||||
- (NSString *)obtainSameSuperViewSameClassViewTreeIndexPat
|
||||
{
|
||||
NSString *classStr = NSStringFromClass([self class]);
|
||||
//cell的子view
|
||||
//UITableView 特殊的superview (UITableViewContentView)
|
||||
//UICollectionViewCell
|
||||
BOOL shouldUseSuperView =
|
||||
([classStr isEqualToString:@"UITableViewCellContentView"]) ||
|
||||
([[self.superview class] isKindOfClass:[UITableViewCell class]])||
|
||||
([[self.superview class] isKindOfClass:[UICollectionViewCell class]]);
|
||||
if (shouldUseSuperView) {
|
||||
return [self obtainIndexPathByView:self.superview];
|
||||
}else {
|
||||
return [self obtainIndexPathByView:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)obtainIndexPathByView:(UIView *)view
|
||||
{
|
||||
NSInteger viewTreeNodeDepth = NSIntegerMin;
|
||||
NSInteger sameViewTreeNodeDepth = NSIntegerMin;
|
||||
|
||||
NSString *classStr = NSStringFromClass([view class]);
|
||||
|
||||
NSMutableArray *sameClassArr = [[NSMutableArray alloc]init];
|
||||
//所处父view的全部subviews根节点深度
|
||||
for (NSInteger index =0; index < view.superview.subviews.count; index ++) {
|
||||
//同类型
|
||||
if ([classStr isEqualToString:NSStringFromClass([view.superview.subviews[index] class])]){
|
||||
[sameClassArr addObject:view.superview.subviews[index]];
|
||||
}
|
||||
if (view == view.superview.subviews[index]) {
|
||||
viewTreeNodeDepth = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//所处父view的同类型subviews根节点深度
|
||||
for (NSInteger index =0; index < sameClassArr.count; index ++) {
|
||||
if (view == sameClassArr[index]) {
|
||||
sameViewTreeNodeDepth = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return [NSString stringWithFormat:@"%ld",sameViewTreeNodeDepth];
|
||||
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 6. 同类型的view,但是点击的意义却不一样。如何唯一标识?
|
||||
|
||||
问题5说明的是在一个界面上有多个不同的 view,他们的类型是同一种(CycleBannerView,但是数据源不一样,那么当数据源长度大于1的时候会轮播,下面会展示 UIPageControl。如果数据源是1个,那么就不会轮播和展示 UIPageControl)。情况6是同一种类型的 View,但是根据展示的内容不一样,点击的意义也不一样。也就是运营需要去知道用户到底点击的是哪一个。如下图所示,「立即抢购」和「分享赚佣金」是同一种类型的 View,但是点击意义不一样,需要我们需要唯一标识出来。之前的方法通过 **“viewPath 配合同类型的 view 去加索引值“** 的方式还是没有办法唯一标识出来。所以想到一个方案,给 NSObject 添加一个分类,在分类里面添加一个协议。让需要复用但需要唯一标识的 view 去实现协议方法,因为是给 NSObject 分类添加的协议,所以 view 不需要去指定遵循。
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
关键步骤:
|
||||
|
||||
- 添加 NSObject 的 Category。在分类里面声明唯一标识的协议
|
||||
|
||||
- 在生成 viewPath 的地方去拿出当前 view 的唯一标识(view 调用协议方法)。然后拼接之前拿出的 viewPath
|
||||
|
||||
|
||||
|
||||
```objective-c
|
||||
//NSObject+UniqueIdentify.h
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class NSObject;
|
||||
@protocol UniqueIdentify<NSObject>
|
||||
|
||||
@optional
|
||||
- (NSString *)setUniqueIdentifier;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSObject (UniqueIdentify)<UniqueIdentify>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
//NSObject+UniqueIdentify.m
|
||||
#import "NSObject+UniqueIdentify.h"
|
||||
|
||||
@implementation NSObject (UniqueIdentify)
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
|
||||
```objective-c
|
||||
//MallTGoodTagView.h
|
||||
|
||||
extern NSString * _Nonnull const ImmediateyPurchase;
|
||||
extern NSString * _Nonnull const ShareToAward;
|
||||
|
||||
//MallTGoodTagView.m
|
||||
NSString *const ImmediateyPurchase = @"立即抢购";
|
||||
NSString *const ShareToAward = @"分享赚佣金";
|
||||
|
||||
- (NSString *)setUniqueIdentifier
|
||||
{
|
||||
if (self.tagString) {
|
||||
return self.tagString;
|
||||
} else {
|
||||
return NSStringFromClass([self class]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
```objective-c
|
||||
//UIResponder Category 生成 viewPath
|
||||
- (NSString *)lbp_identifierKa
|
||||
{
|
||||
// if (self.xq_identifier_ka == nil) {
|
||||
if ([self isKindOfClass:[UIView class]]) {
|
||||
UIView *view = (id)self;
|
||||
NSString *sameViewTreeNode = [view obtainSameSuperViewSameClassViewTreeIndexPath];
|
||||
NSMutableString *str = [NSMutableString string];
|
||||
//特殊的 加减购 因为带有spm但是要区分加减 需要带TreeNode
|
||||
NSString *className = [NSString stringWithUTF8String:object_getClassName(view)];
|
||||
if (!view.accessibilityIdentifier || [className isEqualToString:@"lbpButton"]) {
|
||||
[str appendString:sameViewTreeNode];
|
||||
[str appendString:@","];
|
||||
}
|
||||
while (view.nextResponder) {
|
||||
if ([view respondsToSelector:@selector(setUniqueIdentifier)]) {
|
||||
NSString *unqiueIdentifier = [view setUniqueIdentifier];
|
||||
if (unqiueIdentifier) {
|
||||
[str appendFormat:@"%@,", unqiueIdentifier];
|
||||
}
|
||||
}00
|
||||
[str appendFormat:@"%@,", NSStringFromClass(view.class)];
|
||||
if ([view.class isSubclassOfClass:[UIViewController class]]) {
|
||||
break;
|
||||
}
|
||||
view = (id)view.nextResponder;
|
||||
}
|
||||
self.xq_identifier_ka = [self md5String:[NSString stringWithFormat:@"%@",str]];
|
||||
// self.xq_identifier_ka = [NSString stringWithFormat:@"%@",str];
|
||||
}
|
||||
// }
|
||||
return self.xq_identifier_ka;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 7. 疑惑点
|
||||
|
||||
根据在同一个 view 上会有多个 subview,那么生成的 xpath 会携带在同类型 views 中的索引,所以一个登录、注册按钮的 xpath 可能为 ...btn1、...btn2。那么在版本A上线后运行了一段时间,上传并统计了数据。过了一段时间版本迭代,UI 搞事情,把登录和注册按钮的位置欢乐,变成了注册、登录。按照之前的逻辑生成的 xpath 为 ...btn1、...btn2。那么新的 xpath 虽然唯一,但是点击产生的数据会和之前的埋点数据意义不一样。别怕,你忘了还有一步绑定的逻辑。绑定的这一步会把每次开发的功能,通过可视化界面去将 xpath 和功能名称绑定一下。看下面的动图。所以不用担心虽然生成了唯一的 xpath,但是 App 在不同版本之间 UI 控件位置更换造成之前的统计数据在分析的时候不准确的问题。因为在绑定的时候就将新的 xpath 和功能名称进行了绑定,接口携带版本号。所以分析的时候注意版本号就好了。sql 一句话的事情。
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 8. 数据如何处理
|
||||
|
||||
|
||||
#### A. 如何处理业务数据
|
||||
|
||||
利用系统提供的 **accessibilityIdentifier** 官方给出的解释是标识用户界面元素的字符串
|
||||
|
||||
> */**
|
||||
>
|
||||
> *A string that identifies the user interface element.*
|
||||
>
|
||||
> *default == nil*
|
||||
>
|
||||
> **/*
|
||||
>
|
||||
> **@property**(**nullable**, **nonatomic**, **copy**) NSString *accessibilityIdentifier NS_AVAILABLE_IOS(5_0);
|
||||
|
||||
|
||||
|
||||
服务端下发唯一标识
|
||||
|
||||
接口获取的数据,里面有当前元素的唯一标识。比如在 UITableView 的界面去请求接口拿到数据,那么在在获取到的数据源里面会有一个字段,专门用来存储动态化的经常变动的业务数据。
|
||||
|
||||
```objective-c
|
||||
cell.accessibilityIdentifier = [[[SDGGoodsCategoryServices sharedInstance].categories[indexPath.section] children][indexPath.row].spmContent yy_modelToJSONString];
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### B. 基础数据
|
||||
|
||||
设计上分为2个 pod 库,一个是 TriggerKit(专门用来 hook 机会需要的所有事件,页面停留时间、页面标识、view标识),另一个是 Appmonitor(专门用来提供基础数据、埋点数据的维护、上传机制)。所以在 Appmonitor 里面有个类叫做 UserTrackDataCenter 的类,专门提供一些基础数据(系统版本、操作系统、地理位置、网络等信息)。
|
||||
|
||||
对外暴露出一些方法,用来将埋点数据交给 Appmonitor 去维护埋点数据,达到合适的“机制”再去上传埋点数据到服务端。
|
||||
|
||||
```objective-c
|
||||
+ (void)clickEventUuid:(NSString *)uuid otherParam:(NSDictionary *)otherParam spmContent:(NSDictionary *)spmContent
|
||||
{
|
||||
if (uuid) {
|
||||
NSMutableDictionary *params = [[NSMutableDictionary alloc] initWithDictionary:otherParam];
|
||||
params[SDGStatisticEventtagKey] = @"clickMonitorV1";
|
||||
NSMutableDictionary *valueDict = [[NSMutableDictionary alloc] initWithDictionary:spmContent];
|
||||
valueDict[@"xpath"] = uuid?:@"";
|
||||
params[SDGStatisticEventtagValue] = valueDict?:@{};
|
||||
[[AppMonotior shareInstance] traceEvent:[AMStatisticEvent eventWithInfo:params]];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 9. 曝光时间的统计
|
||||
|
||||
|
||||
|
||||
曝光的意义是什么?
|
||||
|
||||
我们的产品中可能有合作伙伴的广告,我们需要收取服务费。那如何计价?CPM(cost per Mille)每千人成本、CPC(cost per click)每点击成本、CPA(cost per action)每行动成本,根据这些指标来计算价格。或者自己的产品中运营人员在商城中投放了一次新的活动,为了这次活动在某个钻石展位放了设计人员精心设计的炫酷 Banner。这次活动后运营人员想分析在这个图片的作用下有多少人点击了这个活动页。
|
||||
|
||||
|
||||
|
||||
何为曝光?
|
||||
|
||||
一个 view 或者一个组件或者一个资源位在屏幕上可见区域内停留的时间称为一次曝光。那么这个时间怎么统计?有一个点需要注意,那就是当用户在快速滑动的过程中页面上的元素或者组件都会在页面可见区域内快速闪过,那这种算一次曝光吗?当然不算啊,想了想设置了一个时间临界值,大于这个临界值那么算做一次有效的曝光。
|
||||
|
||||
|
||||
|
||||
#### A. 有效曝光的判断
|
||||
|
||||
显示在屏幕可见区域如何判断?一个 View 显示在屏幕可见区域内,那么它肯定是经过从未初始化到初始化,再到设置 Frame 或者 Bounds 或者 Alpha 或者 Hidden 的。且它的根 view 一定是 UIWindow 对象。所以上面这句话进行分析整理就是下面的条件
|
||||
|
||||
- 自身 frame 的改变或者父视图 bounds 的改变
|
||||
- alpha 小于 0.1 或者 hidden 为 YES
|
||||
- 根视图为 window
|
||||
|
||||
对于上面的三点可以用 AOP 进行判断。 hook 掉相应的方法,然后处理判断是否在可见区域内显示。最后的一个点经过一番查找,看到了一个 api `didMoveToWindow` ,根据它可以判断 view 是否显示到屏幕中(文档中说明:当它的 window 对象发送改变的时候会调用 view 的 didMoveToWindow 方法)。
|
||||
|
||||
```
|
||||
Tells the view that its window object changed.
|
||||
|
||||
The default implementation of this method does nothing. Subclasses can override it to perform additional actions whenever the window changes.
|
||||
|
||||
The window property may be nil by the time that this method is called, indicating that the receiver does not currently reside in any window. This occurs when the receiver has just been removed from its superview or when the receiver has just been added to a superview that is not attached to a window. Overrides of this method may choose to ignore such cases if they are not of interest.
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### B. 曝光代码的执行效率优化
|
||||
|
||||
设想一下,某个复杂的页面可能是一个大的 UIViewController 顶部是店铺的基本信息,下面是 2个 UIViewController:左侧负责展示商品的一级、二级、三级分类,且负责选中和未选中的 UI 效果;右侧负责展示商品信息(顶部有商品的排序查找 ,下面是商品展示的 UICollectionView)。由于页面结构复杂,UI 层级嵌套严重,所以代码层面不注意的话,页面上计算量会比较大,CPU 负荷严重,直接影响着手机的 `耗电量`。改进的手段是在合适的地方提前 return 掉(比如 hidden 等于 YES 或者 aplha 小于 0.1 的时候)。
|
||||
|
||||
|
||||
|
||||
另外一个方面就是当用户在滑动页面到感兴趣的模块的时候,开始点击执行某个逻辑,但此时我们的无痕埋点的代码也在偷偷的工作,那么势必会对用户体验造成影响。该方案的改进是监听 `RunLoop`,等到 RunLoop 空闲的时候判断当前 view 是否是一次有效的曝光。
|
||||
|
||||
|
||||
|
||||
实际上发现某些 view 的判断会比较特殊,比如当在 UITableView 的 cell 判断的时候,我们发现 cell 的 superview 为 UITableViewWrapperView 时,我们使用 UITableViewWrapperView 的父视图来计算。
|
||||
|
||||
iOS 11 以下 UITableViewWrapperView 大小为屏幕中第一个完整的屏幕大小视图,且会随着 contentOffset 的改变而改变。所以当 UITableViewWrapperView 滑出屏幕可见区域的时候,cell 判断父视图是否可见的时候不准确。
|
||||
|
||||
|
||||
|
||||
整个流程见下面的流程图。
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 10. 数据的上报
|
||||
|
||||
|
||||
|
||||
数据通过上面的办法收集完了,那么如何及时、高效的上传到后端,给运营分析、处理呢?
|
||||
|
||||
App 运行期间用户会点击非常多的数据,如果实时上传的话对于网络的利用率较低,所以需要考虑一个机制去控制用户产生的埋点数据的上传。
|
||||
|
||||
|
||||
|
||||
思路是这样的。对外部暴露出一个接口,用来将产生的数据往数据中心存储。用户产生的数据会先保存到 AppMonitor 的内存中去,设置一个临界值(memoryEventMax = 50),如果存储的值达到设置的临界值 memoryEventMax,那么将内存中的数据写入文件系统,以 zip 的形式保存下来,然后上传到埋点系统。如果没有达到临界值但是存在一些 App 状态切换的情况,这时候需要及时保存数据到持久化。当下次打开 App 就去从本地持久化的地方读取是否有未上传的数据,如果有就上传日志信息,成功后删除本地的日志压缩包。
|
||||
|
||||
App 应用状态的切换策略如下:
|
||||
|
||||
- didFinishLaunchWithOptions:内存日志信息写入硬盘
|
||||
- didBecomeActive:上传
|
||||
- willTerimate:内存日志信息写入硬盘
|
||||
- didEnterBackground:内存日志信息写入硬盘
|
||||
|
||||
下面的代码是 App 埋点数据的保存与上传
|
||||
|
||||
```objective-c
|
||||
// 将App日志信息写入到内存中。当内存中的数量到达一定规模(超过设置的内存中存储的数量)的时候就将内存中的日志存储到文件信息中
|
||||
- (void)joinEvent:(NSDictionary *)dictionary
|
||||
{
|
||||
if (dictionary) {
|
||||
NSDictionary *tmp = [self createDicWithEvent:dictionary];
|
||||
if (!s_memoryArray) {
|
||||
s_memoryArray = [NSMutableArray array];
|
||||
}
|
||||
[s_memoryArray addObject:tmp];
|
||||
if ([s_memoryArray count] >= s_flushNum) {
|
||||
[self writeEventLogsInFilesCompletion:^{
|
||||
[self startUploadLogFile];
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 外界调用的数据传递入口(App埋点统计)
|
||||
- (void)traceEvent:(AMStatisticEvent *)event
|
||||
{
|
||||
// 线程锁,防止多处调用产生并发问题
|
||||
@synchronized (self) {
|
||||
if (event && event.userInfo) {
|
||||
[self joinEvent:event.userInfo];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将内存中的数据写入到文件中,持久化存储
|
||||
- (void)writeEventLogsInFilesCompletion:(void(^)(void))completionBlock
|
||||
{
|
||||
NSArray *tmp = nil;
|
||||
@synchronized (self) {
|
||||
tmp = s_memoryArray;
|
||||
s_memoryArray = nil;
|
||||
}
|
||||
if (tmp) {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSString *jsonFilePath = [weakSelf createTraceJsonFile];
|
||||
if ([weakSelf writeArr:tmp toFilePath:jsonFilePath]) {
|
||||
NSString *zipedFilePath = [weakSelf zipJsonFile:jsonFilePath];
|
||||
if (zipedFilePath) {
|
||||
[AppMonotior clearCacheFile:jsonFilePath];
|
||||
if (completionBlock) {
|
||||
completionBlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 从App埋点统计压缩包文件夹中的每个压缩包文件上传服务端,成功后就删除本地的日志压缩包
|
||||
- (void)startUploadLogFile
|
||||
{
|
||||
NSArray *fList = [self listFilesAtPath:[self eventJsonPath]];
|
||||
if (!fList || [fList count] == 0) {
|
||||
return;
|
||||
}
|
||||
[fList enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
||||
if (![obj hasSuffix:@".zip"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *zipedPath = obj;
|
||||
unsigned long long fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:zipedPath error:nil] fileSize];
|
||||
if (!fileSize || fileSize < 1) {
|
||||
return;
|
||||
}
|
||||
// 调用接口上传埋点数据
|
||||
[self uploadZipFileWithPath:zipedPath completion:^(NSString *completionResult) {
|
||||
if ([completionResult isEqual:@"OK"]) {
|
||||
[AppMonotior clearCacheFile:zipedPath];
|
||||
}
|
||||
}];
|
||||
}];
|
||||
}
|
||||
```
|
||||
|
||||
使用的时候就是在 hook 系统事件的时候,去调用统计页面上传数据
|
||||
|
||||
```objective-c
|
||||
//UIViewController
|
||||
[UserTrackDataCenter openPage:[self getPageUrl:className] fromPage:refer]; // 页面出现
|
||||
[UserTrackDataCenter leavePage:[self getPageUrl:className] spendTime:[self p_calculationTimeSpend]]; //页面消失
|
||||
```
|
||||
|
||||
|
||||

|
||||
|
||||
总结下来关键步骤:
|
||||
|
||||
1. hook 系统的各种事件(UIResponder、UITableView、UICollectionView代理事件、UIControl事件、UITapGestureRecognizers)、hook 应用程序、控制器生命周期。在做本来的逻辑之前添加额外的监控代码
|
||||
2. 对于点击的元素按照视图树生成对应的唯一标识(addCartButton.GoodsView.GoodsViewController) 的 md5 值
|
||||
3. 在业务开发完毕,进入埋点的编辑模式,将 md5 和关键的页面的关键事件(运营、产品想统计的关键模块:App层级、业务模块、关键页面、关键操作)给绑定起来。比如 addCartButton.GoodsView.GoodsViewController.tbApp 对应了 tbApp-商城模块-商品详情页-加入购物车功能。
|
||||
4. 将所需要的数据存储下来
|
||||
5. 设计机制等到合适的时机去上传数据
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 举例说明一个完整的埋点上报流程
|
||||
|
||||
埋伏模块分为2个pod组件库,TriggerKit 负责拦截系统事件,拿到埋点数据。Appmonitor 负责收集埋点数据,本地持久化或内存储存,等到合适时机去上传埋点数据。
|
||||
|
||||
|
||||
|
||||
1. 通过接口获取数据,给对应的 view 的 accessibilityIdentifier 属性绑定埋点数据
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
2. hook 系统事件,点击拿到 view,获取 accessibilityIdentifier 属性值
|
||||
|
||||

|
||||
|
||||
3. 将数据向的数据中心发送,数据中心处理数据(埋点数据结合App基础信息,图上 UserTrackDataCenter 对象)。根据情况将数据存储到内存或者本地,等到合适的时机去上传
|
||||
|
||||

|
||||
682
Chapter1 - iOS/1.56.md
Normal file
682
Chapter1 - iOS/1.56.md
Normal file
@@ -0,0 +1,682 @@
|
||||
# 大前端时代安全性如何做
|
||||
|
||||
> 之前在做过一些爬虫的工作,也帮助爬虫工程师解决过一些问题。然后我写过一些文章发布到网上,之后有一些人就找我做一些爬虫的外包,内容大概是爬取小红书的用户数据和商品数据,但是我没做。我觉得对于国内的大数据公司没几家是有真正的大数据量,而是通过爬虫工程师团队不断的去各地爬取数据,因此不要以为我们的数据没价值,对于内容型的公司来说,数据是可信竞争力。那么我接下来想说的就是网络和数据的安全性问题。
|
||||
> 对于内容型的公司,数据的安全性很重要。对于内容公司来说,数据的重要性不言而喻。比如你一个做在线教育的平台,题目的数据很重要吧,但是被别人通过爬虫技术全部爬走了?如果核心竞争力都被拿走了,那就是凉凉。再比说有个独立开发者想抄袭你的产品,通过抓包和爬虫手段将你核心的数据拿走,然后短期内做个网站和 App,短期内成为你的劲敌。
|
||||
|
||||
|
||||
|
||||
## 一、爬虫手段
|
||||
|
||||
目前爬虫技术都是从渲染好的 html 页面直接找到感兴趣的节点,然后获取对应的文本.
|
||||
有些网站安全性做的好,比如列表页可能好获取,但是详情页就需要从列表页点击对应的 item,将 itemId 通过 form 表单提交,服务端生成对应的参数,然后重定向到详情页(重定向过来的地址后才带有详情页的参数 detailID),这个步骤就可以拦截掉一部分的爬虫开发者
|
||||
|
||||
|
||||
|
||||
## 二、制定出**Web 端反爬技术方案**
|
||||
|
||||
从这2个角度(网页所见非所得、查接口请求没用)出发,制定了下面的反爬方案。
|
||||
|
||||
|
||||
1. 使用HTTPS 协议
|
||||
2. 单位时间内限制掉请求次数过多,则封锁该账号
|
||||
3. 前端技术限制 (接下来是核心技术)
|
||||
|
||||
举例:比如需要正确显示的数据为“19950220”
|
||||
|
||||
#### 2.1 原始数据加密
|
||||
|
||||
1. 先按照自己需求利用相应的规则(数字乱序映射,比如正常的0对应还是0,但是乱序就是 0 <-> 1,1 <-> 9,3 <-> 8,...)制作自定义字体(ttf)
|
||||
2. 根据上面的乱序映射规律,求得到需要返回的数据 19950220 -> 17730220
|
||||
3. 对于第一步得到的字符串,依次遍历每个字符,将每个字符根据按照线性变换(y=kx+b)。线性方程的系数和常数项是根据当前的日期计算得到的。比如当前的日期为“2018-07-24”,那么线性变换的 k 为 7,b 为 24。
|
||||
4. 然后将变换后的每个字符串用“3.1415926”拼接返回给接口调用者。(为什么是3.1415926,因为对数字伪造反爬,所以拼接的文本肯定是数字的话不太会引起研究者的注意,但是数字长度太短会误伤正常的数据,所以用所熟悉的 Π)
|
||||
|
||||
```
|
||||
1773 -> “1*7+24” + “3.1415926” + “7*7+24” + “3.1415926” + “7*7+24” + “3.1415926” + “3*7+24” -> 313.1415926733.1415926733.141592645
|
||||
02 -> "0*7+24" + "3.1415926" + "2*7+24" -> 243.141592638
|
||||
20 -> "2*7+24" + "3.1415926" + "0*7+24" -> 383.141592624
|
||||
````
|
||||
|
||||
#### 2.2 前端拿到数据后再解密,解密后根据自定义的字体 Render 页面
|
||||
|
||||
1. 先将拿到的字符串按照“3.1415926”拆分为数组
|
||||
2. 对数组的每1个数据,按照“线性变换”(y=kx+b,k和b同样按照当前的日期求解得到),逆向求解到原本的值。
|
||||
3. 将步骤2的的到的数据依次拼接,再根据 ttf 文件 Render 页面上。
|
||||
|
||||
|
||||
#### 2.3 后端需要根据上一步设计的协议将数据进行加密处理
|
||||
|
||||
下面以 **Node.js** 为例讲解后端需要做的事情
|
||||
|
||||
1. 首先后端设置接口路由
|
||||
2. 获取路由后面的参数
|
||||
3. 根据业务需要根据 SQL 语句生成对应的数据。如果是数字部分,则需要按照上面约定的方法加以转换
|
||||
4. 将生成数据转换成 JSON 返回给调用者
|
||||
|
||||
```js
|
||||
// json
|
||||
var JoinOparatorSymbol = "3.1415926";
|
||||
function encode(rawData, ruleType) {
|
||||
if (!isNotEmptyStr(rawData)) {
|
||||
return "";
|
||||
}
|
||||
var date = new Date();
|
||||
var year = date.getFullYear();
|
||||
var month = date.getMonth() + 1;
|
||||
var day = date.getDate();
|
||||
|
||||
var encodeData = "";
|
||||
for (var index = 0; index < rawData.length; index++) {
|
||||
var datacomponent = rawData[index];
|
||||
if (!isNaN(datacomponent)) {
|
||||
if (ruleType < 3) {
|
||||
var currentNumber = rawDataMap(String(datacomponent), ruleType);
|
||||
encodeData += (currentNumber * month + day) + JoinOparatorSymbol;
|
||||
}
|
||||
else if (ruleType == 4) {
|
||||
encodeData += rawDataMap(String(datacomponent), ruleType);
|
||||
}
|
||||
else {
|
||||
encodeData += rawDataMap(String(datacomponent), ruleType) + JoinOparatorSymbol;
|
||||
}
|
||||
}
|
||||
else if (ruleType == 4) {
|
||||
encodeData += rawDataMap(String(datacomponent), ruleType);
|
||||
}
|
||||
|
||||
}
|
||||
if (encodeData.length >= JoinOparatorSymbol.length) {
|
||||
var lastTwoString = encodeData.substring(encodeData.length - JoinOparatorSymbol.length, encodeData.length);
|
||||
if (lastTwoString == JoinOparatorSymbol) {
|
||||
encodeData = encodeData.substring(0, encodeData.length - JoinOparatorSymbol.length);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
//字体映射处理
|
||||
function rawDataMap(rawData, ruleType) {
|
||||
|
||||
if (!isNotEmptyStr(rawData) || !isNotEmptyStr(ruleType)) {
|
||||
return;
|
||||
}
|
||||
var mapData;
|
||||
var rawNumber = parseInt(rawData);
|
||||
var ruleTypeNumber = parseInt(ruleType);
|
||||
if (!isNaN(rawData)) {
|
||||
lastNumberCategory = ruleTypeNumber;
|
||||
//字体文件1下的数据加密规则
|
||||
if (ruleTypeNumber == 1) {
|
||||
if (rawNumber == 1) {
|
||||
mapData = 1;
|
||||
}
|
||||
else if (rawNumber == 2) {
|
||||
mapData = 2;
|
||||
}
|
||||
else if (rawNumber == 3) {
|
||||
mapData = 4;
|
||||
}
|
||||
else if (rawNumber == 4) {
|
||||
mapData = 5;
|
||||
}
|
||||
else if (rawNumber == 5) {
|
||||
mapData = 3;
|
||||
}
|
||||
else if (rawNumber == 6) {
|
||||
mapData = 8;
|
||||
}
|
||||
else if (rawNumber == 7) {
|
||||
mapData = 6;
|
||||
}
|
||||
else if (rawNumber == 8) {
|
||||
mapData = 9;
|
||||
}
|
||||
else if (rawNumber == 9) {
|
||||
mapData = 7;
|
||||
}
|
||||
else if (rawNumber == 0) {
|
||||
mapData = 0;
|
||||
}
|
||||
}
|
||||
//字体文件2下的数据加密规则
|
||||
else if (ruleTypeNumber == 0) {
|
||||
|
||||
if (rawNumber == 1) {
|
||||
mapData = 4;
|
||||
}
|
||||
else if (rawNumber == 2) {
|
||||
mapData = 2;
|
||||
}
|
||||
else if (rawNumber == 3) {
|
||||
mapData = 3;
|
||||
}
|
||||
else if (rawNumber == 4) {
|
||||
mapData = 1;
|
||||
}
|
||||
else if (rawNumber == 5) {
|
||||
mapData = 8;
|
||||
}
|
||||
else if (rawNumber == 6) {
|
||||
mapData = 5;
|
||||
}
|
||||
else if (rawNumber == 7) {
|
||||
mapData = 6;
|
||||
}
|
||||
else if (rawNumber == 8) {
|
||||
mapData = 7;
|
||||
}
|
||||
else if (rawNumber == 9) {
|
||||
mapData = 9;
|
||||
}
|
||||
else if (rawNumber == 0) {
|
||||
mapData = 0;
|
||||
}
|
||||
}
|
||||
//字体文件3下的数据加密规则
|
||||
else if (ruleTypeNumber == 2) {
|
||||
|
||||
if (rawNumber == 1) {
|
||||
mapData = 6;
|
||||
}
|
||||
else if (rawNumber == 2) {
|
||||
mapData = 2;
|
||||
}
|
||||
else if (rawNumber == 3) {
|
||||
mapData = 1;
|
||||
}
|
||||
else if (rawNumber == 4) {
|
||||
mapData = 3;
|
||||
}
|
||||
else if (rawNumber == 5) {
|
||||
mapData = 4;
|
||||
}
|
||||
else if (rawNumber == 6) {
|
||||
mapData = 8;
|
||||
}
|
||||
else if (rawNumber == 7) {
|
||||
mapData = 3;
|
||||
}
|
||||
else if (rawNumber == 8) {
|
||||
mapData = 7;
|
||||
}
|
||||
else if (rawNumber == 9) {
|
||||
mapData = 9;
|
||||
}
|
||||
else if (rawNumber == 0) {
|
||||
mapData = 0;
|
||||
}
|
||||
}
|
||||
else if (ruleTypeNumber == 3) {
|
||||
|
||||
if (rawNumber == 1) {
|
||||
mapData = "";
|
||||
}
|
||||
else if (rawNumber == 2) {
|
||||
mapData = "";
|
||||
}
|
||||
else if (rawNumber == 3) {
|
||||
mapData = "";
|
||||
}
|
||||
else if (rawNumber == 4) {
|
||||
mapData = "";
|
||||
}
|
||||
else if (rawNumber == 5) {
|
||||
mapData = "";
|
||||
}
|
||||
else if (rawNumber == 6) {
|
||||
mapData = "";
|
||||
}
|
||||
else if (rawNumber == 7) {
|
||||
mapData = "";
|
||||
}
|
||||
else if (rawNumber == 8) {
|
||||
mapData = "";
|
||||
}
|
||||
else if (rawNumber == 9) {
|
||||
mapData = "";
|
||||
}
|
||||
else if (rawNumber == 0) {
|
||||
mapData = "";
|
||||
}
|
||||
}
|
||||
else{
|
||||
mapData = rawNumber;
|
||||
}
|
||||
} else if (ruleTypeNumber == 4) {
|
||||
var sources = ["年", "万", "业", "人", "信", "元", "千", "司", "州", "资", "造", "钱"];
|
||||
//判断字符串为汉字
|
||||
if (/^[\u4e00-\u9fa5]*$/.test(rawData)) {
|
||||
|
||||
if (sources.indexOf(rawData) > -1) {
|
||||
var currentChineseHexcod = rawData.charCodeAt(0).toString(16);
|
||||
var lastCompoent;
|
||||
var mapComponetnt;
|
||||
var numbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
||||
var characters = ["a", "b", "c", "d", "e", "f", "g", "h", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
|
||||
|
||||
if (currentChineseHexcod.length == 4) {
|
||||
lastCompoent = currentChineseHexcod.substr(3, 1);
|
||||
var locationInComponents = 0;
|
||||
if (/[0-9]/.test(lastCompoent)) {
|
||||
locationInComponents = numbers.indexOf(lastCompoent);
|
||||
mapComponetnt = numbers[(locationInComponents + 1) % 10];
|
||||
}
|
||||
else if (/[a-z]/.test(lastCompoent)) {
|
||||
locationInComponents = characters.indexOf(lastCompoent);
|
||||
mapComponetnt = characters[(locationInComponents + 1) % 26];
|
||||
}
|
||||
mapData = "&#x" + currentChineseHexcod.substr(0, 3) + mapComponetnt + ";";
|
||||
}
|
||||
} else {
|
||||
mapData = rawData;
|
||||
}
|
||||
|
||||
}
|
||||
else if (/[0-9]/.test(rawData)) {
|
||||
mapData = rawDataMap(rawData, 2);
|
||||
}
|
||||
else {
|
||||
mapData = rawData;
|
||||
}
|
||||
|
||||
}
|
||||
return mapData;
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
//api
|
||||
module.exports = {
|
||||
"GET /api/products": async (ctx, next) => {
|
||||
ctx.response.type = "application/json";
|
||||
ctx.response.body = {
|
||||
products: products
|
||||
};
|
||||
},
|
||||
|
||||
"GET /api/solution1": async (ctx, next) => {
|
||||
|
||||
try {
|
||||
var data = fs.readFileSync(pathname, "utf-8");
|
||||
ruleJson = JSON.parse(data);
|
||||
rule = ruleJson.data.rule;
|
||||
} catch (error) {
|
||||
console.log("fail: " + error);
|
||||
}
|
||||
|
||||
var data = {
|
||||
code: 200,
|
||||
message: "success",
|
||||
data: {
|
||||
name: "@杭城小刘",
|
||||
year: LBPEncode("1995", rule),
|
||||
month: LBPEncode("02", rule),
|
||||
day: LBPEncode("20", rule),
|
||||
analysis : rule
|
||||
}
|
||||
}
|
||||
|
||||
ctx.set("Access-Control-Allow-Origin", "*");
|
||||
ctx.response.type = "application/json";
|
||||
ctx.response.body = data;
|
||||
},
|
||||
|
||||
|
||||
"GET /api/solution2": async (ctx, next) => {
|
||||
try {
|
||||
var data = fs.readFileSync(pathname, "utf-8");
|
||||
ruleJson = JSON.parse(data);
|
||||
rule = ruleJson.data.rule;
|
||||
} catch (error) {
|
||||
console.log("fail: " + error);
|
||||
}
|
||||
|
||||
var data = {
|
||||
code: 200,
|
||||
message: "success",
|
||||
data: {
|
||||
name: LBPEncode("建造师",rule),
|
||||
birthday: LBPEncode("1995年02月20日",rule),
|
||||
company: LBPEncode("中天公司",rule),
|
||||
address: LBPEncode("浙江省杭州市拱墅区石祥路",rule),
|
||||
bidprice: LBPEncode("2万元",rule),
|
||||
negative: LBPEncode("2018年办事效率太高、负面基本没有",rule),
|
||||
title: LBPEncode("建造师",rule),
|
||||
honor: LBPEncode("最佳奖",rule),
|
||||
analysis : rule
|
||||
}
|
||||
}
|
||||
ctx.set("Access-Control-Allow-Origin", "*");
|
||||
ctx.response.type = "application/json";
|
||||
ctx.response.body = data;
|
||||
},
|
||||
|
||||
"POST /api/products": async (ctx, next) => {
|
||||
var p = {
|
||||
name: ctx.request.body.name,
|
||||
price: ctx.request.body.price
|
||||
};
|
||||
products.push(p);
|
||||
ctx.response.type = "application/json";
|
||||
ctx.response.body = p;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
```javascript
|
||||
//路由
|
||||
const fs = require("fs");
|
||||
|
||||
function addMapping(router, mapping){
|
||||
for(var url in mapping){
|
||||
if (url.startsWith("GET")) {
|
||||
var path = url.substring(4);
|
||||
router.get(path,mapping[url]);
|
||||
console.log(`Register URL mapping: GET: ${path}`);
|
||||
}else if (url.startsWith('POST ')) {
|
||||
var path = url.substring(5);
|
||||
router.post(path, mapping[url]);
|
||||
console.log(`Register URL mapping: POST ${path}`);
|
||||
} else if (url.startsWith('PUT ')) {
|
||||
var path = url.substring(4);
|
||||
router.put(path, mapping[url]);
|
||||
console.log(`Register URL mapping: PUT ${path}`);
|
||||
} else if (url.startsWith('DELETE ')) {
|
||||
var path = url.substring(7);
|
||||
router.del(path, mapping[url]);
|
||||
console.log(`Register URL mapping: DELETE ${path}`);
|
||||
} else {
|
||||
console.log(`Invalid URL: ${url}`);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function addControllers(router, dir){
|
||||
fs.readdirSync(__dirname + "/" + dir).filter( (f) => {
|
||||
return f.endsWith(".js");
|
||||
}).forEach( (f) => {
|
||||
console.log(`Process controllers:${f}...`);
|
||||
let mapping = require(__dirname + "/" + dir + "/" + f);
|
||||
addMapping(router,mapping);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function(dir){
|
||||
let controllers = dir || "controller";
|
||||
let router = require("koa-router")();
|
||||
|
||||
addControllers(router,controllers);
|
||||
return router.routes();
|
||||
};
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 2.4 前端根据服务端返回的数据逆向解密
|
||||
|
||||
```javascript
|
||||
$("#year").html(getRawData(data.year,log));
|
||||
|
||||
// util.js
|
||||
var JoinOparatorSymbol = "3.1415926";
|
||||
function isNotEmptyStr($str) {
|
||||
if (String($str) == "" || $str == undefined || $str == null || $str == "null") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getRawData($json,analisys) {
|
||||
$json = $json.toString();
|
||||
if (!isNotEmptyStr($json)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var date= new Date();
|
||||
var year = date.getFullYear();
|
||||
var month = date.getMonth() + 1;
|
||||
var day = date.getDate();
|
||||
var datacomponents = $json.split(JoinOparatorSymbol);
|
||||
var orginalMessage = "";
|
||||
for(var index = 0;index < datacomponents.length;index++){
|
||||
var datacomponent = datacomponents[index];
|
||||
if (!isNaN(datacomponent) && analisys < 3){
|
||||
var currentNumber = parseInt(datacomponent);
|
||||
orginalMessage += (currentNumber - day)/month;
|
||||
}
|
||||
else if(analisys == 3){
|
||||
orginalMessage += datacomponent;
|
||||
}
|
||||
else{
|
||||
//其他情况待续,本 Demo 根据本人在研究反爬方面的技术并实践后持续更新
|
||||
}
|
||||
}
|
||||
return orginalMessage;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
比如后端返回的是323.14743.14743.1446,根据我们约定的算法,可以的到结果为1773
|
||||
|
||||
|
||||
|
||||
#### 2.5 根据 ttf 文件 Render 页面
|
||||

|
||||
上面计算的到的1773,然后根据ttf文件,页面看到的就是1995
|
||||
|
||||
#### 2.6 加密混淆
|
||||
为了防止爬虫人员查看 JS 研究问题,所以对 JS 的文件进行了加密处理。如果你的技术栈是 Vue 、React 等,webpack 为你提供了 JS 加密的插件,也很方便处理
|
||||
|
||||
[JS混淆工具](http://www.javascriptobfuscator.com/Javascript-Obfuscator.aspx)
|
||||
|
||||
- 个人觉得这种方式还不是很安全。于是想到了各种方案的组合拳。比如
|
||||
|
||||
|
||||
|
||||
|
||||
## 三、反爬升级版
|
||||
|
||||
个人觉得如果一个前端经验丰富的爬虫开发者来说,上面的方案可能还是会存在被破解的可能,所以在之前的基础上做了升级版本
|
||||
|
||||
1. 组合拳1: 字体文件不要固定,虽然请求的链接是同一个,但是根据当前的时间戳的最后一个数字取模,比如 Demo 中对4取模,有4种值 0、1、2、3。这4种值对应不同的字体文件,所以当爬虫绞尽脑汁爬到1种情况下的字体时,没想到再次请求,字体文件的规则变掉了 😂
|
||||
2. 组合拳2: 前面的规则是字体问题乱序,但是只是数字匹配打乱掉。比如 **1** -> **4**, **5** -> **8**。接下来的套路就是每个数字对应一个 **unicode 码** ,然后制作自己需要的字体,可以是 .ttf、.woff 等等。
|
||||
|
||||

|
||||

|
||||
|
||||
这几种组合拳打下来。对于一般的爬虫就放弃了。
|
||||
|
||||
|
||||
|
||||
## 四、反爬手段再升级
|
||||
|
||||
上面说的方法主要是针对**数字**做的反爬手段,如果要对汉字进行反爬怎么办?接下来提供几种方案
|
||||
|
||||
1. **方案1:** 对于你站点频率最高的词云,做一个汉字映射,也就是自定义字体文件,步骤跟数字一样。先将常用的汉字生成对应的 ttf 文件;根据下面提供的链接,将 ttf 文件转换为 svg 文件,然后在下面的“字体映射”链接点进去的网站上面选择前面生成的 svg 文件,将svg文件里面的每个汉字做个映射,也就是将汉字专为 unicode 码(注意这里的 unicode 码不要去在线直接生成,因为直接生成的东西也就是有规律的。我给的做法是先用网站生成,然后将得到的结果做个简单的变化,比如将“e342”转换为 “e231”);然后接口返回的数据按照我们的这个字体文件的规则反过去映射出来。
|
||||
|
||||
2. **方案2:** 将网站的重要字体,将 html 部分生成图片,这样子爬虫要识别到需要的内容成本就很高了,需要用到 OCR。效率也很低。所以可以拦截钓一部分的爬虫
|
||||
|
||||
3. **方案3:** 看到携程的技术分享“反爬的最高境界就是 Canvas 的指纹,原理是不同的机器不同的硬件对于 Canvas 画出的图总是存在像素级别的误差,因此我们判断当对于访问来说大量的 canvas 的指纹一致的话,则认为是爬虫,则可以封掉它”。
|
||||
|
||||
本人将方案1实现到 Demo 中了。
|
||||
|
||||
|
||||
|
||||
#### 关键步骤
|
||||
|
||||
1. 先根据你们的产品找到常用的关键词,生成**词云**
|
||||
2. 根据词云,将每个字生成对应的 unicode 码
|
||||
3. 将词云包括的汉字做成一个字体库
|
||||
4. 将字体库 .ttf 做成 svg 格式,然后上传到 [icomoon](https://icomoon.io/app/#/select/font) 制作自定义的字体,但是有规则,比如 **“年”** 对应的 **unicode 码**是 **“\u5e74”** ,但是我们需要做一个 **恺撒加密** ,比如我们设置 **偏移量** 为1,那么经过**恺撒加密** **“年”**对应的 **unicode** 码是**“\u5e75”** 。利用这种规则制作我们需要的字体库
|
||||
5. 在每次调用接口的时候服务端做的事情是:服务端封装某个方法,将数据经过方法判断是不是在词云中,如果是词云中的字符,利用规则(找到汉字对应的 unicode 码,再根据凯撒加密,设置对应的偏移量,Demo 中为1,将每个汉字加密处理)加密处理后返回数据
|
||||
6. 客户端做的事情:
|
||||
- 先引入我们前面制作好的汉字字体库
|
||||
- 调用接口拿到数据,显示到对应的 Dom 节点上
|
||||
- 如果是汉字文本,我们将对应节点的 css 类设置成汉字类,该类对应的 font-family 是我们上面引入的汉字字体库
|
||||
|
||||
```css
|
||||
//style.css
|
||||
@font-face {
|
||||
font-family: "NumberFont";
|
||||
src: url('http://127.0.0.1:8080/Util/analysis');
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "CharacterFont";
|
||||
src: url('http://127.0.0.1:8080/Util/map');
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: "NumberFont";
|
||||
}
|
||||
|
||||
h3,a{
|
||||
font-family: "CharacterFont";
|
||||
}
|
||||
```
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
传送门:[字体制作的步骤](https://blog.csdn.net/fdipzone/article/details/68166388)、[ttf转svg](https://everythingfonts.com/ttf-to-svg)、[字体映射规则](https://icomoon.io/app/#/select/font)
|
||||
|
||||
实现效果:
|
||||
|
||||
1. 页面上看到的数据跟审查元素看到的结果不一致
|
||||
2. 去查看接口数据跟审核元素和界面看到的三者不一致
|
||||
3. 页面每次刷新之前得出的结果更不一致
|
||||
4. 对于数字和汉字的处理手段都不一致
|
||||
|
||||
这几种组合拳打下来。对于一般的爬虫就放弃了。
|
||||
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
<hr>
|
||||
|
||||
前面的 ttf 转 svg 网站当 ttf 文件太大会限制转换,让你购买,贴出个新链接。[ttf转svg](https://convertio.co/zh/font-converter/)
|
||||
|
||||
|
||||
|
||||
## [Demo 地址](https://github.com/FantasticLBP/Anti-WebSpider)
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
运行步骤
|
||||
|
||||
```powershell
|
||||
//客户端。先查看本机 ip 在 Demo/Spider-develop/Solution/Solution1.js 和 Demo/Spider-develop/Solution/Solution2.js 里面将接口地址修改为本机 ip
|
||||
|
||||
$ cd Demo
|
||||
$ ls
|
||||
REST Spider-release file-Server.js
|
||||
Spider-develop Util rule.json
|
||||
$ node file-Server.js
|
||||
Server is runnig at http://127.0.0.1:8080/
|
||||
|
||||
//服务端 先安装依赖
|
||||
$ cd REST/
|
||||
$ npm install
|
||||
$ node app.js
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
### 五、 App 端安全的解决方案
|
||||
|
||||
目前 App 的网络通信基本都是用 HTTPS 的服务,但是随便一个抓包工具都是可以看到 HTTPS 接口的详细数据,为了做到防止抓包和无法模拟接口的情况,我们采取以下措施:
|
||||
|
||||
1. 中间人盗用数据,我们可以采取 HTTPS 证书的双向认证,这样子实现的效果就是中间人在开启抓包软件分析 App 的网络请求的时候,网络会自动断掉,无法查看分析请求的情况
|
||||
2. 对于防止用户模仿我们的请求再次发起请求,我们可以采用 「防重放策略」,用户再也无法模仿我们的请求,再次去获取数据了。
|
||||
3. 对于 App 内的 H5 资源,反爬虫方案可以采用上面的解决方案,H5 内部的网络请求可以通过 Hybrid 层让 Native 的能力去完成网络请求,完成之后将数据回调给 JS。这么做的目的是往往我们的 Native 层有完善的账号体系和网络层以及良好的安全策略、鉴权体系等等。
|
||||
<details>
|
||||
<summary>JS端发起网络请求代码:点击展开</summary>
|
||||
|
||||
```Javascript
|
||||
var requestObject = {
|
||||
url: arg.Api + "SearchInfo/getLawsInfo",
|
||||
params: requestparams,
|
||||
Hybrid_Request_Method: 0
|
||||
};
|
||||
requestHybrid({
|
||||
tagname: 'NativeRequest',
|
||||
param: requestObject,
|
||||
encryption: 1,
|
||||
callback: function (data) {
|
||||
renderUI(data);
|
||||
}
|
||||
})
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Objective-C代码:点击展开</summary>
|
||||
|
||||
```Objective-C
|
||||
[self.bridge registerHandler:@"NativeRequest" handler:^(id data, WVJBResponseCallback responseCallback) {
|
||||
|
||||
NSAssert([data isKindOfClass:[NSDictionary class]], @"H5 端不按套路");
|
||||
if ([data isKindOfClass:[NSDictionary class]]) {
|
||||
|
||||
NSDictionary *dict = (NSDictionary *)data;
|
||||
RequestModel *requestModel = [RequestModel yy_modelWithJSON:dict];
|
||||
NSAssert( (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Post) || (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Get ), @"H5 端不按套路");
|
||||
|
||||
[HybridRequest requestWithNative:requestModel hybridRequestSuccess:^(id responseObject) {
|
||||
|
||||
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableLeaves error:nil];
|
||||
responseCallback([self convertToJsonData:@{@"success":@"1",@"data":json}]);
|
||||
|
||||
} hybridRequestfail:^{
|
||||
|
||||
LBPLog(@"H5 call Native`s request failed");
|
||||
responseCallback([self convertToJsonData:@{@"success":@"0",@"data":@""}]);
|
||||
}];
|
||||
}
|
||||
}];
|
||||
```
|
||||
</details>
|
||||
|
||||
4. 有些人觉得利用 RSA 加密虽然可以保证数据的安全,但是因为每次都是大量字符串的运算,觉得数据量大的情况下用 RSA 加解密会非常耗时。对,肯定耗时,所以较好的做法就是将通信的 Alice 和 Bob 两方利用 **RSA** 的方式交换密钥。然后两方在通信的时候数据内容采用**对称加密**的方式进行。但是私钥在本地如何存放呢?想到的办法就是将关键密钥的字符串提高到较高的安全级别,比如这个文件用加密保存。接下来推荐一个[工具](https://github.com/RNCryptor/RNCryptor),可以将代码文件进行加密保存和解密访问。
|
||||
|
||||
|
||||
|
||||
|
||||
## 六、 数据安全(反爬虫)之「防重放」策略
|
||||
|
||||
虽然话题都是大前端时代的安全性,但是防重放策略篇幅较长,开了新的[章节](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.8.md)。感兴趣的同学请移步查看。
|
||||
|
||||
|
||||
|
||||
|
||||
## 七、Canvas 反爬虫技术方案
|
||||
|
||||
> 后期打到白热化的时候,用的技术越来越匪夷所思。举个例子,很多人会提,做反爬虫会用到canvas指纹,并认为是最高境界。其实这个东西对于反爬虫来说也只是个辅助,canvas指纹的含义是,因为不同硬件对canvas支持不同,因此你只要画一个很复杂的canvas,那么得出的image,总是存在像素级别的误差。考虑到爬虫代码都是统一的,就算起selenium,也是ghost的,因此指纹一般都是一致的,因此绕过几率非常低。<br><br>但是!这个东西天生有两个缺陷。第一是,无法验证合法性。当然了,你可以用非对称加密来保证合法,但是这个并不靠谱。其次,canvas的冲突概率非常高,远远不是作者宣称的那样,冲突率极低。也许在国外冲突是比较低,因为国外的语言比较多。但是国内公司通常是IT统一装机,无论是软件还是硬件都惊人的一致。我们测试canvas指纹的时候,在携程内部随便找了20多台机器,得出的指纹都完全一样,一丁点差别都没有。因此,有些“高级技巧”其实一点都不实用。<br><br>浏览器指纹技术常用于客户端跟踪及反机器人的场景。核心思路是, 不同浏览器、操作系统、以及操作系统环境,会使得canvas的同一绘图操作流程产生不同的结果。如果是相同的运行环境,同一套Canvas操作流程会产生相同的结果。 浏览器指纹的优势是不需要浏览器保持本地状态,即可跟踪浏览器。 由于国内特色的Ghost系统安装,这种方式的误杀率并不低。
|
||||
|
||||
|
||||
|
||||
以上是第一阶段的安全性总结,后期应该会更新(App逆向、防重放、Canvas 反爬虫技术方案做深入探讨等)。
|
||||
|
||||
|
||||
补充:
|
||||
- 关于 Hybrid 的更多内容,可以看看这篇文章 [Awesome Hybrid](https://github.com/FantasticLBP/knowledge-kit/blob/master/%E7%AC%AC%E4%B8%80%E9%83%A8%E5%88%86%20iOS/1.46.md)
|
||||
- [基于canvas绘图的网页信息防采集技术研究](https://www.doc88.com/p-9186178372509.html)
|
||||
72
Chapter1 - iOS/1.57.md
Normal file
72
Chapter1 - iOS/1.57.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# 自动布局
|
||||
|
||||
## 1. iOS开发之AutoLayout中的Content Hugging Priority和 Content Compression Resistance Priority解析
|
||||
|
||||
- Content Hugging Priority:直译成中文就是“内容拥抱优先级”,从字面意思上来看就是两个视图,谁的“内容拥抱优先级”高,谁就优先环绕其内容。稍后我们会根据一些示例进行介绍。
|
||||
|
||||
- Content Compression Resistance Priority:该优先级直译成中文就是“内容压缩阻力优先级”。也就是视图的“内容压缩阻力优先级”越大,那么该视图中的内容越难被压缩。而该优先级小的视图,则内容优先被压缩。稍后我们也会通过相应的实例来看一下这个优先级的具体表现。
|
||||
|
||||
这两个属性是可以在Storyboard中直接设置的,选中要设置的控件,在右边约束一栏里边就有Content Hugging Priority以及Content Compression Resistance Priority的设置地方。Content Hugging Priority的水平和竖直方向的默认值都是250,而Content Compression Resistance Priority的水平和竖直的默认值是750。我们可以在此对该值进行设置。
|
||||
|
||||
|
||||
假如要实现界面上 Label1、Label2、Label3。Label1宽度固定,Label3右侧对齐,且Label3必须显示完全,Label2距离左侧5px,右侧和Label3连在一起,当Label3文字较多的时候,Label2显示不全,显示省略号,Label3文字较少,则Label2完全显示
|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
下面是代码实现。
|
||||
|
||||
```Objective-c
|
||||
UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||
label1.text = @"生生世世";
|
||||
label1.font = [UIFont systemFontOfSize:15];
|
||||
label1.textColor = [UIColor brownColor];
|
||||
label1.textAlignment = NSTextAlignmentLeft;
|
||||
label1.backgroundColor = [UIColor yellowColor];
|
||||
|
||||
[self.view addSubview:label1];
|
||||
|
||||
[label1 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.view);
|
||||
make.top.equalTo(self.view).offset(100);
|
||||
make.width.mas_equalTo(64);
|
||||
make.height.mas_equalTo(30);
|
||||
}];
|
||||
|
||||
UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||
label2.text = @"杭城小刘杭城小刘杭城小刘杭城小刘杭城小刘杭城小刘杭城小刘";
|
||||
label2.textColor = [UIColor purpleColor];
|
||||
label2.textAlignment = NSTextAlignmentLeft;
|
||||
label2.backgroundColor = [UIColor grayColor];
|
||||
|
||||
[self.view addSubview:label2];
|
||||
// [label2 setContentHuggingPriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
|
||||
|
||||
[label2 setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
|
||||
[label2 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(label1.mas_right).offset(5);
|
||||
make.top.equalTo(self.view).offset(100);
|
||||
make.height.mas_equalTo(30);
|
||||
}];
|
||||
|
||||
|
||||
|
||||
UILabel *label3 = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||
label3.text = @"刘";
|
||||
label3.textColor = [UIColor redColor];
|
||||
label3.textAlignment = NSTextAlignmentRight;
|
||||
label3.backgroundColor = [UIColor blueColor];
|
||||
|
||||
[self.view addSubview:label3];
|
||||
[label3 setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal];
|
||||
// [label3 setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal];
|
||||
[label3 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.right.equalTo(self.view).offset(-10);
|
||||
make.top.equalTo(self.view).offset(100);
|
||||
make.height.mas_equalTo(30);
|
||||
make.left.equalTo(label2.mas_right);
|
||||
}];
|
||||
```
|
||||
21
Chapter1 - iOS/1.58.md
Normal file
21
Chapter1 - iOS/1.58.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Swift 版本迁移问题总结
|
||||
|
||||
> 工程中存在一部分代码逻辑是 Swift 实现的,每次 Swift 版本升级、Xcode 版本升级或许意味着你需要对 Swfit 实现的这部分代码进行升级改动,此篇文章就记录 Swift 每次升级时踩过的坑
|
||||
|
||||
## Swift 5 && Xcode 10.2 的踩坑
|
||||
|
||||
1. 一部分改动是由于系统通知的名称改变造成的
|
||||
|
||||
| 改动前 | 改动后 |
|
||||
|:--:|:--:|
|
||||
| .UIKeyboardWillShow | UIResponder.keyboardWillShowNotification |
|
||||
| UIAlertControllerStyle.alert | UIAlertController.Style.alert |
|
||||
| UIAlertActionStyle.cancel | UIAlertAction.Style.alert |
|
||||
| UIControlState.normal | UIControl.State.normal |
|
||||
| UIEdgeInsetsMake(0, 20, 0, 20) | UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) |
|
||||
| XQNetworkingManager.jsonManager | XQNetworkingManager.useJSONSerializer |
|
||||
| NSNotification.Name.UITextFieldTextDidChange | UITextField.textDidChangeNotification |
|
||||
|
||||
2. 当你修改完语法问题的时候,编译工程,发现还是存在一些问题。大体意思是说 Swift 的编译版本不再支持。所以我们需要选中 targets ,切换到 「Build Settings」 下面,搜索 「Swift Language Version」,在后面勾选合适的 Swift 版本。在这里我选择了 Swift 5
|
||||
|
||||
$$
|
||||
20
Chapter1 - iOS/1.59.md
Normal file
20
Chapter1 - iOS/1.59.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 零散知识
|
||||
|
||||
1. 为什么线上代码尽量不要使用 NSLog("%@", person)?
|
||||
因为NSLog使用%@输出本质上是调用了对象的 description 方法,所以代码中存在大量的 NSLog 的时候。会造成性能问题。
|
||||
解决方案:使用宏定义判断当前代码的运行环境,DEBUG 模式下才输出 NSLog,否则就空实现。
|
||||
```objective-c
|
||||
#ifdef DEBUG
|
||||
///一个区分开发和线上环境的Log。NSLog的本质是调用对象方法的 description 方法,所以线上代码使用NSLog会造成性能和安全问题
|
||||
#define SafeLog(...) NSLog(__VA_ARGS__)
|
||||
#else
|
||||
#define SafeLog(...)
|
||||
#endif
|
||||
```
|
||||
2. 如果我们想在某个函数或者方法参数指定参数的类型的话,使用 `id` 编译器不会在编译阶段对其真正的类型做检查,如果我们想指定为一个类的对象或者一个类的子类对象的时候可以使用 `__kindof` 。
|
||||
```Objective-c
|
||||
- (void)test:(__kindof UIView *)view
|
||||
{
|
||||
view.subviews;
|
||||
}
|
||||
```
|
||||
146
Chapter1 - iOS/1.6.md
Normal file
146
Chapter1 - iOS/1.6.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# 双列表联动
|
||||
|
||||
|
||||
> 用过了那么多的外卖App,总结出一个规律,那就是“所有的外卖App都有双列表联动功能”。哈哈哈哈,这是一个玩笑。
|
||||
>
|
||||
> 这次我也需要开发具有联动效果的双列表。也是首次开发这种类型的UI,记录下步骤与心得
|
||||
|
||||
#### 一、关键思路
|
||||
|
||||
* 懒加载左右2个UITableView
|
||||
* 根据需要自定义Cell
|
||||
* 2个UITableView加载到界面上的时候注意下部剧就好
|
||||
* 因为需要联动效果,所有左侧的UITableView一般是大的分类,右边的UITableView一般是大分类小的小分类,所以有了这样的特点
|
||||
* 左边的UITableView是只有1个section和n个row
|
||||
* 右边的UITableView具有n个section(这里的section 个数恰好是左边UITableView的row数量),且每个section下的row由对应的数据源控制
|
||||
|
||||
#### 二、第一版代码
|
||||
|
||||
```
|
||||
#pragma mark -- UITableViewDelegate
|
||||
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
|
||||
if (tableView == self.leftTablview) {
|
||||
return 1;
|
||||
}
|
||||
return self.datas.count;
|
||||
}
|
||||
|
||||
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
|
||||
if (tableView == self.leftTablview) {
|
||||
return self.datas.count;
|
||||
}
|
||||
QuestionCollectionModel *model = self.datas[section];
|
||||
NSArray *questions =model.questions;
|
||||
return questions.count;
|
||||
}
|
||||
|
||||
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
|
||||
if (tableView == self.leftTablview) {
|
||||
return LeftCellHeight;
|
||||
}
|
||||
return RightCellHeight;
|
||||
}
|
||||
|
||||
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
|
||||
if (tableView == self.leftTablview) {
|
||||
PregnancyPeriodCell *cell = [tableView dequeueReusableCellWithIdentifier:PregnancyPeriodCellID forIndexPath:indexPath];
|
||||
if (self.collectionType == CollectionType_Wrong || self.collectionType == CollectionType_Miss) {
|
||||
QuestionCollectionModel *model = self.datas[indexPath.row];
|
||||
cell.week = model.tag;
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
QuestionCell *cell = [tableView dequeueReusableCellWithIdentifier:QuestionCellID forIndexPath:indexPath];
|
||||
QuestionCollectionModel *model = self.datas[indexPath.section];
|
||||
NSArray *questions =model.questions;
|
||||
QuestionModel *questionModel = questions[indexPath.row];
|
||||
cell.model = questionModel;
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
|
||||
if (tableView == self.leftTablview) {
|
||||
NSIndexPath *indexpath = [NSIndexPath indexPathForRow:0 inSection:indexPath.row];
|
||||
[self.rightTableview scrollToRowAtIndexPath:indexpath atScrollPosition:UITableViewScrollPositionTop animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
|
||||
if (scrollView == self.rightTableview) {
|
||||
NSIndexPath *indexpath = [self.rightTableview indexPathsForVisibleRows].firstObject;
|
||||
NSIndexPath *leftScrollIndexpath = [NSIndexPath indexPathForRow:indexpath.section inSection:0];
|
||||
[self.leftTablview selectRowAtIndexPath:leftScrollIndexpath animated:YES scrollPosition:UITableViewScrollPositionMiddle];
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
缺陷:虽然实现了效果,但是有缺陷。点击左侧的UITableView,右侧的UITableViewe滚动到相应的位置,这是没问题的,但是滚动
|
||||
|
||||
右边,需要根据右边indexPath.section将选中左侧相应的indexPath。这样左侧选中的时候,又会触发右边滚动的事件,整体看上去不是很流畅。
|
||||
|
||||
#### 三、解决方案
|
||||
|
||||
观察了下,发现右侧滚动的时候左侧会上下选中,所以也就是只要让右侧滚动的时候,左侧的UITableView单方向选中,不要滚动就好,所以由于UITableView也是UIScrollview,所以在scrollViewDidScroll方法中判断右侧的UITableView是向上还是向下滚动,以此作为判断条件来让左侧的UITableView选中相应的行。
|
||||
|
||||
且之前是在scrollview代理方法中让左侧的tableview选中,这样子又会触发左侧tableview的选中事件,从而导致右侧的tablview滚动,造成不严谨的联动逻辑
|
||||
|
||||
改进后的方法:
|
||||
|
||||
1. 点击左侧的UITableView,在代理方法didSelectRowAtIndexPath中拿到相应的indexPath.row,计算出右侧UITableView需要滚动的indexPath的位置。
|
||||
```
|
||||
[self.rightTableview scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:indexPath.row] atScrollPosition:UITableViewScrollPositionTop animated:YES];
|
||||
```
|
||||
2. 在willDisplayCell和didEndDisplayingCell代理方法中选中左侧UITableView相应的行。
|
||||
|
||||
```
|
||||
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
|
||||
|
||||
if (tableView == self.rightTableview && !self.isScrollDown && self.rightTableview.isDragging ) {
|
||||
[self.leftTablview selectRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.section inSection:0] animated:YES scrollPosition:UITableViewScrollPositionTop];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
-(void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
|
||||
if (tableView == self.rightTableview && self.isScrollDown && self.rightTableview.isDragging) {
|
||||
[self.leftTablview selectRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.section+1 inSection:0] animated:YES scrollPosition:UITableViewScrollPositionTop];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath
|
||||
{
|
||||
if (self.leftTablview == tableView)
|
||||
{
|
||||
[self.rightTableview scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:indexPath.row] atScrollPosition:UITableViewScrollPositionTop animated:YES];
|
||||
}else{
|
||||
NSLog(@"嗡嗡嗡");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UIScrollViewDelegate
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
|
||||
|
||||
static CGFloat lastOffsetY = 0;
|
||||
|
||||
UITableView *tableView = (UITableView *)scrollView;
|
||||
if (self.rightTableview == tableView){
|
||||
self.isScrollDown = (lastOffsetY < scrollView.contentOffset.y);
|
||||
lastOffsetY = scrollView.contentOffset.y;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
##### 效果图
|
||||
|
||||

|
||||
|
||||
附上Demo:[Demo](https://github.com/FantasticLBP/BlogDemos)
|
||||
688
Chapter1 - iOS/1.60.md
Normal file
688
Chapter1 - iOS/1.60.md
Normal file
@@ -0,0 +1,688 @@
|
||||
# iOS 瘦身之道
|
||||
|
||||
|
||||
|
||||
App 的包大小做优化的目的就是为了节省用户流量,提高用户的下载速度,也是为了用户手机节省更多的空间。另外 App Store 官方规定 App 安装包如果超过 150MB,那么不可以使 OTA(over-the-air)环境下载,也就是只可以在 WiFi 环境下载,企业或者独立开发者万万不想看到这一点。免得失去大量的用户。
|
||||
|
||||
同时如果你的 App 需要适配 iOS7、iOS8 那么官方规定主二进制 text 段的大小不能超过 60MB。如果不能满足这个标准,则无法上架 App Store。
|
||||
|
||||
另一种情况是 App 包体积过大,对用户更新升级率也会有很大影响。
|
||||
|
||||
所以应用包的瘦身迫在眉睫。
|
||||
|
||||
|
||||
|
||||
## 1. App Thinning
|
||||
|
||||
App Thinning 是指 iOS9 以后引入的一项优化,官方描述如下
|
||||
> The App Store and operating system optimize the installation of iOS, tvOS, and watchOS apps by tailoring app delivery to the capabilities of the user’s particular device, with minimal footprint. This optimization, called app thinning, lets you create apps that use the most device features, occupy minimum disk space, and accommodate future updates that can be applied by Apple. Faster downloads and more space for other apps and content provides a better user experience.
|
||||
|
||||
Apple 会尽可能,自动降低分发到具体用户时,所需要下载的 App 大小。其中包含三项主要功能:Slicing、Bitcode、On-Demand Resources。
|
||||
|
||||
App Thinning 是苹果公司推出的一项改善 App 下载进程的新技术,主要为了解决用户下载 App 耗费过高流量的问题,同时还可以节省用户设备存储空间。
|
||||
|
||||
|
||||
### 1.1 Slicing
|
||||
|
||||

|
||||
|
||||
当向 App Store Connect 上传 .ipa 后,App Store Connect 构建过程中,会自动分割该 App,创建特定的变体(variant)以适配不同设备。然后用户从 App Store 中下载到的安装包,即这个特定的变体,这一过程叫做 Slicing。
|
||||
|
||||
> Slicing 是创建、分发不同变体以适应不同目标设备的过程
|
||||
|
||||
而变体之间的差异,又具体体现在架构和资源上。换句话说,App Slicing 仅向设备传送与之相关的资源(取决于屏幕分辨率、系统架构等等)
|
||||
|
||||
其中,2x 和 3x 的细分,要求图片在 **Assets** 中管理。Bundle 内的则会同时包含。
|
||||
|
||||

|
||||
|
||||
|
||||
### 1.2 Bitcode
|
||||
|
||||
> Bitcode is an intermediate representation of a compiled program. Apps you upload to iTunes Connect that contain bitcode will be compiled and linked on the App Store. Including bitcode will allow Apple to re-optimize your app binary in the future without the need to submit a new version of your app to the App Store.
|
||||
|
||||
Bitcode 是一种程序`中间码`。包含 Bitcode 配置的程序将会在 App Store Connect 上被重新编译和链接,进而对可执行文件做优化。这部分都是在服务端自动完成的。所以假如以后 Apple 新推出了新的 CPU 架构或者以后 LLVM 推出了一系列优化,我们不需要重新为其发布新的安装包了。Apple Store 会为我们自动完成这步。然后提供对应的 variant 给具体设备
|
||||
|
||||
对于 iOS 而言,Bitcode 是可选的(Xcode7 以后创建的新项目默认开启),watchOS、tvOS 则是必须的。
|
||||
|
||||
开启位置:Build Settings -> Enable Bitcode -> 设置为 YES
|
||||
|
||||
|
||||
开启 Bitcode,有这么2点需要注意:
|
||||
- 全部都要支持。我们所依赖的静态库、动态库、Cocoapods 管理的第三方库,都需要开启 Bitcode。否则会编译失败
|
||||
|
||||
- 奔溃定位。开启 Bitcode 后最终生成的可执行文件是 Apple 自动生成的,同时会产生新的符号表文件,所以我们无法使用自己包生成的 dYSM 符号化文件来进行符号化。
|
||||
|
||||
> For Bitcode enabled builds that have been released to the iTunes store or submitted to TestFlight, Apple generates new dSYMs. You’ll need to download the regenerated dSYMs from Xcode and then upload them to Crashlytics so that we can symbolicate crashes.For Bitcode enabled apps, ensure that you have checked “Include app symbols for your application…” so that we can provide the most accurate crash reports.
|
||||
|
||||
上面是 fabric 中关于 Downloading Bitcode dYSMs 的描述:
|
||||
|
||||
在上传到 App Store 时需勾选“Includ app symbols for your application...”。勾选之后 Apple 会自动生成对应的 dYSM,然后可以在 Xcode -> Window -> Organizer 中,或者在 Apple Store Connect 中下载对应的 dYSM 来进行符号化
|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
那么 Bitcode 会对 App Thining 有什么作用?
|
||||
|
||||
在 New Features in Xcode7 中有这么一段描述:
|
||||
|
||||
> Bitcode. When you archive for submission to the App Store, Xcode will compile your app into an intermediate representation. The App Store will then compile the bitcode down into the 64 or 32 bit executables as necessary.
|
||||
|
||||
即,App Store 会再按需将这个 bitcode 编译进 32/64 位的可执行文件。
|
||||
所以网上铺天盖地地说 Bitcode 完成了具体架构的拆分,从而实现瘦包
|
||||
|
||||
|
||||
### 1.3 on-Demand Resources
|
||||
|
||||
on-Demand Resource 即一部分图片可以被放置在苹果的服务器上,不随着 App 的下载而下载,直到用户真正进入到某个页面时才下载这些资源文件。
|
||||
|
||||

|
||||
|
||||
应用场景:相机应用的贴纸或者滤镜、关卡游戏等
|
||||
|
||||
如需支持 iOS9 以下系统,那么无法使用这个功能,否则上传会失败
|
||||
|
||||
|
||||
|
||||
## 2 包体积
|
||||
|
||||
2个概念
|
||||
|
||||
- .ipa (iOS Application Package):iOS 应用程序归档文件,即提交到 App Store Connect 的文件
|
||||
|
||||
- .app (Application):应用的具体描述,即安装到 iOS 设备上的文件
|
||||
|
||||
当我们拿到 Archive 后的 .ipa,使用解压软件打开后,Payload 目录下存放的就是 .app 文件,二者大小相当
|
||||
|
||||
包体积,评判标准是以 App Store 上看到的为准。但是上传到 App Store Connect 处理完后,会自动帮我们生成具体设备上看到的大小。如下:
|
||||
|
||||

|
||||
|
||||
这其中:又可以分为2类: Universal 和具体设备
|
||||
Universal 指通用设备,即未应用 App slicing 优化,同时包含了所有架构、资源。所以包体积会比较大
|
||||
|
||||
观察 .ipa 的大小和 Universal 对应的包大小相当,稍微小一点,因为 App Store 对 .ipa 做了加密处理
|
||||
|
||||
|
||||
有时候下载 App 会提示“此项目大于 150MB,除非此项目支持增量下载,否则您必须连接至 WiFi 才能下载”。150MB 针对的是下载大小。
|
||||
|
||||
|
||||
- 下载大小:通过 WiFi 下载的压缩 App 大小
|
||||
- 安装大小:此 App 将在用户设备上占用磁盘空间的大小
|
||||
|
||||
所以我们要瘦包,关键在于减小 .app 文件的大小。
|
||||
|
||||
|
||||
### 2.1 Architectures
|
||||
|
||||
如果不支持32位以及 iOS8 ,去掉 armv7 ,可执行文件以及库会减小,即本地 .ipa 也会减小
|
||||
|
||||
|
||||
### 2.2 Resources
|
||||
|
||||
资源的优化也就是平时的细心与审查。
|
||||
|
||||
图片、内置素材、Bundle、多语言、Json、字体、脚本、Plist、音频
|
||||
|
||||
图片:Assets.car
|
||||
Bundle: 非放在 Asset Catlog 中管理的图片资源。包括 Bundle,散落的 png、jpg 等
|
||||
|
||||
瘦包具体的方式:
|
||||
|
||||
- 无用资源的删除
|
||||
- 重复文件的删除
|
||||
- 大文件压缩
|
||||
- 图片管理方式规范
|
||||
- on-Demand Resource(游戏的、前置关卡依赖、滤镜App 等的依赖资源,建议用这种方式动态下载图片资源)
|
||||
|
||||
|
||||
#### 2.2.1 无用文件的删除
|
||||
|
||||
|
||||
无用文件主要包含:无用图片、无用非图片部分。
|
||||
|
||||
非图片部分:资源较少,使用方式固定。比如音频、字体。需要手动排查
|
||||
图片部分:主要使用一个开源的 Mac App [LSUnusedResources](https://github.com/tinymind/LSUnusedResources) 进行冗余图片的排查。
|
||||
|
||||
|
||||
删除无用的图片过程,可以概括为下面6步:
|
||||
1. 通过 find 命令获取 App 安装包中的所有资源文件
|
||||
2. 设置用到的资源类型。比如 gif、jpg、jpeg、png、webp
|
||||
3. 使用正则匹配出在源码中使用到的资源名,比如 pattern = @"@"(.+?)""
|
||||
4. 使用 find 命令找到篇所有资源文件,再去源码中找到使用到的资源文件,2个集合的差集就是无用资源了。
|
||||
5. 确认无用资源后可以使用 NSFileManager 进行文件的删除。
|
||||
|
||||
|
||||
如果不想重新写一个工具,那么可以直接使用开源的工具 LSUnusedResources
|
||||
|
||||
但是存在一点问题。会出现误报,因为不同的项目,图片使用方式不一样。
|
||||
|
||||
```
|
||||
- (BOOL)containsSimilarResourceName:(NSString *)name {
|
||||
NSString *regexStr = @"([-_]?\\d+)";
|
||||
NSRegularExpression* regexExpression = [NSRegularExpression regularExpressionWithPattern:regexStr options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
NSArray* matchs = [regexExpression matchesInString:name options:0 range:NSMakeRange(0, name.length)];
|
||||
//...
|
||||
}
|
||||
```
|
||||
源码中的正则表达式处理的情况并不是很准确。可以根据自己的情况修改正则即可
|
||||
|
||||
|
||||
#### 2.2.2 图片资源的压缩
|
||||
|
||||
删除了无用的资源,那么对于资源这块还是有操作的空间的,比如图片资源的压缩。目前压缩比较好的方案就是 WebP,它是谷歌公司的一个开源项目。
|
||||
|
||||
WebP 的优势:
|
||||
- 压缩率高。支持有损和无损2种方式,比如将 Gif 图可以转换为 Animated WebP,有损模式下可以减小 64%,无损模式下可以减小 19%
|
||||
- WebP 支持 Alpha 透明和 24-bit 颜色数,不会像 PNG8 那样因为色彩不够出现毛边。
|
||||
|
||||
Google 公司在开源 WebP 的同时,还提供了一个图片压缩工具 [cwebp](https://developers.google.com/speed/webp/docs/precompiled)。
|
||||
压缩完之后使用 WebP 格式的图片还需使用 libwebp 进行解析,参考这个[Demo](https://github.com/carsonmcdonald/WebP-iOS-example)。
|
||||
|
||||
缺点:WebP 在 CUP 消耗和解码时间上会比 PNG 高2倍,所以我们做选择的时候需要取舍。
|
||||
|
||||
|
||||
#### 2.2.3 重复文件删除
|
||||
|
||||
重复文件,即两个内容完全一致的文件。但是文件命名不一样。
|
||||
|
||||
借助 [fdupes](https://github.com/adrianlopezroche/fdupes) 这个开源工具,校验各资源的 MD5。
|
||||
|
||||
fdupes 是 Linux 下的一个工具,它由 Adrian Lopez 用 C 语言编写并基于 MIT 许可证发行,该应用程序可以在指定的目录及子目录中查找重复的文件。fdupes 通过对比文件的 MD5 签名,以及逐字节比较文件来识别重复内容,fdupes 有各种选项,可以实现对文件的列出、删除、替换为文件副本的硬链接等操作。
|
||||
|
||||
文件对比从以下顺序开始:
|
||||
大小对比 > 部分 MD5 签名对比 > 完整 MD5 签名对比 > 逐字节对比
|
||||
|
||||
执行结束后会在命令行展示出来,所以需要我们人工将这些文件确认对比后删除掉。
|
||||
|
||||
|
||||
#### 2.2.4 大文件压缩
|
||||
|
||||
图片本身的压缩,建议使用 ImageOptim。它整合了 Win、Linux 上诸多著名图片处理工具的特色,比如 PNGOUT、AdvPNG、Pngcrush、OptiPNG、JpegOptim、Gifsicle 等。
|
||||
Bundle 内的图片资源必须压缩,因为 Xcode 并不会对其进行压缩。所以做好将图片都用 Assets 管理。
|
||||
|
||||
Xcode 提供给我们2个编译选项来帮助压缩图像:
|
||||
|
||||
- Compress PNG Files: 打包的时候自动对图片进行无损压缩。使用的工具为 pngcrush,压缩比蛮高。
|
||||
- Remove Text Medadata From PNG Files:移除 PNG 资源的文本字符,比如图像名称、作者、版权、创作时间、注释等信息
|
||||
|
||||
|
||||
#### 2.2.5 图片管理方式规范
|
||||
|
||||
|
||||
##### 2.2.5.1 主工程中的图片管理
|
||||
|
||||
工程中所有使用的 Asset Catlog 管理的图片(在 .xcassets 文件夹下)最终都会输出到 Asset.car 内。不在 Asset.car 内的都归为 Bundle 管理。
|
||||
|
||||
- xcassets 里面的图片。只能通过 imageNamed 加载。 Bundle 里面的图片还可以通过 imageWithContentsOfFile 等方式
|
||||
- xcassets 里面的 @2x、@3x 会根据具体设备分发,不会同时包含。Bundle 都包含(不进行 App Slicing)
|
||||
- xcassets 内可以对图片进行 Slicing,即裁剪和拉伸、Bundle 不支持
|
||||
- Bundle 内支持多语言,Images.xcassets 不支持
|
||||
|
||||
> 使用 imageNamed 创建的 UIImage 会被立即加入到 NSCache 中(解码后的 Image Buffer),直到收到内存警告的时候才会释放不使用的 UIImage。而 imageWithContentsOfFile 会每次重新申请内存,相同图片不会缓存,所以 xcassets 内的图片,加载后会产生缓存
|
||||
|
||||
综上:常用的、较小的图建议存放在 Images.xcassets 内管理。大图放在 Bundle 内管理。
|
||||
|
||||
这里讲一个插曲了,曾经很多文章都在谈一个结论,那就是「图片放在 Images.xcassets 里面更加快速且节省空间,直接放在 bundle 里面会比较慢」。我做过实验,实验环境和结论如下。使用 Instruments 测量耗时。
|
||||
|
||||
<details>
|
||||
<summary>点击展开</summary>
|
||||
|
||||
```Objective-C
|
||||
//实验1
|
||||
NSMutableArray *images = [NSMutableArray array];
|
||||
for (NSInteger index = 0; index < 10; index++) {
|
||||
UIImage *image = [UIImage imageNamed:@"icon-iOS"];
|
||||
[images addObject:image];
|
||||
}
|
||||
self.imageView.image = images.lastObject;
|
||||
//实验2
|
||||
NSMutableArray *images = [NSMutableArray array];
|
||||
for (NSInteger index = 0; index < 10; index++) {
|
||||
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"iOS" ofType:@"png"];
|
||||
[UIImage imageNamed:@"icon-iOS"];
|
||||
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
|
||||
[images addObject:image];
|
||||
}
|
||||
self.imageView.image = images.lastObject;
|
||||
```
|
||||
</details>
|
||||
|
||||
Timeprofile-imageNamedFromAssets
|
||||

|
||||
|
||||
TimeProfile-imageWithContentsOfFile
|
||||

|
||||
|
||||
Timeprofile-UIImageNamedFromFolder
|
||||

|
||||
|
||||
|
||||
Images.xcassets :
|
||||
- 图片大小要精确,不要出现图片太大的情况
|
||||
- 不要存放大图,不然会产生缓存
|
||||
- 不要存 jpg 图片,打包会变大
|
||||
- 图片不需要额外压缩(有人做过实验,对放入 assets 里面的图片进行压缩后打包发现包体积反而增大,怀疑是 Xcode 的编译选项 Compress PNG Files 自动对图片进行压缩,2种压缩起了冲突反而增大)
|
||||
|
||||
|
||||
##### 2.2.5.2 各个 pod 库中的图片管理
|
||||
|
||||
CocoPods 中两种资源引用方式介绍下:
|
||||
- resource_bundles
|
||||
> We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute.
|
||||
允许定义当前的 pod 库的最远包的名称和文件。用 hash 形式声明,key 是 bundle 的名称,value 是需要包含文件的通配 patterns
|
||||
CocoPods 官方强烈推荐该方法引用资源,因为 key-value 可以避免相同资源的名称冲突
|
||||
- resources
|
||||
> We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute. Moreover, resources specified with this attribute are copied directly to the client target and therefore they are not optimised by Xcode.
|
||||
使用该方法引用资源,被指定的资源会被拷贝进 target 工程的 main bundle 中。
|
||||
|
||||
|
||||
|
||||
说说项目中的情况吧:在工程中之前是通过 resource_bundles 引用资源的。资源是放在 Resources 目录下的图片引用。查询资料后说「如果图片资源放到 .xcasset 里面 Xcode 会帮我们自动优化、可以使用 Slicing 等(这里不仅仅指的是 resource_bundle 下的 xcassets」。所以动手将各个 Pod 库里面的图片全都通过 Assets Catalog 的方式进行处理。
|
||||
|
||||

|
||||
|
||||
步骤:
|
||||
- 在各个 Pod 组件库里面的 Resources 目录下新建 Asset Catalog 文件,命名为 Images.xcassets
|
||||
- 将 Resources 里面零散的图片资源拖进 Images.xcassets 里面
|
||||
- 修改每个组件库的 podspec 文件
|
||||
<details>
|
||||
|
||||
<summary>点击展开</summary>
|
||||
|
||||
```
|
||||
s.resource_bundles = {
|
||||
'XQ_UI' => ['XQ_UI/Assets/*.xcassets']
|
||||
}
|
||||
</details>
|
||||
```
|
||||
- 主工程执行 pod install
|
||||
|
||||
话说 `resources` 和 `resource_bundles` 都可以使用 Asset Catalog,那么有何区别?
|
||||
- resources 只会将资源文件 copy 到 target 工程,最后和 target 工程的图片资源以及同样使用该方式的 Pod 库的图片资源共同打包到一个 `Assets.car` 中。因此图片资源会有混乱的可能。
|
||||
- resource_bundles 会生成一个你在 `podspec` 中指定名称的 bundle,且在 bundle 中也会生成一个 Assets.car。所以图片是肯定不会混乱的,但是图片的访问方式需要注意。
|
||||
|
||||
|
||||
解决方法:为每个 pod 新建一个图片的分类,比如 UIImage+XQUIModule。然后访问图片的时候通过 `[UIImage xquiModuleImageNamed:@"pull"]` 访问。
|
||||
|
||||
<details>
|
||||
<summary>点击展开</summary>
|
||||
|
||||
```Objective-C
|
||||
#import "UIImage+XQUIModule.h"
|
||||
#import <SDGBase/UIImage+Bundle.h>
|
||||
|
||||
@implementation UIImage (XQUIModule)
|
||||
|
||||
+ (nonnull UIImage *)xquiModuleImageNamed:(nonnull NSString *)name
|
||||
{
|
||||
return [UIImage imageNamed:name inBundleName:@"XQ_UI"];
|
||||
}
|
||||
@end
|
||||
|
||||
//UIImage+Bundle.m
|
||||
#import "UIImage+Bundle.h"
|
||||
|
||||
@implementation UIImage (Bundle)
|
||||
|
||||
+ (nullable UIImage *)imageNamed:(NSString *)name inBundleName:(nullable NSString *)bundleName {
|
||||
NSBundle *bundle = [NSBundle bundleWithURL:[[NSBundle mainBundle] URLForResource:bundleName withExtension:@"bundle"]];
|
||||
return [UIImage imageNamed:name inBundle:bundle compatibleWithTraitCollection:nil];
|
||||
}
|
||||
@end
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
#### 2.2.6 矢量图的使用
|
||||
|
||||
事实上,对于 App 里面的单色图标,比如左上角的返回按钮、底部的 tabBar等,只要是单色的纯色图标都是可以使用矢量图代替的,比如 PDF、ttf 字体图标等。这样就不需要添加 @2x、@3x 图标,节省了空间。
|
||||
|
||||
iOS 中如何使用 ttf 矢量图,可以查看这个 [Repo](https://github.com/FantasticLBP/IconFont_Demo)
|
||||
|
||||
|
||||
|
||||
## 3. Executable file
|
||||
|
||||
### 3.1 编译选项优化
|
||||
|
||||
#### 3.1.1 Generate Debug Symbols
|
||||
|
||||
> Enables or disables generation of debug symbos. When debug symbols are enabled, the level of detail can be controller by the build 'Level of Debug Symbols' Setting.
|
||||
|
||||
调试符号是在编译时形成的。当 Generate Debug Symbols 选项为 YES 的时,每个源文件在编译成 .o 文件时,编译参数多了 -g 和 -gmodules 两项。打包会生成 symbols 文件。设置为 NO 则 ipa 中不会生成 symbol 文件,可以减少 ipa 大小。但会影响到崩溃的定位。保持默认的开启,不做修改。
|
||||
|
||||
#### 3.1.2 Asset Catalog Compiler
|
||||
|
||||
optimization 选项设置为 space 可以减少包大小
|
||||
默认选项,不做修改。
|
||||
|
||||
#### 3.1.3 Dead Code Stripping
|
||||
|
||||
> For statically linked executables, dead-code stripping is the process of removing unreferenced code from the executable file. If the code is unreferenced, it must not be used and therefore is not needed in the executable file. Removing dead code reduces the size of your executable and can help reduce paging.
|
||||
|
||||
|
||||
删除静态链接的可执行文件中未引用的代码
|
||||
|
||||
Debug 设置为 NO, Release 设置为 YES 可减少可执行文件大小。
|
||||
|
||||
Xcode 默认会开启此选项,C/C++/Swift 等静态语言编译器会在 link 的时候移除未使用的代码,但是对于 Objective-C 等动态语言是无效的。因为 Objective-C 是建立在运行时上面的,底层暴露给编译器的都是 Runtime 源码编译结果,所有的部分应该都是会被判别为有效代码。
|
||||
|
||||
默认选项,不做修改。
|
||||
|
||||
#### 3.1.4 Apple Clang - Code Generation
|
||||
|
||||
Optimization Level 编译参数决定了程序在编译过程中的两个指标:编译速度和内存的占用,也决定了编译之后可执行结果的两个指标:速度和文件大小。
|
||||
Build Settings -> code Generation -> Optimization Level
|
||||
默认情况下,Debug 设定为 None[-O0] ,Release 设定为 Fastest,Smallest[-Os]。
|
||||
|
||||
- None[-O0]。 Debug 默认级别。不进行任何优化,直接将源代码编译到执行文件中,结果不进行任何重排,编译时比较长。主要用于调试程序,可以进行设置断点、改变变量 、计算表达式等调试工作。
|
||||
|
||||
- Fast[-O,O1]。最常用的优化级别,不考虑速度和文件大小权衡问题。与-O0级别相比,它生成的文件更小,可执行的速度更快,编译时间更少。
|
||||
|
||||
- Faster[-O2]。在-O1级别基础上再进行优化,增加指令调度的优化。与-O1级别相,它生成的文件大小没有变大,编译时间变长了,编译期间占用的内存更多了,但程序的运行速度有所提高。
|
||||
|
||||
- Fastest[-O3]。在-O2和-O1级别上进行优化,该级别可能会提高程序的运行速度,但是也会增加文件的大小。
|
||||
|
||||
- Fastest Smallest[-Os]。Release 默认级别。这种级别用于在有限的内存和磁盘空间下生成尽可能小的文件。由于使用了很好的缓存技术,它在某些情况下也会有很快的运行速度。
|
||||
|
||||
- Fastest, Aggressive Optimization[-Ofast]。 它是一种更为激进的编译参数, 它以点浮点数的精度为代价。
|
||||
|
||||
默认选项,不做修改。
|
||||
|
||||
|
||||
#### 3.1.5 Swift Compiler - Code Generation
|
||||
|
||||
Xcode 9.3 版本之后 Swift 编译器提供了新的 Optimization Level 选项来帮助减少 Swift 可执行文件的大小:
|
||||
|
||||
- No optimization[-Onone]:不进行优化,能保证较快的编译速度。
|
||||
- Optimize for Speed[-O]:编译器将会对代码的执行效率进行优化,一定程度上会增加包大小。
|
||||
- Optimize for Size[-Osize]:编译器会尽可能减少包的大小并且最小限度影响代码的执行效率。
|
||||
|
||||
> We have seen that using -Osize reduces code size from 5% to even 30% for some projects. But what about performance? This completely depends on the project. For most applications the performance hit with -Osize will be negligible, i.e. below 5%. But for performance sensitive code -O might still be the better choice.
|
||||
|
||||
官方提到,-Osize 根据项目不同,大致可以优化掉 5% - 30% 的代码空间占用。 相比 -0 来说,会损失大概 5% 的运行时性能。 如果你的项目对运行速度不是特别敏感,并且可以接受轻微的性能损失,那么 -Osize 是首选。
|
||||
|
||||
除了 -O 和 -Osize, 还有另外一个概念也值得说一下。 就是 Single File 和 Whole Module 。 在之前的 XCode 版本,这两个选项和 -O 是连在一起设置的,Xcode 9.3 中,将他们分离出来,可以独立设置:
|
||||
|
||||
|
||||
Single File 和 Whole Module 这两个模式分别对应编译器以什么方式处理优化操作。
|
||||
|
||||
- Single File:逐个文件进行优化,它的好处是对于增量编译的项目来说,它可以减少编译时间,对没有更改的源文件,不用每次都重新编译。并且可以充分利用多核 CPU,并行优化多个文件,提高编译速度。但它的缺点就是对于一些需要跨文件的优化操作,它没办法处理。如果某个文件被多次引用,那么对这些引用方文件进行优化的时候,会反复的重新处理这个被引用的文件,如果你项目中类似的交叉引用比较多,就会影响性能。
|
||||
|
||||
- Whole Module: 将项目所有的文件看做一个整体,不会产生 Single File 模式对同一个文件反复处理的问题,并且可以进行最大限度的优化,包括跨文件的优化操作。缺点是,不能充分利用多核处理器的性能,并且对于增量编译,每次也都需要重新编译整个项目。
|
||||
|
||||
如果没有特殊情况,使用默认的 Whole Module 优化即可。 它会牺牲部分编译性能,但的优化结果是最好的。
|
||||
|
||||
故,在 Relese 模式下 -Osize 和 Whole Module 同时开启效果会最好!
|
||||
|
||||
|
||||
#### 3.1.6 Strip Symbol Information
|
||||
|
||||
1、Deployment Postprocessing
|
||||
2、Strip Linked Product
|
||||
3、Strip Debug Symbols During Copy
|
||||
4、Symbols hidden by default
|
||||
|
||||
设置为 YES 可以去掉不必要的符号信息,可以减少可执行文件大小。但去除了符号信息之后我们就只能使用 dSYM 来进行符号化了,所以需要将 Debug Information Format 修改为 DWARF with dSYM file。
|
||||
|
||||
Symbols Hidden by Default 会把所有符号都定义成”private extern”,详细信息见官方文档。
|
||||
|
||||
故,Release 设置为 YES,Debug 设置为 NO。
|
||||
|
||||
|
||||
#### 3.1.7 Exceptions
|
||||
|
||||
在 iOS微信安装包瘦身 一文中,有提到:
|
||||
|
||||
> 去掉异常支持,Enable C++ Exceptions和Enable Objective-C Exceptions设为NO,并且Other C Flags添加-fno-exceptions,可执行文件减少了27M,其中__gcc_except_tab段减少了17.3M,__text减少了9.7M,效果特别明显。可以对某些文件单独支持异常,编译选项加上-fexceptions即可。但有个问题,假如ABC三个文件,AC文件支持了异常,B不支持,如果C抛了异常,在模拟器下A还是能捕获异常不至于Crash,但真机下捕获不了(有知道原因可以在下面留言:)。去掉异常后,Appstore 后续几个版本 Crash 率没有明显上升。
|
||||
|
||||
个人认为关键路径支持异常处理就好,像启动时NSCoder读取setting配置文件得要支持捕获异常,等等
|
||||
|
||||
看这个优化效果,感觉发现了新大陆。关闭后验证.. 毫无感知,基本没什么变化。
|
||||
|
||||
可能和项目中用到比较少有关系。故保持开启状态。
|
||||
|
||||
|
||||
#### 3.1.8 Link-Time Optimization
|
||||
|
||||
Link-Time Optimization 是 LLVM 编译器的一个特性,用于在 link 中间代码时,对全局代码进行优化。这个优化是自动完成的,因此不需要修改现有的代码;这个优化也是高效的,因为可以在全局视角下优化代码。
|
||||
|
||||
苹果在 WWDC 2016 中,明确提出了这个优化的概念,What’s New in LLVM。并且说在苹果内部已经广泛地使用这个优化方法进行编译。
|
||||
|
||||
它的优化主要体现在如下几个方面:
|
||||
|
||||
1. 多余代码去除(Dead code elimination):如果一段代码分布在多个文件中,但是从来没有被使用,普通的 -O3 优化方法不能发现跨中间代码文件的多余代码,因此是一个“局部优化”。但是Link-Time Optimization 技术可以在 link 时发现跨中间代码文件的多余代码。
|
||||
|
||||
2. 跨过程优化(Interprocedural analysis and optimization):这是一个相对广泛的概念。举个例子来说,如果一个 if 方法的某个分支永不可能执行,那么在最后生成的二进制文件中就不应该有这个分支的代码。
|
||||
|
||||
3. 内联优化(Inlining optimization):内联优化形象来说,就是在汇编中不使用 “call func_name” 语句,直接将外部方法内的语句“复制”到调用者的代码段内。这样做的好处是不用进行调用函数前的压栈、调用函数后的出栈操作,提高运行效率与栈空间利用率。
|
||||
|
||||
在新的版本中,苹果使用了新的优化方式 Incremental,大大减少了链接的时间。建议开启。
|
||||
|
||||
|
||||
总结,开启这个优化后,一方面减少了汇编代码的体积,一方面提高了代码的运行效率。
|
||||
|
||||
|
||||
### 3.2 代码瘦身
|
||||
|
||||
代码的优化,即通过删除无用类、无用方法、重复方法等,来达到可执行文件大小的减小。
|
||||
而如何筛选出符合条件的无用类、方法,则需要通过一些工具来完成(fui)
|
||||
|
||||
扫描无用代码的基本思路都是查找已经使用的方法/类和所有的类/方法,然后从所有的类/方法当中剔除已经使用的方法/类剩下的基本都是无用的类/方法,但是由于 Objective-C 是动态语言,可以使用字符串来调用类和方法,所以检查结果一般都不是特别准确,需要二次确认。目前市面上的扫描的思路大致可以分为 3 种:
|
||||
|
||||
- 基于 Clang 扫描
|
||||
- 基于可执行文件扫描
|
||||
- 基于源码扫描
|
||||
|
||||
|
||||
先谈几个概念。
|
||||
|
||||
可执行文件就是 **Mach-O** 文件,其大小是油代码量决定的,通常情况下,对可执行文件进行瘦身,就是找到并删除无用代码的过程。找到无用代码的过程类比找到无用图片的思路。
|
||||
- 找到类和方法的全集
|
||||
- 找到使用过的类和方法集合
|
||||
- 取2者差集得到无用代码集合
|
||||
- 工程师确认后,删除即可
|
||||
|
||||
|
||||
LinkMap 文件分为3部分:Object File、Section、Symbols。
|
||||

|
||||
- Object File:包含了代码工程的所有文件
|
||||
- Section:描述了代码段在生成的 Mach-O 里的偏移位置和大小
|
||||
- Symbols:会列出每个方法、类、Block,以及它们的大小
|
||||
|
||||
先说说如何快速找到方法和类的全集?
|
||||
|
||||
|
||||
我们可以通过 **LinkMap** 来获得所有的代码类和方法的信息。获取 LinkMap 可以通过将 Build Setting 里面的 **Write Link Map File** 设置为 YES,然后指定 **Path to Link Map File** 的路径就可以得到每次编译后的 LinkMap 文件了。
|
||||

|
||||
|
||||
|
||||
#### 3.2.1 基于 clang 扫描
|
||||
|
||||
基本思路是基于 clang AST。追溯到函数的调用层级,记录所有定义的方法/类和所有调用的方法/类,再取差集。具体原理参考 如何使用 Clang Plugin 找到项目中的无用代码,目前只有思路没有现成的工具。
|
||||
|
||||
|
||||
#### 3.2.2 基于可执行文件扫描(LinkMap 结合 Mach-O 找无用代码)
|
||||
|
||||
上面我们得知可以通过 LinkMap 统计出所有的类和方法,还可以清晰地看到代码所占包大小的具体分布,进而有针对性地进行代码优化。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
得到了代码的全集信息后,我们还需要找到已经使用过的方法和类,这样才可以获取差集,找到无用代码。所以接下来就谈谈如何通过 Mach-O 取到使用过的类和方法。
|
||||
|
||||
Objective-C 中的方法都会通过 **objc_msgSend** 来调用,而 objc_msgSend 在 Mach-O 文件里是通过 **_objc_selrefs** 这个 **section** 来获取 selector 这个参数的。
|
||||
|
||||
所以,_objc_selrefs 里的方法一定是被调用了的。**_objc_classrefs** 里是被调用过的类, **objc_superrefs** 是调用过 super 的类(继承关系)。通过 _objc_classrefs 和 _objc_superrefs,我们就可以找出使用过的类和子类。
|
||||
|
||||
那么,Mach-O 文件中的 _objc_selrefs、_objc_classrefs、_objc_superrefs 如何查看呢?
|
||||
|
||||
|
||||
1. 使用 otool 等命令逆向可执行文件中引用到的类/方法和所有定义的类/方法,然后计算差集。具体参考iOS微信安装包瘦身,目前只有思路没有现成的工具。
|
||||
2. 使用 [MachOView](https://github.com/gdbinit/MachOView) 查看。但是这个项目运行不起来,这个新的 [Repo](https://github.com/fangshufeng/MachOView) 可以运行起来。
|
||||
|
||||
下面举例说明:
|
||||
|
||||
前置条件:先运行项目,在生成的 Products 目录下的 BridgeLabiPhone.app 解压,取出对应的和工程同名的 BridgeLabiPhone。然后运行上面的 Github 项目。可以看到运行了一个 Mac App。点击顶部的菜单栏里面的 File->Open。选择电脑上的 BridgeLabiPhone.app 选择里面的 BridgeLabiPhone。见下图
|
||||
|
||||

|
||||
|
||||
由于 Objective-C 是一门动态语言,所以检测出的结果仍旧需要我们2次确认。
|
||||
|
||||
|
||||
#### 3.2.3 基于源码扫描
|
||||
|
||||
一般都是对源码文件进行字符串匹配。例如将 A *a、[A xxx]、NSStringFromClass("A")、objc_getClass("A") 等归类为使用的类,@interface A : B 归类为定义的类,然后计算差集。
|
||||
|
||||
基于源码扫描 有个已经实现的工具 - fui,但是它的实现原理是查找所有 #import "A" 和所有的文件进行比对,所以结果相对于上面的思路来说可能更不准确。
|
||||
|
||||
|
||||
#### 3.2.4 通过 AppCode 查找无用代码
|
||||
|
||||
AppCode 提供了 Inspect Code 来诊断代码,其中含有查找无用代码的功能。它可以帮助我们查找出 AppCode 中无用的类、无用的方法甚至是无用的 import ,但是无法扫描通过字符串拼接方式来创建的类和调用的方法,所以说还是上面所说的 基于源码扫描 更加准确和安全。
|
||||
|
||||

|
||||
|
||||
说明:AppCode检测出了实际上需要的大部分场景的问题,但是由于 Objective-C 是一门动态性语言,所以 AppCode 检测出无用的方法等都需要工程师自己再次确认后删除。(在我们的工程中有一些和 H5 交互的桥接方法,因此 AppCode 视为 Unused Method,但是你删除的话,那就自己哭去吧 😭)。实际经验告诉我,使用 AppCode 的时候如果工程比较大,则整个 code inspect 会非常耗时(给你打个预防针哦,笔芯)
|
||||
|
||||
- 无用类:Unused class 是无用类,Unused import statement 是无用类引入声明,Unused property 是无用的属性;
|
||||
- 无用方法:Unused method 是无用的方法,Unused parameter 是无用参数,Unused instance variable 是无用的实例变量,Unused local variable 是无用的局部变量,Unused value 是无用的值;
|
||||
- 无用宏:Unused macro 是无用的宏。
|
||||
- 无用全局:Unused global declaration 是无用全局声明。
|
||||
|
||||
|
||||
#### 3.2.5 运行时真正检测类是否用过
|
||||
|
||||
通过上述手段找到并删除了无用代码。App 不断上线迭代蛮多代码都不会被调用了(业务被砍掉了)。这种方式下这些无用的代码也是可以被删除的。
|
||||
|
||||
通过 Objective-C 的 runtime 源码,我们可以找到如何判断一个类是否初始化过的函数。
|
||||
|
||||
```Objective-c
|
||||
#define RW_INITIALIZED (1<<29)
|
||||
bool isInitialized() {
|
||||
return getMeta()->data()->flags & RW_INITIALIZED;
|
||||
}
|
||||
```
|
||||
|
||||
isInitialized 的结果会保存到元类的 class_rw_t 结构体的 flags 信息里, flags 的 1<<29 位记录的就是这个类是否初始化了的信息,而 flags 的其他位记录的信息,可以查看 rumtime 的源码
|
||||
|
||||
```Objective-c
|
||||
// 类的方法列表已修复
|
||||
#define RW_METHODIZED (1<<30)
|
||||
|
||||
// 类已经初始化了
|
||||
#define RW_INITIALIZED (1<<29)
|
||||
|
||||
// 类在初始化过程中
|
||||
#define RW_INITIALIZING (1<<28)
|
||||
|
||||
// class_rw_t->ro 是 class_ro_t 的堆副本
|
||||
#define RW_COPIED_RO (1<<27)
|
||||
|
||||
// 类分配了内存,但没有注册
|
||||
#define RW_CONSTRUCTING (1<<26)
|
||||
|
||||
// 类分配了内存也注册了
|
||||
#define RW_CONSTRUCTED (1<<25)
|
||||
|
||||
// GC:class 有不安全的 finalize 方法
|
||||
#define RW_FINALIZE_ON_MAIN_THREAD (1<<24)
|
||||
|
||||
// 类的 +load 被调用了
|
||||
#define RW_LOADED (1<<23)
|
||||
```
|
||||
|
||||
既然可以在运行的期间知道类是否初始化了,那么就可以找出哪些类未初始化,即可以找到在真实环境里面没有用到的类并删除掉。
|
||||
|
||||
|
||||
|
||||
## 4. App Extension
|
||||
|
||||
App Extension 的占用,都放在 Plugin 文件夹内。它是独立打包签名,然后再拷贝进 Target App Bundle 的。
|
||||
关于 Extension,有两个点要注意:
|
||||
|
||||
静态库最终会打包进可执行文件内部,所以如果 App Extension 依赖了三方静态库,同时主工程也引用了相同的静态库的话,最终 App 包中可能会包含两份三方静态库的体积。
|
||||
|
||||
动态库是在运行的时候才进行加载链接的,所以 Plugin 的动态库是可以和主工程共享的,把动态库的加载路径 Runpath Search Paths 修改为跟主工程一致就可以共享主工程引入的动态库。
|
||||
|
||||
所以,如果可能的话,把相关的依赖改成动态库方式,达到共享。
|
||||
|
||||
|
||||
|
||||
## 5. 静态库瘦身
|
||||
|
||||
项目中都会引入第三方静态库。通过 lipo 工具可以查看支持的指令集,比如查看微博 SDK
|
||||
终端切换到微博 SDK 的目录下执行下面命令
|
||||
- 静态库指令集信息查看:`lipo -info libname.a(或者libname.framework/libname)`
|
||||
|
||||
```Shell
|
||||
lipo -info libWeiboSDK.a
|
||||
//Architectures in the fat file: libWeiboSDK.a are: armv7 arm64 i386 x86_64
|
||||
```
|
||||
|
||||
我们知道 i386、x86_64 是模拟器的指令集。所以我们可以模拟器版本的指令集。因为 armv7 也可以兼容 armv7s。所以 armv7s 也可以删除了。只保留 armv7 和 arm64
|
||||
|
||||
- 静态库拆分:`lipo 静态库文件路径 -thin CPU架构 -output 拆分后的静态库文件路径`
|
||||
- 静态库合并:`lipo -create 静态库1文件路径 静态库2文件路径... 静态库n文件路径 -output 合并后的静态库文件径`
|
||||
|
||||
|
||||
```Shell
|
||||
lipo libWeiboSDK.a -thin armv7 -output libWeiboSDK-armv7.a
|
||||
lipo libWeiboSDK.a -thin arm64 -output libWeiboSDK-arm64.a
|
||||
lipo create libWeiboSDK-armv7.a libWeiboSDK-arm64.a -output libWeiboSDK.device.a
|
||||
```
|
||||
|
||||
通过上面的操作我们将静态库里面支持模拟器的指令集给去掉了,所以模拟器是无法跑代码的,如何解决?
|
||||
|
||||
1. 平时使用包含模拟器指令集的静态库,在 App 发布的时候去掉
|
||||
2. 如果使用 Cocoapods 管理可以使用2份 Podfile 文件。一份包含指令集一份不包含,发布的时候切换 Podfile 文件即可。或者一份 Podfile 文件,但是配置不同的环境设置
|
||||
|
||||
|
||||
补充2个说明:
|
||||
|
||||
1. dSYM 文件
|
||||
符号表文件 .dSYM 文件是从 Mach-O 文件中抽取调试信息而得到的文件目录,实际用于保存调试信息的是 DWARF 文件
|
||||
|
||||
- 自动生成。Xcode 会在工程编译或者归档的时候自动生成 .dSYM 文件,在 Buld setting 设置中有开关可以设置去关掉 .dSYM 文件
|
||||
- 手动生成。通过脚本从 Mach-O 文件中提取出来。
|
||||
```
|
||||
$ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/dsymutil /Users/wangzz/Library/Developer/Xcode/DerivedData/YourApp-cqvijavqbptjyhbwewgpdmzbmwzk/Build/Products/Debug-iphonesimulator/YourApp.app/YourApp -o YourApp.dSYM
|
||||
```
|
||||
该方式通过 dsymutil 工具,从项目编译结果 .app 目录下的 Mach-O 文件中提取出调试符号表文件。Xcode 在归档的时候是通过它生辰的 .dSYM 文件
|
||||
|
||||
2. DWARF 文件
|
||||
DebuggingWith Arbitrary Record Formats 是 ELF 和 Mach-O 等文件格式中用来存储和处理调试信息的标准格式,.dSYM 文件中真正保存符号表数据的是 DWARF 文件。DWARF 文件中不同的数据都保存在相应的 section 中。
|
||||
|
||||
|
||||
最后的一个对比效果图:
|
||||

|
||||
|
||||
|
||||
总结:瘦身技术常见操作就这些,但是维持应用包体积的瘦身却是一个观念,从日常开发到线上发布都需要有这个意识。这样当你在写代码的时候就会考虑同样一个效果,你的具体实现手段是怎么样的。比如为了一个稍微炫酷的效果就要引入一个很大的三方库,有了“瘦身”的意识,你很大可能就是自己动手撸一个代码。比如一些无用资源的管理方式、有用的图片资源的高效管理方式等等。有了意识,行动自然会往这个方面去靠。(😂大道理一套一套的。我也不想的,毕竟是playboy)
|
||||
|
||||
其中遇到了一个神奇的问题。lint 的时候看到一些未使用的依赖库。见 [问题](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.53.md)
|
||||
|
||||
|
||||
**By the way:**
|
||||
如果在应用包瘦身方面有其他的做法,请告知,完善文章。
|
||||
|
||||
参考文章:
|
||||
- [Humble Assets Catalog](http://lingyuncxb.com/2019/04/14/HumbleAssetCatalog/)
|
||||
- [关于 Pod 库的资源引用 resource_bundles or resources](http://zhoulingyu.com/2018/02/02/pod-resource-reference/)
|
||||
- 部分图片或者文字内容引用来自网络(若有引用到,请告诉我地址,及时补充)
|
||||
|
||||
|
||||
|
||||
|
||||
- 线程、队列、runloop 的关系?主串行队列,在 Mach 内核中创建
|
||||
- block:堆内存中的结构体变量
|
||||
- 全局并发队列:队列(FIFO)。所以最后加入到全局并发队列中的任务最后执行,执行的时候就可以拿到结果
|
||||
16
Chapter1 - iOS/1.61.md
Normal file
16
Chapter1 - iOS/1.61.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# App 启动时间优化
|
||||
|
||||
在看 App 启动时间优化之前先看2个方法: **load** 和 **initialize**。
|
||||
|
||||
load
|
||||
> Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.
|
||||
当一个类或者它的分类被加载到 Objective-c 的 runtime 中的时候会出发 load 方法。可以实现这个方法去执行一些类特定的行为。
|
||||
|
||||
initialize
|
||||
> Initializes the class before it receives its first message.
|
||||
当一个类接收到第一条消息的时候会初始化。
|
||||
|
||||
load 方法会在类被加载到 runtime 的时候调用。且父类的 load 方法比子类先执行。load 方法只会执行1次。
|
||||
initialize 方法会在第一次收到消息的时候调用。父类的 initialize 方法比子类先执行。假如有 Person 类,还有一个子类 children。子类第一次收到消息的时候会先调用父类的 initialize,然后调用子类的 initialize,如果子类没有实现 initialize 那么父类的 initialize 会执行多次。
|
||||
|
||||
|
||||
534
Chapter1 - iOS/1.62.md
Normal file
534
Chapter1 - iOS/1.62.md
Normal file
@@ -0,0 +1,534 @@
|
||||
# OCLint 实现 Code Review - 给你的代码提提质量
|
||||
|
||||
工程代码质量,一个永恒的话题。好的质量的好处不言而喻,团队成员间除了保持统一的风格和较高的自我约束力之外,还需要一些工具来统计分析代码质量问题。
|
||||
|
||||
本文就是针对 OC 项目,提出的一个思路和实践步骤的记录,最后形成了一个可以直接用的脚本。如果觉得文章篇幅过长,则直接可以下载[脚本](https://github.com/FantasticLBP/knowledge-kit/tree/master/assets/auto_Lint.sh)
|
||||
|
||||
> OCLint is a static code analysis tool for improving quality and reducing defects by inspecting C, C++ and Objective-C code and looking for potential problems ...
|
||||
|
||||
从官方的解释来看,它通过检查 C、C++、Objective-C 代码来寻找潜在问题,来提高代码质量并减少缺陷的静态代码分析工具
|
||||
|
||||
|
||||
|
||||
## OCLint 的下载和安装
|
||||
|
||||
有3种方式安装,分别为 Homebrew、源代码编译安装、下载安装包安装。
|
||||
区别:
|
||||
- 如果需要自定义 Lint 规则,则需要下载源码编译安装
|
||||
- 如果仅仅是使用自带的规则来 Lint,那么以上3种安装方式都可以
|
||||
|
||||
|
||||
### 1. Homebrew 安装
|
||||
|
||||
在安装前,确保安装了 homebrew。步骤简单快捷
|
||||
|
||||
```Shell
|
||||
brew tap oclint/formulae
|
||||
brew install oclint
|
||||
```
|
||||
|
||||
|
||||
### 2. 安装包安装
|
||||
|
||||
- 进入 OCLint 在 Github 中的[地址](https://github.com/oclint/oclint/releases),选择 Release。选择最新版本的安装包(目前最新版本为:oclint-0.13.1-x86_64-darwin-17.4.0.tar.gz)
|
||||
- 解压下载文件。将文件存放到一个合适的位置。(比如我选择将这些需要的源代码存放到 Document 目录下)
|
||||
- 在终端编辑当前环境的配置文件,我使用的是 zsh,所以编辑 .zshrc 文件。(如果使用系统的终端则编辑 .bash_profile 文件)
|
||||
```Shell
|
||||
OCLint_PATH=/Users/liubinpeng/Desktop/oclint/build/oclint-release
|
||||
export PATH=$OCLint_PATH/bin:$PATH
|
||||
```
|
||||
- 将配置文件 source 一下。
|
||||
```Shell
|
||||
source .zshrc // 如果你使用系统的终端则执行 soucer .bash_profile
|
||||
```
|
||||
- 验证是否安装成功。在终端输入 `oclint --version`
|
||||
|
||||
|
||||
### 3. 源码编译安装
|
||||
|
||||
- homebrew 安装 CMake 和 Ninja 这2个编译工具
|
||||
```Shell
|
||||
brew install cmake ninja
|
||||
```
|
||||
|
||||
- 进入 Github 搜索 OCLint,clone 源码
|
||||
```Shell
|
||||
gc https://github.com/oclint/oclint
|
||||
```
|
||||
|
||||
- 进入 oclint-scripts 目录,执行 ./make 命令。这一步的时间非常长。会下载 oclint-json-compilation-database、oclint-xcodebuild、llvm 源码以及 clang 源码。并进行相关的编译得到 oclint。且必须使用翻墙环境不然会报 timeout。如果你的电脑支持翻墙环境,但是在终端下不支持翻墙,可以查看我的这篇[文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/第六部分%20开发杂谈/6.11.md)
|
||||
```Shell
|
||||
./make
|
||||
```
|
||||
|
||||
- 编译结束,进入同级 build 文件夹,该文件夹下的内容即为 oclint。可以看到 `build/oclint-release`。方式2下载的安装包的内容就是该文件夹下的内容。
|
||||
|
||||
- cd 到根目录,编辑环境文件,比如我 zsh 对应的 .zshrc 文件。编辑下面的内容
|
||||
```Shell
|
||||
OCLint_PATH=/Users/liubinpeng/Desktop/oclint/build/oclint-release
|
||||
export PATH=$OCLint_PATH/bin:$PATH
|
||||
```
|
||||
|
||||
- source 下 .zhsrc 文件
|
||||
```Shell
|
||||
source .zshrc // source .bash_profile
|
||||
```
|
||||
|
||||
- 进入 `oclint/build/oclint-release` 目录执行脚本
|
||||
```Shell
|
||||
cp ~/Documents/oclint/build/oclint-release/bin/oclint* /usr/local/bin/
|
||||
ln -s ~/Documents/oclint/build/oclint-release/lib/oclint /usr/local/lib
|
||||
ln -s ~/Documents/oclint/build/oclint-release/lib/clang /usr/local/lib
|
||||
```
|
||||
这里使用 ln -s,把 lib 中的 clang 和 oclint 链接到 /usr/local/bin 目录下。这样做的目的是为了后面如果编写了自己创建的 lint 规则,不必要每次更新自定义的 rule 库,必须手动复制到 /usr/local/bin 目录下。
|
||||
|
||||
- 验证下 OCLint 是否安装成功。输入 oclint --version
|
||||
|
||||

|
||||
|
||||
注意:如果你采用源码编译的时候直接 clone 官方的源码会有问题,编译不过,所以提供了一个可以编译过的[版本](https://github.com/FantasticLBP/oclint)。分支切换到 llvm-7.0。
|
||||
|
||||
|
||||
### 4. xcodebuild 的安装
|
||||
xcode 下载安装好就已经成功安装了
|
||||
|
||||
|
||||
### 5. xcpretty 的安装
|
||||
|
||||
先决条件,你的机器已经安装好了 Ruby gem.
|
||||
|
||||
```Shell
|
||||
gem install xcpretty
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 二、 自定义 Rule
|
||||
|
||||
OClint 提供了 70+ 项的检查规则,你可以直接去使用。但是某些时候你需要制作自己的检测规则,接下来就说说如何自定义 lint 规则。
|
||||
|
||||
|
||||
1. 进入 ~/Document/oclint 目录,执行下面的脚本
|
||||
|
||||
```shell
|
||||
oclint-scripts/scaffoldRule CustomLintRules -t ASTVisitor
|
||||
```
|
||||
其中,*CustomLintRules* 就是定义的检查规则的名字, *ASTVisitor* 就是你继承的 lint 规则
|
||||
|
||||
可以继承的规则有:ASTVisitor、SourceCodeReader、ASTMatcher。
|
||||
|
||||
2. 执行上面的脚本,会生成下面的文件
|
||||
- Documents/oclint/oclint-rules/rules/custom/CustomLintRulesRule.cpp
|
||||
- Documents/oclint/oclint-rules/test/custom/CustomLintRulesRuleTest.cpp
|
||||
|
||||
3. 要方便的开发自定义的 lint 规则,则需要生成一个 xcodeproj 项目。切换到项目根目录,也就是 Documents/oclint,执行下面的命令
|
||||
```Shell
|
||||
mkdir Lint-XcodeProject
|
||||
cd Lint-XcodeProject
|
||||
touch generate-lint-rules.sh
|
||||
chmod +x generate-lint-rules.sh
|
||||
```
|
||||
给上面的 generate-lint-rules.sh 里面添加下面的脚本
|
||||
|
||||
```Shell
|
||||
#! /bin/sh -e
|
||||
cmake -G Xcode \
|
||||
-D CMAKE_CXX_COMPILER=../build/llvm-install/bin/clang++ \
|
||||
-D CMAKE_C_COMPILER=../build/llvm-install/bin/clang \
|
||||
-D OCLINT_BUILD_DIR=../build/oclint-core \
|
||||
-D OCLINT_SOURCE_DIR=../oclint-core \
|
||||
-D OCLINT_METRICS_SOURCE_DIR=../oclint-metrics \
|
||||
-D OCLINT_METRICS_BUILD_DIR=../build/oclint-metrics \
|
||||
-D LLVM_ROOT=../build/llvm-install/ ../oclint-rules
|
||||
```
|
||||
|
||||
4. 执行 generate-lint-rules.sh 脚本(./generate-lint-rules.sh)。如果出现下面的 Log 则说明生成 xcodeproj 项目成功
|
||||
|
||||

|
||||

|
||||
|
||||
5. 打开步骤4生成的项目,看到有很多文件夹,代表 oclint 自带的 lint 规则,我们自定义的 lint 规则在最下面。
|
||||

|
||||
|
||||
关于如何自定义 lint 规则的具体还没有深入研究,这里给个例子
|
||||
|
||||
<details>
|
||||
<summary>点击查看示例代码</summary>
|
||||
|
||||
```C
|
||||
#include "oclint/AbstractASTVisitorRule.h"
|
||||
#include "oclint/RuleSet.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace clang;
|
||||
using namespace oclint;
|
||||
#include <iostream>
|
||||
|
||||
class MVVMRule : public AbstractASTVisitorRule<MVVMRule>
|
||||
{
|
||||
public:
|
||||
virtual const string name() const override
|
||||
{
|
||||
return "Property in 'ViewModel' Class interface should be readonly.";
|
||||
}
|
||||
|
||||
virtual int priority() const override
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
virtual const string category() const override
|
||||
{
|
||||
return "mvvm";
|
||||
}
|
||||
|
||||
virtual unsigned int supportedLanguages() const override
|
||||
{
|
||||
return LANG_OBJC;
|
||||
}
|
||||
|
||||
#ifdef DOCGEN
|
||||
virtual const std::string since() const override
|
||||
{
|
||||
return "0.18.10";
|
||||
}
|
||||
|
||||
virtual const std::string description() const override
|
||||
{
|
||||
return "Property in 'ViewModel' Class interface should be readonly.";
|
||||
}
|
||||
|
||||
virtual const std::string example() const override
|
||||
{
|
||||
return R"rst(
|
||||
.. code-block:: cpp
|
||||
|
||||
@interface FooViewModel : NSObject // This is a "ViewModel" Class.
|
||||
|
||||
@property (nonatomic, strong) NSObject *bar; // should be readonly.
|
||||
|
||||
@end
|
||||
)rst";
|
||||
}
|
||||
|
||||
virtual const std::string fileName() const override
|
||||
{
|
||||
return "MVVMRule.cpp";
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
virtual void setUp() override {}
|
||||
virtual void tearDown() override {}
|
||||
|
||||
/* Visit ObjCImplementationDecl */
|
||||
bool VisitObjCImplementationDecl(ObjCImplementationDecl *node)
|
||||
{
|
||||
ObjCInterfaceDecl *interface = node->getClassInterface();
|
||||
|
||||
bool isViewModel = interface->getName().endswith("ViewModel");
|
||||
if (!isViewModel) {
|
||||
return false;
|
||||
}
|
||||
for (auto property = interface->instprop_begin(),
|
||||
propertyEnd = interface->instprop_end(); property != propertyEnd; property++)
|
||||
{
|
||||
clang::ObjCPropertyDecl *propertyDecl = (clang::ObjCPropertyDecl *)*property;
|
||||
if (propertyDecl->getName().startswith("UI")) {
|
||||
addViolation(propertyDecl, this);
|
||||
}
|
||||
auto attrs = propertyDecl->getPropertyAttributes();
|
||||
bool isReadwrite = (attrs & ObjCPropertyDecl::PropertyAttributeKind::OBJC_PR_readwrite) > 0;
|
||||
if (isReadwrite && isViewModel) {
|
||||
addViolation(propertyDecl, this);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
static RuleSet rules(new MVVMRule());
|
||||
```
|
||||
</details>
|
||||
|
||||
6. 修改自定义规则后就需要编译。成功后在 Products 目录下会看到对应名称的 CustomLintRulesRule.dylib 文件,就需要复制到 /Documents/oclint/oclint-release/lib/oclint/rules。讲道理,生成新的 lint rule 文件,需要把新的 dylib 文件复制到 /usr/local/lib。因为我们在源代码安装的第4部,设置了 ln -s 链接,所以不需要每次复制到相应文件夹。
|
||||
|
||||
但是还是比较麻烦,每次都需要编译新的 lint rule 之后需要将相应的 dylib 文件复制到源代码目录下的 oclint-release/lib/oclint/rules 目录下,本着「可以偷懒绝不动手」的原则,在自定义的 rule 的 target 中,在 Build Phases 选项下 CMake PostBuild Rules 中的脚本下将下面的代码复制进去
|
||||
|
||||
```Shell
|
||||
cp /Users/liubinpeng/Documents/oclint/Lint-XcodeProject/rules.dl/Debug/libCustomLintRulesRule.dylib /Users/liubinpeng/Documents/oclint/build/oclint-release/lib/oclint/rules/libCustomLintRulesRule.dylib
|
||||
```
|
||||
|
||||
7. 规则限定的3个类说明:
|
||||
```Shell
|
||||
RuleBase
|
||||
|
|
||||
|-AbstractASTRuleBase
|
||||
| |_ AbstractASTVisitorRule
|
||||
| |_AbstractASTMatcherRule
|
||||
|
|
||||
|-AbstractSourceCodeReaderRule
|
||||
```
|
||||
- AbstractSourceCodeReaderRule:eachLine 方法,读取每行的代码,如果想编写的规则是需要针对每行的代码内容,则可以继承自该类
|
||||
- AbstractASTVisitorRule:可以访问 AST 上特定类型的所有节点,可以检查特定类型的所有节点是递归实现的。在 **apply** 方法内可以看到代码实现。开发者只需要重载 bool visit* 方法来访问特定类型的节点。其值表明是否继续递归检查
|
||||
- AbstractASTMatcherRule:实现 setUpMatcher 方法,在方法中添加 matcher,当检查发现匹配结果时会调用 callback 方法。然后通过 callback 方法来继续对匹配到的结果进行处理
|
||||
|
||||
8. 知其所以然
|
||||
oclint 依赖与源代码的语法抽象树(AST)。开源 clang 是 oclint 获的语法抽象树的依赖工具。你如果想对 AST 有个了解,可以查看这个[视频](https://www.youtube.com/watch?v=VqCkCDFLSsc&feature=youtu.be),相关讲解https%3A%2F%2Fjonasdevlieghere.com%2Funderstanding-the-clang-ast%2F)
|
||||
|
||||
如果想查看某个文件的 AST 结构,你可以进入该文件的命令行,然后执行下面的脚本
|
||||
```Shell
|
||||
clang -Xclang -ast-dump -fsyntax-only main.m
|
||||
```
|
||||
|
||||
## 三、 Homebrew 方式安装的 oclint 如何使用自定义规则
|
||||
|
||||
1. 查看 OCLint 安装路径
|
||||
```Shell
|
||||
which oclint
|
||||
// 输出:/usr/local/bin/oclint
|
||||
ls -al /usr/local/bin/oclint
|
||||
// 输出:本机安装路径
|
||||
```
|
||||
|
||||
2. 把上面生成的新的 lint rule 下的 dylib 文件复制到步骤1得到的额本机安装路径下
|
||||
|
||||
|
||||
|
||||
## 四、 使用 oclint
|
||||
|
||||
|
||||
### 在命令行中使用
|
||||
|
||||
1. 如果项目使用了 Cocopod,则需要指定 -workspace xxx.workspace
|
||||
2. 每次编译之前需要 clean
|
||||
|
||||
|
||||
实操:
|
||||
|
||||
- 进入项目
|
||||
```Shell
|
||||
cd /Workspace/Native/iOS/lianhua
|
||||
```
|
||||
- 查看项目基本信息
|
||||
```Shell
|
||||
xcodebuild -list
|
||||
//输出
|
||||
information about project "BridgeLabiPhone":
|
||||
Targets:
|
||||
BridgeLabiPhone
|
||||
lint
|
||||
|
||||
Build Configurations:
|
||||
Debug
|
||||
Release
|
||||
|
||||
If no build configuration is specified and -scheme is not passed then "Release" is used.
|
||||
|
||||
Schemes:
|
||||
BridgeLabiPhone
|
||||
lint
|
||||
```
|
||||
|
||||
- 编译
|
||||
```Shell
|
||||
xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace clean && xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json
|
||||
```
|
||||
|
||||
编译成功后,会在项目的文件夹下出现 compile_commands.json 文件
|
||||
|
||||
- 生成 html 报表
|
||||
```Shell
|
||||
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html
|
||||
```
|
||||
|
||||
看到有报错,但是报错信息太多了,不好定位,利用下面的脚本则可以将报错信息写入 log 文件,方便查看
|
||||
|
||||
```Shell
|
||||
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html 2>&1 | tee 1.log
|
||||
```
|
||||
|
||||
报错信息是:**oclint: error: one compiler command contains multiple jobs:**
|
||||
查找资料,解决方案如下
|
||||
- 将 Project 和 Targets 中 Building Settings 下的 COMPILER_INDEX_STORE_ENABLE 设置为 **NO**
|
||||
- 在 podfile 中 target 'xx' do 前面添加下面的脚本
|
||||
|
||||
```Shell
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['COMPILER_INDEX_STORE_ENABLE'] = "NO"
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
然后继续尝试编译,发现还是报错,但是报错信息改变了,如下
|
||||
|
||||

|
||||
|
||||
看到报错信息是默认的警告数量超过限制,则 lint 失败。事实上 lint 后可以跟参数,所以我们修改脚本如下
|
||||
|
||||
```Shell
|
||||
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html -rc LONG_LINE=9999 -max-priority-1=9999 -max-priority-2=9999 -max-priority-3=9999
|
||||
```
|
||||
|
||||
生成了 lint 的结果,查看 html 文件可以具体定位哪个代码文件,哪一行哪一列有什么问题,方便修改。
|
||||

|
||||
|
||||
- 如果项目工程太大,整个 lint 会比较耗时,所幸 oclint 支持针对某个代码文件夹进行 lint
|
||||
```Shell
|
||||
oclint-json-compilation-database -i 需要静态分析的文件夹或文件 -- -report-type html -o oclintReport.html 其他的参数
|
||||
```
|
||||
|
||||
- 参数说明
|
||||
|
||||
| 名称 | 描述 | 默认阈值 |
|
||||
| ----------------------- | ---------------------------- | ---- |
|
||||
| CYCLOMATIC_COMPLEXITY | 方法的循环复杂性(圈负责度) | 10 |
|
||||
| LONG_CLASS | C类或Objective-C接口,类别,协议和实现的行数 | 1000 |
|
||||
| LONG_LINE | 一行代码的字符数 | 100 |
|
||||
| LONG_METHOD | 方法或函数的行数 | 50 |
|
||||
| LONG_VARIABLE_NAME | 变量名称的字符数 | 20 |
|
||||
| MAXIMUM_IF_LENGTH | `if`语句的行数 | 15 |
|
||||
| MINIMUM_CASES_IN_SWITCH | switch语句中的case数 | 3 |
|
||||
| NPATH_COMPLEXITY | 方法的NPath复杂性 | 200 |
|
||||
| NCSS_METHOD | 一个没有注释的方法语句数 | 30 |
|
||||
| NESTED_BLOCK_DEPTH | 块或复合语句的深度 | 5 |
|
||||
| SHORT_VARIABLE_NAME | 变量名称的字符数 | 3 |
|
||||
| TOO_MANY_FIELDS | 类的字段数 | 20 |
|
||||
| TOO_MANY_METHODS | 类的方法数 | 30 |
|
||||
| TOO_MANY_PARAMETERS | 方法的参数数 | 10 |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 在 Xcode 中使用
|
||||
|
||||
- 在项目的 TARGETS 下面,点击下方的 "+" ,选择 cross-platform 下面的 Aggregate。输入名字,这里命名为 Lint
|
||||

|
||||
|
||||
- 选择对应的 TARGET -> lint。在 Build Phases 下 Run Script 下写下面的脚本代码
|
||||
```Shell
|
||||
export LC_CTYPE=en_US.UTF-8
|
||||
cd ${SRCROOT}
|
||||
xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace clean && xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json && oclint-json-compilation-database -e Pods -- -report-type xcode
|
||||
```
|
||||
|
||||
- 说明,虽然有时候没有编译通过,但是看到如下图的关于代码相关的 warning 则达到目的了。
|
||||

|
||||
|
||||
- lint 结果如下,根据相应的提示信息对代码进行调整。当然这只是一种参考,不一定要采纳 lint 给的提示。
|
||||

|
||||
|
||||
|
||||
## 脚本化
|
||||
|
||||
每次都在终端命令行去写 lint 的脚本,效率很低,所以想做成 shell 脚本。需要的同学直接直接拷贝进去,直接在工程的根目录下使用,我这边是一个 Cocopod 工程。拿走拿走别客气
|
||||
|
||||
```Shell
|
||||
#!/bin/bash
|
||||
|
||||
COLOR_ERR="\033[1;31m" #出错提示
|
||||
COLOR_SUCC="\033[0;32m" #成功提示
|
||||
COLOR_QS="\033[1;37m" #问题颜色
|
||||
COLOR_AW="\033[0;37m" #答案提示
|
||||
COLOR_END="\033[1;34m" #颜色结束符
|
||||
|
||||
# 寻找项目的 ProjectName
|
||||
function searchProjectName () {
|
||||
# maxdepth 查找文件夹的深度
|
||||
find . -maxdepth 1 -name "*.xcodeproj"
|
||||
}
|
||||
|
||||
function oclintForProject () {
|
||||
# 预先检测所需的安装包是否存在
|
||||
if which xcodebuild 2>/dev/null; then
|
||||
echo 'xcodebuild exist'
|
||||
else
|
||||
echo '🤔️ 连 xcodebuild 都没有安装,玩鸡毛啊? 🤔️'
|
||||
fi
|
||||
|
||||
if which oclint 2>/dev/null; then
|
||||
echo 'oclint exist'
|
||||
else
|
||||
echo '😠 完蛋了你,玩 oclint 却不安装吗,你要闹哪样 😠'
|
||||
echo '😠 乖乖按照博文:https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.63.md 安装所需环境 😠'
|
||||
fi
|
||||
if which xcpretty 2>/dev/null; then
|
||||
echo 'xcpretty exist'
|
||||
else
|
||||
gem install xcpretty
|
||||
fi
|
||||
|
||||
|
||||
# 指定编码
|
||||
export LANG="zh_CN.UTF-8"
|
||||
export LC_COLLATE="zh_CN.UTF-8"
|
||||
export LC_CTYPE="zh_CN.UTF-8"
|
||||
export LC_MESSAGES="zh_CN.UTF-8"
|
||||
export LC_MONETARY="zh_CN.UTF-8"
|
||||
export LC_NUMERIC="zh_CN.UTF-8"
|
||||
export LC_TIME="zh_CN.UTF-8"
|
||||
export xcpretty=/usr/local/bin/xcpretty # xcpretty 的安装位置可以在终端用 which xcpretty找到
|
||||
|
||||
searchFunctionName=`searchProjectName`
|
||||
path=${searchFunctionName}
|
||||
# 字符串替换函数。//表示全局替换 /表示匹配到的第一个结果替换。
|
||||
path=${path//.\//} # ./BridgeLabiPhone.xcodeproj -> BridgeLabiPhone.xcodeproj
|
||||
path=${path//.xcodeproj/} # BridgeLabiPhone.xcodeproj -> BridgeLabiPhone
|
||||
|
||||
myworkspace=$path".xcworkspace" # workspace名字
|
||||
myscheme=$path # scheme名字
|
||||
|
||||
# 清除上次编译数据
|
||||
if [ -d ./derivedData ]; then
|
||||
echo -e $COLOR_SUCC'-----清除上次编译数据derivedData-----'$COLOR_SUCC
|
||||
rm -rf ./derivedData
|
||||
fi
|
||||
|
||||
# xcodebuild clean
|
||||
xcodebuild -scheme $myscheme -workspace $myworkspace clean
|
||||
|
||||
|
||||
# # 生成编译数据
|
||||
xcodebuild -scheme $myscheme -workspace $myworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json
|
||||
|
||||
if [ -f ./compile_commands.json ]; then
|
||||
echo -e $COLOR_SUCC'编译数据生成完毕😄😄😄'$COLOR_SUCC
|
||||
else
|
||||
echo -e $COLOR_ERR'编译数据生成失败😭😭😭'$COLOR_ERR
|
||||
return -1
|
||||
fi
|
||||
|
||||
# 生成报表
|
||||
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html \
|
||||
-rc LONG_LINE=200 \
|
||||
-disable-rule ShortVariableName \
|
||||
-disable-rule ObjCAssignIvarOutsideAccessors \
|
||||
-disable-rule AssignIvarOutsideAccessors \
|
||||
-max-priority-1=100000 \
|
||||
-max-priority-2=100000 \
|
||||
-max-priority-3=100000
|
||||
|
||||
if [ -f ./oclintReport.html ]; then
|
||||
rm compile_commands.json
|
||||
echo -e $COLOR_SUCC'😄分析完毕😄'$COLOR_SUCC
|
||||
else
|
||||
echo -e $COLOR_ERR'😢分析失败😢'$COLOR_ERR
|
||||
return -1
|
||||
fi
|
||||
echo -e $COLOR_AW'将为您自动打开 lint 的分析结果...'$COLOR_AW
|
||||
# 用 safari 浏览器打开 oclint 的结果
|
||||
open -a "/Applications/Safari.app" oclintReport.html
|
||||
}
|
||||
|
||||
oclintForProject
|
||||
```
|
||||
|
||||
同类型的文章:
|
||||
- [如何打造团队的代码风格统一以及开发效率的提升](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.53.md)
|
||||
- [oclint介绍](https://github.com/hdw09/CIHexoBlog/blob/master/source/_posts/OClint学习笔记.md)
|
||||
- [自定义oclint规则](https://github.com/hdw09/CIHexoBlog/blob/master/source/_posts/OCLint-自定义规则101.md)
|
||||
47
Chapter1 - iOS/1.63.md
Normal file
47
Chapter1 - iOS/1.63.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# 苹果官方开源资料
|
||||
|
||||
- [苹果最新开源 opensource 网站](https://developer.apple.com/opensource/)
|
||||
- [旧版本苹果开源资料](https://opensource.apple.com)
|
||||
- [苹果开发者](https://developer.apple.com/develop/)
|
||||
- [苹果 github](https://github.com/apple)
|
||||
|
||||
## 视频
|
||||
WWDC
|
||||
- [视频分类汇总](https://developer.apple.com/videos/topics/)
|
||||
- [编译器和LLVM](https://developer.apple.com/videos/developer-tools/compiler-and-llvm)
|
||||
|
||||
## 源码
|
||||
|
||||
- [开源苹果](https://opensource.apple.com/source/)
|
||||
- [dyld源代码](https://opensource.apple.com/tarballs/dyld/)
|
||||
- [iOS11 源码](https://opensource.apple.com/release/ios-110.html)
|
||||
- JavaScriptCore-7604.1.38.0.7 推荐
|
||||
- WebKit-7604.1.38.0.7
|
||||
- WebKit2-7604.1.38.0.7
|
||||
- libiconv-51
|
||||
- [objective-c 运行时 源码](https://opensource.apple.com/source/objc4/objc4-723/runtime/)
|
||||
- [objective-c 消息机制汇编源码](https://opensource.apple.com/source/objc4/objc4-723/runtime/Messengers.subproj/)
|
||||
|
||||
## 文档
|
||||
- [官方新文档入口](https://developer.apple.com/documentation)
|
||||
- [UI 官方指南](https://developer.apple.com/design/human-interface-guidelines/)
|
||||
- [Block ABI](https://clang.llvm.org/docs/Block-ABI-Apple.html)
|
||||
|
||||
## 旧版本文档汇总(有些补充或者更底层些)
|
||||
- [旧版本文档](https://developer.apple.com/library/archive/navigation/)
|
||||
- [WebKit Objective-C 编码指南](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/DisplayWebContent/DisplayWebContent.html)
|
||||
- [Concurrency 并发编程指南](https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html)
|
||||
- [Kernel 内核编码指南](https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/build/build.html)
|
||||
|
||||
## 工具
|
||||
|
||||
- [下载中心](https://developer.apple.com/download/)
|
||||
|
||||
## 第三方资料
|
||||
|
||||
|
||||
- [cassowary 布局算法](https://constraints.cs.washington.edu/cassowary/)
|
||||
- [fishhook - 高级 hook 框架源码](https://github.com/facebook/fishhook)
|
||||
- [可运行 runtime 项目](https://github.com/RetVal/objc-runtime)
|
||||
- [InjectionPlugin - 热加载 DEBUG iOS 代码 插件](https://github.com/johnno1962/InjectionIII)
|
||||
- [第三方开源]()
|
||||
35
Chapter1 - iOS/1.64.md
Normal file
35
Chapter1 - iOS/1.64.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# 组件化、模块化、插件、子应用、框架、库理解
|
||||
|
||||
|
||||
|
||||
> 作为大前端时代下开发的我们,经常会被组件化、模块化、框架、库、插件、子应用等术语所迷惑。甚至有些人将组件化和模块化的概念混混为一谈。大量的博客和文章将这些概念混淆,误导了诸多读者。所以本文的目的主要是结合作者本人前后端、移动端等经验,谈谈这几个概念。
|
||||
|
||||
|
||||
## 组件
|
||||
|
||||
组件,最初的目的是为了**代码重用**。功能相对单一、独立。在整个系统结构中位于最底层,被其他代码所依赖。组件是 **“纵向分层”**
|
||||
|
||||
|
||||
## 模块
|
||||
|
||||
模块,最初的目的是将同一类型的代码整合在一起,所以模块的功能相对全面、复杂些,但都同属于一个业务。不同模块之间也会存在相互依赖的关系,但大多数情况下这种相互依赖的关系只是业务之间的相互跳转。所以不同模块之间的地位是平级的。模块是 **“横向分块”**
|
||||
|
||||
因为从代码组织层面上讲,组件化开发是纵向分层,模块化是横向分块。所以模块化和组件化之间没有什么必然的联系。你可以将工程中的代码,按照功能模块进行逻辑上的拆分,然后将代码实现,按照模块化开发的思想,只需相应的代码按照**高内聚**的方式进行整合。假如一个 iOS 工程,使用 cocoapods 组织代码,将模块 A 相关的代码进行整理打包。
|
||||
|
||||
但是这样结果就是你的 App 工程虽然按照模块化的方式进行组织开发,那么某个功能模块进行修改或者升级的时候只需要修改相应模块的代码。假如个人中心模块和购物车模块都有数据持久化的代码。在不使用组件化开发的时候可能在2个模块的代码里面都有数据持久化的代码。这样一个地方有问题改动,另一个也要改动,这样工程组织方式不友好且代码复用率低。
|
||||
|
||||
那么在实际的项目中我们一般是组件化结合模块化一起开发的。
|
||||
|
||||
|
||||
|类别| 目的 | 特点 | 接口 | 成果 | 架构定位 |
|
||||
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
|组件化|重用、解耦|高重用、低耦合|无统一接口|基础库、基础组件|纵向分层|
|
||||
|模块化|封装、隔离|高内聚、低耦合|有统一接口|业务模块、业务框架|横向切块|
|
||||
|
||||
|
||||
参考:
|
||||
- https://blog.csdn.net/blog_jihq/article/details/79191008
|
||||
- https://blog.csdn.net/blog_jihq/article/details/80669616
|
||||
|
||||
|
||||
- RN iOS 插件化开发
|
||||
89
Chapter1 - iOS/1.65.md
Normal file
89
Chapter1 - iOS/1.65.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 多端融合方案
|
||||
|
||||
## SwfitUI
|
||||
|
||||
SwiftUI 自亮相以来,全网就在讨论其与 React、Flutter 之间的关系。
|
||||
|
||||
首先是与 Flutter 的对比,Flutter 的思路是从 0 开始,即语言、基础库、渲染引擎、排版引擎即框架本身全部由自己实现,其渲染引擎 Skia 只需要操作系统为其提供一个 GL Context 便可以完成所有图形渲染,这使得其跨平台性变得十分强大,到目前为止 Windows、Linux、macOS、Fuchsia 都已经得到了 Flutter 官方的支持。
|
||||
|
||||
这种做法我认为有利有弊,首先好处是所有平台下行为一致,不管是滚动视图、Material Design 控件还是模糊效果这些在其他平台没有的都得到了全平台的支持,开发者并不需要为这些去做平台间的适配,反观 React Native… 当然缺点也是存在的,Flutter 这种做法类似于游戏引擎,平台提供的 UI 特性它一概不用,因此 Flutter View 与原生视图的交互就没有那么容易了,同时新的 Dart 语言貌似也不是非常受社区和开发者喜爱。
|
||||
|
||||
SwiftUI 没有像 Flutter 那样从头再来,这个全新的框架依旧使用了 UIKit、AppKit 等作为基础。但它并不是一个 UIKit 的声明式封装
|
||||
|
||||
许多基础组件,像 Text、Button 等都并不是直接使用 UILabel、UIButton 而是一个名为 DisplayList.ViewUpdater.Platform.CGDrawingView 的 UIView 子类。它们使用了自定义绘制,但又集成于 UIKit 的环境中,因此我猜测 SwiftUI 只提供了组件的自定义渲染和布局引擎,它使用到的底层技术还是 Core Animation、Core Graphics、Core Text 等。使用自定义绘制去实现组件可以理解成为跨平台提供便利,毕竟一个按钮还要区分 UIButton、NSButton 来实现未免有些麻烦。但是部分复杂的控件还是采用了 UIKit 中已有的类,比如 UISwitch 等。由于未脱离 UIKit 体系,嵌入一个 UIView 非常容易,你不需要搞什么外部纹理(Flutter 需要),因为它们的上下文是同一个,坐标系也是同一个。
|
||||
|
||||
所以我认为 SwiftUI 更加类似 React Native,使用系统框架提供的组件,只不过绘制和布局可以自己来实现,这在 SwiftUI 之前也有相关的框架这样实践的,比如 Yoga、ComponentKit 等。
|
||||
|
||||
|
||||
SwiftUI 是声明式的 UI 开发方式。关于声明式和命令式的介绍可以查看这篇[博文](https://github.com/FantasticLBP/knowledge-kit/blob/master/第七部分%20设计模式/7.1.md)。声明式开发框架 SwiftUI、React、Flutter、Vue 等都具备下面的特点。
|
||||
|
||||
- 使用各自的 DSL 来描述UI 该长什么样子(样式模版),而不是一句句代码描述来告诉系统该如何一步步构建 UI
|
||||
- 声明所需要的数据部分
|
||||
- 框架内部通过模版和数据部分,负责渲染绘制
|
||||
- 数据发送变动
|
||||
- 框架根据最新的数据和样式模版计算出最新的样式声明
|
||||
- 最新的样式声明和之前的样式声明比较,计算出差值。系统重新绘制
|
||||
|
||||
```Swift
|
||||
@State var name: String = "Tom"
|
||||
var body: some View {
|
||||
Text("Hello \(name)")
|
||||
}
|
||||
```
|
||||
|
||||
在 SwiftUI 中, view 是由纯数据结构描述的。因此这些数据的创建和差分计算都不会带来太多的性能开销。
|
||||
|
||||
## React && React Native
|
||||
|
||||
先谈谈 React 吧。React 的优秀的地方在于:Virtual DOM、JSX、单向数据流等等。但是谈谈 React 这些框架为什么可以做 Web 也可以做跨端解决方案 RN。传统的 Web 开发是基于命令式编程的方式,监听事件、发起请求、操作 DOM、刷新页面。想想看,每次数据变动了都需要刷新 DOM,然后用户就可以在浏览器上看到了最新的数据,DOM 的操作是很耗费资源的。怎么理解这句话,其实 DOM 对象本身就是一个 JS 对象,所以 JS 对于 JS 对象的操作来说性能耗费基本不用考虑,微乎其微。但是 DOM 每次变动到真实 UI 的渲染是非常耗费性能的(触发浏览器的布局和绘制)。至于浏览器的布局重绘原理我会新开文章进行讨论和总结,可以先看看文章底部的参考资料。
|
||||
|
||||
在 React 中使用数据(state、props)+ 样式模版(JSX)的方式开发 UI。以下是 React 大致的工作原理
|
||||
|
||||
- 设置页面所需要的数据 State、props
|
||||
- 创建页面的模版样式部分 JSX
|
||||
- 根据数据和样式模版生成 Virtual DOM
|
||||
- 页面首次渲染的时候先根据 Virtual DOM 生成真实的 UI
|
||||
- 数据变动(setState)结合「批更新策略」
|
||||
- 根据变动后的数据和模版样式生成新的 Virtual DOM
|
||||
- 根据 Diff 算法计算变动的 Virtual DOM 部分
|
||||
- 根据变动的 Virtual DOM 部分去绘制 UI
|
||||
|
||||
什么是 Virtual DOM?
|
||||
|
||||
Virtual DOM 就是一个 JS 对象,用来描述真实的 DOM。看看下面的例子
|
||||
|
||||
```HTML
|
||||
<ol id='ol-list'>
|
||||
<li class='item'>Item 1</li>
|
||||
<li class='item'>Item 2</li>
|
||||
<li class='item'>Item 3</li>
|
||||
</ol>
|
||||
```
|
||||
|
||||
转换为 Virtual DOM
|
||||
|
||||
```Javascript
|
||||
var olElement = {
|
||||
tagName: 'ol',
|
||||
props: {
|
||||
id: 'ol-list'
|
||||
},
|
||||
children: [
|
||||
{tagName: 'li', props: {class: 'item'}, children: ['Item 1']},
|
||||
{tagName: 'li', props: {class: 'item'}, children: ['Item 2']},
|
||||
{tagName: 'li', props: {class: 'item'}, children: ['Item 3']}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
所以 Virtual DOM 抽象出来后就很方便了,在 Web 端可以去渲染到真实的 DOM;在 Native 端可以去映射到 Native UI 组件上。所以 React 有了 Virtual DOM 便可以在 Web 端和 Native 端大展拳脚。
|
||||
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [浏览器的布局绘制与DOM操作](https://blog.csdn.net/sinat_32434539/article/details/77894009)
|
||||
- [前端必读:浏览器内部工作原理](https://www.cnblogs.com/rainy-shurun/p/5603686.html)
|
||||
- [深度理解 Virtual DOM](https://www.cnblogs.com/wubaiqing/p/6726429.html)
|
||||
|
||||
|
||||
242
Chapter1 - iOS/1.66.md
Normal file
242
Chapter1 - iOS/1.66.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# 移动端网络层优化
|
||||
|
||||
当关心 App 的用户体验的时候,不得不考虑网络层相关的问题。因为一个 App 通常来说网络层的操作占据了大多数的场景。几乎每个成熟的 iOS 项目都有一个网络模块,大部分的网络请求都是基于 HTTP 完成,iOS 端采用成熟的 AFNetworking 很容易完成一个功能简单的网络模块,但是使用起来往往会有大量的问题。所以网络层优化是需要大量的经验和知识水平的。对数据的分析和调研、用户反馈,现总结网络层相关的优化手段。
|
||||
优化方面:
|
||||
1. 速度:网络请求速度如何进一步提升
|
||||
2. 弱网:移动端网络环境随时变化,经常出现网络连接很不稳定可用性差的情况。怎样在这种情况下最大限度最快完成网络请求
|
||||
3. 安全:怎样防止被第三方窃听。篡改或冒充,防止运营商劫持,同时有不影响性能
|
||||
|
||||
|
||||
|
||||
## 一、速度
|
||||
|
||||
正常一条网络请求需要经过的流程是:
|
||||
1. DNS 解析。请求 DNS 服务器,获取域名对应的 IP 地址
|
||||
2. 与服务器建立连接。包括 TCP 三次握手,安全协议同步流程
|
||||
3. 连接建立完成,发送、接收数据,解码数据
|
||||
|
||||
这里存在3个优化点:
|
||||
1. 直接使用 IP 地址,去除 DNS 解析步骤
|
||||
2. 不要每次请求都重新建立连接,复用连接或一直使用同一条连接(长连接)
|
||||
3. 压缩数据,减小传输数据的大小
|
||||
|
||||
### 1. DNS
|
||||
|
||||
DNS 完整的解析流程很长,会先从本地系统缓存读取,若没有就到最近的 DNS 服务器取,若没有再到主域名服务器取,每一层都有缓存,但为了域名解析的实时性。每一层缓存都设有过期时间,这种 DNS 解析机制有几个缺点:
|
||||
- 缓存时间设置过长,域名更新不及时。设置时间短,大量 DNS 解析请求影响请求毒素
|
||||
- 域名劫持。容易被中间人攻击,或者运营商劫持。把域名解析道第三方 IP 地址,据统计劫持率高达 7%
|
||||
- DNS 解析过程不受控制,无法保证最快的解析速度
|
||||
- 一次请求只可以借此一个域名
|
||||
|
||||
为了解决上述问题,就有了 HTTPDNS。原理就是代替系统的 DNS 解析工作,解决上述问题。
|
||||
- 域名解析与请求分离,所有请求都直接使用 IP 地址,无需 DNS 解析,App 定时请求 HTTPDNS 服务器更新 IP 地址即可
|
||||
- 通过签名等方式,保证 HTTPDNS 请求的安全,避免被劫持
|
||||
- DNS 解析由自己控制。可以保证根据用户所在地返回就近的 IP 地址。或根据客户端测速结果使用最快的 IP
|
||||
- 一次请求可以解析多个域名
|
||||
|
||||
对于 DNS 解析的情况,业界主流做法就是 HTTPDNS 或者内置 Server IP 列表。客户端直接访问 HTTPDNS 接口,获取业务在域名配置系统上配置的访问延迟最优的 IP,获取到 IP 后就直接往此 IP 发送业务协议请求,不再需要本地 DNS 服务器进行解析,从根本上解决了劫持问题。同时可以降低网络延迟,提高连接的成功率。
|
||||
|
||||
建立的 Server IP 列表,是在本地缓存一个 IP 映射表,可以在 App 启动时请求接口下发更新。访问其他的服务的时候根据映射拿到 IP 再发出请求。
|
||||
|
||||

|
||||
|
||||
绝大多数的 App 的第一步都是 DNS 解析,解析请求回根据当时的网络情况不同而不同,各平台的 DNS 缓存策略存在差异,因此对于移动 App 网络性能会产生影响。App 网络情况跟很多因素都相关。但是 DNS 是第一步也是最重要的一环。
|
||||
|
||||
1. 降低 DNS 请求带来的延迟
|
||||
客户端 App 请求第一步是 DNS 解析。但是由于 Cache 的存在,使得大部分的解析请求都不会产生任何延迟。各平台都有自己的 Cache 过期策略。像 iOS 系统一般都是 24h 后过期。还有就是从飞行模式切换回来、开关机、重置网络设置等都会导致 DNS Cache 的清除。所以一般情况用户在第二天打开你的 App 都会经历一次完整的 DNS 解析请求。网络情况差的时候会明显的请求总耗时增加。如果可以直接跳过 DNS 解析这一步,就可以提高网络性能了。
|
||||
|
||||
2. 预防 NDS 劫持
|
||||
DNS 劫持指的是改变 DNS请求返回的结果,将目的域名对应的 IP 指向另一个地址。一般有两种方式,一种是通过病毒的方式改变本机配置的 DNS 服务器地址,二是通过攻击正常的 DNS 服务器而改变其行为。不管何种方式都会影响 App 的业务请求,如果遇到恶意的攻击还会衍生出各种安全问题。客户端自己做 DNS 到 IP 地址的映射就绕过了向 DNS 服务器请求而可能被遭到攻击的可能,让劫持者无从下手
|
||||
|
||||
3. 服务器动态部署
|
||||
DNS 映射实际上是模拟了 DNS 请求的解析行为。如果客户端将自己的位置信息(例如ip地址、国家码等)上传给服务器,服务器就可以根据位置信息就近推荐合适的 Server IP 地址。从而减小了整体网络请求延迟、实现了动态部署
|
||||
|
||||
如何设计自己的 DNS 映射机制?
|
||||
|
||||
DNS 服务器做的事情就是输入一个域名,输出一个 IP 地址,做自己的 DNS 映射机制就是在客户端维护一个这样的映射文件。不过这个映射文件可以根据服务器在 App 端进行更新。还需要具备一定的容错处理。
|
||||
|
||||
- 一个打包到 App 包里面的默认域名 IP 映射文件,这样就可以避免第一次去服务器取配置文件带来的延迟
|
||||
- 一个定时器可以每隔一段时间**通过签名等方式(避免被劫持)**去服务器获取最新的域名映射文件,并保存到本地
|
||||
- 每次取到最新的映射文件后,保存到本地,并将上次的映射文件保存作为备份。一旦出现线上配置错误的情况,不至于导致请求无法处理
|
||||
- 如果映射文件不能处理域名映射,那么可以回滚到使用默认的 DNS 解析服务
|
||||
- 如果映射后的一个 IP 持续请求失败,那么应从机制上避免这个问题。也就是需要一个无效使用的淘汰机制
|
||||
- 无效的 IP 可以上报到服务器。发现问题解决问题
|
||||
|
||||
在 iOS 端实践。大致有3个角色:mapper、validator、reporter
|
||||
|
||||
- mapper
|
||||
mapper 是负责和外部交互的部分。主要负责在输入 domain name 的情况下输出 ip。同时校验来自应用层请求成功和失败的信息。失败的情况下需要将 ip 进一步验证,以确定是真的无效。如果无效则进行上报。同时还负责更新机制
|
||||
|
||||
- validator
|
||||
负责在接收到请求失败的 ip 时,对这个 ip 进行有效性验证。检测的强弱规则可以自定义。但是一般规则是在后台线程使用这个 ip 进行多次尝试,如果都不成功则告诉 mapper 这个 ip 确实无用,如果成功则说明有效。(某次失败有可能意味着当时的网络环境不稳定)
|
||||
|
||||
- reporter
|
||||
主要负责告诉 server 整个 mapping 机制的健康状况。在出现某个 ip 导致请求失败并由 validator 校验多次还是失败的情况下需要上报到服务端,让服务端去维护或验证。(有可能是在配置的时候少打了个字母 😂)
|
||||
|
||||
根据公司业务情况进行改造。比如采用服务器定时更新映射文件的这一步骤,可以更改为 socket 长链接通道在需要更新时 push,或利用 HTTP2.0 的 server push 机制。还有上报机制。还可以对请求的总量、成功率、映射成功率等数据做侦测。
|
||||
|
||||
### 2. 连接
|
||||
|
||||
第二个问题,连接建立的耗时问题,这里主要的优化思路是复用连接,不用每次请求都重新建立连接,如何更有效率地复用连接,可以说是网络请求速度优化里最主要的点了,并且这里的优化在不断的演进中,值得关注
|
||||
|
||||
#### 2.1 keep-alive
|
||||
|
||||
HTTP 协议里有个 `keep-alive`,HTTP1.1 默认开启,一定程度上缓解了每次请求都需要进行 TCP 三次握手建立连接的耗时。原理是请求完成后不立即释放连接,而是放入**连接池**中。若此时有另一个请求要发出,如果请求的端口和域名在复用池里面有一致的,那么就直接拿出连接池中的连接进行发送和接收数据,少了建立连接的耗时。
|
||||
|
||||
实际上,现在无论是客户端还是浏览器都默认开启了 keep-alive,对同个域名不会再有每发一个请求就进行一次建连的情况,纯短连接已经不存在了。但是有问题,就是这个 keep-alive 的短连接一次只能发送接收一个请求,在上一个请求处理完成之前。无法接受新的请求。若同时发起多个请求,就有两种情况:
|
||||
|
||||
1. 若串行发送请求,可以一直复用一个连接,但速度很慢,每个请求都需要等待上个请求完成再进行发送
|
||||
2. 若并行发送请求,那么首次每个请求都要进行 TCP 三次握手建立新的连接,虽然第二次可以复用连接池里面的这堆连接,但若连接池里面保留的过多,对服务端资源产生交大浪费,若限制了保持的连接数,并行请求超出的连接仍每次需要建立连接。对于这个问题新一代的 HTTP2.0 提出了多路复用解决方案。
|
||||
|
||||
#### 2.2 多路复用
|
||||
|
||||
HTTP2 的多路复用机制一样是复用连接。但它复用的这条连接支持同时处理多条请求,所有请求都可以在这条连接上进行,也就是解决了上面说的并发请求需要建立多条连接带来的问题。
|
||||
|
||||

|
||||
|
||||
HTTP1.1 的协议里,在一个连接里传输数据都是串行顺序传输的,必须等上一个请求全部处理完成后,下一个请求才能进行处理,导致这些请求期间这条连接并不是满带宽传输的,即使是 HTTP1.1 的 pipelining 可以同时发送多个 Request,但 response 仍是按请求的顺序串行返回,只要其中一个 response 稍微大一点或发送错误,就会阻塞住后面的请求。
|
||||
|
||||
HTTP2 这里的多路复用协议解决了这些问题,它把在连接里传输的数据都封装成一个个 stream,每个 stream 都有标识,stream 的发送和接收可以是乱序的,不依赖顺序,也就不会有阻塞的问题,接收端可以根据 stream 的标识去区分属于哪个请求,再进行数据拼接,得到最终数据。
|
||||
|
||||
解释下多路复用这个词,多路可以认为是多个连接,多个操作,复用就是 复用一条连接或一个线程。HTTP2 这里是连接的多路复用,网络相关的还有一个 I/O 的多路复用(select/epoll),指通过事件驱动的方式让多个网络请求返回的数据在同一条线程里完成读写。
|
||||
|
||||
客户端来说,iOS9 以上 NSURLSession 原生支持 HTTP2,只要服务端也支持就可以直接使用,Android 的 okhttp3 以上也支持了 HTTP2,国内一些大型 APP 会自建网络层,支持 HTTP2 的多路复用,避免系统的限制以及根据自身业务需要增加一些特性,例如微信的开源网络库 [mars](https://github.com/Tencent/mars/issues?page=2&q=is%3Aissue+is%3Aopen),做到一条长连接处理微信上的大部分请求,多路复用的特性上基本跟 HTTP2 一致。
|
||||
|
||||
|
||||
#### 2.3 TCP队头阻塞
|
||||
|
||||
HTTP2 的多路复用看起来是完美的解决方案,但还有个问题,就是队头阻塞,这是受限于 TCP 协议,TCP 协议为了保证数据的可靠性,若传输过程中一个 TCP 包丢失,会等待这个包重传后,才会处理后续的包。HTTP2的多路复用让所有请求都在同一条连接进行,中间有一个包丢失,就会阻塞等待重传,所有请求也就被阻塞了。
|
||||
|
||||
对于这个问题不改变 TCP 协议就无法优化,但 TCP 协议依赖操作系统实现以及部分硬件的定制,改进缓慢,于是 GOOGLE 提出 QUIC 协议,相当于在 UDP 协议之上再定义一套可靠传输协议,解决 TCP 的一些缺陷,包括队头阻塞。具体解决原理网上资料较多,可以看看。
|
||||
|
||||
QUIC 处于起步阶段,少有客户端接入,QUIC 协议相对于 HTTP2 最大的优势是对TCP队头阻塞的解决,其他的像安全握手 0RTT / 证书压缩等优化 TLS1.3 已跟进,可以用于 HTTP2,并不是独有特性。TCP 队头阻塞在 HTTP2 上对性能的影响有多大,在速度上 QUIC 能带来多大提升待研究。
|
||||
|
||||
### 3. 数据
|
||||
|
||||
第三个问题,传输数据大小问题。数据对请求速度的影响分两方面,一是压缩率,二是解压序列化反序列化的速度。目前最流行的两种数据格式是 json 和 protobuf。json 是字符串,protobuf 是二进制。即使采用各种压缩算法压缩后,protobuf 仍会比 json 小。protobuf 在数据量和序列化速度上均占优势。
|
||||
|
||||
压缩算法多种多样,且在不断演进。最新出得 Brotli 和 [Z-standard](https://github.com/facebook/zstd) 实现了更高的压缩率。Z-standard 可以根据业务数据样本训练出适合的字典,进一步提高压缩率。是目前最好的压缩算法
|
||||
|
||||
除了传输数据的 body 大小,每个 HTTP 协议头的数据也不可忽视,HTTP2 里对 HTTP 协议头也进行了压缩,HTTP 头大多是重复数据,固定的字段如 method 可以用静态字典,不固定但多个请求重复的字段例如 cookie 用动态字典,可以打到非常高的压缩率。可以查看这篇[文章](https://imququ.com/post/header-compression-in-http2.html)查看介绍。
|
||||
|
||||
|
||||
总结:通过 HTTPDNS,连接多路复用,更好的压缩算法,可以把网络请求的速度优化到不错的程度了。接下来看看弱网环境和安全方面的手段吧
|
||||
|
||||
## 弱网
|
||||
|
||||
手机无线网络环境不稳定,针对弱网的优化,微信有较多的实践和分享
|
||||
|
||||
1. 提升连接的成功率
|
||||
复合连接。建立连接时,阶梯式并发连接,其中一条连通后其他连接都关闭。这个方案结合串行和并发的优势。提高弱网下的连接成功率,同时又不会增加服务器资源消耗
|
||||
|
||||

|
||||
|
||||
2. 制定最合适的超时时间
|
||||
对总读写超时(从请求到响应的超时)、首包超时、包包超时(两个数据段之间的超时)时间制定不同的计算方案,加快对超时的判断,减少等待时间,尽早重试。这里的超时时间还可以根据网络状态动态设定
|
||||
|
||||
3. 调优 TCP 参数,使用 TCP 优化算法
|
||||
对于服务端的 TCP 协议参数进行调优。以及开启各种优化算法使得业务特性和移动端网络环境,包括 RTO 初始值,混合慢启动,TLP、F-RTO 等
|
||||
|
||||
针对弱网的优化未成为标准方案,系统网络库没有内置,不过前两个客户端优化微信的开源网络库 [mars](https://github.com/Tencent/mars) 有实现。
|
||||
|
||||
## 安全
|
||||
|
||||
标准安全协议 `TLS` 保证了网络传输的安全,前身是 SSL,不断在演进。我们日常使用的 HTTPS 就是 HTTP 协议加上 TLS 安全协议。
|
||||
|
||||
安全协议概括性地说解决两个问题:1. 保证安全;2. 降低加密成本
|
||||
|
||||
在保证安全上:
|
||||
1. 使用加密算法组合对传输数据加密,避免被窃听和篡改,
|
||||
2. 认证对方身份,避免被第三方冒充
|
||||
3. 加密算法保持灵活可更新,防止定死算法被破解后无法更换,禁用已破解的算法。
|
||||
|
||||
降低加密成本上:
|
||||
1. 用对称加密算法加密传输数据,解决非对称加密算法的性能低以及长度限制问题
|
||||
2. 缓存安全协议握手后的密钥数据,加快第二次建连的速度。
|
||||
3. 加快握手过程:2RTT -> 0RTT。加快握手的思路,就是原本客户端和服务端需要协商使用什么算法后才能加密发送数据,变成通过内置的公钥和默认的算法,在握手的同时就把数据发出去,也就是不需要等待握手就开始发送数据,打到 0RTT
|
||||
|
||||
想详细看看 TLS 的可以看看这篇[文章](https://blog.helong.info/blog/2015/09/07/tls-protocol-analysis-and-crypto-protocol-design/)
|
||||
|
||||
目前基本主流都支持 TLS 1.2。iOS 网络库默认使用 TLS 1.2,Android 4.4 以上支持 1.2。
|
||||
|
||||
|
||||
## 其他优化方案
|
||||
|
||||
- 域名合并:淘宝、美团等公司公布的解决方案中都有提到,就是将公司原来的很多域名都合并到较少的几个域名。为什么?因为 HTTP 的通道复用就是基于域名划分的。如果域名只有几个,那么多数请求都可以在长连接通道进行,这样就可以降低延迟、增加成功率
|
||||
- 预热,尽早建立长连接。这样其他的业务请求就可以复用长连接通道。加快访问速度。因为每次建立连接都需要经过 DNS 域名解析、TCP 三次握手等漫长步骤。建立长连接的时机可以考虑:冷启动、前后台切换、网络切换等
|
||||
- 如果情况允许,可以将网络切换到 HTTP 2.0,解决了 HTTP1.1 的 head of blocking ,降低了网络延迟,提供了更强大的多路复用技术。还加入了流量控制、新的二进制格式、Server Push、请求优先级和依赖等待等特性。
|
||||
- 建立多通道。比如携程、艺龙、美团等公司都有自己的 TCP、UDP 通道。具有多域名共用通道。
|
||||
- 有些超级大厂还自研了协议。比如 QUIC
|
||||
- 加入 CDN 加速,动态静态资源分离
|
||||
- 对于类似埋点的业务数据请求,可以合并请求,减小流量。另外结合埋点数据压缩上传
|
||||
- App 网络情况诊断
|
||||
- 根据网络情况,动态设置超时时间等
|
||||
|
||||
|
||||
|
||||
## 最后
|
||||
|
||||
网络层涉及的学问非常多,需要懂得多端的重视才可以提出靠谱的解决方案。希望不断认识不断思考。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [2016年携程App网络服务通道治理和性能优化实践](https://chuansongme.com/n/466033251461)
|
||||
|
||||
|
||||
## 参考点:
|
||||
- 移动调度
|
||||
1. DNS(DNS劫持、运营商DNS层次不齐、1RTT请求DNS、不支持LDC多中心调度、不支持自定义调度)。移动调度优势:LDC多中心调度、异地多活快速容灾、白名单问题排查
|
||||
- 接口设计优化
|
||||
1. 慢逻辑监控
|
||||
2. 多次查询优化
|
||||
3. 接口 cache 等
|
||||
- 静态资源、图片等相关策略
|
||||
1. 使用更快的图片格式(WebP等)
|
||||
2. 不同网络的不同图片下发
|
||||
3. 资源合并、压缩(combo)
|
||||
4. 图片压缩(webp)
|
||||
- 让用户觉得快
|
||||
1. 优先级加载
|
||||
2. 异步加载
|
||||
- 减小数据包大小和优化包量
|
||||
1. 推广 Protocol Buffer 等序列化方式
|
||||
2. 接入 SYNC
|
||||
- 监控体系建设
|
||||
1. 全链路数据打通,问题剖析一杆子到底
|
||||
2. 多维评价模型、监控预警、数据化研发
|
||||
3. 管理决策有依据,结果有数据
|
||||
|
||||
|
||||
## 疑难杂症
|
||||
|
||||
1. 有人遇到使用网络经常出现内存泄漏的情况。我觉得这是属于基础功不扎实的情况,因为 [AFHTTPSessionManager manager] 它返回的对象持有一个 session。且 session 的 delegate 对象也是强引用。AFHTTPSessionManager 的父类是 AFURLSessionManager。AFURLSessionManager initWithSessionConfiguration 底层就是 `self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];` 所以会强引用
|
||||
解决方案:
|
||||
- 每次请求完网络后需要给 [AFHTTPSessionManager manager] 这种方式初始化的 manager 释放掉。比如 AFNetWorking 提供的 *invalidateSessionCancelingTasks* 方法。
|
||||
- 将 AFHTTPSessionManager 对象做成单例获取,这样带来另一个好处,NSURLSession 不销毁,另外的请求继续发起的时候不需要初始的网络握手,达到「链路复用」的功能。
|
||||
|
||||
```
|
||||
//AFURLSessionManager.h
|
||||
@property (readonly, nonatomic, strong) NSURLSession *session;
|
||||
|
||||
// AFHTTPSessionManager.m
|
||||
|
||||
|
||||
|
||||
// NSURLSessionConfiguration
|
||||
@property (nullable, readonly, retain) id <NSURLSessionDelegate> delegate;
|
||||
```
|
||||
|
||||
|
||||
```Objective-C
|
||||
- (void)invalidateSessionCancelingTasks:(BOOL)cancelPendingTasks {
|
||||
if (cancelPendingTasks) {
|
||||
[self.session invalidateAndCancel];
|
||||
} else {
|
||||
[self.session finishTasksAndInvalidate];
|
||||
}
|
||||
}
|
||||
```
|
||||
2. 在 iOS 10.3 系统上存在 SSL 证书校验的问题。报错信息如下图。目前没有找到具体原因和解决方案,如果有人有解决方案请联系我。
|
||||

|
||||

|
||||
11
Chapter1 - iOS/1.67.md
Normal file
11
Chapter1 - iOS/1.67.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# iOS工程编译速度优化
|
||||
|
||||
要提升编译速度,我们首先要知道有没有提升?那就需要一个量化的标准。下面的命令让 Xcode 告诉你编译耗费多久时间。
|
||||
|
||||
```shell
|
||||
defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES
|
||||
```
|
||||
|
||||
## ccache
|
||||
|
||||
提高编译速度需要依靠缓存的能力, [ccache](https://ccache.dev) 是一个靠谱的缓存。基于编译器层面。
|
||||
25
Chapter1 - iOS/1.68.md
Normal file
25
Chapter1 - iOS/1.68.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 守护你的App安全
|
||||
|
||||
App Crash 会严重影响用户体验,Crash 率和客户端工程师的个人评级和绩效考核挂钩。因此写出的代码必须安全可靠。崩溃率要保持在一个什么样的水平以下。
|
||||
|
||||
App 在不断业务迭代,经过多人开发维护后可能因为开发者的水平层次不齐,造成代码质量较低,所以要实现的效果就是保证 App 稳健运行,常见的问题捕获处理,将造成奔溃或者 Crash 的因素处理掉,让 App 正常运行。
|
||||
|
||||
|
||||
## 功能设想
|
||||
|
||||
对业务代码零侵入性地将原本会导致 App 奔溃的 crash 信息处理掉,保证 App 正常稳健运行,再将 Crash 信息提取出来呈现给开发者。开发者可以根据相应的 Crash 信息去处理解决对应的代码。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## KVO
|
||||
|
||||
|
||||
```
|
||||
*** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <LHMainHomePageNavigationBar 0x1035a05a0> for the key path "name" from <LHMainHomePageNavigationBar 0x1035a05a0> because it is not registered as an observer.'
|
||||
*** First throw call stack:
|
||||
(0x2045ac518 0x2037879f8 0x2044b6c70 0x204f52470 0x204f5222c 0x101d8ed68 0x2309ad230 0x230456af8 0x100f0edb0 0x230456e18 0x230455e84 0x2309e429c 0x2309e54c4 0x2309c5534 0x230a8b7c0 0x230a8deec 0x230a8711c 0x20453e2bc 0x20453e23c 0x20453db24 0x204538a60 0x204538354 0x20673879c 0x2309abb68 0x101c7086c 0x203ffe8e0)
|
||||
libc++abi.dylib: terminating with uncaught exception of type NSException
|
||||
```
|
||||
8
Chapter1 - iOS/1.69.md
Normal file
8
Chapter1 - iOS/1.69.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# React Native 总结
|
||||
|
||||
## 学习方面
|
||||
|
||||
学过 React.js 之后你再去学习 React Native 会很简单,一些核心的东西理解之后会很简单。比如 React 中的单向数据流、虚拟 Dom、diff 算法、数据变动的批量更新机制、diff 之后的 UI 渲染。
|
||||
|
||||
|
||||
样式布局方面增加了 flexbox,这样子布局在移动端会非常方便,非常简单,
|
||||
104
Chapter1 - iOS/1.7.md
Normal file
104
Chapter1 - iOS/1.7.md
Normal file
@@ -0,0 +1,104 @@
|
||||
|
||||
|
||||
# 对象在内存中的存储
|
||||
|
||||
* 栈、堆、BSS、数据段、代码段是什么?
|
||||
|
||||
* 栈(stack):又称作堆栈,用来存储程序的局部变量(但不包括static声明的变量,static修饰的数据存放于数据段中)。除此之外,在函数被调用时,栈用来传递参数和返回值。
|
||||
|
||||
* 堆(heap):用于存储程序运行中被动态分配的内存段,它的大小并不固定,可动态的扩张和缩减。操作函数(malloc/free)
|
||||
|
||||
* BSS段(bss segment):通常用来存储程序中未被初始化的全局变量和静态变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段输入静态内存分配
|
||||
|
||||
* 数据段(data segment):通常用来存储程序中已被初始化的全局变量和静态变量和字符串的一块内存区域
|
||||
|
||||
* 代码段(code segment):通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量。
|
||||
|
||||

|
||||
|
||||
* ##### 搞清楚上面的概念再来研究下对象在内存中如何存储?
|
||||
|
||||
```
|
||||
Person *p1 = [Person new]
|
||||
```
|
||||
|
||||
看这行代码,先来看几个注意点:
|
||||
|
||||
* new底层做的事情:
|
||||
|
||||
* 在堆内存中申请1块合适大小的空间
|
||||
|
||||
* 在这块内存上根据类模版创建对象。类模版中定义了什么属性就依次把这些属性声明在对象中;对象中还存在一个属性叫做**isa**,是一个指针,指向对象所属的类在代码段中地址
|
||||
|
||||
* 初始化对象的属性。这里初始化有几个原则:a、如果属性的数据类型是基本数据类型则赋值为0;b、如果属性的数据类型是C语言的指针类型则赋值为NULL;c、如果属性的数据类型为OC的指针类型则赋值为nil。
|
||||
|
||||
* 返回堆空间上对象的地址
|
||||
|
||||
* 注意
|
||||
|
||||
* 对象只有属性,没有方法。包括类本身的属性和一个指向代码段中的类isa指针
|
||||
|
||||
* 如何访问对象的属性?指针名->属性名;本质:根据指针名找到指针指向的对象,再根据属性名查找来访问对象的属性值
|
||||
|
||||
* 如何调用方法?[指针名 方法];本质:根据指针名找到指针指向的对象,再发现对象需要调用方法,再通过对象的isa指针找到代码段中的类,再调用类里面方法
|
||||
|
||||
* 为什么不把方法存储在对象中?
|
||||
|
||||
* 因为以类为模版创建的对象只有属性可能不相同,而方法相同,如果堆区的对象里面也保存方法的话就会很重复,浪费了堆空间,因此将方法存储与代码段
|
||||
|
||||
* 所以一个类创建的n个对象的isa指针的地址值都相同,都指向代码段中的类地址
|
||||
|
||||
**做个小实验**
|
||||
|
||||
```
|
||||
#import <Foundation/Foundation.h>
|
||||
@interface Person : NSObject{
|
||||
@public
|
||||
int _age;
|
||||
NSString *_name;
|
||||
int *p;
|
||||
}
|
||||
|
||||
-(void)sayHi;
|
||||
@end
|
||||
|
||||
@implementation Person
|
||||
|
||||
-(void)sayHi{
|
||||
NSLog(@"Hi, %@",_name);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
int main(int argc, const char * argv[]) {
|
||||
Person *p1 = [Person new];
|
||||
Person *p2 = [Person new];
|
||||
Person *p3 = [Person new];
|
||||
p1->_age = 20;
|
||||
p2->_age = 20;
|
||||
|
||||
[p1 sayHi];
|
||||
[p2 sayHi];
|
||||
[p3 sayHi];
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
Person *p1 = [Person new];
|
||||
```
|
||||
|
||||
**这句代码在内存分配原理如下图所示**
|
||||
|
||||

|
||||
|
||||
**结论**
|
||||
|
||||

|
||||

|
||||
|
||||
**可以 看到Person类的3个对象p1、p2、p3的isa的值相同。**
|
||||
|
||||
补充:
|
||||
- [alloc与init区别](https://www.jianshu.com/p/daf668b76861)
|
||||
312
Chapter1 - iOS/1.70.md
Normal file
312
Chapter1 - iOS/1.70.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# 不一样的动态化能力
|
||||
|
||||
> 对于热修复,对于大多数公司来说都是可望而不可及的技术手段。热修复对于线上问题是杀手锏级别项目。Android 热修复方案很多,典型的属微信的 `Tinker` 莫属,而苹果公司对于安全的要求非常高,所以一些动态调用的能力都会被封杀,这篇文章主要研究下 iOS 端的热修复技术方案。
|
||||
|
||||
|
||||
## 热修复方案
|
||||
|
||||
- 将下发的原生代码,通过自己实现的代码解析引擎,将代码转换为AST树,然后存储在相关的模型里面,在通过一个上下文注入到runtime里面,当runtime回调到当前函数的时候,上下文从存储的相关模型取出各个参数,然后放到当前堆栈里面去执行相关的逻辑,执行问之后,在返回之前调用的地方,这里跟腾讯的OCS有点像.
|
||||
|
||||
- JSPatch:加加密,多混淆,关键词替换。(其实重要封杀的是respondsToSelector:, performSelector:, method_exchangeImplementations() 这些函数,然后现在aop、hook、jspatch 都是离不开这些函数的。解决方案将 动态能力的 API 替换名字:而是本地已经处理好,写到代码的静态变量里面,执行的时候去按照相应的解密方法去解密,然后得到 respondsToSelector:, 再去执行)
|
||||
|
||||
- 几大app中的方案都是自己研发的,不过大同小异,有比较多的是从编译器层面出发,直接把写的代码编译好,然后自己再写解析器解析执行
|
||||
|
||||
- lua kit:https://github.com/alibaba/LuaViewSDK;https://alibaba.github.io/LuaViewSDK/guide.html
|
||||
|
||||
其实重要封杀的是respondsToSelector:, performSelector:, method_exchangeImplementations() 这些函数,然后现在aop、hook、jspatch 都是离不开这些函数的。
|
||||
|
||||
|
||||
## 思路
|
||||
|
||||
`JavaScriptCore` 是苹果给开发者操作 Javascript 的一个库,因此使用 JavaScriptCore 基本不存在问题。另外做热修复的基本思路就是在某个类执行某个类方法、某个类的对象执行某个对象方法的时候做一些处理。所以这里涉及到几个因素:类、类对象、类方法、实例方法、方法执行前、方法执行后、方法完全替换。 Objective-C 有运行时特性,所以可以很容易实现上面的几个点,但是直接使用 Runtime 会比较麻烦,这时候就不得不提一下一个面向切面编程的开源库-[Aspects](https://github.com/steipete/Aspects)。
|
||||
|
||||
|
||||
所以剩下来的事情就是将 Aspects 的几个能力暴露给 JavascriptCore 对象。然后 App 在启动的时候去调用热修复接口,拿到修复的字符串,然后给 JavascriptCore 对象,然后 Javascript 对象去执行拿到的热修复的字符串,这样子整个流程下来,当我们去进入某个页面或者调用某个功能的时候,发现 A 类的 methodA 方法有问题,我们下发了热修复代码,就可以在 methodA 的前后加入逻辑,甚至是完全替换。
|
||||
|
||||
|
||||
## 代码实现
|
||||
|
||||
<details>
|
||||
<summary>FixManager</summary>
|
||||
|
||||
```Objective-C
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "Aspects.h"
|
||||
#import <objc/runtime.h>
|
||||
#import <JavaScriptCore/JavaScriptCore.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FixManager : NSObject
|
||||
|
||||
+ (FixManager *)sharedInstance;
|
||||
+ (void)fixIt;
|
||||
+ (void)evalString:(NSString *)javascriptString;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
#import "FixManager.h"
|
||||
|
||||
@implementation FixManager
|
||||
|
||||
+ (FixManager *)sharedInstance
|
||||
{
|
||||
static FixManager *manager = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
manager = [[self alloc] init];
|
||||
});
|
||||
return manager;
|
||||
}
|
||||
|
||||
|
||||
+ (JSContext *)context
|
||||
{
|
||||
static JSContext *_context;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_context = [[JSContext alloc] init];
|
||||
[_context setExceptionHandler:^(JSContext *context, JSValue *exception) {
|
||||
NSLog(@"Ooops, %@", exception);
|
||||
}];
|
||||
});
|
||||
return _context;
|
||||
}
|
||||
|
||||
+ (void)fixIt
|
||||
{
|
||||
[self context][@"fixInstanceMethodBefore"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl){
|
||||
[self _fixWithMethod:NO
|
||||
aspectionOptions:AspectPositionBefore instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
|
||||
};
|
||||
|
||||
[self context][@"fixInstanceMethodReplace"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
|
||||
[self _fixWithMethod:NO aspectionOptions:AspectPositionInstead instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
|
||||
};
|
||||
|
||||
[self context][@"fixInstanceMethodAfter"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
|
||||
[self _fixWithMethod:NO aspectionOptions:AspectPositionAfter instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
|
||||
};
|
||||
|
||||
[self context][@"fixClassMethodBefore"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
|
||||
[self _fixWithMethod:YES aspectionOptions:AspectPositionBefore instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
|
||||
};
|
||||
|
||||
[self context][@"fixClassMethodReplace"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
|
||||
[self _fixWithMethod:YES aspectionOptions:AspectPositionInstead instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
|
||||
};
|
||||
|
||||
[self context][@"fixClassMethodAfter"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
|
||||
[self _fixWithMethod:YES aspectionOptions:AspectPositionAfter instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
|
||||
};
|
||||
|
||||
[self context][@"runClassWithNoParamter"] = ^id(NSString *className, NSString *selectorName) {
|
||||
return [self _runClassWithClassName:className selector:selectorName obj1:nil obj2:nil];
|
||||
};
|
||||
|
||||
[self context][@"runClassWith1Paramter"] = ^id(NSString *className, NSString *selectorName, id obj1) {
|
||||
return [self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:nil];
|
||||
};
|
||||
|
||||
[self context][@"runClassWith2Paramters"] = ^id(NSString *className, NSString *selectorName, id obj1, id obj2) {
|
||||
return [self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:obj2];
|
||||
};
|
||||
|
||||
[self context][@"runVoidClassWithNoParamter"] = ^(NSString *className, NSString *selectorName) {
|
||||
[self _runClassWithClassName:className selector:selectorName obj1:nil obj2:nil];
|
||||
};
|
||||
|
||||
[self context][@"runVoidClassWith1Paramter"] = ^(NSString *className, NSString *selectorName, id obj1) {
|
||||
[self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:nil];
|
||||
};
|
||||
|
||||
[self context][@"runVoidClassWith2Paramters"] = ^(NSString *className, NSString *selectorName, id obj1, id obj2) {
|
||||
[self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:obj2];
|
||||
};
|
||||
|
||||
[self context][@"runInstanceWithNoParamter"] = ^id(id instance, NSString *selectorName) {
|
||||
return [self _runInstanceWithInstance:instance selector:selectorName obj1:nil obj2:nil];
|
||||
};
|
||||
|
||||
[self context][@"runInstanceWith1Paramter"] = ^id(id instance, NSString *selectorName, id obj1) {
|
||||
return [self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:nil];
|
||||
};
|
||||
|
||||
[self context][@"runInstanceWith2Paramters"] = ^id(id instance, NSString *selectorName, id obj1, id obj2) {
|
||||
return [self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:obj2];
|
||||
};
|
||||
|
||||
[self context][@"runVoidInstanceWithNoParamter"] = ^(id instance, NSString *selectorName) {
|
||||
[self _runInstanceWithInstance:instance selector:selectorName obj1:nil obj2:nil];
|
||||
};
|
||||
|
||||
[self context][@"runVoidInstanceWith1Paramter"] = ^(id instance, NSString *selectorName, id obj1) {
|
||||
[self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:nil];
|
||||
};
|
||||
|
||||
[self context][@"runVoidInstanceWith2Paramters"] = ^(id instance, NSString *selectorName, id obj1, id obj2) {
|
||||
[self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:obj2];
|
||||
};
|
||||
|
||||
[self context][@"runInvocation"] = ^(NSInvocation *invocation) {
|
||||
[invocation invoke];
|
||||
};
|
||||
|
||||
// helper:将 JS 的 console.log 用 Native Log 替换
|
||||
[[self context] evaluateScript:@"var console = {}"];
|
||||
[self context][@"console"][@"log"] = ^(id message) {
|
||||
NSLog(@"Javascript log: %@",message);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
+ (void)_fixWithMethod:(BOOL)isClassMethod aspectionOptions:(AspectOptions)option instanceName:(NSString *)instanceName selectorName:(NSString *)selectorName fixImpl:(JSValue *)fixImpl
|
||||
{
|
||||
Class klass = NSClassFromString(instanceName);
|
||||
if (isClassMethod) {
|
||||
klass = object_getClass(klass);
|
||||
}
|
||||
SEL sel = NSSelectorFromString(selectorName);
|
||||
[klass aspect_hookSelector:sel withOptions:option usingBlock:^(id<AspectInfo> aspectInfo){
|
||||
[fixImpl callWithArguments:@[aspectInfo.instance, aspectInfo.originalInvocation, aspectInfo.arguments]];
|
||||
} error:nil];
|
||||
}
|
||||
|
||||
+ (id)_runClassWithClassName:(NSString *)className selector:(NSString *)selector obj1:(id)obj1 obj2:(id)obj2
|
||||
{
|
||||
Class klass = NSClassFromString(className);
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
return [klass performSelector:NSSelectorFromString(selector) withObject:obj1 withObject:obj2];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
|
||||
+ (id)_runInstanceWithInstance:(id)instance selector:(NSString *)selector obj1:(id)obj1 obj2:(id)obj2
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
return [instance performSelector:NSSelectorFromString(selector) withObject:obj1 withObject:obj2];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
|
||||
+ (void)evalString:(NSString *)javascriptString
|
||||
{
|
||||
[[self context] evaluateScript:javascriptString];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary>BugProtector</summary>
|
||||
|
||||
```Objective-C
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BugProtector : NSObject
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
+ (void)getFixScript:(NSString *)scriptText;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
#import "BugProtector.h"
|
||||
#import "FixManager.h"
|
||||
|
||||
@interface BugProtector ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation BugProtector
|
||||
|
||||
static BugProtector *_sharedInstance = nil;
|
||||
|
||||
#pragma mark - life cycle
|
||||
+ (instancetype)sharedInstance
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
//because has rewrited allocWithZone use NULL avoid endless loop lol.
|
||||
_sharedInstance = [[super allocWithZone:NULL] init];
|
||||
[FixManager fixIt];
|
||||
});
|
||||
|
||||
return _sharedInstance;
|
||||
}
|
||||
|
||||
+ (FixManager *)fixManager
|
||||
{
|
||||
static FixManager *_manager;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_manager = [FixManager sharedInstance];
|
||||
});
|
||||
return _manager;
|
||||
}
|
||||
|
||||
+ (id)allocWithZone:(struct _NSZone *)zone
|
||||
{
|
||||
return [BugProtector sharedInstance];
|
||||
}
|
||||
|
||||
+ (instancetype)alloc
|
||||
{
|
||||
return [BugProtector sharedInstance];
|
||||
}
|
||||
|
||||
- (id)copy
|
||||
{
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)mutableCopy
|
||||
{
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(struct _NSZone *)zone
|
||||
{
|
||||
return self;
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
- (void)dealloc
|
||||
{
|
||||
NSLog(@"%s",__func__);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#pragma mark - public Method
|
||||
+ (void)getFixScript:(NSString *)scriptText
|
||||
{
|
||||
[FixManager evalString:scriptText];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
完整的 [Demo](https://github.com/FantasticLBP/BlogDemos/tree/master/HotFix) 可以点此查看链接.
|
||||
|
||||
|
||||
## 未完,待续
|
||||
102
Chapter1 - iOS/1.71.md
Normal file
102
Chapter1 - iOS/1.71.md
Normal file
@@ -0,0 +1,102 @@
|
||||
|
||||
# Flutter初体验-安装
|
||||
|
||||
> 多端融合能力是现在大前端研究的技术风向标之一,当前 Flutter 风头正盛,它的设计之初就是为了解决移动端的当今的诸多问题。
|
||||
|
||||
|
||||
Flutter 的设计的思想以及出发点不是本篇的重点,所以直奔主题,如何在 Mac 上安装 Flutter 的开发环境。
|
||||
|
||||
### 1. Homebrew
|
||||
|
||||
Homebrew 是 Mac 是安装各种软件包的一个工具,所以你需要先安装好 Homebrew。
|
||||
|
||||
```shell
|
||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||
```
|
||||
|
||||
### 2. 安装 Flutter SDK
|
||||
|
||||
#### 下载 SDK
|
||||
SDK 下载方式有2种。一是使用 `git clone -b beta https://github.com/flutter/flutter.git` 下载,二是通过官网选择符合自己机器环境的文件。(实验后发现方式一特别慢,建议大家直接使用方式二)
|
||||
|
||||

|
||||
|
||||
下载好之后解压,安装到自己指定的位置。
|
||||
|
||||
#### 配置环境变量
|
||||
|
||||
Flutter 在运行的时候需要去官网下载一些需要的资源,所以需要设置镜像服务器。我使用的是 iterm2。所以打开 **.zshrc** 文件。如果使用的是系统的 Terminal,则需要打开 **.bash_profile**。
|
||||
|
||||
```shell
|
||||
export PUB_HOSTED_URL=https://pub.flutter-io.cn
|
||||
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
|
||||
export PATH=/Users/liubinpeng/flutter/bin:$PATH
|
||||
```
|
||||
`/Users/liubinpeng` 是我电脑环境下的 Flutter 路径,这里改成你自己的路径。之后执行 `source .zshrc`。
|
||||
|
||||
验证 Flutter 环境是否成功。
|
||||
|
||||
```shell
|
||||
flutter -h
|
||||
```
|
||||
|
||||
### 3. 配置 Android studio
|
||||
|
||||
一般认为用户是 iOS 开发,电脑没有 Android 开发环境,首先去[官网](https://developer.android.google.cn/studio)下载。
|
||||
|
||||
然后通过 `flutter doctor` 来检查 flutter 的环境配置。一般可以看到多个 ✗ 。每个 ✗ 后面的描述内容都是我们需要解决的问题。
|
||||
|
||||
- 打开 Android studio。但是首次打开会报错,提示找不到 SDK。解决方案:在应用程序文件夹下面找到 Android studio,显示包内容。路径如下
|
||||
|
||||
`/Applications/Android\ Studio.app/Contents/bin/idea.properties`,用文本编辑器打开,在最下面添加如下代码
|
||||
|
||||
```shell
|
||||
isable.android.first.run=true
|
||||
```
|
||||
|
||||
- 设置 Android studio 的环境变量。
|
||||
```shell
|
||||
export ANDROID_HOME=~/Library/Android/sdk
|
||||
export PATH=${PATH}:${ANDROID_HOME}/emulator
|
||||
export PATH=${PATH}:${ANDROID_HOME}/tools
|
||||
export PATH=${PATH}:${ANDROID_HOME}/platform-tools
|
||||
```
|
||||
分别是安卓 SDK 路径、安卓模拟器路径、安卓 tools 路径、安卓平台工具。
|
||||
|
||||
- 安装 Android studio Flutter 插件
|
||||
接下来使用 flutter doctor 检查,显示信息
|
||||
|
||||
```shell
|
||||
✗ Flutter plugin not installed; this adds Flutter specific functionality.
|
||||
✗ Dart plugin not installed; this adds Dart specific functionality.
|
||||
```
|
||||
意思是缺少 Flutter 插件。步骤:Preferences -> Plugins -> 搜索栏输入 Flutter,找到第一个点击 install。此时会弹出对话框让你选择安装 Dart,点击 YES。之后重启 Android studio,看到在主界面会多出下图红色框的内容。至此我们可以创建 Flutter 工程了。
|
||||
|
||||

|
||||
|
||||
- 给 Android studio 设置模拟器
|
||||
|
||||
点击右上方区域一个类似手机的按钮,选择手机(Pixel 2).下载对应的系统(pie)(需要开启翻墙模式)。
|
||||
|
||||
### 配置 iOS 环境
|
||||
|
||||
前提安装好 Xcode 和选择好对应的模拟器。并执行下面的脚本
|
||||
|
||||
```shell
|
||||
brew link pkg-config
|
||||
brew install --HEAD usbmuxd
|
||||
brew unlink usbmuxd
|
||||
brew link usbmuxd
|
||||
brew install --HEAD libimobiledevice
|
||||
brew install ideviceinstaller
|
||||
```
|
||||
|
||||
|
||||
### 配置 VSCode
|
||||
|
||||
安装好 VSCode,在插件的地方搜索 Flutter 和 Dart 对应的插件。
|
||||
|
||||

|
||||
最后输入 flutter doctor 检测你的全部是否完毕,至此你可以展开 Flutter 之旅了。祝愉快
|
||||
|
||||
|
||||
99
Chapter1 - iOS/1.72.md
Normal file
99
Chapter1 - iOS/1.72.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# 架构心得
|
||||
|
||||
> 2019-07月底跳槽,从事的工作内容是基础平台内容,主要是基础工具和 SDK 的封装;工程化 cli 落地、研发管理、静态代码扫描等。虽然以前写代码也是站在封装、复用、聚合等出发点写代码,但是还是和真正写 SDK 注意点有很多不同,这也是为什么写这篇文章总结的原因。
|
||||
|
||||
|
||||
## 一些注意点
|
||||
|
||||
- 当你开发某个功能的时候,轻易不要使用第三方的库。为什么?因为你难以确保业务方是否也在使用这个库,可能库在使用了,但是版本号不一致,就会造成 api 内部实现可能不一样,造成功能不符合预期或者一些神奇的 Bug。
|
||||
- 假如你遇到上面的情况,你出于某种原因不得不使用某个第三方,但是你又必须考虑调用者的工程可能也加入了该库。解决方案大体有3种。1、推进业务方不要使用离散的功能三方库,比如 AFNetWorkging 不要自己引入,而是引入基础平台方封装好的网络功能库;2、自己将引入的第三方网络库选取主要用到的功能去自己实现掉。我们首先要自己这个第三方做了什么事情,提供了哪些功能,其中哪些功能是我们会使用到的,那么我们可以借鉴源代码,自己去做类似的事情,然后一个精简版的 AFNetWorking 就出来了;3、将第三方库的类名称、方法名称、Block、宏...都给更换名称(一开始想到找到一定的规则用自动化脚本去做,发现这样子不可能处理全部的 case,程序员自己脑子都想不全所有的 case,所以代码实现根本不可能;)一番操作下来发现还是人工手动操作效果最好
|
||||
- 当你写某个功能的时候,你封装的 SDK 对于提供某个能力,项目以组件化的形式开展,所以你对外暴露的地方在于 Router 文件中, Router 负责解析 url,最后调用 [target performSelector withObject],然后在 target 对象内部真正去实现某个功能,Router 一定只做最简单的事情,也就是 url parse,寻找 target,执行 performSelector。target 暴露某个接口,也许接口内部实现也很复杂,需要依赖其他几个 api 或者其他几个类的 api。所以 api 也就是函数需要做到单一原则。可能某个大的能力需要几个能力的聚合,这个大的函数内部依靠几个单独的函数逻辑才实现某个能力。可能由于版本迭代,你需要将之前不对外暴露的能力也要暴露出去,所以做好函数的单一功能非常重要,可拓展性强、易测试。
|
||||
- 一定要写好 Unit Test。这样子不断版本迭代,对于 UT,输入恒定,输出恒定,这样内部实现如何变动不需要关心,只需要判断恒定输入,恒定输出就足够了。(针对每个函数单一原则的基础上也是满足 UT)
|
||||
- 在做 SDK 的时候,对于一些方法或者函数的返回值,尽量要做到 iOS 和 Android 端的输出值的数据类型一致,除非某些特殊情况,无法保证一致的输出。
|
||||
- 当你想写宏定义的时候应该先判断下是否存在,因为工程中很可能已经存在一个同名的宏。
|
||||
```Objective-C
|
||||
#ifndef Hi
|
||||
#define Hi @"Hello, nice to meet you"
|
||||
#endif
|
||||
```
|
||||
- 避免重复宏定义
|
||||
因为宏定义可以多次,但是一个工程中有可能因为命名太规范了,大家不小心会为一个功能起一个同名的宏定义,所以我们在宏定义的时候需要做判断,不然多个同名宏定义,最后的功能会根据文件编译顺序决定,最后的宏定义才生效。
|
||||
```Objective-c
|
||||
#ifndef CM_IS_CLASS
|
||||
#define CM_IS_CLASS(obj,cls) [obj isKindOfClass:[cls class]]
|
||||
#endif
|
||||
```
|
||||
- 对于你的某个 SDK,你在为某个方法、某个类、某个宏定义命名的时候需要注意选择合适的前缀
|
||||
比如。你的某个项目是在做监控,SDK 的名字叫做 Prism-Client。那么你的类名称、类方法名称、宏定义、分类名称、分类方法名称等都需要合适且统一的前缀,一般选取 `前3个字母组合`。当前的项目叫做 `PCT`。类前面加 PCT,类里面的方法不加前缀。分类名称加前缀 PCT,分类里面的方法前面加前缀,小写的 pct。
|
||||
普通类的方法不加前缀是因为普通类已经通过类名的唯一性确定了方法的唯一。
|
||||
分类里面方法加前缀是因为分类的方法在工程里面这个类都可以访问。所以要在方法前面区分
|
||||
```Objective-C
|
||||
// 安全的数据获取方法
|
||||
#ifndef PCT_SAFE_STRING
|
||||
#define PCT_SAFE_STRING(x) (x) != nil ? (x) : @""
|
||||
#endif
|
||||
|
||||
NSData+PCTAES.h
|
||||
- (NSData *)pct_AES128EncryptWithKey:(NSString *)key gIv:(NSString *)Iv;
|
||||
|
||||
PCTRequestFactory.h
|
||||
+ (void)fetchUploadConfigurationWithRequestURL:(NSString *)requestUrlString
|
||||
params:(NSDictionary *)params
|
||||
success:(void (^)(PRCConfigurationModel*model))success
|
||||
failure:(void (^)(NSError *error))failure;
|
||||
```
|
||||
|
||||
- 一般来说如果你的某个文件代码中高频率的使用宏,且宏里面是做一些运算,建议使用内联函数代替,因为内联函数效率高,且在编译阶段可以检查错误。函数的调用顺序底层是出入栈的过程,Frame Pointer、Stack Pointer。一个栈保存当前函数的局部变量、参数、返回地址。所以不同函数的调用会效率有影响,如果高频使用的函数建议用内联函数。
|
||||
内联函数和宏的区别
|
||||
优点相比于函数
|
||||
|
||||
- inline 函数避免了普通函数的,在汇编时必须调用 call 的缺点:取消了函数的参数压栈,减少了调用的开销,提高效率.所以执行速度确比一般函数的执行速度要快
|
||||
- 集成了宏的优点,使用时直接用代码替换(像宏一样)
|
||||
优点相比于宏
|
||||
- 避免了宏的缺点:需要预编译.因为 inline 内联函数也是函数,不需要预编译
|
||||
- 编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性
|
||||
- 可以使用所在类的保护成员及私有成员。
|
||||
注意事项
|
||||
- 内联函数只是我们向编译器提供的申请,编译器不一定采取inline形式调用函数
|
||||
- 内联函数不能承载大量的代码.如果内联函数的函数体过大,编译器会自动放弃内联
|
||||
- 内联函数内不允许使用循环语句或开关语句
|
||||
- 内联函数的定义须在调用之前
|
||||
- Objective-C 中内联函数用 NS_INLINE ,等价于 static inline。且内联函数的命名需要注意,在该模块内的内联函数需要加前缀。
|
||||
```Objective-C
|
||||
NS_INLINE NSString * PCTGetTableNameFromType(PCTLogTableType type){
|
||||
if (type == PCTLogTableTypeMeta) {
|
||||
return PRC_LOG_TABLE_META;
|
||||
}
|
||||
if (type == PCTLogTableTypePayload) {
|
||||
return PRC_LOG_TABLE_PAYLOAD;
|
||||
}
|
||||
return @"";
|
||||
}
|
||||
```
|
||||
- 什么情况下用统跳(路由能力)?
|
||||
技术 SDK 的话,因为可能依赖非常多的其他技术 SDK 所以会比较难梳理出一个需要暴露的能力,非常难抽象
|
||||
业务 SDK 很清楚需要暴露哪些能力。所以我们一般将业务 SDK 提供统跳能力,技术 SDK 不提供
|
||||
|
||||
- 基础平台组做什么?怎么做?
|
||||
业务线的同学一般做的事情就是在操作 UI,手机屏幕很小,要做的事情也会比较单一,可能就是单击某个按钮然后多线程异步去处理某个逻辑(网络、数据库、File等),然后异步回调里面回调主线程去更新 UI。所以做的事情的广度不一样。基础平台组做的事情一般来说脱离独立的 UI,换句话说就是焦点不在于 UI,而在于整个的架构逻辑,比如一个数据上报 SDK。它考虑的事情不是 UI 怎么用,而是数据来源是什么,我设计的接口需要暴露什么信息,数据如何高效存储、数据如何校验、数据如何高效及时上报。
|
||||
|
||||
假如我做的数据上报 SDK 可以上报 APM 监控数据、同时也开放能力给业务线使用,业务线自己将感兴趣的数据并写入保存,保证不丢失的情况下如何高效上报。因为数据实时上报,所以需要考虑上传的网络环境、Wi-Fi 环境和 4G 环境下的逻辑不一样的、数据聚合组装成自定义报文并上报、一个自然天内数据上传需要做流量限制等等、App 版本升级一些数据可能会失去意义、当然存储的数据也存在时效性。种种这些东西就是在开发前需要考虑清楚的。所以基础平台做事情基本是 **设计思考时间:编码时间 = 7:3**
|
||||
|
||||
为什么?假设你一个需求,预期10天时间;前期架构设计、类的设计、Uint Test 设计估计7天,到时候编码开发2天完成。
|
||||
|
||||
这么做的好处很多,比如:
|
||||
1. 除非是非常优秀,不然脑子想的再前面到真正开发的时候发现有出入,coding 完发现和前期方案设计不一样。所以建议用流程图、UML图、技术架构图、UT 也一样,设计个表格,这样等到时候编码也就是 coding 的工作了,将图翻译成代码
|
||||
2. 后期和别人讨论或者沟通或者 CTO 进行 code review 的时候不需要一行行看代码。你将相关的架构图、流程图、UML 图给他看看。他再看看一些关键逻辑的 UT,保证输入输出正确,一般来说这样就够了
|
||||
3. 软件项目管理也一样,制定进度表、确定干系人、kick-of meeting 等、定期碰头
|
||||
|
||||
- 一般来说不要在 load 方法里面做非本类的事情。
|
||||
一般来说,不应该在当前类的 `load` 方法里面写和其他类有关系的代码,除非非做不可。
|
||||
```Objective-C
|
||||
+ (void)load
|
||||
{
|
||||
NSLog(@"%zd", [AFNetworkReachabilityManager sharedManager].networkReachabilityStatus);
|
||||
}
|
||||
```
|
||||
之前在做一个类 `PCTRequestFactory` 用来管理网络相关的逻辑。需要判断网络状态,我们都知道 AFNetWorking 第一次判断网络状态得到的是 AFNetworkReachabilityStatusUnknown。而我的逻辑需要 SDK 启动的时候判断网络状态,然后去上报数据。所以刚开始 AFNetworkReachabilityStatusUnknown 显然不能上报 Crash 数据,所以想着是将第一次的网络状态获取放到 **load** 方法里。这样是没问题的,可以拿到网络状态,但是我们知道 load 是类加载的时候调用的,打开 Xcode 看到 Build Phases 里面 `Link BiBinary With Libraries` 这个里面的库的顺序决定了里面的类加载顺序。我们知道 Pod 的原理是在 Podfile 里面描述的 pod 库依赖,然后会按照字典序(首字母排序去)引入,所以 AFNetWorking 这个肯定早,所以会成功的。但是万一是人工手动去引入或者修改库的位置,则在 PCTRequestFactory 里面的 load 方法执行的时候不一定可以保证 AFNetworkReachabilityManager 已经加载好。所以将 load 逻辑移动到 init 里面。
|
||||
|
||||
另外,load 方法一般只做和本类有关系的逻辑,比如 hook 方法。
|
||||
10
Chapter1 - iOS/1.73.md
Normal file
10
Chapter1 - iOS/1.73.md
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
# Ruby
|
||||
|
||||
> 为了 iOS 工程化开展,自己最近开始了 Ruby 的学习,本篇博文就用来记录 Ruby 的学习心得和体验。
|
||||
|
||||
- block 最后一行是返回值,不需要指定 return
|
||||
- ruby 脚本语言在编写好代码后也想像其他 GPL 语言一样断点可以,需要安装依赖和相应的代码调整
|
||||
- `gem install pry`
|
||||
- 文件引入 `require 'pry'`
|
||||
- 需要 debug 的地方加上 `binding.pry`
|
||||
739
Chapter1 - iOS/1.74.md
Normal file
739
Chapter1 - iOS/1.74.md
Normal file
@@ -0,0 +1,739 @@
|
||||
# APM
|
||||
|
||||
> Application Performance Management 应用性能管理对一个应用的持续稳定运行至关重要。所以这篇文章就从一个 iOS App 的性能管理的纬度谈谈如何精确监控以及数据如何上报等技术点
|
||||
|
||||
App 的性能问题是影响用户体验的重要因素之一。性能问题主要包含:Crash、网络请求错误或者超时、UI 响应速度慢、主线程卡顿、CPU 和内存使用率高、耗电量大等等。大多数的问题原因在于开发者错误地使用了线程锁、系统函数、编程规范问题、数据结构等等。解决问题的关键在于尽早的发现和定位问题。
|
||||
|
||||
本篇文章着重总结了 APM 的原因以及如何收集数据。APM 数据收集后结合数据上报机制,按照一定策略上传数据到服务端。服务端消费这些信息并产出报告。请结合[姊妹篇](./1.80.md), 总结了如何打造一款灵活可配置、功能强大的数据上报组件。
|
||||
|
||||
|
||||
|
||||
## 监控项目
|
||||
|
||||
- 页面渲染时长
|
||||
- 主线程卡顿
|
||||
- 网络错误+
|
||||
- FPS
|
||||
- 大文件存储
|
||||
- CPU
|
||||
- 内存使用
|
||||
- Crash
|
||||
- 启动时长
|
||||
|
||||
|
||||
|
||||
|
||||
## 一、卡顿监控
|
||||
|
||||
卡顿问题,就是在主线程上无法响应用户交互的问题。影响着用户的直接体验,所以针对 App 的卡顿监控是 APM 里面重要的一环。
|
||||
|
||||
FPS(frame per second)每秒钟的帧刷新次数,iPhone 手机以 60 为最佳,iPad 某些型号是 120,也是作为卡顿监控的一项参考参数,为什么说是参考参数?因为它不准确。先说说怎么获取到 FPS。CADisplayLink 是一个系统定时器,会以帧刷新频率一样的速率来刷新视图。 `[CADisplayLink displayLinkWithTarget:self selector:@selector(###:)]`。至于为什么不准我们来看看下面的示例代码
|
||||
|
||||
|
||||
```Objective-C
|
||||
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_displayLinkTick:)];
|
||||
[_displayLink setPaused:YES];
|
||||
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
|
||||
```
|
||||
|
||||
代码所示,CADisplayLink 对象是被添加到指定的 RunLoop 的某个 Mode 下。所以还是 CPU 层面的操作。请继续往下看
|
||||
|
||||
|
||||
|
||||
|
||||
#### 1. 屏幕绘制原理
|
||||
|
||||

|
||||
|
||||
讲讲老式的 CRT 显示器的原理。 CRT 电子枪按照上面方式,从上到下一行行扫描,扫面完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;当一帧画面绘制完成后,电子枪恢复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(Vertical synchronization),简称 VSync。显示器通常以固定的频率进行刷新,这个固定的刷新频率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏,但是原理保持不变。
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
通常,屏幕上一张画面的显示是由 CPU、GPU 和显示器是按照上图的方式协同工作的。CPU 根据工程师写的代码计算好需要现实的内容(比如视图创建、布局计算、图片解码、文本绘制等),然后把计算结果提交到 GPU,GPU 负责图层合成、纹理渲染,随后 GPU 将渲染结果提交到帧缓冲区。随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过数模转换传递给显示器显示。
|
||||
|
||||
在帧缓冲区只有一个的情况下,帧缓冲区的读取和刷新都存在效率问题,为了解决效率问题,显示系统会引入2个缓冲区,即双缓冲机制。在这种情况下,GPU 会预先渲染好一帧放入帧缓冲区,让视频控制器来读取,当下一帧渲染好后,GPU 直接把视频控制器的指针指向第二个缓冲区。提升了效率。
|
||||
|
||||
目前来看,双缓冲区提高了效率,但是带来了新的问题:当视频控制器还未读取完成时,即屏幕内容显示了部分,GPU 将新渲染好的一帧提交到另一个帧缓冲区并把视频控制器的指针指向新的帧缓冲区,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂的情况。
|
||||
|
||||
为了解决这个问题,GPU 通常有一个机制叫垂直同步信号(V-Sync),当开启垂直同步信号后,GPU 会等到视频控制器发送 V-Sync 信号后,才进行新的一帧的渲染和帧缓冲区的更新。这样的几个机制解决了画面撕裂的情况,也增加了画面流畅度。但需要更多的计算资源
|
||||
|
||||
|
||||

|
||||
|
||||
#### 答疑
|
||||
|
||||
可能有些人会看到「当开启垂直同步信号后,GPU 会等到视频控制器发送 V-Sync 信号后,才进行新的一帧的渲染和帧缓冲区的更新」这里会想,GPU 收到 V-Sync 才进行新的一帧渲染和帧缓冲区的更新,那是不是双缓冲区就失去意义了?
|
||||
|
||||
设想一个显示器显示第一帧图像和第二帧图像的过程。首先在双缓冲区的情况下,GPU 首先渲染好一帧图像存入到帧缓冲区,然后让视频控制器的指针直接直接这个缓冲区,显示第一帧图像。第一帧图像的内容显示完成后,视频控制器发送 V-Sync 信号,GPU 收到 V-Sync 信号后渲染第二帧图像并将视频控制器的指针指向第二个帧缓冲区。
|
||||
|
||||
**看上去第二帧图像是在等第一帧显示后的视频控制器发送 V-Sync 信号。是吗?真是这样的吗? 😭 想啥呢,当然不是。 🐷 不然双缓冲区就没有存在的意义了**
|
||||
|
||||
|
||||
|
||||
揭秘。请看下图
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
当第一次 V-Sync 信号到来时,先渲染好一帧图像放到帧缓冲区,但是不展示,当收到第二个 V-Sync 信号后读取第一次渲染好的结果(视频控制器的指针指向第一个帧缓冲区),并同时渲染新的一帧图像并将结果存入第二个帧缓冲区,等收到第三个 V-Sync 信号后,读取第二个帧缓冲区的内容(视频控制器的指针指向第二个帧缓冲区),并开始第三帧图像的渲染并送入第一个帧缓冲区,依次不断循环往复。
|
||||
|
||||
请查看资料,需要梯子:[Multiple buffering](https://en.m.wikipedia.org/wiki/Multiple_buffering)
|
||||
|
||||
|
||||
|
||||
#### 2. 卡顿产生的原因
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容(视图绘制、图形解码、文本绘制等)。然后将计算的内容提交到 GPU,GPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内,CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃,等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。
|
||||
|
||||
|
||||
目前 iOS 设备有双缓存机制,也有三缓冲机制,Android 现在主流是三缓冲机制,在早期是单缓冲机制。
|
||||
[iOS 三缓冲机制例子](https://ios.developreference.com/article/12261072/Metal+newBufferWithBytes+usage)
|
||||
|
||||
|
||||
CPU 和 GPU 资源消耗原因很多,比如对象的频繁创建、属性调整、文件读取、视图层级的调整、布局的计算(AutoLayout 视图个数多了就是线性方程求解难度变大)、图片解码(大图的读取优化)、图像绘制、文本渲染、数据库读取(多读还是多写乐观锁、悲观锁的场景)、锁的使用(举例:自旋锁使用不当会浪费 CPU)等方面。开发者根据自身经验寻找最优解(这里不是本文重点)。
|
||||
|
||||
|
||||
|
||||
|
||||
#### 3. APM 如何监控卡顿并上报
|
||||
|
||||
|
||||
CADisplayLink 肯定不用了,这个 FPS 仅作为参考。一般来讲,卡顿的监测有2种方案:**监听 RunLoop 状态回调、子线程 ping 主线程**
|
||||
|
||||
|
||||
|
||||
##### 3.1 RunLoop 状态监听的方式
|
||||
|
||||
RunLoop 负责监听输入源进行调度处理。比如网络、输入设备、周期性或者延迟事件、异步回调等。RunLoop 会接收2种类型的输入源:一种是来自另一个线程或者来自不同应用的异步消息(source0事件)、另一种是来自预定或者重复间隔的事件。
|
||||
|
||||
RunLoop 状态如下图
|
||||
|
||||

|
||||
|
||||
第一步:通知 Observers,RunLoop 要开始进入 loop,紧接着进入 loop
|
||||
```Objective-c
|
||||
if (currentMode->_observerMask & kCFRunLoopEntry )
|
||||
// 通知 Observers: RunLoop 即将进入 loop
|
||||
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
|
||||
// 进入loop
|
||||
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
|
||||
```
|
||||
|
||||
第二步:开启 do while 循环保活线程,通知 Observers,RunLoop 触发 Timer 回调、Source0 回调,接着执行被加入的 block
|
||||
```Objective-c
|
||||
if (rlm->_observerMask & kCFRunLoopBeforeTimers)
|
||||
// 通知 Observers: RunLoop 即将触发 Timer 回调
|
||||
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
|
||||
if (rlm->_observerMask & kCFRunLoopBeforeSources)
|
||||
// 通知 Observers: RunLoop 即将触发 Source 回调
|
||||
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
|
||||
// 执行被加入的block
|
||||
__CFRunLoopDoBlocks(rl, rlm);
|
||||
```
|
||||
|
||||
第三步:RunLoop 在触发 Source0 回调后,如果 Source1 是 ready 状态,就会跳转到 handle_msg 去处理消息。
|
||||
```Objective-c
|
||||
// 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息
|
||||
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
msg = (mach_msg_header_t *)msg_buffer;
|
||||
|
||||
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
|
||||
goto handle_msg;
|
||||
}
|
||||
#elif DEPLOYMENT_TARGET_WINDOWS
|
||||
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
|
||||
goto handle_msg;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
第四步:回调触发后,通知 Observers 即将进入休眠状态
|
||||
```Objective-c
|
||||
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
|
||||
// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)
|
||||
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
|
||||
__CFRunLoopSetSleeping(rl);
|
||||
```
|
||||
|
||||
第五步:进入休眠后,会等待 mach_port 消息,以便再次唤醒。只有以下4种情况才可以被再次唤醒。
|
||||
- 基于 port 的 source 事件
|
||||
- Timer 时间到
|
||||
- RunLoop 超时
|
||||
- 被调用者唤醒
|
||||
```Objective-c
|
||||
do {
|
||||
if (kCFUseCollectableAllocator) {
|
||||
// objc_clear_stack(0);
|
||||
// <rdar://problem/16393959>
|
||||
memset(msg_buffer, 0, sizeof(msg_buffer));
|
||||
}
|
||||
msg = (mach_msg_header_t *)msg_buffer;
|
||||
|
||||
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
|
||||
|
||||
if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
|
||||
// Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
|
||||
while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
|
||||
if (rlm->_timerFired) {
|
||||
// Leave livePort as the queue port, and service timers below
|
||||
rlm->_timerFired = false;
|
||||
break;
|
||||
} else {
|
||||
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
|
||||
}
|
||||
} else {
|
||||
// Go ahead and leave the inner loop.
|
||||
break;
|
||||
}
|
||||
} while (1);
|
||||
```
|
||||
|
||||
第六步:唤醒时通知 Observer,RunLoop 的线程刚刚被唤醒了
|
||||
```Objective-C
|
||||
// 通知 Observers: RunLoop 的线程刚刚被唤醒了
|
||||
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
|
||||
// 处理消息
|
||||
handle_msg:;
|
||||
__CFRunLoopSetIgnoreWakeUps(rl);
|
||||
```
|
||||
|
||||
第七步:RunLoop 唤醒后,处理唤醒时收到的消息
|
||||
- 如果是 Timer 时间到,则触发 Timer 的回调
|
||||
- 如果是 dispatch,则执行 block
|
||||
- 如果是 source1 事件,则处理这个事件
|
||||
```Objective-C
|
||||
#if USE_MK_TIMER_TOO
|
||||
// 如果一个 Timer 到时间了,触发这个Timer的回调
|
||||
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_TIMER();
|
||||
// On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
|
||||
// In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
|
||||
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
|
||||
// Re-arm the next timer
|
||||
__CFArmNextTimerInMode(rlm, rl);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// 如果有dispatch到main_queue的block,执行block
|
||||
else if (livePort == dispatchPort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
|
||||
__CFRunLoopModeUnlock(rlm);
|
||||
__CFRunLoopUnlock(rl);
|
||||
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
|
||||
#if DEPLOYMENT_TARGET_WINDOWS
|
||||
void *msg = 0;
|
||||
#endif
|
||||
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
|
||||
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
|
||||
__CFRunLoopLock(rl);
|
||||
__CFRunLoopModeLock(rlm);
|
||||
sourceHandledThisLoop = true;
|
||||
didDispatchPortLastTime = true;
|
||||
}
|
||||
// 如果一个 Source1 (基于port) 发出事件了,处理这个事件
|
||||
else {
|
||||
CFRUNLOOP_WAKEUP_FOR_SOURCE();
|
||||
|
||||
// If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again.
|
||||
voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);
|
||||
|
||||
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
|
||||
if (rls) {
|
||||
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
|
||||
mach_msg_header_t *reply = NULL;
|
||||
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
|
||||
if (NULL != reply) {
|
||||
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
|
||||
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
|
||||
}
|
||||
#elif DEPLOYMENT_TARGET_WINDOWS
|
||||
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
|
||||
#endif
|
||||
```
|
||||
|
||||
第八步:根据当前 RunLoop 状态判断是否需要进入下一个 loop。当被外部强制停止或者 loop 超时,就不继续下一个 loop,否则进入下一个 loop
|
||||
```Objective-C
|
||||
if (sourceHandledThisLoop && stopAfterHandle) {
|
||||
// 进入loop时参数说处理完事件就返回
|
||||
retVal = kCFRunLoopRunHandledSource;
|
||||
} else if (timeout_context->termTSR < mach_absolute_time()) {
|
||||
// 超出传入参数标记的超时时间了
|
||||
retVal = kCFRunLoopRunTimedOut;
|
||||
} else if (__CFRunLoopIsStopped(rl)) {
|
||||
__CFRunLoopUnsetStopped(rl);
|
||||
// 被外部调用者强制停止了
|
||||
retVal = kCFRunLoopRunStopped;
|
||||
} else if (rlm->_stopped) {
|
||||
rlm->_stopped = false;
|
||||
retVal = kCFRunLoopRunStopped;
|
||||
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
|
||||
// source/timer一个都没有
|
||||
retVal = kCFRunLoopRunFinished;
|
||||
}
|
||||
```
|
||||
|
||||
完整且带有注释的 RunLoop 代码见[此处](./../assets/CFRunLoop.c)。 Source1 是 RunLoop 用来处理 Mach port 传来的系统事件的,Source0 是用来处理用户事件的。收到 Source1 的系统事件后本质还是调用 Source0 事件的处理函数。
|
||||
|
||||
|
||||

|
||||
RunLoop 6个状态
|
||||
```Objective-C
|
||||
|
||||
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
|
||||
kCFRunLoopEntry , // 进入 loop
|
||||
kCFRunLoopBeforeTimers , // 触发 Timer 回调
|
||||
kCFRunLoopBeforeSources , // 触发 Source0 回调
|
||||
kCFRunLoopBeforeWaiting , // 等待 mach_port 消息
|
||||
kCFRunLoopAfterWaiting ), // 接收 mach_port 消息
|
||||
kCFRunLoopExit , // 退出 loop
|
||||
kCFRunLoopAllActivities // loop 所有状态改变
|
||||
}
|
||||
```
|
||||
|
||||
RunLoop 在进入睡眠前的方法执行时间过长而导致无法进入睡眠,或者线程唤醒后接收消息时间过长而无法进入下一步,都会阻塞线程。如果是主线程,则表现为卡顿。
|
||||
|
||||
一旦发现进入睡眠前的 KCFRunLoopBeforeSources 状态,或者唤醒后 KCFRunLoopAfterWaiting,在设置的时间阈值内没有变化,则可判断为卡顿,此时 dump 堆栈信息,还原案发现场,进而解决卡顿问题。
|
||||
|
||||
|
||||
开启一个子线程,不断进行循环监测是否卡顿了。在 n 次都超过卡顿阈值后则认为卡顿了。卡顿之后进行堆栈 dump 并上报(具有一定的机制,数据处理在下一 part 讲)。
|
||||
|
||||
卡顿阈值的设置的依据是 WatchDog 的机制。APM 系统里面的阈值需要小于 WatchDog 的值,不需要非常小,一般认为启动时间在 5s,其他状态下都是 3s。WatchDog 在不同状态下具有不同的值。
|
||||
- 启动(Launch):20s
|
||||
- 恢复(Resume):10s
|
||||
- 挂起(Suspend):10s
|
||||
- 退出(Quit):6s
|
||||
- 后台(Background):3min(在 iOS7 之前可以申请 10min;之后改为 3min;可连续申请,最多到 10min)
|
||||
|
||||
|
||||
```Objective-c
|
||||
// 设置Runloop observer的运行环境
|
||||
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
|
||||
// 创建Runloop observer对象
|
||||
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
|
||||
kCFRunLoopAllActivities,
|
||||
YES,
|
||||
0,
|
||||
&runLoopObserverCallBack,
|
||||
&context);
|
||||
// 将新建的observer加入到当前thread的runloop
|
||||
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
|
||||
// 创建信号
|
||||
_semaphore = dispatch_semaphore_create(0);
|
||||
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
// 在子线程监控时长
|
||||
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
while (YES) {
|
||||
if (strongSelf.isCancel) {
|
||||
return;
|
||||
}
|
||||
// N次卡顿超过阈值T记录为一次卡顿
|
||||
long semaphoreWait = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, strongSelf.limitMillisecond * NSEC_PER_MSEC));
|
||||
if (semaphoreWait != 0) {
|
||||
if (self->_activity == kCFRunLoopBeforeSources || self->_activity == kCFRunLoopAfterWaiting) {
|
||||
if (++strongSelf.countTime < strongSelf.standstillCount){
|
||||
continue;
|
||||
}
|
||||
// 堆栈信息 dump 并结合数据上报机制,按照一定策略上传数据到服务器。堆栈 dump 会在下面讲解。数据上报会在 [打造功能强大、灵活可配置的数据上报组件](./1.80.md) 讲
|
||||
}
|
||||
}
|
||||
strongSelf.countTime = 0;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
可能很多人纳闷 RunLoop 状态那么多,为什么选择 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting?因为大部分卡顿都是在 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting 之间。比如 Source0 类型的 App 内部事件等
|
||||
|
||||
# todo:虽然知道 RunLoop 的状态,但是为什么是 dispatch_semaphore_wait
|
||||
|
||||
|
||||
|
||||
##### 3.2 子线程 ping 主线程监听的方式
|
||||
|
||||
开启一个子线程,创建一个初始值为0的信号量、一个初始值为 YES 的布尔值类型标志位。将设置标志位为 NO 的任务派发到主线程中去,子线程休眠阈值时间,时间到后判断标志位是否被主线程成功(值为 NO),如果没成功则认为猪线程发生了卡顿情况,此时 dump 堆栈信息并结合数据上报机制,按照一定策略上传数据到服务器。数据上报会在 [打造功能强大、灵活可配置的数据上报组件](./1.80.md) 讲
|
||||
|
||||
```Objective-c
|
||||
while (self.isCancelled == NO) {
|
||||
@autoreleasepool {
|
||||
__block BOOL isMainThreadNoRespond = YES;
|
||||
|
||||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
isMainThreadNoRespond = NO;
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
});
|
||||
|
||||
[NSThread sleepForTimeInterval:self.threshold];
|
||||
|
||||
if (isMainThreadNoRespond) {
|
||||
if (self.handlerBlock) {
|
||||
self.handlerBlock(); // 外部在 block 内部 dump 堆栈(下面会讲),数据上报
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 4. 堆栈 dump
|
||||
|
||||
方法堆栈的获取是一个麻烦事。理一下思路。`[NSThread callStackSymbols]` 可以获取当前线程的调用栈。但是当监控到卡顿发生,需要拿到主线程的堆栈信息就无能为力了。从任何线程回到主线程这条路走不通。先做个知识回顾。
|
||||
|
||||
在计算机科学中,调用堆栈是一种栈类型的数据结构,用于存储有关计算机程序的线程信息。这种栈也叫做执行堆栈、程序堆栈、控制堆栈、运行时堆栈、机器堆栈等。调用堆栈用于跟踪每个活动的子例程在完成执行后应该返回控制的点。
|
||||
|
||||
维基百科搜索到 “Call Stack” 的一张图和例子,如下
|
||||

|
||||
上图表示为一个栈。分为若干个栈帧(Frame),每个栈帧对应一个函数调用。下面蓝色部分表示 `DrawSquare` 函数,它在执行的过程中调用了 `DrawLine` 函数,用绿色部分表示。
|
||||
|
||||
可以看到栈帧由三部分组成:函数参数、返回地址、局部变量。比如在 DrawSquare 内部调用了 DrawLine 函数:第一先把 DrawLine 函数需要的参数入栈;第二把返回地址(控制信息。举例:函数 A 内调用函数 B,调用函数B 的下一行代码的地址就是返回地址)入栈;第三函数内部的局部变量也在该栈中存储。
|
||||
|
||||
栈指针 Stack Pointer 表示当前栈的顶部,大多部分操作系统都是栈向下生长,所以栈指针是最小值。帧指针 Frame Pointer 指向的地址中,存储了上一次 Stack Pointer 的值,也就是返回地址。
|
||||
|
||||
大多数操作系统中,每个栈帧还保存了上一个栈帧的帧指针。因此知道当前栈帧的 Stack Pointer 和 Frame Pointer 就可以不断回溯,递归获取栈底的帧。
|
||||
|
||||
接下来的步骤就是拿到所有线程的 Stack Pointer 和 Frame Pointer。然后不断回溯,还原案发现场。
|
||||
|
||||
App 在运行的时候,会对应一个 Mach Task,而 Task 下可能有多条线程同时执行任务。《OS X and iOS Kernel Programming》 中描述 Mach Task 为:任务(Task)是一种容器对象,虚拟内存空间和其他资源都是通过这个容器对象管理的,这些资源包括设备和其他句柄。
|
||||
|
||||
系统方法 `kern_return_t task_threads(task_inspect_t target_task, thread_act_array_t *act_list, mach_msg_type_number_t *act_listCnt);` 可以获取到所有的线程,不过这种方法获取到的线程信息是最底层的 **mach 线程**。
|
||||
|
||||
对于每个线程,可以用 `kern_return_t thread_get_state(thread_act_t target_act, thread_state_flavor_t flavor, thread_state_t old_state, mach_msg_type_number_t *old_stateCnt);` 方法获取它的所有信息,信息填充在 `_STRUCT_MCONTEXT` 类型的参数中,这个方法中有2个参数随着 CPU 架构不同而不同。所以需要定义宏屏蔽不同 CPU 之间的区别。
|
||||
|
||||
`_STRUCT_MCONTEXT` 结构体中,存储了当前线程的 Stack Pointer 和最顶部栈帧的 Frame pointer,进而回溯整个线程调用堆栈。
|
||||
|
||||
但是上述方法拿到的是内核线程,我们需要的信息是 NSThread,所以需要将内核线程转换为 NSThread。
|
||||
|
||||
pthread 的 p 是 **POSIX** 的缩写,表示「可移植操作系统接口」(Portable Operating System Interface)。设计初衷是每个系统都有自己独特的线程模型,且不同系统对于线程操作的 API 都不一样。所以 POSIX 的目的就是提供抽象的 pthread 以及相关 API。这些 API 在不同的操作系统中有不同的实现,但是完成的功能一致。
|
||||
|
||||
Unix 系统提供的 `task_threads` 和 `thread_get_state` 操作的都是内核系统,每个内核线程由 thread_t 类型的 id 唯一标识。pthread 的唯一标识是 pthread_t 类型。其中内核线程和 pthread 的转换(即 thread_t 和 pthread_t)很容易,因为 pthread 设计初衷就是「抽象内核线程」。
|
||||
|
||||
`pthread_create` 方法创建线程的回调函数为 **nsthreadLauncher**。
|
||||
```Objective-c
|
||||
static void *nsthreadLauncher(void* thread)
|
||||
{
|
||||
NSThread *t = (NSThread*)thread;
|
||||
[nc postNotificationName: NSThreadDidStartNotification object:t userInfo: nil];
|
||||
[t _setName: [t name]];
|
||||
[t main];
|
||||
[NSThread exit];
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
|
||||
NSThreadDidStartNotification 其实就是字符串 @"_NSThreadDidStartNotification"。
|
||||
|
||||
```Objective-c
|
||||
<NSThread: 0x...>{number = 1, name = main}
|
||||
```
|
||||
为了 NSThread 和内核线程对应起来,只能通过 name 一一对应。 pthread 的 API `pthread_getname_np` 也可获取内核线程名字。np 代表 not POSIX,所以不能跨平台使用。
|
||||
|
||||
思路概括为:将 NSThread 的原始名字存储起来,再将名字改为某个随机数(时间戳),然后遍历内核线程 pthread 的名字,名字匹配则 NSThread 和内核线程对应了起来。找到后将线程的名字还原成原本的名字。对于主线程,由于不能使用 `pthread_getname_np`,所以在当前代码的 load 方法中获取到 thread_t,然后匹配名字。
|
||||
|
||||
|
||||
```Objective-c
|
||||
static mach_port_t main_thread_id;
|
||||
+ (void)load {
|
||||
main_thread_id = mach_thread_self();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## 二、 App 启动时间监控
|
||||
|
||||
#### 1. App 启动时间的监控
|
||||
|
||||
思路比较简单。如下
|
||||
|
||||
- 在监控类的 `load` 方法中先拿到当前的时间值
|
||||
- 监听 App 启动完成后的通知 `UIApplicationDidFinishLaunchingNotification`
|
||||
- 收到通知后拿到当前的时间
|
||||
- 步骤1和3的时间差就是 App 启动时间。
|
||||
|
||||
`mach_absolute_time` 是一个 CPU/总线依赖函数,返回一个基于系统启动后的时钟的“嘀嗒”数。系统休眠时不会增加。是一个纳秒级别的数字。获取前后2个纳秒后需要转换到秒。需要基于系统时间的基准,通过 `mach_timebase_info` 获得。
|
||||
|
||||
```Objective-c
|
||||
mach_timebase_info_data_t g_cmmStartupMonitorTimebaseInfoData = 0;
|
||||
mach_timebase_info(&g_cmmStartupMonitorTimebaseInfoData);
|
||||
uint64_t timelapse = mach_absolute_time() - g_cmmLoadTime;
|
||||
double timeSpan = (timelapse * g_cmmStartupMonitorTimebaseInfoData.numer) / (g_cmmStartupMonitorTimebaseInfoData.denom * 1e9);
|
||||
```
|
||||
|
||||
|
||||
#### 2. 线上监控启动时间就好,但是在开发阶段需要对启动时间做优化。
|
||||
|
||||
要优化启动时间,就先得知道在启动阶段到底做了什么事情,针对现状作出方案。
|
||||
|
||||
pre-main 阶段定义为 App 开始启动到系统调用 main 函数这个阶段;main 阶段定义为 main 函数入口到主 UI 框架的 viewDidAppear。
|
||||
|
||||
App 启动过程:
|
||||
- 解析 Info.plist:加载相关信息例如闪屏;沙盒建立、权限检查;
|
||||
- Mach-O 加载:如果是胖二进制文件,寻找合适当前 CPU 架构的部分;加载所有依赖的 Mach-O 文件(递归调用 Mach-O 加载的方法);定义内部、外部指针引用,例如字符串、函数等;加载分类中的方法;c++ 静态对象加载、调用 Objc 的 `+load()` 函数;执行声明为 __attribute_((constructor)) 的 c 函数;
|
||||
- 程序执行:调用 main();调用 UIApplicationMain();调用 applicationWillFinishLaunching();
|
||||
|
||||
Pre-Main 阶段
|
||||

|
||||
|
||||
Main 阶段
|
||||

|
||||
|
||||
|
||||
##### 2.1 加载 Dylib
|
||||
|
||||
每个动态库的加载,dyld 需要
|
||||
- 分析所依赖的动态库
|
||||
- 找到动态库的 Mach-O 文件
|
||||
- 打开文件
|
||||
- 验证文件
|
||||
- 在系统核心注册文件签名
|
||||
- 对动态库的每一个 segment 调用 mmap()
|
||||
|
||||
优化:
|
||||
- 减少非系统库的依赖
|
||||
- 使用静态库而不是动态库
|
||||
- 合并非系统动态库为一个动态库
|
||||
|
||||
##### 2.2 Rebase && Binding
|
||||
|
||||
优化:
|
||||
- 减少 Objc 类数量,减少 selector 数量,把未使用的类和函数都可以删掉
|
||||
- 减少 c++ 虚函数数量
|
||||
- 转而使用 Swift struct(本质就是减少符号的数量)
|
||||
|
||||
|
||||
##### 2.3 Initializers
|
||||
|
||||
优化:
|
||||
- 使用 `+initialize` 代替 `+load`
|
||||
- 不要使用过 attribute*((constructor)) 将方法显示标记为初始化器,而是让初始化方法调用时才执行。比如使用 dispatch_one、pthread_once() 或 std::once()。也就是第一次使用时才初始化,推迟了一部分工作耗时也尽量不要使用 c++ 的静态对象
|
||||
|
||||
|
||||
##### pre-main 阶段影响因素
|
||||
|
||||
- 动态库加载越多,启动越慢。
|
||||
- ObjC 类越多,函数越多,启动越慢。
|
||||
- 可执行文件越大启动越慢。
|
||||
- C 的 constructor 函数越多,启动越慢。
|
||||
- C++ 静态对象越多,启动越慢。
|
||||
- ObjC 的 +load 越多,启动越慢。
|
||||
|
||||
优化手段:
|
||||
- 减少依赖不必要的库,不管是动态库还是静态库;如果可以的话,把动态库改造成静态库;如果必须依赖动态库,则把多个非系统的动态库合并成一个动态库
|
||||
- 检查下 framework应当设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查
|
||||
- 合并或者删减一些OC类和函数。关于清理项目中没用到的类,使用工具AppCode代码检查功能,查到当前项目中没有用到的类(也可以用根据linkmap文件来分析,但是准确度不算很高)
|
||||
有一个叫做[FUI](https://github.com/dblock/fui)的开源项目能很好的分析出不再使用的类,准确率非常高,唯一的问题是它处理不了动态库和静态库里提供的类,也处理不了C++的类模板
|
||||
- 删减一些无用的静态变量
|
||||
- 删减没有被调用到或者已经废弃的方法
|
||||
- 将不必须在 +load 方法中做的事情延迟到 +initialize中,尽量不要用 C++ 虚函数(创建虚函数表有开销)
|
||||
- 类和方法名不要太长:iOS每个类和方法名都在 __cstring 段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的
|
||||
因还是 Object-c 的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用,Object-c 对象模型会把类/方法名字符串都保存下来;
|
||||
- 用 dispatch_once() 代替所有的 attribute((constructor)) 函数、C++ 静态对象初始化、ObjC 的 +load 函数;
|
||||
- 在设计师可接受的范围内压缩图片的大小,会有意外收获。
|
||||
压缩图片为什么能加快启动速度呢?因为启动的时候大大小小的图片加载个十来二十个是很正常的,
|
||||
图片小了,IO操作量就小了,启动当然就会快了,比较靠谱的压缩算法是 TinyPNG。
|
||||
|
||||
|
||||
##### main 阶段优化
|
||||
|
||||
- 减少启动初始化的流程。能懒加载就懒加载,能放后台初始化就放后台初始化,能延迟初始化的就延迟初始化,不要卡主线程的启动时间,已经下线的业务代码直接删除
|
||||
- 优化代码逻辑。去除一些非必要的逻辑和代码,减小每个流程所消耗的时间
|
||||
- 启动阶段使用多线程来进行初始化,把 CPU 性能发挥最大
|
||||
- 使用纯代码而不是 xib 或者 storyboard 来描述 UI,尤其是主 UI 框架,比如 TabBarController。因为 xib 和 storyboard 还是需要解析成代码来渲染页面,多了一步。
|
||||
|
||||
|
||||
|
||||
|
||||
## 三、 CPU 使用率监控
|
||||
|
||||
#### 1. CPU 架构
|
||||
|
||||
CPU(Central Processing Unit)中央处理器,市场上主流的架构有 ARM(arm64)、Intel(x86)、AMD 等。其中 Intel 使用 CISC(Complex Instruction Set Computer),ARM 使用 RISC(Reduced Instruction Set Computer)。区别在于**不同的 CPU 设计理念和方法**。
|
||||
|
||||
早期 CPU 全部是 CISC 架构,设计目的是**用最少的机器语言指令来完成所需的计算任务**。比如对于乘法运算,在 CISC 架构的 CPU 上。一条指令 `MUL ADDRA, ADDRB` 就可以将内存 ADDRA 和内存 ADDRB 中的数香乘,并将结果存储在 ADDRA 中。做的事情就是:将 ADDRA、ADDRB 中的数据读入到寄存器,相乘的结果写入到内存的操作依赖于 CPU 设计,所以** CISC 架构会增加 CPU 的复杂性和对 CPU 工艺的要求。**
|
||||
|
||||
RISC 架构要求软件来指定各个操作步骤。比如上面的乘法,指令实现为 `MOVE A, ADDRA; MOVE B, ADDRB; MUL A, B; STR ADDRA, A;`。这种架构可以降低 CPU 的复杂性以及允许在同样的工艺水平下生产出功能更加强大的 CPU,但是对于编译器的设计要求更高。
|
||||
|
||||
目前市场是大部分的 iPhone 都是基于 arm64 架构的。且 arm 架构能耗低。
|
||||
|
||||
|
||||
|
||||
#### 2. 获取线程信息
|
||||
|
||||
讲完了区别来讲下如何做 CPU 使用率的监控
|
||||
- 开启定时器,按照设定的周期不断执行下面的逻辑
|
||||
- 获取当前任务 task。从当前 task 中获取所有的线程信息(线程个数、线程数组)
|
||||
- 遍历所有的线程信息,判断是否有线程的 CPU 使用率超过设置的阈值
|
||||
- 假如有线程使用率超过阈值,则 dump 堆栈
|
||||
- 组装数据,上报数据
|
||||
|
||||
线程信息结构体
|
||||
```Objective-c
|
||||
struct thread_basic_info {
|
||||
time_value_t user_time; /* user run time(用户运行时长) */
|
||||
time_value_t system_time; /* system run time(系统运行时长) */
|
||||
integer_t cpu_usage; /* scaled cpu usage percentage(CPU使用率,上限1000) */
|
||||
policy_t policy; /* scheduling policy in effect(有效调度策略) */
|
||||
integer_t run_state; /* run state (运行状态,见下) */
|
||||
integer_t flags; /* various flags (各种各样的标记) */
|
||||
integer_t suspend_count; /* suspend count for thread(线程挂起次数) */
|
||||
integer_t sleep_time; /* number of seconds that thread
|
||||
* has been sleeping(休眠时间) */
|
||||
};
|
||||
```
|
||||
|
||||
代码在讲堆栈还原的时候讲过,忘记的看一下上面的分析
|
||||
```Objective-C
|
||||
thread_act_array_t threads;
|
||||
mach_msg_type_number_t threadCount = 0;
|
||||
const task_t thisTask = mach_task_self();
|
||||
kern_return_t kr = task_threads(thisTask, &threads, &threadCount);
|
||||
if (kr != KERN_SUCCESS) {
|
||||
return ;
|
||||
}
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
thread_info_data_t threadInfo;
|
||||
thread_basic_info_t threadBaseInfo;
|
||||
mach_msg_type_number_t threadInfoCount;
|
||||
|
||||
kern_return_t kr = thread_info((thread_inspect_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount);
|
||||
|
||||
if (kr == KERN_SUCCESS) {
|
||||
|
||||
threadBaseInfo = (thread_basic_info_t)threadInfo;
|
||||
// todo:条件判断,看不明白
|
||||
if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {
|
||||
integer_t cpuUsage = threadBaseInfo->cpu_usage / 10;
|
||||
if (cpuUsage > CPUMONITORRATE) {
|
||||
|
||||
NSMutableDictionary *CPUMetaDictionary = [NSMutableDictionary dictionary];
|
||||
NSData *CPUPayloadData = [NSData data];
|
||||
|
||||
NSString *backtraceOfAllThread = [BacktraceLogger backtraceOfAllThread];
|
||||
// 1. 组装卡顿的 Meta 信息
|
||||
CPUMetaDictionary[@"MONITOR_TYPE"] = CMMonitorCPUType;
|
||||
|
||||
// 2. 组装卡顿的 Payload 信息(一个JSON对象,对象的 Key 为约定好的 STACK_TRACE, value 为 base64 后的堆栈信息)
|
||||
NSData *CPUData = [SAFE_STRING(backtraceOfAllThread) dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSString *CPUDataBase64String = [CPUData base64EncodedStringWithOptions:0];
|
||||
NSDictionary *CPUPayloadDictionary = @{@"STACK_TRACE": SAFE_STRING(CPUDataBase64String)};
|
||||
|
||||
NSError *error;
|
||||
// NSJSONWritingOptions 参数一定要传0,因为服务端需要根据 \n 处理逻辑,传递 0 则生成的 json 串不带 \n
|
||||
NSData *parsedData = [NSJSONSerialization dataWithJSONObject:CPUPayloadDictionary options:0 error:&error];
|
||||
if (error) {
|
||||
CMMLog(@"%@", error);
|
||||
return;
|
||||
}
|
||||
CPUPayloadData = [parsedData copy];
|
||||
|
||||
// 3. 数据上报会在 [打造功能强大、灵活可配置的数据上报组件](./1.80.md) 讲
|
||||
[[PrismClient sharedInstance] sendWithType:CMMonitorCPUType meta:CPUMetaDictionary payload:CPUPayloadData];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## 四、App 占有内存
|
||||
|
||||
#### 1. 基础知识回顾
|
||||
硬盘:也叫做磁盘,用于存储数。你存储的歌曲、图片、视频都是在硬盘里。
|
||||
|
||||
内存:由于硬盘读取速度较慢,如果 CPU 运行程序期间,所有的数据都直接从硬盘中读取,则非常影响效率。所以 CPU 会将程序运行所需要的数据从硬盘中读取到内存中。然后 CPU 与内存中的数据进行计算、交换。内存是易失性存储器(断电后,数据消失)。内存条区是计算机内部(在主板上)的一些存储器,用来保存 CPU 运算的中间数据和结果。内存条是程序与 CPU 之间的桥梁。从硬盘读取出数据或者运行程序提供给 CPU。
|
||||
|
||||
|
||||
|
||||
**虚拟内存** 是计算机系统内存管理的一种技术。它使得程序认为它拥有连续的可用内存,而实际上,它通常被分割成多个物理内存碎片,还是部分暂时存储在外部磁盘(硬盘)存储器上(当需要使用时则用硬盘中数据交换到内存中)。Windows 系统中称为 “虚拟内存”,Linux/Unix 系统中称为 ”交换空间“。
|
||||
|
||||
内存(RAM)与 CPU 一样都是系统中最稀少的资源,也很容易发生竞争,应用内存与性能直接相关。iOS 没有交换空间作为备选资源,所以内存资源尤为重要。
|
||||
|
||||
内存过大的几种情况
|
||||
|
||||
- App 内存消耗较低,同时其他 App 内存管理也很棒,那么即使切换到其他 App,我们自己的 App 依旧是“活着”的,保留了用户状态。体验好
|
||||
- App 内存消耗较低,但其他 App 内存消耗太大(可能是内存管理糟糕,也可能是本身就耗费资源,比如游戏),那么除了在前台的线程,其他 App 都会被系统杀死,回收内存资源,用来给活跃的进程提供内存。
|
||||
- App 内存消耗较大,切换到其他 App 后,即使其他 App 向系统申请的内存不大,系统也会因为内存资源紧张,优先把内存消耗大的 App 杀死。表现为用户将 App 退出到后台,过会儿再次打开会发现 App 重新加载启动。
|
||||
- App 内存消耗非常大,在前台运行时就被系统杀死,造成闪退。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### 2. 获取内存信息
|
||||
|
||||
// todo:APM 下 CPU 可以分析什么?什么场景?参数还是单独
|
||||
App 内存信息的 API 可以在 Mach 层找到,`mach_task_basic_info` 结构体存储了 Mach task 的内存使用信息,其中 resident_size 就是应用使用的物理内存大小,virtual_size 是虚拟内存大小。
|
||||
|
||||
```Objective-c
|
||||
#define MACH_TASK_BASIC_INFO 20 /* always 64-bit basic info */
|
||||
struct mach_task_basic_info {
|
||||
mach_vm_size_t virtual_size; /* virtual memory size (bytes) */
|
||||
mach_vm_size_t resident_size; /* resident memory size (bytes) */
|
||||
mach_vm_size_t resident_size_max; /* maximum resident memory size (bytes) */
|
||||
time_value_t user_time; /* total user run time for
|
||||
terminated threads */
|
||||
time_value_t system_time; /* total system run time for
|
||||
terminated threads */
|
||||
policy_t policy; /* default policy for new threads */
|
||||
integer_t suspend_count; /* suspend count for task */
|
||||
};
|
||||
```
|
||||
所以获取代码为
|
||||
```Objective-c
|
||||
task_vm_info_data_t vmInfo;
|
||||
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
|
||||
kern_return_t kr = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count);
|
||||
|
||||
if (kr != KERN_SUCCESS) {
|
||||
return ;
|
||||
}
|
||||
CGFloat memoryUsed = (CGFloat)(vmInfo.phys_footprint/1024.0/1024.0);
|
||||
```
|
||||
|
||||
可能有人好奇不应该是 `resident_size` 这个字段获取内存的使用情况吗?一开始测试后发现 resident_size 和 Xcode 测量结果差距较大。而使用 phys_footprint 则接近于 Xcode 给出的结果。且可以从 [WebKit 源码](https://github.com/WebKit/webkit/blob/52bc6f0a96a062cb0eb76e9a81497183dc87c268/Source/WTF/wtf/cocoa/MemoryFootprintCocoa.cpp)中得到印证。
|
||||
|
||||
基于近期的发现,可以在线下获取 App 的 high water mark,也就是 oom 内存阈值。 那么就产生了方案3
|
||||
|
||||
所以对于内存的监控思路就是找到系统给 App 的内存上限,然后当接近内存上限值的时候,dump 内存情况,组装基础数据信息成一个合格的上报数据,经过一定的数据上报策略到服务端,服务端消费数据,分析产生报表,客户端工程师根据报表分析问题。不同工程的数据以邮件、短信、企业微信等形式通知到该项目的 owner、开发者。(情况严重的会直接电话给开发者,并给主管跟进每一步的处理结果)。
|
||||
问题分析处理后要么发布新版本,要么 hot fix。
|
||||
|
||||
监控内存增长,在达到 high water mark 附近的时候,dump 内存信息,获取对象名称、对象个数、各对象的内存值;如果稳定可以全量开启,不会有性能问题
|
||||
OOMDetector 可以拿到分配内存的堆栈,对于定位到代码层面更加有效;可以灰度开放
|
||||
|
||||
|
||||
|
||||
## 五、 App 网络监控
|
||||
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [iOS 保持界面流畅的技巧](https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/)
|
||||
- [Call Stack](https://en.wikipedia.org/wiki/Call_stack)
|
||||
- [关于函数调用栈(call stack)的个人理解](https://blog.csdn.net/VarusK/article/details/83031643)
|
||||
- [获取任意线程调用栈的那些事](https://bestswifter.com/callstack/)
|
||||
- [iOS启动时间优化](https://www.zoomfeng.com/blog/launch-time.html)
|
||||
- [WWDC2019之启动时间与Dyld3](https://www.zoomfeng.com/blog/launch-optimize-from-wwdc2019.html)
|
||||
|
||||
29
Chapter1 - iOS/1.75.md
Normal file
29
Chapter1 - iOS/1.75.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 如何写好测试
|
||||
|
||||
> 软件测试的功能非常重要,现在结合过往经验谈谈如何做软件测试
|
||||
|
||||
- 每个类具有什么属性、具有什么方法都是确定的,所以拿 Objective-C 举例子,`.h` 中会有公有的属性以及方法,`.m` 中一般是私有属性和私有方法、公有方法。类的行为设计为方法,写代码之前我们一般需要做到心中有数,一个功能需要几个类、每个类的属性和方法分别是什么,需要暴露什么属性和接口。**UML图** 不是必须要画,但是需要胸有成竹。针对一个类在测试的时候 `.m` 文件中的私有方法没有办法在 Unit Test 类中访问,一般可以将需要测试的类增加分类。在分类的 `.h` 文件中将方法进行声明。在测试的 Uint Test 中引入分类头文件。下面举例子
|
||||
|
||||
```Objective-C
|
||||
// Person.h
|
||||
|
||||
- (void)eat;
|
||||
|
||||
// Person.m
|
||||
|
||||
- (void)eat
|
||||
{
|
||||
NSLog(@"eat");
|
||||
}
|
||||
|
||||
- (vood)sleep
|
||||
{
|
||||
NSLog(@"sleep");
|
||||
}
|
||||
|
||||
// Person+UnitTestHelper.h
|
||||
|
||||
- (void)sleep;
|
||||
```
|
||||
|
||||
另一种思路是没必要针对每个类的私有方法或者每个方法进行测试,因为等全部功能做完后针对每个类的接口测试,一般会覆盖据大多数的方法。等测试完看如果方法未被覆盖,则针对性的补充 Unit Test
|
||||
37
Chapter1 - iOS/1.76.md
Normal file
37
Chapter1 - iOS/1.76.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# iOS Crash分析
|
||||
|
||||
> 目前在重构 APM,在做测试的时候有这样一个需求,分析线上环境的 top10 Crash, APM SDK 的提测 Demo 工程中需要有模拟能力并全链路分析是否可以在 Demo 工程中生成 Crash -> 上报组件上报 -> 服务端符号化. 在查询目前 App 的 top 10 Crash 的时候发现系统层面的 Crash 还是无法符号化成功. 虽然这不重要,但是还是想探讨下如何可以解析这种系统层面的 Crash 信息.
|
||||
|
||||
|
||||
## 背景
|
||||
|
||||
APM 监控到 Crash,然后线程回溯拿到堆栈信息,再调用上报组件的能力,上报组件按照一定的策略去上报数据,服务端符号化解析堆栈信息.为了方便查看拿 stack_trace_id 去查询堆栈信息.接口信息如下图
|
||||
|
||||

|
||||
|
||||
拿到堆栈信息里面的 json 本地保存成拓展名为 ***.crash** 文件,Mac 可以打开拓展名为 crash 的文件. 然后根据 **Crashed Thread** 后面的数字去查找对应的 Thread 里面的信息
|
||||
|
||||

|
||||
|
||||
结果发现是系统层级的信息,看不懂
|
||||
|
||||
```Shell
|
||||
Thread 12 Crashed:
|
||||
0 libsystem_platform.dylib 0x00000001c34a87e8 0x1c34a2000 + 26600 (<redacted> + 16)
|
||||
1 Foundation 0x00000001c42fe698 0x1c41ea000 + 1132184 (<redacted> + 52)
|
||||
2 Foundation 0x00000001c42091fc 0x1c41ea000 + 127484 (<redacted> + 1744)
|
||||
3 Foundation 0x00000001c4208af4 0x1c41ea000 + 125684 (<redacted> + 1232)
|
||||
4 Foundation 0x00000001c42fecec 0x1c41ea000 + 1133804 (<redacted> + 272)
|
||||
5 libdispatch.dylib 0x00000001c32d8a38 0x1c3279000 + 391736 (<redacted> + 24)
|
||||
6 libdispatch.dylib 0x00000001c32d97d4 0x1c3279000 + 395220 (<redacted> + 16)
|
||||
7 libdispatch.dylib 0x00000001c32b0c34 0x1c3279000 + 228404 (<redacted> + 404)
|
||||
8 libdispatch.dylib 0x00000001c32b0314 0x1c3279000 + 226068 (<redacted> + 592)
|
||||
9 libdispatch.dylib 0x00000001c32bc9d4 0x1c3279000 + 276948 (<redacted> + 340)
|
||||
10 libdispatch.dylib 0x00000001c32bd248 0x1c3279000 + 279112 (<redacted> + 116)
|
||||
11 libsystem_pthread.dylib 0x00000001c34b91b4 0x1c34ad000 + 49588 (_pthread_wqthread + 464)
|
||||
```
|
||||
|
||||
为了搞懂这种 Crash,这篇文章就诞生了.
|
||||
|
||||
|
||||
##
|
||||
264
Chapter1 - iOS/1.77.md
Normal file
264
Chapter1 - iOS/1.77.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# iOS 打包系统构建加速
|
||||
|
||||
|
||||
|
||||
### 目标
|
||||
|
||||
> iOS 单包构建加速、支持多包并行打包
|
||||
|
||||
|
||||
|
||||
### 基础知识
|
||||
|
||||
CI、CD 在稍微有点规模的公司内部都会内建一套自己的系统。目前主流的是在 Jenkins 的基础上进行的打包系统。公司只有1个 App 的情况下一台打包机就够了,但是有多个 SDK、App 那肯定不够的,各个业务线都需要测试、上架等等,任务太多了,一台机器别人要等到花儿谢了...
|
||||
|
||||
分布式构建系统可解决上述问题,即一个 master 为中心,多个 slave 来进行具体的构建操作。多台执行机来进行任务的构建以及自动化脚本的执行。Jenkins 具备分布式特性,是 Master/Slave 模式(主从模式,将设备分为主设备和从设备,主设备负责分配工作并整合结果,或作为指令的来源;从设备负责完成任务,从设备一般只和主设备通信)。这个模式有2个好处:
|
||||
|
||||
- 能够有效分担主节点的压力,加快构建速度
|
||||
- 能够指定特定的任务在特定的主机上进行
|
||||
|
||||
|
||||
|
||||
## 背景
|
||||
|
||||

|
||||
|
||||
- 描述现状
|
||||
|
||||
我们公司的 WAES 平台下子平台 candle 是专门用来打包构建的,可以打包 iOS SDK、iOS App、Android SDK、Android App、React Native 包、H5、Node 包、React 包等等。iOS 端将 SDK、App 通过 candle 打包平台进行任务创建、排队、打包,根据任务的特点和语言去调度合适的打包机器进行打包。 现状是整体速度觉得较慢,还有加速空间。
|
||||
|
||||
- 问题原因
|
||||
|
||||
1. iOS 打包目前都是在使用旧的打包构建系统,所以在单包构建方面会慢一些;
|
||||
2. 在 pod install 这一步,打包机使用的 1.3.1 版本的 cocoapods 版本在进行依赖分析,它本质上是操纵打包机上全局的 git 目录,由于本质如此所以没办法多包并行打包。
|
||||
|
||||
- 风险预警
|
||||
|
||||
1. cocoapods 升级到最新版本,目前的 cocoapods 的相关的 ruby 脚本可能会有问题,没办法良好运行。
|
||||
2. 开启 Xcode 的新构建系统,可能会造成现有工程报错,没办法编译成功,需要改动。这些改动可能不只是主工程,也许是各个 SDK 自身的修改,所以会比较零散。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 改造
|
||||
|
||||
#### 单包加速
|
||||
|
||||
|
||||
|
||||
##### 一些背景:
|
||||
|
||||
1. 打包机暂时不支持各个依赖的 SDK 以静态库的方式引入。
|
||||
|
||||
原因是目前 APM 监控系统不支持。因为多个静态库则会生成多个 DYSM 文件,这样子 APM 定位 Crash、ANR 等都会生成多个 DYSM 文件的信息,配套的后端在符号化处理的时候只支持一个 DYSM 的模式,所以在如此背景下打包机不支持静态库。
|
||||
|
||||
2. 看到博客说可以通过 `generate_multiple_pod_projects` 和 `disable_input_output_paths` 来加速构建速度,这些在本地开发过程中是可以提高构建速度的。但是在打包机这种环境下是不太适用的。因为 iOS 在打包机环境下都会执行 pod install 的过程.
|
||||
```ruby
|
||||
install! 'cocoapods', :generate_multiple_pod_projects => true, :incremental_installation => true, :disable_input_output_paths => true
|
||||
```
|
||||
- generate_multiple_pod_projects
|
||||
生成多个 XcodeProj。在 1.7.0 之前,cocoapods 只生成一个 Pod.xcodeproj,随着 Library 增多,文件越来越大,解析时间越来越长,在 1.7.0 之后每个 Library 都允许生成单独的 Project,提高项目的编译时间。 默认关闭
|
||||
- incremental_installation
|
||||
增量安装,每次执行 pod install 都会生成整个 workspace,现在支持只更新的 Library 编译。节省时间
|
||||
|
||||
3. 另外网上的部分优化提速手段也不太适合,因为这些手段基本上只是会加快一些速度,但是不可能把一个项目的构建速度提升明显,所以这次的方案主要是单包开启 New Build System 和支持多包并行能力。
|
||||
|
||||
|
||||
|
||||
##### 理论基础
|
||||
|
||||
本质上就是开启 New Build System,苹果在 WWDC 2017 中描述新构建系统的有点为:**降低构建开销,尤其可以降低大型项目的构建开销**。但是在新构建系统下现有的工程会报错。经过查看报错信息,基本都是在资源方面的错误(图片等)和偶尔一些 SDK 不规范造成的问题。
|
||||
|
||||
苹果从 Xcode 9 开始推出了新构建系统(New Build System),并在 Xcode 10 使用其为默认构建系统来替代旧构建系统(Legacy Build System)。采用新构建系统能够减少构建时间。
|
||||
|
||||
简要介绍一下原理,对于旧构建系统,当我们构建一个程序的时候,会明确所需要构建的所有 Target Dependencies、Link Binary With Libraries,这些 Target 之间的依赖关系,以及这些 Target 构建的顺序。采用顺序会造成多处理器系统资源的浪费,从而表现为编译时间的浪费,解决这个问题的方式就是采用并行编译,这也是新构建系统优化的核心思想。详细了解新构建系统,探究 Xcode New Build System 对于构建速度的提升。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
##### 测试实验
|
||||
|
||||
|
||||
|
||||
注意: 报错提示找不到 `coderay`. 可以运行 `sudo gem install coderay` 解决该问题。
|
||||
|
||||
本地 cocoapods 版本为 1.4.0,打包机环境为 1.3.1,所以方案评估有问题。这几天花时间做了对比实验,数据如下
|
||||
|
||||
1. New Build System 是否可以让单包构建变快?
|
||||
cocoapods 模拟打包机环境 1.3.1。在 Legacy Build System 和 New Build System 下运行项目。
|
||||
1.3.1 不能开启 New Build System。报错信息: New Build System Multiple commands produce script phase “[CP] Copy Pods Resources”
|
||||
1.3.1 Legacy Build System 构建时间为 335.4s.
|
||||
所以尝试升级 cocoapods 继续做对比实验
|
||||
|
||||
2. cocoapods 小版本升级到 1.4.0 在 Legacy Build System 和 New Build System 下运行项目(为什么选择升级到 1.4.0? cocoapods 小版本升级则改动较小,业务线可以快速享受到 New Build System 改动带来的收益)
|
||||
New Build System: 383.5s
|
||||
Legacy Build System: 302.9s
|
||||
|
||||
3. 升级 cocoapods 到 1.8.0,查看在 New Build System 和 Legacy Build System 下的构建时间
|
||||
cocoapods 升级到 1.8.0 会报错,修改错误后运行对比。
|
||||
New Build System: 324.4s
|
||||
Legacy Build System: 262.2s
|
||||
|
||||
|
||||
|
||||
实验数据如下:
|
||||
|
||||
| App| 构建系统 | Cocoapods 版本 | Build 结果 | 编译时间 |
|
||||
|:-:|:-:|:-:|:-:|:-:|
|
||||
| **App | New Build System |1.3.1 | 失败 | ~ |
|
||||
| **App | Legacy Build System |1.3.1| 成功| 335.4s |
|
||||
| **App | New Build System | 1.4.0 | 失败 | 383.5s |
|
||||
| **App | Legacy Build System |1.4.0 | 成功 | 302.9s |
|
||||
| **App | New Build System | 1.8.4 |成功 | 324.4s|
|
||||
| **App | Legacy Build System | 1.8.4 | 成功 | 262.2s |
|
||||
|
||||
|
||||
结论:从实验数据来看, New Build System 并不能单包加速。所以 New Build System 不做了。构建加速是升级cocoapods 1.8.4 带来的,并不是 new build system 带来的。后续计划分2步:
|
||||
1. 升级 cocoapods 到 1.8.4,可以体验到单包构建加速的效果。
|
||||
2. 自建 CDN。
|
||||
|
||||
拿自己的电脑部署脚本,当作本地打包机;拿**App App 打包,指定打包机为自己的电脑
|
||||
| App| Cocoapods 版本 | 编译时间 |
|
||||
|:-:|:-:|:-:|
|
||||
| **App | 1.3.1 | 8m37s |
|
||||
| **App | 1.8.4 | 7min47s |
|
||||
|
||||
|
||||
|
||||
|
||||
开启 New Build System 带来的改动
|
||||
|
||||
1. SDK 中图片是通过 resource 的方式管理的,cocoapods 1.8.4 会将它打包到 `Assets.car` 和 App 主工程图片打包的结果一致,导致 Xcode 主工程报错,大体意思是说工程包含多个 **Assets.car**. 原因在于 SDK 通过 resource 管理图片,打出包所以可以使用 ``resource_bundles`` 的形式管理
|
||||
- 涉及到的 SDK:TrinityConfiguration(公共 SDK)
|
||||
- 改造点:
|
||||
注意:改变 SDK 内部修改 xcassets 文件名是无用的,Xcode 编译后查看包内容,结果还是 `Assests.car` 。
|
||||
图片使用方式改变。由之前的 **resources** 方式改为 **`resource_bundles`** 的形式。这样做有2个优点:解决了图片资源打包后造成 `Assets.car` 冲突的问题;`resource_bundles` 还可以解决图片访问速度的优化。
|
||||
2. 图片资源重复
|
||||
**App工程中有些图片和 理财的 SDK `SdkFinanceHome` 里面的图片资源重名,但是内容却不一致,需要协商改动。
|
||||
|
||||
- 涉及到的 SDK:SdkFinanceHome (理财业务线 SDK)
|
||||
- 改造点:
|
||||
有2张图片在 SdkFinanceHome SDK 内重复出现2次(形状、大小一致)。App 也存在同名的图片,图形一致、尺寸大小不一致。所以需要**App业务线开发者确认,保留什么图片或者资源重命名。建议图片资源也用 **`resource_bundles`** 的形式管理
|
||||
```ruby
|
||||
s.resource_bundles = {
|
||||
'SdkFinanceHome' => ['***/Assets/*.xcassets']
|
||||
}
|
||||
```
|
||||
|
||||
升级 1.8.4 带来的改动点:
|
||||
|
||||
1. 部分 SDK 的头文件引用方式有问题
|
||||
- 涉及到的 SDK:SdkFundWax
|
||||
- 改造点:将 `FCH5AuthRouter.m` 文件中关于 NativeQS 中头文件的引入方式改变下。`#import <NQSParser.h>` 改为 `#import <NativeQS/NativeQS.h>`,或者改为 `#import "NQSParser.h"`
|
||||
```ruby
|
||||
"dependencies": {
|
||||
"NativeQS": "~> 1.0"
|
||||
},
|
||||
```
|
||||
|
||||
注意:
|
||||
因为打包机目前是源码引入编译成 .a 文件。如果是以 framework 的形式,则必须以依赖描述的方式进行调整。
|
||||
|
||||
新版本 cocoapods 中:
|
||||
|
||||
- cocoapods 在 SDK 里面引用别的 SDK 如果 **podspec** 里面存在 **dependencies** 描述,则可以使用 `#import <NativeQS/NativeQS.h>` 或者 `#import "NativeQS.h"`;如果不存在 **dependencies** 描述,则需要使用 `#import <NativeQS/NativeQS.h>`
|
||||
- 在主工程中引用 SDK 的头文件,使用 `#import <NativeQS/NativeQS.h>`、`#import "NativeQS.h"` 都可以
|
||||
|
||||
旧版本 cocoapods 中:
|
||||
|
||||
- SDK、App 主工程都可以使用 `#import <NativeQS/NativeQS.h>`、`#import "NativeQS.h"`、`#import <NativeQS.h>`
|
||||
2. 部分 SDK 的使用了未在 podspec 文件中声明的依赖,在新版本 cocoapods 下会报错(某些 SDK 由于历史原因造成新版本丢失依赖描述)
|
||||
- 涉及到的 SDK:CMRCTToast
|
||||
- 改造点:
|
||||
问题基本定位是在于, App 主工程引用的 SdkBbs2 SDK 依赖了 SdkBbs2 版本(该版本的依赖描述为 `CMRCTToast (~> 0.1)` )
|
||||
历史原因: 早期在做 RN SDK 封装的时候在第一个版本的时候只有某个版本的 React Native 库,所以在 `0.1.0` 的时候依赖的描述可以看到如下的代码
|
||||
|
||||
```ruby
|
||||
s.dependency 'React/Core', '0.41.2'
|
||||
s.dependency 'React/RCTNetwork', '0.41.2'
|
||||
s.dependency 'React/RCTImage', '0.41.2'
|
||||
s.dependency 'React/RCTText', '0.41.2'
|
||||
s.dependency 'React/RCTWebSocket', '0.41.2'
|
||||
s.dependency 'React/RCTAnimation', '0.41.2'
|
||||
```
|
||||
随着版本的不断迭代,在第二个版本 `0.1.1` 的时候可以看到下面的描述. 可以看到对 RN 的描述不存在了,因为当时的代码对 RN 的2个版本都做了兼容,所以 App 主工程肯定是有 RN 的库,所以索性就不在单独描述,直接随着 App 依赖的 RN 库而使用。之后的版本也是如此。
|
||||
```ruby
|
||||
s.dependency 'CMDevice', '~> 0.1'
|
||||
```
|
||||
所以, 将 `CMRCTToast.podspec` 中的依赖修改掉。需要兼容不同 RN SDK 的版本。
|
||||
3. 部分 pod 的 hook 脚本会失败。
|
||||
- 涉及到的 SDK:无
|
||||
- 改造点:
|
||||
`TrinityParams.rb` 类方法 `generate_mods` 会报错。逻辑是通过遍历每个 pod_target,获取到 PBXNativeTarget,然后访问 source_build_phase 属性去遍历内部的每个文件,判断是否是 `properties.yml`。
|
||||
1. [官方文档地址](https://rubydoc.info/gems/xcodeproj/Xcodeproj/Project/Object/PBXNativeTarget#source_build_phase-instance_method).
|
||||
2. 公共组做的库,Android 和 iOS 都是对应的,但是 SDK 的名字不一定严格一致。但是通跳后台是配置的时候不可能设置多个名字,所以设置了一个通用的名字,然后 iOS SDK 和 Android SDK 各自用一个描述文件将本地的 SDK 和下发需要命中的 SDK 名字做一个对映射关系。iOS 端用 `properties.yml`来描述
|
||||
cocoapods 新版本里面每个 pod_target 没有 native_target 属性,也就是没办法获取到 PBXNativeTarget。
|
||||
感觉之前的脚本写法有问题,内部基既有 file_accessors,也有 pod_target.native_target 的形式继续访问 yml 文件。所以升级 cocoapods 1.8.4 之后修改脚本直接改为用 file_accessors 寻找 yml
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### 多包加速
|
||||
|
||||
目前不能开启多包并行的瓶颈在于打包机操作的是本地下载下来的 `.cocoapods` 文件夹,所以当一个项目操作的时候其他项目没办法操作。
|
||||
|
||||
CDN 提供了通过网络接口处理依赖的能力,通过网络去操作文件,所以是可以多包并行打包的。
|
||||
|
||||
但是由于以下2个原因,我们需要自建``` CDN``` 的能力:
|
||||
|
||||
1. 我们的依赖存在的位置有2个,1个是私有源、1个是官方源。目前使用官方``` CDN``` 就是官方源,因为我们要并行打包,所以私有源也需要 `CDN` 化。不然难以免于多个项目进行文件的读写锁操作问题。
|
||||
2. 但是``` CDN``` 跟网络的状态有关,依据所处位置附近的服务器有关系,严重依赖于外界因素,不可控。所以想拥有快速稳定的`` CDN`` 查询能力就需要自建`` CDN`` 了。
|
||||
|
||||
另外一个可预期的点就是自建了` CDN` ,wax SDK 发布的相关逻辑也需要修改。
|
||||
|
||||
根据 Cocoapods 的 changeLog 知道 CDN 的实现是借助 **Netlify** 实现的。所以接下去的研究方向就是如何利用 Netlify 自建 CDN。
|
||||
|
||||
> ### Directory Listing Denied
|
||||
>
|
||||
> It was obvious to many that the spec repo should be put behind a CDN, but there were several constraints:
|
||||
>
|
||||
> 1. It had to be a free CDN, as the project is free and open-source.
|
||||
> 2. It had to allow some way of obtaining directory listings, for retrieving versions of pods.
|
||||
> 3. It had to auto-update from GitHub as the source of truth.
|
||||
>
|
||||
> The [first implementation](https://github.com/CocoaPods/Core/pull/469) was a shell script, polling GitHub and piping `find` into `ls` into index files. This ran on a machine that was not open or free and therefore could not be the true solution. Nevertheless, this auto-updated repo was put behind a [jsDelivr CDN](https://www.jsdelivr.com/) and the client interfacing with it was released in [1.7.0](http://blog.cocoapods.org/CocoaPods-1.7.0-beta#cdn-support) labeled "highly experimental".
|
||||
>
|
||||
> ### Final Lap with Netlify
|
||||
>
|
||||
> The [final version](https://github.com/CocoaPods/Core/pull/541) of the CDN for CocoaPods/Specs was implemented on [Netlify](https://www.netlify.com/), a static site hosting service supporting flexible site generation. This solution ticked all the boxes: a generous open-source plan, fast CDN and continuous deployment from GitHub.
|
||||
>
|
||||
> Upon each commit, Netlify runs a [specialized script](https://github.com/CocoaPods/Specs/tree/master/Scripts) which generates a per-shard index for all the pods and versions in the repo. If you've ever noticed that the directory structure for our Podspecs repo was strange, this is what we call sharding. An example of a shard index can be found at https://cdn.cocoapods.org/all_pods_versions_2_2_2.txt. This would correspond to `~/.cocoapods/repos/master/Specs/2/2/2/` locally.
|
||||
>
|
||||
> Additionally, we create an `all_pods.txt` file which contains a list of all pods.
|
||||
>
|
||||
> Finally, any other request made is redirected to GitHub's CDN.
|
||||
|
||||
###
|
||||
|
||||
#### 接入方式
|
||||
|
||||
考虑到业务线 App 升级是分开的,不可能同步进行,所以需要考虑到接入计划。
|
||||
|
||||
- 能否提供 wax 项目指定到特定环境打包机的能力(该打包机升级了 cocoapods 版本)
|
||||
- 假如没有上述能力,则考虑其他方式支持业务线自定义打包所需的 cocoapods 版本
|
||||
- 将 2个版本的 cocoapods 做成2个 Bundle 包,读取 wax 工程配置,指定某个 Bundle
|
||||
- 假如打包机由于某些原因没办法升级 cocoapods 版本,但是某个 wax 项目又需要新版的 cocoapods 进行打包,则需要则代码上传的时候提交 `Pods` 文件夹。这样在打包机上面不需要执行 install 的操作,将本地的 Pods 目录上传上来,全部使用本地的一套。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
[1. cocoapods changeLog](http://blog.cocoapods.org/CocoaPods-1.7.2/)
|
||||
|
||||
[2. 版本清单](https://github.com/CocoaPods/Specs/tree/master/Scripts)
|
||||
|
||||
[3.探究Xcode New Build System对于构建速度的提升](https://blog.csdn.net/TuGeLe/article/details/84885211)
|
||||
|
||||
342
Chapter1 - iOS/1.78.md
Normal file
342
Chapter1 - iOS/1.78.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# 上架包预检
|
||||
|
||||
## 常见被拒原因汇总
|
||||
|
||||
### iOS
|
||||
|
||||
钱伴
|
||||
|
||||
敏感词:审核、热更新、开关
|
||||
1. App 内包含分发下载分发功能(比如贷款超市、引导用户下载 App 等功能)。
|
||||
2. 提供的测试账号无法查看贷款实际利率。
|
||||
3. notification 接口返回:showupgrade:true,提示用户升级。 审核期间接口不要返回该字段。
|
||||
4. 审核账号,任何时候在任何ip登录看到的都是审核版。
|
||||
5. 审核的时候屏蔽 getwarehouseinfo 接口(tab接口)。审核期间 getwarehouseinfo 接口不返回数据。读取本地的配置
|
||||
6. upgrade:签到不能展示。 方案:把现在提额页copy一份,通过审核开关控制展示哪个页面。隐藏活动入口
|
||||
7. getSdkConfig 屏蔽接口返回内容。审核阶段接口返回空
|
||||
8. switch字样接口
|
||||
9. review字样接口
|
||||
|
||||
基金:
|
||||
没有发版问题
|
||||
|
||||
|
||||
挖财宝:
|
||||
1. App 内部某个控件点击后跳转到了信用卡的 H5 页面,可以引导用户线下办卡
|
||||
2. 提供的登陆账号和密码不对,登陆不上
|
||||
3. 运营填写的营销关键字有问题
|
||||
4. 元数据问题,iPhoneX 截图中 iPhone 壳子是 iPhone7 的,应该是 iPhoneX
|
||||
5. 说明隐私权限的作用。
|
||||
|
||||
闪电公积金:
|
||||
1. 苹果看到“代缴公积金”,这个需要有政府机构资质,此类功能在审核期间都关闭。
|
||||
|
||||
|
||||
社保:
|
||||
1. 修改隐私权限相关的文案,做到让审核人员看得懂
|
||||
|
||||
|
||||
记账:
|
||||
1. Guidelines 3.2.2 审核期间,隐藏邀请好友、天天挖宝相关的运营活动
|
||||
|
||||
|
||||
信用保镖:
|
||||
1. 狗年大礼包: 修改元数据后申诉;更换销售地区后提审。
|
||||
|
||||
|
||||
### Android
|
||||
|
||||
信贷超人:
|
||||
1. App 无法登陆进去,属于 bug 级别
|
||||
2. App 没有适配 ipad。
|
||||
3. Privacy - Data Collection and Storage,说明 App 没有做隐私权限的收集。
|
||||
4. 访问 h5 页面出现问题。 属于 bug 级别
|
||||
|
||||
微记账:
|
||||
暂无
|
||||
|
||||
|
||||
挖财宝:
|
||||
|
||||
1. 原因:挖财宝集成了设备指纹SDK, 会上传用户设备安装应用列表,所以被拒. 解决:移除设备指纹SDK, 成功上架
|
||||
2. 原因:挖财宝中,存在更多App按钮,会导向应用宝,所以被华为拒过,其他应用市场没有拒过. 解决:移除更多App按钮,无再被华为拒过。
|
||||
|
||||
|
||||
基金:
|
||||
暂未遇到审核被拒的情况。
|
||||
|
||||
快贷:
|
||||
暂未遇到审核被拒的情况。
|
||||
|
||||
钱伴:
|
||||
暂未遇到审核被拒的情况。
|
||||
|
||||
信用卡:
|
||||
|
||||
1. 经检测你的应用targetsdk版本低于26 ,请修改。
|
||||
2. 你的应用未加固
|
||||
3. 你提交的应用被检测到存在不合理获取短信/通话记录相关权限的行为,请勿获取SEND_SMS、READ_SMS、READ_CALL_LOG、RECEIVE_SMS权限。
|
||||
4. 你的应用注册登录界面的《用户协议》内容空白,请添加。
|
||||
5. 你的应用注册登录时无法接收验证码,注册登录时需输入图形验证码,且无图形参考,请定位修复。
|
||||
6. 在你的应用内任选一款借贷产品注册点击下载应用后直接进入第三方下载渠道,请修改为华为应用市场下载链接或者直接删除应用分发模块。
|
||||
7. 你的应用版权未通过审核,请提供a.《计算机软件著作权证书》或《APP电子版权证书》; b. ICP备案;c.《免责函》;d.你提供的合作协议已到期,请重新提供至少三家贷款产品合作单位的金融监管部门备案登记文件、授权书。e.请提供《金融许可证》或对应银行信用卡中心授权证明(至少三家)。免责函模板下载链接,https://url.cloud.huawei.com/1vbaVckwPC
|
||||
8. 你的应用截图与实际内容不相符,请修改。如有疑问,请发邮件至developer@huawei.com。
|
||||
9. 使用了动态更新的能力。回复苹果,问具体使用的地方,并贴出代码。
|
||||
|
||||
|
||||
|
||||
只能根据政策去做,比如当前的隐私权权限问题就是政策
|
||||
还有一些如理财、贷款类,没有资质就无法过审,这个也是政策。
|
||||
|
||||
解决方案:
|
||||
|
||||
苹果那边是把接口字段定义的隐晦一点。之前是定义了一个颜色值作为审核的开关
|
||||
如果是对应的颜色,就开启相应功能。
|
||||
|
||||
|
||||
|
||||
|
||||
## 原因汇总
|
||||
|
||||
从 Android 和 iOS 2端 App 被驳回的一些信息来看,驳回原因一般划分为下面几类:
|
||||
|
||||
1. 审核期间,资源和配置都应该调节为审核模式
|
||||
2. App 包含某些关键字
|
||||
3. 审核相关的元数据问题(截图与实际内容不匹配、机型和截图不匹配、提供给审核的账号和密码登陆不上)
|
||||
4. 使用的隐私权限必须说明,文案描述必须清晰
|
||||
5. App 存在 bug (账号无法登陆、没有适配 ipad、访问 h5 打不开 )
|
||||
6. 诱导用户打开查看更多 App
|
||||
7. Android 应用未加固
|
||||
8. 应用缺乏相关的资质和证书
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 方案
|
||||
|
||||
### 敏感词云
|
||||
|
||||
loan、online、ischeck、isonline、switch、review、审核、热更新、开关、代缴公积金
|
||||
|
||||
扫描是基于源代码出发的扫描的
|
||||
1. 关键词词云在什么阶段分析,构建前、构建后?
|
||||
放在构建前进行。因为需要对敏感词划分等级,如果是 error 级别,则在扫描构建的时候匹配,匹配任意一个则马上构建失败;如果是 warning 级别,则需要在构建完成后展示出来。
|
||||
2. 词云要不要分等级,比如 error、warning。
|
||||
遇到 error 构建马上停止;waring 的话给出警告 console
|
||||
3. 私有 api 要不要扫描,扫描的话怎么做?
|
||||
性质和关键词一样。做到一个 yml 文件里面。然后用正则将一堆关键词和每个代码文件进行匹配。
|
||||
4. 私有 api 是做 top 100 呢?还是 top 50,还是全部的?
|
||||
私有 api 扫描要做,将私有 api 打包到关键词 yml 文件中。所以本质上还是字符串查找的问题。
|
||||
5. 私有 api 要不要设置开关,让打包的人选择要不要扫描私有 api
|
||||
不做关键词扫描的场景目前没想到,所以最好是每个打包都需要做关键词扫描。
|
||||
|
||||
测试实验:
|
||||
NodeJS 实现: 9个铭感词语、代码文件5967个,耗时3.5秒
|
||||
|
||||
|
||||
|
||||
## 技术方案
|
||||
|
||||
1. 业务线需要自定义敏感词云(因为每条业务线的关键词云都不一样)
|
||||
2. 敏感词需要划分等级:error、warning。扫描到 error 需要马上停止构建,并提示「已扫描到你的源码中存在敏感词***,可能存在提交审核失败的可能,请修改后再次构建」。warning 的情况不需要马上停止构建,等任务全部结束后汇总给出提示「已扫描到你的源码中存在敏感词***、***...,可能存在提交审核失败的可能,请开发者自己确认」
|
||||
3. 铭感词云的格式。scaner.yml 文件。
|
||||
- error: 数组的格式。后面写需要扫描的关键词,且等级为 error,表示扫描到 error 则马上停止构建
|
||||
- warning:数组的格式。后面写需要扫描的关键词,且等级为 warning,扫描结果不影响构建,最终只是展示出来
|
||||
- searchPath:字符串格式。可以让业务线自定义需要进行扫描的路径。
|
||||
- fileType:数组格式。可以让业务线自定义需要扫描的文件类型。默认为 `sh|pch|json|xcconfig|mm|cpp|h|m`
|
||||
- warningkeywordsScan:布尔值。业务线可以设置是否需要扫描 warning 级别的关键词。
|
||||
- errorKeywordsScan:布尔值。业务线可以设置是否需要扫描 error 级别的关键词。
|
||||
```
|
||||
error:
|
||||
- checkSwitch
|
||||
warning:
|
||||
- loan
|
||||
- online
|
||||
- ischeck
|
||||
searchPath:
|
||||
../fixtures
|
||||
fileType:
|
||||
- h
|
||||
- m
|
||||
- cpp
|
||||
- mm
|
||||
- js
|
||||
warningkeywordsScan:
|
||||
true
|
||||
errorKeywordsScan:
|
||||
true
|
||||
```
|
||||
4. iOS 端存在私有 api 的情况,Android 端不存在该问题
|
||||
私有 api 70111个文件,每个文件假设10个方法,则共70万个 api。所以计划找出 top 100.去扫描匹配,支持业务线是否开启的选项
|
||||
5. 利用 NodeJS 实现关键词预审能力,wax cli 调度各个模版的能力实现相应的功能,所以要做的事情就是找到相应的时机去编码实现相应的需求。几个问题需要确认?包预检需要放在构建的什么阶段?需要不需要设置关键词的等级?需要不需要设置关键词扫描的文件路径?需要不需要支持自定义文件匹配类型?
|
||||
|
||||
其实这些问题都是业界标准的做法,肯定需要预留这样的能力,所以自定义规则的格式可以查看上面 yml 文件的各个字段所确定。明确了做什么事,以及做事情的标准,那就可以很快的开展并落地实现。
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
|
||||
<summary>点击展开代码</summary>
|
||||
|
||||
```javascript
|
||||
'use strict'
|
||||
|
||||
const { Error, logger } = require('@wac/wax-utils')
|
||||
const fs = require('fs-extra')
|
||||
const glob = require('glob')
|
||||
const YAML = require('yamljs')
|
||||
|
||||
module.exports = class PreBuildCommand {
|
||||
constructor(ctx) {
|
||||
this.ctx = ctx
|
||||
this.projectPath = ''
|
||||
this.fileNum = 0
|
||||
this.isExist = false
|
||||
this.errorFiles = []
|
||||
this.warningFiles = []
|
||||
this.keywordsObject = {}
|
||||
this.errorReg = null
|
||||
this.warningReg = null
|
||||
this.warningkeywordsScan = false
|
||||
this.errorKeywordsScan = false
|
||||
this.scanFileTypes = ''
|
||||
}
|
||||
|
||||
async fetchCodeFiles(dirPath, fileType = 'sh|pch|json|xcconfig|mm|cpp|h|m') {
|
||||
return new Promise((resolve, reject) => {
|
||||
glob(`**/*.?(${fileType})`, { root: dirPath, cwd: dirPath, realpath: true }, (err, files) => {
|
||||
if (err) reject(err)
|
||||
resolve(files)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async scanConfigurationReader(keywordsPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(keywordsPath, 'UTF-8', (err, data) => {
|
||||
if (!err) {
|
||||
let keywords = YAML.parse(data)
|
||||
resolve(keywords)
|
||||
} else {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async run() {
|
||||
const { argv } = this.ctx
|
||||
const buildParam = {
|
||||
scheme: argv.opts.scheme,
|
||||
cert: argv.opts.cert,
|
||||
env: argv.opts.env
|
||||
}
|
||||
|
||||
// 处理包关键词扫描(敏感词汇 + 私有 api)
|
||||
this.keywordsObject = (await this.scanConfigurationReader(this.ctx.cwd + '/.scaner.yml')) || {}
|
||||
this.warningkeywordsScan = this.keywordsObject.warningkeywordsScan || false
|
||||
this.errorKeywordsScan = this.keywordsObject.errorKeywordsScan || false
|
||||
if (Array.isArray(this.keywordsObject.fileType)) {
|
||||
this.scanFileTypes = this.keywordsObject.fileType.join('|')
|
||||
}
|
||||
if (Array.isArray(this.keywordsObject.error)) {
|
||||
this.errorReg = this.keywordsObject.error.join('|')
|
||||
}
|
||||
if (Array.isArray(this.keywordsObject.warning)) {
|
||||
this.warningReg = this.keywordsObject.warning.join('|')
|
||||
}
|
||||
|
||||
// 从指定目录下获取所有文件
|
||||
this.projectPath = this.keywordsObject ? this.keywordsObject.searchPath : this.ctx.cwd
|
||||
const files = await this.fetchCodeFiles(this.projectPath, this.scanFileTypes)
|
||||
|
||||
if (this.errorReg && this.errorKeywordsScan) {
|
||||
await Promise.all(
|
||||
files.map(async file => {
|
||||
try {
|
||||
const content = await fs.readFile(file, 'utf-8')
|
||||
const result = await content.match(new RegExp(`(${this.errorReg})`, 'g'))
|
||||
if (result) {
|
||||
if (result.length > 0) {
|
||||
this.isExist = true
|
||||
this.fileNum++
|
||||
this.errorFiles.push(
|
||||
`编号: ${this.fileNum}, 所在文件: ${file}, 出现次数: ${result &&
|
||||
(result.length || 0)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (this.errorFiles.length > 0) {
|
||||
throw new Error(
|
||||
`从你的项目中扫描到了 error 级别的敏感词,建议你修改方法名称、属性名、方法注释、文档描述。\n敏感词有 「${
|
||||
this.errorReg
|
||||
}」\n存在问题的文件有 ${JSON.stringify(this.errorFiles, null, 2)}`
|
||||
)
|
||||
}
|
||||
|
||||
// warning
|
||||
if (this.warningReg && !this.isExist && this.fileNum === 0 && this.warningkeywordsScan) {
|
||||
await Promise.all(
|
||||
files.map(async file => {
|
||||
try {
|
||||
const content = await fs.readFile(file, 'utf-8')
|
||||
const result = await content.match(new RegExp(`(${this.warningReg})`, 'g'))
|
||||
if (result) {
|
||||
if (result.length > 0) {
|
||||
this.isExist = true
|
||||
this.fileNum++
|
||||
this.warningFiles.push(
|
||||
`编号: ${this.fileNum}, 所在文件: ${file}, 出现次数: ${result &&
|
||||
(result.length || 0)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if (this.warningFiles.length > 0) {
|
||||
logger.info(
|
||||
`从你的项目中扫描到了 warning 级别的敏感词,建议你修改方法名称、属性名、方法注释、文档描述。\n敏感词有 「${
|
||||
this.warningReg
|
||||
}」。有问题的文件有${JSON.stringify(this.warningFiles, null, 2)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in buildParam) {
|
||||
if (!buildParam[key]) {
|
||||
throw new Error(`build: ${key} 参数缺失`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## wax ?
|
||||
公司自研工具 wax 是「一站式协同开发利器,重新定义混合开发」。包含若干子项目,每个子项目就是所谓的 “**模版**”,每个模版其实就是一个 Node 工程,一个 npm 模块,主要负责以下功能:
|
||||
|
||||
- 特定项目类型的目录结构
|
||||
- 自定义命令供开发、构建等使用
|
||||
- 模版持续更新及 patch 等
|
||||
|
||||
按照 iOS 端 `pod install` 这个过程,cocoapods 为我们预留了钩子:`PreInstallHook.rb`、`PostInstallHook.rb`,允许我们在不同的阶段为工程做一些自定义的操作,所以我们的 iOS 模版设计也参考了这个思想,在打包构建前、构建中、构建后提供了钩子:`prebuild`、`build`、`postbuild`。定位好了问题,要做的就是在 prebuild 里面进行关键词扫描的编码工作。
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
86
Chapter1 - iOS/1.79.md
Normal file
86
Chapter1 - iOS/1.79.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# 深入理解各种锁
|
||||
|
||||
## 乐观锁、悲观锁
|
||||
|
||||
乐观锁对应于现实生活中乐观的人,思考事情总往好的方向发展;悲观锁对应于现实生活悲观的人,思考事情总往坏的方向发展。不同性格的人都有优缺点,不能抛开场景说一种人好而另一种人不好。
|
||||
|
||||
乐观锁和悲观锁是一种广义上的概念,体现了看待线程同步问题的不同角度,在 iOS、Java、数据库中都有此概念。
|
||||
|
||||
|
||||
|
||||
### 悲观锁
|
||||
|
||||
对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定会有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
|
||||
这种线程一旦得到锁,其他需要锁的线程就挂起。共享资源每次只给一个线程使用,其他线程阻塞,用完再把资源转让给其他线程。传统的关系型数据库就用到很多悲观锁这种几只,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
|
||||
|
||||
### 乐观锁
|
||||
|
||||
乐观锁认为自己在使用数据的时候不会有别的线程来修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果数据已经被别的线程更新,则根据不同方式执行不同操作(例如报错或者自动重试)。
|
||||
|
||||
可以根据版本号机制和 CAS 算法实现。
|
||||
|
||||
乐观锁适合多读少写的应用类型或者场景,即冲突真的很少发生的场景,这样省去了锁的开销,加大了系统的吞吐量。但是如果多写少读的情况,一般会经常发生冲突,这样会导致上层应用层不断 retry,这样反而降低了性能,所以一般建议多写的场景下使用悲观锁比较合适。
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 乐观锁常见的实现方式
|
||||
|
||||
乐观锁一般使用版本号机制或者 CAS 算法实现。
|
||||
|
||||
|
||||
|
||||
#### 1. 版本号机制
|
||||
|
||||
在数据表增加一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时, version 值加1。当线程1更新数据的时候,先拿到数据并读取出 version 值,修改完数据进行提交更新的时候时,若读取出的 version 值为当前数据库中 version 值相等时才更新,否则重试更新操作,直到更新成功。
|
||||
|
||||
举个例子:
|
||||
假设数据库中账户信息表有一个字段 version,值为1;当前账户余额为100。当需要对账户信息表进行更新的时候,需要读取 version 字段,以及账户余额信息
|
||||
|
||||
- 用户 A 读出数据:version = 1,balance = 100。从账户余额中扣除 50, balacne = 50
|
||||
|
||||
- 用户 B 比用户 A 刚刚晚一点点时间,读出数据 :version = 1, balance = 100。从账户余额中扣除 20,balance = 80
|
||||
|
||||
- 用户 A 完成修改操作,需要提交更新,但是在更新之前会先判断数据库中的版本号 version 值和自己读取到的 version 值是否一致,如果一致,则将版本号 version 字段的值加1(version = 2),连同账户扣除后的余额(balance = 50),提交到数据库服务器执行更新操作,此时由于提交数据中版本号大于数据库记录中的版本,则数据被更新,数据库记录 version = 2
|
||||
|
||||
- 用户 B 完成修改操作,同样在更新之前先读取数据库中的版本号 version 值和自己读取到的 version 值是否一致,但此时发现自己读取到的 version = 1,数据库中的 version = 2,很显然不满足“当前最后更新的版本号 version 与操作员第一次读取到的版本号 version 相等”的乐观锁策略,因此用户 B 的提交被驳回。
|
||||
|
||||
这样,就避免了用户 B 基于 version = 1 的旧数据修改的结果覆盖用户 A 操作的结果,
|
||||
|
||||
#### 2. CAS 算法
|
||||
|
||||
**compare and swap(比较与交换)** ,是一种有名的**无锁算法**。 无锁编程,即在不实用锁的情况下实现多线程之间的数据同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫做**非阻塞同步(Non-blocking Synchorization)**。CAS 算法涉及到的三个操作数
|
||||
|
||||
- 需要读写的内存值 V
|
||||
- 进行比较的值 A
|
||||
- 拟写入的新值 B
|
||||
|
||||
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V,否则不会执行任何操作。比较和替换是一个原则操作。一般情况下是一个自旋操作,即不断的重试
|
||||
|
||||
|
||||
|
||||
### 乐观锁的缺点
|
||||
|
||||
1. ABA 问题
|
||||
如果一个变量 V 初次读取的时候的值为 A,并且在准备赋值的时候检查到变量 V 的值仍然是 A,那么可以说是 V 的值从来没被其他线程修改吗?很明显不能,因为有可能变量 V 的值,从 A 变到 B,然后又改回到 A,那么 CAS 的标准就会认为变量 V 从来没被修改过,这类问题被成为 CAS 的 **ABA** 问题。
|
||||
2. 循环时间长、开销大
|
||||
自旋 CAS (也就是不成功就一直循环操作直到成功)如果长时间不成功,会给 CPU 带来非常大的执行开销。
|
||||
3. 只能保证一个共享变量的原则操作
|
||||
CAS 只对单个变量共享有效,当操作涉及到多个共享变量时,CSA 无效。
|
||||
|
||||
|
||||
|
||||
### CAS 与 synchorized 的使用场景
|
||||
|
||||
一般来说, CAS 适用于乐观锁,多读少写场景,冲突一般较少,则自旋操作的情况非常少,不会消耗 CPU,该场景合适。synchorized 使用悲观锁,多写少读场景,冲突一般较多。
|
||||
|
||||
1. 对于资源竞争比较少(线程冲突较轻)的情况,如果使用 synchorized 同步锁进行线程阻塞和唤醒切换以及用户内核态间的切换操作额外浪费 CPU 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋的几率较少,因此可以获得更高的性能。
|
||||
2. 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的几率会比较大,从而浪费更多的 CPU 资源。效率低于 synchorized
|
||||
|
||||
|
||||
|
||||
|
||||
### 参考资料
|
||||
- [不可不说的Java“锁”事](https://tech.meituan.com/2018/11/15/java-lock.html)
|
||||
141
Chapter1 - iOS/1.8.md
Normal file
141
Chapter1 - iOS/1.8.md
Normal file
@@ -0,0 +1,141 @@
|
||||
|
||||
# 长按UIWebView上的图片保存到相册
|
||||
|
||||
> 不知道各位对于这个需求要如何解决?
|
||||
>
|
||||
> 可能有些人会想到js与原生交互,js监听图片点击事件,然后将图片的url传递给原生App端,然后原生App将图片保存到相册,这样子麻烦吗?超麻烦。(1)、js监听图片长按事件;(2)、js将图片url传递给原生;(3)、原生通过图片的url生成UIImage;(4)、保存UIImage到系统相册,巨麻烦啊,大哥,我很懒的好不好
|
||||
|
||||
#### 那么问题跑出来了,怎么办最简单?
|
||||
|
||||
* 鉴于个人道行尚浅,我就将自己的想法说出来
|
||||
|
||||
* 有个js的api:`Document.elementFromPoint()`
|
||||
|
||||
> The`elementFromPoint()`method of the[`Document`](https://developer.mozilla.org/en-US/docs/Web/API/Document)interface returns the topmost element at the specified coordinates.
|
||||
|
||||
所以根据这个提示,我们完全可以只在App原生端做一些代码开发,实现这个需求
|
||||
|
||||
#### 开发步骤
|
||||
|
||||
*给UIWebView添加长按手势
|
||||
*监听手势动作,拿到坐标点(x,y)
|
||||
*UIWebView注入js:Document.elementFromPoint(x,y).src拿到img标签的src
|
||||
*判断拿到的src是否有值,有值则代表点击的网页上的img标签,此时弹出对话框,是否保存到相册。如果src为空,则代表点击网页上的非img标签,则不需要弹出对话框。
|
||||
*拿到图片的url,生成UIImage。再将图片保存到相册
|
||||
|
||||
#### 有巨坑
|
||||
|
||||
* 长按手势事件不能每次都响应,据我猜测UIWebView本身就有很多事件,所以实现下UIGestureRecognizerDelegate代理方法。长按手势准确率100%
|
||||
|
||||
```
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
|
||||
return YES;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
//
|
||||
// ViewController.m
|
||||
// WebView长按图片保存到相册
|
||||
//
|
||||
// Created by 杭城小刘 on 2017/8/2.
|
||||
// Copyright © 2017年 杭城小刘. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ViewController.h"
|
||||
|
||||
@interface ViewController ()<UIGestureRecognizerDelegate,NSURLSessionDelegate>
|
||||
@property (weak, nonatomic) IBOutlet UIWebView *webView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ViewController
|
||||
|
||||
#pragma mark -- life cycle
|
||||
- (void)viewDidLoad{
|
||||
[super viewDidLoad];
|
||||
|
||||
NSString *htmlURL = [[NSBundle mainBundle] pathForResource:@"saveImage" ofType:@"html"];
|
||||
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:htmlURL]]];
|
||||
//给UIWebView添加手势
|
||||
UILongPressGestureRecognizer* longPressed = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressed:)];
|
||||
longPressed.delegate = self;
|
||||
[self.webView addGestureRecognizer:longPressed];
|
||||
}
|
||||
|
||||
#pragma mark -- UIGestureRecognizerDelegate
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
|
||||
UIActivityTypeAddToReadingList
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)longPressed:(UILongPressGestureRecognizer*)recognizer{
|
||||
if (recognizer.state != UIGestureRecognizerStateBegan) {
|
||||
return;
|
||||
}
|
||||
CGPoint touchPoint = [recognizer locationInView:self.webView];
|
||||
NSString *imgURL = [NSString stringWithFormat:@"document.elementFromPoint(%f, %f).src", touchPoint.x, touchPoint.y];
|
||||
NSString *urlToSave = [self.webView stringByEvaluatingJavaScriptFromString:imgURL];
|
||||
if (urlToSave.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"大宝贝儿" message:@"你真的要保存图片到相册吗?" preferredStyle:UIAlertControllerStyleAlert];
|
||||
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"真的啊" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
||||
[self saveImageToDiskWithUrl:urlToSave];
|
||||
}];
|
||||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"大哥,我点错了,不好意思" style:UIAlertActionStyleDefault handler:nil];
|
||||
[alertVC addAction:okAction];
|
||||
[alertVC addAction:cancelAction];
|
||||
[self presentViewController:alertVC animated:YES completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - private method
|
||||
- (void)saveImageToDiskWithUrl:(NSString *)imageUrl{
|
||||
NSURL *url = [NSURL URLWithString:imageUrl];
|
||||
|
||||
NSURLSessionConfiguration * configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
|
||||
|
||||
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue new]];
|
||||
|
||||
NSURLRequest *imgRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:30.0];
|
||||
|
||||
NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:imgRequest completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
||||
if (error) {
|
||||
return ;
|
||||
}
|
||||
NSData * imageData = [NSData dataWithContentsOfURL:location];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
UIImage * image = [UIImage imageWithData:imageData];
|
||||
UIImageWriteToSavedPhotosAlbum(image, self, @selector(imageSavedToPhotosAlbum:didFinishSavingWithError:contextInfo:), NULL);
|
||||
});
|
||||
}];
|
||||
[task resume];
|
||||
}
|
||||
|
||||
#pragma mark 保存图片后的回调
|
||||
- (void)imageSavedToPhotosAlbum:(UIImage*)image didFinishSavingWithError: (NSError*)error contextInfo:(id)contextInfo{
|
||||
NSString*message =@"嘿嘿";
|
||||
if(!error) {
|
||||
UIAlertController *alertControl = [UIAlertController alertControllerWithTitle:@"提示" message:@"成功保存到相册" preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction *action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDestructive handler:nil];
|
||||
[alertControl addAction:action];
|
||||
[self presentViewController:alertControl animated:YES completion:nil];
|
||||
}else{
|
||||
message = [error description];
|
||||
UIAlertController *alertControl = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];
|
||||
UIAlertAction *action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:nil];
|
||||
[alertControl addAction:action];
|
||||
[self presentViewController:alertControl animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
附上关键的js官方文档:[Document.elementFromPoint()](https://developer.mozilla.org/en-US/docs/Web/API/Document/elementFromPoint)
|
||||
|
||||
附上Demo:[Demo](https://github.com/FantasticLBP/BlogDemos)
|
||||
179
Chapter1 - iOS/1.9.md
Normal file
179
Chapter1 - iOS/1.9.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# hittest方法
|
||||
|
||||
* 就是用来寻找最合适的view
|
||||
* 当一个事件传递给一个控件,就会调用这个控件的hitTest方法
|
||||
* 点击了白色的view: 触摸事件 -> UIApplication -> UIWindow 调用 \[UIWindow hitTest\] -> 白色view \[WhteView hitTest\]
|
||||
|
||||
实验1:
|
||||
|
||||
定义 BaseView,在里面实现方法touchBegan,监听当前哪个类调用了该方法。
|
||||
|
||||
定义KeyWindow,在里面实现hitTest方法,监听哪个类调用了该方法,用来追踪判断哪个view是最合适的view
|
||||
|
||||
在控制器的界面上加5个颜色不同的view,每个view自定义view去实现,因此在不同的view上的手势就可以由不同的view拦截到。
|
||||
|
||||

|
||||
|
||||
|
||||
```
|
||||
//KeyWindow
|
||||
|
||||
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
|
||||
UIView *view = [super hitTest:point withEvent:event];
|
||||
NSLog(@"fittest->%@",view);
|
||||
return view;
|
||||
}
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
点击了白色1:
|
||||
|
||||
```
|
||||
2017-10-11 16:48:52.882547+0800 主流App框架[16295:358790] BrownView--hitTest withEvent
|
||||
2017-10-11 16:48:59.646610+0800 主流App框架[16295:358790] GreenView--hitTest withEvent
|
||||
2017-10-11 16:48:59.647145+0800 主流App框架[16295:358790] fittest-><UIView: 0x7f8f23406510; frame = (0 0; 414 736); autoresize = W+H; layer = <CALayer: 0x60c000221840>>
|
||||
2017-10-11 16:48:59.647575+0800 主流App框架[16295:358790] BrownView--hitTest withEvent
|
||||
2017-10-11 16:48:59.647702+0800 主流App框架[16295:358790] GreenView--hitTest withEvent
|
||||
2017-10-11 16:48:59.647880+0800 主流App框架[16295:358790] fittest-><UIView: 0x7f8f23406510; frame = (0 0; 414 736); autoresize = W+H; layer = <CALayer: 0x60c000221840>>
|
||||
```
|
||||
|
||||
点击了蓝色3:
|
||||
|
||||
```
|
||||
2017-10-11 16:49:56.331024+0800 主流App框架[16295:358790] BrownView--hitTest withEvent
|
||||
2017-10-11 16:49:56.331335+0800 主流App框架[16295:358790] BView--hitTest withEvent
|
||||
2017-10-11 16:49:56.331617+0800 主流App框架[16295:358790] BlueView--hitTest withEvent
|
||||
2017-10-11 16:49:56.331968+0800 主流App框架[16295:358790] YellowView--hitTest withEvent
|
||||
2017-10-11 16:49:56.333206+0800 主流App框架[16295:358790] fittest-><BlueView: 0x7f8f23406f10; frame = (19 21; 240 128); autoresize = RM+BM; layer = <CALayer: 0x60c0002218c0>>
|
||||
2017-10-11 16:49:56.333633+0800 主流App框架[16295:358790] BrownView--hitTest withEvent
|
||||
2017-10-11 16:49:56.333762+0800 主流App框架[16295:358790] BView--hitTest withEvent
|
||||
2017-10-11 16:49:56.333893+0800 主流App框架[16295:358790] BlueView--hitTest withEvent
|
||||
2017-10-11 16:49:56.334005+0800 主流App框架[16295:358790] YellowView--hitTest withEvent
|
||||
2017-10-11 16:49:56.334185+0800 主流App框架[16295:358790] fittest-><BlueView: 0x7f8f23406f10; frame = (19 21; 240 128); autoresize = RM+BM; layer = <CALayer: 0x60c0002218c0>>
|
||||
2017-10-11 16:49:56.334644+0800 主流App框架[16295:358790] BlueView
|
||||
```
|
||||
|
||||
那么看出来hitTest方法的作用就是找出最合适的view,那么我们可以指定任何事情的最合适的view为特定的view
|
||||
|
||||
实验2:
|
||||
|
||||
在KeyWindow中hitTest方法中返回BlueView,那么点击任何色块的view那么都会交给BlueView去处理事件。
|
||||
|
||||
```
|
||||
//KeyWindow
|
||||
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
|
||||
return self.subviews.firstObject.subviews.firstObject;
|
||||
}
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```
|
||||
2017-10-11 22:48:46.102793+0800 主流App框架[21498:749663] GreenView
|
||||
2017-10-11 22:48:46.668595+0800 主流App框架[21498:749663] GreenView
|
||||
```
|
||||
|
||||
因为事件的响应者链条就是当用户操作屏幕会产生一个事件,该事件被系统加入到事件队列中去,UIApplication对象会将事件队列中最早加入进去的事件传递给window,然后window找到最合适的view去处理事件。因此任何事件都会先通过KeyWindow对象去判断并找到最合适的view
|
||||
|
||||
## 2个重要的方法
|
||||
|
||||
* -\(BOOL\)pointInside:\(CGPoint\)point withEvent:\(UIEvent \*\)event: 用来判断触摸点是否在控件上
|
||||
|
||||
* -\(UIView \*\)hitTest:\(CGPoint\)point withEvent:\(UIEvent \*\)event: 用来判断控件是否接受事件以及找到最合适的view
|
||||
|
||||
## 模仿系统实现找出最合适的view
|
||||
|
||||
```
|
||||
//KeyWindow
|
||||
|
||||
/**
|
||||
模仿系统实现寻找最合适的view步骤
|
||||
1、控件接收事件
|
||||
2、触摸点在自己身上
|
||||
3、从后往前遍历子控件,重复前面2个步骤
|
||||
4、如果没有符合条件的子控件,那么就自己最合适
|
||||
|
||||
*/
|
||||
|
||||
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
|
||||
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![self pointInside:point withEvent:event]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
for (NSUInteger index = self.subviews.count - 1; index >= 0; index--) {
|
||||
CGPoint childViewPoint = [self convertPoint:point toView:self.subviews[index]];
|
||||
UIView *fitestView = [self.subviews[index] hitTest:childViewPoint withEvent:event];
|
||||
if (fitestView) {
|
||||
return fitestView;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
```
|
||||
|
||||
给出 一个Demo地址:[https://github.com/FantasticLBP/BlogDemos/tree/master/模仿系统找出事件的最佳响应者](https://github.com/FantasticLBP/BlogDemos/tree/master/模仿系统找出事件的最佳响应者 "模仿系统找出事件的最佳响应者")
|
||||
|
||||
实验:
|
||||
|
||||
在控制器(ViewController)的view上先添加一个UIButton,再添加一个自定义的UIView\(ShelterView\),盖在button的上面。
|
||||
|
||||
需求:点击ShelterView上的点,如果点也在UIButton范围上则交给UIButton处理事件,如果不在UIButton上则交给ShelterView处理,如果点击屏幕上除了ShelterView之外的点则交给控制器的view处理。
|
||||
|
||||
```
|
||||
//ViewController
|
||||
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
|
||||
NSLog(@"viewController->%s",__func__);
|
||||
}
|
||||
|
||||
|
||||
//ShelterView
|
||||
#import "ShelterView.h"
|
||||
|
||||
@implementation ShelterView
|
||||
|
||||
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
|
||||
NSLog(@"%s",__func__);
|
||||
}
|
||||
|
||||
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
|
||||
NSLog(@"%s",__func__);
|
||||
/**
|
||||
需求:不管点击按钮还是view都交给button处理
|
||||
思路:在view的hitTest方法中寻找最合适的view,那么返回nil告诉系统view不是最合适的view,那么系统则认为按钮是最合适的view
|
||||
return nil;
|
||||
*/
|
||||
|
||||
//需求,在view上点击,如果点击范围在button上则由button进行处理事件;否则交给view处理事件
|
||||
|
||||
UIView *button = nil;
|
||||
for (UIView *subView in self.superview.subviews) {
|
||||
//判断事件的点是否在按钮上
|
||||
if ([subView isKindOfClass:[UIButton class]]) {
|
||||
button =subView;
|
||||
}
|
||||
|
||||
|
||||
CGPoint btnPoint = [self convertPoint:point toView:button];
|
||||
if ([button pointInside:btnPoint withEvent:event]) {
|
||||
return button;
|
||||
}else{
|
||||
//此时代表事件触摸点不在button上,但是也不能写nil,写nil的话点击屏幕上的其他地方系统会寻找最合适的view,此时返回nil( return nil;),则代表view不是最合适的view,那么此时点击屏幕上除了按钮之外的区域,最合适的view就是控制器上面的view
|
||||
return [super hitTest:point withEvent:event];
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
```
|
||||
|
||||
要看完整Demo,地址为:[https://github.com/FantasticLBP/BlogDemos/tree/master/hitTest的神奇效果(一)](https://github.com/FantasticLBP/BlogDemos/tree/master/hitTest的神奇效果(一) "hitTest的神奇效果")
|
||||
|
||||
86
Chapter1 - iOS/chapter1.md
Normal file
86
Chapter1 - iOS/chapter1.md
Normal file
@@ -0,0 +1,86 @@
|
||||
|
||||
# 第一部分
|
||||
|
||||
第一部分主要介绍 iOS 开发中遇到的问题或者有趣的知识
|
||||
|
||||
* [1、工程大小优化之图片资源](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.1.md)
|
||||
* [2、看透构造方法](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.2.md)
|
||||
* [3、控制器加载的玄机](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.3.md)
|
||||
* [4、如何优雅地调试手机网页?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.4.md)
|
||||
* [5、事件响应者链](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.5.md)
|
||||
* [6、外卖App双列表联动](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.6.md)
|
||||
* [7、在内存剖析对象](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.7.md)
|
||||
* [8、长按UIWebView上的图片保存到相册](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.8.md)
|
||||
* [9、hitTest和pointInside方法你真的熟吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.9.md)
|
||||
* [10、HyBrid探索(一)](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.10.md)
|
||||
* [11、iOS中的事件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.11.md)
|
||||
* [12、NSFileManager终极杀手](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.12.md)
|
||||
* [13、UINavigationController的妙用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.13.md)
|
||||
* [14、URL-Schemes深度剖析(上)](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.14.md)
|
||||
* [15、URL Schemes 的发展](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.15.md)
|
||||
* [16、CocoaPods的使用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.16.md)
|
||||
* [16、OC与Swift混编](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.16.md)
|
||||
* [17、对于不可调节高度的UI控件进行改变frame](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.17.md)
|
||||
* [18、YYModel 的使用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.18.md)
|
||||
* [19、实现波浪动画](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.19.md)
|
||||
* [20、底层原理探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.20.md)
|
||||
* [21、禅与 Objective-C 编程艺术](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.21.md)
|
||||
* [22、修改 UITextField placeholder 样式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.22.md)
|
||||
* [23、UIScrollView拖拽时回收键盘](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.23.md)
|
||||
* [24、读 Apple 源码看看 NSRange](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.24.md)
|
||||
* [25、复制层(CAReplicatorLayer)](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.25.md)
|
||||
* [26、CAShapeLayer](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.26.md)
|
||||
* [27、仿微博动画](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.27.md)
|
||||
* [28、UILabel 全局匹配并高亮](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.28.md)
|
||||
* [29、JavascriptCore](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.29.md)
|
||||
* [30、Xcode 小技巧](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.30.md)
|
||||
* [31、终端效率](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.31.md)
|
||||
* [32、终极截屏](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.32.md)
|
||||
* [33、推送](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.33.md)
|
||||
* [34、App 评分](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.34.md)
|
||||
* [35、一些布局小知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.35.md)
|
||||
* [36、iOS数值计算精度丢失问题](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.36.md)
|
||||
* [37、一些看到但未尝试的知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.37.md)
|
||||
* [38、RunLoop上](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.38.md)
|
||||
* [39、RunLoop下](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.39.md)
|
||||
* [40、RunLoop的应用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.40.md)
|
||||
* [41、iOS 应用启动性能优化资料汇总](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.41.md)
|
||||
* [42、App security](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.42.md)
|
||||
* [43、奇技淫巧调试篇](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.43.md)
|
||||
* [44、Awesome Hybrid - 1](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.44.md)
|
||||
* [45、NSTimer 的内存泄漏](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.45.md)
|
||||
* [46、KVC && KVO](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.46.md)
|
||||
* [47、金额格式化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.47.md)
|
||||
* [48、OC类别(Catrgory)和拓展(Extension)](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.48.md)
|
||||
* [49、MVC、MVP、MVVM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.49.md)
|
||||
* [50、“静态库”和“动态库”](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.50.md)
|
||||
* [51、cocopod](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.51.md)
|
||||
* [52、如何打造团队的代码风格统一以及开发效率的提升](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.52.md)
|
||||
* [53、iOS 数据持久化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.53.md)
|
||||
* [54、Xcode 设置作者信息](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.54.md)
|
||||
* [55、史上最强、最详细无痕埋点方案](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.55.md)
|
||||
* [56、大前端时代的安全性](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.56.md)
|
||||
* [57、自动布局的思考](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.57.md)
|
||||
* [58、Swift每个版本迁移的总结](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.58.md)
|
||||
* [59、iOS零散知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.59.md)
|
||||
* [60、App瘦身之道](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.60.md)
|
||||
* [61、App启动时间优化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.61.md)
|
||||
* [62、OCLint实现Code Review](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.62.md)
|
||||
* [63、苹果官方开源资料](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.63.md)
|
||||
* [64、组件化、模块化、插件、子应用、框架、库理解](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.64.md)
|
||||
* [65、多端融合方案](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.65.md)
|
||||
* [66、移动端网络层优化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.66.md)
|
||||
* [67、iOS工程编译速度优化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.67.md)
|
||||
* [68、守护你的App安全](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.68.md)
|
||||
* [69、React-Native总结](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.69.md)
|
||||
* [70、不一样的动态化能力](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.70.md)
|
||||
* [71、Flutter初体验-安装](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.71.md)
|
||||
* [72、架构设计心得](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.72.md)
|
||||
* [73、Ruby学习](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.73.md)
|
||||
* [74、APM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.74.md)
|
||||
* [75、如何写好测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.75.md)
|
||||
* [76、iOS Crash分析](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.76.md)
|
||||
* [77、iOS 打包系统构建加速](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.77.md)
|
||||
* [78、上架包预检](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.78.md)
|
||||
* [79、深入理解各种锁](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.79.md)
|
||||
* [80、打造功能强大的数据上报组件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md)
|
||||
Reference in New Issue
Block a user