【iOS 客户端专场 学习资料一】第四届字节跳动青训营

1,678 阅读37分钟

第四届字节跳动青训营讲师非常用心给大家整理了课前、中、后的学习内容,同学们自我评估,选择性查漏补缺,便于大家更好的跟上讲师们的节奏,祝大家学习愉快,多多提问交流~

第一节:iOS 现状与开发者生态

课程大纲

image.png

课程问题

iOS 开发者生态

  • iOS 的APP的市场情况是否稳定?
  • 目前国内外有多少Apple 开发者?
  • iPhone 的销售情况怎样?
  • WWDC22 都介绍的内容都能反应苹果怎样的动向?

iOS 开发者现状

  • 什么是客三消?
  • 为什么会出现客三消的观点?
  • iOS 开发还值得选择吗?

客户端团队介绍

  • 业务技术团队都在做哪些事情?
  • 基建团队在做哪些事情?
  • 中台技术团队在做哪些事情?

iOS 开发者的要求

  • 对新同学来说什么最重要?
  • 后续课程都有哪些内容,对我们有怎样的帮助?
  • 作为一个iOS 开发者,有什么建议给到我们的青训营学员?

\

参考资料:

2021年一季度互联网和相关服务业运行情况

2021年上半年互联网和相关服务业运行情况

2021年1-10月份互联网和相关服务业运行情况

2021年互联网和相关服务业运行情况

2022年一季度互联网和相关服务业运行情况

WWDC22 - Videos - Apple Developer

Canalys Newsroom - North American smartphone shipments up 4% in Q1 despite growing inflation concern

Canalys Newsroom - Canalys: Global smartphone shipments fall 11% due to adverse headwinds

Canalys Newsroom - Canalys: Apple grows 40% to take the crown in China smartphone market in Q4 2021

工业和信息化部举行2021年汽车工业发展情况新闻发布会(附实录)

第二节:Xcode & Objective-C 简介

课程概述

"Xcode 作为 Apple 向开发人员提供的的 IDE,其集编译,真机/模拟器调适,Git 等诸多功能为一体。可以说是 iOS 开发的官方唯一指定渠道,开发者的必备吃饭工具~

本节课将介绍 Xcode 的基础功能,以及专案的结构,结合 Objective-C 语言,带大家完成课程的第一个 Hello World!"

课前准备

  • mac电脑
  • 非常建议提前下载Xcode

课程大纲

image.png

课中

Xcode

Xcode 简介

Xcode 是苹果公司推出的一款开发工具,第一个版本于2003时推出,截至目前最新的版本是14.0

它集合了 编译,测试,Git,甚至能直接将你的App提交到AppStore 去做审核。

developer.apple.com/documentati…

下载与安装

  • 注册一个Apple ID
  • 在mac自带的AppStore下载

创建项目

如果你还没有任何项目,你可以在 Xcode的欢迎页上创建一个Project

你也可以在 Xcode的顶部导航栏,透过 File -> New -> 选择创建 Project , Workspace 或是 Target

  • 创建后,在你保存的文件目录会有类似这样的结构,其中蓝色的符号就是Project
  • 下次在打开时,可以直接双击Project 开启项目

\

Xcode工程体系介绍

  • Workspace:Workspace 是 Xcode 提供的一个工作空间,一个Workspace可以包含多个project,可以通过多个Project分工组合成一个庞大且复杂的工程
  • Project:Project 是一个工程的核心,你可以透过它来管理源代码,资源文件,添加其他三方库...等等,一个Project 可以包含多个 Target
  • 而Target可以看做是一个特定的构建目标,可以是以构建一个App主体为目标,或是构建命令行工具,构建代码二进制库...等

    • 每个Target都可以从它隶属的Project中,圈出一批源代码文件或资源文件,然后基于该Target的配置Build Configurations, Build Phase ,执行构建就能得到Target对应的产物 Product,也就是运行的最终结果。

Project文件

Project他是一个大型目录,它既包含了各种文件,资源,以及构建信息,也包含了一个或多个的Target。

.pbxproj 详细介绍:www.jianshu.com/p/e82ec6a56…

Project本质是一个 .pbxproj 为后坠名的文件,文件的内容使用 Old-Style ASCII Property Lists的格式,记录了各种信息,信息包括以下内容:

  • 对文件的引用

    • 原代码,.m / .h / .cc / .swift... 等等
    • 资源文件
    • Framework, Library
  • 虚拟文件夹,Group

    • 将多个文件组织成虚拟文件夹,但不影响实际文件目录的结构
  • 包含的Target
  • 构建配置

Targets介绍

原文:developer.apple.com/library/arc…

翻译大致如下:

一个Target,详细定义了Project中的一批文件到构建出一个Product的整个过程,

它定义了构建系统的所有输入和文件的处理配置,而构建系统的输出就是Product。

处理配值指的则是 Target的Build Settings 和 Build Phases。

同学们可以这样理解,你创建了一个食品工厂(Workspace & Project),然后你创建一个鸡块生产线(Target),生产线的输入是鸡肉,淀粉,食用油(Input File, 资源文件),你需要设置搅拌速度,油温和处理顺序(Build Settings, Build Phases),最后你按下机器运行(Build & Run),就会得到产物麦乐鸡块(Product)

你还可以依赖本工厂(Workspace & Project)其他生產線的产物,例如酱料(Other Target & Product),编译器会自动排列编译的顺序。

Targets 分类

Objective-C

同学们需注意每个代码块都有黄色标记是写在哪个文件

Class的编写:介面与实现

Objective-C是一種通用、面相对象的程式語言,通常我们简称他为OC,它扩充了标准的ANSI C程式语言,可以理解为在C语言之上加了一层,在OC的代码中使用C语言代码也是完全合法可以通过编译的,

基本上除了OC中面相对象的语法是采用讯息传递的风个,其他基础变数型别,预处理机制,还有流程控制等基本都跟C语言完全一致

编写一个Objective-C的类的,由两部分组成,一个是类的定义或叫做介面 Interface,第二个是这个类的实现 Implementation。

  • 介面是由 @interface开头 与 @end,interface只是一个声明,可以声明属性, 变量和函数方法,它并不对声明的任何方法进行实现,

    • 值得注意的是,一个类它可以拥有多个 interface,你可以将不同功能的函数分散,写在多个Interface中,但必须注意,你都要有个对应的实现。
  • 实现是由 @implementation开头和@end结尾,基本上你在 interface声明的方法,都需要去做实现,否则调用这函数可能会发生崩溃,当然编译器可能都不让你通过。

    • 你可以添加interface中没有声明的函数或变量,当做私有函数,这个函数就只有这个implementation区块内可以访问。
