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

读罢此书,感慨良多,没想到作者竟也是潮州人。此书以平实的语言记录了作者行走东南亚与西藏长达八个月的旅行生活,平淡而精彩,让我思考起“旅行”与“旅游”的区别。
大约此前我走过的旅程多数都是在“观光旅游”,令人身心疲惫的详尽的计划,提前预定好机票酒店,每天都排满了“著名的景点”,到一个地方便匆匆走过留下相片无数。不能说这种旅行没有给我带来愉悦与美景,但是相比而言,海南独自的骑行更让我觉得这是一场“旅行”而不是“旅游”。
一向我都算是比较保守的旅行者,还没到一个地方就会把所有行程的酒店都定好,但是这次在海南,我留了一天的酒店没有定,于是也给我了那一天改变行程不去猴岛而是直奔三亚的空间。在寻找酒店的过程中我体会到了“酒店也能讲价”的特色,由于没有安排太多景点,我在博鳌的海滩发呆了几个小时,沿着海滩骑车,坐在海边看落日的余晖。即使是在景点蜈支洲岛上,我也只是坐着发了几个小时呆然后就离开了。这种“慢节奏”的方式令我更加深刻体会到当地的生活,在每次骑累了找到小店休息的间隙跟当地人聊天,在路上遇到骑友交换各自旅途的故事,这些不属于景点的体验让我更加难以忘怀。
尽管这些事情比起孙东纯在间隔年的际遇,连九牛一毛都算不上,但是有了这几天的经历,我能体会到作者一个人在路上的寂寞与孤独,碰上旅行者的欢笑与感动,还有每天无所事事坐在树下抽烟晒太阳的那种悠闲的放空的状态。我想,不管是骑车也好,背包也罢,无论是提前排满行程抑或随心而至随遇而安,这些都只不过是旅行方式的不同罢了,旅行的心态决定了一趟旅程的收获。
作者所谓的间隔年(Gap Year)是西方国家比较流行的东西:大学生在毕业后并不急于进入社会工作,而是花上数月甚至一两年的时间周游世界各地,多数都会选择在旅途中做义工。通过这个旅程来重塑三观,适应社会,然后再回到社会当中来。作者在他的间隔年启程的前一天心中仍对自己有着满腹怀疑,我在出发去海南骑行之前也有着类似的想法:对于这次旅行我是否已经准备好了?同样是怀着惴惴不安的心态出发,同样是遇到旅伴的时候欣喜,却又渴望一个人独自旅行的自由,内心矛盾而纠结。我想是六天也好八个月也好,真正的旅行是可以改变一个人的三观的。
作者在旅行后回归“现实”的时候有点无法融入社会,相信这是多数做过长途旅行的人都遇到过的困扰。其实没有必要困扰,恰恰是因为这种“不适应”,证明了这趟旅行绝不仅仅是一次纯粹的物理移动这么简单。
如果我回来了,和出去的时候一样,那我的间隔年还有什么意义呢?正是这种格格不入在证明它带给我的感悟和改变,我需要做的不是怀疑他,而是调整自己,将自己的感悟和改变融入到生活里。我想到当初离开广州的时候对自己说的话:我不是为了逃避,是为了更好地面对原来的生活。
我在海南骑行回来之后也有数天时间适应不过来,旅行是一种毒,我想,海南的骑行之旅让我中毒已深。
此书在 z.cn 有 Kindle 版:孙东纯《迟到的间隔年》
2022-08-20 原《每周读书》系列更名为《枫影夜读》

