主要记录一些题目所关联的知识点, 详解
iOS 题目详解 部分一
iOS 题目详解 部分二
iOS 题目详解 部分三
iOS 题目简述 部分一
题目1 关于打印self 和 super
三个类的继承关系如下NSObject->Dog->Doggy
;下列代码打印结果是什么?
#import "Doggy.h"
@implementation Doggy
- (instancetype)init {
self = [super init];
if (self) {
NSLog(@"%@", [self class]);
NSLog(@"%@", [super class]);
}
return self;
}
@end
打印结果是:
2020-07-06 11:43:28.341267+0800 Topic[8721:101914] Doggy
2020-07-06 11:43:28.341389+0800 Topic[8721:101914] Doggy
对于 [self class]
没什么疑问就是当前类, 所以打印Doggy
, 但是为什么[super class]
也是Doggy
而不是Dog
呢, 下面从源码角度验证此问题;
我们都知道 iOS
中调用方法最后都会转变为objc_msgSend(receiver, sel)
的方式, 而且从objc4-781的 runtime版本 中可以看到 NSObject.mm
的源码有如下, 有此可以得知, class
方法是得到当前类的类对象;
+ (Class)class {
///类方法直接返回本身
return self;
}
- (Class)class {
///通过实例对象获取类对象
return object_getClass(self);
}
因此可以得知不论是[self class]
还是[super class]
最终返回的是调用者 revceiver
的类对象;
通过指令将文件转化为C++
文件;得到的代码如下:
static instancetype _I_Doggy_init(Doggy * self, SEL _cmd) {
self = ((Doggy *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Doggy"))}, sel_registerName("init"));
if (self) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8m_1nrsxxrn5ln9zflyd82pvypc0000gn_T_Doggy_d4a898_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8m_1nrsxxrn5ln9zflyd82pvypc0000gn_T_Doggy_d4a898_mi_1, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Doggy"))}, sel_registerName("class")));
}
return self;
}
将源码简化下
static instancetype _I_Doggy_init(Doggy * self, SEL _cmd) {
self = objc_msgSendSuper)({(id)self, [[Doggy class] superClass]}, sel);
if (self) {
NSLog(objc_msgSend)(self, sel));
NSLog(objc_msgSendSuper)({self, [[Doggy class] superClass]}, sel);
}
return self;
}
因此我们只需要弄清楚objc_msgSendSuper()
的内部逻辑即可, 我们从源码objc-msg-arm.s
中可以得知此方法的定义如下, 通过汇编实现, 但是我们不需要知道具体逻辑,通过注释即可找到我们所需要的;objc_msgSendSuper()
的入参也是两个参数, 第一个参数是一个结构体objc_super
; 结构体中两个变量第一个是消息接收者, 第二个是接收者的父类类对象; 至此我们可以得知为什么调用[super class]也会得到当前类的的类对象,因为它的消息接收者仍然是当前类的实例对象;
/********************************************************************
* id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
*
* struct objc_super {
* 消息接收者
* id receiver;
* 从那个类开始搜索方法
* Class cls; // the class to search
* }
********************************************************************/
ENTRY _objc_msgSendSuper
ldr r9, [r0, #CLASS] // r9 = struct super->class
CacheLookup NORMAL, _objc_msgSendSuper
// cache hit, IMP in r12, eq already set for nonstret forwarding
ldr r0, [r0, #RECEIVER] // load real receiver
bx r12 // call imp
CacheLookup2 NORMAL, _objc_msgSendSuper
// cache miss
ldr r9, [r0, #CLASS] // r9 = struct super->class
ldr r0, [r0, #RECEIVER] // load real receiver
b __objc_msgSend_uncached
END_ENTRY _objc_msgSendSuper
通过
clang
语句将OC
文件转化为C++
文件, 并不是100%
实际底层执行逻辑, 只是大概的逻辑, 就如这里, 其实真正调用的方法是objc_msgSendSuper2()
这个方法, 不过不论哪个方法对我们的分析结构影响不大;
1.1 题目1衍生问题, 分析调用分类方法为什么先调用[super sel]
;
分析如下子类调用父类方法为什么先写[super sel]
;
///父类方法的实现
- (void)eatSomething {
NSLog(@"Dog Like Bones");
}
///子类的方法实现
- (void)eatSomething {
[super eatSomething];
NSLog(@"Doggy like Bones Too");
}
子类调用父类方法先调用[super sel]
通过上面的底层分析我们可以得知这样写的调用流程为, 通过结构体objc_super
我们可以得知实际的调用者是当前类, 但是查找方法的过程是从父类开始查找的, 因从当前类开始查找会造成死循环;方法调用顺序, 具体可以通过objc_msgSend()
的流程得知
struct objc_super {
id receiver;
Class cls; // the class to search
}
题目2 :isKindOfClass 和 isMemberOfClass 的区别;
如下代码的打印结果
Doggy *doggy = [[Doggy alloc] init];
Dog *dog = [[Dog alloc] init];
BOOL case1 = [doggy isKindOfClass:[Dog class]];
BOOL case2 = [doggy isMemberOfClass:[Dog class]];
BOOL case3 = [dog isKindOfClass:[NSObject class]];
BOOL case4 = [dog isMemberOfClass:[NSObject class]];
BOOL case5 = [[Doggy class] isMemberOfClass:object_getClass([Dog class])];
BOOL case6 = [[Doggy class] isKindOfClass:object_getClass([Dog class])];
打印结果为
case1 = 1, case2 = 0, case3 = 1, case4 = 0, case5 = 0, case6 = 1
下面通过源码分析为什么是这个结果, 通过NSObject.mm
中我们可以得知isMemberOfClass
和isKindOfClass
的方法实现为
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
首先得知这俩方法都有实例方法和类方法;分别用来判断类对象
和元类对象
相等或者包含逻辑;
-
实例方法: 是判断类对象是否相等或是其子类;
- (BOOL)isMemberOfClass:(Class)cls
:判断[当前实例对象的类对象]是否等于[类对象cls
]; (判断相等
)
- (BOOL)isKindOfClass:(Class)cls
: 判断[当前实例对象的类对象]是否属于 [类对象cls
] 的子类或者等于 [类型cls
];(判断相等或包含
) -
类方法: 是判断元类对象是否相等或是其子类;
+ (BOOL)isMemberOfClass:(Class)cls
: 判断[当前类对象指向的元类对象]是否等于 [元类对象cls
]; (判断相等
)
+ (BOOL)isKindOfClass:(Class)cls
: 判断[当前类对象指向元类对象] 是否属于 [元类对象 cls子类]或者等于[元类对象cls
], 注意这里有个隐藏点, 基类的元类对象的 superclass指向基类;(判断相等或包含
)
得知方法的底层实现后即可得知为什么打印结果是那样;
题目3 以下代码能否执行, 如果能执行, 结果是什么;
如下代码
///自定义的Model.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Model : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *age;
- (void)printName;
- (void)printAge;
+ (void)printSuperClass;
@end
NS_ASSUME_NONNULL_END
///Model.m文件
#import "Model.h"
@implementation Model
- (void)printName {
NSLog(@"name is : %@", self.name);
}
- (void)printAge {
NSLog(@"age is : %@", self.age);
}
+ (void)printSuperClass {
NSLog(@"superclass is : %@", [self superclass]);
}
@end
以下代码能不能执行, 如果能执行, 结果是什么?
NSString *testAge = @"TheAge";
NSString *testName = @"TheName";
id cls = [Model class];
void *clsPoint = &cls;
[(__bridge id)clsPoint printAge];
首先说下答案, 是可以编译, 可以执行, 而且不会报错; 打印结果是
2020-07-07 22:37:37.526402+0800 Topic[7827:1339842] age is : TheAge
此问题牵扯知识点比较多, 一点一点梳理;
- 3.1 首先是一个结构体的的首个变量的地址就是结构的地址, 并且通过内存地址的加减偏移获得其他的变量地址, 关于验证过程可以看结尾的补充1部分;
- 3.2 实例对象和类对象的底层结构都为结构体, 并且首个元素是
isa
指针, 因此实例对象的地址就是isa
的地址, 因为在64位架构之前isa
直接就是指向的类对象, 64位架构开始isa&ISAac_MASK
可以得到类对象, 因此我们可以简单的只要拿到isa
就可以拿到类对象的地址, 进而可以查找到整个类的实例方法, 因为实例方法存储在类对象中,可以看这篇文章; - 3.3 方法的调用最后都会转化为
objc_msgSend(receiver, sel)
的方式, 以实例对象为例, 则实例对象receiver
的首地址就是isa
, 通过首地址可以找到对应的类对象, 进而查找调用方法;因此我们可以推测只要是这种指向结构的均可以查找并调用方法; - 3.4 iOS 中采用小端模式, 验证过程可以看结尾的补充2部分
通过这个图我们可以看出当实例方法中调用_age
的变量时, 是实例对象地址(结构体, 向下寻找)加上16个字节就是_name
的地址, 所以通过clsPoint
查找到方法并调用时内存地址(向上寻找)加16个字节得到的是 testAge
变量的值; 至此, 方法调用结束, 注意如果地址(向上寻找)加上16个字节找不到合适的变量, 则会崩溃;
题目4 日常开发中哪些地方用到 runtime?
OC
是动态 语言, 它的动态性就是由runtime
来支撑的;平时编写的 OC
代码基本上底层都是转化为了runtime
来调用的;
具体运用:
- 利用关联对象技术来跟分类添加属性;
- 遍历类的所有成员变量(例如之前可以直接修改
UITextField
占位文字颜色背景色之类的操作); - 方法交换,替换系统方法的实现;
- 利用消息转发处理找不到方法的异常(动态添加方法);
题目5 如何通过runtime替换系统的 UIButton 的点击事件;
例如我们需要打印出来每个 UIButton
的点击事件时的信息, 可以为其添加一个分类将点击后调用的方法替换掉;
直接为 UIControl
添加一个 Category
将原本方法替换掉, 一旦执行了方法替换, 则方法查找, 缓存会被清空;
@implementation UIControl (Extention)
+ (void)load {
Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method method2 = class_getInstanceMethod(self, @selector(x_sendAction:to:forEvent:));
method_exchangeImplementations(method1, method2);
}
- (void)x_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
///如果是 UIButton则处理下
if ([self isKindOfClass:[UIButton class]]) {
NSLog(@"UIButton 点击着的信息action: %@__target: %@__event: %@", NSStringFromSelector(action), target, event);
/*
如果原本的方法中仍然想让执行, 则此处要这样调用一下, 为什么这样不会造成死循环
因为方法的实现(IMP)已经被替换了;源码中可以得知这一点;
*/
[self x_sendAction:action to:target forEvent:event];
}
}
@end
===>
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return;
mutex_locker_t lock(runtimeLock);
///将方法的 IMP 替换
IMP m1_imp = m1->imp;
m1->imp = m2->imp;
m2->imp = m1_imp;
///将方法的缓存清空
flushCaches(nil);
adjustCustomFlagsForMethodChange(nil, m1);
adjustCustomFlagsForMethodChange(nil, m2);
}
题目6 关于 Tagged Pointer
有如下两段代码, 请问分别能不能执行, 执行结果如何
情况1:
for (int i = 0; i < 1000; i ++) {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0 );
dispatch_async(queue, ^{
NSString *name = [NSString stringWithFormat:@"abcdefghhijklmnopqrst"];
// NSLog(@"name = %p", name);
self.name = name;
});
}
情况2
for (int i = 0; i < 10000; i ++) {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0 );
dispatch_async(queue, ^{
NSString *name = [NSString stringWithFormat:@"abc"];
// NSLog(@"name = %p", name);
self.name = name;
});
}
运行结果是, 情况1会崩溃,情况2能正常运行;
首先我们知道self.name = name;
的本质是调用其setter
方法, 而ARC
下也是有内存管理这个概念, 只不过系统帮忙做了这个工作, 所以不论是ARC
还是MRC
最后的setter
方法都会执行如下
- (void)setName:(NSString *)name {
if (_name != name)
[_name release];
_name = [name retain];
}
情况1为什么会崩溃, 因为NSString *name = [NSString stringWithFormat:@"abcdefghhijklmnopqrst"];
执行多少次就在堆区创建了多少个新对象, 通过打印即可确定; 所以我们可以知道if (_name != name)
会一直成立, 这样的话[_name release];
和 _name = [name retain];
也会每次都执行, 由于是多线程, 同时修改_name
崩溃风险;
解决方案1, 使用原子操作:
@property (nonatomic, strong) NSString *name;
改为@property (natomic, strong) NSString *name;
解决方案2: 为 setter
的赋值过程加锁;
为什么情况2可以正常运行, 那是因为在64架构以后使用了Tagged Pointer
技术, 针对小的NSNumber
, NSString
做了优化, 将值直接存在指针内, 所以不论多少次NSString *name = [NSString stringWithFormat:@"abc"];
在name
中存储的始终是一个地址(abc
值则直接封装在这个地址中), 通过打印即可验证;
因为始终是同一个地址, 所以判断条件if (_name != name)
只会在第一次成立, 后续就不再成立, 即使再多线程也没风险;
题目7 OC的内存管理
- 在
iOS
中使用引用计数
的概念来管理内存; - 新创建的对象引用计数为1, 引用计数为0时对象会被销毁, 释放其所占内存;
- 调用
retain
会使引用计数加1, 调用release
会使引用计数减1; - 调用
alloc
,new
,copy
,mutablecopy
创建新对象, 引用计数变为1; 调用release
,autorelease
释放对象;
7.1 引用计数存储在哪里?
答案: 引用计数存放在 isa
指针中, 如果不够存放则存放在一个散列表SideTable
中;
通过在runtime
部分学习isa
指针的结构可以得知这个结论;
下面通过源码来验证, 使用runtime
的版本为objc4-781
版本;
找到NSObject.mm
的实现, 找到retainCount
, 可以找到SideTable
的结构为
struct SideTable {
spinlock_t slock;
///存放的是引用计数reference counts;
RefcountMap refcnts;
///弱指针引用表, weak 修饰的对象, 释放时使用这个表;
weak_table_t weak_table;
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
...
}
我们知道查看引用计数使用的是retainCount
, 所以可以得到
///引用计数入口
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}
===>
_objc_rootRetainCount(id obj) {
ASSERT(obj);
return obj->rootRetainCount();
}
===>
/*
这里需要注意, 你会找到两个rootRetainCount()方法, 需要注意的是一个是
区别一个支持NONPOINTER_ISA, 一个是不支持NONPOINTER_ISA;
现在的64位架构都是使用SUPPORT_NONPOINTER_ISA, 具体验证过程可以点击#if SUPPORT_NONPOINTER_ISA宏去查看,
不再贴出验证过程;
*/
#if SUPPORT_NONPOINTER_ISA
...
inline uintptr_t
objc_object::rootRetainCount()
{
///如果是TaggedPointer 直接返回, 因为这是一个指针, 不是对象, 不需要考虑引用计数;
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
///获取 isa 指针的内容
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
///如果使用了nonpointer, 64架构值是1
if (bits.nonpointer) {
/*
rc 就是1加上存在 isa 中的extra_rc;
跟之前的 runtime 中讲解extra_rc的对应上. extra_rc存的值是 rc 的值减1;
*/
uintptr_t rc = 1 + bits.extra_rc;
/// 跟之前的 runtime 中讲解has_sidetable_rc对应上, 如果是1, 则使用散列表存储引用计数;
if (bits.has_sidetable_rc) {
///rc 的值为1加上散列表中 rc 的值
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
// SUPPORT_NONPOINTER_ISA
#else
// not SUPPORT_NONPOINTER_ISA
....
#endif
===>
size_t
objc_object::sidetable_getExtraRC_nolock()
{
ASSERT(isa.nonpointer);
///散列表SideTable, 第二个变量存储的是引用计数
SideTable& table = SideTables()[this];
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) return 0;
else return it->second >> SIDE_TABLE_RC_SHIFT;
}
至此可以验证引用计数的存储位置;
7.2 weak 指针的原理是什么?
跟引用计数一样, weak
指针会被存放在散列表中; 在对象执行dealloc
的时候将这个散列表遍历置为nil
;
通过源码查看流程, 入口为NSObject
的dealloc
函数;
- (void)dealloc {
_objc_rootDealloc(self);
}
===>
void _objc_rootDealloc(id obj){
ASSERT(obj);
obj->rootDealloc();
}
===>
inline void
objc_object::rootDealloc()
{
///判断是否是 TaggedPointer 如果是则不考虑内存管理
if (isTaggedPointer()) return; // fixme necessary?
/*
判断是否有弱引用, 关联对象, C++析构函数, 是否散列表存储
如果没有这些则会释放的更快
*/
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
///有弱引用, 关联对象, C++析构函数, 散列表存储之类
object_dispose((id)this);
}
}
===>
id
object_dispose(id obj) {
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
===>
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
///处理需要 dealloc 的对象
obj->clearDeallocating();
}
return obj;
}
===>
inline void
objc_object::clearDeallocating()
{
sidetable_clearDeallocating();
}
===>
void
objc_object::sidetable_clearDeallocating()
{
///通过当前为 key 取出散列表
SideTable& table = SideTables()[this];
// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
///通过&SIDE_TABLE_WEAKLY_REFERENCED 获取弱引用散列表 然后清理
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
///清理弱引用指针, 通过弱引用散列表
weak_clear_no_lock(&table.weak_table, (id)this);
}
table.refcnts.erase(it);
}
table.unlock();
}
至此可以确定weak
的原理;
补充和验证部分
补充1
结构体的首个变量的地址就是结构体的地址, 并且可以通过通过内存地址的加减获得其他变量的地址;
例如有如下结构体:
struct Person {
int age;
int height;
};
struct Man {
struct Person person;
int weight;
};
则下述的操作进和判断都是合理的
//**验证代码如下**//
struct Man man = {
{30, 180},
100
};
void *ageAddress = &(man.person.age);
///第一个元素的的地址就是结构体的地址 地址: 0x00007ffee44e4b80
void *manAddress = &(man);
///地址: 0x00007ffee44e4b80
void *firstParaAddress = &(man.person);
///拿到 man 结构体中第一个元素的地址+8就是 man.weight 的地址; firstParaAddress + 8 = 0x00007ffee44e4b88;
void *weightAddress = firstParaAddress + 8;
NSLog(@"结构体中第一个元素地址: %p",ageAddress );
NSLog(@"结构体中第一个元素地址: %p",manAddress );
NSLog(@"结构体中地址: %p",manAddress );
NSLog(@"首地址+8 就是weight的地址: %p == %p", &(man.weight), weightAddress);
//**打印结果如下**//
2020-07-07 22:37:37.526033+0800 Topic[7827:1339842] 结构体中第一个元素地址: 0x16f73f840
2020-07-07 22:37:37.526191+0800 Topic[7827:1339842] 结构体中第一个元素地址: 0x16f73f840
2020-07-07 22:37:37.526240+0800 Topic[7827:1339842] 结构体中地址: 0x16f73f840
2020-07-07 22:37:37.526290+0800 Topic[7827:1339842] 首地址+8 就是weight的地址: 0x16f73f848 == 0x16f73f848
补充2
iOS 采用小段模式,存储在栈区的变量的内存地址是递减的;
///&str1 = 0x00007ffee71a6ed8
NSString *str1 = @"abc";
///&str2 = 0x00007ffee71a6ed0
NSString *str2 = @"111";
///&str3 = 0x00007ffee71a6ec8
NSString *str3 = @"123asdf";
///&num1 = 0x00007ffee71a6ec4
int num1 = 1;
///&num3 = 0x00007ffee71a6ec0
int num2 = 2;
///num3Address = 0x00007ffee71a6ebc;
int num3 = 3;
void *num3Address = &num3;
通过上面我们可以看到在这个方法中开辟的变量在栈中的地址是递减存在的; 每次是递减8个字节或者4个字节;
参考文章和下载链接
Apple 一些源码的下载地址
大小端模式
iOS 判断大小端字节序
方法的查找顺序
LP64 结构数据占据多少位
LP64什么意思
isa 的结构
iOS Tagged Pointer技术