// 类的Interface开始
@interface ByteDancer : NSObject
// 属性, 方法声明
// Interface中 方法不需要写实现
@property (nonatomic, copy) NSString* name;
- (void)sayHello;
@end
// 类的Interface结束
// 类implementation 开始
@implementation ByteDancer
- (void)sayHello {
    // 方法实现
    NSLog(@"My name is %@", self.name);
}
- (void)private_method {
    // 只有当前的区块能使用本方法
}
@end
// 类implementation 结束

.h 文件 和 .m文件

victorleungtw.medium.com/connection-…

对应OC的代码文件,它也分为了声明文件(后坠名 .h )与实现文件(后坠名 .m),

  • 其中类的 @interface ,它可以写在 .h文件 也可以 放在 .m文件,

    • 如果放在 .h 文件,那这个介面里的声明,它就能被其他的 .h 或是 .m 引入后(也就是import),进而被调用
    • 如果放在.m 文件里,那他就只能在这个文件里的其它类调用。
  • 而 @implementation 就只能写在 .m文件里

    • 你需要在.m文件的顶部引入 .h 文件,才能在实现代码里调用其他类的函数
    • 需注意 .m文件是不能被引入的,只能引入.h,所以你想让其他.m调用方法,必须把它都声明在.h

对象与构造函数

  • 创建对象

注意 person 前面必须有个 *号,因为他是一个对象指针。

 // main.m
 #import "ByteDancer.h"
 
//注意 person 前面必须有个 *号,因为他是一个对象指针。
ByteDancer  *byteDancer1 = [[ByteDancer alloc] init];

[byteDancer1 sayHello];
int staticReturn = [ByteDancer staticFunction];
        

(override)父类默认声明的构造函数init

// ByteDancer.m
@implementation ByteDancer
// 覆盖 父类 NSObject 的 init
- (instancetype)init {
    // 注意!需要调用父类的init
    self = [super init];
    if (self) {
        self.name = @"James";
    }
    NSLog(@"A ByteDancer Joined!");
    return self;
}
@end

也可以Interface 声明一个需要参数的构造函数

// ByteDancer.h
@interface ByteDancer: NSObject
// MARK: 构造函数 - (instancetype)initWithName:(NSString *)name;
@end

// ByteDancer.m
@implementation ByteDancer
- (instancetype)initWithName:(NSString *)name {
    // 注意!需要调用父类的init
    self = [super init];
    if (self) {
        self.name = name;
    }
    NSLog(@"A ByteDancer, %@ Joined!", name);
    return self;
}
@end
 // main.m
 ByteDancer *byteDancer2 = [[ByteDancer alloc] initWithName:@"James"];

函数方法

OC的方法不管是介面还是实现一定由 加号或是减号 开头,+/- 號代表函数的類型:加號(+)代表類別方法(class method),不需要實例就可以呼叫,與C++ 的靜態函式(static member function)相似。減號(-)即是一般的實例方法(instance method)。

  • interface 如何声明函数
// ByteDancer.h
@interface ByteDancer: NSObject

// MARK: 函数声明 - (void)sayHello;
// (回传值)方法名:(参数1类型)参数1 标签:(参数2类型)参数2
- (void)groupWith:(NSString *)name;
// 返回对象
- (ByteDancer *)groupWith:(NSString *)name andWith:(NSString *)name2;

// 类方法(静态函数)
+ (int)staticFunction;
@end
  • 对应的 implementation
// ByteDancer.m
@implementation ByteDancer
//MARK: 函数实现 // 与 Interface一一对应,但完成函数主体实现
- (void)sayHello {
    NSLog(@"Hi");
}
- (void)groupWith:(NSString *)name {
    NSLog(@"Hi, I'm group with %@", name);
}
- (ByteDancer *)groupWith:(NSString *)name and:(NSString *):name2 {
    ByteDancer *group = [[ByteDancer alloc] initWithName:
    [NSString stringWithFormat:@"%@,%@,%@", self.name, name, name2]];
    return group;
}
+ (int)staticFunction {
    return 0;
}
@end

函数调用

  • 在OC里,函数的调用写法,是透过两个方括号包起来

    • 如果方法是类方法,就直接使用方框 - 类名 - 方法签名 - 方框
//main.m
#import "ByteDancer.h"
 
ByteDancer *byteDancer2 = [[ByteDancer alloc] initWithName:@"James"];
// 实例方法
[byteDancer2 sayHello];
// 实例方法 - 多参数, 返回对象
[[byteDancer2 groupWith:@"Andy" andWith:@"Amy"] sayHello]; //第一个函数返回对象后再调用

// 类方法 (静态函数)
int staticReturn = [ByteDancer staticFunction];

成员变量

变量的声明可以放在 interface中,也可以放在 implementation中,就跟方法一样取决于你是否要将这个变量供外部调用或存取,如果需要就要放在头文件的interface里让别人引入

// ByteDancer.h
@interface ByteDancer: NSObject{
    //公开变量 @public int public_int_variable; @protected double protected_double_variable; }

// ByteDancer.m
@implementation ByteDancer {
   // 私有
    int _private_int_variable;
    double _private_double_variable
}
@end
//main.m 外部访问公开成员变量
byteDancer3->_public_double_variable = 0.1;
byteDancer3->_public_int_variable = 0;
       
// ByteDancer.m 内部可以访问公开和私有的成员变量
self->_public_double_variable = 0.1;
self->_private_int_variable = 0;

\

Objective-C 语言特性

@property属性

基于面相对象封装性的角度,外部想要访问类中的成员变量,最好是要通过Get Set方法访问的,而不是直接访问变量,因为多个地方都能直接存取变量,会导致这个值容易错乱。

于是就有了属性,属性的语法,是在 Objective-C 2.0 才引入的用法,property已经是当前主要的变量声明方式,开发者已经不会在Interface去声明变量了

并且外部你可以透过 点符号访问属性,相等于访问对应的Get / Set 方法。

特别注意,自动声明的变量是带底线的,这就是为什么我们声明私有变量时习惯带底线。

// ByteDancer.h
@interface ByteDancer: NSObject
// MARK: 属性 @property (nonatomic, copy) NSString* name;
@end
  • 可以使用 . (点符号)调用对应Get / Set 方法
//main.m
// MARK: 对象外部
// 属性设置 == [byteDancer3 setName:@"James"]
byteDancer3.name = @"James";
// 访问属性 == [byteDancer3 name];
 NSString *yourName = byteDancer3.name;
 
