精华内容
参与话题
问答
  • iOS架构设计与底层开发

    千人学习 2017-11-16 22:20:58
    课程内容包括以下:架构设计与底层开发两大知识点
  • iOS底层探索一(底层探索方法) iOS底层探索二(OC 中 alloc 方法 初探) iOS底层探索三(内存对齐与calloc分析) iOS底层探索四(isa初探-联合体,位域,内存优化) iOS底层探索五(isa与类的关系) iOS底层探索六...

     前言


        相关文章:   
           iOS底层探索一(底层探索方法)       

           iOS底层探索二(OC 中 alloc 方法 初探)

           iOS底层探索三(内存对齐与calloc分析)  

    iOS底层探索四(isa初探-联合体,位域,内存优化)     

    iOS底层探索五(isa与类的关系)  

    iOS底层探索六(类的分析上)

    iOS底层探索七(类的分析下)

    iOS底层探索九(方法的本质下objc_msgSend慢速及方法转发初探)

    iOS底层探索十(方法的本质下-消息转发流程)

        相关代码:
          objc4_752源码 方法的本质    方法分发

        温馨提示

    这里先需要和大家解释一下,相关代码每次里面都只有objc4_752,因为这里探索的时候这份代码一直在github上,我这边会把每次探索中,需要对应的类会进行添加相应的注释,其中如果,有的人下载下来代码后,发现运行后结果不一样,例如XZPerson类中可能当前文章中需要里面有属性,成员变量,实例方法,类方法 等,会直接添加上,但是后续文章可能不需要,所以下载下来源码后,建议大家可以结合博文进行对应阅读,其中README.md文件中,也会添加上对应博文地址,后续文章会根据不同文章直接新建不同的target工程,避免影响。

    前几篇文章,我们分析了,alloc方法,对象的本质,类的本质,这篇文章我们对方法的本质进行探索;

    方法的本质

    1.方法底层调用

    首先写一个简单Demo 

    #import <Foundation/Foundation.h>
    #import "XZPerson.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            XZPerson *p = [XZPerson alloc];
             
            [p sayHappy];
        }
        return 0;
    }
    
    
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface XZPerson : NSObject
    -(void)sayHappy;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    #import "XZPerson.h"
    
    @implementation XZPerson
    -(void)sayHappy{
        NSLog(@"say happy")
    }
    @end
    
    

    可以看出,很简单,只是在main函数中调用了一个XZPerson类中的一个方法,下面我们通过clang,对main文件进行编译;使用终端进入main文件目录:clang -rewrite-objc main.m -o main1.cpp  ,同级目录下生成编译后cpp文件

    直接查看main函数

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
            XZPerson *p = ((XZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XZPerson"), sel_registerName("alloc"));
    
            ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHappy"));
        }
        return 0;
    }

    我们将类型强转去掉方便我们对代码进行分析

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
            XZPerson *p = (objc_msgSend)(objc_getClass("XZPerson")
                                        ,sel_registerName("alloc"));
    
            (objc_msgSend)(p, sel_registerName("sayHappy"));
        }
        return 0;
    }

    这样我们就可以很清晰的看出来,方法的本质,其实就是底层通过objc_msgSend方法进行发送消息其中有2个参数

    • 参数一:消息接收者

    • 参数二:sel类型的方法编号

    这里的方法编号sel 就可以联系到上篇文章中的cache中,通过sel在找到imp 并在cache中进行方法查找,并进行返回

    我们在main中写一个C语言的方法

    #import <Foundation/Foundation.h>
    #import "XZPerson.h"
    
    void run(){
        NSLog(@"%s",__func__);
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            XZPerson *p = [XZPerson alloc];
             
            [p sayHappy];
    
            run();
        }
        return 0;
    }
    

    再次进行clang :这次输出main2.m文件,编译完成后查看源码

    
    void run(){
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_vl_y9rmzqq165ngxyd_yz2dmz0h0000gn_T_main_784d77_mi_0,__func__);
    }
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
            XZPerson *p = ((XZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XZPerson"), sel_registerName("alloc"));
    
            ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHappy"));
    
            run();
        }
        return 0;
    }

    我们可以看到C语言方法,直接进行调用,而我们的OC方法需要使用objc_msgSend方法进行查找方法进行调用,这就说明

    objc_msgSeng 方法其实就是OC方法进行查找底层实现的方法实现的过程并进行调用,而C语言函数实现可以直接找到C语言函数名就是函数指针直接找到地址进行调用;

    2.方法几种情况

    当前类方法由objc_msgSend,进行底层查找,父类使用objc_msgSendSuper方法进行底层查找

    实例方法:objc_msgSend -->person对象-->找到对应sel -->对应imp

    类方法:objc_msgSend-->Person类  -->找到sel  -->对应imp

    我们先看一下objc_msgSend 方法底层是怎么声明的:

    需要一个消息接收这,和一给方法编号
    objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

    再看一下objc_msgSendSuper 是怎么声明的

    需要声明一给super结构体 和一个方法编号
    objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
        OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
    
    结构体中有一下属性
    struct objc_super {
        /// Specifies an instance of a class.
        __unsafe_unretained _Nonnull 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 _Nonnull Class class;
    #else
        __unsafe_unretained _Nonnull Class super_class;
    #endif
        /* super_class is the first class to search */
    };
    

    我们使用objc_msgSend,和objc_msgSendSuper代码演示下怎么调用方法

    首先XZPerson继承NSObject ,XZTeacher 继承XZPerson;在main函数中来进行编码,

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface XZPerson : NSObject
    - (void)sayHello;
    + (void)sayNB;
    
    @end
    #import "XZPerson.h"
    
    @implementation XZPerson
    - (void)sayHello{
        NSLog(@"%s",__func__);
    }
    
    + (void)sayNB{
        NSLog(@"%s",__func__);
    }
    
    @end
    
    #import "XZPerson.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface XZTeacher : XZPerson
    - (void)sayCode;
    
    @end
    
    NS_ASSUME_NONNULL_END
    @implementation XZTeacher
    - (void)sayCode{
        NSLog(@"%s",__func__);
    }
    
    @end
    
    

     在main函数中编码调用

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            XZTeacher *t = [XZTeacher new];
            [t sayCode];
            // 方法调用底层编译
            // 方法的本质: 消息 : 消息接受者 消息编号 ....参数 (消息体)
            objc_msgSend(t, sel_registerName("sayCode"));
            
            // 类方法编译底层
            //        id cls = [XZTeacher class];
            //        void *pointA = &cls;
            //        [(__bridge id)pointA sayNB];
            objc_msgSend(objc_getClass("XZTeacher"), sel_registerName("sayNB"));
            
            // 向父类发消息(对象方法)
            struct objc_super xzSuper;
            xzSuper.receiver = t;
            xzSuper.super_class = [XZPerson class];
            objc_msgSendSuper(&xzSuper, @selector(sayHello));
            
            //向父类发消息(类方法)
            struct objc_super myClassSuper;
            myClassSuper.receiver = [t class];
            myClassSuper.super_class = class_getSuperclass(object_getClass([t class]));// 元类
            objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));
             
        }
        return 0;
    }
    

    我们可以看到输出结果:

    按照这种调用方式,方法是可以正常被调用并进行输出打印的,这里需要注意的是,如果要实验objc_msgSend 方法,我们需要在工程的bulid Setting 找到下面属性并置为NO ,否则clang替我们检查语法会直接报错的;

    看来现在所有问题都集中在objc_msgSend方法,那我们先看一下objc_msgSend方法在那个库中

    3.objc_msgSend所在库探索

    我们还是在main函数中写个方法,打断点进行调用,我们打开Xcode栈地址调试模式Debug->DebugWorkflow->Always Show Disassembly,在调用[person sayHello],方法处打断点看下;

    结果

    看到objc_msgSend方法:我们进入这个方法,

    这里我们可以看到是libobjc.A.dylib库中的objc_msgSend方法,对于这个libobjc.A.dylib库我们还是了解的objc4_752中就有这个库,我们可以进入继续源码的探索了;

    4.objc_msgSend源码探索

    在objc_msgSeng查找方法分为2种情况

    1.objc_msgSend汇编写的《快速查找流程》

         1.1:因为RunTime底层是有C,C++和汇编阻证的一套API,而C,C++是一种静态语言

                1.2:是因为在C,C++ 语言中没有必要通过写一个函数来保留位置的参数,并且跳转到任意的函数指针,C,C++语言没有满足做这个事情的必要性

               1.3:是objc_msgSend必须要快,快的话,汇编语言肯定比C,C++速度要快

           2.objc_msgSend慢速查找流程使用C语言进行实现的

    4.1objc_msgSend快速查找

      由于快速查找为汇编语言,本人能力有限,看了个大概,告诉大家位置,可以自行查看,大概梳理了下流程做了部分注释

      我们主要看的是,因为我们手机为arm64架构,所以只需要看这个objc-msg-arm64.s类即可

    1. ENTRY _objc_msgSend
    2. 对消息接受者进行判断处理 (id self, sel _cmd)
    3. taggedPointer 判断处理
    4. GetClassFromisa_p16 isa指针处理 —>找到class
    5. CacheLookup 查找缓存 
    6. ’_cache_t’处理’bucket‘ 以及内存哈希处理(这里其实就是上篇文章中cachet进行处理)
    7.  __objc_msgSend_uncached 告诉我们找不到缓存的 ’imp‘
    8.  ’STATIC_ENTRY’ __objc_msgSend_uncached;
    9. MethodTableLookup:方法查找  
    10. .save parameter register 参数进行准备就绪self和_cmd进行准备
    11. 调用C方法_class_lookupMethodAndCache3调用

    今天有点事情,就先写这么多吧,下篇文章对objc_msgSend慢速流程进行详细分析;

    总结:

    此篇文章对方法的本质进行了初步探索,发现了方法是指是RunTime底层调用objc_msgSend 进行消息转发,并对快速转发流程进行了稍微梳理;如果有错误的地方还请指正,大家一起讨论,开发水平一般(文章中有错误后,我发现会第一时间对博文进行修改),还望大家体谅,欢迎大家点赞,关注我的CSDN,我会定期做一些技术分享!

    写给自己

    喋喋不休不如观心自省,埋怨他人不如即听即忘。能干扰你的,往往是自己的太在意,能伤害你的,往往是自己的想不开,未完待续...

     

    展开全文
  • 面试题block的原理是怎样的?本质是什么?__block的作用是什么?有什么使用注意点?block的属性修饰词为什么是copy?使用block有哪些使用注意?block在修改NSMutableArray,需不需要添加__block?...

    面试题

    1. block的原理是怎样的?本质是什么?
    2. __block的作用是什么?有什么使用注意点?
    3. block的属性修饰词为什么是copy?使用block有哪些使用注意?
    4. block在修改NSMutableArray,需不需要添加__block?

    首先对block有一个基本的认识

    block本质上也是一个oc对象,他内部也有一个isa指针。block是封装了函数调用以及函数调用环境的OC对象。

    探寻block的本质

    首先写一个简单的block

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int age = 10;
            void(^block)(int ,int) = ^(int a, int b){
                NSLog(@"this is block,a = %d,b = %d",a,b);
                NSLog(@"this is block,age = %d",age);
            };
            block(3,5);
        }
        return 0;
    }
    

    使用命令行将代码转化为c++查看其内部结构,与OC代码进行比较

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

    c++与oc代码对比

    上图中将c++中block的声明和定义分别与oc代码中相对应显示。将c++中block的声明和调用分别取出来查看其内部实现。

    定义block变量

    // 定义block变量代码
    void(*block)(int ,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
    

    上述定义代码中,可以发现,block定义中调用了__main_block_impl_0函数,并且将__main_block_impl_0函数的地址赋值给了block。那么我们来看一下__main_block_impl_0函数内部结构。

    __main_block_imp_0结构体

    __main_block_imp_0结构体

    __main_block_imp_0结构体内有一个同名构造函数__main_block_imp_0,构造函数中对一些变量进行了赋值最终会返回一个结构体。

    那么也就是说最终将一个__main_block_imp_0结构体的地址赋值给了block变量

    __main_block_impl_0结构体内可以发现__main_block_impl_0构造函数中传入了四个参数。(void *)__main_block_func_0、&__main_block_desc_0_DATA、age、flags。其中flage有默认值,也就说flage参数在调用的时候可以省略不传。而最后的 age(_age)则表示传入的_age参数会自动赋值给age成员,相当于age = _age。

    接下来着重看一下前面三个参数分别代表什么。

    (void *)__main_block_func_0

    在__main_block_func_0函数中首先取出block中age的值,紧接着可以看到两个熟悉的NSLog,可以发现这两段代码恰恰是我们在block块中写下的代码。 那么__main_block_func_0函数中其实存储着我们block中写下的代码。而__main_block_impl_0函数中传入的是(void *)__main_block_func_0,也就说将我们写在block块中的代码封装成__main_block_func_0函数,并将__main_block_func_0函数的地址传入了__main_block_impl_0的构造函数中保存在结构体内。

    &__main_block_desc_0_DATA

    我们可以看到__main_block_desc_0中存储着两个参数,reserved和Block_size,并且reserved赋值为0而Block_size则存储着__main_block_impl_0的占用空间大小。最终将__main_block_desc_0结构体的地址传入__main_block_func_0中赋值给Desc。

    age

    age也就是我们定义的局部变量。因为在block块中使用到age局部变量,所以在block声明的时候这里才会将age作为参数传入,也就说block会捕获age,如果没有在block中使用age,这里将只会传入(void *)__main_block_func_0,&__main_block_desc_0_DATA两个参数。

    这里可以根据源码思考一下为什么当我们在定义block之后修改局部变量age的值,在block调用的时候无法生效。

    int age = 10;
    void(^block)(int ,int) = ^(int a, int b){
         NSLog(@"this is block,a = %d,b = %d",a,b);
         NSLog(@"this is block,age = %d",age);
    };
         age = 20;
         block(3,5); 
         // log: this is block,a = 3,b = 5
         //      this is block,age = 10
    

    因为block在定义的之后已经将age的值传入存储在__main_block_imp_0结构体中并在调用的时候将age从block中取出来使用,因此在block定义之后对局部变量进行改变是无法被block捕获的。

    此时回过头来查看__main_block_impl_0结构体

    __main_block_impl_0结构体

    首先我们看一下__block_impl第一个变量就是__block_impl结构体。 来到__block_impl结构体内部

    __block_impl结构体内部

    我们可以发现__block_impl结构体内部就有一个isa指针。因此可以证明block本质上就是一个oc对象。而在构造函数中将函数中传入的值分别存储在__main_block_impl_0结构体实例中,最终将结构体的地址赋值给block。

    接着通过上面对__main_block_impl_0结构体构造函数三个参数的分析我们可以得出结论:

    1. __block_impl结构体中isa指针存储着&_NSConcreteStackBlock地址,可以暂时理解为其类对象地址,block就是_NSConcreteStackBlock类型的。

    2. block代码块中的代码被封装成__main_block_func_0函数,FuncPtr则存储着__main_block_func_0函数的地址。

    3. Desc指向__main_block_desc_0结构体对象,其中存储__main_block_impl_0结构体所占用的内存。

    调用block执行内部代码

    // 执行block内部的代码
    ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);
    

    通过上述代码可以发现调用block是通过block找到FunPtr直接调用,通过上面分析我们知道block指向的是__main_block_impl_0类型结构体,但是我们发现__main_block_impl_0结构体中并不直接就可以找到FunPtr,而FunPtr是存储在__block_impl中的,为什么block可以直接调用__block_impl中的FunPtr呢?

    重新查看上述源代码可以发现,(__block_impl *)block将block强制转化为__block_impl类型的,因为__block_impl是__main_block_impl_0结构体的第一个成员,相当于将__block_impl结构体的成员直接拿出来放在__main_block_impl_0中,那么也就说明__block_impl的内存地址就是__main_block_impl_0结构体的内存地址开头。所以可以转化成功。并找到FunPtr成员。

    上面我们知道,FunPtr中存储着通过代码块封装的函数地址,那么调用此函数,也就是会执行代码块中的代码。并且回头查看__main_block_func_0函数,可以发现第一个参数就是__main_block_impl_0类型的指针。也就是说将block传入__main_block_func_0函数中,便于重中取出block捕获的值。

    如何验证block的本质确实是__main_block_impl_0结构体类型。

    通过代码证明一下上述内容: 同样使用之前的方法,我们按照上面分析的block内部结构自定义结构体,并将block内部的结构体强制转化为自定义的结构体,转化成功说明底层结构体确实如我们之前分析的一样。

    struct __main_block_desc_0 { 
        size_t reserved;
        size_t Block_size;
    };
    struct __block_impl {
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr;
    };
    // 模仿系统__main_block_impl_0结构体
    struct __main_block_impl_0 { 
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        int age;
    };
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int age = 10;
            void(^block)(int ,int) = ^(int a, int b){
                NSLog(@"this is block,a = %d,b = %d",a,b);
                NSLog(@"this is block,age = %d",age);
            };
    // 将底层的结构体强制转化为我们自己写的结构体,通过我们自定义的结构体探寻block底层结构体
            struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;
            block(3,5);
        }
        return 0;
    }
    

    通过打断点可以看出我们自定义的结构体可以被赋值成功,以及里面的值。

    blockStruct

    接下来断点来到block代码块中,看一下堆栈信息中的函数调用地址。Debuf workflow -> always show Disassembly

    Debuf workflow -> always show Disassembly

    通过上图可以看到地址确实和FuncPtr中的代码块地址一样。

    总结

    此时已经基本对block的底层结构有了基本的认识,上述代码可以通过一张图展示其中各个结构体之间的关系。

    图示block结构体内部之间的关系

    block底层的数据结构也可以通过一张图来展示

    block底层的数据结构

    block的变量捕获

    为了保证block内部能够正常访问外部的变量,block有一个变量捕获机制。

    局部变量

    auto变量

    上述代码中我们已经了解过block对age变量的捕获。 auto自动变量,离开作用域就销毁,通常局部变量前面自动添加auto关键字。自动变量会捕获到block内部,也就是说block内部会专门新增加一个参数来存储变量的值。 auto只存在于局部变量中,访问方式为值传递,通过上述对age参数的解释我们也可以确定确实是值传递。

    static变量

    static 修饰的变量为指针传递,同样会被block捕获。

    接下来分别添加aotu修饰的局部变量和static修饰的局部变量,重看源码来看一下他们之间的差别。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            auto int a = 10;
            static int b = 11;
            void(^block)(void) = ^{
                NSLog(@"hello, a = %d, b = %d", a,b);
            };
            a = 1;
            b = 2;
            block();
        }
        return 0;
    }
    // log : block本质[57465:18555229] hello, a = 10, b = 2
    // block中a的值没有被改变而b的值随外部变化而变化。
    

    重新生成c++代码看一下内部结构中两个参数的区别。

    局部变量c++代码

    从上述源码中可以看出,a,b两个变量都有捕获到block内部。但是a传入的是值,而b传入的则是地址。

    为什么两种变量会有这种差异呢,因为自动变量可能会销毁,block在执行的时候有可能自动变量已经被销毁了,那么此时如果再去访问被销毁的地址肯定会发生坏内存访问,因此对于自动变量一定是值传递而不可能是指针传递了。而静态变量不会被销毁,所以完全可以传递地址。而因为传递的是值得地址,所以在block调用之前修改地址中保存的值,block中的地址是不会变得。所以值会随之改变。

    全局变量

    我们同样以代码的方式看一下block是否捕获全局变量

    int a = 10;
    static int b = 11;
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void(^block)(void) = ^{
                NSLog(@"hello, a = %d, b = %d", a,b);
            };
            a = 1;
            b = 2;
            block();
        }
        return 0;
    }
    // log hello, a = 1, b = 2
    

    同样生成c++代码查看全局变量调用方式

    全局变量c++代码

    通过上述代码可以发现,__main_block_imp_0并没有添加任何变量,因此block不需要捕获全局变量,因为全局变量无论在哪里都可以访问。

    局部变量因为跨函数访问所以需要捕获,全局变量在哪里都可以访问 ,所以不用捕获。

    最后以一张图做一个总结

    block的变量捕获

    总结:局部变量都会被block捕获,自动变量是值捕获,静态变量为地址捕获。全局变量则不会被block捕获

    疑问:以下代码中block是否会捕获变量呢?

    #import "Person.h"
    @implementation Person
    - (void)test
    {
        void(^block)(void) = ^{
            NSLog(@"%@",self);
        };
        block();
    }
    - (instancetype)initWithName:(NSString *)name
    {
        if (self = [super init]) {
            self.name = name;
        }
        return self;
    }
    + (void) test2
    {
        NSLog(@"类方法test2");
    }
    @end
    

    同样转化为c++代码查看其内部结构

    c++代码

    上图中可以发现,self同样被block捕获,接着我们找到test方法可以发现,test方法默认传递了两个参数self和_cmd。而类方法test2也同样默认传递了类对象self和方法选择器_cmd。

    对象方法和类方法对比

    不论对象方法还是类方法都会默认将self作为参数传递给方法内部,既然是作为参数传入,那么self肯定是局部变量。上面讲到局部变量肯定会被block捕获。

    接着我们来看一下如果在block中使用成员变量或者调用实例的属性会有什么不同的结果。

    - (void)test
    {
        void(^block)(void) = ^{
            NSLog(@"%@",self.name);
            NSLog(@"%@",_name);
        };
        block();
    }
    

    c++代码

    上图中可以发现,即使block中使用的是实例对象的属性,block中捕获的仍然是实例对象,并通过实例对象通过不同的方式去获取使用到的属性。

    block的类型

    block对象是什么类型的,之前稍微提到过,通过源码可以知道block中的isa指针指向的是_NSConcreteStackBlock类对象地址。那么block是否就是_NSConcreteStackBlock类型的呢?

    我们通过代码用class方法或者isa指针查看具体类型。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // __NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
            void (^block)(void) = ^{
                NSLog(@"Hello");
            };
            
            NSLog(@"%@", [block class]);
            NSLog(@"%@", [[block class] superclass]);
            NSLog(@"%@", [[[block class] superclass] superclass]);
            NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
        }
        return 0;
    }
    

    打印内容

    block的类型

    从上述打印内容可以看出block最终都是继承自NSBlock类型,而NSBlock继承于NSObjcet。那么block其中的isa指针其实是来自NSObject中的。这也更加印证了block的本质其实就是OC对象。

    block的3种类型

    block有3中类型

    __NSGlobalBlock__ ( _NSConcreteGlobalBlock )
    __NSStackBlock__ ( _NSConcreteStackBlock )
    __NSMallocBlock__ ( _NSConcreteMallocBlock )
    

    通过代码查看一下block在什么情况下其类型会各不相同

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // 1. 内部没有调用外部变量的block
            void (^block1)(void) = ^{
                NSLog(@"Hello");
            };
            // 2. 内部调用外部变量的block
            int a = 10;
            void (^block2)(void) = ^{
                NSLog(@"Hello - %d",a);
            };
           // 3. 直接调用的block的class
            NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
                NSLog(@"%d",a);
            } class]);
        }
        return 0;
    }
    

    通过打印内容确实可以发现block的三种类型

    block的三种类型

    但是我们上面提到过,上述代码转化为c++代码查看源码时却发现block的类型与打印出来的类型不一样,c++源码中三个block的isa指针全部都指向_NSConcreteStackBlock类型地址。

    我们可以猜测runtime运行时过程中也许对类型进行了转变。最终类型当然以runtime运行时类型也就是我们打印出的类型为准。

    block在内存中的存储

    通过下面一张图看一下不同block的存放区域

    不同类型block的存放区域

    上图中可以发现,根据block的类型不同,block存放在不同的区域中。 数据段中的__NSGlobalBlock__直到程序结束才会被回收,不过我们很少使用到__NSGlobalBlock__类型的block,因为这样使用block并没有什么意义。

    __NSStackBlock__类型的block存放在栈中,我们知道栈中的内存由系统自动分配和释放,作用域执行完毕之后就会被立即释放,而在相同的作用域中定义block并且调用block似乎也多此一举。

    __NSMallocBlock__是在平时编码过程中最常使用到的。存放在堆中需要我们自己进行内存管理。

    block是如何定义其类型

    block是如何定义其类型,依据什么来为block定义不同的类型并分配在不同的空间呢?首先看下面一张图

    block是如何定义其类型

    接着我们使用代码验证上述问题,首先关闭ARC回到MRC环境下,因为ARC会帮助我们做很多事情,可能会影响我们的观察。

    // MRC环境!!!
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // Global:没有访问auto变量:__NSGlobalBlock__
            void (^block1)(void) = ^{
                NSLog(@"block1---------");
            };   
            // Stack:访问了auto变量: __NSStackBlock__
            int a = 10;
            void (^block2)(void) = ^{
                NSLog(@"block2---------%d", a);
            };
            NSLog(@"%@ %@", [block1 class], [block2 class]);
            // __NSStackBlock__调用copy : __NSMallocBlock__
            NSLog(@"%@", [[block2 copy] class]);
        }
        return 0;
    }
    

    查看打印内容

    block类型

    通过打印的内容可以发现正如上图中所示。没有访问auto变量的block是__NSGlobalBlock__类型的,存放在数据段中。 访问了auto变量的block是__NSStackBlock__类型的,存放在栈中。__NSStackBlock__类型的block调用copy成为__NSMallocBlock__类型并被复制存放在堆中。

    上面提到过__NSGlobalBlock__类型的我们很少使用到,因为如果不需要访问外界的变量,直接通过函数实现就可以了,不需要使用block。

    但是__NSStackBlock__访问了aotu变量,并且是存放在栈中的,上面提到过,栈中的代码在作用域结束之后内存就会被销毁,那么我们很有可能block内存销毁之后才去调用他,那样就会发生问题,通过下面代码可以证实这个问题。

    void (^block)(void);
    void test()
    {
        // __NSStackBlock__
        int a = 10;
        block = ^{
            NSLog(@"block---------%d", a);
        };
    }
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            test();
            block();
        }
        return 0;
    }
    

    此时查看打印内容

    打印内容

    可以发现a的值变为了不可控的一个数字。为什么会发生这种情况呢?因为上述代码中创建的block是__NSStackBlock__类型的,因此block是存储在栈中的,那么当test函数执行完毕之后,栈内存中block所占用的内存已经被系统回收,因此就有可能出现乱得数据。查看其c++代码可以更清楚的理解。

    c++代码

    为了避免这种情况发生,可以通过copy将__NSStackBlock__类型的block转化为__NSMallocBlock__类型的block,将block存储在堆中,以下是修改后的代码。

    void (^block)(void);
    void test()
    {
        // __NSStackBlock__ 调用copy 转化为__NSMallocBlock__
        int age = 10;
        block = [^{
            NSLog(@"block---------%d", age);
        } copy];
        [block release];
    }
    

    此时在打印就会发现数据正确

    打印内容

    那么其他类型的block调用copy会改变block类型吗?下面表格已经展示的很清晰了。

    不同类型调用copy效果

    所以在平时开发过程中MRC环境下经常需要使用copy来保存block,将栈上的block拷贝到堆中,即使栈上的block被销毁,堆上的block也不会被销毁,需要我们自己调用release操作来销毁。而在ARC环境下系统会自动调用copy操作,使block不会被销毁。

    ARC帮我们做了什么

    在ARC环境下,编译器会根据情况自动将栈上的block进行一次copy操作,将block复制到堆上。

    什么情况下ARC会自动将block进行一次copy操作?以下代码都在ARC环境下执行。

    1. block作为函数返回值时

    typedef void (^Block)(void);
    Block myblock()
    {
        int a = 10;
        // 上文提到过,block中访问了auto变量,此时block类型应为__NSStackBlock__
        Block block = ^{
            NSLog(@"---------%d", a);
        };
        return block;
    }
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Block block = myblock();
            block();
           // 打印block类型为 __NSMallocBlock__
            NSLog(@"%@",[block class]);
        }
        return 0;
    }
    

    看一下打印的内容

    打印内容

    上文提到过,如果在block中访问了auto变量时,block的类型为__NSStackBlock__,上面打印内容发现blcok为__NSMallocBlock__类型的,并且可以正常打印出a的值,说明block内存并没有被销毁。

    上面提到过,block进行copy操作会转化为__NSMallocBlock__类型,来讲block复制到堆中,那么说明ARC在 block作为函数返回值时会自动帮助我们对block进行copy操作,以保存block,并在适当的地方进行release操作。

    2. 将block赋值给__strong指针时

    block被强指针引用时,ARC也会自动对block进行一次copy操作。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // block内没有访问auto变量
            Block block = ^{
                NSLog(@"block---------");
            };
            NSLog(@"%@",[block class]);
            int a = 10;
            // block内访问了auto变量,但没有赋值给__strong指针
            NSLog(@"%@",[^{
                NSLog(@"block1---------%d", a);
            } class]);
            // block赋值给__strong指针
            Block block2 = ^{
              NSLog(@"block2---------%d", a);
            };
            NSLog(@"%@",[block1 class]);
        }
        return 0;
    }
    

    查看打印内容可以看出,当block被赋值给__strong指针时,ARC会自动进行一次copy操作。

    打印内容

    3. block作为Cocoa API中方法名含有usingBlock的方法参数时

    例如:遍历数组的block方法,将block作为参数的时候。

    NSArray *array = @[];
    [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                
    }];
    

    4. block作为GCD API的方法参数时

    例如:GDC的一次性函数或延迟执行的函数,执行完block操作之后系统才会对block进行release操作。

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
                
    });        
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                
    });
    

    block声明写法

    通过上面对MRC及ARC环境下block的不同类型的分析,总结出不同环境下block属性建议写法。

    MRC下block属性的建议写法

    @property (copy, nonatomic) void (^block)(void);

    ARC下block属性的建议写法

    @property (strong, nonatomic) void (^block)(void);@property (copy, nonatomic) void (^block)(void);


    文中如果有不对的地方欢迎指出。


    作者:xx_cc
    链接:https://juejin.im/post/5b0181e15188254270643e88
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    展开全文
  • iOS底层原理总结 - Category的本质面试题Category的实现原理,以及Category为什么只能加方法不能加属性。Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?load、initialize的区别,以及它们...

    iOS底层原理总结 - Category的本质

    面试题

    1. Category的实现原理,以及Category为什么只能加方法不能加属性。
    2. Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
    3. load、initialize的区别,以及它们在category重写的时候的调用的次序。

    Category的本质

    首先我们写一段简单的代码,之后的分析都基于这段代码。 iOS学习群:727474737

    Presen类 
    // Presen.h
    #import <Foundation/Foundation.h>
    @interface Preson : NSObject
    {
        int _age;
    }
    - (void)run;
    @end
    
    // Presen.m
    #import "Preson.h"
    @implementation Preson
    - (void)run
    {
        NSLog(@"Person - run");
    }
    @end
    
    Presen扩展1
    // Presen+Test.h
    #import "Preson.h"
    @interface Preson (Test) <NSCopying>
    - (void)test;
    + (void)abc;
    @property (assign, nonatomic) int age;
    - (void)setAge:(int)age;
    - (int)age;
    @end
    
    // Presen+Test.m
    #import "Preson+Test.h"
    @implementation Preson (Test)
    - (void)test
    {
    }
    
    + (void)abc
    {
    }
    - (void)setAge:(int)age
    {
    }
    - (int)age
    {
        return 10;
    }
    @end
    
    Presen分类2
    // Preson+Test2.h
    #import "Preson.h"
    @interface Preson (Test2)
    @end
    
    // Preson+Test2.m
    #import "Preson+Test2.h"
    @implementation Preson (Test2)
    - (void)run
    {
        NSLog(@"Person (Test2) - run");
    }
    @end
    

    我们之前讲到过实例对象的isa指针指向类对象,类对象的isa指针指向元类对象,当p调用run方法时,类对象的isa指针找到类对象的isa指针,然后在类对象中查找对象方法,如果没有找到,就通过类对象的superclass指针找到父类对象,接着去寻找run方法。

    那么当调用分类的方法时,步骤是否和调用对象方法一样呢?
    分类中的对象方法依然是存储在类对象中的,同对象方法在同一个地方,那么调用步骤也同调用对象方法一样。如果是类方法的话,也同样是存储在元类对象中。
    那么分类方法是如何存储在类对象中的,我们来通过源码看一下分类的底层结构。

    分类的底层结构

    如何验证上述问题?通过查看分类的源码我们可以找到category_t 结构体。

    struct category_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; // 属性
        // Fields below this point are not always present on disk.
        struct property_list_t *_classProperties;
    
        method_list_t *methodsForMeta(bool isMeta) {
            if (isMeta) return classMethods;
            else return instanceMethods;
        }
    
        property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    };
    

    从源码基本可以看出我们平时使用categroy的方式,对象方法,类方法,协议,和属性都可以找到对应的存储方式。并且我们发现分类结构体中是不存在成员变量的,因此分类中是不允许添加成员变量的。分类中添加的属性并不会帮助我们自动生成成员变量,只会生成get set方法的声明,需要我们自己去实现。

    通过源码我们发现,分类的方法,协议,属性等好像确实是存放在categroy结构体里面的,那么他又是如何存储在类对象中的呢?
    我们来看一下底层的内部方法探寻其中的原理。
    首先我们通过命令行将Preson+Test.m文件转化为c++文件,查看其中的编译过程。

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Preson+Test.m
    

    在分类转化为c++文件中可以看出_category_t结构体中,存放着类名,对象方法列表,类方法列表,协议列表,以及属性列表。



    c++文件中category_t结构体

    紧接着,我们可以看到_method_list_t类型的结构体,如下图所示


    
    

    对象方法列表结构体

    上图中我们发现这个结构体_OBJC_$_CATEGORY_INSTANCE_METHODS_Preson_$_Test从名称可以看出是INSTANCE_METHODS对象方法,并且一一对应为上面结构体内赋值。我们可以看到结构体中存储了方法占用的内存,方法数量,以及方法列表。并且从上图中找到分类中我们实现对应的对象方法,test , setAge, age三个方法

    接下来我们发现同样的_method_list_t类型的类方法结构体,如下图所示



    类对象方法列表

    同上面对象方法列表一样,这个我们可以看出是类方法列表结构体 _OBJC_$_CATEGORY_CLASS_METHODS_Preson_$_Test,同对象方法结构体相同,同样可以看到我们实现的类方法,abc。

    接下来是协议方法列表

    协议方法列表

    通过上述源码可以看到先将协议方法通过_method_list_t结构体存储,之后通过_protocol_t结构体存储在_OBJC_CATEGORY_PROTOCOLS_$_Preson_$_Test中同_protocol_list_t结构体一一对应,分别为protocol_count 协议数量以及存储了协议方法的_protocol_t结构体。

    最后我们可以看到属性列表




    协议方法列表

    通过上述源码可以看到先将协议方法通过_method_list_t结构体存储,之后通过_protocol_t结构体存储在_OBJC_CATEGORY_PROTOCOLS_$_Preson_$_Test中同_protocol_list_t结构体一一对应,分别为protocol_count 协议数量以及存储了协议方法的_protocol_t结构体。

    最后我们可以看到属性列表





    属性列表结构体

    属性列表结构体_OBJC_$_PROP_LIST_Preson_$_Test同_prop_list_t结构体对应,存储属性的占用空间,属性属性数量,以及属性列表,从上图中可以看到我们自己写的age属性。

    最后我们可以看到定义了_OBJC_$_CATEGORY_Preson_$_Test结构体,并且将我们上面着重分析的结构体一一赋值,我们通过两张图片对照一下。




    _category_t

    _OBJC_$_CATEGORY_Preson_$_Test

    上下两张图一一对应,并且我们看到定义_class_t类型的OBJC_CLASS_$_Preson结构体,最后将_OBJC_$_CATEGORY_Preson_$_Testcls指针指向OBJC_CLASS_$_Preson结构体地址。我们这里可以看出,cls指针指向的应该是分类的主类类对象的地址。

    通过以上分析我们发现。分类源码中确实是将我们定义的对象方法,类方法,属性等都存放在catagory_t结构体中。接下来我们在回到runtime源码查看catagory_t存储的方法,属性,协议等是如何存储在类对象中的。

    首先来到runtime初始化函数





    runtime初始化函数

    接着我们来到 &map_images读取模块(images这里代表模块),来到map_images_nolock函数中找到_read_images函数,在_read_images函数中我们找到分类相关代码



    Discover categories代码

    从上述代码中我们可以知道这段代码是用来查找有没有分类的。通过_getObjc2CategoryList函数获取到分类列表之后,进行遍历,获取其中的方法,协议,属性等。可以看到最终都调用了remethodizeClass(cls);函数。我们来到remethodizeClass(cls);函数内部查看。





    remethodizeClass函数内部

    通过上述代码我们发现attachCategories函数接收了类对象cls和分类数组cats,如我们一开始写的代码所示,一个类可以有多个分类。之前我们说到分类信息存储在category_t结构体中,那么多个分类则保存在category_list中。

    我们来到attachCategories函数内部。




    attachCategories函数内部实现

    上述源码中可以看出,首先根据方法列表,属性列表,协议列表,malloc分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址。之后从分类数组里面往三个数组里面存放分类数组里面存放的分类方法,属性以及协议放入对应mlist、proplists、protolosts数组中,这三个数组放着所有分类的方法,属性和协议。
    之后通过类对象的data()方法,拿到类对象的class_rw_t结构体rw,在class结构中我们介绍过,class_rw_t中存放着类对象的方法,属性和协议等数据,rw结构体通过类对象的data方法获取,所以rw里面存放这类对象里面的数据。
    之后分别通过rw调用方法列表、属性列表、协议列表的attachList函数,将所有的分类的方法、属性、协议列表数组传进去,我们大致可以猜想到在attachList方法内部将分类和本类相应的对象方法,属性,和协议进行了合并。

    我们来看一下attachLists函数内部。




    attachLists函数内部实现

    上述源代码中有两个重要的数组
    array()->lists: 类对象原来的方法列表,属性列表,协议列表。
    addedLists:传入所有分类的方法列表,属性列表,协议列表。

    attachLists函数中最重要的两个方法为memmove内存移动和memcpy内存拷贝。我们先来分别看一下这两个函数




    下面我们图示经过memmove和memcpy方法过后的内存变化。



    未经过内存移动和拷贝时

    经过memmove方法之后,内存变化为


    memmove方法之后内存变化

    经过memmove方法之后,我们发现,虽然本类的方法,属性,协议列表会分别后移,但是本类的对应数组的指针依然指向原始位置。

    memcpy方法之后,内存变化



    memmove方法之后,内存变化

    我们发现原来指针并没有改变,至始至终指向开头的位置。并且经过memmove和memcpy方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。

    那么为什么要将分类方法的列表追加到本来的对象方法前面呢,这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。
    其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用。本类的方法依然在内存中的。我们可以通过打印所有类的所有方法名来查看




    通过下图中打印内容可以发现,调用的是Test2中的run方法,并且Person类中存储着两个run方法。


    打印所有方法

    总结:

    问: Category的实现原理,以及Category为什么只能加方法不能加属性?

    答:分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。
    Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。

    load 和 initialize

    load方法会在程序启动就会调用,当装载类信息的时候就会调用。
    调用顺序看一下源代码。




    load方法调用顺序

    通过源码我们发现是优先调用类的load方法,之后调用分类的load方法。

    我们通过代码验证一下:
    我们添加Student继承Presen类,并添加Student+Test分类,分别重写只+load方法,其他什么都不做通过打印发现


    load方法打印

    确实是优先调用类的load方法之后调用分类的load方法,不过调用类的load方法之前会保证其父类已经调用过load方法。

    之后我们为Preson、Student 、Student+Test 添加initialize方法。
    我们知道当类第一次接收到消息时,就会调用initialize,相当于第一次使用类的时候就会调用initialize方法。调用子类的initialize之前,会先保证调用父类的initialize方法。如果之前已经调用过initialize,就不会再调用initialize方法了。当分类重写initialize方法时会先调用分类的方法。但是load方法并不会被覆盖,首先我们来看一下initialize的源码。




    initialize调用源码

    上图中我们发现,initialize是通过消息发送机制调用的,消息发送机制通过isa指针找到对应的方法与实现,因此先找到分类方法中的实现,会优先调用分类方法中的实现。

    我们再来看一下load方法的调用源码



    load方法的调用源码

    我们看到load方法中直接拿到load方法的内存地址直接调用方法,不在是通过消息发送机制调用。


    load方法的调用源码

    我们看到load方法中直接拿到load方法的内存地址直接调用方法,不在是通过消息发送机制调用。


    分类load方法的调用源码

    我们可以看到分类中也是通过直接拿到load方法的地址进行调用。因此正如我们之前试验的一样,分类中重写load方法,并不会优先调用分类的load方法,而不调用本类中的load方法了。

    总结

    问:Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
    答:Category中有load方法,load方法在程序启动装载类信息的时候就会调用。load方法可以继承。调用子类的load方法之前,会先调用父类的load方法

    问:load、initialize的区别,以及它们在category重写的时候的调用的次序。
    答:区别在于调用方式和调用时刻
    调用方式:load是根据函数地址直接调用,initialize是通过objc_msgSend调用
    调用时刻:load是runtime加载类、分类的时候调用(只会调用1次),initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)

    调用顺序:先调用类的load方法,先编译那个类,就先调用load。在调用load之前会先调用父类的load方法。分类中load方法不会覆盖本类的load方法,先编译的分类优先调用load方法。initialize先初始化父类,之后再初始化子类。如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次),如果分类实现了+initialize,就覆盖类本身的+initialize调用。

     iOS学习群:727474737

    本文是对底层原理学习的总结,如果有不对的地方请指正,一起学习共同进步










    展开全文
  • iOS底层原理之架构设计

    千次阅读 2018-10-29 16:30:08
    iOS底层原理之架构设计

    何为架构?

    • 架构(Architecture):软件开发中的设计方案,类与类之间的关系、模块与模块之间的关系、客户端与服务端的关系。
    • 经常听到的架构名词:MVC、MVP、MVVM、VIPER、CDD、三层架构、四层架构等。

    MVC - Apple版

    Model-View-Controller
    MVC - Apple版
    是iOS开发中常用的模式 ,Model和View之间没有任何直接到关系,通过Controller作为桥梁将二者联系起来。(Controller可初始化数据并将数据传递给Model;Model可以将数据传递给Controller,Controller将这些数据赋给View展示;View可以传递事件给Controller,Controller通过事件判断区出来数据Model)。
    优点:View、Model可以重复利用,可以独立使用
    缺点:Controller的代码过于臃肿。
    Model-View-Controller 苹果官方文档

    MVC – 变种

    Model-View-Controller
     MVC – 变种
    变种的MVC,每一个view对应一个Model,只要Controller设置View的Model,就可直接展示对应的示图(常见的cell添加一个model属性就是这种模式)。
    优点:对Controller进行瘦身,将View内部的细节封装起来了,外界不知道View内部的具体实现;
    缺点:View依赖于Model。

    MVP

    • Model-View-Presenter
      MVP
    • MVP中的V在iOS中指的是ViewController和View。MVP将MVC的ViewController进行拆分:视图数据逻辑处理部分为P,ViewController剩余部分与View合并成V,V和P之间通过Protocol进行通信。
    • MVP实现了各模块的解藕,具有更好的可测试性。但是总体代码量比MVC大。
      另外,iOS MVC更适用于快速开发,即代码规模较小的项目。因此将简单的MVC的Demo改成MVP,反而会显得笨拙。
    #import "MJAppPresenter.h"
    #import "MJApp.h"//Model
    #import "MJAppView.h"//View
    @interface MJAppPresenter() <MJAppViewDelegate>
    @property (weak, nonatomic) UIViewController *controller;
    @end
    @implementation MJAppPresenter
    - (instancetype)initWithController:(UIViewController *)controller
    {
        if (self = [super init]) {
            self.controller = controller;
            // 创建View
            MJAppView *appView = [[MJAppView alloc] init];
            appView.frame = CGRectMake(100, 100, 100, 150);
            appView.delegate = self;
            [controller.view addSubview:appView];
            
            // 加载模型数据
            MJApp *app = [[MJApp alloc] init];
            app.name = @"QQ";
            app.image = @"QQ";
            
            // 赋值数据
            [appView setName:app.name andImage:app.image];
        }
        return self;
    }
    #pragma mark - MJAppViewDelegate
    - (void)appViewDidClick:(MJAppView *)appView
    {
        NSLog(@"presenter 监听了 appView 的点击");
    }
    @end
    
    #import "ViewController.h"
    #import "MJAppPresenter.h"
    @interface ViewController ()
    @property (strong, nonatomic) MJAppPresenter *presenter;
    //@property (strong, nonatomic) MJOtherPresenter *presenter1;
    //@property (strong, nonatomic) MJNewsPresenter *presenter2;
    @end
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.presenter = [[MJAppPresenter alloc] initWithController:self];
    }
    

    可以看出View和Model的都是直接和Presenter传递值和交互的,时间处理也在Presenter中,不同的View和Model可以给Controller添加不同的Presenter属性即可,Controller不再直接是View和Model的桥梁。

    MVVM

    • Model-View-ViewModel
      MVVM
    • Model-View-ViewModel for iOS
    • MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。 唯一的区别是,它采用双向绑定(data-binding);当被绑定对象某个值的变化时,绑定对象会自动感知,无需被绑定对象主动通知绑定对象。可以使用KVO和RAC实现。例如在Label中显示倒计时,是V绑定了包含定时器的VM。
    • 双向绑定在MVVM中指的是V和VM之间相互绑定。例如TextField的text长度达到阈值,另一个Button改变背景颜色。这个过程中首先VM感知V中TextField的text属性长度变化,V感知VM中对应的状态属性。一旦V中TextField的text属性长度超出VM中的阈值,VM中的状态属性改变,触发V中Button的背景色发生改变。
    • MVVM的优点:
    1. 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
    2. 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
    3. 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
    4. 可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写。
    • facebookKVOController九可以用来双向绑定,其github地址为:KVOController
    //View
    #import <UIKit/UIKit.h>
    @class MJAppView, MJAppViewModel;
    @protocol MJAppViewDelegate <NSObject>
    @optional
    - (void)appViewDidClick:(MJAppView *)appView;
    @end
    @interface MJAppView : UIView
    @property (weak, nonatomic) MJAppViewModel *viewModel;
    @property (weak, nonatomic) id<MJAppViewDelegate> delegate;
    @end
    
    #import "MJAppView.h"
    #import "NSObject+FBKVOController.h"
    @interface MJAppView()
    @property (weak, nonatomic) UIImageView *iconView;
    @property (weak, nonatomic) UILabel *nameLabel;
    @end
    @implementation MJAppView
    - (instancetype)initWithFrame:(CGRect)frame
    {
        if (self = [super initWithFrame:frame]) {
            UIImageView *iconView = [[UIImageView alloc] init];
            iconView.frame = CGRectMake(0, 0, 100, 100);
            [self addSubview:iconView];
            _iconView = iconView;
            UILabel *nameLabel = [[UILabel alloc] init];
            nameLabel.frame = CGRectMake(0, 100, 100, 30);
            nameLabel.textAlignment = NSTextAlignmentCenter;
            [self addSubview:nameLabel];
            _nameLabel = nameLabel;
        }
        return self;
    }
    - (void)setViewModel:(MJAppViewModel *)viewModel
    {
        _viewModel = viewModel;
        __weak typeof(self) waekSelf = self;
        //监听viewModel的name和image属性变化,一旦变化就给view赋值
        [self.KVOController observe:viewModel keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
            waekSelf.nameLabel.text = change[NSKeyValueChangeNewKey];
        }];
        [self.KVOController observe:viewModel keyPath:@"image" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
            waekSelf.iconView.image = [UIImage imageNamed:change[NSKeyValueChangeNewKey]];
        }];
    }
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        if ([self.delegate respondsToSelector:@selector(appViewDidClick:)]) {
            [self.delegate appViewDidClick:self];
        }
    }
    @end
    
    //ViewModel
    #import <UIKit/UIKit.h>
    @interface MJAppViewModel : NSObject
    - (instancetype)initWithController:(UIViewController *)controller;
    @end
    #import "MJAppViewModel.h"
    #import "MJApp.h"
    #import "MJAppView.h"
    @interface MJAppViewModel() <MJAppViewDelegate>
    @property (weak, nonatomic) UIViewController *controller;
    @property (copy, nonatomic) NSString *name;
    @property (copy, nonatomic) NSString *image;
    @end
    @implementation MJAppViewModel
    - (instancetype)initWithController:(UIViewController *)controller
    {
        if (self = [super init]) {
            self.controller = controller;
            // 创建View
            MJAppView *appView = [[MJAppView alloc] init];
            appView.frame = CGRectMake(100, 100, 100, 150);
            appView.delegate = self;
            appView.viewModel = self;//双向绑定
            [controller.view addSubview:appView];
            // 加载模型数据
            MJApp *app = [[MJApp alloc] init];
            app.name = @"QQ";
            app.image = @"QQ";
            // 设置数据
            self.name = app.name;
            self.image = app.image;
        }
        return self;
    }
    #pragma mark - MJAppViewDelegate
    - (void)appViewDidClick:(MJAppView *)appView
    {
        NSLog(@"viewModel 监听了 appView 的点击");
    }
    @end
    
    //Controller
    #import "ViewController.h"
    #import "MJAppViewModel.h"
    @interface ViewController ()
    @property (strong, nonatomic) MJAppViewModel *viewModel;
    @end
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.viewModel = [[MJAppViewModel alloc] initWithController:self];
    }
    @end
    

    可以看出Controller 中只添加了ViewModel,不再与View和Model有任何关系,ViewModel初始化了View和Model,并将Model数据绑定给自己;而View绑定了ViewModel属性,并监听器值得变化,一旦有变化设置相应的控件;ViewModel为View代理,响应view的一些事件。

    设计模式

    • 设计模式(Design Pattern)
      是一套被反复使用、代码设计经验的总结
      使用设计模式的好处是:可重用代码、让代码更容易被他人理解、保证代码可靠性
      一般与编程语言无关,是一套比较成熟的编程思想
    • 设计模式可以分为三大类
    1. 创建型模式:对象实例化的模式,用于解耦对象的实例化过程
      单例模式、工厂方法模式,等等
    2. 结构型模式:把类或对象结合在一起形成一个更大的结构
      代理模式、适配器模式、组合模式、装饰模式,等等
    3. 行为型模式:类或对象之间如何交互,及划分责任和算法
      观察者模式、命令模式、责任链模式,等等

    面试题

    • 讲讲 MVC、MVVM、MVP,以及你在项目里具体是怎么写的?
      把上面的讲一下就好了。

    • 你自己用过哪些设计模式?
      单例、代理、观察者模等等。

    • 一般开始做一个项目,你的架构是如何思考的?
      从实现方式思考,从结构模块思考,自己发挥。



    展开全文
  • iOS底层原理之性能优化

    千次阅读 2018-10-29 16:29:11
    #iOS底层原理之性能优化
  • E-moss,程序员,爱好阅读和撸狗,主要从事iOS开发工作,公众号:知本集。 主要分享和编写技术方面文章,不定期分享读书笔记,亦可访问“知本集”Git地址:https://github.com/knowtheroot/KnowTheRoot_iOS,欢迎...
  • iOS底层原理 - Block本质探究 本质 block 本质是一个OC对象,也存在 isa 指针。或者说Block 是封装了函数调用和函数调用环境的OC对象。 1.底层实现 编写一段最简单的OC代码顶一个block,代码如: int ...
  • 得遇名师,突飞猛进!iOS培训王者MJ(李明杰)老师精心研发,iOS进阶课程,实用技术不断的更新和升级,更快帮助职场人士在开发领域脱颖而出。远程视频教学,无须长途奔袭,碎片化时间学习,成长随时随地!
  • iOS 底层实现 - Block

    2016-11-24 22:33:57
    iOS 底层实现 - Block
  • BlockiOS4.0+ 和Mac OS X 10.6+ 引进的对C语言的扩展,用来实现匿名函数的特性。 用维基百科的话来说,Block是Apple Inc.为C、C++以及Objective-C添加的特性,使得这些语言可以用类lambda表达式的语法来创建闭包。...
  • ios __block修饰词底层实现原理

    千次阅读 2018-10-23 14:25:28
    注:此文章为自己学习笔记,部分来自欧阳大哥博客https://www.jianshu.com/p/595a1776ba3a 让我们看下代码: //文件test.m #import &lt;Foundation/Foundation.h&gt; void test() ... ...
  • IOS底层原理(二十一)Block 原理源码分析
  • iOS底层开发班实战视频培训课程:APP逆向实战、加壳脱壳、数据安全、编译原理、iOS底层开发实现、iOS底层开发机制 iOS进阶课程,实用技术不断的更新和升级,更快帮助职场人士在开发领域脱颖而出。远程视频教学,...
  • iOS底层原理视频

    千次阅读 热门讨论 2018-11-11 22:40:40
    iOS底层原理班mj视频找了好久终于找到了 链接: https://pan.baidu.com/s/1S5S3QbTdDtFODfjwpKkRPQ 提取码: zf8q
  • 他只是负责展示和响应交互事件的,而真正的图形绘制工作,全部是CALayer来完成的,其实CALayer并不属于UIKit架构,而是属于QuartzCore架构,他是一个专门负责图形绘制的工具类,而且是完全跨IOS和OS X平台的。...
  • 得遇名师,突飞猛进!iOS培训王者MJ(李明杰)老师精心研发,iOS进阶课程,实用技术不断的更新和升级,更快帮助职场人士在开发领域脱颖而出。远程视频教学,无须长途奔袭,碎片化时间学习,成长随时随地!...
  • 2.2:在arc下,block 自动加上copy的情况---:返回block。 2.3:在arc下,block 自动加上copy的情况---:强指针__block; 2.4:arc环境下:方法名中有usingBlock的方法参数时,也会进行copy操作。 2.5:在arc下,...
  • Block是什么?苹果推荐的类型,效率高,在运行中保存代码。用来封装和保存代码,...Block 底层实现 定义一个简单的block 我们再给a赋值为20,此时打印出来a 的值还是10 但当我们在第一次给a
  • iOS Block底层实现原理详解

    千次阅读 2016-04-28 14:57:50
    block原始代码为: int main(int argc, const char * argv[]) { @autoreleasepool { int a = 10; void (^block)() = ^{ printf("%d\n", a); };
  • iOSblock实现的探究

    万次阅读 多人点赞 2012-07-17 20:23:08
    BlockiOS4.0+ 和Mac OS X 10.6+ 引进的对C语言的扩展,用来实现匿名函数的特性。 用维基百科的话来说,Block是Apple Inc.为C、C++以及Objective-C添加的特性,使得这些语言可以用类lambda表达式的语法来创建闭包...
  • iOSBlock的用法,举例,解析与底层原理(Block看我really足够了)
  • iOS底层音频处理技术(带源代码)

    千次阅读 2012-09-11 17:40:58
    连接:... iOS底层音频处理技术(带源代码)  ...前几天搜索资料时发现一个网站:iPhone Core Audio Development,里面有iOS底层音频技术的几个源代码,如果你要实现VoIP
  • block : Object - C对于闭包的实现 . 闭包 = 一个函数(或是指向函数的指针) +该函数执行的外部的上下文变量(自由变量) 2.对block的理解 可以嵌套定义,定义 block 方法和定义函数方法相似 block可以定义在...
  • 看了一篇文章,发现遍历数组、字典中的数据时,除了使用for循环外,还可以使用block块进行操作,瞬间感觉iOS的语言代码确实有点高大上的感觉,下面就简单的介绍一下这个方法。首先是最基本的运用形式,//基于 块...
  • iOS OC语言: Block底层实现原理

    千次阅读 2016-04-28 14:59:40
    先来简单介绍一下Block Block是什么?苹果推荐的类型,效率高,在运行中保存代码。用来封装和保存代码,有点像函数,Block可以在任何时候执行。...Block 底层实现 定义一个简单的block
  • 看完这两篇基本就够了 个人觉得 希望看到的朋友 都能有所收获 来自程序猿 http://www.jianshu.com/p/404ff9d3cd42 来自bat大神 http://www.jianshu.com/p/e03292674e60
  • iOS 教你如何使用代码块Block

    千次阅读 2018-09-10 16:25:05
    代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实现匿名函数的特性,Block是一种特殊的数据类型,其可以正常定义变量、作为参数、作为返回值,特殊地,Block还可以保存一段代码,在需要的时候调用,目前Block已经...
  • iOS block的使用总结

    2016-04-27 13:29:40
    一.block的原理:Block的实质是指向结构体的指针,查看block底层代码:在终端中cd到工程路径,然后执行clang -rewrite-objc main.m block的数据结构定义我们通过大师文章中的一张图来说明:上图这个结构是在栈中的...
  • iOS block详解

    千次阅读 2014-01-09 16:21:19
    BlockiOS在4.0之后新增的程式语法,严格来说block的概念并不算是基础程式设计的范围,对初学者来说也不是很容易了解,但是在iOS SDK 4.0之后,block几乎出现在所有新版的API之中,换句话说,如果不了解block这个...

空空如也

1 2 3 4 5 ... 20
收藏数 1,391,952
精华内容 556,780
关键字:

ios底层