正在加载今日诗词....
11 min read

iOS 天问 第2讲

这是iOS 每日一题的第二篇了, 坚持下来不容易, 刚开始想每个问题都整理很多, 细细地写下了, 后面发现很多时候,因为工作时间问题,并不能如愿, 而且目前还负责一部分服务器的开发, 又忙了起来. 而最重要的是要坚持做下去这件事, 不在于问题的复杂度. 所以加油!
iOS 天问 第2讲

一、 cancelPreviousPerformRequestsWithTarget 是什么 ? 用在哪些场景 ? 如何做到的 ?

查看苹果文档可以看到

/**************** 	Delayed perform	 ******************/

@interface NSObject (NSDelayedPerforming)

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;

@end

@interface NSRunLoop (NSOrderedPerform)

- (void)performSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg order:(NSUInteger)order modes:(NSArray<NSRunLoopMode> *)modes;
- (void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg;
- (void)cancelPerformSelectorsWithTarget:(id)target;

@end

// <Foundation/NSThread.h>
@interface NSObject (NSThreadPerformAdditions)

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
	// equivalent to the first method with kCFRunLoopCommonModes

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
	// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);

@end
  • 此类方法一共有两类:

  • NSObject 的分类 NSDelayedPerforming 延迟执行的方法

    • 延迟执行依赖定时器
  • NSRunLoop 的分类 NSOrderedPerform 顺序执行的方法

    • 依赖定时器
  • 每一类都有自己的 perform.. 和 自己的 cancel...

  • 补充: NSObject 的另一个分类 NSThreadPerformAdditions, 可以通过指定线程去执行延迟操作

NSObject 延迟方法

All perform requests are canceled that have the same target as aTarget, argument as anArgument, and selector as aSelector. This method removes perform requests only in the current run loop, not all run loops.

总结:

  • 取消的请求 target 都是指定的 aTarget, 务必保证 performSelector... 系列方法的参数一致
  • 取消的请求是 aSelector , 务必保证 performSelector... 系列方法的参数一致
  • 且请求比如在当前的 run loop 也就是说必须在当前的线程才可以, 即便同一个 target , 同一个 SEL ,只要是不再同一个线程, 也不会起作用.
  • 其中参数 使用 isEqual 来判断 同一性 !
The argument for requests previously registered with the performSelector:withObject:afterDelay: instance method. Argument equality is determined using isEqual:, so the value need not be the same object that was passed originally. Pass nil to match a request for nil that was originally passed as the argument.

使用场景:

  • 取消相同行为的请求, 比如一个按钮的点击事件, 不想让它同时响应多次用户的点击事件.

NSRunloop 方法

方法执行

- (void)performSelector:(SEL)aSelector 
                 target:(id)target 
               argument:(id)arg 
                  order:(NSUInteger)order 
                  modes:(NSArray<NSRunLoopMode> *)modes;
This method sets up a timer to perform the aSelector message on the receiver at the start of the next run loop iteration. The timer is configured to run in the modes specified by the modes parameter. When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in one of the specified modes; otherwise, the timer waits until the run loop is in one of those modes.

This method returns before the aSelector message is sent. The receiver retains the target and anArgument objects until the timer for the selector fires, and then releases them as part of its cleanup.

Use this method if you want multiple messages to be sent after the current event has been processed and you want to make sure these messages are sent in a certain order.
  • 此方法会创建一个定时器, 在下一个运行循环开始的时候,
  • 定时器执行的 mode 根据执行的 modes 确定
  • 注意: 消息接收者会持有目标 target 和 参数对象, 一直到 这个定时器执行这个方法

使用场景:

  • 某个事件中, 需要按指定的顺序执行 一些行为.

方法取消

- (void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg

This method cancels the previously scheduled messages associated with the target, ignoring the selector and argument of the scheduled operation. This is in contrast to cancelPerformSelector:target:argument:, which requires you to match the selector and argument as well as the target. This method removes the perform requests for the object from all modes of the run loop.
  • 它会取消那些仍然 没有完成 的 任务, 无关任务所在 runloop 运行模式.

  • 注意要取消的行为, 相同的参数, 相同的 target, 相同的方法

  • todo:// 示例代码

  • FakeRunLoop

  • 多线程问题

by l.j. twan 2018/11/19


二、 ORM 是什么 ? 解决什么问题? iOS 有哪些应用 ?

by l.j. twan) 2018/11/20


三、 子线程如何保活, 以及正确退出 ?

Runloop-info-a033384f-067b-4013-a6f1-955076417fe9-1542766808815-86649094

Runloop 相关的类

启动

既然是保活, 就要有启动一说, 而官方文档中启动方式有

@interface NSRunLoop (NSRunLoopConveniences)

- (void)run; 
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
@end
  • - (void)run; 内部循环调用 - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate; , 一直到 runloop 终结.
  • - (void)runUntilDate:(NSDate *)limitDate; //内部循环调用 - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate; 一直到设置的超时
  • - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate; // 执行一次, 任务完成退出,或者 执行过程中超时退出.

- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate; 是其他两种方式的基础操作

任务

NSRunLoopMode

对于这个, 需要了解的是, 子线程即便开启了 run, 但是如果没有 mode, 仍然会立即退出, 如果想真正的执行任务, 我们就要指定一个或多个 NSRunLoopMode 才可以.

typedef NSString * NSRunLoopMode NS_EXTENSIBLE_STRING_ENUM;
@property (nullable, readonly, copy) NSRunLoopMode currentMode;

iOS使用的 NSRunLoopMode 总共有 3种.

  • NSRunLoopCommonModes
    • Objects added to a run loop using this value as the mode are monitored by all run loop modes that have been declared as a member of the set of “common" modes; see the description of CFRunLoopAddCommonMode for details.
  • NSDefaultRunLoopMode
    • The mode to deal with input sources other than NSConnection objects.
  • UITrackingRunLoopMode
    • The mode set while tracking in controls takes place. You can use this mode to add timers that fire during tracking.
    • 经常处理的 列表 timers 的问题
  • NSEventTrackingRunLoopMode
    • 用于 macOS A run loop should be set to this mode when tracking events modally, such as a mouse-dragging loop.
  • NSModalPanelRunLoopMode
    - 用于 macOS - A run loop should be set to this mode when waiting for input from a modal panel, such as NSSavePanel or NSOpenPanel

RunLoopModeItem

而每种 NSRunLoopMode 运行模式都需要具体的 RunLoopModeItem, 一共有3类

  • CFRunLoopSourceRef : 输入源, 也就是事件产出的地方; 如屏幕触碰, 锁屏等
  • CFRunLoopObserverRef: 观察源, AutoReleasePool 涉及到的 Runloop生命状态检测来 管理自动释放池的创建与销毁
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};
  • CFRunLoopTimerRef: 定时源 , 如平时使用的 NSTimer

