iOS知识总结

263 阅读10分钟

为什么设计MetalClass

从Smalltalk重新认识面向对象

参考文章

面向对象三特征:封装、继承、多态。但其实,面向对象中也分流派,如C++这种来自Simula的设计思想的,更注重的是类的划分,因为方法调用是静态的。而如Objective-C这种借鉴Smalltalk的,更注重的是消息传递,是动态响应消息。

这两种思想最大的不同,我认为是自上而下和自下而上的思考方式。

  • 类的划分,要求类的设计者是以一个很高的层次去设计这个类,提取出类的特性和本质,进行类的构建。知道类型才可以去发送消息给对象。
  • 消息传递,要求的是类的设计者以消息为起点去构建类,也就是对外界的变化进行响应,而不关心自身的类型,设计接口。尝试理解消息,无法处理则进行特殊处理。

着重讲讲Smalltalk这种设计

而面向对象优点之一的复用,在这种设计里,更多在于复用解决方案,而不是单纯的类本身。这种思想就如设计组件一般,关心接口,关心组合而非类本身。其实之所以有MetaClass这种设计,我的理解并不是先有MetaClass,而是**在万物都是对象的Smalltalk里,向对象发送消息的基本解决方案是统一的,希望复用的。**而实例和类之间用的这一套通过isa指针指向的Class单例中存储方法列表和查询方法的解决方案的流程,是应该在类上复用的,而MetaClass就顺理成章出现罢了。

这样的设计是C++那种自上而下的设计方式,类方法也是类的一种特征描述。而Smalltalk的精髓正在于消息传递,复用消息传递才是根本目的,而MetaClass只不过是因此需要的一个工具罢了

从OC的消息机制分析元类存在的意义

OC的消息机制流程:

  • objc_msgSend用汇编写的
  1. 因为在C中不可能通过写一个函数来保留未知的参数并且跳转到一个任意的函数指针。
  2. 必须足够快
  • 消息的流程