奥地利小说家茨威格讲述了人类历史上(其实是欧美历史上)一些星光闪耀的时刻,每个时刻都有一颗明星英雄,这个英雄在这个时刻所做的事情,都一定程度上改变了人类历史。这令我想起某狼曾经说过的,本科生的知识范围大概就是人类知识圈的 80%,研究生要求到 100%,而博士生则要求在原有的知识基础上,找到一个突破点,把原有的知识圈打破,人类的进步就是由这些一个个很小的突破点慢慢累积起来的。抛去学位不说,某狼这个比喻还是挺有意思的。
这本书里有大航海时代里痞子流氓开启的人类新航线,有滑铁卢中规矩呆板的格鲁希元帅决定了拿破仑的陨落,有年过古稀的老诗人歌德恋上花季少女后创作的文学巨作,当然也有中学课本中读到的南极探险队悲壮的故事。这些故事篇幅不长,但是都挺精彩,对于欧美历史并不熟悉的我来说,算是本挺不错的科普读物。
第一篇地址:一路骑下去,直到见到蓝色的大海(一)
博鳌——万宁兴隆 101公里
博鳌这个海滨小镇着实让我好好发了一呆,尽管当晚睡不不太踏实,第二天早上 6 点还是带着惺忪睡眼就出发了。
过了博鳌亚洲论坛(对人造的地方没什么兴趣,就直接路过不理会了),一路都是绿树成荫,骑行起来甚是惬意。等上了 223 国道就状况不佳了,到处在修路,跟在大车后面尘土飞扬,赶紧找个路边小店休息一下避避大车。有的时候小店老板见我骑车会聊上两句,海南话有闽南话的渊源,我这个潮州人也能听懂一二。通常休息的时候小店都会有几个村民在坐着聊天,有的人会讨论我一天能骑多少公里,有的人会直接问我国家给了我多少钱要我来骑车。当然每个人都会问我去哪儿然后在得知我去三亚之后说一句:“现在三亚好热的!”我也只能莞尔:“热也得去呗。”
话说正跟小店老板聊天,问起有没有其他骑车的人经过,一个骑行者背着包就从我面前飞驰而过。赶紧喝光手中的汽水骑上车追出去,却苦于后拨在飞机托运的时候被压坏了最高速上不去,直追出几公里才赶上我的第二个骑行同伴:海南人科材。
科材是个胖胖的黑黑的小伙子,海南人都是黑黑的,并且越往三亚越黑,还都黑得相当均匀。科材是万宁人,在海口工作,这次是趁着端午假期,用两天时间骑过万宁家里过节的,于是我们开始了从龙滚镇到万宁市一段艰难的山路陡坡之战。

这段路大约二、三十公里均是山路,上坡下坡上山下山,尽管不是极高的山,但是山坡连绵不绝对体力消耗极大,有些坡翻完还有第二级上坡,我们俩一前一后踩得气喘吁吁,看着并行的高速公路平整的路面,真是恨得牙痒痒的。所幸今天一路有云朵遮荫,否则估计得晒干在半路上。一路到达和乐镇,已近中午 11 点,这十几公里路可不太好骑,两人均已饿得头晕眼花,正好在和乐山脚下吃和乐粽子。