 //MARK: 内部
 self.name = @"James";
 self->_name =  @"James";

声明属性等于声明跟实现了以下

// ByteDancer.h
@interface ByteDancer: NSObject
// MARK: 自动生成Get / Set 方法
- (void)setName:(NSString *)name;
- (NSString *)name;
@end

// ByteDancer.m
@implementation ByteDancer {
    // 自动声明变量 (带底线)
    NSString *_name;
}
// MARK: 自动实现
- (void)setName:(NSString *)name {
    self->_name = name;
}
- (NSString *)name {
    return _name;
}
@end

同学们需要注意分清楚 变量和属性的区别,开发者可以在类的实现里覆盖属性为我们实现的 Get / Set方法

但如果搞混了变量与属性,会导致程序无法正常运行而崩溃

// ByteDancer.h
@interface ByteDancer: NSObject
// MARK: 属性 @property (nonatomic, copy) NSString* name;
@end

下面这是错误用法

(在属性生成的Set方法设置属性 (Set方法)) -> 死循环

// ByteDancer.m
@implementation ByteDancer
// 覆盖 Set 方法
- (void)setName:(NSString *)name {
    self.name = [NSString stringWithFormat:@"%@ %@", name, @"!"];
}
@end

下面这是正确用法

// ByteDancer.m
@implementation ByteDancer
// 覆盖 Set 方法
- (void)setName:(NSString *)name {
    _name = [NSString stringWithFormat:@"%@ %@", name, @"!"];
}
@end

属性特性

www.jianshu.com/p/035977d1b…

协议

www.jianshu.com/p/d7dc2e54c…

协议的目的接近于使用父类,就是希望多个不同的类之间都有共通的方法或变量(也就是共通的介面 ),不需要每个类各自的Interface去声明方法

  • 协议声明
  • 类遵守协议 (Interface)
// ByteDancerProtocol.h
@protocol SeniorLevel <NSObject>
- (void)doCodeReview;
@end
@protocol JuniorLevel <NSObject>
// id<SeniorLevel> 代表任何遵守SeniorLevel的类
- (void)assignMentor:(id<SeniorLevel>)mentor;
@end
@protocol Developer <NSObject>
- (bool)doCoding;
@end // 协议可以继承多个协议
@protocol ClientDeveloper <Developer>
@end
#import "ByteDancerProtocol.h"
// ByteDancer.h
@interface SenioriOSDeveloper
: ByteDancer <SeniorLevel, ClientDeveloper>

@end
@interface JunioriOSDeveloper
: ByteDancer <JuniorLevel, ClientDeveloper>
// myMentor 类型是任何遵守 id<SeniorLevel> 的类
@property (strong, nonatomic) id<SeniorLevel> myMentor;
@end

\


  • 类实现协议方法 (implementation)
  • 创建遵守协议的对象语调用
// ByteDancer.m
@implementation SenioriOSDeveloper
- (bool)doCoding {
    return YES;
}
- (void)doCodeReview {
    NSLog(@"Code Review Done");
}
@end
@implementation JunioriOSDeveloper
- (bool)doCoding {
    return YES;
}
- (void)assignMentor:(id<SeniorLevel>)mentor {
    self.myMentor = mentor;
}
@end
//main.m
// 任何遵守 SeniorLevel 的对象都可以被指派
id <SeniorLevel> seniorEmployee
    = [[SenioriOSDeveloper alloc] initWithName:@"Bob"];
JunioriOSDeveloper *junioriOSDeveloper
    = [[JunioriOSDeveloper alloc] initWithName:@"James"];
// 也可以使用原本的方式创建对象
[junioriOSDeveloper assignMentor:seniorEmployee];
[junioriOSDeveloper doCoding];
[seniorEmployee doCodeReview];

\

协议的设计

  • 协议声明 与 系统类和自定义类
 //CommonProtocolDefineExample.h @protocol CommonDataSource <NSObject> // MARK: DataSource 一般用来要求协议的遵守者,提供数据或是其他信息 - (NSString *)dataRequestFrom:(NSString *)source; - (int)numerOfDatasFrom:(NSString *)source; @end  @protocol CommonDelegate <NSObject> // MARK: Delegate 一般用来要求协议的遵守者,提供数据或是其他信息 - (void)onSessionStateChange:(int)oldState newState:(int)newState; - (void)onError:(NSError *)error; @end
//MARK: 假设 系统网路请求类 @interface SystemNetworkService : NSObject // MARK: 底层的持有 DataSource和Delegate @property (weak, nonatomic) id<CommonDelegate> delegate; @property (weak, nonatomic) id<CommonDataSource> datasource; @end  //由自定义的类遵守两个协议 @interface MyClass : NSObject <CommonDataSource, CommonDelegate>  @end

方法 = 讯息传递

C++的函数调用在编译时,就已经确定了 调用A对象的C方法这个行为,基本上底层已经是一个对象方法调用的汇编代码。

但是在OC里,调用对象的方法,可以看做是 给对象发送一个消息,至于他要如何处理这条消息甚至可以到程序在运行时才去决定要如何去回应这个消息。

延伸阅读:Objective-C 消息发送与转发机制原理

你还可以使用Runtime框架,在程序运行时,动态的替换方法的实现指针,或是新增方法还有删除,这我们一般称为黑魔法

调用可以使用 respondsToSelector 这个方法,并传入方法签名(也称为选择器)

去确认这个对象有没有响应我这个方法,

如果有那就调用方法,如果没有那就使用默认数据。

MyClass *myObject = [[MyClass alloc] init];
if ([myObject respondsToSelector: @selector(numerOfDatasFrom:)]) {
    int dataCount = [myObject numerOfDatasFrom:@"main"];
    NSLog(@"%d", dataCount);
} else {
    // MyObject 不回应我的 numerOfDatasFrom 消息(没实现)
    int dataCount = -1;
    NSLog(@"%d", dataCount);
}

小结

总结一下需要注意几点:

  • Interface 和 implementation 之间的对应关系
  • Interface 写在 .h(头文件的)位置与被引用关系
  • 变量与属性的区别,属性的Get / Set 方法如何灵活使用

实战

实现一个简易的JSON解析成对象工具

main函数

可以看到文件的顶部帮我们引入了Foundation这个框架,Foundation包含了我们常用的许多类,例如字符串 / 数组/ 字典... 等常用的Class,都是系统在Foundation里声明与实现,我们会在下一节课去做介绍。

接著是main函数,跟C语言一样,main函数是一个程序的主入口,不管是命令行工具还是App,而main函数的参数则是当初启动程序时,外部传入的参数

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}

自定义解析类(interface)

