Oc语言学习 —— 重点内容总结与拓展(下)

目录 类别(分类)和拓展 三种字符串的管理方式 NSCFConstantString Constant 常量 NSCFString NSTaggedPointerString 三种字符串类型的copy/ mutablecopy / retainCount情况 内存分布补充 NSC

作者 TommyWu
封面圖片: Oc语言学习 —— 重点内容总结与拓展(下)

目录

类别(分类)和拓展

三种字符串的管理方式

__NSCFConstantString

Constant-> 常量

__NSCFString

NSTaggedPointerString

三种字符串类型的 copy/ mutablecopy / retainCount 情况

内存分布补充

_NSCFConstantString

__NSCFString

NSTaggedPointerString

多态

二、指针类型强制转换

​协议与委托

规范、协议与接口

使用类别实现非正式协议

正式协议

遵守(实现协议)

协议的意义

正式协议与非正式协议

协议与委托

​深拷贝与浅拷贝

#类别(分类)和拓展

1、拓展在 编译时 就被添加到类中,而分类则是 运行时 才被整合到类信息中。

2、合并后的分类数据(方法,属性,协议),会被插入到原来数据的前面。也就是说当分类的方法与原始类的方法重名时,会先去调用分类中实现的方法。

分类:

        专门用来给类添加新方法

        不能给类添加成员属性,添加成员属性也无法取到

        注意:其实可与通过 runtime 给分类添加属性,即属性关联,重写 setter,getter 方法

        分类中用 @property 定义变量,只会生成变量的 setter,getter 方法的声明,不能生成方法实现和带下划线的成员变量

拓展:

        可以说是特殊的分类,也可以称为匿名分类

        可以给类添加成员属性,但是是私有变量

        可以给类添加方法,也是私有方法

        拓展只能本类来用        

#三种字符串的管理方式

NSString 的三种类型:

实际类型 说明 特点 __NSCFConstantString 编译时字面量字符串 常量区,不可变 NSTaggedPointerString 小型字符串优化类型 存储在指针本身中,提高性能 __NSCFString NSMutableString 或动态创建的 NSString 存储在堆上,可变或不可变

#__NSCFConstantString

#Constant-> 常量

通俗理解其就是常量字符串,是一种编译时常量。 这种对象存储在字符串常量区。

通过打印起 retainCount 的值,发现很大,2^64 - 1,测试证明对其进行 release 操作时,retainCount 不会产生任何变化是创建之后便无法释放掉的对象。

当我们使用不同的字符串对象进行创建时当内容相同,其对象的地址也相同,这也就证明了常量字符串是一种 ** 单例 ** 。

这种对象一般通过字面值 @”…”、CFSTR (”…”) (一种宏定义创建字符串的方法) 或者 stringWithString: 方法(现在会报警告⚠️这个方法等同于字面值创建的方法)。

#__NSCFString

和 __NSCFConstantString 不同, __NSCFString 对象是在运行时创建的一种 NSString 子类,他并不是一种字符串常量。所以和其他的对象一样在被创建时获得了 1 的引用计数。

这种对象被存储在堆上。

通过 NSString 的 stringWithFormat 等方法创建的 NSString 对象一般都是这种类型。 如果字符串长度大于 9 或者如果有中文或其他特殊符号(可能是非 ASCII 字符)存在的话则会直接成为 __NSCFString 类型

#NSTaggedPointerString

理解这个类型,需要明白什么是标签指针,这是苹果在 64 位环境下对 NSString,NSNumber 等对象做的一些优化。

简单来讲可以理解为 ** 把指针指向的内容直接放在了指针变量的内存地址中 ,因为在 64 位环境下指针变量的大小达到了 8 字节足以容纳一些长度较小的内容 **。于是使用了标签指针这种方式来优化数据的存储方式。

从其的引用计数可以看出,这也是一个释放不掉的单例常量对象。当我们使用不同的字符串对象进行创建时当内容相同,其对象的地址也相同。在运行时根据实际情况创建。

对于 NSString 对象来讲,当非字面值常量的数字,英文字母字符串的长度小于等于 9 的时候会自动成为 NSTaggedPointerString 类型。(小于等于 9 这个数据也是原博主进行猜测,经过测试在字符串含有 q 的时候是小于等于 7)

#三种字符串类型的 copy/ mutablecopy / retainCount 情况

总结就是

无论原来的三个的类型是 NSString 还是 NSMutableString 类型

copy 会使原来的对象引用计数加一 (当然仅有正常类型的字符串,而不是单例创建的,毕竟那两个引用计数是无限的),并拷贝对象地址给新的指针,所以类型与原类型一致。

mutableCopy 不会改变引用计数 ,会拷贝内容到堆上,生成一个 __NSCFString 对象,新对象的引用计数为 1.

#内存分布补充

#_NSCFConstantString

对于 ConstantString,我们想查看内存分布情况,直接打印 str 得到的其实是 str 这个指针的地址信息,前 8 位是 isa 指针,17 到 24 位是对应常量字符串的地址,25~32 位是字符串的长度。

