macOS 解决 App Nap 导致 Timer 回调变慢问题

背景 在开发 macOS 上的窗口共享过程中,发现 demo 被其它应用覆盖后,在25s左右,观众收到的窗口画面开始变得卡顿。 经过排查发现问题出在主播端的采集逻辑处。在我的上一篇文章 macOS 屏幕共享开发 提到,窗口共享的实现跟屏幕共享不一样,窗口共享没有现有的系统接口,需要启动一个 Timer 去定时对窗口截图,再对截图进行处理。 通过打印日志发现,demo 进入后台后,Timer 在25s左右,回调开始变慢,大约2s才回调一次,问题的原因就出在这里。 排查原因 为了避免其它逻辑影响了结果,新建一个干净的 demo。 我在窗口共享中用的是 GCD Timer,一开始猜测是 GCD Timer 的某些参数没有设置,查看文档后没找到相关的参数;然后猜测 GCD Timer 的问题,但是换成 NSTimer 后,发现也有一样的问题,这时候我开始猜测是系统导致的问题了。 开始查阅资料,发现了 一篇文章 里面提到: App Nap 是 OS X 10.9 Mavericks 的一项新功能。它能帮你在同时运行多个应用程序时节省电能。 这时候有了切入点,查询关键字 App Nap 就可以发现相关资料。 App Nap 附上苹果官方说明文档:Extend App Nap If an app isn’t performing user-initiated work such as updating content on screen, playing music, or downloading a file, the system may put the app in App Nap. App Nap conserves battery life by regulating the app’s CPU usage and by reducing the frequency with which its timers are fired. ...

2021年7月5日 · Darren Ou

macOS 屏幕共享开发

