近期的一些感想

嗯,今天是七夕,首先祝各位有伴的码农们甜蜜幸福。不过hikui没那么幸运,和前21年一样依然一个人过。

最近几天终于开始忙起来了,负责三个项目的某些部分。hikui初来乍到,从来没有用职业程序员的思维想过问题,这几天的忙碌让我感受颇多。

首先在工作安排上面,hikui以前以为做毕业设计时已经很正式了,但是从来都是单干,没有跟别人交流,并且只做一个项目,而正式工作时需要和美工、需求组、测试组频繁地交流,又有三个项目同时进行,这就出现了频繁地被别人打断,频繁地增加任务、修改任务以及频繁地上下文切换这些问题。在前一个礼拜所有需要完成的任务全部都靠脑子记下来的,但是大概是我老了,经常会忘记一些任务,于是有一天发现有一个界面安排给我做的我却没做。后来我参看了Get things done这本书,以及番茄工作法,又搞了个omnifocus(当然这个软件太贵,暂时只能用盗版)。主要在omnifocus上面做任务管理,感觉还是不错的,不过很多功能并没有用到,Get things done这本书也没看完。在接下来的时间里要继续学习这方面的东西来提高工作效率。

说道工作效率,现在公司使用RTX作为各部门之间的交流工具,hikui觉得这个非常的不好。一方面RTX只能运行在Windows平台上,而hikui做iOS开发,用的是OS X,所以需要两台电脑,非常不方便。hikui也试过使用crossover在OS X平台下面跑起RTX,但是crossover不是免费软件,所以用了几天就过期了。最主要的是因为RTX只有讨论组的概念而没有固定的群概念。很多时候为了发一句话或者一个文件,大家直接新建了一个讨论群,虽然是同一批人,但可能一天下来开了十几个讨论组,你完全无法分清某个讨论组曾经讨论过什么事情,没法对聊天记录进行追踪,不如使用Email来的清晰。而且因为是即时聊天工具,显然对工作的中断非常明显,所以会出现很多上下文切换的情况,如果是用番茄工作法的话,可能一天下来一直不断地在中断,效果非常不好。而hikui也有为难的地方,如果在每个25分钟之内对于RTX信息不理不睬的话,如果在此期间需求变动了,那么就意味着白做。在少中断和快速应对需求的权衡上,hikui还要下很大的功夫。

在代码层面,我有两个感想。

一是变量名非常的重要。有些同事惯用拼音来给文件和变量名取名,我觉得这非常不好。因为中文字的拼音除了拼法,还有声调之分,没有了声调,就会有严重的歧义,况且即便是声调一样,也可能是两个完全不同的意思。而用正宗的英文很少有这样的问题。所以hikui在起变量名和文件名时,都使用英语,即便不知道单词,也要到字典里去查,顺便能提高词汇量。

二是hikui之前一直希望自己写出来的代码结构清晰易读,一开始看同事写的代码,觉得他们很多地方写的太复杂。不过后来hikui意识到自己图样图森破了。因为hikui之前没有经历过需求频繁变动这个问题,有时候代码虽然很简单很易读很清晰,但是需求一改,改动就非常大。所以我也开始在代码灵活性方面下了点功夫。虽然结构变的复杂了一点,但是能够快速地应对需求的变动。当然这种付出还是有所回报的,昨天变动了一个需求,我只改了两行代码就搞定了。而如果前天我没有修改代码结构的话,恐怕又要做好几个小时的工作了。

嗯,这就是这几天的一些小小的思考,hikui还只是一个初级码农,还需要很多的历练和思考。

对iOS系统键盘的hack以及注意事项

本文写于iOS5时代,后面几个版本的iOS版本对自带控件的view结构做了很大的变化,所以不适用了。

建议少用这类的hack方法,以保证iOS更新迭代的时候不出问题。

在做东方财富通新版的时候,需求组给出的新的需求是,数字键盘使用我们自己做的,而字母键盘使用系统自带的键盘,然而这里出现的一个问题就是,系统键盘进行输入法切换的时候是没有办法切换到自带的键盘中去的。这时候就需要对系统自带键盘进行hack。

