IT牛人博客聚合网站 发现IT技术最优秀的内容, 寻找IT技术的价值 http://www.udpwork.com/ zh_CN http://www.udpwork.com/about hourly 1 Sat, 24 Jun 2017 18:27:57 +0800 <![CDATA[由 rc_mksid 引起 pppd 奔溃的一个 bug]]> http://www.udpwork.com/item/16321.html http://www.udpwork.com/item/16321.html#reviews Fri, 23 Jun 2017 18:32:56 +0800 jaseywang http://www.udpwork.com/item/16321.html 最近手贱想把手头的几台玩具机器统一下标准,其中一个标准是将 kernel.pid_max 增加到了 512000,结果就在当天的凌晨,一台跑着 pptp 的 VPS 崩溃了:

Jun 20 22:53:09 jaseywang vps pptpd: ======= Backtrace: =========
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(__fortify_fail+0x37)[0x7fdba6416047]
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(+0x10d200)[0x7fdba6414200]
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(+0x10c709)[0x7fdba6413709]
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(_IO_default_xsputn+0xbc)[0x7fdba637f60c]
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(_IO_vfprintf+0xb0d)[0x7fdba634ec3d]
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(__vsprintf_chk+0x88)[0x7fdba6413798]
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(__sprintf_chk+0x7d)[0x7fdba64136ed]
Jun 20 22:53:09 jaseywang vps pptpd: /usr/lib/../lib64/pppd/2.4.5/radius.so(rc_mksid+0x43)[0x7fdba4272763]
Jun 20 22:53:09 jaseywang vps pptpd: /usr/lib/../lib64/pppd/2.4.5/radius.so(+0x48f0)[0x7fdba426d8f0]
Jun 20 22:53:09 jaseywang vps pptpd: /usr/sbin/pppd(notify+0x27)[0x562eaeb64847]
Jun 20 22:53:09 jaseywang vps pptpd: /usr/sbin/pppd(+0x1cfff)[0x562eaeb6cfff]
Jun 20 22:53:09 jaseywang vps pptpd: /usr/sbin/pppd(fsm_input+0x671)[0x562eaeb669a1]
Jun 20 22:53:09 jaseywang vps pptpd: /usr/sbin/pppd(main+0xbe7)[0x562eaeb63317]
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(__libc_start_main+0xf5)[0x7fdba6328b35]
Jun 20 22:53:09 jaseywang vps pptpd: /usr/sbin/pppd(+0x1393d)[0x562eaeb6393d]
Jun 20 22:53:09 jaseywang vps pptpd: ======= Memory map: ========
Jun 20 22:53:09 jaseywang vps pptpd: 562eaeb50000-562eaeba6000 r-xp 00000000 fd:01 1510143                    /usr/sbin/pppd
Jun 20 22:53:09 jaseywang vps pptpd: 562eaeda6000-562eaeda7000 r–p 00056000 fd:01 1510143                    /usr/sbin/pppd
Jun 20 22:53:09 jaseywang vps pptpd: 562eaeda7000-562eaedad000 rw-p 00057000 fd:01 1510143                    /usr/sbin/pppd
Jun 20 22:53:09 jaseywang vps pptpd: 562eaedad000-562eaedf9000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 562eb07ef000-562eb0810000 rw-p 00000000 00:00 0                          [heap]
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba28a0000-7fdba28af000 r-xp 00000000 fd:01 1508183                    /usr/lib64/libbz2.so.1.0.6
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba28af000-7fdba2aae000 —p 0000f000 fd:01 1508183                    /usr/lib64/libbz2.so.1.0.6
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2aae000-7fdba2aaf000 r–p 0000e000 fd:01 1508183                    /usr/lib64/libbz2.so.1.0.6
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2aaf000-7fdba2ab0000 rw-p 0000f000 fd:01 1508183                    /usr/lib64/libbz2.so.1.0.6
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2ab0000-7fdba2ad5000 r-xp 00000000 fd:01 1507951                    /usr/lib64/liblzma.so.5.2.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2ad5000-7fdba2cd4000 —p 00025000 fd:01 1507951                    /usr/lib64/liblzma.so.5.2.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2cd4000-7fdba2cd5000 r–p 00024000 fd:01 1507951                    /usr/lib64/liblzma.so.5.2.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2cd5000-7fdba2cd6000 rw-p 00025000 fd:01 1507951                    /usr/lib64/liblzma.so.5.2.2
Jun 20 22:53:09 jaseywang vps charon: 05[KNL] interface ppp1 deactivated
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2cd6000-7fdba2ceb000 r-xp 00000000 fd:01 1508207                    /usr/lib64/libelf-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2ceb000-7fdba2eea000 —p 00015000 fd:01 1508207                    /usr/lib64/libelf-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2eea000-7fdba2eeb000 r–p 00014000 fd:01 1508207                    /usr/lib64/libelf-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2eeb000-7fdba2eec000 rw-p 00015000 fd:01 1508207                    /usr/lib64/libelf-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2eec000-7fdba2ef0000 r-xp 00000000 fd:01 1508224                    /usr/lib64/libattr.so.1.1.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2ef0000-7fdba30ef000 —p 00004000 fd:01 1508224                    /usr/lib64/libattr.so.1.1.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba30ef000-7fdba30f0000 r–p 00003000 fd:01 1508224                    /usr/lib64/libattr.so.1.1.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba30f0000-7fdba30f1000 rw-p 00004000 fd:01 1508224                    /usr/lib64/libattr.so.1.1.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba30f1000-7fdba3106000 r-xp 00000000 fd:01 1510624                    /usr/lib64/libgcc_s-4.8.5-20150702.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3106000-7fdba3305000 —p 00015000 fd:01 1510624                    /usr/lib64/libgcc_s-4.8.5-20150702.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3305000-7fdba3306000 r–p 00014000 fd:01 1510624                    /usr/lib64/libgcc_s-4.8.5-20150702.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3306000-7fdba3307000 rw-p 00015000 fd:01 1510624                    /usr/lib64/libgcc_s-4.8.5-20150702.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3307000-7fdba334c000 r-xp 00000000 fd:01 1508259                    /usr/lib64/libdw-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba334c000-7fdba354b000 —p 00045000 fd:01 1508259                    /usr/lib64/libdw-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba354b000-7fdba354d000 r–p 00044000 fd:01 1508259                    /usr/lib64/libdw-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba354d000-7fdba354e000 rw-p 00046000 fd:01 1508259                    /usr/lib64/libdw-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba354e000-7fdba3555000 r-xp 00000000 fd:01 1507938                    /usr/lib64/librt-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3555000-7fdba3754000 —p 00007000 fd:01 1507938                    /usr/lib64/librt-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3754000-7fdba3755000 r–p 00006000 fd:01 1507938                    /usr/lib64/librt-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3755000-7fdba3756000 rw-p 00007000 fd:01 1507938                    /usr/lib64/librt-2.17.so
Jun 20 22:53:09 jaseywang vps charon: 14[KNL] 10.8.0.1 disappeared from ppp1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3756000-7fdba3856000 r-xp 00000000 fd:01 1507916                    /usr/lib64/libm-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3856000-7fdba3a56000 —p 00100000 fd:01 1507916                    /usr/lib64/libm-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3a56000-7fdba3a57000 r–p 00100000 fd:01 1507916                    /usr/lib64/libm-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3a57000-7fdba3a58000 rw-p 00101000 fd:01 1507916                    /usr/lib64/libm-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3a58000-7fdba3a5c000 r-xp 00000000 fd:01 1508228                    /usr/lib64/libcap.so.2.22
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3a5c000-7fdba3c5b000 —p 00004000 fd:01 1508228                    /usr/lib64/libcap.so.2.22
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3c5b000-7fdba3c5c000 r–p 00003000 fd:01 1508228                    /usr/lib64/libcap.so.2.22
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3c5c000-7fdba3c5d000 rw-p 00004000 fd:01 1508228                    /usr/lib64/libcap.so.2.22
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3c5d000-7fdba3c62000 r-xp 00000000 fd:01 1507924                    /usr/lib64/libnss_dns-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3c62000-7fdba3e61000 —p 00005000 fd:01 1507924                    /usr/lib64/libnss_dns-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3e61000-7fdba3e62000 r–p 00004000 fd:01 1507924                    /usr/lib64/libnss_dns-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3e62000-7fdba3e63000 rw-p 00005000 fd:01 1507924                    /usr/lib64/libnss_dns-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3e63000-7fdba3e64000 r-xp 00000000 fd:01 1517634                    /usr/lib64/pptpd/pptpd-logwtmp.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3e64000-7fdba4063000 —p 00001000 fd:01 1517634                    /usr/lib64/pptpd/pptpd-logwtmp.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4063000-7fdba4064000 r–p 00000000 fd:01 1517634                    /usr/lib64/pptpd/pptpd-logwtmp.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4064000-7fdba4065000 rw-p 00001000 fd:01 1517634                    /usr/lib64/pptpd/pptpd-logwtmp.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4065000-7fdba4066000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4066000-7fdba4067000 r-xp 00000000 fd:01 1565616                    /usr/lib64/pppd/2.4.5/radattr.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4067000-7fdba4267000 —p 00001000 fd:01 1565616                    /usr/lib64/pppd/2.4.5/radattr.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4267000-7fdba4268000 r–p 00001000 fd:01 1565616                    /usr/lib64/pppd/2.4.5/radattr.so
Jun 20 22:53:09 jaseywang vps charon: 08[KNL] interface ppp1 deleted
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4268000-7fdba4269000 rw-p 00002000 fd:01 1565616                    /usr/lib64/pppd/2.4.5/radattr.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4269000-7fdba4275000 r-xp 00000000 fd:01 1565617                    /usr/lib64/pppd/2.4.5/radius.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4275000-7fdba4474000 —p 0000c000 fd:01 1565617                    /usr/lib64/pppd/2.4.5/radius.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4474000-7fdba4475000 r–p 0000b000 fd:01 1565617                    /usr/lib64/pppd/2.4.5/radius.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4475000-7fdba4476000 rw-p 0000c000 fd:01 1565617                    /usr/lib64/pppd/2.4.5/radius.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4476000-7fdba4478000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4478000-7fdba4484000 r-xp 00000000 fd:01 1507926                    /usr/lib64/libnss_files-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4484000-7fdba4683000 —p 0000c000 fd:01 1507926                    /usr/lib64/libnss_files-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4683000-7fdba4684000 r–p 0000b000 fd:01 1507926                    /usr/lib64/libnss_files-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4684000-7fdba4685000 rw-p 0000c000 fd:01 1507926                    /usr/lib64/libnss_files-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4685000-7fdba468b000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba468b000-7fdba46eb000 r-xp 00000000 fd:01 1508069                    /usr/lib64/libpcre.so.1.2.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba46eb000-7fdba48ea000 —p 00060000 fd:01 1508069                    /usr/lib64/libpcre.so.1.2.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba48ea000-7fdba48eb000 r–p 0005f000 fd:01 1508069                    /usr/lib64/libpcre.so.1.2.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba48eb000-7fdba48ec000 rw-p 00060000 fd:01 1508069                    /usr/lib64/libpcre.so.1.2.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba48ec000-7fdba4910000 r-xp 00000000 fd:01 1508068                    /usr/lib64/libselinux.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4910000-7fdba4b0f000 —p 00024000 fd:01 1508068                    /usr/lib64/libselinux.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4b0f000-7fdba4b10000 r–p 00023000 fd:01 1508068                    /usr/lib64/libselinux.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4b10000-7fdba4b11000 rw-p 00024000 fd:01 1508068                    /usr/lib64/libselinux.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4b11000-7fdba4b13000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4b13000-7fdba4b2a000 r-xp 00000000 fd:01 1507934                    /usr/lib64/libpthread-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4b2a000-7fdba4d29000 —p 00017000 fd:01 1507934                    /usr/lib64/libpthread-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4d29000-7fdba4d2a000 r–p 00016000 fd:01 1507934                    /usr/lib64/libpthread-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4d2a000-7fdba4d2b000 rw-p 00017000 fd:01 1507934                    /usr/lib64/libpthread-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4d2b000-7fdba4d2f000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4d2f000-7fdba4d45000 r-xp 00000000 fd:01 1507936                    /usr/lib64/libresolv-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4d45000-7fdba4f45000 —p 00016000 fd:01 1507936                    /usr/lib64/libresolv-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4f45000-7fdba4f46000 r–p 00016000 fd:01 1507936                    /usr/lib64/libresolv-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4f46000-7fdba4f47000 rw-p 00017000 fd:01 1507936                    /usr/lib64/libresolv-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4f47000-7fdba4f49000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4f49000-7fdba4f4c000 r-xp 00000000 fd:01 1508495                    /usr/lib64/libkeyutils.so.1.5
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4f4c000-7fdba514b000 —p 00003000 fd:01 1508495                    /usr/lib64/libkeyutils.so.1.5
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba514b000-7fdba514c000 r–p 00002000 fd:01 1508495                    /usr/lib64/libkeyutils.so.1.5
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba514c000-7fdba514d000 rw-p 00003000 fd:01 1508495                    /usr/lib64/libkeyutils.so.1.5
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba514d000-7fdba515a000 r-xp 00000000 fd:01 1508745                    /usr/lib64/libkrb5support.so.0.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba515a000-7fdba535a000 —p 0000d000 fd:01 1508745                    /usr/lib64/libkrb5support.so.0.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba535a000-7fdba535b000 r–p 0000d000 fd:01 1508745                    /usr/lib64/libkrb5support.so.0.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba535b000-7fdba535c000 rw-p 0000e000 fd:01 1508745                    /usr/lib64/libkrb5support.so.0.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba535c000-7fdba5377000 r-xp 00000000 fd:01 1508093                    /usr/lib64/libaudit.so.1.0.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5377000-7fdba5577000 —p 0001b000 fd:01 1508093                    /usr/lib64/libaudit.so.1.0.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5577000-7fdba5578000 r–p 0001b000 fd:01 1508093                    /usr/lib64/libaudit.so.1.0.0
Jun 20 22:53:09 jaseywang vps pppd[88951]: Exit.
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5578000-7fdba5579000 rw-p 0001c000 fd:01 1508093                    /usr/lib64/libaudit.so.1.0.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5579000-7fdba5583000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5583000-7fdba5585000 r-xp 00000000 fd:01 1507867                    /usr/lib64/libfreebl3.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5585000-7fdba5784000 —p 00002000 fd:01 1507867                    /usr/lib64/libfreebl3.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5784000-7fdba5785000 r–p 00001000 fd:01 1507867                    /usr/lib64/libfreebl3.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5785000-7fdba5786000 rw-p 00002000 fd:01 1507867                    /usr/lib64/libfreebl3.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5786000-7fdba579b000 r-xp 00000000 fd:01 1507927                    /usr/lib64/libz.so.1.2.7
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba579b000-7fdba599a000 —p 00015000 fd:01 1507927                    /usr/lib64/libz.so.1.2.7
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba599a000-7fdba599b000 r–p 00014000 fd:01 1507927                    /usr/lib64/libz.so.1.2.7
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba599b000-7fdba599c000 rw-p 00015000 fd:01 1507927                    /usr/lib64/libz.so.1.2.7
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba599c000-7fdba59cb000 r-xp 00000000 fd:01 1508733                    /usr/lib64/libk5crypto.so.3.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba59cb000-7fdba5bca000 —p 0002f000 fd:01 1508733                    /usr/lib64/libk5crypto.so.3.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5bca000-7fdba5bcc000 r–p 0002e000 fd:01 1508733                    /usr/lib64/libk5crypto.so.3.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5bcc000-7fdba5bcd000 rw-p 00030000 fd:01 1508733                    /usr/lib64/libk5crypto.so.3.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5bcd000-7fdba5bce000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5bce000-7fdba5bd1000 r-xp 00000000 fd:01 1508119                    /usr/lib64/libcom_err.so.2.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5bd1000-7fdba5dd0000 —p 00003000 fd:01 1508119                    /usr/lib64/libcom_err.so.2.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5dd0000-7fdba5dd1000 r–p 00002000 fd:01 1508119                    /usr/lib64/libcom_err.so.2.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5dd1000-7fdba5dd2000 rw-p 00003000 fd:01 1508119                    /usr/lib64/libcom_err.so.2.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5dd2000-7fdba5ea8000 r-xp 00000000 fd:01 1508743                    /usr/lib64/libkrb5.so.3.3
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5ea8000-7fdba60a8000 —p 000d6000 fd:01 1508743                    /usr/lib64/libkrb5.so.3.3
Jun 20 22:53:09 jaseywang vps pptpd[88950]: GRE: read(fd=6,buffer=55afa3981480,len=8196) from PTY failed: status = -1 error = Input/output error, usually caused by unexpected termination of pppd, check option syntax and pppd logs
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba60a8000-7fdba60b6000 r–p 000d6000 fd:01 1508743                    /usr/lib64/libkrb5.so.3.3
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba60b6000-7fdba60b9000 rw-p 000e4000 fd:01 1508743                    /usr/lib64/libkrb5.so.3.3
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba60b9000-7fdba6104000 r-xp 00000000 fd:01 1508729                    /usr/lib64/libgssapi_krb5.so.2.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6104000-7fdba6304000 —p 0004b000 fd:01 1508729                    /usr/lib64/libgssapi_krb5.so.2.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6304000-7fdba6305000 r–p 0004b000 fd:01 1508729                    /usr/lib64/libgssapi_krb5.so.2.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6305000-7fdba6307000 rw-p 0004c000 fd:01 1508729                    /usr/lib64/libgssapi_krb5.so.2.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6307000-7fdba64bd000 r-xp 00000000 fd:01 1507908                    /usr/lib64/libc-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba64bd000-7fdba66bd000 —p 001b6000 fd:01 1507908                    /usr/lib64/libc-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba66bd000-7fdba66c1000 r–p 001b6000 fd:01 1507908                    /usr/lib64/libc-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba66c1000-7fdba66c3000 rw-p 001ba000 fd:01 1507908                    /usr/lib64/libc-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba66c3000-7fdba66c8000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba66c8000-7fdba6706000 r-xp 00000000 fd:01 1508723                    /usr/lib64/libpcap.so.1.5.3
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6706000-7fdba6905000 —p 0003e000 fd:01 1508723                    /usr/lib64/libpcap.so.1.5.3
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6905000-7fdba6907000 r–p 0003d000 fd:01 1508723                    /usr/lib64/libpcap.so.1.5.3
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6907000-7fdba6908000 rw-p 0003f000 fd:01 1508723                    /usr/lib64/libpcap.so.1.5.3
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6908000-7fdba6909000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6909000-7fdba690b000 r-xp 00000000 fd:01 1507914                    /usr/lib64/libdl-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba690b000-7fdba6b0b000 —p 00002000 fd:01 1507914                    /usr/lib64/libdl-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6b0b000-7fdba6b0c000 r–p 00002000 fd:01 1507914                    /usr/lib64/libdl-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6b0c000-7fdba6b0d000 rw-p 00003000 fd:01 1507914                    /usr/lib64/libdl-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6b0d000-7fdba6b1a000 r-xp 00000000 fd:01 1508738                    /usr/lib64/libpam.so.0.83.1
Jun 20 22:53:09 jaseywang vps pptpd[88950]: CTRL: PTY read or GRE write failed (pty,gre)=(6,7)
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6b1a000-7fdba6d1a000 —p 0000d000 fd:01 1508738                    /usr/lib64/libpam.so.0.83.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6d1a000-7fdba6d1b000 r–p 0000d000 fd:01 1508738                    /usr/lib64/libpam.so.0.83.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6d1b000-7fdba6d1c000 rw-p 0000e000 fd:01 1508738                    /usr/lib64/libpam.so.0.83.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6d1c000-7fdba6d24000 r-xp 00000000 fd:01 1507912                    /usr/lib64/libcrypt-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6d24000-7fdba6f23000 —p 00008000 fd:01 1507912                    /usr/lib64/libcrypt-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6f23000-7fdba6f24000 r–p 00007000 fd:01 1507912                    /usr/lib64/libcrypt-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6f24000-7fdba6f25000 rw-p 00008000 fd:01 1507912                    /usr/lib64/libcrypt-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6f25000-7fdba6f53000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6f53000-7fdba7113000 r-xp 00000000 fd:01 1508751                    /usr/lib64/libcrypto.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba7113000-7fdba7313000 —p 001c0000 fd:01 1508751                    /usr/lib64/libcrypto.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba7313000-7fdba732d000 r–p 001c0000 fd:01 1508751                    /usr/lib64/libcrypto.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba732d000-7fdba7339000 rw-p 001da000 fd:01 1508751                    /usr/lib64/libcrypto.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba7339000-7fdba733d000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba733d000-7fdba73a1000 r-xp 00000000 fd:01 1508753                    /usr/lib64/libssl.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba73a1000-7fdba75a0000 —p 00064000 fd:01 1508753                    /usr/lib64/libssl.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba75a0000-7fdba75a4000 r–p 00063000 fd:01 1508753                    /usr/lib64/libssl.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba75a4000-7fdba75ab000 rw-p 00067000 fd:01 1508753                    /usr/lib64/libssl.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba75ab000-7fdba75ad000 r-xp 00000000 fd:01 1507942                    /usr/lib64/libutil-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba75ad000-7fdba77ac000 —p 00002000 fd:01 1507942                    /usr/lib64/libutil-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba77ac000-7fdba77ad000 r–p 00001000 fd:01 1507942                    /usr/lib64/libutil-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba77ad000-7fdba77ae000 rw-p 00002000 fd:01 1507942                    /usr/lib64/libutil-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba77ae000-7fdba77ce000 r-xp 00000000 fd:01 1507901                    /usr/lib64/ld-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba7998000-7fdba79a5000 r-xp 00000000 fd:01 1512314                    /usr/lib64/libnss_myhostname.so.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79a5000-7fdba79a8000 r–p 0000c000 fd:01 1512314                    /usr/lib64/libnss_myhostname.so.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79a8000-7fdba79a9000 rw-p 0000f000 fd:01 1512314                    /usr/lib64/libnss_myhostname.so.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79b5000-7fdba79c0000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79c9000-7fdba79ca000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79ca000-7fdba79cc000 rw-s 00000000 00:16 19496                      /run/ppp/pppd2.tdb
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79cc000-7fdba79cd000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79cd000-7fdba79ce000 r–p 0001f000 fd:01 1507901                    /usr/lib64/ld-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79ce000-7fdba79cf000 rw-p 00020000 fd:01 1507901                    /usr/lib64/ld-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79cf000-7fdba79d0000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7ffc6d3db000-7ffc6d3fc000 rw-p 00000000 00:00 0                          [stack]
Jun 20 22:53:09 jaseywang vps pptpd: 7ffc6d3fc000-7ffc6d3fe000 r–p 00000000 00:00 0                          [vvar]
Jun 20 22:53:09 jaseywang vps pptpd: 7ffc6d3fe000-7ffc6d400000 r-xp 00000000 00:00 0                          [vdso]
Jun 20 22:53:09 jaseywang vps pptpd: ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
Jun 20 22:53:09 jaseywang vps pptpd[88950]: CTRL: Client 117.73.146.179 control connection finished

现象就是 pptp VPN 连接失败,每次连接都会报上面的错误,寻着关键词找到了这个 15 年发现的 bug:
https://nvd.nist.gov/vuln/detail/CVE-2015-3310

简而言之就是 2.4.6 版本之前的 ppp 进程每次 fork 的时候产生 PID 的时候,一旦大于 65535,plugins/radius/util.c 里面的函数 rc_mksid 就 overflow 了,只能重启机器解决。最坑的是,Ubuntu/Debain/OpenSuse 都更新了相应的补丁,但是 RedHat 没有,没有,并且对于 5/6/7 三个大版本都不会修复

这么大的影响,尤其对于一台生产的服务器,pid_max 设置超过 65535 是件很正常的事情,不知道 Redhat 是怎么考虑这件事情的。
临时的解决办法要将 pid_max 设置成 65535 以下,要么定期设置 /proc/sys/kernel/ns_last_pid 的值,指定接下来新生成的 PID 获得比较低的值,后者在 3.3 以上的内核才支持。

]]>
最近手贱想把手头的几台玩具机器统一下标准,其中一个标准是将 kernel.pid_max 增加到了 512000,结果就在当天的凌晨,一台跑着 pptp 的 VPS 崩溃了:

Jun 20 22:53:09 jaseywang vps pptpd: ======= Backtrace: =========
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(__fortify_fail+0x37)[0x7fdba6416047]
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(+0x10d200)[0x7fdba6414200]
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(+0x10c709)[0x7fdba6413709]
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(_IO_default_xsputn+0xbc)[0x7fdba637f60c]
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(_IO_vfprintf+0xb0d)[0x7fdba634ec3d]
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(__vsprintf_chk+0x88)[0x7fdba6413798]
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(__sprintf_chk+0x7d)[0x7fdba64136ed]
Jun 20 22:53:09 jaseywang vps pptpd: /usr/lib/../lib64/pppd/2.4.5/radius.so(rc_mksid+0x43)[0x7fdba4272763]
Jun 20 22:53:09 jaseywang vps pptpd: /usr/lib/../lib64/pppd/2.4.5/radius.so(+0x48f0)[0x7fdba426d8f0]
Jun 20 22:53:09 jaseywang vps pptpd: /usr/sbin/pppd(notify+0x27)[0x562eaeb64847]
Jun 20 22:53:09 jaseywang vps pptpd: /usr/sbin/pppd(+0x1cfff)[0x562eaeb6cfff]
Jun 20 22:53:09 jaseywang vps pptpd: /usr/sbin/pppd(fsm_input+0x671)[0x562eaeb669a1]
Jun 20 22:53:09 jaseywang vps pptpd: /usr/sbin/pppd(main+0xbe7)[0x562eaeb63317]
Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(__libc_start_main+0xf5)[0x7fdba6328b35]
Jun 20 22:53:09 jaseywang vps pptpd: /usr/sbin/pppd(+0x1393d)[0x562eaeb6393d]
Jun 20 22:53:09 jaseywang vps pptpd: ======= Memory map: ========
Jun 20 22:53:09 jaseywang vps pptpd: 562eaeb50000-562eaeba6000 r-xp 00000000 fd:01 1510143                    /usr/sbin/pppd
Jun 20 22:53:09 jaseywang vps pptpd: 562eaeda6000-562eaeda7000 r–p 00056000 fd:01 1510143                    /usr/sbin/pppd
Jun 20 22:53:09 jaseywang vps pptpd: 562eaeda7000-562eaedad000 rw-p 00057000 fd:01 1510143                    /usr/sbin/pppd
Jun 20 22:53:09 jaseywang vps pptpd: 562eaedad000-562eaedf9000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 562eb07ef000-562eb0810000 rw-p 00000000 00:00 0                          [heap]
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba28a0000-7fdba28af000 r-xp 00000000 fd:01 1508183                    /usr/lib64/libbz2.so.1.0.6
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba28af000-7fdba2aae000 —p 0000f000 fd:01 1508183                    /usr/lib64/libbz2.so.1.0.6
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2aae000-7fdba2aaf000 r–p 0000e000 fd:01 1508183                    /usr/lib64/libbz2.so.1.0.6
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2aaf000-7fdba2ab0000 rw-p 0000f000 fd:01 1508183                    /usr/lib64/libbz2.so.1.0.6
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2ab0000-7fdba2ad5000 r-xp 00000000 fd:01 1507951                    /usr/lib64/liblzma.so.5.2.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2ad5000-7fdba2cd4000 —p 00025000 fd:01 1507951                    /usr/lib64/liblzma.so.5.2.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2cd4000-7fdba2cd5000 r–p 00024000 fd:01 1507951                    /usr/lib64/liblzma.so.5.2.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2cd5000-7fdba2cd6000 rw-p 00025000 fd:01 1507951                    /usr/lib64/liblzma.so.5.2.2
Jun 20 22:53:09 jaseywang vps charon: 05[KNL] interface ppp1 deactivated
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2cd6000-7fdba2ceb000 r-xp 00000000 fd:01 1508207                    /usr/lib64/libelf-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2ceb000-7fdba2eea000 —p 00015000 fd:01 1508207                    /usr/lib64/libelf-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2eea000-7fdba2eeb000 r–p 00014000 fd:01 1508207                    /usr/lib64/libelf-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2eeb000-7fdba2eec000 rw-p 00015000 fd:01 1508207                    /usr/lib64/libelf-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2eec000-7fdba2ef0000 r-xp 00000000 fd:01 1508224                    /usr/lib64/libattr.so.1.1.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba2ef0000-7fdba30ef000 —p 00004000 fd:01 1508224                    /usr/lib64/libattr.so.1.1.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba30ef000-7fdba30f0000 r–p 00003000 fd:01 1508224                    /usr/lib64/libattr.so.1.1.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba30f0000-7fdba30f1000 rw-p 00004000 fd:01 1508224                    /usr/lib64/libattr.so.1.1.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba30f1000-7fdba3106000 r-xp 00000000 fd:01 1510624                    /usr/lib64/libgcc_s-4.8.5-20150702.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3106000-7fdba3305000 —p 00015000 fd:01 1510624                    /usr/lib64/libgcc_s-4.8.5-20150702.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3305000-7fdba3306000 r–p 00014000 fd:01 1510624                    /usr/lib64/libgcc_s-4.8.5-20150702.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3306000-7fdba3307000 rw-p 00015000 fd:01 1510624                    /usr/lib64/libgcc_s-4.8.5-20150702.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3307000-7fdba334c000 r-xp 00000000 fd:01 1508259                    /usr/lib64/libdw-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba334c000-7fdba354b000 —p 00045000 fd:01 1508259                    /usr/lib64/libdw-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba354b000-7fdba354d000 r–p 00044000 fd:01 1508259                    /usr/lib64/libdw-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba354d000-7fdba354e000 rw-p 00046000 fd:01 1508259                    /usr/lib64/libdw-0.163.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba354e000-7fdba3555000 r-xp 00000000 fd:01 1507938                    /usr/lib64/librt-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3555000-7fdba3754000 —p 00007000 fd:01 1507938                    /usr/lib64/librt-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3754000-7fdba3755000 r–p 00006000 fd:01 1507938                    /usr/lib64/librt-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3755000-7fdba3756000 rw-p 00007000 fd:01 1507938                    /usr/lib64/librt-2.17.so
Jun 20 22:53:09 jaseywang vps charon: 14[KNL] 10.8.0.1 disappeared from ppp1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3756000-7fdba3856000 r-xp 00000000 fd:01 1507916                    /usr/lib64/libm-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3856000-7fdba3a56000 —p 00100000 fd:01 1507916                    /usr/lib64/libm-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3a56000-7fdba3a57000 r–p 00100000 fd:01 1507916                    /usr/lib64/libm-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3a57000-7fdba3a58000 rw-p 00101000 fd:01 1507916                    /usr/lib64/libm-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3a58000-7fdba3a5c000 r-xp 00000000 fd:01 1508228                    /usr/lib64/libcap.so.2.22
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3a5c000-7fdba3c5b000 —p 00004000 fd:01 1508228                    /usr/lib64/libcap.so.2.22
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3c5b000-7fdba3c5c000 r–p 00003000 fd:01 1508228                    /usr/lib64/libcap.so.2.22
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3c5c000-7fdba3c5d000 rw-p 00004000 fd:01 1508228                    /usr/lib64/libcap.so.2.22
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3c5d000-7fdba3c62000 r-xp 00000000 fd:01 1507924                    /usr/lib64/libnss_dns-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3c62000-7fdba3e61000 —p 00005000 fd:01 1507924                    /usr/lib64/libnss_dns-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3e61000-7fdba3e62000 r–p 00004000 fd:01 1507924                    /usr/lib64/libnss_dns-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3e62000-7fdba3e63000 rw-p 00005000 fd:01 1507924                    /usr/lib64/libnss_dns-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3e63000-7fdba3e64000 r-xp 00000000 fd:01 1517634                    /usr/lib64/pptpd/pptpd-logwtmp.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba3e64000-7fdba4063000 —p 00001000 fd:01 1517634                    /usr/lib64/pptpd/pptpd-logwtmp.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4063000-7fdba4064000 r–p 00000000 fd:01 1517634                    /usr/lib64/pptpd/pptpd-logwtmp.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4064000-7fdba4065000 rw-p 00001000 fd:01 1517634                    /usr/lib64/pptpd/pptpd-logwtmp.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4065000-7fdba4066000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4066000-7fdba4067000 r-xp 00000000 fd:01 1565616                    /usr/lib64/pppd/2.4.5/radattr.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4067000-7fdba4267000 —p 00001000 fd:01 1565616                    /usr/lib64/pppd/2.4.5/radattr.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4267000-7fdba4268000 r–p 00001000 fd:01 1565616                    /usr/lib64/pppd/2.4.5/radattr.so
Jun 20 22:53:09 jaseywang vps charon: 08[KNL] interface ppp1 deleted
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4268000-7fdba4269000 rw-p 00002000 fd:01 1565616                    /usr/lib64/pppd/2.4.5/radattr.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4269000-7fdba4275000 r-xp 00000000 fd:01 1565617                    /usr/lib64/pppd/2.4.5/radius.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4275000-7fdba4474000 —p 0000c000 fd:01 1565617                    /usr/lib64/pppd/2.4.5/radius.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4474000-7fdba4475000 r–p 0000b000 fd:01 1565617                    /usr/lib64/pppd/2.4.5/radius.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4475000-7fdba4476000 rw-p 0000c000 fd:01 1565617                    /usr/lib64/pppd/2.4.5/radius.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4476000-7fdba4478000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4478000-7fdba4484000 r-xp 00000000 fd:01 1507926                    /usr/lib64/libnss_files-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4484000-7fdba4683000 —p 0000c000 fd:01 1507926                    /usr/lib64/libnss_files-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4683000-7fdba4684000 r–p 0000b000 fd:01 1507926                    /usr/lib64/libnss_files-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4684000-7fdba4685000 rw-p 0000c000 fd:01 1507926                    /usr/lib64/libnss_files-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4685000-7fdba468b000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba468b000-7fdba46eb000 r-xp 00000000 fd:01 1508069                    /usr/lib64/libpcre.so.1.2.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba46eb000-7fdba48ea000 —p 00060000 fd:01 1508069                    /usr/lib64/libpcre.so.1.2.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba48ea000-7fdba48eb000 r–p 0005f000 fd:01 1508069                    /usr/lib64/libpcre.so.1.2.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba48eb000-7fdba48ec000 rw-p 00060000 fd:01 1508069                    /usr/lib64/libpcre.so.1.2.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba48ec000-7fdba4910000 r-xp 00000000 fd:01 1508068                    /usr/lib64/libselinux.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4910000-7fdba4b0f000 —p 00024000 fd:01 1508068                    /usr/lib64/libselinux.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4b0f000-7fdba4b10000 r–p 00023000 fd:01 1508068                    /usr/lib64/libselinux.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4b10000-7fdba4b11000 rw-p 00024000 fd:01 1508068                    /usr/lib64/libselinux.so.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4b11000-7fdba4b13000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4b13000-7fdba4b2a000 r-xp 00000000 fd:01 1507934                    /usr/lib64/libpthread-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4b2a000-7fdba4d29000 —p 00017000 fd:01 1507934                    /usr/lib64/libpthread-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4d29000-7fdba4d2a000 r–p 00016000 fd:01 1507934                    /usr/lib64/libpthread-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4d2a000-7fdba4d2b000 rw-p 00017000 fd:01 1507934                    /usr/lib64/libpthread-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4d2b000-7fdba4d2f000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4d2f000-7fdba4d45000 r-xp 00000000 fd:01 1507936                    /usr/lib64/libresolv-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4d45000-7fdba4f45000 —p 00016000 fd:01 1507936                    /usr/lib64/libresolv-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4f45000-7fdba4f46000 r–p 00016000 fd:01 1507936                    /usr/lib64/libresolv-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4f46000-7fdba4f47000 rw-p 00017000 fd:01 1507936                    /usr/lib64/libresolv-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4f47000-7fdba4f49000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4f49000-7fdba4f4c000 r-xp 00000000 fd:01 1508495                    /usr/lib64/libkeyutils.so.1.5
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba4f4c000-7fdba514b000 —p 00003000 fd:01 1508495                    /usr/lib64/libkeyutils.so.1.5
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba514b000-7fdba514c000 r–p 00002000 fd:01 1508495                    /usr/lib64/libkeyutils.so.1.5
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba514c000-7fdba514d000 rw-p 00003000 fd:01 1508495                    /usr/lib64/libkeyutils.so.1.5
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba514d000-7fdba515a000 r-xp 00000000 fd:01 1508745                    /usr/lib64/libkrb5support.so.0.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba515a000-7fdba535a000 —p 0000d000 fd:01 1508745                    /usr/lib64/libkrb5support.so.0.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba535a000-7fdba535b000 r–p 0000d000 fd:01 1508745                    /usr/lib64/libkrb5support.so.0.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba535b000-7fdba535c000 rw-p 0000e000 fd:01 1508745                    /usr/lib64/libkrb5support.so.0.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba535c000-7fdba5377000 r-xp 00000000 fd:01 1508093                    /usr/lib64/libaudit.so.1.0.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5377000-7fdba5577000 —p 0001b000 fd:01 1508093                    /usr/lib64/libaudit.so.1.0.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5577000-7fdba5578000 r–p 0001b000 fd:01 1508093                    /usr/lib64/libaudit.so.1.0.0
Jun 20 22:53:09 jaseywang vps pppd[88951]: Exit.
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5578000-7fdba5579000 rw-p 0001c000 fd:01 1508093                    /usr/lib64/libaudit.so.1.0.0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5579000-7fdba5583000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5583000-7fdba5585000 r-xp 00000000 fd:01 1507867                    /usr/lib64/libfreebl3.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5585000-7fdba5784000 —p 00002000 fd:01 1507867                    /usr/lib64/libfreebl3.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5784000-7fdba5785000 r–p 00001000 fd:01 1507867                    /usr/lib64/libfreebl3.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5785000-7fdba5786000 rw-p 00002000 fd:01 1507867                    /usr/lib64/libfreebl3.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5786000-7fdba579b000 r-xp 00000000 fd:01 1507927                    /usr/lib64/libz.so.1.2.7
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba579b000-7fdba599a000 —p 00015000 fd:01 1507927                    /usr/lib64/libz.so.1.2.7
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba599a000-7fdba599b000 r–p 00014000 fd:01 1507927                    /usr/lib64/libz.so.1.2.7
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba599b000-7fdba599c000 rw-p 00015000 fd:01 1507927                    /usr/lib64/libz.so.1.2.7
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba599c000-7fdba59cb000 r-xp 00000000 fd:01 1508733                    /usr/lib64/libk5crypto.so.3.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba59cb000-7fdba5bca000 —p 0002f000 fd:01 1508733                    /usr/lib64/libk5crypto.so.3.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5bca000-7fdba5bcc000 r–p 0002e000 fd:01 1508733                    /usr/lib64/libk5crypto.so.3.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5bcc000-7fdba5bcd000 rw-p 00030000 fd:01 1508733                    /usr/lib64/libk5crypto.so.3.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5bcd000-7fdba5bce000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5bce000-7fdba5bd1000 r-xp 00000000 fd:01 1508119                    /usr/lib64/libcom_err.so.2.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5bd1000-7fdba5dd0000 —p 00003000 fd:01 1508119                    /usr/lib64/libcom_err.so.2.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5dd0000-7fdba5dd1000 r–p 00002000 fd:01 1508119                    /usr/lib64/libcom_err.so.2.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5dd1000-7fdba5dd2000 rw-p 00003000 fd:01 1508119                    /usr/lib64/libcom_err.so.2.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5dd2000-7fdba5ea8000 r-xp 00000000 fd:01 1508743                    /usr/lib64/libkrb5.so.3.3
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba5ea8000-7fdba60a8000 —p 000d6000 fd:01 1508743                    /usr/lib64/libkrb5.so.3.3
Jun 20 22:53:09 jaseywang vps pptpd[88950]: GRE: read(fd=6,buffer=55afa3981480,len=8196) from PTY failed: status = -1 error = Input/output error, usually caused by unexpected termination of pppd, check option syntax and pppd logs
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba60a8000-7fdba60b6000 r–p 000d6000 fd:01 1508743                    /usr/lib64/libkrb5.so.3.3
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba60b6000-7fdba60b9000 rw-p 000e4000 fd:01 1508743                    /usr/lib64/libkrb5.so.3.3
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba60b9000-7fdba6104000 r-xp 00000000 fd:01 1508729                    /usr/lib64/libgssapi_krb5.so.2.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6104000-7fdba6304000 —p 0004b000 fd:01 1508729                    /usr/lib64/libgssapi_krb5.so.2.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6304000-7fdba6305000 r–p 0004b000 fd:01 1508729                    /usr/lib64/libgssapi_krb5.so.2.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6305000-7fdba6307000 rw-p 0004c000 fd:01 1508729                    /usr/lib64/libgssapi_krb5.so.2.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6307000-7fdba64bd000 r-xp 00000000 fd:01 1507908                    /usr/lib64/libc-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba64bd000-7fdba66bd000 —p 001b6000 fd:01 1507908                    /usr/lib64/libc-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba66bd000-7fdba66c1000 r–p 001b6000 fd:01 1507908                    /usr/lib64/libc-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba66c1000-7fdba66c3000 rw-p 001ba000 fd:01 1507908                    /usr/lib64/libc-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba66c3000-7fdba66c8000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba66c8000-7fdba6706000 r-xp 00000000 fd:01 1508723                    /usr/lib64/libpcap.so.1.5.3
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6706000-7fdba6905000 —p 0003e000 fd:01 1508723                    /usr/lib64/libpcap.so.1.5.3
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6905000-7fdba6907000 r–p 0003d000 fd:01 1508723                    /usr/lib64/libpcap.so.1.5.3
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6907000-7fdba6908000 rw-p 0003f000 fd:01 1508723                    /usr/lib64/libpcap.so.1.5.3
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6908000-7fdba6909000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6909000-7fdba690b000 r-xp 00000000 fd:01 1507914                    /usr/lib64/libdl-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba690b000-7fdba6b0b000 —p 00002000 fd:01 1507914                    /usr/lib64/libdl-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6b0b000-7fdba6b0c000 r–p 00002000 fd:01 1507914                    /usr/lib64/libdl-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6b0c000-7fdba6b0d000 rw-p 00003000 fd:01 1507914                    /usr/lib64/libdl-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6b0d000-7fdba6b1a000 r-xp 00000000 fd:01 1508738                    /usr/lib64/libpam.so.0.83.1
Jun 20 22:53:09 jaseywang vps pptpd[88950]: CTRL: PTY read or GRE write failed (pty,gre)=(6,7)
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6b1a000-7fdba6d1a000 —p 0000d000 fd:01 1508738                    /usr/lib64/libpam.so.0.83.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6d1a000-7fdba6d1b000 r–p 0000d000 fd:01 1508738                    /usr/lib64/libpam.so.0.83.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6d1b000-7fdba6d1c000 rw-p 0000e000 fd:01 1508738                    /usr/lib64/libpam.so.0.83.1
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6d1c000-7fdba6d24000 r-xp 00000000 fd:01 1507912                    /usr/lib64/libcrypt-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6d24000-7fdba6f23000 —p 00008000 fd:01 1507912                    /usr/lib64/libcrypt-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6f23000-7fdba6f24000 r–p 00007000 fd:01 1507912                    /usr/lib64/libcrypt-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6f24000-7fdba6f25000 rw-p 00008000 fd:01 1507912                    /usr/lib64/libcrypt-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6f25000-7fdba6f53000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba6f53000-7fdba7113000 r-xp 00000000 fd:01 1508751                    /usr/lib64/libcrypto.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba7113000-7fdba7313000 —p 001c0000 fd:01 1508751                    /usr/lib64/libcrypto.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba7313000-7fdba732d000 r–p 001c0000 fd:01 1508751                    /usr/lib64/libcrypto.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba732d000-7fdba7339000 rw-p 001da000 fd:01 1508751                    /usr/lib64/libcrypto.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba7339000-7fdba733d000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba733d000-7fdba73a1000 r-xp 00000000 fd:01 1508753                    /usr/lib64/libssl.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba73a1000-7fdba75a0000 —p 00064000 fd:01 1508753                    /usr/lib64/libssl.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba75a0000-7fdba75a4000 r–p 00063000 fd:01 1508753                    /usr/lib64/libssl.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba75a4000-7fdba75ab000 rw-p 00067000 fd:01 1508753                    /usr/lib64/libssl.so.1.0.1e
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba75ab000-7fdba75ad000 r-xp 00000000 fd:01 1507942                    /usr/lib64/libutil-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba75ad000-7fdba77ac000 —p 00002000 fd:01 1507942                    /usr/lib64/libutil-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba77ac000-7fdba77ad000 r–p 00001000 fd:01 1507942                    /usr/lib64/libutil-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba77ad000-7fdba77ae000 rw-p 00002000 fd:01 1507942                    /usr/lib64/libutil-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba77ae000-7fdba77ce000 r-xp 00000000 fd:01 1507901                    /usr/lib64/ld-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba7998000-7fdba79a5000 r-xp 00000000 fd:01 1512314                    /usr/lib64/libnss_myhostname.so.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79a5000-7fdba79a8000 r–p 0000c000 fd:01 1512314                    /usr/lib64/libnss_myhostname.so.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79a8000-7fdba79a9000 rw-p 0000f000 fd:01 1512314                    /usr/lib64/libnss_myhostname.so.2
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79b5000-7fdba79c0000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79c9000-7fdba79ca000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79ca000-7fdba79cc000 rw-s 00000000 00:16 19496                      /run/ppp/pppd2.tdb
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79cc000-7fdba79cd000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79cd000-7fdba79ce000 r–p 0001f000 fd:01 1507901                    /usr/lib64/ld-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79ce000-7fdba79cf000 rw-p 00020000 fd:01 1507901                    /usr/lib64/ld-2.17.so
Jun 20 22:53:09 jaseywang vps pptpd: 7fdba79cf000-7fdba79d0000 rw-p 00000000 00:00 0
Jun 20 22:53:09 jaseywang vps pptpd: 7ffc6d3db000-7ffc6d3fc000 rw-p 00000000 00:00 0                          [stack]
Jun 20 22:53:09 jaseywang vps pptpd: 7ffc6d3fc000-7ffc6d3fe000 r–p 00000000 00:00 0                          [vvar]
Jun 20 22:53:09 jaseywang vps pptpd: 7ffc6d3fe000-7ffc6d400000 r-xp 00000000 00:00 0                          [vdso]
Jun 20 22:53:09 jaseywang vps pptpd: ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
Jun 20 22:53:09 jaseywang vps pptpd[88950]: CTRL: Client 117.73.146.179 control connection finished

