1
2
3
4
5
6
7
8
9
10 1、为什么说Objective-C是一门动态的语言
①、概念介绍
②、Objective-c的动态性
Objective-C 可以通过Runtime 这个运行时机制,在运行时动态的添加变量、方法、类等,所以说Objective-C 是一门动态的语言。
ObjC之所以说是面向Runtime的语言,最重要的特征是其消息传递机制。
Objective-C的动态性,主要体现在3个方面:动态类型、动态绑定、动态载入。
2、消息转发
3、为什么说 Objective-C 没有真正的私有方法和私有变量
4、消息结构
1 1、浅谈runtime运行时机制
## 一、为什么说Objective-C是一门动态的语言? > [< 返回目录](#目录)
1
2
3
4
5
6 1、#import、#include、@class](#import、#include、@class
2、const 与define使用的区别
3、==、 isEqualToString、isEqual区别
4、字符串的比较
5、isMemberOfClass 和 isKindOfClass 的联系和区别
6、nil, Nil,NULL 与 NSNull 的区别
问:我们说的OC是动态运行时语言是什么意思?
1、之所以叫做动态,是因为必须到运行时(run time)才会做一些事情。主要是将数据类型的确定由编译时,【推迟到了运行时】。简单来说, 运行时机制使我们直到运行时才去决定一个对象的类别,以及调用该类别对象指定方法。
所以说NSString *obj = [[NSData alloc] init] 在编译时是NSString类型,在运行时是NSData类型。2、另外,OC还可以通过Runtime 这个运行时机制,在运行时【动态的添加】变量、方法、类等。
1、Objective-C是一门”动态”的语言,动态语言是什么意思?
OC中的对象,都是用指针表示
OC中方法的调用,是基于消息机制实现
①、”动态”概念介绍
(1)动态类型语言:动态类型语言是指在运行期间才去做数据类型检查的语言,Python和Ruby就是一种典型的动态类型语言,其他的各种脚本语言如VBScript也多少属于动态类型语言。(自己概括为动态语言指的是不需要在编译时确定所有的东西,在运行时还可以动态的添加变量、方法和类,而Objective-C 可以通过Runtime 这个运行时机制,在运行时动态的添加变量、方法、类等,所以说Objective-C 是一门动态的语言)
(2)静态类型语言:静态类型语言与动态类型语言刚好相反,它的数据类型是在编译其间检查的,也就是说在写程序时要声明所有变量的数据类型,C/C++是静态类型语言的典型代表,其他的静态类型语言还有C#、JAVA等。
所以说NSString *obj = [[NSData alloc] init] 在编译时是NSString类型,在运行时是NSData类型。
②、Objective-c的动态性
Objective-C的动态性,让程序在运行时判断其该有的行为,而不是像c等静态语言在编译构建时就确定下来。Objective-C的动态性主要体现在3个方面:动态类型、动态绑定、动态载入。
1.动态类型:如id类型。
简单点的如id类型在编译器编译的时候是不能被识别的,要等到运行时(run time),即程序运行的时候才会根据语境来识别。
1
2
3
4id obj = someInstance;
if ([obj isKindOfClass:someClass]) {
someClass *classSpecifiedInstance = (someClass *)obj;
}2.动态绑定:
在OC中,其实是没有函数的概念的,我们叫“消息机制”,所谓的函数调用就是给对象发送一条消息。
OC可以先跳过编译,在运行时才决定要调用什么方法,需要传什么参数进去,这就是动态绑定。要实现他就必须用SEL变量绑定一个方法。最终形成的这个SEL变量就代表一个方法的引用。(这里要注意一点:SEL并不是C里面的函数指针,虽然很像,但真心不是函数指针。SEL变量只是一个整数,他是该方法的ID,@selector()就是取类方法的编号。以前的函数调用,是根据函数名,也就是 字符串去查找函数体。但现在,我们是根据一个ID整数来查找方法,整数的查找字自然要比字符串的查找快得多!所以,动态绑定的特定不仅方便,而且效率更高。)3.动态载入。
根据需求加载所需要的资源,这点很容易理解,对于iOS开发来说,基本就是根据不同的机型做适配。最经典的例子就是在Retina设备上加载@2x的图片,而在老一些的普通屏设备上加载原图。
笔试题:对于语句NSString *obj = [[NSData alloc] init]; obj在编译时和运行时分别是什么类型的对象?
答:编译时是NSString的类型;运行时是NSData类型的对象。
2、怎么理解Objective-C”运行时”
基于Runtime的消息传递机制
ObjC之所以说是面向Runtime的语言,最重要的特征是其消息传递机制。
什么是消息传递?消息传递不就和C语言的函数调用一个意思么?
在C语言中,我们调用函数时,必须先声明函数(或者自上而下),而实际上,声明函数就是获取函数地址,调用函数就是直接跳到地址执行,代码在被编译器解析、优化后,便是成为一堆汇编代码,然后连接各种库,完了生成可执行的代码(即是静态的)。
在ObjC中,首先要搞清楚为什么不用Function Call 而用 Messaging 呢?一般调用函数(方法),讲的就是object对象,
事情证明,在编译阶段,oc可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错,而c语言在编译阶段或报错
1,给分类添加属性
OC分类不能添加属性,如果你在分类中添加了属性,编译器就会报警告。(记得导入 #import <objc/runtime.h>)
// UIButton+CJMoreProperty.h

// UIButton+CJMoreProperty.m
为分类添加属性,就用运用到OC的运行时来解决。
1 | // 设置关联 |

2、消息转发
ObjC之所以说是面向Runtime的语言,最重要的特征是其消息传递机制
1 | Runtime的特性主要是消息(方法)传递,如果消息(方法)在对象中找不到,就进行转发。 |
延展:消息转发机制
因为在运行期还可以继续向类中添加方法,所以编译器在编译时还无法确定类中是否有某个方法的实现。对于类无法处理一个消息就会触发消息转发机制。
消息转发步骤(如果前一套方案实现后一套方法就不会执行)
方案一:首先,系统会调用resolveInstanceMethod(当然,如果这个方法是一个类方法,就会调用resolveClassMethod)让你自己为这个方法增加实现
1 | + (BOOL)resolveInstanceMethod:(SEL)sel { |
方案二:如果不去对方案一的resolveInstanceMethod做任何处理,直接调用父类方法。可以看到,系统已经来到了forwardingTargetForSelector方法,这个方法返回你需要转发消息的对象。
1 | //返回你需要转发消息的对象 |
方案三:如果如果我们也不实现forwardingTargetForSelector,系统就会调用方案三的两个方法methodSignatureForSelector和forwardInvocation。其中methodSignatureForSelector用来生成方法签名,这个签名就是给forwardInvocation中的参数NSInvocation调用的。
1 | //生成方法签名 |
消息转发分为两大阶段:
“动态方法解析”:先征询接收者,所属的类,能否动态添加方法,来处理这个消息,若可以则结束,如不能则继续往下走
“完整的消息转发机制”:
请接收者看看有没其他对象能处理这条消息,若有,就把这个消息转发给那个对象然后结束
运行时系统会把与消息有关细节全部封装到NSInvocation 对象中,再给对象最后一次机会,令其设法解决当前还未处理的这条消息
其他文章:NSInvocation简单使用
在 iOS中可以直接调用 某个对象的消息 方式有2中
一种是performSelector:withObject:
再一种就是NSInvocation
第一种方式比较简单,能完成简单的调用。但是对于>2个的参数或者有返回值的处理,那就需要做些额外工作才能搞定。那么在这种情况下,我们就可以使用NSInvocation来进行这些相对复杂的操作。
NSInvocation可以处理参数、返回值。会java的人都知道凡是操作,其实NSInvocation就相当于反射操作。
1 | //如果此消息有参数需要传入,那么就需要按照如下方法进行参数设置,需要注意的是,atIndex的下标必须从2开始。原因为:0 1 两个参数已经被target 和selector占用 |
3、为什么说 Objective-C 没有真正的私有方法和私有变量
以下内容摘自:为什么说 Objective-C 没有真正的私有方法和私有变量
私有的定义:私有是指只能够在本类内部使用或访问,但是不能在类的外部被访问。
在 Objective-C 中,对象调用方法是以发送消息的形式实现的。所有方法的调用最终都会转化为发送消息的形式,原型如下:
id objc_msgSend(id self, SEL op, …)
Objective-C 为什么能够实现访问「私有方法」呢?其实这跟 Objective-C 语言的动态特性有密切的关系,对象在运行的时候才会去查找方法。Objective-C 对象有一个 isa 指针指向其父类,在向该实例发送消息的时候,若它自己不能识别回到父类中去查找该消息。
综上,OC中其实并无真正意义上的的私有方法和私有属性。但是在实际使用中,我们应遵守规则,不能调用的方法,不调用。
4、消息结构
Objective-C 使用的是 “消息结构” 并非 “函数调用”。(使用消息结构的的语言,其运行时所应执行的代码由运行期决定;而使用函数调用的语言,则由编译器决定)
以下内容摘自:OC消息机制和动态运行时
1 | struct objc_class |
Runtime 4种用法
1 | 1)替换系统方法,[数组越界苹果就会直接crash](http://blog.csdn.net/u012103785/article/details/50817876) |
OBJC_ASSOCIATION_ASSIGN造成的崩溃
objc_setAssociatedObject 关联对象的学习
为CJKeyboardAvoidingTableView增加cjKeyboardAvoidingOffset属性的时候,使用OBJC_ASSOCIATION_ASSIGN而造成崩溃,参考FDTemplateLayoutCell源码,改成使用OBJC_ASSOCIATION_RETAIN,问题解决。原因是
详情查询:objc_setAssociatedObject 关联对象的学习
1、浅谈runtime运行时机制
OC是运行时语言,只有在程序运行时,才会去确定对象的类型,并调用类与对象相应的方法。利用runtime机制让我们可以在程序运行时动态修改类、对象中的所有属性、方法。
iOS运行时初探-使用运行时机制向Category中添加属性
在一般情况下,我们是不能向Category中添加属性的,只能添加方法,但有些情况向,我们确实需要向Category中添加属性。而且很多系统的API也有一些在Category添加属性的情况,例如我们属性的UITableView的section和row属性,就是定义在一个名为NSIndexPath的分类里的,如下:
那这到底是怎么实现的呢?
+load +initialize的区别
\1. load方法只会调用一次
\2. +(void)load方法:程序一运行就会把所有的类加载进内存,调用这个方法
(表叙:只要程序一运行,就会调用类的load方法,目的:把这个类加载进内存)
\3. load是只要类所在文件被引用就会被调用,而initialize是在类或者其子类的第一个方法被调用前调用,所以没有被引用进项目,就不会调用load方法,但即使类文件只被引用进来,而没有使用,那么initialize也不会被调用
+load方法的调用是在main() 函数之前。load 方法内部一般用来实现 Method Swizzle(面向切面AOP变成)
调用main() 函数之前的什么时候?答:+load方法会再这个类或者其分类被添加到运行时的时候调用。
附:所有类的 load 方法都会被调用,先调用父类、再调用子类,多个分类会按照Compile Sources 顺序加载。
initialize一般在main()函数之后。Initialize方法一般用来初始化全局变量或者静态变量。
问是在main() 函数之后的什么时候?答:在第一次主动使用当前类的时候。
附:initialize一般在main()函数之后的情况是什么?A类的 load 方法中调用 B 类的类方法,导致在调用A的Load 方法之前,会先调用一下B类的initialize 方法,但是B类的load 方法还是按照 Compile Source 顺序进行加载
附2:Initialize 方法会被覆盖,子类父类分类中只会执行一个
load和initialize都不能主动调用。
initialize不是init,initialize在程序运行过程中,它会在你程序中每个类调用仅一次initialize。这个调用的时间发生在你的类接收到消息之前,但是在它的父类接收到initialize之后。
init则是你手动初始化几次就调用几次,和普通方法一样
三、KVC与KVO
1、KVO
KVO的概述与使用 内容如下
一、概述
KVO,即键值观察者(key-value-observing),它提供一种观察者的机制,当指定的对象的属性被修改后,则对象就会接受到通知。简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了。
二、KVO原理/本质
Aspect 的实现很想 KVO的本质:详见 Aspect 与 消息转发
① KVO是使用isa-swizzling的技术实现的:当某个类的对象第一次被观察时,系统在运行时会通过Runtime动态创建一个该类的派生类,并将这个对象的isa指针指向这个派生类,派生类的isa指针指向原来的类,这样修改对象a的属性即对象a调用set方法就会变成是调用的是NSKVONotifying_A这个类的set方法。
②随后在该派生类中重写了该对象的setter方法,并且在setter方法中实现了通知的机制。
派生类重写了class方法,以“欺骗”外部调用者他就是原先那个类。系统将这个类的isa指针指向新的派生类,因此该对象也就是新的派生类的对象了。因而该对象调用setter就会调用重写的setter,从而激活键值通知机制。此外派生类还重写了delloc方法来释放资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
- (void)cjl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block {
......
//2、动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
object_setClass(self, newClass); // 将self的isa指向由原有类改为指向创建出来的派生类/中间类
// 给子类NSKVONotifying_A添加方法一个名为setterSel的方法,这个方法的实现是cjl_setter。
SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
Method method = class_getInstanceMethod([self class], setterSel);
const char *type = method_getTypeEncoding(method);
class_addMethod(newClass, setterSel, (IMP)cjl_setter, type);
}
static void cjl_setter(id self, SEL _cmd, id newValue){
......
}
- (Class)createChildClass {
// 动态创建子类
NSString *oldClassName = NSStringFromClass([self class]); //获取原本的类名
NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@", oldClassName];//拼接新的类名
Class newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
objc_registerClassPair(newClass);
// 给子类NSKVONotifying_A添加方法一个名为class的方法,这个方法的实现是cjl_class。
SEL classSel = @selector(class);
Method classMethod = class_getInstanceMethod([self class], classSel);
const char *classType = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSel, (IMP)cjl_class, classType);
return newClass;
}
Class cjl_class(id self, SEL _cmd){
return class_getSuperclass(object_getClass(self));//通过[self class]获取会造成死循环
}以上实现参考自:iOS-底层原理 23:KVO 底层原理 自定义KVO 、 iOS 探究 OC对象、isa指针及KVO实现原理
其他参考:KVO原理及自定义KVO
答. 当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现,set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。
如何手动触发KVO 答. 被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。
isa指针
isa是结构体指针,只要是OC对象都有isa
- 实例对象的isa指向它的类对象。(让对象知道自己是属于哪个类的实例。通过 isa 指针,Runtime 可以在该类的方法列表中查找方法实现。)
- 类对象的isa指向元类对象,当调用类方法时,会去元类中查找方法。(元类是类的类,它存储了类的方法和属性信息。为什么要设计元类,为了复用消息传递功能)。
- 元类对象的 isa 指向 NSObject 的元类,最终指向 nil,形成一个闭环。
当调用对象方法时,实例对象内存并没有包含对象方法,而是通过它内部的isa指针找到它的类对象,从而在类对象中找到对象方法的实现进行调用;
当调用类方法时,类对象并没有类方法的信息,而是通过类对象的isa找到元类对象,最后找到类方法的实现进行调用
- instance调用对象方法的轨迹:isa找到class,方法不存在,就通过superclass找父类
- class调用类方法的轨迹:isa找meta-class,方法不存在,就通过superclass找父类
三、使用方法
系统框架已经支持KVO,所以程序员在使用的时候非常简单。
- 注册,指定被观察者的属性,
- 实现回调方法
- 移除观察
四,实例:
假设一个场景,股票的价格显示在当前屏幕上,当股票价格更改的时候,实时显示更新其价格。
2、KVC
KVC 就是键值编码(key-value-coding)
KVC 的主要作用:
(1)通过键值路径为对象的属性赋值。主要是可以为私有的属性赋值。
(2)通过键值路径获取属性的值。主要是可以通过key获得私有属性的值。
1 | // valueForKeyPath:(多级访问,不仅可以对当前对象属性进行赋值,也可以对“深层”对象的属性进行取值) |
其他参考资料:
## 四、Objective-C和C++ > [< 返回目录](#目录)1、Objective-C与C++之间的差异
最大的区别就是Objective C的成员函数调用是基于动态绑定的,类本身并没有限制对象可以有什么函数。相对于C++类会规定好成员有什么函数。这使得Objective C的灵活性很大,但是执行效率也很低。
2、Object-c的类可以多重继承么?
答: Object-c的类不可以多重继承;
面向对象(OOP)
面向对象的三个特性:封装、继承和多态(没有继承就没有多态)。
所谓封装,也就是把客观事物封装成抽象的类;
所谓多态就是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。
没有继承就没有多态。代码的体现:父类类型的指针指向子类对象。
没使用多态前:
1 | Person.m |
使用多态后:
1 | Person.m |
1、#import、#include、@class
可参考:#import、#include、@class、#import<>和#import””的区别
1、#import介绍:
In the C language, the #include pre-compile directive always causes a file’s contents to be inserted into the source. at that point, Objective-C has the equivalent #import directive except each file is included only once per compilation unit, obviating(去除) the need for include guards.
即在C语言中,#include预编译指令经常引起一个文件的被重复引用。基于这一点,OC有一个等价的预编译指令#import来防止文件重复导入。所以OC给出来的新的预处理指令import是防止重复导入,避免程序员去书写头文件卫士,降低程序员的负担。
2、#import和#include的异同点:
①#include是C语言中的,需要条件编译语句控制重复包含问题。
②在Objective-C中,#import是OC中对#include的改进版本,#import使得我们不需要写条件编译语句就可以确保引用的文件只会被包含一次,不会陷入递归版包含的问题。即import与#include比较的优点:会自动防止重复导入,使得不会引起交叉编译
#import不会引起交叉编译的问题。因为在Objective-C中会存在C/C++和Object-C混编的问题,如果用#include引入头文件,会导致交叉编译。
举例:三个文件①文件A.h、②文件B.h、③文件C.h。其中文件C.h需要引入A.h、B.h,而文件B.h也需要引入文件A.h,这样在C.h中就重复引用了A.h两次,使用#import可以进行优化
3、#import和#include的相同点:
①都可以用在OC程序中起到导入文件的作用
②同样的 包含系统文件都是<>,是包本地文件都用””
例如:系统文件#import<Foundation/Foundation.h>, #include<stdio.h>
本地文件#import”test.h”, #include”test.h”
到底该如何选择使用呢
一般来说,你包含的是C语言中的文件就用#include,你用的是OC中的文件就用#import
4、**#import与@class二者的区别**
#import会链入该头文件的全部信息,包括实体变量和方法等;而@class只是告诉编译器,其后面声明的名称是类的名称,至于这些类是如何定义的,暂时不用考虑。在头文件中, 一般只需要知道被引用的类的名称就可以了。不需要知道其内部的实体变量和方法,所以在头文件中一般使用@class来声明这个名称是类的名称。 而在实现类里面,因为会用到这个引用类的内部的实体变量和方法,所以需要使用#import来包含这个被引用类的头文件。
附:@class能解决循环包含的问题:当两个类文件有循环依赖关系 (A引用B,B引用A)时,需要用@class
5、import<> 和 import””
①<> : 引用系统文件,它用于对系统自带的头文件的引用,编译器会在系统文件目录下去查找该文件.
②””: 用户自定义的文件用双引号引用,编译器首先会在用户目录下查找,然后到安装目录中查
4、字符串的比较
;
字符串compare的比较是一个字符串一个字符串比较的,所以才会出现1814比860小的错误情况。
5、isMemberOfClass 和 isKindOfClass 的联系和区别
联系:两者都能检测一个对象是不是某个类的成员
区别:isKindOfClass不仅能确定一个对象是不是某个类(即不算父类)的成员,也能确定一个对象是否是派生自该类的类的成员,而isMemberOfClass只能做到前者.
a.为什么基本类型和C数据类型的修饰用assign?
因为基本数据类型不是对象,不需要引用计数,所以使用assign(附:在内存中创建和使用后,在一个方法体结束后就被删除。)。
iOS NS/CG/CF前缀代表什么意思
NS/CG/CF前缀代表什么意思。
回答:NS是前缀,代表它是来自Cocoa Foundation框架,这个框架是OC的基础库
UI—UIKit框架,UI库
CF—Core Foundation框架
NS—Cocoa Foundation框架,OC的基础库。
CG—CoreGraphics.frameworks框架,用于渲染的库。
CA—CoreAnimation.frameworks框架
swift @EnvironmentObject 等于全局变量?
在SwiftUI中,@EnvironmentObject 并不是全局变量,但它确实提供了一种在多个视图之间共享和访问数据的方式,这在某种程度上类似于全局变量的作用。不过,它们之间还是有一些关键的区别:
- 所有权和生命周期:
- 全局变量:在程序的整个生命周期内都存在,并且全局可访问。
- @EnvironmentObject:需要一个环境对象提供者(通常是视图树中的父视图)来创建和管理其实例。它只在提供者的子视图中可访问,并且其生命周期受限于提供者。
- 访问控制:
- 全局变量:任何地方都可以访问,这可能导致难以追踪变量的使用情况和修改点,从而增加代码的复杂性和出错概率。
- @EnvironmentObject:只能在环境对象被提供的子视图中访问,这有助于限制数据的访问范围,使得数据流和状态变化更容易理解和跟踪。
- 类型安全和可组合性:
- 全局变量:通常不是类型安全的,因为它们可以在任何地方被任何类型的代码访问和修改。
- @EnvironmentObject:是类型安全的,因为它要求使用特定的对象类型,并且可以通过SwiftUI的视图组合模型来管理。
- 响应性:
- 全局变量:不是响应式的,意味着它们的状态变化不会触发UI的更新。
- @EnvironmentObject:是响应式的,基于
ObservableObject协议。当环境对象的状态发生变化时,依赖于这个环境对象的视图会自动更新。
- 依赖注入:
- 全局变量:不直接支持依赖注入。
- @EnvironmentObject:支持依赖注入,可以通过环境对象提供者来注入所需的依赖。
总结来说,@EnvironmentObject 提供了一种更加模块化和响应式的方式来共享和管理状态,而不是使用全局变量。它有助于构建更清晰、更易于维护的代码结构,尤其是在复杂的应用程序中。
END
1 |
;