• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

gozelus/iOSReview: 常见iOS面试中考察的知识点整理

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称:

gozelus/iOSReview

开源软件地址:

https://github.com/gozelus/iOSReview

开源编程语言:


开源软件介绍:

严禁转载!

一.objc对象内存模型

二.Runtime

三.Category

四. KVO 原理

五. iOS App 启动过程 (未完待续)

六. Block原理

七. GCD 与多线程

八. Runloop

九. ARC

十. Tableview 优化

十一. 开源库 (未完待续)

十二. Weex

十三. iOS 中的渲染

十四. 多线程

一、对象内存模型

  • isa指针的作用

  • 对象的isa指向类对象,类对象的isa指向元类,元类的isa指向根元类,根元类的isa指向自己。

  • 类对象的superClass指针指向父类对象,直到指向根类对象,根类对象的superClass指向nil,元类也如此,直到根元类,根元类的superClass指向根类对象

  • 对象的内存分布

    • 对象的变量表

    • 对象大小

    • 对象方法表

    • cache?

    • 协议

    • char *name?

       struct objc_class { 
           Class isa  OBJC_ISA_AVAILABILITY;
       
       #if !__OBJC2__
           Class super_class                                        OBJC2_UNAVAILABLE;
           const char *name                                         OBJC2_UNAVAILABLE;
           long version                                             OBJC2_UNAVAILABLE;
           long info                                                OBJC2_UNAVAILABLE;
           long instance_size                                       OBJC2_UNAVAILABLE;
           struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
           struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
           struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
           struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
       #endif
       
       } OBJC2_UNAVAILABLE;
       /* Use `Class` instead of `struct objc_class *` */
      

Runtime

reference:objc runtime

  1. 消息接收

    • 编译时间确定接受到的消息,运行时间通过@selector找到对应的方法。
    • 消息接受者如果能直接找到@selector则直接执行方法,否则转发消息。若最终找不到,则运行时崩溃。
  2. 术语

    • SEL
      • 方法名的C字符串,用于辨别不同的方法。
      • 用于传递给objc_msgSend(self, SEL)函数
    • Class
      • Class是指向类对象的指针,继承于objc_object
    • categoty
      • categoty是结构体``categoty_t的指针:`typedef struct `categoty`_t *`categoty`; `
      • categoty是在app启动时加载镜像文件,通过read_imgs函数向类对象中的class_rw_t中的某些分组中添加指针,完成categoty的属性添加
    • Method
      • 方法包含以下
        • IMP:函数指针,方法的具体实现
        • types:char*,函数的参数类型,返回值等信息
        • SEL:函数名
  3. 消息转发

    • objc_msgSend()函数并不返回数据,而是它转发消息后,调用了相关的方法返回了数据。
    • 整个流程: 1. 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。 2. 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个nil对象执行任何一个方法不会 Crash,因为会被忽略掉。 3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。 4. 如果 cache 找不到就找一下方法分发表。 5. 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。 6. 进入动态解析:resolveInstanceMethod:resolveClassMethod:方法 7. 若上一步返回NO,进入重定向:- (id)forwardingTargetForSelector:(SEL)aSelector+ (id)forwardingTargetForSelector:(SEL)aSelector 8. 若上一步返回的对象或者类对象仍然没能处理消息或者返回NO,进入消息转发流程:forwardInvocation
    • 重定向和消息转发都可以用于实现多继承
  4. Objective-C Associated Objects关联对象

    • OS X 10.6 之后,Runtime系统让Objc支持向对象动态添加变量。涉及到的函数有以下三个:
    void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
    id objc_getAssociatedObject ( id object, const void *key );
    void objc_removeAssociatedObjects ( id object );
    • 这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联政策是一组枚举常量:
    enum {
          OBJC_ASSOCIATION_ASSIGN  = 0,
          OBJC_ASSOCIATION_RETAIN_NONATOMIC  = 1,
          OBJC_ASSOCIATION_COPY_NONATOMIC  = 3,
          OBJC_ASSOCIATION_RETAIN  = 01401,
          OBJC_ASSOCIATION_COPY  = 01403
    };
  5. Method Swizzling 方法混淆

    • 作用是修改SEL对应的IMP指针
    • 用于debug,避免数组越界等问题.

Categoty

reference:美团技术博客:深入理解Objective-C:Category

categoty 简介

  • categoty开头动态地给类添加属性及方法,通过这个特性,使得objc可以模拟多继承。也可以把一个类拆分到各个文件中,使用方可以按需加载。也可以多人协作共同完成一个类。

categoty 类比 extension

  • extension看起来很像一个匿名的categoty,但是extension和有名字的categoty几乎完全是两个东西。 extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension所以你无法为系统的类比如NSString添加extension

  • 但是categoty则完全不一样,它是在运行期决议的。 就categotyextension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而categoty是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

categoty 内存结构

typedef struct `categoty`_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
} `categoty`_t;

