Objective-C 学习笔记 | 基础
参考书:《Objective-C 编程(第2版)》
第1部分 入门
Objective-C语言是以C语言为基础的,但增加了对面向对象编程的支持。Objective-C语言是用来开发在苹果iOS以及OS X操作系统上运行的应用的编程语言。
第2部分 如何编程
该部分讲解了C语言编程的必要知识,这里只记录Objective-C新增内容。
NSInteger和NSUInteger
NSUInteger是无符号的整型,NSInteger是有符号的整型。
NSInteger是一个封装,它会识别当前操作系统的位数,自动返回最大的类型。
定义的代码类似于下:
#if __LP64__ || TARGET_OS_EMBEDDED || TARGET_OS_IPHONE || TARGET_OS_WIN32 || NS_BUILD_32_LIKE_64
typedef long NSInteger;
typedef unsigned long NSUInteger;
#else
typedef int NSInteger;
typedef unsigned int NSUInteger;
#endif
这样做让NSInteger和NSUInteger变得通用,不用考虑设备是32位还是64位。
如果使用printf()来输出这两种类型的变量,需要先将NSInteger转换成long,NSUInteger转换成unsigned long。
第3部分 Objective-C与Foundation
该部分开始介绍Objective-C。编写Objective-C程序时,要使用Foundation框架。框架(framework)是由很多类(Class)组成的库。这里记录一些新的知识点。
#import和#include的区别
#include指令告诉编译器做呆板的复制粘贴,将包含的内容粘贴到目标文件中来。而#import指令则让编辑器先检查之前是否导入过这个文件,或是已经被包含到目标文件中了。因此,#import指令导入更快、更有效率。
类与实例、方法与消息
Objective-C也有类和对象的概念,Objective-C的方法和消息与函数类似。如需执行方法中的代码,首先需要发送一条消息给包含这个方法的对象或类。消息发送(指令)必须写在一对方括号中,并且必须包含接收方(receiver)和选择器(selector),如下图所示:
date是一个类方法。date方法执行后,NSDate类会在堆上new一个NSDate实例:now,并初始化为当前的日期/时间,然后返回新对象(now)的地址。
有了NSDate实例:now之后,我们可以给这个对象发一个实例方法,比如:timeIntervalSince1970。通常来说,实例方法会提供实例中实例变量的信息,或是对实例的实例变量进行操作。
测试程序:
//
// main.m
// TimeAfterTime
//
// Created by 刘文晨 on 2024/6/5.
//
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 向 NSDate 类发送 date 消息,让它执行 date 方法
NSDate *now = [NSDate date];
// 打印实例的地址
NSLog(@"This NSDate object lives at %p", now);
// %@ 会输出相应对象的“描述信息”
NSLog(@"The date is %@", now);
// 实例方法
double seconds = [now timeIntervalSince1970];
NSLog(@"It has benen %f seconds since the start of 1970.", seconds);
}
return 0;
}
注意:
- 类方法和类对应,实例方法和实例对应。receiver和selector不匹配就会出错。
- Objective-C是区分大小写的,方法名也是区分大小写的。
- Objective-C语言命名习惯为“驼峰式”或“前缀大写的驼峰式”。
alloc和init
消息可以以嵌套的形式连续发送,而唯一必须要以嵌套的形式发送的消息是alloc和init。
每个类都有一个alloc类方法,它创建一个新的对象并返回指向该对象的指针。通过alloc类方法创建出来的对象,必须要经过初始化才能使用。每个类也都有一个init实例方法,它用来初始化实例。
NSDate *now = [[NSDate alloc] init]; // 消息嵌套,和下面的效果一样
NSDate *now = [NSDate date]; // date 方法代码最少,称之为便利方法
nil
nil就是空指针,不指向任何对象。
在Objective-C中,可以向nil发送消息,是合法的,但得到的返回值没有意义。
注意,如果程序向某个对象发送了消息,但不符合预期,应该检查receiver是否为nil。
id
id 类型的含义是:可以指向任意类型的Objective-C对象的指针。‘
id delegate; // id 已经隐含了 * 的作用
类似于 auto(?)
ARC
ARC(automatic reference counting,自动引用计数)为Objective-C提供了一种自动销毁不被引用的对象的机制。当项目开启了ARC,编译器会自动给项目添加代码来计算每个对象的引用数,即每个对象都会对指向自己的指针计数。当引用数为0时, 程序会自动销毁该对象,释放内存。
类似于 shared_ptr。
在Objective-C加入ARC之前,程序员必须手动维护引用计数。
[anObject release]; // abOject 会失去一个拥有方
[anObject retain]; // abOject 会得到一个拥有方
[anObject autorelease]; // abOject 会在 autorelease 池(对象)被排干(drain)的时候收到 release 消息
虽然 ARC 会自动使用 autorelease 池,但是必须由程序创建并排空相应的 autoreleasepool 池,语法如下:
// 创建 autorelease 池(对象)
@autoreleasepool {
...
} // autorelease 池被排空
字面量语法
格式:
NSString *lament = @"Hello World!";
NSArray *dataList = @[now, tomorrow, yesterday]; // 这三个都是 NSDate 对象
字面量语法是Objective-C语言的一种缩写,可以以不明确发送消息的方式创建实例。
还没有字面量语法的时候,只能使用 arrayWithObjects: 类方法来创建NSArray实例:
NSArray *dataList = [NSDate arrayWithObjects:now, tomorrow, yesterday, nil]; // nil 是结束标记,让方法停止运行
self
Objective-C的方法都包含一个隐含的局部变量 self。self 是指针,指向运行当前方法的对象。当某个对象要向自己发送消息时,就需要使用 self。它有 2 个简单的用法:
- 调用自身的存取方法,避免直接存取实例变量。
- 将 self 作为实参传给其他方法,以便其访问“当前的”对象。
NSObject
协议中的 self:
#include <objc/objc.h>
#include <objc/NSObjCRuntime.h>
@class NSString, NSMethodSignature, NSInvocation;
@protocol NSObject
- (BOOL)isEqual:(id)object;
@property (readonly) NSUInteger hash;
@property (readonly) Class superclass;
......
- (Class)class OBJC_SWIFT_UNAVAILABLE("use 'anObject.dynamicType' instead");
- (instancetype)self;
......
@end
self最终返回的结果就是instancetype类型的代理方法,它是动态类型,最终运行时才会确定,实例方法返回实例类型、静态方法返回的是Class。
super
super 的作用:直接调用父类中的某个方法,或者说从父类开始查找与之匹配的实现。
使用场合:子类重写父类的方法时想保留父类的一些行为。
结构体 objc_super
的官方解释:The compiler generates an objc_super data structure when it encounters the super keyword as the receiver of a message. It specifies the class definition of the particular superclass that should be messaged.
#include <objc/objc.h>
#include <objc/runtime.h>
#pragma GCC system_header
#ifndef OBJC_SUPER
#define OBJC_SUPER
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
__unsafe_unretained Class class;
#else
__unsafe_unretained Class super_class;
#endif
};
当遇到super关键字时,编译器会生成一个objc_super结构体,作为消息的接收者,objc_super结构体使得接收消息的父类的定义被明确化。
isa指针
任何一个对象的isa指针都会指向创建该对象的类。
给对象发送消息的时候,对象就会通过isa指针找到该对象的类并查询是否有该消息名的方法。如果没有,就会继续查询它的父类,直到找到名为消息名的方法,或者到达继承链的顶端(NSObject)为止。
description方法和%@转换说明
格式说明符%@让对象描述自己,实际上在处理%@时,程序会向相应的指针变量所指的对象发送 description 消息。
description 方法会返回一个描述类实例的字符串。description 是一个 NSObject 方法,所以所有的对象都有这个方法。默认的 NSObject 实现会以字符串的形式返回该对象在内存的地址。不同的类可以重写(override)description 方法,来最有效地描述实例。
强引用循环和弱引用
两个对象互相拥有的关系将导致相关对象都无法释放,这种情况叫强引用循环,这是导致内存泄露的常见原因。
Xcode的Instruments中的Leaks组件可以找出程序中的强引用循环:
通过弱引用,可以解决该问题。弱引用是不说明所有权的指针,把 BNRAsset 的 holder 属性改成 weak,就能让 BNRAsset 对象不拥有它的 holder(BNREmployee 对象)。代码如下:
#import <Foundation/Foundation.h>
@class BNREmployee;
NS_ASSUME_NONNULL_BEGIN
@interface BNRAsset : NSObject
@property (nonatomic, copy) NSString *label;
@property (nonatomic, weak) BNREmployee *holder;
@property (nonatomic) unsigned int resaleValue;
@end
NS_ASSUME_NONNULL_END
完整程序见于:UestcXiye/Objective-C-Practice。
弱引用的自动置零特性:强引用会保留对象的拥有方,使其不被释放。而弱引用则不会保留,因此标为弱引用的实例变量与属性指向的对象可能会消失,如果发生了这种情况,那么这个实例变量或属性会被设为 nil。
Collection 类
Collection 类的实例用于保存指向其他对象的指针。主要分为三种:
- NSArray 及其子类 NSMutableArray
- NSSet/NSMutableSet
- NSDictionary/NSMutableDictionary
注意以下 4 点:
-
Collection 对象只能保存对象的指针,不能保存基本类型变量或指向结构的指针,需要先将这些 C 语言基本类型封装成对象,再存入 Collection 对象。
例如:float变量、int变量等要先转换成NSNumber再存入Collection对象,或者直接用NSNumber字面量实例。结构可以用NSValue(它是NSNumber的父类)实例来封装。
-
其中 NSArray、NSSet、NSDictionary 具有不可修改性。使用它们可以节约内存提高性能,因为它们的 copy 方法仅仅返回指向自己的指针,不会做拷贝之类的事情。
-
向可改变的 Collection 对象中加入某个对象时,Collection 对象会成为该对象的拥有方;同理,移除对象时,Collection 对象就不再是该对象的拥有方了。对于不可改变的 Collection 对象而言,创建时就拥有其中所有对象的所有权,而 Collection 对象被释放时,它就放弃其中所有对象的所有权。
-
Collection 对象不能保存 nil,需要将“空”包装成一个对象,可以使用 NSNull 类。