41.谈谈对OC和Swift动态特性的理解
runtime其实就是OC的动态机制。runtime执行的是编译后的代码,这时它可以动态加载对象、添加方法、修改属性、传递信息等。具体过程是,在OC中,对像调用方法时,如[self.tableview reload],经历了两个过程。
- 编译阶段: 编译器会将OC代码翻译成objc_msgSend(self.tableview,@slector(reload)),把消息发送给self.tableview。
- 运行阶段: 接收者(self.tableview)会响应这个消息,其间可能会直接执行,转发消息,也可能会找不到方法导致程序崩溃。
所以,整个流程是:编译器翻译->给接收者发送消息->接收者响应消息。
接收者如何去响应消息,就发生在运行时。runtime执行的是编译后的代码,而代码的具体实现是运行时才能够确定,这时它就可以动态加载对象、添加方法、修改属性、传递消息等。runtime的运行机制就是OC的动态特性。
Swift公认的是一个静态语言,它的动态特性都是通过桥接OC来实现的,像一些动态的特性其实是可以用Swift的面向协议的编程那样使用的。
例子:
在OC中我们去动态的调用一个方法,首先需要判断这个方法是否存在然后再去调用它,这就是纯正的动态特性:代码
OC的动态特性
UserModel类的实现
@implementation UserModel
- (void)todoSomething{
NSLog(@"正在做什么事情。");
}
@end
动态调用UserModel的方法。
UserModel *user = [UserModel new];
if([user respondsToSelector:NSSelectorFromString(@"todoSomething")]){
[user performSelector:NSSelectorFromString(@"todoSomething")];
}
Swift的面向协议编程
面向协议的实现代码
protocol TodoProtocol {
func todosomething()
}
class UserModel: TodoProtocol {
func todosomething() {
print("要做的事情")
}
}
调用代码
let user:UserModel = UserModel()
if let user1 = user as? TodoProtocol {
user1.todosomething()
print("实现了这个协议")
}else{
print("没有实现这个协议")
}
这样来使用,这个协议的方法,其实这步骤判断不需要因为swift中的协议的方法都是required的没有optional。
42.谈谈as、as!、as? 含义是什么?
1)as的三种使用方式
- as第一种用法是将派生类转换成基类,也就是说子类直接转换成父类。 向上转型。
代码:
class Animal{
var name:String
init(_ name:String){
self.name = name
}
}
class catanimal : Animal {
}
class doganimal: Animal {
}
//as 向上强转 子转父
let cat = catanimal("汤姆") as Animal
let dog = doganimal("泰克") as Animal
showAnimalName(cat)
showAnimalName(dog)
如果是父类转换成子类,那么这种形式在Swift是会报错的,就算是加上!强转a!运行的时候也会崩溃。
- as第二种用法消除多义性,数据类型转换
比如说
let AnimalAge = 3 as Int //3 有可能是Int 也有可能是CGFloat 也可能是long Int
let AnimalWeight = 20 as CGFloat
let AnimalHeight = (50 / 2) as Double
- as的第三种用法switch语句中进行模式匹配,通过switch愈发检测对象的类型,根据对象类型进行处理。
switch animalType{
case let animal as catanimal:
print("是catanimal类型")
case let animal as doganimal:
print("是doganimal类型")
default: break
}
2)as!的用法
向下转型,强制转换类型,有个缺点就是如果转化失败就会报错误。
一般形式下可以这么使用,
catanimal是Animal的子类。
let animal:Animal = catanimal("汤姆")
let cat = animal as!catanimal
上述代码就是将类型强制转化,首先要注意一点,在强转之前一定要判断animal这个类是用catanimal这个类初始化的, 如果不这样做的话,是用Animal初始化的,那么在第二行强制转化的话就会报错。
3)as?的用法
as?跟as!是一样的用法,只不过as?如果转化不成功的话会直接返回一个nil对象,成功的话返回可选类型。如果说能%100的会成功转化那么请用as!如果说不能的话请用as?
使用例子:
let animal:Animal = catanimal("汤姆")
if let cat = animal as? catanimal {
print("是catanimal类型")
}else{
print("nil")
}
43.查看下述代码输出是什么?为什么?
protocol Police{
func HandlingCases()
}
extension Police{
func HandlingCases(){
print("Police doing Handling cases!" );
}
}
struct Judge:Police{
func HandlingCases(){
print("Judge doing Handling cases!" );
}
}
调用代码
let police1:Police = Judge()
let police2:Judge = Judge()
police1.HandlingCases()
police2.HandlingCases()
两个输出的内容都是Judge doing Handling cases!
,
因为在Swift中protocol声明了某个方法,在没有extension扩展协议的情况下必须在实现类中实现该方法,swift中的协议的方法都是required的,如果extension中实现了该方法,则在实现类Judge
中可以不用去实现,一旦实现,那么还是以实现类Judge
的实现方法为准。
HandlingCases
方法在Police
协议中声明了,police1
虽然声明的是Police
但是实际实现还是Judge
所以根据实际情况是调用了Judge
的HandlingCases
实现方法。同样道理police2
也是如此。
但是如果说在Police
中没有生命方法HandlingCases
,其他不变的情况下,那么police1
、police2
的输出就会不一样了。因为police1
的实际类型是Police
,Police
中并没有声明HandlingCases
,但是在类扩展中有实现该方法,实际类调用实际的方法,那么就会调用扩展类中实现HandlingCases
,而police2
的实际类型是Judge
那肯定就会调用Judge
中的实现方法。
所以他们的输出是:
police1
输出 Police doing Handling cases!
police2
输出 Judge doing Handling cases!
44.message send如果找不到对象,则会如何进行后续处理
这种形式一般会有两种情况:1)对象是nil; 2)对象不为nil但是就是找不到对应的方法;
1)对象为空的时候,在OC中向一个nil的对象发送消息是可以的,如果这个方法的返回值是对象那么返回的是nil,如果返回值是结构体,那么就是0.
2)对象不为空,找不到方法,就会崩溃,报错。
45.method swizzling 是什么?
每一个类都会维护一个方法列表,并且方法名跟方法实现是一一对应的,也就是SEL(方法名)和IMP(方法实现的指针)的对应关系,
method swizzling的意义就是运用runtime的特性跟方法来进行SEL和IMP这一对的IMP进行更换操作。如果SELa对应IMPa SELb对应IMPb 使用method swizzling后可以成为SELa对应IMPb SELb对应IMPa。
代码实现
在本类实现2个方法:
- (void)onefunc{
NSLog(@"one");
}
- (void)twofunc{
NSLog(@"two");
}
在本类中调用代码
SEL one = @selector(onefunc);
Method OneMethod = class_getInstanceMethod([self class], one);
SEL two = @selector(twofunc);
Method TwoMethod = class_getInstanceMethod([self class], two);
method_exchangeImplementations(OneMethod, TwoMethod);
这样就可以看到调用onefunc会打印two, 调用twofunc会打印one
46.Swift和OCjective-C的自省(Introspection)有什么不同
自省在Objective-C中就是:判断一个对象是否属于某个类的操作。它有两种形式。
[objc isKindOfClass:[SomeClass class]];
[objc isMemberOfClass:[SomeClass class]];
第一个判断isKindOfClass
是判断objc是否为SomeClass或者其子类的实例对象。
第二个判断isMemberOfClass
是判断objc仅仅是SomeClass这个类(非子类,当前类)的实例对象,并且不能检测任何类都是基于NSObject类。
这两种判断的前提是objc必须是NSObject的子类。
Swift中只有isKindOfClass
类似的方法is
,很多的类并不是继承自NSObject,不过比OC的功能更加强大,is
可以判断enum、struct类型
自省操作一般和动态类型一起出现,比如说OC中的id类型,以及Swift中的可选类型、anyobject。
cat是animal的子类
id animal = catInstance;
if([animal isKindOfClass:[animal class]]){
NSLog(@"是 animal class");
if(animal isMemberOfClass:[cat class]){
NSLog(@"是 cat class");
}
}else if([animal isKindOfClass:[any(其他类) class]]){
NSLog(@"是 其他 class");
}
47.能否通过Category给已有的类添加属性(property)
无论是Swift还是OC都可以用Category来添加属性,只不过添加的方式不一样。
Objective-C:
OC中通过Category中直接添加属性(property)会报错,提示找不到getter和setter方法,那是因为在Category中不会自动生成这两个方法,解决的方法就是运用runtime,关联对象的形式来添加属性,主要涉及到的两个函数是,objc_getAssocicatedObject和objc_setAssociatedObject.
objc_getAssocicatedObject两个参数: 本类实例对象、 关联属性的key
objc_setAssociatedObject中的方法有4个参数,分别是 本类实例对象、关联属性的key、新值、关联策略。
@interface UserModel : NSObject
@end
@implementation UserModel
@end
@interface UserModel(en)
@end
#import "UserModel+En.h"
#import <objc/runtime.h>
static void *EnNameKey = &EnNameKey;
@implementation UserModel(en)
-(void)setEnName:(NSString *)EnName{
objc_setAssociatedObject(self, &EnNameKey, EnName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)EnName{
return objc_getAssociatedObject(self, &EnNameKey);
}
@end
Swift跟Objective-C的使用方式差不多,如下代码
class UserModel {
var Name:String = "小明"
}
var EnNameKey:Void?
extension UserModel{
var EnName:String? {
get{
return objc_getAssociatedObject(self, &EnNameKey) as? String
}
set{
objc_setAssociatedObject(self, &EnNameKey, "你的英文名字", objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
}
48.谈谈看对Xcode你有多少理解。
iOS开的IDE是Xcode,它是apple开发的主流工具,目前Xcode已经更新到了9个版本。功能蕴含了开发、测试、性能分析、文档查询、源代码管理等多方面功能。
C、C++与Objective-C密不可分,自动化用Ruby,熟悉的工具:fastlane、cocoapods、Automation工具的脚本大多是用javaScript,刚发布的coreML采用的模型工具则是用Python开发的。最新的Xcode采用完全由Swift重写的Source Editor,在代码修改、补全、模拟器运行方面有了很大的提升,目前,Xcode最大的缺点是稳定性还不够。
Xcode工具想用的熟练,则必须从Intruments性能分析和LLDB调试,一步一步进行由浅入深、Swift最新的Playground也是一个不错的工具。
49.LLDB中的p和po的区别
- p是exor的缩写。它的工作是把接受到的参数在当前环境下运行编译,人后打印出对应的值。
- Po即expr-o-。操作跟p相同,如果接收的是一个指针,那么它会调用对象的description方法并打印;如果接收到的参数是一个core foundation对象,那么它会调用CFShow方法并打印,如果两个方法都调用失败,那么po打印出和p相同的内容。
- Po相对于p来说可以多打印一些内容,一般用p即可,毕竟打印的东西越少越快,效率越高。如果需要查看详情就用Po。
50.description方法是干什么用的?
这个是iOS对象默认的一个方法,它的输出格式一般为<类名: 对象的内存地址>
也可以自定义这个输出格式,NSObject类所包含。
51.Xcode中的Buildtime issues和Runtime issues指的是什么?
Buildtime issues一般分为三类:编译器识别出的警告(Warning)、错误(Error)和静态分析(Static Code Analysis)。前两者一般经常会遇到,不用多说,静态分析也可以分为三种:1)未初始化变量,未使用数据和API使用错误。
Swift代码:
class CusViewController: UIViewController {
override func viewDidLoad() {
let PeopleList:[UserModel]
let somePeopleList = PeopleList
let otherPeopleList:[UserModel]?
}
}
分析代码:
PeopleList并没有初始化就去赋值给somePeopleList所以是未初始化报错。
otherPeopleList并没有使用,那么就会出现未使用的数据,在viewDidLoad中没有使用super.viewDidLoad()那么就是Api调用错误。
Runtime issues也有三类错误:线程问题、UI布局和渲染问题、内存问题。线程的相关问题最多,最常见的就是数据的竞争。
var num = 0
let addnum1 = 10,addnum2 = 100
DispatchQueue.global().async {
for _ in 0...10{
num += addnum1
}
}
for _ in 0...10 {
num += addnum2
}
两个线程都对num进行写操作,这样的话,谁先操作,那么值就会根据方法+几 因为数据直接就存在了争抢关系,当然最终结果是一样的,但是中间的先后顺序就会打乱了。
UI布局和渲染上面的时候尺寸设定、布局没有给全,渲染设定模糊,因而造成的autolayout无法渲染。
内存上的问题就是内存泄漏,比如循环引用等。
52.App启动时间过长,该怎么去优化?
导致App启动时间过长的原因有多种,从理论上面来讲有两种情况:1)main函数加在之前;2)main还是加载之后。
main 函数加载之前,如果想要分析这块儿的代码,需要去Xcode中添加DYLD_PRINT_STATISTICS环境变量,并将其值设置为1,这样就可以得到如下的启动日志:
还有很多的静态变量,如果想查看的话,在终端man dyld
会打印出你要的静态变量的列表,可以一个一个的去打印看看。
例子:
Total pre-main time: 339.26 milliseconds (100.0%) //总的时间 毫秒
dylib loading time: 154.24 milliseconds (45.4%) //库加载
rebase/binding time: 78.42 milliseconds (23.1%) //重定向/绑定
ObjC setup time: 69.27 milliseconds (20.4%) //对象设置
initializer time: 37.18 milliseconds (10.9%) //初始化
slowest intializers :
libSystem.B.dylib : 8.49 milliseconds (2.5%) //系统库
libMainThreadChecker.dylib : 17.09 milliseconds (5.0%) //系统库
从上面打印的内容来看,大致就是上面的四个方面: 动态库加载、重定位绑定对象、设置对象、对象的初始化。
通过上述打印我们可以通过以下方式来优化App的启动时间:
减少动态库的数量,动态库加载时间会减少,apple推荐的动态库数量不超过6个。
减少Objective-C的类数量,例如合并和删除,这样可以加快动态链接,重定位/绑定耗费的时间会相应的减少。
使用initialize方法替代load方法,或尽量将load方法中的代码延后调用。对象的初始化所耗费的时间会相应减少。
在main之后的app启动时间主要是要优化第一个界面的渲染速度,主要是看进入Viewdidload viewwillAppear这两个方法是否有其他操作。
53.如何检测代码中的循环引用?
目前所了解的有两种方式:
1) 使用Xcode中的Memory Debug Graph。在Xcode中运行代码,在有可能循环引用的地方添加断点,然后点击如图所示的按钮就能查看是否循环引用。
上图中的内容,点击了这个按钮以后左边是类,右边是类图,其中谁引用了谁,这里很清楚的可以看到引用示意图。
2) 使用Instruments里面的leaks选项--这是一个专门检测内存泄漏的工具,在进入首页以后,发现leak Checks中出现内存泄漏,就是出现小红点的时候,可以将导航栏中的选项切换到call tree模式下,如果调试自己写的代码的话,建议勾选Display Settings 中勾选 "Separate by Thread"和"Hide System Libraries"两个选项。这样可以隐藏系统和应用程序本身的调用路径,从而更方便地找出retain cycle位置。
54.怎么解决EXC_BAD_ACCESS
产生EXC_BAD_ACCESS的主要原因是访问了某些已经被释放掉的对象,或者访问了它们已经释放的成员变量或方法。解决方法主要有下面几种:
- 设置全局断点,快速定位缺陷所在:这种方法效果一般。
- 重写Object和respondsToSelector方法:这个方法效果一般,并且要在每个class上进行定点排查,所以不推荐使用该方法。
- 使用Zombie和Address Sanitizer:可以在绝大多数情况下定位问题代码,
55.如何在Playground中执行异步操作
阅读下述代码,打印结果是什么?
import Foundation
let urlstr = URL.init(string: "https://api.apiopen.top/getSingleJoke")
let task = URLSession.shared.dataTask(with: urlstr!) { (data, response, error) in
do {
let dict:NSDictionary = try JSONSerialization.jsonObject(with: data!, options: []) as! NSDictionary
print(dict.object(forKey: "message") as! String)
print(dict)
} catch {
print(error)
}
} //这个是初始化任务,但是并没有执行,下面必须调用恢复,证明这个任务已经开始只不过暂停,字面意思哈。
task.resume() //恢复任务
答案是:什么都不会打印出来,原因是playground在执行完所有的操作以后会自动退出,要让playground打印出异步执行的内容,需要具备延时运行的特征,所以需要在Playground中添加下述代码:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
56.在Playground中实现一个10行的列表,每行显示一个0~100的整数。
啥也不说了上代码
import UIKit
import PlaygroundSupport
class ViewController :UITableViewController{
override func viewDidLoad() {
super.viewDidLoad()
}
}
extension ViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "\(Int(arc4random_uniform(100)))"
return cell
}
}
PlaygroundPage.current.liveView = ViewController()
57.runtime如何实现weak变量的自动置nil?
//问题延伸
Weak指针不会增加所引用对象的引用计数,并且在引用对象被回收的时候自动置nil,通常解决循环引用的问题,自动置nil的原理是什么?
Runtime维护了一个Weak的表,用于存储指向某个对象的所有Weak指针。Weak表就是一个hash表(哈希表),Key是所指对象地址,Value是Weak指针的地址(这个地址的值是所指对象的地址)的数组。
在对象被回收的时候,经过层层调用,会最终将这个Weak表中所有的Weak指针全部置nil。
源码在Runtime的源码中有个文件objc-weak.mm 其中就有这段代码。
/**
* Called by dealloc; nils out all weak pointers that point to the
* provided object so that they can no longer be used.
*
* @param weak_table
* @param referent The object being deallocated.
*/
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// zero out references
weak_referrer_t *referrers;
size_t count;
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
weak_entry_remove(weak_table, entry);
}
根据对象地址,找到weak指针的数组,遍历所有数组的weak指针置nil,最后将这个entry从weak表中删除 weak_entry_remove(weak_table, entry);
58.介绍一下GCD、NSOperation、NSTread分别是是什么?NSOperation和GCD比有哪些优缺点?
GCD是一个基于C语言的多线程编程的解决方式。NSOperation是更加注重面向对象的多线程编程的解决方式。NSTread是一个轻量级的多线程编程方式。
NSOperation更加抽象化,抽象化的方式可以使多线程管理并发,顺序,依赖关系时更加灵活:
- 性能上:GCD接近底层,基于C语言的代码执行更加高效,理论上速度比NSOperation快
- 异步队列操作上,管理顺序、依赖关系,这些面向对象的编程会更加方便去实现,而GCD在这种方式上面会更加麻烦,代码量会更多。
- 日常使用上,如果是简单的异步操作回调的形式建议是GCD,简单方便,如果对一些异步操作管理过程中有更多的线程顺序、依赖关系那么建议用NSOperation
最后扯个犊子, 这玩意就是看你用的哪个熟悉。只要能够快速准确的实现,用什么都是一样的。
59.怎么用Copy关键字
copy一般用在NSString、NSArray、NSDictionary等属性字段的修饰符。
因为这些属性都有与之对应可变的子类。用copy修饰的上述几个类型,在赋值的时候有坑会赋到可变的类型的指针,如果这个可变子类用strong修饰,那么一旦这个可变对象的值被修改了,那么这个对象也就被修改了,所以copy就是为了复制一份不可变的对象付给copy修饰的对象。
//mutableString这个参数是类A的一个变量类型是NSMutableString
NSMutableString* mutableString = [[NSMutableString alloc]initWithString:@"mutablestring"];
UserModel *user = [[UserModel alloc]init];
//首先将字符串赋值
user.title = @"title";
user.title = mutableString; //这时候title==mutableString==@"mutablestring"
//修改mutableString的值
[mutableString appendString:@"111"];
//得到的结果是user.title == mutableString == @"mutablestring111"
NSLog(@"title=%@--%p mutableString=%@--%p",user.title,user.title,mutableString,mutableString);
最终 title在不知不觉中就被修改了。所以用copy,修饰出来的属性都是不可变的。
在setter方法中:
- (void)setStr:(NSString*)str
if(_Str != str){
[_Str release];
_Str = [str copy];
}
60.什么是MVC、MVP、MVVM?MVVM的每一层关系是什么?
MVC 是Model、View、Controller。
View与Controller通信、Model与Controller通信 View与Model严格意义上是完全解耦合的。
-
View与Controller:
- action:点击、滑动、跳转、刷新UI、网络请求等,用户与View交互触发、控制器方法
- 委托delegate:View向Controller询问一些自己无法做到的事情,让Controller去解决,比如说获取数据
- 数据Model:View跟Controller要需要显示的数据,Controller需要访问Model,从Model中获取数据告诉View让其显示
-
Model与Controller
- 广播(Notification),Controller注册监听Model的数据变化的通知,当Model变化的时候告诉ControllerModel数据更新了
- KVO(KEY-Value-Observing):Model作为Controller的属性,Controller监听这个属性变化,当Model属性被修改时,这个Controller会接收到通知
分离了视图层和业务层
耦合性低
开发速度快
可维护性高
MVP 是Model-View(Controller)-Presenter
MVP模式也是一种经典的界面模式。MVP中的M代表Model, V是View, P是Presenter。在MVP里,Presenter完全把Model和View进行了分离,主要的程序逻辑在Presenter里实现。而且,Presenter与具体的View是没有直接关联的,而是通过定义好的接口(协议)进行交互,从而使得在变更View时候可以保持Presenter的不变,重用
优点
低耦合
可重用性
独立开发
可测试
缺点
如果view跟Presenter的交互太过于频繁,那就会跟特定的界面过于紧密,如果视图变更,那么presenter也要跟着变更了。
MVVM 是 Model View ViewModel
Model:业务逻辑处理、数据控制(本地缓存、网络加载)
View(Controller):显示用户可见、用户交互
ViewModel:组织View和Model的逻辑层
View绑定到ViewModel,然后执行一些命令在向它请求一个动作。
ViewModel跟Model通讯,反过来ViewModel在告诉View更新UI
优点
低耦合
可重用性
独立开发
可测试
缺点
因为ViewModel跟Presenter不一样的是所有的数据都是协议、接口回调,而ViewModel则是数据绑定到View上面,如果是数据bug问题,很难定位到数据哪一步出错。
在一个模块中 viewModel中如果Model的数据很大,长期不释放,这是一个不必要的花销。
数据双向绑定不利于代码的重用,开发中常见的重用都是view,一个view中绑定一个model,不同的模块中的model不同,到时候如果只简单的重用view,Model不做任何变化的话不太现实。