我们知道,所有的OC类和对象,在runtime层都是用struct表示的,categoty也不例外,在runtime层,categoty用结构体categoty_t(在objc-runtime-new.h中可以找到此定义),它包含了

  1. 类的名字(name)
  2. 类(cls)
  3. category中所有给类添加的实例方法的列表(instanceMethods
  4. category中所有添加的类方法的列表(classMethods
  5. category实现的所有协议的列表(protocols
  6. category中添加的所有属性(instanceProperties

category 的加载

category是在runtime进入入口时被添加进类的,runtime通过遍历编译后产生的DATA端上的category_t数组动态地将内容添加到类上。

需要注意的时,category中的方法其实并没有覆盖原类中的同名方法,只是单纯地添加到方法列表中而已。但是由于objc在方法调用中查找到类对象中的方法列表时,只要找到了第一个对应消息中的选择子的方法,就认为是找到了对应的方法并调用。所以形成了category中的方法会覆盖原类中的同名方法的假象。

根据这个原理,我们可以得知,如果有多个类别中同时都复写了类中的某个方法,那么最终调用的是最后被加载的category,也就是最后被编译的category文件。

category 中 Load 方法

category中也可以复写原类中的+Load方法,因为类的Load方法永远发生在categoryLoad方法之前。如果多个category都复写了Load方法,则categoryLoad执行顺序取决于对应category文件的编译顺序。

category 与关联对象

由于category中无法动态添加实例变量,所以可以通过关联对象的手段为对象增加实例变量:

#import "MyClass+Category1.h"
#import <objc/runtime.h>

@implementation MyClass (Category1)

+ (void)load
{
    NSLog(@"%@",@"load in Category1");
}

- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self,
                             "name",
                             name,
                             OBJC_ASSOCIATION_COPY);
}

- (NSString*)name
{
    NSString *nameObject = objc_getAssociatedObject(self, "name");
    return nameObject;
}

@end

关联对象存储在一张全局的map里,其中mapkey是被关联的对象的指针地址,该mapvalue存储的是另外一张map,此处称为AssociationsHashMap,该AssociationsHashMap的kv分别是设置关联对象时的kv。

KVO

  • 原理:

    1. isa swizzling方法,通过runtime动态创建一个中间类,继承自被监听的类。
    2. 使原有类的isa指针指向这个中间类,同时重写改类的Class方法,使得该类的Class方法返回自己而不是isa的指向。
    3. 复写中间类的对应被监听属性的setter方法,调用添加进来的方法,然后给当前中间类的父类也就是原类的发送setter消息。
  • 自定义KVO:

    1. 通过给NSObject添加分类的方法,添加新的对象方法:
        #import <Foundation/Foundation.h>
        @interface NSObject (ACoolCustom_KVO)
        /**
         *    自定义添加观察者
         *
         *    @param oberserver 观察者
         *    @param keyPath    要观察的属性
         *    @param block      自定义回调
         */
        - (void)zl_addOberver:(id)oberserver
                  forKeyPath:(NSString *)keyPath
                       block:(void(^)(id oberveredObject,NSString *keyPath,id newValue,id oldValue))block;
    1. 动态创建中间类, 更改原有类的isa指向,同时重写中间类的Class方法:
     Class originalClass = object_getClass(self);
     //创建中间类 并使其继承被监听的类
     Class kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, 0);
     //向runtime动态注册类
     objc_registerClassPair(kvoClass);
     object_setClass(self, kvoClass);
     //替换被监听对象的class方法...
     //原始类的class方法的实现
     //原始类的class方法的参数等信息
     Method clazzMethod = class_getInstanceMethod(originalClass, @selector(class));
     const char *types = method_getTypeEncoding(clazzMethod);
     class_addMethod(kvoClass, @selector(class), (IMP)new_class, types);
    1. 利用runtime给对象动态增加关联属性保存外部传进来的回调block:
     objc_setAssociatedObject(self, kObjectPropertyKey ,block, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    1. 利用runtime替换中间类的setter方法,在新的方法中,调用block后再向父类发送原消息:
     // 利用函数指针强制转换
     void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
     // 给父类 发送原消息
     objc_msgSendSuperCasted(&superclass, _cmd, newValue);
     // 调用block
     ZLObservingBlock block = objc_getAssociatedObject(self, kObjectPropertyKey);
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         block(self, nil, newValue);
     });

App 启动

Load 与 Initialize

Load

+Load方法的含义是将类对象或者category加载到内存中,这个过程发生在App启动时,在所有类都被注册后。在Category详解中也有所提及。

+Load方法是自顶而下的,也就是说会从类的根类由上而下执行。所以如果重写+Load方法,是不需要调用super的。

值得第一提的是,Category中的+Load方法会在类的+Load方法之后被调用,所以category+Load方法不会覆盖本类的+Load方法。而且由于Category的加载顺序取决编译顺序,所以一个类下不同的Category+Load方法都会被调用,而且调用顺序和编译顺序是一致的。

