0%

前言

这篇文章既是对上篇《手Y开发规范化建设一:Commit Message 规范建设》在文章结尾抛出的问题的解答,也是对外分享已经在手Y业务团队落地实施5个月(自2021年8月初开始)了的《手Y业务工程规范》的建设经验。

业务背景

截止到目前,手Y业务组件数量已经达到30+。伴随着业务的进一步复杂化,团队中出现了不少有碍代码质量、影响效率的问题,比如:

  • 部分手Y业务组件随便引入体积超标图片资源,引起手Y和联运SDK体积劣化
  • 部分已做二进制化的手Y业务组件不正确设置代码文件的Target Membership,导致构建的二进制产物丢失头文件或者符号,阻塞了手Y的正常开发和构建
  • 部分手Y业务组件不规范使用API,导致手Y运行时异常
  • 手Y业务组件提交的 commit 日志格式各异

为进一步提升手Y的代码质量,提高团队的协作开发效率,我制定了《手Y业务工程规范》来引导团队成员规范开发。

Read more »

前言

我于2020年年中加入了手机YY iOS团队,那时发现团队成员在特性阶段所写的Commit Message比较随意;在集成阶段,则比较统一:

image.png

这里简要介绍一下手Y团队采用的Git分支管理模型和YY GitLab Server端对maint分支实施的Commit Message规范。

手Y团队采用的Git分支管理模型是一种基于主干的分支模型:master-maint-feature,其中:

  • master分支是主干分支,用于存放对外发布的版本,永久存在,属于长生命周期
  • maint分支是集成分支,从master分支拉出,用于集成特性和发布版本,存在于集成阶段(在此阶段,团队成员都往该分支提交和只提交修复代码),属于中生命周期(版本发布后,合并回master分支,这之后一般还会存在一段时间)
  • feature分支是特性分支,从master分支拉出,用于开发特性,存在于特性阶段(在此阶段,团队成员都往该分支提交特性代码、修复代码等),属于短生命周期(特性集成之日,合并回master分支,然后被删除)

YY GitLab Server端对maint分支实施的Commit Message规范是,要求提交格式如下:

1
[bug id <bugId>] <subject> review by [<reviewer>]

为了使得手Y工程的Commit Message规范变得完整(任意阶段都有规范)和完善,从而帮助团队更好地发展和更好地维护工程,我根据团队实际情况和需求,制定和落地了一套合适手Y的新的Commit Message规范。截止到目前,该规范已经运行快一年,给团队和工程带来了显著的收益:

  • 帮助团队成员建立更好的规范意识
  • 帮助团队新成员更好地理解Commit历史代码
  • 帮助团队成员更快地对当前Commit进行Code Review
  • 让工程变得更好维护
  • 其他等等

Commit Message规范在经过实践的验收后,作为一个负责的开发团队,我们觉得可以对外分享我们的这个新的Commit Message规范,以及分享我们如何去保障新Commit Message规范的实施。

Read more »

背景

在iOS工程接入CocoaPods做依赖管理后,开发者对Podfile.lock的管理主要有以下两种方案:

  1. 不把Podfile.lock纳入版本管理
  2. Podfile.lock纳入版本管理

这两种方案各有优劣和各有适用场景:

  1. 第1种方案不强制团队成员使用统一版本的CocoaPods,对团队成员较为友好,但是无法保证团队成员在本地安装的依赖是一致的;其适合个人或者前期对规范不作要求、规模很小的团队
  2. 第2种方案能保证团队成员在本地安装的依赖是一致的,但是却要强制团队成员使用统一版本的CocoaPods;其适合对规范有要求的团队

在我加入手Y团队之前,手Y团队选用的是第1种方案。基于第1种方案的选择,为了能保证团队成员在本地安装的依赖是一致的,手Y团队又做了以下的解决措施:

Podfile里的每个库都声明一个具名的固定版本号,如pod 'yyabtestsdk', '2.1.0-dev.2'pod 'yybaseapisdk', :git=>'https://xxx/yybaseapisdk-ios.git', :tag => '7.46.0-dev.8'