现象就是 pptp VPN 连接失败,每次连接都会报上面的错误,寻着关键词找到了这个 15 年发现的 bug:
https://nvd.nist.gov/vuln/detail/CVE-2015-3310

简而言之就是 2.4.6 版本之前的 ppp 进程每次 fork 的时候产生 PID 的时候,一旦大于 65535,plugins/radius/util.c 里面的函数 rc_mksid 就 overflow 了,只能重启机器解决。最坑的是,Ubuntu/Debain/OpenSuse 都更新了相应的补丁,但是 RedHat 没有,没有,并且对于 5/6/7 三个大版本都不会修复

这么大的影响,尤其对于一台生产的服务器,pid_max 设置超过 65535 是件很正常的事情,不知道 Redhat 是怎么考虑这件事情的。
临时的解决办法要将 pid_max 设置成 65535 以下,要么定期设置 /proc/sys/kernel/ns_last_pid 的值,指定接下来新生成的 PID 获得比较低的值,后者在 3.3 以上的内核才支持。

]]>
0
<![CDATA[[转]Golang 中使用 JSON 的小技巧]]> http://www.udpwork.com/item/16317.html http://www.udpwork.com/item/16317.html#reviews Thu, 22 Jun 2017 17:55:11 +0800 鸟窝 http://www.udpwork.com/item/16317.html taowenjson-iterator的作者。 序列化和反序列化需要处理JSON和struct的关系,其中会用到一些技巧。 原文Golang 中使用 JSON 的小技巧是他的经验之谈,介绍了一些struct解析成json的技巧,以及 json-iterator 库的一些便利的处理。

有的时候上游传过来的字段是string类型的,但是我们却想用变成数字来使用。 本来用一个json:",string" 就可以支持了,如果不知道golang的这些小技巧,就要大费周章了。

参考文章:http://attilaolah.eu/2014/09/10/json-and-struct-composition-in-go/

临时忽略struct空字段

12345
type User struct {    Email    string `json:"email"`    Password string `json:"password"`    // many more fields…}

如果想临时忽略掉空Password字段,可以用omitempty:

123456
json.Marshal(struct {    *User    Password bool `json:"password,omitempty"`}{    User: user,})

临时添加额外的字段

12345
type User struct {    Email    string `json:"email"`    Password string `json:"password"`    // many more fields…}

临时忽略掉空Password字段,并且添加token字段

12345678
json.Marshal(struct {    *User    Token    string `json:"token"`    Password bool `json:"password,omitempty"`}{    User: user,    Token: token,})

临时粘合两个struct

通过嵌入struct的方式:

1234567891011121314
type BlogPost struct {    URL   string `json:"url"`    Title string `json:"title"`}type Analytics struct {    Visitors  int `json:"visitors"`    PageViews int `json:"page_views"`}json.Marshal(struct{    *BlogPost    *Analytics}{post, analytics})

一个json切分成两个struct

123456789
json.Unmarshal([]byte(`{  "url": "attila@attilaolah.eu",  "title": "Attila's Blog",  "visitors": 6,  "page_views": 14}`), &struct {  *BlogPost  *Analytics}{&post, &analytics})

临时改名struct的字段

12345678910111213141516171819202122232425
type CacheItem struct {    Key    string `json:"key"`    MaxAge int    `json:"cacheAge"`    Value  Value  `json:"cacheValue"`}json.Marshal(struct{    *CacheItem    // Omit bad keys    OmitMaxAge omit `json:"cacheAge,omitempty"`    OmitValue  omit `json:"cacheValue,omitempty"`    // Add nice keys    MaxAge int    `json:"max_age"`    Value  *Value `json:"value"`}{    CacheItem: item,    // Set the int by value:    MaxAge: item.MaxAge,    // Set the nested struct by reference, avoid making a copy:    Value: &item.Value,})

用字符串传递数字

123
type TestObject struct {    Field1 int    `json:",string"`}

这个对应的json是{"Field1": "100"}

如果json是{"Field1": 100}则会报错

容忍字符串和数字互转

如果你使用的是jsoniter,可以启动模糊模式 来支持 PHP 传递过来的 JSON。

123
import "github.com/json-iterator/go/extra"extra.RegisterFuzzyDecoders()

这样就可以处理字符串和数字类型不对的问题了。比如

12
var val stringjsoniter.UnmarshalFromString(`100`, &val)

又比如

12
var val float32jsoniter.UnmarshalFromString(`"1.23"`, &val)

容忍空数组作为对象

PHP另外一个令人崩溃的地方是,如果 PHP array是空的时候,序列化出来是[]。但是不为空的时候,序列化出来的是{"key":"value"}。 我们需要把[]当成{}处理。

如果你使用的是jsoniter,可以启动模糊模式来支持 PHP 传递过来的 JSON。

123
import "github.com/json-iterator/go/extra"extra.RegisterFuzzyDecoders()

这样就可以支持了

12
var val map[string]interface{}jsoniter.UnmarshalFromString(`[]`, &val)

使用 MarshalJSON支持time.Time

golang 默认会把time.Time用字符串方式序列化。如果我们想用其他方式表示time.Time,需要自定义类型并定义MarshalJSON。

123456
type timeImplementedMarshaler time.Timefunc (obj timeImplementedMarshaler) MarshalJSON() ([]byte, error) {    seconds := time.Time(obj).Unix()    return []byte(strconv.FormatInt(seconds, 10)), nil}

序列化的时候会调用 MarshalJSON

123456789
type TestObject struct {    Field timeImplementedMarshaler}should := require.New(t)val := timeImplementedMarshaler(time.Unix(123, 0))obj := TestObject{val}bytes, err := jsoniter.Marshal(obj)should.Nil(err)should.Equal(`{"Field":123}`, string(bytes))

使用 RegisterTypeEncoder支持time.Time

jsoniter 能够对不是你定义的type自定义JSON编解码方式。比如对于time.Time可以用 epoch int64 来序列化

12345
import "github.com/json-iterator/go/extra"extra.RegisterTimeAsInt64Codec(time.Microsecond)output, err := jsoniter.Marshal(time.Unix(1, 1002))should.Equal("1000001", string(output))

如果要自定义的话,参见RegisterTimeAsInt64Codec的实现代码

使用 MarshalText支持非字符串作为key的map

虽然 JSON 标准里只支持string作为key的map。但是 golang 通过MarshalText()接口,使得其他类型也可以作为map的key。例如

1234
f, _, _ := big.ParseFloat("1", 10, 64, big.ToZero)val := map[*big.Float]string{f: "2"}str, err := MarshalToString(val)should.Equal(`{"1":"2"}`, str)

其中big.Float就实现了MarshalText()

使用 json.RawMessage

如果部分json文档没有标准格式,我们可以把原始的信息用[]byte保存下来。

1234567
type TestObject struct {    Field1 string    Field2 json.RawMessage}var data TestObjectjson.Unmarshal([]byte(`{"field1": "hello", "field2": [1,2,3]}`), &data)should.Equal(` [1,2,3]`, string(data.Field2))

使用 json.Number

默认情况下,如果是interface{}对应数字的情况会是float64类型的。如果输入的数字比较大,这个表示会有损精度。所以可以UseNumber()启用json.Number来用字符串表示数字。

12345
decoder1 := json.NewDecoder(bytes.NewBufferString(`123`))decoder1.UseNumber()var obj1 interface{}decoder1.Decode(&obj1)should.Equal(json.Number("123"), obj1)

jsoniter 支持标准库的这个用法。同时,扩展了行为使得Unmarshal也可以支持UseNumber了。

1234
json := Config{UseNumber:true}.Froze()var obj interface{}json.UnmarshalFromString("123", &obj)should.Equal(json.Number("123"), obj)

统一更改字段的命名风格

经常 JSON 里的字段名 Go 里的字段名是不一样的。我们可以用 field tag 来修改。

12345678
output, err := jsoniter.Marshal(struct {    UserName      string `json:"user_name"`    FirstLanguage string `json:"first_language"`}{    UserName:      "taowen",    FirstLanguage: "Chinese",})should.Equal(`{"user_name":"taowen","first_language":"Chinese"}`, string(output))

但是一个个字段来设置,太麻烦了。如果使用 jsoniter,我们可以统一设置命名风格。

123456789101112
import "github.com/json-iterator/go/extra"extra.SetNamingStrategy(LowerCaseWithUnderscores)output, err := jsoniter.Marshal(struct {    UserName      string    FirstLanguage string}{    UserName:      "taowen",    FirstLanguage: "Chinese",})should.Nil(err)should.Equal(`{"user_name":"taowen","first_language":"Chinese"}`, string(output))

使用私有的字段

Go 的标准库只支持 public 的 field。jsoniter 额外支持了 private 的 field。需要使用SupportPrivateFields()来开启开关。

123456789
import "github.com/json-iterator/go/extra"extra.SupportPrivateFields()type TestObject struct {    field1 string}obj := TestObject{}jsoniter.UnmarshalFromString(`{"field1":"Hello"}`, &obj)should.Equal("Hello", obj.field1)

下面是我补充的内容

忽略掉一些字段

原文中第一节有个错误,我更正过来了。omitempty不会忽略某个字段,而是忽略空的字段,当字段的值为空值的时候,它不会出现在JSON数据中。

如果想忽略某个字段,需要使用json:"-"格式。

12345
type User struct {    Email    string `json:"email"`    Password string `json:"password"`    // many more fields…}

如果想临时忽略掉空Password字段,可以用-:

123456
json.Marshal(struct {    *User    Password bool `json:"-"`}{    User: user,})

忽略掉一些字段2

如果不想更改原struct,还可以使用下面的方法:

12345678910111213141516
type User struct {    Email    string `json:"email"`    Password string `json:"password"`    // many more fields…}type omit *struct{}type PublicUser struct {    *User    Password omit `json:"-"`}json.Marshal(PublicUser{    User: user,})
]]>
taowenjson-iterator的作者。 序列化和反序列化需要处理JSON和struct的关系,其中会用到一些技巧。 原文Golang 中使用 JSON 的小技巧是他的经验之谈,介绍了一些struct解析成json的技巧,以及 json-iterator 库的一些便利的处理。

有的时候上游传过来的字段是string类型的,但是我们却想用变成数字来使用。 本来用一个json:",string" 就可以支持了,如果不知道golang的这些小技巧,就要大费周章了。

参考文章:http://attilaolah.eu/2014/09/10/json-and-struct-composition-in-go/

临时忽略struct空字段

12345
type User struct {    Email    string `json:"email"`    Password string `json:"password"`    // many more fields…}

如果想临时忽略掉空Password字段,可以用omitempty:

123456
json.Marshal(struct {    *User    Password bool `json:"password,omitempty"`}{    User: user,})

临时添加额外的字段

12345
type User struct {    Email    string `json:"email"`    Password string `json:"password"`    // many more fields…}

临时忽略掉空Password字段,并且添加token字段

12345678
json.Marshal(struct {    *User    Token    string `json:"token"`    Password bool `json:"password,omitempty"`}{    User: user,    Token: token,})

临时粘合两个struct

通过嵌入struct的方式:

1234567891011121314
type BlogPost struct {    URL   string `json:"url"`    Title string `json:"title"`}type Analytics struct {    Visitors  int `json:"visitors"`    PageViews int `json:"page_views"`}json.Marshal(struct{    *BlogPost    *Analytics}{post, analytics})

一个json切分成两个struct

123456789
json.Unmarshal([]byte(`{  "url": "attila@attilaolah.eu",  "title": "Attila's Blog",  "visitors": 6,  "page_views": 14}`), &struct {  *BlogPost  *Analytics}{&post, &analytics})

临时改名struct的字段

12345678910111213141516171819202122232425
type CacheItem struct {    Key    string `json:"key"`    MaxAge int    `json:"cacheAge"`    Value  Value  `json:"cacheValue"`}json.Marshal(struct{    *CacheItem    // Omit bad keys    OmitMaxAge omit `json:"cacheAge,omitempty"`    OmitValue  omit `json:"cacheValue,omitempty"`    // Add nice keys    MaxAge int    `json:"max_age"`    Value  *Value `json:"value"`}{    CacheItem: item,    // Set the int by value:    MaxAge: item.MaxAge,    // Set the nested struct by reference, avoid making a copy:    Value: &item.Value,})

用字符串传递数字

123
type TestObject struct {    Field1 int    `json:",string"`}

这个对应的json是{"Field1": "100"}

如果json是{"Field1": 100}则会报错

容忍字符串和数字互转

如果你使用的是jsoniter,可以启动模糊模式 来支持 PHP 传递过来的 JSON。

123
import "github.com/json-iterator/go/extra"extra.RegisterFuzzyDecoders()

这样就可以处理字符串和数字类型不对的问题了。比如

12
var val stringjsoniter.UnmarshalFromString(`100`, &val)

又比如

12
var val float32jsoniter.UnmarshalFromString(`"1.23"`, &val)

容忍空数组作为对象

PHP另外一个令人崩溃的地方是,如果 PHP array是空的时候,序列化出来是[]。但是不为空的时候,序列化出来的是{"key":"value"}。 我们需要把[]当成{}处理。

如果你使用的是jsoniter,可以启动模糊模式来支持 PHP 传递过来的 JSON。

123
import "github.com/json-iterator/go/extra"extra.RegisterFuzzyDecoders()

这样就可以支持了

12
var val map[string]interface{}jsoniter.UnmarshalFromString(`[]`, &val)

使用 MarshalJSON支持time.Time

golang 默认会把time.Time用字符串方式序列化。如果我们想用其他方式表示time.Time,需要自定义类型并定义MarshalJSON。

123456
type timeImplementedMarshaler time.Timefunc (obj timeImplementedMarshaler) MarshalJSON() ([]byte, error) {    seconds := time.Time(obj).Unix()    return []byte(strconv.FormatInt(seconds, 10)), nil}

序列化的时候会调用 MarshalJSON

123456789
type TestObject struct {    Field timeImplementedMarshaler}should := require.New(t)val := timeImplementedMarshaler(time.Unix(123, 0))obj := TestObject{val}bytes, err := jsoniter.Marshal(obj)should.Nil(err)should.Equal(`{"Field":123}`, string(bytes))

使用 RegisterTypeEncoder支持time.Time

jsoniter 能够对不是你定义的type自定义JSON编解码方式。比如对于time.Time可以用 epoch int64 来序列化

12345
import "github.com/json-iterator/go/extra"extra.RegisterTimeAsInt64Codec(time.Microsecond)output, err := jsoniter.Marshal(time.Unix(1, 1002))should.Equal("1000001", string(output))

如果要自定义的话,参见RegisterTimeAsInt64Codec的实现代码

使用 MarshalText支持非字符串作为key的map

虽然 JSON 标准里只支持string作为key的map。但是 golang 通过MarshalText()接口,使得其他类型也可以作为map的key。例如

1234
f, _, _ := big.ParseFloat("1", 10, 64, big.ToZero)val := map[*big.Float]string{f: "2"}str, err := MarshalToString(val)should.Equal(`{"1":"2"}`, str)

其中big.Float就实现了MarshalText()

使用 json.RawMessage

如果部分json文档没有标准格式,我们可以把原始的信息用[]byte保存下来。

1234567
type TestObject struct {    Field1 string    Field2 json.RawMessage}var data TestObjectjson.Unmarshal([]byte(`{"field1": "hello", "field2": [1,2,3]}`), &data)should.Equal(` [1,2,3]`, string(data.Field2))

使用 json.Number

默认情况下,如果是interface{}对应数字的情况会是float64类型的。如果输入的数字比较大,这个表示会有损精度。所以可以UseNumber()启用json.Number来用字符串表示数字。

12345
decoder1 := json.NewDecoder(bytes.NewBufferString(`123`))decoder1.UseNumber()var obj1 interface{}decoder1.Decode(&obj1)should.Equal(json.Number("123"), obj1)

jsoniter 支持标准库的这个用法。同时,扩展了行为使得Unmarshal也可以支持UseNumber了。

1234
json := Config{UseNumber:true}.Froze()var obj interface{}json.UnmarshalFromString("123", &obj)should.Equal(json.Number("123"), obj)

统一更改字段的命名风格

经常 JSON 里的字段名 Go 里的字段名是不一样的。我们可以用 field tag 来修改。

12345678
output, err := jsoniter.Marshal(struct {    UserName      string `json:"user_name"`    FirstLanguage string `json:"first_language"`}{    UserName:      "taowen",    FirstLanguage: "Chinese",})should.Equal(`{"user_name":"taowen","first_language":"Chinese"}`, string(output))

但是一个个字段来设置,太麻烦了。如果使用 jsoniter,我们可以统一设置命名风格。

123456789101112
import "github.com/json-iterator/go/extra"extra.SetNamingStrategy(LowerCaseWithUnderscores)output, err := jsoniter.Marshal(struct {    UserName      string    FirstLanguage string}{    UserName:      "taowen",    FirstLanguage: "Chinese",})should.Nil(err)should.Equal(`{"user_name":"taowen","first_language":"Chinese"}`, string(output))

使用私有的字段

Go 的标准库只支持 public 的 field。jsoniter 额外支持了 private 的 field。需要使用SupportPrivateFields()来开启开关。

123456789
import "github.com/json-iterator/go/extra"extra.SupportPrivateFields()type TestObject struct {    field1 string}obj := TestObject{}jsoniter.UnmarshalFromString(`{"field1":"Hello"}`, &obj)should.Equal("Hello", obj.field1)

下面是我补充的内容

忽略掉一些字段

原文中第一节有个错误,我更正过来了。omitempty不会忽略某个字段,而是忽略空的字段,当字段的值为空值的时候,它不会出现在JSON数据中。

如果想忽略某个字段,需要使用json:"-"格式。

12345
type User struct {    Email    string `json:"email"`    Password string `json:"password"`    // many more fields…}

如果想临时忽略掉空Password字段,可以用-:

123456
json.Marshal(struct {    *User    Password bool `json:"-"`}{    User: user,})

忽略掉一些字段2

如果不想更改原struct,还可以使用下面的方法:

12345678910111213141516
type User struct {    Email    string `json:"email"`    Password string `json:"password"`    // many more fields…}type omit *struct{}type PublicUser struct {    *User    Password omit `json:"-"`}json.Marshal(PublicUser{    User: user,})
]]>
0
<![CDATA[OMG,腾讯的OMG]]> http://www.udpwork.com/item/16320.html http://www.udpwork.com/item/16320.html#reviews Thu, 22 Jun 2017 14:15:34 +0800 魏武挥 http://www.udpwork.com/item/16320.html

江湖上有个段子,是这么说的。

有一家公司叫OMG,有一家公司叫腾讯。OMG是OMG,腾讯是腾讯。

把腾讯的一个事业群(OMG全称是online media group)看成独立的一家公司,这个梗不在圈子里的人,是很难接到的。但在圈子里的人,都会发出会心一笑。

OMG和腾讯的游离,自成体系封闭运作是表象,是结果。最关键的是,OMG一直以来奉行的理念,其实与腾讯“产品主导”并不契合。

OMG奉行的是:标准传媒业的内容主导。

这两者,有何本质的区别?

我的父母都是近乎一生奉献给传媒事业的。

我记得小时候家里客厅里有一幅对联:铁肩担道义,妙手著文章。

我倒不是说这十个字有什么不对的,我想说的是,它暗含了一个倾向,我称之为“传者本位主义”。

传媒业的人,很讲究这点:我们编发你们应该看的内容。

请注意“应该”这两个字。

这和百年以降的“启蒙”有关。

即便是立场倾向态度有着极大裂痕的环球时报与老南方,他们只是观念不同,但“我们编发你们应该看的内容”这一点上,并无二致。

这就叫内容导向。

这就叫传者本位主义。

这其实是数百年来传媒(无论是左中右)奉行的一个传统。

传媒业文人扎堆,文人通常有精英的情结,引领潮流的思维,俯瞰芸芸众生的姿势,一点都不奇怪。

腾讯是一家产品导向的公司。

什么叫产品导向?

其实就是:消费者本位主义。套用到内容业态里,应该是:受者本位主义。

受者本位主义的理念并不是“我们编发你们应该看的内容”,而是“我们编发你们喜欢看的内容”。

应该,喜欢。

就这两个字的差别,但天壤之别。

我一直认为,在移动端接过百度李彦宏衣钵的,是头条的张一鸣。

而百度,在06-07年,便已经颠覆了新浪陈彤为代表的门户。

门户依然是有传者本位倾向的,哪一条东西可以放首页,哪一条东西放头条,有着编辑或编辑部的价值观在里头。

但百度并没有。

有一阵子,IT评论圈喜欢批评百度没有价值观。但从搜索的角度讲,它的确不需要什么价值观。或者说,它唯一的价值观就是:用户用最快的速度得到了ta需要的内容——注意,不是ta应该得到的内容。

头条亦然。

在一次财经杂志对张一鸣的专访中,后者亲口说:头条没有价值观。

或者说,用户喜欢看什么,就是头条的价值观。

这是一个相当标准的产品导向、受者本位的思维。

腾讯这一次对OMG的大折腾,其实是蛮耐人寻味的。

如果是OMG自己折腾,倒是容易理解。毕竟,对头条不服,拉开阵势要怼一下,是很自然的事。

但这次大折腾,并不是OMG自发的,而是来自腾讯总部。

这里的耐人寻味之处就在于:事实上,腾讯的微信平台上已经聚集了中国可能最好的内容生产者群体,为什么还要让OMG拼命去冲锋,构建一个新的内容生态?

难道只是要怼一下今日头条么?

阴谋论倒是不缺。我自己也有。

比如这一个,毕竟腾讯是干过把成立了杀猫打狗指挥部的易讯,最后和京东换股了事的事。

但或许我更倾向于这样理解:OMG,必须是腾讯的OMG,要将这个偏离产品导向的事业群,纳入到腾讯整体的产品导向思维上来。

这是铁了心要干传播渠道的节奏。

传播渠道的核心,就是两个字。

分发。

我和腾讯的一位朋友交流时,他提到,头条这种挖掘用户喜欢看什么然后推送的算法分发,容易形成信息茧房。

持同样看法的人,不是只有他一个。

但我不以为然。

我觉得,如果头条真能完成信息茧房,它的技术已经到了登峰造极的地步。

试举一例。

张三关心美国大选问题——这个叫兴趣属性。头条今天的技术可以做到,如果张三用了一阵子头条,它完全可以命中到这个兴趣点,推送美国大选的文章给到张三。

但有趣的事是这样的,张三其实骨子里是支持希拉里的,他特别讨厌站在特朗普那一头的内容——这个叫观念属性,以我的观察,头条今天的技术,并不能有效命中张三的观念。

结果是:张三看了蛮多与他观念不符的内容。

他恨恨地想:一点都不准嘛!

这哪里构得成什么信息茧房!

对于一个产品的用户,有三个维度可以挖掘。

社会属性、兴趣属性、观念属性。

前两者今天几个巨头的技术都已经基本完善,即便是到了17年才打算重注入场的百度,这方面的技术储备,并非乏善可陈。

但观念属性,从目前来看,大概也就是搞社交分发的微信微博,或许能挖掘一二:用户会转发,转发时如果强烈反对,通常会在转发语中写出来:什么狗屁东西。

但内容客户端就缺乏这个数据。用户会转发头条的东西,并写出ta的观念,但可惜,ta转发到的场域是:微信,或者微博。

一旦能有效命中用户的观念属性,这才叫“我们编发/推送了您喜欢的内容”。

这里,依然有足够的空间,让后来者施为。

只是

我们离信息茧房,还有多远呢?

—— 首发 扯氮集 ——

版权声明 及 商业合作

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金管理合伙人

OMG,腾讯的OMG,首发于扯氮集

]]>

江湖上有个段子,是这么说的。

有一家公司叫OMG,有一家公司叫腾讯。OMG是OMG,腾讯是腾讯。

把腾讯的一个事业群(OMG全称是online media group)看成独立的一家公司,这个梗不在圈子里的人,是很难接到的。但在圈子里的人,都会发出会心一笑。

OMG和腾讯的游离,自成体系封闭运作是表象,是结果。最关键的是,OMG一直以来奉行的理念,其实与腾讯“产品主导”并不契合。

OMG奉行的是:标准传媒业的内容主导。

这两者,有何本质的区别?

我的父母都是近乎一生奉献给传媒事业的。

我记得小时候家里客厅里有一幅对联:铁肩担道义,妙手著文章。

我倒不是说这十个字有什么不对的,我想说的是,它暗含了一个倾向,我称之为“传者本位主义”。

传媒业的人,很讲究这点:我们编发你们应该看的内容。

请注意“应该”这两个字。

这和百年以降的“启蒙”有关。

即便是立场倾向态度有着极大裂痕的环球时报与老南方,他们只是观念不同,但“我们编发你们应该看的内容”这一点上,并无二致。

这就叫内容导向。

这就叫传者本位主义。

这其实是数百年来传媒(无论是左中右)奉行的一个传统。

传媒业文人扎堆,文人通常有精英的情结,引领潮流的思维,俯瞰芸芸众生的姿势,一点都不奇怪。

腾讯是一家产品导向的公司。

什么叫产品导向?

其实就是:消费者本位主义。套用到内容业态里,应该是:受者本位主义。

受者本位主义的理念并不是“我们编发你们应该看的内容”,而是“我们编发你们喜欢看的内容”。

应该,喜欢。

就这两个字的差别,但天壤之别。

我一直认为,在移动端接过百度李彦宏衣钵的,是头条的张一鸣。

而百度,在06-07年,便已经颠覆了新浪陈彤为代表的门户。

门户依然是有传者本位倾向的,哪一条东西可以放首页,哪一条东西放头条,有着编辑或编辑部的价值观在里头。

但百度并没有。

有一阵子,IT评论圈喜欢批评百度没有价值观。但从搜索的角度讲,它的确不需要什么价值观。或者说,它唯一的价值观就是:用户用最快的速度得到了ta需要的内容——注意,不是ta应该得到的内容。

头条亦然。

在一次财经杂志对张一鸣的专访中,后者亲口说:头条没有价值观。

或者说,用户喜欢看什么,就是头条的价值观。

这是一个相当标准的产品导向、受者本位的思维。

腾讯这一次对OMG的大折腾,其实是蛮耐人寻味的。

如果是OMG自己折腾,倒是容易理解。毕竟,对头条不服,拉开阵势要怼一下,是很自然的事。

但这次大折腾,并不是OMG自发的,而是来自腾讯总部。

这里的耐人寻味之处就在于:事实上,腾讯的微信平台上已经聚集了中国可能最好的内容生产者群体,为什么还要让OMG拼命去冲锋,构建一个新的内容生态?

难道只是要怼一下今日头条么?

阴谋论倒是不缺。我自己也有。

比如这一个,毕竟腾讯是干过把成立了杀猫打狗指挥部的易讯,最后和京东换股了事的事。

但或许我更倾向于这样理解:OMG,必须是腾讯的OMG,要将这个偏离产品导向的事业群,纳入到腾讯整体的产品导向思维上来。

这是铁了心要干传播渠道的节奏。

传播渠道的核心,就是两个字。

分发。

我和腾讯的一位朋友交流时,他提到,头条这种挖掘用户喜欢看什么然后推送的算法分发,容易形成信息茧房。

持同样看法的人,不是只有他一个。

但我不以为然。

我觉得,如果头条真能完成信息茧房,它的技术已经到了登峰造极的地步。

试举一例。

张三关心美国大选问题——这个叫兴趣属性。头条今天的技术可以做到,如果张三用了一阵子头条,它完全可以命中到这个兴趣点,推送美国大选的文章给到张三。

但有趣的事是这样的,张三其实骨子里是支持希拉里的,他特别讨厌站在特朗普那一头的内容——这个叫观念属性,以我的观察,头条今天的技术,并不能有效命中张三的观念。

结果是:张三看了蛮多与他观念不符的内容。

他恨恨地想:一点都不准嘛!

这哪里构得成什么信息茧房!

对于一个产品的用户,有三个维度可以挖掘。

社会属性、兴趣属性、观念属性。

前两者今天几个巨头的技术都已经基本完善,即便是到了17年才打算重注入场的百度,这方面的技术储备,并非乏善可陈。

但观念属性,从目前来看,大概也就是搞社交分发的微信微博,或许能挖掘一二:用户会转发,转发时如果强烈反对,通常会在转发语中写出来:什么狗屁东西。

但内容客户端就缺乏这个数据。用户会转发头条的东西,并写出ta的观念,但可惜,ta转发到的场域是:微信,或者微博。

一旦能有效命中用户的观念属性,这才叫“我们编发/推送了您喜欢的内容”。

这里,依然有足够的空间,让后来者施为。

只是

我们离信息茧房,还有多远呢?

—— 首发 扯氮集 ——

版权声明 及 商业合作

作者执教于上海交通大学媒体与设计学院,天奇阿米巴创投基金管理合伙人

OMG,腾讯的OMG,首发于扯氮集

]]>
0
<![CDATA[HTML 自定义元素教程]]> http://www.udpwork.com/item/16319.html http://www.udpwork.com/item/16319.html#reviews Thu, 22 Jun 2017 11:50:17 +0800 阮一峰 http://www.udpwork.com/item/16319.html 组件是 Web 开发的方向,现在的热点是 JavaScript 组件,但是 HTML 组件未来可能更有希望。

本文就介绍 HTML 组件的基础知识:自定义元素(custom elements)。

文章结尾还有一则React 培训消息(含 React Native),欢迎关注。

一、浏览器处理

我们一般都使用标准的 HTML 元素。

<p>Hello World</p>

上面代码中,<p>就是标准的 HTML 元素。

如果使用非标准的自定义元素,会有什么结果?

<greeting>Hello World</greeting>

上面代码中,<greeting>就是非标准元素,浏览器不认识它。这段代码的运行结果是,浏览器照常显示Hello World,这说明浏览器并没有过滤这个元素。

现在,为自定义元素加上样式。

greeting {
  display: block;
  font-size: 36px;
  color: red;
}

运行结果如下。

接着,使用脚本操作这个元素。

function customTag(tagName, fn){
  Array
    .from(document.getElementsByTagName(tagName))
    .forEach(fn);
}

function greetingHandler(element) {
  element.innerHTML = '你好,世界';
}   

customTag('greeting', greetingHandler);

运行结果如下。

这说明,浏览器对待自定义元素,就像对待标准元素一样,只是没有默认的样式和行为。这种处理方式是写入HTML5 标准的。

"User agents must treat elements and attributes that they do not understand as semantically neutral; leaving them in the DOM (for DOM processors), and styling them according to CSS (for CSS processors), but not inferring any meaning from them."

上面这段话的意思是,浏览器必须将自定义元素保留在 DOM 之中,但不会任何语义。除此之外,自定义元素与标准元素都一致。

事实上,浏览器提供了一个HTMLUnknownElement对象,所有自定义元素都是该对象的实例。

var tabs = document.createElement('tabs');

tabs instanceof HTMLUnknownElement // true
tabs instanceof HTMLElement // true

上面代码中,tabs是一个自定义元素,同时继承了HTMLUnknownElement和HTMLElement接口。

二、HTML import

有了自定义元素,就可以写出语义性非常好的 HTML 代码。

<share-buttons>
  <social-button type="weibo">
    <a href="...">微博</a>
  </social-button>
  <social-button type="weixin">
    <a href="...">微信</a>
  </social-button>
</share-buttons>

上面的代码,一眼就能看出语义。

如果将<share-buttons>元素的样式与脚本,封装在一个 HTML 文件share-buttons.html之中,这个元素就可以复用了。

使用的时候,先引入share-buttons.html。

<link rel="import" href="share-buttons.html">

然后,就可以在网页中使用<share-buttons>了。

<article>
  <h1>Title</h1>
  <share-buttons/>
  ... ...
</article>

HTML imports 的更多用法可以参考教程(12)。目前只有 Chrome 浏览器支持这个语法。

三、Custom Elements 标准

HTML5 标准规定了自定义元素是合法的。然后,W3C 就为自定义元素制定了一个单独的Custom Elements 标准

它与其他三个标准放在一起---- HTML Imports,HTML Template、Shadow DOM----统称为Web Components规范。目前,这个规范只有 Chrome 浏览器支持

Custom Elements 标准对自定义元素的名字做了限制

"自定义元素的名字必须包含一个破折号(-)所以<x-tags>、<my-element>和<my-awesome-app>都是正确的名字,而<tabs>和<foo_bar>是不正确的。这样的限制使得 HTML 解析器可以分辨那些是标准元素,哪些是自定义元素。"

注意,一旦名字之中使用了破折号,自定义元素就不是HTMLUnknownElement的实例了。

var xTabs = document.createElement('x-tabs');

xTabs instanceof HTMLUnknownElement // false
xTabs instanceof HTMLElement // true

Custom Elements 标准规定了,自定义元素的定义可以使用 ES6 的class语法

// 定义一个 <my-element></my-element>
class MyElement extends HTMLElement {...}
window.customElements.define('my-element', MyElement);

上面代码中,原生的window.customElements对象的define方法用来定义 Custom Element。该方法接受两个参数,第一个参数是自定义元素的名字,第二个参数是一个 ES6 的class。

这个class使用get和set方法定义 Custom Element 的某个属性。

class MyElement extends HTMLElement {
  get content() {
    return this.getAttribute('content');
  }

  set content(val) {
    this.setAttribute('content', val);
  }
}

有了这个定义,网页之中就可以插入<my-element>了。

<my-element content="Custom Element">
  Hello
</my-element>

处理脚本如下。

function customTag(tagName, fn){
  Array
    .from(document.getElementsByTagName(tagName))
    .forEach(fn);
}

function myElementHandler(element) {
  element.textConent = element.content;
}

customTag('my-element', myElementHandler);

运行结果如下。

ES6 Class 的一个好处是,可以很容易地写出继承类。

class MyNewElement extends MyElement {
  // ...
}

customElements.define('my-new-element', MyNewElement);

今天的教程就到这里,更多用法请参考谷歌的官方教程

四、参考链接

(正文完)

==============================

下面是一则培训消息。

自从我写了《React 技术栈系列教程》以后,有两种反馈:一种是觉得内容不够完整深入,希望有更详细的介绍,另一种是要求补上 React Native。对此我也没办法,精力有限,无法持续投入,只能推荐大家自己去看官方文档。

昨天,优达学城的朋友联系我。他们与React Training合作,正式推出了React 培训课程,希望我帮忙推广。

我听了很兴奋,因为React Training是美国最专业的 React 培训机构,水平很高。几个讲课老师在 React 社区都非常有名,React Routerunpkg和 mustache.js 就是他们的作品。我相信,国内很难找到这样水平的老师和课程。

实际上,这件事在美国也很受关注,Techcrunch进行了报道。

整个课程与美国完全同步,一共持续4个月,分成三个环节。

课程内容涉及整个 React 技术栈,PC 端和手机端并重。学完之后,可以获得纳米学位的证书。

课程价格是 3399 元人民币。注意,该课程不是零基础的,要求学习者已经掌握 JavaScript 基本语法,所以有报名审核环节。

想学 React/React Native 的同学可以考虑一下,点击这里了解详情,报名到6月27日截止。

(完)

文档信息

]]>
组件是 Web 开发的方向,现在的热点是 JavaScript 组件,但是 HTML 组件未来可能更有希望。

本文就介绍 HTML 组件的基础知识:自定义元素(custom elements)。

文章结尾还有一则React 培训消息(含 React Native),欢迎关注。

一、浏览器处理

我们一般都使用标准的 HTML 元素。

<p>Hello World</p>

上面代码中,<p>就是标准的 HTML 元素。

如果使用非标准的自定义元素,会有什么结果?

<greeting>Hello World</greeting>

上面代码中,<greeting>就是非标准元素,浏览器不认识它。这段代码的运行结果是,浏览器照常显示Hello World,这说明浏览器并没有过滤这个元素。

现在,为自定义元素加上样式。

greeting {
  display: block;
  font-size: 36px;
  color: red;
}

运行结果如下。

接着,使用脚本操作这个元素。

function customTag(tagName, fn){
  Array
    .from(document.getElementsByTagName(tagName))
    .forEach(fn);
}

function greetingHandler(element) {
  element.innerHTML = '你好,世界';
}   

customTag('greeting', greetingHandler);

运行结果如下。

这说明,浏览器对待自定义元素,就像对待标准元素一样,只是没有默认的样式和行为。这种处理方式是写入HTML5 标准的。

"User agents must treat elements and attributes that they do not understand as semantically neutral; leaving them in the DOM (for DOM processors), and styling them according to CSS (for CSS processors), but not inferring any meaning from them."

上面这段话的意思是,浏览器必须将自定义元素保留在 DOM 之中,但不会任何语义。除此之外,自定义元素与标准元素都一致。

事实上,浏览器提供了一个HTMLUnknownElement对象,所有自定义元素都是该对象的实例。

var tabs = document.createElement('tabs');

tabs instanceof HTMLUnknownElement // true
tabs instanceof HTMLElement // true

上面代码中,tabs是一个自定义元素,同时继承了HTMLUnknownElement和HTMLElement接口。

二、HTML import

有了自定义元素,就可以写出语义性非常好的 HTML 代码。

<share-buttons>
  <social-button type="weibo">
    <a href="...">微博</a>
  </social-button>
  <social-button type="weixin">
    <a href="...">微信</a>
  </social-button>
</share-buttons>

上面的代码,一眼就能看出语义。

如果将<share-buttons>元素的样式与脚本,封装在一个 HTML 文件share-buttons.html之中,这个元素就可以复用了。

使用的时候,先引入share-buttons.html。

<link rel="import" href="share-buttons.html">

然后,就可以在网页中使用<share-buttons>了。

<article>
  <h1>Title</h1>
  <share-buttons/>
  ... ...
</article>

HTML imports 的更多用法可以参考教程(12)。目前只有 Chrome 浏览器支持这个语法。

三、Custom Elements 标准

HTML5 标准规定了自定义元素是合法的。然后,W3C 就为自定义元素制定了一个单独的Custom Elements 标准

它与其他三个标准放在一起---- HTML Imports,HTML Template、Shadow DOM----统称为Web Components规范。目前,这个规范只有 Chrome 浏览器支持

Custom Elements 标准对自定义元素的名字做了限制

"自定义元素的名字必须包含一个破折号(-)所以<x-tags>、<my-element>和<my-awesome-app>都是正确的名字,而<tabs>和<foo_bar>是不正确的。这样的限制使得 HTML 解析器可以分辨那些是标准元素,哪些是自定义元素。"

注意,一旦名字之中使用了破折号,自定义元素就不是HTMLUnknownElement的实例了。

var xTabs = document.createElement('x-tabs');

xTabs instanceof HTMLUnknownElement // false
xTabs instanceof HTMLElement // true

Custom Elements 标准规定了,自定义元素的定义可以使用 ES6 的class语法

// 定义一个 <my-element></my-element>
class MyElement extends HTMLElement {...}
window.customElements.define('my-element', MyElement);

上面代码中,原生的window.customElements对象的define方法用来定义 Custom Element。该方法接受两个参数,第一个参数是自定义元素的名字,第二个参数是一个 ES6 的class。

这个class使用get和set方法定义 Custom Element 的某个属性。

class MyElement extends HTMLElement {
  get content() {
    return this.getAttribute('content');
  }

  set content(val) {
    this.setAttribute('content', val);
  }
}

有了这个定义,网页之中就可以插入<my-element>了。

<my-element content="Custom Element">
  Hello
</my-element>

处理脚本如下。

function customTag(tagName, fn){
  Array
    .from(document.getElementsByTagName(tagName))
    .forEach(fn);
}

function myElementHandler(element) {
  element.textConent = element.content;
}

customTag('my-element', myElementHandler);

运行结果如下。

ES6 Class 的一个好处是,可以很容易地写出继承类。

class MyNewElement extends MyElement {
  // ...
}

customElements.define('my-new-element', MyNewElement);

今天的教程就到这里,更多用法请参考谷歌的官方教程

四、参考链接

(正文完)

==============================

下面是一则培训消息。

自从我写了《React 技术栈系列教程》以后,有两种反馈:一种是觉得内容不够完整深入,希望有更详细的介绍,另一种是要求补上 React Native。对此我也没办法,精力有限,无法持续投入,只能推荐大家自己去看官方文档。

昨天,优达学城的朋友联系我。他们与React Training合作,正式推出了React 培训课程,希望我帮忙推广。

我听了很兴奋,因为React Training是美国最专业的 React 培训机构,水平很高。几个讲课老师在 React 社区都非常有名,React Routerunpkg和 mustache.js 就是他们的作品。我相信,国内很难找到这样水平的老师和课程。

实际上,这件事在美国也很受关注,Techcrunch进行了报道。

整个课程与美国完全同步,一共持续4个月,分成三个环节。

课程内容涉及整个 React 技术栈,PC 端和手机端并重。学完之后,可以获得纳米学位的证书。

课程价格是 3399 元人民币。注意,该课程不是零基础的,要求学习者已经掌握 JavaScript 基本语法,所以有报名审核环节。

想学 React/React Native 的同学可以考虑一下,点击这里了解详情,报名到6月27日截止。

(完)

文档信息

]]>
0
<![CDATA[The Right Way to Architect iOS App with Swift]]> http://www.udpwork.com/item/16318.html http://www.udpwork.com/item/16318.html#reviews Thu, 22 Jun 2017 08:00:00 +0800 李忠 http://www.udpwork.com/item/16318.html 关于 iOS 架构的文章感觉已经泛滥了,前一阵正好 Android 官方推了一套App Architecture,于是就在想,对于 iOS 来说,怎样的架构才是最适合的。带着这个问题,我开始了探索。

