IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> iOS开发面试攻略(KVO、KVC、多线程、锁、runloop、计时器) -> 正文阅读

[移动开发]iOS开发面试攻略(KVO、KVC、多线程、锁、runloop、计时器)

KVO & KVC

KVO用法和底层原理

  • 使用方法:添加观察者,然后怎样实现监听的代理
  • KVO底层使用了 isa-swizling的技术.
  • OC中每个对象/类都有isa指针, isa 表示这个对象是哪个类的对象.
  • 当给对象的某个属性注册了一个 observer,系统会创建一个新的中间类(intermediate class)继承原来的class,把该对象的isa指针指向中间类。
  • 然后中间类会重写setter方法,调用setter之前调用willChangeValueForKey, 调用setter之后调用didChangeValueForKey,以此通知所有观察者值发生更改。
  • 重写了 -class 方法,企图欺骗我们这个类没有变,就是原本那个类。

KVO的优缺点

  • 优点
    • 1、可以方便快捷的实现两个对象的关联同步,例如view & model
    • 2、能够观察到新值和旧值的变化
    • 3、可以方便的观察到嵌套类型的数据变化
  • 缺点
    • 1、观察对象通过string类型设置,如果写错或者变量名改变,编译时可以通过但是运行时会发生crash
    • 2、观察多个值需要在代理方法中多个if判断
    • 3、忘记移除观察者或重复移除观察者会导致crash

怎么手动触发KVO

  • KVO机制是通过willChangeValueForKey:didChangeValueForKey:两个方法触发的。
  • 在观察对象变化前调用willChangeValueForKey:
  • 在观察对象变化后调用didChangeValueForKey:
  • 所以只需要在变更观察值前后手动调用即可。

给KVO添加筛选条件

  • 重写automaticallyNotifiesObserversForKey,需要筛选的key返回NO
  • setter里添加判断后手动触发KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
 ?  if ([key isEqualToString:@"age"]) {
 ? ? ?  return NO;
 ?  }
 ?  return [super automaticallyNotifiesObserversForKey:key];
}
?
- (void)setAge:(NSInteger)age {
 ?  if (age >= 18) {
 ? ? ?  [self willChangeValueForKey:@"age"];
 ? ? ?  _age = age;
 ? ? ?  [self didChangeValueForKey:@"age"];
 ?  }else {
 ? ? ?  _age = age;
 ?  }
}

使用KVC修改会触发KVO吗?

  • 会,只要accessInstanceVariablesDirectly返回YES,通过KVC修改成员变量的值会触发KVO。
  • 这说明KVC内部调用了willChangeValueForKey:方法和didChangeValueForKey:方法

直接修改成员变量会触发KVO吗?

  • 不会

KVO的崩溃与防护

崩溃原因:

  • KVO 添加次数和移除次数不匹配,大部分是移除多于注册。
  • 被观察者dealloc时仍然注册着 KVO,导致崩溃。
  • 添加了观察者,但未实现 observeValueForKeyPath:ofObject:change:context: 。 防护方案1:
  • 直接使用facebook开源框架KVOController 防护方案2:
  • 自定义一个哈希表,记录观察者和观察对象的关系。
  • 使用fishhook替换 addObserver:forKeyPath:options:context:,在添加前先判断是否已经存在相同观察者,不存在才添加,避免重复触发造成bug。
  • 使用fishhook替换removeObserver:forKeyPath:removeObserver:forKeyPath:context,移除之前判断是否存在对应关系,如果存在才释放。
  • 使用fishhook替换dealloc,执行dealloc前判断是否存在未移除的观察者,存在的话先移除。

KVC底层原理

