iOS语言工具 - Objective-C

1. Swift

2. Objective-C

3. Swift VS Objective-C

4. Xcode 使用

2. Objective-C

Objective-C 是苹果公司为 iOS 和 Mac 开发量身定制的语言。它随着 iPhone 的出现而大火,如今仍是国内外很多 App 使用的开发语言。

Objective-C 一度在 TIOBE 排行榜上位列第三,仅次于 Java 和 C,其市场占有份额也远超其他语言。光看名字就知道 Objective-C 与 C 语言有着千丝万缕的联系,事实上也确实如此:Objective-C 是 C 语言的超集,它在 C 语言的主题上加了面向对象的特性。这是为了方便 App 的开发,也是为了兼顾语言的整体性能。

在如今的企业面试中,传统知名的企业,如“BAT”,对 Objective-C 的考察比较多,其日常开发也是以 Objective-C 为主。这里会探讨 Objective-C 语言的基本特性。其动态性会在与 Swift 的比较中涉及。

Objective-C 面试理论题

什么是 ARC

关键词: #内存管理

ARC 即 Automatic Reference Counting,它是 Objective-C 的内存管理机制。简单的说就是代码中自动加入了 retain/release,原先需要手动添加用来处理内存管理的引用计数的代码可以自动地由编译器完成了。

ARC 的使用是为了解决对象 retain 和 release 匹配的问题。以前因手动管理而造成的内存泄漏或者重复释放的问题将不复存在。

加分回答:

以前需要手动通过 retain 为对象获取内存,并用 release 释放内存,这种操作被称为 MRC(Manual Reference Counting)。

ARC 与 Garbage Collection 的区别在于,Garbage Collection 在运行时管理内存,可以解决 retain recycle,而 ARC 在编译时管理内存。

什么情况下会出现循环引用

关键词: #内存管理

循环引用是指两个或两个以上的对象相互引用,导致所有的对象无法被释放的现象。这是内存泄露的一种情况。例如下面的例子:

扫描二维码关注公众号,回复: 5194873 查看本文章
=== class Father ===
@interface Father: NSObjct
@property (strong, nonatomic) Son *son;
@end

=== class Son ===
@interface Son: NSObject
@property (strong, nonatomic) Father *father;
@end

上述代码有两个类,分别是爸爸(Father)和儿子(Son)。爸爸对儿子强引用,儿子对爸爸强引用。所以要释放儿子,则必须先释放爸爸,要释放爸爸,则必须先释放儿子。如此一来,两个对象都无法被释放。

解决方法是将 Father 中的 Son 对象属性由 strong 改为 weak。

加分回答:

内存泄漏可以用 Xcode 中的 Debug Memory Graph检查。同时,Xcode 也会在 runtime 中自动汇报内存泄漏的问题。

说明并比较关键词: strong,weak,assign 和 copy

关键词: #内存管理 引用类型

  • strong 表示指向并拥有该对象。其修饰的对象引用计数会增加 1。该对象只要引用计数不为 0,就不会销毁。当然,强行将其置为 nil 也可以销毁它。

  • weak 表示指向但不拥有该对象。其修饰的对象引用计数不会增加。无需手动设置,该对象会自行在内存中销毁。

  • assign 主要用于修饰基本数据类型,如 NSInteger 和 CGFloat,这些数值主要存在于栈中。

  • weak 一般用来修饰对象,assign 一般用来修饰基本数据类型。原因是 assign 修饰的对象被释放后,指针的地址依然存在,造成野指针,在堆上容易造成崩溃,而栈上的内存系统会自动处理,不会造成野指针。

  • copystrong 类似。不同之处是, strong 的复制是多个指针指向同一个地址,而 copy 的复制是每次都会在内存中复制一份对象,指针指向不同的地址。copy 一般用在修饰有对应可变类型的不可变对象上。如 NSString,NSArray 和 NSDictionary。

加分回答:

在 Objective-C 中,基本数据类型的默认关键字是 atomic,readwrite 和 assign,普通属性的默认关键字是 atomic,readwrite 和 strong。

说明比较关键词:atomic 和 nonatomic

关键词: #线程

  • atomic 修饰的对象会保证 setter 和 getter 的完整性,任何线程访问它都可以得到一个完整的初始化后的对象。因为要保证操作完成,所以速度比较慢。atomic 比 nonatomic 安全,但也不是决定的线程安全,例如,当多个线程同时调用 setter 和 getter 时,就会导致获得的对象不一致。想要线程绝对安全,就要用 @sychronized。

  • nanotomic 修饰的对象不保证 setter 和 getter 的完整性,所以,当多个线程访问它时,它可能会返回未初始化的对象。正因为如此,nonatomic 比 atomic 速度快,但是线程也是不安全的。