随着团队的变大(现在iOS业务端已有40+人),这种方案的弊端逐渐暴露:

  • 无法百分百确保编译运行阶段,团队成员的本地安装的依赖是一致的

    这个弊端对应的场景是:有人更新了Podfile,安装了非BreakingChanges的、新版本的依赖库,并进行了代码推送;其他人拉取代码后,若不手动执行一次Pod install/update,其本地安装的依赖是落后的,并且在编译运行阶段,由于代码兼容,若非出现严重bug,其不会发现其本地依赖需要更新。

  • 因本地依赖的版本不正确导致编译失败的时机延迟发生到了编译中后期

    这个弊端对应的场景是:有人更新了Podfile,安装了BreakingChanges的、新版本的依赖库,并进行了代码推送;其他人拉取代码后,若不手动执行一次Pod install/update,其本地安装的依赖是落后的,然后其进行编译时,由于代码不兼容,会发现编译失败了——但是这时候编译失败的时机常常是发生在编译中后期——在让开发者至少等待了十几分钟后才抛出编译失败的错误——这相当影响开发者的心情和工作效率。

    对于BreakingChanges的疑问可看下文的《Q&A》

为了解决这些弊端,有必要考虑重新把Podfile.lock纳入版本管理。那有没有一种方案,能同时获得上述第1种方案和第2种方案的收益呢?具体是,希望有一种方案能满足以下的需求:

  1. 保证团队成员本地安装的依赖是一致的
  2. 允许团队成员不使用统一版本的CocoaPods
  3. 把因本地依赖的版本不正确导致编译失败的时机从编译中后期提前到编译前,帮助提升开发效率
  4. 新方案对当前团队成员是零负担(不强制团队成员做不必要的事、不耗费团队成员不必要的注意力和精力)

为此,我制定了一个新的Podfile.lock管理方案,下面将会做具体的介绍。

Read more »

前言

UIImage和UIImageView

UIImage 和 UIImageView 的角色类似于 MVC 架构模式中的数据和视图,如下图所示。

image

UIImage 是 iOS 中处理图像的高级类。创建一个 UIImage 实例只会加载 Data Buffer,将图像显示到屏幕上才会触发解码,也就是 Data Buffer 解码为 Image Buffer。Image Buffer 也关联在 UIImage 上。

  • Data Buffer 是存储在内存中的原始数据,图像可以使用不同的格式保存,如 jpg、png。Data Buffer 的信息不能用来描述图像的像素信息。

  • Image Buffer 是图像在内存中的存在方式,其中每个元素描述了一个像素点。Image Buffer 的大小和图像的大小成正比。

参考资料:

UIImage解码

在iOS中,UIImage解码分为2种:

  • 隐式解码

    将图像显示到屏幕上会触发隐式解码。(必须同时满足图像被设置到 UIImageView 中、UIImageView 添加到视图,才会触发图像解码。)

    1
    2
    3
    UIImageView *imageView = [[UIImageView alloc] init];
    [self.view addSubview:imageView];
    [imageView setImage:image];
  • 手动解码

    使用iOS提供的图形框架(UIKit、Core Graphics、Image I/O等)提供的API进行手动解码

参考资料:

  1. 减少内存占用

    当实际显示的图片尺寸小于图片真实大小时,对图片进行向下采样,生成缩略图供UI层使用。

  2. 优化 CPU 使用

    建立后台线程,在CPU空闲阶段(监听Runloop状态,进入休眠状态时,主线程变空闲)对图片进行提前解码。

UIView的渲染

基于Core Animation的视图渲染流水线

image

  1. Commit Transaction

    • Layout(布局):Set up the views

      执行细节:调用layoutSubviews(若重写了);创建View和执行addSubview:;填充内容和数据查找;一般会造成CPU和 I/O瓶颈

      可优化手段:简化view的布局和层级;减少使用xib、storyboard等

    • Display(显示):Draw the views

      执行细节: 调用 drawRect:(若重写了);绘制字符串;一般会造成CPU和 内存瓶颈

      可优化手段:减少自定义绘制(drawRect:),或者在自定义绘制中减少字符串、圆角layer等图层绘制

    • Prepare(准备):Additional Core Animation work

      执行细节:图片解码;图片格式转换(转换GPU不支持的图片);一般会造成CPU和 内存瓶颈

      可优化手段:异步线程解码图片;对大图进行向下采样显示;使用GPU支持的图片

    • Commit(提交):Package up layers and send them to render server

      执行细节:递归处理Layer-Tree:打包Layer信息并通过IPC发送到Render Server进程;如果图层树太复杂会消耗很大,对性能有很大的影响

      可优化手段:简化图层树(如果图层树太复杂会消耗很大,对性能有很大的影响)

  2. Decode

  3. DrawCalls

  4. Render

  5. Display

Core Animation 性能优化

Core Animation执行动画时涉及的图层树类型有几种