#__NSCFString

与__NSCFConstantString 的存储常量的地址不同

__NSCFString 直接将对应字符串的 ASCII 码存储在之前 17~24 字节存储对应字符串地址的地方,而不是通过再存一个地址来进行存储。

所以对于常量字符串的单例来说,仅仅存储地址,哪怕后面再创建新的字符串,但是只要内容相同,str 对象里面存储的该字符串的地址都是一样的。而对于 CFString 来说,每个对象都是新的,每个对象都是由自己内部的地址来直接存储,省略了再次通过地址获取内容的步骤。大家哪怕内容相同,自己也是自己的。

其实存在于堆中的

#NSTaggedPointerString

所以 taggedPointer 是进行了一个编码的过程,在 Mac10.14 和 iOS12 之前,对 value 做异或操作的 objc_debug_taggedpointer_obfuscator 值为 0,之后为 objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK。

所以我们想得到解码,重新声明全局变量 objc_debug_taggedpointer_obfuscator 和内联函数_objc_decodeTaggedPointer 就好了

Tagged Pointer 是一个特殊的指针,不指向任何实质地址。使用编码的方式产生一个假地址,在需要时,通过解码方式得到其内部存储的数据。TaggedPointer 极大的提高了内存利用率和简化了查询步骤。它不单单是一个指针,还包括了其值 + 某些具体信息(比如个数等等),节省了对象的查询流程。

关于三种字符串的管理方式这部分,详情可以参考学长的博客[iOS 开发] NSString 的三种类型管理方式_ios nsstring 类型 - CSDN 博客

这一部分有些无聊。。

#多态

oc 中指针变量有两个,一个是编译时的类型,一个是运行时的类型

编译时的类型由声明该变量时使用的类型决定

运行时的类型与实际赋给该变量的对象决定

如果编译和运行的类型不一致就可能出现所谓的多态

FKBaba:

#import "FKBaba.h"
@implementation FKBaba
-(void) baba {
NSLog(@"别墅里面唱k");
}
-(void) test {
NSLog(@"我送阿叔茶具");
}
@end

FKSon:

#import "FKSon.h"
@implementation FKSon
-(void)son {
NSLog(@"水池里面银龙鱼");
}
-(void) test {
NSLog(@"研磨下笔亲手为我提笔字:大展宏图");
}
@end

main:

图片

子类对象赋给父类指针变量,这里就出现了多态。

a 编译时是 FKBaba 类型,运行时是 FKSon 类型

图片

同时指针变量在编译时只能调用其编译类型所具有的方法,但在运行时将执行运行时类型所具有的方法。

我们以这个程序为例子,虽然我们定义了父类的指针变量,但我们指针变量所指向的对象是子类。

所以我们只能调用父类中的方法,但是因为我们的变量实际指向子类,所以我们用的方法实际上是子类覆盖父类的方法或父类继承给子类的方法。

这也是我们在上面单独调用子类方法会出错的原因。

当把一个子类对象直接赋给父类指针变量,例如我们上方的初始化操作,a 变量编译时是 FKBaba 类型,运行时类型是 FKSon。当运行时调用该指针变量的方法,其方法行为总是表现出子类方法的行为特征。这就可能出现:相同类型的变量调用同一个方法呈现出不同的行为特征,这就是多态

当把一个子类对象直接赋给父类指针变量,例如我们上方的初始化操作,a 变量编译时是 FKBaba 类型,运行时类型是 FKSon。当运行时调用该指针变量的方法,其方法行为总是表现出子类方法的行为特征。这就可能出现:相同类型的变量调用同一个方法呈现出不同的行为特征,这就是多态

图片

而在这个程序中,编译时  [b son]; 不会报错,因为 a 在编译时系统认为其是 FKSon 类型,但是运行时就不行了,会报错

那为什么 [a baba] 又可以通过编译呢? 因为 FKSon 是 FKBaba,自动继承了 FKBaba 的所有非私有成员方法,所以 FKSon 的对象自然可以调用 FKBaba 的方法!

当然了,在这个程序中我迷把一个 FKBaba 对象赋值给 FKSon * 类型的变量,这在逻辑上不合理。

FKSon 是 FKBaba 的子类,你可以把 FKSon 指针赋给 FKBaba(向上转型),但是不能向下转型,除非你显示强转。

#二、指针类型强制转换

这一点与 C 语言十分类似。 除了 id 类型变量之外,指针变量只能调用编译时的类型方法,而不能调用运行时的类型方法所以我们需要将变量类型强制转换为运行时的类型,也就是对象的类。 ​

#​ 协议与委托

#规范、协议与接口

类是一种具体的实现题,而协议这是定义了一种规范,定义了某一类所需要遵守的规范。

协议不提供任何实现,它体现的是规范和实现分离的设计哲学。   让规范和实现分离正是协议的好处,是一种松耦合的设计。

tips:OC 中协议的作用就相当于其他语言中接口的作用。

  协议定义的是多个类共同的公共行为规范,协议里通常定义的是一组公用方法,但不会为这些方法提供实现,方法的实现则交给类去实现。

