2022-08-20 原《每周读书》系列更名为《枫影夜读》
在马蜂窝看台湾攻略的时候看到有人推荐这本书,于是找来看了看,没想到刚看了第一节就放不下了。
作者石田裕辅本是一个在大公司做事的安安分分的小职员,可以跟其他普通人一样领着不算低的工资,一辈子本分做事,娶妻生子过完平凡的一生。但是偏生他一直有一个不切实际的——环游世界。于是某天他终于下定决心,要骑着自行车,环游世界一圈!
但是当他从机场出发的时候开始,那才是他第一次出国呀。于是从第一站拉斯维加斯的开始,这个旅途就从充满着不顺与荆棘。他的单车从美洲最北边的拉斯维加斯一路到最南端,又从欧洲最北端一路环游欧洲,在伦敦休整了半年之后,又从北非一路南下直到好望角,最后沿着丝绸之路回到日本。全程历时整整 7 年。
这就是不一样的人生啊!
一路上可以记载的精彩、刺激、冒险、感动实在太多,这本书本身并不厚,每一节每一个故事的记载也都挺简洁,读之流畅而激昂,读罢有种“还有很多故事没有记录下来”的遗憾。
漫漫旅途中,作者遇到了很多志同道合的自行车骑士,一起走过几段美洲的历险路程。本来作者想以自行车骑遍世界的想法就已经足够怪异,却还遇到了如一直住在深山里的人,划独木舟漂流一个月的人,一句话就买上一部“中国制造”的超烂单车却一起骑上一千多公里的人……这些奇人异事构成了这段旅途精彩的故事。
还有在南美洲遇上抢劫差点没命的恐怖事情,在北非遇上土著人差点二度被抢,在黑色非洲碰上疟疾幸亏有好伙伴照顾才不至于丧命,在内蒙古遇上一家人送水才不至支气管炎发作在在风沙中死去等等生死之间的故事。
至于非洲的贫穷至极却还不肯收裕辅买蘑菇的钱,非要塞给他很多水果的婆婆,欧洲邂逅漂亮的学日语的姑娘,还有很多感动,都是人与人之间淳朴的善意呀。
作者花费 7 年时间走过的这段旅程,经历的这些故事,都是我们这些身处钢筋水泥城市里的人们所无法体会得到的啊!
《不去会死!》这本书本身文笔也好,故事描述也好,算不上最上乘的作品,但是其讲述的故事却令人如痴如醉,心潮随之跌宕,是值得一读的好书!
2022-08-20 原《每周读书》系列更名为《枫影夜读》
这是一本读起来有点像“成功学”“xx天心理学”之类的书,但境界比这些“机场书”高得太多。这本书让我第一次接触到 NLP,用个不恰当的比喻,就像“xx教你管理时间”之类的鸡汤跟《Getting things done》一样,一个是鸡汤,一个是真正有实践意义的指导。
NLP,Neuro-linguistic programming 的缩写,译作中文是:
神經語言程式學(Neuro-Linguistic Programming,簡稱為NLP,又譯作身心語言程序學)美國國家醫學圖書館醫學主題詞編號D020557。[1]是一套原理、信念和技術,其核心為心理學、神經學、語言學與人類感知,安排組織以使之成為系統化模式,並建立主觀現實的人類行為,屬於實用心理學與行動策略的一種。维基百科
有点像心理学但是不是,有点像骗人的鸡汤但是又具有具体可实践的技巧。《重塑心灵》这本书是香港学者李中莹先生所著,以书中的说法,当是目前少见的介绍 NLP 的中文资料了。
NLP 是门高深的学问,《重塑心灵》以通俗易懂的语言对其历史、原理、技巧以及如何运用到我们的人际关系、工作、人生做了详细的介绍。
在看这本书的过程中,我不停地以自己的实际情况与书中的内容做对比,一点一点发现自己过去觉得“就是这样”的事情其实也有“原来能这么做的”的情况。发现自己过去常常应付不了的对话其实可以很简单地通过 NLP 的技巧就做得更好。
一边看着书,一边看到微信群、QQ 群里那些“泡妞高手”跟女生的对话,才发现,噢,以前觉得这些人讲话厉害,其实也是有技巧可寻的,其实只要经过练习,也是能做到别人这样的水平的。
第一遍看这本书,我通读了一次全书,接下来,我准备挑出自己最感兴趣的一章进行仔细阅读和练习。 NLP 能帮助提升自己的思考,当然需要一定的练习过程。
简而言之就是,我们的思想基本上分为意识与潜意识,很多人遇到事情不知所措或者遇到美女不知道该说什么,其实就是自己控制不了自己的大脑。有些意识层面的东西我们可以很清晰地去捕捉,但是潜意识的基本上无能为力。 NLP 研究的就是大脑如何运作的原理,利用一些 NLP 的技巧,帮助你与自己的意识甚至潜意识进行沟通,改变自己,从而实现“成功和快乐的人生”。
我想每个人都有对现状的不满和各种烦恼,NLP 几乎就是一套全能的工具。因为基于 NLP 的思想,世界是你的世界,存在与你的脑中,你的世界与别人的世界是不一样的。而要使自己的现状有所改变,只要改变自己的世界就行了。再由于每个人能改变的都只有自己,只有自己可以改变自己,你最多只能影响别人去改变自己。所以首先要改变自己。
《重塑心灵》这本书的理论和技巧很多,技巧是工具,是帮助我们入门,去改变自己,掌握这些技巧,不过是在行为和能力层次改变了自己,只有运用到这些技巧,在精神层次改变和提升自己,才能更好地实现“成功而快乐的人生”。
这么写着确实有“成功学”之嫌,我一边看也一边有这种感觉,但是看了那些技巧之后我愿意试一试,如果这些 NLP 的技巧确实能帮助我更好地去思考,去工作,去改变自己,那么即使它真有一天被贴上“成功学”的标签也无所谓,我需要的是能改变提升自己的效果。其实,这也是 NLP 的思考方式之一,看你要注重的是什么。如果你做的这件事情能是你得到提高,一句被人说是“鸡汤”的代价又如何呢?最怕的就是真的信了“鸡汤”但事实上自己一无所得的。
上周末一时有空,拎着相机就往中大瞎拍了一通,天气不好,只能拍些近景了。
这几天在整 DMG 安装包打开时添加软件协议声明(SLA, Software License Agreement),这个东西算是个比较古老的东西了。在缺乏文档和相关信息的情况下,往 DMG 文件添加 SLA 还是很折腾人的。
简单地说大致步骤如下:
但是这些鬼东西每一步都坑爹。
首先 SLA_for_UDIFs_1.0.dmg 文件在 developer.apple.com 已经搜索不到,我是在 www.tribler.org 上搜到的,附件可以下载。这里附上 Dropbox 下载地址:https://dl.dropboxusercontent.com/u/6750144/SLAs_for_UDIFs_1.0.dmg
OK,下载完这个 DMG 文件后挂载之,在终端运行命令行:
DeRez SLAResrouces > sla.r
把 DMG 里面的 SLAResources 文件解出来,得到一个 sla.r 文件。如果遇到 DeRez 命令不可用,那你估计没安装 CommandLine Tools,去 Xcode 里面下一个,或者去 Developer.apple.com 也有可以下载的。
提取完的 .r 文件可以直接用文本编辑器打开:
data 'TMPL' (128, "LPic") { $"1344 6566 6175 6C74 204C 616E 6775 6167" /* .Default Languag */ $"6520 4944 4457 5244 0543 6F75 6E74 4F43" /* e IDDWRD.CountOC */ $"4E54 042A 2A2A 2A4C 5354 430B 7379 7320" /* NT.****LSTC.sys */ $"6C61 6E67 2049 4444 5752 441E 6C6F 6361" /* lang IDDWRD.loca */ $"6C20 7265 7320 4944 2028 6F66 6673 6574" /* l res ID (offset */ $"2066 726F 6D20 3530 3030 4457 5244 1032" /* from 5000DWRD.2 */ $"2D62 7974 6520 6C61 6E67 7561 6765 3F44" /* -byte language?D */ $"5752 4404 2A2A 2A2A 4C53 5445" /* WRD.****LSTE */ };
data 'LPic' (5000) { $"0052 0002 0034 000A 0000 0000 0002 0000" /* ...........4.Â.. */ };
data 'STR#' (5000, "English buttons") { $"0006 0D45 6E67 6C69 7368 2074 6573 7431" /* ...English test1 / $"0541 6772 6565 0844 6973 6167 7265 6505" / .Agree.Disagree. / $"5072 696E 7407 5361 7665 2E2E 2E7A 4966" / Print.Save...zIf / $"2079 6F75 2061 6772 6565 2077 6974 6820" / you agree with / $"7468 6520 7465 726D 7320 6F66 2074 6869" / the terms of thi / $"7320 6C69 6365 6E73 652C 2063 6C69 636B" / s license, click / $"2022 4167 7265 6522 2074 6F20 6163 6365" / "Agree" to acce / $"7373 2074 6865 2073 6F66 7477 6172 652E" / ss the software. / $"2020 4966 2079 6F75 2064 6F20 6E6F 7420" / If you do not / $"6167 7265 652C 2070 7265 7373 2022 4469" / agree, press "Di / $"7361 6772 6565 2E22" / sagree." */ };
发现其实就是一个配置文件, DMG 文件被打开的时候, Mac OS X 会自动去读取这个配置文件,然后自动生成一个 SLA 窗口,但是这个文件全是 hex 值咱看不懂,官方给的那个 SLA_for_UDIFs 那个 dmg 里的文档又写得不明不白。里面提及可以用 ResEdit 来打开,但是,坑爹啊,这玩意 Mac 10.7 以后估计就用不了了,靠。
于是乎找另一个替代软件 ResKnife。
搜一下出来一堆下载链接,不要管,直接进 ResKnife 的 Github 页面:https://github.com/slobo/ResKnife
直接 clone 一个到本地,然后 XCode Run 一遍,再用它打开那个官方下载的 SLAResources 文件,大概如下:
这里面 LPic TMPL 是给 Res 编辑器用的,用来解析 LPic 这个类型里面的内容。 直接看 LPic Type, ID 5000 的这条数据。
Default Lan 是默认选中的语言,Count 是多少种语言可选,下面的就是可选的是什么语言了。这里 sys lang 是定义于 CoreService/CarbonCore/Script.h 里面的枚举值。对应的语言是多少得自己去查。
而 local res 就是这个 sla.r 文件里面一样的那些 ID, 比如简体中文是 5010,local res 就是 10。剩下那个不用管。修改这条数据我们就改了那个 SLA 窗口里面选择多语言的下拉菜单。
我们可以看到 English SLA 有两种 Type,一种 TEXT,一种styl,其实是两种不同的数据类型,都是用来填协议内容的文本的,但是格式不同。这时如果我们直接填进英文版本的协议,wow! It works!
但是坑爹的来了,如果在 TEXT 和 styl 里面填入中文等会出来就一定是乱码。一开始我以为是 encoding 的问题,但是换了无数种 encode 方式还是没用,坑爹的。而那个官方说明文件里面只是说 styl 数据跟 encoding 有关,但又不说明跟什么有关。 ResKnife 打开 Text 类型的数据还算能看到文本内容,打开 styl 类型的数据就全是 ... 了我擦。最后 google 了半天,总算有相关的文章讲到这个问题了。
2010 年 Dan Wood 这篇文章 http://gigliwood.com/weblog/cocoa/Converting_Rich_Tex.html 谈到怎么自动化把 SLA 集成到 DMG 文件。
到 TEXT/styl 数据这里,作者很牛叉地利用 ObjC 代码,读进文本再贴进 Pastboard,然后再写入文件,但是文章中用的把 NSData 输出的方法的那个项目已经没了,于是我找到了另一个项目,FreeDMG on Github ,里面的 rtf2r.m 文件里面有同样的方法,而且可以 dump 进文件里面。
大致上我是这样做的:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"sla" ofType:@"rtf"]; NSAttributedString *str = [[NSAttributedString alloc] initWithPath:sourcePath documentAttributes:nil]; NSData *data = [str RTFFromRange:NSMakeRange(0, [str length]) documentAttributes:nil];
NSPasteboard *pb = [NSPasteboard generalPasteboard]; [pb declareTypes:[NSArray arrayWithObject:NSRTFPboardType] owner:nil]; [pb setData:data forType:NSRTFPboardType]; NSData *textData = [pb dataForType:@"CorePasteboardFlavorType 0x54455854"]; // TEXT NSData *styleData = [pb dataForType:@"CorePasteboardFlavorType 0x7374796C"]; // styl int len = [styleData length]; char *bytes = malloc(len); [styleData getBytes:bytes length:len]; OSStatus status = CoreEndianFlipData ( kCoreEndianResourceManagerDomain, //OSType dataDomain, 'styl', //OSType dataType, 0, //SInt16 id, bytes, //void *data, len, //ByteCount dataLen,
#ifdef BIG_ENDIAN true #else false //Boolean currentlyNative #endif );
NSData *newStyleData = [[NSData alloc] initWithBytesNoCopy:bytes length:len freeWhenDone:YES]; NSString * outPath = @"/Users/justinyan/Downloads/test.txt"; dump_rsrc_file("TEXT", textData, outPath); dump_rsrc_file("styl", newStyleData, outPath);
}
读进一个 RTF 文件,然后把内容转成 TEXT/styl 数据并保存起来。这段代码其实有一个很重要的点,就是
CoreEndianFlipData
这个函数,把大端小端交换了一下。如果没有交换大小端,生成的数据是无法 Rez 到 DMG 文件里面的,文章中作者也纳闷为毛 Intel CPU 的 Mac 就不行,于是他在他的旧机器 G5 (真土豪啊)上面跑了一遍发现出来的 styl 数据如下:
0060 0000 0000 000F 000C 0400 0100 000C
而这个数据是正确的,可以 Rez 进去的,分别表示 0x0060 style runs, 0x00000000 first offset, 0x000F line height, 0x000C font ascent, 0x0400 font family, 0x0100 char style, 0x000c pixel size.
但是 Intel 机器跑出来是这样:
6000 0000 0000 0f00 0C00 0004 0001 0C00
于是他猜到可能是大端小端的问题于是交换了一下(要不是有台旧 Mac,我盯着这坨 hex 一年都猜不到),解决了!
完了我再手动去把这段数据给粘贴到用 ResKnife 编辑好,生成的 .r 文件。
DeRez SLAResources.rsrc > sla.r
这样用 DeRez 命令就可以生成 .r 文件了。把对应的中文的数据贴进 .r 文件之后,再用 Rez 命令集成到 DMG 文件里面,终于大功告成~!
Rez -a sla.r -o your_file.dmg
https://dl.dropboxusercontent.com/u/6750144/SLAs_for_UDIFs_1.0.dmg
https://github.com/slobo/ResKnife
在慵懒的阳光浸漫的下午,开出灿烂的水仙,炮竹声由远及近,终于在欢笑与镜头下,绽放在夜空燃出烟花朵朵。这是一个并不寒冷的冬天,是 2014 的过年。
从猴年说到马年,终于抓住 2013 的尾巴拔起一个个“一夜城”似的车站,半个月前守在 12306 的网页的我,理所当然地抢不到春节的高铁票,一路闷在臭气盎然的大巴里从省城颠簸到潮州,我的家。
还是习惯说“外婆家”,老妈说,应该说“外公家了”。
这座 50 年的大房子,村子里的一号门牌。后院的水井倾听过几代人的故事,多年前的龙眼树终于没留到现在,消失掉了,还有外婆的照片上的绿意盎然的背景,有片夕阳落在上面。园拱门上的金漆的字迹,是十多年前外公写下的“安乐”。
很久以前好多小孩在后院里玩耍,水井旁边小孩不能过去哦。不是太久以前,我们在后院玩耍,水井旁边小孩不能过去哦。现在我们看着小孩在那里玩耍,水井已经盖上盖子,夕阳落去隔壁屋顶的时候,本来是龙眼树繁盛的地方,一个很大的信号塔立着,有些年了。
月亮悬在池塘的上空的时候,我们曾在外婆的屋顶放过烟火。南方无雪,那些星点飘零的火光缓缓而优雅地坠落的时候,那些孩童的欢声还萦绕耳畔。
外婆,外婆。
可惜在广州已看不到这样明晰的蓝天,尽管广州有二沙岛,有沙面,有圣心大教堂,但是在潮州的夏天,仍有我喜欢的感觉,乡村而且台湾。
今年的气温一解小时候过年湿冷的阴霾,没有满大街湿嗒嗒的绵绵小雨,没有穿再多都会冷的瑟瑟寒风,今年的过年,阳光好得不可思议。
于是借着阳光明媚的午后,泡一壶功夫茶,两杯三杯,茶香回味。
角落里摆了个企鹅十三周年的礼物盒子,阳光木纹鲜花,很快我又要回到 450 公里的路上。
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这个奇葩的东西,可以好好研究一下。