image

  • model layer tree (or simply “layer tree”)

    模型层树的对象(modelLayer)用于存放动画的目标值(比如起始值和结束值)。

  • presentation tree

    表示层树的对象(presentationLayer)用于给上层访问动画过程中的实时值。

  • render tree

    渲染树的对象(renderLayer)用于存放动画的绘制数据,供图形系统绘制动画使用,对上层屏蔽。

    render tree 在系统的Render Server进程中,是真正处理动画的地方。而且线程的优先级也比我们主线程优先级高。所以有时候即使我们的App主线程busy,依然不会影响到手机屏幕的绘制工作

  • model layer tree其实就是我们口中常说的图层树(layer tree
  • 通过layer.modelLayer可以获得一个layer对象的model layer object;而事实上layer.modelLayer就是Layer对象本身,即layer == layer.modelLayer
  • 通过layer.presentationLayer可以获得一个layer对象的presentation layer object的副本

iOS 渲染框架

  • UIKit Framework

    • 正如Apple官方文档对UIKit Framework的介绍,它主要提供了:界面呈现能力、事件响应能力、驱动RunLoop运行和与系统内核通信的数据。简单来说就是:主要负责界面展示、事件响应以及是RunLoop的需求方。UIView当然是属于UIKit Framework。
  • QuartzCore Framework(CoreAnimation)

    • 正如Apple官方文档对Quarz Core Framework的介绍,它提供了图形处理和视频图像处理的能力。简单来说就是:QuartzCore Framework负责把图形图像最终显示到屏幕上,这里我觉得应该是特指CoreAnimation。不要从字面上去理解CoreAnimation的职责,因为它的核心工作不单是负责动画的创建和执行,它还负责把我们用代码构建的界面显示到屏幕上,实际上是CoreAnimation通过OpenGLES做的。(别急,虽然这里面的机理很复杂,但是在后面会提到)。CALayer是属于QuarzCore Framework下的CoreAnimation
  • CoreGraphics Framework

    • 如Apple官方文档对Core Graphics Framework的介绍。CoreGraphics Framework一个基于C库函数的高级绘画引擎,它提供了非常强大的轻量级2D渲染能力。可以使用CoreGraphics处理基于path的绘图工作(如,CGPath)、变形操作(如,CGAffineTransform)、颜色管理(如,CGColor)、离屏渲染(如,CGBitmapContextCreateImage)、渲染模式(patterns)、渐变(gradients)、阴影效果、图形数据管理、图形创建、蒙版以及PDF文档的创建、显示和解析。 千万别和QuartzCore混淆,CoreGraphics负责创建显示到屏幕上的数据模型,QuartzCore(CoreAnimation –> OpenGLES)负责把CoreGraphics创建的数据模型真正显示到屏幕上。 CG打头的类都是属于CoreGraphics Framework
    • 当开发者需要在 运行时创建图像 时,可以使用 Core Graphics 去绘制。与之相对的是 运行前创建图像,例如用 Photoshop 提前做好图片素材直接导入应用。相比之下,我们更需要 Core Graphics 去在运行时实时计算、绘制一系列图像帧来实现动画。
  • Core Image

    • Core Image 与 Core Graphics 恰恰相反,Core Graphics 用于在 运行时创建图像,而 Core Image 是用来处理 运行前创建的图像 的。Core Image 框架拥有一系列现成的图像过滤器,能对已存在的图像进行高效的处理。
    • 大部分情况下,Core Image 会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理。
  • OpenGL ES

    • OpenGL ES(OpenGL for Embedded Systems,简称 GLES),是 OpenGL 的子集。在前面的 图形渲染原理综述 一文中提到过 OpenGL 是一套第三方标准,函数的内部实现由对应的 GPU 厂商开发实现。
  • Metal

    • Metal 类似于 OpenGL ES,也是一套第三方标准,具体实现由苹果实现。大多数开发者都没有直接使用过 Metal,但其实所有开发者都在间接地使用 Metal。Core Animation、Core Image、SceneKit、SpriteKit 等等渲染框架都是构建于 Metal 之上的。
    • 当在真机上调试 OpenGL 程序时,控制台会打印出启用 Metal 的日志。根据这一点可以猜测,Apple 已经实现了一套机制将 OpenGL 命令无缝桥接到 Metal 上,由 Metal 担任真正于硬件交互的工作。

UIKit Framework、QuartzCore Framework(CoreAnimation)、CoreGraphics Framework 三者的关系

image

UIKit 建立在 Core Animation 基础之上,在 Core Animation 之下是 OpenGL ES 和 Core Graphics,分别对应 GPU 和 CPU。Core Animation 本身并不做渲染工作,而是将这些工作转交给 Graphics Hardware(GPU)处理。

前言

当App启动后,用户第一次点击App内的一个视图,这时候将会产生一个点击事件。

下面将会讲述这点击事件背后的相关知识点,比如:

  • 点击事件如何从硬件层传递到App
  • App如何查找到最佳响应者(hit-tested view)
  • App查找到最佳响应者(hit-tested view)后,如何把点击事件传递给最佳响应者(hit-tested view)
  • 最佳响应者(hit-tested view)作为第一响应者(first responder),如何对事件进行响应和传递?
  • 异常场景的冲突解决
    • 手势与点击冲突
    • 多个手势冲突
    • 让多个UIView响应同一个点击事件

iOS设备事件的类型

  • Touch Events(触摸事件)
  • Motion Events(运动事件,比如重力感应和摇一摇等)
  • Remote Events(远程事件,比如用耳机上得按键来控制手机)

可以处理触摸事件的类有哪些

  • UIResponder的子类(UIApplication,UIWindow,UIViewController和UIView)
  • UIGestureRecognizer的子类
  • UIControl的子类(UIButton,UISegmentedControl,UISwitch)
  • UIResponder是iOS中用于处理用户事件的API,可以处理触摸事件、按压事件(3D touch)、远程控制事件、硬件运动事件。 可以通过touchesBegan、pressesBegan、motionBegan、remoteControlReceivedWithEvent等方法,获取到对应的回调消息。UIResponder不只用来接收事件,还可以处理和传递对应的事件,如果当前响应者不能处理,则转发给其他合适的响应者处理。
  • UIConotrol是UIView(UIResponder)的子类,相比于UIResponder,其增加了target-action的特别设计。

UIConotrol、UIGestureRecognizer、 UIResponder的触摸事件响应优先级排名

  • UIResponder优先级最低
  • UIConotrol的优先级大于其父视图上UIGestureRecognizer,小于添加在其自身上的UIGestureRecognizer

触摸事件如何从硬件层转发到App

  1. 触摸发生时,系统内核生成触摸事件,然后交由给IOKit进程

  2. IOKit进程把触摸事件封装成IOHIDEvent对象,通过IPC(mach port)发送给SpringBoard进程。

  3. SpringBoard进程通过IPC(mach port)发送IOHIDEvent对象给当前App进程。

    需要注意的是,若当前前台无App运行,则SpringBoard进程会把该IOHIDEvent对象发送给桌面系统处理。

  4. App进程的mach port收到IOHIDEvent后,会唤醒主线程的runloop,触发source1回调;source1回调又触发一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,发送给App进程的UIApplication对象处理。

UIApplication收到触摸事件(UIEvent)后的处理流程

  1. UIApplication对象通过 hit-tesing 机制查找最佳响应视图(hit-tested view)。

  2. 确定触摸事件的第一响应者(first-responder)——一般情况下,最佳响应视图(hit-tested view)将会是触摸事件的第一响应者(first-responder)。

    具体地,根据最佳响应视图(hit-tested view)的类型和既定的UIConotrol、UIGestureRecognizer、 UIResponder的触摸事件响应优先级排名 ,确定第一响应者(first-responder):

    • hit-tested view是UIControl类型,hit-tested view自身没有UIGestureRecognizer,则第一响应者是hit-tested view

    • hit-tested view是UIControl类型,hit-tested view自身有UIGestureRecognizer,则第一响应者是hit-tested view上的UIGestureRecognizer,第二响应者是hit-tested view

    • hit-tested view是UIResponder类型,hit-tested view自身和其后面的响应者视图上都没有UIGestureRecognizer,则第一响应者是hit-tested view

    • hit-tested view是UIResponder类型,hit-tested view自身或者和其后面的响应者视图上有UIGestureRecognizer,则第一响应者是hit-tested view上或者和其后面的响应者视图上的UIGestureRecognizer,第二响应者是hit-tested view

      在进行hit-tesing过程中,UIEvent会收集这过程发现的UIGestureRecognizer到它的_allTouchesMutable-_gestureRecognizers

      image

  3. UIApplication对象通过sendEvent:把触摸事件(UIEvent)发送给第一响应者(first-responder)或者最佳响应视图(hit-tested view);最佳响应视图(hit-tested view)收到事件后会沿着其响应链决策处理该触摸事件(UIEvent)。

    具体地,UIApplication对象将事件通过sendEvent: 传递给事件所属的UIWindow对象,UIWindow对象根据第一响应者的类型,采用不同的逻辑使用 sendEvent:把触摸事件分发给发送给第一响应者(first-responder)或者最佳响应视图(hit-tested view):

    • 若第一响应者(first-responder)是最佳响应视图(hit-tested view),则事件直接发送给最佳响应视图(hit-tested view)

    • 若第一响应者(first-responder)UIGestureRecognizer,则最佳响应视图(hit-tested view)是第二响应者,然后事件会先发给UIGestureRecognizer,再发给最佳响应视图(hit-tested view);UIGestureRecognizer收到事件后,会开始对事件进行识别,识别过程中,事件会继续分发给最佳响应视图(hit-tested view);当UIGestureRecognizer识别出事件为其对应的手势后会触发target-action调用,并且事件将停止分发给最佳响应视图(hit-tested view)

      手势识别过程中或者识别成功后,可分别通过设置UIGestureRecognizer的delaysTouchesBegan属性和cancelsTouchesInView属性来控制是否分发事件给最佳响应视图(hit-tested view)

      image

hit-tesing 机制是什么

image

  1. 当手指接触屏幕,UIApplication接收到手指的触摸事件之后,就会去调用UIWindow的hitTest: withEvent:方法

    如果存在多个UIWindow,则从KeyWindow开始遍历UIWindow数组

  2. hitTest: withEvent:方法中会调用pointInside: withEvent:去判断当前点击的point是否属于UIWindow范围内,如果是,就会以倒序的方式遍历它的子视图,即越后添加的视图,越先遍历

  3. 子视图也调用自身的hitTest: withEvent:方法,来查找最佳响应视图,若某个子视图就是最佳响应视图,则返回该子视图;若最终没有任何一个子视图是最佳响应视图,就返回nil

hitTest: withEvent:方法的内部逻辑

  1. 判断当前视图是否满足响应条件:

    • 可交互: userInteractionEnabled = YES
    • 非隐藏:hidden = NO
    • 透明度大于0:alpha > 0.01
    • 触摸点的位置在视图范围内:通过pointInside: withEvent:方法判断触摸点是否在视图的坐标范围内
  2. 若满足响应条件,则倒序遍历其子视图查找最佳响应视图,若存在更何时的子视图,就返回子视图,否则返回自身

具体代码逻辑如下:

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
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//3种状态无法响应事件
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}