// MyJsonParser.h
// 标注会用到 ByteDancer 而不直接引入 ByteDancer.h,可以有效避免头文件引入重复
@class ByteDancer;
@interface MyJsonParser : NSObject
- (instancetype)initWithFile:(NSString *)filePath;
- (ByteDancer *)parse;

@end

自定义解析类(implementation)

注意因为我们没有在头文件去真正引入 ByteDancer.h,所以在.m文件中必须去引入,否则编译器不知道 ByteDancer 的interface介面,就无法推断他有哪些方法

#import <Foundation/Foundation.h>
#import "MyJsonParser.h"
#import "ByteDancer.h"

@implementation MyJsonParser {
    NSString *_filePath;
}

- (instancetype)initWithFile:(NSString *)filePath {
    self = [super init];
    if (self) {
        _filePath = filePath;
    }
    return self;
}

- (ByteDancer *)parse {
    // 读取文件
    NSData *fileContent = [self readFile];
    // 解析JSON内容成字典
    NSDictionary *dictionary = [MyJsonParser parseJsonStr:fileContent];
    // 创建 ByteDancer对象
    ByteDancer *byteDancer = [[ByteDancer alloc] init];
    if ([dictionary objectForKey:@"name"]) {
        byteDancer.name = (NSString *)[dictionary objectForKey:@"name"];
    }
    return byteDancer;
}

- (NSData *)readFile {
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSData *content = [fileManager contentsAtPath:_filePath];
    if (!content)
    {
        exit(1);
    }
    return [fileManager contentsAtPath:_filePath];
}

+ (NSDictionary *)parseJsonStr:(NSData *)jsonData {
    return [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:nil];
}
@end

调用代码

从index = 1 开始才是外部的传餐,我们将参数转换成字符串,给到 jsonParser 对象

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        for (int i=0; i<argc; i++)
        {
            NSString *str = [NSString stringWithUTF8String:argv[i]];
            NSLog(@"argv[%d] = '%@'", i, str);
        }
        if (argc < 1) {
            NSLog(@"参数不足");
            return -1;
        }
        NSString *filePath = [NSString stringWithUTF8String:argv[1]];
        MyJsonParser *jsonParser = [[MyJsonParser alloc] initWithFile:filePath];
        ByteDancer *newByteDancer = [jsonParser parse];
        NSLog(@"欢迎! %@", newByteDancer.name);
    }
    return 0;
}

设置程序启动参数

接著我们点击上方的运行,就可以在控制台看到对应的日志信息

产物位置


运行产物

打开我们的终端,即可透过跟shell一样的语法,执行我们使用Objective-C编写的工具

admin@C02D37RGML85 ByteDanceCamp_Demo_cmd % cd /Users/admin/Library/Developer/Xcode/DerivedData/ByteDanceCamp_Demo-hbubaiudubqytwgenasnmclnbvmi/Build/Products/Debug
admin@C02D37RGML85 Debug % ./ByteDanceCamp_Demo_cmd /Users/admin/Documents/masterProject/ByteDanceCamp_Demo_cmd/ByteDanceCamp_Demo_cmd/myJson.json
2022-06-19 22:10:50.296 ByteDanceCamp_Demo_cmd[22588:14757072] argv[0] = './ByteDanceCamp_Demo_cmd'
2022-06-19 22:10:50.296 ByteDanceCamp_Demo_cmd[22588:14757072] argv[1] = '/Users/admin/Documents/masterProject/ByteDanceCamp_Demo_cmd/ByteDanceCamp_Demo_cmd/myJson.json'
2022-06-19 22:10:50.296 ByteDanceCamp_Demo_cmd[22588:14757072] A ByteDancer Joined!
2022-06-19 22:10:50.297 ByteDanceCamp_Demo_cmd[22588:14757072] 欢迎! 郭介騵

总结

总结一下我们今天的课程,

  • Xcode

    • Project是一个工程的核心,开发前一定会需要创建一个Project
    • Project里至少会有一个Target构建,可以是构建命令行程序或是App 程序...等
    • 我们可以往Project里添加代码文件(.m / .swift),并成为Target构建产物的一部分
  • Objective-C

    • Interface 和 implementation 要分清楚,.h / .m 的职责和引入关系也需要理清
    • 属性与变量的区别,改写属性的Get / Set 方法的注意事项,可以不在interface中声明变量,多使用属性控制
    • 注意协议的语法,自己开发的类遵守协议后别忘了实现需要的方法
  • 程序

    • main 函数,控制台打印,编译和运行程序,查询产物位置

建议阅读

chuquan.me/2021/12/03/…

static.kancloud.cn/digest/obje…

zh.wikipedia.org/wiki/Object…

victorleungtw.medium.com/connection-…

davedelong.tumblr.com/post/584281…

第三节:iOS 开发基础工具

课程大纲

image.png

iOS系统框架

框架 Frameworks

框架的原文是Frameworks,而Frameworks的本意,是指一个核心的支撑结构,任何东西都可以搭建在他之上。

而在软件开发的领域里,Frameworks指的是一个通用的,可复用,具备特定功能的软件或是环境,

他可能包含了代码库,API文件,或是一些工具的集合体。

Frameworks的提供方可能是系统,可能是其他开发者,或是软件开发公司,而他的用途呢就是支撑软件开发者能更容易更方便的达到某个程序功能,结合Framework的原意呢,就是任何人的程序都可以基于这个框架来开发。

iOS 框架 iOS Frameworks

那我们把这个Framework定义的范围收回来到我们苹果开发的领域,这个 Framework的定义就更明确了,

这里我们一样引用苹果官方文档的解释:

Framework是一个有层级的目录,他将 动态代码库,nib files,图片文件,头文件和参考文件 全部封装成一个单一的资源包,多个程序之间可以同时共用它,并被程序调用去执行某个任务。

简单来说,对于Xcode而言,Framework就是一个文件后坠为 .framework的文件包,里面包含了与其相关的文件,程序在启动时会将它加载进内存。


iOS Framework 文件夹

通常一个Framework 的文件目录,就像图片这样,其中有几个重要的地方

  • 第一个就是Headers文件夹,里面是存放了大量的.h头文件,里面记录了这个Framework对外开放类的声明或是一些静态变量...等
  • 再来是这个跟Framework同名的二进至文件,它的类型在Finder显示是 Unix executable 可执行文件,大家可以理解为里面记录的就是系统的实现代码被编译后的产物。
  • 如果Framewoek有携带其他资源文件的话,会放在这个Resources的文件夹下

Framework 就是已一个这样像资源包的形式去供开发者使用的,当然除了系统提供的之外,你也可以自己去开发Framework或是使用其他三方提供的Framework


  • Foundation.framework / Headers

iOS 系统框架 iOS System Frameworks

