Objective-C
- Load和initialize的不同
调用顺序不同,以
main
函数为分界,+load
方法在main
函数之前执行,+initialize
在main
函数之后执行。子类中没有实现
+load
方法的话,子类不会调用父类的+load
方法;而子类如果没有实现+initialize
方法的话,也会自动调用父类的+initialize
方法。
+load
方法是在类被装载进来的时候就会调用,+initialize
在实例化对象的时候调用,并且只会调用一次,是懒加载模式,如果这个类一直没有使用,就不会调用到+initialize
方法。
- NSInteger 和 int 的区别
NSInteger
是基本数据类型,并不是NSNumber
的子类,当然也不是NSObject
的子类。NSInteger
是基本数据类型Int
或者Long
的别名(NSInteger
的定义typedef long NSInteger
),它的区别在于,NSInteger
会根据系统是32位还是64位来决定是本身是int
还是long
。
- id 声明的对象有什么特性?
id 声明的对象具有运行时的特性,即可以指向任意类型的Objcetive-C的对象
- HTTPS和HTTP的区别
- https协议需要到ca申请证书,一般免费证书很少,需要交费。
- http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议。
- http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
- http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
- 为什么我们常见的delegate属性都用是weak而不是retain/strong?
为了避免循环引用和内存泄漏。
@property (nonatomic, weak) id<UITableViewDelegate> delegate;
在iOS中,UI控件的代理通常是由控制器管理的。为了防止控制器和控件之间的循环引用,代理属性通常使用weak修饰符。
- KVC的底层实现
当一个对象调用setValue方法时,方法内部会做以下操作:
1). 检查是否存在相应的key的set方法,如果存在,就调用set方法。
2). 如果set方法不存在,就会查找与key相同名称并且带下划线的成员变量,如果有,则直接给成员变量属性赋值。
3). 如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值。
4). 如果还没有找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。
这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。
- KVO内部实现原理
1.KVO是基于runtime机制实现的
2.当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制
3.如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
5.键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey: 和 didChangevalueForKey
- 你是否接触过OC中的反射机制?简单聊一下概念和使用
1). class反射
通过类名的字符串形式实例化对象,将类名变为字符串。 NSClassFromString 、NSClassFromString
2). SEL的反射 NSSelectorFromString、NSStringFromSelector
- 内存泄漏检查
静态分析方法(Analyze)和动态分析方法(Instrument的leak)。
Analyze 优点:
1). 能够在编码阶段,开发自行进行代码检查。早期发现代码隐患。
2). 直接分析源代码来发现程序中的错误,而不需要实际运行。
3). 自动检测Objective-C程序中的BUG,发现内存泄露和其它问题。
4). 内存问题发现越早,解决的代价就越小。
主要分析以下四种问题:
1). 逻辑错误:访问空指针或未初始化的变量等;
2). 内存管理错误:如内存泄漏等;
3). 声明错误:从未使用过的变量;
4). Api调用错误:未包含使用的库和框架。
Instruments里面工具很多,常用:
1). Time Profiler: 性能分析
2). Zombies:检查是否访问了僵尸对象,但是这个工具只能从上往下检查,不智能。
3). Allocations:用来检查内存,写算法的那批人也用这个来检查。
4). Leaks:检查内存,看是否有内存泄露。
- 用Swift 将协议(protocol)中的部分方法设计成可选(optional),该怎样实现?
Swift中,默认所有方法在协议中都是必须实现的。 在协议前加上
@objc
,然后再在方法前加上@objc optional
- struct与class 的区别
struct是值类型,class是引用类型。
值类型的变量直接包含它们的数据,对于值类型都有它们自己的数据副本,因此对一个变量操作不可能影响另一个变量。
引用类型的变量存储对他们的数据引用,因此后者称为对象,因此对一个变量操作可能影响另一个变量所引用的对象。 二者的本质区别:struct是深拷贝,拷贝的是内容;class是浅拷贝,拷贝的是指针。
property的初始化不同:class 在初始化时不能直接把 property 放在 默认的constructor 的参数里,而是需要自己创建一个带参数的constructor;而struct可以,把属性放在默认的constructor 的参数里。
变量赋值方式不同:struct是值拷贝;class是引用拷贝。
immutable变量:swift的可变内容和不可变内容用var和let来甄别,如果初始为let的变量再去修改会发生编译错误。struct遵循这一特性;class不存在这样的问题。
mutating function: struct 和 class 的差別是 struct 的 function 要去改变 property 的值的时候要加上 mutating,而 class 不用。
继承: struct不可以继承,class可以继承。
struct比class更轻量:struct分配在栈中,class分配在堆中
- swift把struct作为数据模型
优点
安全性: 因为 Struct 是用值类型传递的,它们没有引用计数。
内存: 由于他们没有引用数,他们不会因为循环引用导致内存泄漏。
速度: 值类型通常来说是以栈的形式分配的,而不是用堆。因此他们比 Class 要快很多!
拷贝:Objective-C 里拷贝一个对象,你必须选用正确的拷贝类型(深拷贝、浅拷贝),而值类型的拷贝则非常轻松!
线程安全: 值类型是自动线程安全的。无论你从哪个线程去访问你的 Struct ,都非常简单。
缺点
Objective-C与swift混合开发:
OC调用的swift代码必须继承于NSObject。
继承:struct不能相互继承。
NSUserDefaults:Struct 不能被序列化成 NSData 对象。
- 如何设置实时渲染?
@IBDesignable让Interface Bulider在特定视图上执行实时渲染
- swift中,关键字 guard 和 defer 的用法
guard是一个条件声明,用于在条件不满足时退出当前代码块。其基本语法如下:
guard condition else {
// 处理条件不满足的情况
}
defer关键字用于延迟执行某些操作,直到包含它的函数或方法结束。其基本语法如下:
defer {
// 延迟执行的代码
}
15. @property 的本质是什么?
@property的本质是实例变量(ivar)+ 存取方法,即 @property = ivar + getter + setter;
属性” (property)作为 Objective-C 的特性,主要用于封装对象中的数据。 Objective-C 对象数据保存为实例变量。实例变量一般通过“存取方法”来访问。 getter读取,setter写入。
- 什么情况使用 weak 关键字,相比 assign 有什么不同?
什么情况使用 weak 关键字?
循环引用的时候,比如: delegate 代理属性
xib的IBOutlet 控件属性一般也使用 weak
不同点:
assigin 可以修饰非 OC 对象,比如简单数据类型 int bool,weak 必须用于 OC 对象,
weak 是弱引用,被weak修饰的对象,引用计数不会加1, 不会产生野指针。weak修饰的对象释放后,指针会自动被置nil,是安全的。
assign 修饰基本数据类型是安全的。如果修饰对象,会产生野指针;修饰的对象释放后,指针不会自动置空。
所以一般都是用 assign 来修饰基本类型,weak 来修饰对象
- __weak __block
通过 __weak 弱引用,来打破循环引用。
__block:__block修饰的变量可以修改。在代码块中会被retain(ARC)__weak:__weak修饰的变量不可以修改。不会在block代码块中被retain
__block不能修饰全局变量、静态变量(static)
- App的优化
- 启动过程耗时
- 优化内存泄露,包括循环引用
- 优化闪退或卡死
- 优化过大图片、冗余文件
- 控制ipa包体积
- 页面流畅度
- cocoapods
Swift Package Manager
- Alamofire:http网络请求框架 要在Alamofire之后解析JSON,定义一个遵循Codable协议的Swift结构体或类,然后使用 JSONDecoder,可以设置 CodingKeys
- SnapKit:autoLayout自动布局框架
- Kingfisher:喵神王巍写的一款关于图片下载、缓存的框架。灵感取自OC里面的SDWebImage
- RxSwift:函数响应式编程框架,是ReactiveX的swift版本,可以简化异步操作和事件/数据流
- ObjectMapper:把json对象映射为model对象
- 不可变对象的copy是浅复制,mutablecopy是深复制
- 可变对象的copy与mutablecopy都是深复制
- 关于WKWebview
1)、相较于UIWebview,WKWebview占内存小,加载快,增加了加载进度条。
2)、出现白屏的原因和解决办法:
① 网页内存过大会白屏,需要在webViewWebContentProcessDidTerminate方法中调用[webview reload];方法。
② url中有中文字符,需要后台配合修改;
③ h5中使用了第三方组件,加载失败,先是白屏。一般正式环境下会恢复正常。3)、关于cookie
为了解决加载H5界面无法携带原生界面登录用户信息问题,除了在url上拼接用户uid等信息的方法,还可以在请求的时候,拦截请求头,将这些信息拼接起来,使用request的set value方法,将这些信息传递过去。4)、原生界面和H5界面的交互:
JS调用OC方法:WKWebview中有一个WKScriptMessageHandler协议,想要实现交互,需要定义一个方法名,使用[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"methodname"]方法设置,并通过userContentController:didReceiveScriptMessage方法,获取对应名称的数据体进行解析。
OC调用JS:通过evaluateJavaScript方法调用JS方法,需要在HTML文件中定义方法名,在原生这里调用。5)、获取网页高度
① 在webviewdidFinishNavigation中获取高度。
② 执行evaluateJavaScript方法获取网页宽高,计算比例,再根据手机界面设置的宽高去设置。
③ 在HTML标签中加入特殊字符去接收高度,OC这边通过evaluateJavaScript方法获取。
- 当子view大小超出了父view,怎么获取并响应点击事件? 重新父view的hintTest方法,遍历父view上所有的子view,判断这个point是否在子view上,如果在,返回子view去响应事件,如果都没有,返回父view去响应事件。
- 扩大UIButton的点击范围
① 创建分类(Category)并重写UIButton的pointInside:withEvent:方法
②创建分类(Category)并重写UIButton的hitTest::withEvent:方法
- 为什么子线程不能更新UI
线程安全:UIKit不是线程安全的,所有UI操作都必须在主线程上执行。直接在子线程中更新UI可能导致数据竞争和不一致的状态,从而引发崩溃或未定义的行为。
主线程的职责:主线程负责处理用户交互和UI更新。如果在子线程中进行UI更新,可能会导致主线程的任务被阻塞,从而影响用户体验,导致界面卡顿或无响应。
崩溃风险:如果在子线程中尝试更新UI,可能会导致应用程序崩溃,通常会抛出[ setValue:forUndefinedKey:]或类似的异常。
- Swift 中如何实现单例模式?
在Swift中,单例模式的实现通常采用静态属性和私有初始化方法来确保一个类仅有一个实例。具体做法是:定义一个静态属性来存储这个单例实例,然后将类的初始化方法设为私有,以阻止外部通过构造函数创建实例。
可以通过类型属性+let+private
来写单例 实现步骤如下:1)、 创建一个名为shared的静态属性,这个属性将存储单例类的唯一实例。
2)、 将类的构造器声明为私有,使用private init()。这样外部代码不能直接调用这个构造器来创建实例。
3)、 通过ClassName.shared的方式访问这个类的单例实例。
这种方法既保证了单例模式的核心要求——确保一个类仅有一个实例,同时也利用了Swift语言的特性来简化实现。
- Swift 中的协议和继承有什么区别?
协议和继承在Swift中都用于定义一个类型应有的行为,但它们的使用场景和方式有明显区别:
1、 协议定义了一个蓝图,规定了遵循协议的类型必须实现的方法和属性,但不提供这些方法和属性的具体实现。协议可以被枚举、结构体和类遵循。
2、 继承允许一个类继承另一个类的特性,如方法和属性。子类可以重写父类中的方法和属性来提供特定的实现。继承仅限于类之间的关系。
协议支持多重继承,即一个类型可以遵循多个协议,而继承则是单一继承,一个类只能继承自另一个类。协议适用于定义一组应该被不同类型实现的接口,而继承更多的是为了代码的复用和扩展已有的类行为。
- 在Swift中如何使用map、filter和reduce?
map、filter和reduce是Swift标准库中的三个高阶函数,它们对于处理集合类型(如数组)非常有用。
1)、 map函数用于遍历集合中的每个元素,并对每个元素应用一个你提供的转换方法,最终生成一个新的集合。
2)、 filter函数用于遍历集合,然后返回一个新集合,这个新集合只包含满足你提供的条件的元素。
3)、 reduce函数将集合中的所有元素合并成一个单一的值,这个过程是通过你提供的一个初始值和一个应用于每个元素的合并方法完成的。
这些函数通过提供一种声明式的方法来操作集合,可以使代码更简洁、更易读,并减少出错的可能。
- Swift中枚举的高级用法有哪些?
Swift中的枚举支持许多高级特性,使得它们不仅能用于表示一组相关的值,还能承载更复杂的功能:
1)、 关联值:允许你存储与枚举成员值相关联的自定义类型的值。这使得枚举可以存储更多的信息,并能根据不同的场景返回不同类型的关联值。
2)、 原始值:枚举成员可以有原始值,常见的原始值类型有字符串、字符或任何整数或浮点数类型。这使得枚举更容易在不同的上下文中转换和使用。
3)、 递归枚举:通过在枚举成员前使用indirect关键字,枚举可以是递归的。这意味着枚举成员的关联值可以是枚举本身的一个实例,非常适合表示具有递归结构的数据模型,如树形结构。
4)、 扩展和协议:枚举可以遵循协议,并且可以通过扩展来增加额外的功能。这为在枚举上定义共通的行为提供了一种强大的方式。
这些高级用法提升了枚举在Swift编程中的灵活性和表达能力。
- 解释 Swift 中的泛型编程及其好处。
Swift中的泛型编程允许你编写灵活、可重用的函数和类型,它们可以工作于任意类型,条件是这些类型满足定义的要求。泛型编程的好处包括:
1)、 类型安全:泛型代码让你能够写出抽象和可复用的函数和类型,同时保留类型检查的优点。这意味着编译器可以自动检测类型错误。
2)、 减少代码量:使用泛型可以减少重复代码,因为你可以用单一的函数或类型来处理不同类型的数据,而不是为每种数据类型编写特定的函数或类型。
3)、 提高性能:泛型代码在编译时被实例化,这意味着编译器生成的代码已经是针对特定类型优化的。这可以在保持代码抽象和灵活性的同时,提供与非泛型代码相同的运行时性能。
4)、 提升表达能力和灵活性:泛型让库和框架的设计者能够提供高度灵活和可配置的API,而无需牺牲类型安全或性能。
泛型是Swift语言中强大的特性之一,通过使用泛型,开发者可以编写更清晰、更抽象且高度复用的代码。
- 解释 Swift 中的可选链式调用及其用途。
可选链式调用是一种在当前可选值可能为nil的情况下查询和调用属性、方法及下标的方式。如果可选值有值,那么调用就会成功;如果可选值是nil,则调用返回nil。可选链式调用的用途包括:
1)、 简化对可选类型值的查询。它允许你编写一条语句来尝试访问多层可选类型的属性、方法和下标,而无需每次查询都进行显式的解包。
2)、 增强代码的安全性。使用可选链式调用可以避免运行时的nil值错误,因为它会在尝试访问的任何一个点上值为nil时,整个表达式的结果也为nil。
3)、 提高代码的简洁性和可读性,避免冗长的条件判断语句。
可选链式调用是处理Swift中可选类型的一个非常有效和安全的工具,使得操作可选类型更加灵活和简洁。
- Swift 中如何使用高阶函数优化代码?
Swift的标准库中包含了多种高阶函数,例如map、filter、reduce、flatMap和compactMap等。这些函数接受一或多个函数作为输入,并返回一个新的集合,使得你可以用更少、更清晰的代码来执行复杂的操作。使用高阶函数优化代码的方式包括:
1)、 使用map进行转换:将集合中的每个元素通过某个函数映射转换,生成一个新的集合。
2)、 使用filter进行筛选:根据提供的条件过滤集合中的元素,仅保留满足条件的元素。
3)、 使用reduce进行汇总:将集合中的所有元素合并成一个单一的值,比如计算总和或找出最大值。
4)、 使用flatMap和compactMap进行扁平化处理和空值过滤:flatMap将集合中的集合展开成一个单一集合,compactMap则在此基础上过滤掉nil值。
通过利用这些高阶函数,可以使得代码更加简洁、易读,并减少出错的机会,同时也提高了代码的表达力和功能性。
- Swift 中的 defer 语句有什么用途?
在Swift中,defer语句用于在即将离开当前代码块前执行一段代码,无论是由于抛出错误而离开还是正常的离开。这个语句主要用于清理工作或释放资源,确保即使在发生错误时也能执行某些必要的代码。使用defer的好处包括:
1)、 确保代码的执行:无论函数是通过哪种路径退出的,defer块中的代码都会被执行。
2)、 提高代码清晰度:将清理或关闭资源的代码放在defer语句中,可以让你的逻辑更加集中,更容易阅读和维护。
3)、 避免代码重复:在多个退出点需要执行相同的清理代码时,使用defer可以避免代码重复。
defer语句的这些特性使得它成为管理资源和保证代码执行的有力工具。
- Swift 中的值类型和引用类型有什么区别?
在Swift中,基本的分类型为值类型和引用类型,它们之间的主要区别在于如何存储和传递。
1)、 值类型:每次赋值或传递时,都会进行拷贝操作,创建一个新的独立实例。基本数据类型(如Int、String、Array等)和结构体(struct)、枚举(enum)是值类型。
2)、 引用类型:赋值或传递时,不拷贝实例本身而是其引用或指针。因此,多个变量可以引用同一个实例。类(class)是引用类型。
值类型的特性使得它在多线程环境下工作时更加安全,因为每个实例都有自己的独立拷贝,避免了意外修改。而引用类型允许多个引用或者变量共享同一个实例,适合执行更复杂的数据结构和逻辑。
- Swift 中如何使用扩展(Extensions)增加现有类型的功能?
Swift中的扩展允许给现有的类、结构体、枚举或协议类型添加新的功能,无需修改原始定义。扩展的使用方式和优势包括:
1)、 添加计算实例属性和计算类型属性。扩展可以添加新的计算属性,但它们不能存储属性或属性观察器。
2)、 定义实例方法和类型方法。扩展允许给类型添加新的方法。
3)、 提供新的构造器。扩展可以给结构体和类添加新的便利构造器,但它们不能添加新的指定构造器或析构器。
4)、 定义下标。扩展允许给现有的类型添加新的下标。
5)、 定义和使用新的嵌套类型。扩展可以添加新的嵌套类型,这对于组织复杂的类型特别有用。
6)、 使现有类型遵循一个或多个协议。可以通过扩展来添加协议遵循性,无论是为了添加新的功能,还是使现有功能适配某个协议。
扩展使得无需继承就可以修改类型,提供了一种模块化和可维护的方式来组织和重用代码。
- 在Swift中,何时应该使用结构体(Struct)而不是类(Class)?
在Swift中,结构体(Struct)和类(Class)都可以用来定义属性和方法来创建复杂的数据类型。但是,根据特定的场景和需求,选择使用结构体还是类有以下几个考虑点:
1)、 当你需要一个轻量级的数据载体,并且数据的拷贝或值传递是可预期的行为时,应该优先考虑使用结构体。结构体实例在代码中传递时总是被拷贝,这有利于保证数据的不可变性和安全性。
2)、 如果你要表示的数据结构需要利用继承来避免代码重复,或者需要在运行时检查和解释类型的实例,那么应该使用类。类支持继承,多态和类型转换,而结构体不支持。
3)、 对于小型的数据结构,结构体由于其值类型的特性,在性能上通常优于类。在数组和字典这样的集合类型中使用结构体可以获得更好的性能。
4)、 当你的数据结构需要通过网络传输或者需要与外部系统进行交互时,使用遵循Codable协议的结构体可以简化序列化和反序列化的过程。
5)、 使用结构体可以避免内存泄漏和循环引用问题,这是因为结构体作为值类型,不会形成引用计数。
总的来说,如果数据结构比较简单,不需要用到继承,且期望通过值传递来确保数据安全,那么结构体是更好的选择。而对于需要利用面向对象特性的复杂数据模型,类则是更合适的选择。
- Swift 中的属性包装器是什么?提供使用场景。
属性包装器是Swift中的一种特性,允许你定义一个结构、类或枚举用作处理属性的存储方式和定义属性如何被访问的代码的重用。使用属性包装器可以实现代码的复用,以及更干净、更直观的属性定义。使用场景包括:
1)、 数据验证:可以创建属性包装器来自动检查属性值是否满足特定条件,例如是否在给定的范围内,或者是否符合正则表达式。
2)、 管理线程访问:对于多线程或并发编程,属性包装器可以用来确保属性访问的线程安全性,例如通过同步访问控制。
3)、 懒加载:属性包装器可以用于实现属性的懒加载逻辑,即仅在第一次访问属性时计算其初始值。
4)、 存储管理:可以利用属性包装器来透明地实现属性的持久化,比如自动从数据库加载和保存数据。
5)、 观察者模式:属性包装器可以用来监控属性值的变化,执行一些操作,如更新UI或触发事件,当属性值改变时。
属性包装器通过@propertyWrapper声明,它们提供了一种高度可重用的方式来封装对属性的共通处理逻辑,使得属性的管理更加灵活和强大。
- 如何在Swift中实现并发编程?
Swift中实现并发编程主要依靠Grand Central Dispatch (GCD)和Operation Queues两种机制,以及Swift 5.5引入的异步/等待(async/await)模式。
1)、 Grand Central Dispatch (GCD):是一种基于C语言的低层次并发API,提供了强大的工具来管理并发任务。通过使用队列(Dispatch Queue),开发者可以异步或同步地执行任务。GCD支持串行队列和并发队列,允许你控制任务的执行方式。
2)、 Operation Queues:是基于对象的高级并发API。它允许你封装任务为Operation对象,并加入到OperationQueue中。Operation提供了更多控制,包括设置任务依赖、优先级和取消操作。
3)、 异步/等待(async/await):Swift 5.5中引入的新特性,提供了一种更简洁和直观的方式来处理异步代码。使用async标记的函数可以在其中执行耗时操作而不阻塞当前线程,await用于调用异步函数,直到它完成。
这些并发编程工具和模型提供了不同的抽象级别,让Swift开发者可以根据需要选择最合适的方式来实现并发,从而提高应用的性能和响应性。
- Swift 中的泛型如何用于提高代码复用性?
Swift中的泛型是编程中一种允许你编写灵活、可重用函数和类型的方法,它可以工作于任何类型。泛型的主要目的是减少代码重复,提高代码的复用性和清晰度。
1)、 泛型函数:你可以创建可以接受任何类型参数的函数。这意味着相同的函数可以用于不同类型的输入,避免了为每种类型编写重复的代码。
2)、 泛型类型:除了函数,Swift允许你定义泛型类型,如自定义的数据结构,它们可以与任何类型一起使用。这对于创建通用的数据结构如栈、队列和链表等特别有用。
3)、 类型约束:泛型代码还可以通过类型约束来定义某些类型必须遵循的特定协议,这提供了使用泛型时的灵活性和强大的类型安全。
4)、 泛型扩展:Swift允许对泛型类型进行扩展,为泛型类型添加新的功能。这使得你可以对泛型类型进行高度定制化的扩展,而无需修改原始的泛型定义。
通过使用泛型,Swift开发者可以编写更加抽象和通用的代码库,这些代码可以工作于多种类型上,减少了重复代码,提高了项目的维护性和可扩展性。
- 在Swift中,怎样使用协议来定义一组相关的方法和属性?
在Swift中,协议是一种定义一组方法和属性的蓝图。类型(如类、结构体和枚举)可以采用(Adopt)协议来提供这些方法和属性的具体实现。通过使用协议,你可以定义一组应该由特定类型实现的相关方法和属性,从而实现接口的抽象和多态性。
1)、 定义协议:通过protocol关键字来定义一个协议,并声明一组方法和属性。这些方法和属性可以是实例的也可以是类型的,且可以指定是否只读或读写。
2)、 采用协议:类型通过在其定义中列出协议名来采用协议。这样,该类型就承诺了会提供协议要求的方法和属性的实现。
3)、 实现协议:一个类型可以实现一个或多个协议要求的方法和属性。对于协议中的每一个要求,该类型必须提供具体的实现。
4)、 协议作为类型:协议本身可以作为函数、方法或构造器中的参数类型、返回类型或者变量、常量的类型使用。这使得你可以在运行时改变具体实现。
5)、 协议继承:协议可以继承一个或多个其他协议,并可以添加新的要求。
6)、 协议扩展:Swift允许给协议提供默认实现,通过协议扩展可以给协议的方法或计算属性提供默认的实现。
通过定义和使用协议,Swift允许创建灵活和可重用的代码,促进了不同类型之间的解耦,提高了代码的模块化水平。
- Swift中的可选链是什么,如何使用它来简化代码?
可选链(Optional Chaining)是一种在当前可选项可能为nil的情况下查询和调用属性、方法及下标的过程。如果可选项有值,那么可选链调用会成功;如果可选项是nil,则可选链调用返回nil。可选链可以让你在不需要强制解包的情况下,安全地访问可选项的属性、方法和下标。
1)、 使用可选链代替强制解包:当你尝试从可选项中取出值时,可选链提供了一种不会引起运行时错误的方法。
2)、 多级可选链:你可以通过连接多个可选链调用来深入访问多层可选类型的属性、方法和下标。如果链中的任何一个节点是nil,整个表达式的结果也是nil。
3)、 与可选绑定结合使用:可选链的结果是一个可选值,你可以使用可选绑定(if let或guard let)来检测可选链的结果是否存在。
4)、 对方法的可选链调用:如果你尝试通过可选链调用方法,该方法的返回类型将是一个可选值,即使方法本身定义时返回的不是可选值。
使用可选链可以大大简化对可选项的访问和操作,使代码更加清晰和安全,避免了因强制解包导致的潜在崩溃。
- Swift的优点
- Swift更加安全,它是类型安全的语言。
- Swift容易阅读,语法和文件结构简易化。
- Swift更易于维护,文件分离后结构更清晰。
- Swift代码更少,简洁的语法,可以省去大量冗余代码
- Swift速度更快,运算性能更高。
- swift 为什么将 String,Array,Dictionary设计为值类型?
值类型和引用类型相比,最大优势可以高效的使用内存,值类型在栈上操作,引用类型在堆上操作,栈上操作仅仅是单个指针的移动,而堆上操作牵涉到合并,位移,重链接,Swift 这样设计减少了堆上内存分配和回收次数,使用 copy-on-write将值传递与复制开销降到最低
- 如何将Swift 中的协议(protocol)中的部分方法设计为可选(optional)?
- 在协议和方法前面添加 @objc,然后在方法前面添加 optional关键字,改方式实际上是将协议转为了OC的方式
- 使用扩展(extension),来规定可选方法,在 swift 中,协议扩展可以定义部分方法的默认实现
- 比较 Swift和OC中的 protocol 有什么不同?
- Swift 和OC中的 protocol相同点在于: 两者都可以被用作代理;
- 不同点: Swift中的 protocol还可以对接口进行抽象,可以实现面向协议,从而大大提高编程效率,Swift中的protocol可以用于值类型,结构体,枚举;
- 什么是尾随闭包?
- 将一个很长的闭包表达式作为函数的最后一个实参
- 使用尾随闭包可以增强函数的可读性
- 尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式
- 什么是逃逸闭包?
当闭包作为一个实际参数传递给一个函数或者变量的时候,我们就说这个闭包逃逸了,可以在形式参数前写
@escaping
来明确闭包是允许逃逸的。
- 非逃逸闭包、逃逸闭包,一般都是当做参数传递给函数
- 非逃逸闭包:闭包调用发生在函数结束前,闭包调用在函数作用域内
- 逃逸闭包:闭包有可能在函数结束后调用,闭包调用逃离了函数的作用域,需要通过@escaping声明
- 什么是自动闭包?
自动闭包是一种自动创建的用来把作为实际参数传递给函数的表达式打包的闭包。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值。这个语法的好处在于通过写普通表达式代替显式闭包而使你省略包围函数形式参数的括号
-
为了避免与期望冲突,使用了@autoclosure的地方最好明确注释清楚:这个值会被推迟执行
-
@autoclosure 会自动将 20 封装成闭包 { 20 }
-
@autoclosure 只支持 () -> T 格式的参数
-
@autoclosure 并非只支持最后1个参数
-
有@autoclosure、无@autoclosure,构成了函数重载
-
如果你想要自动闭包允许逃逸,就同时使用 @autoclosure 和 @escaping 标志。
- 主类方法和分类方法重名
-
load方法优先主类方法
-
除load方法外的其他方法,优先执行分类方法
Objective-C
属性特性@property
详解 -->assign
setter方法将传入参数赋值给实例变量,仅设置变量时,assign适用于基本数据类型,并且是一个弱引用。assign其实也可以用来修饰对象。那么我们为什么不用它修饰对象呢?因为被assign修饰的对象(一般编译的时候会产生警告:Assigning retained object to unsafe property; object will be released after assignment)在释放之后,指针的地址还是存在的,也就是说指针并没有被置为nil,造成野指针。对象一般分配在堆上的某块内存,如果在后续的内存分配中,刚好分到了这块地址,程序就会崩溃掉。
那为什么可以用assign修饰基本数据类型?因为基础数据类型一般分配在栈上,栈的内存会由系统自己自动处理,不会造成野指针。
Objective-C
属性特性@property
详解 -->retain
表示持有特性,setter方法将传入参数先保留,再赋值,传入参数的retaincount会+1;
-
Objective-C
属性特性@property
详解 -->copy
表示拷贝特性,setter方法将传入对象在内存里拷贝一份,两个指针指向不同的内存地址,需要完全一份新的变量时。在 MRC 时是这样做的 release 旧对象( 旧对象的引用计数器 -1 ) , copy 新对象( 新对象的引用计数器 +1 ) , 然后指向新对象 (新对象是指最终指向的那个对象,不管深拷贝还是浅拷贝)。在 ARC 时 copy 新对象( 新对象的引用计数器 +1 ) , 然后指向新对象 。
copy 是内容拷贝,mutablecopy是指针拷贝。 -
Objective-C
属性特性@property
详解 -->strong
强引用,会使引用计数器+1,两个指针指向同一个内存地址,在ARC下(替代了 retain 的作用),一个对象没有强引用指向时,系统会释放这个对象
-
Objective-C
属性特性@property
详解 -->weak
弱引用,不会使引用计数器+1,weak修饰的对象在释放之后,指针地址会被置为nil。
weak使用场景:
在ARC下,在有可能出现循环引用的时候,往往要通过让其中一端使用weak来解决,比如: delegate代理属性,通常就会声明为weak。自身已经对它进行一次强引用,没有必要再强引用一次时也会使用weak。比如:自定义 IBOutlet控件属性一般也使用weak,当然也可以使用strong。为什么IBOutlet属性是weak的? 因为当我们将控件拖到Storyboard上,相当于新创建了一个对象,而这个对象是加到视图控制器的view上,view有一个subViews属性,这个属性是一个数组,里面是这个view的所有子view,而我们加的控件就位于这个数组中,那么说明,实际上我们的控件对象是属于view的,也就是说view对加到它上面的控件是强引用。当我们使用Outlet属性的时候,我们是在viewController里面使用,而这个Outlet属性是有view来进行强引用的,我们在viewController里面仅仅是对其使用,并没有必要拥有它,所以是weak的。
如果将weak改为strong,也是没有问题的,并不会造成强引用循环。当viewController的指针指向其他对象或者为nil,这个viewController销毁,那么对控件就少了一个强引用指针。然后它的view也随之销毁,那么subViews也不存在了,那么控件就又少了一个强引用指针,如果没有其他强引用,那么这个控件也会随之销毁。
-
Objective-C
属性特性@property
详解 -->readonly
只读属性,只生成getter方法,也就是说只能访问变量,不能修改 。 -
Objective-C
属性特性@property
详解 -->readwrite
默认属性,可读可写,生成setter和getter方法。 -
Objective-C
属性特性@property
详解 -->atomic
原子性,属性安全级别的表示,同一时刻只有一个线程访问,具有资源的独占性,但是效率很低。对set 方法加互斥锁 @synchronized(锁对象) 。互斥锁是利用线程同步实现的 , 意在保证同一时间只有一个线程调用 set 方法 ,其实还有 get 方法 , 要是同时 set 和 get 一起调用还是会有问题的 . 所以即使用了 atomic 修饰 还是不够安全 。 -
Objective-C
属性特性@property
详解 -->nonatomic
非原子性,可以多线程访问,效率高,不对set方法加锁 ,性能好,线程不安全 -
内存管理 在dealloc方法中用【xxx release】还是self.xxx = nil 好 ?
self.xxx = nil 好,会先release再置为nil,这样就更安全的释放对象,防止野指针,防止内存泄漏 -
load与c++构造函数调用顺序
-
load是在dyld回调load_images中进行调用的,这个回调是在_objc_init的过程中进行注册的。
- C++构造函数对于同一个image而言是在load回调后dyld调用的。(并不是绝对的)
- image内部是先加载所有类的+ load,再加载分类的+ load,最后加载C++全局构造函数。(类load -> 分类load -> C++构造函数)。
- +load是objc中调用的,C++全局构造函数是在dyld中调用的。(image内部的顺序默认是按Compile Sources中顺序进行加载的,当然对于有依赖库的image,依赖库+load先被调用)。
- Dyld初始化image是按Link Binary With Libraries顺序逐个初始化的,从下标1开始,最后再初始化主程序(下标0)。
- 当然对于同一个image而言C++构造函数在load之后调用并不是绝对的。比如objc系统库,在进行dyld注册回调之前调用了自身库的C++构造函数(自启)。
- initialize调用顺序
-
initialize是在第一次发送消息的时候进行的调用。
-
load是在load_images的时候调用的,load比initialize调用时机早(initialize在lookupimporforward慢速消息查找的时候调用)。
-
整个调用顺序load > C++构造函数 > initialize。
- @protocol 和 category 中如何使用 @property
objc_setAssociatedObject/ objc_setAssociatedObject
- @property声明的 NSArray、NSDictionary为什么经常使用 copy 关键字
分析: NSString、NSArray、NSDictionary 含有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作(就是把可变的赋值给不可变的),为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。
经常使用copy修饰是因为:
1)、遵循父类指针可以指向子类对象原理,使用 copy修饰是为了让本对象的属性不受外界影响,无论给我传入是一个可变对象还是不可对象,我本身持有的是一个不可变的副本。
2)、如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。
注意:copy 一般用来修饰有对应可变类型的不可变对象上,NSMutableString、NSMutableArray和NSMutableDictionary等尽量不要直接copy修饰,否则修改时会出现崩溃。
原因:因为用copy修饰得到的是不可变类型对象
- 在Objective-C中向nil对象发送消息会crash吗?
不会。因为OC是动态语言,调用方法时会动态转成消息发送(如:objc_msgSend(test,@selector(go)))。当你向一个对象发送消息时,runtime会根据对象的isa找到其所属类,然后在该类的缓存方法列表、方法列表、其父类方法列表中寻找对应的方法运行,在发送消息时,objc_msgSend方法无返回值(void objc_msgSend(id self,SEL cmd,...))。那么向nil对象发送消息,第一步就直接判空返回了,因此不会出现任何问题。
- 可变数组是线程安全的吗?什么情况下不安全?怎么保证它是线程安全的?
可变数组不是线程安全的,当多个线程同时对数组进行操作时可能会导致数据错误或crash。我们可以通过加锁或者内部维护一个队列来确保它是线程安全的。
- NSMutableDictionary中的setObject:forKey:与setValue:forKey:方法有什么区别?
setObject:forkey:中的value是不能够为nil的,不然会报错。
setValue:forKey:中的value能够为nil,但是当value为nil的时候,会自动调用removeObject:forKey方法。
setValue:forKey:中的key的参数只能是NSString类型,而setObject:forKey中的key是可以任意类型。
- delegate(代理)能否实现一对多的通信?
可以,采用多播委托的方式来实现。多播委托:它是指允许创建方法的调用列表或者链表的能力。当多播委托被调用时,列表中的方法均自动执行。
普通代理只能做到一对一,无法做到一对多的回调,而多播委托则是对delegate的一种扩展与延伸,多了一个注册和取消的过程。任何需要回调的对象都必须先注册。
- KVC、KVO是什么?简述KVO的实现原理。KVO是否能监听数组?如何实现?
KVC(键值编码):它是一种间接访问对象实例变量机制,可以不通过存取方法访问对象的实例变量。
KVO(键值观察):它可以使对象获取其他对象属性变化的通知机制。
KVO使用了isa-swizzling方式结合RunTime动态性,在给对象首次添加KVO时,RunTime会动态创建被监听对象的子类(NSKVONofifying_ClassName),然后实现setter,class,dealloc,_isKVOA方法,并在setter方法中实现对应通知机制。接下来将被监听对象isa指向动态创建的子类。使用KVC修改属性值时,会调用动态创建的子类中对应setter方法,触发通知机制,如此便实现了KVO。
KVO监听数组需实现NSMutableArray的增删改操作遵从KVC的规则:
增:-insertObject:inAtIndex:或者-insert:atIndexes:
删:-removeObjectFromAtIndex:或者-removeAtIndexes:
改:-replaceObjectInAtIndex:withObject:或者-replaceAtIndexes:with:
并将这些接口暴露给调用者,在对数组进行操作时需使用上述实现的接口。
- KVC的底层结束?
当一个方针调用setValue方法时,方法内部会做以下操作:
①.检查是否存在相应的key的set方法,假设存在,就调用set方法。
②.假设set方法不存在,就会查找与key相同称谓并且带下划线的成员变量,假设有,则直接给成员变量特征赋值。
③.假设没有找到_key,就会查找相同称谓的特征key,假设有就直接赋值。
④.假设还没有找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。
这些方法的默许结束都是抛出异常,咱们可以根据需求重写它们。
- 讲讲iOS中数据存储方法、哪些数据持久化方式?
数据存储有四种计划:NSUserDefault、KeyChain、File、DB。
其间File有三种方法:writeToFile:atomically:、Plist、NSKeyedAchiever(归档)
iOS中数据持久化方式有以下几种:
SQLite3,CoreData,文件归档,属性列表(plist文件写入)。
属性列表(plist文件):涉及的主要类是NSUserDefaults,存储小量的数据。
文件归档(archive):文件归档和解归档的对象必须实现NSCoding协议。实现initWithCoder:方法与encodeWithCoder方法。同时也建议实现NSCopying协议。
SQLite3:SQLite是一个开源的嵌入式关系数据库。可移植性好,容易使用,需要内存开销小。适合嵌入式设备。
CoreData:它底层可以使用SQLite保存数据,且无需写SQL语句。另外它还可以使用XML方式保存数据。要使用CoreData,需要在Xcode中的数据模型编辑器中设计好各个实体以及定义好他们的属性和关系。通过操作这些对象来完成数据的持久化。
- fmdb中支持多线程吗?它是如何实现的!
fmdb是支持多线程的。它里面有个FMDatabaseQueue类。看似是一个队列,实际上不是,它继承NSObject,通过在内部创建一个Serial的dipatch_queue_t来处理inDatabase/inTransaction传入的Block,所以当我们在主线程或后台调用inDatabase/inTransaction时,代码实际上是同步的。FMDatabaseQueue这样设计是为了避免并发访问数据库时造成的线程安全问题,所有的数据库访问都是同步执行,比@synchronized与NSLock效率高。
- ViewController生命周期?
①.loadView:开端加载视图控制器自带的view。
②.viewDidLoad:视图控制器的view被加载结束。
③.viewWillAppear:视图控制器的view行将闪现在window上。
④.viewWillLayoutSubviews:视图控制器的view行将更新内容视图的方位。
⑤.viewDidLayoutSubviews:视图控制器的view现已更新视图的方位。
⑥.viewDidAppear:视图控制器的view现已展示到window上。
⑦.viewWillDisappear:视图控制器的view行将从window上消失。
⑧.viewDidDisappear:视图控制器的view现已从window上消失。
- tableView的重用机制?
UITableView通过重用单元格来到达节省内存的目的: 通过为每个单元格指定一个重用标识符,即指定了单元格的种类,当屏幕上的单元格滑出屏幕时,系统会把这个单元格添加到重用队伍中,等候被重用,当有新单元格从屏幕外滑入屏幕内时,从重用队伍中找看有没有可以重用的单元格,假设有,就拿过来用,假设没有就创建一个来运用。
- OC中的反射机制?
1).class反射
通过类名的字符串方法实例化方法。
Class class = NSClassFromString(@”student”);
Student *stu = [[class alloc] init];
将类名变为字符串。
Class class =[Student class];
NSStringclassName = NSStringFromClass(class);
2).SEL的反射通过方法的字符串方法实例化方法。
SEL selector = NSSelectorFromString(@”setName”);
[stu performSelector:selector withObject:@”Mike”];
将方法变成字符串。NSStringFromSelector(@selector(setName:));
调用方法有两种方法:
①.直接通过方法名来调用。[person show];
②.直接的通过SEL数据来调用。SEL aaa = @selector(show); [person performSelector:aaa];
- weak原理?
Runtime维护了一个weak表,用来存储某个对象的所有的weak指针。
weak表其实是一个哈希表,key是所指对象的指针,value是weak指针的地址数据(value是数据的原因是:因为一个对象可能被多个弱引用指针指向)。
weak原理实现过程三步骤:
初始化开始时,会调用objc_initWeak()函数,初始化新的weak指针指向对象的地址
紧接着,objc_initWeak()函数里面会调用objc_storeWeak()函数,objc_initWeak()函数的作用是用来更新指针的指向,创建弱引用表
在最后会调用clearDeallocating()函数,而clearDeallocating()函数首先根据对象的地址获取weak指针地址的数组,然后紧接着遍历这个数组,将其中的数组开始置为nil,把这个entry从weak表中删除,最后一步清理对象的记录
- 怎样对iOS设备进行功能测验?
Profile-> Instruments ->Time Profiler
- 怎样检查内存泄露?
①.静态分析 analyze。
②.instruments东西里面有个leak可以动态分析。
- 你一般是怎样用Instruments的?
Instruments里面东西许多,常用:
①.Time Profiler: 功用分析
②.Zombies:检查是否访问了僵尸方法,但是这个东西只能从上往下检查,不智能。
③.Allocations:用来检查内存,写算法的那批人也用这个来检查。
④.Leaks:检查内存,看是否有内存泄露。
多线程
1. 谈谈你对多线程开发的理解?
好处:
①.使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片,视频的下载;
②.发挥多核处理器的优势,并发执行让系统运行的更快,更流畅,用户体验更好;
缺点:
①.大量的线程降低代码的可读性;
②.更多的线程需要更多的内存空间;
③.当多个线程对同一个资源出现争夺的时候要注意线程安全的问题。
2. iOS中实现多线程编程有哪几种方式?分别有什么区别?
①.NSThread:面向对象操作线程,使用相对简单,需要手动管理线程生命周期。
②.GCD:苹果多核编程解决方案,使用起来非常方便。需要自己实现如:限制并发数,任务间的依赖等功能。自动管理线程生命周期。
③. NSOperation:基于GCD的封装,面向对象操作线程,提供了比GCD更丰富的API:限制最大并发数,设置任务依赖关系。但是它不能直接使用,因为它是一个抽象 类,可以继承它或者使用系统定义NSInvocationOperation或NSBlockOperation。自动管理线程生命周期。
3. NSOperationQueue和GCD有什么区别?
①.GCD底层是C语言构成的API。NSOperationQueue及相关对象是Objc对象。在GCD中,在队列中执行的是由block构成的任务,这是一个轻量级的数据结构。而NSOperation作为一个对象,为我们提供了更多的选择。
②.在NSOperationQueue中,取消任务非常方便,而GCD没法停止已经加入queue的block。
③.NSOperation能够方便的设置依赖关系,还能设置NSOperation的priority优先级,能够使同一个并行队列中的任务区分先后地执行。在GCD中,我们只能区分不同任务队列的优先级,如果要区分block任务优先级也需要大量复杂代码。
NSOperationQueue还可以设置最大并发数,GCD则需要自己实现。
④.NSOperation任务状态属性支持KVO,可以通过KVO来监听operation的就绪、取消、执行中、执行完成等状态。GCD则无法判断当前任务执行状态。
Runloop和Runtime
1. 什么是Runloop?
是通过内部维护的事件循环来对事件/消息进行管理的一个对象。
①.没有消息需要处理时,休眠以避免资源占用
②.有消息需要处理时,立刻被唤醒
③.线程是和runloop一一对应的
④.自己创建的线程默认是没有runloop的
其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)
一个线程对应一个RunLoop,底子作用就是保持程序的继续作业,处理app中的各种作业。
通过runloop,有事作业,没事就休息,可以节省cpu资源,前进程序功用。
主线程的run loop默许是发起的。iOS的应用程序里面,程序发起后会有一个如下的main()函数
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
2. RunLoop在子线程和主线程的区别?
来源区别:
主线程runloop是随着app启动,在applicationmain方法中由系统创建并启动运行的,随着app的运行而不停的循环运行,直到app被杀死;而部分子线程是由开发人员编写的程序手动创建,并且需要单独启动运行,然后控制其调用和结束。
执行任务区别:
UI刷新相关的任务必须执行在主线程runloop中,而预排版,预渲染以及其他任务则可以在主线程runloop或子线程runloop中运行;耗时操作建议放在子线程runloop中执行,避免卡住主线程runloop造成UI卡顿
3. 怎样实现一个常驻线程?分三步
①.为当前线程开启一个runloop
②.向该runloop中添加一个port/source等维持ruunloop的事件循环
③.启动该ruunloop
4. 怎么做到有事做事,没事休息的?
由于在调用CFRunlooprun相关方法后会调用系统一个函数mach_msg,同时发生用户态向核心态的一个切换,然后当前线程处于休眠状态,达到有事做事没事休息。
5. 有了Runtime库,能做什么作业呢?
Runtime库里面包含了跟类、成员变量、方法相关的API。
比如:
①.获取类里面的全部成员变量。
②.为类动态添加成员变量。
③.为类动态添加新的方法。
④.动态改动类的方法结束等。(Method Swizzling)
6. App的启动过程
①.iOS系统首先会加载解析该APP的Info.plist文件,因为Info.plist文件中包含了支持APP加载运行所需要的众多Key,value配置信息,例如APP的运行条件(Required device capabilities),是否全屏,APP启动图信息等。
②.创建沙盒(iOS8后,每次启动APP都会生成一个新的沙盒路径)
③.根据Info.plist的配置检查相应权限状态
④.加载Mach-O文件读取dyld路径并运行dyld动态连接器(内核加载了主程序,dyld只会负责动态库的加载)
1).首先dyld会寻找合适的CPU运行环境
2).然后加载程序运行所需的依赖库和我们自己写的.h.m文件编译成的.o可执行文件,并对这些库进行链接。
3).加载所有方法(runtime就是在这个时候被初始化并完成OC的内存布局)
4).加载C函数
5).加载category的扩展(此时runtime会对所有类结构进行初始化)
6).加载C++静态函数,加载OC+load
7).最后dyld返回main函数地址,main函数被调用
7. Runtime 是什么
runtime 是由C 和C++ 汇编 实现的⼀套API,为OC语⾔加⼊了⾯向对象,运⾏时的功能
运⾏时(Runtime)是指将数据类型的确定由编译时推迟到了运⾏时 - 举例⼦🌰 : extension - category 的区别
平时编写的OC代码,在程序运⾏过程中,其实最终会转换成Runtime的C语⾔代码,Runtime
是 Object-C
的幕后⼯作者