普通视图

发现新文章,点击刷新页面。
昨天以前libfeihu Blog

追查闪退的六个日与夜

作者 feihu
2021年1月7日 00:00

老Y是一名iOS开发,近日在工作遇到一个闪退,临近提交App Store时才发现,虽然紧急修复后顺利提交审核,但由于问题暴露的时间过晚,因此引起了众多额外的关注。问题本身理应很容易发现,但为什么临近审核才反馈,老Y为了分析该问题,花了六天的时间才找到原因。在一个具有数百个模块的大型App中,如何根据各种蛛丝马迹找出问题根因?本文记录了老Y追查该问题的六个日与夜。

闪退不可怕,开发与测试的过程都应该能够发现,这个问题本身没有特别值得记录的地方。可怕的是为什么经过这么多轮测试却没能发现闪退,开发联调阶段也没能发现闪退,正是为了解释这些才有了本文。

如果你的App中也使用了PLCrashReporter,或者你对老Y追寻六天的过程感兴趣,不妨看完这个故事。如果想直接看结论,可跳至后记

目录


第0天 阻审

晚上11点,老Y刚洗完澡准备睡觉之际,看了一眼手机,发现被拉进一个群,群名称:”xxx crash”,心想:“我去,今晚就要提交App Store审核,该不会这个点出现问题了吧。”老Y点进去一看消息,不幸言中。业务发现一个必现的闪退,其它同学已经可以在本地复现,正在分析问题产生的原因,PM在等着结论,决定何时提交审核。

这样的情形对于老Y来说已经不会再大惊小怪,多年的互联网从业经历让老Y养成了电脑随时带在身边的习惯。12月的深圳此时终于有了点冬天的样子,夜里气温很低。于是老Y披上外套,拿着电脑来到客厅。

打开电脑后,老Y首先看了看群里的上下文,目前了解到的情况是:业务每天都有进行测试,但之前一直未发现问题。而今晚却在另外一个安装包中发现该问题是必现的。“之前的安装包与今晚测试的安装包有什么不同?”,老Y开始与反馈该问题的同学一起分析。刚开始推断可能前几天有几个模块进过一次集成,但很快这个猜想就被推翻。测试发现集成前后的安装包都会出现该闪退。

测试在开发的询问下,不断的提供越来越多的信息,完整的问题逐渐显现出来。终于,在午夜钟声敲响之际,定位到是debug包与release包的问题。之前测试使用的都是debug包在测试,而今晚切换到了release包。“又是release与debug的问题,一定是有什么变量没有初始化导致的,release下未初始变量可能是个随机值”,凭借之前踩过的很多坑,老Y信心满满的猜想。

而此时,负责该模块的同学老J本地调试也发现了该问题,准确的定位到闪退处。从Xcode的debug窗口中可以明显的看到一个空的数组指针,访问该数组时出现EXC_BAD_ACCESS闪退。那么问题看起来很容易解释,只需要查出为什么该数组是空指针就能够解决问题,笑容在老Y与老J脸上浮现出来。

可是,他们高兴的太早。这个数组指针在一个函数中赋值,但该函数没有异常路径,数组一定不会为空。“这就奇怪了,这个函数出了什么问题?”,老Y与屏幕另一端的老J都感到不解。为了弄清楚函数执行时发生了什么事,老J在该函数中设置了一个断点,打算单步调试该函数,这是最直接的方式。

没想到奇怪的一幕再次发生,老J刚刚设置的断点竟然不生效。老J不敢相信自己的眼睛,于是又在函数调用前与调用后又加了断点,再次运行。函数前的断点正常停下,“不错,证明代码走到了这里”,老J开始紧张起来,小心的点了单步执行,没想到程序直接到了函数调用之后的断点,函数中设置的断点依然没有停下来。

“这不可能啊,同一个代码库中的代码,函数前后的断点都能够停下,为什么这个函数就是进不去?”他们大为不解。此时,老Y说,用lldb命令试试吧。于是老J再次调试,在调试控制台中输入了breakpoint set -name xxx,再次运行,他们紧张的期待着运行的结果。

老Y说:有时候在Xcode的源代码中直接添加断点无法进入时,可以通过lldb或者添加符号断点的方式来设置断点

终于,如他们所愿,程序在函数入口的断点处停了下来,然而展现在断点处的竟然是汇编。“源文件明明就在工程里面,为什么会进到汇编?”,老J说。“clean下试试。”于是老J将整个工程清理了一遍,为以防万一,又手动删除了Derived Data,然后再次尝试。可结果依然如此。

“直接用代码编译的,又是同一个工程下,函数前后可以断在代码处,所以排除了静态库的影响。”老Y分析道,“难道?。。。”老Y此时心里冒出了一个大胆的想法,于是他和老J说,“VNC地址发我下,我VNC过去看看”。随后老Y连上了老J的电脑,打开了命令行,在工程目录中敲了一个命令,开始对所有的静态库查找断点函数的符号。当命令运行结束时,大家看到结果后心里似乎明白了老Y这么做的原因。原来,这个函数在另外一个静态库F中也存在一份。听到这个消息后,写下这个函数的另外一名同学老C想到了什么,突然说,“这个函数是是我从一个静态库中拷贝过来的,并且更新了代码。”

这时,老Y与老J都明白了问题所在,他们讨论道:“静态库X与F中有两个同样的符号,但是似乎真正链接到二进制中只保留了一个”,老J补充道,“是的,所以业务在运行时,本应该执行静态库X的代码,实际上却执行了静态库F,后者没有对数组指针赋值,便导致了闪退的出现”。

“我们要快速解决这个问题,PM还等着提交App Store呢”,老J说,“把函数名都改一下吧。”

“可是相似的代码还有挺多,一个个去修改风险比较高,直接加上namespace作隔离吧。”老Y提议,于是他们快速给相似的符号加上了新的namespace,再次本地打包验证,问题解决,此时时间刚过午夜1点。

本地验证通过之后,他们立刻在服务端打包,发给测试及业务同学验证。从问题出现到解决只花了一个多小时,如果不是因为debug无法进入断点,根本不可能这么快。在等待测试同学反馈给果时,老J问老Y,“怎么会想到去运行那个命令去查找所有静态库中的符号?”“其实也是比较偶然,已经是同一份代码进行编译,符号一定是同时被加载的,一些函数可以进入断点,另外一些无法进入,这有点说不通。我在前一家公司遇到过类似的问题”,老Y回忆起了往事,“但那时是动态库,两个库中也有相同的符号,当时现象并不是必现的闪退,而是概率性的出现。后来分析了很久才找到原因,结果是一个叫做 Global Symbol Interpose的问题,最终也是通过不同的namespace才解决了问题。”

老Y说:全局符号介入(Global Symbol Interpose)是在动态链接加载的过程中出现的,Linux采用广度优先搜索的方式来加载每个库中的符号,它会将每个符号都放到一个全局的符号表中,如果表中已经有了这个符号,那么它会忽略后面需要加载的符号

“但是为什么测试验证时,debug包不会出现闪退呢,按理说这是必现的问题”,老J问,“我推测是debug与release在链接两个库时的顺序不一致导致的,debug与release分别链接了不同的符号,导致debug是期望的符号,release是另外一个静态库的符号。但为什么这样我也还不清楚,现在也只是猜测”,老Y解释道。

