您的位置 首页 > 数码极客

runtime如何实现weak变量的自动置nil

程序是由一行行代码堆叠而成的,看似是一个整体,其实内部包含了很多结构。我们可以简单的认为软件应用就是一个人体,而代码就是其中的细胞。世界上有很多人,有的强壮,有的瘦弱,程序也是一样。好的程序是需要锻炼的,一个癌细胞便可使程序处于崩溃,一个应用的健壮性,很大程度上都在于对细节的把控。程序,每个开发者都会写,但有的人就能写的更好,功夫在于每一行代码的讲究。对于代码,不要吝啬于您的时间,它会在合适的时候回报于您。

下面我们就来谈一谈那些需要关注的细节。

block和delegate的使用

Delegate

委托是协议的一种,顾名思义,就是委托他人帮自己去做事。委托是给一个对象提供机会对另一个对象中的变化做出反应或者影响另一个对象的行为。其基本思想是两个对象协同解决问题,并且打算在广泛的情形中重用。委托指向另一个对象(即它的委托)的引用,并在关键时刻给委托发消息。消息可能只是通知委托发生了某件事情,给委托提供机会执行额外的处理,或者消息可能要求委托提供一些关键的信息以控制所发生的事情。委托的作用主要有两个,一个是传值,一个是传事件。

传值常用在B类要把自己的一个数据或者对象传给A类,让A类去展示或者处理(切分紧耦合,和代码分块时常用)。传事件是A类发生了什么事,把这件事告诉关注自己的类,也就是委托的对象,由委托的对象去考虑发生这个事件后应该做出什么反映(例如在异步请求中,界面事件触发数据层改变等)。利用委托赋值,这种方法是为了不暴露自己的属性就可以给自己赋值,这样方便了类的管理,只有在你想要让别人给你赋值的时候才调用,这样的赋值更可控一些。(如tableView中的委托dateSource等)。

在iOS中委托通过一种@protocol的方式实现,所以又称为协议。协议是多个类共享的一个方法列表,在协议中所列出的方法没有响应的实现,由其它类来实现。delegate是一对一的关系,对同一个协议,一个对象只能设置一个代理delegate,所以单例对象就不能用代理。代理更注重过程信息的传输,如发起一个网络请求,是否此时请求已经开始、是否收到了数据、数据是否已经接受完成、数据接收失败等。

从委托类的定义可以看出,委托与协议有一定的相似性,但是delegate与protocol本质上是不同的,delegate本身应该称为一种设计模式,是把一个类自己需要做的一部分事情,让另一个类(也可以就是自己本身)来完成,而实际做事的类为delegate。而protocol是一种语法,它的主要目标是提供接口给遵守协议的类使用,而这种方式提供了一个很方便的实现delegate模式的机会。

委托模式的实现思路如下:

通常是在对象主体包含一个委托对象的弱引用。

委托对象的实现有两种方式:一种是必须实现,一种是可选实现,即@required和@optional的区别。

判断触发委托方法。

Block

Block是Apple为Objective-C添加的特性,使得OC可以用类lambda表达式的语法来创建闭包,是iOS4.0以后和Mac OS X 10.6以后引进的对C语言的扩展,用来实现匿名函数的特性。Block能够读取其它函数内部变量的函数,在一段请求连续代码中可以看到调用参数(如发送请求)和响应结果。采用Block技术能够抽象出很多共用函数,提高了代码的可读性,可维护性,封装性。

block写法更简练,不需要写protocol、函数等等。block注重结果的传输,比如对于一个事件,只想知道成功或者失败,并不需要知道进行了多少或者额外的一些信息,不过block需要注意防止循环引用。

2.block所在函数中的,可以捕获自动变量,但是不能修改它,不然就是编译错误。但是可以改变全局变量、静态变量、全局静态变量。为何不让修改变量?这个是编译器决定的。理论上当然可以修改变量了,只不过block捕获的是自动变量的副本,名字一样。为了不给开发者迷惑,干脆不让赋值。静态变量是属于类的,不是某一个实例的变量,所以block内部不用调用self指针。解决block不能保存值这一问题的一个办法是使用__block修饰符。

Block和Delegate的区别

