既然给空指针发送消息不会崩溃,那么我们是否还有必要在发消息前判断一下指针是否为空?

在知乎上有人问到说,斯坦福课程里推荐不用判断 nil。但是有时候不判断 nil 又会导致程序崩溃,例如往 NSArray 里插入一个 nil 的情况。

以下是我的回答:

不管斯坦福怎么说,我的建议是如果这个指针可能为空,那么用之前都做一下判断。

为什么这么建议呢?

首先,最实际的理由是,给空指针发消息是非常慢的,而做一下 if 判断是否为空是非常快的操作。
这个速度上的差别是百倍这个单位的。

为什么呢?因为给 nil 发消息这件事情是在运行时判断的,而系统也并非简单的丢掉这个消息,系统还需要判断这个消息是否需要返回值,如果需要,还要判断返回值的类型:如果返回指针、数字、真伪等,则返回 0,如果返回的是 OS 定义过的数据结构,那么会返回一个被填充上 0 的结构回来,如果返回的是其它类型,则系统会返回一个未定义的类型回来。而一个 if,则能够直接跳过这些运行时的处理。

我做过一个实验:

分别是两个100万次的循环,其中一个给 nil 对象发送了 100万次消息,另外一个循环在发消息前,会判断一下该对象是否为空。

实验的结果是两个循环的执行时间相差了200多倍。这还是消息中没有传任何参数的情况。

这就好像我们不建议用 try cache 来做本可以用 if else 判断掉的错误处理一样,因为开销太大。

另外,“给空指针发消息不会崩溃”是语言特性,是为了防止人为失误导致程序崩溃的“保险”机制。我们写代码时应该尽量避免依赖语言特性的写法,因为这不利于代码(逻辑)移植,语言特性本身也是会变化的(哪怕变化的可能性为零)。

最后,这能养成一个好习惯,培养出一个好的思维方式。上述的这种遇到“错误”也让程序继续跑下去的语言特性本身就是有争议的,虽然给 nil 发消息不会崩溃,但也正是由于程序不会崩溃,当你的代码写错时你也无法察觉到,你的程序有可能会进入一个你完全没有预期到的状态,看似一切正常,但返回的结果却是完全错误的。而写代码时花1秒钟加一个简单的 if 判断,就能够清楚地看出哪个指针是可能为空的,这些信息在分析代码问题的时候是非常宝贵的。

两个小时解决掉3个 BAD_ACCESS 的收获

首先,我说的是 iOS 开发,不是 Mac OS。

其次,这次解决的三个 BAD_ACCESS 都是由于 iOS 程序在收到 Memory Warning 后,非当前 UIViewController 执行 viewDidUnload 后出现的问题。

这类崩溃在真机上比较难测,因为是随机出现的,而且看 Crash Log 经常会看不出是哪行代码 BAD_ACCESS 了。所以这就需要善用模拟器提供的 “Hardware -> Simulate Memory Warning” 功能了。

同时为了能够方便的 Debug 出僵尸指针是谁,我在 Xcode 里加入以下三个环境变量:

NSZombieEnabled NSAutoreleaseFreedObjectCheckEnabled NSDebugEnabled

Xcode 4 的话编辑一下 Scheme,加到 Environment Variables 里即可(如果以前没加过的话)。

做完准备工作就能比较方便的进行 Debug 了。可以每切换一次 UIViewController 就模拟一次内存警告,看会不会崩溃。

接下来说说我这次遇到的三个问题。
1、一个低级错误

由于我们有时会几个人同时改一个文件,结果就发生了A用完一个实例以后顺手给释放掉了,但是却把B写在 viewDidUnload 里的代码给留下了。自然也就过度释放了。

这类过度释放的错误比较容易发现,只要改代码的时候稍微在原有文件里做一下搜索就能避免。

2、关于 Interface Builder 里的实例生命周期有多长的问题。

开发过 Mac OS 应用的同学都知道在开发 Mac 应用的时候,IB 里的实例 IBOutlet 到代码里是不用 retain 的。但是在 iOS 里却是要 retain 的。

