普通视图
iOS 知识点 - Block 讲解
不踩坑!苹果开发者账号:公司号和个人号,到底该怎么选?
脱离 SwiftUI 也能用 @Observable:深入理解 withObservationTracking 的玩法、坑点与 Swift 6 突围
iOS 知识点 - Category / Extension / Protocol 小合集
独立开发者亲测:MLX框架让我的App秒变AI原生!15年iOS老兵的2025新感悟
[转载] 基于dylib注入原理实现iOS热重载框架CocoaHotReload
背景
在iOS开发过程中,是否经常遇到这样的场景:简单修改几行代码,想要立刻看到效果,却需要重新增量编译,苦苦等待编译、链接、安装、运行这个漫长的过程;特别是在中大型项目(如手Q)中,增量编译耗时也要60s甚至更多,非常影响开发效率。
为了解决增量编译,业界上也有IPAPatch及InjectionIII方案:
IPAPatch 是基于编译子工程,链接为动态库打包到宿主工程,需要改变代码依赖逻辑,使用局限性大且效果并不理想。
InjectionIII 也是基于dylib注入原理实现的iOS热重载能力,但是不支持真机以及dylib注入时仅简单处理函数替换,热重载的能力存在比较大的局限性,如存在对release版本静态库或动态库依赖的代码文件或存在被hook的类的实现代码文件进行热重载,要么无法支持,要么可能出现循环调用而导致应用出现Crash,且对Swift、C++代码的热重载支持也非常有限。如果基于InjectionIII的基础上进行开发,后续如果InjectionIII更新则不利于迭代。
上述两个方案均不能满足我们的需求,因此我们决定在基于dylib注入的原理基础上对热重载实现进行探索实现。
挑战
在复杂项目中实现热重载能力,我们面临的挑战也是巨大的。这里主要体现在dylib动态库的生成及注入后复杂场景的处理,在保证代码快速生效的同时也要保证业务逻辑的稳定性。
下图展示了iOS 源码文件的编译流程:
![]()
-
预编译(Precompile): 编译预处理,即替换宏、删除注释、展开头文件等,产生
.i文件。 -
编译(Compiling): 将之前的.i文件转换为汇编语言,产生
.s文件。 -
汇编(Assembly): 将汇编语言转换为机器码文件,产生
.o文件。 -
链接(Link): 将所以
.o文件以及依赖的库链接后生成Mach-O类型的可执行文件。
可以看出,要生成dylib可执行文件,就需要获取源码文件,并进行预编译、编译及汇编操作后生成.o文件,再分析其依赖的库(.a或.framework),最后链接后才可生成dylib。将dylib注入到运行的App中,并进行逻辑替换,即可使得代码生效,实现热重载能力。
所以我们就需要对源码文件变化的进行监听、查找对应的编译指令并使用xcode编译工具(clang swift)进行文件编译,接着分析编译后的.o文件依赖的库,并进行链接后,最终通过生成dylib。
此外,生成dylib库后还需要有通信模块,将动态库发送到App端,进行dlopen注入到app中。
这里需要特别提下真机情况:通过dlopen注入时,会因为安全校验,导致dlopen失败,不过好消息是,在iOS13后,苹果在debug模式下开放了这个能力,使得热重载能力在真机上得到应用,而且在dlopen动态库后,还需要针对不同语言(ObjC、Swift、C)通过Runtime及其他方式进行代码逻辑的处理,才能保证代码及时生效。
针对复杂场景,如hook场景,还需要做保存原有调用栈,及避免死循环等逻辑的处理,保证逻辑正常运行及功能的稳定性,这又加大了热重载能力实现的难度。
设计与实现
项目架构
如前文分析,为了实现dylib的生成和注入以及两端之间的通信完成热重载能力,我们的思路是设计Mac app和iOS framework来分别完成dylib的生成和注入,且两端的网络通信是通过Socket(TCP/USB)来完成。
主要的架构图如下:
![]()
整个项目分为Mac app和iOS framework两部分。
Mac app
主要职责是根据修改文件查找对应的编译指令并执行生成.o文件,接着链接.o文件及依赖的库生成dylib动态库。
主要的处理逻辑为标红部分:
ComplileCommandManager:
主要负责解压编译日志并通过正则表达式进行编译指令的提取&缓存,其中核心的正则表达式如下:
1 |
// 提取编译指令正则表达式 |
- FileDependentManager : 主要分析
.h文件的依赖关系,当改动到.h文件时,需要分析所有依赖该.h的所有.m文件,并进行重编译,才能保证这个.h的改动能完全生效。分析依赖的逻辑是直接分析Xcode编译的中间产物Intermediates.noindex目录下的.d文件,该文件中记录.m依赖的所有文件。
举个例子,ViewController.m 引用的头文件如下:
1 |
#import "ViewController.h" |
对应生成的ViewController.d文件如下:
1 |
dependencies: \ |
- LibManager:主要职责是分析编译日志中,各个Target依赖的
framework、lib,并查找私有符号库,并在.o文件link为dlyib时,进行依赖私有符号库查找一同link打包到dylib中。主要目的是为了解决dlopen dylib时,出现Symbol not found的问题,后面内容会详细介绍。
这里查找私有符号库的条件是使用nm工具进行判断的,判断条件如下:
1 |
// 判断私有符号库 |
iOS Framework
主要职责是进行动态库注入和资源包(xib、storyboard)更新。
其中,主要的逻辑处理是类替换和hook函数的处理。
-
类替换:
- ObjC 的替换主要是通过Runtime实现的,通过Runtime保证同一个类的所有的函数实现指针都指向最新注入的动态库地址,以保证所有镜像中的同一个类的实现都一致。
- Swift 的函数替换就较为复杂,由于Swfit涉及到和ObjC混编,及继承ObjC类或者与ObjC相互调用等复杂场景。导致Swift类的派发方式出现了Static、V-Table、Witness Table和Message四种派发方式,解决方案是根据这四种派发方式,分别进行函数指针替换以实现热重载能力。具体实现,后续内容会详细描述。
- C 函数的替换,由于C函数是直接派发的方式,所以不能通过Runtime机制处理,最终是通过第三方开源库 Dobby实现C函数替换。
-
hook函数处理:
-
由于热重载最主要的逻辑是函数替换,所以函数替换的处理逻辑要极其严谨,否则就会导致逻辑异常甚至Crash。比如当热重载一个存在被其他分类hook的时候,如果直接使用method_exchangeImplementations进行替换的话,就会破坏函数调用栈;例子如下:
- 如Class A的原始函数OriginMethod,有两个Category
分别hook了这个OriginMethod,其调用栈流程如下:1
Call OriginMethod -> hook1 imp -> hook2 imp -> OriginMehod imp
- 当热重载Calss A时,会产生新的New
OriginMethod实现,使用method_exchangeImplementations进行替换, 导致最终调用栈如下:1
Call OriginMethod -> New OriginMehtod imp
- 如Class A的原始函数OriginMethod,有两个Category
以上情况会导致直接不调用hook 1 imp 和 hook 2
-
imp,影响正常的业务逻辑。正确的做法应该是找到OriginMehod
imp这个节点,进行替换,来维持之前的调用栈。
问题来了,如果找到正确的替换节点呢?
-
需要知道哪些函数被hook
目前我们这里只考虑常规的hook方法(通过Category进行hook),所以优先判断函数是否属于Catrgory。
-
可通过如下代码获取imp信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* Structure filled in by dladdr().
*/
typedef struct dl_info {
const char *dli_fname; /* Pathname of shared object */
void *dli_fbase; /* Base address of shared object */
const char *dli_sname; /* Name of nearest symbol */
void *dli_saddr; /* Address of nearest symbol */
} Dl_info;
Dl_info info;
IMP imp = method_getImplementation(method);
int result = dladdr(imp, &info); -
如TestClass和TestClass (TestCategory) 两个类对应的函数如下:
1
2
3
4
5
6
7@interface TestClass : NSObject
- (void)testFunction;
@end
// 分类
@interface TestClass (TestCategory)
- (void)tc_testFunction;
@end -
获取到的Dl_info中的dli_sname分别如下:
1
2-[TestClass testFunction]
-[TestClass(TestCategory) tc_testFunction] 由上诉结果可以看出,Category函数是会包含分类名称的。由此来判断分类函数,至于判断是否被hook,只需判断method name 和 imp中的函数名,如果不一致则代表被hook了。
-
需要找到哪个函数指向的imp是当前要替换的函数的同名imp,即递归获取函数调用栈,找到method对应的imp name为当前热重载的method name一致的这个method进行指针替换。
- 关键代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44// 查找原始实现所在的method
+ (Method)methodWithOrigImpForOriginClass:(Class)originClass
originSel:(SEL)originSel
baseClass:(Class)baseClass
baseSel:(SEL)baseSel
isClassMethod:(BOOL)isClassMethod
{
if (!originClass || !originSel) {
return NULL;
}
NSString *baseClassName = NSStringFromClass(baseClass);
NSString *baseSelName = NSStringFromSelector(baseSel);
NSString *originClassName = NSStringFromClass(originClass);
NSString *originSelName = NSStringFromSelector(originSel);
Method originMethod = class_getInstanceMethod(originClass, originSel);
IMP orginSelImp = method_getImplementation(originMethod);
NSString *orginSelImpClassName = [self classNameForCategoryClassName:[self classNameForImp:orginSelImp]];
NSString *orginSelImpMethodName = [self methodNameForImp:orginSelImp];
// 这里只处理标准的hook即只在当前类的Category进行hook
if (![originClassName isEqualToString:orginSelImpClassName] ||
[originSelName isEqualToString:orginSelImpMethodName]) { // 没有被hook或者hook出现异常,返回base
return class_getInstanceMethod(baseClass, baseSel);
}
if (([baseClassName isEqualToString:orginSelImpClassName] &&
[baseSelName isEqualToString:orginSelImpMethodName])) { // 递归结束 // 返回需要baseClass类需要替换的函数
return class_getInstanceMethod(baseClass, originSel);
} else {
Class class = NSClassFromString(orginSelImpClassName);
if (isClassMethod) {
class = object_getClass(class);
}
SEL sel = sel_registerName(orginSelImpMethodName.UTF8String);
if (class && sel) { // 递归查找
return [self methodWithOrigImpForOriginClass:class originSel:sel baseClass:baseClass baseSel:baseSel isClassMethod:isClassMethod];
} else { // 失败 返回base
return class_getInstanceMethod(baseClass, baseSel);
}
}
}
工作原理
![]()
如上图所示,Mac app 和 iOS framework(CocoaHotReload.framework)之间是通过Socket(tcp/usb)进行发送command通信,连接成功后,Mac
app会监听文件变化和进行项目初始化(解压&解析编译日志、查找编译指令、签名、私有符号库),Xcode编辑源码并保存后,触发热重载指令,Mac app会查找修改文件的编译指令并执行,生成.o文件并分析.o文件依赖的私有符号库进行link后生成.dylib,并通过Socket发送到app;iOS framework 需内嵌在app中,当接收到.dylib时,通过Runtime&finshook进行类(ObjC&Swift)替换&C函数(动态库中)替换实现、资源(storyboardc&nib)更新及hook函数调用栈&死循环处理,实现dylib注入,完成一次热重载。
具体时序图如下:
![]()
所遇挑战
这里主要介绍下项目实现过程中,比较难的挑战点:
dlopen error: Symbol not found
由于这个项目是通过dylib注入原理来实现的,所以dlopen是加载动态库的必经之路,但是在dlopen的时遇到了Symbol not found的问题,导致后续注入操作无法进行,直接导致热重载失效,这对于热重载能力是致命的问题。
具体问题如下图所示:
![]()
该问题的背景如下:
![]()
如上图所示:触发热重载的文件是QQWalletViewController.mm,该文件依赖了libPayCenterSDK.a文件中的TenpayPlugin类;为了减包,libPayCenterSDK.a在Release模式下Symbol Hidden by Default 设置为YES,该设置导致符号都被隐藏,dlopen时就出现了上方的错误。
最开始为了解决这个问题,尝试了以下两个方案:
![]()
-
方案一: 直接将这个Release模式改为NO,这样虽说能解决符号隐藏问题,但是带来了两个问题
- 需要业务更改工程设置
- 引来业务增包问题(特别是在手Q中对于包增量是很敏感的),所以增包问题是无法接受的。
-
方案二: 在工程里面
link库时,debug模式下link是debug包,这样也可以解决问题。但是引来的其他问题是:业务都需要更改工程设置,且生成库的时候需要分别生成debug和release的库,对于业务使用热重载的成本还是不小。
以上两个方案,虽说解决了符号隐藏问题,但会带来业务增包问题和影响业务的工程设置。这对于用户使用来说并不友好。理想方案肯定既解决符号隐藏问题且对用户来说是无入侵、无感知且不要带来额外的问题。为了彻底解决问题,刨根问底,去探究下Symbol Hidden by Default这个设置对符号的影响。
这里通过下面的例子来分析下:
![]()
根据苹果官方文档说明:Symbol Hidden by Default设置对应符号的影响等用于使用__attribute__((visibility("default|hidden")))修饰函数,来控制符号的可见性,所以通过__atrribute__修饰来看下符号影响。
新建一个SymbolTest工程,依赖libSymbolVisibilityTest.a静态库。
静态库中有两个函数分别通过__attribute__的visibility("default")和visibility("hidden")修饰。
生成静态库后,通过nm -nm libSymbolVisibility.a 查看符号信息,可以看到通过default修饰的符号为external,公开符号,可对外导出。
通过hidden修饰的,符号为private external,表示私有符号,不可导出。
再看下最终静态库打包到app后的符号信息,通过nm -nm symbolTest查看符号信息,可以明显看出default修饰的函数仍为external 可导出符号。
而hidden修饰的函数,则变成non-external (was a private external)私有符号。
最后通过MachOView软件查看下SymbolText的Export Info对开开放的符号表信息可以看到只有external修饰的符号才会出现在这个表上面。
了解完Symbol Hidden by Default 设置对符号的影响后,来看下dlopen出现symbol not found的原因:
![]()
如上图所示,动态库test.dylib,在没有link libSymblVisibilityTest.a库的情况下使用了这个库中的SymbolVisibilityHidden函数,当dlopen test.dylib时,会解析dylib中的符号,发现SymbolVisibilityHidden这个符号为 undenfine symbol时,会尝试调用resolveUndefined继续查找,继续调用findExportedSymbolAddress去符号导出表里面查找,如果能查找到就返回,查找不到及调用throwSymbolNotFound函数抛出”Symbol not found xxx”异常。之前尝试通过Symbol hidden by default设置为No可解决的原因就是设置完,符号会出现在符号导出表中,可被找到。但刚才分析了这个解决方案的不足之处,所以我们在这个调用链上再次寻找解决方案,发现在第二步undefine symbol是因为库没有一起打包到dylib,如果将库打包到dylib中,就不会出现undefine symbol符号问题,
因此,完美的解决方案出现了 通过link依赖的私有符号库即可解决 。现在有了解决方案以后,主要面临了以下两个实现问题:
-
如何查找哪些是私有符号库?
- 首先,在解析编译日志的时候,需要解析出工程中所有target依赖的(farmework、lib),并且通过
nm -nm去分析这些库,只要库中出现了private external符号及表示私有符号库,并记录下来。
- 首先,在解析编译日志的时候,需要解析出工程中所有target依赖的(farmework、lib),并且通过
-
如何判断哪些符号是私有符号且需要
link哪个库?- 当生成
.o文件后,可通过nm -nm获取(undefined) external
获取未定义的符号,但是这里有个问题就是undefined符号包含大量系统符号和私有符号。为了提升效率就需要需要先把系统符号过滤掉,要想过滤掉系统符号,就要先获取私有符号的集合,通过集合去过滤。获取app私有符号的集合可通过nm -nm去分析app可执行文件,通过non-external (was a private external)来记录下整个app中的所有私有符号,即可过滤掉系统符号,并通过这些私有符号在对应的哪些库中,就可以知道需要link哪些库。
- 当生成
最终的完整的实现方案如下:
![]()
如上图所示,最终在项目初始化时,提前获取好项目的私有符号及私有符号库,触发热重载后,编译生成.o文件,获取.o的undedined symbol符号,并且通过项目私有符号过滤掉系统符后,查找undeined symbol所在的库中,接着对依赖的库进行库瘦身再剔除库间重复的.o文件后link库生成dylib。这样就可以无入侵、无感知地完美解决symbol not found的问题。
Swift 函数热重载问题
最开始实现Swfit函数热重载的时候,并没有很透彻,导致出现各种热重载函数不生效的问题,这里看下影响Swift派发方式的因素,这里总结了下一些因素,不详细介绍,有兴趣的同学也可以了解相关的文章,如Swift 底层是怎么调度方法的
![]()
由上图可以看出,影响Swift派发方式有多种,且可以多个因素联合影响最终的派发方式,这里收敛后分析出最终派发的4种方式,如下:
![]()
如上图所示:Swift的派发发送根据不同的数据类型和调用方式影响后,最终收归到4种派发方式,分别为Static(静态派发)、V-Table(虚函数表)、Witness
Table以及Message(消息转发)。最终分别针对这4种派发方式进行热重载方案实现,如下图:
![]()
Static: 与C函数一致都是属于静态派发函数,所以和C函数的热重载实现一致,均是通过
Dobby进行inline hook来实现的热重载。V-Table 和 Witness Table: 根据struct TargetClassMetadata的数据结构和内存布局去获取对应的表指针进行整个表的替换,实现热重载能力。
Message: 与ObjC的消息转发机制一致,即可以读取类结构中的MethodList函数列表,并通过Runtime 进行函数实现指针的替换,达到热重载的效果。
实现效果
在iOS开发中,通过CocoaHotReload热重载能力,能够节省增量编译时的链接、安装、运行App的时间,大大提升研发效率。
Features
- 支持模拟器&真机(iOS 13+)
- 支持ObjC属性和方法多种操作(增加|删除|修改)
- 支持第三方库设置Symbol hidden by Default为YES
- 支持修改多种文件类型(.h|.m|.mm|.swift|.storyboard|.xib)
- 支持新增的文件进行hot reload
- 支持通过命令行工具生成dylib
- 支持Swift(仅支持函数替换)
- 支持Storyboard&xib
- 支持Unit Tests
- 支持C函数
效率提升效果
QQ场景
![]()
编译测试工具
![]()
总结与展望
本项目的初衷就是为了提升iOS开发的研发效率,以手Q开发为例,单文件从完成增量编译到应用完成安装、启动平均时间大约70s,使用CocoaHotReload,无需启动应用,改动的代码完成带入注入后直接生效,改动生效的平均时间降低到6s左右。在公司内部的大型项目QQ、微信等都接入使用,并整体反馈也是不错的。
我们团队也在不断地探索更高效的研发方式,后续也在尝试实现远程热重载能力(使用远程构建机来完成动态库的生成,本机无需编译,仅拉取代码和安装包即可调试)。
如果你对这方面的技术也比较感兴趣,可以在文末留言,跟我们一起讨论。也欢迎大家给一些建议,非常感谢。
SwiftUI 图文混排深度剖析:超越 AttributedString —— MarkdownView 与 RichText 的实现
作为 MarkdownView 和 RichText 的作者,LiYanan 不仅解决了 SwiftUI 在 Markdown 渲染与图文混排上的诸多痛点,其方案更被 X (Grok) 等重量级产品采用。在本文中,我邀请他毫无保留地分享了这一路的技术演进——从最初基于 Layout 协议的尝试,到踩坑 TextRenderer,最终通过底层 TextKit 实现完美的交互体验。这是一份关于坚持、成长与硬核技术的深度复盘,也是当前互联网上关于 SwiftUI 文本渲染领域不可多得的稀缺资料。
碰到一个不听劝的老板,喜提4.3a!
背景
之前公司的老板,突然找到我想把之前的老包迭代一手。
说实话看在之前老板劝退给赔偿的份上,迭代一下赚点维护费也不是不行。但是迭代之前,特别强调了本身是兄弟来砍我类型的马甲包(没错,就是那个渣渣辉代言的游戏),而且还是AB的骚操作。
如果不是必须要迭代,建议不动最安全。毕竟AB马甲包迭代 = 渡劫。
再三确认,要迭代就谈好了费用直接开动。
内购端倪
之前也知道,老板对上架这块规则不懂。但是没有想到开局就暴击!
说实话内购定价能到2w和5w,我是万万没想到的。激进派看了都得说激进!
![]()
本着对迭代负责的态度,建议了定价不要这么离谱!毕竟我这边主动拿出来阴阳师和王者荣耀,晓之以理,动之以情。摆事实,讲道理!
![]()
讲了半天,没办法。还是觉得定价低了。最低也得来个5000元的充值档次。
拿人钱财替人消灾,既然老板愿意承担风险。倒也无所谓了。
![]()
首轮被拒
不出意外的话,肯定是要出意外了! 苹果直接打回了4999元,定价的合理性。
于是又开始了新一轮的劝解,没办法。经过协商同意将4999元档次删除。其他的充值档次保留。
![]()
第二轮被拒
其实提交的时候,已经预想到这种结局了。果然又是被拒了。苹果质疑1999元充值档次的合理性。
本来以为这时候的老板能够及时醒悟,奈何永远叫不醒一个装睡的人! 宁可撞南墙也不知悔改,`主打一个
不撞南墙不回头,不见棺材不掉泪!` 最后,劝不动。按照老板的意思,回复尝试。
![]()
![]()
4.3a 判定
好消息:金额苹果觉得没有问题了。
坏消息:没得玩,4.3a了。
其实遭遇4.3a,已经是意料之中的事情了。 这套马甲包代码,分别上架了海外和国内。(因为都是我上的,所以我门清。)
![]()
![]()
总结
所以,专业的人做专业的事儿。 外行的人,要多听劝。
之所以金额是1999元和4999元,是因为苹果充值档次没有2000元和5000元的档次。
4.3a 垃圾应用 和 2.3.1 隐藏功能,都是因为不懂规则的人过度自以为是。没有吃过合规化上架的苦,把偶尔一两次的侥幸,当作区区AppStore不过如此的谈资。
截至发稿,已将产品通过审核。重新规划功能模块,顺利解决。
遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!
相关推荐
iOS MMKV原理整理总结:比UserDefaults快100倍的存储方案是如何炼成的?
Swift ——详解Any、AnyObject、 AnyClass
Swift 中的async和await
《Flutter全栈开发实战指南:从零到高级》- 20 -主题与国际化
Textture 生命周期
当 Android 手机『强行兼容』AirDrop -- 肘子的 Swift 周报 #113
我理解的保险产品
首先申明: 本文不是广告,也不推荐任何保险产品 。
我之前一直不理解保险,最近借助一些资料,终于想明白了各种保险的价值,给大家分享一下。
保险其实分很多种,我们需要分开理解它的用途。
一、意外险
意外险是杠杆最高的保险。每年大概几百块钱,就可以保上百万的保额。因为对于大部分人来说,这个事情发生的概率极低,所以它的杠杆很高。
意外险的价值是给家庭或者父母留下一笔财富。特别适合家里面负责挣钱的那个顶梁柱买,这样可以应对极端概率情况下的风险。
很多人会想:这么低的概率,有必要买吗?有可能一辈子都遇不到意外。
我们在考虑这种保险的时候,要有 “平行宇宙”思维。即:我们要假设这个世界是量子态的,同时有许多平行宇宙,意外险是为众多平行宇宙中的某一个 “我” 的意外买单。这样,那一个平行宇宙里面的倒霉的 “我”,被另外平行宇宙中的 “我” 的保费接济,获得了极大的补偿。
我们不知道我们身处在哪个平行宇宙。所以意外险保证了我们在每个平行宇宙过得都不算太差,最倒霉的那个 “我”,也用保险给家庭留了一大笔钱。
二、医疗险
医疗险大多数报销门诊或者住院时候的大额费用。一般这种保险都有起付金额(比如超过 1 万部分)。
这种医疗险的费用也很低,一年也是几百块钱就可以买到。这种保险其实也是杠杆率很高的保险,因为大部分年轻人不太会超过起付金额。
医疗险和意外险类似,也是保障极端情况,比如如果一个突发疾病住院要花 10 来万,这个保险就可以报销大部分,让家庭不至于因病返贫。
三、高端医疗险
高端医疗险一般一年费用得好几千,是普通医疗险的 10 倍。大概率高端医疗险是很难从期望上 “回本” 的,而且很多疑难杂症,可能公立的三甲医院医生更有临床经验(因为他们看的病例更多)。
购买高端医疗险更多可以看成是一种 “消费”。因为你得任何小病都可以享受非常好的看病体验,不用担心看个感冒花几千块钱(是的,和睦家看个感冒几千块钱很正常)。
四、分红险
分红险在我看来已经脱离了保险原本的意义,但是最近我稍微理解了一点它的价值。
分红险通常需要购买者每年交上万块钱,连续交 20 年左右,之后开始累积复利,最后在几十年后,可以提取出来一笔财富。在现在低利率时代,它能保证的年化收益大概有 2.5% 左右(以后如果利率下行应该收益会更低一点)。
我开始很不喜欢分红险,因为首先它的收益率并不高。不管是股票,债券,还是黄金,如果你拉一下 30 年收益率的话,大多数都远远超过 2.5% 。另外,这笔几十万的保费,其实是丧失了几十年的流动性,如果你要强行赎回,就会损失巨大。我认为现金流对家庭来说还是很重要的,所以我很不喜欢这类保险。
大部分销售推销的香港保险也属于这类。
哦,不得不提,这类保险也是对销售来说提成最高的产品。这也是我不喜欢它的原因。因为这就相当于你的本金一开始就打了一个 9 折,对于一个打折的本金,它的复利增长就更难。
那我现在为什么稍微理解了它呢?因为我发现大部分人只会把钱存定期。对于一个定期存款来说,换成这种保险,稍微能够提升一点点长期收益率,同时帮助这些人能够 “锁定” 财富,如果希望这个钱用于养老,它被锁定就不至于被各种意外用掉。
但是我个人还是宁愿持有股票或债券。
另外,给孩子买这个保险的家长可能要想清楚,这个保单什么时候兑现?如果一直不兑现,理论上可能是给 “孙子” 买的,那么做好保单两代人的传承也是一个问题。因为如果 10 岁给孩子买,那么要 60 年之后可能才会兑现保单价值。到时候大概率自己已经不在了,孩子已经 70 岁了,保单传承不好就相当于捐给保险公司了。
五、终身寿险
高额的终身寿险其实相对于把意外险和分红险做了一个组合。拿分红险的收益来 cover 意外险的保费。美其名曰:如果意外发生可以保多少,最后你还能拿回全部本金,还附加一些特别红利(不保证兑现)。
殊不知羊毛出在羊身上,本金每年的部分利息就其实是意外险的成本。只是换了一个说法和组合。
我是很不喜欢这种类似雪球的复合结构,因为你搞不明白年化收益率,也搞不明白你的意外险部分的杠杆率。
七、车险
车险里面的车损险是杠杆率极低的产品。拿我的特斯拉来说,一年保费要 5000 多,但是我大部分时候在城市里面开,就算有小磕碰,修车也不会花到这么多。
车险里面杠杆最高的是三者险,大概 600 块钱左右就可以保 200-300 万的保额。这样万一撞到人或者豪车,都可以 cover 全部费用。
我已经连续很多年只买交强险和三者险。这也让我驾车的时候更小心,自己不撞别人就不需要车损险,如果别人撞到自己,可以走别人的保险。
八、小结
- 意外险和医疗险可以保证极端情况发生后的体验,杠杆很高,费用相对低(一年几百块钱)
- 高端医疗险类似消费,提升普通看病体验,一年几千。
- 分红险年化收益不如很多股票和债券等产品,但是比定期强。另外牺牲了现金流,但同时保证这笔钱不会被挪用。因为利润高,销售都喜欢卖这个产品。
- 终身寿险是意外险和分红险的组合。
- 车险里面三者险杠杆最高,车损险性价比低。
以上。本文仅表达个人观点,不构成任何购买建议。
真相不重要
真相有时候不重要,举几个例子。
第一个例子是身边一个朋友的故事。一天早上,从来不做饭的妻子心血来潮,给他做了一份早餐,但是因为是第一次做,手艺不太娴熟。这个时候,妻子问他味道怎么样?他随口就说:感觉一般。妻子的脸色瞬间就阴下去了,说:那我以后再也不做饭了。对于妻子来说,早餐好不好吃的真相不重要,鼓励和认可才是重要的。丈夫说了实话,但是却伤了妻子的心。
去年农夫山泉被全民网暴的时候,我发文章说农夫山泉的瓶盖和日本国旗没关系。结果一堆人在评论区谩骂我。对于网暴的人来说,真相不重要,情绪和宣泄的重要性大于真相。对于这些谩骂的人来说,我忽略了讲真话的时机,所以被骂。
在今年的脱口秀节目上,一个脱口秀演员提到自己的爸妈本来打算把自己打掉的,老罗也提到他也有同样的家庭情况,他花了很多年很多时间才对这个事情“放下”。对于老罗来说,“自己出身下来不是被需要的”这个真相不重要,重要的是自己的人生意义。即便是事实,如果会伤害孩子,父母本来可以不说,真相不重要。
我们曾经有一个实习生,偷偷利用公司的9点后加班可以打车福利,下班后去健身房,然后等到9点后再打车回家。我们后来给他说,我们实习岗位取消了,让他离开了公司。对于我们来说,告诉他离开的真相不重要,对于不合适的人,让他快速地离开不会起任何冲突。对于我们来说,事情顺利地执行比说出真相重要。
我是一个业余编程老师,编程这件事情很难,所以孩子第一次接触编程容易会发怵。这个时候,我会设置一些很简单的题目,但是告诉他这个题目很难,然后等他做出来,我会惊讶地说:哇,你真的很有天赋!对于孩子来说,真相不重要,学习的兴趣和信心最重要。
对于美国两党各自拥护的媒体,真相也不重要。如果发现事情有利于他们的政治宣传,他们就大力宣传。如果发现事情不利于他们的政党,他们就会有意淡化。在美国,媒体的中立客观在政治面前不重要,政治更重要。
邓小平很早就明白真相不重要。每次被打倒他都默默承受,尽力照顾好自己的家人,不争辩不气馁。因为他知道,在那个时候只要是他说的,就都是错的,真相不重要。在后来,邓小平坚决保护毛主席,保护国家稳定,因为他知道:稳定大于一切。只要政局稳定,一切问题都可以慢慢解决。而如果为了真相导致政局混乱,那将天下大乱。
有些时候,真相因为不太容易被人接受,甚至会变成秘密,比如大家的工资。试想一下,如果每个人的工资是公开的,那么就会有大量的人向 HR 投诉自己的薪资为什么不如某某某。因为人们总是容易高看自己,低看别人。所以,薪资的真相变得不重要,大家相互之间不知道薪资变得很重要。
那真相很多时候不重要,我们是不是就不需要真相了?不是的。大部分时候,真相都是重要的。大部分时候,我们也需要讲真话,追求真相。只是我们需要明白,这个世界在运转过程中,真相的权重不一定是最大的。当真相被掩盖的时候,我们可能需要接受这样的现实。当我们决策的时候,有时候需要相对真相,给其他因素更高的权重。
最后,我想引用最近看到的一篇《南方周末》的报道 佛学与官场,一个主持的官场往事。在文章中,释传真(时任南京市佛教协会副会长)说:
我常常跟来聊天的官员说,做官啊,第一有文化没文化要学会听话;
第二,得过且过太阳出来暖和;
第三,有一些矛盾就要睁一只眼闭一只眼。
你看,一位佛教高僧给官员的经验分享,每一点都在说:有比真相更重要的事情。
以上。最后再强调一遍:我不是教大家骗人,我是在强调给真相赋予合适的决策权重。
当 Android 手机『强行兼容』AirDrop - 肘子的 Swift 周报 #113
AirDrop 让使用者可以在各种不同类似的苹果设备上高效、无损的传输数据,它一直是苹果生态的专属且核心功能。但,这种情况现在出现了“奇怪”的变化。几天前,谷歌宣布在 Pixel 10 中,在没有苹果的参与下,为 Quick Share 提供了 AirDrop 的兼容机制,实现了安卓手机与苹果手机基于 AirDrop 的无线互通。