setValue:forKey:的实现

  • 查找setKey:方法和_setKey:方法,只要找到就直接传递参数,调用方法;
  • 如果没有找到setKey:_setKey:方法,查看accessInstanceVariablesDirectly方法的返回值,如果返回NO(不允许直接访问成员变量),调用setValue:forUndefineKey:并抛出异常NSUnknownKeyException
  • 如果accessInstanceVariablesDirectly方法返回YES(可以访问其成员变量),就按照顺序依次查找 _key、_isKey、key、isKey 这四个成员变量,如果查找到了就直接赋值;如果没有查到,调用setValue:forUndefineKey:并抛出异常NSUnknownKeyException

valueForKey:的实现

  • 按照getKey,key,isKey的顺序查找方法,只要找到就直接调用;
  • 如果没有找到,accessInstanceVariablesDirectly返回YES(可以访问其成员变量),按照顺序依次查找_key、_isKey、key、isKey 这四个成员变量,找到就取值;如果没有找到成员变量,调用valueforUndefineKey并抛出异常NSUnknownKeyException
  • accessInstanceVariablesDirectly返回NO(不允许直接访问成员变量),那么会调用valueforUndefineKey:方法,并抛出异常NSUnknownKeyException

多线程

进程和线程的区别

  • 进程:进程是指在系统中正在运行的一个应用程序,一个进程拥有多个线程。
  • 线程:线程是进程中的一个单位,一个进程想要执行任务, 必须至少有一条线程。应程序启动默认开启主线程。

进程都有什么状态

  • Not Running:未运行。
  • Inactive:前台非活动状态。处于前台,但是不能接受事件处理。
  • Active:前台活动状态。处于前台,能接受事件处理。
  • Background:后台状态。进入后台,如果又可执行代码,会执行代码,代码执行完毕,程序进行挂起。
  • Suspended:挂起状态。进入后台,不能执行代码,如果内存不足,程序会被杀死。

什么是线程安全?

  • 多条线程同时访问一段代码,不会造成数据混乱的情况

