iOS (四)

如何重写UIButton来实现自定义image和title的布局?

  • 重写UIButtontitleRectForContentRect:的方法返回 title 的frame
  • 重写UIButtonimageRectForContentRect:的方法返回 image 的frame

Method,SEL 和 IMP

  • IMP,函数指针,指向方法的具体实现;
  • SEL,方法选择器,是一个C语言字符串。运行时系统可以根据SEL在类对象的方法列表中查找对应的IMP
  • Method,方法,由SELIMP和参数类型(方法参数类型和返回值类型)组成。

方法交换的原理

通过使用void method_exchangeImplementations(Method m1, Method m2)函数去交换两个Method实例m1m2IMP函数指针。之后,通过m1方法的SEL去进行消息传递时,会调用m2方法原本的IMP函数指针所指向的函数;通过m2方法的SEL去进行消息传递时,会调用m1方法原本的IMP函数指针所指向的函数。

当我们 hook 某个类的由其父类实现的方法时,例如,创建一个BaseViewController类,hook 由其父类UIViewController实现的viewWillAppear方法,代码如下:

@interface BaseViewController : UIViewController

@end


@implementation BaseViewController

+ (void)load
{
    Method original = class_getInstanceMethod([self class], @selector(viewWillAppear:));
    Method replacement = class_getInstanceMethod([self class], @selector(xxx_viewWillAppear:));

    method_exchangeImplementations(original, replacement);
}

- (void)xxx_viewWillAppear:(BOOL)animated
{
    [self xxx_viewWillAppear:animated];

    NSLog(@"test");
}

在进行方法交换之后,在调用父类UIViewControllerviewWillAppear:方法时,会根据SELUIViewController及其父类的方法列表中去查找viewWillAppear:方法,并调用viewWillAppear:方法的IMP函数指针所指向的函数。这时,实际上调用的是BaseViewControllerxxx_viewWillAppear:方法的原始对应函数。而在BaseViewControllerxxx_viewWillAppear:方法的原始对应函数实现中,又会执行[self xxx_viewWillAppear:animated]方法。

这时,会再一次进入到消息传递流程。运行时系统会根据SELUIViewController及其父类(self属于UIViewController类,而不是BaseViewController类)的方法列表中查找xxx_viewWillAppear :方法,但是UIViewController类及其父类并没有实现xxx_viewWillAppear :方法,所以方法查找会失败,导致 app 运行崩溃

为了不影响父类UIViewController调用viewWillAppear:方法,我们可以在执行method_exchangeImplementations函数之前,先调用class_addMethod函数给BaseViewController类动态添加一个viewWillAppear :方法(这样做相当于子类重写了父类的方法)并且设置该方法的IMP函数指针为xxx_viewWillAppear :方法的原始函数指针。接着,调用class_replaceMethod函数去将xxx_viewWillAppear:方法的函数指针替换为viewWillAppear :方法的原始函数指针。这样,父类UIViewController调用viewWillAppear:方法时,就不会受到影响了。只有BaseViewController调用viewWillAppear:时,才会去执行xxx_viewWillAppear:方法。

+ (void)load
{
    Method original = class_getInstanceMethod([self class], @selector(viewWillAppear:));
    Method replacement = class_getInstanceMethod([self class], @selector(xxx_viewWillAppear:));

    BOOL result = class_addMethod([self class], @selector(testMethod), method_getImplementation(replacement), method_getTypeEncoding(replacement));
    
    if (result)
    {
        class_replaceMethod([self class], @selector(test_testMethod), method_getImplementation(original), method_getTypeEncoding(original));
    }
    else
    {
        method_exchangeImplementations(original, replacement);
    }
}

由于类的+initialize方法可能被多次执行,并且 category 实现的+initialize方法会覆盖掉宿主类实现的+initialize方法,我们选择在类的+load方法中去执行方法交换。

虽然系统只会调用一次类的+load方法,但是为了防止开发者自行再次调用类的+load方法,需要使用dispatch_once函数去保证方法交换操作只执行一次。

如何hook一个类的某个实例对象的方法而不会影响该类的其他实例对象?

使用 runtime 提供的 API 在运行时动态创建一个继承自实例对象所属类的子类,并向运行时系统注册这个子类,然后重写这个子类的对应方法,最后将这个实例对象的 cls 指针指向这个子类。

NSString 和 NSDictionary 类型的属性为什么要用 copy 修饰?

NSMutableStringNSString的子类,在开发过程中,有可能误将NSMutableString类型的字符串对象赋给了NSString类型的属性。如果后续修改了可变字符串对象的内容,属性值也会被修改,这可能会导致程序出错。为了避免出现这种错误,需要使用copy去修饰NSString类型的属性。

如何销毁单例?

onceToken置为0,并将单例对象置为nil

dispatch_once实现原理

void dispatch_once_f(dispatch_once_t *val, void *ctxt, void (*func)(void *))
{    
    volatile long *vval = val;
    if (dispatch_atomic_cmpxchg(val, 0l, 1l)) 
    {
        func(ctxt); // block真正执行
        dispatch_atomic_barrier();
        *val = ~0l;
    } 
    else 
    {
        do
        {
            _dispatch_hardware_pause();
        } while (*vval != ~0l);
        dispatch_atomic_barrier();
    }
}