block不像代理声明了一个代理函数,在调用的类内部还要实现该函数,若一个页面能发送多个请求,并且用多点触控同时触发发送多个请求,那么这个页面的代理函数很难区分是那个请求的结果,只有你的响应消息中带有消息类型可能会分出来,若服务器做的不够强大,当出现异常时,找不到相对的发送请求,对于开发来说是个问题,同时多个消息在一个函数里解析也不利于封装。另外Block比代理更加清晰, Block可以在创建事件的时候区分开来。这也是为什么现在苹果 API 中越来越多地使用 Block而不是 Delegate。

block有三个存储区域_NSConcretStackBlock ,_NSConcretGlobalBlock和_NSConcretMallocBlock。正如它们名字说的那样,说明了block的三种存储方式:栈、全局、堆。定义在函数外面的block是global的,如果是函数内部的block但是没有捕获任何自动变量,那么它也是全局的。伪代码如下:

int val肯定是在栈上的,我保存了val的地址,看看block调用前后是否变化。输出一致说明是栈上,不一致说明是堆上。有兴趣的可以手动试一下ARC和MRC下的结果。

但是delegate运行成本低,block成本很高。block出栈需要将使用的数据从栈内存拷贝到堆内存,当然是对象的话就是加计数,使用完或者block置nil后才消除。delegate只是保存了一个对象指针,直接回调,没有额外消耗。相对C的函数指针,只多做了一个查表动作。

一般来说公共接口和方法较多用delegate进行解耦,iOS有很多例子如最常用tableViewDelegate,textViewDelegate等。异步和简单的回调用block更好,如常用的网络库AFNetwork等。

OC中的Class

先看一个网上的示例

在Objective-C中任何的类定义都是对象,即在程序启动的时候任何类定义都对应于一块内存。在编译的时候,编译器会给每一个类生成一个且只生成一个描述其定义的对象,也就是苹果公司说的类对象(class object),它是一个单例。

C++等语言中所谓的对象,叫做实例对象(instance object),Objective-C是门很动态的语言,因此程序里的所有实例对象(instace object)都是在运行时由Objective-C的运行时库生成的,而这个类对象(class object)就是运行时库用来创建实例对象(instance object)的依据。实例对象(instance object)的isa指针指向的类对象(class object)里面还有一个isa,类对象(class objec)的isa指向的依然是一个objc-class,它就是元类对象(metaclass object)。

尽管类对象保留了一个类实例的原型,但它并不是实例本身。它没有自己的实例变量,也不能执行那些类的实例的方法(只有实例对象才可以执行实例方法)。然而,类的定义能包含那些特意为类对象准备的方法–类方法(不是实例方法)。类对象从父类那里继承类方法,就像实例从父类那里继承实例方法一样。

类对象(class object)

类对象像其他对象一样,也是id类型。类对象是由编译器创建的,即在编译时所谓的类就是指类对象。任何直接或间接继承了NSObject的类,它的实例对象(instance objec)中都有一个isa指针指向它的类对象(class object)。这个类对象(class object)中存储了关于这个实例对象(instace object)所属的类的定义的一切:包括变量,方法,遵守的协议等等。因此,类对象能访问所有关于这个类的信息,利用这些信息可以产生一个新的实例,但是类对象不能访问任何实例对象的内容。当你调用一个类方法如[NSObject alloc],你事实上是发送了一个消息给他的类对象。类对象是一个功能完整的对象,所以也能被动态识别(dynamically typed),接收消息,从其他类继承方法。特殊之处在于它们是由编译器创建的,缺少它们自己的数据结构(实例变量),只是在运行时产生实例的代理。

元类对象(metaclass object)

类对象是元类对象的一个实例,元类描述了一个类对象就像类对象描述了普通对象一样。不同的是元类的方法列表是类方法的集合,由类对象的选择器来响应。当向一个类发送消息时,objc_msgSend会通过类对象的isa指针定位到元类,并检查元类的方法列表(包括父类)来决定调用哪个方法。元类代替了类对象描述了类方法,就像类对象代替了实例对象描述了实例化方法。元类也是对象,元类是根元类(root class’s metaclass)的实例,而根元类是其自身的实例,即根元类的isa指针指向自身。

类对象(class object)中包含了类的实例变量,实例方法的定义,而元类对象(metaclass object)中包括了类的类方法(也就是C++中的静态方法)的定义。类对象存的是关于实例对象的信息(变量,实例方法等),而元类对象(metaclass object)中存储的是关于类的信息(类的版本,名字,类方法等)。类对象(class object)和元类对象(metaclass object)的定义都是objc_class结构,其不同仅仅是在用途上,比如其中的方法列表在类对象(instance object)中保存的是实例方法(instance method),而在元类对象(metaclass object)中则保存的是类方法(class method)。