怎样保证线程安全?

  • 通过线程加锁
  • pthread_mutex 互斥锁(C语言)
  • @synchronized
  • NSLock 对象锁
  • NSRecursiveLock 递归锁
  • NSCondition & NSConditionLock 条件锁
  • dispatch_semaphore GCD信号量实现加锁
  • OSSpinLock自旋锁(不建议使用)
  • os_unfair_lock自旋锁(IOS10以后替代OSSpinLock

你接触到的项目,哪些场景运用到了线程安全?

  • 在线列表的增员和减员,需要加锁保持其线程安全。

iOS开发中有多少类型的线程?分别说说

  • 1、pthread
    • C语言实现的跨平台通用的多线程API
    • 使用难度大,没有用过
  • 2、NSThread
    • OC面向对象的多线程API
    • 简单易用,可以直接操作线程对象。
    • 需要手动管理生命周期
  • 3、GCD
    • C语言实现的多核并行CPU方案,能更合理的运行多核CPU
    • 可以自动管理生命周期
  • 4、NSOperation
    • OC基于GCD的封装
    • 完全面向对象的多线程方案
    • 可以自动管理生命周期

GCD有什么队列,默认提供了哪些队列

  • 串行同步队列,任务按顺序(串行),在当前线程执行(同步)
  • 串行异步队列,任务按顺序(串行),开辟新的线程执行(异步)
  • 并行同步队列,任务按顺序(无法体现并行),在当前线程执行(同步)
  • 并行异步队列,任务同时执行(并行),开辟新的线程执行(异步)
  • 默认提供了主队列和全局队列

GCD主线程 & 主队列的关系

  • 主队列任务只在主线程中被执行的
  • 主线程运行的是一个 runloop,除了主队列的任务,还有 UI 处理和绘制任务。

描述一下线程同步与异步的区别?

  • 线程同步是指当前有多个线程的话,必须等一个线程执行完了才能执行下一个线程。
  • 线程异步指一个线程去执行,他的下一个线程不用等待他执行完就开始执行。

线程同步的方式

  • GCD的串行队列,任务都一个个按顺序执行
  • NSOperationQueue设置maxConcurrentOperationCount = 1,同一时刻只有1个NSOperation被执行
  • 使用dispatch_semaphore信号量阻塞线程,直到任务完成再放行
  • dispatch_group也可以阻塞到所有任务完成才放行

什么情况下会线程死锁

  • 串行队列,正在进行的任务A向串行队列添加一个同步任务B,会造成AB两个任务互相等待,形成死锁。
  • 优先级反转,OSSpinlock

dispatch_once实现原理

  • dispatch_once需要传入dispatch_once_t类型的参数,其实是个长整形
  • 处理block前会判断传入的dispatch_once_t是否为0,为0表示block 尚未执行。
  • 执行后把token的值改为1,下次再进来的时候判断非0直接不处理了。

performSelectorrunloop的关系

  • 调用 performSelecter:afterDelay: ,其内部会创建一个Timer并添加到当前线程的RunLoop
  • 如果当前线程Runloop没有跑起来,这个方法会失效。
  • 其他的performSelector系列方法是类似的

子线程执行 [p performSelector:@selector(func) withObject:nil afterDelay:4] 会发生什么?

  • 上面这个方法放在子线程,其实内部会创建一个NSTimer定时器。

  • 子线程不会默认开启runloop,如果需要执行func函数得手动开启runloop

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
     ?  dispatch_async(queue, ^{
     ? ? ?  // [[NSRunLoop currentRunLoop] run]; 放在上面无效
     ? ? ?  // 只开启runloop但是里面没有任何事件,开启失败
     ? ? ?  [self performSelector:@selector(test) withObject:nil afterDelay:2];
     ? ? ?  [[NSRunLoop currentRunLoop] run];
    });
    
    

为什么只在主线程刷新UI

  • UIKit是线程不安全的,UI操作涉及到渲染和访问View的属性,异步操作会存在读写问题,为其加锁则会耗费大量资源并拖慢运行速度。
  • 程序的起点UIApplication在主线程初始化,所有的用户事件(触摸交互)都在主线程传递,所以view只能在主线程上才能对事件进行响应。

一个队列负责插入数据操作,一个队列负责读取操作,同时操作一个存储的队列,如何保证顺利进行

  • 使用GCD栅栏函数实现多度单写
  • 读取的时候使用 dispatch_sync 立刻返回数据
  • 写入的时候使用 dispatch_barrier_async 阻塞其他操作后写入
  • 注意尽量不要使用全局队列,因为全局队列里还有其他操作

为什么需要锁?

  • 多线程编程中会出现线程相互干扰的情况,如多个线程访问一个资源。
  • 需要一些同步工具,确保当线程交互的时候是安全的。

什么是互斥锁

  • 如果共享数据已经有了其他线程加锁了,线程会进行休眠状态等待锁
  • 一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
  • 任务复杂的时间长的情况建议使用互斥锁
  • 优点
    • 获取不到资源时线程休眠,cpu可以调度其他的线程工作
  • 缺点
    • 存在线程调度的开销
    • 如果任务时间很短,线程调度降低了cpu的效率

什么是自旋锁

  • 如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁
  • 一旦被访问的资源被解锁,则等待资源的线程会立即执行
  • 适用于持有锁较短时间
  • 优点:
    • 自旋锁不会引起线程休眠,不会进行线程调度和CPU时间片轮转等耗时操作。
    • 如果能在很短的时间内获得锁,自旋锁效率远高于互斥锁。
  • 缺点:
    • 自旋锁一直占用CPU,未获得锁的情况下处于忙等状态。
    • 如果不能在很短的时间内获得锁,使CPU效率降低。
    • 自旋锁不能实现递归调用。

读写锁

  • 读写锁又被称为 rw锁或者 readwrite锁
  • 不是最常用的,一般是数据库操作才会用到。
  • 具体操作为多读单写,写入操作只能串行执行,且写入时不能读取;读取需支持多线程操作,且读取时不能写入

说说你知道的锁有哪些

  • pthread_mutex 互斥锁(C语言)
  • @synchronized
  • NSLock 对象锁
  • NSRecursiveLock 递归锁
  • NSCondition & NSConditionLock 条件锁
  • dispatch_semaphore GCD信号量实现加锁
  • OSSpinLock自旋锁(暂不建议使用)
  • os_unfair_lock自旋锁(IOS10以后替代OSSpinLock

说说@synchronized

  • 原理
    • 内部应该是一个可重入互斥锁(recursive_mutex_t
    • 底层是链表,存储SyncData,SyncData里面包含一个 threadCount,就是访问资源的线程数量。
    • objc_sync_enter(obj),objc_sync_exit(obj),通过obj的地址作为hash传参查找SyncData,上锁解锁。
    • 传入的obj被释放或为nil,会执行锁的释放
  • 优点
    • 不需要创建锁对象也能实现锁的功能
    • 使用简单方便,代码可读性强
  • 缺点
    • 加锁的代码尽量少
    • 性能没有那么好
    • 注意锁的对象必须是同一个OC对象

说说NSLock

  • 遵循NSLocking协议
  • 注意点
    • 同一线程lockunlock需要成对出现
    • 同一线程连续lock两次会造成死锁

说说NSRecursiveLock

  • NSRecursiveLock是递归锁
  • 注意点
    • 同一个程lock多次而不造成死锁
    • 同一线程当lock & unlock数量一致的时候才会释放锁,其他线程才能上锁

说说NSCondition & NSConditionLock

  • 条件锁:满足条件执行锁住的代码;不满足条件就阻塞线程,直到另一个线程发出解锁信号。
  • NSCondition对象实际上作为一个锁和一个线程检查器
    • 锁保护数据源,执行条件引发的任务。
    • 线程检查器根据条件判断是否阻塞线程。
    • 需要手动等待和手动信号解除等待
    • 一个wait必须对应一个signal,一次唤醒全部需要使用broadcast
  • NSConditionLockNSCondition的封装
    • 通过不同的condition值触发不同的操作
    • 解锁时通过unlockWithCondition 修改condition实现任务依赖
    • 通过condition自动判断阻塞还是唤醒线程

说说GCD信号量实现锁

  • dispatch_semaphore_creat(0)生成一个信号量semaphore = 0( 传入的值可以控制并行任务的数量)
  • dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) 使semaphore - 1,当值小于0进入等待
  • dispatch_semaphore_signal(semaphore)发出信号,使semaphore + 1,当值大于等于0放行

说说OSSpinLock

  • OSSpinLock是自旋锁,忙等锁

  • 自旋锁存在优先级反转的问题,线程有优先级的时候可能导致下列情况。

    • 一个优先级低的线程先访问某个数据,此时使用自旋锁进行了加锁。
    • 一个优先级高的线程又去访问这个数据,优先级高的线程会一直占着CPU资源忙等访问
    • 结果导致优先级低的线程没有CPU资源完成任务,也无法释放锁。
  • 由于自旋锁本身存在的问题,所以苹果已经废弃了OSSpinLock

说说 os_unfair_lock

  • iOS10以后替代OSSpinLock的锁,不再忙等
  • 获取不到资源时休眠,获取到资源时由内核唤醒线程
  • 没有加强公平性和顺序,释放锁的线程可能立即再次加锁,之前等待锁的线程唤醒后可能也没能加锁成功。
  • 虽然解决了优先级反转,但也造成了饥饿(starvation
  • starvation 指贪婪线程占用资源事件太长,其他线程无法访问共享资源。

5个线程读一个文件,如何实现最多只有2个线程同时读这个文件

  • dispatch_semaphore信号量控制

Objective-C中的原子和非原子属性

  • OC在定义属性时有nonatomicatomic两种选择
  • atomic:原子属性,为setter/getter方法都加锁(默认就是atomic),线程安全,需要消耗大量的资源
  • nonatomic:非原子属性,不加锁,非线程安全
atomic加锁原理:
property (assign, atomic) int age;
 - (void)setAge:(int)age
{ 
 ?  @synchronized(self) { ?
 ? ? ? _age = age;
 ?  }
}
?
- (int)age {
  int age1 = 0;
  @synchronized(self) {
 ?  age1 = _age;
  }
}

atomic 修饰的属性 int a,在不同的线程执行 self.a = self.a + 1 执行一万次,这个属性的值会是一万吗?

  • 不会,左边的点语法调用的是setter,右边调用的是getter,这行语句并不是原子性的。

atomic就一定能保证线程安全么?

  • 不能,只能保证settergetter在当前线程的安全
  • 一个线程在连续多次读取某条属性值的时候,别的线程同时在改值,最终无法得出期望值
  • 一个线程在获取当前属性的值, 另外一个线程把这个属性释放调了,有可能造成崩溃

nonatomic是非原子操作符,为什么用nonatomic不用atomic

  • 如果该对象无需考虑多线程的情况,请加入这个属性修饰,这样会让编译器少生成一些互斥加锁代码,可以提高效率。
  • 使用atomic,编译器会在settergetter方法里面自动生成互斥锁代码,避免该变量读写不同步。

有人说能atomic耗内存,你觉得呢?

  • 因为会自动加锁,所以性能比nonatomic差。

atomic为什么会失效

  • atomic修饰的属性靠编译器自动生成的get/set方法实现原子操作,如果重写了任意一个,atomic关键字的特性将失效

nonatomic实现

- (NSString *)userName {
 ?  return _userName;
}
?
- (void)setUserName:(NSString *)userName {
 ?  _userName = userName;
}

atomic 的实现

- (NSString *)userName {
 ?  NSString *name;
 ?  @synchronized (self) {
 ? ? ?  name = _userName;
 ?  }
 ?  return name;
}
?
- (void)setUserName:(NSString *)userName {
 ?  @synchronized (self) {
 ? ? ?  _userName = userName;
 ?  }
}

runloop

runloop是什么?

  • 系统内部存在管理事件的循环机制
  • runloop 是利用这个循环,管理消息和事件的对象。

runloop 是否等于 while(1) { do something ... }

  • 不是
  • while(1) 是一个忙等的状态,需要一直占用资源。
  • runloop 没有消息需要处理时进入休眠状态,消息来了,需要处理时才被唤醒。

runloop的基本模式

  • iOS中有五种runLoop模式
  • UIInitializationRunLoopMode(启动后进入的第一个Mode,启动完成后就不再使用,切换到 kCFRunLoopDefaultMode
  • kCFRunLoopDefaultMode(App的默认Mode,通常主线程是在这个 Mode 下运行)
  • UITrackingRunLoopMode(界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响)
  • NSRunLoopCommonModes (这是一个伪Mode,等效于NSDefaultRunLoopModeNSEventTrackingRunLoopMode的结合 )
  • GSEventReceiveRunLoopMode(接受系统事件的内部 Mode,通常用不到)

runLoop的基本原理

  • 系统中的主线程会默认开启runloop检测事件,没有事件需要处理的时候runloop会处于休眠状态。
  • 一旦有事件触发,例如用户点击屏幕,就会唤醒runloop使进入监听状态,然后处理事件。
  • 事件处理完成后又会重新进入休眠,等待下一次事件唤醒

runloop和线程的关系

  • runloop和线程一一对应。
  • 主线程的创建的时候默认开启runloop,为了保证程序一直在跑。
  • 支线程的runloop是懒加载的,需要手动开启。

runloop事件处理流程

  • 事件会触发runloop的入口函数CFRunLoopRunSpecific,函数内部首先会通知observer把状态切换成kCFRunLoopEntry,然后通过__CFRunLoopRun启动runloop处理事件
  • __CFRunLoopRun的核心是是一个do - while循环,循环内容如下

runloop是怎么被唤醒的

  • 没有消息需要处理时,休眠线程以避免资源占用。从用户态切换到内核态,等待消息;
  • 有消息需要处理时,立刻唤醒线程,回到用户态处理消息;
  • source0通过屏幕触发直接唤醒
  • source0通过调用mach_msg()函数来转移当前线程的控制权给内核态/用户态。

什么是用户态、核心态

  • 内核态:运行操作系统程序 ,表示一个应用进程执行系统调用后,或I/O 中断,时钟中断后,进程便处于内核执行
  • 用户态:运行用户程序 ,表示进程正处于用户状态中执行

runloop的状态

CFRunLoopObserverRef observerRef = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
 ? ? ?  switch (activity) {
 ? ? ? ? ?  case kCFRunLoopEntry: NSLog(@"runloop启动"); break;
 ? ? ? ? ?  case kCFRunLoopBeforeTimers: NSLog(@"runloop即将处理timer事件"); break;
 ? ? ? ? ?  case kCFRunLoopBeforeSources: NSLog(@"runloop即将处理sources事件"); break;
 ? ? ? ? ?  case kCFRunLoopBeforeWaiting: NSLog(@"runloop即将进入休眠"); break;
 ? ? ? ? ?  case kCFRunLoopAfterWaiting: NSLog(@"runloop被唤醒"); break;
 ? ? ? ? ?  case kCFRunLoopExit: NSLog(@"runloop退出"); break;
 ? ? ? ? ?  default: break;
 ? ? ?  }
 ?  });
 ?  CFRunLoopAddObserver(CFRunLoopGetCurrent(), observerRef, kCFRunLoopDefaultMode);
}

runLoop 卡顿检测的方法

  • NSRunLoop 处理耗时主要下面两种情况
    • kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting 之间
    • kCFRunLoopAfterWaiting 之后
  • 上述两个时间太长,可以判定此时主线程卡顿
  • 可以添加Observer到主线程Runloop中,监听Runloop状态切换耗时,监听卡顿
    • 用一个do-while循环处理路基,信号量设置阈值判断是否卡顿
    • dispatch_semaphore_wait 返回值 非0 表示timeout卡顿发生
    • 获取卡顿的堆栈传至后端,再分析

怎么启动一个常驻线程

// 创建线程
NSThread *thread = [[NSThread alloc]  initWithTarget:self selector:@selector(play) object:nil];
[thread start];
?
// runloop保活
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];

