击水湘江

Born To Fight!


  • Home

  • About

  • Tags

  • Archives

iOS中的国际化

Posted on 2016-06-01

IOS中,如果系统的语言或者地区变化了,我们怎样让App中显示的语言, 日期,数字,货币单位的格式随着变化呢?下面将介绍IOS中简单的国际化的方法:

在GitHub上下载一个需要国际化的工程:
打开这个工程你可以当看到如下的一个界面:

然后点开StoryBoard,你会发现里面的控件都非常简单。为了国际话,我们需要往项目中再添加一门语言。添加语言的方式是,Project--->Info--->Localizations点击”+”来添加相应语言,这里我们选择Chinese(simplified)简体中文。然后将弹出的对话框中的LaunchScreen.strings和main.storyBoard都勾选了。这样我们的基本工作就完成了,下面正式开始:

创建.strings文件

创建.strings文件,点击command + N 新建文件,在Resouce中选择Strings File文件,命名为Localizable,这样系统就会在不同语言环境下选择不同的.strings文件进行加载,显示不同的语言。

处理Localizable.strings

点击Localizable.strings文件,点击右侧的Localization按钮,然后选择需要本地化的语言,选择汉语,然后再次点击Localizable.strings,你会发现右侧的Localizattion下面多了几个选择语言的选项,Base,English,Chinese(simplified)选中English,然后就会发现又多了一个.strings文件。

配置.string文件

对不同的.string文件配置不同的Value值,在.string中配置的格式是”KEY” = “VALUE”;, 注意最后的分号。我们在localizable.strings(English)中加上

1
2
"I write %@ lines every day." = "I write %@ lines every day.";
"Do you like coding ?" = "Do you like coding ?";

在Localizable.strings(Chinese simplified)中添加

1
2
"I write %@ lines every day." = "我每天写%@行代码。";
"Do you like coding ?" = "你喜欢编程吗?";

使用方法

在ViewController.m中改写相应的加载方法将原来的

1
2
_viewControllerNumLabel.text = @"I write 1000000000 lines every day.";
[_viewControllerLikeButotn setTitle:@"Do you like coding ?" forState:UIControlStateNormal];

替换为:

1
2
_viewControllerNumLabel.text = [NSString stringWithFormat:NSLocalizedString(@"I write %@ lines every day.", nil),@1000000000];
[_viewControllerLikeButotn setTitle:NSLocalizedString(@"Do you like coding ?", nil) forState:UIControlStateNormal];

这时候将系统的语言设置成简体中文,General -> International -> Language -> Chinese,然后再重新运行App,你会发现变其中的一个Label和一个Button上的文字改变了。

图片的处理

修改不同的图片。这时你会发现,有些时候,我们的图片上会有文字,这些文字是不能用代码改变的,所以这时要加载不同的图片,利用步骤4中的方法,我们给图片名用一个Key来表示,然后将不同的图片名作为value写在不同的.strings文件中,我们在English中加入:

"imageName" = "english";
我们在Chinese中加入:"imageName" = "chinese";
其中english和chinese都是图片的文字,然后在ViewControlelr中添加如下代码

1
_viewControllerImageView.image = [UIImage imageNamed:NSLocalizedString(@"imageName",nil)];

这时,修改相应的语言就将会出现不同的图片。

处理StoryBoard

修改没有被引出的控件的显示。点击Main.StoryBoard,你会发现下面有一个Main.strings(Chinese Simplified)文件,这时将里面有关Lable的代码:

1
2
/* Class = "UILabel"; text = "Hello I am a Lable"; ObjectID = "zki-n6-dit"; */
"zki-n6-dit.text" = "Hello I am a Lable";

修改为:

1
2
/* Class = "UILabel"; text = "Hello I am a Lable"; ObjectID = "zki-n6-dit"; */
"zki-n6-dit.text" = "您好,我是一个Label";

再次运行代码,你会发现这个时候没有用代码修改的Lable显示的内容也变了。

数字的处理

还有一个细节问题,数字的格式,运行App时候你会发现原来显示的格式是1000000000,但是我国用的数字表示方式应该是1,000,000,000,西班牙用的数字表示方式是:1.000.000.000,这个怎么国际化呢?这个时候需要在ViewController中添加如下代码:

1
2
3
4
NSNumberFormatter *numberFormatter = [NSNumberFormatter new];
numberFormatter.numberStyle =NSNumberFormatterDecimalStyle;
NSString *numberString = [numberFormatter stringFromNumber:@1000000000];
_viewControllerNumLabel.text = [NSString stringWithFormat:NSLocalizedString(@"I write %@ lines every day.", nil),numberString];

这里利用NSNumberFormatter来进行对数字格式的转化,这里需要注意,再运行App的时候需要将相应的地区也设置成相应的 General -> International -> Region Format -> China

App显示国际化

如何让App显示的名字也国际化?这时需要添加一个plist文件InfoPlist.strings,然后将这个文件进行Localizable,在生成的相应的english和chinese中添加需要显示的App的名字,如:在Chinese的文件中添加:”CFBundleDisplayName” = “双语者”,重新运行App就会发现App的名字也随着语言的改变也改变了,最后App国际化的结果为:

Swift学习Tip之Closures

Posted on 2016-05-31

闭包简介