dispatch_once函数的内部实现使用了原子操作来同步地判断并修改dispatch_once_t的值。如果dispatch_once_t的值为0l,就将dispatch_once_t的值改为1l,并执行block。block执行完毕后,会将dispatch_once_t的值改为~0l;如果dispatch_once_t的值不为0l,则说明其他线程正在执行block或者block已经执行过一次了。此时,dispatch_once函数就会进入忙等状态,直到dispatch_once_t的值变为~0l

NSNotificationCenter通知中心的实现原理

NSNotificationCenter内部维护着一个哈希表,其 key 是通知名称,value 是一个数组,数组中保存着一组包含被通知对象和回调方法的数据结构。发送通知时,会根据通知名称查找对应的数组,然后遍历数组并调用被通知对象的回调方法。

远程推送(APNs)的原理

APNs.jpg
  1. 应用程序使用 UDID 和 BundleID 向 iOS 注册消息推送;
  2. iOS 向 APNs 服务器请求 device token,应用程序接收 device token;
  3. 应用程序将 device token 发送给应用程序的 push 服务器;
  4. 应用程序的 push 服务器将要发送的消息和 device token 打包发送给 APNs 服务器;
  5. APNs 服务器收到消息之后,根据 device token 找到对应的注册设备,然后将内容推送给目标设备。

如何调试 EXC_BAD_ACCESS 崩溃?

访问一个已经释放内存的对象时,可能会引发 EXC_BAD_ACCESS 崩溃。

对象的dealloc方法只是告诉系统其不再使用这块内存了,系统并没有使这块内存不可访问。因而,在访问一个已经释放内存的对象时,如果这块内存还没有被覆盖,原来的数据保存完好,那么可能就不会立即引发崩溃。只有当这块内存已经被覆盖,并写上了不可访问的数据之后,才会立即引发崩溃。

勾选 Xcode 的Product->Scheme->Edit Scheme->Run->Diagnostics->Memory Management->Zombie Objects选项后,当对象的引用计数为0时,系统会将对象转换为一个僵尸对象。向僵尸对象发送消息时,就会立即引发 EXC_BAD_ACCESS 崩溃。同时,Xcode 控制台会输出访问已释放对象的具体代码;

勾选 Xcode 的Memory Management->Malloc Scribble选项,对象释放内存后,这块内存会被立即填上不可访问的数据,当再次访问这块内存时,会立即引发 EXC_BAD_ACCESS 崩溃。

如果僵尸对象不能帮助解决问题,则可以使用 Xcode 的Product->Analyze选项去静态分析代码,找出所有可能产生野指针的代码,然后再进一步分析。

更多信息,可以参看:如何定位Obj-C野指针随机Crash 和 如何处理iOS崩溃crash大解析。

如何实现多个任务执行完毕后再执行其他任务?

  • 使用dispatch_async_group函数向并行队列中添加多个任务,并使用dispatch_group_notify函数向并行队列中添加前面任务执行完毕后才开始执行的任务。如果任务内部有自己的子线程,例如,使用AFNetworking发送网络请求,则需要使用dispatch_group_enterdispatch_group_leave函数标识任务是否执行完毕;
  • 使用NSOperation,配置好NSOperation之间的依赖后,将它们添加到NSOperationQueue中。如果任务内部有自己的子线程,则需要使用自定义NSOperation来自行控制任务是否执行完毕;
  • 使用dispatch_async函数向并行队列中添加多个任务,使用dispatch_barrier_async函数向并行队列中添加前面任务执行完毕后才开始执行的任务。如果任务内部有自己的子线程,这种方式就会失效。

数组去重方式

  • 使用数组创建一个NSSetNSSet会自动去重,NSSetallObjects就是去重之后的数组;
  • 创建一个可变数组,然后遍历不可变数组的元素,使用可变数组的containsObject:方法判断是否包含该元素,不包含就将其添加到可变数组中;
  • 创建一个NSDictionary,遍历数组,将数组中的元素作为key添加到字典中,最后NSDictionaryallKeys就是去重之后的数组;
  • 使用KVC的数组运算符去重,resultArray = [array valueForKeyPath:@"@distinctUnionOfObjects.self"]

如何寻找出两个视图的最近的公共父视图?

查找视图的所有父视图:

- (NSArray *)superviewsOfView:(UIView *)view
{
    if (view == nil) return @[];

    NSMutableArray *array = [[NSMutableArray alloc] init];
    
    while(view != nil)
    {
        if (view.superview)
        {
            [array addObject:view.superview];
        }
        view = view.superview;
    }
    return array;
}

查找两个数组中第一个重复的元素:

  • 遍历数组A,看数组B是否包含数组A的元素([array containsObject:object]),第一次查找到的数组B也包含的元素就是两个视图最近的公共父视图。这种方法使用了双重遍历,其时间复杂度为O(N^2);
  • 根据数组B创建一个NSSet集合,遍历数组A,看NSSet集合是否包含数组A的元素([set containsObject:object]),第一次查找到的NSSet集合也包含的元素就是两个视图最近的公共父视图。NSSet集合内部是用哈希表实现的,其时间复杂度为O(N);

alloc 方法、 init 方法和 new 方法分别做了什么事情?

// 类
type struct objc_class *Class

// 类的实例
struct objc_object {
    Class isa;
    ......
}

// 一个指向类的实例的指针
typedef struct objc_object *id;

alloc

