mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-26 13:41:32 +00:00
feat: 初始化
This commit is contained in:
BIN
第一部分 iOS/.DS_Store
vendored
Normal file
BIN
第一部分 iOS/.DS_Store
vendored
Normal file
Binary file not shown.
93
第一部分 iOS/1.1.md
Normal file
93
第一部分 iOS/1.1.md
Normal file
@@ -0,0 +1,93 @@
|
||||
|
||||
### 工程大小优化之图片资源
|
||||
|
||||
> 一点点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. 打开网站在线挑选好合适的图标加入购物车,如图
|
||||

|
||||
|
||||
3. 选择好之后在购物车查看,然后点击下载代码
|
||||
|
||||
4. 打开下载好的文件,其机构如下,我们在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检查
|
||||
|
||||

|
||||
|
||||
3. 怎么用?
|
||||
|
||||
```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)
|
||||
|
||||
|
||||
134
第一部分 iOS/1.10.md
Normal file
134
第一部分 iOS/1.10.md
Normal file
@@ -0,0 +1,134 @@
|
||||
|
||||
## 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;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 同步和异步问题
|
||||
|
||||
js调用native是通过在一个网页上插入一个iframe,这个iframe插入完了就完了,执行的结果需要native另外调用stringByEvaluatingJavaScriptString 方法通知js。这明显是1个异步的调用。而stringByEvaluatingJavaScriptString方法会返回执行js脚本的结果。本质上是一个同步调用
|
||||
|
||||
所以js call native是异步,native call js是同步。
|
||||
51
第一部分 iOS/1.11.md
Normal file
51
第一部分 iOS/1.11.md
Normal file
@@ -0,0 +1,51 @@
|
||||
|
||||
|
||||
|
||||
|
||||
* 用户在使用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
第一部分 iOS/1.12.md
Normal file
224
第一部分 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];
|
||||
}
|
||||
```
|
||||
228
第一部分 iOS/1.13.md
Normal file
228
第一部分 iOS/1.13.md
Normal file
@@ -0,0 +1,228 @@
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
145
第一部分 iOS/1.14.md
Normal file
145
第一部分 iOS/1.14.md
Normal file
@@ -0,0 +1,145 @@
|
||||
|
||||
###自定义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
第一部分 iOS/1.15.md
Normal file
54
第一部分 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)
|
||||
|
||||
|
||||
|
||||
|
||||
137
第一部分 iOS/1.16.md
Normal file
137
第一部分 iOS/1.16.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# CocoaPods最新安装步骤
|
||||
|
||||
## 一、简介
|
||||
|
||||
* ### 什么是CocoaPods
|
||||
|
||||
* CocoaPods是OS X和iOS下的一个第三方库管理工具,通过CocoaPods我们可以为项目添加所依赖的库。(这种库必须是CocoaPods本身可以找到且支持的),且可以方便管理这些库的版本(更新、回退)
|
||||
* ### 好处
|
||||
|
||||
* 在引入第三方库的时候它自动为我们完成各种各样的配置,包括配置编译阶段、连接器选项、甚至是ARC环境下的-fno-objc-arc配置等
|
||||
|
||||
* 使用CocoaPods可以方便查找第三方库(pod search AFNetworking\),这些库是比较标准的,因为在CocoaPods上可以找得到所以符合CocoaPods的规范比较标准
|
||||
|
||||
## 二、CocoaPods的安装步骤
|
||||
|
||||
### 1、升级机器的Ruby环境
|
||||
|
||||
```
|
||||
终端输入:$ gem update --system
|
||||
```
|
||||
|
||||
此时有可能出现You do not have write permissions for the /Library/Ruby/Gems/2.0.0 directory
|
||||
|
||||
明显告诉我们没有权限,用过linux的人知道需要加上sudo接下来会让你输入密码。不出意外接下来会提示
|
||||
|
||||
```
|
||||
RubyGems system software updated
|
||||
```
|
||||
|
||||
### 2、更换Ruby镜像
|
||||
|
||||
* 首先移除现有的Ruby镜像
|
||||
|
||||
```
|
||||
终端输入:$ gem sources --remove https://rubygems.org/
|
||||
```
|
||||
|
||||
* 然后添加国内镜像
|
||||
|
||||
```
|
||||
终端输入:$ gem sources -a https://gems.ruby-china.org/
|
||||
```
|
||||
|
||||
* 执行完毕之后输入gem sources -l来查看当前镜像
|
||||
|
||||
```
|
||||
终端输入:$ gem sources -l
|
||||
```
|
||||
|
||||
如果结果是
|
||||
|
||||
`*** CURRENT SOURCES ***https://gems.ruby-china.org/`
|
||||
|
||||
### 3、安装Cocopods
|
||||
|
||||
输入
|
||||
|
||||
```
|
||||
终端输入:$ sudo gem install cocoapods
|
||||
```
|
||||
|
||||
在我的MBP上出现了提示说权限不足,接下来再次输入
|
||||
|
||||
```
|
||||
终端输入:$ sudo gem install -n /usr/local/bin cocoapods
|
||||
```
|
||||
|
||||
安装成功
|
||||
|
||||
```
|
||||
终端显示 21 gems installed
|
||||
```
|
||||
|
||||
继续
|
||||
|
||||
```
|
||||
pod setup
|
||||
```
|
||||
|
||||
这个过程比较长
|
||||
|
||||
|
||||
|
||||
## 三、Cocopods的使用
|
||||
|
||||
### 1、查找
|
||||
|
||||
```
|
||||
终端输入:pod search ReactiveObjc
|
||||
```
|
||||
|
||||
会显示找到的最新库及其历史版本
|
||||