端午节到了,很多万宁市民还专程开车到和乐镇上来买粽子,可知这里的粽子有多出名。粽子 8 块钱一个,比起广东的粽子要大得多,也不知是否饿极的关系,只觉风味极佳,比起广东甜腻细致的粽子,这里的粽子更多几分油香。海南有四大名菜:文昌鸡、加积鸭、东山羊和和乐蟹。在文昌没吃上文昌鸡,在琼海勉强算是吃了加积鸭,现在在和乐,螃蟹没吃上倒是吃了和乐粽子,哈哈。
一口气吃了两只拳头大的粽子,继续爬坡!直到过了大茂镇路况才渐渐好了起来。到达万宁市区已经中午 12 点多,昨天骑 60+ 公里花了 5 个小时,今天同样是 60+ 公里却花了 6 个小时,这段爬坡路真是挺可怕的。科材是个挺善良的小伙,中午天气太热,还邀请我一起到他的同学家里吹空调乘凉,我当然就恭敬不如从命啦。他的同学也是挺热情的人,于是三人聊天聊得火热。科材还提到昨天遇上一个拉着驼包帐篷睡袋的人,负重 80斤,也是海口到三亚方向,准备环海南岛以后去骑西藏。骑西藏估计是国内所有喜欢骑车的人的圣地了,只要一听到谁去骑过西藏,便会对他肃然起敬。不知不觉聊到下午两点,辞别了二人,我又再次独自出发,往兴隆温泉方向骑行。这段路况不佳,道路坑坑洼洼的,但好在不用再爬坡了。
在一段山路上,远远看到前面树荫下貌似有骑行者在修车,车后架还放个大驼包。赶紧上前打声招呼,一瞥眼看到他驼包上的帐篷睡袋,冲口而出:“难道你就是传说中的负重 80 斤去西藏的人?”
那人一阵错愕,点了点头。于是我在一天时间内遇上了两位骑友,这第二位骑友就是辞职要骑去西藏的猛哥。两人结伴到兴隆温泉预定的酒店住下,找了家路边饭店吃饭喝酒聊天。原来猛哥大学时读完一年之后,觉得学校里学不到什么东西,便辍学出去打工。做过餐厅服务员,开过叉车,也做过工厂流水线工人各种各样的工作,辞职前在东莞打工,觉得没什么意思,于是带上全部积蓄,买齐了装备准备从广东上云南、西藏,再到青海环游一圈,以后的事情以后再做打算,真是潇洒。
不过我倒并不羡慕,每个人都有自己的生活方式罢了。这几天骑行下来,让我觉得在每天早 10 点晚 10 点,除了上班就是睡觉的生活以外,其实还可以有很多别的活法,这个世界可以很丰富很精彩,我们活着是为了享受生活,而不仅仅是挣扎地生存下去。这两天我总回想起高中时在学校走廊写过的半个句子,现在或许可以拼凑完整了:
当生活已经成为生存的负担
你步入村道踏出朵朵迟暮的花
夏天的斜阳还在山肩延烧
你年轻的背上弯出艰辛的弧度
沉默如一抹夕照,一撮黄土

万宁——三亚滕海村 100公里
昨天与科材朋友聊天后决定今天不去南湾猴岛,那里其实并不好玩,而且住宿也难找,于是和猛哥决定一路骑到三亚海棠镇的滕海村,到那里找住宿,第二天再上蜈支洲岛。
不知不觉到海南已经第四天,双腿也习惯了每天 100 公里左右的骑行,早 6 点起床出发,晚 9 点上床睡觉已是常态。在兴隆吃了早餐喝完兴隆咖啡,兴致满满地出发,却在不久就遇上今天最大的挑战:爬山!
虽说前一天在进万宁以前已经爬过一两座小山外加无数连续的上下坡,但是对比起今天要爬的山,昨天那些都只能算是小坡了。跟猛哥一起骑行通常我都喜欢骑到前面,但是一遇到大坡我就骑不上去了,眼看着猛哥超过去一路蛇行上坡。我一开始还跟了一段,大概转了两个盘山弯道之后就顶不住了,只能下来推车。推了一段猛哥也顶不住了,这座山可真是厉害,连续发夹弯有如头文字D的漂移赛道一般。昨天听科材的朋友说他以前上学的时候跟朋友骑摩托车经过这座山,真是东边日出西边雨,翻山又淋雨,他骑摩托车都觉得很危险,别说我要骑车上山了,只能一阵苦笑。一路气喘吁吁地推着车,大概爬了十几二十分钟才到达山顶。

哇哇!常听人说最美的风景在路上,真是只有亲眼所见才能信服。我们下坡的时候看到一片辽阔的原野,明媚的阳光打在朵朵白云上面,映出一个个影子在水田,在池塘,在绿色的山坡上。山的那边是陵水黎族自治县,一派人文与自然和谐相处的景象啊!在山路上我们停下来远眺,即使是下坡路我们也不忍放弃如此美景,把车靠在路边好好地品味一番。