alloc方法内部会调用_objc_rootAlloc函数,_objc_rootAlloc函数会调用callAlloc函数,callAlloc函数会调用_objc_rootAllocWithZone函数,_objc_rootAllocWithZone函数会调用_class_creatInstanceFormZone函数去创建实例。_class_creatInstanceFormZone函数主要做了以下事情:

  • 调用类对象的alignedInstanceSize函数计算需要为实例对象分配的内存大小。由于64位架构下是以 16 字节为单位来进行内存分配的,而实例对象实际占用的内存可能不足 16 字节或者大于 16 字节,所以需要进行内存对齐。(不足 16 字节的,则对齐为 16 字节;大于 16 字节的,则对齐为 16 字节的整数倍。
  • 调用calloc函数申请内存。
  • 调用objc_constructInstance函数构造实例对象,这一步主要是初始化isa,并调用实例对象的 C++ 构造函数(如果存在的话),最后,返回一个指向所申请内存的首地址的id类型的指针。

init

NSObjectinit方法默认会调用_objc_rootInit函数,_objc_rootInit函数会直接返回指向对象内存首地址的id类型指针,而不会做其他任何事情。

子类可以重写init方法来对实例对象进行初始化设置。

new

new方法会调用callAlloc函数,然后再调用init方法。

Objective-C 对象占用的内存大小

基本数据类型的内存占用.png

在64位架构下,int类型占用 4 个字节,long类型占用 8 个字节,float类型占用 4 个字节,double类型占用 8 个字节,BOOL类型占用 1 个字节,char类型占用 1 个字节,void *类型占用 8 个字节。

Objective-C 对象本质上是一个结构体。未对齐的结构体占用的内存大小是其每个数据成员所占用的内存大小之和,编译器在编译时需要对未对齐结构体进行内存对齐。

这是因为 CPU 在存取数据时并不是每次只存取 1 个字节的数据,而是一块一块的进行存取。块的大小被称为内存存取粒度,内存存取粒度可以为 2,4,8,16 字节。当存取未对齐数据时,需要进行 2 次存取操作。而当存取已对齐数据时,只需要进行 1 次存取操作。由于每次内存存取都会产生一个固定的开销,所以减少内存存取次数就可以提升程序的性能。所以编译器在编译时,会对结构体进行内存对齐。

结构体的内存对齐规则如下:

  • 结构体中第一个数据成员的起始位置为0,之后每个数据成员的起始位置是该数据成员本身尺寸的整数倍。
  • 结构体的尺寸是最大的基本数据类型数据成员尺寸的整数倍。
  • 结构体作为数据成员时,结构体数据成员的起始位置就是该结构体数据成员中尺寸最大的基本数据类型数据成员尺寸的整数倍。而结构体的尺寸则是其所有结构体数据成员的与自身中的最大的基本数据类型数据成员尺寸的整数倍。

由于每个 Objective-C 对象都包含一个isa指针,isa指针是void *类型(对象类型的成员变量是一个指针,指针属于void *类型),在64位架构下占用 8 个字节,所以64位架构下的 Objective-C 对象占用的内存大小都是 8 字节的整数倍。

Objective-C 类中定义的属性顺序会在编译时进行优化调整,其调整规则就是先按数据类型的尺寸从小到大进行排列,相同尺寸的数据成员则按字母顺序进行排列。另外,Objective-C 类转换成结构体之后,继承自父类的属性会排在最前面,之后才是自己定义的属性,所以 Objective-C 类的第一个数据成员始终是isa指针。

有关内存对齐的更多信息,可以参看 iOS 内存字节对齐,iOS开发之内存对齐,一个OC对象在内存中的布局&&占用多少内存,iOS结构体尺寸。

内存平移问题:执行以下 viewDidLoad 方法后,程序是否会崩溃?两种调用方式的输出结果又是什么?

@interface Person : NSObject

@property (nonatomic, strong) NSString *name;

@end

@implementation NSObject

- (void)doSomething
{
    NSLog(@"%@", self.name);
}

@end

struct TestStruct {
    int a;
};

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // struct TestStruct a = {100};

    // 调用方式 1
    Class cls = [Person class];
    void *kc = &cls;
    [(__bridge id)kc doSomething];

    //  调用方式 2
    Person *person = [Person alloc];
    [person doSomething]
}

@end

调用[Person alloc]方法后,会创建一个实例对象,该实例对象存储在堆区,该实例对象第一个属性是指向其类对象的isa指针,isa指针占用 8 个字节(64位架构下)。person是一个指向该实例对象的指针变量,占用 8 个字节。执行[person doSomething]方法时,会先获取person指针指向的实例对象,并从实例对象所在内存空间的起始地址开始,读取 8 个字节的数据,这 8 个字节的数据就是实例对象的isa指针的值,然后使用这个isa指针去查找doSomething方法的函数指针。

cls是一个指向Person类对象的指针,占用 8 个字节。kc是一个指向cls指针的指针,占用 8 个字节。当执行[(__bridge id)kc doSomething]方法时,会首先获取到cls指针,然后从cls指针所在内存空间的起始地址开始,读取 8 个字节的数据,这 8 个字节的数据就是cls指针的值,然后使用这个cls指针去查找doSomething方法的函数指针。

因此,以上两种调用方式都是可以成功调用doSomething方法的。

使用[person doSomething]方式调用时,执行self.name时,会从实例对象所在内存空间的起始地址偏移 8 个字节开始,读取 8 个字节的数据,这 8 个字节的数据就是name的值。由于并没有设置name属性的值,所以打印结果为null