这有什么区别呢?

如果你用的是 UIView 的话,的确,在 iOS 里不做 retain 也不要紧。因为 UIViewController 里的 UIView 本质上都是被 addSubview 到别的 UIView(UIWindow)里去的。但如果你在 IB 里放的是一个 NSObject,问题就出来了。

这也就是我遇到的问题。iOS 会把这个 NSObject 释放掉!

找这个问题花了我不少功夫,因为这个 NSObject 我只是在 IB 里连了连线,压根儿就没在代码里出现……解决方法其实很简单,在代码里加上

@property (nonatomic, retain) NSObject *someObject;

就好了(注意内存释放)。

3、BCTabBarController 带来的噩梦

在解决了以上问题后,整个程序仍然在满世界地不停的崩溃。这让我也很崩溃。咱好歹也算对 iOS 内存管理略有心得,搞个程序崩成这样实在是说不过去。

继续找下去发现问题出在一个第三方的开源控件:BCTabBarController 上。

用一个水准不高的第三方控件的结果就是给开发带来一定方便的同时,也带来了更多的麻烦(我不是在指责 BCTabBarController 的水准,因为要在苹果有限的公开 API 上完全自己来模拟原生 UITabBarController 的行为,的确是有难度的)。

这次的问题出在 BC 调用其管理的 UIViewController 的 viewWillAppear: 函数时,忽略了该 UIViewController 可能已经 viewDidUnload 过了。一上来就直接调用 viewWillAppear: ,结果当然不是崩掉就是逻辑乱掉。

解决方法是在 BC 调用 [viewController viewWillAppear:NO]; 之前加一句 [viewController view]; ,确保 viewDidLoad 先于 viewWillAppear: 被调用即可。

注:loadView 和 viewDidLoad 都不应该自己直接去调用,用 [viewController view]; 这种方法来让 view 自行载入的好处是,如果 view 已经被载入了,这一步操作不会让所有 view 再被载入一次。

现已发现的问题就是这么多,要在 iOS 上写个没有内存泄露又不会崩溃的程序实在是有难度。

Use Chrome or Safari to Debug your Node.js codes

1. Install node-inspector

npm install node-inspector

2. Start up node-inspector

node-inspector

>visit http://0.0.0.0:8080/debug?port=5858 to start debugging

3. Start up node

node –debug test.js

>debugger listening on port 5858 >Server running at http://127.0.0.1:8124/

4. Open URL: http://127.0.0.1:8080/debug?port=5858 in Chrome or Safari, Open Inspector in your browser, select the test.js file

5. Add a breakpoint in the test.js file

6. Open URL: http://127.0.0.1:8124/

You should see what you want.

test.js file:

var http = require('http');

var x = 0;
http.createServer(function (req, res) {
  x += 1;
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World ' + x);
}).listen(8124);
console.log('Server running at http://127.0.0.1:8124/');

 

How to know the UINavigationController is pushing or poping by using UINavigationControllerDelegate?


- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    BOOL isPush = YES;
    UINavigationItem *newTopItem = navigationController.topViewController.navigationItem;
    if ([newTopItem isEqual:navigationController.navigationBar.topItem]) {
        // nothing happened.
        return;
    } else {
        for (UINavigationItem *item in navigationController.navigationBar.items) {
            if ([item isEqual:newTopItem]) {
                isPush = NO;
            }
        }
    }
    NSLog(@"isPush: %d", isPush);
}

NOTE: This code has been tested on iOS 4.2 & iOS 4.3.

WARNING: If you are hiding the UINavigationBar, this code may NOT always get correct result. In that case, [newTopItem isEqual:navigationController.navigationBar.topItem] is always YES.

Why not use UINavigationBarDelegate?

This is a great idea if you do not need to know if the push/pop is animated.

But sometimes, we have to know the animated situation, and we need the navigationController and the viewController instance passed from the delegate.