再次出发已进入陵水地界,少数民族果然保留了完好的传统,到处都在放鞭炮庆祝端午节。路过一个小学,正好放学,小学生们好奇地看着装扮怪异的两个骑行者,还跟我们打招呼加油。看着道路两边的田野,骑行在起伏的山路上,心中倍感愉悦,第四天,我开始感受到骑着单车旅行的魅力。
中午我们赶到英州镇,大概 60+ 公里。小镇甚是荒凉,主干道上一棵树也没有,烈日之下兜兜转转,最后决定在一家有空调的饭店坐下来吃饭。这真是个错误的决定,午饭难以下咽不说,镇上的村民习惯吃完饭不走,呆着这间可能是镇上唯一一家有空调的饭馆里乘凉,或者就站在门口屋檐下聊天。于是一波又一波的大叔围着我的车在那里抽烟聊天,有的扶着我的驼包有的扶着车把,还有的拎起驼包上的松紧带就把玩不停,再加上饭馆里一桌光头猛汉人手一支啤酒在那里大声吃喝,这餐饭真是吃得我心惊胆战的。吃完赶紧又换了一个有树荫的冷饮店喝水休息去了。
或许是天热的原因,海南的冷饮店和茶店特别多,三步一家,很多村民中午就呆在冷饮店两块钱买一壶红茶,坐着聊天打牌就是一下午。我呆在树荫下,趁这时间补上了前两天的骑行日志。出来骑行已经四天,还是第一次往日志上写东西,只怪前几天太累,到了酒店都是洗个澡早早睡去。

午休结束,我们一口气骑到三亚海棠镇。进入三亚地界,这里猛烈的太阳与前面一路过来简直判若两地,在海边看到当地人的皮肤也是一下进入非洲人的水平,简直黑如木炭一般。我们从海棠下了国道本来想沿着海岸线骑车,结果到了海边发现路边已经修起一座座奇形怪状的如“鸟巢”、“维多利亚港”的建筑物,上面大书:“国际购物中心”一类字眼,完全遮蔽了滨海路看海的视线。失望之余我们还得沿着这条没有树荫遮挡的柏油路一直骑行 20 公里,没有地方休息,没有水喝,到海边的时候已经累得不行。在海棠湾广场我第一次跟三亚人买椰子,三亚的物价极黑我早已有所耳闻,心里已经有所准备,所以不冰的椰子开价 8 块我已心里有数。到海南以来天天吃椰子,不冰的 5 块,冰的 6 块是市场价,海岛上热带水果极便宜,5 块钱可以买两个菠萝,10 块钱可以买一个哈密瓜。于是三亚给我的第一个坏印象就是恐怖的热与晒,第二个坏印象就是极黑的物价。

但这并不妨碍世界各地的游客往海南三亚来度假,毕竟这里有真正的白色沙滩蓝色大海,亚龙湾里的热带雨林度假区也是风景极美的去处。我们在海边呆了一会儿便出发到蜈支洲岛码头所在的滕海村寻找住宿。前一天晚上我查到有家“梦回天堂”客栈很不错,但是不好找,于是打了个电话给客栈老板。老板果然跟网友评论的一样骑着单车出来接我们,看我们骑着自行车出来,老板还给我们优惠的房价。这里物价很高,老板给的房价已经算特别低的了,这个世界还是好人多啊。
“梦回天堂”客栈最美的地方在于,出了大堂一下楼梯就是大海!这海其实严格上说是个海湾,但是也足以令人兴奋的了。我们放下行李,换好泳裤就直接下水游泳去了。来海南四天了,这还是我第一次下水游泳。此时已是傍晚 6 点,夕阳已西斜,泡在水中一身舒畅。海湾的水虽不及蜈支洲岛,但是也属于清可见底的了,站在沙滩边上还能看到许多小鱼在水中畅游。

今天是端午节,海南人有传统是端午节要下水游泳。许多中学生穿着一样的班服在海里游泳,在沙滩上玩躲避球,还有许多男女老少一家大小在水中欢乐地玩耍。看着阳光下波光粼粼的大海,以及海中人们幸福的笑容,恍惚间我似乎明白为什么有些人到了丽江就不想回家,都是因为大自然的美景啊!像客栈老板一样,在海的边上开一家小店,一下楼梯就是大海,这得是多么令人身心愉悦的事啊!

