[{"content":"WWDC16 照片和相机 iOS 摄影功能改进 人们喜欢用 iPhone 拍照。实际上，它是全世界广受欢迎的相机，而摄影类 app 能够强化这一体验。探索新的 AVFoundation Capture API，它们可以用来拍摄实况照片、从相机提取 RAW 图像数据以及拍摄广色域照片。\nAVCapturePhotoOutput - 高级知识 本讲座是“讲座 501：iOS 摄影功能改进”的延续，将详细介绍 AVFoundation 中新增的强大 AVCapturePhotoOutput API 的场景监控和资源管理功能。\nWWDC17 照片和相机 在 iPhone 摄影中获取景深 iPhone 7 Plus 上的人像模式展现了摄影中景深的力量。在 iOS 11 中，您的 app 现在也可以使用驱动这项功能的景深数据。了解如何利用景深来为创意图像带来新的可能。更深刻地理解高级景深概念，并学习如何从相机获取流媒体和静态图像景深数据。\nWWDC19 照片和相机 iOS 多相机拍摄简介 现在可以通过 iOS 13 上的 AVCapture，从 iPhone XS、iPhone XS Max、iPhone XR 和最新的 iPad Pro 上的多个摄像头同时拍摄照片和视频。也可以配置设备上的多个麦克风来打造捕捉到的声音。了解如何利用这些强大的功能来为您的相机 app 增添富有创意的新功能，比如画中画和空间音频等。更深入地了解可能影响 app 设计的性能注意事项。\n照片分割蒙版简介 iOS 12 上使用人像模式拍摄的照片包含一个嵌入式人像分割蒙版，这样可以轻松地打造背景替换等富有创意的视觉效果。iOS 13 利用设备端机器学习来为拍摄的所有照片提供新的分割蒙版。了解您可从 AVCapture 和 Core Image 获得的新语义分割蒙版，以便分离人物的头发、皮肤和牙齿。您的 app 现能通过单独使用其中任一蒙版或结合使用所有蒙版，提供大量的照片编辑控制功能。\n相机拍摄和照片分割功能改进 AVCapture API 中新增的强大功能可让您同时从多个相机拍摄照片和视频。照片现在可以从语义分割中获益，您可以借助这项功能来分离照片中的头发、皮肤和牙齿。了解如何利用这些增强功能来创建出色的相机 app，并轻松实现惊艳的照片效果。\nWWDC21 照片和相机 使用视频格式拍摄高质量照片 通过使用 AVCapture API，您的 app 可以充分利用 iPhone 强大的相机系统。了解如何为您的用例选择最合适的照片或视频格式，同时权衡照片质量和成像速度。探究一些强大的新算法，如果您在 app 中使用视频格式，这些算法可以帮助您显著提高照片质量。要了解有关 AVCapture 改进情况的更多信息，另请务必观看“摄像头捕捉新功能”视频。\n探索使用 VideoToolbox 的低延迟视频编码 支持低延迟编码器已成为视频应用开发过程的重要部分。探究 VideoToolbox 如何支持低延迟 H.264 硬件编码，以最小化端到端延迟，达到新的性能水平，实现极佳的实时通信和高质量的视频播放。\n相机拍摄的新功能 了解如何在控制中心中与 Video Effects 交互，包括 Center Stage、Portrait 模式和 Mic 模式。我们将展示如何检测何时为您的 app 启用了这些功能，并探索采用自定义界面以便可从您的 app 控制它们的方式。了解如何启用 10 位 HDR 视频捕捉，并利用最小焦距报告功能，改进视频捕捉体验。探索针对 IOSurface 压缩和提供优异的摄像头捕捉性能的支持。为了解关于摄像头捕捉的更多信息，我们还建议观看 WWDC21 的“捕捉使用视频格式的高质量照片”。\n音频和视频 AVFoundation 的新功能 探索 AVFoundation、Apple 框架用于检查、播放和创作视听内容的最新更新。我们将探索如何使用 AVFoundation 查询视听资源属性，使用定时元数据进一步定制您的自定义视频作品，并创作字幕文件。\nWWDC22 照片和相机 集成连续互通相机至您的 macOS App 中 了解如何借助连续互通相机，在任何 Mac App 中将 iPhone 设置为外部相机。不管您是在开发视频会议软件，或是在打造以创新方式使用相机的体验，您都将学习到如何利用自动相机切换来优化您的 App。我们还将探索如何识别用户偏好和系统偏好相机，带您了解从 iPhone 视频流捕捉高分辨率优质照片的 API，以及更多内容。要进一步了解相机拍摄，请随后观看 WWDC22 的“了解 iOS 相机拍摄的改进”。\n了解 iOS 相机拍摄的改进：深度、聚焦和多任务处理 探索如何利用 App 中的高级相机拍摄功能。我们将介绍如何使用激光雷达扫描仪创建照片和视频效果，并执行准确的深度测量。了解您的 App 如何利用相机的画中画或多任务处理，并在相机拍摄期间控制面部自动聚焦和自动曝光等。我们还将分享使用多个视频输出的策略，让您在捕捉优质视频输出的同时优化实时预览。如需了解有关相机拍摄功能的概述，请观看 WWDC21 的“相机拍摄的新功能”。\n利用 RoomPlan 创建参数 3D 空间扫描 RoomPlan 可以帮助您的 App 快速为空间创建简易版参数 3D 扫描。了解如何利用此 API 轻松添加空间扫描体验。我们还将介绍如何采用此 API，探索 3D 参数输出，并分享最佳实践以帮助您在每次扫描时得到理想的结果。\n在您的 App 中添加“实时文本”交互 学习如何在您的 App 中添加对静态照片或暂停视频帧的“实时文本”支持。我们将分享如何在 iOS、iPadOS 或 macOS 的任何图像视图中轻松启用文本交互、翻译、数据检测和二维码扫描。我们还将讨论如何控制交互类型，管理补充界面，并解决潜在的手势冲突。要进一步了解如何捕捉实时摄像头源中检测到的数据并与其进行交互，请观看 WWDC22 的“利用 VisionKit 捕捉机器可读的代码和文本”。\n探索 PhotoKit 的变更记录 PhotoKit 可以帮助您构建丰富的以照片为中心的功能。了解如何借助 PhotoKit 中的最新 API 轻松追踪对图片素材的变更。我们将介绍 PHPhotoLibrary 变更记录 API 并演示如何在每次启动时保留变更令牌，以帮助您的 App 识别用户照片图库新增和删除的内容，以及对照片图库的更新。要进一步了解照片图库集成，请务必观看 WWDC22 的“照片挑选器的新增内容”和 WWDC21 的“优化 App 中的照片访问”。\n音频和视频 借助 Core Image、Metal 和 SwiftUI 显示 EDR 内容 探索如何借助基于 Core Image 的多平台 SwiftUI App 在扩展动态范围 (EDR) 中进行渲染，并为其提供支持。我们将简要介绍使用 ViewRepresentable 向 MTKView 显示 CIImages 的最佳实践。我们还将分享启用 EDR 渲染的简单步骤，并探索 150 多种支持 EDR 的内置 CIFilter 中的一部分。\n利用 AVFoundation 和 Metal 在 EDR 中显示 HDR 视频 了解如何利用 AVFoundation 和 Metal 来构建高效的 EDR 管道。跟着我们一起学习如何使用 AVPlayer 显示具有 EDR 的 HDR 视频，在 App 视图中添加播放功能，使用 Metal 进行渲染，以及使用 Core Image 或自定义 Metal 着色器添加视频效果，如抠像或颜色管理等。无论您在开发游戏还是专业 App，我们都将帮助您确定要使用哪些框架，分享选择传输、颜色空间和像素缓冲区格式的最佳实践。\n探索 iOS 上的 EDR EDR 是 Apple 的高动态范围表示和渲染管道。探索如何借助 App 中的 EDR 渲染 HDR 内容，并在 iPhone 和 iPad 上充分挖掘 HDR 显示屏的动态范围功能。我们将介绍如何利用 iOS 上的原生 EDR API，提供最佳实践以帮助您确定使用 HDR 的正确时机，以及分享有关色调映射和 HDR 内容渲染的技巧。我们还将讨论参考模式，重点说明它会如何提供参考响应以启用颜色关键型工作流程，如调色、编辑和内容审核等。\n构建更快速响应的媒体 App 了解如何利用 AVFoundation 让用户专注于您的媒体 App 的内容，而不用盯着加载动画长时间等待。我们将介绍如何在您的 App 中提供响应快速且运行流畅的界面，帮助您创建丰富的音视频复合内容、加载音视频素材以及制作媒体缩略图。学习如何在您的 App 的主线程上执行这些操作并同时并行运行 I/O 处理，探索如何在加载自定义存储等数据时实现一流的播放性能。为能更好地理解此讲座，我们建议您先观看 WWDC21 的“AVFoundation 的新功能”。\nWWDC23 照片和相机 打造更流畅的相机体验 了解 AVCapture 和 PhotoKit 如何帮助你打造响应更快、更令人愉悦的 App。了解相机捕捉过程，以及延迟照片处理如何帮助创建最佳质量的照片。我们将向你展示零快门延迟如何利用 Time Travel 来捕捉完美的动作照片，深入介绍构建响应式捕获流程，并分享如何利用视频特效 API 来识别触发实时视频效果的预定义手势。\n在 iPadOS App 中支持外部摄像头 了解如何使用 AVFoundation 捕获类在 iPadOS App 中发现并连接到外部摄像头。我们将向你展示如何旋转来自外部和内置摄像头的视频、支持具有 USB-C 端口的外部麦克风以及执行音频路由。探索电话支持、优化回声消除的调整以及外部摄像头采用的最佳实践。\n在你的 App 中支持 HDR 图像 了解如何在你的 App 中识别、加载、显示和创建高动态范围（HDR）静态图像。探索常见的 HDR 概念，了解 ISO 规范的最新更新。了解如何使用 SwiftUI 和 UIKit 识别和显示 HDR 图像，如何从 ProRAW 和 RAW 捕获中创建 HDR 图像并在 CALayer 中显示。我们还将带你了解 CoreGraphics 对 ISO HDR 的支持，并分享采用 HDR 技术的最佳实践。\n将照片选择器嵌入你的 App 中 了解如何简单、安全、可靠地访问 App 中的照片库。了解如何开始使用嵌入式选择器并探索选项菜单和高动态范围静态图像支持。我们还将向你展示如何利用 UI 自定义选项来帮助选择器融入你现有的界面。\n音频和视频 在你的 App 中支持电影效果模式的视频 了解电影效果相机 API 如何帮助你的 App 处理在相机 App 中拍摄的电影效果模式视频。我们将分享电影效果模式视频的基础知识，包括对焦决策层级，向你展示如何在 App 中使用和更新对焦决策，并帮助你保存和加载这些更改。\nWWDC24 照片和相机 打造出色的锁定屏幕相机拍摄体验 了解 LockedCameraCapture API 如何帮你将拍摄应用程序最有用的信息直接整合到锁定屏幕。研究这个 API 的特性和功能，了解如何开始构建拍摄扩展，并探索此扩展在设备锁定时的运行情况。\n音频和视频 利用 HDR 为 App 打造动态图像体验 探索如何在 App 中完成 HDR 图像的读写操作，以及处理 HDR 内容。探究新增的受支持 HDR 图像格式，以及多种显示 HDR 图像的先进方法。了解 HDR 内容可以如何与你的用户界面实现共存，以及为 App 添加 HDR 图像支持时有哪些注意事项。\n探索 AVFoundation 中的媒体性能指标 探索如何利用新的媒体性能 API 来监测、分析并改进用户体验。了解如何使用不同的 AVMetricEvents 来监测音视频播放器在 HLS 素材方面的性能，并学习如何利用这些指标来理解播放器性能问题，以及进行相关分类。\n","permalink":"https://blog.axqora.com/posts/ios-wwdc-camera-feature/","summary":"\u003ch1 id=\"wwdc16\"\u003eWWDC16\u003c/h1\u003e\n\u003ch2 id=\"照片和相机\"\u003e照片和相机\u003c/h2\u003e\n\u003ch3 id=\"ios-摄影功能改进\"\u003e\u003ca href=\"https://developer.apple.com/cn/videos/play/wwdc2016/501/\"\u003eiOS 摄影功能改进\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e人们喜欢用 iPhone 拍照。实际上，它是全世界广受欢迎的相机，而摄影类 app 能够强化这一体验。探索新的 AVFoundation Capture API，它们可以用来拍摄实况照片、从相机提取 RAW 图像数据以及拍摄广色域照片。\u003c/p\u003e\n\u003ch3 id=\"avcapturephotooutput---高级知识\"\u003e\u003ca href=\"https://developer.apple.com/cn/videos/play/wwdc2016/511/\"\u003eAVCapturePhotoOutput - 高级知识\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e本讲座是“讲座 501：iOS 摄影功能改进”的延续，将详细介绍 AVFoundation 中新增的强大 AVCapturePhotoOutput API 的场景监控和资源管理功能。\u003c/p\u003e\n\u003ch1 id=\"wwdc17\"\u003eWWDC17\u003c/h1\u003e\n\u003ch2 id=\"照片和相机-1\"\u003e照片和相机\u003c/h2\u003e\n\u003ch3 id=\"在-iphone-摄影中获取景深\"\u003e\u003ca href=\"https://developer.apple.com/cn/videos/play/wwdc2017/507/\"\u003e在 iPhone 摄影中获取景深\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eiPhone 7 Plus 上的人像模式展现了摄影中景深的力量。在 iOS 11 中，您的 app 现在也可以使用驱动这项功能的景深数据。了解如何利用景深来为创意图像带来新的可能。更深刻地理解高级景深概念，并学习如何从相机获取流媒体和静态图像景深数据。\u003c/p\u003e\n\u003ch1 id=\"wwdc19\"\u003eWWDC19\u003c/h1\u003e\n\u003ch2 id=\"照片和相机-2\"\u003e照片和相机\u003c/h2\u003e\n\u003ch3 id=\"ios-多相机拍摄简介\"\u003e\u003ca href=\"https://developer.apple.com/cn/videos/play/wwdc2019/249/\"\u003eiOS 多相机拍摄简介\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e现在可以通过 iOS 13 上的 AVCapture，从 iPhone XS、iPhone XS Max、iPhone XR 和最新的 iPad Pro 上的多个摄像头同时拍摄照片和视频。也可以配置设备上的多个麦克风来打造捕捉到的声音。了解如何利用这些强大的功能来为您的相机 app 增添富有创意的新功能，比如画中画和空间音频等。更深入地了解可能影响 app 设计的性能注意事项。\u003c/p\u003e\n\u003ch3 id=\"照片分割蒙版简介\"\u003e\u003ca href=\"https://developer.apple.com/cn/videos/play/wwdc2019/260/\"\u003e照片分割蒙版简介\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eiOS 12 上使用人像模式拍摄的照片包含一个嵌入式人像分割蒙版，这样可以轻松地打造背景替换等富有创意的视觉效果。iOS 13 利用设备端机器学习来为拍摄的所有照片提供新的分割蒙版。了解您可从 AVCapture 和 Core Image 获得的新语义分割蒙版，以便分离人物的头发、皮肤和牙齿。您的 app 现能通过单独使用其中任一蒙版或结合使用所有蒙版，提供大量的照片编辑控制功能。\u003c/p\u003e","title":"历年 WWDC 音视频相关内容整理"},{"content":"一、工具集 TrollStore TrollStore 利用了 iOS 的 AMFI/CoreTrust 漏洞，允许在非越狱的手机上安装自签名的 ipa，并使通过 TrollStore 安装的应用程序能获得更高的权限（如获取手机内的应用列表等）。\n版本限制：14.0 beta 2 - 16.6.1, 16.7 RC (20H18), 17.0\n根据指引在手机上安装 TrollStore：安装指引\nTrollFools TrollFools 通过 TrollStore 安装手机上，可以对手机上的应用进行动态库注入和重签名。\n关键逻辑在 Injector.swift 文件中，原理大致如下：\n对所有的 MachO 文件（包含主程序和注入的动态库）使用 Idid 进行重签名、使用 ChOma 绕过 CoreTrust、使用 chown 修改权限； 使用 install_name_tool 注入依赖的系统框架，路径为@executable_path/Frameworks； 使用 insert_dylib 注入 LC_LOAD_DYLIB； 注入完成后对 MachO 文件再进行第 1 步。 Theos 和 Tweak Theos 是一个强大的开源开发工具链，广泛用于开发 iOS 越狱插件（tweaks）。这些插件可以修改和扩展应用程序的功能。\n二、Theos 1、环境 // 项目安装 sudo git clone --recursive https://github.com/theos/theos.git /opt/theos // 配置环境变量 export THEOS=/opt/theos export PATH=/opt/theos/bin/:$PATH 环境变量添加到 ~/.zshrc 后需要执行 source ~/.zshrc或者新开终端窗口。\n2、创建项目 运行 $THEOS/bin/nic.pl 可以运行 theos 的项目创建程序，这里选择 19 创建一个 OC 应用的插件，然后按提示输入信息即可。\n3、代码编写 编辑 Tweak.x 文件，添加以下代码\n#import \u0026lt;AVFoundation/AVFoundation.h\u0026gt; %hook AVCaptureVideoDataOutput - (void)_processSampleBuffer:(CMSampleBufferRef)buffer { %orig(buffer); NSLog(@\u0026#34;delegate: %@ _processSampleBuffer: %@\u0026#34;, [self sampleBufferDelegate], buffer); } %end %hook AVCaptureFigVideoDevice - (void)setVideoZoomFactor:(CGFloat)factor { %orig(factor); NSLog(@\u0026#34;Device: %@, Video zoom factor was set to: %f\u0026#34;, self, factor); } %end 这段代码使用了 Logos 语法 hook 了相机数据输出的方法和设置 Zoom 的接口。\n4、引用第三方库 FLEX_ROOT = libs/FLEX # Function to convert /foo/bar to -I/foo/bar dtoim = $(foreach d,$(1),-I$(d)) # Gather FLEX sources SOURCES = $(shell find $(FLEX_ROOT)/Classes -name \u0026#39;*.c\u0026#39;) SOURCES += $(shell find $(FLEX_ROOT)/Classes -name \u0026#39;*.m\u0026#39;) SOURCES += $(shell find $(FLEX_ROOT)/Classes -name \u0026#39;*.mm\u0026#39;) # Gather FLEX headers for search paths _IMPORTS = $(shell /bin/ls -d $(FLEX_ROOT)/Classes/*/) _IMPORTS += $(shell /bin/ls -d $(FLEX_ROOT)/Classes/*/*/) _IMPORTS += $(shell /bin/ls -d $(FLEX_ROOT)/Classes/*/*/*/) _IMPORTS += $(shell /bin/ls -d $(FLEX_ROOT)/Classes/*/*/*/*/) IMPORTS = -I$(FLEX_ROOT)/Classes/ $(call dtoim, $(_IMPORTS)) $(TWEAK_NAME)_CFLAGS += $(IMPORTS) -g $(TWEAK_NAME)_CCFLAGS += -std=gnu++11 目前我以源码方式引入第三方库。把第三方库的源文件放到 libs 中，在 Makefile 中添加以上代码。\nTODO：Theos 支持以动态库或静态库的方式引入第三方库，因本文的前提是非越狱手机，所以这里应该使用静态库的方式引入，并编译到输出的文件中。\n5、编译 编辑 Makefile 文件，添加以下代码，表示项目依赖 AVFoundation 框架。\nTestTweak_FRAMEWORKS = AVFoundation 在项目文件夹直接执行 make。\n如果是越狱手机，可以运行 make do 把插件编译后直接安装到手机上，当然也需要其它的配置，这里先不讨论越狱手机的插件开发。\n6、安装 arm64 架构的编译产物在路径 .theos/obj/debug/arm64/TestTweak.dylib 中。\n把编译产物导入到手机（可用 AirDrop 或数据线导入，存到手机的 Files 中）。\n打开已经安装的 TrollFools 应用，选择想要注入动态库的应用（如 BigoLive），选择需要注入的动态库（TestTweak.dylib）。\n注入成功后，打开 BigoLive 开始直播，这时候可以从 Mac 上的 Console 查看到相关的打印信息。\n7、Demo https://github.com/oudushu/DSHookTweak\n该 Demo 已实现：\n摇一摇呼出选择列表； 内置 FLEX 工具集； 内置 LookinServer，可使用 macOS 端的 Lookin 客户端快速查看应用的图层； 日志写文件，并可把文件通过 AirDrop 分享出来； 相机相关 hook。 三、总结 我们可以利用以上的工具，做到给非越狱手机安装插件，从而方便地 hook 从 App Store 安装的应用程序。\n可以开发通用的插件，做到一个插件适用不同的应用，提高逆向的效率。\n缺点：\n非越狱手机上无法调试； 对 iOS 版本有限制，太新的版本暂不支持。 ","permalink":"https://blog.axqora.com/posts/ios-tweak-dev/","summary":"\u003ch1 id=\"一工具集\"\u003e一、工具集\u003c/h1\u003e\n\u003ch2 id=\"trollstore\"\u003eTrollStore\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/opa334/TrollStore\"\u003eTrollStore\u003c/a\u003e 利用了 iOS 的 AMFI/CoreTrust 漏洞，允许在非越狱的手机上安装自签名的 ipa，并使通过 TrollStore 安装的应用程序能获得更高的权限（如获取手机内的应用列表等）。\u003c/p\u003e\n\u003cp\u003e版本限制：14.0 beta 2 - 16.6.1, 16.7 RC (20H18), 17.0\u003c/p\u003e\n\u003cp\u003e根据指引在手机上安装 TrollStore：\u003ca href=\"https://ios.cfw.guide/installing-trollstore/\"\u003e安装指引\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"trollfools\"\u003eTrollFools\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/Lessica/TrollFools\"\u003eTrollFools\u003c/a\u003e 通过 TrollStore 安装手机上，可以对手机上的应用进行动态库注入和重签名。\u003c/p\u003e\n\u003cp\u003e关键逻辑在 \u003ccode\u003eInjector.swift\u003c/code\u003e 文件中，原理大致如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e对所有的 MachO 文件（包含主程序和注入的动态库）使用 Idid 进行重签名、使用 \u003ca href=\"https://github.com/opa334/ChOma\"\u003eChOma\u003c/a\u003e 绕过 CoreTrust、使用 chown 修改权限；\u003c/li\u003e\n\u003cli\u003e使用 install_name_tool 注入依赖的系统框架，路径为\u003ccode\u003e@executable_path/Frameworks\u003c/code\u003e；\u003c/li\u003e\n\u003cli\u003e使用 \u003ca href=\"https://github.com/tyilo/insert_dylib\"\u003einsert_dylib\u003c/a\u003e 注入 LC_LOAD_DYLIB；\u003c/li\u003e\n\u003cli\u003e注入完成后对 MachO 文件再进行第 1 步。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"theos-和-tweak\"\u003eTheos 和 Tweak\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/theos/theos\"\u003eTheos\u003c/a\u003e 是一个强大的开源开发工具链，广泛用于开发 iOS 越狱插件（tweaks）。这些插件可以修改和扩展应用程序的功能。\u003c/p\u003e\n\u003ch1 id=\"二theos\"\u003e二、Theos\u003c/h1\u003e\n\u003ch2 id=\"1环境\"\u003e1、环境\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// 项目安装\nsudo git clone --recursive https://github.com/theos/theos.git /opt/theos\n\n// 配置环境变量\nexport THEOS=/opt/theos\nexport PATH=/opt/theos/bin/:$PATH\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e环境变量添加到 \u003ccode\u003e~/.zshrc\u003c/code\u003e 后需要执行 \u003ccode\u003esource ~/.zshrc\u003c/code\u003e或者新开终端窗口。\u003c/p\u003e","title":"iOS Tweak 插件开发及非越狱安装"},{"content":"一、背景 iPhone 的大部分机器都提供了后置双摄像头，包含广角和超广角（某些机型上是广角和长焦），Pro 和 Pro Max 机型上提供了后置三摄像头，包含广角、超广角和长焦。\n我们目前只用到了广角，了解虚拟摄像头设备挖掘更多的使用场景。\n二、简介 1、采集设备类型 iOS 或 macOS 上提供的逻辑摄像头如下：\nAVCaptureDeviceTypeBuiltInWideAngleCamera：广角摄像头（默认，约 28mm 焦段）。 AVCaptureDeviceTypeBuiltInTelephotoCamera：长焦摄像头（2x 或 3x 光学变焦）。 AVCaptureDeviceTypeBuiltInUltraWideCamera：超广角摄像头（约 0.5x 视野扩展），iPhone 13 以上机器提供了微距能力。 AVCaptureDeviceTypeBuiltInDualCamera：双摄像头（广角 + 长焦，如 iPhone X）。 AVCaptureDeviceTypeBuiltInDualWideCamera：双摄像头（广角 + 超广角，如 iPhone 15）。 AVCaptureDeviceTypeBuiltInTripleCamera：三摄像头（超广角 + 广角 + 长焦，如 iPhone 15 Pro Max）。 AVCaptureDeviceTypeBuiltInTrueDepthCamera：前置原深感摄像头（支持 Face ID 的设备）。 AVCaptureDeviceTypeBuiltInLiDARDepthCamera：LiDAR 深度摄像头，用于增强现实和测量。 AVCaptureDeviceTypeContinuityCamera：连续互通相机，用于跨设备视频功能。 AVCaptureDeviceTypeDeskViewCamera：从超广角摄像头上剪下来的经过畸变校正的图像，使其近似于指向桌子的架空摄像机，用于演示和直播（只支持 macOS）。 以上类型只能通过 AVCaptureDeviceDiscoverySession 使用。\n2、虚拟摄像头设备 虚拟摄像头设备（Virtual Device）提供了一种抽象层，代表一个包含多个物理摄像头的虚拟摄像头设备，使得开发者可以通过一个单一的 AVCaptureDevice 来访问和控制多个物理摄像头。\n当在操作虚拟设备时，iOS 会在 constituentDevices 之间自动切换，以提供一致的用户体验。\niPhone 15 Pro 里，以下设备都属于虚拟设备：\n前置原深感摄像头 后置双镜头 后置双广角镜头 后置三镜头 后置激光雷达景深相机 下面主要讨论后置双镜头，后置双广角镜头，后置三镜头。\n自动切换需要具备的条件是：变焦系数、光照水平、焦点位置在系统要求的范围内。\n变焦系数 系统会根据当前的变焦系数自动选择最适合的摄像头以保持最佳的图像质量。\n假如在使用 iPhone 15 Pro（带有广角、超广角和长焦摄像头），从 1x 变焦逐渐增加到 3x 变焦时，系统可能会从广角摄像头切换到长焦摄像头，因为长焦摄像头更适合拍摄远距离对象而不会降低图像质量。\n光照水平 系统会根据当前环境光照水平决定使用哪一个摄像头。\n在较暗的环境中，系统可能会选择使用广角摄像头而非长焦摄像头，因为广角摄像头通常具有更大的光圈和更好的低光性能。\n焦点位置 系统会根据焦点位置判断哪个摄像头能够更好地对焦并提供最佳图像质量。\n当焦点位置接近镜头，系统可能会选择使用超广角摄像头以获得更好的微距能力；如果焦点位置远离镜头，系统可能会切换到长焦摄像头以捕捉更远处的细节而不会丢失画面细节。\n三、应用场景 1、场景 提供光学变焦能力； 提供微距能力； 提供更好的对焦能力（解决在 iPhone 15 系列 PC 直播扫码不聚焦的问题）； 2、竞品的情况 在 iPhone 11 上通过 hook 抖音，可以看到切换到后置相机时，使用的是 AVCaptureDeviceTypeBuiltInDualWideCamera。\n缩放系数设置 1.0 后马上设置为 2.0（这里用的是超广角和广角组合的双摄像头设备，所以相当于是设置了 0.5x 的缩放系数，然后再设置 1x 的缩放系数）。\n四、系统接口 // 指示设备是否为由多个物理设备组成的虚拟设备。 @property(nonatomic, readonly, getter=isVirtualDevice) BOOL virtualDevice; // 构成虚拟设备的物理摄像头数组。如果设备不是虚拟设备，则返回空数组。 @property(nonatomic, readonly) NSArray\u0026lt;AVCaptureDevice *\u0026gt; *constituentDevices; // 虚拟设备在变焦到某一倍数或更高时可能切换到下一个物理设备的变焦系数数组。非虚拟设备返回空数组。 @property(nonatomic, readonly) NSArray\u0026lt;NSNumber *\u0026gt; *virtualDeviceSwitchOverVideoZoomFactors; // 设置摄像头切换行为和限制条件。 - (void)setPrimaryConstituentDeviceSwitchingBehavior:(AVCapturePrimaryConstituentDeviceSwitchingBehavior)switchingBehavior restrictedSwitchingBehaviorConditions:(AVCapturePrimaryConstituentDeviceRestrictedSwitchingBehaviorConditions)restrictedSwitchingBehaviorConditions; // 设备的当前摄像头切换行为。支持键值观察。 @property(nonatomic, readonly) AVCapturePrimaryConstituentDeviceSwitchingBehavior primaryConstituentDeviceSwitchingBehavior; // 当前摄像头切换行为的限制条件。默认设置为无条件限制，支持键值观察。 @property(nonatomic, readonly) AVCapturePrimaryConstituentDeviceRestrictedSwitchingBehaviorConditions primaryConstituentDeviceRestrictedSwitchingBehaviorConditions; // 当前激活的摄像头切换行为。对于非虚拟设备，返回“不支持”。支持键值观察。 @property(nonatomic, readonly) AVCapturePrimaryConstituentDeviceSwitchingBehavior activePrimaryConstituentDeviceSwitchingBehavior; // 当前激活的摄像头切换限制条件。对于非虚拟设备，返回“无条件限制”。支持键值观察。 @property(nonatomic, readonly) AVCapturePrimaryConstituentDeviceRestrictedSwitchingBehaviorConditions activePrimaryConstituentDeviceRestrictedSwitchingBehaviorConditions; // 当前激活的主要摄像头设备。非虚拟设备或未运行的虚拟设备返回 nil。支持键值观察。 @property(nonatomic, readonly, nullable) AVCaptureDevice *activePrimaryConstituentDevice; // 可作为主要摄像头设备的备用摄像头数组。 @property(nonatomic, readonly) NSArray\u0026lt;AVCaptureDevice *\u0026gt; *supportedFallbackPrimaryConstituentDevices; // 当主要摄像头设备受限时，可用作备用的摄像头设备数组。 @property(nonatomic) NSArray\u0026lt;AVCaptureDevice *\u0026gt; *fallbackPrimaryConstituentDevices; 五、代码修改 1、设备选择策略 现有的逻辑会把几乎所有类型的设备都获取出来，但是真正打开相机时只会选择第一个设备，即只会使用广角镜头。而在双摄场景中，则只会获取广角镜头。\n精简获取设备的类型，优化设备获取及选择策略。\n选择策略：优先选择使用虚拟摄像头设备，没有虚拟设备时使用广角摄像头作为兜底设备。 虚拟摄像头设备优先级：TripleCamera \u0026gt; DualWideCamera \u0026gt; DualCamera， 拥有后置三摄的设备可以获取到 TripleCamera，拥有后置双摄的设备只可能获取 DualWideCamera 或 DualCamera 其中的一个。\n2、Zoom 相关改动 a. 缩放系数 iPhone 15 Pro 中能获得最大 0.5x 的视野扩展，但是在设置虚拟设备的 videoZoomFactor 时不能直接设置为 0.5，因为即使是包含超广角摄像头的虚拟设备中，最小的缩放系数依然为 1.0。\n这里我们需要通过 virtualDeviceSwitchOverVideoZoomFactors 接口获取物理设备的变焦系数数组，对实际设置的缩放系数做一个转换：\nif (device.constituentDevices.count \u0026gt; 0) { NSArray\u0026lt;NSNumber *\u0026gt; *zoomFactorsNS = device.virtualDeviceSwitchOverVideoZoomFactors; NSMutableArray\u0026lt;NSNumber *\u0026gt; *zoomFactors = [NSMutableArray arrayWithObject:@(1)]; for (NSNumber *zoomFactor in zoomFactorsNS) { [zoomFactors addObject:@(zoomFactor.floatValue)]; } NSUInteger mainIndex = [device.constituentDevices indexOfObjectPassingTest:^BOOL(AVCaptureDevice * _Nonnull device, NSUInteger idx, BOOL * _Nonnull stop) { return [device.deviceType isEqualToString:AVCaptureDeviceTypeBuiltInWideAngleCamera]; }]; float mainZoomFactor = zoomFactors[mainIndex].floatValue; NSMutableArray\u0026lt;NSNumber *\u0026gt; *factors = [NSMutableArray arrayWithCapacity:zoomFactors.count]; for (NSNumber *zoomFactor in zoomFactors) { [factors addObject:@(zoomFactor.floatValue / mainZoomFactor)]; } targetFactor = (CGFloat)pam-\u0026gt;value * mainZoomFactor; } b. 缩放动画 原来只会使用 setVideoZoomFactor: 来设置缩放系数，客户端需要提供按钮直接设置缩放系数，点击按钮时画面缩放需要有动画。\n新增系统接口调用：\n// 设置缩放动画的速度 - (void)rampToVideoZoomFactor:(CGFloat)factor withRate:(float)rate; // 是否在缩放动画中 @property(nonatomic, readonly, getter=isRampingVideoZoom) BOOL rampingVideoZoom; 3、客户端 Zoom 相关修改 客户端目前的后摄的缩放系数写死了范围为 [1, 4]，可跟竞品对齐修改为 [0.5, 10]，并可提供与竞品类似的按钮直接设置缩放系数，点击按钮时画面缩放需要有动画。\n为此，SDK 提供获取物理设备的变焦系数数组\n六、其它 1、注意点 虚拟摄像头设备不支持的功能：\n自定义曝光模式：该设备不支持 AVCaptureExposureModeCustom 模式，也不支持手动曝光包围功能。这意味着你无法手动控制曝光参数来进行精确的曝光调整。 锁定非当前镜头位置的焦点：设备不支持在镜头位置不为当前状态时锁定焦点。这意味着你无法强制设备在非当前镜头位置时保持对焦锁定。 锁定非当前状态的自动白平衡：设备不支持在白平衡增益不为当前状态时锁定自动白平衡。这限制了在复杂光照条件下进行精确白平衡调整的能力。 以上功能我们都没有使用到，所以不会有影响。\n另外，即使在锁定状态下，当设备从一台相机切换到另一台相机时，曝光时间、ISO、光圈、白平衡增益或镜头位置也可能发生变化。\n但是系统会尽量保证整体曝光、白平衡和焦点位置是一致的。\n2、设置相机参数的影响 帧率 无影响\n分辨率 设置采集分辨率后缩放系数会重置为 1.0，这里已处理为设置采集分辨率前的缩放系数\nZoom 已经设置了 Zoom 时，系统自动切换相机后，会维持该缩放系数\nEV EV 设置太高时，系统自动切换相机会变得不灵敏。\n这里需要监听场景变化，场景变化后需要重置对焦点（但不重置 EV）\n3、性能 在本地测试中，使用虚拟摄像头设备后 CPU、GPU、功耗等指标均无劣化。\n","permalink":"https://blog.axqora.com/posts/ios-virtual-camera-device/","summary":"\u003ch1 id=\"一背景\"\u003e一、背景\u003c/h1\u003e\n\u003cp\u003eiPhone 的大部分机器都提供了后置双摄像头，包含广角和超广角（某些机型上是广角和长焦），Pro 和 Pro Max 机型上提供了后置三摄像头，包含广角、超广角和长焦。\u003c/p\u003e\n\u003cp\u003e我们目前只用到了广角，了解虚拟摄像头设备挖掘更多的使用场景。\u003c/p\u003e\n\u003ch1 id=\"二简介\"\u003e二、简介\u003c/h1\u003e\n\u003ch2 id=\"1采集设备类型\"\u003e1、采集设备类型\u003c/h2\u003e\n\u003cp\u003eiOS 或 macOS 上提供的逻辑摄像头如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAVCaptureDeviceTypeBuiltInWideAngleCamera：广角摄像头（默认，约 28mm 焦段）。\u003c/li\u003e\n\u003cli\u003eAVCaptureDeviceTypeBuiltInTelephotoCamera：长焦摄像头（2x 或 3x 光学变焦）。\u003c/li\u003e\n\u003cli\u003eAVCaptureDeviceTypeBuiltInUltraWideCamera：超广角摄像头（约 0.5x 视野扩展），iPhone 13 以上机器提供了\u003cstrong\u003e微距能力\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003eAVCaptureDeviceTypeBuiltInDualCamera：双摄像头（广角 + 长焦，如 iPhone X）。\u003c/li\u003e\n\u003cli\u003eAVCaptureDeviceTypeBuiltInDualWideCamera：双摄像头（广角 + 超广角，如 iPhone 15）。\u003c/li\u003e\n\u003cli\u003eAVCaptureDeviceTypeBuiltInTripleCamera：三摄像头（超广角 + 广角 + 长焦，如 iPhone 15 Pro Max）。\u003c/li\u003e\n\u003cli\u003eAVCaptureDeviceTypeBuiltInTrueDepthCamera：前置原深感摄像头（支持 Face ID 的设备）。\u003c/li\u003e\n\u003cli\u003eAVCaptureDeviceTypeBuiltInLiDARDepthCamera：LiDAR 深度摄像头，用于增强现实和测量。\u003c/li\u003e\n\u003cli\u003eAVCaptureDeviceTypeContinuityCamera：\u003ca href=\"https://support.apple.com/zh-cn/102332\"\u003e连续互通相机\u003c/a\u003e，用于跨设备视频功能。\u003c/li\u003e\n\u003cli\u003eAVCaptureDeviceTypeDeskViewCamera：从超广角摄像头上剪下来的经过畸变校正的图像，使其近似于指向桌子的架空摄像机，用于演示和直播（只支持 macOS）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e以上类型只能通过 \u003ccode\u003eAVCaptureDeviceDiscoverySession\u003c/code\u003e 使用。\u003c/p\u003e\n\u003ch2 id=\"2虚拟摄像头设备\"\u003e2、虚拟摄像头设备\u003c/h2\u003e\n\u003cp\u003e虚拟摄像头设备（Virtual Device）提供了一种抽象层，代表一个包含多个物理摄像头的虚拟摄像头设备，使得开发者可以通过一个单一的 AVCaptureDevice 来访问和控制多个物理摄像头。\u003c/p\u003e\n\u003cp\u003e当在操作虚拟设备时，iOS 会在 constituentDevices 之间自动切换，以提供一致的用户体验。\u003c/p\u003e\n\u003cp\u003eiPhone 15 Pro 里，以下设备都属于虚拟设备：\u003c/p\u003e","title":"iOS 虚拟摄像头设备调研"},{"content":"一、背景 苹果在 iOS 13 支持了多摄，iPhone XS 以上设备支持。\nhttps://developer.apple.com/videos/play/wwdc2019/225\n二、TikTok 相关调研 1、相机 打开相机或关闭双摄\n\u0026lt;\u0026lt;\u0026lt;\u0026lt; FigCaptureSession \u0026gt;\u0026gt;\u0026gt;\u0026gt; captureSession_commitInflightConfiguration: \u0026lt;0xe9aa2d480[1156][com.zhiliaoapp.musically]\u0026gt; committing configuration FigCaptureSessionConfiguration 0xe9ab39c10: ID 30, AVCaptureSessionPreset1280x720 multiCam: 0, appAudio: 1, autoConfig: 1, runWhileMultitasking: 0 VC 0xe9ab28390: \u0026lt;SRC:Wide back 420f/1280x720, 25-25(max:30), Z:1.00, ICM:0, (FD E:0 B:0 S:0), HR:1, FaceDrivenAEAFMode:3, FaceDrivenAEAFEnabledByDefault:1\u0026gt; -\u0026gt; \u0026lt;SINK 0xe9ab2c430:Iris movies:0, suspended:0, preserveSuspended:0, movieDur:nans, trim:0, 0fps, preparedID:12, (QHR ON) maxQuality:1, (maxPhotoDims 4224x2376)\u0026gt;, /0x0, E:1, VIS:0, M:0, O:Unspecified, DOC:0, RBC:0, CIM:0 VC 0xe9ab2c4f0: \u0026lt;SRC:Wide back 420f/1280x720, 25-25(max:30), Z:1.00, ICM:0, (FD E:0 B:0 S:0), HR:1, FaceDrivenAEAFMode:3, FaceDrivenAEAFEnabledByDefault:1\u0026gt; -\u0026gt; \u0026lt;SINK 0xe9ab7a070:VideoData discards:1, preview:0, stability:0\u0026gt;, 420f/720x1280, E:1, VIS:1, M:0, O:Portrait, DOC:0, RBC:12, CIM:0 第一个 VC 对应的是 AVCapturePhotoOutput。\n打开双摄\n\u0026lt;\u0026lt;\u0026lt;\u0026lt; FigCaptureSession \u0026gt;\u0026gt;\u0026gt;\u0026gt; captureSession_commitInflightConfiguration: \u0026lt;0xe9da26050[1156][com.zhiliaoapp.musically]\u0026gt; committing configuration FigCaptureSessionConfiguration 0xe9ab82cf0: ID 24, AVCaptureSessionPresetInputPriority multiCam: 1, appAudio: 1, autoConfig: 1, runWhileMultitasking: 0 VC 0xe9ab831a0: \u0026lt;SRC:Wide back 420f/1280x720, 25-25(max:60), Z:1.00, ICM:0, (FD E:0 B:0 S:0), FaceDrivenAEAFMode:3, FaceDrivenAEAFEnabledByDefault:1\u0026gt; -\u0026gt; \u0026lt;SINK 0xe9ab83250:VideoData discards:1, preview:0, stability:0\u0026gt;, 420f/720x1280, E:1, VIS:1, M:0, O:Portrait, DOC:0, RBC:12, CIM:0 VC 0xe9ab832c0: \u0026lt;SRC:Wide back 420f/1280x720, 25-25(max:60), Z:1.00, ICM:0, (FD E:0 B:0 S:0), FaceDrivenAEAFMode:3, FaceDrivenAEAFEnabledByDefault:1\u0026gt; -\u0026gt; \u0026lt;SINK 0xe9abab6d0:Iris movies:0, suspended:0, preserveSuspended:0, movieDur:nans, trim:0, 0fps, preparedID:8, (QHR ON) maxQuality:1, (maxPhotoDims 2112x1188)\u0026gt;, /0x0, E:1, VIS:0, M:0, O:Unspecified, DOC:0, RBC:0, CIM:0 VC 0xe9abab7e0: \u0026lt;SRC:Wide front 420f/1280x720, 25-25(max:60), Z:1.00, ICM:0, (FD E:0 B:0 S:0), FaceDriven 2、合流 回显前合流\n三、相机模块 1、相关接口 AVCaptureMultiCamSession - isMultiCamSupported - hardwareCost - systemPressureCost - addInputWithNoConnections - addOutputWithNoConnections\nAVCaptureDevice - supportedMultiCamDeviceSets\n2、异常处理 硬件可能不支持； hardwareCost 大于 1.0 时，启动相机时会收到一个 AVCaptureSessionRuntimeErrorNotification 通知； systemPressureCost 大于 1.0 时，会收到一个 AVCaptureSessionWasInterruptedNotification 通知； 开启双摄（openCamera）前检查兼容性及 hardwareCost，不符合条件则返回失败，并通知客户端做相应提示。\n开播过程中收到 systemPressureCost 重试策略：降采集分辨率 -\u0026gt; 降帧率 -\u0026gt; 切换单摄。\n3、相机配置 一个 AVCaptureDeviceInput 可能会有多个端口（Ports），把对应的端口跟 AVCaptureVideoDataOutput 连接起来的就是 AVCaptureConnection。\n在以往的使用中，AVCaptureSession 使用addInput和addOutput添加 input 和 output 时，系统会添加对应的 Connection。\n在 AVCaptureMultiCamSession 中避免隐式添加 Connection（WWDC 2019 session），使用addInputWithNoConnections和addOutputWithNoConnections添加相关 inputs 和 outputs。\n手动创建 AVCaptureConnection，指定对应 port 到 output，再把 Connection 添加到 Session。\n前后相机分别创建对应 AVCaptureDeviceInput，AVCaptureVideoDataOutput，AVCaptureConnection\n4、采集分辨率 参考 TikTok，主副相机初始化为相同的分辨率，避免切换前后相机时需要重启相机。\n5、相机操作 enum class CaptureSceneType : uint64_t { ... kSceneVideo_Dual = 1 \u0026lt;\u0026lt; 12, //!\u0026lt; 视频双摄像机，默认主摄为后置 kSceneVideo_Dual_Front = SCENE_SUBTYPE(kSceneVideo_Dual, 1), //!\u0026lt; 视频双摄像机，主摄为前置 kSceneVideo_Dual_Rear = SCENE_SUBTYPE(kSceneVideo_Dual, 2), //!\u0026lt; 视频双摄像机，主摄为后置 }; 新建 HEMultiCameraIOS HEMultiCameraImpl 类，处理相机相关的操作。\n新增双摄的 cam_v2::CaptureSceneType，会在HECameraSystemInner::_handleOpen里通过HECameraDecision::createCameraByType 创建对应的 HEMultiCameraImpl，\n6、相机切换 单摄与双摄之间的切换，参考 ARKit 切换：先设置对应的 cam_v2::CaptureSceneType，并重新调用 openCamera。\n单摄与双摄切换时需要切换 session，所以会重启相机，画面会有明暗变化，已同步到产品\n如果只是临时关闭双摄，可使用：frontCameraInputVideoPort.enabled = false\n7、图像回调 把需要添加特效的相机定义为主相机。\n在 Demo 中，主副相机的视频帧异步回调。\n由主相机回调来驱动视频帧回调到前处理链路，若主相机回调时副相机还没有数据，则沿用上一帧副相机数据。\n8、主副相机切换 前处理链路对 HEFrame 做完整的前处理，对 HEFrame.secondaryFrame 只做格式转换及合流。 主副相机切换时需要考虑是否固定前置相机添加特效，\n1. 固定前置相机添加特效 把前置相机的图像固定赋值到 HEFrame，把后置相机的图像固定赋值到 HEFrame.secondaryFrame 进行主副相机切换时，只需要调整合流 Entity 的层级即可。\n2. 固定主相机添加特效 把主相机的图像固定赋值到 HEFrame，把副相机的图像固定赋值到 HEFrame.secondaryFrame 进行主副相机切换时，交换赋值的位置即可\n9、测光对焦 目前看在 AVCaptureMultiCamSession 中获取不到人脸信息，可能不能以此实现人脸测光对焦。\n经产品初步确认，需可对主画面进行 EV、ZOOM 操作\n10、合流 #include \u0026lt;metal_stdlib\u0026gt; using namespace metal; struct MixerParameters { float2 pipPosition; float2 pipSize; float2 pipScaleSize; float2 pipOffset; float cornerRadius; float blurSize; }; constant sampler kBilinearSampler(filter::linear, coord::pixel, address::clamp_to_edge); float4 blur(texture2d\u0026lt;float, access::sample\u0026gt; pipInput, float2 uv, float blurSize) { float4 color = float4(0.0); color += pipInput.sample(kBilinearSampler, uv + float2(-blurSize, -blurSize)) * 0.0625; color += pipInput.sample(kBilinearSampler, uv + float2( blurSize, -blurSize)) * 0.0625; color += pipInput.sample(kBilinearSampler, uv + float2(-blurSize, blurSize)) * 0.0625; color += pipInput.sample(kBilinearSampler, uv + float2( blurSize, blurSize)) * 0.0625; color += pipInput.sample(kBilinearSampler, uv + float2(0.0, -blurSize)) * 0.125; color += pipInput.sample(kBilinearSampler, uv + float2(0.0, blurSize)) * 0.125; color += pipInput.sample(kBilinearSampler, uv + float2(-blurSize, 0.0)) * 0.125; color += pipInput.sample(kBilinearSampler, uv + float2( blurSize, 0.0)) * 0.125; color += pipInput.sample(kBilinearSampler, uv) * 0.25; return color; } // Compute kernel kernel void reporterMixer(texture2d\u0026lt;float, access::read\u0026gt; fullScreenInput [[texture(0)]], texture2d\u0026lt;float, access::sample\u0026gt; pipInput [[texture(1)]], texture2d\u0026lt;float, access::write\u0026gt; outputTexture [[texture(2)]], const device MixerParameters\u0026amp; mixerParameters [[buffer(0)]], uint2 gid [[thread_position_in_grid]]) { uint2 pipPosition = uint2(mixerParameters.pipPosition); uint2 pipSize = uint2(mixerParameters.pipSize); uint2 pipScaleSize = uint2(mixerParameters.pipScaleSize); uint2 pipOffset = uint2(mixerParameters.pipOffset); float cornerRadius = mixerParameters.cornerRadius; float blurSize = mixerParameters.blurSize; float4 output = fullScreenInput.read(gid); bool withinPipBounds = gid.x \u0026gt;= pipPosition.x \u0026amp;\u0026amp; gid.y \u0026gt;= pipPosition.y \u0026amp;\u0026amp; gid.x \u0026lt; pipPosition.x + pipSize.x \u0026amp;\u0026amp; gid.y \u0026lt; pipPosition.y + pipSize.y; if (withinPipBounds) { float2 center = float2(pipSize) * 0.5; uint2 localPos = gid - pipPosition; float2 pipCoord = float2(localPos) - center; float2 edgeDistVec = abs(pipCoord) - (center - float2(cornerRadius)); float2 edgeDist = max(edgeDistVec, float2(0.0)); float edgeDistance = length(edgeDist); float feather = 1.0; float alpha = 1.0 - smoothstep(cornerRadius - feather, cornerRadius + feather, edgeDistance); if (alpha \u0026gt; 0.0) { float2 pipSamplingCoord = float2(gid - pipPosition + pipOffset) * float2(pipInput.get_width(), pipInput.get_height()) / float2(pipScaleSize); float4 pipColor = blur(pipInput, pipSamplingCoord, blurSize); output = mix(output, pipColor, alpha); } } outputTexture.write(output, gid); } ","permalink":"https://blog.axqora.com/posts/ios-dual-camera/","summary":"\u003ch2 id=\"一背景\"\u003e一、背景\u003c/h2\u003e\n\u003cp\u003e苹果在 iOS 13 支持了多摄，iPhone XS 以上设备支持。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://developer.apple.com/videos/play/wwdc2019/225\"\u003ehttps://developer.apple.com/videos/play/wwdc2019/225\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"二tiktok-相关调研\"\u003e二、TikTok 相关调研\u003c/h2\u003e\n\u003ch3 id=\"1相机\"\u003e1、相机\u003c/h3\u003e\n\u003cp\u003e打开相机或关闭双摄\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026lt;\u0026lt;\u0026lt;\u0026lt; FigCaptureSession \u0026gt;\u0026gt;\u0026gt;\u0026gt; captureSession_commitInflightConfiguration: \u0026lt;0xe9aa2d480[1156][com.zhiliaoapp.musically]\u0026gt; committing configuration FigCaptureSessionConfiguration 0xe9ab39c10: ID 30, AVCaptureSessionPreset1280x720 multiCam: 0, appAudio: 1, autoConfig: 1, runWhileMultitasking: 0\n\nVC 0xe9ab28390: \u0026lt;SRC:Wide back 420f/1280x720, 25-25(max:30), Z:1.00, ICM:0, (FD E:0 B:0 S:0), HR:1, FaceDrivenAEAFMode:3, FaceDrivenAEAFEnabledByDefault:1\u0026gt; -\u0026gt; \u0026lt;SINK 0xe9ab2c430:Iris movies:0, suspended:0, preserveSuspended:0, movieDur:nans, trim:0, 0fps, preparedID:12, (QHR ON) maxQuality:1, (maxPhotoDims 4224x2376)\u0026gt;, /0x0, E:1, VIS:0, M:0, O:Unspecified, DOC:0, RBC:0, CIM:0\n\nVC 0xe9ab2c4f0: \u0026lt;SRC:Wide back 420f/1280x720, 25-25(max:30), Z:1.00, ICM:0, (FD E:0 B:0 S:0), HR:1, FaceDrivenAEAFMode:3, FaceDrivenAEAFEnabledByDefault:1\u0026gt; -\u0026gt; \u0026lt;SINK 0xe9ab7a070:VideoData discards:1, preview:0, stability:0\u0026gt;, 420f/720x1280, E:1, VIS:1, M:0, O:Portrait, DOC:0, RBC:12, CIM:0\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e第一个 VC 对应的是 AVCapturePhotoOutput。\u003c/p\u003e","title":"iOS 双摄"},{"content":"一、前言 一直梦想着能有一间充满极客范的书房，里面有简洁的电脑桌，墙上挂满各类工具，旁边的工作台上面放着 3D 打印机。\n小孩差不多两岁了，经常给他买各种各样的玩具，想着如果有 3D 打印机，想要什么玩具都能打印出来。\n其实就是给自己一个任性的理由买一台“不实用”的 3D 打印机哈哈。\n二、选购 买之前大概做了下功课，消费级的 3D 打印机大致有两种，一种是 FDM 3D 打印机，另一种是光固化 3D 打印机。\n光固化打印机打印出来的模型会比较精致，不会有明显的层纹，比较适合打印手办。入门门槛比较高，操作较为繁琐，关键是打印时会散发有毒气体，家有小孩就排除光固化打印机了。\n所以只在 FDM 打印机里面挑选，其实 FDM 的原理就是把材料熔化后一层层地堆积成型，我主要在关注创想三维跟拓竹这两个国内的品牌。\n可选的型号也不多，创想三维的 K1 跟拓竹的 P1S，两者相差接近一千块，所以我最后选择的是性价比较高的 K1。\n三、打印 大家可以先看看 3D 打印的效果\n随着对 3D 打印理解的深入，发现 3D 打印里面的学问非常多，我还只是懂点皮毛。\n1. 温度 不同的材料对温度的要求不一样，比如：\nPLA 材料，喷嘴温度 190 – 230°C，热床温度 35 – 45°C；\nABS 材料，喷嘴温度 220 – 250°C，热床温度 80 – 100°C。温度不对，容易出现翘边的情况。\n2. 速度 速度会影响打印模型的最终效果，总体来说速度越慢精度越高，打印的效果越好。\n特别是起始层的打印，起始层相当于整个模型的基础，我习惯使用一半的速度来打印起始层，比如速度我设置 150 mm/s，起始层的打印速度我会设置为 75 mm/s。\n3. 平台附着 平台附着其实就是先在平台上打印一层，解决模型无法粘到平台上的问题，特别是一些跟平台接触面很小的模型，使用平台附着就非常必要。\n4. 支撑 因为模型是一层层地打印的，如果遇到一些悬空的结构，如果没有支撑会导致打印失败，所以需要根据实际情况添加支撑。\n5. 材料保存 3D 打印材料容易受潮，特别是 PLA，受潮后的材料打印出来的模型效果会变差，会拉丝的情况，甚至出现断裂的情况。所以需要妥善保存，比如长时间不用的材料放到密封袋保存。\n四、问题 1. 喷头加热故障 打印机用了不到一个月就出现喷头温度传感器故障的问题，最后联系客服重新寄了一个喷头套件自行替换解决了问题。\n2. 摄像头无法连接 在网页上或者在 App 上都连不上摄像头，也尝试让客服更换了摄像头，问题依然没有解决。\n后面怀疑是网络问题，于是在电脑端的切片软件上连接摄像头，切片软件上属于局域网连接，局域网连接摄像头画面正常。\n最后才想起来软路由里面设置了代理，创想的网络流量走了代理导致摄像头连接失败。代理软件里面设置一下就好了。\n五、后记 目前我还只是打印现成的模型，后面计划好好熟悉下 3D 建模软件，设计自己的模型才是真正的极客范。\n小孩对 3D 打印机充满了好奇，我希望能在小孩成长的路上尽量满足他的好奇心，让他长大后能尽情发挥自己的创造力。\n","permalink":"https://blog.axqora.com/posts/3d-printer/","summary":"\u003ch2 id=\"一前言\"\u003e一、前言\u003c/h2\u003e\n\u003cp\u003e一直梦想着能有一间充满极客范的书房，里面有简洁的电脑桌，墙上挂满各类工具，旁边的工作台上面放着 3D 打印机。\u003c/p\u003e\n\u003cp\u003e小孩差不多两岁了，经常给他买各种各样的玩具，想着如果有 3D 打印机，想要什么玩具都能打印出来。\u003c/p\u003e\n\u003cp\u003e其实就是给自己一个任性的理由买一台“不实用”的 3D 打印机哈哈。\u003c/p\u003e\n\u003ch2 id=\"二选购\"\u003e二、选购\u003c/h2\u003e\n\u003cp\u003e买之前大概做了下功课，消费级的 3D 打印机大致有两种，一种是 FDM 3D 打印机，另一种是光固化 3D 打印机。\u003c/p\u003e\n\u003cp\u003e光固化打印机打印出来的模型会比较精致，不会有明显的层纹，比较适合打印手办。入门门槛比较高，操作较为繁琐，关键是打印时会散发有毒气体，家有小孩就排除光固化打印机了。\u003c/p\u003e\n\u003cp\u003e所以只在 FDM 打印机里面挑选，其实 FDM 的原理就是把材料熔化后一层层地堆积成型，我主要在关注创想三维跟拓竹这两个国内的品牌。\u003c/p\u003e\n\u003cp\u003e可选的型号也不多，创想三维的 K1 跟拓竹的 P1S，两者相差接近一千块，所以我最后选择的是性价比较高的 K1。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20230915173517406\" loading=\"lazy\" src=\"/image/3d-printer/image-20230915173517406.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"三打印\"\u003e三、打印\u003c/h2\u003e\n\u003cp\u003e大家可以先看看 3D 打印的效果\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20230915173718621\" loading=\"lazy\" src=\"/image/3d-printer/image-20230915173718621.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"IMG_5598\" loading=\"lazy\" src=\"/image/3d-printer/IMG_5598.JPG\"\u003e\u003c/p\u003e\n\u003cp\u003e随着对 3D 打印理解的深入，发现 3D 打印里面的学问非常多，我还只是懂点皮毛。\u003c/p\u003e\n\u003ch3 id=\"1-温度\"\u003e1. 温度\u003c/h3\u003e\n\u003cp\u003e不同的材料对温度的要求不一样，比如：\u003c/p\u003e\n\u003cp\u003ePLA 材料，喷嘴温度 190 – 230°C，热床温度 35 – 45°C；\u003c/p\u003e\n\u003cp\u003eABS 材料，喷嘴温度 220 – 250°C，热床温度 80 – 100°C。温度不对，容易出现翘边的情况。\u003c/p\u003e\n\u003ch3 id=\"2-速度\"\u003e2. 速度\u003c/h3\u003e\n\u003cp\u003e速度会影响打印模型的最终效果，总体来说速度越慢精度越高，打印的效果越好。\u003c/p\u003e\n\u003cp\u003e特别是起始层的打印，起始层相当于整个模型的基础，我习惯使用一半的速度来打印起始层，比如速度我设置 150 mm/s，起始层的打印速度我会设置为 75 mm/s。\u003c/p\u003e\n\u003ch3 id=\"3-平台附着\"\u003e3. 平台附着\u003c/h3\u003e\n\u003cp\u003e平台附着其实就是先在平台上打印一层，解决模型无法粘到平台上的问题，特别是一些跟平台接触面很小的模型，使用平台附着就非常必要。\u003c/p\u003e","title":"3D 打印入坑记"},{"content":"一、前言 我在平时工作生活中 ChatGPT 用得很频繁，但会有时候不方便设置代理使用。有时朋友需要用时教育成本也比较高。\n如果可以直接访问使用就很好解决我的痛点了。\n在上一篇文章「群晖 NAS - 代理设置」中，已经设置好了群晖的代理。\n另外，本文不涉及 ChatGPT 账号注册、APIKey 申请等内存，下面只介绍 ChatGPT 的部署。\n二、部署 网上有非常多优秀的 ChatGPT 开源项目，我这里使用的是 chatgpt-web。\n1、镜像下载 # ssh登录到群晖 ssh user_name@ip_address # 获取超级权限 sudo -i # 拉取镜像 docker pull chenzhaoyu94/chatgpt-web 2、配置 设置访问 web 使用的端口即可\n只需要关注两个环境变量：\nHTTPS_PROXY：这里填写上篇文章设置好的端口，要注意的是这个 IP 地址需要填写群晖的 IP 地址，不能使用 127.0.0.1； OPENAI_API_KEY：这个填写 API Key，教程网上一大堆。 3、测试 ","permalink":"https://blog.axqora.com/posts/nas-chatgpt/","summary":"\u003ch2 id=\"一前言\"\u003e一、前言\u003c/h2\u003e\n\u003cp\u003e我在平时工作生活中 ChatGPT 用得很频繁，但会有时候不方便设置代理使用。有时朋友需要用时教育成本也比较高。\u003c/p\u003e\n\u003cp\u003e如果可以直接访问使用就很好解决我的痛点了。\u003c/p\u003e\n\u003cp\u003e在上一篇文章「\u003ca href=\"https://blog.axqora.com/posts/nas-proxy/\"\u003e群晖 NAS - 代理设置\u003c/a\u003e」中，已经设置好了群晖的代理。\u003c/p\u003e\n\u003cp\u003e另外，本文不涉及 ChatGPT 账号注册、APIKey 申请等内存，下面只介绍 ChatGPT 的部署。\u003c/p\u003e\n\u003ch2 id=\"二部署\"\u003e二、部署\u003c/h2\u003e\n\u003cp\u003e网上有非常多优秀的 ChatGPT 开源项目，我这里使用的是 \u003ca href=\"https://github.com/Chanzhaoyu/chatgpt-web\"\u003echatgpt-web\u003c/a\u003e。\u003c/p\u003e\n\u003ch3 id=\"1镜像下载\"\u003e1、镜像下载\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# ssh登录到群晖\nssh user_name@ip_address\n# 获取超级权限\nsudo -i\n# 拉取镜像\ndocker pull chenzhaoyu94/chatgpt-web\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"2配置\"\u003e2、配置\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image-20230609165150310\" loading=\"lazy\" src=\"/image/NAS-ChatGPT/image-20230609165150310.png\"\u003e\u003c/p\u003e\n\u003cp\u003e设置访问 web 使用的端口即可\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20230609164249659\" loading=\"lazy\" src=\"/image/NAS-ChatGPT/image-20230609164249659.png\"\u003e\u003c/p\u003e\n\u003cp\u003e只需要关注两个环境变量：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cem\u003e\u003cstrong\u003eHTTPS_PROXY\u003c/strong\u003e\u003c/em\u003e：这里填写上篇文章设置好的端口，要注意的是这个 IP 地址需要填写群晖的 IP 地址，不能使用 127.0.0.1；\u003c/li\u003e\n\u003cli\u003e\u003cem\u003e\u003cstrong\u003eOPENAI_API_KEY\u003c/strong\u003e\u003c/em\u003e：这个填写 API Key，教程网上一大堆。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"3测试\"\u003e3、测试\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image-20230609165349852\" loading=\"lazy\" src=\"/image/NAS-ChatGPT/image-20230609165349852.png\"\u003e\u003c/p\u003e","title":"群晖 NAS - ChatGPT 部署"},{"content":"一、前言 因网络环境导致，在群晖中的 Docker 拉取镜像或者部分套件中的服务会经常失败，对我们来说有一个顺畅的网络环境能大大提高幸福感。\n其实方案有非常多，例如前置软路由，或者使用 Tailscale 设置出口节点等。这里介绍一个相对简单的方法。\n二、部署 1、镜像下载 我们使用 Docker 进行部署，方便后续管理。\n此时直接使用群晖里的 Docker 图形界面拉取镜像会失败，我们通过 ssh 登录到群晖，使用 docker pull xxx 的方式会比较方便。\n# ssh登录到群晖 ssh user_name@ip_address # 获取超级权限 sudo -i # 拉取镜像 docker pull dreamacro/clash docker pull haishanh/yacd 这里比较简单，只用到了 docker pull 命令，如果对 Docker 命令不是很熟悉，配置可以使用群晖内部的 Docker 图形界面。\n2、Docker 配置 a. Clash 配置 首先需要从服务商那里获取配置文件，这里命名为config.yaml，然后存储到群晖 docker/clash/config.yaml; 配置文件映射到 /root/.config/clash/config.yaml； 设置端口，下面群晖配置时需要用到， 运行。 b. yacd 配置 yacd 是 clash 的仪表盘，yacd 配置非常简单，指定访问端口即可。\n3、群晖配置 控制面板 - 网络 - 常规 - 代理服务器 - 填写刚设置的IP端口 - 应用\n三、测试 ","permalink":"https://blog.axqora.com/posts/nas-proxy/","summary":"\u003ch2 id=\"一前言\"\u003e一、前言\u003c/h2\u003e\n\u003cp\u003e因网络环境导致，在群晖中的 Docker 拉取镜像或者部分套件中的服务会经常失败，对我们来说有一个顺畅的网络环境能大大提高幸福感。\u003c/p\u003e\n\u003cp\u003e其实方案有非常多，例如前置软路由，或者使用 Tailscale 设置出口节点等。这里介绍一个相对简单的方法。\u003c/p\u003e\n\u003ch2 id=\"二部署\"\u003e二、部署\u003c/h2\u003e\n\u003ch4 id=\"1镜像下载\"\u003e1、镜像下载\u003c/h4\u003e\n\u003cp\u003e我们使用 Docker 进行部署，方便后续管理。\u003c/p\u003e\n\u003cp\u003e此时直接使用群晖里的 Docker 图形界面拉取镜像会失败，我们通过 ssh 登录到群晖，使用 \u003ccode\u003edocker pull xxx\u003c/code\u003e 的方式会比较方便。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# ssh登录到群晖\nssh user_name@ip_address\n# 获取超级权限\nsudo -i\n# 拉取镜像\ndocker pull dreamacro/clash\ndocker pull haishanh/yacd\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e这里比较简单，只用到了 \u003ccode\u003edocker pull\u003c/code\u003e 命令，如果对 Docker 命令不是很熟悉，配置可以使用群晖内部的 Docker 图形界面。\u003c/p\u003e\n\u003ch4 id=\"2docker-配置\"\u003e2、Docker 配置\u003c/h4\u003e\n\u003ch5 id=\"a-clash-配置\"\u003ea. Clash 配置\u003c/h5\u003e\n\u003cp\u003e\u003cimg alt=\"image-20230609151156957\" loading=\"lazy\" src=\"/image/NAS-Proxy/image-20230609151156957.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20230609151247861\" loading=\"lazy\" src=\"/image/NAS-Proxy/image-20230609151247861.png\"\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e首先需要从服务商那里获取配置文件，这里命名为config.yaml，然后存储到群晖 \u003ccode\u003edocker/clash/config.yaml\u003c/code\u003e;\u003c/li\u003e\n\u003cli\u003e配置文件映射到 \u003ccode\u003e/root/.config/clash/config.yaml\u003c/code\u003e；\u003c/li\u003e\n\u003cli\u003e设置端口，下面群晖配置时需要用到，\u003c/li\u003e\n\u003cli\u003e运行。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch5 id=\"b-yacd-配置\"\u003eb. yacd 配置\u003c/h5\u003e\n\u003cp\u003eyacd 是 clash 的仪表盘，yacd 配置非常简单，指定访问端口即可。\u003c/p\u003e\n\u003ch4 id=\"3群晖配置\"\u003e3、群晖配置\u003c/h4\u003e\n\u003cp\u003e\u003cimg alt=\"image-20230609152440696\" loading=\"lazy\" src=\"/image/NAS-Proxy/image-20230609152440696.png\"\u003e\u003c/p\u003e\n\u003cp\u003e控制面板 - 网络 - 常规 - 代理服务器 - 填写刚设置的IP端口 - 应用\u003c/p\u003e","title":"群晖 NAS - 代理设置"},{"content":" frp 是一个专注于内网穿透的高性能的反向代理应用，支持 TCP、UDP、HTTP、HTTPS 等多种协议。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。\n一、服务端配置 1、安装及配置 这里以群晖 NAS 为例：\n在 Docker 注册表里搜索 frps，安装 latest 版本（也可以安装指定版本）； 在映像里创建容器，并进入高级设置； 在高级设置里启用自动重新启动； 在存储空间添加文件，装载路径为 /etc/frp/frps.ini； 网络里勾选使用与 Docker Host 相同的网络； 启动容器。 2、frps.ini 配置文件 [common] bind_port = 7000 token = your_token dashboard_port = 7500 dashboard_user = your_dashboard_user dashboard_pwd = your_dashboard_pwd vhost_http_port = 8000 vhost_https_port = 8001 二、客户端配置 1、安装及配置 同样以群晖 NAS 为例，安装及配置基本跟服务端相同，配置文件改为 frpc.ini 即可。\n2、frpc.ini 配置文件 [common] server_addr = your_server_addr server_port = 7000 tls_enable = true token = your_token [ssh] type = tcp local_ip = 192.168.100.2 local_port = 22 remote_port = 8022 use_compression = true use_encryption = true [chevereto] type = tcp local_ip = 192.168.100.2 local_port = 8090 remote_port = 8090 use_compression = true use_encryption = true [WebDAV_https] type = tcp local_ip = 192.168.100.2 local_port = 5007 remote_port = 8007 use_compression = true use_encryption = true [WebDAV_http] type = tcp local_ip = 192.168.100.2 local_port = 5005 remote_port = 8005 use_compression = true use_encryption = true [dsm_http] type = tcp local_ip = 192.168.100.2 local_port = 5000 remote_port = 8000 use_compression = true use_encryption = true [http] type = http custom_domains = your_custom_domains local_port = 5000 use_compression = true use_encryption = true [https2http] type = https custom_domains = your_custom_domains local_port = 5000 plugin = https2http plugin_local_addr = 127.0.0.1:5000 plugin_crt_path = /etc/frp/cert/your_custom_domains.pem plugin_key_path = /etc/frp/cert/your_custom_domains.key plugin_host_header_rewrite = 127.0.0.1 plugin_header_X-From-Where = frp 三、Mac 远程访问 公司因安全原因，内网屏蔽了 TeamViewer、花生壳等远程桌面工具，而我时常需要在家里访问公司的 Mac 电脑。\n一种方法是连接 VPN，然后通过 公司的 Mac IP 地址这种场景使用 frp 工具就非常好解决了。\n1、Mac 屏幕共享设置 在 System Preferences -\u0026gt; Sharing -\u0026gt; 勾选 Screen Sharing\n2、Mac 安装 frpc 可以在 frp GitHub 下载 darwin 二进制包，使用 ./frpc -c frpc.ini 命令即可启动客户端，但是如果想要自启动，还得配置 launchctl。\n我们可以直接使用 Homebrew 进行安装会更加方便，Homebrew 内置了 brew services 服务（Manage background services with macOS\u0026rsquo; launchctl(1) daemon manager.），我们就不用单独配置 launchctl。\n// 安装 brew install frpc // 编辑配置 vim /usr/local/etc/frp/frpc.ini // 新版路径 vim /opt/homebrew/etc/frp/frpc.ini // 启动服务 brew services start frpc frpc.ini 配置\n[common] server_addr = your_server_addr server_port = 7000 tls_enable = true token = your_token [vnc] type = tcp local_ip = 127.0.0.1 local_port = 5900 // 本地 vnc 默认端口 remote_port = 8050 // 远程访问时使用的端口 use_compression = true use_encryption = true 3、远程连接 可以安装 VNC Viewer 进行访问。\n如果使用 macOS，\n打开 Finder -\u0026gt; Go -\u0026gt; Connect to Server，或者使用快捷键 Command + K； 输入 vnc://your_server_addr:8050，然后点击 Connect； 弹出输入框，输入 Mac 用户名及锁屏密码。 ","permalink":"https://blog.axqora.com/posts/frp-skills/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/fatedier/frp\"\u003efrp\u003c/a\u003e 是一个专注于内网穿透的高性能的反向代理应用，支持 TCP、UDP、HTTP、HTTPS 等多种协议。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"一服务端配置\"\u003e一、服务端配置\u003c/h2\u003e\n\u003ch3 id=\"1安装及配置\"\u003e1、安装及配置\u003c/h3\u003e\n\u003cp\u003e这里以群晖 NAS 为例：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e在 Docker 注册表里搜索 frps，安装 latest 版本（也可以安装指定版本）；\u003c/li\u003e\n\u003cli\u003e在映像里创建容器，并进入高级设置；\u003c/li\u003e\n\u003cli\u003e在高级设置里启用自动重新启动；\u003c/li\u003e\n\u003cli\u003e在存储空间添加文件，装载路径为 \u003ccode\u003e/etc/frp/frps.ini\u003c/code\u003e；\u003c/li\u003e\n\u003cli\u003e网络里勾选使用与 Docker Host 相同的网络；\u003c/li\u003e\n\u003cli\u003e启动容器。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220707211357236\" loading=\"lazy\" src=\"/image/frp-skills/image-20220707211357236.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220707211924033\" loading=\"lazy\" src=\"/image/frp-skills/image-20220707211924033.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220707211655902\" loading=\"lazy\" src=\"/image/frp-skills/image-20220707211655902.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220707211700736\" loading=\"lazy\" src=\"/image/frp-skills/image-20220707211700736.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220707211739436\" loading=\"lazy\" src=\"/image/frp-skills/image-20220707211739436.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"2frpsini-配置文件\"\u003e2、frps.ini 配置文件\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e[common]\nbind_port = 7000\ntoken = your_token\ndashboard_port = 7500\ndashboard_user = your_dashboard_user\ndashboard_pwd = your_dashboard_pwd\nvhost_http_port = 8000\nvhost_https_port = 8001\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"二客户端配置\"\u003e二、客户端配置\u003c/h2\u003e\n\u003ch3 id=\"1安装及配置-1\"\u003e1、安装及配置\u003c/h3\u003e\n\u003cp\u003e同样以群晖 NAS 为例，安装及配置基本跟服务端相同，配置文件改为 frpc.ini 即可。\u003c/p\u003e\n\u003ch3 id=\"2frpcini-配置文件\"\u003e2、frpc.ini 配置文件\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e[common]\nserver_addr = your_server_addr\nserver_port = 7000\ntls_enable = true\ntoken = your_token\n\n[ssh]\ntype = tcp\nlocal_ip = 192.168.100.2\nlocal_port = 22\nremote_port = 8022\nuse_compression = true\nuse_encryption = true\n\n[chevereto]\ntype = tcp\nlocal_ip = 192.168.100.2\nlocal_port = 8090\nremote_port = 8090\nuse_compression = true\nuse_encryption = true\n\n[WebDAV_https]\ntype = tcp\nlocal_ip = 192.168.100.2\nlocal_port = 5007\nremote_port = 8007\nuse_compression = true\nuse_encryption = true\n\n[WebDAV_http]\ntype = tcp\nlocal_ip = 192.168.100.2\nlocal_port = 5005\nremote_port = 8005\nuse_compression = true\nuse_encryption = true\n\n[dsm_http]\ntype = tcp\nlocal_ip = 192.168.100.2\nlocal_port = 5000\nremote_port = 8000\nuse_compression = true\nuse_encryption = true\n\n[http]\ntype = http\ncustom_domains = your_custom_domains\nlocal_port = 5000\nuse_compression = true\nuse_encryption = true\n\n[https2http]\ntype = https\ncustom_domains = your_custom_domains\nlocal_port = 5000\nplugin = https2http\nplugin_local_addr = 127.0.0.1:5000\n\nplugin_crt_path = /etc/frp/cert/your_custom_domains.pem\nplugin_key_path = /etc/frp/cert/your_custom_domains.key\nplugin_host_header_rewrite = 127.0.0.1\nplugin_header_X-From-Where = frp\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"三mac-远程访问\"\u003e三、Mac 远程访问\u003c/h2\u003e\n\u003cp\u003e公司因安全原因，内网屏蔽了 TeamViewer、花生壳等远程桌面工具，而我时常需要在家里访问公司的 Mac 电脑。\u003c/p\u003e","title":"群晖 NAS - frp 使用及实现 Mac 远程访问"},{"content":"一、前言 阿里云盘即使不是会员也不会限速，获得广大好评。如果能把阿里云盘挂载到 NAS 上，相当于扩充了 NAS 的磁盘容量，也可以利用这一点实现在线播放视频、数据同步等功能。\naliyundrive-webdav 这个工具可以让阿里云盘成为支持 WebDAV 协议的文件服务器，下面简单介绍其安装及使用。\n二、安装配置 1、aliyundrive-webdav 安装 在 Docker 注册表里搜索 aliyundrive-webdav，安装 latest 版本（也可以安装指定版本）； 在映像里创建容器，并进入高级设置； 在高级设置里启用自动重新启动； 在存储空间添加文件夹，装载路径为 /etc/aliyundrive-webdav/； 添加 8080 端口； 添加 REFRESH_TOKEN、WEBDAV_AUTH_USER、WEBDAV_AUTH_PASSWORD 三个环境变量（REFRESH_TOKEN 在后面介绍如何获取）； 启动容器。 2、refresh_token 获取 登录阿里云盘网页版，F12 进入开发者模式，然后在对应位置获取 refresh_token。\n三、挂载 四、数据同步 ","permalink":"https://blog.axqora.com/posts/nas-adrive-webdav/","summary":"\u003ch2 id=\"一前言\"\u003e一、前言\u003c/h2\u003e\n\u003cp\u003e阿里云盘即使不是会员也不会限速，获得广大好评。如果能把阿里云盘挂载到 NAS 上，相当于扩充了 NAS 的磁盘容量，也可以利用这一点实现在线播放视频、数据同步等功能。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/messense/aliyundrive-webdav\"\u003ealiyundrive-webdav\u003c/a\u003e 这个工具可以让阿里云盘成为支持 WebDAV 协议的文件服务器，下面简单介绍其安装及使用。\u003c/p\u003e\n\u003ch2 id=\"二安装配置\"\u003e二、安装配置\u003c/h2\u003e\n\u003ch3 id=\"1aliyundrive-webdav-安装\"\u003e1、aliyundrive-webdav 安装\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e在 Docker 注册表里搜索 aliyundrive-webdav，安装 latest 版本（也可以安装指定版本）；\u003c/li\u003e\n\u003cli\u003e在映像里创建容器，并进入高级设置；\u003c/li\u003e\n\u003cli\u003e在高级设置里启用自动重新启动；\u003c/li\u003e\n\u003cli\u003e在存储空间添加文件夹，装载路径为 \u003ccode\u003e/etc/aliyundrive-webdav/\u003c/code\u003e；\u003c/li\u003e\n\u003cli\u003e添加 8080 端口；\u003c/li\u003e\n\u003cli\u003e添加 REFRESH_TOKEN、WEBDAV_AUTH_USER、WEBDAV_AUTH_PASSWORD 三个环境变量（REFRESH_TOKEN 在后面介绍如何获取）；\u003c/li\u003e\n\u003cli\u003e启动容器。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220708205229838\" loading=\"lazy\" src=\"/image/NAS-Adrive-WebDAV/image-20220708205229838.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220708205302961\" loading=\"lazy\" src=\"/image/NAS-Adrive-WebDAV/image-20220708205302961.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220708205357614\" loading=\"lazy\" src=\"/image/NAS-Adrive-WebDAV/image-20220708205357614.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220708205427065\" loading=\"lazy\" src=\"/image/NAS-Adrive-WebDAV/image-20220708205427065.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220708205522864\" loading=\"lazy\" src=\"/image/NAS-Adrive-WebDAV/image-20220708205522864.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"2refresh_token-获取\"\u003e2、refresh_token 获取\u003c/h3\u003e\n\u003cp\u003e登录阿里云盘网页版，F12 进入开发者模式，然后在对应位置获取 refresh_token。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220708210047626\" loading=\"lazy\" src=\"/image/NAS-Adrive-WebDAV/image-20220708210047626.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"三挂载\"\u003e三、挂载\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220708211002716\" loading=\"lazy\" src=\"/image/NAS-Adrive-WebDAV/image-20220708211002716.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220708211339111\" loading=\"lazy\" src=\"/image/NAS-Adrive-WebDAV/image-20220708211339111.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"四数据同步\"\u003e四、数据同步\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220708210208125\" loading=\"lazy\" src=\"/image/NAS-Adrive-WebDAV/image-20220708210208125.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220708205705598\" loading=\"lazy\" src=\"/image/NAS-Adrive-WebDAV/image-20220708205705598.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220708205801023\" loading=\"lazy\" src=\"/image/NAS-Adrive-WebDAV/image-20220708205801023.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220708210814491\" loading=\"lazy\" src=\"/image/NAS-Adrive-WebDAV/image-20220708210814491.png\"\u003e\u003c/p\u003e","title":"群晖 NAS - 阿里云盘挂载及数据同步"},{"content":"一、前言 距离去年入坑 NAS 已经一年多了（群晖 NAS - 购置篇），这一年多里学习了很多 NAS 的玩法，感觉能够发挥了 NAS 应有的作用。\n这里简单介绍一下我利用 NAS 做了什么事情，但不会介绍详细的实现方法，因为网上已经有大量的参考文章。\n二、外网访问 能在外网访问家里的 NAS，会多很多玩法，所以怎么实现外网访问是我入坑 NAS 的第一课。\n1、QuickConnect QuickConnect 是群晖官方提供的远程访问服务，使用起来最简单门槛最低，但存在几个缺点：\n限速比较严重，只有 500k/s 左右下载速度； 数据会经过群晖服务器进行中转，且账号必须绑定手机号，让我觉得会有隐私问题； 只有白群晖能用，黑群晖无法激活。 基于这几个缺点，我需要找其它的替代方案。\n2、公网 IP 最方便的公网访问方式就是家里的宽带有公网 IP，无需转发直接访问，但不是所有宽带都能开通公网 IP。\n这里的公网 IP 特指 IPv4 的公网 IP，因为 IPv4 的公网 IP 的兼容性最好，所有运营商都支持访问；IPv6 的公网 IP 甚至每个网口都能分配一个 IP，但目前来说应用还不是很广泛，有些运营商有些路由器都还不支持访问。\n不同运营商情况大概如下：\n如果家里宽带用的是电信，那么可以打电话给客服免费开通公网 IP，很大概率可以开通，如果开通不了就多打几次电话。电信宽带还不支持分配和访问 IPv6 公网 IP。\n移动宽带则是完全不向个人开放 IPv4 的公网 IP，但是默认支持 IPv6 IP。\n联通的话据说申请难度比电信要高，但我还没用过没有发言权。\n我在广州家里用的是移动宽带，默认分配了 IPv6 的公网 IP，所以是可以直接 IPv6 IP 来访问我家里的 NAS。\n但是访问 IPv6 的公网 IP 存在两个问题：\n通过一大串的 IPv6 IP 来访问我家里的 NAS（如：http://[FE80:CD00:0000:0CDE:1257:0000:211E:729C]:5000/）； 大部分网络环境下都不支持访问 IPv6 地址，目前我只发现两种途径访问：1. 在移动宽带下且路由器打开了 IPv6 选项，2. 手机的移动网络下。 其实 IP 地址长的问题很好解决，最主要的问题是大部分网络环境下都不支持访问 IPv6 地址，比如我想在公司网络下访问家里的 NAS，这就做不到了。\n3、FRP 内网穿透 还好我老家家里用的是电信宽带，并且申请到了公网的 IPv4 IP。那我就可以在老家部署一台服务器，使用 FRP 工具，老家的服务器作为中转服务器，我在广州的 NAS 就可以实现内网穿透。\n4、域名 \u0026amp; DDNS 1. 域名 我们在外网访问 NAS 时，如果直接通过 IP 地址来访问会非常不方便，很简单的一个解决方法就是购买一个域名，然后把 IP 地址绑定到此域名，以后我们就只需要通过域名访问 NAS 就可以了。\n2. DDNS 无论是 IPv4 还是 IPv6 IP，只要重新拨号，地址就可能会被修改，这时候我们需要 DDNS 服务定时把最新的 IP 绑定到域名上。这里推荐使用 ddns-go。\n5、第三方服务 如果没有公网的 IPv4 IP，又不想用群晖的 QuickConnect 服务，可以考虑使用 NATAPP、花生壳 等付费服务。\n6、访问方式 1. HTTPS 外网访问时，HTTPS 比明文传输要安全得多，这里要注意的是 FRP 配置 SSL 证书，请查阅官方手册。\n2. 网页 最简单的访问方式就是网页访问，群晖的话就是默认的 5000 端口（HTTPS 则是 5001 端口）。\n3. 文件传输协议 外网访问时，我使用的是 WebDAV 协议；\n内网访问时，使用的是 smb 协议。\n还有 ftp、afp 等选择，请按需选择。\n三、服务 目前我在群晖里面安装的服务如下：\n1、套件中心 WebDAV Server，启动 WebDAV 服务； Cloud Sync，配合 webdav-aliyundriver 实现同步数据到阿里云盘； Snapshot Replication，数据定时快照，即使中毒了，也可以恢复到之前的版本； Virtual Machine Manager，虚拟机，我用于安装 OpenWrt； Transmission，P2P 下载； Download Station，下载工具，跟 Transmission 互补使用； Moments，用于备份照片； Syncthing，开源的多设备数据同步工具，我用于取代 iCloud Drive； Docker，很重要的一个套件； 2、Docker home-assistant，用于桥接米家设备到 HomeKit（之前用的是 homebridge）； ddns-go，ddns 服务，用于把最新的 IP 地址绑定到域名； icloudpd，iCloud 照片备份服务； chevereto，图床服务； frpc，内网穿透服务； webdav-aliyundriver，阿里云盘同步服务； jellyfin，媒体服务； ","permalink":"https://blog.axqora.com/posts/nas-skill/","summary":"\u003ch2 id=\"一前言\"\u003e一、前言\u003c/h2\u003e\n\u003cp\u003e距离去年入坑 NAS 已经一年多了（\u003ca href=\"https://blog.axqora.com/posts/nas-purchase/\"\u003e群晖 NAS - 购置篇\u003c/a\u003e），这一年多里学习了很多 NAS 的玩法，感觉能够发挥了 NAS 应有的作用。\u003c/p\u003e\n\u003cp\u003e这里简单介绍一下我利用 NAS 做了什么事情，但不会介绍详细的实现方法，因为网上已经有大量的参考文章。\u003c/p\u003e\n\u003ch2 id=\"二外网访问\"\u003e二、外网访问\u003c/h2\u003e\n\u003cp\u003e能在外网访问家里的 NAS，会多很多玩法，所以怎么实现外网访问是我入坑 NAS 的第一课。\u003c/p\u003e\n\u003ch3 id=\"1quickconnect\"\u003e1、QuickConnect\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://quickconnect.to/\"\u003eQuickConnect\u003c/a\u003e 是群晖官方提供的远程访问服务，使用起来最简单门槛最低，但存在几个缺点：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e限速比较严重，只有 500k/s 左右下载速度；\u003c/li\u003e\n\u003cli\u003e数据会经过群晖服务器进行中转，且账号必须绑定手机号，让我觉得会有隐私问题；\u003c/li\u003e\n\u003cli\u003e只有白群晖能用，黑群晖无法激活。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e基于这几个缺点，我需要找其它的替代方案。\u003c/p\u003e\n\u003ch3 id=\"2公网-ip\"\u003e2、公网 IP\u003c/h3\u003e\n\u003cp\u003e最方便的公网访问方式就是家里的宽带有公网 IP，无需转发直接访问，但不是所有宽带都能开通公网 IP。\u003c/p\u003e\n\u003cp\u003e这里的公网 IP 特指 \u003cstrong\u003eIPv4\u003c/strong\u003e 的公网 IP，因为 IPv4 的公网 IP 的兼容性最好，所有运营商都支持访问；IPv6 的公网 IP 甚至每个网口都能分配一个 IP，但目前来说应用还不是很广泛，有些运营商有些路由器都还不支持访问。\u003c/p\u003e\n\u003cp\u003e不同运营商情况大概如下：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e如果家里宽带用的是电信，那么可以打电话给客服免费开通公网 IP，很大概率可以开通，如果开通不了就多打几次电话。电信宽带还不支持分配和访问 IPv6 公网 IP。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e移动宽带则是完全不向个人开放 IPv4 的公网 IP，但是默认支持 IPv6 IP。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e联通的话据说申请难度比电信要高，但我还没用过没有发言权。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e我在广州家里用的是移动宽带，默认分配了 IPv6 的公网 IP，所以是可以直接 IPv6 IP 来访问我家里的 NAS。\u003c/p\u003e\n\u003cp\u003e但是访问 IPv6 的公网 IP 存在两个问题：\u003c/p\u003e","title":"群晖 NAS - 使用篇"},{"content":"本博客使用 Hugo 搭建，这里记录一下搭建过程。\nHugo is one of the most popular open-source static site generators. With its amazing speed and flexibility, Hugo makes building websites fun again.\n一、Hugo 基本使用 i. 命令使用 # Hugo 安装 brew install hugo # 新建博客 hugo new site hugo-blog # 添加主题（修改 config.yml 配置文件中 `theme: PaperMod`） git submodule add https://github.com/oudushu/hugo-PaperMod.git themes/PaperMod # 新建博文 hugo new posts/web-hugo-blog.md # 网站预览（http://localhost:1313/） hugo server # 生成静态文件（public 文件夹） hugo ii. 图片 图片资源存放在 static 文件夹中，我习惯每篇文章都新建一个文件夹存放图片，如：hugo-blog/static/image/web-hugo-blog/image-20220316160415439.png\n文章中引用图片：![image-20220316160841470](/image/web-hugo-blog/image-20220316160415439.png)\n二、服务端配置 云服务商：腾讯云\n操作系统：Ubuntu Server 18.04.1 LTS 64bit\ni. Nginx 安装 sudo apt-get install nginx ii. HTTPS 配置 首先需要申请 SSL 证书，这里不详述。\n把 SSL 证书放到 /etc/nginx/cert 路径下\n# 修改配置 sudo vim /etc/nginx/sites-available/default # 添加以下内容 server { # SSL configuration listen 443 ssl default_server; listen [::]:443 ssl default_server; ssl_certificate cert/c.pem; ssl_certificate_key cert/c.key; ssl_session_timeout 24h; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; root /var/www/html; # Add index.php to the list if you are using PHP index index.html index.htm index.nginx-debian.html; server_name _; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; } } # 重启 Nginx 服务 sudo service nginx restart iii. HTTP 转发到 HTTPS # 修改配置 sudo vim /etc/nginx/sites-available/default # 添加以下内容 server { listen 80; server_name _; return 301 https://$host$request_uri; } # 重启 Nginx 服务 sudo service nginx restart iv. 传输 Hugo 静态文件 把 Hugo 生成的静态文件 public 文件夹下所有文件传输到服务端 /var/www/html 路径下。\n这时候在浏览器访问服务器的地址，即可看到博客内容了。\nv. 一键发布脚本 每次写完文章都需要重新部署静态文件，稍显繁琐，这里写了两个脚本，可以减少每次部署静态文件的工作。\n客户端：\n#!/bin/bash if [ ! -n \u0026#34;$1\u0026#34; ] ;then echo \u0026#34;Aborting commit due to empty commit message.\u0026#34; else git stash git pull --rebase git stash pop hugo -D git add . git commit -m \u0026#34;$*\u0026#34; git push fi 服务端：\n#!/bin/bash GIT_STR=$(git pull) RES_STR=\u0026#34;Already up to date.\u0026#34; if [ \u0026#34;$GIT_STR\u0026#34; = \u0026#34;$RES_STR\u0026#34; ];then echo \u0026#34;$RES_STR\u0026#34; else echo \u0026#34;Updating to nginx.\u0026#34; sudo rm -rf /var/www/html/ sudo cp -R -a public/ /var/www/html/ fi 三、评论插件 评论模块是博客的重要功能，可以方便跟读者的沟通，我这里使用 utterances 插件作为评论模块。\nutterances 是一款基于 GitHub Issues 的开源评论插件，配置使用非常方便。\nA lightweight comments widget built on GitHub issues. Use GitHub issues for blog comments, wiki pages and more!\ni. 新建 GitHub Public 仓库 仓库必须设置为 Public，否则插件无法访问。\niii. 安装 GitHub App 打开 utterances - GitHub App 安装并配置，选择刚刚新建的 Public 仓库。\niii. Hugo 主题配置 这里以 PaperMod 主题为例，修改 themes/PaperMod/layouts/partials/post_nav_links.html 模版：\ndiff --git a/layouts/partials/post_nav_links.html b/layouts/partials/post_nav_links.html index b988641b..7352719d 100644 --- a/layouts/partials/post_nav_links.html +++ b/layouts/partials/post_nav_links.html @@ -16,4 +16,12 @@ \u0026lt;/a\u0026gt; {{- end }} \u0026lt;/nav\u0026gt; + +\u0026lt;script src=\u0026#34;https://utteranc.es/client.js\u0026#34; + repo=\u0026#34;oudushu/utterances\u0026#34; + issue-term=\u0026#34;title\u0026#34; + theme=\u0026#34;preferred-color-scheme\u0026#34; + crossorigin=\u0026#34;anonymous\u0026#34; + async\u0026gt; +\u0026lt;/script\u0026gt; {{- end }} iv. 效果 四、备案 众所周知的原因，国内的网站都需要到工信部进行备案，各大云服务平台都有相关备案服务（腾讯云备案），实际操作非常简单，只需要按照提示填写资料即可。一切顺利的话一周左右即可走完流程。\n我在备案过程中被打回了一次，原因是网站底部没有注明备案号。只需要在 Hugo 的主题模版中修改相关配置即可。\n同样以 PaperMod 主题为例，修改 themes/PaperMod/layouts/partials/footer.html 模版：\ndiff --git a/layouts/partials/footer.html b/layouts/partials/footer.html index 8dee3280..28185b5d 100644 --- a/layouts/partials/footer.html +++ b/layouts/partials/footer.html @@ -6,9 +6,8 @@ \u0026lt;span\u0026gt;\u0026amp;copy; {{ now.Year }} \u0026lt;a href=\u0026#34;{{ \u0026#34;\u0026#34; | absLangURL }}\u0026#34;\u0026gt;{{ .Site.Title }}\u0026lt;/a\u0026gt;\u0026lt;/span\u0026gt; {{- end }} \u0026lt;span\u0026gt; - Powered by - \u0026lt;a href=\u0026#34;https://gohugo.io/\u0026#34; rel=\u0026#34;noopener noreferrer\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;Hugo\u0026lt;/a\u0026gt; \u0026amp; - \u0026lt;a href=\u0026#34;https://git.io/hugopapermod\u0026#34; rel=\u0026#34;noopener\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;PaperMod\u0026lt;/a\u0026gt; + \u0026amp; \u0026lt;a href=\u0026#34;https://beian.miit.gov.cn\u0026#34; rel=\u0026#34;noopener\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;粤ICP备2021054614号\u0026lt;/a\u0026gt; \u0026lt;/span\u0026gt; \u0026lt;/footer\u0026gt; {{- end }} ","permalink":"https://blog.axqora.com/posts/web-hugo-blog/","summary":"\u003cp\u003e本博客使用 \u003ca href=\"https://gohugo.io/\"\u003eHugo\u003c/a\u003e 搭建，这里记录一下搭建过程。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eHugo is one of the most popular open-source static site generators. With its amazing speed and flexibility, Hugo makes building websites fun again.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"一hugo-基本使用\"\u003e一、Hugo 基本使用\u003c/h2\u003e\n\u003ch3 id=\"i-命令使用\"\u003ei. 命令使用\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# Hugo 安装\nbrew install hugo\n\n# 新建博客\nhugo new site hugo-blog\n\n# 添加主题（修改 config.yml 配置文件中 `theme: PaperMod`）\ngit submodule add https://github.com/oudushu/hugo-PaperMod.git themes/PaperMod\n\n# 新建博文\nhugo new posts/web-hugo-blog.md\n\n# 网站预览（http://localhost:1313/）\nhugo server\n\n# 生成静态文件（public 文件夹）\nhugo\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"ii-图片\"\u003eii. 图片\u003c/h3\u003e\n\u003cp\u003e图片资源存放在 \u003ccode\u003estatic\u003c/code\u003e 文件夹中，我习惯每篇文章都新建一个文件夹存放图片，如：\u003ccode\u003ehugo-blog/static/image/web-hugo-blog/image-20220316160415439.png\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e文章中引用图片：\u003ccode\u003e![image-20220316160841470](/image/web-hugo-blog/image-20220316160415439.png)\u003c/code\u003e\u003c/p\u003e","title":"Hugo 博客搭建"},{"content":"一、工具 在逆向的过程中，我们经常需要往目标执行文件中注入自己的逻辑，从而实现 hook 的目的。\n我们常用 optool 工具实现动态库注入。\nCommand Line Tool for interacting with MachO binaries on OSX/iOS\n了解这个工具的原理，有助于我们对 MachO 文件有更深入的了解。\n二、源码解析 关键代码如下：\nBOOL insertLoadEntryIntoBinary(NSString *dylibPath, NSMutableData *binary, struct thin_header macho, uint32_t type) { // 判断 Load Command 类型 if (type != LC_REEXPORT_DYLIB \u0026amp;\u0026amp; type != LC_LOAD_WEAK_DYLIB \u0026amp;\u0026amp; type != LC_LOAD_UPWARD_DYLIB \u0026amp;\u0026amp; type != LC_LOAD_DYLIB) { LOG(\u0026#34;Invalid load command type\u0026#34;); return NO; } // 判断动态库是否已经注入 // parse load commands to see if our load command is already there uint32_t lastOffset = 0; if (binaryHasLoadCommandForDylib(binary, dylibPath, \u0026amp;lastOffset, macho)) { // there already exists a load command for this payload so change the command type uint32_t originalType = *(uint32_t *)(binary.bytes + lastOffset); if (originalType != type) { LOG(\u0026#34;A load command already exists for %s. Changing command type from %s to desired %s\u0026#34;, dylibPath.UTF8String, LC(originalType), LC(type)); [binary replaceBytesInRange:NSMakeRange(lastOffset, sizeof(type)) withBytes:\u0026amp;type]; } else { LOG(\u0026#34;Load command already exists\u0026#34;); } return YES; } // create a new load command unsigned int length = (unsigned int)sizeof(struct dylib_command) + (unsigned int)dylibPath.length; unsigned int padding = (8 - (length % 8)); // 判断 Load Command 段末尾是否还有空白位置 // check if data we are replacing is null NSData *occupant = [binary subdataWithRange:NSMakeRange(macho.header.sizeofcmds + macho.offset + macho.size, length + padding)]; // All operations in optool try to maintain a constant byte size of the executable // so we don\u0026#39;t want to append new bytes to the binary (that would break the executable // since everything is offset-based–we\u0026#39;d have to go in and adjust every offset) // So instead take advantage of the huge amount of padding after the load commands if (strcmp([occupant bytes], \u0026#34;\\0\u0026#34;)) { NSLog(@\u0026#34;cannot inject payload into %s because there is no room\u0026#34;, dylibPath.fileSystemRepresentation); return NO; } LOG(\u0026#34;Inserting a %s command for architecture: %s\u0026#34;, LC(type), CPU(macho.header.cputype)); // 新建 dylib_command 并替换到对应位置的空白区域 struct dylib_command command; struct dylib dylib; dylib.name.offset = sizeof(struct dylib_command); dylib.timestamp = 2; // load commands I\u0026#39;ve seen use 2 for some reason dylib.current_version = 0; dylib.compatibility_version = 0; command.cmd = type; command.dylib = dylib; command.cmdsize = length + padding; unsigned int zeroByte = 0; NSMutableData *commandData = [NSMutableData data]; [commandData appendBytes:\u0026amp;command length:sizeof(struct dylib_command)]; [commandData appendData:[dylibPath dataUsingEncoding:NSASCIIStringEncoding]]; [commandData appendBytes:\u0026amp;zeroByte length:padding]; // remove enough null bytes to account of our inserted data [binary replaceBytesInRange:NSMakeRange(macho.offset + macho.header.sizeofcmds + macho.size, commandData.length) withBytes:0 length:0]; // insert the data [binary replaceBytesInRange:NSMakeRange(lastOffset, 0) withBytes:commandData.bytes length:commandData.length]; // 修改 header 内容， // fix the existing header macho.header.ncmds += 1; macho.header.sizeofcmds += command.cmdsize; // this is safe to do in 32bit because the 4 bytes after the header are still being put back [binary replaceBytesInRange:NSMakeRange(macho.offset, sizeof(macho.header)) withBytes:\u0026amp;macho.header]; return YES; } 三、MachO 文件变化 利用 MachOView 工具查看注入动态库后的 MachO 文件，可以发现在 Load Command 段的后面多了一个 LC，此 LC 明确了动态库的加载路径；且可发现其他段的 offset 并没有被修改。\n四、总结 动态库注入的流程总结如下：\n判断 Load Command 类型（LC_REEXPORT_DYLIB/LC_LOAD_WEAK_DYLIB/LC_LOAD_UPWARD_DYLIB/LC_LOAD_DYLIB）； 判断动态库是否已经注入（通过遍历 Load Command 段，取出几种 dylib 的 LC 的 name 字段跟加载动态库的路径做比对）； 判断 Load Command 段末尾是否还有空白位置（1. segment 中进行页对齐时会产生空白区域；2. 避免改变可执行文件大小；3. MachO 里几乎所有操作都基于偏移量进行计算，如果直接插入新的数据，会导致后面的偏移量都需要调整。）； 新建 dylib_command 并替换到对应位置的空白区域； 修改 header 内容； 把动态库复制到应用内的 Frameworks 文件夹中，重签名后打包成 ipa 包并安装到手机； iOS 系统使用 dyld 加载应用，加载的 Load Command 时会找到对应的动态库并进行动态链接。 ","permalink":"https://blog.axqora.com/posts/ios-reverse-insert-dylib/","summary":"\u003ch2 id=\"一工具\"\u003e一、工具\u003c/h2\u003e\n\u003cp\u003e在逆向的过程中，我们经常需要往目标执行文件中注入自己的逻辑，从而实现 hook 的目的。\u003c/p\u003e\n\u003cp\u003e我们常用 \u003ca href=\"https://github.com/alexzielenski/optool\"\u003eoptool\u003c/a\u003e 工具实现动态库注入。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eCommand Line Tool for interacting with MachO binaries on OSX/iOS\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e了解这个工具的原理，有助于我们对 MachO 文件有更深入的了解。\u003c/p\u003e\n\u003ch2 id=\"二源码解析\"\u003e二、源码解析\u003c/h2\u003e\n\u003cp\u003e关键代码如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eBOOL insertLoadEntryIntoBinary(NSString *dylibPath, NSMutableData *binary, struct thin_header macho, uint32_t type) {\n    // 判断 Load Command 类型\n    if (type != LC_REEXPORT_DYLIB \u0026amp;\u0026amp;\n        type != LC_LOAD_WEAK_DYLIB \u0026amp;\u0026amp;\n        type != LC_LOAD_UPWARD_DYLIB \u0026amp;\u0026amp;\n        type != LC_LOAD_DYLIB) {\n        LOG(\u0026#34;Invalid load command type\u0026#34;);\n        return NO;\n    }\n    // 判断动态库是否已经注入\n    // parse load commands to see if our load command is already there\n    uint32_t lastOffset = 0;\n    if (binaryHasLoadCommandForDylib(binary, dylibPath, \u0026amp;lastOffset, macho)) {\n        // there already exists a load command for this payload so change the command type\n        uint32_t originalType = *(uint32_t *)(binary.bytes + lastOffset);\n        if (originalType != type) {\n            LOG(\u0026#34;A load command already exists for %s. Changing command type from %s to desired %s\u0026#34;, dylibPath.UTF8String, LC(originalType), LC(type));\n            [binary replaceBytesInRange:NSMakeRange(lastOffset, sizeof(type)) withBytes:\u0026amp;type];\n        } else {\n            LOG(\u0026#34;Load command already exists\u0026#34;);\n        }\n        \n        return YES;\n    }\n    \n    // create a new load command\n    unsigned int length = (unsigned int)sizeof(struct dylib_command) + (unsigned int)dylibPath.length;\n    unsigned int padding = (8 - (length % 8));\n    \n    // 判断 Load Command 段末尾是否还有空白位置\n    // check if data we are replacing is null\n    NSData *occupant = [binary subdataWithRange:NSMakeRange(macho.header.sizeofcmds + macho.offset + macho.size,\n                                                            length + padding)];\n\n    // All operations in optool try to maintain a constant byte size of the executable\n    // so we don\u0026#39;t want to append new bytes to the binary (that would break the executable\n    // since everything is offset-based–we\u0026#39;d have to go in and adjust every offset)\n    // So instead take advantage of the huge amount of padding after the load commands\n    if (strcmp([occupant bytes], \u0026#34;\\0\u0026#34;)) {\n        NSLog(@\u0026#34;cannot inject payload into %s because there is no room\u0026#34;, dylibPath.fileSystemRepresentation);\n        return NO;\n    }\n    \n    LOG(\u0026#34;Inserting a %s command for architecture: %s\u0026#34;, LC(type), CPU(macho.header.cputype));\n    \n    // 新建 dylib_command 并替换到对应位置的空白区域\n    struct dylib_command command;\n    struct dylib dylib;\n    dylib.name.offset = sizeof(struct dylib_command);\n    dylib.timestamp = 2; // load commands I\u0026#39;ve seen use 2 for some reason\n    dylib.current_version = 0;\n    dylib.compatibility_version = 0;\n    command.cmd = type;\n    command.dylib = dylib;\n    command.cmdsize = length + padding;\n    \n    unsigned int zeroByte = 0;\n    NSMutableData *commandData = [NSMutableData data];\n    [commandData appendBytes:\u0026amp;command length:sizeof(struct dylib_command)];\n    [commandData appendData:[dylibPath dataUsingEncoding:NSASCIIStringEncoding]];\n    [commandData appendBytes:\u0026amp;zeroByte length:padding];\n    \n    // remove enough null bytes to account of our inserted data\n    [binary replaceBytesInRange:NSMakeRange(macho.offset + macho.header.sizeofcmds + macho.size, commandData.length)\n                      withBytes:0\n                         length:0];\n    // insert the data\n    [binary replaceBytesInRange:NSMakeRange(lastOffset, 0) withBytes:commandData.bytes length:commandData.length];\n    \n    // 修改 header 内容，\n    // fix the existing header\n    macho.header.ncmds += 1;\n    macho.header.sizeofcmds += command.cmdsize;\n    \n    // this is safe to do in 32bit because the 4 bytes after the header are still being put back\n    [binary replaceBytesInRange:NSMakeRange(macho.offset, sizeof(macho.header)) withBytes:\u0026amp;macho.header];\n    \n    return YES;\n}\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"三macho-文件变化\"\u003e三、MachO 文件变化\u003c/h2\u003e\n\u003cp\u003e利用 MachOView 工具查看注入动态库后的 MachO 文件，可以发现在 Load Command 段的后面多了一个 LC，此 LC 明确了动态库的加载路径；且可发现其他段的 offset 并没有被修改。\u003c/p\u003e","title":"iOS 逆向入门 - 动态库注入原理"},{"content":"在通过逆向分析竞品过程中，经常需要分析其实现逻辑。但由于没有符号，我们会遇到一些阻碍。\n一、符号恢复 我们经常 hook 一个方法并加断点可快速获得参数值，但是使用 bt 命令打印堆栈信息时，只能看到 ___lldb_unnamed_symbol279180$$TikTok 的信息，原因是 Objective-C 在打包时会被 stripped out，导致无法看到具体符号。\n1. restore-symbol 使用 可以利用 restore-symbol 工具恢复符号（只能恢复 Objective-C 符号，无法恢复 C/C++ 符号），具体使用如下：\n# git clone --recursive https://github.com/tobefuturer/restore-symbol.git # cd restore-symbol # make # ./restore-symbol TikTok -o TikTok_symbol 2. 错误处理 在运行时会报错：2022-01-06 18:07:15.103 restore-symbol[17534:7647405] *** Assertion failure in -[CDObjectiveC2Processor loadClassAtAddress:], CDObjectiveC2Processor.m:258，原因是使用了 Swift 语言，而解析 Swift 的方法已过期。我们可以简单粗暴地把抛出异常的代码注释掉，重新 make 即可成功恢复符号。\n3. 验证 把 ipa 包里的 MachO 文件替换为恢复符号后的 MachO 文件，重签名后运行到手机上（MonkeyDev 工具已经集成重签名的功能，这里只需要替换 TargetApp 里面的 MachO 文件，clean 后 rebuild 即可）。\n在断点处使用 bt 命令打印堆栈信息：\n可以看到大部分 ___lldb_unnamed_symbol279180$$TikTok 都被恢复符号了，还有部分未被恢复的猜测是使用 Swift 语言编写的代码。\n二、IDA Pro 反汇编工具 下面通过反编译上面输出的堆栈：frame #3: 0x000000010650cdac TikTok -[LSGLPreviewView newFrameReadyAtTime:atIndex:] + 512 简单介绍 IDA 的使用。\n1. 使用 IDA 打开 MachO 文件 IDA 分析 MachO 文件时间较长，需要耐心等待。\n2. 计算对应方法在 MachO 文件中的偏移地址 在断点处，输入 image list -o -f，可获得 ASLR 偏移地址，运行时基地址(0x000000010650cdac) = ASLR 偏移地址(0x0000000002a00000) + 基地址(0x103B0CDAC)\n3. 定位到基地址 在 IDA 中使用快捷键 g，可跳转到地址\n4. 反汇编 使用快捷键 F5，反汇编对应代码。至此，我们已经可以大致看到 -[LSGLPreviewView newFrameReadyAtTime:atIndex:] 这个方法的实现逻辑了。\n三、暂未解决问题 Swift、C/C++ 的无法恢复符号问题； 恢复符号后，Instruments 依然无法显示符号； ","permalink":"https://blog.axqora.com/posts/ios-restore-symbol/","summary":"\u003cp\u003e在通过逆向分析竞品过程中，经常需要分析其实现逻辑。但由于没有符号，我们会遇到一些阻碍。\u003c/p\u003e\n\u003ch2 id=\"一符号恢复\"\u003e一、符号恢复\u003c/h2\u003e\n\u003cp\u003e我们经常 hook 一个方法并加断点可快速获得参数值，但是使用 bt 命令打印堆栈信息时，只能看到 \u003ccode\u003e___lldb_unnamed_symbol279180$$TikTok\u003c/code\u003e 的信息，原因是 Objective-C 在打包时会被 stripped out，导致无法看到具体符号。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220106175647663\" loading=\"lazy\" src=\"/image/iOS-restore-symbol/image-20220106175647663.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"1-restore-symbol-使用\"\u003e1. restore-symbol 使用\u003c/h3\u003e\n\u003cp\u003e可以利用 \u003ca href=\"https://github.com/tobefuturer/restore-symbol\"\u003erestore-symbol\u003c/a\u003e 工具恢复符号（只能恢复 Objective-C 符号，无法恢复 C/C++ 符号），具体使用如下：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# git clone --recursive https://github.com/tobefuturer/restore-symbol.git\n# cd restore-symbol\n# make\n# ./restore-symbol TikTok -o TikTok_symbol\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"2-错误处理\"\u003e2. 错误处理\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220106180745368\" loading=\"lazy\" src=\"/image/iOS-restore-symbol/image-20220106180745368.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220106181010510\" loading=\"lazy\" src=\"/image/iOS-restore-symbol/image-20220106181010510.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20220106181518223\" loading=\"lazy\" src=\"/image/iOS-restore-symbol/image-20220106181518223.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在运行时会报错：\u003ccode\u003e2022-01-06 18:07:15.103 restore-symbol[17534:7647405] *** Assertion failure in -[CDObjectiveC2Processor loadClassAtAddress:], CDObjectiveC2Processor.m:258\u003c/code\u003e，原因是使用了 Swift 语言，而解析 Swift 的方法已过期。我们可以简单粗暴地把抛出异常的代码注释掉，重新 make 即可成功恢复符号。\u003c/p\u003e\n\u003ch3 id=\"3-验证\"\u003e3. 验证\u003c/h3\u003e\n\u003cp\u003e把 ipa 包里的 MachO 文件替换为恢复符号后的 MachO 文件，重签名后运行到手机上（MonkeyDev 工具已经集成重签名的功能，这里只需要替换 TargetApp 里面的 MachO 文件，clean 后 rebuild 即可）。\u003c/p\u003e","title":"iOS 逆向入门 - 符号恢复及反汇编"},{"content":"开发工具 iTerm2 iTerm2 is a terminal emulator for macOS that does amazing things.\nTabby Tabby is an infinitely customizable cross-platform terminal app for local shells, serial, SSH and Telnet connections.\nFig Fig adds VSCode-style autocomplete to your existing terminal.\n终端自动补全工具，能在终端里有 VSCode 的自动补全体验，支持 iTerm2。如果你使用的是 zsh，也可以安装 zsh-autosuggestions 作为补充使用。\nHomebrew The Missing Package Manager for macOS.\nOh My Zsh Oh My Zsh is an open source, community-driven framework for managing your ### zsh configuration.\nXcodes The easiest way to install and switch between multiple versions of Xcode.\nCakebrew The Homebrew GUI App for OS X\nMacSymbolicator A simple Mac app for symbolicating macOS/iOS crash reports.\nTinyPNG4Mac TinyPNG client for Mac.\nFork A fast and friendly GIT client.\n日常工具 Raycast Raycast is a blazingly fast, totally extendable launcher.\n系统 Spotlight 的替代产品，插件超多，另一个出名的类似产品就是 Alfred，我选择这个的原因是功能设计更符合我的喜好。\nTypora A truly minimal markdown editor.\nMarkdown 编辑器，我用过最好用的 Markdown 编辑器，最近正式版也上线了，我第一时间也付费支持。\nSnipaste Snip \u0026amp; Paste\n截图工具，功能齐全，性能稳定，缺点是不支持截长图。\nMacZip MacZip Mac版是Mac电脑上的一款专为Mac系统设计的压缩解压软件\nPicGo 一个用于快速上传图片并获取图片 URL 链接的工具\niStat Menus An advanced Mac system monitor for your menubar.\n腾讯柠檬清理 腾讯柠檬清理- 全新Mac清理工具，实时了解Mac系统状况\nClashX 代理客户端\nShadowsocksX-NG 代理客户端\n娱乐相关 IINA The modern media player for macOS.\n持续更新。。。\n","permalink":"https://blog.axqora.com/posts/mac-tools/","summary":"\u003ch2 id=\"开发工具\"\u003e开发工具\u003c/h2\u003e\n\u003ch3 id=\"iterm2\"\u003e\u003ca href=\"https://iterm2.com/\"\u003eiTerm2\u003c/a\u003e\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003eiTerm2 is a terminal emulator for macOS that does amazing things.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"tabby\"\u003e\u003ca href=\"https://tabby.sh/\"\u003eTabby\u003c/a\u003e\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003eTabby is an infinitely customizable cross-platform terminal app for local shells, serial, SSH and Telnet connections.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"fig\"\u003e\u003ca href=\"https://fig.io/\"\u003eFig\u003c/a\u003e\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003eFig adds VSCode-style autocomplete to your existing terminal.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e终端自动补全工具，能在终端里有 VSCode 的自动补全体验，支持 \u003ccode\u003eiTerm2\u003c/code\u003e。如果你使用的是 \u003ccode\u003ezsh\u003c/code\u003e，也可以安装 \u003ca href=\"https://github.com/zsh-users/zsh-autosuggestions\"\u003ezsh-autosuggestions\u003c/a\u003e 作为补充使用。\u003c/p\u003e\n\u003ch3 id=\"homebrew\"\u003e\u003ca href=\"https://brew.sh/\"\u003eHomebrew\u003c/a\u003e\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003eThe Missing Package Manager for macOS.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"oh-my-zsh\"\u003e\u003ca href=\"https://github.com/ohmyzsh/ohmyzsh\"\u003eOh My Zsh\u003c/a\u003e\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003eOh My Zsh is an open source, community-driven framework for managing your ### \u003ca href=\"https://www.zsh.org/\"\u003ezsh\u003c/a\u003e configuration.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"xcodes\"\u003e\u003ca href=\"https://github.com/RobotsAndPencils/XcodesApp\"\u003eXcodes\u003c/a\u003e\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003eThe easiest way to install and switch between multiple versions of Xcode.\u003c/p\u003e","title":"Mac 装机必备"},{"content":"在「iOS 逆向入门 - 绕过抖音反调试」中提到常用反调试手段，这里写下具体实现。\n一、常用反逆向手段 1、反调试：ptrace ptrace 被常用于防止 lldb 依附，原理是一个进程只能被 ptrace 只能被一次，先于别人调用 ptrace 则可以防止别人依附。\nptrace 有几种方式进行调用：\n1）直接调用 #import \u0026lt;sys/ptrace.h\u0026gt; ptrace(PT_DENY_ATTACH,0,0,0); iOS SDK 中不包含 ptrace.h 头文件，无法使用此方法调用，可使用以下方法。\n2）通过 dlopen + dlsym 调用 #import \u0026lt;dlfcn.h\u0026gt; #import \u0026lt;sys/types.h\u0026gt; typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data); #if !defined(PT_DENY_ATTACH) #define PT_DENY_ATTACH 31 #endif void disable_attach() { void* handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW); ptrace_ptr_t ptrace_ptr = dlsym(handle, \u0026#34;ptrace\u0026#34;); ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0); dlclose(handle); } 3）通过 syscall 调用 #import \u0026lt;sys/syscall.h\u0026gt; #if !defined(PT_DENY_ATTACH) #define PT_DENY_ATTACH 31 #endif void disable_attach() { syscall(SYS_ptrace, PT_DENY_ATTACH, 0, 0, 0); // #define SYS_ptrace 26 } 4）通过汇编调用 内联 svc + ptrace 实现，相当于直接调用 ptrace(PT_DENY_ATTACH,0,0,0);\nstatic __attribute__((always_inline)) void disable_attach() { #ifdef __arm64__ __asm__(\u0026#34;mov X0, #0x1F\u0026#34;); __asm__(\u0026#34;mov X1, #0\u0026#34;); __asm__(\u0026#34;mov X2, #0\u0026#34;); __asm__(\u0026#34;mov X3, #0\u0026#34;); __asm__(\u0026#34;mov X16, #0x1A\u0026#34;); __asm__(\u0026#34;svc #0x80\u0026#34;); #endif } 内联 svc + syscall + ptrace 实现，相当于 syscall(SYS_ptrace, PT_DENY_ATTACH, 0, 0, 0);\nstatic __attribute__((always_inline)) void disable_attach() { #ifdef __arm64__ __asm__(\u0026#34;mov X0, #0x1A\u0026#34;); __asm__(\u0026#34;mov X1, #0x1F\u0026#34;); __asm__(\u0026#34;mov X2, #0\u0026#34;); __asm__(\u0026#34;mov X3, #0\u0026#34;); __asm__(\u0026#34;mov X4, #0\u0026#34;); __asm__(\u0026#34;mov X16, #0\u0026#34;); __asm__(\u0026#34;svc #0x80\u0026#34;); #endif } 2、sysctl 通过 sysctl 去查看当前进程的信息，看有没有这个标记位即可检查当前调试状态。\n#import \u0026lt;sys/sysctl.h\u0026gt; bool isBeingAttach() { size_t size = sizeof(struct kinfo_proc); struct kinfo_proc info; int ret = 0, name[4]; memset(\u0026amp;info, 0, sizeof(struct kinfo_proc)); name[0] = CTL_KERN; name[1] = KERN_PROC; name[2] = KERN_PROC_PID; name[3] = getpid(); if (ret == (sysctl(name, 4, \u0026amp;info, \u0026amp;size, NULL, 0))) { return ret != 0; } return (info.kp_proc.p_flag \u0026amp; P_TRACED) ? YES : NO; } ","permalink":"https://blog.axqora.com/posts/ios-reverse-anti/","summary":"\u003cp\u003e在「\u003ca href=\"https://blog.axqora.com/posts/ios-reverse-lldb-attach/\"\u003eiOS 逆向入门 - 绕过抖音反调试\u003c/a\u003e」中提到常用反调试手段，这里写下具体实现。\u003c/p\u003e\n\u003ch2 id=\"一常用反逆向手段\"\u003e一、常用反逆向手段\u003c/h2\u003e\n\u003ch3 id=\"1反调试ptrace\"\u003e1、反调试：ptrace\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://blog.csdn.net/yiyaaixuexi/article/details/18222339\"\u003e\u003ccode\u003eptrace\u003c/code\u003e\u003c/a\u003e 被常用于防止 lldb 依附，原理是一个进程只能被 \u003ccode\u003eptrace\u003c/code\u003e 只能被一次，先于别人调用 \u003ccode\u003eptrace\u003c/code\u003e 则可以防止别人依附。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eptrace\u003c/code\u003e 有几种方式进行调用：\u003c/p\u003e\n\u003ch4 id=\"1直接调用\"\u003e1）直接调用\u003c/h4\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e#import \u0026lt;sys/ptrace.h\u0026gt;\n\nptrace(PT_DENY_ATTACH,0,0,0);\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eiOS SDK 中不包含 ptrace.h 头文件，无法使用此方法调用，可使用以下方法。\u003c/p\u003e\n\u003ch4 id=\"2通过-dlopen--dlsym-调用\"\u003e2）通过 \u003ccode\u003edlopen\u003c/code\u003e + \u003ccode\u003edlsym\u003c/code\u003e 调用\u003c/h4\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e#import \u0026lt;dlfcn.h\u0026gt;\n#import \u0026lt;sys/types.h\u0026gt;\n \ntypedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);\n#if !defined(PT_DENY_ATTACH)\n#define PT_DENY_ATTACH 31\n#endif\n \nvoid disable_attach() {\n    void* handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);\n    ptrace_ptr_t ptrace_ptr = dlsym(handle, \u0026#34;ptrace\u0026#34;);\n    ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);\n    dlclose(handle);\n}\n\u003c/code\u003e\u003c/pre\u003e\u003ch4 id=\"3通过-syscall-调用\"\u003e3）通过 \u003ccode\u003esyscall\u003c/code\u003e 调用\u003c/h4\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e#import \u0026lt;sys/syscall.h\u0026gt;\n\n#if !defined(PT_DENY_ATTACH)\n#define PT_DENY_ATTACH 31\n#endif\n\nvoid disable_attach() {\n    syscall(SYS_ptrace, PT_DENY_ATTACH, 0, 0, 0); // #define SYS_ptrace 26\n}\n\u003c/code\u003e\u003c/pre\u003e\u003ch4 id=\"4通过汇编调用\"\u003e4）通过汇编调用\u003c/h4\u003e\n\u003cp\u003e内联 svc + ptrace 实现，相当于直接调用 \u003ccode\u003eptrace(PT_DENY_ATTACH,0,0,0);\u003c/code\u003e\u003c/p\u003e","title":"iOS 逆向入门 - 常用反逆向手段"},{"content":"一、dSYM 文件生成 1、Xcode 自动生成，配置： Xcode -\u0026gt; Build Settings -\u0026gt; Code Generation -\u0026gt; Generate Debug Symbols -\u0026gt; Yes\nXcode -\u0026gt; Build Settings -\u0026gt; Build Option -\u0026gt; Debug Information Format -\u0026gt; DWARF with dSYM File\n2、手动生成： /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/dsymutil /Users/oudushu/Library/Developer/Xcode/DerivedData/YourApp-cqvijavqbptjyhbwewgpdmzbmwzk/Build/Products/Debug-iphonesimulator/YourApp.app/YourApp -o YourApp.dSYM 二、如何找到对应的 dSYM 文件： 从发布的归档包里面找： Xcode -\u0026gt; Window -\u0026gt; Organizer -\u0026gt; 找到打包好的文件（Show in Finder）-\u0026gt; 选中文件（右键显示包内容）-\u0026gt; dSYMs文件夹下就是了 从iTunes Connect里面找 mdfind工具：mdfind \u0026ldquo;com_apple_xcode_dsym_uuids == E30FC309-DF7B-3C9F-8AC5-7F0F6047D65F\u0026rdquo; 三、symbolicatecrash 1、查找 find /Applications/Xcode.app -name symbolicatecrash -type f /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash 2、使用 export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer ./symbolicatecrash ./appName.crash ./appName.app.dSYM \u0026gt; customName.log 四、atos 1、使用 atos -o yourAppName.app.dSYM/Contents/Resources/DWARF/yourAppName -arch arm64/armv7 -l \u0026lt;load-address\u0026gt; \u0026lt;address\u0026gt; 或 atos -o yourAppName.app.dSYM/Contents/Resources/DWARF/yourAppName -arch arm64/armv7 \u0026lt;address\u0026gt; // 崩溃地址对应的符号表地址 二、实践 1、直接传入基地址与方法地址 atos -o liveshow.app.dSYM/Contents/Resources/DWARF/liveshow -arch arm64 -l 0x100124000 0x00000001027bd964 __55+[BLCrashGuard setupCrashGuardWithConfig:captureBlock:]_block_invoke_2 (in liveshow) (BLCrashGurad.m:0) 2、计算崩溃地址对应的符号表地址 1、从崩溃堆栈文件可知：\n运行时堆栈地址（0x00000001027bd964） 运行时起始地址（0x100124000） 偏移量（40474980，转为十六进制：0x2699964） 2、符号表堆栈地址计算方式： 运行时堆栈地址 = 运行时起始地址 + 偏移量\n3、根据虚拟内存偏移量不变原理 符号表堆栈地址 = 符号表起始地址 + 偏移量\n知道偏移量后，只需知道符号表起始地址，即可计算出符号表堆栈地址。\n可通过 otool 工具获得 __TEXT 段起始地址：\notool -l liveshow.app.dSYM/Contents/Resources/DWARF/liveshow Load command 3 cmd LC_SEGMENT_64 cmdsize 1912 segname __TEXT vmaddr 0x0000000100000000 vmsize 0x0000000005bf8000 fileoff 0 filesize 0 maxprot 0x00000005 initprot 0x00000005 nsects 23 flags 0x0 0x0000000102699964 = 0x0000000100000000 + 0x2699964\natos -o liveshow.app.dSYM/Contents/Resources/DWARF/liveshow -arch arm64 0x0000000102699964 __55+[BLCrashGuard setupCrashGuardWithConfig:captureBlock:]_block_invoke_2 (in liveshow) (BLCrashGurad.m:0) 五、dwarfdump 1、DWARF（Debugging With Attributed Record Formats） .xcarchive \u0026ndash;dSYMs |\u0026ndash;Your.app.dSYM |\u0026ndash;Contents |\u0026ndash;Resources |\u0026ndash;DWARF\n2、查看 dSYM 文件的 UUID： dwarfdump --uuid Your.app.dSYM 或 dwarfdump --uuid Your.app.dSYM/Contents/Resources/DWARF/Your 3、崩溃信息还原： dwarfdump --arch armv7 Your.app.dSYM --lookup \u0026lt;address\u0026gt; // 符号表堆栈地址 dwarfdump --arch arm64 liveshow.app.dSYM --lookup 0x0000000102699964 liveshow.app.dSYM/Contents/Resources/DWARF/liveshow(arm64):\tfile format Mach-O arm64 0x0218d124: Compile Unit: length = 0x00000a63, format = DWARF32, version = 0x0004, abbr_offset = 0x0000, addr_size = 0x08 (next unit at 0x0218db8b) 0x0218d12f: DW_TAG_compile_unit DW_AT_producer\t(\u0026#34;Apple clang version 12.0.0 (clang-1200.0.32.29)\u0026#34;) DW_AT_language\t(DW_LANG_ObjC) DW_AT_name\t(\u0026#34;/Users/Shared/Jenkins_Legacy/workspace/UNQ_liveLIVE_RELEASE_3/Pods/BLCrashGuard/BLCrashGuard/Classes/BLCrashGurad.m\u0026#34;) DW_AT_LLVM_sysroot\t(\u0026#34;/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.4.sdk\u0026#34;) DW_AT_APPLE_sdk\t(\u0026#34;iPhoneOS14.4.sdk\u0026#34;) DW_AT_stmt_list\t(0x01351f66) DW_AT_comp_dir\t(\u0026#34;/Users/Shared/Jenkins_Legacy/workspace/UNQ_liveLIVE_RELEASE_3/Pods\u0026#34;) DW_AT_APPLE_optimized\t(true) DW_AT_APPLE_major_runtime_vers\t(0x02) DW_AT_low_pc\t(0x00000001002c1c20) DW_AT_high_pc\t(0x00000001026999f0) 0x0218da06: DW_TAG_subprogram DW_AT_low_pc\t(0x0000000102699914) DW_AT_high_pc\t(0x00000001026999f0) DW_AT_frame_base\t(DW_OP_reg29 W29) DW_AT_call_all_calls\t(true) DW_AT_linkage_name\t(\u0026#34;__55+[BLCrashGuard setupCrashGuardWithConfig:captureBlock:]_block_invoke_2\u0026#34;) DW_AT_name\t(\u0026#34;__55+[BLCrashGuard setupCrashGuardWithConfig:captureBlock:]_block_invoke_2\u0026#34;) DW_AT_decl_file\t(\u0026#34;/Users/Shared/Jenkins_Legacy/workspace/UNQ_liveLIVE_RELEASE_3/Pods/BLCrashGuard/BLCrashGuard/Classes/BLCrashGurad.m\u0026#34;) DW_AT_decl_line\t(32) DW_AT_prototyped\t(true) DW_AT_APPLE_optimized\t(true) Line info: file \u0026#39;BLCrashGurad.m\u0026#39;, line 0, column 91, start line 32 六、其它常用命令行工具 1、otool otool命令全称object file displaying tool，是针对目标文件的展示工具，可以帮助我们发现应用中使用了哪些系统库，以及调用了哪些对象的方法和属性。 常用的几个子命令如下：\notool -L path // 查看可执行程序都链接了那些库 otool -ov path // 输出 Object-C 类结构以及定义的方法 2、lipo lipo 命令的作用是 create or operate on universal files，即创建或处理通用文件的工具。 常用的几个子命令如下：\nlipo -info XXX // 判断静态库所支持的平台 armv7 x86_64 arm64 lipo -remove armv7 origin_xxx.a -output op_xxx.a // 删除静态库包括的 armv7 平台 lipo -thin arm64 origin_xxx.a -output op_xxx.a // 拆分静态库，只保留 arm64 CPU 架构 lipo -create device_xxx.a simulator_xxx.a -output universal_xxx.a // 对真机或者模拟器分别打出 .a 文件合并 3、nm nm 命令的作用是显示符号表，常用的几个子命令如下：\nnm path //得到Mach-O中的程序符号表 nm -nm path //目标文件的所有符号 nm -u path //Display only undefined symbols. 4、strings strings 命令，它可以帮助我们排查二进制文件中是否包含相关字符串，常用的命令为：\nstrings XXX | grep yourString 5、ar ar -t xxx.a 可以知道静态库里的 .o 文件（ .a 文件是 .o 文件的合集） ","permalink":"https://blog.axqora.com/posts/ios-dsym-symbolicate/","summary":"\u003ch2 id=\"一dsym-文件生成\"\u003e一、dSYM 文件生成\u003c/h2\u003e\n\u003ch3 id=\"1xcode-自动生成配置\"\u003e1、Xcode 自动生成，配置：\u003c/h3\u003e\n\u003cp\u003eXcode -\u0026gt; Build Settings -\u0026gt; Code Generation -\u0026gt; Generate Debug Symbols -\u0026gt; Yes\u003c/p\u003e\n\u003cp\u003eXcode -\u0026gt; Build Settings -\u0026gt; Build Option -\u0026gt; Debug Information Format -\u0026gt; DWARF with dSYM File\u003c/p\u003e\n\u003ch3 id=\"2手动生成\"\u003e2、手动生成：\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/dsymutil \n\n/Users/oudushu/Library/Developer/Xcode/DerivedData/YourApp-cqvijavqbptjyhbwewgpdmzbmwzk/Build/Products/Debug-iphonesimulator/YourApp.app/YourApp -o YourApp.dSYM\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"二如何找到对应的-dsym-文件\"\u003e二、如何找到对应的 dSYM 文件：\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e从发布的归档包里面找：\nXcode -\u0026gt; Window -\u0026gt; Organizer -\u0026gt; 找到打包好的文件（Show in Finder）-\u0026gt; 选中文件（右键显示包内容）-\u0026gt; dSYMs文件夹下就是了\u003c/li\u003e\n\u003cli\u003e从iTunes Connect里面找\u003c/li\u003e\n\u003cli\u003emdfind工具：mdfind \u0026ldquo;com_apple_xcode_dsym_uuids == E30FC309-DF7B-3C9F-8AC5-7F0F6047D65F\u0026rdquo;\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"三symbolicatecrash\"\u003e三、symbolicatecrash\u003c/h2\u003e\n\u003ch3 id=\"1查找\"\u003e1、查找\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003efind /Applications/Xcode.app -name symbolicatecrash -type f\n\n/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"2使用\"\u003e2、使用\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eexport DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer\n\n./symbolicatecrash ./appName.crash ./appName.app.dSYM \u0026gt; customName.log\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"四atos\"\u003e四、atos\u003c/h2\u003e\n\u003ch3 id=\"1使用\"\u003e1、使用\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eatos -o yourAppName.app.dSYM/Contents/Resources/DWARF/yourAppName -arch arm64/armv7 -l \u0026lt;load-address\u0026gt; \u0026lt;address\u0026gt;\n或\natos -o yourAppName.app.dSYM/Contents/Resources/DWARF/yourAppName -arch arm64/armv7 \u0026lt;address\u0026gt; // 崩溃地址对应的符号表地址\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"二实践\"\u003e二、实践\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/image/iOS-dsym-symbolicate/image-20211208152745891.png\"\u003e\u003c/p\u003e","title":"iOS dSYM 文件 \u0026 符号化"},{"content":"一、背景 经过之前对 iOS 逆向的初步了解，想了个实现 TikTok 自动播放下一个视频的小需求实践一下。\n二、实现过程 1、使用 class-dump 工具获取类信息 class-dump 工具通过解析 MachO 文件生成类信息，包括 OC 方法、属性、成员变量等。\n有几个点需要留意：\nC 语言函数无法 dump； OC 方法的参数如果是对象类型，则只会显示 id； dump 出来的 OC 方法包含了 .h .m 文件里面的。 2、通过视图堆栈找出对应文件 从视图堆栈里面可以很方便找出播放器相关的文件，从 class-dump 出来的文件中找到相关文件，在文件中找出相关方法。\n比如我需要实现自动播放下一个视频的话，分为两个步骤：1.监听播放完一个视频的事件；2.播放下一个视频。\n找到“播放下一个视频”的方法：\n我从视图堆栈里定位到 AWEFeedTableViewController 类； 从 class-dump 出来的文件中打开 AWEFeedTableViewController.h； 在文件中搜索 NextVideo，很容易可以找到 - (void)scrollToNextVideo; 方法。 类似的方法找出“播放完一个视频的事件”。\n3、快速验证 从上一步找到的类还有对应方法是否真的可以实现“播放下一个视频”呢？我们可以在代码中 Hook 对应方法验证，但是这样验证的方法比较低效。\n使用 Cycript 可以动态调试 App，这里简单介绍如何通过 Cycript 快速验证：\n越狱手机上通过 Cydia 安装 Cycript； 通过 SSH 连接手机； 找到进程：root# ps -e | grep 'TikTok'； 依附到进程：cycript -p 进程号； 打印视图堆栈，输入命令：UIApp.keyWindow.recursiveDescription() ； 找到 AWEFeedTableViewController 内存地址并执行 [#0x1118da200 scrollToNextVideo]（内存地址前需要加 # 号） 此时发现 App 真的切换到了下一个视频，证明这个方法就是我们需要找的方法。\n4、实现 CHDeclareClass(AWEAwemePlayVideoViewController) CHDeclareClass(AWEFeedTableViewController) CHOptimizedMethod1(self, void, AWEAwemePlayVideoViewController, playerWillLoopPlaying, id, arg1) { [[NSNotificationCenter defaultCenter] postNotificationName:@\u0026#34;DSScrollToNextVideo\u0026#34; object:nil]; CHSuper1(AWEAwemePlayVideoViewController, playerWillLoopPlaying, arg1); } CHOptimizedMethod0(self, void, AWEFeedTableViewController, _addNotifications) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToNextVideo) name:@\u0026#34;DSScrollToNextVideo\u0026#34; object:nil]; CHSuper0(AWEFeedTableViewController, _addNotifications); } CHConstructor { CHLoadLateClass(AWEAwemePlayVideoViewController); CHLoadLateClass(AWEFeedTableViewController); CHHook1(AWEAwemePlayVideoViewController, playerWillLoopPlaying); CHHook0(AWEFeedTableViewController, _addNotifications); } 5、优化 编译运行后即可以实现自动播放下一个视频了，但是还存在一些问题，比如短视频列表中可能存在广告、直播，这两个场景就不会触发“播放完一个视频的事件”，但是我们也可以使用类似的方法去解决。\n","permalink":"https://blog.axqora.com/posts/ios-reverse-auto-next-video/","summary":"\u003ch2 id=\"一背景\"\u003e一、背景\u003c/h2\u003e\n\u003cp\u003e经过之前对 iOS 逆向的初步了解，想了个实现 TikTok 自动播放下一个视频的小需求实践一下。\u003c/p\u003e\n\u003ch2 id=\"二实现过程\"\u003e二、实现过程\u003c/h2\u003e\n\u003ch3 id=\"1使用-class-dump-工具获取类信息\"\u003e1、使用 class-dump 工具获取类信息\u003c/h3\u003e\n\u003cp\u003eclass-dump 工具通过解析 MachO 文件生成类信息，包括 OC 方法、属性、成员变量等。\u003c/p\u003e\n\u003cp\u003e有几个点需要留意：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eC 语言函数无法 dump；\u003c/li\u003e\n\u003cli\u003eOC 方法的参数如果是对象类型，则只会显示 id；\u003c/li\u003e\n\u003cli\u003edump 出来的 OC 方法包含了 .h .m 文件里面的。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"2通过视图堆栈找出对应文件\"\u003e2、通过视图堆栈找出对应文件\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"88d9f22ea0434052530532d18a38a1cc\" loading=\"lazy\" src=\"/image/iOS-Reverse-auto-next-video//A7145663-03CA-4BAF-85D9-0422C4FAC13D.png\"\u003e\u003c/p\u003e\n\u003cp\u003e从视图堆栈里面可以很方便找出播放器相关的文件，从 class-dump 出来的文件中找到相关文件，在文件中找出相关方法。\u003c/p\u003e\n\u003cp\u003e比如我需要实现自动播放下一个视频的话，分为两个步骤：1.监听播放完一个视频的事件；2.播放下一个视频。\u003c/p\u003e\n\u003cp\u003e找到“播放下一个视频”的方法：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e我从视图堆栈里定位到 \u003ccode\u003eAWEFeedTableViewController\u003c/code\u003e 类；\u003c/li\u003e\n\u003cli\u003e从 class-dump 出来的文件中打开 \u003ccode\u003eAWEFeedTableViewController.h\u003c/code\u003e；\u003c/li\u003e\n\u003cli\u003e在文件中搜索 \u003ccode\u003eNextVideo\u003c/code\u003e，很容易可以找到 \u003ccode\u003e- (void)scrollToNextVideo;\u003c/code\u003e 方法。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e类似的方法找出“播放完一个视频的事件”。\u003c/p\u003e\n\u003ch3 id=\"3快速验证\"\u003e3、快速验证\u003c/h3\u003e\n\u003cp\u003e从上一步找到的类还有对应方法是否真的可以实现“播放下一个视频”呢？我们可以在代码中 Hook 对应方法验证，但是这样验证的方法比较低效。\u003c/p\u003e\n\u003cp\u003e使用 \u003ca href=\"http://www.cycript.org/\"\u003eCycript\u003c/a\u003e 可以动态调试 App，这里简单介绍如何通过 Cycript 快速验证：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e越狱手机\u003c/strong\u003e上通过 Cydia 安装 Cycript；\u003c/li\u003e\n\u003cli\u003e通过 SSH 连接手机；\u003c/li\u003e\n\u003cli\u003e找到进程：\u003ccode\u003eroot# ps -e | grep 'TikTok'\u003c/code\u003e；\u003c/li\u003e\n\u003cli\u003e依附到进程：\u003ccode\u003ecycript -p 进程号\u003c/code\u003e；\u003c/li\u003e\n\u003cli\u003e打印视图堆栈，输入命令：\u003ccode\u003eUIApp.keyWindow.recursiveDescription()\u003c/code\u003e ；\u003c/li\u003e\n\u003cli\u003e找到 \u003ccode\u003eAWEFeedTableViewController\u003c/code\u003e 内存地址并执行 \u003ccode\u003e[#0x1118da200 scrollToNextVideo]\u003c/code\u003e\u003cstrong\u003e（内存地址前需要加 # 号）\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e此时发现 App 真的切换到了下一个视频，证明这个方法就是我们需要找的方法。\u003c/p\u003e","title":"iOS 逆向入门 - 实现 TikTok 自动播放下一个视频"},{"content":"一、背景 在「iOS 逆向入门 - TikTok 调试」文章中介绍了使用 MonkeyDev 工具对 TikTok 进行调试。使用同样的方法对抖音进行调试的过程中遇到了几个问题，在这里记录一下。\n二、动态库注入失败 按照 TikTok 的方式运行后，发现控制台没有打印 insert dylib success 的信息，猜测动态库注入失败了，果然在编译信息的输出中找到相关错误打印：\n在 MonkeyDev 的 change.log 中找到信息，尝试恢复使用 optool 工具注入动态库。\n下载 optool 的二进制文件并复制到相应位置，然后修改以下代码：\n# 注释原来的 MONKEYPARSER # \u0026#34;$MONKEYPARSER\u0026#34; install -c load -p \u0026#34;@executable_path/Frameworks/lib\u0026#34;\u0026#34;${TARGET_NAME}\u0026#34;\u0026#34;Dylib.dylib\u0026#34; -t \u0026#34;${BUILD_APP_PATH}/${APP_BINARY}\u0026#34; # 修改为 optool OPTOOL=\u0026#34;${MONKEYDEV_PATH}/bin/optool\u0026#34; \u0026#34;$OPTOOL\u0026#34; install -c load -p \u0026#34;@executable_path/Frameworks/lib\u0026#34;\u0026#34;${TARGET_NAME}\u0026#34;\u0026#34;Dylib.dylib\u0026#34; -t \u0026#34;${BUILD_APP_PATH}/${APP_BINARY}\u0026#34; 重新编译运行后可发现控制台中出现 insert dylib success 信息，证明动态库已经注入成功。\n三、绕过反调试 动态库注入成功后，发现控制台没有了任何输出，且 lldb 已经断开，证明抖音使用了某种反调试手段。\n1、反调试常规手段 1）ptrace ptrace 被常用于防止 lldb 依附，原理是一个进程只能被 ptrace 只能被一次，先于别人调用 ptrace 则可以防止别人依附。\nptrace 有几种方式进行调用：\n通过 dlsym 调用 通过汇编调用； 通过 syscall 调用，syscall(26,31,0,0)。 2）sysctl 通过 sysctl 去查看当前进程的信息，看有没有这个标记位即可检查当前调试状态。\nbool isBeingAttach() { size_t size = sizeof(struct kinfo_proc); struct kinfo_proc info; int ret = 0, name[4]; memset(\u0026amp;info, 0, sizeof(struct kinfo_proc)); name[0] = CTL_KERN; name[1] = KERN_PROC; name[2] = KERN_PROC_PID; name[3] = getpid(); if (ret == (sysctl(name, 4, \u0026amp;info, \u0026amp;size, NULL, 0))) { return ret != 0; } return (info.kp_proc.p_flag \u0026amp; P_TRACED) ? YES : NO; } 2、反反调试常规手段 我们知道了反调试的常用函数，我们可以使用 fishhook hook 这些函数，达到反反调试的效果。\nMonkeyDev 已经为我们写好了相关实现，我们打开注释即可：\n3、进一步 打开上面的注释重新运行后，lldb 依然依附失败，说明抖音有更进一步的反调试手段。\n通过网上查阅资料，有以下解题套路：\n1）判断反调试代码的位置 Xcode 上添加 AppDelegate 的符号断点：\n-[AppDelegate application:didFinishLaunchingWithOptions:] 运行后断点依然失败，证明反调试代码在 didFinishLaunchingWithOptions 之前。\n2）利用 IDA 进行分析 把二进制文件拖入 IDA 后进行分析，在 start 函数中的 0x0000000109640CF4~0x0000000109640D00 就是反调试代码，相当于调用了上面提到的 syscall(26,31,0,0)，最终就是调用了 ptrace 函数。\n3）绕过反调试代码 lldb 中执行 image list -o -f 打印链接的库，其中第一个地址则是 ASLR 的偏移地址。 运行时基地址(0x000000010BEC8D00) = ASLR 偏移地址(0x0000000002888000) + 基地址(0x0000000109640D00)\n添加地址断点，breakpoint set -a 0x000000010BEC8D00；\n断点到该地址后，执行 register write $x1 0，修改 x1 寄存器值。\n继续运行后则发现已经成功依附到 lldb。\nASLR 偏移地址每次 App 运行都会被修改，所以每次运行都要进行上面的操作。\n","permalink":"https://blog.axqora.com/posts/ios-reverse-lldb-attach/","summary":"\u003ch2 id=\"一背景\"\u003e一、背景\u003c/h2\u003e\n\u003cp\u003e在「iOS 逆向入门 - TikTok 调试」文章中介绍了使用 MonkeyDev 工具对 TikTok 进行调试。使用同样的方法对抖音进行调试的过程中遇到了几个问题，在这里记录一下。\u003c/p\u003e\n\u003ch2 id=\"二动态库注入失败\"\u003e二、动态库注入失败\u003c/h2\u003e\n\u003cp\u003e按照 TikTok 的方式运行后，发现控制台没有打印 \u003ccode\u003einsert dylib success\u003c/code\u003e 的信息，猜测动态库注入失败了，果然在编译信息的输出中找到相关错误打印：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"165c3e57dcade6ada8d0a2fd28b85ee5\" loading=\"lazy\" src=\"/image/iOS-Reverse-LLDB-Attach/CE38BC32-EC74-44A1-89A3-7C92EEEB526A.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"85bf3e1b4d094b2ccd415bd033a11a68\" loading=\"lazy\" src=\"/image/iOS-Reverse-LLDB-Attach/017557E8-7B56-4B01-ACA6-3D2ED70A9046.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在 MonkeyDev 的 change.log 中找到信息，尝试恢复使用 \u003ca href=\"https://github.com/alexzielenski/optool\"\u003eoptool\u003c/a\u003e 工具注入动态库。\u003c/p\u003e\n\u003cp\u003e下载 optool 的二进制文件并复制到相应位置，然后修改以下代码：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e# 注释原来的 MONKEYPARSER\n# \u0026#34;$MONKEYPARSER\u0026#34; install -c load -p \u0026#34;@executable_path/Frameworks/lib\u0026#34;\u0026#34;${TARGET_NAME}\u0026#34;\u0026#34;Dylib.dylib\u0026#34; -t \u0026#34;${BUILD_APP_PATH}/${APP_BINARY}\u0026#34;\n# 修改为 optool\nOPTOOL=\u0026#34;${MONKEYDEV_PATH}/bin/optool\u0026#34;\n\u0026#34;$OPTOOL\u0026#34; install -c load -p \u0026#34;@executable_path/Frameworks/lib\u0026#34;\u0026#34;${TARGET_NAME}\u0026#34;\u0026#34;Dylib.dylib\u0026#34; -t \u0026#34;${BUILD_APP_PATH}/${APP_BINARY}\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cimg alt=\"d9d23d29c9046b42770d53780277c92a\" loading=\"lazy\" src=\"/image/iOS-Reverse-LLDB-Attach/1BE048B2-B962-4D07-B430-18E2371624E6.png\"\u003e\u003c/p\u003e\n\u003cp\u003e重新编译运行后可发现控制台中出现 \u003ccode\u003einsert dylib success\u003c/code\u003e 信息，证明动态库已经注入成功。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"8c7632998f7a2a5e45a9073f24fc2ae4\" loading=\"lazy\" src=\"/image/iOS-Reverse-LLDB-Attach/BF45431C-F1CD-45C2-988C-45131AFD9809.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"三绕过反调试\"\u003e三、绕过反调试\u003c/h2\u003e\n\u003cp\u003e动态库注入成功后，发现控制台没有了任何输出，且 lldb 已经断开，证明抖音使用了某种反调试手段。\u003c/p\u003e\n\u003ch3 id=\"1反调试常规手段\"\u003e1、反调试常规手段\u003c/h3\u003e\n\u003ch4 id=\"1ptrace\"\u003e1）ptrace\u003c/h4\u003e\n\u003cp\u003e\u003ca href=\"https://blog.csdn.net/yiyaaixuexi/article/details/18222339\"\u003e\u003ccode\u003eptrace\u003c/code\u003e\u003c/a\u003e 被常用于防止 lldb 依附，原理是一个进程只能被 \u003ccode\u003eptrace\u003c/code\u003e 只能被一次，先于别人调用 \u003ccode\u003eptrace\u003c/code\u003e 则可以防止别人依附。\u003c/p\u003e","title":"iOS 逆向入门 - 绕过抖音反调试"},{"content":"一、简介 libimobiledevice 是一个与iOS设备通信的工具集，类似于安卓上的 adb 工具，像爱思助手、PP助手等底层都是用的这个工具。\nA library to communicate with services on iOS devices using native protocols.\nideviceinstaller 依赖于 libimobiledevice，主要用于操作 App，如获取应用列表、安装卸载应用等。\nA command-line application to manage apps and app archives on iOS devices.\n二、安装 Mac 上可以直接利用 Homebrew 安装：\n// 安装 libimobiledevice brew install libimobiledevice // 安装 ideviceinstaller brew install ideviceinstaller 三、使用 下面介绍常用的命令：\n1. idevice_id 打印连接的设备的 UUID\n➜ ~ idevice_id -l 0e68ed5333802b17d5ac62bfa708619de597eef2 2. idevicecrashreport 把设备上的崩溃报告移到指定文件夹。\n➜ ~ idevicecrashreport -u 0e68ed5333802b17d5ac62bfa708619de597eef2 crash Move: /com.apple.appstored/appstored.log Move: /Retired/rtcreportingd_2021-09-02-17-17-59_messageLog.ips Move: /Retired/liveshow-2021-08-31-165309.ips Move: /Retired/log-aggregated-2021-08-31-080228.ips ... 3. idevicedate 获取或者设置设备时间。\n➜ ~ idevicedate Thu Sep 2 17:21:12 CST 2021 4. idevicediagnostics 获取设备的诊断信息，重启设备、关闭设备等。\n➜ ~ idevicediagnostics diagnostics ➜ ~ idevicediagnostics shutdown ➜ ~ idevicediagnostics restart 5. ideviceinfo 获取设备信息。\n➜ ~ ideviceinfo ActivationState: Activated ActivationStateAcknowledged: true BasebandActivationTicketVersion: V2 BasebandCertId: 2315222105 BasebandChipID: 241889 ... 6. idevicename 获取或设置设备名称\n➜ ~ idevicename iPhone🤓 device name set to \u0026#39;iPhone🤓\u0026#39; 7. ideviceprovision 管理设备上的 provisioning profiles。\nprovisioning profiles 可以理解成包含了 证书、应用权限开关、appIDs、deviceIds等数据 的数据包。\n➜ ~ ideviceprovision list Device has 20 provisioning profiles installed: 50c80637-31ce-4229-805b-97f84cc0e17d - iOS Team Provisioning Profile: test.lydi.shareExtensions ... 8. idevicescreenshot 设备截图（需要设备已越狱），默认存放路径：~/screenshot-2021-09-06-03-29-15.png\n➜ ~ idevicescreenshot Screenshot saved to screenshot-2021-09-06-03-29-15.png 9. idevicesetlocation 设置设备的经纬度，可打开指南针 app 验证是否设置成功。如设置为北京的经纬度：\n➜ ~ idevicesetlocation -- 39.9042 116.4074 ➜ ~ idevicesetlocation reset 10. idevicesyslog 系统日志。\n➜ ~ idevicesyslog -u 0e68ed5333802b17d5ac62bfa708619de597eef2 [connected] Sep 6 11:44:02 atc(Apps)[43] \u0026lt;Notice\u0026gt;: found 42 apps using 4782030848 bytes (0 purgeable) Sep 6 11:44:02 atc(LogsPlugin)[43] \u0026lt;Notice\u0026gt;: found 11 files [6361088 bytes] of logs in \u0026#39;/private/var/log/\u0026#39; Sep 6 11:44:02 atc(LogsPlugin)[43] \u0026lt;Notice\u0026gt;: found 24 files [5181440 bytes] of logs in \u0026#39;/private/var/logs/\u0026#39; Sep 6 11:44:02 atc(LogsPlugin)[43] \u0026lt;Notice\u0026gt;: found 11 files [4096 bytes] of logs in \u0026#39;/private/var/wireless/Library/Logs/\u0026#39; ... 11. ideviceinstaller 获取设备应用列表。\n➜ ~ ideviceinstaller --list-apps CFBundleIdentifier, CFBundleVersion, CFBundleDisplayName com.buuuk.Klok, \u0026#34;81\u0026#34;, \u0026#34;Klok\u0026#34; com.tencent.xin, \u0026#34;7.0.4.40\u0026#34;, \u0026#34;微信\u0026#34; ... 安装 ipa 包:\n➜ ~ ideviceinstaller --install /Users/oudushu/Downloads/微信.ipa WARNING: could not locate iTunesMetadata.plist in archive! WARNING: could not locate Payload/liveshow.app/SC_Info/liveshow.sinf in archive! Copying \u0026#39;/Users/oudushu/Downloads/微信.ipa\u0026#39; to device... DONE. Installing \u0026#39;微信\u0026#39; Install: CreatingStagingDirectory (5%) Install: ExtractingPackage (15%) Install: InspectingPackage (20%) Install: TakingInstallLock (20%) Install: PreflightingApplication (30%) Install: InstallingEmbeddedProfile (30%) Install: VerifyingApplication (40%) Install: CreatingContainer (50%) Install: InstallingApplication (60%) Install: PostflightingApplication (70%) Install: SandboxingApplication (80%) Install: GeneratingApplicationMap (90%) Install: Complete 卸载应用:\n➜ ~ ideviceinstaller --uninstall 微信 (ruby-2.6.5) Uninstalling \u0026#39;微信\u0026#39; Uninstall: RemovingApplication (50%) Uninstall: GeneratingApplicationMap (90%) Uninstall: Complete ","permalink":"https://blog.axqora.com/posts/ios-libimobiledevice/","summary":"\u003ch2 id=\"一简介\"\u003e一、简介\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003elibimobiledevice\u003c/code\u003e 是一个与iOS设备通信的工具集，类似于安卓上的 \u003ccode\u003eadb\u003c/code\u003e 工具，像爱思助手、PP助手等底层都是用的这个工具。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eA library to communicate with services on iOS devices using native protocols.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003ccode\u003eideviceinstaller\u003c/code\u003e 依赖于 \u003ccode\u003elibimobiledevice\u003c/code\u003e，主要用于操作 App，如获取应用列表、安装卸载应用等。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eA command-line application to manage apps and app archives on iOS devices.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"二安装\"\u003e二、安装\u003c/h2\u003e\n\u003cp\u003eMac 上可以直接利用 Homebrew 安装：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// 安装 libimobiledevice\nbrew install libimobiledevice\n// 安装 ideviceinstaller\nbrew install ideviceinstaller\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"三使用\"\u003e三、使用\u003c/h2\u003e\n\u003cp\u003e下面介绍常用的命令：\u003c/p\u003e\n\u003ch3 id=\"1-idevice_id\"\u003e1. idevice_id\u003c/h3\u003e\n\u003cp\u003e打印连接的设备的 UUID\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e➜  ~ idevice_id -l\n0e68ed5333802b17d5ac62bfa708619de597eef2\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"2-idevicecrashreport\"\u003e2. idevicecrashreport\u003c/h3\u003e\n\u003cp\u003e把设备上的崩溃报告移到指定文件夹。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e➜  ~ idevicecrashreport -u 0e68ed5333802b17d5ac62bfa708619de597eef2 crash\nMove: /com.apple.appstored/appstored.log\nMove: /Retired/rtcreportingd_2021-09-02-17-17-59_messageLog.ips\nMove: /Retired/liveshow-2021-08-31-165309.ips\nMove: /Retired/log-aggregated-2021-08-31-080228.ips\n...\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"3-idevicedate\"\u003e3. idevicedate\u003c/h3\u003e\n\u003cp\u003e获取或者设置设备时间。\u003c/p\u003e","title":"iOS 上的 adb 工具 - libimobiledevice"},{"content":"一、ifuse ifuse 是一个文件系统工具，在未越狱的设备上可以挂载 App 的文件夹，在已越狱的设备上可以挂载根文件夹\nThis project allows mounting various directories of an iOS device locally using the FUSE file system interface.\n二、安装 利用 Homebrew 进行安装：\n// 期间可能需要输入电脑密码 brew install macfuse brew install ifuse 安装时可能出现错误：\n// 编辑 ifuse 的安装配置 vim `brew formula ifuse` // 注释或删除以下几行 # on_macos do # disable! date: \u0026#34;2021-04-08\u0026#34;, because: \u0026#34;requires closed-source macFUSE\u0026#34; # end // 重新安装 brew install ifuse 三、使用 1. 挂载 // 新建挂载点文件夹 mkdir ~/Sandbox // 设置挂载点 ifuse --container ifuse.test ~/Sandbox 此时 ifuse.test 的沙盒被成功挂载到 Sandbox 文件夹：\n2. 可能出现错误： 需要在 Info.plist 添加 key UIFileSharingEnabled value YES，重新编译即可。\n","permalink":"https://blog.axqora.com/posts/ios-mount-tool-ifuse/","summary":"\u003ch2 id=\"一ifuse\"\u003e一、ifuse\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/libimobiledevice/ifuse\"\u003eifuse\u003c/a\u003e 是一个文件系统工具，在未越狱的设备上可以挂载 App 的文件夹，在已越狱的设备上可以挂载根文件夹\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eThis project allows mounting various directories of an iOS device locally using the FUSE file system interface.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"二安装\"\u003e二、安装\u003c/h2\u003e\n\u003cp\u003e利用 Homebrew 进行安装：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// 期间可能需要输入电脑密码\nbrew install macfuse\nbrew install ifuse\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e安装时可能出现错误：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"a146fb09dc7470ed5292740e7c6b29cc\" loading=\"lazy\" src=\"/image/iOS-mount-tool-ifuse/5D78A86E-8916-4648-99BD-EC7F474C62D0.png\"\u003e\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// 编辑 ifuse 的安装配置\nvim `brew formula ifuse`\n\n// 注释或删除以下几行\n#  on_macos do\n#    disable! date: \u0026#34;2021-04-08\u0026#34;, because: \u0026#34;requires closed-source macFUSE\u0026#34;\n#  end\n\n// 重新安装\nbrew install ifuse\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cimg alt=\"aca212e2910b38571d8a47a185bdbfe7\" loading=\"lazy\" src=\"/image/iOS-mount-tool-ifuse/FAACA319-84CA-4DAF-A5C6-AA3AC71BD6C4.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"三使用\"\u003e三、使用\u003c/h2\u003e\n\u003ch3 id=\"1-挂载\"\u003e1. 挂载\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// 新建挂载点文件夹\nmkdir ~/Sandbox\n\n// 设置挂载点\nifuse --container ifuse.test ~/Sandbox\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e此时 \u003ccode\u003eifuse.test\u003c/code\u003e 的沙盒被成功挂载到 \u003ccode\u003eSandbox\u003c/code\u003e 文件夹：\u003c/p\u003e","title":"iOS 沙盒挂载工具 - ifuse"},{"content":"一、MonkeyDev 1、简介 MonkeyDev 是一个工具集合，基于 iOSOpenDev 进行升级并集成到 Xcode 里面，可以用来开发越狱机器的插件，可以很方便地注入动态库并重签名安装到非越狱机器，堪称神器。\n2、安装 参考官方文档进行安装：https://github.com/AloneMonkey/MonkeyDev/wiki\n3、开始使用 新建 MonkeyApp 项目。 拖入 已砸壳 的 ipa 包到指定文件夹 配置开发证书。 如无意外即可运行到手机上。\n4、其它运行问题 1. 链接错误 ld: file not found: /usr/lib/libstdc++.dylib 这是由于 Xcode 10 开始就不再集成 libstdc++ 库，需要把相关的库文件重新添加到对应的路径。\n使用 GitHub 上的这个库 https://github.com/devdawei/libstdc- 可以比较方便地添加 libstdc++ 库。\n2. Bundle ID TikTok 对 Bundle ID 应该有校验，用其它 Bundle ID 会导致无法联网，这时候可以在 Build Settings 里面，找到 MONKEYDEV_DEFAULT_BUNDLEID 设为 YES，这时候重新编辑安装到手机即可。\n3. 需要国外手机卡 TikTok 对地区有限制，手机需要插上国外手机卡才能进行开播，这样对调试不太友好。\nCTCarrier 是iOS上获取运营商信息的系统框架，我们可以对其进行 Hook，使其返回指定运营商信息。\n得益于 MonkeyDev 维护了非越狱插件 CocoaPods 私有仓库（https://github.com/AloneMonkey/MonkeyDevSpecs），我们可以直接使用别人提供的插件，使用方法跟我们平时使用 CocoaPods 完全一致，添加对应的源地址即可：\nsource \u0026#39;https://github.com/AloneMonkey/MonkeyDevSpecs.git\u0026#39; source \u0026#39;https://github.com/CocoaPods/Specs.git\u0026#39; use_frameworks! target \u0026#39;TikTokDevDylib\u0026#39; do pod \u0026#39;FLEX\u0026#39; pod \u0026#39;MonkeyDevPod\u0026#39;, \u0026#39;2.0.0\u0026#39; pod \u0026#39;AYTikTokPod\u0026#39; pod \u0026#39;LookinServer\u0026#39; end 5、Hook 相关函数 1. Objective-C Objective-C 是一门动态语言，可以利用运行时的特性，使用 Method Swizzling 技术进行 Hook。\n在 MonkeyDev 上使用如下：\nCHDeclareClass(AVCaptureSession) CHMethod1(void, AVCaptureSession, setSessionPreset, NSString *, arg1) { NSLog(@\u0026#34;---- - sessionPreset:%@\u0026#34;, arg1); CHSuper1(AVCaptureSession, setSessionPreset, arg1); } CHConstructor { CHLoadLateClass(AVCaptureSession); CHHook1(AVCaptureSession, setSessionPreset); } 2. C 语言 C 语言是静态语言，编译后变量、函数都已确定，我们使用 fishhook 框架即可对系统函数进行 Hook，fishhook 原理。\n使用如下：\nstatic OSStatus (*oldVTDecompressionSessionCreate)(CM_NULLABLE CFAllocatorRef allocator, CM_NONNULL CMVideoFormatDescriptionRef videoFormatDescription, CM_NULLABLE CFDictionaryRef videoDecoderSpecification, CM_NULLABLE CFDictionaryRef destinationImageBufferAttributes, const VTDecompressionOutputCallbackRecord * CM_NULLABLE outputCallback, CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTDecompressionSessionRef * CM_NONNULL decompressionSessionOut); OSStatus myVTDecompressionSessionCreate(CM_NULLABLE CFAllocatorRef allocator, CM_NONNULL CMVideoFormatDescriptionRef videoFormatDescription, CM_NULLABLE CFDictionaryRef videoDecoderSpecification, CM_NULLABLE CFDictionaryRef destinationImageBufferAttributes, const VTDecompressionOutputCallbackRecord * CM_NULLABLE outputCallback, CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTDecompressionSessionRef * CM_NONNULL decompressionSessionOut) { NSLog(@\u0026#34;---- - myVTDecompressionSessionCreate\u0026#34;); return oldVTDecompressionSessionCreate(allocator, videoFormatDescription, videoDecoderSpecification, destinationImageBufferAttributes, outputCallback, decompressionSessionOut); } CHConstructor { struct rebinding rebind; rebind.name = \u0026#34;VTDecompressionSessionCreate\u0026#34;; rebind.replacement = myVTDecompressionSessionCreate; rebind.replaced = (void *)\u0026amp;oldVTDecompressionSessionCreate; struct rebinding rebs[] = {rebind}; rebind_symbols(rebs, 3); } 6、Instruments 可以使用 Instruments，但目前还无法查看符号。\n二、frida 如果手机已经越狱，还可以使用 frida 工具进行调试。\n1. 跟踪函数调用 ➜ ~ frida-trace -U TikTok -i \u0026#34;VTCompressionSessionCreate\u0026#34; Instrumenting... VTCompressionSessionCreate: Loaded handler at \u0026#34;/Users/oudushu/__handlers__/VideoToolbox/VTCompressionSessionCreate.js\u0026#34; Started tracing 1 function. Press Ctrl+C to stop. /* TID 0x25203 */ 45773 ms VTCompressionSessionCreate() 2. 获取设备进程信息 ➜ ~ frida-ps --usb PID Name ---- ------------------------------------------------------------ 1139 ACCHWComponentAuthService 1168 AccountExtension 1169 AppManagementExtension 495 AppSSODaemon 81 AppleCredentialManagerDaemon 1586 AssetCacheLocatorService 112 BlueTool 4139 CAReportingService 3. 获取已连接的设备 ➜ ~ frida-ls-devices Id Type Name ---------------------------------------- ------ ------------ local local Local System 58e668d38f015b99730912550655d9671f3185fb usb iPhone socket remote Local Socket 4. 结束进程 ➜ ~ frida-kill -D 58e668d38f015b99730912550655d9671f3185fb 5136 5. 调试 ➜ ~ frida -U Safari浏览器 ____ / _ | Frida 15.0.18 - A world-class dynamic instrumentation toolkit | (_| | \u0026gt; _ | Commands: /_/ |_| help -\u0026gt; Displays the help system . . . . object? -\u0026gt; Display information about \u0026#39;object\u0026#39; . . . . exit/quit -\u0026gt; Exit . . . . . . . . More info at https://frida.re/docs/home/ [iPhone::Safari浏览器]-\u0026gt; help Help: #TODO :) 三、其它 目前还处于使用工具的阶段，需要往深挖的话需要了解相关技术的原理，这里先挖一些坑：\nclass-dump 原理 restore-symbol 原理 fishhook 原理 动态库注入原理 重签名原理 \u0026hellip; ","permalink":"https://blog.axqora.com/posts/ios-reverse-hook/","summary":"\u003ch2 id=\"一monkeydev\"\u003e一、MonkeyDev\u003c/h2\u003e\n\u003ch3 id=\"1简介\"\u003e1、简介\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/AloneMonkey/MonkeyDev\"\u003eMonkeyDev\u003c/a\u003e 是一个工具集合，基于 \u003ca href=\"https://github.com/kokoabim/iOSOpenDev\"\u003eiOSOpenDev\u003c/a\u003e 进行升级并集成到 Xcode 里面，可以用来开发越狱机器的插件，可以很方便地注入动态库并重签名安装到非越狱机器，堪称神器。\u003c/p\u003e\n\u003ch3 id=\"2安装\"\u003e2、安装\u003c/h3\u003e\n\u003cp\u003e参考官方文档进行安装：\u003ca href=\"https://github.com/AloneMonkey/MonkeyDev/wiki\"\u003ehttps://github.com/AloneMonkey/MonkeyDev/wiki\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"3开始使用\"\u003e3、开始使用\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e新建 MonkeyApp 项目。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cimg alt=\"0042e463aedfee90fd1ac2dc4fa6ac8e\" loading=\"lazy\" src=\"/image/iOS-Reverse-hook/4A1EF4A6-E825-4D9D-ACDF-CDB0216225AE.png\"\u003e\u003c/p\u003e\n\u003col start=\"2\"\u003e\n\u003cli\u003e拖入 \u003cem\u003e\u003cstrong\u003e已砸壳\u003c/strong\u003e\u003c/em\u003e 的 ipa 包到指定文件夹\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cimg alt=\"cbdfe51ba5f5faeee12318e6ac9ad6de\" loading=\"lazy\" src=\"/image/iOS-Reverse-hook/B18B3CBA-9C76-4C70-B9B5-463E11757ED2.png\"\u003e\u003c/p\u003e\n\u003col start=\"3\"\u003e\n\u003cli\u003e配置开发证书。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cimg alt=\"c4fe4957ddcbaea66aa0747783c62fcb\" loading=\"lazy\" src=\"/image/iOS-Reverse-hook/7CB74642-3748-4EB7-9581-1EAEFAC85AE2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e如无意外即可运行到手机上。\u003c/p\u003e\n\u003ch3 id=\"4其它运行问题\"\u003e4、其它运行问题\u003c/h3\u003e\n\u003ch4 id=\"1-链接错误\"\u003e1. 链接错误\u003c/h4\u003e\n\u003cp\u003e\u003cimg alt=\"6d63a3425c582d18ec96bb089cf07787\" loading=\"lazy\" src=\"/image/iOS-Reverse-hook/646A6B61-2468-4C13-AB09-11E775477602.png\"\u003e\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eld: file not found: /usr/lib/libstdc++.dylib\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e这是由于 Xcode 10 开始就不再集成 \u003ccode\u003elibstdc++\u003c/code\u003e 库，需要把相关的库文件重新添加到对应的路径。\u003c/p\u003e\n\u003cp\u003e使用 GitHub 上的这个库 \u003ca href=\"https://github.com/devdawei/libstdc-\"\u003ehttps://github.com/devdawei/libstdc-\u003c/a\u003e 可以比较方便地添加  \u003ccode\u003elibstdc++\u003c/code\u003e 库。\u003c/p\u003e\n\u003ch4 id=\"2-bundle-id\"\u003e2. Bundle ID\u003c/h4\u003e\n\u003cp\u003eTikTok 对 Bundle ID 应该有校验，用其它 Bundle ID 会导致无法联网，这时候可以在 \u003ccode\u003eBuild Settings\u003c/code\u003e 里面，找到 \u003ccode\u003eMONKEYDEV_DEFAULT_BUNDLEID\u003c/code\u003e 设为 \u003ccode\u003eYES\u003c/code\u003e，这时候重新编辑安装到手机即可。\u003c/p\u003e\n\u003ch4 id=\"3-需要国外手机卡\"\u003e3. 需要国外手机卡\u003c/h4\u003e\n\u003cp\u003eTikTok 对地区有限制，手机需要插上国外手机卡才能进行开播，这样对调试不太友好。\u003c/p\u003e\n\u003cp\u003eCTCarrier 是iOS上获取运营商信息的系统框架，我们可以对其进行 Hook，使其返回指定运营商信息。\u003c/p\u003e\n\u003cp\u003e得益于 MonkeyDev 维护了非越狱插件 CocoaPods 私有仓库（\u003ca href=\"https://github.com/AloneMonkey/MonkeyDevSpecs\"\u003ehttps://github.com/AloneMonkey/MonkeyDevSpecs\u003c/a\u003e），我们可以直接使用别人提供的插件，使用方法跟我们平时使用 CocoaPods 完全一致，添加对应的源地址即可：\u003c/p\u003e","title":"iOS 逆向入门 - TikTok 调试"},{"content":"背景 在进行与竟品的性能对比中，需要知道竟品的部分参数，与竟品对齐参数后再进行性能测试。\n在 hook TikTok 的摄像头输出参数时发现，PixelFormatType 的值是整数 875704422，这样的一串整数很难知道其含义。\n分析 从 key PixelFormatType 可以猜出是 kCVPixelFormatType 枚举中的一个，kCVPixelFormatType 枚举中大部分的值是以 '420f' 这样的单引号形式来表示，通过逐一对比知道，原来 875704422 对应的就是 kCVPixelFormatType_420YpCbCr8BiPlanarFullRange 的值，也就是 '420f'。\n那么，为什么 875704422 == '420f' ?\n原来，可以通过 ascii 编码进行转换，以 '420f' 为例：\n4/2/0/f 的 ascii 编码分别为 52/50/48/102 ，转换为二进制分别为 00110100/00110010/00110000/01100110，合并起来 00110100001100100011000001100110 转换为十进制，刚好就是 875704422。\n为方便看出对应的字符串，写了以下转换算法：\n转换算法 NSString * changeToTypeStr(int value) { char *str = malloc(10); int count = 0; while (value \u0026gt; 0) { char ch = (char)(value \u0026amp; 0xFF); str[count] = ch; value = value \u0026gt;\u0026gt; 8; count ++; } if (count \u0026gt; 1) { int left = 0, right = count - 1; while (left \u0026lt; right) { char tmp = str[left]; str[left] = str[right]; str[right] = tmp; left ++; right --; } } str[count] = \u0026#39;\\0\u0026#39;; NSString *res = [[NSString alloc] initWithUTF8String:str]; free(str); return res; } ","permalink":"https://blog.axqora.com/posts/c-int-express/","summary":"\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"2c3521a4edff88f7854d7b0bc310f596\" loading=\"lazy\" src=\"/image/C-Int-Express/5341CE7D-7880-43CE-9C7B-C0799EB766A8.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在进行与竟品的性能对比中，需要知道竟品的部分参数，与竟品对齐参数后再进行性能测试。\u003c/p\u003e\n\u003cp\u003e在 hook TikTok 的摄像头输出参数时发现，\u003ccode\u003ePixelFormatType\u003c/code\u003e 的值是整数 \u003ccode\u003e875704422\u003c/code\u003e，这样的一串整数很难知道其含义。\u003c/p\u003e\n\u003ch2 id=\"分析\"\u003e分析\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"a9ecba8f3f6a0b41acdcb63637f60e80\" loading=\"lazy\" src=\"/image/C-Int-Express/ABCC4B74-B0BC-42F5-A2F0-395E4D7E5F9F.png\"\u003e\u003c/p\u003e\n\u003cp\u003e从 key \u003ccode\u003ePixelFormatType\u003c/code\u003e 可以猜出是 \u003ccode\u003ekCVPixelFormatType\u003c/code\u003e 枚举中的一个，\u003ccode\u003ekCVPixelFormatType\u003c/code\u003e 枚举中大部分的值是以 \u003ccode\u003e'420f'\u003c/code\u003e 这样的单引号形式来表示，通过逐一对比知道，原来 \u003ccode\u003e875704422\u003c/code\u003e 对应的就是 \u003ccode\u003ekCVPixelFormatType_420YpCbCr8BiPlanarFullRange\u003c/code\u003e 的值，也就是 \u003ccode\u003e'420f'\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"c0a3caf746deb9cd89ff242957ab9b9e\" loading=\"lazy\" src=\"/image/C-Int-Express/043D4388-A0A4-458D-85EB-2CC1E51E5877.png\"\u003e\u003c/p\u003e\n\u003cp\u003e那么，为什么 \u003ccode\u003e875704422 == '420f'\u003c/code\u003e ?\u003c/p\u003e\n\u003cp\u003e原来，可以通过 ascii 编码进行转换，以 \u003ccode\u003e'420f'\u003c/code\u003e 为例：\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e4/2/0/f\u003c/code\u003e 的 ascii 编码分别为 \u003ccode\u003e52/50/48/102\u003c/code\u003e ，转换为二进制分别为 \u003ccode\u003e00110100/00110010/00110000/01100110\u003c/code\u003e，合并起来 \u003ccode\u003e00110100001100100011000001100110\u003c/code\u003e 转换为十进制，刚好就是 \u003ccode\u003e875704422\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"c5094241625b01a9af3fd7c370e78bb8\" loading=\"lazy\" src=\"/image/C-Int-Express/19F1ED01-810C-4226-B46E-7184A3F7F5DF.png\"\u003e\u003c/p\u003e\n\u003cp\u003e为方便看出对应的字符串，写了以下转换算法：\u003c/p\u003e\n\u003ch2 id=\"转换算法\"\u003e转换算法\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eNSString * changeToTypeStr(int value) {\n    char *str = malloc(10);\n    int count = 0;\n    while (value \u0026gt; 0) {\n        char ch = (char)(value \u0026amp; 0xFF);\n        str[count] = ch;\n        value = value \u0026gt;\u0026gt; 8;\n        count ++;\n    }\n    if (count \u0026gt; 1) {\n        int left = 0, right = count - 1;\n        while (left \u0026lt; right) {\n            char tmp = str[left];\n            str[left] = str[right];\n            str[right] = tmp;\n            left ++;\n            right --;\n        }\n    }\n    str[count] = \u0026#39;\\0\u0026#39;;\n    NSString *res = [[NSString alloc] initWithUTF8String:str];\n    free(str);\n    return res;\n}\n\u003c/code\u003e\u003c/pre\u003e","title":"C 语言中利用单引号表示整数"},{"content":"一、背景 iOS 手机通过 App Store 安装的应用都经过加密，无法直接进行逆向分析。我们需要对应用进行解密，称为“砸壳”。\n我手上的设备是 iPhone 6s，系统是 iOS 13.5.1\n二、手机越狱 安装爱思助手，一键越狱的工具里面有很多个工具（不一定需要安装爱思助手，可以直接从官网下载越狱工具）。\n这里主要推荐两个：一个是通过硬件漏洞进行越狱的 checkra1n；一个是通过软件漏洞进行越狱的 Unc0ver。\n相对来说通过硬件漏洞进行越狱的 checkra1n 会比较稳定，我使用的是 checkra1n。\n按照提示进行操作，手机重启后会出现 checkra1n 应用，打开应用安装 Cydia （这个过程手机需要连接代理）。\n注意：不同的越狱工具，对手机跟系统都有要求，太新的设备或者太新的系统都有可能不支持，需要仔细阅读官网的文档。另外，保证手机的重要资料都已经备份。\n三、通过 SSH 连接手机 1. Wi-Fi 手机安装 Cydia 后，默认安装了 OpenSSH，可以直接通过 Wi-Fi 或者 USB 连接手机。\nssh root@your_device_ip_address // 密码默认为 alpine 2. USB // 安装 usbmuxd brew install usbmuxd // 建立端口映射（把 2222 映射到 22） iproxy 2222 22 // 建立 ssh 连接 ssh -p 2222 root@localhost 建立端口映射时，终端会一直显示 \u0026ldquo;waiting for connection\u0026rdquo;，这是正常的，新建一个终端窗口进行其它操作即可。\n四、砸壳 砸壳分为静态砸壳和动态砸壳，静态砸壳是指直接解密磁盘中的应用（难度较高），而动态砸壳是指系统把解密后的 MachO 文件加载到内存中后，将 MachO 文件拷贝出来。\n1. 常用工具 常用的砸壳工具有以下三个：\ndumpdecrypted Clutch frida-ios-dump 其中 frida-ios-dump 最为方便，以下简单介绍 frida-ios-dump 的使用。\n2. frida-ios-dump 的简单实用 1) 手机上的操作 手机上打开 Cydia，添加源 https://build.frida.re; 搜索 Frida 并安装。 2) Mac上的操作 // 1. 克隆 frida-ios-dump git clone https://github.com/AloneMonkey/frida-ios-dump.git // 2. 安装环境 sudo pip3 install -r requirements.txt --upgrade // 3. 获取应用名称 frida-ps --usb // 4. 建立端口映射（把 2222 映射到 22） iproxy 2222 22 // 5. 运行脚本，参数为应用显示名称或者 Bundle Id // 这个过程需要打开应用（应用已加载到内存中） ./dump.py 美团 3) 验证是否已经砸壳 利用 otool 工具：\notool -l Payload/imeituan.app/imeituan | grep \u0026#34;crypt\u0026#34; // cryptid 0 已砸壳 ","permalink":"https://blog.axqora.com/posts/ios-reverse-dump/","summary":"\u003ch2 id=\"一背景\"\u003e一、背景\u003c/h2\u003e\n\u003cp\u003eiOS 手机通过 App Store 安装的应用都经过加密，无法直接进行逆向分析。我们需要对应用进行解密，称为“砸壳”。\u003c/p\u003e\n\u003cp\u003e我手上的设备是 \u003ccode\u003eiPhone 6s\u003c/code\u003e，系统是 \u003ccode\u003eiOS 13.5.1\u003c/code\u003e\u003c/p\u003e\n\u003ch2 id=\"二手机越狱\"\u003e二、手机越狱\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"7438f42620d7b110699c27f2a1e92ad2\" loading=\"lazy\" src=\"/image/iOS-Reverse-dump/DBDC0602-2AEE-468C-BB0A-D74996B39C97.png\"\u003e\u003c/p\u003e\n\u003cp\u003e安装爱思助手，一键越狱的工具里面有很多个工具（不一定需要安装爱思助手，可以直接从官网下载越狱工具）。\u003c/p\u003e\n\u003cp\u003e这里主要推荐两个：一个是通过硬件漏洞进行越狱的 \u003ca href=\"https://checkra.in/\"\u003echeckra1n\u003c/a\u003e；一个是通过软件漏洞进行越狱的 \u003ca href=\"https://unc0ver.dev/\"\u003eUnc0ver\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e相对来说通过硬件漏洞进行越狱的 checkra1n 会比较稳定，我使用的是 checkra1n。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"b8e13fe10d7072773197a107aa6c36bd\" loading=\"lazy\" src=\"/image/iOS-Reverse-dump/0198AE0A-5680-4FB4-A9BA-EC03160EC54F.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"b29d4cda912a49aa2a77cf35eedc304e\" loading=\"lazy\" src=\"/image/iOS-Reverse-dump/1B646924-B37E-4CA4-9407-B631EDB93BA5.png\"\u003e\u003c/p\u003e\n\u003cp\u003e按照提示进行操作，手机重启后会出现 checkra1n 应用，打开应用安装 Cydia \u003cstrong\u003e（这个过程手机需要连接代理）\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e注意：不同的越狱工具，对手机跟系统都有要求，太新的设备或者太新的系统都有可能不支持，需要仔细阅读官网的文档。另外，保证手机的重要资料都已经备份。\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"三通过-ssh-连接手机\"\u003e三、通过 SSH 连接手机\u003c/h2\u003e\n\u003ch3 id=\"1-wi-fi\"\u003e1. Wi-Fi\u003c/h3\u003e\n\u003cp\u003e手机安装 Cydia 后，默认安装了 OpenSSH，可以直接通过 Wi-Fi 或者 USB 连接手机。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003essh root@your_device_ip_address\n// 密码默认为 alpine\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"2-usb\"\u003e2. USB\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// 安装 usbmuxd\nbrew install usbmuxd\n\n// 建立端口映射（把 2222 映射到 22）\niproxy 2222 22\n\n// 建立 ssh 连接\nssh -p 2222 root@localhost\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cimg alt=\"e342217dae5a9c9408068e018665c221\" loading=\"lazy\" src=\"/image/iOS-Reverse-dump/E92C6731-CF4E-4516-AB5D-F479D089845D.png\"\u003e\u003c/p\u003e\n\u003cp\u003e建立端口映射时，终端会一直显示 \u0026ldquo;waiting for connection\u0026rdquo;，这是正常的，新建一个终端窗口进行其它操作即可。\u003c/p\u003e","title":"iOS 逆向入门 - 砸壳"},{"content":"iOS 越狱 https://checkra.in/ （基于硬件漏洞，较稳定） https://unc0ver.dev/ （基于软件漏洞，每次重启手机后都要再越狱） App文件系统结构 https://www.i-funbox.com/zh-cn/index.html https://macroplant.com/iexplorer App UI调试工具 https://revealapp.com/ https://lookin.work/ （免费） 反编译工具 https://hex-rays.com/ida-pro/ https://www.hopperapp.com/ MachO https://github.com/gdbinit/MachOView 越狱开发框架 https://github.com/theos/theos ipa砸壳工具 https://github.com/stefanesser/dumpdecrypted https://github.com/KJCracks/Clutch https://github.com/AloneMonkey/frida-ios-dump class-dump http://stevenygard.com/projects/class-dump/ 动态库注入 https://github.com/alexzielenski/optool 获取keychain信息 https://github.com/ptoomey3/Keychain-Dumper 工具集 https://github.com/kokoabim/iOSOpenDev https://github.com/AloneMonkey/MonkeyDev （基于iOSOpenDev开发，可开发非越狱插件） 持续更新。。。\n","permalink":"https://blog.axqora.com/posts/ios-reverse-tools/","summary":"\u003ch4 id=\"ios-越狱\"\u003eiOS 越狱\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"https://checkra.in/\"\u003ehttps://checkra.in/\u003c/a\u003e （基于硬件漏洞，较稳定）\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://unc0ver.dev/\"\u003ehttps://unc0ver.dev/\u003c/a\u003e （基于软件漏洞，每次重启手机后都要再越狱）\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"app文件系统结构\"\u003eApp文件系统结构\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"https://www.i-funbox.com/zh-cn/index.html\"\u003ehttps://www.i-funbox.com/zh-cn/index.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://macroplant.com/iexplorer\"\u003ehttps://macroplant.com/iexplorer\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"app-ui调试工具\"\u003eApp UI调试工具\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"https://revealapp.com/\"\u003ehttps://revealapp.com/\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://lookin.work/\"\u003ehttps://lookin.work/\u003c/a\u003e （免费）\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"反编译工具\"\u003e反编译工具\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"https://hex-rays.com/ida-pro/\"\u003ehttps://hex-rays.com/ida-pro/\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.hopperapp.com/\"\u003ehttps://www.hopperapp.com/\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"macho\"\u003eMachO\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/gdbinit/MachOView\"\u003ehttps://github.com/gdbinit/MachOView\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"越狱开发框架\"\u003e越狱开发框架\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/theos/theos\"\u003ehttps://github.com/theos/theos\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"ipa砸壳工具\"\u003eipa砸壳工具\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/stefanesser/dumpdecrypted\"\u003ehttps://github.com/stefanesser/dumpdecrypted\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/KJCracks/Clutch\"\u003ehttps://github.com/KJCracks/Clutch\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/AloneMonkey/frida-ios-dump\"\u003ehttps://github.com/AloneMonkey/frida-ios-dump\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"class-dump\"\u003eclass-dump\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"http://stevenygard.com/projects/class-dump/\"\u003ehttp://stevenygard.com/projects/class-dump/\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"动态库注入\"\u003e动态库注入\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/alexzielenski/optool\"\u003ehttps://github.com/alexzielenski/optool\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"获取keychain信息\"\u003e获取keychain信息\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/ptoomey3/Keychain-Dumper\"\u003ehttps://github.com/ptoomey3/Keychain-Dumper\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"工具集\"\u003e工具集\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/kokoabim/iOSOpenDev\"\u003ehttps://github.com/kokoabim/iOSOpenDev\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/AloneMonkey/MonkeyDev\"\u003ehttps://github.com/AloneMonkey/MonkeyDev\u003c/a\u003e （基于iOSOpenDev开发，可开发非越狱插件）\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e持续更新。。。\u003c/p\u003e","title":"iOS 逆向入门 - 工具"},{"content":"一、OpenGL 相关库的下载与安装 GLFW GLFW 是配合 OpenGL 使用的轻量级工具程序库，缩写自 Graphics Library Framework（图形库框架）。GLFW 的主要功能是创建并管理窗口和 OpenGL 上下文，同时还提供了处理手柄、键盘、鼠标输入的功能。\n就是用来创建窗口界面。\n// 使用 Homebrew 安装： brew install glfw3 // 头文件路径 /usr/local/Cellar/glfw/3.3.4/include/GLFW // 库路径 /usr/local/Cellar/glfw/3.3.4/lib GLEW OpenGL Extension Wrangler Library (GLEW), a cross-platform C/C++ library that helps in querying and loading OpenGL extensions.\n实现对底层OpenGL接口封装。\n// 使用 Homebrew 安装： brew install glew // 头文件路径 /usr/local/Cellar/glew/2.2.0_1/include/GL // 库路径 /usr/local/Cellar/glew/2.2.0_1/lib Glad 作用与 GLEW 类似。\n下载地址：https://glad.dav1d.de/\nLanguage: C/C++ Specification: OpenGL API: Version 3.3 Profile: Core 选中 Generate a loader 并点击 GENERATE 按钮，下载 glad.zip 文件，解压后复制到 /usr/local/Cellar/glad/。\nOpenGL Mathematics OpenGL Mathematics (GLM) is a header only C++ mathematics library for graphics software based on the OpenGL Shading Language (GLSL) specifications.\n// 使用 Homebrew 安装： brew install glm // 头文件路径 /usr/local/Cellar/glm/0.9.9.8/include/glm https://glm.g-truc.net/0.9.8/index.html\nstb_image.h stb_image.h是Sean Barrett的一个非常流行的单头文件图像加载库，它能够加载大部分流行的文件格式，并且能够很简单得整合到你的工程之中。\nhttps://github.com/nothings/stb\n各个库的关系图 图片来源：https://www.cnblogs.com/chencarl/p/10722839.html\n二、Xcode 配置 1、Header Search Paths $(inherited) /usr/local/include /usr/local/Cellar/glfw/3.3.4/include/GLFW /usr/local/Cellar/glew/2.2.0_1/include/GL /usr/local/Cellar/glad/include 2、Library Search Paths /usr/local/Cellar/glew/2.2.0_1/lib /usr/local/Cellar/glfw/3.3.4/lib 3、User Header Search Paths /usr/local/include 4、Link Binary With Libraries 三、编译错误处理 1、找不到符号 需要选择 macOS。\n2、找不到镜像 选中 Disable Library Validation\n3、stb_image.h 找不到符号 在代码最前面添加：\n#define STB_IMAGE_IMPLEMENTATION 四、Demo https://github.com/oudushu/LearnOpenGL\n","permalink":"https://blog.axqora.com/posts/opengl-env/","summary":"\u003ch2 id=\"一opengl-相关库的下载与安装\"\u003e一、OpenGL 相关库的下载与安装\u003c/h2\u003e\n\u003ch3 id=\"glfw\"\u003eGLFW\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003eGLFW 是配合 OpenGL 使用的轻量级工具程序库，缩写自 Graphics Library Framework（图形库框架）。GLFW 的主要功能是创建并管理窗口和 OpenGL 上下文，同时还提供了处理手柄、键盘、鼠标输入的功能。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e就是用来创建窗口界面。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// 使用 Homebrew 安装：\nbrew install glfw3\n\n// 头文件路径\n/usr/local/Cellar/glfw/3.3.4/include/GLFW\n// 库路径\n/usr/local/Cellar/glfw/3.3.4/lib\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"glew\"\u003eGLEW\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003eOpenGL Extension Wrangler Library (GLEW), a cross-platform C/C++ library that helps in querying and loading OpenGL extensions.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e实现对底层OpenGL接口封装。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// 使用 Homebrew 安装：\nbrew install glew\n\n// 头文件路径\n/usr/local/Cellar/glew/2.2.0_1/include/GL\n// 库路径\n/usr/local/Cellar/glew/2.2.0_1/lib\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"glad\"\u003eGlad\u003c/h3\u003e\n\u003cp\u003e作用与 GLEW 类似。\u003c/p\u003e\n\u003cp\u003e下载地址：\u003ca href=\"https://glad.dav1d.de/\"\u003ehttps://glad.dav1d.de/\u003c/a\u003e\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eLanguage: C/C++\nSpecification: OpenGL\nAPI: Version 3.3\nProfile: Core\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e选中 \u003cem\u003eGenerate a loader\u003c/em\u003e 并点击 \u003cem\u003eGENERATE\u003c/em\u003e 按钮，下载 \u003cem\u003eglad.zip\u003c/em\u003e 文件，解压后复制到 \u003ccode\u003e/usr/local/Cellar/glad/\u003c/code\u003e。\u003c/p\u003e","title":"OpenGL + Xcode 开发环境搭建"},{"content":"背景 在开发 macOS 上的窗口共享过程中，发现 demo 被其它应用覆盖后，在25s左右，观众收到的窗口画面开始变得卡顿。\n经过排查发现问题出在主播端的采集逻辑处。在我的上一篇文章 macOS 屏幕共享开发 提到，窗口共享的实现跟屏幕共享不一样，窗口共享没有现有的系统接口，需要启动一个 Timer 去定时对窗口截图，再对截图进行处理。\n通过打印日志发现，demo 进入后台后，Timer 在25s左右，回调开始变慢，大约2s才回调一次，问题的原因就出在这里。\n排查原因 为了避免其它逻辑影响了结果，新建一个干净的 demo。\n我在窗口共享中用的是 GCD Timer，一开始猜测是 GCD Timer 的某些参数没有设置，查看文档后没找到相关的参数；然后猜测 GCD Timer 的问题，但是换成 NSTimer 后，发现也有一样的问题，这时候我开始猜测是系统导致的问题了。\n开始查阅资料，发现了 一篇文章 里面提到：\nApp Nap 是 OS X 10.9 Mavericks 的一项新功能。它能帮你在同时运行多个应用程序时节省电能。\n这时候有了切入点，查询关键字 App Nap 就可以发现相关资料。\nApp Nap 附上苹果官方说明文档：Extend App Nap\nIf 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.\n官方说明文档介绍很清楚了，包括 App Nap 工作的条件、表现等，我们要处理的就是在启动 Timer 时，暂时把 App Nap 功能关掉。\n解决方案 在开启 Timer 前加入以下代码：\n// 避免 macOS 的 App Nap 功能导致在后台时 timer 停止运行。 NSActivityOptions option = NSActivityBackground; self.processActivity = [[NSProcessInfo processInfo] beginActivityWithOptions:option reason:@\u0026#34;process.activity\u0026#34;]; 关闭 Timer 后把 Activity 停掉：\nif (self.processActivity) { [[NSProcessInfo processInfo] endActivity:self.processActivity]; self.processActivity = nil; } 这时候查看 Timer 的打印，发现25s后的回调都已经正常了。\n","permalink":"https://blog.axqora.com/posts/macos-timer/","summary":"\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003e在开发 macOS 上的窗口共享过程中，发现 demo 被其它应用覆盖后，在25s左右，观众收到的窗口画面开始变得卡顿。\u003c/p\u003e\n\u003cp\u003e经过排查发现问题出在主播端的采集逻辑处。在我的上一篇文章 \u003ca href=\"/posts/macos-screensharing/\"\u003emacOS 屏幕共享开发\u003c/a\u003e 提到，窗口共享的实现跟屏幕共享不一样，窗口共享没有现有的系统接口，需要启动一个 Timer 去定时对窗口截图，再对截图进行处理。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"188bf0d5672f682c730e8d524608379a\" loading=\"lazy\" src=\"/image/macOS-Timer/2F4FE15A-02D5-4C4E-9598-DF9B424C33FE.png\"\u003e\u003c/p\u003e\n\u003cp\u003e通过打印日志发现，demo 进入后台后，Timer 在25s左右，回调开始变慢，大约2s才回调一次，问题的原因就出在这里。\u003c/p\u003e\n\u003ch2 id=\"排查原因\"\u003e排查原因\u003c/h2\u003e\n\u003cp\u003e为了避免其它逻辑影响了结果，新建一个干净的 demo。\u003c/p\u003e\n\u003cp\u003e我在窗口共享中用的是 GCD Timer，一开始猜测是 GCD Timer 的某些参数没有设置，查看文档后没找到相关的参数；然后猜测 GCD Timer 的问题，但是换成 NSTimer 后，发现也有一样的问题，这时候我开始猜测是系统导致的问题了。\u003c/p\u003e\n\u003cp\u003e开始查阅资料，发现了 \u003ca href=\"https://sspai.com/post/26538\"\u003e一篇文章\u003c/a\u003e 里面提到：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eApp Nap 是 OS X 10.9 Mavericks 的一项新功能。它能帮你在同时运行多个应用程序时节省电能。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这时候有了切入点，查询关键字 \u003ccode\u003eApp Nap\u003c/code\u003e 就可以发现相关资料。\u003c/p\u003e\n\u003ch2 id=\"app-nap\"\u003eApp Nap\u003c/h2\u003e\n\u003cp\u003e附上苹果官方说明文档：\u003ca href=\"https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/AppNap.html\"\u003eExtend App Nap\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eIf 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.\u003c/p\u003e","title":"macOS 解决 App Nap 导致 Timer 回调变慢问题"},{"content":"一、屏幕录制权限 应用首次创建屏幕截图或者窗口截图时（以下任一接口都可以），就会显示系统的权限申请弹窗，通过选中Screen Recording里面的相关应用，重启应用后即可开启权限。\nCGDisplayCreateImage() CGDisplayCreateImageForRect() CGWindowListCreateImage() CGWindowListCreateImageFromArray() 二、获取screenId 通过AppKit的NSScreen.screens或者通过CoreGraphics的CGGetActiveDisplayList()都可以获取当前的所有显示器信息，下面用NSScreen.screens举例子：\n[NSScreen.screens enumerateObjectsUsingBlock:^(NSScreen * _Nonnull screen, NSUInteger idx, BOOL * _Nonnull stop) { NSNumber *screenNumber = screen.deviceDescription[@\u0026#34;NSScreenNumber\u0026#34;]; if (screenNumber) { CGDirectDisplayID displayId = screenNumber.unsignedIntValue; NSLog(@\u0026#34;displayId: %d\u0026#34;, displayId); } }]; 注意：如果是镜像显示，则只会打印一个显示器信息。\n三、获取windowId 1、获取windowId列表 通过接口CGWindowListCopyWindowInfo()可以获取windowId列表。\n其中kCGWindowListExcludeDesktopElements参数表示从列表中排除所有属于桌面元素的窗口。\nCGWindowListCopyWindowInfo()返回的是一个CFArrayRef，使用完后需要主动release。 这里有一个技巧，利用CFBridgingRelease()函数，可以把CFArrayRef桥接到NSArray，并且会把内存管理转移到ARC，就是说我们不需要再去主动release了。\nNSArray *windowDictArr = CFBridgingRelease(CGWindowListCopyWindowInfo(kCGWindowListOptionAll | kCGWindowListExcludeDesktopElements, 0)); 2、过滤无用windowId 通过以上接口获取到的windowId列表可能会包含很多我们不关心的windowId：\n1. 没显示出来的window 通过kCGWindowLayer跟kCGWindowAlpha两个key，可以获取到当前window是否有layer以及是否透明，我们需要过滤掉不可见的window。\n2. 过滤信息不全的window kCGWindowNumberkey可以从字典里获取到windowId，需要过滤没有windowId的字典信息。\n3. 过滤无法获取截图的window 通过CGWindowListCreateImage()接口，传入对应windowId，可获取到window的截图，如果无法获取到截图，可能没有录屏权限、不是window主体、window不可见。\n3、获取window frame 通过CGRectMakeWithDictionaryRepresentation()可获取窗口的frame，某些业务可能需要用到frame。\n四、屏幕共享 屏幕共享的视频输入总体来说跟摄像头的视频输入相当类似，可以理解成视频流从摄像头改成了屏幕输入（AVCaptureDeviceInput-\u0026gt;AVCaptureScreenInput），视频输出的处理则跟摄像头的输出处理完全一样。\n1、关键代码： - (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(\u0026#34;smaple_buffer\u0026#34;, 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，一个是避免捕获到的图片过大造成性能浪费，一个是避免难以适配太多的屏幕分辨率，我们需要指定输出的分辨率。\ncaptureSession.sessionPreset = AVCaptureSessionPreset1280x720; // 如果下面代码不设置不会生效 AVCaptureVideoDataOutput *output = captureSession.outputs[0]; [output setVideoSettings:@{(id)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange], (id)kCVPixelBufferWidthKey:@1280, (id)kCVPixelBufferHeightKey:@720}]; 五、窗口共享 窗口共享没有现有的系统接口，跟屏幕共享差异较大，总体来说，有以下几步\n启动定时器； 定时获取窗口的截图； 对截图进行处理（添加光标、裁剪）； 把CGImageRef转换为yuv数据； 调整分辨率。 1、启动定时器 定时器的间隔与设置的帧率相关，dispatch_source_set_timer()要注意的是interval参数的单位是纳秒。\n2、获取窗口截图 通过CGWindowListCreateImage()可以获取指定windowId的窗口截图，我们需要原始分辨率，而且截图中不需要包含窗口的装饰（如阴影），所以其中imageOption需要指定(kCGWindowImageNominalResolution | kCGWindowImageBoundsIgnoreFraming)。\nCGImageRef windowImage = CGWindowListCreateImage(CGRectNull, kCGWindowListOptionIncludingWindow, windowId, (kCGWindowImageNominalResolution | kCGWindowImageBoundsIgnoreFraming)); 3、追加光标 通过CGWindowListCreateImage()获取的窗口截图不会包含光标，所以需要额外追加光标。\n获取光标的坐标，[NSEvent mouseLocation] 获取光标的图片，[[NSCursor currentSystemCursor] image] 创建上下文，CGBitmapContextCreate() 画背景、画光标，CGContextDrawImage() 4、裁剪 这里主要是为了需要指定区域捕获视频，接口CGImageCreateWithImageInRect()\n5、转换为yuv数据 把CGImageRef转换为rgbaBuffer，CVPixelBufferCreateWithBytes() 创建yuvBuffer的内存空间，CVPixelBufferCreate() 利用libyuv库的ARGBToI420()函数把rgbaBuffer的信息写到yuvBuffer里 6、分辨率处理 窗口的分辨率相对不可控，可能会被用户拉伸成一个特别大的窗口，我们在初始化渲染画布时就不好确定长宽。\n设置最大分辨率为1280*720，把窗口截图的长宽等比缩放为小于最大分辨率的长宽，关键代码： - (CGSize)resize:(CGSize)size maxSize:(CGSize)maxSize { CGSize outSize = size; if (size.width \u0026gt; maxSize.width || size.height \u0026gt; maxSize.height) { if ((size.width / size.height) \u0026gt; (maxSize.width / maxSize.height)) { outSize.width = maxSize.width; outSize.height = ceil(size.height * maxSize.width / size.width); } else { outSize.height = maxSize.height; outSize.width = ceil(size.width * maxSize.height / size.height); } } return outSize; } 通过libyuv库的I420Scale()把上面的yuvBuffer缩放为scaledYuvBuffer 六、捕获指定区域视频 1、计算Rect 判断是否需要裁剪，若传入的是CGRectNull或CGRectZero，则不需要裁剪； 取交集，可通过CGRectIntersection()比较方便获取两个Rect的交集，如果无交集则认为传入的Rect无效，不做裁剪；另外，如果计算出来的Rect的长宽小于8，编码时可能崩溃，这里加上了长宽大于等于10的判断； 转换坐标系，参数传入时的坐标系是左上角为原点，屏幕共享时设置捕获区域的接口的坐标系是左下角为原点，targetRect.origin.y = CGRectGetMaxY(screenBound) - CGRectGetMaxY(intersection); 2、屏幕共享 直接设置AVCaptureScreenInput的cropRect属性即可，self.screenInput.cropRect = targetRect;\n3、窗口共享 获取到窗口截图后做裁剪即可，CGImageRef croppedImage = CGImageCreateWithImageInRect(showImage, cropRect);\n七、设置捕获帧率 1、屏幕共享 设置AVCaptureScreenInput的minFrameDuration属性即可。\nAVCaptureScreenInput *screenInput = (AVCaptureScreenInput *)input; screenInput.minFrameDuration = CMTimeMake(1, videoFrameRate); 2、窗口共享 设置窗口截图定时器的时间间隔即可。\n八、其它 1、窗口最小化 窗口最小化后，下面接口都返回了空，注意要做防崩溃处理\nCGWindowListCopyWindowInfo() CGWindowListCreateImage() 2、屏幕的不同显示模式 Mirror Displays，如果选中了镜像模式，则NSScreen.screens只返回一个显示器的信息； Scaled，如果选中了缩放，则输出的分辨率会变化，注意要做处理； Rotation，同上； 不同分辨率的显示器，同上。 ","permalink":"https://blog.axqora.com/posts/macos-screensharing/","summary":"\u003ch2 id=\"一屏幕录制权限\"\u003e一、屏幕录制权限\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"e33c5801ba456666fadd2aa2602bd730\" loading=\"lazy\" src=\"/image/macOS-ScreenSharing/AE71518A-D357-473C-AD35-4C4FFB62AD34.png\"\u003e\u003c/p\u003e\n\u003cp\u003e应用首次创建屏幕截图或者窗口截图时（以下任一接口都可以），就会显示系统的权限申请弹窗，通过选中\u003ccode\u003eScreen Recording\u003c/code\u003e里面的相关应用，重启应用后即可开启权限。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-objc\" data-lang=\"objc\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCGDisplayCreateImage()\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCGDisplayCreateImageForRect()\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCGWindowListCreateImage()\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eCGWindowListCreateImageFromArray()\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"二获取screenid\"\u003e二、获取screenId\u003c/h2\u003e\n\u003cp\u003e通过AppKit的\u003ccode\u003eNSScreen.screens\u003c/code\u003e或者通过CoreGraphics的\u003ccode\u003eCGGetActiveDisplayList()\u003c/code\u003e都可以获取当前的所有显示器信息，下面用\u003ccode\u003eNSScreen.screens\u003c/code\u003e举例子：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-objc\" data-lang=\"objc\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e[NSScreen.screens enumerateObjectsUsingBlock:\u003cspan style=\"color:#f92672\"\u003e^\u003c/span\u003e(NSScreen \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e _Nonnull screen, NSUInteger idx, \u003cspan style=\"color:#66d9ef\"\u003eBOOL\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e _Nonnull stop) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    NSNumber \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003escreenNumber \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e screen.deviceDescription[\u003cspan style=\"color:#e6db74\"\u003e@\u0026#34;NSScreenNumber\u0026#34;\u003c/span\u003e];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (screenNumber) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        CGDirectDisplayID displayId \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e screenNumber.unsignedIntValue;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        NSLog(\u003cspan style=\"color:#e6db74\"\u003e@\u0026#34;displayId: %d\u0026#34;\u003c/span\u003e, displayId);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}];\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e注意：如果是镜像显示，则只会打印一个显示器信息。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"三获取windowid\"\u003e三、获取windowId\u003c/h2\u003e\n\u003ch4 id=\"1获取windowid列表\"\u003e1、获取windowId列表\u003c/h4\u003e\n\u003cp\u003e通过接口\u003ccode\u003eCGWindowListCopyWindowInfo()\u003c/code\u003e可以获取windowId列表。\u003c/p\u003e\n\u003cp\u003e其中\u003ccode\u003ekCGWindowListExcludeDesktopElements\u003c/code\u003e参数表示从列表中排除所有属于桌面元素的窗口。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ccode\u003eCGWindowListCopyWindowInfo()\u003c/code\u003e返回的是一个\u003ccode\u003eCFArrayRef\u003c/code\u003e，使用完后需要主动release。\n这里有一个技巧，利用\u003ccode\u003eCFBridgingRelease()\u003c/code\u003e函数，可以把\u003ccode\u003eCFArrayRef\u003c/code\u003e桥接到\u003ccode\u003eNSArray\u003c/code\u003e，并且会把内存管理转移到ARC，就是说我们不需要再去主动release了。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eNSArray *windowDictArr = CFBridgingRelease(CGWindowListCopyWindowInfo(kCGWindowListOptionAll | kCGWindowListExcludeDesktopElements, 0));\n\u003c/code\u003e\u003c/pre\u003e\u003ch4 id=\"2过滤无用windowid\"\u003e2、过滤无用windowId\u003c/h4\u003e\n\u003cp\u003e通过以上接口获取到的windowId列表可能会包含很多我们不关心的windowId：\u003c/p\u003e\n\u003ch5 id=\"1-没显示出来的window\"\u003e1. 没显示出来的window\u003c/h5\u003e\n\u003cp\u003e通过\u003ccode\u003ekCGWindowLayer\u003c/code\u003e跟\u003ccode\u003ekCGWindowAlpha\u003c/code\u003e两个key，可以获取到当前window是否有layer以及是否透明，我们需要过滤掉不可见的window。\u003c/p\u003e\n\u003ch5 id=\"2-过滤信息不全的window\"\u003e2. 过滤信息不全的window\u003c/h5\u003e\n\u003cp\u003e\u003ccode\u003ekCGWindowNumber\u003c/code\u003ekey可以从字典里获取到windowId，需要过滤没有windowId的字典信息。\u003c/p\u003e\n\u003ch5 id=\"3-过滤无法获取截图的window\"\u003e3. 过滤无法获取截图的window\u003c/h5\u003e\n\u003cp\u003e通过\u003ccode\u003eCGWindowListCreateImage()\u003c/code\u003e接口，传入对应windowId，可获取到window的截图，如果无法获取到截图，可能没有录屏权限、不是window主体、window不可见。\u003c/p\u003e\n\u003ch4 id=\"3获取window-frame\"\u003e3、获取window frame\u003c/h4\u003e\n\u003cp\u003e通过\u003ccode\u003eCGRectMakeWithDictionaryRepresentation()\u003c/code\u003e可获取窗口的frame，某些业务可能需要用到frame。\u003c/p\u003e\n\u003ch2 id=\"四屏幕共享\"\u003e四、屏幕共享\u003c/h2\u003e\n\u003cp\u003e屏幕共享的视频输入总体来说跟摄像头的视频输入相当类似，可以理解成视频流从摄像头改成了屏幕输入（\u003ccode\u003eAVCaptureDeviceInput\u003c/code\u003e-\u0026gt;\u003ccode\u003eAVCaptureScreenInput\u003c/code\u003e），视频输出的处理则跟摄像头的输出处理完全一样。\u003c/p\u003e\n\u003ch4 id=\"1关键代码\"\u003e1、关键代码：\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-objc\" data-lang=\"objc\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e- (\u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e)\u003cspan style=\"color:#a6e22e\"\u003estartCapture\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    self.captureSession \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [[AVCaptureSession alloc] init];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    self.captureSession.sessionPreset \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e AVCaptureSessionPresetMedium;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// input\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    AVCaptureScreenInput \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003einput \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [[AVCaptureScreenInput alloc] initWithDisplayID:kCGDirectMainDisplay];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    [self.captureSession addInput:input];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// output\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    [self.captureSession beginConfiguration];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    dispatch_queue_t queue \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e dispatch_queue_create(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;smaple_buffer\u0026#34;\u003c/span\u003e, DISPATCH_QUEUE_SERIAL);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    AVCaptureVideoDataOutput \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003evideoDataOutput \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [[AVCaptureVideoDataOutput alloc] init];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    videoDataOutput.videoSettings \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e@{\u003c/span\u003e(NSString \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e)kCVPixelBufferPixelFormatTypeKey: \u003cspan style=\"color:#ae81ff\"\u003e@(\u003c/span\u003ekCVPixelFormatType_420YpCbCr8BiPlanarFullRange\u003cspan style=\"color:#ae81ff\"\u003e)}\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    [videoDataOutput setSampleBufferDelegate:self queue:queue];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    AVCaptureConnection \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003evideoConnection \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [videoDataOutput connectionWithMediaType:AVMediaTypeVideo];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    videoConnection.videoOrientation \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e AVCaptureVideoOrientationPortrait;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    [self.captureSession addOutput:videoDataOutput];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    [self.captureSession commitConfiguration];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    [self.captureSession startRunning];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch4 id=\"2视频输出分辨率设置\"\u003e2、视频输出分辨率设置\u003c/h4\u003e\n\u003cp\u003e我们的编码分辨率最大为720p，一个是避免捕获到的图片过大造成性能浪费，一个是避免难以适配太多的屏幕分辨率，我们需要指定输出的分辨率。\u003c/p\u003e","title":"macOS 屏幕共享开发"},{"content":"为什么想要购买NAS 我的理解NAS无非就两个作用，一个是作为资料存储容器，一个是作为个人服务器\n作为资料存储容器 这个很容易理解，NAS就是一块很大的磁盘（空间大），足够我接下来三年的资料存储需要。两块磁盘以上组raid（安全）。\n作为个人服务器 这个就很好玩了，可以说是只有想不到，没有做不到。\n举几个例子：\n文件共享服务器，可以在任意地方访问我的NAS里面的文件（局域网或公网）； Homebridge服务器，把Homebridge部署到NAS里面，可以轻松桥接小米智能家居； Gitlab服务器，把仓库放在自己家，放心； 资料备份，Mac可以直接通过Time Machine备份到NAS，手机也有相应的工具备份； 下载工具、邮件服务器、娱乐中心\u0026hellip;\u0026hellip; 选购 明确需求 我选购前就明确了几方面的需求，优先级由高到低排序：\n数据安全优于一切； 系统成熟稳定，考虑到家人也会使用，需要足够易用； 双盘位以上，用于组raid保障数据安全； 作为第一个NAS，有一定的性价比； 系统选择 对比了几个系统：\n群晖，各大博主推荐； 威联通，很多人把威联通比作安卓，群晖比作iOS； TrueNAS，开源的NAS系统，前身是FreeNAS，功能非常强大，但对硬件有一定门槛，且操作也不足够易用； Linux或Windows自组系统，需要有一定运维能力，使用不太友好。 综上，最终选择群晖系统。\n品牌选择 确认需要群晖系统后，有两种选择，一个是选择群晖官方的机器，一个是购买其它的机器刷入黑群晖系统。\n考虑到其它机器不一定能很好地兼容群晖系统，而且黑群晖系统没有官方的技术支持与QuickConnect功能（黑群晖洗白后可支持），作为第一台NAS，我还是买群晖官方的机器会更有安全感。\n型号选择 明确了需求已经很好选择了，最终选择了群晖DS220+。\n硬盘 NAS是7x24运行，而且数据安全非常重要，所以选择硬盘需要相当谨慎。\n酷狼是希捷主推的NAS专用硬盘，大部分店家都是推荐酷狼，在没有了解过硬盘信息的情况下，闭着眼睛选酷狼就可以了。\n于是我买了两块酷狼8T硬盘。不过我后悔了。\n因为希捷还有一个银河系列，专为企业用户打造。对比酷狼转速跟缓存都更高，意味着读写速度更快，官方宣称的无故障时长比酷狼多出两倍，而且质保是5年，比酷狼多出两年，价格还比酷狼便宜（2021.3）。\n所以我是首推希捷银河系列，其次是酷狼系列。\n至于西数，我还没用过西数的产品，我后面估计会换一个西数的硬盘，毕竟鸡蛋不能放在一个篮子里。\nUPS UPS 即不间断电源 (Uninterruptible Power Supply)\n虽说酷狼硬盘是专为NAS打造，但是突然断电对硬盘的伤害非常大，非常容易造成数据的丢失，非常多的博主都建议买一个UPS，避免突然断电造成数据丢失。\nUPS我选择的是施耐德BK650M2，原因是它跟群晖有很好的兼容性，可以实现断电后UPS备用电将要耗尽时通知群晖关机。\n路由器 我原来在用的路由器是小米路由器mini，说是千兆路由器，但实际上远远没有千兆（这个我还排查了很久，珍惜生命远离小米路由器），这肯定会影响NAS的使用体验。趁着这次，顺便把路由器也换了。\n需要有WiFi6、链路聚合功能，主要在考虑华硕路由器的两个型号：AX82U、AX86U\n我最终选择了AX82U，价钱是AX86U的一半，我觉得最大的不足就是没有2.5G网口。\n其它 内存 群晖DS220+是2G内存，考虑到会使用docker，2G内存可能不太够，我添加了根三星4G内存条。\n机柜 我喜欢整洁，还买了个家用机柜收纳上面的设备。\n总支出（2021年3月购买） 群晖DS220+(2250) + 希捷酷狼8T * 2(2360) + 施耐德UPS(420) + 三星4G内存条(140) + 华硕路由器AX82U(770) + 机柜(130) = 6070\n可怕😨。\n","permalink":"https://blog.axqora.com/posts/nas-purchase/","summary":"\u003ch2 id=\"为什么想要购买nas\"\u003e为什么想要购买NAS\u003c/h2\u003e\n\u003cp\u003e我的理解NAS无非就两个作用，一个是作为资料存储容器，一个是作为个人服务器\u003c/p\u003e\n\u003ch4 id=\"作为资料存储容器\"\u003e作为资料存储容器\u003c/h4\u003e\n\u003cp\u003e这个很容易理解，NAS就是一块很大的磁盘（空间大），足够我接下来三年的资料存储需要。两块磁盘以上组raid（安全）。\u003c/p\u003e\n\u003ch4 id=\"作为个人服务器\"\u003e作为个人服务器\u003c/h4\u003e\n\u003cp\u003e这个就很好玩了，可以说是只有想不到，没有做不到。\u003c/p\u003e\n\u003cp\u003e举几个例子：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e文件共享服务器，可以在任意地方访问我的NAS里面的文件（局域网或公网）；\u003c/li\u003e\n\u003cli\u003eHomebridge服务器，把Homebridge部署到NAS里面，可以轻松桥接小米智能家居；\u003c/li\u003e\n\u003cli\u003eGitlab服务器，把仓库放在自己家，放心；\u003c/li\u003e\n\u003cli\u003e资料备份，Mac可以直接通过Time Machine备份到NAS，手机也有相应的工具备份；\u003c/li\u003e\n\u003cli\u003e下载工具、邮件服务器、娱乐中心\u0026hellip;\u0026hellip;\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"选购\"\u003e选购\u003c/h2\u003e\n\u003ch4 id=\"明确需求\"\u003e明确需求\u003c/h4\u003e\n\u003cp\u003e我选购前就明确了几方面的需求，优先级由高到低排序：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e数据安全优于一切；\u003c/li\u003e\n\u003cli\u003e系统成熟稳定，考虑到家人也会使用，需要足够易用；\u003c/li\u003e\n\u003cli\u003e双盘位以上，用于组raid保障数据安全；\u003c/li\u003e\n\u003cli\u003e作为第一个NAS，有一定的性价比；\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"系统选择\"\u003e系统选择\u003c/h4\u003e\n\u003cp\u003e对比了几个系统：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e群晖，各大博主推荐；\u003c/li\u003e\n\u003cli\u003e威联通，很多人把威联通比作安卓，群晖比作iOS；\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.truenas.com/\"\u003eTrueNAS\u003c/a\u003e，开源的NAS系统，前身是FreeNAS，功能非常强大，但对硬件有一定门槛，且操作也不足够易用；\u003c/li\u003e\n\u003cli\u003eLinux或Windows自组系统，需要有一定运维能力，使用不太友好。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e综上，最终选择群晖系统。\u003c/p\u003e\n\u003ch4 id=\"品牌选择\"\u003e品牌选择\u003c/h4\u003e\n\u003cp\u003e确认需要群晖系统后，有两种选择，一个是选择群晖官方的机器，一个是购买其它的机器刷入黑群晖系统。\u003c/p\u003e\n\u003cp\u003e考虑到其它机器不一定能很好地兼容群晖系统，而且黑群晖系统没有官方的技术支持与QuickConnect功能（黑群晖洗白后可支持），作为第一台NAS，我还是买群晖官方的机器会更有安全感。\u003c/p\u003e\n\u003ch4 id=\"型号选择\"\u003e型号选择\u003c/h4\u003e\n\u003cp\u003e\u003cimg alt=\"b603418e614f0c45c438e8abe66f100e\" loading=\"lazy\" src=\"/image/NAS-Purchase/2601624539477_.pic.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e明确了需求已经很好选择了，最终选择了群晖DS220+。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"8c3a4dcd77d548ca427e12f89e5b2667\" loading=\"lazy\" src=\"/image/NAS-Purchase/583815A6-260E-401D-B298-2B36CFD6515F.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"硬盘\"\u003e硬盘\u003c/h2\u003e\n\u003cp\u003eNAS是7x24运行，而且数据安全非常重要，所以选择硬盘需要相当谨慎。\u003c/p\u003e\n\u003cp\u003e酷狼是希捷主推的NAS专用硬盘，大部分店家都是推荐酷狼，在没有了解过硬盘信息的情况下，闭着眼睛选酷狼就可以了。\u003c/p\u003e\n\u003cp\u003e于是我买了两块酷狼8T硬盘。不过我后悔了。\u003c/p\u003e\n\u003cp\u003e因为希捷还有一个银河系列，专为企业用户打造。对比酷狼转速跟缓存都更高，意味着读写速度更快，官方宣称的无故障时长比酷狼多出两倍，而且质保是5年，比酷狼多出两年，价格还比酷狼便宜（2021.3）。\u003c/p\u003e\n\u003cp\u003e所以我是首推希捷银河系列，其次是酷狼系列。\u003c/p\u003e\n\u003cp\u003e至于西数，我还没用过西数的产品，我后面估计会换一个西数的硬盘，毕竟鸡蛋不能放在一个篮子里。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"e5d83415596ea1b5532a91c5329b094c\" loading=\"lazy\" src=\"/image/NAS-Purchase/IMG_1049.JPG\"\u003e\u003c/p\u003e\n\u003ch2 id=\"ups\"\u003eUPS\u003c/h2\u003e\n\u003cp\u003eUPS 即不间断电源 (Uninterruptible Power Supply)\u003c/p\u003e\n\u003cp\u003e虽说酷狼硬盘是专为NAS打造，但是突然断电对硬盘的伤害非常大，非常容易造成数据的丢失，非常多的博主都建议买一个UPS，避免突然断电造成数据丢失。\u003c/p\u003e\n\u003cp\u003eUPS我选择的是施耐德BK650M2，原因是它跟群晖有很好的兼容性，可以实现断电后UPS备用电将要耗尽时通知群晖关机。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"38f9b544acbba386a9c04b7470710efb\" loading=\"lazy\" src=\"/image/NAS-Purchase/IMG_1050.JPG\"\u003e\u003c/p\u003e\n\u003ch2 id=\"路由器\"\u003e路由器\u003c/h2\u003e\n\u003cp\u003e我原来在用的路由器是小米路由器mini，说是千兆路由器，但实际上远远没有千兆（这个我还排查了很久，珍惜生命远离小米路由器），这肯定会影响NAS的使用体验。趁着这次，顺便把路由器也换了。\u003c/p\u003e\n\u003cp\u003e需要有WiFi6、链路聚合功能，主要在考虑华硕路由器的两个型号：AX82U、AX86U\u003c/p\u003e\n\u003cp\u003e我最终选择了AX82U，价钱是AX86U的一半，我觉得最大的不足就是没有2.5G网口。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"8fc3d0dae7cd6a114b28d46d22339e4c\" loading=\"lazy\" src=\"/image/NAS-Purchase/IMG_1051.JPG\"\u003e\u003c/p\u003e\n\u003ch2 id=\"其它\"\u003e其它\u003c/h2\u003e\n\u003ch4 id=\"内存\"\u003e内存\u003c/h4\u003e\n\u003cp\u003e群晖DS220+是2G内存，考虑到会使用docker，2G内存可能不太够，我添加了根三星4G内存条。\u003c/p\u003e\n\u003ch4 id=\"机柜\"\u003e机柜\u003c/h4\u003e\n\u003cp\u003e我喜欢整洁，还买了个家用机柜收纳上面的设备。\u003c/p\u003e\n\u003ch2 id=\"总支出2021年3月购买\"\u003e总支出（2021年3月购买）\u003c/h2\u003e\n\u003cp\u003e群晖DS220+(2250) + 希捷酷狼8T * 2(2360) + 施耐德UPS(420) + 三星4G内存条(140) + 华硕路由器AX82U(770) + 机柜(130) = 6070\u003c/p\u003e","title":"群晖 NAS - 购置篇"},{"content":"前言 发布内测版的时候，每次在提交代码前、上传ipa包前、发布前都需要手动地跑一下主流程，比如在视频详情页划几页，在直播页划几个直播间等。都是比较重复且有规律的操作，可以利用自动化测试来代替手工操作。\n我们在Jenkins上面打包时，会有一个UI自动化测试的选项，是架构组自己弄的一套自动化测试环境，想要自己搭建估计不太方便。所以需要有一套轻量级切足够易用的UI自动化测试框架。\nXCUITest 简介 XCUITest是苹果在iOS9.3提供的自动化测试框架（iOS9.3之前提供的是UI Automation），Xcode自带的框架，不需要搭建其它环境，由于是苹果提供的框架，运行稳定。使用Swift或者OC编写测试用例（可能对于测试不是很友好）。\n使用 func testExample() throws { // UI tests must launch the application that they test. let app = XCUIApplication(bundleIdentifier: \u0026#34;appium.test\u0026#34;) app.launch() // 启动应用 for _ in 1 ..\u0026lt; 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[\u0026#34;live_room_quit_btn\u0026#34;].tap() // 点击标志btn } 通过id获取UI元素，需要先在代码设置accessibilityIdentifier: _quitButton.accessibilityIdentifier = @\u0026quot;live_room_quit_btn\u0026quot;;\nAppium 简介 Appium是一个开源的、跨平台的自动化测试工具，支持iOS、Android和FirefoxOS平台。支持多种语言编写用例，对测试比较友好。\n原理 在iOS平台中，需要把WebDriverAgent编译安装到手机上，WebDriverAgent用于跟苹果的XCTest框架交互，WebDriverAgent还会起一个服务，监听Appium客户端发送的指令。\n整个流程就是这样子： 测试脚本 -\u0026gt; Appium -\u0026gt; 把脚本转化为json数据 -\u0026gt; 传输到WebDriverAgent -\u0026gt; 转化为XCTest可识别的指令 -\u0026gt; 控制app执行\n使用 #! /usr/local/bin/python3 # -*- coding: utf-8 -*- import time import wda import logging if __name__ == \u0026#39;__main__\u0026#39;: bundle_id = \u0026#34;appium.test\u0026#34; c = wda.USBClient() s = c.session(bundle_id) s.app_start(bundle_id) for time in range(5): s.swipe_up() for time in range(3): s.swipe_right() s.swipe_down() s.click(100, 200) for time in range(5): s.swipe_up() s.close() WebDriverAgent python-wda iOS 真机如何安装 WebDriverAgent Appium文档\n其它自动化测试框架 其它自动化测试框架简介\n注意点 不要在正式环境测试，避免短时间内出现多次请求，触发后台告警（灰度环境也属于正式环境）。\n","permalink":"https://blog.axqora.com/posts/ios-test/","summary":"\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e发布内测版的时候，每次在提交代码前、上传ipa包前、发布前都需要手动地跑一下主流程，比如在视频详情页划几页，在直播页划几个直播间等。都是比较重复且有规律的操作，可以利用自动化测试来代替手工操作。\u003c/p\u003e\n\u003cp\u003e我们在Jenkins上面打包时，会有一个UI自动化测试的选项，是架构组自己弄的一套自动化测试环境，想要自己搭建估计不太方便。所以需要有一套轻量级切足够易用的UI自动化测试框架。\u003c/p\u003e\n\u003ch2 id=\"xcuitest\"\u003eXCUITest\u003c/h2\u003e\n\u003ch3 id=\"简介\"\u003e简介\u003c/h3\u003e\n\u003cp\u003eXCUITest是苹果在iOS9.3提供的自动化测试框架（iOS9.3之前提供的是UI Automation），Xcode自带的框架，不需要搭建其它环境，由于是苹果提供的框架，运行稳定。使用Swift或者OC编写测试用例（可能对于测试不是很友好）。\u003c/p\u003e\n\u003ch3 id=\"使用\"\u003e使用\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003efunc testExample() throws {\n    // UI tests must launch the application that they test.\n    let app = XCUIApplication(bundleIdentifier: \u0026#34;appium.test\u0026#34;)\n    app.launch() // 启动应用\n\n    for _ in 1 ..\u0026lt; 5 {\n        app.swipeUp() // 上划\n    }\n    \n    for _ in 1 ... 3 {\n        app.swipeRight() // 右划\n    }\n    \n    app.swipeDown() // 下划刷新\n    \n    sleep(5) // 等待5s再执行下一步\n    \n    app.cells.firstMatch.tap() // 点击当前的第一个cell\n    \n    for _ in 1 ... 5 {\n        app.swipeUp() // 上划\n        sleep(2) // 等待2s再执行下一步\n    }\n    \n    app.buttons[\u0026#34;live_room_quit_btn\u0026#34;].tap() // 点击标志btn\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e通过id获取UI元素，需要先在代码设置accessibilityIdentifier:\n\u003ccode\u003e_quitButton.accessibilityIdentifier = @\u0026quot;live_room_quit_btn\u0026quot;;\u003c/code\u003e\u003c/p\u003e","title":"iOS UI 自动化测试简介"},{"content":"一、什么是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 \u0026ldquo;smart home\u0026rdquo; devices.\nHomeKit是苹果的智能家居平台，是iOS系统层面支持的平台，拥有其它平台无法比拟用户体验。可惜大部分米家设备及其它平台的设备都不支持HomeKit，但是可以通过Homebridge作为桥接，实现HomeKit控制其它平台的设备。\n二、如何在树莓派上搭建Homebridge https://github.com/homebridge/homebridge/wiki/Install-Homebridge-on-Raspbian\n三、安装插件 在这个网址https://www.npmjs.com/search?q=keywords%3Ahomebridge-plugin上可以通过关键字搜索插件，\n常用插件：\n1.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有以下几种方法：\n1、使用安卓手机下载旧版本的米家app，导出里面的数据库找到设备token 2、使用miio discover命令获取（米家新的设备基本都隐藏了token，这种方法基本失效） 3、利用Charles抓包\nHomebridge具体配置如下：\n{ \u0026#34;bridge\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Homebridge D535\u0026#34;, \u0026#34;username\u0026#34;: \u0026#34;0E:E2:26:BC:D5:35\u0026#34;, \u0026#34;port\u0026#34;: 51762, \u0026#34;pin\u0026#34;: \u0026#34;720-92-067\u0026#34; }, \u0026#34;accessories\u0026#34;: [], \u0026#34;platforms\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;Config\u0026#34;, \u0026#34;port\u0026#34;: 8581, \u0026#34;platform\u0026#34;: \u0026#34;config\u0026#34; }, { \u0026#34;deviceCfgs\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;MiPhilipsSmartBulb\u0026#34;, \u0026#34;ip\u0026#34;: \u0026#34;192.168.31.183\u0026#34;, \u0026#34;token\u0026#34;: \u0026#34;3f94f9fd80bcef65478ca59464522c93\u0026#34;, \u0026#34;lightName\u0026#34;: \u0026#34;灯泡\u0026#34;, \u0026#34;lightDisable\u0026#34;: false } ], \u0026#34;platform\u0026#34;: \u0026#34;MiPhilipsLightPlatform\u0026#34; }, { \u0026#34;platform\u0026#34;: \u0026#34;MiOutletPlatform\u0026#34;, \u0026#34;deviceCfgs\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;MiPlugBase\u0026#34;, \u0026#34;ip\u0026#34;: \u0026#34;192.168.31.29\u0026#34;, \u0026#34;token\u0026#34;: \u0026#34;83a7c8f83f44f2adefac68ce828aa084\u0026#34;, \u0026#34;outletName\u0026#34;: \u0026#34;客厅插座\u0026#34;, \u0026#34;outletDisable\u0026#34;: false, \u0026#34;temperatureName\u0026#34;: \u0026#34;客厅插座温度\u0026#34;, \u0026#34;temperatureDisable\u0026#34;: false, \u0026#34;switchLEDName\u0026#34;: \u0026#34;客厅插座指示灯\u0026#34;, \u0026#34;switchLEDDisable\u0026#34;: false } ] } ] } 注意：每次修改完配置都需要重启Homebridge服务\n五、效果 ","permalink":"https://blog.axqora.com/posts/raspberry-homebridge/","summary":"\u003ch2 id=\"一什么是homebridge\"\u003e一、什么是Homebridge\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003eHomebridge 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 \u0026ldquo;smart home\u0026rdquo; devices.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eHomeKit是苹果的智能家居平台，是iOS系统层面支持的平台，拥有其它平台无法比拟用户体验。可惜大部分米家设备及其它平台的设备都不支持HomeKit，但是可以通过Homebridge作为桥接，实现HomeKit控制其它平台的设备。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"387d405333a0546f694749cbffedc354\" loading=\"lazy\" src=\"/image/Raspberry-Homebridge/13008FF4-40EC-42EE-9666-BD1226CC70E5.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"二如何在树莓派上搭建homebridge\"\u003e二、如何在树莓派上搭建Homebridge\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/homebridge/homebridge/wiki/Install-Homebridge-on-Raspbian\"\u003ehttps://github.com/homebridge/homebridge/wiki/Install-Homebridge-on-Raspbian\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"562e7f425d01d86f52f15567c363b282\" loading=\"lazy\" src=\"/image/Raspberry-Homebridge/iShot2020-08-02AM01.36.52.png\"\u003e\n\u003cimg alt=\"0048c88f9c0ba43c8235a20d386fcbaa\" loading=\"lazy\" src=\"/image/Raspberry-Homebridge/iShot2020-08-02AM01.35.53.png\"\u003e\n\u003cimg alt=\"0638eebebe1e467b00cb548ac3b73dab\" loading=\"lazy\" src=\"/image/Raspberry-Homebridge/iShot2020-08-02AM01.37.25.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"三安装插件\"\u003e三、安装插件\u003c/h2\u003e\n\u003cp\u003e在这个网址\u003ccode\u003ehttps://www.npmjs.com/search?q=keywords%3Ahomebridge-plugin\u003c/code\u003e上可以通过关键字搜索插件，\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"92672193b785e2e16ffe5d4d79179357\" loading=\"lazy\" src=\"/image/Raspberry-Homebridge/F9814B10-3D40-4E81-9080-5C9096E674BE.png\"\u003e\u003c/p\u003e\n\u003cp\u003e常用插件：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e1.Aqara套装：homebridge-mi-aqara\n2.米家空调伴侣：homebridge-mi-acpartner\n3.米家yeelight台灯：homebridge_yeelight\n4.小米风扇：homebridge-mi-fan\n5.米家智能摄像机：homebridge-mi-camera\n6.米家智能排插：homebridge-mi-outlet\n7.米家空气净化器：homebridge-mi-airpurifier\n8.飞利浦灯泡：homebridge-mi-philips-light\n9.智能插座：homebridge-mi-outlet\n...\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"四配置设备\"\u003e四、配置设备\u003c/h2\u003e\n\u003cp\u003e在Homebridge上，插件利用设备的IP地址及设备的token即可以实现控制某个设备，IP地址我们可以登录路由器查看已连接的设备获取，获取token有以下几种方法：\u003c/p\u003e\n\u003cp\u003e1、使用安卓手机下载旧版本的米家app，导出里面的数据库找到设备token\n2、使用miio discover命令获取（米家新的设备基本都隐藏了token，这种方法基本失效）\n3、利用Charles抓包\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"059c08db26f43e551a0dce918b752f4f\" loading=\"lazy\" src=\"/image/Raspberry-Homebridge/1CF17D8B-E6BC-4FF8-B969-44B033F79F79.png\"\u003e\u003c/p\u003e","title":"在树莓派上搭建 Homebridge，实现 HomeKit 控制智能家居"},{"content":"一、目的 通过标识关键代码，利用Lint定期检测关键代码，当关键代码发生变更时，可以在报告中体现，避免引入意外的修改。\n二、Lint Lint介绍 Lint 作为一种工具程序，主要负责静态源码分析，负责代码规范、代码缺陷等检测。(https://zh.wikipedia.org/wiki/Lint) Android 原生已经提供了 Lint 静态分析工具，而 iOS 则有对应的 OCLint 和 SwiftLint。\n三、android实现代码监控的原理 Android Lint可以对Java/Kotlin源码使用uast进行语法分析，最后生成一棵AST抽象语法树，这棵树的每个节点都是一个方法，包含方法签名和方法体。 通过自定义的Lint规则，找到应用启动的入口方法。从入口方法开始遍历整棵调用树，即可对每个方法节点的代码段进行分析。\n1. 获取关键代码段 open fun shouldMonitor(node: UMethod): Boolean { return node.getAnnotations().find { it.qualifiedName == LINT_MONITOR_ANNOTATION } != null } android通过注解的方式，标识需要检测的方法实现。通过Android Lint的自定义规则判断注解标识的方法，找到入口方法，然后对代码段进行分析。\niOS对应的OCLint也可以通过自定义规则进行静态分析，但是iOS没有注解，iOS如果需要标识关键代码段，目前想到的只能hardcode检测列表（包括类和方法名）来实现标识关键代码段的效果。但是这样的维护成本比较大。\niOS可以利用类似以下的代码片段进行类和方法的判断：\nfor(auto it = parent-\u0026gt;meth_begin(), end = parent-\u0026gt;meth_end(); it != end; ++it) { const auto method = *it; if(declHasEnforceAttribute(method, *this)) { const auto selector = method-\u0026gt;getSelector(); if(!implementation-\u0026gt;getMethod(selector, method-\u0026gt;isInstanceMethod())) { const string className = parent-\u0026gt;getNameAsString(); const string methodName = selector.getAsString(); addViolation(implementation, this, \u0026#34;subclasses of \u0026#34; + className + \u0026#34; must implement \u0026#34; + methodName); } } } 注：在生成OCLint自定义规则工程时，Xcode 12会因架构问题出错，暂未找到解决方案，可暂时先切换Xcode 11生成OCLint自定义规则工程\n2. 给Report增加代码diff功能 报告中可视化的代码diff功能主要是基于这个开源库diff2html。但原生的Lint Report不支持直接嵌入html的，这里通过重写了HtmlReporter，实现html注入，详细代码可以参考 CodeLink\n获取到代码片段后，安卓利用diff2html工具可显示代码修改记录，理论上iOS只要获取到代码片段后，也可以利用这个工具进行diff显示。\n四、iOS平台的OCLint初步使用 1. 流程 2. 安装OCLint 参考官方文档：http://docs.oclint.org/en/stable/\n安装方式有三种：\nHomebrew安装 源码编译安装 安装包安装 这里使用Homebrew安装会比较方便：\nInstalling OCLint\n$ brew tap oclint/formulae $ brew install oclint 利用oclint --version命令验证OCLint安装成功\n4. 安装xcpretty xcpretty用于对xcodebuild的输出进行格式化（https://github.com/xcpretty/xcpretty）\n安装xcpretty\n$ gem install xcpretty 5. 执行命令 # 生成编译数据 xcodebuild -scheme $myscheme -workspace $myworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json # 生成报表 oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html \\ -rc LONG_LINE=200 \\ -disable-rule ShortVariableName \\ -disable-rule ObjCAssignIvarOutsideAccessors \\ -disable-rule AssignIvarOutsideAccessors \\ -max-priority-1=100000 \\ -max-priority-2=100000 \\ -max-priority-3=100000 利用浏览器打开以上命令生成的oclintReport.html，即可以查看报告\n6. 报告 7. 错误 1、oclint: error: one compiler command contains multiple jobs\n将Project和Targets中Building Settings下的COMPILER_INDEX_STORE_ENABLE设置为NO\n解决方案：\n# Podfile文件添加如下 post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings[\u0026#39;COMPILER_INDEX_STORE_ENABLE\u0026#39;] = \u0026#34;NO\u0026#34; end end end https://github.com/oclint/oclint/issues/462 2、扫描的生成的compile_commands.json文件过大\nTraceback (most recent call last): File \u0026#34;/usr/local/bin/oclint-json-compilation-database\u0026#34;, line 86, in \u0026lt;module\u0026gt; exit_code = subprocess.call(oclint_arguments) File \u0026#34;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py\u0026#34;, line 172, in call return Popen(*popenargs, **kwargs).wait() File \u0026#34;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py\u0026#34;, line 394, in __init__ errread, errwrite) File \u0026#34;/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py\u0026#34;, line 1047, in _execute_child raise child_exception OSError: [Errno 7] Argument list too long 解决方案：\n通过自定义规则过滤不重要的报错，减少分析文件大小 网上有解决方案，但目前我尝试过并不能解决，具体原因未知 https://github.com/oclint/oclint/issues/233 https://github.com/wuwen1030/oclint_argument_list_too_long_solution/tree/master 五、结论 1、查询较多文档发现，OCLint多用于代码的静态分析，约束代码成员的代码风格。问过平台组的同事，表示OCLint的静态分析不大靠谱； 2、如果要达到代码监控的目的，必须自定义规则，缺少注解功能的OC语言不太好标识关键代码。且自定义规则需要翻阅OCLint开放的接口和学习C++语言，有比较高的入门门槛；\n综上所述，方案应该可行，但受限于语言特性，OC的实现难度会更大，且最终的效果可能达不到android的效果。\n六、参考 http://docs.oclint.org/en/stable/ http://yulingtianxia.com/blog/2019/01/27/MVVM-Rules-for-OCLint/ https://juejin.im/post/6844903940375445517#heading-1\n","permalink":"https://blog.axqora.com/posts/ios-lint/","summary":"\u003ch2 id=\"一目的\"\u003e一、目的\u003c/h2\u003e\n\u003cp\u003e通过标识关键代码，利用Lint定期检测关键代码，当关键代码发生变更时，可以在报告中体现，避免引入意外的修改。\u003c/p\u003e\n\u003ch2 id=\"二lint\"\u003e二、Lint\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://zh.wikipedia.org/wiki/Lint\"\u003eLint介绍\u003c/a\u003e\nLint 作为一种工具程序，主要负责静态源码分析，负责代码规范、代码缺陷等检测。(\u003ca href=\"https://zh.wikipedia.org/wiki/Lint\"\u003ehttps://zh.wikipedia.org/wiki/Lint\u003c/a\u003e)\nAndroid 原生已经提供了 Lint 静态分析工具，而 iOS 则有对应的 \u003ca href=\"http://oclint.org/\"\u003eOCLint\u003c/a\u003e 和 SwiftLint。\u003c/p\u003e\n\u003ch2 id=\"三android实现代码监控的原理\"\u003e三、android实现代码监控的原理\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003eAndroid Lint可以对Java/Kotlin源码使用uast进行语法分析，最后生成一棵AST抽象语法树，这棵树的每个节点都是一个方法，包含方法签名和方法体。\n通过自定义的Lint规则，找到应用启动的入口方法。从入口方法开始遍历整棵调用树，即可对每个方法节点的代码段进行分析。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch4 id=\"1-获取关键代码段\"\u003e1. 获取关键代码段\u003c/h4\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eopen fun shouldMonitor(node: UMethod): Boolean {\n    return node.getAnnotations().find { it.qualifiedName == LINT_MONITOR_ANNOTATION } != null\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eandroid通过注解的方式，标识需要检测的方法实现。通过Android Lint的自定义规则判断注解标识的方法，找到入口方法，然后对代码段进行分析。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eiOS对应的OCLint也可以通过自定义规则进行静态分析，但是iOS没有注解，iOS如果需要标识关键代码段，目前想到的只能hardcode检测列表（包括类和方法名）来实现标识关键代码段的效果。但是这样的维护成本比较大。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eiOS可以利用类似以下的代码片段进行类和方法的判断：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003efor(auto it = parent-\u0026gt;meth_begin(), end = parent-\u0026gt;meth_end(); it != end; ++it) {\n    const auto method = *it;\n    if(declHasEnforceAttribute(method, *this)) {\n        const auto selector = method-\u0026gt;getSelector();\n        if(!implementation-\u0026gt;getMethod(selector, method-\u0026gt;isInstanceMethod())) {\n            const string className = parent-\u0026gt;getNameAsString();\n            const string methodName = selector.getAsString();\n            addViolation(implementation, this,\n                \u0026#34;subclasses of \u0026#34; + className + \u0026#34; must implement \u0026#34; + methodName);\n        }\n    }\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e注：在生成OCLint自定义规则工程时，Xcode 12会因架构问题出错，暂未找到解决方案，可暂时先切换Xcode 11生成OCLint自定义规则工程\u003c/p\u003e","title":"【利用基于 Lint 的启动代码监控 Android 进房间流程】iOS 平台的可行性调研"},{"content":"一、什么是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.\n管理连接到同一存储库的多个工作树。一个git仓库可以支持多个工作树，允许一次迁出多个分支。\n二、为什么要用worktree？ 场景：正在开发新的需求，这时候还需要对已提测功能的bug fixes\n方法1：先stash正在开发的代码，再check out到原分支进行bug fixes 缺点：分支切换后需要重新pod install，然后xcode重建索引并重新编译，耗时耗力\n方法2：clone两个仓库 缺点：.git目录会占空间，且两个仓库之间毫无关联，无法相互查看本地提交\n方法3：git worktree\n三、worktree的相关命令 ➜ worktree_test git:(develop) git worktree -h usage: git worktree add [\u0026lt;options\u0026gt;] \u0026lt;path\u0026gt; [\u0026lt;commit-ish\u0026gt;] or: git worktree list [\u0026lt;options\u0026gt;] or: git worktree lock [\u0026lt;options\u0026gt;] \u0026lt;path\u0026gt; or: git worktree move \u0026lt;worktree\u0026gt; \u0026lt;new-path\u0026gt; or: git worktree prune [\u0026lt;options\u0026gt;] or: git worktree remove [\u0026lt;options\u0026gt;] \u0026lt;worktree\u0026gt; or: git worktree unlock \u0026lt;path\u0026gt; 四、常用操作 1、添加一个worktree git worktree add ../live_protocol_isolation team_live/live_protocol_isolation 2、移除一个worktree git worktree remove /Users/oudushu/WorkSpace/live_protocol_isolation 3、查看工作区 git worktree list 4、查看本地提交 git show team_live/live_protocol_isolation 因为是同一个repo，所以切到dev分支后，可以马上查看到worktree分支的提交。\n五、注意点 1、无法checkout到同一个分支 2、通过git worktree add出来的分支，没有.git文件夹 原Podfile文件要处理：\n#避免在使用git worktree的时候编译报错 #git worktree中.git是一个文件而不是目录 if File.directory?(\u0026#34;.git\u0026#34;) == true then FileUtils.cp_r(\u0026#34;scripts/git-hooks/commit-msg\u0026#34;, \u0026#34;.git/hooks/commit-msg\u0026#34;) end https://git-scm.com/docs/git-worktree\n","permalink":"https://blog.axqora.com/posts/git-worktree/","summary":"\u003ch2 id=\"一什么是worktree\"\u003e一、什么是worktree？\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003eManage multiple working trees attached to the same repository.\nA git repository can support multiple working trees, allowing you to check out more than one branch at a time.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e管理连接到同一存储库的多个工作树。一个git仓库可以支持多个工作树，允许一次迁出多个分支。\u003c/p\u003e\n\u003ch2 id=\"二为什么要用worktree\"\u003e二、为什么要用worktree？\u003c/h2\u003e\n\u003cp\u003e场景：正在开发新的需求，这时候还需要对已提测功能的bug fixes\u003c/p\u003e\n\u003cp\u003e方法1：先stash正在开发的代码，再check out到原分支进行bug fixes\n缺点：分支切换后需要重新pod install，然后xcode重建索引并重新编译，耗时耗力\u003c/p\u003e\n\u003cp\u003e方法2：clone两个仓库\n缺点：.git目录会占空间，且两个仓库之间毫无关联，无法相互查看本地提交\u003c/p\u003e\n\u003cp\u003e方法3：git worktree\u003c/p\u003e\n\u003ch2 id=\"三worktree的相关命令\"\u003e三、worktree的相关命令\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e➜  worktree_test git:(develop) git worktree -h\nusage: git worktree add [\u0026lt;options\u0026gt;] \u0026lt;path\u0026gt; [\u0026lt;commit-ish\u0026gt;]\n   or: git worktree list [\u0026lt;options\u0026gt;]\n   or: git worktree lock [\u0026lt;options\u0026gt;] \u0026lt;path\u0026gt;\n   or: git worktree move \u0026lt;worktree\u0026gt; \u0026lt;new-path\u0026gt;\n   or: git worktree prune [\u0026lt;options\u0026gt;]\n   or: git worktree remove [\u0026lt;options\u0026gt;] \u0026lt;worktree\u0026gt;\n   or: git worktree unlock \u0026lt;path\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"四常用操作\"\u003e四、常用操作\u003c/h2\u003e\n\u003ch5 id=\"1添加一个worktree\"\u003e1、添加一个worktree\u003c/h5\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003egit worktree add ../live_protocol_isolation team_live/live_protocol_isolation\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cimg alt=\"20298fd5dd1613e9ecfa2a8f2c5a8e2f\" loading=\"lazy\" src=\"/image/git-worktree/iShot2020-08-15AM10.47.39.png\"\u003e\n\u003cimg alt=\"289af9dbfe6df4e4e62253452a7d3408\" loading=\"lazy\" src=\"/image/git-worktree/iShot2020-08-15AM10.57.52.png\"\u003e\u003c/p\u003e","title":"利用 git worktree 并行开发"},{"content":"一、树莓派是什么 树莓派是一款基于ARM的微型电脑主板，系统基于Linux，以MicroSD卡为磁盘，树莓派主板有4个USB接口，一个以太网接口，一个3.5mm音频口，一个HDMI视频接口，40-pin的GPIO连接端口 https://www.raspberrypi.org/products/\n二、树莓派可以干什么 可以当做一个微型服务器，作为FTP文件服务器、网站服务器、代码托管等 游戏机（刷入游戏模拟器系统，可玩大部分复古游戏）https://www.jianshu.com/p/feffa629084a 遥控车（手机通过socket连接控制小车，且小车装有红外线传感器，可以自动避障）， 可以结合其他传感器搭建智能家居 三、DHT11温湿度传感器 传感器文档\n价格便宜，体积小，功耗低，缺点是精度不高\n有四个针脚：正极、负极、串行数据总线、空脚\nDHT11温湿度传感器数据读取原理\n代码：\n__author__ = \u0026#39;DuShu\u0026#39; import time import RPi.GPIO as GPIO def getIntVal(binayList): idx = 0 binaryStr = \u0026#39;\u0026#39; while idx \u0026lt; 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 \u0026gt; 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 \u0026gt; 100: break dataArr.append(k) if k \u0026lt; 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__ == \u0026#39;__main__\u0026#39;: hum = getHumidity() print(\u0026#34;hum:\u0026#34;, hum[0], \u0026#34; temp:\u0026#34;, hum[1]) 四、参考 https://blog.csdn.net/guyang1995/article/details/54692810 https://zhuanlan.zhihu.com/p/74440206\n","permalink":"https://blog.axqora.com/posts/raspberry-introduce/","summary":"\u003ch2 id=\"一树莓派是什么\"\u003e一、树莓派是什么\u003c/h2\u003e\n\u003cp\u003e树莓派是一款基于ARM的微型电脑主板，系统基于Linux，以MicroSD卡为磁盘，树莓派主板有4个USB接口，一个以太网接口，一个3.5mm音频口，一个HDMI视频接口，40-pin的GPIO连接端口\n\u003cimg alt=\"9f6168dd4970d875a1943d309e803227\" loading=\"lazy\" src=\"/image/Raspberry-introduce/55F6100B-118A-4D51-A4B6-F06859D02BEF.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.raspberrypi.org/products/\"\u003ehttps://www.raspberrypi.org/products/\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"二树莓派可以干什么\"\u003e二、树莓派可以干什么\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e可以当做一个微型服务器，作为FTP文件服务器、网站服务器、代码托管等\u003c/li\u003e\n\u003cli\u003e游戏机（刷入游戏模拟器系统，可玩大部分复古游戏）\u003ca href=\"https://www.jianshu.com/p/feffa629084a\"\u003ehttps://www.jianshu.com/p/feffa629084a\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e遥控车（手机通过socket连接控制小车，且小车装有红外线传感器，可以自动避障），\u003c/li\u003e\n\u003cli\u003e可以结合其他传感器搭建智能家居\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cimg alt=\"9a9839f9b765d28f5df2d7a760cb1fac\" loading=\"lazy\" src=\"/image/Raspberry-introduce/11C75768-9048-4D23-A2A2-2254CB8E0B9B.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"a52ee48448e97a690a17bdf3c1a55334\" loading=\"lazy\" src=\"/image/Raspberry-introduce/3006C5C3-0148-40FA-9D9F-90460E934C99.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"三dht11温湿度传感器\"\u003e三、DHT11温湿度传感器\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://d1.amobbs.com/bbs_upload782111/files_46/ourdev_683249AKFTJF.pdf\"\u003e传感器文档\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"2dc11330550baa9f61098b5424b6138c\" loading=\"lazy\" src=\"/image/Raspberry-introduce/1E58A027-E495-42C0-9697-DC911117AA16.png\"\u003e\u003c/p\u003e\n\u003cp\u003e价格便宜，体积小，功耗低，缺点是精度不高\u003c/p\u003e\n\u003cp\u003e有四个针脚：正极、负极、串行数据总线、空脚\u003c/p\u003e\n\u003cp\u003eDHT11温湿度传感器数据读取原理\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"e5b13279dbc6a9bcdc79646b4de445db\" loading=\"lazy\" src=\"/image/Raspberry-introduce/F1E0616F-BEEC-4A03-9B86-EE40330FA404.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"f61c334aa069792bf1721a3398ac241c\" loading=\"lazy\" src=\"/image/Raspberry-introduce/728062C2-1091-477E-9C84-88915CAC426A.png\"\u003e\u003c/p\u003e\n\u003cp\u003e代码：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e__author__ = \u0026#39;DuShu\u0026#39;\nimport time\nimport RPi.GPIO as GPIO\n\ndef getIntVal(binayList):\n    idx = 0 \n    binaryStr = \u0026#39;\u0026#39; \n    while idx \u0026lt; len(binayList):\n        binaryStr += str(binayList[idx]) \n        idx += 1 \n    return int(binaryStr ,2) \n\ndef getHumidity():\n    GPIO_DATA = 18\n    GPIO_HIGH = GPIO.HIGH \n    GPIO_LOW = GPIO.LOW \n    GPIO.setmode(GPIO.BCM) \n    \n    #一秒后开始工作\n    time.sleep(1)\n\n    #设置GPIO接口为输出数据模式\n    GPIO.setup(GPIO_DATA,GPIO.OUT) \n    #输出一个低电平信号\n    GPIO.output(GPIO_DATA, GPIO.LOW) \n    time.sleep(0.02) #0.018\n\n    #0.02秒后输出一个高电平信号，启动模块测量\n    GPIO.output(GPIO_DATA, GPIO.HIGH)\n\n    #设置GPIO接口为读取读取数据模式\n    GPIO.setup(GPIO_DATA,GPIO.IN) \n\n    #等待，获取到高电平信号\n    while GPIO.input(GPIO_DATA) == GPIO_HIGH:\n        pass \n\n    #等待，获取到低电平信号\n    while GPIO.input(GPIO_DATA) == GPIO_LOW:\n        pass \n\n    #获取到高底电平信号后，开始读取模块获取数据\n    data = [] \n    cnt = 40 \n    dataArr = []\n    while cnt \u0026gt; 0:\n        cnt = cnt - 1 \n\n        while GPIO.input(GPIO_DATA) == GPIO_LOW:\n            pass \n\n        k = 0 \n        while GPIO.input(GPIO_DATA) == GPIO_HIGH:\n            k += 1 \n            if k \u0026gt; 100:\n                break \n\n        dataArr.append(k)\n\n        if k \u0026lt; 10:\n            data.append(0) \n        else:\n            data.append(1) \n\n    print(dataArr)\n\n    #根据公式定义，解析温湿度数据\n    val1 = getIntVal(data[0:8]) \n    val2 = getIntVal(data[8:16]) \n    val3 = getIntVal(data[16:24]) \n    val4 = getIntVal(data[24:32]) \n    val5 = getIntVal(data[32:40]) \n\n    if (val5 == val1 + val2 + val3 + val4):\n        return [val1, val3] \n    else:\n        return [-1, -1] \n\n    GPIO.cleanup() \n\nif __name__ == \u0026#39;__main__\u0026#39;:\n    hum = getHumidity() \n    print(\u0026#34;hum:\u0026#34;, hum[0], \u0026#34; temp:\u0026#34;, hum[1]) \n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"四参考\"\u003e四、参考\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://blog.csdn.net/guyang1995/article/details/54692810\"\u003ehttps://blog.csdn.net/guyang1995/article/details/54692810\u003c/a\u003e\n\u003ca href=\"https://zhuanlan.zhihu.com/p/74440206\"\u003ehttps://zhuanlan.zhihu.com/p/74440206\u003c/a\u003e\u003c/p\u003e","title":"树莓派简介"},{"content":"一、背景 目前直播间已支持PC推流直播，但只支持竖屏播放，导致观看体验不佳，故需要把横屏模式加上。\n二、面临的问题 1、只有两天开发时间，时间紧迫； 2、屏幕切换时存在UI异常风险； 3、横屏模式需要应用全局支持横屏方向，存在影响其他业务风险；\n三、方案 1、切换横屏入口的显示（加开关） joinChannel/joinGroup成功 -\u0026gt; roomType == BSRoomType_PCRoom -\u0026gt; 判断开关 -\u0026gt; 显示入口\n2、横竖屏切换初步方案 1.项目配置： 在AppDelegate里重写方法：\n- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window { UIViewController *topViewController = [BaseViewController bl_findTopMostViewController]; if (topViewController \u0026amp;\u0026amp; [topViewController isKindOfClass:NSClassFromString(@\u0026#34;DSLandscapeViewController\u0026#34;)]) { return topViewController.supportedInterfaceOrientations; } return UIInterfaceOrientationMaskPortrait; } 2.竖屏转横屏： 新建一个支持横屏的DSLandscapeViewController并重写UIViewController的方法：\n- (UIInterfaceOrientationMask)supportedInterfaceOrientations { return UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskPortrait; // 这里加上竖屏，是因为横屏转竖屏时，需要把当前vc先转过来，后面会讲到 } - (BOOL)shouldAutorotate { return YES; } 从竖屏vc（DSLiveShowViewController）旋转时，初始化横屏vc（DSLandscapeViewController），并把竖屏vc当前持有的BLLiveRoomAudienceSession传递给横屏vc。然后在当前navigationController的子vc中，把竖屏vc替换成横屏vc（原竖屏vc会销毁）。\n- (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];）\n3.横屏转竖屏： 正常情况下，从横屏转竖屏时，新的竖屏vc会被初始化。而竖屏vc的部分子view初始化时会通过[[UIScreen mainScreen] bounds].size取当前屏幕的size布局frame，但是此时的屏幕状态还是横屏状态，所以取出来的屏幕size是不对的，导致旋转后的vc的UI元素会出现错乱。\n为了避免UI元素出现错乱的问题，目前有三种方案：\n(1)子view全改为AutoLayout布局（工时不允许，放弃） (2)子view补充autoresizingMask属性（工时不允许，放弃） (3)横屏vc先转成竖屏，再跳转真正的竖屏vc（鉴于当前横屏vc的UI元素比较简单，且工时紧迫，现采用这种临时方案，若后面要丰富横屏模式的内容，估计还是需要采用上面两种方案。）\n在用户点击恢复竖屏的按钮时，通过[self setOrientation:UIInterfaceOrientationPortrait];设置当前vc为竖屏状态，然后重写UIViewController的方法监听旋转进度：\n- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id\u0026lt;UIViewControllerTransitionCoordinator\u0026gt;)coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; self.rt_navigationController.view.backgroundColor = [UIColor blackColor]; // 解决旋转时view的空隙白色的问题 [coordinator animateAlongsideTransition:^(id\u0026lt;UIViewControllerTransitionCoordinatorContext\u0026gt; context) { // Place code here to perform animations during the rotation. // You can pass nil or leave this block empty if not necessary. } completion:^(id\u0026lt;UIViewControllerTransitionCoordinatorContext\u0026gt; context) { // Code here will execute after the rotation has finished. // Equivalent to placing it in the deprecated method -[didRotateFromInterfaceOrientation:] self.rt_navigationController.view.backgroundColor = [UIColor whiteColor]; // 恢复原背景色 if (self.isUserClickedToPortraite) { [self jumpToPortraitViewController]; // 旋转完成，判断是用户触发的，则跳转到竖屏vc } }]; } 四、异常case 1、旋转时白底 重写UIViewController的方法监听旋转进度，在执行旋转前把BVRootNavigationController的背景色设为黑色，旋转完成后恢复为原来的白色，self.rt_navigationController.view.backgroundColor = [UIColor blackColor];\n2、横屏转竖屏视频闪烁一下 原因是把新的videoView设置到mediasdk时，mediasdk把画面渲染在新的videoView上会卡顿一下。目前的做法是，在新建竖屏vc时，会把当前的videoView传递到竖屏vc，在竖屏vc重新布局约束。\n3、横屏转竖屏时旋转时画面变形 原因是mediasdk识别到方向改变时，会修改渲染方向，此时画面比例不对画面会有拉伸。目前的做法是横屏状态下videoView写死16:9比例。\n4、旋屏时视频过渡不自然 转场之前先把videoView改为新的vc大小\n//模拟横屏video展示状态，圆滑的过渡到横屏 [self.videoView mas_remakeConstraints:^(MASConstraintMaker *make) { make.edges.mas_equalTo(self.watchLiveView); }]; 5、屏蔽手机自动旋转 前面为了能手动把横屏转成竖屏，在supportedInterfaceOrientations方法里添加了竖屏的支持，这会导致应用会随系统自动切换方向，而我们是希望能屏蔽自动切换方向，目前的做法是维护一个标志位，在需要手动把横屏转成竖屏时再把标志位打开：\n- (BOOL)shouldAutorotate { return self.shouldRotate; } - (void)setOrientation:(UIInterfaceOrientation)orientation { self.shouldRotate = YES; [super setOrientation:orientation]; self.shouldRotate = NO; } 6、启动页横屏了 原来的做法在项目的General里选中 “Landscape Left” 和 “Landscape Right\u0026quot;，这会导致启动页横屏，目前是不勾选General里的横屏选项，而在AppDelegate里重写，也能达到一样的效果：\n- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window { } Discussion\nThis method returns the total set of interface orientations supported by the app. When determining whether to rotate a particular view controller, the orientations returned by this method are intersected with the orientations supported by the root view controller or topmost presented view controller. The app and view controller must agree before the rotation is allowed.\nIf you do not implement this method, the app uses the values in the UIInterfaceOrientation key of the app’s Info.plist as the default interface orientations.\n7、[UIDevice currentDevice].orientation获得的方向不对 在一些极端情况，如从竖屏转横屏时，把设备迅速摆正，此时偶现[UIDevice currentDevice].orientation获得的方向不对。目前的做法是不依赖[UIDevice currentDevice].orientation，而是自己维护一个状态。\n8、崩溃 UIAlertController加了一个分类，重写了shouldAutorotate\n2019-12-25 20:15:51.180535+0800 LandscapteTest[894:136992] *** Terminating app due to uncaught exception \u0026#39;UIApplicationInvalidInterfaceOrientation\u0026#39;, reason: \u0026#39;Supported orientations has no common orientation with the application, and [UIAlertController shouldAutorotate] is returning YES\u0026#39; *** First throw call stack: (0x1a0fb098c 0x1a0cd90a4 0x1a0ea6054 0x1a49ca8cc 0x1a49cae14 0x1a49cb450 0x1a49b7cb8 0x1a543f048 0x1a54331d4 0x1a128830c 0x1a54330d8 0x1a54419c4 0x1a4615ec8 0x1a48d69b4 0x1a48d44f8 0x1a4fc6b98 0x1a4fb67c0 0x1a4fe6594 0x1a0f2dc48 0x1a0f28b34 0x1a0f29100 0x1a0f288bc 0x1aad93328 0x1a4fbd6d4 0x104f97f30 0x1a0db3460) libc++abi.dylib: terminating with uncaught exception of type NSException ","permalink":"https://blog.axqora.com/posts/ios-landscape/","summary":"\u003ch3 id=\"一背景\"\u003e一、背景\u003c/h3\u003e\n\u003cp\u003e目前直播间已支持PC推流直播，但只支持竖屏播放，导致观看体验不佳，故需要把横屏模式加上。\u003c/p\u003e\n\u003ch3 id=\"二面临的问题\"\u003e二、面临的问题\u003c/h3\u003e\n\u003cp\u003e1、只有两天开发时间，时间紧迫；\n2、屏幕切换时存在UI异常风险；\n3、横屏模式需要应用全局支持横屏方向，存在影响其他业务风险；\u003c/p\u003e\n\u003ch3 id=\"三方案\"\u003e三、方案\u003c/h3\u003e\n\u003ch4 id=\"1切换横屏入口的显示加开关\"\u003e1、切换横屏入口的显示（加开关）\u003c/h4\u003e\n\u003cp\u003ejoinChannel/joinGroup成功 -\u0026gt; roomType == BSRoomType_PCRoom -\u0026gt; 判断开关 -\u0026gt; 显示入口\u003c/p\u003e\n\u003ch4 id=\"2横竖屏切换初步方案\"\u003e2、横竖屏切换初步方案\u003c/h4\u003e\n\u003ch5 id=\"1项目配置\"\u003e1.项目配置：\u003c/h5\u003e\n\u003cp\u003e在AppDelegate里重写方法：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {\n    UIViewController *topViewController = [BaseViewController bl_findTopMostViewController];\n    if (topViewController \u0026amp;\u0026amp; [topViewController isKindOfClass:NSClassFromString(@\u0026#34;DSLandscapeViewController\u0026#34;)]) {\n        return topViewController.supportedInterfaceOrientations;\n    }\n    return UIInterfaceOrientationMaskPortrait;\n}\n\u003c/code\u003e\u003c/pre\u003e\u003ch5 id=\"2竖屏转横屏\"\u003e2.竖屏转横屏：\u003c/h5\u003e\n\u003cp\u003e新建一个支持横屏的DSLandscapeViewController并重写UIViewController的方法：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e- (UIInterfaceOrientationMask)supportedInterfaceOrientations {\n    return UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskPortrait; // 这里加上竖屏，是因为横屏转竖屏时，需要把当前vc先转过来，后面会讲到\n}\n\n- (BOOL)shouldAutorotate {\n    return YES;\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e从竖屏vc（\u003ccode\u003eDSLiveShowViewController\u003c/code\u003e）旋转时，初始化横屏vc（\u003ccode\u003eDSLandscapeViewController\u003c/code\u003e），并把竖屏vc当前持有的\u003ccode\u003eBLLiveRoomAudienceSession\u003c/code\u003e传递给横屏vc。然后在当前navigationController的子vc中，把竖屏vc替换成横屏vc（原竖屏vc会销毁）。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e- (void)switchToLandscapeView {\n    DSLandscapeViewController *viewController = [[DSLandscapeViewController alloc] initWithAudienceSession:self.audienceSession];\n    \n    NSMutableArray *viewControllers = [[NSMutableArray alloc] initWithArray:self.navigationController.viewControllers];\n    [viewControllers removeObject:self];\n    [viewControllers addObject:viewController];\n    [self.navigationController setViewControllers:viewControllers animated:YES];\n    \n    self.audienceSession = nil;\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e横屏vc初始化videoView，并调用session的方法传递给mediasdk（\u003ccode\u003e[self.audienceSession changeRemoteViewForGroup:self.videoView];\u003c/code\u003e）\u003c/p\u003e","title":"直播间横屏技术方案"},{"content":"CallKit是什么 苹果在WWDC 2016发布了iOS 10的新框架CallKit，它允许开发者在VoIP类型APP整合系统原生语音界面，以获得更好的用户体验。\n接入CallKit后，APP里面的通话会被写入系统通话记录，而且APP通话时的优先级比一般VoIP的APP优先级要高。\nCallKit主要类 我们可以把系统提供的CallKit框架理解成一个通话的界面UI，我们做的只是把这个通话界面跟我们的应用联系起来，\n比如有一个语音电话过来了，那么我们就呼起这个界面，用户在这个界面上点击了静音按钮，我们相应地做静音的处理。\n同样地，如果用户在应用里面点击了静音按钮，也要把状态同步到系统的界面。\n呼起CallKit \u0026amp; CallKit回调 CXProviderConfiguration 描述系统通话界面的标题、logo、来电铃声等 CXCallUpdate 描述来电信息，如来电用户名、是否视频聊天等 CXProvider 同步连接状态到CallKit CXProviderDelegate 用户在系统通话界面上动作的回调 应用把状态同步到系统： CXCallAction 用户不同动作的描述，如：呼出电话、接听、挂断、静音等（分别对应CXStartCallAction、CXAnswerCallAction、CXEndCallAction、CXSetMutedCallAction） CXTransaction 把用户的不同动作打包成CXTransaction再传递给系统 CXCallController 把CXTransaction传递给系统 原理 1、呼入 来电 1.CallKitTestApp接收到服务器的来电信息（后台时VoIP Push）\n2.创建CXCallUpdate对象记录来电信息，并记录该通话唯一标识\n3.通过CXProvider对象把来电信息通知系统\n4.系统接收到来电信息后显示原生来电UI\n响应来电 1.用户在原生来电UI上点击接听按钮（或挂断按钮）\n2.系统把动作封装成CXAnswerCallAction，通过CXProvider的Delegate回调到CallKitTestApp\n3.CallKitTestApp收到回调后会开始配置mediasdk，调整UI等操作，最后开始通话\n结束来电 1.CallKitTestApp在前台时，用户点击挂断按钮\n2.创建CXTransaction对象，用于包装挂断动作CXEndCallAction（包含该通话唯一标识）\n3.通过CXCallController对象把挂断动作通知系统\n4.系统成功挂断该通话后，通过CXProvider的Delegate回调到CallKitTestApp\n5.CallKitTestApp收到回调后会开始释放mediasdk（这里要注意释放顺序，不能在系统回调前把sdk释放掉），调整UI等操作\n2、呼出 1.用户在CallKitTestApp呼出电话\n2.创建CXTransaction对象，用于包装呼出动作CXStartCallAction（包含该通话唯一标识、呼出号码等信息）\n3.通过CXCallController对象把呼出动作通知系统\n4.系统成功呼出该通话后，通过CXProvider的Delegate回调到CallKitTestApp\n5.CallKitTestApp收到回调后会开始配置mediasdk，调整UI等操作，最后开始通话\n以上呼出逻辑是CallKitTestApp中IP-IP电话的处理逻辑，因为CallKitTestApp里面还有自动转换IP-PSTN逻辑，涉及到mediasdk资源释放问题，导致应用不稳定，内测版中暂时把呼出部分屏蔽。\nCallKitTestApp如何接入 1、mediasdk做兼容 1.系统资源申请成功后再配置mediasdk\n2.等系统资源释放后再释放mediasdk\n为满足以上两点，mediasdk开放了下面几个接口，在CallKit模式下，等系统回调后再做mediasdk配置、激活、释放等操作\n// 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通信的桥梁。\n2.CallKitTestApp在前台，并且linkd正常连接，CallController负责监听来电拓传\n3.接收到来电拓传后，初始化mediasdk，并通过BOProviderDelegate对象通知系统，触发系统来电页面，并监听系统回调\n4.系统回调- provider:performAnswerCallAction:时，调整CallKitTestApp的来电UI\n5.系统回调- provider:didActivateAudioSession:时，配置mediasdk音频参数callKit_test_configAudio()，并激活音频通信callKit_test_startAudio()\n6.用户点击挂断或者对方点击挂断，通过BOProviderDelegate对象通知系统通话结束，并监听系统回调\n7.系统回调- provider:performEndCallAction:后，释放mediasdk音频资源callKit_test_releaseAudio()，释放mediasdk\n调用时序如下：\n// 来电 2018-05-10 20:05:58.640136+0800 CallKitTest[19394:8610838] [I][CallKit] handleIncomingCallWithUUID 2018-05-10 20:05:58.725955+0800 CallKitTest[19394:8610838] [I][CallKit] unprepareSDK 2018-05-10 20:05:58.726996+0800 CallKitTest[19394:8610627] [V][CallKit] prepareSDK 2018-05-10 20:06:02.327572+0800 CallKitTest[19394:8610627] [I][CallKit] performAnswerCallAction 2018-05-10 20:06:02.892451+0800 CallKitTest[19394:8610627] [I][CallKit] didActivateAudioSession 2018-05-10 20:06:03.087589+0800 CallKitTest[19394:8610628] [I][CallKit] callKit_test_configAudio 2018-05-10 20:06:03.240767+0800 CallKitTest[19394:8610627] [I][CallKit] callKit_test_startAudio // 挂断 2018-05-10 20:06:13.660428+0800 CallKitTest[19394:8610838] [I][CallKit] endCallWithCompletion 2018-05-10 20:06:13.680038+0800 CallKitTest[19394:8610838] [I][CallKit] performEndCallAction 2018-05-10 20:06:13.798381+0800 CallKitTest[19394:8610628] [I][CallKit] callKit_test_releaseAudio 2018-05-10 20:06:13.799067+0800 CallKitTest[19394:8610838] [I][CallKit] unprepareSDK 2018-05-10 20:06:14.392857+0800 CallKitTest[19394:8610850] [I][CallKit] didDeactivateAudioSession CallKitTestApp结构如下：\n3、CallKitTestApp在后台状态（应用被杀掉、手机锁屏） VoIP Push 请看 VoIP Push介绍\nCallKitTestApp接收到VoIP Push后，如果应用未被完全kill掉，应用则被唤醒，\n若被完全kill掉，AppDelegate的-application:didFinishLaunchingWithOptions:会被调起，\nlinkd正常登录后接收到来电拓传，这时候会走在前台状态的流程，系统通话页面会被呼起。\nCallKitTestApp被kill掉后接收到VoIP Push后的调用时序如下： 风险评估 （风险从高到低排序）\n1.mediasdk释放问题 CallKit模式下mediasdk依赖系统的回调，如果mediasdk没被正常释放，会影响到下一次通话，所以code review要更加彻底，稳定性测试上要更加严格\n2.VoIP Push延迟问题 目前在内测版中，VoIP存在一定的延迟，CallKitTestApp还有一个自动切换IP-PSTN逻辑，导致CallKit没有被触发，直接触发了IP-PSTN通话。\n3.优化CallKitTestApp启动速度 CallKitTestApp被杀掉后台，收到VoIP Push后会开始启动，如果启动速度太慢会影响用户体验\n4.异常处理 要处理好手机设置勿扰模式，被系统电话打断等异常处理（接入CallKit后通话的优先级会变高，但系统电话的优先级最高，会被其打断。）\n注意点 1、CallKit的通话中UI 通话接通后，在非锁屏状态下，会跳转到CallKitTestApp的通话页面\n在锁屏状态下，会直接显示系统通话页面\n2、点击系统通话记录无法跳转 接入CallKit后，通话信息会同步到系统的通话记录里面，点击CallKitTestApp对应的通话记录后，正常情况下会跳转到CallKitTestApp并触发以下delegate：\n- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler { return [[BOStartCallViewModel shareInstance] startCallWithUserActivity:userActivity]; } 但是我遇到了点击通话记录后无法跳转到CallKitTestApp的情况。原因是CXHandle的type设置成了CXHandleTypeGeneric，但是CXProviderConfiguration的supportedHandleTypes并没有包含CXHandleTypeGeneric，所以初始化配置时加上该类型即可。\n+ (CXProviderConfiguration *)providerConfiguration API_AVAILABLE(ios(10.0)) { static dispatch_once_t onceToken; static CXProviderConfiguration *config = nil; dispatch_once(\u0026amp;onceToken, ^{ config = [[CXProviderConfiguration alloc] initWithLocalizedName:@\u0026#34;CallKitTestApp\u0026#34;]; config.supportedHandleTypes = [NSSet setWithObjects:@(CXHandleTypeGeneric), @(CXHandleTypePhoneNumber), nil]; UIImage *iconMaskImage = [UIImage imageNamed:@\u0026#34;bo_userCenter_about_icon\u0026#34;]; config.iconTemplateImageData = UIImagePNGRepresentation(iconMaskImage); config.ringtoneSound = @\u0026#34;ringback.mp3\u0026#34;; }); return config; } CXProviderDelegate 需要回复系统的动作执行结果 - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action API_AVAILABLE(ios(10.0)) { if (self.currentUUID.UUIDString.length == 0 || ![action.callUUID.UUIDString isEqualToString:self.currentUUID.UUIDString]) { [action fail]; // 执行失败 return; } if ([self.delegate respondsToSelector:_cmd]) { [self.delegate provider:provider performAnswerCallAction:action]; } [action fulfill]; // 执行成功 } 参考 官方文档\n官方Demo\n","permalink":"https://blog.axqora.com/posts/ios-callkit/","summary":"\u003ch2 id=\"callkit是什么\"\u003eCallKit是什么\u003c/h2\u003e\n\u003cp\u003e苹果在WWDC 2016发布了iOS 10的新框架CallKit，它允许开发者在VoIP类型APP整合系统原生语音界面，以获得更好的用户体验。\u003c/p\u003e\n\u003cp\u003e接入CallKit后，APP里面的通话会被写入系统通话记录，而且APP通话时的优先级比一般VoIP的APP优先级要高。\u003c/p\u003e\n\u003ch2 id=\"callkit主要类\"\u003eCallKit主要类\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"743ef574cad53b6cd95a0c15f47cbe93\" loading=\"lazy\" src=\"/image/iOS-CallKit/DADA50DE-AAC8-4C63-A010-92F2CCFCB307.png\"\u003e\u003c/p\u003e\n\u003cp\u003e我们可以把系统提供的CallKit框架理解成一个通话的界面UI，我们做的只是把这个通话界面跟我们的应用联系起来，\u003c/p\u003e\n\u003cp\u003e比如有一个语音电话过来了，那么我们就呼起这个界面，用户在这个界面上点击了静音按钮，我们相应地做静音的处理。\u003c/p\u003e\n\u003cp\u003e同样地，如果用户在应用里面点击了静音按钮，也要把状态同步到系统的界面。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e呼起CallKit \u0026amp; CallKit回调\nCXProviderConfiguration 描述系统通话界面的标题、logo、来电铃声等\nCXCallUpdate 描述来电信息，如来电用户名、是否视频聊天等\nCXProvider 同步连接状态到CallKit\nCXProviderDelegate 用户在系统通话界面上动作的回调\n\n应用把状态同步到系统：\nCXCallAction 用户不同动作的描述，如：呼出电话、接听、挂断、静音等（分别对应CXStartCallAction、CXAnswerCallAction、CXEndCallAction、CXSetMutedCallAction）\nCXTransaction 把用户的不同动作打包成CXTransaction再传递给系统\nCXCallController 把CXTransaction传递给系统\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"原理\"\u003e原理\u003c/h2\u003e\n\u003ch3 id=\"1呼入\"\u003e1、呼入\u003c/h3\u003e\n\u003ch4 id=\"来电\"\u003e来电\u003c/h4\u003e\n\u003cp\u003e\u003cimg alt=\"9be34d79d5f65ffbc36d6789fb739e07\" loading=\"lazy\" src=\"/image/iOS-CallKit/ios-callkit-24.png\"\u003e\u003c/p\u003e\n\u003cp\u003e1.CallKitTestApp接收到服务器的来电信息（后台时VoIP Push）\u003c/p\u003e\n\u003cp\u003e2.创建CXCallUpdate对象记录来电信息，并记录该通话唯一标识\u003c/p\u003e\n\u003cp\u003e3.通过CXProvider对象把来电信息通知系统\u003c/p\u003e\n\u003cp\u003e4.系统接收到来电信息后显示原生来电UI\u003c/p\u003e\n\u003ch4 id=\"响应来电\"\u003e响应来电\u003c/h4\u003e\n\u003cp\u003e\u003cimg alt=\"1224bc11bb8896323fedf104c82daacd\" loading=\"lazy\" src=\"/image/iOS-CallKit/ios-callkit-30.png\"\u003e\u003c/p\u003e\n\u003cp\u003e1.用户在原生来电UI上点击接听按钮（或挂断按钮）\u003c/p\u003e\n\u003cp\u003e2.系统把动作封装成CXAnswerCallAction，通过CXProvider的Delegate回调到CallKitTestApp\u003c/p\u003e\n\u003cp\u003e3.CallKitTestApp收到回调后会开始配置mediasdk，调整UI等操作，最后开始通话\u003c/p\u003e\n\u003ch4 id=\"结束来电\"\u003e结束来电\u003c/h4\u003e\n\u003cp\u003e\u003cimg alt=\"7a366e5e7912a7b713bc35b4730d54bb\" loading=\"lazy\" src=\"/image/iOS-CallKit/ios-callkit-37.png\"\u003e\u003c/p\u003e\n\u003cp\u003e1.CallKitTestApp在前台时，用户点击挂断按钮\u003c/p\u003e\n\u003cp\u003e2.创建CXTransaction对象，用于包装挂断动作CXEndCallAction（包含该通话唯一标识）\u003c/p\u003e\n\u003cp\u003e3.通过CXCallController对象把挂断动作通知系统\u003c/p\u003e\n\u003cp\u003e4.系统成功挂断该通话后，通过CXProvider的Delegate回调到CallKitTestApp\u003c/p\u003e\n\u003cp\u003e5.CallKitTestApp收到回调后会开始释放mediasdk（这里要注意释放顺序，不能在系统回调前把sdk释放掉），调整UI等操作\u003c/p\u003e\n\u003ch3 id=\"2呼出\"\u003e2、呼出\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"12a3efe995310a25d8415b8713cb7ef4\" loading=\"lazy\" src=\"/image/iOS-CallKit/ios-callkit-42.png\"\u003e\u003c/p\u003e\n\u003cp\u003e1.用户在CallKitTestApp呼出电话\u003c/p\u003e\n\u003cp\u003e2.创建CXTransaction对象，用于包装呼出动作CXStartCallAction（包含该通话唯一标识、呼出号码等信息）\u003c/p\u003e\n\u003cp\u003e3.通过CXCallController对象把呼出动作通知系统\u003c/p\u003e\n\u003cp\u003e4.系统成功呼出该通话后，通过CXProvider的Delegate回调到CallKitTestApp\u003c/p\u003e\n\u003cp\u003e5.CallKitTestApp收到回调后会开始配置mediasdk，调整UI等操作，最后开始通话\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e以上呼出逻辑是CallKitTestApp中IP-IP电话的处理逻辑，因为CallKitTestApp里面还有自动转换IP-PSTN逻辑，涉及到mediasdk资源释放问题，导致应用不稳定，内测版中暂时把呼出部分屏蔽。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"callkittestapp如何接入\"\u003eCallKitTestApp如何接入\u003c/h2\u003e\n\u003ch3 id=\"1mediasdk做兼容\"\u003e1、mediasdk做兼容\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e1.系统资源申请成功后再配置mediasdk\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2.等系统资源释放后再释放mediasdk\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e为满足以上两点，mediasdk开放了下面几个接口，在CallKit模式下，等系统回调后再做mediasdk配置、激活、释放等操作\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// In CallKit, it should be called by `perform Answer/start CallAction`\nbool callKit_test_configAudio(MEDIA_HANDLE handle);\n\n// In CallKit, it should be Called by `didActivateAudioSession`\nbool callKit_test_startAudio(MEDIA_HANDLE handle);\n\n// In CallKit, it should be Called by `didDeactivateAudioSession`\nvoid callKit_test_stopAudio(MEDIA_HANDLE handle);\n\n// In CallKit, it should be Called by `performEndCallAction`\nvoid callKit_test_releaseAudio(MEDIA_HANDLE handle);\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"2callkittestapp在前台状态\"\u003e2、CallKitTestApp在前台状态\u003c/h3\u003e\n\u003cp\u003e1.在CallKitTestApp项目中添加\u003ccode\u003eBOProviderDelegate\u003c/code\u003e对象，作为与CallKit通信的桥梁。\u003c/p\u003e","title":"iOS 项目接入 CallKit 框架"},{"content":"多任务页面底部banner出现的种类 1、handoff（接力） 2、xxx，早上好 3、基于您的当前位置 4、\u0026ldquo;耳机\u0026quot;已连接 5、（未知。。。)\n实现 handoff 设置 -\u0026gt; 通用 -\u0026gt; 接力 -\u0026gt; 打开接力\n通过“接力”功能，您在某设备开始操作后，可立即使用iCloud账号从另一设备继续相同操作。您需要的应用会显示在应用切换器和Mac的程序坞上。\n这种一般应用在需要跨设备共享编辑的应用上，例如“印象笔记”，在手机上编辑了一段文字后，可以切换到Mac上继续编辑。\n官方简介：https://support.apple.com/zh-cn/HT204681\n这是开发者唯一能控制的底部banner种类\nSiri应用建议 xxx，早上好 基于您的当前位置 “耳机”已连接\n关于前两种情况： 系统会记录用户长期的使用应用的习惯，自主地向用户推荐相关的应用。例如，早上起床后我会习惯打开微博，系统早上就会给我推荐微博；来到公司我会习惯打开微信，系统也会根据地理位置给我推荐微信。系统会通过记录用户的使用频率、时间、地理位置等维度，向用户推荐应用。\n关于“耳机”已连接 手机连接蓝牙耳机或者有线耳机后，系统会弹出 “耳机”已连接 的banner，如果手机装有多个音乐应用，目前观察到的情况是会弹出最常用的那个应用。但是有时候又不会弹，而且弹出维持的时间也没有规律。\n国外用户如是说： 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.\n用户怎么打开应用推荐 1、设置 -\u0026gt; Siri与搜索 -\u0026gt; 打开 搜索建议 查询建议 锁屏建议 2、设置 -\u0026gt; 隐私 -\u0026gt; 定位服务 -\u0026gt; 系统服务 -\u0026gt; 打开 基于位置的建议 基于位置的提醒 3、通知栏 -\u0026gt; 右滑 -\u0026gt; 编辑 -\u0026gt; 添加 Siri应用建议 小组件\n结论 iOS多任务页面底部出现“xxx，早上好”banner是一个系统行为，系统的Siri会根据用户的使用习惯，在多任务页面底部显示推荐应用，开发者无法干预。\n参考 https://stackoverflow.com/questions/46039851/how-may-i-customize-app-suggestions-in-ios https://www.phonearena.com/news/App-suggestions-in-iOS-10-how-they-work-whats-changed-how-to-disable-them_id89219 https://discussions.apple.com/message/30065692#30065692\n","permalink":"https://blog.axqora.com/posts/ios-banners/","summary":"\u003ch3 id=\"多任务页面底部banner出现的种类\"\u003e多任务页面底部banner出现的种类\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"ios_bottom_banners_1\" loading=\"lazy\" src=\"/image/iOS_Banners/ios_bottom_banners_1.png\"\u003e\n\u003cem\u003e\u003cstrong\u003e1、handoff（接力）\n2、xxx，早上好\n3、基于您的当前位置\n4、\u0026ldquo;耳机\u0026quot;已连接\n5、（未知。。。)\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003ch3 id=\"实现\"\u003e实现\u003c/h3\u003e\n\u003ch4 id=\"handoff\"\u003ehandoff\u003c/h4\u003e\n\u003cp\u003e设置 -\u0026gt; 通用 -\u0026gt; 接力 -\u0026gt; 打开接力\u003c/p\u003e\n\u003cp\u003e通过“接力”功能，您在某设备开始操作后，可立即使用iCloud账号从另一设备继续相同操作。您需要的应用会显示在应用切换器和Mac的程序坞上。\u003c/p\u003e\n\u003cp\u003e这种一般应用在需要跨设备共享编辑的应用上，例如“印象笔记”，在手机上编辑了一段文字后，可以切换到Mac上继续编辑。\u003c/p\u003e\n\u003cp\u003e官方简介：https://support.apple.com/zh-cn/HT204681\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e这是开发者唯一能控制的底部banner种类\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003ch4 id=\"siri应用建议\"\u003eSiri应用建议\u003c/h4\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003exxx，早上好\n基于您的当前位置\n“耳机”已连接\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003ch5 id=\"关于前两种情况\"\u003e关于前两种情况：\u003c/h5\u003e\n\u003cp\u003e系统会记录用户长期的使用应用的习惯，自主地向用户推荐相关的应用。例如，早上起床后我会习惯打开微博，系统早上就会给我推荐微博；来到公司我会习惯打开微信，系统也会根据地理位置给我推荐微信。系统会通过记录用户的使用频率、时间、地理位置等维度，向用户推荐应用。\u003c/p\u003e\n\u003ch5 id=\"关于耳机已连接\"\u003e关于“耳机”已连接\u003c/h5\u003e\n\u003cp\u003e手机连接蓝牙耳机或者有线耳机后，系统会弹出 “耳机”已连接 的banner，如果手机装有多个音乐应用，目前观察到的情况是会弹出最常用的那个应用。但是有时候又不会弹，而且弹出维持的时间也没有规律。\u003c/p\u003e\n\u003ch5 id=\"国外用户如是说\"\u003e国外用户如是说：\u003c/h5\u003e\n\u003chr\u003e\n\u003cp\u003eThis 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.\u003c/p\u003e","title":"多任务页面底部 banner 出现的种类"},{"content":"一、内存分配 结构体分配内存，内存对齐（8的倍数） 对象分配内存，内存对齐（16的倍数） 一个NSObject对象占用多少内存？（一个指针变量所占用的大小（64bit，8个字节，32bit，4个字节）） class_getInstanceSize() 至少需要多少内存 malloc_size() 实际分配了多少内存 二、对象 对象类型（实例对象、类对象、元类对象）\n对象的isa指针指向哪里？（instance对象的isa指向class对象；class对象的isa指向meta-class对象；meta-class对象的isa指向基类的meta-class对象）\nOC的类信息存放在哪里？（对象方法、属性、成员变量、协议信息，存放在class对象中；类方法存放在meta-class对象中；成员变量的具体值存放在instance对象中）\nisa \u0026amp; ISA_MASK 才是真正的类对象或元类对象地址（arm64下是0x0000000ffffffff8ULL）\n三、KVO、KVC KVO的本质（利用runtime动态生成一个子类，重写set方法、class、dealloc、isKVOA方法，并且让instance对象的isa指向这个全新的子类；当修改instance对象的属性时，会调用_NSSetXXXValueAndNotify函数；willChangeValueForKey/setter/didChangeValueForKey） 如何手动触发KVO（手动调用willChangeValueForKey/didChangeValueForKey，必须两个方法都调用） 直接修改成员变量会触发KVO吗（不会） KVC set原理（查找set方法：setKey -\u0026gt; _setKey；查看accessInstanceVariablesDirectly；查找成员变量：_key -\u0026gt; _isKey -\u0026gt; key -\u0026gt; isKey） KVC get原理（查找get方法：getKey -\u0026gt; key -\u0026gt; isKey -\u0026gt; _key；查看accessInstanceVariablesDirectly；查找成员变量：_key -\u0026gt; _isKey -\u0026gt; key -\u0026gt; isKey） 四、Category Category 原理（编译成struct category_t，存储这对象方法、类方法、属性、协议信息，程序运行时会将Category的数据合并到类信息中） Category和Class Extension区别（Class Extension在编译的时候数据就包含在类信息中；Category在运行时才会将数据合并到类信息中） +load方法的调用顺序（类：先编译的先调用；先调用父类的 分类：按照编译顺序调用） +initialize方法在类第一次接收到消息时调用，先调用父类的+initialize，再调用子类的+initialize +load和+initialize最大的区别是，+load通过函数地址直接调用，+initialize通过objc_msgSend进行调用，遵循消息发送的机制（如果分类实现了+initialize，则调用分类的；如果子类没有实现，则调用父类的+initialize；父类的可能被调用多次） 五、关联对象 key的设置（@selector(key) / static const char key; \u0026amp;key; / static const void *key = \u0026amp;key / @“key\u0026quot;） 关联对象不是存储在被关联对象本身内存中，而是存储在全局的统一的一个AssociationManager中 AssciationsManager -\u0026gt; AssociationHashMap -\u0026gt; object: ObjectAssociationMap -\u0026gt; key: ObjectAssociation -\u0026gt; policy/value 对象销毁时会把对象对应的ObjectAssociationMap销毁 六、Block 本质是结构体对象（__main_block_impl_0） block类型（NSGlobalBlock：没有访问auto变量；NSStackBlock：访问了auto变量；NSMallocBlock：NSStackBlock copy） 捕获auto变量（NSStackBlock：不会强引用auto变量，栈block随时释放，没必要强引用） NSMallocBlock：当block拷贝到堆上时，会调用desc里面的copy函数，把auto变量拷贝到堆上；当从堆上移除时，会调用desc里面的dispose函数，release auto变量 __block 修饰符，会将其修饰的变量包装成一个对象；不能修饰全局变量、静态变量 MRC下，block不会对__block修饰的对象进行强引用（MRC下，可用__block解决循环引用问题） __weak：不会产生强引用，指向的对象销毁时会自动置nil； __unsafe_unretained：不会产生强引用，不安全，指向的对象销毁时，指针存储的地址值不变 七、Runtime isa 位域（nonpointer 1为表示优化过的指针；has_assoc 是否有设置过关联对象；has_cxx_dtor 是否有c++的析构函数；shiftcls Class、mata-Class对象的内存地址；magic 对象是否完成初始化；weakly_referenced 是否被弱引用指向过；deallocating 对象是否正在释放；extra_rc 引用计数减1；has_sidetable_rc 引用计数器过大会存储在SideTable中） SEL 代表方法、函数名，底层结构跟 char *类似 不同类中相同名字的方法，对应的SEL是相同的 imp 指向函数实现的指针 Type Encoding “i24@0:8i16f20” 代表：返回值是int，参数总大小为24字节，第一个参数类型是id，从全部参数的第0个字节开始，第二个参数类型是SEL，从全部参数的第8个字节开始，第三个参数类型是int，从全部参数的第16个字节开始，第四个参数类型是float，从全部参数的第24个字节开始 方法缓存 cache_t 用散列表缓存曾经调用过的方法，提高方法查找速度（空间换时间） 消息发送流程：从cache查找 -\u0026gt; 通过isa指针找到类对象并从class_rw_t里面的方法列表里面查找 -\u0026gt; 从superclass的cache里面查找 -\u0026gt; 从superclass的方法列表里面查找 -\u0026gt; resolveInstanceMathod 尝试动态方法解析一次 -\u0026gt; 重走一次查找方法流程 -\u0026gt; forwardingTargetForSelector 快速消息转发 -\u0026gt; forwardInvocation 完整消息转发（需要先获取方法签名 methodSignatureForSelector） NSMethodSignature 方法签名：返回值类型、参数类型 NSInvocation 封装了一个方法调用，包括：消息接受者、方法名、方法参数 super：objc_msgSendSuper({self, superclass}, @selector(class))，接收者仍然是子类对象，从父类开始查找方法实现 runtime用途：关联对象，遍历所有成员变量，字典转模型，自动归档解档，交换方法实现，消息转发机制 交换方法时注意类簇，如 NSMutableArray -\u0026gt; __NSArrayM 八、属性 @dynamic 不要自动生成setter、getter、成员变量 九、RunLoop 每条线程都有唯一的与之对应的RunLoop对象 RunLoop保存在一个全局的字典里，线程作为key，RunLoop作为value 线程刚创建时没有RunLoop对象，RunLoop会在第一次获取它时创建 一个RunLoop包含若干个Mode，RunLoop启动时只能选择其中一个Mode作为currentMode，如果需要切换Mode，只能重启RunLoop 如果Mode里面没有任何Sources0/Source1/Timer/Observer，RunLoop会立马退出 Source0: 触摸事件/performSelector:onThread: Source1: 基于Port的进程间通信/系统事件捕捉 Timers: NSTimer/performSelector:withObject:afterDelay: Observers: 监听RunLoop状态/UI刷新/AutoreleasePool（BeforeWaiting） 作用：保持程序持续运行、处理各种事件、节省cpu资源 GCD不依赖RunLoop，但是异步到主线程则会唤醒RunLoop RunLoop响应用户操作：Source1接收事件，交给Source0处理 RunLoop应用：线程保活/卡顿监控/Timer滑动时停止/性能优化 defaultMode和trackingMode都是真实存在的模式，commonMode时一个标记 [NSRunLoop run] 相当于不停调用 [NSRunLoop runMode:beforeDate:] 子线程默认没有RunLoop，子线程中调用[performSelector:withObject:afterDelay:]无效 十、多线程 同步：在当前线程中执行任务，不具备开启新线程的能力； 异步：在新的线程中执行任务，具备开启新线程的能力； 并发：多个任务并发执行； 串行：一个任务执行完毕后，再执行下一个任务； 使用sync往当前串行队列中添加任务，会产生死锁； OSSpinLock：自旋锁，等待锁的线程会处于忙等状态，一直占用CPU资源；优先级反转；如果等待锁的线程优先级较高，它会一直占用着CPU资源，优先级低的线程就无法释放锁； 递归锁：允许同一个线程对一把锁重复加锁 atomic，相当于在getter、setter内部加了锁，并不能保证使用属性的过程是线程安全的 多读单写：pthread_rwlock dispatch_barrier_async 通过dispatch_barrier_async实现多读单写，传入的并发队列必须时通过dispatch_queue_create创建的 十一、Timer NSTimer：内存泄漏问题，不准时问题 十二、内存布局 从低地址到高地址：保留 -\u0026gt; 代码段 -\u0026gt; 数据段（字符串常量 -\u0026gt; 已初始化数据 -\u0026gt; 未初始化数据） -\u0026gt; 堆 -\u0026gt; 栈 -\u0026gt; 内核区 TaggedPointer：Tag + Data，将数据直接存储到指针中；当指针不够存储数据时，才会使用动态分配内存的方式来存储数据 如何判断TaggedPointer：macOS：最低有效位为1；iOS：最高有效位为1 堆空间中对象的内存地址最后一位是0，因为结构体有内存对齐，16的倍数 引用计数：新创建的对象引用计数为1，当引用计数为0时对象会被销毁 synthesize：生成成员变量和属性的setter、getter alloc/new/copy/mutableCopy 返回了一个对象，需要调用release或者autorelease copy：不可变拷贝，产生不可变副本 mutableCopy：可变拷贝，产生可变副本 深拷贝：内容拷贝，产生新的对象 浅拷贝：指针拷贝，没有产生新的对象 自定义对象实现copy：遵守NSCopying协议，并实现-copyWithZone方法 引用计数的存储：64bits中，引用计数可以存储在isa指针中，如果不够存储，则会存储在SideTable中 AutoreleasePoolPage：每个对象占用4096个字节，存放内部成员变量和autorelease对象的地址 AutoreleasePoolPage通过双向链表的形式连接在一起 autorelease与runloop的关系：entry -\u0026gt; push; beforeWaiting -\u0026gt; pop -\u0026gt; push（runloop休眠之前）; exit -\u0026gt; pop 十三、CPU卡顿优化 尽量使用轻量级的对象，如CALayer代替UIView 不要频繁修改UIView的frame、bounds、trasform等属性 提前计算好布局，有需要时一次性修改，避免多次修改 Autolayout会比直接设置frame消耗更多CPU资源 图片的size最好跟UIImageView的size保持一致 控制线程的最大并发数 把耗时操作放在子线程（文本处理（尺寸计算、绘制）、图片处理（解码、绘制）） 十四、GPU卡顿优化 避免短时间内大量图片的显示，尽可能将多张图片合成一张进行显示 GPU能处理的最大纹理尺寸是409684096，超过这个尺寸会占用CPU资源进行处理 尽量减少视图数量和层次 减少透明的视图 避免出现离屏渲染 离屏喧嚷需要创建新的缓冲区，需要多次切换上下文环境 触发离屏渲染：光栅化（layer.shouldRasterize = YES）；遮罩（layer.mask）；圆角（同时设置 layer.masksToBounds = YES、layer.cornerRadius \u0026gt; 0） 十五、耗电优化 尽可能降低CPU、GPU功耗 少用定时器 优化IO操作（避免频繁写入小数据，批量写入；使用dispatch_io操作文件；数据量比较大时使用数据库） 网络优化（压缩网络数据；使用缓存；使用断点续传；用户可取消未响应请求；批量传输） 十六、App的加载 装载App的可执行文件，递归加载所有依赖的动态库 调用map_images进行可执行文件内容的解析和处理 在load_images中调用call_load_methods，调用所有Class和Category的+load方法 进行各种objc结构的初始化（注册Objc类，初始化类对象等等） 调用C++静态初始化器和__attribute__((constructor))修饰的函数 十七、启动优化 减少动态库 减少Objc类、分类、Selector数量 减少C++虚函数数量 Swift尽量使用struct +initialize和dispatch_once取代__attribute__((constructor))、C++静态构造器、Objc的+load ","permalink":"https://blog.axqora.com/posts/ios-learn-note/","summary":"\u003ch2 id=\"一内存分配\"\u003e一、内存分配\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e结构体分配内存，内存对齐（8的倍数）\u003c/li\u003e\n\u003cli\u003e对象分配内存，内存对齐（16的倍数）\u003c/li\u003e\n\u003cli\u003e一个NSObject对象占用多少内存？（一个指针变量所占用的大小（64bit，8个字节，32bit，4个字节））\u003c/li\u003e\n\u003cli\u003eclass_getInstanceSize() 至少需要多少内存\u003c/li\u003e\n\u003cli\u003emalloc_size() 实际分配了多少内存\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"二对象\"\u003e二、对象\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e对象类型（实例对象、类对象、元类对象）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e对象的isa指针指向哪里？（instance对象的isa指向class对象；class对象的isa指向meta-class对象；meta-class对象的isa指向基类的meta-class对象）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eOC的类信息存放在哪里？（对象方法、属性、成员变量、协议信息，存放在class对象中；类方法存放在meta-class对象中；成员变量的具体值存放在instance对象中）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eisa \u0026amp; ISA_MASK 才是真正的类对象或元类对象地址（arm64下是0x0000000ffffffff8ULL）\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"F4AA41A6-F6E4-4F4E-9BA7-5CC9AC8C4733\" loading=\"lazy\" src=\"/image/iOS-learn-note/F4AA41A6-F6E4-4F4E-9BA7-5CC9AC8C4733.png\"\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"三kvokvc\"\u003e三、KVO、KVC\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eKVO的本质（利用runtime动态生成一个子类，重写set方法、class、dealloc、isKVOA方法，并且让instance对象的isa指向这个全新的子类；当修改instance对象的属性时，会调用_NSSetXXXValueAndNotify函数；willChangeValueForKey/setter/didChangeValueForKey）\u003c/li\u003e\n\u003cli\u003e如何手动触发KVO（手动调用willChangeValueForKey/didChangeValueForKey，必须两个方法都调用）\u003c/li\u003e\n\u003cli\u003e直接修改成员变量会触发KVO吗（不会）\u003c/li\u003e\n\u003cli\u003eKVC set原理（查找set方法：setKey -\u0026gt; _setKey；查看accessInstanceVariablesDirectly；查找成员变量：_key -\u0026gt; _isKey -\u0026gt; key -\u0026gt; isKey）\u003c/li\u003e\n\u003cli\u003eKVC get原理（查找get方法：getKey -\u0026gt; key -\u0026gt; isKey -\u0026gt; _key；查看accessInstanceVariablesDirectly；查找成员变量：_key -\u0026gt; _isKey -\u0026gt; key -\u0026gt; isKey）\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"四category\"\u003e四、Category\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eCategory 原理（编译成struct category_t，存储这对象方法、类方法、属性、协议信息，程序运行时会将Category的数据合并到类信息中）\u003c/li\u003e\n\u003cli\u003eCategory和Class Extension区别（Class Extension在编译的时候数据就包含在类信息中；Category在运行时才会将数据合并到类信息中）\u003c/li\u003e\n\u003cli\u003e+load方法的调用顺序（类：先编译的先调用；先调用父类的  分类：按照编译顺序调用）\u003c/li\u003e\n\u003cli\u003e+initialize方法在类第一次接收到消息时调用，先调用父类的+initialize，再调用子类的+initialize\u003c/li\u003e\n\u003cli\u003e+load和+initialize最大的区别是，+load通过函数地址直接调用，+initialize通过objc_msgSend进行调用，遵循消息发送的机制（如果分类实现了+initialize，则调用分类的；如果子类没有实现，则调用父类的+initialize；父类的可能被调用多次）\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"五关联对象\"\u003e五、关联对象\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003ekey的设置（@selector(key) / static const char key; \u0026amp;key; / static const void *key = \u0026amp;key / @“key\u0026quot;）\u003c/li\u003e\n\u003cli\u003e关联对象不是存储在被关联对象本身内存中，而是存储在全局的统一的一个AssociationManager中\u003c/li\u003e\n\u003cli\u003eAssciationsManager -\u0026gt; AssociationHashMap -\u0026gt; object: ObjectAssociationMap -\u0026gt; key: ObjectAssociation -\u0026gt; policy/value\u003c/li\u003e\n\u003cli\u003e对象销毁时会把对象对应的ObjectAssociationMap销毁\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"六block\"\u003e六、Block\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e本质是结构体对象（__main_block_impl_0）\u003c/li\u003e\n\u003cli\u003eblock类型（NSGlobalBlock：没有访问auto变量；NSStackBlock：访问了auto变量；NSMallocBlock：NSStackBlock copy）\u003c/li\u003e\n\u003cli\u003e捕获auto变量（NSStackBlock：不会强引用auto变量，栈block随时释放，没必要强引用）\u003c/li\u003e\n\u003cli\u003eNSMallocBlock：当block拷贝到堆上时，会调用desc里面的copy函数，把auto变量拷贝到堆上；当从堆上移除时，会调用desc里面的dispose函数，release auto变量\u003c/li\u003e\n\u003cli\u003e__block 修饰符，会将其修饰的变量包装成一个对象；不能修饰全局变量、静态变量\u003c/li\u003e\n\u003cli\u003eMRC下，block不会对__block修饰的对象进行强引用（MRC下，可用__block解决循环引用问题）\u003c/li\u003e\n\u003cli\u003e__weak：不会产生强引用，指向的对象销毁时会自动置nil； __unsafe_unretained：不会产生强引用，不安全，指向的对象销毁时，指针存储的地址值不变\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cimg alt=\"6E1C8B94-0447-4416-873D-9705EA7026F9\" loading=\"lazy\" src=\"/image/iOS-learn-note/6E1C8B94-0447-4416-873D-9705EA7026F9.png\"\u003e\u003c/p\u003e","title":"iOS 学习笔记"},{"content":"Reveal是一款UI调试工具，在iOS开发过程中可查看UI的层级关系并可动态修改界面，可以有效提高开发效率。这篇文章主要介绍如何把Reveal集成到实际的项目中。\n一、Reveal主要有两种集成方式： 1、framework集成（可忽略，直接看第二点） （1）打开Reveal，选择 Reveal \u0026ndash;\u0026gt; Help \u0026ndash;\u0026gt; Show Reveal Library in Finder \u0026ndash;\u0026gt; iOS Library\n（2）把Reveal.framework导入到项目中\n（3）配置Target，Target \u0026ndash;\u0026gt; Build Setting \u0026ndash;\u0026gt; Other Linker Flags 添加以下4项：\n（4）运行项目 \u0026ndash;\u0026gt; 打开Reveal \u0026ndash;\u0026gt; 选择连接设备\n项目的UI图层就可以出来了。\n这种方法有几个弊端：1、需要配置项目的Build Setting，修改Other Linker Flags，多人协作开发的话如果有人没有配置就会编译出错；2、如果不留意容易打包的时候把Reveal的framework也打包进去，增大包的体积。\n以下推荐另一种集成方法。\n2、LLDB集成（推荐！！） （1）运行项目 \u0026ndash;\u0026gt; 点击Pause program execution。\n（2）在LLDB依次输入以下两条命令\n# expr (Class)NSClassFromString(@\u0026ldquo;IBARevealLoader\u0026rdquo;) == nil ? (void*)dlopen(\u0026quot;/Applications/Reveal.app/Contents/SharedSupport/iOS-Libraries/libReveal.dylib\u0026quot;, 0x2) : ((void*)0)\n# expr (void)[(NSNotificationCenter *)[NSNotificationCenter defaultCenter] postNotificationName:@\u0026ldquo;IBARevealRequestStart\u0026rdquo; object:nil];\n第一条命令用于加载Reveal的动态链接库；\n第二条命令用于启动Reveal调试服务。\n输入两条命令后，如果LLDB打印 “ INFO: Reveal Server started ” 即成功启动Reveal服务，点击Continue即可。\n虽然说输入两条命令即可启动服务，但是每次重新启动项目后都要重新输入这么长的命令也是挺苦恼的，下面介绍一种方法可以非常方便地使用Reveal的命令。\n二、设置LLDB命令别名 打开终端，输入 vim ~/.lldbinit ，然后输入以下内容：\ncommand alias reveal_load_sim expr (Class)NSClassFromString(@\u0026ldquo;IBARevealLoader\u0026rdquo;) == nil ? (void*)dlopen(\u0026quot;/Applications/Reveal.app/Contents/SharedSupport/iOS-Libraries/libReveal.dylib\u0026quot;, 0x2) : ((void*)0)\ncommand alias reveal_start expr (void)[(NSNotificationCenter*)[NSNotificationCenter defaultCenter] postNotificationName:@\u0026ldquo;IBARevealRequestStart\u0026rdquo; object:nil];\ncommand alias reveal_stop expr (void)[(NSNotificationCenter*)[NSNotificationCenter defaultCenter] postNotificationName:@\u0026ldquo;IBARevealRequestStop\u0026rdquo; object:nil];\nreveal_load_sim为模拟器加载reveal调试用的动态链接库\nreveal_start启动reveal调试功能\nreveal_stop结束reveal调试功能\n输入 :wq 保存\n这时候打开Xcode，在LLDB里面就会有输入提示了。\n按顺序输入reveal_load_sim \u0026ndash;\u0026gt; reveal_start 即可达到上面的效果。\n参考资料 iOS开发中集成Reveal\nUI调试神器 for ios：Reveal的使用与破解\n","permalink":"https://blog.axqora.com/posts/ios-reveal/","summary":"\u003cp\u003eReveal是一款UI调试工具，在iOS开发过程中可查看UI的层级关系并可动态修改界面，可以有效提高开发效率。这篇文章主要介绍如何把Reveal集成到实际的项目中。\u003c/p\u003e\n\u003ch1 id=\"一reveal主要有两种集成方式\"\u003e一、Reveal主要有两种集成方式：\u003c/h1\u003e\n\u003ch3 id=\"1framework集成可忽略直接看第二点\"\u003e1、framework集成（可忽略，直接看第二点）\u003c/h3\u003e\n\u003cp\u003e（1）打开Reveal，选择 Reveal \u0026ndash;\u0026gt; Help \u0026ndash;\u0026gt; Show Reveal Library in Finder \u0026ndash;\u0026gt; iOS Library\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/iOS-Reveal/ios_reveal_1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/iOS-Reveal/ios_reveal_2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e（2）把Reveal.framework导入到项目中\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/iOS-Reveal/ios_reveal_3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e（3）配置Target，Target \u0026ndash;\u0026gt; Build Setting \u0026ndash;\u0026gt; Other Linker Flags 添加以下4项：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/iOS-Reveal/ios_reveal_4.png\"\u003e\u003c/p\u003e\n\u003cp\u003e（4）运行项目 \u0026ndash;\u0026gt; 打开Reveal \u0026ndash;\u0026gt; 选择连接设备\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/iOS-Reveal/ios_reveal_5.png\"\u003e\u003c/p\u003e\n\u003cp\u003e项目的UI图层就可以出来了。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/iOS-Reveal/ios_reveal_6.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这种方法有几个弊端：1、需要配置项目的Build Setting，修改Other Linker Flags，多人协作开发的话如果有人没有配置就会编译出错；2、如果不留意容易打包的时候把Reveal的framework也打包进去，增大包的体积。\u003c/p\u003e\n\u003cp\u003e以下推荐另一种集成方法。\u003c/p\u003e\n\u003ch3 id=\"2lldb集成推荐\"\u003e2、LLDB集成（推荐！！）\u003c/h3\u003e\n\u003cp\u003e（1）运行项目 \u0026ndash;\u0026gt; 点击Pause program execution。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/iOS-Reveal/ios_reveal_7.png\"\u003e\u003c/p\u003e\n\u003cp\u003e（2）在LLDB依次输入以下两条命令\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e# expr (Class)NSClassFromString(@\u0026ldquo;IBARevealLoader\u0026rdquo;) == nil ? (void*)dlopen(\u0026quot;/Applications/Reveal.app/Contents/SharedSupport/iOS-Libraries/libReveal.dylib\u0026quot;, 0x2) : ((void*)0)\u003c/p\u003e\n\u003cp\u003e# expr (void)[(NSNotificationCenter *)[NSNotificationCenter defaultCenter] postNotificationName:@\u0026ldquo;IBARevealRequestStart\u0026rdquo; object:nil];\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e第一条命令用于加载Reveal的动态链接库；\u003c/p\u003e\n\u003cp\u003e第二条命令用于启动Reveal调试服务。\u003c/p\u003e","title":"iOS 项目集成 Reveal"},{"content":"\n一、前言 前一段时间彩云小译上了App Store的推荐，我下载试玩了一下，效果还是非常不错的。它可以实现实时翻译的功能，我自己粗浅地分析了一下彩云小译的实现原理，其中最重要的一步就是声音转文字。\n目前市面上也有很多服务商提供声音转文字的服务，有收费的有免费的，但是毕竟是第三方的服务商，接口的性能和稳定性都不一定能保证。\n2016年Apple在发布重磅产品iOS10的同时也发布了Speech Kit语音识别框架，大名鼎鼎的Siri的语音识别就是基于Speech Kit实现的。有了Speech Kit，我们就可以非常简单地实现声音转文字的功能。下面我就简单介绍一下Speech Kit的用法。\n二、实现 1、页面布局 因为只是实现一个Demo，页面不需要多复杂，只需要在Storyboard上拖入两个控件：一个UITextView用于展示声音转文字的结果，一个UIButton用于触发语音识别，最好布置好约束即可。具体效果如下图：\n2、申请用户权限 首先需要引入Speech Kit框架\n#import \u0026lt;Speech/Speech.h\u0026gt; 申请权限非常简单，在识别前（viewDidLoad:）加入以下代码即可申请语音识别的权限：\n- (void)viewDidLoad { [super viewDidLoad]; // 请求权限 [SFSpeechRecognizer requestAuthorization:^(SFSpeechRecognizerAuthorizationStatus status) { NSLog(@\u0026#34;status %@\u0026#34;, status == SFSpeechRecognizerAuthorizationStatusAuthorized ? @\u0026#34;授权成功\u0026#34; : @\u0026#34;授权失败\u0026#34;); }]; } 这时候运行起来会崩溃，原因是在iOS10后需要在info.plist文件中添加麦克分和语音识别权限申请信息：\n\u0026lt;key\u0026gt;NSSpeechRecognitionUsageDescription\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;请允许语音识别\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;NSMicrophoneUsageDescription\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;请打开麦克风\u0026lt;/string\u0026gt; 运行项目，会提示打开语音识别和打开麦克风权限，至此我们已经完成了权限的申请。\n3、初始化语音识别引擎 添加以下代码：\n- (void)initEngine { if (!self.speechRecognizer) { // 设置语言 NSLocale *locale = [NSLocale localeWithLocaleIdentifier:@\u0026#34;zh-CN\u0026#34;]; self.speechRecognizer = [[SFSpeechRecognizer alloc] initWithLocale:locale]; } if (!self.audioEngine) { self.audioEngine = [[AVAudioEngine alloc] init]; } AVAudioSession *audioSession = [AVAudioSession sharedInstance]; [audioSession setCategory:AVAudioSessionCategoryRecord mode:AVAudioSessionModeMeasurement options:AVAudioSessionCategoryOptionDuckOthers error:nil]; [audioSession setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil]; if (self.recognitionRequest) { [self.recognitionRequest endAudio]; self.recognitionRequest = nil; } self.recognitionRequest = [[SFSpeechAudioBufferRecognitionRequest alloc] init]; self.recognitionRequest.shouldReportPartialResults = YES; } 初始化SFSpeechRecognizer时需要传入一个NSLocle对象，用于标识用户输入的语种，如\u0026quot;zh-CN\u0026quot;代表普通话，\u0026ldquo;en_US\u0026quot;代表英文。 AVAudioEngine是音频引擎，用于音频输入。 利用AVAudioSession对象进行音频录制的配置。 在语音识别产生最终结果之前可能产生多种结果，设置SFSpeechAudioBufferRecognitionRequest对象的shouldReportPartialResult属性为YES意味着每产生一种结果就马上返回。 4、启动语音识别引擎 添加以下代码：\n- (void)startRecording:(UIButton *)recordButton { [self initEngine]; AVAudioFormat *recordingFormat = [[self.audioEngine inputNode] outputFormatForBus:0]; [[self.audioEngine inputNode] installTapOnBus:0 bufferSize:1024 format:recordingFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) { [self.recognitionRequest appendAudioPCMBuffer:buffer]; }]; [self.audioEngine prepare]; [self.audioEngine startAndReturnError:nil]; [recordButton setTitle:@\u0026#34;录音ing\u0026#34; forState:UIControlStateNormal]; } 为self.recordButton添加点击事件。 设置音频录制的格式及音频流回调的处理(把音频流拼接到self.recognitionRequest)。 开始录制音频。 修改按钮文案。 5、重置语音识别引擎 添加以下代码：\n- (void)stopRecording:(UIButton *)recordButton { [[self.audioEngine inputNode] removeTapOnBus:0]; [self.audioEngine stop]; [self.recognitionRequest endAudio]; self.recognitionRequest = nil; [recordButton setTitle:@\u0026#34;录音\u0026#34; forState:UIControlStateNormal]; } 为self.recordButton添加点击事件。 停止音频录制引擎。 停止识别器。 修改按钮文案。 6、语音识别结果的回调 下面是语音识别器SFSpeechRecognizer的API描述：\n// Recognize speech utterance with a request // If request.shouldReportPartialResults is true, result handler will be called // repeatedly with partial results, then finally with a final result or an error. - (SFSpeechRecognitionTask *)recognitionTaskWithRequest:(SFSpeechRecognitionRequest *)request resultHandler:(void (^)(SFSpeechRecognitionResult * __nullable result, NSError * __nullable error))resultHandler; // Advanced API: Recognize a custom request with with a delegate // The delegate will be weakly referenced by the returned task - (SFSpeechRecognitionTask *)recognitionTaskWithRequest:(SFSpeechRecognitionRequest *)request delegate:(id \u0026lt;SFSpeechRecognitionTaskDelegate\u0026gt;)delegate; 语音识别结果的回调有两种方式，一种是delegate，一种是block，这里为了简单，先采用block的方式回调。\n初始化语音识别器SFSpeechRecognizer时添加以下代码：\n[self.speechRecognizer recognitionTaskWithRequest:self.recognitionRequest resultHandler:^(SFSpeechRecognitionResult * _Nullable result, NSError * _Nullable error) { NSLog(@\u0026#34;is final: %d result: %@\u0026#34;, result.isFinal, result.bestTranscription.formattedString); if (result.isFinal) { self.textView.text = [NSString stringWithFormat:@\u0026#34;%@%@\u0026#34;, self.textView.text, result.bestTranscription.formattedString]; } }]; 7、识别音频文件 添加以下代码\n- (IBAction)startRecognizing:(id)sender { SFSpeechRecognizer *recognizer = [[SFSpeechRecognizer alloc] initWithLocale:[NSLocale localeWithLocaleIdentifier:@\u0026#34;zh_CN\u0026#34;]]; NSURL *url = [[NSBundle mainBundle] URLForResource:@\u0026#34;test.mp3\u0026#34; withExtension:nil]; SFSpeechURLRecognitionRequest *request = [[SFSpeechURLRecognitionRequest alloc] initWithURL:url]; [recognizer recognitionTaskWithRequest:request resultHandler:^(SFSpeechRecognitionResult * _Nullable result, NSError * _Nullable error) { if (result.isFinal) { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@\u0026#34;识别结果\u0026#34; message:[NSString stringWithFormat:@\u0026#34;%@\u0026#34;, result.bestTranscription.formattedString] preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *confirm = [UIAlertAction actionWithTitle:@\u0026#34;OK\u0026#34; style:UIAlertActionStyleCancel handler:nil]; [alert addAction:confirm]; [self presentViewController:alert animated:YES completion:nil]; } }]; } 初始化语音识别器SFSpeechRecognizer。 获取音频文件路径。 初始化语音识别请求SFSpeechURLRecognitionRequest。 设置回调。 三、总结 本文章主要介绍了如何利用iOS系统自带的Speech Kit框架实现音频转文字的功能，Speech Kit相当强大，本文章只是非常简单的介绍了录音识别及音频文件识别而已，大家有兴趣可以深入研究，有问题也可以一起探讨。\n声音转文字的功能我们已经实现了，下一步我们可以找到翻译服务提供商的服务，就可以实现一个简易版的实时翻译应用了。\n本文Demo：https://github.com/OuDuShu/SpeechTest\n四、参考 http://swift.gg/2016/09/30/siri-speech-framework/ https://developer.apple.com/videos/play/wwdc2016/509/ https://developer.nuance.com/public/Help/DragonMobileSDKReference_iOS/Getting-started.html https://www.raywenderlich.com/60870/building-ios-app-like-siri\n","permalink":"https://blog.axqora.com/posts/ios-speech-kit/","summary":"\u003cp\u003e\u003cimg alt=\"Siri.jpeg\" loading=\"lazy\" src=\"/image/Speech-Kit/speech_kit_1.jpg\"\u003e\u003c/p\u003e\n\u003ch2 id=\"一前言\"\u003e一、前言\u003c/h2\u003e\n\u003cp\u003e前一段时间彩云小译上了App Store的推荐，我下载试玩了一下，效果还是非常不错的。它可以实现实时翻译的功能，我自己粗浅地分析了一下彩云小译的实现原理，其中最重要的一步就是声音转文字。\u003c/p\u003e\n\u003cp\u003e目前市面上也有很多服务商提供声音转文字的服务，有收费的有免费的，但是毕竟是第三方的服务商，接口的性能和稳定性都不一定能保证。\u003c/p\u003e\n\u003cp\u003e2016年Apple在发布重磅产品iOS10的同时也发布了Speech Kit语音识别框架，大名鼎鼎的Siri的语音识别就是基于Speech Kit实现的。有了Speech Kit，我们就可以非常简单地实现声音转文字的功能。下面我就简单介绍一下Speech Kit的用法。\u003c/p\u003e\n\u003ch2 id=\"二实现\"\u003e二、实现\u003c/h2\u003e\n\u003ch3 id=\"1页面布局\"\u003e1、页面布局\u003c/h3\u003e\n\u003cp\u003e因为只是实现一个Demo，页面不需要多复杂，只需要在Storyboard上拖入两个控件：一个UITextView用于展示声音转文字的结果，一个UIButton用于触发语音识别，最好布置好约束即可。具体效果如下图：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"控件布局\" loading=\"lazy\" src=\"/image/Speech-Kit/speech_kit_2.jpg\"\u003e\u003c/p\u003e\n\u003ch3 id=\"2申请用户权限\"\u003e2、申请用户权限\u003c/h3\u003e\n\u003cp\u003e首先需要引入Speech Kit框架\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e#import \u0026lt;Speech/Speech.h\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e申请权限非常简单，在识别前（viewDidLoad:）加入以下代码即可申请语音识别的权限：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e- (void)viewDidLoad {\n    [super viewDidLoad];\n    \n    // 请求权限\n    [SFSpeechRecognizer requestAuthorization:^(SFSpeechRecognizerAuthorizationStatus status) {\n        NSLog(@\u0026#34;status %@\u0026#34;, status == SFSpeechRecognizerAuthorizationStatusAuthorized ? @\u0026#34;授权成功\u0026#34; : @\u0026#34;授权失败\u0026#34;);\n    }];\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e这时候运行起来会崩溃，原因是在iOS10后需要在info.plist文件中添加麦克分和语音识别权限申请信息：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026lt;key\u0026gt;NSSpeechRecognitionUsageDescription\u0026lt;/key\u0026gt;\n\u0026lt;string\u0026gt;请允许语音识别\u0026lt;/string\u0026gt;\n\u0026lt;key\u0026gt;NSMicrophoneUsageDescription\u0026lt;/key\u0026gt;\n\u0026lt;string\u0026gt;请打开麦克风\u0026lt;/string\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e运行项目，会提示打开语音识别和打开麦克风权限，至此我们已经完成了权限的申请。\u003c/p\u003e\n\u003ch3 id=\"3初始化语音识别引擎\"\u003e3、初始化语音识别引擎\u003c/h3\u003e\n\u003cp\u003e添加以下代码：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e- (void)initEngine {\n    if (!self.speechRecognizer) {\n        // 设置语言\n        NSLocale *locale = [NSLocale localeWithLocaleIdentifier:@\u0026#34;zh-CN\u0026#34;];\n        self.speechRecognizer = [[SFSpeechRecognizer alloc] initWithLocale:locale];\n    }\n    if (!self.audioEngine) {\n        self.audioEngine = [[AVAudioEngine alloc] init];\n    }\n    \n    AVAudioSession *audioSession = [AVAudioSession sharedInstance];\n    [audioSession setCategory:AVAudioSessionCategoryRecord mode:AVAudioSessionModeMeasurement options:AVAudioSessionCategoryOptionDuckOthers error:nil];\n    [audioSession setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];\n    \n    if (self.recognitionRequest) {\n        [self.recognitionRequest endAudio];\n        self.recognitionRequest = nil;\n    }\n    self.recognitionRequest = [[SFSpeechAudioBufferRecognitionRequest alloc] init];\n    self.recognitionRequest.shouldReportPartialResults = YES; \n}\n\u003c/code\u003e\u003c/pre\u003e\u003col\u003e\n\u003cli\u003e初始化\u003ccode\u003eSFSpeechRecognizer\u003c/code\u003e时需要传入一个\u003ccode\u003eNSLocle\u003c/code\u003e对象，用于标识用户输入的语种，如\u0026quot;zh-CN\u0026quot;代表普通话，\u0026ldquo;en_US\u0026quot;代表英文。\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eAVAudioEngine\u003c/code\u003e是音频引擎，用于音频输入。\u003c/li\u003e\n\u003cli\u003e利用\u003ccode\u003eAVAudioSession\u003c/code\u003e对象进行音频录制的配置。\u003c/li\u003e\n\u003cli\u003e在语音识别产生最终结果之前可能产生多种结果，设置\u003ccode\u003eSFSpeechAudioBufferRecognitionRequest\u003c/code\u003e对象的\u003ccode\u003eshouldReportPartialResult\u003c/code\u003e属性为YES意味着每产生一种结果就马上返回。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"4启动语音识别引擎\"\u003e4、启动语音识别引擎\u003c/h3\u003e\n\u003cp\u003e添加以下代码：\u003c/p\u003e","title":"iOS 利用 Speech Kit 实现语音识别"},{"content":"一、前言 作为移动开发者的我们，为了良好的用户体验，经常需要模拟手机应用在比较差的网络环境下的表现，模拟网络环境的方式有很多，比如使用Charles，或者在手机的开发者模式下模拟网络环境等等，但是这些都有一定的门槛。\n使用Charles，首先你得连接WiFi，然后设置代理，接着开启网络模拟模式，最后测试完了如果忘了关闭代理，可能手机就上不了网了。而且都连上你电脑的代理的话就一次只能模拟一种网络环境。\n使用手机的开发者模式，首先你的手机能进入开发者模式。\n如果你需要随便抓一位不懂技术同事帮你测试，这些方式都不太友好，有没有一种方式可以连上WiFi就可以使用的测试方式呢？有！接下来就介绍Facebook出品的一款网络模拟工具ATC。\n二、简介 Augmented Traffic Control (ATC) is a tool to simulate network conditions. It allows controlling the connection that a device has to the internet. Developers can use ATC to test their application across varying network conditions, easily emulating high speed, mobile, and even severely impaired networks.\nATC全名叫Augmented Traffic Control，是Facebook出品的一款网络模拟工具，移动开发者可以通过这款工具模拟不同条件下的网络环境，可以通过网页自由地模拟网络带宽（bandwidth）、延迟（latency）、丢包率（packet loss）、错包率（corrupted packets）和乱序率（packets ordering）。\n而且！！！更牛逼的是：不同的设备连接到同一WiFi还可以模拟不同的网络环境互不影响。\ngithub地址\n三、准备 1、树莓派3（已内置有无线网卡）\n2、已刷入最新RASPBIAN系统的SD卡\n四、安装 安装主要有两步：\n1、让树莓派有发射AP热点的能力；\n2、安装ATC。\n1、让树莓派有发射AP热点的能力 1.安装hostapd虚拟热点程序和dnsmasq配置DHCP、DNS服务程序：\nsudo apt-get install dnsmasq hostapd\n2.编辑sudo vim /etc/dhcpcd.conf文件，在该文件的最后加入下面的命令，用来为wlan0固定一个内网IP。\ninterface wlan0 static ip_address=10.0.0.1/24 3.编辑sudo vim /etc/dnsmasq.confdnsmasq配置文件，有600+行的内容，我们可以shift+g滚动到最下面，然后加入下面的命令，用来控制IP地址的取值范围\ninterface=wlan0 dhcp-range=10.0.0.2,10.0.0.50,255.255.255.0,12h 4.添加hostapd.conf配置文件sudo vim /etc/hostapd/hostapd.conf并添加下面内容，用于配置热点的账号密码等信息。\nssid=PI3 #账号 wpa_passphrase=12345678 #密码 interface=wlan0 driver=nl80211 hw_mode=g channel=10 macaddr_acl=0 auth_algs=1 wpa=2 wpa_key_mgmt=WPA-PSK wpa_pairwise=CCMP rsn_pairwise=CCMP 5.编辑sysctl.conf文件，设置路由转发sudo vim /etc/sysctl.conf，打开下面的注释\n# Uncomment the next line to enable packet forwarding for IPv4 net.ipv4.ip_forward=1 6.更新iptables规则，依次执行下面命令\nsudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE sudo iptables -A FORWARD -i eth0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT sudo iptables -A FORWARD -i wlan0 -o eth0 -j ACCEPT sudo sh -c \u0026#34;iptables-save \u0026gt; /etc/iptables.ipv4.nat\u0026#34; 7.编辑sudo vim /etc/network/interfaces，在最后加入下面的命令\nup iptables-restore \u0026lt; /etc/iptables.ipv4.nat 8.重启树莓派后执行下面命令，如无意外就可以发射热点了\nsudo service hostapd start sudo service dnsmasq start 2、安装ATC ATC需要依赖Python环境，RASPBIAN系统默认已经支持Python，所以不用再安装，如果你的是Ubuntu或者其他Linux系统，你需要输入下面命令先安装Python的依赖，并安装pip包管理工具。\nsudo apt-get install python-pip python-dev build-essential） sudo pip install --upgrade pip 1.安装ATC依赖库\nsudo pip install atc_thrift atcd django-atc-api django-atc-demo-ui django-atc-profile-storage 注意这里要加上sudo，不然会因没有权限而安装失败。\n2.创建新的Django项目\ndjango-admin startproject atcui cd atcui 3.编辑settings.py文件sudo vim atcui/settings.py，并在INSTALLED_APPS的后面加入下面参数\nINSTALLED_APPS = ( ... # Django ATC API \u0026#39;rest_framework\u0026#39;, \u0026#39;atc_api\u0026#39;, # Django ATC Demo UI \u0026#39;bootstrap_themes\u0026#39;, \u0026#39;django_static_jquery\u0026#39;, \u0026#39;atc_demo_ui\u0026#39;, # Django ATC Profile Storage \u0026#39;atc_profile_storage\u0026#39;, ) 4.编辑urls.py文件，并加入下面内容\ncd atcui sudo vim urls.py ... ... from django.views.generic.base import RedirectView from django.conf.urls import include urlpatterns = [ ... # Django ATC API url(r\u0026#39;^api/v1/\u0026#39;, include(\u0026#39;atc_api.urls\u0026#39;)), # Django ATC Demo UI url(r\u0026#39;^atc_demo_ui/\u0026#39;, include(\u0026#39;atc_demo_ui.urls\u0026#39;)), # Django ATC profile storage url(r\u0026#39;^api/v1/profiles/\u0026#39;, include(\u0026#39;atc_profile_storage.urls\u0026#39;)), url(r\u0026#39;^$\u0026#39;, RedirectView.as_view(url=\u0026#39;/atc_demo_ui/\u0026#39;, permanent=False)), ] 5.更新Django数据库\npython manage.py migrate 到这里，我们已经完成整个环境的部署，步骤有点多，只要一步一步做，应该不会出问题。\n五、使用 1、启动核心组件atcd\nsudo atcd --atcd-lan wlan0\n出现Awaiting graceful shutdown.后按Ctrl+c退出。\n2、启动Django工程\ncd .. sudo python manage.py runserver 0.0.0.0:8000 3、移动设备连接WiFi，使用浏览器输入10.0.0.1:8000/atc_demo_ui/即可进入控制中心，添加网络配置\nALLOWED_HOSTS = [\u0026lsquo;10.0.0.1\u0026rsquo;]\n六、坑 在部署ATC工具的过程中，我遇到过三个坑，还好解决起来不太难，希望大家再遇到的时候可以快速解决。\n1、权限问题 安装ATC依赖库时没有权限，在命令签名加上sudo即可。\n2、Invalid HTTP_HOST header :\u0026lsquo;xxx\u0026rsquo;. You may need to add u\u0026rsquo;xxx\u0026rsquo; to ALLOWED_HOSTS. 这个时候只需要编辑atcui目录下的settings.py文件，在ALLOWED_HOSTS后加上本机ip即可：\nALLOWED_HOSTS = [\u0026#39;10.0.0.1\u0026#39;] 3、成功进入控制中心，但是中间提示ATC is not running Google了一下，发现还蛮多人遇到同样的问题，下面是作者的回复： Issues地址\n根据作者的提示，我重新安装了django-rest-framework\nsudo apt-get install django-rest-framework 然后重启服务即可。\n","permalink":"https://blog.axqora.com/posts/raspberry-atc/","summary":"\u003ch2 id=\"一前言\"\u003e一、前言\u003c/h2\u003e\n\u003cp\u003e作为移动开发者的我们，为了良好的用户体验，经常需要模拟手机应用在比较差的网络环境下的表现，模拟网络环境的方式有很多，比如使用Charles，或者在手机的开发者模式下模拟网络环境等等，但是这些都有一定的门槛。\u003c/p\u003e\n\u003cp\u003e使用Charles，首先你得连接WiFi，然后设置代理，接着开启网络模拟模式，最后测试完了如果忘了关闭代理，可能手机就上不了网了。而且都连上你电脑的代理的话就一次只能模拟一种网络环境。\u003c/p\u003e\n\u003cp\u003e使用手机的开发者模式，首先你的手机能进入开发者模式。\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e如果你需要随便抓一位不懂技术同事帮你测试，这些方式都不太友好，有没有一种方式可以连上WiFi就可以使用的测试方式呢？有！接下来就介绍Facebook出品的一款网络模拟工具ATC。\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"二简介\"\u003e二、简介\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003eAugmented Traffic Control (ATC) is a tool to simulate network conditions. It allows controlling the connection that a device has to the internet. Developers can use ATC to test their application across varying network conditions, easily emulating high speed, mobile, and even severely impaired networks.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eATC全名叫Augmented Traffic Control，是Facebook出品的一款网络模拟工具，移动开发者可以通过这款工具模拟不同条件下的网络环境，可以通过网页自由地模拟网络带宽（bandwidth）、延迟（latency）、丢包率（packet loss）、错包率（corrupted packets）和乱序率（packets ordering）。\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e而且！！！更牛逼的是：不同的设备连接到同一WiFi还可以模拟不同的网络环境互不影响。\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://camo.githubusercontent.com/99a779517d18a605046dec36d2beb02166f4e77b/68747470733a2f2f66616365626f6f6b2e6769746875622e696f2f6175676d656e7465642d747261666669632d636f6e74726f6c2f696d616765732f6174635f6f766572766965772e706e67\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e\u003ca href=\"https://github.com/facebook/augmented-traffic-control\"\u003egithub地址\u003c/a\u003e\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"三准备\"\u003e三、准备\u003c/h2\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e1、树莓派3（已内置有无线网卡）\u003cbr\u003e\n2、已刷入最新RASPBIAN系统的SD卡\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"四安装\"\u003e四、安装\u003c/h2\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e安装主要有两步：\u003cbr\u003e\n1、让树莓派有发射AP热点的能力；\u003cbr\u003e\n2、安装ATC。\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003ch3 id=\"1让树莓派有发射ap热点的能力\"\u003e1、让树莓派有发射AP热点的能力\u003c/h3\u003e\n\u003cp\u003e1.安装hostapd虚拟热点程序和dnsmasq配置DHCP、DNS服务程序：\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003esudo apt-get install dnsmasq hostapd\u003c/code\u003e\u003c/p\u003e","title":"在树莓派上部署 ATC 网络模拟工具（Augmented Traffic Control）"},{"content":"一、主流保证数据安全的方式 1、网络传输安全 1.采用HTTPS通信协议\n可防止抓包窃取、篡改传输数据，大大增加了中间人攻击的成本。\n2.对敏感数据进行签名校正\n采用非对称加密的方式，对敏感数据使用密钥加密，到了客户端用公钥解密，验证数据一致性，防止通信过程中被篡改。\n3.采用密文传输\n别人即使截取了传输信息，也无法看懂其中的意思。\n2、本地存储数据安全 1.数据库加密\n通过越狱设备可以很容易地把整个应用包（包括里面的数据）copy出来，这样就可以获取里面的数据库，对于没有加密的数据库就可以非常轻易地读取里面的信息，造成信息的泄漏。 数据库加密可分两个维度：1.整个数据库加密；2.对部分字段先加密再存数据库。 对部分字段加密并不适合多字段的加密存储，容易导致加密数据太过分散，影响性能。所以推荐对整个数据库加密。\n2.KeyChain存储敏感数据\nKeyChain是iOS系统级的存储方式，安全性无需质疑，且删除应用或者升级系统依然可以保留里面的信息。\n3、源码安全 使用越狱设备可以很轻易地把应用砸壳，从而把源码dump下来，即使是没有太多经验的开发者也可以得到应用的类信息，包括函数名等。使用IDA等反编译工具可以看到应用的一些类名和方法名，进而可以分析功能实现的逻辑。\n1.字符串混淆\n对应用程序中使用到的字符串进行加密，保证源码被逆向后也能保护明文字符串。\n2.类名、方法名混淆\n市面上很多iOS应用都没有混淆类名方法名，以致于很容易使用class-dump下来，从而进行hook操作，一步一步实现iOS微信自动抢红包(非越狱)是很有趣的一个应用。这个库可以混淆OC的类名、协议、属性还有方法名。\n3.程序结构混淆加密\n对应用程序逻辑结构进行打乱混排，保证源码可读性降到最低。可参考这个库\n4.反调试、反注入等一些主动保护策略\n加入第三方安全性SDK\n二、对数据库加密的研究 1、主流数据库加密方式 The SQLite Encryption Extension（收费）\nSQLiteEncrypt（收费）\nSQLiteCrypt（收费）\nSQLCipher（开源免费）\n只有SQLCipher是免费的，所以本文主要对SQLCipher进行研究。\n2、引入SQLCipher第三方库 1.手动引入\n请参考官方教程。\n2.使用CocoaPods\npod 'FMDB/SQLCipher', '2.5'\n3、SQLCipher的可行性研究 1.新建加密数据库 若没有旧数据的情况下使用很简单，只需要在FMDatabase里面的-open和-openWithFlags:方法里面添加[self setKey:kDatabaseEncryptKey];即可。 如下图所示\n但是在团队协作中，如果直接修改pod仓库里面的文件，可能不好同步，下面有一个技巧，就是继承FMDatabase和FMDatabaseQueue并重载其中的方法。\n继承FMDatabase的子类需要重载以下方法\n- (BOOL)open { if (_db) { return YES; } int err = sqlite3_open([self sqlitePath], \u0026amp;_db ); if(err != SQLITE_OK) { NSLog(@\u0026#34;error opening!: %d\u0026#34;, err); return NO; } else { // 设置密钥 [self setKey:kDatabaseEncryptKey]; } if (_maxBusyRetryTimeInterval \u0026gt; 0.0) { // set the handler [self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval]; } return YES; } - (BOOL)openWithFlags:(int)flags { if (_db) { return YES; } int err = sqlite3_open_v2([self sqlitePath], \u0026amp;_db, flags, NULL /* Name of VFS module to use */); if(err != SQLITE_OK) { NSLog(@\u0026#34;error opening!: %d\u0026#34;, err); return NO; } else { // 设置密钥 [self setKey:kDatabaseEncryptKey]; } if (_maxBusyRetryTimeInterval \u0026gt; 0.0) { // set the handler [self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval]; } return YES; } - (const char*)sqlitePath { if (!_databasePath) { return \u0026#34;:memory:\u0026#34;; } if ([_databasePath length] == 0) { return \u0026#34;\u0026#34;; // this creates a temporary database (it\u0026#39;s an sqlite thing). } return [_databasePath fileSystemRepresentation]; } 继承FMDatabaseQueue的子类只需要重载一个方法\n+ (Class)databaseClass { return [FMEncryptDatabase class]; } 使用的时候只需要更改下面的语句\n// 原： self.normalQueue = [FMDatabaseQueue databaseQueueWithPath:self.normalDbPath]; // 改为： self.encryptQueue = [FMEncryptDatabaseQueue databaseQueueWithPath:self.encryptDbPath]; 2.加密已有数据库 加密已有数据库我已经封装成下面的方法：\n/** 加密数据库（保留原有数据库） */ + (BOOL)encryptDatabase:(NSString *)origPath toPath:(NSString *)toPath { sqlite3 *db; if (sqlite3_open([origPath UTF8String], \u0026amp;db) == SQLITE_OK) { char *err = NULL; sqlite3_exec(db, [[NSString stringWithFormat:@\u0026#34;ATTACH DATABASE \u0026#39;%@\u0026#39; AS encrypted KEY \u0026#39;%@\u0026#39;;\u0026#34;, toPath, kDatabaseEncryptKey] UTF8String], NULL, NULL, \u0026amp;err); sqlite3_exec(db, \u0026#34;SELECT sqlcipher_export(\u0026#39;encrypted\u0026#39;);\u0026#34;, NULL, NULL, \u0026amp;err); sqlite3_exec(db, \u0026#34;DETACH DATABASE encrypted;\u0026#34;, NULL, NULL, \u0026amp;err); sqlite3_close(db); return err ? NO : YES; } else { sqlite3_close(db); NSLog(@\u0026#34;Open db failed:%s\u0026#34;, sqlite3_errmsg(db)); return NO; } } 这里有两点需要注意的地方：\n1、这里必须使用全路径\n2、加密后的queue必须同步更换为FMEncryptDatabaseQueue，不然无法读写数据\n3.解密已加密数据库 解密已加密的数据库已封装为以下方法：\n/** 解密数据库（保留原有数据库） */ + (BOOL)unencryptDatabase:(NSString *)origPath toPath:(NSString *)toPath { sqlite3 *db; if (sqlite3_open([origPath UTF8String], \u0026amp;db) == SQLITE_OK) { char *err = NULL; sqlite3_exec(db, [[NSString stringWithFormat:@\u0026#34;PRAGMA key = \u0026#39;%@\u0026#39;;\u0026#34;, kDatabaseEncryptKey] UTF8String], NULL, NULL, \u0026amp;err); sqlite3_exec(db, [[NSString stringWithFormat:@\u0026#34;ATTACH DATABASE \u0026#39;%@\u0026#39; AS plaintext KEY \u0026#39;\u0026#39;;\u0026#34;, toPath] UTF8String], NULL, NULL, NULL); sqlite3_exec(db, \u0026#34;SELECT sqlcipher_export(\u0026#39;plaintext\u0026#39;);\u0026#34;, NULL, NULL, \u0026amp;err); sqlite3_exec(db, \u0026#34;DETACH DATABASE plaintext;\u0026#34;, NULL, NULL, \u0026amp;err); sqlite3_close(db); return err ? NO : YES; } else { sqlite3_close(db); NSLog(@\u0026#34;Open db failed:%s\u0026#34;, sqlite3_errmsg(db)); return NO; } } 这里也有两点需要注意的地方：\n1、这里必须使用全路径\n2、解密后的queue必须同步更换为FMDatabaseQueue，不然无法读写数据\n4、SQLCipher性能分析 本次性能分析主要从两个维度下手： 1.对比无加密数据库和加密数据库的插入数据速度； 2.加密已有数据的数据库耗时。\n本次测试都基于iOS10.3的iPad mini2下进行。\n1.对比插入数据速度 规则：对比无加密数据库和加密数据库使用事务时，在插入1w条、5w条和10w条8个字段的数据的耗时，每组数据测试5次，最后取平均值。最后的测试结果如下图所示：\n结论：两者相差在4%左右，在移动设备上存储大数据的情况很少，所以我认为是完全可以接受的。\n2.测试加密数据库耗时 规则：对比存储1w条、5w条和10w条数据的数据库加密的耗时，每组数据测试5次，最后取平均值。最后的测试结果如下图所示：\n结论：加密过程需要一定的时间，且跟数据库所存储的数据量有关，数据量越大耗时越长，从本次的测试结果来看，加密存储10w条数据的数据库耗时1.38s是可以接受的。\n5、采用SQLCipher存在的风险 1、SQLCipher依赖本地存储的字符串进行数据库的加密和解密，如果这个字符串被泄漏出去，本地的数据库依然容易被解密，进而被读取里面的信息。可采用KeyChain保存？ 2、加密已存储大数据的数据库会消耗一定的时间，这段时间怎么处理才能让用户无感知？加密失败了怎么处理？ 3、欢迎大家补充。。。\n6、本文demo ","permalink":"https://blog.axqora.com/posts/ios-security/","summary":"\u003ch2 id=\"一主流保证数据安全的方式\"\u003e一、主流保证数据安全的方式\u003c/h2\u003e\n\u003ch3 id=\"1网络传输安全\"\u003e1、网络传输安全\u003c/h3\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e1.采用HTTPS通信协议\u003c/strong\u003e\u003c/em\u003e\u003cbr\u003e\n可防止抓包窃取、篡改传输数据，大大增加了中间人攻击的成本。\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e2.对敏感数据进行签名校正\u003c/strong\u003e\u003c/em\u003e\u003cbr\u003e\n采用非对称加密的方式，对敏感数据使用密钥加密，到了客户端用公钥解密，验证数据一致性，防止通信过程中被篡改。\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e3.采用密文传输\u003c/strong\u003e\u003c/em\u003e\u003cbr\u003e\n别人即使截取了传输信息，也无法看懂其中的意思。\u003c/p\u003e\n\u003ch3 id=\"2本地存储数据安全\"\u003e2、本地存储数据安全\u003c/h3\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e1.数据库加密\u003c/strong\u003e\u003c/em\u003e\u003cbr\u003e\n通过越狱设备可以很容易地把整个应用包（包括里面的数据）copy出来，这样就可以获取里面的数据库，对于没有加密的数据库就可以非常轻易地读取里面的信息，造成信息的泄漏。\n\u003cem\u003e\u003cstrong\u003e数据库加密可分两个维度：1.整个数据库加密；2.对部分字段先加密再存数据库。\u003c/strong\u003e\u003c/em\u003e\n\u003cem\u003e\u003cstrong\u003e对部分字段加密并不适合多字段的加密存储，容易导致加密数据太过分散，影响性能。所以推荐对整个数据库加密。\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e2.KeyChain存储敏感数据\u003c/strong\u003e\u003c/em\u003e\u003cbr\u003e\nKeyChain是iOS系统级的存储方式，安全性无需质疑，且删除应用或者升级系统依然可以保留里面的信息。\u003c/p\u003e\n\u003ch3 id=\"3源码安全\"\u003e3、源码安全\u003c/h3\u003e\n\u003cp\u003e使用越狱设备可以很轻易地把应用砸壳，从而把源码dump下来，即使是没有太多经验的开发者也可以得到应用的类信息，包括函数名等。使用IDA等反编译工具可以看到应用的一些类名和方法名，进而可以分析功能实现的逻辑。\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e1.字符串混淆\u003c/strong\u003e\u003c/em\u003e\u003cbr\u003e\n对应用程序中使用到的字符串进行加密，保证源码被逆向后也能保护明文字符串。\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e2.类名、方法名混淆\u003c/strong\u003e\u003c/em\u003e\u003cbr\u003e\n市面上很多iOS应用都没有混淆类名方法名，以致于很容易使用class-dump下来，从而进行hook操作，\u003ca href=\"http://www.jianshu.com/p/189afbe3b429\"\u003e一步一步实现iOS微信自动抢红包(非越狱)\u003c/a\u003e是很有趣的一个应用。\u003ca href=\"https://github.com/Polidea/ios-class-guard\"\u003e这个库\u003c/a\u003e可以混淆OC的类名、协议、属性还有方法名。\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e3.程序结构混淆加密\u003c/strong\u003e\u003c/em\u003e\u003cbr\u003e\n对应用程序逻辑结构进行打乱混排，保证源码可读性降到最低。\u003ca href=\"https://github.com/obfuscator-llvm/obfuscator/\"\u003e可参考这个库\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e4.反调试、反注入等一些主动保护策略\u003c/strong\u003e\u003c/em\u003e\u003cbr\u003e\n加入第三方安全性SDK\u003c/p\u003e\n\u003ch2 id=\"二对数据库加密的研究\"\u003e二、对数据库加密的研究\u003c/h2\u003e\n\u003ch3 id=\"1主流数据库加密方式\"\u003e1、主流数据库加密方式\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003eThe SQLite Encryption Extension（收费）\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003eSQLiteEncrypt（收费）\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003eSQLiteCrypt（收费）\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003eSQLCipher（开源免费）\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e只有SQLCipher是免费的，所以本文主要对SQLCipher进行研究。\u003c/p\u003e\n\u003ch3 id=\"2引入sqlcipher第三方库\"\u003e2、引入SQLCipher第三方库\u003c/h3\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e1.手动引入\u003c/strong\u003e\u003c/em\u003e\u003cbr\u003e\n请参考\u003ca href=\"https://www.zetetic.net/sqlcipher/ios-tutorial/\"\u003e官方教程\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e2.使用CocoaPods\u003c/strong\u003e\u003c/em\u003e\u003cbr\u003e\n\u003ccode\u003epod 'FMDB/SQLCipher', '2.5'\u003c/code\u003e\u003c/p\u003e\n\u003ch3 id=\"3sqlcipher的可行性研究\"\u003e3、SQLCipher的可行性研究\u003c/h3\u003e\n\u003ch4 id=\"1新建加密数据库\"\u003e1.新建加密数据库\u003c/h4\u003e\n\u003cp\u003e若没有旧数据的情况下使用很简单，只需要在FMDatabase里面的\u003ccode\u003e-open\u003c/code\u003e和\u003ccode\u003e-openWithFlags:\u003c/code\u003e方法里面添加\u003ccode\u003e[self setKey:kDatabaseEncryptKey];\u003c/code\u003e即可。\n如下图所示\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"添加密钥\" loading=\"lazy\" src=\"/image/iOS-Security/ios_security_1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e但是在团队协作中，如果直接修改pod仓库里面的文件，可能不好同步，下面有一个技巧，就是继承FMDatabase和FMDatabaseQueue并重载其中的方法。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e继承FMDatabase的子类需要重载以下方法\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e- (BOOL)open {\n    if (_db) {\n        return YES;\n    }\n    int err = sqlite3_open([self sqlitePath], \u0026amp;_db );\n    if(err != SQLITE_OK) {\n        NSLog(@\u0026#34;error opening!: %d\u0026#34;, err);\n        return NO;\n    } else {\n        // 设置密钥\n        [self setKey:kDatabaseEncryptKey];\n    }\n    if (_maxBusyRetryTimeInterval \u0026gt; 0.0) {\n        // set the handler\n        [self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];\n    }\n    return YES;\n}\n\n- (BOOL)openWithFlags:(int)flags {\n    if (_db) {\n        return YES;\n    }\n    int err = sqlite3_open_v2([self sqlitePath], \u0026amp;_db, flags, NULL /* Name of VFS module to use */);\n    if(err != SQLITE_OK) {\n        NSLog(@\u0026#34;error opening!: %d\u0026#34;, err);\n        return NO;\n    } else {\n        // 设置密钥\n        [self setKey:kDatabaseEncryptKey];\n    }\n    if (_maxBusyRetryTimeInterval \u0026gt; 0.0) {\n        // set the handler\n        [self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];\n    }\n    return YES;\n}\n\n- (const char*)sqlitePath {\n    if (!_databasePath) {\n        return \u0026#34;:memory:\u0026#34;;\n    }\n    if ([_databasePath length] == 0) {\n        return \u0026#34;\u0026#34;; // this creates a temporary database (it\u0026#39;s an sqlite thing).\n    }\n    return [_databasePath fileSystemRepresentation];\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e继承FMDatabaseQueue的子类只需要重载一个方法\u003c/p\u003e","title":"对 iOS 数据安全的一次小探索"},{"content":"\n一、前言 偶尔会好怀念小时候跟朋友一起玩小霸王的日子，那时候很少家里有游戏机的人，一台游戏机有好几个朋友在等着玩，那时候能玩的东西不多，但日子过得简单而开心。虽说现在万能的淘宝还可以买到小霸王游戏机，但是远远不如自己做一个出来的成就感来的强。下面就一步一步把小时候的游戏机做出来。\n二、准备 1、树莓派+SD卡\n2、显示器、游戏手柄、键盘等外设\n三、下载RetroPie镜像 RetroPie是一个开源的游戏模拟器，可以让你的树莓派或者PC变成一个复古游戏机，它集成了任天堂红白机、GameBoy、SNES等著名游戏系统，具有很高的可玩性。\n你可以在Raspbian直接安装RetroPie，但是会耗费很长的时间，所以我在这里选择直接下载RetroPie镜像。\n下载地址：https://retropie.org.uk/download/\n四、往SD卡写入RetroPie镜像 PC上可以使用 Win32DiskImager 工具写入镜像；\nMac系统下可以用 ApplePi-Baker 工具；\n具体写入方法及下载地址网上都很容易找到，在这里就不再赘述。\n把已经写入RetroPie镜像的SD卡插入树莓派并连接电源。\n五、配置 1.设置输入设备 我们的游戏手柄有时候并没有那么多个键位，我们在设置的时候怎么跳过一个键位的设置呢？这里有个小技巧：长按“下”键即可跳过。\n设置游戏手柄键位\n2.连接WiFi 设置好键位后，我们就可以进入主界面，上面有“13 GAMES AVAILABLE”字样，这并不是意味着里面就有13个游戏可以马上玩的，应该理解为内置了13个游戏模拟器，这时候我们怎么样才能开始玩呢？\n初始主界面\n按“A”键，进入设置界面，点击WIFI，连接你电脑同一个的局域网。\n连接局域网\n3.传输roms 打开PC，点击“计算机”上面的地址栏，输入“\\retropie\\roms”，即可进入游戏roms文件夹，把下载好的游戏rom拉到对应的模拟器文件夹，如.nes后缀的游戏rom拉进对应的nes文件夹。\n模拟器文件夹\n4.重启模拟器 重启模拟器\n重启模拟器后就可以看到主界面多了一个选项，选择游戏就可以开始你的怀旧之旅了！\n多了一个选项\n超级马里奥\n六、其他配置 有人会发现自己的SD卡的内存明明很大，但是没有塞多少个游戏内存就满了，可以进行下面的配置“扩展”内存：\n进入RetroPie的设置界面，进入RASPI-CONFIG\nRASPI-CONFIG\n选择第一项Expand Filesystem，重启树莓派即可扩展内存了。\n另外，你还可以在这里进行修改登录密码、国际化选项、设置超频等操作。\n树莓派设置界面\n七、祝你玩得开心😊 ","permalink":"https://blog.axqora.com/posts/raspberry-restropie/","summary":"\u003cp\u003e\u003cimg alt=\"RetroPie\" loading=\"lazy\" src=\"/image/Raspberry-RetroPie/raspberry_retropie_1.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"一前言\"\u003e一、前言\u003c/h1\u003e\n\u003cp\u003e偶尔会好怀念小时候跟朋友一起玩小霸王的日子，那时候很少家里有游戏机的人，一台游戏机有好几个朋友在等着玩，那时候能玩的东西不多，但日子过得简单而开心。虽说现在万能的淘宝还可以买到小霸王游戏机，但是远远不如自己做一个出来的成就感来的强。下面就一步一步把小时候的游戏机做出来。\u003c/p\u003e\n\u003ch1 id=\"二准备\"\u003e二、准备\u003c/h1\u003e\n\u003cp\u003e1、树莓派+SD卡\u003c/p\u003e\n\u003cp\u003e2、显示器、游戏手柄、键盘等外设\u003c/p\u003e\n\u003ch1 id=\"三下载retropie镜像\"\u003e三、下载RetroPie镜像\u003c/h1\u003e\n\u003cp\u003eRetroPie是一个开源的游戏模拟器，可以让你的树莓派或者PC变成一个复古游戏机，它集成了任天堂红白机、GameBoy、SNES等著名游戏系统，具有很高的可玩性。\u003c/p\u003e\n\u003cp\u003e你可以在Raspbian直接安装RetroPie，但是会耗费很长的时间，所以我在这里选择直接下载RetroPie镜像。\u003c/p\u003e\n\u003cp\u003e下载地址：https://retropie.org.uk/download/\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/Raspberry-RetroPie/raspberry_retropie_2.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"四往sd卡写入retropie镜像\"\u003e四、往SD卡写入RetroPie镜像\u003c/h1\u003e\n\u003cp\u003ePC上可以使用 Win32DiskImager 工具写入镜像；\u003c/p\u003e\n\u003cp\u003eMac系统下可以用 ApplePi-Baker 工具；\u003c/p\u003e\n\u003cp\u003e具体写入方法及下载地址网上都很容易找到，在这里就不再赘述。\u003c/p\u003e\n\u003cp\u003e把已经写入RetroPie镜像的SD卡插入树莓派并连接电源。\u003c/p\u003e\n\u003ch1 id=\"五配置\"\u003e五、配置\u003c/h1\u003e\n\u003ch2 id=\"1设置输入设备\"\u003e1.设置输入设备\u003c/h2\u003e\n\u003cp\u003e我们的游戏手柄有时候并没有那么多个键位，我们在设置的时候怎么跳过一个键位的设置呢？这里有个小技巧：长按“下”键即可跳过。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/Raspberry-RetroPie/raspberry_retropie_3.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e设置游戏手柄键位\u003c/p\u003e\n\u003ch2 id=\"2连接wifi\"\u003e2.连接WiFi\u003c/h2\u003e\n\u003cp\u003e设置好键位后，我们就可以进入主界面，上面有“13 GAMES AVAILABLE”字样，这并不是意味着里面就有13个游戏可以马上玩的，应该理解为内置了13个游戏模拟器，这时候我们怎么样才能开始玩呢？\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/Raspberry-RetroPie/raspberry_retropie_4.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e初始主界面\u003c/p\u003e\n\u003cp\u003e按“A”键，进入设置界面，点击WIFI，连接你电脑同一个的局域网。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/Raspberry-RetroPie/raspberry_retropie_5.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e连接局域网\u003c/p\u003e\n\u003ch2 id=\"3传输roms\"\u003e3.传输roms\u003c/h2\u003e\n\u003cp\u003e打开PC，点击“计算机”上面的地址栏，输入“\\retropie\\roms”，即可进入游戏roms文件夹，把下载好的游戏rom拉到对应的模拟器文件夹，如.nes后缀的游戏rom拉进对应的nes文件夹。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/Raspberry-RetroPie/raspberry_retropie_6.png\"\u003e\u003c/p\u003e\n\u003cp\u003e模拟器文件夹\u003c/p\u003e\n\u003ch2 id=\"4重启模拟器\"\u003e4.重启模拟器\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/Raspberry-RetroPie/raspberry_retropie_7.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e重启模拟器\u003c/p\u003e\n\u003cp\u003e重启模拟器后就可以看到主界面多了一个选项，选择游戏就可以开始你的怀旧之旅了！\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/Raspberry-RetroPie/raspberry_retropie_8.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e多了一个选项\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/Raspberry-RetroPie/raspberry_retropie_9.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e超级马里奥\u003c/p\u003e\n\u003ch1 id=\"六其他配置\"\u003e六、其他配置\u003c/h1\u003e\n\u003cp\u003e有人会发现自己的SD卡的内存明明很大，但是没有塞多少个游戏内存就满了，可以进行下面的配置“扩展”内存：\u003c/p\u003e\n\u003cp\u003e进入RetroPie的设置界面，进入RASPI-CONFIG\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/Raspberry-RetroPie/raspberry_retropie_10.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003eRASPI-CONFIG\u003c/p\u003e\n\u003cp\u003e选择第一项Expand Filesystem，重启树莓派即可扩展内存了。\u003c/p\u003e\n\u003cp\u003e另外，你还可以在这里进行修改登录密码、国际化选项、设置超频等操作。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img\" loading=\"lazy\" src=\"/image/Raspberry-RetroPie/raspberry_retropie_11.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e树莓派设置界面\u003c/p\u003e\n\u003ch1 id=\"七祝你玩得开心\"\u003e七、祝你玩得开心😊\u003c/h1\u003e","title":"利用树莓派 + RetroPie 做一个怀旧游戏机"},{"content":"参考：https://i.cmgine.net/archives/14896.html 入门： 1、用ARC管理内存 2、在正确的地方使用reuseIdentifier 3、尽量吧views设置为完全不透明 4、避免过于庞大的xib 5、不要阻塞主线程 6、不要在ImageView中调整图片大小，尽量使用UIImageView大小相同的图片 7、选择正确的Collection Arrays: 有序的一组值。使用index来lookup很快，使用value lookup很慢， 插入/删除很慢。\nDictionaries: 存储键值对。 用键来查找比较快。\nSets: 无序的一组值。用值来查找很快，插入/删除很快。\n8、打开gzip压缩 中级： 1、重用和懒加载views 2、Cache，缓存不大可能改变但是需要经常读取的东西（NSCache） 3、权衡渲染方法（View？CALayer？CoreGraphics？OpenGL？） 4、处理内存警告 在app delegate中使用applicationDidReceiveMemoryWarning:的方法\n在你的自定义UIViewController的子类(subclass)中覆盖didReceiveMemoryWarning\n注册并接收 UIApplicationDidReceiveMemoryWarningNotification 的通知\n5、重用大开销对象（如NSDateFormatter，NSCalendar） 6、使用Sprite Sheets 7、避免反复处理数据 解析JSON会比XML更快一些，JSON也通常更小更便于传输。从iOS5起有了官方内建的JSON deserialization 就更加方便使用了。\n但是XML也有XML的好处，比如使用SAX 来解析XML就像解析本地文件一样，你不需像解析json一样等到整个文档下载完成才开始解析。当你处理很大的数据的时候就会极大地减低内存消耗和增加性能。\n8、正确设定背景图片 使用UIColor的 colorWithPatternImage来设置背景色；\n在view中添加一个UIImageView作为一个子View。（不使用“imageName:”）\n9、减少使用Web特性 10、设定Shadow Path view.layer.shadowPath = [[UIBezierPath bezierPathWithRect:view.bounds] CGPath];\n11、优化UITableView 正确使用reuseIdentifier来重用cells\n尽量使所有的view opaque，包括cell自身\n避免渐变，图片缩放，后台选人\n缓存行高\n如果cell内现实的内容来自web，使用异步加载，缓存请求结果\n使用shadowPath来画阴影\n减少subviews的数量\n尽量不适用cellForRowAtIndexPath:，如果你需要用到它，只用一次然后缓存结果\n使用正确的数据结构来存储数据\n尽量使用rowHeight,sectionFooterHeight和sectionHeaderHeight来设定固定的高，不要请求delegate\n12、选择正确的数据存储选项 使用NSUerDefaults\n使用XML, JSON, 或者 plist\n使用NSCoding存档\n使用类似SQLite的本地SQL数据库\n使用 Core Data\n进阶： 1、加速启动时间 2、使用Autorelease Pool 3、选择是否缓存图片 4、避免日期格式转换 ","permalink":"https://blog.axqora.com/posts/ios-performance-optimization/","summary":"\u003ch4 id=\"参考httpsicmginenetarchives14896html\"\u003e参考：https://i.cmgine.net/archives/14896.html\u003c/h4\u003e\n\u003ch1 id=\"入门\"\u003e入门：\u003c/h1\u003e\n\u003ch4 id=\"1用arc管理内存\"\u003e1、用ARC管理内存\u003c/h4\u003e\n\u003ch4 id=\"2在正确的地方使用reuseidentifier\"\u003e2、在正确的地方使用reuseIdentifier\u003c/h4\u003e\n\u003ch4 id=\"3尽量吧views设置为完全不透明\"\u003e3、尽量吧views设置为完全不透明\u003c/h4\u003e\n\u003ch4 id=\"4避免过于庞大的xib\"\u003e4、避免过于庞大的xib\u003c/h4\u003e\n\u003ch4 id=\"5不要阻塞主线程\"\u003e5、不要阻塞主线程\u003c/h4\u003e\n\u003ch4 id=\"6不要在imageview中调整图片大小尽量使用uiimageview大小相同的图片\"\u003e6、不要在ImageView中调整图片大小，尽量使用UIImageView大小相同的图片\u003c/h4\u003e\n\u003ch4 id=\"7选择正确的collection\"\u003e7、选择正确的Collection\u003c/h4\u003e\n\u003cp\u003eArrays: 有序的一组值。使用index来lookup很快，使用value lookup很慢， 插入/删除很慢。\u003c/p\u003e\n\u003cp\u003eDictionaries: 存储键值对。 用键来查找比较快。\u003c/p\u003e\n\u003cp\u003eSets: 无序的一组值。用值来查找很快，插入/删除很快。\u003c/p\u003e\n\u003ch4 id=\"8打开gzip压缩\"\u003e8、打开gzip压缩\u003c/h4\u003e\n\u003ch1 id=\"中级\"\u003e中级：\u003c/h1\u003e\n\u003ch4 id=\"1重用和懒加载views\"\u003e1、重用和懒加载views\u003c/h4\u003e\n\u003ch4 id=\"2cache缓存不大可能改变但是需要经常读取的东西nscache\"\u003e2、Cache，缓存不大可能改变但是需要经常读取的东西（NSCache）\u003c/h4\u003e\n\u003ch4 id=\"3权衡渲染方法viewcalayercoregraphicsopengl\"\u003e3、权衡渲染方法（View？CALayer？CoreGraphics？OpenGL？）\u003c/h4\u003e\n\u003ch4 id=\"4处理内存警告\"\u003e4、处理内存警告\u003c/h4\u003e\n\u003cp\u003e在app delegate中使用applicationDidReceiveMemoryWarning:的方法\u003c/p\u003e\n\u003cp\u003e在你的自定义UIViewController的子类(subclass)中覆盖didReceiveMemoryWarning\u003c/p\u003e\n\u003cp\u003e注册并接收 UIApplicationDidReceiveMemoryWarningNotification 的通知\u003c/p\u003e\n\u003ch4 id=\"5重用大开销对象如nsdateformatternscalendar\"\u003e5、重用大开销对象（如NSDateFormatter，NSCalendar）\u003c/h4\u003e\n\u003ch4 id=\"6使用sprite-sheets\"\u003e6、使用Sprite Sheets\u003c/h4\u003e\n\u003ch4 id=\"7避免反复处理数据\"\u003e7、避免反复处理数据\u003c/h4\u003e\n\u003cp\u003e解析JSON会比XML更快一些，JSON也通常更小更便于传输。从iOS5起有了官方内建的JSON deserialization 就更加方便使用了。\u003c/p\u003e\n\u003cp\u003e但是XML也有XML的好处，比如使用SAX 来解析XML就像解析本地文件一样，你不需像解析json一样等到整个文档下载完成才开始解析。当你处理很大的数据的时候就会极大地减低内存消耗和增加性能。\u003c/p\u003e\n\u003ch4 id=\"8正确设定背景图片\"\u003e8、正确设定背景图片\u003c/h4\u003e\n\u003cp\u003e使用UIColor的 colorWithPatternImage来设置背景色；\u003c/p\u003e\n\u003cp\u003e在view中添加一个UIImageView作为一个子View。（不使用“imageName:”）\u003c/p\u003e\n\u003ch4 id=\"9减少使用web特性\"\u003e9、减少使用Web特性\u003c/h4\u003e\n\u003ch4 id=\"10设定shadow-path\"\u003e10、设定Shadow Path\u003c/h4\u003e\n\u003cp\u003eview.layer.shadowPath = [[UIBezierPath bezierPathWithRect:view.bounds] CGPath];\u003c/p\u003e\n\u003ch4 id=\"11优化uitableview\"\u003e11、优化UITableView\u003c/h4\u003e\n\u003cp\u003e正确使用reuseIdentifier来重用cells\u003c/p\u003e\n\u003cp\u003e尽量使所有的view opaque，包括cell自身\u003c/p\u003e\n\u003cp\u003e避免渐变，图片缩放，后台选人\u003c/p\u003e\n\u003cp\u003e缓存行高\u003c/p\u003e\n\u003cp\u003e如果cell内现实的内容来自web，使用异步加载，缓存请求结果\u003c/p\u003e\n\u003cp\u003e使用shadowPath来画阴影\u003c/p\u003e\n\u003cp\u003e减少subviews的数量\u003c/p\u003e\n\u003cp\u003e尽量不适用cellForRowAtIndexPath:，如果你需要用到它，只用一次然后缓存结果\u003c/p\u003e\n\u003cp\u003e使用正确的数据结构来存储数据\u003c/p\u003e\n\u003cp\u003e尽量使用rowHeight,sectionFooterHeight和sectionHeaderHeight来设定固定的高，不要请求delegate\u003c/p\u003e\n\u003ch4 id=\"12选择正确的数据存储选项\"\u003e12、选择正确的数据存储选项\u003c/h4\u003e\n\u003cp\u003e使用NSUerDefaults\u003c/p\u003e\n\u003cp\u003e使用XML, JSON, 或者 plist\u003c/p\u003e\n\u003cp\u003e使用NSCoding存档\u003c/p\u003e","title":"iOS 性能优化汇总"},{"content":"当你设计你的应用程序时，你可能需要动态的响应事件。例如，一个触摸事件可以由屏幕上的很多对象产生，你必须决定由哪个对象响应对应的事件和明白该对象如何接收到事件的。\n当用户产生了一个事件，UIKit创建了一个包含处理事件所需信息的事件对象。然后把这个事件对象放置在全局事件队列。对于触摸事件，会被包装成一个UIEvent对象。对于动作事件，会根据你所使用的框架和你感兴趣的动作事件而有所不同。\n一个事件沿着一个具体的路径向前传递，直到有一个对象可以对其进行处理。首先，\nUIApplication单例对象会从事件队列里获取一个事件并把它分发，一贯地将事件发送到应用的key window对象，key window对象会把事件传递到原始对象进行处理，原始对象会根据事件的类型而有所不同。\n· 触摸事件。对于触摸事件，window对象首先会尝试传递事件到产生触摸事件的视图。这个视图被称为hit-test视图，寻找hit-test视图的过程被称为hit-testing。\n· 动作和远程控制事件。window对象会把摇一摇动作或者远程控制事件发送到第一响应者进行处理。\n这些事件路径的最终目标是寻找一个对象可以处理和相应一个事件。因此，UIKit首先会事件发送到最适合处理该事件的对象。最适合处理触摸事件的对象就是hit-test视图，对于其他事件，对象就是第一响应者。接下来的部分会详细展开hit-test视图和第一响应者对象是如何被确认的。\nHit-Testing返回触摸事件发生的视图\niOS利用hit-tesing寻找被触摸的视图。hit-tesing需要检查一个触摸事件是否在相关的视图对象的范围内，如果是，它会递归地检查这个视图对象的所有子视图。处于视图层级中包含触摸点的最底层视图就会成为hit-test视图。当iOS确定hit-test视图后，它会把触摸事件抛给这个视图去处理。\n例如，假如用户触摸视图E（图2-1）。iOS通过以下顺序从子视图中找出hit-test视图\n1.触摸点在视图A里面，所以会检查子视图B和C。\n2.触摸点不在视图B的范围内，但是在视图C的范围内，所以它会检查子视图D和E。\n3.触摸点不在视图D，但在视图E的范围内。\n视图E是在视图层级中包含触摸点的最底层视图，所以它就成为了hit-test视图\n（图2-1）\n（hitTest:withEvent:）方法返回一个给定CGPoint和UIEvent的hit-test视图。（hitTest:withEvent:）方法通过（pointInside:withEvent:）方法调用。如果一个点通过（hitTest:withEvent:）方法判断是在视图的范围内，（pointInside:withEvent:）方法就会返回YES。然后这个方法就会在返回YES的子视图上面递归地调用（hitTest:withEvent:）方法。\n如果这个点通过（hitTest:withEvent:）方法判断不在视图的范围内，最先调用的（pointInside:withEvent:）方法就会返回NO。这个点被忽略了，（hitTest:withEvent:）方法就会返回nil。如果一个子视图返回NO，那么在视图层级中整个分支都会被忽略，因为如果这个触摸点不在这个视图中产生，那么也不会再这个视图的子视图中产生。这就意味着一些在父视图外的子视图的点不能接收到触摸事件，因为触摸点必须在父视图和子视图的范围内。子视图的范围超出父视图的情况会发生在子视图的clipsToBounds属性被设为NO。\n注意：一个触摸对象的生命周期会跟它的hit-test密切关联，即使后来触摸移到view的外面。\nhit-test视图有第一次机会去处理一个触摸事件。如果hit-test视图不能处理一个事件，这个事件就会在视图的响应者链条上传递，直到系统找到一个可以找到该事件的对象。\n响应者链条由响应者对象组成\n很多类型的事件依赖一个响应者链条进行传递。响应者链条是一系列的响应者对象。由第一个响应者开始，并由application对象结束。如果第一响应者不能处理一个事件，事件会向前传递给响应者链条的下一个响应者。\n响应者对象是一个可以处理事件的对象。UIResponder类是所有响应者对象的基类，它定义的程序接口不只是为了事件的处理，而且为了普通的响应者行为。UIApplication,UIViewController和UIView的实例都可以成为响应者，意味着所有的视图和很多的关键控制器对象都可以是响应者。注意：核心动画图层都不可以成为响应者。\n首先由第一响应者接收事件。一般的，第一项响应者是一个视图对象。一个对象通过以下两种方式成为第一响应者：\n1、复写（canBecomeFirstResponder）方法并返回YES。\n2、接收一个（becomFirstResponder）信息。如果有必要，一个对象可以发送这条信息给它自己。\n注意：一个对象成为第一响应者前确保应用已经把这个对象的图层渲染出来。例如，我们一般在（viewDidAppear:）方法里面调用（becomeFirstResponder）方法，如果尝试在（viewWillAppear:）方法里面分配第一响应者，对象的图层还没有渲染出来，所以（becomeFirstResponder）方法就会返回NO。\n事件不是唯一的依赖响应者链条的对象。响应者链条被用于以下的情况：\n· 触摸事件。如果hit-test视图不能处理一个触摸事件，那么这个事件就会从hit-test视图开始沿着响应者链条传递。\n· 动作事件。通过UIKit来处理摇一摇动作事件，第一响应者必须实现UIResponder类的（motionBegan:withEvent:）方法或者（motionEnded:withEvent:）方法之一。\n· 远程控制事件。第一响应者处理远程控制事件必须实现UIResponder类的（remoteControlReceivedWithEvent:）方法。\n· 动作信息。\n","permalink":"https://blog.axqora.com/posts/ios-response-chain/","summary":"\u003cp\u003e当你设计你的应用程序时，你可能需要动态的响应事件。例如，一个触摸事件可以由屏幕上的很多对象产生，你必须决定由哪个对象响应对应的事件和明白该对象如何接收到事件的。\u003c/p\u003e\n\u003cp\u003e当用户产生了一个事件，UIKit创建了一个包含处理事件所需信息的事件对象。然后把这个事件对象放置在全局事件队列。对于触摸事件，会被包装成一个UIEvent对象。对于动作事件，会根据你所使用的框架和你感兴趣的动作事件而有所不同。\u003c/p\u003e\n\u003cp\u003e一个事件沿着一个具体的路径向前传递，直到有一个对象可以对其进行处理。首先，\u003c/p\u003e\n\u003cp\u003eUIApplication单例对象会从事件队列里获取一个事件并把它分发，一贯地将事件发送到应用的key window对象，key window对象会把事件传递到原始对象进行处理，原始对象会根据事件的类型而有所不同。\u003c/p\u003e\n\u003cp\u003e· 触摸事件。对于触摸事件，window对象首先会尝试传递事件到产生触摸事件的视图。这个视图被称为hit-test视图，寻找hit-test视图的过程被称为hit-testing。\u003c/p\u003e\n\u003cp\u003e· 动作和远程控制事件。window对象会把摇一摇动作或者远程控制事件发送到第一响应者进行处理。\u003c/p\u003e\n\u003cp\u003e这些事件路径的最终目标是寻找一个对象可以处理和相应一个事件。因此，UIKit首先会事件发送到最适合处理该事件的对象。最适合处理触摸事件的对象就是hit-test视图，对于其他事件，对象就是第一响应者。接下来的部分会详细展开hit-test视图和第一响应者对象是如何被确认的。\u003c/p\u003e\n\u003cp\u003eHit-Testing返回触摸事件发生的视图\u003c/p\u003e\n\u003cp\u003eiOS利用hit-tesing寻找被触摸的视图。hit-tesing需要检查一个触摸事件是否在相关的视图对象的范围内，如果是，它会递归地检查这个视图对象的所有子视图。处于视图层级中包含触摸点的最底层视图就会成为hit-test视图。当iOS确定hit-test视图后，它会把触摸事件抛给这个视图去处理。\u003c/p\u003e\n\u003cp\u003e例如，假如用户触摸视图E（图2-1）。iOS通过以下顺序从子视图中找出hit-test视图\u003c/p\u003e\n\u003cp\u003e1.触摸点在视图A里面，所以会检查子视图B和C。\u003c/p\u003e\n\u003cp\u003e2.触摸点不在视图B的范围内，但是在视图C的范围内，所以它会检查子视图D和E。\u003c/p\u003e\n\u003cp\u003e3.触摸点不在视图D，但在视图E的范围内。\u003c/p\u003e\n\u003cp\u003e视图E是在视图层级中包含触摸点的最底层视图，所以它就成为了hit-test视图\u003c/p\u003e\n\u003cp\u003e（图2-1）\u003c/p\u003e\n\u003cp\u003e（hitTest:withEvent:）方法返回一个给定CGPoint和UIEvent的hit-test视图。（hitTest:withEvent:）方法通过（pointInside:withEvent:）方法调用。如果一个点通过（hitTest:withEvent:）方法判断是在视图的范围内，（pointInside:withEvent:）方法就会返回YES。然后这个方法就会在返回YES的子视图上面递归地调用（hitTest:withEvent:）方法。\u003c/p\u003e\n\u003cp\u003e如果这个点通过（hitTest:withEvent:）方法判断不在视图的范围内，最先调用的（pointInside:withEvent:）方法就会返回NO。这个点被忽略了，（hitTest:withEvent:）方法就会返回nil。如果一个子视图返回NO，那么在视图层级中整个分支都会被忽略，因为如果这个触摸点不在这个视图中产生，那么也不会再这个视图的子视图中产生。这就意味着一些在父视图外的子视图的点不能接收到触摸事件，因为触摸点必须在父视图和子视图的范围内。子视图的范围超出父视图的情况会发生在子视图的clipsToBounds属性被设为NO。\u003c/p\u003e\n\u003cp\u003e注意：一个触摸对象的生命周期会跟它的hit-test密切关联，即使后来触摸移到view的外面。\u003c/p\u003e\n\u003cp\u003ehit-test视图有第一次机会去处理一个触摸事件。如果hit-test视图不能处理一个事件，这个事件就会在视图的响应者链条上传递，直到系统找到一个可以找到该事件的对象。\u003c/p\u003e\n\u003cp\u003e响应者链条由响应者对象组成\u003c/p\u003e\n\u003cp\u003e很多类型的事件依赖一个响应者链条进行传递。响应者链条是一系列的响应者对象。由第一个响应者开始，并由application对象结束。如果第一响应者不能处理一个事件，事件会向前传递给响应者链条的下一个响应者。\u003c/p\u003e\n\u003cp\u003e响应者对象是一个可以处理事件的对象。UIResponder类是所有响应者对象的基类，它定义的程序接口不只是为了事件的处理，而且为了普通的响应者行为。UIApplication,UIViewController和UIView的实例都可以成为响应者，意味着所有的视图和很多的关键控制器对象都可以是响应者。注意：核心动画图层都不可以成为响应者。\u003c/p\u003e\n\u003cp\u003e首先由第一响应者接收事件。一般的，第一项响应者是一个视图对象。一个对象通过以下两种方式成为第一响应者：\u003c/p\u003e\n\u003cp\u003e1、复写（canBecomeFirstResponder）方法并返回YES。\u003c/p\u003e\n\u003cp\u003e2、接收一个（becomFirstResponder）信息。如果有必要，一个对象可以发送这条信息给它自己。\u003c/p\u003e\n\u003cp\u003e注意：一个对象成为第一响应者前确保应用已经把这个对象的图层渲染出来。例如，我们一般在（viewDidAppear:）方法里面调用（becomeFirstResponder）方法，如果尝试在（viewWillAppear:）方法里面分配第一响应者，对象的图层还没有渲染出来，所以（becomeFirstResponder）方法就会返回NO。\u003c/p\u003e\n\u003cp\u003e事件不是唯一的依赖响应者链条的对象。响应者链条被用于以下的情况：\u003c/p\u003e\n\u003cp\u003e· 触摸事件。如果hit-test视图不能处理一个触摸事件，那么这个事件就会从hit-test视图开始沿着响应者链条传递。\u003c/p\u003e\n\u003cp\u003e· 动作事件。通过UIKit来处理摇一摇动作事件，第一响应者必须实现UIResponder类的（motionBegan:withEvent:）方法或者（motionEnded:withEvent:）方法之一。\u003c/p\u003e\n\u003cp\u003e· 远程控制事件。第一响应者处理远程控制事件必须实现UIResponder类的（remoteControlReceivedWithEvent:）方法。\u003c/p\u003e\n\u003cp\u003e· 动作信息。\u003c/p\u003e","title":"事件传递：响应者链条（译）"},{"content":"Axqora 在做一些极简、轻量、克制的数字产品。\nPrismark 轻量的 iPhone 专业相机 App：拍照、视频录制、双摄同录、相册回看。\n官网 · App Store\nLuxoria 照片与视频的 HDR 转换工具，把 SDR 素材转换为 HDR 输出，支持 HEVC HLG 视频导出。\n官网 · App Store\nMarkView 注重隐私的本地 Markdown 阅读器，适用于 iPhone 与 iPad：从「文件」或分享面板打开文档，离线阅读，无账号、不上传云端。\n官网 · App Store\nNiceDays 极简倒数日 App，支持 iPhone、Apple Watch 与小组件：公历农历、重复与区间事件、无账号 iCloud 同步。\n官网（App Store 上架中）\n更多信息见 axqora.com。\n","permalink":"https://blog.axqora.com/products/","summary":"Axqora 正在做的产品","title":"产品"}]