什么是Closures? Closures也就是我们常说的闭包,闭包是一个具有函数功能的代码块,它类似于OC中的Block,它能够在它被定义的上下文中,被常量或者变量所捕捉和存储。也就是是说把常量和变量Closing(包)住了,这也是Closures的由来。全局函数和嵌套函数(Global and nested functions)是闭包的一种特例,闭包有一下三种形式:

  • 全局函数是有名称但是不会捕获任何数值得闭包。
  • 嵌套函数是有名称并且可以从它们的封闭的函数体内部捕获数值的闭包。
  • 闭包表达式是用轻量级的语言所书写,并且能够从它们所在的上下文中获取数值的一种没有名称的closures。

由于Swift本身对闭包做了很多的优化,这使得闭包的使用更加的简单明了,这些优化包括:

  • 从上下文中推断出参数类型和返回值类型
  • 在单一表达的闭包(single-expression)中隐含返回值
  • 简化参数名
  • 尾随闭包语法

闭包的基本语法

1
2
3
4
5
//闭包表达式的类型和函数的类型是一样的,都是参数加上返回值,但是闭包后面有一个in, 这个关键字来告诉编译器,闭包的参数和返回值已经定义完了,下面是闭包执行语句了。
{
(参数) -> 返回值类型 in
执行语句
}

下面说明闭包的基本用法:

  • 闭包的完整写法:
1
2
3
4
5
6
7
  // 闭包的完整写法
let sayHiToSomeBody:(String) -> Void = {
(name: String) -> Void in
print("Hi \(name)")
}
sayHiToSomeBody("Mike")
// Print: Hi Mike
  • 没有返回值得闭包
1
2
3
4
5
6
// 由于这个闭包没有返回值,所以Void可以省略
let sayHiToSomeBodyWithoutReturnValue:(String) -> Void = {
(name: String) in
print("Hi \(name)")
}
sayHiToSomeBodyWithoutReturnValue("Jimme")
  • 没有参数没有返回值得闭包
1
2
3
4
5
6
// 没有参数没有返回值的写法
let sayHiToSomeBodyWithoutParaAndReturnValue:() ->Void = {
print("Hi Wallace")
}
sayHiToSomeBodyWithoutParaAndReturnValue()
// Print: Hi Wallace
  • 闭包做为一个参数
1
2
3
4
5
6
7
8
9
10
11
12
       // 闭包做为一个参数
func functionToRunClosureAsParam(someClosure:() -> ()) ->String{
print("run before the closure")
someClosure()
return "I ran the closure"
}

print(functionToRunClosureAsParam({print("try it now")}))
// Print:
// run before the closure
// try it now
// I ran the closure
  • 闭包做为函数的返回值
1
2
3
4
5
6
7
8
9

// 闭包做为函数的返回值
func makeFunctionWithClosureToReturn(somePara:String) ->() -> (){
let resultClosure = { print("\(somePara)")}
return resultClosure
}
let yellSomeString = makeFunctionWithClosureToReturn("I Love Swift")
yellSomeString()
// Print: I Love Swift
  • 捕获值,(在闭包内部捕获闭包所在的上下文(surrounding context)的变量)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 捕获值(这里也是一个以闭包做为返回值的函数)
func someFuncWithClosureReturn(forIncrement amount: Int) -> () ->Int{

var runningTotal = 0
// 这个闭包是没有参数,只有返回值,这个返回值是由someFuncWithClosureReturn()的参数,和内部的变量
let incrementer:()->Int = {
runningTotal += amount
return runningTotal
}
// 函数其实也是闭包的一种,所以可以这样写
// func incrementer() ->Int {
//
// runningTotal += amount;
// return runningTotal
// }
return incrementer
}
let incrementByTen = someFuncWithClosureReturn(forIncrement: 10)
print(incrementByTen()) // Print 10
print(incrementByTen()) // Print 20
print(incrementByTen()) // Print 30
  • 尾随闭包Trailing Closures:如果闭包是函数的最后一个参数,并且闭包比较长,这个时候可以在()的后面写一个{}来做为闭包。上文提到的functionToRunClosureAsParam()就是这样一个例子,所以我们可以使用尾随闭包来简化函数的调用。
1
2
3
4
5
6
7
8
9
//闭包做为函数的最后一个参数
func functionToRunClosureAsParam(someClosure:() -> ()) ->String
print("run before the closure")
someClosure()
return "I ran the closure"
}
// 这里将闭包提出了()
// 而不是上文中的:print(functionToRunClosureAsParam({print("try it now")}))
print(functionToRunClosureAsParam(){print("Try it now ")})

闭包的高级用法

  • Noescape Closures:非逃逸型闭包,如果闭包是函数的一个参数,那么这个闭包只能在函数内部使用;Escape Closures:逃逸型闭包,当闭包做为函数的一个参数的时候,有时候在函数已经执行完,并且已经返回,但是这之后才调用闭包,这种闭包就称为逃逸闭包(Escaping Closure),逃逸型闭包可以用在异步操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 非逃逸型闭包
func someFunctionWithNoescapeClosure(@noescape closure: () -> Void) {
closure()
}

// 逃逸型闭包,如果强制将其改变为非逃逸型的将会报错,因为非逃逸型只能在函数内部使用,而这个闭包却被存在了函数之外的数组中,所以必须是逃逸型的,如果强制改为非逃逸型的,会报错
var completionHandlers: [() -> Void] = [] // 这是一个存贮闭包的数组
func someFunctionWithEscapingClosure(completionHandler: () -> Void) {
completionHandlers.append(completionHandler)
}