原理很简单,就是在系统键盘弹出来时找到系统键盘所在的UIView,然后贴上自己的按钮覆盖掉原先的按钮。难点在于如何找到系统键盘所在的View。

效果图:

从效果图上可见,原先系统键盘的123和地球的图标被替换成了自定义的按钮。

做法:首先在当前的ViewController中监听:UIKeyboardDidShowNotification

 

[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];

 

然后自己实现keyboardDidShow方法,在里面根据需要调用下面hackSystemKeyboard方法:

- (void)hackSystemKeyboard
{
    UIView *foundKeyboard = [self getSystemKeyboard];
    if (foundKeyboard) {
        //如果有原先存在的hack button,删之
        [self clearKeyboardHack];
        //hackButton 为123button
        //新建hackButton,根据orentation定义不同的frame
        UIButton *hackButton = [UIButton buttonWithType:UIButtonTypeCustom];
        hackButton.backgroundColor = [UIColor clearColor];
        hackButton.tag = 19100;
        if (self.isLandscape) {
            hackButton.frame = CGRectMake(3, 124, 90, 38);
            UIImage *btnBG = [UIImage imageNamed:@"hackButtonLandscape.png"];
            [hackButton setBackgroundImage:btnBG forState:UIControlStateNormal];
        }else {
            hackButton.frame = CGRectMake(2, 173, 77, 43);
            UIImage *btnBG = [UIImage imageNamed:@"hackButton.png"];
            [hackButton setBackgroundImage:btnBG forState:UIControlStateNormal];
        }
        [hackButton setShowsTouchWhenHighlighted:YES];
        [hackButton addTarget:self action:@selector(switchToNumberPad:) forControlEvents:UIControlEventTouchUpInside];
        [foundKeyboard addSubview:hackButton];
    }
}

这里要注意的是,如果这个ViewController支持横屏,则需要准备两套button图片。并且在willAnimateRotationToInterfaceOrientation中需要调用hackSystemButton这个方法。

里面比较重要的是getSystemKeyboard方法:

 

- (UIView *)getSystemKeyboard
{
    UIView *foundKeyboard = nil;
    
    UIWindow *keyboardWindow = nil;
    for (UIWindow *testWindow in [[UIApplication sharedApplication] windows])
    {
        if (![[testWindow class] isEqual:[UIWindow class]])
        {
            keyboardWindow = testWindow;
            break;
        }
    }
    if (!keyboardWindow)
        return nil;
    
    for (UIView *possibleKeyboard in [keyboardWindow subviews])
    {
        //iOS3
        if ([[possibleKeyboard description] hasPrefix:@"<UIKeyboard"])
        {
            foundKeyboard = possibleKeyboard;
            break;
        }
        else
        {
            // iOS 4 sticks the UIKeyboard inside a UIPeripheralHostView.
            if ([[possibleKeyboard description] hasPrefix:@"<UIPeripheralHostView"])
            {
                possibleKeyboard = [[possibleKeyboard subviews] objectAtIndex:0];
            }
            
            if ([[possibleKeyboard description] hasPrefix:@"<UIKeyboard"])
            {
                foundKeyboard = possibleKeyboard;
                break;
            }
        }
    }
    return foundKeyboard;
}

这里利用了iOS系统组件的Description进行View的判断,iOS 4以后的版本和之前的版本不一样,新版本中把UIKeyboard放在UIPeripheralHostView中。

最后需要注意的是,一个系统键盘在一个应用程序里面是共享的,所以如果这个ViewController中对键盘进行了hack,而在另外一个ViewController中不需要hack,那么需要在本ViewController退出的时候把Hack清理掉。但是在ViewWillDisappear中,我发现getSystemKeyboard方法寻找到的View并不是真正的keyboard,我的一个变通的办法就是在所有会导致ViewWillDisappear的步骤中插入clearKeyboardHack方法。

101年10月17日更新:

清理hack的时机应该是键盘将要隐藏前。只要在NotificationCenter中注册名为UIKeyboardWillHideNotification,收到notification的时候进行一次清理即可。