苹果官方为开发者准备了上百个稳定且成熟的框架,并且在每年的WWDC大会上都会机会看到一些新的框架加入,这也使iOS开发者每年都会对WWDC非常期待,然后在会前会后也会有很多的交流,可能是对于这些新框架的观察,调查跟使用的心得,也有些时候是老框架的一个升级更新,当然也把一些老的框架淘汰掉

而大部分的框架都只提供了 Objective-C 跟 Swift的接口,这也就是我在上一堂课说的,为什么开发iOS的主流语言是 Objecitve-C 和 Swift。


目前框架有:developer.apple.com/documentati…

系统框架分层

所有的系统框架可以被分作四层(四个Layer),分别是 Cocoa Touch (触摸层),Media (媒体层),Core Services (核心服务层),以及 Core OS (操作系统层),

处于上层的框架会去依赖底层的框架,但底层的框架它不会有依赖上层的情况


系统框架分层 - Core OS

位于最底层的是CoreOS 核心操作系统层,这层提供的这些框架,已经是苹果提供的最靠近硬件的框架,通常情况下,这些功能不会直接应用于我们的应用程序,而是应用于其他框架。但是,在直接处理安全事务或和其他设备通讯的时候,则必须要应用到该层的框架。

  • 你可以利用CoreBluetooth 框架使用蓝牙和外部设备交互
  • Security框架提供管理证书,公钥私钥的策略
  • 如果有复杂的计算可以使用Accelerate框架,委托硬件进行计算

系统框架分层 - Core Service层

第二层是提供应用程序所需要的基础系统服务的核心服务层 Core Service ,

系统框架分层 - Media层

媒体层里的框架功能就更明确了,是提供包括图行,音频,视频相关的技术支持

如果你想要开发一个短视频/直播,或是滤镜相机之类的App,那就会需要跟这一层的框架多打交道了。


系统框架分层 - CocoaTouch层

最上一层 CocoaTouch层,大部分框架都与界面有关,也都具备自己特定的UI介面

  • 其中包含了开发App的最重要的框架之一 UIKit,UIKit提供了大量的UI控件,例如按钮, Label,滚动列表,宫格视图,以及App的页面UIViewController 和 UIView试图

    • 一个App的使用介面,跟页面之间的跳转都离不开UIKit,我们也会在后面的课程专门去介绍UIKit 介绍如何搭建App的页面。
  • 其他像是MapKit地图框架 和 NotificationCenter 通知中心框架也是开发常用的。

系统框架存放位置

当初在安装Xcode时,Xcode内部就自带了各个平台的SDK文件目录,我们可以从Finder找到各个平台SDK文件夹,每个SDK下的 /System/Library/Frameworks 就能看到对应所有的系统框架了,

那实际上运行在设备上时,设备上的iOS文件路经中,也有 /System/Library/Frameworks 文件夹,里面也放著对应的系统框架,并且是包含实现的也就是包含可执行文件。于是我们的App在启动时,也能链接到对应的Framework,并且正常执行

Foundation框架

Foundation 框架

Foundation框架为 App和其他框架提供了基础的能力,包括 数据存储持久化,文本处理,日期时间计算,排序筛选,还有网路。并且Foundation框架的能力是MacOS, iOS, watchOS, tvOS通用的


developer.apple.com/documentati…

引入 Foundation.h (框架伞头文件)

上面那行 import <Foundation/Foundation.h>就是引入Foundation框架的Foundation头文件,

而Foundation头文件里面的内容呢,就是把所有Foundation框架中公开类的头文件给引入了,那这样我们一行引入就等于把所有的类的声明都给引入了,像Foundation.h这种没有其他声明,但是把所有公开类都引入的头文件,我们称作 Umbrella Header 伞头文件

引入了伞头文件,那我就不需要个别去引入每个要用类的头文件了。

//main.m 
// 引入 Foundation框架的 Foundation.h头文件
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello");
    }
    return 0;
}

NSObject 类

//NSObject.h
@protocol NSObject
//.....
- (id)performSelector:(SEL)aSelector;
- (BOOL)isKindOfClass:(Class)aClass;
//.....
@end

@interface NSObject <NSObject> {
//.....
+ (instancetype)alloc
- (instancetype)init
//.....
}
  • 所有类的根类 (root Class)
  • 大量的适应Objective-C语言的方法
  • NSObject是一个遵守NSObject协议的类

\

补充 - 如何查看系统头文件

关于如何查看NSObject这种系统类的头文件,这里分享一些方法

  • 第一个方法是 NSObject关键字的地方,例如我自己声明的类,按住 command键在点击NSObject关键字,并跳转到定义
  • 第二个方法是在Xcode任意编辑页面中,按著 command + shift + o,会跳出一个搜索框,搜索NSObject.h 即可

线上版本 developer.apple.com/documentati…


  • 按住 command键在点击NSObject关键字

  • command + shift + o,搜索NSObject.h

NSObject - 分配内存空间 & 初始化

我们在创建任何一个对象时,都会用到这两个方法 alloc 和 init

首先调用的静态方法 alloc 系统会为这个对象分配内存并返回一个空的实例

之后在调用init实例方法进行初始化,通常子类也会覆盖这个方法去做相应的初始化

还有另外一个用法 new ,它等于 alloc + init,我们自己定义的类或是其他系统类,也都可以使用 new 去创建对象,代替 alloc + init

// NSObject.h
@interface NSObject <NSObject> {  
/** 为新对象分配内存空间 */  
+ (instancetype)alloc;
/** 初始化对象 */  
- (instancetype)init;

/** 为新对象分配内存空间并初始化, 等于[[NSObject alloc] init] */  
+ (instancetype)new;
}
//main.m
ByteDancer *byteDancer1 = [[ByteDancer alloc] init];
// 等同于
ByteDancer *byteDancer2 = [ByteDancer new];

NSObject - 发送消息(方法调用)

透过 performSelector这种方式调用呢,不会在编译时做任何的较验,也就是你可以去创建一个该对象没有在interface声明的方法选择器,并调用,那由于没有编译器把关,如果该方法确实没有实现那就会崩溃,所以一般在使用时会搭配 respondsToSelector 提前确认该对象是否响应消息

通常这种 interface没有声明还要使用performSelector去掉用的情况:

  • 第一个可能是这个方法是在运行时透过Runtime添加进来的方法,在编译时不存在
  • 第二种可能是在明确知道该对象有些实现私有方法,但没有声明在interface时,会使用 performSelector去掉用