当一个消息发送给任何一个对象,方法的检查从对象的 isa 指针开始,然后是父类。实例方法在类中定义,类方法在元类和根类中定义(根类的元类就是根类自己)。在一些计算机语言的原理中,一个类和元类层次结构可以更自由的组成,更深的元类链和从单一的元类继承的更多的实例化的类。Objective-C的类方法是使用元类的根本原因,在其他方面试图在隐藏元类。例如 [NSObject class] 完全相等于 [NSObject self],所以,在形式上他还是返回的 NSObject->isa 指向的元类,Objective-C语言是一组实用的折中方案。

实例对象和类的分析

在objc中不论是实例对象还是Class,都是id类型的对象(Class同样是对象)。实例对象的isa指向它的Class(储存所有减号方法),Class对象的isa指向元类(储存所有加号方法)。向一个对象(id类型)发送消息时,都是从这个对象的isa指针指向的Class中寻找方法,类的super class指向其父类,而元类的super class则指向父类的元类。元类的super class链与类的super class链平行,所以类方法的继承与实例方法的继承也是并行的。而根元类(root class’s metaclass)的super class指向根类(root class)。object_getClass跟随实例的isa指针,返回此实例所属的类,对于实例对象(instance)返回的是类(class),对于类(class)则返回的是元类(metaclass)。

-class方法对于实例对象(instance)会返回类(class),但对于类(class)则不会返回元类(metaclass),而只会返回类本身,即[@"instance" class]返回的是__NSCFConstantString,而[NSString class]返回的是NSString,class_isMetaClass可判断某类是否为元类。

从OC的动态性可以知道,使用objc_allocateClassPair可在运行时创建新的类与元类对,使用class_addMethod和class_addIvar可向类中增加方法和实例变量,最后使用objc_registerClassPair注册后就可以使用此类了。动态语言可以在需要时更改已经定义好的类,由于在编译的时候就为每个类分配了内存空间大小,根据他们的属性,而且这些属性直接保存在名objc_ivar_list的struct容器里,一旦编译后就锁定了大小,所以不能在运行时随意增加,也没有空间再分给他们。之所以category可以是因为他的方法是存在名为objc_method_list的指针指向的struct容器里,由此可以看出属性是全部都是丢在类里,而方法只是把装他们的容器的地址丢在类里,所以属性不能扩展,而方法可以无限制,因为他的大小不影响类的空间。

回到题目,当像MyClass类发送一个实例方法(- responseToSelector)消息时会从它的isa,也就是MyClass元类对象中寻找,由于元类中的方法都是类方法,所以自然找不到。于是沿继承链去父类NSObject元类中寻找,依然没有。由于OC对这块的设计是,NSObject的元类的父类是NSObject类(也就是我们熟悉的NSObject类),其中有所有的实例方法,因此找到了- responseToSelector。NSObject类中的所有实例方法很可能都对应实现了一个类方法(至少从开源的代码中可以看出来),如+ resonseToSelector,但这并非公开的API,如果真的是这样,第2步就可以找到这个方法。 非NSObject的selector这样做是无效的。

Category

Category用于向已经存在的类添加方法从而达到扩展已有类的目的,在很多情形下Category也是比创建子类更优的选择。Category用于大型类有效分解。新添加的方法会被被扩展的类的所有子类自动继承。Category也可以用于替代这个已有类中某个方法的实体,从而达到修复BUG的目的,如此就不能去调用已有类中原有的那个被替换掉方法实体了。需要注意的是,当准备有Category来替换某一个方法的时候,一定要保证实现原来方法的所有功能,否则这种替代就是没有意义而且会引起新的BUG。

Category的方法不一定非要在@implementation中实现,也可以在其他位置实现,但是当调用Category的方法时,依据继承树没有找到该方法的实现,程序则会崩溃。Category理论上不能添加变量,但是可以使用关联属性来弥补这种不足。

Category通常作为一种组织框架代码的工具来使用,通常来说如果需要添加一个新的变量,可以使用子类,如果只是添加一个新的方法,用Category是比较好的选择。

runtime对category的加载过程

下面是runtime中category的结构:

category动态扩展了原来类的方法,在调用者看来好像原来类本来就有这些方法似的,不论有没有import category 的.h文件,都可以成功调用category的方法,都影响不到category的加载流程,import只是帮助了编译检查和链接过程。runtime加载完成后,category的原始信息在类结构里将不会存在。

objc runtime的加载入口是一个叫_objc_init的方法,在library加载前由libSystem dyld调用,进行初始化操作。调用map_images方法将文件中的image map到内存。调用_read_images方法初始化map后的image,这里面干了很多的事情,像load所有的类、协议和category,著名的+ load方法就是这一步调用的。category的初始化,循环调用了_getObjc2CategoryList方法。

在调用完_getObjc2CategoryList后,runtime终于开始了category的处理,首先分成两拨,一拨是实例对象相关的调用addUnattachedCategoryForClass,一拨是类对象相关的调用addUnattachedCategoryForClass,然后会调到attachCategoryMethods方法,这个方法把一个类所有的category_list的所有方法取出来组成一个method_list_t ,这里是倒序添加的,也就是说,新生成的category的方法会先于旧的category的方法插入。

生成了所有method的list之后,调用attachMethodLists将所有方法前序添加进类的方法的数组中,也就是说,如果原来类的方法是a,b,c,类别的方法是1,2,3,那么插入之后的方法将会是1,2,3,a,b,c,也就是说,原来类的方法被category的方法覆盖了,但被覆盖的方法确实还在那里,这也即是我们上面说的Category修复Bug的原理。

Extension

Extension非常像是没有命名的类别,扩展只是用来定义类的私有方法的,实现要在原始的.m里面,还可以用来改变原始属性的一些性质。一般的时候,Extension都是放在.m文件中@implementation的上方,Extension中的方法必须在@implementation中实现,否则编译会报错。Category可以为没有源代码的类添加方法,格式是定义一对.h和.m。Extension作用在管理类中,格式是把代码写到原始类的.m文件中。

iOS视图生命周期

视图控制对象通过alloc和init来创建,但是视图控制对象不会在创建的那一刻就马上创建相应的视图,而是等到需要使用的时候才通过调用loadView来创建,这样的做法能提高内存的使用率。视图使用中用到的方法有init、viewDidLoad、viewWillAppear、viewDidAppear、viewWillDisappear、viewDidDisappear。它们的区别及用途如下:

init: 初始化程序

viewDidLoad: 加载视图

viewWillAppear: UIViewController对象的视图即将加入窗口时调用

viewDidApper: UIViewController对象的视图已经加入到窗口时调用

viewWillDisappear: UIViewController对象的视图即将消失、被覆盖或是隐藏时调用

viewDidDisappear: UIViewController对象的视图已经消失、被覆盖或是隐藏时调用

当一个视图控制器被创建,并在屏幕上显示的时候,代码的执行顺序如下:

  1. alloc 创建对象,分配空间

  2. init (initWithNibName) 初始化对象,初始化数据

  3. loadView 从nib载入视图,通常这一步不需要去干涉。除非你没有使用xib文件创建视图

  4. viewDidLoad 载入完成,可以进行自定义数据以及动态创建其他控件

  5. viewWillAppear 视图将出现在屏幕之前,马上这个视图就会被展现在屏幕上了

  6. viewDidAppear 视图已在屏幕上渲染完成

当一个视图被移除屏幕并且销毁的时候的执行顺序,这个顺序差不多和上面的相反:

  1. viewWillDisappear 视图将被从屏幕上移除之前执行

  2. viewDidDisappear 视图已经被从屏幕上移除,用户看不到这个视图了

  3. dealloc 视图被销毁,此处需要对你在init和viewDidLoad中创建的对象进行释放

在早期的iOS中,发生内存警告的时候如果本视图不是当前屏幕上正在显示的视图的话,viewDidUnload将会被执行,本视图的所有子视图将被销毁,以释放内存,此时开发者需要手动对viewLoad、viewDidLoad中创建的对象释放内存。因为当这个视图再次显示在屏幕上的时候,viewLoad、viewDidLoad 再次被调用,以便再次构造视图。在iOS4之后,系统允许将APP在后台挂起,并将其继续滞留在内存中,因此,viewcontroller不会再调用这个方法来清除内存。

NSNotification效率问题