使用[(__bridge id)kc doSomething]方式调用时,执行self.name时,会从cls指针所在内存空间的起始地址偏移 8 个字节开始,读取 8 个字节的数据。由于cls指针只占用了 8 个字节的内存空间,所以读取的 8 个字节数据会是其他变量的数据内容。

在执行函数时,会生成一个栈帧,栈帧就是一个函数执行的环境,其包含有函数参数、函数的局部变量、函数的返回地址,栈是从高地址向低地址扩展的。在执行过程中,函数的参数和局部变量会被压入到栈中,所以viewDidLoad方法对应的栈从高地址低地址依次存放的是self(指向 viewController 对象的指针)->_cmd(指向 sel 的指针)->objc_super 结构体->cls 指针->kc 指针->person 指针。(self_cmdviewDidLoad方法的参数,super本质上是一个objc_super结构体

使用calloc函数创建的实例对象和类对象存放在堆区,局部变量(函数内部的指针和结构体)存放在栈区。

cls指针所在内存空间的结束地址开始,读取 8 个字节的数据,这 8 个字节的数据是objc_super结构体的数据内容。objc_super结构体第一个数据成员是id类型的receiver指针,其指向 viewController 对象,第二个数据成员是Class类型的super_class指针,其指向 viewController 对象的父类类对象。因此,objc_super结构体所在内存空间从低地址高地址依次存放的是receiver指针和super_class指针,读取的 8 个字节数据就是receiver指针,最后输出结果就是viewController对象的地址。

如果Person类的name属性是一个int类型,就会读取 4 个字节的数据,这 4 个字节的数据只是receiver指针的一部分,所以输出结果就是这 4 个字节的数据转换为int类型后的值。如果我们在[super viewDidLoad]后面添加代码去创建一个结构体,这个结构体只包含一个int类型的数据成员,该数据成员的值为100,那么读取到的 4 个字节数据,就是这个结构体的int类型数据成员的值,最后输出结果就是100。

不使用其他变量,如何实现交换两个变量的值?

int a = 10;
int b = 15;
b = a + b; 
a = b - a; // a + b - a
b = b - a; // a + b - b

使用递归求1到n的和

int sum(int n)
{
    if(n == 1)
    {
        return 1;
    }else
    {
        return n + sum(n-1);
    }
}

NSCache 和 NSMutableDictionary

  • NSCacheNSMutableDictionary提供的功能是相同的;
  • NSCache是线程安全的,NSMutableDictionary是线程不安全的;
  • 在应用程序内存不足时,NSCache会自动释放一部分缓存。还可以给NSCache设置一个最大缓存容量,当缓存超过了最大缓存容量,NSCache会自动释放一部分缓存。
  • NSMutableDictionary的key对象必须实现NSCopying协议,而NSCache的key对象不需要。

super 和 self 的区别

  • selfself所在方法的调用对象,使用self去调用某个方法时,会使用objc_msgSend函数从self的类对象的方法列表中开始查找该方法的方法实现。
  • super是一个objc_super结构体,其包含一个消息接收者对象,是super所在方法的调用对象,也就是self。使用super去调用某个方法时,会使用objc_msgSendSuper函数从objc_super结构体中的消息接收者对象的父类类对象的方法列表中开始查找该方法的方法实现。找到方法实现后,还是会使用这个消息接收者对象去调用该方法。

[self class][super class]的执行结果是相同的,[self superClass][super superClass的执行结果也是相同的。因为父类和子类的class方法的方法实现是一样的,都是根据消息接收者对象的isa指针去获取类对象,而它们的消息接收者对象都是self

nil、Nil、NULL 和 [NSNull null] 之间的区别

  • nil是一个空实例对象;
  • Nil是一个空类对象;
  • NULL是一个非对象类型的空值;
  • [NSNull null]是一个代表空值的单例对象,添加到字典和数组中的对象不能为nil,但可以为[NSNull null]

iOS为什么只能在主线程操作UI?

UIKit框架不是线程安全的,如果同时在多个线程操作UI,可能会出现一些问题。例如,在一个线程中遍历当前的视图层时,在另一个线程对视图层执行了更改操作,这可能会引发崩溃。

内存调优

使用 Instruments 中的 Allocations 查看应用程序的内存占用,使用 Leaks 检测内存泄漏。

主要有以下几方面的原因导致内存占用高:

  • 使用了不合理的 API,例如:使用[UIImage imageNamed:]从给定的文件加载图片数据时,系统会缓存创建的UIImage对象,且没有提供 API 进行清理。而使用[UIImage imageWithContentsOfFile:]从给定的文件加载图片数据时,系统不会缓存创建的UIImage对象,当UIImage对象没有再使用后,会释放掉这个UIImage对象;
  • 没必要常驻内存的对象,被实现为常驻内存;
  • 数据模型中存在多余的字段;
  • 循环引用导致的内存泄漏。

id 、 NSObject * 和 instancetype

  • idNSObject *都可以指向任何类型的对象;
  • NSObject *在指向对象时,需要强制进行类型转换;
  • id在指向对象时不用强制进行类型转换,可以直接使用;
  • instancetype只能用作方法的返回值类型,表示以方法所在类的类型作为返回值类型。

数组和链表的区别

  • 数组:数组是一块连续的内存空间,数组元素在内存上连续存放,可以通过索引查找元素;插入和删除操作会移动大量元素,比较适用于元素很少变化的情况。
  • 链表:链表中的元素在内存中不是连续存储的,查找较慢,插入和删除操作只需要对元素指针重新赋值,效率更高。

组件化

什么是组件化

将项目中的业务和基础功能剥离出来,并划分为一个个相互独立的模块,然后通过 cocoapods 来管理这些模块。

组件划分.png

组件化的优点

没有组件化的项目,业务模块之间互相引入,耦合严重。每次发版时,都要对所有业务进行回归测试。而且业务之间相互依赖,可能会导致一个业务只能在另一个业务开发完成后才能开始开发。另外,每次功能开发完成后准备提交代码时,往往有其他工程师提交了代码,需要重新拉去代码合并后再提交,即使开发一个很小的功能,也需要在整个工程里做编译和调试,效率较低。

项目完成组件化之后,会带来如下效果:

  • 加快编译速度,可以把不会经常变动的组件做成静态库,同时每个组件可以独立编译,不依赖于主工程或者其他组件;
  • 每个组件都可以选择自己擅长的开发模式(MVC / MVVM / MVP);
  • 可以单独测试每个组件;
  • 多条业务线可以并行开发,提高开发效率。

组件化的方案

  • url scheme:在与模块有关的类的+(void)load方法中创建一个执行模块的初始化和调用的block,并以指定的url作为key,将该block存储到组件管理器对象维护的字典中。模块在调用其他模块时,传递特定的url给组件管理器对象,然后组件管理器对象通过url查找相应的block,并执行这个block。这种方式需要提前将block存储到内存中,并且传递参数时不够透明;
  • target - action:每个模块都定义有一个 target 类,target 类中引用了与该模块相关的类,并封装了该模块的初始化和调用的方法。另外,为每个模块都定义一个组件管理器的分类,该分类中封装了执行该模块的 target 中封装的指定 action 的方法,组件管理器基于反射机制通过模块的 target 名称和 action 名称来调用 target 类中的方法。模块通过调用组件管理器分类中定义的方法来调用其他模块。这种方式在参数传递上比较透明,而且不需要注册block,但是需要实现 target 类和组件管理器的分类。
  • 依赖注入:使用 objection 第三方库。

什么是DNS解析?什么是DNS劫持以及如何解决该问题?

DNS解析

在客户端发起一个HTTP请求时,请求会先到达运营商的DNS服务器,DNS服务器将域名解析成对应的IP地址,然后根据IP地址在互联网上找到对应的服务器,向这个服务器发起一个HTTP请求,该服务器找到对应的资源后沿原路将资源返回给客户端。

DNS劫持

DNS劫持又称域名劫持,是指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,把审查范围以外的请求放行。否则,返回假的IP地址,或者什么都不做,使请求失去响应。其最后效果是对特定的网络不能访问,或者将请求转发到一个虚假的服务器。

解决办法

  • HttpDNS:国内提供域名解析 API 接口的,有 DNSPod,现在国内有很多厂商为 DNSPod 开发了 SDK,例如阿里、七牛等;
  • 内置IP列表:可以在启动阶段由服务端下发域名和 IP 的对应列表,客户端来进行缓存,发起网络请求的时候,直接根据域名映射到 IP 来进行业务访问。实现 HTTP 协议下 IP 连接其实是很简单的,只需要通过 NSURLProtocol 来拦截网络请求,然后将符合条件的网络请求 URL 中的域名修改为 IP 就可以了。

对称加密和非对称加密

对称加密又叫公开密钥加密,加密和解密都会用到同一个密钥。如果密钥被攻击者获得,此时加密就失去了意义。常见的对称加密算法有 DES、3DES。

非对称加密又称共享密钥加密,使用一对非对称的密钥,一把叫做私有密钥,一把叫做公有密钥,公钥加密的数据只能使用私钥解密,私钥加密的数据只能使用公钥解密。常见的非对称加密算法有 RSA。

TCP连接过程中的保活机制

  • 心跳检测:服务端定时向客户端发送一个数据包,如果在一定时间内没有收到客户端的回应,即认为客户端已经掉线;同样,如果客户端在一定时间内没有收到服务器的心跳包,则认为连接不可用;
  • TCP的Keep-Alive保活机制:服务端或者客户端开启Keep-Alive功能后,会自动在规定时间内向对方发送心跳包,而另一方在收到心跳包后就会自动回复,以告诉对方其仍然在线。 由于开启Keep-Alive功能需要消耗额外的宽带和流量,所以TCP协议层默认关闭Keep-Alive功能。

如何解决TCP连接的粘包和拆包问题?

TCP连接在发送数据包的时候会建立一个缓存区,发送的数据都会先进入这个缓存区,只有当上一条数据被确认接收或者到达最大等待时间之后,才会将缓存区的数据一块发送过去,如此反复。将小包进行整合,可以避免小包多次发送造成的传输速度慢等问题,但是会产生粘包和拆包问题。

粘包、拆包问题说明:

  • 服务端分2次读取到了两个独立的包,分别是D1,D2,没有粘包和拆包;
  • 服务端一次性接收了两个包,D1和D2粘在一起了,被成为TCP粘包;
  • 服务端分2次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为拆包。

粘包和拆包的解决方案:

  • 在每个数据包的包尾增加回车换行符来分割数据包,例如FTP协议;
  • 发送的每个数据包是由包头、包长和包体组成的。

发布应用程序的过程

  • 申请发布证书
  • 创建 App ID 并与应用程序的 Bundle Identifier 绑定
  • 生成描述文件将发布证书与 Bundle Identifier 绑定起来
  • 在 iTunes Connect 中新建一个应用程序,设置好应用程序名称和App ID
  • 下载证书和描述文件,并在项目工程文件中配置好,然后打包应用程序并上传
  • 填写应用程序的详细资料,并构建一个新版本,然后提交审核。

如何基于Audio Queue来实现音频播放?

  1. 从远程下载音频数据,并使用NSFileHandle将音频数据写入到本地文件中。
  2. 在下载和缓存音频数据的同时,使用NSFileHandle从本地文件中读取音频数据。
  3. 使用AudioFileStream解析音频数据来获取采样率、码率和时长等信息,并分离音频帧得到PCM数据。
  4. AudioQueue把PCM数据解码成音频信号,并将音频信号交给硬件播放。
  5. 重复2-4步直到播放完成。

AVAudioSession有几种模式?

  • AVAudioSessionCategoryAmbient:混音播放,可以与其他音频应用同时播放。当用户锁屏或者静音时,也会随着静音;
  • AVAudioSessionCategorySoloAmbient:独占播放,会打断其他音频应用的播放。当用户锁屏或者静音时,也会随着静音;
  • AVAudioSessionCategoryPlayback:后台播放并且独占,当用户锁屏或者静音时,会继续播放;
  • AVAudioSessionCategoryRecord:录音模式,会打断其他音频应用的播放;
  • AVAudioSessionCategoryPlayAndRecord:播放和录音,可以一边播放音频一边录制音频,用于音频通话;
  • AVAudioSessionCategoryMultiRoute:多种输入输出,例如可以耳机和USB设备同时播放;
  • AVAudioSessionCategoryAudioProcessing:硬件解码音频,此时不能播放和录制。

如何处理音频打断事件?

系统在接收到音频打断事件时,会发出通知。在收到音频打断开始通知时,系统已经暂停播放音频,
此时我们需要更新音频播放状态和UI,同时标记一下暂停播放音频的原因是接收到了打断事件。在收到音频打断结束通知后,判断一下当前暂停播放音频的原因是否是因为接收到了打断事件。如果是,则恢复播放并更新UI。

六大设计原则

  • 单一职责原则:一个类只负责一件事;
  • 开闭原则:不允许直接修改类的原有内容,但可以对类进行扩展添加其他内容;
  • 接口隔离原则:使用多个专门的协议,而不是一个庞大臃肿的协议;
  • 依赖倒置原则:外界可以直接调用接口而不用关心接口的具体实现,在具体实现中可以调用接口;
  • 理氏替换原则:父类可以被子类无缝替换,且原有功能不受任何影响;
  • 迪米特法则:一个对象应当对其他对象有尽可能少的了解,这样就能做到高内聚、低耦合。

MVC

MVC设计模式.png

MVC(Model - View - Controller)即模型 - 视图 - 控制器。

  • 模型对象负责封装应用程序的数据;
  • 视图对象负责展示数据;
  • 控制器对象负责协调模型对象和视图对象之间的通信,响应用户交互。

模型对象和视图对象不能直接进行通信,必须通过控制器来协调视图和模型之间的通信。控制器对模型和视图的访问是不受限的,但不允许模型和视图直接访问控制器,模型通过KVO或Notification来通知控制器,视图通过delegate和target-action来通知控制器。

MVC的缺点:控制器不仅要协调模型和视图之间的通信,还要管理视图层次结构和响应用户交互,这样就会导致控制器中的代码非常臃肿,不易于管理和维护。

MVVM

MVVM设计模式.png

MVVM(Model - View - ViewModel)即模型 - 视图 - 视图模型,其衍生于MVC。

  • 视图负责数据的展示;
  • 模型负责封装数据;
  • 视图模型封装了响应用户交互事件的逻辑、视图显示的逻辑、发起网络请求的逻辑以及其他逻辑。

模型对象和视图对象不能直接进行通信,必须通过视图模型对象来协调视图和模型之间的通信。视图可以直接访问视图模型,但不允许视图模型直接访问视图,视图模型可以使用回调来通知视图。视图模型可以直接访问模型,但不允许模型直接访问视图模型,模型可以使用回调来通知视图模型。

MVVM的缺点:数据绑定和数据转化需要花费更多的成本。

MVP

MVP.png

MVP 全称 Model - View / ViewController - Presenter,即模型-视图-协调器,是面向协议的设计模式。

  • Model:模型负责封装数据;
  • View:视图负责数据的展示;
  • Presenter: Model 和 View 之间的中间人。当用户对 View 有操作时,由它负责去修改相应的 Model。当 Model 发生变化时,由它负责去更新对应的 View。

MVP的缺点:需要写更多的代码。

设计模式

责任链模式

责任链模式就是为一个消息创建一个由接收者对象组成的链,这条链上的每一个对象都可以去响应这个消息。这种模式把消息的发送和接收进行解耦,每个接收者都包含对另一个接收者的引用。如果一个接收者不能处理该消息,那么它会把相同的消息传给下一个接收者,依此类推。例如,UIKit中的事件响应链就运用了责任链设计模式。

桥接模式

桥接模式将抽象部分与它的具体实现部分分离,使它们都可以独立地变化。例如,Foundation框架中的·NSOperationQueueNSOperation就使用了桥接模式,NSOperation中定义了startmaincancel方法,而NSBlockOperationNSInvocationOperation都继承自NSOperation并各自实现了这些方法。向NSOperationQueue中添加NSBlockOperationNSInvocationOperation后,NSOperationQueue内部在使用它们时,不用关心它们到底是NSBlockOperation还是NSInvocationOperation,而是直接调用NSOperation的接口。

适配器模式

当希望复用一个已经存在的类,而它的接口不符合复用环境的规范时,如果直接对这个类进行修改,可能会存在风险。这时,可以使用适配器模式来解决这个问题。适配器模式使用一个适配对象来引用被适配对象,由适配对象执行一些额外工作来适应新环境的规范,然后在复用被适配对象。

单例模式

一个类同时只能存在唯一的一个对象,当使用类的alloc方法创建对象时,会始终返回这个单例对象。

命令模式

命令模式把一系列动作或者行为封装成一个命令对象,客户端不需要知道其实现细节就可以执行这些动作或者行为。例如,Foundation 框架中的NSInvocation就使用了命令模式。

工厂模式

同一类型的不同实例对象有它们各自的创建方法。

享元模式

享元模式通过重复使用对象来减少同一类对象的大量创建,从而提高程序执行效率。UITableview的Cell重用就是一种享元模式。

装饰器模式

装饰器模式可以在不改变原类文件和不使用继承的情况下,动态地扩展一个对象的功能。装饰器模式是使用一个包装对象来包裹真实的对象。

项目的整体架构

  • 业务层,例如,“首页”模块,“消息”模块,“我的”模块;
  • 中间层,基于反射机制实现业务层之间的解耦;
  • 通用业务层,例如,通用的UI控件;
  • 独立于App的通用层,例如,AFNetworking、SDWebImage 等框架。

如何减少ipa包体积?

删除无用类、无用代码、无用的第三方库,删除不再使用的图片。

如何快速定位Bug?

使用第三方库 bugly 收集并上传应用程序的崩溃日志,根据崩溃日志获取设备机型、iOS 系统版本、app 版本、崩溃时的函数调用栈和引发崩溃的原因。接着,给程序设置异常断点和关键断点,并在代码中添加打印关键数据的 NSLog 语句。然后,使用 xcode 在设备上运行 app。当遇到断点时,使用 lldb 获取有关对象的属性信息。一步一步地分析这些数据来找到产生 bug 的原因,并修复 bug。

app版本升级,数据库中的表结构变了,如何进行数据库迁移?

  • 在本地保存数据表的当前版本号,在每次启动 app 时,读取数据表的当前版本号;
  • 根据数据表的当前版本号判断是否需要迁移,如果需要,则使用数据库事务执行以下操作:
    • 将数据表的名称改为 temp;
    • 创建一个新的数据表,其名称与旧数据表的原始名称一致;
    • 把旧数据表中的数据插入到新数据表中;
    • 删除旧数据表;
    • 如果以上其中一步执行失败,则回滚事务;
  • 如果数据库迁移成功了,则更新本地保存的数据表的当前版本号;

什么是事务

事务是用户定义的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。

事务的特性

  • 原子性:一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
  • 一致性:事务必须是使数据库从一个一致性状态变到另一个一致性状态,一致性与原子性是密切相关的。
  • 隔离性:一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
  • 持久性:一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

讲讲项目中遇到的难题以及是如何解决这些难题的?

导航栏背景透明以及导航栏背景动态切换

设置某个页面的导航栏背景的透明度,并且在跳转新页面时将导航栏背景设置为不透明。以及页面之间跳转时,改变导航栏的背景颜色或者背景图片。由于 UIKit 框架并没有提供 API 来设置导航栏背景的透明度,并且使用官方 API 切换导航栏背景时,视觉体验不太好,所以需要自行实现导航栏背景透明和动态切换背景效果。

使用 xcode 运行应用程序并查看UINavigationBar视图层次结构,发现UINavigationBar的背景是由其最底层的子视图_UIBarBackground控制的,设置_UIBarBackground的透明度就可以改变导航栏的透明度。

查阅UIViewController的官方文档得知,在视图控制器执行 push 或 pop 转场动画时,UIKit 会为视图控制器创建一个转场动画协调器transitionCoordinator,可以使用转场动画协调器提交其他动画与 push 或 pop动画一起同时执行。我们可以将_UIBarBackground设置为完全透明,并在_UIBarBackground添加一个背景子视图,通过设置这个背景子视图的透明度和背景来控制导航栏的透明度和背景。在每次执行 push 或者 pop 转场动画时,向_UIBarBackground添加一个临时视图,并将临时视图的透明度和背景设置为跳转界面的导航栏的透明度和背景,然后让临时视图跟随视图控制器一起 push 或 pop 到界面中。最后,更新背景子视图的透明度和背景,并删除临时子视图,这样就能实现导航栏背景的动态切换效果。

根据以上思路,最终实现了一个UINavigationController的 category,在每次 push 和 pop 视图控制器时,自动添加导航栏背景切换的转场动画,使用者只需要在视图控制器的viewWillAppear:方法中调用相关 API 设置导航栏的透明度和背景即可。

高德2D地图展示车辆行驶轨迹的动画

网约车订单在执行过程中,乘客端需要在高德2D地图上动画显示汽车标注的行驶轨迹。而高德2D地图是没有提供这个功能的,所以需要自行实现。

CADisplayLink是一个与屏幕刷新率一致的定时器,每次刷新屏幕时,都会触发CADisplayLink。计算出汽车标注从一个坐标点动画移动到另一个坐标点的关键帧坐标点,然后在每次刷新屏幕时,将汽车标注的坐标设置为关键帧坐标点,这样人眼看到的效果就是汽车标注从一个坐标点动态移动到了另一个坐标点。

另外,汽车标注从一个坐标点动态移动到另一个坐标点之前,还需要先动画旋转汽车标注来调整汽车行驶的方向。汽车标注旋转角度的计算可以根据汽车标注的上一个坐标点、当前坐标点和下一个坐标点这三个坐标点构造两个向量,然后使用向量的夹角公式计算出角度(cosθ = 向量a与向量b的点积 / 向量a的模长 x 向量b的模长)。计算出角度后,还要知道是顺时针旋转还是逆时针旋转,这个可以通过向量a与向量b的叉积来判断。

由于乘客端每隔一段时间就会向服务端发送一个HTTP请求来获取司机端的轨迹数组,这就有可能出现之前请求的轨迹坐标点还没有动态更新完毕,又有新的轨迹坐标点传送过来了,因此需要队列执行汽车标注从一个坐标点移动到另一个坐标点的动画。可以自定义NSOperation来封装汽车标注从一个坐标点移动到另一个坐标点的动画,这需要将CADisplayLink添加到主线程的runloop中,所以这个操作是一个异步操作,因而需要我们自行去控制操作是否已经执行完毕。根据坐标点创建一个自定义的NSOperation对象后,将其添加到最大并发数为1NSOperationQueue中。

应用程序上架被拒绝的原因有哪些?

2.1 App 完成度

主要有应用出现崩溃、加载失败等非常明显的Bug、应用不支持 IPv6网络下使用、测试账号、隐藏开关等。

解决方法:提前测试产品是否有bug、在IPV6网络下是否能使用等,根据反馈邮件,一个个审查自身产品信息是否符合,适当情况下可以发送截图视频给苹果官方以证明自己的清白。

2.3 准确的元数据

主要是应用标题、描述、截图等与应用功能严重不符。如用安卓手机截图,浏览器截图。

解决方法:重新更换截图,保证整个APP功能、流程看起来是一致的。去除隐藏功能模块代码或将需要隐藏功能的代码及定向跳转链接网址做混淆处理,适当增加逻辑复杂度。

2.5 软件要求

主要是产品加入违规代码。

解决方法:很可能是三方库中含有SDK,可以更新所有三方库,或者反编译提交的ipa,检查文档中是否有违规字符串,有的话删掉。

3.1.1 购买项目

主要是接入第三方支付,支付宝、微信等。

解决方法:老老实实地走苹果支付的支付方式,用内购。如果隐藏虚拟产品或者通过后更改支付方式,都是有一定风险的。

3.2.1 可接受的商业模式

主要是没有资质。

解决方法:最佳方案是拿到资质,如果实在没有资质,建议大家尽可能多的把自己公司合规的证据资料发给苹果,而套壳、换新账号碰运气上架等操作,不得已的话可以尝试。

4.2 最低功能要求

主要问题在于苹果认为部分开发者上传的App功能不够,或者没有自己的核心功能,比如直接打包一个网页上架的很容易触发这个问题。

解决办法:可以添加一些功能丰富产品(导航栏,下拉刷新,推送通知等功能),如果觉得功能已经全了,还没有通过审核,可以向苹果解释产品解决的用户需求,以及具体功能的展现。

4.3 重复 App

主要针对的是重复App,就是马甲包。

解决办法:可通过修改名字、icon、主色调、代码等解决,并且注意相同的马甲包提交至少间隔一天以上。

5.1.1 数据收集和存储

主要是App强制用户注册,且基于不需要用户信息的功能之上、暗中采集/共享用户的个人信息。

解决方法:先与用户协商,让用户同意后注册,有“强登陆”功能的一定要修改为提示登陆的版本。

5.1.5 定位服务

主要是 App 未得到允许,与第三方共享收集的用户数据,且并未说明使用目的等,例:位置、账号……

解决方法:如果要采取用户数据信息,需要给予用户提示,并得到用户的允许,或设置为可选,并且明确告知苹果采集用户数据信息的使用目的。总的来说就是要弹出提示说明使用这个权限做什么用,写清楚。

5.2 知识产权

主要是未经授权,使用受版权保护的第三方材料、App不得与苹果现有产品类似等。

解决方法:确保 app 只包含由您创建或拥有使用许可的内容,提交产品时使用受版权保护的第三方的书面证据或者将产品中包含的未经第三方授权的部分隐藏。

自我介绍

我叫xxx,今年xx岁,xx省xx市人,20xx年毕业,从事 iOS 开发至今已有 x 年。在上家公司的时候,独立负责公司 iOS 端应用程序的开发和维护,有比较丰富的项目开发经验,我相信自己能够胜任贵公司的这一职位。非常感谢贵公司给与我的这次面试机会,希望以后能为贵公司贡献自己的价值。

职业规划

未来1-3年,打算继续在iOS开发技术上沉淀,同时提升自己的项目管理能力和团队协作能力,争取为公司贡献自己的最大价值。

离职原因

从上家公司的工作中,我学到了很多东西,但是因为上家公司的业务发展比较局限,与我的职业规划有些偏差,所以打算换个平台继续努力。

你可能感兴趣的:(iOS (四))