执行 : 创建具体的 RunLoopModeItem 并 添加到 指定的 NSRunLoopMode 中.

  • 一个 item 可以被同时加入多个 mode。
  • 但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环
  • 切换 item 需要 退出当前的 item

官方相关 API

/// Schedules the execution of a block on the target run loop in given modes.
/// - parameter: modes   An array of input modes for which the block may be executed.
/// - parameter: block   The block to execute
- (void)performInModes:(NSArray<NSRunLoopMode> *)modes block:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

/// Schedules the execution of a block on the target run loop.
/// - parameter: block   The block to execute
- (void)performBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

上面的是 ios(10.0) 之后的, 不是用来 runloop 启动的, 而是用来执行某个操作, 使用 Block 的方式紧耦合.

退出

如果想退出, 一定不要使用 - (void)run; 去开启 Runloop

  1. 一般我们可以使用 - (void)runUntilDate:(NSDate *)limitDate;- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate; 主要是依赖一个 超时时间 去处理
  2. CFRunLoopStop() 主动退出 当前的 - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;

示例代码摘自 深入研究 Runloop 与线程保活

CFRunLoopStop(CFRunLoopGetCurrent());
NSThread *thread = [NSThread currentThread];
[thread cancel];

by l.j. twan) 2018/11/21


四、 如何保证图片压缩的效率和准确性 ?

  • 二分查找 优化时间复杂度
  • 注意边界
  • 避免递归

以前遇到一个微信分享的问题, 总是发现有些图片分享, 微信无法正常调用, 检查发现是,项目中的压缩算法得出的图片超过了微信的限制 , 贴一下代码如下

/**
 压缩到指定千字节(kb)
 */