runloop 和线程有什么关系

关键词: #线程 #runloop

**runloop` 是每个线程一直运行的一个对象,它主要用来负责响应需要处理的各种事件和消息。每个线程有且仅有一个 runloop 与其对应,没有现成,就没有 runloop。

在所有线程中,只有主线程的 runloop 是默认启动的,main 函数会设置一个 NSRunLoop 对象。而其他线程的 runloop 是默认没有启动的,可以通过 [NSRunLoop currentRunLoop] 来启动。

类似问题:

atomic 是百分之百安全的么?

说明并比较关键词: __weak 和 __block

关键词: #变量修改 #block

  • __weakweak 基本相同,前者用于修饰变量(variable),后者用户修饰属性(property)。__weak 主要用于防止 block 中的循环引用。

  • __block 也用于修饰变量。它是引用修饰,所以其修饰的值是动态变化的,即可以被重新赋值的。__block 用于修饰某些 block 内部将要修改的外部变量。

加分回答:

__weak 和 __block 的使用场景几乎与 block 息息相关。而所谓的 block,就是 Objective-C 对于闭包的实现。闭包就是没有名字的函数,或者可以理解为指向函数的指针。

什么是 block?它和代理的区别是什么

关键词: #回调

在 iOS 开发中,block 和 代理都是回调方式。block 是一段封装好的代码,下面是个最简单的例子:

/// 动画结束后的内容就是一个 block
[UIView animateWithDurtion: 1.0 animations: ^{
    NSLog(@"动画已完成");
}];

/// 声明一个名为 sum 的 block,返回两个整数值之和
NSInteger (^sumOfNumbers)(NSInteger a, NSInteger b) = ^(NSInteger a, NSInteger b) {
    return a + b;
};

而代理的声明和实现一般分开,比如 UITableViewDelegate,就是代理的声明在 UITableView 中,实现在某个 UIViewController 中。

Block 和 代理的区别首先在于,block 集中代码块,而代理分散代码块,所以,block 更适用于轻便、简单的回调,如网络传输。而代理更适用于公共接口较多的情况,这样做也更易于解耦代码架构。

两者的另一个却别在于,block 的运行成本高。block 出栈时,需要将使用的数据从栈内存复制到堆内存,当然,如果是对象就是加计数,使用完或者 block 置为 nil 后才消除;delegate 只是保存了一个对象指针,直接回调,并没有额外消耗。相对 C 的函数指针,只是多做了一个查表动作。

注意,block 容易造成循环引用,解决的方式是用 __weak 关键词修饰变量构成弱引用。

Objective-C 面试实战题

属性声明代码风格考察

关键词: #属性声明

请问下面代码有什么问题?

@property (nonatomic, strong) NSString *title;
@property (assign, nonatomic) int workID;
  • title 不应该用 strong 来修饰,而应该用 copy。因为 NSString 是不可变量的数据类型,它有对应的 NSMutableString 数据类型,用 strong 来修饰会有 NSString 被修改的可能,下面举个例子:

    self.title = @"defineTitle";
    NSMutableString *mutableTitle = @"mutableTitle";
    self.title = mutableTitle;
    

    原来 title 的值是 "defineTitle",后来被改成了 "mutableTitle"。

    有对应可变类型的不可变数据类型都应该修饰为 copy。copy 表示该属性不能被修改。

    <del>如果对可变类型如 NSMutableString 用 copy 来修饰,那么当对其进行修改时,程序就会崩溃。)</del> 备注:我测试时并未崩溃,如下:

    #import "ViewController.h"
    
    @interface ViewController ()
    @property (nonatomic, copy) NSMutableString *name;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
       [super viewDidLoad];
        self.name = @"hello";
        NSMutableString *t1 = @"哈哈";
        self.name = t1;
        NSString * t2 = @"天气";
        self.name = t2;
    }
    @end
    
  • workID 不应该用 int 类型, 而应该用 NSInteger 类型。Int 只表示 32 位的整数类型,而 NSInteger 类型在 32 位计算机中与 Int 一样,在 64 位计算机中则是 64 位的整型数据。对弈不同类型的计算机,NSInteger 更加灵活和准确。同理,可以用 NSInteger 替代 unsigned,用 CGFloat 代理 Float。

  • 在属性声明时,最好遵循原子性,以及读写顺序、内存管理顺序,这样可读性更高。

    上面代码的正确写法如下:

    @property (nonatomic, copy) NSString *title;
    @property (nonatomic, assign) NSInteger workID;
    

架构解耦代码考察

关键词: #属性声明 架构解耦

请问下面代码有什么问题?

typedef enum {
    Normal,
    VIP
} CustomerType;

@interface Customer : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) UIImage * profileImage;
@property (nonatomic, assign) CustomerType customerType;

@end
  • enum 的定义写得不够好。苹果官方推荐使用 NS_ENUM 来定义枚举。同时,在枚举的每个类型前应加上 enum 的名称,这样方便在混合编程时直接在 Swift 中调用。

  • UIImage 不应该出现在 Customer 中。Customer 明显是一个 Model 类,UIImage 应该归属于 UIView 部分。无论是 MVC 还是 MVVM,亦或是 VIPER,Model 都应该和 UIView 划清界限,避免整个价格耦合。下面是正确代码:

    typedef NS_ENUM(NSInteger, CustomerType) {
    CustomerTypeNormal,
    CustomerTypeVIP
    };
    
    @interface Customer : NSObject
    
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, strong) NSData * profileImageData;
    @property (nonatomic, assign) CustomerType customerType;
    
    @end
    

内存管理语法考察

关键词: #内存管理 引用类型

请问下面代码打印的结果是什么?

NSString *firstStr = @"helloworld";
NSString *secondStr = @"helloworld";
if (firstStr == secondStr) {
   NSLog(@"Equal");
} else {
   NSLog(@"Not Equal");
}

答案是打印出 "Equal"。

  • == 判断的可不是这两个值是否相等,而是这两个指针是否指向同一个对象。如果要判断两个 NSString 值是否相等, 那么应该用 isEqualToString 这个方法。

  • 上面的代码中,两个指针指向不同的对象,尽管他们的值相同。但是 iOS 的编译器优化了内存分配,当两个指针指向两个值一样的 NSString 时,两者指向同一个内存地址。所以代码会进入 if 语句的判断,打印出 “Equal” 字符串。

多线程语法考察

关键词: #多线程

请问下面的代码有什么问题?

- (void)viewDidLoad {
    [super viewDidLoad];
    UILabel *alertLabel = [[UILabel alloc]initWithFrame:CGRectMake(10, 0100, 100, 100)];
    alertLabel.text = @"wait 4 seconds";
    [self.view addSubview:alertLabel];
    alertLabel.backgroundColor = [UIColor redColor];
    
    NSOperationQueue *background = [[NSOperationQueue alloc]init];
    [background addOperationWithBlock:^{
        [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:4]];
        alertLabel.text = @"Ready to go!";
    }];
}

上面的代码问题在于,再等了 4s 后,alertLabel 并不会更新 Ready to go!。原因是,所有与 UI 相关的操作都应该放到主线程进行。我们当然可以在一个后台线程中等 4s,但是一定要在主线程更新 alertLabel。

最简单的修正方法如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    UILabel *alertLabel = [[UILabel alloc]initWithFrame:CGRectMake(10, 0100, 100, 100)];
    alertLabel.text = @"wait 4 seconds";
    [self.view addSubview:alertLabel];
    alertLabel.backgroundColor = [UIColor redColor];
    
    NSOperationQueue *background = [[NSOperationQueue alloc]init];
    [background addOperationWithBlock:^{
        [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:4]];
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
             alertLabel.text = @"Ready to go!";
        }];
    }];
}

以 scheduledTimerWithTimeInterval 的方式触发 timer,在滑动页面上的列表时,timer 会暂停,为什么?该如何解决?

关键词: #线程 runloop

造成此问题的原因,在于滑动页面上的列表时,当前线程的 runloop 切换了 mode 的模式,导致 timer 暂停。

runloop 中的 mode 主要用来指定事件在 runloop 中的优先级,具体有以下几种。

  • Default(NSDefaultRunLoopMode):默认设置。一般情况下使用。

  • Connection(NSConnectionReplyMode):用来处理 NSConnection 相关事件,开发者一般用不到。

  • Modal(NSModalPanelRunloopMode):用来处理 modal panels 事件。

  • Event Tracking(NSEventTrackingRunloopModes):用于处理拖拽和和用户交互的模式。

  • Common(NSRunloopCommonModes):模式合集,默认包括 Default、Modal 和 Event Tracking 三大模式。可以处理几乎所有事件。

再看看上述问题。在滑动列表时,runloop 中的 mode 由原来的 Default 模式切换到了 Event Tracking 模式,timer 原来运行在 Default 模式中,被关闭后自然就停止工作了。

解决方法:方法一是将 timer 加入 NSRunloopCommonModes 中。方法二是将 timer 放到另一个线程中,然后开启另一个线程的 runloop,这样可以保证与主线程互不干扰,而现在主线程正在处理页面滑动。

示例代码如下:

方法一:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

方法二:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
   timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(repeat:) userInfo:nil repeats:true];
   [[NSRunLoop currentRunLoop] run];
})

猜你喜欢

转载自blog.csdn.net/weixin_33851604/article/details/87210554