Why Architecture Matters?

这是第一个也是最重要的问题,为什么会出现各种 Architecture Pattern?真的那么重要么?

我们来想一下,无论是做一个 App 还是搭一套后台系统,如果是一次性的,今天用完明天就可以扔掉,那么怎么快怎么来,代码重复、代码逻辑、代码格式统统不重要。

这种场景比较适合黑客马拉松,而真实情况往往是我们的代码需要上线,要对用户负责,而一套好的架构会让这些事情变得更加容易。

好的架构简洁且整洁

说到架构,往往会想到建筑,软件架构跟建筑不同的点是软件架构会随着时间的推移进行演进,而实体建筑则没这个特性。抛开时间维度,这二者还是有一定的相似性的。

好的架构容易催生好的代码,就像住在干净整洁的房子里,会下意识地让其中的家具、电器、摆饰等也井井有条。

好的架构让代码更加容易维护

不容易维护的代码往往有这么几个特点:

  1. 抽象程度低
  2. 职责不明确
  3. 喜欢走捷径

好的架构能对 2 和 3 有一定的作用,对于第 1 点还是要看程序员的能力和经验。

抽象程度低

这样的代码往往是命令式编程产生的,也就是像 CPU 那样的思考方式,把产品经理的需求直观地翻译成代码,而不对其中的共性、本质进行抽离和抽象,时间一长就容易看不懂其中的逻辑,需求一变就要改核心代码。

比如下面这段代码,不知道具体要完成什么任务。

职责不明确

这也是产生「一大坨代码」的原因之一,就像 MVC 模式里,没有说明用户的操作应该在哪里处理,业务逻辑放在什么地方,这样就容易走捷径,怎么方便怎么来,而越是方便到后来就越容易出问题。

喜欢走捷径

这是我们的天性,毕竟能够更快更方便地达到目标,为什么不做呢?

比如我们都知道「通知」用起来很方便,所有涉及到单向数据传递的地方都可以使用,比如 Cell 通过通知向 VC 传递点击事件信息、Model 通过通知向 VC 传递数据信息、VC 之间通过通知进行解耦等等。

又比如可以很方便地在 VC 存储状态信息,慢慢地 VC 里这些状态变量就多了起来,到后来要维护这些变量就变得非常困难,出了问题也不好排查。

Clojure 的作者 Rich Hickey 有一个非常著名的Simple Made Easy分享

Simple is often erroneously mistaken for easy. “Easy” means “to be at hand”, “to be approachable”. “Simple” is the opposite of “complex” which means “being intertwined”, “being tied together”. Simple != easy.

Simple 是我们所追求的,而 Easy 往往会让事情往反方向发展。

好的架构能够覆盖大多数场景

产品经理:老板说要做一个插座,具体怎么实现我不管,下周一就要。拿到这个需求之后,你觉得很简单,完美符合需求,就像这样:

可是好景不长,老板新买了一个电脑,只支持两相的插座,而且现在就要,作为工程师,你不能被这么简单朴实的需求难倒,于是稍微动了下脑筋,就出了一个解决方案:

虽然丑陋,但是可以工作。但我们的目标不只是可以工作(紧急情况除外),更要优雅地工作。

举一个现实的例子,比如页面间支持通过 Router 进行跳转,但有一天发现有页面间通信的需求,然后就会出来一些 trick 的解决方案,比如发通知或者给 Router 加一个- (id)objectForURL:的方法,本质上跟上图的解决方案没什么区别。

好的架构能够提升开发效率,方便定位问题

好的架构能够支持多人并行开发、一定程度的代码复用、单元测试,出了问题能比较方便地找到原因。这几点是架构要解决的主要问题。

当前的状态

目前主流的主要有 MVC 和 MVVM,VIPER 用的会少一些,它们之间的优劣对比这里就不展开了,可以查看这篇文章来了解:iOS 架构模式 - 简述 MVC, MVP, MVVM 和 VIPER (译) - Coding 博客

简单总结下:

  • MVC 模式过于简单,定的标准过于粗放, 容易滋生捷径。
  • MVVM 会好很多,但场景的覆盖还不够全,比如缺少页面间跳转/通信、数据获取等。
  • VIPER 更加细致,但有点臃肿。

How to Define “Right”

每种架构都有自己的特点,如果要定义「Right」的话,至少要符合一些标准,以下是我整理的觉得比较重要的几条:

  • 尽量简单
  • 结构清晰
  • 职责明确
  • 符合 GUI 编程的特点

尽量简单

简单的事物容易理解,也比较容易接受,用爱因斯坦的话来说「尽量简单,但不要过于简单」。VIPER 其实已经挺完善的了,但就是有点复杂,可以看这篇文章感受下。

结构清晰

清晰的结构让外人也能很快地知道每个目录是做什么的,里面的文件起着怎样的作用,自己维护起来也方便。

职责明确

也就是Separation of Concern,每个单元只需要关心自己的事情,跟外部尽量解耦,这样无论是对代码复用和测试都会很有帮助。

符合 GUI 编程的特点

GUI 编程和其他的非界面编程还是有差异的,对 GUI 编程的特点进行合适地抽象,并在此基础上形成的架构才更有「对」的感觉。

我比较认同view = render(state) + handle(event)这个定义,view 本身只做两件事,给 state 包一层漂亮的外衣,同时对用户的操作做出响应。

Inspiring

差不多心里有谱了,现在来看看相关领域的架构大概是怎样的,找点启发。

Android Architecture

Android 最近出了一套官方推荐的架构,挺细致的,主要的流程如下图所示

大意就是ViewModel通过调用Repository从Model或Remote中获取数据,然后放到内置的LiveData里,而LiveData在Activity初始化时即被绑定,因此当LiveData变化时,可以马上反馈到界面。

当用户操作界面时,Activity会捕获到这些事件,然后调用ViewModel的特定方法,这些方法最终会导致LiveData发生改变,再次反馈到界面。

整体也是 MVVM 的模式,但也有自己的特点:

  • 通过LiveData来做单向绑定。
  • 使用Repository来统一数据的交互。
  • 内置Room作为持久层。
  • 内置ViewModel供使用。
  • 内置LifeCycle来简化跟生命周期相关的对象的操作,避免内存泄漏。(比如 ViewModel)
  • 使用Dagger2这个依赖注入工具来避免依赖。

Elm Architecture

Elm is a functional language that compiles to JavaScript. It competes with projects like React as a tool for creating websites and web apps. Elm has a very strong emphasis on simplicity, ease-of-use, and quality tooling.

Elm 是一个主打函数式编程,同时通过强大的编译器来尽量确保没有 runtime error 的编程语言,著名的 Redux 就是受它启发。来感受下它的代码:

import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

main =
  Html.beginnerProgram { model = 0, view = view, update = update }

type Msg = Increment | Decrement

update msg model =
  case msg of
    Increment ->
      model + 1

    Decrement ->
      model - 1

view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (toString model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

主要分为 4 块,model,view,update,message

  • view 展示 model 数据,同时将用户的操作作为 message 抛出。
  • model 包含了页面所需的所有信息。
  • 当 message 被抛出时,会自动进入到 update 方法,update 返回的新 model 自动进入到 view 里被展示。

跟其他的前端框架不同,Elm 不喜欢 parent-child communication, 也不提倡 components,作为函数式编程语言,它在乎的就是创建 function,通过helper function来达到类似的效果。

Vue Architecture

Vue 也是采用的 MVVM 模式,把数据绑定在内部处理了,对外部来说只要在data里声明特定的 key,在view里就可以直接使用,并且实时响应。对于view的事件,也会映射到ViewModel的特定方法。

Vue 的Router是把 path 映射到 component 上,看着也比较清晰。

1
2
3
4
5
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar },
	{ path: '/user/:id', component: User }
]

The Right Way (IMO)

目录结构

目录结构需要能够让不同职责的文件找到自己的归属,同时尽量清晰。这个是我目前觉得还不错的分类

  • External:一些第三方的 framework。
  • Extensions: 针对当前 App 做的一些针对性扩展。
  • Infrastructure: 比较重要的基础组件,在前期就要管控起来。
  • Models: 对应服务端的 Objects。
  • Views: 页面。
  • Shared: 会在 App 内部被公用的部分,方便统一管控。
  • Utilities: 一些帮助类。

Architecture

本质上跟 MVVM 差不多,只是多补充了些细节。之前也有考虑过采用 ReSwift + RxSwift 的方式,也就是 Redux,后来写下来发现还是有点复杂:比如下拉刷新的 3 个 state ( loading / loaded / failed),action 要定义(毕竟获取数据的逻辑写在 Action 中),state 中也要定义(视图最终关心的是 state 的变化);没有很方便的 diff 支持等。于是就回归到了 MVVM 模式。

ViewModel

ViewModel 主要有 3 个职责:

  • 通过 Repository 获取/修改数据。
  • 提供Observable Properties供 View 使用。
  • 提供Functions供 View 调用,通常会导致Observable Properties的改变。

这块也算是常规手法,需要注意的一点是 Repository 的初始化,如果要方便测试的话,最好提供注入点(比如初始化时注入或提供 set 方法注入)。

Repository

Repository 的职责就是跟数据打交道,获取远程/本地数据,并将其转换成 Model 返回给 ViewModel。

页面间跳转和通信

使用 Router 即可,如果是内部的 VC 之间跳转,还可以携带 model 信息。

通用的小模块( Components )

我发现前端开发里,Components用得还蛮多的,客户端开发倒不那么常见。这些小模块其实就是一些可在多个页面复用的业务相关的视图(Widget),可能带有业务逻辑,方便复用,比如「赞」按钮。

服务调用

比如在详情页要使用购物车的「加购」功能,通常做法是采用Register Procotol方式,维护一个 Protocol 和 Class 的注册表,并且在 App 启动时进行注册。我发现使用 Swift 的 POP 就不需要这么麻烦了,具体怎么做,我们后面讲。

Demo

这个 Demo 演示了知乎日报的列表和详情页:

看起来蛮简单的,不过事实可能并非如此,我们来慢慢捋一下。

初始页

刚进来时,会处于原始的 loading 状态,这个状态不同于下拉刷新,可能是一个萌萌的 loading 图。

首先这个页面属于NewsFeed页,因此在该目录下新建 3 个文件

1
2
3
|- NewsFeedViewModel.swift
|- NewsFeedViewController.swift
|- NewsFeedRepository.swift

本着 view 只是展示 state 的原则,我们首先要处理的就是 state,那么怎么处理? 这个 Event 是从 View 那边触发的,触发之后,对于 View 来说只能求助于 ViewModel,于是 VM 就提供了一个initialLoading方法。

那这个initialLoading里该做些什么呢?其实也就是根据 repository 的不同结果,设置不同的 state,然后 view 来响应这些 state。同时考虑到之后的「下拉刷新」和「加载更多」,顺便分离出一个通用的loadData:方法

ViewModel

1
2
3
4
5
6
7
8
9
class NewsFeedViewModel {
	func initialLoading() {
        loadData(.initial)
    }

	func loadData(_ loadingType: LoadingType, offset: String = "") {
		// todo
	}
}

那么Observable Properties应该是怎样的呢?在 OC 时代,只要简单的暴露 readonly 的 property,外部无论是 KVO 还是 RAC 都能很方便地进行绑定,到了 swift 时代,如果要做 KVO 就要继承NSObject,还要加一个@dynamic前缀,不优雅。比较理想的状态是使用 RxSwift 的Observable作为属性,外部只要subscribe就行了。不过在内部如何给这个Observable塞数据又有点小问题。最终决定使用Variable作为暴露的属性,它的好处是内部不需要再新建一个变量,直接设置这个Variable的value即可,弊端就是对于使用方需要先通过asObservable()转一下再进行 subscribe,并且只要愿意,也可以设置value值,存在误操作的风险。在这里我们先简单起见用Variable来做。

接下来的问题就是这个Variable里应该放什么?肯定要放一些当前的 loading 状态,比如 loaded,failed,loading 这些,那么要不要带上 data?如果不一起带上 data,那么状态的改变和数据的改变就不是一个原子操作,有可能会带来一些异常(比如 view 发现 loading 状态变为 loaded,自动去取最新的 data,但此时 data 可能还没有改变)。因此,我把它们都放到了一起,首先来看一下ResultModel

Model

这是一个通用的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ResultModel.swift

enum LoadingType {
    case initial, refresh, more
}

enum LoadingStatus: Equatable {
    case none
    case loading
    case loaded
    case failure(Error)
    
    static func ==(lhs: LoadingStatus, rhs: LoadingStatus) -> Bool {
        switch (lhs, rhs) {
        case (.none, .none):
            return true
        case (.loading, .loading):
            return true
        case (.loaded, .loaded):
            return true
        default:
            return false
        }
    }
}

struct ResultModel<T> {
    var loadingStatus: LoadingStatus = .none
    var loadingType: LoadingType = .initial
    
    var previousItems = [T]()
    var currentItems = [T]()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// NewsModel.swift

class NewsFeedViewModel {
    // 1
	  static var news:Variable<ResultModel<NewsItem>> = Variable(ResultModel())

    func initialLoading() {
        loadData(.initial)
    }

    func loadData(_ loadingType: LoadingType, offset: String = "") {
        // 2 如果当前处于 loading 状态,就不继续处理了
        if (NewsFeedViewModel.news.value.loadingStatus == .loading) {
            return
        }

        // 3 设置新的 loading 类型和状态
        var value = NewsFeedViewModel.news.value
        value.loadingStatus = .loading
        value.loadingType = loadingType
        NewsFeedViewModel.news.value = value
        
        // 4 接下来就是发网络请求,根据不同的请求结果设置 state
    }
}
  1. 这里使用static主要是出于方便。
  2. 这里纠结了一段时间,之前是新建了 3 个 loading status(initial, refresh, loadmore),然后每个 status 再细分为 3 种状态(loading, loaded, error),后来发现这样的话,「当前是哪个 loading status,该 status 目前处于什么状态」判断起来会比较麻烦。于是就按照现在这样进行了拆分。
  3. 在这里对状态进行更改之后,UI 那边可以自动收到更新。
  4. 这里会调用 Repository 来获取数据。

Repository

Repository 这块由于是异步交互,因此直接就上 RxSwift 了,返回一个Observable,VM 作为消费方来订阅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Foundation
import RxSwift

class NewsFeedRepository {
    static func news(_ offset: String = "") -> Observable<[String:Any]?> {
        return Observable.create({ observer in
            let path = offset.characters.count > 0 ? "/api/4/news/before/\(offset)" : "/api/4/news/latest"
            let resource = Resource(path: path, method: .GET, requestBody: nil, headers: ["Content-Type": "application/json"], parse: decodeJSON)
            
            // 这个用的是 chris 开源的简单的 API 请求封装 http://chris.eidhof.nl/posts/tiny-networking-in-swift.html
            apiRequest(baseURL: URL(string: "https://news-at.zhihu.com")!, resource: resource, failure: { (reason, result) in
                observer.on(.error(reason))
            }, success: { result in
                observer.on(.next(result))
                observer.on(.completed)
            })
            
            return Disposables.create()
        })
    }
}

也可以在这里直接返回解析后的 Model,这样 VM 那边就不用处理了。

ViewModel 调用 Repository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class NewsFeedViewModel {
    // 4
    NewsFeedRepository.news(offset).asObservable().subscribe(onNext: {[unowned self] (result) in
        // 把 json 转换为 model
        let parsedResult = self._parseResult(result: result)
        var value = NewsFeedViewModel.news.value
        value.previousItems = NewsFeedViewModel.news.value.currentItems
        
        // 设置对应的 value
        if value.loadingType == .more {
            value.currentItems = value.previousItems + (parsedResult?.news ?? [])
        } else {
            value.currentItems = parsedResult?.news ?? []
        }
            
        value.loadingStatus = .loaded
        NewsFeedViewModel.news.value = value
        self.offset = parsedResult?.date ?? ""
        value.loadingStatus = .none
        
        // 统一设置 value,对外部 subscriber 来说就是原子操作
        NewsFeedViewModel.news.value = value
    }, onError: { (error) in
        NewsFeedViewModel.news.value.loadingStatus = .failure(error)
    }, onCompleted: {  
    }) {
    }.addDisposableTo(disposeBag)
}

这里你会注意到有一个previousItems和currentItems,这个主要是提供灵活性,避免暴力的reloadData(),比如获取到了更多的数据之后,可以只 reload 新的数据。

View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// NewsFeedViewController.swift
class NewsFeedViewController: UITableViewController {
    override func viewDidLoad() {
        handleDataChange()
        viewModel.initialLoading()
    }

    func handleDataChange() {
        NewsFeedViewModel.news.asObservable()
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: {[unowned self] item in
                if item.loadingStatus != .loading {
                    self.initialLoadingIndicator.stopAnimating()
                }
                if item.loadingStatus == .loaded {
                    // 这里调用 Diff 这个 framework 提供的 extension
                    self.tableView.animateRowChanges(oldData: item.previousItems, newData: item.currentItems)
                }
                if item.loadingType == .initial && item.loadingStatus == .loading {
                    self.initialLoadingIndicator.startAnimating()
                }
            }).addDisposableTo(disposeBag)
    }
}

「正在加载」和「已经加载」的场景已经处理完了,「加载失败」的处理也类似,比如失败之后显示一个 reload button,点击 reload button 之后,再调用一下viewModel.initialLoading()

TableView

接下来就来看看如何处理 TableView 的数据展示,其实就是消费 VM 的 property

1
2
3
4
5
6
7
8
9
10
11
12
extension NewsFeedViewController {
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return NewsFeedViewModel.news.value.currentItems.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! NewsCell
        let newsItem: NewsItem = NewsFeedViewModel.news.value.currentItems[indexPath.row]
        cell.configure(newsItem)
        return cell
    }
}

到这里最基本的首页数据展示就基本完成了。

加载更多

之前一直在纠结这块到底该怎么做才比较合适,如果直接把 newItems append 到原有的 items 列表,形成新的列表,UI 那边拿到之后就只能reloadData()了,最好能让 UI 那边知道新的和旧的之间发生了哪些变化,于是就找到了Diff这个 framework,它能够定位出两个 collection 之间的差异,但前提是 collection item 要实现Equatable协议。于是就有了previousItems和currentItems的设计。

喜欢功能

喜欢功能本质上是修改 NewsItem 的hasFaved属性,然后让 UI 可以感知到这个变化。这里问题就来了:如何对列表中的一个struct进行调整?我们知道struct是值拷贝的,只要发生赋值行为,拿到的就不再是原先的那个 struct 了(比如把 items 通过参数传递,要修改的话就要进行拷贝,除非设置为inout)。

这个问题本质上是如何操作 Immutable Objects,然后就想到了Immutable.js,它也提供了一些修改 List 的方法,只不过都是返回一个新的:

1
2
3
4
const { List } = require('immutable');
const list = List([ 0, 1, 2, List([ 3, 4 ])])
list.setIn([3, 0], 999);
// List [ 0, 1, 2, List [ 999, 4 ] ]

