你是否曾经苦恼于理解你的代码,而去尝试打印一个变量的值?

NSLog(@"%@", whatIsInsideThisThing);

或者跳过一个函数调用来简化程序的行为?

NSNumber *n = @7; // 实际应该调用这个函数:Foo();

或者短路一个逻辑检查?

if (1 || theBooleanAtStake) { ... }

或者伪造一个函数实现?

int calculateTheTrickyValue {
  return 9;

  /*
   先这么着
   ...
}

并且每次必须重新编译,从头开始?

构建软件是复杂的,并且 Bug 总会出现。一个常见的修复周期就是修改代码,编译,重新运行,并且祈祷出现最好的结果。

但是不一定要这么做。你可以使用调试器。而且即使你已经知道如何使用调试器检查变量,它可以做的还有很多。

这篇文章将试图挑战你对调试的认知,并详细地解释一些你可能还不了解的基本原理,然后展示一系列有趣的例子。现在就让我们开始与调试器共舞一曲华尔兹,看看最后能达到怎样的高度。

LLDB

LLDB 是一个有着 REPL 的特性和 C++ ,Python 插件的开源调试器。LLDB 绑定在 Xcode 内部,存在于主窗口底部的控制台中。调试器允许你在程序运行的特定时暂停它,你可以查看变量的值,执行自定的指令,并且按照你所认为合适的步骤来操作程序的进展。(这里有一个关于调试器如何工作的总体的解释。)

你以前有可能已经使用过调试器,即使只是在 Xcode 的界面上加一些断点。但是通过一些小的技巧,你就可以做一些非常酷的事情。GDB to LLDB 参考是一个非常好的调试器可用命令的总览。你也可以安装 Chisel,它是一个开源的 LLDB 插件合辑,这会使调试变得更加有趣。

与此同时,让我们以在调试器中打印变量来开始我们的旅程吧。

基础

这里有一个简单的小程序,它会打印一个字符串。注意断点已经被加在第 8 行。断点可以通过点击 Xcode 的源码窗口的侧边槽进行创建。

程序会在这一行停止运行,并且控制台会被打开,允许我们和调试器交互。那我们应该打些什么呢?

help

最简单命令是 help,它会列举出所有的命令。如果你忘记了一个命令是做什么的,或者想知道更多的话,你可以通过 help <command> 来了解更多细节,例如 help print 或者 help thread。如果你甚至忘记了 help 命令是做什么的,你可以试试 help help。不过你如果知道这么做,那就说明你大概还没有忘光这个命令。😛

print

打印值很简单;只要试试 print 命令:

LLDB 实际上会作前缀匹配。所以你也可以使用 prinpri,或者 p。但你不能使用 pr,因为 LLDB 不能消除和 process 的歧义 (幸运的是 p 并没有歧义)。

你可能还注意到了,结果中有个 $0。实际上你可以使用它来指向这个结果。试试 print $0 + 7,你会看到 106。任何以美元符开头的东西都是存在于 LLDB 的命名空间的,它们是为了帮助你进行调试而存在的。

expression

如果想改变一个值怎么办?你或许会猜 modify。其实这时候我们要用到的是 expression 这个方便的命令。

这不仅会改变调试器中的值,实际上它改变了程序中的值。这时候继续执行程序,将会打印 42 red balloons。神奇吧。

注意,从现在开始,我们将会偷懒分别以 pe 来代替 printexpression

什么是 print 命令

考虑一个有意思的表达式:p count = 18。如果我们运行这条命令,然后打印 count 的内容。我们将看到它的结果与 expression count = 18 一样。

expression 不同的是,print 命令不需要参数。比如 e -h +17 中,你很难区分到底是以 -h 为标识,仅仅执行 +17 呢,还是要计算 17h 的差值。连字符号确实很让人困惑,你或许得不到自己想要的结果。

幸运的是,解决方案很简单。用 来表征标识的结束,以及输入的开始。如果想要 -h 作为标识,就用 e -h – +17,如果想计算它们的差值,就使用 e – -h +17。因为一般来说不使用标识的情况比较多,所以 e – 就有了一个简写的方式,那就是 print

输入 help print,然后向下滚动,你会发现:

'print' is an abbreviation for 'expression --'.   
(print是 `expression --` 的缩写)

打印对象

尝试输入

p objects

输出会有点啰嗦

(NSString *) $7 = 0x0000000104da4040 @"red balloons"

如果我们尝试打印结构更复杂的对象,结果甚至会更糟

(lldb) p @[ @"foo", @"bar" ]

(NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects" 

实际上,我们想看的是对象的 description 方法的结果。我么需要使用 -O (字母 O,而不是数字 0) 标志告诉 expression 命令以 对象 (Object) 的方式来打印结果。

(lldb) e -O -- $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)

幸运的是,e -o – 有也有个别名,那就是 po (print object 的缩写),我们可以使用它来进行简化:

(lldb) po $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)
(lldb) po @"lunar"
lunar
(lldb) p @"lunar"
(NSString *) $13 = 0x00007fdb9d0003b0 @"lunar"

打印变量

可以给 print 指定不同的打印格式。它们都是以 print/<fmt> 或者简化的 p/<fmt> 格式书写。下面是一些例子:

默认的格式

(lldb) p 16
16

十六进制:

(lldb) p/x 16
0x10

二进制 (t 代表 two):

(lldb) p/t 16
0b00000000000000000000000000010000
(lldb) p/t (char)16
0b00010000

你也可以使用 p/c 打印字符,或者 p/s 打印以空终止的字符串 (译者注:以 ‘\0’ 结尾的字符串)。

这里是格式的完整清单。

变量

现在你已经可以打印对象和简单类型,并且知道如何使用 expression 命令在调试器中修改它们了。现在让我们使用一些变量来减少输入量。就像你可以在 C 语言中用 int a = 0 来声明一个变量一样,你也可以在 LLDB 中做同样的事情。不过为了能使用声明的变量,变量必须以美元符开头。

(lldb) e int $a = 2
(lldb) p $a * 19
38
(lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday" ]
(lldb) p [$array count]
2
(lldb) po [[$array objectAtIndex:0] uppercaseString]
SATURDAY
(lldb) p [[$array objectAtIndex:$a] characterAtIndex:0]
error: no known method '-characterAtIndex:'; cast the message send to the method's return type
error: 1 errors parsing expression

悲剧了,LLDB 无法确定涉及的类型 (译者注:返回的类型)。这种事情常常发生,给个说明就好了:

(lldb) p (char)[[$array objectAtIndex:$a] characterAtIndex:0]
'M'
(lldb) p/d (char)[[$array objectAtIndex:$a] characterAtIndex:0]
77

变量使调试器变的容易使用得多,想不到吧?😉

流程控制

当你通过 Xcode 的源码编辑器的侧边槽 (或者通过下面的方法) 插入一个断点,程序到达断点时会就会停止运行。

调试条上会出现四个你可以用来控制程序的执行流程的按钮。

从左到右,四个按钮分别是:continue,step over,step into,step out。

第一个,continue 按钮,会取消程序的暂停,允许程序正常执行 (要么一直执行下去,要么到达下一个断点)。在 LLDB 中,你可以使用 process continue 命令来达到同样的效果,它的别名为 continue,或者也可以缩写为 c

第二个,step over 按钮,会以黑盒的方式执行一行代码。如果所在这行代码是一个函数调用,那么就不会跳进这个函数,而是会执行这个函数,然后继续。LLDB 则可以使用 thread step-overnext,或者 n 命令。

如果你确实想跳进一个函数调用来调试或者检查程序的执行情况,那就用第三个按钮,step in,或者在LLDB中使用 thread step instep,或者 s 命令。注意,当前行不是函数调用时,nextstep 效果是一样的。

大多数人知道 cns,但是其实还有第四个按钮,step out。如果你曾经不小心跳进一个函数,但实际上你想跳过它,常见的反应是重复的运行 n 直到函数返回。其实这种情况,step out 按钮是你的救世主。它会继续执行到下一个返回语句 (直到一个堆栈帧结束) 然后再次停止。

例子

考虑下面一段程序:

假如我们运行程序,让它停止在断点,然后执行下面一些列命令:

p i
n
s
p i
finish
p i
frame info

这里,frame info 会告诉你当前的行数和源码文件,以及其他一些信息;查看 help framehelp threadhelp process 来获得更多信息。这一串命令的结果会是什么?看答案之前请先想一想。

(lldb) p i
(int) $0 = 99
(lldb) n
2014-11-22 10:49:26.445 DebuggerDance[60182:4832768] 101 is odd!
(lldb) s
(lldb) p i
(int) $2 = 110
(lldb) finish
2014-11-22 10:49:35.978 DebuggerDance[60182:4832768] 110 is even!
(lldb) p i
(int) $4 = 99
(lldb) frame info
frame #0: 0x000000010a53bcd4 DebuggerDance`main + 68 at main.m:17

它始终在 17 行的原因是 finish 命令一直运行到 isEven() 函数的 return,然后立刻停止。注意即使它还在 17 行,其实这行已经被执行过了。

Thread Return

调试时,还有一个很棒的函数可以用来控制程序流程:thread return 。它有一个可选参数,在执行时它会把可选参数加载进返回寄存器里,然后立刻执行返回命令,跳出当前栈帧。这意味这函数剩余的部分不会被执行。这会给 ARC 的引用计数造成一些问题,或者会使函数内的清理部分失效。但是在函数的开头执行这个命令,是个非常好的隔离这个函数,伪造返回值的方式 。

让我们稍微修改一下上面代码段并运行:

p i
s
thread return NO
n
p even0
frame info

看答案前思考一下。下面是答案:

(lldb) p i
(int) $0 = 99
(lldb) s
(lldb) thread return NO
(lldb) n
(lldb) p even0
(BOOL) $2 = NO
(lldb) frame info
frame #0: 0x00000001009a5cc4 DebuggerDance`main + 52 at main.m:17

断点

我们都把断点作为一个停止程序运行,检查当前状态,追踪 bug 的方式。但是如果我们改变和断点交互的方式,很多事情都变成可能。


断点允许控制程序什么时候停止,然后允许命令的运行。


想象把断点放在函数的开头,然后用 thread return 命令重写函数的行为,然后继续。想象一下让这个过程自动化,听起来不错,不是吗?

管理断点

Xcode 提供了一系列工具来创建和管理断点。我们会一个个看过来并介绍 LLDB 中等价的命令 (是的,你可以在调试器内部添加断点)。

在 Xcode 的左侧面板,有一组按钮。其中一个看起来像断点。点击它打开断点导航,这是一个可以快速管理所有断点的面板。

在这里你可以看到所有的断点 - 在 LLDB 中通过 breakpoint list (或者 br li) 命令也做同样的事儿。你也可以点击单个断点来开启或关闭 - 在 LLDB 中使用 breakpoint enable <breakpointID>breakpoint disable <breakpointID>

(lldb) br li
Current breakpoints:
1: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line = 16, locations = 1, resolved = 1, hit count = 1

  1.1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab, resolved, hit count = 1

(lldb) br dis 1
1 breakpoints disabled.
(lldb) br li
Current breakpoints:
1: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line = 16, locations = 1 Options: disabled

  1.1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab, unresolved, hit count = 1

(lldb) br del 1
1 breakpoints deleted; 0 breakpoint locations disabled.
(lldb) br li
No breakpoints currently set.

创建断点

在上面的例子中,我们通过在源码页面器的滚槽 16 上点击来创建断点。你可以通过把断点拖拽出滚槽,然后释放鼠标来删除断点 (消失时会有一个非常可爱的噗的一下的动画)。你也可以在断点导航页选择断点,然后按下删除键删除。

要在调试器中创建断点,可以使用 breakpoint set 命令。

(lldb) breakpoint set -f main.m -l 16
Breakpoint 1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab

也可以使用缩写形式 br。虽然 b 是一个完全不同的命令 (_regexp-break 的缩写),但恰好也可以实现和上面同样的效果。

(lldb) b main.m:17
Breakpoint 2: where = DebuggerDance`main + 52 at main.m:17, address = 0x000000010a3f6cc4

也可以在一个符号 (C 语言函数) 上创建断点,而完全不用指定哪一行

(lldb) b isEven
Breakpoint 3: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00
(lldb) br s -F isEven
Breakpoint 4: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00

这些断点会准确的停止在函数的开始。Objective-C 的方法也完全可以:

(lldb) breakpoint set -F "-[NSArray objectAtIndex:]"
Breakpoint 5: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950
(lldb) b -[NSArray objectAtIndex:]
Breakpoint 6: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950
(lldb) breakpoint set -F "+[NSSet setWithObject:]"
Breakpoint 7: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820
(lldb) b +[NSSet setWithObject:]
Breakpoint 8: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820

如果想在 Xcode 的UI上创建符号断点,你可以点击断点栏左侧的 + 按钮。

然后选择第三个选项:

这时会出现一个弹出框,你可以在里面添加例如 -[NSArray objectAtIndex:] 这样的符号断点。这样每次调用这个函数的时候,程序都会停止,不管是你调用还是苹果调用。

如果你 Xcode 的 UI 上右击任意断点,然后选择 “Edit Breakpoint” 的话,会有一些非常诱人的选择。

这里,断点已经被修改为只有i99 的时候才会停止。你也可以使用 “ignore” 选项来告诉断点最初的 n 次调用 (并且条件为真的时候) 的时候不要停止。

接下来介绍 ‘Add Action’ 按钮…

断点行为 (Action)

上面的例子中,你或许想知道每一次到达断点的时候 i 的值。我们可以使用 p i 作为断点行为。这样每次到达断点的时候,都会自动运行这个命令。

你也可以添加多个行为,可以是调试器命令,shell 命令,也可以是更直接的打印:

可以看到它打印 i,然后大声念出那个句子,接着打印了自定义的表达式。

下面是在 LLDB 而不是 Xcode 的 UI 中做这些的时候,看起来的样子。

(lldb) breakpoint set -F isEven
Breakpoint 1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00
(lldb) breakpoint modify -c 'i == 99' 1
(lldb) breakpoint command add 1
Enter your debugger command(s).  Type 'DONE' to end.
> p i
> DONE
(lldb) br li 1
1: name = 'isEven', locations = 1, resolved = 1, hit count = 0
    Breakpoint commands:
      p i

Condition: i == 99

  1.1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0 

接下来说说自动化。

赋值后继续运行

看编辑断点弹出窗口的底部,你还会看到一个选项: “Automatically continue after evaluation actions.” 。它仅仅是一个选择框,但是却很强大。选中它,调试器会运行你所有的命令,然后继续运行。看起来就像没有执行任何断点一样 (除非断点太多,运行需要一段时间,拖慢了你的程序)。

这个选项框的效果和让最后断点的最后一个行为是 continue 一样。选框只是让这个操作变得更简单。调试器的输出是:

(lldb) breakpoint set -F isEven
Breakpoint 1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00
(lldb) breakpoint command add 1
Enter your debugger command(s).  Type 'DONE' to end.
> continue
> DONE
(lldb) br li 1
1: name = 'isEven', locations = 1, resolved = 1, hit count = 0
    Breakpoint commands:
      continue

  1.1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0

执行断点后自动继续运行,允许你完全通过断点来修改程序!你可以在某一行停止,运行一个 expression 命令来改变变量,然后继续运行。

例子

想想所谓的”打印调试”技术吧,不要这么做:

NSLog(@"%@", whatIsInsideThisThing);

而是用个打印变量的断点替换 log 语句,然后继续运行。

也不要:

int calculateTheTrickyValue {
  return 9;

  /*
   Figure this out later.
   ...
}

而是加一个使用 thread return 9 命令的断点,然后让它继续运行。

符号断点加上 action 真的很强大。你也可以在你朋友的 Xcode 工程上添加一些断点,并且加上大声朗读某些东西的 action。看看他们要花多久才能弄明白发生了什么。😄

完全在调试器内运行

在开始舞蹈之前,还有一件事要看一看。实际上你可以在调试器中执行任何 C/Objective-C/C++/Swift 的命令。唯一的缺点就是不能创建新函数… 这意味着不能创建新的类,block,函数,有虚拟函数的 C++ 类等等。除此之外,它都可以做。

我们可以申请分配一些字节:

(lldb) e char *$str = (char *)malloc(8)
(lldb) e (void)strcpy($str, "munkeys")
(lldb) e $str[1] = 'o'
(char) $0 = 'o'
(lldb) p $str
(char *) $str = 0x00007fd04a900040 "monkeys"

我们可以查看内存 (使用 x 命令),来看看新数组中的四个字节:

(lldb) x/4c $str
0x7fd04a900040: monk

我们也可以去掉 3 个字节 (x 命令需要斜引号,因为它只有一个内存地址的参数,而不是表达式;使用 help x 来获得更多信息):

(lldb) x/1w `$str + 3`
0x7fd04a900043: keys

做完了之后,一定不要忘了释放内存,这样才不会内存泄露。(哈,虽然这是调试器用到的内存):

(lldb) e (void)free($str)

让我们起舞

现在我们已经知道基本的步调了,是时候开始跳舞并玩一些疯狂的事情了。我曾经写过一篇 NSArray 深度探究的博客。这篇博客用了很多 NSLog 语句,但实际上我的所有探索都是在调试器中完成的。看看你能不能弄明白怎么做的,这会是一个有意思的练习。

不用断点调试

程序运行时,Xcode 的调试条上会出现暂停按钮,而不是继续按钮:

点击按钮会暂停 app (这会运行 process interrupt 命令,因为 LLDB 总是在背后运行)。这会让你可以访问调试器,但看起来可以做的事情不多,因为在当前作用域没有变量,也没有特定的代码让你看。

这就是有意思的地方。如果你正在运行 iOS app,你可以试试这个: (因为全局变量是可访问的)

    (lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
<UIWindow: 0x7f82b1fa8140; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x7f82b1fa92d0>; layer = <UIWindowLayer: 0x7f82b1fa8400>>
   | <UIView: 0x7f82b1d01fd0; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x7f82b1e2e0a0>>

你可以看到整个层次。Chiselpviews 就是这么实现的。

更新UI

有了上面的输出,我们可以获取这个 view:

(lldb) e id $myView = (id)0x7f82b1d01fd0

然后在调试器中改变它的背景色:

(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]

但是只有程序继续运行之后才会看到界面的变化。因为改变的内容必须被发送到渲染服务中,然后显示才会被更新。

渲染服务实际上是一个另外的进程 (被称作 backboardd)。这就是说即使我们正在调试的内容所在的进程被打断了,backboardd 也还是继续运行着的。

这意味着你可以运行下面的命令,而不用继续运行程序:

(lldb) e (void)[CATransaction flush]

即使你仍然在调试器中,UI 也会在模拟器或者真机上实时更新。Chisel 为此提供了一个别名叫做 caflush,这个命令被用来实现其他的快捷命令,例如 hide <view>show <view> 以及其他很多命令。所有 Chisel 的命令都有文档,所以安装后随意运行 help show 来看更多信息。

Push 一个 View Controller

想象一个以 UINavigationController 为 root ViewController 的应用。你可以通过下面的命令,轻松地获取它:

(lldb) e id $nvc = [[[UIApplication sharedApplication] keyWindow] rootViewController]

然后 push 一个 child view controller:

(lldb) e id $vc = [UIViewController new]
(lldb) e (void)[[$vc view] setBackgroundColor:[UIColor yellowColor]]
(lldb) e (void)[$vc setTitle:@"Yay!"]
(lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]

最后运行下面的命令:

(lldb) caflush // e (void)[CATransaction flush]

navigation Controller 就会立刻就被 push 到你眼前。

查找按钮的 target

想象你在调试器中有一个 $myButton 的变量,可以是创建出来的,也可以是从 UI 上抓取出来的,或者是你停止在断点时的一个局部变量。你想知道,按钮按下的时候谁会接收到按钮发出的 action。非常简单:

(lldb) po [$myButton allTargets]
{(
    <MagicEventListener: 0x7fb58bd2e240>
)}
(lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0]
<__NSArrayM 0x7fb58bd2aa40>(
_handleTap:
)

现在你或许想在它发生的时候加一个断点。在 -[MagicEventListener _handleTap:] 设置一个符号断点就可以了,在 Xcode 和 LLDB 中都可以,然后你就可以点击按钮并停在你所希望的地方了。

观察实例变量的变化

假设你有一个 UIView,不知道为什么它的 _layer 实例变量被重写了 (糟糕)。因为有可能并不涉及到方法,我们不能使用符号断点。相反的,我们想监视什么时候这个地址被写入。

首先,我们需要找到 _layer 这个变量在对象上的相对位置:

(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer"))
(ptrdiff_t) $0 = 8

现在我们知道 ($myView + 8) 是被写入的内存地址:

(lldb) watchpoint set expression -- (int *)$myView + 8
Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabled type = w
    new value: 0x0000000000000000

这被以 wivar $myView _layer 加入到 Chisel 中。

非重写方法的符号断点

假设你想知道 -[MyViewController viewDidAppear:] 什么时候被调用。如果这个方法并没有在MyViewController 中实现,而是在其父类中实现的,该怎么办呢?试着设置一个断点,会出现以下结果:

(lldb) b -[MyViewController viewDidAppear:]
Breakpoint 1: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.

因为 LLDB 会查找一个符号,但是实际在这个类上却找不到,所以断点也永远不会触发。你需要做的是为断点设置一个条件 [self isKindOfClass:[MyViewController class]],然后把断点放在 UIViewController 上。正常情况下这样设置一个条件可以正常工作。但是这里不会,因为我们没有父类的实现。

viewDidAppear: 是苹果实现的方法,因此没有它的符号;在方法内没有 self 。如果想在符号断点上使用 self,你必须知道它在哪里 (它可能在寄存器上,也可能在栈上;在 x86 上,你可以在 $esp+4 找到它)。但是这是很痛苦的,因为现在你必须至少知道四种体系结构 (x86,x86-64,armv7,armv64)。想象你需要花多少时间去学习命令集以及它们每一个的调用约定,然后正确的写一个在你的超类上设置断点并且条件正确的命令。幸运的是,这个在 Chisel 被解决了。这被成为 bmessage

(lldb) bmessage -[MyViewController viewDidAppear:]
Setting a breakpoint at -[UIViewController viewDidAppear:] with condition (void*)object_getClass((id)$rdi) == 0x000000010e2f4d28
Breakpoint 1: where = UIKit`-[UIViewController viewDidAppear:], address = 0x000000010e11533c

LLDB 和 Python

LLDB 有内建的,完整的 Python 支持。在LLDB中输入 script,会打开一个 Python REPL。你也可以输入一行 python 语句作为 script 命令 的参数,这可以运行 python 语句而不进入REPL:

(lldb) script import os
(lldb) script os.system("open http://www.objc.io/")

这样就允许你创造各种酷的命令。把下面的语句放到文件 ~/myCommands.py 中:

def caflushCommand(debugger, command, result, internal_dict):
  debugger.HandleCommand("e (void)[CATransaction flush]")

然后再 LLDB 中运行:

command script import ~/myCommands.py

或者把这行命令放在 /.lldbinit 里,这样每次进入 LLDB 时都会自动运行。Chisel 其实就是一个 Python 脚本的集合,这些脚本拼接 (命令) 字符串 ,然后让 LLDB 执行。很简单,不是吗?

紧握调试器这一武器

LLDB 可以做的事情很多。大多数人习惯于使用 pponsc,但实际上除此之外,LLDB 可以做的还有很多。掌握所有的命令 (实际上并不是很多),会让你在揭示代码运行时的运行状态,寻找 bug,强制执行特定的运行路径时获得更大的能力。你甚至可以构建简单的交互原型 - 比如要是现在以 modal 方式弹出一个 View Controller 会怎么样?使用调试器,一试便知。

这篇文章是为了想你展示 LLDB 的强大之处,并且鼓励你多去探索在控制台输入命令。

打开 LLDB,输入 help,看一看列举的命令。你尝试过多少?用了多少?

但愿 NSLog 看起来不再那么吸引你去用,每次编辑再运行并不有趣而且耗时。

调试愉快!

#各种补

编辑断点窗口中Action相关参数说明

Log Message

%B:打印断点的名字

%H:打印断点的调用次数

@exp@:输出表达式

###Debugger Command

po:打印对象信息

bt:打印函数栈

expression:表达式

异常断点的作用

数组越界访问时程序运行崩溃,但是崩溃停在了main函数里面,就算看了栈信息也不能马上定位到到底是那个数组越界访问了。为什么崩溃不能停在数组越界哪里?这是因为数组越界访问不一定会导致程序崩溃的,数组越界访问会导致异常抛出,而抛出的异常没有得到处理才会导致程序崩溃。因此最后会导致崩溃停在CoreFoundation框架里面。这个时候就需要设置Exception Breakpoint产生断点来定位错误了。

打印View Controller Hierarchy

_printHierarchy是 UIViewController 的一个私有方法,你可以用它将view controller 层次打印到控制台。

1
po [[[[UIApplication sharedApplication] keyWindow] rootViewController] _printHierarchy]

常用快捷键

光标切换到控制台:cmd+shift+c
控制台显示/隐藏:cmd+shift+y

精彩博文

Chisel-LLDB命令插件,让调试更Easy

LLdb篇2教你使用faceBook的chisel来提高调试效率

小笨狼与LLDB的故事

来源:objcio.cn

应用沙盒

iOS应用数据一般存储于应用沙盒,每个iOS应用都有自己的应用沙盒(应用沙盒就是文件系统目录),与其他文件系统隔离。应用必须待在自己的沙盒里,其他应用不能访问该沙盒。

应用沙盒的文件系统目录如下图所示:

模拟器应用沙盒的根路径在:

/Users/用户名/Library/Developer/CoreSimulator/Devices/模拟器UDID/data/Containers/Data/Application/对应应用程序文件夹

应用沙盒结构分析

###Documents###

保存应用运行时生成的 需要持久化的数据 ,iTunes同步设备时会 会备份 该目录。例如,游戏应用可将游戏存档保存在该目录。

###Library/Caches###

保存应用运行时生成的 需要持久化的数据 ,iTunes同步设备时 不会备份 该目录。一般存储体积大、不需要备份的非重要数据。

###Library/Preference###

保存应用的所有 偏好设置 ,iOS的Settings(设置)应用会在该目录中查找应用的设置信息。iTunes同步设备时 会备份 该目录。

###temp###

保存应用运行时所需的 临时文件 ,使用完毕后再将相应的文件从该目录删除。应用没有运行时,系统也可能会清除该目录下的文件。iTunes同步设备时 不会备份 该目录。

应用沙盒目录的常见获取方式

沙盒根目录

1
NSString *home=NSHomeDirectory();

Documnets

利用NSSearchPathForDirectoriesInDomains函数

1
NSString *path=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];

利用NSFileManager的URLsForDirectory:inDomains:函数

1
NSURL *path=[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];

参数说明

在iOS中,只有一个目录跟传入的参数匹配,所以这个集合里面只有一个元素。

NSUserDomainMask 代表从用户文件夹下找

YES 代表展开路径中的波浪线字符“~”

Library/Caches

利用NSSearchPathForDirectoriesInDomains函数

利用NSFileManager的URLsForDirectory:inDomains:函数

将函数的第1个参数改为:NSCachesDirectory即可

Library/Preference

通过过 NSUserDefaults 类存取该目录下的设置信息

temp

1
NSString *path=NSTemporaryDirectory();

#XML属性列表(plist)归档
属性列表是一种XML格式的文件,拓展名为plist。
如果对象是NSString、NSDictionary、NSArray、NSData、NSNumber等类型,就可以使用这些对象的writeToFile:atomically:方法直接将对象写到属性列表文件中。

所谓归档,是一个过程,即用某种格式来保存一个或者多个对象以便以后还原这些对象。

归档NSDictionary

将一个NSDictionary对象归档到一个plist属性列表中。

1
2
3
4
5
6
7
8
9
NSString *path=[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"info.plist"];

//将数据封装成字典
NSMutableDictionary *dict=[NSMutableDictionary dictionary];
[dict setValue:@"朱利" forKey:@"name"];
[dict setValue:@"http://www.zhuli8.com" forKey:@"site"];

//将字典持久化到Docums/info.plist文件中
[dict writeToFile:path atomically:YES];

##恢复NSDictionary

1
2
3
4
5
6
7
NSString *path=[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"info.plist"];

//读取Documents/info.plist的内容,实例化NSDictionary。
NSDictionary *dict=[NSDictionary dictionaryWithContentsOfFile:path];

NSString *name=[dict objectForKey:@"name"];
NSString *site=[dict objectForKey:@"site"];

#Preference(偏好设置)

很多iOS应用都支持偏好设置,比如保存用户名、是否自动登录、字体大小等设置,iOS提供了一套标准的解决方案来为应用程序加入偏好设置功能。每个应用都有个 NSUserDefaults 实例,通过它来存取偏好设置。

1
2
3
4
5
6
NSUserDefaults *defaults= [NSUserDefaults standardUserDefaults];
[defaults setObject:@"朱利" forKey:@"name"];
[defaults setFloat:18 forKey:@"fontSize"];
[defaults setBool:YES forKey:@"autoLogin"];

[defaults synchronize];

注意

NSUserDefauts设置数据时,不是立即写入,而是根据时间戳定时地把缓存中的数据写入本地磁盘。所以调用了set方法之后数据有可能还没有写入磁盘应用程序就终止了。出现以上问题,可以通过调用synchronize方法强制写入。

读取上次保存的偏好设置

1
2
3
4
5
NSUserDefaults *defaults= [NSUserDefaults standardUserDefaults];

NSString *name=[defaults objectForKey:@"name"];
float fontSize=[defaults floatForKey:@"fontSize"];
BOOL autoLogin=[defaults boolForKey:@"autoLogin"];

#NSKeyedArchiver归档(NSCoding)

如果对象是NSString、NSDictionary、NSArray、NSData、NSNumber等类型,可以直接用NSKeyedArchiver进行归档和恢复,不是所有的对象都可以直接用这种方法进行归档,只有遵守了NSCoding协议的对象才可以。

NSCoding协议的2个方法

initWithCoder

每次从文件中恢复(解码)对象时都会调用这个方法,一般在这个方法里面指定如何解码文件中的数据为对象的实例变量,可以使用 decodeObject:forKey:方法解码实例变量。

encodeWithCoder

每次归档对象时,都会调用这个方法,一般这个方法里面指定如何归档对象中的每个实例变量,可以使用 encoderObject:forKey:方法归档实例变量。

##NSKeyedArchiver归档NSArray

###归档一个NSArray对象到Documents/array.archive

1
2
3
4
NSString *path=[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"array.archive"];

NSArray *array = [NSArray arrayWithObjects:@”a”,@”b”,nil];
[NSKeyedArchiver archiveRootObject:array toFile:path];

###恢复(解码)NSArray对象

1
2
3
4
NSString *path=[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"array.archive"];

NSArray *array= [NSKeyedUnarchiver unarchiveObjectWithFile:path];
NSLog(@"%@",array);

##NSKeyedArchiver-归档Person对象

Person.h

1
2
3
4
5
@interface Person : NSObject<NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) float height;
@end

Person.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation Person
-(void)encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeObject:self.name forKey:@"name"];
[encoder encodeInt:self.age forKey:@"age"];
[encoder encodeFloat:self.height forKey:@"height"];
}
-(id)initWithCoder:(NSCoder *)decoder
{
self.name=[decoder decodeObjectForKey:@"name"];
self.age=[decoder decodeIntForKey:@"age"];
self.height=[decoder decodeFloatForKey:@"height"];
return self;
}
@end

归档(编码)

1
2
3
4
5
6
7
NSString *path=[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"person.archive"];

Person *person=[[Person alloc] init];
person.name=@"zhuli8.com";
person.age=1;
person.height=2.0f;
[NSKeyedArchiver archiveRootObject:person toFile:path];

恢复(解码)

1
2
3
4
NSString *path=[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"person.archive"];

Person *person= [NSKeyedUnarchiver unarchiveObjectWithFile:path];
NSLog(@"%@",person);

NSKeyedArchiver-归档对象的注意

如果父类也遵守了NSCoding协议,请注意:

  • 应该在encodeWithCoder:方法中加上一句[super encodeWithCode:encode];确保继承的实例变量也能被编码,即也能被归档;
  • 应该在initWithCoder:方法中加上一句self = [super initWithCoder:decoder];确保继承的实例变量也能被解码,即也能被恢复。

NSData

使用archiveRootObject:toFile:方法可以将一个对象直接写入到一个文件中,但有时候可能想将多个对象写入到同一个文件中,那么就要使用NSData来进行归档对象。NSData可以为一些数据提供临时存储空间,以便随后写入文件,或者存放从磁盘读取的文件内容。可以使用[NSMutableData data]创建可变数据空间。

归档2个Person对象到同一文件中

归档(编码)

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
Person *person1=[[Person alloc] init];
person1.name=@"zhuli8.com";
person1.age=1;
person1.height=1.2f;

Person *person2=[[Person alloc] init];
person2.name=@"朱利";
person2.age=28;
person2.height=1.7f;

//新建一块可变数据区
NSMutableData *data=[NSMutableData data];

//将数据区连接到一个NSKeyedArchiver对象
NSKeyedArchiver *archiver=[[NSKeyedArchiver alloc] initForWritingWithMutableData:data];

//开始存档对象,存档的数据都会存储到NSMutableData中
[archiver encodeObject:person1 forKey:@"person1"];
[archiver encodeObject:person2 forKey:@"person2"];

//存档完毕(一定要调用这个方法)
[archiver finishEncoding];

//将存档的数据写入文件
NSString *path=[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"persons.archive"];
[data writeToFile:path atomically:YES];

恢复(解码)

1
2
3
4
5
6
7
8
9
10
11
12
//从文件中读取数据
NSString *path=[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"persons.archive"];
NSData *data=[NSData dataWithContentsOfFile:path];
//根据数据,解析成一个NSKeyedUnarchiver对象
NSKeyedUnarchiver *unarchiver=[[NSKeyedUnarchiver alloc] initForReadingWithData:data];
Person *person1=[unarchiver decodeObjectForKey:@"person1"];
Person *person2=[unarchiver decodeObjectForKey:@"person2"];

//恢复完毕
[unarchiver finishDecoding];

NSLog(@"%@,%@",person1,person2);

利用归档实现深复制

比如对一个Person对象进行深复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Person *person1=[[Person alloc] init];
person1.name=@"zhuli8.com";
person1.age=1;
person1.height=1.2f;

//临时存储person1的数据
NSData *data=[NSKeyedArchiver archivedDataWithRootObject:person1];

//解析data,生成一个新的Person对象
Person *person2=[NSKeyedUnarchiver unarchiveObjectWithData:data];

//分别打印内存地址
NSLog(@"person1=%p,person2=%p",person1,person2);

//person1=0x7fc478c49390,person2=0x7fc478e1c180

SQLite3

SQLite3是一款开源的嵌入式关系型数据库,可移植性好、易使用、内存开销小。
SQLite3是无类型的,意味着你可以保存任何类型的数据到任意表的任意字段中。比如下列的创表语句是合法的:

1
create table t_person(name, age);

为了保证可读性,建议还是把字段类型加上:

1
create table t_person(name text, age integer);

SQLite3常用的5种数据类型:text、integer、float、boolean、blob。

##创建、打开、关闭数据库

创建或打开数据库

1
2
3
4
5
6
NSString *path=[[NSSearchPathForDirectoriesInDomains(NSDocumentationDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"person.db"];

sqlite3 *db;//一个打开的数据库实例

//将根据文件路径打开数据库,如果不存在,则会创建一个新的数据库。如果result等于常量SQLITE_OK,则表示成功打开数据库。
int result=sqlite3_open([path UTF8String], &db);//数据库文件的路径必须以C字符串(而非NSString)传入

关闭数据库:

1
sqlite3_close(db);

执行不返回数据的SQL语句

执行创表语句

1
2
3
char *errorMsg;//用来存储错误信息
char *sql="create table if not exists t_person(id integer primary key autoincrement,name text,age integer);";
int result=sqlite3_exec(db, sql, NULL, NULL, &errorMsg);

sqlite3_exec()可以执行任何SQL语句,比如创表、更新、插入和删除操作。但是一般不用它执行查询语句,因为它不会返回查询到的数据。
sqlite3_exec()还可以执行的语句:

  • 开启事务:begin transaction;
  • 回滚事务:rollback;
  • 提交事务:commit;

带占位符插入数据

1
2
3
4
5
6
7
8
9
10
11
12
char *sql="insert into t_person(name,age) values(?,?);";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL)==SQLITE_OK) {
sqlite3_bind_text(stmt, 1, "朱利", -1, NULL);
sqlite3_bind_int(stmt, 2, 1);
}

if (sqlite3_step(stmt)!=SQLITE_DONE) {
NSLog(@"插入数据错误");
}

sqlite3_finalize(stmt);

sqlite3_prepare_v2():返回值等于SQLITE_OK,说明SQL语句已经准备成功,没有语法问题。
sqlite3_bind_text():大部分绑定函数都只有3个参数

  • 第1个参数是sqlite3_stmt *类型
  • 第2个参数指占位符的位置,第一个占位符的位置是1,不是0
  • 第3个参数指占位符要绑定的值
  • 第4个参数指在第3个参数中所传递数据的长度,对于C字符串,可以传递-1代替字符串的长度
  • 第5个参数是一个可选的函数回调,一般用于在语句执行后完成内存清理工作
    sqlite_step():执行SQL语句,返回SQLITE_DONE代表成功执行完毕。
    sqlite_finalize():销毁sqlite3_stmt *对象。

##查询数据

1
2
3
4
5
6
7
8
9
10
11
12
char *sql = "select id,name,age from t_person;";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
int _id = sqlite3_column_int(stmt, 0);
char *_name = (char *)sqlite3_column_text(stmt, 1);
NSString *name = [NSString stringWithUTF8String:_name];
int _age = sqlite3_column_int(stmt, 2);
NSLog(@"id=%i, name=%@, age=%i", _id, name, _age);
}
}
sqlite3_finalize(stmt);

sqlite3_step():返回SQLITE_ROW代表遍历到一条新记录。
sqlite3column*()用于获取每个字段对应的值,第2个参数是字段的索引,从0开始。

补充

控制台操作

进入数据库目录后使用 sqlite3 msg.sqlite
.tables 查看表
select * from 上面查到的表;(注意一定要加分号才符合sql语句)

Core Data

Core Data框架提供了对象-关系映射(ORM)的功能,即能够将OC对象转化成数据,保存在SQLite3数据库文件中,也能够将保存在数据库中的数据还原成OC对象。在此数据操作期间,不需要编写任何SQL语句。

##推荐文章阅读
初识Core Data(1)

初识Core Data(2)

初识Core Data(3)

打开Core Data的SQL日志输出开关

  • 打开Product,点击Edit Scheme…
  • 点击Arguments,在Arguments Passed On Launch中添加2项
    • -com.apple.CoreData.SQLDebug
    • 1

Core Data的延迟加载

Core Data不会根据实体中的关联关系立即获取相应的关联对象,比如通过Core Data取出Person实体时,并不会立即查询相关联的Card实体;当应用真的需要使用Card时,才会查询数据库,加载Card实体的信息。

#社区博客

CocoaChina

苹果开发中文站 - 最热的iPhone开发社区,最热的苹果开发社区,最热的iPad开发社区。

Code4App

Code4App是一个移动平台的代码库,是iOS开发好帮手。Code4App为移动开发程序员提供大量的开源代码,iOS代码实例搜索,iOS特效示例,iOS代码例子下载。所有的开源代码都经过严格测试,并且配有效果图和演示视频。你可以搜索并下载你需要的任意代码,当然,你也可以浏览这些效果图或者视频,来找找你的UI设计灵感。同时网站内的火花社区供开发者进行求职招聘、技术交流、分享设计创意、展示作品等方面的交流。

伯乐在线

分享iOS开发资讯和文章

开源中国

iOS开发社区 - 开源中国社区

开发者头条

开发者头条 - 开发者的首选阅读分享平台

码农周刊

码农周刊是一份专为程序员打造的IT技术周刊。我们会为你精选一周IT技术干货,每周一发送,完全免费。

福利社

各种经典博文整理。

51ios

提供iOS,swift,iOS开发,iOS开发教程,oc,swift 教程,swift代码,iOS视频,swift视频

#个人博客
唐巧

InfoQ编辑, 《iOS开发进阶》作者, 在猿题库创业。Java开发工程师,曾开发网易微博后台。 iOS开发工程师,曾开发有道云笔记、猿题库。

王巍

一名来自中国的 iOS / Unity 开发者。现居日本,就职于 LINE。正在修行,探求创意之源。

破船之家

我是谁,我从哪里来,我到哪里去?我就是我,是颜色不一样的烟火…

池建强

70后程序员。先后从事互联网和企业级应用开发。目前就职瑞友科技(原用友软件工程)IT应用研究院,任职副院长。技术创新控,坚持年轻时的理想,倒霉的乐观者,关注互联网技术、应用平台研发、领域驱动,OSGi,动态语言应用 、云计算、Mac OS相关技术。喜欢的一句话:虽万千人,吾往矣

田伟宇

Currently working at Alibaba.inc as an iOS engineer for food。

雷纯锋的技术博客

iOS 开发者,GitBucket 的作者,热衷于分享。

sunnyxx的技术博客

孙源(sunnyxx),目前就职于百度,负责百度知道 iOS 客户端的开发工作,对技术喜欢刨根问底和总结最佳实践,热爱分享和开源,维护一个叫 forkingdog 的开源小组。

iOS程序员

r p y b b

springox的博客

只因Objective-C与Runtime这么一篇文章而被吸引。

南峰子的技术博客

各种好文,各种源码解析。

岁寒

网络封装的博文很精彩。

#其他推荐
objc中国

为中国 Objective-C 社区带来最佳实践和先进技术

Glow技术团队博客

Thoughts, stories and ideas.

iOS 学习资料整理

这份学习资料是为 iOS 初学者所准备的, 旨在帮助 iOS 初学者们快速找到适合自己的学习资料, 节省他们搜索资料的时间, 使他们更好的规划好自己的 iOS 学习路线, 更快的入门, 更准确的定位的目前所处的位置.

推荐博客列表

中文 iOS/Mac 开发博客列表

优质博文

那些在学习iOS开发前就应该知道的事(part 1)

那些在学习iOS开发前就应该知道的事(part 2)

初识Core Data(1)

初识Core Data(2)

初识Core Data(3)

ReactiveCocoa入门教程:第一部分

ReactiveCocoa 和 MVVM 入门

ReactiveCocoa - iOS开发的新框架

ReactiveCocoa自述:工作原理和应用

Method Swizzling 和 AOP 实践

Java转iOS-第一个项目总结(1)

Java转iOS-第一个项目总结(2)

详解Xcode 6的视图调试

小心别让圆角成了你列表的帧数杀手

#写在最后
在长的路,一步步也能走完;在短的路,不迈开双脚也无法到达!

Instagram,Snapchat,Photoshop。

所有这些应用都是用来做图像处理的。图像处理可以简单到把一张照片转换为灰度图,也可以复杂到是分析一个视频,并在人群中找到某个特定的人。尽管这些应用非常的不同,但这些例子遵从同样的流程,都是从创造到渲染。

在电脑或者手机上做图像处理有很多方式,但是目前为止最高效的方法是有效地使用图形处理单元,或者叫 GPU。你的手机包含两个不同的处理单元,CPU 和 GPU。CPU 是个多面手,并且不得不处理所有的事情,而 GPU 则可以集中来处理好一件事情,就是并行地做浮点运算。事实上,图像处理和渲染就是在将要渲染到窗口上的像素上做许许多多的浮点运算。

通过有效的利用 GPU,可以成百倍甚至上千倍地提高手机上的图像渲染能力。如果不是基于 GPU 的处理,手机上实时高清视频滤镜是不现实,甚至不可能的。

着色器 (shader) 是我们利用这种能力的工具。着色器是用着色语言写的小的,基于 C 语言的程序。现在有很许多种着色语言,但你如果做 OS X 或者 iOS 开发的话,你应该专注于 OpenGL 着色语言,或者叫 GLSL。你可以将 GLSL 的理念应用到其他的更专用的语言 (比如 Metal) 上去。这里我们即将介绍的概念与和 Core Image 中的自定义核矩阵有着很好的对应,尽管它们在语法上有一些不同。

这个过程可能会很让人恐惧,尤其是对新手。这篇文章的目的是让你接触一些写图像处理着色器的必要的基础信息,并将你带上书写你自己的图像处理着色器的道路。

什么是着色器

我们将乘坐时光机回顾一下过去,来了解什么是着色器,以及它是怎样被集成到我们的工作流当中的。

如果你在 iOS 5 或者之前就开始做 iOS 开发,你或许会知道在 iPhone 上 OpenGL 编程有一个转变,从 OpenGL ES 1.1 变成了 OpenGL ES 2.0。

OpenGL ES 1.1 没有使用着色器。作为替代,OpenGL ES 1.1 使用被称为固定功能管线 (fixed-function pipeline) 的方式。有一系列固定的函数用来在屏幕上渲染对象,而不是创建一个单独的程序来指导 GPU 的行为。这样有很大的局限性,你不能做出任何特殊的效果。如果你想知道着色器在工程中可以造成怎样的不同,看看这篇 Brad Larson 写的他用着色器替代固定函数重构 Molecules 应用的博客

OpenGL ES 2.0 引入了可编程管线。可编程管线允许你创建自己的着色器,给了你更强大的能力和灵活性。

在 OpenGL ES 中你必须创建两种着色器:顶点着色器 (vertex shaders) 和片段着色器 (fragment shaders)。这两种着色器是一个完整程序的两半,你不能仅仅创建其中任何一个;想创建一个完整的着色程序,两个都是必须存在。

顶点着色器定义了在 2D 或者 3D 场景中几何图形是如何处理的。一个顶点指的是 2D 或者 3D 空间中的一个点。在图像处理中,有 4 个顶点:每一个顶点代表图像的一个角。顶点着色器设置顶点的位置,并且把位置和纹理坐标这样的参数发送到片段着色器。

然后 GPU 使用片段着色器在对象或者图片的每一个像素上进行计算,最终计算出每个像素的最终颜色。图片,归根结底,实际上仅仅是数据的集合。图片的文档包含每一个像素的各个颜色分量和像素透明度的值。因为对每一个像素,算式是相同的,GPU 可以流水线作业这个过程,从而更加有效的进行处理。使用正确优化过的着色器,在 GPU 上进行处理,将使你获得百倍于在 CPU 上用同样的过程进行图像处理的效率。

把东西渲染到屏幕上从一开始就是一个困扰 OpenGL 开发者的问题。仅仅让屏幕呈现出非黑色就要写很多样板代码和设置。开发者必须跳过很多坑 ,而这些坑所带来的沮丧感以及着色器测试方法的匮乏,让很多人放弃了哪怕是尝试着写着色器。

幸运的是,过去几年,一些工具和框架减少了开发者在尝试着色器方面的焦虑。

下面我将要写的每一个着色器的例子都是从开源框架 GPUImage 中来的。如果你对 OpenGL/OpenGL ES 场景如何配置,从而使其可以使用着色器渲染感到好奇的话,可以 clone 这个仓储。我们不会深入到怎样设置 OpenGL/OpenGL ES 来使用着色器渲染,这超出了这篇文章的范围。

我们的第一个着色器的例子

顶点着色器

好吧,关于着色器我们说的足够多了。我们来看一个实践中真实的着色器程序。这里是一个 GPUImage 中一个基础的顶点着色器:

attribute vec4 position;  
attribute vec4 inputTextureCoordinate;

varying vec2 textureCoordinate;

void main()  
{
    gl_position = position;
    textureCoordinate = inputTextureCoordinate.xy;
}

我们一句一句的来看:

attribute vec4 position;  

像所有的语言一样,着色器语言的设计者也为常用的类型创造了特殊的数据类型,例如 2D 和 3D 坐标。这些类型是向量,稍后我们会深入更多。回到我们的应用程序的代码,我们创建了一系列顶点,我们为每个顶点提供的参数里的其中一个是顶点在画布中的位置。然后我们必须告诉我们的顶点着色器它需要接收这个参数,我们稍后会将它用在某些事情上。因为这是一个 C 程序,我们需要记住要在每一行代码的结束使用一个分号,所以如果你正使用 Swift 的话,你需要把在末尾加分号的习惯捡回来。

attribute vec4 inputTextureCoordinate;  

现在你或许很奇怪,为什么我们需要一个纹理坐标。我们不是刚刚得到了我们的顶点位置了吗?难道它们不是同样的东西吗?

其实它们并非一定是同样的东西。纹理坐标是纹理映射的一部分。这意味着你想要对你的纹理进行某种滤镜操作的时候会用到它。左上角坐标是 (0,0)。右上角的坐标是 (1,0)。如果我们需要在图片内部而不是边缘选择一个纹理坐标,我们需要在我们的应用中设定的纹理坐标就会与此不同,像是 (.25, .25) 是在图片左上角向右向下各图片高宽 1/4 的位置。在我们当前的图像处理应用里,我们希望纹理坐标和顶点位置一致,因为我们想覆盖到图片的整个长度和宽度。有时候你或许会希望这些坐标是不同的,所以需要记住它们未必是相同的坐标。在这个例子中,顶点坐标空间从 -1.0 延展到 1.0,而纹理坐标是从 0.0 到 1.0。

varying vec2 textureCoordinate;  

因为顶点着色器负责和片段着色器交流,所以我们需要创建一个变量和它共享相关的信息。在图像处理中,片段着色器需要的唯一相关信息就是顶点着色器现在正在处理哪个像素。

gl_Position = position;  

gl_Position 是一个内建的变量。GLSL 有一些内建的变量,在片段着色器的例子中我们将看到其中的一个。这些特殊的变量是可编程管道的一部分,API 会去寻找它们,并且知道如何和它们关联上。在这个例子中,我们指定了顶点的位置,并且把它从我们的程序中反馈给渲染管线。

textureCoordinate = inputTextureCoordinate.xy;  

最后,我们取出这个顶点中纹理坐标的 X 和 Y 的位置。我们只关心 inputTextureCoordinate 中的前两个参数,X 和 Y。这个坐标最开始是通过 4 个属性存在顶点着色器里的,但我们只需要其中的两个。我们拿出需要的属性,然后赋值给一个将要和片段着色器通信的变量,而不是把更多的属性反馈给片段着色器。

在大多数图像处理程序中,顶点着色器都差不多,所以,这篇文章接下来的部分,我们将集中讨论片段着色器。

片段着色器

看过了我们简单的顶点着色器后,我们再来看一个可以实现的最简单的片段着色器:一个直通滤镜:

varying highp vec2 textureCoordinate;

uniform sampler2D inputImageTexture;

void main()  
{
    gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
}

这个着色器实际上不会改变图像中的任何东西。它是一个直通着色器,意味着我们输入每一个像素,然后输出完全相同的像素。我们来一句句的看:

varying highp vec2 textureCoordinate;  

因为片段着色器作用在每一个像素上,我们需要一个方法来确定我们当前在分析哪一个像素/片段。它需要存储像素的 X 和 Y 坐标。我们接收到的是当前在顶点着色器被设置好的纹理坐标。

uniform sampler2D inputImageTexture;  

为了处理图像,我们从应用中接收一个图片的引用,我们把它当做一个 2D 的纹理。这个数据类型被叫做 sampler2D ,这是因为我们要从这个 2D 纹理中采样出一个点来进行处理。

gl_FragColor = texture2D(inputImageTexture, textureCoordinate);  

这是我们碰到的第一个 GLSL 特有的方法:texture2D,顾名思义,创建一个 2D 的纹理。它采用我们之前声明过的属性作为参数来决定被处理的像素的颜色。这个颜色然后被设置给另外一个内建变量,gl_FragColor。因为片段着色器的唯一目的就是确定一个像素的颜色,gl_FragColor 本质上就是我们片段着色器的返回语句。一旦这个片段的颜色被设置,接下来片段着色器就不需要再做其他任何事情了,所以你在这之后写任何的语句,都不会被执行。

就像你看到的那样,写着色器很大一部分就是了解着色语言。即使着色语言是基于 C 语言的,依然有很多怪异和细微的差别让它和普通的 C 语言有不同。

GLSL 数据类型和运算

各式着色器都是用 OpenGL 着色语言 (GLSL) 写的。GLSL 是一种从 C 语言导出的简单语言。它缺少 C 语言的高级功能,比如动态内存管理。但是,它也包含一些在着色过程中常用的数学运算函数。

在负责 OpenGL 和 OpenGL ES 实现的 Khronos 小组的网站上有一些有用的参考资料。在你开始之前,一件你可以做的最有价值的事情就是获取 OpenGL 和 OpenGL ES 的快速入门指导:

通过查看这些参考卡片,你可以快速简单地了解在写 OpenGL 应用时需要的着色语言函数和数据类型。

尽早用,经常用。

即使在这么简单的着色器的例子里,也有一些地方看起来很怪异,不是吗?看过了基础的着色器之后,是时候开始解释其中一些内容,以及它们为什么存在于 GLSL 中。

输入,输出,以及精度修饰 (Precision Qualifiers)

看一看我们的直通着色器,你会注意到有一个属性被标记为 “varying”,另一个属性被标记为 “uniform”。

这些变量是 GLSL 中的输入和输出。它允许从我们应用的输入,以及在顶点着色器和片段着色器之间进行交流。

在 GLSL 中,实际有三种标签可以赋值给我们的变量:


  • Uniforms

  • Attributes

  • Varyings

Uniforms 是一种外界和你的着色器交流的方式。Uniforms 是为在一个渲染循环里不变的输入值设计的。如果你正在应用茶色滤镜,并且你已经指定了滤镜的强度,那么这些就是在渲染过程中不需要改变的事情,你可以把它作为 Uniform 输入。 Uniform 在顶点着色器和片段着色器里都可以被访问到。

Attributes 仅仅可以在顶点着色器中被访问。Attribute 是在随着每一个顶点不同而会发生变动的输入值,例如顶点的位置和纹理坐标等。顶点着色器利用这些变量来计算位置,以它们为基础计算一些值,然后把这些值以 varyings 的方式传到片段着色器。

最后,但同样重要的,是 varyings 标签。Varying 在顶点着色器和片段着色器都会出现。Varying 是用来在顶点着色器和片段着色器传递信息的,并且在顶点着色器和片段着色器中必须有匹配的名字。数值在顶点着色器被写入到 varying ,然后在片段着色器被读出。被写入 varying 中的值,在片段着色器中会被以插值的形式插入到两个顶点直接的各个像素中去。

回看我们之前写的简单的着色器的例子,在顶点着色器和片段着色器中都用 varying 声明了 textureCoordinate。我们在顶点着色器中写入 varying 的值。然后我们把它传入片段着色器,并在片段着色器中读取和处理。

在我们继续之前,最后一件要注意的事。看看创建的这些变量。你会注意到纹理坐标有一个叫做 highp 的属性。这个属性负责设置你需要的变量精度。因为 OpenGL ES 被设计为在处理能力有限的系统中使用,精度限制被加入进来可以提高效率。

如果不需要非常高的精度,你可以进行设定,这或许会允许在一个时钟循环内处理更多的值。相反的,在纹理坐标中,我们需要尽可能的确保精确,所以我们具体说明确实需要额外的精度。

精度修饰存在于 OpenGL ES 中,因为它是被设计用在移动设备中的。但是,在老版本的桌面版的 OpenGL 中则没有。因为 OpenGL ES 实际上是 OpenGL 的子集,你几乎总是可以直接把 OpenGL ES 的项目移植到 OpenGL。如果你这样做,记住一定要在你的桌面版着色器中去掉精度修饰。这是很重要的一件事,尤其是当你计划在 iOS 和 OS X 之间移植项目时。

向量

在 GLSL 中,你会用到很多向量和向量类型。向量是一个很棘手的话题,它们表面上看起来很直观,但是因为它们有很多用途,这使我们在使用它们时常常会感到迷惑。

在 GLSL 环境中,向量是一个类似数组的特殊的数据类型。每一种类型都有固定的可以保存的元素。深入研究一下,你甚至可以获得数组可以存储的数值的精确的类型。但是在大多数情况下,只要使用通用的向量类型就足够了。

有三种向量类型你会经常看到:


  • vec2

  • vec3

  • vec4

这些向量类型包含特定数量的浮点数:vec2 包含两个浮点数,vec3 包含三个浮点数,vec4 包含四个浮点数。

这些类型可以被用在着色器中可能被改变或者持有的多种数据类型中。在片段着色器中,很明显 X 和 Y 坐标是的你想保存的信息。 (X,Y) 存储在 vec2 中就很合适。

在图像处理过程中,另一个你可能想持续追踪的事情就是每个像素的 R,G,B,A 值。这些可以被存储在 vec4 中。

矩阵

现在我们已经了解了向量,接下来继续了解矩阵。矩阵和向量很相似,但是它们添加了额外一层的复杂度。矩阵是一个浮点数数组的数组,而不是单个的简单浮点数数组。

类似于向量,你将会经常处理的矩阵对象是:


  • mat2

  • mat3

  • mat4

vec2 保存两个浮点数,mat 保存相当于两个 vec2 对象的值。将向量对象传递到矩阵对象并不是必须的,只需要有足够填充矩阵的浮点数即可。在 mat2 中,你需要传入两个 vec2 或者四个浮点数。因为你可以给向量命名,而且相比于直接传浮点数,你只需要负责两个对象,而不是四个,所以非常推荐使用封装好的值来存储你的数字,这样更利于追踪。对于 mat4 会更复杂一些,因为你要负责 16 个数字,而不是 4 个。

在我们 mat2 的例子中,我们有两个 vec2 对象。每个 vec2 对象代表一行。每个 vec2 对象的第一个元素代表一列。构建你的矩阵对象的时候,确保每个值都放在了正确的行和列上是很重要的,否则使用它们进行运算肯定得不到正确的结果。

既然我们有了矩阵也有了填充矩阵的向量,问题来了:“我们要用它们做什么呢?“ 我们可以存储点和颜色或者其他的一些的信息,但是要如果通过修改它们来做一些很酷的事情呢?

向量和矩阵运算,也就是初等线性代数

我找到的最好的关于线性代数和矩阵是如何工作的资源是这个网站的更好的解释。我从这个网站偷来借鉴的一句引述就是:


线性代数课程的幸存者都成为了物理学家,图形程序员或者其他的受虐狂。


矩阵操作总体来说并不“难”;只不过它们没有被任何上下文解释,所以很难概念化地理解究竟为什么会有人想要和它们打交道。我希望能在给出一些它们在图形编程中的应用背景后,我们可以了解它们怎样帮助我们实现不可思议的东西。

线性代数允许你一次在很多值上进行操作。假想你有一组数,你想要每一个数乘以 2。你一般会一个个地顺次计算数值。但是因为对每一个数都进行的是同样的操作,所以你完全可以并行地实现这个操作。

我们举一个看起来可怕的例子,CGAffineTransforms。仿射转化是很简单的操作,它可以改变具有平行边的形状 (比如正方形或者矩形) 的大小,位置,或者旋转角度。

在这种时候你当然可以坐下来拿出笔和纸,自己去计算这些转化,但这么做其实没什么意义。GLSL 有很多内建的函数来进行这些庞杂的用来计算转换的函数。了解这些函数背后的思想才是最重要的。

GLSL 特有函数

这篇文章中,我们不会把所有的 GLSL 内建的函数都过一遍,不过你可以在 Shaderific 上找到很好的相关资源。很多 GLSL 函数都是从 C 语言数学库中的基本的数学运算导出的,所以解释 sin 函数是做什么的真的是浪费时间。我们将集中阐释一些更深奥的函数,从而达到这篇文章的目的,解释怎样才能充分利用 GPU 的性能的一些细节。

step(): GPU 有一个局限性,它并不能很好的处理条件逻辑。GPU 喜欢做的事情是接受一系列的操作,并将它们作用在所有的东西上。分支会在片段着色器上导致明显的性能下降,在移动设备上尤其明显。step() 通过允许在不产生分支的前提下实现条件逻辑,从而在某种程度上可以缓解这种局限性。如果传进 step() 函数的值小于阈值,step() 会返回 0.0。如果大于或等于阈值,则会返回 1.0。通过把这个结果和你的着色器的值相乘,着色器的值就可以被使用或者忽略,而不用使用 if() 语句。

mix(): mix 函数将两个值 (例如颜色值) 混合为一个变量。如果我们有红和绿两个颜色,我们可以用 mix() 函数线性插值。这在图像处理中很常用,比如在应用程序中通过一组独特的设定来控制效果的强度等。

*clamp(): GLSL 中一个比较一致的方面就是它喜欢使用归一化的坐标。它希望收到的颜色分量或者纹理坐标的值在 0.0 和 1.0 之间。为了保证我们的值不会超出这个非常窄的区域,我们可以使用 clamp() 函数。 clamp() 会检查并确保你的值在 0.0 和 1.0 之间。如果你的值小于 0.0,它会把值设为 0.0。这样做是为了防止一些常见的错误,例如当你进行计算时意外的传入了一个负数,或者其他的完全超出了算式范围的值。

更复杂的着色器的例子

我知道数学的洪水一定让你快被淹没了。如果你还能跟上我,我想举几个优美的着色器的例子,这会更有意义,这样你又有机会淹没在 GLSL 的潮水中。

饱和度调整

实践中的饱和度滤镜

这是一个做饱和度调节的片段着色器。这个着色器出自 《图形着色器:理论和实践》一书,我强烈推荐整本书给所有对着色器感兴趣的人。

饱和度是用来表示颜色的亮度和强度的术语。一件亮红色的毛衣的饱和度要远比北京雾霾时灰色的天空的饱和度高得多。

在这个着色器上,参照人类对颜色和亮度的感知过程,我们有一些优化可以使用。一般而言,人类对亮度要比对颜色敏感的多。这么多年来,压缩软件体积的一个优化方式就是减少存储颜色所用的内存。

人类不仅对亮度比颜色要敏感,同样亮度下,我们对某些特定的颜色反应也更加灵敏,尤其是绿色。这意味着,当你寻找压缩图片的方式,或者以某种方式改变它们的亮度和颜色的时候,多放一些注意力在绿色光谱上是很重要的,因为我们对它最为敏感。

varying highp vec2 textureCoordinate;

uniform sampler2D inputImageTexture;  
uniform lowp float saturation;

const mediump vec3 luminanceWeighting = vec3(0.2125, 0.7154, 0.0721);

void main()  
{
   lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
   lowp float luminance = dot(textureColor.rgb, luminanceWeighting);
   lowp vec3 greyScaleColor = vec3(luminance);

    gl_FragColor = vec4(mix(greyScaleColor, textureColor.rgb, saturation), textureColor.w);

}

我们一行行的看这个片段着色器的代码:

varying highp vec2 textureCoordinate;

uniform sampler2D inputImageTexture;  
uniform lowp float saturation;  

再一次,因为这是一个要和基础的顶点着色器通信的片段着色器,我们需要为输入纹理坐标和输入图片纹理声明一个 varyings 变量,这样才能接收到我们需要的信息,并进行过滤处理。这个例子中我们有一个新的 uniform 的变量需要处理,那就是饱和度。饱和度的数值是一个我们从用户界面设置的参数。我们需要知道用户需要多少饱和度,从而展示正确的颜色数量。

const mediump vec3 luminanceWeighting = vec3(0.2125, 0.7154, 0.0721);  

这就是我们设置三个元素的向量,为我们的亮度来保存颜色比重的地方。这三个值加起来要为 1,这样我们才能把亮度计算为 0.0 - 1.0 之间的值。注意中间的值,就是表示绿色的值,用了 70% 的颜色比重,而蓝色只用了它的 10%。蓝色对我们的展示不是很好,把更多权重放在绿色上是很有意义的。

lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);  

我们需要取样特定像素在我们图片/纹理中的具体坐标来获取颜色信息。我们将会改变它一点点,而不是想直通滤镜那样直接返回。

lowp float luminance = dot(textureColor.rgb, luminanceWeighting);  

这行代码会让那些没有学过线性代数或者很早以前在学校学过但是很少用过的人看起来不那么熟悉。我们是在使用 GLSL 中的点乘运算。如果你记得在学校里曾用过点运算符来相乘两个数字的话,那么你就能明白是什么回事儿了。点乘计算以包含纹理颜色信息的 vec4 为参数,舍弃 vec4 的最后一个不需要的元素,将它和相对应的亮度权重相乘。然后取出所有的三个值把它们加在一起,计算出这个像素综合的亮度值。

lowp vec3 greyScaleColor = vec3(luminance);  

我们创建一个三个值都是亮度信息的 vec3。如果你只指定一个值,编译器会帮你把该将向量中的每个分量都设成这个值。

gl_FragColor = vec4(mix(greyScaleColor, textureColor.rgb, saturation), textureColor.w);  

最后,我们把所有的片段组合起来。为了确定每个新的颜色是什么,我们使用刚刚学过的很好用的 mix 函数。mix 函数会把我们刚刚计算的灰度值和初始的纹理颜色以及我们得到的饱和度的信息相结合。

这就是一个很棒的,好用的着色器,它让你用主函数里的四行代码就可以把图片从彩色变到灰色,或者从灰色变到彩色。还不错,不是吗?

球形折射

最后,我们来看一个很漂亮的滤镜,你可以用来向你的朋友炫耀,或者吓唬你的敌人。这个滤镜看起来像是有一个玻璃球在你的图片上。这会比之前的看起来更复杂。但我相信我们可以完成它。

实践中的球形折射滤镜!

varying highp vec2 textureCoordinate;

uniform sampler2D inputImageTexture;

uniform highp vec2 center;  
uniform highp float radius;  
uniform highp float aspectRatio;  
uniform highp float refractiveIndex;

void main()  
{
    highp vec2 textureCoordinateToUse = vec2(textureCoordinate.x, (textureCoordinate.y * aspectRatio + 0.5 - 0.5 * aspectRatio));
    highp float distanceFromCenter = distance(center, textureCoordinateToUse);
    lowp float checkForPresenceWithinSphere = step(distanceFromCenter, radius);

    distanceFromCenter = distanceFromCenter / radius;

    highp float normalizedDepth = radius * sqrt(1.0 - distanceFromCenter * distanceFromCenter);
    highp vec3 sphereNormal = normalize(vec3(textureCoordinateToUse - center, normalizedDepth));

    highp vec3 refractedVector = refract(vec3(0.0, 0.0, -1.0), sphereNormal, refractiveIndex);

    gl_FragColor = texture2D(inputImageTexture, (refractedVector.xy + 1.0) * 0.5) * checkForPresenceWithinSphere;
}

再一次,看起来很熟悉…

uniform highp vec2 center;  
uniform highp float radius;  
uniform highp float aspectRatio;  
uniform highp float refractiveIndex;  

我们引入了一些参数,用来计算出图片中多大的区域要通过滤镜。因为这是一个球形,我们需要一个中心点和半径来计算球形的边界。宽高比是由你使用的设备的屏幕尺寸决定的,所以不能被硬编码,因为 iPhone 和 iPad 的比例是不相同的。我们的用户或者程序员会决定折射率,从而确定折射看起来是什么样子的。GPUImage 中折射率被设置为 0.71.

highp vec2 textureCoordinateToUse = vec2(textureCoordinate.x, (textureCoordinate.y * aspectRatio + 0.5 - 0.5 * aspectRatio));  

图像的纹理坐标是在归一化的 0.0-1.0 的坐标空间内。归一化的坐标空间意味着考虑屏幕是一个单位宽和一个单位长,而不是 320 像素宽,480 像素高。因为手机的高度比宽度要长,我们需要为球形计算一个偏移率,这样球就是圆的而不是椭圆的。

我们希望正确的宽高比

highp float distanceFromCenter = distance(center, textureCoordinateToUse);  

我们需要计算特定的像素点距离球形的中心有多远。我们使用 GLSL 内建的 distance() 函数,它会使用勾股定律计算出中心坐标和长宽比矫正过的纹理坐标的距离。

lowp float checkForPresenceWithinSphere = step(distanceFromCenter, radius);  

这里我们计算了片段是否在球体内。我们计算当前点距离球形中心有多远以及球的半径是多少。如果当前距离小于半径,这个片段就在球体内,这个变量被设置为 1.0。否则,如果距离大于半径,这个片段就不在球内,这个变量被设置为 0.0 。

像素在球内或者球外

distanceFromCenter = distanceFromCenter / radius;  

By dividing it by the radius, we are making our math calculations easier in the next few lines of code.

既然我们已经计算出哪些像素是在球内的,我们接着要对这些球内的像素进行计算并做些事情。再一次,我们需要标准化到球心的距离。我们直接重新设置 distanceFromCenter 的值,而不是新建一个变量,因为那会增加我们的开销。 通过将它与半径相除,我们可以让之后几行计算代码变得简单一些。

highp float normalizedDepth = radius * sqrt(1.0 - distanceFromCenter * distanceFromCenter);  

因为我们试图模拟一个玻璃球,我们需要计算球的“深度”是多少。这个虚拟的球,不论怎样,在 Z 轴上,将会延伸图片表面到观察者的距离。这将帮助计算机确定如何表示球内的像素。还有,因为球是圆的,距离球心不同的距离,会有不同的深度。由于球表面方向的不同,球心处和边缘处对光的折射会不相同:

球有多深?

highp vec3 sphereNormal = normalize(vec3(textureCoordinateToUse - center, normalizedDepth));  

这里我们又进行了一次归一化。为了计算球面某个点的方向,我们用 X ,Y 坐标的方式,表示当前像素到球心的距离,然后把这些和计算出的球的深度结合。然后把结果向量进行归一化。

想想当你正在使用 Adobe Illustrator 这样的软件时,你在 Illustrator 中创建一个三角形,但是它太小了。你按住 option 键,放大三角形,但是它现在太大了。你然后把它缩小到你想要的尺寸:

什么是角?

highp vec3 refractedVector = refract(vec3(0.0, 0.0, -1.0), sphereNormal, refractiveIndex);  

refract() 是一个很有趣的 GLSL 函数。refract() 以我们刚才创建的球法线和折射率来计算当光线通过这样的球时,从任意一个点看起来是怎样的。

gl_FragColor = texture2D(inputImageTexture, (refractedVector.xy + 1.0) * 0.5) * checkForPresenceWithinSphere;  

最后,通过所有这些障碍后,我们终于凑齐了计算片段使用的颜色所需要的所有信息。折射光向量用来查找读取的输入位于图片哪个位置的,但是因为在那个向量中,坐标是从 -1.0 到 1.0 的,我们需要把它调整到 0.0-1.0 的纹理坐标空间内。

我们然后把我们的结果和球边界检查的值相乘。如果我们的片段没有在球内,一个透明的像素 (0.0, 0.0, 0.0, 0.0) 将被写入。如果片段在球形内,这个结果被使用,然后返回计算好的颜色值。这样我们在着色器中可以就避免昂贵的条件逻辑。

调试着色器

着色器调试不是一件直观的工作。普通的程序中,如果程序崩溃了,你可以设置一个断点。这在每秒会被并行调用几百万次的运算中是不可能的。在着色器中使用 printf() 语句来调试哪里出错了也是不可能的,因为输出到哪里呢?考虑你的着色器运行在黑盒中,你怎么才能打开它然后看看为什么它们不工作呢?

你有一个可以使用的输出:我们的老朋友 gl_FragColorgl_FragColor 会给你一个输出,换一种思路想一想,你可以用它来调试你的代码。

所有你在屏幕上看到的颜色都是由一系列的数字表示的,这些数字是每一个像素的红绿蓝和透明度的百分比。你可以用这些知识来测试着色器的每一部分是不是像你构建的那样工作,从而确定它是不是按照你想的那样在运行。和一般调试不同,你不会得到一个可以打印的值,而是拿到一个颜色以及和它相关的某个指定值,依靠这些你可以进行逆向反推。

如果想知道你的一个在 0 和 1 之间的值,你可以把它设置给一个将要传入 gl_FragColorvec4 中。假设你把它设置进第一部分,就是红色值。这个值会被转换然后渲染到屏幕上,这时候你就可以检查它来确定原始的传进去的值是什么。

你会有几种方法来捕捉到这些值。从着色器输出的图片可以被捕获到然后作为图片写进磁盘里 (最好用户没有压缩过的格式)。这张图片之后就可以放进像 Photoshop 这样的应用,然后检查像素的颜色。

为了更快一些,你可以将图片用 OS X 的程序或者 iOS 的模拟器显示到屏幕上。在你的应用程序文件夹下的实用工具里有一个“数码测色计”的工具可以用来分析这些渲染过的视图。把鼠标放在桌面的任何一个像素点上,它都会精确的展示这个像素点 RGB 的值。因为 RGB 值在数码测色计和 Photoshop 中是从 0 到 255 而不是 从 0 到 1,你需要把你想要的值除以 255 来获得一个近似的输入值。

回顾下我们的球形折射着色器。简直无法想象没有任何测试就可以写下整个程序。我们有很大一块代码来确定当前处理的像素是不是在这个圆形当中。那段代码的结尾用 step() 函数来设置像素的这个值为 0.0 或者 1.0 。

把一个 vec4 的红色分量设为 step() 的输出,其他两个颜色值设为 0,然后传入gl_FragColor 中去。如果你的程序正确的运行,你将看到在黑色的屏幕上一个红色的圈。如果整个屏幕都是黑色,或者都是红色,那么肯定是有什么东西出错了。

性能调优

性能测试和调优是非常重要的事情。尤其是你想让你的应用在旧的 iOS 设备上也能流畅运行时。

测试着色器性能很重要,因为你总是不能确定一个东西的性能会怎样。着色器性能变化的很不直观。你会发现 Stack Overflow 上一个非常好的优化方案并不会加速你的着色器,因为你没有优化代码的真正瓶颈。即使仅只是调换你工程里的几行代码都有可能非常大的减少或增加渲染的时间。

分析的时候,我建议测算帧渲染的时间,而不是每秒钟渲染多少帧。帧渲染时间随着着色器的性能线性的增加或减少,这会让你观察你的影响更简单。FPS 是帧时间的倒数,在调优的时候可能会难于理解。最后,如果你使用 iPhone 的相机捕捉图像,它会根据场景的光亮来调整 FPS ,如果你依赖于此,会导致不准确的测量。

帧渲染时间是帧从开始处理到完全结束并且渲染到屏幕或者一张图片所花费的时间。许多移动 GPU 用一种叫做 “延迟渲染” 的技术,它会把渲染指令批量处理,并且只会在需要的时候才会处理。所以,需要计算整个渲染过程,而不是中间的操作过程,因为它们或许会以一种与你想象不同的顺序运行。

不同的设备上,桌面设备和移动设备上,优化也会很不相同。你或许需要在不同类型的设备上进行分析。例如,GPU 的性能在移动 iOS 设备上有了很大的提升。iPhone 5S 的 CPU 比 iPhone 4 快了接近十倍,而 GPU 则快上了好几百倍。

如果你在有着 A7 芯片或者更高的设备上测试你的应用,相比于 iPhone 5 或者更低版本的设备,你会获得非常不同的结果。Brad Larson 测试了高斯模糊在不同的设备上花费的时间,并且非常清晰的展示了在新设备上性能有着令人惊奇的提升:























iPhone 版本 帧渲染时间 (毫秒)
iPhone 4873
iPhone 4S145
iPhone 555
iPhone 5S3

你可以下载一个工具,Imagination Technologies PowerVR SDK,它会帮助你分析你的着色器,并且让你知道着色器渲染性能的最好的和最坏的情况 。为了保持高帧速率,使渲染着色器所需的周期数尽可能的低是很重要的。如果你想达成 60 帧每秒,你只有 16.67 毫秒来完成所有的处理。

这里有一些简单的方式来帮助你达成目标:


  • 消除条件逻辑: 有时候条件逻辑是必须得,但尽量最小化它。在着色器中使用像 step() 函数这样的变通方法可以帮助你避免一些昂贵的条件逻辑。


  • 减少依赖纹理的读取: 在片段着色器中取样时,如果纹理坐标不是直接以 varying 的方式传递进来,而是在片段着色器中进行计算时,就会发生依赖纹理的读取。依赖纹理的读取不能使用普通的纹理读取的缓存优化,会导致读取更慢。例如,如果你想从附近的像素取样,而不是计算和片段着色器中相邻像素的偏差,最好在顶点着色器中进行计算,然后把结果以 varying 的方式传入片段着色器。在 Brad Larson的文章中关于索贝尔边缘检测的部分有一个这方面的例子。


  • 让你的计算尽量简单: 如果你在避免一个昂贵的操作情况下可以获得一个近似的足够精度的值,你应该这样做。昂贵的计算包括调用三角函数 (像sin(), cos(), 和 tan())。


  • 如果可以的话,把工作转移到顶点着色器: 之前讲的关于依赖纹理的读取就是把纹理坐标计算转移到顶点着色器的很有意义的一种情况。如果一个计算在图片上会有相同的结果,或者线性的变化,看看能不能把计算移到顶点着色器进行。顶点着色器对每个顶点运行一次,片段着色器在每个像素上运行一次,所以在前者上的计算会比后者少很多。


  • 在移动设备上使用合适的精度 在特定的移动设备上,在向量上使用低精度的值会变得更快。在这些设备上,两个 lowp vec4 相加的操作可以在一个时钟周期内完成,而两个 highp vec4 相加则需要四个时钟周期。但是在桌面 GPU 和最近的移动 GPU 上,这变得不再那么重要,因为它们对低精度值的优化不同。


结论和资源

着色器刚开始看起来很吓人,但它们也仅仅是改装的 C 程序而已。创建着色器相关的所有事情,我们大多数都在某些情况下处理过,只不过在不同的上下文中罢了。

对于想深入了解着色器的人,我非常推荐的一件事就是回顾下三角学和线性代数。做相关工作的时候,我遇到的最大的阻力就是忘了很多大学学过的数学,因为我已经很长时间没有实际使用过它们了。

如果你的数学有些生疏了,我有一些书可以推荐给你:

也有数不清的关于GLSL书和特殊着色器被我们行业突出的人士创造出来:

还有,再一次强调,GPUImage是一个开源的资源,里面有一些非常酷的着色器。一个非常好的学习着色器的方式,就是拿一个你觉得很有意思的着色器,然后一行一行看下去,搜寻任何你不理解的部分。GPUImage 还有一个着色器设计的 Mac 端应用,可以让你测试着色器而不用准备 OpenGL 的代码。

学习有效的在代码中实现着色器可以给你带来很大的性能提升。不仅如此,着色器也使你可以做以前不可能做出来的东西。

学习着色器需要一些坚持和好奇心,但是并不是不可能的。如果一个 33 岁的还在康复中的新闻专业的人都能够克服她对数学的恐惧来处理着色器的话,那么你肯定也可以。

原文 GPU-Accelerated Image Processing 译文

扩展阅读 Advanced Graphics and Animations for iOS Apps

XMPP和SIP都是应用层协议,主要用于互联网上发送语音和即时通讯。 SIP在RFC 3621中定义,XMPP在RFC 3920中定义。XMPP是从即时通讯中演变而来,SIP是从VOIP中演变而来,XMPP为了会话协商添加了一个扩展叫做Jingle,SIP为了即时通讯业务添加了一个扩展叫做SIMPLE。

#SIP (Session Initiation Protocol)
SIP是一个应用层协议,是用在类似VOIP这样的场合,用来建立、修改、中止会话,同时在多人会议中他也能在已有会话中加入新的会话。基本上SIP是VOIP中的信令协议,它处理呼叫建立,呼叫转移和产生CDR(Call Detail Record,供通话计费用)。

#XMPP (Extensible Messaging Presence Protocol)
XMPP是一个为即时通讯和请求响应业务服务的XML协议,最早由Jabber开源社区在1999年开发,2002年XMPP工作组为了更适合即时通讯对Jabber进行了扩展。

#SIP和XMPP的异同
其实我们不能简单地拿SIP和XMPP做比对,就像我们不能直接比较苹果和橘子,前者主要是为了会话协商,后者主要是为了结构化数据交换,只不过随着各自对Simple和Jingle的引入,他们有了一些相似。

1、SIP提供连接的建立、修改和终止,而XMPP在客户端内部提供流管道、交换结构化数据。

也就是说:SIP的重点是终端之间连接的建立和维护,连接以后的数据和信息传送他不关注;而XMPP重点是考虑终端内部的数据交换,连接建立是基本的功能,而不是重点。所以,XMPP对应用的支持和扩展性的考虑很充分,比SIP天生要好。

2、SIP的信令和消息传送是基于文本的,不太好解析,或者说解析起来缺少规律性,在新增数据消息体的时候缺少继承性,需要开发新的代码来封装和解析,原有代码的继承性比较差。而XMPP采用XML,是一种结构化的消息结构,能够方便地表达层次化的内容,以及内容之间的内在逻辑。这种XML结构对应用的扩展和内容的解析带来极大的方便,大量软件代码可以复用。

3、SIP信令由header和body两部分组成,也就是说,SIP报文格式的header已经包含了部分内容,类似于HTTP,与具体的上层应用直接关联,而不是通用的报文格式;而XMPP所有信息都是采用XML在流管道之间透明传送。

SIP的连接建立通道与数据传送通道是各自独立的,连接建立在SIP client与Server之间,而数据传送通道是在Client–Client之间直接进行的。这个对视频、语音和文件传送业务很合适,但是不适合其他形式的应用。

XMPP的控制和数据通道是一体的,Clent只与Server建立连接,而Client与Client之间是没有之间连接的。Client之间传送的通道是:Client1—〉Server1—〉Server2—〉Client2。这种方式看起来扩展性差,server压力很大,但是能够实现很好的业务功能,比如留言、广播、群聊、状态更新、Blog、微博、数据共享等等。

这种C-S模型,很多业务的控制在Server上完成,新功能的增加在server上实现,在server上定义新的XML对象和逻辑,客户端只要负责XML数据流的解析和呈现就可以了, 所以,终端实现简单。

4、SIP可以使用UDP,TCP,TLS进行传送,而XMPP仅仅使用TCP和TLS进行发送.

5、SIP是双向对称,客户端和服务器都可以主动发起连接请求并响应,这种对称连接的方式在穿越NAT和Firewall的时候很麻烦,无法保证穿越NAT。而XMPP是单向的连接,只有Client可以向Server发起连接请求,Server不会向Client发起连接。这样便于NAT和Firewall的穿越。

UIViewControllerTransitioning

##UIViewControllerAnimatedTransitioning
采用UIViewControllerAnimatedTransitioning协议实现自定义转场动画,它提供了两个方法:

1、transitionDuration:指定转场的过渡时间。

2、animateTransition:创建转场动画。

在转场中涉及到的信息通过一个实现了UIViewControllerContextTransitioning的transitionContext上下文对象进行传递,使用这个transitionContext参数能移动或关闭目标控制器以及控制这个过程中持续的时间。

首先,我们需要自定义一个实现了UIViewControllerAnimatedTransitioning协议的对象。创建一个类CustomPresentAnimation继承自NSObject并遵守UIViewControllerAnimatedTransitioning协议。

CustomPresentAnimation.h

1
2
3
@interface CustomPresentAnimation : NSObject <UIViewControllerAnimatedTransitioning>

@end

CustomPresentAnimation.m

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
@implementation CustomPresentAnimation

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 1.0;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
//使用transitionContext可以得到参与切换的两个ViewController的信息
UIViewController *toVC= [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

//将控制器视图初始位置设置到屏幕下边缘,从而从屏幕下方出现
CGRect finalRect=[transitionContext finalFrameForViewController:toVC];
toVC.view.frame=CGRectOffset(finalRect, 0, [[UIScreen mainScreen] bounds].size.height);

//所有的动画视图都必须放在transitionContext的containerView里
[[transitionContext containerView] addSubview:toVC.view];

[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.5 initialSpringVelocity:0.5 options:UIViewAnimationOptionCurveEaseInOut animations:^{
toVC.view.frame=finalRect;
} completion:^(BOOL finished) {
//动画完成或者取消之后必须得调用的方法,系统接收到这个消息后将对控制器的状态进行维护
[transitionContext completeTransition:YES];
}];
}

##UIViewControllerTransitioningDelegate
实现UIViewControllerTransitioningDelegate协议的对象在控制器 presented 或者 dismissed 时返回上面继承自NSObject并遵守UIViewControllerAnimatedTransitioning协议的CustomPresentAnimation对象。这个接口的作用比较单一,在需要控制器切换的时候会向实现了这个接口的对象询问是否需要使用自定义的切换效果。我们可以让需要实现切换的控制器实现这个协议。

PresentedViewController.m

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
@interface PresentedViewController () <UIViewControllerTransitioningDelegate>
@property (nonatomic,strong) CustomPresentAnimation *presentAnimation;
@end

@implementation PresentedViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.transitioningDelegate=self;
self.presentAnimation=[[CustomPresentAnimation alloc] init];
}

- (IBAction)clickDismmissButton:(UIButton *)sender {

[self dismissViewControllerAnimated:YES completion:^{
NSLog(@"clickDismmissButton");
}];
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
return self.presentAnimation;
}
@end

UIViewController中自定义转场动画到这就结束了,点击查看的完整代码。

让你的app绚起来

iOS开发系列–让你的应用“动”起来

iOS自定义转场详解

关于自定义转场动画,我都告诉你。

iOS自定义转场

Git 是一个开源的分布式版本控制系统,用以有效、高速的处理从很小到非常大的项目版本管理。Git 是 Linux Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。 在分布式版本控制上,使用 Git 会比其他解决方案更可靠。

#前提
默认进入到了工程目录,即 cd xxx。

在安装好git后第一件是是设置你的名字和电子邮箱,因为每次提交都要用到这些信息:

1
2
git config --user.name "Your name"
git config --user.email "Your email"

#查看其他指令的用途及配置指令
1、git help

2、git config user.name ‘zhuli8’

3、git config user.email ‘zhuli1228@163.com’

4、git config –list:查看配置

5、git log:查看日志

#操作github上的已有项目
注意:

使用Xcode工具连接github仓库时使用的用户名必须是自己GitHub上的账号名而不是邮箱。

1、clone远程仓库

git clone https://github.com/zhuli8com/transition.git

2、添加文件

git add . 或者git add test.txt

3、提交到本地

git commit -m ‘注释’

4、提交到github主分支

git push origin master

5、删除文件test.txt文件

git delete test.txt,重复3、4步骤

6、从远程更新本地代码

git pull origin master

#创建本地仓库并上传github
1、初始化一个版本仓库

git init

2、添加远程版本库

git remote add [shortname] [url]

git remote add origin https://github.com/zhuli8com/transition.git

3、查看远程仓库

git remote -v

4、下拉远程仓库文件到本地,抓取远程仓库master分支更新合并到本地

git pull origin master (最最关键的步骤)

5、添加到本地暂存区

git add .

6、提交到本地版本库

git commit -m ‘注释’

7、提交到github,将本地分支推到远程主分支

git push origin master

tag

用git tag打标签只是在本地仓库打标签而已,为了能把标签同步到远程服务器,我们可以这样做:

默认情况下,git push并不会把tag标签传送到远端服务器上,只有通过显式命令才能分享标签到远端仓库。

  1. push单个tag,命令格式为:git push origin [tagname]

例如:

1
git push origin v1.0 #将本地v1.0的tag推送到远端服务器

  1. push所有tag,命令格式为:git push [origin] –tags

例如:

1
2
3
git push --tags

git push origin --tags

删除分支

1
2
git tag -d <tagname>
git push origin :refs/tags/<tagname>

参考

GIT分支管理是一门艺术

Git使用教程

git常用命令

Xcode 6.3 用 GitHub 托管项目

#宏定义

不带参数的宏定义

1
#define 宏名 字符串

它的作用是在编译预处理时,将源程序中所有”宏名”替换成右边的”字符串”,常用来定义常量。

1.宏名一般用大写字母,以便与变量名区别开来,但用小写也没有语法错误

2.在编译预处理用字符串替换宏名时,不作语法检查,只是简单的字符串替换。只有在编译的时候才对已经展开宏名的源程序进行语法检查

3.宏名的有效范围是从定义位置到文件结束。如果需要终止宏定义的作用域,可以用#undef命令

带参数的宏定义

1
#define 宏名(参数列表) 字符串

在编译预处理时,将源程序中所有宏名替换成字符串,并且将 字符串中的参数 用 宏名右边参数列表 中的参数替换

1.宏名和参数列表之间不能有空格,否则空格后面的所有字符串都作为替换的字符串

2.带参数的宏在展开时,只作简单的字符和参数的替换,不进行任何计算操作。所以在定义宏时,一般用一个小括号括住字符串的参数。

3.计算结果最好也用括号括起来

条件编译

在很多情况下,我们希望程序的其中一部分代码只有在满足一定条件时才进行编译,否则不参与编译(只有参与编译的代码最终才能被执行),这就是条件编译。

1
2
3
4
5
6
7
#if 条件1
...code1...
#elif 条件2
...code2...
#else
...code3...
#endif

#if 和 #elif后面的条件一般是判断宏定义而不是判断变量,因为条件编译是在编译之前做的判断,宏定义也是编译之前定义的,而变量是在运行时才产生的、才有使用的意义

其他

  1. #if defined()和#if !defined()
  2. #ifdef和#ifndef

文件包含

#include的作用纯粹就是内容拷贝

1
#include <文件名>

直接到C语言库函数头文件所在的目录中寻找文件

1
#include "文件名"

系统会先在源程序当前目录下寻找,若找不到,再到操作系统的path路径中查找,最后才到C语言库函数头文件所在目录中查找

其他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
one.h
#ifndef _ONE_H_
#define _ONE_H_
void one();
#endif

tow.h
#ifndef _TWO_H_
#define _TWO_H_
#include "one.h"
void two();
#endif

main.h
#include "one.h"
#include "two.h"
int main()
{

return 0;
}

最后的main.h的文件相当于下面的代码:

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
// #include "one.h"
#ifndef _ONE_H_
#define _ONE_H_

void one();

#endif

// #include "two.h"
#ifndef _TWO_H_
#define _TWO_H_

// #include "one.h"
#ifndef _ONE_H_
#define _ONE_H_

void one();

#endif

void two();

#endif
int main()
{

return 0;
}

vim的学习曲线相当的大,下面推荐一个靠谱的文章

存活

mac系统自带vim,当在终端输入“vim/vi 文件名”时会启动vim,默认在normal模式,按下键 i 进入insert模式(此时可以输入文本,就像用记事本一样),如果你想返回normal模式按 esc 键。

i → Insert 模式,按 ESC 回到 Normal 模式.
x → 删当前光标所在的一个字符。
:wq → 存盘 + 退出 (:w 存盘, :q 退出)   (陈皓注::w 后可以跟文件名)
dd → 删除当前行,并把删除的行存到剪贴板里
p → 粘贴剪贴板
u → 撤销上一步的操作
Ctrl+r → 恢复上一步被撤销的操作

推荐:

hjkl (强例推荐使用其移动光标,但不必需) →你也可以使用光标键 (←↓↑→). 注: j 就像下箭头。
:help <command> → 显示相关命令的帮助。你也可以就输入 :help 而不跟命令。(退出帮助需要输入:q)

感觉良好

所有的命令都需要在Normal模式下使用,如果你不知道现在在什么样的模式,你就狂按几次ESC键。

插入

o → 在当前行后插入一个新行
O → 在当前行前插入一个新行

移动光标

^ → 到本行第一个不是blank字符的位置(所谓blank字符就是空格,tab,换行,回车等)
$ → 到本行行尾
/pattern → 搜索 pattern 的字符串(如果搜索出多个匹配,可按n键到下一个)

拷贝/粘贴

p/P都可以,p是表示在当前位置之后,P表示在当前位置之前。

yy → 拷贝当前行当行于 ddP
P → 粘贴

respondsToSelector&instancesRespondToSelector

respondsToSelector既可以检查类(是否响应指定类方法),也可以检查实例(是否响应制定实例)。

instancesRespondToSelector只能响应类方法。

isKindOfClass&isMemberOfClass

isKindOfClass判断是否是这个类或者这个类的子类的实例。

isMemberOfClass判断是否是这个类的实例。

static&extern

extern定义和声明外部函数,是默认值;static定义和声明内部函数。

extern声明外部全局变量,用在const常量优化,头文件用;static定义局部变量,延长生命周期。

const

用const修饰一般变量

在C语言中,用const修饰的变量必须在声明时进行初始化(用来修饰函数的形参除外)。

1
2
3
4
5
6
const int n;  这种声明方式是错误的
const int n=5; 正确
void fun(const int n); 正确
const char a; 错误
char * const p; 错误
const char *p;正确(注意这种为什么是正确的),因为这里const是修饰p指向的变量,而不是指针变量p本身

const与指针搭配使用

因为指针本身也是一个变量,只不过指针存放的是地址而已,而一旦指针变成了常量,即指针本身的值是不可变的,此时指针只能指向固定的存储单元;指针一般会指向一个变量,如果该变量成为一个常量,那么该变量的值就不能被修改,即常量指针,指针指向的是一个不可变的变量。

1
2
3
4
5
int a=3;
const int *p=&a;
int const *p1=&a;
*p=4;
a=4;

第2行和第3行是等价的;第4行错误,因为用const限定p指向的变量的值是不可以修改的,即不可以通过指针p去修改变量a的值;第5行是正确的,因为a本身没用const进行修饰,即a本事的值事可以修改的。

1
2
3
4
5
6
7
8
9
10
int a=1;
int b=2;
const int*p;
int const*p1;
int* const p2;
int* const p3=&b
p=&a;
p1=&a;
p2=&b;
p3=&a;

第3、4、6、7、8行是正确的;第5、10行是错误的,因为const在*后面表示是来修饰指针变量本身,因此在声明时必须进行初始化,并且在后面不能再指向其他的变量。

C++中的const

C语言和C++中的const有很大区别。在C语言中用const修饰的变量仍然是一个变量;而在C++中用const修饰过后,就变成常量了。

总结:

(1)在C语言中用const去修饰一个变量,表示这个变量是只读的,不可通过显式的调用a去修改a的值,并且此时a仍然是一个变 量,不能等同于常量;

(2)要注意const在声明变量时所处的位置,位置不同,在意义上可能会有很大的不同。(前固值,后固址。)

如果const在’*’左边,则表示指针指向的变量的值不可变;

如果const在’*’右边,则表示指针的值是不可变的;

1
2
3
4
5
6
7
8
9
10
const int a;
int const a;
const int *a;
int const *a;
int * const a;
int const * const a;
1> 前两个的作用是一样:a 是一个常整型数
2> 第三、四个意味着 a 是一个指向常整型数的指针(整型数是不可修改的,但指针可以)
3> 第五个的意思:a 是一个指向整型数的常指针(指针指向的整型数是可以修改的,但指针是不可修改的)
4> 最后一个意味着:a 是一个指向常整型数的常指针(指针指向的整型数是不可修改的,同时指针也是不可修改的)

注:a是指针变量,*a是取指针对应的地址中的数据;一般修改变量其地址不变,修改指针即为修改指针变量的地址其内容不变

NSTimer

1
2
3
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

schedule…方法默认加入到default运行循环(默认运行循环监听普通的点击等事件,滑动等事件需要common模式)。

1
2
3
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

timer…默认没有加到任何runloop。

NSTimer不精确,适用于周期较长不精确的计时,否则使用CADisplayLink。

delegate命名

should表示一个动作发生前,通常带有返回值,可以在动作发生之前改变对象状态。

will在动作发生前,委托可以对动作做出响应,但不带有返回值。

did在动作发生后做出的响应。

load&initialize

+load 方法是当类或分类被添加到 Objective-C runtime 时被调用的,实现这个方法可以让我们在类加载的时候执行一些类相关的行为。子类的 +load 方法会在它的所有父类的 +load 方法之后执行,而分类的 +load 方法会在它的主类的 +load 方法之后执行。但是不同的类之间的 +load 方法的调用顺序是不确定的。

子类、父类和分类中的 +load 方法的实现是被区别对待的。也就是说如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用。因此,我们常常可以利用这个特性做一些“邪恶”的事情,比如说方法混淆(Method Swizzling)。

+initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说 +initialize 方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize 方法是永远不会被调用的。那这样设计有什么好处呢?好处是显而易见的,那就是节省系统资源,避免浪费。

如果一个子类没有实现 +initialize 方法,那么父类的实现是会被执行多次的。有时候,这可能是你想要的;但如果我们想确保自己的 +initialize 方法只执行一次,避免多次执行可能带来的副作用时,我们可以使用下面的代码来实现:

1
2
3
4
5
+ (void)initialize {
if (self == [ClassName self]) {
// ... do the initialization ...
}
}