#使用类别实现非正式协议

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (EaTable)
-(void) Lai;
@end
NS_ASSUME_NONNULL_END
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKLaiCai : NSObject
-(void) Lai;
@end
NS_ASSUME_NONNULL_END
#import "FKLaiCai.h"
@implementation FKLaiCai
-(void) Lai {
NSLog(@"虔诚拜三拜,钱包里面多几百");
}
@end
#import <Foundation/Foundation.h>
#import "FKLaiCai.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
FKLaiCai* Laicai = [[FKLaiCai alloc] init];
[Laicai Lai];
}
return 0;
}

运行结果:

图片

#正式协议

定义正式协议的时候,不再使用 @interface,@implementation关键字了,而是使用 @protocol关键字,定义正式协议语法如下:

@protocol 协议名<父协议1,父协议2>
{
多个方法定义
}

对于上述语法,做出以下详细说明:

1、协议名应与类名采用相同的命名规则

2、一个协议可以有多个字节父协议,但协议只能继承协议,不能继承类

3、协议中定义的方法只有方法签名,没有方法实现

4、协议中包含的方法可以是类方法,也可以是实例方法

tips:因为协议定义的是多个类共同的公共行为规范,所以,协议里所有的方法都是公开的访问权限。

Printable.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol Printable <NSObject>
@required
-(void)printInfo;
@end
NS_ASSUME_NONNULL_END
Person.h
#import <Foundation/Foundation.h>
#import "Printable.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject<Printable>
@property (nonatomic,copy) NSString* name;
@property (nonatomic,assign) NSInteger age;
@end
NS_ASSUME_NONNULL_END
Person.m
#import "Person.h"
@implementation Person
-(void) printInfo {
NSLog(@"Name: %@, Age: %ld", self.name, self.age);
}
- (NSString *)infoSummary {
return [NSString stringWithFormat:@"%@ is %ld years old.", self.name, (long)self.age];
}
@end
#import "Person.h"
#import "Printable.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
p.name = @"揽佬";
p.age = 99;
[p printInfo];
}
return 0;
}

图片

我们也可以一个协议,这个协议同时继承两个协议:

#import <Foundation/Foundation.h>
#import "FKOutput.h"
#import "FKProductable.h"
NS_ASSUME_NONNULL_BEGIN
//定义协议,继承了FKutput、FKProductable两个协议
@protocol FKPrintable <FKOutput, FKProductable>
//定义协议的方法
- (NSString*) printColor;
@end
NS_ASSUME_NONNULL_END

tips:协议的继承和类的继承不一样,协议完全支持多继承,即一个协议可以有多个直接的父协议。和类继承相似,子协议继承某个父协议,将会获得父协议中的所有方法。一个协议继承多个父协议时, 多个父协议排在 <> 中间,多个协议口见以(,)隔开。

#遵守(实现协议)

在类的接口可以继承该类继承的父类,以及遵守的协议,一个类可以同时遵守多个协议,语法如下:

@interface 类名:父类<协议1, 协议2...>

如果程序需要使用协议来定义变量,有以下两种语法:

- NSObject<协议1,协议2...>* 变量;
- id<协议1,协议2...>* 变量;
### 协议的意义
1,在多个类具有相同行为时,如果没有协议,你必须知道每个对象的具体类型并单独处理。
```objective-c
NSArray<id<Printable>> *objects = @[p, d, r];
for (id<Printable> obj in objects) {
[obj printInfo]; // 不关心具体类名
}

但是在使用协议后,只要你遵守了协议,我就可以调用你的方法。

2,解耦代码,实现高内聚 

#正式协议与非正式协议

非正式协议通过为 NSObject 创建类别来实现,而正式协议直接使用 @protocol 创建; 遵守非正式协议通过继承带特定类别的 NSObject 来实现,而遵守正式协议则有专门的 OC 语法来实现; 遵守非正式协议不要求实现协议中定义的所有方法;而遵守正式协议则必须实现协议中定义的所有方法。   为了弥补遵守正式协议必须实现协议的所有方法造成灵活性不足,在 OC 还有两个关键字:

**@optional:** 位于该关键字只后、@required 或 @end 之前声明的方法是可选的,实现类可选择是否实现这些方法。 **@required:** 位于该关键字之后、@optional 或 @end 之前声明的方法是必需的,实现类必需实现这些方法。 通过在正式协议中使用以上两个关键字,正式协议完全可以代替非正式协议的功能。

#协议与委托

协议体现的是一种规范,定义协议的类可以把协议定义的方法委托给实现协议的类,这样可以让类定义具有更好的通用性,因为具体的动作将由该协议的实现类去完成。无论是基于 Mac 的 Cococa 应用开发还是 iOS 开发,各种应用程序大量依赖委托这个概念。

#​ 深拷贝与浅拷贝

本人拙作呈上: OC 语言学习 —— 对象复制 - CSDN 博客


原文发布于 CSDN:Oc 语言学习 —— 重点内容总结与拓展(下)