因此,这里简单的处理方式就是通过传进来的newsItem找到它在 list 中的 index(newsItem已经实现了Equatable协议),然后把修改过hasFaved属性的新的newsItem放到 index 位置来达到替换的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
class NewsFeedViewModel {
    func toggleFav(_ newsItem: NewsItem) {
        if let newsIndex = NewsFeedViewModel.news.value.currentItems.index(of: newsItem) {
            var _newsItem = NewsFeedViewModel.news.value.currentItems[newsIndex]
            _newsItem.hasFaved = !_newsItem.hasFaved

            var value = NewsFeedViewModel.news.value
            value.currentItems[newsIndex] = _newsItem

            NewsFeedViewModel.news.value = value
        }
    }
}

Components

由于新闻列表和喜欢的新闻列表表现上一致,那么就可以进行一些复用,比如可以把 Cell 作为 Component。

那对于一个 Component 来说,需要具备哪些特性呢?这个并没有什么约定,本质上就是一个或几个函数,外部调用后会返回一个 view,或者提供一些 block 回调,仅此而已。

Truth and Computed Properties

这里的Truth是指最源头的数据,比如一个数组,Computed Properties是指对源头数据进行消费可以得到的结果,比如数组的长度,或数组中的正数等。

在这个例子中,Truth就是newsItems列表,而喜欢的newsItems就是Computed Properties。因此只要 newsItems 发生变化,就重新计算喜欢的 NewsItems。

1
2
3
4
5
NewsFeedViewModel.news.asObservable().subscribe(onNext: { item in
    NewsFeedViewModel.favedNews.value = NewsFeedViewModel.news.value.currentItems.filter { (item) -> Bool in
         return item.hasFaved
    }
}).addDisposableTo(disposeBag)

喜欢功能的 View

主要就是两件事:

  1. 点击 Fav 按钮时,调用 VM 的toggleFav方法。
  2. 当 Fav 列表更新时,刷新 TableView。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
extension FavedViewController {
    func handleDataChange() {
        NewsFeedViewModel.favedNews.asObservable().subscribe(onNext:{[unowned self] item in
            self.tableView.reloadData()
        }).addDisposableTo(disposeBag)
    }
}

extension FavedViewController {
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return NewsFeedViewModel.favedNews.value.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! NewsCell
        var newsItem: NewsItem = NewsFeedViewModel.favedNews.value[indexPath.row]
        
        cell.configure(newsItem) { [unowned self] (button) in
            if button.tag == 0 {
                button.tag = 1
                button.setTitle("♥︎", for: .normal)
            } else {
                button.tag = 0
                button.setTitle("♡", for: .normal)
            }
            self.viewModel.toggleFav(newsItem)
            self.tableView.reloadData()
        }
        
        return cell
    }
}

页面跳转

页面间的跳转用到了Router,也就是 open 一个 url 就能到达特定的页面,这么做的好处是可以和外部跳转进来的情况统一处理(因为从外部跳到某个 app 只能通过 openURL)。

但在内部直接输入 URL 总觉得不优雅,而且容易出错,将来如果要修改 URL 也不方便。因此做了一个简单的Router来达到这个效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import Foundation
import UIKit

// 1
enum RouterTable: String {
    case home = "home"
    case detail = "detail/:id"
    
    func asController() -> UIViewController.Type {
        switch self {
        case .home:
            return NewsFeedViewController.self
        case .detail:
            return NewsDetailViewController.self
        }
    }
}

// 2
class Router {
    static func to(_ route: RouterTable, parameters: Dictionary<String, Any>?) -> Void {
        let viewController = route.asController().init()

        // 2.1
        if let parameters = parameters {
            for (key, value) in parameters {
                viewController.putExtra(key, value)
            }
        }

        //TODO: 添加 shouldBePushed 调用,比如有些页面需要先登录
        DispatchQueue.main.async {
            UINavigationController.current().pushViewController(viewController, animated: true)
        }
    }
}

// 3
extension Router {
    func parseURL(_ url: String) -> (RouterTable, Dictionary<String, String>?) {
        //TODO: add implementation
        return (.home, nil)
    }
}

主要分为 3 部分:

  1. 这个跟 vue-router 里定义 url 和 components 的关系一样,主要是为了方便统一管理。
  2. 这里主要是把 enum 转换为对应的 Controller,因为限制了类型,也就不会出现找不到 VC 的情况。
  3. 这个是用来应对外部跳转进来的 URL,把它解析成RouterTable,统一逻辑。

针对 2 重点说一下,这个是最简实现,真实场景会比这复杂得多,比如有些页面是 present 出来的,有些页面 push 前需要先判断是否登录等等。

注意到2.1的部分,这里有一个putExtra方法,这是新添加的一个扩展,参考了 Android 的IntentputExtra。实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protocol ViewCotrollerIntent {
    func putExtra(_ key: String, _ value: Any)
    func getExtra(_ key: String) -> Any?
}

extension UIViewController: ViewCotrollerIntent {
    
    private struct IntentStorage {
        static var extra: [String:Any] = [:]
    }
    
    func putExtra(_ key: String, _ value: Any) {
        IntentStorage.extra[key] = value
    }
    
    func getExtra(_ key: String) -> Any? {
        return IntentStorage.extra[key]
    }
}

由于 extension 不支持 associated properties,因此用 struct 做了个中转。这样,VC 之间的跳转如果要带上额外的参数,只要放到 extra 里即可。

详情页

详情页比较简单,只是展示一个 webview,这里比较棘手的问题是 model 数据的同步。由于详情页也可以修改NewsItem的hasFaved属性,这个改变需要能够实时同步到列表页,不然就会出现状态不同步的情况。

这块的设计也想了一段时间,Pinterest 采用的是通知的方式,并且额外开发了一个用来支持这种方式的,不想整的这么麻烦。本质需求是:当传过去的 model 发生变化时通知我。而 RxSwift 里的Variable不是正好可以达到这个效果么?于是就有了基于Variable的解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extension NewsFeedViewController {
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let newsItem: NewsItem = NewsFeedViewModel.news.value.currentItems[indexPath.row]
        let newsItemVariable = Variable<NewsItem>(newsItem)

        // 详情页可能会对这个 newsItemVariable 进行调整
        newsItemVariable.asObservable().subscribe(onNext: { [unowned self] item in
            // 找到这个 item 所在的 index,并进行替换
            self.viewModel.update(item: item)
            self.tableView.reloadData()
        }).addDisposableTo(disposeBag)

        // 带上这个 Variable 到新的 VC
        Router.to(.detail, parameters: ["model": newsItemVariable])
    }
}

详情页 View 的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class NewsDetailViewController: UIViewController {
    override func viewDidLoad() {
        // favButton
        navigationItem.rightBarButtonItem = favButton
        favButton.rx.tap
            .subscribe(onNext: { [unowned self] item in
                self.viewModel.toggleFav()
            })
            .addDisposableTo(disposeBag)
        
        // 1
        if let id = self.getExtra("id") as? Int {
            // viewModel.load(id)
        }
        
        // 2
        if let model = self.getExtra("model") as? Variable<NewsItem> {
            favButton.title = model.value.hasFaved ? "♥︎" : "♡"
            viewModel.load(Int(model.value.id))
            NewsDetailViewModel.newsItem = model
        }
        
        handleDataChange()
    }

    // 3
    func handleDataChange() {
        NewsDetailViewModel.newsDetail.asObservable()
            .subscribe(onNext:{ [unowned self] item in
                if let item = item {
                    let request = URLRequest(url: URL(string: item.shareURL)!)
                    self.webView.loadRequest(request)
                }
            })
            .addDisposableTo(disposeBag)
        
        NewsDetailViewModel.newsItem?.asObservable()
            .subscribe(onNext: { [unowned self] item in
                self.favButton.title = item.hasFaved ? "♥︎" : "♡"
            })
            .addDisposableTo(disposeBag)
    }
}
  1. 这里为通过外部 URL 进来的留一个入口。
  2. 通过getExtra拿到Variable后,接下来就交给 VM 了。
  3. handleDataChange做的事情就是响应 VM 的 properties 的变化,做一些 UI 上的调整。

Service

之前说过使用 Swift 提供 Service 会比较方便,都不需要在 App 启动时进行注册,利用自带的 Protocol Extension 就能达到效果。这个例子中没有用到,就举个其他的例子吧,以购物车为例:

1
2
3
4
5
6
7
8
9
10
11
// 放在 Services 目录下的 Protocols.swift
protocol Cart {
    public func add(_ item: Item) -> Bool
}

// 具体的实现可以放到对应的页面
extension Cart {
    public func add(_ item: Item) -> Bool {
        // business logic
    }
}

对于想要使用这个功能的开发来说,只要看Services/Protocols.swift就行了。跟 Objective-C 不同,extension 里如果有两个相同的方法,编译器会直接报错,这样就避免了运行期可能出现多个实现的问题。

Local Reasoning

Local Reasoning 的意思是对于数据的改动都发生在某一个特定的单元。这也是使用 Value Type 的好处,因为如果使用 Reference Type,只要把其中的一个 Reference 给了出去,就不知道什么时间什么场景下数据会在外部被改变,就像给了你一张银行卡,今天看还剩 1 万,可能明天再去看就只剩 1 千了。

使用 VM 后,所有对数据的改动都发生在 VM 里面,同时对数据的消费也尽量在一个地方,方便维护。

小结

以上是我自己对「Right Architecture」的一些理解和实践,实际过程中肯定还有很多细节要调整,如果你有什么想法欢迎交流~

]]>
关于 iOS 架构的文章感觉已经泛滥了,前一阵正好 Android 官方推了一套App Architecture,于是就在想,对于 iOS 来说,怎样的架构才是最适合的。带着这个问题,我开始了探索。

Why Architecture Matters?

这是第一个也是最重要的问题,为什么会出现各种 Architecture Pattern?真的那么重要么?

我们来想一下,无论是做一个 App 还是搭一套后台系统,如果是一次性的,今天用完明天就可以扔掉,那么怎么快怎么来,代码重复、代码逻辑、代码格式统统不重要。

这种场景比较适合黑客马拉松,而真实情况往往是我们的代码需要上线,要对用户负责,而一套好的架构会让这些事情变得更加容易。

好的架构简洁且整洁

说到架构,往往会想到建筑,软件架构跟建筑不同的点是软件架构会随着时间的推移进行演进,而实体建筑则没这个特性。抛开时间维度,这二者还是有一定的相似性的。

好的架构容易催生好的代码,就像住在干净整洁的房子里,会下意识地让其中的家具、电器、摆饰等也井井有条。

好的架构让代码更加容易维护

不容易维护的代码往往有这么几个特点:

  1. 抽象程度低
  2. 职责不明确
  3. 喜欢走捷径

好的架构能对 2 和 3 有一定的作用,对于第 1 点还是要看程序员的能力和经验。

抽象程度低

这样的代码往往是命令式编程产生的,也就是像 CPU 那样的思考方式,把产品经理的需求直观地翻译成代码,而不对其中的共性、本质进行抽离和抽象,时间一长就容易看不懂其中的逻辑,需求一变就要改核心代码。

比如下面这段代码,不知道具体要完成什么任务。

职责不明确

这也是产生「一大坨代码」的原因之一,就像 MVC 模式里,没有说明用户的操作应该在哪里处理,业务逻辑放在什么地方,这样就容易走捷径,怎么方便怎么来,而越是方便到后来就越容易出问题。

喜欢走捷径

这是我们的天性,毕竟能够更快更方便地达到目标,为什么不做呢?

比如我们都知道「通知」用起来很方便,所有涉及到单向数据传递的地方都可以使用,比如 Cell 通过通知向 VC 传递点击事件信息、Model 通过通知向 VC 传递数据信息、VC 之间通过通知进行解耦等等。

又比如可以很方便地在 VC 存储状态信息,慢慢地 VC 里这些状态变量就多了起来,到后来要维护这些变量就变得非常困难,出了问题也不好排查。

Clojure 的作者 Rich Hickey 有一个非常著名的Simple Made Easy分享

Simple is often erroneously mistaken for easy. “Easy” means “to be at hand”, “to be approachable”. “Simple” is the opposite of “complex” which means “being intertwined”, “being tied together”. Simple != easy.

Simple 是我们所追求的,而 Easy 往往会让事情往反方向发展。

好的架构能够覆盖大多数场景

产品经理:老板说要做一个插座,具体怎么实现我不管,下周一就要。拿到这个需求之后,你觉得很简单,完美符合需求,就像这样:

可是好景不长,老板新买了一个电脑,只支持两相的插座,而且现在就要,作为工程师,你不能被这么简单朴实的需求难倒,于是稍微动了下脑筋,就出了一个解决方案:

虽然丑陋,但是可以工作。但我们的目标不只是可以工作(紧急情况除外),更要优雅地工作。

举一个现实的例子,比如页面间支持通过 Router 进行跳转,但有一天发现有页面间通信的需求,然后就会出来一些 trick 的解决方案,比如发通知或者给 Router 加一个- (id)objectForURL:的方法,本质上跟上图的解决方案没什么区别。

好的架构能够提升开发效率,方便定位问题

好的架构能够支持多人并行开发、一定程度的代码复用、单元测试,出了问题能比较方便地找到原因。这几点是架构要解决的主要问题。

当前的状态

目前主流的主要有 MVC 和 MVVM,VIPER 用的会少一些,它们之间的优劣对比这里就不展开了,可以查看这篇文章来了解:iOS 架构模式 - 简述 MVC, MVP, MVVM 和 VIPER (译) - Coding 博客

简单总结下:

  • MVC 模式过于简单,定的标准过于粗放, 容易滋生捷径。
  • MVVM 会好很多,但场景的覆盖还不够全,比如缺少页面间跳转/通信、数据获取等。
  • VIPER 更加细致,但有点臃肿。

How to Define “Right”

每种架构都有自己的特点,如果要定义「Right」的话,至少要符合一些标准,以下是我整理的觉得比较重要的几条:

  • 尽量简单
  • 结构清晰
  • 职责明确
  • 符合 GUI 编程的特点

尽量简单

简单的事物容易理解,也比较容易接受,用爱因斯坦的话来说「尽量简单,但不要过于简单」。VIPER 其实已经挺完善的了,但就是有点复杂,可以看这篇文章感受下。

结构清晰

清晰的结构让外人也能很快地知道每个目录是做什么的,里面的文件起着怎样的作用,自己维护起来也方便。

职责明确

也就是Separation of Concern,每个单元只需要关心自己的事情,跟外部尽量解耦,这样无论是对代码复用和测试都会很有帮助。

符合 GUI 编程的特点

GUI 编程和其他的非界面编程还是有差异的,对 GUI 编程的特点进行合适地抽象,并在此基础上形成的架构才更有「对」的感觉。

我比较认同view = render(state) + handle(event)这个定义,view 本身只做两件事,给 state 包一层漂亮的外衣,同时对用户的操作做出响应。

Inspiring

差不多心里有谱了,现在来看看相关领域的架构大概是怎样的,找点启发。

Android Architecture

Android 最近出了一套官方推荐的架构,挺细致的,主要的流程如下图所示

大意就是ViewModel通过调用Repository从Model或Remote中获取数据,然后放到内置的LiveData里,而LiveData在Activity初始化时即被绑定,因此当LiveData变化时,可以马上反馈到界面。

当用户操作界面时,Activity会捕获到这些事件,然后调用ViewModel的特定方法,这些方法最终会导致LiveData发生改变,再次反馈到界面。

整体也是 MVVM 的模式,但也有自己的特点:

  • 通过LiveData来做单向绑定。
  • 使用Repository来统一数据的交互。
  • 内置Room作为持久层。
  • 内置ViewModel供使用。
  • 内置LifeCycle来简化跟生命周期相关的对象的操作,避免内存泄漏。(比如 ViewModel)
  • 使用Dagger2这个依赖注入工具来避免依赖。

Elm Architecture

Elm is a functional language that compiles to JavaScript. It competes with projects like React as a tool for creating websites and web apps. Elm has a very strong emphasis on simplicity, ease-of-use, and quality tooling.

Elm 是一个主打函数式编程,同时通过强大的编译器来尽量确保没有 runtime error 的编程语言,著名的 Redux 就是受它启发。来感受下它的代码:

import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

main =
  Html.beginnerProgram { model = 0, view = view, update = update }

type Msg = Increment | Decrement

update msg model =
  case msg of
    Increment ->
      model + 1

    Decrement ->
      model - 1

view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (toString model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

主要分为 4 块,model,view,update,message

  • view 展示 model 数据,同时将用户的操作作为 message 抛出。
  • model 包含了页面所需的所有信息。
  • 当 message 被抛出时,会自动进入到 update 方法,update 返回的新 model 自动进入到 view 里被展示。

跟其他的前端框架不同,Elm 不喜欢 parent-child communication, 也不提倡 components,作为函数式编程语言,它在乎的就是创建 function,通过helper function来达到类似的效果。

Vue Architecture

Vue 也是采用的 MVVM 模式,把数据绑定在内部处理了,对外部来说只要在data里声明特定的 key,在view里就可以直接使用,并且实时响应。对于view的事件,也会映射到ViewModel的特定方法。

Vue 的Router是把 path 映射到 component 上,看着也比较清晰。

1
2
3
4
5
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar },
	{ path: '/user/:id', component: User }
]

The Right Way (IMO)

目录结构

目录结构需要能够让不同职责的文件找到自己的归属,同时尽量清晰。这个是我目前觉得还不错的分类

  • External:一些第三方的 framework。
  • Extensions: 针对当前 App 做的一些针对性扩展。
  • Infrastructure: 比较重要的基础组件,在前期就要管控起来。
  • Models: 对应服务端的 Objects。
  • Views: 页面。
  • Shared: 会在 App 内部被公用的部分,方便统一管控。
  • Utilities: 一些帮助类。

Architecture

本质上跟 MVVM 差不多,只是多补充了些细节。之前也有考虑过采用 ReSwift + RxSwift 的方式,也就是 Redux,后来写下来发现还是有点复杂:比如下拉刷新的 3 个 state ( loading / loaded / failed),action 要定义(毕竟获取数据的逻辑写在 Action 中),state 中也要定义(视图最终关心的是 state 的变化);没有很方便的 diff 支持等。于是就回归到了 MVVM 模式。

ViewModel

ViewModel 主要有 3 个职责:

  • 通过 Repository 获取/修改数据。
  • 提供Observable Properties供 View 使用。
  • 提供Functions供 View 调用,通常会导致Observable Properties的改变。

这块也算是常规手法,需要注意的一点是 Repository 的初始化,如果要方便测试的话,最好提供注入点(比如初始化时注入或提供 set 方法注入)。

Repository

Repository 的职责就是跟数据打交道,获取远程/本地数据,并将其转换成 Model 返回给 ViewModel。

页面间跳转和通信

使用 Router 即可,如果是内部的 VC 之间跳转,还可以携带 model 信息。

通用的小模块( Components )

我发现前端开发里,Components用得还蛮多的,客户端开发倒不那么常见。这些小模块其实就是一些可在多个页面复用的业务相关的视图(Widget),可能带有业务逻辑,方便复用,比如「赞」按钮。

服务调用

比如在详情页要使用购物车的「加购」功能,通常做法是采用Register Procotol方式,维护一个 Protocol 和 Class 的注册表,并且在 App 启动时进行注册。我发现使用 Swift 的 POP 就不需要这么麻烦了,具体怎么做,我们后面讲。

Demo

这个 Demo 演示了知乎日报的列表和详情页:

看起来蛮简单的,不过事实可能并非如此,我们来慢慢捋一下。

初始页

刚进来时,会处于原始的 loading 状态,这个状态不同于下拉刷新,可能是一个萌萌的 loading 图。

首先这个页面属于NewsFeed页,因此在该目录下新建 3 个文件

1
2
3
|- NewsFeedViewModel.swift
|- NewsFeedViewController.swift
|- NewsFeedRepository.swift

本着 view 只是展示 state 的原则,我们首先要处理的就是 state,那么怎么处理? 这个 Event 是从 View 那边触发的,触发之后,对于 View 来说只能求助于 ViewModel,于是 VM 就提供了一个initialLoading方法。

那这个initialLoading里该做些什么呢?其实也就是根据 repository 的不同结果,设置不同的 state,然后 view 来响应这些 state。同时考虑到之后的「下拉刷新」和「加载更多」,顺便分离出一个通用的loadData:方法

ViewModel

1
2
3
4
5
6
7
8
9
class NewsFeedViewModel {
	func initialLoading() {
        loadData(.initial)
    }

	func loadData(_ loadingType: LoadingType, offset: String = "") {
		// todo
	}
}

那么Observable Properties应该是怎样的呢?在 OC 时代,只要简单的暴露 readonly 的 property,外部无论是 KVO 还是 RAC 都能很方便地进行绑定,到了 swift 时代,如果要做 KVO 就要继承NSObject,还要加一个@dynamic前缀,不优雅。比较理想的状态是使用 RxSwift 的Observable作为属性,外部只要subscribe就行了。不过在内部如何给这个Observable塞数据又有点小问题。最终决定使用Variable作为暴露的属性,它的好处是内部不需要再新建一个变量,直接设置这个Variable的value即可,弊端就是对于使用方需要先通过asObservable()转一下再进行 subscribe,并且只要愿意,也可以设置value值,存在误操作的风险。在这里我们先简单起见用Variable来做。

接下来的问题就是这个Variable里应该放什么?肯定要放一些当前的 loading 状态,比如 loaded,failed,loading 这些,那么要不要带上 data?如果不一起带上 data,那么状态的改变和数据的改变就不是一个原子操作,有可能会带来一些异常(比如 view 发现 loading 状态变为 loaded,自动去取最新的 data,但此时 data 可能还没有改变)。因此,我把它们都放到了一起,首先来看一下ResultModel

Model

这是一个通用的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ResultModel.swift

enum LoadingType {
    case initial, refresh, more
}

enum LoadingStatus: Equatable {
    case none
    case loading
    case loaded
    case failure(Error)
    
    static func ==(lhs: LoadingStatus, rhs: LoadingStatus) -> Bool {
        switch (lhs, rhs) {
        case (.none, .none):
            return true
        case (.loading, .loading):
            return true
        case (.loaded, .loaded):
            return true
        default:
            return false
        }
    }
}

struct ResultModel<T> {
    var loadingStatus: LoadingStatus = .none
    var loadingType: LoadingType = .initial
    
    var previousItems = [T]()
    var currentItems = [T]()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// NewsModel.swift

class NewsFeedViewModel {
    // 1
	  static var news:Variable<ResultModel<NewsItem>> = Variable(ResultModel())

    func initialLoading() {
        loadData(.initial)
    }

    func loadData(_ loadingType: LoadingType, offset: String = "") {
        // 2 如果当前处于 loading 状态,就不继续处理了
        if (NewsFeedViewModel.news.value.loadingStatus == .loading) {
            return
        }

        // 3 设置新的 loading 类型和状态
        var value = NewsFeedViewModel.news.value
        value.loadingStatus = .loading
        value.loadingType = loadingType
        NewsFeedViewModel.news.value = value
        
        // 4 接下来就是发网络请求,根据不同的请求结果设置 state
    }
}
  1. 这里使用static主要是出于方便。
  2. 这里纠结了一段时间,之前是新建了 3 个 loading status(initial, refresh, loadmore),然后每个 status 再细分为 3 种状态(loading, loaded, error),后来发现这样的话,「当前是哪个 loading status,该 status 目前处于什么状态」判断起来会比较麻烦。于是就按照现在这样进行了拆分。
  3. 在这里对状态进行更改之后,UI 那边可以自动收到更新。
  4. 这里会调用 Repository 来获取数据。

Repository

Repository 这块由于是异步交互,因此直接就上 RxSwift 了,返回一个Observable,VM 作为消费方来订阅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Foundation
import RxSwift

class NewsFeedRepository {
    static func news(_ offset: String = "") -> Observable<[String:Any]?> {
        return Observable.create({ observer in
            let path = offset.characters.count > 0 ? "/api/4/news/before/\(offset)" : "/api/4/news/latest"
            let resource = Resource(path: path, method: .GET, requestBody: nil, headers: ["Content-Type": "application/json"], parse: decodeJSON)
            
            // 这个用的是 chris 开源的简单的 API 请求封装 http://chris.eidhof.nl/posts/tiny-networking-in-swift.html
            apiRequest(baseURL: URL(string: "https://news-at.zhihu.com")!, resource: resource, failure: { (reason, result) in
                observer.on(.error(reason))
            }, success: { result in
                observer.on(.next(result))
                observer.on(.completed)
            })
            
            return Disposables.create()
        })
    }
}

也可以在这里直接返回解析后的 Model,这样 VM 那边就不用处理了。

ViewModel 调用 Repository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class NewsFeedViewModel {
    // 4
    NewsFeedRepository.news(offset).asObservable().subscribe(onNext: {[unowned self] (result) in
        // 把 json 转换为 model
        let parsedResult = self._parseResult(result: result)
        var value = NewsFeedViewModel.news.value
        value.previousItems = NewsFeedViewModel.news.value.currentItems
        
        // 设置对应的 value
        if value.loadingType == .more {
            value.currentItems = value.previousItems + (parsedResult?.news ?? [])
        } else {
            value.currentItems = parsedResult?.news ?? []
        }
            
        value.loadingStatus = .loaded
        NewsFeedViewModel.news.value = value
        self.offset = parsedResult?.date ?? ""
        value.loadingStatus = .none
        
        // 统一设置 value,对外部 subscriber 来说就是原子操作
        NewsFeedViewModel.news.value = value
    }, onError: { (error) in
        NewsFeedViewModel.news.value.loadingStatus = .failure(error)
    }, onCompleted: {  
    }) {
    }.addDisposableTo(disposeBag)
}

这里你会注意到有一个previousItems和currentItems,这个主要是提供灵活性,避免暴力的reloadData(),比如获取到了更多的数据之后,可以只 reload 新的数据。

View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// NewsFeedViewController.swift
class NewsFeedViewController: UITableViewController {
    override func viewDidLoad() {
        handleDataChange()
        viewModel.initialLoading()
    }

    func handleDataChange() {
        NewsFeedViewModel.news.asObservable()
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: {[unowned self] item in
                if item.loadingStatus != .loading {
                    self.initialLoadingIndicator.stopAnimating()
                }
                if item.loadingStatus == .loaded {
                    // 这里调用 Diff 这个 framework 提供的 extension
                    self.tableView.animateRowChanges(oldData: item.previousItems, newData: item.currentItems)
                }
                if item.loadingType == .initial && item.loadingStatus == .loading {
                    self.initialLoadingIndicator.startAnimating()
                }
            }).addDisposableTo(disposeBag)
    }
}

「正在加载」和「已经加载」的场景已经处理完了,「加载失败」的处理也类似,比如失败之后显示一个 reload button,点击 reload button 之后,再调用一下viewModel.initialLoading()

TableView

接下来就来看看如何处理 TableView 的数据展示,其实就是消费 VM 的 property

1
2
3
4
5
6
7
8
9
10
11
12
extension NewsFeedViewController {
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return NewsFeedViewModel.news.value.currentItems.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! NewsCell
        let newsItem: NewsItem = NewsFeedViewModel.news.value.currentItems[indexPath.row]
        cell.configure(newsItem)
        return cell
    }
}

到这里最基本的首页数据展示就基本完成了。

加载更多

之前一直在纠结这块到底该怎么做才比较合适,如果直接把 newItems append 到原有的 items 列表,形成新的列表,UI 那边拿到之后就只能reloadData()了,最好能让 UI 那边知道新的和旧的之间发生了哪些变化,于是就找到了Diff这个 framework,它能够定位出两个 collection 之间的差异,但前提是 collection item 要实现Equatable协议。于是就有了previousItems和currentItems的设计。

喜欢功能

喜欢功能本质上是修改 NewsItem 的hasFaved属性,然后让 UI 可以感知到这个变化。这里问题就来了:如何对列表中的一个struct进行调整?我们知道struct是值拷贝的,只要发生赋值行为,拿到的就不再是原先的那个 struct 了(比如把 items 通过参数传递,要修改的话就要进行拷贝,除非设置为inout)。

这个问题本质上是如何操作 Immutable Objects,然后就想到了Immutable.js,它也提供了一些修改 List 的方法,只不过都是返回一个新的:

1
2
3
4
const { List } = require('immutable');
const list = List([ 0, 1, 2, List([ 3, 4 ])])
list.setIn([3, 0], 999);
// List [ 0, 1, 2, List [ 999, 4 ] ]

因此,这里简单的处理方式就是通过传进来的newsItem找到它在 list 中的 index(newsItem已经实现了Equatable协议),然后把修改过hasFaved属性的新的newsItem放到 index 位置来达到替换的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
class NewsFeedViewModel {
    func toggleFav(_ newsItem: NewsItem) {
        if let newsIndex = NewsFeedViewModel.news.value.currentItems.index(of: newsItem) {
            var _newsItem = NewsFeedViewModel.news.value.currentItems[newsIndex]
            _newsItem.hasFaved = !_newsItem.hasFaved

            var value = NewsFeedViewModel.news.value
            value.currentItems[newsIndex] = _newsItem

            NewsFeedViewModel.news.value = value
        }
    }
}

Components

由于新闻列表和喜欢的新闻列表表现上一致,那么就可以进行一些复用,比如可以把 Cell 作为 Component。

那对于一个 Component 来说,需要具备哪些特性呢?这个并没有什么约定,本质上就是一个或几个函数,外部调用后会返回一个 view,或者提供一些 block 回调,仅此而已。

Truth and Computed Properties

这里的Truth是指最源头的数据,比如一个数组,Computed Properties是指对源头数据进行消费可以得到的结果,比如数组的长度,或数组中的正数等。

在这个例子中,Truth就是newsItems列表,而喜欢的newsItems就是Computed Properties。因此只要 newsItems 发生变化,就重新计算喜欢的 NewsItems。

1
2
3
4
5
NewsFeedViewModel.news.asObservable().subscribe(onNext: { item in
    NewsFeedViewModel.favedNews.value = NewsFeedViewModel.news.value.currentItems.filter { (item) -> Bool in
         return item.hasFaved
    }
}).addDisposableTo(disposeBag)

喜欢功能的 View

主要就是两件事:

  1. 点击 Fav 按钮时,调用 VM 的toggleFav方法。
  2. 当 Fav 列表更新时,刷新 TableView。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
extension FavedViewController {
    func handleDataChange() {
        NewsFeedViewModel.favedNews.asObservable().subscribe(onNext:{[unowned self] item in
            self.tableView.reloadData()
        }).addDisposableTo(disposeBag)
    }
}

extension FavedViewController {
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return NewsFeedViewModel.favedNews.value.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! NewsCell
        var newsItem: NewsItem = NewsFeedViewModel.favedNews.value[indexPath.row]
        
        cell.configure(newsItem) { [unowned self] (button) in
            if button.tag == 0 {
                button.tag = 1
                button.setTitle("♥︎", for: .normal)
            } else {
                button.tag = 0
                button.setTitle("♡", for: .normal)
            }
            self.viewModel.toggleFav(newsItem)
            self.tableView.reloadData()
        }
        
        return cell
    }
}

页面跳转

页面间的跳转用到了Router,也就是 open 一个 url 就能到达特定的页面,这么做的好处是可以和外部跳转进来的情况统一处理(因为从外部跳到某个 app 只能通过 openURL)。

但在内部直接输入 URL 总觉得不优雅,而且容易出错,将来如果要修改 URL 也不方便。因此做了一个简单的Router来达到这个效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import Foundation
import UIKit

// 1
enum RouterTable: String {
    case home = "home"
    case detail = "detail/:id"
    
    func asController() -> UIViewController.Type {
        switch self {
        case .home:
            return NewsFeedViewController.self
        case .detail:
            return NewsDetailViewController.self
        }
    }
}

// 2
class Router {
    static func to(_ route: RouterTable, parameters: Dictionary<String, Any>?) -> Void {
        let viewController = route.asController().init()

        // 2.1
        if let parameters = parameters {
            for (key, value) in parameters {
                viewController.putExtra(key, value)
            }
        }

        //TODO: 添加 shouldBePushed 调用,比如有些页面需要先登录
        DispatchQueue.main.async {
            UINavigationController.current().pushViewController(viewController, animated: true)
        }
    }
}

// 3
extension Router {
    func parseURL(_ url: String) -> (RouterTable, Dictionary<String, String>?) {
        //TODO: add implementation
        return (.home, nil)
    }
}

主要分为 3 部分:

  1. 这个跟 vue-router 里定义 url 和 components 的关系一样,主要是为了方便统一管理。
  2. 这里主要是把 enum 转换为对应的 Controller,因为限制了类型,也就不会出现找不到 VC 的情况。
  3. 这个是用来应对外部跳转进来的 URL,把它解析成RouterTable,统一逻辑。

针对 2 重点说一下,这个是最简实现,真实场景会比这复杂得多,比如有些页面是 present 出来的,有些页面 push 前需要先判断是否登录等等。

注意到2.1的部分,这里有一个putExtra方法,这是新添加的一个扩展,参考了 Android 的IntentputExtra。实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protocol ViewCotrollerIntent {
    func putExtra(_ key: String, _ value: Any)
    func getExtra(_ key: String) -> Any?
}

extension UIViewController: ViewCotrollerIntent {
    
    private struct IntentStorage {
        static var extra: [String:Any] = [:]
    }
    
    func putExtra(_ key: String, _ value: Any) {
        IntentStorage.extra[key] = value
    }
    
    func getExtra(_ key: String) -> Any? {
        return IntentStorage.extra[key]
    }
}

由于 extension 不支持 associated properties,因此用 struct 做了个中转。这样,VC 之间的跳转如果要带上额外的参数,只要放到 extra 里即可。

详情页

详情页比较简单,只是展示一个 webview,这里比较棘手的问题是 model 数据的同步。由于详情页也可以修改NewsItem的hasFaved属性,这个改变需要能够实时同步到列表页,不然就会出现状态不同步的情况。

这块的设计也想了一段时间,Pinterest 采用的是通知的方式,并且额外开发了一个用来支持这种方式的,不想整的这么麻烦。本质需求是:当传过去的 model 发生变化时通知我。而 RxSwift 里的Variable不是正好可以达到这个效果么?于是就有了基于Variable的解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extension NewsFeedViewController {
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let newsItem: NewsItem = NewsFeedViewModel.news.value.currentItems[indexPath.row]
        let newsItemVariable = Variable<NewsItem>(newsItem)

        // 详情页可能会对这个 newsItemVariable 进行调整
        newsItemVariable.asObservable().subscribe(onNext: { [unowned self] item in
            // 找到这个 item 所在的 index,并进行替换
            self.viewModel.update(item: item)
            self.tableView.reloadData()
        }).addDisposableTo(disposeBag)

        // 带上这个 Variable 到新的 VC
        Router.to(.detail, parameters: ["model": newsItemVariable])
    }
}

详情页 View 的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class NewsDetailViewController: UIViewController {
    override func viewDidLoad() {
        // favButton
        navigationItem.rightBarButtonItem = favButton
        favButton.rx.tap
            .subscribe(onNext: { [unowned self] item in
                self.viewModel.toggleFav()
            })
            .addDisposableTo(disposeBag)
        
        // 1
        if let id = self.getExtra("id") as? Int {
            // viewModel.load(id)
        }
        
        // 2
        if let model = self.getExtra("model") as? Variable<NewsItem> {
            favButton.title = model.value.hasFaved ? "♥︎" : "♡"
            viewModel.load(Int(model.value.id))
            NewsDetailViewModel.newsItem = model
        }
        
        handleDataChange()
    }

    // 3
    func handleDataChange() {
        NewsDetailViewModel.newsDetail.asObservable()
            .subscribe(onNext:{ [unowned self] item in
                if let item = item {
                    let request = URLRequest(url: URL(string: item.shareURL)!)
                    self.webView.loadRequest(request)
                }
            })
            .addDisposableTo(disposeBag)
        
        NewsDetailViewModel.newsItem?.asObservable()
            .subscribe(onNext: { [unowned self] item in
                self.favButton.title = item.hasFaved ? "♥︎" : "♡"
            })
            .addDisposableTo(disposeBag)
    }
}
  1. 这里为通过外部 URL 进来的留一个入口。
  2. 通过getExtra拿到Variable后,接下来就交给 VM 了。
  3. handleDataChange做的事情就是响应 VM 的 properties 的变化,做一些 UI 上的调整。