// 非逃逸型的闭包如果使用了全局变量,那么可以省去self,下面举例说明
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNoescapeClosure { x = 200 }
}
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// prints "200"

completionHandlers.first?() // 这个时候取出数组中存放的已经逃逸的闭包,然后执行它,就会让 instanc.x变为100
print(instance.x)
// prints "100”

下面在举个例子说明Noescape Closures的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    // 再比如非逃逸型闭包
func doIt(@noescape code: () -> ()) {
/* 我们可以这样做 */
// 仅调用它
code()
// 做为非逃逸型参数传递给另外一个函数
doItMore(code)
// 在另外一个非逃逸的函数中捕获智
doItMore {
code()
}
/* 我们不可以做 */

/*
// 将它赋值为一个需要可逃逸闭包参数的函数
dispatch_async(dispatch_get_main_queue(), code)
// 存储它
let _code:() -> () = code
// 在另一个可逃逸的闭包
let __code = { code() }
*/
}
func doItMore(@noescape code: () -> ()) {}
  • Autoclosures自动闭包:如果函数中的一个闭包参数被@autoclosures修饰,那么这个函数在被调用的时候可以省去花括号,注意如果一个闭包被autoClosures修饰的时候默认是noescape型的,所以在不能用到异步处理中,这是只需要在将@autoclosure变为@autoclosure(escaping)就可以了

