注:本文是对 Colin Wheeler 的 Understanding the Objective-C Runtime 的翻译。
初学 Objective-C(以下简称ObjC) 的人很容易忽略一个 ObjC 特性 —— ObjC Runtime。这是因为这门语言很容易上手,几个小时就能学会怎么使用,所以程序员们往往会把时间都花在了解 Cocoa 框架以及调整自己的程序的表现上。然而 Runtime 应该是每一个 ObjC 都应该要了解的东西,至少要理解编译器会把
[target doMethodWith:var1];
编译成:
objc_msgSend(target,@selector(doMethodWith:),var1);
这样的语句。理解 ObjC Runtime 的工作原理,有助于你更深入地去理解 ObjC 这门语言,理解你的 App 是怎样跑起来的。我想所有的 Mac/iPhone 开发者,无论水平如何,都会从中获益的。
ObjC Runtime 的代码是开源的,可以从这个站点下载: opensource.apple.com。
这个是所有开源代码的链接: http://www.opensource.apple.com/source/
这个是ObjC rumtime 的源代码: http://www.opensource.apple.com/source/objc4/
4应该代表的是build版本而不是语言版本,现在是ObjC 2.0
ObjC 是一种面向runtime(运行时)的语言,也就是说,它会尽可能地把代码执行的决策从编译和链接的时候,推迟到运行时。这给程序员写代码带来很大的灵活性,比如说你可以把消息转发给你想要的对象,或者随意交换一个方法的实现之类的。这就要求 runtime 能检测一个对象是否能对一个方法进行响应,然后再把这个方法分发到对应的对象去。我们拿 C 来跟 ObjC 对比一下。在 C 语言里面,一切从 main 函数开始,程序员写代码的时候是自上而下地,一个 C 的结构体或者说类吧,是不能把方法调用转发给其他对象的。举个栗子:
#include < stdio.h >
int main(int argc, const char **argv[]) { printf("Hello World!"); return 0; }
这段代码被编译器解析,优化后,会变成一堆汇编代码:
.text
.align 4,0x90
.globl _main
_main:
Leh_func_begin1:
pushq %rbp
Llabel1:
movq %rsp, %rbp
Llabel2:
subq $16, %rsp
Llabel3:
movq %rsi, %rax
movl %edi, %ecx
movl %ecx, -8(%rbp)
movq %rax, -16(%rbp)
xorb %al, %al
leaq LC(%rip), %rcx
movq %rcx, %rdi
call _printf
movl $0, -4(%rbp)
movl -4(%rbp), %eax
addq $16, %rsp
popq %rbp
ret
Leh_func_end1:
.cstring
LC:
.asciz "Hello World!"
然后,再链接 include 的库,完了生成可执行代码。对比一下 ObjC,当我们初学这门语言的时候教程是这么说滴:用中括号括起来的语句,
[self doSomethingWithVar:var1];
被编译器编译之后会变成:
objc_msgSend(self,@selector(doSomethingWithVar:),var1);
一个 C 方法,传入了三个变量,self指针,要执行的方法 @selector(doSomethingWithVar:) 还有一个参数 var1。但是在这之后就不晓得发生什么了。
ObjC Runtime 其实是一个 Runtime 库,基本上用 C 和汇编写的,这个库使得 C 语言有了面向对象的能力(脑中浮现当你乔帮主参观了施乐帕克的 SmallTalk 之后嘴角一抹浅笑)。这个库做的事前就是加载类的信息,进行方法的分发和转发之类的。
再往下深谈之前咱先介绍几个术语。
目前说来Runtime有两种,一个 Modern Runtime 和一个 Legacy Runtime。Modern Runtime 覆盖了64位的Mac OS X Apps,还有 iOS Apps,Legacy Runtime 是早期用来给32位 Mac OS X Apps 用的,也就是可以不用管就是了。
一种 Instance Method,还有 Class Method。instance method 就是带“-”号的,需要实例化才能用的,如 :
-(void)doFoo;
[aObj doFoot];
Class Method 就是带“+”号的,类似于静态方法可以直接调用:
+(id)alloc;
[ClassName alloc];
这些方法跟 C 函数一样,就是一组代码,完成一个比较小的任务。
-(NSString *)movieTitle
{
return @"Futurama: Into the Wild Green Yonder";
}
一个 Selector 事实上是一个 C 的结构体,表示的是一个方法。定义是:
typedef struct objc_selector *SEL;
使用起来就是:
SEL aSel = @selector(movieTitle);
这样可以直接取一个selector,如果是传递消息(类似于C的方法调用)就是:
[target getMovieTitleForObject:obj];
在 ObjC 里面,用'[]'括起来的表达式就是一个消息。包括了一个 target,就是要接收消息的对象,一个要被调用的方法还有一些你要传递的参数。类似于 C 函数的调用,但是又有所不同。事实上上面这个语句你仅仅是传递了 ObjC 消息,并不代表它就会一定被执行。target 这个对象会检测是谁发起的这个请求,然后决策是要执行这个方法还是其他方法,或者转发给其他的对象。
Class 的定义是这样的:
typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;
我们可以看到这里这里有两个结构体,一个类结构体一个对象结构体。所有的 objc_object 对象结构体都有一个 isa 指针,这个 isa 指向它所属的类,在运行时就靠这个指针来检测这个对象是否可以响应一个 selector。完了我们看到最后有一个 id 指针。这个指针其实就只是用来代表一个 ObjC 对象,有点类似于 C++ 的泛型。当你拿到一个 id 指针之后,就可以获取这个对象的类,并且可以检测其是否响应一个 selector。这就是对一个 delegate 常用的调用方式啦。这样说还有点抽象,我们看看 LLVM/Clang 的文档对 Blocks 的定义:
struct Block_literal_1 {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 {
unsigned long int reserved; // NULL
unsigned long int size; // sizeof(struct Block_literal_1)
// optional helper functions
void (*copy_helper)(void *dst, void *src);
void (*dispose_helper)(void *src);
} *descriptor;
// imported variables
};
可以看到一个 block 是被设计成一个对象的,拥有一个 isa 指针,所以你可以对一个 block 使用 retain, release, copy 这些方法。
接下来看看啥是IMP。
typedef id (*IMP)(id self,SEL _cmd,...);
一个 IMP 就是一个函数指针,这是由编译器生成的,当你发起一个 ObjC 消息之后,最终它会执行的那个代码,就是由这个函数指针指定的。
OK,回过头来看看一个 ObjC 的类。举一个栗子:
@interface MyClass : NSObject {
//vars
NSInteger counter;
}
//methods
-(void)doFoo;
@end
定义一个类我们可以写成如上代码,而在运行时,一个类就不仅仅是上面看到的这些东西了:
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
可以看到运行时一个类还关联了它的父类指针,类名,成员变量,方法,cache 还有附属的 protocol。
上面我提到过一个 ObjC 类同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做 标签类 元类(Meta Class)的东西。当你发出一个消息的时候,比方说
[NSObject alloc];
你事实上是把这个消息发给了一个类对象(Class Object),这个类对象必须是一个 Meta Class 的实例,而这个 Meta Class 同时也是一个根 MetaClass 的实例。当你继承了 NSObject 成为其子类的时候,你的类指针就会指向 NSObject 为其父类。但是 Meta Class 不太一样,所有的 Meta Class 都指向根 Meta Class 为其父类。一个 Meta Class 持有所有能响应的方法。所以当 [NSObject alloc] 这条消息发出的时候,objc_msgSend() 这个方法会去 NSObject 它的 Meta Class 里面去查找是否有响应这个 selector 的方法,然后对 NSObject 这个类对象执行方法调用。
初学 Cocoa 开发的时候,多数教程都要我们继承一个类比方 NSObject,然后我们就开始 Coding 了。比方说:
MyObject *object = [[MyObject alloc] init];
这个语句用来初始化一个实例,类似于 C++ 的 new 关键字。这个语句首先会执行 MyObject 这个类的 +alloc 方法,Apple 的官方文档是这样说的:
The isa instance variable of the new instance is initialized to a data structure that describes the class; memory for all other instance variables is set to 0.
新建的实例中,isa 成员变量会变初始化成一个数据结构体,用来描述所指向的类。其他的成员变量的内存会被置为0.
所以继承 Apple 的类我们不仅是获得了很多很好用的属性,而且也继承了这种内存分配的方法。
刚刚我们看到 runtime 里面有一个指针叫 objc_cache *cache,这是用来缓存方法调用的。现在我们知道一个实例对象被传递一个消息的时候,它会根据 isa 指针去查找能够响应这个消息的对象。但是实际上我们在用的时候,只有一部分方法是常用的,很多方法其实很少用或者根本用不到。比如一个object你可能从来都不用copy方法,那我要是每次调用的时候还去遍历一遍所有的方法那就太笨了。于是 cache 就应运而生了,每次你调用过一个方法,之后,这个方法就会被存到这个 cache 列表里面去,下次调用的时候 runtime 会优先去 cache 里面查找,提高了调用的效率。举一个栗子:
MyObject *obj = [[MyObject alloc] init]; // MyObject 的父类是 NSObject
@implementation MyObject -(id)init { if(self = [super init]){ [self setVarA:@”blah”]; } return self; } @end
这段代码是这样执行的:
OK,这就是一个很简单的初始化过程,在 NSObject 类里面,alloc 和 init 没做什么特别重大的事情,但是,ObjC 特性允许你的 alloc 和 init 返回的值不同,也就是说,你可以在你的 init 函数里面做一些很复杂的初始化操作,但是返回出去一个简单的对象,这就隐藏了类的复杂性。再举个栗子:
#import < Foundation/Foundation.h>
@interface MyObject : NSObject { NSString *aString; }
@property(retain) NSString *aString;
@end
@implementation MyObject
-(id)init { if (self = [super init]) { [self setAString:nil]; } return self; }
@synthesize aString;
@end
int main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
id obj1 = [NSMutableArray alloc]; id obj2 = [[NSMutableArray alloc] init];
id obj3 = [NSArray alloc]; id obj4 = [[NSArray alloc] initWithObjects:@"Hello",nil];
NSLog(@"obj1 class is %@",NSStringFromClass([obj1 class])); NSLog(@"obj2 class is %@",NSStringFromClass([obj2 class]));
NSLog(@"obj3 class is %@",NSStringFromClass([obj3 class])); NSLog(@"obj4 class is %@",NSStringFromClass([obj4 class]));
id obj5 = [MyObject alloc]; id obj6 = [[MyObject alloc] init];
NSLog(@"obj5 class is %@",NSStringFromClass([obj5 class])); NSLog(@"obj6 class is %@",NSStringFromClass([obj6 class]));
[pool drain]; return 0; }
如果你是ObjC的初学者,那么你很可能会认为这段代码执的输出会是:
NSMutableArray
NSMutableArray
NSArray
NSArray
MyObject
MyObject
但事实上是这样的:
obj1 class is __NSPlaceholderArray
obj2 class is NSCFArray
obj3 class is __NSPlaceholderArray
obj4 class is NSCFArray
obj5 class is MyObject
obj6 class is MyObject
这是因为 ObjC 是允许运行 +alloc 返回一个特定的类,而 init 方法又返回一个不同的类的。可以看到 NSMutableArray 是对普通数组的封装,内部实现是复杂的,但是对外隐藏了复杂性。
这个方法做的事情不少,举个栗子:
[self printMessageWithString:@"Hello World!"];
这句语句被编译成这样:
objc_msgSend(self,@selector(printMessageWithString:),@"Hello World!");
这个方法先去查找 self 这个对象或者其父类是否响应 @selector(printMessageWithString:),如果从这个类的方法分发表或者 cache 里面找到了,就调用它对应的函数指针。如果找不到,那就会执行一些其他的东西。步骤如下:
在编译的时候,你定义的方法比如:
-(int)doComputeWithNum:(int)aNum
会编译成:
int aClass_doComputeWithNum(aClass *self,SEL _cmd,int aNum)
然后由 runtime 去调用指向你的这个方法的函数指针。那么之前我们说你发起消息其实不是对方法的直接调用,其实 Cocoa 还是提供了可以直接调用的方法的:
// 首先定义一个 C 语言的函数指针 int (computeNum *)(id,SEL,int);
// 使用 methodForSelector 方法获取对应与该 selector 的杉树指针,跟 objc_msgSend 方法拿到的是一样的 // methodForSelector 这个方法是 Cocoa 提供的,不是 ObjC runtime 库提供的 computeNum = (int (*)(id,SEL,int))[target methodForSelector:@selector(doComputeWithNum:)];
// 现在可以直接调用该函数了,跟调用 C 函数是一样的 computeNum(obj,@selector(doComputeWithNum:),aNum);
如果你需要的话,你可以通过这种方式你来确保这个方法一定会被调用。
在 ObjC 这门语言中,发送消息给一个并不响应这个方法的对象,是合法的,应该也是故意这么设计的。换句话说,我可以对任意一个对象传递任意一个消息(看起来有点像对任意一个类调用任意一个方法,当然事实上不是),当然如果最后找不到能调用的方法就会 Crash 掉。
Apple 设计这种机制的原因之一就是——用来模拟多重继承(ObjC 原生是不支持多重继承的)。或者你希望把你的复杂设计隐藏起来。这种转发机制是 Runtime 非常重要的一个特性,大概的步骤如下:
这就给了程序员一次机会,可以告诉 runtime 在找不到改方法的情况下执行什么方法。举个栗子,先定义一个函数:
void fooMethod(id obj, SEL _cmd)
{
NSLog(@"Doing Foo");
}
完了重载 resolveInstanceMethod 方法:
+(BOOL)resolveInstanceMethod:(SEL)aSEL
{
if(aSEL == @selector(doFoo:)){
class_addMethod([self class],aSEL,(IMP)fooMethod,"v@:");
return YES;
}
return [super resolveInstanceMethod];
}
其中 "v@:" 表示返回值和参数,这个符号涉及 Type Encoding,可以参考Apple的文档 ObjC Runtime Guide。
接下来 Runtime 会调用 - (id)forwardingTargetForSelector:(SEL)aSelector 方法。
这就给了程序员第二次机会,如果你没办法在自己的类里面找到替代方法,你就重载这个方法,然后把消息转给其他的Object。
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
这样你就可以把消息转给别人了。当然这里你不能 return self,不然就死循环了=.=
-(void)forwardInvocation:(NSInvocation *)invocation { SEL invSEL = invocation.selector;
if([altObject respondsToSelector:invSEL]) { [invocation invokeWithTarget:altObject]; } else { [self doesNotRecognizeSelector:invSEL]; }
}
默认情况下 NSObject 对 forwardInvocation 的实现就是简单地执行 -doesNotRecognizeSelector: 这个方法,所以如果你想真正的在最后关头去转发消息你可以重载这个方法(好折腾-.-)。
原文后面介绍了 Non Fragile ivars (Modern Runtime), Objective-C Associated Objects 和 Hybrid vTable Dispatch。鉴于一是底层的可以不用理会,一是早司空见惯的不用详谈,还有一个是很简单的,就是一个建立在方法分发表里面填入默认常用的 method,所以有兴趣的读者可以自行查阅原文,这里就不详谈鸟。
在不使用 ARC 的时候,内存要自己管理,这时重复或过早释放都有可能导致 Crash。
NSObject * aObj = [[NSObject alloc] init]; [aObj release];
NSLog(@"%@", aObj);
aObj 这个对象已经被释放,但是指针没有置空,这时访问这个指针指向的内存就会 Crash。
[aObj release];
aObj = nil;
由于ObjC的特性,调用 nil 指针的任何方法相当于无作用,所以即使有人在使用这个指针时没有判断至少还不会挂掉。
在ObjC里面,一切基于 NSObject 的对象都使用指针来进行调用,所以在无法保证该指针一定有值的情况下,要先判断指针非空再进行调用。
if (aObj) {
//...
}
常见的如判断一个字符串是否为空:
if (aString && aString.length > 0) {//...}
有些时候不能知道自己创建的对象什么时候要进行释放,可以使用 autoRelease,但是不鼓励使用。因为 autoRelease 的对象要等到最近的一个 autoReleasePool 销毁的时候才会销毁,如果自己知道什么时候会用完这个对象,当然立即释放效率要更高。如果一定要用 autoRelease 来创建大量对象或者大数据对象,最好自己显式地创建一个 autoReleasePool,在使用后手动销毁。以前要自己手动初始化 autoReleasePool,现在可以用以下写法:
@autoreleasepool{
for (int i = 0; i < 100; ++i) {
NSObject * aObj = [[[NSObject alloc] init] autorelease];
//....
}
}
NSMutableArray/NSMutableDictionary/NSMutableSet 等类下标越界,或者 insert 了一个 nil 对象。
一个固定数组有一块连续内存,数组指针指向内存首地址,靠下标来计算元素地址,如果下标越界则指针偏移出这块内存,会访问到野数据,ObjC 为了安全就直接让程序 Crash 了。
而 nil 对象在数组类的 init 方法里面是表示数组的结束,所以使用 addObject 方法来插入对象就会使程序挂掉。如果实在要在数组里面加入一个空对象,那就使用 NSNull。
[array addObject:[NSNull null]];
使用数组时注意判断下标是否越界,插入对象前先判断该对象是否为空。
if (aObj) {
[array addObject:aObj];
}
可以使用 Cocoa 的 Category 特性直接扩展 NSMutable 类的 Add/Insert 方法。比如:
@interface NSMutableArray (SafeInsert) -(void) safeAddObject:(id)anObject; @end
@implementation NSMutableArray (SafeInsert) -(void) safeAddObject:(id)anObject { if (anObject) { [self addObject:anObject]; } } @end
这样,以后在工程里面使用 NSMutableArray 就可以直接使用 safeAddObject 方法来规避 Crash。
ObjC 的方法调用跟 C++ 很不一样。 C++ 在编译的时候就已经绑定了类和方法,一个类不可能调用一个不存在的方法,否则就报编译错误。而 ObjC 则是在 runtime 的时候才去查找应该调用哪一个方法。
这两种实现各有优劣,C++ 的绑定使得调用方法的时候速度很快,但是只能通过 virtual 关键字来实现有限的动态绑定。而对 ObjC 来说,事实上他的实现是一种消息传递而不是方法调用。
[aObj aMethod];
这样的语句应该理解为,像 aObj 对象发送一个叫做 aMethod 的消息,aObj 对象接收到这个消息之后,自己去查找是否能调用对应的方法,找不到则上父类找,再找不到就 Crash。由于 ObjC 的这种特性,使得其消息不单可以实现方法调用,还能紧系转发,对一个 obj 传递一个 selector 要求调用某方法,他可以直接不理会,转发给别的 obj 让别的 obj 来响应,非常灵活。
[self methodNotExists];
调用一个不存在的方法,可以编译通过,运行时直接挂掉,报 NSInvalidArgumentException 异常:
-[WSMainViewController methodNotExist]: unrecognized selector sent to instance 0x1dd96160
2013-10-23 15:49:52.167 WSCrashSample[5578:907] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[WSMainViewController methodNotExist]: unrecognized selector sent to instance 0x1dd96160'
像这种类型的错误通常出现在使用 delegate 的时候,因为 delegate 通常是一个 id 泛型,所以 IDE 也不会报警告,所以这种时候要用 respondsToSelector 方法先判断一下,然后再进行调用。
if ([self respondsToSelector:@selector(methodNotExist)]) {
[self methodNotExist];
}
可能由于强制类型转换或者强制写内存等操作,CPU 执行 STMIA 指令时发现写入的内存地址不是自然边界,就会硬件报错挂掉。iPhone 5s 的 CPU 从32位变成64位,有可能会出现一些字节对齐的问题导致 Crash 率升高的。
char *mem = malloc(16); // alloc 16 bytes of data
double *dbl = mem + 2;
double set = 10.0;
*dbl = set;
像上面这段代码,执行到
*dbl = set;
这句的时候,报了 EXC_BAD_ACCESS(code=EXC_ARM_DA_ALIGN) 错误。
要了解字节对齐错误还需要一点点背景知识,知道的童鞋可以略过直接看后面了。
背景知识
计算机最小数据单位是bit(位),也就是0或1。
而内存空间最小单元是byte(字节),一个byte为8个bit。
内存地址空间以byte划分,所以理论上访问内存地址可以从任意byte开始,但是事实上我们不是直接访问硬件地址,而是通过操作系统的虚拟内存地址来访问,虚拟内存地址是以字为单位的。一个32位机器的字长就是32位,所以32位机器一次访问内存大小就是4个byte。再者为了性能考虑,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
举一个栗子:
struct foo {
char aChar1;
short aShort;
char aChar2;
int i;
};
上面这个结构体,在32位机器上,char 长度为8位,占一个byte,short 占2个byte, int 4个byte。
如果内存地址从 0 开始,那么理论上顺序分配的地址应该是:
aChar1 0x00000000
aShort 0x00000001
aChar2 0x00000003
i 0x00000004
但是事实上编译后,这些变量的地址是这样的:
aChar1 0x00000000
aShort 0x00000002
aChar2 0x00000004
i 0x00000008
这就是 aChar1 和 aChar2 都被做了内存对齐优化,都变成 2 byte 了。
char *mem = malloc(16); // alloc 16 bytes of data
double *dbl = mem + 2;
double set = 10.0;
memcpy(dbl, &set, sizeof(set));
改用 memcpy 之后运行就不会有问题了,这是因为 memcpy 自己的实现就已经做了字节对齐的优化了。我们来看glibc2.5中的memcpy的源码:
void *memcpy (void *dstpp, const void *srcpp, size_t len) {
unsigned long int dstp = (long int) dstpp; unsigned long int srcp = (long int) srcpp; if (len >= OP_T_THRES) { len -= (-dstp) % OPSIZ; BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ); PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len); WORD_COPY_FWD (dstp, srcp, len, len); } BYTE_COPY_FWD (dstp, srcp, len); return dstpp;
}
分析这个函数,首先比较一下需要拷贝的内存块大小,如果小于 OP_T_THRES (这里定义为 16),则直接字节拷贝就完了,如果大于这个值,视为大内存块拷贝,采用优化算法。
len -= (-dstp) % OPSIZ; BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);
// #define OPSIZ (sizeof(op_t)) // enum op_t
OPSIZE 是 op_t 的长度,op_t 是字的类型,所以这里 OPSIZE 是获取当前平台的字长。
dstp 是内存地址,内存地址是按byte来算的,对内存地址 unsigned long 取负数再模 OPSIZE 得到需要对齐的那部分数据的长度,然后用字节拷贝做内存对齐。取负数是因为要以dstp的地址作为起点来进行复制,如果直接取模那就变成0作为起点去做运算了。
对 BYTE_COPY_FWD 这个宏的源码有兴趣的同学可以看看这篇:BYTE_COPY_FWD 源码解析(感谢 @raincai 同学提醒)
这样对齐了之后,再做大数据量部分的拷贝:
PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);
看这个宏的源码,尽可能多地作页拷贝,剩下的大小会写入len变量。
///////////////////////////////////////////////// #if PAGE_COPY_THRESHOLD
#include <assert.h>
#define PAGE_COPY_FWD_MAYBE(dstp, srcp, nbytes_left, nbytes)
do
{
if ((nbytes) >= PAGE_COPY_THRESHOLD &&
PAGE_OFFSET ((dstp) - (srcp)) == 0)
{
/* The amount to copy is past the threshold for copying
pages virtually with kernel VM operations, and the
source and destination addresses have the same alignment. /
size_t nbytes_before = PAGE_OFFSET (-(dstp));
if (nbytes_before != 0)
{
/ First copy the words before the first page boundary. */
WORD_COPY_FWD (dstp, srcp, nbytes_left, nbytes_before);
assert (nbytes_left == 0);
nbytes -= nbytes_before;
}
PAGE_COPY_FWD (dstp, srcp, nbytes_left, nbytes);
}
} while (0)/* The page size is always a power of two, so we can avoid modulo division. */ #define PAGE_OFFSET(n) ((n) & (PAGE_SIZE - 1))
#else
#define PAGE_COPY_FWD_MAYBE(dstp, srcp, nbytes_left, nbytes) /* nada */
#endif
PAGE_COPY_FWD 的宏定义:
#define PAGE_COPY_FWD ( dstp,
srcp,
nbytes_left,
nbytes
)
Value:
((nbytes_left) = ((nbytes) - \
(__vm_copy (__mach_task_self (), \
(vm_address_t) srcp, trunc_page (nbytes), \
(vm_address_t) dstp) == KERN_SUCCESS \
? trunc_page (nbytes) \
: 0)))
页拷贝剩余部分,再做一下字拷贝:
#define WORD_COPY_FWD ( dst_bp,
src_bp,
nbytes_left,
nbytes
)
Value:
do \
{ \
if (src_bp % OPSIZ == 0) \
_wordcopy_fwd_aligned (dst_bp, src_bp, (nbytes) / OPSIZ); \
else \
_wordcopy_fwd_dest_aligned (dst_bp, src_bp, (nbytes) / OPSIZ); \
src_bp += (nbytes) & -OPSIZ; \
dst_bp += (nbytes) & -OPSIZ; \
(nbytes_left) = (nbytes) % OPSIZ; \
} while (0)
再再最后就是剩下的一点数据量了,直接字节拷贝结束。memcpy 可以用来解决内存对齐问题,同时对于大数据量的内存拷贝,使用 memcpy 效率要高很多,就因为做了页拷贝和字拷贝的优化。
char *mem = malloc(16); // alloc 16 bytes of data
double *dbl = mem + 4;
double set = 10.0;
*dbl = set;
ARM Hacking: EXC_ARM_DA_ALIGN exception
一般情况下应用程序是不需要考虑堆和栈的大小的,总是当作足够大来使用就能满足一般业务开发。但是事实上堆和栈都不是无上限的,过多的递归会导致栈溢出,过多的 alloc 变量会导致堆溢出。
不得不说 Cocoa 的内存管理优化做得挺好的,单纯用 C++ 在 Mac 下编译后执行以下代码,递归 174671 次后挂掉:
#include <iostream> #include <stdlib.h>
void test(int i) { void* ap = malloc(1024); std::cout << ++i << "\n"; test(i); }
int main() { std::cout << "start!" << "\n"; test(0); return 0; }
而在 iOS 上执行以下代码则怎么也不会挂,连 memory warning 都没有:
- (void)stackOverFlow:(int)i {
char * aLeak = malloc(1024); NSLog(@"try %d", ++i); [self stackOverFlow:i];
}
而且如果 malloc 的大小改成比 1024 大的如 10240,其内存占用的增长要远慢于 1024。这大概要归功于 Cocoa 的 Flyweight 设计模式,不过暂时还没能真的理解到其优化原理,猜测可能是虽然内存空间申请了但是一直没用到,针对这种循环 alloc 的场景,做了记录,等到用到内存空间了才真正给出空间。
iOS 内存布局如下图所示:
在应用程序分配的内存空间里面,最低地址位是固定的代码段和数据段,往上是堆,用来存放全局变量,对于 ObjC 来说,就是 alloc 出来的变量,都会放进这里,堆不够用的时候就会往上申请空间。最顶部高地址位是栈,局部的基本类型变量都会放进栈里。 ObjC 的对象都是以指针进行操控的,局部变量的指针都在栈里,全局的变量在堆里,而无论是什么指针,alloc 出来的都在堆里,所以 alloc 出来的变量一定要记得 release。
对于 autorelease 变量来说,每个函数有一个对应的 autorelease pool,函数出栈的时候 pool 被销毁,同时调用这个 pool 里面变量的 dealloc 函数来实现其内部 alloc 出来的变量的释放。
这个应该是全平台都会遇到的问题了。当某个对象会被多个线程修改的时候,有可能一个线程访问这个对象的时候另一个线程已经把它删掉了,导致 Crash。比较常见的是在网络任务队列里面,主线程往队列里面加入任务,网络线程同时进行删除操作导致挂掉。
这个真要写比较完整的并发操作的例子就有点复杂了。
普通的锁,加锁的时候 lock,解锁调用 unlock。
- (void)addPlayer:(Player *)player { if (player == nil) return; NSLock* aLock = [[NSLock alloc] init]; [aLock lock];
[players addObject:player]; [aLock unlock];
} }
可以使用标记符 @synchronized 简化代码:
- (void)addPlayer:(Player *)player {
if (player == nil) return;
@synchronized(players) {
[players addObject:player];
}
}
使用普通的 NSLock 如果在递归的情况下或者重复加锁的情况下,自己跟自己抢资源导致死锁。Cocoa 提供了 NSRecursiveLock 锁可以多次加锁而不会死锁,只要 unlock 次数跟 lock 次数一样就行了。
多数情况下锁是不需要关心什么条件下 unlock 的,要用的时候锁上,用完了就 unlock 就完了。Cocoa 提供这种条件锁,可以在满足某种条件下才解锁。这个锁的 lock 和 unlock, lockWhenCondition 是随意组合的,可以不用对应起来。
这是用在多进程之间共享资源的锁,对 iOS 来说暂时没用处。
无锁
放弃加锁,采用原子操作,编写无锁队列解决多线程同步的问题。酷壳有篇介绍无锁队列的文章可以参考一下:无锁队列的实现
如果一个 Timer 是不停 repeat,那么释放之前就应该先 invalidate。非repeat的timer在fired的时候会自动调用invalidate,但是repeat的不会。这时如果释放了timer,而timer其实还会回调,回调的时候找不到对象就会挂掉。
NSTimer 是通过 RunLoop 来实现定时调用的,当你创建一个 Timer 的时候,RunLoop 会持有这个 Timer 的强引用,如果你创建了一个 repeating timer,在下一次回调前就把这个 timer release了,那么 runloop 回调的时候就会找不到对象而 Crash。
我写了个宏用来释放Timer
/*
* 判断这个Timer不为nil则停止并释放
* 如果不先停止可能会导致crash
*/
#define WVSAFA_DELETE_TIMER(timer) { \
if (timer != nil) { \
[timer invalidate]; \
[timer release]; \
timer = nil; \
} \
}
为了弥补上周只去华侨城和园博园没去拍火车的遗憾,今天就带上N4,DC去拍火车去。中午吃了饭就出发了,外面有点小雨,DC最后没用上,只用了N4就够拍了。
坐上公车一路来到信诺公司站,下了车往西面走,月亮湾大道上全是货柜车,尘土飞扬的。好彩刚下过雨,空气还算清新,用N4拍了张白花,挺好看的。
一路向西,看到铁路公司的大门没敢进,绕了一下到后面,结果是个边防。问了下坐在那里的士兵,他都不晓得附近有火车可以看-.- 完了再去铁路公司问保安,保安说绕到外面走绿化道一直走。我就往外走了,看到有条小路,想起来之前搜到的帖子说要走小路进去,就往里头钻了。结果是条旧的绿化道,以前的人行道,现在没人走了,非常荒凉,很恐怖的感觉。
沿着废人行道往前走,没看出有什么东西来,最后还是捡了条小路往外走了,太恐怖了。结果走着走着到了个驾校。问了小卖部的人,说是前面有个修火车的地方,就我刚走的那条绿化道里面往前走,有个铁丝网就可以看到火车了。于是乎我又往回走,还看到个大叔,在那里吹喇叭。大叔让我直走有条小路可以看到火车,于是我直走,发现又回到刚刚走过来的地方=.=(Stupid
然后,在刚才不走的那里有条小路,钻过重重蜜蜂、蝴蝶、苍蝇、小虫的阻挡,来到一个狗?洞?前面。。。
钻过狗洞,铁轨赫然出现在眼前!
有几个工作人员在那里,后来遇到几个工作人员都说让我离开,闲人免进,不过还是给我拍到了一些好东西,Nexus 4的镜头差强人意,不过也算能看的片子了:D
2022-08-20 原《每周读书》系列更名为《枫影夜读》
又一年立秋。2013的春夏,过得混沌而麻木。《流星之绊》的主角们初出社会被欺骗后才幡然醒悟识破这个丑恶的世界。东野笔下的人物总是亦邪亦正,阳光与阴暗并存。
这本书其实算不上太好的作品,只是某天拿起快放到没电PaperWhite,回想起10年的冬天,在湿冷的大学宿舍里背着无聊的课本挣扎在及格线上,忽然就想写『每周读书』,于是过两年多过去了。时间过得真快。
写作也好读书也好,在面临枯燥乏味的考试的时候都是我最喜欢的解压方式。在公司呆了两年了,经历了不少事情,兴奋过,激动过,颓废过,迷茫过。始终没办法像安藤忠雄一样在二十几岁的时候就明白自己想要什么,追求什么。
自从买了PC之后,躲进游戏的世界便是数月。这种状态下人是蒙蔽的,麻木的,不知道自己在做什么,要什么,只是整天打打酱油,玩玩游戏,吃个饭睡个觉就完事了。即便6月份那场持续一个多月大病之后,仍如醉汉避世,睁眼亦无所识。
于是以捡起尘封的PaperWhite为契机,我修好了几个月前就没电的手表,找回了抽屉里早已干掉的钢笔,更改了我的所有电子设备的桌面,给我的手机带上套更换手感,以图时刻提醒自己,回头便是混沌无所知。
《流星之绊》依然有《幻夜》和《白夜行》的影子,以一个杀人事件开端,长达十四年后结案。死者是洋食屋的老板和老板娘,三个孩子成为孤儿在充满险恶的社会里挣扎,而凶手一直在逃毫无线索。十四年后,在案件失效之前,偶然的机遇找到杀人凶手的孩子,开始了引导警察开展调查的计划,而他们这时,却已经成为很熟练的欺诈师…
2022-08-20 原《每周读书》系列更名为《枫影夜读》
知道「胭脂扣」是小时候看的张国荣和梅艳芳的电影版。印象很深,画面很美,但是电影的结局有点无聊。前两日无事翻到李碧华原著,便看看了,没想到这竟是李碧华的第一部小说。
初读这部作品,尚不知作者名讳。还以为是亦舒所作,文笔柔静似水,带点香港白话的语调。因为电影印象太深,读书的时候脑中主角的形象便一直是张国荣与梅艳芳,一个俊朗一个冷艳。小说本身有些许不太成熟的处理,比如碰到身为女鬼的如花,虽有着笔墨解释主角的恐惧心理,却还是太容易便接受了与鬼对话的事实。比如结局,虽不是电影版结局那般无聊俗气,但是也交待得有些仓促,有悬念,有意犹未尽,但没有结果与答案。
总的来说我挺喜欢李碧华的文字,清清淡淡,白如水,洁如霜,又带点港式幽默,令人神往起旧时光,一如黑白默片,文艺得自然。
2012年5月24日到今天(2013年4月16日),竟然只读了这么少的书,真是令人汗颜。想到读第四十周的「佐藤可士和的超整理术」的时候我还在实习,恍如隔世。
Cocoa设计模式,都是我们平时用惯了的东西,取了个名字,介绍了一下问题、解决方案、应用场景、示例代码。
一种很简单,很容易实践的时间管理办法。
iOS 设计规范,即使老手也有不熟悉的地方,读之颇为受益。
很震撼的小说,戏子与爱情。
东野圭吾的推理作,感觉一般,主角失忆,根据蛛丝马迹找回真相。
作者对精神病人进行采访后集合成的故事集,故事精彩离奇,颇具启发。
悬疑推理类小说,英文版生词少,阅读起来很简单。
设计类书籍,讲述设计的基本原理。
在后台需要与多种终端如iPhone,Android,Web或者WinPhone之类的不同平台作通信的时候,常常需要使用一种中间的通信协议,并且使用通用数据类型如XML。
Protocol Buffers(以下简称protobuf)就是类似于XML这样的东西,可以在后台与多终端间进行通信,但是比它要远强大的多。
Protobuf由Google出品,08年的时候Google把这个项目开源了,截至发稿已发展到2.5.0版本,官方支持C++,Java和Python三种语言,但是由于其设计得很简单,所以衍生出很多第三方的支持,基本上常用的PHP,C,Actoin Script,Javascript,Perl等多种语言都已有第三方的库。
Protobuf比起XML有很多优势,首先是更简单了。
一个XML文件我们编写的时候需要定义各种各种的节点,而Proto文件则使用Google定义的有点像Java的语言来编写,简洁很多。
XML长得像这样:
<person>
<name>John Doe</name>
<email>[email protected]</email>
</person>
而proto文件则长得像这样:
# Textual representation of a protocol buffer.
# This is *not* the binary format used on the wire.
person {
name: "John Doe"
email: "[email protected]"
}
其次是快了。proto文件是给程序猿阅读的时候用的,真正传输的时候是序列化的二进制数据,比起XML要小得多,解析起来也快得多。
第三是可以直接生成供程序使用的类。XML文件接收后我们还得手工去解析然后转化为可以使用的对象,但是PB文件接收后,PB的库就已经帮我们转化为对应的数据类了。
Protobuf主要分为两个部分,一是编译器protoc,一是分包组包用的库。
编译器是用来编译proto文件为目标语言的,比如一个上面那个 Person.proto 文件,我可以用 protoc 直接编译成C++类 Person,用的时候就很方便了:
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;
对应的可以变成ObjC的类、Java的类等等。
在我接收到数据之后,我可以使用 parseFrom 方法直接对 byte 数据进行解析,得到一个可以用的类,如Java例子:
byte[] msg = b.getByteArray(PERSON_MSG_EXTRA); // 接收byte数据
person = Person.parseFrom(msg).toBuilder(); // 直接解析为对应的类
下面几节介绍一下一个服务器对多种终端通信的实际例子。服务器上使用 Ruby on Rails,终端有 iOS 和 Android。也就是 Ruby 和 ObjC 、 Android 之间的通信了。
首先,最重要的是定义好 proto 文件:
package Tutorial;
message Source { required string title = 1; required string description = 2; optional int id = 3; }
message SourceAllResponse { required uint32 count = 1; repeated Source source_list = 2; }
有点像 Java 语法,有个 package 在最前面,这个 Tutorial 在 Java 里面就会生成一个类为 Tutorial,对于 Java,有个可选的选项,可以填上包名。
option java_package = "com.example.protobuf";
option java_outer_classname = "Tutorial";
如果是 Ruby 则生成module Tutorial, ObjC 则是 TutorialRoot,用来管理 extensionRegistry(暂时还没搞懂用来干啥)。
message 是对应一个类,required 是必须字段,通信发起方必有的字段,对应 optional 则是可选的。如果后台某天升级了协议要增加返回字段,那么新增的字段就必须是 optional 的,以防客户端接收失败(当然如果能保证客户端永远最新那是另一回事)。repeated 可以看成是返回多个同类型的值,如一个数组,像SourceAllResponse会返回所有的source,第一个是source的个数,第二个是多个source对象。
protobuf 数据类型看起来像 C++ 有 double, float, int32等等,在 https://developers.google.com/protocol-buffers/docs/proto 里有表格详细说明。
定义完proto文件后,使用官方的 protoc 可以对其进行编译。下载地址在: https://code.google.com/p/protobuf/downloads/list
如果是 Mac OS X 或者 Linux ,需要下载官方的源码,解压后根据官方的 README.txt 里的说明:
$ ./configure
$ make
$ make check
$ make install
编译安装,然后就可以使用protoc命令了。windows用户则下载 *.win32.zip 文件后里面就有 protoc.exe 了,命令行下使用就行。把上面那个 proto 结构体保存成 tutorial.proto,然后就可以用 protoc 编译了。
Ruby 可以用 codekitchen 的 ruby-protocol-buffers 或者 macks 的 ruby-protobuf,我用前者。ruby-protobuf 没有能使用成功。
首先 Gemfile 里面加入:
gem 'ruby-protocol-buffers'
然后 bundle install。
使用的方法就是
ruby-protoc Tutorial.proto
注意:ruby-protocol-buffers依赖于官方的Protoc,所以需要你这台机器装了 protoc 才行。
如果用 ruby-protobuf 则是:
rprotoc examples/addressbook.proto
而且不依赖官方的protoc,不过我没使用成功就是了。
编译后会生成 tutorial.pb.rb,在ruby中:
require 'tutorial.pb'
aResponse = Tutorial::SourceAllResponse.new
aResponse.count = sources.count
//...
send_data aResponse
就可以直接使用proto来通信了。
protoc --java_out=. tutorial.proto
会生成 Tutorial.java ,引入到工程里面,这时会发现一对 Error,因为还没有引入 jar 包。在解压好的 protobuf 源码目录, cd 到 java 目录里面,查看 README.txt 文件发现我们可以使用 Maven 对其进行编译。我在 Mac OS 上没编译成功, Linux 可能比较好编。
$ protoc --version
$ mvn test
$ mvn install
$ mvn package
完了就会发现在 target 文件夹里面有 jar 包了。
然后引入这个 jar 包,注意,如果你用Eclipse,除了 Build Path里面加了jar包,还得把它放进libs目录,否则只能编译不能使用(被这个坑惨了T_T)。
Protobuf 官方不支持 ObjC 需要使用别人写的库,https://code.google.com/p/metasyntactic/wiki/ProtocolBuffers 其实就是作为 protoc 的一个插件而已。这个库已经几年没更新了,还是 2.2 版本的 protobuf,不过由于 protobuf 良好的向上向下兼容,用什么版本其实无所谓,协议没有变。
首先到这里下载源码 http://code.google.com/p/metasyntactic/downloads/list ,完了根据官方的方法,到解压目录下:
./autogen.sh
./configure
make
使用的时候就
protoc --proto_path=src --objc_out=build/gen src/foo.proto src/bar/baz.proto
但是由于我的 Mac 里面已经装了官方的 protoc 了,所以我的命令带改成在源码的 src 文件夹下
./protoc --proto_path=. --objc_out=. /PATH_TO_TUTORIAL/Tutorial.proto
可以使用 shell 脚本来搞定这个,直接在 XCode 的 Build Phases 里面加个 Run Script,然后就会在每次编译的时候去编译这个 proto 文件了。编译后把生成的 Tutorial.pb.h 和 Tutorial.pb.m 文件加进工程,同样编译不过,还需要添加第三方库。
把源码目录下,objectiveC里面的所有Classes加入工程,然后编辑你的 prefix.pch 文件,import一下protobuffer
大工告成,可以接收服务器下发的PB消息了。
NSData * aData = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://xxxx"]];
SourceAllResponse * aResponse = [SourceAllResponse parseFromData:aData];
Protobuffer 在一个后台对付多终端的通信方面还是非常好用的,方便、可扩展是它的特点,当然对于后台开发的同学来说还有性能上的优势。
2022-08-20 原《每周读书》系列更名为《枫影夜读》
当我还不了解「单例」是什么的时候我觉得「设计模式」是很高深的东西,直到看了这本书我才知道,原来设计模式不过是对我们平时常用的编程方式提炼一下给个名字罢了。
更准确地说,设计模式是针对一类问题,给出一种通用的解决方案,设计模式的名字是为了更方便程序猿们交流(虽然我不这么觉得)。设计模式这个名词来自于91年四人帮GoF出的书,书名叫「设计模式」(「Design Patterns - Elements of Reusable Object-Oriented Software」)。该书收录了23种设计模式,应该都是讲C++的,我没看过书的内容。
Cocoa Design Patterns这本书则是专门讲Mac OS和iOS的,例子都来自Cocoa框架,用ObjC语言讲解。全书主要有5个部分,涉及MVC模式,基础库涉及的模式,有助于解耦的模式,有助于隐藏复杂性的模式以及最后的实践。
1.MVC模式应该是很常见的模式了无需多言。
2.基础模式主要都是Cocoa框架提供的,像[[XXClass alloc] init]这样分两阶段的创建实例,和使用Category扩展类的方法这些。
3.有助于解耦的模式包括单例模式,NSNotification通知中心和delegate这些。
4.有助于隐藏复杂性的模式有Bundle,和奇葩的Class Cluter等等。
基本上如果ObjC开发掌握得毕竟熟练的话,这本书看起来意义不算太大=..=!!!
不过至少这本书让我记得了更多的模式词汇,而且更重要的是,以前我只是用着delegate这样的东西,但是不晓得为什么要设计出这样的东西,看着本书其实就是点到面的总结。
书的每一节都分为问题(Motivation,个人感觉翻译为提出问题比较恰当),解决方案(Solution),Cocoa例子(Examples in Cocoa),结论(Consequences)四个部分。结构非常清晰。书读起来也很容易,而且我通过这本书还发现了Class Cluter这个奇葩的东西,可以好好研究一下。
2022-08-20 原《每周读书》系列更名为《枫影夜读》
最早在退墨博客上接触到GTD思想,使用过Doit.im, Things, Any.do等工具,也看过笑来老师的「与时间做朋友」,但是实际效果均不甚理想。翻阅「Getting Things Done」这本书,只觉大道理很多,但是看过就算,留不下痕迹与思考。直到回过头看退墨博客,提到「蕃茄工作法」这本书,好奇这书的名字于是下载看了,才发现一个具体可行的时间管理方法。
「蕃茄」这本书篇幅非常短,整个PDF只有44页,一个小时之内可以看完。这个好玩的名字其实来源于厨房里的蕃茄计时器,而所谓的「蕃茄工作法」其实只是一套游戏规则,简单的说就是:
以30分钟为一个番茄时间,其中25分钟集中精神工作,5分钟休息,每完成一个番茄时间就记录一下。每个番茄时间都是不可分割的,如果中途被人打断,则尽量推迟处理时间到这个蕃茄时间结束后,如果无法推迟则取消当前番茄时间,绝不可手软。
蕃茄时间除了连续集中精力25分钟之外,还要求做好记录。每天第一个番茄时间用来计划今天要做的事情,并留有一部分时间用来应对突发事件,然后一天结束之后需要对今天做的事情做总结。通过记录我发现,每天我竟然有一半的时间是用来处理计划以外的事情!
我从上周一才算是正式开始使用蕃茄工作法,虽然书里鼓励使用真正的蕃茄钟,因为嘀嗒嘀嗒有时间流逝感,不过办公室里不适合用这个,于是我用了个Android软件作代替[Colock Tomato](蕃茄官网自己做了一个,但是我觉得这个钟盘更适合我,有兴趣的读者也可以看看官网的)。每天早上我第一个番茄时间用来计划当天需要做的事情,由于番茄时间为30分钟有点长,我实际上在计划完当天要做的事情之后就用剩下的时间扫办公和私人邮件,回复RTX离线消息,查阅公司BBS新闻等琐事。等到第二个番茄时间开始,则正式进入工作。
计划好任务也是一项脑力活,首先需要分割好任务,比如说我今天需要完成一个需求,修复一个bug。如果这个工作项不大,比如这个bug我可能一个番茄时间差不多,那就比较好办,但是如果像需求比较大的情况下,就需要细细划分了。一般是一个任务不要超过5个番茄时间,我只有看书的时候会达到4个番茄时间的量,一般任务都在3个以内。需求如果比较大,需要先沟通需求,然后设计,然后分几个步骤来完成,可以把这几个阶段划分出来作为几个任务去完成。
在完成任务的过程中不可避免地会被人打扰,要你协助做这个那个,一般是RTX找我,我的RTX是关掉notification的,不会弹出popup,所以通常我会在一个完整的番茄时间过完后才看消息,如果需要回复我则会商量如:5分钟/10分钟后我怎样怎样。一般来说大多数的事件都是可以等到25分钟之后再处理的。而如果被电话或者有人过来我办公位的话,我就会把当前番茄时间cancel,先处理紧急事情,或者重开一个蕃茄时间。这是规则,其实也是真理,一块连续的集中精神的工作时间很重要。
每天工作结束后我会把当天做的事情记录起来,我是用google drive 的google sheet来做记录,存在google的云里。事实上过去的一周我是第二天早上作计划的时候顺便做总结的,总结的好处在于你可以看到自己预估给每项任务的时间,和实际使用的蕃茄时间。我发现1个番茄时间的预估通常是准确的,但是如果我给出2个3个番茄时间,这项任务通常会不准,可能多也可能少。而定位bug是最不可预估的,有时候花上3个番茄时间都查不出来。需求如果明确也相对比较好预估,如果作优化的话就比较难。目前我只试行了一周,得到的数据还不算准确,需要再观察一段时间。
总结这一周的蕃茄工作法试行,我每天大概有9个可以完全集中精神的番茄时间,算上用来作计划的那个蕃茄,共有10个。看起来每天可以利用的时间不算多,蕃茄在这第一个星期里面并没有提高我的生产效率,但是蕃茄令我心情愉悦,我知道自己每天都干了什么,而且劳逸结合使我不容易疲劳,蕃茄的优势正在于此,能使我专注于自己的工作而不是整天被邮件和IM骚扰,而且更好地做出任务计划和时间管理。
蕃茄的劣势很明显,我必须每天都做记录,我必须一次次去开启一个新的番茄时间,必须在这个时间内集中精神,不能中断,如果中断了我必须放弃这个番茄时间,不能记录下来,也就是说,一切靠自觉。目前来说我上周的最后两天不是很自觉,有几个番茄时间其实已经被别人打扰了,但是我看剩下几分钟于是就记录为一个番茄时间,但其实并没有连续地集中精神在工作上,不能算数。
但是总体而言,我还是挺喜欢蕃茄工作法的,毕竟这是唯一一个我看到的,具体可行的时间管理办法。
2022-08-20 原《每周读书》系列更名为《枫影夜读》
11年电子工业出版社出版的这本书,现在才读到真有点相见恨晚。不过如同所有「有道理的废话」一般,这本书固然给出了一些iPhone App设计的指导意见,但接受与否就因人而异了。对我来说,我觉得这本书是比较中肯的,大多数都是业界已经公认可行的设计意见。
本书介绍了iOS提供的常用控件的用法及其背后的设计原理,对于iOS设计入门而言非常实用,而有些藏得较深的功能,即使是老用户也不一定能知道得全。比方iOS点击顶部Statusbar可以滚动TableView回到顶部的功能有些开发同事还没有发觉到,而我自己也不知道辅助功能选项里面有设置为黑白的功能,设计师可以用来检测App设计的对比度。所以即使是老用户,看看这本书还是有些收获的,可以知其然且知其所以然。像为什么较长时间的等待使用进度条比起使用Loading菊花要好之类的设计意见都是可以很好地改善用户体验的细节。
这本书的中译本翻译得不错,本地化做得挺好。像Activity Indicator就被译成「菊花转」,WTF Button就被译成「搞什么飞机」按钮等,令人会心一笑。意译较原文有差的地方会给出原文,读者可以自己对照,不至找不回原意。
作者在讲述设计原理的时候也结合了大量优秀的App作为例子,使得论述不会空洞无力,也就是「吹水吹得有料」。不过毕竟属于「吹水」一类,「成王败寇」,成功的应用要找亮点很容易,失败的应用要说它不好也挺简单。作者谈了Reeder的优点也指出了Reeder过度修改标准控件导致表意不明的缺点,意见很客观,吹得很好,至于信与不信接受与否就交给读者自己去判断了。
断断续续读完这本书,确实给我带来一些思考,我对照着书里的理论去反思我做过的或者正在做的App,发现了一些问题,也发现了一些不同。现实需要妥协的东西实在太多,我觉得设计师追求也许不是尽善,或许只不过是一种平衡罢了。
总而言之我觉得这本书很适合从事iOS开发的人去读,无论是开发是设计是产品,都应该了解iOS这个平台一些基本的设计原理、用户习惯。书的内容现在来看已经有点旧了,书里iOS 4是最新的系统,现在读还是可以的,通用的控件和思想没有变,不过过几年就无法预知了。
Youku:
<object width="600" height="500" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0" align="middle"><param name="src" value="http://player.youku.com/player.php/sid/XNDc5ODE2MTQw/v.swf" /><param name="allowfullscreen" value="true" /><param name="quality" value="high" /><param name="allowscriptaccess" value="always" /></object>
Youtube:
<iframe loading="lazy" width="600" height="500" src="http://www.youtube.com/embed/yZkl12eW8PA" frameborder="0" allowfullscreen></iframe>
2022-08-20 原《每周读书》系列更名为《枫影夜读》
远钟入夜,北平的故事一百年。
初读《霸王别姬》,只觉干净,利落,语言拿捏恰到好处,读罢前两节已自不能罢休。随着情节展开,段小楼(小石子)和程蝶衣(小豆子)坎坷曲折的人生之路交织缠绵,兴与衰,成与败。故事从民国到日战,从解放到文革,从新世到终老;两人跌跌荡荡,风光到霸王一时,不可一世;各自兴兴衰衰,末路到虞姬刎颈,四面楚歌。
全书首尾呼应,令人回味。故事是男男之恋,以新旧社会交替为故事时间,通过民国到抗日,内战到解放,文革到毛逝等几个大事件动荡故事的起伏。每个时期又有各自的小事件小冲突,读来只觉高潮迭起,手不释卷。
故事主场景在北平,随故事发展在最后有所转移。93年上映的同名电影《霸王别姬》也是由作者亲自改编而成的剧本,据说这部小说一开始是电视剧的剧本,后来多次修改后成小说,只不知我所读的版本是哪个了。(百度百科:http://baike.baidu.com/view/8506.htm#sub5395582)
但是小说中镜头的转移和故事的巧妙展开是和电影的手法很像的。如:
待 往 前 走 , 又 更 熱 鬧 了 。 有 說 書 的 、 變 戲 法 的 、 摔 跤 的 、 抖 空 竹 的 、 打 把 式 的 、 翻 觔 斗 的 、 葷 相 聲 的 、 拉 大 弓 的 、 賣 大 力 丸 的 、 演 硬 氣 功 的 、 還 有 拔 牙 的 …… 艷 紅 找 到 她 要 找 的 人 了 。
一个排比的手法,在想象的场景转变之时巧妙地带出角色此行目的。时间的过度上也有类似电影的手法:
日子过去了。就这样一圈一圈的在院子中走着,越来越快,总是走不完。棍子敲打突地停住,就得挺住亮相。一两个瘫下来,散漫地必吃上一记。到了稍息,腿不自已地在抖。好象。好累。
轻而易举地,在同一个空间,加载了不同的时间。这样简洁的叙事使得全书看下来毫不觉得啰嗦,只沉浸在故事的发展和冲突中大呼过瘾。这部小说的中心在于两位男性主角段小楼和程蝶衣的感情纠葛。一个霸王一个虞姬,虞姬自小便钟情于霸王。
所谓正常不过是多数人都在做的事情罢了,正常与不正常不能代表对与错,对与错本身也不是绝对的,道德原本是对对与错的探索,但是道德也不是绝对的。看过文革的故事就知道了。
李碧华爱写畸恋,这本身就是小说的一个冲突,再加上纠缠不清你来我往,仿佛船行大海,时而风平浪静,时而骇浪滔天。《霸王别姬》里头最精彩的描写要属“女性”心理描写了。程蝶衣虽是男性,但心女的。程蝶衣和段小楼的元配之间纠葛几十年争斗几十年的嫉恨,可谓纸上活人,生生般演在你眼前。看书的时候我就感慨,电影中要一个男人去如此这般饰演这个角色,对演技的要求实在太苛刻。
“此 時 , 一 柄 紫 竹 油 紙 傘 撐 過 來 , 打 在 小 樓 頭 上 。 是 蝶 衣 。 傘 默 默 地 遮 擋 著 雨 。 兩 個 人 , 又 共 用 一 傘 。 大 師 哥 的 影 兒 回 來 了 , 他 仍 是 當 頭 兒 的 料 , 他 是 他 主 子 。 彼 此 諒 宥 , 一 切 冰 釋 。 什 麼 也 沒 發 生 過 。 真 像 是 夢 裡 的 洪 荒 世 界 。”
这是爱。
“菊仙急得泪盈于睫,窘,但为了男人,她为了他,肺腑被一只长了尖利指爪的手在刺着、撕着、掰着,有点支离破碎,为了大局着想,只隐忍不发:“你帮小楼过这关。蝶衣,我感激你!” 蝶衣也很心焦,只故作姿态,不想输人,也不想输阵。 ”
这是恨。
“受惊过度的蝶衣,瞪大了眼睛,极目不见尽头。他同死人一起。他也等于死人。墓地失控,在林子涑涑地跑,跑,跑。仓皇自他身后,企图淹没他。他跑得快,淹得也更快。跌跌撞撞地,逃不出生天。蝶衣虚弱地,在月亮下跪倒了。像抽掉了一身筋骨,他没脊梁,他哈腰。是他听觉的错觉,轰隆一响,趴唯一声,万籁竟又全寂,如同失聪。 人在天地中,极为渺小,子然一身。浸淫在月色下。他很绝望。一切都完了。”
这是绝望。
李碧华的文字清秀绮丽,既有秋风扫落叶般利落,又有杨柳醉春风似柔美,成语雅句信手拈来,潇潇洒洒,字字珠玑。因《霸王别姬》方知所谓语言,实当如此。
本文中的代码托管在github上:https://github.com/WindyShade/DataSaveMethods
相对复杂的App仅靠内存的数据肯定无法满足,数据写磁盘作持久化存储是几乎每个客户端软件都需要做的。简单如“是否第一次打开”的BOOL值,大到游戏的进度和状态等数据,都需要进行本地持久化存储。这些数据的存储本质上就是写磁盘存文件,原始一点可以用iOS本身支持有NSFileManager这样的API,或者干脆C语言fwrite/fread,Cocoa Touch本身也提供了一些存储方式,如NSUserDefaults,CoreData等。总的来说,iOS平台数据持久存储方法大致如下所列:
ObjC是C的一个超集,所以最笨的方法我们可以直接用C作文件读写来实现数据存储:
1. 写入文件
[code lang="objc"]
// File path
const char * pFilePath = [_path cStringUsingEncoding:NSUTF8StringEncoding];
// Create a new file
FILE * pFile = fopen(pFilePath, "w+");
if (pFile == NULL) {
NSLog(@"Open File ERROR!");
return;
}
const char * content = [_textField.text cStringUsingEncoding:NSUTF8StringEncoding];
fwrite(content, sizeof(content), 1, pFile);
fclose(pFile);
[/code]
2. 读取文件
[code lang="objc"]
// File path
const char * pFilePath = [_path cStringUsingEncoding:NSUTF8StringEncoding];
// Create a new file
FILE * pFile = fopen(pFilePath, "r+");
if (pFile == NULL) {
NSLog(@"Open File ERROR!");
return;
}
int fileSize = ftell(pFile);
NSLog(@"fileSize: %d", fileSize);
char * content[20];
fread(content, 20, 20, pFile);
NSString * aStr = [NSString stringWithFormat:@"%s", &content];
if (aStr != nil && ![aStr isEqualToString:@""]) {
_textField.text = aStr;
}
fclose(pFile);
[/code]
但是既然在iOS平台作开发,我们当然不至于要到使用C的原生文件接口这种地步,下面就介绍几种iOS开发中常用的数据本地存储方式。使用起来最简单的大概就是Cocoa提供的NSUserDefaults了,Cocoa会为每个app自动创建一个数据库,用来存储App本身的偏好设置,如:开关音效,音量调整之类的少量信息。NSUserDefaults是一个单例,生命后期由App掌管,使用时用 [NSUserDefaults standardUserDefaults] 接口获取单例对象。NSUserDefaults本质上是以Key-Value形式存成plist文件,放在App的Library/Preferences目录下,对于已越狱的机器来说,这个文件是不安全的,所以**千万不要用NSUserDefaults来存储密码之类的敏感信息**,用户名密码应该使用**KeyChains**来存储。
1.写入数据
[code lang="objc"]
// 获取一个NSUserDefaults对象
NSUserDefaults * aUserDefaults = [NSUserDefaults standardUserDefaults];
// 插入一个key-value值
[aUserDefaults setObject:_textField.text forKey:@"Text"];
// 这里是为了把设置及时写入文件,防止由于崩溃等情况App内存信息丢失
[aUserDefaults synchronize];
[/code]
2.读取数据
[code lang="objc"]
NSUserDefaults * aUserDefaults = [NSUserDefaults standardUserDefaults];
// 获取一个key-value值
NSString * aStr = [aUserDefaults objectForKey:@"Text"];
[/code]
使用起来很简单吧,它的接口跟 NSMutableDictionary 一样,看它的头文件,事实上在内存里面也是用dictionary来存的。写数据的时候记得用 synchronize 方法写入文件,否则 crash了数据就丢了。
上一节提到NSUserDefaults事实上是存成Plist文件,只是Apple帮我们封装好了读写方法而已。NSUserDefaults的缺陷是存储只能是Library/Preferences/<Application BundleIdentifier>.plist 这个文件,如果我们要自己写一个Plist文件呢? 使用NSFileManger可以很容易办到。事实上Plist文件是XML格式的,如果你存储的数据是Plist文件支持的类型,直接用NSFileManager的writToFile接口就可以写入一个plist文件了。 ### Plist文件支持的数据格式有: NSString, NSNumber, Boolean, NSDate, NSData, NSArray, 和NSDictionary. 其中,Boolean格式事实上以[NSNumber numberOfBool:YES/NO];这样的形式表示。NSNumber支持float和int两种格式。
1. 首先创建plist文件:
[code lang="objc"]
// 文件的路径
NSString * _path = [[NSTemporaryDirectory() stringByAppendingString:@"save.plist"] retain];
// 获取一个NSFileManger
NSFileManager * aFileManager = [NSFileManager defaultManager];
if (![aFileManager fileExistsAtPath:_path]){
// 文件不存在,创建之
NSMutableDictionary * aDefaultDict = [[NSMutableDictionary alloc] init];
// 插入一个值,此时数据仍存在内存里
[aDefaultDict setObject:@"test" forKey:@"TestText"];
// 使用NSMutableDictionary的写文件接口自动创建一个Plist文件
if (![aDefaultDict writeToFile:_path atomically:YES]) {
NSLog(@"OMG!!!");
}
[aDefaultDict release];
}
[/code]
2. 写入文件
[code lang="objc"]
// 写入数据
NSMutableDictionary * aDataDict = [NSMutableDictionary dictionaryWithContentsOfFile:_path];
[aDataDict setObject:_textField.text forKey:@"TestText"];
if (![aDataDict writeToFile:_path atomically:YES]) {
NSLog(@"OMG!!!");
}
[/code]
3. 读取文件
[code lang="objc"]
NSMutableDictionary * aDataDict = [NSMutableDictionary dictionaryWithContentsOfFile:_path];
NSString * aStr = [aDataDict objectForKey:@"TestText"];
if (aStr != nil && aStr.length > 0) {
_textField.text = aStr;
}
[/code]
上面介绍的几种方法中,直接用C语言的接口显然是最不方便的,拿出来的数据还得自己进行类型转换。NSUserDefaults和Plist文件支持常用数据类型,但是不支持自定义的数据对象,好像Cocoa提供了NSCoding和NSKeyArchiver两个工具类,可以把我们自定义的对象编码成二进制数据流,然后存进文件里面,下面的Sample为了简单我直接用cocoa的接口写成plist文件。 如果要使用这种方式进行存储,首先自定义的对象要继承NSCoding的delegate。
[code lang="objc"]
@interface WSNSCodingData : NSObject<NSCoding>
然后继承两个必须实现的方法encodeWithCoder:和initWithCoder:
- (void)encodeWithCoder:(NSCoder *)enoder {
[enoder encodeObject:data forKey:kDATA_KEY];
}
- (id)initWithCoder:(NSCoder *)decoder {
data = [[decoder decodeObjectForKey:kDATA_KEY] copy];
return [self init];
}
[/code]
这里data是我自己定义的WSNSCodingData这个数据对象的成员变量,由于数据在使用过程中需要持续保存在内存中,所以类型为copy,或者retain也可以,记得在dealloc函数里面要realease。这样,我们就定义了一个可以使用NSCoding进行编码的数据对象。
保存数据:
[code lang="objc"]
- (void)saveData {
if (aData == nil) {
aData = [[WSNSCodingData alloc] init];
}
aData.data = _textField.text;
NSLog(@"save data...%@", aData.data);
// 这里init的NSMutableData是临时用来存储数据的
NSMutableData * data = [[NSMutableData alloc] init];
// 这个NSKeyedArchiver则是进行编码用的
NSKeyedArchiver * archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:aData forKey:DATA_KEY];
[archiver finishEncoding];
// 编码完成后的NSData,使用其写文件接口写入文件存起来
[data writeToFile:_path atomically:YES];
[archiver release];
[data release];
NSLog(@"save data: %@", aData.data);
}
[/code]
读取数据:
[code lang="objc"]
- (void)loadData {
NSLog(@"load file: %@", _path);
NSData * codedData = [[NSData alloc] initWithContentsOfFile:_path];
if (codedData == nil) return;
// NSKeyedUnarchiver用来解码
NSKeyedUnarchiver * unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:codedData];
// 解码后的数据被存在一个WSNSCodingData数据对象里面
aData = [[unarchiver decodeObjectForKey:DATA_KEY] retain];
[unarchiver finishDecoding];
[unarchiver release];
[codedData release];
if (aData.data != nil) {
_textField.text = aData.data;
}
}
[/code]
所以其实使用NSCoding和NSKeyedArchiver事实上也是写plist文件,只不过对复杂对象进行了编码使得plist支持更多数据类型而已。
如果App涉及到的数据多且杂,还涉及关系查询,那么毋庸置疑要使用到数据库了。Cocoa本身提供了CoreData这样比较重的数据库框架,下一节会讲到,这一节讲一个轻量级的数据库——SQLite。 SQLite是C写的的,做iOS开发只需要在工程里面加入需要的框架和头文件就可以用了,只是我们得用C语言来进行SQLite操作。 关于SQLite的使用参考了这篇文章:http://mobile.51cto.com/iphone-288898.htm但是稍微有点不一样。
1. 在编写SQLite代码之前,我们需要引入SQLite3头文件:
[code lang="objc"]
#import <sqlite3.h>
[/code]
2. 然后给工程加入 libsqlite3.0.dylib 框架。 3. 然后就可以开始使用了。首先是打开数据库:
[code lang="objc"]
- (void)openDB {
NSArray * documentsPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory
, NSUserDomainMask
, YES);
NSString * databaseFilePath = [[documentsPaths objectAtIndex:0] stringByAppendingPathComponent:@"mydb"];
// SQLite存的最终还是文件,如果没有该文件则会创建一个
if (sqlite3_open([databaseFilePath UTF8String], &_db) == SQLITE_OK) {
NSLog(@"Successfully open database.");
// 如果没有表则创建一个表
[self creatTable];
}
}
[/code]
3.关闭数据库,在dealloc函数里面调用:
[code lang="objc"]
- (void)closeDB {
sqlite3_close(_db);
}
[/code]
4.创建一个表:
[code lang="objc"]
- (void)creatTable {
char * errorMsg;
const char * createSql="create table if not exists datas (id integer primary key autoincrement,name text)";
if (sqlite3_exec(_db, createSql, NULL, NULL, &errorMsg) == SQLITE_OK) {
NSLog(@"Successfully create data table.");
}
else {
NSLog(@"Error: %s",errorMsg);
sqlite3_free(errorMsg);
}
}
[/code]
5. 写入数据库
[code lang="objc"]
- (void)saveData {
char * errorMsg;
// 向 datas 表中插入 name = _textFiled.text 的数据
NSString * insertSQL = [NSString stringWithFormat:@"insert into datas (name) values('%@')", _textField.text];
// 执行该 SQL 语句
if (sqlite3_exec(_db, [insertSQL cStringUsingEncoding:NSUTF8StringEncoding], NULL, NULL, &errorMsg)==SQLITE_OK) {
NSLog(@"insert ok.");
}
}
[/code]
6. 读取数据库
[code lang="objc"]
- (void)loadData {
[self openDB];
const char * selectSql="select id,name from datas";
sqlite3_stmt * statement;
if (sqlite3_prepare_v2(_db, selectSql, -1, &statement, nil)==SQLITE_OK) {
NSLog(@"select ok.");
}
while (sqlite3_step(statement) == SQLITE_ROW) {
int _id = sqlite3_column_int(statement, 0);
NSString * name = [[NSString alloc] initWithCString:(char *)sqlite3_column_text(statement, 1) encoding:NSUTF8StringEncoding];
NSLog(@"row>>id %i, name %@",_id,name);
_textField.text = name;
}
sqlite3_finalize(statement);
}
[/code]
大型数据存储和管理。 XCode自带有图形化工具,可以自动生成数据类型的代码。 最终存储格式不一定存成SQLite,可以是XML等形式。 (未完待续。。。)
最近某项更新的工作需要做大量重复的工作,一方面在作打包工作时在图形界面上做文件十分繁琐,另一方面在编辑config文件时即使用上正则替换也常常需要作人工检查和C&P,非常浪费时间。于是乎写了个Shell脚本把该自动化的都自动化,以后作这种工作一个命令就解决。
脚本是在 Mac OS X 上写的,没用到什么特殊的命令,所以类Unix的操作系统应该都能跑。文末有今天编写的脚本。
Shell需要注意的地方有:
#!后面带上解释脚本的绝对路径。
如果脚本第一句不以#!开头则默认使用 Bourne Shell, 即:/bin/sh
如果第一个非空字符是#但不是#!开头则默认使用 C Shell。
常用的解释脚本有:
[code lang="bash"]
#!/usr/bin/perl
#!/bin/php
#!/bin/expect
[/code]
Shell脚本通常都可以用命令+参数的形式调用,如:
command -p1 - p2
在脚本中 $# 代表参数个数,$0 代表程序的名字。所以跟平时习惯不同的是,第一个参数是 $1 而不是 $0。最多支持9个参数, $1 - $9.
Shell的比较常用
-lt #小于
-gt #大于
-eq #等于
如:
[code lang="bash"]
if [ "$a" -eq "$b" ];
[/code]
也可以使用CShell风格的 < , > 但是需要加上双层括号,如:
[code lang="bash"]
if (("$a" < "$b")) then
[/code]
还有一些其他的比较方式,不复杂的脚本有 -lt -gt 就足够了。
[code lang="bash"]
aVar=123 #变量赋值
echo $aVar #变量使用
[/code]
字符串操作在作文件命名的时候很有用,可以批量重命名之类的。
这篇文章有很详细的字符串操作的介绍:http://www.cnblogs.com/chengmo/archive/2010/10/02/1841355.html
[code lang="bash"]
#[]里面一定要有两个空格
if [ $# -lt 3 ];
then
else
fi
#以fi结束每一个if
[/code]
[code lang="bash"]
#!/bin/bash
#配置打包后会输出的路径
targetPath=~/Download/packages/
echo "****************************************"
echo "* Pack Language Packages. -- JustinYim *"
echo "****************************************"
echo "ouput path: ~/Download/packages/"
echo "Now Starting..."
if [ $# -lt 2 ];
then
echo "Oops, please append the Resources folder's path."
echo "And the version name like: pack xxx/xxx 1.1.2"
else
echo "Resources path: "$1
echo "Clening target path..."
rm -rf ~/Downloads/packages/*
echo "CPing files..."
#CP Files
cp -rf $1/*.lproj ~/Downloads/packages/
cd ~/Downloads/packages
#中文简体不进行更新,直接删掉
rm -rf zh_cn.lproj
#zip files
#新建一个配置文件
echo '[package]' > conf.conf
#设置包的起始值,因为之前已经有很多个包在服务器上了
num=7
#遍历所有文件
for filename in $(ls)
do
#不对配置文件进行压缩动作
if [ "$filename" = "conf.conf" ]
then
echo "Config file!"
continue
fi
#echo "ZIPing "$filename"..."
#替换掉版本号里面的.字符,如1.1.2改为1_1_2作为文件名用
ver=${2//./_}
#去掉文件名后缀,%表示从字符串尾部开始,找到.字符的时候,去掉后面的字符,如:ar.lproj会变成ar
#如果是 ${filename#.*} 则会变成从头部开始, ar.lproj会变成 lproj
lang=${filename%.*}
#设置zip文件的名字
zipname=x.strings.$lang.$ver.zip
#复制并压缩x.strings文件
cp $filename/x.strings x.strings.$lang
zip $zipname x.strings.$lang
#echo 'wirting config file...'
#自动编写配置文件
echo '[number'$num']' >> conf.conf
echo 'Id='$((++num)) >> conf.conf
echo 'PackPath=/xxx/package/'$zipname >> conf.conf
echo 'Name='$lang'|'$2 >> conf.conf
echo '' >> conf.conf
#clean files
#删掉临时文件
echo "Remove temp files..."
rm -rf $filename
rm x.strings.$lang
done
fi
[/code]
2022-08-20 原《每周读书》系列更名为《枫影夜读》
这部东野圭吾1995年的作品,使用了一个在当时应该挺新颖的概念——“平行世界”。这个平行有两层意思,其一是两条条路线京滨东北线和山手线在中间有一段平行而驰,以男女主角的邂逅为故事开端,其二则是整部小说在描述的时候分为两条线展开,相互交错,最终会于结局。
依然是东野一贯的手法,给出很少的线索,随着情节的展开一点点剥开事实的真相。但是这部作品给我感觉没啥太奇特的地方,故事的冲突不够壮观,起伏也不够惊心动魄。一开始我以为是真正的多宇宙科幻类型的小说,结果发现不过是以推理的手法回头找寻事实真相罢了,有点失望。不过故事的展开还是很流畅的,阅读也很轻松,算是闲暇之余的读物吧。
2022-08-20 原《每周读书》系列更名为《枫影夜读》
前两天在Mindon的博客看到推荐这本书的文章,就随手推了一本到Kindle上。结果跟Mindon一样,一上手就一发不可收拾,一气儿把它读完了。
这是本以访谈形式记下的精神病人的故事。作者高铭,这本书最初于2009年发表在天涯社区的“莲蓬鬼话”版块。全书分为48篇,每篇一个病例,篇幅都不长,很快可以看完。看最初的几篇以为作者是位精神科医师,其实不是,作者现在某公司任职影视策划总监。2004年到2008年期间,作者通过各种渠道,对一百多名精神病人进行了近距离接触,最终把访谈的草稿整理成书。(via 百度百科)
曾有一句段子“精神病人思维广,弱智儿童欢乐多”,尽管调侃的事实上是“正常人”太多烦恼,但是也表明精神病人看世界时跟“正常人”不同的角度。看完第一篇故事,便以感受到这种有趣,越往后面的故事看则越觉得震撼,甚至恐惧。文章发表在“莲蓬鬼话”版块不是没理由的,有些故事其实还是很唬人的。全书几十个故事外加几个解释性的“篇外篇”,有无趣的有好笑的,有恐怖的也有完全看不懂的。故事光怪陆离,天马行空,不管这些故事是作者自己臆造出来的还是访谈后加工出来的都好,这本书本身已是一部极具想象力的作品,甚至当你代入感太强的时候,还会给你带来心理压力。
大部分的文字都是对白,这些时而精彩时而俏皮,时而高深时而诡异,叙事过程的流畅使得看故事的人大呼过瘾。其实全书的主旨就一个——“换个角度看世界”。
2022-08-20 原《每周读书》系列更名为《枫影夜读》
疏忽一梦,便二十年。
主人公 Christine Lucas 醒来的时候以为自己还是年轻的少女,为身边躺着的陌生的男人而恐慌。她患有很罕见的失忆症——只能记得当天发生的事情,一旦熟睡,则记忆全消。她每天都在困惑中醒来,为眼前陌生的房间,为身边陌生的男子而恐慌。他叫Ben,她的丈夫,尽管她毫无记忆。
一位叫Nash的医生在帮她努力找回记忆,他让Chris把每天发生的事情写在日记里。于是某个「初次」醒来的一天,Chris发现日记封面赫然写着:「DON'T TRUST BEN」。
这本书事实上是一本悬疑推理小说,尽管内容上与以「刑事案件」为主线的侦探小说不同,手法上仍是同理的。全书共分三个部分:
日记的内容是主体,每天醒来,Chris都能找回一些许新的记忆,然后写进日记本。这样第二天她仍可以通过阅读记得之前的事情。所有的这些记忆碎片慢慢拼凑出一个现实,一部历史,一场悲剧。
这样的失忆设定令人想起2004年的一部电影《Momento》,影片的主人公也有「短期失忆症」,只能记住10分钟内的事情。他把所有需要记忆的事物拍成照片,记下线索,串成自己的记忆。这本书则出版于2011年。作者S. J. Watson(1971-)毕业于柏林明翰大学(University of Birmingham),他在写作课上完成了这部作品。由于年代和情节类似,我总认为这部小说不乏参考借鉴前述电影之处,不过以课程学习的处女之作的角度来看,确是难能可贵,而且这部小说情节轻重缓急处理得当,尤其后半部分高潮迭起,环环相扣,反转连连,令人读来废寝忘食,欲罢不能。
2011年一鸣惊人的处女作,使得S. J. Watson 成为文坛备受瞩目的新人。本书更登上 Sunday Times 和 New York Times 畅销榜,现已被翻译成40+种语言畅销世界各地。
这本书我看的是英文版,中信的中译本我试读了前面几节,确实觉得这种欧美文学经过转译实在流失了太多东西了。语法差异导致阅读不畅,文化和语法差异导致文字游戏翻译后所剩无几,翻译太过遵循原意则容易生硬,太过本地化则又显得艳俗。相反,日文作品的中译本读来要更觉顺畅,只是自己不懂日语,倒是无法和阅读原作相论了。
读完这部作品,感觉其中陷进设计还是有些漏洞的,重复的部分和过于拖沓的铺垫也有,但是瑕不掩瑜,仍是一部很吸引人的好作品。昨夜读至妙处,不由睡意全消,硬是看完了全书,但觉背有凉意,情节久久在脑中回荡。
一切起源于Apple官方文档里面关于单例(Singleton)的示范代码:Creating a Singleton Instance.
主要的争议集中在下面这一段:
[objc]static MyGizmoClass *sharedGizmoManager = nil;
+ (MyGizmoClass*)sharedManager
{
if (sharedGizmoManager == nil) {
sharedGizmoManager = [[super allocWithZone:NULL] init];
}
return sharedGizmoManager;
}
+ (id)allocWithZone:(NSZone *)zone
{
return [[self sharedManager] retain];
}[/objc]
其中:
[objc]
sharedGizmoManager = [[super allocWithZone:NULL] init];
[/objc]
这段有另一个版本,不使用 allocWithZone 而是直接 alloc,如下:
[objc]
sharedGizmoManager = [[super alloc] init];
[/objc]
这就引发了一个讨论,为什么要覆盖allocWithZone方法,到底 alloc 和 allocWithZone 有啥区别呢?
PS:关于ObjC单例的实现,@Venj 的这篇博文有比较详细的讨论,包括了线程安全的考虑,有兴趣的童鞋可以围观一下。
首先我们知道,我们需要保证单例类只有一个唯一的实例,而平时我们在初始化一个对象的时候, [[Class alloc] init],其实是做了两件事。 alloc 给对象分配内存空间,init是对对象的初始化,包括设置成员变量初值这些工作。而给对象分配空间,除了alloc方法之外,还有另一个方法: allocWithZone.
在NSObject 这个类的官方文档里面,allocWithZone方法介绍说,该方法的参数是被忽略的,正确的做法是传nil或者NULL参数给它。而这个方法之所以存在,是历史遗留原因。
Do not override allocWithZone: to include any initialization code. Instead, class-specific versions of init... methods.
This method exists for historical reasons; memory zones are no longer used by Objective-C.
文档里面提到,memory zone已经被弃用了,只是历史原因才保留这个接口。详细是什么历史原因我没找到,不过后面介绍的内容会稍微涉及到。
而实践证明,使用alloc方法初始化一个类的实例的时候,默认是调用了 allocWithZone 的方法。于是覆盖allocWithZone方法的原因已经很明显了:为了保持单例类实例的唯一性,需要覆盖所有会生成新的实例的方法,如果有人初始化这个单例类的时候不走[[Class alloc] init] ,而是直接 allocWithZone, 那么这个单例就不再是单例了,所以必须把这个方法也堵上。allocWithZone的答案到此算是解决了,但是,问题是无止境的。
这里引出了另外一个问题: What the hell is Memory Zone?
Apple官方文档里面就简单的几句,吝啬得很:
[objc]NSZone
Used to identify and manage memory zones.
typedef struct _NSZone NSZone;
Availability
Available in OS X v10.0 and later.
Declared In
NSZone.h[/objc]
CocaDev的wiki就写得详细的多了,原文地址在这里:http://cocoadev.com/wiki/NSZone
大意上是说NSZone是Apple用来分配和释放内存的一种方式,它不是一个对象,而是使用C结构存储了关于对象的内存管理的信息。基本上开发者是不需要去理会这个东西的,cocoa Application使用一个系统默认的NSZone来对应用的对象进行管理。那么在什么时候你会想要有一个自己控制的NSZone呢?当默认的NSZone里面管理了大量的对象的时候。这种时候,大量对象的释放可能会导致内存严重碎片化,cocoa本身有做过优化,每次alloc的时候会试图去填满内存的空隙,但是这样做的话时间的开销很大。于是乎,你可以自己创建一个NSZone,这样当你有大量的alloc请求的时候就全部转移到指定的NSZone里面去,减少了大量的时间开销。而且,使用NSZone还可以一口气把你创建的zone里面的东西都清除掉,省掉了大量的时间去一个个dealloc对象。
总的来说,当你需要创建大量的对象的时候,使用NSZone还是能节省一些时间的,不过前提是你得知道怎么去用它。这篇wiki里面也写了NSZone的用法,感兴趣的童鞋可以看看,不过另一篇2002年的文章就说开发者已经不能创建一个真正的NSZone了(看来也许这就是历史原因了),只能创建main zone的一个child zone。文章在这里:http://www.cocoabuilder.com/archive/cocoa/65056-what-an-nszone.html#65056 Timothy J.wood 的回答。
Timothy还讲到如果可以使用NSZone的话,多个对象在同一时间alloc可以减少分页使用,而且在同一个时间dealloc可以减少内存碎片。想必后来Apple在这方面是做了处理了,对开发者透明,无需开发者自己去做。
allocWithZone不被Apple鼓励使用,基本上多数时候程序员也不需要自己去管理自己的zone。当然多了解一些东西总是好的嘛。
2022-08-20 原《每周读书》系列更名为《枫影夜读》
《认知与设计——理解UI设计准则》,书名初见,会以为是偏视觉设计的方法指引,其实本书是以心理学的角度讲述计算机系统的交互设计原理的。作者Jeff Johnson本身拥有耶鲁和斯坦福的心理学学位,著有畅销书《GUI禁忌》。
我看的是图灵的版本,译者是老程序员 @yining 张一宁。书本身是针对计算机交互来写的,用了很多计算机系统的例子来作比,译本中除了解释计算机专业的部分要用到计算机术语,在解释心理学原理的时候写得也颇有“Geek”的味道。比如第8章解释人的注意力很容易转移的时候,把注意力比作稀缺资源,容易被释放并转移到更重要的信息上。这里有点像C/C++释放资源的感觉。
这样的语言风格决定了这本书表达清晰,逻辑分明的特点。但于此同时也带来一个问题——多数的读者是没受过计算机专业训练的,大量的计算机术语会使人望而却步。当然,作者与译者或许都认为,软件开发者无论是测试、运维、后台还是前端,都至少应该要了解简单的心理学,基本的交互原理。可惜这理想很丰满,现实却很骨感,相信多数程序员都对交互和设计兴趣不大——虽然我也是程序员——而设计师们则因为看不懂这些术语亦兴味索然。
不过于我个人而言,读这本书仍感到受益匪浅。尽管生物神经的解释有时比较枯燥,但至少让没受过美术和设计专业训练的我知道了格式塔,知道了基本交互原理,还有些从未接触过的想法。在平时的工作中,自己很多时候只是自然而然就这么做了,但是并不知道为什么,解释为什么就是一种理论总结,借助这本书的理论,我得以在项目中与成员沟通时更好地解释自己的想法。
我现在看书的时间基本安排在中午休息的时候,不比之前在学校的清闲,现在看书只求能让自己在工作的忙碌与混沌中看到一丝灵感的光亮,代码诚可贵,想法价更高。无论架构也好设计也罢,无论营销也好推广也罢,混沌的脑袋出不来好的想法。近期看佐藤的整理术有所受益,再看这本认知与设计又有受益,心情不由也跟着好起来,工作也跟着有热情了,不由感慨心情对自己状态调整重要性。我觉得程序员们不妨多尝试进行技术外的阅读(或者画画或者其他不同于技术的活动),即使只是作为心理调整也好。
===============以下是读书笔记===============
Chapter 1.我们感知自己的期望
我们自身的经验,所处的环境以及我们做事的目标都会影响我们的感知。所以在设计中我们必须:
1.避免歧义(如:不要用跟用户经验不同的表达)
2.保持一致(如:不要随意更换元素经常出现的位置)
3.理解目标(如:提前预知用户的目标而作优化,预读下一页等)
Chapter 2.为观察结构优化我们的视觉
本章简单讲述了格式塔(Gestalt)原理,都是我们平时显而易见但是缺少总结的方法。
如,接近性原理——我们会把接近的物体视为同一组。类似的还有相似性、连续性、封闭性、对称性、主体/背景和共同命运(如一起运动的物体看作一组,不动的为一组)原理。有兴趣深入研究的童鞋可以看看[德]库尔特 考夫卡的《格式塔心理学原理》(Principle of Gestalt Psychology)。
Chapter 3.我们寻找和使用视觉结构
视觉结构能提高我们阅读速度,浏览长数字的能力。本章讲述了视觉结构在实际设计的运用,如:把大段文字分结构显示(ul li样式),把长数字分成几段输入(银行卡帐号)等。
Chapter 4.阅读不是自然的
本章首先解释我们的阅读能力不是与生俱来的,而是后天培养的。熟练的无意识的特征驱动阅读和费劲的语境驱动阅读是设计中需要注意的。在设计的时候我们应该支持用户阅读而不是干扰,出现的文字尽量短小精悍,尽量以图代文。
Chapter 5.色觉是有限的
本章解释了人类眼睛成像中重要的视锥细胞和视杆细胞的工作主次,得出视觉是为边缘反差而不是为亮度优化的结论。提示我们在设计的时候,应当注意区分色块反差,做到不干扰用户的视觉。
Chapter 6.我们的边界视力很糟糕
我们的视域范围有中央凹和边界视野,我们大多数的注意力都放在中央凹上面,眼球会根据环境自动做快速移动扫描,从而使得我们的眼睛能很快转移到另一个物体上面。由于阅读是后天的,所以基本上需要中央凹的持续注意来支持,这也是我们阅读时感到比看图更费力的原因之一。设计上我们应该尽量避免把重要信息放在边界视野上面,这有可能使信息进入用户盲点。
Chapter 7.我们的注意力有限,记忆力也不完美
记忆有短期记忆和长期记忆之分。
短期记忆低容量而且高度不稳定。这提示我们在设计时应该尽量避免大量的模式运用,在多步操作的时候应该时时给予用户提示信息,否则用户很可能进入下一步就忘记上一步做了什么。
长期记忆的特点是容易出错、印象派、可修改的。长期记忆需要我们使用工具去加强它,而设计中应该避免造成用户长期记忆的负担。比方现在多数的系统需要我们记住一长串很难记的密码。
Chapter 8.注意力对思考以及行动的限制
我们专注目标,很少注意到工具。当我们在进行一项任务的时候,我们通常只关心这项任务怎么实现而且实现得怎样了,至于工具本身长啥样我们通常不关心,而作为工具本身,我们在设计的时候就应当让它处于背景,减少对用户目标的干扰。
我们的思考周期:目标、执行和评估。几乎我们所有的任务都可以这么划分。制定目标,执行目标,最后评估结果。
这样的分析方法有助于我们在设计的时候去仔细分析用户行为的每个阶段,找出关键点,进行设计。
由于我们的注意力很容易转移,很经常我们会忘记收尾工作,像在网吧登录SNS忘记退出就走了之类的。这就需要我们在设计的时候,尽量让机器作收尾工作,当用户忘记的时候要给予提醒。这点上,iOS的app没有“退出”的概念,就是系统帮助用户管理内存和进程的一个例子。
Chapter 9.识别容易,回忆很难
本章先从心理学的角度简述识别一个图形或者一个人的脸很容易,而从自己的脑袋里去回忆某件事情却很难。对于设计来说,由于识别很容易,我们可以采用缩略图方式来展示相册。
Chapter 10.经验中学习很容易,解决问题和计算很难
我们的大脑分为
旧脑->中脑->新脑
三个部分。由低级到高级。
操作已经学会的动作很简单,学会新的动作很难。对于设计来说,应该尽量引入用户熟悉的元素,如Apple最常用的拟物设计,还有触屏的流行。
Chapter 11.许多因素影响学习
当操作专注于任务、简单和一致时,我们学得更快。另外文案设计应该尽量让用户感到熟悉和一致,有助于用户学习。
Chapter 12.我们有时间要求
这对于程序来说,性能要求是其一,交互响应也是很重要的一环。这一章讲述了人类对于常见情况的反应时间,对于设计的感性分析很有帮助。