Service

之前说过使用 Swift 提供 Service 会比较方便,都不需要在 App 启动时进行注册,利用自带的 Protocol Extension 就能达到效果。这个例子中没有用到,就举个其他的例子吧,以购物车为例:

1
2
3
4
5
6
7
8
9
10
11
// 放在 Services 目录下的 Protocols.swift
protocol Cart {
    public func add(_ item: Item) -> Bool
}

// 具体的实现可以放到对应的页面
extension Cart {
    public func add(_ item: Item) -> Bool {
        // business logic
    }
}

对于想要使用这个功能的开发来说,只要看Services/Protocols.swift就行了。跟 Objective-C 不同,extension 里如果有两个相同的方法,编译器会直接报错,这样就避免了运行期可能出现多个实现的问题。

Local Reasoning

Local Reasoning 的意思是对于数据的改动都发生在某一个特定的单元。这也是使用 Value Type 的好处,因为如果使用 Reference Type,只要把其中的一个 Reference 给了出去,就不知道什么时间什么场景下数据会在外部被改变,就像给了你一张银行卡,今天看还剩 1 万,可能明天再去看就只剩 1 千了。

使用 VM 后,所有对数据的改动都发生在 VM 里面,同时对数据的消费也尽量在一个地方,方便维护。

小结

以上是我自己对「Right Architecture」的一些理解和实践,实际过程中肯定还有很多细节要调整,如果你有什么想法欢迎交流~

]]>
0
<![CDATA[如何优雅的使用 Vim]]> http://www.udpwork.com/item/16316.html http://www.udpwork.com/item/16316.html#reviews Tue, 20 Jun 2017 19:14:54 +0800 skywind http://www.udpwork.com/item/16316.html 根据 Bram 前后几个关于高效使用 Vim的视频,大家每天需要花很多时间来编辑:代码、文档、邮件、日志 等等,除去这些外,还要分时间参加会议和人沟通,每个人的时间却都是不够的,优雅使用 Vim 无外乎:

  • 检测不高效的地方:你的整个工作流里,什么地方比较浪费时间?
  • 寻找一个更快的方式:官方文档,学习他人经验,自己编写 VimScript
  • 使它习惯化:开始使用,并且不断完善

以上三点反复循环,能让你的 Vim 越来越顺手。所以重点是根据自己的工作流不断迭代。而不是象大部分教程那样教你安装一大堆插件。插件都是别人写的为了解决通用需求而提炼的东西,和每个人的具体需求都有差别。上面这三点我屡试不爽,随着时间增长,有种越来越顺手的感觉,举几个我具体碰到的例子:

问题1:边开发边参考网上解决方案的问题

比如碰到问题搜到一段代码,需要试一下,一会又看会 Chrome ,一会又切回 GVim 里去写代码,反复 ALTTAB,有时候中间使用了一下资源管理器或者其他程序,ALTTAB 的顺序就会被打乱,你一切换就切跑了,十分低效。

于是我用 VimScript + 内嵌 Python 写了一个功能,按快捷键可以让 GVim 在透明/不透明两种状态间自由切换:

就是 VimScript 简单封装一个函数,里面用内嵌 Python 找到 GVim 的顶层 HWND,并设置透明度。平时默认不透明,需要参考其他资料时切换成透明,参考完了又快捷键切换回来,感觉比缘来切来切去顺畅很多。

问题2:浏览文档时的窗口滚动问题

比如你在抄写或者改写一段代码,窗口分为左右两个,左边是你引用参考的源代码,右边是你正在编辑的源代码。你抄着抄着,抄到左边最后一行了,或者你想前后看看正在引用的文本,你就需要将焦点从右边切换到左边,滚动,再切换交点回来,十分麻烦,于是撸一小段 VimScript 来解决这个问题:

" 0:up, 1:down, 2:pgup, 3:pgdown, 4:top, 5:bottom
function! Tools_PreviousCursor(mode)
    if winnr('$') <= 1
        return
    endif
    noautocmd silent! wincmd p
    if a:mode == 0
        exec "normal! \<c-y>"
    elseif a:mode == 1
        exec "normal! \<c-e>"
    elseif a:mode == 2
        exec "normal! ".winheight('.')."\<c-y>"
    elseif a:mode == 3
        exec "normal! ".winheight('.')."\<c-e>"
    elseif a:mode == 4
        normal! gg
    elseif a:mode == 5
        normal! G
    elseif a:mode == 6
        exec "normal! \<c-u>"
    elseif a:mode == 7
        exec "normal! \<c-d>"
    elseif a:mode == 8
        exec "normal! k"
    elseif a:mode == 9
        exec "normal! j"
    endif
    noautocmd silent! wincmd p
endfunc

把这个函数绑定到 ALTU, ALTD 两个按键上,你正在编辑着当前文档时,不用退出 INSERT 模式,更不用切换窗口交点,直接 ALTU, ALTD,就可以上下滚动正在参考的文档内容了,有了这个改进后,我的工作又高效了那么一点点。

同理,Quickfix 窗口经常用来查看编译错误,或者 Grep 结果,我也写了一个专门针对 Quickfix 窗口的滚屏函数,不用切焦点随时浏览 Quickfix 内容。

问题3:Tag Preview 十分低效

默认 CTRL-] 可以跳转到 Tag 定义处,但有时候我就像看一眼,并不像把当前文档给切走,也不想预览下其他文件就开一大堆文件,污染 buffer list。后来发现一个快捷键<C-W><C-]>可以打开预览窗口预览符号定义。

用了一段时间又发现两个问题:每次<c-w><c-]>如果有多处定义,vim会打印显示一大串定义位置,让你选择要看哪个,比较讨厌,而且开始预览时是默认上下分屏,现在都是宽屏显示器时代了,上下分屏十分影响观感。于是自己用 VimScript 重新撸了 Tag Preview:

需要预览时按下 “ALT+;”,右边弹出预览窗口,并且高亮符号名称,下面显示该 tag一共有 3处定义,现在正在查看第1个定义,连续按下 “ALT+;” 可以将右边预览窗口切换到下一处定义,切换到最后一个了又会折头到第一个。

配合前面的当前窗口控制其他窗口滚动的代码,编辑模式下直接 “ALT+;” 打开 tag预览并切换到你想要的定义处,然后 ALT+U, ALT+D 上下滚动预览窗口来查看前后内容,看完了 “ALT+SHIFT+;” 就可以关闭预览窗口。

从开始到结束不需要切换窗口交点,更不需要切换编辑模式,还没有<c-w><c-]>乱七八糟的列表选择过程,使用一个星期以后,彻底淘汰原生的 tag功能了,感觉自己的流畅度又提升了一些。

具体代码见:
https://github.com/skywind3000/vim/blob/master/autoload/asclib.vim#L365

后面继续迭代,有时在函数调用时候,想不起该函数的参数了,需要查看一下原型(声明和参数),不需要打开整个预览窗口查看实现,我就又开发了一个查看函数原型的 VimScript 函数,绑定到 ALT_Q 上,同样不需要退出插入模式:

按 ALTQ 可以循环显示该函数原型的多处定义,就在最下面一行,连 Preview 窗口都不需要。有的自动补全插件可以显示函数的原型,比如 YCM,但是支持的语言很少,你换个语言,YCM就不能帮你显示原型了,这时 ALTQ 来查看原型支持多达 85 种 Universal Ctags 支持的语言,基本满足我的需求,我平时主要用的编程语言大概有 4种,如今不管写什么但凡想不起定义来的时候,ALT_Q 一下就出来了,简洁明了。

问题4:代码片段

个人编码习惯,经常输入一些格式化注释:

//---------------------------------------------------------------------
// 这里是注释
//---------------------------------------------------------------------

以前基本都是复制粘贴,或者重新敲一遍那么多减号,每次都觉得有些麻烦。久而久之就想有没有更好点的方法,于是继续编写 VimScript 用来快捷键一键生成我要的这个注释框,顺畅了不少,又进一步添加了不少一键生成的文字模版,比如 copyright 信息,简单的一个带 main函数和一些 include 的代码框架。

期间发现了 UltiSnip ,是比较好用,就是比较慢,打开新文件速度慢了不少,所以我目前还是使用自己的 VimScript 实现一些简单模版的代码片段快速插入。

问题5:加速 Cygwin 开发

调试跨平台代码时,Cygwin 是一个离不开的东西,同一个功能,我一会需要用 MinGW 编译测试一下,一会又需要用 Cygwin 测试一下。传统编译 Cygwin 无外乎打开 Cygwin 的终端窗口,调用 make 或者 gcc 编译当前代码,然后又运行,窗口切来切去,十分别扭,于是开始想,有没有可能同时在 GVim 里面快捷完成 MinGW 和 Cygwin 的一键编译和运行呢?

之前比较麻烦,但是 Vim 8.0 和 NeoVim 发布后提供了后台任务机制,于是我撸了个插件 :AsyncRun:https://github.com/skywind3000/asyncrun.vim

使用 “:AsyncRun ” 来用后台运行的模式代替原先的阻塞式的 “!” 命令。

然后设置了 F7 编译 MinGW 程序:

:noremap <F7> :AsyncRun gcc "%" -o "%<" <cr> 

并设置了 F8 编译 Cygwin 程序:

:noremap <F8> :AsyncRun d:\cygwin\bin\bash --login -c "gcc % -o %<"

继续设置了其他快捷键用于打开 cmd窗口运行刚才的 MinGW程序或者 Cygwin 程序

这样不切换 Vim,不用打开 Cygwin 窗口,不用敲命令,一个快捷键,完成了 Cygwin 的编译,自此调试跨平台程序顺畅了很多。

问题6:加速 Android NDK 开发效率低

先前我在开发 Android 下 OpenSLES 的相关功能时,先要写个 NDK工程,又需要命令行编辑,然后导到 Eclipse 里面或者 Android Studio 里面,写两行 java ,做一个 gui 来调用刚才 NDK生成的 .so 动态库,十分别扭,每次改一个地方就需要折腾一半天才看得到效果,使用频率最高的:“编辑-编译-运行” 循环如果无法做到足简短紧凑的话,平时工作急不死你。

于是我开始考虑,对于大部分非 GUI的 NDK功能,有没有可能象开发调试 Windows 命令行一样的只需要写 C/C++,不需要外面套一层 java gui,并且,继续象 MinGW/Cygwin 一样的做到一键编译,一键运行呢?

于是结合前面的 AsyncRun 插件和一些少量的 Python + VimScript 脚本,实现了编译和运行:

F9 按 Android 命令行程序编译 NDK代码或者工程,并异步调用 adb 上传到设备的 /data/local/tmp/ 下设置好 755

F10 打开 cmd窗口,调用 adb 运行刚才编译的文件,然后退出。

支持模拟器和真实设备,最终 android下面整个 “编辑-编译-运行” 的工作流比以前缩短了很多,跟开发PC命令行程序一样的开发 Android NDK 程序,比起以前快多了。

类似的功能还有很多,比如玩 Prolog 时,Vim 里面写好了程序,不想每次打开 SWI-Prolog 的 GUI窗口,手动在那里加载半天,程序修改完又要在 SWI-Prolog 里敲一堆命令来复位加载运行,后来也直接集成了一个快捷键完成这些事情。

不用换一个语言就换一个 编辑器/IDE 那么麻烦,那么多不同语言不同平台的开发任务,全部都用 Vim 解决了,还跟原生的一样。

问题7:高效的 Grep

在工程中查找字符串,对于特定语言使用 gtags / cscope 之外,对于通用语言,传统的 grep 用的也比较多。传统 vim 内 grep 不支持异步, 需要等待 Grep 结束后才能返回,并没有很好的跟进 Vim8/ NeoVim 的异步机制,于是用我前面前面定义的 AsyncRun 插件来配合使用:

:AsyncRun! grep -R word . 
:AsyncRun! grep -R <cword> . 

其中<cword>的意思是当前光标下的符号,再把该功能绑定到快捷键上,一键即可在当前目录下 Grep 光标下面的符号,结果实时输出到 Quickfix 窗口,双击 Quickfix 的具体输出就可以打开对应文件跳转到对应行号。

问题8:日常小工具

之前学习做爬虫时,经常从网上复制一些 html 片段,都是挤在一起的,需要格式化后查看的更清楚一点,类似这种小功能,基本懒得再去找专门的格式化工具,三分钟几行 VimScript 就搞定了:

function! asclib#html_prettify()
    if &ft != 'html'
        echo "not a html file"
        return
    endif
    silent! exec "s/<[^>]*>/\r&\r/g"
    silent! exec "g/^$/d"
    exec "normal ggVG="
endfunc

将该函数绑定到快捷键上后,粘贴 html 到 vim 里一键就完成了格式化工作。我在 Vim 里写了大量类似的小工具,比如GTAGS支持(GTAGS官方插件太难用,又不支持Vim8的异步模式),比如一键 touch 当前项目 wsgi 的主文件,源代码头文件快速切换(有一个类似插件,但是我嫌它太难用了),快速打开 Zeal 查看 Dash 文档。。。。。

这些都是围绕自己工作习惯来优化的,这些具体的问题都不是拼凑通用插件能解决的。编写 VimScript 是一件值得花时间学习的技巧,当你熟练掌握后,你就脱开了各种插件的束缚,能够让你的 Vim 日常使用得以不断的进化再进化,这远远不是堆砌插件所能比的效果。

回顾一下:

磨刀不误砍柴工,熟练掌握 VimScript,熟读官方文档的情况下,坚持:“发现问题,找到更高效的方法,习惯化” 的三个循环,让我工作流在不同地方都变的优雅和流畅了那么一点点,日积月累,流畅的编辑体验让身心愉悦。

其他编辑器/IDE的效率是常数,上手以后效率就很难提升了,Vim效率是变量,越调教越顺手。它的反面,低效的习惯是:

  • 你已经学会了编辑器的基本使用方法,却不花间去学习新的技巧的话,将永远停留在最原始的使用方式,得不到提高。
  • 你想要学习一切技巧,让自己每个步奏都用最优方式解决的话,你又会浪费大量时间,学习一大堆永远用不上的东西。
  • 摆弄各种插件,插件能做的可以做一下,插件不能做的或者做的不太好的,束手无策。

更多参考:

《Practical Vim》,推荐学完 vimtutor 开始使用时阅读。

《Learn Vimscript the Hard Way》,VimScript 上手必备。

官方必读1:h index (Vim 中所有默认键位说明)

官方必读2:h quickref(速查手册)

扩展你的键位:《Vim 中正确使用 Alt映射

超脱堆插件的误区,根据你的日常工作情况,不断打磨编辑器的利爪吧。

]]>
根据 Bram 前后几个关于高效使用 Vim的视频,大家每天需要花很多时间来编辑:代码、文档、邮件、日志 等等,除去这些外,还要分时间参加会议和人沟通,每个人的时间却都是不够的,优雅使用 Vim 无外乎:

  • 检测不高效的地方:你的整个工作流里,什么地方比较浪费时间?
  • 寻找一个更快的方式:官方文档,学习他人经验,自己编写 VimScript
  • 使它习惯化:开始使用,并且不断完善

以上三点反复循环,能让你的 Vim 越来越顺手。所以重点是根据自己的工作流不断迭代。而不是象大部分教程那样教你安装一大堆插件。插件都是别人写的为了解决通用需求而提炼的东西,和每个人的具体需求都有差别。上面这三点我屡试不爽,随着时间增长,有种越来越顺手的感觉,举几个我具体碰到的例子:

问题1:边开发边参考网上解决方案的问题

比如碰到问题搜到一段代码,需要试一下,一会又看会 Chrome ,一会又切回 GVim 里去写代码,反复 ALTTAB,有时候中间使用了一下资源管理器或者其他程序,ALTTAB 的顺序就会被打乱,你一切换就切跑了,十分低效。

于是我用 VimScript + 内嵌 Python 写了一个功能,按快捷键可以让 GVim 在透明/不透明两种状态间自由切换:

就是 VimScript 简单封装一个函数,里面用内嵌 Python 找到 GVim 的顶层 HWND,并设置透明度。平时默认不透明,需要参考其他资料时切换成透明,参考完了又快捷键切换回来,感觉比缘来切来切去顺畅很多。

问题2:浏览文档时的窗口滚动问题

比如你在抄写或者改写一段代码,窗口分为左右两个,左边是你引用参考的源代码,右边是你正在编辑的源代码。你抄着抄着,抄到左边最后一行了,或者你想前后看看正在引用的文本,你就需要将焦点从右边切换到左边,滚动,再切换交点回来,十分麻烦,于是撸一小段 VimScript 来解决这个问题:

" 0:up, 1:down, 2:pgup, 3:pgdown, 4:top, 5:bottom
function! Tools_PreviousCursor(mode)
    if winnr('$') <= 1
        return
    endif
    noautocmd silent! wincmd p
    if a:mode == 0
        exec "normal! \<c-y>"
    elseif a:mode == 1
        exec "normal! \<c-e>"
    elseif a:mode == 2
        exec "normal! ".winheight('.')."\<c-y>"
    elseif a:mode == 3
        exec "normal! ".winheight('.')."\<c-e>"
    elseif a:mode == 4
        normal! gg
    elseif a:mode == 5
        normal! G
    elseif a:mode == 6
        exec "normal! \<c-u>"
    elseif a:mode == 7
        exec "normal! \<c-d>"
    elseif a:mode == 8
        exec "normal! k"
    elseif a:mode == 9
        exec "normal! j"
    endif
    noautocmd silent! wincmd p
endfunc

把这个函数绑定到 ALTU, ALTD 两个按键上,你正在编辑着当前文档时,不用退出 INSERT 模式,更不用切换窗口交点,直接 ALTU, ALTD,就可以上下滚动正在参考的文档内容了,有了这个改进后,我的工作又高效了那么一点点。

同理,Quickfix 窗口经常用来查看编译错误,或者 Grep 结果,我也写了一个专门针对 Quickfix 窗口的滚屏函数,不用切焦点随时浏览 Quickfix 内容。

问题3:Tag Preview 十分低效

默认 CTRL-] 可以跳转到 Tag 定义处,但有时候我就像看一眼,并不像把当前文档给切走,也不想预览下其他文件就开一大堆文件,污染 buffer list。后来发现一个快捷键<C-W><C-]>可以打开预览窗口预览符号定义。

用了一段时间又发现两个问题:每次<c-w><c-]>如果有多处定义,vim会打印显示一大串定义位置,让你选择要看哪个,比较讨厌,而且开始预览时是默认上下分屏,现在都是宽屏显示器时代了,上下分屏十分影响观感。于是自己用 VimScript 重新撸了 Tag Preview:

需要预览时按下 “ALT+;”,右边弹出预览窗口,并且高亮符号名称,下面显示该 tag一共有 3处定义,现在正在查看第1个定义,连续按下 “ALT+;” 可以将右边预览窗口切换到下一处定义,切换到最后一个了又会折头到第一个。

配合前面的当前窗口控制其他窗口滚动的代码,编辑模式下直接 “ALT+;” 打开 tag预览并切换到你想要的定义处,然后 ALT+U, ALT+D 上下滚动预览窗口来查看前后内容,看完了 “ALT+SHIFT+;” 就可以关闭预览窗口。

从开始到结束不需要切换窗口交点,更不需要切换编辑模式,还没有<c-w><c-]>乱七八糟的列表选择过程,使用一个星期以后,彻底淘汰原生的 tag功能了,感觉自己的流畅度又提升了一些。

具体代码见:
https://github.com/skywind3000/vim/blob/master/autoload/asclib.vim#L365

后面继续迭代,有时在函数调用时候,想不起该函数的参数了,需要查看一下原型(声明和参数),不需要打开整个预览窗口查看实现,我就又开发了一个查看函数原型的 VimScript 函数,绑定到 ALT_Q 上,同样不需要退出插入模式:

按 ALTQ 可以循环显示该函数原型的多处定义,就在最下面一行,连 Preview 窗口都不需要。有的自动补全插件可以显示函数的原型,比如 YCM,但是支持的语言很少,你换个语言,YCM就不能帮你显示原型了,这时 ALTQ 来查看原型支持多达 85 种 Universal Ctags 支持的语言,基本满足我的需求,我平时主要用的编程语言大概有 4种,如今不管写什么但凡想不起定义来的时候,ALT_Q 一下就出来了,简洁明了。

问题4:代码片段

个人编码习惯,经常输入一些格式化注释:

//---------------------------------------------------------------------
// 这里是注释
//---------------------------------------------------------------------

以前基本都是复制粘贴,或者重新敲一遍那么多减号,每次都觉得有些麻烦。久而久之就想有没有更好点的方法,于是继续编写 VimScript 用来快捷键一键生成我要的这个注释框,顺畅了不少,又进一步添加了不少一键生成的文字模版,比如 copyright 信息,简单的一个带 main函数和一些 include 的代码框架。

期间发现了 UltiSnip ,是比较好用,就是比较慢,打开新文件速度慢了不少,所以我目前还是使用自己的 VimScript 实现一些简单模版的代码片段快速插入。

问题5:加速 Cygwin 开发

调试跨平台代码时,Cygwin 是一个离不开的东西,同一个功能,我一会需要用 MinGW 编译测试一下,一会又需要用 Cygwin 测试一下。传统编译 Cygwin 无外乎打开 Cygwin 的终端窗口,调用 make 或者 gcc 编译当前代码,然后又运行,窗口切来切去,十分别扭,于是开始想,有没有可能同时在 GVim 里面快捷完成 MinGW 和 Cygwin 的一键编译和运行呢?

之前比较麻烦,但是 Vim 8.0 和 NeoVim 发布后提供了后台任务机制,于是我撸了个插件 :AsyncRun:https://github.com/skywind3000/asyncrun.vim

使用 “:AsyncRun ” 来用后台运行的模式代替原先的阻塞式的 “!” 命令。

然后设置了 F7 编译 MinGW 程序:

:noremap <F7> :AsyncRun gcc "%" -o "%<" <cr> 

并设置了 F8 编译 Cygwin 程序:

:noremap <F8> :AsyncRun d:\cygwin\bin\bash --login -c "gcc % -o %<"

继续设置了其他快捷键用于打开 cmd窗口运行刚才的 MinGW程序或者 Cygwin 程序

这样不切换 Vim,不用打开 Cygwin 窗口,不用敲命令,一个快捷键,完成了 Cygwin 的编译,自此调试跨平台程序顺畅了很多。

问题6:加速 Android NDK 开发效率低

先前我在开发 Android 下 OpenSLES 的相关功能时,先要写个 NDK工程,又需要命令行编辑,然后导到 Eclipse 里面或者 Android Studio 里面,写两行 java ,做一个 gui 来调用刚才 NDK生成的 .so 动态库,十分别扭,每次改一个地方就需要折腾一半天才看得到效果,使用频率最高的:“编辑-编译-运行” 循环如果无法做到足简短紧凑的话,平时工作急不死你。

于是我开始考虑,对于大部分非 GUI的 NDK功能,有没有可能象开发调试 Windows 命令行一样的只需要写 C/C++,不需要外面套一层 java gui,并且,继续象 MinGW/Cygwin 一样的做到一键编译,一键运行呢?

于是结合前面的 AsyncRun 插件和一些少量的 Python + VimScript 脚本,实现了编译和运行:

F9 按 Android 命令行程序编译 NDK代码或者工程,并异步调用 adb 上传到设备的 /data/local/tmp/ 下设置好 755

F10 打开 cmd窗口,调用 adb 运行刚才编译的文件,然后退出。

支持模拟器和真实设备,最终 android下面整个 “编辑-编译-运行” 的工作流比以前缩短了很多,跟开发PC命令行程序一样的开发 Android NDK 程序,比起以前快多了。

类似的功能还有很多,比如玩 Prolog 时,Vim 里面写好了程序,不想每次打开 SWI-Prolog 的 GUI窗口,手动在那里加载半天,程序修改完又要在 SWI-Prolog 里敲一堆命令来复位加载运行,后来也直接集成了一个快捷键完成这些事情。

不用换一个语言就换一个 编辑器/IDE 那么麻烦,那么多不同语言不同平台的开发任务,全部都用 Vim 解决了,还跟原生的一样。

问题7:高效的 Grep

在工程中查找字符串,对于特定语言使用 gtags / cscope 之外,对于通用语言,传统的 grep 用的也比较多。传统 vim 内 grep 不支持异步, 需要等待 Grep 结束后才能返回,并没有很好的跟进 Vim8/ NeoVim 的异步机制,于是用我前面前面定义的 AsyncRun 插件来配合使用:

:AsyncRun! grep -R word . 
:AsyncRun! grep -R <cword> . 

其中<cword>的意思是当前光标下的符号,再把该功能绑定到快捷键上,一键即可在当前目录下 Grep 光标下面的符号,结果实时输出到 Quickfix 窗口,双击 Quickfix 的具体输出就可以打开对应文件跳转到对应行号。

问题8:日常小工具

之前学习做爬虫时,经常从网上复制一些 html 片段,都是挤在一起的,需要格式化后查看的更清楚一点,类似这种小功能,基本懒得再去找专门的格式化工具,三分钟几行 VimScript 就搞定了:

function! asclib#html_prettify()
    if &ft != 'html'
        echo "not a html file"
        return
    endif
    silent! exec "s/<[^>]*>/\r&\r/g"
    silent! exec "g/^$/d"
    exec "normal ggVG="
endfunc

将该函数绑定到快捷键上后,粘贴 html 到 vim 里一键就完成了格式化工作。我在 Vim 里写了大量类似的小工具,比如GTAGS支持(GTAGS官方插件太难用,又不支持Vim8的异步模式),比如一键 touch 当前项目 wsgi 的主文件,源代码头文件快速切换(有一个类似插件,但是我嫌它太难用了),快速打开 Zeal 查看 Dash 文档。。。。。

这些都是围绕自己工作习惯来优化的,这些具体的问题都不是拼凑通用插件能解决的。编写 VimScript 是一件值得花时间学习的技巧,当你熟练掌握后,你就脱开了各种插件的束缚,能够让你的 Vim 日常使用得以不断的进化再进化,这远远不是堆砌插件所能比的效果。

回顾一下:

磨刀不误砍柴工,熟练掌握 VimScript,熟读官方文档的情况下,坚持:“发现问题,找到更高效的方法,习惯化” 的三个循环,让我工作流在不同地方都变的优雅和流畅了那么一点点,日积月累,流畅的编辑体验让身心愉悦。

其他编辑器/IDE的效率是常数,上手以后效率就很难提升了,Vim效率是变量,越调教越顺手。它的反面,低效的习惯是:

  • 你已经学会了编辑器的基本使用方法,却不花间去学习新的技巧的话,将永远停留在最原始的使用方式,得不到提高。
  • 你想要学习一切技巧,让自己每个步奏都用最优方式解决的话,你又会浪费大量时间,学习一大堆永远用不上的东西。
  • 摆弄各种插件,插件能做的可以做一下,插件不能做的或者做的不太好的,束手无策。

更多参考:

《Practical Vim》,推荐学完 vimtutor 开始使用时阅读。

《Learn Vimscript the Hard Way》,VimScript 上手必备。

官方必读1:h index (Vim 中所有默认键位说明)

官方必读2:h quickref(速查手册)

扩展你的键位:《Vim 中正确使用 Alt映射

超脱堆插件的误区,根据你的日常工作情况,不断打磨编辑器的利爪吧。

]]>
0
<![CDATA[MacBook Pro 2013 款和 2017 款简单性能对比测试]]> http://www.udpwork.com/item/16314.html http://www.udpwork.com/item/16314.html#reviews Tue, 20 Jun 2017 14:38:38 +0800 图拉鼎 http://www.udpwork.com/item/16314.html 今天我购买的 2017 新款 MacBook Pro 到了,换下了我用了近四年的 2013 Late 那款 MacBook Pro。

配置对比

我那台 2013 款 MacBook Pro 是一台定制机,在中配版本的基础上将内存定制到了 16G(记得当时 8G 是默认的)。如下图:

MacBook Pro 17

而我这次买的是 MacBook Pro 高配版,没做其他定制。主要原因是:这次内存定制居然不在可选范围里,最高就只有 16G 内存。有点小失望。

MacBook Pro 13

无论如何,电脑是买了,从性能上来看,两台电脑主要是异同是:

CPU:都是四核,但 2.0 GHz vs 2.9 GHz

内存:都是 16G,但频率 1600 MHz vs 2133 MHz

SSD:256G vs 512G。

这几年大家都吐槽 Intel 的 CPU 像挤牙膏一年挤性能,提升不大。本来我也没有更新电脑的动力,毕竟用了快四年了,硬盘空间常常不足,也对 TouchBar 和 TrackPad 比较好奇,于是就趁这次 WWDC 结束后就立即打算换了。

那么,新款的 MacBook Pro 到底有没有性能提升呢?光看主频当然是提升不少,实际应用如何?于是我打算对比新旧电脑,用 Xcode 编译我的「奇点」Swift 项目得出一个实际生产中的数据来说话。

测试过程

开启 Xcode 的显示编译时间的参数功能,编译项目,每次编译前按 Shift + Option + CMD + K 来进行一个 Clean Build,连续编译五次看看结果。

Xcode Clean Build

测试结果

新的 MacBook Pro 的五次测试编译的结果是:

  • 66.623
  • 64.277
  • 64.186
  • 69.685
  • 64.902

平均值:65.9346

而旧的 MacBook Pro 的五次测试编译的结果是:

  • 81.894
  • 81.709
  • 81.204
  • 81.628
  • 87.440

平均值:82.885。

一计算,新款比旧款性能提升幅度刚好 20%。

这个测试结果,相信在越大的项目上、越复杂的项目上效果越明显。

当然这只仅仅是 Xcode 编译项目的一个对比测试,只能给出在这一个环境下,新的比旧的提升 20% 的性能。其他的比如 SSD 性能、内存性能,甚至更考验综合性能的视频编码等待动作我都没做。对我来说,这个应用场景已经足够是我升级的理由了。

总结

那么,这台新款的 MacBook Pro 到底值不值得购买呢?我的意见是:

假如你用的是 2015 款和 2016 款的,倒不用急着升级,提升应该有限,在此之前的,可以严肃考虑升级,毕竟新的 TouchBar 和带压感的 TrackPad 都值得体验。

很多人都吐槽新 MacBook Pro 的键盘,说它的键程超短。在我看来这是一个习惯的问题。

我在去年刚刚开始用 Magic Keyboard 的时候也觉得这个键盘键程太短,刚开始用的时候还经常打错字,用了一段时间后就适应而且喜欢上了,喜欢上这种非常省力而且轻巧的感觉。所以今天我用这台新电脑码了一些字,也是这个感觉。一点点键程的变化,带来更轻松的敲击感。当然如果你是机械键盘的用户,你得严肃考虑一下要不要用自带的键盘,我觉得这是两个东西——就像电动车就汽油车一样。

最后,期望我在新电脑上拥有更好的生产力吧!

本站架设于Linode 东京机房,同时使用云梯进行科学上网

]]>
今天我购买的 2017 新款 MacBook Pro 到了,换下了我用了近四年的 2013 Late 那款 MacBook Pro。

配置对比

我那台 2013 款 MacBook Pro 是一台定制机,在中配版本的基础上将内存定制到了 16G(记得当时 8G 是默认的)。如下图:

MacBook Pro 17

而我这次买的是 MacBook Pro 高配版,没做其他定制。主要原因是:这次内存定制居然不在可选范围里,最高就只有 16G 内存。有点小失望。

MacBook Pro 13

无论如何,电脑是买了,从性能上来看,两台电脑主要是异同是:

CPU:都是四核,但 2.0 GHz vs 2.9 GHz

内存:都是 16G,但频率 1600 MHz vs 2133 MHz

SSD:256G vs 512G。

这几年大家都吐槽 Intel 的 CPU 像挤牙膏一年挤性能,提升不大。本来我也没有更新电脑的动力,毕竟用了快四年了,硬盘空间常常不足,也对 TouchBar 和 TrackPad 比较好奇,于是就趁这次 WWDC 结束后就立即打算换了。

那么,新款的 MacBook Pro 到底有没有性能提升呢?光看主频当然是提升不少,实际应用如何?于是我打算对比新旧电脑,用 Xcode 编译我的「奇点」Swift 项目得出一个实际生产中的数据来说话。

测试过程

开启 Xcode 的显示编译时间的参数功能,编译项目,每次编译前按 Shift + Option + CMD + K 来进行一个 Clean Build,连续编译五次看看结果。

Xcode Clean Build

测试结果

新的 MacBook Pro 的五次测试编译的结果是:

  • 66.623
  • 64.277
  • 64.186
  • 69.685
  • 64.902

平均值:65.9346

而旧的 MacBook Pro 的五次测试编译的结果是:

  • 81.894
  • 81.709
  • 81.204
  • 81.628
  • 87.440

平均值:82.885。

一计算,新款比旧款性能提升幅度刚好 20%。

这个测试结果,相信在越大的项目上、越复杂的项目上效果越明显。

当然这只仅仅是 Xcode 编译项目的一个对比测试,只能给出在这一个环境下,新的比旧的提升 20% 的性能。其他的比如 SSD 性能、内存性能,甚至更考验综合性能的视频编码等待动作我都没做。对我来说,这个应用场景已经足够是我升级的理由了。

总结

那么,这台新款的 MacBook Pro 到底值不值得购买呢?我的意见是:

假如你用的是 2015 款和 2016 款的,倒不用急着升级,提升应该有限,在此之前的,可以严肃考虑升级,毕竟新的 TouchBar 和带压感的 TrackPad 都值得体验。

很多人都吐槽新 MacBook Pro 的键盘,说它的键程超短。在我看来这是一个习惯的问题。

我在去年刚刚开始用 Magic Keyboard 的时候也觉得这个键盘键程太短,刚开始用的时候还经常打错字,用了一段时间后就适应而且喜欢上了,喜欢上这种非常省力而且轻巧的感觉。所以今天我用这台新电脑码了一些字,也是这个感觉。一点点键程的变化,带来更轻松的敲击感。当然如果你是机械键盘的用户,你得严肃考虑一下要不要用自带的键盘,我觉得这是两个东西——就像电动车就汽油车一样。

最后,期望我在新电脑上拥有更好的生产力吧!

本站架设于Linode 东京机房,同时使用云梯进行科学上网

]]>
0
<![CDATA[Go 1.9 的新特性]]> http://www.udpwork.com/item/16315.html http://www.udpwork.com/item/16315.html#reviews Tue, 20 Jun 2017 14:24:06 +0800 鸟窝 http://www.udpwork.com/item/16315.html 现在 Go 1.9 beta版已发布, 正式版预期在8月初发布,让我们先来看看你Go 1.9带来了那些新特性。

type alias

类型别名原本在1.8中加入的,但是临时发现有些问题,为了能全面的设计type alias被移到了 Go 1.9中了。

这个特性主要用在类型从一个package移动到另外一个package中的时候,导致的项目中对引入的路径不一致导致的问题, 比如原先context是在golang.org/x/net/context包下,在Go 1.7中菜正式移到标准库context。

相关的issue:go#16339go#18130
提案:type alias

并发map

在Go 1.6之前, 内置的map类型是部分goroutine安全的,并发的读没有问题,并发的写可能有问题。自go 1.6之后, 并发地读写map会报错,这在一些知名的开源库中都存在这个问题,所以go 1.9之前的解决方案是额外绑定一个锁,封装成一个新的struct或者单独使用锁都可以。

