今天是 10 月 24 日,不知道你的朋友圈有没有被程序员节刷屏,反正我的是被刷了。 看到 1024 这个数字,相信很多人都怀着特别的感情,比如我,游泳不会止步于 1000 米,肯定会补 24 米凑个整,跑步如果跑到 10 公里,那一定再多跑个 0.24 出来。 搞不好还会想起那些年追过的社区,嗟叹一下逝去的青春: 那么,这样一个特别的日子,我的交游圈里大家是以怎样的姿势度过的呢? 程序员们怎么过 聚众自黑型 作为互联网上最擅长自黑自嘲,以至于现在不明真相的群众都把他们的自黑当真话听的群体,这一天怎么会甘于寂寞,今天微信群里的画风是这样的: (from 掘金.专栏作者群) 大家纷纷表示自己是个假程序员。 感(xuan)恩(yao)公司关怀型 以重视员工工作体验著称的互联网公司们也没闲着,为程序员们推出了各种福利,所以今天朋友圈里的画风是这样的: 还有程序员鼓励师出没: (from 掘金.专栏作者群) 非程序员们怎么过 关我屁事? 公众号们怎么过 也算一年一度的节,相关的公众号们也没闲着。 科普型 (知晓程序员) 在世界中心呼唤爱型 (Java 程序员联盟) 趁势搞活动型 (InfoQ) (码农翻身) 欠揍型 (微信派) 掘金怎么过 文首的图就是掘金社区去年 1024 出品,今年他们录了一首歌,链接: 老子今天不加班,程序员也需要自由 后话 好了,夜已深,打完收工。祝 WE 程序员们少熬夜,保住我们的发际线。
在过去的一个来月,我利用业余时间做了一款光谷社区的第三方 Android 客户端。 前言 光谷社区是我在决定离开帝都回武汉的过程中,及回武汉之后关注得较多的武汉本土社区,网站 http://guanggoo.com 自己的 description 是这样的: 光谷社区是源自光谷的高端社交网络,这里有关于创业、创意、IT、金融等最热话题的交流,也有招聘问答、活动交友等最新资讯的发布。 描述得还比较准确。我觉得身在光谷,或者心系光谷的童鞋们可以关注一下。 发布详情 目前支持特性: 登录 首页主题列表(三种视图) 主题详情 / 评论列表 节点列表 / 节点主题列表 评论 / 艾特用户 分享主题链接 发表新主题 查看用户信息 源码放在 GitHub 上: https://github.com/mzlogin/guanggoo-android 部分界面截图: 更多的功能开发、完善以及优化还在进行中,也希望看到的朋友们下载试用起来,多提建议多交流。 好吧,啰嗦了这么多,哪里能够下载得到呢? APK 下载链接 (如果是在微信里看到这里,建议长按后复制链接到浏览器打开) https://mazhuang.org/guanggoo-android/guanggoo-lastest.apk 百度网盘备用链接: https://pan.baidu.com/s/1pL0t1Zd 扫描或识别二维码下载 (如果使用微信识别二维码不能开始下载,还是复制上方的链接到浏览器打开下载吧) 为什么会做这个 社区目前只有 Web 页面,做了移动端适配,体验也还不错。不过作为一个打开频率较高的应用,我还是希望能用上 App; 之前偶然在社区的几个帖子里也有一些用户问到是否有 App 可用,都没有了下文,可以满足一下这部分用户的需求; 作为一个长期维护的业余项目,更深刻地体会 App 开发的整个生命周期,也将一些想学习的技术应用到实际项目中; 借此机会认识一下光谷技术圈子里志趣相投的朋友。 前缘后续 上 GitHub 搜索 guanggoo 出来的结果很少,发现有一个 cauil/react-native-guanggoo 的项目适配了 iOS,独缺 Android 客户端,于是决定自己写一个。要不是那一阵刚好闹 Facebook 开源许可证风波,让人没有学习 React Native 的信心和欲望,也许我就学点 React Native 在这位仁兄的基础上开发了。 经过几周业余时间和十一长假期间的开发,目前完成度不算特别高,但常用的功能已经基本可用了,当然还有一些功能比如注册、帖子里的外部链接打开等,我是先抛给了系统浏览器。想着只埋头自己开发也比较枯燥,决定先放出一个版本来让网友们吐吐槽,提提意见,应该能做得更好。 PS: 本文非软文,也没有收取光谷社区任何好处,请光谷社区嘴炮管理员看到这里帮我开通个 VIP,我的社区 ID 是 mzlogin,:-P。 好了,最后照例安利一下我自己的微信公众号,近期专注 Java、Android 相关的技术分享,如果你感兴趣,可以关注一下接收最新动态。
思索了这两个问题良久,也去知乎找了一些相关话题的问答,但并没有标准答案。所以,我这里也只是记录一些我对此的看法,也许会随着 RTFSC 阅历的丰富而发生变化,我会记录更新于 https://github.com/mzlogin/rtfsc-android。 意义 在我看来,阅读源码的意义在于学习优秀的「套路」。 这里的「套路」所指范围很广,大到架构设计,小到可取的命名风格,还有设计模式、实现某类功能使用到的数据结构和算法等等。所谓高手,其实就是能比大部分人更早更快地掌握套路并熟练运用之人。 埋头在自己的天地里耕芸固然也能逐渐进步和成长,但总会有时候会遇到一些场景,你苦思良久也无法做出良好的设计,总会有一些时候,纠结如何为一个变量命名让你停下飞速敲击的手指。这些令你为难的场景,先贤们也许早就遇到过,并且给出了优雅的解决方案。看优秀的源码的时候,将这样的场景与对应的方案收入囊中,或者仅仅在脑中留下一个印象也好,以便在需要的时候,你的武器库里总能掏出一把称手的家伙来。 一些方法 不应该这样 不应该漫无目的地随手拿起一分源码,试图去通读。这一方面会过目即忘无所收获,另一方面会枯燥得让你迅速从着手到放弃。学习的方式有很多种,阅读源码并不一定是最适合你当前的情况的。 应该这样 精心挑选要阅读的源码项目。 这最好是与你的编程语言、你的工作内容、你的兴趣所在相关的,这样才能更切实地感受到阅读源码给你带来的益处,更有动力继续。 如果你想学习的知识点有官方文档,先看文档再看源码。 直接从源码着手,搞清楚原理固然是好,但是源码有可能是难啃的,先熟悉官方提供给所有人看的文档,能较为平滑地对这方面的知识先有个大概的了解,然后再结合源码去深入。 提出具体的问题,然后带着问题到源码中找答案。 比如在使用 Toast 的过程中,你可能会想到一些问题:Toast.makeText(...).show() 时发生了什么?Toast 能不能在非 UI 线程调用?能不能自定义 Toast 布局?诸如此类。在源码中探寻完你想要的答案,你的目的也就达到了。 从一些共性层面入手。 大部分的程序里都会使用到的东西,比如线程模型、UI 组织结构、任务调度方式等等。针对某一个方面去了解,比漫无目的要有效率得多。 最好能够编译运行起来。 如果一份代码你只能看不能跑,那可能读到一些地方你只能猜这个地方的数据值和跳转结构是怎么样的,而很有可能你猜的是错的。但如果你能编译运行,那在需要的时候你可以修改,加日志等等来更好地观察和验证你的想法,得到正确的理解。 做一些笔记。 一方面是将你的学习成果保留下来,方便随时查阅,毕竟只凭脑子记忆是不靠谱的;另一方面在学习的过程中,也能帮助理解。 对我的文章感兴趣的朋友,可以关注我的微信公众号 isprogrammer,接收我的更新通知。
在心中谋划已久的此事终于要开篇了,虽然迟了点,十年前没有种的树,就从现在开始种吧。 自 2014 年底从 Windows 开发转向 Android 开发以后,到现在的两年多时间里,也陆陆续续看了一些强相关的书籍和教程,按我在豆瓣标记的时间顺序: 《Android Programming: The Big Nert Ranch Guide》 《疯狂 Java 讲义》 《疯狂 Android 讲义》 《Android 软件安全与逆向分析》 《深入理解 Java 虚拟机》 《Android Training》 《Android 开发艺术探索》 《Android 群英传》 《App 研发录》 再加上平时在 GitHub 和掘金等技术社区的晃荡,接触过的资料其实也不少了,但回想起年初找工作的时候一些比较失落的经历,自身存在一些很要命的问题,就是如自己一直也知道的那样: 技术的广度还可以,可深度不够,不够聚焦; 掌握的知识是散乱的,不成体系,禁不住往深了问。 这一方面归因于自己的学习方式需要优化,技术书籍的学习不能只是通读,精读、主题阅读和针对性的实践总结一样不能少;另一方面挑选书籍要更有目的性,最好是明晰的由浅入深的层次递进;还有就是市面上流行的书籍和网络上的技术分享恐怕都无法让我彻底改善这种情况。 这个尴尬的年纪,在一个个焦虑的夜晚,躺在床上听着窗外偶尔传来的狗叫,汽车的呼啸,也会对自己呐喊,我不要做一个会用九种语言写 Hello World 但无一精通的程序员,迫切需要 聚焦、精通 和 成体系。 一切就从自己的工作内容最相关的业务与技术开始入手。业务相关的知识可以在工作时间来积累,业余的时间里,多 Read The F*cking Source Code,从优秀的源码和设计里汲取营养。有 AOSP 这样包罗万象的宝藏在侧,也无需纠结和寻觅到底应该看些什么,里面程序设计语言、设计模式、架构模式等等,应有尽有。 所以,最近计划开始写一系列 Android 源码分析的文章,包括 Android 系统源码及一些优秀的第三方类库等等。这将是一个比较漫长的过程,需要一个脉络,可选的方式是自底向上和自顶向下: 自底向上,一开始就是啃硬骨头,理解各种底层概念和知识,对我这样的选手来讲,容易失去兴趣,但是后期理解上层的东西更顺畅,毕竟基础知识点已经铺垫好了。比如《老罗的 Android 之旅》,就是比较典型的采用这种方式的。 ^ 难度 | --- | / \ | | | | / | | | | | / | | | | | | +----------------- |/ +-------------------------> 时间 自顶向下,预计是需要从几个熟悉的话题展开,充分下探之后,基础知识大致铺垫开,后面才会比较容易。这样做的一个好处是比较有目的性,总是知道下一个步要去找些什么,看些什么;另一点是从熟悉的东西讲起,前期曲线比较平滑。 ^ 难度 | _ | _/ | | _/ \ | _/ \ | / \ | _/ | | _/ \ | _/ \---- |/ +-------------------------> 时间 自认为没有恒心与毅力像罗升阳那样自底向上面面俱到地把 Android 的各个方面都分析一遍,我的大致的思路是打算从我们最常用的一些类开始,自顶向下摸索,牵扯到哪些需要深入理解的知识点,就另开一篇详细说明,这个过程类似 JVM 的 GC 中采用的可达性算法,只要选好了 GC roots,无论是深度优先还是广度优先,天长日久,总会覆盖所有我们常用的技术及其内部的原理。 举个例子,比如我第一篇打算分析 Toast 类,那由它展开可能会逐渐讲到很多的话题: Toast | |--- Handler | | | |--- Communicating with the UI thread | | | |--- Looper/Message/MessageQueue | | | |--- ThreadLocal | |--- Binder | | | |--- ServiceManager | | | |--- Inter-process communication | | | |--- ... | |--- Update UI outside the main thread | |--- ... 每介绍一篇,可能会发散出一些本篇中涉及但是没有深入讲解的知识点,留待后续补上,我将在文章最后把这些知识点列出来。在当前文章中需要提到的地方,就插个桩,留个尽量恰当的比喻在那里,「把它当作 XXX 理解就好了」。 当暂时沿着某一个主题延伸不下去,又没有找到合适的新主题的时候,就按一些比较有体系的主题来写,比如「Android 里的设计模式」,可以单独写一个系列。 这样做的目的,主要是想进行一些比较深入的学习和对过往知识的整理,将一个一个散落的知识点孤岛串联起来,让它们成为我的技能战斗群,写着应用代码时能对表面之下的原理了然于胸,减少写代码时对猜测和调试的依赖,对每一个我敢写出来的技能点,都能自信满满地与人对谈。 目前的计划是按照我最近一段时间更新公众号的频率,大约十天更新一篇,阅读源码主要使用的工具和方式是: GitHub 上的 android/platform_frameworks_base 等源码配合 Chrome 插件 insight.io 自己电脑上搭建的 OpenGrok 浏览 Android 7.1.2 源码 http://androidxref.com/7.1.1_r6/ 大致思路就是这样,实践一阵试试。不知道自顶向下的方法是否走得下去,要是实在不行,还是乖乖回去跟着老罗的脚步学习吧。 开篇,就当先立个 Flag 在这里,万一坚持做下去了呢,牛皮都不敢吹还哪敢做成什么事啊,最次也「世上无难事,只要肯放弃」嘛。对此有兴趣的朋友,请关注我的微信公众号「闷骚的程序员」,一起 Read The F*cking Android Source Code。 后续相关的系列文章,会在 GitHub 仓库 mzlogin/rtfsc-android 汇总更新,欢迎关注,看我起高楼,或是啪啪打脸。
从业以来主要在做客户端,用到的数据库都是表结构比较简单的 SQLite,以我那还给老师一大半的 SQL 水平倒也能对付。现在偶尔需要到后台的 SQL Server 里追查一些数据问题,就显得有点捉襟见肘了,特别是各种 JOIN,有时候傻傻分不清楚,于是索性弄明白并做个记录。 前言 在各种问答社区里谈及 SQL 里的各种 JOIN 之间的区别时,最被广为引用的是 CodeProject 上 C.L. Moffatt 的文章 Visual Representation of SQL Joins,他确实讲得简单明了,使用文氏图来帮助理解,效果明显。本文将沿用他的讲解方式,稍有演绎,可以视为该文较为粗糙的中译版。 约定 下文将使用两个数据库表 Table_A 和 Table_B 来进行示例讲解,其结构与数据分别如下: mysql> SELECT * FROM Table_A ORDER BY PK ASC; +----+---------+ | PK | Value | +----+---------+ | 1 | both ab | | 2 | only a | +----+---------+ 2 rows in set (0.00 sec) mysql> SELECT * from Table_B ORDER BY PK ASC; +----+---------+ | PK | Value | +----+---------+ | 1 | both ab | | 3 | only b | +----+---------+ 2 rows in set (0.00 sec) 其中 PK 为 1 的记录在 Table_A 和 Table_B 中都有,2 为 Table_A 特有,3 为 Table_B 特有。 常用的 JOIN INNER JOIN INNER JOIN 一般被译作内连接。内连接查询能将左表(表 A)和右表(表 B)中能关联起来的数据连接后返回。 文氏图: 示例查询: SELECT A.PK AS A_PK, B.PK AS B_PK, A.Value AS A_Value, B.Value AS B_Value FROM Table_A A INNER JOIN Table_B B ON A.PK = B.PK; 查询结果: +------+------+---------+---------+ | A_PK | B_PK | A_Value | B_Value | +------+------+---------+---------+ | 1 | 1 | both ab | both ab | +------+------+---------+---------+ 1 row in set (0.00 sec) 注:其中 A 为 Table_A 的别名,B 为 Table_B 的别名,下同。 LEFT JOIN LEFT JOIN 一般被译作左连接,也写作 LEFT OUTER JOIN。左连接查询会返回左表(表 A)中所有记录,不管右表(表 B)中有没有关联的数据。在右表中找到的关联数据列也会被一起返回。 文氏图: 示例查询: SELECT A.PK AS A_PK, B.PK AS B_PK, A.Value AS A_Value, B.Value AS B_Value FROM Table_A A LEFT JOIN Table_B B ON A.PK = B.PK; 查询结果: +------+------+---------+---------+ | A_PK | B_PK | A_Value | B_Value | +------+------+---------+---------+ | 1 | 1 | both ab | both ba | | 2 | NULL | only a | NULL | +------+------+---------+---------+ 2 rows in set (0.00 sec) RIGHT JOIN RIGHT JOIN 一般被译作右连接,也写作 RIGHT OUTER JOIN。右连接查询会返回右表(表 B)中所有记录,不管左表(表 A)中有没有关联的数据。在左表中找到的关联数据列也会被一起返回。 文氏图: 示例查询: SELECT A.PK AS A_PK, B.PK AS B_PK, A.Value AS A_Value, B.Value AS B_Value FROM Table_A A RIGHT JOIN Table_B B ON A.PK = B.PK; 查询结果: +------+------+---------+---------+ | A_PK | B_PK | A_Value | B_Value | +------+------+---------+---------+ | 1 | 1 | both ab | both ba | | NULL | 3 | NULL | only b | +------+------+---------+---------+ 2 rows in set (0.00 sec) FULL OUTER JOIN FULL OUTER JOIN 一般被译作外连接、全连接,实际查询语句中可以写作 FULL OUTER JOIN 或 FULL JOIN。外连接查询能返回左右表里的所有记录,其中左右表里能关联起来的记录被连接后返回。 文氏图: 示例查询: SELECT A.PK AS A_PK, B.PK AS B_PK, A.Value AS A_Value, B.Value AS B_Value FROM Table_A A FULL OUTER JOIN Table_B B ON A.PK = B.PK; 查询结果: ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'FULL OUTER JOIN Table_B B ON A.PK = B.PK' at line 4 注:我当前示例使用的 MySQL 不支持 FULL OUTER JOIN。 应当返回的结果(使用 UNION 模拟): mysql> SELECT * -> FROM Table_A -> LEFT JOIN Table_B -> ON Table_A.PK = Table_B.PK -> UNION ALL -> SELECT * -> FROM Table_A -> RIGHT JOIN Table_B -> ON Table_A.PK = Table_B.PK -> WHERE Table_A.PK IS NULL; +------+---------+------+---------+ | PK | Value | PK | Value | +------+---------+------+---------+ | 1 | both ab | 1 | both ba | | 2 | only a | NULL | NULL | | NULL | NULL | 3 | only b | +------+---------+------+---------+ 3 rows in set (0.00 sec) 小结 以上四种,就是 SQL 里常见 JOIN 的种类和概念了,看一下它们的合影: 有没有感觉少了些什么,学数学集合时完全不止这几种情况?确实如此,继续看。 延伸用法 LEFT JOIN EXCLUDING INNER JOIN 返回左表有但右表没有关联数据的记录集。 文氏图: 示例查询: SELECT A.PK AS A_PK, B.PK AS B_PK, A.Value AS A_Value, B.Value AS B_Value FROM Table_A A LEFT JOIN Table_B B ON A.PK = B.PK WHERE B.PK IS NULL; 查询结果: +------+------+---------+---------+ | A_PK | B_PK | A_Value | B_Value | +------+------+---------+---------+ | 2 | NULL | only a | NULL | +------+------+---------+---------+ 1 row in set (0.01 sec) RIGHT JOIN EXCLUDING INNER JOIN 返回右表有但左表没有关联数据的记录集。 文氏图: 示例查询: SELECT A.PK AS A_PK, B.PK AS B_PK, A.Value AS A_Value, B.Value AS B_Value FROM Table_A A RIGHT JOIN Table_B B ON A.PK = B.PK WHERE A.PK IS NULL; 查询结果: +------+------+---------+---------+ | A_PK | B_PK | A_Value | B_Value | +------+------+---------+---------+ | NULL | 3 | NULL | only b | +------+------+---------+---------+ 1 row in set (0.00 sec) FULL OUTER JOIN EXCLUDING INNER JOIN 返回左表和右表里没有相互关联的记录集。 文氏图: 示例查询: SELECT A.PK AS A_PK, B.PK AS B_PK, A.Value AS A_Value, B.Value AS B_Value FROM Table_A A FULL OUTER JOIN Table_B B ON A.PK = B.PK WHERE A.PK IS NULL OR B.PK IS NULL; 因为使用到了 FULL OUTER JOIN,MySQL 在执行该查询时再次报错。 ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'FULL OUTER JOIN Table_B B ON A.PK = B.PK WHERE A.PK IS NULL OR B.PK IS NULL' at line 4 应当返回的结果(用 UNION 模拟): mysql> SELECT * -> FROM Table_A -> LEFT JOIN Table_B -> ON Table_A.PK = Table_B.PK -> WHERE Table_B.PK IS NULL -> UNION ALL -> SELECT * -> FROM Table_A -> RIGHT JOIN Table_B -> ON Table_A.PK = Table_B.PK -> WHERE Table_A.PK IS NULL; +------+--------+------+--------+ | PK | Value | PK | Value | +------+--------+------+--------+ | 2 | only a | NULL | NULL | | NULL | NULL | 3 | only b | +------+--------+------+--------+ 2 rows in set (0.00 sec) 总结 以上七种用法基本上可以覆盖各种 JOIN 查询了。七种用法的全家福: 看着它们,我仿佛回到了当年学数学,求交集并集的时代…… 顺带张贴一下 C.L. Moffatt 带 SQL 语句的图片,配合学习,风味更佳: 更新:更多的 JOIN 除以上几种外,还有更多的 JOIN 用法,比如 CROSS JOIN(迪卡尔集)、SELF JOIN,可以参考 SQL JOINS Slide Presentation 学习。 CROSS JOIN 返回左表与右表之间符合条件的记录的迪卡尔集。 图示: 示例查询: SELECT A.PK AS A_PK, B.PK AS B_PK, A.Value AS A_Value, B.Value AS B_Value FROM Table_A A CROSS JOIN Table_B B; 查询结果: +------+------+---------+---------+ | A_PK | B_PK | A_Value | B_Value | +------+------+---------+---------+ | 1 | 1 | both ab | both ba | | 2 | 1 | only a | both ba | | 1 | 3 | both ab | only b | | 2 | 3 | only a | only b | +------+------+---------+---------+ 4 rows in set (0.00 sec) 上面讲过的几种 JOIN 查询的结果都可以用 CROSS JOIN 加条件模拟出来,比如 INNER JOIN 对应 CROSS JOIN ... WHERE A.PK = B.PK。 SELF JOIN 返回表与自己连接后符合条件的记录,一般用在表里有一个字段是用主键作为外键的情况。 比如 Table_C 的结构与数据如下: +--------+----------+-------------+ | EMP_ID | EMP_NAME | EMP_SUPV_ID | +--------+----------+-------------+ | 1001 | Ma | NULL | | 1002 | Zhuang | 1001 | +--------+----------+-------------+ 2 rows in set (0.00 sec) EMP_ID 字段表示员工 ID,EMP_NAME 字段表示员工姓名,EMP_SUPV_ID 表示主管 ID。 示例查询: 现在我们想查询所有有主管的员工及其对应的主管 ID 和姓名,就可以用 SELF JOIN 来实现。 SELECT A.EMP_ID AS EMP_ID, A.EMP_NAME AS EMP_NAME, B.EMP_ID AS EMP_SUPV_ID, B.EMP_NAME AS EMP_SUPV_NAME FROM Table_C A, Table_C B WHERE A.EMP_SUPV_ID = B.EMP_ID; 查询结果: +--------+----------+-------------+---------------+ | EMP_ID | EMP_NAME | EMP_SUPV_ID | EMP_SUPV_NAME | +--------+----------+-------------+---------------+ | 1002 | Zhuang | 1001 | Ma | +--------+----------+-------------+---------------+ 1 row in set (0.00 sec) 补充说明 文中的图使用 Keynote 绘制; 个人的体会是 SQL 里的 JOIN 查询与数学里的求交集、并集等很像; SQLite 不支持 RIGHT JOIN 和 FULL OUTER JOIN,可以使用 LEFT JOIN 和 UNION 来达到相同的效果; MySQL 不支持 FULL OUTER JOIN,可以使用 LEFT JOIN 和 UNION 来达到相同的效果; 假如你对我的文章感兴趣,可以关注我的微信公众号 isprogrammer 随时阅读更多内容。 参考 Visual Representation of SQL Joins How to do a FULL OUTER JOIN in MySQL? SQL JOINS Slide Presentation SQL Self Join
自从几年前开始在 GitHub 玩耍,接触到 Markdown 之后,就一发不可收拾,在各种文档编辑上,有条件用 Markdown 的尽量用,不能用的创造条件也要用——README、博客、公众号、接口文档等等全都是,比如当前这篇文章就是用 Markdown 编辑而成。 这几年也发现越来越多的网站和程序提供了对 Markdown 的支持,从最初接触的 GitHub、Jekyll,到简书、掘金、CSDN 等等,由此也从别人做得好的文档中,学到了一些『奇技淫巧』,所以本文不是对 Markdown 基础语法的介绍,而是一些相对高级、能将 Markdown 玩出更多花样的小技巧。 注:如下技巧大多是利用 Markdown 兼容部分 HTML 标签的特性来完成,不一定在所有网站和软件里都完全支持,主要以 GitHub 支持为准。 在表格单元格里换行 借助于 HTML 里的 <br /> 实现。 示例代码: | Header1 | Header2 | |---------|----------------------------------| | item 1 | 1. one<br />2. two<br />3. three | 示例效果: Header1 Header2 item 1 1. one2. two3. three 图文混排 使用 <img> 标签来贴图,然后指定 align 属性。 示例代码: <img align="right" src="https://raw.githubusercontent.com/mzlogin/mzlogin.github.io/master/images/posts/markdown/demo.png"/> 这是一个示例图片。 图片显示在 N 段文字的右边。 N 与图片高度有关。 刷屏行。 刷屏行。 到这里应该不会受影响了,本行应该延伸到了图片的正下方,所以我要足够长才能确保不同的屏幕下都看到效果。 示例效果: 这是一个示例图片。 图片显示在 N 段文字的右边。 N 与图片高度有关。 刷屏行。 刷屏行。 到这里应该不会受影响了,本行应该延伸到了图片的正下方,所以我要足够长才能确保不同的屏幕下都看到效果。 控制图片大小和位置 标准的 Markdown 图片标记 ![]() 无法指定图片的大小和位置,只能依赖默认的图片大小,默认居左。 而有时候源图太大想要缩小一点,或者想将图片居中,就仍需要借助 HTML 的标签来实现了。图片居中可以使用 <div> 标签加 align 属性来控制,图片宽高则用 width 和 height 来控制。 示例代码: **图片默认显示效果:**  **加以控制后的效果:** <div align="center"><img width="65" height="75" src="https://raw.githubusercontent.com/mzlogin/mzlogin.github.io/master/images/posts/markdown/demo.png"/></div> 示例效果: 图片默认显示效果: 加以控制后的效果: 格式化表格 表格在渲染之后很整洁好看,但是在文件源码里却可能是这样的: |Header1|Header2| |---|---| |a|a| |ab|ab| |abc|abc| 不知道你能不能忍,反正我是不能忍。 好在广大网友们的智慧是无穷的,在各种编辑器里为 Markdown 提供了表格格式化功能,比如我使用 Vim 编辑器,就有 vim-table-mode 插件,它能帮我自动将表格格式化成这样: | Header1 | Header2 | |---------|---------| | a | a | | ab | ab | | abc | abc | 是不是看着舒服多了? 如果你不使用 Vim,也没有关系,比如 Atom 编辑器的 markdown-table-formatter 插件,Sublime Text 3 的 MarkdownTableFormatter 等等,都提供了类似的解决方案。 使用 Emoji 这个是 GitHub 对标准 Markdown 标记之外的扩展了,用得好能让文字生动一些。 示例代码: 我和我的小伙伴们都笑了。:smile: 示例效果: 我和我的小伙伴们都笑了。:smile: 更多可用 Emoji 代码参见 https://www.webpagefx.com/tools/emoji-cheat-sheet/。 行首缩进 直接在 Markdown 里用空格和 Tab 键缩进在渲染后会被忽略掉,需要借助 HTML 转义字符在行首添加空格来实现,  代表半角空格,  代表全角空格。 示例代码:   春天来了,又到了万物复苏的季节。 示例效果: 春天来了,又到了万物复苏的季节。 展示数学公式 如果是在 GitHub Pages,可以参考 http://wanguolin.github.io/mathmatics_rending/ 使用 MathJax 来优雅地展示数学公式(非图片)。 如果是在 GitHub 项目的 README 等地方,目前我能找到的方案只能是贴图了,以下是一种比较方便的贴图方案: 在 https://www.codecogs.com/latex/eqneditor.php 网页上部的输入框里输入 LaTeX 公式,比如 $$x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}$$; 在网页下部拷贝 URL Encoded 的内容,比如以上公式生成的是 https://latex.codecogs.com/png.latex?%24%24x%3D%5Cfrac%7B-b%5Cpm%5Csqrt%7Bb%5E2-4ac%7D%7D%7B2a%7D%24%24; 在文档需要的地方使用以上 URL 贴图,比如  示例效果: 任务列表 在 GitHub 和 GitLab 等网站,除了可以使用有序列表和无序列表外,还可以使用任务列表,很适合要列出一些清单的场景。 示例代码: **购物清单** - [ ] 一次性水杯 - [x] 西瓜 - [ ] 豆浆 - [x] 可口可乐 - [ ] 小茗同学 示例效果: 购物清单 一次性水杯 西瓜 豆浆 可口可乐 小茗同学 自动维护目录 有时候维护一份比较长的文档,希望能够自动根据文档中的标题生成目录(Table of Contents),并且当标题有变化时自动更新目录,能减轻工作量,也不易出错。 如果你使用 Vim 编辑器,那可以使用我维护的插件 vim-markdown-toc 来帮你完美地解决此事: 插件地址:https://github.com/mzlogin/vim-markdown-toc 如果你使用其它编辑器,一般也能找到对应的解决方案,比如 Atom 编辑器的 markdown-toc 插件,Sublime Text 的 MarkdownTOC 插件等。 后话 好了,这一波的奇技淫巧就此告一段落,希望大家在了解这些之后能有所收获,更好地排版,专注写作。 如果你觉得本文对你有帮助,欢迎关注我的微信公众号 isprogrammer,获取更多有帮助的内容。 参考 https://raw.githubusercontent.com/matiassingers/awesome-readme/master/readme.md https://www.zybuluo.com/songpfei/note/247346
前天遇到了一个 NullPointerException,触发的代码类似下面这样: public class Test { public static long test(long value) { return value; } public static void main(String[] args) { Long value = null; // ... test(value); } } main 方法里的代码实际上相当于调用 test(null);,为什么不直接这样写呢?因为编译不过,会报 错误: 不兼容的类型: <空值>无法转换为long。 抛出问题 运行时提示 test(value); 这一行抛出 NullPointerException,但是看着以上代码会有些许困惑:以上代码里一个对象方法都没有调用啊,NullPointerException 从何而来? 原因分析 这时,如果留意到 test 方法接受的参数是 long 类型,而我们传入的是 Long 类型(虽然其实是 null),就会想到这会经历一次从类型 Long 到基本数据类型 long 的自动拆箱过程,那会不会是这个过程中抛出的 NullPointerException 呢?因为以前只知道 Java 为一些基础数据类型与对应的包装器类型之间提供了自动装箱拆箱机制,而并不知这机制的具体实现方法是怎么样的,正好学习一下。 用命令 javap -c Test 将以上代码编译出的 Test.class 文件进行反汇编,可以看到如下输出: Compiled from "Test.java" public class Test { public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static long test(long); Code: 0: lload_0 1: lreturn public static void main(java.lang.String[]); Code: 0: aconst_null 1: astore_1 2: aload_1 3: invokevirtual #2 // Method java/lang/Long.longValue:()J 6: invokestatic #3 // Method test:(J)J 9: pop2 10: return } 从以上字节码及对应的注释可以看出,test(value); 这一行被编译后等同于如下代码: long primitive = value.longValue(); test(promitive); 相比实际代码,多出的 long primitive = value.longValue(); 这一行看起来就是自动拆箱的过程了,而我们传入的 value 为 null,value.longValue() 会抛出 NullPointerException,一切就解释得通了。用更简洁的代码表达出了更丰富的含义,这就是所谓的语法糖了。 证实猜想 那么我们上面得出的自动拆箱机制的结论是否正确呢?选择一种其它基本数据类型,比如 int,来佐证一下: public class Test { public static void main(String[] args) { Integer value = 10; int primitive = value; } } 反汇编后对应的字节码: Compiled from "Test.java" public class Test { public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: bipush 10 2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: astore_1 6: aload_1 7: invokevirtual #3 // Method java/lang/Integer.intValue:()I 10: istore_2 11: return } 由以上字节码我们可以印证下文里的知识点了。 自动装箱与拆箱 自动装箱与拆箱是 Java 1.5 引入的新特性,是一种语法糖。 在此之前,我们要创建一个值为 10 的 Integer 对象,只能写作: Integer value = new Integer(10); 而现在,我们可以更方便地写为: Integer value = 10; 定义与实现机制 自动装箱,是指从基本数据类型值到其对应的包装类对象的自动转换。比如 Integer value = 10;,是通过调用 Integer.valueOf 方法实现转换的。 自动拆箱,是指从包装类对象到其对应的基本数据类型值的自动转换。比如 int primitive = value;,是通过调用 Integer.intValue 方法实现转换的。 基本数据类型 包装类型 装箱方法 拆箱方法 boolean Boolean Boolean.valueOf(boolean) Boolean.booleanValue() byte Byte Byte.valueOf(byte) Byte.byteValue() char Character Character.valueOf(char) Character.charValue() short Short Short.valueOf(short) Short.shortValue() int Integer Integer.valueOf(int) Integer.intValue() long Long Long.valueOf(long) Long.longValue() float Float Float.valueOf(float) Float.floatValue() double Double Double.valueOf(double) Double.doubleValue() 发生时机 自动装箱与拆箱主要发生在以下四种时机: 赋值时; 比较时; 算术运算时; 方法调用时。 常见应用场景 Case 1: Integer value = 10; // 自动装箱(赋值时) int primitive = value; // 自动拆箱(方法调用时) Case 2: Integer value = 1000; // ... if (value <= 1000) { // 自动拆箱(比较时) // ... } Case 3: List<Integer> list = new ArrayList<>(); list.add(10); // 自动装箱(方法调用时) int i = list.get(0); // 自动拆箱(赋值时) 注:集合(Collections)里不能直接放入原始类型,集合只接收对象。 Case 4: ThreadLocal<Integer> local = new ThreadLocal<Integer>(); local.set(10); // 自动装箱(方法调用时) int i = local.get(); // 自动拆箱(赋值时) 注:ThreadLocal 不能存储基本数据类型,只接收引用类型。 Case 5: public void fun1(Integer value) { // } public void fun2(int value) { // } public void test() { fun1(10); // 自动装箱(方法调用时) Integer value = 10; fun2(value); // 自动拆箱(方法调用时) } Case 6: Integer v1 = new Integer(10); Integer v2 = new Integer(20); int v3 = 30; int sum = v1 + v2; // 自动拆箱(算术运算时) sum = v1 + 30; // 自动拆箱(算术运算时) 相关知识点 比较 除 == 以外,包装类对象与基本数据类型值的比较,包装类对象与包装类对象之间的比较,都是自动拆箱后对基本数据类型值进行比较,所以,要注意这些类型间进行比较时自动拆箱可能引发的 NullPointerException。 == 比较特殊,因为可以用于判断左右是否为同一对象,所以两个包装类对象之间 ==,会用于判断是否为同一对象,而不会进行自动拆箱操作;包装类对象与基本数据类型值之间 ==,会自动拆箱。 示例代码: Integer v1 = new Integer(10); Integer v2 = new Integer(20); if (v1 < v2) { // 自动拆箱 // ... } if (v1 == v2) { // 不拆箱 // ... } if (v1 == 10) { // 自动拆箱 // ... } 缓存 Java 为整型值包装类 Byte、Character、Short、Integer、Long 设置了缓存,用于存储一定范围内的值,详细如下: 类型 缓存值范围 Byte -128 ~ 127 Character 0 ~ 127 Short -128 ~ 127 Integer -128 ~ 127(可配置) Long -128 ~ 127 在一些情况下,比如自动装箱时,如果值在缓存值范围内,将不创建新对象,直接从缓存里取出对象返回,比如: Integer v1 = 10; Integer v2 = 10; Integer v3 = new Integer(10); Integer v4 = 128; Integer v5 = 128; Integer v6 = Integer.valueOf(10); System.out.println(v1 == v2); // true System.out.println(v1 == v3); // false System.out.println(v4 == v5); // false System.out.println(v1 == v6); // true 缓存实现机制: 这里使用了设计模式享元模式。 以 Short 类实现源码为例: // ... public final class Short extends Number implements Comparable<Short> { // ... private static class ShortCache { private ShortCache(){} static final Short cache[] = new Short[-(-128) + 127 + 1]; static { for(int i = 0; i < cache.length; i++) cache[i] = new Short((short)(i - 128)); } } // ... public static Short valueOf(short s) { final int offset = 128; int sAsInt = s; if (sAsInt >= -128 && sAsInt <= 127) { // must cache return ShortCache.cache[sAsInt + offset]; } return new Short(s); } // ... } 在第一次调用到 Short.valueOf(short) 方法时,将创建 -128 ~ 127 对应的 256 个对象缓存到堆内存里。 这种设计,在频繁用到这个范围内的值的时候效率较高,可以避免重复创建和回收对象,否则有可能闲置较多对象在内存中。 使用不当的情况 自动装箱和拆箱这种语法糖为我们写代码带来了简洁和便利,但如果使用不当,也有可能带来负面影响。 性能的损耗 Integer sum = 0; for (int i = 1000; i < 5000; i++) { // 1. 先对 sum 进行自动拆箱 // 2. 加法 // 3. 自动装箱赋值给 sum,无法命中缓存,会 new Integer(int) sum = sum + i; } 在循环过程中会分别调用 4000 次 Integer.intValue() 和 Integer.valueOf(int),并 new 4000 个 Integer 对象,而这些操作将 sum 的类型改为 int 即可避免,节约运行时间和空间,提升性能。 java.lang.NullPointerException 尝试对一个值为 null 的包装类对象进行自动拆箱,就有可能造成 NullPointerException。 比如: Integer v1 = null; int v2 = v1; // NullPointerException if (v1 > 10) { // NullPointerException // ... } int v3 = v1 + 10; // NullPointerException 还有一种更隐蔽的情形,感谢 @周少 补充: public class Test { public static void main(String[] args) { long value = true ? null : 1; // NullPointerException } } 这实际上还是对一个值为 null 的 Long 类型进行自动拆箱,反汇编代码: Compiled from "Test.java" public class Test { public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: aconst_null 1: checkcast #2 // class java/lang/Long 4: invokevirtual #3 // Method java/lang/Long.longValue:()J 7: lstore_1 8: return } 参考 Java中的自动装箱与拆箱 深入剖析Java中的装箱和拆箱
我做了一个有点艰难的决定,离开帝都,回到了武汉工作和生活。 这让我想起近几年在各种新闻流标题里经常看到的两种说法,“逃离北上广”和“逃回北上广”,既然是逃,自然便是有什么东西在后面追,或者是高房价,或者是职业前景,或者是生活压力。回首我自己的决策过程,虽然也像现在好多年轻人一样处在“留不下的城市,回不去的故乡”的尴尬境地里,有一些无奈的成分,庆幸的是选择的成分相对还是更多一点。 身处北京时,常有一种莫名的“临时感”,总感觉生活里的很多东西都是临时不可长久持续的:房子是租来的,不想花钱和精力去提升它的舒适度;工作和同事们很给力,但不知道什么时候会中断,离开;想进行一些学习与深造,会考虑周期那么长,会不会无法在北京待到学完的时候?后来我明白了,这就是一个心态的问题,总认为生活不在北京,在未来的某个地方。想起多年前非常火的一个词,北漂,在北京的这些年头里从来没把自己跟这个词联系起来,但实际上我就是北漂,身漂心也漂。 我曾经跟小别同学讲说,有时候加个班回家晚,在路上听到《北京北京》鼻子会有点酸,她表示无法理解这种感受。其实早些年我也不会,也许是我老了。 即使心里非常清楚这样一个决定会给生活带来颠覆式的变化,我也还是比较匆忙地结束了在北京的一切,匆忙地做了工作交接,匆忙地与朋友们告别,约或者不约,故意推迟周知朋友们的时间,因为怕过渡期时间久会引得自己惆怅和伤感,但这些又如何避免得了呢。 我知道我从此放弃了一些东西,我将逐渐淡出几年里融入的圈子,跟好些朋友也许从此不会再见。 我也知道我将收获很多新的东西,认识很多不同的人,见到很多不一样的风景。我有点失落,但并不慌张,我告诉自己牢记一点:每当迷失方向,想想曾经艰难地放弃过什么。 在纠结惆怅的日子里,我曾经一度以为我要放弃的还有梦想,但后来想通了,不是只有北上广才能承载梦想,与我爱的人一起幸福平静地生活,我正奔着这个梦想而去。 何必寻找所谓的天堂 原来我 因为你 不想再去流浪 情愿平凡 不拥有一切也无妨 有了你 在心上 已然是天堂 — 光良《天堂》
《追风筝的人》确实是一部相当优秀而又深刻的作品。——小别老师 图 1. 《追风筝的人》/卡勒德.胡赛尼 封面 当在我用一整个下午读完这本书,印象最深的句子是下面这两个: 那儿有再次成为好人的路。 为你,千千万万遍。 这样的句子翻译之后总觉得少了点意思,感受下原文: There is a way to be good again. For you, a thousand times over. 就是这两个简单的句子,承载了故事的主要线索——自我救赎与献身。 剧透 小说其实讲述了一个非常简单的故事: 一心只想讨父亲欢心的少爷阿米尔与他忠诚的仆人哈桑从小一起长大,哈桑总是为阿米尔背锅,还在危急关头为他挺身而出,但是在哈桑被反社会分子逼到巷角时,阿米尔却眼睁睁地看着强暴发生,什么都没有做。 阿米尔后来无法承受对自己懦弱的自责所带来的痛苦,就设计逼走了哈桑。随后阿富汗发生战乱,阿米尔随父亲逃到美国讨生活,有了美丽的妻子和体面的职业,过上了岁月静好的生活。 但是多年以后,父亲旧时好友告诉了他一个惊天秘密:原来哈桑是他同父异母的弟弟。虽然被一直以来形象正面的父亲蒙蔽多年给心灵带来的冲击巨大,但为了救赎自己少年时的背叛,他还是冒着生命危险去营救弟弟唯一的骨肉,最后果然有了生命危险,他也为自己多年前的「罪行」受到了「惩罚」,完成了自我救赎。一番波折之后,带着弟弟的骨肉回到了美国。 笔记 整体评价 对于我这样的俗人来说,一部小说或者讲故事的电影优秀与否其实很好判断: 看完之后能否比较容易地复述整个故事。 不为了制造戏剧冲突而夸大其词,造成情节和情感脱线。 能让人在听故事之余,在人性或亲情等方面产生更深刻的认识。 从这几点来讲,《追风筝的人》都不失为一部好作品。 下面分几个方面来记录一下我个人的理解: 哈桑 哈桑是忠诚的,一辈子都在「献身」,但他却因此成为故事里悲剧部分的主要载体。 少爷出主意恶作剧时,他负责背锅;少爷受欺负时,他负责挡枪;自己遭受暴行时,少爷袖手旁观,但他却依然不变初心;少爷想要激怒他来减轻自己的负罪感时,默默承受;少爷栽赃给他时,波澜不惊地应承下来;直至最后,都是为了守护少爷的房子而死。 他让我想起我们小学时接受思想品德教育时,「舍己为人」、「先人后己」和「毫不利己,专门利人」这些词汇,我们可以逐步跳脱出来,而身处那个环境的哈桑——少爷说,「哈桑从未拒绝我任何事情」。 但在你为他献身之前,你想过吗?他会为你献身吗? 他对少爷怀着的是一种我无法理解的感情。 父亲 在大部分时候,他是一个令人敬佩的形象。 他乐善好施,不屈从世俗与宗教,选择自己认为对的路,并且将生活经营得有声有色;他心怀高尚的情操,面对俄国士兵的枪杆也毫不退缩,靠自己赢得所有人的尊敬;即使逃到美国,也只靠自己双手劳作,拒绝靠政府的救济来生存。 他对世间罪行的概括: 罪行只有一种,那就是盗窃,其它罪行都是盗窃的变种。当你杀害一个人,你偷走一条性命,你偷走他妻子身为人妇的权利,夺走他子女的父亲。当你说谎,你偷走别人知道真相的权利。当你诈骗,你偷走公平的权利。 在阅读他的事迹的时候,我常在想,原来这样的人,真的存在。 他这一生也许只做过一件羞耻的错事,那就是给阿米尔带来了一个同父异母的兄弟。所以当我看到这一点,受到的冲击是巨大的,可以想见阿米尔为何会因此而崩溃。 他展现在大伙面前的正面的神迹般的一生,也是对这唯一一件错事的自我救赎。 阿米尔 故事的绝对主线,一个懦弱的孩子,要不是在父亲,哈桑,和好些其它人的保护之下,我很怀疑他能顺利长大。小时候的他,身上充斥着各种他自己的察觉或未察觉的阴暗面。 他与哈桑的一段对话让我印象深刻: 「我骗过你吗,阿米尔少爷?」 「我不知道,你会骗我吗?」 「我宁愿吃泥巴也不会骗你。」 「真的吗?你会那样做?」 「做什么?」 「如果我让你吃泥巴,你会吃吗?」 「如果你要求,我会的。不过我怀疑,你是否会让我那么做。你会吗,阿米尔少爷?」 阿塞夫是反社会,但是他对阿米尔的总结却也并不见得有多偏颇: 为什么他跟客人玩总不喊上你?为什么他总是在没有人的时候才理睬你?我告诉你为什么,哈扎拉人。因为对他来说,你什么都不是,只是一只丑陋的宠物。一种他无聊的时候可以玩的东西,一种他发怒的时候可以踢开的东西。 阿米尔小时候拼命想得到父亲的认可与亲近,而哈桑却在不经意间就能获得,所以一定程度上讲,他是嫉妒哈桑的。 他了解哈桑对自己的感情与忠诚,并且利用和背叛了它,却又受自己的良心煎熬,毕竟「一直以来,你对自己太严苛了」,所以在二十五年后,冒着生命危险也要回到故乡去带回索拉博,名义上是援救这个孩子,实际只为了完成那一场自我救赎。只是与他受人尊敬坚毅果敢的父亲不同,他的这场救赎来得实在太晚。 阿富汗 这个国度以前在我的印象里真的只有连年战乱,塔利班,火箭弹。 现在,脑海里对它的曾经的样子,经历的历史进程,国民遭受的苦难有了更具象的刻画,也许有一天,我会去查更多关于它的资料,只为理解这个世界上真的有这样一个地方,它的人民真的生活在这样的环境里,而它曾经孕育过一代又一代能快乐地追风筝的少年。 作者胡赛尼「立志拂去蒙在阿富汗普通民众面孔的尘灰,将背后灵魂的悸动展示给世人」,从某种意义上讲,他实现了这个志向。 同名电影 在 2017 贺岁档电影里,我唯一觉得还不错的《乘风破浪》(豆瓣 7.0 分),之所以广受好评,主要也是因为第一点做得比较好,完整地讲述了一个简单的故事,让人知所云,这本来应该是一部电影的基本素质,但是它做到这一点,却已比如今一众故弄玄虚总想搞个大新闻的烂片不知道高到哪里去了。 而本书的同名电影在豆瓣上评分高达了 8.2 分,可见也很值得一看。但我的体会是在阅读本书的过程中,脑海里能根据作者的讲述不断脑补起各种场景、人物和剧情,所以不建议看完本书之后立即去看电影——脑海里刚刚放映过包含各种细节和微妙之处的完整版,马上接着去看有删节有改编版,难免会失落。 感受 一个好的故事,应该连续地读完,及时记下所感所想。 虽然我不擅长写读书笔记、读后感,但是输入之后有输出真的是一个能让自己受益良多的好习惯。不顾忌那么多,先写下来,然后对记录的模板、方式和内容进行迭代。 希望本篇成为一个良好的开端。
总结是为了更好地再上路。 在我人生的前二十几年是没有做年度计划和总结的习惯的,但在 2015-12-31 时和饭团团员们心血来潮一起做了个 2016 PLAN,现在 2016 已经被完整地甩在身后,是时候回顾以明得失,展望以知进退了。 如果用一句话来概括,这一年是玩得不错,学得不咋样;展开来讲的话,按传统得欲扬先抑,先把批评做在前头,上成绩单: 成绩单 当时制定的计划一共 16 项,现状如下: 自评 惨,不及格。 当然我的过去就是我存在的基石,一味地全盘否定并不能改变什么,需要认可我自己的是清单里可能最难、但对我提升最大的两项任务完成了。 反思 我对跨度较大的计划的把控能力略差,做这件事情需要的能力跟项目管理很类似,都还需要我有意识地花大力气去培养。 目标的约束力 引用最有故事的女同学曲玮玮的话说:真正的目标是清晰明确的、有时间约束的、可操作执行并且可以检验的。 我在制定计划时存在一个很大的问题是没有给任务加上时间约束,而在一年这样一个相对很长的时间跨度里,没有时间约束的目标很容易被无限押后,并最终导致计划的流产。 计划的迭代 在一个计划周期里,处境,心态,都可能发生了天翻地覆的变化,想要做的事情也可能需要随之调整,保持对计划清单的关注,确保留在清单里的仍然是你最想完成的事项。 执行力 候捷老师说,天下大事,必作于细。 小别老师说,「做事情抓重点」+「执行力」。 我对自己说,想明白最重要的里面最想做的,迈出第一步,少年莫踌躇。 有形的记录 阅读和影音娱乐 数据主要来自豆瓣上的记录。 用一句网络流行语来说,努力了可能没什么卵用,但不努力真的很舒服啊。:sob: 从数字上看,我这一年过得没有之前努力了,花在书籍上的时间与影音方面的时间此消彼长,特别是技术类书籍的阅读少了很多,这都是需要注意的地方。 网络活动 GitHub 贡献日历: 过去一年总共有 935 次 Commits/Issues/PR,相比 2015 年的 765 次小幅提升; 最长连击记录是 28 天,是 2015-12-31 到 2016-01-27; 每一周都活跃。 相较于活跃的数字来讲,过去一年里值得一提的个人项目主要是下面这三个: awesome-adb 目前网络上能找到的最完善的 ADB 用法大全。 它在 GitHub 的 Trending 上待了几天,最高 Repositories 排名第 4,最高 Developers 排名第 6,目前 Star 数过 2000。我至今仍记得上 Trending 的第一天我激动地与 Linus 合影留恋时的心情。 vim-markdown-toc 一款 Vim 插件,主要功能是为 Markdown 文档自动生成和维护 Table of Contents。 Star 数不到百,但每天都有人 clone,Vim 必备插件 nerdtree 的作者 scrooloose 还来提过 Issue 和 PR,让我体会到与传说中的人交流的感受。 chinese-copywriting-guidelines 中文文案排版指北(简体中文版)。 这是一个 Fork 项目,却也是第一个让我尝到 Star 数破百的滋味的项目,感谢它。 得与失: 参与和维护开源项目是一件很有意思的事情,可能让你接触到久仰的大神,也可能占用你很多的时间,可以从中学到多人协作的方式与智慧,也可能让你在连击与 Star 里迷失。起初最盼望的是自己的项目有人关注,这样的时刻真正来临之后你就会发现身上多了许多责任,解答网友们与项目相关的问题义不容辞,及时响应对维持热度必不可少。 在这方面的付出与收获自己觉得还比较满意,以后会继续保持活跃,而且要活跃得更有质量,更有效率。 需要引起重视的地方是作为一名 Android 开发者,没有一个长期维护的相关代表作,这是严重的失误。 技术社区 这一年里我成为了掘金联合编辑、极乐知乎专栏原创作者,在掘金分享和发布了 11 篇文章,收获了 1388 个收藏,在极乐知乎专栏发布 1 篇文章,收获 80 个赞和 60 名粉丝。 在它们的原创作者群里可以接触到很多业界知名的开发者和很厉害的小朋友们,他们也跟大家一样嬉笑怒骂,但又明摆着实力碾压,无形中带来很多的压力和激励。 博客 这一年在个人博客上共发表 17 篇文章,大致上半年每月 2 篇,下半年每月 1 篇的节奏。 以后可以再多写一点。我比较满意自己的一点是近两年我对学过的东西和遇到过的问题在 OneNote 上都作了详细的分类记录,而这些笔记趁热乎的成篇发布出来对整理思路和加深理解都是有益的,毕竟你能清楚地讲解给别人听,才算是真懂了。 运动 过去一年的运动记录主要分布在羽毛球、跑步、瑜伽和网球上。 羽毛球 参加公司羽毛球俱乐部活动 20 次,集团俱乐部活动几次,搜狐联赛 1 次。技术上进步不大,抽空看完了李在福的《追球》系列,周末偶尔参加回龙观的情怀羽毛球俱乐部的活动。 跑步 参加公司跑团活动 15 次,和小伙伴们约跑以及自己练习若干次,收获了两枚半程马拉松奖牌和很多好友。 瑜伽 参加集团俱乐部活动几次,身体僵硬,练习完很放松,很久没去了。 网球 参加公司网球俱乐部活动 15 次,入门菜鸡选手,到处捡球,很久没打了。 工作 占据绝大多数时间的工作,在这一年里只能说中规中矩,却也实现了升职加薪的愿望。 继续从事 Android 开发,因工作需要也接手了一些 PHP 后台代码维护,踩过很多坑,改过很多 Bug,个人能力的提升并不能令自己满意。 一方面深感自己在技术上缺乏深度,另一方面也因职业发展焦虑而迫切想要进一步提升在实际编码层次之上的架构规划与设计能力。试着去阅读更多优秀的源码,也试着让自己在接手任务之后将更多的时间用于设计,更多地画图、做笔记来捋清和记录自己的想法,确保自己的编码过程中是思路清晰的。蜕变的过程是辛苦的,但这一步别人只能提供一些建议,更多的还得靠自己去学习和思考。 制订一个清晰合理的职业规划来指引自己前行是必要的,否则可能看上去很努力,实际也做了很多事情,但两点之间直线最短,有明确的目标你才能笔直前进。 游戏 今年游戏玩得比去年多,主要是手游《皇室战争》和《阴阴师》,在业余时间里它们能让我暂时摆脱焦虑与烦恼,专注于一个小屏幕里,去发掘和操作一些套路,尽管那也许就是游戏策划们设计好的套路。 另外,和李小昂同学合作使用网易的自动化测试框架写网易游戏的自动化脚本也挺有意思。 昨天我卸载了《阴阳师》,从中节省出来的时间我想干一点更有意思的事情,目前还没有想好,欢迎推荐。 旅行 由于异地装修,这一年里稍长一点的假期基本奔波于帝都和武汉之间,能称得上旅行的只有和范师傅周大神一起的大五朝台了。 两天步行五十多公里,单日计步超过五万,在徒步过程中数次想放弃然而最终坚持下来,第一次在寺庙挂单,这些都深深地印在脑海里。 无形的收获 除了上述的这些,我还收获了一些看不见摸不着,但是也实实在在的东西。 一是和小别同学在经历了无数的争吵后达成的共识越来越多,求同存异中也多了许多体谅与包容。 二是在参加各种运动的过程中收获了许多好友,让我得以在工作之外感受更多生活的精彩。 展望 参加工作六年有余,近一两年日益焦虑,猜想大致与若干年后将体会到的中年危机类似,唯不断的思考和努力方可突破。 在接下来的日子里,我期望自己有: 更多的思考,虽然 done is better than perfect; 更多的阅读,特别是 the fucking source code; 更多的交流,训练简洁的表达能联动促进思维更敏捷; 更少的焦虑,想清楚,笃定前行; 一个家。 结语 最后要感谢亲人朋友们一如既往的支持,感谢领导和同事们的爱护与帮助,引用最近认识的超厉害的 小故事 的年终总结标题与大家共勉: 你只管努力,剩下的交给时光。 祝大家新年快乐!