//触摸点若不在当前视图上则无法响应事件
if ([self pointInside:point withEvent:event]) {
//从后往前遍历子视图数组
for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
// 坐标系的转换,把触摸点在当前视图上坐标转换为在子视图上的坐标
CGPoint convertedPoint = [subView convertPoint:point fromView:self];
//询问子视图层级中的最佳响应视图
UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event];
if (hitTestView) {
//如果子视图中有更合适的就返回
return hitTestView;
}
}
//没有在子视图中找到更合适的响应视图,那么自身就是最合适的
return self;
}

return nil;
}

响应链是什么

响应链是一个以第一响应者对象为起点,以AppDelegate对象为终点,以响应者对象(UIResponder对象)的nextResponder属性作为连接上一个和下一个响应者对象工具的,一个单向的,响应者对象链条。

每一个响应者对象(UIResponder对象)都有一个nextResponder属性,用于获取响应链中当前对象的下一个响应者。因此,一旦事件的最佳响应者确定了,这个事件所处的响应链就确定了。

响应者对象的nextResponder是什么

不同响应者对象(UIViewUIViewControllerUIWindowUIApplication),其默认的nextResponder实现如下:

  • UIView

    若视图是控制器的根视图,则其nextResponder为控制器对象;否则,其nextResponder为父视图。

  • UIViewController

    若控制器的视图是window的根视图,则其nextResponder为窗口对象;若控制器是从别的控制器present出来的,则其nextResponder为presenting view controller。

  • UIWindow

    nextResponder为UIApplication对象。

  • UIApplication

    若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate。