下面举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serveCustomer(@autoclosure customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serveCustomer(customersInLine.removeAtIndex(0)) // 这个时候回省去{}

// 再比如一个以闭包做为参数的函数
func f(@autoclosure pred: () -> Bool) {
if pred() {
print("It's true")
}
}
f(2 > 1) // 如果不加@autoclosure,我们不得不f({2 > 1})
// It's true

iOS消应用实例--异常处理

Posted on 2016-05-19

问题来源

最近发现了一个在项目中常用的异常处理工具NullSafe,分析了它的实现原理,不小心发现了一个小Bug,现将其分享出来,关于这篇文章的Demo已经上传至GitHub,看完如有收获,欢迎Star,如有疑问欢迎issue,大家一起学习。在IOS开发中我们可能会遇到下面的情景:服务器给我们返回得某个字段是null,比如someValue:null,这个时候我们利用第三方工具转化之后会得到someValue = <null>,这个时候如果我们判断这个someValue的类型,会看到其为:NSNull。那么问题来了,如果这个someValue是要给控件赋值,比如:someLabel.text = someVlaue,这个时候相当于someLabel.text = nil,显然,是不会有问题的。但是有时候我们可能会给这个貌似是NSString的对象发送消息(因为我们在Model里定义了NSString * someValue),比如:[someValue length]。这个时候由于null这个对象没有这个方法,也就是是说:null 这个对象不能处理这个消息所有就会Crash,让程序闪退。那么我们怎样处理来避免这种Crash呢?我们怎样处理这个消息呢?

OC的消息转发流程

首先我们来看一下NSObject.h中我们不常用到的几个方法,以及它们的含义:

1
2
3
4
5
6
7
8
9
10
11
12
// 判断是否发现了这个method,如果发了,将其添加给该对象,并且返回YES,如果没有返回NO。
+ (BOOL)resolveClassMethod:(SEL)sel;
// 同上类似
+ (BOOL)resolveInstanceMethod:(SEL)sel;
// 这个方法来指定未被识别的消息首先要指向得对象
- (id)forwardingTargetForSelector:(SEL)aSelector;
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector;
// 遍历一个类的实例方法得到这个消息得NSMethodSignature,这个返回值包含了对这个method相关的描述,如果这个method不能找到,那么返回nil.
//遍历一个类的实例方法或者类方法来得到这个消息的NSMethodSignature
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// NSObject的子类可以覆盖这个方法来讲消息转发给其它的对象。当一个对象发送一个消息,但是这个对象不能响应这个消息,那么RunTime会给这个对象一个来转发这个消息的机会。
- (void)forwardInvocation:(NSInvocation *)anInvocation;

这个对象通过创建一个NSInvocation对象作为参数来调用这个forwardInvocation方法,然后改对象会调用这个方法来将消息转发给其它对象。
这个NSInvocation对象其实是有上一个方法methodSignatureForSelector中返回的NSMethodSignature来得到的,所以在重写这个方法之前我们必须重写methodSignatureForSelector方法。

1
2
3
4
5
6
7
8
9
// 这个类的实例是否具有相应这个selector的能力,也就是说这个类有没有这样一个方法
+ (BOOL)instancesRespondToSelector:(SEL)aSelector;
// 遍历一个类的实例方法或者类方法 得到这个方法的IMP(函数指针,指向这个方法的具体实现)
- (IMP)methodForSelector:(SEL)aSelector;
// 遍历一个类的实例方法列表,得到这个方法IMP
+ (IMP)instanceMethodForSelector:(SEL)aSelector;
// 如果一个对象收到了一个消息,但是它不能处理这个消息,并且这个消息没有被转发,那么系统将会调用这个方法。
- (void)doesNotRecognizeSelector:(SEL)aSelector;
同时这个方法会引发一个NSInvalidArgumentException,并且引发error.

那么这几个方法,在系统中是怎样的调用顺序呢?我们来看下图:

IOS中消息处理得流程

从中可以看出在给一个对象发送消息的时候,如果对象没有对应的IML,那么会调用对象所属类的

1
+ (BOOL)resolveInstanceMethod:(SEL)sel

方法,然后看对于这个SEL对象是否可以执行,如果不可以执行则会调用

1
- (id)forwardingTargetForSelector:(SEL)aSelector

来找到一个对象处理这个方法(我们可以返回一个对象,这个对象可以处理这个方法),如果这个方法返回的是nil,那么会调用这个对象的

1
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

这个方法,如果这方法调用之后,仍然没有找到对应的NSMethodSignature,那么会调用:

1
- (void)doesNotRecognizeSelector:(SEL)aSelector

这个方法,并且抛出异常,如果这个时候返回了一个有效的NSMethodSignature,那么会调用

1
- (void)forwardInvocation:(NSInvocation *)anInvocation

到这里消息的处理结束。

那么对于NSNull这个对象,我们如何让它处理一个其自身不能处理的消息呢?这时候,我们肯定会想到,将这个消息传递给其它可以处理的对象。那么问题来了:我们如何能找到这样一个对象呢?这时候我们想到了RunTime,利用RunTime的objc_getClassList方法,我们可以获取整个项目中注册得所有类(只要在项目中添加了这个类文件,无论这个类是否被使用),这个时候我们可以过滤掉用不到的父类,以节约循环得次数,因为子类已经继承了父类的方法,所以具有处理这个消息得能力,之后首先利用上文提到的instancesRespondToSelector来判断这个类是否可以响应这个消息,如果可以响应,那么可以利用上文提到的instanceMethodSignatureForSelector来得到这个NSMethodSignature,并且返回。通过上述分析,系统会调用这个forwardInvocation,这个是时候我们调用NSInvocation的invokeWithTarget这个方法来将这个消息发送给nil,在OC中向一个nil发送任何消息都不会引起程序Crash,至此一个由于服务器返回数据异常而导致的Crash被解决了。

这显然增加了系统的容错能力,在项目调试阶段,可能由于数据不完善,所以可以利用这个方法来规避Crash,但是在数据基本完善之后,我们可以去掉这种方法以便我们在程序Crash的时候,及时提醒后台人员来完善数据。

NullSafe的实现详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
 @implementation NSNull (NullSafe)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
@synchronized([self class])
{
// 寻找 method signature
NSMethodSignature *signature = [super methodSignatureForSelector:selector];
if (!signature)
{
//改消息不能被NSNull处理,所以我们要寻找其它的可以处理的类
static NSMutableSet *classList = nil;
static NSMutableDictionary *signatureCache = nil;// 缓存这个找到的 method signature,以便下次寻找
if (signatureCache == nil)
{
classList = [[NSMutableSet alloc] init];
signatureCache = [[NSMutableDictionary alloc] init];

// 获取项目中的所有类,并且去除有子类的类。
// objc_getClassList:这个方法会将所有的类缓存,以及这些类的数量。我们需要提供一块足够大得缓存来存储它们,所以我们必须调用这个函数两次。第一次来判断buffer的大小,第二次来填充这个buffer。
int numClasses = objc_getClassList(NULL, 0);
Class *classes = (Class *)malloc(sizeof(Class) * (unsigned long)numClasses);
numClasses = objc_getClassList(classes, numClasses);

NSMutableSet *excluded = [NSMutableSet set];
for (int i = 0; i < numClasses; i++)
{

Class someClass = classes[i];

// 筛选出其中含有子类的类,加入:excluded中
Class superclass = class_getSuperclass(someClass);
while (superclass)
{
// 如果父类是NSObject,则跳出循环,并且加入classList
if (superclass == [NSObject class]) {
// 将系统中用到的所有类都加到了ClassList中
[classList addObject:someClass];
break;
}

//父类不是NSObject,将其父类添加到excluded
[excluded addObject:superclass];
superclass = class_getSuperclass(superclass);
}
}

// 删除所有含有子类的类
for (Class someClass in excluded)
{
[classList removeObject:someClass];
}

//释放内存
free(classes);
}

// 首先检测缓存是否有这个实现
NSString *selectorString = NSStringFromSelector(selector);
signature = signatureCache[selectorString];
if (!signature)
{
//找到方法的实现
for (Class someClass in classList)
{
if ([someClass instancesRespondToSelector:selector])
{
signature = [someClass instanceMethodSignatureForSelector:selector];
break;
}
}
//缓存以备下次使用
signatureCache[selectorString] = signature ?: [NSNull null];
}
else if ([signature isKindOfClass:[NSNull class]])
{
signature = nil;
}
}
return signature;
}
}
- (void)forwardInvocation:(NSInvocation *)invocation {
// 让nil来处理这个invocation
[invocation invokeWithTarget:nil];
}
@end

在原文中作者是这样写的: [excluded addObject:NSStringFromClass(superclass)];
这样ClassList中存放的是Class,而excluded中存放的确是String,这样就不能过滤掉不必要的类。所以,我将其改为了: [excluded addObject:superclass];不知道作者是不是考虑了其他问题,也可能是由于其大意。

延伸阅读:

  1. http://www.jianshu.com/p/8774e192d8db
  2. http://www.cocoabuilder.com/archive/cocoa/48930-objc-getclasslist-pointers-and-nsarray.html
  3. https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html

iOS开发Tips

Posted on 2016-05-18