群众的呼声是响亮的,并发map在项目中大量使用,所以Go 1.9中在包sync加入了新的map, 查询、存储和删除都是平均常数时间,可以并发访问。

Monotonic Time

先前的time包的实现都是基于wall time的,但是当机器的时钟调整后会有问题。 比如在计算duration的时候,如果时钟往回拨,可能导致end时间比start时间还早。

所以Go 1.9使用monotonic Time来实现大部分的time中的函数,在计算duration的时候不会出现因为时钟调整出现的误差了。

设计文档:monotonic time

位处理操作

新增加了math/bits包, 提供了很多位运算的函数。

Test Helper函数

新加`(T).Helper和(B).Helper m`, 用来标记调用的函数是一个测试辅助函数,当输出文件名和行数的时候,这个函数回呗忽略。

标准库的微小改动

标准库也有一些小的功能的加入和提升, 比如image、net、runtime、sync等。

并行编译

支持并行地编译函数,并且在Go 1.9中势默认设置。如果不想并行编译,设置GO19CONCURRENTCOMPILATION为0。

./... 会忽略vendor下的包

这一条很有用,以后你在Makefile中可以直接使用./...,而不是曲折地将vendor文件夹排除。

如果你想使用vendor下的包, 可以使用./vendor/...通配符。

性能提升

性能提升多少势很难精确描述的,对于大部分的程序,应该运行更快一点。

主要在于垃圾回收器的优化、更好的生成的代码以及核心库的优化。

完整的信息可以参考Tip Go 1.9 Release Notes, Go 1.9发布后可以访问Go 1.9 Release Notes

]]>
现在 Go 1.9 beta版已发布, 正式版预期在8月初发布,让我们先来看看你Go 1.9带来了那些新特性。

type alias

类型别名原本在1.8中加入的,但是临时发现有些问题,为了能全面的设计type alias被移到了 Go 1.9中了。

这个特性主要用在类型从一个package移动到另外一个package中的时候,导致的项目中对引入的路径不一致导致的问题, 比如原先context是在golang.org/x/net/context包下,在Go 1.7中菜正式移到标准库context。

相关的issue:go#16339go#18130
提案:type alias

并发map

在Go 1.6之前, 内置的map类型是部分goroutine安全的,并发的读没有问题,并发的写可能有问题。自go 1.6之后, 并发地读写map会报错,这在一些知名的开源库中都存在这个问题,所以go 1.9之前的解决方案是额外绑定一个锁,封装成一个新的struct或者单独使用锁都可以。

群众的呼声是响亮的,并发map在项目中大量使用,所以Go 1.9中在包sync加入了新的map, 查询、存储和删除都是平均常数时间,可以并发访问。

Monotonic Time

先前的time包的实现都是基于wall time的,但是当机器的时钟调整后会有问题。 比如在计算duration的时候,如果时钟往回拨,可能导致end时间比start时间还早。

所以Go 1.9使用monotonic Time来实现大部分的time中的函数,在计算duration的时候不会出现因为时钟调整出现的误差了。

设计文档:monotonic time

位处理操作

新增加了math/bits包, 提供了很多位运算的函数。

Test Helper函数

新加`(T).Helper和(B).Helper m`, 用来标记调用的函数是一个测试辅助函数,当输出文件名和行数的时候,这个函数回呗忽略。

标准库的微小改动

标准库也有一些小的功能的加入和提升, 比如image、net、runtime、sync等。

并行编译

支持并行地编译函数,并且在Go 1.9中势默认设置。如果不想并行编译,设置GO19CONCURRENTCOMPILATION为0。

./... 会忽略vendor下的包

这一条很有用,以后你在Makefile中可以直接使用./...,而不是曲折地将vendor文件夹排除。

如果你想使用vendor下的包, 可以使用./vendor/...通配符。

性能提升

性能提升多少势很难精确描述的,对于大部分的程序,应该运行更快一点。

主要在于垃圾回收器的优化、更好的生成的代码以及核心库的优化。

完整的信息可以参考Tip Go 1.9 Release Notes, Go 1.9发布后可以访问Go 1.9 Release Notes

]]>
0
<![CDATA[在Java中调用v4l2]]> http://www.udpwork.com/item/16313.html http://www.udpwork.com/item/16313.html#reviews Tue, 20 Jun 2017 12:00:00 +0800 wendal http://www.udpwork.com/item/16313.html 前言

V4L2, linux下视频采集的事实标准, 通常的设备路径就是

/dev/video0
/dev/video1
....
/dev/videoX

其Java库,自然只能是JNI库https://github.com/sarxos/v4l4j这个库似乎已经停止维护,但v4l2的接口本身很稳定,没有更新的必要吧

添加maven/gradle依赖

不知道从哪个时间点开始,我本地的项目大多转成maven项目

<dependency>
    <groupId>com.github.sarxos</groupId>
    <artifactId>v4l4j</artifactId>
    <version>0.9.1-r507</version>
</dependency>
compile group: 'com.github.sarxos', name: 'v4l4j', version: '0.9.1-r507'

初始化本地库

v4l4j这个项目只提供了 linux x64和linux arm的二进制库, 其他系统需要自行编译了

so文件在v4l4j-0.9.1-r507.jar里面可以找到, 路径是 META-INF/native/linux-64

有两种加载方式,二选一就行.

// 标准做法
V4L4J.init();
// 不知名做法, 新版JDK支持指定路径, 貌似是JDK7开始的吧
System.load("/usr/local/lib/libvideo.so");
System.load("/usr/local/lib/libv4l4j.so");

开启设备,检查设备特性

基本流程, 创建设备,获取DeviceInfo, 检测是否兼容特定的特性.

很自然的,下面的代码当然用到了nutz相关的类

    private static final Log log = Logs.get();

    protected VideoDevice device;
    
    protected RGBFrameGrabber grabber;
    
    public void checkV4L4() {
        try {
            device = new VideoDevice("/dev/video0"); // 通常就是第0个设备
            DeviceInfo dinfo = device.getDeviceInfo();
            log.info("name=" + dinfo.getName());
            log.info("DeviceFile=" + dinfo.getDeviceFile());
            ImageFormatList ifl = dinfo.getFormatList();
            int w = 1920, h = 1080;
            if (ifl != null) {
                // 获取是原生支持的格式, 还有BGR/YVU/YUV等等,貌似我用不上,不打印了,反正触类旁通
                List<ImageFormat> natives = ifl.getNativeFormats();
                if (natives != null) {
                    for (ImageFormat format : natives) { 
                        // 理论上说支持多种分辨率,尤其是摄像头,但我这个采集卡只输出一种分辨率
                        log.info("native format = " + format.toNiceString());
                        w = format.getResolutionInfo().getDiscreteResolutions().get(0).width;
                        h = format.getResolutionInfo().getDiscreteResolutions().get(0).height;
                        log.infof("w=%d,h=%d", w, h);
                    }
                }
            }
            // 是否支持 BGR/JPEG/RGB/YUV/YVU转换,这些都非常重要, 最起码有一个会是true,通常是全部为true,取决是设备和linux内核版本.
            log.info("supportBGRConversion=" + device.supportBGRConversion());
            log.info("supportJPEGConversion=" + device.supportJPEGConversion());
            log.info("supportRGBConversion=" + device.supportRGBConversion());
            log.info("supportYUVConversion=" + device.supportYUVConversion());
            log.info("supportYVUConversion=" + device.supportYVUConversion());
            
            // 操作完成,释放设备. 但如果要开始捕捉,这一步必须去掉.
            device.release();
        }
        catch (V4L4JException e) {
            log.info("OhOhOh", e);
            device = null;
        }

通过v4l2-ctl获取更新信息

通过v4l4j的api能输出大部分必要的信息,但设备信息是拿不到的.

下面是执行v4l2-ctl –all输出的内容,中文注释是我手动加上的…

root@danoo-desktop:/dev/shm# v4l2-ctl --all
Driver Info (not using libv4l2):
        Driver name   : LINUXV4L2
        Card type     : PL330B:RAW 00.00 a0011af2
        Bus info      : PCIe: PCI Bus 0000:01
        Driver version: 3.12.0
        Capabilities  : 0x84221001
                Video Capture
                Video Capture Multiplanar
                Audio
                Streaming
                Device Capabilities
        Device Caps   : 0x04220001
                Video Capture
                Audio
                Streaming
Priority: 0
Video input : 3 (DVI-A INPUT(3): ok) ## 从0 - 7 代表各种接口. 其中3代表VGA(虽然名字里面有DVI)
Audio input : 0 () ## 木有音频输入.
Video Standard = 0x00001000
        NTSC-M     # PAL还是NTSC呢? 这里给出了答案
Format Video Capture:
        Width/Height  : 1920/1080 # 分辨率数据
        Pixel Format  : 'YV12'    # 原始像素格式
        Field         : Interlaced
        Bytes per Line: 1920
        Size Image    : 3110400
        Colorspace    : HDTV and modern devices (ITU709)
        Custom Info   : feedcafe
Crop Capability Video Capture:
        Bounds      : Left 0, Top 0, Width 1920, Height 1080
        Default     : Left 0, Top 0, Width 1920, Height 1080
        Pixel Aspect: 1/1
Streaming Parameters Video Capture:
        Capabilities     : timeperframe
        Frames per second: 60.000 (60000/1000) # 刷新率
        Read buffers     : 0
                     brightness (int)    : min=0 max=255 step=1 default=128 value=128
                       contrast (int)    : min=0 max=255 step=1 default=128 value=128
                     saturation (int)    : min=0 max=255 step=1 default=128 value=128
                            hue (int)    : min=0 max=255 step=1 default=128 value=128
                      auto_gain (int)    : min=0 max=1 step=1 default=1 value=1
                      sharpness (int)    : min=0 max=255 step=1 default=128 value=128

开始进行视频捕捉

            # 首先,需要确定哪种FrameGrabber可用(supportXXXConversion是否为true),然后选一种合适的. 对我来说就是RGBFrameGrabber
            # 然后,选择合适的输入设备, 我这里选3, VGA
            # 再然后, 选择NTSC, 取决于v4l2-ctl所显示的值. 我猜从DeviceInfo应该也能查到吧,没研究.
            grabber = device.getRGBFrameGrabber(w, h, 3, V4L4JConstants.STANDARD_NTSC);
            # 设置视频帧回调
            grabber.setCaptureCallback(new CaptureCallback() {
                public void nextFrame(VideoFrame frame) {
                    //log.info("frame income !!!" + frame.getFrameLength()); // 不晓得为啥,我这里总是0 ,但图片的确有了
                    try {
                        final BufferedImage image = frame.getBufferedImage();
                        # 做爱做的事吧... 如果是耗时操作,建议用线程池来执行,而不是在这个线程内完成
                    } catch (Throwable e) {
                        log.debug("BufferedImage FAIL..."  + e);
                    }
                    // 把当前帧回收,非常非常非常重要
                    frame.recycle();
                }
                public void exceptionReceived(V4L4JException e) {
                    log.info("something happen!!!", e);
                }
            });
            // 来吧, 开始趴体...
            grabber.startCapture();
            log.info("party start!!");
            // 趴体永不结束...
            Lang.quiteSleep(3600000);

性能

在我所使用的设备

"Intel(R) Celeron(R) CPU 1037U"
圆刚mini PICE采集卡

使用vga接口,采集1920x1080的彩色图像数据,能达到30fps左右, cpu占用率40%.

可能的原因:

  • 原生YV12数据,转为RGB数据需要一些计算量
  • 每帧图像需要8mb内存(BufferedImage),每秒30帧,该图像数据用完就扔, 相当于每秒产生240mb的可GC数据
]]>
前言

V4L2, linux下视频采集的事实标准, 通常的设备路径就是

/dev/video0
/dev/video1
....
/dev/videoX

其Java库,自然只能是JNI库https://github.com/sarxos/v4l4j这个库似乎已经停止维护,但v4l2的接口本身很稳定,没有更新的必要吧

添加maven/gradle依赖

不知道从哪个时间点开始,我本地的项目大多转成maven项目

<dependency>
    <groupId>com.github.sarxos</groupId>
    <artifactId>v4l4j</artifactId>
    <version>0.9.1-r507</version>
</dependency>
compile group: 'com.github.sarxos', name: 'v4l4j', version: '0.9.1-r507'

初始化本地库

v4l4j这个项目只提供了 linux x64和linux arm的二进制库, 其他系统需要自行编译了

so文件在v4l4j-0.9.1-r507.jar里面可以找到, 路径是 META-INF/native/linux-64

有两种加载方式,二选一就行.

// 标准做法
V4L4J.init();
// 不知名做法, 新版JDK支持指定路径, 貌似是JDK7开始的吧
System.load("/usr/local/lib/libvideo.so");
System.load("/usr/local/lib/libv4l4j.so");

开启设备,检查设备特性

基本流程, 创建设备,获取DeviceInfo, 检测是否兼容特定的特性.

很自然的,下面的代码当然用到了nutz相关的类

    private static final Log log = Logs.get();

    protected VideoDevice device;
    
    protected RGBFrameGrabber grabber;
    
    public void checkV4L4() {
        try {
            device = new VideoDevice("/dev/video0"); // 通常就是第0个设备
            DeviceInfo dinfo = device.getDeviceInfo();
            log.info("name=" + dinfo.getName());
            log.info("DeviceFile=" + dinfo.getDeviceFile());
            ImageFormatList ifl = dinfo.getFormatList();
            int w = 1920, h = 1080;
            if (ifl != null) {
                // 获取是原生支持的格式, 还有BGR/YVU/YUV等等,貌似我用不上,不打印了,反正触类旁通
                List<ImageFormat> natives = ifl.getNativeFormats();
                if (natives != null) {
                    for (ImageFormat format : natives) { 
                        // 理论上说支持多种分辨率,尤其是摄像头,但我这个采集卡只输出一种分辨率
                        log.info("native format = " + format.toNiceString());
                        w = format.getResolutionInfo().getDiscreteResolutions().get(0).width;
                        h = format.getResolutionInfo().getDiscreteResolutions().get(0).height;
                        log.infof("w=%d,h=%d", w, h);
                    }
                }
            }
            // 是否支持 BGR/JPEG/RGB/YUV/YVU转换,这些都非常重要, 最起码有一个会是true,通常是全部为true,取决是设备和linux内核版本.
            log.info("supportBGRConversion=" + device.supportBGRConversion());
            log.info("supportJPEGConversion=" + device.supportJPEGConversion());
            log.info("supportRGBConversion=" + device.supportRGBConversion());
            log.info("supportYUVConversion=" + device.supportYUVConversion());
            log.info("supportYVUConversion=" + device.supportYVUConversion());
            
            // 操作完成,释放设备. 但如果要开始捕捉,这一步必须去掉.
            device.release();
        }
        catch (V4L4JException e) {
            log.info("OhOhOh", e);
            device = null;
        }

通过v4l2-ctl获取更新信息

通过v4l4j的api能输出大部分必要的信息,但设备信息是拿不到的.

下面是执行v4l2-ctl –all输出的内容,中文注释是我手动加上的…

root@danoo-desktop:/dev/shm# v4l2-ctl --all
Driver Info (not using libv4l2):
        Driver name   : LINUXV4L2
        Card type     : PL330B:RAW 00.00 a0011af2
        Bus info      : PCIe: PCI Bus 0000:01
        Driver version: 3.12.0
        Capabilities  : 0x84221001
                Video Capture
                Video Capture Multiplanar
                Audio
                Streaming
                Device Capabilities
        Device Caps   : 0x04220001
                Video Capture
                Audio
                Streaming
Priority: 0
Video input : 3 (DVI-A INPUT(3): ok) ## 从0 - 7 代表各种接口. 其中3代表VGA(虽然名字里面有DVI)
Audio input : 0 () ## 木有音频输入.
Video Standard = 0x00001000
        NTSC-M     # PAL还是NTSC呢? 这里给出了答案
Format Video Capture:
        Width/Height  : 1920/1080 # 分辨率数据
        Pixel Format  : 'YV12'    # 原始像素格式
        Field         : Interlaced
        Bytes per Line: 1920
        Size Image    : 3110400
        Colorspace    : HDTV and modern devices (ITU709)
        Custom Info   : feedcafe
Crop Capability Video Capture:
        Bounds      : Left 0, Top 0, Width 1920, Height 1080
        Default     : Left 0, Top 0, Width 1920, Height 1080
        Pixel Aspect: 1/1
Streaming Parameters Video Capture:
        Capabilities     : timeperframe
        Frames per second: 60.000 (60000/1000) # 刷新率
        Read buffers     : 0
                     brightness (int)    : min=0 max=255 step=1 default=128 value=128
                       contrast (int)    : min=0 max=255 step=1 default=128 value=128
                     saturation (int)    : min=0 max=255 step=1 default=128 value=128
                            hue (int)    : min=0 max=255 step=1 default=128 value=128
                      auto_gain (int)    : min=0 max=1 step=1 default=1 value=1
                      sharpness (int)    : min=0 max=255 step=1 default=128 value=128

开始进行视频捕捉

            # 首先,需要确定哪种FrameGrabber可用(supportXXXConversion是否为true),然后选一种合适的. 对我来说就是RGBFrameGrabber
            # 然后,选择合适的输入设备, 我这里选3, VGA
            # 再然后, 选择NTSC, 取决于v4l2-ctl所显示的值. 我猜从DeviceInfo应该也能查到吧,没研究.
            grabber = device.getRGBFrameGrabber(w, h, 3, V4L4JConstants.STANDARD_NTSC);
            # 设置视频帧回调
            grabber.setCaptureCallback(new CaptureCallback() {
                public void nextFrame(VideoFrame frame) {
                    //log.info("frame income !!!" + frame.getFrameLength()); // 不晓得为啥,我这里总是0 ,但图片的确有了
                    try {
                        final BufferedImage image = frame.getBufferedImage();
                        # 做爱做的事吧... 如果是耗时操作,建议用线程池来执行,而不是在这个线程内完成
                    } catch (Throwable e) {
                        log.debug("BufferedImage FAIL..."  + e);
                    }
                    // 把当前帧回收,非常非常非常重要
                    frame.recycle();
                }
                public void exceptionReceived(V4L4JException e) {
                    log.info("something happen!!!", e);
                }
            });
            // 来吧, 开始趴体...
            grabber.startCapture();
            log.info("party start!!");
            // 趴体永不结束...
            Lang.quiteSleep(3600000);

性能

在我所使用的设备

"Intel(R) Celeron(R) CPU 1037U"
圆刚mini PICE采集卡

使用vga接口,采集1920x1080的彩色图像数据,能达到30fps左右, cpu占用率40%.

可能的原因:

  • 原生YV12数据,转为RGB数据需要一些计算量
  • 每帧图像需要8mb内存(BufferedImage),每秒30帧,该图像数据用完就扔, 相当于每秒产生240mb的可GC数据
]]>
0
<![CDATA[基于 TCP UDP 协议的实时流媒体的实时性分析]]> http://www.udpwork.com/item/16311.html http://www.udpwork.com/item/16311.html#reviews Mon, 19 Jun 2017 20:31:58 +0800 ideawu http://www.udpwork.com/item/16311.html 直播,电话通话,视频聊天都是实时流媒体的范畴。无论使用 TCP 还是 UDP,都会有延时。有个过时的观点是 UDP 更实时,但我不认为是这样。

实时流媒体的延时主要有几个因素:发送方缓冲,接收方缓冲,网络延时。缓冲包括网络缓冲,录制缓和冲播放缓冲。假设发送方缓冲是 10ms,接收方缓冲都是 50ms,网络延时是 100ms,那么就有至少 160ms 的播放延时。接收方缓冲比发送方多,是为了解决所谓的 jitter,网络延时不均匀导致的播放断续。

如果使用 UDP,如果丢包,那么接收方的缓冲就从 50ms 变为 40ms,交互延时变小,随着继续丢包变为 0 时,网络延时不旦不均匀就会发生 jitter 了。这时开始缓冲,一共花 50ms,所以信号中断最多 50ms,也即接收方缓冲。

如果使用 TCP,丢包会导致 TCP 重传,因为网络 rtt 是 200ms,到 50ms 时,接收方的缓冲已经没有数据了,只能等待网络传输,然后再等 150 ms(也即信号中断 150 ms),突然一次收到 110ms 的数据到接收方缓冲中。为了保证低延时,主动丢弃最早的 60ms 数据。使用 TCP,信号中断时时长是 150 ms,也即网络 rtt 减去接收方缓冲。因为 TCP 最坏情况要重传3次,所以是 3*rtt 减去接收方缓冲。

上面的例子,如果我们把接收缓冲加大,或者网络延时减小,那么两者的信号中断时长会变得更接近。目前,中国南北的网络 rtt 一般在 100ms,我没找到资料,但感觉语音通话的延时在 500ms 以内人应该是感觉不到的。所以我们可以增加接收方缓冲,这时最好没有信号中断。这时,UDP 因为丢包导致的好效应就没那么诱人了。

总结一下,UDP 开始丢包会导致好的效应,能让延时越来越小,小到只有发送缓冲时长和网络延时,累积丢包导致信号中断时长最多为接收方缓冲的时长。TCP 的延时保持不变,任何丢包都会导致信号中断,而且信号中断时长可能会更长,但加大缓冲区可以减少信号中断出现的概率。

Related posts:

  1. OS X 屏幕录制视频转 GIF 动画
  2. P2P与即时消息(IM)
  3. iOS流式布局UI框架CocoaUI开源
  4. Master-Workers 模式处理高负载
  5. SIP INVITE 会话建立过程
]]>
直播,电话通话,视频聊天都是实时流媒体的范畴。无论使用 TCP 还是 UDP,都会有延时。有个过时的观点是 UDP 更实时,但我不认为是这样。

实时流媒体的延时主要有几个因素:发送方缓冲,接收方缓冲,网络延时。缓冲包括网络缓冲,录制缓和冲播放缓冲。假设发送方缓冲是 10ms,接收方缓冲都是 50ms,网络延时是 100ms,那么就有至少 160ms 的播放延时。接收方缓冲比发送方多,是为了解决所谓的 jitter,网络延时不均匀导致的播放断续。

如果使用 UDP,如果丢包,那么接收方的缓冲就从 50ms 变为 40ms,交互延时变小,随着继续丢包变为 0 时,网络延时不旦不均匀就会发生 jitter 了。这时开始缓冲,一共花 50ms,所以信号中断最多 50ms,也即接收方缓冲。

如果使用 TCP,丢包会导致 TCP 重传,因为网络 rtt 是 200ms,到 50ms 时,接收方的缓冲已经没有数据了,只能等待网络传输,然后再等 150 ms(也即信号中断 150 ms),突然一次收到 110ms 的数据到接收方缓冲中。为了保证低延时,主动丢弃最早的 60ms 数据。使用 TCP,信号中断时时长是 150 ms,也即网络 rtt 减去接收方缓冲。因为 TCP 最坏情况要重传3次,所以是 3*rtt 减去接收方缓冲。

上面的例子,如果我们把接收缓冲加大,或者网络延时减小,那么两者的信号中断时长会变得更接近。目前,中国南北的网络 rtt 一般在 100ms,我没找到资料,但感觉语音通话的延时在 500ms 以内人应该是感觉不到的。所以我们可以增加接收方缓冲,这时最好没有信号中断。这时,UDP 因为丢包导致的好效应就没那么诱人了。

总结一下,UDP 开始丢包会导致好的效应,能让延时越来越小,小到只有发送缓冲时长和网络延时,累积丢包导致信号中断时长最多为接收方缓冲的时长。TCP 的延时保持不变,任何丢包都会导致信号中断,而且信号中断时长可能会更长,但加大缓冲区可以减少信号中断出现的概率。

Related posts:

  1. OS X 屏幕录制视频转 GIF 动画
  2. P2P与即时消息(IM)
  3. iOS流式布局UI框架CocoaUI开源
  4. Master-Workers 模式处理高负载
  5. SIP INVITE 会话建立过程
]]>
0
<![CDATA[skynet 网络线程的一点优化]]> http://www.udpwork.com/item/16310.html http://www.udpwork.com/item/16310.html#reviews Mon, 19 Jun 2017 19:59:45 +0800 云风 http://www.udpwork.com/item/16310.html skynet 是一个注重并行业务处理的框架,设计它的初衷是可以充分利用多核 CPU 更好的处理那些比较消耗 CPU 的,天然可以并行的业务,比如网络游戏。网络 I/O 并不是优化重点。

基于这个设计动机,skynet 的网络层使用单线程实现。因为我认为,即使是代码量稍大一些的单线程程序,也会比代码量较小的多线程程序更容易理解,出 bug 的机会也更少。而且经典的网络服务程序,如 redis nginx 并没有因为单线程处理网络 IO 而变现得不堪,反而有不错的口碑。

所以,skynet 的 epoll 循环并不像 erlang 那样,只关注读写事件,而让每个 actor 自己去处理真正的 socket 读写。那样固然可以获得更高的网络处理能力,但势必让网络 API (由存在多个工作线程里的多个 actor 分别调用)依赖锁来保证正确性。这是我不太希望看到的。目前的设计是,所有网络请求,都通过把指令写到一个进程内的 pipe ,串行化到网络处理线程,依次处理,然后再把结果投递到 skynet 的其它服务中。

这个做法未必最好,但也恰恰能用,一般网络游戏服务器,根据我们的实际项目数据,在其它业务处理的 CPU 占用到极限时,单台机器网络带宽不大会超过 30MB 左右的上下行带宽。一个核每秒处理 60MB 的数据是绰绰有余的。

不过我一直有个想法,或许可以优化一下这部分,让 skynet 可以适应一些重 IO 而不仅仅是重业务处理的场合。前些年和一个使用 skynet 做流媒体广播的同学交流过,他们的生产环境上,一台机器会配置几块千兆网卡,skynet 在处理高带宽的 udp 广播时,跑不满硬件的带宽。

前几天,skynet 的issue #646又让我想到这件事情。就这个 issue 而言,我不认为达到网络线程处理极限,可能尚有未发现的其它因素影响,不过就这个机会,我试着实现了一下以前的想法。

我的想法是,可以把网络写操作从网络线程中拿出来。当每次要写数据时,先检查一下该 fd 中发送队列是否为空,如果为空的话,就尝试直接在当前工作线程发送(这往往是大多数情况)。发送成功就皆大欢喜,如果失败或部分发送,则把没发送的数据放在 socket 结构中,并开启 epoll 的可写事件。

网络线程每次发送待发队列前,需要先检查有没有直接发送剩下的部分,有则加到队列头,然后再依次发送。

当然 udp 会更简单一些,一是 udp 包没有部分发送的可能,二是 udp 不需要保证次序。所以 udp 立即发送失败后,可以直接按原流程扔到发送队列尾即可。

加上这个优化后,就必须在每个 socket 结构上增加一个 spinlock 。直接发送的逻辑可以用 try lock 尝试加锁,而不一定要获得锁;只在网络线程发送队列期间这一个地方才需要加锁。所以这个锁几乎不会竞争。

毕竟是多线程代码容易出 bug ,而且也不太容易做测试。所以我暂时把它放在一个独立分支上,希望感兴趣的同学可以帮助 review 一下,以后再考虑是否合并到主干。

]]>
skynet 是一个注重并行业务处理的框架,设计它的初衷是可以充分利用多核 CPU 更好的处理那些比较消耗 CPU 的,天然可以并行的业务,比如网络游戏。网络 I/O 并不是优化重点。

基于这个设计动机,skynet 的网络层使用单线程实现。因为我认为,即使是代码量稍大一些的单线程程序,也会比代码量较小的多线程程序更容易理解,出 bug 的机会也更少。而且经典的网络服务程序,如 redis nginx 并没有因为单线程处理网络 IO 而变现得不堪,反而有不错的口碑。

所以,skynet 的 epoll 循环并不像 erlang 那样,只关注读写事件,而让每个 actor 自己去处理真正的 socket 读写。那样固然可以获得更高的网络处理能力,但势必让网络 API (由存在多个工作线程里的多个 actor 分别调用)依赖锁来保证正确性。这是我不太希望看到的。目前的设计是,所有网络请求,都通过把指令写到一个进程内的 pipe ,串行化到网络处理线程,依次处理,然后再把结果投递到 skynet 的其它服务中。

这个做法未必最好,但也恰恰能用,一般网络游戏服务器,根据我们的实际项目数据,在其它业务处理的 CPU 占用到极限时,单台机器网络带宽不大会超过 30MB 左右的上下行带宽。一个核每秒处理 60MB 的数据是绰绰有余的。

不过我一直有个想法,或许可以优化一下这部分,让 skynet 可以适应一些重 IO 而不仅仅是重业务处理的场合。前些年和一个使用 skynet 做流媒体广播的同学交流过,他们的生产环境上,一台机器会配置几块千兆网卡,skynet 在处理高带宽的 udp 广播时,跑不满硬件的带宽。

前几天,skynet 的issue #646又让我想到这件事情。就这个 issue 而言,我不认为达到网络线程处理极限,可能尚有未发现的其它因素影响,不过就这个机会,我试着实现了一下以前的想法。

我的想法是,可以把网络写操作从网络线程中拿出来。当每次要写数据时,先检查一下该 fd 中发送队列是否为空,如果为空的话,就尝试直接在当前工作线程发送(这往往是大多数情况)。发送成功就皆大欢喜,如果失败或部分发送,则把没发送的数据放在 socket 结构中,并开启 epoll 的可写事件。

网络线程每次发送待发队列前,需要先检查有没有直接发送剩下的部分,有则加到队列头,然后再依次发送。

当然 udp 会更简单一些,一是 udp 包没有部分发送的可能,二是 udp 不需要保证次序。所以 udp 立即发送失败后,可以直接按原流程扔到发送队列尾即可。

加上这个优化后,就必须在每个 socket 结构上增加一个 spinlock 。直接发送的逻辑可以用 try lock 尝试加锁,而不一定要获得锁;只在网络线程发送队列期间这一个地方才需要加锁。所以这个锁几乎不会竞争。

毕竟是多线程代码容易出 bug ,而且也不太容易做测试。所以我暂时把它放在一个独立分支上,希望感兴趣的同学可以帮助 review 一下,以后再考虑是否合并到主干。

]]>
0
<![CDATA[SIP INVITE 会话建立过程]]> http://www.udpwork.com/item/16309.html http://www.udpwork.com/item/16309.html#reviews Mon, 19 Jun 2017 19:05:35 +0800 ideawu http://www.udpwork.com/item/16309.html 运行于 UDP 之上的 SIP,因为 UDP 是不可靠传输的,所以 SIP 协议本身要自己实现可靠传输。对于如何可靠传输,SIP 的 RFC 文档没有要求实现独立的传输层,而是将可靠传输隐含于交互过程本身。如果像 TCP/IP 协议那样分层,特点是清晰。而将可靠传输隐含于交互,则可控程度更高,当然也更复杂。

所以,RFC 中创造了一些概念,如 Transaction 等等,对于有经验的程序员来说,这完全没必须,反而造成困扰。对于程序员,用几句简单的过程描述就可以解决。如下。

Client: 定期重复发送 INVITE 直到收到响应。
Server:收到 INVITE 后,定期重复发送响应,直到收到 ACK。
Client:收到响应后回复 ACK,认为会话已经建立。这时如果再次收到响应,回复一个 ACK。
Server:收到 ACK,认为会话已经建立。这时如果再次收到 INVITE 或者 ACK,丢弃。

里面有两个“定期发送”,其实就是超时重传的意思。当然,超时重传有最终次数限制。

程序看到上面的话,实现起来就简单了。不太明白 RFC 为什么要用 Trasaction 那样奇怪的方式来描述,可能和那个时期或者某个公司的编程习惯有关,作者可能是基于自己的某个代码实现来描述。其他人实现时,未必需要 Transaction 这种东西。

关于可靠传输,我已经在另一篇博客文章(http://www.ideawu.net/blog/archives/782.html)中介绍了,就是确认,重传,排序(序号)。SIP 把确认隐含于交互过程,例如响应是对 INVITE 的确认,ACK 是对响应的确认。在不同的状态接受不同类型的报文,以及会话建立之后对非期望报文的丢弃就属于排序(序号)。SIP 本身也有序号,并不是单纯的报文排序之用。

Related posts:

  1. SIP tag 和 Call-ID 的区别
  2. Nginx 安装 HTTPS SSL 证书
  3. 使用dbproxy来处理高并发数据库请求
  4. SSH ProxyCommand及其思想
  5. lighttpd配置HTTPS(SSL)
]]>
运行于 UDP 之上的 SIP,因为 UDP 是不可靠传输的,所以 SIP 协议本身要自己实现可靠传输。对于如何可靠传输,SIP 的 RFC 文档没有要求实现独立的传输层,而是将可靠传输隐含于交互过程本身。如果像 TCP/IP 协议那样分层,特点是清晰。而将可靠传输隐含于交互,则可控程度更高,当然也更复杂。

所以,RFC 中创造了一些概念,如 Transaction 等等,对于有经验的程序员来说,这完全没必须,反而造成困扰。对于程序员,用几句简单的过程描述就可以解决。如下。

Client: 定期重复发送 INVITE 直到收到响应。
Server:收到 INVITE 后,定期重复发送响应,直到收到 ACK。
Client:收到响应后回复 ACK,认为会话已经建立。这时如果再次收到响应,回复一个 ACK。
Server:收到 ACK,认为会话已经建立。这时如果再次收到 INVITE 或者 ACK,丢弃。

里面有两个“定期发送”,其实就是超时重传的意思。当然,超时重传有最终次数限制。

程序看到上面的话,实现起来就简单了。不太明白 RFC 为什么要用 Trasaction 那样奇怪的方式来描述,可能和那个时期或者某个公司的编程习惯有关,作者可能是基于自己的某个代码实现来描述。其他人实现时,未必需要 Transaction 这种东西。

关于可靠传输,我已经在另一篇博客文章(http://www.ideawu.net/blog/archives/782.html)中介绍了,就是确认,重传,排序(序号)。SIP 把确认隐含于交互过程,例如响应是对 INVITE 的确认,ACK 是对响应的确认。在不同的状态接受不同类型的报文,以及会话建立之后对非期望报文的丢弃就属于排序(序号)。SIP 本身也有序号,并不是单纯的报文排序之用。

Related posts:

  1. SIP tag 和 Call-ID 的区别
  2. Nginx 安装 HTTPS SSL 证书
  3. 使用dbproxy来处理高并发数据库请求
  4. SSH ProxyCommand及其思想
  5. lighttpd配置HTTPS(SSL)
]]>
0
<![CDATA[[译]使用os/exec执行命令]]> http://www.udpwork.com/item/16312.html http://www.udpwork.com/item/16312.html#reviews Mon, 19 Jun 2017 17:30:37 +0800 鸟窝 http://www.udpwork.com/item/16312.html 原文:Advanced command execution in Go with os/execby Krzysztof Kowalczyk.
完整代码在作者的github上:advanced-exec

Go可以非常方便地执行外部程序,让我们开始探索之旅吧。

执行命令并获得输出结果

最简单的例子就是运行ls -lah并获得组合在一起的stdout/stderr输出。

12345678
func main() {	cmd := exec.Command("ls", "-lah")	out, err := cmd.CombinedOutput()	if err != nil {		log.Fatalf("cmd.Run() failed with %s\n", err)	}	fmt.Printf("combined out:\n%s\n", string(out))}

将stdout和stderr分别处理

和上面的例子类似,只不过将stdout和stderr分别处理。

123456789101112
func main() {	cmd := exec.Command("ls", "-lah")	var stdout, stderr bytes.Buffer	cmd.Stdout = &stdout	cmd.Stderr = &stderr	err := cmd.Run()	if err != nil {		log.Fatalf("cmd.Run() failed with %s\n", err)	}	outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())	fmt.Printf("out:\n%s\nerr:\n%s\n", outStr, errStr)}

命令执行过程中获得输出

如果一个命令需要花费很长时间才能执行完呢?

除了能获得它的stdout/stderr,我们还希望在控制台显示命令执行的进度。

有点小复杂。

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
func copyAndCapture(w io.Writer, r io.Reader) ([]byte, error) {	var out []byte	buf := make([]byte, 1024, 1024)	for {		n, err := r.Read(buf[:])		if n > 0 {			d := buf[:n]			out = append(out, d...)			os.Stdout.Write(d)		}		if err != nil {			// Read returns io.EOF at the end of file, which is not an error for us			if err == io.EOF {				err = nil			}			return out, err		}	}	// never reached	panic(true)	return nil, nil}func main() {	cmd := exec.Command("ls", "-lah")	var stdout, stderr []byte	var errStdout, errStderr error	stdoutIn, _ := cmd.StdoutPipe()	stderrIn, _ := cmd.StderrPipe()	cmd.Start()	go func() {		stdout, errStdout = copyAndCapture(os.Stdout, stdoutIn)	}()	go func() {		stderr, errStderr = copyAndCapture(os.Stderr, stderrIn)	}()	err := cmd.Wait()	if err != nil {		log.Fatalf("cmd.Run() failed with %s\n", err)	}	if errStdout != nil || errStderr != nil {		log.Fatalf("failed to capture stdout or stderr\n")	}	outStr, errStr := string(stdout), string(stderr)	fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr)}

命令执行过程中获得输出2

上一个方案虽然工作,但是看起来copyAndCapture好像重新实现了io.Copy。由于Go的接口的功能,我们可以重用io.Copy。

我们写一个CapturingPassThroughWriterstruct,它实现了io.Writer接口。它会捕获所有的数据并写入到底层的io.Writer。

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
// CapturingPassThroughWriter is a writer that remembers// data written to it and passes it to wtype CapturingPassThroughWriter struct {	buf bytes.Buffer	w io.Writer}// NewCapturingPassThroughWriter creates new CapturingPassThroughWriterfunc NewCapturingPassThroughWriter(w io.Writer) *CapturingPassThroughWriter {	return &CapturingPassThroughWriter{		w: w,	}}func (w *CapturingPassThroughWriter) Write(d []byte) (int, error) {	w.buf.Write(d)	return w.w.Write(d)}// Bytes returns bytes written to the writerfunc (w *CapturingPassThroughWriter) Bytes() []byte {	return w.buf.Bytes()}func main() {	var errStdout, errStderr error	cmd := exec.Command("ls", "-lah")	stdoutIn, _ := cmd.StdoutPipe()	stderrIn, _ := cmd.StderrPipe()	stdout := NewCapturingPassThroughWriter(os.Stdout)	stderr := NewCapturingPassThroughWriter(os.Stderr)	err := cmd.Start()	if err != nil {		log.Fatalf("cmd.Start() failed with '%s'\n", err)	}	go func() {		_, errStdout = io.Copy(stdout, stdoutIn)	}()	go func() {		_, errStderr = io.Copy(stderr, stderrIn)	}()	err = cmd.Wait()	if err != nil {		log.Fatalf("cmd.Run() failed with %s\n", err)	}	if errStdout != nil || errStderr != nil {		log.Fatalf("failed to capture stdout or stderr\n")	}	outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())	fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr)}

命令执行过程中获得输出3

事实上Go标准库包含一个更通用的io.MultiWriter,我们可以直接使用它。

12345678910111213141516171819202122232425262728293031323334
func main() {	var stdoutBuf, stderrBuf bytes.Buffer	cmd := exec.Command("ls", "-lah")	stdoutIn, _ := cmd.StdoutPipe()	stderrIn, _ := cmd.StderrPipe()	var errStdout, errStderr error	stdout := io.MultiWriter(os.Stdout, &stdoutBuf)	stderr := io.MultiWriter(os.Stderr, &stderrBuf)	err := cmd.Start()	if err != nil {		log.Fatalf("cmd.Start() failed with '%s'\n", err)	}	go func() {		_, errStdout = io.Copy(stdout, stdoutIn)	}()	go func() {		_, errStderr = io.Copy(stderr, stderrIn)	}()	err = cmd.Wait()	if err != nil {		log.Fatalf("cmd.Run() failed with %s\n", err)	}	if errStdout != nil || errStderr != nil {		log.Fatal("failed to capture stdout or stderr\n")	}	outStr, errStr := string(stdoutBuf.Bytes()), string(stderrBuf.Bytes())	fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr)}

自己实现是很好滴,但是熟悉标准库并使用它更好。

改变执行程序的环境(environment)

你已经知道了怎么在程序中获得环境变量,对吧: `os.Environ()`返回所有的环境变量[]string,每个字符串以FOO=bar格式存在。FOO是环境变量的名称,bar是环境变量的值, 也就是os.Getenv("FOO")的返回值。

有时候你可能想修改执行程序的环境。

你可设置exec.Cmd的Env的值,和os.Environ()格式相同。通常你不会构造一个全新的环境,而是添加自己需要的环境变量:

123456789
   cmd := exec.Command("programToExecute")additionalEnv := "FOO=bar"newEnv := append(os.Environ(), additionalEnv))cmd.Env = newEnvout, err := cmd.CombinedOutput()if err != nil {	log.Fatalf("cmd.Run() failed with %s\n", err)}fmt.Printf("%s", out)

shurcooL/go/osutil提供了便利的方法设置环境变量。

预先检查程序是否存在

想象一下你写了一个程序需要花费很长时间执行,再最后你调用foo做一些基本的任务。

如果foo程序不存在,程序会执行失败。

当然如果我们预先能检查程序是否存在九完美了,如果不存在久打印错误信息。

你可以调用exec.LookPath方法来检查:

12345678
func checkLsExists() {	path, err := exec.LookPath("ls")	if err != nil {		fmt.Printf("didn't find 'ls' executable\n")	} else {		fmt.Printf("'ls' executable is in '%s'\n", path)	}}

另一个检查的办法就是让程序执行一个空操作, 比如传递参数"--help"显示帮助信息。

下面的章节是译者补充的内容

管道

我们可以使用管道将多个命令串联起来, 上一个命令的输出是下一个命令的输入。

使用os.Exec有点麻烦,你可以使用下面的方法:

123456789101112131415161718192021222324252627
package mainimport (    "bytes"    "io"    "os"    "os/exec")func main() {    c1 := exec.Command("ls")    c2 := exec.Command("wc", "-l")    r, w := io.Pipe()     c1.Stdout = w    c2.Stdin = r    var b2 bytes.Buffer    c2.Stdout = &b2    c1.Start()    c2.Start()    c1.Wait()    w.Close()    c2.Wait()    io.Copy(os.Stdout, &b2)}

或者直接使用Cmd的StdoutPipe方法,而不是自己创建一个io.Pipe`。

12345678910111213141516
package mainimport (    "os"    "os/exec")func main() {    c1 := exec.Command("ls")    c2 := exec.Command("wc", "-l")    c2.Stdin, _ = c1.StdoutPipe()    c2.Stdout = os.Stdout    _ = c2.Start()    _ = c1.Run()    _ = c2.Wait()}

管道2

上面的解决方案是Go风格的解决方案,事实上你还可以用一个"Trick"来实现。

12345678910111213141516
package mainimport (	"fmt"	"os/exec")func main() {	cmd := "cat /proc/cpuinfo | egrep '^model name' | uniq | awk '{print substr($0, index($0,$4))}'"	out, err := exec.Command("bash", "-c", cmd).Output()	if err != nil {		fmt.Printf("Failed to execute command: %s", cmd)	}	fmt.Println(string(out))}
]]>
原文:Advanced command execution in Go with os/execby Krzysztof Kowalczyk.
完整代码在作者的github上:advanced-exec

Go可以非常方便地执行外部程序,让我们开始探索之旅吧。

执行命令并获得输出结果

最简单的例子就是运行ls -lah并获得组合在一起的stdout/stderr输出。

12345678
func main() {	cmd := exec.Command("ls", "-lah")	out, err := cmd.CombinedOutput()	if err != nil {		log.Fatalf("cmd.Run() failed with %s\n", err)	}	fmt.Printf("combined out:\n%s\n", string(out))}

将stdout和stderr分别处理

和上面的例子类似,只不过将stdout和stderr分别处理。

123456789101112
func main() {	cmd := exec.Command("ls", "-lah")	var stdout, stderr bytes.Buffer	cmd.Stdout = &stdout	cmd.Stderr = &stderr	err := cmd.Run()	if err != nil {		log.Fatalf("cmd.Run() failed with %s\n", err)	}	outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())	fmt.Printf("out:\n%s\nerr:\n%s\n", outStr, errStr)}

命令执行过程中获得输出

如果一个命令需要花费很长时间才能执行完呢?

除了能获得它的stdout/stderr,我们还希望在控制台显示命令执行的进度。

有点小复杂。

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
func copyAndCapture(w io.Writer, r io.Reader) ([]byte, error) {	var out []byte	buf := make([]byte, 1024, 1024)	for {		n, err := r.Read(buf[:])		if n > 0 {			d := buf[:n]			out = append(out, d...)			os.Stdout.Write(d)		}		if err != nil {			// Read returns io.EOF at the end of file, which is not an error for us			if err == io.EOF {				err = nil			}			return out, err		}	}	// never reached	panic(true)	return nil, nil}func main() {	cmd := exec.Command("ls", "-lah")	var stdout, stderr []byte	var errStdout, errStderr error	stdoutIn, _ := cmd.StdoutPipe()	stderrIn, _ := cmd.StderrPipe()	cmd.Start()	go func() {		stdout, errStdout = copyAndCapture(os.Stdout, stdoutIn)	}()	go func() {		stderr, errStderr = copyAndCapture(os.Stderr, stderrIn)	}()	err := cmd.Wait()	if err != nil {		log.Fatalf("cmd.Run() failed with %s\n", err)	}	if errStdout != nil || errStderr != nil {		log.Fatalf("failed to capture stdout or stderr\n")	}	outStr, errStr := string(stdout), string(stderr)	fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr)}

命令执行过程中获得输出2

上一个方案虽然工作,但是看起来copyAndCapture好像重新实现了io.Copy。由于Go的接口的功能,我们可以重用io.Copy。

我们写一个CapturingPassThroughWriterstruct,它实现了io.Writer接口。它会捕获所有的数据并写入到底层的io.Writer。

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
// CapturingPassThroughWriter is a writer that remembers// data written to it and passes it to wtype CapturingPassThroughWriter struct {	buf bytes.Buffer	w io.Writer}// NewCapturingPassThroughWriter creates new CapturingPassThroughWriterfunc NewCapturingPassThroughWriter(w io.Writer) *CapturingPassThroughWriter {	return &CapturingPassThroughWriter{		w: w,	}}func (w *CapturingPassThroughWriter) Write(d []byte) (int, error) {	w.buf.Write(d)	return w.w.Write(d)}// Bytes returns bytes written to the writerfunc (w *CapturingPassThroughWriter) Bytes() []byte {	return w.buf.Bytes()}func main() {	var errStdout, errStderr error	cmd := exec.Command("ls", "-lah")	stdoutIn, _ := cmd.StdoutPipe()	stderrIn, _ := cmd.StderrPipe()	stdout := NewCapturingPassThroughWriter(os.Stdout)	stderr := NewCapturingPassThroughWriter(os.Stderr)	err := cmd.Start()	if err != nil {		log.Fatalf("cmd.Start() failed with '%s'\n", err)	}	go func() {		_, errStdout = io.Copy(stdout, stdoutIn)	}()	go func() {		_, errStderr = io.Copy(stderr, stderrIn)	}()	err = cmd.Wait()	if err != nil {		log.Fatalf("cmd.Run() failed with %s\n", err)	}	if errStdout != nil || errStderr != nil {		log.Fatalf("failed to capture stdout or stderr\n")	}	outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())	fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr)}

命令执行过程中获得输出3

事实上Go标准库包含一个更通用的io.MultiWriter,我们可以直接使用它。

12345678910111213141516171819202122232425262728293031323334
func main() {	var stdoutBuf, stderrBuf bytes.Buffer	cmd := exec.Command("ls", "-lah")	stdoutIn, _ := cmd.StdoutPipe()	stderrIn, _ := cmd.StderrPipe()	var errStdout, errStderr error	stdout := io.MultiWriter(os.Stdout, &stdoutBuf)	stderr := io.MultiWriter(os.Stderr, &stderrBuf)	err := cmd.Start()	if err != nil {		log.Fatalf("cmd.Start() failed with '%s'\n", err)	}	go func() {		_, errStdout = io.Copy(stdout, stdoutIn)	}()	go func() {		_, errStderr = io.Copy(stderr, stderrIn)	}()	err = cmd.Wait()	if err != nil {		log.Fatalf("cmd.Run() failed with %s\n", err)	}	if errStdout != nil || errStderr != nil {		log.Fatal("failed to capture stdout or stderr\n")	}	outStr, errStr := string(stdoutBuf.Bytes()), string(stderrBuf.Bytes())	fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr)}

自己实现是很好滴,但是熟悉标准库并使用它更好。

改变执行程序的环境(environment)

你已经知道了怎么在程序中获得环境变量,对吧: `os.Environ()`返回所有的环境变量[]string,每个字符串以FOO=bar格式存在。FOO是环境变量的名称,bar是环境变量的值, 也就是os.Getenv("FOO")的返回值。

有时候你可能想修改执行程序的环境。

你可设置exec.Cmd的Env的值,和os.Environ()格式相同。通常你不会构造一个全新的环境,而是添加自己需要的环境变量:

123456789
   cmd := exec.Command("programToExecute")additionalEnv := "FOO=bar"newEnv := append(os.Environ(), additionalEnv))cmd.Env = newEnvout, err := cmd.CombinedOutput()if err != nil {	log.Fatalf("cmd.Run() failed with %s\n", err)}fmt.Printf("%s", out)

shurcooL/go/osutil提供了便利的方法设置环境变量。

预先检查程序是否存在

想象一下你写了一个程序需要花费很长时间执行,再最后你调用foo做一些基本的任务。

如果foo程序不存在,程序会执行失败。

当然如果我们预先能检查程序是否存在九完美了,如果不存在久打印错误信息。

你可以调用exec.LookPath方法来检查:

12345678
func checkLsExists() {	path, err := exec.LookPath("ls")	if err != nil {		fmt.Printf("didn't find 'ls' executable\n")	} else {		fmt.Printf("'ls' executable is in '%s'\n", path)	}}

另一个检查的办法就是让程序执行一个空操作, 比如传递参数"--help"显示帮助信息。

下面的章节是译者补充的内容

管道

我们可以使用管道将多个命令串联起来, 上一个命令的输出是下一个命令的输入。

使用os.Exec有点麻烦,你可以使用下面的方法:

123456789101112131415161718192021222324252627
package mainimport (    "bytes"    "io"    "os"    "os/exec")func main() {    c1 := exec.Command("ls")    c2 := exec.Command("wc", "-l")    r, w := io.Pipe()     c1.Stdout = w    c2.Stdin = r    var b2 bytes.Buffer    c2.Stdout = &b2    c1.Start()    c2.Start()    c1.Wait()    w.Close()    c2.Wait()    io.Copy(os.Stdout, &b2)}

或者直接使用Cmd的StdoutPipe方法,而不是自己创建一个io.Pipe`。

12345678910111213141516
package mainimport (    "os"    "os/exec")func main() {    c1 := exec.Command("ls")    c2 := exec.Command("wc", "-l")    c2.Stdin, _ = c1.StdoutPipe()    c2.Stdout = os.Stdout    _ = c2.Start()    _ = c1.Run()    _ = c2.Wait()}

管道2

上面的解决方案是Go风格的解决方案,事实上你还可以用一个"Trick"来实现。

12345678910111213141516
package mainimport (	"fmt"	"os/exec")func main() {	cmd := "cat /proc/cpuinfo | egrep '^model name' | uniq | awk '{print substr($0, index($0,$4))}'"	out, err := exec.Command("bash", "-c", cmd).Output()	if err != nil {		fmt.Printf("Failed to execute command: %s", cmd)	}	fmt.Println(string(out))}
]]>
0
<![CDATA[我当初是怎么管理技术团队的 - 旁观者]]> http://www.udpwork.com/item/16308.html http://www.udpwork.com/item/16308.html#reviews Mon, 19 Jun 2017 09:52:00 +0800 旁观者 http://www.udpwork.com/item/16308.html 【摘要】我们认为管理技术人才是一门学问,第一,外行不可能领导内行,第二,靠挖人,靠猎头,一朝一夕之间不可能组建一支能打硬仗的技术团队,那只能攒出乌合之众。阅读全文

]]>
【摘要】我们认为管理技术人才是一门学问,第一,外行不可能领导内行,第二,靠挖人,靠猎头,一朝一夕之间不可能组建一支能打硬仗的技术团队,那只能攒出乌合之众。阅读全文

]]>
0
<![CDATA[支持部分共享的树结构]]> http://www.udpwork.com/item/16307.html http://www.udpwork.com/item/16307.html#reviews Sun, 18 Jun 2017 16:23:52 +0800 云风 http://www.udpwork.com/item/16307.html 因为图形引擎中的对象天然适合用树 (n-ary tree) 表达,所以它在图形引擎中被广泛使用。通常,子节点会继承父节点的一些状态,比如变换矩阵,在渲染或更新的时候,可以通过先序遍历逐级相乘。

在 PC 内存充裕的条件下,我们通常不必考虑树结构储存的开销,所以大多数图形引擎通常会为每个渲染对象独立生成一个树结构,比如 Unity 中的 GameObject 就是这么一个东西。在 Ejoy2D 中,从节约内存的角度考虑,把树节点上的一部分可共享的状态信息(不变的矩阵、纹理坐标等)移到了资源数据块中,但是树结构的拓扑关系还是在新创建出每个 sprite 时复制了一份。

随着游戏制作的工艺提高,而大众使用的移动设备的内存增长有限,这部分的开销慢慢变得显著。在我们正在开发的几个项目中,渲染对象本身,而不算图形资源(贴图、模型等),也占据了以 M 为单位计算的可用内存。比如在一个项目中,某个巨型对象由多达 2000+ 个节点构成,创建速度和内存开销都成为一个不可忽视的问题。由于是于自研引擎,所以同事尝试过一些优化的尝试。

图形引擎处理的大多数的树结构对象,其实仅在编辑环境会对树的拓扑关系进行调整:增加、移动、复制节点。而运行环境下,树结构本身几乎是不变的。一般的树结构的实现是用指针来相互引用,编辑好的资源是树结构的序列化结果,而创建过程是一个反序列化过程,在内存中重建整棵树,并用指针重新建立节点间的联系。比如在 Unity 中,就把这种编辑好的树对象叫做 prefab 预制件。如果预制件比较复杂,加载预制件和从预制件中构建对象的时间成本都不算低。

一个优化方法是去掉运行时内存中的指针,改用 id 或资源数据块中的偏移量,这样,预制件可以直接从资源文件中成块读入,创建内存对象时也只需要用指针引用即可。可以节省大量在内存中重建的时间。Ejoy2D 现在就是这么做的。去掉指针的额外好处是在 64 位系统下,可以从 8 字节的引用开销减少到 4 字节(或更少)。Ejoy2D 当初做此修改,为一个项目一举减少了 10M+ 的运行期内存占用。而且资源文件可以在暂时不用的时候移出内存,等需要的时候再加载回来,而不用担心数据的内存地址发生了变化。

但是,如果想让游戏对象直接共享使用预制件,最核心需要解决的问题是:大部分对象并不能做到完全不修改树结构,且预制件中的树的拓扑关系并不直接对应到内存。

我们先来看对应关系的问题:

在 ejoy2d 中,资源(预制件)中的对象结构其实并不是树,而是一个有向无环图。例如:美术制作了一个房子,房子上挂着三个灯笼,在资源中,灯笼只有一个预置对象,是房子对它有三个引用。而在运行期,三个灯笼必须是三个独立对象;程序是有能力对它们分别修改属性的。比如改改颜色,摆动频率等等。也就是说,这个房子的预制件一但在内存中还原,原本共享的灯笼子物件,必须有三份可供修改的状态数据区。

再来看前一个问题:

对于制作好的预制件,运行期很可能对树上的某个节点做属性调整。比如、预制件制作了一个仓库,仓库中的货物就是由运行时决定的。我们在陌陌争霸中,为仓库的货物状态预设了数帧图片,在运行时可以设置显示第几帧,用来表示仓库的库存情况。如果仓库中的货物还可以根据运行情况实际表现出来,则还需要用到挂接的功能:把货物对象挂接到仓库预设的挂接点上。

由于这些实现上的复杂需求,ejoy2d 和大多数图形引擎一样,从预制件创建出游戏对象直接简单的重建树的过程。当你从资源中创建一个 sprite ,你会发现逐层创建出了 lua 对象。然而大多数情况下,运行期并不会修改其中的绝大多数对象的属性,而仅使用资源中预制好的那一份。这就造成了一些浪费。如果资源中的树不算复杂时,这个浪费是有限的;但是我们的一个使用 ejoy2d 开发的新项目,随着内部工具的完善,美术逐渐创作出越来越复杂的结构。使用 ejoy2d 做新项目开发的同学花了半年多精力想优化这个问题。

我最近也重新思考了一下,这个问题的本质是,如果已有一个层级复杂的树状数据结构(下面称它为蓝图),我们可不可以做到有很多基于它的副本,每个副本对其中的部分做修改,而共享那些没有修改的部分。

比较容易想到的方案有这些:

  1. 在蓝图的每个节点上预留一个指针,指向一个列表;如果有副本向修改这个节点,就把修改后的属性加到这个列表中。在遍历这个节点时,一旦发现这个节点有人修改过,就找到对应的修改数据。另外,蓝图中如果有重复引用的节点(例如上面提到的屋檐下挂的灯笼),在修改其属性时,需要拷贝分裂。

  2. 对 1 方案做一些修改,预留指针只保存一个槽位,副本修改属性记录在副本对象中,而在遍历蓝图前,将副本中修改过的属性填入蓝图,遍历完成后再清除;或是在填入的同时把副本指针也记录在节点上,遍历时忽略非自己修改的节点。

  3. 在副本中保留一个修改节点数据的 hash 表,遍历蓝图的时候查询 hash 表,找到可能修改的属性。

  4. 在修改蓝图的某个节点时,部分复制创建出一颗不完全树,只保留被修改属性的节点和它们的祖先;让那些没有修改的分支引用回蓝图。遍历的时候改遍历复制出来的不完全树。

这几个方案大同小异,我们实际在项目中采用的方案 1 ,数据结构比原先的 ejoy2d 复杂了很多,衍生出不少 bug ,目前还不够稳定。

我这几天的思考结果是,在方案 3 的基础上做改进,或许是更好的选择。

方案三的本质是:运行时的树由 蓝图 和 对蓝图的补丁 两个部分构成。这样从数据结构上来说,它们是独立的两块数据,相互引用关系比较简单。可能的额外开销是遍历每个节点时需要额外的一次 hash 表查询,虽然大多数情况下是 Hash 表查询是 O(1) 的操作,但实际上会因为冲突而 O(1) 高一些,尤其是这个方案的初衷之一就是节省内存,如果预留比较大的 hash 表池来减少冲突率的话,很可能剩下的内存又再次消耗掉了。另外 hash 表的 key 也不太好选择,不能直接用节点对象的指针,因为在蓝图中有大量的对象是被共享的。

怎么改进呢?

其实、我们对树的遍历方式其实是唯一的。在图形引擎中多半选择深度优先的先序遍历,也就是先访问根节点,再依次访问子节点。如果树的拓扑结构稳定,其实每次遍历的过程都是唯一的。所以运行时的每个节点其实都可以根据其遍历次序有一个不变的唯一序号。对于上面提到的屋檐下的灯笼,多个灯笼虽然共享着蓝图上的同一块数据,但每个灯笼,以及灯笼下的子节点都有不同的访问编号。我们可以用这个编号来索引附加(修改的)节点属性。

由于访问次序固定,我们也不再需要用 hash 表来记录 patch 集合,而只需要按访问先后次序来排列好 patch 即可。再访问过程中,每访问到一个节点,就把访问编号加一,检查 patch 链表头就知道是否当前节点丫头修改属性了,这样就可以做到严格的 O(1) 时间,同时也不增加内存开销。

order 的值虽然无法直接记录在蓝图上,但我们可以在蓝图中预算好每个子树的总节点数,这样计算节点的 order 成本仅 O(log n) 。而且仅在取节点引用,或是从非根节点开始遍历时才需要计算一次。

还剩一个问题:

遍历树结构变复杂了,即使我们把遍历算法写对,却很难用常规手法封装。用简单的迭代器模式是不够的,遍历树不仅仅需要访问到每个节点,还需要在遍历过程中感知到树的遍历层次变化。比如在 ejoy2d 现在的公开版本中,需要按树层级来累乘变换矩阵,这个临时矩阵是存放在遍历过程中的函数调用栈上的。我们还可能根据枝干上的(隐藏)属性来跳过子节点等等。

否则,既然访问持续恒定,为什么不直接把树摊平?btw, 在上面提到的当前项目中,维护引擎的同学已经把摊平树结构作为了一项优化。

我还没有见过哪个 C 版本的 n-ary tree 能够把接口设计好,做到高内聚性的实现;大多数情况下,C 语言的项目中都会根据实际需要来实现一个仅满足自身需要的 n-ary tree ,把模块内聚性的边界放在更高一些的层次,而不直接暴露树结构的接口。

C++ 倒是有几个树结构的实现:比如tree.hh以及boost::graph,它们都是想切合 STL like 的迭代器模式,实际用起来是很麻烦的。我想这也是为什么即使是 C++ 项目,大多数人也自己用更基本的数据结构 std::map std::set std::list 等组装。

我花了两天时间来实现上面的想法,代码放在 github 上,尚还有一些部分(主要是生命期管理)需要完善。

在我的实现中,我设计了以下数据结构:

tree_blueprint表示树结构蓝图,在蓝图阶段可以从根开始构建树,增加子节点,但不可以删除和移动节点。这是因为这些灵活的需求多半在编辑器中才有,而编辑器完全可以用动态语言来实现编辑过程,蓝图只需要满足基本的构建需求就可以了。如果需要修改蓝图,不如从头创建一个新的。蓝图的序列化过程并没有实现,但它应该很容易做到。蓝图结构主要记录的是树的拓扑形态,每个节点上的附加属性用一个 void * userdata 传入,它不关心具体是什么。

tree_patch是针对某张已经定型(不再修改)的蓝图的一些修改,它由一系列的补丁循序组成。每个补丁记录有对应的节点遍历次序号(order) , 和修改过的属性段 userdata 。补丁也允许对蓝图上对应节点的做额外挂接 (mount) ,但不可以新增子节点。重新挂接的子树是一个 tree 结构,而不能使蓝图节点。

tree是tree_blueprint和tree_patch的联合。一张蓝图和一组对应的补丁,构成了一个运行时的树结构。蓝图斯不可修改的,可以被多个 tree 引用。补丁是 tree 唯一的,不可被引用。补丁在每次新增节点后,都会生成一个新版本。对已有节点上修改属性数据则不会影响补丁的版本。

tree_node是对 tree 上任意节点的引用,每个节点可以有多个tree_node引用共享。实际封装到上层后,可以用 cache 来保证做唯一引用以简化生命期管理。所有的运行期读写树节点的操作都需要通过tree_node来完成。

tree_blueprint可以 print 出tree,tree可以取得根节点的tree_node,tree_node可以 setpatch 和 getpatch ,也可以 read 出对应蓝图上的原始属性信息,还可以 fetch 其子节点,或 mount 一个新的 tree 。

我觉得把出去tree_node之外的结构统一起来做生命期管理比较好。因为他们的数据间的相互引用关系比较复杂,用标记清除的垃圾回收算法比用引用计数更为简单牢靠。所以另外设计了一个tree_manager结构,管理了所有的以上结构,并用数字 id 来自带具体对象。

tree_node是个例外,在 api 设计时,全部都由调用者传入这个结构的内存,它是对实际数据的引用,这里并没有做反向引用,也就是说 tree 本身并不知道自己被多少tree_node引用过了,我想这个问题在上层封装中比较容易解决。比如在 lua 封装中,可以用 tree 的 id 和 order 做唯一索引来制作一个tree_node的 cache 。

这里预设的场景是:fetch 一个 tree node 预留额外的属性空间这个操作是比较低频的,可以接受较大的时间成本。因为实际使用的时候,我们都是在初始化阶段就把需要关注的节点引用好,或者只在第一次访问时做唯一一次 fetch 操作。一旦保留了 tree node 对象,读写上面的属性应该是最快的,保证 O(1) 时间可以完成。遍历是另一个需要关注的性能热点,数据结构的设计应该尽量满足遍历最快,空间最省。

树的遍历接口我仔细设计过,虽然用起来比较麻烦,但基本保证了高内聚性。好在上层封装也仅有有限的几处调用树的遍历:渲染遍历、更新遍历、点击测试遍历。ejoy2d 并没有专门对遍历做封装,而是简单的复制了遍历代码;这次树结构变复杂,恐怕就不适合复制代码了。

这次我使用了一个回调函数的接口:

void (*tree_traverse_func)(void *bp, void *patch, void *argument, void *stackframe);

在访问每个节点时都会调用,前两个参数指蓝图上绑定的 userdata 及补丁(如果有)上绑定的 userdata 。

后两个参数用于模拟递归层次。argment 指上层调用传入的数据块,对于根节点,它就是tree_traverse调用传入的数据;然后在遍历过程中,遍历函数会在自己的栈帧上分配出一块内存,通过 stackframe 参数给回调函数,回调函数可以把需要传递给下层的数据写入,遍历框架会将其用 argument 传给下一层节点。如果当前访问的节点是叶节点,那么 stackframe 为 NULL 。

解释起来可能说不太清楚,可以看看例子里是怎么使用的。

]]>
因为图形引擎中的对象天然适合用树 (n-ary tree) 表达,所以它在图形引擎中被广泛使用。通常,子节点会继承父节点的一些状态,比如变换矩阵,在渲染或更新的时候,可以通过先序遍历逐级相乘。

在 PC 内存充裕的条件下,我们通常不必考虑树结构储存的开销,所以大多数图形引擎通常会为每个渲染对象独立生成一个树结构,比如 Unity 中的 GameObject 就是这么一个东西。在 Ejoy2D 中,从节约内存的角度考虑,把树节点上的一部分可共享的状态信息(不变的矩阵、纹理坐标等)移到了资源数据块中,但是树结构的拓扑关系还是在新创建出每个 sprite 时复制了一份。

随着游戏制作的工艺提高,而大众使用的移动设备的内存增长有限,这部分的开销慢慢变得显著。在我们正在开发的几个项目中,渲染对象本身,而不算图形资源(贴图、模型等),也占据了以 M 为单位计算的可用内存。比如在一个项目中,某个巨型对象由多达 2000+ 个节点构成,创建速度和内存开销都成为一个不可忽视的问题。由于是于自研引擎,所以同事尝试过一些优化的尝试。

图形引擎处理的大多数的树结构对象,其实仅在编辑环境会对树的拓扑关系进行调整:增加、移动、复制节点。而运行环境下,树结构本身几乎是不变的。一般的树结构的实现是用指针来相互引用,编辑好的资源是树结构的序列化结果,而创建过程是一个反序列化过程,在内存中重建整棵树,并用指针重新建立节点间的联系。比如在 Unity 中,就把这种编辑好的树对象叫做 prefab 预制件。如果预制件比较复杂,加载预制件和从预制件中构建对象的时间成本都不算低。

一个优化方法是去掉运行时内存中的指针,改用 id 或资源数据块中的偏移量,这样,预制件可以直接从资源文件中成块读入,创建内存对象时也只需要用指针引用即可。可以节省大量在内存中重建的时间。Ejoy2D 现在就是这么做的。去掉指针的额外好处是在 64 位系统下,可以从 8 字节的引用开销减少到 4 字节(或更少)。Ejoy2D 当初做此修改,为一个项目一举减少了 10M+ 的运行期内存占用。而且资源文件可以在暂时不用的时候移出内存,等需要的时候再加载回来,而不用担心数据的内存地址发生了变化。

但是,如果想让游戏对象直接共享使用预制件,最核心需要解决的问题是:大部分对象并不能做到完全不修改树结构,且预制件中的树的拓扑关系并不直接对应到内存。

我们先来看对应关系的问题:

在 ejoy2d 中,资源(预制件)中的对象结构其实并不是树,而是一个有向无环图。例如:美术制作了一个房子,房子上挂着三个灯笼,在资源中,灯笼只有一个预置对象,是房子对它有三个引用。而在运行期,三个灯笼必须是三个独立对象;程序是有能力对它们分别修改属性的。比如改改颜色,摆动频率等等。也就是说,这个房子的预制件一但在内存中还原,原本共享的灯笼子物件,必须有三份可供修改的状态数据区。

再来看前一个问题:

对于制作好的预制件,运行期很可能对树上的某个节点做属性调整。比如、预制件制作了一个仓库,仓库中的货物就是由运行时决定的。我们在陌陌争霸中,为仓库的货物状态预设了数帧图片,在运行时可以设置显示第几帧,用来表示仓库的库存情况。如果仓库中的货物还可以根据运行情况实际表现出来,则还需要用到挂接的功能:把货物对象挂接到仓库预设的挂接点上。

由于这些实现上的复杂需求,ejoy2d 和大多数图形引擎一样,从预制件创建出游戏对象直接简单的重建树的过程。当你从资源中创建一个 sprite ,你会发现逐层创建出了 lua 对象。然而大多数情况下,运行期并不会修改其中的绝大多数对象的属性,而仅使用资源中预制好的那一份。这就造成了一些浪费。如果资源中的树不算复杂时,这个浪费是有限的;但是我们的一个使用 ejoy2d 开发的新项目,随着内部工具的完善,美术逐渐创作出越来越复杂的结构。使用 ejoy2d 做新项目开发的同学花了半年多精力想优化这个问题。

我最近也重新思考了一下,这个问题的本质是,如果已有一个层级复杂的树状数据结构(下面称它为蓝图),我们可不可以做到有很多基于它的副本,每个副本对其中的部分做修改,而共享那些没有修改的部分。

比较容易想到的方案有这些:

  1. 在蓝图的每个节点上预留一个指针,指向一个列表;如果有副本向修改这个节点,就把修改后的属性加到这个列表中。在遍历这个节点时,一旦发现这个节点有人修改过,就找到对应的修改数据。另外,蓝图中如果有重复引用的节点(例如上面提到的屋檐下挂的灯笼),在修改其属性时,需要拷贝分裂。

  2. 对 1 方案做一些修改,预留指针只保存一个槽位,副本修改属性记录在副本对象中,而在遍历蓝图前,将副本中修改过的属性填入蓝图,遍历完成后再清除;或是在填入的同时把副本指针也记录在节点上,遍历时忽略非自己修改的节点。

  3. 在副本中保留一个修改节点数据的 hash 表,遍历蓝图的时候查询 hash 表,找到可能修改的属性。

  4. 在修改蓝图的某个节点时,部分复制创建出一颗不完全树,只保留被修改属性的节点和它们的祖先;让那些没有修改的分支引用回蓝图。遍历的时候改遍历复制出来的不完全树。

这几个方案大同小异,我们实际在项目中采用的方案 1 ,数据结构比原先的 ejoy2d 复杂了很多,衍生出不少 bug ,目前还不够稳定。

我这几天的思考结果是,在方案 3 的基础上做改进,或许是更好的选择。

方案三的本质是:运行时的树由 蓝图 和 对蓝图的补丁 两个部分构成。这样从数据结构上来说,它们是独立的两块数据,相互引用关系比较简单。可能的额外开销是遍历每个节点时需要额外的一次 hash 表查询,虽然大多数情况下是 Hash 表查询是 O(1) 的操作,但实际上会因为冲突而 O(1) 高一些,尤其是这个方案的初衷之一就是节省内存,如果预留比较大的 hash 表池来减少冲突率的话,很可能剩下的内存又再次消耗掉了。另外 hash 表的 key 也不太好选择,不能直接用节点对象的指针,因为在蓝图中有大量的对象是被共享的。

怎么改进呢?

其实、我们对树的遍历方式其实是唯一的。在图形引擎中多半选择深度优先的先序遍历,也就是先访问根节点,再依次访问子节点。如果树的拓扑结构稳定,其实每次遍历的过程都是唯一的。所以运行时的每个节点其实都可以根据其遍历次序有一个不变的唯一序号。对于上面提到的屋檐下的灯笼,多个灯笼虽然共享着蓝图上的同一块数据,但每个灯笼,以及灯笼下的子节点都有不同的访问编号。我们可以用这个编号来索引附加(修改的)节点属性。

由于访问次序固定,我们也不再需要用 hash 表来记录 patch 集合,而只需要按访问先后次序来排列好 patch 即可。再访问过程中,每访问到一个节点,就把访问编号加一,检查 patch 链表头就知道是否当前节点丫头修改属性了,这样就可以做到严格的 O(1) 时间,同时也不增加内存开销。

order 的值虽然无法直接记录在蓝图上,但我们可以在蓝图中预算好每个子树的总节点数,这样计算节点的 order 成本仅 O(log n) 。而且仅在取节点引用,或是从非根节点开始遍历时才需要计算一次。

还剩一个问题:

遍历树结构变复杂了,即使我们把遍历算法写对,却很难用常规手法封装。用简单的迭代器模式是不够的,遍历树不仅仅需要访问到每个节点,还需要在遍历过程中感知到树的遍历层次变化。比如在 ejoy2d 现在的公开版本中,需要按树层级来累乘变换矩阵,这个临时矩阵是存放在遍历过程中的函数调用栈上的。我们还可能根据枝干上的(隐藏)属性来跳过子节点等等。

否则,既然访问持续恒定,为什么不直接把树摊平?btw, 在上面提到的当前项目中,维护引擎的同学已经把摊平树结构作为了一项优化。

我还没有见过哪个 C 版本的 n-ary tree 能够把接口设计好,做到高内聚性的实现;大多数情况下,C 语言的项目中都会根据实际需要来实现一个仅满足自身需要的 n-ary tree ,把模块内聚性的边界放在更高一些的层次,而不直接暴露树结构的接口。

C++ 倒是有几个树结构的实现:比如tree.hh以及boost::graph,它们都是想切合 STL like 的迭代器模式,实际用起来是很麻烦的。我想这也是为什么即使是 C++ 项目,大多数人也自己用更基本的数据结构 std::map std::set std::list 等组装。

我花了两天时间来实现上面的想法,代码放在 github 上,尚还有一些部分(主要是生命期管理)需要完善。

在我的实现中,我设计了以下数据结构:

tree_blueprint表示树结构蓝图,在蓝图阶段可以从根开始构建树,增加子节点,但不可以删除和移动节点。这是因为这些灵活的需求多半在编辑器中才有,而编辑器完全可以用动态语言来实现编辑过程,蓝图只需要满足基本的构建需求就可以了。如果需要修改蓝图,不如从头创建一个新的。蓝图的序列化过程并没有实现,但它应该很容易做到。蓝图结构主要记录的是树的拓扑形态,每个节点上的附加属性用一个 void * userdata 传入,它不关心具体是什么。

tree_patch是针对某张已经定型(不再修改)的蓝图的一些修改,它由一系列的补丁循序组成。每个补丁记录有对应的节点遍历次序号(order) , 和修改过的属性段 userdata 。补丁也允许对蓝图上对应节点的做额外挂接 (mount) ,但不可以新增子节点。重新挂接的子树是一个 tree 结构,而不能使蓝图节点。

tree是tree_blueprint和tree_patch的联合。一张蓝图和一组对应的补丁,构成了一个运行时的树结构。蓝图斯不可修改的,可以被多个 tree 引用。补丁是 tree 唯一的,不可被引用。补丁在每次新增节点后,都会生成一个新版本。对已有节点上修改属性数据则不会影响补丁的版本。

tree_node是对 tree 上任意节点的引用,每个节点可以有多个tree_node引用共享。实际封装到上层后,可以用 cache 来保证做唯一引用以简化生命期管理。所有的运行期读写树节点的操作都需要通过tree_node来完成。

tree_blueprint可以 print 出tree,tree可以取得根节点的tree_node,tree_node可以 setpatch 和 getpatch ,也可以 read 出对应蓝图上的原始属性信息,还可以 fetch 其子节点,或 mount 一个新的 tree 。

我觉得把出去tree_node之外的结构统一起来做生命期管理比较好。因为他们的数据间的相互引用关系比较复杂,用标记清除的垃圾回收算法比用引用计数更为简单牢靠。所以另外设计了一个tree_manager结构,管理了所有的以上结构,并用数字 id 来自带具体对象。

tree_node是个例外,在 api 设计时,全部都由调用者传入这个结构的内存,它是对实际数据的引用,这里并没有做反向引用,也就是说 tree 本身并不知道自己被多少tree_node引用过了,我想这个问题在上层封装中比较容易解决。比如在 lua 封装中,可以用 tree 的 id 和 order 做唯一索引来制作一个tree_node的 cache 。

这里预设的场景是:fetch 一个 tree node 预留额外的属性空间这个操作是比较低频的,可以接受较大的时间成本。因为实际使用的时候,我们都是在初始化阶段就把需要关注的节点引用好,或者只在第一次访问时做唯一一次 fetch 操作。一旦保留了 tree node 对象,读写上面的属性应该是最快的,保证 O(1) 时间可以完成。遍历是另一个需要关注的性能热点,数据结构的设计应该尽量满足遍历最快,空间最省。

树的遍历接口我仔细设计过,虽然用起来比较麻烦,但基本保证了高内聚性。好在上层封装也仅有有限的几处调用树的遍历:渲染遍历、更新遍历、点击测试遍历。ejoy2d 并没有专门对遍历做封装,而是简单的复制了遍历代码;这次树结构变复杂,恐怕就不适合复制代码了。

这次我使用了一个回调函数的接口:

void (*tree_traverse_func)(void *bp, void *patch, void *argument, void *stackframe);

在访问每个节点时都会调用,前两个参数指蓝图上绑定的 userdata 及补丁(如果有)上绑定的 userdata 。

后两个参数用于模拟递归层次。argment 指上层调用传入的数据块,对于根节点,它就是tree_traverse调用传入的数据;然后在遍历过程中,遍历函数会在自己的栈帧上分配出一块内存,通过 stackframe 参数给回调函数,回调函数可以把需要传递给下层的数据写入,遍历框架会将其用 argument 传给下一层节点。如果当前访问的节点是叶节点,那么 stackframe 为 NULL 。

解释起来可能说不太清楚,可以看看例子里是怎么使用的。

]]>
0
<![CDATA[自建一个电话呼叫中心要多少钱?]]> http://www.udpwork.com/item/16306.html http://www.udpwork.com/item/16306.html#reviews Sun, 18 Jun 2017 00:46:22 +0800 ideawu http://www.udpwork.com/item/16306.html 我十分看不惯任何行业的潜规则行为。自建一个电话呼叫中心的报价是多少钱?没有人敢公开报价。我明说吧,自建一个电话呼叫中心,只需要3万元左右,而且还能更省钱。

这个报价是针对小型企业的,也就是广大人民群众。至于大型企业,它们自己去定制,钱不是问题。

3万元建一个电话呼叫中心,包括什么?包括硬件设备,软件。软件是硬件设备上免费赠送的,不要钱!有了这个呼叫中心,你可以有语音导航功能(也就是按0转人工客服),还有人工客服排队,电话录音。够用了,中小企业没那么多花哨的需求。

其它的什么狗屁工单,CRM,根本没人用,没这个需求,不会有人愿意付费的。这年头,开发一个工单系统CRM系统根本就是几块钱的事,再说,现在的公司哪个没有自己的CRM工作流?没有人愿意为这些鸡肋功能付费,所以很多厂商逮着一个人就漫天报价,10万,20万,100万!就想着坑到一个是一个。

Related posts:

  1. 音频编码的一些笔记
  2. SIP INVITE 会话建立过程
  3. SIP tag 和 Call-ID 的区别
  4. SIP报文Via和Contact的区别
  5. 流式布局的原理和代码实现
]]>
我十分看不惯任何行业的潜规则行为。自建一个电话呼叫中心的报价是多少钱?没有人敢公开报价。我明说吧,自建一个电话呼叫中心,只需要3万元左右,而且还能更省钱。

这个报价是针对小型企业的,也就是广大人民群众。至于大型企业,它们自己去定制,钱不是问题。

3万元建一个电话呼叫中心,包括什么?包括硬件设备,软件。软件是硬件设备上免费赠送的,不要钱!有了这个呼叫中心,你可以有语音导航功能(也就是按0转人工客服),还有人工客服排队,电话录音。够用了,中小企业没那么多花哨的需求。

其它的什么狗屁工单,CRM,根本没人用,没这个需求,不会有人愿意付费的。这年头,开发一个工单系统CRM系统根本就是几块钱的事,再说,现在的公司哪个没有自己的CRM工作流?没有人愿意为这些鸡肋功能付费,所以很多厂商逮着一个人就漫天报价,10万,20万,100万!就想着坑到一个是一个。

Related posts:

  1. 音频编码的一些笔记
  2. SIP INVITE 会话建立过程
  3. SIP tag 和 Call-ID 的区别
  4. SIP报文Via和Contact的区别
  5. 流式布局的原理和代码实现
]]>
0
<![CDATA[SIP tag 和 Call-ID 的区别]]> http://www.udpwork.com/item/16305.html http://www.udpwork.com/item/16305.html#reviews Fri, 16 Jun 2017 19:29:25 +0800 ideawu http://www.udpwork.com/item/16305.html SIP 的一次通话,可以通过 From, To, Call-ID 三元组来区分。但是,为什么 From 和 To 不用固定的地址,而要在地址后面加上tag=随机数呢?

tag 的目的是为了解决自己给自己打电话的问题(Hairpinning)。如果你自己给自己打电话,那么你应该有两个 Session,但是,如果 From 和 To 是固定的,你就无法区别这两个 Session 哪个是 caller 哪个是 callee。发送 INVITE 时,caller 会在 From 中带有tag=随机数,而 callee 发送响应时,在 To 后面补充tag=随机数,不同的随机数分别表示 caller 和 callee。

所以,RFC 3261 中说:

The combination of the To tag, From tag, and Call-ID completely defines a peer-to-peer SIP relationship...

用的是 From tag 和 To tag,而不是用 From 和 To。

Related posts:

  1. P2P 的定义
  2. Ideawu.P2P API 简介
  3. P2P应用的架构
  4. The Day You Went Away — M2M
  5. 让你的网站支持手机二维码登录
]]>
SIP 的一次通话,可以通过 From, To, Call-ID 三元组来区分。但是,为什么 From 和 To 不用固定的地址,而要在地址后面加上tag=随机数呢?

tag 的目的是为了解决自己给自己打电话的问题(Hairpinning)。如果你自己给自己打电话,那么你应该有两个 Session,但是,如果 From 和 To 是固定的,你就无法区别这两个 Session 哪个是 caller 哪个是 callee。发送 INVITE 时,caller 会在 From 中带有tag=随机数,而 callee 发送响应时,在 To 后面补充tag=随机数,不同的随机数分别表示 caller 和 callee。

所以,RFC 3261 中说:

The combination of the To tag, From tag, and Call-ID completely defines a peer-to-peer SIP relationship...

用的是 From tag 和 To tag,而不是用 From 和 To。

Related posts:

  1. P2P 的定义
  2. Ideawu.P2P API 简介
  3. P2P应用的架构
  4. The Day You Went Away — M2M
  5. 让你的网站支持手机二维码登录
]]>
0
<![CDATA[SIP报文Via和Contact的区别]]> http://www.udpwork.com/item/16304.html http://www.udpwork.com/item/16304.html#reviews Fri, 16 Jun 2017 18:54:05 +0800 ideawu http://www.udpwork.com/item/16304.html Via 是网络层的信息,SIP 报文将通过网络层发往这两个地址。Contact 是业务上的地址。那么问题是,应该发往哪个?

正确的做法是,请求响应模式中的响应发往 Via。如果解析 DNS 之后能直连 Contact,那么之后的报文(无论是否是请求响应模式)发往 Contact。

请求如果经过多个代理,每个代理都增加自己的 Via,变成 Via 列表。最终节点回复响应时,带有全部 Via 列表,根据最后一个 Via 获知要发送的目的网络地址。每个代理转发响应时,把最后一个属于自己的 Via 删除,再根据前一个 Via 得到要转发的目的网络地址。

这样,代理可以做无状态转发请求和响应,其中请求转发依赖路由表,响应转发依赖 Via 列表。

Related posts:

  1. SIP INVITE 会话建立过程
  2. SIP tag 和 Call-ID 的区别
  3. 基于 TCP UDP 协议的实时流媒体的实时性分析
  4. 音频编码的一些笔记
  5. WebRTC源码架构浅析
]]>
Via 是网络层的信息,SIP 报文将通过网络层发往这两个地址。Contact 是业务上的地址。那么问题是,应该发往哪个?

正确的做法是,请求响应模式中的响应发往 Via。如果解析 DNS 之后能直连 Contact,那么之后的报文(无论是否是请求响应模式)发往 Contact。

请求如果经过多个代理,每个代理都增加自己的 Via,变成 Via 列表。最终节点回复响应时,带有全部 Via 列表,根据最后一个 Via 获知要发送的目的网络地址。每个代理转发响应时,把最后一个属于自己的 Via 删除,再根据前一个 Via 得到要转发的目的网络地址。

这样,代理可以做无状态转发请求和响应,其中请求转发依赖路由表,响应转发依赖 Via 列表。

Related posts:

  1. SIP INVITE 会话建立过程
  2. SIP tag 和 Call-ID 的区别
  3. 基于 TCP UDP 协议的实时流媒体的实时性分析
  4. 音频编码的一些笔记
  5. WebRTC源码架构浅析
]]>
0
<![CDATA[Webapp执行reload后内存泄漏之SSLSocketFactory]]> http://www.udpwork.com/item/16303.html http://www.udpwork.com/item/16303.html#reviews Fri, 16 Jun 2017 12:00:00 +0800 wendal http://www.udpwork.com/item/16303.html tomcat/jetty是怎么做reload的呢?

首先, webapp的WEB-INF/lib目录并不在jvm的classpath内, javaee容器(tomcat/jetty/jboss)是通过自定义的ClassLoader来加载它们的.

而这个ClassLoader,通常的名字就叫做WebappClassLoader, 容器会为每个webapp的每次启动,都创建一个新的ClassLoader.

“每个webapp”,保证了不同webapp之间的类隔离, 例如有A/B两个webapp,都使用了DEF类的XXX静态属性,那么在JVM里面就有2份DEF类,两份XXX静态属性.

“每次启动”, 是因为容器会先执行一次unload,再执行load,相当于一个新的webapp加载进来.

为什么reload有泄漏?

首先,什么是泄漏? 就是你创建了某些对象/数据, 期望它会被GC, 但事实上没有.

那为啥不被GC呢? 那肯定是被引用了.

虚无的ROOT --> 根ClassLoader --> 一些类的静态属性 --> 对象(webapp里面创建的对象) --> 对象的类 --> WebappClassLoader --> 类 --> 静态属性
虚无的ROOT --> 线程组 --> 线程 --> 对象(webapp里面创建的对象) --> 对象的类 --> WebappClassLoader --> 其他类 --> 静态属性

两条路径:

  • webapp内创建的对象,赋值到了根ClassLoader加载的一个类的静态属性上.
  • webapp内创建的线程,reload之后也没有stop

一时半刻想不出其他路径了 -_-

静态属性的实例

一个非常非常经典的写法, 忽略Https的无效证书(通常是自签名证书)

            SSLContext sc = SSLContext.getInstance("SSL");
            TrustManager[] tmArr = {new X509TrustManager() {
                // 全是空实现,不写出来了.
            }};
            sc.init(null, tmArr, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

HttpsURLConnection并非WebappClassLoader加载,而是由根ClassLoader加载.

然后, new X509TrustManager(){}所创建的匿名内部类对象的类,是由WebappClassLoader加载的.

所以呢, 上述代码就把一个 WebappClassLoader所加载的类的实例,赋值给根ClassLoader加载的类的一个静态属性.

最后, 当Webapp被reload时, 老的WebappClassLoader不会被GC, 直至上述代码再被执行,属性值被覆盖.

在一年前, nutz的Http也是这个写法, 后来改成HttpsURLConnection的实例方法setSSLSocketFactory

类似的代码存在于很多需要访问第三方网站的java库里面, 例如jpush, socialauth

线程创建导致的泄漏

这种就只能靠自律了… 例如dubbo里面就建立一堆线程池,然而没有提供销毁的方法…

]]>
tomcat/jetty是怎么做reload的呢?

首先, webapp的WEB-INF/lib目录并不在jvm的classpath内, javaee容器(tomcat/jetty/jboss)是通过自定义的ClassLoader来加载它们的.

而这个ClassLoader,通常的名字就叫做WebappClassLoader, 容器会为每个webapp的每次启动,都创建一个新的ClassLoader.

“每个webapp”,保证了不同webapp之间的类隔离, 例如有A/B两个webapp,都使用了DEF类的XXX静态属性,那么在JVM里面就有2份DEF类,两份XXX静态属性.

“每次启动”, 是因为容器会先执行一次unload,再执行load,相当于一个新的webapp加载进来.

为什么reload有泄漏?

首先,什么是泄漏? 就是你创建了某些对象/数据, 期望它会被GC, 但事实上没有.

那为啥不被GC呢? 那肯定是被引用了.

虚无的ROOT --> 根ClassLoader --> 一些类的静态属性 --> 对象(webapp里面创建的对象) --> 对象的类 --> WebappClassLoader --> 类 --> 静态属性
虚无的ROOT --> 线程组 --> 线程 --> 对象(webapp里面创建的对象) --> 对象的类 --> WebappClassLoader --> 其他类 --> 静态属性

两条路径:

  • webapp内创建的对象,赋值到了根ClassLoader加载的一个类的静态属性上.
  • webapp内创建的线程,reload之后也没有stop

一时半刻想不出其他路径了 -_-

静态属性的实例

一个非常非常经典的写法, 忽略Https的无效证书(通常是自签名证书)

            SSLContext sc = SSLContext.getInstance("SSL");
            TrustManager[] tmArr = {new X509TrustManager() {
                // 全是空实现,不写出来了.
            }};
            sc.init(null, tmArr, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

HttpsURLConnection并非WebappClassLoader加载,而是由根ClassLoader加载.

然后, new X509TrustManager(){}所创建的匿名内部类对象的类,是由WebappClassLoader加载的.

所以呢, 上述代码就把一个 WebappClassLoader所加载的类的实例,赋值给根ClassLoader加载的类的一个静态属性.

最后, 当Webapp被reload时, 老的WebappClassLoader不会被GC, 直至上述代码再被执行,属性值被覆盖.

在一年前, nutz的Http也是这个写法, 后来改成HttpsURLConnection的实例方法setSSLSocketFactory

类似的代码存在于很多需要访问第三方网站的java库里面, 例如jpush, socialauth

线程创建导致的泄漏

这种就只能靠自律了… 例如dubbo里面就建立一堆线程池,然而没有提供销毁的方法…

]]>
0
<![CDATA[树莓派新手入门教程]]> http://www.udpwork.com/item/16302.html http://www.udpwork.com/item/16302.html#reviews Thu, 15 Jun 2017 17:41:51 +0800 阮一峰 http://www.udpwork.com/item/16302.html 树莓派(Raspberry Pi)是学习计算机知识、架设服务器的好工具,价格低廉,可玩性高。

本文根据我的亲身经验,介绍如何从零开始,搭建一个树莓派服务器,控制 LED 灯。你会看到,树莓派玩起来实在很容易。

我要感谢100offer对我提供赞助。100offer是国内第一流的人力资源服务网站,本文结尾有他们的简介,最近想换工作的朋友可以看一下

一、型号

树莓派是一个迷你电脑,集成在一块电路板。目前,最新的型号有两个。

(1)Raspberry Pi 3代 B 型

(2)Raspberry Pi zero (含 zero w)

虽然后者便宜,但是少了许多接口(比如只有一个 USB 口),CPU 和内存都比较低,配件也少,因此推荐购买第3代的 B 型。以下都针对这个型号,但大部分内容对 zero 也适用。

二、配件

树莓派本身只是一个主机。要运行起来,必须有配件。

(1)电源

Micro USB 接口的手机充电器,就可以充当电源,但输出必须是 5V 电压、至少 2A 电流。充电宝当电源也没问题。

(2)Micro SD 卡

树莓派不带硬盘,Micro SD 卡就是硬盘。最小容量8G,推荐使用16G和32G的卡。

(3)显示器

树莓派有 HDMI 输出,显示器必须有该接口。如果有 HDMI 转 VGA 的转接线,那么 VGA 显示器也可以。我用的是一个 7 寸的液晶监视器。

不过,显示器只在安装系统时需要,后面可以SSH登录,就不需要了。

(4)无线键鼠

树莓派内置蓝牙,USB 或蓝牙的无线键鼠都可以用。

就像显示器一样,如果树莓派已经装好系统,而且只当作服务器,无线键鼠也可以不配。

三、电子元件

除了配件,下面的实验还需要一些电子元件。

(1)面包板(一块)

(2)连接线(若干)

注意,连接线必须一端是公头,一端是母头。

另外,最好也备一些两端都是公头的连接线。

(3)LED 二极管(若干)

(4)270欧姆的电阻(若干)

四、安装系统

如果商家已经装好系统,可以跳过这一步,否则需要自己安装操作系统。

官方提供的操作系统是Raspbian,这是 Debian 系统的定制版。

官方还提供一个安装器NOOBS,建议通过它来安装 Raspbian,相对简单一点。

  1. 下载 NOOBS
  2. 格式化 Micro SD 卡为 FAT 格式(操作指导)。
  3. 解压NOOBS.zip到 Micro SD 卡根目录。
  4. 插入 Micro SD 卡到树莓派底部的卡槽,接通电源,启动系统。

正常情况下,按照屏幕上的提示,一路回车,就能装好系统。

五、SSH 登录

安装系统后,树莓派就可以上网了(Wifi 或者网线)。这时,你要看一下它的局域网 IP 地址,可以使用下面的命令。

$ sudo ifconfig

然后,更改系统设置,打开 SSH 登录(默认是禁止的)。

接着,从另一台电脑 SSH 登录树莓派。下面的命令是在局域网的另一台电脑上执行的。

$ ssh pi@192.168.1.5

上面代码中,192.168.1.5是我的树莓派的地址,你要换成你的地址。树莓派的默认用户是pi。

树莓派会提示你输入密码。pi的默认密码是raspberry。正常情况下,这样就可以登录树莓派了。接着,就可以进行各种服务器操作了,比如修改密码。

$ passwd

后面的实验需要将用户加入gpio用户组。

$ sudo adduser pi gpio

上面的代码表示将用户pi加入gpio用户组。

六、安装 Node

为了运行 Node 脚本,树莓派必须安装 Node,可以参考这篇文章

$ curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
$ sudo apt install nodejs

正常情况下,Node 8.x 版就已经安装成功了。

$ node -v
v8.1.0

七、点亮 LED

树莓派提供了一组对外的 IO 接口,称为 GPIO( 通用 IO 接口,General-purpose input/output)。

它的 40 个脚的定义如下图。

注意,左上角的第1针(3.3V)是一个方块,其他针脚都是圆的。将树莓派翻过来,背后可以看到 GPIO 有一个角是方的,通过这种方法就可以确认哪一个针眼是3.3V。

通过 GPIO ,树莓派可以与其他电子元件连接。下面根据 Jonathan Perkin 的文章,使用树莓派连接 LED 二极管。

这里需要用到面包板。本质上,面包板就是几根导线,上面开了许多可以连到导线的孔。

+极和-极是两根垂直的导线,标着1、5、10这些数字的行,每一行都是一根水平的导线。导线与导线之间互不连接,另外,面包板的左右两半也是互不连接的。

然后,按照下面的图,将树莓派、面包板、LED 灯、电阻连起来。

上图中,红色导线表示电流的正极,从 GPIO 的第1针(3.3V)连到面包板。黑色导线表示电流的负极,从 GPIO 第三排的第6针(ground)连到面包板。它们连到面包板的哪个眼并不重要,但必须保证能组成一个完整的电路(上图的箭头流向)。注意,LED 二极管也有正负极,长脚表示正极,短脚表示负极。电阻没有正负极。

连接完成后,打开树莓派的电源,LED 应该就会亮起来了。

八、LED 控制脚本

下面,我们使用 Node 脚本控制 LED。

首先,将正极的导线从1号针脚(3.3V)拔出,插到第6排的11号针脚(上图的 GPIO 17)。这个针脚的电流是脚本可以控制的。

然后,在树莓派上新建一个实验目录,并安装控制 GPIO 的 Node 模块rpio。。

$ mkdir led-demo && cd led-demo
$ npm init -y
$ npm install -S rpio

接着,新建一个脚本led-on.js

// led-on.js
var rpio = require('rpio');

// 打开 11 号针脚(GPIO17) 作为输出
rpio.open(11, rpio.OUTPUT);

// 指定 11 号针脚输出电流(HIGH)
rpio.write(11, rpio.HIGH);

运行这个脚本,应该就会看到 LED 灯泡变亮了。

$ node led-on.js

再新建一个led-off.js脚本,只要改一行(完整代码看这里)。

// led-off.js
//...

// 指定 11 号针脚停止输出电流(LOW)
rpio.write(11, rpio.LOW);

运行这个脚本,LED 灯泡应该就会熄灭了。

$ node led-off.js

有了这两个脚本,让 LED 闪烁就轻而易举了。新建一个led-blink.js脚本。

// led-blink.js
var rpio = require('rpio');
rpio.open(11, rpio.OUTPUT);

function blink() {
  rpio.write(11, rpio.HIGH);
  setTimeout(function ledoff() {
    rpio.write(11, rpio.LOW);
  }, 50);
}

setInterval(blink, 100);

上面的脚本让 LED 每秒闪烁10次。

$ node led-blink.js

九、HTTP 服务器

通过控制 LED 可以做很多事,比如架设一个 HTTP 服务器,每当有人访问,LED 就闪烁一下。

首先,在刚才的目录里面装一个服务器模块。

$ npm install -S server

然后,新建一个脚本server.js(完整代码看这里)。

// server.js
var server = require('server');
var { get } = server.router;

// ...

server({ port: 8080 }, [
  get('/' ,  ctx => {
    console.log('a request is coming...');
    blink();
  }),
]);

console.log('server starts on 8080 port');

运行这个脚本。

$ node server.js

然后,再打开一个命令行终端,访问8080端口,LED 就会闪一下。

$ curl http://localhost:8080

好了,今天的教程就到这里。接下来,你可以自己探索,做更多的尝试,比如写一个测试用例脚本,只要测试失败 LED 就会长亮,或者组装一个8位的加法器

(正文完)

================================

下面是推广时间,向大家推荐求职就业的好帮手----100offer

如果你对现在工作不甚满意,希望寻找更好的职位,或者你已经有了很多工作邀请,感到无所适从,不知道哪个最合适自己,请尝试一下100offer。它能让你节省精力,从海量机会中找到最适合自己的那个。

100offer会对平台上的人才和企业进行严格筛选,让"最好的人才"和"最好的公司"相遇。点击这里注册,提交资料并通过网站对您的评估和配对后,就可以收获 5~10 个满足你要求的好机会。

(完)

文档信息

]]>
树莓派(Raspberry Pi)是学习计算机知识、架设服务器的好工具,价格低廉,可玩性高。

本文根据我的亲身经验,介绍如何从零开始,搭建一个树莓派服务器,控制 LED 灯。你会看到,树莓派玩起来实在很容易。

我要感谢100offer对我提供赞助。100offer是国内第一流的人力资源服务网站,本文结尾有他们的简介,最近想换工作的朋友可以看一下

一、型号

树莓派是一个迷你电脑,集成在一块电路板。目前,最新的型号有两个。

(1)Raspberry Pi 3代 B 型

(2)Raspberry Pi zero (含 zero w)

虽然后者便宜,但是少了许多接口(比如只有一个 USB 口),CPU 和内存都比较低,配件也少,因此推荐购买第3代的 B 型。以下都针对这个型号,但大部分内容对 zero 也适用。

二、配件

树莓派本身只是一个主机。要运行起来,必须有配件。

(1)电源

Micro USB 接口的手机充电器,就可以充当电源,但输出必须是 5V 电压、至少 2A 电流。充电宝当电源也没问题。

(2)Micro SD 卡

树莓派不带硬盘,Micro SD 卡就是硬盘。最小容量8G,推荐使用16G和32G的卡。

(3)显示器

树莓派有 HDMI 输出,显示器必须有该接口。如果有 HDMI 转 VGA 的转接线,那么 VGA 显示器也可以。我用的是一个 7 寸的液晶监视器。

不过,显示器只在安装系统时需要,后面可以SSH登录,就不需要了。

(4)无线键鼠

树莓派内置蓝牙,USB 或蓝牙的无线键鼠都可以用。

就像显示器一样,如果树莓派已经装好系统,而且只当作服务器,无线键鼠也可以不配。

三、电子元件

除了配件,下面的实验还需要一些电子元件。

(1)面包板(一块)

(2)连接线(若干)

注意,连接线必须一端是公头,一端是母头。

另外,最好也备一些两端都是公头的连接线。

(3)LED 二极管(若干)

(4)270欧姆的电阻(若干)

四、安装系统

如果商家已经装好系统,可以跳过这一步,否则需要自己安装操作系统。

官方提供的操作系统是Raspbian,这是 Debian 系统的定制版。

官方还提供一个安装器NOOBS,建议通过它来安装 Raspbian,相对简单一点。

  1. 下载 NOOBS
  2. 格式化 Micro SD 卡为 FAT 格式(操作指导)。
  3. 解压NOOBS.zip到 Micro SD 卡根目录。
  4. 插入 Micro SD 卡到树莓派底部的卡槽,接通电源,启动系统。

正常情况下,按照屏幕上的提示,一路回车,就能装好系统。

五、SSH 登录

安装系统后,树莓派就可以上网了(Wifi 或者网线)。这时,你要看一下它的局域网 IP 地址,可以使用下面的命令。

$ sudo ifconfig

然后,更改系统设置,打开 SSH 登录(默认是禁止的)。

接着,从另一台电脑 SSH 登录树莓派。下面的命令是在局域网的另一台电脑上执行的。

$ ssh pi@192.168.1.5

上面代码中,192.168.1.5是我的树莓派的地址,你要换成你的地址。树莓派的默认用户是pi。

树莓派会提示你输入密码。pi的默认密码是raspberry。正常情况下,这样就可以登录树莓派了。接着,就可以进行各种服务器操作了,比如修改密码。

$ passwd

后面的实验需要将用户加入gpio用户组。

$ sudo adduser pi gpio

上面的代码表示将用户pi加入gpio用户组。

六、安装 Node

为了运行 Node 脚本,树莓派必须安装 Node,可以参考这篇文章

$ curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
$ sudo apt install nodejs

正常情况下,Node 8.x 版就已经安装成功了。

$ node -v
v8.1.0

七、点亮 LED

树莓派提供了一组对外的 IO 接口,称为 GPIO( 通用 IO 接口,General-purpose input/output)。

它的 40 个脚的定义如下图。

注意,左上角的第1针(3.3V)是一个方块,其他针脚都是圆的。将树莓派翻过来,背后可以看到 GPIO 有一个角是方的,通过这种方法就可以确认哪一个针眼是3.3V。

通过 GPIO ,树莓派可以与其他电子元件连接。下面根据 Jonathan Perkin 的文章,使用树莓派连接 LED 二极管。

这里需要用到面包板。本质上,面包板就是几根导线,上面开了许多可以连到导线的孔。

+极和-极是两根垂直的导线,标着1、5、10这些数字的行,每一行都是一根水平的导线。导线与导线之间互不连接,另外,面包板的左右两半也是互不连接的。

然后,按照下面的图,将树莓派、面包板、LED 灯、电阻连起来。

上图中,红色导线表示电流的正极,从 GPIO 的第1针(3.3V)连到面包板。黑色导线表示电流的负极,从 GPIO 第三排的第6针(ground)连到面包板。它们连到面包板的哪个眼并不重要,但必须保证能组成一个完整的电路(上图的箭头流向)。注意,LED 二极管也有正负极,长脚表示正极,短脚表示负极。电阻没有正负极。

连接完成后,打开树莓派的电源,LED 应该就会亮起来了。

八、LED 控制脚本

下面,我们使用 Node 脚本控制 LED。

首先,将正极的导线从1号针脚(3.3V)拔出,插到第6排的11号针脚(上图的 GPIO 17)。这个针脚的电流是脚本可以控制的。

然后,在树莓派上新建一个实验目录,并安装控制 GPIO 的 Node 模块rpio。。

$ mkdir led-demo && cd led-demo
$ npm init -y
$ npm install -S rpio

接着,新建一个脚本led-on.js

// led-on.js
var rpio = require('rpio');

// 打开 11 号针脚(GPIO17) 作为输出
rpio.open(11, rpio.OUTPUT);

// 指定 11 号针脚输出电流(HIGH)
rpio.write(11, rpio.HIGH);

运行这个脚本,应该就会看到 LED 灯泡变亮了。

$ node led-on.js

再新建一个led-off.js脚本,只要改一行(完整代码看这里)。

// led-off.js
//...

// 指定 11 号针脚停止输出电流(LOW)
rpio.write(11, rpio.LOW);

运行这个脚本,LED 灯泡应该就会熄灭了。

$ node led-off.js

有了这两个脚本,让 LED 闪烁就轻而易举了。新建一个led-blink.js脚本。

// led-blink.js
var rpio = require('rpio');
rpio.open(11, rpio.OUTPUT);

function blink() {
  rpio.write(11, rpio.HIGH);
  setTimeout(function ledoff() {
    rpio.write(11, rpio.LOW);
  }, 50);
}

setInterval(blink, 100);

上面的脚本让 LED 每秒闪烁10次。

$ node led-blink.js

九、HTTP 服务器

通过控制 LED 可以做很多事,比如架设一个 HTTP 服务器,每当有人访问,LED 就闪烁一下。

首先,在刚才的目录里面装一个服务器模块。

$ npm install -S server

然后,新建一个脚本server.js(完整代码看这里)。

// server.js
var server = require('server');
var { get } = server.router;

// ...

server({ port: 8080 }, [
  get('/' ,  ctx => {
    console.log('a request is coming...');
    blink();
  }),
]);

console.log('server starts on 8080 port');

运行这个脚本。

$ node server.js

然后,再打开一个命令行终端,访问8080端口,LED 就会闪一下。

$ curl http://localhost:8080

好了,今天的教程就到这里。接下来,你可以自己探索,做更多的尝试,比如写一个测试用例脚本,只要测试失败 LED 就会长亮,或者组装一个8位的加法器

(正文完)

================================

下面是推广时间,向大家推荐求职就业的好帮手----100offer

如果你对现在工作不甚满意,希望寻找更好的职位,或者你已经有了很多工作邀请,感到无所适从,不知道哪个最合适自己,请尝试一下100offer。它能让你节省精力,从海量机会中找到最适合自己的那个。

100offer会对平台上的人才和企业进行严格筛选,让"最好的人才"和"最好的公司"相遇。点击这里注册,提交资料并通过网站对您的评估和配对后,就可以收获 5~10 个满足你要求的好机会。

(完)

文档信息

]]>
0