蜈支洲岛——三亚市区 48公里
今天是第一次可以睡到自然醒的日子,因为今天的主要任务就是上蜈支洲岛游玩,晚上骑到市区预定的酒店住下就行了,不过才二、三十公里,对于骑惯一天 100 公里的我们来说已经不算什么。只是可惜的是我已经习惯了早 6 点起床,自然醒也就是睡到 7 点而已。我们 8 点开始收拾行李,把车寄存在酒店,背上背包相机就往岛上跑。

进岛要坐渡轮,其实就是小型游轮,门票含船票 168/人,光船票就 110 了。在船上摇摇晃晃 20 分钟,终于来到中国的马尔代夫——蜈支洲岛。这里不愧是三亚最美的海岛,这里的沙滩这里的水,就是梦中的海的模样。对于只见过潮汕与深圳那些黑色的大海的我来说,这里的海是令人震撼的美。

我们沿着环岛的路步行,一路相机咔嚓不停,贪婪地拍下眼前这个天堂也似的地方。很快我就遇到三亚的第三个坏印象了:景点的商业化太重。蜈支洲岛有一条环岛的路,但是不允许行人步行,行人只能走岛的中间的山路,这样我们只能看到岛上三分之一的景点,无法环岛。要环岛只能坐电瓶车,收费 150 块一个人。再一个黑旅客的点在于,上下岛的渡轮是 8:30 - 16:30 才有,也就是说,如果想要在早上 6 点和晚上 6 点这两个太阳舒适的时间在岛上玩耍,你只能选择住在岛上,而岛上的酒店最便宜的也要 1200 块起,沙滩不允许扎帐篷。所以像我这种想到岛上观光但是又不想花钱住岛上的,只能走三分之一的山路,到岛上的顶点观日岩看海,然后沿路返回。岛上的水上娱乐项目挺多,但是费用也是极高的,这是商业化准则了,越是供不应求的地方越要分清消费者等级区别对待,以榨取最高利润。

三亚的云少,紫外线异常强烈,正午的太阳晒得我头晕眼花的。找了个凉亭,面朝大海,坐着发呆。对于我这种穷游的旅客,除了在凉亭坐着发呆似乎已没有别的想法。这么猛烈的太阳别说下水游泳,只是在凉亭里面坐着都能感受到阳光的威力。坐在亭子里,又补起了这两天的日志。

蜈支洲岛,让我想起柬埔寨的 Koh Rong Samloem,但是相较之下, Koh Rong Samloem 是个没有开发的地方,没有蜈支洲这么重的商业气息,不由令我起了去柬埔寨骑行的念头。
在蜈支洲岛发呆到下午两点多,我们离岛往亚龙湾出发了。亚龙湾热带雨林是个非常漂亮的度假区,但是如蜈支洲岛一样,也是商业化极重的地方,到处是酒店和所谓景点,骑进亚龙湾,看了一眼海滩就走了,这样的地方,再好看也令人兴味索然。晚上我回市区预定的酒店,猛哥则继续往西线走,到下一个小镇去寻找住宿。于是惜别了这两天同行的伙伴,相互道了珍重便各奔东西了。
三亚给我的又一个坏印象:市区是个破旧的,交通极差的地方,尽管有麦当劳肯德基和一切大城市有品牌步行街,但是服务态度极差,我在三亚两天消费的地方只有一家店是态度好的,其他的地方服务员都很不耐烦的样子。
鉴于三亚给我留下的印象极差,市区我也不抱有可以闲逛的地方,便在酒店写了一上午明信片,找间 KFC 解决午饭又补了一番日志便骑车到三亚凤凰机场,启程回家了。路上遇到可能三亚给我的唯一的好印象,就是滨海的椰林长廊,绵延 20 公里,公路旁边就是大海,可以随意下水游泳。海水虽不如蜈支洲清澈,但是也算不错的了,想像一下傍晚在海边伴着夕阳戏水,晚上在椰树下借着月色乘凉的情景,倒也甚是惬意。