在iOS开发中,应用大多以XML或JSON的格式传输数据的,并且XML和JSON通常会比较大,所以客户端需要用下载或者上传的时间会较长,这时我们可以考虑压缩数据,Gzip是一种比zip更优的压缩技术,它可以将数据压缩到60%,因此对客户端和服务器端来说就更加的轻量级了。总结下Gzip的优点:

  1. 降低客户端对数据的下载时间和上传时间
  2. 节省流量**。那么在IOS中我们如何使用gZip呢?

我们可以使用:LFCGzipUtility这个框架来进行,我们可以先讲字符串转换成NSData,然后压缩成NSData:

1
NSData * gZipData = [LFCGzipUtility gzipData:[needCompressedString dataUsingEncoding:NSUTF8StringEncoding]];

解压缩和这个几乎一样,这里就不再赘述

1.iOS开发中,由于版本经常更新,为适配新的版本我们通常要相应的更新Xcode,通常我们可以在iTunes上直接更新,但是由于网速的问题,一般会非常之慢。这时候我们可能会选择在网上找一个安装包,由于前段时间在网上看到有些Xcode有病毒,所以最好用官方的版本。这里提供了官方的下载地址,要比在iTunes上更新快很多的。打开这个网址:
https://developer.apple.com/downloads/

详细得步骤如下所示:
官网下载各种版本Xcode详解

2.当我们的Xcode升级了,这时候很多第三方插件都不可用了,这时候怎么办?

这个时候我们需要给plist文件加一个键值对就可以了,找到plist止呕中的** DVTPlugInCompatibilityUUIDs**新加一个item,这个item的value值可以在终端中执行:defaults read /Applications/Xcode.app/Contents/Info DVTPlugInCompatibilityUUID来进行获取,如下图所示:
给plist添加DVTPlugInCompatibilityUUIDs

3.要发布新版本了,忽然发现This certificate has an invalid issuer,这时候我们一般这么办:

  • 下载这个证书,并且双击安装到Keychain。
    https://developer.apple.com/certificationauthority/AppleWWDRCA.cer
  • 在KeyChain中选中”View”->”Show Expired Certificates”
  • 确保”Certificates”是选中的。如下图所示:

选中Certificates

  • 从”login”和’’system”选项中,移除和苹果开发者相关的证书即可。该问题在Stackoverflow中有详细的说明。

GitHub上LFCGzipUtility的下载地址:
https://github.com/levinXiao/LFCGzipUtility

实例化讲解RunLoop

Posted on 2016-04-25

之前看过很多有关RunLoop的文章,其中要么是主要介绍RunLoop的基本概念,要么是主要讲解RunLoop的底层原理,很少用真正的实例来讲解RunLoop的,这其中有大部分原因是由于大家在项目中很少能用到RunLoop吧。基于这种原因,本文中将用很少的篇幅来对基础内容做以介绍,然后主要利用实例来加深大家对RunLoop的理解,本文中的代码已经上传GitHub,大家可以下载查看,有问题欢迎Issue我。本文主要分为如下几个部分:

  • RunLoop的基础知识
  • 初识RunLoop,如何让RunLoop进驻线程
  • 深入理解Perform Selector
  • 一直”活着”的后台线程
  • 深入理解NSTimer
  • 让两个后台线程有依赖性的一种方式
  • NSURLConnetction的内部实现
  • AFNetWorking中是如何使用RunLoop的?
  • 其它:利用GCD实现定时器功能
  • 延伸阅读

RunLoop的基本概念

什么是RunLoop?提到RunLoop,我们一般都会提到线程,这是为什么呢?先来看下官方对RunLoop的定义:RunLoop系统中和线程相关的基础架构的组成部分(和线程相关),一个RunLoop是一个事件处理环,系统利用这个事件处理环来安排事务,协调输入的各种事件。RunLoop的目的是让你的线程在有工作的时候忙碌,没有工作的时候休眠(和线程相关)。可能这样说你还不是特别清楚RunLoop究竟是用来做什么的,打个比方来说明:我们把线程比作一辆跑车,把这辆跑车的主人比作RunLoop,那么在没有’主人’的时候,这个跑车的生命是直线型的,其启动,运行完之后就会废弃(没有人对其进行控制,’撞坏’被收回),当有了RunLoop这个主人之后,‘线程’这辆跑车的生命就有了保障,这个时候,跑车的生命是环形的,并且在主人有比赛任务的时候就会被RunLoop这个主人所唤醒,在没有任务的时候可以休眠(在IOS中,开启线程是很消耗性能的,开启主线程要消耗1M内存,开启一个后台线程需要消耗512k内存,我们应当在线程没有任务的时候休眠,来释放所占用的资源,以便CPU进行更加高效的工作),这样可以增加跑车的效率,也就是说RunLoop是为线程所服务的。这个例子有点不是很贴切,线程和RunLoop之间是以键值对的形式一一对应的,其中key是thread,value是runLoop(这点可以从苹果公开的源码中看出来),其实RunLoop是管理线程的一种机制,这种机制不仅在IOS上有,在Node.js中的EventLoop,Android中的Looper,都有类似的模式。刚才所说的比赛任务就是唤醒跑车这个线程的一个source;RunLoop Mode就是,一系列输入的source,timer以及observer,RunLoop Mode包含以下几种: NSDefaultRunLoopMode,NSEventTrackingRunLoopMode,UIInitializationRunLoopMode,NSRunLoopCommonModes,NSConnectionReplyMode,NSModalPanelRunLoopMode,至于这些mode各自的含义,读者可自己查询,网上不乏这类资源;