+ (NSData *)compressImage:(UIImage *)sourceImage  targetKB:(NSInteger )numOfKB {
    if (!sourceImage) return nil;
    
    CGFloat compressionQuality = 0.9f;
    CGFloat compressionCount = 0;
    
    NSData *imageData = UIImageJPEGRepresentation(sourceImage,compressionQuality);
    
    while (imageData.length >= 1000 * numOfKB && compressionCount < 15) {  //15是最大压缩次数.mac中文件大小1000进制
        compressionQuality = compressionQuality * 0.9;
        compressionCount ++;
        imageData = UIImageJPEGRepresentation(sourceImage, compressionQuality);
    }
    return imageData;
}

compressionCount 增大, 效果也不会理想, 很简单的功能, 但是效率太低, 如果采用此种方式, 不进行 compressionCount 的限制, 大抵会严重损耗手机性能.

正确的二分法压缩方式 ✅
摘录代码

NS_INLINE CGFloat clampCompressionFactor(CGFloat factor)
{
    return factor <= 1e-10 ? 1e-10 : factor > 0.1 ? 0.1 : factor;
}

- (NSData *)compressToJPEGFormatDataWithFactor:(CGFloat)factor maxFileSize:(u_int64_t)fileSize
{
    if (!self) return nil;
    
    NSData *tempImageData = UIImageJPEGRepresentation(self, 1.0);
    if ([tempImageData length] <= fileSize) return tempImageData;
    
    NSData *targetImageData = nil;
    CGFloat compressionFactor = clampCompressionFactor(factor);
    CGFloat minFactor = 0;
    CGFloat maxFactor = 1.0;
    CGFloat midFactor = 0;
    
    while (fabs(maxFactor-minFactor) > compressionFactor)
    {
        @autoreleasepool
        {
            midFactor = minFactor + (maxFactor - minFactor)/2;
            tempImageData = UIImageJPEGRepresentation(self, midFactor);
            
            if ([tempImageData length] > fileSize)
            {
                maxFactor = midFactor;
            }
            else
            {
                minFactor = midFactor;
                targetImageData = tempImageData;
            }
        }
    }
    
    return targetImageData;
}

by l.j. twan 2018/11/23


五、有个排名的场景,有用户几十万, 每个人都有自己的钻石数据,数据库只暴露小数点四位,目前最大的数值是两三百的值, 如何高效获取排名前30的用户数据。

orderByTree-c488185b-108e-4ed5-9dc4-facac4a968ca-1543029530169-32877756

  1. 首先考虑的是因为不是整数,无法使用 桶排序, 只能使用分治-快排 (也适合内存比较小的场景,不能使用归并排序) 时间复杂度 O(Nlog(N))
    • 分成100份,快排,每份提取出前30, 再对 100 * 30 进行快排.
  2. 一种是直接 sql 排序返回 , 效率比较低,且依赖数据库设备
  3. 一种是 使用 redis 的 有序集合 ,直接获取排名
    • redis 的有序集合 实现依赖于 跳表 + 散列表 + ...
  4. 其实将第一种优化, 使用 数值 * 10000 , 将小数变为整数, 然后进行 桶排序 效率更高, 不过需要额外的空间, 不能使用位图排序, 因为不能去重!

摘录:
在大规模数据处理环境下,作业效率并不是首要考虑的问题,算法的扩展性和容错性才是首要考虑的。算法应该具有良好的扩展性,以便数据量进一步加大(随着业务的发展,数据量加大是必然的)时,在不修改算法框架的前提下,可达到近似的线性比;算法应该具有容错性,即当前某个文件处理失败后,能自动将其交给另外一个线程继续处理,而不是从头开始处理

by l.j. twan 2018/11/24


快速从 ocr 的结果集中, 归类到 关键字标签的算法

类似问题: Word 的拼写检查如何实现 ?

  • Trie Tree 前缀树
  • 优化 模糊匹配的结果

具体代码暂时只是一个思路, 自己实现一个 Trie 还是比较难的 😢

by l.j. twan 2018/11/25

摘录:

若要在某一领域内达到专家级的水平,其关键在于“审慎地重复”,也就是说,并非是机械地,一遍又一遍地练习,而是要不断地挑战自我,试图超越自身当前的水平,通过不断的尝试挑战,并在尝试的过程中和尝试之后对自身的表现进行分析和总结,吸取经验,纠正之前犯过的各种错误。把这一“审慎”的过程不断重复,才能取得成功。