于是六天的海南骑行之旅就这样结束了,回到广州已经两天,时差还有点倒不过来,习惯了早上 6 点起床,现在睡到 8 点就觉得有点睡过了,当然晚上加班也然后犯困。有了这次长途骑行的经验,我想我已经喜欢上自行车旅行了,下一站会是哪里呢?

骑着单车去旅行,我心向往已久,自从看了石田裕辅的《不去会死!》买了人生中第一部正经的山地车之后,对骑行我就欲罢不能。终于两个月后,我踏上了海南骑行之旅。这些天里,有汗水滴落过孤独与无助,也有笑容绽开过感动与愉悦,这个热带岛屿的蓝天白云,阳光海滩,一切一切,已成为我久久不能忘怀的回忆。
海口——文昌 90KM

尽管两个月来已骑过广佛两城各处共 500 多公里路,但是独自一人到异地他乡连着骑上六天,这对我来说还是头一回。于是光准备出发物资,行程计划等等就花了不少时间,再加上出发前广州连着下了一个月的大雨,不由让我对未知的旅行心生忐忑。但是无论如何,盛夏的五月底,我带上车子,出发了。
上飞机托运是遇到的第一件麻烦事,此前因为想给单车包减重以防托运超重,把修车工具放随身包里结果没能通过安检,好在机场工作人员好心给我免重多办了一次托运,但也因此耗了些时间。凌晨 5 点就起床赶飞机的我,一上飞机就疲累地睡去了。一个小时抵达海口,拿行李准备装车的时候发现后变速有点问题,前轮的刹车也不好调,更麻烦的是之前没有装后轮的经验,一下子手忙脚乱,在机场装车装了快一个小时才装完。
最后终于满手油污拖着车子出了机场,呵!好厉害的热带阳光。在广州闷了一个月大雨的阴抑心情一下散尽,刚刚在机场装车差点绝望的感觉也抛在脑后,于是戴齐手套、袖套、魔术巾、头盔等装备,塞上耳机,朝着手机定位的方向,前进!
只有被火辣辣的太阳晒到肌肤生疼,我才确信我已经骑车踏上一座热带岛屿,只有感受到来自海岛剧烈的海风,我才知道我已经开始了第一次长途骑行的旅程。
而这热带的风好不凶猛,沿着 201 省道王文昌的方向,一路逆风,再加上沉重的驼包和前轮刹车的磨碟,骑出去没多远,疼痛的双腿就开始抗议了。不到三公里我就得停下车来修整前刹,绵长的柏油路穿过丘陵,树木倒是很多,但是并不遮荫。省道上除了我看不到其他骑行者,偶尔一辆汽车从我旁边呼啸而过,扬起一阵尘土,我顶着烈日继续修车,最后不得已只能先拆了前刹,只用一个后刹上路。
旅途的开头并不顺利,201 省道除了蓝天白云以外基本没什么景色可言,一路上只是与大风和太阳在作斗争,骑个十几公里就得休息一下。从美兰机场到文昌预定的酒店不过 65 公里,我从中午 11 点骑到傍晚 5 点多才到达。放下行李,却被告知清澜港已经没有渡轮去往今天的景点——东郊椰林了,只能走清澜大桥,于是一路问人一路骑,在村道小路之间跌跌撞撞终于上了清澜大桥,骑了一天第一次看到海,咸咸的海风夹着腥味在桥上呼啸,心里却觉得这海看起来跟广州深圳的也没啥不同,不由有点失望。下了桥抬眼一看路牌,离东郊椰林还有 12 KM!糟了!即使不进景点,一来一回 24KM,按照这样逆风的情况起码得一个半小时才能回到酒店。而此时已是下午 6 点多,再过一个小时太阳就要下山了,这条村道看起来是新修的,竟没有路灯。越走心里越是发虚,骑了十几分钟,脚步越来越沉重,最终只能咬咬牙放弃景点往回走。刚刚下桥的时候一路下坡,现在是一路上坡,骑上桥的时候一阵狂风扑来差点骑不动摔倒。一时间,孤独、沮丧、无助从四面八方袭来,透不过气的我鼻头一酸,差点落泪:为什么我要一个人到这里来,一路上明明什么风景都没看到,受尽逆风的折磨,还看不到今天唯一可看的景点?
文昌——博鳌 78公里
尽管前一天的旅途并不愉快,我还是早早就起床出发了。在路边小店吃早餐,老板娘挺和善的,问我大包小包的往哪儿去。我说我从海口骑过来,要去三亚,老板娘惊讶了一下,“挺辛苦的呀小伙,现在三亚可热了。”。老板娘的微笑是我来海南以来遇到的第一个善良的鼓舞了。一时间心情舒畅很多,饭毕又匆匆出发了。
昨天的骑行方向正好对着东南季风骑得非常辛苦,今天方向转了一路便轻松了许多。从文昌清谰镇骑上迈号小镇,然后再上 201 省道,应该说,我的海南骑行之旅美好的部分从今天才正式开始。
夏天在海南骑行必须全副武装,这时一旦速度稍慢就会觉得闷得难受。在去会文镇的路上我正喘着粗气爬坡,迎面遇上了第一支同样骑行的队伍。尽管方向相反,我也还是很兴奋地跟他们竖起大拇指,大声喊一句“加油!”。当对方也对我竖起大拇指喊加油的时候,一阵感动涌来,仿佛电影《单车环岛日记》里面的情节:”哦,原来骑车的时候真的是这样子的。“
出了会文快到长坡的路上有一小段路就在海边,路过一个路口的时候瞥见一眼海滩的样子,正犹豫要不要过去瞧瞧的时候一句话在我脑中回响起来“骑行只是一种旅行的方式,遇上美景一定要停下来好好看一看,不要因为赶路而错过了美景。”于是我按下刹车,立刻调头直接往海滩边跑。