初识RunLoop,如何让RunLoop进驻线程

我们在主线程中添加如下代码:

1
2
3
4
5
6
7
8
while (1) {
NSLog(@"while begin");
// the thread be blocked here
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// this will not be executed
NSLog(@"while end");
}

这个时候我们可以看到主线程在执行完[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; 之后被阻塞而没有执行下面的NSLog(@"while end");同时,我们利用GCD,将这段代码放到一个后台线程中:

1
2
3
4
5
6
7
8
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
NSLog(@"while begin");
NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
[subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"while end");
}
});

这个时候我们发现这个while循环会一直在执行;这是为什么呢?我们先将这两个RunLoop分别打印出来:
主线程的RunLoop
由于这个日志比较长,我就只截取了上面的一部分。
我们再看我们新建的子线程中的RunLoop,打印出来之后:
backGroundThreadRunLoop.png
从中可以看出来:我们新建的线程中:

1
2
3
4
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null),

我们看到虽然有Mode,但是我们没有给它soures,observer,timer,其实Mode中的这些source,observer,timer,统称为这个Mode的item,如果一个Mode中一个item都没有,则这个RunLoop会直接退出,不进入循环(其实线程之所以可以一直存在就是由于RunLoop将其带入了这个循环中)。下面我们为这个RunLoop添加个source:

1
2
3
4
5
6
7
8
9
10
11
12
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

while (1) {
NSPort *macPort = [NSPort port];
NSLog(@"while begin");
NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
[subRunLoop addPort:macPort forMode:NSDefaultRunLoopMode];
[subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"while end");
NSLog(@"%@",subRunLoop);
}
});

这样我们可以看到能够实现了和主线程中相同的效果,线程在这个地方暂停了,为什么呢?我们明天让RunLoop在distantFuture之前都一直run的啊?相信大家已经猜出出来了。这个时候线程被RunLoop带到‘坑’里去了,这个‘坑’就是一个循环,在循环中这个线程可以在没有任务的时候休眠,在有任务的时候被唤醒;当然我们只用一个while(1)也可以让这个线程一直存在,但是这个线程会一直在唤醒状态,及时它没有任务也一直处于运转状态,这对于CPU来说是非常不高效的。
小结:我们的RunLoop要想工作,必须要让它存在一个Item(source,observer或者timer),主线程之所以能够一直存在,并且随时准备被唤醒就是应为系统为其添加了很多Item

深入理解Perform Selector

我们先在主线程中使用下performselector:

1
2
3
4
5
6
7
- (void)tryPerformSelectorOnMianThread{

[self performSelector:@selector(mainThreadMethod) withObject:nil]; }
- (void)mainThreadMethod{
NSLog(@"execute %s",__func__);
// print: execute -[ViewController mainThreadMethod]
}

这样我们在ViewDidLoad中调用tryPerformSelectorOnMianThread,就会立即执行,并且输出:print: execute -[ViewController mainThreadMethod];
和上面的例子一样,我们使用GCD,让这个方法在后台线程中执行:

1
2
3
4
5
6
7
8
9
 - (void)tryPerformSelectorOnBackGroundThread {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
});
}
- (void)backGroundThread{
NSLog(@"%u",[NSThread isMainThread]);
NSLog(@"execute %s",__FUNCTION__);
}

同样的,我们调用tryPerformSelectorOnBackGroundThread这个方法,我们会发现,下面的backGroundThread不会被调用,这是什么原因呢?
这是因为,在调用performSelector:onThread: withObject: waitUntilDone的时候,系统会给我们创建一个Timer的source,加到对应的RunLoop上去,然而这个时候我们没有RunLoop,如果我们加上RunLoop:

1
2
3
4
5
6
7
 - (void)tryPerformSelectorOnBackGroundThread{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
});
}

这时就会发现我们的方法正常被调用了。那么为什么主线程中的perfom selector却能够正常调用呢?通过上面的例子相信你已经猜到了,主线程的RunLoop是一直存在的,所以我们在主线程中执行的时候,无需再添加RunLoop。从Apple的文档中我们也可以得到验证:

Each request to perform a selector is queued on the target thread’s run loop and the requests are then processed sequentially in the order in which they were received. 每个执行perform selector的请求都以队列的形式被放到目标线程的run loop中。然后目标线程会根据进入run loop的顺序来一一执行。
当perform selector在后台线程中执行的时候,这个线程必须有一个开启的runLoop

一直”活着”的后台线程

现在有这样一个需求,每点击一下屏幕,让子线程做一个任务,然后大家一般会想到这样的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface ViewController ()
@property(nonatomic,strong) NSThread *myThread;
@end

@implementation ViewController
- (void)alwaysLiveBackGoundThread {
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(myThreadRun) object:@"etund"];
self.myThread = thread;
[self.myThread start];
}
- (void)myThreadRun {
NSLog(@"my thread run");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%@",self.myThread);
[self performSelector:@selector(doBackGroundThreadWork) onThread:self.myThread withObject:nil waitUntilDone:NO];
}
- (void)doBackGroundThreadWork{
NSLog(@"do some work %s",__FUNCTION__);
}
@end