官网对于响应链的示例展示:

image

UIResponder类型响应者如何处理触摸事件?

UIResponder类型响应者对于接收到的事件有3种操作:

  • 不拦截,默认操作:事件会自动沿着默认的响应链往下传递
  • 拦截,不再往下分发事件:重写 touchesBegan:withEvent: 进行事件处理,不调用父类的 touchesBegan:withEvent:
  • 拦截,继续往下分发事件:重写 touchesBegan:withEvent: 进行事件处理,同时调用父类的 touchesBegan:withEvent: 将事件往下传递

UIControl类型响应者如何处理触摸事件?

UIControl是UIResponder的子类,其内部重写了touch相关的方法,当其收到触摸事件后,会对事件进行识别,若符合响应条件,会触发target-action进行响应:

当UIControl监听到需要处理的交互事件时,会调用sendAction:to:forEvent: 将target、action以及event对象发送给全局应用,Application对象再通过 sendAction:to:from:forEvent: 向target发送action。

注意:

UIControl对象执行addTarget:action:forControlEvents: 时,若target传空,那么当事件发生时,UIApplication对象从hited-view开始沿着响应链从上往下寻找能响应action的对象。

image

如何手动控制触摸事件的流向

一般有2个方案:

  1. 重写(Override)hit-testing的逻辑,具体地就是重写UIResponder子类的hitTest: withEvent:方法或者pointInside: withEvent:方法,控制返回的最佳响应视图
  2. 重写(Override)触摸事件在响应链的传递逻辑,控制触摸事件的分发,具体地就是重写UIResponder子类的touchesBegan:withEvent:方法,根据业务需求增加响应逻辑和调用父类的touchesBegan:withEvent:方法,以控制是否把事件传递给nextResponder