哦哦哦!这可是我来到海南第一次见到海滩呀!尽管不是梦中白色沙滩蓝色大海的样子,但也是海天一线,白浪层层的景色呀!在出发前做的功课里说这一段路是最美的,于是我一边骑上国道一边留意。可惜一路再没机会靠近海滩,于是我在太阳最猛烈的时候停了下来,随便找了条小路,一路往前冲吧,向着海滩的方向,拍下了这张照片:

再次出发,勉力骑过一个沙石满满的大坡后,远远看到前面桥底有自行车和人影,我一下兴奋起来,搞不好是跟我同个方向的骑行者呀。于是我就在桥底遇上了来自广州的阿浩和阿 Sam 两兄弟,这还是我出来骑车以来第一次遇到一起同行的伙伴。阿 Sam 是哥哥,有过数年的骑行经验,曾经环过青海湖,挺厉害的,这次跟弟弟到海南来是想拉练一番,后面阿浩就要自己去骑滇藏线了,真令人羡慕。
于是三人成行,一路飞奔到琼海市区已经中午 12 点多,随便找家路边大排档点了餐开吃。阿浩骑得挺快,我的后变速在飞机上被压歪了,上不了最高速,还差点跟不上。三个人一起吃饭,终于可以吃到丰盛的一餐了。想起昨天在文昌吃不到文昌鸡,只能吃快餐的窘境,不由得一阵感慨。
饭罢三人休息了一阵,海南的正午太晒根本无法骑车,只能休息到下午两点多再行出发。再骑十几公里,我们到了博鳌小镇。

博鳌的海水尽管不如三亚的清澈,但是也算是难得的海滩了。我到预定的酒店放好行李再回到海滩的时候,广州兄弟正好准备走了,于是第一个同行的伙伴就这样惜别了。广州兄弟只预了三天从海口骑到三亚,每天要骑 150+KM,强度挺大的,很厉害。

博鳌的海滩很长很长,我在海滩上骑行,发呆了几个小时。坐在沙滩上,海风轻抚一如夏天的拥吻,椰林摇曳的尽头有夕阳的温软。海上白浪层层,天空万里无云,海天一线处,晕染出淡紫的光芒,那是另一个世界的声音,在海风中沙沙作响。当小镇的黑色的影子如同垂首的老人向我发出诘问,我远望着大海,没有作声,我只是站在我的身后,以叩问的姿态,去想我的世界与另一个世界,到底有何不同,到底,有何不同?