|
||||
|
||||
|
||||
|
||||
### 2、用法
|
||||
|
||||
* 新建工程
|
||||
* 命令行切换到工程目录下
|
||||
* 新建一个Podfile
|
||||
|
||||
```
|
||||
终端:touch Podfile
|
||||
```
|
||||
|
||||
* 编辑Podfile
|
||||
|
||||
```
|
||||
终端:vim Podfile
|
||||
```
|
||||
|
||||
输入以下内容
|
||||
|
||||
```
|
||||
target 'RAC' do
|
||||
|
||||
pod 'ReactiveObjC', '~> 3.0.0'
|
||||
|
||||
end
|
||||
```
|
||||
|
||||
* 终端安装库
|
||||
|
||||
```
|
||||
终端:pod install
|
||||
```
|
||||
|
||||
* 完成之后会看到一个在项目目录下多出一个RAC.xcworkspace文件
|
||||
* 终端输入
|
||||
|
||||
```
|
||||
终端:open RAC.xcworkspace
|
||||
```
|
||||
|
||||
|
||||
|
||||
10
第一部分 iOS/1.17.md
Normal file
10
第一部分 iOS/1.17.md
Normal file
@@ -0,0 +1,10 @@
|
||||
### Swift、OC混编
|
||||
|
||||
```
|
||||
1、在oc文件中使用swift文件。
|
||||
选中项目TARGETS->Building Settings->搜索“Objective-C Genereated Interface Header Name”对应的名字。
|
||||
在oc文件中需要使用swift的地方,头文件导入上一步对应的名字。
|
||||
```
|
||||
|
||||
|
||||
|
||||
8
第一部分 iOS/1.18.md
Normal file
8
第一部分 iOS/1.18.md
Normal file
@@ -0,0 +1,8 @@
|
||||
* 对于不能调节高度的控件比如 UISlider、UISwitch、UIProgressView 等控件的宽高可以用 \(仿射变化\)transform 属性控制高度。
|
||||
|
||||
```
|
||||
myswitch.transform = CGAffineTransformMakeScale(1,5);
|
||||
```
|
||||
|
||||
|
||||
|
||||
193
第一部分 iOS/1.19.md
Normal file
193
第一部分 iOS/1.19.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
|
||||
|
||||
```
|
||||
183
第一部分 iOS/1.2.md
Normal file
183
第一部分 iOS/1.2.md
Normal file
@@ -0,0 +1,183 @@
|
||||
|
||||
# 构造方法
|
||||
|
||||
* 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开头的方法是构造方法
|
||||
4
第一部分 iOS/1.20.md
Normal file
4
第一部分 iOS/1.20.md
Normal file
@@ -0,0 +1,4 @@
|
||||
1、豆瓣模块化解决方案:https://github.com/lincode/FRDModuleManager(主要解决Appdelegate臃肿)
|
||||
|
||||
|
||||
|
||||
56
第一部分 iOS/1.21.md
Normal file
56
第一部分 iOS/1.21.md
Normal file
@@ -0,0 +1,56 @@
|
||||
## 实现原理 {#实现原理}
|
||||
|
||||
波浪的形状绘制在 CAShapeLayer 上。通过 CADisplayLink 与屏幕刷新频率同步,每次刷新都绘制新的波浪,并改变小船的位置和角度。另外,水和天空的颜色是渐变的,由 CAGradientLayer 实现,其中,显示水的 CAGradientLayer 需要有波浪形状的 CAShapeLayer 的遮罩\(mask\)。
|
||||
|
||||
### CAShapeLayer {#cashapelayer}
|
||||
|
||||
CAShapeLayer 的属性 path \(CGPath\)就是图层要显示的形状。把波浪的形状绘制出来,赋值给此属性即可。
|
||||
|
||||
### CADisplayLink {#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}
|
||||
|
||||
CAGradientLayer 默认的颜色渐变方向是由上至下。给 colors 属性赋值一个包含 CGColor 的数组,则图层颜色由上至下,从数组第一个值经中间值渐变至最后一个值。
|
||||
|
||||
显示水的 CAGradientLayer 需要呈现波浪形状,需要 CAShapeLayer 的遮罩。把绘制好波浪形状的 CAShapeLayer 赋值给 CAGradientLayer 的 mask 属性即可。
|
||||
|
||||
88
第一部分 iOS/1.22.md
Normal file
88
第一部分 iOS/1.22.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
第一部分 iOS/1.23.md
Normal file
80
第一部分 iOS/1.23.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
第一部分 iOS/1.24.md
Normal file
35
第一部分 iOS/1.24.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
第一部分 iOS/1.25.md
Normal file
27
第一部分 iOS/1.25.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
第一部分 iOS/1.26.md
Normal file
86
第一部分 iOS/1.26.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
第一部分 iOS/1.27.md
Normal file
210
第一部分 iOS/1.27.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
第一部分 iOS/1.28.md
Normal file
172
第一部分 iOS/1.28.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
第一部分 iOS/1.29.md
Normal file
65
第一部分 iOS/1.29.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/微博发帖动画)
|
||||
|
||||
99
第一部分 iOS/1.3.md
Normal file
99
第一部分 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方法;存在则直接返回,所以界面先是绿色,再是棕色最后是红色
|
||||
|
||||
#### 来一个官方解释
|
||||
|
||||

|
||||
|
||||
|
||||
39
第一部分 iOS/1.30.md
Normal file
39
第一部分 iOS/1.30.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
第一部分 iOS/1.31.md
Normal file
155
第一部分 iOS/1.31.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);
|
||||
}
|
||||
|
||||
|
||||
39
第一部分 iOS/1.32.md
Normal file
39
第一部分 iOS/1.32.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 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']
|
||||
```
|
||||
|
||||
\(参考来自:「 iOS知识小集 」2018 · 第 18 期\)
|
||||
|
||||
30
第一部分 iOS/1.33.md
Normal file
30
第一部分 iOS/1.33.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
第一部分 iOS/1.34.md
Normal file
303
第一部分 iOS/1.34.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
第一部分 iOS/1.35.md
Normal file
28
第一部分 iOS/1.35.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
第一部分 iOS/1.36.md
Normal file
55
第一部分 iOS/1.36.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
第一部分 iOS/1.37.md
Normal file
163
第一部分 iOS/1.37.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
第一部分 iOS/1.38.md
Normal file
72
第一部分 iOS/1.38.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
第一部分 iOS/1.39.md
Normal file
34
第一部分 iOS/1.39.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个对象的内存地址必须相等
|
||||
|
||||
|
||||
|
||||
17
第一部分 iOS/1.4.md
Normal file
17
第一部分 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使用效果啊。
|
||||