OC消息查找&转发

  • lookUpImpOrForward的细节 - 慢速流程
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    //....
    // Optimistic cache lookup - 1. 查找缓存
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }
    // ....
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
    {
        // 2. 二分查找方法
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            //在慢速查找流程中,找到了就填充缓存
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists. 3. 查找父类的缓存&方法列表
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }

            // Superclass cache. 3.1 先从父类缓存开始找
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }

            // Superclass method list. 3.2 父类缓存找不到,再到父类慢速流程查找
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // No implementation found. Try method resolver once. 4. 动态方法决议

    if (resolver  &&  !triedResolver) {
         _class_resolveMethod(cls, sel, inst);
        //... 动态方法决议
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding. 5. 消息转发
    
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

lookUpImpOrForward 执行的过程可以看出,方法在查找的时候并没有区分是实例方法还是类方法,因为类方法存在元类中,对元类而言也是实例方法,所以类方法的查找流程是和实例方法的查找流程一样,只是在实例方法的时候cls是类,而类方法的时候是元类而已,对底层来说其实都是实例方法,这样就能避免查找方法的时候要进行一堆if/else的判断。

总结:

通过元类就可以让各类各司其职,实例对象就干存储属性值的事,类对象存储实例方法列表,元类对象存储类方法列表,完美的符合6大设计原则「单一原则、开闭原则、接口隔离、迪米特原则-最少知识法则、依赖反转、里氏替换」中的单一职责,而且忽略了对对象类型的判断和方法类型的判断可以大大的提升消息发送的效率,并且在不同种类的方法走的都是同一套流程,在之后的维护上也大大节约了成本。 总结 本文从OC的消息机制分析了元类存在的意义,元类的存在巧妙的简化了实例方法和类方法的调用流程,大大提升了消息发送的效率。

6大设计原则

单一

OC里元类的设计

开闭

里氏替换

父类、子类的关系,子类可以替换父类出现的地方

依赖反转

迪米法法则(最少知识原则)

接口隔离

ARC&MRC

使⽤函数objc_autoreleaseReturnValue来 替代autorelease、函数objc_retainAutoreleasedReturnValue来替代retain

NSString

  • 由initWithString和stringWithString创建的NSString对象,不管字符串的内容和长度怎么变化,该字符串对象始终是存储在data/bss的,引用计数为-1,没有引用计数这个概念;
  • 由initWithFormat和stringWithFormat创建的对象
    当字符串非汉子
        长度超过10个以后,该字符串在堆中,与正常的OC对象一样。
        字符串长度小于10个时,该字符串存储区域在五大区域之外,且随着字符串长度的变化,存储地址会有很大变化
    字符串内容是汉字,不管字符串的内容和长度怎么变化,该字符串都是在堆中,与正常OC对象一样。
    

weak 实现

我们基本都知道,weak修饰的对象是没有retain的,也就是不会操作引用计数,只是会存储到一个weak表中,所以才能够打破循环引用,那么weak是如何存入到weak表的呢,如何管理多个weak的关系呢?

SideTable

struct SideTable {
    spinlock_t slock;//自旋锁,防止多线程访问冲突
    RefcountMap refcnts;//对象引用计数map,(仅在未开启isa优化 或 在isa优化情况下isa_t的引用计数溢出时才会用到)。
    weak_table_t weak_table;//对象弱引用map
    ...
}

1. SideTables是一个全局的64个元素长度的hash数组,里面存储来SideTable。SideTables的hash键值就是一个对象obj的地址;
2. 一个obj对应了一个SideTable;
3. 一个SideTable,会对应多个obj。因为SideTable的数量只有64个,所以会有很多obj共用同一个SideTable;
4. SideTables多张散列表:每次访问的时候,都要对表进行加锁和解锁操作,如果放在一张表里面,安全性&性能的角度,设计多张表

SideTables

SideTabls 实质类型为模板类型StripedMap
// StripedMap<T> is a map of void* -> T, sized appropriately 
// for cache-friendly lock striping. 
// For example, this may be used as StripedMap<spinlock_t>
// or as StripedMap<SomeStruct> where SomeStruct stores a spin lock.
template<typename T>
class StripedMap {}

StripedMap 是一个以void *为hash key, T为vaule的hash

weak 对象存表 & 置 nil

关于weak_entry_t的详解

weak的存储:SideTable -> weak_table_t -> weak_entries -> weak_entry_t

struct weak_table_t {
    weak_entry_t *weak_entries;// 哈希表结构
    size_t    num_entries; // weak_entry_t个数
    uintptr_t mask;// 总容量-1,用来弄哈希值的 (n& mask)就是“除留取余法”(n%总容量)
    uintptr_t max_hash_displacement;// 碰撞次数
};

typedef objc_object ** weak_referrer_t;
struct weak_entry_t {
    DisguisedPtr<objc_object> referent;
    union {
        struct {
            weak_referrer_t *referrers; // 哈希表结构
            uintptr_t        out_of_line : 1; // 1是referrers存储,0是inline_referrers
            uintptr_t        num_refs : PTR_MINUS_1;// 已存referrer总数
            uintptr_t        mask; // 总容量-1,用来弄哈希值的 (n& mask)就是“除留取余法”(n%总容量
            uintptr_t        max_hash_displacement; // 碰撞次数
        };
        struct {
            //  如果weak变量少,用数组来存放,如果大于4个,就使用referrers
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    }
}

static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
    assert(referent);

    weak_entry_t *weak_entries = weak_table->weak_entries;

    if (!weak_entries) return nil;
    // 对象地址哈希,得到了对应的index
    size_t begin = hash_pointer(referent) & weak_table->mask;
    size_t index = begin;
    size_t hash_displacement = 0;
    // 判断是否找到referent对应的weak_entry_t
    while (weak_table->weak_entries[index].referent != referent) {
          // 如果发生碰撞,则index依次+1,再次查找
        index = (index+1) & weak_table->mask;
         
        if (index == begin) bad_weak_table(weak_table->weak_entries);// 异常,crash提示
        hash_displacement++;
        // 满足冲撞次数,直接返回nil
        if (hash_displacement > weak_table->max_hash_displacement) {
            return nil;
        }
    }
    
    // 返回取到的weak_entry_t
    return &weak_table->weak_entries[index];
}

weak = nil: dealloc -> objc_object::rootDealloc -> object_dispose -> objc_destructInstance -> weak_clear_no_lock

void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    objc_object *referent = (objc_object *)referent_id;
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    ...
    //将包含在记录中的所有附有__weak修饰符变量的地址,赋值为nil
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            ...
        }
    }
    //从表中删除记录
    weak_entry_remove(weak_table, entry);
}

weak指针的使用

NSObject *obj = [[NSObject alloc] init];
// weak的三种赋值情况
// (1)变量赋值
_weakObj = obj; // 编译为:objc_storeWeak(&_weakObj, obj);

//  (2) 直接初始化,strong对象赋值
__weak NSObject *obj1 = obj; // 编译为:objc_initWeak(&obj1, obj);

//  (3) 直接初始化,weak对象赋值
__weak NSObject *obj2 = _weakObj; // 编译为:objc_copyWeak(&obj2, & _weakObj);