正当他们讨论之际,测试同学反馈新包问题已解决。此时已经是夜里2点,PM还在等待着他们的结论,看到老Y他们在群里的结论之后,PM给老Y打了电话,老Y说明了结论和问题之后,PM问道,“为什么这个时候才发现?现在阻塞了提审,今晚肯定无法提交了,明天再提交吧。这个问题需要做一个复盘。”老Y想解释,想说这一定不是我们的问题,但无奈现在也只是推测,而且确实阻塞了提审,也没有作更多的辩解。挂了电话之后,老Y有些气愤,虽然他理解PM面临的压力,可是还没搞清楚状况就劈头盖脸一顿责问,这让老Y有些难以接受。/p p“我们先申请模块集成吧,为什么debug包无法出现闪退的问题明天继续分析”,集成申请之后,他们便合上了电脑,兴奋中夹杂着疲惫,沉沉的睡去。/p p老J和老Y快速解决了阻塞提审的问题,但似乎这个问题本身并不是很严重,更加严重的是为什么一个本该必现的问题这么晚才验证出来。老Y隐隐感到这里面有一个巨坑,也因为PM的一通电话,他下定决心要查明该问题,还团队一个清白。但没想到的是,这一查就是足足六天,接下来的六天时间里,老Y一步步找出了问题,原来那是一个隐藏了多年的bug。/p h2 id=第1天-嫌疑第1天 嫌疑/h2 p第二天老Y早早醒来,虽然前一天折腾到很晚才睡,但精神还不错。在去公司的路上,他就向负责构建的同学L描述了这个问题。L是打包这块的专家,他对打包过程极其了解。/p p互联网公司的上班时间相比传统公司较晚,9点公司楼下人依然很少。老Y在公司吃完早餐后,泡了一杯龙井茶振作精神,到了这个年纪,大家的标配似乎已经是枸杞菊花保温杯,但老Y还是喜欢喝点茶。一口热茶暖到胃里,老Y心想,“开始吧”。于是他打开了电脑,准备投入工作。/p p电脑是一台几年前的Macbook Pro,已经服役三年,很快就要退休。近段时间电脑越来越无法跟上老Y的使用强度,当然,也可能是跟不上软件的更新速度,隔三叉五的就会罢工一次。/p p对于前一天的问题,老Y已经有一个初步的推断,“debug与release包链接了不同的符号”,但为什么会这样,还需要等待构建专家L的分析。L收到了早上的消息后,与老Y通了电话,对问题进行了详细的询问,了解到问题之后,L心里也犯起了嘀咕,“这个问题还是得详细的查下,万一是打包平台的锅,影响可就大了。”于是他们先兵分两路,老Y准备下午的复盘文档,测试同学定了下午2点的复盘会议。而L去分析打包的问题。/p p老Y这么多年一直有个工作习惯,在遇到复杂问题时,会创建一个笔记,用于记录对问题的分析及进展。一方面有助于理清思路,防止被其它问题中断后无法迅速找到之前的状态。另一方面通过详细的记录,又可以后续对问题进行总结,遇到相似问题时能够通过这些记录找到当时详细的分析过程。于是他新建了一个笔记,取名:“XXX阻审问题分析”,笔记创建完成后,他首先记录了当前的问题与进展,接着开始准备复盘的文档,很快就将问题、解决方案、后续如何避免等写完,但其中有一个点还没有结论,也就是为什么提审前才发现问题,这个点老Y觉得可能问题没那么简单。/p p果然,在复盘文档刚刚写完时,L来了消息,印证了老Y的担忧。L发现不管是debug包还是release包,都是一样的符号,符号来自静态库F而不是静态库X。“如此说来,前面的推断都错了,并不是链接了不同的符号导致现象不同”,老Y说,然后接着问,“两个包在链接静态库的时候,依赖的顺序是一样的对吗?”L回答,“是的,顺序是固定的,和debug、release无关。”/p p“明白了,这样就不存在一个静态库的链接顺序导致不同符号被链接的问题”,老Y分析说,“但这样解释不通,如果是同样的符号,应该debug包也有同样的问题,而不仅是release包才出现闪退。”/p p“debug和release包除了宏及优化level不同以外,还有其它的区别吗?”老Y问L。/p p“有的,debug包里面多了一些测试模块,一会我做一些实验验证下是否是这些测试模块的问题”,L回答。接下来他便与测试同学一起验证有关测试模块的猜想。/p p老Y在等待L分析具体依赖差别的时候,又想了想,“昨天业务测试的反馈信息有些多,需要重新梳理一遍,究竟什么样的包出现,什么样的包不出现,先整理一个详细的列表出来。然后看看问题可能出现在哪个包中”,于是他将昨天修复问题前后各个场景都打了一个安装包并测试,结果发现只有在release包中才会闪退,其它所有安装包都正常。/p p“空指针访问时闪退是必然的,这样就可以集中精力定位为什么debug包不闪退,现在关键的问题在于找出空指针访问后,本该闪退的线程现在在做什么。先在本地尝试保持与debug包一样的版本依赖,看本地是否可以复现该问题。”于是老Y找到对应debug包依赖的各模块版本,然后搭建本地环境,开始调试。但让他没想到的是,本地不管是debug还是release,都是必现的闪退,程序停留在空指针访问的那一行,code class=language-plaintext highlighter-rougeEXC_BAD_ACCESS/code异常。/p p“奇怪,本地打包与后台打包怎么会表现不一样,难道后台还有很多其它的设置?”老Y很困惑。“既然本地无法复现这个问题,那只有增加一些日志,查看这个线程在空指针访问的前后在做什么”,老Y便在前前后后增加了大量的日志,期望这些日志可以告诉他发生了什么事。由于本地无法复现,所以他只好将代码提交,并依赖后台打出来的包才能够获得日志。提交代码之后,便安心等待后台的安装包构建。因为安装包依赖了数百个模块,所以每次的等待都需要接近十分钟的时间。在这期间,老Y又问了L那边的进展,L发现debug与release总共有6个测试模块不同,而去除了这些测试模块的debug包让测试同学去验证时,如期望的那样闪退了。“也就是说。。。”老Y激动的说,“没错,也就是这六个测试模块导致了本该出现的闪退被吞掉”,L回答,但L有一点忘了说,有一次测试时发现没有测试模块的debug包也未出现闪退。/p p“帮忙把这6个模块的名字发出来,我打算找出它们的负责人,然后拉个群问问看它们有没有在这些测试模块中catch住这个闪退。”老Y激动的说,L便去查找每个模块的负责人。正当此时,老Y增加日志的安装包已经构建完成,他下载并安装了这个包,然后熟练的在业务环境去验证。果然,这个包依然没有闪退。接着他获取了程序运行的日志并开始分析。/p p现象很奇怪,日志中有刚刚老Y增加的部分,在野指针访问之前,一切都正常,并且也证实了他们之前的猜想,代码走到了另外一个静态库F中,而这个旧的代码并没有初始化新增加的成员变量,这也是导致野指针的原因。可是,时间在这个线程仿佛停止了,野指针访问之后的日志都没有出现。同时,接下来的所有日志中再也没有出现该线程的其它任何活动。“奇怪,看起来像是这个线程在访问了野指针之后被干掉了一样,会发生这种事情吗?”,老Y疑惑的想,“难道是那几个测试模块把闪退捕获了,但没有处理好?”/p p这时,L拿到了这6个模块的名称,并找到了对应负责的同学。老Y将他们拉到一个新建的群中,然后描述了所遇到的问题:“各位测试模块的owner们,我们这边遇到一个棘手的问题,一个C++的必现闪退在debug包中不会出现,但在release包中会出现,对比发现debug包里面多了各位的测试模块。在必现闪退的地方,打了日志发现在闪退前的位置有日志,闪退后的位置没有日志,且整个线程也看起来被干掉了,所以想问下各位,有没有哪位的模块把C++的野指针访问异常catch住了。”在漫长的等待之后,有几个模块的负责人表示其负责的模块中并没有捕获闪退的代码。/p p老Y也继续尝试增加日志,发现不管是空指针,还是野指针,都一样不会闪退。刚开始他还怀疑是野指针是不是有可能访问到一个有效的地址而没有闪退,空指针才会闪退,但打印出来的日志粉碎了他这一猜想。时间就在他们不停的尝试与分析当中迅速流逝,夜幕很快就降临,在与团队同学一起吃饭时,他描述了问题以及当前的进展,大家也都表示这个问题很诡异,没有遇到过类似的问题。/p p当天是周五,看这天的进度,问题原因恐怕很难被快速定位出来,所以复盘也从2点推迟到4点,虽然最终阻塞提审的问题早就解决了,但是背后影响更大的问题还没有思路,如果这个问题不分析出来,复盘意义不大。所以老Y与测试同学讨论将复盘改在下周进行。/p p老Y所在的公司地理位置较好,楼下有个海边公园,每次吃完晚饭,大家便不约而同的绕着海边散步,缓解一天的疲劳。但今天老Y却没有和大家一起去,他打算尽量在周末之前搞定,这样便不用在周末还挂念此事,于是吃完晚饭之后,老Y立刻回到了办公位,他还是想着在本地复现这个问题,定位出具体哪个测试模块是罪魁祸首。他又对比本地与后台安装包的模块与版本是否完全一致,以防止出现遗漏。经过仔细对比,发现确实有几个模块在本地没有,于是他又尝试去后台下载了这几个缺少的模块,手动链接这些库,但还是和之前一样,本地必现的闪退,code class=language-plaintext highlighter-rougeEXC_BAD_ACCESS/code,无法在本地验证。/p p不知不觉折腾到11点多,老Y周末之前搞定的计划落空。很无奈,本地无法复现,只能依赖服务端。他找到L,“我们目前的结论是6个测试模块捕获了闪退异常,有没有办法能够在服务端尝试加载不同的测试模块,确定是哪个测试模块吞了这个闪退吗?”好在L也很重视这个问题,即便是周末,也答应了一定帮定位出是哪个模块,这让折腾一天疲惫的老Y心里感到一丝安慰。老Y查了一整天,但总有些力不从心的感觉,无法在本地复现问题,甚至打个日志验证都需要10分钟以上的时间,身心俱疲。老Y准备周末好好休息一下,调整好状态,周一接着查。/p h2 id=第2天-堆栈第2天 堆栈/h2 p深圳虽已是冬季,但仍和中部地区的初秋差不多,一件薄外套即可抵御寒冷。一年当中深圳从3月-11月都是夏季,只有此时的阳光显得温暖却不炎热,即便是正午,也可在户外活动。深圳遍布的公园草地上都铺满了大大小小的野餐垫,孩子们与爸爸妈妈享受这个快节奏城市里难得悠闲的时光,空中飘着各色各样的风筝,宛如秋游的景象,如果不看日历的话,绝难想到此时已是12月中旬。/p p老Y晚上睡得不好,好像做梦都在思考这个问题。早上起来之后,老Y回顾了昨天的分析过程,“这个线程后面没有了任何的日志,那么它到底在干什么?是线程被干掉了吗?还是说函数异常中止去做了其它的事情?”,“有什么办法能够获取到当前这个线程的状态?”,这几个问题一直萦绕在老Y的心里。整个上午老Y做什么事都一幅心不在焉的样子。午饭过后,突然间老Y有了灵感,“是否能用什么手段让整个应用崩掉,从而获取所有线程的堆栈?”/p p顺着这个思路,老Y想到一个办法。此刻也顾不得午睡,打开电脑便开始了尝试。他想在另外一个线程直接通过ObjC的方式来让程序崩掉,让一个程序崩掉的方法有很多,因为之前是C++的闪退无法处理,那么这里就通过ObjC的方式。很轻松的,老Y写了几行代码,直接创建一个ObjC对象,然后向它发送一个并不存在的方法。“这样便会出现对象无法响应方法的闪退了”,老Y自言自语道,“那么在什么时机来调用这个方法?先试试压后台吧。”老Y在一个类的code class=language-plaintext highlighter-rougeload/code方法中注册了应用压后台的通知,并在回调中调用了触发闪退的方法。/p div class=language-objc highlighter-rougediv class=highlightpre class=highlightcodespan class=k-/span span class=p(/spanspan class=ktvoid/spanspan class=p)/spanspan class=ntriggerCrash/span span class=p{/span span class=nXLog/spanspan class=p(/spanspan class=s@enter crash/spanspan class=p);/span span class=nUIView/span span class=o*/spanspan class=nview/span span class=o=/span span class=p[[/spanspan class=nUIView/span span class=nfalloc/spanspan class=p]/span span class=nfinitWithFrame/spanspan class=p:/spanspan class=nCGRectZero/spanspan class=p]];/span span class=p[/spanspan class=nview/span span class=nfperformSelector/spanspan class=p:/spanspan class=k@selector/spanspan class=p(/spanspan class=nhello/spanspan class=p)];/span span class=nXLog/spanspan class=p(/spanspan class=s@leave crash/spanspan class=p);/span span class=p}/span /code/pre/div/div p接着提交代码,等待漫长的打包结果。打包成功后,老Y立刻安装并进行了测试,首先是测试业务流程,空指针访问处没有闪退。接着程序压入后台,如他所料,闪退出现。老Y兴奋的赶紧去服务端捞取对应的闪退报告,可奇怪的是,服务端的最新报告日期竟然还是前一天,刚刚的尝试完全没有闪退报告。“难道是OOM了?只有这种情况下才没有报告”,老Y猜想。他又试了一次,更为奇怪的是,程序在还未进入后台时就崩了,服务端依然没有闪退报告。因为老Y的注意力都在没有查到闪退报告这件事上,所以他忽略了为什么程序还未进入后台时就崩溃这个细节。/p p这下老Y突然有些不知所措,怪事一件接一件的发生,但好在他已经做好了没那么顺利的心理准备,冷静下来之后想,“之前即使是发生了OOM,在Xcode中应该可以直接从手机上捞出来对应的报告,这是由系统生成的,可以试着捞一下”,老Y便连上手机,在Xcode中查看设备日志。/p blockquote p老Y说: 系统在应用闪退之后,会生成一份闪退报告,这里甚至会包含OOM类型的报告,可以通过Xcode的设备日志获取。/p /blockquote p在等待日志同步之后,老Y发现有两份闪退日志,但都不是OOM,而是普通的闪退。最新闪退是第二次的尝试,未压后台就崩掉,它与之前业务测试时发现的报告完全一致。挂在了空指针的访问处,闪退线程如下:/p div class=language-cpp highlighter-rougediv class=highlightpre class=highlightcodespan class=nThread/span span class=mi54/span span class=nCrashed/spanspan class=o:/span span class=mi0/span span class=nXXX/span span class=mh0x0000000108ab01c0/span span class=nA/spanspan class=o::/spanspan class=nrun/spanspan class=p(/spanspan class=nImage/spanspan class=oamp;/spanspan class=p,/span span class=ktfloat/spanspan class=o*/spanspan class=p,/span span class=ktint/spanspan class=p,/span span class=ktbool/spanspan class=p)/span span class=o+/span span class=mi99549632/span span class=p(/spanspan class=nfile/spanspan class=p./spanspan class=ncpp/spanspan class=o:/spanspan class=mi259/spanspan class=p)/span span class=p.../span /code/pre/div/div p老Y又打开了另外一份报告,惊喜的发现这便是他这次实验想获取的那份。闪退的原因正如代码所写,在压后台时上面的code class=language-plaintext highlighter-rougetriggerCrash/code方法闪退,找不到对应的code class=language-plaintext highlighter-rougeselector/code。接着他向下查找其它线程。终于,在最后一个线程,那个本该闪退的地方出现了:/p div class=language-cpp highlighter-rougediv class=highlightpre class=highlightcodespan class=nThread/span span class=mi54/span span class=nname/spanspan class=o:/span span class=nDispatch/span span class=nqueue/spanspan class=o:/span span class=ncom/spanspan class=p./spanspan class=nxxx/spanspan class=p./spanspan class=nxxx/span span class=nThread/span span class=mi54/spanspan class=o:/span span class=mi0/span span class=nXXX/span span class=mh0x00000001066d01c0/span span class=nA/spanspan class=o::/spanspan class=nrun/spanspan class=p(/spanspan class=nImage/spanspan class=oamp;/spanspan class=p,/span span class=ktfloat/spanspan class=o*/spanspan class=p,/span span class=ktint/spanspan class=p,/span span class=ktbool/spanspan class=p)/span span class=o+/span span class=mi99549632/span span class=p(/spanspan class=nfile/spanspan class=p./spanspan class=ncpp/spanspan class=o:/spanspan class=mi259/spanspan class=p)/span span class=mi1/span span class=nXXX/span span class=mh0x00000001066d01b4/span span class=nA/spanspan class=o::/spanspan class=nrun/spanspan class=p(/spanspan class=nImage/spanspan class=oamp;/spanspan class=p,/span span class=ktfloat/spanspan class=o*/spanspan class=p,/span span class=ktint/spanspan class=p,/span span class=ktbool/spanspan class=p)/span span class=o+/span span class=mi99549620/span span class=p(/spanspan class=nfile/spanspan class=p./spanspan class=ncpp/spanspan class=o:/spanspan class=mi259/spanspan class=p)/span span class=p.../span /code/pre/div/div p值得高兴的是,这个线程并没有被干掉,也没有从闪退的方法中退出,它就停在空指针访问的地方。但令人崩溃的是,堆栈中竟然有两个闪退的方法调用。“A::run的代码中并不存在嵌套调用,怎么可能堆栈中会出现两次?”,老Y再次感到不解。从两个frame的地址上看,两者相差12个字节,上面正常闪退的最后一个frame的地址与frame 0相同,都是995449632,那么下面的99549620是什么?“代码上来看,两者是同一行,那便只能从汇编上进行分析了。”/p p老Y打开了专业的汇编分析工具code class=language-plaintext highlighter-rougeHopper Disassembler/code,然后对着code class=language-plaintext highlighter-rougefile.cpp:259/code开始分析,259行代码是:/p div class=language-cpp highlighter-rougediv class=highlightpre class=highlightcodespan class=mi257/span span class=kfor/span span class=p(/spanspan class=ktint/span span class=nj/span span class=o=/span span class=mi0/spanspan class=p;/span span class=nj/span span class=olt;/span span class=nsize/spanspan class=p;/span span class=nj/spanspan class=o++/spanspan class=p)/span span class=p{/span span class=mi258/span span class=nXLog/spanspan class=p(/spanspan class=err@/spanspan class=srun 5.2/spanspan class=p);/span span class=mi259/span span class=ktfloat/span span class=nx/span span class=o=/span span class=na/spanspan class=p[/spanspan class=mi0/spanspan class=p]/span span class=o*/span span class=nb/spanspan class=p./spanspan class=nc/spanspan class=p[/spanspan class=mi2/span span class=o*/span span class=nj/spanspan class=p]/span span class=o+/span span class=na/spanspan class=p[/spanspan class=mi1/spanspan class=p]/span span class=o*/span span class=nb/spanspan class=p./spanspan class=nc/spanspan class=p[/spanspan class=mi2/span span class=o*/span span class=nj/span span class=o+/span span class=mi1/spanspan class=p]/span span class=o+/span span class=na/spanspan class=p[/spanspan class=mi2/spanspan class=p];/span span class=mi260/span span class=nXLog/spanspan class=p(/spanspan class=err@/spanspan class=srun 5.3/spanspan class=p);/span span class=mi261/span span class=ktfloat/span span class=ny/span span class=o=/span span class=na/spanspan class=p[/spanspan class=mi3/spanspan class=p]/span span class=o*/span span class=nb/spanspan class=p./spanspan class=nc/spanspan class=p[/spanspan class=mi2/span span class=o*/span span class=nj/spanspan class=p]/span span class=o+/span span class=na/spanspan class=p[/spanspan class=mi4/spanspan class=p]/span span class=o*/span span class=nb/spanspan class=p./spanspan class=nc/spanspan class=p[/spanspan class=mi2/span span class=o*/span span class=nj/span span class=o+/span span class=mi1/spanspan class=p]/span span class=o+/span span class=na/spanspan class=p[/spanspan class=mi5/spanspan class=p];/span span class=mi262/span span class=nXLog/spanspan class=p(/spanspan class=err@/spanspan class=srun 5.4/spanspan class=p);/span span class=mi263/span span class=nd/spanspan class=p[/spanspan class=mi2/span span class=o*/span span class=nj/spanspan class=p]/span span class=o=/span span class=nx/span span class=o//span span class=nd/spanspan class=p;/span span class=mi264/span span class=nXLog/spanspan class=p(/spanspan class=err@/spanspan class=srun 5.5/spanspan class=p);/span span class=mi265/span span class=p}/span /code/pre/div/div p接着老Y用工具加载闪退版本的code class=language-plaintext highlighter-rouge.ipa/code包,再从闪退报告中找到偏移地址:code class=language-plaintext highlighter-rouge99549632/code,在工具中跳转到该偏移地址处,得到汇编:/p pimg src=/img/posts/6days-assembly.jpg alt=汇编 //p p凭借增加的日志,老Y很方便便找到了C++代码与汇编之间的对应关系。可是,即使从汇编中也无法找到栈的调用关系,理解不了为何出现两个code class=language-plaintext highlighter-rougerun/code。老Y接着在技术群里面询问了该汇编代码及闪退报告的奇怪之处,暂时也没有人从这段代码中找出来问题。/p p此时,L那边也来了消息,他做了很多实验,最后发现即使没有任何测试模块,debug安装包也不会闪退,也就是说闪退根本不是被测试模块吞掉了,而是另有其它原因。连续的两个暂时性的结论让老Y的分析走进了死胡同,不知不觉他已经坐在电脑前接近10个小时。唯一令老Y感到高兴的是,他知道这个线程还活着,并且停留在本该闪退的位置,同时也排除了测试模块的影响。这下他的分析可以聚焦在本该闪退的线程处。/p p暂时也没有其它的思路,老Y在“阻审问题分析”的笔记中记录了分析的过程及当前的进展,然后决定周日休息一天,放空大脑,周一再继续。/p h2 id=第3天-信号第3天 信号/h2 p经过周日充分的休息,老Y周一上班时感觉头脑很清醒,因为之前在笔记中做了详细的记录,他快速的回到了工作的状态,像是程序在函数调用时将调用前的状态压入堆栈,等函数调用完毕后又从堆栈中弹出一般。“突破口应该还是在这段堆栈之中,也许与闪退报告打过无数交道的框架同学可以给些建议”,于是老Y找到框架的同学老Q,描述了这个问题及现在的进展,老Q向老Y要了异常与正常闪退的报告。果然,老Y这一步走的非常正确,老Q很快便看出来了这份报告的异常之处。“这两份报告都是系统生成的,在生成的过程中可能会受到程序运行状态的影响,其寄存器等状态都不对,无法准确反应出闪退时刻的状态”,老Q继续补充道:“你需要找到应用自己生成的闪退报告。”/p p老Y听到这里很兴奋,终于他不用再纠着这段汇编不放,于是说:“我试过了让程序访问无效方法去获取堆栈,虽然应用闪退了,但是并没有在服务端捞到报告,只找到了这两份系统生成的报告。现在我并不需要程序真的崩掉,我只是想拿到程序当前的运行状态,知道那个线程在做什么。”老Y没有停下,继续道,“有没有可以让应用获取当前所有线程堆栈的方法?”/p p果然找对了人,老Q提供了一个API,可以像闪退时程序获取闪退报告一样去获取当前所有线程的运行状态,老Y赶紧将这一API加在了测试代码中。将之前的code class=language-plaintext highlighter-rougetriggerCrash/code方法换成了这个API去获取报告,并将报告打印在日志中。10分钟后,老Y在打好的包中再次尝试,日志中的堆栈出现在老Y眼前的那一刻,他长舒一口气,果然之前的两个code class=language-plaintext highlighter-rougerun/code不正常,堆栈与release包闪退堆栈一致,只有一个code class=language-plaintext highlighter-rougerun/code,“这就可以解释为什么空指针访问之后的日志都没有打印出来,线程完全停在这一行,没能向下执行”,可是除了PC错误以外,老Y暂时想不出还有什么办法能让线程停下。/p p老Y又盯着报告看了很久,突然注意到报告的最上面,这种类型的异常都是段错误code class=language-plaintext highlighter-rougeSIGSEGV/code,非法地址访问。突然老Y想起之前怀疑测试模块时的情形,当时怀疑某个测试模块拦截了闪退,就像在程序遇到闪退时,有时间来生成闪退报告一样。/p div class=language-cpp highlighter-rougediv class=highlightpre class=highlightcodespan class=nException/span span class=nType/spanspan class=o:/span span class=nSIGSEGV/span span class=nException/span span class=nCodes/spanspan class=o:/span span class=nSEGV_MAPERR/span span class=nat/span span class=mh0x0/span span class=nCrashed/span span class=nThread/spanspan class=o:/span span class=mi54/span /code/pre/div/div p“是否可能是哪个模块处理了code class=language-plaintext highlighter-rougeSIGSEGV/code,但是没有处理好导致的?”老Y猜想,“先在demo中试一下,如果这个信号被注册,但是不处理,是否会有一样的现象?”,于是老Y构建了一个这样的demo,给code class=language-plaintext highlighter-rougeSIGSEGV/code设置一个仅打印日志的空handler,观察会发生什么:/p div class=language-cpp highlighter-rougediv class=highlightpre class=highlightcodespan class=c1// ViewController.m/span span class=kstatic/span span class=ktvoid/span span class=nfhandler/spanspan class=p(/spanspan class=ktint/span span class=nsig/spanspan class=p,/span span class=nsiginfo_t/span span class=o*/spanspan class=ndont_care/spanspan class=p,/span span class=ktvoid/span span class=o*/spanspan class=ndont_care_either/spanspan class=p)/span span class=p{/span span class=nNSLog/spanspan class=p(/spanspan class=err@/spanspan class=ssigsegv/spanspan class=p);/span span class=p}/span span class=c1// viewDidLoad/span span class=c1// .../span span class=kstruct/span span class=ncsigaction/span span class=nsa/spanspan class=p;/span span class=nmemset/spanspan class=p(/spanspan class=oamp;/spanspan class=nsa/spanspan class=p,/span span class=mi0/spanspan class=p,/span span class=ksizeof/spanspan class=p(/spanspan class=nsa/spanspan class=p));/span span class=nsigemptyset/spanspan class=p(/spanspan class=oamp;/spanspan class=nsa/spanspan class=p./spanspan class=nsa_mask/spanspan class=p);/span span class=nsa/spanspan class=p./spanspan class=nsa_flags/span span class=o=/span span class=nSA_NODEFER/spanspan class=p;/span span class=nsa/spanspan class=p./spanspan class=nsa_sigaction/span span class=o=/span span class=nhandler/spanspan class=p;/span span class=nsigaction/spanspan class=p(/spanspan class=nSIGSEGV/spanspan class=p,/span span class=oamp;/spanspan class=nsa/spanspan class=p,/span span class=nbNULL/spanspan class=p);/span span class=nNSLog/spanspan class=p(/spanspan class=err@/spanspan class=scurrent SIGSEGV handler address: %p/spanspan class=p,/span span class=nsa/spanspan class=p./spanspan class=nsa_sigaction/spanspan class=p);/span span class=kstruct/span span class=ncsigaction/span span class=noldact/spanspan class=p;/span span class=nsigaction/spanspan class=p(/spanspan class=nSIGSEGV/spanspan class=p,/span span class=nbNULL/spanspan class=p,/span span class=oamp;/spanspan class=noldact/spanspan class=p);/span span class=nNSLog/spanspan class=p(/spanspan class=err@/spanspan class=scurrent SIGSEGV handler address: %p/spanspan class=p,/span span class=noldact/spanspan class=p./spanspan class=nsa_sigaction/spanspan class=p);/span span class=nNSLog/spanspan class=p(/spanspan class=err@/spanspan class=sbefore sigsegv/spanspan class=p);/span span class=ktint/span span class=o*/spanspan class=na/span span class=o=/span span class=nbNULL/spanspan class=p;/span span class=o*/spanspan class=na/span span class=o=/span span class=mi0/spanspan class=p;/span span class=nNSLog/spanspan class=p(/spanspan class=err@/spanspan class=safter sigsegv/spanspan class=p);/span span class=c1// .../span /code/pre/div/div p但当程序运行起来的那一刻,老Y再次懵掉,Xcode直接停留在于了code class=language-plaintext highlighter-rouge*a = 0/code那里,code class=language-plaintext highlighter-rougeEXC_BAD_ACCESS/code错误,虽然日志上可以看出设置了handler,但并没有进入到handler的回调中。“奇怪,明明注册了signal,不可能不回调吧,难道是不能debug?”,老Y抱着试试的想法,直接点开了demo,奇迹发生了,程序竟然没闪退。这可能是分析这个问题几天时间以来,老Y最为震惊,也收获最大的一个意外。他赶紧打开了code class=language-plaintext highlighter-rouge控制台/code,去抓取当前demo的日志,发现不停的打印着code class=language-plaintext highlighter-rougesigsegv/code的日志,而且程序并未崩溃。/p p为了获取handler处理之前的日志,老Y杀了进程,又重新进入demo,闪退之前的日志出现在了code class=language-plaintext highlighter-rouge控制台/code中,随后不停的打印code class=language-plaintext highlighter-rougesigsegv/code,闪退之后的code class=language-plaintext highlighter-rougeafter sigsegv/code一直没有出现。老Y兴奋的想,“这不就和我们遇到的情况一样吗?这个线程显然没有向下执行,一直在打印handler的日志,但主程序还活着”。此时,老Y获取到了handler的地址,“能否通过这个地址找到对应的符号名?”老Y想,code class=language-plaintext highlighter-rougeHopper/code是可以通过地址获取到符号名,但当前的地址是程序加载后符号的地址,包含了二进制本身的偏移,比如任意一个闪退报告:/p div class=language-cpp highlighter-rougediv class=highlightpre class=highlightcode span class=nsymbol/span span class=naddress/span span class=nimage/span span class=naddress/span span class=noffset/span span class=o|/span span class=o|/span span class=o|/span span class=mi0/span span class=nlibsystem_kernel/spanspan class=p./spanspan class=ndylib/span span class=mh0x00000001b2c74dd0/span span class=mh0x1b2c71000/span span class=o+/span span class=mi15824/span /code/pre/div/div p堆栈中有三个地址,分别是:/p ul lisymbol address: 程序运行时符号的地址/li liimage address: 对应image加载的首地址/li lioffset: 符号相对于image的偏移地址/li /ul p而老Y在日志中获取到的是code class=language-plaintext highlighter-rougesymbol address/code,还需要拿到code class=language-plaintext highlighter-rougeimage address/code才能够分析出最终的符号。如何才能够获取到code class=language-plaintext highlighter-rougeimage address/code?老Y已经驾轻就熟,在demo中又构建了一个无响应方法闪退,通过Xcode拿到了设备的闪退日志,这其中就有code class=language-plaintext highlighter-rougeimage address/code,很容易就可以计算出code class=language-plaintext highlighter-rougeoffset/code的值,并在code class=language-plaintext highlighter-rougeHopper/code找到符号的名称就是code class=language-plaintext highlighter-rougehandler/code。但每次code class=language-plaintext highlighter-rougeHopper/code处理的速度太慢,老Y有点不耐烦,换了更快的命令:/p div class=language-sh highlighter-rougediv class=highlightpre class=highlightcodeatos span class=nt-arch/span arm64 span class=nt-o/span demo.app.dSYM/Contents/Resources/DWARF/demo span class=nt-l/span lt;image addressgt; lt;symbol addressgt; /code/pre/div/div p“通过同样的方法可以在App中获取闪退之前注册的所有信号的handler”,老Y似乎看到了希望,因为每实验一次的耗费的时间太长,所以他仔细的设计了这个实验的方案,确保可以一次尝试得到所需要的信息:/p ol li首先在程序启动之后打印一次,获取初始的handler/li li刚进入业务打印,获取当前handler,防止被某个业务修改/li li正常执行业务场景/li li执行完成后打印handler,确认handler是否改变/li /ol p因为是在压后台的时刻调用获取handler方法,所以共总会压后台三次。修改完demo,老Y提交了代码并开始打包,然后在心里记录好操作的步骤,分析是否还有遗漏之处。经过了焦急的等待,服务端终于构建好安装包,老Y赶紧下载并进行测试。他小心翼翼的按照上面设计好的步骤来操作,一切都如他所期望的那样,程序依然没有崩溃。在最后一次压完后之后,他将手机放在了一旁,打开code class=language-plaintext highlighter-rougeMacVim/code开始分析日志。/p p所有的信号都被打印了出来,但有三点出乎他的意料之外,首先每一次压后台的操作获取到的handler都完全一样,这说明在他选择的这几个时机,程序的handler都没有改变。/p p第二个奇怪之处是很多信号的handler地址是0x1。老Y仔细查看了文档,发现原来它是和后面的flags一起使用,代表code class=language-plaintext highlighter-rougeSIG_IGN/code,意味着忽略该信号,通过code class=language-plaintext highlighter-rougesignal(SIGHUP, SIG_IGN);/codeAPI设置。/p p第三个奇怪的地方是只有code class=language-plaintext highlighter-rougeSIGABRT/code注册了handler,code class=language-plaintext highlighter-rougeSIGSEGV/code并没有handler。老Y又查了文档,了解到code class=language-plaintext highlighter-rougeSIGABRT/code是在程序调用code class=language-plaintext highlighter-rougeabort/code时才会抛出的信号。但空指针的访问一定是code class=language-plaintext highlighter-rougeSIGSEGV/code,前面的堆栈报告中也证实了这一结论。“难道code class=language-plaintext highlighter-rougeSIGABRT/code也能够处理非法地址访问的信号吗?”老Y有点不敢相信。/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcodeSIGHUP handler address: 0x1, mask:0, flags:2 SIGINT handler address: 0x1, mask:0, flags:2 SIGQUIT handler address: 0x1, mask:0, flags:2 SIGILL handler address: 0x0, mask:0, flags:0 SIGTRAP handler address: 0x0, mask:0, flags:0 SIGABRT handler address: 0x1013597ac, mask:0, flags:65 SIGSEGV handler address: 0x0, mask:0, flags:0 ... /code/pre/div/div p“先看看code class=language-plaintext highlighter-rougeSIGABRT/code的handler是哪个模块注册的吧”,老Y想,于是他通过前面的code class=language-plaintext highlighter-rougeatos/code命令获得了这个handler的符号名:code class=language-plaintext highlighter-rougeplcrash_signal_handler/code,接着在所有库中查了一次,找到它属于code class=language-plaintext highlighter-rougePLCrashReporter/code模块,这是一个开源的闪退报告库。“这个handler做了些什么?难道它还能处理code class=language-plaintext highlighter-rougeSIGSEGV/code吗?”/p p时间已经是深夜十点半,老Y的眼睛感觉有些干涩,一整天的注意力高度集中让身体有些吃不消。现在他已经不太想看code class=language-plaintext highlighter-rougeplcrash_signal_handler/code的源码,当前的精神状态下完全看不进去,只是随便在Google上搜了这个方法,找到一篇code class=language-plaintext highlighter-rougePLCrashReporter/code的a href=http://www.zoomfeng.com/blog/plcrashreporter-1.html源码分析文章/a ,简单浏览了一遍,但文章太长,他先记了下来,打算明天再看。/p p“从日志来看,code class=language-plaintext highlighter-rougeSIGSEGV/code并没有注册handler,只有code class=language-plaintext highlighter-rougeSIGABRT/code有一个奇怪的handler,难道是就是它导致的闪退被吞掉?”老Y看了看时间,发觉已经很晚,便想先以最快速的方式验证自己的猜想,“先试试如果将这个handler设置成空,是否就一定能够必现闪退了”。他将code class=language-plaintext highlighter-rougesignal(SIGABRT, SIG_DFL)/code加在了进入业务之后,“这样就可以在执行业务的代码之前将handler设置为默认值,如果还是不闪退,则表明不handler的问题。但如果闪退了,则表示是这个handler的影响。”老Y对自己的推理感觉很满意,再次提交代码并进行打包,接着他摘了眼镜,闭上眼睛休息,想着实验的可能性。“已经被这个问题折腾了好几天,这下是不是可以找到突破口了?”,老Y满心期待。/p p十分钟过后,安装包构建完成的通知惊醒了老Y,短短的时间他竟然睡着了,他戴好眼镜,开始尝试这个安装包,果然如他所料,App崩溃了。“太好了,这便足以证明是code class=language-plaintext highlighter-rougeSIGABRT/code handler的问题,接下来就只需要顺着这个handler做了去分析便能够找到问题所在”,老Y很激动,他将这一进展记录在笔记中,准备合上电脑之后,他突然像想起了什么,又打开电脑,补充了一句:“但这里还有一个问题,code class=language-plaintext highlighter-rougeSIGABRT/code为什么还会处理code class=language-plaintext highlighter-rougeSIGSEGV/code的信号?明天继续分析”。/p p写完后,老Y带着电脑离开了公司,此时已经接近12点。老Y开着车行驶在滨海大道上,虽然觉得有些疲惫,但因为三天来的分析终于有了进展,所以内心显得有些兴奋。他不停的想象着明天可能会有什么新的发现,“估计明天就能解决了吧”,老Y想。路上的车依然很多,不停有看似滴滴快车的电动车迅速从旁边驶过,广播中响起FM 104.9的音乐,午夜的音乐广播有种特殊的魔力,像是周围一切都暗了下来,为的便是有人对着耳边低声吟唱一般。不消一会便到了,老Y打开门,一盏温暖的夜灯亮着。/p h2 id=第4天-提速第4天 提速/h2 p前一天的发现让老Y信心满满,一大早来到公司就开始了接下来的实验。“昨天已经确定了是code class=language-plaintext highlighter-rougeSIGABRT/code handler还原成默认值之后就正常,但为什么它会处理本该是code class=language-plaintext highlighter-rougeSIGSEGV/code的信号?”带着这个问题,老Y开始在demo中做起了实验,在前面的demo中不再注册code class=language-plaintext highlighter-rougeSIGSEGV/code,取而代之的是code class=language-plaintext highlighter-rougeSIGABRT/code,其它没有任何变化。“如果程序依然不闪退,则表示空指针访问是可以被code class=language-plaintext highlighter-rougeSIGABRT/code catch住”,虽然不太相信,但老Y期望是这样的结果。/p p可是等到程序运行时,结果却与他所想的完全不同,不管跑多少次都会发生空指针访问的闪退。又看了看前一天打印出的日志,“难道除了信号本身,还有另外一个参数flags是65有关?”,老Y不解的猜到,他又将flags设置成65。可依然得到的是同样的结果。这个结果让老Y喜忧参半,喜的是空指针访问就应该是code class=language-plaintext highlighter-rougeSIGSEGV/code来处理,而不是code class=language-plaintext highlighter-rougeSIGABRT/code。忧的是他还要继续寻找原因,“为什么还原了code class=language-plaintext highlighter-rougeSIGABRT/code就会正常?”/p p老Y还得继续尝试分析原因,然后谨慎的在App中做实验,因为每次尝试都需要十分钟以上的时间,这让老Y感到效率太低,也是过去几天影响分析速度的一个很重要的原因。在刚刚的demo实验失败之后,他突然有了一个想法,足以大大加速这之后的分析过程。/p p因为在demo中测试信号时,他发现只有直接运行时,handler才会被处理,一旦连接Xcode联调,程序会直接挂在空指针的访问处,handler也无法被调用,老Y突然醒悟到:“之前本地调试时一直必现闪退,所以才在服务端打包。但会不会并不是本地与服务端打包的差别导致问题只在服务端的包中出现,而是由于使用Xcode联调?如果用本地打包,不联调,直接运行,会不会也能够重现出问题?”想到这点,老Y立马开始了尝试,首先它直接用Xcode调试,果然,程序挂在了空指针访问处。接着他停止了联调,直接打开程序,进入业务的页面。“会不会不闪退?”老Y竟然有些紧张,拿着手机的掌心微微出了些汗。/p p他尝试了一遍,奇迹般的,闪退并未出现。为以防是程序意外没有走到空指针的访问处,他又一次进行了尝试,依然没有闪退。随后老Y又去分析了一遍日志,确认了空指针访问处的代码的确被执行了,也通过压后台,得到了当时的堆栈,线程停留在空指针的那一行。“这样便确定了即使在本地也可以复现出问题,与服务端打包无关”,老Y为这次意外尝试得出的结论显得极其兴奋,因为所有的猜想与尝试都可以快速得到验证,而不需要经过服务端打包漫长的流程。/p p“从demo中看,只要是空指针访问都会产生code class=language-plaintext highlighter-rougeSIGSEGV/code信号,那是否同样可以直接构建这一闪退,而无需通过业务的复杂入口?”,为了进一步简化复现流程,老Y打算构建新的场景。在应用启动之后,他打算根据固定的事件来触发不同的操作,目前需要的操作包含三个:/p ul li触发空指针访问/li li打印堆栈/li li恢复code class=language-plaintext highlighter-rougeSIGABRT/code信号/li /ul p他同时重写了应用的首页,添加三个按钮分别触发这三个操作,如此一来整体测试的流程大大简化,验证一个猜想也可以控制在一分钟以内。在构建了这样一个简化版的操作之后,老Y在笔记中写下了他的三个验证用例:/p ol li程序启动后,先触发空指针访问,此时应该不会闪退;接着打印堆栈,可以确定线程卡在空指针访问处;/li li从日志中找到code class=language-plaintext highlighter-rougeSIGABRT/code信号的handler,分析是否依然是code class=language-plaintext highlighter-rougeplcrash_reporter_handler/code/li li重启一次应用,先恢复code class=language-plaintext highlighter-rougeSIGABRT/code信号,再次触发空指针操作,此时应该闪退/li /ol pXcode刷完应用后,老Y便开始了测试,用例1和3都如他所料,但第2个用例无法验证,因为code class=language-plaintext highlighter-rougeatos/code的命令需要dsym文件,但刚刚编译时dsym的配置未打开。老Y重新设置好编译选项后,再次安装好应用,继续上面的用例,正当老Y满怀期望之时,意外发生了。第一个用例失败,空指针访问竟然立刻发生了闪退,他有些不敢相信,再次打开应用执行了一遍,依然闪退,而且堆栈也指向空指针的访问处。“为什么会这样?dsym应该不会影响结果,难道是覆盖安装影响?”,老Y将信将疑,删除了旧包,重新进行安装、测试,没想到三个用例成功的通过了。/p p“如果猜想是正确的话,修改代码,再次覆盖安装,用例1应该会失败”,老Y为了证明这个猜想,增加了一行日志,覆盖安装进行了验证,结果如他所料,覆盖安装后会导致闪退行为正常,handler并不会捕获闪退。/p p这个意外的发现让情况似乎又变的更加复杂了些,老Y停下来整理了目前得到的信息,“现象是release包空指针访问闪退,但debug包空指针访问不闪退,可以确定的是code class=language-plaintext highlighter-rougeSIGABRT/code信号被注册,它捕获了本应是code class=language-plaintext highlighter-rougeSIGSEGV/code处理的空指针访问,这里还有些奇怪,刚刚在demo中也验证了code class=language-plaintext highlighter-rougeSIGABRT/code无法捕捉空指针访问,为什么App中就可以?如果将code class=language-plaintext highlighter-rougeSIGABRT/code的handler还原成默认,则闪退立刻会出现。信号的handler是code class=language-plaintext highlighter-rougeplcrash_reporter_handler/code,是由一个闪退报告的第三方开源库注册。另外一个意外获取的信息是首次安装运行时闪退才不会出现,但覆盖安装时会正常闪退。”/p p待老Y整理完所有的信息之后,他微微感到有些头痛,不停出现的奇怪现象让问题看起来越变越复杂,一个意外的出现可能随时就会推翻他之前得到的结论,但好在一个意外的发现也让得以本地验证,从而加速了问题分析的进程。这一天其它方面的事情也不断找上来,老Y不停的被打断,让他无法全身心的分析问题,来回之间的状态切换就像程序有资源消耗一样,也消耗着老Y的精力,在疲惫中老Y结束了一天的工作。/p h2 id=第5天-浮现第5天 浮现/h2 p老Y本想花时间去仔细分析code class=language-plaintext highlighter-rougeplcrash_reporter_handler/code的逻辑,可其源码的逻辑看似很复杂,于是他想先以更快的方式定位到原因。“既然是这个handler的影响,那么先看看这个handler具体是谁在什么时间注册的,如果关掉它是不是闪退就一定会出现?”基于这个想法,老Y先分析了App中code class=language-plaintext highlighter-rougemain/code方法所在的Portal模块,终于在一个code class=language-plaintext highlighter-rougeload/code方法中,找到了名为code class=language-plaintext highlighter-rougeenable_crash_reporter_service/code的方法,看起来它便是启动整个闪退报告服务的地方。/p p老Y尝试将这个方法注释掉,看是否闪退还会被捕获。如他所料,当闪退报告服务关闭之后,空指针立刻触发闪退。为以防万一,他再次取消该注释,重新运行,闪退没有发生,证明该方法调用确实是引起问题的原因。在验证了debug环境之后,老Y又切到release,不管闪退报告服务是否打开,空指针一定会造成闪退。“由以上实验可以得出结论”,老Y在笔记中记录到,“debug环境下的闪退服务导致了空指针访问闪退被吞掉,但release不受影响”。“debug与release有什么区别,即使是debug包,Portal所依赖的所有模块已经是release版本,只有Portal本身不同,同时还有link的参数有区别”,老Y又开始了他的分析。/p p在老Y尝试打开闪退报告服务时,他意外的发现了另一个现象,如果是debug包首次安装,第一次运行时空指针访问时闪退不会出现,但是杀了进程第二次运行时,空指针访问竟然在接近10秒之后出现了闪退。这个意外的收获让老Y对之前的覆盖安装问题有了全新的解释,“难道这就解释了为什么覆盖安装时空指针访问会闪退的原因了?”,老Y接着推测,“其实并不是与覆盖安装有关,而是首次运行与非首次运行的缘故”,“一定是应用在首次运行时写入了某个配置,第二次运行可以读到该配置所以没有走异常的逻辑,导致现象不同。之前认为debug包下闪退会被吞掉其实还有一个限制条件,即首次安装运行时才会发生”,老Y为了验证这一推论,他又找了测试同学帮忙尝试线上业务是否也是同样的现象,即第一次不会闪退,但杀进程后再次运行会正常闪退。不一会,测试的结论也同步了过来,果然如老Y所料,debug包不会吞掉所有的闪退,仅仅是首次运行应用时才会发生。于是老Y顺着这个思路,结合前面得到的debug与release的差别,开始分析Portal的代码。/p p因为Portal的代码并不多,很快老Y便发现了一个可疑之处,有一段被code class=language-plaintext highlighter-rougeDEBUG/code宏所包围的代码逻辑,用于清理缓存目录。本着快速排查问题的原则,老Y并不急于去分析这段代码,而是直接注释掉然后运行。奇迹发生了,debug包不再吞掉空指针的访问闪退。老Y感到十分兴奋,这下已经可以确定是由于闪退报告服务与缓存清理逻辑之间存在问题,可能是清理缓存导致了闪退报告服务的异常。/p p确定了清理缓存的逻辑存在问题之后,老Y开始仔细分析这段代码,想查出具体是哪个目录被清理掉导致的问题。经过几次试验,老Y定位到了code class=language-plaintext highlighter-rougeLibrary/Caches/code目录。他又对比了清理缓存前后目录中文件的具体差别,发现在清理之前有一个闪退报告的缓存目录。“这下可以确定问题所在,由于debug环境下闪退报告的缓存目录被清理,导致闪退报告服务出现问题,所以闪退被吞掉。但第二次运行时,由于code class=language-plaintext highlighter-rougeDEBUG/code宏之间的清理逻辑并不会被执行,因此后续的闪退并不会被吞掉。”/p p老Y接着分析,“闪退报告的目录应该是code class=language-plaintext highlighter-rougeload/code方法中被闪退报告服务所创建,它早于code class=language-plaintext highlighter-rougemain/code函数的执行,可在code class=language-plaintext highlighter-rougemain/code函数中缓存目录又意外被清理,如果将清理逻辑提前到闪退报告服务启动之前是不是就正常了?”,老Y进行了尝试,一切如他所料,这种情况下所有的闪退均能够正常出现。老Y还发现,除了首次安装以外,在切换线上与线下环境时,也会触发清理缓存的逻辑,如此一来,很有可能许多业务在回归验证时,会让本该出现的闪退被吞掉,将一些问题带到线上?认识到问题的严重性,他赶紧联系了Portal模块的负责同学,并告知现在分析出来的结论,由他接手剩余的工作。/p p至此,老Y已经定位到是由于闪退报告服务的缓存目录被清理导致了闪退被吞掉,看起来已经可以结出结论,但现在依然有两个问题没有得到合理的解释,这让老Y并不愿到此为止。/p ol li首先是code class=language-plaintext highlighter-rougeSIGABRT/code为什么会捕获空指针异常/li li为什么闪退报告的缓存目录被清理了之后闪退就会被吞掉?/li /ol p此时已经是深夜11点,老Y感到已经离“真相”不远,“估计明天这个问题就会解决了吧”,老Y在笔记上记录了当前的进展及第二天需要从哪里开始查起的思路,便合上电脑离开了公司。/p h2 id=第6天-真相第6天 真相/h2 p经过前面几天的分析,还剩下两个问题待解决,一是code class=language-plaintext highlighter-rougeSIGABRT/code为什么会捕获空指针访问异常。二是缓存目录被清理之后为什么会导致闪退报告服务异常并吞掉闪退。老Y一大早赶在其它同事过来之前就来到了公司,这样他就可以在正式工作开始之前快速解决剩余的问题,他已经花了太长的时间在这个问题上。/p p这两个问题都需要从code class=language-plaintext highlighter-rougePLCrashReporter/code的a href=https://github.com/microsoft/plcrashreporter/源码/a进行分析,现在该代码库由微软维护,但老Y没有立刻去分析源代码,而是从网上的文章入手,因为这能够帮助他以最快的速度找到切入点。这两篇文章详细的分析了code class=language-plaintext highlighter-rougePLCrashReporter/code的源码:a href=http://www.zoomfeng.com/blog/plcrashreporter-1.htmlPLCrashreporter源码分析其一/a及a href=http://www.zoomfeng.com/blog/plcrashreporter-2.htmlPLCrashreporter源码分析其二/a,它们很好的解释了code class=language-plaintext highlighter-rougeplcrash_signal_handler/code的工作原理,如此一来,前一天遗留的第一个问题得到了解决。/p p原来有两种类型的异常,平常在使用Xcode联调时遇到空指针访问会收到code class=language-plaintext highlighter-rougeEXC_BAD_ACCESS/code的错误,而这便是Mach层抛出的异常,这些异常会被host层转成对应的BSD信号,如空指针访问异常被转换成BSD标准的code class=language-plaintext highlighter-rougeSIGSEGV/code信号抛到出错的线程。前面所说的code class=language-plaintext highlighter-rougeSIGSEGV/code及code class=language-plaintext highlighter-rougeSIGABRT/code都是BSD类型的信号。而在code class=language-plaintext highlighter-rougePLCrashReporter/code中,处理的便是Mach类型的异常。但同时,它还注册了BSD的code class=language-plaintext highlighter-rougeSIGABRT/code信号,其handler为code class=language-plaintext highlighter-rougeplcrash_signal_handler/code,用于捕获最终的闪退异常,具体原因可以参考下面的源码注释:/p pimg src=/img/posts/6days-mach.png alt=SIGABRT //p p“如此一来,便可以解释为什么App没有注册code class=language-plaintext highlighter-rougeSIGSEGV/code却能够收到空指针访问类型的错误了,原来闪退报告模块已经从Mach层捕获了异常”,老Y喃喃道,“这个问题解决之后,接下来便只需分析为什么缓存目录被删除之后空指针访问异常被吞掉”。老Y直接clone了code class=language-plaintext highlighter-rougePLCrashReporter/code的源码,想看看最新的版本中这个问题是否已经处理。于是他模拟之前遇到的情况,在demo中启动闪退报告服务之后,立刻删除了缓存目录,运行起来之后果然空指针的访问无法引起闪退。“看起来这不是一个常见的case,否则这么久以来,怎么可能问题还没有修复”。/p p“因为是缓存目录被清理导致的问题,所以优先需要分析使用了该目录的代码”,老Y便开始搜索整个repo中使用该目录的代码,很快,他便找出一处a href=https://github.com/microsoft/plcrashreporter/blob/e9e257e65624cac025e6012315d2265ef2c8e1d6/Source/PLCrashReporter.m#L298可疑代码/a,如下图所示:/p pimg src=/img/posts/6days-bug.png alt=PLCrashReporter的bug //p p260行处的code class=language-plaintext highlighter-rougemach_exception_callback/code是PLCrashReporter处理Mach异常的回调函数,296行调用了一个函数code class=language-plaintext highlighter-rougemach_exception_callback_live_cb/code,这个函数会写闪退报告,如257行所示。但由于目标路径不存在,所以一定会写失败,从而296行的if条件命中,于是执行297与298处代码,问题就出了这里。/p p向上可以看到260行的返回值类型是code class=language-plaintext highlighter-rougekern_return_t/code,它是一个code class=language-plaintext highlighter-rougeint/code类型的code class=language-plaintext highlighter-rougetypedef/code,但是code class=language-plaintext highlighter-rougeKERN_SUCCESS/code与code class=language-plaintext highlighter-rougeKERN_FAILURE/code等定义如下:/p pimg src=/img/posts/6days-kern-success.jpg alt=KERN_SUCCESS //p pcode class=language-plaintext highlighter-rougeKERN_SUCCESS/code的值是0,而298处返回了code class=language-plaintext highlighter-rougefalse/code,它的值刚好是0,所以本应该是认为失败的场景,却因为返回false,而被当成了code class=language-plaintext highlighter-rougeKERN_SUCCESS/code,误认为是成功,程序逻辑出现异常,这应该是开发者笔误写错了返回值类型导致的问题。“为什么返回了错误的类型就会出现空指针访问被吞掉呢?”老Y继续刨根问底。他找到这个callback的声明处,发现注释中赫然写着:“如果返回了code class=language-plaintext highlighter-rougeKERN_SUCCESS/code,则线程会恢复执行”。/p pimg src=/img/posts/6days-plcrash.jpg alt=mach_exception_callback //p p“原来是这样,因为错误的返回值,导致了空指针的访问线程被恢复执行,立刻再次触发Mach异常,如此陷入了死循环”,老Y想,为了验证这一想法,他在298处增加了一段日志,运行之后如他所料,不断的在打印这处新增加的日志,由此也证明了他刚刚的猜想。/p p为了防止后续有人再遇到这个问题,老Y向code class=language-plaintext highlighter-rougePLCrashReporter/code的github仓库发起了一个a href=https://github.com/microsoft/plcrashreporter/pull/159MR/a。至此,由一个阻塞提审问题引发的耗时长达六天的问题追踪终于告一段落,老Y长长松了一口气,开如准备复盘文档,接下来估计还有一阵枪林弹雨。/p h2 id=后记后记/h2 p老Y与这个问题斗争六天的故事也至此告一段落,后续的复盘会议中依然发现还存在其它的问题,老Y又花了近一天的时间分析并找到解释,但这篇故事已经够长且显得啰嗦,后面的事情有机会再聊。/p p从最终的修复方案上来看,技术上的问题似乎非常简单:/p ol li首先是debug环境下清理缓存的逻辑需要优化,不能够在初始化闪退报告之后又清理其创建的缓存目录/li li然后是code class=language-plaintext highlighter-rougePLCrashReporter/code在写入闪退报告失败后的a href=https://github.com/microsoft/plcrashreporter/pull/159bug/a/li /ol p但就是这样一个简单的问题,为什么耗时如此之久才得出结论,导致复盘的时间一推再推,难点究竟在哪里,老Y在复盘时也进行了总结,主要包含以下几点:/p ul liApp复杂,依赖数百个模块,很难快速定位问题出自何处/li li开始几天只能在后台打包测试,Xcode无法调试,验证猜想的周期太长/li li后台debug包与release包相比依赖了一些测试模块,对推理过程造成干扰/li li测试过程中的出现一些奇怪现象:覆盖安装不出现、第一次不闪退第二次运行正常、不注册code class=language-plaintext highlighter-rougeSIGSEGV/code也异常等也给推理造成了困难/li li。。。/li /ul p正是由于以上一些因素组合起来,才导致了问题复杂度提升,让整个追查过程走了很多弯路。中间出现的异常现象在初次发现时,也曾被老Y当成是异常操作忽略过,并没有当成一回事,但最后发现所有测试中发现的所谓“奇怪”或偶然现象都不是偶然,而是某种条件下由bug引起的必然,都不应该被忽略,它们之中也许就藏着解决问题的关键性钥匙。在程序的世界里,任何问题只要能找出复现路径,问题就能得以复现。整个过程让老Y想起曾着迷过的侦探小说,解bug的过程就是破案的过程,任何蛛丝马迹都可能成为破案的关键。侦探根据获取的线索进行大胆猜想,并小心求证,一步步还原真相。/p p除了技术上的bug以外,该问题也同时暴露出沟通及开发流程中的一些问题,所以老Y也觉得在遇到这类问题时流程中有复盘这一环是个特别好的机制,开发、测试以不同的视角,对遇到的每个问题抛根问底,不会放过任何一个可疑的点,不存在无法解释的问题,任何问题都需要给出合理的解释,这样的复盘才不至于是为了流程而走个过场,才能保证如此大规模的App每次更新都维持最高的质量。出现问题并不可怕,可怕的是下次类似的问题一而再再而三的出现,这便是复盘的意义所在吧。/p p像这样的问题,作为一个程序员可能经常会遇到,尤其在如此大规模的系统中。程序员与bug缠斗的故事每天都在上演,而这六天,是老Y的故事。/p p(全文完)/p pfeihu/p p2021.01.07 于 Shenzhen/p

让小白PPT更加专业的设计书

作者 feihu
2019年7月3日 00:00
img src=https://feihu.me/img/post-banners/design-book.jpg alt= width=600 /br / p前段时间在制作一个PPT时,效果总是不尽如人意,各种图表与文字完全不知放在哪里才合适。试了很多所谓好的模板,但很难将想要呈现的内容与这些模板完美的融合在一起,只能够生搬硬套模板中的样式,一旦版面的内容有所不同时,套用出来的效果就变得很差。网络上的PPT模板千千万,哪款才是适合的那一个?于是想着有没有一些通用的制作PPT的原则,可以让我们设计自己的模板,对于包含很多图表、数据、文字的页面,如何设计才能够让整个页面看起来没有那么业余,虽不一定要精美,但总不致于到难看的程度。看了很多介绍如何设计PPT的文章,但大多给出了结果,但不知道为什么要这么做,遇到自己的场景时依然没有头绪。还有很多文章都是介绍如何制作酷炫的动画,在使用PowerPoint的层面,这些都是术,有没有一种从更高层次来介绍如何设计的书?经过一番搜索,最终找到这本a href=https://book.douban.com/subject/26657933/《写给大家看的设计书》/a。/p p所以这是一篇《写给大家看的设计书》的读后感,可能对于与我一样在制作PPT过程中充满困惑的朋友们有所帮助。/p p今天在飞机上看完了《写给大家看的设计书》,毫无疑问,这是看过的关于设计和排版类的文章和书籍中最好的一本。上周读完a href=https://book.douban.com/subject/3313363/《演说之禅》/a,它解决的更多是演讲方面的问题,关于PPT的设计写的很简单,大多是巨幅图片当成背景,再配上一点简单的文字用于构造对比。可是我们平常的工作中需要制作的PPT远非这些简单的建议可以满足,PPT中需要有大量的图表、数据和文字。《演说》一书在这点上过于抽象,读完之后,对于制作PPT本身并没有太多的帮助。但《写给》这本书完全不一样,它针对的非从事设计专业的小白,书中提供了极其明确的操作指南,并伴随大量的练习。其中有大量的依据原则修改前后对比的案例,读完后有种恍然大悟的通透感,让人忍不住立刻想去实践一番。看着过去做的PPT,或者看到的一些广告、宣传手册等设计,都会想着运用刚刚学到的设计原则,去分析这些设计哪里好,哪里不好,为什么不好,如何改进才能够更好。/p p书中有一个词叫“设计师之眼”,像是一种很抽象的感官,或者说一种审美标准,它是将书中介绍的设计原则内化到大脑之后的能力。记得在《重构——改善既有代码的设计》一书中,作者介绍了很多的重构原则,要一一记住这些原则非常困难。但是,书中还提到了在什么样的情形下需要去做代码重构,用的也是一种类似的抽象感官,叫做“代码的坏味道”,它和这里的“设计师之眼”有着异曲同工之妙。也许去记住所有的原则,记住在什么情况下使用这些原则,如何使用它们并不容易,但从本质上来说,更好的方式可能是训练识别能力,识别好与坏的本能,比如识别代码中好与不好的地方,识别设计中冲突与和谐的地方,一旦掌握了这种本质能力,相当于将原则融入了骨髓,可以做到举一反三的地步。“代码的怪味道”训练的是“嗅觉”,“设计师之眼”训练的是“视觉”。/p p这本书非常难得的是它将看起来高深、抽象,难以描述的“感觉”,提炼成了放之四海皆准的四个原则,并且中英文场景通用,无比佩服作者提炼的功力。这四个原则简单、明确、正交,每个案例中,简单的依据它们做的小调整都可以看到立竿见影的效果。之前看待一些PPT的眼光,只停留在觉得这个做的好,那个看起来很丑,但具体好在哪里,丑在哪里却无法描述出来。因此对它们的改进也只能凭着感觉,套用模板,如同盲人摸象一般胡乱尝试,虽然在“美容”方面花了很长时间,却收效甚微。但看完此书之后,就像打开了新世界的大门,之前那些好与丑一下子变得清晰起来,我们也可以像专业设计师一样,从专业的角度去评判好与坏。但师傅领进门,接下来的修行看个人,如何培养“设计师之眼”变成了最重要的部分。/p p回顾下书中提到的四个原则:亲密性、对齐、重复、对比,这些原则也许你稍经留意,都可以在网络上各种文章与关于PPT制作的书籍中可以看到,它们都源自于此书。除了基础的四个原则外,书中还介绍了色彩与字体两部分,颜色可以从色带中依据一定的规则选取,冷暖色调有各自适合运用的地方。之前在PPT的配色都非常的随意,要么是五颜六色,要么是灰蒙蒙一片,而这本书将看起来完全由审美决定的配色,变成了任何人都可以参考执行的原则。读完后会感叹,原来那些和谐的配色是这样来的,也许我们也可以做得到。作为设计的灵魂——字体,其使用上有着几个原则,字体根据其结构可以分成六类,一些基本的原则,如不要选择同一个类别的字体,字体的对比需要强烈等,对比可以从字体大小、粗细、形状、结构、方向和颜色几个方面来突出效果,读完这章之后,再使用字体时肯定不会那么随意。/p p作者一直在强调不要居中对齐,除非你知道为什么要是用它,大部分情况下,左对齐和右对齐得效果更好,因为有一条很明确的对齐线。另外一点是不要使用Arial和Helvetica字体,因为它们太普通了。在过去,我一直大量的使用居中对齐及这两种字体,作为默认的方式,它们简洁、大方、正式、易读,可能作者从设计的角度,为避免设计作品落入俗套才有这样的论断吧。但在我们PPT的制作与真正的设计可能还不太一样,用一些奇怪的字体可能会显得很不合适,具体还是看场景而定吧。/p p这些原则并仅仅可以运用于工作中,如作者在结尾所说,生活中也可以使用它们,比如穿着搭配,墙壁上的画,都可以运用对比、重复性原则。平常看见家里小孩的玩具乱糟糟的,一个很重要的原因就是没有使用亲密性原则,这些玩具需要放在一起,比如一个盒子里,或者架子上,再可以运用对齐原则,让他们排列整齐,这是一个很有趣的看待问题的角度,但细细想来,的确可以尝试。/p p说了这么多的原则,在使用原则前,有一件非常重要的事情需要确定:这个设计中,最想要突出的主体是什么,有了这个前提之后,再来运用原则才能够避免本末倒置。/p h2 id=结尾结尾/h2 p最后,理论、原则、案例都有了,接着来的便是实践。想要做到“设计师之眼”远非一日之功,需要投入大量的精力与练习。但念念不忘,必有回响,相信平时多加留意,对这些原则灵活运用成为习惯之后,也许某一天我们这些设计小白们也可以培养出“设计师之眼”,做出自己满意的“专业”设计。/p p(全文完)/p pfeihu/p p2019.07.03 于 Hangzhou/p

WWDC之AR

作者 feihu
2018年7月2日 00:00
img src=https://feihu.me/img/post-banners/wwdc.png alt= width=600 /br / p进入iOS开发这些年,今年终于有机会去参加一次WWDC,朋友们笑称,这是趟朝圣之旅,我想也并不为过。/p h3 class=no_toc id=目录目录/h3 ul id=markdown-toc lia href=#背景 id=markdown-toc-背景背景/a/li lia href=#arwwdc id=markdown-toc-arwwdcAR@WWDC/a ul lia href=#usdz格式 id=markdown-toc-usdz格式USDZ格式/a/li lia href=#ar-quick-look id=markdown-toc-ar-quick-lookAR Quick Look/a/li lia href=#与adobe合作 id=markdown-toc-与adobe合作与Adobe合作/a/li lia href=#尺子app id=markdown-toc-尺子app尺子app/a/li lia href=#共享地图 id=markdown-toc-共享地图共享地图/a/li lia href=#乐高世界 id=markdown-toc-乐高世界乐高世界/a/li /ul /li lia href=#arkit-20新特性 id=markdown-toc-arkit-20新特性ARKit 2.0新特性/a ul lia href=#地图持久化 id=markdown-toc-地图持久化地图持久化/a/li lia href=#world-tracking增强 id=markdown-toc-world-tracking增强World Tracking增强/a/li lia href=#环境纹理 id=markdown-toc-环境纹理环境纹理/a/li lia href=#图像跟踪 id=markdown-toc-图像跟踪图像跟踪/a/li lia href=#物体识别 id=markdown-toc-物体识别物体识别/a/li lia href=#人脸跟踪 id=markdown-toc-人脸跟踪人脸跟踪/a/li /ul /li lia href=#其它 id=markdown-toc-其它其它/a ul lia href=#siri-shortcuts id=markdown-toc-siri-shortcutsSiri Shortcuts/a/li lia href=#metal id=markdown-toc-metalMetal/a/li lia href=#ml id=markdown-toc-mlML/a/li lia href=#xcode-10 id=markdown-toc-xcode-10Xcode 10/a/li lia href=#macos id=markdown-toc-macosmacOS/a/li lia href=#facetime id=markdown-toc-facetimeFaceTime/a/li lia href=#非技术 id=markdown-toc-非技术非技术/a/li lia href=#视频地址 id=markdown-toc-视频地址视频地址/a/li /ul /li lia href=#写在最后 id=markdown-toc-写在最后写在最后/a/li /ul hr / h2 id=背景背景/h2 p与往年的抢票不同,今年的门票采用随机抽取的方式,早在三月份就开始了WWDC08的门票登记,而我有幸抽中门票,随后开始了漫长的签证。运气很不好,不知是否是由于中美关系紧张的缘故,IT相关的从业者很容易被行政审查。我在广州大使馆也不幸被抽中,当时准备了WWDC的邀请函,丝毫没有用处。面试官问有没有准备个人简历与旅行计划,这些在网上看攻略时都说不用准备,没想到却刚好被问到,行政审查,让回去准备这些材料并发到大使馆的邮箱。于是开始了漫长的等待期,平均时间是25天左右就能够issue签证,我的却一直等了42天,中间写了几次邮件去催促,无果。准备放弃之际,却在5月底拿到签证,接着匆忙的准备酒店、机票等事宜,赶上了这次WWDC。/p h2 id=arwwdcAR@WWDC/h2 p这次参会主要关注AR方面,AR在今年的大会上占尽了风头。Keynote主题演讲时,iOS 12的性能数据介绍之后,立刻开始了AR相关的介绍,作为iOS最重要的特性登场。下面带着大家一起看看这次的AR都包含了哪些内容。/p h3 id=usdz格式USDZ格式/h3 p第一个主题,苹果发布了新的格式code class=language-plaintext highlighter-rougeUSDZ/code,它是基于Pixar开源的code class=language-plaintext highlighter-rougeUSD/code格式,不同的是前者把所有文件都打包成了一个文件,为分享做了一些优化,并且在Xcode 10中集成了打包转码工具。这个发布很有意思,苹果似乎总是与业界对着来,关于3D文件格式,code class=language-plaintext highlighter-rougeKhronos/code组织已经与2015年就发布了code class=language-plaintext highlighter-rougeglTF/code格式,并且已经在各大厂商、游戏引擎中得到了推广,code class=language-plaintext highlighter-rougeKhronos/code声称,code class=language-plaintext highlighter-rougeglTF/code就是3D格式中的code class=language-plaintext highlighter-rougejpeg/code,俨然一副建立业界新标准的姿态。即使在code class=language-plaintext highlighter-rougeglTF/code已经占有如此大优势的情况下,苹果仍然执意发布自己的3D格式,就好像code class=language-plaintext highlighter-rougeMetal/code与code class=language-plaintext highlighter-rougeVulkan/code,苹果自己构建一道与业界的屏障,也许是因为它可以完全控制自己的硬件与软件,不需要因为兼容性去妥协设计吧。至于最终code class=language-plaintext highlighter-rougeUSDZ/code能否像code class=language-plaintext highlighter-rougeglTF/code一样成功,看后面苹果的推广了。/p p这里对code class=language-plaintext highlighter-rougeglTF/code与code class=language-plaintext highlighter-rougeUSD/USDZ/code作了一个简单的对比:/p table thead tr th /th thglTF/th thUSD/USDZ/th /tr /thead tbody tr td用途/td td兼容GPU的数据格式,快速加载/td td用于分享,扩展/td /tr tr td支持/td tdKhronos, Facebook, Google, Microsoft, Adobe/td tdApple, Pixar/td /tr tr td引擎/td tdUE4, Unity 3D, godot, three.js, Blender, Adobe/td tdApple/td /tr tr td工具/td td集成的转换器,优化,校验,编辑工具等等/td tdXcode命令行/td /tr tr td版本/td td2.0/td td1.0/td /tr tr td发布时间/td td2015.10.19 1.0/2017.6.5 2.0/td td2012 USD/2018 USDZ/td /tr tr td开源/td td开源/td td2016年开源/td /tr /tbody /table p目前看来,code class=language-plaintext highlighter-rougeUSD/USDZ/code全方位的落后,不过因为它是刚刚发布,相信凭着苹果的运营,会得到越来越多的支持与推广。/p p苹果推出这个格式,有一个非常重要的原因,为了code class=language-plaintext highlighter-rougeAR Quick Look/code。/p h3 id=ar-quick-lookAR Quick Look/h3 p本次大会一个非常重要的发布,code class=language-plaintext highlighter-rougeAR Quick Look/code,这是一个什么东西?可以理解为默认的图片浏览器之于图片,这个就是3D文件的浏览器,它将在下一个版本的iOS、macOS中原生支持,已经支持了众多的原生应用,如下图所列:/p pimg src=/img/posts/wwdc-arquicklook-support.png alt=arquicklook-support //p p另外苹果做了件事让这个更加容易推广,可以像开发者直接使用code class=language-plaintext highlighter-rougeMPMoviePlayerController/code来播放视频一样,它将code class=language-plaintext highlighter-rougeAR Quick Look/code也开放给开发者,使开发者可以通过简单的代码就集成这个工具,让应用支持AR能力。/p p既然叫strongAR/strong Quick Look,自然不仅仅能展示3D模型,还可以与实景结合起来,直接使用code class=language-plaintext highlighter-rougeARKit/code的跟踪功能,体验非常好。看下面的两个例子:/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5NDIzNDQ1Mg== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div p看到这里,似乎明白苹果为何要发布新的3D格式了,这两者的结合不仅让3D模型可以更方便的在iOS与macOS里面原生支持,并且开发者可以通过两种方式简单的使用强大的能力:在app中集成code class=language-plaintext highlighter-rougeAR Quick Look/code或者在H5的网页中使用新提供的code class=language-plaintext highlighter-rougelt;argt;/code标签。尤其是后者,此处可以展望一下,继code class=language-plaintext highlighter-rougevideo/code标签之后,code class=language-plaintext highlighter-rougear/code很可能会被纳入到H5的标准之中,只不过可能格式不是使用code class=language-plaintext highlighter-rougeUSDZ/code,而是支持更加广泛的code class=language-plaintext highlighter-rougeglTF/code吧,我们拭目以待。/p p接入code class=language-plaintext highlighter-rougeAR Quick Look/code之后,会随着它本身的升级而自动升级,极大的降低了3D模型与AR场景的门槛,苹果下得一手好棋。/p h3 id=与adobe合作与Adobe合作/h3 p一个巴掌拍不响,3D格式的支持离不开其它大厂商的支持,苹果已经与Autodesk、Adobe等厂商合作,尤其是Adobe,他们的CTO被请来大会现场,专门展示了为苹果新的code class=language-plaintext highlighter-rougeUSDZ/code与code class=language-plaintext highlighter-rougeAR Quick Look/code开发的工具链,可以在Adobe强大的工具链中完成从设计到绘图,到3D制作,到生成3D模型,最终发布在苹果的生态系统中,十分强大,一起来感受一下。/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5NDIzMTIyOA== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div h3 id=尺子app尺子app/h3 p苹果将一个尺子app列入iOS 12系统的默认软件中,它可以利用code class=language-plaintext highlighter-rougeARKit/code的能力来测量真实世界中的物体长度。由于在iOS 11 beta版本发布时,就已经看到过了类似的应用,所以再次看到时并没有很激动。只是在体验上苹果做得更好,像计算器一样集成在手机中,为生活提供了极大的便利。/p p从与苹果工程师的沟通中了解到,这个精度比较高,只有5%以内的误差,1m只是不到5cm的误差,基本可以满足生活中的一般测量需要了。/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5NDIxNjQ0OA== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div h3 id=共享地图共享地图/h3 p终于,苹果将这个功能开放了出来,3D的虚拟地图可以共享,如此一来,多个用户看到的可以是同一个虚拟世界,多人互动的AR游戏便成了可能。相信这个发布之后,AR游戏将会迎来一个大暴发。/p p大会展示了一个双人AR游戏,还有一个观看者,三者同时处于一个虚拟现实结合的世界中,好像将游戏中的功能搬了出来,直接由AR单机游戏变成了AR联机游戏,游戏性将极大丰富。/p p这个AR游戏也成为了一个开源的sample可以下载,并且还有其开发者手把手解析这个游戏的代码,感兴趣的同学可以去观摩下这个视频: a href=https://developer.apple.com/videos/play/wwdc2018/605/Inside SwiftShot: Creating an AR Game/a。/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5NDIzMzc3Mg== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div h3 id=乐高世界乐高世界/h3 p上面的游戏还不过瘾,苹果又邀请了一家大公司来撑场面:乐高。他们基于前面提到的共享地图与后面将要介绍的模型识别,将乐高玩具极其真实的还原到一个AR游戏中,趣味性十足。想想这种场景,孩子拉着父母来到乐高店中,店里摆放了众多的乐高模型,拿起手机一扫,AR便将这些模型直接搬到游戏之中,孩子可以在AR游戏中体验各种乐高模型,在极大的满足之后,离开店里是不是就到了父母们掏腰包的时间:-P。/p p因为还提供了保存的功能,孩子们还可以将游戏进度保存下来,下次进店接着上次结束的地方继续游戏,孩子们一定会爱死这个功能。/p p一起来感受下乐高的世界:/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5NDIzMjE3Ng== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div h2 id=arkit-20新特性ARKit 2.0新特性/h2 p介绍完前面提到的AR能力之后,再来从开发者角度看看iOS 12为开发者带来了哪些新特性。ARKit 2.0将在这个版本发布,经历了开创性的ARKit 1.0与持续增强的ARKit 1.5版本之后,新版本ARKit又迎来了较大的更新,AR体验将进一步得到提升。/p h3 id=地图持久化地图持久化/h3 p这是2.0版本提供的最重要的功能,前面已经分析过,有了它之后,多人互动成为了可能。同苹果的工程师了解了下,这与重定位的原理类似,在1.5版本中,ARKit提供了压后台再回前台之后的恢复功能,两者的原理一样。这个所谓的地图中存储了哪些信息呢?如下图:/p pimg src=/img/posts/wwdc-arworldmap.png alt=wwdc-arworldmap //p p主要包含:/p ul li特征点及其描述子。/li li已经放置的锚点信息,如此一来,之前在检测到平面,以及其它用户放置的模型都可以保存。/li li还有原始的特征点,即接口中提供的raw data。/li /ul p下面是一个直观的地图建立的过程:/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5NDIzMDM4OA== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div p正是因为提供了地图持久化的功能,用户之间可以分享地图,也可以保存再加载地图,类似于游戏中保存进度一样,游戏性大大提升。/p p本以为这个功能可以用在室内导航中,但是咨询苹果工程师之后,由于数据量的原因,这只能够支持普通房间大小,所以不能够直接用于室内导航,可能需要多个地图拼接在一起才行。/p h3 id=world-tracking增强World Tracking增强/h3 pARKit 2.0对于world tracking做了大量的增强,主要包含以下方面:/p ul li更快的初始化速度与平面检测更快。如果熟悉ARKit版本的同学,可能会知道,在ARKit 1.0版本时,初始化的速度非常慢,一般需要5s左右的时间才能够结束,在这5s的时间内,相机只能够传递传感器的方向信息,并没有深度的变化。所以在app的设计上都存在一个引导的过程。为此我们在支付宝中基于slam算法对其专门做了一个优化,一帧就可以初始化完成,解决了初始化引导的问题。到了ARKit 1.5,初始化速度有了提升,只需要3s左右;而到了最新的版本,据说只需要1s左右就能够完成,这样体验会大大提升,可以抛弃长时间的引导动画了。/li li跟踪与平面检测更加稳定。/li li更加精确的平面检测。这与前面提到过的误差范围只有5%有关系,在之前的使用过程中,经常会出现检测到的平面边界无法与真实平面吻合的情况,对真实性的还原有一定的影响。优化之后,这类情形会得到改善。/li li持续对焦。这个功能其实在ARKit 1.5版本就已经支持,预览画面会好很多,否则会出现画面一直模糊的情况。同时跟踪的稳定性并没有受到影响。后面将要提到的图像跟踪与物体跟踪也同样支持。/li li增加了4:3的视频帧采集。第一个版本时,只能够使用1280P的视频帧,现在更加灵活。可能后续会开放更多的AVCaptureSession的参数。/li /ul h3 id=环境纹理环境纹理/h3 p在ARKit 2.0中,苹果引入了一个除地图保存以外最令人激动的功能——环境纹理,它可以大大提高渲染真实性。环境纹理能让模型反射其它物体,反射的可以是虚拟物体,也可以是由真实环境建立的真实物体。它是利用天空盒子提供的纹理信息,当成光照来达到反射的目的,这一过程也叫做code class=language-plaintext highlighter-rougeImage Based Lighting/code,这一关键点在于提供天空盒子。ARKit 2.0支持直接从摄像头拍摄的真实场景中获取这个天空盒子,它是自动构建的,不需要360度扫描。并且这个天空盒子还可以自动更新,至于更新的时机完全由内部机制决定。下面这个视频展示了天空盒子的构建过程:/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5NDIyOTEzMg== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div p从过程来看,其实是运用了ARKit的平面检测功能,它将平面上的纹理大致的贴到了天空盒子对应的平面中,随着平面信息不断的更新,天空盒子中的纹理也逐步完整。/p p对比下使用环境纹理之后的效果:/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5NDIyNzg3Mg== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div p可以看出,环境纹理功能打开之后,虚拟的金属碗逐渐反射出来真实世界的香蕉和桌面,真实性得到极大的提升,可以说,它是虚拟与现实融合的纽带。/p h3 id=图像跟踪图像跟踪/h3 p自从ARKit 1.5版本提供了图像识别之后,图像跟踪也在这个版本得到了支持。做得非常好的一点在于,帧率高达60fps,跟踪效果极其稳定,同时可以多个跟踪目标。可以看下面的视频,红绿蓝的坐标轴是虚拟的模型:/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5NDIyNjc4NA== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div p模型与图片帖合的非常好,并且跟踪过程稳定。支付宝在第一个AR就已经支持了图像跟踪,处理时间只需要3ms左右,可以达到60fps,但目前只能够跟踪一个目标。/p p因为图像跟踪对图片的质量有比较高的要求,图像内容越丰富,跟踪的效果越好,所以Xcode还提供了图像质量检测功能,如下面的两个识别图:/p pimg src=/img/posts/wwdc-image-quality.png alt=wwdc-image-quality / img src=/img/posts/wwdc-image-quality-xcode.png alt=wwdc-image-quality-xcode //p pXcode结出了非常详细的警告信息,如直方图的分布过窄、重复性结构过多、纯色区域过多等,同时给出了优化的建议,这个功能非常赞。在这一点上,支付宝的AR平台是在上传跟踪图像时,在服务端对图像进行打分判定是否能够跟踪,只有过了分数线才支持跟踪。但并没太多的提示信息,对于经验丰富的开发者,这点不是问题,但如果是新手,可能不太友好。/p p苹果给出了一个效果更酷的例子,直接可以在图像跟踪的位置播放视频:/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5NDIyNTUzMg== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div p这个功能支付宝也早在一年前已经支持,并且更进一步,我们还支持透明视频:/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5OTY4NzA0MA== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div h3 id=物体识别物体识别/h3 p图像识别可以适用于很多场景,但对于某些场合,仅用一张图像不够的,比如博物馆的展品。用户可能从任意角度去扫描这些展品,如果使用图像识别的话,需要不同角度非常多的识别图像。有没有更好的方案?苹果给出了物体识别,不同于图像识别,物体识别可以从各个角度识别物体,完美的适用于前面提到的博物馆场景。识别物不再是多张图像,而直接是一个3D的物体。如下视频所示,从任意角度识别到一个展品之后,在对应的位置放置一个展品信息的模型,后续的跟踪还是world tracking。/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5NDIyNDYxMg== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div p图像识别是提供图像,而物体识别则是提供一个预先扫描好的模型,所以它的制作成本稍高。这个模型有点类似于前面提到的地图,它也是一些特征点等信息的集合,原理上应该与可共享的地图一致。苹果还提供了一个专门的工具来对物体进行扫描,降低了识别模型的制作成本。/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5NDIyMjg1Mg== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div h3 id=人脸跟踪人脸跟踪/h3 p从iPhone X开始,苹果引入了前置深度相机,这次也有一些新功能的发布,如支持眼球跟踪:/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5NDIyMjE0NA== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div p以及支持舌头跟踪:/p div style=position:relative;padding-top:56.25%; iframe src=https://player.youku.com/embed/XMzY5NDIyMDIyNA== allowtransparency=true allowfullscreen=true scrolling=no border=0 frameborder=0 style=position:absolute;top:0;left:0;width:100%;height:100%;/iframe /div p这让各种表情变得更加有趣了。但由于这些是基于深度相机,所以只能在iPhone X这款支持前置深度相机的机型才支持。/p h2 id=其它其它/h2 p除了AR相关的新特性以外,这次大会还有些其它有意思的内容,不妨也在这里和大家分享一下。/p h3 id=siri-shortcutsSiri Shortcuts/h3 p这是一个能够将手机上的应用都连接起来的功能,类似功能可以怎么玩,网上有个段子脑洞很大:/p blockquote p一旦老婆的Twitter上出现“加班”字样,立即激活一条手机短信通知。同时,自动检测谷歌日历,找出几个今晚没有事情的老友。随后,在Facebook上新建一个活动“今晚喝大酒”,一旦超过3人同意,触发一条订餐消息给餐厅。餐厅查询Evernote,找到这群人最喜欢的菜和酒。/p /blockquote p也类似于现在的智能家居,比如地图上检测到我即将到家时,打开客厅的灯,空调根据当前季节调到合适的温度,放首我最喜欢的音乐。这不算什么新东西,类似的服务workflow、ifttt已经存在多年,可以连接应用,只是国内支持的服务很少,国外也感觉只在开发者中间流行。/p p苹果是在2017年收购了workflow,现在将Siri与其结合,推出新的shortcuts,由于与Siri可以很好的整合在一起,一方面,Siri的能力更强,正在努力摆脱玩物的尴尬场面。另一方面,有了Siri之后操作shortcuts更加方便,也更加智能。第三方应用也可以提供接入shortcuts的功能,这将赋于shortcuts更加强大的能力。同时Siri可以根据用户不同的习惯提供不同的建议。将来会不会有类似于app store一样的小平台,提供shortcuts的分享,这个功能将来变成什么样,非常值得期待。/p h3 id=metalMetal/h3 p苹果已经坚定了要大力发展它的API: Metal,从下个版本开始OpenGL/ES将会废除,不再提供更多的支持。我在Labs中提了一个OpenGL ES不能在iOS 11的机器上Capture GPU Frame的问题,本来与苹果工程师聊的时候,他说是应该支持的,让我提了bug,结果没多久便收到bug被closed的消息,理由是没有修复的计划。OpenGL/ES的玩家将要无情的被抛弃了。/p p虽然说要被废除,但是就像其它的废弃API一样,还有一段时间的生存期,苹果一样会留给开发者很长的缓冲时间。现在依然有很多iOS 2.0版本的废弃API还能够运行。/p p不过苹果在Metal上的优化,非常让人动心,并且专门有Session来讲述如何从OpenGL/ES迁移到Metal来,还请了一个游戏公司的开发者来讲述他们迁移的经验,游戏性能获取了巨大的提升,也并没有太大的工作量,这样结果似乎更加令人信服。/p p前面提到Xcode可以Capture GPU Frame,现在在Metal更进一步,可以调试shader,这一点真的是太赞,将极大的提升shader的编程,但是很可惜,仍然只能够用在Metal中,OpenGL/ES是无法享受此待遇的。/p h3 id=mlML/h3 pAR与ML是这次大会上非常重要的两个主题,ML也迎了较多的更新。首先它提供了一个新的CreateML的框架,可以直接用MBP通过playground来训练模型,10行以内的代码就搞定,并且模型压缩比高的夸张。由于是直接在Metal基础上开发,所以可以利用GPU的高性能来对模型进行训练,宣称MBP可以用48分钟训练完其它机器上需要24小时才能训练完成的模型。/p p最后一个有趣的是,ML直接支持语义分析,并且支持多国语言,包含中文。/p h3 id=xcode-10Xcode 10/h3 pXcode 10有几个比较有趣的更新,支持多光标、列选择,这两个功能早在很多现代的编辑,VIM,Emacs中就已经支持,不算惊喜。/p p另外一个功能是可以并行运行UT,这个功能很好,现在我们的UT在simulator上面运行一次需要半个小时左右,支持并行UT之后,可以极大的减少UT的时间。它的原理是通过clone出来多个simulator来执行,每个simulator分配了不同的测试用例,执行完成之后,再将结果汇总起来。之前在西门子时,我们也尝试过使用Hadoop来并行跑测试用例的方案,与这个原理一样。/p h3 id=macosmacOS/h3 pmacOS也将迎来新版本macOS Mojave,不过没想到的是在主题演讲时第一点竟然介绍的是dark mode,顿时倍感失望,仅仅是一个主题,竟然作为一个新系统版本的主要卖点着重介绍。/p p有一个比较激动的点,后面版本会让iOS上的应用非常方便的移植在Mac上,大大的降低两个版本之间的开发成本,iOS上这么多年积累的优秀应用也许也可以在Mac上大放异彩。/p p这个版本持续的在与手机端无缝合作上下功夫,苹果在体验上的不懈追求宠坏了用户,让用户不知不觉间就习惯了这种特性,觉得这似乎是理所当然的,没有这个功能反而觉得奇怪,比如之前的Mac与iPhone剪贴板打通,非常体贴。/p h3 id=facetimeFaceTime/h3 pfacetime下个版本将支持最多同时32人通话,查了下微信、QQ,都是最多添加9人,但由于受限于平台,在国内还是无法流行起来,微信与QQ是更好的方案。但从技术层面来说,32人同时通话需要大量的带宽与渲染性能,看起来非常了不起。/p h3 id=非技术非技术/h3 p其它还有一些非技术方面的体验也让我留了下很深的印象。大会包容万象,下至10岁,上至82岁,有身体健全人士,也有听力障碍人士,大会上不止一次的看到听力障碍人士对面坐着一个手语翻译工作人员给他们翻译演讲的内容。头发花白、胡子大把的开发者随处可见,大家来自不同国家,说着不同的语言,都聚集于此。/p p大会的形式也颇有新意,采用Session与Labs结合,Session类似学校时的课堂,不仅讲述新的特性,很多开发者可能会遇到的问题也会独自成章。开发者们可以参加自己感兴趣的主题,四天时间总共有一百多个Session。/p pimg src=/img/posts/wwdc-session.jpg alt=wwdc-session //p p除了Session以外,专门还有一对一的答疑时间,叫做Labs,这可能是WWDC门票里面最值回票价的环节,开发者可以就几乎任何相关的问题与苹果工程师一对一的对话。当然,不限于新特性,也可以咨询在使用这些framework中遇到的问题。除了这些技术性的Labs,还有UI Design Labs,可以就自己正在开发的app向苹果工程师咨询建议,但这种Labs需要提前预约,预约成功之后将得到半个小时的建议时间,非常宝贵。/p pimg src=/img/posts/wwdc-labs.jpg alt=wwdc-labs //p p在午餐时间,苹果还邀请了一些高校和企业来做分享,我去听了一个大学教授关于科技运用到考古学上的实践,还有一个Pixar的光照部门主管关于Pixar一些经典影片开发过程的分享。国外的企业与高校还有其它行业的结合非常紧密,比如很多软件对学生免费,这算是一个双赢的局面,企业既可以提升企业的公众形象,获取更好的政府及学校生源支持,还能够将科研领域的最新研究成果落地商业化。同样高校也可以通过企业的宣传机会,将自己的科研领域分享给大众,吸引更多的人来参与。这点国内的企业似乎做的不足,有种高校与企业各自发展,脱节的感觉。/p p最后一点,虽然乔布斯已经不在,但主题演讲仍然极受欢迎,大家热情不减,9点开始大会,凌晨3点就很多人去排队,为了可以离演讲台更近一些,似乎我们只有买火车票与孩子幼儿园报名才这样:-P。/p h3 id=视频地址视频地址/h3 p这里附上AR相关的session视频地址:/p ul likeynote: a href=https://developer.apple.com/videos/play/wwdc2018/101/https://developer.apple.com/videos/play/wwdc2018/101//a/li liARKit 2.0: a href=https://developer.apple.com/videos/play/wwdc2018/602/https://developer.apple.com/videos/play/wwdc2018/602//a/li liAR Quick Look: a href=https://developer.apple.com/videos/play/wwdc2018/603/https://developer.apple.com/videos/play/wwdc2018/603//a/li li多人游戏: a href=https://developer.apple.com/videos/play/wwdc2018/605/https://developer.apple.com/videos/play/wwdc2018/605//a/li li理解跟踪: a href=https://developer.apple.com/videos/play/wwdc2018/610/https://developer.apple.com/videos/play/wwdc2018/610//a/li liAR体验设计: a href=https://developer.apple.com/videos/play/wwdc2018/805https://developer.apple.com/videos/play/wwdc2018/805/a/li /ul h2 id=写在最后写在最后/h2 p这是一个Geekers们的聚会,大家调侃这是朝圣,这是xie教聚会,大家狂热又专注,不分国度、肤色、语言,不远万里聚集于此。即使所有的Session内容都有视频,但就像你最爱的演唱会即使有DVD,也一定要去现场感受一次一样。对了,这里还是为数不多的男厕排长队,而女厕空荡荡的奇妙地方。这里是WWDC。/p p(全文完)/p pfeihu/p p2018.07.02 于 Shenzhen/p

如何处理iOS中照片的方向

作者 feihu
2015年5月31日 00:00
img src=https://feihu.me/img/post-banners/orientation.png alt= width=600 /br / p使用过iPhone或者iPad的朋友在拍照时不知是否遇到过这样的问题,将设备中的照片导出到Windows上时,经常发现导出的照片方向会有问题,要么横着,要么颠倒着,需要旋转才适合观看。而如果直接在这些设备上浏览时,照片会始终显示正确的方向,在Mac上也能正确显示。最近在iOS的开发中也遇到了同样的问题,将拍摄的照片上传到服务器后,再由Windows端下载该照片,发现手机上完全正常的照片到了这里显示的横七竖八。同一张照片为什么在不同的设备上表现的不同?如何能够避免这种情况?本文将和大家一一解开这些问题。/p h3 class=no_toc id=目录目录/h3 ul id=markdown-toc lia href=#照片的存储演变 id=markdown-toc-照片的存储演变照片的存储演变/a ul lia href=#胶片时代 id=markdown-toc-胶片时代胶片时代/a/li lia href=#数码时代 id=markdown-toc-数码时代数码时代/a/li lia href=#方向传感器 id=markdown-toc-方向传感器方向传感器/a/li /ul /li lia href=#exifexchangeable-image-file-format id=markdown-toc-exifexchangeable-image-file-formatEXIF(Exchangeable Image File Format)/a ul lia href=#orientation id=markdown-toc-orientationOrientation/a/li lia href=#iphone上的情况 id=markdown-toc-iphone上的情况iPhone上的情况/a/li /ul /li lia href=#验证exif id=markdown-toc-验证exif验证EXIF/a ul lia href=#mac平台 id=markdown-toc-mac平台Mac平台/a/li lia href=#windows平台 id=markdown-toc-windows平台Windows平台/a/li /ul /li lia href=#开发时如何避免 id=markdown-toc-开发时如何避免开发时如何避免/a ul lia href=#直观的解决方案 id=markdown-toc-直观的解决方案直观的解决方案/a/li lia href=#第二种简单的方法 id=markdown-toc-第二种简单的方法第二种简单的方法/a/li /ul /li lia href=#结尾 id=markdown-toc-结尾结尾/a/li /ul hr / h2 id=照片的存储演变照片的存储演变/h2 p一切都得从相机的发展开始说起。/p h3 id=胶片时代胶片时代/h3 p一般相机拍摄出来的画面都是长方形,在拍摄的那一瞬间,它会将取景器中的场景对应的颜色值存到对应的像素位置。相机本身并没有任何方向的概念,只是使用者想要拍摄的场景在他期望的照片中显示的方式与实际存在差异时,才有了方向一说。如下图,对一个场景code class=language-plaintext highlighter-rougeF/code进行拍摄,相机的方向可能会有这样四个常见的角度:/p pimg src=/img/posts/orientation-camera-view.png alt=摄像头取景 //p p相机是“自私”的,由于相机仅反应真实的场景,它不理解拍摄的内容,因此照片都以相机的坐标系保存,于是上面四种情形实际拍摄出来的照片会像这样:/p pimg src=/img/posts/orientation-encoded-jpeg.png alt=存储情况 //p p最初的卡片机时代,照片都会经由底片洗出来。那时不存在照片的方向问题,因为不管我们以何种角度拍摄,最终洗出来的照片,它本身非常容易旋转,所以我们总可以通过简单的旋转来观看照片或者保存照片。比如这张照片墙中的照片,你能否说哪些照片是横着?哪些颠倒着?你甚至都无法判断每张照片相机是以何种角度拍摄的,因为每张都已经旋转至适合观看的角度。/p pimg src=/img/posts/orientation-photo-wall.jpg alt=照片墙 //p h3 id=数码时代数码时代/h3 p可是到了数码时代,不再需要底片,照片需要被存成一个图像文件。对于上面的拍摄角度,存储方式并没有变化,所有的场景仍然是以相机的坐标系来保存。于是这些照片仍像上面一样,原封不动的保存了下来:/p pimg src=/img/posts/orientation-encoded-jpeg.png alt=存储情况 //p p虽然存储方式不变,和卡机机时代的实体相片不同的是,由于电脑屏幕可没洗出来的照片那么容易旋转,所以照片只能够以它存储于磁盘中的方向来展示。这便是为何照片传到电脑上之后,会出现横了,或者颠倒的情况。正因为这样,我们只有利用工具来旋转照片才能够正常观看。/p h3 id=方向传感器方向传感器/h3 p为了克服这一情况,让照片可以真实的反应人们拍摄时看到的场景,现在很多相机中就加入了方向传感器,它能够记录下拍摄时相机的方向,并将这一信息保存在照片中。照片的存储方式还是没有任何改变,它仍然是以相机的坐标系来保存,只是当相机来浏览这些照片时,相机可以根据照片中的方向信息,结合此时相机的方向,对照片进行旋转,从而转到适合人们观看的角度。/p p但是很遗憾,这一标准并没有被广泛的传播开来,或者说始终如一的贯彻,这也导致了本文所讨论的问题。/p h2 id=exifexchangeable-image-file-formatEXIF(Exchangeable Image File Format)/h2 p那么,方向信息到底是记录在照片的什么位置?/p p了解图像格式的朋友可能会知道,图像一般都由两大部分组成,一部分是数据本身,它记录了每个像素的颜色值,另外一部分是文件头,这里面记录着形如图像的宽度,高度等信息。我们所讨论的方向信息便是被存储于文件头中。更为具体一些:code class=language-plaintext highlighter-rougeEXIF/code中,a href=http://zh.wikipedia.org/wiki/EXIF维基百科/a上对其的解释为:/p blockquote p可交换图像文件格式常被简称为Exif(Exchangeable image file format),是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据… Exif可以附加于JPEG、TIFF、RIFF等文件之中/p /blockquote pstrong注意/strong:PNG格式的图像中不包含。/p h3 id=orientationOrientation/h3 p在code class=language-plaintext highlighter-rougeEXIF/code涵盖的各种信息之中,其中有一个叫做code class=language-plaintext highlighter-rougeOrientation (rotation)/code的标签,用于记录图像的方向,这便是相机写入方向信息的最终位置。它总共定义了八个值:/p pimg src=/img/posts/orientation-eight-values.png alt=Orientation的八个值 //p pstrong注意/strong:对于上面的八种方向中,加了code class=language-plaintext highlighter-rouge*/code的并不常见,因为它们代表的是镜像方向,如果不做任何的处理,不管相机以任何角度拍摄,都无法出现镜像的情况。/p p这个表格代表什么意义?我们来看第一行,值为1时,右边两列的值分别为:Row #0 is code class=language-plaintext highlighter-rougeTop/code,Column #0 is code class=language-plaintext highlighter-rougeLeft side/code,其实很好理解,它表示照片的第一行位于顶端,而第一列位于左侧,那么这张照片自然就是以正常角度拍摄的。/p p对着前面的四种拍摄角度,由于相机都是以其自身的坐标系来保存照片,因此每张照片对应的第一行和第一列的位置始终如下:/p pimg src=/img/posts/orientation-row0-column0.png alt=第一行第一列 //p p我们来看第二张照片,这张照片需要逆时针旋转90度才能够正常观看。旋转之后,它的第一行位于左侧,而第一列位于下侧。如此一来,对比表格,它的code class=language-plaintext highlighter-rougeOrientation/code值为8。所以说,strong这个code class=language-plaintext highlighter-rougeOrientation/code值提供了想要正常观看图像时应该旋转的方式。/strong/p p以同样的方法,我们可以推断出上面四种方式拍摄时,对应code class=language-plaintext highlighter-rougeEXIF/code中code class=language-plaintext highlighter-rougeOrientation/code的值如下所示:/p pimg src=/img/posts/orientation-value.png alt=图片的方向 //p p由于相机加上了方向传感器的缘故,可以非常容易的检测出以上几种拍摄角度,并将角度对应的code class=language-plaintext highlighter-rougeOrientation/code值保存至图像中。查看图像时,相机检测到其code class=language-plaintext highlighter-rougeEXIF/code中的code class=language-plaintext highlighter-rougeOrientation/code信息,并将图像旋转相应的角度显示给用户,这样便达到了智能显示的目的。/p h3 id=iphone上的情况iPhone上的情况/h3 p作为智能手机的重要组成部分,形形色色的传感器自然必不可少。在iOS的设备中也是包含了这样的方向传感器,它也采用了同样的方式来保存照片的方向信息到code class=language-plaintext highlighter-rougeEXIF/code中。但是它默认的照片方向并不是竖着拿手机时的情况,而是横向,即Home键在右侧,如下:/p pimg src=/img/posts/orientation-zero-degree.png alt=iPhone正常方向 //p p如此一来,如果竖着拿手机拍摄时,就相当于对手机顺时针旋转了90度,也即上面相机图片中的最后一幅,那么它的code class=language-plaintext highlighter-rougeOrientation/code值为6。/p pimg src=/img/posts/orientation-iphone-portrait.png alt=iPhone竖向 //p h2 id=验证exif验证EXIF/h2 p在经过上面的分析之后,我们来看看实际情况如何。我们分别在Mac和Windows平台上对前面的论述做一个验证。/p h3 id=mac平台Mac平台/h3 p可以将照片从iOS设备中导出到Mac系统上,(注意,不能够使用iPhoto或者Photos来导入,因为这样照片在导入之前会被自动调整好方向)在这里我们像Windows中一样,将iPhone当成移动硬盘,直接访问其照片。在Mac上可以使用a href=http://pro.itools.cn/mac/englishiTools/a这一神器。/p p然后用Mac上的code class=language-plaintext highlighter-rouge预览/code程序查看其code class=language-plaintext highlighter-rougeEXIF/code属性,通过code class=language-plaintext highlighter-rouge预览-工具-显示检查器/code打开对话框,即可查看到照片中关于方向的详细信息。下面四张图分别展示了上面四种方向下拍得照片的code class=language-plaintext highlighter-rougeOrientation/code值:/p ul li pHome键位于右侧时,即相机的默认方向,值为1。 img src=/img/posts/orientation-home-right.jpg alt=Home键在右侧 //p /li li pHome键位于上侧时,值为8。 img src=/img/posts/orientation-home-up.jpg alt=Home键在上侧 //p /li li pHome键位于左侧时,值为3。 img src=/img/posts/orientation-home-left.jpg alt=Home键在左侧 //p /li li pHome键位于下侧时,即正常手持手机的方向,值为6。 img src=/img/posts/orientation-home-bottom.jpg alt=Home键在下侧 //p /li /ul p对照前面的分析,完全一致。而且照片显示正常,说明在Mac上默认的code class=language-plaintext highlighter-rouge预览/code程序会自动的处理code class=language-plaintext highlighter-rougeEXIF/code中的code class=language-plaintext highlighter-rougeOrientation/code信息。/p p再次提醒:照片存储在手机中始终是以相机坐标系保存的,只是浏览工作在读取方向信息之后做了旋转。/p h3 id=windows平台Windows平台/h3 p前面提到过,被写在图像文件头中的方向信息并没有被全部支持,Windows的照片查看器便是其中之一,这也是Windows用户最常使用的照片浏览工具。因为没有读取方向信息,照片被读入之后,完全按照其存储方式来显示,这样便出现了横向,或者颠倒的情况。下面四张图便分别是上一节中拍得的照片在Windows上的显示效果,注意看方向。/p pimg src=/img/posts/orientation-windows.jpg alt=Windows上的情况 //p h2 id=开发时如何避免开发时如何避免/h2 p既然不是所有的工具都支持方向属性,这其中甚至包含了具有最多用户群体的Windows,那么我们在开发照片相关的应用时,有没有什么应对之策?/p p当然有!因为可以非常容易的得到照片的方向信息,那么只需要在保存之前将照片旋转至正常观看的方向即可,然后直接将最终具有正确方向的照片保存下来,搞定。/p p当我们得到一个code class=language-plaintext highlighter-rougeUIImage/code对象时,它有一个属性叫:code class=language-plaintext highlighter-rougeimageOrientation/code,这里面便保存了方向信息:/p blockquote pPropertybr / The orientation of the receiver’s image. (read-only)br / Discussionbr / Image orientation affects the way the image data is displayed when drawn. By default, images are displayed in the “up” orientation. If the image has associated metadata (such as EXIF information), however, this property contains the orientation indicated by that metadata. For a list of possible values for this property, see UIImageOrientation./p /blockquote p它刚好也可能为下面八种值,这些值可以和code class=language-plaintext highlighter-rougeEXIF/code中code class=language-plaintext highlighter-rougeOrientation/code的定义一一对应:/p ul liimg src=/img/posts/orientation-UIImageOrientationUp.png alt=Up / UIImageOrientationUp/li liimg src=/img/posts/orientation-UIImageOrientationDown.png alt=Down / UIImageOrientationDown/li liimg src=/img/posts/orientation-UIImageOrientationLeft.png alt=Left / UIImageOrientationLeft/li liimg src=/img/posts/orientation-UIImageOrientationRight.png alt=Right / UIImageOrientationRight/li liimg src=/img/posts/orientation-UIImageOrientationUpMirrored.png alt=UpMirror / UIImageOrientationUpMirrored/li liimg src=/img/posts/orientation-UIImageOrientationDownMirrored.png alt=DownMirror / UIImageOrientationDownMirrored/li liimg src=/img/posts/orientation-UIImageOrientationLeftMirrored.png alt=LeftMirror / UIImageOrientationLeftMirrored/li liimg src=/img/posts/orientation-UIImageOrientationRightMirrored.png alt=RightMirror / UIImageOrientationRightMirrored/li /ul p那么我们便可以根据这一属性对图像进行相应的旋转,从而将图像的原始数据旋转至正确的方向,在浏览照片时无需方向信息便可正常浏览。/p p关于如何旋转图像,StackOverflow上给出了很好的答案,比如a href=http://stackoverflow.com/a/5427890/973315这个/a。我们简单做一个介绍:/p h3 id=直观的解决方案直观的解决方案/h3 p首先,为code class=language-plaintext highlighter-rougeUIImage/code创建一个category,其中包含code class=language-plaintext highlighter-rougefixOrientation/code方法:/p pUIImage+fixOrientation.h/p figure class=highlightprecode class=language-objc data-lang=objcspan class=k@interface/span span class=ncUIImage/span span class=p(/spanspan class=nlfixOrientation/spanspan class=p)/span span class=k-/span span class=p(/spanspan class=nUIImage/span span class=o*/spanspan class=p)/spanspan class=nfixOrientation/spanspan class=p;/span span class=k@end/span/code/pre/figure pUIImage+fixOrientation.m/p figure class=highlightprecode class=language-objc data-lang=objcspan class=k@implementation/span span class=ncUIImage/span span class=p(/spanspan class=nlfixOrientation/spanspan class=p)/span span class=k-/span span class=p(/spanspan class=nUIImage/span span class=o*/spanspan class=p)/spanspan class=nfixOrientation/span span class=p{/span span class=c1// No-op if the orientation is already correct/span span class=kif/span span class=p(/spanspan class=nself/spanspan class=p./spanspan class=nimageOrientation/span span class=o==/span span class=nUIImageOrientationUp/spanspan class=p)/span span class=kreturn/span span class=nself/spanspan class=p;/span span class=c1// We need to calculate the proper transformation to make the image upright./span span class=c1// We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored./span span class=nCGAffineTransform/span span class=ntransform/span span class=o=/span span class=nCGAffineTransformIdentity/spanspan class=p;/span span class=kswitch/span span class=p(/spanspan class=nself/spanspan class=p./spanspan class=nimageOrientation/spanspan class=p)/span span class=p{/span span class=kcase/span span class=nUIImageOrientationDown/spanspan class=p:/span span class=kcase/span span class=nUIImageOrientationDownMirrored/spanspan class=p:/span span class=ntransform/span span class=o=/span span class=nCGAffineTransformTranslate/spanspan class=p(/spanspan class=ntransform/spanspan class=p,/span span class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nwidth/spanspan class=p,/span span class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nheight/spanspan class=p);/span span class=ntransform/span span class=o=/span span class=nCGAffineTransformRotate/spanspan class=p(/spanspan class=ntransform/spanspan class=p,/span span class=nM_PI/spanspan class=p);/span span class=kbreak/spanspan class=p;/span span class=kcase/span span class=nUIImageOrientationLeft/spanspan class=p:/span span class=kcase/span span class=nUIImageOrientationLeftMirrored/spanspan class=p:/span span class=ntransform/span span class=o=/span span class=nCGAffineTransformTranslate/spanspan class=p(/spanspan class=ntransform/spanspan class=p,/span span class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nwidth/spanspan class=p,/span span class=mi0/spanspan class=p);/span span class=ntransform/span span class=o=/span span class=nCGAffineTransformRotate/spanspan class=p(/spanspan class=ntransform/spanspan class=p,/span span class=nM_PI_2/spanspan class=p);/span span class=kbreak/spanspan class=p;/span span class=kcase/span span class=nUIImageOrientationRight/spanspan class=p:/span span class=kcase/span span class=nUIImageOrientationRightMirrored/spanspan class=p:/span span class=ntransform/span span class=o=/span span class=nCGAffineTransformTranslate/spanspan class=p(/spanspan class=ntransform/spanspan class=p,/span span class=mi0/spanspan class=p,/span span class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nheight/spanspan class=p);/span span class=ntransform/span span class=o=/span span class=nCGAffineTransformRotate/spanspan class=p(/spanspan class=ntransform/spanspan class=p,/span span class=o-/spanspan class=nM_PI_2/spanspan class=p);/span span class=kbreak/spanspan class=p;/span span class=kcase/span span class=nUIImageOrientationUp/spanspan class=p:/span span class=kcase/span span class=nUIImageOrientationUpMirrored/spanspan class=p:/span span class=kbreak/spanspan class=p;/span span class=p}/span span class=kswitch/span span class=p(/spanspan class=nself/spanspan class=p./spanspan class=nimageOrientation/spanspan class=p)/span span class=p{/span span class=kcase/span span class=nUIImageOrientationUpMirrored/spanspan class=p:/span span class=kcase/span span class=nUIImageOrientationDownMirrored/spanspan class=p:/span span class=ntransform/span span class=o=/span span class=nCGAffineTransformTranslate/spanspan class=p(/spanspan class=ntransform/spanspan class=p,/span span class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nwidth/spanspan class=p,/span span class=mi0/spanspan class=p);/span span class=ntransform/span span class=o=/span span class=nCGAffineTransformScale/spanspan class=p(/spanspan class=ntransform/spanspan class=p,/span span class=o-/spanspan class=mi1/spanspan class=p,/span span class=mi1/spanspan class=p);/span span class=kbreak/spanspan class=p;/span span class=kcase/span span class=nUIImageOrientationLeftMirrored/spanspan class=p:/span span class=kcase/span span class=nUIImageOrientationRightMirrored/spanspan class=p:/span span class=ntransform/span span class=o=/span span class=nCGAffineTransformTranslate/spanspan class=p(/spanspan class=ntransform/spanspan class=p,/span span class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nheight/spanspan class=p,/span span class=mi0/spanspan class=p);/span span class=ntransform/span span class=o=/span span class=nCGAffineTransformScale/spanspan class=p(/spanspan class=ntransform/spanspan class=p,/span span class=o-/spanspan class=mi1/spanspan class=p,/span span class=mi1/spanspan class=p);/span span class=kbreak/spanspan class=p;/span span class=kcase/span span class=nUIImageOrientationUp/spanspan class=p:/span span class=kcase/span span class=nUIImageOrientationDown/spanspan class=p:/span span class=kcase/span span class=nUIImageOrientationLeft/spanspan class=p:/span span class=kcase/span span class=nUIImageOrientationRight/spanspan class=p:/span span class=kbreak/spanspan class=p;/span span class=p}/span span class=c1// Now we draw the underlying CGImage into a new context, applying the transform/span span class=c1// calculated above./span span class=nCGContextRef/span span class=nctx/span span class=o=/span span class=nCGBitmapContextCreate/spanspan class=p(/spanspan class=nbNULL/spanspan class=p,/span span class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nwidth/spanspan class=p,/span span class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nheight/spanspan class=p,/span span class=nCGImageGetBitsPerComponent/spanspan class=p(/spanspan class=nself/spanspan class=p./spanspan class=nCGImage/spanspan class=p),/span span class=mi0/spanspan class=p,/span span class=nCGImageGetColorSpace/spanspan class=p(/spanspan class=nself/spanspan class=p./spanspan class=nCGImage/spanspan class=p),/span span class=nCGImageGetBitmapInfo/spanspan class=p(/spanspan class=nself/spanspan class=p./spanspan class=nCGImage/spanspan class=p));/span span class=nCGContextConcatCTM/spanspan class=p(/spanspan class=nctx/spanspan class=p,/span span class=ntransform/spanspan class=p);/span span class=kswitch/span span class=p(/spanspan class=nself/spanspan class=p./spanspan class=nimageOrientation/spanspan class=p)/span span class=p{/span span class=kcase/span span class=nUIImageOrientationLeft/spanspan class=p:/span span class=kcase/span span class=nUIImageOrientationLeftMirrored/spanspan class=p:/span span class=kcase/span span class=nUIImageOrientationRight/spanspan class=p:/span span class=kcase/span span class=nUIImageOrientationRightMirrored/spanspan class=p:/span span class=c1// Grr.../span span class=nCGContextDrawImage/spanspan class=p(/spanspan class=nctx/spanspan class=p,/span span class=nCGRectMake/spanspan class=p(/spanspan class=mi0/spanspan class=p,/spanspan class=mi0/spanspan class=p,/spanspan class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nheight/spanspan class=p,/spanspan class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nwidth/spanspan class=p),/span span class=nself/spanspan class=p./spanspan class=nCGImage/spanspan class=p);/span span class=kbreak/spanspan class=p;/span span class=nldefault:/span span class=nCGContextDrawImage/spanspan class=p(/spanspan class=nctx/spanspan class=p,/span span class=nCGRectMake/spanspan class=p(/spanspan class=mi0/spanspan class=p,/spanspan class=mi0/spanspan class=p,/spanspan class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nwidth/spanspan class=p,/spanspan class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nheight/spanspan class=p),/span span class=nself/spanspan class=p./spanspan class=nCGImage/spanspan class=p);/span span class=kbreak/spanspan class=p;/span span class=p}/span span class=c1// And now we just create a new UIImage from the drawing context/span span class=nCGImageRef/span span class=ncgimg/span span class=o=/span span class=nCGBitmapContextCreateImage/spanspan class=p(/spanspan class=nctx/spanspan class=p);/span span class=nUIImage/span span class=o*/spanspan class=nimg/span span class=o=/span span class=p[/spanspan class=nUIImage/span span class=nfimageWithCGImage/spanspan class=p:/spanspan class=ncgimg/spanspan class=p];/span span class=nCGContextRelease/spanspan class=p(/spanspan class=nctx/spanspan class=p);/span span class=nCGImageRelease/spanspan class=p(/spanspan class=ncgimg/spanspan class=p);/span span class=kreturn/span span class=nimg/spanspan class=p;/span span class=p}/span span class=k@end/span/code/pre/figure p代码有些长,不过却非常直观。这里面涉及到图像矩阵变换的操作,理解起来可能稍稍有些困难,接下来,我会有另外一篇文章专门来介绍图像变换。现在,记住下面两点便能够很好的帮助理解:/p ol li图像的原点在左下角/li li矩阵变换时,后面的矩阵先作用,前面的矩阵后作用/li /ol p以code class=language-plaintext highlighter-rougeUIImageOrientationDown/code方向为例,img src=/img/posts/orientation-UIImageOrientationDown.png alt=UIImageOrientationDown /,很明显它翻转了180度。那么对它的旋转需要两步,第一步是以左下方为原点旋转180度,(此时顺时针还是逆时针旋转效果一样)旋转后上图变为:img src=/img/posts/orientation-transform-rotate.png alt=旋转180度后 / 。用代码表示为:/p figure class=highlightprecode class=language-objc data-lang=objcspan class=ntransform/span span class=o=/span span class=nCGAffineTransformRotate/spanspan class=p(/spanspan class=ntransform/spanspan class=p,/span span class=nM_PI/spanspan class=p);/span/code/pre/figure p因为是以左下方为原点旋转的,所以整幅图被移到了第三象限。第二步需要将其平移至第一象限,向右上方进行平移即可。x方向上移动距离为图像的宽度,y方向上移动距离为图像的高度,所以平移后图像变为:img src=/img/posts/orientation-transform-transition.png alt=平移后 /。代码为:/p figure class=highlightprecode class=language-objc data-lang=objcspan class=ntransform/span span class=o=/span span class=nCGAffineTransformTranslate/spanspan class=p(/spanspan class=ntransform/spanspan class=p,/span span class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nwidth/spanspan class=p,/span span class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nheight/spanspan class=p);/span/code/pre/figure p再加上我们前面所说的第二点,矩阵变换时,后面的矩阵先作用,前面的矩阵后作用,那么只需要将上面两步颠倒即可:/p figure class=highlightprecode class=language-objc data-lang=objcspan class=ntransform/span span class=o=/span span class=nCGAffineTransformTranslate/spanspan class=p(/spanspan class=ntransform/spanspan class=p,/span span class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nwidth/spanspan class=p,/span span class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p./spanspan class=nheight/spanspan class=p);/span span class=ntransform/span span class=o=/span span class=nCGAffineTransformRotate/spanspan class=p(/spanspan class=ntransform/spanspan class=p,/span span class=nM_PI/spanspan class=p);/span/code/pre/figure p其它的方向可以用完全一样的方法来分析,这里不再一一赘述。/p h3 id=第二种简单的方法第二种简单的方法/h3 p第二种方法同样也是StackOverflow上的a href=http://stackoverflow.com/a/10611036/973315答案/a,没那么直观,但非常简单:/p figure class=highlightprecode class=language-objc data-lang=objcspan class=k-/span span class=p(/spanspan class=nUIImage/span span class=o*/spanspan class=p)/spanspan class=nnormalizedImage/span span class=p{/span span class=kif/span span class=p(/spanspan class=nself/spanspan class=p./spanspan class=nimageOrientation/span span class=o==/span span class=nUIImageOrientationUp/spanspan class=p)/span span class=kreturn/span span class=nself/spanspan class=p;/span span class=nUIGraphicsBeginImageContextWithOptions/spanspan class=p(/spanspan class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p,/span span class=nbNO/spanspan class=p,/span span class=nself/spanspan class=p./spanspan class=nscale/spanspan class=p);/span span class=p[/spanspan class=nself/span span class=nfdrawInRect/spanspan class=p:(/spanspan class=nCGRect/spanspan class=p){/spanspan class=mi0/spanspan class=p,/span span class=mi0/spanspan class=p,/span span class=nself/spanspan class=p./spanspan class=nsize/spanspan class=p}];/span span class=nUIImage/span span class=o*/spanspan class=nnormalizedImage/span span class=o=/span span class=nUIGraphicsGetImageFromCurrentImageContext/spanspan class=p();/span span class=nUIGraphicsEndImageContext/spanspan class=p();/span span class=kreturn/span span class=nnormalizedImage/spanspan class=p;/span span class=p}/span/code/pre/figure p这里是利用了code class=language-plaintext highlighter-rougeUIImage/code中的code class=language-plaintext highlighter-rougedrawInRect/code方法,它会将图像绘制到画布上,并且已经考虑好了图像的方向,开发文档这样解释:/p blockquote p-drawInRect:br / Draws the entire image in the specified rectangle, scaling it as needed to fit.br //p pDiscussionbr / This method draws the entire image in the current graphics context, respecting the image’s orientation setting. In the default coordinate system, images are situated down and to the right of the origin of the specified rectangle. This method respects any transforms applied to the current graphics context, however./p /blockquote h2 id=结尾结尾/h2 p关于照片方向的处理就介绍到这里,相信看完本文你已经知悉为何以及如何处理这个问题。/p p关于code class=language-plaintext highlighter-rougeEXIF/code,这里面包含了很多有趣的内容,比如iPhone拍摄后,可以记录当时的GPS位置,这样在查看照片的时候就可以很神奇的知道照片的拍摄地。如果感兴趣可以去一探究竟。/p p另外,除去专门的照片浏览工具,所有的现代浏览器也天生具备查看图片的功能。而且有很多浏览器也已经支持code class=language-plaintext highlighter-rougeEXIF/code中的code class=language-plaintext highlighter-rougeOrientation/code,比如Firefox, Chrome, Safari。但同样很可惜,IE并不支持(一直到IE9.0尚不支持)。也许和Win7设计时并没有这些具有方向传感器的手机有关,我从网上了解到,在当初2012年收集building Windows8意见时,就有人提到过这一问题,希望能够考虑图片的方向信息,微软也给出了a href=http://blogs.msdn.com/b/b8/archive/2012/01/30/acting-on-file-management-feedback.aspx回应/a:/p blockquote p(In Windows8)Explorer now respects EXIF orientation information for JPEG images. If your camera sets this value accurately, you will rarely need to correct orientation./p /blockquote p但我一直没有用过Windows8,如果有使用过的,希望可以帮我验证一下是否微软已经修复这个问题。/p p(全文完)/p pfeihubr / 2015.05.31 于 Shenzhen/p

知无涯之回车换行的故事

作者 feihu
2014年12月17日 00:00
img src=https://feihu.me/img/post-banners/end-of-line.png alt= width=600 /br / p不知各位有没有过这样的经历:/p ul liLinux上创建的文件在Windows上打开时,结果所有内容会挤成一行。而Windows上创建的文件在Linux上打开时,每一行的结尾又多了一个奇怪字符code class=language-plaintext highlighter-rouge^M/code。/li li在安装Windows版的git时,安装向导在某一步会提示你选择”Configuring the line ending conversions”,里面提到了Windows-style和unix-style的line endings,为什么会有这些呢?/li li调用C语言的API code class=language-plaintext highlighter-rougefopen/code时,会有text mode和binary mode,这两者有什么区别?/li /ul p其实这一切都和我们常说的回车换行有关,但你有没有很奇怪,什么是回车?直接用换行不就好了,为什么要分开两个词?我们使用的键盘上的键明明起得是换行的作用,为什么叫回车?千万别被绕晕了,本文将和大家讨论有关回车换行的一段有趣的历史,随后将回答这些问题。/p h3 class=no_toc id=目录目录/h3 ul id=markdown-toc lia href=#历史 id=markdown-toc-历史历史/a ul lia href=#打字机 id=markdown-toc-打字机打字机/a/li lia href=#分歧出现 id=markdown-toc-分歧出现分歧出现/a/li lia href=#混乱的状况 id=markdown-toc-混乱的状况混乱的状况/a/li /ul /li lia href=#统一 id=markdown-toc-统一统一/a/li lia href=#text-mode-vs-binary-mode id=markdown-toc-text-mode-vs-binary-modeText Mode VS Binary Mode/a ul lia href=#windows平台 id=markdown-toc-windows平台Windows平台/a/li lia href=#linux和mac-osx平台 id=markdown-toc-linux和mac-osx平台Linux和Mac OSX平台/a/li /ul /li lia href=#更多资料 id=markdown-toc-更多资料更多资料/a/li lia href=#结尾 id=markdown-toc-结尾结尾/a/li /ul hr / h2 id=历史历史/h2 p我们通常所说的回车换行其实只相当于一个概念,即一行结束,开始下一行,英文叫做code class=language-plaintext highlighter-rougeEnd-of-Line/code,简写为code class=language-plaintext highlighter-rougeEOL/code。你也可以将这理解为一个strong逻辑上的/strong换行,但为了与回车换行中的换行区分开来,我们后面还是称呼它为code class=language-plaintext highlighter-rougeEOL/code。/p h3 id=打字机打字机/h3 p回车换行严格说起来是两个独立的概念,即回车和换行,它们的出现要追溯到计算机出现之前,那时有一种电传打字机:Teletype Model 33 ASR,如下图:/p pimg src=http://bytecollector.com/images/asr-33_vcf_02.jpg alt=Teletype Model 33 ASR //p p在打字机上,有一个部件叫code class=language-plaintext highlighter-rougeCarriage/code,它是打字头,相当于打字机的光标。每输入一个字符,code class=language-plaintext highlighter-rougeCarriage/code就前进一格。当输满一行后,想要切换到下一行时,需要code class=language-plaintext highlighter-rougeCarriage/code在两个方向上的运动:水平和竖直。水平方向上需要将code class=language-plaintext highlighter-rougeCarriage/code移到一行的起始位置,竖直方向上需要纸张向上移动一行,此时也就是相当于code class=language-plaintext highlighter-rougeCarriage/code下移了一行。(这在很多影视作品里面可以看到,打字者们打完一行之后,通常会用手拨动一个滑块,然后听到“咔”的一声,接着输入下一行。只是在这款打字机中不再需要人为的去拨动。)而这两个动作分别对应着:/p ul liCarriage Return(CR),也即回车,它在ASCII表中的值为0x0D,可以用转义符code class=language-plaintext highlighter-rouge\r/code表示/li liLine Feed(LF),也即换行,它在ASCII表中的值为0x0A,可以用转义符code class=language-plaintext highlighter-rouge\n/code表示/li /ul p因为打字机是机械的结构,所以虽然从逻辑上只表示为code class=language-plaintext highlighter-rougeEOF/code,但从设计上它需要分为两个独立的操作,这也正是我们习惯连起来说回车换行的原因。可以参照下图看看其键盘的布局:/p pimg src=http://upload.wikimedia.org/wikipedia/commons/e/ea/Mappa_Teletype_ASR-33.jpg alt=键盘布局 //p p键盘的右方有一个code class=language-plaintext highlighter-rougeLine Feed/code和code class=language-plaintext highlighter-rougeReturn/code,从名字可以看出,这分别对应着前面提到的两个操作。然而,通常一个回车操作不能够在一个字符打印的时间内完成,所以可以利用code class=language-plaintext highlighter-rougeCarriage/code移动的时间,去完成另外一个完全独立的操作code class=language-plaintext highlighter-rougeLine Feed/code,这也是通常code class=language-plaintext highlighter-rougeCarriage Return/code会被放在code class=language-plaintext highlighter-rougeLine Feed/code前面的原因。你可以想象,如果在在code class=language-plaintext highlighter-rougeCarriage/code和纸移动的过程中按下了其它的字符键,打印的内容将变得十分混乱。所以在code class=language-plaintext highlighter-rougeCarriage Return/code和code class=language-plaintext highlighter-rougeLine Feed/code之后,有时会有1~3个NUL字符(即相当于汇编语言中的空指令,仅起占位作用),以等待前两个操作的完成。所以实际上打字机的code class=language-plaintext highlighter-rougeEOL/code为:code class=language-plaintext highlighter-rougeEOL = CR + LF + 1~3NUL/code。/p h3 id=分歧出现分歧出现/h3 p等到早期的计算机发明时,很自然的这两个概念被拿了过来。但是由于那时的存储设备非常昂贵,一些人认为在每行的结尾加两个字符用于换行,实在是极大的浪费,于是各个厂商在这一点上便出现了分歧。/p p由于一些早期的微型计算机还没有用于隐藏底层硬件细节的设备驱动,所以它们直接沿用了打字机的惯例,使用不带NUL的code class=language-plaintext highlighter-rougeCRLF/code作为一个code class=language-plaintext highlighter-rougeEOL/code。而CP/M为了和这些微型计算机使用同一个终端,也采用了这种设计。所以它的克隆MS-DOS也同样使用code class=language-plaintext highlighter-rougeCRLF/code,由于Windows又是基于MS-DOS,为保持兼容性,所以就导致了如今的Windows是采用code class=language-plaintext highlighter-rougeCRLF/code作为code class=language-plaintext highlighter-rougeEOL/code,即code class=language-plaintext highlighter-rouge\r\n/code(或code class=language-plaintext highlighter-rouge0x0D 0x0A/code)。/p p而Multics在被设计之时就非常认真的考虑了这一问题,设计者们觉得只需一个字符便完全足够来表示code class=language-plaintext highlighter-rougeEOL/code,这样更加合理。那么选择code class=language-plaintext highlighter-rougeCR/code还是code class=language-plaintext highlighter-rougeLF/code呢?本来由于那时的键盘上都有一个code class=language-plaintext highlighter-rougeReturn/code键,所以可能更好的选择是code class=language-plaintext highlighter-rougeCR/code。但当时考虑到code class=language-plaintext highlighter-rougeCR/code可以用来重写一行,以完成如strong粗体/strong和s删除线/s等效果,所以他们选择了稍稍难以理解的code class=language-plaintext highlighter-rougeLF/code。然后自己设计了一个设备驱动程序来将code class=language-plaintext highlighter-rougeLF/code转换为各种打字机所需要的code class=language-plaintext highlighter-rougeEOL/code,这个方案非常完美,当然除了code class=language-plaintext highlighter-rougeLF/code稍微奇怪一些。随后一脉相承的Unix和Linux们都继承了这个选择,于是你在这些操作系统上可以发现每一行的结尾是一个code class=language-plaintext highlighter-rougeLF/code,即code class=language-plaintext highlighter-rouge\n/code(或code class=language-plaintext highlighter-rouge0x0A/code)。/p pMac系统的选择就更加复杂一些。Apple在设计Mac OS时,他们采用了一个最容易理解的选择:code class=language-plaintext highlighter-rougeCR/code,即code class=language-plaintext highlighter-rouge\r/code(或code class=language-plaintext highlighter-rouge0x0D/code)。但这只维持到Mac OS 9,后一个版本的Mac OSX基于Mach-BSD内核,所以此后版本的Mac OSX在每行的结尾存储了与Linux一样的code class=language-plaintext highlighter-rougeLF/code,即code class=language-plaintext highlighter-rouge\n/code(或code class=language-plaintext highlighter-rouge0x0A/code)。/p h3 id=混乱的状况混乱的状况/h3 p还有很多其它的操作系统a href=http://en.wikipedia.org/wiki/Newline#Representations采用更加不同的方案/a,这也导致了混乱的产生,文章开始提出的几个问题便由该混乱引起。因为Linux和Mac OSX上使用的是code class=language-plaintext highlighter-rougeLF/code,而Windows上使用的是code class=language-plaintext highlighter-rougeCRLF/code,那么Linux和Mac OSX上创建的文件在Windows上打开时,由于每一行的结尾只有一个code class=language-plaintext highlighter-rougeLF/code,但Windows只认识code class=language-plaintext highlighter-rougeCRLF/code,所以便不会有逻辑上的换行处理,故所有的文字被挤到了一行。反过来,如果Windows上的文件在Linux和Mac OSX上打开时,仅需code class=language-plaintext highlighter-rougeLF/code便可换行,那么每一行的结尾便多了一个code class=language-plaintext highlighter-rougeCR/code,对应的ASCII码为code class=language-plaintext highlighter-rouge^M/code。/p p而git的安装向导会特意有一个这样的提醒页面也出于此,因为一个项目可能有多个开发者,每个开发者可能使用的是不同的系统,那么开发者checkout代码时,如果不做换行符的转换,有可能就会出现只有一行或者行尾多了code class=language-plaintext highlighter-rouge^M/code的情况。当然,如果你有一个可以识别多种code class=language-plaintext highlighter-rougeEOL/code的a href=/blog/2014/intro-to-vim/现代文本编辑器/a,那么不做转换也无妨(notepad不行)。/p p如果出现了上面的转换问题时,也别着急,可以a href=http://en.wikipedia.org/wiki/Newline#Conversion_utilities对文件进行转换/a。那在我们写程序时如何正确的处理这些问题?像隐藏硬件细节的驱动程序一样,我们可寄希望于高级语言。/p h2 id=统一统一/h2 p为了避免在这些不同的实现中挣扎,高级语言给我们带来了福音,它们各自使用了a href=http://en.wikipedia.org/wiki/Newline#In_programming_languages统一/a的方式来处理code class=language-plaintext highlighter-rougeEOL/code。在C语言中,你一定知道在字符串中如果要增加一个换行符的话,直接用code class=language-plaintext highlighter-rouge\n/code即可,比如:/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=nprintf/spanspan class=p(/spanspan class=sThis is the first line! /spanspan class=se\n/spanspan class=sThis is a new line!/spanspan class=p);/span/code/pre/figure p上面的输出将是:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcodeThis is the first line! This is a new line! /code/pre/div/div p为什么C语言选择了code class=language-plaintext highlighter-rouge\n/code而不是code class=language-plaintext highlighter-rouge\r/code?这绝非偶然。熟悉C语言历史的朋友可能知道当初C语言是Dennis Ritchie为开发Unix而设计,所以它沿用了Unix上code class=language-plaintext highlighter-rougeEOL/code的惯例便很容易理解了。而我们知道Unix使用的code class=language-plaintext highlighter-rougeLF/code的ASCII码为code class=language-plaintext highlighter-rouge0x0A/code,转义符为code class=language-plaintext highlighter-rouge\n/code,因此C语言中也使用code class=language-plaintext highlighter-rouge\n/code作为换行。/p h2 id=text-mode-vs-binary-modeText Mode VS Binary Mode/h2 p但是,千万别简单的认为上面的code class=language-plaintext highlighter-rouge\n/code最终写到文件中就一定是其ASCII码code class=language-plaintext highlighter-rouge0x0A/code,或者文件中的code class=language-plaintext highlighter-rouge0x0A/code被读到内存中就是其转义符code class=language-plaintext highlighter-rouge\n/code。这取决于你打开文件的方式。在C语言中,在对文件进行读取操作之前,都需要先打开文件,可以使用下面的函数:/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=cp#inlcude lt;stdio.hgt; /spanspan class=ktFILE/span span class=o*/spanspan class=nffopen/spanspan class=p(/spanspan class=kconst/span span class=ktchar/span span class=o*/spanspan class=npath/spanspan class=p,/span span class=kconst/span span class=ktchar/span span class=o*/spanspan class=nmode/spanspan class=p);/span/code/pre/figure p注意看第二个参数code class=language-plaintext highlighter-rougemode/code,它是一个字符指针,通常可以为读(r),写(w),追加(a)或者读写(r+, w+, a+),仅指定这些参数时,文件将被当成是文本文件来操作,即code class=language-plaintext highlighter-rougeText Mode/code,而如果在这些参数之外再指定一个额外的code class=language-plaintext highlighter-rougeb/code时,文件便会被当成是二进制文件,即code class=language-plaintext highlighter-rougeBinary Mode/code。这两种模式的区别在哪里呢?这里稍稍有些复杂,因为它们在不同的平台上表现不同。/p h3 id=windows平台Windows平台/h3 p对于Windows平台,因为其使用code class=language-plaintext highlighter-rougeCRLF/code来表示code class=language-plaintext highlighter-rougeEOL/code,故对于code class=language-plaintext highlighter-rougeText Mode/code需要做一定的转换才能够与C语言保持一致。接下来的两个图可以给出最为直观的描述。/p p先看二者对于读操作的区别:/p pimg src=/img/posts/eol-read.png alt=读操作 //p pcode class=language-plaintext highlighter-rougeText Mode/code下,C语言会尝试去“理解”这些回车与换行,它会知道code class=language-plaintext highlighter-rougeLF/code和code class=language-plaintext highlighter-rougeCRLF/code都可能是code class=language-plaintext highlighter-rougeEOL/code,所以不管文件中是code class=language-plaintext highlighter-rougeLF/code还是code class=language-plaintext highlighter-rougeCRLF/code,被读进内存时都会变成code class=language-plaintext highlighter-rougeLF/code。而code class=language-plaintext highlighter-rougeBinary Mode/code下,C语言不会做任何的“理解”,所以这些字符在文件中什么样,读到内存中依然那样。/p p接下来是写操作的区别:/p pimg src=/img/posts/eol-write.png alt=写操作 //p pcode class=language-plaintext highlighter-rougeText Mode/code下,内存中的每一个code class=language-plaintext highlighter-rougeLF/code写入文件中时都会变为code class=language-plaintext highlighter-rougeCRLF/code,当然,如果不幸内存中为code class=language-plaintext highlighter-rougeCRLF/code,以此种模式写入到文件中时就会变成code class=language-plaintext highlighter-rougeCRCRLF/code(strong注意/strong:这里不是code class=language-plaintext highlighter-rougeCRLF/code。原因我想大概是如果你认为内存中的数据是文本,那么它一定是以code class=language-plaintext highlighter-rougeLF/code作为code class=language-plaintext highlighter-rougeEOL/code,code class=language-plaintext highlighter-rougeCR/code也一定是你有意而为之,是个有意义的字符,所以它并不会处理。)。而code class=language-plaintext highlighter-rougeBinary Mode/code下,内存中的内容会被原封不动的写到文件中。/p p所以为了保证一致性,一定需要注意配套使用读和写,即strong读和写采用同一种模式打开文件/strong。/p h3 id=linux和mac-osx平台Linux和Mac OSX平台/h3 p因为Linux和Mac OSX平台与C语言对待code class=language-plaintext highlighter-rougeEOL/code的方式完全一致,所以code class=language-plaintext highlighter-rougeText Mode/code和code class=language-plaintext highlighter-rougeBinary Mode/code在这些平台下没有任何区别,可以参考code class=language-plaintext highlighter-rougefopen/code的a href=http://man7.org/linux/man-pages/man3/fopen.3.htmlman page/a。实际上,所有遵循POSIX的平台都忽略了code class=language-plaintext highlighter-rougeb/code这个参数。/p p虽说在这些平台上处理code class=language-plaintext highlighter-rougeEOL/code非常简单,但是如果你的程序需要移植到其它非POSIX平台上时,请务必正确对待code class=language-plaintext highlighter-rougeb/code参数。/p h2 id=更多资料更多资料/h2 p如果还有兴趣,可以看看下面这些有趣的资料:/p ul li阮一峰的a href=http://www.ruanyifeng.com/blog/2006/04/post_213.html《回车与换行》/a/li lia href=http://www.oualline.com/practical.programmer/eol.htmlThe End of Line Puzzle/a,也即上面那篇文章的出处/li li关于a href=http://www.quora.com/What-is-the-ASCII-code-for-newline-characterWhat is the ASCII code for newline character?/a的一个回答/li li维基百科上关于a href=http://en.wikipedia.org/wiki/NewlineNewline/a的解释/li li从网络的角度讲述了a href=http://www.rfc-editor.org/EOLstory.txtEnd-of-Line的故事/a/li li打字机的一段a href=https://www.youtube.com/watch?v=LJvGiU_UyEQ视频/a,需梯子/li /ul h2 id=结尾结尾/h2 p这样一个小小的code class=language-plaintext highlighter-rougeEOL/code便如此复杂,给人们带来了极大的困扰,但就如我在a href=/blog/2014/the-origin-and-usage-of-typename/知无涯之C++ typename的起源与用法/a最后讨论过的一样,这个决定是经历过无数决断、波折与妥协才有了现在的结果。你可以选择保守,为向后兼容而作出妥协,那么你得面对不断累加的“不完美”,甚至“丑陋”的设计;你也可大胆尝试,破旧立新,牺牲向后兼容换取进步,那你也许得忍受人们的“唾骂”,或许还需承担被人们抛弃的风险。如何在这之间作出选择,没有明确的答案,恐怕一切就只有靠自己去判断了吧。/p p(全文完)/p pfeihu/p p2014.12.17 于 Shenzhen/p

NBA 38大催泪瞬间:假如

作者 feihu
2014年7月18日 00:00
img src=https://feihu.me/img/post-banners/what-if.png alt= width=600 /br / p此篇文章无关乎技术!献给热爱篮球的朋友!/p h2 id=假如假如/h2 p前段日子逛a href=http://hoopchina.com虎扑/a时看到一帖子,标题叫a href=http://bbs.hupu.com/9885362.html假如/a,进去之后看到一组NBA老照片。刚开始感到有些莫名其妙,为何放一大堆图在这里,和标题strong假如/strong有什么关系?第一印象判定其为哗众取宠的帖子,可再往下来看到楼主自己的回复,他贴了信乐团的《假如》和歌词,但为何一句话没有,只贴了这些?难道和前面的照片有关联?怀着好奇,打算再看一遍。于是戴上耳机,点击播放,很自然随着节奏一句歌词对应着前面的一张图片。一瞬间,原本看似无厘头的每张图好似立刻被赋予了生命,伴着音乐述说着各自背后的故事,而每句歌词是点睛之笔,恰到好处的给了每段故事一个最好的诠释。一遍看下来,无数尘封的记忆被唤醒,不知不觉泪水模糊了眼眶。/p p看完后心情久久无法平复,关注篮球十余年,每张照片都可以勾起一段回忆。但可惜的是楼主没有做成视频,如果像我刚开始一样,点进来看之后发现一堆乱七八糟的照片,就判定为水贴,错过了该有多可惜,我相信一定有很多这样的人。这么好的内容应该以更好的形式展示出来,而并非手动的去拖动页面,播放音乐,竖起耳朵聆听歌词,它们应该是作为一个整体出现,而可以展示这一整体的最好方式便是视频。于是下定决心,花了两天时间将它制作出来,放上a href=http://bbs.hupu.com/9936220.html虎扑/a。除增加了前奏时96黄金一代合影的照片外,所有的内容均来自原贴。/p p这里便是最终的成品:/p iframe frameborder=0 style=width:480px;height:400px; src=https://v.qq.com/iframe/player.html?vid=k013234astcamp;tiny=0amp;auto=0 allowfullscreen=/iframe h2 id=碎碎念碎碎念/h2 p帖子发布后的前几天反应很平淡,并未达到预期的效果,虽有不甘,但也慢慢的被淡忘。没想到三天之后,老弟突然发了张截图给我:/p pimg src=/img/posts/what-if-hoop-homepage.jpeg alt=登上虎扑首页 //p p帖子竟然意外登上虎扑首页,令我有些错谔,因为来得太突然,毫无征兆。首页的帖子改了标题,甚至一度我都不敢确信是同一个,直到看见虎扑的消息量陡增之后才得以接受。/p h3 id=关于平台关于平台/h3 p一瞬间帖子的浏览量涨了strong一百倍/strong,难以想象,第一次让我切身的感受到平台的强大力量。人们常说,是金子在哪里都可以发光,这话到底是不是一种自我安慰的阿Q心态,我不得而知。但是,在这样一个信息爆炸的年代,即使再好的东西,如果位于一个不起眼的位置,受不到足够的关注,那么很容易被淹没在信息汪洋之中,最后为人们所忽略。而一旦给了它一个展示自己的平台,受到足够的关注,它便可以最大限度的展现自己,绽放光芒。/p p最好的例子便是《Flappy Bird》,这样一个火爆全球的游戏,其实早在去年5月就上架苹果App Store,一开始它只是无数默默无名的游戏中的一员,被人遗忘在某个偏远的角落里。沉寂半年之后,去年11月17日,著名宅男论坛Reddit里的一名用户在一个“死了又死的iOS自虐游戏名单”里提到了它并推荐出来,让它莫名其妙暴得大名。一个多月后,Flappy Bird在一小群游戏爱好者的关注中下载量开始上升。一直到一月底、二月初,它在iOS上上架的150多个国家中登顶了140个国家的免费下载榜榜首,这是何等让人咂舌的成绩。游戏最后能够流行起来可能有一些大家跟风的心理,但可以肯定的是它绝对非常优秀,才能获得如此口碑。但同样的游戏,为何前后有如此大的反差,如果那名用户没有在论坛中推荐,那是否到现在它仍然默默的躺在App Store某个不起眼的地方?又或者如果当时不是在Reddit上推荐,而是另外一个关注度没有那么高的论坛中,是否又会是另外一番景象?可以肯定的说,正是由于它意外的出现在关注度极高的平台中,才没有泯然众人。/p p这又好似新闻头条,不知多少人和我一样,很多新闻都只是简单的浏览一遍首页的几条。如果新闻不能够登上头条的话,无法获得足够的关注度,很快便会被人们所忽略。现在可以理解明星们不择手段为了登上头条来增加自己的曝光度,同时对那些位于背后,又甘于默默工作的幕后工作者们充满了敬意。/p p比较幸运的是,我们处于一个比较开放的年代,又有了人类历史上迄今为止最有影响力的发明之一——互联网,让每个人都有权利,有机会向更多的人展现自己,于是各种以此目的开发的产品涌现出来,BBS,博客,贴吧,微博等等,这些都是平台。在体验到它们便利的同时,这也带来了另外一个问题,平台太多,关注点分散,另外人们接受信息量越来越大,导致即使是再热门的东西过不了多久便会渐渐消退,淡出人们视野。很多极好的东西很可惜的被淹没在无尽的信息之中,有些甚至完全没有被关注过。假如…可惜没有假如。我能够想象到它们的创造者内心的不甘与无奈,也为它们感到深深的惋惜。这也是我喜欢博客而不喜欢微博的一个原因,我希望用心写的文章可以有一个属于它们自己的平台,成为这个平台的头条,即使只有我一个观众。/p h3 id=关于展示方式关于展示方式/h3 p这篇帖子其实和原贴没有任何区别,只是对原贴的一个再加工,但是仅从我个人而言,视频这种形式带给我的震撼更大。人天生是一种懒惰的动物,有时会排斥一切麻烦的东西。比如说一本小说,和一部以小说改编的电影,以内容而言,电影不可能会比小说更出色,但很多时候都是电影带给人的震撼更强烈,从而它能够更好的带动小说的销量。究其原因,我想大概是小说只带给人们想象上的体验,但是花费的精力却很巨大,需要眼睛不停的盯着书本,配合大脑发挥自己的想象对看到的内容进行加工。而电影就简单的多,它能够同时给人以视觉,听觉上的冲击,将原来需要大脑加工的内容具象化,直接呈现给观影者。并且音乐和画面相对于文字可以更容易的调动人们的情绪,让人们更轻松的享受整个过程,与导演产生共鸣,从而被更多的人接受。/p p但是电影却比小说更容易受到争议,剧本改编有差距是一方面,另外更重要的一点,也许是电影的这种具象化无法满足每个读者的期望吧,剥夺了有些人想象的乐趣,剥夺了人们去创造自己心中的哈姆雷特的权利。/p h3 id=关于照片关于照片/h3 p照片是种神奇的表现形式,它只记录一瞬间,不加任何修饰,但却和很多艺术一样,可以给你带来无尽的想象空间。面对一张照片时,可能每个人的感受都不一样,即使是同一人,在不同时刻的感受也会存在不同。但正因如此,才使得它更加鲜活,更具生命力。我很喜欢一些开放式结局的小说,电影,比如金庸的《雪山飞狐》,诺兰的《盗梦空间》,《禁闭岛》和李安的《少年派的奇幻漂流》。他们为故事设定一个开放式的结局,不点破,为读者和观众留点想象的余地。这样安排需要勇气,但正是这样一种巧妙的安排,恰到好处,可以让读者,观众们尽情的发挥自己的想象,每个人都能找到自己喜爱的方式来解读,获得自己希望的结局,从而使它们被赋予了更强的生命力。/p p文章开篇的图片便是视频中的一部分,如果没有配文字,看到这张照片时,第一反应可能是天神下凡般的罗伊。可是,一旦加了歌词,意思便截然不同。2006年加入联盟,一出道便获得最佳新秀,随后成为球队最关键的杀手。作为曾经最好的得分后卫之一,他享有黄曼巴的美誉,能力几乎与科比相当,被人当作科比的接班人。铁血的他在2009-2010赛季,全队大部分主力受伤的情况下,带领开拓者力克各路豪强,让开拓者的玫瑰花园球馆成为令人闻风丧胆的魔鬼主场。作为湖人球迷永远都记得那段时间,湖人像中了魔咒一般在这个球馆尝到9连败。火箭球迷也一定记得罗伊的0.8秒一剑封喉。这样一个神一般的男人,就在所有人对他的未来充满无限期待之时,谁能想到2011-2012赛季开赛之前,他因为左右膝盖双双摘除半月板,而不得不宣布退役,那时才年仅26岁。一靠近天堂,也就快醒了,故事还未开始便已结束。视频中最让我痛心的便是这里。假如他还在,假如奥登依然健康,他们是否会像当年的OK组合一样,大杀四方,统治联盟?但这一切都是假如罢了。/p h2 id=结尾结尾/h2 p最后以伴随着帖子一起发布的小段文字作为结尾:/p blockquote p这些球星们陪伴着我们成长,而现在他们或老去或离开,或载着荣耀或带着遗憾,一起带走的还有我们关于青春的记忆。看完帖子之后想起《平凡的世界》中孙少平念的一句诗,非常符合此刻的心情:“金黄的落叶铺满我的心间,我已再不是青春少年。” 谨以此片缅怀那段难忘的青春岁月。/p /blockquote h2 id=附录附录/h2 p附上《假如》的歌词:/p blockquote p一份爱能承受多少的误解br / 熬过飘雪的冬天br / 一句话能撕裂多深的牵连br / 变的比陌生人还遥远br / 最初的爱越像火焰br / 最后越会被风熄灭br / 有时候真话太尖锐br / 有人只好说著谎言br / 假如时光到流 我能做什么br / 找你没说的 却想要的br / 假如我不放手 你多年以后br / 会怪我恨我 或感动br / 想假如 是最空虚的痛br / 一个人要看过 几次爱凋谢br / 才甘心在孤独里冬眠br / 最初的爱越像火焰br / 最后越会被风熄灭br / 有时候真话太尖锐br / 有人只好说著谎言br / 假如时光到流 我能做什么br / 找你没说的 却想要的br / 假如我不放手 你多年以后br / 会怪我恨我 或感动br / 想假如 是最空虚的痛br / 为什么幸福 都是幻梦br / 一靠近天堂 也就快醒了br / 或许爱情 更像落叶br / 看似飞翔却在坠落br / 假如真可以让时光到流 你会做什么br / 一样选择我 或不抱我br / 假如温柔放手 你是否懂得br / 走错了可以 再回头br / 想假如 是无力的寂寞br //p /blockquote p(全文完)/p pfeihu/p p2014.07.18 于 Shenzhen/p

深入理解log机制

作者 feihu
2014年4月7日 00:00
img src=https://feihu.me/img/post-banners/log-insight.gif alt= width=600 /br / p最近在部门内部做了一个关于log机制的知识分享,深入的探讨了log机制中各种概念的来源、常用log库的用法、内部处理流程,以及如何在一个涉及多台主机的复杂系统中部署log等问题。本文是对这次分享的总结,将对这些问题一一展开介绍。/p h3 class=no_toc id=目录目录/h3 ul id=markdown-toc lia href=#开场 id=markdown-toc-开场开场/a/li lia href=#勿在浮沙筑高台 id=markdown-toc-勿在浮沙筑高台勿在浮沙筑高台/a ul lia href=#最简单的log id=markdown-toc-最简单的log最简单的log/a/li lia href=#增加有用信息 id=markdown-toc-增加有用信息增加有用信息/a/li lia href=#简化调用封装 id=markdown-toc-简化调用封装简化调用:封装/a/li lia href=#设定等级tracelevel id=markdown-toc-设定等级tracelevel设定等级:TraceLevel/a/li lia href=#多一些控制marker id=markdown-toc-多一些控制marker多一些控制:Marker/a/li lia href=#改变目的地appender id=markdown-toc-改变目的地appender改变目的地:Appender/a/li lia href=#模块独立控制category id=markdown-toc-模块独立控制category模块独立控制:Category/a/li lia href=#配置文件 id=markdown-toc-配置文件配置文件/a/li /ul /li lia href=#log库常见用法 id=markdown-toc-log库常见用法log库常见用法/a/li lia href=#配置 id=markdown-toc-配置配置/a ul lia href=#tracelevel id=markdown-toc-tracelevelTraceLevel/a/li lia href=#marker id=markdown-toc-markerMarker/a/li lia href=#appender id=markdown-toc-appenderAppender/a/li lia href=#formatter id=markdown-toc-formatterFormatter/a/li lia href=#category id=markdown-toc-categoryCategory/a/li /ul /li lia href=#处理流程 id=markdown-toc-处理流程处理流程/a/li lia href=#log在系统中的部署 id=markdown-toc-log在系统中的部署log在系统中的部署/a/li lia href=#尾声 id=markdown-toc-尾声尾声/a/li /ul hr / h2 id=开场开场/h2 plog如今已经成为了我们日常开发时所必不可少的工具,它同debug一起构成了开发者手中分析问题最有力的两个武器。两者各有优劣,相辅相成,配合起来使用将变得无往不利。通常相比于debug来说,log在很大程度上可以更方便、更迅速的让开发者分析程序的问题,尤其是对于非常庞大的系统、或者已经发布的程序,又或者一些非必现的问题,当我们无法方便的debug问题程序时,log文件可以提供非常多有用的信息,如果开发者log写得比较合适,大多数情况下根据log就可以分析出问题所在。因此,log分析法深受开发者的喜爱。/p p记得初学编程,第一次听到这样一个观点时那种难以接受心情,怎么可能还有比debug更加容易分析程序问题的方法?好一个无知无畏!当然这一切都是源于当时写的程序规模都比较小,非常适合debug的缘故吧。而实际上当时在不知不觉中已经或多或少使用了简单的log,那一条条控制台的cout与printf就是最好的证明。后来随着程序规模越来越大,才明白debug的局限性,逐渐的喜欢上了log。/p h2 id=勿在浮沙筑高台勿在浮沙筑高台/h2 p现如今对于每一种开发语言都有非常多的库来帮我们处理log,比如:log4j(log for Java),log4cpp(log for C++),log4net(log for .NET)等等。最早处理log的库是a href=http://logging.apache.org/log4j/Log4j/a,它是Apache为Java发布的一个开源log库,后来基于这个库衍生了很多具有相似API的库。我们这里介绍的库是基于a href=http://log4cpp.sourceforge.net/log4cpp/a发展而来。/p pem后面就用stronglog4me/strong作为我们使用的库的名称/em/p p让我们先从无到有,从一个个简单的使用场景一步一步分析log库中各种概念如何发展而来。当然,我没有去真正追究它的历史,只是从个人需求角度分析得来。/p h3 id=最简单的log最简单的log/h3 p代码中经常会需要打印一些提示信息用于显示程序工作流程,或者反馈错误信息,这就是所谓的log,就像船员的航海日志一样,我想code class=language-plaintext highlighter-rougelog/code也是由此得名吧。为了输出这些信息,在C/C++中最简单的方法是用code class=language-plaintext highlighter-rougeprintf/code或者code class=language-plaintext highlighter-rougestd::cout/code:/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=c1// I want to print a log: /span span class=nprintf/spanspan class=p(/spanspan class=sIm a message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span/code/pre/figure h3 id=增加有用信息增加有用信息/h3 p我们本可在每处需要打印log信息时都采用这种方式,但不妨先停下来试想一下,如果在一个log文件中你看到满屏幕的这种信息,但是却无法知道是谁,在什么时候,什么位置输出这条信息,那这种log的价值便大大折扣。于是,你会需要在每条log中增加一些额外有用的信息:/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=c1// I want to add more information: /span span class=nprintf/spanspan class=p(/spanspan class=s%s %s %d: Im a message/spanspan class=se\n/spanspan class=s/spanspan class=p,/span span class=ntime/spanspan class=p,/span span class=n__FILE__/spanspan class=p,/span span class=n__LINE__/spanspan class=p);/span/code/pre/figure p这样,每条log就有了时间,文件和行号这些额外有用的信息,非常有利于分析问题。/p h3 id=简化调用封装简化调用:封装/h3 p但是,这样会不会太麻烦?每次在写代码时,打印一条简单的log你需要加这么多无关的内容,万一忘了怎么办,这简直无法接受。你想要把所有的注意力都放在log本身上,不想关注其它的细技末节,怎么办?注意看,上面的函数调用中,后三个参数都是固定的,于是你可以对它进行这样简单的封装:/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=c1// Too complicated: /span span class=cp#define printf0(message) \ /span span class=nprintf/spanspan class=p(/spanspan class=s%s %s %d %s/spanspan class=p,/span span class=ntime/spanspan class=p,/span span class=n__FILE__/spanspan class=p,/span span class=n__LINE__/spanspan class=p,/span span class=nmessage/spanspan class=p);/span span class=nprintf0/spanspan class=p(/spanspan class=sIm a message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span/code/pre/figure p注:这里用宏而不采用函数,正如评价中@weitang指出的,如果是函数的话,__LINE__的值会一直是函数中的行号,是一个固定值,而不是调用处的行号。另外,这个版本的宏只支持一个参数,后面调用它的其它函数中传了可能不止一个参数,是为了演示方便。各位有兴趣的话可以自行写出合适的printf0版本。/p p还是一样简单的调用,不需要你再去输入一些无关的内容,因为这个封装的函数已经替你做好了。/p h3 id=设定等级tracelevel设定等级:TraceLevel/h3 plog信息并不是千篇一律只起一种作用,有的是纪录程序的流程,有的是错误信息,还有一些是警告信息。为了让log更有可读性,你可能想要把不同的信息区分开来,比如这样:/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=c1// I want to distinguish different kinds of message: /span span class=nprintf0/spanspan class=p(/spanspan class=sNormal: Im a normal message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span span class=nprintf0/spanspan class=p(/spanspan class=sWarning: Im a warning message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span span class=nprintf0/spanspan class=p(/spanspan class=sError: Im an error message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span/code/pre/figure p那么,你就可以通过在log文件中搜索Normal、Warning或者Error这些关键字就能够找到特定的log。这对于排错帮助非常大,比如你只需要搜索Error关键字就能够得出程序的出错信息。/p p但是,这些Normal、Warning以及Error关键字需要你每次都加在要输出的字符串中,同前面一样,你还是只想关注log本身,不愿意log和其它的信息混在一起。于是可以这样做:/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=c1// Its too complicated, I want something like this: /span span class=kenum/span span class=nTraceLevel/span span class=p{/span span class=nNormal/spanspan class=p,/span span class=nWarning/spanspan class=p,/span span class=nError/span span class=p};/span span class=ktvoid/span span class=nprintf1/spanspan class=p(/spanspan class=nTraceLevel/span span class=nlevel/spanspan class=p,/span span class=kconst/span span class=ktchar/span span class=o*/spanspan class=nmessage/spanspan class=p)/span span class=p{/span span class=ktchar/span span class=o*/spanspan class=nlevelString/spanspan class=p[]/span span class=o=/span span class=p{/span span class=sNormal: /spanspan class=p,/span span class=sWarning: /spanspan class=p,/span span class=sError: /span span class=p}/span span class=nprintf0/spanspan class=p(/spanspan class=s%s %s/spanspan class=p,/span span class=nmessage/spanspan class=p,/span span class=nlevelString/spanspan class=p[/spanspan class=nlevel/spanspan class=p]);/span span class=p}/span span class=nprintf1/spanspan class=p(/spanspan class=nNormal/spanspan class=p,/span span class=sIm a normal message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span span class=nprintf1/spanspan class=p(/spanspan class=nWarning/spanspan class=p,/span span class=sIm a warning message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span span class=nprintf1/spanspan class=p(/spanspan class=nError/spanspan class=p,/span span class=sIm an error message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span/code/pre/figure p现在你只需要指定一种log类型,就可以全心全意的处理log信息本身了。我们把上面的Normal, Warning和Error叫做code class=language-plaintext highlighter-rougeTraceLevel/code,故名思义,它表示log的等级。/p p可以进一步简化:/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=c1// To be more convenient: /span span class=ktvoid/span span class=nfprintf_out/spanspan class=p(/spanspan class=kconst/span span class=ktchar/span span class=o*/spanspan class=nmessage/spanspan class=p)/span span class=p{/span span class=nprintf1/spanspan class=p(/spanspan class=nNormal/spanspan class=p,/span span class=nmessage/spanspan class=p);/span span class=p}/span span class=ktvoid/span span class=nprintf_warn/spanspan class=p(/spanspan class=kconst/span span class=ktchar/span span class=o*/spanspan class=nmessage/spanspan class=p)/span span class=p{/span span class=nprintf1/spanspan class=p(/spanspan class=nWarning/spanspan class=p,/span span class=nmessage/spanspan class=p);/span span class=p}/span span class=ktvoid/span span class=nprintf_error/spanspan class=p(/spanspan class=kconst/span span class=ktchar/span span class=o*/spanspan class=nmessage/spanspan class=p)/span span class=p{/span span class=nprintf1/spanspan class=p(/spanspan class=nError/spanspan class=p,/span span class=nmessage/spanspan class=p);/span span class=p}/span span class=nprintf_out/spanspan class=p(/spanspan class=sIm a normal message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span span class=nprintf_warn/spanspan class=p(/spanspan class=sIm a warning message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span span class=nprintf_error/spanspan class=p(/spanspan class=sIm an error message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span/code/pre/figure p如此一来,对于特定等级的log只需调用各自的log输出函数即可,除此之外,注意力全部放在log信息本身上。/p p在代码中,通常最多的log是Normal类型,即显示程序流程。有时你可能只想log文件中存储Warning和Error类型的信息,Normal对你来相当于干扰信息,而且log文件也会因此变得很大。有时你又会想让log中包含所有类型。如何协调?如果可以动态的选择哪些等级的信息输出,那岂不是log文件就变得像是根据我的需求定制一般,可以随意控制log包含哪些级别的信息么?/p p根据这一思路,代码可以这样改变:/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=c1// I want to add a control which level should be printed: /span span class=nTraceLevel/span span class=nfgetLevel1/spanspan class=p();/span span class=ktvoid/span span class=nfprintf2/spanspan class=p(/spanspan class=nTraceLevel/span span class=nlevel/spanspan class=p,/span span class=kconst/span span class=ktchar/span span class=o*/spanspan class=nmessage/spanspan class=p)/span span class=p{/span span class=kif/span span class=p(/spanspan class=nlevel/span span class=ogt;=/span span class=ngetLevel1/spanspan class=p())/span span class=nprintf1/spanspan class=p(/spanspan class=nlevel/spanspan class=p,/span span class=nmessage/spanspan class=p);/span span class=p}/span span class=nprintf2/spanspan class=p(/spanspan class=nNormal/spanspan class=p,/span span class=sIm a normal message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span span class=nprintf2/spanspan class=p(/spanspan class=nWarning/spanspan class=p,/span span class=sIm a warning message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span span class=nprintf2/spanspan class=p(/spanspan class=nError/spanspan class=p,/span span class=sIm an error message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span/code/pre/figure pem这里暂时没有采用前面简化的方法。/em/p pcode class=language-plaintext highlighter-rougegetLevel1()/code从配置文件中读取当前允许的Level,代码中只有高于当前Level的log才会被输出,现在log文件便可以随着你的需要而定制了。/p h3 id=多一些控制marker多一些控制:Marker/h3 p再来考虑这样一种情况,如果你的文件非常大,中间要输出的Normal log非常多,分为不同层次,比如:粗略的流程,详细一些的,十分详细的。和很多命令的code class=language-plaintext highlighter-rouge-verbose/code参数一样。由于都是Normal类型的log,所以不能够用前面的code class=language-plaintext highlighter-rougeTraceLevel/code,这时需要引入另外一层控制:/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=c1// My class is too big, I want a filter to determine which /span span class=c1// logs should be generated /span span class=kconst/span span class=ktint/span span class=nSUB/span span class=o=/span span class=mi0/spanspan class=p;/span span class=kconst/span span class=ktint/span span class=nTRACE_1/span span class=o=/span span class=mi1/span span class=olt;lt;/span span class=mi0/spanspan class=p;/span span class=kconst/span span class=ktint/span span class=nTRACE_2/span span class=o=/span span class=mi1/span span class=olt;lt;/span span class=mi1/spanspan class=p;/span span class=kconst/span span class=ktint/span span class=nTRACE_3/span span class=o=/span span class=mi1/span span class=olt;lt;/span span class=mi2/spanspan class=p;/span span class=ktint/span span class=nfgetMarker1/spanspan class=p();/span span class=ktvoid/span span class=nfprintf3/spanspan class=p(/spanspan class=ktint/span span class=nmarker/spanspan class=p,/span span class=nTraceLevel/span span class=nlevel/spanspan class=p,/span span class=kconst/span span class=ktchar/span span class=o*/spanspan class=nmessage/spanspan class=p)/span span class=p{/span span class=kif/span span class=p(/spanspan class=nmarker/span span class=o==/span span class=mi0/span span class=o||/span span class=nmarker/span span class=oamp;/span span class=ngetMarker1/spanspan class=p()/span span class=o!=/span span class=mi0/spanspan class=p)/span span class=nprintf2/spanspan class=p(/spanspan class=nlevel/spanspan class=p,/span span class=nmessage/spanspan class=p);/span span class=p}/span span class=nprintf3/spanspan class=p(/spanspan class=nSUB/spanspan class=p,/span span class=nNormal/spanspan class=p,/span span class=sIm a normal message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span span class=nprintf3/spanspan class=p(/spanspan class=nTRACE_1/spanspan class=p,/span span class=nNormal/spanspan class=p,/span span class=sIm a normal message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span span class=nprintf3/spanspan class=p(/spanspan class=nTRACE_2/spanspan class=p,/span span class=nNormal/spanspan class=p,/span span class=sIm a normal message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span/code/pre/figure p这里提供了四级的控制,和前面的code class=language-plaintext highlighter-rougeTraceLevel/code一样,它也可以通过配置文件配置。假设现在配置的是code class=language-plaintext highlighter-rougeTRACE_1/code,那么代码中想要输出的三条信息中,只有前两条能够输出。这层控制我们称之为code class=language-plaintext highlighter-rougeMarker/code。/p p注意到这里定义的四级控制是可以通过位来操作的,能够任意组合。如果想要code class=language-plaintext highlighter-rougeTRACE_1/code和code class=language-plaintext highlighter-rougeTRACE_2/code都能够输出,那么只需要设置:/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=ktint/span span class=nmarker/span span class=o=/span span class=nTRACE_1/span span class=o|/span span class=nTRACE_2/spanspan class=p;/span span class=nprintf3/spanspan class=p(/spanspan class=nmarker/spanspan class=p,/span span class=nNormal/spanspan class=p,/span span class=sIm a normal message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span/code/pre/figure p如果marker设置为code class=language-plaintext highlighter-rougeSUB/code,则表明全部输出。通过增加这层控制后,log的订制变得更加灵活。/p h3 id=改变目的地appender改变目的地:Appender/h3 p到目前为止,所有的log都写到控制台。如果你想log写到文件中怎么办?如果不是控制台应用程序,比如,Win32或者MFC程序,log又该写到哪里去?也许你想到可以使用code class=language-plaintext highlighter-rougefwrite/code代替前面的code class=language-plaintext highlighter-rougeprintf/code,但是如果你想同时能够将log写到控制台,又写到文件中或者其它地方怎么办?/p p放弃这种硬编码的方法吧,你可以想到一种更加灵活,可以像前面code class=language-plaintext highlighter-rougeTraceLevel/code和code class=language-plaintext highlighter-rougeMarker/code一样容易配置的方法,能够更加优雅的控制log输出的目的地,但不需要硬编码在代码中,而是可以配置的。一起来看下面这段代码:/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=c1// I want my logs to go to console, files, eventlog /span span class=kclass/span span class=ncAppender/span span class=p{/span span class=ktvoid/span span class=nprintf/spanspan class=p(/spanspan class=nTraceLevel/span span class=nlevel/spanspan class=p,/span span class=kconst/span span class=ktchar/span span class=o*/spanspan class=nmessage/spanspan class=p)/span span class=o=/span span class=mi0/spanspan class=p;/span span class=p};/span span class=kclass/span span class=ncConsoleAppender/spanspan class=o:/span span class=kpublic/span span class=nAppender/span span class=p{/spanspan class=cm/* overwrite printf *//spanspan class=p};/span span class=kclass/span span class=ncFileAppender/spanspan class=o:/span span class=kpublic/span span class=nAppender/span span class=p{/spanspan class=cm/* overwrite printf *//spanspan class=p};/span span class=kclass/span span class=ncEventLogAppender/spanspan class=o:/span span class=kpublic/span span class=nAppender/span span class=p{/spanspan class=cm/* overwrite printf *//spanspan class=p};/span span class=nstd/spanspan class=o::/spanspan class=nvector/spanspan class=olt;/spanspan class=nAppender/span span class=o*gt;/span span class=oamp;/spanspan class=ngetAppenders/spanspan class=p();/span span class=ktvoid/span span class=nprintf4/spanspan class=p(/spanspan class=ktint/span span class=nmarker/spanspan class=p,/span span class=nTraceLevel/span span class=nlevel/spanspan class=p,/span span class=kconst/span span class=ktchar/span span class=o*/spanspan class=nmessage/spanspan class=p)/span span class=p{/span span class=kif/span span class=p(/spanspan class=nmarker/span span class=o==/span span class=mi0/span span class=o||/span span class=nmarker/span span class=oamp;/span span class=ngetMarker1/spanspan class=p()/span span class=o!=/span span class=mi0/spanspan class=p)/span span class=p{/span span class=kif/span span class=p(/spanspan class=nlevel/span span class=ogt;=/span span class=ngetLevel1/spanspan class=p())/span span class=p{/span span class=nstd/spanspan class=o::/spanspan class=nvector/spanspan class=olt;/spanspan class=nAppender/span span class=o*gt;::/spanspan class=niterator/span span class=nit/span span class=o=/span span class=ngetAppenders/spanspan class=p./spanspan class=nbegin/spanspan class=p();/span span class=kfor/span span class=p(;/span span class=nit/span span class=o!=/span span class=ngetAppenders/spanspan class=p./spanspan class=nend/spanspan class=p();/span span class=nit/spanspan class=o++/spanspan class=p)/span span class=p(/spanspan class=o*/spanspan class=nit/spanspan class=p)/spanspan class=o-gt;/spanspan class=nprintf/spanspan class=p(/spanspan class=nlevel/spanspan class=p,/span span class=nmessage/spanspan class=p);/span span class=p}/span span class=p}/span span class=nprintf4/spanspan class=p(/spanspan class=nSUB/spanspan class=p,/span span class=nNormal/spanspan class=p,/span span class=sIm a normal message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span span class=nprintf4/spanspan class=p(/spanspan class=nTRACE_1/spanspan class=p,/span span class=nNormal/spanspan class=p,/span span class=sIm a normal message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span span class=nprintf4/spanspan class=p(/spanspan class=nTRACE_2/spanspan class=p,/span span class=nNormal/spanspan class=p,/span span class=sIm a normal message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span/code/pre/figure p这里定义了一个叫做code class=language-plaintext highlighter-rougeAppender/code的基类,可以理解为处理log目的地的类,它有一个方法code class=language-plaintext highlighter-rougeprintf/code,对应着如何处理传给它的log。/p p接下来定义了三个子类,分别代表输出目的地为控制台、文件和Windows的EventLog。它们都覆写了基类的code class=language-plaintext highlighter-rougeprintf/code方法,按照各自的目的地处理log的流向,比如code class=language-plaintext highlighter-rougeConsoleAppender/code调用前面的code class=language-plaintext highlighter-rougeprintf2/code函数,而code class=language-plaintext highlighter-rougeFileAppender/code可能调用类似的code class=language-plaintext highlighter-rougefwrite/code。这样一来,只要我们为一个程序配置用哪些code class=language-plaintext highlighter-rougeAppender/code,log就可以根据这些配置交给对应的code class=language-plaintext highlighter-rougeAppender/code子类处理,从而无需在代码中硬编码。/p p这处理每一种目的地的类我们称之为code class=language-plaintext highlighter-rougeAppender/code。/p h3 id=模块独立控制category模块独立控制:Category/h3 p现在我们的log机制已经足够的完善。但是,随着程序规模越来越大,一个程序所包含的模块也越来越多,有时你并不想要一个全局的配置,而是需要每一个模块可以独立的进行配置,有了前面的介绍,这个需求就变得很简单了:/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=c1// There are too many components, I want different components /span span class=c1// could be configured separately /span span class=nTraceLevel/span span class=nfgetLevel2/spanspan class=p(/spanspan class=kconst/span span class=ktchar/span span class=o*/spanspan class=ncat/spanspan class=p);/span span class=ktint/span span class=nfgetMarker2/spanspan class=p(/spanspan class=kconst/span span class=ktchar/span span class=o*/spanspan class=ncat/spanspan class=p);/span span class=nstd/spanspan class=o::/spanspan class=nvector/spanspan class=olt;/spanspan class=nAppender/span span class=o*gt;/span span class=oamp;/spanspan class=ngetAppenders2/spanspan class=p(/spanspan class=kconst/span span class=ktchar/span span class=o*/spanspan class=ncat/spanspan class=p);/span span class=ktvoid/span span class=nfprintf5/spanspan class=p(/spanspan class=kconst/span span class=ktchar/span span class=o*/spanspan class=ncat/spanspan class=p,/span span class=ktint/span span class=nmarder/spanspan class=p,/span span class=nTraceLevel/span span class=nlevel/spanspan class=p,/span span class=kconst/span span class=ktchar/span span class=o*/spanspan class=nmessage/spanspan class=p)/span span class=p{/span span class=kif/span span class=p(/spanspan class=nmarker/span span class=o==/span span class=mi0/span span class=o||/span span class=nmarker/span span class=oamp;/span span class=ngetMarker2/spanspan class=p(/spanspan class=ncat/spanspan class=p)/span span class=o!=/span span class=mi0/spanspan class=p)/span span class=p{/span span class=kif/span span class=p(/spanspan class=nlevel/span span class=ogt;=/span span class=ngetLevel2/spanspan class=p(/spanspan class=ncat/spanspan class=p))/span span class=p{/span span class=nstd/spanspan class=o::/spanspan class=nvector/spanspan class=olt;/spanspan class=nAppender/span span class=o*gt;::/spanspan class=niterator/span span class=nit/span span class=o=/span span class=ngetAppenders/spanspan class=p(/spanspan class=ncat/spanspan class=p)./spanspan class=nbegin/spanspan class=p();/span span class=kfor/span span class=p(;/span span class=nit/span span class=o!=/span span class=ngetAppenders/spanspan class=p./spanspan class=nend/spanspan class=p(/spanspan class=ncat/spanspan class=p);/span span class=nit/spanspan class=o++/spanspan class=p)/span span class=p(/spanspan class=o*/spanspan class=nit/spanspan class=p)/spanspan class=o-gt;/spanspan class=nprintf/spanspan class=p(/spanspan class=nlevel/spanspan class=p,/span span class=nmessage/spanspan class=p);/span span class=p}/span span class=p}/span span class=nprintf5/spanspan class=p(/spanspan class=sLibrary1/spanspan class=p,/span span class=nSUB/spanspan class=p,/span span class=nNormal/spanspan class=p,/span span class=sIm a normal message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span span class=nprintf5/spanspan class=p(/spanspan class=sLibrary1/spanspan class=p,/span span class=nTRACE_1/spanspan class=p,/span span class=nNormal/spanspan class=p,/span span class=sIm a normal message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span span class=nprintf5/spanspan class=p(/spanspan class=sLibrary1/spanspan class=p,/span span class=nTRACE_2/spanspan class=p,/span span class=nNormal/spanspan class=p,/span span class=sIm a normal message/spanspan class=se\n/spanspan class=s/spanspan class=p);/span/code/pre/figure p对比前一节的代码,可以发现这里除了增加一个参数code class=language-plaintext highlighter-rougeconst char *cat/code以外,其它完全一样。但正是这个参数的出现,才让每一个模块可以独立的配置。这种模块间独立进行配置的方法我们称为code class=language-plaintext highlighter-rougeCategory/code。/p h3 id=配置文件配置文件/h3 p前面多次提到strong配置/strong,为了达到可以灵活配置的目的,通常会将这些配置保存成一个文件,比如code class=language-plaintext highlighter-rougelogConfig.ini/code:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcodeCategory: Library1 -gt; for Library1 category TraceLevel : Warning -gt; only Warning and Error messages are allowed Markers : TRACE_1 -gt; only TRACE_1 is allowed Appenders : ConsoleAppender -gt; write to console FileAppender: -gt; write to file filePath: C:\temp\log\trace_lib1.log Category: Library2 -gt; for Library2 category ... /code/pre/div/div p那么在什么时机读取这个配置文件?一般有这样几种方式:/p ul li程序启动时载入code class=language-plaintext highlighter-rougelogConfig.ini/code,如果配置不常改变时可以采用这种方式,最简单。/li li创建一个新线程,间隔一段时间检查code class=language-plaintext highlighter-rougelogConfig.ini/code是否已经改变,如果改变则重新读取。这种方法比较复杂,可能会影响效率,而且间隔的时间也不好设置。/li li处理每一个log之前先检测code class=language-plaintext highlighter-rougelogConfig.ini/code,如果有改变则重新读取。/li li最后一种方法结合了前两种方法的优点,还是在处理每个log之前检测,但不同的是再加上一个时间间隔,如果超过时间间隔才会真的去检测,而如果在间隔内,则直接忽略。这种方法更加高效且消耗资源最少。/li /ul p对于后面三种方式,每次配置文件有了更新之后,log输出几乎可以实时的作出应变。/p p至此,一个简单灵活的log原型建立了,虽然它还是非常简陋,但已经有了现代log库的雏形,包含了其中几个重要的概念。下面我将以我们所使用的log4me库进行分析。/p h2 id=log库常见用法log库常见用法/h2 p前面介绍的log雏形完全是小儿科式的代码,只是起一个演示作用,实际上我们无需重新发明轮子。如本文开始所介绍,已经有非常多专业的库来处理log,这些库以最简单的接口提供了最大化的log信息。我们这里采用的log4me库就有这样几个优点:/p ul li跨平台,在Windows和Linux上有着完全一样的接口与行为/li li更细的粒度来控制log/li li线程安全/li li高性能/li /ul p我们定义了下面几个宏,专门用于Library1下的log输出,这里会取配置中Library1这个Category的配置,分别输出不同TraceLevel的log。/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=cp#define LIB1_OUT(MESSAGE) LOG_OUT(Library1, DLL, Notice) lt;lt; MESSAGE #define LIB1_WARN(MESSAGE) LOG_OUT(Library1, DLL, Warn) lt;lt; MESSAGE #define LIB1_ERR(MESSAGE) LOG_OUT(Library1, DLL, Error) lt;lt; MESSAGE/span/code/pre/figure p使用时像这样:/p figure class=highlightprecode class=language-cpp data-lang=cppspan class=nLIB1_OUT/spanspan class=p(/spanspan class=sIm a message./spanspan class=p);/span span class=nLIB1_WARN/spanspan class=p(/spanspan class=sIm a message, ID = /span span class=olt;lt;/span span class=mi1234/spanspan class=p);/span span class=nLIB1_ERR/spanspan class=p(/spanspan class=sIm a message./spanspan class=p);/span/code/pre/figure p这里所有的配置都通过配置文件完成,还有一种动态的在代码中创建log的方法,log4cpp的官方网站中有a href=http://log4cpp.sourceforge.net/#simpleexample例子/a,我们这里就不介绍了。/p h2 id=配置配置/h2 p在我们前面的演示代码中,提供了一种非常简单的配置文件,常见的存储配置文件的格式有xml,Windows的ini。log4me中使用的是前者,并且提供了专门的工具来简化其操作,如下图所示:/p pimg src=/img/posts/log-configui.png alt=配置界面 //p p根据上图我们进一步来看一些概念:/p h3 id=tracelevelTraceLevel/h3 pTraceLevel用来控制输出的log等级,下面这些比较常用:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcodeINOUT : 进入和离开函数 DEBUG : 调试信息,通常用于诊断问题 INFO : 确认一切按计划照常工作 WARNING : 程序仍然可以运行,但有意外发生,或者将来有问题可能要出现(比如硬盘容量低) ERROR : 错误信息 CRITICAL : 严重错误,表示程序可能无法继续运行 ALWAYS : 始终输出log /code/pre/div/div p这些等级按从上到下依次增加的顺序排列,配置TraceLevel后,那么只有上表中位于该level之下的才能够输出。/p h3 id=markerMarker/h3 pMarker用来进一步控制log的分类,不像前面的演示代码只定义了四种,通常库会完全使用一个32位的整型来表示这些分类,每一位代表一类,这样就有了32种分类,对于大多数应用场景来说这已经完全足够。/p h3 id=appenderAppender/h3 p前面介绍过Appender这个概念,它用来处理log的输出目的地,但真正的库可远不止前面介绍的三种Appender,log4me提供了这些:/p pimg src=/img/posts/log-appenders.png alt=Appenders //p p注意:最后一个Appender是code class=language-plaintext highlighter-rougeTraceSrv/code,它写到memfile中。什么是memfile?这是Linux上的一种内存管理的方法,它将文件映射到内存,通过直接读写内存来操作文件,从而使文件操作变得极其高效便捷,可以参考这里:a href=http://blog.chinaunix.net/uid-26669729-id-3077015.htmlLinux内存管理之mmap详解/a。/p p在Appender中还有一些常用的属性可以配置:/p ul liCreateNewFile: 表明log库启动时是否创建新文件。/li liFileCount amp; FileSize: 用于文件回卷,比如一个log文件lib.log过大时,可以将它重命名为lib.1.log,然后再重新创建lib.log。可以创建多个文件,而这两个参数就用于控制文件数目和单个文件大小。/li liCategoryFilter: 表明该Appender只处理这个filter列举的Category。/li liProcessFilter: 与上面类似,只处理filter列举的进程。/li /ul h3 id=formatterFormatter/h3 p这个概念在前面没有介绍过,但它也非常容易理解:每个Appender都可以包含一个formatter,它用来格式化log信息。因为一条log信息可能包含时间,文件名,行号,TraceLevel,进程ID,正文等信息,有时为了简化log输出,对所有的这些分类作一个取舍,从而达到格式化的目的。这很像C语言中的code class=language-plaintext highlighter-rougeprintf/code。/p p如果formatter设置的是:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcode%TIME%|%PID%|%LEVEL%|%MARKER%|%CAT%|%FILE%|%LINE%|%FUNC%|%USERTEXT%| /code/pre/div/div p那么log的输出会像这样:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcode2014/04/07-16:03:35.251560|5560|Notice|SUB|COMP1|main.cpp|78|test|Im a message| /code/pre/div/div p每一项都和前面formatter中设置的一一对应。/p h3 id=categoryCategory/h3 p现代的log库一般都将Category组织成树型结构,每一个节点都和前后组成父子关系,根据设置,子节点的Category完全可以继承父节点的配置。所有的Category的根节点是root。这里是一个典型的结构:/p pimg src=/img/posts/log-category-structure.png alt=Category结构 //p p一个Category可以包含下面这几个内容:/p pimg src=/img/posts/log-category-components.png alt=Category组成 //p p注意:一个Category可以有多个Appender。/p pName, TraceLevel, Marker和Appender这里就不再赘述。上图中有一个Flag,这是什么?它的存在和前面的树型结构息息相关。前面讲到,因为Category被组织成了树型关系,子节点可以继承父节点的配置,那么何时可以继承,如何继承?这就是Flag的作用了,它包含了两个选项:/p ul liProcess Parent: 如果勾选这一项,就表示一个子节点的log可以传给它的父节点处理。这也是为什么很多情况下只需要配置Root节点,其它的子节点设置这个Flag,就可以默认使用Root的全部配置。/li liUse All Parent Appenders: 如果只有上面的Flag,那么每次信息传到父节点时,父节点都必须根据自身的TraceLevel及Marker进行匹配,只有匹配时才会处理。而如果此Flag打开,那么在传输过程中,只要传输路径上有一个节点匹配,再向上传的所有节点都不再匹配而直接处理。/li /ul h2 id=处理流程处理流程/h2 p至此你已经完全了解了log的基本概念以及用法,接下来我们更进一步,来看看log内部是如何工作的。/p p有了前面的演示代码之后再来看log内部处理流程将变得十分简单,大致可以分为两步,第一步strong过滤/strong:/p pimg src=/img/posts/log-workflow-step1.png alt=处理流程1 //p p它在log的调用线程中发生,有些线程可能会对实时性有一定的要求,那么log就不能够在这种线程中去直接执行,而是将创建的log对象加入到队列中,由专门的log工作线程处理,这样就完全不会阻塞住主线程,保证主线程畅通无阻的运行。/p p流程的第二步是strong处理消息/strong:/p pimg src=/img/posts/log-workflow-step2.png alt=处理流程2 //p p筛选过Category之后会将消息发给每一个合适的Appender,由Appender进一步的筛选及格式化输出。注意在这一步的刚开始有一个code class=language-plaintext highlighter-rougeCheck Config/code步骤,这和我们前面讲的加载配置文件的时机有关,很明显,这里用的是最后一种读取配置的方案:即每次处理log时,检测配置是否更新。/p h2 id=log在系统中的部署log在系统中的部署/h2 p也许你会想,一个简单的库有什么好部署的,直接拿来用不就得了。可有时因为性能,或者系统过于庞大,配置起来会相当复杂,如果log组织的不好的话,你就会见到log文件满天飞,散落各处的情况。有时你可能会需要一个总的log文件包含所有的信息,一些特定目的的log还要存于不同的文件中。如何保证不同进程,甚至不同的机器上的不同进程能够无冲突的写到同一个log文件中呢?假设一个系统包含一台Windows,一台Linux,如何收集散落各个机器的log?如何方便的在Windows上查看本应出现在Linux上的log?如果你有疑问,请看下面的解决方案:/p pimg src=/img/posts/log-deployment.png alt=部署log //p p这个系统足够庞大,包含了两台机器,左边是Windows,右边是Linux。每台机器除各自保存log之外,还将所有的log都最终交给Windows上的code class=language-plaintext highlighter-rougeTraceSrv/code来处理,最终会有一份完整的包含所有机器的log存在于TraceSrv.log中,还有各个不同模块的log文件。同时,还能够通过远程调用TraceOnlReader来实时从TraceSrv中读取log信息。如上图所示,两侧绿色的Log图例中,红色的信息沿着箭头先全部汇聚到code class=language-plaintext highlighter-rougeTraceSrv/code,然后再分发到不同的文件中。/p p这样,开发者就可以通过一次配置,便可以非常方便的组织好所有的log文件,调用端完全剔除了这些复杂的细节,只需要关注log本身。/p p另外注意到,在Windows和Linux端各有一个memfile,它们各自存有机器上的所有log信息,由于是运用了前面所说的mmap机制,程序直接以操作内存的方式来操作文件,非常高效。/p h2 id=尾声尾声/h2 p好了,至此所有在知识分享中的内容便介绍完毕,希望对感兴趣的你有所帮助。/p p我很喜欢部门的知识分享,分享是件好事,在分享的过程中,不仅仅可以让他人获取有用的信息,而且你在分享前需要不断的归纳总结,印证你的结论。在这个过程中,很多你当时思考不充分的问题也可能会得到解决,对你自身的知识、表达能力都有非常大的提高,利人利己。/p p不知你是否有过这样的经历,你遇到一个问题,百思不得其解,于是想向他人求助,可就在你向他人解释这个问题的过程中,说着说着,你发现你找到问题的所在,于是问题解决了,甚至别人还没明白怎么回事。我就经常遇到这种情况。根据这点,有人总结出了一种新型的解决问题的方法,叫做strong橡皮鸭调试法/strong,a href=http://en.wikipedia.org/wiki/Rubber_duck_debugging维基百科/a对它有一个介绍,Jeff Atwood也专门写过一篇文章:a href=http://blog.codinghorror.com/rubber-duck-problem-solving/Rubber Duck Problem Solving/a。(至于为什么叫橡皮鸭而不叫其它的,我想大概和美国人的成长经历有关吧,每个孩子洗澡时都喜欢在浴缸中放一只橡皮鸭,并与它交谈,就像我们儿时的各种玩具一样。)这样做其实是有根据的,它和分享如出一辄,当你和橡皮鸭”交谈”时,你需要彻底的把你的问题仔仔细细的描述一遍,不会放过每一个细节,为防质疑,你可能会做更多的调查。你在描述的同时,也一定在思考,这时之前没有考虑到的方面可能就会暴露出来了。如果使用得当,也许,strong橡皮鸭调试法/strong可以成为log和debug以外,你分析问题又一强有力的武器:-)/p p最后,强烈建议你去看看Jeff的这篇文章。/p p(全文完)/p pfeihu/p p2014.04.09 于 Shenzhen/p

跟我一起学习VIM - The Life Changing Editor

作者 feihu
2014年1月7日 00:00
img src=https://feihu.me/img/post-banners/vim.png alt= width=600 /br / p前两天同事让我在小组内部分享一下VIM,于是我花了一点时间写了个简短的教程。虽然准备有限,但分享过程中大家大多带着一种惊叹的表情,原来编辑器可以这样强大,这算是对我多年来使用VIM的最大鼓舞吧。所以分享结束之后,将这篇简短教程整理一下作为我2014年的第一篇Blog。/p h3 class=no_toc id=目录目录/h3 ul id=markdown-toc lia href=#写在前面life-changing-editor id=markdown-toc-写在前面life-changing-editor写在前面:Life Changing Editor/a/li lia href=#什么是vim id=markdown-toc-什么是vim什么是VIM/a/li lia href=#为什么选vim id=markdown-toc-为什么选vim为什么选VIM/a ul lia href=#为什么选其它 id=markdown-toc-为什么选其它为什么选其它/a/li lia href=#为什么犹豫选择它们 id=markdown-toc-为什么犹豫选择它们为什么犹豫选择它们/a/li lia href=#vim--sum现代编辑器 id=markdown-toc-vim--sum现代编辑器VIM gt;= SUM(现代编辑器)/a/li /ul /li lia href=#如何学习vim id=markdown-toc-如何学习vim如何学习VIM/a ul lia href=#一秒钟变记事本 id=markdown-toc-一秒钟变记事本一秒钟变记事本/a/li lia href=#vim的基本用法 id=markdown-toc-vim的基本用法VIM的基本用法/a/li lia href=#vim进阶插件 id=markdown-toc-vim进阶插件VIM进阶:插件/a ul lia href=#插件管理神器vundle id=markdown-toc-插件管理神器vundle插件管理神器:Vundle/a/li lia href=#配色方案 id=markdown-toc-配色方案配色方案/a/li lia href=#导航与搜索 id=markdown-toc-导航与搜索导航与搜索/a/li lia href=#自动补全 id=markdown-toc-自动补全自动补全/a/li lia href=#语法 id=markdown-toc-语法语法/a/li lia href=#其它 id=markdown-toc-其它其它/a/li /ul /li lia href=#终极配置-spf13 id=markdown-toc-终极配置-spf13终极配置: spf13/a/li lia href=#与其它软件集成 id=markdown-toc-与其它软件集成与其它软件集成/a/li lia href=#一些资源 id=markdown-toc-一些资源一些资源/a/li /ul /li lia href=#写在最后 id=markdown-toc-写在最后写在最后/a/li /ul hr / p搭完网站之后的第一篇文章有些兴奋,先变身话痨简单回顾一下我是如何接触到VIM的,不感兴趣的同学可以直接跳过这一部分:-)/p h2 id=写在前面life-changing-editor写在前面:Life Changing Editor/h2 p我是一个非常strong懒/strong的人,对于效率有着近乎执拗的追求。比如我会花2个小时来写一个脚本,然后使用这个脚本瞬间完成一个任务,而不愿意花一个小时来手工完成这项任务,从绝对时间上来说,写脚本花的时间更长,但我依然乐此不疲。/p pstrong工欲善其事,必先利其器/strong,折腾各种各样的软件就成为了我的一大爱好,尤其是各种人称strong神器/strong的工具类软件,而a href=http://xbeta.info善用佳软/a是这类工具的聚集地,现在我使用的很多优秀的软件都得知于此,包括VIM,所以,如果你和我一样,希望拥有众多“神器”,让工作事半功倍,可以关注此站。/p p第一次听说VIM已经是离开校园参加工作之后的事,那时部门内部大多使用Source Insight代替Visual Studio编写代码,大家都被它的代码管理,自动完成,代码跳转等功能所吸引,但一个领导说了句很多Vimer经常会说,至今仍让我记忆尤新的一句话:/p blockquote p世界上只有三种编辑器,EMACS、VIM和其它/p /blockquote p我很反对这种极端的言论,使用何种工具是一个人自由,只要能发挥一个工具最大的效率就行,不应该加以约束,更不应该鄙视。话虽如此,我却阻挡不住好奇心的驱使,琢磨着到底是什么样的编辑器会拥有这样高的评价。抱着这份好奇,我搜索到了a href=http://xbeta.info善用佳软/a,看到《a href=http://blog.sina.com.cn/s/blog_46dac66f010005kw.html普通人的编辑利器——Vim/a》,Dieken的《a href=http://arch.pconline.com.cn//pcedu/soft/gj/photo/0609/877033.html程序员的编辑器——VIM/a》,以及王垠的《a href=http://arch.pconline.com.cn//pcedu/soft/gj/photo/0609/865628.htmlEmacs是一种信仰!世界最强编辑器介绍/a》BANG……想到不久前看到的a href=http://wufazhuce.org/discussion/2815/one-%E4%B8%80%E4%B8%AA-vol-435一段话/a:/p blockquote p南中国的雷雨天有怒卷的压城云、低飞的鸟和小虫,有隐隐的轰隆声呜呜咽咽……还有一片肃穆里的电光一闪。那闪电几乎是一棵倒着生长的树,发光发亮的枝丫刚刚舒展,立马结出一枚爆炸的果实,那一声炸响从半空中跌落到窗前,炸得人一个激灵,杯中一圈涟漪。/p /blockquote p这种一个激灵的感觉不仅仅局限于雷雨天。在我读完上面几篇文章之后,简单的文字亦立刻击中儃中,炸的一个激灵。从此,我对编辑器的认识被完全颠覆。/p p很多孩子都有一个梦想:希望能够长大之后可以身着军装,腰插手枪,头戴警帽,遇到坏人之后潇洒拔出枪,瞬间解决战斗,除暴安良,匡扶正义。我这样的程序员们也有一个梦想:希望学成之后可以像电影里黑客们一样,对着满屏幕闪烁的各种符号,双手不离键盘噼里啪啦一阵乱敲,屏幕上的符号不断滚动,就攻破了几百公里之外的某某银行的服务器,向帐户里面增加一笔天文数字,然后潇洒的离去,神不知鬼不觉,留下不知所措的孩子们的梦想——警察叔叔们。这简直构成了程序员们的终极幻想:-P。VIM的出现让我感觉离幻想更近了一步,呃,别想错了,我是指——双手不离键盘,噼里啪啦,黑客的范儿。不可否认,扮酷也是促使我学习VIM的一个重要原因:-P。/p p在一个激灵之后,接下来便是不可自拔的陷入VIM世界,于是网上搜索各种入门教程,_vimrc的配置,折腾插件,研究奇巧淫技,将VIM打造成IDE。那感觉就像世界从此就只有VIM,写代码用VIM,Visual Studio用VIM,Source Insight用VIM,甚至写PDF,浏览网页都要用VIM,够折腾吧。可是像Vimer们一样,我依然折腾着,并快乐着。如今,折腾一圈之后,随着对Unix的KISS设计哲学逐渐理解与认可:strong把所有简单的事情做到极致/strong。所以在对待VIM的态度上也有了一定的转变,不再执著的将它打造成万能的IDE,而仅仅让它将编辑功能发挥到极致,其它的事情交给其它更擅长的工具去做。strongK/strongeep strongI/strongt strongS/strongimple, strongS/strongtupid./p p在VIM的a href=http://www.vim.org/官方网站/a上,对每个插件的评价是这样a href=http://www.vim.org/scripts/script.php?script_id=273分类/a的:/p ul licode class=language-plaintext highlighter-rougeLife Changing/code/li licode class=language-plaintext highlighter-rougeHelpful/code/li licode class=language-plaintext highlighter-rougeUnfulfilling/code/li /ul p而我想将这个分类应用到使用的软件上,对于VIM,它是毫无疑问的code class=language-plaintext highlighter-rougeLife Changing/code。/p h2 id=什么是vim什么是VIM/h2 p以下两句对编辑器的最高评价足矣:/p ul liVIM is the God of editors, EMACS is God’s editor/li liEMACS is actually an OS which pretends to be an editor/li /ul h2 id=为什么选vim为什么选VIM/h2 p我们所处的时代是非常幸运的,有越来越多的a href=http://zh.wikipedia.org/wiki/%E6%96%87%E6%9C%AC%E7%BC%96%E8%BE%91%E5%99%A8%E6%AF%94%E8%BE%83编辑器/a,相对于a href=http://arstechnica.com/information-technology/2011/11/two-decades-of-productivity-vims-20th-anniversary/古老的VIM/a和EMACS,它们被称为strong现代/strong编辑器。我们来看看这两个古董有多大年纪了:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcode**EMACS** : 1975 ~ 2013 = 38岁 **VI** : 1976 ~ 2013 = 37岁 **VIM** : 1991 ~ 2013 = 22岁 /code/pre/div/div p看到这篇文章的人有几个是比它们大的:-)/p pVIM的学习曲线非常陡,a href=http://coolshell.cn/articles/3125.html这里/a有一个主流编辑器的学习曲线对比。既然学习VIM如此之难,而strong现代/strong编辑器又已经拥有了如此多的特性,我们为什么要花大量的时间来学习这个老古董呢?/p h3 id=为什么选其它为什么选其它/h3 p先来看看为什么我们会选现在所使用的编辑器?(也许很多人直接用IDE自带的编辑器,我们暂且也把它们划到编辑器的范畴内。)这里我简单列举一些程序员期望使用的编辑拥有的功能:/p ul li轻量级,迅速启动(相对于IDE)/li li特性 ul li语法高亮/li li自动对齐/li li代码折叠/li li自动补全/li li显示行号/li li重定义Tab/li li十六进制编辑/li li列编辑模式/li li快速注释/li li高级搜索,替代/li li错误恢复/li li迅速跳转/li liMark/li /ul /li li也许,美观也是一个诉求/li /ul p但是…/p h3 id=为什么犹豫选择它们为什么犹豫选择它们/h3 p总有一些理由让我们一再犹豫的选择它们,或者勉强使用它们:/p ul li太贵:虽然知道VS很贵,但看到价格时,还是被吓了一跳 ul liVisual Studio Profession 2012 : 11645元/li liUtralEdit : 420元/li liSource Insight : 2500元/li li$$/li li$$/li li$$/li /ul /li li不能跨平台 ul liVS, SI, UE,Notepad++这些只能在Windows上使用/li liMac上的TextMate只能运行于Mac上/li /ul /li li不容易扩展/li /ul p那么,还有别的选择么?/p h3 id=vim--sum现代编辑器VIM gt;= SUM(现代编辑器)/h3 p首先,VIM包含了上面列的所有现代编辑器的优点,并且远远多于此。/p p并且,VIM拥有让你不再strong犹豫/strong的其它特性:/p ul li无止尽的扩展:现在VIM的官方网站上已经有了a href=http://www.vim.org/scripts/script_search_results.php4704/a个扩展,并且在不断增加…/li li完美的跨平台: ul liWindows : gVim/li liLinux : 内置默认 (e.g., man page)/li liMac : MacVim/li /ul /li li开源/li listrong用起来很酷/strong/li li最关键的,\($**免费**\)$/li /ul p废话结束,开始进入正题。/p h2 id=如何学习vim如何学习VIM/h2 h3 id=一秒钟变记事本一秒钟变记事本/h3 p很多时候大家希望能够以最快的速度编辑文档,而不愿意花大量的时间在学习这一工具上,比如偶尔要去Linux改变一下配置。这时VIM有一种方法可以strong一秒钟变记事本/strong,打开VIM之后,只需要一个键code class=language-plaintext highlighter-rougei/code,接下来所有的操作就和Windows上的记事本无异,你所喜爱与习惯的方向键也回来了。/p p这也并没有多神奇,它只是VIM提供的一种特殊的模式:code class=language-plaintext highlighter-rougeInsert mode/code,在按过code class=language-plaintext highlighter-rougei/code之后,你可以在编辑器的左下角看到code class=language-plaintext highlighter-rougeINSERT/code字样。但是因为VIM无法使用code class=language-plaintext highlighter-rougeCTRL-S/code来保存,那么,在编辑完之后,如何保存退出呢?也很简单,先按code class=language-plaintext highlighter-rougeESC/code,再输入code class=language-plaintext highlighter-rouge:wq/code,前面一步是告诉VIM退出code class=language-plaintext highlighter-rougeINSERT/code模式,后面一个命令是保存退出。/p p我见过很多人这样用,虽然说这很容易,但是有种暴殄天物的感觉,和给了你一把AK47,你却把它当成棍子使一样。要发挥AK47的作用,还请向下看。/p h3 id=vim的基本用法VIM的基本用法/h3 p最好的入门教程非VIM自带的a href=C:\Program%20Files%20(x86)\Vim\vim74\vimtutor.batvimtutor/a莫属,它是VIM安装之后自带的简短教程,可以在安装目录下找到,只需半个小时左右的时间,就可以掌握VIM的绝大部分用法。这是迄今为止我见过的软件自带教程中最好的一个。/p p当然,网上的VIM教程也非常多,我之前看的是李果正的a href=http://www.study-area.org/tips/vim/大家来学VIM/a,很适合入门。/p p另外推荐陈皓的a href=http://coolshell.cn/articles/5426.html简明VIM练级攻略/a,或者创意十足的游戏a href=http://vim-adventures.com/VIM大冒险/a。/p pa href=http://vim-adventures.com/img src=/img/posts/vim-adventures.jpg alt=VIM大冒险 //a/p p这游戏的创意实在是太赞了,打完游戏,你便掌握了VIM,这才是真正的strong寓教于乐/strong,下面是摘自这个游戏的描述:/p blockquote pVIM Adventures is an online game based on VIM’s keyboard shortcuts (commands, motions and operators). It’s the “Zelda meets text editing” game. It’s a puzzle game for practicing and memorizing VIM commands (good old VI is also covered, of course). It’s an easy way to learn VIM without a steep learning curve./p /blockquote p最后在这里给大家分享一个vgod设计的a href=http://blog.vgod.tw/2009/12/08/vim-cheat-sheet-for-programmers/VIM命令图解/a。这也是我看过的最好的命令图示,看完了前面的基本教程后,可以将它作为一个cheat sheet随时查看,相信用不了多久你也可以完全丢掉它。关于此图的详细解释可以参考a href=http://blog.vgod.tw/2009/12/08/vim-cheat-sheet-for-programmers/这里/a。/p pa href=http://blog.vgod.tw/2009/12/08/vim-cheat-sheet-for-programmers/img src=/img/posts/vim-cmd.png alt=VIM命令图解 //a/p h3 id=vim进阶插件VIM进阶:插件/h3 p在学完了上面任何一个教程之后,通过一段时间的练习,你已经可以非常熟练的使用VIM。即使是“裸奔”,VIM已经足够强大,能够完成日常的绝大部分工作。但VIM更加强大的是它的扩展机制,就像Firefox和Chrome的各种插件,它们将令我们的工具更加完美。网上有很多教程里写的插件已经过时,接下来我将介绍一些比较新的,非常有用的插件,看完之后,相信你一定会觉得蠢蠢欲动。/p h4 id=插件管理神器vundle插件管理神器:Vundle/h4 p在这开始之前,先简单介绍VIM插件的管理方式。在我刚接触插件之时,安装一个插件需要:/p ol li去官网下载/li li解压/li li拷贝到VIM的安装目录/li li运行:help tags/li /ol p这些步骤已经足够复杂,更加无法想象的是要strong更新/strong或者strong删除/strong一个插件时,因为它的文件分布在各个目录下,就比如Windows上的code class=language-plaintext highlighter-rouge安装路径/code,code class=language-plaintext highlighter-rougeApplication data/code,code class=language-plaintext highlighter-rouge用户数据/code,code class=language-plaintext highlighter-rouge注册表/code等等,除非你对VIM的插件机制和要删的插件了如直掌,否则你能难将它删除干净。所以一段时间之后,VIM的安装目录下简直就是一团乱麻,管理插件几乎成为了一项不可能完成的任务。想象一下,如果Windows上面没有软件管理工具,你如何安装,卸载一个软件吧。/p p但是这没有难倒聪明的Vimer们,他们利用VIM本身的特性,开发出了神器——a href=https://github.com/gmarik/vundleVundle/a,配合上a href=https://github.com/GitHub/a,VIM插件的管理变得前所未有的简单。来对比一下使用Vundle如何管理插件:/p p在按照官方的a href=https://github.com/gmarik/vundle教程/a安装好Vundle之后,要安装一个插件时,你只需要:/p ol li选好插件/li li在VIM的配置文件中加一句 code class=language-plaintext highlighter-rougeBundle your/script/path/code/li li在VIM中运行 code class=language-plaintext highlighter-rouge:BundleInstall/code/li /ol p卸载时只需:/p ol li去除配置文件中的 code class=language-plaintext highlighter-rougeBundle your/script/name/code/li li在VIM中运行 code class=language-plaintext highlighter-rouge:BundleClean/code/li /ol p更新插件就更加简单,只需一句 code class=language-plaintext highlighter-rouge:BundleUpdate/code。现在你已经完全从粗活累活中解放了出来,从此注意力只需放在挑选自己喜欢的插件上,还有比这更美好的么?下面介绍的所有的插件都以它来管理。/p h4 id=配色方案配色方案/h4 p你是否觉得用了许多年的白底黑字有些刺眼,又或者你是否厌倦了那单调枯燥?如果是,那好,VIM提供了成百上千的a href=http://vimcolorschemetest.googlecode.com/svn/html/index-c.html配色方案/a,终有一款适合你。/p p在所有的配色当中,最受欢迎的是这款a href=http://ethanschoonover.com/solarizedSolarized/a:/p pa href=http://ethanschoonover.com/solarizedimg src=/img/posts/vim-solarized-yinyang.png alt=阴阳八卦 //a/p p在Github上它有a href=https://github.com/altercation/solarized4,930/a个Star,仅靠一个code class=language-plaintext highlighter-rouge配色方案/code就得到如此多的Star,可见它有多么的受欢迎。它有两种完全相反的颜色,一暗一亮,作者非常具有创意将它们设计成一个code class=language-plaintext highlighter-rouge阴阳八卦/code,赏心悦目。下面是采用这种配色的VIM截图:/p pimg src=/img/posts/vim-solarized.png alt=Solarized截图 //p pSolarized配色还有一个使它能够成为最受欢迎的配色方案的理由,除了VIM之外,它还提供了很多a href=https://github.com/altercation/ethanschoonover.com/tree/master/projects/solarized#editors--ides其它软件/a的配色方案,包括:code class=language-plaintext highlighter-rougeEmacs/code, code class=language-plaintext highlighter-rougeVisual Studio/code, code class=language-plaintext highlighter-rougeXcode/code, code class=language-plaintext highlighter-rougeNetBeans/code, code class=language-plaintext highlighter-rougePutty/code,各种终端等等,应该是除了默认的黑白配色之外用途最为广泛的一种了。目前我采用的就是这种配色方案的dark background,它的对比度非常适合长期对着编辑器的程序员们。/p p还有一种很受欢迎的配色方案:a href=https://github.com/tomasr/molokaiMolokai/a,它是Mac上TextMate编辑器的一种经典配色,也非常适合程序员:/p pimg src=/img/posts/vim-molokai.png alt=Molokai截图 //p h4 id=导航与搜索导航与搜索/h4 ol li pa href=https://github.com/scrooloose/nerdtreeNERDTree/a - file navigation img src=/img/posts/vim-the-nerd-tree.gif alt=NERDTree //p p代码资源管理器现在已经成为了各种各样IDE的标配,这可以大大提高管理源代码的效率。这样的功能VIM自然不能少,NERD Tree提供了非常丰富的功能,不仅可以以VIM的方式用键盘来操作目录树,同时也可以像Windows资源管理器一样用鼠标来操作。/p pcode class=language-plaintext highlighter-rouge--help:/code 可以将打开目录树的功能绑定到你所喜欢的快捷键上,比如:code class=language-plaintext highlighter-rougemap lt;leadergt;e :NERDTreeTogglelt;CRgt;/code/p /li li pa href=https://github.com/kien/ctrlp.vimCtrlP/a - fast file finder img src=/img/posts/vim-ctrlp.gif alt=CtrlP //p p如果说上面介绍的NERD Tree极大的方便了源代码的管理方式,那CtrlP可以称的上是革命性的,杀手级的VIM查找文件插件。它以简单符合直觉的输入方式,极快的响应速度,精确的准备度,带你在项目中自由穿越。它可以模糊查询定位,包括工程下的所有文件,已经打开的buffer,buffer中的tag以及最近访问的文件。在这之前,我用的是a href=http://www.vim.org/scripts/script.php?script_id=1581lookupfiles/a,因为依赖了其它的插件和应用程序,这个上古时代的插件逐渐被抛弃了。自从有了它,NERD Tree也常常被我束之高阁。/p p据说它模仿了Sublime的名字和功能,我没用过Sublime,但是听说CtrlP这个功能是Sublime最性感的功能之一。可以去它的a href=http://www.sublimetext.com/官网/a看看。/p pcode class=language-plaintext highlighter-rouge--help:/code 这个插件另一个令人称赞的一点在于无比简单直观的使用方式,正如其名:code class=language-plaintext highlighter-rougeCtrl+P/code,然后享受它带来的快感吧。/p /li li pa href=https://github.com/vim-scripts/taglist.vimTaglist/a - source code browser img src=/img/posts/vim-taglist.png alt=Taglist //p p想必使用过Visual Studio和Source Insight的人都非常喜爱这样一个功能:左边有一个Symbol窗口,它列出了当前文件中的宏、全局变量、函数、类等信息,鼠标点击时就会跳到相应的源代码所在的位置,非常便捷。Taglist就是实现这个功能的插件。可以说symbol窗口是程序员不可缺少的功能,当年有很多人热衷于借助taglist、ctags和cscope,将VIM打造成一个非常强大的Linux下的IDE,所以一直以来,taglist在VIM官方网站的scripts排列榜中一直高居a href=http://www.vim.org/scripts/script_search_results.php?keywords=amp;script_type=amp;order_by=ratingamp;direction=descendingamp;search=search榜首/a,成为VIM使用者的必备插件。/p pcode class=language-plaintext highlighter-rouge--help:/code 最常见的做法也是将它绑定到一个快捷键上,比如:code class=language-plaintext highlighter-rougemap lt;silentgt; lt;F9gt; :TlistTogglelt;CRgt;/code/p /li li pa href=https://github.com/majutsushi/tagbarTagbar/a - tag generation and navigation img src=/img/posts/vim-tagbar.gif alt=Tagbar //p p看起来Tagbar和上面介绍的Taglist很相似,它们都是展示当前文件Symbol的插件,但是两者有一定的区别,大家可以从上图的对比中得知,两者的关注点不同。总的来说Tagbar对面向对象的支持更好,它会自动根据文件修改的时间来重新排列Symbol的列表。它们以不同的纬度展示了当前文件的Symbol。/p pcode class=language-plaintext highlighter-rouge--help:/code 同Taglist一样,可以这样绑定它的快捷键,code class=language-plaintext highlighter-rougenmap lt;silentgt; lt;F4gt; :TagbarTogglelt;CRgt;/code/p /li li pa href=https://github.com/vim-scripts/TaskList.vimTasklist/a - eclipse task list img src=/img/posts/vim-tasklist.gif alt=Tasklist //p p这是一个非常有用的插件,它能够标记文件中的code class=language-plaintext highlighter-rougeFIXME/code、code class=language-plaintext highlighter-rougeTODO/code等信息,并将它们存放到一个任务列表当中,后面随时可以通过Tasklist跳转到这些标记的地方再来修改这些代码,是一个十分方便实用的Todo list工具。/p pcode class=language-plaintext highlighter-rouge--help:/code 通常只需添加一个映射:code class=language-plaintext highlighter-rougemap lt;leadergt;td lt;Pluggt;TaskList/code/p /li /ol h4 id=自动补全自动补全/h4 ol li pa href=https://github.com/Valloric/YouCompleteMeYouCompleteMe/a - visual assist for vim img src=/img/posts/vim-youcompleteme.gif alt=YouCompleteMe //p p这是迄今为止,我认为VIM历史上最好的插件,没有之一。为什么这么说?因为作为一个程序员,这个功能必不可少,而它是迄今为止完成的最好的。从名字可以推断出,它的作用是代码补全。不管是在Source Insight,还是安装了Visual Assist的Visual Studio中,代码补全功能可以极大的提高生产力,增加编码的乐趣。大学第一次遇到Visual Assist时带给我的震撼至今记忆犹新,那感觉就似百兽之王有了翅膀,如虎添翼,从此只要安装有Visual Studio的地方我第一时间就会安装Visual Assist。/p p而作为编辑器的VIM,一直以来都没有一个能够达到Visual Assist哪怕一成功力的插件,不管是自带的补全,code class=language-plaintext highlighter-rougeomnicppcomplete/code,code class=language-plaintext highlighter-rougeneocompletecache/code,完全和Visual Assist不在一个数量级上。Visual Assist借助于Visual Studio,它的补全是语义层面的,它完全能够理解程序语言,而VIM的这些插件仅仅是基于文本匹配,虽然最近的code class=language-plaintext highlighter-rougeneocompletecache/code已经好了很多,但准确率非常低。所以在写代码时,即使VIM用得再顺手,绝大部分情况下我还是倾向于code class=language-plaintext highlighter-rougeVisual Studio + Visual Assist/code。/p p但是YouCompleteMe的出现彻底的改变了这一现状,它对代码的补全完全终于也达到了编译器级别,绝不弱于Visual Assist,遇到它是我使用VIM之后最兴奋的一件事。为什么一个编辑器的插件可以做到如此的神奇,原因就在于它基于a href=http://clang.llvm.org/LLVM/clang/a,一个Apple公司为了代替GNU/GCC而支持的编译器,正因为YouCompleteMe有了编译器的支持,而不再像以往的插件一样基于文本来进行匹配,所以准确率才如此之高。其次,由于它是C/S架构,会在本机创建一个服务器端,利用clang来解析代码,然后将结果返回给客户端,所以也就解决了VIM是单线程而造成的各种补全插件速度奇慢的诟病,在使用时,几乎感觉不到任何的延时,体验达到了Visual Assist的级别。/p pYouCompleteMe也是所有的插件当中安装最为复杂的一个,这是因为需要用clang来编译相应的库。因为clang在Linux和Mac平台上支持的非常好,所以在这两个平台上安装相对简单。但是clang并没有官方支持Windows,所以YouCompleteMe插件也没有官方支持Windows。可这么好的东西,活跃在Windows上聪明的Vimer们怎么可能容忍这种事情呢,有人就提供了a href=https://github.com/Valloric/YouCompleteMe/wiki/Windows-Installation-GuideWindows Installation Guide/a,已经编译好了各种版本的YouCompleteMe插件,可以参考这个Guide来安装。我并没有采用它,而是参考了a href=http://weichong78.blogspot.com/2013/11/building-llvmclang-youcompleteme-etc-in.html这里/a,自己编译了YouCompleteMe,其实也不难,一步一步按照介绍的步骤,相信你也可以。/p pYouCompleteMe除了补全以外,还有一个非常重要的作用:code class=language-plaintext highlighter-rouge代码跳转/code,同样可以达到编译器级别的准确度,媲美Visual Assist与Source Insight。/p p有了YouCompleteMe之后,是时候抛弃昂贵的Visual Assist与Source Insight了。赶快安装尝试吧:-)/p pcode class=language-plaintext highlighter-rouge--help:/code 只要设置好项目的code class=language-plaintext highlighter-rouge.ycm_extra_conf.py/code,自动补全功能就可以完美的使用了。通常一个全局的code class=language-plaintext highlighter-rouge.ycm_extra_conf.py/code足矣。代码跳转可以绑定一个快捷键:code class=language-plaintext highlighter-rougennoremap lt;leadergt;jd :YcmCompleter GoToDefinitionElseDeclarationlt;CRgt;/code,很好理解,先跳到定义,如果没找到,则跳到声明处。/p /li li pa href=https://github.com/SirVer/ultisnipsUltiSnips/a - ultimate snippets img src=/img/posts/vim-ultisnips.gif alt=UltiSnips //p p这是什么?相信大家经常在写代码时需要在文件开头加一个版权声明之类的注释,又或者在头文件中要需要:code class=language-plaintext highlighter-rouge#ifndef... #def... #endif/code这样的宏,亦或者写一个code class=language-plaintext highlighter-rougefor/code、code class=language-plaintext highlighter-rougeswitch/code等很固定的代码片段,这是一个非常机械的重复过程,但又十分频繁。我十分厌倦这种重复,为什么不能有一种快速输入这种代码片段的方法呢?于是,各种snippets插件出现了,而它们之中,UltiSnips是最好的一个。比如上面的一长串code class=language-plaintext highlighter-rouge#ifndef... #def... #endif/code,你只需要输入code class=language-plaintext highlighter-rougeifnlt;TABgt;/code,怎么样,方便吧。更为重要的一点是它支持扩展,你可以随心所欲的编辑你自己的snippets。/p p现在它可以和上面介绍的YouCompleteMe插件一块使用,比如在敲完code class=language-plaintext highlighter-rougeifn/code时,YouCompleteMe会将这个snippet也放在下拉框中让你选择,这样你就不用去记何时按code class=language-plaintext highlighter-rougelt;TABgt;/code来展开snippets,YouCompleteMe已经帮你完成。/p p去它的a href=https://github.com/SirVer/ultisnips#screencasts网站/a看看,有几个视频,绝对亮瞎你的双眼(需要翻墙)。/p pcode class=language-plaintext highlighter-rouge--help:/code 它和YouCompleteMe一块使用时会有一定的冲突,因为两者都默认绑定了code class=language-plaintext highlighter-rougelt;TABgt;/code键,可以参考各自的code class=language-plaintext highlighter-rougehelp/code文档,将其中一个绑定到其它的快捷键,或者借助a href=http://www.tuicool.com/articles/eU7BNf其它的插件/a让它们兼容。/p /li li pa href=http://www.vim.org/scripts/script.php?script_id=2981Zen Coding/a - hi-speed coding for html/css img src=/img/posts/vim-zen-coding.gif alt=Zen Coding //p p比一般的code class=language-plaintext highlighter-rougeC/C++/Java/code等更多重复劳动的语言估计要算HTML/CSS这类前端语言了吧,为此前端大牛发明了Zen Coding,去a href=http://vimeo.com/7405114这里/a(需翻墙)看看演示视频,相当令人震撼。如果是写前端的话,强烈推荐此插件。/p pcode class=language-plaintext highlighter-rouge--help:/code 可以去这里参考前端工程师们写的中文教程a href=http://www.zfanw.com/blog/zencoding-vim-tutorial-chinese.html1/a,a href=http://www.qianduan.net/zen-coding-a-new-way-to-write-html-code.html2/a/p /li /ol h4 id=语法语法/h4 ol li pa href=https://github.com/scrooloose/syntasticSyntastic/a - integrated syntax checking img src=/img/posts/vim-syntastic.png alt=Syntastic //p p这是一个非常有用的插件,它能够实时的进行语法和编码风格的检查,利用它几乎可以做到编码完成后无编译错误。并且它还集成了静态检查工具:code class=language-plaintext highlighter-rougelint/code,可以让你的代码更加完美。更强大的它支持近百种编程语言,像是一个集大成的实时编译器。出现错误之后,可以非常方便的跳转到出错处。strong强烈推荐/strong。/p pcode class=language-plaintext highlighter-rouge--help:/code 这是一个后台运行的插件,不需要手动的任何命令来激活它。/p /li li pa href=https://github.com/klen/python-modePython-mode/a - Python in VIM/p iframe src=http://www.tudou.com/programs/view/html5embed.action?type=0amp;code=ihRCG5iUg_wamp;lcode=amp;resourceId=0_06_05_99 allowtransparency=true scrolling=no border=0 frameborder=0 style=width:480px;height:400px;/iframe p如果你需要写Python,那么Python-mode是你一定不能错过的插件,靠它就可以把你的VIM打造成一个强大的Python IDE,因为它可以做到一个现代IDE能做的一切:/p ul li查询Python文档/li li语法及代码风格检查/li li运行调试/li li代码重构/li li……/li /ul p所以,有了它,你就等于有了一个现代的Python IDE,各位Pythoner们,还等什么呢?/p pcode class=language-plaintext highlighter-rouge--help:/code 默认情况下该插件已经绑定了几个快捷键:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcode K -gt; 跳到Python doc处 lt;leadergt;r -gt; 运行当前代码 lt;leadergt;b -gt; 增加/删除断点 /code/pre/div /div /li /ol h4 id=其它其它/h4 ol li pa href=https://github.com/godlygeek/tabularTabularize/a - align everything img src=/img/posts/vim-easy-align.gif alt=Tabularize //p p这个插件的作用是用于按等号、冒号、表格等来对齐文本,参考下面这个初始化变量的例子:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcode int var1 = 10; float var2 = 10.0; char *var_ptr = hello; /code/pre/div /div p运行code class=language-plaintext highlighter-rougeTabularize /=/code可得:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcode int var1 = 10; float var2 = 10.0; char *var_ptr = hello; /code/pre/div /div p另一个常见的用法是格式化文件头:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcode file: main.cpp author: feihu date: 2013-12-17 description: this is the introduction to vim license: TODO: /code/pre/div /div p运行code class=language-plaintext highlighter-rougeTabularize /:/r0/code可得:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcode file : main.cpp author : feihu date : 2013-12-17 description : this is the introduction to vim license : TODO : /code/pre/div /div p另一种对齐方式,运行code class=language-plaintext highlighter-rougeTabularize /:/r1c1l0/code:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcode file : main.cpp author : feihu date : 2013-12-17 description : this is the introduction to vim license : TODO : /code/pre/div /div p对于写代码的人来说,还是非常有用的。因为没有找到对应的图,所以这里就用a href=https://github.com/junegunn/vim-easy-align另外一个插件/a的动画来代替了,Tabular的功能比它更为强大。/p pcode class=language-plaintext highlighter-rouge--help:/code 通常会绑定这样一些快捷键:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcode nmap lt;Leadergt;aamp; :Tabularize /amp;lt;CRgt; vmap lt;Leadergt;aamp; :Tabularize /amp;lt;CRgt; nmap lt;Leadergt;a= :Tabularize /=lt;CRgt; vmap lt;Leadergt;a= :Tabularize /=lt;CRgt; nmap lt;Leadergt;a: :Tabularize /:lt;CRgt; vmap lt;Leadergt;a: :Tabularize /:lt;CRgt; nmap lt;Leadergt;a:: :Tabularize /:\zslt;CRgt; vmap lt;Leadergt;a:: :Tabularize /:\zslt;CRgt; nmap lt;Leadergt;a, :Tabularize /,lt;CRgt; vmap lt;Leadergt;a, :Tabularize /,lt;CRgt; nmap lt;Leadergt;a,, :Tabularize /,\zslt;CRgt; vmap lt;Leadergt;a,, :Tabularize /,\zslt;CRgt; nmap lt;Leadergt;alt;Bargt; :Tabularize /lt;Bargt;lt;CRgt; vmap lt;Leadergt;alt;Bargt; :Tabularize /lt;Bargt;lt;CRgt; /code/pre/div /div /li li pa href=https://github.com/Lokaltog/vim-easymotionEasymotion/a - jump anywhere img src=/img/posts/vim-easymotion.gif alt=Easymotion //p pVIM本身的移动方式已经是极其高效快速,它在编辑器的世界中独树一帜,算是一个极大的创新。而如果说它的移动方式是一个创新的话,那么Easy Motion的移动方式就是一个划时代的革命。利用VIM的code class=language-plaintext highlighter-rouge#w/code、code class=language-plaintext highlighter-rouge#b/code、code class=language-plaintext highlighter-rouge:#/code等操作,移动到一个位置就像是大炮瞄准一个目标,它可以精确到一个大致的范围内。而Easy Motion可以比作是精确制导,它可以准备无误的定位到一个字母上。/p p这种移动方式我曾在Firefox和Chrome的VIM插件中看到过,跳转到一个超链时就采用了同样的方式,但是由于浏览网页的特殊性与随意性,当时我没有适应。在编辑的时候就不一样了,编辑更加专注,更带有目的性,所以它能够极大的提高移动速度。享受这种光标指间跳跃,指随意动,移动如飞的感觉:-P/p pcode class=language-plaintext highlighter-rouge--help:/code 插件默认的快捷键是:code class=language-plaintext highlighter-rougelt;leadergt;lt;leadergt;w/code,效果如上图所示。/p /li li pa href=https://github.com/scrooloose/nerdcommenterNERDCommenter/a - comment++ img src=/img/posts/vim-nerdcomment.gif alt=NERDCommenter //p p又是一个写代码必备的插件,用于快速,批量注释与反注释。它适用于任何你能想到的语言,会根据不同的语言选择不同的注释方式,方便快捷。/p pcode class=language-plaintext highlighter-rouge--help:/code 十分简单的用法,默认配置情况下选择好要注释的行后,运行code class=language-plaintext highlighter-rougelt;leadergt;cc/code注释,code class=language-plaintext highlighter-rougelt;leadergt;cu/code反注释,也可以都调用code class=language-plaintext highlighter-rougelt;leadergt;clt;SPACEgt;/code,它会根据是否有注释而选择来注释还是取消注释。/p /li li pa href=https://github.com/tpope/vim-surroundSurround/a - managing all the “’[{}]’” etc img src=/img/posts/vim-surround.gif alt=Surround //p p在写代码时经常会遇到配对的符号,比如code class=language-plaintext highlighter-rouge{}[]()lt;gt;/code等,尤其是标记类语言,比如html, xml,它们完全依赖这种语法。现代的各种编辑器一般都可以在输入一半符号的时候帮你自动补全另外一半。可有的时候你想修改、删除或者是增加一个块的配对符号时,它们就无能为力了。/p pSurround就是一个专门用来处理这种配对符号的插件,它可以非常高效快速的修改、删除及增加一个配对符号。如果你经常和这些配对符号打交道,比如你是一个前端工程师,那么请一定不要错过这样一个神级插件。/p pcode class=language-plaintext highlighter-rouge--help:/code:部分常用快捷键如下:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcode Normal mode ds - delete a surrounding cs - change a surrounding ys - add a surrounding yS - add a surrounding and place the surrounded text on a new line + indent it yss - add a surrounding to the whole line ySs - add a surrounding to the whole line, place it on a new line + indent it ySS - same as ySs Visual mode s - in visual mode, add a surrounding S - in visual mode, add a surrounding but place text on new line + indent it Insert mode lt;CTRL-sgt; - in insert mode, add a surrounding lt;CTRL-sgt;lt;CTRL-sgt; - in insert mode, add a new line + surrounding + indent lt;CTRL-ggt;s - same as lt;CTRL-sgt; lt;CTRL-ggt;S - same as lt;CTRL-sgt;lt;CTRL-sgt; /code/pre/div /div /li li pa href=https://github.com/sjl/gundo.vimGundo/a - time machine img src=/img/posts/vim-gundo.jpg alt=Gundo //p p现代编辑器都提供了多次的撤消和重做功能,这样你就可以很放心的修改文档或者恢复文档。可是假如你操作了5次,然后撤消2次,再重新编辑后,你肯定是无法回到最开始的3次编辑了,因为在你复杂的操作后,编辑器维护的Undo Tree实际上出现了分支,而一般的code class=language-plaintext highlighter-rougeCTRL+Z/code和code class=language-plaintext highlighter-rougeCTRL+R/code无法实现这么复杂的操作。/p p这时VIM的优势又体现了出来,它不仅提供无限撤消,VIM 7.3之后还有永久撤消功能,即使文件关闭后再次打开,之前的修改仍然可以撤消。而Gundo提供了一个树状图形的撤消列表,下方还有每次修改的差异对比,分支一目了然,相当于一个面向撤消与编辑操作的版本控制工具。有了它,你的文件编辑就像是有了一台时光机,可以随心所欲的回到任何时间,乘着你的时光机,放心大胆的去穿梭时空吧:-P/p pcode class=language-plaintext highlighter-rouge--help:/code 通常会将这句加入code class=language-plaintext highlighter-rouge_vimrc/code:code class=language-plaintext highlighter-rougennoremap lt;Leadergt;u :GundoTogglelt;CRgt;/code/p /li li pa href=http://www.vim.org/scripts/script.php?script_id=2010Sessionman/a - session manager/p p这是VIM的Session Manager,作用很简单,管理VIM的会话,可以让你在重新打开VIM之后立刻进行之前的编辑状态,就像Windows的休眠一样,相信它一定是你工作的好伴侣。/p pcode class=language-plaintext highlighter-rouge--help:/code 我的配置如下:/p div class=language-plaintext highlighter-rougediv class=highlightpre class=highlightcode set sessionoptions=blank,buffers,curdir,folds,tabpages,winsize nmap lt;leadergt;sl :SessionListlt;CRgt; nmap lt;leadergt;ss :SessionSavelt;CRgt; nmap lt;leadergt;sc :SessionCloselt;CRgt; /code/pre/div /div /li li pa href=https://github.com/Lokaltog/vim-powerlinePowerline/a - ultimate statusline utility img src=/img/posts/vim-powerline.png alt=Powerline //p p增强型的状态栏插件,可以以各种漂亮的颜色展示状态栏,显示文件编码,类型,光标位置,甚至可以显示版本控制信息。不仅功能强大,写着代码时看着下面赏心悦目的状态状,心情也因此大好。像我一样的外观控一定无法抗拒它:-)/p pcode class=language-plaintext highlighter-rouge--help:/code 简单实用,无需多余的配置。/p /li /ol h3 id=终极配置-spf13终极配置: spf13/h3 p至此,我经常用到的所有插件都介绍完了,如果你也都安装尝试一下的话,相信很容易就配置出来符合你个人习惯的强大的IDE。也许有人会想,这么多的主题、个性化设置、插件,配置太麻烦,有没有已经配置好的,可以直接拿来使用呢?其实我当时也有一样的想法,在折腾了很久之后,发现code class=language-plaintext highlighter-rouge_vimrc/code已经非常庞大且混乱,亟需整理。再后来就发现了它,code class=language-plaintext highlighter-rougespf13/code:/p pimg src=/img/posts/vim-spf13.png alt=spf13 //p p它是a href=https://github.com/spf13/spf13-vimSteve Francia’s Vim Distribution/a,但是组织的非常整洁,容易扩展,并且跨平台,易于安装维护。在看到的所有code class=language-plaintext highlighter-rouge_vimrc/code中,这是写的最漂亮的一个。只需要一个简单的脚本就可以a href=http://vim.spf13.com/#install安装/a,这里面利用了方便的code class=language-plaintext highlighter-rougeVundle/code集成了绝大部分前面介绍的插件,并且还有大量其它的插件,具体可以看它的code class=language-plaintext highlighter-rouge.vimrc.bundles/code。/p p因为它完美的结构组织,你完全可以在不修改它任何文件的基础上,对应增加几个自己的code class=language-plaintext highlighter-rouge~/.vimrc.local/code,code class=language-plaintext highlighter-rouge~/.vimrc.bundles.local/code,code class=language-plaintext highlighter-rouge~/.vimrc.before.local/code文件来增加自己的个性化配置,或者增加删除插件,可扩展性极强。在我的code class=language-plaintext highlighter-rouge_vimrc/code乱成一团的情况,果断fork并安装了这个Distribution,增加了自己的一些配置,最终形成了现在的VIM。如果你也不愿折腾配置,那么完全可以直接安装它,省事方便的同时还可以学习一下它的组织结构,一举两得。/p h3 id=与其它软件集成与其它软件集成/h3 p因为VIM的操作方式广泛为人们所逐渐接受,尤其是经常工作在Linux下的人们,所以它越来越多的被集成到其它一些常用的工具上,我用过的就包括:/p ul li pVisual Studio/p p本身Windows下的gVim安装包在安装时会提供一个集成到Visual Studio中的插件code class=language-plaintext highlighter-rougeVsVim/code,可以选择安装,但它是另开一个VIM的窗口来编辑当前的文件,我并不习惯这种方式,所以又找到了a href=http://www.viemu.com/code class=language-plaintext highlighter-rougeViEmu/code/a,它完美的将VIM的操作方式集成到了Visual Studio中,让你根本感觉不到这是在使用Visual Studio。更加强大的是,它可以完美的和Visual Assist集成:/p blockquote p Build 1854 contains a workaround for case=58034. Create a binary registry value named TrackCaretVisibility under HKCU\Software\Whole Tomato\Visual Assist X\VANet10 and set its value to 00 for compatibility with ViEmu. (The value defaults to 01 and is created for you upon exiting VS the first time you run 1854 or higher.) Note you need to close all IDEs before editing this registry key, to avoid Visual Assist X overwriting your change when it exits. /p /blockquote p在遇到YouCompleteMe之前,这就是我所采用的编程环境。但这是一个商业版的插件,只有30天的试用期,如果你真的喜欢它的,完全可以买下它,绝对物超所值。更为强大的是它还支持code class=language-plaintext highlighter-rougeXcode/code、code class=language-plaintext highlighter-rougeWord/code、code class=language-plaintext highlighter-rougeOutlook/code、code class=language-plaintext highlighter-rougeSQL Server/code,这一定是一个极端的Vimer的项目:-),来看看它的动画: img src=/img/posts/vim-viemu.gif alt=ViEmu //p /li li pSource Insight/p pVIM也可以集成到Source Insight中,不过我没有去找相应的插件,只找一种和前面介绍的code class=language-plaintext highlighter-rougeVsVim/code一样的方法:/p ul li在Source Insight菜单中,Options-Custom Commands/li liRun: “C:\Program Files\Vim\vim74\gvim.exe” –remote-silent +%l %f/li liDir: %d/li liAdd之后再Options-Key Assignments,将它绑定到一个快捷键中,比如code class=language-plaintext highlighter-rougeF11/code/li /ul p这样编辑一个文件时,如果你想打开VIM时,直接按code class=language-plaintext highlighter-rougeF11/code,它就会跳到当前行,编辑完之后关闭VIM,又回到Source Insight中。这种方法我用过一段时间,后来由于很少用Source Insight写代码,也逐渐淡忘了。/p /li li pFirefox/Chrome/p p在狂热于VIM的年代,我曾想把一切操作都变成VIM的方式,包括上网。所以就找到了a href=https://addons.mozilla.org/en-US/firefox/addon/vimperator/Vimperator/a,但终究由于上网是一种更加随性、无目的的行为,拿着鼠标随便点点完全可以了,所以也就放弃它,回归到正常的操作方式下,有兴趣的可以把玩一下,很有意思,之前谈到的code class=language-plaintext highlighter-rougeEasy Motion/code我就在这里见识过。Chrome下也有相应的a href=https://chrome.google.com/webstore/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb插件/a。/p /li /ul h3 id=一些资源一些资源/h3 p最后附上一些有趣有用的资源:/p ul li一篇非常好的为什么使用VIM的文章,请看a href=http://www.viemu.com/a-why-vi-vim.html这里/a/li li为什么VIM使用HJKL作为方向键?请看a href=http://news.cnblogs.com/n/141251/这里/a/li li为什么VIM和EMACS被称为最好的编辑器?这看a href=http://blog.csdn.net/canlets/article/details/17307657这里/a/li liVIM作者的演讲:《a href=http://xbeta.info/7habits-edit.htm高效编辑的7个习惯/a》,视频请点a href=http://v.youku.com/v_show/id_XMTIwNDY5MjY4.html这里/a/li /ul h2 id=写在最后写在最后/h2 p网上可能有很多人像我之前一样,过于关注工具本身,而忽略了一个非常重要的问题:工具之所以称为工具,仅仅在于它是被人们拿来使用,只要顺手就好,用它来做的事情才是关键。对于我们开发人员来说,专业知识永远比工具更为重要。自打VIM出生以来,就有几个亘古不变的话题:/p ul liVIM vs Emasc/li liVIM vs 其它编辑器/li liVIM vs IDE/li /ul p争论从来没有平息过,从远古时期的大牛们,到刚刚踏入VIM阵营的我们,也从来没有一个结论。也许很多人争吵已经不再是单单的编辑器之争,而是出于维护心目中最好的工作方式,甚至哲学之争。但对于大部分人来说,只要你的工具足够称手,那么多写几行代码,多看些书,远比参与这些无休止的争吵强得多。但如果你更深一步,开发出更好的编辑器,或者插件,那又另当别论了。/p p这篇教程至此也将告一段落,说是教程,本文却并没有详细的介绍如何入门,反而回忆了一大段个人学习VIM的经历,然后介绍了常用的优秀插件。也许看完本文,你并不一定能够学会VIM,但是它提供了很多比本文更有价值去学习的资源,给了你一个整体的认识,让你看到VIM可以强大到什么程度,避免走很多弯路。看完本文之后,你能够知道如何入门,如何去选插件,我想,对于本文来说,这就够了。/p p(全文完)/p pfeihu/p p2014.01.07 于 Shenzhen/p
❌
❌