NSNotification使用的是同步操作,即如果你在程序中的A位置post了一个NSNotification,在B位置注册了一个observer,通知发出后,必须等到B位置的通知回调执行完以后才能返回到A处继续往下执行。因此,不要过多的或者低效的使用NSNotification,推荐的方式是通过一些中间的观察者将通告的结果传递给它们可以访问的对象。

如果想让NSNotification的post处和observer处异步执行,可以通过NSNotificationQueue实现。对于同一个通知,如果注册了多个观察者,则这多个观察者的执行顺序和他们的注册顺序是保持一致的。

NSNotificationCenter在转发NSNotification消息的时候,在哪个线程中post,就在哪个线程中转发。换句话说,不管你的observer是在哪个线程,observer的回调方法执行线程都和post的线程保持一致,同时NSNotification是线程阻塞的,即走完通知后才继续往下执行。如果想让post的线程和转发的线程不同,可以通过NSNotification重定向技术实现。一种重定向的实现思路是自定义一个通知队列,让这个队列去维护那些我们需要重定向的Notification。当Notification来了时,根据需要将这个Notification存储到我们的队列中,并发送一个信号(signal)来告诉这个线程需要处理一个Notification。指定的线程在收到信号后,将Notification从队列中移除,并进行处理。

示例如下:

如此,在全局dispatch队列中抛出的Notification就在主线程中接收到了。

KVO,NSNotification,delegate及block区别

KVO是cocoa框架实现的观察者模式,一般同KVC搭配使用,通过KVO可以监测一个值的变化,比如View的高度变化。是一对多的关系,一个值的变化会通知所有的观察者。

NSNotification是通知,也是一对多的使用场景。在某些情况下,KVO和NSNotification是一样的,都是状态变化之后告知对方。NSNotification的特点,就是需要被观察者先主动发出通知,然后观察者注册监听后再来进行响应,比KVO多了发送通知的一步,但是其优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,使用也更灵活。

delegate 是代理,就是我不想做的事情交给别人做。比如猫需要吃饭,就通过delegate通知主人,主人就会给他做饭、盛饭、倒水,这些操作,这些猫都不需要关心,只需要调用delegate(代理人)就可以了,由其他类完成所需要的操作。所以delegate是一对一关系。

block是delegate的另一种形式,是函数式编程的一种形式。使用场景跟delegate一样,相比delegate更灵活,而且代理的实现更直观。

KVO一般的使用场景是数据,需求是数据变化,比如股票价格变化,我们一般使用KVO(观察者模式)。delegate一般的使用场景是行为,需求是需要别人帮我做一件事情,比如买卖股票。Notification一般是进行全局通知,比如利好消息一出,通知大家去买入。delegate是强关联,就是委托和代理双方互相知道,你委托别人买股票你就需要知道经纪人,经纪人也要知道自己的顾客。Notification是弱关联,利好消息发出,你不需要知道是谁发的也可以做出相应的反应,同理发消息的人也不需要知道接收的人也可以正常发出消息。

__weak

当一个 __weak 类型的指针指向的对象被释放时,该指针会自动被置成nil,因此__weak关键字修饰的指针又被称为智能指针。

也就是说,让obj1指针指向的内容变成空。

实际上,objc_storeWeak函数会把第二个参数的对象的地址作为key,并将第一个参数(__weak关键字修饰的指针的地址)作为值,注册到weak表中。如果第二个参数为0(说明对应的对象被释放了),则将weak表中将整个key-value键值对删除,这就是__weak关键字的核心思想!

weak表和引用计数表类似,都是通过hash表实现的。如果使用weak表,将被释放的对象地址作为key去检索,就能很高效的获取对应的指向该对象的类型为__weak的指针变量的地址。一个对象可能有多个__weak指针指向,因此一个对象地址key可能对应多个值。

在调用对象的release方法时,会在其中一步调用objc_clear_deallocating函数,该函数会执行以下操作:以当前对象的地址作为key,从weak表中获取对应的值,即指向该对象的__weak类型的指针变量;将取到的所有指针变量的值赋值为nil;从weak表中删除该key对应的整条记录。如果大量使用附有__weak修饰符的变量会消耗响应的CPU资源,因此应该尽量少使用__weak修饰符。

nil、Nil、NULL、NSNull

NULL声明位置在文件,对于普通的iOS开发者来说,通常NULL的定义就是:

NSNull声明位置在NSNull.h文件,定义为:

从前面的介绍可以看出,不管是NULL、nil还是Nil,它们本质上都是一样的,都是(void *)0,只是写法不同。这样做的意义是为了区分不同的数据类型,比如你一看到用到了NULL就知道这是个C指针,看到nil就知道这是个Objective-C对象,看到Nil就知道这是个Class类型的数据。另外,NULL是C指针指向的值为空;nil是OC对象指针自己本身为空,不是值为空。

开发过程中,我们通过http请求后台返回json数据,而有时数据里某一字段的值为null,然后我们把此值赋值给NSArray、NSdictionary或是NSString会判断此值是否为空,做的处理通常是:

但事实并不这么简单。当字典、数组为空时,后台打印的输出结果是这样:

null.png

这就需要在代码判断时利用[NSNull null]来判断。

就可以了。

图像处理中的UI、CG和CI

UIImage、CGImage和CGImageRef

UIImage虽然可以加载、显示各种格式的位图,甚至可以同时加载图片,接下来依次播放多张图片形成动画。但UIImage不能对图片进行缩放、旋转,不能"挖取"源图片的指定区域等,这些功能可借助Quartz 2D的CGImageRef来实现。UIImage与CGImageRef之间可以相互转换,CGImageRef并不是面向对象的API,也不是类,只是一个指针类型,Quartz 2D对CGImageRef的定义为:

系统会维护一个CGContextRef的栈,而UIGraphicsGetCurrentContext()会取栈顶的CGContextRef,正确的做法是只在drawRect里调用UIGraphicsGetCurrentContext(),因为在drawRect之前,系统会往栈里面压入一个valid的CGContextRef,除非自己去维护一个CGContextRef,否则不应该在其他地方取CGContextRef。

UIColor、CGColor和CIColor区别和联系

UIColor是UIKit中存储颜色信息的一个重要的类,一个UIColor对象包含了颜色和透明度的值,它的颜色空间已经针对iOS进行了优化。UIColor包含了一些类方法用于创建一些最常见的颜色,如白色,黑色,红色,透明色等,这些颜色的色彩空间也不尽相同(白色和黑色是kCGColorSpaceDeviceGray,红色的色彩空间是kCGColorSpaceDeviceRGB)。UIColor有两个重要的属性:一个是CGColor,一个是CIColor之后添加)。

CGColor主要用于CoreGaphics框架之中,CGColor其实是个结构体,而我们通常在使用的CGColor的时候使用的是它的引用类型CGColorRef。CGColor主要由CGColorSapce和Color Components两个部分组成,同样的颜色组成,如果颜色空间不同的话,解析出来的结果可能会有所不同,这就像我们在处理图片数据的时候,如果把RGBA格式当成BGRA格式处理的结果一样。在Quartz 2D中CGColor常用来设置context的填充颜色,设置透明度等。

CIColor主要用于和Core Image框架中其他类交互,比如CIFilter,CIContext以及CIImage。CIColor中颜色值的范围是0.0-1.0之间,0.0代表该颜色分量为最小值,1.0代表改颜色分量为最大值。其中alpha值的范围也是0.0到1.0之间,0.0代表全透明,1.0代表完全不透明,CIColor的颜色分量通常都是没有乘以alpha值。可以使用initWithCGColor:函数,通过CGColor创建一个CIColor。其中传入的CGColorRef对象可以使任何任何颜色空间,但是Core Image框架会在传入filter kernel之前把所有的颜色空间转换到Core Image工作颜色空间。Core Image工作颜色空间使用三个颜色分量加上一个alpha分量组成(其实就是kCGColorSpaceDeviceRGB)。

UIColor的CGColor总是有效的,不管它是通过CGColor、CIColor还是其他方法创建的,CGColor属性都总是有效的。但是CIColor属性就不总是有效的了,只有当UIColor是通过CIColor创建的时候,它才是有效的,否则访问该属性将会抛出异常。

当UIColor使用CGColor初始化的时候,所有CGColorRef包含的信息,都会被原封不动的保留,其中就包括Color space,而且通过下面的小例子我们还可以看到如果使用CGColor初始化UIColor的时候,UIColor其实是直接保留了一份这个CGColorRef对象。