这个方法中,我们利用一个强引用来获取了后台线程中的thread,然后在点击屏幕的时候,在这个线程上执行doBackGroundThreadWork这个方法,此时我们可以看到,在touchesBegin方法中,self.myThread是存在的,但是这是为是什么呢?这就要从线程的五大状态来说明了:新建状态、就绪状态、运行状态、阻塞状态、死亡状态,这个时候尽管内存中还有线程,但是这个线程在执行完任务之后已经死亡了,经过上面的论述,我们应该怎样处理呢?我们可以给这个线程的RunLoop添加一个source,那么这个线程就会检测这个source等待执行,而不至于死亡(有工作的强烈愿望而不死亡):

1
2
3
4
5
6
 - (void)myThreadRun{

[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run]
NSLog(@"my thread run");
}

这个时候再次点击屏幕,我们就会发现,后台线程中执行的任务可以正常进行了。

正常情况下,后台线程执行完任务之后就处于死亡状态,我们要避免这种情况的发生可以利用RunLoop,并且给它一个Source这样来保证线程依旧还在

深入理解NSTimer

我们平时使用NSTimer,一般是在主线程中的,代码大多如下:

1
2
3
4
5
6
7
8
9
- (void)tryTimerOnMainThread{
NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self
selector:@selector(timerAction) userInfo:nil repeats:YES];
[myTimer fire];
}

- (void)timerAction{
NSLog(@"timer action");
}

这个时候代码按照我们预定的结果运行,如果我们把这个Tiemr放到后台线程中呢?

1
2
3
4
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[myTimer fire];
});

这个时候我们会发现,这个timer只执行了一次,就停止了。这是为什么呢?通过上面的讲解,想必你已经知道了,NSTimer,只有注册到RunLoop之后才会生效,这个注册是由系统自动给我们完成的,既然需要注册到RunLoop,那么我们就需要有一个RunLoop,我们在后台线程中加入如下的代码:

1
2
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];

这样我们就会发现程序正常运行了。在Timer注册到RunLoop之后,RunLoop会为其重复的时间点注册好事件,比如1:10,1:20,1:30这几个时间点。有时候我们会在这个线程中执行一个耗时操作,这个时候RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer,这就造成了误差(Timer有个冗余度属性叫做tolerance,它标明了当前点到后,容许有多少最大误差),可以在执行一段循环之后调用一个耗时操作,很容易看到timer会有很大的误差,这说明在线程很闲的时候使用NSTiemr是比较傲你准确的,当线程很忙碌时候会有较大的误差。系统还有一个CADisplayLink,也可以实现定时效果,它是一个和屏幕的刷新率一样的定时器。如果在两次屏幕刷新之间执行一个耗时的任务,那其中就会有一个帧被跳过去,造成界面卡顿。另外GCD也可以实现定时器的效果,由于其和RunLoop没有关联,所以有时候使用它会更加的准确,这在最后会给予说明。

让两个后台线程有依赖性的一种方式

给两个后台线程添加依赖可能有很多的方式,这里说明一种利用RunLoop实现的方式。原理很简单,我们先让一个线程工作,当工作完成之后唤醒另外的一线程,通过上面对RunLoop的说明,相信大家很容易能够理解这些代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- (void)runLoopAddDependance{
self.runLoopThreadDidFinishFlag = NO;
NSLog(@"Start a New Run Loop Thread");
NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];
[runLoopThread start];
NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside");

dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (!_runLoopThreadDidFinishFlag) {

self.myThread = [NSThread currentThread];
NSLog(@"Begin RunLoop");
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSPort *myPort = [NSPort port];
[runLoop addPort:myPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"End RunLoop");
[self.myThread cancel];
self.myThread = nil;
}
});
}
- (void)handleRunLoopThreadTask {

NSLog(@"Enter Run Loop Thread");
for (NSInteger i = 0; i < 5; i ++) {
NSLog(@"In Run Loop Thread, count = %ld", i);
sleep(1);
}

#if 0
// 错误示范
_runLoopThreadDidFinishFlag = YES;
// 这个时候并不能执行线程完成之后的任务,因为Run Loop所在的线程并不知道runLoopThreadDidFinishFlag被重新赋值。Run Loop这个时候没有被任务事件源唤醒。
// 正确的做法是使用 "selector"方法唤醒Run Loop。 即如下:
#endif
NSLog(@"Exit Normal Thread");
[self performSelector:@selector(tryOnMyThread) onThread:self.myThread withObject:nil waitUntilDone:NO];
// NSLog(@"Exit Run Loop Thread");
}

NSURLConnection的执行过程

在使用NSURLConnection时,我们会传入一个Delegate,当我们调用了[connection start]之后,这个Delegate会不停的收到事件的回调。实际上,start这个函数的内部会获取CurrentRunloop,然后在其中的DefaultMode中添加4个source。如下图所示,CFMultiplexerSource是负责各种Delegate回调的,CFHTTPCookieStorage是处理各种Cookie的。如下图所示:
NSURLConnection的执行过程
从中可以看出,当开始网络传输是,我们可以看到NSURLConnection创建了两个新的线程:com.apple.NSURLConnectionLoader和com.apple.CFSocket.private。其中CFSocket是处理底层socket链接的。NSURLConnectionLoader这个线程内部会使用RunLoop来接收底层socket的事件,并通过之前添加的source,来通知(唤醒)上层的Delegate。这样我们就可以理解我们平时封装网络请求时候常见的下面逻辑了:

1
2
3
4
5
6
7
8
9
10
11
while (!_isEndRequest)
{
NSLog(@"entered run loop");
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"main finished,task be removed");
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
_isEndRequest = YES;

}

这里我们就可以解决下面这些疑问了:

  1. 为什么这个While循环不停的执行,还需要使用一个RunLoop? 程序执行一个while循环是不会耗费很大性能的,我们这里的目的是想让子线程在有任务的时候处理任务,没有任务的时候休眠,来节约CPU的开支。
  2. 如果没有为RunLoop添加item,那么它就会立即退出,这里的item呢? 其实系统已经给我们默认添加了4个source了。
  3. 既然[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];让线程在这里停下来,那么为什么这个循环会持续的执行呢?因为这个一直在处理任务,并且接受系统对这个Delegate的回调,也就是这个回调唤醒了这个线程,让它在这里循环。

AFNetWorking中是如何使用RunLoop的 ?

在AFN中AFURLConnectionOperation是基于NSURLConnection构建的,其希望能够在后台线程来接收Delegate的回调。
为此AFN创建了一个线程,然后在里面开启了一个RunLoop,然后添加item

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}

+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}

这里这个NSMachPort的作用和上文中的一样,就是让线程不至于在很快死亡,然后RunLoop不至于退出(如果要使用这个MachPort的话,调用者需要持有这个NSMachPort,然后在外部线程通过这个port发送信息到这个loop内部,它这里没有这么做)。然后和上面的做法相似,在需要后台执行这个任务的时候,会通过调用:[NSObject performSelector:onThread:..]来将这个任务扔给后台线程的RunLoop中来执行。

1
2
3
4
5
6
7
8
9
10
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}

GCD定时器的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 - (void)gcdTimer{

// get the queue
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// creat timer
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// config the timer (starting time,interval)
// set begining time
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
// set the interval
uint64_t interver = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interver, 0.0);
dispatch_source_set_event_handler(self.timer, ^{
// the tarsk needed to be processed async
dispatch_async(dispatch_get_global_queue(0, 0), ^{

for (int i = 0; i < 100000; i++) {
NSLog(@"gcdTimer");
}
});
});

dispatch_resume(self.timer);
}

延伸阅读

  1. http://chun.tips/blog/2014/10/20/zou-jin-run-loopde-shi-jie-%5B%3F%5D-:shi-yao-shi-run-loop%3F/
  2. https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
  3. http://www.cocoachina.com/ios/20150601/11970.html
  4. http://www.jianshu.com/p/de2716807570
  5. http://blog.csdn.net/enuola/article/details/9163051

Mac版OBS设置详解

Posted on 2016-03-17

OBS是什么?

OBS是目前为止,最好用的直播软件,它支持Windows 7/8/10, Linux并且还支持OS X(Mac电脑的系统),老外的软件,无广告,全免费,适用于32和64位的各种电脑,所以成为斗鱼,哔哩哔哩等各种直播网站主播的必备品。

怎样使用OBS?

  1. 下载安装

进入OBS官方网站,然后点击绿色的OSX 10.8+(或者是其它的版本),下载安装,然后你会看到如下界面OBS主页面

  1. 点击设置,进入如下界面

(http://upload-images.jianshu.io/upload_images/1513759-56330d89c8171b75.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

通用中可以设置OBS的语言,点击串流会看到如下界面:
通用设置,串流类型选择自定义流媒体服务器,下面的URL和流密钥需要根据直播间中的直播信息进行填写,登陆斗鱼账号,点击用户名—>个人中心—>主播相关—>直播设置—>进入直播房间

然后可以看到下图:
我的直播房间

  1. 这个时候点击获取推流码,即可看到rtmp地址和直播码,将其填入OBS中串流的设置中。

  2. 这个时候OBS的主页面还是黑色的,没有任何的输入,原因是没有给他添加输入源,这是点击场景下面的*+添加一个场景,点击来源下面的+*添加一个来源,一般我们会选择视频捕获或者只窗口捕获 视频捕获是直播电脑摄像头录取的视频,窗口捕获是直播电脑上打开的窗口,这里以窗口捕获为例,添加完窗口捕获之后,点击窗口捕获下面的小齿轮:
    设置窗口
    然后选择需要直播的窗口,这个时候可能里面没有我们要直播的窗口,比如Xcode,这时,可能是我们没有打开Xcode,打开Xcode之后,我们再,次打开OBS,让其重新识别一次窗口,这时就可以选择Xcode了,选择完之后,点击确定。

  3. 这个时候点击开始串流,并且打开直播房间的开始直播就可以了。

其它:

Mac电脑使用OBS的时候往往会遇到这样一个问题,外界的声音可以录取,但是电脑自身发出的声音,比如音乐,某些网站的声音,是不能直播,这时候我们需要下载一个软件,soundflower,下载之后,进入系统偏好设置->声音–>输出,然后选中Multi-Output Device
系统设置,然后点击OB中的视频,将音频中的桌面音频设备设置为Soundflower(2ch)
OBS设置Sondflower
如果噪音太大,这个时候可以点击麦克风下面的齿轮,设置噪音阈值,来对噪音进行过滤。

1…56
击水湘江

击水湘江

努力让明天的自己爱上今天的自己!

56 posts
10 tags
© 2019 击水湘江
Powered by Hexo
|
Theme — NexT.Muse v6.0.3