hidesBottombarWhenPushed的副作用

本文写于iOS5时代,后面几个版本的iOS版本对自带控件的view结构做了很大的变化,所以不适用了。

建议少用这类的hack方法,以保证iOS更新迭代的时候不出问题。

在UITabbarController包含的UINavigationController应用中,如果UINavigationController某一页(某个level)需要隐藏Tabbar,之前的做法是在push那一页之前,将那一页的ViewController中的hidesBottombarWhenPushed参数设为YES,这样当那一页push进UINavigationController中时,底部的Tabbar就会隐藏掉。

但是这种方法有一个潜在的而又巨大的问题,假设现在我有3个ViewController A,B,C,其中为Navigation的RootViewController,A中push B,B中push C。这时,如果我想要在B中隐藏Tabbar,而在C中显示Tabbar,根据最朴素的想法就是:

 

//SecondLevel即为B,此代码在A中进行
SecondLevelViewController *_2vc = [[SecondLevelViewController alloc]initWithNibName:@"SecondLevelViewController" bundle:nil];
    _2vc.hidesBottomBarWhenPushed = YES;
    [self.navigationController pushViewController:_2vc animated:YES];  

//ThirdLevel即为C,此代码在B中进行
ThirdLevelViewController *_3vc = [[ThirdLevelViewController alloc]initWithNibName:@"ThirdLevelViewController" bundle:nil];
    _3vc.hidesBottomBarWhenPushed = NO;
    [self.navigationController pushViewController:_3vc animated:YES];

然而很可惜,虽然A中push B能让Tabbar隐藏,但是B中push C,就没办法让Tabbar显示出来了。这是一个比较郁闷的问题,我只能猜想其中的原理是,当设置某一层hidesBottomBarWhenPushed = YES之后,UITabbarController就像不存在一样,就连调用self.tabbarController或者self.navigationController.tabbarController也应该是nil。

所以解决办法只能是手动隐藏Tabbar。

在B中,加入以下代码:

 

- (void) hideTabBar:(UITabBarController *) tabbarcontroller {
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.5];
    for(UIView *view in tabbarcontroller.view.subviews)
    {
        if([view isKindOfClass:[UITabBar class]])
        {
            [view setFrame:CGRectMake(view.frame.origin.x, 480, view.frame.size.width, view.frame.size.height)];
        } 
        else 
        {
            [view setFrame:CGRectMake(view.frame.origin.x, view.frame.origin.y, view.frame.size.width, 480)];
        }
    }
    [UIView commitAnimations];
}

- (void) showTabBar:(UITabBarController *) tabbarcontroller {

    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.5];
    for(UIView *view in tabbarcontroller.view.subviews)
    {
        NSLog(@"%@", view);

        if([view isKindOfClass:[UITabBar class]])
        {
            [view setFrame:CGRectMake(view.frame.origin.x, 431, view.frame.size.width, view.frame.size.height)];
        } 
        else 
        {
            [view setFrame:CGRectMake(view.frame.origin.x, view.frame.origin.y, view.frame.size.width, 431)];
        }
    }

    [UIView commitAnimations]; 
}

-(void)viewWillAppear:(BOOL)animated
{
   [super viewWillAppear:animated];
   [self hideTabbar:self.tabbarController];
}

-(void)viewWillDisAppear:(BOOL)animated
{
   [super viewWillDisAppear:animated];
   [self showTabbar:self.tabbarController];
}

以上代码参考了 http://stackoverflow.com/questions/5272290/how-to-hide-uitabbarcontroller

在Linux下面使用GCD

GCD(Grand Central Dispatch)是苹果在Mac OS X上面实现的一个C语言的多核编程方式。在iOS和Mac OS X多线程编程中苹果首推这个方式,主要原因是它由内核直接进行负载均衡,所以效率会比较高。再者它使用了被称为blocks的C语言扩展语法,这种语法实际上就是闭包。基于这种语法就可以写出类似javascript中的无阻塞异步方法,使代码非常易读。