当使用CIColor来初始化一个UIColor的时候,再去访问UIColor的CGColor属性的时候,我们会发现CGColor的color Space和设置CIColor的color space的是不完全一样的,在这个过程中CIColor会为我们做一个转换。使用kCGColorSpaceDeviceGray,kCGColorSpaceDeviceRGB,kCGColorSpaceDeviceCMYK三种颜色空间来初始化一个CIColor的时候,使用该CIColor去初始化一个UIColor,然后再去访问其CIColor属性、CGColor属性,获取颜色空间并打印颜色信息。示例如下:

1) 使用kCGColorSpaceDeviceGray初始化CIColor

通过运行程序,我们看出来,如果使用一个kCGColorSpaceDeviceGray的颜色空间的CGColor来初始化CIColor的时候,我们可以看到CIColor的色彩空间一直是kCGColorSpaceDeviceGray,通过访问UIColor的CIColor属性,我们可以看到其颜色空间仍然是kCGColorSpaceDeviceGray,但是当访问UIColor的CGColor属性的时候,通过打印可以发现其色彩空间已经转变成了kCGColorSpaceDeviceRGB空间了,而颜色值也正确的从原来的颜色空间转换到了新的颜色空间。

2) 使用kCGColorSpaceDeviceRGB初始化CIColor

整个过程中CIColor,以及通过UIColor的CGColor和CIColor属性访问到的值,打印出来我们可以发现它们都是kCGColorSpaceDeviceRGB空间的。

3) 使用kCGColorSpaceDeviceCMYK初始化CIColor

当我们用一个CMYK颜色空间的CGColor来初始化CIColor的时候,CIColor的颜色空间依然是CMYK,但是颜色值已经转换成RGB的颜色值。当使用该CIColor创建一个UIColor的时候,我们再通过CIColor和CGColor属性打印信息的时候,我们会发现CIColor的色彩空间依然是CMYK,但是CGColor打印所得到的信息说明它已经被转换成RGB空间了。

创建一个CGColor

最常用的函数是CGColorCreate,该函数有两个参数:

  1. colorspace,指定CGColor对应的颜色空间,Quartz就会retain该对象,因此调用完之后你就可以安全的释放该对象。

  2. components,一个CGFloat的数组,该数组的元素个数是指定色彩空间包含的颜色分量数n,加上对应的alpha值。 

该函数该返回一个新创建的CGColorRef,当我们不再使用该对象的时候使用CGColorRelease函数释放该对象。

在我们创建的时候传入两个重要的参数进去,当我们获取到了CGColorRef以后当然就可以拿到对应的ColorSpace以及Components。

1) 获取ColorSpace

通过CGColorGetColorSpace函数我们可以获取到当前CGColorRef对应的ColorSpace,该函数只接受一个参数就是你要获取ColorSpace的CGColorRef。

判断两个颜色是否相等

不管UIColor使用CIColor、CGColor还是其他方式初始化的,其CGColor属性都是可用的。CoreGraphics中提供一个方法可以判断两个CGColor是否相等,因此我们可以通过判断两个UIColor是否相等。

例子中第一部分是判断两个白色的UIColor是否相等,虽然都是白色,但是颜色空间是不一样的。例子的第二部分简单的创建了两个RGB空间的UIColor,运行程序可以看出,这两种颜色是相同的。

时间复杂度和空间复杂度

按数量级递增排列,常见的时间复杂度有:常数阶O(1),对数阶O(log2n),线性阶O(n),线性对数阶O(nlog2n),平方阶O(n2),立方阶O(n3)...,k次方阶O(nk),指数阶O(2n)。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。

求时间复杂度

如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。

此算法中的语句3的频度不仅与问题规模n有关,还与输入实例中A的各元素取值及K的取值有关: 若A中没有与K相等的元素,则语句3的频度f(n)=n;若A的最后一个元素等于K,则语句3的频度f(n)是常数0。

空间复杂度

一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分。

  1. 固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。

  2. 可变空间,这部分空间主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。

一个算法所需的存储空间用f(n)表示。S(n)=O(f(n))其中n为问题的规模,S(n)表示空间复杂度。

文/吴白(简书作者)

原文链接:

更多内容请关注:“e安在线”微信公众号

责任编辑: 鲁达

1.内容基于多重复合算法人工智能语言模型创作,旨在以深度学习研究为目的传播信息知识,内容观点与本网站无关,反馈举报请
2.仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证;
3.本站属于非营利性站点无毒无广告,请读者放心使用!

“runtime如何实现weak变量的自动置nil”边界阅读