Skip to content

Files

Latest commit

author
Wang,Suyan
Apr 15, 2018
870ff80 · Apr 15, 2018

History

History
591 lines (395 loc) · 35.1 KB

01.md

File metadata and controls

591 lines (395 loc) · 35.1 KB

2018.01

为什么音频播放器突然没声音了呢?

作者: Lefe_x

做音频的同学可能都会遇到播放的音频突然没声音的情况,遇到这种情况后,一般控制台会抛出下面的错误:

[AVAudioSession setActive:withOptions:error:]: Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session.`

遇到这个错误,会导致音频不能正常播放的情况。出现这种情况的主要原因当你设置

[[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];

时还有某些操作占用了 AVAudioSession 权限,必须暂停或停止对 AVAudioSession 的使用。比如使用 AVAudioPlayer 播放某一音频时,此时音频正在播放,直接设置 AVAudioSessionactiveNO,就会报上面提到的错误。而正确的做法是先暂停播放,再设置 AVAudioSessionactiveNO。其正确的做法像下面代码所示,这样的好处是,当遇到设置失败后可以第一时间知道出错的时间点。

NSError *error;
BOOL isSuccess = [[AVAudioSession sharedInstance] setActive:active withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
if (isSuccess) {
   NSLog(@"恭喜你成功设置");
} else {
   NSLog(@"设置失败");
}

当然如果应用中有多个地方使用 AVAudioSession,建议项目中统一处理 AVAudioSessionactive,这样避免出现错误,一旦出现错误,调试起来就非常费劲。

iOS 中音量控制解惑

作者: Lefe_x

iOS 中音量中其实也有好多小窍门,这个小集帮你解惑。iOS 中主要有2个地方可以控制音量,一个是系统音量,用户主动按音量键,调整音量,这种方式会显示系统音量提示框;另一个是播放器的音量,比如通过 AVAudioPlayer 调整音量,这种不会显示系统提示音量框。

如何调节音量时不显示系统音量提示框

主要原理就是获取系统音量 View,并把它让用户不可见。但注意一点,你不能把 MPVolumeViewhidden 属性设置为 YES,这样导致的结果是用户调整音量时任然会显示系统音量提示框,如下代码所示。

_volumeView = [[MPVolumeView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
_volumeView.backgroundColor = [UIColor yellowColor];

// 如果设置了 Hidden 为 YES,那么修改音量时会弹出系统音量框
_volumeView.hidden = NO;
_volumeView.alpha = 0.01;
for (UIView *view in [_volumeView subviews]){
if ([view.class.description isEqualToString:@"MPVolumeSlider"]){
    self.volumeSlider = (UISlider*)view;
    break;
   }
}
[self.view addSubview:_volumeView];

获取系统音量

方法一:通过 self.volumeSlider 获取

如果想获取系统音量,可以通过第一种方式,self.volumeSlider.value 来获取,但是你发现第一次为 0,这很纠结,这样导致的结果就是获取的系统音量不准确。这是因为初始 MPVolumeView 时,volumeSlider.value 还没有赋值,如下图所示:

可以发现,音量是后来通过 [MPVolumeController updateVolumeValue] 来更新的。所以我们可以通过监听 self.volumeSlide 值改变时的事件,达到获取系统音量的目的。

[self.volumeSlider addTarget:self action:@selector(sliderValueDidChange:) forControlEvents:UIControlEventValueChanged];

方法二:通过 AVAudioSession 获取

[[AVAudioSession sharedInstance] outputVolume];

这种方法直接了当。

自定义音量控件

如果想自定义音量控件,可以监听音量的变化,并且通过第一种方法隐藏系统音量提示框。通过监听通知,达到监听音量变化的效果。

监听音量变化

监听音量变化,通过监听通知

AVSystemController_SystemVolumeDidChangeNotification

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(volumeChanged:) name:@"AVSystemController_SystemVolumeDidChangeNotification" object:nil];

最终结果 AVSystemController_AudioVolumeNotificationParameter 表示音量的值,这里需要注意的是 "AVSystemController_AudioVolumeChangeReasonNotificationParameter" = ExplicitVolumeChange; 这个值,它会由于不同的场景,有不同的值。ExplicitVolumeChange 是用户点击音量按钮,CategoryChange 是用户按 home 键调起 SiriRouteChange 这个时路线修改(不太清楚,什么情况下触发的)。

AVSystemController_SystemVolumeDidChangeNotification; object = <AVSystemController: 0x1c4001dc0>; userInfo = {
    "AVSystemController_AudioCategoryNotificationParameter" = "Audio/Video";
    "AVSystemController_AudioVolumeChangeReasonNotificationParameter" = ExplicitVolumeChange;
    "AVSystemController_AudioVolumeNotificationParameter" = "0.5625";
    "AVSystemController_UserVolumeAboveEUVolumeLimitNotificationParameter" = 0;
}}

注意点

如果通过代码修改了 self.volumeSlidevalue,那么会显示出系统音量框,如果你发现某个页面突然蹦出一个系统音量框,原因大多数是你修改了这个值。

动态修改应用程序的icon

作者: 南峰子_老驴

偶然看到 Price Tag 有个替换应用图标的功能,如图,研究了一下。

这个功能是在 iOS 10.3 后新增的,主要的 API 如下所示:

@interface UIApplication (UIAlternateApplicationIcons)
// If false, alternate icons are not supported for the current process.
@property (readonly, nonatomic) BOOL supportsAlternateIcons NS_EXTENSION_UNAVAILABLE("Extensions may not have alternate icons") API_AVAILABLE(ios(10.3), tvos(10.2));

// Pass `nil` to use the primary application icon. The completion handler will be invoked asynchronously on an arbitrary background queue; be sure to dispatch back to the main queue before doing any further UI work.
- (void)setAlternateIconName:(nullable NSString *)alternateIconName completionHandler:(nullable void (^)(NSError *_Nullable error))completionHandler NS_EXTENSION_UNAVAILABLE("Extensions may not have alternate icons") API_AVAILABLE(ios(10.3), tvos(10.2));

// If `nil`, the primary application icon is being used.
@property (nullable, readonly, nonatomic) NSString *alternateIconName NS_EXTENSION_UNAVAILABLE("Extensions may not have alternate icons") API_AVAILABLE(ios(10.3), tvos(10.2));
@end

只读属性 supportsAlternateIcons 用于判断系统是否允许修改 App 图标,只有在允许的情况下才能修改。-setAlternateIconName:completionHandler: 用于执行修改操作,如果 iconName 设置为 nil,则恢复为主图标,使用方式如下代码所示:

- (IBAction)changeIcon:(UIButton *)sender {
    if ([[UIApplication sharedApplication] supportsAlternateIcons]) {
        
        NSString *iconName = nil;
        if (sender.tag == 1) {
            iconName = @"rocket";
        } else if (sender.tag == 2) {
            iconName = @"pin";
        }
        
        [[UIApplication sharedApplication] setAlternateIconName:iconName completionHandler:^(NSError * _Nullable error) {
            
        }];
    }
}

除了调用 API 外,最主要的还需要在 info.plist 中配置 CFBundleIcons 项,这是一个字典,可包含 CFBundlePrimaryIconCFBundleAlternateIconsUINewsstandIcon 三个键。

CFBundlePrimaryIcon 为主图标,即 Assets.xcassetsAppIcon 的信息,一般置空。CFBundleAlternateIcons 即用于设置替换图标,具体的配置项描述可以参考官方文档 ,通常的配置如图所示。

这里需要注意的是,替换图标应该放在工程的某个目录下,而不放在 Assets.xcassets 中,如图所示。

iOS 关于音频播放调研

作者: Lefe_x

由于最近做音频方面的工作,就调研了一下关于音频播放的一些知识,中间也走过不少弯路,希望这篇小集能对关注我们的同学一点启示,少走一些弯路。最后提供一份我看过的资料。这里关于音频播放简单做一个总结。iOS 中音频播放有以下 5 种方式(如果你有更多的方式告诉我,非常感激),它们的使用场景各不同。

[1] 播放小于 30s 的音频: AudioServicesPlaySystemSound 可以播放小于等于 30s 的音频,主要用于播放一些提示音,你可以利用 AudioServicesPlaySystemSoundWithCompletion 的值播放完成的 callback。它有以下特点:

  • 使用系统音量,不能修改播放音量;
  • 立刻开始播放,不能暂停;
  • 不支持快进播放,也不可以循环播放;
  • 同一时刻只能播放一个音频;
  • 只能通过手机播放音频,不能通过其它设备输出,比如不能通过车载播放。

查看更多的系统声音ID

[2] AVAudioPlayer 播放本地的音频,或者已加载到内存中的音频流,主要用于播放本地的一些音频文件。注意它不能播放网络音频。它有以下特点:

  • 可以从任意位置播放,可快进,快退;
  • 可以循环播放;
  • 可以同时播放多个音频;
  • 可以控制播放速率;

[3] AVPlayer 可以播放本地和网络音频,也可以播放视频,它支持流媒体播放,也就是说我们可以用它来做边下别播的使用场景。

[4] AVQueuePlayerAVPlayer 的子类,它含有一个队列,主要用来播放一个音视频队列。

[5] Audio Queue 主要用来播放音频,录音,它比较底层,会有更多的控制权,如果 APP 主要功能是基于音频播放,推荐使用这个。

总的来说,如果普通的本地音频播放,可以选择 AVAudioPlayer ,这个不需要了解更多的音频知识,就可以达到一个基本的播放;如果想做流媒体播放,建议使用 AVPlayer + Local Server 的方式,类似于唱吧目前开源的方式。当然也可以选择 Audio Queue,不过这个难度比较高,需要对音频播放有一个整体的了解,推荐使用三方库 FreeStream,不过需要一些 C++ 的知识,因为使用过程中有一些坑需要填,这样不得不阅读源码。最后推荐一些不错的文章。

官方 Audio Queue

官方 AudioSession

@pp锅的码农生活 博客

@cy_zju 博客

iOS中NSArray/NSSet的一些巧妙用法

作者: Vong_HUST

最近用到很多操作集合类型的方法,这里总结分享一下,也欢迎大家一起补充。

  • 假设我们已经有一个 NSArray<Model *> 类型的数组,但是我们想把这个数组中的 Model 的某个属性取出组成一个新的数组,一般情况下可能是直接去遍历,但是 NSArray/NSSet 有一个更便捷的方法 valueForKey:,可以快速取出对应属性组成的数组。但是有个问题就是这个方法的效率比 for 循环低,数据量不大的时候使用还是没有问题的。如下面两张图:

  • 要取两个数组的交集的时候,可以先将 NSArray 转换成 NSMutableSet,再通过取二者交集即可。但是需要注意一点的是数组中的元素最好复写一下 isEqual:hash 方法,保证取交集后的结果是正确的。

  • 要将数组内元素排序或者过滤等操作,可以结合 NSSortDescriptorNSPredicate 使用,可以避免掉大量冗余的 for 循环之类的代码。关于 NSPredicate 的用法可以参考 NSHipsterRealmCheetsheet

  • 关于图中 valueForKey: 的参数为什么不直接用 @"name" 而是用 NSStringFromSelector(@selector(name)),是因为后者会有代码提示可以避免硬编码带来的错误,同时后续该 key 换名字了之后,会有对应的警告。这个也是从 AFNetworking 中学到的。如图所示:

对清除图片缓存的思考

作者: 高老师很忙

众所周知,使用 +[UIImage imageNamed:] 方法加载图片是会进图片缓存的,清除缓存是系统触发,并没有为我们提供API;使用 +[UIImage imageWithContentsOfFile:] 方法加载图片是不会进入图片缓存的。如果想要有图片缓存机制,并且能手动清除图片缓存,我们可以这样做:

+[UIImage imageWithContentsOfFile:] 方向下手: 我们可以自己维护一套图片缓存,Swizzle +[UIImage imageWithContentsOfFile:] 方法加入缓存机制。加载图片后,加入到 NSCache 缓存,再次取该图片时,优先取 NSCache 内的缓存,如果缓存内没有再去真正加载。NSCacheMemory Warning 的时候会自动清除缓存,我们也可以使用 -[NSCache removeAllObjects] 手动清除缓存。当然,你也可以不使用 Swizzle ,写一个 Manager 也是可以的,我只是提供一种思路。

+[UIImage imageNamed:] 方向下手: 在 Memory Warning 或进入后台时,系统会自动帮我们清除使用 +[UIImage imageNamed:] 的图片缓存。我们也可以通过模拟发送 UIApplicationDidReceiveMemoryWarningNotificationUIApplicationDidEnterBackgroundNotification 来清除图片缓存,风险可以根据实际情况来评估。

还可以从私有API来下手,+[UIImage imageNamed:] 系统底层是通过 UIAssetManager 来管理图片缓存的,如下两图所示,我们可以模拟调用 _clearCachedResources 方法来实现清除缓存。

如果有其他思路的,欢迎提出!

打印App中加载的库

作者: 南峰子_老驴

之前分享过配置环境变量 DYLD_PRINT_STATISTICS,来在控制台打印出程序启动过程中各个阶段所消耗的时间。今天再分享两个环境变量:DYLD_PRINT_LIBRARIESDYLD_PRINT_LIBRARIES_POST_LAUNCH,这两个变量用于打印 dyld 加载的库,如下代码所示:

dyld: loaded: /var/containers/Bundle/Application/717397DD-AE3E-457F-A446-609883FF865C/ChangeIcon.app/ChangeIcon
dyld: loaded: /Developer/usr/lib/libBacktraceRecording.dylib
dyld: loaded: /Developer/usr/lib/libMainThreadChecker.dylib
dyld: loaded: /Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
dyld: loaded: /System/Library/Frameworks/Foundation.framework/Foundation
dyld: loaded: /usr/lib/libobjc.A.dylib
dyld: loaded: /usr/lib/libSystem.B.dylib
dyld: loaded: /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
dyld: loaded: /System/Library/Frameworks/UIKit.framework/UIKit
dyld: loaded: /usr/lib/libarchive.2.dylib
dyld: loaded: /usr/lib/libicucore.A.dylib
dyld: loaded: /usr/lib/libxml2.2.dylib
dyld: loaded: /usr/lib/libz.1.dylib
dyld: loaded: /System/Library/Frameworks/CFNetwork.framework/CFNetwork
dyld: loaded: /System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration
......

两者的区别在于 DYLD_PRINT_LIBRARIES 会打印出所有被加载的库,而 DYLD_PRINT_LIBRARIES_POST_LAUNCH 打印的是通过 dlopen 调用返回的库,包括动态库的依赖库,主要发生在 main 函数运行之后。

参考:Logging Dynamic Loader Events

自定义WebView的UserAgent

作者: 拿破仑的_风火轮

  1. 产品需求:所有 APP 内的 WebView 访问的自家服务, 根据 UserAgent 自动切换显示合适的内容.
  2. 思路:自定义全局的 UserAgent .
  3. 实现代码如下:
/**
 * User-Agent 格式参照了 AFNetworking 设置
 */
- (void)customizeWebViewUserAgent {
    NSString *userAgent = nil;
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
    // User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
    userAgent = [NSString stringWithFormat:@"%@/%@ (%@-WebView; iOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], [[UIScreen mainScreen] scale]];
#pragma clang diagnostic pop
    
    if (userAgent) {
        if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) {
            NSMutableString *mutableUserAgent = [userAgent mutableCopy];
            if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) {//把不符合ASCII编码的字符,转码成ASCII编码格式
                userAgent = mutableUserAgent;
            }
        }
        
        [NSUserDefaults.standardUserDefaults registerDefaults:@{@"UserAgent": userAgent}];
    }
}
  • UIWebViewWkWebView 的默认 UserAgent 抓包如图所示

  • 自定义 UserAgent 如图所示

动画实现竟然可以这么简单,支持 NA 与 Web

作者: Lefe_x

你有没有想过,一个复杂的类似 Twitter 点赞动画使用一行代码就可以完成,而且基本不需要调整什么参数就可以达到 UE 的要求,从几天的工作量可以缩短到几分钟。而这一切多亏了 Lottie,它可以帮我们实现这一切。只需要有个由 Bodymovin 导出的 Json 文件,即可轻松实现 UE 提供的动效。

Lottie 是由 Airbnb 开源的库,它支持 iOS, macOS, Android ,React Native 和 Web。它主要通过解析一个由 Bodymovin 导出的 Json 文件,然后在 NAWeb 上渲染动画。而这些动画最初通过 Adobe After Effects 软件设计。

Adobe After Effects 简称 “AE” 是 Adobe 公司推出的一款图形视频处理软件,适用于从事设计和视频特技的机构,包括电视台、动画制作公司、个人后期制作工作室以及多媒体工作室。

BodymovinAdobe After Effects 的一个插件,可以把通过 AE 做出的动画,导出为 Json 文件,而 Lottie 直接使用导出的 Json 文件在手机端渲染动画。

看看具体的一个 Twitter 点赞动画实例:

LOTAnimatedSwitch *heartIcon = [LOTAnimatedSwitch switchNamed:@"TwitterHeart"];
heartIcon.frame = CGRectMake(100, 100, 200, 200);
[self.view addSubview:heartIcon];

Lottie 可以直接创建一个动画视图添加到另一个视图上,既可以完成显示。而它的原理就是通过解析 Json 文件生成一个 LOTComposition 类,来管理动画所需要的参数,然后利用原生类实现动画效果。目前主要支持的动画特性有:

  1. 转场动画;
  2. 通过 Json 文件渲染动画;
  3. 可以通过一个动画播放器来播放动画,拖到进度条,查看动画的实现,这对自己实现一个原生动画非常有帮助;
  4. 在运行时修改动画;

关于断言的一些用法和坑

作者: Vong_HUST

众所周知,我们在写代码的时候会写一些断言来发现调试阶段的一些异常情况,但是这些异常情况上线后是不应该展示给用户或者让用户感知到的。

通常使用 NSAssert 或基于它的宏。Xcode 4.2 之后在 release 模式下会自动将所有的 NSAssert 优化掉,也就是说,release 模式下,NSAssert 不会被编译到二进制文件中去,主要是通过这个宏 NS_BLOCK_ASSERTIONS 来实现的。 但是项目中接入的第三方库或者其它团队提供的库,无法保证他们也用 NSAssert,写C语言的工程师一般是用 assert 来做断言处理,但是这里就来坑了。具体坑体现在以下两种情况:

  1. assertreleaseXcode 是不会自动将这些断言移除,也就是会导致正式生产环境下会导致一些异常(崩溃)。我们可以通过 Building Settings -> 搜索 “Preprocessor Macros” 找到 release 模式,双击,然后添加,输入 NDEBUG=1 即可。如图所示

  1. 说完第一个坑,又来到第二个,如果 pod 里面也有用到 assert,上述步骤对 pod 无效。解决方案如图

就是 pod install 之后把每个 podtargetrelease 模式添加 NDEBUG=1。或者也可以在 podspec 里面添加对应的 GCC_PREPROCESSOR_DEFINITIONS 定义,不过由于是第三方库,可能不太方便(自己维护的 pod 另说)。

参考

  1. NSAssertion​Handler
  2. NSAssert vs. assert: Which do you use, and when?

证书勿随意Revoke

作者: 高老师很忙

InHouse 证书打了一个包,当 InHouse 证书过期后,已经安装了这个包的用户还可以继续正常使用,但用户想全新安装或者覆盖安装这个包就会失败;如果 InHouse 证书不是自然过期,而是手动 Revoke ,那么注意啦,用户不仅不能安装这个包,还会影响已经安装了这个包的用户,启动就会闪退,无法正常使用,如果此时你正在用蒲公英或者fir等三方平台灰测,那么可想而知。。。。虽然说发布证书Revoke不会影响已经发布到 AppStore 的包,但是依旧不提倡;如果你随意 Revoke 了一个正在使用中的 Push 证书,那么恭喜你,在你生成新的证书,Push 服务器更换之前,你的用户就无法收到 Push 啦。Revoke 需谨慎,且行且珍惜!搞不好,就会影响一大批用户。。。。

动态加载Framework/Library

作者: 南峰子_老驴

在开发 Framework/Library 时,我们可能需要在 Demo 中测试不同版本的兼容性。如果每次都在 DemoBuild Phase 中切换 Framework/Library 的不同版本,是件很麻烦的事。这种情况下,我们就可以考虑在运行时动态加载 Framework/Library。方法很简单,就是使用 "dlfcn.h" 中的 dlopen 函数:

void *framework1Handle = dlopen("DynamicFramework1.framework/DynamicFramework1", RTLD_LAZY);

dlopen 有一个对应的方法 dlclose 用于卸载库,不过在 iOS 上,这个方法似乎不起作用。因此在同一次运行时,没有办法直接切换 Framework/Library 的不同版本,否则会出现如图的提示。实际使用哪个版本的代码无法确定。

objc[10662]: Class CASHello is implemented in both /private/var/containers/Bundle/Application/73362515-9CCA-47E0-B709-49BA437935DC/ios-dynamic-loading-framework.app/Frameworks/DynamicFramework1.framework/DynamicFramework1 (0x105680178) and /private/var/containers/Bundle/Application/73362515-9CCA-47E0-B709-49BA437935DC/ios-dynamic-loading-framework.app/Frameworks/DynamicFramework2.framework/DynamicFramework2 (0x105694178). One of the two will be used. Which one is undefined.

变通的方案是在启动时提供一个选择页面,选择在运行 App 时,使用哪个版本的 Framework/Library。如果要切换版本,再重新启动 App

dlopen 仅限于开发阶段使用。

缓存 NSDateFormatter

作者: 拿破仑的_风火轮

为什么要缓存 NSDateFormatter ?

Creating a date formatter is not a cheap operation. If you are likely to use a formatter frequently, it is typically more efficient to cache a single instance than to create and dispose of multiple instances. One approach is to use a static variable. developer.apple.com

思路: 利用 NSCache, 以 stringFormatter+NSLocale的localeIdentifier 为key缓存 NSDateFormatter. 当UIApplicationDidReceiveMemoryWarningNotificationNSCurrentLocaleDidChangeNotification 释放 NSCache 缓存的对象.

代码参考code, 核心实现代码如下:

- (NSDateFormatter *)dateFormatterWithFormat:(NSString *)format andLocale:(NSLocale *)locale {
    @synchronized(self) {
        NSString *key = [NSString stringWithFormat:@"%@|%@", format, locale.localeIdentifier];
        
        NSDateFormatter *dateFormatter = [loadedDataFormatters objectForKey:key];
        if (!dateFormatter) {
            dateFormatter = [[NSDateFormatter alloc] init];
            dateFormatter.dateFormat = format;
            dateFormatter.locale = locale;
            [loadedDataFormatters setObject:dateFormatter forKey:key];
        }
        
        return dateFormatter;
    }
}

定时器引发的思考

作者: Lefe_x

当使用 AudioServicesPlaySystemSoundWithCompletion 播放完一段音频后,在回调中开启一个定时器,然后发现定时器不执行,代码是这样的:

AudioServicesPlaySystemSoundWithCompletion(sysSoundID, ^{
    AudioServicesDisposeSystemSoundID(sysSoundID);
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(lefexTimerAction:) userInfo:nil repeats:YES];
});

- (void)lefexTimerAction:(NSTimer*)timer
{
   // 这个方法并不会执行
}

如果我把代码改成下面这样,定时器可以正常执行,但是发现方法 lefexTimerAction: 并不在主线程中执行,而是在开启定时器对应的线程中执行,代码如下:

AudioServicesPlaySystemSoundWithCompletion(sysSoundID, ^{
   AudioServicesDisposeSystemSoundID(sysSoundID);
   NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(handlePageStayTimer:) userInfo:nil repeats:YES];
   [[NSRunLoop currentRunLoop] run];
 });

看到这里,相信你已经明白是什么原因了。

系统音频播放完成后回调并不在主线程,导致开启定时器时不在主线程。TimerrunLoop 是一起工作的,没有 runLoop,定时器不能正常执行。而系统中只有 mainRunLoop 会默认开启,这就是为什么在主线程创建定时器可以正常执行的原因。

runLoop 会强引用 timer,这就是我们经常所说的为什么 timer 会导致内存泄漏,即使在 dealloc 中释放 timer,也不能避免内存泄漏,因为 dealloc 就不会执行。

查看 runLoop 中的 timer 信息发现,它会记录 timer 下次要执行的时间,当 runLoop 到下一次循环的时候,会检测 timer 是否需要执行,这也就是 timer 不准的原因,因为每一次 runloop 后才会执行 timer 的事件。

{
  valid = Yes,
  firing = No,
  interval = 1,
  tolerance = 0,
  next fire date = 538315424 (-6.348737 @ 113761747014063),
  callout = (NSTimer) [Lefex handlePageStayTimer:],
  context = <CFRunLoopTimer context 0x600000226680>
 }

总结: 从上面的问题来看,NSTimer对学习 runLoop 有很大帮助。runLoop 和线程是一一对应的,而除主线程外,其他线程对应的 runLoop 并没有创建,当调用 [NSRunLoop currentRunLoop] 时会创建这个线程对应的 runLoop,定时器能跑起来的前提是 runLoop 必须 run

另一种形式定时器

作者: Vong_HUST

从昨天 @Lefe_x 的分享我们知道 NSTimer 需要配合 runloop 使用,而且它计时是不精确的,同时处理不当的情况下会存在循环引用的情况。

今天和大家分享一下基于 GCDTimer,它能解决掉刚刚提及到的 NSTimer 的三大问题。先来看一段示例代码,如图,具体解释已经包含在图中了。

但是使用 dispatch_suspenddispatch_resume 这两个方法需要注意配对使用,不然可能会有意想不到的“惊(崩)喜(溃)”。值得一提的是 dispatch source 并没有提供用于检测 source 本身的挂起计数的 API,也就是说外部无法得知当前 source 状态。还有就是创建 timer 最好被持有,不然 dispatch_suspend 之后,如果没有被持有的话,就会 crash

dispatch_cancel 调用后,这个 timer 就失效了,类似 NSTimerinvalidate

关于 GCD Timer 推荐一个开源库:SwiftTimer

官方文档

弱持有容器方案

作者: 高老师很忙

在通知者模式中,manger 想弱持有一些对象,我们可以怎么做呢?

方案一:使用Foundation为大家提供的弱持有容器:NSHashTableNSMapTableNSPointerArray,初始化时把option设置成weak即可;

方案二:可以使用CFFoundationCFArrayCreateMutable等来创建容器,被添加的对象引用计数不会加1;

方案三:可以妙用NSValuevalueWithNonretainedObject方法,被添加的对象不需要服从NSCopying协议;

方案四:使用我们通常用的强持有容器,让被添加的对象weak一下,比如使用__weakblock配合:在block中返回weak对象。

如果有其他思路的,欢迎提出!

利用 Custom event 解决一个小问题

作者: 拿破仑的_风火轮

做IM开发时,有个场景是:数据异步处理,回到主线程刷新UI、展示消息给用户看到.当用户短时间内收到很多条消息时,我们不想对UI进行频繁而累赘的更新,理想的情况是当主线程繁忙时将所有的改变联结起来。此时,可以利用联结的优势(在异步线程上调用 dispatch_source_merge_data 后,就会执行 dispatch source 事先定义好的 handler )。使用 DISPATCH_SOURCE_TYPE_DATA_ADD,将刷新UI的工作拼接起来,短时间内做尽量少次数的刷新。

伪代码

执行结果

欢迎大家分享其他思路.

谈一谈iOS平台跨域访问漏洞

作者: Lefe_x

最近很多关于 iOS Webview 漏洞的话题,利用这个机会,我也来说说对这个漏洞的理解,如果有理解不对的地方,欢迎指出,同时感谢 @折腾范儿_味精 的指点。

iOS平台跨域访问漏洞成因是由于 UIWebView 默认开启了WebKitAllowUniversalAccessFromFileURLsWebKitAllowFileAccessFromFileURLs 选项。这样黑客利用这个漏洞给某个 APP 下发一个 HTML 文件,当 UIWebView 使用 file 协议打开这个 HTML 文件,而 HTML 文件中含有一段窃取用户数据的 JS 代码,这样就导致用户数据泄露的可能。为了演示这个漏洞,我特意写了一个 Demo,截图是运行结果。

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
_webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
[_webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:filePath]]];
<!DOCTYPE html>
<html>
    <body>
        <script>
            // 这个可以是手机任意一个文件地址
            var localfile = "/etc/passwd"
            var xhr = new XMLHttpRequest();
            xhr.onreadystatechange = function() {
                if (xhr.readyState == 4) {
                    alert(xhr.responseText);
                }
            }
           try {
              xhr.open("GET", localfile, true);
              xhr.send();
           } catch (ex) {
              alert(ex.message);
           }
        </script>
    </body>
</html>

运行上面的代码,会读取出手机端 /etc/passwd 的文件。这样的话,我们可以利用这个漏洞访问其他应用的数据,而不必需要用户的许可。而 WKWiebViewWebKitAllowUniversalAccessFromFileURLsWebKitAllowFileAccessFromFileURLs 默认是关闭的,不会存在这样的风险。

那么如何觉得自己的 APP 是否是安全的呢?看看有没有满足下面几点:

  1. APP 中是否含有远程下发的 htmlAPP 加载,而中途会被别人篡改;
  2. 加载 HTML 文件是通过 file 协议加载的;
  3. 使用的是否为 UIWebView
  4. 使用 WKWiebView 是否有主动开启 WebKitAllowUniversalAccessFromFileURLsWebKitAllowFileAccessFromFileURLs 这两个属性。