本节做为Objective-C的入门课程,笔者会从零基础开始介绍这种程序设计语言的各个方面。
术语
- ObjeC:Objective-C的简称,因为完整的名称过长,后续会经缩写来代替;
- 项目/工程:也称工程,指的是一个App的源文件的文件夹包和结构,有时也称为工程或项目工程。
- 项目模板:不只ObjC,用不同编程语言开发App时其源码都会包含源文件、集成框架、配置文件、资源图片等,这些元素需要放在同一个文件中并要符合某种规则,而针对不同类型的App其元素和组织又不一样,这个工作比较费时,所以为了效率考虑一般会经一个母版进行修改,这个母版就称为项目模板;
Command Line Tool工程
首先,我们需要创建一个Command line tool工程项目(即不带图形化界面的项目)。
在上述工程创建界面上有很多模板项目,可以按需要选择相应的模板开发,这样省去了好多搭建框架的时间,但也可以选择从空项目开始。多数模板可以从字面意思就可以了解。
- 现在我们只需要知道Command Line Tool工程模板就足够了(一种无UI界面的可在命令行执行的脚本工程模板);
- 后续在涉及AppKit之前的所有代码我们全会以这类工程为载体演示代码,其它的工程模板在讲到其内容时再详细解释。
创建HelloWorld工程
按照惯例,我们还是以一个Hello Word项目做为开始,了解一下ObjC(ObjectiveC简称)项目结构和基础语法。项目名称暂时称为helloWorld,项目设置采用默认即可,不需要做任何改变,项目结构如下:
打开main.m文件,我们所有的测试代码暂时全写在这里面,.m是ObjC代码文件的后缀(.c是C语言的源码文件),运行时会交由程序编译器LLVM处理和运行。
objectivec
//
// main.m
// helloWorld
//
// Created by 刘东 on 2023/12/20.
//
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool { // 自动释放池,由系统来管理变量的内存
// insert code here...
NSLog(@"Hello, World!"); // 打包日志函数,用@修饰表示NSString对象
}
return 0; // 规定 0 表示程序正常结束,其它值都是非正常结束
}
除了上述.m文件外还可以兼容几下以类源码文件:
- .c:C文件
- .cc, .cpp:C++文件
- .h:头文件
- .mm:Objective-C++文件,可以同时使用ObjectiveC和C++语法;
- .pl:Perl文件
- .o:ObjectiveC编译后的文件
- .m:Objective-c文件
代码注释
在ObjC中只有两种注释:
- //:行注释,一般用于代码行或代码后面;
- /* */:块注释,一般用于类说明,方法说明或代码块说明使用;
- #pragma mark *** String funnel methods ***,这是一种特殊格式的指令,也能起到注释的作用,详细可参考代码导航一节;
objectivec
// Secondary text that may be displayed
/*
Secondary text that may be displayed adjacent to or below the primary title depending on the configuration of the window.
A value of empty string will remove the subtitle from the window layout.
*/
模块导入
格式为:#import <Foundation/Foundation.h> 注意最后面没有;分号,表示为当前类的实现添加相关的模块依赖。如果导入的是自定义的实现,则需要用双引号(本地)替换<>(系统)。
objectivec
#import <Foundation/Foundation.h>
#import "Fraction.h"
上述所谓的系统其实称为框架更合适,比如Foundation、AppKit框架,每个框架都有一个主头文件,它包含了框架内所有的头文件,只需导入一次就可以使用此框架内所有的功能,这样就省去了一个个导入的麻烦。
MAC OS所有框架的目录位于 /System/Library/Frameworks 目录下。
入口函数main
程序运行的主入口函数,格式为:int main(int argc, const char * argv[]),程序的执行入口和java的main函数功能相同,每个App最多只能允许存在一个main函数。一般练习时用main函数调用就行,如果是大型项目ObjC也提供了专门的单元测试框架,后续会讲到,main.m语法格式如下:
objectivec
static void method(){
}
/*
argc:命令行输入的参数个数
argcv:字符指针数组,即参数值
*/
int main(int argc, const char * argv[]) {
@autoreleasepool {
//代码位置
}
return 0;
}
运行传参
函数说明:int main(int argc, const char * argv[])
- argc:命令行输入的参数个数
- argcv:字符指针数组,即参数值,argcv[0]指向一个函数,argcv[1]为一个字符数组
objectivec
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct entry dict[100] = {
{"abyss", "a bottomless pit"},
{"addle", "to become confused"}
};
int entries = 10;
int entryNumber;
int lookup(struct entry dict[], char search[], int entries);
if (argc!=2){
NSLog(@"no word typed on the command line.");
return 1;
}
entryNumber = lookup(dict, argv[1], entries);
if(entryNumber!=-1){
NSLog(@"%s", dict[entryNumber].definition);
}
}
return 0;
}
调用方法如下,可从命令行,也可从Xcode中执行
Shell
clang -fobjc -arc main.m lookup abyss -
上述程序会调用函数lookup,然后在dict字典中查找argv[1]中的单词,如果找到就返回详细的解释。
添加函数
在main函数所在的类中也可以添加自定义的方法,但方法的命名方式和ObjC的语法有很大不同,这一点需要额外注意。在main中定义的方法是C语言的语法。
objectivec
#import <Foundation/Foundation.h>
//无参方法,在方法前面也可以添加static关键字
void nsRangeTest(){
NSRange range1 = {17, 4};
}
//有参方法
NSComparisonResult *compareArray(id element, id compareEle){
return [[compareEle name] compare: [element name]];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
nsRangeTest();
}
return 0;
}
打印函数NSLog
打印函数,相当于C语言或JAVA语言的print()函数,这里需要注意写法,一定是以@开头,这也说明了NSLog函数的入参是一个NSString类型的对象(在ObjC语言中,字符串用@""表示),另外所有的Cocoa函数和对象全部以NS做为命名前缀(也被称命名空间),也有一些老的API是采用CF开头的但不建议使用了。
objectivec
NSLog(@"Hello, World!");
int sum = 20+25;
NSLog(@"The sum is %i", sum); //NSLog函数如果发现%,则视为占位符,这样的占位符有很多,后续会讲到
也可以用printf()函数来代码,但不是太建议,因为NSLog添加了很多格式化的信息,注意看下面代码的输出
objectivec
NSLog(@"Hello, World!\n");
printf("Hello, World!\n");
~~~~
2024-03-26 19:31:52.091265+0800 helloWorld[46546:5251675] Hello, World!
Hello, World!
键盘输入
scanf()函数也可以使用占位符,因为键盘接收的原始数据全是字符串,在程序中需要做一些类型转换工作。下在程序运行后在scanf处会卡住,然后在控制台输入相应字符后就会往下执行了。
objectivec
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age;
scanf("%i", &age); //注意&的用法,它表示一个指针引用,
NSLog(@"age is %i", age);
}
return 0;
}
/*~~
aaa
2024-03-27 14:39:56.103878+0800 objcBase[59017:6048518] age is 32759
Program ended with exit code: 0
*/
程序运行
除了使用Xcode工具运行,还可以使用命令行工具执行。其命令如下,其中prog1为重命名的项目名称。但这种方式并友好,因为还在设置PATH等资源目录。
Shell
(base) MacBook:~ liudong$ clang -fobjc-arc main.m -o prog1
(base) MacBook:~ liudong$ clang -fobjc-arc -framework Foundation main.m -o prog1
输入类似下面的界面:
用NSLog输出时,上面的4596表示当前应用的进程ID号。另外编译好的文件一般会存放在以下目录中,其中helloWorld-ghwqfuwptpnvhtfpzqoxxzcbnzow是一串随机值。不同版本的OS操作系统存放的位置有可能不太一样。
objectivec
~/Library/Developer/Xcode/DerivedData/helloWorld-ghwqfuwptpnvhtfpzqoxxzcbnzow/Build/Products/Debug
进入这个目录下可以执行./helloWorld就可以直接运行应用了。
程序中止
有两种方式一种是在main函数中中止,一种是在方法中强制中止,在main函数中直接使用retrun 0即可,则在方法中需要使用以下代码。
objectivec
exit(0);
设置XCode
首先来讲,苹果公司通常喜欢在不同版本的XCode中增加或移动一些功能,而且这些功能对开发代码的效率是非常高的。下面就以XCode V13版本为例来说明下这些设置如何操作。
主界面
- 导航器:用来显示项目的源码,最上面的有9个左右的工具栏,包括:符号、搜索、总是、调试、断点和日志等,快捷键盘是command+数字1~9;
- 工具栏:这里最主要的是库面板,需要从菜单View-Show Toolbar打开;
- 检查器:不同的文件会有不同的设置,也就是属性设置面板;
- 代码编辑区:在最上面有一个面包屑栏和工具栏,工具栏上有导航文件功能,面包屑栏显示了当前文件一些内容。这块的功能比较简单,点一次就可以记住了;
环境下载
在Preferences偏好设置中可以下载不同的运行环境:
代码自动完成
XCode有代码提示功能,输入一个字符会有默认提示,通过ESC键来打开或关闭提示框,然后通过Control+/-可实现快速翻页。
-
:代表define指令;
- m :表示method;
- f :表示函数;
- c :表示类
代码导航
可以通过在源码中设置特殊的标识来把需要关心的代码加入到代码面包屑工具栏中,这些标识在编译时会被编译器忽略掉。
这些特殊标记通常用:
- -,减号实现一个分隔线,见上图
- whatever,任意字符;
- //开头的特殊标记,以关键字+冒号+空格+文字描述格式,比如//TODO: 未完成的工作,这些关键字一般有TODO:、FIXME:、!!!:、???:
快捷键
-
鼠标左键+option,然后点击某个类型,在弹出窗口点击相应的类型名可直接跳转到document;
-
control+i,格式化代码;
-
command+d,删除行(需要在keymap中搜索delete line自行设置);
-
command+左/右箭头,快速移到行首和行尾
-
command+shift+o,快速查找;
-
command+option+左/右箭头:展开和折叠代码,功能们于Edit-Code Folding下面
-
command+option+shift+左/右箭头:展开和折叠所有方法
-
command+r,运行程序
-
command+u,测试程序
另外可供编辑使用的快捷键盘就是电脑上的触摸屏,可通过Edit-Customizer Touch bar 来设置,如下:
程序调试
主要使用以下几个工具,依次是:跳到下一个断点、下一行、进入被调用的方法、跳出被调用的方法。分别对应快捷键F5~F8。
另一个调试窗口在导航区上,与调试区联动,主要是下图中这两个标签页,一个是性能查看,另一个是断点浏览
鼠标悬浮到某个程序变量上也会显示相应的信息
还有一些更高级的功能可以在控制台输入相应的指令,比如:
- call [exp]:调用给定对象的方法;
- print [exp]:打印表达式的原始值,比如print [int] [obj length];
- print-object [exp]:打印表达式的对象值;
- set [v] = [exp]:给表达式赋值;
- whatis [exp]:判断变量的类型;
- help:帮助;
静态检查器
这个功能是不是一个新的功能,很多IDE都有此种能力,有些还会以插件的形式存在,比如sonna, understand或是idea中的各种分析插件。
静态检查器的功能就是不运行代码来分析代码中可能存在的一些问题,在xcode中其功能集中在菜单"project-Analyze"中,它可以检查代码中的:
- 安全问题;
- 并发问题;
- 逻辑问题;
- 冗余代码;
疑似有问题的代码可以在导航面板中查看,找到问题后可在面板中点Fix来修复这些警告信息。
有时也会误报,因为检查器毕竟也是一段逻辑固定的程序,没办法覆盖所有的代码模式,如果发现了语报除了用上面 Fix 来关闭外,也可以在方法后加一个特殊标识来告诉检查器这块的这个问题不要检查了,比如:
objectivec
//类似这样的标签还有很多,可以按需选择
static void dataFun (void) NS_RETURNS_RETAINED { }
基础数据类型
基础数据类型
布尔类型
关键字BOOL,其值默认只有YES或NO,在Objc中只可与1和0相互转换,占8位存储空间,在写程序时也可以用#define把TRUE和FALSE定义为1 和 0,示例如下:
objectivec
BOOL areIn(int thing, int ti){
if (thing == ti){
return (YES);
}
return NO;
//return thing = ti; 这行代码有问题,因为ObjC中只有0和1来平替YES和NO
}
NSString *bool2Str(BOOL y){
if (y == YES){
return @"yes";
}else{
return @"no";
}
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
BOOL *boolV = areIn(1, 2);
NSLog(@"%d = %@", boolV, bool2Str(boolV)); //0 = no
NSLog(@"%d = %@", 1, bool2Str(1)); //1 = yes
NSLog(@"%d = %@", YES, bool2Str(YES)); //1 = yes
}
return 0;
}
char字符类型
单个字符,用''单引号表示
objectivec
char c = 'a';
int整数类型
objectivec
int i = 1000;
float浮点类型
带小数点的变量,比如
objectivec
float f = 123.95;
double双精度浮点数
双精度
objectivec
double d = 8.44e+11;
字符串常量
字符不是一个基本类型,它是一个对象,在使用时除了用对象实例化后,简单的可以直接用@"",来表示:
objectivec
NSString *str = @"korgs";
创建一个可变字符串
NSMutableString *stringM1 = [[NSMutableString alloc] initWithString:@"字符串"];
可用cString打印字符串内容,它返回的是一个char *指针地址。
ObjC
NSString *string = @"abdc";
NSLog(@"%@, %s", string, [string cString]);
类型限定词
ObjC中的数据类型定义非常有意思,支持组合定义,这些限定词主要包括:long, long long, short, unsigned和signed这几个,它的作用是扩充原有数字的表述范围,具体的范围会根据系统决定,比如
ObjC
long int factorial; //声明为long的整形变量
long, long long, short, unsigned和signed
类型运算符
- 四则运算: +、 -、 *、 \、 %、++、--
- -(负号)
- 类型强转,这里和java一样, 比如这样的写法 int a = (int)f,f为一float类型
- 赋值运算:=、!=、+=、-=、/=、*=
- 三元运算符:condition ? expression1: expression2
- 位运算:&、 |、 ^、 ~(求反)、 << 、>>
- 关系运算:==、!=、< 、 > 、 <= 、 >=
运算规则
主要是数值上面:
- 同类型的的数相互运算,结果是同类型;
- bool, char, short, int, bit field, enum全部转为int再运算;
- 大类型与小类型运算结果为大类型,比如long int / short int = long int;
数据打印
以上类型如果需要用NSLog等函数打印时,其占位符表示都不太一样,大体如下。当用%@时表示可以打印任何内容。看似很复杂,其实就四个特殊的,float, long, unisigned, long long,分别用f, l(L), u(U), ll(LL)来表示,其它的不是太常用
类型 | 实例示例 | NSLog字符 |
---|---|---|
char | 'a' '\n' | %c |
short int | 123 | %hi, %hx, ho |
unsigned short int | 123 | %hu, %hx, %ho,%hx |
int | 12, -97, 0177(8进制)0xFEE0(16进制) | %i, %x, %o |
unsigned int | 12u, 100U, 0xFFU | %x, %0, %u |
long int | 12L, -2001, 0xFFFFL | %li, %lx, %lo |
unsigned long int | 12UL, 100ul, 0xFFFFUL | %lu, %lx, %lo |
long long int | 500ll, 0xe5e5e5LL | %llu, %llx, %llo |
float | 12.32f, 3.1e-5f | %f, %e, %,g, %a |
double | 12.32, 3.1e-5 | %f, %e, %g, %a |
long double | 12.34L, 3.1e-5l | %Lf, %Le, %Lg |
id | nil | %p |
* | *p(指针) | %@ |
- %@:是一个通用字符可表示任何数据,可归类为打印对象,它会调用类的description:方法;
- %s:打印字符串