第二篇地址:一路骑下去,直到见到蓝色的大海(二)
2022-08-20 原《每周读书》系列更名为《枫影夜读》

读这本书之前,曾看过一篇文章,大意上是指女权主义的延伸一类的。性别这个东西,被认为是后天的,以生殖器官作为区分人类依据的做法,只是生物学上的划分罢了。当然还有染色体为 xx 和 xy 的分法也是。这仅仅是分辨出一个人类的个体是属于雄性还是雌性,或者中性。
但所谓“男人”“女人”的说法,则要复杂得多了。不仅是生物学上的标志,还带上了这个社会对于“男人”、“女人”的先入为主的偏见。这个社会普遍认为“男人”就该阳刚而“女人”就该柔弱之类的太多了。
尽管随着人类文明的发展,“性取向”不寻常比如同性恋这样的事情已经渐渐为人们所接受,但是人类毕竟是复杂的生物,有关“性别”的烦恼还远不止如此。
《单恋》这部小说还是以东野最擅长的推理手法来展开故事,不同的只是故事的人物被赋予了“性别认知障碍”这样的特性。所谓“性别认知障碍”,简单点说可以这么理解:就是一个有“男人”身体的人,却拥有一个“女人”的内心,反之亦然。
这本就是一个很难理清的课题,但是东野却在小说中做了十分有见地的讨论。把男性和女性完全当做黑与白这样分开是不对的,黑白中间还有灰色,即连 Facebook 也会有十几种性别让用户选择。小说中借某种“特殊的店”的老板之口,把人的心灵比作梅比乌斯环。乍一看男人女人是正反两面,但其实是个梅比乌斯环,正面画上去的线是可以走到背面的。一个人的内心没有完全的男性或者完全的女性,人们总是处于这个环上的某个点,只是比较偏向男性或者女性而已。
偏向男性的心灵,却拥有一副女性的身体,于是会喜欢女人,但这是不同于同性恋的喜欢,而是以“男人”的身份去喜欢。小说中的故事就围绕主人公的一个大学好友展开,这个好友就是这么样一个人,拥有女性身体但是渴望成为男性。事实上追根到底他希望成为男性的根本原因,其实还是想被社会接受。如果这个社会的文明足够进步,能够接受一个人的心灵独立于躯体而存在,不管他身体是男是女,心灵是男是女,都当做是一个人来对待,他也不需要这么辛苦。就好像白人黑人都是人一样。
这就让我想起美国的黑人解放运动。黑人也是付出了很大的代价才解放出来争取了平等,但是至今种族歧视还是到处都有存在。至于这种“性别”之争恐怕就不晓得何年何月才能真正解放了。
其实每个人内心都会有异性的一面,而那一面并不是什么值得羞耻、惭愧和掩盖的事情。那其实应该是很正常的事情,是人类多样化的体现。只是我们的社会还是太多成见,而偏生人类又是群居动物,渴望融入社会,为社会所接受,所以才会产生诸多烦恼。
这不禁让我慨叹起多年前,所谓“性格的转变”的事情。真的融入社会了又如何呢?谁能说那就一定是正确的呢?有得必有失,目前来说我还是比较庆幸自己得到的比失去的要多。
再说起那些“特殊群体”为什么能掩人耳目地在某些地方聚集交流,其实我认为是绝对可能的。在买自行车之前,尽管大街上到处都是自行车专卖店,但是买完后我还是觉得像发现了一个新的世界一样。原来这个世界还是有很多很有趣的东西等待我们去发掘。在圈子里的人就心知肚明,圈子外的人可能连圈子的存在都一无所知。这是完全可能的。
于是我又想,这个世界太奇妙,自行车也好,攀岩也好,随便找出来都有很多我们所不知道的事情,等着我们去探索,去体验。比起整天除了工作就是睡觉,我当然更希望能去探索世界,体验生活,享受生活。这才是我们活着的意义啊!
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






