2022-08-20 原《每周读书》系列更名为《枫影夜读》

上一次写每周读书已经是 13 年 8 月份了,东野圭吾的《流星之绊》,转眼已过去半年了,慨叹时光飞逝什么的虽然老套,却是事实。刚开始工作的时候,说起我写《每周读书》,leader 怀疑地说你能每个礼拜读完一本书?直到今天,由于工作的关系,不仅是每周读书没有每周读完一本书,就是写作、吉他都很少去触碰了。这不是什么好的现象,尤其是 13 年年底,转到广州部门之后,这里的工作时间比以前要再长一些,就更体会到什么叫做“没有时间”了。
最近看《极简欧洲史》和《世界简史》,以这两本书的相对广袤的时间视角去看,这世上多数人都在过着一样平凡而单调的生活,而且其实不是自己主动去思考的结果,多数都是随波逐流罢了。我自己当然不想随波逐流,但是固有的限制太大,也不过是在这些限制之中努力去寻找差异罢了。
与其望着似水流年自怨自艾,还不如给点实际行动出来。但是对我而言,最大的阻碍大概便是自制啊。在广州的生活虽然有点日夜颠倒(其实比起去年的广州已经要好上很多,但还是日夜颠倒),但如果我自制得了,那么每天晚上下班回家,洗澡便睡,第二天起来便可以多出些时间来自己做些其他的事情了,还有午休的时间,饭后的休息时间诸如此类。谈何容易。
罢了,这些牢骚便到此为止吧。这几天看了东野圭吾的《盛夏的方程式》,这部小说是 11 年出版的,中文版是 12 年。东野后期的作品其实真心没什么看头了,《放学后》让我第一次认识东野圭吾,《白夜行》、《幻夜》和《嫌疑人X的献身》都属于巅峰之作,令人大为赞叹,到后来这些年,《红手指》、《毒笑小说》一类作品,实在食之无味了,弃之亦不可惜。
《盛夏的方程式》其实还是算有些看点的,只是不如巅峰作品一样紧凑扣动人心。
再看《极简欧洲史》。以前对欧洲的认识是分散的,割裂的,没有一个完整的思路去把所有的事件和碎片串联起来,这部《极简欧洲史》,以简练通俗的文笔,将欧洲史整个梳理了一遍。
首先该书把欧洲史大致分为古典时期、中世纪和现代,以这个时间轴讲述了欧洲最重要的希腊罗马文化、基督教文化和日尔曼文化这三大元素在欧洲大陆上的冲突和并存。
之后,在这种大背景下,又讲述了欧洲的君主和民主,语言的发展史,等与中国大相径庭的文化,正是欧洲这种自古君主受制于民的文化,才能自发地产生现代民主。而这样看来民主也不过是一种制度罢了。
看完这本书,我觉得最大的收获有几点:
在中世纪欧洲的国家实际上并没有非常明显的分界。古希腊时期只要是城邦组成,后来罗马帝国时期实现了欧洲真正意义上的统一,但是罗马灭亡之后,欧洲就长期出于分裂状态,各种满族入侵欧洲大陆,出现了大量的小国,神奇的是这些小国的君主可以随意穿越,英国的国王可以从法国王室里面找个人过来当。这也跟君主本身权力没有太强有关。
教皇。教皇本身是掌管教会的。尽管基督教很早就被耶稣创立,但是知道四世纪成为罗马帝国国教之后才慢慢兴盛起来,直到整个欧洲大陆,人人都是基督教徒。教会出现以后,便拥有教会自己管辖的封地及收入,所以教皇实际上统治的是整个欧洲大陆所有的基督教徒,比起一个小国的国王来说,管辖的地域要更加广泛。国王是由教皇来加冕的,但是教皇也是脆弱的,需要国王提供保护。这两大势力长期以来竞争合作,互不相让,但是从来没有真正意义上地分出过胜负。也算是挺神奇的文化现象了。现在的教皇依然拥有自己的封地,梵蒂冈。
以上是最近读过的两本书,比较推荐《极简欧洲史》,篇幅不长,内容却挺丰富,可以从中一窥欧洲历史。
今天有同事问我之前写的那篇 iOS 常见 Crash 及解决方案 里面粘贴的 GLibC 关于 memcpy 的代码怎么理解,然后我囧了一下,当时就是随手一 copy,其实没理解透,于是花了点时间看了一下,学了不少东西,写篇博客记录一下。这里真得感谢一下 @raincai 同学的提醒。之前我粘贴的代码如下:
#define BYTE_COPY_FWD(dst_bp, src_bp, nbytes) \
do { \
int __d0; \
asm volatile(/* Clear the direction flag, so copying goes forward. */ \
"cld\n" \
/* Copy bytes. */ \
"rep\n" \
"movsb" : \
"=D" (dst_bp), "=S" (src_bp), "=c" (__d0) : \
"0" (dst_bp), "1" (src_bp), "2" (nbytes) : \
"memory"); \
} while (0)
其实上面这段代码有点问题,整理一下应该是这样:
#define BYTE_COPY_FWD(dst_bp, src_bp, nbytes) \
do {
__asm__ __volatile__ (/* Clear the direction flag, so copying goes forward. */ \
"cld\n" \
/* Copy bytes. */ \
"rep\n" \
"movsb"
:"=D" (dst_bp), "=S" (src_bp), "=c" (__d0) \
:"0" (dst_bp), "1" (src_bp), "2" (nbytes) \
:"memory");
} while (0)
我们一步步来解,看到已经理解的直接跳过就是了。
linux内核代码很多宏都要加上这个,主要是为了是为了防止被调用的时候,复杂语句有些没被执行到。
举个栗子:
#define SOMETHING()\
fun1();\
fun2();
这个宏是为了能执行到 fun1 和 fun2,但是如果你调用这个宏的时候,加上了条件判断:
if (condition == true)
SOMETHING();
那就悲剧了,预编译的时候,宏定义被代码替换掉,那就是
if (condition == true)
fun1();
fun2();
fun2()就掉到判断的外面去了。所以加上这个是为了保险。
这个其实就是用于在 C 语言内嵌汇编的关键字 asm, 有下划线的是个宏,看源码是这样定义的:
#ifndef __GNUC__
#define __asm__ asm
#endif
volatile
跟 asm 类似,带下划线就是个宏,其实就是 volatile 关键字:
#define __volatile__ volatile
带上这个关键字就是告诉 GCC 不要做优化,要完全保留我写的指令,不要做任何修改。所以这个关键字是可选的。
所以总的来说,在 C 语言里面,内嵌汇编的写法就是
__asm__ ("汇编代码段")
或者
__asm__ __volatile__ (指定操作 + "汇编代码段")
复位方向表标记位 DF,即 DF = 0。DF为 0 则源寄存器地址 ESI/EDI (源寄存器/目标寄存器) 递增,1 则递减。
表示重复,repeat,当 ECX (计数器) > 0 的时候就一直 rep。
就是搬移字串,汇编搬移字串有 movsb 和 movsw 两种,movsb 就是 moving string byte,就是一次搬一个字节,mvsw就是搬移字了
EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP等都是X86汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。如果用C语言来解释,可以把这些寄存器当作变量看待。
EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
EBX 是"基地址"(base)寄存器, 在内存寻址时存放基地址。
ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
EDX 则总是被用来放整数除法产生的余数。
ESI/EDI 分别叫做"源/目标索引寄存器"(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.
EBP 是"基址指针"(BASE POINTER), 它最经常被用作高级语言函数调用的"框架指针"(frame pointer).
OK,接下来是那些冒号,插入C代码中的一个汇编语言代码片断可以分成四部分,以“:”号加以分隔,其一般形式为:
指令部:输出部:输入部:损坏部
=D 这样的语句是对输出部的约束条件:
常用约束条件一览
m, v, o —— 表示内存单元;
r —— 表示任何寄存器;
q —— 表示寄存器eax、ebx、ecx、edx之一;
i, h —— 表示直接操作数;
E, F —— 表示浮点数;
g —— 表示”任意“;
a, b, c, d —— 分表表示要求使用寄存器eax、ebx、ecx和edx;
S, D —— 分别表示要求使用寄存器esi和edi;
I —— 表示常数(0到31)。
所以 "=D" (dst_bp), "=S" (src_bp), "=c" (__d0) 就是把 dst_bp 放进 EDI 寄存器, src_bp 放进 ESI 寄存器, __d0 放进 ECX 寄存器。
:"0" (dst_bp), "1" (src_bp), "2" (nbytes) 这里的 0, 1, 2 不属于上面约束条件的字母,而是数字,数字代表跟输出部的第 0/1/2 个约束条件是同一个寄存器,那就很好理解了,就是说 EDI 寄存器里面将会输入 dst_bp, ESI 会输入 src_bp,最后的 ECX 会输入 nbytes 这个变量。
这里以“memory”为约束条件,表示操作完成后内存中的内容已有改变,如果原来某个寄存器(也许在本次操作中并未用到)的内容来自内存,则现在可能已经不一致。
总的来说就是使用movsb指令来按字节搬运字符串,先设置了 EDI, ESI, ECX 几个寄存器的值, 其中EDI寄存器存放拷贝的目的地址,ESI寄存器存放拷贝的源地址,ECX为需要拷贝的字节数。所以最后汇编执行完之后,EDI中的值会保存到dst_bp中,ESI中的值会保存到src_bp中。
这个函数有几个版本的,上面是汇编版本,下面这个是 C 版本,这个就很好理解了:
do \
{ \
size_t __nbytes = (nbytes); \
while (__nbytes > 0) \
{ \
byte __x = ((byte *) src_bp)[0]; \
src_bp += 1; \
__nbytes -= 1; \
((byte *) dst_bp)[0] = __x; \
dst_bp += 1; \
} \
} while (0)

从日升昌走出来,对面就是和日升昌纠葛一个整个世纪的“蔚泰厚”票号。常谓一山不容二虎,日升昌的除了大掌柜雷履泰之外,二掌柜毛鸿翙也是有才之士。毛鸿翙后来执掌蔚泰厚票号,连“蔚丰厚”、“蔚盛长”、“新泰厚”和“天成享”为“蔚”字五联号,成为当时全国规模最大的票号联盟,后期甚至比日升昌还要昌盛。

当时蔚泰厚的老板侯庆来是平遥西南的介休人氏,其父侯兴域在祖业之上苦心经营多年,给侯家积累了大量财富,单在平遥的商号就有协泰蔚、厚长来、新泰永、新泰义、蔚盛长五家。嘉庆十三年左近,侯兴域去世,不久长子泰来、次子恩来相继去世,于是三子侯庆来便主掌了家业。当时日升昌创立票号,极短时间内汇兑生意做得极为红火,侯庆来看着眼红,自恃家财颇丰却苦于没有一个有才干的经理,迟迟未能介入票号行业。要知道当时晋商经营是两权分离,财东只负责投资和选掌柜,实际经营还得是掌柜来做,侯氏正是有钱缺人。而恰恰在这时候,日升昌两个掌柜的一起内斗,便成了侯氏票号起家的及时雨。
日升昌初创之时雷履泰与毛鸿翙齐心协力,日升昌业务蒸蒸日上,但是时日久了,毛鸿翙不甘位居人下,常有揽权之意。正巧雷履泰身染重病,但仍在大掌柜房休养,于是票号大小事务还是得请大掌柜批示。毛鸿翙便趁机对少东家李箴视进言,让雷履泰回家养病。其时正是道光六年,李大全病故,李箴视年方十六,初掌家业,其为人也是秉性忠厚,朴诚无文,于是便听信毛鸿翙建议,对雷履泰说:“你患病多日,号内不能静养,可且回家休养。”雷履泰不知李箴视心性单纯,还以为话中有话,于是脸上不动声色,却答应着回家去了。
雷履泰回家后细思气极,于是给各个分号写下书信,意欲撤回分号。次日李箴视来探望雷履泰,看到桌上书信,不由大惊,便问雷履泰道:“这是为何?”雷履泰淡淡的说:“票号是你家的,各分庄则是我安的,我召回来不过吩咐给你,没什么意思。”此时李箴视便是再笨也明白雷履泰的意思了,何况他只是经验尚浅,为人却极有见地。当下解释道:“李某请雷掌柜在家静养,真心是为了你早日康复,别无他意,雷掌柜千万不要误会。”李箴视再三解释,雷履泰只是不听。
雷履泰一手创办日升昌,从道光三年至当时不过三年,票号业务未稳,李箴视又是初掌家业,如若没了雷履泰的协助,实不知如何是好,于是李箴视双膝一软,当场给雷履泰下跪。雷履泰心性极高,一句“在下可以受不起”,便任他跪去。
李箴视脾气也是极倔,便道:“雷掌柜不答应,我就不起来。”这一跪就是大半天,直到半夜,雷履泰确信少东家确无异心,便把他扶起来,说:“让我回去,大量不是你的主意,其非毛某乎?”
雷履泰虽答应不撤分号,却也不即刻回票号办事,只是在家呆着。于是李箴视便让人每天送酒席一桌,白银五十两到雷履泰家里,誓要求得雷履泰回来。这时毛鸿翙看到少东家全心倚仗雷履泰,而自己又与雷履泰不和,自觉此地再无容人之处,于是心灰意冷,主动请辞,离开自己供职多年的西裕成颜料庄,自己参与创办的日升昌。
离开日升昌的毛鸿翙,只觉怀才不遇,前途迷茫,不知何去何从。便在这时,酝酿票号多时的侯庆来,成为了毛鸿翙的知遇之主。于是“蔚泰厚”便在毛鸿翙的主持下改组为票号,毛鸿翙也以“蔚泰厚”票号为一身抱负施展之地,誓以“蔚泰厚”与雷履泰一决雌雄。
但是经营票号并不是光有资本和人才就足够的,“蔚泰厚”票号初创之时,虽然业务日渐增长,但与日升昌相比仍差距甚远。而且日升昌的前身西裕成颜料庄本就在全国各地有十多家分号,而侯氏光“蔚泰厚”一家实在难以望其项背。于是侯氏动员旗下数家“蔚丰厚”、“蔚盛长”、“新泰厚”和“天成享”四家绸缎庄,全部改组为票号,毛鸿翙又拉拢日升昌旧日熟人郝名扬、阎永安任票号掌柜,自此侯氏票号渐成规模,五家票号联合被称为“蔚”字五联号。这五个票号每家都在全国各地有数十家分号,合五家之力与日升昌比拼。初时“蔚”字五联号的总资产堪与日升昌持平,后来渐渐地超越了日升昌。
毛鸿翙和雷履泰自此在各地市场相互争斗,直到任何一方最终故去。“蔚”字五联号与其他山西票号的历史命运类似,都在经历了庚子之变,太平天国之后,最终消失在辛亥革命的战乱之中。所谓“革命”,真的不像教科书说的那样和平。


我们到平遥的时候是秋天,秋天的阳光慵懒如猫,摊在墙上。我们沿着北大街一路往南,很快就走到了东西南北大街交汇的地方。
这里人头攒动,四条大街的人流汇在一起,老人小孩,游客团体熙熙攘攘,路边的香草肉热腾腾地冒着蒸汽,街上的各种金字招牌在阳光底下晃晃地闪耀着许多不知真假的流传了千年的名字。我们穿过人流,来到一个人气颇旺的院落门前,抬头一看,嗯,来平遥的目的地到了。
如果说尹吉甫征俨狁是平遥诞生的伊始,那么眼前这座大院——日升昌票号——便是平遥兴衰的见证。自从雷履泰和李大全于道光三年(1823年)创立了日升昌,这家票号就注定了要让平遥在百年之中一跃而成全国的金融中心,又在朝代更迭之中辗转而终究一落千丈。日升昌票号历经道光、咸丰、同治、光绪、宣统五代皇帝和中华民国,鼎盛时期分号遍布汉口、天津、济南、西安、开封、南京等地共四十多处,执全国金融之牛耳。当时各地富商争相仿效日升昌开设票号,而全国五十一家票号就有二十二家在平遥,可以说是日升昌成就了平遥,使其在历史中留下灿烂的一笔。而今再看日升昌旧址,昔日繁华不在,宏伟的院落被熙熙攘攘的游客拥得水泄不通,站在大掌柜房门外,连转个身都困难,真是哭笑不得。

大掌柜房看上去颇为窄小,布置也极简单,跟账房满屋算盘天平笔墨纸砚相比,更像是一间供人沉思的静室。好在因此游客大妈们都对这间小房间不太感兴趣,可以在此驻足多看一会。当年创始人雷履泰便是在此冥思苦想,一边探索一边带领着日升昌一步步走向巅峰。雷履泰也不是凭空就能创造出这么一个惊世骇俗的行业出来,日升昌的成功有个先决条件:晋商的兴盛。
晋商本以经营边防军需物资起家,随后又经营“盐运”,凭着山西南部的盐池,在卖盐的期间积累了大量的财富。后来徽商兴起,逼得晋商把目光从盐运转向对外贸易,在明末通过向后金走私大量军火等物资又重新兴盛起来。到了雷履泰时期已是清朝道光年间,晋商已经遍布天下,雷履泰当时所供职的李大全的“西裕成”颜料庄,除了平遥达薄村本部拥有颇具规模的手工作坊之外,在北京、天津、汉口、重庆等地都有分庄。这就给了雷履泰大展宏图的客观条件:有雄厚的资金,有遍布各地的分庄,有遍布天下的晋商,即广大的市场。
当时晋商在外,往家里捎钱的时候极为不便,大量钱银必须走镖,镖费贵而且并不安全,于是有人便想到把钱交给西裕成分号,由分号掌柜亲笔写信给总号,最后再到平遥总号取钱。起初还只是朋友亲戚相求,并不收取费用。后来同乡觉得这种办法挺好纷纷来投,甚至愿意支付一定的费用。于是雷履泰觉得这是一个商机,便是借鉴史上汇兑的经验,兼营起汇兑业务,初试之下,盈利颇丰。终于道光三年,雷履泰和李大全共同创设了“日升昌”票号,从颜料庄转而经营汇兑生意。

从零开始创设一个票号实属不易,除了雄厚的财力和遍布天下的分号,还要有极好的信誉和极高的人才管理能力。雷履泰在创设“日升昌”之后,业务日渐繁忙,由此推想其他各地的商人托镖局押运银钱一样会有诸多麻烦,于是除了颜料庄原有的分号,又在濟南、西安、開封、成都、重慶、長沙、廈門、廣州、桂林、南昌、蘇州、揚州、上海、鎮江、奉天、南京等地先后设立分号,雷履泰亲自联络晋商,招揽业务,在他的经营下,业务蒸蒸日上,慢慢地不只晋商,外省商人,甚至沿海的米帮,丝帮也通过日升昌进行汇兑,在雷履泰治下,日升昌真正做到“汇通天下”。

道光八年,江苏巡抚陶澍曾上奏曰:
向来山东、山西、河南、陕西等处每年来苏置货,约可到银数百万两,……自上年秋冬至今,各省商贾系汇票往来,并无现银运到。
日升昌道光三年创建,短短五年时间,已经成为江苏商人资金往来的主要手段,也因为汇票这种虚拟信用货币加大了市场流通性,而导致江苏通货膨胀,物价上涨。由此日升昌业务之兴盛可见一斑。
日升昌的成功一是靠着李家雄厚的财富,二是在山西占着晋商商路之中心,占尽地利,三是晋商遍布天下,资金流转的需求极强,最后便是日升昌自身信誉保证,最终催使票号的诞生。这些都还是大背景下的客观条件,在运营票号的时候,前无古人之鉴,要从零开始思索票号的发展路线,设计一套稳妥的密文,培养一帮可靠的伙计,都不是容易的事。所以日升昌除了大掌柜雷履泰,还得有二掌柜毛鸿翙以及其他未入史册的大将方才得以支持。而二掌柜这位奇才也有一段精彩的故事,我们回头再详说之。
且说日升昌兴起之后,山西富商也纷纷效仿,直到咸丰十年,山西票号已经发展到一十七家,光绪中年已遍布全国共四百余家分号。可惜后来太平天国兴起之时,连年战乱导致票号开始衰退,至辛亥革命,山西票号相继倒闭,从此空余大院座座,这一百年间无数个故事被埋进砖缝,在墙上斑斑驳驳,只等着对过往的游人诉说。
山西其实并不是我最想去的地方,古韵盎然的江南水乡,幽僻安逸的世外桃源,黄沙万里的玉门关外,还有咸咸海边的宝岛台湾,都是我顶想去而没去过的地方。这次把山西纳入行程主要还是为了拣一个清净的所在。于是摊开地图,圈点几处,竟一路游上了内蒙。
山西似乎有道不尽看不完的古代建筑,但这一路上最是令人沉浸其中的,还得是平遥古城。现在回忆起平遥的砖瓦与城楼,虽不及凤凰一般具有异族风情,山水烟雨迷迷蒙蒙,但其一砖一瓦之间,一宅一楼之中,却蕴藏着凤凰所没有的历史的故事。在凤凰,看景色,在平遥,我们听故事。
故事从诗经开始:
昔我往矣,黍稷方华。今我来思,雨雪载途。王事多难,不遑启居。岂不怀归?畏此简书。

这几句出自《诗经·小雅·出车》,写的是西周末年,西北俨狁犯境,宣王为中兴周室,命大将尹吉甫北伐猃狁之事,当时尹吉甫驻兵于平遥,修西北二面城墙,被平遥人认为是建城的始祖,而这也是平遥在中国历史记载中的第一次出场。说起西周,为人们所熟识的大约便是开国皇帝文王武王,以及末代皇帝——烽火戏诸侯的周幽王。这宣王便是周幽王的父亲。其时周朝疆域在经过成、康二帝的开拓后已经北至肃慎,南到汉水,东到大海,西至渭河,幅员辽阔。但是其后由于西北戎狄逐渐壮大,国家处于常年征战之中,历经四代皇帝,国力耗尽。直至周宣王时整顿朝政,才使国力有所复兴。当时平遥的位置正处犬戎西周边境,乃军事重镇,于是这道战火中修起的城墙,从公元前八二三年,便默默俯视着这座古城两千八百余年的兴衰起伏。

尹吉甫与平遥的渊源只是传说,除了诗经以外,便是清光绪八年的《平遥县志 ·建置》中的记载:
旧城狭小,东西二面俱低,周宣王 时,大将尹吉甫北伐猃狁,驻兵于此,筑西北二面。
也已距周朝两千余年。今天再到平遥,除了上东太和门破败的尹庙、人迹罕至的点将台及尹吉甫墓等遗迹之外,再无尹吉甫的音容事迹。平遥在中国历史上曾经太繁华太灿烂,以致筑城的祖先被掩在城东一角,匆匆旅客流连在东西南北大街,在文武城隍之庙,在票号钱庄之中,而忘却了这遥远的历史。
我们便不曾去寻尹吉甫的古迹,这座城池可看的历史太多,甚至还来不及一一走过便已离此北上。我们从北门开始,走进拱极门的瓮城。拱极二字出自于《旧唐书·礼仪志二》:
叶台耀以分辉,契编珠而拱极。
拱极即指北极星,平遥的城墙于明洪武三年曾大修过,在旧城墙“九里十八步”的基础上扩建成今日的样子。今天站在城墙上俯瞰这座小城,一座座四合院栉比鳞次,皆为灰色的清水砖墙所砌,望眼过去犹如黄沙万里,天地一线,南北大街之间,市楼倚立之下,男女老少熙熙攘攘,一座丰满生动的古城跃然活于眼底,这是一座仍旧活着的古城。

下了城墙,我们沿着古城北大街南下,随意转入一条小巷以避开汹涌的人流。这里同其他古城景区一样,处处有住宿,家家是客栈,不同的大约是这里的客栈多是四合院土炕房,与凤凰的吊脚楼相比有不同的体验罢了。客栈老板娘是当地人,与其交谈只觉当地人许是悠闲惯了,事情多有爱理不理的意思。这让我想起平遥在文革那场浩劫中能完整保留下来的原因:穷。因为穷,拆不起城墙建不起高楼,平遥便一直维持着原状,到后来改革开放了,有钱可拆了,在有识之士的劝谏下,又保留了古城,申请了文化遗产,从而成为中国今天保存最为完好的古城。但即使在今天,平遥也还算是个贫穷落后的地方,客栈的老板们似乎只要每年旺季的时候捞上一笔就算了,该过悠闲的日子还是悠闲着。

后来才知道,能住在古城里悠哉悠哉的居民也是有限的。从 1997 年开始,平遥政府为了保护古城,应对日益增多的客流量,开始慢慢迁出古城内的居民,现今古城已外迁近半数人口,只留下 2 万多人。不晓得对迁出的居民来说是幸或不幸,但十几年过去了,平遥的旅游开发似乎都未达到凤凰那般成熟。许是出于保护,许是出于政策,或是本来人们便慵慵懒懒,你来亦好不来也罢,我有我的生活,我住我的古城。
从客栈出来回到北大街,一路商铺食肆虽然繁华,但其实平遥除了冠云牛肉名声在外,其他店铺基本都不入流。这也是平遥旅游开发程度不高的表现之一。北大街一路食肆,吃的名头无非那几样,多属面食,除了名字可能没听过之外,味道都是普通面食的味道,而且淡而无味。价格虽没到其他旅游景区那样高价,但也不算太便宜了。北大街一路下来,除了豆腐脑算挺有味道,就餐的另一家食肆只能说难以下咽,失望而走。
循着地图一路走向南大街,地图上看东西大街是一条直线,南北大街却是错了开来。这与平遥本身的设计格局有关,平遥又名“龟城”,南首北尾,上下东西四门即为神龟四足,城南柳根河河岸蜿蜒,城墙亦随之蜿蜒起伏,柳根河主干汾河在平遥境内是略偏南北走向,于是古城垂直与汾河程略偏东西的南北走向,南城门便立在东南角,再立两口石井意为龟眼。南北大街成“S”形为神龟爬行之态,龟头在东南是朝东摆,龟尾瓮城即拱极门的西北角设计为钝角,则意喻龟尾朝西摆。古人以“龟”筑城,是期望城如龟般固若金汤,长治久安。而平遥历经两千多年仍能保存得这么完好,大约也应了这“龟城”之说了。


上图是现在的我(11 月 28 日)跟 8 月 20 日的对比。原先我是个挺瘦的瘦子,今年 6 月份开始到公司的健身房去尝试增肥锻炼,那时什么都不懂,就是瞎练,举举哑铃做做器械什么的。一个月后算是有点点效果,这时候大病来袭,一场病持续了两个月,我的体重也急剧下降,两个礼拜几乎降了 10 斤。病好了以后决心要把肉练回来,于是一路练到现在。
有同学问我锻炼的方法,想把身体练健康一点。于是我决定把我的健身的经验写下来。首先一点很重要:三分练七分吃。饮食是至关重要的一环。
无论是像我一样吃什么都长不胖的瘦子还是吃什么都容易胖的人,都需要注意饮食。由于肌肉主要是蛋白质组成的,所以每天要保证足够的蛋白质摄入量,简而言之就是少吃多餐。基本上每天的饮食可以这样安排:
大约8点到9点左右。2-4个鸡蛋,吃2个全蛋,可以外加2个蛋白(不要吃太多蛋黄会胆固醇过高,一天两个蛋黄是可以接受的)。我现在只是吃两个全蛋,外加一大杯牛奶,偶尔会加上两片面包。
大约10点半到11点左右。可以吃一个面包或者一杯酸奶,小吃即可。
要保证碳水化合物充足摄入,主要是米饭。如果容易胖的人,中午就不要吃太多肉,吃肉的时候最好吃鸡胸肉,去掉皮,因为皮下脂肪多,容易发胖。
大约下午4点。我一般吃两个蛋糕,保证5点半去健身的时候有足够的血糖,不然健身的时候血液集中到肌肉上容易头晕无力,导致锻炼时间过短。
大约6点半到7点。看自己健身的时间,一般要在健身结束后的两个小时内进食。一般我晚餐吃一块鸡扒,一碗米饭,还有其他的肉和蔬菜。
一般健身训练都安排在下午,所以晚餐至关重要。当你训练的时候肌肉是不会长的,这时候只是刺激肌肉,在休息的两个小时里,机体会寻找蛋白质补充肌肉的劳损,所以这时候必须摄入充足的蛋白质。早餐的牛奶鸡蛋和晚餐的肉就很重要了。
如果是像我一样怎么吃都不胖的人,那就不用理会那些去掉鸡皮啦,少点碳水化合物之类的禁忌,只要不停地吃就可以了。
健身训练为的是两个目的:减脂和增肌。每个人的身体都有肌肉,看不到一是可能肌肉不够发达一是脂肪太多看不出线条,所以要增肌和减脂。但是二者没法同时进行,增肌的时候减不了脂,减脂的时候无法增肌,所以要错开。
增肌靠力量练习,减脂靠有氧运动。一般比较健康的人都容易吃胖,容易吃胖的人建议这样安排训练时间:一周练6天,3天力量练习3天有氧运动,间隔一天力量一天有氧,1天休息。
肌肉分为大肌肉群和小肌肉群,大肌肉群就是胸背腿,小肌肉群就是肱二肱三和肩膀。大肌肉群需要大强度练习,练习后需要休息3天,小肌肉群可以天天练都没关系。一般是一天练一个大肌肉群搭配一个小肌肉群,隔天做一次有氧运动。其中腹肌是特殊的肌肉群,需要每天都练习。因为无论你做什么动作基本都会用到腹肌,所以腹肌是最难疲劳的肌肉,需要天天练才会有效果。
建议饮食容易发胖的人这样安排时间(如果动作不清楚的 google 一下都有视频可以看):
胸(俯卧撑 + 杠铃卧推) + 肱二头肌(哑铃弯举 + 锤击式哑铃弯举 + 二十一响礼炮) + 腹肌(仰卧起坐 + 腹肌八分钟)
有氧运动(跑步机或者疯狂单车30分钟) + 腹肌(仰卧起坐 + 腹肌八分钟)
周三:背(硬拉 + 背阔肌器械 + 杠铃耸肩) + 肱三头肌(哑铃颈后屈伸 + 哑铃臂后弯举) + 腹肌(仰卧起坐 + 腹肌八分钟)
有氧运动(跑步机或者疯狂单车30分钟) + 腹肌八分钟
腿(史密斯机深蹲 + 箭步蹲) + 肩(哑铃前平举 + 哑铃侧平举 + 杠铃划船) + 腹肌(仰卧起坐 + 腹肌八分钟)
以上运动,胸背腿都是每组 15 次,每个动作做 4 组。其他小肌肉群就每组 10 次,一个动作 4 组。腹肌就仰卧起坐 40 次,再做腹肌八分钟。
如果是像我一样吃不胖的,就可以不需要有氧运动来减脂了,直接去掉有氧运动循环一周就行了。当然有氧运动可以增强体力,也是不错的运动。
注意:用到杠铃的运动都是复合运动,一定要在做之前搞清楚动作要点否则锻炼不成反而伤身。比如杠铃卧推,有宽握窄握,一般重量上去了要有人护着否则容易失去平衡砸下来。硬拉和史密斯机深蹲一定要注意动作到位,否则伤膝盖。运动前和做完一组运动之后最好做一下拉伸运动,可以减少肌肉酸痛。
今天在改代码的时候看到定义的 delegate 里面都写了 <NSObject> 在后面:
@protocol APerfectDelegate <NSObject>@optional
- (void)optionalSel;
@required
- (void)requriedSel;
@end
由于太久没写 ObjC 了,顺手就给去掉了。回头人告诉我这东西编译时会报 warning。我就觉得奇怪了,其实基本上常用的类都是以 NSObject 为基类的,除非是为了周密考虑,把以 NSProxy 为基类的类给排除掉,否则干嘛非得加个 <NSObject> 协议不可。问了人然后自己也试了一下,发现是在这里 warning:
// Instance method 'respondsToSelector:' not found
if ( _delegate != nil && [_delegate respondsToSelector:@selector(optionalSel)] ) {
[_delegate optionalSel];
}
respondsToSelector 这个方法找不到。明白了,遵循 <NSObject> 是为了确保实现了这个方法,这样在调用的时候就可以直接用这个方法检测是否能响应这个 SEL 了。
其实在 ObjC 1.0 的时候,protocol 的这个 @optional 选项是不存在的,所有的 protocol 方法都是必须实现的。所以不遵循 <NSObject> 也没关系,只要判断指针是否存在然后直接调用就完了。但是 ObjC 2.0 加入了 @optional 特性,于是乎必须使用 <NSObject> 的 respondsToSelector: 方法先做一次判断了。
references: Must Delegates Conform To The NSObject Protocol?
注:本文是对 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等形式。 (未完待续。。。)