// weak的访问情况,就是调用 objc_loadWeakRetained(id *location)
NSLog(@"=====%@",_weakObj);
// 编译为下面代码
/*
id temp = objc_loadWeakRetained(&weakObj);
NSLog(@"=====%@",temp);
objc_release(temp);
*/

1. weakA = weakB;
2. weakA = strongB;
3. __weak TFBook *weakBook = nil;
4. strongA = weakB;

1 & 2 & 3 都是调用objc_storeWeak(id *location, id newObj)
4 是id objc_loadWeakRetained(id *location),而objc_loadWeakRetained实际就是把weak对象retain了一下,属于另外的问题了。

设置:storeWeak

storeWeak
读读objc源码(二):weak类型指针的实现

读取:objc_loadWeakRetained(ARC)、objc_loadWeak(MRC)

相关面试题

  1. __weak对象是何时被释放的
在runloop准备进入休眠时和即将退出时被释放的
  1. __weak修饰的作用以及如何被自动置为nil
作用:解决循环引用
使用objc_storeWeak来将变量存在weak hash 表中,key是引用的对象的地址,value是__weak变量的地址
1. 执⾏dealloc
2. _objc_rootDealloc
3. object_dispose(⼀个object开头)
4. objc_destructInstance
5. objc_clear_deallocating
⽽,调⽤objc_clear_deallocating的动作如下:
    1. 从weak表中获取废弃对象的地址为键值的记录。
    2. 将包含在记录中的所有附有__weak修饰符变量的地址,赋值为nil
    3. 从weak表中删除记录
  1. WeakSelf 和 StrongSelf的区别,为什么在block外先Weak后Strong?
进⾏weak是为了防⽌循环引⽤,进⾏strong是因为block调用可能会存在延迟,未来防⽌weak对象被提前置为nil,所以要strong。

autoreleasepool

面试题

  1. autoreleasepool的底层实现原理及使⽤场景?

autoreleasepool 是没有单独的内存结构的,它是通过以 AutoreleasePoolPage 为结点的双向链表来实现的。 每⼀个线程的 autoreleasepool 其实就是⼀个指针的堆栈; 每⼀个指针代表⼀个需要 release 的对象或者 POOL_SENTINEL(哨兵对象,代表⼀个 autoreleasepool 的边 界);

单个 autoreleasepool 的运⾏过程可以简单地理解为 objc_autoreleasePoolPush()、[对象 autorelease] 和 objc_autoreleasePoolPop(void *) 三个过程。

使⽤场景: 如果你编写的程序不是基于 UI 框架的,⽐如说命令⾏⼯具; 如果你编写的循环中创建了⼤量的临时对象; 如果你创建了⼀个辅助线程。

Block

__block 修饰变量 block具有修改能力

// 问题 - __block 修饰变量 block具有修改能力
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy;  // patch stack to point to heap copy

__block修饰有3层拷贝

  1. block自身拷贝
  2. Block_byref *copy = (struct Block_byref )malloc(src->size); Block_byref_name_0 结构体 堆区内存空间的拷贝 struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
  3. (src2->byref_keep)(copy, src);在第二层里面的针对实际操作的变量name的对象 再进行一次copy

block为什么要⽤copy?为什么不⽤retain? 在栈中⽽不是在堆中

Runtime

为什么说OC是一门动态语言? 动态绑定:对象类型、调用方法均是在运行时确定的,使用消息机制

OC如何响应一条消息 objc_msgSend -> Cache查询 -> 方法列表查询 -> 父类 -> 消息转发

Objc_msgSend类似的⽅法 objc_msgSend_stret; objc_msgSend_fpret; objc_msgSendSuper

hook一个class的实例方法,class方法

method_exchangeImplementations(Method m1, Method m2)
Method class_getClassMethod(class, selector)
Method class_getInstanceMethod(class, selector)
具体步骤:
if(class_addMethod()) {
    class_replaceMethod
} else {
    method_exchangeImplementations
}

能否向编译后得到的类增加实例变量?能否向运行时创建的类添加实例变量?为什么?

不能向编译后得到的类增加实例变量 能向运行时创建的类添加实例变量 编译后的类已经注册在runtime中,类结构中的objc_ivar_list 实例变量的链表instance_size实例变量的内存大小已经确定,同时runtime中会调用class_setIvarLayout或class_setWeakIvarLayout来处理strong,weak引用。 运行时可以,调用class_addIvar函数,但是在objc_allocatedClassPair之后,objc_registerClassPair之前。

ios 基础知识