iOS开发面试整理之小知识篇(一)

  1. 当系统出现内存警告时会发生什么?
    1. 会将不在当前窗口上的view暂时移除
    2. 如果放任内存警告,最终会导致软件强制被系统关闭
  2. @property与@synthesize的本质是什么?
    1. 都是编译器特性
    2. Xcode4.4之前:
      1. 自动生成get方法与set方法的声明
      2. @synthesize age;:默认会访问age这个成员变量,如果没有age,就会自动生成@private类型的age变量
      3. @synthesize age = _age;: 自动生成age的setter和getter实现,并且会访问_age这个成员变量,如果不存在,就会自动生成@private类型的_age成员变量
    3. Xcode4.4之后
      1. 生成get/set方法的声明
      2. 生成带下划线的成员变量_age(注意这变量是@private)
      3. 生成get/set方法的实现
  3. 桥接(Toll-Free Bridging)是什么?什么情况下会使用(注意:这里ARC下需要管理内存)?
    1. 用于在Foundation对象与Core Foundation对象之间交换数据,俗称桥接
    2. 在ARC环境下,Foundation对象转成 Core Foundation对象
      1. 使用__bridge桥接以后ARC会自动管理2个对象
      2. 使用__bridge_retained桥接需要手动释放Core Foundation对象
    3. 在ARC环境下, Core Foundation对象转成 Foundation对象
      1. 使用__bridge桥接,如果Core Foundation对象被释放,Foundation对象也同时不能使用了,需要手动管理Core Foundation对象
      2. 使用__bridge_transfer桥接,系统会自动管理2个对象
  4. Push Notification 是如何工作的?
    1. 推送通知分为两种,一个是本地推送,一个是远程推送
      1. 本地推送:不需要联网也可以推送,是开发人员在APP内设定特定的时间来提醒用户干什么
      2. 远程推送:需要联网,用户的设备会与苹果APNS服务器形成一个长连接,用户设备会发送uuid和Bundle idenidentifier给苹果服务器,苹果服务器会加密生成一个deviceToken给用户设备,然后设备会将deviceToken发送给APP的服务器,服务器会将deviceToken存进他们的数据库,这时候如果有人发送消息给我,服务器端就会去查询我的deviceToken,然后将deviceToken和要发送的信息发送给苹果服务器,苹果服务器通过deviceToken找到我的设备并将消息推送到我的设备上,这里还有个情况是如果APP在线,那么APP服务器会于APP产生一个长连接,这时候APP服务器会直接通过deviceToken将消息推送到设备上
  5. 什么是 Protocol,Delegate 一般是怎么用的?
    1. 协议是一个方法签名的列表,在其中可以定义若干个方法,遵守该协议的类可以实现协议里的方法,在协议中使用@property只会生成setter和getter方法的声明
    2. delegate用法:成为一个类的代理,可以去实现协议里的方法,delegate用weak修饰,防止循环引用
  6. 使用 Block 时需要注意哪些问题?
    1. block引发循环引用的因素:
      1. 一个对象拥有一个block属性,然而这个block内部却又直接或间接使用了这个对象或者对象的属性.
    2. block的循环引用分为2种:
      1. 第一种: self(控制器)有一个block属性,而在block中却又使用了self的属性
        1. 解决办法: __weak typeof(self) weakSelf = self;
      2. 第二种: 如果不是self呢? 是一个P对象有一个block属性,而在block中却又使用了P的属性呢?
        1. 解决办法: 创建这个对象时用__block修饰,此时的作用是当block内部使用了自己时,不让block对自己retain(计数器加1)
        2. 代码举例:

           //如果对象中的block又用到了对象自己, 那么为了避免内存泄露, 应该将对象修饰为__block
           __block Person *p = [[Person alloc] init]; // 1
           p.name = @"lnj";
           NSLog(@"retainCount = %lu", [p retainCount]);
           p.pBlock = ^{
               NSLog(@"name = %@", p.name); // 1
           };
           NSLog(@"retainCount = %lu", [p retainCount]);
           p.pBlock();
                          
           [p release]; // 0
           //    [p release]; // 2B
          
    3. 在block内部如果调用了延时函数还使用弱指针会取不到该指针,因为已经被销毁了,需要在block内部再将弱指针重新强引用一下:__strong typeof(self) strongSelf = weakSelf;
    4. 如果需要在block内部改变外部变量的话,需要在用__block修饰外部变量
  7. performSelector:withObject:afterDelay: 内部大概是怎么实现的,有什么注意事项么?
    1. 创建一个定时器,时间结束后系统会使用runtime通过方法名称(Selector本质就是方法名称)去方法列表中找到对应的方法实现并调用方法
    2. 注意事项
      1. 调用performSelector:withObject:afterDelay:方法时,先判断希望调用的方法是否存在respondsToSelector:
      2. 这个方法是异步方法,必须在主线程调用,在子线程调用永远不会调用到想调用的方法(除非子线程手动开启runloop)
        1. 当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。(子线程需要手动开启Runloop,子线程没有runloop执行完会立刻释放线程)
        2. 当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
  8. 有哪些常见的 Crash 场景?
    1. 访问了僵尸对象
    2. 访问了不存在的方法
    3. 数组越界
    4. 在定时器下一次回调前将定时器释放,会Crash
    5. block没有实现
    6. 字典/数组..等集合中存入了nil对象
    7. KVO监听没有删除
  9. 你一般是怎么用 Instruments 的?
    1. Time Profiler:性能分析
    2. Zombies:检查是否访问了僵尸对象,但是这个工具只能从上往下检查,不智能
    3. Allocations:用来检查内存,写算法的那批人也用这个来检查
    4. Leaks:检查内存,看是否有内存泄露
  10. UIView 和 CALayer 之间的关系?
    1. UIView显示在屏幕上归功于CALayer,通过调用drawRect方法来渲染自身的内容,调节CALayer属性可以调整UIView的外观,UIView继承自UIResponder,CALayer不可以响应用户事件
    2. UIView是iOS系统中界面元素的基础,所有的界面元素都继承自它。它内部是由Core Animation来实现的,它真正的绘图部分,是由一个叫CALayer(Core Animation Layer)的类来管理。UIView本身,更像是一个CALayer的管理器,访问它的根绘图和坐标有关的属性,如frame,bounds等,实际上内部都是访问它所在CALayer的相关属性
    3. UIView有个layer属性,可以返回它的主CALayer实例,UIView有一个layerClass方法,返回主layer所使用的类,UIView的子类,可以通过重载这个方法,来让UIView使用不同的CALayer来显示
  11. @property (nonatomic,copy) NSMutableString *name;这句话合适吗?
    1. 不合适,因为属性name的真实类型是不可变的即NSString类型
    2. 该类的set方法为:

       -(void)setName:(NSMutableString *)name{
        _name = [name copy];
        }
      
    3. 由于copy方法返回的是一个不可变对象,然而_name却是NSMutableString类型修饰!!!
    4. 但是如果用strong修饰没有问题,nameNSMutableString
  12. 哪些地方用到copy? 为什么?
    1. NSString用到
      1. 对象的属性不受外边的影响,具有保护对象属性值的作用.
      2. 举例:

         @property (nonatomic, strong) NSString *nameStrong;    // 用strong修饰
         @property (nonatomic, copy) NSString *nameCopy;      // 用copy修饰
         @property (nonatomic, copy) NSString *normalName;    // 原字符串-不可变
         @property (nonatomic, strong) NSMutableString  *mutableName;   // 原字符串-可变
         //场景1:
         self.normalName    = @"1111";
         self.nameStrong    = self.normalName;
         self.nameCopy      = self.normalName;
         //修改
         self.normalName    = @"2222";
         //结果,self.nameStrong、self.nameCopy仍然为:1111,不变
                    
         //场景2:
         self.mutableName     = [NSMutableString stringWithString:@"1111"];
         self.nameStrong      = self.mutableName;
         self.nameCopy        = self.mutableName;
         //修改
         self.mutableName    = [NSMutableString stringWithString:@"222"];
         //结果,self.nameStrong、self.nameCopy仍然为:1111,不变
                    
         //场景3:
         self.mutableName     = [NSMutableString stringWithString:@"1111"];
         self.nameStrong      = self.mutableName;
         self.nameCopy        = self.mutableName;
         //修改
         self.mutableName    =  [self.mutableName appendString:@"aaaa"];
         //结果,self.nameStrong 为1111aaaa ; self.nameCopy为1111,改变变
        
        1. 从上面来看,只有场景3,实现了外部修改,内部改变
        2. 从内存来分析:
          1. 场景1,场景2,从外部变量修改前到修改后,nameStrong、nameCopy指向的内存数据没有发生改变
          2. 场景3,从外部变量修改前到修改后,是在原来指向的内存修改了数据,因此nameStrong指向也会跟着修改。然而nameCopy 做了深拷贝,指向的是新的内存区,所以原来的内存区改变,不影响nameCopy
    2. block用到了copy
      1. copy的作用不是复制,而是将这个block从栈转移到堆中.
      2. 栈中的block不会对其内部使用的对象自动计数器+1,但是堆中的block会.
      3. 防止外界对象(block内部使用外界的对象)提前销毁,出现野指针错误.
    3. block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。
  13. HTTP/HTTPS/TCP/UDP/SOCKET等知识区别,看iOS开发网络篇系列
    1. HTTP是超文本传输协议,是基于TCP传输方式的应用层协议
      1. http1.1之前是短连接,每次请求之后就会断开,多次请求都需要3次握手,耗时。
      2. http 1.1 之后可以设置为长连接,提高效率。
    2. HTTPs是在HTTP与TCP之间加了一层SSL层,用于加密传输的内容,采用混合加密的方式
      1. HTTPS在真正发送数据之前需要先安装服务器返回的CA证书,安装之后才会正式传输数据,有些网址提示安装,有些不提示(默认强制安装)
    3. TCP与UDP
      1. TCP是传输层协议,安全,耗时。需要三次握手,四次挥手。
      2. UDP也是传输层协议,不安全,快速。不需要三次握手、四次挥手。
    4. socket层只是在TCP/UDP传输层上做的一个抽象接口层,因此一个socket连接可以基于TCP,也有可能基于UDP。基于TCP协议的socket连接同样需要通过三次握手建立连接,是可靠的;基于UDP协议的socket连接不需要建立连接的过程,不过对方能不能收到都会发送过去,是不可靠的,大多数的即时通讯IM都是后者。
    5. 什么时候该用HTTP,什么时候该用socket?
      1. HTTP是短连接,Socket(基于TCP协议的)是长连接。尽管HTTP1.1开始支持持久连接,但仍无法保证始终连接。而Socket连接一旦建立TCP三次握手,除非一方主动断开,否则连接状态一直保持。
      2. HTTP连接服务端无法主动发消息,Socket连接双方都可以主动发送消息
      3. HTTP使用场合:双方不需要时刻保持连接在线,比如客户端资源的获取、文件上传等
      4. socket使用场合:大部分即时通讯应用(QQ、微信)、聊天室、苹果APNs等
  14. BAD_ACCESS在什么情况下出现?
    1. 这种问题在开发时经常遇到。原因是访问了野指针,比如访问已经释放对象的成员变量或者发消息、死循环等。
  15. lldb(gdb)常用的控制台调试命令?
    1. LLDB与GDB是什么?
      1. Xcode里有内置的Debugger(调试器),老版使用的是GDB,Xcode自4.3之后默认使用的就是LLDB了。
      2. 设备上自带的有debugserver,专门用于对应LLDB的调试
      3. GDB: UNIX及UNIX-like下的调试工具。
      4. LLDB: 开源的内置于XCode的具有REPL(read-eval-print-loop)特征的Debugger,其可以安装C++或者Python插件。
      5. 两个都是调试用的Debugger,只是LLDB是比较高级的版本,或者在调试开发iOS应用时比较好用
      6. 就是打印窗口
      7. 调试过程
        1. Xcode调试窗口通过输入LLDB指令,给设备的debugserver
        2. debugserver接受到这个指令后执行到相应的APP上
        3. APP执行调试指令然后将执行的反馈信息返回给debugserver
        4. debugserver将这个信息反馈给LLDB
        5. LLDB将这个返回的信息,打印出来
    2. 如何使用?
      1. 打局部断点,然后就进入调试器了.
    3. 一些Xcode调试快捷键:
      1. command+shift+Y: 打开/关闭调试窗口
      2. command+Y 调试运行程序(取消/开始所有断点)
    4. 常用命令:
      1. p : 输出基本类型。是打印命令,需要指定类型。是print的简写
      2. po : 打印对象,会调用对象description方法。是print-object的简写
      3. expr : 可以在调试时动态执行指定表达式,并将结果打印出来。常用于在调试过程中修改变量的值。
      4. bt : 打印调用堆栈,是thread backtrace的简写,加all可打印所有thread的堆栈
      5. br l : 是breakpoint list的简写
  16. iOS中你都用到了那些调试方法?
    1. NSLog打印调试
    2. 打断点
      1. 局部断点:在指定的哪一行打断点,然后在用控制台调试(po命令)
      2. 全局断点: 定位到代码崩溃的地方,然后用控制台调试(可以设置只检查OC/swift/C++代码等)
    3. 视图调试也叫拖图层分析(Debug View Hierarchy),通过拖出来的层分析
    4. EXC_BAD_ACCESS: 打开僵尸对象监控调试
      1. Zombie模式不能再真机上使用,只能在模拟器上使用, 只能用在模拟器和OC语言。
    5. LLDB调试(上面已讲)
    6. instruments(上面已讲)
  17. block的内存管理
    1. 无论当前环境是ARC还是MRC,只要block没有访问外部变量,block始终在全局区
    2. MRC情况下
      1. block如果访问外部变量,block在栈里
      2. 不能对block使用retain,否则不能保存在堆里
      3. 只有使用copy,才能放到堆里
    3. ARC情况下
      1. block如果访问外部变量,block在堆里
      2. block可以使用copy和strong,并且block是一个对象
  18. iOS的本地存储以及沙盒结构:
    1. iOS的本地存储方式
      1. 偏好设置:NSUserDefaults

         //存储
         NSUserDefaults *login = [NSUserDefaults standardUserDefaults];
         [login setObject:self.passwordField.text forKey:@"token"];
         [login synchronize];
         //取
         NSUserDefaults *login = [NSUserDefaults standardUserDefaults];
         NSString *str = [login objectForKey:@"token"];
        
        1. 只能存储OC常用数据类型(NSString、NSDictionary、NSArray、NSData、NSNumber等类型)而不能直接存储自定义数据。
      2. 自定义归档:NSKeyedArchiver
        1. 自定义对象准守NSCoding协议,并实现协议方法

           //Person
           - (void)encodeWithCoder:(nonnull NSCoder *)aCoder {
               [aCoder encodeObject:self.name forKey:@"name"];
           }
           - (nullable instancetype)initWithCoder:(nonnull NSCoder *)aDecoder {
               if (self = [super init]) {
                   self.name = [aDecoder decodeObjectForKey:@"name"];
               }
               return self;
           }
                          
           //使用
           //归档
           //沙盒ducument目录
           NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
           //完整的文件路径
           NSString *path = [docPath stringByAppendingPathComponent:@"person.archive"];
           //将数据归档到path文件路径里面
          BOOL success = [NSKeyedArchiver archiveRootObject:person toFile:path];
              
           //解档
           //沙盒ducument目录
           NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
           //完整的文件路径
           NSString *path = [docPath stringByAppendingPathComponent:@"person.archive"];
                          
           Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
          
      3. plist存储

         NSString *arrayPath = [NSHomeDirectory() stringByAppendingString:@"/Desktop/arrayToPList.plist"];
         // 待写入数据
         NSArray *array = @[@"bei", @"jing", @"huan", @"ying", @"nin"];
         // 数组写入 plist 文件
         BOOL bl1 = [array writeToFile:arrayPath atomically:YES];
         //读取
         NSArray *arrayFromPlist = [NSArray arrayWithContentsOfFile:arrayPath];
        
      4. sqlite3数据库存储:
        1. 源生:#import <sqlite3.h>
        2. 第三方:FMDB/BGFMDB
      5. core data 没有使用
      6. 钥匙串存储:Keychain
        1. 存储到钥匙串中
        2. 可以多个APP共享、删除APP数据仍然存在
        3. 相对安全,越狱设备可以拿到
    2. iOS的沙盒结构
      1. Application
        1. 存放程序源文件,上架前经过数字签名,上架后不可修改。
      2. Documents:
        1. 常用目录,iCloud备份目录,存放数据。(这里不能存缓存文件,否则上架不被通过)
      3. Library:
        1. Caches:存放体积大又不需要备份的数据。(常用的缓存路径)
        2. Preference:设置目录,iCloud会备份设置信息。
      4. tmp:存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除的可能。
  19. iOS多线程技术有哪几种方式?
    1. pthread、NSThread、GCD、NSOperation
  20. GCD 与 NSOperation 的区别:
    1. GCD 和 NSOperation 都是用于实现多线程:
    2. GCD 基于C语言的底层API,GCD主要与block结合使用,代码简洁高效。
    3. NSOperation 属于Objective-C类,是基于GCD更高一层的封装。复杂任务一般用NSOperation实现。
  21. 线程间通信
    1. 写出使用GCD方式从子线程回到主线程的方法代码

       dispatch_sync(dispatch_get_main_queue(), ^{ });
      
  22. dispatch_barrier_async(栅栏函数)的作用是什么?
    1. 函数定义:dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
    2. 作用:
      1. 在它前面的任务执行结束后它才执行,它后面的任务要等它执行完成后才会开始执行。
  23. 描述下SDWebImage里面给UIImageView加载图片的逻辑
    1. SDWebImage 中为 UIImageView 提供了一个分类UIImageView+WebCache.h, 这个分类中有一个最常用的接口sd_setImageWithURL:placeholderImage:,会在真实图片出现前会先显示占位图片,当真实图片被加载出来后再替换占位图片。
    2. 加载图片的过程大致如下:
      1. 首先会在 SDWebImageCache 中寻找图片是否有对应的缓存, 它会以url 作为数据的索引先在内存中寻找是否有对应的缓存
      2. 如果缓存未找到就会利用通过MD5处理过的key来继续在磁盘中查询对应的数据, 如果找到了, 就会把磁盘中的数据加载到内存中,并将图片显示出来
      3. 如果在内存和磁盘缓存中都没有找到,就会向远程服务器发送请求,开始下载图片
      4. 下载后的图片会加入缓存中,并写入磁盘中
      5. .整个获取图片的过程都是在子线程中执行,获取到图片后回到主线程将图片显示出来
    3. SDWebImage原理:
      1. 从内存(字典)中找图片(当这个图片在本次使用程序的过程中已经被加载过),找到直接使用
      2. 从沙盒中找(当这个图片在之前使用程序的过程中被加载过),找到使用,缓存到内存中。
      3. 从网络上获取,使用,缓存到内存,缓存到沙盒。
  24. 请简单的介绍下APNS发送系统消息的机制
    1. 应用在通知中心注册,由iOS系统向APNS请求返回设备令牌(device Token)
    2. 应用程序接收到设备令牌并发送给自己的后台服务器
    3. 服务器把要推送的内容和设备发送给APNS
    4. APNS根据设备令牌找到设备,再由iOS根据APPID把推送内容展示
  25. 你是否接触过OC中的反射机制?简单聊一下概念和使用
    1. class反射
      1. 通过类名的字符串形式实例化对象。

         Class class = NSClassFromString(@"student");
        
         Student *stu = [[class alloc] init];
        
      2. 将类名变为字符串。

         Class class =[Student class];
        
         NSString *className = NSStringFromClass(class);
        
    2. SEL反射
      1. 通过方法的字符串形式实例化方法。

         SEL selector = NSSelectorFromString(@"setName");
        
         [stu performSelector:selector withObject:@"Mike"];
        
      2. 将方法变成字符串。

         NSStringFromSelector(@selector*(setName:));
        
  26. ViewController生命周期
    1. initWithCoder:通过nib文件初始化时触发。
    2. awakeFromNib:nib文件被加载的时候,会发生一个awakeFromNib的消息到nib文件中的每个对象。
    3. loadView:开始加载视图控制器自带的view。
    4. viewDidLoad:视图控制器的view被加载完成。
    5. viewWillAppear:视图控制器的view将要显示在window上。
    6. updateViewConstraints:视图控制器的view开始更新AutoLayout约束。
    7. viewWillLayoutSubviews:视图控制器的view将要更新内容视图的位置。
    8. viewDidLayoutSubviews:视图控制器的view已经更新视图的位置。
    9. viewDidAppear:视图控制器的view已经展示到window上。
    10. viewWillDisappear:视图控制器的view将要从window上消失。
    11. viewDidDisappear:视图控制器的view已经从window上消失。
  27. KVC的底层实现?
    1. 当一个对象调用setValue方法时,方法内部会做以下操作:
      1. 检查是否存在相应的key的set方法,如果存在,就调用set方法。
      2. 如果set方法不存在,就会查找与key相同名称并且带下划线的成员变量,如果有,则直接给成员变量属性赋值
      3. 如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值。
      4. 如果还没有找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法,这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。
  28. 写一个 setter 方法用于完成 @property (nonatomic, retain) NSString*name,写一个 setter 方法用于完成 @property (nonatomic, copy) NSString *name
    1. retain

         - (void)setCar:(Car *)car
         {
            // 1.先判断是不是新传进来对象
            if ( car != _car )
            {
                // 2.对旧对象做一次release
                [_car release];
                       
                // 3.对新对象做一次retain
                _car = [car retain];
            }
         }
      
    2. copy

         - (void)setCar:(Car *)car
         {
            // 1.先判断是不是新传进来对象
            if ( car != _car )
            {
                // 2.对旧对象做一次release
                [_car release];
                       
                // 3.对新对象做一次retain
                _car = [car copy];
            }
         }
      
  29. 谈谈汇编
    1. 常见的汇编有:16位汇编,win32汇编、win64汇编、arm汇编、A&T汇编
    2. 汇编的核心知识(win32)
      1. 通常计算结果放在eax寄存器
      2. 函数的帧栈平衡的方式
        1. 外平栈
        2. 内平栈
      3. win32汇编的常识

         EBP 原EBP的内容
         EBP+4 函数返回地址
         EBP+8 参数区
         EBP-4 局部变量区域,即函数缓冲区
         EAX寄存器用于存放计算结果
        
      4. 堆栈段分配内存是从高地址向低地址分配,堆栈未使用前指向栈顶-最高地址处。
    3. windwos的破解、外挂
      1. 破解:通过IDA、OD、DT、CE等工具,修改掉PE可执行文件的二进制码。
      2. 外挂:使用MFC编写C++程序,通过windows api 监听应用进程,修改原程序的操作。
  30. 谈谈BlE开发
    1. 使用第三方SDK
      1. SDK将数据解析部分封装好,接入方实现SDK代理方法,可查看读取硬件结果
    2. 自己开发
      1. 条件
        1. 蓝牙传输协议
          1. 显示硬件连接、读、写的特征值
          2. 用于向硬件发送的命令
        2. 数据解析协议
          1. 返回数据为二进制(16进制),根据数据解析协议通过位运算等方法解析成我们想要的数据。
      2. 采用苹果自带CoreBluetooth框架
        1. 实现连接、读、写、监听硬件的功能
      3. 数据解析的两种方式
        1. 本地自己编写算法,缺点:iOS和安卓都要编写
        2. 后台编写算法,优点:只需要写一套算法,缺点:必须网络请求对网络依赖
Table of Contents