利用 git worktree 并行开发

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

May 10, 2020 · Darren Ou

树莓派简介

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

March 20, 2020 · Darren Ou

直播间横屏技术方案

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

January 4, 2020 · Darren Ou

iOS 项目接入 CallKit 框架

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

May 10, 2019 · Darren Ou

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

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

November 12, 2018 · Darren Ou

iOS 学习笔记

一、内存分配 结构体分配内存,内存对齐(8的倍数) 对象分配内存,内存对齐(16的倍数) 一个NSObject对象占用多少内存?(一个指针变量所占用的大小(64bit,8个字节,32bit,4个字节)) class_getInstanceSize() 至少需要多少内存 malloc_size() 实际分配了多少内存 二、对象 对象类型(实例对象、类对象、元类对象) 对象的isa指针指向哪里?(instance对象的isa指向class对象;class对象的isa指向meta-class对象;meta-class对象的isa指向基类的meta-class对象) OC的类信息存放在哪里?(对象方法、属性、成员变量、协议信息,存放在class对象中;类方法存放在meta-class对象中;成员变量的具体值存放在instance对象中) isa & ISA_MASK 才是真正的类对象或元类对象地址(arm64下是0x0000000ffffffff8ULL) 三、KVO、KVC KVO的本质(利用runtime动态生成一个子类,重写set方法、class、dealloc、isKVOA方法,并且让instance对象的isa指向这个全新的子类;当修改instance对象的属性时,会调用_NSSetXXXValueAndNotify函数;willChangeValueForKey/setter/didChangeValueForKey) 如何手动触发KVO(手动调用willChangeValueForKey/didChangeValueForKey,必须两个方法都调用) 直接修改成员变量会触发KVO吗(不会) KVC set原理(查找set方法:setKey -> _setKey;查看accessInstanceVariablesDirectly;查找成员变量:_key -> _isKey -> key -> isKey) KVC get原理(查找get方法:getKey -> key -> isKey -> _key;查看accessInstanceVariablesDirectly;查找成员变量:_key -> _isKey -> key -> 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; &key; / static const void *key = &key / @“key") 关联对象不是存储在被关联对象本身内存中,而是存储在全局的统一的一个AssociationManager中 AssciationsManager -> AssociationHashMap -> object: ObjectAssociationMap -> key: ObjectAssociation -> 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查找 -> 通过isa指针找到类对象并从class_rw_t里面的方法列表里面查找 -> 从superclass的cache里面查找 -> 从superclass的方法列表里面查找 -> resolveInstanceMathod 尝试动态方法解析一次 -> 重走一次查找方法流程 -> forwardingTargetForSelector 快速消息转发 -> forwardInvocation 完整消息转发(需要先获取方法签名 methodSignatureForSelector) NSMethodSignature 方法签名:返回值类型、参数类型 NSInvocation 封装了一个方法调用,包括:消息接受者、方法名、方法参数 super:objc_msgSendSuper({self, superclass}, @selector(class)),接收者仍然是子类对象,从父类开始查找方法实现 runtime用途:关联对象,遍历所有成员变量,字典转模型,自动归档解档,交换方法实现,消息转发机制 交换方法时注意类簇,如 NSMutableArray -> __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:内存泄漏问题,不准时问题 十二、内存布局 从低地址到高地址:保留 -> 代码段 -> 数据段(字符串常量 -> 已初始化数据 -> 未初始化数据) -> 堆 -> 栈 -> 内核区 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 -> push; beforeWaiting -> pop -> push(runloop休眠之前); exit -> pop 十三、CPU卡顿优化 尽量使用轻量级的对象,如CALayer代替UIView 不要频繁修改UIView的frame、bounds、trasform等属性 提前计算好布局,有需要时一次性修改,避免多次修改 Autolayout会比直接设置frame消耗更多CPU资源 图片的size最好跟UIImageView的size保持一致 控制线程的最大并发数 把耗时操作放在子线程(文本处理(尺寸计算、绘制)、图片处理(解码、绘制)) 十四、GPU卡顿优化 避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示 GPU能处理的最大纹理尺寸是409684096,超过这个尺寸会占用CPU资源进行处理 尽量减少视图数量和层次 减少透明的视图 避免出现离屏渲染 离屏喧嚷需要创建新的缓冲区,需要多次切换上下文环境 触发离屏渲染:光栅化(layer....

March 8, 2018 · Darren Ou

iOS 项目集成 Reveal

Reveal是一款UI调试工具,在iOS开发过程中可查看UI的层级关系并可动态修改界面,可以有效提高开发效率。这篇文章主要介绍如何把Reveal集成到实际的项目中。 一、Reveal主要有两种集成方式: 1、framework集成(可忽略,直接看第二点) (1)打开Reveal,选择 Reveal –> Help –> Show Reveal Library in Finder –> iOS Library (2)把Reveal.framework导入到项目中 (3)配置Target,Target –> Build Setting –> Other Linker Flags 添加以下4项: (4)运行项目 –> 打开Reveal –> 选择连接设备 项目的UI图层就可以出来了。 这种方法有几个弊端:1、需要配置项目的Build Setting,修改Other Linker Flags,多人协作开发的话如果有人没有配置就会编译出错;2、如果不留意容易打包的时候把Reveal的framework也打包进去,增大包的体积。 以下推荐另一种集成方法。 2、LLDB集成(推荐!!) (1)运行项目 –> 点击Pause program execution。 (2)在LLDB依次输入以下两条命令 # expr (Class)NSClassFromString(@“IBARevealLoader”) == nil ? (void*)dlopen("/Applications/Reveal.app/Contents/SharedSupport/iOS-Libraries/libReveal.dylib", 0x2) : ((void*)0) # expr (void)[(NSNotificationCenter *)[NSNotificationCenter defaultCenter] postNotificationName:@“IBARevealRequestStart” object:nil]; 第一条命令用于加载Reveal的动态链接库; 第二条命令用于启动Reveal调试服务。 输入两条命令后,如果LLDB打印 “ INFO: Reveal Server started ” 即成功启动Reveal服务,点击Continue即可。...

December 21, 2017 · Darren Ou

iOS 利用 Speech Kit 实现语音识别

一、前言 前一段时间彩云小译上了App Store的推荐,我下载试玩了一下,效果还是非常不错的。它可以实现实时翻译的功能,我自己粗浅地分析了一下彩云小译的实现原理,其中最重要的一步就是声音转文字。 目前市面上也有很多服务商提供声音转文字的服务,有收费的有免费的,但是毕竟是第三方的服务商,接口的性能和稳定性都不一定能保证。 2016年Apple在发布重磅产品iOS10的同时也发布了Speech Kit语音识别框架,大名鼎鼎的Siri的语音识别就是基于Speech Kit实现的。有了Speech Kit,我们就可以非常简单地实现声音转文字的功能。下面我就简单介绍一下Speech Kit的用法。 二、实现 1、页面布局 因为只是实现一个Demo,页面不需要多复杂,只需要在Storyboard上拖入两个控件:一个UITextView用于展示声音转文字的结果,一个UIButton用于触发语音识别,最好布置好约束即可。具体效果如下图: 2、申请用户权限 首先需要引入Speech Kit框架 #import <Speech/Speech.h> 申请权限非常简单,在识别前(viewDidLoad:)加入以下代码即可申请语音识别的权限: - (void)viewDidLoad { [super viewDidLoad]; // 请求权限 [SFSpeechRecognizer requestAuthorization:^(SFSpeechRecognizerAuthorizationStatus status) { NSLog(@"status %@", status == SFSpeechRecognizerAuthorizationStatusAuthorized ? @"授权成功" : @"授权失败"); }]; } 这时候运行起来会崩溃,原因是在iOS10后需要在info.plist文件中添加麦克分和语音识别权限申请信息: <key>NSSpeechRecognitionUsageDescription</key> <string>请允许语音识别</string> <key>NSMicrophoneUsageDescription</key> <string>请打开麦克风</string> 运行项目,会提示打开语音识别和打开麦克风权限,至此我们已经完成了权限的申请。 3、初始化语音识别引擎 添加以下代码: - (void)initEngine { if (!self.speechRecognizer) { // 设置语言 NSLocale *locale = [NSLocale localeWithLocaleIdentifier:@"zh-CN"]; self.speechRecognizer = [[SFSpeechRecognizer alloc] initWithLocale:locale]; } if (!self.audioEngine) { self....

December 10, 2017 · Darren Ou

在树莓派上部署 ATC 网络模拟工具(Augmented Traffic Control)

一、前言 作为移动开发者的我们,为了良好的用户体验,经常需要模拟手机应用在比较差的网络环境下的表现,模拟网络环境的方式有很多,比如使用Charles,或者在手机的开发者模式下模拟网络环境等等,但是这些都有一定的门槛。 使用Charles,首先你得连接WiFi,然后设置代理,接着开启网络模拟模式,最后测试完了如果忘了关闭代理,可能手机就上不了网了。而且都连上你电脑的代理的话就一次只能模拟一种网络环境。 使用手机的开发者模式,首先你的手机能进入开发者模式。 如果你需要随便抓一位不懂技术同事帮你测试,这些方式都不太友好,有没有一种方式可以连上WiFi就可以使用的测试方式呢?有!接下来就介绍Facebook出品的一款网络模拟工具ATC。 二、简介 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. ATC全名叫Augmented Traffic Control,是Facebook出品的一款网络模拟工具,移动开发者可以通过这款工具模拟不同条件下的网络环境,可以通过网页自由地模拟网络带宽(bandwidth)、延迟(latency)、丢包率(packet loss)、错包率(corrupted packets)和乱序率(packets ordering)。 而且!!!更牛逼的是:不同的设备连接到同一WiFi还可以模拟不同的网络环境互不影响。 github地址 三、准备 1、树莓派3(已内置有无线网卡) 2、已刷入最新RASPBIAN系统的SD卡 四、安装 安装主要有两步: 1、让树莓派有发射AP热点的能力; 2、安装ATC。 1、让树莓派有发射AP热点的能力 1.安装hostapd虚拟热点程序和dnsmasq配置DHCP、DNS服务程序: sudo apt-get install dnsmasq hostapd...

April 11, 2017 · Darren Ou

对 iOS 数据安全的一次小探索

一、主流保证数据安全的方式 1、网络传输安全 1.采用HTTPS通信协议 可防止抓包窃取、篡改传输数据,大大增加了中间人攻击的成本。 2.对敏感数据进行签名校正 采用非对称加密的方式,对敏感数据使用密钥加密,到了客户端用公钥解密,验证数据一致性,防止通信过程中被篡改。 3.采用密文传输 别人即使截取了传输信息,也无法看懂其中的意思。 2、本地存储数据安全 1.数据库加密 通过越狱设备可以很容易地把整个应用包(包括里面的数据)copy出来,这样就可以获取里面的数据库,对于没有加密的数据库就可以非常轻易地读取里面的信息,造成信息的泄漏。 数据库加密可分两个维度:1.整个数据库加密;2.对部分字段先加密再存数据库。 对部分字段加密并不适合多字段的加密存储,容易导致加密数据太过分散,影响性能。所以推荐对整个数据库加密。 2.KeyChain存储敏感数据 KeyChain是iOS系统级的存储方式,安全性无需质疑,且删除应用或者升级系统依然可以保留里面的信息。 3、源码安全 使用越狱设备可以很轻易地把应用砸壳,从而把源码dump下来,即使是没有太多经验的开发者也可以得到应用的类信息,包括函数名等。使用IDA等反编译工具可以看到应用的一些类名和方法名,进而可以分析功能实现的逻辑。 1.字符串混淆 对应用程序中使用到的字符串进行加密,保证源码被逆向后也能保护明文字符串。 2.类名、方法名混淆 市面上很多iOS应用都没有混淆类名方法名,以致于很容易使用class-dump下来,从而进行hook操作,一步一步实现iOS微信自动抢红包(非越狱)是很有趣的一个应用。这个库可以混淆OC的类名、协议、属性还有方法名。 3.程序结构混淆加密 对应用程序逻辑结构进行打乱混排,保证源码可读性降到最低。可参考这个库 4.反调试、反注入等一些主动保护策略 加入第三方安全性SDK 二、对数据库加密的研究 1、主流数据库加密方式 The SQLite Encryption Extension(收费) SQLiteEncrypt(收费) SQLiteCrypt(收费) SQLCipher(开源免费) 只有SQLCipher是免费的,所以本文主要对SQLCipher进行研究。 2、引入SQLCipher第三方库 1.手动引入 请参考官方教程。 2.使用CocoaPods pod 'FMDB/SQLCipher', '2.5' 3、SQLCipher的可行性研究 1.新建加密数据库 若没有旧数据的情况下使用很简单,只需要在FMDatabase里面的-open和-openWithFlags:方法里面添加[self setKey:kDatabaseEncryptKey];即可。 如下图所示 但是在团队协作中,如果直接修改pod仓库里面的文件,可能不好同步,下面有一个技巧,就是继承FMDatabase和FMDatabaseQueue并重载其中的方法。 继承FMDatabase的子类需要重载以下方法 - (BOOL)open { if (_db) { return YES; } int err = sqlite3_open([self sqlitePath], &_db ); if(err != SQLITE_OK) { NSLog(@"error opening!...

March 30, 2017 · Darren Ou

利用树莓派 + RetroPie 做一个怀旧游戏机

一、前言 偶尔会好怀念小时候跟朋友一起玩小霸王的日子,那时候很少家里有游戏机的人,一台游戏机有好几个朋友在等着玩,那时候能玩的东西不多,但日子过得简单而开心。虽说现在万能的淘宝还可以买到小霸王游戏机,但是远远不如自己做一个出来的成就感来的强。下面就一步一步把小时候的游戏机做出来。 二、准备 1、树莓派+SD卡 2、显示器、游戏手柄、键盘等外设 三、下载RetroPie镜像 RetroPie是一个开源的游戏模拟器,可以让你的树莓派或者PC变成一个复古游戏机,它集成了任天堂红白机、GameBoy、SNES等著名游戏系统,具有很高的可玩性。 你可以在Raspbian直接安装RetroPie,但是会耗费很长的时间,所以我在这里选择直接下载RetroPie镜像。 下载地址:https://retropie.org.uk/download/ 四、往SD卡写入RetroPie镜像 PC上可以使用 Win32DiskImager 工具写入镜像; Mac系统下可以用 ApplePi-Baker 工具; 具体写入方法及下载地址网上都很容易找到,在这里就不再赘述。 把已经写入RetroPie镜像的SD卡插入树莓派并连接电源。 五、配置 1.设置输入设备 我们的游戏手柄有时候并没有那么多个键位,我们在设置的时候怎么跳过一个键位的设置呢?这里有个小技巧:长按“下”键即可跳过。 设置游戏手柄键位 2.连接WiFi 设置好键位后,我们就可以进入主界面,上面有“13 GAMES AVAILABLE”字样,这并不是意味着里面就有13个游戏可以马上玩的,应该理解为内置了13个游戏模拟器,这时候我们怎么样才能开始玩呢? 初始主界面 按“A”键,进入设置界面,点击WIFI,连接你电脑同一个的局域网。 连接局域网 3.传输roms 打开PC,点击“计算机”上面的地址栏,输入“\retropie\roms”,即可进入游戏roms文件夹,把下载好的游戏rom拉到对应的模拟器文件夹,如.nes后缀的游戏rom拉进对应的nes文件夹。 模拟器文件夹 4.重启模拟器 重启模拟器 重启模拟器后就可以看到主界面多了一个选项,选择游戏就可以开始你的怀旧之旅了! 多了一个选项 超级马里奥 六、其他配置 有人会发现自己的SD卡的内存明明很大,但是没有塞多少个游戏内存就满了,可以进行下面的配置“扩展”内存: 进入RetroPie的设置界面,进入RASPI-CONFIG RASPI-CONFIG 选择第一项Expand Filesystem,重启树莓派即可扩展内存了。 另外,你还可以在这里进行修改登录密码、国际化选项、设置超频等操作。 树莓派设置界面 七、祝你玩得开心😊

March 12, 2017 · Darren Ou

iOS 性能优化汇总

参考: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很慢, 插入/删除很慢。 Dictionaries: 存储键值对。 用键来查找比较快。 Sets: 无序的一组值。用值来查找很快,插入/删除很快。 8、打开gzip压缩 中级: 1、重用和懒加载views 2、Cache,缓存不大可能改变但是需要经常读取的东西(NSCache) 3、权衡渲染方法(View?CALayer?CoreGraphics?OpenGL?) 4、处理内存警告 在app delegate中使用applicationDidReceiveMemoryWarning:的方法 在你的自定义UIViewController的子类(subclass)中覆盖didReceiveMemoryWarning 注册并接收 UIApplicationDidReceiveMemoryWarningNotification 的通知 5、重用大开销对象(如NSDateFormatter,NSCalendar) 6、使用Sprite Sheets 7、避免反复处理数据 解析JSON会比XML更快一些,JSON也通常更小更便于传输。从iOS5起有了官方内建的JSON deserialization 就更加方便使用了。 但是XML也有XML的好处,比如使用SAX 来解析XML就像解析本地文件一样,你不需像解析json一样等到整个文档下载完成才开始解析。当你处理很大的数据的时候就会极大地减低内存消耗和增加性能。 8、正确设定背景图片 使用UIColor的 colorWithPatternImage来设置背景色; 在view中添加一个UIImageView作为一个子View。(不使用“imageName:”) 9、减少使用Web特性 10、设定Shadow Path view.layer.shadowPath = [[UIBezierPath bezierPathWithRect:view.bounds] CGPath]; 11、优化UITableView 正确使用reuseIdentifier来重用cells 尽量使所有的view opaque,包括cell自身 避免渐变,图片缩放,后台选人 缓存行高 如果cell内现实的内容来自web,使用异步加载,缓存请求结果 使用shadowPath来画阴影 减少subviews的数量 尽量不适用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果 使用正确的数据结构来存储数据 尽量使用rowHeight,sectionFooterHeight和sectionHeaderHeight来设定固定的高,不要请求delegate 12、选择正确的数据存储选项 使用NSUerDefaults 使用XML, JSON, 或者 plist 使用NSCoding存档...

February 11, 2017 · Darren Ou

事件传递:响应者链条(译)

当你设计你的应用程序时,你可能需要动态的响应事件。例如,一个触摸事件可以由屏幕上的很多对象产生,你必须决定由哪个对象响应对应的事件和明白该对象如何接收到事件的。 当用户产生了一个事件,UIKit创建了一个包含处理事件所需信息的事件对象。然后把这个事件对象放置在全局事件队列。对于触摸事件,会被包装成一个UIEvent对象。对于动作事件,会根据你所使用的框架和你感兴趣的动作事件而有所不同。 一个事件沿着一个具体的路径向前传递,直到有一个对象可以对其进行处理。首先, UIApplication单例对象会从事件队列里获取一个事件并把它分发,一贯地将事件发送到应用的key window对象,key window对象会把事件传递到原始对象进行处理,原始对象会根据事件的类型而有所不同。 · 触摸事件。对于触摸事件,window对象首先会尝试传递事件到产生触摸事件的视图。这个视图被称为hit-test视图,寻找hit-test视图的过程被称为hit-testing。 · 动作和远程控制事件。window对象会把摇一摇动作或者远程控制事件发送到第一响应者进行处理。 这些事件路径的最终目标是寻找一个对象可以处理和相应一个事件。因此,UIKit首先会事件发送到最适合处理该事件的对象。最适合处理触摸事件的对象就是hit-test视图,对于其他事件,对象就是第一响应者。接下来的部分会详细展开hit-test视图和第一响应者对象是如何被确认的。 Hit-Testing返回触摸事件发生的视图 iOS利用hit-tesing寻找被触摸的视图。hit-tesing需要检查一个触摸事件是否在相关的视图对象的范围内,如果是,它会递归地检查这个视图对象的所有子视图。处于视图层级中包含触摸点的最底层视图就会成为hit-test视图。当iOS确定hit-test视图后,它会把触摸事件抛给这个视图去处理。 例如,假如用户触摸视图E(图2-1)。iOS通过以下顺序从子视图中找出hit-test视图 1.触摸点在视图A里面,所以会检查子视图B和C。 2.触摸点不在视图B的范围内,但是在视图C的范围内,所以它会检查子视图D和E。 3.触摸点不在视图D,但在视图E的范围内。 视图E是在视图层级中包含触摸点的最底层视图,所以它就成为了hit-test视图 (图2-1) (hitTest:withEvent:)方法返回一个给定CGPoint和UIEvent的hit-test视图。(hitTest:withEvent:)方法通过(pointInside:withEvent:)方法调用。如果一个点通过(hitTest:withEvent:)方法判断是在视图的范围内,(pointInside:withEvent:)方法就会返回YES。然后这个方法就会在返回YES的子视图上面递归地调用(hitTest:withEvent:)方法。 如果这个点通过(hitTest:withEvent:)方法判断不在视图的范围内,最先调用的(pointInside:withEvent:)方法就会返回NO。这个点被忽略了,(hitTest:withEvent:)方法就会返回nil。如果一个子视图返回NO,那么在视图层级中整个分支都会被忽略,因为如果这个触摸点不在这个视图中产生,那么也不会再这个视图的子视图中产生。这就意味着一些在父视图外的子视图的点不能接收到触摸事件,因为触摸点必须在父视图和子视图的范围内。子视图的范围超出父视图的情况会发生在子视图的clipsToBounds属性被设为NO。 注意:一个触摸对象的生命周期会跟它的hit-test密切关联,即使后来触摸移到view的外面。 hit-test视图有第一次机会去处理一个触摸事件。如果hit-test视图不能处理一个事件,这个事件就会在视图的响应者链条上传递,直到系统找到一个可以找到该事件的对象。 响应者链条由响应者对象组成 很多类型的事件依赖一个响应者链条进行传递。响应者链条是一系列的响应者对象。由第一个响应者开始,并由application对象结束。如果第一响应者不能处理一个事件,事件会向前传递给响应者链条的下一个响应者。 响应者对象是一个可以处理事件的对象。UIResponder类是所有响应者对象的基类,它定义的程序接口不只是为了事件的处理,而且为了普通的响应者行为。UIApplication,UIViewController和UIView的实例都可以成为响应者,意味着所有的视图和很多的关键控制器对象都可以是响应者。注意:核心动画图层都不可以成为响应者。 首先由第一响应者接收事件。一般的,第一项响应者是一个视图对象。一个对象通过以下两种方式成为第一响应者: 1、复写(canBecomeFirstResponder)方法并返回YES。 2、接收一个(becomFirstResponder)信息。如果有必要,一个对象可以发送这条信息给它自己。 注意:一个对象成为第一响应者前确保应用已经把这个对象的图层渲染出来。例如,我们一般在(viewDidAppear:)方法里面调用(becomeFirstResponder)方法,如果尝试在(viewWillAppear:)方法里面分配第一响应者,对象的图层还没有渲染出来,所以(becomeFirstResponder)方法就会返回NO。 事件不是唯一的依赖响应者链条的对象。响应者链条被用于以下的情况: · 触摸事件。如果hit-test视图不能处理一个触摸事件,那么这个事件就会从hit-test视图开始沿着响应者链条传递。 · 动作事件。通过UIKit来处理摇一摇动作事件,第一响应者必须实现UIResponder类的(motionBegan:withEvent:)方法或者(motionEnded:withEvent:)方法之一。 · 远程控制事件。第一响应者处理远程控制事件必须实现UIResponder类的(remoteControlReceivedWithEvent:)方法。 · 动作信息。

January 13, 2017 · Darren Ou