// NSObject.h
@protocol NSObject  
/** 发送指定的消息给对象, 返回消息执行结果(相当于方法调用) */  
- (id)performSelector:(SEL)aSelector;  
/** 发送带一个参数的消息给对象, 返回消息执行结果(相当于方法调用) */  
- (id)performSelector:(SEL)aSelector withObject:(id)object;  
/** 判断对象是否能够调用给定的方法 */  
- (BOOL)respondsToSelector:(SEL)aSelector; 
@end

// main.m
// 先判断对象是否能调用方法,再执行调用方法  
if ([bytedancer1 respondsToSelector:@selector(sayHello)]) {  
// 调用无参无返回值方法  
    [bytedancer1 performSelector:@selector(sayHello)];  
}  
// 可以用于调用私有方法
if ([bytedancer1 respondsToSelector:@selector(private_method)]) {  
    [bytedancer1 performSelector:@selector(private_method)];  
}

NSObject - 类关系判断

最后还有一些方法是用来判断对与对象之间类的关系,以及获取对象的类的方法

这里可以注意一个特别的类型 Class,这种参数它不是对象,就是一个类的型别

判断类别常用在你不确定某个对象的类型时,例如你从一个数组或字典里取出来一个对象,你只能确定这个对象保存的可能是字符串,数字,或是某个字定义类,这时候你就需要先使用if判断加上 isKindOfClass方法,你能确定他是某个类型后,在去做类型转换根方法调用

// NSObject.h
/** 获取当前对象的类 */  
- (Class)class;
/** 获取当前对象的类 */  
- (Class)superclass;

/** 判断对象是否是给定类或给定类子类的实例 */  
- (BOOL)isKindOfClass:(Class)aClass;  
/** 判断对象是否是给定类的实例 */  
- (BOOL)isMemberOfClass:(Class)aClass;  
/** 判断对象是否遵从给定的协议 */  
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;  

类关系判断使用场景

// 字典对应的不确定类型
NSObject *getValue = [dictionary1 objectForKey:@"key1"];
if ([getValue isKindOfClass:NSString.class]) {
    // 确定是NSString 在转型成NSString
    NSString *strValue = (NSString *)getValue;
    [strValue length];
} else if([getValue isKindOfClass:ByteDancer.class]) {
    // 确定是ByteDancer 在转型成ByteDancer
    ByteDancer *byteDacner1 = (ByteDancer *)getValue;
    [byteDacner1 sayHello];
} else {
    NSLog(@"unkown class");
}

数据类型

NSString 字符串

NSString是在Objective-C语言里最常用的专门处理字符串的类,

为什么不说他是字符串呢,因为严格来说NSString并不是OC的字符串纯值,OC的字符串纯值可以使用C语言的 char形式的纯字符串。而NSString 它继承自NSObject,有方法有变量,妥妥的是个类,不过一般开发不太会使用纯字符串,因为他不好跟其他数据交互,而是使用NSString,所以我们还是把它视为OC里的字符串。

// C语言字符串(基础数据类型)
char *cString = "James";

//NSString.h (专门处理字符串的类)
@interface NSString : NSObject <NSCopying, NSMutableCopying, NSSecureCoding>
//......
@property (readonly) NSUInteger length;
- (unichar)characterAtIndex:(NSUInteger)index;
- (instancetype)init;
//.....
@end

NSString - 创建

NSString有许多种的创建方式

可以注意到若是双引号前面不加 @符号,那就是纯的C字符串,

如果使用了 @双引号,那实际上你已经创建了一个NSString对象了

当然你也可以透过刚刚介绍的父类NSObject的 alloc init方法,像其他类一样创建字符串对象

// @"" 创建 NSString 对象 (常用)
NSString *stringObject = @"hello";

// C语言字符串
char *cString = "James";
// C语言字符串 -> NSString 对象
NSString *stringObject1 = [NSString stringWithUTF8String:"James"];

// 初始化一个空字符串
NSString *stringObject2 = [[NSString alloc] init];
// 初始化一个字符串
NSString *stringObject3 = [NSString stringWithString:@"hello"];
// 拼接一个字符串
NSString *stringObject4 = [stringObject3 stringByAppendingString:@" world"];

NSString - 类型转换

如果要将其他类型单独或组合转换成字符串,可以使用initWithFormat初始化方法,使用类似C语言的格式化表达创建,其中常用的格式例如 %d表达整数,%f 表达浮点数,还有一种 %@ 去获得一个对象的描述

如果%@是用在NSString对象上那就是该字符串的值,如果是用在其他对象上则会获得该对象description方法里的回传值,通常是该对象的类和内存地址描述

如果要将现有的字符串转成基础类型,可以直接使用 NSString提供的方法转成 int, float, double等等

// 创建格式化字符串 - @"123 , str"
int a = 123;
NSString *stringObject5 = [[NSString alloc]initWithFormat:@"%d , %@", a, "str"];

// MARK: NSString转成基础类型
NSString *numberStr = @"123";
// BOOL
BOOL boolValue = [numberStr boolValue];
// int
int intValue = [numberStr intValue];
//float 
float floatValue = [testStr floatValue];
// double
double doubleValue = [testStr doubleValue];

NSString - 子字符串

NSString *stringObject5 = @"hello";

// 获取字符串的长度
NSUInteger length = [stringObject5 length];
// 获取索引下标的字符
unichar index_char = [stringObject5 characterAtIndex:0];

// 截取字符串,从索引位置到结尾
NSString *subStr1 = [stringObject5 substringFromIndex:1];
// 截取字符串,从开始到索引位置
NSString *subStr2 = [stringObject5 substringToIndex:3];
// 截取字符串,从索引开始,取长度个数组成的字符串
NSRange range = NSMakeRange(1, 2);
NSString *subStr3 = [stringObject5 substringWithRange:range];

NSArray 数组

NSArray跟NSString一样是NSObject子类,NSArray它也不是纯值类型了,所以具备了许多实现好的数组访问方法。如果你用过C语言的数组,就会觉得OC的NSArray实在是太方便了。

跟NSString一样,创建NSArray也有简写,使用 @ + 方框 就相当于创建一个NSArray对象

需要注意的是:NSArray中不能存放 int, float, bool 等基本类型,只能保存Objective-C对象,但是一个数组对象里面可以保存不同Class的对象,

因为每个Objc的对象实际是一个指针,所以从指针的这个角度来看的话,数组存放的每一项不管是什么类的对象都是等长的。这样处理比如length,get、add等操作的时候就非常简单了。

如果想保存基本类型的话可以透过NSNumber这个类去封装后在放进数组里

另外你也可以在声明时,限制数组保存同一种对象类型

// 空数组
NSArray *arr1 = [NSArray array];
// 效果同上
NSArray *arr2 = [[NSArray alloc] init];