苹果提供libdispatch这个库来支持GCD,并且将这个库开源。libdispatch依赖于BSD内核的一些服务,所以在FreeBSD 9.0之后进入到BSD ports中并且可以原生地使用。今天我搜索了一下,发现libdispatch在Debian系的Linux系统里面也已经有了支持,比如Ubuntu 12.04就有一个libdispatch-dev的包,安装之后就能使用GCD了。不过Linux的GCD并不是深入到内核的,而是在用户态模拟了一些BSD服务,所以只能说实现了一种语法显现。即便如此,GCD所带来的编程上的便利也足够体现出来了。

Ubuntu下,只要apt-get install libdispatch-dev就能安装所有依赖包了,这些依赖包主要包括llvm以及实现blocks语法的runtime。顺便说一下,如果要用GCD,就一定要用LLVM来编译,GCC是不行的。

安装好之后,新建一个test.c文件:

#include <dispatch/dispatch.h>

#include <err.h>
#include <stdio.h>
#include <stdlib.h>

int
main(int argc, char *argv[])
{
        dispatch_queue_t q;
        dispatch_time_t t;

        q = dispatch_get_main_queue();
        t = dispatch_time(DISPATCH_TIME_NOW, 5LL * NSEC_PER_SEC);

        // Print a message and exit after 5 seconds.
        dispatch_after(t, q, ^{
                printf("block_dispatch\n");
                exit(0);
            });

        dispatch_main();
        return (0);
}

在终端进行编译:

clang -fblocks -o test test.c -ldispatch -lBlocksRuntime

如果编译成功,说明GCD安装是成功的。(注意后面两个参数一定要加上,在FreeBSD中,-lBlocksRuntime这个参数可以不加,但是在Linux中不加会报错)。

关于GCD更多的信息,直接参考苹果的文档 https://developer.apple.com/library/mac/#documentation/Performance/Reference/GCD_libdispatch_Ref/Reference/reference.html

iOS编程新手的一些问题(2)

2、Cocoa collections的问题。

NSArray, NSDictionary, NSSet,以及这些类型的Mutable形式类型有一个共同点,就是只能接受对象作为Value。结构体和基本类型是不能保存进collections的。所以诸如

    NSMutableArray *array = [[NSMutableArray alloc]init];
    int i = 1;
    NSInteger nsi = 2;
    CGRect rect = CGRectMake(0, 0, 0, 0);
    [array addObject:1];
    [array addObject:nsi];
    [array addObject:rect];

都是不正确的。int是基本类型,CGRect是结构体,所以不能被collections接受。

这里要注意的就是NSInteger声明的变量不是对象,它只是int或者long的别名。NSInteger的定义是这样的:

 

#if __LP64__ || TARGET_OS_EMBEDDED || TARGET_OS_IPHONE || TARGET_OS_WIN32 || NS_BUILD_32_LIKE_64
typedef long NSInteger;
#else
typedef int NSInteger;
#endif

另外的CGFloat也是一样的。

所以要将这种东西存储到collections里面,需要包装成对象。比如数字类型的东西都包装成NSNumber,CG等类型的结构体包装成NSValue,然后存入collections里面,读取的时候,调用相应的xxValue方法就能还原成基本类型。

3、使用Delegate模式需要小心:

我上次调试别人的程序时,几乎所有的崩溃都是因为使用delegate模式不当造成的。Java、C#使用delegate没有什么困难,而Objective-C里面,在没有ARC的情况下,需要手动管理内存,问题就出现了。

现在有A、B两个对象,A作为B的delegate,即B.delegate = A。假设B需要花很长的时间做完工作,做完工作之后调用A实现的delegate方法,而此时A已经被销毁了。这时B.delegate依旧指向A原来的地址而不是nil,B就会向原来地址的已经被销毁的对象发送消息。这时程序就崩溃了。

所以当B.delegate = A的情况下,在A会被销毁的时候,必须设置B.delegate = nil。这样,即便B要调用delegate方法时,A已经被销毁,B也是像nil发送消息。在Objective -C中,这样是完全合法的,并且会什么都不做。

很多时候,给B设置delegate的工作是在A内部做的,即在A的某个方法里面,有B.delegate = self这样的语句。所以A的dealloc方法里面需要增加