|
||||
938
第一部分 iOS/1.40.md
Normal file
938
第一部分 iOS/1.40.md
Normal file
@@ -0,0 +1,938 @@
|
||||
## 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;
|
||||
}
|
||||
```
|
||||
|
||||
771
第一部分 iOS/1.41.md
Normal file
771
第一部分 iOS/1.41.md
Normal file
@@ -0,0 +1,771 @@
|
||||
## 监听 RunLoop
|
||||
|
||||
//给 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;
|
||||
}
|
||||
```
|
||||
|
||||
143
第一部分 iOS/1.42.md
Normal file
143
第一部分 iOS/1.42.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
第一部分 iOS/1.43.md
Normal file
154
第一部分 iOS/1.43.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 {#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
第一部分 iOS/1.44.md
Normal file
84
第一部分 iOS/1.44.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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
11
第一部分 iOS/1.45.md
Normal file
11
第一部分 iOS/1.45.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# 调试小技巧
|
||||
|
||||
1. 在日常开发中我们经常会封装某个功能模块然后暴露某个方法给外部。但是很多时候调用我们封装功能的人可能会不按照约定的方法传递参数。所以我们会使用断言。但是在线上的时候如果使用了断言,那么程序肯定会 **Crash** ,Xcode 提供了一个小功能可以解决这个问题。
|
||||
|
||||
|
||||
|
||||
NS_BLOCK_ASSERTIONS: 表明在 Release 状态下过滤 NSAssert,只需要这一个条件就可以过滤掉 NSAssert。
|
||||
|
||||
方法:在 “Build Settings” 下搜索 **Preprocessor Macros** ,然后在 Release 下面添加 NS_BLOCK_ASSERTIONS
|
||||
|
||||

|
||||
75
第一部分 iOS/1.46.md
Normal file
75
第一部分 iOS/1.46.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Hybrid
|
||||
|
||||
|
||||
|
||||
### 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调用的")`。
|
||||
171
第一部分 iOS/1.47.md
Normal file
171
第一部分 iOS/1.47.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 (SGLUnRetain)
|
||||
+ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval
|
||||
repeats:(BOOL)repeats
|
||||
block:(void(^)(NSTimer *timer))block;
|
||||
@end
|
||||
|
||||
//.m文件
|
||||
#import "NSTimer+SGLUnRetain.h"
|
||||
|
||||
@implementation NSTimer (SGLUnRetain)
|
||||
|
||||
+ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{
|
||||
|
||||
return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(sgl_blcokInvoke:) userInfo:[block copy] repeats:repeats];
|
||||
}
|
||||
|
||||
+ (void)sgl_blcokInvoke:(NSTimer *)timer {
|
||||
|
||||
void (^block)(NSTimer *timer) = timer.userInfo;
|
||||
|
||||
if (block) {
|
||||
block(timer);
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
//控制器.m
|
||||
|
||||
#import "ViewController.h"
|
||||
#import "NSTimer+SGLUnRetain.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 sgl_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 参数的方法,最终的行为一致
|
||||
57
第一部分 iOS/1.48.md
Normal file
57
第一部分 iOS/1.48.md
Normal file
@@ -0,0 +1,57 @@
|
||||
### 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
128
第一部分 iOS/1.49.md
Normal file
128
第一部分 iOS/1.49.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)
|
||||
|
||||
|
||||
|
||||
68
第一部分 iOS/1.5.md
Normal file
68
第一部分 iOS/1.5.md
Normal file
@@ -0,0 +1,68 @@
|
||||
|
||||
实验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...方法如果子控件不处理那么都会顺着响应者链条向上传递给上一层响应者对象,比如可以交给控制器处理。
|
||||
|
||||
|
||||
|
||||
148
第一部分 iOS/1.6.md
Normal file
148
第一部分 iOS/1.6.md
Normal file
@@ -0,0 +1,148 @@
|
||||
|
||||
|
||||
### 双列表联动
|
||||
|
||||
|
||||
> 用过了那么多的外卖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)
|
||||
101
第一部分 iOS/1.7.md
Normal file
101
第一部分 iOS/1.7.md
Normal file
@@ -0,0 +1,101 @@
|
||||
|
||||
|
||||
# 对象在内存中的存储
|
||||
|
||||
* 栈、堆、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的值相同。**
|
||||
141
第一部分 iOS/1.8.md
Normal file
141
第一部分 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)
|
||||
180
第一部分 iOS/1.9.md
Normal file
180
第一部分 iOS/1.9.md
Normal file
@@ -0,0 +1,180 @@
|
||||
|
||||
### 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的神奇效果")
|
||||
|
||||
6
第一部分 iOS/chapter1.md
Normal file
6
第一部分 iOS/chapter1.md
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
# 第一部分
|
||||
|
||||
第一部分主要介绍 iOS 开发中遇到的问题或者有趣的知识
|
||||
|
||||
|
||||
Reference in New Issue
Block a user