min函数为什么要加if&=min{......}

为什么 Math.min() 比 Math.max() 大?
作者:51cto
考虑如下代码:
var&min&=&Math.min();&&var&max&=&Math.max();&&console.log(min&&&max);&
按照常规思路,这段代码应该输出 true,毕竟最小值应该小于最大值。但是当我们运行这段代码时,却神奇的输出了 false。
为什么会这样呢?
还得去查查 MDN 的相关文档。
The Math.min() function returns the smallest of zero or more numbers.
Math.min 的参数是 0 个或者多个。如果是多个参数很容易理解,返回参数中最小的。
如果是 0 个参数呢?文档中写到:
If no arguments are given, the result is Infinity.
If at least one of arguments cannot be converted to a number, theresult is
如果没有参数,则返回 Infinity。Infinity 是什么呢?Infinity 是 javascript
中全局对象的一个属性,在浏览器环境中就是window 对象的一个属性,表示无穷大。
而 Math.max() 没有传递参数时返回的是 -Infinity。因此 Math.min() 要比 Math.max() 大。
为什么很多人会有这种疑惑,是因为犯了想当然的错误&&望文生义。Math.min() 作为一个函数,返回的并不是最小值;同理 Math.max()
返回的也不是最大值。他们返回的是参数的最大值和最小值,而我们却没有传递任何参数。
javascript 中数值的最大值和最小值常量分别是 Number.MAX_VALUE 和 Number.MIN_VALUE,在我电脑 V8
的运行结果是 1. 和 5e-324。
这里也有一个坑,那就是 MIN_VALUE 并不是最小的数,而是绝对值最小的数,它是一个正数。而 Math.min() 的返回值比
Number.MAX_VALUE 还要大。
Math.min() 返回了 Infinity,这个值对应了 Number.POSITIVE_INFINITY 常量。Math.max() 返回了
-Infinity,这个值对应了 Number.NEGATIVE_INFINITY 常量。
就像数学里面那样,正无穷和负无穷并不是确切的数,只是集合里面的概念。我们可以使用 0 减去正无穷来得到负无穷:0 - Infinity =
-Infinity,或者通过 0 减去负无穷来得到正无穷:0 - (-Infinity) = Infinity。但是我们把正无穷和负无穷相加却得不到
0,事实上我们得到的是 NaN。
当我们真正明白了 Math.min 和 Math.max 只是返回了所有参数的最大值,就很容易理解这个看似匪夷所思的结果了。
本来想查看 V8 的源码,但是搜索了一圈,也只找到了几个测试用例(文件: test/mjsunit/math-min-max.js):
assertEquals(Infinity,&Math.min());&&assertEquals(1,&Math.min(1));&&assertEquals(1,&Math.min(1,&2));&&assertEquals(1,&Math.min(2,&1));&&assertEquals(1,&Math.min(1,&2,&3));&&assertEquals(1,&Math.min(3,&2,&1));&&assertEquals(1,&Math.min(2,&3,&1));&
从此测试用例中可以看出,Math.min() 确实返回了 Infinity。继续阅读此测试用例还会发现更有意思的事:
assertEquals(-Infinity,&Infinity&/&Math.min(-0,&+0));&&assertEquals(-Infinity,&Infinity&/&Math.min(+0,&-0));&&assertEquals(-Infinity,&Infinity&/&Math.min(+0,&-0,&1));&
对比下面的结果:
assertEquals(Infinity,&Infinity&/&Math.max(-0,&+0));&&assertEquals(Infinity,&Infinity&/&Math.max(+0,&-0));&&assertEquals(Infinity,&Infinity&/&Math.max(+0,&-0,&-1));&
也就是说在 Math.min 和 Math.max 进行数值比较(javascript不分整型和浮点型)时,负零是小于正零的。
-0、+0、-Infinity、+Infinity、NaN 这几个概念就可以讲上一天一夜了,篇幅有限就不展开讲了,以后有时间再填坑。
言归正传,现在我们换个思路,试着从算法的角度去考虑最大值和最小值的问题。
我们如何求 N 个参数的最大值呢?为了描述简单我们使用数组(感谢 @f2er前端百科)
var&max&=&___;&arr.forEach(function(n)&{&&&&&if(n&&&max)&{&&&&&&&&&max&=&n;&&&&&}&});&
如果是填空题,你会在空格处填什么呢?如此想来,这个返回值就很合理了。
--------------------------------
&del&本来以为是无参调用时返回了运算的幺元,后来细琢磨,好像没有什么关系,2333
对于运算集合 S 上的二元运算*,如果满足 a*x = x*a = x,则 a 是*运算的幺元。加法运算的幺元是 0,因为 0+x = x+0 =
x。乘法运算的幺元是 1,因为 1*x = x*1 = x。但是我们定义函数 add() 或者 mult()
函数,如果不传递参数时,返回幺元的话也是不合理的。&/del&
官方微博/微信
每日头条、业界资讯、热点资讯、八卦爆料,全天跟踪微博播报。各种爆料、内幕、花边、资讯一网打尽。百万互联网粉丝互动参与,TechWeb官方微博期待您的关注。
↑扫描二维码
想在手机上看科技资讯和科技八卦吗?想第一时间看独家爆料和深度报道吗?请关注TechWeb官方微信公众帐号:1.用手机扫左侧二维码;2.在添加朋友里,搜索关注TechWeb。
对于想换机的用户来说,本周五的确是个难忘的日子,因为iPhone X将开启预售,鉴于这款...
一加年底前有望推出的一加5T新机的配置近日被GizChina.it曝光。...
谷歌和LG相继放弃了模块化手机的研发,不过摩托罗拉依然还在坚持。就在今天,它们正式...
据SK海力士(SK Hynix)今日发布的一份声明显示,收购东芝芯片业务部门后,贝恩资本(Bai...
监管文件显示,科斯罗萨西周四通知纽约时报公司董事会,他将因为出任Uber的新职位而卸...
一名美国法官已责令进行一项新的审判,目的是判定三星应因其抄袭苹果公司iPhone外观设...
WPS Office是金山公司开发的一款国产办公软件,虽然历史甚至比Office更久远,但不可否...
尽管微软为了Windows 10 Fall Creators Update(秋季创意者更新)做了很多普遍准备工作...
随着 iOS 11 的到来,我们也看到了一个经过重新设计的 App Store 。在 App Store 中,...
张良的技能营造出的敌我交互过程有些过于粗暴了。承担主要输出功能的2技能,,......
皇家狮鹫觉醒季,炸裂福利强势登场!经典坐骑皇家狮鹫专属坐骑配饰--艳阳羽冠震撼登场......
炉石传说2017万圣节活动上线,10月24日11点到11月5日22点期间,玩家不但可以领取免费......
《绝地求生大逃杀》中的四排是需要有一些战术策略的,这样团队合作才能发挥出较好的效......
Copyright (C)
All rights reserved.
请选择一张图片分享
要转发到新浪微博,请
要转发到QQ空间,请河南文娱-名人·名企&正文
MissA Min恋情得到证实 中国成员孟佳退队的原因是什么
扫描到手机,看更多国搜资讯
您可以用手机或平板电脑的二维码应用拍下左侧二维码,您可以在手机国搜客户端继续浏览本文,并可以分享给你的好友。
核心提示:7日,据歌坛相关人员向媒体表示“Min与G.Soul正在恋爱中,原本就很亲近的两人已经发展成恋人的事情是歌坛公开的事实”。随后,JYP娱乐证实了恋爱传闻。Miss A中国成员孟佳退队的原因是什么?
MissA Min恋情得到证实 中国成员孟佳退队的原因是什么
7日,据歌坛相关人员向媒体表示“Min与G.Soul正在恋爱中,原本就很亲近的两人已经发展成恋人的事情是歌坛公开的事实”。随后,JYP娱乐对此表示“Min与G.Soul很久之前在往返于美国和韩国的时候便互相支持打气,关系就走得很近,近日才知道了彼此的心意。请大家多多支持”,证实了恋爱传闻。
另外,Min在2010年4月作为组合miss A成员出道,不仅展开了音乐活动,还出演了《倒计时》等影视作品。G.Soul于2001年被朴振荣发掘后便在JYP娱乐做了15年的练习生,在2015年1月才得以出道,不仅一直推出音乐作品,还出演了音乐剧。
延伸阅读:
MISS A中国成员孟佳退队原因引猜测
missA在2015年推出《Only You》,口碑、销量都不错,团体声势相当旺,然而因为团员秀智太红了,团体活动的行程安排经常得配合秀智的档期,其他3名成员的发展形同被限制了,因此外界认为这可能是Jia在约满后选择不与公司「JYP娱乐」续约的原因之一。
此外,过去MissA屡传不合的传言也在这次Jia退团后再次浮出水面,不少网友指证历历认为公司将资源大量集中在秀智身上,行程满档的她变得和其他3名成员越来越不熟,甚至连她和李敏镐交往的事情,其他团员也一无所知,她被排挤的说法在2015年时曾闹得沸沸扬扬,不过这些传言未经任何人证实,粉丝则希望未来成员们能各自加油。(&国搜河南综合新浪娱乐、粉丝网报道&)
相关搜索:
责任编辑:白阳
点击加载更多
河南热搜词
黄金周哪些景点最受欢迎,会不会被挤成饼干?
扫码关注中国搜索官方微信
扫码关注中国搜索官方微信习题1_百度文库
两大类热门资源免费畅读
续费一年阅读会员,立省24元!
&&C语言的习题1
阅读已结束,下载文档到电脑
想免费下载更多文档?
定制HR最喜欢的简历
下载文档到电脑,方便使用
还剩32页未读,继续阅读
定制HR最喜欢的简历
你可能喜欢招人手淘架构组招人 iOS/Android 皆可,地点杭州,有兴趣的请联系我!!
iOS内存abort(Jetsam) 原理探究苹果最近开源了iOS系统上的XNU内核代码,加上最近又开始负责手淘/猫客的稳定性及性能相关的工作,所以赶紧拜读下苹果的大作。今天主要开始想分析跟abort相关的内存Jetsam原理。
什么是Jetsam关于Jetsam,可能有些人还不是很理解。我们可以从手机设置-&隐私-&分析这条路径看看系统的日志,会发现手机上有许多JetsamEvent开头的日志。打开这些日志,一般会显示一些内存大小,CPU时间什么的数据。
之所以会发生这么JetsamEvent,主要还是由于iOS设备不存在交换区导致的内存受限,所以iOS内核不得不把一些优先级不高或者占用内存过大的杀掉。这些JetsamEvent就是系统在杀掉App后记录的一些数据信息。
从某种程度来说,JetsamEvent是一种另类的Crash事件,但是在常规的Crash捕获工具中,由于iOS上能捕获的信号量的限制,所以因为内存导致App被杀掉是无法被捕获的。为此,许多业界的前辈通过设计flag的方式自己记录所谓的abort事件来采集数据。但是这种采集的abort,一般情况下都只能简单的记录次数,而没有详细的堆栈。
源码探究MacOS/iOS是一个从BSD衍生而来的系统。其内核是Mach,但是对于上层暴露的接口一般都是基于BSD层对于Mach包装后的。虽然说Mach是个微内核的架构,真正的虚拟内存管理是在其中进行,但是BSD对于内存管理提供了相对较为上层的接口,同时,各种常见的JetSam事件也是由BSD产生,所以,我们从bsd_init这个函数作为入口,来探究下原理。
bsd_init中基本都是在初始化各个子系统,比如虚拟内存管理等等。
跟内存相关的包括如下几步可能:
1. 初始化BSD内存Zone,这个Zone是基于Mach内核的zone构建
kmeminit();
2. iOS上独有的特性,内存和进程的休眠的常驻监控线程
#if CONFIG_FREEZE
#ifndef CONFIG_MEMORYSTATUS
#error "CONFIG_FREEZE defined without matching CONFIG_MEMORYSTATUS"
bsd_init_kprintf("calling memorystatus_freeze_init\n");
memorystatus_freeze_init();
3. iOS独有,JetSAM(即低内存事件的常驻监控线程)
#if CONFIG_MEMORYSTATUS
bsd_init_kprintf("calling memorystatus_init\n");
memorystatus_init();
这两步代码都是调用kern_memorystatus.c里面暴露的接口,主要的作用就是从内核中开启了两个最高优先级的线程,来监控整个系统的内存情况。
首先先来看看CONFIG_FREEZE涉及的功能。当启用这个效果的时候,内核会对进程进行冷冻而不是Kill。
这个冷冻的功能是通过在内核中启动一个memorystatus_freeze_thread进行。这个线程在收到信号后调用memorystatus_freeze_top_process进行冷冻。
当然,涉及到进程休眠相关的代码,就需要谈谈苹果系统里面其他相关概念了。扯开又是一个比较大的话题,后续单独开文章来进行阐述。
回到iOS Abort问题上的话,我们只需要关注memorystatus_init即可,去除平台无关的代码后如下:
__private_extern__ void
memorystatus_init(void)
thread_t thread = THREAD_NULL;
kern_return_
for (i = 0; i & MEMSTAT_BUCKET_COUNT; i++) {
TAILQ_INIT(&memstat_bucket[i].list);
memstat_bucket[i].count = 0;
memorystatus_idle_demotion_call = thread_call_allocate((thread_call_func_t)memorystatus_perform_idle_demotion, NULL);
#if CONFIG_JETSAM
nanoseconds_to_absolutetime((uint64_t)DEFERRED_IDLE_EXIT_TIME_SECS * NSEC_PER_SEC, &memorystatus_sysprocs_idle_delay_time);
nanoseconds_to_absolutetime((uint64_t)DEFERRED_IDLE_EXIT_TIME_SECS * NSEC_PER_SEC, &memorystatus_apps_idle_delay_time);
PE_get_default("kern.jetsam_delta", &delta_percentage, sizeof(delta_percentage));
if (delta_percentage == 0) {
delta_percentage = 5;
assert(delta_percentage & 100);
PE_get_default("kern.jetsam_critical_threshold", &critical_threshold_percentage, sizeof(critical_threshold_percentage));
assert(critical_threshold_percentage & 100);
PE_get_default("kern.jetsam_idle_offset", &idle_offset_percentage, sizeof(idle_offset_percentage));
assert(idle_offset_percentage & 100);
PE_get_default("kern.jetsam_pressure_threshold", &pressure_threshold_percentage, sizeof(pressure_threshold_percentage));
assert(pressure_threshold_percentage & 100);
PE_get_default("kern.jetsam_freeze_threshold", &freeze_threshold_percentage, sizeof(freeze_threshold_percentage));
assert(freeze_threshold_percentage & 100);
if (!PE_parse_boot_argn("jetsam_aging_policy", &jetsam_aging_policy,
sizeof (jetsam_aging_policy))) {
if (!PE_get_default("kern.jetsam_aging_policy", &jetsam_aging_policy,
sizeof(jetsam_aging_policy))) {
jetsam_aging_policy = kJetsamAgingPolicyL
if (jetsam_aging_policy & kJetsamAgingPolicyMax) {
jetsam_aging_policy = kJetsamAgingPolicyL
switch (jetsam_aging_policy) {
case kJetsamAgingPolicyNone:
system_procs_aging_band = JETSAM_PRIORITY_IDLE;
applications_aging_band = JETSAM_PRIORITY_IDLE;
case kJetsamAgingPolicyLegacy:
system_procs_aging_band = JETSAM_PRIORITY_AGING_BAND1;
applications_aging_band = JETSAM_PRIORITY_IDLE;
case kJetsamAgingPolicySysProcsReclaimedFirst:
system_procs_aging_band = JETSAM_PRIORITY_AGING_BAND1;
applications_aging_band = JETSAM_PRIORITY_AGING_BAND2;
case kJetsamAgingPolicyAppsReclaimedFirst:
system_procs_aging_band = JETSAM_PRIORITY_AGING_BAND2;
applications_aging_band = JETSAM_PRIORITY_AGING_BAND1;
assert(JETSAM_PRIORITY_ELEVATED_INACTIVE & system_procs_aging_band);
assert(JETSAM_PRIORITY_ELEVATED_INACTIVE & applications_aging_band);
if (!PE_parse_boot_argn("jetsam_idle_snapshot", &memorystatus_idle_snapshot, sizeof (memorystatus_idle_snapshot))) {
PE_get_default("kern.jetsam_idle_snapshot", &memorystatus_idle_snapshot, sizeof(memorystatus_idle_snapshot));
memorystatus_delta = delta_percentage * atop_64(max_mem) / 100;
memorystatus_available_pages_critical_idle_offset = idle_offset_percentage * atop_64(max_mem) / 100;
memorystatus_available_pages_critical_base = (critical_threshold_percentage / delta_percentage) * memorystatus_
memorystatus_policy_more_free_offset_pages = (policy_more_free_offset_percentage / delta_percentage) * memorystatus_
if (max_mem &= (512 * 1024 * 1024)) {
memorystatus_jld_eval_period_msecs = 8000;
memorystatus_jld_eval_period_msecs = 6000;
memorystatus_jld_enabled = TRUE;
memorystatus_update_levels_locked(FALSE);
#endif /* CONFIG_JETSAM */
memorystatus_jetsam_snapshot_max =
memorystatus_jetsam_snapshot =
(memorystatus_jetsam_snapshot_t*)kalloc(sizeof(memorystatus_jetsam_snapshot_t) +
sizeof(memorystatus_jetsam_snapshot_entry_t) * memorystatus_jetsam_snapshot_max);
if (!memorystatus_jetsam_snapshot) {
panic("Could not allocate memorystatus_jetsam_snapshot");
nanoseconds_to_absolutetime((uint64_t)JETSAM_SNAPSHOT_TIMEOUT_SECS * NSEC_PER_SEC, &memorystatus_jetsam_snapshot_timeout);
memset(&memorystatus_at_boot_snapshot, 0, sizeof(memorystatus_jetsam_snapshot_t));
result = kernel_thread_start_priority(memorystatus_thread, NULL, 95 , &thread);
if (result == KERN_SUCCESS) {
thread_deallocate(thread);
panic("Could not create memorystatus_thread");
下面先介绍几个知识点
内核里面对于所有的进程都有一个优先级的分布,通过一个数组维护,数组每一项是一个进程的list。这个数组的大小是JETSAM_PRIORITY_MAX + 1。其结构体定义如下:
typedef struct memstat_bucket {
TAILQ_HEAD(, proc)
} memstat_bucket_t;
这结构体非常通俗易懂。
线程在Mach下采用了不同的优先级,其中MAXPRI_KERNEL代表的是分配给内核可用范围内最高优先级的线程。其他级别还有如下这些:
* // 优先级最高的实时线程 (不太清楚谁用)
Reserved (real-time)
(32 levels)
Reserved (real-time)
* // 给内核用的线程优先级(MAXPRI_KERNEL)
Kernel mode only
(16 levels)
Kernel mode only
* // 给操作系统分配的线程优先级
System high priority
(16 levels)
System high priority
* // 剩下的全是用户态的普通程序可以用的
Elevated priorities
(12 levels)
Elevated priorities
Elevated priorities (incl. BSD +nice)
(20 levels)
Elevated priorities (incl. BSD +nice)
Default (default base for threads)
Lowered priorities (incl. BSD -nice)
(20 levels)
Lowered priorities (incl. BSD -nice)
Lowered priorities (aged pri's)
(11 levels)
Lowered priorities (aged pri's / idle)
*************************************************************************
从上图不难看出,用户态的应用程序的线程不可能高于操作系统和内核。而且,在用户态的应用程序间的线程优先级分配也有区别,前台活动的应用程序优先级高于后台的应用程序。iOS上大名鼎鼎的SpringBoard是应用程序中优先级最高的程序。
当然线程的优先级也不是一成不变。Mach会针对每一个线程的利用率和整体系统负载动态调整优先级。如果耗费CPU太多就降低优先级,如果一个线程过度挨饿CPU则会提升其优先级。但是无论怎么变,程序都不能超过其所在的线程优先级区间范围。
好,预备知识说完,那苹果究竟是怎么处理JetSam事件呢?
result = kernel_thread_start_priority(memorystatus_thread, NULL, 95 /* MAXPRI_KERNEL */, &thread);
苹果其实处理的思路非常简单。如上述代码,BSD层起了一个内核优先级最高的线程VM_memorystatus,这个线程会在维护两个列表,一个是我们之前提到的基于进程优先级的进程列表,还有一个是所谓的内存快照列表,即保存了每个进程消耗的内存页memorystatus_jetsam_snapshot。
这个常驻线程接受从内核对于内存的守护程序pageout通过内核调用给每个App进程发送的内存压力通知,来处理事件,这个事件转发成上层的UI事件就是平常我们会收到的全局内存警告或者每个ViewController里面的didReceiveMemoryWarning。
当然,我们自己开发的App是不会主动注册监听这个内存警告事件的,帮助我们在底层完成这一切的都是libdispatch,如果你感兴趣的话,可以钻研下_dispatch_source_type_memorypressure和__dispatch_source_type_memorystatus。
那么在哪些情况下会出现内存压力呢?我们来看一看memorystatus_action_needed这段函数:
static boolean_t
memorystatus_action_needed(void)
#if CONFIG_EMBEDDED
return (is_reason_thrashing(kill_under_pressure_cause) ||
is_reason_zone_map_exhaustion(kill_under_pressure_cause) ||
memorystatus_available_pages &= memorystatus_available_pages_pressure);
return (is_reason_thrashing(kill_under_pressure_cause) ||
is_reason_zone_map_exhaustion(kill_under_pressure_cause));
概括来说:
频繁的的页面换进换出is_reason_thrashing,Mach Zone耗尽了is_reason_zone_map_exhaustion(这个涉及Mach内核的虚拟内存管理了,单独写)以及可用的页低于一个门槛了memorystatus_available_pages。
在这几种情况下,就会准备去Kill 进程了。但是,在这个处理下面,有一段代码特别有意思,我们看看这个函数memorystatus_act_aggressive:
if ( (jld_bucket_count == 0) ||
(jld_now_msecs & (jld_timestamp_msecs + memorystatus_jld_eval_period_msecs))) {
jld_timestamp_msecs
= jld_now_
jld_idle_kill_candidates = jld_bucket_
*jld_idle_kills
jld_eval_aggressive_count = 0;
jld_priority_band_max
= JETSAM_PRIORITY_UI_SUPPORT;
这段代码很明显,是基于某个时间间隔在做条件判断。如果不满足这个判断,后续真正执行的Kill也不会走到。那我们来看看memorystatus_jld_eval_period_msecs这个变量:
if (max_mem &= (512 * 1024 * 1024)) {
memorystatus_jld_eval_period_msecs = 8000;
memorystatus_jld_eval_period_msecs = 6000;
这个时间窗口是根据设备的物理内存上限来设定的,但是无论如何,看起来至少有个6秒的时间可以给我们来做点事情。
当然,如果满足了时间窗口的需求,就会根据我们提到的优先级进程列表进行寻找可杀目标:
proc_list_lock()
switch (jetsam_aging_policy) {
case kJetsamAgingPolicyLegacy:
bucket = &memstat_bucket[JETSAM_PRIORITY_IDLE]
jld_bucket_count = bucket-&count
bucket = &memstat_bucket[JETSAM_PRIORITY_AGING_BAND1]
jld_bucket_count += bucket-&count
case kJetsamAgingPolicySysProcsReclaimedFirst:
case kJetsamAgingPolicyAppsReclaimedFirst:
bucket = &memstat_bucket[JETSAM_PRIORITY_IDLE]
jld_bucket_count = bucket-&count
bucket = &memstat_bucket[system_procs_aging_band]
jld_bucket_count += bucket-&count
bucket = &memstat_bucket[applications_aging_band]
jld_bucket_count += bucket-&count
case kJetsamAgingPolicyNone:
bucket = &memstat_bucket[JETSAM_PRIORITY_IDLE]
jld_bucket_count = bucket-&count
bucket = &memstat_bucket[JETSAM_PRIORITY_ELEVATED_INACTIVE]
elevated_bucket_count = bucket-&count
需要注意的是,JETSAM不一定只杀一个进程,他可能会大杀特杀,杀掉N多进程。
if (memorystatus_avail_pages_below_pressure()) {
return TRUE;
至于杀进程的话,最终都会落到函数memorystatus_do_kill-&jetsam_do_kill去执行。
其他看苹果代码的时候,发现了不少内核的参数,一一进行了尝试后,发现sysctlname和sysctl的系统调用都被苹果禁用了,比如这些:
"kern.jetsam_delta"
"kern.jetsam_critical_threshold"
"kern.jetsam_idle_offset"
"kern.jetsam_pressure_threshold"
"kern.jetsam_freeze_threshold"
"kern.jetsam_aging_policy"
不过,我试了下通过kern.boottime获取机器的开机时间还是可以的,代码示例如下:
sysctlbyname("kern.boottime", NULL, &size, NULL, 0);
char *boot_time = malloc(size);
sysctlbyname("kern.boottime", boot_time, &size, NULL, 0);
uint32_t timestamp = 0;
memcpy(&timestamp, boot_time, sizeof(uint32_t));
free(boot_time);
NSDate* bootTime = [NSDate dateWithTimeIntervalSince1970:timestamp];
最后嘻嘻,技术原理研究了一些,心里顿时对解决公司的Abort问题有了一定的眉目。嘿嘿,我写了个DEMO验证了我的思路,是可行的。哇咔咔。等我的好消息吧~
Please enable JavaScript to view the
最近随着iOS11的正式发布,手淘/天猫也开始逐步用Xcode 9开始编译。在调试过程中,很多同事发现经常报许多API会报线程使用错误的问题。摸索了下,发现是Xcode 9里面带上了一个叫libMainThreadChecker.dylib的动态库,在运行时提供了主线程检查的功能,今天就从探究苹果的实现开始讲起。
0x1 苹果的实现把苹果的动态库拖入hopper里面看看,基本上扫一眼以后,比较可疑的是__library_initializer和__library_deintializer。
我看反汇编,第一直觉就是猜,然后都试一把。
我们来看看其伪代码实现,可以分为几个部分来探究:
1.1 环境变量
从图中不难看出,libMainThreadChecker的运行依赖于许多的环境变量,我们可以在Xcode-&Scheme-&Arguments里面一个个输入这些变量进行测试,我发现比较重要的是MTC_VERBOSE这个参数,使用后,可以输出究竟对于哪些类进行了线程监控。
Swizzling class: UIKeyboardEmojiCollectionViewCell
Swizzling class: UIKeyboardEmojiSectionHeader
Swizzling class: UIPrinterSetupPINScrollView
Swizzling class: UIPrinterSetupPINView
Swizzling class: UIPrinterSetupConnectingView
Swizzling class: UICollectionViewTableHeaderFooterView
Swizzling class: UIPrinterSetupDisplayPINView
Swizzling class: UIStatusBarMapsCompassItemView
Swizzling class: UIStatusBarCarPlayTimeItemView
Swizzling class: UIKeyboardCandidateBarCell
Swizzling class: UIKeyboardCandidateBarCell_SecondaryCandidate
Swizzling class: UIActionSheetiOSDismissActionView
Swizzling class: UIKeyboardCandidateFloatingArrowView
Swizzling class: UIKeyboardCandidateGridOverlayBackgroundView
Swizzling class: UIKeyboardCandidateGridHeaderContainerView
Swizzling class: UIStatusBarBreadcrumbItemView
Swizzling class: UIInterfaceActionGroupView
Swizzling class: UIKeyboardFlipTransitionView
Swizzling class: UIKeyboardAssistantBar
Swizzling class: UITextMagnifier
Swizzling class: UIKeyboardSliceTransitionView
Swizzling class: UIWKSelectionView
Swizzled 10717 methods in 384 classes.
可以看出,苹果会在启动前对于这些类进行所谓的线程监控。
1.2 逻辑看完了输出,我们来看看其中的逻辑实现,如下所示:
CFAbsoluteTimeGetCurrent();
var_270 = intrinsic_movsd(var_270, xmm0);
*_indirect__main_thread_checker_on_report = dlsym(0xfffffffffffffffd, "__main_thread_checker_on_report");
if (objc_getClass("UIView") != 0x0) {
*_XXKitImage = dyld_image_header_containing_address(objc_getClass("UIView"));
*_CoreFoundationImage = dyld_image_header_containing_address(_CFArrayGetCount);
rax = objc_getClass("WKWebView");
rax = dyld_image_header_containing_address(rax);
*_WebKitImage =
*_InlineCallsMachHeaders = *_XXKitI
*0x1ec3e8 = *_CoreFoundationI
*0x1ec3f0 =
*___CATransaction = objc_getClass("CATransaction");
*___NSGraphicsContext = objc_getClass("NSGraphicsContext");
*_SEL_currentState = sel_registerName("currentState");
*_SEL_currentContext = sel_registerName("currentContext");
*_MyOwnMachHeader = dyld_image_header_containing_address(___library_initializer);
*_classesToSwizzle = CFArrayCreateMutable(0x0, 0x200, 0x0);
var_240 = objc_getClass("UIView");
_FindClassesToSwizzleInImage(*_XXKitImage, &var_240, 0x2);
if (*_WebKitImage != 0x0) {
var_230 = objc_getClass("WKWebView");
*(&var_230 + 0x8) = objc_getClass("WKWebsiteDataStore");
*(&var_230 + 0x10) = objc_getClass("WKUserScript");
*(&var_230 + 0x18) = objc_getClass("WKUserContentController");
*(&var_230 + 0x20) = objc_getClass("WKScriptMessage");
*(&var_230 + 0x28) = objc_getClass("WKProcessPool");
*(&var_230 + 0x30) = objc_getClass("WKProcessGroup");
*(&var_230 + 0x38) = objc_getClass("WKContentExtensionStore");
_FindClassesToSwizzleInImage(*_WebKitImage, &var_230, 0x8);
rcx = CFArrayGetCount(*_classesToSwizzle);
if (rcx != 0x0) {
rax = 0x0;
rax = CFArrayGetValueAtIndex(*_classesToSwizzle, rax);
rbx = objc_getClass(rax);
var_290 = dyld_image_header_containing_address(rbx);
var_230 = 0x0;
r14 = class_copyMethodList(rbx, &var_230);
if (var_230 != 0x0) {
rbx = 0x0;
r13 = *(r14 + rbx * 0x8);
r12 = method_getName(r13);
r15 = sel_getName(r12);
if ((((((((((((((((*(int8_t *)r15 != 0x5f) && (dyld_image_header_containing_address(method_getImplementation(r13)) == var_290)) && (((*(int8_t *)_envIgnoreRetainRelease == 0x0) || (((strcmp(r15, "retain") != 0x0) && (strcmp(r15, "release") != 0x0)) && (strcmp(r15, "autorelease") != 0x0))))) && (((*(int8_t *)_envIgnoreDealloc == 0x0) || ((strcmp(r15, "dealloc") != 0x0) && (strcmp(r15, ".cxx_destruct") != 0x0))))) && (((*(int8_t *)_envIgnoreNSObjectThreadSafeMethods == 0x0) || ((((strcmp(r15, "description") != 0x0) && (strcmp(r15, "debugDescription") != 0x0)) && (strcmp(r15, "self") != 0x0)) && (strcmp(r15, "class") != 0x0))))) && (strcmp(r15, "beginBackgroundTaskWithExpirationHandler:") != 0x0)) && (strcmp(r15, "beginBackgroundTaskWithName:expirationHandler:") != 0x0)) && (strcmp(r15, "endBackgroundTask:") != 0x0)) && (strcmp(r15, "lockFocus") != 0x0)) && (strcmp(r15, "lockFocusIfCanDraw") != 0x0)) && (strcmp(r15, "lockFocusIfCanDrawInContext:") != 0x0)) && (strcmp(r15, "unlockFocus") != 0x0)) && (strcmp(r15, "openGLContext") != 0x0)) && (strncmp(r15, "webThread", 0x9) != 0x0)) && (strncmp(r15, "nsli_", 0x5) != 0x0)) && (strncmp(r15, "nsis_", 0x5) != 0x0)) {
if (*_userSuppressedClasses != 0x0) {
rax = CFStringCreateWithCStringNoCopy(0x0, var_258, 0x8000100, *_kCFAllocatorNull);
var_244 = CFSetContainsValue(*_userSuppressedClasses, rax) != 0x0 ? 0x1 : 0x0;
CFRelease(rax);
var_244 = 0x0;
if (*_userSuppressedSelectors != 0x0) {
rax = CFStringCreateWithCStringNoCopy(0x0, r15, 0x8000100, *_kCFAllocatorNull);
if (CFSetContainsValue(*_userSuppressedSelectors, rax) != 0x0) {
var_244 = 0x1;
CFRelease(var_250);
if (*_userSuppressedMethods != 0x0) {
rax = CFStringCreateWithFormat(0x0, 0x0, @"-[%s %s]");
var_250 = CFSetContainsValue(*_userSuppressedMethods, rax);
CFRelease(rax);
rax = var_250 | var_244;
if (rax == 0x0) {
_addSwizzler(r13, r12, var_258, r15, 0x1);
*_userSuppressionsCount = *_userSuppressionsCount + 0x1;
if (var_244 != 0x0) {
*_userSuppressionsCount = *_userSuppressionsCount + 0x1;
_addSwizzler(r13, r12, var_258, r15, 0x1);
rbx = rbx + 0x1;
} while (rbx & var_230);
_objc_flush_caches(var_280);
free(r14);
rax = var_288 + 0x1;
rcx = var_278;
} while (rax != rcx);
*_totalSwizzledClasses =
if (*(int8_t *)_envVerbose != 0x0) {
rdx = *_totalSwizzledM
fprintf(*___stderrp, "Swizzled %zu methods in %zu classes.\n", rdx, rcx);
代码乍一看很多,其实逻辑非常简单,概述如下:
通过获取UIView的类实体(不理解类实体的去看runtime)所在的地址来反推所在的image(二进制产物,基本是动态库),这里基本能猜测是UIKit。
从UIKit中获取所有继承自UIView和UIApplication的类及其子类(这也是你为什么会在刚刚上文提到的输出中发现UIIBApplication这种不知道啥类的原因),过滤到带_的私有类,然后对剩下的类的所有的方法进行Swizzle。
对于需要Swizzle的方法,要额外判断是不是真正属于UIKit这个动态库的。 比如我们在调试的时候,Xcode会加载libViewDebugging.dylib等不会用于用于线上的动态库,里面会给UIView填上很多奇奇怪怪的方法。
过滤如下的方法,以及以nsli_和nsis_开头的方法。
autorelease
.cxx_destruct
description
debugDescription
beginBackgroundTaskWithExpiratonHandler
beginBackgroundTaskWithName:expirationHandler:
endBackgroundTask:
opneGLContext:
lockFocusIfCanDrawInContext:
lockFocusIfCanDraw
unlockFocus
可选,如果还要检查WebKit相关的方法,还可以Hook如下这些类的子类:
WKWebsiteDataStore
WKUserScript
WKUserContentController
WKScriptMessage
WKProcessPool
WKProcessGroup
WKContentExtensionStore
0x2 自己实现当时看到这,关于苹果的实现我觉得实在是太简单了,即使不用私有API,结合现在Github上的轮子我自己造一个估计1、2个小时就解决了。现在回想起来,自己还是too simple, sometimes native
大致代码获取UIKit中UIView和UIApplication所有子类的代码如下:
NSArray *findAllUIKitClasse()
static NSMutableArray *viewClasses = nil;
if (!viewClasses) return
uint32_t image_count = _dyld_image_count();
for (uint32_t image_index = 0; image_index & image_ image_index++) {
const my_macho_header *mach_header = (const my_macho_header *)_dyld_get_image_header(image_index);
const char *image_name = _dyld_get_image_name(image_index);
NSString *imageName = [NSString stringWithUTF8String:image_name];
if ([imageName hasSuffix:@"UIKit"]) {
unsigned int
const char **
dladdr(mach_header, &info);
classes = objc_copyClassNamesForImage(info.dli_fname, &count);
for (int i = 0; i & i++) {
const char *className = (const char *)classes[i];
NSString *classname = [NSString stringWithUTF8String:className];
if ([classname hasPrefix:@"_"]) {
Class cls = objc_getClass(className);
Class superCls =
bool isNeedChild = NO;
while (superCls != [NSObject class]) {
if (superCls == NSClassFromString(@"UIView") || superCls == NSClassFromString(@"UIApplication")) {
isNeedChild = YES;
superCls = class_getSuperclass(superCls);
if (isNeedChild) {
[viewClasses addObject:cls];
return viewC
2.1 现有方案Hook的缺陷到这,我们就只差把这些类的方法都Hook掉就行了。传统的Method Swizzling肯定不行,那样我们需要对每个方法对应实现一个新的方法进行替换,工作量太大。所以我们需要一个思路能够中心重定向整个过程。
之前跟着网易iOS大佬刘培庆学习iOS的时候,了解到了AnyMethodLog,听说能监控所有类所有方法的执行,于是我就直接套用了这个框架,嘿嘿,使用起来真方便,看起来大功告成了,Build & Run。
卧槽,怎么运行了就启动崩了,一脸懵逼。
没事,我换个开源库BigBang改改。卧槽,还是崩了。这下必须要开下源码分析下原因了。
从AnyMethodLog的实现来看,如下所示:
BOOL qhd_replaceMethod(Class cls, SEL originSelector, char *returnType) {
Method originMethod = class_getInstanceMethod(cls, originSelector);
if (originMethod == nil) {
return NO;
const char *originTypes = method_getTypeEncoding(originMethod);
IMP msgForwardIMP = _objc_msgF
#if !defined(__arm64__)
if (qhd_isStructType(returnType)) {
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:originTypes];
if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
msgForwardIMP = (IMP)_objc_msgForward_
IMP originIMP = method_getImplementation(originMethod);
if (originIMP == nil || originIMP == msgForwardIMP) {
return NO;
class_replaceMethod(cls, originSelector, msgForwardIMP, originTypes);
class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)qhd_forwardInvocation, "v@:@");
SEL newSelecotr = qhd_createNewSelector(originSelector);
BOOL isAdd = class_addMethod(cls, newSelecotr, originIMP, originTypes);
if (!isAdd) {
DEV_LOG(@"class_addMethod fail");
return YES;
void qhd_forwardInvocation(id target, SEL selector, NSInvocation *invocation) {
NSArray *argList = qhd_method_arguments(invocation);
SEL originSelector = invocation.selector;
NSString *originSelectorString = NSStringFromSelector(originSelector);
[invocation setSelector:qhd_createNewSelector(originSelector)];
[invocation setTarget:target];
[invocation invoke];
作者的意图比较简单,主要可以概述为如下几点:
把每个类的forwardInvocation,替换成自己实现的一个C函数。
把需要Hook原来selector获取的method的IMP指向objc_msgForward,通过其触发消息转发,也就是触发forwardI
对每个需要重定向的selector,生成一个特定的格式的新selector,将其IMP指向原来method的IMP。
对于刚刚重定向的C函数,通过NSInvocation获取要调用的target和selector,再次将这个selector生成特定格式的新selector,反射调用。
为啥能把OC的函数forwardInvocation换成C函数,原因就在于只要补上OC函数隐式的前两个参数self, selector,让其的函数签名一致即可。
读到这,看起来没有啥问题吧?为什么会崩溃呢!!原因在于这种调用方式,缺少了super上下文。
假设我们现在对UIView、UIButton都Hook了initWithFrame:这个方法,在调用[[UIView alloc] initWithFrame:]和[[UIButton alloc] initWithFrame:]都会定向到C函数qhd_forwardInvocation中,在UIView调用的时候没问题。但是在UIButton调用的时候,由于其内部实现获取了super initWithFrame:,就产生了循环定向的问题。
问题本质的原因是,由于我们对于父类、子类同名的方法都换成了同一个IMP,那么不论是走objc_msgSend抑或是objc_msgSendSuper2,获取到的IMP都是一致的。而在Hook之前,objc_msgSendSuper2拿到的是super_imp, objc_msgSend拿到是imp,从而不会有问题。
2.2 基于桥的全量Hook方法好,上面的一个小节我们说,如果我们把所有方法都重定向到一个IMP上的时候,就会丧失关于继承关系之间的父子上下文关系,导致重定向循环。所以,我们需要一个思路,能够正确解决上下文的问题。
首先我们来回顾下runtime的消息转发机制:
1. 调用resolveInstanceMethod:方法 (或 resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。
2. 调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。
3. 调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。
4. 调用forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非ni。
5. 调用doesNotRecognizeSelector: ,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。
对于我们来说,我们至少要在第四步之前(确切的是第三步之前),我们就要保留好super上下文。一旦到了forwardInvocation函数,留给我们的又只有self这样的残缺信息了。
哎,我就是卡在这思考了一天,最终我想出了一个思路。
提供一个桩WZQMessageStub,这个桩保留了class和selector,拼接成不一样的函数名,这样就能区分UIButton和UIView的同名initWithFrame:方法,因为不同的selector找到的IMP肯定不一样。
在NSObject里面实现forwardingTargetForSelector,在消息转发的时候指定把消息全部转发给WZQMessageStub。
WZQMessageStub实现methodSignatureForSelector和forwardInvocation:方法,承担真正的方法反射调用的职责。
好,思路确定了,难点还剩一个。对于forwardingTargetForSelector这个函数来说,能拿到的参数也是target和selector。在super和self调用场景下,这个参数毫无价值,因此我们需要从selector上着手。如果不做任何改变,我们这里拿到的selector肯定是诸如initWithFrame:的old selector,所以我们需要在这之前桥一下,可以按照下述流程理解:
每个方法置换到不同的IMP桥上 -& 从桥上反推出当前的调用关系(class和selector)-& 构造一个中间态新名字 -& forwardingTargetForSelector(self, 中间态新名字)
OK,大功告成。具体桥的实现我待会再单独开篇博客讲一讲。
嘿嘿,看起来很简单的任务也学习到了不少新知识。一会把代码开源到Github上。
0x3 遗留问题我在开启Main Thread Chekcer后,build了一次产物,但是在通过Mach-O文件中Load Commands部分的时候,却没有发现libMainThreadChecker.dylib的踪影,如下所示:
符号断点dlopen也并没有发现这个动态库调用的踪影,所以非常好奇苹果是怎么加载这个动态库的,有大佬知道请赐教。
Please enable JavaScript to view the
现在各大公司的App容纳的功能越来越多,导致应用包大小越来越大。而苹果对于text段的大小现在在60MB,为了避免无法上架的问题,所以很多App都开始用了动态库来避免这个问题。
这两天在帮支付宝开发一个功能的时候,由于支付宝许多模块的代码保密设计,因此只能采用动态库注入的方式进行调试。
一开始都没啥问题,但是当我在调试一个API接口的时候,却出现了一个必现的和MBProgressHUD有关的Crash问题。今天就让我用这个Crash开始,来探讨下KVO在不同的二进制中多个符号并存的Crash问题。
不同产物中同名符号的处理问题我们都知道,在同一个编译-&Link的最终产物中,符号(类、MetaClass、甚至是全局的函数符号)定义是不能重复的(当然,我们需要排除weak symbol)。否则在ld期间,就会报duplicate symbol这样的错误。
但是在不同的最终产物里,比如一个主二进制和其相关的动态库,由于这两种MachO类型为产物完全脱离,因此在这两个产物中分别定义相同的符号是完全行得通的。
有人会问了,那我们在主二进制中定义一个类,在动态库中又定义了一个同名的类,当我在主二进制中加载了动态库后,两个同名的类会冲突吗?
答案是不会的,其原因在于苹果使用的是two level namespace的技术。在这种形式下,符号所在的“库”的名称也会作为符号的一部分。链接的时候,staic linker会标记住在这个符号是来自于哪个库的。这样不仅大大减少了dyld搜索符号所需要的时间,也更好对后续库的更新进行了兼容。
类的加载熟悉runtime的人都知道,iOS中的类和其metaClass都是objc_class对象,这些“类”所代表的结构体,在编译期间都存在于Mach-O文件中了,位于objc_data这个section中。
而这个对象所包含的如方法、协议等等,则是以class_ro_t的形式存在于objc_const节中。
struct class_ro_t {
uint32_t instanceS
uint32_t instanceS
#ifdef __LP64__
const uint8_t * ivarL
const char *
method_list_t * baseMethodL
protocol_list_t * baseP
const ivar_list_t *
const uint8_t * weakIvarL
property_list_t *baseP
method_list_t *baseMethods() const {
return baseMethodL
无论Mach-O的产物如何,这都是静态的数据。当我们在程序使用的过程中想调用这些类,都需要将这些类从二进制中读取并进行realize变成一个正确的类。而整个realize的过程,是在主二进制程序和其依赖的动态库加载完成后进行调用的,realize的过程如下:
static Class realizeClass(Class cls)
runtimeLock.assertWriting();
const class_ro_t *
class_rw_t *
if (!cls) return
if (cls-&isRealized()) return
assert(cls == remapClass(cls));
ro = (const class_ro_t *)cls-&data();
if (ro-&flags & RO_FUTURE) {
rw = cls-&data();
ro = cls-&data()-&
cls-&changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw-&flags = RW_REALIZED|RW_REALIZING;
cls-&setData(rw);
isMeta = ro-&flags & RO_META;
rw-&version = isMeta ? 7 : 0;
cls-&chooseClassArrayIndex();
if (PrintConnecting) {
_objc_inform("CLASS: realizing class '%s'%s %p %p #%u",
cls-&nameForLogging(), isMeta ? " (meta)" : "",
(void*)cls, ro, cls-&classArrayIndex());
supercls = realizeClass(remapClass(cls-&superclass));
metacls = realizeClass(remapClass(cls-&ISA()));
#if SUPPORT_NONPOINTER_ISA
bool instancesRequireRawIsa = cls-&instancesRequireRawIsa();
bool rawIsaIsInherited = false;
static bool hackedDispatch = false;
if (DisableNonpointerIsa) {
instancesRequireRawIsa = true;
else if (!hackedDispatch
!(ro-&flags & RO_META)
0 == strcmp(ro-&name, "OS_object"))
hackedDispatch = true;
instancesRequireRawIsa = true;
else if (supercls
supercls-&superclass
supercls-&instancesRequireRawIsa())
instancesRequireRawIsa = true;
rawIsaIsInherited = true;
if (instancesRequireRawIsa) {
cls-&setInstancesRequireRawIsa(rawIsaIsInherited);
cls-&superclass =
cls-&initClassIsa(metacls);
if (supercls
!isMeta) reconcileInstanceVariables(cls, supercls, ro);
cls-&setInstanceSize(ro-&instanceSize);
if (ro-&flags & RO_HAS_CXX_STRUCTORS) {
cls-&setHasCxxDtor();
if (! (ro-&flags & RO_HAS_CXX_DTOR_ONLY)) {
cls-&setHasCxxCtor();
if (supercls) {
addSubclass(supercls, cls);
addRootClass(cls);
methodizeClass(cls);
从上述代码不难看出,整个过程非常简单,分为几个步骤:
把从二进制里面读取的readonly data变成rw data,这也是我们在iOS编程中很多运行时黑魔法的基础。
把父类和metaclass都realize一下,然后建立合理的层次依赖关系。
根据父类的布局,把自己的ivar布局动态更新,这也是大名鼎鼎的non-fragile layout
把category里面的东西都加载进来。
整个过程结束。
KVO的机制说了这么多铺垫的知识,我们来开始分析下我们程序在加载动态库后会KVO Crash的原因。处于公司数据保密的原因,我构造了一个最简单的场景,这个主二进制和动态库都包含了MBProgressHUD对应的代码,
我们可以通过nm来查看下符号:
在MBProgressHUD里面,有如下一段代码:
- (void)registerForKVO {
for (NSString *keyPath in [self observableKeypaths]) {
[self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL];
它会分别对所有的对应属性进行KVO监听,由于KVO本身的机制是通过创建一个“xxxNotify_KVO类”,所以,整体的调用顺序如下图所示:
概括如下:
整个流程会为MBProgressHUD这个类以NSKVONotifying_MBProgressHUD的名称,动态添加一个类。
对这个类构建和原先类的父子关系,注册到全局的类表中。
对KVO中使用到的监听的属性进行setter方法的覆写。
这几个流程的代码分别如下:
创建类代码非常简单,逻辑上就是这父类-子类的关系构建一个新的类出来:
Class objc_allocateClassPair(Class superclass, const char *name,
size_t extraBytes)
Class cls,
rwlock_writer_t lock(runtimeLock);
if (getClass(name)
!verifySuperclass(superclass, true)) {
= alloc_class_for_subclass(superclass, extraBytes);
meta = alloc_class_for_subclass(superclass, extraBytes);
objc_initializeClassPair_internal(superclass, name, cls, meta);
当创建完成后,就会对这个类进行registerClassPair的工作,这一步的目的很简单,就是将类注册到一个全局的map中gdb_objc_realized_classes。
重写setter, class, description之类的
Crash原因知道了原理,我们来分析Crash的原因就非常简单了,我们先看Crash的堆栈。
从汇编中不难看出,[x19, #0x20]对应的地址是个非法访问地址,导致了Crash。而x19寄存器又是从x0中赋值而来,根据函数objc_registerClassPair的参数,x0为Class,那很明显,就是从Class对象的0x20,即32 bytes偏移地方的数据。根据定义,
struct objc_class : objc_object {
// Class ISA; // 8byte
C // 8byte
// formerly cache pointer and vtable // 4 + 4 + 8
class_data_bits_
// class_rw_t * plus custom rr/alloc flags
我们要获取的数据就是bits。通过输出寄存器,我们发现x0为0,也就是nil。而x0又是从哪来的呢?
倒推堆栈,我们发现,在函数_NSKVONotifyingCreateInfoWithOriginalClass,我们首先调用了objc_allocateClassPair,将其返回值传入objc_registerClassPair(ARM64 Calling Convention)
所以,问题的本质就出现在allocateClassPair返回了nil,而allocateClassPair只有在如下场景下才会返回nil。
if (getClass(name)
!verifySuperclass(superclass, true)) {
return nil;
通过LLDB调试,在根据name查询NSKVONotifying_MBProgressHUD时,由于全局的类表已经存在了对应的类,所以在getClass就会返回之前注册的类,从而使得allocate直接返回了nil。
NXMapTable *gdb_objc_realized_
// exported for debuggers in objc-gdb.h
static Class getClass_impl(const char *name)
runtimeLock.assertLocked();
// allocated in _read_images
assert(gdb_objc_realized_classes);
// Try runtime-allocated table
Class result = (Class)NXMapGet(gdb_objc_realized_classes, name);
if (result) return result;
// Try table from dyld shared cache
return getPreoptimizedClass(name);
static Class getClass(const char *name)
runtimeLock.assertLocked();
// Try name as-is
Class result = getClass_impl(name);
if (result) return result;
// Try Swift-mangled equivalent of the given name.
if (char *swName = copySwiftV1MangledName(name)) {
result = getClass_impl(swName);
free(swName);
return result;
return nil;
结论当两个产物都有相同的类名时,这两个类都会被realize,都能够被正常调用。但是由于全局类表的存在,在动态创建KVO的子类时,只能产生一个。所以就导致allocate失败,从而引发register过程的Crash问题。
Please enable JavaScript to view the
之前在做XXXSDK的时候,我hook的UITableView的setDelegate:方法。整个SDK在接入手淘、天猫以及闲鱼等其他App的时候都没啥问题。
上周,UC的同学突然找到说,给我说了如下图所示的问题:
商业保密,不显示了
卧槽,这下我就懵逼了,看样子是把整个rowHeight给Hook坏了,那这是为什么呢?
从开发UITableView的正向角度来说:我们一般都需要给其提供一个必选的UITableViewDataSource和一个可选的UITableViewDelegate,其中,涉及到高度的是如下这个API:
()tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
有人说可以直接通过tableview.rowHeight设置高度,但是对于不同cell不同高度的动态需求,但是这里我们暂不提这种分支情况。
通过UC同学的协助,我们发现了如下输出:
通过输出不难发现,是最后的delegate被从对应的UIViewController改成了一个乱七八糟没实现对应heightForRowAtIndexPath方法的对象。
为什么会这样呢?
通过如下图所示的调用栈,
调用栈最下层是UC同学的代码;
self.tableview = [[xxxTableView alloc] init]
调用栈最上层是我们的一层防护性hook,其代码如下:
+ (void)load {
static dispatch_once_t onceT
dispatch_once(&onceToken, ^{
Swizzle([UIScrollView class], @selector(init), @selector(swizzled_init));
- (instancetype)swizzled_init
id obj = [self swizzled_init];
UIScrollView *scrollView = (UIScrollView *)
if (!scrollView.delegate) {
这段代码是什么作用呢?
我们之前提了UITableViewDelegate不是必需,因此,为了能够抓去所有UITableView的代码,我们会提供一个内置的默认delegate(当时的实现存在bug,没有实现heightForRowAtIndexPath方法)。
而且,为了防止我们的delegate覆盖了有delegate的情况,我们还特地做了!scroll.delegate的判断。
按照我们的预期设想,存在两种时间顺序情况:
我们的init先执行,此时肯定会进入我们设置默认的逻辑;然后当外部代码调用tableview.delegate = xxx的时候,会把我们这个替换掉,不会影响正常的逻辑。
我们的init后执行(比如某些子类覆盖的情况),那这样的话,当子类已经设置好delegate后,压根不会进入我们的设置逻辑。
然而,就是这一小段看起来无错的代码导致了UC的App出现了文章开头的Bug。
逆向分析UITableViewController基于10.2的UIKit,我们通过汇编来分析-[UITableViewController setTableView:]的流程:
0x18c84c640 &+0&:
x26, x25, [sp, #-0x50]!
0x18c84c644 &+4&:
x24, x23, [sp, #0x10]
0x18c84c648 &+8&:
x22, x21, [sp, #0x20]
0x18c84c64c &+12&:
x20, x19, [sp, #0x30]
0x18c84c650 &+16&:
x29, x30, [sp, #0x40]
0x18c84c654 &+20&:
x29, sp, #0x40 =0x40
0x18c84c658 &+24&:
0x18c84c65c &+28&:
0x18c84c660 &+32&:
0x objc_retain
0x18c84c664 &+36&:
0x18c84c668 &+40&:
x8, 124100
0x18c84c66c &+44&:
x1, [x8, #0xd78]
0x18c84c670 &+48&:
0x18c84c674 &+52&:
0x objc_msgSend
0x18c84c678 &+56&:
0x18c84c67c &+60&:
0x1851ca48c objc_retainAutoreleasedReturnValue
0x18c84c680 &+64&:
0x18c84c684 &+68&:
0x18c84c688 &+72&:
0x18c84c7d4 &+404&
0x18c84c68c &+76&:
x8, 124074
0x18c84c690 &+80&:
x23, [x8, #0x2e0]
0x18c84c694 &+84&:
0x18c84c698 &+88&:
0x18c84c69c &+92&:
0x objc_msgSend
0x18c84c6a0 &+96&:
0x18c84c6a4 &+100&: bl
0x1851ca48c objc_retainAutoreleasedReturnValue
0x18c84c6a8 &+104&: mov
0x18c84c6ac &+108&: adrp
x8, 124145
0x18c84c6b0 &+112&: ldrsw
x8, [x8, #0x7ac]
0x18c84c6b4 &+116&: ldr
x8, [x20, x8]
0x18c84c6b8 &+120&: cmp
0x18c84c6bc &+124&: ccmp
x22, x8, #0x4, ne
0x18c84c6c0 &+128&: b.ne
0x18c84c6d8 &+152&
0x18c84c6c4 &+132&: adrp
x8, 124073
0x18c84c6c8 &+136&: ldr
x1, [x8, #0x3c0]
0x18c84c6cc &+140&: mov
0x18c84c6d0 &+144&: mov
0x18c84c6d4 &+148&: bl
0x objc_msgSend
0x18c84c6d8 &+152&: adrp
x8, 124074
0x18c84c6dc &+156&: ldr
x24, [x8, #0x7d8]
0x18c84c6e0 &+160&: mov
0x18c84c6e4 &+164&: mov
0x18c84c6e8 &+168&: bl
0x objc_msgSend
0x18c84c6ec &+172&: mov
0x18c84c6f0 &+176&: bl
0x1851ca48c objc_retainAutoreleasedReturnValue
0x18c84c6f4 &+180&: mov
0x18c84c6f8 &+184&: bl
0x objc_release
0x18c84c6fc &+188&: cmp
0x18c84c700 &+192&: b.ne
0x18c84c718 &+216&
0x18c84c704 &+196&: adrp
x8, 124073
0x18c84c708 &+200&: ldr
x1, [x8, #0x3c8]
0x18c84c70c &+204&: mov
0x18c84c710 &+208&: mov
0x18c84c714 &+212&: bl
0x objc_msgSend
0x18c84c718 &+216&: adrp
x8, 124080
0x18c84c71c &+220&: ldr
x1, [x8, #0xe80]
0x18c84c720 &+224&: mov
0x18c84c724 &+228&: bl
0x objc_msgSend
0x18c84c728 &+232&: mov
0x18c84c72c &+236&: bl
0x1851ca48c objc_retainAutoreleasedReturnValue
0x18c84c730 &+240&: mov
0x18c84c734 &+244&: adrp
x8, 124076
0x18c84c738 &+248&: ldr
x1, [x8, #0x4b0]
0x18c84c73c &+252&: mov
0x18c84c740 &+256&: mov
0x18c84c744 &+260&: bl
0x objc_msgSend
0x18c84c748 &+264&: adrp
x8, 124080
0x18c84c74c &+268&: ldr
x1, [x8, #0x810]
0x18c84c750 &+272&: mov
0x18c84c754 &+276&: mov
0x18c84c758 &+280&: bl
0x objc_msgSend
0x18c84c75c &+284&: mov
0x18c84c760 &+288&: mov
0x18c84c764 &+292&: bl
0x objc_msgSend
0x18c84c768 &+296&: mov
0x18c84c76c &+300&: bl
0x1851ca48c objc_retainAutoreleasedReturnValue
0x18c84c770 &+304&: mov
0x18c84c774 &+308&: bl
0x objc_release
0x18c84c778 &+312&: cbnz
x23, 0x18c84c790 &+336&
0x18c84c77c &+316&: adrp
x8, 124100
0x18c84c780 &+320&: ldr
x1, [x8, #0xd80]
0x18c84c784 &+324&: mov
0x18c84c788 &+328&: mov
0x18c84c78c &+332&: bl
0x objc_msgSend
0x18c84c790 &+336&: mov
0x18c84c794 &+340&: mov
0x18c84c798 &+344&: bl
0x objc_msgSend
0x18c84c79c &+348&: mov
0x18c84c7a0 &+352&: bl
0x1851ca48c objc_retainAutoreleasedReturnValue
0x18c84c7a4 &+356&: mov
0x18c84c7a8 &+360&: bl
0x objc_release
0x18c84c7ac &+364&: cbnz
x23, 0x18c84c7c4 &+388&
0x18c84c7b0 &+368&: adrp
x8, 124073
0x18c84c7b4 &+372&: ldr
x1, [x8, #0x3c8]
0x18c84c7b8 &+376&: mov
0x18c84c7bc &+380&: mov
0x18c84c7c0 &+384&: bl
0x objc_msgSend
0x18c84c7c4 &+388&: mov
0x18c84c7c8 &+392&: bl
0x objc_release
0x18c84c7cc &+396&: mov
0x18c84c7d0 &+400&: bl
0x objc_release
0x18c84c7d4 &+404&: mov
0x18c84c7d8 &+408&: bl
0x objc_release
0x18c84c7dc &+412&: mov
0x18c84c7e0 &+416&: ldp
x29, x30, [sp, #0x40]
0x18c84c7e4 &+420&: ldp
x20, x19, [sp, #0x30]
0x18c84c7e8 &+424&: ldp
x22, x21, [sp, #0x20]
0x18c84c7ec &+428&: ldp
x24, x23, [sp, #0x10]
0x18c84c7f0 &+432&: ldp
x26, x25, [sp], #0x50
0x18c84c7f4 &+436&: b
0x objc_release
一看到adrp, ldr的搭配,基本可以确定是取某个方法进行调用。
看到一大堆的bl objc_retain,bl objc_release,不用管,反正都是ARC帮我们自动插入的。
可以看出,当传入给UITableViewController的tableView含有dataSource和delegate,UITableViewController都不会对其进行处理;否则会进行一个默认的设置。
我自己理解后转写的伪代码如下:
UITableView *oldTableView = [self __existingTableView];
if (oldTableView == xxxtableView) {
id oldDataSource = [oldTableView dataSource];
id filteredDataSource = [self _filteredDataSource];
if (oldDataSource != filteredDataSource)
[oldTableView setDataSource:nil];
id oldDelegate = [oldTableView delegate];
if (oldeDelegate != self)
[oldTableView setDelegate:nil];
id oldRefreshControl = [oldTableView _refreshControl];
[self setView:xxtableView];
[xxxtableView _setRefreshControl:oldRefreshControl];
id newDataSource = [xxxtableview dataSource];
if (!newDataSource) {
[self _applyDefaultDataSourceToTable:xxxTableView];
id newDelegate = [xxxtableview delegate];
if (!newDelegate) {
[xxxTableView setDelegate:self];
结论通过上面对汇编和伪代码的理解,我们可以很轻易的得出结论:当我们处于第一种情形的实现,我们将tableview.delegate设置成了我们的stub。因为不为空,所以UITableViewController默认不会对其进行处理,而由于我们当时没有提供stub对于heightForRowAtIndexPath的实现,导致出现了UC的bug。
Please enable JavaScript to view the
微信高性能线上日志系统xlog剖析做移动开发的同学经常会遇到一个头疼的问题,就是当用户反馈一些问题,又比较冷僻难以复现的时候(不是Crash),常常就会陷入一筹莫展的境地。因此,很多人就研发了相关的监控系统,比如一些知名的APM来监测帧率、内存、电量等等,将这些数据进行采集、合并再上报至专门的平台供开发测试同学查看。但是这些APM往往都是粗粒度的监控,究其原因就在于如果特别精细的进行监控,线上的性能会吃不消,一些监控反而影响了用户的正常使用。
说了这么多,抛开获取数据方面的难度不提,线上监控的本质还是在于信息(日志)记录,而端上的日志记录存在一个社会主义初级阶段的供需矛盾:
即实时细粒度的日志记录的性能落差和日志的完整不丢失无法兼顾。
如果你要高性能、细粒度的记录日志,那你势必大量使用内存。而大量使用使用内存,万一没电了、程序突然崩了,这些中间态的日志还没持久化,就相当于白费了精力;而如果你想保证可靠性,那你就需要经常实时落盘。我们知道,写磁盘的行为是会设计用户态和内核态的切换,在高流畅性的要求下是绝对会影响性能了,而且这还不是你开多线程能够解决的问题。
写磁盘为什么会非常慢现如今、几乎所有的操作系统在管理内存的时候,基本采用了页式管理的策略。即将连续的内存空间(注意空间,不是地址)换成了一个个页式大小。这样的好处有几点:
按页这种大小进行管理、可以有效的减少内存碎片的粒度。
按页加载,可以充分利用磁盘上的交换空间,使得程序使用的空间能大大超过内存限制。
当然,iOS设备上不存在交换空间,但是也依然按照页式结构进行内存管理。
回到为什么写磁盘会慢的问题上。我们一般会把内存中的数据进行持久化储存到磁盘上。但是写入磁盘并不是你想写就立刻写的,数据是通过flush的方式从内存写回到磁盘,一般有如下几种情况:
通过页的flag标记为有改动,操作系统定时将这种脏页写回到磁盘上,时机不可控。
调用用户态的写接口-&触发内核态的sys_write-&文件系统将数据写回磁盘。
乍一看上述第二种方式非常适合写日志,但是其包含两个非常明显的问题:
文件系统处于效率不会立刻将数据写回到磁盘(比如磁道寻址由于机械操作的原因相对非常耗时),而是以Block块的形式缓存在队列中,经过排序、合并到达一定比例之后再写回磁盘。
这种方式在将数据写回到磁盘时,需要经历两次拷贝。一次是把数据从用户态拷贝到内核态,需要经历上下文切换;还有一次是内核空间到硬盘上真正的数据拷贝。当切换次数过于频繁,整体性能也会下降。
基于上述这些问题,xlog采用了mmap的方案进行日志系统的设计:
mmap 是使用逻辑内存对磁盘文件进行映射,中间只是进行映射没有任何拷贝操作,避免了写文件的数据拷贝。操作内存就相当于在操作文件,避免了内核空间和用户空间的频繁切换。
除了系能耐,使用mmap还能保证日志的完整性,因为如下这些情况下回自动回写磁盘:
进程 crash
调用 msync 或者 munmap
不设置 MAP_NOSYNC 情况下 30s-60s(仅限FreeBSD)
xlog源码分析xlog的代码主要分为两块,面向上层的使用封装xlogger,暴露了一系列的借口。以及核心的appender和log等。
log_bufferlog_buffer其目的是封装了一个对mmap/传统内存操作的数据结构。其核心思想就是将上层的操作转换对实际开辟出来的日志缓存地址进行读写(也封装了加密压缩操作等等)。我们以写操作为例子进行剖析:
bool LogBuffer::Write(const void* _data, size_t _length) {
if (NULL == _data || 0 == _length) {
return false;
if (buff_.Length() == 0) {
if (!__Reset()) return false;
size_t before_len = buff_.Length();
size_t write_len = _
if (is_compress_) {
cstream_.avail_in = (uInt)_
cstream_.next_in = (Bytef*)_
uInt avail_out = (uInt)(buff_.MaxLength() - buff_.Length());
cstream_.next_out = (Bytef*)buff_.PosPtr();
cstream_.avail_out = avail_
if (Z_OK != deflate(&cstream_, Z_SYNC_FLUSH)) {
return false;
write_len = avail_out - cstream_.avail_
buff_.Write(_data, _length);
before_len -= remain_nocrypt_len_;
AutoBuffer out_
size_t last_remain_len = remain_nocrypt_len_;
log_crypt_-&CryptAsyncLog((char*)buff_.Ptr() + before_len, write_len + remain_nocrypt_len_, out_buffer, remain_nocrypt_len_);
buff_.Write(out_buffer.Ptr(), out_buffer.Length(), before_len);
before_len += out_buffer.Length();
buff_.Length(before_len, before_len);
log_crypt_-&UpdateLogLen((char*)buff_.Ptr(), (uint32_t)(out_buffer.Length() - last_remain_len));
return true;
不难看出,整体上就是对写入的数据进行加密,如果有压缩的需求同时进行压缩。并将修改后的数据存入真正的mmap文件/内存缓存中。
如果不能理解的话,可以看下我画的这幅图进行表示:
appenderxlog方案真正的核心实际上只有一个appender文件,本质上的思路都比较清晰,将添加日志分为同步写和异步写。异步写的方式比较常用,下文会基于这个分析。
首先是日志系统的初始化配置
assert(_dir);
assert(_nameprefix);
if (!sg_log_close) {
__writetips2file("appender has already been opened. _dir:%s _nameprefix:%s", _dir, _nameprefix);
xlogger_SetAppender(&xlogger_appender);
boost::filesystem::create_directories(_dir);
tickcount_t
tick.gettickcount();
__del_timeout_file(_dir);
tickcountdiff_t del_timeout_file_time = tickcount_t().gettickcount() -
tick.gettickcount();
char mmap_file_path[512] = {0};
snprintf(mmap_file_path, sizeof(mmap_file_path), "%s/%s.mmap2", sg_cache_logdir.empty()?_dir:sg_cache_logdir.c_str(), _nameprefix);
bool use_mmap = false;
if (OpenMmapFile(mmap_file_path, kBufferBlockLength, sg_mmmap_file))
sg_log_buff = new LogBuffer(sg_mmmap_file.data(), kBufferBlockLength, true, _pub_key);
use_mmap = true;
char* buffer = new char[kBufferBlockLength];
sg_log_buff = new LogBuffer(buffer, kBufferBlockLength, true, _pub_key);
use_mmap = false;
4. 注意点1!!!!!!!!!!!!!!!!!!!!
if (NULL == sg_log_buff-&GetData().Ptr()) {
if (use_mmap && sg_mmmap_file.is_open())
CloseMmapFile(sg_mmmap_file);
5. 注意点2!!!!!!!!!!!!!!!!
sg_log_buff-&Flush(buffer);
ScopedLock lock(sg_mutex_log_file);
sg_logdir = _
sg_logfileprefix = _
sg_log_close = false;
appender_setmode(_mode);
lock.unlock();
char mark_info[512] = {0};
get_mark_info(mark_info, sizeof(mark_info));
if (buffer.Ptr()) {
__writetips2file("~~~~~ begin of mmap ~~~~~\n");
__log2file(buffer.Ptr(), buffer.Length());
__writetips2file("~~~~~ end of mmap ~~~~~%s\n", mark_info);
6. 添加一些关于xlog自身的信息
tickcountdiff_t get_mmap_time = tickcount_t().gettickcount() -
char appender_info[728] = {0};
snprintf(appender_info, sizeof(appender_info), "^^^^^^^^^^" __DATE__ "^^^" __TIME__ "^^^^^^^^^^%s", mark_info);
xlogger_appender(NULL, appender_info);
char logmsg[64] = {0};
snprintf(logmsg, sizeof(logmsg), "del time out files time: %" PRIu64, (int64_t)del_timeout_file_time);
xlogger_appender(NULL, logmsg);
snprintf(logmsg, sizeof(logmsg), "get mmap time: %" PRIu64, (int64_t)get_mmap_time);
xlogger_appender(NULL, logmsg);
xlogger_appender(NULL, "MARS_URL: " MARS_URL);
xlogger_appender(NULL, "MARS_PATH: " MARS_PATH);
xlogger_appender(NULL, "MARS_REVISION: " MARS_REVISION);
xlogger_appender(NULL, "MARS_BUILD_TIME: " MARS_BUILD_TIME);
xlogger_appender(NULL, "MARS_BUILD_JOB: " MARS_TAG);
snprintf(logmsg, sizeof(logmsg), "log appender mode:%d, use mmap:%d", (int)_mode, use_mmap);
xlogger_appender(NULL, logmsg);
BOOT_RUN_EXIT(appender_close);
有几点需要特别注意点:
如果我们尝试打开mmap成功了,但是mmap对应的数据地址是NULL,那我们必须停止映射。因为NULL所代表的地址处于内核态,一旦映射了,势必造成Crash。
注意点2:使用mmap的情况下,如果上次应用断电了、Crash,日志的信息还是存在的,但是并不一定能及时的转换成我们想要的日志文件。因此我们首先检查下mmap文件里面有没有数据,有的话先把这部分转换成日志。
而通过上层添加的日志,都会通过之前的xlogger_appender进行调用,进而往下层的__appender_async 记录日志。
__appender_async__appender_async 需要和其异步dump线程一起搭配看,是两段非常有意思的代码,它涉及了一个将mmap/内存数据写回到磁盘的策略。
首先是添加日志:
static void __appender_async(const XLoggerInfo* _info, const char* _log) {
ScopedLock lock(sg_mutex_buffer_async);
if (NULL == sg_log_buff) return;
char temp[16*1024] = {0};
PtrBuffer log_buff(temp, 0, sizeof(temp));
log_formater(_info, _log, log_buff);
if (sg_log_buff-&GetData().Length() &= kBufferBlockLength*4/5) {
int ret = snprintf(temp, sizeof(temp), "[F][ sg_buffer_async.Length() &= BUFFER_BLOCK_LENTH*4/5, len: %d\n", (int)sg_log_buff-&GetData().Length());
log_buff.Length(ret, ret);
if (!sg_log_buff-&Write(log_buff.Ptr(), (unsigned int)log_buff.Length())) return;
if (sg_log_buff-&GetData().Length() &= kBufferBlockLength*1/3 || (NULL!=_info && kLevelFatal == _info-&level)) {
sg_cond_buffer_async.notifyAll();
其次是异步线程Dump成日志
static void __async_log_thread() {
while (true) {
ScopedLock lock_buffer(sg_mutex_buffer_async);
if (NULL == sg_log_buff) break;
sg_log_buff-&Flush(tmp);
lock_buffer.unlock();
if (NULL != tmp.Ptr())
__log2file(tmp.Ptr(), tmp.Length());
if (sg_log_close) break;
sg_cond_buffer_async.wait(15 * 60 *1000);
不难看出,整个日志的主要策略就是利用mmap将日志写入到磁盘映射上,当超过三分之一的时候通知异步线程去写日志。
这样就利用了mmap的实时性、完整性打造了一个逻辑非常清晰易懂的日志,整体架构图如下:
Please enable JavaScript to view the
在上文中,我们提到了有个神秘的__OBJC段,Runtime的许多机制就是依赖于它。但是无论我怎么搜索网上相关的资料、苹果的官方文档,都发现找不到这个段了。
一脸懵逼。没事,打开class-dump,看看它怎么处理的。嘿嘿,果不其然,在Class-Dump的代码里,有着如下注释:
@0xced Old ABI has an OBJC segment. New ABI has a DATA,__objc_info section
通俗解释来说,我们先如今使用的都是Objective-C2.0,所以原先的__OBJC段的东西都不存在了,而是存入了__DATA段里。所以,我们就以如下这张图来探究下这些与Runtime加载有关的节。
这个节可以看作是区别Objective-C 1.0与2.0的区别。从苹果的OBJC源码中能看到这个节的数据结构定义(去除Swift相关)如下:
typedef struct {
} objc_image_
其中version这个字段目前永远为0。flags是用于做表示需要支持的特性的,比如是否需要/支持 Garbage Collection。
SupportsGC
RequiresGC
if (ii.flags & (1&&1)) {
wantsGC = YES;
__objc _classlist节这个节列出了所有的class(metaclass自身也是一种class)。
以计算器举例:我们先从MachoView找出一段数据,这个数据代表的就是class结构体所在的地址,如下图:
通过hopper查看地址:A128,得到如下结果:
内存地址(还没rebase过)中包含一个类本身的含义是什么意思呢?这都需要从Runtime里面来说起。
我们假设说我们有个类A,其父类为AA。有两个A类型的实例a1, a2。
我们都知道在真正调用[a haha]的方法的时候,实质上是通过objc_msgSend执行一系列的函数查询来找到真正的函数IMP,进而产生函数调用的。
由于objc_msgSend的调用返回值是不确定的,需要根据不同的状态来返回,比如ARM64下的。因此其本身的实现需要通过汇编来,我们截取最终要的一段ARM64的汇编如下:
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
LNilOrTagged
x16, x13, #ISA_MASK
LGetIsaDone:
CacheLookup NORMAL
X0是函数调用者,即Self,比较其和nil的关系,如果是nil(或者tagged pointer)就走另外一种分支。通过此,我们也不难理解为什么可以对nil发送消息了
根据self所在的地址,取其成员变量isa。
x16 = x13 & MASK,也就意味着x16指向了内存里面的对应A class对象(注意:不是A class的实例对象)
上述为什么要对ISA进行一个mask的位与操作,主要原因和Tagged Pointer类似,理由就不再赘述。
执行CacheLookUp,具体的代码流程简要如下:
.macro CacheLookup
x10, x11, [x16, #CACHE]
w12, w1, w11
x12, x10, x12, LSL #4
x9, x17, [x12]
CacheHit $0
CheckMiss $0
x9, x17, [x12, #-16]!
x12, x12, w11, UXTW #4
x9, x17, [x12]
CacheHit $0
CheckMiss $0
x9, x17, [x12, #-16]!
JumpMiss $0
我们接着再来读读这段汇编。
x16承接上段汇编,是A class的实体,取出其cache成员变量。
按照_cmd和mask的位运算,找出其在bucket数组中的偏移量。取出的数据结构是个bucket_t,如下:
struct bucket_t {
cache_key_t _
从上述数据结构不难理解,cache对象里面存了一个bucket数组,用于进行SEL对应的IMP,缓存。key是SEL对应的地址。
如果地址相同,就代表命中,执行CacheHit,其实就是简单的br x17。由于此时x17是IMP,即对应的函数地址,直接跳过去就完事了,这个分支下的objc_msgSend就执行完成了。
那如果不相同,即命中的bucket里面不是我们要的SEL,就检查这个命中的桶是不是没有SEL,如果是空的,执行__objc_msgSend_uncached。这步后续开始就是去查找类方法列表-&父类方法列表了。
如果不为空,否则就执行循环,进行查询。
**一些细节知识:
.macro可以在汇编里面定义一段可以被复用的代码段。
.1b 代表的是向回找label定义为1的代码片段起始;1f代表向下找label定义为1的代码片段起始。
为什么在计算isa的时候先要位与一个mask,其原因在于现在的isa是一个兼具多种含义的指针。**
本文重点不在讲述Runtime上,所以objc_msgSend的细节就不去更深入的探究了。
所以,按照上述步骤来理解,我们可以发现,苹果实例对象的objc_msgSend的机制可以简要抽象如下图例子:
__objc _catlist节该节顾名思义,代表的就是程序里面有哪些Category。我们还是通过MachoView和Hopper来看一看:
从Hopper里面看出的内容我们不难得到,catlist也对应着一个Category_t的实体,会在程序运行的过程中存在于内存中。这个结构体的数据定义如下:
struct category_t {
const char *
classref_t
struct method_list_t *instanceM
struct method_list_t *classM
struct protocol_list_t *
struct property_list_t *instanceP
struct property_list_t *_classP
__objc_protolist该节的理解也非常简单,代表的就是程序里面有哪些Protocol。数据结构定义如下:
struct protocol_t : objc_object {
const char *mangledN
struct protocol_list_t *
method_list_t *instanceM
method_list_t *classM
method_list_t *optionalInstanceM
method_list_t *optionalClassM
property_list_t *instanceP
const char **_extendedMethodT
const char *_demangledN
property_list_t *_classP
__objc_classrefs一开始这个节的意义我实在是没看懂。实在不理解在已经存在classlist这个数据节的情况下,为啥还是需要用这个类。后来经过一番实验发现,该节的目的是为了标记这个类究竟有没有被引用
那有没有被引用的意义是什么?可以包瘦身。如果在MachoView中都能直观告诉我们没有引用的类甚至是方法,都可以直接剔除了。
但是,作为一名经常奋战在包瘦身一线的同学,我可以直接告诉你,上述的想法是大错特错的。苹果这种可以利用字符串拼接从而调用大量runtime的方法,绝对坑哭了做包瘦身的人。
嘿嘿,不过其实这样也没啥难度,下一篇我会写一个基于Macho的包瘦身方案,绝对轻便简洁,不用基于AST来分析各种调用关系,这里卖个关子。
__objc_selrefs这节的原理同上,告诉你究竟有哪些SEL对应的字符串被引用了。
__objc_superrefs这节虽然中字面意义上我们知道,是对超类(即父类)的引用,但是没理解啊,为什么要有这么一个破玩意。不懂就一点点摸索,从MachoView里面来看,数据对应的地址还是指向一个个在classlist出现的类实体。
通过和classlist里面出现的数据进行diff对比,如下图所示:
可以发现,所有出现的objc_superrefs都是会被继承的类。那么,为什么要单独设计这样一个来存放这样的信息呢?
哈哈哈:我上面的分析都是错的!!!!哈哈哈:我上面的分析都是错的!!!!哈哈哈:我上面的分析都是错的!!!!
真正的原因如下:我们知道,我们在子类调用一个方法的时候,为了调用上层的父类的实现(如果有),常常会写出一个[super message]的代码。而这样的代码,在底层是会转换成调用objc_msgSendSuper2。而其接受的参数,第一个为结构体objc_super2,第二个为SEL。其中objc_super2的定义如下:
struct objc_super2 {
Class current_
为了构造这样的数据结构体,在汇编层面会将[super message]转换成如下的汇编指令:
注意看红框内的汇编代码,我们来分步骤解释下整体的汇编结构:
首先在调用[ViewController viewDidLoad]的时候,x0是self(ViewController的实例),x1是@selector(viewDidLoad)。
0x 偏移的地方将sp向下申请了48(0x30)bytes的空间。
0x 将SP的地址存到的x8寄存器中。 这个X8寄存器会很关键
0x 通过adrp指令加载内存数据中的一个page,根据这个page的offset找到对应的viewDidLoad方法的ref。存入x9。
0x 通过x9寄存器中ref指向的地址,以该地址为内存读取真正的SEL,存入x1。
至此,调用objc_msgSendSuper2的第二个参数准备完毕,我们再来看看第一个的参数是如何设置的。
0x 同样的方式,加载一个page的0x78的偏移位置的数据,点进去会发现是个class地址,存到x10中。
然后,就轮到我们的栈空间出场了。我们先把x0存到sp处,然后再把x10,也就是上面说的class地址存入sp+8 (str x10, [sp, #0x8]。
最后,还记得我们之前提到的x8寄存器吗?我们之前可是将sp的值赋予了x8了。所以,在1000046fc x0, x8这个地方,我们将x8的值赋予了x0。至此,调用objc_msgSendSuper2的第一个参数也准备完毕
最后附上objc_msgSendSuper2的代码供参考,逻辑非常简单,不再赘述。
ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
MESSENGER_START
x0, x16, [x0]
x16, [x16, #SUPERCLASS]
CacheLookup NORMAL
END_ENTRY _objc_msgSendSuper2
等等,心急的读者会问:你说了那么一大堆,你还是没解释到底为什么要存在superrefs?
在Objective-C的设计里面,函数就是函数,它并不知道自己属于哪个类里面。换句通俗的话来说,必须是你(编译器)说去哪个class实体的方法列表里面寻找调用,才会真正的去找对应的方法,函数自身不知道是父类还是子类。同时,由于苹果的设计原因,一个类初始化的实例,是不具备了解superclass的条件的,只有通过isa对应的类实体才能获得。因此,在构建objc_msgSendSuper2的第一个参数的时候,就不如指在编译期定其对应的current_class,以方便后续的superclass方法列表查找。
而且,也必须在编译期间,根据当前的类,去定义current_class这个字段的值,不然当我们有多个层级的继承关系时,在运行时如何从单一的self参数构建正确的向上查找层级,就当前的OC设计里,就做不到了。
C++里面,对于函数来说,是可以明确知道对应的所属类的。究其原因,在于C++的不同类,都是不同的命名空间,调用父类的方法时,需明确指定父类的命名空间,如BASE::method。
__objc_const这个节的含义是所有初始化的常量的都显示在这。但是很多人都对此节有着巨大的误解,认为const int k = 5对应的数据会存放在__objc_const节中。
但是这是大错特错的,在代码里声明的const类型,实质上都属于__TEXT段,并属于其中的const节。而在__objc_const中存放的,是一些需要在类加载过程中用到的readonly data。具体这个readonly data包含了如下(但不限于)的数据结构:
struct class_ro_t {
uint32_t instanceS
uint32_t instanceS
#ifdef __LP64__
const uint8_t * ivarL
const char *
method_list_t * baseMethodL
protocol_list_t * baseP
const ivar_list_t *
const uint8_t * weakIvarL
property_list_t *baseP
method_list_t *baseMethods() const {
return baseMethodL
struct method_list_t:entsize_list_tt {
uint32_t entsizeAndF
struct method_t {
const char *
关于readonly data后续会再开一个章节单独讲解。
结尾基本上MachO 关于Runtime涉及的主要的类就分析到这了,下一次继续剖析其他细枝末节。
Please enable JavaScript to view the
起因最近在公司里和一些同事搞了一些东西,略微底层。于是希望借这个机会好好把Macho相关的知识点梳理下。
虽然网上关于Macho的文章介绍一大堆,但是我希望能够从Macho的构成,加载过程以及需要了解的相关背景角度去进行分析,每一个点都力图深入。也会在这篇文章最后打造一个类似class-dump的小型工具。
程序启动加载的过程当你点击一个icon启动应用程序的时候,系统在内部大致做了如下几件事:
内核(OS Kernel)创建一个进程,分配虚拟的进程空间等等,加载动态链接器。
通过动态链接器加载主二进制程序引用的库、绑定符号。
虽然简要概述很简单,但是有几个需要特别主要的地方:
二进制程序的格式是怎么样的?内核是如何加载它的?
内核是如何得知要使用哪种动态链接器的?
动态链接器和静态链接器的区别是啥?
程序在运行前究

我要回帖

更多关于 min要不要加s 的文章

 

随机推荐