// @[...] = NSArray对象创建
NSArray *arr3 = @[@"iOS", @"Android", @"Server"];
// 效果同上
NSArray *arr4 = [NSArray arrayWithObjects:@"iOS", @"Android", @"Server"];

// 如果需要放基本类型,可使用NSNumber or @(value) = 自动转成NSNumber对象
NSNumber *numberObject = [[NSNumber alloc] initWithInt:100];
NSArray *arr4 = [NSArray arrayWithObjects:@(123), numberObject];

// 限制存放对象为NSNumer
NSArray<NSNumber *> *numberArrayObject = @[@(1), @(-1), @(3.3)];

NSArray 查询

需要特别注意得是数组的查询都是针对对象指针的,也就是如果你拿另外一个虽然是同样内容的字符串查询,但因为不是同个对象,也会有查询不到的情况。

如果你想要寻找同等值的,可以使用 for循环去遍历数组,并比较字符串的内容

NSString *str = @"bytedance";
NSArray *array = @[str, @"iOS", @"Android"];

// count  数组中所含元素个数
NSLog(@"count = %d", array.count);
// 返回对应位置对象
NSObject *obj1 = array[0]
// 返回元素对应位置(认对象)
NSUInteger index = [array indexOfObject:obj1];
// lastObject  返回数组最后一个元素
NSObject *obj2 = [array lastObject];

// containsObject   是否包含指定对象
if ([array containsObject:str]) {
    // true
}
if ([array containsObject:@"iOS"]) {
    // true
}

//使用for循环遍历
for (NSString *strObj in array) {
    NSLog(@"%@", strObj);
}

不可变数组 NSArray vs. 可变 NSMutableArray

刚刚介绍的NSArray数组,在设计上,它并不能添加或删除树组里面的元素,也就是一旦这个NSArray对象一旦被创建了就没法修改

如果你需要一个可以增删的数组,就必须使用 NSArray的子类 NSMutableArray,只有NSMutableArray才有声明增删方法,

 @interface NSMutableArray<ObjectType> : NSArray<ObjectType>
// 以下方法NSArray无
- (void)addObject:(ObjectType)anObject;
- (void)insertObject:(ObjectType)anObject atIndex:(NSUInteger)index;
- (void)removeLastObject;
- (void)removeObjectAtIndex:(NSUInteger)index;
@end

我们对于 不可变有时也叫只读。 不可变它是个约束限制,而这种限制可以减少对象传递上的不确定性,我们举个例子

如果一个方法doSomething参数宣告的是NSMubaleArray

那doSomething 里面就有可能修改 array 的内容,这样外部使用这种函数的时候,就需要考虑数据被修改应该怎么办?是不是应该先复制一份,防止修改呢?这样就多了不确定性。

如果是NSArrays那就没有这个问题

除了用法上的区别之外,实际上 NSArray的底层实现与NSMutableArray的底层实现区别非常大,NSArray的查找性能也确实优于NSMutableArray

// 可变
NSMubaleArray *arr1 = .....
doSomething(NSMubaleArray* array);
// arr1 是否已经被修改
// 不可变
NSArray *arr2 = .....
doSomething(NSArray* array);
// arr2 还是原数据
  • 对象数据可能被修改
  • 线程不安全
  • 查找性能较差
  • 一般用作方法内的临时变量,可做增删
  • 传进去的对象不会被修改
  • 线程安全
  • 查找性能好
  • 用于属性声明或是方法参数/回传值
// NSArray -> NSMutableArray
NSMutableArray *mutableArr1 = [[NSMutableArray alloc] initWithArray:numberArrayObject];
// NSMutableArray -> NSArray
NSArray *fixArr1 = [[NSArray alloc] initWithArray:mutableArr1];

NSMutableArray 数据增删

如果在声明数组时没有限制数组的存放类型的话,那么任何对象都能被新增到数组里

可以单独新增对象,可以指定添加的位置,还能和入另外一个Array

删除则可以使用 删除末尾, 删除指定位置,和移除全部...等

除了这些方法还有需多修改方法,同学们可以查看NSMutableArray的头文件了解更多。

//MARK: NSMutableArray 数组增删 NSMutableArray *mutableArrayObject = [[NSMutableArray alloc] init];
// 新增任意对象
[mutableArrayObject addObject:@"StringObject1"];
// 在指定下标添加对象
[mutableArrayObject insertObject:@(0) atIndex:0];
// 批量添加
[mutableArrayObject addObjectsFromArray:@[@"StringObject2", @(1)]];

// 移除末尾
[mutableArrayObject removeLastObject];
// 删除指定下标
[mutableArrayObject removeObjectAtIndex:0];
// 移除全部
[mutableArrayObject removeAllObjects];

NSDictionary 字典

字典它是一个基于Key-Value键值对访问的数据类型,我们在上一堂的实战中有使用到NSDictionary 来保存我们的JSON数据

跟字符串和数组类似,使用 @花括号就可以快捷的创建字典对象,

我们一般就使用NSString当做字典的Key做访问,而Value呢跟数组一样必须是个Objective-C的对象,如果要保存基础类型一样可以先转成NSNumber。

NSDictionary本身也是个OC对象,所以字典的Value也可以是字典对象,

// @{} = 创建NSDictionary对象
NSDictionary *dict1 = @{
    @"key1":@"value1"
};
// 效果同上
NSDictionary *dict2 = [NSDictionary dictionaryWithObject:@"key1" forKey:@"value1"];

// 多Key
NSDictionary *dict1 = @{
    @"key1":@"value1",
    @"key2":@"value2"
};
//效果同上
NSDictionary *dict5 = [[NSDictionary alloc]initWithObjects:@[@"value1",@"value2"] 
                                                   forKeys:@[@"key1",@"key2"]];
// 指定保存类型   
NSDictionary<NSString*, NSNumber*> *dictionaryObject = @{
    @"numberKey1": @(0),
    @"numberKey2": @(1),
}                                                                  

NSDictionary 访问

NSDictionary字典的访问方法跟NSArray数组类似,不过把访问的下标从整数替换成了字符串

如果这个字典在声明时没有加上类型限制Value是什么类型那不管使用key下标直接访问或是透过 objectForKey 方法访问,返回的都是id类型,也就是可能是任意类型的对象,

在调用这个从字典里取出来的对象方法时,谨慎一点都要先判断过类型,否则如果这对象不是我们预期的类型,我们还去掉用方法,就会导致崩溃

NSDictionary *dictionary1 = @{
    @"key1": @"value1"
};

// count  (字典元素个数   key:value 是一一对应的,所以是算作一个的)
int dictionary1Count = [dictionary1 count];