// 处理事件
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:NO];
复制代码

计时器

NSTimer、CADisplayLink、dispatch_source_t 的优劣

  • NSTimer

    • 优点在于使用的是target-action模式,简单好用
    • 缺点是容易不小心造成循环引用。需要依赖runlooprunloop如果被阻塞就要延迟到下一次runloop周期才执行,所以时间精度上也略为不足
  • CADisplayLink

    • 优点是精度高,每次刷新结束后都调用,适合不停重绘的计时,例如视频
    • 缺点容易不小心造成循环引用。selector循环间隔大于重绘每帧的间隔时间,会导致跳过若干次调用机会。不可以设置单次执行。
  • dispatch_source_t

    • 基于GCD,精度高,不依赖runloop,简单好使,最喜欢的计时器
    • 需要注意的点是使用的时候必须持有计时器,不然就会提前释放。

NSTimer在子线程执行会怎么样?

  • NSTimer在子线程调用需要手动开启子线程的runloop
  • [[NSRunLoop currentRunLoop] run];

NSTimer为什么不准?

  • 如果runloop正处在阻塞状态的时候NSTimer到达触发时间,NSTimer的触发会被推迟到下一个runloop周期

NSTimer的循环引用?

timertarget互相强引用导致了循环引用。可以通过中间件持有timer & target解决

GCD计时器

NSTimeInterval interval = 1.0;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), interval * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{
 ?  NSLog(@"GCD timer test");
});
dispatch_resume(_timer);

同时我也整理了一些面试题,有需要的朋友可以加QQ群:1012951431 获取

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-04-22 18:47:45  更:2022-04-22 18:51:06 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 22:57:27-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码