一、屏幕录制权限 应用首次创建屏幕截图或者窗口截图时(以下任一接口都可以),就会显示系统的权限申请弹窗,通过选中Screen Recording里面的相关应用,重启应用后即可开启权限。 CGDisplayCreateImage() CGDisplayCreateImageForRect() CGWindowListCreateImage() CGWindowListCreateImageFromArray() 二、获取screenId 通过AppKit的NSScreen.screens或者通过CoreGraphics的CGGetActiveDisplayList()都可以获取当前的所有显示器信息,下面用NSScreen.screens举例子: [NSScreen.screens enumerateObjectsUsingBlock:^(NSScreen * _Nonnull screen, NSUInteger idx, BOOL * _Nonnull stop) { NSNumber *screenNumber = screen.deviceDescription[@"NSScreenNumber"]; if (screenNumber) { CGDirectDisplayID displayId = screenNumber.unsignedIntValue; NSLog(@"displayId: %d", displayId); } }]; 注意:如果是镜像显示,则只会打印一个显示器信息。 三、获取windowId 1、获取windowId列表 通过接口CGWindowListCopyWindowInfo()可以获取windowId列表。 其中kCGWindowListExcludeDesktopElements参数表示从列表中排除所有属于桌面元素的窗口。 CGWindowListCopyWindowInfo()返回的是一个CFArrayRef,使用完后需要主动release。 这里有一个技巧,利用CFBridgingRelease()函数,可以把CFArrayRef桥接到NSArray,并且会把内存管理转移到ARC,就是说我们不需要再去主动release了。 NSArray *windowDictArr = CFBridgingRelease(CGWindowListCopyWindowInfo(kCGWindowListOptionAll | kCGWindowListExcludeDesktopElements, 0)); 2、过滤无用windowId 通过以上接口获取到的windowId列表可能会包含很多我们不关心的windowId: 1. 没显示出来的window 通过kCGWindowLayer跟kCGWindowAlpha两个key,可以获取到当前window是否有layer以及是否透明,我们需要过滤掉不可见的window。 2. 过滤信息不全的window kCGWindowNumberkey可以从字典里获取到windowId,需要过滤没有windowId的字典信息。 3. 过滤无法获取截图的window 通过CGWindowListCreateImage()接口,传入对应windowId,可获取到window的截图,如果无法获取到截图,可能没有录屏权限、不是window主体、window不可见。 3、获取window frame 通过CGRectMakeWithDictionaryRepresentation()可获取窗口的frame,某些业务可能需要用到frame。 四、屏幕共享 屏幕共享的视频输入总体来说跟摄像头的视频输入相当类似,可以理解成视频流从摄像头改成了屏幕输入(AVCaptureDeviceInput->AVCaptureScreenInput),视频输出的处理则跟摄像头的输出处理完全一样。 1、关键代码: - (void)startCapture { self.captureSession = [[AVCaptureSession alloc] init]; self.captureSession.sessionPreset = AVCaptureSessionPresetMedium; // input AVCaptureScreenInput *input = [[AVCaptureScreenInput alloc] initWithDisplayID:kCGDirectMainDisplay]; [self.captureSession addInput:input]; // output [self.captureSession beginConfiguration]; dispatch_queue_t queue = dispatch_queue_create("smaple_buffer", DISPATCH_QUEUE_SERIAL); AVCaptureVideoDataOutput *videoDataOutput = [[AVCaptureVideoDataOutput alloc] init]; videoDataOutput.videoSettings = @{(NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)}; [videoDataOutput setSampleBufferDelegate:self queue:queue]; AVCaptureConnection *videoConnection = [videoDataOutput connectionWithMediaType:AVMediaTypeVideo]; videoConnection.videoOrientation = AVCaptureVideoOrientationPortrait; [self.captureSession addOutput:videoDataOutput]; [self.captureSession commitConfiguration]; [self.captureSession startRunning]; } 2、视频输出分辨率设置 我们的编码分辨率最大为720p,一个是避免捕获到的图片过大造成性能浪费,一个是避免难以适配太多的屏幕分辨率,我们需要指定输出的分辨率。 ...

2021年5月18日 · Darren Ou

iOS UI 自动化测试简介

前言 发布内测版的时候,每次在提交代码前、上传ipa包前、发布前都需要手动地跑一下主流程,比如在视频详情页划几页,在直播页划几个直播间等。都是比较重复且有规律的操作,可以利用自动化测试来代替手工操作。 我们在Jenkins上面打包时,会有一个UI自动化测试的选项,是架构组自己弄的一套自动化测试环境,想要自己搭建估计不太方便。所以需要有一套轻量级切足够易用的UI自动化测试框架。 XCUITest 简介 XCUITest是苹果在iOS9.3提供的自动化测试框架(iOS9.3之前提供的是UI Automation),Xcode自带的框架,不需要搭建其它环境,由于是苹果提供的框架,运行稳定。使用Swift或者OC编写测试用例(可能对于测试不是很友好)。 使用 func testExample() throws { // UI tests must launch the application that they test. let app = XCUIApplication(bundleIdentifier: "appium.test") app.launch() // 启动应用 for _ in 1 ..< 5 { app.swipeUp() // 上划 } for _ in 1 ... 3 { app.swipeRight() // 右划 } app.swipeDown() // 下划刷新 sleep(5) // 等待5s再执行下一步 app.cells.firstMatch.tap() // 点击当前的第一个cell for _ in 1 ... 5 { app.swipeUp() // 上划 sleep(2) // 等待2s再执行下一步 } app.buttons["live_room_quit_btn"].tap() // 点击标志btn } 通过id获取UI元素,需要先在代码设置accessibilityIdentifier: _quitButton.accessibilityIdentifier = @"live_room_quit_btn"; ...

2020年11月28日 · Darren Ou

在树莓派上搭建 Homebridge,实现 HomeKit 控制智能家居

一、什么是Homebridge Homebridge is a lightweight NodeJS server you can run on your home network that emulates the iOS HomeKit API. It supports Plugins, which are community-contributed modules that provide a basic bridge from HomeKit to various 3rd-party APIs provided by manufacturers of “smart home” devices. HomeKit是苹果的智能家居平台,是iOS系统层面支持的平台,拥有其它平台无法比拟用户体验。可惜大部分米家设备及其它平台的设备都不支持HomeKit,但是可以通过Homebridge作为桥接,实现HomeKit控制其它平台的设备。 二、如何在树莓派上搭建Homebridge https://github.com/homebridge/homebridge/wiki/Install-Homebridge-on-Raspbian 三、安装插件 在这个网址https://www.npmjs.com/search?q=keywords%3Ahomebridge-plugin上可以通过关键字搜索插件, 常用插件: 1.Aqara套装:homebridge-mi-aqara 2.米家空调伴侣:homebridge-mi-acpartner 3.米家yeelight台灯:homebridge_yeelight 4.小米风扇:homebridge-mi-fan 5.米家智能摄像机:homebridge-mi-camera 6.米家智能排插:homebridge-mi-outlet 7.米家空气净化器:homebridge-mi-airpurifier 8.飞利浦灯泡:homebridge-mi-philips-light 9.智能插座:homebridge-mi-outlet ... 四、配置设备 在Homebridge上,插件利用设备的IP地址及设备的token即可以实现控制某个设备,IP地址我们可以登录路由器查看已连接的设备获取,获取token有以下几种方法: 1、使用安卓手机下载旧版本的米家app,导出里面的数据库找到设备token 2、使用miio discover命令获取(米家新的设备基本都隐藏了token,这种方法基本失效) 3、利用Charles抓包 ...

2020年10月11日 · Darren Ou

【利用基于 Lint 的启动代码监控 Android 进房间流程】iOS 平台的可行性调研

一、目的 通过标识关键代码,利用Lint定期检测关键代码,当关键代码发生变更时,可以在报告中体现,避免引入意外的修改。 二、Lint Lint介绍 Lint 作为一种工具程序,主要负责静态源码分析,负责代码规范、代码缺陷等检测。(https://zh.wikipedia.org/wiki/Lint) Android 原生已经提供了 Lint 静态分析工具,而 iOS 则有对应的 OCLint 和 SwiftLint。 三、android实现代码监控的原理 Android Lint可以对Java/Kotlin源码使用uast进行语法分析,最后生成一棵AST抽象语法树,这棵树的每个节点都是一个方法,包含方法签名和方法体。 通过自定义的Lint规则,找到应用启动的入口方法。从入口方法开始遍历整棵调用树,即可对每个方法节点的代码段进行分析。 1. 获取关键代码段 open fun shouldMonitor(node: UMethod): Boolean { return node.getAnnotations().find { it.qualifiedName == LINT_MONITOR_ANNOTATION } != null } android通过注解的方式,标识需要检测的方法实现。通过Android Lint的自定义规则判断注解标识的方法,找到入口方法,然后对代码段进行分析。 iOS对应的OCLint也可以通过自定义规则进行静态分析,但是iOS没有注解,iOS如果需要标识关键代码段,目前想到的只能hardcode检测列表(包括类和方法名)来实现标识关键代码段的效果。但是这样的维护成本比较大。 iOS可以利用类似以下的代码片段进行类和方法的判断: for(auto it = parent->meth_begin(), end = parent->meth_end(); it != end; ++it) { const auto method = *it; if(declHasEnforceAttribute(method, *this)) { const auto selector = method->getSelector(); if(!implementation->getMethod(selector, method->isInstanceMethod())) { const string className = parent->getNameAsString(); const string methodName = selector.getAsString(); addViolation(implementation, this, "subclasses of " + className + " must implement " + methodName); } } } 注:在生成OCLint自定义规则工程时,Xcode 12会因架构问题出错,暂未找到解决方案,可暂时先切换Xcode 11生成OCLint自定义规则工程 ...

2020年8月20日 · Darren Ou

利用 git worktree 并行开发

一、什么是worktree? Manage multiple working trees attached to the same repository. A git repository can support multiple working trees, allowing you to check out more than one branch at a time. 管理连接到同一存储库的多个工作树。一个git仓库可以支持多个工作树,允许一次迁出多个分支。 二、为什么要用worktree? 场景:正在开发新的需求,这时候还需要对已提测功能的bug fixes 方法1:先stash正在开发的代码,再check out到原分支进行bug fixes 缺点:分支切换后需要重新pod install,然后xcode重建索引并重新编译,耗时耗力 方法2:clone两个仓库 缺点:.git目录会占空间,且两个仓库之间毫无关联,无法相互查看本地提交 方法3:git worktree 三、worktree的相关命令 ➜ worktree_test git:(develop) git worktree -h usage: git worktree add [<options>] <path> [<commit-ish>] or: git worktree list [<options>] or: git worktree lock [<options>] <path> or: git worktree move <worktree> <new-path> or: git worktree prune [<options>] or: git worktree remove [<options>] <worktree> or: git worktree unlock <path> 四、常用操作 1、添加一个worktree git worktree add ../live_protocol_isolation team_live/live_protocol_isolation ...

2020年5月10日 · Darren Ou

树莓派简介

一、树莓派是什么 树莓派是一款基于ARM的微型电脑主板,系统基于Linux,以MicroSD卡为磁盘,树莓派主板有4个USB接口,一个以太网接口,一个3.5mm音频口,一个HDMI视频接口,40-pin的GPIO连接端口 https://www.raspberrypi.org/products/ 二、树莓派可以干什么 可以当做一个微型服务器,作为FTP文件服务器、网站服务器、代码托管等 游戏机(刷入游戏模拟器系统,可玩大部分复古游戏)https://www.jianshu.com/p/feffa629084a 遥控车(手机通过socket连接控制小车,且小车装有红外线传感器,可以自动避障), 可以结合其他传感器搭建智能家居 三、DHT11温湿度传感器 传感器文档 价格便宜,体积小,功耗低,缺点是精度不高 有四个针脚:正极、负极、串行数据总线、空脚 DHT11温湿度传感器数据读取原理 代码: __author__ = 'DuShu' import time import RPi.GPIO as GPIO def getIntVal(binayList): idx = 0 binaryStr = '' while idx < len(binayList): binaryStr += str(binayList[idx]) idx += 1 return int(binaryStr ,2) def getHumidity(): GPIO_DATA = 18 GPIO_HIGH = GPIO.HIGH GPIO_LOW = GPIO.LOW GPIO.setmode(GPIO.BCM) #一秒后开始工作 time.sleep(1) #设置GPIO接口为输出数据模式 GPIO.setup(GPIO_DATA,GPIO.OUT) #输出一个低电平信号 GPIO.output(GPIO_DATA, GPIO.LOW) time.sleep(0.02) #0.018 #0.02秒后输出一个高电平信号,启动模块测量 GPIO.output(GPIO_DATA, GPIO.HIGH) #设置GPIO接口为读取读取数据模式 GPIO.setup(GPIO_DATA,GPIO.IN) #等待,获取到高电平信号 while GPIO.input(GPIO_DATA) == GPIO_HIGH: pass #等待,获取到低电平信号 while GPIO.input(GPIO_DATA) == GPIO_LOW: pass #获取到高底电平信号后,开始读取模块获取数据 data = [] cnt = 40 dataArr = [] while cnt > 0: cnt = cnt - 1 while GPIO.input(GPIO_DATA) == GPIO_LOW: pass k = 0 while GPIO.input(GPIO_DATA) == GPIO_HIGH: k += 1 if k > 100: break dataArr.append(k) if k < 10: data.append(0) else: data.append(1) print(dataArr) #根据公式定义,解析温湿度数据 val1 = getIntVal(data[0:8]) val2 = getIntVal(data[8:16]) val3 = getIntVal(data[16:24]) val4 = getIntVal(data[24:32]) val5 = getIntVal(data[32:40]) if (val5 == val1 + val2 + val3 + val4): return [val1, val3] else: return [-1, -1] GPIO.cleanup() if __name__ == '__main__': hum = getHumidity() print("hum:", hum[0], " temp:", hum[1]) 四、参考 https://blog.csdn.net/guyang1995/article/details/54692810 https://zhuanlan.zhihu.com/p/74440206

2020年3月20日 · Darren Ou

直播间横屏技术方案

一、背景 目前直播间已支持PC推流直播,但只支持竖屏播放,导致观看体验不佳,故需要把横屏模式加上。 二、面临的问题 1、只有两天开发时间,时间紧迫; 2、屏幕切换时存在UI异常风险; 3、横屏模式需要应用全局支持横屏方向,存在影响其他业务风险; 三、方案 1、切换横屏入口的显示(加开关) joinChannel/joinGroup成功 -> roomType == BSRoomType_PCRoom -> 判断开关 -> 显示入口 2、横竖屏切换初步方案 1.项目配置: 在AppDelegate里重写方法: - (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window { UIViewController *topViewController = [BaseViewController bl_findTopMostViewController]; if (topViewController && [topViewController isKindOfClass:NSClassFromString(@"DSLandscapeViewController")]) { return topViewController.supportedInterfaceOrientations; } return UIInterfaceOrientationMaskPortrait; } 2.竖屏转横屏: 新建一个支持横屏的DSLandscapeViewController并重写UIViewController的方法: - (UIInterfaceOrientationMask)supportedInterfaceOrientations { return UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskPortrait; // 这里加上竖屏,是因为横屏转竖屏时,需要把当前vc先转过来,后面会讲到 } - (BOOL)shouldAutorotate { return YES; } 从竖屏vc(DSLiveShowViewController)旋转时,初始化横屏vc(DSLandscapeViewController),并把竖屏vc当前持有的BLLiveRoomAudienceSession传递给横屏vc。然后在当前navigationController的子vc中,把竖屏vc替换成横屏vc(原竖屏vc会销毁)。 - (void)switchToLandscapeView { DSLandscapeViewController *viewController = [[DSLandscapeViewController alloc] initWithAudienceSession:self.audienceSession]; NSMutableArray *viewControllers = [[NSMutableArray alloc] initWithArray:self.navigationController.viewControllers]; [viewControllers removeObject:self]; [viewControllers addObject:viewController]; [self.navigationController setViewControllers:viewControllers animated:YES]; self.audienceSession = nil; } 横屏vc初始化videoView,并调用session的方法传递给mediasdk([self.audienceSession changeRemoteViewForGroup:self.videoView];) ...

2020年1月4日 · Darren Ou

iOS 项目接入 CallKit 框架

CallKit是什么 苹果在WWDC 2016发布了iOS 10的新框架CallKit,它允许开发者在VoIP类型APP整合系统原生语音界面,以获得更好的用户体验。 接入CallKit后,APP里面的通话会被写入系统通话记录,而且APP通话时的优先级比一般VoIP的APP优先级要高。 CallKit主要类 我们可以把系统提供的CallKit框架理解成一个通话的界面UI,我们做的只是把这个通话界面跟我们的应用联系起来, 比如有一个语音电话过来了,那么我们就呼起这个界面,用户在这个界面上点击了静音按钮,我们相应地做静音的处理。 同样地,如果用户在应用里面点击了静音按钮,也要把状态同步到系统的界面。 呼起CallKit & CallKit回调 CXProviderConfiguration 描述系统通话界面的标题、logo、来电铃声等 CXCallUpdate 描述来电信息,如来电用户名、是否视频聊天等 CXProvider 同步连接状态到CallKit CXProviderDelegate 用户在系统通话界面上动作的回调 应用把状态同步到系统: CXCallAction 用户不同动作的描述,如:呼出电话、接听、挂断、静音等(分别对应CXStartCallAction、CXAnswerCallAction、CXEndCallAction、CXSetMutedCallAction) CXTransaction 把用户的不同动作打包成CXTransaction再传递给系统 CXCallController 把CXTransaction传递给系统 原理 1、呼入 来电 1.CallKitTestApp接收到服务器的来电信息(后台时VoIP Push) 2.创建CXCallUpdate对象记录来电信息,并记录该通话唯一标识 3.通过CXProvider对象把来电信息通知系统 4.系统接收到来电信息后显示原生来电UI 响应来电 1.用户在原生来电UI上点击接听按钮(或挂断按钮) 2.系统把动作封装成CXAnswerCallAction,通过CXProvider的Delegate回调到CallKitTestApp 3.CallKitTestApp收到回调后会开始配置mediasdk,调整UI等操作,最后开始通话 结束来电 1.CallKitTestApp在前台时,用户点击挂断按钮 2.创建CXTransaction对象,用于包装挂断动作CXEndCallAction(包含该通话唯一标识) 3.通过CXCallController对象把挂断动作通知系统 4.系统成功挂断该通话后,通过CXProvider的Delegate回调到CallKitTestApp 5.CallKitTestApp收到回调后会开始释放mediasdk(这里要注意释放顺序,不能在系统回调前把sdk释放掉),调整UI等操作 2、呼出 1.用户在CallKitTestApp呼出电话 2.创建CXTransaction对象,用于包装呼出动作CXStartCallAction(包含该通话唯一标识、呼出号码等信息) 3.通过CXCallController对象把呼出动作通知系统 4.系统成功呼出该通话后,通过CXProvider的Delegate回调到CallKitTestApp 5.CallKitTestApp收到回调后会开始配置mediasdk,调整UI等操作,最后开始通话 以上呼出逻辑是CallKitTestApp中IP-IP电话的处理逻辑,因为CallKitTestApp里面还有自动转换IP-PSTN逻辑,涉及到mediasdk资源释放问题,导致应用不稳定,内测版中暂时把呼出部分屏蔽。 CallKitTestApp如何接入 1、mediasdk做兼容 1.系统资源申请成功后再配置mediasdk 2.等系统资源释放后再释放mediasdk 为满足以上两点,mediasdk开放了下面几个接口,在CallKit模式下,等系统回调后再做mediasdk配置、激活、释放等操作 // In CallKit, it should be called by `perform Answer/start CallAction` bool callKit_test_configAudio(MEDIA_HANDLE handle); // In CallKit, it should be Called by `didActivateAudioSession` bool callKit_test_startAudio(MEDIA_HANDLE handle); // In CallKit, it should be Called by `didDeactivateAudioSession` void callKit_test_stopAudio(MEDIA_HANDLE handle); // In CallKit, it should be Called by `performEndCallAction` void callKit_test_releaseAudio(MEDIA_HANDLE handle); 2、CallKitTestApp在前台状态 1.在CallKitTestApp项目中添加BOProviderDelegate对象,作为与CallKit通信的桥梁。 ...

2019年5月10日 · Darren Ou

多任务页面底部 banner 出现的种类

多任务页面底部banner出现的种类 1、handoff(接力) 2、xxx,早上好 3、基于您的当前位置 4、“耳机"已连接 5、(未知。。。) 实现 handoff 设置 -> 通用 -> 接力 -> 打开接力 通过“接力”功能,您在某设备开始操作后,可立即使用iCloud账号从另一设备继续相同操作。您需要的应用会显示在应用切换器和Mac的程序坞上。 这种一般应用在需要跨设备共享编辑的应用上,例如“印象笔记”,在手机上编辑了一段文字后,可以切换到Mac上继续编辑。 官方简介:https://support.apple.com/zh-cn/HT204681 这是开发者唯一能控制的底部banner种类 Siri应用建议 xxx,早上好 基于您的当前位置 “耳机”已连接 关于前两种情况: 系统会记录用户长期的使用应用的习惯,自主地向用户推荐相关的应用。例如,早上起床后我会习惯打开微博,系统早上就会给我推荐微博;来到公司我会习惯打开微信,系统也会根据地理位置给我推荐微信。系统会通过记录用户的使用频率、时间、地理位置等维度,向用户推荐应用。 关于“耳机”已连接 手机连接蓝牙耳机或者有线耳机后,系统会弹出 “耳机”已连接 的banner,如果手机装有多个音乐应用,目前观察到的情况是会弹出最常用的那个应用。但是有时候又不会弹,而且弹出维持的时间也没有规律。 国外用户如是说: This is a feature suggesting which app you normally use in a certain situation. Like location, time of day, headphones or Bluetooth connected. Phone tries to learn if you always do something. Like I always open maps when I connect to my car Bluetooth. Or always open music when by headphones are plugged in. Or always open weather at 7am. ...

2018年11月12日 · Darren Ou