// objectForKey:  (获取一个key对应的值)
NSObject *getValue = [dictionary1 objectForKey:@"key1"];
// 等同
id getValueDirect = dictionary1[@"key1"];

// allKeys   (获取key集合)
NSArray *keysArr = [dictionary1 allKeys];

//allValues   (获取所有value)
NSArray *valuesArr = [dictionary1 allValues];

// 遍历所有Key,并访问对应Value
[dictionary1 enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
     NSLog(@"key = %@  value = %@",key,[dictionary1 objectForKey:key]);
}];

NSMutableDictionary 增删

字典也分为了不可变字典NSDictionary以及可变字典NSMutableDictionary

如果在声明字典对象时没有指定任何类型,那这个字典就可以添加任何的NSObject子类对象。

NSMutableDictionary *mutableDictionary = [[NSMutableDictionary alloc] initWithDictionary:@{
  @"key1": @"value1"
}];

//setObject:(setValue:)forKey:   (设置某Key对应值)
[mutableDictionary setObject:@"value2" forKey:@"key2"];
[mutableDictionary setValue:@"value3" forKey:@"key3"];
mutableDictionary[@"key4"] = @"value4";

//removeObjectForKey:   (移除某一个key对应的元素)
[mutableDictionary removeObjectForKey:@"key2"];
//removeAllObjects  (移除所有元素)
[mutableDictionary removeAllObjects];

实战 - 断点与调适

创建iOS App Target

一样是先创建一个Xcode Project,你也可以使用上堂课实战的Project的基础上新增一个Target,然后在选择Target的页面上,选择 iOS标签中的 App Target

App程序初始文件

  • main.m -> 程序的入口
  • AppDelegate -> App的入口代码
  • ViewController -> App的第一个页面代码

\

\

main.m

// App Target's main.m
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
     // 程序入口 -> App入口
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

// Command Line Tool Target's main.m
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello");
    }
    return 0;
}

选择Scheme(方案)

官方给出的定义如下

A scheme is a collection of settings that specify the targets to build, the build configuration, and the executable environment. Xcode creates a default scheme for each target in your project

一个Scheme是定义了 要用什么配置,什么运行参数 去执行哪个Target。

Xcode会默认为每一个Target新建一个默认的Scheme

--

简单来说Scheme就代表我要如何去执行我的Target,是要用Debug模式还是用Release模式,

启动的时候要加上什么环境变量

我们在上一堂课的实战中,为我们的终端程序指定一个 JSON文件的路径,其实改的就是 终端Scheme的运行参数


一个Scheme是定义了 要用什么配置,什么运行参数 去执行哪个Target。

Xcode会默认为每一个Target新建一个Scheme

运行装置

而在Scheme选择的右边则是运行装置选择,

如果选择的Scheme是对应mac平台的Target,那在右边的运行装置就可以选择在mac电脑上运行,

那如果我们选择的Scheme是刚刚创建的App Target,那么我的运行装置选项就会对应著接入电脑的真实设备以及Xcode附带的iphone模拟器

运行模拟器

为了比较能看清我们成功的把App执行在模拟器上,我们在App第一个页面也就是ViewController.m里写上这样一个代码后,并点击执行按钮

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 350, 100)];
    [label setText:@"Welcom, 我的第一个App"];
    [self.view addSubview:label];
}

在你点击执行按钮后,Xcode会把这个App Target下的所有代码文件编译可执行文件,

并且生成 .app 产物,之后就会将 .app装到你选择的模拟器上,开启模拟器,并发送执行你的App的指令

那如果你看到刚刚设置的文字,出现在模拟器上,那就代表你已经成功执行你的第一个iOS App


\

代码调适工具 - LLDB

Xcode内置了一个代码调适工具叫做LLDB,他是一个开源项目,是Xcode的LLVM编译器项目中的一个Debug组件,所以他也使用了许多LLVM在使用的子组件(例如Clang表达是解析器跟反汇编器),

这也让LLDB天生跟编译器有著很好的相容性,使他与Xcode高度的融合,你不仅可以用它来调适对象和变量,也可以用它来调适当前页面的UI


  • LLVM编译器

  • LLDB调适组件

代码调适 - 手动断点

手动断点的用法非常简单,就是在断点的代码位置左侧,点击出一个蓝色的箭头,

那程序运行到该代码就会进入断点状态,程序会暂停,此时你就可以根据当前的对象状态去找出Bug的根因。

进入调适介面

那当程序进入断点状态时,Xcode就会切换成调适介面,在这个介面你有几个功能窗口,能够帮你更好的定位问题

  • 第一个是左边Debug区,现在正展试著代码堆栈,代码堆栈会展示当前程序的方法调用栈,可以帮你清楚的了解到为什么你的程序会走到这行代码之中,点击栈的任意一步,可以跳转到对应的代码
  • 第二个是对象变量区,你可以在这里看到目前的对象或是变量,包括临时对象也包括当前被呼叫的对象self
  • 第三是控制台区,你可以在这里看到断点之前的日志打印,通常碰到一些系统错误,都会有日志打印具体问题,除了查看日志外,你还可以在这里输入调适指令,打印当前程序状态。
  • 以及一个程序控制区,在进入断点状态后,你可以操控程序一步步的逐行执行,可以跳出某个方法,或是进入某个方法内。

常用调试指令介绍 - p / po

lldb指令ppo
范例
执行将右边表达式的计算结果保存在 lldb临时变量中,可供本次调试使用打印右边对象的 debugDescription 方法
本质expression --expression -O -- expression —object-description

常用调试指令介绍 - expression

那我们就接著介绍expression命令,expression命令可以简写成e,

该命令会通过LLDB format格式求出一个表达式的值,我们可以用它来赋值,修改变量值,求一个表达式结果,调用方法等等,更多功能我们可以通过help e来查看文档学习

  • Breakpoint 指令

程序控制区

最后我们来介绍下程序控制区,程序控制区有以下几种功能

  • 首先是 程序继续执行结束暂停状态,直到碰到下个断点
  • 第二个是 stepover,每点击一次程序就会往下执行一行,如果是方法调用,会保持在当前堆栈正常执行完方法
  • 第三个是 setp into,如果当前行是方法调用,会进入到该方法的堆栈里
  • 第四个是stepout,会直接执行到当前方法执行完的堆栈中,如果确定当前方法没有问题时,可以使用
  • 第五个是在调适UI介面非常好用的UI视图层级检视,点击后可以查看当前App的视图情况
  • 第六个则是在查询内存泄漏特别好用的,内存分布图,可以查看到每个对象的持有状况,能看到一个对象持有其他对象或被其他对象持有的的关系,