题目变形:如何让多个视图响应触摸事件

如何扩大视图的点击范围

重载视图的hitTest: withEvent:方法或者pointInside: withEvent:方法

如何让多个手势识别器并存

重载手势识别器的UIGestureRecognizerDelegate的gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法

题目变形:解决右滑返回手势和UIScrollView中的手势冲突

参考资料

前言

当用户第一次点击iPhone手机屏幕上的一个App 图标时,该APP将会被iOS加载启动起来。

下面将会讲述这加载启动背后的相关知识点,比如:

  • App的启动类型
  • App冷启动的完整过程
  • App的冷启动优化手段
  • 可执行文件的介绍

image

前言

本文主要是对计算机科学领域中的函数式编程(英语:Functional Programming)范式的理论、特性及特性的实现方案进行探索。

Read more »

前言

Future模型和Promise模型是并发编程领域中的两种相近的异步编程模型,Future和Promise则是并发编程语言分别对Future模型和Promise模型进行实现后的产物。

Future和Promise之间,既有相同之处,也有不同之处。

Read more »

前言

这是一道从此篇博客《神经病院 Objective-C Runtime 入院第一天—— isa 和 Class》看到的题目:

问:下面代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
@implementation Son : Father
- (id)init
{
self = [super init];
if (self)
{
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end

答案是: 输出的都是Son

对此,博客作者给出的理由是:

self和super的区别:

self是类的一个隐藏参数,每个方法的实现的第一个参数即为self。

super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的方法。

在调用[super class]的时候,runtime会去调用objc_msgSendSuper方法,而不是objc_msgSend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )


/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;

/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Class class;
#else
__unsafe_unretained Class super_class;
#endif
/* super_class is the first class to search */
};

在objc_msgSendSuper方法中,第一个参数是一个objc_super的结构体,这个结构体里面有两个变量,一个是接收消息的receiver,一个是
当前类的父类super_class。

入院考试第一题错误的原因就在这里,误认为[super class]是调用的[super_class class]。

objc_msgSendSuper的工作原理应该是这样的:
从objc_super结构体指向的superClass父类的方法列表开始查找selector,找到后以objc->receiver去调用父类的这个selector。注意,最后的调用者是objc->receiver,而不是super_class!

那么objc_msgSendSuper最后就转变成

1
2
3
4
5
6
7
8
9
10
11
// 注意这里是从父类开始msgSend,而不是从本类开始,谢谢@Josscii 和他同事共同指点出此处描述的不妥。
objc_msgSend(objc_super->receiver, @selector(class))

/// Specifies an instance of a class. 这是类的一个实例
__unsafe_unretained id receiver;


// 由于是实例调用,所以是减号方法
- (Class)class {
return object_getClass(self);
}

由于找到了父类NSObject里面的class方法的IMP,又因为传入的入参objc_super->receiver = self。self就是son,调用class,所以父类的方法class执行IMP之后,输出还是son,最后输出两个都一样,都是输出son。

其实,上述的解答只答对了一大半,还有一小半没正确。没正确的部分主要在于对objc_msgSendobjc_msgSendSuper的工作原理理解错误,以及对方法实现的函数原型的忽略。

下面将会对此进行重新解答。

Read more »