构建测试平台
获取隐藏的文件信息,在终端输入如下命令:
defaults write com.apple.Finder AppleShowAllFiles TRUE
defaults write com.apple.Finder Showpathbar -bool true
defaults write com.apple.Finder _FXShowPosixPathInTitle -bool true
defaults write NSGlobalDomain AppleShowAllExtensions -bool true
chflags nohidden ~/Library/
通过上述设置,Finder中的所有文件都变为可见,包括以“.”开头的隐藏文件。此外还会显示详细的文件路径和扩展信息,最为重要的是能看到与用户相关的Library目录,iOS模拟器和Xcode的数据都存储在此目录下。
chflags命令将那些苹果认为会迷惑用户的隐藏文件全部显示了出来,例如/tmp或/usr。这样更方便查看模拟器目录。
把$SIMPATH添加到Finder的侧边栏,便于访问。默认设置下,Finder没有$SIMPATH。想要实现上述设置,在终端输入以下命令:
cd ~/Library/Developer
open .
Developer文件夹下有三个目录。其中CoreSimulator、Xcode是我们常用的。将Developer拖拽到Finder侧边栏中。
Xcode构建设置
首先将警告视为错误。大部分警告都是由clang产生的,它属于Xcode编译器的前端(frontend),值得认真对待。这样做可以减少代码复杂度、确保语法正确,还可以捕获那些难以发现的错误,比如无符号问题或是格式字符串漏洞,看如下代码:
- (void)validate:(NSArray *)someTribbles withValue:(NSInteger)desired{
if(desired > [someTribbles count]) {
}
}
NSArray的count方法返回一个无符号整形(NSUInteger),if判断语句会提示 “'NSInteger' (aka 'long') and 'NSUInteger' (aka 'unsigned long'”警告。启用将警告视为错误的选项,从而使clang标记该类型为bug,运行工程将会失败。
这里的意思是说启用项目构建配置中的更多警告模式,并将警告提升到bug的高度,强迫自己尽可能在开发的早期阶段解决掉一些隐患,培养良好的编码习惯。
在target->build setting->Warning Policies下,把Treat Warnings as Errors 设置为YES。
在Custom Compiler Flags下,设置Other Warning Flags,这里有-Wall、-Wextra、-Weverything三个值可选,含义如下:
-Wall 并不是所有警告。这一个警告组开启的是编译器开发者对于“你所写的代码中有问题”这一命题有着很高的自信的那些警告。要是在这一组设定下你的代码出现了警告,那基本上就是你的代码真的存在严重问题了。但是同时,并不是说打开Wall就万事大吉了,因为Wall所针对的仅仅只是经典代码库中的为数不多的问题,因此有一些致命的警告并不能被其捕捉到。但是不论如何,因为Wall的警告提供的都是可信度和优先级很高的警告,所以为所有项目(至少是所有新项目)打开这组警告,应该成为一种良好的习惯。
-Wextra 如其所名,-Wextra组提供“额外的”警告。这个组和-Wall组几乎一样有用,但是有些情况下对于代码相对过于严苛。一个很常见的例子是,-Wextra中包含了-Wsign-compare, 这个警告标识会开启比较时候对signed和unsigned的类型检查,当比较符两边一边是signed一边是unsigned时,产生警告。其实很多 代码并没有特别在意这样的比较,而且绝大多数时候,比较signed和unsigned也是没有太大问题的(当然不排除会有致命错误出现的情况)。需要注意,-Wextra和-Wall是相互独立的两个警告组,虽然里面打开的警告标识有个别是重复的,但是两组并没有包含的关系。想要同时使用的话必须都加上
-Weverything 这个是真正的所有警告。但是一般开发者不会选择使用这个标识,因为它包含了那些还正在开发中的可能尚存bug的警告提示。这个标识一般是编译器开发者用来调试时使用的,如果你想在自己的项目里开启的话,警告一定会爆棚导致你想开始撞墙..
这里推荐-Wall和-Wextra,或者-Wextra,设置后运行程序警告数成倍的增长,看看其中哪些是真正的bug。
Clang和静态分析(Static Analyzer)
静态分析一般指的是使用工具来分析代码并找出安全漏洞。这可能涉及识别一系列危险的API,或分析通过应用程序的数据流,来标识潜在不安全的程序输入。
Xcode中提供了一个UI界面工具。用户可以使用该工具很直观的完成逻辑追踪、代码漏洞以及通用API误用等分析操作。但有一些重要特性,Xcode默认是禁止。其中值得关注的特性有:对C语言库函数等经典威胁的检查,比如针对strcpy和strcat的检查就是默认关闭的。你可以在项目或Target设置中开启这些特性:
在Xcode8中,静态分析能够检测出三种新的错误, 它们分别是Localizability、Instance Cleanup、Nullability。
- Localizability 其实说的是静态分析能够检测出本地化信息缺失的问题,目前能够检测出来两种类型的错误,一种是没有使用 NSLocalizeString这样的API,而直接给控件设置Sting的情况,一种是使用了相应的API,但在comment信息里面赋值为nil。默认是不开启的,在BuildSting中作如下设置:
Instance Cleanup 在MRC的代码中,尤其在dealloc中,我们不应该对assign类型的属性进行release操作,应该对retain或者 copy类型的属性进行release操作,如果不这样操作的话,会引发一些不必要的麻烦
Nullability 在2015年的WWDC大会上,Objective-C引入的一个新特性就叫做Nullability,用于表明一个东西到底可以为nil还是不可以为nil,这和Swift里的option类型很相似。
Sanitizer和动态分析
- Sanitizer是一个用于优化的工具。
在edit scheme->Diagnostics->Runtime Sanitizer中勾选相应的Sanitizer选项。
勾选了相应的选项并不代表你就能使用 Sanitizer 来Check代码了, 你还必须重新run一下代码,为什么呢?
这就必须说说整个代码 build flow 了. 如下图所示, 通过勾选了对应的选项, Xcode 会向 clang 传递一个特定的参数, 然后生成一个独特的 binary, 然后这个 binary 会和 Thread Sanitizer 或者 Address Sanitizer 的 dylib 链接在一起. 这样 Sanitizer 就实现了它想要达到的功能.
Address Sanitizer(ASan)是一个类似Valgrind的动态分析工具,ASan可以检测出堆、栈溢出和释放后又被使用的bug,还能找到关键的安全漏洞。ASan会对性能产生一些影响(程序执行速度会慢一些),因此不要在发行版本中启用这个选项,但是在测试、质量保证检测或是缺陷测试阶段,使用这一特性带来便利吧。ASan能检查以下类型的错误:
- Use after free
- Heap buffer overflow
- Stack buffer overflow
- Global variable overflow
- Overflows in C++ containers
- Use after return
- (void)viewDidLoad {
[super viewDidLoad];
char *buffer = malloc(10);
buffer[10] = 'A';
free(buffer);
}
运行上面的代码,在开启ASan时,程序会崩溃在“buffer[10] = 'A';”这一行,并显示相应的崩溃类型。在不开启ASan时,不添加任何断点时,程序会崩溃在main函数里;添加任何断点后,程序不会崩溃。
问题很明显,这是一个数组越界,开启ASan后会直接定位到问题发生的地方,不开启的话,阿弥陀佛了。
- Thread Sanitizer (TSan)是Xcode8的新特性,检测线程方面的问题。
让我们想想自己在调试线程方面的 bug 时, 有哪些令人记忆深刻的东西:
- 线程方面的 bug 对时间很敏感, 这就导致很多线程的 bug 极难复现, 复现都成问题, 还怎么改 bug
- 由于线程的抽象概念导致在 debug 时候也比一般的 debug 更费劲儿, 这时候总觉自己脑子不够使
- 有时候, 由于线程引起的 crash 或者 error ,让我们根本意识不到这其实是线程出了问题
在Address Sanitizer下面勾选Thread Sanitizer。至于 Thread Sanitizer下面的那个Pause on Issues的选项就是说,如果你想一个一个看runtime issue就勾选它,如果你不想这样,就不要勾选它, 具体是个神马感觉,你自己试试喽。
如果你喜欢使用 Comman-Line ,那么请记住下面的代码
//Compile and Link with TSan
$ clang -fsanitize=thread source.c -o executable
$ swift -sanitize=thread source.swift -o executable
$ xcodebuild -enableThreadSanitizer YES
//Stop after the first error
$ TSAN_OPTIONS=halt_on_error=1 ./executable
TSan现在只支持64位macOS,以及64位的iOS和tvOS的模拟器,并不支持真机调试和watchOS。
那么TSan作为一个能够检查线程错误的工具, 它能检查以下类型的错误:
- Use of uninitialized mutexes(使用未初始化的互斥器)
- Thread leaks (missing phread_johin)(线程泄漏)
- Unsafe calls in signal handlers (ex:malloc)()
- Unlock from wrong thread(从错误的线程解锁)
- Data race(数据冲突)
那么我们拿下面的这段代码来举例:
- (void)viewDidLoad {
[super viewDidLoad];
[self resetStatue];
pthread_mutex_init(&(_mutex), NULL);
}
- (void)resetStatue{
[self acquireLock];
self.dataArray = nil;
[self releaseLock];
}
- (void)acquireLock{
pthread_mutex_lock(&_mutex);
}
- (void)releaseLock{
pthread_mutex_unlock(&_mutex);
}
这里我们为防止多个线程去访问同一个dataArray属性,在resetStatue方法中使用了互斥锁,但是resetStatue的调用是在互斥锁初始化(pthread_mutex_init(&(_mutex), NULL))之前,这样的调用顺序是错误的,在不开启TSan时,程序不会发生崩溃。开启Tasn后,效果如下:
左边runtime issue中明确的告诉了我们错误的类型(Use of uninitialized mutexes),而且把线程中的历史信息都记录了下来以便我们分析并解决这个问题。
在开发中我们还会用到Reveal、Charles、Instruments的Leaks、Time Profiler等工具,掌握这些后会让你在开发中发现问题、解决问题更加容易,提高生产力。