if (B.delegate==self){
    B.delegate = nil;
}

这样的语句。

4、viewDidUnload需要做事:

很多人在写代码的时候,总是忽略viewDidUnload这个方法。但是在使用xcode生成的ViewController模板里面又一直会看到viewDidUnload这个方法。模板里面明显有两行注释:

 

// Release any retained subviews of the main view.

// e.g. self.myOutlet = nil;

这是很重要的信息。它的意思是说,如果你在ViewController的对象里面retain了一些subview,就需要在这里将他们释放掉。

如果不写的话,乍看程序不会有什么问题,既不报错也不崩溃。但是在系统内存紧缺时,你的程序被系统kill的概率就非常的高了。

iOS在内存紧缺时发送memory warning。收到这个信号后,你的程序会自动调用现在还活着的ViewController的didReceiveMemoryWarning方法,并且如果你的ViewController并不在当前显示状态(可能在NavigationController栈里面),就会销毁这个ViewController.view。此时,view是retain过它的subview的,而如果viewController也retain过里面某些subview,那么这些subview的retainCount==2。系统销毁view时,对里面所有的subview进行release,此时被ViewController retain过的subview的retainCount==1,系统无法销毁。接下来,系统调用ViewController的viewDidUnload,如果此时你做了self.someSubview = nil这件事,那么对这个subview来说,又进行了release(由于点表达式是setter方法,参加前篇),此时retainCount==0,就可以被销毁了。如果什么都没做,那么这个Subview就不会被销毁,额外增加了内存负担,就提高被系统杀死的风险了。

这里另外要注意的是,写self.someSubview = nil是在someSubview为ViewController的@property(retain)时才能用的,如果不是property,而又在程序里retain了,那么viewDidUnload必须写成:

 

[someSubview release];
someSubview = nil;

切不可直接写someSubview = nil,否则就内存泄漏了。

iOS编程新手的一些问题(1)

这几天加班改另外几个已经离职的实习生遗留下来的代码,真是吐血三升啊。不知道这个公司为什么招这么多没有什么经验的iOS实习生,写出来的程序几秒钟就crash一次。

Objective-C因为只有苹果在用,所以接触的机会比较少,大多数人都是从别的语言转过来的,我也是,所以我也犯过很多错误,在错误中渐渐学会了Objective-C。

这里说一下这几天改代码发现的常见错误:

0、retain, release问题:

虽然现在有ARC了,不需要写retain, release,但是由于这个只能在iOS5以上才能实现全部功能,所以现在很多项目依然使用手动内存管理。retain, release的原理很简单,当你创建一个对象之后,什么时候把它销毁呢?答案是在没有人用它的时候就可以销毁了。每个对象有一个计数器叫做retainCount。当对象被创建时,retainCount为1,如果另外有一段代码要用这个对象一段时间,除了获取对象地址之外,还要向对象发送retain消息,这时候retainCount就会+1,此时,即便对象原先的拥有者释放了对象,该对象也不会被销毁。所谓的释放就是向对象发送release消息,retainCount会-1,表明又有一个人不需要用它了,当retainCount==0时,就说明没有人要用它了,便销毁了。

手动管理内存的Objective-C对此必须斤斤计较,否则很容易出现内存泄漏情况。很多Objective-C新手经常忘记release,导致大量的内存泄漏。在手机开发中是很危险的,因为程序会被系统kill掉,看起来就像崩溃一样。

初学者容易犯的错误是:没有retain之后没有release掉。导致大量内存泄漏。

Objective-C的内存管理有个规则:

当你是对象的创造者时(使用alloc, copy方法),在你不用它时,向它发送release。

当你需要保留不是你创造的对象一段时间时,需要向这个对象发送retain,并且用完后向它发送release。如果不需要保留一段时间,则不需要发送retain,也不需要发送release。

当一个函数需要返回一个对象时,需要将这个对象标记为autorelease。

就这三点。

1、getter, setter问题:

用Java的时候写getter setter再容易不过了,事实上只要用IDE生成一下就行了。但是由于Objective-C需要管理内存,所以没那么简单。

