zoukankan      html  css  js  c++  java
  • 《Effective Objective-C 2.0》摘要

    前一段时间将《Effective Objective-C 2.0》这本书浏览了一遍,说一下几个觉得比较有意思的知识点。
    感觉这本书是ios开发必看的一本书,最基础的,以及稍微高阶一点的oc技术基本都涉及到了。如果书中的涉及的主题能够都掌握,那么绝对可以宣称自己是一个高级oc使用者。

    __objc_msgSend__ (原书Item 11)

    如果有一个oc的方法调用id returnValue = [someObject messageName:parameter],那个这个调用最终会被编译器转换为一个oc runtime c函数调用,也就是objc_msgSend(id self,SEL cmd,…)
    如果你在仔细看过iOS APP的crash栈的话,一定不会感到惊奇。
    oc的对象的方法的实现最终会被编译为一个c函数,objc_msgSend会去寻找selector对应的实现函数,再调用它;为了加速这个过程,runtime内部会缓存这个对应关系。

    有意思的是这个函数还有几个变体,用来处理返回值无法暂存在寄存器里的情况:
    objc_msgSend_stret: 如果返回值是一个struct;
    objc_msgSend_fpret: 如果返回值是一个浮点数,浮点数可能能放到寄存器里面,但有些CPU架构对浮点数的处理有特殊要求;
    objc_msgSendSuper: 这个是用来处理[super message]调用的,当然它也有对应 objc_msgSend_stret,objc_msgSend_fpret的变体

    __Class Object__ (原书Item 14)

    原文很详细的阐述了oc的Class对象是如何构成的。
    一个oc对象如果当做一个struct来看待,第一个字段就是一个Class对象指针,见头文件objc.h中的定义:

    /// Represents an instance of a class.
    struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
    };
    
    /// An opaque type that represents an Objective-C class.
    typedef struct objc_class *Class;
    

    而Class类型又被定义为:

    struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
    } 
    

    这说明Class对象本身也是一种oc对象。
    再一琢磨,这个定义完全就是数据结构中的单链表节点嘛,我们所直接使用的oc对象则是那个链表头节点,那么这个链表结构到底是怎么回事?

    按照原文的说法,oc对象的isa指向对应的Class对象,Class对象的isa指向一个metaClass对象。
    这样设计的一个好处,就是Class对象可以完全作为一个oc对象来使用。

    一个问题是metaClass对象的isa又指向什么,这个链表如何终结?通过下面这段代码,可以强行获取对象的isa字段,进而得到答案:

    typedef struct ClassDump {
    Class isa;
    } ClassDump;
    
    void dumpClassInfoOfClass(Class cls) {
    ClassDump *dump = (__bridge ClassDump*)cls;
    Class metaClass = dump->isa;
    
    ClassDump *dumpOfMetaClass = (__bridge ClassDump*)metaClass;
    Class isaOfMetaClass = dumpOfMetaClass->isa;
    
    NSLog(@"%p %@
    ",cls,NSStringFromClass(cls));
    NSLog(@"%p %@",metaClass,NSStringFromClass(metaClass));
    NSLog(@"%p %@
    ",isaOfMetaClass,NSStringFromClass(isaOfMetaClass));
    NSLog(@"----");
    }
    

    可以分别对NSObject及派生类调用dumpClassInfoOfClass,发现NSObjectClass对象的isa指向另一个Class对象,按原书的说法,就叫NSObjectMetaClass吧;NSObjectMetaClass对象的isa指向自身。
    而UIViewController的Class对象的isa,指向UIViewControllerMetaClass对象,后者的isa指向NSObjectMetaClass。

    可见,对于NSObject及其派生类来说,这个链表的最后一个节点就是NSObjectMetaClass(暂且这么叫吧),而且这个节点是一个自环。

    Class对象的isa通过一般的手段是访问不到的,如果对一个class对象执行[classObj class]返回的是classObj 本身。

    Designated Initializer (原书Item 16)

    大家都知道在设计oc类时,应该有一个叫做designated initializer的init方法,该类的其他init方法都应该通过调用该init方法完成核心的初始化功能。当派生类需要定义自己的init方法时,一定要重写父类的designated initializer。

    但有时候我们有不止一个designated initializer,比如当实现NSCoding协议时,这是一种完全不同的初始化机制,此时initWithCode:(NSCoder)显然也是一个designated initializer,也不该与类的其他init方法有什么关联。

    除了这种特殊情况外,还是应该坚持一个类只有一个designated initializer的原则。

    Block (原书 Item 37)

    block其实就是一种特殊的oc对象,可以被retain,release;

    一个特殊之处在于它不仅可以存储在堆上,还可以在栈上或全局数据区。开发者一般是在方法体里面定义block,此时block实际是在栈上的,超出代码作用域就失效了;要使block任然有效,就需要copy它,因此一些异步回调的block都是需要copy的。

    copy一个block实际上就是把block从栈上拷贝到堆里,如果block已经在堆里了,copy就仅仅增加reference count而已。

    block位于全局数据区的条件是block不捕获任何上下文变量,这样的block对象的内存映像在编译时就是可知的,完全可以当做一个全局常量来对待,对这样的block执行copy不会有任何作用。

    performSelector为什么会有内存警告 (原书Item 41)

    这个问题被原书放在了gcd的相关章节里面。
    在arc模式下,如果我们把一个selector暂存起来,然后通过performSelector来执行,就会有这样的警告:
    warning: performSelector may cause a leak because its selector is unknown.

    这个警告是因为arc管理对象引用的规则,arc模式下,对于返回对象引用的方法,依然要依赖于方法的名称来自动管理对象引用计数,如果方法的名字是以copy,new,alloc开头,那么arc则认为返回了一个强引用;如果一个方法返回了强引用,却没有一个强引用变量去接受这个返回值,arch是要进行release的。

    因此,performSelector由于不能在编译时知道selector的名字,因此arc处于保险起见,只好什么都不做,然后给出一个告警。

    gcd的dispatch queues (原书Item 46)

    GCD默认有5个queue,Main Queue是在主线程调度的queue,serial类型;其他High Priority Queue,Default Priority Queue,Low Priority Queue, Background Priority Queue都是concurrent类型,通过后台线程池进行调度。

    而我们自己创建的queue实际上是基于系统默认的这些queue的,dispatch_queue_create方法的注释是这样说的:

    * The target queue of a newly created dispatch queue is the default priority
    * global concurrent queue.
    

    也就是说我们自己创建的queue里面的task最终在Default Priority Queue上进行执行,只不过增加了一层调度而已。

    这就是原书说不要使用dispatch_get_current_queue的原因了,因为你得到的不一定是你想要的,它的头文件注释

    * Recommended for debugging and logging purposes only:
    * The code must not make any assumptions about the queue returned, unless it
    * is one of the global queues or a queue the code has itself created.
    * The code must not assume that synchronous execution onto a queue is safe
    * from deadlock if that queue is not the one returned by
    * dispatch_get_current_queue().
    
    * When dispatch_get_current_queue() is called on the main thread, it may
    * or may not return the same value as dispatch_get_main_queue(). Comparing
    * the two is not a valid way to test whether code is executing on the
    * main thread.
    

    这个需求一般发生在dispatch_syn调用的时候,如果调用的目标queu是一个serial queue(QueueA),此时就要小心了,如果当前正处在QueueA当中,那么就可能发生死锁,于是就会有这样的判断:

    if (dispatch_get_current_queue()==QueueA) {
       dosSomething()
    } else {
       dispatch_sync(QueueA,accessBlock)。
    }
    

    但如果上述代码调用的的外层是这样的,就会发生死锁:

    dispatch_sync(queueA,^{
       dispatch_sync(queueB,^{
          dispatch_sync(queueA,^{
             //code
          });
       });
    });
    

    原书给出了一个替代方案dispatch_queue_set_specific,我觉得也挺蛋疼的,这里就不说了。
    最好还是避免这样的情况出现:
    1)尽量不要使用自定义的serial queue,如果要创建serial queue,这个queue应该是封装在某个类的内部,它用法一定要足够单纯,不要涉及复杂的调用关系;
    2)记住main queue是serial的,如果要使用dispatch_sync且目标是main queue,可以通过[NSThread isMainThread]来判断当前的执行的的线程是否主线程

    类方法load和initialize(原书Item 51)

    这两个类方法可以在类被使用之前进行一些初始化工作:

    先说load方法,这个方法在类被加载到runtime的时候被调用,对于ios程序来说,所有类的load方法都是在启动时被调用的,而mac os程序则不一定。该方法被runtime调用且仅调用一次,程序员不应该调用这个方法。
    load方法的调用规则:

    • 基类的load方法,在子类的load方法之前调用;
    • 类的load方法在对应category的load方法之前被调用;
    • 被依赖的库中类的load方法先被调用;
    • 对于同一个库中被没有继承关系的类,load方法的调用顺序是不确定的;这就说明,在一个类load方法里面去使用另外一个类的时候,要谨慎小心,也许另一个类的load方法还没被调用。
    • load方法是每个类、category可以定义一个,oc方法的继承重写规则在此不适用:如果基类定义了load,子类没有定义load,子类就相当于没有load;如果一个类有多个category,且多个category都有load方法,这些load方法都会被调用;

    结论:load方法可以说是runtime启动的一部分,除调试目的外,咱们实在不应该掺和。

    再说initialize方法,这个方法在类第一次被使用之前调用,如果在程序的执行过称中类没有被使用,initialize方法是不会被调用的;同样程序员不应该调用这个方法。

    • 相比于load方法,initialize方法调用的时候rumtime已经启动好了,因此在initialize方法理论上可以做任何事;
    • runtime会保证initialize方法的线程安全性,当initialize在一个线程中执行的时候,其他的线程是不能访问这个类的;
    • initialize和一般的oc方法一样,适用继承重写规则。

    上面1)说理论上initialize可以做任何事,但在实际中不要这么干:

    • initialize是同步执行的,如果是在主线程中被执行,就会阻碍UI;
    • initialize的执行时机仍然是不可控的,runtime只承诺在类第一次被使用之前执行;
    • 如果ClassA的initialize方法实现使用了ClassB的方法,那么此时ClassB的initialize方法执行,如果后者又适用ClassA的方法,而此时ClassA的initialize还没执行完毕,就可能出现意外。

    结论:initialize方法可以适当使用,但必须简单,一般用来初始化几个静态变量;不应该在initialize里面调用其他方法,包括本类定义的方法。

  • 相关阅读:
    PHP调用WCF提供的方法
    关于git报 warning: LF will be replaced by CRLF in README.md.的警告的解决办法
    vue中引入mui报Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them的错误
    微信小程序报Cannot read property 'setData' of undefined的错误
    Vue那些事儿之用visual stuido code编写vue报的错误Elements in iteration expect to have 'v-bind:key' directives.
    关于xampp中无法启动mysql,Attempting to start MySQL service...的解决办法!!
    PHP的环境搭建
    新手PHP连接MySQL数据库出问题(Warning: mysqli_connect(): (HY000/1045): Access denied for user 'root'@'localhost' (using password: YES))
    手机号码、获得当前时间,下拉框,填写限制
    团队作业(五):冲刺总结
  • 原文地址:https://www.cnblogs.com/longhuihu/p/4032645.html
Copyright © 2011-2022 走看看