├── .gitignore ├── 0x10 ├── 0x11-mars.md ├── 0x12-tinker.md └── 0x13-wcdb.md ├── ART下的方法内联策略及其对Android热修复方案的影响分析.md ├── Android_N混合编译与对热补丁影响解析.md ├── IPv6 socket编程.md ├── README.md ├── SUMMARY.md ├── Tinker:技术的初心与坚持.md ├── assets ├── android_video_record │ ├── encodeProcess.png │ ├── frame_compress.png │ └── mediacodec_buffers.png ├── ios_sql │ ├── SQLite-Arch.png │ ├── code-default-busy.png │ ├── code-malloc.png │ ├── lag-rw.png │ ├── lag-wait-lock.png │ ├── new-schema.png │ ├── old-schema.png │ ├── sqlite-ios-mmap.png │ ├── timeline-busy.png │ └── trend.png ├── mars │ ├── mars.png │ ├── tcpdump_client.png │ └── tcpdump_server.png ├── migrate_to_wcdb │ ├── baseline_batch_write.png │ ├── baseline_read.png │ ├── baseline_write.png │ ├── initialization.png │ ├── multithread_read_read.png │ ├── multithread_read_write.png │ └── multithread_write_write.png ├── mmtls_image │ ├── 1.jpg │ ├── 10.jpg │ ├── 11.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ ├── 7.jpg │ ├── 8.jpg │ ├── 9.jpg │ └── se.png ├── nano_free │ ├── LSEnvironment.png │ ├── assembly.jpg │ ├── crash.jpg │ ├── crash_os.png │ ├── malloc.png │ ├── malloc_zone_t.jpg │ ├── nano_crash_guard_1.jpg │ ├── nano_crash_guard_2.jpg │ ├── otool.jpg │ ├── scalable0x17.jpg │ ├── tricky_fall_through.jpg │ └── xcode_schema.jpg ├── qrcode_for_wemobiledev.jpg ├── soter │ ├── SOTER交流群群二维码.png │ ├── SoterFramework.png │ ├── all_sequence.txt │ ├── check_support.png │ ├── get_challenge.png │ ├── netwrapper.png │ ├── qrcode_for_gh_6410b016e824_258.jpg │ ├── upload_ask.png │ ├── upload_auth_key.png │ ├── verify_signature.png │ ├── 准备业务密钥.gif │ ├── 准备应用密钥.gif │ ├── 准备根密钥.gif │ └── 认证或开通.gif ├── tinker-open │ ├── androidn.png │ ├── dex-anr.png │ ├── dex-art.png │ ├── dex-diff.jpg │ ├── dex-format.png │ ├── dex-merge.jpg │ ├── dex-method.jpg │ ├── dex-result.png │ ├── open.jpg │ ├── section1.jpg │ └── tinker.png ├── tinker-research │ └── method-inline │ │ ├── 1480488144557.png │ │ ├── 1480488768332.png │ │ ├── 1480489428895.png │ │ ├── 1480489905685.png │ │ ├── 1480494090755.png │ │ ├── 1480496299998.png │ │ ├── 1480497454319.png │ │ └── 1480595723104.png ├── tinker │ ├── abtest.png │ ├── all.png │ ├── alldiff.png │ ├── andfix.png │ ├── andfixend.png │ ├── data.png │ ├── qzone-art.png │ ├── qzone-dalvik-end.png │ ├── qzone-dalvik.png │ ├── tinker.png │ ├── use.png │ ├── userlog.png │ ├── wechat-dexdiff.png │ ├── wechat.png │ ├── workmodel.png │ └── workmodel2.png ├── tinker_summary │ ├── android_n.png │ ├── github.png │ ├── huawei_fenshen.jpg │ ├── inline.png │ ├── miui.png │ ├── qzone-art.png │ ├── shwenzhang.jpg │ ├── thanks1.jpg │ └── thanks2.jpg ├── wcdb_ios_1 │ ├── as_1.jpg │ ├── as_2.jpg │ ├── chaincall_1.jpg │ ├── chaincall_2.jpg │ ├── coding_1.jpg │ ├── coding_2.jpg │ ├── coding_3.jpg │ ├── coding_4.jpg │ ├── coding_5.jpg │ ├── crud_1.jpg │ ├── crud_2.jpg │ ├── crud_3.jpg │ ├── crud_4.jpg │ ├── multiselect_1.jpg │ ├── orm_1.jpg │ ├── orm_2.jpg │ ├── orm_3.jpg │ ├── transaction_1.jpg │ ├── transaction_2.jpg │ ├── winq_1.jpg │ ├── winq_2.jpg │ ├── winq_3.jpg │ ├── winq_4.jpg │ └── winq_5.jpg ├── wcdb_repair │ ├── backup-compare.png │ ├── backup-optimization.png │ ├── dump-example.png │ ├── repair-united.png │ ├── sqlite-arch-core.png │ └── sqlite_master-struct.png └── winq │ ├── error.jpg │ ├── expr.jpg │ ├── hint.jpg │ └── select.jpg ├── final-微信热补丁实践演进之路-v2016-9-24.pdf ├── 为什么要从FMDB迁移到WCDB.md ├── 基于TLS1.3的微信安全通信协议mmtls介绍.md ├── 微信Android热补丁实践演进之路.md ├── 微信Android视频编码爬过的那些坑.md ├── 微信Mars — 移动互联网下的高质量网络连接探索.pdf ├── 微信Tinker的一切都在这里,包括源码(一).md ├── 微信iOS SQLite源码优化实践.md ├── 微信客户端怎样应对弱网络.pdf ├── 微信移动端数据库组件WCDB系列(一)-iOS基础篇.md ├── 微信移动端数据库组件WCDB系列(三) — WINQ原理篇.md ├── 微信移动端数据库组件WCDB系列(二) — 数据库修复三板斧.md ├── 微信移动端数据库组件WCDB系列(四) — Android 特性篇.md ├── 微信终端跨平台组件 Mars 系列 - 我们如约而至.md ├── 微信终端跨平台组件 Mars 系列(一) - 高性能日志模块xlog.md └── 聊聊苹果的Bug - iOS 10 nano_free Crash.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /0x10/0x11-mars.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/0x10/0x11-mars.md -------------------------------------------------------------------------------- /0x10/0x12-tinker.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/0x10/0x12-tinker.md -------------------------------------------------------------------------------- /0x10/0x13-wcdb.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/0x10/0x13-wcdb.md -------------------------------------------------------------------------------- /ART下的方法内联策略及其对Android热修复方案的影响分析.md: -------------------------------------------------------------------------------- 1 | ### ART下的方法内联策略及其对Android热修复方案的影响分析 2 | **tomystang** 3 | 4 | ---- 5 | 6 | > 为了解决ART模式下的占用Rom空间问题,Tinker曾经花了一个半月时间实现分平台合成。Android N后对内联的新发现,似乎再一次认证了"热补丁不是请客吃饭"这句话。 7 | > 8 | > 研究或填坑的路可能永远不会停,但Tinker团队有决心与信心可以陪大家一起走下去。 9 | 10 | #### 0x00 背景 11 |   ART(Android Runtime)是Android在4.4版本中引入的新虚拟机环境,在5.0版本正式取代了Dalvik VM。ART环境下,App安装时其包含的Dex文件将被dex2oat预编译成目标平台的机器码,从而提高了App的运行效率。在这个预编译过程中,dex2oat对目标代码的优化过程与Dalvik VM下的dexopt有较大区别,尤其是在5.0版本以后ART环境下新增的方法内联优化。由于方法内联改变了原本的方法分布和调用流程,对热修复方案势必会带来影响,本文将分析ART下方法内联策略,并总结方法内联对现有的主流热修复方案造成的影响。 12 | 13 |   浏览Android源码可知,Android用来生成oat文件的Compiler有多种实现,各Android版本中存在的实现类型和默认使用的类型如下: 14 | 15 | | 版本 | Compiler类型 | 默认使用的类型 | 备注 | 16 | | :---------- | :-------------------------- | :---------- | :-------------------------------- | 17 | | Android 4.4 | kQuick | kQuick | 此版kQuick未引入方法内联优化 | 18 | | Android 5.x | kQuick,kOptimizing,kPortable| kQuick | kPortable是半成品,并没有实际被使用 | 19 | | Android 6.x | kQuick,kOptimizing | kQuick | 从此版本开始,kOptimizing加入下文归纳的新内联特性| 20 | | Android 7.0 | kOptimizing | kOptimizing | | 21 | | Android 7.1 | kOptimizing | kOptimizing | | 22 | 23 | 其中Quick Compiler的方法内联条件可以从`art/runtime/quick/inline_method_analyser.cc`里的`InlineMethodAnalyser::AnalyseMethodCode`方法开始分析,篇幅关系这里直接给出结论。对于Quick Compiler,当以下条件**均满足**时被调用的方法将被inline: 24 | 1. App不是Debug版本的; 25 | 2. 被调用方法的实现满足下列条件之一: 26 | 2.1. 空方法; 27 | 2.2. 仅返回方法参数; 28 | 2.3. 仅返回一个方法内声明的常量或null; 29 | 2.4. 从被调用方法所在类的非静态成员获取并返回获取的值;(注意,static final成员会被优化成常量,此时要参照2.3) 30 | 2.5. 仅设置了被调用方法所在类的**非静态**成员的值; 31 | 2.6. 仅设置了被调用方法所在类的**非静态**成员的值,并返回一个方法内声明的常量或null。 32 | 33 | 注:条件2隐含了一个条件,就是被调用的方法的字节码不超过2条。 34 | 35 | Optimizing Compiler的内联条件可以从`art/compiler/optimizing/inliner.cc`里的`HInliner::Run()`方法开始分析,篇幅关系这里同样直接给出结论。对于Optimizing Compiler,当以下条件**均满足**时被调用的方法将被inline: 36 | 1. App不是Debug版本的; 37 | 2. 被调用的方法所在的类与调用者所在的类位于同一个Dex;(注意,符合Class N命名规则的多个Dex要看成同一个Dex) 38 | 3. 被调用的方法的字节码条数不超过dex2oat通过`--inline-max-code-units`指定的值,6.x默认为100,7.x默认为32; 39 | 4. 被调用的方法不含try块; 40 | 5. 被调用的方法不含非法字节码; 41 | 6. 对于7.x版本,被调用方法还不能包含对接口方法的调用。(invoke-interface指令) 42 | 43 | 此外,Optimizing Compiler的方法内联可以跨多级方法调用进行,若有这样的调用链:method1->method2->method3->method4,则在四个方法都满足内联条件的情况下,最终内联的结果将是method1包含method2,method3,method4的代码,method2包含method3,method4的代码,以此类推。但这种跨调用链内联会受到调用dex2oat时通过`--inline-depth-limit`参数指定的值的限制,默认为5,即超过5层的调用就不会再被内联到当前方法了。 44 | 45 | #### 0x01 主流热修复方案 46 | 47 |   目前主流热修复方案可分为Native派和Java派,Native派的做法大致有以下两种: 48 | 49 | ![用新方法的Native描述结构体覆盖旧方法的Native描述结构体,从而替换旧方法的逻辑](assets/tinker-research/method-inline/1480488144557.png) 50 | 51 | ![将旧方法修改为Native类型,并将其实现指向一个公共分发函数,由该函数负责调用新方法](assets/tinker-research/method-inline/1480488768332.png) 52 | 53 | Java派的做法也有两种: 54 | 55 | ![将修改过的类汇集成一个Dex,插入到BaseDexClassLoader的DexPathList的最前面,这样在加载类时就会优先加载修改过的类](assets/tinker-research/method-inline/1480489428895.png) 56 | 57 | ![对每个函数插一段逻辑,此逻辑判断方法是否被打补丁,是则执行新逻辑](assets/tinker-research/method-inline/1480489905685.png) 58 | 59 |   显然,对Native派而言,如果修改的方法被内联到了调用者的代码里,则修改将不会生效,因为内联的代码不再需要方法调用,也就不会涉及到额外的Native层方法描述结构体。对于Java派而言,插入一段逻辑的做法基本不受影响,因为内联时会将被修改的函数连同插入的那段逻辑一起复制到调用者的代码里,结果和内联之前是等价的。但通过优先加载补丁Dex里的类来取代旧类的做法会受到怎样的影响呢?这就需要进一步分析了。 60 | 61 | #### 0x02 方法内联对Tinker的影响 62 | 63 |   Tinker通过让VM优先加载补丁Dex里的类来使补丁生效,即Java派里的第一种方案。在没有考虑到内联的影响之前,这套方案有些兼容性问题已得到解决,但在最近的灰度过程中,我们又发现了这样的Crash: 64 | 65 | ![诡异的Crash, 前面有P的行表示补丁Dex里包含该类。](assets/tinker-research/method-inline/1480494090755.png) 66 | 67 | ![h.getExternalStorageDirectory方法的字节码,报错的为iget-object,因为compatible.d.p.cdl为空](assets/tinker-research/method-inline/1480496299998.png) 68 | 69 | 其中compatible.d.j的实例在类compatible.d.p里是定义时赋值给cdl字段的,按理说不该为Null,看着好像又是地址错乱的问题了,于是按照之前的经验,dump了栈顶附近几个方法的机器码,发现`f.sC`方法内联了`h.getExternalStorageDirectory`方法,不过现在还不能肯定是不是内联带来的问题,于是跟着`f.sC`方法的机器码走一段: 70 | 71 | ![f.sC方法的机器码](assets/tinker-research/method-inline/1480595723104.png) 72 | 73 | 图里(12288+1292)/4 = 3395,对应compatible.util.h的typeid;(12288+1120) / 4 = 3352,对应compatible.d.p的typeid。图里提到的DexCache是一个缓存数组,用来存放resolve过的类的地址。由机器码的这一行 74 |
`0x036e3c80: 6985 ldr r5, [r0, #24]`
75 | 可知,用来检查compatible.d.p是否resolve过的DexCache是从compatible.util.h的Native结构里的某个字段获取的,这样我们只要知道compatible.util.h的DexCache是OldDex的还是NewDex的就可以了。于是回到上面判断compatible.util.h那部分,我们跟着机器码跳到0x036e40ea的位置: 76 | 77 | ![](assets/tinker-research/method-inline/1480497454319.png) 78 | 79 | 这个`pInitializeStaticStorage`实际上就是`artInitializeStaticStorageFromCode`方法,看`art/runtime/arch/arm/quick_entrypoints_arm.S`就知道了。从`artInitializeStaticStorageFromCode`方法开始又依次调用了`ResolveVerifyAndClinit` -> `ClassLinker::ResolveType`,经过几个不同原型的ResolveType之后最终调到了`ClassLinker::FindClass`,从FindClass开始就是经典的ART加载类的流程了,大家可以参考老罗的这篇文章:[Android运行时ART加载类和方法的过程分析](http://blog.csdn.net/luoshengyang/article/details/39533503)。根据这个流程,最后被加载的compatible.util.h来自NewDex,所以保存在它的Native描述结构体里的DexCache就是NewDex的了。 80 | 81 |   搞清楚这个问题之后,我们再来看为何会报NPE。回到f.sC方法的机器码,我们看第二部分,在从r0+24这个位置拿到DexCache之后,接着就会判断compatible.d.p是否被resolve,然后问题就来了,既然这个DexCache是NewDex的,那机器码里用3352这个OldDex里的typeid来访问究竟靠谱吗?和之前那个地址错乱的问题类似,这里会遇到三种情况: 82 | 83 | | 情况 | 后果 | 84 | | :-----------------------------------------------| :-------------------------------------- | 85 | | oldDex里的typeid恰好就是newDex里的typeid | 没事,补丁正常生效 | 86 | | oldDex里的typeid >= newDex的DexCache的长度 | 被SIGABORT干掉,Abort Message为数组下标越界 | 87 | | oldDex里的typeid在newDex的DexCache里指向了其他类 | 无法预料,可能崩溃,可能导致程序流程异常 | 88 | 89 | 如果你的补丁变更规模很小,一般会大概率命中第二种情况;如果你的补丁变更规模很大,则会有很大概率命中第三种情况。微信这次灰度报的crash对应的就是第三种情况了,所以最后通过offset去拿成员的值的时候会拿到0,于是就报了NPE。 90 | 91 | #### 0x03 可能的应对方案 92 | 93 |   参考0x01里的内联条件,如果阻止方法内联,就可以避免出现机器码里用旧typeid去新DexCache里查找类的情况了。对我们来说比较方便的条件就是在每个方法前面插入一个空try块,这样这些方法就不会参与内联了。不过考虑到ART的内联触发条件随时都在更新,保险起见Tinker并没有这样做。 94 | 95 |   另外一个思路是把修改类的整个调用链(调用修改类的类,与调用[调用修改类的类]的类,一直递归下去)都放到补丁中,需要包括所有可能被影响的类。这套规则的主要问题在于整个完整调用链的类会非常庞大,很有可能与全量差别不大,其次不排除Android未来有新的优化导致这样的方式会失效。 96 | 97 |   Tinker最终采用的应对方案是去掉ART环境下的合成增量Dex的逻辑,直接合成全量的NewDex,这样除了loader类,所有方法统一都用了NewDex里的,也就不怕有方法被内联了。至于全量新Dex在系统OTA之后触发dex2oat可能导致App启动时ANR的问题,Tinker是通过在进入ApplicationLike之前判断fingerprint是否变化来得知系统是否进行过OTA,然后根据判断结果手动触发多线程dex2oat加以缓解的。 98 | 99 | #### 0x04 总结 100 | 101 |   方法内联之所以会导致优先加载补丁Dex的方案出现上述问题,本质上是因为补丁Dex只覆盖了旧Dex里的一部分类,一旦被覆盖的类的方法被内联到了调用者里,则加载类的过程还是正常的,即从补丁Dex里加载了新版本的类。但由于内联,执行流程并未跳转到新的方法里,于是所有关于新版本的类的方法、成员、字符串的查找用的就都是旧方法里的索引了。这里“用旧索引找新目标”的场景又出现了,App自然可能出现异常。 102 | 103 |   ART 6.0及之后的Optimizing Compiler通过相对激进的内联策略,进一步提升了App的运行效率,但这也为各类热修复方案带来了一些麻烦。通过分析,理论上我们确实可以通过破坏内联条件强行让所有的方法都不参与内联,但这样一来App的运行时性能就会受到较大影响,而且今后如果内联标准更新了,我们还需要持续跟进,这也是不太现实的。因此通过这次灰度发现的问题,我们最终还是用了相对保守的方案来解决。 104 | 105 |   最后,通过分析这个问题,我们也更加深刻地认识到了ART的复杂性。时间仓促,关于方法内联的策略及其带来的问题还有许多细节并没有在这篇文章里提及,如果有补充的知识或者更好的应对方案,欢迎一同交流学习。 -------------------------------------------------------------------------------- /Android_N混合编译与对热补丁影响解析.md: -------------------------------------------------------------------------------- 1 | #Android N混合编译与对热补丁影响深度解析# 2 | > 首先非常抱歉Tinker没有按期内测,这主要因为开源的代码需要通过公司内部审核与评测,这项工作大约还需要一个月左右。当前Tinker已经在公司内部开源,我们会努力让它以更完善的姿态与大家见面。 3 | 4 | 大约在六月底,Tinker在微信全量上线了一个补丁版本,随即华为反馈在Android N上微信无法启动。冷汗冒一地,Android N又搞了什么东东?为什么与instant run保持一致的补丁方式也跪了?talk is cheap,show me the code。趁着台风妮妲肆虐广东,终于有时间总结一把。在此非常感谢华为工程师谢小灵与胡海亮的帮助,事实上微信与各大厂商都保持着非常紧密的联系。 5 | 6 | ##无法启动的原因 7 | 我们遵循从问题出发的思路,针对华为提供的日志,我们看到微信在Android N上启动时会报`IllegalAccessError`。可以从`/data/user/0/com.tencent.mm/tinker/patch-a002c56d/dex/classes2.dex`看到,的确跟补丁是有关系的。 8 | 9 | ``` 10 | java.lang.IllegalAccessError: 11 | Illegal class access: 12 | 'com.tencent.mm.ui.conversation.ConversationOverscrollListView' 13 | attempting to access 14 | 'com.tencent.mm.ui.conversation.ConversationOverscrollListView$c' 15 | (declaration of 'com.tencent.mm.ui.conversation.ConversationOverscrollListView' 16 | appears in /data/user/0/com.tencent.mm/tinker/patch-a002c56d/dex/classes2.dex) 17 | ``` 18 | 19 | 但是在我们手上Android N却无法复现,同时跟华为的进一步沟通中,他们也明确只有一少部分N的用户会出现问题。这就很难办了,但是根据之前在art地址错乱的经验(似乎这里我还欠大家一篇分析文章),跟这里似乎有点相似。 20 | 21 | 但是Tinker已经做了全量替换,所以我怀疑由于Android N的某种机制这里只有部分用了补丁中的类,但是部分类导致使用了原来的dex中的。接下来就跟着我一起去研究Android N在编译运行究竟做了什么改变吧? 22 | 23 | ##Android N的混合编译运行模式 24 | 网上关于Android N混合编译的文章并不多,infoq上有一篇翻译文章:[Android N混合使用AOT编译,解释和JIT三种运行时](http://www.infoq.com/cn/news/2016/04/android-n-aot-jit)。混合编译运行主要指AOT编译,解释执行与JIT编译,它主要解决的问题有以下几个: 25 | 26 | 1. `应用安装时间过长`;在N之前,应用在安装时需要对所有ClassN.dex做AOT机器码编译,类似微信这种比较大型的APP可能会耗时数分钟。但是往往我们只会使用一个应用20%的功能,剩下的80%我们付出了时间成本,却没带来太大的收益。 27 | 2. `降低占ROM空间`;同样全量编译AOT机器码,12M的dex编译结果往往可以达到50M之多。只编译用户用到或常用的20%功能,这对于存储空间不足的设备尤其重要。 28 | 3. `提升系统与应用性能`;减少了全量编译,降低了系统的耗电。在boot.art的基础上,每个应用增加了base.art(这块后面会详细解析), 通过预加载与缓存提升应用性能。 29 | 4. `快速的系统升级`;以往厂商ota时,需要对安装的所有应用做全量的AOT编译,这耗时非常久。事实上,同样只有20%的应用是我们经常使用的,给不常用的应用,不常用的功能付出的这些成本是不值得的。 30 | 31 | Android N为了解决这些问题,通过管理**解释,AOT与JIT**三种模式,以达到一种运行效率、内存与耗电的折中。简单来说,在应用运行时分析运行过的代码以及“热代码”,并将配置存储下来。在设备空闲与充电时,ART仅仅编译这份配置中的“热代码”。我们先来看看Android N上有哪些编译方法: 32 | 33 | ###Android N的编译模式 34 | 在[compiler_filter.h](https://android.googlesource.com/platform/art/+/android-n-preview-5/runtime/compiler_filter.h#32),我们可以看到dex2oat一共有12种编译模式: 35 | 36 | ``` 37 | enum Filter { 38 | VerifyNone, // Skip verification but mark all classes as verified anyway. 39 | kVerifyAtRuntime, // Delay verication to runtime, do not compile anything. 40 | kVerifyProfile, // Verify only the classes in the profile, compile only JNI stubs. 41 | kInterpretOnly, // Verify everything, compile only JNI stubs. 42 | kTime, // Compile methods, but minimize compilation time. 43 | kSpaceProfile, // Maximize space savings based on profile. 44 | kSpace, // Maximize space savings. 45 | kBalanced, // Good performance return on compilation investment. 46 | kSpeedProfile, // Maximize runtime performance based on profile. 47 | kSpeed, // Maximize runtime performance. 48 | kEverythingProfile, // Compile everything capable of being compiled based on profile. 49 | kEverything, // Compile everything capable of being compiled. 50 | }; 51 | ``` 52 | 53 | 以上12种编译模式**按照排列次序逐渐增强**,那系统默认采用了哪些编译模式呢?我们可以在在手机上执行`getprop | grep pm`查看: 54 | 55 | ``` 56 | pm.dexopt.ab-ota: [speed-profile] 57 | pm.dexopt.bg-dexopt: [speed-profile] 58 | pm.dexopt.boot: [verify-profile] 59 | pm.dexopt.core-app: [speed] 60 | pm.dexopt.first-boot: [interpret-only] 61 | pm.dexopt.forced-dexopt: [speed] 62 | pm.dexopt.install: [interpret-only] 63 | pm.dexopt.nsys-library: [speed] 64 | pm.dexopt.shared-apk: [speed] 65 | ``` 66 | 其中有几个我们是特别关心的, 67 | 68 | 1. `install`(应用安装)与`first-boot`(应用首次启动)使用的是[interpret-only],即只verify,代码解释执行即不编译任何的机器码,它的性能与Dalvik时完全一致,先让用户愉快的玩耍起来。 69 | 2. `ab-ota`(系统升级)与`bg-dexopt`(后台编译)使用的是[speed-profile],即只根据“热代码”的profile配置来编译。这也是N中混合编译的核心模式。 70 | 3. 对于动态加载的代码,即`forced-dexopt`,它采用的是[speed]模式,即最大限度的编译机器码,它的表现与之前的AOT编译一致。 71 | 72 | 总的来说,程序使用loaddex动态加载的代码是无法享受混合编译带来的好处,我们应当尽量**采用ClassN.dex方式来符合Google的规范**。这不仅在ota还是混合编译上,都会带来很大的提升。 73 | 74 | ###Android N的Profile文件 75 | 在讲[speed-profile]是怎样编译之前,这里先简单描述一下profile文件。profile相关的核心代码都在art/runtime/jit中。简单来说,[profile_saver.cc](https://android.googlesource.com/platform/art/+/android-n-preview-5/runtime/jit/profile\_saver.cc)会开启线程去专门收集已经resolved的类与函数,达到一定条件即会持久化存储在`/data/misc/profiles`文件夹中。具体的条件可以在[profile\\_saver\\_options.h](https://android.googlesource.com/platform/art/+/android-n-preview-5/runtime/jit/profile_saver_options.h)中查看,在收集过程会出现类似以下的日志: 76 | 77 | ``` 78 | tinker.sample.android I/art: Collecting resolved classes 79 | tinker.sample.android I/art: Collecting class profile for dex file /data/app/tinker.sample.android-1/base.apk types=2406 class_defs=1719 80 | tinker.sample.android I/art: Dex location /data/app/tinker.sample.android-1/base.apk has 232 / 1719 resolved classes 81 | ``` 82 | profile的存储格式在[offline\\_profiling\\_info.h](https://android.googlesource.com/platform/art/+/android-n-preview-5/runtime/jit/offline_profiling_info.h)中定义,我们也可以通过`profman`命令查看profile文件中的数据,命令如下: 83 | 84 | ``` 85 | profman --profile-file=/data/misc/profiles/cur/0/tinker.sample.android/primary.prof --dump-only 86 | ``` 87 | 88 | 具体输出如下: 89 | 90 | ``` 91 | === profile === 92 | ProfileInfo: 93 | base.apk 94 | methods: 297,302,303,424,427,665,668,669,700,756,757,759,760,761,765,766,768,772,774, 95 | classes: 52,124,456, 96 | ``` 97 | 98 | 其中base.apk代表dex的位置,这里代表的是ClassN中的第一个dex。其他dex会使用类似base.apk:classes2.dex方式命名。后面的methods与classes代表的是它们在dex格式中的index,只有这些类与方法是我们需要在[speed-profile]模式中需要编译。 99 | 100 | ###Android N的dex2oat编译 101 | 在这里我们比较关心系统究竟是什么时候会去对应用做类似增量的编译,还有具体的编译流程是怎么样的? 102 | 103 | ####dex2oat编译的时机 104 | 首先我们来看系统在什么时候会对各个应用做这种渐进式编译呢?手机在充电+空闲+四个小时间隔等多个条件下,通过[BackgroundDexOptService.java](https://android.googlesource.com/platform/frameworks/base/+/android-n-preview-5/services/core/java/com/android/server/pm/BackgroundDexOptService.java)中的JobSchedule下触发编译优化。 105 | 106 | ``` 107 | new JobInfo.Builder(BACKGROUND_DEXOPT_JOB, sDexoptServiceName) 108 | .setRequiresDeviceIdle(true) 109 | .setRequiresCharging(true) 110 | .setMinimumLatency(minLatency) 111 | .build(); 112 | ``` 113 | ####dex2oat编译的流程 114 | 对于[speed-profile]模式,dex2oat编译命令的核心参数如下: 115 | 116 | ``` 117 | dex2oat --dex-file=./base.apk --oat-file=./base.odex --compiler-filter=speed-profile --app-image-file=./base.art 118 | --profile-file=./primary.prof ... 119 | ``` 120 | 121 | 入口文件位于[dex2oat.cc](https://android.googlesource.com/platform/art/+/android-n-preview-5/dex2oat/dex2oat.cc)中,在这里并不想贴具体的调用函数,简单的描述一下流程:若dex2oat参数中有输入profile文件,会读取profile中的数据。与以往不同的是,这里不仅会根据profile文件来生成base.odex文件,同时还会生成称为app_image的base.art文件。与boot.art类似,base.art文件主要为了加快应用的对“热代码”的加载与缓存。 122 | 123 | 我们可以通过oatdump命令来看到art文件的内容,具体命令如下: 124 | 125 | ``` 126 | oatdump --app-image=base.art --app-oat=base.odex --image=/system/framework/boot.art --instruction-set=arm64 127 | ``` 128 | 我们可以dump到art文件中的所有信息,这里我只将它的头部信息输出如下: 129 | 130 | ``` 131 | IMAGE LOCATION: base.art 132 | IMAGE BEGIN: 0x77ea1000 133 | IMAGE SIZE: 1597200 134 | IMAGE SECTION SectionObjects: size=2040 range=0-2040 135 | IMAGE SECTION SectionArtFields: size=0 range=2040-2040 136 | IMAGE SECTION SectionArtMethods: size=0 range=2040-2040 137 | IMAGE SECTION SectionRuntimeMethods: size=0 range=2040-2040 138 | IMAGE SECTION SectionIMTConflictTables: size=0 range=2040-2040 139 | IMAGE SECTION SectionDexCacheArrays: size=1591080 range=2040-1593120 140 | IMAGE SECTION SectionInternedStrings: size=4040 range=1593120-1597160 141 | IMAGE SECTION SectionClassTable: size=40 range=1597160-1597200 142 | IMAGE SECTION SectionImageBitmap: size=4096 range=1597440-1601536 143 | ``` 144 | 145 | base.art文件主要记录已经编译好的类的具体信息以及函数在oat文件的位置,一个class的输出格式如下: 146 | 147 | ``` 148 | 0x78c8f768: java.lang.Class "com.tencent.mm.ui.d.a" (StatusInitialized) 149 | shadow$_klass_: 0x6fc76488 Class: java.lang.Class 150 | shadow$_monitor_: 0 (0x0) 151 | accessFlags: 524305 (0x80011) 152 | annotationType: null sun.reflect.annotation.AnnotationType 153 | classFlags: 0 (0x0) 154 | classLoader: 0x787b5140 java.lang.ClassLoader 155 | classSize: 460 (0x1cc) 156 | clinitThreadId: 0 (0x0) 157 | componentType: null java.lang.Class 158 | copiedMethodsOffset: 3 (0x3) 159 | dexCache: 0x782290c8 java.lang.DexCache 160 | dexCacheStrings: 2036372056 (0x79609258) 161 | dexClassDefIndex: 12138 (0x2f6a) 162 | dexTypeIndex: 11797 (0x2e15) 163 | iFields: 2031076964 (0x790fc664) 164 | ifTable: 0x78836500 java.lang.Object[] 165 | methods: 2032787876 (0x7929e1a4) 166 | name: null java.lang.String 167 | numReferenceInstanceFields: 4 (0x4) 168 | numReferenceStaticFields: 0 (0x0) 169 | objectSize: 36 (0x24) 170 | primitiveType: 131072 (0x20000) 171 | referenceInstanceOffsets: 63 (0x3f) 172 | sFields: 0 (0x0) 173 | status: 10 (0xa) 174 | superClass: 0x78bcc968 Class: com.tencent.mm.pluginsdk.ui.b.b 175 | verifyError: null java.lang.Object 176 | virtualMethodsOffset: 1 (0x1) 177 | vtable: null java.lang.Object 178 | ``` 179 | 180 | method的输出格式如下: 181 | 182 | ``` 183 | 0x792b639c ArtMethod: void com.tencent.mm.e.a.je.() 184 | OAT CODE: 0x471dae14-0x471daece 185 | SIZE: Dex Instructions=10 StackMaps=0 AccessFlags=0x90001 186 | 0x792b63c0 ArtMethod: void com.tencent.mm.e.a.je.(byte) 187 | OAT CODE: 0x471daee4-0x471daf52 188 | SIZE: Dex Instructions=48 StackMaps=0 AccessFlags=0x90002 189 | 0x792b63e8 ArtMethod: void com.tencent.mm.e.a.jo.() 190 | OAT CODE: 0x463d5f44-0x463d5f50 191 | SIZE: Dex Instructions=10 StackMaps=0 AccessFlags=0x90001 192 | ``` 193 | 194 | 那么我们就剩下最后一个问题,app image文件是什么时候被加载,并且为什么它会影响热补丁的机制? 195 | 196 | ###App image文件的加载 197 | 在apk启动时我们需要加载应用的oat文件以及可能存在的app image文件,它的大致流程如下: 198 | 199 | 1. 通过[OpenDexFilesFromOat](https://android.googlesource.com/platform/art/+/android-n-preview-5/runtime/oat_file_manager.cc#541)加载oat时,若app image存在,则通过调用[OpenImageSpace](https://android.googlesource.com/platform/art/+/android-n-preview-5/runtime/oat_file_assistant.cc#989)函数加载; 200 | 2. 在加载app image文件时,通过[UpdateAppImageClassLoadersAndDexCaches](https://android.googlesource.com/platform/art/+/android-n-preview-5/runtime/class_linker.cc#1227)函数,将art文件中的dex\_cache中dex的所有class插入到ClassTable,同时将method更新到dex\_cache; 201 | 3. 在类加载时,使用时[ClassLinker::LookupClass](https://android.googlesource.com/platform/art/+/android-n-preview-5/runtime/class_linker.cc#3599)会先从ClassTable中去查找,找不到时才会走到[DefineClass](https://android.googlesource.com/platform/art/+/android-n-preview-5/runtime/class_linker.cc#2437)中。 202 | 203 | 非常简单的说,app image的作用是记录已经编译好的“热代码”,并且在启动时一次性把它们加载到缓存。预先加载代替用时查找以提升应用的性能,到这里我们终于明白为什么base.art会影响热补丁的机制。 204 | 205 | **无论是使用插入pathlist还是parent classloader的方式,若补丁修改的class已经存在与app image,它们都是无法通过热补丁更新的。它们在启动app时已经加入到PathClassLoader的ClassTable中,系统在查找类时会直接使用base.apk中的class。** 206 | 207 | ###instant run为什么没有影响 208 | 对于instant run来说,它的目标是快速debug。从上面的编译条件看来,它是不太可能可以触发[speed-profile]编译的。事实上,它在dex2oat上面传入了**--debugable**参数, 不过dex2oat并没有单独处理这个参数。感兴趣的同学,可以再详细研究这一块。 209 | 210 | 最后我们再来总结一下Android N混合编译运行的整个流程,它就像一个小型生态系统那样和谐。 211 | 212 | ![](http://i.imgur.com/aT7JJJs.png) 213 | 214 | ##Android N上热补丁的出路 215 | 假设base.art文件在补丁前已经存在,这里存在三种情况: 216 | 217 | 1. 补丁修改的类都不app image中;这种情况是最理想的,此时补丁机制依然有效; 218 | 2. 补丁修改的类部分在app image中;这种情况我们只能更新一部分的类,此时是最危险的。一部分类是新的,一部分类是旧的,app可能会出现地址错乱而出现crash。 219 | 3. 补丁修改的类全部在app image中;这种情况只是造成补丁不生效,app并不会因此造成crash。 220 | 221 | 如何解决这个问题呢?下面根据当时我的一些思路分别说明: 222 | 223 | ###插桩? 224 | 当时第一反应想到是通过插桩是否能阻止类被编译到app image中,从而规避了这个问题。事实上,在生成profile时,使用的是[ClassLinker::GetResolvedClasses](https://android.googlesource.com/platform/art/+/android-n-preview-5/runtime/class_linker.cc#8111)函数,插桩并没有任何作用。 225 | 226 | 我这边也专门单独看了插桩后编译的机器码,仅仅是通过Trampoline模式跳回虚拟机查找而已。 227 | 228 | ``` 229 | DEX CODE: 230 | ... 231 | 0x0018: 0e00 | return-void 232 | 0x45f0dda2: f8d9e29c ldr.w lr, [r9, #668] ; pInvokeStaticTrampolineWithAccessCheck 233 | ... 234 | ``` 235 | 236 | ###miniloader方案 237 | 假设我们实现一个最小化的loader,这部分代码我们补丁时是不会去改变。然后其他代码都通过动态方式加载,这套方案的确是可行的,但是并不会被采用,因为它会带来以下几个代价: 238 | 239 | 1. 对Android N之前,由于不使用ClassN方式,带来首次加载过慢甚至黑屏的问题; 240 | 2. 对于Android N,不仅存在第一点问题,同时将混合编译的好处完全废掉了(因为动态加载的代码是相当于完全编译的); 241 | 242 | 在微信中,补丁方案的原则应该是不能影响运行时的性能,所以这套方案也是不可取的。 243 | 244 | ###运行时替换PathClassLoader方案 245 | 事实上,App image中的class是插入到PathClassloader中的ClassTable中。假设我们完全废弃掉PathClassloader,而采用一个新建Classloader来加载后续的所有类,即可达到将cache无用化的效果。 246 | 247 | 需要注意的问题是我们的Application类是一定会通过PathClassloader加载的,所以我们需要将Application类与我们的逻辑解耦,这里方式有两种: 248 | 249 | 1. 采用类似instant run的实现;在代理application中,反射替换真正的application。这种方式的优点在于接入容易,但是这种方式无法保证兼容性,特别在反射失败的情况,是无法回退的。 250 | 2. 采用代理Application实现的方法;即Application的所有实现都会被代理到其他类,Application类不会再被使用到。这种方式没有兼容性的问题,但是会带来一定的接入成本。 251 | 252 | 我想说明的是许多号称毫无兼容性问题的反射框架,在微信Android 数亿用户面前往往都是经不起考验的。这也是为什么我们尽管采用增加接入成本方式也不愿意再多的使用反射的原因。总的来说,这种方式不会影响没有补丁时的性能,但在加载补丁后,由于废弃了App image带来一定的性能损耗。具体数据如下: 253 | 254 | ![](http://i.imgur.com/Z42vvMJ.png) 255 | 256 | 事实上,在Android N上我们不会出现完整编译一个应用的base.odex与base.art的情况。base.art的作用是加快类与方法的第一次查找速度,所以在启动时这个数据是影响最大的。在这种情况,废弃base.art大约带来15%左右的性能损耗。在其他情况下,这个数字应该是远远小于这个数字。 257 | 258 | ##Tinker的后续计划 259 | 在Android N上,Tinker全量合成方案带来了一个较为严重的问题。即将Android N的混合编译退化了,因为动态编译的代码采用的是[speed]方式完整编译,它会占用比较多Rom空间。所以未来我们计划根据平台区分合成的方式,在Dalvik平台我们合成一个完整的dex,但在Art平台只合成需要的类,它的规则如下: 260 | 261 | 1. 修改跟新增的class; 262 | 2. 若class有field,method或interface数量变化,它们所有的子类; 263 | 3. 若class有field,method或interface数量变化d,它们以及它们所有子类的调用类。如果采用ClassN方式,即需要多个dex一起处理。 264 | 265 | 规则看起来很复杂,同一个diff文件,根据不同平台合成不同文件看起来也很复杂。更困难的是,dex格式是存在大量的互相引用,除了index区域,还有使用绝对地址引用的区域,大量的变长结构,4字节对齐...... 266 | 267 | 所以Tinker最终期望的结构图应该如下,在art上面仅仅合成mini.dex即可: 268 | ![](http://i.imgur.com/6OPWAYj.png) 269 | 270 | ##结语 271 | 建议大家通过"阅读全文"查看,以获得更好的阅读体验。尽管当前Tinker还没有开启内测,我们会尽力在开源前做的更好。让Tinker无论在Dalvik还是Art上,都有着最好的表现,同时也恳请大家继续耐心等候我们。 272 | 273 | 274 | 275 | -------------------------------------------------------------------------------- /IPv6 socket编程.md: -------------------------------------------------------------------------------- 1 | #IPv6 socket编程 2 | 3 | ##背景 4 | 研究IPv6 socket编程原因: 5 | > [Supporting IPv6 in iOS 9](https://developer.apple.com/news/?id=08282015a)
6 | > WWDC2015苹果宣布在ios9支持纯IPv6的网络服务,并且要求2016年提交到app store的应用必须兼容纯IPv6的网络,要求适配的系统版本是ios9以上(包括ios9)。 7 | 8 | 写这篇文章虽然是来源于iOS的需求,但是下面的内容除了特别说明外,大部分都适用于其他平台。
9 | IPv6的复杂度之一,在于和IPv4的兼容和相互访问。本文会提及其他的互相访问技术,但是重点是`NAT64`,也是一般手机用户最有可能遇到的纯IPv6环境。
10 | 本文重点在**不同IP stack组合的处理方式**和**判断客户端支持的IP stack**。 11 | 12 | ##问题复杂性 13 | 为了降低问题的复杂性,我们先把v4 socket排除掉,统一使用v6 socket。 14 | v6 socket的区别是使用`AF_INET6`来创建。
15 | IPv6转换机制有很多种,苹果期望iOS app能兼容NAT64/DNS64的方式,因此其他方式我们先不考虑。 16 | 17 | 1. socket api支持 [RFC 4038 - Application Aspects of IPv6 Transition] 18 | - v4 socket接口只能支持IPv4 stack 19 | - v6 socket能支持IPv4 stack和IPv6 stack 20 | 2. 服务器IP 21 | - 返回v4 IP 22 | - 返回v6 IP 23 | 3. 用户本地IP stack 24 | - IPv4-only 25 | - IPv6-only 26 | - IPv4-IPv6 Dual stack 27 | 4. 各种IPv6转换机制 28 | - NAT64/DNS64 `64:ff9b::/96`用于v6的本地网络通过NAT访问v4的资源。[RFC 6146](https://tools.ietf.org/html/rfc6146) 、[RFC 6147](https://tools.ietf.org/html/rfc6147) 29 | - 6to4 `2002::/16`用于两个拥有v4公网地址的IPv6 only子网的互相访问。[RFC 6343](https://tools.ietf.org/html/rfc6343) 30 | - Teredo tunneling `2001::/32`通过隧道的方式让两个IPv6 only子网互相访问,没有NAT问题。[RFC 4380](https://tools.ietf.org/html/rfc4380) 31 | - 464XLAT 用于程序只有v4地址(使用v4 socket),但是本地网络是ipv6网络,程序需要访问v4资源,类似NAT64,不过区别在于服务器是运营商提供,手机上需要安装`CLAT服务` 。[RFC 6877](https://tools.ietf.org/html/rfc6877) 32 | - 还有很多兼容方案,复杂程度都很高,这里不介绍了 33 | 34 | 35 | 36 | ##不同IP stack组合的处理方式 37 | ###v4 ip + IPv4-only or IPv4-IPv6 Dual stack 38 | 在这样的情况下我们虽然用的是v6的socket,但是必须要让socket走的是v4的协议。 39 | 这里,让我们先了解下IPv6的保留地址(类似IPv4,192.168.*.*, 127.*.*.*这种)这里假设读者已经对IPv6地址组成和书写方式有一定了解的了解。 40 | 41 | > ::ffff:0:0/96 — This prefix is designated as an IPv4-mapped IPv6 address. With a few exceptions, this address type allows the transparent use of the Transport Layer protocols over IPv4 through the IPv6 networking application programming interface. Server applications only need to open a single listening socket to handle connections from clients using IPv6 or IPv4 protocols. IPv6 clients will be handled natively by default, and IPv4 clients appear as IPv6 clients at their IPv4-mapped IPv6 address. Transmission is handled similarly; established sockets may be used to transmit IPv4 or IPv6 datagram, based on the binding to an IPv6 address, or an IPv4-mapped address. (See also Transition mechanisms.) [^1] 42 | 43 | 从上文可以看到如果服务器地址为`128.0.0.128`,我们转换成IPv4-mapped IPv6 address`::ffff:128.0.0.128`或者纯16进制`::ffff:ff00:00ff`, 然后赋值给`sockaddr_in6.sin6_addr = "::ffff:128.0.0.128";`(注意这里是伪代码,真正代码还要用inet_pton进行转换)。这个socket虽然用了IPv6的`sockaddr_in6`,但实际上走的是IPv4 stack。

44 | 45 | IPv4-mapped IPv6 address是让用户能够使用一致的socket api,来访问IPv4和IPv6网络。 46 | 47 | 上文提及[RFC 4038 - Application Aspects of IPv6 Transition](https://www.ietf.org/rfc/rfc4038)对这种情况进行说明。 48 | 49 | //IPv4-mapped IPv6 address sample 50 | //address init 51 | const char* ipv4mapped_str ="::FFFF:14.17.32.211"; 52 | in6_addr ipv4mapped_addr = {0}; 53 | int v4mapped_r = inet_pton(AF_INET6, ipv4mapped_str, &ipv4mapped_addr); 54 | 55 | sockaddr_in6 v4mapped_addr = {0}; 56 | v4mapped_addr.sin6_family = AF_INET6; 57 | v4mapped_addr.sin6_port = htons(80); 58 | v4mapped_addr.sin6_addr = ipv4mapped_addr; 59 | 60 | //socket connect 61 | int v4mapped_sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); 62 | std::string v4mapped_error; 63 | if (0 != connect(v4mapped_sock, (sockaddr*)&v4mapped_addr, 28)) 64 | { 65 | v4mapped_error = strerror(errno); 66 | } 67 | 68 | //get local ip 69 | sockaddr_in6 v4mapped_local_addr = {0}; 70 | socklen_t v4mapped_local_addr_len = 28; 71 | char v4mapped_str_local_addr[64] = {0}; 72 | getsockname(v4mapped_sock, (sockaddr*)&v4mapped_local_addr, &v4mapped_local_addr_len); 73 | inet_ntop(v4mapped_local_addr.sin6_family, &v4mapped_local_addr.sin6_addr, v4mapped_str_local_addr, 64); 74 | 75 | close(v4mapped_sock); 76 | 77 | 78 | ###v4 ip + IPv6-only 79 | 这里是重点,也是苹果要求支持的主要场景。这里会涉及到NAT64/DNS64,关于这个环境的搭建请参考[Supporting IPv6 DNS64/NAT64 Networks](废弃了的SIIT技术我们就不讨论了) 80 | 81 | 这里我们先看看wikipedia对NAT64/DNS64的描述。 82 | >NAT64 is a mechanism to allow IPv6 hosts to communicate with IPv4 servers. The NAT64 server is the endpoint for at least one IPv4 address and an IPv6 network segment of 32-bits, e.g., 64:ff9b::/96 (RFC 6052, RFC 6146). The IPv6 client embeds the IPv4 address with which it wishes to communicate using these bits, and sends its packets to the resulting address. The NAT64 server then creates a NAT-mapping between the IPv6 and the IPv4 address, allowing them to communicate.[^2] 83 | 84 | >DNS64 describes a DNS server that when asked for a domain's AAAA records, but only finds A records, synthesizes the AAAA records from the A records. The first part of the synthesized IPv6 address points to an IPv6/IPv4 translator and the second part embeds the IPv4 address from the A record. The translator in question is usually a NAT64 server. The standard-track specification of DNS64 is in RFC 6147. 85 | > 86 | > There are two noticeable issues with this transition mechanism: 87 | > 88 | > - It only works for cases where DNS is used to find the remote host address, if IPv4 literals are used the DNS64 server will never be involved. 89 | > - Because the DNS64 server needs to return records not specified by the domain owner, DNSSEC validation against the root will fail in cases where the DNS server doing the translation is not the domain owner's server.[^3] 90 | 91 | 这里大概描述一下NAT64的工作流程,首先局域网内有一个NAT64的路由设备并且有DNS64的服务。 92 | 93 | 1. 客户端进行getaddrinfo的域名解析. 94 | 2. DNS返回结果,如果返回的IP里面只有v4地址,并且当前网络是IPv6-only网络,DNS64服务器会把v4地址加上`64:ff9b::/96`的前缀,例如`64:ff9b::14.17.32.211`。如果当前网络是IPv4-only或IPv4-IPv6,DNS64不会做任何事情。 95 | 3. 客户端拿到IPv6的地址进行connect 96 | 4. 路由器发现地址的前缀为`64:ff9b::/96`,知道这个是NAT64的映射,是需要访问`14.17.32.211`。这个时候进行需要NAT64映射,因为到外网需要转换成IPv4 stack。 97 | 5. 当数据返回的时候,按照NAT映射,IPv4回包重新加上前缀`64:ff9b::/96`,然后返回给客户端。 98 | 99 | apple的文档里面也有很详细的描述: 100 | ![img](https://developer.apple.com/library/ios/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/art/NAT64-DNS64-ResolutionOfIPv4_2x.png) 101 | ![img](https://developer.apple.com/library/ios/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/art/NAT64-DNS64-Workflow_2x.png) 102 | 103 | //NAT64 address sample 104 | //address init 105 | const char* ipv6_str ="64:ff9b::14.17.32.211"; 106 | in6_addr ipv6_addr = {0}; 107 | int v6_r = inet_pton(AF_INET6, ipv6_str, &ipv6_addr); 108 | sockaddr_in6 v6_addr = {0}; 109 | v6_addr.sin6_family = AF_INET6; 110 | v6_addr.sin6_port = htons(80); 111 | v6_addr.sin6_addr = ipv6_addr; 112 | 113 | //socket connect 114 | int v6_sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); 115 | std::string v6_error; 116 | if (0 != connect(v6_sock, (sockaddr*)&v6_addr, 28)) 117 | { 118 | v6_error = strerror(errno); 119 | } 120 | 121 | //get local ip 122 | sockaddr_in6 v6_local_addr = {0}; 123 | socklen_t v6_local_addr_len = 28; 124 | char v6_str_local_addr[64] = {0}; 125 | getpeername(v6_sock, (sockaddr*)&v6_local_addr, &v6_local_addr_len); 126 | inet_ntop(v6_local_addr.sin6_family, &v6_local_addr.sin6_addr, v6_str_local_addr, 64); 127 | 128 | close(v6_sock); 129 | 这里讨论下比较坑的地方,按照NAT64的规则,客户端如果没有做DNS域名解析的话(微信依赖的是自己实现的NEWDNS),客户端就需要完成DNS64的工作。这里的关键点是,发现网络是IPv6-only的NAT64网络的情况下,我们可以自己补充上前缀`64:ff9b::/96`,然后进行正常的访问。然而这里客户端能获取的信息量一般都是很有限的,怎么样处理这个问题,后面有专门的章节来处理这个问题(**判断客户端支持的IP stack**)。 130 | ###v6 ip + IPv4-only 131 | 这里一般connect的时候会返回错误码`network is unreachable`,因为根本没有v6的协议栈,就像没有硬件设备一样,但是不排除会有系统会返回`no route to host `。 132 | 当然,如果服务器的地址是 `Teredo tunneling 2001::/32`,可以客户端直接做隧道。如果是`6to4 2002::/16`,并且客户端有RAW socket权限加上非NAT网络,这种情况下可以客户端自己做6to4的路由。(这里的结论不一定百分百正确,还需要继续研读RFC)。 133 | 134 | ###v6 ip + IPv6-only or IPv4-IPv6 135 | 这里只要没有配置上,是可以直接通讯的。 136 | 当然这里会涉及到一个问题,如果DNS返回上文说的`6to4`或`Teredo tunneling`或`pure native IPv6 addresses`,这样的情况下我们怎么样做IP的选择呢,这个可以参照[RFC 3484 - Default Address Selection for Internet Protocol version 6 (IPv6)](https://tools.ietf.org/html/rfc3484)。 137 | 138 | ##判断客户端可用的IP stack(IPv4-only、 IPv6-only、IPv4-IPv6 Dual stack) 139 | 原理大家都明白了,但是客户端做不同的处理的前提是需要知道**客户端可用的IP协议栈**。
140 | 我们先定义**客户端可用的IP协议栈**的意思是,获取客户端当前能使用的IP协议栈。例如iOS在NAT64 WIFI连接上的情况下,Mobile的网卡虽然存在IPv4的协议栈,但是系统是不允许使用的。IOS只能使用WIFI的协议栈,在NAT64 WIFI的情况下就是IPv6-only网络了。
141 | 这里还有一个问题需要讨论,如果遇到IPv6-only网络,需要把它当作NAT64来处理,在v4 IP前添加前缀`64:ff9b::/96`。
142 | 但是这里NAT64和IPv6-only不是等价的。IPv6-only网络可能支持NAT64,能访问v4的互联网资源,但是IPv6-only能访问v6的互联网资源,不支持NAT64。这里假设IPv6-only的网络都是支持NAT64的,对v4 IP进行`64:ff9b::/96`的处理。因为不支持NAT64的话,微信服务器v4地址根本就不可访问(当然如果手机系统有`464XLAT`服务,并且运营商支持,也是可以访问v4资源的,但是不在讨论范围了)。 143 | 144 | ###获取本地IP和网关方案(iOS) 145 | - IOS通过sysctl获取当前网关或路由 146 | 147 | 如果只能获取IPv6网关,那当前是IPv6-only 148 | 如果只能获取IPv4网关,那当前是IPv4-only 149 | 如果同时能获取IPv6/IPv4路由,那情况就比复杂,分析如下 150 | IOS在WIFI连接上的情况下,并不会关闭Mobile的网卡。 151 | 在WIFI是IPv6-only网络,Mobile是IPv4-only网络,下v4 socket或者v4-mapped都无法出去。 152 | 证明apple应该对TCP connect函数进行过改造,在WIFI和Mobile共存的情况下,只能走WIFI网络,和Android不一样,iOS不是通过去掉Mobile网卡的方式来做。 153 | 这样导致的一个有趣的特性:网络切换时候如果Mobile 下建立的socket不关闭可以继续使用Mobile网络。 154 | 如果程序使用bind接口绑定到Mobile的网卡下,这个时候是可以使用Mobile网络进行访问的。(这里算不算偷流量呢,当然这里是特性,具体怎么样应用是程序的问题了)。 155 | 因此我们可以考虑WIFI连接了的情况下,我们只要知道网关是对应那张网卡,就可以知道当前是不是当前支持的IP协议栈? 156 | 然而事情没有那么简单,我们先按照刚刚说的思路走下去 157 | 158 | 159 | - 通过getifaddr接口,可以拿到当前全部网络的IP地址(排除掉非活跃和loopback的网卡) 160 | 161 | 如果IPv4、IPv6网关都属于WIFI网卡,那当前是IPv4-IPv6 Dual stack 162 | 如果IPv4、IPv6网关都属于Mobile网卡,那当前是IPv4-IPv6 Dual stack 163 | 到这里都没有问题,但是下面的情况呢: 164 | 如果IPv4网关属于Mobile网卡,IPv6网关属于WIFI? 165 | 如果IPv4网关属于WIFI网卡,IPv6网关属于Mobile? 166 | 这里的情况还要分开,如果是正常情况下IOS在WIFI连接后是不允许使用Mobile网卡的,但是iOS又有一个特性是3G热点。 167 | 在这样的情况下IOS手机本身是走Mobile网络的,WIFI只是做桥接。 168 | 169 | 170 | 这个方案非常复杂,而且跟iOS平台的系统实现强耦合,其他平台必须重新实现,后续如果iOS进行网络逻辑的更新,这里还必须修改。因此这个的方案不太建议大家用。 171 | 172 | ###DNS方案 173 | 这里的方案是直接做DNS解析,然后判断返回的IP有没有带上`64:ff9b`前缀来确定当前的IP协议栈。这也是唯一能够判断IPv6-only网络是否支持NAT64的方案。 174 | 175 | //gateway 176 | in6_addr addr6_gateway = {0}; 177 | if (0 != getdefaultgateway6(&addr6_gateway)) 178 | return EIPv4; 179 | 180 | if (IN6_IS_ADDR_UNSPECIFIED(&addr6_gateway)) 181 | return EIPv4; 182 | 183 | in_addr addr_gateway = {0}; 184 | if (0 != getdefaultgateway(&addr_gateway)) 185 | return EIPv6; 186 | 187 | if (INADDR_NONE == addr_gateway.s_addr || INADDR_ANY == addr_gateway.s_addr ) 188 | return EIPv6; 189 | 190 | //getaddrinfo 191 | struct addrinfo hints, *res, *res0; 192 | memset(&hints, 0, sizeof(hints)); 193 | hints.ai_family = PF_INET6; 194 | hints.ai_socktype = SOCK_STREAM; 195 | hints.ai_flags = AI_ADDRCONFIG|AI_V4MAPPED; 196 | int error = getaddrinfo("dns.weixin.qq.com", "http", &hints, &res0); 197 | 198 | if (0 != error) { 199 | return EIPv4; 200 | } 201 | 202 | for (res = res0; res; res = res->ai_next) { 203 | if (AF_INET6 == res->ai_addr.sa_family) { 204 | if (is_nat64_address(((sockaddr_in6&)res->ai_addr).sin6_addr)) { 205 | return EIPv6; 206 | } 207 | } 208 | } 209 | 210 | return EIPv4; 211 | 212 | 213 | 我们分析下上面的sample,前面gateway的代码是为了加速判断过程,我们知道DNS是一个网络过程,耗时很有可能是非常久的。 214 | `dns.weixin.qq.com`必须保证解析的域名只有v4 ip地址。`hints.ai_family = PF_INET6`利用了DNS64的特性,如果在纯IPv6环境下会返回NAT64映射地址的方式。`AI_V4MAPPED`为了在非DNS64网络下,返回v4-mapped ipv6 address,不会返回EAI_NONAME失败,导致判断不准确。`AI_ADDRCONFIG`返回的地址是本地能够使用的(具体可以看文档下面的介绍)。如果有NAT64前缀的v6地址返回,证明当前网络是IPv6-only NAT64网络。
215 | 不过这个方案有很多缺点,就是耗时不确定,可能因为网络失败导致错误的结果,需要网络流量,会对运营商的DNS服务器造成压力,网络切换需要立刻进行重试重连。
216 | 结论,这个方案不太合适。 217 | 218 | ###socket connect的方式(支持iOS9和Android) 219 | 这里的方案是直接使用v4 IP地址和v6 IP地址进行连接,通过结果来确认当前客户端可用IP stack。 220 | 221 | _test_connect(int pf, struct sockaddr *addr, socklen_t addrlen) { 222 | int s = socket(pf, SOCK_STREAM, IPPROTO_TCP); 223 | if (s < 0) 224 | return 0; 225 | int ret; 226 | do { 227 | ret = connect(s, addr, addrlen); 228 | } while (ret < 0 && errno == EINTR); 229 | int success = errno; 230 | do { 231 | ret = close(s); 232 | } while (ret < 0 && errno == EINTR); 233 | return success; 234 | } 235 | 236 | static int 237 | _have_ipv6() { 238 | static const struct sockaddr_in6 sin6_test = { 239 | .sin6_len = sizeof(sockaddr_in6), 240 | .sin6_family = AF_INET6, 241 | .sin6_port = htons(0xFFFF), 242 | .sin6_addr.s6_addr = { 243 | 0, 0x64, 0xff, 0x9b, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} 244 | }; 245 | sockaddr_union addr = { .in6 = sin6_test }; 246 | return _test_connect(PF_INET6, &addr.generic, sizeof(addr.in6)); 247 | } 248 | 249 | static int 250 | _have_ipv4() { 251 | static const struct sockaddr_in sin_test = { 252 | .sin_len = sizeof(sockaddr_in), 253 | .sin_family = AF_INET, 254 | .sin_port = htons(0xFFFF), 255 | .sin_addr.s_addr = htonl(0x08080808L), // 8.8.8.8 256 | }; 257 | sockaddr_union addr = { .in = sin_test }; 258 | return _test_connect(PF_INET, &addr.generic, sizeof(addr.in)); 259 | } 260 | 261 | enum TLocalIPStack { 262 | ELocalIPStack_None = 0, 263 | ELocalIPStack_IPv4 = 1, 264 | ELocalIPStack_IPv6 = 2, 265 | ELocalIPStack_Dual = 3, 266 | }; 267 | 268 | void test() { 269 | TLocal IPlocal_stack = 0; 270 | int errno_ipv4 = _have_ipv4(); 271 | int errno_ipv6 = _have_ipv6(); 272 | int local_stack = 0; 273 | if ( errno_ipv4 != EHOSTUNREACH && errno_ipv4 != ENETUNREACH) { 274 | local_stack |= ELocalIPStack_IPv4; 275 | } 276 | if (errno_ipv6 != EHOSTUNREACH && errno_ipv6 != ENETUNREACH) { 277 | local_stack |= ELocalIPStack_IPv6; 278 | } 279 | } 280 | 281 | 这个方案是利用外网IP进行连接,如果返回`EHOSTUNREACH`的时候说明本地没有对应的路由到达目标地址,如果`ENETUNREACH`的时候说明本地没有相应的协议栈,这两种情况都是说明相应的协议栈不可用。
282 | 分析下这个方案的缺点,和getaddrinfo一样,耗时不确定,因为有调用connect动作,进行tcp连接。如果connect遇到`EHOSTUNREACH ENETUNREACH`错误是不会耗费流量和立刻返回的,因为这些都是本地网络判断。但是,如果相应网络可用,这个是要花费网络流量的,耗时也不能确定。如果我们连接一个存在的IP,这样在网络好的时候很快返回(这样会对服务器造成连接的压力),网络差的时候很久才返回。如果连接一个不存在的IP,需要很久时间才会返回(75s的连接超时)。 283 | 284 | 这样看来,这三个方案都不完美,根本不能在真实场景中使用, 有没有更加可用的方案呢?iOS 9.0 上层Objc Framework可以无缝支持,但是用bsd socket需要代码完成对应的工作。但是iOS Framework的最新源码也没有开源出来,无法知道其实现原理。
285 | 继续研究发现,getaddrinfo的`AI_ADDRCONFIG` flags有点像我们需要实现的功能,要去掉IP,就必须要知道当前的IP stack。它是怎么样实现的? 286 | 287 | //Android的AI_ADDRCONFIG 功能的sample 288 | _test_connect(int pf, struct sockaddr *addr, socklen_t addrlen) { 289 | int s = socket(pf, SOCK_DGRAM, IPPROTO_UDP); 290 | if (s < 0) 291 | return 0; 292 | int ret; 293 | do { 294 | ret = connect(s, addr, addrlen); 295 | } while (ret < 0 && errno == EINTR); 296 | int success = (ret == 0); 297 | do { 298 | ret = close(s); 299 | } while (ret < 0 && errno == EINTR); 300 | return success; 301 | } 302 | 303 | static int 304 | _have_ipv6() { 305 | static const struct sockaddr_in6 sin6_test = { 306 | .sin6_len = sizeof(sockaddr_in6), 307 | .sin6_family = AF_INET6, 308 | .sin6_port = htons(0xFFFF), 309 | .sin6_addr.s6_addr = { 310 | 0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} 311 | }; 312 | sockaddr_union addr = { .in6 = sin6_test }; 313 | return _test_connect(PF_INET6, &addr.generic, sizeof(addr.in6)); 314 | } 315 | 316 | static int 317 | _have_ipv4() { 318 | static const struct sockaddr_in sin_test = { 319 | .sin_len = sizeof(sockaddr_in), 320 | .sin_family = AF_INET, 321 | .sin_port = htons(0xFFFF), 322 | .sin_addr.s_addr = htonl(0x08080808L), // 8.8.8.8 323 | }; 324 | sockaddr_union addr = { .in = sin_test }; 325 | return _test_connect(PF_INET, &addr.generic, sizeof(addr.in)); 326 | } 327 | 328 | enum TLocalIPStack { 329 | ELocalIPStack_None = 0, 330 | ELocalIPStack_IPv4 = 1, 331 | ELocalIPStack_IPv6 = 2, 332 | ELocalIPStack_Dual = 3, 333 | }; 334 | 335 | void test() { 336 | TLocal IPlocal_stack = 0; 337 | int have_ipv4 = _have_ipv4(); 338 | int have_ipv6 = _have_ipv6(); 339 | int local_stack = 0; 340 | if ( have_ipv4) { 341 | local_stack |= ELocalIPStack_IPv4; 342 | } 343 | if (have_ipv6) { 344 | local_stack |= ELocalIPStack_IPv6; 345 | } 346 | } 347 | 这里的代码构造成和第一个socket connect的类型,只修改一部分代码。 348 | 可以看到,和第一个例子的区别是`socket(pf, SOCK_DGRAM, IPPROTO_UDP)`用了UDP进行连接,UDP可以进行connect,只是进行绑定服务器地址的动作,并不会有网络数据的产生,后续可以直接使用send接口,不需要使用sendto接口(每次都需指定服务器的地址)。
349 | 经过测试iOS和Android都能检测出当前可用的IP stack。我们再做一些思考,如果connect接口在UDP的时候,应该是除了TCP发送syn包外的全部事情都做了的。如果这样考虑的话,这个方案成立的依据还是足够的。
350 | 351 | ###混合的方案(Mac OS,iOS,Linux,Android都支持,Windows/wp待测试) 352 | 发现在iOS8/Mac OS上述方案会有点问题(iOS9正常),就是iOS8上IPv6-only网络也会有`169.254.x.x`的自组网的IPv4 stack(其实iOS9上也有,但不影响测试结果),这样会导致IPv4 stack的udp socket能够connect成功(_have_ipv4()返回1)。应对这种情况,我们可以用前面getdefaultgateway的方案,把自组网排除出没有网关的情况。当然,有手机网的时候,IPv4网关是可以获取到的,还是会走到_have_ipv4的路径。当然,如果have_ipv4和have_ipv6只有一个返回1的情况,我们可以认为只有一个IP stack能用。当然如果是local_stack为ELocalIPStack_Dual,还需要用getdnssvraddrs的函数获取当前的dns服务器列表,通过dns服务器的地址确认当前可用的IP stack。必须说明下,这个不是一个准确的判断,如果网络是ELocalIPStack_Dual,但是dns服务只设置了IPv6的地址(如果是dhcp配置的情况,很少出现这样,一般情况都是手工设置才会出现),会判断当前网络为ELocalIPStack_IPv6。这样ELocalIPStack_Dual的网络可能不支持NAT64,这样会导致程序无法访问网络。
353 | 这个方案是本地操作,成本低,没有网络流量消耗和耗时问题,暂时是最好的可用IP stack检测方案。(当然NAT64检测不了) 354 | 新的实现代码如下: 355 | 356 | TLocalIPStack local_ipstack_detect() { 357 | in6_addr addr6_gateway = {0}; 358 | if (0 != getdefaultgateway6(&addr6_gateway)){ return ELocalIPStack_IPv4;} 359 | if (IN6_IS_ADDR_UNSPECIFIED(&addr6_gateway)) { return ELocalIPStack_IPv4;} 360 | 361 | in_addr addr_gateway = {0}; 362 | if (0 != getdefaultgateway(&addr_gateway)) { return ELocalIPStack_IPv6;} 363 | if (INADDR_NONE == addr_gateway.s_addr || INADDR_ANY == addr_gateway.s_addr ) { return ELocalIPStack_IPv6;} 364 | 365 | int have_ipv4 = _have_ipv4(); 366 | int have_ipv6 = _have_ipv6(); 367 | int local_stack = 0; 368 | if (have_ipv4) { local_stack |= ELocalIPStack_IPv4; } 369 | if (have_ipv6) { local_stack |= ELocalIPStack_IPv6; } 370 | if (ELocalIPStack_Dual != local_stack) { return (TLocalIPStack)local_stack; } 371 | 372 | int dns_ip_stack = 0; 373 | std::vector dnssvraddrs; 374 | getdnssvraddrs(dnssvraddrs); 375 | 376 | for (int i = 0; i < dnssvraddrs.size(); ++i) { 377 | if (AF_INET == dnssvraddrs[i].address().sa_family) { dns_ip_stack |= ELocalIPStack_IPv4; } 378 | if (AF_INET6 == dnssvraddrs[i].address().sa_family) { dns_ip_stack |= ELocalIPStack_IPv6; } 379 | } 380 | 381 | return (TLocalIPStack)(ELocalIPStack_None==dns_ip_stack? local_stack:dns_ip_stack); 382 | } 383 | 384 | ##其他编程问题 385 | 建议大家认真看apple的文档[Supporting IPv6 DNS64/NAT64 Networks]和[RFC 4038 - Application Aspects of IPv6 Transition],里面很多事情都说清楚了,这里说下其他需要关注的地方。 386 | 387 | ###sockaddr的存储sockaddr_storage 388 | 这里千万不要犯傻用`sockaddr`存储`sockaddr_in6`数据,IOS上sockaddr的大小是16,和sockaddr_in一致的,但是sockaddr_in6大小是28(不要问我为什么会知道,都是泪)。通用的sockaddr的存储的结构体是`sockaddr_storage`,它是能存储任何sockaddr的结构。 389 | 你可能会问,如果socket用AF_INET6的时候,用`sockaddr_in6`结构体不就好了。不是说不可以,就是代码会变成IPv6专用的了,如果用到其他地方可能会出错。但是如果用AF_INET呢,虽然强转成sockaddr_in没有任何问题,但是程序逻辑上蛋疼,如果大家要写v4/v6通用的逻辑的话,最好还是用`sockaddr_storage`存储,然后通过`ss_family`进行判断,最后做不同分支的处理。 390 | 391 | //sockaddr_storage sample 392 | socket_address socket_address::getsockname(SOCKET _sock) 393 | { 394 | struct sockaddr_storage addr = {0}; 395 | socklen_t addr_len = sizeof(addr); 396 | ::getsockname(_sock, (sockaddr*)&addr, &addr_len); 397 | 398 | if (AF_INET == addr.ss_family) 399 | { 400 | return socket_address((const sockaddr_in&)addr); 401 | } 402 | else if (AF_INET6 == addr.ss_family) 403 | { 404 | return socket_address((const sockaddr_in6&)addr); 405 | } 406 | 407 | return socket_address("", 0); 408 | } 409 | 410 | ###更加节省空间的方案 411 | `sockaddr_storage`是能够保存所有`sockaddr`下属的类型,但是128字节的大小有时候有点不可接受,而且每次使用都需要做类型转换。下面提供一个更加优雅的方案,大小是28字节,节省了很多。 412 | 413 | union sockaddr_union { 414 | struct sockaddr sa; 415 | struct sockaddr_in in; 416 | struct sockaddr_in6 in6; 417 | } m_addr; 418 | 419 | if (AF_INET == m_addr.sa.sa_family) { 420 | return ntohs(m_addr.in.sin_port); 421 | } else if (AF_INET6 == m_addr.sa.sa_family) { 422 | return ntohs(m_addr.in6.sin6_port); 423 | } 424 | 425 | ###NSURLConnection 426 | apple要求大家不要直接用IP访问,不过,中国的DNS环境这么恶劣,没有其他更好的办法。 427 | 那NSURLConnection怎么样能够在IPv6访问正常的访问呢?我们应该构建怎么样的URL呢?
428 | 我们先看看wikipedia的说法 429 | > 430 | > **Literal IPv6 addresses in network resource identifiers** [^4] 431 | > 432 | > Colon (:) characters in IPv6 addresses may conflict with the established syntax of resource identifiers, such as URIs and URLs. The colon has traditionally been used to terminate the host path before a port number.[6] To alleviate this conflict, literal IPv6 addresses are enclosed in square brackets in such resource identifiers, for example: 433 | > 434 | > http://[2001:db8:85a3:8d3:1319:8a2e:370:7348]/ 435 | > 436 | > When the URL also contains a port number the notation is: 437 | > 438 | > https://[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443/ 439 | 440 | 我们可以看到除了IP形式不一样外,还多了中括号。当然,上面我们说到**IPv4-mapped IPv6 address**和**NAT64 mapped address**同样也是适用的,例如:`http://[::ffff:14.17.32.21]:80`走IPv4协议栈或`http://[64:ff9b::14.17.32.21]`走NAT64。关键点还是判断当前 441 | **客户端可用的IP stack**。 442 | 443 | ###IOS下CoreFoudation或者更高级的API 444 | 引用手Q同事的原话: 445 | > 如果使用CoreFoudation或者更高级的API,即使在纯IPv6环境下使用IPv4的ip进行网络通信,iOS9会自动把IPv4地址转换成IPv6地址。换句话说,因为手q里面大部分api都是满足要求的,基本上不用改动。(注意iOS9.0或以上) 446 | 447 | 例如CFStreamCreatePairWithSocketToCFHost ..待续 448 | 449 | ###DNS API 450 | apple的文档说了,gethostbyname这些已经不能用了(只支持IPv4),只能用getaddrinfo。 451 | 452 | struct addrinfo hints, *res, *res0; 453 | memset(&hints, 0, sizeof(hints)); 454 | hints.ai_family = PF_UNSPEC; 455 | hints.ai_socktype = SOCK_STREAM; 456 | hints.ai_flags = AI_DEFAULT; 457 | getaddrinfo("www.qq.com", "http", &hints, &res0); 458 | 459 | 这里sample比较简单,其实getaddrinfo的重点在`hints.ai_family`和`hints.ai_flags`的设置上,apple已经给出了一个很好sample。我们分析下这两个变量不同设置下的效果,看看有什么区别。 460 | 461 | - `hints.ai_family = PF_UNSPEC`的意思是v4地址和v6地址都返回,不过呢,这里可是会触发两个UDP的请求,当年微信就给运营商吐槽过,你没有v6地址,就不要做v6请求拉(微信量大)。不过apple爸爸要求用v6地址,怎么办? 462 | - `hints.ai_family = PF_INET`的意思是只返回v4地址 463 | - `hints.ai_family = PF_INET6`的意思是只返回v6地址 464 | - `hints.ai_flags |= AI_V4MAPPED 且 hints.ai_family = PF_INET6`的情况下,如果需要dns的host没有v6地址的情况下,getaddinfo会把v4地址转换成`v4-mapped ipv6 address`,如果有v6地址返回就不会做任何动作。 465 | - `hints.ai_flags |= AI_ADDRCONFIG`这个是一个很有用的特性,这个flags表示getaddrinfo会根据本地网络情况,去掉不支持的IP协议地址。 466 | - `hints.ai_flags = AI_DEFAULT`其实就是`AI_V4MAPPED|AI_ADDRCONFIG_CFG`,也是apple推荐的flags设置方式。 467 | 468 | > 域名 对应着如下 IP 地址:
469 | > 173.194.127.180
470 | > 173.194.127.176
471 | > 2404:6800:4005:802::1010
472 | > 若本地主机仅配置了 IPV4 地址,则返回的查询结果中不包含 IPV6 地址,即此时只有:
473 | > 173.194.127.180
474 | > 173.194.127.176
475 | > 同样若本地主机仅配置了 IPV6 地址,则返回的查询结果中仅包含IPV6地址. 476 | > 2404:6800:4005:802::1010
477 | 478 | 用这个API的时候,建议大家还是按照apple的sample来做`hints.ai_family`暂时先`PF_INET`,免得运营商投诉,当然最好是能后台进行控制。 479 | 480 | 下面一段话是apple文档内对getaddrinfo对NAT64支持的描述。 481 | > The current implementation supports synthesis of NAT64 mapped IPv6 addresses. If hostname is a numeric string defining an IPv4 address (for example, '192.0.2.1' ) and ai_family is set to PF_UNSPEC or PF_INET6, getaddrinfo() will synthesize the appropriate IPv6 address(es) (for example, '64:ff9b::192.0.2.1' ) if the current interface supports IPv6, NAT64 and DNS64 and does not support IPv4. If the AI_ADDRCONFIG flag is set, the IPv4 address will be suppressed on those interfaces. On non-qualifying interfaces, getaddrinfo() is guaranteed to return immediately without attempting any resolution, and will return the IPv4 address if ai_family is PF_UNSPEC or PF_INET. NAT64 address synthesis can be disabled by setting the AI_NUMERICHOST flag. To best support NAT64 networks, it is recommended to resolve all IP address literals with ai_family set to PF_UNSPEC and ai_flags set to AI_DEFAULT. 482 | 483 | 可以看到apple最推荐的getaddrinfo用法就是sample那样。 484 | 485 | ###iOS SCNetworkReachabilityCreateWithAddress API问题 486 | 在iOS下,一般判断网络连通性和WIFI/Mobile网络的判断是使用SCNetworkReachabilityCreateWithAddress API,一般的sample里面只会测试IPv4的IP地址,这样有可能导致在纯IPv6网络下判断出当前是没有网络,这样明显是不对的。 487 | 488 | struct sockaddr_in zeroAddress; 489 | bzero(&zeroAddress, sizeof(zeroAddress)); 490 | zeroAddress.sin_len = sizeof(zeroAddress); 491 | zeroAddress.sin_family = AF_INET; 492 | 493 | SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)zeroAddress); 494 | 495 | 针对IPv6可以使用下面的方式来判断 496 | 497 | struct sockaddr_in6 zeroAddress6; 498 | bzero(&zeroAddress6, sizeof(zeroAddress6)); 499 | zeroAddress6.sin6_len = sizeof(zeroAddress6); 500 | zeroAddress6.sin6_family = AF_INET6; 501 | 502 | SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)zeroAddress6); 503 | 504 | 当然大家写通用代码的时候,可以IPv6和IPv4都判断。
505 | 最后苹果建议的方式是SCNetworkReachabilityCreateWithName这个API,个人暂时不确定这个API是不是会进行DNS解析。 506 | 507 | ##文档的推荐和说明 508 | [Beej's Guide to Network Programming 简体中文](http://www.netdpi.net/files/beej/Beej-cn-20140429.zip) 这本书不错的,介绍了很多API的使用,当然IPv6部分也有
509 | [Beej's Guide to Network Programming 繁体中文](http://beej-zhtw.netdpi.net) 排版比简体好
510 | unix network programming 不用说了,不过没有v6的部分
511 | [Dual-Stack Sockets for IPv6 Winsock Applications(Windows XP SP1后都支持)](https://msdn.microsoft.com/zh-cn/library/windows/desktop/bb513665(v=vs.85).aspx) 512 | ##IPv6下不能使用的API列表 513 | - gethostbyname() 514 | - gethostbyaddr() 515 | - getservbyname() 516 | - getservbyport() 517 | - gethostbyname2() 518 | - inet_addr() 519 | - inet_aton() 520 | - inet_lnaof() 521 | - inet_makeaddr() 522 | - inet_netof() 523 | - inet_network() 524 | - inet_ntoa() 525 | - inet_ntoa_r() 526 | - bindresvport() 527 | - getipv4sourcefilter() 528 | - setipv4sourcefilter() 529 | 530 | 下面类型或者结构需要注意使用的正确性 531 | 532 | IPv4|IPv6 533 | ---|--- 534 | AF_INET|AF_INET6 535 | PF_INET|PF_INET6 536 | struct in_addr|struct in_addr6 537 | struct sockaddr_in|struct sockaddr_in6 538 | kDNSServiceProtocol_IPv4|kDNSServiceProtocol_IPv6 539 | 540 | 541 | [Supporting IPv6 DNS64/NAT64 Networks]:https://developer.apple.com/library/ios/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/UnderstandingandPreparingfortheIPv6Transition/UnderstandingandPreparingfortheIPv6Transition.html#//apple_ref/doc/uid/TP40010220-CH213-SW11 (Supporting IPv6 DNS64/NAT64 Networks) 542 | [RFC 4038 - Application Aspects of IPv6 Transition]:https://www.ietf.org/rfc/rfc4038 (RFC 4038 - Application Aspects of IPv6 Transitio) 543 | [^1]: [wikipedia Transition from IPv4](https://en.wikipedia.org/wiki/IPv6_address#Transition_from_IPv4) 544 | [^2]: [wikipedia NAT64](https://en.wikipedia.org/wiki/IPv6_address#NAT64) 545 | [^3]: [wikipedia DNS64](https://en.wikipedia.org/wiki/IPv6_address#DNS64) 546 | [^4]: [wikipedia IPv6 address](https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers) 547 | 548 | 549 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## WeMobileDev Articles 2 | 3 | Articles in [mp.weixin.qq.com](http://mp.weixin.qq.com), you can scan the following qrcode to follow us! 4 | 5 | ![qrcode_for_wemobiledev.jpg](assets/qrcode_for_wemobiledev.jpg) 6 | 7 | 8 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [关于](README.md) 4 | 5 | ## 0x00 微信实践 6 | 7 | * [基于 TLS 1.3 的微信安全通信协议 mmtls 介绍](基于TLS1.3的微信安全通信协议mmtls介绍.md) 8 | * [IPv6 socket 编程](IPv6 socket编程.md) 9 | * [微信 iOS SQLite 源码优化实践](微信iOS SQLite源码优化实践.md) 10 | * [聊聊苹果的 Bug - iOS 10 nano free crash](聊聊苹果的Bug - iOS 10 nano_free Crash.md) 11 | * [微信Android视频编码爬过的那些坑](微信Android视频编码爬过的那些坑.md) 12 | 13 | ## 0x10 开源组件 14 | 15 | * [0x11 mars](0x10/0x11-mars.md) 16 | * [微信终端跨平台组件 Mars 系列(一) - 高性能日志模块 xlog](微信终端跨平台组件 Mars 系列(一) - 高性能日志模块xlog.md) 17 | * [微信终端跨平台组件 Mars 系列 - 我们如约而至](微信终端跨平台组件 Mars 系列 - 我们如约而至.md) 18 | * [0x12 tinker](0x10/0x12-tinker.md) 19 | * [微信 Android 热补丁实践演进之路](微信Android热补丁实践演进之路.md) 20 | * [微信 Tinker 的一切都在这里,包括源码(一)](微信Tinker的一切都在这里,包括源码(一).md) 21 | * [ART 下的方法内联策略及其对 Android 热修复方案的影响分析](ART下的方法内联策略及其对Android热修复方案的影响分析.md) 22 | * [Android N 混合编译与对热补丁影响解析](Android_N混合编译与对热补丁影响解析.md) 23 | * [0x13 wcdb](0x10/0x13-wcdb.md) 24 | * [微信移动端数据库组件WCDB系列(一)- iOS基础篇](微信移动端数据库组件WCDB系列(一)-iOS基础篇.md) 25 | * [微信移动端数据库组件WCDB系列(二)- 数据库修复三板斧](微信移动端数据库组件WCDB系列(二) — 数据库修复三板斧.md) 26 | * [微信移动端数据库组件WCDB系列(三)- WINQ原理篇](微信移动端数据库组件WCDB系列(三) — WINQ原理篇.md) -------------------------------------------------------------------------------- /Tinker:技术的初心与坚持.md: -------------------------------------------------------------------------------- 1 | # Tinker:技术的初心与坚持 # 2 | 2016年3月10日,Tinker项目正式启动,并在同年9月23日举行的MDCC会议上开源。一年过去了,两个人,50%的工作时间。总的来说,填了一些坑,获得少许成绩,也遭受不少批评。究竟Tinker是否将已经很糟糕的Android的生态变得更差,会不会对用户的安全造成更大的挑战? 3 | 4 | 回想Tinker的初心,我们希望开发者可以用很小代价进行快速升级,它是国内追求快速迭代诉求。立项至今,Tinker踩了很多坑也填了很多坑。今天,我希望跟大家分享这一年来我们遇到的一些问题,以及解决它们的思路与过程。 5 | 6 | ## Tinker的现状 ## 7 | 首先在回顾过去之前,我想先简单的介绍一下Tinker的现状。 8 | 9 | ### 开源的现状 ### 10 | Tinker的开源地址为:[https://github.com/Tencent/tinker](https://github.com/Tencent/tinker)。它作为Github/Tencent的第一个开源项目,也让Tencent第一次在Github周排名第一。微信也在持续使用Tinker,并且我们承诺与外部开发者使用同样的开源版本。不仅如此,在应用宝Top 1000的应用中,有60多个应用已经使用了Tinker,使用第三方平台接入Tinker并持续使用的应用也超过1000个。 11 | 12 | ![](assets/tinker_summary/github.png) 13 | 14 | ### 生态的现状 ### 15 | 使用的开发者数是一方面,更令人振奋的是,Tinker初步建立了它自己的小生态。 16 | 17 | **一. 热修复服务平台** 18 | 19 | 个人感觉热补丁不是请客吃饭,如果不了解它,直接使用它可能会造成更大的问题,所以在一些接入上面,的确人为的增加了难度。 20 | >热修复不是请客吃饭 21 | 22 | 对于某些的产品来说并不一定成立,它们希望无论客户端、后台都有一整套的服务,它只要: 23 | >一行代码,快速接入 24 | 25 | 当前[TinkerPatch](http://www.tinkerpatch.com/)与[Bugly](https://bugly.qq.com/v2/)都基于Tinker提供了热修复的一站式服务,降低了许多开发者的工作。 26 | 27 | **二. 厂商** 28 | 29 | 受益于微信的产品影响力,我们与OPPO、Vivo、Huawei、小米、一加、联想等厂商都建立了紧密的联系。他们不仅帮助我们解决了许多兼容性问题,每次Tinker升级,厂商也会帮忙做相关兼容性的测试。更重要的是Tinker的出现与推广使得厂商在系统定制改造时也会考虑到是否会影响热修复。 30 | 31 | 同时我们一直极力反对厂商对微信做定制的优化,我们希望在Tinker框架内能够解决,所有的用户所有的产品表现是一致的。在这一年来,的确十分感谢厂商的帮助与支持。 32 | 33 | **三. 加固** 34 | 35 | 对于许多开发者,它们因为各种各样的原因必须要使用加固。首先在`1.7.0`版本我们通过回退QZone的方案支持加固,但是发现市面上各种的加固实现差异非常大,且对我们是黑箱。最终在`1.7.6`版本取消了对加固的支持。最近我们联合乐加固、360、爱加密等加固厂商,一起讨论协商了支持热修复的加固方案。最终我们商定如下规则: 36 | 37 | 1. 不能提前导入类; 38 | 2. 在Art平台若要编译oat文件,需要将内联取消。 39 | 40 | 我们并不想让它们仅仅支持Tinker,我们希望整个生态是健康的,其他的热修复方案也应该被支持。随着加固厂商陆续发布了新版,Tinker在1.7.9版本也可以很好的支持上述加固厂商。 41 | 42 | 此外我们也看到有一些基于Tinker衍生的开源项目,例如[tinker-dex-dump](https://github.com/LaurenceYang/tinker-dex-dump) 、[tinker-manager](https://github.com/baidao/tinker-manager)、[TinkerPatch](https://github.com/TinkerPatch)等。 43 | 44 | ## 跪着走完的路 ## 45 | 一个开源项目不仅仅是一堆源码,更像一个产品。这里需要很多技术之外的努力,与第三方平台、厂商沟通,争取加固厂商的支持等等。但是技术本身才是最大的影响因素,我们一直坚持使用最大的努力去保证质量。下面简单回顾一下Tinker这一年遇到的一些比较有代表性的问题。大家可能不一定会遇到,希望解决问题的思路与过程会对你们有所启发。 46 | 47 | ### 一、Qzone方案在Dalvik与Art的问题分析 ### 48 | Qzone方案在Dalvik与Art的问题是我们在热修复道路上第一个比较大的挑战,也是我们启动Tinker项目的主要原因。详细分析可以参考[微信Android热补丁实践演进之路](https://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286306&idx=1&sn=d6b2865e033a99de60b2d4314c6e0a25#rd)。 49 | 50 | 以Art地址偏移的例子来说,当时我们某次补丁发现在5.0的机器线上发现以下的一个crash: 51 | 52 | ![](assets/tinker_summary/qzone-art.png) 53 | 54 | 对应的crash路径对应代码是一个static Boolean对象为空,这非常颠覆我们的认知。 55 | 56 | ``` 57 | static Boolean addFriendTipsShow = false 58 | ``` 59 | 60 | 但是这个问题只有补丁存在的情况会出现,如何去定位与分析? 61 | 62 | 1. 增加日志;通过增加日志,我们发现整个调用流程并没有问题,但是访问这个变量的时候还是会出现NPE。 63 | 2. 查看源码;在Android 5.0之后,推出了AOT,它在dex2oat的时候提前生成机器码,提升运行速度。我们怀疑补丁有可能造成访问了错误的地址,但是过程并不容易。Art相关的代码比Dalvik复杂很多,我们大约花了一周时间才把相关的代码研究了一遍,的确发现了可疑的路径。 64 | 3. 编Rom证实;如何证实?我们通过自己编译Rom并增加相关的日志,为了看清对象内存排列,还把内存地址Dump出来。最后发现地址的确错乱了,错误的调用了`static ImageView sightChangeImage`变量。 65 | 66 | 这个经历告诉我一个道理,在使用一个方案之前,需要知其然以及所以然。同时也给我们很大的信心,让我们坚信只要能复现都是可以找到原因的。 67 | 68 | ### 二、Android N混合编译问题 ### 69 | Android N的问题是在Tinker 1.7.0版本解决,对这个问题的详细分析可以参考[Android N混合编译与对热补丁影响解析](https://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286341&idx=1&sn=054d595af6e824cbe4edd79427fc2706#rd)。 70 | 71 | 这个问题源于华为在Android N内测过程给我们提的一个crash: 72 | 73 | ![](assets/tinker_summary/android_n.png) 74 | 75 | 这个问题只在Android N出现,但是在本地却无法复现。首先我们怀疑一定与Android N的某些变更相关,对虚拟机部分N重大的变化是默认打开了JIT,并使用了混合编译模式。通过跟Google工程师与华为负责Art的工程师讨论沟通,比较确认是这块的变动导致的。怎么样去解决? 76 | 77 | 1. 查看源码;一定要带着目标去阅读源码,不然容易被庞大的代码淹没。因为有了之前的基础,这里大约花了3天时间也大致知道原因。在本地通过生成全量的`base.art`成功复现。 78 | 2. 问题解决;这里提出了几种方案,最后采用了替换Classloader的方式。不得不说,这个方案并不完美,对启动时间也造成了部分影响。 79 | 80 | 在Android O出来之后,我们惊奇的发现ClassTable的指针移到上层,我们也在尝试其他的方式去规避`base.art`的影响。这个问题给我比较大的体会是解决问题时不能拘泥于现象,从一些可疑的点出发,尝试本地去构造复现的场景。 81 | 82 | ### 三、Art的内联导致Crash ### 83 | 内联的问题对Tinker的影响是非常巨大的,这个问题在Tinker 1.7.6版本解决,对它的详细分析可以参考[ART下的方法内联策略及其对Android热修复方案的影响分析](https://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286426&idx=1&sn=eb75349c0c3663f10fbdd74ef87be338&chksm=8334c398b4434a8e6933ddb4fda4a4f06c729c7d2ffef37e4598cb90f4602f5310486b7f95ff#rd)。 84 | 85 | 问题的发现首先是线上出现下面的一个Crash: 86 | 87 | ![](assets/tinker_summary/inline.png) 88 | 89 | 这个Crash与之前Art地址偏移的问题非常像,当时我们还在使用分平台合成。这里首先怀疑是我们某些类没有正确的打入或者没有生效,通过查看出现异常代码的机器码,我们发现是因为虚拟机内联导致出现类似地址错乱的问题。 90 | 91 | 因为这个问题,我们忍痛将之前花费一个多月实现的分平台合成废弃,强制使用全量覆盖的方式。但是这个也带了另外一个严重问题,即厂商OTA之后导致的启动过慢,甚至ANR的问题,这里在后面会详细说明。 92 | 93 | ### 四、华为微信双开导致Crash ### 94 | 这个问题在Tinker 1.7.7版本发现,在某次补丁,我们发现在华为部分Android N的机器会出现以下的Crash: 95 | 96 | ![](assets/tinker_summary/huawei_fenshen.jpg) 97 | 98 | 这个问题只在华为的部分机器出现,而且量的占比并不大。当时的解决思路主要分为以下几步: 99 | 100 | 1. 我们怀疑是华为修改了部分虚拟机的逻辑导致的,经过跟华为工程师的沟通了解,这个怀疑点初步排除。 101 | 2. 找到相同固件的手机,但是并不重现。这个问题外部用户并没有反馈,所以怀疑可能是某些对微信特殊的逻辑导致。在手机的设置中,发现微信分身的设置。呵呵,年轻人你很大嫌疑。 102 | 3. 补丁后,再开启分身,问题的确重现了。原因是华为分身时没有将所有路径都映射,导致分身没有读取补丁路径的权限。补丁由于安全模式被清除,导致主号出现上面的异常。 103 | 104 | 把问题报给华为的工程师后,在新版将这个问题解决了。这个问题的难度在于如何构造复现场景,如何将Crash跟微信分身关联起来。当时考虑的点主要是这个问题只在非常少部分的机器出现,这些人非常可能使用了特殊的功能。到此我们依然坚信,只要能复现,一切都好说。 105 | 106 | ### 五、Oppo/Vivo 异步dex2oat问题 ### 107 | 这个问题在Tinker 1.7.6版本发现,并在Tinker 1.7.7版本解决。dex2oat系统的实现是会阻塞调用线程,Oppo/Vivo为了加快调用,先使用解释模式执行,然后异步去生成oat文件。 108 | 109 | 这个问题会导致我们以为oat文件已经生成,事实上并没有。修改的方案一是跟他们沟通,希望他们不要对微信做这个特殊的优化,然后是补丁合成时需要等待oat文件真正有效的生成。 110 | 111 | 厂商的这个思路对我们后来解决OTA的问题有着一定的启发。 112 | 113 | ### 六、Dexdiff 算法有效性与性能优化 ### 114 | Dexdiff算法非常复杂,若有兴趣可以查看[Tinker Dexdiff算法解析](https://www.zybuluo.com/dodola/note/554061)与[Tinker MDCC会议 slide](https://github.com/WeMobileDev/article/blob/master/final-%E5%BE%AE%E4%BF%A1%E7%83%AD%E8%A1%A5%E4%B8%81%E5%AE%9E%E8%B7%B5%E6%BC%94%E8%BF%9B%E4%B9%8B%E8%B7%AF-v2016-9-24.pdf)。 115 | 116 | 我们也非常害怕它出问题,所以做了一套验证流程与方案: 117 | 118 | 1. 通过固定Method、Field、Class随机生成1000个dex互相做Diff与Patch验证; 119 | 2. 为了更加真实,使用微信最近100个版本的,随机选择两个出来做Diff与Patch验证; 120 | 3. 还是不敢保证100%?在编译的时候提前验证最终Dex的合法性,即使出现问题,只能编译不出来补丁包,而不能影响线上用户。 121 | 122 | 在有效性之外,我们还做过几轮的内存与耗时优化,具体可以查看1.7.7版本的提交。我坚信我们要用科学的态度去研究问题,所以在线上我对Tinker加了129个监控上报,每个问题每个更改都会去总结分析线上的数据。 123 | 124 | ### 七、Android N之前的JIT问题 ### 125 | 这个问题在Tinker 1.7.8解决。有厂商反馈在它们的某台6.0的机器,微信在补丁后有一定的概率出现Crash。 126 | 127 | 拿到厂商快递的机器之后,在补丁后连续进入一个群8次以上,的确会出现Crash。根据之前的经验,如果是经过一段时间才出现问题,跟JIT应该是有关的。查看了这台手机的配置,的确是打开了JIT,事实上7.0之前JIT都是默认关闭的,这只是一个实现中功能。询问了厂商,这个打开其实是某个开发在Debug过程无意打开的,如何解决? 128 | 129 | 事实上,所有的热修复方案可能都会踩中这个问题。这部分的用户并不多,灰度30W人,只有46人在N之前打开了JIT。具体的解决方法是过滤掉Android N之前开启JIT的机器,详细的解决代码可参考Tinker 1.7.8的commit。 130 | 131 | ### 八、Odex损坏问题 ### 132 | Odex损坏事实上会出现在任意动态加载dex过程,这个问题在Tinker 1.7.8解决。在线上我们有时候会发现部分用户会出现`NoSuchMehod`等奇怪的Crash,之前一直不知道原因。 133 | 134 | [issue 328](https://github.com/Tencent/tinker/issues/328) 指的可能是由于oat文件异常导致,通过提取部分Crash用户的Odex文件,我们发现该Odex文件的确偏小,而且不是合法的Elf文件。 135 | 136 | 解决方案是在oat结束时检测补丁生成的odex文件是否为合法的Elf文件。具体的检测方法可参考文件[ShareElfFile.java](https://github.com/Tencent/tinker/blob/master/tinker-android/tinker-android-loader/src/main/java/com/tencent/tinker/loader/shareutil/ShareElfFile.java)。同样灰度30W人,出现odex异常的有336人,大约0.1%概率。若出现这种情况,我们需要删除非法的odex文件,重新执行dex2oat。 137 | 138 | ### 九、厂商OTA后应用启动耗时问题 ### 139 | 在系统OTA后,旧的补丁的oat文件都已经过期。系统会在首次加载时,会重新执行dex2oat。这导致可能会在前台等待很长的时间,甚至出现ANR。这也是Vivo在某次会议上点名批评Tinker的最大原因。 140 | 141 | 事实上,我们并非没有努力过。更早的时候,我们花费了1个多月的时间实现了分平台合成方案。即在Dalvik合成全量的Dex,在Art合成一个小的Dex。同一个输入,生成不同的并且合法的输出。这个的确不容易,我们也是踩过无数的坑,无数次尝试才实现。但是由于Art的内联问题,这个方案需要废弃。 142 | 143 | 还有什么样的方案?这个时候,厂商给我们抛出橄榄枝,可以给微信做单独的优化: 144 | 145 | 1. 在系统升级时,帮助微信把Tinker目录的odex文件重新做dex2oat; 146 | 2. 首次调用补丁dex2oat时,采用类似Oppo/Vivo的异步策略。 147 | 148 | 但是个人坚决反对这些特殊的优化,如果没有做定制的厂商怎么办?外部使用Tinker的应用怎么办?这不是一个非常良好的选择。如何解决: 149 | 150 | 1. 回退版本;检测到厂商OTA之后,我们立刻删除补丁,然后再在后台异步重新做dex2oat。这个方法非常简单,看起来也的确可以解决OTA的问题,但大范围的回退版本是否会造成更大的问题,尽管只是短暂的回退。假设我某次补丁修改了某些数据库的结构?所以这个方案是不能采用的。 151 | 152 | 2. 弹等待框;看起来厂商OTA的间隔不会非常频繁,如果使用等待框的方式用户也可以接受。这个我们采用的方案是当检测到系统OTA后,使用单独的进程去展示等待框。看起来好像没问题,但是这个有个非常大的问题,当主进程dex2oat超过60S的时候,一样会由于bg anr被系统杀死。这个方案在[Commit](https://github.com/Tencent/tinker/commit/28f1852809b07556918ddb7791fa8254d7bfe51f)中提交,很快被删除了。但是这套代码其实可以应用在Dalvik的多dex加载,大家可以参考一下。 153 | 154 | 3. 解释执行;受Oppo/Vivo异步执行dex2oat启发,我们是否可以在OTA的首次先使用解释模式执行odex文件,在后台再做异步的dex2oat?事实上,这也是我们最终采用的方案。但是这里要注意的细节其实非常多,如果判断解释执行成功,解释执行的命令参数如何拼写,instruction set如何获取?大家可以参考这个[Commit](https://github.com/Tencent/tinker/commit/30c03bd0d4e64a102dab9f5d5ec3ff6d954507ad). 155 | 156 | 事实上,往往大家看到的是我们在尝试多个方案,踩过各种坑后的结果。但是过程也是很重要,对我们解决问题的思路与经验的积累都有着非常大的帮助。 157 | 158 | ### 十、资源相关的问题 ### 159 | 上面讲的都是Dex相关的一些问题,但是资源相关的问题也是非常多的。例如 160 | 161 | 1. 一些厂商的适配`BaiduAssetManager/HwResourceImpl`; 162 | 2. 如何检测Dex或者Resource是否真正的补丁成功,通过自定义checkDexInstall/checkResUpdate方法; 163 | 3. 如何快速的合成资源补丁,这里通过研究Zip格式,做到没有解压与压缩实现补丁的资源合成; 164 | 4. resources.arsc如何判断真正的内容修改?这里是通过重写[apk-parser](https://github.com/shwenzhang/apk-parser)去解析resources.arsc的内容,忽略结构、顺序、以及public属性的影响; 165 | 5. webview的问题,Android 7.0之后需要反射`mResDir`以及`publicSourceDir`字段。 166 | 167 | 对于资源,感觉最大的挑战是小米的一个问题。在某次补丁后,我们发现在小米的5.0手机会出现以下的Crash: 168 | 169 | ![](assets/tinker_summary/miui.png) 170 | 171 | 这个问题只在小米出现,而且微信补丁了非常多次,为什么只有这次会出现?更加神奇的是,重启手机的第一次这个问题不会出现。从现象看来,似乎是资源错乱了。为什么会这样,有什么样的解决思路? 172 | 173 | 1. 寻求小米的帮助;这个问题应该跟Miui的一些修改相关,询问了小米相关的开发人员。由于无法定位到具体的模块,无法得到进一步的帮助; 174 | 2. 看代码;将出现问题小米的Rom提取出来,同样由于源码的范围太大,如果无法定位到相关的刻意模块。同样无法进一步分析; 175 | 3. Xposed hook函数分析;终于我们使用迫不得已的绝招,对出现问题的函数前后的系统函数一个个的hook。看究竟是在哪一个方法导致资源的读取错乱了。 176 | 177 | 最后我们发现,这个crash是在ImageView初始化drawable时加载了错误的xml文件导致的。由于Miui在加载自己的主题资源时使用自定义MiuiTypedArray,所以这里拿出来的就非常有可能是之前用过的MiuiTypedArray,导致后面调用typedArray的getDrawable方法时逻辑与原生系统不一致。 178 | 179 | 这个问题只在补丁的时候增加资源Type的时候会出现,为什么重启的第一次没有问题?那是因为重启的第一次微信被broadcast拉起来,没有加载UI,所以也不会出现MiuiTypedArray缓存的错乱问题。最后的解决方法也非常简单,资源补丁时直接清空TypedArray的缓存即可。 180 | 181 | ### 十一、其他 ### 182 | Xposed/厂商预加载的问题,classloader的问题,proguard冲突的问题... 183 | 184 | 回想起来,这的确是一条跪着走完的路。特别是被Vivo点名批评之后,我们也做了反思。解决OTA的问题,限定dex2oat的线程数,锁屏后去做补丁的合成,我们希望减少对用户的影响,与厂商共赢。 185 | 186 | 这一路走完,对我们的收获也是巨大。让我们坚信只要复现都应该可以找到原因,若无法复现,请创造一切条件复现。这个过程经验往往非常重要,而经验则需要我们不断的去尝试与总结。 187 | 188 | # 没做好的事情 # 189 | 由于边幅问题,还有很多技术细节问题没有讲到。Tinker这个项目我们的确花了比较多的心血,特别是在2017年Tom和我都有其他一些高优先级的工作,很多Tinker的工作都会放到晚上或者周末。但是尽管这样,Tinker还是有很多未完成的工作(我们承诺会把这些坑填上,也欢迎大家一起来PR),比如: 190 | 191 | 1. 四大组件代理 192 | 2. 启动保护 193 | 194 | 开源的本意就是希望和大家一起进步而不是闭门造车,衷心希望有更多的开发者可以与我们共同努力,可以让国内的开源环境变的更好。 195 | 196 | ## 致谢 ## 197 | 有很多的用户对Tinker做出了各种各样的贡献,这里都会有一份小小的礼品感谢对腾讯开源的支持。当然我希望有越来越多的人可以加入到这个队伍,反馈社区。 198 | 199 | 感谢 TinkerPatch的孙胜杰、百度的孙鹏飞、蘑菇街的往之/谢国、UC的吴志伟、58同城的赵聪颖、欧应科技的郭永平、360的刘敏、滴滴的赵旭阳、华为的穆俊含/谢小灵、Vivo的郝雄、小米的陶建涛等。 200 | 201 | ![](assets/tinker_summary/thanks2.jpg) 202 | 203 | ![](assets/tinker_summary/thanks1.jpg) 204 | 205 | ## 参考资料 ## 206 | 关于更多Tinker的实现原理与技术细节,可以参考如下文章: 207 | 208 | 1. [微信Android热补丁实践演进之路](https://github.com/WeMobileDev/article/blob/master/%E5%BE%AE%E4%BF%A1Android%E7%83%AD%E8%A1%A5%E4%B8%81%E5%AE%9E%E8%B7%B5%E6%BC%94%E8%BF%9B%E4%B9%8B%E8%B7%AF.md) 209 | 2. [Android N混合编译与对热补丁影响解析](https://github.com/WeMobileDev/article/blob/master/Android_N%E6%B7%B7%E5%90%88%E7%BC%96%E8%AF%91%E4%B8%8E%E5%AF%B9%E7%83%AD%E8%A1%A5%E4%B8%81%E5%BD%B1%E5%93%8D%E8%A7%A3%E6%9E%90.md) 210 | 3. [Dev Club 微信热补丁Tinker分享](http://dev.qq.com/topic/57ad7a70eaed47bb2699e68e) 211 | 4. [微信Tinker的一切都在这里,包括源码(一)](http://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286384&idx=1&sn=f1aff31d6a567674759be476bcd12549&scene=4#wechat_redirect) 212 | 5. [Tinker Dexdiff算法解析](https://www.zybuluo.com/dodola/note/554061) 213 | 6. [ART下的方法内联策略及其对Android热修复方案的影响分析](http://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286426&idx=1&sn=eb75349c0c3663f10fbdd74ef87be338&chksm=8334c398b4434a8e6933ddb4fda4a4f06c729c7d2ffef37e4598cb90f4602f5310486b7f95ff#rd) 214 | 7. [Tinker MDCC会议 slide](https://github.com/WeMobileDev/article/blob/master/final-%E5%BE%AE%E4%BF%A1%E7%83%AD%E8%A1%A5%E4%B8%81%E5%AE%9E%E8%B7%B5%E6%BC%94%E8%BF%9B%E4%B9%8B%E8%B7%AF-v2016-9-24.pdf) 215 | 8. [DexDiff格式查看工具](https://github.com/LaurenceYang/tinker-dex-dump) 216 | 217 | ![](assets/tinker_summary/shwenzhang.jpg) -------------------------------------------------------------------------------- /assets/android_video_record/encodeProcess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/android_video_record/encodeProcess.png -------------------------------------------------------------------------------- /assets/android_video_record/frame_compress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/android_video_record/frame_compress.png -------------------------------------------------------------------------------- /assets/android_video_record/mediacodec_buffers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/android_video_record/mediacodec_buffers.png -------------------------------------------------------------------------------- /assets/ios_sql/SQLite-Arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/ios_sql/SQLite-Arch.png -------------------------------------------------------------------------------- /assets/ios_sql/code-default-busy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/ios_sql/code-default-busy.png -------------------------------------------------------------------------------- /assets/ios_sql/code-malloc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/ios_sql/code-malloc.png -------------------------------------------------------------------------------- /assets/ios_sql/lag-rw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/ios_sql/lag-rw.png -------------------------------------------------------------------------------- /assets/ios_sql/lag-wait-lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/ios_sql/lag-wait-lock.png -------------------------------------------------------------------------------- /assets/ios_sql/new-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/ios_sql/new-schema.png -------------------------------------------------------------------------------- /assets/ios_sql/old-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/ios_sql/old-schema.png -------------------------------------------------------------------------------- /assets/ios_sql/sqlite-ios-mmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/ios_sql/sqlite-ios-mmap.png -------------------------------------------------------------------------------- /assets/ios_sql/timeline-busy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/ios_sql/timeline-busy.png -------------------------------------------------------------------------------- /assets/ios_sql/trend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/ios_sql/trend.png -------------------------------------------------------------------------------- /assets/mars/mars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mars/mars.png -------------------------------------------------------------------------------- /assets/mars/tcpdump_client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mars/tcpdump_client.png -------------------------------------------------------------------------------- /assets/mars/tcpdump_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mars/tcpdump_server.png -------------------------------------------------------------------------------- /assets/migrate_to_wcdb/baseline_batch_write.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/migrate_to_wcdb/baseline_batch_write.png -------------------------------------------------------------------------------- /assets/migrate_to_wcdb/baseline_read.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/migrate_to_wcdb/baseline_read.png -------------------------------------------------------------------------------- /assets/migrate_to_wcdb/baseline_write.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/migrate_to_wcdb/baseline_write.png -------------------------------------------------------------------------------- /assets/migrate_to_wcdb/initialization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/migrate_to_wcdb/initialization.png -------------------------------------------------------------------------------- /assets/migrate_to_wcdb/multithread_read_read.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/migrate_to_wcdb/multithread_read_read.png -------------------------------------------------------------------------------- /assets/migrate_to_wcdb/multithread_read_write.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/migrate_to_wcdb/multithread_read_write.png -------------------------------------------------------------------------------- /assets/migrate_to_wcdb/multithread_write_write.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/migrate_to_wcdb/multithread_write_write.png -------------------------------------------------------------------------------- /assets/mmtls_image/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mmtls_image/1.jpg -------------------------------------------------------------------------------- /assets/mmtls_image/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mmtls_image/10.jpg -------------------------------------------------------------------------------- /assets/mmtls_image/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mmtls_image/11.jpg -------------------------------------------------------------------------------- /assets/mmtls_image/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mmtls_image/2.jpg -------------------------------------------------------------------------------- /assets/mmtls_image/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mmtls_image/3.jpg -------------------------------------------------------------------------------- /assets/mmtls_image/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mmtls_image/4.jpg -------------------------------------------------------------------------------- /assets/mmtls_image/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mmtls_image/5.jpg -------------------------------------------------------------------------------- /assets/mmtls_image/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mmtls_image/6.jpg -------------------------------------------------------------------------------- /assets/mmtls_image/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mmtls_image/7.jpg -------------------------------------------------------------------------------- /assets/mmtls_image/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mmtls_image/8.jpg -------------------------------------------------------------------------------- /assets/mmtls_image/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mmtls_image/9.jpg -------------------------------------------------------------------------------- /assets/mmtls_image/se.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/mmtls_image/se.png -------------------------------------------------------------------------------- /assets/nano_free/LSEnvironment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/nano_free/LSEnvironment.png -------------------------------------------------------------------------------- /assets/nano_free/assembly.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/nano_free/assembly.jpg -------------------------------------------------------------------------------- /assets/nano_free/crash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/nano_free/crash.jpg -------------------------------------------------------------------------------- /assets/nano_free/crash_os.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/nano_free/crash_os.png -------------------------------------------------------------------------------- /assets/nano_free/malloc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/nano_free/malloc.png -------------------------------------------------------------------------------- /assets/nano_free/malloc_zone_t.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/nano_free/malloc_zone_t.jpg -------------------------------------------------------------------------------- /assets/nano_free/nano_crash_guard_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/nano_free/nano_crash_guard_1.jpg -------------------------------------------------------------------------------- /assets/nano_free/nano_crash_guard_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/nano_free/nano_crash_guard_2.jpg -------------------------------------------------------------------------------- /assets/nano_free/otool.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/nano_free/otool.jpg -------------------------------------------------------------------------------- /assets/nano_free/scalable0x17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/nano_free/scalable0x17.jpg -------------------------------------------------------------------------------- /assets/nano_free/tricky_fall_through.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/nano_free/tricky_fall_through.jpg -------------------------------------------------------------------------------- /assets/nano_free/xcode_schema.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/nano_free/xcode_schema.jpg -------------------------------------------------------------------------------- /assets/qrcode_for_wemobiledev.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/qrcode_for_wemobiledev.jpg -------------------------------------------------------------------------------- /assets/soter/SOTER交流群群二维码.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/soter/SOTER交流群群二维码.png -------------------------------------------------------------------------------- /assets/soter/SoterFramework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/soter/SoterFramework.png -------------------------------------------------------------------------------- /assets/soter/all_sequence.txt: -------------------------------------------------------------------------------- 1 | title: Check support SOTER 2 | App Client->App Server: device_model_info 3 | App Server->Wechat\nOpen\nPlatform: device_model_info \n& access_token 4 | Wechat\nOpen\nPlatform->WeChat Server: device_model_info 5 | Note over WeChat Server: check support 6 | WeChat Server-->Wechat\nOpen\nPlatform: support or not 7 | Wechat\nOpen\nPlatform-->App Server: support or not 8 | App Server-->App Client: support or not 9 | 10 | title: Upload ASK public key 11 | App Client->App Server: ASKModelJSON \n&JSONSignature 12 | App Server->Wechat\nOpen\nPlatform: ASKModelJSON \n&JSONSignature\n&access_token 13 | Wechat\nOpen\nPlatform->TAM server: ASKModelJSON \n&JSONSignature 14 | Note over TAM server: verify(ASKModelJSON, \nJSONSignature, \nATTK_pub[device]) 15 | TAM server-->Wechat\nOpen\nPlatform: verify result 16 | Wechat\nOpen\nPlatform-->App Server: verify result 17 | Note over App Server: save ASK public key\nof the device 18 | App Server-->App Client: verify result 19 | 20 | title: Upload Auth Key public key 21 | App Client->App Server: AuthKeyModelJSON \n&JSONSignature 22 | Note over App Server: verify(AuthKeyModelJSON, \nJSONSignature, \nASK_pub[device]) 23 | Note over App Server: save authkey_pub \nif verified 24 | App Server-->App Client: verify result 25 | 26 | title: Get Challenge 27 | App Client->App Server: No parameter 28 | Note over App Server: generate challenge\nand save in memory\nfor use 29 | App Server-->App Client: challenge string 30 | 31 | title: Verify the Final Signature 32 | App Client->App Server: SignatureInfoJSON\n&signature\n&saltlength 33 | Note over App Server: verify(SignatureInfoJSON, \nsignature, authkey_pub[device][scene])\nwith given saltlength 34 | App Server-->App Client: verify result -------------------------------------------------------------------------------- /assets/soter/check_support.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/soter/check_support.png -------------------------------------------------------------------------------- /assets/soter/get_challenge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/soter/get_challenge.png -------------------------------------------------------------------------------- /assets/soter/netwrapper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/soter/netwrapper.png -------------------------------------------------------------------------------- /assets/soter/qrcode_for_gh_6410b016e824_258.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/soter/qrcode_for_gh_6410b016e824_258.jpg -------------------------------------------------------------------------------- /assets/soter/upload_ask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/soter/upload_ask.png -------------------------------------------------------------------------------- /assets/soter/upload_auth_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/soter/upload_auth_key.png -------------------------------------------------------------------------------- /assets/soter/verify_signature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/soter/verify_signature.png -------------------------------------------------------------------------------- /assets/soter/准备业务密钥.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/soter/准备业务密钥.gif -------------------------------------------------------------------------------- /assets/soter/准备应用密钥.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/soter/准备应用密钥.gif -------------------------------------------------------------------------------- /assets/soter/准备根密钥.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/soter/准备根密钥.gif -------------------------------------------------------------------------------- /assets/soter/认证或开通.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/soter/认证或开通.gif -------------------------------------------------------------------------------- /assets/tinker-open/androidn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-open/androidn.png -------------------------------------------------------------------------------- /assets/tinker-open/dex-anr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-open/dex-anr.png -------------------------------------------------------------------------------- /assets/tinker-open/dex-art.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-open/dex-art.png -------------------------------------------------------------------------------- /assets/tinker-open/dex-diff.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-open/dex-diff.jpg -------------------------------------------------------------------------------- /assets/tinker-open/dex-format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-open/dex-format.png -------------------------------------------------------------------------------- /assets/tinker-open/dex-merge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-open/dex-merge.jpg -------------------------------------------------------------------------------- /assets/tinker-open/dex-method.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-open/dex-method.jpg -------------------------------------------------------------------------------- /assets/tinker-open/dex-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-open/dex-result.png -------------------------------------------------------------------------------- /assets/tinker-open/open.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-open/open.jpg -------------------------------------------------------------------------------- /assets/tinker-open/section1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-open/section1.jpg -------------------------------------------------------------------------------- /assets/tinker-open/tinker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-open/tinker.png -------------------------------------------------------------------------------- /assets/tinker-research/method-inline/1480488144557.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-research/method-inline/1480488144557.png -------------------------------------------------------------------------------- /assets/tinker-research/method-inline/1480488768332.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-research/method-inline/1480488768332.png -------------------------------------------------------------------------------- /assets/tinker-research/method-inline/1480489428895.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-research/method-inline/1480489428895.png -------------------------------------------------------------------------------- /assets/tinker-research/method-inline/1480489905685.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-research/method-inline/1480489905685.png -------------------------------------------------------------------------------- /assets/tinker-research/method-inline/1480494090755.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-research/method-inline/1480494090755.png -------------------------------------------------------------------------------- /assets/tinker-research/method-inline/1480496299998.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-research/method-inline/1480496299998.png -------------------------------------------------------------------------------- /assets/tinker-research/method-inline/1480497454319.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-research/method-inline/1480497454319.png -------------------------------------------------------------------------------- /assets/tinker-research/method-inline/1480595723104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker-research/method-inline/1480595723104.png -------------------------------------------------------------------------------- /assets/tinker/abtest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/abtest.png -------------------------------------------------------------------------------- /assets/tinker/all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/all.png -------------------------------------------------------------------------------- /assets/tinker/alldiff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/alldiff.png -------------------------------------------------------------------------------- /assets/tinker/andfix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/andfix.png -------------------------------------------------------------------------------- /assets/tinker/andfixend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/andfixend.png -------------------------------------------------------------------------------- /assets/tinker/data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/data.png -------------------------------------------------------------------------------- /assets/tinker/qzone-art.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/qzone-art.png -------------------------------------------------------------------------------- /assets/tinker/qzone-dalvik-end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/qzone-dalvik-end.png -------------------------------------------------------------------------------- /assets/tinker/qzone-dalvik.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/qzone-dalvik.png -------------------------------------------------------------------------------- /assets/tinker/tinker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/tinker.png -------------------------------------------------------------------------------- /assets/tinker/use.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/use.png -------------------------------------------------------------------------------- /assets/tinker/userlog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/userlog.png -------------------------------------------------------------------------------- /assets/tinker/wechat-dexdiff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/wechat-dexdiff.png -------------------------------------------------------------------------------- /assets/tinker/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/wechat.png -------------------------------------------------------------------------------- /assets/tinker/workmodel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/workmodel.png -------------------------------------------------------------------------------- /assets/tinker/workmodel2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker/workmodel2.png -------------------------------------------------------------------------------- /assets/tinker_summary/android_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker_summary/android_n.png -------------------------------------------------------------------------------- /assets/tinker_summary/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker_summary/github.png -------------------------------------------------------------------------------- /assets/tinker_summary/huawei_fenshen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker_summary/huawei_fenshen.jpg -------------------------------------------------------------------------------- /assets/tinker_summary/inline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker_summary/inline.png -------------------------------------------------------------------------------- /assets/tinker_summary/miui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker_summary/miui.png -------------------------------------------------------------------------------- /assets/tinker_summary/qzone-art.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker_summary/qzone-art.png -------------------------------------------------------------------------------- /assets/tinker_summary/shwenzhang.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker_summary/shwenzhang.jpg -------------------------------------------------------------------------------- /assets/tinker_summary/thanks1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker_summary/thanks1.jpg -------------------------------------------------------------------------------- /assets/tinker_summary/thanks2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/tinker_summary/thanks2.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/as_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/as_1.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/as_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/as_2.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/chaincall_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/chaincall_1.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/chaincall_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/chaincall_2.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/coding_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/coding_1.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/coding_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/coding_2.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/coding_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/coding_3.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/coding_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/coding_4.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/coding_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/coding_5.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/crud_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/crud_1.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/crud_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/crud_2.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/crud_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/crud_3.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/crud_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/crud_4.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/multiselect_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/multiselect_1.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/orm_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/orm_1.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/orm_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/orm_2.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/orm_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/orm_3.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/transaction_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/transaction_1.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/transaction_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/transaction_2.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/winq_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/winq_1.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/winq_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/winq_2.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/winq_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/winq_3.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/winq_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/winq_4.jpg -------------------------------------------------------------------------------- /assets/wcdb_ios_1/winq_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_ios_1/winq_5.jpg -------------------------------------------------------------------------------- /assets/wcdb_repair/backup-compare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_repair/backup-compare.png -------------------------------------------------------------------------------- /assets/wcdb_repair/backup-optimization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_repair/backup-optimization.png -------------------------------------------------------------------------------- /assets/wcdb_repair/dump-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_repair/dump-example.png -------------------------------------------------------------------------------- /assets/wcdb_repair/repair-united.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_repair/repair-united.png -------------------------------------------------------------------------------- /assets/wcdb_repair/sqlite-arch-core.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_repair/sqlite-arch-core.png -------------------------------------------------------------------------------- /assets/wcdb_repair/sqlite_master-struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/wcdb_repair/sqlite_master-struct.png -------------------------------------------------------------------------------- /assets/winq/error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/winq/error.jpg -------------------------------------------------------------------------------- /assets/winq/expr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/winq/expr.jpg -------------------------------------------------------------------------------- /assets/winq/hint.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/winq/hint.jpg -------------------------------------------------------------------------------- /assets/winq/select.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/assets/winq/select.jpg -------------------------------------------------------------------------------- /final-微信热补丁实践演进之路-v2016-9-24.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/final-微信热补丁实践演进之路-v2016-9-24.pdf -------------------------------------------------------------------------------- /为什么要从FMDB迁移到WCDB.md: -------------------------------------------------------------------------------- 1 | # 背景 2 | 3 | WCDB开源至今已两个月有余,我们在不断迭代功能、完善文档的同时,也与来自世界各地的开发者进行交流,帮助他们更快地了解、掌握WCDB。这其中,也不乏使用FMDB的开发者。他们正准备将项目的数据库模块改为WCDB。 4 | 5 | 对于一个已经上线运行的项目,数据库这类基础组件与业务的耦合通常较多,迁移有一定工作量的。因此,开发者通常会做很多预研,以确定是否进行迁移。 6 | 7 | WCDB在Github的wiki上提供了专门的教程,帮助使用FMDB的开发者进行迁移。同时,也希望通过本文全面地介绍WCDB和FMDB在使用方式、性能等方面的差异,以及迁移中可能遇到的问题,帮助开发者决定是否进行迁移。 8 | 9 | # 平滑迁移 10 | 11 | #### 文件格式 12 | 13 | 由于FMDB和WCDB都基于SQLite,因此两者在数据库的文件格式上一致。用FMDB创建、操作的数据库,可以直接通过WCDB打开、使用。因此开发者无需做额外的数据迁移。 14 | 15 | #### 表结构 16 | 17 | WCDB提供了ORM的功能,将类的属性绑定到数据库表的字段。在日常实践中,类的属性名和表的字段名通常不一致。因此,WCDB提供了`WCDB_SYNTHESIZE_COLUMN(className, propertyName, columnName)`宏,用于映射属性名。 18 | 19 | 对于 20 | 21 | - 表:`CREATE TABLE message (db_id INTEGER, db_content TEXT)` 22 | 23 | - 类: 24 | 25 | ```objective-c 26 | //Message.h 27 | @interface Message : NSObject 28 | 29 | @property int localID; 30 | @property(retain) NSString *content; 31 | 32 | @end 33 | 34 | //Message.mm 35 | @implementation Message 36 | 37 | @end 38 | ``` 39 | 40 | 这里表字段都加了"db_"的前缀,并且使用了不一样的字段名。通过WCDB的ORM,可以映射为 41 | 42 | ```objective-c 43 | //Message.h 44 | @interface Message : NSObject 45 | 46 | @property int localID; 47 | @property(retain) NSString *content; 48 | 49 | WCDB_PROPERTY(localID) 50 | WCDB_PROPERTY(content) 51 | 52 | @end 53 | 54 | //Message.mm 55 | @implementation Message 56 | 57 | WCDB_IMPLEMENTATION(Message) 58 | WCDB_SYNTHESIZE_COLUMN(Message, localID, "db_id") 59 | WCDB_SYNTHESIZE_COLUMN(Message, content, "db_content") 60 | 61 | @end 62 | ``` 63 | 64 | 通过`WCDB_SYNTHESIZE_COLUMN`宏映射后,WCDB同样能兼容FMDB的表结构,开发者也不需要做数据迁移。 65 | 66 | 因此,开发者可以平滑地从FMDB迁移到WCDB。 67 | 68 | # 性能比较 69 | 70 | 对于已经上线运行的项目,解决性能瓶颈会是一个常见的迁移理由。相较于FMDB直白的封装,WCDB上到OC层的ORM,下到SQLite源码,都做了各类性能优化。 71 | 72 | 为了验证优化效果,我们提供了benchmark,并将性能测试结果和测试代码上传到了Github。同时,benchmark中也加入了FMDB的测试代码,用于横向比较。 73 | 74 | 以下性能测试均为WAL模式、缓存大小2000字节、页大小4 kb: 75 | 76 | - `PRAGMA cache_size=-2000` 77 | - `PRAGMA page_size=4096` 78 | - `PRAGMA journal_mode=WAL` 79 | 80 | 测试数据均为含有一个整型和一个二进制数据的表:`CREATE TABLE benchmark(key INTEGER, value BLOB)`,二进制数据长度为100字节。 81 | 82 | #### 读操作性能测试 83 | 84 | ![](assets/migrate_to_wcdb/baseline_read.png) 85 | 86 | #### 写操作性能测试 87 | 88 | ![](assets/migrate_to_wcdb/baseline_write.png) 89 | 90 | #### 批量写操作性能测试 (事务) 91 | 92 | ![](assets/migrate_to_wcdb/baseline_batch_write.png) 93 | 94 | 对于读操作,SQLite速度很快,因此封装层的消耗占比较多。FMDB只做了最简单的封装, 而WCDB还包括ORM、WINQ等操作,因此执行的指令会比FMDB多,从而导致性能**劣于FMDB 5%**。 95 | 96 | 而写操作通常是性能的瓶颈,WCDB对其做了许多针对性的优化,使得写操作和批量写操作的性能分别**优于FMDB 28% 和 180%**。 97 | 98 | #### 多线程读并发性能测试 99 | 100 | ![](assets/migrate_to_wcdb/multithread_read_read.png) 101 | 102 | #### 多线程读写并发性能测试 103 | 104 | ![](assets/migrate_to_wcdb/multithread_read_write.png) 105 | 106 | #### 多线程写并发性能测试 107 | 108 | ![](assets/migrate_to_wcdb/multithread_write_write.png) 109 | 110 | 在多线程读操作的测试中,WCDB多线程并发的优势,将读操作的性能劣势拉了回来,使得最终结果与FMDB**基本持平**,而多线程读写操作性能则优于FMDB **62%** 。 111 | 112 | 在多线程写操作的测试中,FMDB直接返回错误`SQLITE_BUSY`,无法完成。 113 | 114 | #### 初始化性能测试 115 | 116 | ![](assets/migrate_to_wcdb/initialization.png) 117 | 118 | SQLite连接的初始化速度会随着数据库内表的数量增加而逐渐上升,WCDB也针对这个场景做了优化。相较于没有优化的FMDB,WCDB 有**107%** 的性能优势。 119 | 120 | # 易用性比较 121 | 122 | 与已经上线运行项目不同,新项目更关注开发的效率。此时数据库的易用和便捷更重要。 123 | 124 | 对于等价的功能,WCDB所需的代码量往往会比FMDB少很多。而更少的代码量通常意味着更快的开发效率和更少的错误。 125 | 126 | ## 基础操作 127 | 128 | ORM是现代客户端数据库比较普遍的功能。CoreData、Realm都支持ORM,WCDB也不例外。 129 | 130 | FMDB因其直白的封装,没有提供该功能。但在设计数据库表时,开发者通常会对数据进行建模。因此开发者只需将已有建模用WCDB的ORM表达出来即可。 131 | 132 | 对于在FMDB的一组定义: 133 | 134 | * 表:`CREATE TABLE message (localID INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, createTime INTEGER, modfiedTime INTEGER)` 135 | * 索引:`CREATE INDEX message_index ON message(createTime)` 136 | * 类: 137 | 138 | ```objective-c 139 | //Message.h 140 | @interface Message : NSObject 141 | 142 | @property int localID; 143 | @property(retain) NSString *content; 144 | @property(retain) NSDate *createTime; 145 | @property(retain) NSDate *modifiedTime; 146 | 147 | @end 148 | //Message.mm 149 | @implementation Message 150 | 151 | @end 152 | ``` 153 | 154 | WCDB需要对其建模,可以定义为 155 | 156 | ```objective-c 157 | //Message.h 158 | @interface Message : NSObject 159 | 160 | @property int localID; 161 | @property(retain) NSString *content; 162 | @property(retain) NSDate *createTime; 163 | @property(retain) NSDate *modifiedTime; 164 | 165 | WCDB_PROPERTY(localID) 166 | WCDB_PROPERTY(content) 167 | WCDB_PROPERTY(createTime) 168 | WCDB_PROPERTY(modifiedTime) 169 | 170 | @end 171 | 172 | //Message.mm 173 | @implementation Message 174 | 175 | WCDB_IMPLEMENTATION(Message) 176 | WCDB_SYNTHESIZE(Message, localID) 177 | WCDB_SYNTHESIZE(Message, content) 178 | WCDB_SYNTHESIZE(Message, createTime) 179 | WCDB_SYNTHESIZE_COLUMN(Message, modifiedTime, "db_modifiedTime") 180 | 181 | WCDB_PRIMARY_AUTO_INCREMENT(Message, localID) 182 | WCDB_INDEX(Message, "_index", createTime) 183 | 184 | @end 185 | ``` 186 | 187 | 其中: 188 | 189 | * `WCDB_IMPLEMENTATION(className)`用于定义进行绑定的类 190 | * `WCDB_PROPERTY(propertyName)`和`WCDB_SYNTHESIZE(className, propertyName)`用于声明和定义字段。 191 | * `WCDB_PRIMARY_AUTO_INCREMENT(className, propertyName)`用于定义主键且自增。 192 | * `WCDB_INDEX(className, indexNameSubfix, propertyName)`用于定义索引。 193 | 194 | 虽然WCDB多了一步ORM的操作,但这是一劳永逸的,并且会给我们后续的使用带来很大的便利。 195 | 196 | 经过ORM的类,大部分操作都只需要一行代码即可完成。Talk is cheap,直接看代码对比: 197 | 198 | ### 查询操作 199 | 200 | ```objective-c 201 | /* 202 | FMDB Code 203 | */ 204 | FMResultSet *resultSet = [fmdb executeQuery:@"SELECT * FROM message"]; 205 | NSMutableArray *messages = [[NSMutableArray alloc] init]; 206 | while ([resultSet next]) { 207 | Message *message = [[Message alloc] init]; 208 | message.localID = [resultSet intForColumnIndex:0]; 209 | message.content = [resultSet stringForColumnIndex:1]; 210 | message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]]; 211 | message.modifiedTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:3]]; 212 | [messages addObject:message]; 213 | } 214 | ``` 215 | 216 | ```objective-c 217 | /* 218 | WCDB Code 219 | */ 220 | NSArray *messages = [wcdb getAllObjectsOfClass:Message.class fromTable:@"message"]; 221 | ``` 222 | 223 | ### 插入操作 224 | 225 | ```objective-c 226 | /* 227 | FMDB Code 228 | */ 229 | [fmdb executeUpdate:@"INSERT INTO message VALUES(?, ?, ?, ?)", @(message.localID), message.content, @(message.createTime.timeIntervalSince1970), @(message.modifiedTime.timeIntervalSince1970)]; 230 | ``` 231 | 232 | ```objective-c 233 | /* 234 | WCDB Code 235 | */ 236 | [wcdb insertObject:message into:@"message"]; 237 | ``` 238 | 239 | 可以看到, 240 | 241 | * 对于查询操作,FMDB需要进行很多拼装组合,而WCDB只需要一行代码就能完成。 242 | * 对于插入操作,FMDB也只用了一行代码,但其需要将property逐个拆分为最基本的类型。而WCDB所需要关注的只有object和表名两个参数。 243 | 244 | ## 数据库升级 245 | 246 | SQLite的数据库升级一直是一个比较繁杂的问题。 247 | 248 | 通常的做法是,开发者自行定义一个版本号,并保存下来。数据库创建时每次检查版本号,若版本号较低,则对其字段进行升级,并更新版本号。但在多个版本的增增减减之后,版本的处理逻辑会越来越复杂,甚至可能弄错表内哪些字段是新增的,哪些是废弃的。 249 | 250 | WCDB将数据库升级和ORM结合起来,对于需要增删改的字段,只需直接在ORM层面修改,并再次调用`createTableAndIndexesOfName:withClass:`接口即可自动升级。以下是一个数据库升级的例子。 251 | 252 | ```objective-c 253 | //Message.h 254 | @interface Message : NSObject 255 | 256 | @property int localID; 257 | @property(assign) const char *content;//NSString *content; 258 | //@property(retain) NSDate *createTime; 259 | @property(retain) NSDate *aNewModifiedTime; 260 | @property(retain) NSDate *aNewProperty; 261 | 262 | WCDB_PROPERTY(localID) 263 | WCDB_PROPERTY(content) 264 | //WCDB_PROPERTY(createTime) 265 | WCDB_PROPERTY(modifiedTime) 266 | WCDB_PROPERTY(newProperty) 267 | 268 | @end 269 | 270 | //Message.mm 271 | @implementation Message 272 | 273 | WCDB_IMPLEMENTATION(Message) 274 | WCDB_SYNTHESIZE(Message, localID) 275 | WCDB_SYNTHESIZE(Message, content) 276 | //WCDB_SYNTHESIZE(Message, createTime) 277 | WCDB_SYNTHESIZE_COLUMN(Message, aNewModifiedTime, "modifiedTime") 278 | WCDB_SYNTHESIZE(Message, aNewProperty) 279 | 280 | WCDB_PRIMARY_AUTO_INCREMENT(Message, localID) 281 | //WCDB_INDEX(Message, "_index", createTime) 282 | WCDB_UNIQUE(Message, aNewModifiedTime) 283 | WCDB_INDEX(Message, "_newIndex", aNewProperty) 284 | 285 | @end 286 | ``` 287 | 288 | ```objective-c 289 | WCTDatabase* db = [[WCTDatabase alloc] initWithPath:path]; 290 | [db createTableAndIndexesOfName:@"message" withClass:Message.class]; 291 | ``` 292 | 293 | #### 删除字段 294 | 295 | 如例子中的`createTime`字段,删除字段只需直接将ORM中的定义删除即可。 296 | 297 | #### 增加字段 298 | 299 | 如例子中的`aNewProperty`字段,增加字段只需直接添加ORM的定义即可。 300 | 301 | #### 修改字段类型 302 | 303 | 如例子中的`content`字段,字段类型可以直接修改,但需要确保新类型与旧类型兼容; 304 | 305 | #### 修改字段名称 306 | 307 | 如例子中的`aNewModifiedTime`,字段名称可以通过`WCDB_SYNTHESIZE_COLUMN(className, propertyName, columnName)`重新映射。 308 | 309 | #### 增加约束 310 | 311 | 如例子中的`WCDB_UNIQUE(Message, aNewModifiedTime)`,新的约束只需直接在ORM中添加即可。 312 | 313 | #### 增加索引 314 | 315 | 如例子中的`WCDB_INDEX(Message, "_newIndex", aNewProperty)`,新的索引只需直接在ORM添加即可。 316 | 317 | ## 多线程操作 318 | 319 | WCDB与FMDB都支持多线程操作。 320 | 321 | 在FMDB内,当开发者需要进行多线程操作时,需要使用另外一个类`FMDatabasePool`来进行操作。 322 | 323 | 而WCDB基础的CRUD接口都支持多线程,因此开发者不需要额外关心线程安全的问题。同样的,WCDB多线程使用的代码量也比FMDB少得多。 324 | 325 | ```objective-c 326 | /* 327 | FMDB Code 328 | */ 329 | //thread-1 read 330 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ 331 | [fmdbPool inDatabase:^(FMDatabase *_Nonnull db) { 332 | NSMutableArray *messages = [[NSMutableArray alloc] init]; 333 | FMResultSet *resultSet = [db executeQuery:@"SELECT * FROM message"]; 334 | while ([resultSet next]) { 335 | Message *message = [[Message alloc] init]; 336 | message.localID = [resultSet intForColumnIndex:0]; 337 | message.content = [resultSet stringForColumnIndex:1]; 338 | message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]]; 339 | message.modifiedTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:3]]; 340 | [messages addObject:message]; 341 | } 342 | //... 343 | }]; 344 | }); 345 | //thread-2 write 346 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ 347 | [fmdbPool inDatabase:^(FMDatabase *_Nonnull db) { 348 | [db beginTransaction] 349 | for (Message *message in messages) { 350 | [db executeUpdate:@"INSERT INTO message VALUES(?, ?, ?, ?)", @(message.localID), message.content, @(message.createTime.timeIntervalSince1970), @(message.modifiedTime.timeIntervalSince1970)]; 351 | } 352 | if (![db commit]) { 353 | [db rollback]; 354 | } 355 | }]; 356 | }); 357 | ``` 358 | 359 | ```objective-c 360 | /* 361 | WCDB Code 362 | */ 363 | //thread-1 read 364 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ 365 | NSArray *messages = [wcdb getAllObjectsOfClass:Message.class fromTable:@"message"]; 366 | //... 367 | }); 368 | //thread-2 write 369 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ 370 | [wcdb insertObjects:messages into:@"message"]; 371 | }); 372 | ``` 373 | 374 | # 功能完整性比较 375 | 376 | #### 加密 377 | 378 | WCDB基于SQLCipher提供了加密功能 379 | 380 | ```objective-c 381 | [database setCipherKey:password]; 382 | ``` 383 | 384 | #### 统计 385 | 386 | WCDB内提供统计的接口注册获取数据库操作的SQL、性能、错误等,开发者可以将这些信息打印到日志或上报到后台,以调试或统计 387 | 388 | ```objective-c 389 | //Error Monitor 390 | [WCTStatistics SetGlobalErrorReport:^(WCTError *error) { 391 | NSLog(@"[WCDB]%@", error); 392 | }]; 393 | 394 | //Performance Monitor 395 | [WCTStatistics SetGlobalPerformanceTrace:^(WCTTag tag, NSDictionary *sqls, NSInteger cost) { 396 | NSLog(@"Database with tag:%d", tag); 397 | NSLog(@"Run :"); 398 | [sqls enumerateKeysAndObjectsUsingBlock:^(NSString *sqls, NSNumber *count, BOOL *) { 399 | NSLog(@"SQL %@ %@ times", sqls, count); 400 | }]; 401 | NSLog(@"Total cost %ld nanoseconds", (long)cost); 402 | }]; 403 | 404 | //SQL Execution Monitor 405 | [WCTStatistics SetGlobalSQLTrace:^(NSString *sql) { 406 | NSLog(@"SQL: %@", sql); 407 | }]; 408 | ``` 409 | 410 | #### 修复 411 | 412 | WCDB提供了数据库修复工具,以应对数据库损坏无法使用的极端情况。 413 | 414 | ```objective-c 415 | WCTDatabase *recover = [[WCTDatabase alloc] initWithPath:recoverPath]; 416 | [database close:^{ 417 | [recover recoverFromPath:path withPageSize:pageSize backupCipher:backupCipher databaseCipher:databaseCipher]; 418 | }]; 419 | ``` 420 | 421 | # 总结 422 | 423 | 与FMDB对比,WCDB使得开发者可以写更少的代码,但能获得更高的性能。开发者不需要额外关注数据库升级和多线程操作的问题。同时,WCDB还提供了加密、统计、修复等功能。 424 | 425 | 因此,对于新项目,我们推荐使用WCDB,以获得更好的性能和开发效率。对于已经上线、稳定运行的项目,如果遇到性能瓶颈,或者对加密、统计、修复等功能有需求,我们建议参考Github上的文档进行迁移。 426 | 427 | 后续我们还将加入更多的功能,欢迎来Github关注我们。 -------------------------------------------------------------------------------- /基于TLS1.3的微信安全通信协议mmtls介绍.md: -------------------------------------------------------------------------------- 1 | #一、背景 # 2 | 3 |   随着近些年网络安全事情的频繁发生,使得用户对网络通信安全的意识越来越强。国内外的网络服务提供商都逐渐提供全站的安全通信服务,如国内的淘宝、百度先后宣布已经完成了全站部署https。微信现有的安全通信协议是基于用户登录的时候派发的SessionKey对应用数据进行加密的,该协议在工程实现上,已经过多次迭代优化,但是仍然有一些缺点: 4 | 5 | 1. 原有的加密通信协议是存在于业务层的。加密保护的是请求包包体部分,但是包头部分是明文,包头包含用户id和请求的业务id等信息,这主要是为了在proxy做路由所需要的。这样会存在数据被截获后建立映射关联分析的风险。  6 | 2. 原有的加密通信协议使用的密码学协议和算法与业界最新成果有差距,安全强度有待加强。  7 |   8 |   鉴于上述原因,微信需要一套能够加密保护Client到Server之间所有网络通信数据、且加密通信保护必须对业务开发人员透明的安全通信协议,由此诞生了mmtls。 9 | 10 | #二、目标 # 11 | 12 |   考虑到系统安全性与可用性和性能等指标之间可能存在相互影响,某种程度上,安全性与这些指标是负相关的。因此在设计的时候对mmtls提出了以下要求:  13 | 14 | 1. 安全性。主要体现在防窃听,防篡改,防重放,防伪造。  15 | 2. 低延迟、低资源消耗。要保证数据在传输过程中不会增加明显的延迟;后台负载增加在可接受范围。   16 | 3. 可用性。在一些极端情况下(如server负载过高),后台能够控制提供降级服务,但是要注意不能被攻击者利用,进行降级攻击。 17 | 4. 可扩展性。协议要可扩展、可升级,方便添加安全强度更高的密码学组件,方便禁止过时的密码学组件。 18 | 19 |   通过分析一些业界公开的安全通信协议发现,它们都不能完全满足我们的要求,例如TLS1.2中每次建立一个安全连接都需要额外的2~1个RTT(全握手需要2-RTT),对于微信这么一个需要频繁网络通信的IM软件来说,这对用户体验的伤害是极大的,尤其是在大量短连接存在的情况下,额外的RTT对用户延迟的影响是相当明显的。好在TLS1.3草案标准中提出了0-RTT(不额外增加网络延时)建立安全连接的方法,另外TLS协议本身通过版本号、CipherSuite、Extension机制提供了良好的可扩展性。但是,TLS1.3草案标准仍然在制定过程中,基于标准的实现更是遥遥无期,并且TLS1.3是一个对所有软件制定的一个通用协议,如果结合微信自己的特点,还有很大的优化空间。因此我们最终选择基于TLS1.3草案标准,设计实现我们自己的安全通信协议mmtls。 20 | 21 | 22 | #三、mmtls协议设计 # 23 | ##3.1 总体架构 ## 24 | ![](assets/mmtls_image/1.jpg) 25 | 26 |   业务层数据加上mmtls之后,由mmtls提供安全性,保护业务数据,这类似于http加上tls后,变成https,由tls保护http数据。mmtls处于业务层和原有的网络连接层之间,不影响原有的网络策略。对于微信来说这里的网络连接层就是微信长连接(私有协议)和微信短连接(http协议),都是基于TCP的。 27 | 28 |   图1描述的是把mmtls看成一个整体,它所处的位置。进入mmtls内部,它包含三个子协议:Record协议、Handshake协议、Alert协议。这其实是和TLS类似的。它们之间的关系如下图: 29 | ![](assets/mmtls_image/2.jpg) 30 | 31 |   Handshake和Alert协议是Record协议的上层协议,业务层协议(Application Protocol)也是record协议的上层协议,在Record协议包中有一个字段,用来区分当前这个Record数据包的上层协议是Handshake、Alert还是业务协议数据包。Handshake协议用于完成Client与Server的握手协商,协商出一个对称加密密钥Key以及其他密码材料,用于后续数据加密。Alert协议用于通知对端发生错误,希望对端关闭连接,目前mmtls为了避免server存在过多TCP Time-Wait状态,Alert消息只会server发送给client,由client主动关闭连接。 32 | 33 |   说明:在下文中,会出现多个对称密钥和多个非对称密钥对,在本文中我会给有些密钥取一个专有的名字,以方便理解避免混淆,如:`pre_master_key,pre_shared_key,cli_pub_key,cli_pri_key` 等,凡是类似`xxx_pub_key、xxx_pri_key`这种名字的都是一对非对称公私钥,`sign_key`和`verify_key`是一对签名/验签的密钥,其他的以key结尾的`xxx_key`都是对称密钥。 34 | 35 | ##3.2 Handshake协议 --- 安全地协商出对称加密密钥 ## 36 |   Handshake协议其实做的最主要的事情就是完成加密密钥的协商,即让通信双方安全地获得一致的对称密钥,以进行加密数据传输。在此基础上,还完成了一些优化工作,如复用session以减少握手时间。 37 | 38 |   在这里说明一下,为什么mmtls以及TLS协议需要一个Handshake子协议和Record子协议?其实“认证密钥协商+对称加密传输”这种混合加密结构,是绝大多数加密通信协议的通用结构,在mmtls/TLS中Handshake子协议负责密钥协商, Record子协议负责数据对称加密传输。造成这种混合加密结构的本质原因还是因为单独使用公钥加密组件或对称加密组件都有不可避免的缺点。公钥加密组件计算效率往往远低于对称加密组件,直接使用公钥加密组件加密业务数据,这样的性能损耗任何Server都是无法承受的;而如果单独使用对称加密组件进行网络加密通信,在Internet这种不安全的信道下,这个对称加密密钥如何获取往往是一个难以解决的问题,因此结合两类密码组件的优势,产生了“认证密钥协商+对称加密传输”这种混合加密结构。另外,mmtls/TLS这种安全性和扩展性都很强的安全通信协议,在解决实际安全通信问题的时候,会有非常多的细节问题,因此分离出两个子协议来隔离复杂性。 39 | ###  3.2.1 带认证的密钥协商### 40 |   根据TLS1.3的描述,实际上有2种1-RTT的密钥协商方式(1-RTT ECDHE、 1-RTT PSK)和4种0-RTT的密钥协商方式(0-RTT PSK, 0-RTT ECDH, 0-RTT PSK-ECDHE, 0-RTT ECDH-ECDHE),mmtls结合微信的特点,在保证安全性和性能的前提下,只保留了三种密钥协商方式(1-RTT ECDHE, 1-RTT PSK, 0-RTT PSK),并做了一些优化,后面会详细分析如何产生这种决策的。 41 | ####  3.2.1.1 1-RTT密钥协商#### 42 | #####  1.ECDH密钥协商##### 43 |   首先看一个,会遭受到攻击的密钥协商过程。通信双方Alice和Bob使用ECDH密钥交换协议进行密钥协商,ECDH密钥交换协议拥有两个算法: 44 | 45 | 46 | - 密钥生成算法`ECDH_Generate_Key`,输出一个公钥和私钥对`(ECDH_pub_key, ECDH_pri_key)`,`ECDH_pri_key`需要秘密地保存,`ECDH_pub_key`可以公开发送给对方。 47 | - 密钥协商算法`ECDH_compute_key`,以对方的公钥和自己的私钥作为输入,计算出一个密钥Key,`ECDH_compute_key`算法使得通信双方计算出的密钥Key是一致的。 48 | 49 |   这样一来Alice和Bob仅仅通过交换自己的公钥`ECDH_pub_key`,就可以在Internet这种公开信道上共享一个相同密钥Key,然后用这个Key作为对称加密算法的密钥,进行加密通信。 50 | ![](assets/mmtls_image/3.jpg) 51 | 52 |   但是这种密钥协商算法仍然存在一个问题。当Bob将他的`Bob_ECDH_pub_key`发送给Alice时,攻击者可以截获`Bob_ECDH_pub_key`,自己运行`ECDH_Generate_Key算法`产生一个公钥/私钥对,然后把他产生的公钥发送给Alice。同理,攻击者可以截获Alice发送给Bob的`Alice_ECDH_pub_key`,再运行`ECDH_Generate_Key算法`产生一个公钥/私钥对,并把这个公钥发送给Bob。Alice和Bob仍然可以执行协议,产生一个密钥Key。但实际上,Alice产生的密钥Key实际上是和攻击者Eve协商的;Bob产生的密钥Key也是和攻击者协商Eve的。这种攻击方法被称为中间人攻击(Man In The Middle Attack)。 53 | ![](assets/mmtls_image/4.jpg) 54 | 55 |   那么,有什么解决办法中间人攻击呢?产生中间人攻击的本质原因是协商过程中的数据没有经过端点认证,通信两端不知道收到的协商数据是来自对端还是来自中间人,因此单纯的“密钥协商”是不够的,还需要“带认证的密钥协商”。对数据进行认证其实有对称和非对称的两种方式:基于消息认证码(Message Authentication Code)的对称认证和基于签名算法的非对称认证。消息认证码的认证方式需要一个私密的Key,由于此时没有一个私密的Key,因此ECDH认证密钥协商就是ECDH密钥协商加上数字签名算法。在mmtls中我们采用的数字签名算法为ECDSA。 56 | 57 |   双方密钥协商时,再分别运行签名算法对自己发出的公钥`ECDH_pub_key`进行签名。收到信息后,首先验证签名,如果签名正确,则继续进行密钥协商。注意到,由于签名算法中的公钥`ECDSA_verify_key`是一直公开的,攻击者没有办法阻止别人获取公钥,除非完全掐断发送方的通信。这样一来,中间人攻击就不存在了,因为Eve无法伪造签名。具体过程如图5所示: 58 | ![](assets/mmtls_image/5.jpg) 59 | 60 |   事实上,在实际通信过程中,只需要通信中某一方签名它的协商数据就可以保证不被中间人攻击,mmtls就是只对Server做认证,不对Client做认证,因为微信客户端发布出去后,任何人都可以获得,只要能够保证客户端程序本身的完整性,就相当于保证了客户端程序是由官方发布的,为认证合法的客户端,而客户端程序本身的完整性不是mmtls协议保护的范畴。在这一点上,TLS要复杂一些,TLS作为一个通用的安全通信协议,可能会存在一些需要对Client进行认证的场合,因此TLS提供了可选的双方相互认证的能力,通过握手协商过程中选择的CipherSuite是什么类型来决定是否要对Server进行认证,通过Server是否发送CertificateRequest握手消息来决定是否要对Client进行认证。由于mmtls不需要对Client做认证,在这块内容上比TLS简洁许多,更加轻量级。 61 | #####2.PSK密钥协商##### 62 |   PSK是在一次ECDH握手中由server下发的内容,它的大致数据结构为`PSK{key,ticket{key}}`,即PSK包含一个用来做对称加密密钥的key明文,以及用`ticket_key`对`key`进行加密的密文`ticket`,当然PSK是在安全信道中下发的,也就是说在网络中进行传输的时候PSK是加密的,中间人是拿不到`key`的。其中`ticket_key`只有server才知道,由server负责私密保存。 63 | 64 |   PSK协商比较简单,Client将PSK的`ticket{key}`部分发送给Server,由于只有Server才知道`ticket_key`,因此key是不会被窃听的。Server拿到ticket后,使用`ticket_key`解密得到key,然后Server用基于协商得到的密钥key,对协商数据计算消息认证码来认证,这样就完成了PSK认证密钥协商。PSK认证密钥协商使用的都是对称算法,性能上比ECDH认证密钥协商要好很多。 65 | ![](assets/mmtls_image/6.jpg) 66 | ####  3.2.1.2 0-RTT密钥协商#### 67 |   上述的两种认证密钥协商方式(1-RTT ECDHE, 1-RTT PSK)都需要一个额外RTT去获取对称加密key,在这个协商的RTT中是不带有业务数据的,全部都是协商数据。那么是否存在一种密钥协商方式是在握手协商的过程中就安全地将业务数据传递给对端呢?答案是有的,TLS1.3草案中提到了0-RTT密钥协商的方法。 68 | #####1. 0-RTT ECDH密钥协商##### 69 |   0-RTT 握手想要达到的目标是在握手的过程中,捎带业务数据到对端,这里难点是如何在客户端发起协商请求的时候就生成一个可信的对称密钥加密业务数据。在1-RTT ECDHE中,Client生成一对公私钥`(cli_pub_key, cli_pri_key)`,然后将公钥`cli_pub_key`传递给Server,然后Server生成一对公私钥`(svr_pub_key, svr_pri_key)`并将公钥`svr_pub_key`传递给Client,Client收到`svr_pub_key`后才能计算出对称密钥。上述过程`(svr_pub_key, svr_pri_key)`由于是临时生成的,需要一个RTT将`svr_pub_key`传递给客户端,如果我们能够预先生成一对公私钥`(static_svr_pub_key, static_svr_pri_key)`并将`static_svr_pub_key`预置在Client中,那么Client可以在发起握手前就通过`static_svr_pub_ke`和`cli_pub_key`生成一个对称密钥`SS(Static Secret)`,然后用SS加密第一个业务数据包(实际上是通过SS衍生的密钥对业务数据进行加密,后面详述),这样将SS加密的业务数据包和`cli_pub_key`一起传给Server,Server通过`cli_pub_key`和`static_server_private_key`算出SS,解密业务数据包,这样就达到了0-RTT密钥协商的效果。 70 | ![](assets/mmtls_image/7.jpg) 71 | 72 |   这里说明一下:ECDH协商中,如果公私钥对都是临时生成的,一般称为ECDHE,因此1-RTT的ECDH协商方式被称为1-RTT ECDHE握手,0-RTT 中有一个静态内置的公钥,因此称为0-RTT ECDH握手。 73 | #####  2. 0-RTT PSK密钥协商##### 74 |   0-RTT PSK握手比较简单,回顾1-RTT PSK握手,其实在进行1-RTT PSK握手之前,Client已经有一个对称加密密钥key了,就直接拿这个对称加密密钥key加密业务数据,然后将其和握手协商数据`ticket{key}`一起传递给Server就可以了。 75 | 76 | #####  3.提高0-RTT密钥协商的安全性##### 77 |   PFS(perfect forward secrecy),中文可叫做完全前向保密。它要求一个密钥被破解,并不影响其他密钥的安全性,反映的密钥协商过程中,大致的意思是用来产生会话密钥的长期密钥泄露出去,不会造成之前通信时使用的会话密钥的泄露;或者密钥协商方案中不存在长期密钥,所有协商材料都是临时生成的。 78 | 79 |   上面所述的0-RTT ECDH密钥协商加密的数据的安全性依赖于长期保存的密钥`static_svr_pri_key`,如果`static_svr_pri_key`泄露,那么所有基于0-RTT ECDH协商的密钥SS都将被轻松计算出来,它所加密的数据也没有任何保密性可言,为了提高前向安全性,我们在进行0-RTT ECDH协商的过程中也进行ECDHE协商,这种协商方式称为0-RTT ECDH-ECDHE密钥协商。如下图所示: 80 | ![](assets/mmtls_image/8.jpg) 81 |   这样,我们基于`static_svr_pri_key`保护的数据就只有第一个业务数据包AppData,后续的包都是基于ES(Ephemeral Secret)对业务数据进行保护的。这样即使`static_svr_pri_key`泄露,也只有连接的第一个业务数据包能够被解密,提高前向安全性。 82 | 83 |   同样的,0-RTT PSK密钥协商加密的数据的安全性依赖于长期保存密钥`ticket_key`,如果`ticket_key`泄露,那么所有基于`ticket_key`进行保护的数据都将失去保密性,因此同样可以在0-RTT PSK密钥协商的过程中,同时完成ECDHE密钥协商,提高前向安全性。 84 | ###  3.2.2 密钥协商需要关注的细节### 85 |   根据前面的描述可以知道,要使得密钥协商过程不被中间人攻击,就必须要对协商数据进行认证。下面拿1-RTT ECDHE握手方式来说明在进行认证过程中需要注意的细节。在1-RTT ECDHE中的认证方式是使用ECDSA签名算法的非对称认证方式,整个过程大致如下:Server在收到客户端的`cli_pub_key`后,随机生成一对ECDH公私钥`(svr_pub_key, svr_pri_key)`,然后用签名密钥`sign_key`对`svr_pub_key`进行签名,得到签名值Signature,并把签名值Signature和`svr_pub_key`一起发送给客户端。客户端收到之后,用`verify_key`进行验签(`verify_key`和`sign_key`是一对ECDSA密钥),验签成功后才会继续走协商对称密钥的流程。 86 | 87 |   上面的认证过程,有三个值得关注的点: 88 | 89 | - Verify_Key如何下发给客户端? 90 | 91 |   这实际上是公钥派发的问题,TLS是使用证书链的方式来派发公钥(证书),对于微信来说,如果使用证书链的方式来派发Server的公钥(证书),无论自建Root CA还是从CA处申请证书,都会增加成本且在验签过程中会存在额外的资源消耗。由于客户端是由我们自己发布的,我们可以将`verify_key`直接内置在客户端,这样就避免证书链验证带来的时间消耗以及证书链传输带来的带宽消耗。 92 | 93 | - 如何避免签名密钥`sign_key`泄露带来的影响? 94 | 95 |   如果`sign_key`泄露,那么任何人都可以伪造成Server欺骗Client,因为它拿到了`sign_key`,它就可以签发任何内容,Client用`verify_key`去验证签名必然验签成功。因此`sign_key`如果泄露必须要能够对`verify_key`进行撤销,重新派发新的公钥。这其实和前一问题是紧密联系的,前一问题是公钥派发问题,本问题是公钥撤销问题。TLS是通过CRL和OCSP两种方式来撤销公钥的,但是这两种方式存在撤销不及时或给验证带来额外延迟的副作用。由于mmtls是通过内置·verify_key·在客户端,必要时通过强制升级客户端的方式就能完成公钥撤销及更新。另外,`sign_key`是需要Server高度保密的,一般不会被泄露,对于微信后台来说,类似于`sign_key`这样,需要长期私密保存的密钥在之前也有存在,早已形成了一套方法和流程来应对长期私密保存密钥的问题。 96 | 97 | - 用`sign_key`进行签名的内容仅仅只包含`svr_pub_key`是否有隐患? 98 | 99 |   回顾一下,上面描述的带认证的ECDH协商过程,似乎已经足够安全,无懈可击了,但是,面对成亿的客户端发起ECDH握手到成千上万台接入层机器,每台机器对一个TCP连接随机生成不同的ECDH公私钥对,这里试想一种情况,假设某一台机器某一次生成的ECDH私钥`svr_pri_key1`泄露,这实际上是可能的,因为临时生成的ECDH公私钥对本身没有做任何保密保存的措施,是明文、短暂地存放在内存中,一般情况没有问题,但在分布式环境,大量机器大量随机生成公私钥对的情况下,难保某一次不被泄露。这样用`sign_key`(`sign_key`是长期保存,且分布式环境共享的)对`svr_pub_key1`进行签名得到签名值Signature1,此时攻击者已经拿到`svr_pri_key1,svr_pub_key1和Signature1`,这样他就可以实施中间人攻击,让客户端每次拿到的服务器ECDH公钥都是`svr_pub_key1`:客户端随机生成ECDH公私钥对(`cli_pub_key, cli_pri_key)`并将`cli_pub_key`发给Server,中间人将消息拦截下来,将`client_pub_key`替换成自己生成的`client_pub_key’`,并将`svr_pub_key1`和Signature1回给Client,这样Client就通过计算`ECDH_Compute_Key(svr_pub_key1, cli_pri_key)=Key1`, Server通过计算`ECDH_Compute_Key(client_pub_key’, svr_pub_key)=Key’`,中间人既可以计算出Key1和Key’,这样它就可以用Key1和Client通信,用Key’和Server进行通信。发生上述被攻击的原因在于一次握手中公钥的签名值被用于另外一次握手中,如果有一种方法能够使得这个签名值和一次握手一一对应,那么就能解决这个问题。解决办法也很简单,就是在握手请求的ClientHello消息中带一个`Client_Random`随机值,然后在签名的时候将`Client_Random`和`svr_pub_key`一起做签名,这样得到的签名值就与`Client_Random`对应了。mmtls在实际处理过程中,为了避免Client的随机数生成器有问题,造成生成不够随机的Client_Random,实际上Server也会生成一个随机数`Server_Random`,然后在对公钥签名的时候将`Client_Random、Server_Random、svr_pub_key`一起做签名,这样由`Client_Random、Server_Random`保证得到的签名值唯一对应一次握手。 100 | 101 | ###  3.2.3 mmtls对认证密钥协商的选择### 102 |   上面一共介绍了2种1-RTT 密钥协商方式和4种0-RTT 密钥协商方式。 103 | 104 | ![](assets/mmtls_image/9.jpg) 105 | 106 |   PSK握手全程无非对称运算,Server性能消耗小,但前向安全性弱,ECDHE握手有非对称运算,Server性能消耗大,但前向安全性相对更强,那么如何结合两者优势进行密钥协商方式的选择呢? 107 | 108 |   首先PSK是如何获得的呢?PSK是在一次成功的ECDH(E)握手中下发的(在上面的图7、图8没有画出下发PSK的部分),如果客户端没有PSK,那么显然是只能进行ECDH(E)握手了。由于PSK握手和ECDH(E)握手的巨大性能差异,那么在Client有PSK的情况下,应该进行PSK握手。那么在没有PSK的情况下,上面的1-RTT ECDHE、0-RTT ECDH、0-RTT ECDH-ECDHE具体应该选择哪一种呢?在有PSK的情况下,应该选择1-RTT PSK、0-RTT PSK还是0-RTT PSK-ECDHE呢? 109 | 110 |   对于握手方式的选择,我们也是几经过修改,最后结合微信网络连接的特点,我们选择了1-RTT ECDHE握手、1-RTT PSK握手、0-RTT PSK握手。微信目前有两个数据传输通道:1.基于HTTP协议的短连接 2.基于私有协议的长连接。 111 | 112 |   微信长连接有一个特点,就是在建立好TCP连接之后,会在此TCP连接上先发一个长连nooping包,目的是验证长连接的连通性(由于长连接是私有协议,部分中间路由会过滤掉这种私有协议的数据包),这就是说长连接在建立时的第一个数据包是不会发送业务数据的,因此使用1-RTT的握手方式,由第一个握手包取代之前的nooping包去探测长连的连通性,这样并不会增加长连的网络延时,因此我们选取在长连接情况下,使用1-RTT ECDHE和1-RTT PSK这两种密钥协商方式。 113 | 114 |   微信短连接为了兼容老版本的HTTP协议,整个通信过程就只有一个RTT,也就是说Client建立TCP连接后,通过HTTP POST一个业务请求包到Server,Server回一个HTTP响应,Client处理后立马断掉TCP连接。对于短连接,我们应该尽量使用0-RTT的握手方式,因为一个短连接原来就只存在一个RTT,如果我们大量使用1-RTT的握手方式,那么直接导致短连接至少需要2个RTT才能完成业务数据的传输,导致时延加倍,用户体验较差。这里存在两种情况:(1)客户端没有PSK,为了安全性,这时和长连接的握手方式一样,使用1-RTT ECDHE;(2)客户端有PSK,这时为了减少网络时延,应该使用0-RTT PSK或0-RTT PSK-ECDHE,在这两种握手方式下,由于业务请求包始终是基于PSK进行保护的,同一个PSK多次协商出来的对称加密key是同一个,这个对称加密key的安全性依赖于`ticket_key`的安全性,因此0-RTT情况下,业务请求包始终是无法做到前向安全性。0-RTT PSK-ECDHE这种方式,只能保证本短连接业务响应回包的前向安全性,这带来安全性上的优势是比较小的,但是与0-RTT PSK握手方式相比,0-RTT PSK-ECDHE在每次握手对server会多2次ECDH运算和1次ECDSA运算。微信的短连接是非常频繁的,这对性能影响极大,因此综合考虑,在客户端有PSK的情况下,我们选择使用0-RTT PSK握手。由于0-RTT PSK握手安全性依赖`ticket_key`,为了加强安全性,在实现上,PSK必须要限制过期时间,避免长期用同一个PSK来进行握手协商;`ticket_key`必须定期轮换,且具有高度机密的运维级别。 115 | 116 |   另外,为了提高系统可用性,实际上mmtls在一次成功的ECDH握手中会下发两个PSK,一个生命周期短保证安全性,一个生命周期长保证可用性。在一次ECDH握手中,请求会带上生命周期长的PSK(如果存在的话),后台可根据负载情况进行权衡,选择使用ECDH握手或者PSK握手。 117 | 118 | ##3.3 Record协议 --- 使用对称加密密钥进行安全的通信 ## 119 |   经过上面的Handshake过程,此时Client和Server已经协商出了一致的对称加密密钥`pre_master_key`,那么接下来就可以直接用这个`pre_master_key`作为密钥,选择一种对称加密算法(如常用的AES-CBC)加密业务数据,将密文发送给Server。是否真的就这么简单呢?实际上如果真的按这个过程进行加密通信是有很多安全漏洞的。 120 | 121 | ###  3.3.1 认证加密(Authenticated Encryption)### 122 |   “加密并不是认证”在密码学中是一个简单的共识,但对于我们很多程序员来说,并不知道这句话的意义。加密是隐藏信息,使得在没有正确密钥的情况下,信息变得难以读懂,加密算法提供保密性,上面所述的AES-CBC这种算法只是提供保密性,即防止信息被窃听。在信息安全领域,消息认证(message authentication)或数据源认证(data origin authentication)表示数据在传输过程中没有被修改(完整性),并且接收消息的实体能够验证消息的源(端点认证)。AES-CBC这种加密算法只提供保密性,但是并不提供完整性。这似乎有点违反直觉,好像对端发给我一段密文,如果我能够解密成功,通过过程就是安全的,实则不然,就拿AES-CBC加密一段数据,如果中间人篡改部分密文,只要不篡改padding部分,大部分时候仍旧能够正常解密,只是得到的明文和原始明文不一样。现实中也有对消息追加CRC校验来解决密文被篡改问题的,实际上经过精心构造,即使有CRC校验仍然能够被绕过。本质的原因是在于进行加密安全通信过程,只使用了提供保密性的对称加密组件,没有使用提供消息完整性的密码学组件。因此只要在用对称加密算法加密明文后,再用消息认证码算法对密文计算一次消息认证码,将密文和消息认证码发送给Server,Server进行验证,这样就能保证安全性了。实际上加密过程和计算消息认证码的过程,到底应该如何组合,谁先谁后,在密码学发展的历史上先后出现了三种组合方式:(1)Encrypt-and-MAC (2)MAC-then-Encrypt (3)Encrypt-then-MAC,根据最新密码学动态,目前学术界已经一致同意Encrypt-then-MAC是最安全的,也就是先加密后算消息认证码的方式。鉴于这个陷阱如此险恶,因此就有人提出将Encrypt和MAC直接集成在一个算法内部,让有经验的密码专家在算法内部解决安全问题,不让算法使用者选择,这就是这就是AEAD(Authenticated-Encryption With Addtional data)类的算法。TLS1.3彻底禁止AEAD以外的其他算法。mmtls经过综合考虑,选择了使用AES-GCM这种AEAD类算法,作为协议的认证加密组件,而且AES-GCM也是TLS1.3要求必须实现的算法。 123 | 124 | ###  3.3.2 密钥扩展### 125 |   TLS1.3明确要求通信双方使用的对称加密Key不能完全一样,否则在一些对称加密算法下会被完全攻破,即使是使用AES-GCM算法,如果通信双方使用完全相同的加密密钥进行通信,在使用的时候也要小心翼翼的保证一些额外条件,否则会泄露部分明文信息。另外,AES算法的初始化向量(IV)如何构造也是很有讲究的,一旦用错就会有安全漏洞。也就是说,对于handshake协议协商得到的`pre_master_secret`不能直接作为双方进行对称加密密钥,需要经过某种扩展变换,得到六个对称加密参数: 126 | 127 |   Client Write MAC Key (用于Client算消息认证码,以及Server验证消息认证码) 128 |   Server Write MAC Key (用于Server算消息认证码,以及Client验证消息认证码) 129 |   Client Write Encryption Key(用做Client做加密,以及Server解密) 130 |   Server Write Encryption Key(用做Server做加密,以及Client解密) 131 |   Client Write IV (Client加密时使用的初始化向量) 132 |   Server Write IV (Server加密时使用的初始化向量) 133 |   当然,使用AES-GCM作为对称加密组件,MAC Key和Encryption Key只需要一个就可以了。 134 | 135 |   握手生成的`pre_master_secret`只有48个字节,上述几个加密参数的长度加起来肯定就超过48字节了,所以需要一个函数来把48字节延长到需要的长度,在密码学中专门有一类算法承担密钥扩展的功能,称为密钥衍生函数(Key Derivation Function)。TLS1.3使用的HKDF做密钥扩展,mmtls也是选用的HKDF做密钥扩展。 136 | 137 |   在前文中,我用`pre_master_secret`代表握手协商得到的对称密钥,在TLS1.2之前确实叫这个名字,但是在TLS1.3中由于需要支持0-RTT握手,协商出来的对称密钥可能会有两个,分别称为Static Secret(SS)和Ephemeral Secret(ES)。从TLS1.3文档中截取一张图进行说明一下: 138 | 139 | ![](assets/mmtls_image/10.jpg) 140 | 141 |   上图中Key Exchange就是代表握手的方式,在1-RTT ECDHE握手方式下, 142 | 143 | ES=SS = ECDH_Compute_Key(svr_pub_key, cli_pri_key); 144 | 145 |   在0-RTT ECDH下, 146 | 147 | SS=ECDH_Compute_Key(static_svr_pub_key, cli_pri_key), 148 | ES=ECDH_Compute_Key(svr_pub_Key, cli_pri_Key); 149 |   在0-RTT/1-RTT PSK握手下, 150 | 151 | ES=SS=pre-shared key; 152 |   在0-RTT PSK-ECDHE握手下, 153 | 154 | SS=pre-shared key, 155 | ES=ECDH_Compute_Key(svr_pub_key, cli_pri_key); 156 | 157 |   前面说过mmtls使用的密钥扩展组件为HKDF,该组件定义了两个函数来保证扩展出来的密钥具有伪随机性、唯一性、不能逆推原密钥、可扩展任意长度密钥。两个函数分别是: 158 | 159 | HKDF-Extract( salt, initial-keying-material ) 160 | 161 |   该函数的作用是对initial-keying-material进行处理,保证它的熵均匀分别,足够的伪随机。 162 | 163 | HKDF-Expand( pseudorandom key, info, out_key_length ) 164 | 165 |   参数pseudorandom key是已经足够伪随机的密钥扩展材料,HKDF-Extract的返回值可以作为`pseudorandom key`,`info`用来区分扩展出来的Key是做什么用,`out_key_length`表示希望扩展输出的key有多长。mmtls最终使用的密钥是有HKDF-Expand扩展出来的。mmtls把info参数分为:`length,label,handshake_hash`。其中length等于`out_key_length`,`label`是标记密钥用途的固定字符串,`handshake_hash`表示握手消息的hash值,这样扩展出来的密钥保证连接内唯一。 166 | 167 | ![](assets/mmtls_image/11.jpg) 168 | 169 |   TLS1.3草案中定义的密钥扩展方式比较繁琐,如上图所示。为了得到最终认证加密的对称密钥,需要做3次HDKF-Extract和4次HKDF-Expand操作,实际测试发现,这种密钥扩展方式对性能影响是很大的,尤其在PSK握手情况(PSK握手没有非对称运算)这种密钥扩展方式成为性能瓶颈。TLS1.3之所以把密钥扩展搞这么复杂,本质上还是因为TLS1.3是一个通用的协议框架,具体的协商算法是可以选择的,在有些协商算法下,协商出来的`pre_master_key`(SS和ES)就不满足某些特性(如随机性不够),因此为了保证无论选择什么协商算法,用它来进行通信都是安全的,TLS1.3就在密钥扩展上做了额外的工作。而mmtls没有TLS1.3这种包袱,可以针对微信自己的网络通信特点进行优化(前面在握手方式选择上就有体现)。mmtls在不降低安全性的前提下,对TLS1.3的密钥扩展做了精简,使得性能上较TLS1.3的密钥扩展方式有明显提升。 170 | 171 |   在mmtls中,`pre_master_key`(SS和ES)经过密钥扩展,得到了一个长度为`2*enc_key_length+2*iv_length`的一段buffer,用`key_block`表示,其中: 172 | 173 |   client_write_key = key_block[0...enc_key_length-1] 174 |   client_write_key = key_block[enc_key_length...2*enc_key_length-1] 175 |   client_write_IV = key_block[2*enc_key_length...2*enc_key_length+iv_length-1] 176 |   server_write_IV = key_block[2*enc_key_length+iv_length...2*enc_key_length+2*iv_length-1] 177 | 178 | ###  3.3.3 防重放### 179 | 180 |   重放攻击(Replay Attacks)是指攻击者发送一个接收方已经正常接收过的包,由于重防的数据包是过去的一个有效数据,如果没有防重放的处理,接收方是没办法辨别出来的。防重放在有些业务是必须要处理的,比如:如果收发消息业务没有做防重放处理,就会出现消息重复发送的问题;如果转账业务没有做防重放处理,就会重现重复转账问题。微信在一些关键业务层面上,已经做了防重放的工作,但如果mmtls能够在下层协议上就做好防重放,那么就能有效减轻业务层的压力,同时为目前没有做防重放的业务提供一个安全保障。 181 |    182 | 183 |   防重放的解决思路是为连接上的每一个业务包都编一个递增的sequence number,这样只要服务器检查到新收到的数据包的sequence number小于等于之前收到的数据包的sequence number,就可以断定新收到的数据包为重放包。当然sequence number是必须要经过认证的,也就是说sequence number要不能被篡改,否则攻击者把sequence number改大,就绕过这个防重放检测逻辑了。可以将sequence number作为明文的一部分,使用AES-GCM进行认证加密,明文变长了,不可避免的会增加一点传输数据的长度。实际上,mmtls的做法是将sequence number作为构造AES-GCM算法参数nonce的一部分,利用AES-GCM的算法特性,只要AES-GCM认证解密成功就可以确保sequence number符合预期。 184 | 185 |   上述防重放思路在1-RTT的握手方式下是没有问题的,因为在1-RTT握手下,第一个RTT是没有业务数据的,可以在这个RTT下由Client和Server共同决定开始计算sequence number的起点。但是在0-RTT的握手方式,第一个业务数据包和握手数据包一起发送给服务器,对于这第一个数据包的防重放,Server只能完全靠Client发来的数据来判断是否重放,如果客户端发送的数据完全由自己生成,没有包含服务器参与的标识,那么这份数据是无法判断是否为重放数据包的。在TLS1.3给了一个思路来解决上述这个“0-RTT跨连接重放的问题”:在Server处保存一个跨连接的全局状态,每新建一个连接都更新这个全局状态,那么0-RTT握手带来的第一个业务数据也可以由这个跨连接的全局状态来判断是否重放。但是,在一个分布式系统中每新建一个连接都读写这个全局状态,如此频繁的读写,无疑在可用性和性能消耗上都不可接受。事实上,0-RTT跨连接防重放确实困难,目前没有比较通用、高效的方案。其实在Google的QUIC crypto protocol中也存在0-RTT跨连接重放的问题,由于QUIC主要应用在Chrome浏览器上,在浏览器上访问网站时,建连接的第一个请求一般是GET而不是POST,所以0-RTT加密的数据不涉及多少敏感性,被重放也只是刷新一次页面而已,所以其选择了不解决0-RTT防重放的问题。但是微信短连接是POST请求,带给Server的都是上层的业务数据,因此0-RTT防重放是必须要解决的问题。mmtls根据微信特有的后台架构,提出了基于客户端和服务器端时间序列的防重放策略,mmtls能够保证超过一段时间T的重放包被服务器直接解决,而在短时间T内的重放包需要业务框架层来协调支持防重放,这样通过proxy层和logic框架层一起来解决0-RTT PSK请求包防重放问题,限于篇幅,详细方案此处不展开介绍。 186 | #四、小结 # 187 | 188 |   mmtls是参考TLS1.3草案标准设计与实现的,使用ECDH来做密钥协商,ECDSA进行签名验证,AES-GCM作为对称加密算法来对业务数据包进行认证加密,使用HKDF进行密钥扩展,摘要算法为SHA256。另外,结合具体的使用场景,mmtls在TLS1.3的基础上主要做了以下几方面的工作: 189 |    190 | 191 | 1. 轻量级。砍掉了客户端认证相关的内容;直接内置签名公钥,避免证书交换环节,减少验证时网络交换次数。  192 | 2. 安全性。选用的基础密码组件均是TLS1.3推荐、安全性最高的密码组件;0-RTT防重放由proxy层和logic框架层协同控制。   193 | 3. 高性能。使用0-RTT握手方式没有增加原有Client和Server的交互次数;和TLS1.3比,优化了握手方式和密钥扩展方式。  194 | 4. 高可用性。服务器的过载保护,确保服务器能够在容灾模式下提供安全级别稍低的有损服务。 195 | 196 | #五、参考资料 # 197 | 1. [TLS协议分析与现代加密通信协议设计](http://blog.helong.info/blog/2015/09/07/tls-protocol-analysis-and-crypto-protocol-design/) 198 | 2. [RFC5246](https://tools.ietf.org/html/rfc5246) 199 | 3. [The Transport Layer Security (TLS) Protocol Version 1.3](https://tlswg.github.io/tls13-spec/) 200 | -------------------------------------------------------------------------------- /微信Android热补丁实践演进之路.md: -------------------------------------------------------------------------------- 1 | # 微信Android热补丁实践演进之路 # 2 | 继插件化后,热补丁技术在2015年开始爆发,目前已经是非常热门的Android开发技术。其中比较著名的有淘宝的Dexposed、支付宝的AndFix以及QZone的超级热补丁方案。微信对热补丁技术的研究并不算早,大约开始于2015年6月。经过研究与尝试现有的各个方案,我们发现它们都有着自身的一些局限性。微信最终采用不同于它们的技术方案,走出了自己的实践演进之路。 3 | 4 | 另外一方面,技术应当只是热补丁方案中的一环。随着对热补丁的多次尝试与应用,微信建立起自身的流程规范,同时也不断的尝试拓展它的应用场景。通过本文,我希望大家不仅能够全面的了解各项热补丁技术的优缺点,同时也能对它的应用场景有着更加全面的认识。在此基础上,大家或许能更容易的决定是否在自己的项目中使用热补丁技术,以及应当如何使用它。 5 | 6 | ## 为什么需要热补丁 ## 7 | 8 | > 热补丁:让应用能够在无需重新安装的情况实现更新,帮助应用快速建立动态修复能力。 9 | 10 | 从上面的定义来看,热补丁节省Android大量应用市场发布的时间。同时用户也无需重新安装,只要上线就能无感知的更新。看起来很美好,这是否可以意味我们可以尽量使用补丁来代替发布呢?事实上,热补丁技术当前依然存在它的局限性,主要表现在以下几点: 11 | 12 | 1. 补丁只能针对单一客户端版本,随着版本差异变大补丁体积也会增大; 13 | 2. 补丁不能支持所有的修改,例如AndroidManifest; 14 | 3. 补丁无论对代码还是资源的更新成功率都无法达到100%。 15 | 16 | 既然补丁技术无法完全代替升级,那它适合使用在哪些场景呢? 17 | 18 | ###一. 轻量而快速的升级 ### 19 | 20 | 热补丁技术也可以理解为一个动态修改代码与资源的通道,它适合于修改量较少的情况。以微信的多次发布为例,补丁大小均在300K以内,它相对于传统的发布有着很大的优势。 21 | 22 | ![](assets/tinker/data.png) 23 | 24 | 以Android用户的升级习惯,即使是相对活跃的微信也需要10天以上的时间去覆盖50%的用户。使用补丁技术,我们能做到1天覆盖70%以上。这也是基于补丁体积较小,可以直接使用移动网络下载更新。 25 | 26 | 正因如此,补丁技术非常适合使用在灰度阶段。在过去,我们需要在正式发布前保证所有严重的问题都已经得到修复,这通常需要我们经过三次以上的灰度过程,而且无法快速的验证这些问题在同一批用户的修复效果。利用热补丁技术,我们可以快速对同一批用户验证修复效果,这大大缩短了我们的发布流程。 27 | 28 | ![](assets/tinker/workmodel.png) 29 | 30 | 若发布版本出现问题或紧急漏洞,传统方式需要单独灰度验证修改,然后重新发布新的版本。利用补丁技术,我们只需要先上线小部分用户验证修改的效果,最后再全量上线即可。但是此种发布对线上用户影响较大, 我们需要谨慎而为。本着对用户负责的态度,**发布补丁等同于发布版本**,它也应该严格执行完整的测试与上线流程。 31 | 32 | ![](assets/tinker/workmodel2.png) 33 | 34 | 总的来说,补丁技术可以降低开发成本,缩短开发周期,实现轻量而快速的升级。 35 | 36 | ###二. 远端调试### 37 | 38 | 一入Android深似海,Android开发的另外一个痛是机型的碎片化。我们也许都会遇到"本地不复现","日志查不出","联系用户不鸟你"的烦恼。所以补丁机制非常适合使用在远端调试上。即我们需要具备只特定用户发送补丁的能力,这对我们查找问题非常有帮助。 39 | 40 | ![](assets/tinker/userlog.png) 41 | 42 | 利用补丁技术,我们避免了骚扰用户而默默的为用户解决问题。当然这也需要非常严格的权限管理,以防恶意或随意使用。 43 | 44 | ###三. 数据统计### 45 | 46 | 数据统计在微信中也占据着非常重要的位置,我们也非常希望将热补丁与数据统计结合的更好。事实上,热补丁无论在普通的数据统计还是ABTest都有着非常大的优势。例如若我想对同一批用户做两种test, 传统方式无法让这批用户去安装两个版本。使用补丁技术,我们可以方便的对同一批用户不停的更换补丁。 47 | 48 | ![](assets/tinker/abtest.png) 49 | 50 | 在数据统计之路,如何与补丁技术结合的更好,更加精准的控制样本人数与比例,这也是微信当前努力发展的一个方向。 51 | 52 | ###四. 其他### 53 | 事实上,Android官方也使用热补丁技术实现Instant Run。它分为Hot Swap、Warm Swap与Cold Swap三种方式,大家可以参考[英文介绍](https://medium.com/google-developers/instant-run-how-does-it-work-294a1633367f#.c088qhdxu),也可以看参考文章中的翻译稿。最新的Instant App应该也是采用类似的原理,但是Google Play是不允许下发代码的,这个海外App需要注意一下。 54 | 55 | ## 微信热补丁技术的演进之路 ## 56 | 在了解补丁技术可以与适合做什么之后,我们回到技术本身。由于[Dexposed](https://github.com/alibaba/dexposed)无法支持全平台,并不适合应用到商业产品中。所以这里我们只简单介绍Andfix、QZone、微信几套方案的实现,以及它们方案面临着的问题,大家也可以参考资料中的[各大热补丁方案分析和比较](http://blog.zhaiyifan.cn/2015/11/20/HotPatchCompare/)一文。 57 | 58 | ###一. AndFix### 59 | [AndFix](https://github.com/alibaba/AndFix)采用native hook的方式,这套方案直接使用`dalvik_replaceMethod`替换class中方法的实现。由于它并没有整体替换class, 而field在class中的相对地址在class加载时已确定,所以AndFix无法支持新增或者删除filed的情况(通过替换`init`与`clinit`只可以修改field的数值)。 60 | 61 | ![](assets/tinker/andfix.png) 62 | 63 | 也正因如此,Andfix可以支持的补丁场景相对有限,仅仅可以使用它来修复特定问题。结合之前的发布流程,我们更希望补丁对开发者是不感知的,即他不需要清楚这个修改是对补丁版本还是正式发布版本(事实上我们也是使用git分支管理+cherry-pick方式)。另一方面,使用native替换将会面临比较复杂的兼容性问题。 64 | 65 | ![](assets/tinker/andfixend.png) 66 | 67 | 相比其他方案,AndFix的最大优点在于立即生效。事实上,AndFix的实现与[Instant Run的热插拔](http://www.jianshu.com/p/2e23ba9ff14b)有点类似,但是由于使用场景的限制,微信在最初期已排除使用这一方案。 68 | 69 | ###二. QZone### 70 | QZone方案并没有开源,但在github上的[Nuwa](https://github.com/jasonross/Nuwa)采用了相同的方式。这个方案使用classloader的方式,能实现更加友好的类替换。而且这与我们加载Multidex的做法相似,能基本保证稳定性与兼容性。具体原理在这里不再细说,大家可以[参考这篇文章](https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a)。 71 | 72 | 本方案为了解决`unexpected DEX problem`异常而采用插桩的方式,从而规避问题的出现。事实上,Android系统的这些检查规则是非常有意义的,这会导致QZone方案在Dalvik与Art都会产生一些问题。 73 | 74 | - Dalvik; 在dexopt过程,若class verify通过会写入pre-verify标志,在经过optimize之后再写入odex文件。这里的optimize主要包括inline以及quick指令优化等。 75 | 76 | ![](assets/tinker/qzone-dalvik.png) 77 | 78 | 若采用插桩导致所有类都非preverify,这导致verify与optimize操作会在加载类时触发。这会有一定的性能损耗,微信分别采用插桩与不插桩两种方式做过两种测试,一是连续加载700个50行左右的类,一是统计微信整个启动完成的耗时。 79 | 80 | ![](assets/tinker/qzone-dalvik-end.png) 81 | 82 | 平均每个类verify+optimize(跟类的大小有关系)的耗时并不长,而且这个耗时每个类只有一次。但由于启动时会加载大量的类,在这个情况影响还是比较大的。 83 | 84 | - Art; Art采用了新的方式,插桩对代码的执行效率并没有什么影响。但是若补丁中的类出现修改类变量或者方法,可能会导致出现内存地址错乱的问题。为了解决这个问题我们需要将修改了变量、方法以及接口的类的父类以及调用这个类的所有类都加入到补丁包中。这可能会带来补丁包大小的急剧增加。 85 | 86 | ![](assets/tinker/qzone-art.png) 87 | 88 | 这里是因为在dex2oat时`fast*`已经将类能确定的各个地址写死。如果运行时补丁包的地址出现改变,原始类去调用时就会出现地址错乱。这里说的可能不够详细,事实上微信当时为了查清这两个问题,也花费了一定的时间将Dalvik跟Art的流程基本搞透。若大家对这里感兴趣,后续在单独的文章详细论述。 89 | 90 | 总的来说,Qzone方案好处在于开发透明,简单,这一套方案目前的应用成功率也是最高的,但在补丁包大小与性能损耗上有一定的局限性。特别是无论我们是否真正应用补丁,都会因为插桩导致对程序运行时的性能产生影响。微信对于性能要求较高,所以我们也没有采用这套方案。 91 | 92 | ###三. 微信热补丁方案### 93 | 94 | 有没有那么一种方案,能做到开发透明,但是却没有QZone方案的缺陷呢?Instant Run的冷插拔与buck的[exopackage](https://buckbuild.com/article/exopackage.html)或许能给我们灵感,它们的思想都是全量替换新的Dex。即我们完全使用了新的Dex,那样既不出现Art地址错乱的问题,在Dalvik也无须插桩。当然考虑到补丁包的体积,我们不能直接将新的Dex放在里面。但我们可以将新旧两个Dex的差异放到补丁包中,最简单我们可以采用BsDiff算法。 95 | 96 | ![](assets/tinker/wechat.png) 97 | 98 | 简单来说,在编译时通过新旧两个Dex生成差异path.dex。在运行时,将差异patch.dex重新跟原始安装包的旧Dex还原为新的Dex。这个过程可能比较耗费时间与内存,所以我们是单独放在一个后台进程:patch中。为了补丁包尽量的小,微信自研了DexDiff算法,它深度利用Dex的格式来减少差异的大小。它的粒度是Dex格式的每一项,可以充分利用原本Dex的信息,而BsDiff的粒度是文件,AndFix/QZone的粒度为class。 99 | 100 | ![](assets/tinker/alldiff.png) 101 | 102 | 这块后面我希望后面用单独的文章来讲述,这里先做一个铺垫,大致的效果如下图。在最极端的情况,由于利用了原本dex的信息完全替换一个13M的Dex,我们的补丁大小也仅仅只有6.6M。 103 | 104 | ![](assets/tinker/wechat-dexdiff.png) 105 | 106 | 但是这套方案并非没有缺点,它带来的问题有两个: 107 | 108 | 1. 占用Rom体积;这边大约是你修改Dex数量的1.5倍(dexopt与dex压缩成jar)的大小。 109 | 2. 一个额外的合成过程;虽然我们单独放在一个进程上处理,但是合成时间的长短与内存消耗也会影响最终的成功率。 110 | 111 | > 微信的热补丁方案叫做Tinker,也算缅怀一下Dota中的地精修补匠,希望能做到无限刷新。 112 | > 113 | > ![](assets/tinker/tinker.png) 114 | 115 | 限于篇幅,这里对Dex、library以及资源的更多技术细节并没有详细的论述,这里希望放在后面的单独文章中。我们最后从整体比较一下这几种方案: 116 | 117 | ![](assets/tinker/all.png) 118 | 119 | 若不care性能损耗与补丁包大小,QZone方案是最简单且成功率最高的方案(没有单独的合成过程)。相对Tinker来说,它的占用Rom体积也更小。另一方面,QZone与Tinker的成功率大约相差3%左右。 120 | 121 | 事实上,一个完整的框架应该也是一个容易使用的框架。Tinker对补丁版本管理、进程管理、安全校验等都有着很好的支持。同时我们也支持gradle与命名行两种接入方式。希望在不久的将来,它可以很快的跟大家见面。 122 | 123 | ## 微信的热补丁应用现状 ## 124 | 上一章节我们简单比较了各个热补丁的技术方案,它们解决了如何生成与加载补丁包的问题。但一个完善的热补丁系统不应该仅限于此,它还需要包括以下几个方面: 125 | 126 | - 网络通道;这里要解决的问题是决定补丁以何种方式推送给哪部分的用户。 127 | - 上线与后台管理平台;这里主要包括热补丁的上线管理,历史管理以及上报分析,报警监控等; 128 | 129 | ###一. 网络通道现状### 130 | 131 | 网络通道负责的将补丁包交付给用户,这个包括特定用户与全量用户两种情况。事实上,微信当前针对热补丁有以下三种通道更新: 132 | 133 | - pull通道; 在登陆/24小时等时机,通过pull方式查询后台是否有对应的补丁包更新,这也是我们最常用的方式; 134 | - 指定版本的push通道; 针对版本的通道,在紧急情况下,我们可以在一个小时内向所有用户下发补丁包更新。 135 | - 指定特定用户的push通道;对特定用户或用户组做远程调试。 136 | 137 | 事实上,对于大部分的应用来说,假设不实现push通道,CDN+pull通道实现起来还是较为容易。 138 | 139 | ###二. 上线与管理平台现状### 140 | 141 | 上线与管理平台主要为了快速上线,管理历史记录,以及监控补丁的运行情况等。 142 | 143 | ![](assets/tinker/use.png) 144 | 145 | 事实上,微信发布热补丁是非常慎重的。它整个发布流程与升级版本是保持一致的,也必须修改版本号、经过严格的完整测试流程等。我们也会通过灰度的方式上线,同时监控补丁版本的各个指标。这里的为了完整的监控补丁的情况,我们做的工作有: 146 | 147 | - 1分钟粒度的每小时/每天的各版本累积用户,及时监控补丁版本的人数与活跃; 148 | - 3分钟粒度的Crash统计,基准版本与补丁版本的Crash每小时/每天的两个维度对照; 149 | - 10分钟粒度的补丁监控信息上报。 150 | 151 | ###三. 补丁成功率现状### 152 | > 应用成功率= 补丁版本人数/补丁发布前该版本人数 153 | > 由于可能存在基准或补丁版本用户安装了其他版本,所以本统计结果应略为偏低,但它能现实的反应补丁的线上覆盖情况。 154 | 155 | 使用Qzone方案,微信补丁在10天后的应用成功率大约在98.5%左右。使用Tinker大约只有95.5%左右,主要原因在于空间不足以及后台进程被杀。在这里我们也在尝试使用重试的方式以及降低合成的耗时与内存,从而提升成功率。 156 | 157 | 热补丁技术发展的很快,Android推出的Instant App也令人期待。但是在国内,似乎我们还是指望自己更靠谱一点。每一个的应用的需求都不太一致,这里大致讲了一些微信的实践经验,希望对大家有帮助。 158 | 159 | ## 未来工作 ## 160 | 161 | 随着微信部门内从“单APP”向“多APP”演进,微信也正在迈入开源化的开发实践。我们希望将各个功能组件化,从而做可以到快速复制与应用。微信的热补丁框架“Tinker”当前也在经历从微信分离,又合入到微信的过程。希望在不久的将来,我们也可以将“Tinker”以及微信中一些其他的组件开源出去。 162 | 163 | 我们也希望可以找一个App作为内测,给我们提供宝贵的意见。若对微信的Tinker方案感兴趣的用户,可以单独发消息或在文章末留言。注明姓名、所在公司以及负责的App,我们希望挑选部分产品作为内测。 164 | 165 | ## 参考文章 ## 166 | 1. Dexposed github ([https://github.com/alibaba/dexposed](https://github.com/alibaba/dexposed)) 167 | 2. AndFix github ([https://github.com/alibaba/AndFix](https://github.com/alibaba/AndFix)) 168 | 3. Nuwa github ([https://github.com/jasonross/Nuwa](https://github.com/jasonross/Nuwa)) 169 | 4. QZone实现原理解析 ([https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a](https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a)) 170 | 5. Instant Run英文原文 ([https://medium.com/google-developers/instant-run-how-does-it-work-294a1633367f#.c088qhdxu](https://medium.com/google-developers/instant-run-how-does-it-work-294a1633367f#.c088qhdxu)) 171 | 6. Instant Run工作原理及用法中文翻译稿 ([http://www.jianshu.com/p/2e23ba9ff14b](http://www.jianshu.com/p/2e23ba9ff14b)) 172 | 7. Buck exopackage 介绍 ([https://buckbuild.com/article/exopackage.html](https://buckbuild.com/article/exopackage.html)) 173 | 8. 各大热补丁方案分析和比较 ([http://blog.zhaiyifan.cn/2015/11/20/HotPatchCompare/](http://blog.zhaiyifan.cn/2015/11/20/HotPatchCompare/)) -------------------------------------------------------------------------------- /微信Android视频编码爬过的那些坑.md: -------------------------------------------------------------------------------- 1 | Android的视频相关的开发,大概一直是整个Android生态,以及Android API中,最为分裂以及兼容性问题最为突出的一部分。摄像头,以及视频编码相关的API,Google一直对这方面的控制力非常差,导致不同厂商对这两个API的实现有不少差异,而且从API的设计来看,一直以来优化也相当有限,甚至有人认为这是“Android上最难用的API之一” 2 | 3 | 以微信为例,我们录制一个540p的mp4文件,对于Android来说,大体上是遵循这么一个流程: 4 | 5 | ---- 6 | 7 | ![](assets/android_video_record/encodeProcess.png) 8 | 9 | ---- 10 | 11 | 大体上就是从摄像头输出的YUV帧经过预处理之后,送入编码器,获得编码好的h264视频流。 12 | 13 | 上面只是针对视频流的编码,另外还需要对音频流单独录制,最后再将视频流和音频流进行合成出最终视频。 14 | 15 | 这篇文章主要将会对视频流的编码中两个常见问题进行分析: 16 | 17 | 1. 视频编码器的选择(硬编 or 软编)? 18 | 2. 如何对摄像头输出的YUV帧进行快速预处理(镜像,缩放,旋转)? 19 | 20 | ---- 21 | ### 视频编码器的选择 22 | 对于录制视频的需求,不少app都需要对每一帧数据进行单独处理,因此很少会直接用到``MediaRecorder``来直接录取视频,一般来说,会有这么两个选择 23 | 24 | - MediaCodec 25 | - FFMpeg+x264/openh264 26 | 27 | 我们来逐个解析一下 28 | 29 | --- 30 | #### MediaCodec 31 | 32 | MediaCodec是API 16之后Google推出的用于音视频编解码的一套偏底层的API,可以直接利用硬件加速进行视频的编解码。调用的时候需要先初始化MediaCodec作为视频的编码器,然后只需要不停传入原始的YUV数据进入编码器就可以直接输出编码好的h264流,整个API设计模型来看,就是同时包含了输入端和输出端的两条队列: 33 | 34 | ![](assets/android_video_record/mediacodec_buffers.png) 35 | 36 | 因此,作为编码器,输入端队列存放的就是原始YUV数据,输出端队列输出的就是编码好的h264流,作为解码器则对应相反。在调用的时候,MediaCodec提供了同步和异步两种调用方式,但是异步使用Callback的方式是在API 21之后才加入的,以同步调用为例,一般来说调用方式大概是这样(摘自官方例子): 37 | 38 | ```Java 39 | MediaCodec codec = MediaCodec.createByCodecName(name); 40 | codec.configure(format, …); 41 | MediaFormat outputFormat = codec.getOutputFormat(); // option B 42 | codec.start(); 43 | for (;;) { 44 | int inputBufferId = codec.dequeueInputBuffer(timeoutUs); 45 | if (inputBufferId >= 0) { 46 | ByteBuffer inputBuffer = codec.getInputBuffer(…); 47 | // fill inputBuffer with valid data 48 | … 49 | codec.queueInputBuffer(inputBufferId, …); 50 | } 51 | int outputBufferId = codec.dequeueOutputBuffer(…); 52 | if (outputBufferId >= 0) { 53 | ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); 54 | MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A 55 | // bufferFormat is identical to outputFormat 56 | // outputBuffer is ready to be processed or rendered. 57 | … 58 | codec.releaseOutputBuffer(outputBufferId, …); 59 | } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 60 | // Subsequent data will conform to new format. 61 | // Can ignore if using getOutputFormat(outputBufferId) 62 | outputFormat = codec.getOutputFormat(); // option B 63 | } 64 | } 65 | codec.stop(); 66 | codec.release(); 67 | ``` 68 | 简单解释一下,通过``getInputBuffers``获取输入队列,然后调用``dequeueInputBuffer``获取输入队列空闲数组下标,注意``dequeueOutputBuffer``会有几个特殊的返回值表示当前编解码状态的变化,然后再通过``queueInputBuffer``把原始YUV数据送入编码器,而在输出队列端同样通过``getOutputBuffers``和``dequeueOutputBuffer``获取输出的h264流,处理完输出数据之后,需要通过``releaseOutputBuffer``把输出buffer还给系统,重新放到输出队列中。
69 | 关于MediaCodec更复杂的使用例子,可以参照下CTS测试里面的使用方式:[EncodeDecodeTest.java](https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/EncodeDecodeTest.java) 70 | 71 | 从上面例子来看的确是非常原始的API,由于MediaCodec底层是直接调用了手机平台硬件的编解码能力,所以速度非常快,但是因为Google对整个Android硬件生态的掌控力非常弱,所以这个API有很多问题: 72 | 73 | 1. 颜色格式问题 74 | 75 | MediaCodec在初始化的时候,在``configure``的时候,需要传入一个MediaFormat对象,当作为编码器使用的时候,我们一般需要在MediaFormat中指定视频的宽高,帧率,码率,I帧间隔等基本信息,除此之外,还有一个重要的信息就是,指定编码器接受的YUV帧的颜色格式。这个是因为由于YUV根据其采样比例,UV分量的排列顺序有很多种不同的颜色格式,而对于Android的摄像头在``onPreviewFrame``输出的YUV帧格式,如果没有配置任何参数的情况下,基本上都是NV21格式,但Google对MediaCodec的API在设计和规范的时候,显得很不厚道,过于贴近Android的HAL层了,导致了NV21格式并不是所有机器的MediaCodec都支持这种格式作为编码器的输入格式! 76 | 因此,在初始化MediaCodec的时候,我们需要通过``codecInfo.getCapabilitiesForType``来查询机器上的MediaCodec实现具体支持哪些YUV格式作为输入格式,一般来说,起码在4.4+的系统上,这两种格式在大部分机器都有支持: 77 | 78 | ```Java 79 | MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar 80 | MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar 81 | ``` 82 | 83 | 两种格式分别是YUV420P和NV21,如果机器上只支持YUV420P格式的情况下,则需要先将摄像头输出的NV21格式先转换成YUV420P,才能送入编码器进行编码,否则最终出来的视频就会花屏,或者颜色出现错乱 84 | 85 | 这个算是一个不大不小的坑,基本上用上了MediaCodec进行视频编码都会遇上这个问题 86 | 87 | 2. 编码器支持特性相当有限 88 | 89 | 如果使用MediaCodec来编码H264视频流,对于H264格式来说,会有一些针对压缩率以及码率相关的视频质量设置,典型的诸如Profile(baseline, main, high),Profile Level, Bitrate mode(CBR, CQ, VBR),合理配置这些参数可以让我们在同等的码率下,获得更高的压缩率,从而提升视频的质量,Android也提供了对应的API进行设置,可以设置到MediaFormat中这些设置项: 90 | 91 | ```Java 92 | MediaFormat.KEY_BITRATE_MODE 93 | MediaFormat.KEY_PROFILE 94 | MediaFormat.KEY_LEVEL 95 | ``` 96 | 97 | 但问题是,对于Profile,Level, Bitrate mode这些设置,在大部分手机上都是不支持的,即使是设置了最终也不会生效,例如设置了Profile为high,最后出来的视频依然还会是Baseline.... 98 | 99 | 这个问题,在7.0以下的机器几乎是必现的,其中一个可能的原因是,Android在源码层级[hardcode](http://androidxref.com/6.0.1_r10/xref/frameworks/av/media/libstagefright/ACodec.cpp)了profile的的设置: 100 | 101 | ```Java 102 | // XXX 103 | if (h264type.eProfile != OMX_VIDEO_AVCProfileBaseline) { 104 | ALOGW("Use baseline profile instead of %d for AVC recording", 105 | h264type.eProfile); 106 | h264type.eProfile = OMX_VIDEO_AVCProfileBaseline; 107 | } 108 | ``` 109 | 110 | Android直到[7.0](http://androidxref.com/7.0.0_r1/xref/frameworks/av/media/libstagefright/ACodec.cpp)之后才取消了这段地方的Hardcode 111 | 112 | ```Java 113 | if (h264type.eProfile == OMX_VIDEO_AVCProfileBaseline) { 114 | .... 115 | } else if (h264type.eProfile == OMX_VIDEO_AVCProfileMain || 116 | h264type.eProfile == OMX_VIDEO_AVCProfileHigh) { 117 | ..... 118 | } 119 | ``` 120 | 121 | 这个问题可以说间接导致了MediaCodec编码出来的视频质量偏低,同等码率下,难以获得跟软编码甚至iOS那样的视频质量。 122 | 123 | 3. 16位对齐要求 124 | 125 | 前面说到,MediaCodec这个API在设计的时候,过于贴近HAL层,这在很多Soc的实现上,是直接把传入MediaCodec的buffer,在不经过任何前置处理的情况下就直接送入了Soc中。而在编码h264视频流的时候,由于h264的编码块大小一般是16x16,于是乎在一开始设置视频的宽高的时候,如果设置了一个没有对齐16的大小,例如960x540,在某些cpu上,最终编码出来的视频就会直接**花屏**! 126 | 127 | 很明显这还是因为厂商在实现这个API的时候,对传入的数据缺少校验以及前置处理导致的,目前来看,华为,三星的Soc出现这个问题会比较频繁,其他厂商的一些早期Soc也有这种问题,一般来说解决方法还是在设置视频宽高的时候,统一设置成对齐16位之后的大小就好了。 128 | 129 | ---- 130 | #### FFMpeg+x264/openh264 131 | 132 | 除了使用MediaCodec进行编码之外,另外一种比较流行的方案就是使用ffmpeg+x264/openh264进行软编码,ffmpeg是用于一些视频帧的预处理。这里主要是使用x264/openh264作为视频的编码器。 133 | 134 | x264基本上被认为是当今市面上最快的商用视频编码器,而且基本上所有h264的特性都支持,通过合理配置各种参数还是能够得到较好的压缩率和编码速度的,限于篇幅,这里不再阐述h264的参数配置,有兴趣可以看下[这里](https://www.nmm-hd.org/d/index.php?title=X264%E4%BD%BF%E7%94%A8%E4%BB%8B%E7%BB%8D&variant=zh-cn)和[这里](http://www.cnblogs.com/wainiwann/p/5647521.html)对x264编码参数的调优。 135 | 136 | [openh264](https://github.com/cisco/openh264)则是由思科开源的另外一个h264编码器,项目在2013年开源,对比起x264来说略显年轻,不过由于思科支付满了h264的年度专利费,所以对于外部用户来说,相当于可以直接免费使用了,另外,firefox直接内置了openh264,作为其在webRTC中的视频的编解码器使用。 137 | 138 | 但对比起x264,openh264在h264高级特性的支持比较差: 139 | 140 | - Profile只支持到baseline, level 5.2 141 | - 多线程编码只支持slice based,不支持frame based的多线程编码 142 | 143 | 从编码效率上来看,openh264的速度也并不会比x264快,不过其最大的好处,还是能够直接免费使用吧。 144 | 145 | #### 软硬编对比 146 | 147 | 从上面的分析来看,硬编的好处主要在于速度快,而且系统自带不需要引入外部的库,但是特性支持有限,而且硬编的压缩率一般偏低,而对于软编码来说,虽然速度较慢,但是压缩率比较高,而且支持的H264特性也会比硬编码多很多,相对来说比较可控。就可用性而言,在4.4+的系统上,MediaCodec的可用性是能够基本保证的,但是不同等级的机器的编码器能力会有不少差别,建议可以根据机器的配置,选择不同的编码器配置。 148 | 149 | ----- 150 | ### YUV帧的预处理 151 | 152 | 根据最开始给出的流程,在送入编码器之前,我们需要先对摄像头输出的YUV帧进行一些前置处理 153 | 154 | 1.缩放 155 | 156 | 如果设置了camera的预览大小为1080p的情况下,在``onPreviewFrame``中输出的YUV帧直接就是1920x1080的大小,如果需要编码跟这个大小不一样的视频,我们就需要在录制的过程中,**实时**的对YUV帧进行缩放。 157 | 158 | 以微信为例,摄像头预览1080p的数据,需要编码960x540大小的视频。 159 | 160 | 最为常见的做法是使用ffmpeg这种的sws_scale函数进行直接缩放,效果/性能比较好的一般是选择SWS_FAST_BILINEAR算法: 161 | 162 | ```cpp 163 | mScaleYuvCtxPtr = sws_getContext( 164 | srcWidth, 165 | srcHeight, 166 | AV_PIX_FMT_NV21, 167 | dstWidth, 168 | dstHeight, 169 | AV_PIX_FMT_NV21, 170 | SWS_FAST_BILINEAR, NULL, NULL, NULL); 171 | sws_scale(mScaleYuvCtxPtr, 172 | (const uint8_t* const *) srcAvPicture->data, 173 | srcAvPicture->linesize, 0, srcHeight, 174 | dstAvPicture->data, dstAvPicture->linesize); 175 | ``` 176 | 177 | 在nexus 6p上,直接使用ffmpeg来进行缩放的时间基本上都需要**40ms+**,对于我们需要录制30fps的来说,每帧处理时间最多就30ms左右,如果光是缩放就消耗了如此多的时间,基本上录制出来的视频只能在15fps上下了。 178 | 179 | 很明显,直接使用ffmpeg进行缩放是在是太慢了,不得不说swsscale简直就是ffmpeg里面的渣渣,在对比了几种业界常用的算之后,我们最后考虑实现使用这种快速缩放的算法: 180 | 181 | ![](assets/android_video_record/frame_compress.png) 182 | 183 | 我们选择一种叫做的**局部均值**算法,前后两行四个临近点算出最终图片的四个像素点,对于源图片的每行像素,我们可以使用Neon直接实现,以缩放Y分量为例: 184 | 185 | ```asm 186 | const uint8* src_next = src_ptr + src_stride; 187 | asm volatile ( 188 | "1: \n" 189 | "vld4.8 {d0, d1, d2, d3}, [%0]! \n" 190 | "vld4.8 {d4, d5, d6, d7}, [%1]! \n" 191 | "subs %3, %3, #16 \n" // 16 processed per loop 192 | 193 | "vrhadd.u8 d0, d0, d1 \n" 194 | "vrhadd.u8 d4, d4, d5 \n" 195 | "vrhadd.u8 d0, d0, d4 \n" 196 | 197 | "vrhadd.u8 d2, d2, d3 \n" 198 | "vrhadd.u8 d6, d6, d7 \n" 199 | "vrhadd.u8 d2, d2, d6 \n" 200 | 201 | "vst2.8 {d0, d2}, [%2]! \n" // store odd pixels 202 | 203 | "bgt 1b \n" 204 | : "+r"(src_ptr), // %0 205 | "+r"(src_next), // %1 206 | "+r"(dst), // %2 207 | "+r"(dst_width) // %3 208 | : 209 | : "q0", "q1", "q2", "q3" // Clobber List 210 | ); 211 | ``` 212 | 上面使用的Neon指令每次只能读取和存储8或者16位的数据,对于多出来的数据,只需要用同样的算法改成用C语言实现即可。 213 | 214 | 在使用上述的算法优化之后,进行每帧缩放,在Nexus 6p上,只需要不到**5ms**就能完成了,而对于缩放质量来说,ffmpeg的SWS_FAST_BILINEAR算法和上述算法缩放出来的图片进行对比,峰值信噪比(psnr)在大部分场景下大概在**38-40**左右,质量也足够好了。 215 | 216 | 2.旋转 217 | 218 | 在android机器上,由于摄像头安装角度不同,``onPreviewFrame``出来的YUV帧一般都是旋转了90或者270度,如果最终视频是要竖拍的,那一般来说需要把YUV帧进行旋转。 219 | 220 | 对于旋转的算法,如果是纯C实现的代码,一般来说是个O(n^2 ) 复杂度的算法,如果是旋转960x540的yuv帧数据,在nexus 6p上,每帧旋转也需要**30ms+**,这显然也是不能接受的。 221 | 222 | 在这里我们换个思路,能不能不对YUV帧进行旋转? 223 | 224 | 事实上在mp4文件格式的头部,我们可以指定一个旋转矩阵,具体来说是在**moov.trak.tkhd box**里面指定,视频播放器在播放视频的时候,会在读取这里矩阵信息,从而决定视频本身的旋转角度,位移,缩放等,具体可以参考下苹果的[文档](https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap4/qtff4.html#//apple_ref/doc/uid/TP40000939-CH206-18737) 225 | 226 | 通过ffmpeg,我们可以很轻松的给合成之后的mp4文件打上这个旋转角度: 227 | 228 | ```cpp 229 | char rotateStr[1024]; 230 | sprintf(rotateStr, "%d", rotate); 231 | av_dict_set(&out_stream->metadata, "rotate", rotateStr, 0); 232 | ``` 233 | 234 | 于是可以在录制的时候省下一大笔旋转的开销了,excited! 235 | 236 | 237 | 3.镜像 238 | 239 | 在使用前置摄像头拍摄的时候,如果不对YUV帧进行处理,那么直接拍出来的视频是会**镜像翻转**的,这里原理就跟照镜子一样,从前置摄像头方向拿出来的YUV帧刚好是反的,但有些时候拍出来的镜像视频可能不合我们的需求,因此这个时候我们就需要对YUV帧进行镜像翻转。 240 | 241 | 但由于摄像头安装角度一般是90或者270度,所以实际上原生的YUV帧是水平翻转过来的,因此做镜像翻转的时候,只需要刚好以中间为中轴,分别上下交换每行数据即可,注意Y跟UV要分开处理,这种算法用Neon实现相当简单: 242 | 243 | ```asm 244 | asm volatile ( 245 | "1: \n" 246 | "vld4.8 {d0, d1, d2, d3}, [%2]! \n" // load 32 from src 247 | "vld4.8 {d4, d5, d6, d7}, [%3]! \n" // load 32 from dst 248 | "subs %4, %4, #32 \n" // 32 processed per loop 249 | "vst4.8 {d0, d1, d2, d3}, [%1]! \n" // store 32 to dst 250 | "vst4.8 {d4, d5, d6, d7}, [%0]! \n" // store 32 to src 251 | "bgt 1b \n" 252 | : "+r"(src), // %0 253 | "+r"(dst), // %1 254 | "+r"(srcdata), // %2 255 | "+r"(dstdata), // %3 256 | "+r"(count) // %4 // Output registers 257 | : // Input registers 258 | : "cc", "memory", "q0", "q1", "q2", "q3" // Clobber List 259 | ); 260 | ``` 261 | 262 | 同样,剩余的数据用纯C代码实现就好了, 在nexus6p上,这种镜像翻转一帧1080x1920 YUV数据大概只要不到**5ms** 263 | 264 | ----- 265 | 266 | 在编码好h264视频流之后,最终处理就是把音频流跟视频流合流然后包装到mp4文件,这部分我们可以通过系统的[MediaMuxer](https://developer.android.com/reference/android/media/MediaMuxer.html),[mp4v2](https://code.google.com/archive/p/mp4v2/),或者ffmpeg来实现,这部分比较简单,在这里就不再阐述了 267 | 268 | ----- 269 | 270 | ### References 271 | 272 | 1. [雷霄骅(leixiaohua1020)的专栏](http://blog.csdn.net/leixiaohua1020) ,大名鼎鼎雷神的博客,里面有非常多关于音视频编码/ffmpeg相关的学习资料,入门必备。也祝愿他能够在天堂安息吧 273 | 2. [Android MediaCodec stuff](http://bigflake.com/mediacodec/),包含了一些MediaCodec使用的示例代码,初次使用可以参考下这里 274 | 3. [Coding for NEON](https://community.arm.com/processors/b/blog/posts/coding-for-neon---part-1-load-and-stores),一个系列教程,讲述了一些常用Neon指令使用方法。上面在介绍缩放的时候使用到了Neon,事实上大部分音视频处理过程都会使用到,以YUV帧处理为例,缩放,旋转,镜像翻转都可以使用neon来做优化 275 | 4. [libyuv](https://chromium.googlesource.com/libyuv/libyuv/),Google开源的一个YUV处理库,上面只针对1080p->540p视频帧缩放的算法,而对于通用的压缩处理,可以直接使用这里的实现,对比起ffmpeg的速度快上不少 276 | 277 | -------------------------------------------------------------------------------- /微信Mars — 移动互联网下的高质量网络连接探索.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/微信Mars — 移动互联网下的高质量网络连接探索.pdf -------------------------------------------------------------------------------- /微信Tinker的一切都在这里,包括源码(一).md: -------------------------------------------------------------------------------- 1 | # 微信Tinker的一切都在这里,包括源码(一) # 2 | 最近半年以来,Android热补丁技术热潮继续爆发,各大公司相继推出自己的开源框架。Tinker在最近也顺利完成了公司的审核,并非常荣幸的成为github.com/Tencent上第一个正式公开的项目。 3 | 4 | 回顾这半年多的历程,这是一条跪着走完,坑坑不息之路。或许只有自己真正经历过,深入研究过, 才会真正的明白 5 | > 热补丁不是请客吃饭 6 | 7 | 对热补丁技术本身,还是对使用者来说都是如此。它并不简单,也有着自己的局限性,在使用之前我们需要对它有所了解。我希望通过分享微信在这历程中的思考与经验,能帮助大家更容易的决定是否在自己的项目中使用热补丁技术,以及选择什么样方案。 8 | 9 | ## 热补丁技术背景 ## 10 | 热补丁是什么以及它的应用场景介绍,大家可以参考文章[微信Android热补丁实践演进之路](http://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286306&idx=1&sn=d6b2865e033a99de60b2d4314c6e0a25#rd)。 11 | 12 | 在笔者看来Android热补丁技术应该分为以下两个流派: 13 | 14 | * Native,代表有阿里的Dexposed、AndFix与腾讯的内部方案KKFix; 15 | * Java, 代表有Qzone的超级补丁、大众点评的nuwa、百度金融的rocooFix, 饿了么的amigo以及美团的robust。 16 | 17 | Native流派与Java流派都有着自己的优缺点,它们具体差异大家可参考上文。事实上从来都没有最好的方案,只有最适合自己的。 18 | 19 | 对于微信来说,我们希望得到一个“高可用”的补丁框架,它应该满足以下几个条件: 20 | 21 | 1. **稳定性与兼容性**;微信需要在数亿台设备上运行,即使补丁框架带来1%的异常,也将影响到数万用户。保证补丁框架的稳定性与兼容性是我们的第一要务; 22 | 2. **性能**;微信对性能要求也非常苛刻,首先补丁框架不能影响应用的性能,这里基于大部分情况下用户不会使用到补丁。其次补丁包应该尽量少,这关系到用户流量与补丁的成功率问题; 23 | 3. **易用性**;在解决完以上两个核心问题的前提下,我们希望补丁框架简单易用,并且可以全面支持,甚至可以做到功能发布级别。 24 | 25 | 在“高可用”这个大前提下,微信对当时存在的两个方案做了大量的研究: 26 | 27 | 1. Dexposed/AndFix;最大挑战在于稳定性与兼容性,而且native异常排查难度更高。另一方面,由于无法增加变量与类等限制,无法做到功能发布级别; 28 | 2. Qzone;最大挑战在于性能,即Dalvik平台存在插桩导致的性能损耗,Art平台由于地址偏移问题导致补丁包可能过大的问题; 29 | 30 | 在2016年3月,微信为了追寻“高可用”这个目标,决定尝试搭建自己的补丁框架——Tinker。Tinker框架的演绎并不是一蹴而就,它大致分为三个阶段,每一阶段需要解决的核心问题并不相同。而Tinker v1.0的核心问题是实现符合性能要求的Dex补丁框架。 31 | 32 | ## Tinker v1.0-性能极致追求之路 ## 33 | 为了稳定性与兼容性,微信选择了Java流派。当前最大难点在于如何突破Qzone方案的性能问题,这时通过研究Instant Run的冷插拔与buck的[exopackage](https://buckbuild.com/article/exopackage.html)给了我们灵感。它们的思想都是全量替换新的Dex。 34 | 35 | ![](assets/tinker-open/tinker.png) 36 | 37 | 简单来说,我们通过完全使用了新的Dex,那样既不出现Art地址错乱的问题,在Dalvik也无须插桩。当然考虑到补丁包的体积,我们不能直接将新的Dex放在里面。但我们可以将新旧两个Dex的差异放到补丁包中,这里我们可以调研的方法有以下几个: 38 | 39 | ![](assets/tinker-open/dex-method.jpg) 40 | 41 | 1. BsDiff;它格式无关,但对Dex效果不是特别好,而且非常不稳定。当前微信对于so与部分资源,依然使用bsdiff算法; 42 | 2. DexMerge;它主要问题在于合成时内存占用过大,一个12M的dex,峰值内存可能达到70多M; 43 | 3. DexDiff;通过深入Dex格式,实现一套diff差异小,内存占用少以及支持增删改的算法。 44 | 45 | 如何选择?在“高可用”的核心诉求下,性能问题也尤为重要。非常庆幸微信在当时那个节点坚决的选择了自研DexDiff算法,这过程虽然有苦有泪,但也正是有它,才有现在的Tinker。 46 | 47 | ### 一. DexDiff技术实践 ### 48 | 49 | 在不断的深入研究[Dex格式](https://source.android.com/devices/tech/dalvik/dex-format.html )后,我们发现自己跳进了一个深坑,主要难点有以下三个: 50 | 1. Dex格式复杂;Dex大致分为像StringID,TypeID这些Index区域以及使用Offset的Data区域。它们有大量的互相引用,一个小小的改变可能导致大量的Index与Offset变化; 51 | 2. dex2opt与dex2oat校验;在这两个过程系统会做例如四字节对齐,部分元素排序等校验,例如StringID按照内容的Unicode排序,TypeID按照StringID排序... 52 | 3. 低内存,快速;这要求我们对Dex每一块做到一次读写,无法像baksmali与dexmerge那样完全结构化。 53 | ![](assets/tinker-open/dex-format.png) 54 | 55 | 这不仅要求我们需要研究透Dex的格式,也要把dex2opt与dex2oat的代码全部研究透。现在回想起来,这的确是一条跪着走完的路。与研究Dalvik与Art执行一致,这是经历一次次翻看源码,一次次编Rom查看日志,一次次dump内存结构换来的结果。 56 | 57 | 下面以最简单的Index区域举例: 58 | 59 | ![](assets/tinker-open/dex-diff.jpg) 60 | 61 | 要想将从左边序列更改成右边序列,Diff算法的核心在于如何生成最小操作序列,同时修正Index与Offset,实现增删改的功能。 62 | 63 | 1. Del 2;"b"元素被删除,它对应的Index是2,为了减少补丁包体积,除了新增的元素其他一律只存Index; 64 | 2. "c", "d", "e"元素自动前移,无须操作; 65 | 3. Addf(5); 在第五个位置增加"f"这个元素。 66 | 67 | 对于Offset区,由于每个Section可能有非常多的元素,这里会更加复杂。最后我们得到最终的操作队列,为什么DexDiff可以做到内存非常少?这是因为DexDiff算法是每一个操作的处理,它无需一次性读入所有的数据。DexDiff的各项数据如下: 68 | 69 | ![](assets/tinker-open/dex-result.png) 70 | 71 | 通过DexDiff算法的实现,我们既解决了Dalvik平台的性能损耗问题,又解决了Art平台补丁包过大的问题。但这套方案的缺点在于占Rom体积比较大,微信考虑到移动设备的存储空间提升比较快,增加几十M的Rom空间这个代价可以接受。 72 | 73 | ### 二. Android N的挑战 ### 74 | 信心满满上线后,却很快收到华为反馈的一个Crash: 75 | 76 | ![](assets/tinker-open/androidn.png) 77 | 78 | 而且这个Crash只在Android N上出现,在当时对我们震动非常大,难道Android N不支持Java方式热补丁了?难道这两个月的辛苦都白费了吗?一切想象都苍白无力,只有继续去源码里面找原因。 79 | 80 | 在之前的基础上,这一块的研究并没有花太多的时间,主要是Android N的混合编译模式导致。更多的详细分析可参考文章[Android N混合编译与对热补丁影响解析](http://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286341&idx=1&sn=054d595af6e824cbe4edd79427fc2706#rd)。 81 | 82 | ### 三. 厂商OTA的挑战 ### 83 | 84 | 刚刚解决完Android N的问题,还在沉醉在自己的胜利的愉悦中。前线很快又传来噩耗,小米反馈开发版的一些用户在微信启动时黑屏,甚至ANR. 85 | 86 | ![](assets/tinker-open/dex-anr.png) 87 | 88 | 当时第一反应是不可能,所有的DexOpt操作都是放到单独的进程,为什么只在Art平台出现?为什么小米开发版用户反馈比较多?经过分析,我们发现优化后odex文件存在有效性的检查: 89 | 90 | * Dalvik平台:modtime/crc... 91 | * Art平台: checksum/image_checksum/image_offset... 92 | 93 | 这就非常好理解了,因为OTA之后系统image改变了,odex文件用到image的偏移地址很可能已经错误。对于ClassN.dex文件,在OTA升级系统已完成重新dex2oat,而补丁是动态加载的,只能在第一次执行时同步执行。 94 | 95 | 这个耗时可能高达十几秒,黑屏甚至ANR也是非常好理解。那为什么只有小米用户反馈比较多呢?这也是因为小米开发版每周都会推送系统升级的原因。 96 | 97 | 在当时那个节点上,我们重新的审视了全量合成这一思路,再次对方案原理本身产生怀疑,它在Art平台上面带来了以下几个代价: 98 | 99 | 1. OTA后黑屏问题;这里或许可以通过lLoading界面实现,但并不是很好的方案; 100 | 2. Rom体积问题;一个10M的Dex,在Dalvik下odex产物只有11M左右,但在Art平台,可以达到30多M; 101 | 3. Android N的问题;Android N在混合编译上努力,被补丁全量合成机制所废弃了。这是因为动态加载的Dex,依然是全量编译。 102 | 103 | 回想起来,Qzone方案它只把需要的类打包成补丁推送,在Art平台上可能导致补丁很大,但它肯定比全量合成10M的Dex少很多很多。在此我们提出分平台合成的想法,即在Dalvik平台合成全量Dex,在Art平台合成需要的Dex 104 | 105 | ![](assets/tinker-open/dex-art.png) 106 | 107 | DexDiff算法已经非常复杂,事实上要实现分平台合成并不容易。 108 | 109 | ![](assets/tinker-open/dex-merge.jpg) 110 | 111 | 主要难点有以下几个方面: 112 | 113 | * small dex的类收集;什么类应该放在这个小的Dex中呢? * ClassN处理;对于ClassN怎么样处理,可能出现类从一个Dex移动到另外一个Dex? * 偏移二次修正; 补丁包中的操作序列如何二次修正? * Art.info的大小; 为了修正偏移所引入的info文件的大小? 114 | 115 | 庆幸的是,面对困难我们并没有畏惧,最后实现了这一套方案,这也是其他全量合成方案所不能做到的: 116 | 117 | 1. Dalvik全量合成,解决了插桩带来的**性能损耗**; 2. Art平台合成small dex,解决了**全量合成方案占用Rom体积大, OTA升级以及Android N的问题**; 118 | 3. 大部分情况下Art.info仅仅1-20K, **解决由于补丁包可能过大的问题**; 119 | 120 | 事实上,DexDiff算法变的如此复杂,怎么样保证它的正确性呢?微信为此做了以下三件事情: 121 | 122 | 1. 随机组成Dex校验,覆盖大部分case; 123 | 2. 微信200个版本的随机Diff校验, 覆盖日常使用情况; 124 | 3. Dex文件合成产物有效性校验,即使算法出现问题,也只是编译不出补丁包。 125 | 每一次DexDiff算法的更新,都需要经过以上三个Test才可以提交,这样DexDiff的这套算法已完成了整个闭环。 126 | 127 | ### 四. 其他技术挑战 ### 128 | 在实现过程,我们还发现其他的一些问题: 129 | 130 | 1. Xposed等微信插件; 市面上有各种各样的微信插件,它们在微信启动前会提前加载微信中的类,这会导致两个问题: 131 | 132 | a. Dalvik平台:出现**Class ref in pre-verified class resolved to unexpected implementation**的crash; 133 | 134 | b. Art平台:出现部分类使用了旧的代码,这可能导致补丁无效,或者地址错乱的问题。 135 | 136 | **微信在这里的处理方式是若crash时发现安装了Xposed,即清除并不再应用补丁。** 137 | 138 | 2. Dex反射成功但是不生效;部分三星android-19版本存在Dex反射成功,但出现类重复时,查找顺序始终从base.apk开始。 139 | **微信在这里的处理方式是增加Dex反射成功校验,具体通过在框架中埋入某个类的isPatch变量为false。在补丁时,我们自动将这个变量改为true。通过这个变量最终的数值,我们可以知道反射成功与否。** 140 | 141 | 142 | ## Tinker v1.0总结 ## 143 | ### 一. 关于性能 ### 144 | 145 | 通过Tinker v1,0的努力,我们解决了Qzone方案的性能问题,得到一个符合“高可用”性能要求的补丁框架。 146 | 147 | * 它补丁包大小非常少,通常都是10k以内; 148 | * 对性能几乎没有影响, 2%的性能影响主要原因是微信运行时校验补丁Dex文件的md5导致(虽然文件在/data/data/目录,微信为了更高级别的安全); 149 | * Art平台通过革命性的分平台合成,既解决了地址偏移的问题,占Rom体积与Qzone方案一致。 150 | 151 | ![](assets/tinker-open/section1.jpg) 152 | 153 | ### 二. 关于成功率 ### 154 | 155 | 也许有人会质疑微信成功率为什么这么低,其他方案都是99%以上。事实上,我们的成功率计算方式是: 156 | > 应用成功率= 补丁版本转化人数/基准版本安装人数 157 | 158 | 即三天后,94.1%的基础版本都成功升级到补丁版本,由于基础版本人数也是持续增长,同时可能存在基准或补丁版本用户安装了其他版本,所以本统计结果应略为偏低,但它能现实的反应补丁的线上总体覆盖情况。 159 | 160 | 事实上,采用Qzone方案,3天的成功率大约为96.3%,这里还是有很多的优化空间。 161 | 162 | ### 三. Tinker v2.0-稳定性的探寻之路 ### 163 | 164 | 在v1.0阶段,大部分的异常都是通过厂商反馈而来,Tinker并没有解决“高可用”下最核心的稳定性与兼容性问题。我们需要建立完整的监控与补丁回退机制,监控每一个阶段的异常情况。这也是Tinker v2.0的核心任务,由于边幅问题这部分内容将放在下一篇文章。 165 | 166 | --- 167 | 关注Tinker,来Github给我们star吧 168 | >[https://github.com/Tencent/tinker](https://github.com/Tencent/tinker) -------------------------------------------------------------------------------- /微信iOS SQLite源码优化实践.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 随着微信iOS客户端业务的增长,在数据库上遇到的性能瓶颈也逐渐凸显。在微信的卡顿监控系统上,数据库相关的卡顿不断上升。而在用户侧也逐渐能感知到这种卡顿,尤其是有大量群聊、联系人和消息收发的重度用户。 4 | 5 | 我们在对SQLite进行优化的过程中发现,靠单纯地修改SQLite的参数配置,已经不能彻底解决问题。因此从6.3.16版本开始,我们合入了SQLite的源码,并开始进行源码层的优化。 6 | 7 | 本文将分享在SQLite源码上进行的多线程并发、I/O性能优化等,并介绍优化相关的SQLite原理。 8 | 9 | ## 多线程并发优化 10 | 11 | #### 1. 背景 12 | 13 | 由于历史原因,旧版本的微信一直使用单句柄的方案,即所有线程共有一个SQLite Handle,并用线程锁避免多线程问题。当多线程并发时,各线程的数据库操作同步顺序进行,这就导致后来的线程会被阻塞较长的时间。 14 | 15 | #### 2. SQLite的多句柄方案及Busy Retry方案 16 | 17 | SQLite实际是支持多线程(几乎)无锁地并发操作。只需 18 | 19 | 1. 开启配置 `PRAGMA SQLITE_THREADSAFE=2` 20 | 2. 确保同一个句柄同一时间只有一个线程在操作 21 | 22 | > [Multi-thread. In this mode, SQLite can be safely used by multiple threads provided that no single database connection is used simultaneously in two or more threads.](http://www.sqlite.org/threadsafe.html) 23 | 24 | 倘若再开启SQLite的WAL模式(Write-Ahead-Log),多线程的并发性将得到进一步的提升。 25 | 26 | 此时写操作会先append到wal文件末尾,而不是直接覆盖旧数据。而读操作开始时,会记下当前的WAL文件状态,并且只访问在此之前的数据。这就确保了多线程**读与读**、**读与写**之间可以并发地进行。 27 | 28 | 然而,阻塞的情况并非不会发生。 29 | 30 | * 当多线程写操作并发时,后来者还是必须在源码层等待之前的写操作完成后才能继续。 31 | 32 | SQLite提供了Busy Retry的方案,即发生阻塞时,会触发Busy Handler,此时可以让线程休眠一段时间后,重新尝试操作。重试一定次数依然失败后,则返回`SQLITE_BUSY`错误码。 33 | 34 | ```c 35 | static int sqliteDefaultBusyCallback( 36 | void *ptr, /* Database connection */ 37 | int count /* Number of times table has been busy */ 38 | ){ 39 | sqlite3 *db = (sqlite3 *)ptr; 40 | int timeout = ((sqlite3 *)ptr)->busyTimeout; 41 | if( (count+1)*1000 > timeout ){ 42 | return 0; 43 | } 44 | sqlite3OsSleep(db->pVfs, 1000000); 45 | return 1; 46 | } 47 | ``` 48 | 49 | #### 3. SQLite Busy Retry方案的不足 50 | 51 | Busy Retry的方案虽然基本能解决问题,但对性能的压榨做的不够极致。在Retry过程中,休眠时间的长短和重试次数,是决定性能和操作成功率的关键。 52 | 53 | 然而,它们的最优值,因不同操作不同场景而不同。若休眠时间太短或重试次数太多,会空耗CPU的资源;若休眠时间过长,会造成等待的时间太长;若重试次数太少,则会降低操作的成功率。 54 | 55 | ![原生方案的不足](assets/ios_sql/old-schema.png) 56 | 57 | 我们通过A/B Test对不同的休眠时间进行了测试,得到了如下的结果: 58 | 59 | ![成功率曲线](assets/ios_sql/trend.png) 60 | 61 | 可以看到,倘若休眠时间与重试成功率的关系,按照绿色的曲线进行分布,那么p点的值也不失为该方案的一个次优解。然而事总不遂人愿,我们需要一个更好的方案。 62 | 63 | #### 4. SQLite中的线程锁及进程锁 64 | 65 | 作为有着十几年发展历史、且被广泛认可的数据库,SQLite的任何方案选择都是有其原因的。在完全理解由来之前,切忌盲目自信、直接上手修改。因此,首先要了解SQLite是如何控制并发的。 66 | 67 | ![SQLite架构](assets/ios_sql/SQLite-Arch.png) 68 | 69 | SQLite是一个适配不同平台的数据库,不仅支持多线程并发,还支持多进程并发。它的核心逻辑可以分为两部分: 70 | 71 | * Core层。包括了接口层、编译器和虚拟机。通过接口传入SQL语句,由编译器编译SQL生成虚拟机的操作码opcode。而虚拟机是基于生成的操作码,控制Backend的行为。 72 | * Backend层。由B-Tree、Pager、OS三部分组成,实现了数据库的存取数据的主要逻辑。 73 | 74 | 在架构最底端的OS层是对不同操作系统的系统调用的抽象层。它实现了一个VFS(Virtual File System),将OS层的接口在编译时映射到对应操作系统的系统调用。锁的实现也是在这里进行的。 75 | 76 | SQLite通过两个锁来控制并发。第一个锁对应DB文件,通过5种状态进行管理;第二个锁对应WAL文件,通过修改一个16-bit的unsigned short int的每一个bit进行管理。尽管锁的逻辑有一些复杂,但此处并不需关心。这两种锁最终都落在OS层的`sqlite3OsLock`、`sqlite3OsUnlock`和`sqlite3OsShmLock`上具体实现。 77 | 78 | 它们在锁的实现比较类似。以lock操作在iOS上的实现为例: 79 | 80 | 1. 通过`pthread_mutex_lock`进行线程锁,防止其他线程介入。然后比较状态量,若当前状态不可跳转,则返回`SQLITE_BUSY` 81 | 2. 通过`fcntl`进行文件锁,防止其他进程介入。若锁失败,则返回`SQLITE_BUSY` 82 | 83 | 而SQLite选择Busy Retry的方案的原因也正是在此---**文件锁没有线程锁类似pthread_cond_signal的通知机制。当一个进程的数据库操作结束时,无法通过锁来第一时间通知到其他进程进行重试。因此只能退而求其次,通过多次休眠来进行尝试。** 84 | 85 | #### 5. 新的方案 86 | 87 | 通过上面的各种分析、准备,终于可以动手开始修改了。 88 | 89 | 我们知道,iOS app是单进程的,并**没有多进程并发的需求**,这和SQLite的设计初衷是不相同的。这就给我们的优化提供了理论上的基础。在iOS这一特定场景下,我们可以舍弃兼容性,提高并发性。 90 | 91 | 新的方案修改为,当OS层进行lock操作时: 92 | 93 | 1. 通过`pthread_mutex_lock`进行线程锁,防止其他线程介入。然后比较状态量,若当前状态不可跳转,则将当前期望跳转的状态,插入到一个FIFO的Queue尾部。最后,线程通过`pthread_cond_wait`进入 休眠状态,等待其他线程的唤醒。 94 | 2. 忽略文件锁 95 | 96 | 当OS层的unlock操作结束后: 97 | 98 | 1. 取出Queue头部的状态量,并比较状态是否能够跳转。若能够跳转,则通过`pthread_cond_signal_thread_np`唤醒对应的线程重试。 99 | 100 | > `pthread_cond_signal_thread_np`是Apple在pthread库中新增的接口,与`pthread_cond_signal`类似,它能唤醒一个等待条件锁的线程。不同的是,`pthread_cond_signal_thread_np`可以指定一个特定的线程进行唤醒。 101 | 102 | ![新的方案](assets/ios_sql/new-schema.png) 103 | 104 | 新的方案可以在DB空闲时的第一时间,通知到其他正在等待的线程,最大程度地降低了空等待的时间,且准确无误。此外,由于Queue的存在,当主线程被其他线程阻塞时,可以将主线程的操作“插队”到Queue的头部。当其他线程发起唤醒通知时,主线程可以有更高的优先级,从而降低用户可感知的卡顿。 105 | 106 | 该方案上线后,卡顿检测系统检测到 107 | 108 | * 等待线程锁的造成的卡顿下降超过90% 109 | 110 | 111 | * SQLITE_BUSY的发生次数下降超过95% 112 | 113 | ![等锁卡顿](assets/ios_sql/lag-wait-lock.png) 114 | 115 | ![朋友圈Busy](assets/ios_sql/timeline-busy.png) 116 | 117 | ## I/O 性能优化 118 | 119 | #### 保留WAL文件大小 120 | 121 | 如上文多线程优化时提到,开启WAL模式后,写入的数据会先append到WAL文件的末尾。待文件增长到一定长度后,SQLite会进行checkpoint。这个长度默认为1000个页大小,在iOS上约为3.9MB。 122 | 123 | 同样的,在数据库关闭时,SQLite也会进行checkpoint。不同的是,checkpoint成功之后,会将WAL文件长度删除或truncate到0。下次打开数据库,并写入数据时,WAL文件需要重新增长。而对于文件系统来说,这就意味着需要**消耗时间重新寻找合适的文件块**。 124 | 125 | 显然SQLite的设计是针对容量较小的设备,尤其是在十几年前的那个年代,这样的设备并不在少数。而随着硬盘价格日益降低,对于像iPhone这样的设备,几MB的空间已经不再是需要斤斤计较的了。 126 | 127 | 因此我们可以修改为: 128 | 129 | * 数据库关闭并checkpoint成功时,不再truncate或删除WAL文件只修改WAL的文件头的Magic Number。下次数据库打开时,SQLite会识别到WAL文件不可用,重新从头开始写入。 130 | 131 | > 保留WAL文件大小后,每个数据库都会有这约3.9MB的额外空间占用。如果数据库较多,这些空间还是不可忽略的。因此,微信中目前只对读写频繁且检测到卡顿的数据库开启,如聊天记录数据库。 132 | 133 | #### mmap优化 134 | 135 | mmap对I/O性能的提升无需赘言,尤其是对于读操作。SQLite也在OS层封装了mmap的接口,可以无缝地切换mmap和普通的I/O接口。只需配置`PRAGMA mmap_size=XXX`即可开启mmap。 136 | 137 | > [There are advantages and disadvantages to using memory-mapped I/O. Advantages include:](https://www.sqlite.org/mmap.html) 138 | > 139 | > [Many operations, especially I/O intensive operations, can be much faster since content does need to be copied between kernel space and user space. In some cases, performance can nearly double.](https://www.sqlite.org/mmap.html) 140 | > 141 | > [The SQLite library may need less RAM since it shares pages with the operating-system page cache and does not always need its own copy of working pages.](https://www.sqlite.org/mmap.html) 142 | 143 | 然而,你在iOS上这样配置恐怕不会有任何效果。因为早期的iOS版本的存在一些bug,SQLite在编译层就关闭了在iOS上对mmap的支持,并且后知后觉地在[16年1月才重新打开](http://www.sqlite.org/src/info/e9a51d2a580daa0f)。所以如果使用的SQLite版本较低,还需注释掉相关代码后,重新编译生成后,才可以享受上mmap的性能。 144 | 145 | ![SQLite开启iOS mmap](assets/ios_sql/sqlite-ios-mmap.png) 146 | 147 | 开启mmap后,SQLite性能将有所提升,但这还不够。因为它只会对DB文件进行了mmap,而WAL文件享受不到这个优化。 148 | 149 | WAL文件长度是可能变短的,而在多句柄下,对WAL文件的操作是并行的。一旦某个句柄将WAL文件缩短了,而没有一个通知机制让其他句柄进行更新mmap的内容。此时其他句柄若使用mmap操作已被缩短的内容,就会造成crash。而普通的I/O接口,则只会返回错误,不会造成crash。因此,SQLite没有实现对WAL文件的mmap。 150 | 151 | 还记得我们上一个优化吗?没错,我们保留了WAL文件的大小。因此它在这个场景下是不会缩短的,那么不能mmap的条件就被打破了。实现上,只需在WAL文件打开时,用`unixMapfile`将其映射到内存中,SQLite的OS层即会自动识别,将普通的I/O接口切换到mmap上。 152 | 153 | ## 其他优化 154 | 155 | #### 禁用文件锁 156 | 157 | 如我们在多线程优化时所说,对于iOS app并没有多进程的需求。因此我们可以直接注释掉`os_unix.c`中所有文件锁相关的操作。也许你会很奇怪,虽然没有文件锁的需求,但这个操作耗时也很短,是否有必要特意优化呢?其实并不全然。耗时多少是比出来。 158 | 159 | SQLite中有cache机制。被加载进内存的page,使用完毕后不会立刻释放。而是在一定范围内通过LRU的算法更新page cache。这就意味着,如果cache设置得当,大部分读操作不会读取新的page。然而因为文件锁的存在,本来只需在内存层面进行的读操作,不得不进行至少一次I/O操作。而我们知道,I/O操作是远远慢于内存操作的。 160 | 161 | #### 禁用内存统计锁 162 | 163 | SQLite会对申请的内存进行统计,而这些统计的数据都是放到同一个全局变量里进行计算的。这就意味着统计前后,都是需要加线程锁,防止出现多线程问题的。 164 | 165 | ```c 166 | void *sqlite3Malloc(u64 n){ 167 | void *p; 168 | if( n==0 || n>=0x7fffff00 ){ 169 | /* A memory allocation of a number of bytes which is near the maximum 170 | ** signed integer value might cause an integer overflow inside of the 171 | ** xMalloc(). Hence we limit the maximum size to 0x7fffff00, giving 172 | ** 255 bytes of overhead. SQLite itself will never use anything near 173 | ** this amount. The only way to reach the limit is with sqlite3_malloc() */ 174 | p = 0; 175 | }else if( sqlite3GlobalConfig.bMemstat ){ 176 | sqlite3_mutex_enter(mem0.mutex); 177 | mallocWithAlarm((int)n, &p); 178 | sqlite3_mutex_leave(mem0.mutex); 179 | }else{ 180 | p = sqlite3GlobalConfig.m.xMalloc((int)n); 181 | } 182 | assert( EIGHT_BYTE_ALIGNMENT(p) ); /* IMP: R-11148-40995 */ 183 | return p; 184 | } 185 | ``` 186 | 187 | 内存申请虽然不是非常耗时的操作,但却很频繁。多线程并发时,各线程很容易互相阻塞。 188 | 189 | 阻塞虽然也很短暂,但频繁地切换线程,却是个很影响性能的操作,尤其是单核设备。 190 | 191 | 因此,如果不需要内存统计的特性,可以通过`sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0) `进行关闭。这个修改虽然不需要改动源码,但如果不查看源码,恐怕是比较难发现的。 192 | 193 | 优化上线后,卡顿监控系统监测到 194 | 195 | * DB写操作造成的卡顿下降超过80% 196 | 197 | 198 | * DB读操作造成的卡顿下降超过85% 199 | 200 | ![db读写卡顿](assets/ios_sql/lag-rw.png) 201 | 202 | ## 结语 203 | 204 | 移动客户端数据库虽然不如后台数据库那么复杂,但也存在着不少可挖掘的技术点。本次尝试了仅对SQLite原有的方案进行优化,而市面上还有许多优秀的数据库,如LevelDB、RocksDB、Realm等,它们采用了和SQLite不同的实现原理。后续我们将借鉴它们的优化经验,尝试更深入的优化。 -------------------------------------------------------------------------------- /微信客户端怎样应对弱网络.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeMobileDev/article/6d3fd6a962b54fbb8d870bfcc0175cd13a71a387/微信客户端怎样应对弱网络.pdf -------------------------------------------------------------------------------- /微信移动端数据库组件WCDB系列(一)-iOS基础篇.md: -------------------------------------------------------------------------------- 1 | 2 | 前言 3 | == 4 | 5 | **WCDB**(**W**e**C**hat **D**ata**B**ase)是微信官方的移动端数据库组件,致力于提供一个**高效**、**易用**、**完整**的移动端存储方案。 6 | 7 | 它包含三个模块: 8 | 9 | * WCDB-iOS/Mac 10 | * WCDB-Android 11 | * 数据库损坏修复工具WCDBRepair 12 | 13 | 目前正在筹备开源中。 14 | 15 | 16 | 背景 17 | == 18 | 19 | 对于iOS开发者来说,数据库的技术选型一直是个令人头痛的问题。由于Apple提供的CoreData框架差强人意,使得开发者们纷纷将目光投向开源社区,寻找更好的存储方案。 20 | 21 | 对于微信也是如此。数据库是微信内最基础的组件之一,消息收发、联系人、朋友圈等等业务都离不开数据库的支持。为了满足需求,我们也对现有方案做了对比研究: 22 | 23 | 目前移动端数据库方案按其实现可分为两类, 24 | 25 | * **关系型数据库**,代表有 26 | * **CoreData**。它是苹果内建框架,和Xcode深度结合,可以很方便进行ORM;但其上手学习成本较高,不容易掌握。稳定性也堪忧,很容易crash;多线程的支持也比较鸡肋。 27 | * **FMDB**。它基于SQLite封装,对于有SQLite和ObjC基础的开发者来说,简单易懂,可以直接上手;而缺点也正是在此,FMDB只是将SQLite的C接口封装成了ObjC接口,没有做太多别的优化,即所谓的胶水代码(Glue Code)。使用过程需要用大量的代码拼接SQL、拼装Object,并不方便。 28 | 29 | * **key-value数据库**。代表有Realm、LevelDB、RocksDB等。 30 | * **Realm**。因其在各平台封装、优化的优势,比较受移动开发者的欢迎。对于iOS开发者,key-value的实现直接易懂,可以像使用`NSDictionary`一样使用Realm。并且ORM彻底,省去了拼装Object的过程。但其对代码侵入性很强,Realm要求类继承RLMObject的基类。这对于单继承的ObjC,意味着不能再继承其他自定义的子类。同时,key-value数据库对较为复杂的查询场景也比较无力。 31 | 32 | 可见,各个方案都有其独特的优势及劣势,没有最好的,只有最适合的。 33 | 34 | 而对于微信来说,我们所期望的数据库应满足: 35 | 36 | * **高效**;增删改查的高效是数据库最基本的要求。除此之外,我们还希望能够支持多个线程高并发地操作数据库,以应对微信频繁收发消息的场景。 37 | * **易用**;这是微信开源的原则,也是WCDB的原则。SQLite本不是一个易用的组件:为了完成一个查询,往往我们需要写很多拼接字符串、组装Object的胶水代码。这些代码冗长繁杂,而且容易出错,我们希望组件能统一完成这些任务。 38 | * **完整**;数据库操作是一个复杂的场景,我们希望数据库组件能完整覆盖各种场景。包括数据库损坏、监控统计、复杂的查询、反注入等。 39 | 40 | 显然,上述各个方案都不能完全满足微信的需求。 41 | 42 | 于是,我们造了这个“轮子” - **WCDB-iOS/Mac** 43 | 44 | 45 | WCDB-iOS/Mac 46 | ============ 47 | 48 | WCDB-iOS/Mac(以下简称WCDB,均指代WCDB的iOS/Mac版本),是一个基于SQLite封装的Objective-C++数据库组件,提供了如下功能: 49 | 50 | * **便捷的ORM和CRUD接口**:通过WCDB,开发者可以便捷地定义数据库表和索引,并且无须写一坨胶水代码拼装对象。 51 | * **WINQ(WCDB语言集成查询)**:通过WINQ,开发者无须拼接字符串,即可完成SQL的条件、排序、过滤等等语句。 52 | * **支持多线程高并发**:基本的增删查改等接口都支持多线程访问,开发者无需操心线程安全问题。 53 | * 线程间读与读、读与写操作均支持并发执行。 54 | * 写与写操作串行执行,并且有基于SQLite源码优化的性能提升。可参考我们分享的另一篇文章《微信iOS SQLite源码优化实践》 55 | * **损坏修复**:数据库损坏一直是个难题,WCDB内置了我们自研的修复工具WCDBRepair。同样可参考我们分享的另一篇文章《微信 SQLite 数据库修复实践》 56 | * **统计分析**:WCDB提供接口直接获取SQL的执行耗时,可用于监控性能。 57 | * **反注入**:WCDB框架层防止了SQL注入,以避免恶意信息危害用户数据。 58 | * ... 59 | 60 | WCDB覆盖了数据库使用的绝大部分场景,且经过微信海量用户的验证,并将持续不断地增加新的能力。 61 | 62 | 本文是WCDB系列文章的第一篇,主要介绍WCDB-iOS/Mac的基本用法,包含: 63 | 64 | * ORM、CRUD与Transaction 65 | * WINQ 66 | * 高级用法 67 | 68 | 69 | ORM 70 | --- 71 | 72 | 在WCDB内,ORM(**O**bject **R**elational **M**apping)是指 73 | 74 | * 将一个ObjC的类,映射到数据库的表和索引; 75 | * 将类的property,映射到数据库表的字段; 76 | 77 | 这一过程。通过ORM,可以达到直接通过Object进行数据库操作,省去拼装过程的目的。 78 | 79 | WCDB通过内建的宏实现ORM的功能。如下图 80 | 81 | ![](assets/wcdb_ios_1/orm_1.jpg) 82 | 83 | ![](assets/wcdb_ios_1/orm_2.jpg) 84 | 85 | 对于一个已有的ObjC类, 86 | 87 | * 引用WCDB框架头文件`#import `,并定义类遵循`WCTTableCoding`协议 88 | * `WCDB_PROPERTY`用于在头文件中声明绑定到数据库表的字段。 89 | * `WCDB_IMPLEMENTATION`,用于在类文件中定义绑定到数据库表的类。同时,该宏内实现了`WCTTableCoding`。因此,开发者无须添加更多的代码来完成`WCTTableCoding`的接口 90 | * `WCDB_SYNTHESIZE`,用于在类文件中定义绑定到数据库表的字段。 91 | 92 | 简单几行代码,就完成了将类和需要的字段绑定到数据库表的过程。这三个宏在名称和使用习惯上,也都和定义一个ObjC类相似,以此便于记忆。 93 | 94 | 除此之外,WCDB还提供了许多可选的宏,用于定义数据库索引、约束等,如: 95 | 96 | * `WCDB_PRIMARY`用于定义主键 97 | * `WCDB_INDEX`用于定义索引 98 | * `WCDB_UNIQUE`用于定义唯一约束 99 | * `WCDB_NOT_NULL`用于定义非空约束 100 | * ... 101 | 102 | 定义完成后,只需要调用`createTableAndIndexesOfName:withClass:`接口,即可创建表和索引。 103 | 104 | ![](assets/wcdb_ios_1/orm_3.jpg) 105 | 106 | 接口会根据ORM的定义,创建对应表和索引。 107 | 108 | 109 | CRUD 110 | ---- 111 | 112 | 得益于ORM的定义,WCDB可以直接进行通过object进行增删改查(CRUD)操作。 113 | 114 | ![](assets/wcdb_ios_1/crud_1.jpg) 115 | 116 | ![](assets/wcdb_ios_1/crud_2.jpg) 117 | 118 | ![](assets/wcdb_ios_1/crud_3.jpg) 119 | 120 | ![](assets/wcdb_ios_1/crud_4.jpg) 121 | 122 | 123 | Transaction 124 | ----------- 125 | 126 | --- 127 | 128 | WCDB内可通过两种方式执行Transaction(事务),一是`runTransaction:`接口 129 | 130 | ![](assets/wcdb_ios_1/transaction_1.jpg) 131 | 132 | 这种方式要求数据库操作在一个BLOCK内完成,简单易用。 133 | 134 | 另一种方式则是获取`WCTTransaction`对象 135 | 136 | ![](assets/wcdb_ios_1/transaction_2.jpg) 137 | 138 | `WCTTransaction`对象可以在类或函数间传递,因此这种方式也更具灵活性。 139 | 140 | 141 | WINQ 142 | ---- 143 | 144 | 有心的同学可能会注意到上述例子中的一些特殊语法: 145 | 146 | * `where:Message.localID>0` 147 | * `onProperties:Message.content` 148 | * `orderBy:Message.localID.order(WCTOrderedDescending)` 149 | 这个便是WINQ。 150 | 151 | WINQ(**W**CDB **In**tegrated **Q**uery,音'wink'),即WCDB集成查询,是将自然查询的SQL集成到WCDB框架中的技术,基于C++实现。 152 | 153 | 传统的SQL语句,通常是开发者拼接字符串完成。这种方式不仅繁琐、易错,而且出错后很难定位到问题所在。同时也容易给SQL注入留下可乘之机。 154 | 155 | 而WINQ将查询语言集成到了C++中,可以通过类似函数调用的方式来写SQL查询。借用IDE的代码提示和编译器的语法检查,达到易用、纠错的效果。 156 | 157 | 对于一个已绑定ORM的类,可以通过`className.propertyName`的方式,获得数据库内字段的映射,以此书写SQL的条件、排序、过滤等等所有语句。如下是几个例子: 158 | 159 | ![](assets/wcdb_ios_1/winq_1.jpg) 160 | 161 | ![](assets/wcdb_ios_1/winq_2.jpg) 162 | 163 | ![](assets/wcdb_ios_1/winq_3.jpg) 164 | 165 | 由于WINQ通过接口调用实现SQL查询,因此在书写过程中会有**IDE的代码提示**和**编译器的语法检查**,从而提升开发效率,避免写错。 166 | 167 | ![](assets/wcdb_ios_1/winq_4.jpg) 168 | 169 | ![](assets/wcdb_ios_1/winq_5.jpg) 170 | 171 | WINQ的接口包括但不限于: 172 | 173 | * 一元操作符:+、-、!等 174 | * 二元操作符:||、&&、+、-、\*、/、|、&、\<\<、\>\>、\<、\<=、==、!=、\>、\>=等 175 | * 范围比较:IN、BETWEEN等 176 | * 字符串匹配:LIKE、GLOB、MATCH、REGEXP等 177 | * 聚合函数:AVG、COUNT、MAX、MIN、SUM等 178 | * ... 179 | 180 | 凡是SQLite支持的语法规则,WINQ基本都有其对应的接口。且接口名称与SQLite的语法规则基本保持一致。对于熟悉SQL的开发者,无须特别学习即可立刻上手使用。 181 | 182 | 183 | 高级用法 184 | ---- 185 | 186 | --- 187 | 188 | ### as重定向 189 | 190 | 基于ORM的支持,我们可以从数据库直接取出一个Object。然而,有时候需要取出并非是某个字段,而是有一些组合。例如: 191 | 192 | ![](assets/wcdb_ios_1/as_1.jpg) 193 | 194 | 这段代码从数据库中取出了消息的最新的修改时间,并以此将此时间作为消息的创建时间,新建了一个message。这种情况下,就可以使用as重定向。 195 | 196 | as重定向,它可以将一个查询结果重定向到某一个字段,如下: 197 | 198 | ![](assets/wcdb_ios_1/as_2.jpg) 199 | 200 | 通过`as(Message.createTime)`的语法,将查询结构重新指向了createTime。因此只需一行代码便可完成原来的任务。 201 | 202 | ### 链式调用 203 | 204 | 链式调用是指对象的接口返回一个对象,从而允许在单个语句中将调用链接在一起,而不需要变量来存储中间结果。 205 | 206 | WCDB对于增删改查操作,都提供了对应的类以实现链式调用 207 | 208 | * WCTInsert 209 | * WCTDelete 210 | * WCTUpdate 211 | * WCTSelect 212 | * WCTRowSelect 213 | * WCTMultiSelect 214 | 215 | ![](assets/wcdb_ios_1/chaincall_1.jpg) 216 | 217 | `where`、`orderBy`、`limit`等接口的返回值均为self,因此可以通过链式调用,更自然更灵活的写出对应的查询。 218 | 219 | 传统的接口方便快捷,可以直接获得操作结果;链式接口则更具灵活性,开发者可以获取数据库操作的耗时、错误信息;也可以通过遍历逐个生成object。 220 | 221 | ![](assets/wcdb_ios_1/chaincall_2.jpg) 222 | 223 | WCDB内同时支持这两种接口,优势互补,开发者可以根据需求,选择使用。 224 | 225 | ### 多表查询 226 | 227 | SQLite支持联表查询,在某些特定的场景下,可以起到优化性能、简化表结构的作用。 228 | 229 | WCDB同样提供了对应的接口,并在ORM的支持下,通过`WCTMultiSelect`的链式接口,可以同时从表中取出多个类的对象。 230 | 231 | ![](assets/wcdb_ios_1/multiselect_1.jpg) 232 | 233 | ### 类字段绑定 234 | 235 | 在ORM中,我们通过宏,将ObjC类的property绑定为数据库的一个字段。但并非所有property的类型都能绑定到字段。 236 | 237 | WCDB内置支持的类型有: 238 | 239 | * const char\*的C字符串类型 240 | * 包括但不限于`int`、`unsigned`、`long`、`unsigned long`、`long long`、`unsigned long long`等所有基于整型的C基本类型 241 | * 包括但不限于`float`、`double`、`long double`等所有基于浮点型的C基本类型 242 | * enum及所有基于枚举型的C基本类型 243 | * `NSString`、`NSMutableString` 244 | * `NSData`、`NSMutableData` 245 | * `NSArray`、`NSMutableArray` 246 | * `NSDictionary`、`NSMutableDictionary` 247 | * `NSSet`、`NSMutableSet` 248 | * `NSValue` 249 | * `NSDate` 250 | * `NSNumber` 251 | * `NSURL` 252 | 253 | 然而,内置支持得再多,也不可能完全覆盖开发者所有的需求。 254 | 255 | 因此WCDB支持开发者自定义类字段绑定。类只需实现`WCTColumnCoding`协议,即可支持绑定。 256 | 257 | ![](assets/wcdb_ios_1/coding_1.jpg) 258 | 259 | * `columnTypeForWCDB`接口定义类对应数据库中的类型 260 | * `unarchiveWithWCTValue:`接口定义从数据库类型反序列化到类的转换方式 261 | * `archivedWCTValue`接口定义从类序列化到数据库类型的转换方式 262 | 263 | 为了简化定义,WCDB提供了文件模版来创建类字段绑定。 264 | 265 | 首先需要安装文件模版。该模版的安装脚本集成在WCDB的编译脚本中,只需编译一次WCDB,就会自动安装文件模版。安装完成后重启Xcode,新建文件,即可看到对应的文件模版 266 | 267 | ![](assets/wcdb_ios_1/coding_2.jpg) 268 | 269 | 选择WCTColumnCoding 270 | 271 | ![](assets/wcdb_ios_1/coding_3.jpg) 272 | 273 | * Class:需要进行字段绑定的类,这里以`NSDate`为例 274 | * Language:WCDB支持绑定ObjC类和C++类,这里选择Objective-C 275 | * Type In DataBase:类对应数据库中的类型。包括 276 | * WCTColumnTypeInteger32 277 | * WCTColumnTypeInteger64 278 | * WCTColumnTypeDouble 279 | * WCTColumnTypeString 280 | * WCTColumnTypeBinary 281 | 282 | 我们知道`NSDate`是遵循`NSCoding`协议的,因此这里选择了Binary类型。即,将NSDate以二进制数据的形式存到数据库中。完成后会自动创建如下的文件模版: 283 | 284 | ![](assets/wcdb_ios_1/coding_4.jpg) 285 | 286 | 然后只需将NSDate和NSData互相转换的方式填上去即可。如下: 287 | 288 | ![](assets/wcdb_ios_1/coding_5.jpg) 289 | 290 | 291 | 总结 292 | == 293 | 294 | WCDB通过ORM和WINQ,体现了其易用性上的优势,使得数据库操作不再繁杂。同时,通过链式调用,开发者也能够方便地获取数据库操作的耗时等性能信息。而高级用法则扩展了WCDB的功能和用法。 295 | 296 | 由于篇幅所限,本文只介绍了WCDB最表层的功能。该系列接下来还将深入介绍WCDB的架构和原理,分享WCDB高并发的解决方案、WINQ实现中的思考等等。敬请期待! -------------------------------------------------------------------------------- /微信移动端数据库组件WCDB系列(三) — WINQ原理篇.md: -------------------------------------------------------------------------------- 1 | 背景 2 | == 3 | 4 | 高效、完整、易用是WCDB的基本原则。前几篇文章分享了WCDB的基本用法和修复工具,接下来将更深入地聊聊WCDB在易用性上的思考和实践。 5 | 6 | 对于各类客户端数据库,似乎都绕不开拼接字符串这一步。即便在Realm这样的NoSQL的数据库中,在进行查询时,也依赖于字符串的语法: 7 | 8 | ```c 9 | //Realm code 10 | [Dog objectsWhere:@"age < 2"] 11 | ``` 12 | 13 | 别看小小的字符串拼接,带来的麻烦可不小: 14 | 15 | * 代码冗余。为了拼接出匹配的SQL语句,业务层往往要写许多胶水代码来format字符串。这些代码冗长且没有什么“营养”。 16 | * 难以查错。对于编译器而言,SQL只是一个字符串。这就意味着即便你只写错了一个字母,也得在代码run起来之后,通过log或断点才能发现错误。倘若SQL所在的代码文件依赖较多,即使改正一个敲错的字母,就得将整个工程重新编译一遍,简直是浪费生命。 17 | * SQL注入。举一个简单的例子: 18 | 19 | ```objc 20 | - (BOOL)insertMessage:(NSString*)message 21 | { 22 | NSString* sql = [NSString stringWithFormat:@"INSERT INTO message VALUES('%@')", message]; 23 | return [db executeUpdate:sql]; 24 | } 25 | ``` 26 | 27 | 这是插入消息的SQL。倘若对方发来这样的消息:`');DELETE FROM message;--`,那么这个插入的SQL就会被分成三段进行解析: 28 | 29 | ```sql 30 | INSERT INTO message VALUES(''); 31 | DELETE FROM message; 32 | --') 33 | ``` 34 | 35 | 它会在插入一句空消息后,将message表内的所有消息删除。若App内存在这样的漏洞被坏人所用,后果不堪设想。 36 | 37 | 反注入的通常做法是, 38 | 39 | * 利用SQLite的绑定参数。通过绑定参数避免字符串拼接。 40 | 41 | ```objc 42 | - (BOOL)insertMessage:(NSString*)message 43 | { 44 | return [db executeUpdate:@"INSERT INTO message VALUES(?)", message]; 45 | } 46 | ``` 47 | 48 | * 对于不适用绑定参数的SQL,则可以将单引号替换成双单引号,避免传入的单引号提前截断SQL。 49 | 50 | ```objc 51 | - (BOOL)insertMessage:(NSString*)message 52 | { 53 | NSString* sql = [NSString stringWithFormat:@"INSERT INTO message VALUES('%@')", [message stringByReplacingOccurrencesOfString:@"'" withString:@"''"]]; 54 | return [db executeUpdate:sql]; 55 | } 56 | ``` 57 | 58 | 尽管反注入并不难,但要求业务开发都了解、并且在开发过程中时时刻刻都警惕着SQL注入,是不现实的。 59 | 60 | 一旦错过了在框架层统一解决这些问题的机会,后面再通过代码规范、Code Review等等人为的方式去管理,就难免会发生疏漏。 61 | 62 | 因此,WCDB的原则是,问题应当更早发现更早解决。 63 | 64 | * 能在编译期发现的问题,就不要拖到运行时; 65 | * 能在框架层解决的问题,就不要再让业务去分担。 66 | 67 | 基于这个原则,我开始进行对SQLite的接口的抽象。 68 | 69 | SQL的组合能力 70 | ======== 71 | 72 | 思考的过程注定不会是一片坦途,我遇到的第一个挑战就是: 73 | 74 | ### 问题一:SQL应该怎么抽象? 75 | 76 | SQL是千变万化的,它可以是一个很简单的查询,例如: 77 | 78 | ```sql 79 | SELECT * FROM message; 80 | ``` 81 | 82 | 这个查询只是取出message表中的所有元素。假设我们可以封装成接口: 83 | 84 | ```c 85 | StatementSelect getAllFromTable(const char* tableName); 86 | ``` 87 | 88 | 但SQL也可以是一个很复杂的查询,例如: 89 | 90 | ```sql 91 | SELECT max(localID), count(content) FROM message 92 | WHERE content IS NOT NULL 93 | AND createTime!=modifiedTime 94 | OR type NOT BETWEEN 0 AND 2 95 | GROUP BY type 96 | HAVING localID>0 97 | ORDER BY createTime ASC 98 | LIMIT (SELECT count(*) FROM contact, contact_ext 99 | WHERE contact.username==contact_ext.username) 100 | ``` 101 | 102 | 这个查询包含了条件、分组、分组过滤、排序、限制、聚合函数、子查询,多表查询。什么样的接口才能兼容这样的SQL? 103 | 104 | 遇到这种两极分化的问题,我的思路通常是二八原则。即 105 | 106 | * 封装常用操作,覆盖80%的使用场景。 107 | * 暴露底层接口,适配剩余20%的特殊情况。 108 | 109 | 但更多的问题出现: 110 | 111 | ### 问题二:怎么定义常用操作? 112 | 113 | * 对于微信常用的操作,是否也适用于所有开发者? 114 | * 现在不使用的操作,以后是否会变成常用? 115 | 116 | ### 问题三:常用操作与常用操作的组合,是否仍属于常用操作? 117 | 118 | 查询某个字段的最大值或最小值,应该属于常用操作的: 119 | 120 | ```sql 121 | SELECT max(localID) FROM message; 122 | SELECT min(localID) FROM message; 123 | ``` 124 | 125 | 假设可以封装为 126 | 127 | ```c 128 | StatementSelect getMaxOfColumnFromTable(const char* columnName, const char* tableName); 129 | StatementSelect getMinOfColumnFromTable(const char* columnName, const char* tableName); 130 | ``` 131 | 132 | 但,SQL是存在组合的能力的。同时查询最大值和最小值,是否仍属于常用操作? 133 | 134 | ```sql 135 | SELECT max(localID), min(localID) FROM message; 136 | ``` 137 | 138 | 若以此规则,继续封装为: 139 | 140 | ```c 141 | StatementSelect getMaxAndMinOfColumnFromTable(const char* columnName, const char* tableName); 142 | ``` 143 | 144 | 那同时查询最大值、最小值和总数怎么办? 145 | 146 | ```sql 147 | SELECT max(localID), min(localID), count(localID) FROM message; 148 | ``` 149 | 150 | 显然,“常用接口”的定义在不断地扩大,接口的复杂性也在增加。以后维护起来,就会疲于加新接口,并且没有边界。 151 | 152 | ### 问题四:特殊场景所暴露的底层接口,应该以什么形式存在? 153 | 154 | 若底层接口还是接受字符串参数的传入,那么前面所思考的一切都是徒劳。 155 | 156 | 157 | 158 | 因此,这里就需要一个理论的基础,去支持WCDB封装是合理的,而不仅仅是堆砌接口。 159 | 160 | 于是,我就去找了SQL千变万化组合的根源 --- SQL语法规则。 161 | 162 | SQL语法规则 163 | ======= 164 | 165 | SQLite官网提供了SQL的语法规则: 166 | 167 | 例如,这是一个`SELECT`语句的语法规则: 168 | 169 | ![](assets/winq/select.jpg) 170 | 171 | SQLite按照图示箭头流向的语法规则解析传入的SQL字符串。每个箭头都有不同的流向可选。 172 | 173 | 例如,`SELECT`后,可以直接接`result-column`,也可以插入`DISTINCT`或者`ALL`。 174 | 175 | 语法规则中的每个字段都有其对应涵义,其中 176 | 177 | * `SELECT`、`DISTINCT`、`ALL`等等大写字母是`keyword`,属于SQL的保留字。 178 | * `result-column、``table-or-subquery`、`expr`等等小写字母是token。token可以再进一步地展开其构成的语法规则。 179 | 180 | 例如,在`WHERE`、`GROUP BY`、`HAVING`、`LIMIT`、`OFFSET`后所跟的参数都是`expr`,它的展开如下: 181 | 182 | ![](assets/winq/expr.jpg) 183 | 184 | 可以看到,`expr`有很多种构成方式,例如: 185 | 186 | * `expr`: `literal-value`。`literal-value`可以进一步展开,它是纯粹的数值。 187 | * 如数字1、数字30、字符串"Hello"等都是`literal-value`,因此它们也是`expr`。 188 | * `expr`: `expr (binary operator) expr`。两个`expr`通过二元操作符进行连接,其结果依然属于`expr`。 189 | * 如1+"Hello"。1和"Hello"都是`literal-value`,因此它们都是`expr`,通过二元操作符"+"号连接,其结果仍然是一个`expr`。尽管1+"Hello"看上去没有实质的意义,但它仍是SQL正确的语法。 190 | 191 | 以刚才那个复杂的SQL中的查询语句为例: 192 | 193 | ```sql 194 | content IS NOT NULL 195 | AND createTime!=modifiedTime 196 | OR type NOT BETWEEN 0 AND 2 197 | ``` 198 | 199 | 1. `content IS NOT NULL`,符合 `expr IS NOT NULL`的语法,因此其可以归并为`expr` 200 | 2. `createTime!=modifiedTime`,符合 `expr (binary operator) expr`的语法,因此其可以归并为`expr` 201 | 3. `type NOT BETWEEN 0 AND 2`,符合 `expr NOT BETWEEN expr AND expr`的语法,因此其可以归并为`expr` 202 | 4. `1. AND 2.`,符合`expr (binary operator) expr`的语法,因此其可以归并为`expr` 203 | 5. `4. OR 3.`,符合`expr (binary operator) expr`的语法,因此其可以归并为`expr` 204 | 205 | 最终,这么长的条件语句归并为了一个`expr`,符合`SELECT`语法规则中`WHERE expr`的语法,因此是正确的SQL条件语句。 206 | 207 | 也正是基于此,可以得出:只要按照SQL的语法封装,就可以保留其组合的能力,就不会错过任何接口,落入疲于加接口的陷阱。 208 | 209 | WCDB的具体做法是: 210 | 211 | 1. 将固定的keyword,封装为函数名,作为连接。 212 | 2. 将可以展开的token,封装为类,并在类内实现其不同的组合。 213 | 214 | 以SELECT语句为例: 215 | 216 | ```c 217 | class StatementSelect : public Statement { 218 | public: 219 | //... 220 | StatementSelect &where(const Expr &where); 221 | StatementSelect &limit(const Expr &limit); 222 | StatementSelect &having(const Expr &having); 223 | //... 224 | }; 225 | ``` 226 | 227 | 在语法规则中,`WHERE`、`LIMIT`等都接受`expr`作为参数。因此,不管SQL多么复杂,`StatementSelect`也只接受`Expr`的参数。而其组合的能力,则在`Expr`类内实现。 228 | 229 | ```c 230 | class Expr : public Describable { 231 | public: 232 | Expr(const Column &column); 233 | template 234 | Expr(const T &value, 235 | typename std::enable_if::value || 236 | std::is_enum::value>::type * = nullptr) 237 | : Describable(literalValue(value)) 238 | { 239 | } 240 | Expr(const std::string &value); 241 | 242 | Expr operator||(const Expr &operand) const; 243 | Expr operator&&(const Expr &operand) const; 244 | Expr operator!=(const Expr &operand) const; 245 | 246 | Expr between(const Expr &left, const Expr &right) const; 247 | Expr notBetween(const Expr &left, const Expr &right) const; 248 | 249 | Expr isNull() const; 250 | Expr isNotNull() const; 251 | 252 | //... 253 | }; 254 | ``` 255 | 256 | `Expr`通过构造函数和C++的偏特化模版,实现了从字符串和数字等进行初始化的效果。同时,通过C++运算符重载的特性,可以将SQL的运算符无损地移植到过来,使得语法上也可以更接近于SQL。 257 | 258 | 在对应函数里,再进行SQL的字符串拼接即可。同时,所有传入的字符串都会在这一层预处理,以防注入。如: 259 | 260 | ```c 261 | Expr::Expr(const std::string &value) : Describable(literalValue(value)) 262 | { 263 | } 264 | 265 | std::string Expr::literalValue(const std::string &value) 266 | { 267 | //SQL anti-injection 268 | return "'" + stringByReplacingOccurrencesOfString(value, "'", "''") + "'"; 269 | } 270 | 271 | Expr Expr::operator&&(const Expr &operand) const 272 | { 273 | Expr expr; 274 | expr.m_description.append("(" + m_description + " AND " + 275 | operand.m_description + ")"); 276 | return expr; 277 | } 278 | ``` 279 | 280 | 基于这个抽象方式,就可以对复杂查询中的条件语句进行重写为: 281 | 282 | ```c 283 | Column content("content"); 284 | Column createTime("createTime"); 285 | Column modifiedTime("modifiedTime"); 286 | Column type("type"); 287 | StatementSelect select; 288 | //... 289 | //WHERE content IS NOT NULL 290 | // AND createTime!=modifiedTime 291 | // OR type NOT BETWEEN 0 AND 2 292 | select.where(Expr(content).isNotNull() 293 | &&Expr(createTime)!=Expr(modifiedTime) 294 | ||Expr(type).notBetween(0, 2)); 295 | //... 296 | ``` 297 | 298 | 首先通过`Column`创建对应数据库字段的映射,再转换为`Expr`,调用对应封装的函数或运算符,即可完成字符串拼接操作。 299 | 300 | 这个抽象便是WCDB的语言集成查询的特性 --- WINQ(**W**CDB **In**tegrated **Q**uery)。 301 | 302 | 303 | 304 | 更进一步,由于WCDB在接口层的ORM封装,使得开发者可以直接通过`className.propertyName`的方式,拿到字段的映射。因此连上述的转换操作也可以省去,查询代码可以在一行代码内完成。 305 | 306 | 以下是WCDB在接口层和WINQ的支持下,对前面所提到的SQL语句的代码示例: 307 | 308 | ```objc 309 | //SELECT * FROM message; 310 | [db getAllObjectsOfClass:Message.class 311 | fromTable:@"message"]; 312 | 313 | /* 314 | SELECT max(localID), count(content) 315 | FROM message 316 | WHERE content IS NOT NULL 317 | AND createTime!=modifiedTime 318 | OR type NOT BETWEEN 0 AND 2 319 | GROUP BY type 320 | HAVING localID>0 321 | ORDER BY createTime ASC 322 | LIMIT (SELECT count(*) 323 | FROM contact, contact_ext 324 | WHERE contact.username==contact_ext.username) 325 | */ 326 | [[[[[[db prepareSelectRowsOnResults:{Message.localID.max(), Message.content.count()} 327 | fromTable:@"message"] 328 | where:Message.content.isNotNull() 329 | && Message.createTime != Message.modifiedTime 330 | || Message.type.notBetween(0, 2)] 331 | groupBy:{Message.type}] 332 | having:Message.localID > 0] 333 | orderBy:Message.createTime.order(WCTOrderedAscending)] 334 | limit:[[[WCTSelectBase alloc] initWithResultList:Contact.AnyProperty.count() 335 | fromTables:@[ @"contact", @"contact_ext" ]] 336 | where:Contact.username.inTable(@"contact") == ContactExt.username.inTable(@"contact_ext")]]; 337 | 338 | /* 339 | SELECT max(localID) FROM message; 340 | */ 341 | [db getOneValueOnResult:Message.localID.max() 342 | fromTable:@"message"]; 343 | /* 344 | SELECT min(localID) FROM message; 345 | */ 346 | [db getOneValueOnResult:Message.localID.min() 347 | fromTable:@"message"]; 348 | /* 349 | SELECT max(localID), min(localID) FROM message; 350 | */ 351 | [db getOneRowOnResults:{Message.localID.max(), Message.localID.min()} 352 | fromTable:@"message"]; 353 | /* 354 | SELECT max(localID), min(localID), count(localID) FROM message 355 | */ 356 | [db getOneRowOnResults:{Message.localID.max(), Message.localID.min(), Message.localID.count()} 357 | fromTable:@"message"]; 358 | ``` 359 | 360 | 总结 361 | == 362 | 363 | WCDB通过WINQ抽象SQLite语法规则,使得开发者可以告别字符串拼接的胶水代码。通过和接口层的ORM结合,使得即便是很复杂的查询,也可以通过一行代码完成。并借助IDE的代码提示和编译检查的特性,大大提升了开发效率。同时还内建了反注入的保护。 364 | 365 | ![](assets/winq/hint.jpg) 366 | 367 | 代码提示 368 | 369 | ![](assets/winq/error.jpg) 370 | 371 | 编译时检查 372 | 373 | 虽然WINQ在实现上使用了C++11特性和模版等,但在使用过程并不需要涉及。对于熟悉SQL的开发,只需按照本能即可写出SQL对应的WINQ语句。最终达到提高WCDB易用性的目的。 374 | 375 | 同时,基于C++的实现也使得WINQ在性能可以期待。 376 | 377 | 后续我们还将分享WCDB在多线程管理上的思考。开发者也可以点击阅读原文访问WCDB的Github仓库,先睹为快! 378 | 379 | -------------------------------------------------------------------------------- /微信移动端数据库组件WCDB系列(二) — 数据库修复三板斧.md: -------------------------------------------------------------------------------- 1 | # 微信移动端数据库组件WCDB系列(二) — 数据库修复三板斧 2 | 3 | ## 前言 4 | 5 | 长久以来SQLite DB都有损坏问题,从Android、iOS等移动系统,到Windows、Linux 6 | 等桌面系统都会出现。由于微信所有消息都保存在DB,服务端不保留备份,一旦损坏将导致用户 7 | 消息被清空,显然不能接受。 8 | 9 | 我们即将开源的移动数据库组件 WCDB (WeChat Database),致力于解决 DB 损坏导致数据丢失的问题。 10 | 11 | 之前的一篇文章《微信 SQLite 数据库修复实践》介绍了微信对SQLite数据库修复 12 | 以及降低损坏率的实践,这次再深入介绍一下微信数据库修复的具体方案和发展历程。 13 | 14 | ## 我们的需求 15 | 16 | 具体来说,微信需要一套满足以下条件的DB恢复方案: 17 | 18 | * **恢复成功率高。** 由于牵涉到用户核心数据,“姑且一试”的方案是不够的,虽说 100% 19 | 成功率不太现实,但 90% 甚至 99% 以上的成功率才是我们想要的。 20 | * **支持加密DB。** Android 端微信客户端使用的是加密 SQLCipher DB,加密会改变信息 21 | 的排布,往往对密文一个字节的改动就能使解密后一大片数据变得面目全非。这对于数据恢复 22 | 不是什么好消息,我们的方案必须应对这种情况。 23 | * **能处理超大的数据量。** 经过统计分析,个别重度用户DB大小已经超过2GB,恢复方案 24 | 必须在如此大的数据量下面保证不掉链子。 25 | * **不影响体验。** 统计发现只有万分之一不到的用户会发生DB损坏,如果恢复方案 26 | 需要事先准备(比如备份),它必须对用户不可见,不能为了极个别牺牲全体用户的体验。 27 | 28 | 经过多年的不断改进,微信先后采用出三套不同的DB恢复方案,离上面的目标已经越来越近了。 29 | 30 | ## 官方的Dump恢复方案 31 | 32 | Google 一下SQLite DB恢复,不难搜到使用`.dump`命令恢复DB的方法。`.dump`命令的作用是将 33 | 整个数据库的内容输出为很多 SQL 语句,只要对空 DB 执行这些语句就能得到一个一样的 DB。 34 | 35 | `.dump`命令原理很简单:每个SQLite DB都有一个`sqlite_master`表,里面保存着全部table 36 | 和index的信息(table本身的信息,不包括里面的数据哦),遍历它就可以得到所有表的名称和 37 | `CREATE TABLE ...`的SQL语句,输出`CREATE TABLE`语句,接着使用`SELECT * FROM ...` 38 | 通过表名遍历整个表,每读出一行就输出一个`INSERT`语句,遍历完后就把整个DB dump出来了。 39 | 这样的操作,和普通查表是一样的,遇到损坏一样会返回`SQLITE_CORRUPT`,我们忽略掉损坏错误, 40 | 继续遍历下个表,最终可以把所有没损坏的表以及**损坏了的表的前半部分**读取出来。将dump 41 | 出来的SQL语句逐行执行,最终可以得到一个等效的新DB。由于直接跑在SQLite上层,所以天然 42 | 就支持加密SQLCipher,不需要额外处理。 43 | 44 | ![](assets/wcdb_repair/dump-example.png) 45 | (图:dump输出样例) 46 | 47 | 这个方案不需要任何准备,只有坏DB的用户要花好几分钟跑恢复,大部分用户是不感知的。 48 | 数据量大小,主要影响恢复需要的临时空间:先要保存dump 出来的SQL的空间,这个 49 | 大概一倍DB大小,还要另外一倍 DB大小来新建 DB恢复。至于我们最关心的成功率呢?上线后, 50 | **成功率约为30%**。这个成功率的定义是至少恢复了一条记录,也就是说一大半用户 51 | 一条都恢复不成功! 52 | 53 | 研究一下就发现,恢复失败的用户,原因都是`sqlite_master`表读不出来,特别是第一页损坏, 54 | 会导致后续所有内容无法读出,那就完全不能恢复了。恢复率这么低的尴尬状况维持了好久, 55 | 其他方案才渐渐露出水面。 56 | 57 | ## 备份恢复方案 58 | 59 | 损坏的数据无法修复,最直观的解决方案就是备份,于是备份恢复方案被提上日程了。备份恢复这个 60 | 方案思路简单,SQLite 也有不少备份机制可以使用,具体是: 61 | 62 | * **拷贝:** 不能再直白的方式。由于SQLite DB本身是文件(主DB + journal 或 WAL), 63 | 直接把文件复制就能达到备份的目的。 64 | * **Dump:** 上一个恢复方案用到的命令的本来目的。在DB完好的时候执行`.dump`, 65 | 把 DB所有内容输出为 SQL语句,达到备份目的,恢复的时候执行SQL即可。 66 | * **Backup API:** SQLite自身提供的一套备份机制,按 Page 为单位复制到新 DB, 67 | 支持热备份。 68 | 69 | 这么多的方案孰优孰劣?作为一个移动APP,我们关心的无非就是 **备份大小、备份性能、 70 | 恢复性能** 几个指标。微信作为一个重度DB使用者,备份大小和备份性能是主要关注点, 71 | 原本用户就可能有2GB 大的 DB,如果备份数据本身也有2GB 大小,用户想必不会接受; 72 | 性能则主要影响体验和备份成功率,作为用户不感知的功能,占用太多系统资源造成卡顿 73 | 是不行的,备份耗时越久,被系统杀死等意外事件发生的概率也越高。 74 | 75 | 对以上方案做简单测试后,备份方案也就基本定下了。测试用的DB大小约 **50MB**, 76 | 数据条目数大约为 **10万条**: 77 | 78 | ![](assets/wcdb_repair/backup-compare.png) 79 | 80 | 可以看出,比较折中的选择是 **Dump + 压缩**,备份大小具有明显优势,备份性能尚可, 81 | 恢复性能较差但由于需要恢复的场景较少,算是可以接受的短板。 82 | 83 | 微信在Dump + gzip方案上再加以优化,由于格式化SQL语句输出耗时较长,因此使用了自定义 84 | 的二进制格式承载Dump输出。第二耗时的压缩操作则放到别的线程同时进行,在双核以上的环境 85 | 基本可以做到无额外时间消耗。由于数据保密需要,二进制Dump数据也做了加密处理。 86 | 采用自定义二进制格式还有一个好处是,恢复的时候不需要重复的编译SQL语句,编译一次就可以 87 | 插入整个表的数据了,恢复性能也有一定提升。优化后的方案比原始的Dump + 压缩, 88 | **每秒备份行数提升了 150%,每秒恢复行数也提升了 40%**。 89 | 90 | ![](assets/wcdb_repair/backup-optimization.png) 91 | 92 | 即使优化后的方案,对于特大DB备份也是耗时耗电,对于移动APP来说,可能未必有这样的机会 93 | 做这样重度的操作,或者频繁备份会导致卡顿,这也是需要开发者衡量的。比如Android微信会 94 | 选择在 **充电并灭屏** 时进行DB备份,若备份过程中退出以上状态,备份会中止,等待下次机会。 95 | 96 | 备份方案上线后,恢复成功率**达到72%**,但有部分重度用户DB损坏时,由于备份耗时太久, 97 | 始终没有成功,而对DB数据丢失更为敏感的也恰恰是这些用户,于是新方案应运而生。 98 | 99 | ## 解析B-tree恢复方案(RepairKit) 100 | 101 | 备份方案的高消耗迫使我们从另外的方案考虑,于是我们再次把注意力放在之前的Dump方案。 102 | Dump 方案本质上是尝试从坏DB里读出信息,这个尝试一般来说会出现两种结果: 103 | 104 | * DB的基本格式仍然健在,但个别数据损坏,读到损坏的地方SQLite返回`SQLITE_CORRUPT`错误, 105 | 但已读到的数据得以恢复。 106 | * 基本格式丢失(文件头或`sqlite_master`损坏),获取有哪些表的时候就返回`SQLITE_CORRUPT`, 107 | 根本没法恢复。 108 | 109 | 第一种可以算是预期行为,毕竟没有损坏的数据能 **部分恢复**。从之前的数据看, 110 | 不少用户遇到的是第二种情况,这种有没挽救的余地呢? 111 | 112 | 要回答这个问题,先得搞清楚`sqlite_master`是什么。它是一个每个SQLite DB都有的特殊的表, 113 | 无论是查看官方文档[Database File Format][sqlite-format],还是执行SQL语句 114 | `SELECT * FROM sqlite_master;`,都可得知这个系统表保存以下信息: 表名、类型(table/index)、 115 | 创建此表/索引的SQL语句,以及表的RootPage。`sqlite_master`的表名、表结构都是固定的, 116 | 由文件格式定义,RootPage 固定为 page 1。* 117 | 118 | ![](assets/wcdb_repair/sqlite_master-struct.png) 119 | (图:sqlite_master表) 120 | 121 | 正常情况下,SQLite 引擎打开DB后首次使用,需要先遍历`sqlite_master`,并将里面保存的SQL语句再解析一遍, 122 | 保存在内存中供后续编译SQL语句时使用。假如`sqlite_master`损坏了无法解析,“Dump恢复”这种走正常SQLite 123 | 流程的方法,自然会卡在第一步了。为了让`sqlite_master`受损的DB也能打开,需要想办法绕过SQLite引擎的逻辑。 124 | 由于SQLite引擎初始化逻辑比较复杂,为了避免副作用,没有采用hack的方式复用其逻辑,而是决定仿造一个只可以 125 | 读取数据的最小化系统。 126 | 127 | 虽然仿造最小化系统可以跳过很多正确性校验,但`sqlite_master`里保存的信息对恢复来说也是十分重要的, 128 | 特别是RootPage,因为它是表对应的B-tree结构的根节点所在地,没有了它我们甚至不知道从哪里开始解析对应的表。 129 | 130 | `sqlite_master`信息量比较小,而且只有改变了表结构的时候(例如执行了`CREATE TABLE`、`ALTER TABLE` 131 | 等语句)才会改变,因此对它进行备份成本是非常低的,一般手机典型只需要几毫秒到数十毫秒即可完成,一致性也容易保证, 132 | 只需要执行了上述语句的时候重新备份一次即可。有了备份,我们的逻辑可以在读取DB自带的`sqlite_master`失败的时候 133 | 使用备份的信息来代替。 134 | 135 | DB初始化的问题除了文件头和`sqlite_master`完整性外,还有加密。SQLCipher加密数据库,对应的恢复逻辑还需要加上 136 | 解密逻辑。按照SQLCipher的实现,加密DB 是按page 进行包括头部的完整加密,所用的密钥是根据用户输入的原始密码和 137 | 创建DB 时随机生成的 salt 运算后得出的。可以猜想得到,如果保存salt错了,将没有办法得出之前加密用的密钥, 138 | 导致所有page都无法读出了。由于salt 是创建DB时随机生成,后续不再修改,将它纳入到备份的范围内即可。 139 | 140 | 到此,初始化必须的数据就保证了,可以仿造读取逻辑了。我们常规使用的读取DB的方法(包括dump方式恢复), 141 | 都是通过执行SQL语句实现的,这牵涉到SQLite系统最复杂的子系统——SQL执行引擎。我们的恢复任务只需要遍历B-tree所有节点, 142 | 读出数据即可完成,不需要复杂的查询逻辑,因此最复杂的SQL引擎可以省略。同时,因为我们的系统是只读的, 143 | 写入恢复数据到新 DB 只要直接调用 SQLite 接口即可,因而可以省略同样比较复杂的B-tree平衡、Journal和同步等逻辑。 144 | 最后恢复用的最小系统只需要: 145 | 146 | * VFS读取部分的接口(Open/Read/Close),或者直接用stdio的fopen/fread、Posix的open/read也可以 147 | * SQLCipher的解密逻辑 148 | * B-tree解析逻辑 149 | 150 | 即可实现。 151 | 152 | ![](assets/wcdb_repair/sqlite-arch-core.png) 153 | (图:最小化系统) 154 | 155 | [Database File Format][sqlite-format] 一文详细描述了SQLite文件格式, 156 | 参照之实现B-tree解析可读取 SQLite DB。加密 SQLCipher 情况较为复杂,幸好SQLCipher 157 | 加密部分可以单独抽出,直接套用其解密逻辑。 158 | 159 | 实现了上面的逻辑,就能读出DB的数据进行恢复了,但还有一个小插曲。我们知道,使用SQLite查询一个表, 160 | 每一行的列数都是一致的,这是Schema层面保证的。但是在Schema的下面一层——B-tree层,没有这个保证。 161 | B-tree的每一行(或者说每个entry、每个record)可以有不同的列数,一般来说,SQLite插入一行时, 162 | B-tree里面的列数和实际表的列数是一致的。但是当对一个表进行了`ALTER TABLE ADD COLUMN`操作, 163 | 整个表都增加了一列,但已经存在的B-tree行实际上没有做改动,还是维持原来的列数。 164 | 当SQLite查询到`ALTER TABLE`前的行,缺少的列会自动用默认值补全。恢复的时候,也需要做同样的判断和支持, 165 | 否则会出现缺列而无法插入到新的DB。 166 | 167 | 解析B-tree方案上线后,**成功率约为78%**。这个成功率计算方法为恢复成功的 Page 数除以总 Page 数。 168 | 由于是我们自己的系统,可以得知总 Page 数,使用恢复 Page 数比例的计算方法比人数更能反映真实情况。 169 | B-tree解析好处是准备成本较低,不需要经常更新备份,对大部分表比较少的应用备份开销也小到几乎可以忽略, 170 | 成功恢复后能还原损坏时最新的数据,不受备份时限影响。 171 | 坏处是,和Dump一样,如果损坏到表的中间部分,比如非叶子节点,将导致后续数据无法读出。 172 | 173 | ## 不同方案的组合 174 | 175 | 由于解析B-tree恢复原理和备份恢复不同,失败场景也有差别,可以两种手段混合使用覆盖更多损坏场景。 176 | 微信的数据库中,有部分数据是临时或者可从服务端拉取的,这部分数据可以选择不修复,有些数据是不可恢复或者 177 | 恢复成本高的,就需要修复了。 178 | 179 | 如果修复过程一路都是成功的,那无疑使用B-tree解析修复效果要好于备份恢复。备份恢复由于存在 180 | 时效性,总有部分最新的记录会丢掉,解析修复由于直接基于损坏DB来操作,不存在时效性问题。 181 | 假如损坏部分位于不需要修复的部分,解析修复有可能不发生任何错误而完成。 182 | 183 | 若修复过程遇到错误,则很可能是需要修复的B-tree损坏了,这会导致需要修复的表发生部分或全部缺失。 184 | 这个时候再使用备份修复,能挽救一些缺失的部分。 185 | 186 | 最早的Dump修复,场景已经基本被B-tree解析修复覆盖了,若B-tree修复不成功,Dump恢复也很有可能不会成功。 187 | 即便如此,假如上面的所有尝试都失败,最后还是会尝试Dump恢复。 188 | 189 | ![](assets/wcdb_repair/repair-united.png) 190 | 191 | 上面说的三种修复方法,原理上只涉及到SQLite文件格式以及基本的文件系统,是跨平台的。 192 | 实际操作上,各个平台可以利用各自的特性做策略上的调整,比如 Android 系统使用 `JobScheduler` 193 | 在充电灭屏状态下备份。 194 | 195 | ## 我们的组件 196 | 197 | WCDB - WeChat Database,微信的移动数据库组件,包含上面几种修复方案, 198 | 以及加密、连接池并发、ORM、性能优化等特性,将在近日开源,欢迎关注。 199 | 200 | [dump-example]: http://stackoverflow.com/questions/18259692/how-to-recover-a-corrupt-sqlite3-database 201 | [sqlite-format]: http://sqlite.org/fileformat2.html 202 | -------------------------------------------------------------------------------- /微信移动端数据库组件WCDB系列(四) — Android 特性篇.md: -------------------------------------------------------------------------------- 1 | # 微信移动端数据库组件WCDB系列(四) — Android 特性篇 2 | 3 | 微信的移动端数据库组件 WCDB 已经正式开源了,有关注的小伙伴可能已经用上了。如果还没用上, 4 | 可以翻到文末关注我们的 GitHub 和公众号其他文章。 5 | 6 | 之前我们已经发过几篇 iOS 和修复的文章,Android 由于接口跟系统几乎一样,相信大家都比较熟悉, 7 | 不熟悉用法也可以到 Android Developer 官网看一下。但是,我们也有一些特色功能和优化大家可能不容易注意到, 8 | 现在就单独拿出来说说。 9 | 10 | # 加密接口 11 | 12 | WCDB 使用了 SQLCipher 的 C 层库,但没有直接使用 SQLCipher Android 的封装层。SQLCipher Android 13 | 封装层中很多设置需要手写 PRAGMA 语句实现,比如设置 KDF 迭代次数(兼容老版本 SQLCipher DB)、设置 14 | Page Size 等操作。 15 | 16 | ```java 17 | private static SQLiteDatabaseHook hook = new SQLiteDatabaseHook() { 18 | @Override 19 | public void postKey(SQLiteDatabase db) { 20 | db.rawExecSQL("PRAGMA kdf_iter = 4000;"); 21 | db.rawExecSQL("PRAGMA cipher_page_size = 1024;"); 22 | } 23 | 24 | @Override 25 | public void preKey(SQLiteDatabase db) { 26 | // ...... 27 | } 28 | }; 29 | SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(path, "password", 30 | null /*factory*/, hook); 31 | ``` 32 | 33 | 对于开发者来说,这需要了解 SQLCipher 底下的 PRAGMA 指令,更重要的是要搞清楚这些指令正确的调用顺序。 34 | 哪些是需要在设置 key 之前执行的?哪些是只有设置了 key 之后才生效的?开发者往往必须仔细查阅 SQLCipher 35 | 的文档来了解这些细节。 36 | 37 | WCDB 对这个部分做了改进,封装了 `SQLiteCipherSpec` 用于设置加密参数,设置好了传给 `SQLiteDatabase` 38 | 工厂方法就好了,不需要考虑 PRAGMA 语法和调用顺序。 39 | 40 | ```java 41 | SQLiteCipherSpec cipher = new SQLiteCipherSpec() 42 | .setPageSize(1024) 43 | .setKDFIteration(4000); 44 | SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(path, "password".getBytes(), 45 | cipher, null /*factory*/); 46 | ``` 47 | 48 | 使用 `SQLiteCipherSpec` 另一个好处是,同样的结构可以传给 `RepairKit` 用于恢复损坏 DB,不需要两套 49 | 接口了。由于 RepairKit 底层不使用 PRAGMA,原来 hook 的形式不能满足需要。 50 | 51 | 另外,WCDB 将 `String` 类型的密码改为 `byte[]` 类型,可以支持非打印字符作为密码(比如 52 | `hash(user id)` 方式),原来字符类型密码只要转换为 UTF-8 的 byte 数组即可,和 SQLCipher Android 53 | 兼容。 54 | 55 | # 数据迁移 56 | 57 | SQLCipher 提供了 `sqlcipher_export` SQL 函数用于导出数据到挂载的另一个 DB,可以用于数据迁移。 58 | 但这个函数用于 Android 的 `SQLiteOpenHelper` 并不方便。 59 | 60 | `SQLiteOpenHelper` 主要帮助开发者做 Schema 版本管理,通过它打开 SQLite 数据库,会读取 `user_version` 61 | 字段来判断是否需要升级,并调用子类实现的 `onCreate`、`onUpgrade` 等接口来完成创建或升级操作。 62 | `sqlcipher_export` 由于是导出而非导入,就跟 `onCreate` 等接口不搭了,因为要关闭原来的 DB, 63 | 打开老的 DB,执行 export 到新 DB,再重打开。 64 | 65 | 为了方便使用,WCDB 就做了扩展,将 `sqlcipher_export` 扩展为可以接受第二个参数表示从哪里导出, 66 | 从而实现了导入。 67 | 68 | ```sql 69 | ATTACH DATABASE '/path/to/old/db' AS old; 70 | 71 | -- 将 old 的数据导出到 main,也就是从 old 导入数据 72 | SELECT sqlcipher_export('main', 'old'); 73 | ``` 74 | 75 | 如此就可以不关闭原来的数据库实现数据导入,可以兼容 `SQLiteOpenHelper` 的接口了。详细可以看 76 | 我们的 Sample。 77 | 78 | # 设备锁定 79 | 80 | WCDB 在加密的基础上加上的一个小功能, 81 | 82 | # 全文搜索分词器与动态 ICU 加载 83 | 84 | WCDB Android 自带了一个 FTS3/4 分词器,名为 `mmicu`,用于实现 [SQLite 全文搜索][sqlite-fts]。 85 | 分词器的使用与 SQLite 自带的 `simple`、`icu` 等分词器一样,创建虚拟表的时候带上名字即可: 86 | 87 | ```java 88 | SQLiteDatabase db = getDB(); 89 | db.execSQL("CREATE VIRTUAL TABLE message USING fts4 (content, tokenize=mmicu);") 90 | 91 | ``` 92 | 93 | MMICU 分词器与官方 ICU 分词器类似,但对中文(象形文字)分词以及 ICU 库加载做了特殊处理。 94 | ICU 对中文的分词是基于词库的,Android 系统不同版本会附带不同版本的 ICU,捎带不同版本的中文 95 | 词库,当然也会带来不同的分词结果,这个对于统一产品体验是非常不利的。 96 | 97 | 另外,ICU 自带的中文词库并非非常完整,组词效果也一般,但若自带一个完整好用的词库, 98 | 又需要非常大的空间,这个空间会体现在 APK 体积上。最终,我们做了折中, 99 | **中文字全部单字成词,其他文字则使用 ICU 默认规则。** 100 | 101 | ICU 还有一个严重的问题是动态库和自带的数据文件体积很大,超过 10MB,编译进 APK 里相当不划算, 102 | 最好能直接加载系统自带的 ICU 库。但加载系统库有另一个障碍:**ICU 库不同版本会在函数名称后面 103 | 带上版本号后缀**,直接编译时连接行不通。 104 | 105 | 为了克服这个障碍,WCDB 做了一个兼容层 `icucompat`,通过系统带的数据文件推断 ICU 版本, 106 | 通过 `dlopen` 动态加载不同的符号名称,然后通过宏来模拟直接调用方便开发。最终实现效果便是 107 | **在不需要自带 ICU 库的前提下使用 ICU 库的断词、归一化等功能**,为最终 APK 包省下 10MB 108 | 以上空间。 109 | 110 | 有了 ICU 兼容层,要实现 Android 框架自带的 ICU 相关功能就简单了,比如 `LOCALIZED` 排序。 111 | 但是,WCDB 目前没有接入(主要是没有相关需求),有这方面需要的话,可以到我们的 GitHub 112 | 提 Issue 哦。 113 | 114 | # 日志重定向与性能监控 115 | 116 | SQLite 和 WCDB 框架在运行中会产生日志,这些日志默认会打印到系统日志(logcat),但这可能不是 117 | 所有开发者都希望的行为。比如担心日志里带有敏感信息,直接输出到系统不妥,或者希望将日志写到文件 118 | 用于上报和分析,WCDB 提供接口来完成日志重定向。 119 | 120 | ```java 121 | // 不打印任何日志 122 | Log.setLogger(Log.LOGGER_NONE); 123 | 124 | // 或者使用自定义日志逻辑 125 | Log.setLogger(new Log.LogCallback() { 126 | @Override 127 | public void println(int priority, String tag, String msg) { 128 | // 处理日志 129 | } 130 | }); 131 | ``` 132 | 133 | 要实现高性能日志持久化,可以考虑使用我们 mars 里面的 xlog 组件哦。 134 | 135 | WCDB 还提供了性能监控接口 `SQLiteTrace`,实现接口并绑定到 `SQLiteDatabase` 可以在每次 136 | 执行 SQL 语句或连接池拥堵的时候得到回调。 137 | 138 | ```java 139 | SQLiteTrace myTrace = new SQLiteTrace() { 140 | @Override 141 | public void onSQLExecuted(SQLiteDatabase db, String sql, int type, long time) { 142 | // 每次执行完一条 SQL 语句时回调 143 | Log.i(TAG, "trace", "Execute " + sql + " took " + time + "milliseconds"); 144 | } 145 | 146 | @Override 147 | public void onConnectionPoolBusy(SQLiteDatabase db, String sql, List requests, 148 | String message) { 149 | // 等待连接池超过3秒时回调,一般是因为别的操作占用连接池全部连接 150 | Log.i(TAG, "trace", "SQL: " + sql + " is waiting for execution. Message: " + message); 151 | } 152 | 153 | @Override 154 | public void onDatabaseCorrupted(SQLiteDatabase db) { 155 | // 数据库损坏时回调,只有使用默认 DatabaseErrorHandler 才会回调, 156 | // 若使用自定义 DatabaseErrorHandler 可直接在里面执行对应逻辑 157 | Log.i(TAG, "trace", "Database corrupted!"); 158 | } 159 | } 160 | 161 | SQLiteDatabase db = getDB(); 162 | db.setTraceCallback(myTrace); 163 | ``` 164 | 165 | `SQLiteDatabase` 也开放了 `dump` 方法,可以打印出数据库的当前状态,包括连接池内所有连接 166 | 被持有的状态以及最近执行的 SQL 语句和耗时,对排查性能和死锁问题也有很大帮助。 167 | 168 | # 优化 Cursor 实现 169 | 170 | 在 WCDB 发布时,我们的一篇文章上提到 Cursor 实现优化。对于 **查询获取 Cursor → 遍历 → 关闭** 171 | 这种简单的场景,我们通过 `SQLiteDirectCursor` 直接操作 SQLite 底层的查询,避免 CursorWindow 172 | 的重复分配带来的损耗。 173 | 174 | 可以看一下我们发布时的文章。 175 | 176 | 需要注意的是 Direct Cursor 未关闭前会占用一个数据库连接,使用完需要尽快关闭,否则会一直占用 177 | 造成别的线程无法请求到连接。遍历 Cursor 过程中同一线程不做其他 DB 操作,遍历完关闭,配合 WAL 178 | 使用,是最佳实践。 179 | 180 | # 关注我们 181 | 182 | 到 GitHub 关注 WCDB 的最新动态。 183 | 184 | https://github.com/Tencent/wcdb 185 | 186 | 187 | [sqlite-fts]: https://sqlite.org/fts3.html 188 | -------------------------------------------------------------------------------- /微信终端跨平台组件 Mars 系列 - 我们如约而至.md: -------------------------------------------------------------------------------- 1 | ##背景 2 | 2012 年中,微信支持包括 Android、iOS、Symbian 等三个平台。但在各个平台上,微信客户端没有任何统一的基础模块。2012 年的微信正处于高速发展时期,各平台的迭代速度不一、使用的编程语言各异,后台架构也处在不断探索的过程中。多种因素使得各个平台基础模块的实现出现了差异,导致出现多次需要服务器做兼容的善后工作。网络作为微信的基础,重要性不言而喻。任何网络实现的 bug 都可能导致重大事故。例如微信的容灾实现,如果因为版本的实现差异,导致某些版本上无法进行容灾恢复,将会严重的影响用户体验,甚至造成用户的流失。我们急需一套统一的网络基础库,为微信的高速发展保驾护航。 3 | 4 | 恰好,这个时候塞班渐入日暮,微信对塞班的支持也逐渐减弱。老大从塞班组抽调人力,组成一个三人小 team 的初始团队,开始着手做通用的基础组件。这个基础组件最初就定位为:跨平台、跨业务的基础组件。现在看,这个组件除了解决了已有问题,还给微信的高速发展带来了很多优势,例如: 5 | * 基础组件方便了开展专项的网络基础研究与优化。 6 | * 基础组件为多平台的快速实现提供了有力的支持。 7 | 8 | 经过四年多的发展,跨平台的基础组件已经包含了网络组件、日志组件在内的多个组件。回头看,这是一条开荒路。 9 | 10 | ##设计原则 11 | 在基础模块的开发中,设计尤为重要。在设计上,微信基础组件以跨平台、跨业务为前提,遵从**高可用,高性能,负载均衡**的设计原则。 12 | 13 | 可用是一个即时通讯类 App 的立身之本。高可用又体现在多个层面上:网络的可用性、 App 的可用性、系统的可用性等。 14 | 15 | * 网络的可用性 16 | 移动互联网有着丢包率高、带宽受限、延迟波动、第三方影响等特点,使得网络的可用性,尤其是弱网络下的可用性变得尤为关键。Mars 的 STN 组件作为基于 socket 层的网络解决方案,在很多细节设计上会充分考虑弱网络下的可用性。 17 | 18 | * App 的可用性 19 | App 的可用性包含稳定性、运行性能等多个方面。文章[高性能日志模块 xlog](https://mp.weixin.qq.com/s/cnhuEodJGIbdodh0IxNeXQ) 描述了 xlog 在不影响 App 运行性能的前提下进行的大量设计思考。 20 | 21 | * 系统的可用性 22 | 除了考虑正常的使用场景,APP的设计还需要从整个系统的角度进行设计思考。例如在容灾设计上,Mars 不仅使用了服务器容灾方案,也设计了客户端的本地容灾。当部分服务器出灾时,目前微信可以做到,15min 内把95%以上的用户转移到可用服务器上。 23 | 24 | 保障高可用并不代表可以牺牲性能,对于一个用户使用最频繁的应用,反而更要对使用的资源精打细算。例如在 [Mars 信令传输超时设计](https://mp.weixin.qq.com/s/PnICVDyVuMSyvpvTrdEpSQ)中,多级超时的设计充分的考虑了可用性与高性能之间的平衡取舍。 25 | 26 | 如果说高可用高性能只是客户端本身的考虑的话,负载均衡就需要结合服务器端来考虑了,做一个客户端网络永远不能只把眼光放在客户端上。任何有关网络访问的决策都要考虑给服务器所带来的额外压力是多大。为了选用质量较好的 IP,曾经写了完整的客户端测速代码,后来删掉,其中一个原因是因为不想给服务器带来额外的负担。Mars 的代码中,选择 IP 时用了大量的随机函数也是为了规避大量的用户同时访问同一台服务器而做的。 27 | 28 | 在这四年,我学到最多的就是简单和平衡。 把方案做的尽可能简单,这样才不容易出错。设计方案时大多数时候都不可能满足所有想达到的条件,这个时候就需要去平衡各个因素。在组件中一个很好的例子就是长连接的连接频率(具体实现见longlink_connect_monitor.cc),这个连接频率就是综合耗电量,流量,网络高可用,用户行为等因素进行综合考虑的。 29 | 30 | ##Mars 的发展历程 31 | 32 | ####阶段一:让微信跑起来 33 | 跨平台基础组件的需求起源于微信,首要目标当然是先承载起微信业务。为了不局限于微信,满足跨平台、跨业务的设计目标,在设计上,网络组件定位为客户端与服务端之间的无状态网络信令通道,即交互方式主要包含一来一回、主动push两种方式。这使得基础组件无需考虑请求间的关联性、时序性,核心接口得到了极大的简化。同时,简洁的交互也使得业务逻辑的耦合极少。目前基础组件与业务的交互只包括:编解码、auth状态查询两部分。核心接口如下:(具体见stn_logic.h)。 34 | 35 | ``` 36 | void StartTask(...); 37 | int OnTaskEnd(...); 38 | void OnPush(...); 39 | bool Req2Buf(...); 40 | int Buf2Resp(...); 41 | bool MakeSureAuthed(); 42 | ``` 43 | 在线程模型的选择上,最早使用的是多线程模型。当需要异步做一个工作,就起一个线程。多线程势必少不了锁。但当灰度几次之后发现,想要规避死锁的四个必要条件并没有想象中的那么容易。用户使用场景复杂,客户端的时序、状态的影响因素多,例如网络切换事件、前后台事件、定时器事件、网络事件、任务事件等,导致了不少的死锁现象和对象析构时序错乱导致的内存非法访问问题。 44 | 45 | 这时,我们开始思考,多线程确实有它的优点:可以并发甚至并行提高运行速度。但是对于网络模块来说,性能瓶颈主要是在网络耗时上,并不在于本地程序执行速度上。那为何不把大部分程序执行改成串行的,这样就不会存在多线程临界区的问题,无锁自然就不会死锁。 46 | 47 | 因此,我们目前使用了消息队列的方案(具体实现见 comm/messagequeue 目录),把绝大多数非阻塞操作放到消息队列里执行。并且规定,基础组件与调用方之间的交互必须1. 尽快完成,不进行任何阻塞操作;2. 单向调用,避免形成环状的复杂时序。消息队列的引入很好的改善了死锁问题,但消息队列的线程模型中,我们还是不能避免存在需要阻塞的调用,例如网络操作。在未来的尝试中,我们计划引入协程的方式,将线程模型尽可能的简化。 48 | 49 | 在其它技术选型上,有时甚至需要细节到API 的使用,比如考虑平台兼容性问题,舍弃了一些函数的线程安全版本,使用了 asctime、localtime、rand 等非线程安全的版本。 50 | 51 | ####阶段二:修炼内功 52 | 在多次的灰度验证、数据比对下,微信各平台的网络逻辑顺利的过渡到了统一基础组件。为了有效的验证组件的效果,我们开发了 smc 的统计监控组件,开始关注网络的各项指标,进行网络基础研究与优化,尤其是关注移动网络的特征。 53 | 54 | * 基础网络优化。 55 | 常规的网络能力,例如 DNS 防劫持、动态 IP 下发、就近接入、容灾恢复等,在这一阶段得到逐步的建设与完善。除此之外,Mars 的网络模块是基于 socket 层的网络解决方案,在缺失大而全的 HTTP 能力的同时,却可以将优化做到更细致,细致到连接策略、连接超时、多级读写超时、收发策略等每个网络过程中。例如,当遇到弱网络下连通率较低,或者某些连通率不好的的服务器影响使用时,我们使用了复合连接(代码见complexconnect.inl)和 IP 排序(代码见simple_ipport_sort.cc)的方案很好的应对这两个问题。 56 | 57 | 58 | * 平台特性优化。虽然 Mars 是跨平台的基础组件,但在很多设计上是需要结合各平台的特性的。例如为了尽量减少频繁的唤醒手机,引入了[智能心跳](https://mp.weixin.qq.com/s/ghnmC8709DvnhieQhkLJpA),并且在智能心跳中考虑了 Android 的 alarm 对齐特性(具体实现见smart_heartbeat.cc)。再如在网络切换时,为了平滑切换的过程,使用了 iOS 中网络的特性,在 iOS 中做了延迟处理等。 59 | 60 | * 移动特性优化。微信的使用场景大部分是在手机端进行使用,在组件的设计过程中,我们也会研究移动设备的特性,并进行结合优化。例如,结合移动设备的无线电资源控制器(RRC)的状态切换,对一些性能要求特别特别敏感的请求,进行提前激活的优化处理等。 61 | 62 | ####阶段三:“抓妖记” 63 | 基础组件全量上线微信后,以微信的用户量,当然也会遇到各种各样的“妖”。例如,写网络程序躲不开运营商。印象比较深刻的某地的用户反馈连接 WiFi 时,微信不可用,后来 tcpdump 发现,当包的大小超过一定大小后就发不出去。解决方案:在 WiFi 网络下强制把 MSS 改为1400(代码见 unix_socket.cc)。 64 | 65 | 做移动客户端更避不开手机厂商。一次遇到了一个百思不得其解的 crash,堆栈如下: 66 | 67 | ```cpp 68 | #00 pc 0x43e50 /system/lib/libc.so (???) 69 | #01 pc 0x3143 /system/vendor/lib/libvendorconn.so (handleDpmIpcReq+154) 70 | #02 pc 0x2f6d /system/vendor/lib/libvendorconn.so (send_ipc_req+276) 71 | #03 pc 0x30ff /system/vendor/lib/libcneconn.so (connect+438) 72 | ``` 73 | 看堆栈结合程序 xlog 分析,非阻塞 socket 卡在了 connect 函数里超过了6 min, 被我们自带的 anr 检测(代码见anr.cc)发现然后自杀。最后实在束手无策,联系厂商一起排查,最终查明原因:为了省电,当手机锁屏时连的不是 WiFi 且又没有下行网络数据时,芯片 gate 会关闭,block 住所有网络请求,直到有下行数据或者超过 20min 才会放开。当手机有网络即使是手机网络的情况下,很难没有下行数据,所以基本不会触发组件自带的 anr 检测,但当手机没连接任何网络时,就很容易触发。解决方案:厂商修改代码逻辑,当没有任何网络时不 block 网络请求。 74 | 75 | 运营商和手机厂商对我们来说已经是一个黑盒,但其实也遇到过更黑的黑盒。当手机长时间不重启,有极小概率不能继续使用微信,重启手机会恢复。但因为一直找不到一个愿意配合我们又满足条件的用户,导致这个问题很长一段时间内都没有任何进展,最终偶然一个机会,在一台测试机器上重现了该问题,tcpdump 发现在三步握手阶段,服务器带回的客户端带过去的 tsval 字段被篡改,导致三步握手直接失败,而且这个篡改发生在离开服务器之后到达客户端之前。 76 | ![](assets/mars/tcpdump_client.png) 77 | ![](assets/mars/tcpdump_server.png) 78 | 79 | 这个问题是微信网络模块中排查时间最长也是花费精力最多的一个问题,不仅因为很长一段时间内无案例可分析,也因为在重现后,联系了大量的同事和外部有关人的帮忙,想排查出罪魁祸首。但因为中间涉及的环节和运营商相关部门过多,无法继续排查下去,最终也没找到根本原因。 解决办法:服务器更改 net.ipv4.tcp_timestamps = 0。 80 | 81 | 这段时间是痛并快乐着,见识到了各种极差的网络,才切肤感受到移动网络环境的恶劣程度,但看着我们的网络性能数据在稳步提升又有种满足感。截止到今天,已经很少有真正的网络问题需要跟进了。这也是我们能有时间开始把这些代码开源出去的很大的一个原因。 82 | 83 | ##Mars 介绍 84 | 讲述了一大堆 Mars 的发展历程,终于来到主角的介绍了。大概一年前,我们开始有想法把基础组件开源出去,当时大家都在纠结叫什么名字好呢?此时恰逢《火星救援》正在热映,一位同事说干脆叫 Mars 吧,于是就定下来叫了 Mars。看了看代码,发现想要开源出去可能还是需要做一些其他工作的。 85 | ####代码重构 86 | 首先,代码风格方面,因为最初我们使用文件名、函数名、变量名的规则是内部定义的规则,为了能让其他人读起来更舒心,我们决定把代码风格改为谷歌风格,比如:变量名一律小写, 单词之间用下划线连接;左大括号不换行等等。但是为了更好的区分访问空间,我们又在谷歌代码风格进行了一些变通,比如:私有函数全部是"__"开头;函数参数全部以"_"开头 等等。 87 | 88 | 其次,虽然最初的设计一直是秉承着业务性无关的设计,但在实际开发过程中仍然难免带上了微信的业务性相关代码,比较典型的就是 newdns 。为了 Mars 以后的维护以及保证开源出去代码的同源,在开源出去之前必须把这些业务性有关的代码抽离出来,抽离后的结构如下: 89 | 90 | * mars-open 也就是要开源出去的代码,独立 git repo。 91 | * mars-private 是可能开源出去的代码,依赖 mars-open。 92 | * mars-wechat 是微信业务性相关的代码,依赖 mars-open 和 mars-private。 93 | 94 | 最后,为了接口更易用,对调用接口以及回调接口的参数也进行了反复思考与修改。 95 | 96 | ####编译优化 97 | 在 Mars之前,是直接给 Android 提供动态库(.so),因为代码逻辑都已经固定,不需要有可定制的部分。给 Apple 系平台提供静态库(.a),因为对外暴露的函数几乎不会改变,直接把相应的头文件放到相应的项目里就行。但对外开源就完全不一样了:日志的加密算法可能别人需要自己实现;长连或者短连的包头有人需要自己定制;对外接口的头文件我们可能会修改…… 98 | 99 | 为了让使用者可定制代码,对于编译 Android 平台我们提供了两种选择:1. 动态库。有些可能需要定制的代码都提供了默认实现。2. 先编译静态库,再编译动态库。编译出来静态库后,实现自己需要定制的代码后,执行 ndk-build 后即可编译出来动态库。 对于 Apple 系平台,把头文件全部收拢为 Mars 维护,直接编译出 Framework。 100 | 101 | 为了能让开发者快速的入门,我们提供了 Android、iOS、OS X 平台的 demo,其他平台的编译和 demo 会在不久就加上支持。 102 | 103 | 成型的 Mars 结构图如下: 104 | 105 | ![](assets/mars/mars.png) 106 | 107 | ####业界对比 108 | 我们做的一直都不是满足所有需求的组件,只是做了一个更适合我们使用的组件,这里也列了下和同类型的开源代码的对比。 109 | 110 | | |Mars|AFNetworking|OkHttp| 111 | |----|----|----|---| 112 | |跨平台|yes|no|no|no| 113 | |实现语言|C++|Objective-C|Java| 114 | |具体实现|基于 socket|基于 HTTP|基于 HTTP| 115 | |支持完整的 HTTP|no|yes|yes| 116 | |支持长连|yes|no|no| 117 | |DNS 扩展|yes|no|yes| 118 | |结合移动 App做设计|yes|no|no| 119 | #####可以看出: 120 | 1. Mars 中包括一个完整的高性能的日志组件 xlog; 121 | 2. Mars 中 STN 是一个跨平台的 socket 层解决方案,并不支持完整的 HTTP 协议; 122 | 3. Mars 中 STN 模块是更加贴合“移动互联网”、“移动平台”特性的网络解决方案,尤其针对弱网络、平台特性等有很多的相关优化策略。 123 | 124 | 总的来说,Mars 是一个结合移动 App 所设计的基于 socket 层的解决方案,在网络调优方面有更好的可控性,对于 HTTP 完整协议的支持,已经考虑后续版本会加入。 125 | 126 | ##Sample 127 | 试用 Android sample 请在「WeMobileDev」 公众账号输入 mars 获取下载链接。iOS sample 请通过 Github 编译获得。另: Sample 聊天室里的人不是机器人,也和你一样是尝鲜者。 128 | 129 | ##总结 130 | 经常有朋友和我说:发现网络信号差的时候或者其他应用不能用的时候,微信仍然能发出去消息。不知不觉我们好像什么都没做,回头看,原来我们已经做了这么多。我想,并不是任何一行代码都可以经历日活跃5亿用户的考验,感谢微信给我们提供了这么一个平台。现在我们想把这些代码和你们分享,运营方式上 Mars 所开源出去的代码会和微信所用的代码保持同源,所有开源出去的代码也首先会在微信上验证通过后再公开。开源并不是结束,只是开始。我们后续仍然会继续探索在移动互联网下的网络优化。Talk is cheap, show you our code. 131 | ********************* 132 | 关注 Mars , 来 Github 给我们 star 吧 133 | > https://github.com/Tencent/mars 134 | 135 | 查看 Mars 项目源码,请点击[阅读全文]。 136 | 137 | -------------------------------------------------------------------------------- /微信终端跨平台组件 Mars 系列(一) - 高性能日志模块xlog.md: -------------------------------------------------------------------------------- 1 | ###前言 2 | *** 3 | mars 是微信官方的终端基础组件,是一个使用 C++ 编写的业务性无关,平台性无关的基础组件。目前已接入微信 Android、iOS、Mac、Windows、WP 等客户端。现正在筹备开源中,它主要包括以下几个部分: 4 | 5 | 1. comm:可以独立使用的公共库,包括 socket、线程、消息队列等 6 | 2. xlog:可以独立使用的日志模块 7 | 3. sdt:可以独立使用的网络诊断模块 8 | 4. stn:可以独立使用的信令分发网路模块 9 | 10 | 本文章是 mars 系列的第一篇:高性能跨平台日志模块。 11 | 12 | ###正文 13 | ******* 14 | 对于移动开发者来说,最大的尴尬莫过于用户反馈程序出现问题,但因为不能重现且没有日志无法定位具体原因。这样看来客户端日志颇有点“养兵千日,用兵一时”的感觉,只有当出现问题且不容易重现时才能体现它的重要作用。为了保证关键时刻有日志可用,就需要保证程序整个生命周期内都要打日志,所以日志方案的选择至关重要。 15 | 16 | 17 | ####常规方案 18 | ******** 19 | >方案描述: 对每一行日志加密写文件 20 | 21 |
22 | 例如 Android 平台使用 java 实现日志模块,每有一句日志就加密写进文件。这样在使用过程中不仅存在大量的 GC,更致命的是因为有大量的 IO 需要写入,影响程序性能很容易导致程序卡顿。选择这种方案,在 release 版本只能选择把日志关掉。当有用户反馈时,就需要给用户重新编一个打开日志的安装包,用户重新安装重现后再通过日志来定位问题。不仅定位问题的效率低下,而且并不能保证每个需要定位的问题都能重现。这个方案可以说主要是为程序发布前服务的。 23 | 24 |
25 | 来看一下直接写文件为什么会导致程序卡顿 26 | ![](http://o9gsoxx4j.bkt.clouddn.com/16-10-5/54469556.jpg) 27 | 28 |
29 | 当写文件的时候,并不是把数据直接写入了磁盘,而是先把数据写入到系统的缓存(dirty page)中,系统一般会在下面几种情况把 dirty page 写入到磁盘: 30 | * 定时回写,相关变量在/proc/sys/vm/dirty_writeback_centisecs和/proc/sys/vm/dirty_expire_centisecs中定义。 31 | * 调用 write 的时候,发现 dirty page 占用内存超过系统内存一定比例,相关变量在/proc/sys/vm/dirty_background_ratio( 后台运行不阻塞 write)和/proc/sys/vm/dirty_ratio(阻塞 write)中定义。 32 | * 内存不足。 33 | 34 | 数据从程序写入到磁盘的过程中,其实牵涉到两次数据拷贝:一次是用户空间内存拷贝到内核空间的缓存,一次是回写时内核空间的缓存到硬盘的拷贝。当发生回写时也涉及到了内核空间和用户空间频繁切换。 35 | dirty page 回写的时机对应用层来说又是不可控的,所以性能瓶颈就出现了。 36 | 37 | 38 |
39 | 这个方案存在的最主要的问题:因为性能影响了程序的**流畅性**。对于一个 App 来说,流畅性尤为重要,因为流畅性直接影响用户体验,最基本的流畅性的保证是使用了日志不会导致卡顿,但是流畅性不仅包括了系统没有卡顿,还要尽量保证没有 CPU 峰值。所以一个优秀的日志模块必须保证**流畅性**: 40 | * 不能影响程序的性能。最基本的保证是使用了日志不会导致程序卡顿 41 | 42 |
43 | 我觉得绝大部分人不会选择这一个方案。 44 | ###进一步思考 45 | ********* 46 | 在上个方案中,因为要写入大量的 IO 导致程序卡顿,那是否可以先把日志缓存到内存中,当到一定大小时再加密写进文件,为了进一步减少需要加密和写入的数据,在加密之前可以先进行压缩。至于 Android 下存在频繁 GC 的问题,可以使用 C++ 来实现进行避免,而且通过 C++ 可以实现一个平台性无关的日志模块。 47 | >方案描述:把日志写入到作为 log 中间 buffer 的内存中,达到一定条件后压缩加密写进文件。 48 | 49 | 这个方案的整体的流程图: 50 | 51 | ![](http://o9gsoxx4j.bkt.clouddn.com/16-10-5/94646493.jpg) 52 | 53 |
54 | 这个方案基本可以解决 release 版本因为流畅性不敢打日志的问题,并且对于流畅性解决了最主要的部分:由于写日志导致的程序卡顿的问题。但是因为压缩不是 realtime compress,所以仍然存在 CPU 峰值。但这个方案却存在一个致命的问题:丢日志。 55 | 56 |
57 | 理想中的情况:当程序 crash 时, crash 捕捉模块捕捉到 crash, 然后调用日志接口把内存中的日志刷到文件中。但是实际使用中会发现程序被系统杀死不会有事件通知,而且很多异常退出,crash 捕捉模块并不一定能捕捉到。而这两种情况恰恰是平时跟进的重点,因为没有 crash 堆栈辅助定位问题,所以丢日志的问题这个时候显得尤为凸显。 58 | 59 |
60 | 在实际实践中,Android 可以使用共享内存做中间 buffer 防止丢日志,但其他平台并没有太好的办法,而且 Android 4.0 以后,大部分手机不再有权限使用共享内存,即使在 Android 4.0 之前,共享内存也不是一个公有接口,使用时只能通过系统调用的方式来使用。所以这个方案仍然存在不足: 61 | 62 | * 如果损坏一部分数据虽然不会累及整个日志文件但会影响整个压缩块 63 | 64 | * 个别情况下仍然会丢日志,而且集中压缩会导致 CPU 短时间飙高 65 | 66 |
67 | 通过这个方案,可以看出日志不仅要保证程序的**流畅性**,还要保证日志内容的**完整性**和**容错性**: 68 | * 不能因为程序被系统杀掉,或者发生了 crash, crash 捕捉模块没有捕捉到导致部分时间点没有日志, 要保证程序整个生命周期内都有日志。 69 | * 不能因为部分数据损坏就影响了整个日志文件,应该最小化数据损坏对日志文件的影响。 70 | 71 | #### mars 的日志模块 xlog 72 | ********* 73 | 前面提到了使用内存做中间 buffer 做日志可能会丢日志,直接写文件虽然不会丢日志但又会影响性能。所以亟需一个既有直接写内存的性能,又有直接写文件的可靠性的方案,也就是 mars 在用的方案。 74 | 75 | #####mmap 76 | mmap 是使用逻辑内存对磁盘文件进行映射,中间只是进行映射没有任何拷贝操作,避免了写文件的数据拷贝。操作内存就相当于在操作文件,避免了内核空间和用户空间的频繁切换。 77 | 78 | ![](http://o9gsoxx4j.bkt.clouddn.com/16-10-5/2522870.jpg) 79 | 80 |
81 | 为了验证 mmap 是否真的有直接写内存的效率,我们写了一个简单的测试用例:把512 Byte的数据分别写入150 kb大小的内存和 mmap,以及磁盘文件100w次并统计耗时 82 | 83 | ![](http://o9gsoxx4j.bkt.clouddn.com/16-10-5/74645782.jpg) 84 | 85 |
86 | 87 | 88 | 从上图看出mmap几乎和直接写内存一样的性能,而且 mmap 既不会丢日志,回写时机对我们来说又基本可控。 mmap 的回写时机: 89 | * 内存不足 90 | 91 | * 进程 crash 92 | 93 | * 调用 msync 或者 munmap 94 | 95 | * 不设置 MAP_NOSYNC 情况下 30s-60s(仅限FreeBSD) 96 | 97 | 98 |
99 | 如果可以通过引入 mmap 既能保证高性能又能保证高可靠性,那么还存在的其他问题呢?比如集中压缩导致 CPU 短时间飙高,这个问题从上个方案就一直存在。而且使用 mmap 后又引入了新的问题,可以看一下使用 mmap 之后的流程: 100 | 101 | 102 | ![](http://o9gsoxx4j.bkt.clouddn.com/16-10-5/8731278.jpg) 103 | 104 | 前面已经介绍了,当程序被系统杀掉会把逻辑内存中的数据写入到 mmap 文件中,这时候数据是明文的,很容易被窥探,可能会有人觉得那在写进 mmap 之前先加密不就行了,但是这里又需要考虑,是压缩后再加密还是加密后再压缩的问题,很明显先压缩再加密效率比较高,这个顺序不能改变。而且在写入 mmap 之前先进行压缩,也会减少所占用的 mmap 的大小,进而减少 mmap 所占用内存的大小。所以最终只能考虑:是否能在写进逻辑内存之前就把日志先进行压缩,再进行加密,最后再写入到逻辑内存中。问题明确了:就是怎么对单行日志进行压缩,也就是其他模块每写一行日志日志模块就必须进行压缩。 105 | #####压缩 106 | 现在是研究压缩的时候了。比较通用的压缩方案是先进行短语式压缩, 短语式压缩过程中有两个滑动窗口,历史滑动窗口和前向缓存窗口,在前向缓存窗口中通过和历史滑动窗口中的内容进行匹配从而进行编码。 107 | 108 | ![](http://o9gsoxx4j.bkt.clouddn.com/16-10-5/82359559.jpg) 109 | 110 | 比如这句绕口令:吃葡萄不吐葡萄皮,不吃葡萄倒吐葡萄皮。中间是有两块重复的内容“吃葡萄”和“吐葡萄皮”这两块。第二个“吃葡萄”的长度是 3 和上个“吃葡萄”的距离是 10 ,所以可以用 (10,3) 的值对来表示,同样的道理“吐葡萄皮”可以替换为 (10,4 ) 111 | 112 | ![](http://o9gsoxx4j.bkt.clouddn.com/16-10-5/86780648.jpg) 113 | 114 |
115 | 这些没压缩的字符通过 ascci 编码其实也是 0-255 的整数,所以通过短语式压缩得到的结果实质上是一堆整数。对整数的压缩最常见的就是 huffman 编码。通用的压缩方案也是这么做的,当然中间还掺杂了游程编码,code length 的转换。但其实这个不是关注的重点。我们只需要明白整个压缩过程中,短语式压缩也就是 LZ77 编码完成最大的压缩部分也是最重要的部分就行了,其他模块的压缩其实是对这个压缩结果的进一步压缩,进一步压缩的方式主要使用 huffman 压缩,所以这里就需要基于数字出现的频率进行统计编码,也就是说如果滑动窗口大小没上限的前提下,越多的数据集中压缩,压缩的效果就越好。日志模块使用这个方案时压缩效果可以达到 86.3%。 116 | 117 |
118 | 既然 LZ77 编码已经完成了大部分压缩,那么是否可以弱化 huffman 压缩部分,比如使用静态 huffman 表,自定义字典等。于是我们测试了四种方案: 119 | ![](http://o9gsoxx4j.bkt.clouddn.com/16-10-5/13312352.jpg) 120 | 121 |
122 | 这里可以看出来后两种方案明显优于前两种,压缩率都可以达到 83.7%。第三种是把整个 app 生命周期作为一个压缩单位进行压缩,如果这个压缩单位中有数据损坏,那么后面的日志也都解压不出来。但其实在短语式压缩过程中,滑动窗口并不是无限大的,一般是 32kb ,所以只需要把一定大小作为一个压缩单位就可以了。这也就是第四个方案, 这样的话即使压缩单位中有部分数据损坏,因为是流式压缩,并不影响这个单位中损坏数据之前的日志的解压,只会影响这个单位中这个损坏数据之后的日志。 123 | 124 |
125 | 对于使用流式压缩后,我们采用了三台安卓手机进行了耗时统计,和之前使用通用压缩的的日志方案进行了对比(耗时为单行日志的平均耗时): 126 | ![](http://o9gsoxx4j.bkt.clouddn.com/16-10-5/25464129.jpg) 127 | 128 |
129 | 通过横向对比,可以看出虽然使用流式压缩的耗时是使用多条日志同时压缩的 2.5 倍左右,但是这个耗时本身就很小,是微秒级别的,几乎不会对性能造成影响。最关键的,多条日志同时压缩会导致 CPU 曲线短时间内极速升高,进而可能会导致程序卡顿,而流式压缩是把时间分散在整个生命周期内,CPU 的曲线更平滑,相当于把压缩过程中使用的资源均分在整个 app 生命周期内。 130 | 131 |
132 | #####xlog 方案总结 133 | 该方案的简单描述: 134 | >使用流式压缩方式对单行日志进行压缩,压缩加密后写进作为 log 中间 buffer的 mmap 中,当 mmap 中的数据到达一定大小后再写进磁盘文件中。 135 | 136 | 虽然使用流式压缩并没有达到最理想的压缩率,但和 mmap 一起使用能兼顾**流畅性 完整性 容错性 **的前提下,83.7%的压缩率也是能接受的。使用这个方案,除非 IO 损坏或者磁盘没有可用空间,基本可以保证不会丢失任何一行日志。 137 | 138 | 在实现过程中,各个平台上也踩了不少坑,比如: 139 | 140 | * iOS 锁屏后,因为文件保护属性的问题导致文件不可写,需要把文件属性改为 NSFileProtectionNone。 141 | 142 | * boost 使用 ftruncate 创建的 mmap 是稀疏文件,当设备上无可用存储时,使用 mmap 过程中可能会抛出 SIGBUS 信号。通过对新建的 mmap 文件的内容全写'0'来解决。 143 | 144 | * …… 145 | 146 | 147 |
148 | 日志模块还存在一些其他策略: 149 | 150 | * 每次启动的时候会清理日志,防止占用太多用户磁盘空间 151 | 152 | * 为了防止 sdcard 被拔掉导致写不了日志,支持设置缓存目录,当 sdcard 插上时会把缓存目录里的日志写入到 sdcard 上 153 | * …… 154 | 155 | 156 | 157 |
158 | 在使用的接口方面支持多种匹配方式: 159 | * 类型安全检测方式:%s %d 。例如:xinfo(“%s %d”, “test”, 1) 160 | * 序号匹配的方式:%0 %1 。例如:xinfo(TSF”%0 %1 %0”, “test”, 1) 161 | * 智能匹配的懒人模式:%_ 。例如:xinfo(TSF”%_ %_”, “test”, 1) 162 | 163 | 164 | 165 | 166 |
167 | ####最后 168 | ***** 169 | 对于终端设备来说,打日志并不只是把日志信息写到文件里这么简单。除了前文提到的**流畅性 完整性 容错性**,还有一个最重要的是**安全性**。基于**不怕被破解,但也不能任何人都能破解**的原则,对日志的规范比加密算法的选择更为重要,所以本文并没有讨论这一点。 170 | 171 |
172 | 从前面的几个方案中可以看出,一个优秀的日志模块必须做到: 173 | * 不能把用户的隐私信息打印到日志文件里,不能把日志明文打到日志文件里。 174 | * 不能影响程序的性能。最基本的保证是使用了日志不会导致程序卡顿。 175 | * 不能因为程序被系统杀掉,或者发生了 crash,crash 捕捉模块没有捕捉到导致部分时间点没有日志, 要保证程序整个生命周期内都有日志。 176 | * 不能因为部分数据损坏就影响了整个日志文件,应该最小化数据损坏对日志文件的影响。 177 | 178 | 179 | 上面这几点也即**安全性 流畅性 完整性 容错性**, 它们之间存在着矛盾关系: 180 | * 如果直接写文件会卡顿,但如果使用内存做中间 buffer 又可能丢日志 181 | * 如果不对日志内容进行压缩会导致 IO 卡顿影响性能,但如果压缩,部分损坏可能会影响整个压缩块,而且为了增大压缩率集中压缩又可能导致 CPU 短时间飙高。 182 | 183 | 184 | 185 | mars 的日志模块 xlog 就是在兼顾这四点的前提下做到:高性能高压缩率、不丢失任何一行日志、避免系统卡顿和 CPU 波峰。 186 | -------------------------------------------------------------------------------- /聊聊苹果的Bug - iOS 10 nano_free Crash.md: -------------------------------------------------------------------------------- 1 | 2 | 背景 3 | -- 4 | 5 | iOS 10.0-10.1.1上,新出现了一类堆栈为nano\_free字样的crash问题,困扰了我们一段时间,这里主要分享解决这个问题的思路,最后尝试提出一个解决方案可供参考。 6 | 7 | 它的crash堆栈如下图: 8 | 9 | ```coffee 10 | Thread 0 Crashed: 11 | 0 libsystem_kernel.dylib __pthread_kill (in libsystem_kernel.dylib) 12 | 1 libsystem_c.dylib abort (in libsystem_c.dylib) 13 | 2 libsystem_malloc.dylib nanozone_error (in libsystem_malloc.dylib) 14 | 3 libsystem_malloc.dylib nano_free (in libsystem_malloc.dylib) 15 | ``` 16 | 17 | 这种crash我们并不陌生,一般野指针的问题,也是这样的堆栈。但在iOS 10发布之后,这类crash就嗖地窜到了微信的crash排行榜的前列,而此时微信并没有发布新版本。 通过和一些内部、外部团队的交流,发现这是个共性问题,例如: 这两种迹象表明,这很可能是苹果的bug。按流程,我们向苹果提了bug report,并得到回复:“iOS 10.2 Beta有稳定性提升”。 18 | 19 | 终于等到iOS 10.2 Beta发布,我们重新统计了此类crash的系统版本分布。发现不仅在10.2 Beta正常,而且iOS 9也没有crash。苹果给我们的建议是:“引导用户升级系统”。这当然能解决问题,但用户升级系统是个漫长的周期。 20 | 21 | ![](assets/nano_free/crash_os.png) 22 | 23 | 而其实我们非常关注这个问题的原因,不仅是线上版本的crash,更是在我们的开发分支,它的crash概率异常的高。如果不搞清楚触发crash的原因,那这将是一颗定时炸弹,不知道何时就会被我们合入主线,发布出去。因此我们着手开始做一些尝试。 24 | 25 | 26 | 尝试 27 | -- 28 | 29 | 首先我们的切入点是iOS 9和10.2 Beta没有crash。既然如此,能否将正常的代码合入微信,替换掉系统的呢? 30 | 31 | ### 尝试一:替换dylib 32 | 33 | 各版本的dylib可以在mac os的`~/Library/Developer/Xcode/iOS DeviceSupport/`找到,我们选了9.3.5的dylib。尝试编入时报错: 34 | 35 | ```c 36 | ld: cannot link directly with /Users/sanhuazhang/Desktop/TestNanoCrash/libsystem_malloc.dylib. Link against the umbrella framework 'System.framework' instead. for architecture arm64 37 | clang: error: linker command failed with exit code 1 (use -v to see invocation) 38 | ``` 39 | 40 | 这个是因为dylib的`LY_SUB_FRAMEWORK`段指明该dylib属于`System.framework`,直接被编译器拒绝了。看来没有办法。(如果有同学知道如何绕过这个保护,麻烦告知我) 41 | 42 | ### 尝试二:编入源码 43 | 44 | `libsystem_malloc.dylib`的源码可以在找到。这里有多个版本,用otool找到iOS 9.3.5对应的源码是libmalloc-67.40.1.tar.gz。 45 | 46 | ![](assets/nano_free/otool.jpg) 47 | 48 | 然而这份源码是不完整的,只能读不能编译。看来这个方法也行不通。 49 | 50 | 51 | 阅读源码 52 | ---- 53 | 54 | 上述两个方法不行,就有点束手无策了,只能阅读源码,尝试找突破口。 55 | 56 | 在`libsystem_malloc.dylib`中,对内存的管理有两个实现:nano zone和scalable zone。他们分别管理不同大小的内存块: 57 | 58 | ![](assets/nano_free/malloc.png) 59 | 60 | 可以看到nano zone的管理区间和scalable zone是有重叠的,可以认为nano zone是一个针对小内存的优化方法。这两种方法通过MallocZoneNano的环境变量进行配置: 61 | 62 | 当`MallocZoneNano=1`时,default zone为nano zone,不满足nano zone的内存会fall through到它的helper zone,而helper zone是一个scalable zone。 63 | 64 | 当`MallocZoneNano=0`时,deafult zone为scalable zone。 65 | 66 | 通过`getenv("MallocZoneNano")`可以拿到环境变量的值,我们发现,在iOS 9和iOS 10.2 Beta中,`MallocZoneNano=0`,而其他系统`MallocZoneNano=1`。换句话说,苹果并不是修复了这个问题,而只是屏蔽了。因此其实我们在尝试一中提到替换dylib,即使替换成功,也是不解决问题的。 67 | 68 | 结合最初的crash堆栈,我们知道crash是发生在nano zone内的,那是否可以关掉nano zone呢? 69 | 70 | ### 尝试三:修改环境变量`MallocZoneNano=0` 71 | 72 | 通过`setenv`方法,可以设置环境变量,修改`MallocZoneNano=0`。然而并没有生效,因为dylib的初始化在微信之前,此时微信还未启动。 73 | 74 | 根据苹果的文档,Info.plist的`LSEnvironment`字段,可以设置环境变量,然而这个只适用于mac os。 75 | 76 | ![](assets/nano_free/LSEnvironment.png) 77 | 78 | 在Xcode的Schema里设置`MallocZoneNano=0`后,本地不再出现crash。 79 | 80 | ![](assets/nano_free/xcode_schema.jpg) 81 | 82 | 但schema只适用于调试阶段,不能编进app里。 83 | 84 | 看来这个方法也行不通,但起码验证了,关掉nano zone是一个可行的解决方案。 85 | 86 | ### 尝试四:hook 87 | 88 | 既然无法完全关闭nano zone,那就尝试跳过它。 89 | 90 | 首先通过`malloc_zone_create`创建一个新的zone,命名为guard zone。而自己的创建的zone都是scalable zone的实现,因此不会出现crash。然后通过fishhook,将`malloc`和`malloc_zone_malloc`等一众常用的内存管理的方法,转发到guard zone,crash概率降了不少。但fishhook无法hook掉其他dylib的调用,也就是说,系统的调用(如Cocoa、CoreFoundation等)依然是走nano zone,还是会crash。并不彻底解决问题。 91 | 92 | ### 尝试五:跳过nano zone 93 | 94 | 从上面我们知道,nano zone管理的是0-256字节的内存,如果内存不在这个区间,则会fall through到helper zone。而zone的结构是公开的: 95 | 96 | ![](assets/nano_free/malloc_zone_t.jpg) 97 | 98 | 那么可以用tricky一点的方法:修改nano zone和helper zone的函数指针,让nano zone的内存申请虚增,超过256字节,以骗过nano zone,而fall through到helper zone后,再恢复为真正的大小。以malloc为例,具体实现为: 99 | 100 | ![](assets/nano_free/tricky_fall_through.jpg) 101 | 102 | 由于内存有限,size的最高位一般不会被使用,因此我们可以用这一位来标记。 103 | 104 | 当我满心以为终于解决问题时,却发现,crash概率不仅没有降低,反而到了几乎必现的程度。而此时除了少数在替换前就申请的内存是走的nano zone,其他内存都是在scalable zone内被管理。这一现象不禁让人怀疑,nano\_free的crash,很可能是zone判断错误 - 在scalable zone申请的内存,却在nano zone中释放。 105 | 106 | 要验证这个问题,还得从源码中搞清楚一个指针是怎么区分属于nano zone还是scalable zone的: 107 | 108 | ```coffee 109 | //nano_malloc.c 110 | #if defined(__x86_64) 111 | #define NANO_SIGNATURE_BITS 20 112 | #define NANOZONE_SIGNATURE 0x00006ULL // 0x00006nnnnnnnnnnn the address range devoted to us. 113 | #define NANO_MAG_BITS 5 114 | #define NANO_BAND_BITS 18 115 | #define NANO_SLOT_BITS 4 116 | #define NANO_OFFSET_BITS 17 117 | #else 118 | #error Unknown Architecture 119 | #endif 120 | 121 | struct nano_blk_addr_s { 122 | uint64_t 123 | nano_offset:NANO_OFFSET_BITS, // locates the block 124 | nano_slot:NANO_SLOT_BITS, // bucket of homogenous quanta-multiple blocks 125 | nano_band:NANO_BAND_BITS, 126 | nano_mag_index:NANO_MAG_BITS, // the core that allocated this block 127 | nano_signature:NANO_SIGNATURE_BITS; // 0x00006nnnnnnnnnnn the address range devoted to us. 128 | }; 129 | ``` 130 | 131 | 可以看到,在x86下,是通过获取指针地址所属的段来判断zone的。当signature满足0x00006这个段时,则属于nano zone。 132 | 133 | 虽然这份代码里没有提供arm下的判断方式,但可以结合源码中对signature判断的函数,并通过符号断点,很快就能找到arm下比较signature的汇编。 134 | 135 | ![](assets/nano_free/assembly.jpg) 136 | 137 | 即:当ptr\>\>28==0x17时,属于nano zone。 138 | 139 | 通过测试代码可以发现,确实小于256字节的指针都是0x17。测试代码中大于256字节则落在了0x11。然而,代码跑了一阵子之后,大于256字节的指针也落在了0x17段。似乎我们已经很接近问题了。再来一段测试代码验明真身。 140 | 141 | ![](assets/nano_free/scalable0x17.jpg) 142 | 143 | 先通过循环不断地申请257字节的内存,并保存起来,这些内存应该都落在scalable zone中。刚开始的指针是0x11的,当出现0x17时,我们break掉。可以假设在此之后scalable zone内申请的内存,都是0x17,具体代码为: 144 | 145 | ```coffee 146 | //Nano Crash测试代码,适用于iOS 10 真机 147 | #define NANO_MAX (256) 148 | std::vector ptrs; 149 | while (1) { 150 | uintptr_t ptr = (uintptr_t)malloc(NANO_MAX+1); 151 | ptrs.push_back(ptr); 152 | if (ptr>>28==0x17) { 153 | break; 154 | } 155 | } 156 | ``` 157 | 158 | 我们新建了一个iOS的Single View Application,除了这段代码,没有做其他任何的修改。问题重现了: 159 | 160 | ![](assets/nano_free/crash.jpg) 161 | 162 | ### 解决方案 163 | 164 | 从重现的代码来看,要真正减少nano crash的出现,只能是减少内存的使用,但这并不好操作。因此,解决思路还是回到保护上。 165 | 166 | 结合上面提到尝试3和4,我们进行了这样的修改。 167 | 168 | 1\. 创建一个自己的zone,命名为guard zone。 169 | 170 | 2\. 修改nano zone的函数指针,重定向到guard zone。 171 | 172 | 3\. 对于没有传入指针的函数,直接重定向到guard zone。 173 | 174 | 4\. 对于有传入指针的函数,先用size判断所属的zone,再进行分发。 175 | 176 | 这里需要特别注意的是,因为在修改函数指针前,已经有一部分指针在nano zone中申请了。因此需找到每个传入的指针所属的zone。代码示例为: 177 | 178 | ![](assets/nano_free/nano_crash_guard_1.jpg) 179 | 180 | ![](assets/nano_free/nano_crash_guard_2.jpg) 181 | 182 | 该问题不止有一种方式解决。这种方式目前还在灰度中,若要使用,请搭配适当的灰度和回退措施。 183 | 184 | 185 | 186 | 187 | 188 | --------------------------------------------------------------------------------