setter的写法是这样的:

 

-(void)setSomething:(SomeClass *)anObj
{
    if(anObject != _something){
        [_something release];
        _something = [anObj retain];
    }
}

因为setter和getter都是针对成员变量的,所以要拥有成员变量一段时间,加之你用setter设置成员变量,说明要赋给成员变量的那个anObj不是你创造的,所以需要retain。但是这还没完。当你第二次用setter设置成员变量时,原先的那个对象需要被释放掉,否则原先的对象的retainCount无法清零了。所以首先要release掉原来的,在retain新的。那么第一次使用setter时,something是nil,岂不是变成[nil release]了?没错,这在objective-c里面完全合法,向nil发送任何消息都返回nil且没有错误。

主要问题还不是在这里,现在的Objective C增加了@property和@synthesize指令,说白了就是自动帮你生成setter和getter,并且用了这两个指令之后,可以用点表达式进行getter和setter调用,看起来就像Java一样:

 

anObject.someProperty = newPropertyObject; //相当于调用[anObject setSomeProperty];
tempProperty = anObject.someProperty; //相当于调用[anObject someProperty];

而这个情况放在self的情况下就容易让人疑惑,在Java里面,在对象方法里面访问成员变量可以直接写someProperty,也可以写成this.someProperty,两者几乎完全一样。然而在Objective-C里面则完全不同。

self.someProperty = xxx时,实际上是执行了[self setSomeProperty],而这个方法里面包含了内存管理的方法。如果直接调用someProperty = xxx时,则是直接将someProperty指向xxx的地址,并没有做内存管理,此时就发生内存泄漏了。

初学者往往会被这个迷惑,从而写出

 

[someProperty release];
self.someProperty = nil;

这样的语句(多数在dealloc中)。编码者原来的意图是好的,将这个对象释放掉之后,将指针归到nil。然而这样写是错误的,因为在调用self.someProperty = xxx时,又会发生一次[someProperty release],但是此时someProperty已经被上面一句代码释放了,再释放一次就报错了。另外,

 

self.someProperty = [[SomeClass alloc]init];

这样的写法也是错误的,因为[[SomeClass alloc]init]时,新对象的retainCount==1,self.someProperty = xxx的语句执行了setter,又被retain了一次,此时retainCount==2了。所以之后就总是多1,无法清零了。

正确的写法是:

 

SomeClass *temp = [[SomeClass alloc]init];
self.someProperty = temp;
[temp release];

或者

self.someProperty = [[[SomeClass alloc]init]autorelease];

或者

someProperty = [[SomeClass alloc]init];

实际上,苹果的官方模板在写@synthesize时,特地写成了:

 

@synthesize someProperty = _someProperty;

这样一来,就只能使用self.someProperty和_someProperty了,someProperty是无效的,这样可以避免混淆。

CSS做下拉列表的倒三角方法

最近做练习的时候要用到下拉列表,上面有个倒三角。当时用贴图的方法做的。因为我是一个新手,所以不知道别人普遍怎么做,今天看了一眼淘宝的css,真是出乎意料啊。

效果图:

这个效果的原理很简单:对一个元素的四个面都设置border时,不同方向上的border衔接之处是呈45度切面的,只要随便拿一个元素加上一个很粗的border就可以看出来了。那么当元素高宽都为0,但是有一个非常粗的border的时候,就会出现4个三角形。

div{
            border-color: #666666 blue red;
            border-style: solid;
            border-width: 14px;
            font-size:0;
            height: 0px;
            line-height: 0;
            width: 0px;   
        }
<body>
    <div></div>
</body>

效果:

那么,只要把左右和下面的border颜色设为和背景色一样,就出现倒三角效果了。

不过显然,这个办法限制比较大,只有在纯色箭头和纯色背景时才能用。

Hello world

哥果然是到處留情,在新浪、百度、QQ空間都有博客,自己還有一個http://herkuang.info。現在看到這個網站也心潮澎湃地註冊一下。

現在先在這裡留個種,以後看看我的VPS情况,再决定在哪裡寫文章。

EOF