Initialize

Load不同的是,Initialize方法不会在一开始就会被调用,而是在类收到第一条消息时才会被调用。

值得注意的点是:类初始化的时候每个类只会调用一次+initialize,如果子类没有实现initialize,那么将会调用父类的+initialize,也就是意味着父类的+initialize可能会被多次调用。这与Load方法截然不同。

无论是重写+Load还是+Inilialize方法,都不需要调用superClass。 通过查看这两个方法的源码,还可以得知+Load方法是直接取函数指针调用,不走消息流程,而+inilialize是走消息流程的。也就是说Category里的Initialize 是会覆盖类的initialize方法。

Block

Block 基本原理

int main() {
    void (^blk)(void) = ^{
        (printf("hello world!"));
    };
    blk();
    return 0;
}

如上,通过clang转换为cpp源码,截取关键部分:

// block 的真面目
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

// ..... 一大坨无关代码

// 通过这个结构体包装block,用于快速构造block
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        (printf("hello world!"));
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main() {
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

比较核心的是以下内容:__block_impl结构体,__main_block_impl_0结构体,__main_block_func_0函数,&_NSConcreteStackBlock__main_block_desc_0结构体。

  • __block_impl结构体:

    该结构体定义在源文件上方,其中isa指针指向当前block对象类对象,FuncPtr指向block保存的函数指针。

  • __main_block_desc_0结构体:

    • 用于保存__main_block_impl_0 结构体大小等信息。
  • __main_block_func_0静态函数:

    • 用于存储当前block的代码块。
  • __main_block_impl_0结构体:

    该结构体包装了__block_impl结构体,同时包含__main_block_desc_0结构体。对外提供一个构造函数,构造函数需要传递函数指针(__main_block_func_0静态函数)、__main_block_desc_0实例。

由此我们可知,上述一个简单的block定义及调用过程被转换为了:

  1. 定义block变量相当于调用__main_block_impl_0构造函数,通过函数指针传递代码块进__main_block_impl_0实例。

  2. 构造函数内部,将外部传递进来的__main_block_func_0函数指针,设置内部实际的block变量(__block_impl类型的结构体)的函数指针。

  3. 调用block时,取出__main_block_impl_0类型结构体中的__block_impl类型的结构体的函数指针(__main_block_func_0)并调用。

至此,一个简单的block原理描述完毕。

__block 捕获原理

block内部可以直接使用外部变量,但是在不加__block修饰符的情况下,是无法修改的。比如下面这段代码:

int main() {
    int a = 1;
    void (^blk)(void) = ^{
        printf("%d", a);
    };
    blk();
    return 0;
}

经过cpp重写后:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

        printf("%d", a);
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main() {
    int a = 1;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

经过上一节的讨论,我们知道代码块是由__main_block_impl_0构造函数传通过函数指针传递进来的。在这节的例子中,可以发现构造函数中多了一个int _a参数。同时__main_block_impl_0结构体也多了一个int a属性用于保存block内部使用的变量a

由于构造函数的参数为整形,在c++中,函数的形参为值拷贝,也就是说__main_block_impl_0结构体中的属性a,是外部a变量的拷贝。在代码块内部(也就是__main_block_func_0 函数)我们通过__cself指针拿到a变量。故我们在代码块中是无法修改a变量的,同时如果外部a变量被修改了,那么block内部也是无法得知的。

如果想要在内部修改a变量,可以通过__block关键字:

int main() {
    __block int number = 1;
    void (^blk)(void) = ^{
        number = 3;
        printf("%d", number);
    };
    blk();
    return 0;
}
struct __Block_byref_number_0 {
  void *__isa;
__Block_byref_number_0 *__forwarding;
 int __flags;
 int __size;
 int number;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_number_0 *number; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_number_0 *_number, int flags=0) : number(_number->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_number_0 *number = __cself->number; // bound by ref

        (number->__forwarding->number) = 3;
        printf("%d", (number->__forwarding->number));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->number, (void*)src->number, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->number, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main() {
    __attribute__((__blocks__(byref))) __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 1};
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_number_0 *)&number, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

可以看到整形number变量变成了__Block_byref_number_0结构体实例,在__main_block_impl_0构造函数中,将该实例的指针传递进来。__Block_byref_number_0结构体中,通过__forwarding指向自己,整形number存储具体的值。

上节提到,构造函数中的参数是值拷贝,故此处代码块内部拿到的指针拷贝一样可以操作外部的__Block_byref_number_0结构体中的值,通过这种方式实现了block内部修改外部值。

block 引起的循环引用原理

RunLoop

reference:深入理解Runloop

Runloop 基本原理

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

iOS中,Runloop与线程是一一对应的关系,这种关系存在一张全局字典里:

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
 
/// 获取一个 pthread 对应的 RunLoop。
                      

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap