再见,BLE的那些坑!

蓝牙,平常你用的多吗?上班路上戴着蓝牙耳机听音乐、看视频打开蓝牙分享个人热点给小伙伴们解锁共享单车时,打开蓝牙就能迅速解锁……BLE—蓝牙低能耗技术,方便了我们的生活,但是开发者在开发过程中却遇到了不少坑,尤其对小白用户来说坑真的太多了,都是安卓、iOS系统惹的祸。今天,柚子君给大家分享一个解决BLE那些坑的经验,希望能帮助更多人。项目中用到BLE模块时,要注意以下问题:❶ 用示例代码安卓系统扫描蓝牙设备就无法成功,而苹果则没问题。✓ 解决办法:将serviceUUIDs: [’’, ‘’]改为serviceUUIDs: []即可。一次扫描不出来可以多扫描几次。❷ 安卓和苹果的设备UUID不一样,安卓为xx:xx:xx:xx:xx:xx格式,苹果为GUID格式。✓ 解决办法:以你扫描获取的UUID访问设备就行了。❸ 安桌和苹果的服务UUID和服务特征CharacteristicsUUID不一样(服务UUID和服务特征CharacteristicsUUID是父子关系)。安卓为GUID格式,苹果为短格式。两者扫描出的服务数也不一样,苹果只扫描出用户能调用的,安卓会把系统级的也扫描出来,安卓一般第3个服务是用户能调用的。✓ 解决办法:以你扫描获取的获取的服务UUID的服务特征CharacteristicsUUID访问就行了。❹ 最重要的一点,也是为什么调用老不对的问题。✓ 解决办法:不管你是否知道设备UUID,服务UUID,蓝牙是否打开。请一定要按步骤调用:扫描设备->连接设备->获取所有服务UUID->服务UUID对应的所有服务特征CharacteristicsUUID。这些步骤走完了,才能调用服务UUID和对应的服务特征CharacteristicsUUID。▌本文作者:APICloud 版主 scmyzyh

December 24, 2018 · 1 min · jiezi

美团开源Graver框架:用“雕刻”诠释iOS端UI界面的高效渲染

Graver 是一款高效的 UI 渲染框架,它以更低的资源消耗来构建十分流畅的 UI 界面。Graver 独创性的采用了基于绘制的视觉元素分解方式来构建界面,得益于此,该框架能让 UI 渲染过程变得更加简单、灵活。目前,该框架已经在美团 App 的外卖频道、独立外卖 App 核心业务场景的大多数业务中进行了应用,同时也得到美团外卖内部技术团队的认可和肯定。App 渲染性能优化是一个普遍存在的问题,为了惠及更多的前端开发同学,美团外卖 iOS 开发团队将其进行开源,Github 项目地址与使用文档详见:https://github.com/Meituan-Di… 。我们希望该框架能够应用到更广阔的业务场景。当然,我们也知道该框架尚有待完善之处,也希望能与更多技术同行一起交流、探讨、共建。前言我们为什么需要关注界面的渲染性能?App 使用体验主要包含产品功能、交互视觉、前端性能,而使用体验的好与坏,直接影响用户持续使用还是转而使用其他 App,所以我们非常关注 App 的渲染性能。而且在互联网产品流量竞争愈发激烈的大背景下,优质的使用体验可以为现有用户提供更好的服务,进而提高用户转化和留存,这也意味着创收、盈利。<center><font color=gray size = 2>图1 使用体验与转化、留存</font></center>背景美团外卖 App 从2013年成立至今,已经走过了五个春秋,在技术层面先后经历了快速验证、模块化、精细化和平台化四个阶段,产品形态上也日趋成熟。在此期间,我们构建并完善了监控、报警、容灾、备份等各项基础设施,Metrics 即是其中的性能监控系统。曾经一段时间,我们以外卖 App 首页商家卡片列表为例,通过 Metrics 性能监控系统发现其在 FPS、CPU、Memory 等方面的各项指标并不理想。于是,通过 Xcode 自带的 TimeProfile 等性能检测工具,然后结合代码分析等手段找到了现存性能瓶颈。与此同时,我们梳理其近半年的迭代版本需求发现,UI 往往需要根据不同场景甚至不同用户展示不同的内容。为了不断迎合用户的需求,快速应对市场变化,这种特征还会持续存在。然而,它会带来以下问题:视图层级愈加复杂、视图数量愈加众多,从版本长期迭代来看是潜在的性能瓶颈点。如何快速、高效支撑 UI 变化,同时保证不会二次引入性能瓶颈。<center><font color=gray size = 2>图2 影响渲染性能、研发效率的瓶颈点</font></center>Graver 介绍为了解决现存的性能瓶颈以及后续潜在的性能瓶颈,我们期望构建一套解决方案,该方案能在充分满足外卖业务特征的前提下,以标准化、一站式的方式解决 iOS 端 App 的渲染性能问题,并且对研发效率有一定提升, Graver(雕工)框架应运而生。因为 Graver 独创性地采用了全新的视觉元素分解思路,所以该框架使用起来十分灵活、简单。我们先来看一下 Graver 的主要特点:性能表现优异以外卖 App 首页商家列表为例,应用 Graver 之后5分位滚动帧率从满帧的<font color=red size = 2>84%</font>提升至<font color=red size = 2>96%</font>,50分位几乎满帧;CPU 占用率下降了近<font color=red size = 2>6个百分点</font>,有效提升了空闲 CPU 的资源利用率,降低了峰值 CPU 的占用率。如图3所示:<center><font color=gray size = 2>图3 优化前后技术指标对比</font></center>“一站式”异步化Graver 从文本计算、样式排版渲染、图片解码,再到绘制,实现了全程异步化,并且是线程安全的。使用 Graver 可以一站式获得全部性能优化点,可以让我们:不再担心散点式的“遇见一处改一处”的麻烦。不再担心离屏渲染等各种可能导致性能瓶颈的问题,以及令人头痛的解决办法。不再担心优化会有遗漏、优化不到位。不再担心未来变化可能带来的任何性能瓶颈。性能消耗的“边际成本”几乎为零Graver 渲染整个过程除画板视图外完全没有使用 UIKit 控件,最终产出的结果是一张位图(Bitmap),视图层级、数量大幅降低。以外卖 App 首页铂金展位视图为例,原有方案由58个控件、12层级拼接而成;而应用 Graver 后仅需1个视图、1级层级绘制而成。 伴随着需求迭代、视觉元素变化,性能消耗恒属常数级。如图4所示:<center><font color=gray size = 2>图4 外卖 App 铂金展位应用 Graver 前后对比</font></center>渲染速度快Graver 并发进行多个画板视图的渲染、显示工作。得益于图文混排技术的应用,达到了内存占用低,渲染速度快的效果。由于排版数据是不变的,所以内部会进行缓存、复用,这又进一步促进了整体渲染效率。Graver 既做到了高效渲染,又保证了低时延页面加载。<center><font color=gray size = 2>图5 渲染效率说明</font></center>以“少”胜“繁”Graver 重新抽象封装 CoreText、CoreGraphic 等系统基础能力,通过少量系统标准图形绘制接口即可实现复杂界面展示。基于位图(Bitmap)的轻量事件交互系统如上述所说,界面展示从传统的视图树转变为一张位图,而位图不能响应、区分内部具体位置的点击事件。Graver 提供了基于位图的轻量事件交互系统,可以准确识别点击位置发生在位图的哪一块“绘制单元”内。该“绘制单元”可以理解为与我们一贯使用的某个具体 UI 控件相对应的视觉展示。使用 Graver 为某一视觉展示添加事件如同使用系统 UIButton 添加事件一样简单。全新的视觉元素分解思路Graver 一改界面编程思路,与传统的通过控件“拼接”、“添加”,视图排列组合方式构建界面不同,它提供了灵活、便捷的接口让我们以“视觉所见”的方式构建界面。这一特点在下文Graver使用中详细阐述,正是因为该特点实现了研发效率的提升。Graver 使用Graver 引入了全新的视觉元素分解的思路。借助该思路可以实现通过一种对象来表达任一视觉元素、甚至是任一视觉元素的组合,从而消除界面布局的复杂性。我们先来回顾下传统界面的构建方式,以外卖 App 商家卡片其中一种样式为例,如图6所示:<center><font color=gray size = 2>图6 外卖 App 商家卡片</font></center>在实现商家卡片的界面样式时,通常会根据视觉上的识别、交互要求来建立界面展示与系统提供的 UI 控件间的映射关系。以标号②位置的样式为例,在考虑复用的情况下通常这部分会使用三个系统控件来完成,分别是左侧蓝底的“预订”使用 UILabel 控件、右侧的蓝色边框“2.26.21:30起送”使用 UILabel 控件、把左右两侧 UILabel 控件装起来的 UIView 控件;在确定好采用的 UI 控件之后,需要针对展示样式分门别类的设置各个控件的渲染属性来实现图示 UI 效果,渲染属性通常一部分预设,一部分根据业务数据的不同再进行二次设置;其次,设置各个控件的内容属性实现业务数据内容的展示,展示的内容一般是网络业务数据经逻辑处理、加工后的数据。如果涉及到点击事件,还需要添加手势或者更换成 UIButton 控件。接下来,需要根据视觉要求实现排版逻辑,以标号⑧、⑨为例,当标号⑧位置的数据没有的情况下,需要上提标号⑨位置的“美团专送”到图示标号⑧位置。诸如类似的排版逻辑随处可见。对于图示任一位置的展示内容都存在上述的循环思考、编写工作。随着界面元素的增加、变化,问题会变得更加复杂。传统的界面构建方式其实是在 UI控件的维度去分解视觉元素,具体是做以下四方面的编写工作:控件选择:根据展示内容、样式、交互要求确定采用哪种系统控件。布局信息:UI 控件的大小、位置,即 Frame。内容信息:UI 控件展示出来的业务数据,如标号①位置的“星巴克咖啡店”。渲染信息:UI 控件展示出来的效果,如字体、字号、透明度、边框、颜色等。最后,将各个控件以排列组合方式合成为一棵视图树。Graver 框架提供了以画板视图为基础,通过对更底层的 CoreText、CoreGraphic 框架封装,以更贴近“视觉所见”的角度定义了全新视觉元素分解、界面展示构建的过程。通常“视觉所见”可划分为两部分:静态展示、动态展示。静态展示包含图片、文本;动态展示包含视频、动画等。在视觉展示全部为静态内容的时候,一个 Cell 即是一个画布,除此以外没有任何 UI 控件;否则,可以按需灵活的进行画布拆分来满足动画、视频等需要。<center><font color=gray size = 2>图7 画板和传统视图树</font></center>以图6商家卡片中标号②、⑧为例,新实现方式的伪代码是这样的:WMMutableAttributedItem *item = [[WMMutableAttributedItem alloc] init];[[[[item appendImage:[[UIImage wmg_imageWithColor:“blue”] wmg_drawText:“预订”]] appendImage:[[UIImage wmg_imageWithColor:“clear” borderWidth:1 borderColor:“blue”] wmg_drawText:“2.26.21:30起送”] appendWhiteSpaceWithWidth:“width”]//总体宽度减去②和⑧的宽度总和剩余部分 apendText:“50分钟|2.5km”];上述实现方式即是把标号②、⑧部分作为一个整体来实现,任何单一系统控件都无法做到这一点。Graver 渲染原理<center><font color=gray size = 2>图8 Graver 工作时序</font></center>如图8所示,Graver 涉及多个队列间的交互,以外卖 App 商家列表为例,整体流程如下:主线程构建请求参数,创建请求任务并放入网络线程队列中,发起网络请求。网络线程向后端服务发起请求,获得对应的业务模型数据(如包含了店铺名称,商家头图,评分,配送时长,客单价,优惠活动等店铺属性的商家卡片列表)。网络线程创建包含业务模型数据(如商家卡片列表)的排版任务,提交到预排版线程处理,进入预排版流程。预排版队列取出排版任务,交由布局引擎计算 UI 布局,将业务模型解析成可被渲染引擎直接处理的,包含布局、层级、渲染信息的排版模型。解析结束后,通知主线程排版完成。主线程获取排版模型后,随即触发内容显示。根据相对屏幕位置及出现的先后顺序,创建包含将需要显示区域信息的绘制任务,放入异步绘制线程队列中,发起绘制流程。异步绘制线程队列取出绘制任务,进行图文绘制,最终输出一张包含了图文内容(如商家卡片)的图片。绘制任务结束后,通知主线程队绘制完成,主线程随后展示绘制区域。整体按照队列间串行、队列内并行的方式执行。业务应用Graver 在外卖内部发布之后,我们也将其推广到更多的业务线,并希望 Graver 能够成为对业务开展有重要保障的一项基础服务。经过半年多的内部试用,Graver 的可靠性、渲染性能、业务适应能力也受到外卖内部的肯定和认可。截止发稿时,Graver 已经基本覆盖了美团 App 的外卖频道、独立外卖 App 核心业务场景的大多数业务。下面列举 Graver 在外卖业务的部分应用案例:经验总结总结一下,对于界面渲染性能优化而言,要站在一个更高角度来思考问题的解决方案。横向上,从普适性角度解决性能瓶颈点,避免其他人遇到类似问题的重复工作;纵向上,从长远考虑问题做到防微杜渐,一次优化,长期受益。基于此,我们提出一站式、标准化的渲染性能解决方案。诚然,这会遇到很多难点。面对界面样式构建的问题,系统 UIKit 框架着实为我们提供了便利,然而有时候我们需要跳出固有思维,尝试建立一套全新界面构建、视觉元素分解的思路。参考资料前端感官性能的衡量和优化实践作者简介洋洋,美团高级工程师。2018年加入美团,目前负责【美团外卖】和【美团外卖频道】的 iOS 客户端首页业务,以及支撑首页业务的技术架构、工具和系统的开发和维护工作。招聘美团外卖长期招聘 Android、iOS、FE 高级/资深工程师和技术专家,Base 北京、上海、成都,欢迎有兴趣的同学投递简历到chenhang03#meituan.com。 ...

December 24, 2018 · 1 min · jiezi

WKCrashSDK - crash拦截工具

前言由于线上始终出现部分未知原因崩溃问题,遂遵循网易出的crash拦截机制,自实现了一个crash拦截工具,现已上线运行数月,累计拦截闪退···总之很多啦···实现原理原理网上已有很多文章阐述,这里推荐几个链接。网易iOS App运行时Crash自动防护实践[黑魔法教你让iOS APP防住Crash](https://www.jianshu.com/p/021…优势:封装完善,使用方便,仅需将文件导入项目即可生效。具备debug期crash发生的UI层级提示。可和线上接口配合实现实时开关操作。可自定crashinfo上传地点(我司是直接上传到bugly搜集)经过实际测试,已在我司多个线上APP实测有效,暂未发现有什么奇怪的问题。项目要点其实从上述原理文章以及能够了解基本的实现逻辑,只是在实现过程中也遇到了不少的坑。下面就和大家分享一下一些实现过程的坑以及为了满足我司需求拓展的一些功能点。KVO这里划重点1、拦截KVO时,存在部分三方库的不能拦截,以及系统的相机相册无需拦截,否则会出现无效的crash提示,在我的项目已经进行了白名单过滤。如果用了一些特殊的三方,可能在使用此工具时,需要收录一下,避免无效的crashinfo被收集。//白名单主要针对观察者,因为被观察者很有可能是系统类,所以只能针对观察者处理,如果拦截到系统的观察者,则记录入白名单+ (NSArray *)kvoWhiteList{ static NSArray *whiteList = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ whiteList = @[@“WKKVOProxy”,//自己的 @“RACKVOProxy”,//RAC的 @“BLYSDKManager”,//bugly的 @"_YYTextKeyboardViewFrameObserver",//YYKit的 //相册相关 @“PLManagedAlbum”, @“AVCapturePhotoOutput”, @“AVCaptureStillImageOutput”, //3.2.9添加 拍照相关 @“AVCaptureSession”, @“PLPhotoStreamAlbum”, @“AVKVODispatcher”, @“PLCloudSharedAlbum”, @“AVPlayerPropertyCache”, ];//@“AVCaptureFigVideoDevice” }); return whiteList;}2、对KVO的拦截,需使用递归锁保证线程安全。 wk_pthread_mutex_init_recursive(&_lock,true); pthread_mutex_lock(&_lock); pthread_mutex_unlock(&lock);Zombie划重点在有僵尸对象造成崩溃时,实际是将其数据置为空,但是并不释放它,然后将其isa指向一个可接受任何方法的中转类中,以此来拦截掉崩溃。为了统一处理crash上报,在这里用了动态类创建传递类型信息的方式。并且.m文件需要使用MRC,在编译处添加-fno-objc-arc即可。 NSString *className = NSStringFromClass(selfClass); NSString *zombieClassName = [@“WKZombie” stringByAppendingString: className];//这一步很重要,动态生成类,如果被僵尸,则可以得知实际是哪个类产生了僵尸指针 导致崩溃 Class zombieClass = NSClassFromString(zombieClassName); if(!zombieClass) { zombieClass = objc_allocateClassPair([WKZombieStub class], [zombieClassName UTF8String], 0); } objc_destructInstance(self);//销毁实例 相关信息 内存不释放 object_setClass(self, zombieClass); instanceList.size(); if (instanceList.size() >= maxCount) { id object = instanceList.front(); instanceList.pop_front(); free(object); } instanceList.push_back(self);Container在拦截NSArray以及NSDictionary的系列方法时,需要注意一下它们的实现方式是类簇实现,需要找到它们真实的类来拦截才有效。swizzling_exchangeMethod(objc_getClass("__NSArray0"), @selector(objectAtIndex:), @selector(emptyArray_objectAtIndex:));swizzling_exchangeMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:), @selector(arrayI_objectAtIndex:));swizzling_exchangeMethod(objc_getClass("__NSSingleObjectArrayI"), @selector(objectAtIndex:), @selector(singleObjectArrayI_objectAtIndex:));划重点在对NSMutablArray拦截时,需要特别注意其objectAtIndex的方法,需得在遵守MRC的文件下拦截,否则会在iOS8上弹出键盘时,APP进入后台产生崩溃。是必现的。所以在工具中 这个方法是单独放到一个文件里面hook的,然后在编译处为此文件添加-fno-objc-arc。UI层级提示信息在Debug模式下,当拦截到crash时,会出现UI层级的提示,如下图:点击按钮可以查看具体的崩溃信息,如下图前面title表示为崩溃的类型,后面数字为拦截的次数。再次点击cell可定位崩溃的文件、对应方法名、最近一次崩溃发生的时间以及在本机上这个崩溃发生的次数。大家可能也注意到了Crash的按钮是可以随意拖动,以及根据你进入的大类型不同来变更提示信息的。一个可有可无的小优化CrashInfo上报CrashInfo的收集,我们只需要关注WKCrashReport类,去实现它的一个代理即可。@protocol WKCrashReportDelegate <NSObject>- (void)handleCrashInfo:(WKCrashModel *)model type:(NSString *)type;@end返回的两个参数:WKCrashModel 以及 NSString type其功用如下:WKCrashModel@interface WKCrashModel : NSObject@property (nonatomic, strong) NSString * clasName; //产生crash的类名@property (nonatomic, strong) NSString * msg; //could be 方法名,或者其他有效信息@property (nonatomic, strong) NSArray * threadStack;//crash时的堆栈信息@property (nonatomic, assign) NSTimeInterval time;//crash时间@property (nonatomic, strong, readonly) NSString * deviceType;//设备信息@property (nonatomic, strong, readonly) NSString * systemVersion;//系统版本@endNSString type其返回值可能有UnrecognizedSelector,KVO,Container,Timer,NotificationCenter,Null,String,Zombie分别代表八种拦截的crash类型PS:如有特殊需求可自行扩充使用方式Demo地址进入Demo地址找到WKCrashManagerDemo里面的WKCrashSDK文件夹,拖入项目即可。后续我会抽空将其加入cocoapods豪华午餐注:如从Demo中直接拖入,则默认开启除了Zomie拦截外的其他7种类型的crash拦截。如需自定义请查看WKCrashManager的实现文件。联系方式如有兴趣可通过邮箱357863248@qq.com一起交流进步。 ...

December 22, 2018 · 1 min · jiezi

WKViewManager iOS 弹窗架构

前言近来由于App中弹窗过多,再加上还有半透明的引导层,时常会出现多个弹窗重叠弹出,甚至会伴随引导层一起弹出,极大的影响了用户体验。上述问题,其实很简单,只需要添加一个弹窗队列即可。但是与此同时我们又添加了新的需求如下:广告弹窗不能出现在阅读页订阅更新弹窗只能出现在首页版本更新功能提示页不允许出现任何弹窗重新安装后出现的选择性别和分类的弹窗不允许出现任何弹窗广告观赏页面不能出现任何弹窗由于广告和公告页面的弹出顺序不定,当弹出广告时,广告视图可点击产生跳转,但是在跳转后公告会弹出(公告具备倒计时只显示多少秒,然后自动消失),则会出现公告无法被用户看到的情况,则需要在跳转离开页面时,暂停队列由于项目中我们采用了模块化的方式精简代码、降低工作耦合,所以我们的弹窗并不是基于ViewController弹出的,而是在需要弹出的时候获取当前显示的ViewController用于显示的。例如项目中任务模块,任务是可以在任何地方任何时候被完成,任务本身不关心它是位于何时何地被完成,只需要关心完成后,需要对应做出什么操作,例如写本地数据库,更新用户数据,弹出完成任务提示,和ViewController并无大的关联。简单的说,我们的弹窗是属于功能块的,而功能块不应该且不用去关心当前展示的ViewController是哪一个。功能介绍本文介绍的一个弹窗架构实际也可以归类为一个弹窗功能模块,当弹窗出现在不该出现的地方时,它来负责调度和处理,其过程对于其它功能模块透明,并不产生任何耦合度。其实现功能如下:弹窗视图按队列依次出现弹窗视图支持优先级模式队列基于ViewController,杜绝一个弹窗问题导致全局弹窗失效弹窗视图可指定出现于某ViewController出现后(白名单)ViewController可指定不接收弹窗视图出现于自身出现时(黑名单)实现ViewController didDisAppear后 队列暂停,willAppear队列继续优点WKViewManager 弹窗队列管理类,使用AOP切片基于self-manager模式封装,高内聚性,几乎不会和ViewController或功能模块产生耦合。劣势由于是弹窗视图的一个管理工具,所有需要被管理的弹窗均需属于WKBaseView的子类,项目中已书写对应所需要的弹窗基类如WKPopBaseView、WKStepMaskGuideView等等,有需要构建弹窗的时候可以去查看相关类。(关于WKBaseView,是一个封装的非常完善的弹窗基类,预计后续会写一篇文章详细介绍,它的整个结构是非常完善,针对弹窗功能几乎没有扩充瓶颈)实现原理白名单模式(功能点4)白名单模式只有一个关键内容白名单字典 - whiteList白名单字典位于WKViewManager类中。字典中key为从属于WKBaseView的子类的类名字符串,value是一个数组,里面放入对应的ViewController的类名字符串。例如 白名单内容为@{@“KMAnnouncementView”:@[@“HJTMainTabBarVC”]};则在广告公告模块中,KMAnnouncementView调用显示时,所获取的View不是HJTMainTabBarVC的View,则KMAnnouncementView会进入白名单待显示数组中,当HJTMainTabBarVC调用viewwillappear时才会实际被显示出来。一句话介绍白名单白名单中的key对应的view永远只会出现在其对应的value中的vc上,即便你在其他任何VC上调用显示显示KMAnnouncementView, KMAnnouncementView也只会出现在其对应的value的vc上。黑名单模式(功能点5)黑名单模式包括两个关键内容。 黑名单字典 - blackList 黑名单HomeVC - blackListHomeVCClassName黑名单字典位于WKViewManager类中。字典中key为ViewController的类名字符串,value是一个数组,里面应放入对应的从属于WKBaseView的子类的类名字符串。例如 黑名单内容为 @{@“KMBaseViewController”,@[@“WKPopBaseView”]};则表明,WKPopBaseView类(不包含其子类),如果由于功能块解耦或者其它原因,导致WKPopBaseView被展示于KMBaseViewController(不包含其子类)的子视图上时,WKViewManager会拦截此行为,将WKPopBaseView实例存入黑名单待显示数组,当回到黑名单HomeVC时再显示出来。从而实现了WKPopBaseView一定不会出现在KMBaseViewController的视图上。一句话解释黑名单黑名单中key对应的vc显示时,其对应的value中的view必然不会出现,如果被强制出现时,其也不会出现,直到黑名单homeVC的viewwillappear被调用才会显示。功能点6由于队列基于viewcontroller的,所以很简单只需要hook viewcontroller的 willappear和diddisappear即可,在willappear里面找到队列设置suspended为NO,在diddisappear里面suspended为yes即可Demo地址使用方式前提:因为内部实现使用了Masonry布局,所以需要先Pod Masonry三方库。步骤一、下载Demo,找到BaseView文件夹拖入项目。步骤二、将文件中的WKViewManagerHeader.h文件加入全局头文件.pch中。步骤三、新建的View继承自WKPopBaseView,并重写setInterFace,需首先调用父类的实现- (void)setInterFace{ [super setInterFace]; //TODO contentview的高度必须设置 [self.contentView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self); make.centerY.equalTo(self).offset(SCREEN_HEIGHT); make.width.height.equalTo(@200); }];}可自定义出现和消失动画——–实现updateContentViewConstraint:(BOOL)isShow方法- (void)updateContentViewConstraint:(BOOL)isShow{ [self.contentView mas_updateConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(self).offset(isShow ? 0 : SCREEN_HEIGHT); }];}还有更多可自定义的效果,可通过顶层基类WKbaseVIew封装的声明周期实现。详情请查看Demo步骤四、初始化后,在需要展示的地方调用showInView:isShow:方法即可。 WKTestView * v = [[WKTestView alloc] init]; v.title = @“This is Test View can not show in HomePage”; [v showInView:self.view isShow:YES];交流方式邮箱357863248@qq.com

December 21, 2018 · 1 min · jiezi

批量修改OC类名脚本 -- python

前言近日由于种种原因,需要把代码里面的类全部都修改一遍类名。在网上找了一圈相关的开源库,没有发现有合适满意的,始终都存在一些问题,要么出现误修改,要么把方法名或者属性名也给修改了等等情况。于是决定掏出生疏的Python来写一个。批量修改类名需要解决四个问题:如何取得需要修改的类名?如何修改文件中出现的类名并且不会出现误改等情况?如何修改文件名?修改文件名后,如何同步修改project.pbxproj里面的文件名信息?问题一、如何取得需要修改的类名?想法A:由于自己前不久写了一个获取target对应的编译以及资源文件的工具MacPbxprojHelper,利用其来获取到target对应的类名写入文件,然后用python来读取文件获取到需要修改的类名,实时上第一版我也是这么做的,但是不够优雅,明明一个脚本能解决的事,为什么需要那么多步操作呢?想法B:利用python直接遍历文件夹获取类名,没错就决定是你了。其实用python遍历文件实现起来特别简单快捷。但是其中也有可能存在的问题,例如我们的项目中包含有部分资源文件,而资源文件的后缀名是不定的,但是实际我们只需要.m以及.mm结尾的文件前缀作为我们的类名,当然其中包含有类目也需要过滤掉。并且考虑到我们具备一些特殊的需求,需要过滤某些文件甚至某些文件夹下的文件等等。我这边的脚本都有考虑到实现代码如下:def getClassNames(filepath): #读取文件名入数组 #过滤文件夹 filterDirs = [“ThirdKit/”,“小说/”] #过滤文件 filterFiles = [] classNames = [] #遍历filepath下所有文件,包括子目录 for root, dirs, files in os.walk(filepath): for name in files: path = os.path.join(root, name) #过滤文件夹 isFilterDir = 0 for filterDir in filterDirs: if filterDir in path: isFilterDir = 1 break if isFilterDir != 1: if “.m” in name: splitNames = name.split(".m") #只选择.m和.mm结尾的文件 if splitNames[1] == "" or splitNames[1] == “m”: cn = splitNames[0] #过滤文件 isFilterFile = 0 for ff in filterFiles: if ff == cn: isFilterFile = 1 if isFilterFile != 1: #过滤类目 if cn.find("+") == -1: print(cn) classNames.append(cn) return classNames问题二、如何修改文件中出现的类名并且不出现误改的情况?在我搜集到的开源的脚本中,有极大比例的一部分是直接通过类名替换,其实这特别容易出现误改。比如我有一个类名为ABC,一个类名为AB,那么ABC就可能出现被修改两次的情况。所以我这边就采用笨办法,先敲定类名出现的场景,根据其场景设定规则,只有出现类名出现在这些规则中时,才去替换,这样就可以保证100%的正确率。(这也是大坑,因为不同的程序员书写习惯的问题,导致规则的定义实际极度繁琐,而且会出现部分遗漏,每一次遗漏后,我都需要添加规则再重跑来验证··)下面就是这个脚本最大的贡献·· 自认为基本涵盖完了类名的出现场景(如果有没有涵盖的,请记得一定联系我!!),一共37种规则,运用这37种规则,做到了一次脚本,既完美修改,无需在动代码就可以直接运行并且不会有闪退问题··#一开始构思了 14个 后来…. rule_1 = ‘"’ + cn + ‘.’ #引用修改 rule_2 = ‘"’ + cn + ‘"’ #类名被直接使用 rule_3 = ‘:’ + cn + ’ ’ #继承 后续有空格 rule_4 = ‘:’ + cn + ‘\n’ #类名后面是换行 rule_5 = ’ ’ + cn + ‘<’ rule_6 = ’ ’ + cn + ‘\n’ #类名后面是换行 rule_7 = ’ ’ + cn + ’ ’ rule_8 = ’ ’ + cn + ‘(’ rule_9 = ‘[’ + cn + ’ ’ rule_10 = ‘[’ + cn + ‘’ rule_11 = ‘(’ + cn + ‘’ rule_12 = ‘)’ + cn + ‘’ rule_13 = ’ ’ + cn + ‘’ rule_14 = ’ ’ + cn + ‘\n’ rule_15 = ‘(’ + cn + ’ ’ rule_16 = ’ ’ + cn + ‘;’ rule_17 = ‘,’ + cn + ’ ’ rule_18 = ‘,’ + cn + ‘’ rule_19 = ‘)’ + cn + ’ ’ rule_20 = ‘,’ + cn + ‘;’ rule_21 = ’ ’ + cn + ‘,’ rule_22 = ‘<’ + cn + ‘’ #被当做协议 遵守类 rule_23 = ‘<’ + cn + ’ ’ rule_24 = ‘,’ + cn + ‘,’ rule_25 = ’ ’ + cn + ‘:’ rule_26 = ‘:’ + cn + ‘//’ #后面跟注释的、、我TM。。 rule_27 = ’ ’ + cn + ‘//’ rule_28 = ’ ’ + cn + ‘{’ #后面跟大括号的·· rule_29 = ‘)’ + cn + ‘<’ #为什么遵守协议 也用了 实际类名 没有用ID rule_30 = ‘(’ + cn + ‘.’ #为什么类要使用.语法 rule_31 = ’ ’ + cn + ‘.’ #类名调用.语法 rule_32 = ‘!’ + cn + ‘.’ rule_33 = ‘:’ + cn + ‘.’ #类名调用点语法 被当做参数传入 rule_34 = ‘[’ + cn + ‘\n’ #类名被换行 rule_35 = ‘:’ + cn + ‘<’ #继承类名后面 直接接入协议 默认实际是不存在这个问题的 rule_36 = ‘"’ + cn + ‘’ #类取名有下划线以及数字 以及通过工厂用数字来创建的情况 rule_37 = ‘"’ + cn + ‘%’ #类取名有数字 以及通过工厂用数字来创建的情况问题三、如何修改文件名?这个问题其实很好解决,利用python的os库就可以直接rename,只是需要拼接全路径。并且在给类文件重命名的时,需要注意前缀和后缀添加的位置,不要添加到.h和.m后面去了即可。问题四、如何同步修改project.pbxproj里面的文件信息?这里又和第二个问题一样需要定义一定规则,才能确保不会出现误替换,但是这个规则和上面比起来简直是小巫见大巫。规则如下:def pbRule(cn): rule_1 = ‘=’ + cn + “.” rule_2 = ’ ’ + cn + “.” rule_3 = ‘/’ + cn + “.” rule_4 = ‘"’ + cn + “.“以上,问题都解决了。使用方式———-脚本说明———-脚本用于批量修改类名脚本可传入四个参数1、待处理的文件路径. example–>’./KanManHua'2、待修改的pbxproj文件地址 example– >’./KanManHua.xcodeproj/project.pbxproj'3、类名前缀 example–>‘MHT‘4、类名后缀 example–>’MHT’其中前三个参数为必传参数 划重点,要考———-请开心使用———另脚本支持 -h –help 操作当参数错误时,也有对应提示以及上述帮助信息正常使用示例:python changeClassName.py ./KanManHua ./KanManHua.xcodeproj/project.pbxproj MHT _MHThelpinfo获取示例:python changeClassName.pypython changeClassName.py -hpython changeClassName.py –help脚本地址后记由于我司还有需求修改指定 target的类名,实际也有一套可以仅仅只修改target类名的方法,但是由于使用要复杂一点,首先要提取target对应的编译类,再使用脚本来修改,修改使用到我之前发布的一个工具类MacPbxprojHelper,如有需要的话请和我联系。联系方式:357863248@qq.comMacPbxprojHelper介绍链接 ...

December 21, 2018 · 3 min · jiezi

WKAnimatorManager - 转场管理类

前言现如今App的体验越来越重要,我们的App中也要求加入更多的动画效果,其中最广泛的是转场动画,由于需要批量的加入转场动画,如果于每一个VC中去添加修改是一件费时费力的事情,所以有了下面要介绍的一个开源组件–WKAnimatorManager功能介绍实现了对转场动画的封装以及左滑的集中处理。优点:集成方便,使用简单,拓展性强。简要介绍转场管理结构中只有四个类:UINavigationController+WKTransitions功能:hook了UINavigationController的部分方法,实现集成即可使用。其一:hook了push,pop系列方法,于其中设置动画转场子类,便于统一处理动画其二:重写了viewdidload 设置UINavigationController的转场代理为WKAnimatorManager的单例UIViewController+WKTransitions功能:hook了UIViewController的部分方法以及新增了两个属性,实现集成即可使用。其一:为UIViewController添加了两个转场动画属性,一个用于模态转场wk_modelAnimator,一个用于导航栏转场wk_navAnimator,均为WKBaseAnimator的子类其二:hook了dismiss以及present模态转场方法,便于统一处理动画其三:hook了init,讲其模态转场代理设置为WKAnimatorManager单例WKAnimatorManager功能:遵循模态以及导航栏转场协议,集中处理转场逻辑内部自定义了UIScreenEdgePanGestureRecognizer手势,实现左滑返回逻辑自处理。WKBaseAnimator功能:动画处理基类自定义动画只需要继承自此类,将动画效果写于present以及dismiss方法即可。如需左滑手势返回,系统只支持了view层级动画的左滑返回操作,所以左滑返回动画必须为view层级动画,其次需设置edgeType为UIRectEdgeLeft即可。使用方式步骤一、下载Demo,将Demo里的Transitions文件夹拖入项目,再将已整合好的头文件WKAnimatorManagerHeader.h加入全局头文件.pch中。步骤二、创建继承自WKBaseAnimator.h的自定义动画类,下面以Demo中已存在的的动画作为例子(后面有效果展示):#import “WKBaseAnimator.h”@interface MagicMoveTransition : WKBaseAnimator//自定义需要的属性@property (nonatomic, assign) CGRect endRect;//终点@property (nonatomic, strong) UIView * sourceView;@end于.m文件中实现从父类继承而来的present以及dismiss- (void)present{ [UIView animateWithDuration:[self transitionDuration] delay:0.0f usingSpringWithDamping:0.8f initialSpringVelocity:1.0f options:UIViewAnimationOptionCurveLinear animations:^{ ….. } completion:^(BOOL finished) { ….. //告诉系统动画结束 [self completeTransition]; }];}- (void)dismiss { ….. //发生动画 [UIView animateWithDuration:[self transitionDuration] delay:0.0f usingSpringWithDamping:0.8f initialSpringVelocity:1.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ ….. } completion:^(BOOL finished) { ….. //告诉系统动画结束 [self completeTransition]; }];}可自定义动画执行时间- (NSTimeInterval)transitionDuration{ return 0.6f;}支持左滑返回可以这样,在初始化时将其边界type修改为left- (instancetype)init{ self = [super init]; if (self) { self.edgeType = UIRectEdgeLeft; } return self;}步骤三、在push 或者 present之前为vc设置上对应的动画- (void)gotoWindoeModelVC{ WindowModelVC * vc = [WindowModelVC new]; WKWindowedModelAnimator * animator = [WKWindowedModelAnimator new]; animator.toViewHeight = SCREEN_HEIGHT / 2.f;// vc.wk_modelAnimator = animator;// [self.navigationController presentViewController:vc animated:YES completion:nil];//都可以啊 vc.wk_navAnimator = animator; [self.navigationController pushViewController:vc animated:YES];}效果展示在Demo中提供了一些常用的转场动画的封装,可直接使用。更多自定义的效果,可参考WKAnimatorManager的Demo。后记既然各位看到这了,再给大家分享一个小知识。在我实现自定义侧滑(UIScreenEdgePanGestureRecognizer)时,发现设置了其edges为UIRectEdgeLeft时,手势被触发的时机并不是在我进行侧滑时,而是在全屏任何一个地方都会触发这个手势。这一度对我造成了困扰。那么从触发手势开始思考,手势一开始于屏幕上触发(硬件层面),实质在这个层面是不会也不应该去做任何判断其手势的合法性,而是直接传递到系统由软件来判断其合法性,那么为什么我设置了UIRectEdgeLeft这个值之后,却没有达到预想的效果呢。带着这个疑问,我去拦截了系统的侧滑手势,发现其也是全屏可以被触发,只是一开始是UIGestureRecognizerStatePossible状态,然后如果其起始点超过50则变为了UIGestureRecognizerStateFailed状态,而小于等于50时则进入UIGestureRecognizerStateBegan状态,开始执行手势。那么接下来就简单了,我们只需要实现下面一个代理方法即可实现完全和系统一模一样的侧滑手势了。//系统左滑返回x实现方式- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{ if (gestureRecognizer == self.edgePan) { UIScreenEdgePanGestureRecognizer * pan = (UIScreenEdgePanGestureRecognizer *)gestureRecognizer; CGPoint point = [pan locationInView:self.edgePanVC.view]; return (point.x < 50); } return NO;} ...

December 21, 2018 · 1 min · jiezi

【开源】合摩 WeexBox 正式发布

WeexBox一套简洁高效的APP混合开发解决方案写在开头一提到 Weex,相信下面已经有一群小伙伴在哀嚎了,是的,大多数开发者对 Weex 的感情是既爱又恨的。Weex 是优秀的跨平台框架,「Write Once, Run Everywhere」,但坑也多的不要不要的,特别对于刚开始尝试 Weex 开发的团队来说,各种坑和不友好把许多人劝退了。首先聊聊我们为什么选择weex,在我们做技术选型时,综合考虑了目前市面上比较流行的RN、Weex,最终我们选择了 Weex。在决定使用哪种技术前,我们也在不断的问自己,为什么,为什么,为什么?在综合考虑各种因素后,我们技术团队决定使用weex。决定使用哪种技术,主要看其优缺点:其优点来验证,是否符合我们的业务场景;其缺点来判断,是否限制我们的业务场景,是否有方案避开这种限制。使用weex的优点:Weex 使用同一套代码来构建 Android、iOS 和 Web 应用。Weex 能用 Vue 的前端框架,贴近我们的技术栈Weex 比 RN 更轻量,体积小巧,可以分包,每个页面一个实例,性能更好学习成本低,上手快有良好的扩展性,比较好扩展新的 Component 和 Module同时,它也因资料少,社区活跃度不够,相对的坑较多,被大家一直诟病。我们收集了大家在开发中遇见的问题,针对这些问题,我们开发了WeexBox框架,并致力于:扩展 weex 的能力把最佳实践带入进来,提供大前端正确拥抱的姿势开发一些实用工具,带来更棒的开发体验填掉 weex 的坑最终,开发者能够专注写bug了~~~WeexBox 的特色零配置,开箱即用的项目,自带最佳实践无需安装 weex-toolkit(有多少汪在这一步安装不上、运行报错的。来,举个爪)比 weex-debugger、weex-builder 更快的构建速度支持 sass、es6、file-loader、uglify、eslint等可通过审核的热更新,静默模式和强制模式随意切换N 多实用的 Module 扩展合摩大前端团队实现了app开发从0到1,9天上线的壮举!可见 WeexBox 能够带来开发效率的巨大提升。快速上手@weexbox/cli 帮助你快速初始化工程项目。# 安装cnpm i -g @weexbox/cli# 新建一个weex工程weexbox create projectName# 进入工程cd projectName# 安装依赖cnpm i# 随后,可以愉快的写bug了…项目结构.├── config // 配置文件夹│ ├── update-config.json // 热更新的配置文件│ └── weexbox-config.js // 图片资源的配置文件├── deploy // 输出文件夹├── platforms // 原生文件夹│ ├── android // Android工程│ └── ios // iOS工程├── src // vue源码文件夹│ └── module // 模块文件夹│ ── page // 页面文件夹│ ├── App.vue // vue源码│ └── index.js // 入口文件├── static // 图片资源文件夹└── package.json // 配置文件安装依赖后,项目的结构如上,同时也搭建了app 的基础架构;在工程 platforms 文件夹中,会看到两个文件夹 android 、ios,Android 端使用 Android Studio 开发工具,导入 platforms/android 文件夹,构建打包生成项目的apk;iOS 端使用 Xcode 开发工具,导入 platforms/ios 文件夹,构建打包生成项目的ipa;随后,在src下建立业务模块,就可以愉快的开发了~这时你可能又有疑问了?本地书写的代码,如何能及时的展示在app界面上呢,不可能要每次打包吧,这样的话,也太LOW了!对的,不需要,这时你需要进入 Debug 调试 中了。Debug 调试Tips: 确保电脑与手机处于同一网段.1、调试打包在真机上的代码npm run debug打开app的调试扫码工具,扫二维码使pc与移动终端建立连接2、调试正在开发的页面npm run debug [vue/weex页面的路径]打开app的调试扫码工具,扫二维码使pc与移动终端建立连接更多详细的 debug 步骤请查看,WeexBox 开发指南中的 Debug 调试WeexBox 也提供了很多常见的模块。如何使用呢?1、丰富的 modal 模块modal 模块,除了常见的:alert、confirm外,还延伸了一些更频繁使用的api,eg:actionSheet(操作表弹框)、showLoading(显示菊花)等,更加常态化、大众化以及多元化。# 引用const modal = weex.requireModule(‘wb-modal’)# 警告弹框modal.alert({ title: ‘标题’, message: ‘弹窗内容’, okTitle: ‘确定’}, (result) => {})// callback参数result: { status: 0}效果图:# 引用const modal = weex.requireModule(‘wb-modal’)# 操作表弹窗,配合 wb-external 可调取摄像头及相册modal.actionSheet({ title: ‘标题’, message: ‘弹窗内容’, actions: [{ // 按钮类型’danger’, ‘cancel’, ’normal’。默认normal type: ‘danger’, // 按钮的标题 title: ‘删除’ }]}, (result) => {})// callback参数result: { // 取消按钮-1,其他0 status: 0, data: { // 按钮的索引 index: 0 }}效果图:等等功能,更多常见的modal api,详情请查看 传送门2、 打开外部功能 Module# 引用const external = weex.requireModule(‘wb-external’)# 调用摄像头拍照,实现图片裁剪上传external.openCamera({ // 能否剪裁 enableCrop: true, // 是否矩形剪裁,true为圆形剪裁 isCircle: true, // 宽度 width: 100, // 高度 height:100}, (result) => {})// callback参数result: { status: 0, error: ‘’, data: { // 图片的存储路径 url: ‘/docment/123.png’ }}等等功能,更多常见的external api,详情请查看 传送门写在最后目前 weex 官方也在不断的更新,虽然有各种bug被人诟病,但是,哪个优秀的技术发展没有经历这样的过程呢,“不经一番寒彻骨,怎得梅花扑鼻香”,技术本身就没有对错,只有我们调整好自己的心态,挖掘底层事物,垫好自己的基石,让技术更好的为业务服务。项目团队通过大量的业务实践和积累以后,总结归纳出这套基于 weex 的技术方案 WeexBox 并开源,解决大家在使用 weex 所遇见的各种坑,同时对官方的 Module 进行拓展、延伸,提供了更加丰富的模块,解决实际业务场景中的问题。目前团队使用 WeexBox 已研发了好几款APP,它能满足及支撑我们上百个页面的业务场景,让我们的开发效率大大提升,使我们的技术栈更加完善。在使用中有任何问题,欢迎给我们issue,有任何想法也欢迎PR。最后希望我们的方案能帮助开发中的你。官网地址:https://aygtech.github.io/wee…github: https://github.com/aygtech/ay…附上一份完整功能列表 ...

December 20, 2018 · 2 min · jiezi

探索新零售时代背后的技术变革

随着线下场景布局的不断发展,以及线上技术的持续推进,一个真正属于新零售的时代已经来临。走完了广州、成都、北京、深圳等四大城市后,个推技术沙龙TechDay于上海完美收官。来自京东到家、个推、亿咖通、Pinlan的技术大咖们,在上海站的现场,为大家解析并展示了新零售时代下的新技术。《大数据时代,用户画像实践与应用》叶政君 个推大数据分析师用户画像,即用户信息的标签化,本质上来说,用户画像是数据的标签化。常见的用户画像体系有三种:结构化体系、非结构化体系和半结构化体系。非结构化体系没有明显的层级,较为独立。半结构化层次有一定的层级概念,但是没有过于严格的依赖关系。在电商行业中,较多的企业会选择半结构化的用户画像体系进行应用。以一个简单的三级结构化标签为例,一级标签有基本属性和兴趣偏好,并且由此可以延伸至二级标签和三级标签,具体到哪些属性、兴趣。在画像建设方面,开发者们可以参考一些通用的做法,如标签体系设计、基础数据收集和多数据源数据融合、实现用户统一标识、构建用户画像特征层、画像标签规则+算法建模、对所有用户进行算法打标签和画像质量监控等。个推用户画像构建的整体流程,可以分为三个部分,第一,基础数据处理。基础数据包括设备信息、线上APP偏好数据和线下场景数据。第二,画像中间数据处理。处理结果包括线上APP偏好特征和线下场景特征等。第三,画像信息表。表中应有四种信息:设备基础属性、用户基础画像、用户兴趣画像和用户其它画像。同时,用户画像的构建需要技术和业务人员的共同参与,避免形式化的用户画像。在进行用户画像构建的过程中,个推主要运用到的技术有数据存储、实时计算、机器学习和深度学习。而用户画像的应用则包括:精准营销、用户分析、数据挖掘、服务产品、行业报告以及用户研究。针对新零售时代下,APP的运营者对于用户画像的需求,个推依托多年推送服务的积累和强大的大数据能力,推出了用户画像产品“个像”,为APP开发者提供丰富的用户画像数据以及实时的场景识别能力。同时,个推独有的冷、热、温数据标签,也可以有效分析用户的线上线下行为,挖掘用户特征,助力APP运营者筛选目标人群。同时,个推还可以为APP提供定制化标签,满足APP运营者在用户数字化管理方面的需求。在实践中,定制化标签的整合也有一定的难度,个推会结合双方的数据,对其进行建模分析,输出定制化标签。总的来说,个推不仅有通用的标签维度,也有定制化标签的输出能力。举两个用户画像在个推业务中的典型应用:第一,精准推荐,APP的运营者可以通过个像提供的性别、年龄层次、兴趣爱好等标签,分别展示不同的内容给用户,以达到精准化运营,千人千面。第二,用户聚类,处理客户提供的用户数据,补全用户画像,最终进行用户聚类分析。《即时物流场景下的机器学习实践》庄学坤 达达-京东到家 物流算法团队Leader即时物流作为新零售的“水电煤”,在新零售模式中处于基础核心环节,解决的是商品的配送效率问题。达达-京东到家作为国内即时物流的领先平台,在这方面进行了大量的技术探索与积累。与传统物流模式相比,即时物流场景下的配送具有更高的复杂度,具体表现为以下四点:1. 订单类型多样化;2. 时效性要求更高;3. 配送骑士的运力难以掌控;4.送达目的地复杂多样。而即时物流形式中存在的问题和挑战,也可以总结为四个部分:高度动态的物流订单、配送成本的动态性、订单派发需要兼顾公平与效率、骑士自由抢单的管理。现如今,新的算法模型层出不穷,算法可以选择的自由度较高。但是在实践中,数据决定了机器学习的上限,而算法只是尽可能逼近这个上限。一个成功而实用的算法体系,必须非常重视特征工程。开发者研发一套优秀算法体系的前提,是获取到优质的、具有精确特征的数据。达达结合自身的配送场景,积累了海量而精确的配送场景特征集合。没有最好的算法,只有某种场景下最合适的算法。在获取到特征数据之后,即时物流场景所应用的机器学习体系可以分为四层:基础数据层、特征工程层、算法模型层、业务应用层。开发团队还需要根据业务的应用场景,对不同的算法模型进行技术选型,比如线性模型擅长处理高维微观特征,非线性模型则擅长处理较低维的宏观特征,而在路径规划与调度当中,传统的运筹学模型动态规划等可能更加合适。最后,更加通用的AI可以由两部分组成,第一部分是深度学习(DeepLearning),解决端对端的学习问题;第二部分称之为强化学习(Reinforcement Learning),允许更加通用的学习架构。如果这两个部分结合到一起,就可以变为一个非常通用的学习算法。达达智能供需调控系统的设计中,借鉴了AlphaGo的思路,充分发挥了这种模式的优势,使得调控效率的效果和自动化程度同时获得了大幅提升。《使用智能对话机器人增强新零售服务链》孔晓泉 吉利集团Ecarx算法专家与以往的零售方式不同的地方在于,新零售的过程中,没有商超反馈和中间链条,企业需要直达顾客。这会使得一个to C企业,在客服和相关支持等方面,花费更多的成本,并且承担很大的压力。而使用智能对话机器人提供新零售的服务链,则可以减少用户的等待时间,提高用户体验,并且极大地减低公司的客服成本。智能对话机器人应用最多的领域是在线客服,其次是智能问答,如智能医疗等。从技术角度来说,人机对话的流程是:语音识别(ASR)、基于文本的方式进行自然语言理解(NLU)、通过理解到的意图或实体进行对话管理(DM)、自然语言生成(NLG)和语音合成(TTS)。企业可以选择Rasa Stack作为构建智能对话机器人的基础,它是一款开源的、基于机器学习的、为开发者和公司设计的机器人,智能性较高。由于对话机器人的软件开发难度很高,自然语言的理解需要很多组件的配合,而Rasa Stack的优势是完全的数据控制、自行扩充、自定义模型和完全的自驱动,并且其背靠德国的Rasa Technologies GmbH,有一定的发展保障。Rasa NLU能够提取用户的意图和相关的实体,这相当于把用户千奇百怪的、非结构化的、长短不一的数据转化成结构化数据。Rasa NLU的特色是基于 pipeline 的工作模式,扩展能力强,并且支持多种语言,如英语、德语、中文、日文等,RASA NLU还内置多种算法和配置,如MITIE、CRF、Embedding等。RASA Core则是一个对话管理体系,如下图所示,图中的每一个箭头都代表数据的流动。 Rasa Core的特性是数据驱动、扩展能力强、支持多种Policy协同工作、内置多种算法和配置,并且支持Interactive learning.实际上,强化学习不仅是一种框架,它还提供了算法和配置,但是具体的做法和参数调节,还需要在实践场景下进行确定。另外,交互式学习能够很快地测试到,用户所得到的回复是否正确,并在错误的情况下,进行相应的更改。《AI赋能营销数字转化转型》李泽洲 Pinlan联合创始人兼CTO当前,线下营销正在从以零售商为中心,转化成以购物者为中心的形式。而在这其中,机器视觉的落地,也对整个零售行业的转变起到了很大的推动作用。计算机视觉是一个跨学科的领域,涉及到如何使计算机从数字图像或视频中,获得高层次的理解。硬件和算法的进步均催生了大量计算机视觉的应用落地。在深度学习进入到计算机视觉的领域之前,计算机视觉技术主要被应用于图像处理、特征检测和匹配以及运动估计。随着深度学习网络的发展,传统的神经网络很难被单纯地应用到计算机视觉领域。图像的纬度很大,而人的观察方式是对图像当中的某个局部信息,进行认真的观察之后,才会逐渐地观察到全局信息。机器学习的流程是数据采集、数据预处理、模型训练、模型测试和模型服务。其中,零售行业的零售商更关心的是,SKU在超市中铺货时,是如何摆放的。线下零售商有两种方式可以进行数据采集。方式一是利用手持终端(SFA)采集数据; 方式二是在超市中架设摄像头,进行固定场景的拍摄。方式二相较于方式一,有一定的优势存在,如可选择高像素摄像头,图像质量高;固定场景下的拍摄,变化较小;数据可用性高;模型可以确保细粒度商品的识别。在数据预处理阶段,也有两种方式可以对已采集的图像数据信息进行处理,方式一是提高图像质量,如调整亮度、对比度,对图像进行去模糊、超分辨率重建等。方式二是训练图像增强,在AI的实现过程中,在训练CNN网络之前,对数据进行增强是一个非常重要的环节。一般情况下,现实场景中所能收集到的数据量不是很大,这对于深度学习来说是一个致命的问题,这时便可以利用图像增强或者图像数据的扩充,增加数据量,如图像裁剪、图像对比度变化、图像亮度变化和图像微旋转。在商品检测模型训练的阶段,目前前沿的算法框架包括 Faster-RCNN、RetinaNet和YOLO等。通用商品检测模型能够支持海量的多种包装类别实际场景数据,可以针对大小目标和不同包装类别,进行大类拆分,并且能够优化模型结构,增强场景适应性。同时,通用商品检测模型可以实现移动端压缩,支持移动端检测。而细粒度商品识别模型训练则需要先收集海量SKU数据,建立商品数据库,之后结合注意力机制,训练细粒度识别模型,然后在真实场景验证模型效果。在实践中,Pinlan的细粒度商品识别模型,已经能够使自然场景商品识别准确率提升至97%.建立检测模型和识别模型之后,开发者可以将两者进行结合,进行线下零售的智能陈列分析,如陈列位置检查、数量检查、陈列规范检查和陈列推荐。以数据驱动的新零售时代已经来临,面临零售场所和消费观念的转变,传统零售需要整合和重组,充分地利用电子商务、大数据云平台、移动互联网和人工智能等技术,让线上线下一体化成为可能。

December 20, 2018 · 1 min · jiezi

Flutter 插件开发:以微信SDK为例

就像 React Native 一样,在 Flutter 应用中,如果需要调用第三方库的方法或者有一些功能需要使用原生的开发来提供,使用 Flutter Plugin 是一种不错的方式,它本质上就是一个 Dart Package,但与其它的 package 不同点在于,Flutter 插件中一般都存在两个特殊的文件夹:android 与 ios,如果需要编写Java、Kotlin或者 Object-C 以及 Swift 代码,我们就需要在这两个文件夹项目中进行,然后通过相应的方法将原生代码中开发的方法映射到 dart 中。本文以开发一个微信插件为例,为Flutter应用提供微信分享、登录、支付等功能,项目代码可以直接在下方找到,也已经提交至Pub库:原文地址:https://pantao.onmr.com/press/flutter-wechat-plugin.htmlPub库:https://pub.dartlang.org/packages/wechat项目地址:https://github.com/pantao/flutter-wechat创建插件目录要开发插件,可以使用下面的代码快速基于 plugin 模板开始:flutter create –template=plugin wechat上面的代码中,表示以 plugin 模板创建一个名为 wechat 的 package,创建完成之后,整个项目的目录结构就都提供好了,并且官方还提供了一些基本开发示例。目录结构- android // Android 相关原生代码目录- ios // ios 相关原生代码目录- lib // Dart 代码目录- example // 一个完整的调用了我们正在开发的插件的 Flutter App- pubspec.yaml // 项目配置文件从 example/lib/main.dart 开始在开发我们的应用之后,先来了解一下 flutter 为我们生成的文件们,打开 example/lib/main.dart,代码如下:import ‘package:flutter/material.dart’;import ‘dart:async’;import ‘package:flutter/services.dart’;import ‘package:wechat/wechat.dart’;void main() => runApp(MyApp());class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState();}class _MyAppState extends State<MyApp> { String _platformVersion = ‘Unknown’; @override void initState() { super.initState(); initPlatformState(); } // Platform messages are asynchronous, so we initialize in an async method. Future<void> initPlatformState() async { String platformVersion; // Platform messages may fail, so we use a try/catch PlatformException. try { platformVersion = await Wechat.platformVersion; } on PlatformException { platformVersion = ‘Failed to get platform version.’; } // If the widget was removed from the tree while the asynchronous platform // message was in flight, we want to discard the reply rather than calling // setState to update our non-existent appearance. if (!mounted) return; setState(() { _platformVersion = platformVersion; }); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text(‘Plugin example app’), ), body: Center( child: Text(‘Running on: $_platformVersion\n’), ), ), ); }}这里需要特别注意的就是 initPlatformState() 方法中对 Wechat.platformVersion 的调用,这里面的 Wechat 就是我们的插件,platformVersion 就是插件提供的 get 方法,跟着这个文件,找到 lib/wechat.dart 文件,代码如下:import ‘dart:async’;import ‘package:flutter/services.dart’;class Wechat { static const MethodChannel _channel = const MethodChannel(‘wechat’); static Future<String> get platformVersion async { final String version = await _channel.invokeMethod(‘getPlatformVersion’); return version; }}在该文件中,可以看到 class Wechat 定义了一个 get 方法 platformVersion,它的函数体有点特别:final String version = await _channel.invokeMethod(‘getPlatformVersion’);return version;我们的 version 是通过 _channel.invokeMethod(‘getPlatformVersion’) 方法的调用得到的,这个 _channel 就是我们 Dart 代码与 原生代码进行通信的桥了,也是 Flutter 原生插件的核心(当然,如果你编写的插件并不需要原生代码相关的功能,那么,_channel 就是可有可无的了,比如我们可以写一个下面这样的方法,返回 两个数字 a 与 b 的和:class Wechat { … static int calculate (int a, int b) { return a + b; }}之后,修改 example/lib/main.dart 代码:class _MyAppState extends State<MyApp> { String _platformVersion = ‘Unknown’; // 定义一个 int 型变量,用于保存计算结果 int _calculateResult; @override void initState() { super.initState(); initPlatformState(); } Future<void> initPlatformState() async { String platformVersion; try { platformVersion = await Wechat.platformVersion; } on PlatformException { platformVersion = ‘Failed to get platform version.’; } if (!mounted) return; // init 的时候,计算一下 10 + 10 的结果 _calculateResult = Wechat.calculate(10, 10); setState(() { _platformVersion = platformVersion; }); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text(‘Plugin example app’), ), body: Container( padding: EdgeInsets.all(16.0), child: SingleChildScrollView( child: Column( children: <Widget>[ Text(‘Running on: $_platformVersion\n’), // 输出该结果 Text(‘Calculate Result: $_calculateResult\n’), ], ), ), ), ), ); }}支持原生编码提供的方法很多时候,写插件,更多的是因为我们需要让应用能够调用原生代码提供的方法,怎么做呢?Android 系统打开 android/src/main/java/com/example/wechat/WechatPlugin.java 文件,看如下代码:package com.example.wechat;import io.flutter.plugin.common.MethodCall;import io.flutter.plugin.common.MethodChannel;import io.flutter.plugin.common.MethodChannel.MethodCallHandler;import io.flutter.plugin.common.MethodChannel.Result;import io.flutter.plugin.common.PluginRegistry.Registrar;/** WechatPlugin /public class WechatPlugin implements MethodCallHandler { /* Plugin registration. / public static void registerWith(Registrar registrar) { final MethodChannel channel = new MethodChannel(registrar.messenger(), “wechat”); channel.setMethodCallHandler(new WechatPlugin()); } @Override public void onMethodCall(MethodCall call, Result result) { if (call.method.equals(“getPlatformVersion”)) { result.success(“Android " + android.os.Build.VERSION.RELEASE); } else { result.notImplemented(); } }}还记得上面提到的 getPlatformVersion 吗?还记得 _channel 那么,是不是在这里面也看到的对应的存在?没错, dart 中的 getPlatformVersion 通过 _channel.invokeMethod 发起一次请求,然后,Java 代码中的 onMethodCall 方法回被调用,该方法有两个参数:MethodCall call:请求本身Result result:结果处理方法然后通过 call.method 可以知到 _channel.invokeMethod 中的方法名,然后通过 result.success 回调返回成功结果响应。registerWith在上面还有一小段代码 registerWith,可以看到里面有一个调用:final MethodChannel channel = new MethodChannel(registrar.messenger(), “wechat”);channel.setMethodCallHandler(new WechatPlugin());这里就是在注册我们的插件,将 wechat 注册成为我们的 channel 名,这样,才不会调用 alipay 插件的调用最后到了 wechat 插件这里。iOS 系统同样的,这次我们打开 ios/Classes/WechatPlugin.m 文件:#import “WechatPlugin.h”@implementation WechatPlugin+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>)registrar { FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@“wechat” binaryMessenger:[registrar messenger]]; WechatPlugin* instance = [[WechatPlugin alloc] init]; [registrar addMethodCallDelegate:instance channel:channel];}- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([@“getPlatformVersion” isEqualToString:call.method]) { result([@“iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); } else { result(FlutterMethodNotImplemented); }}@end虽然语法有所不同,但是,可以看到,跟 android 的 Java 代码结构上几乎是一模一样的,首先 register 一个名为 wechat 的 channel,然后去 handleMethodCall,同样的通过 call.method 拿到方法名,通过 result 做出响应。小试牛刀接下来,我们将前面的 caculate 方法,移到原生代码中来提供(虽然这很没必要,但毕竟,只是为了演示嘛)。Android在前面打开的 android/src/main/java/com/example/wechat/WechatPlugin.java 文件中,修改 onMethodCall 方法: @Override public void onMethodCall(MethodCall call, Result result) { if (call.method.equals(“getPlatformVersion”)) { result.success(“Android " + android.os.Build.VERSION.RELEASE); } else if (call.method.equals(“calculate”)) { int a = call.argument(“a”); int b = call.argument(“b”); int r = a + b; result.success(”” + r); } else { result.notImplemented(); } }添加了 call.method.equals(“calculate”) 判断,这里面具体的过程是:调用 call.argument() 方法,可以取得由 wechat.dart 传递过来的参数计算结果调用 result.success() 响应结果然后,我们需要在 lib/wechat.dart 中修改 calculate 方法的实现,代码如下: static Future<int> calculate (int a, int b) async { final String result = await _channel.invokeMethod(‘calculate’, { ‘a’: a, ‘b’: b }); return int.parse(result); }由于 _channel.invokeMethod 是一个异步操作,所以,我们需要将 calculate 的返回类型修改为 Future,同时加上 async,此时我们就可以直接使用 await 关键字了,跟 JavaScript 中的 await 一样,让我们用同步的方式编写异步代码,在新版的 calculate 代码中,我们并没有直接计算 a+b 的结果,而是调用 _channel.invokeMethod 方法,将 a 与 b 传递给了 Java 端的 onMethodCall 方法,然后返回该方法返回的结果。_channel.invokeMethod该方法接受两个参数,第一个定义一个方法名,它是一个标识,简单来说,它告诉原生端的代码,我们这次是要干什么,第二个参数是一个 Map<String, dynamic> 型数据,是参数列表,我们可以在原生代码中获取到。接着,我们需要更新一下对该方法的调用了,回到 example/lib/main.dart 中,修改成如下调用:_calculateResult = await Wechat.calculate(10, 10);因为我们现在的 calculate 方法已经是一个异步方法了。iOS如果我们的插件需要支持 Android 与 IOS 两端,那么需要同步的在 ios 中实现上面的方法,打开 ios/Classes/WechatPlugin.m 文件,作如下修改:- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { NSDictionary arguments = [call arguments]; if ([@“getPlatformVersion” isEqualToString:call.method]) { result([@“iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); } else if ([@“calculate” isEqualToString:call.method]) { NSInteger a = [arguments[@“a”] intValue]; NSInteger b = [arguments[@“b”] intValue]; result([NSString stringWithFormat:@"%d”, a + b]); } else { result(FlutterMethodNotImplemented); }}实现过程与 java 端保持一致即可。添加第三方 SDK我们的插件是可以提供微信的分享相关功能的,所以,肯定需要用到第三方SDK,还是从 Android 开始。Android 端 WechatSDK按 官方接入指南 所述,我们需要添加依赖:dependencies { compile ‘com.tencent.mm.opensdk:wechat-sdk-android-with-mta:+’}或dependencies { compile ‘com.tencent.mm.opensdk:wechat-sdk-android-without-mta:+’}前者带有统计功能,这很简单,打开 android/build.gradle 文件 ,在最下方粘贴以上片段即可:…android { compileSdkVersion 27 defaultConfig { minSdkVersion 16 testInstrumentationRunner “android.support.test.runner.AndroidJUnitRunner” } lintOptions { disable ‘InvalidPackage’ }}dependencies { compile ‘com.tencent.mm.opensdk:wechat-sdk-android-with-mta:+’}然后,回到 WechatPlugin.java 文件,先添加一个 register 方法,它将我们的Appid 注册给微信,还是接着前面的 onMethodCall 中的 if 判断:…import com.tencent.mm.opensdk.openapi.WXAPIFactory;… else if (call.method.equals(“register”)) { appid = call.argument(“appid”); api = WXAPIFactory.createWXAPI(context, appid, true); result.success(api.registerApp(appid)); }…然后回到 lib/wechat.dart 添加相应调用:… /// Register app to Wechat with [appid] static Future<dynamic> register(String appid) async { var result = await _channel.invokeMethod( ‘register’, { ‘appid’: appid } ); return result; }…此时,在我们的 example 应该中,就可以调用 Wechat.register 方法,来注册应用了ios按照官方 ios 接入指南所述,我们可以通过 pod 添加依赖:pod ‘WechatOpenSDK’打开 ios/wechat.podspec ,可以看到如下内容:## To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html#Pod::Spec.new do |s| s.name = ‘wechat’ s.version = ‘0.0.1’ s.summary = ‘A new flutter plugin project.’ s.description = <<-DESCA new flutter plugin project. DESC s.homepage = ‘http://example.com’ s.license = { :file => ‘../LICENSE’ } s.author = { ‘Your Company’ => ’email@example.com’ } s.source = { :path => ‘.’ } s.source_files = ‘Classes/**/’ s.public_header_files = ‘Classes//*.h’ s.dependency ‘Flutter’ s.ios.deployment_target = ‘8.0’end留意到数第三行的 s.dependency,这就是在指定我们依赖 Flutter,如果有其它依赖在这里添加一行即可:… s.public_header_files = ‘Classes//*.h’ s.dependency ‘Flutter’ s.dependency ‘WechatOpenSDK’ s.ios.deployment_target = ‘8.0’end然后打开 ios/Classes/WechatPlugin.h 文件,修改如下:#import <Flutter/Flutter.h>#include “WXApi.h”@interface WechatPlugin : NSObject<FlutterPlugin, WXApiDelegate>@end再回到 ios/Classes/WechatPlugin.m,接着前面的 if 条件继续添加判断:… // Register app to Wechat with appid else if ([@“register” isEqualToString:call.method]) { [WXApi registerApp:arguments[@“appid”]]; result(nil); }…此时,我们的插件已经支持微信 SDK 的 注册至微信 功能了,更多实现,本文就不再讨论,有兴趣,可以直接下载完整项目,后面都是大同小异的实现,唯一需要的是,你需要有一定的 Java 编码与 Objective-C 编码能力。 ...

December 20, 2018 · 5 min · jiezi

iOS分段选择器、旅行App、标度尺、对对碰小游戏、自定义相册等源码

iOS精选源码企业级开源项目,模仿艺龙旅行App标签选择器–LeeTagViewCSSegmentedControl常用的分段选择器,简单易用!仿微信左滑删除IOS左滑返回输入框iOS 基于PhotoKit框架的自定义相册JHDraw - 画线,画虚线,画五角星,画矩形,画虚线矩形标度尺对对碰小游戏iOS优质博客iOS无痕埋点方案分享探究前言当前互联网行业的竞争已经是非常激烈了, “功能驱动”的时代已经过去了, 现在更加注重软件的细节, 以及用户的体验问题。 说到用户体验,就不得不提到用户的操作行为。 在我们的软件中,我们会到处进行埋点, 以便提取到我们想要的数据,进而分析用户的行为习惯。 通过这些数据,我们也可以更好的分析出用户的操作趋势,从而在用户… 阅读原文Python的iOS自动化打包前言这段时间刚刚学习了一段时间的Python,加上自己是做iOS开发的,就想着用Python来做一个自动化打包,可以自动完成打包,上传到蒲公英,并且发送邮箱给测试人员.一是可以减少打包功夫,二来可以练练手,结合自己的工作来输出一点东西.废话不多说,直接上代码…原理就是使用xcodebuild来控制Xcode进行一系… 阅读原文Swift 中的类型擦除你可能听过这个术语 :类型擦除。甚至你也用过标准库中的类型擦除(AnySequence)。但是具体什么是类型擦除, 我们怎么才能实现类型擦除呢?这篇文章就是介绍这件事情的。在日常的开发中, 总有想要把某个类或者是某些实现细节对其他模块隐藏起来, 不然总会感觉这些类在项目里到处都是。或者想要实现两个不同类之间的互相转换。… 阅读原文一道值得思考的iOS面试题前言最近在群里看到有人发的一道面试题,题目如下:@interface Spark : NSObject @property(nonatomic,copy) NSString *name; @end@implementation Spark-… 阅读原文浅谈iOS之weak底层实现原理前言在iOS开发过程中,会经常使用到一个修饰词“weak”,使用场景大家都比较清晰,用于一些对象相互引用的时候,避免出现强强引用,对象不能被释放,出现内存泄露的问题。weak 关键字的作用弱引用,所引用对象的计数器不会加一,并在引用对象被释放的时候自动被设置为 nil。weak底层原理1.weak编译解析首先需要看一下… 阅读原文更多源码更多博文

December 19, 2018 · 1 min · jiezi

AI如何赋能传统的物业公司?

近年来,人工智能在各国发展十分迅速,将成为第四次工业革命的核心技术驱动力。传统的物业公司借助人工智能这班车,产业升级指日可待。作为传统服务型的物业公司,面临着许多现实问题。管理模式陈旧和硬件设备落后,导致经营成本过高,服务品质提升受限。随着互联网应用的普及和人工智能时代的来临,传统企业如何才能完美蜕变,实现创新转型呢?用工成本逐年上升,是很多物业公司较为头疼的事情。选择合适的智能化产品来替代人工作业将成为必然趋势,比如:用无人驾驶扫地机器人代替传统的保洁员;用智能高楼外墙清洗机器人代替高危险的蜘蛛人。相比较来说,智能机器人不仅管理方便,操作简单,而且作业效率和质量也明显优于人工。在企业经营中融入科技元素,将会给企业发展带来不错的机遇。“智能清洁”的概念从20世纪末进入我国,经过近20年的发展,但在商业实践方面我国与欧美国家相比还差很多。发达国家的智能清洁已经发展到了细化分工并依靠智能清洁设备实现多场景应用的层面了,而我国的绝大多数物业公司还是比较传统的雇佣人工的形式,差距在于智能清洁技术的应用及普及上。随着中国人口红利的褪去,基于雇佣大量廉价劳动力的“人海战术”已经不能适用于现在的服务业了。现在的服务业已经呈现出了新的发展趋势,客户的服务需求量成倍增长,服务质量和服务体验要求越来越讲究品质。现实的情况也催促着物业公司改变服务模式,运用智能清洁思维可以使企业在人力结构优化和服务标准化方面更具有优势。在日常生活中,物业公司时常被舆论媒体妖魔化。看到社区业主论坛上多是对物业公司的批评和谩骂。一个物业公司要应付全小区的众多业主,确实免不了众口难调。但是物业和业主之间就只能是对立关系吗?物业公司失去了业主,就等于失去了市场,失去了立身之地;业主离开物业公司,就相当于花了几代人的血本买了一栋居住不舒适的房子。这么看来,物业和业主之间恰似鱼和水的关系,相伴相生。随着社会的发展,人们的生活发生着巨大的改变,物业管理领域值得有更深层面的思考。中国有13.7万家物业公司,如何运用互联网的方式更好地服务于业主?如何借助人工智能浪潮赋能物业公司?在这些领域都将催生巨大的商业机会,当然也将面临不小的挑战。在可预见的未来,传统的物业管理公司很可能将不复存在,取而代之的是以业主服务为核心的物业服务公司,这将成为主流。建立智能化、标准化的服务社区,让业主充分体验到物业公司转型后带来的品质居住享受,从而赋能物业公司,提高其在行业内的影响力和话语权。

December 19, 2018 · 1 min · jiezi

《Flutter实战》中文原创书籍开源

《Flutter实战》 为Flutter中文网开源电子书项目,本书系统介绍了Flutter各个方面,是第一本中文原创Flutter技术书籍:在线阅读地址:https://book.flutterchina.club《Flutter实战》部分目录缘起起步移动开发技术简介Flutter简介搭建Flutter开发环境Dart语言简介第一个Flutter应用计数器示例路由管理包管理资源管理调试Flutter APP基础WidgetsWidget简介文本、字体样式按钮图片和Icon单选框和复选框输入框和表单布局类Widgets布局类Widgets简介线性布局Row、Column弹性布局Flex流式布局Wrap、Flow层叠布局Stack、Positioned容器类WidgetsPadding布局限制类容器ConstrainedBox、SizeBox装饰容器DecoratedBox变换TransformContainer容器可滚动Widgets可滚动Widgets简介SingleChildScrollViewListViewGridViewCustomScrollView滚动监听及控制ScrollController功能型Widgets导航返回拦截-WillPopScope数据共享-InheritedWidget主题-Theme事件处理与通知原始指针事件处理手势识别全局事件总线通知Notification动画Flutter动画简介动画结构自定义路由过渡动画Hero动画交错动画自定义Widget自定义Widget方法简介通过组合现有Widget实现实例:TurnBoxCustomPaint与Canvas实例:圆形渐变进度条(自绘)文件操作与网络请求文件操作Http请求-HttpClientHttp请求-Dio packageWebSocket使用Socket APIJson转Model包与插件开发package插件开发:平台通道简介插件开发:实现Android端API插件开发:实现IOS端API系统能力调用国际化让App支持多语言实现Localizations使用Intl包更多内容,请移步《Flutter实战》

December 18, 2018 · 1 min · jiezi

历途机器人首次商业清洗告捷!

一年最热七八月,猛地从空调房出去,身体还真的不太适应。伴着湛蓝的天空、火热的烈日和偶遇的微风,户外还真像一个大蒸笼。就在8月份左右小编发现这栋楼和以前不一样了……别误会,不是被晒化了,而是被清洗的焕然一新了!这次高楼外墙清洗采用的是历途公司研发的第六代高楼外墙清洗机器人产品。只见机器人在外墙清洗作业前,工程师将专用清洁剂按照比例稀释后装入机身两侧的水箱,然后将机器人吊出女儿墙,一键启动后,机器人便牢牢地吸附在蓝色的玻璃上,开始进行智能行走清洗了。机器人从高处缓缓的下移,经过之处焕然一新,左侧清洗过的墙面与右侧未清洗的玻璃形成鲜明对比。这款机器人清洗时的效率约每小时120平米,按照工作10小时计算,一天可以清洗1200平米的面积,而且工作一小时仅需0.5度电,用5升水就能清洗100平米的楼宇外立面,既安全高效,又节能环保。在炎热夏日,早上八九点就已经非常热了,用人工清洗确实存在诸多不便。“蜘蛛人”顶着30几度的高温,拿着刮板和抹布清洗墙面,衣服很快就被汗水浸透了,很容易引起中暑及其他安全事故。用机器人代替“蜘蛛人”进行清洗作业,就可以有效解决这个问题。机器人具有更好的耐高温性,在炎炎烈日下依然可以正常工作,清洁速度相比人工更快,清洁效果更好,而且工作起来还不会疲劳。工作人员仅需简单的操作,就可以坐等清洗完成了。远远看去,机器人就像在高楼外墙上移动的小方块,如果采用多机联合作业时,想必一定会成为城市的一道风景线。一个个小机器人就像是高楼外墙上跳动的音符,演奏着清洁魔术。历途机器人此次商业清洗大获成功,特别鸣谢中铁集团的鼎力支持!

December 17, 2018 · 1 min · jiezi

人工智能涉足清洁行业,将带来哪些机遇和挑战?

随着人工智能技术的发展,越来越多的行业都在向科技靠拢,AII IN AI将是更多公司的发展战略。去年11月,“第九届中国清洁环卫论坛”在京召开。清洁领域的专业人士和众多企业家都在积极探讨如何改善环境质量,如何加速企业转型升级,如何加速新商业模式在传统产业中的推广和应用。在这些新契机下,将会诞生更多的商业机会。Leatu-Robot据清研智库发布的《2016-2017年度商业清洁行业发展趋势研究报告》数据显示,预计到2021年,大清洁市场规模将达到1,505亿元,其中清洁服务约占67.6%;小清洁市场规模将达到488.2亿元,未来五年复合增长率有望达到9.6%。这无疑是给保洁公司和清洁设备供应商带来了利好消息。目前清洁行业市场正在逐步规范,管理体系日趋完善,对外交流频繁,理念及技术迅速提高。传统的清洁行业想要获得突破性发展,就要从人员服务品质上下功夫,进而逐步实现无人化服务,实现绿色创新发展。未来的竞争是客户服务体验的竞争。前几天有个客户向我抱怨,“在过去的20多年里,清洁设备一直没有质的改变,唯一变的就是国内品牌更多了,进口设备更便宜了”。听了他的话,我很欣慰,现在保洁公司正在期待像我们一样的机器人研发公司,为他们提供代替人工和节省人工的机器人产品,真正的解放劳动力,实现降本增效的目的。随着人工智能和大数据技术逐步渗透到各个行业,未来一定会淘汰更多的重复性工作人员,但同时也会增加更多具有技术含量的蓝领岗位。未来清洁行业的发展离不开人工智能的参与。现在人工智能技术还处在起步阶段,各科技公司的机器人产品研发成本还很高,但随着技术逐步成熟和普及,将会给清洁行业带来翻天覆地的变化。不断为客户提供高品质的专业服务是我们的愿景,诚信经营是核心。希望不久的将来能够看到越来越多的专业服务机器人走上工作岗位,越来越多的企业敢于创新,拥抱变革,共同促进科技发展,社会进步。

December 14, 2018 · 1 min · jiezi

iOS App冷启动治理:来自美团外卖的实践

一、背景冷启动时长是App性能的重要指标,作为用户体验的第一道“门”,直接决定着用户对App的第一印象。美团外卖iOS客户端从2013年11月开始,历经几十个版本的迭代开发,产品形态不断完善,业务功能日趋复杂;同时外卖App也已经由原来的独立业务App演进成为一个平台App,陆续接入了闪购、跑腿等其他新业务。因此,更多更复杂的工作需要在App冷启动的时候被完成,这给App的冷启动性能带来了挑战。对此,我们团队基于业务形态的变化和外卖App的特点,对冷启动进行了持续且有针对性的优化工作,目的就是为了呈现更加流畅的用户体验。二、冷启动定义一般而言,大家把iOS冷启动的过程定义为:从用户点击App图标开始到appDelegate didFinishLaunching方法执行完成为止。这个过程主要分为两个阶段:T1:main()函数之前,即操作系统加载App可执行文件到内存,然后执行一系列的加载&链接等工作,最后执行至App的main()函数。T2:main()函数之后,即从main()开始,到appDelegate的didFinishLaunchingWithOptions方法执行完毕。然而,当didFinishLaunchingWithOptions执行完成时,用户还没有看到App的主界面,也不能开始使用App。例如在外卖App中,App还需要做一些初始化工作,然后经历定位、首页请求、首页渲染等过程后,用户才能真正看到数据内容并开始使用,我们认为这个时候冷启动才算完成。我们把这个过程定义为T3。综上,外卖App把冷启动过程定义为:__从用户点击App图标开始到用户能看到App主界面内容为止这个过程,即T1+T2+T3。__在App冷启动过程当中,这三个阶段中的每个阶段都存在很多可以被优化的点。三、问题现状性能存量问题美团外卖iOS客户端经过几十个版本的迭代开发后,在冷启动过程中已经积累了若干性能问题,解决这些性能瓶颈是冷启动优化工作的首要目标,这些问题主要包括:注:启动项的定义,在App启动过程中需要被完成的某项工作,我们称之为一个启动项。例如某个SDK的初始化、某个功能的预加载等。性能增量问题一般情况下,在App早期阶段,冷启动不会有明显的性能问题。冷启动性能问题也不是在某个版本突然出现的,而是随着版本迭代,App功能越来越复杂,启动任务越来越多,冷启动时间也一点点延长。最后当我们注意到,并想要优化它的时候,这个问题已经变得很棘手了。外卖App的性能问题增量主要来自启动项的增加,随着版本迭代,启动项任务简单粗暴地堆积在启动流程中。如果每个版本冷启动时间增加0.1s,那么几个版本下来,冷启动时长就会明显增加很多。四、治理思路冷启动性能问题的治理目标主要有三个:解决存量问题:优化当前性能瓶颈点,优化启动流程,缩短冷启动时间。管控增量问题:冷启动流程规范化,通过代码范式和文档指导后续冷启动过程代码的维护,控制时间增量。完善监控:完善冷启动性能指标监控,收集更详细的数据,及时发现性能问题。五、规范启动流程截止至2017年底,美团外卖用户数已达2.5亿,而美团外卖App也已完成了从支撑单一业务的App到支持多业务的平台型App的演进(美团外卖iOS多端复用的推动、支撑与思考),公司的一些新兴业务也陆续集成到外卖App当中。下面是外卖App的架构图,外卖的架构主要分为三层,底层是基础组件层,中层是外卖平台层,平台层向下管理基础组件,向上为业务组件提供统一的适配接口,上层是基础组件层,包括外卖业务拆分的子业务组件(外卖App和美团App中的外卖频道可以复用子业务组件)和接入的其他非外卖业务。App的平台化为业务方提供了高效、标准的统一平台,但与此同时,平台化和业务的快速迭代也给冷启动带来了问题:现有的启动项堆积严重,拖慢启动速度。新的启动项缺乏添加范式,杂乱无章,修改风险大,难以阅读和维护。面对这个问题,我们首先梳理了目前启动流程中所有的启动项,然后针对App平台化设计了新的启动项管理方式:__分阶段启动和启动项自注册__分阶段启动早期由于业务比较简单,所有启动项都是不加以区分,简单地堆积到didFinishLaunchingWithOptions方法中,但随着业务的增加,越来越多的启动项代码堆积在一起,性能较差,代码臃肿而混乱。通过对SDK的梳理和分析,我们发现启动项也需要根据所完成的任务被分类,有些启动项是需要刚启动就执行的操作,如Crash监控、统计上报等,否则会导致信息收集的缺失;有些启动项需要在较早的时间节点完成,例如一些提供用户信息的SDK、定位功能的初始化、网络初始化等;有些启动项则可以被延迟执行,如一些自定义配置,一些业务服务的调用、支付SDK、地图SDK等。我们所做的分阶段启动,首先就是把启动流程合理地划分为若干个启动阶段,然后依据每个启动项所做的事情的优先级把它们分配到相应的启动阶段,优先级高的放在靠前的阶段,优先级低的放在靠后的阶段。下面是我们对美团外卖App启动阶段进行的重新定义,对所有启动项进行的梳理和重新分类,把它们对应到合理的启动阶段。这样做一方面可以推迟执行那些不必过早执行的启动项,缩短启动时间;另一方面,把启动项进行归类,方便后续的阅读和维护。然后把这些规则落地为启动项的维护文档,指导后续启动项的新增和维护。通过上面的工作,我们梳理出了十几个可以推迟执行的启动项,占所有启动项的30%左右,有效地优化了启动项所占的这部分冷启动时间。启动项自注册确定了启动项分阶段启动的方案后,我们面对的问题就是如何执行这些启动项。比较容易想到的方案是:在启动时创建一个启动管理器,然后读取所有启动项,然后当时间节点到来时由启动器触发启动项执行。这种方式存在两个问题:所有启动项都要预先写到一个文件中(在.m文件import,或用.plist文件组织),这种中心化的写法会导致臃肿的代码,难以阅读维护。启动项代码无法复用:启动项无法收敛到子业务库内部,在外卖App和美团App中要重复实现,和外卖App平台化的方向不符。而我们希望的方式是,启动项维护方式可插拔,启动项之间、业务模块之间不耦合,且一次实现可在两端复用。下图是我们采用的启动项管理方式,我们称之为启动项的自注册:一个启动项定义在子业务模块内部,被封装成一个方法,并且自声明启动阶段(例如一个启动项A,在独立App中可以声明为在willFinishLaunch阶段被执行,在美团App中则声明在resignActive阶段被执行)。这种方式下,启动项即实现了两端复用,不相关的启动项互相隔离,添加/删除启动项都更加方便。那么如何给一个启动项声明启动阶段?又如何在正确的时机触发启动项的执行呢?在代码上,一个启动项最终都会对应到一个函数的执行,所以在运行时只要能获取到函数的指针,就可以触发启动项。美团平台开发的组件启动治理基建Kylin正是这样做的:Kylin的核心思想就是在编译时把数据(如函数指针)写入到可执行文件的__DATA段中,运行时再从__DATA段取出数据进行相应的操作(调用函数)。为什么要用借用__DATA段呢?原因就是为了能够覆盖所有的启动阶段,例如main()之前的阶段。Kylin实现原理简述:Clang 提供了很多的编译器函数,它们可以完成不同的功能。其中一种就是 section() 函数,section()函数提供了二进制段的读写能力,它可以将一些编译期就可以确定的常量写入数据段。 在具体的实现中,主要分为编译期和运行时两个部分。在编译期,编译器会将标记了 attribute((section())) 的数据写到指定的数据段中,例如写一个{key(key代表不同的启动阶段), *pointer}对到数据段。到运行时,在合适的时间节点,在根据key读取出函数指针,完成函数的调用。上述方式,可以封装成一个宏,来达到代码的简化,以调用宏 KLN_STRINGS_EXPORT(“Key”, “Value”)为例,最终会被展开为:attribute((used, section("__DATA" "," "kylin"))) static const KLN_DATA __kylin__0 = (KLN_DATA){(KLN_DATA_HEADER){“Key”, KLN_STRING, KLN_IS_ARRAY}, “Value”};使用示例,编译器把启动项函数注册到启动阶段A:KLN_FUNCTIONS_EXPORT(STAGE_KEY_A)() { // 在a.m文件中,通过注册宏,把启动项A声明为在STAGE_KEY_A阶段执行 // 启动项代码A}KLN_FUNCTIONS_EXPORT(STAGE_KEY_A)() { // 在b.m文件中,把启动项B声明为在STAGE_KEY_A阶段执行 // 启动项代码B}在启动流程中,在启动阶段STAGE_KEY_A触发所有注册到STAGE_KEY_A时间节点的启动项,通过对这种方式,几乎没有任何额外的辅助代码,我们用一种很简洁的方式完成了启动项的自注册。- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary )launchOptions { // 其他逻辑 [[KLNKylin sharedInstance] executeArrayForKey:STAGE_KEY_A]; // 在此触发所有注册到STAGE_KEY_A时间节点的启动项 // 其他逻辑 return YES;}完成对现有的启动项的梳理和优化后,我们也输出了后续启动项的添加&维护规范,规范后续启动项的分类原则,优先级和启动阶段。目的是管控性能问题增量,保证优化成果。六、优化main()之前在调用main()函数之前,基本所有的工作都是由操作系统完成的,开发者能够插手的地方不多,所以如果想要优化这段时间,就必须先了解一下,操作系统在main()之前做了什么。main()之前操作系统所做的工作就是把可执行文件(Mach-O格式)加载到内存空间,然后加载动态链接库dyld,再执行一系列动态链接操作和初始化操作的过程(加载、绑定、及初始化方法)。这方面的资料网上比较多,但重复性较高,此处附上一篇WWDC的Topic:Optimizing App Startup Time 。加载过程—从exec()到main()真正的加载过程从exec()函数开始,exec()是一个系统调用。操作系统首先为进程分配一段内存空间,然后执行如下操作:把App对应的可执行文件加载到内存。把Dyld加载到内存。Dyld进行动态链接。下面我们简要分析一下Dyld在各阶段所做的事情:阶段工作 加载动态库Dyld从主执行文件的header获取到需要加载的所依赖动态库列表,然后它需要找到每个 dylib,而应用所依赖的 dylib 文件可能会再依赖其他 dylib,所以所需要加载的是动态库列表一个递归依赖的集合 Rebase和Bind- Rebase在Image内部调整指针的指向。在过去,会把动态库加载到指定地址,所有指针和数据对于代码都是对的,而现在地址空间布局是随机化,所以需要在原来的地址根据随机的偏移量做一下修正- Bind是把指针正确地指向Image外部的内容。这些指向外部的指针被符号(symbol)名称绑定,dyld需要去符号表里查找,找到symbol对应的实现 Objc setup- 注册Objc类 (class registration) - 把category的定义插入方法列表 (category registration) - 保证每一个selector唯一 (selector uniquing) Initializers- Objc的+load()函数 - C++的构造函数属性函数 - 非基本类型的C++静态全局变量的创建(通常是类或结构体) 最后 dyld 会调用 main() 函数,main() 会调用 UIApplicationMain(),before main()的过程也就此完成。了解完main()之前的加载过程后,我们可以分析出一些影响T1时间的因素:动态库加载越多,启动越慢。ObjC类,方法越多,启动越慢。ObjC的+load越多,启动越慢。C的constructor函数越多,启动越慢。C++静态对象越多,启动越慢。针对以上几点,我们做了如下一些优化工作:代码瘦身随着业务的迭代,不断有新的代码加入,同时也会废弃掉无用的代码和资源文件,但是工程中经常有无用的代码和文件被遗弃在角落里,没有及时被清理掉。这些无用的部分一方面增大了App的包体积,另一方便也拖慢了App的冷启动速度,所以及时清理掉这些无用的代码和资源十分有必要。通过对Mach-O文件的了解,可以知道__TEXT:__objc_methname:中包含了代码中的所有方法,而__DATA__objc_selrefs中则包含了所有被使用的方法的引用,通过取两个集合的差集就可以得到所有未被使用的代码。核心方法如下,具体可以参考:objc_cover:def referenced_selectors(path): re_sel = re.compile("__TEXT:__objc_methname:(.+)") //获取所有方法 refs = set() lines = os.popen("/usr/bin/otool -v -s __DATA __objc_selrefs %s" % path).readlines() # ios & mac //真正被使用的方法 for line in lines: results = re_sel.findall(line) if results: refs.add(results[0]) return refs}通过这种方法,我们排查了十几个无用类和250+无用的方法。+load优化目前iOS App中或多或少的都会写一些+load方法,用于在App启动执行一些操作,+load方法在Initializers阶段被执行,但过多+load方法则会拖慢启动速度,对于大中型的App更是如此。通过对App中+load的方法分析,发现很多代码虽然需要在App启动时较早的时机进行初始化,但并不需要在+load这样非常靠前的位置,完全是可以延迟到App冷启动后的某个时间节点,例如一些路由操作。其实+load也可以被当做一种启动项来处理,所以在替换+load方法的具体实现上,我们仍然采用了上面的Kylin方式。使用示例:// 用WMAPP_BUSINESS_INIT_AFTER_HOMELOADING声明替换+load声明即可,不需其他改动WMAPP_BUSINESS_INIT_AFTER_HOMELOADING() { // 原+load方法中的代码}// 在某个合适的时机触发注册到该阶段的所有方法,如冷启动结束后[[KLNKylin sharedInstance] executeArrayForKey:@kWMAPP_BUSINESS_INITIALIZATION_AFTER_HOMELOADING_KEY] }七、优化耗时操作在main()之后主要工作是各种启动项的执行(上面已经叙述),主界面的构建,例如TabBarVC,HomeVC等等。资源的加载,如图片I/O、图片解码、archive文档等。这些操作中可能会隐含着一些耗时操作,靠单纯阅读非常难以发现,如何发现这些耗时点呢?找到合适的工具就会事半功倍。Time ProfilerTime Profiler是Xcode自带的时间性能分析工具,它按照固定的时间间隔来跟踪每一个线程的堆栈信息,通过统计比较时间间隔之间的堆栈状态,来推算某个方法执行了多久,并获得一个近似值。Time Profiler的使用方法网上有很多使用教程,这里我们也不过多介绍,附上一篇使用文档:Instruments Tutorial with Swift: Getting Started。火焰图除了Time Profiler,火焰图也是一个分析CPU耗时的利器,相比于Time Profiler,火焰图更加清晰。火焰图分析的产物是一张调用栈耗时图片,之所以称为火焰图,是因为整个图形看起来就像一团跳动的火焰,火焰尖部是调用栈的栈顶,底部是栈底,纵向表示调用栈的深度,横向表示消耗的时间。一个格子的宽度越大,越说明其可能是瓶颈。分析火焰图主要就是看那些比较宽大的火苗,特别留意那些类似“平顶山”的火苗。下面是美团平台开发的性能分析工具-Caesium的分析效果图:通过对火焰图的分析,我们发现了冷启动过程中存在着不少问题,并成功优化了0.3S+的时间。优化内容总结如下:优化点举例发现隐晦的耗时操作发现在冷启动过程中archive了一张图片,非常耗时推迟&减少I/O操作减少动画图片组的数量,替换大图资源等。因为相比于内存操作,硬盘I/O是非常耗时的操作推迟执行的一些任务如一些资源的I/O,一些布局逻辑,对象的创建时机等八、优化串行操作在冷启动过程中,有很多操作是串行执行的,若干个任务串行执行,时间必然比较长。如果能变串行为并行,那么冷启动时间就能够大大缩短。闪屏页的使用现在许多App在启动时并不直接进入首页,而是会向用户展示一个持续一小段时间的闪屏页,如果使用恰当,这个闪屏页就能帮我们节省一些启动时间。因为当一个App比较复杂的时候,启动时首次构建App的UI就是一个比较耗时的过程,假定这个时间是0.2秒,如果我们是先构建首页UI,然后再在Window上加上这个闪屏页,那么冷启动时,App就会实实在在地卡住0.2秒,但是如果我们是先把闪屏页作为App的RootViewController,那么这个构建过程就会很快。因为闪屏页只有一个简单的ImageView,而这个ImageView则会向用户展示一小段时间,这时我们就可以利用这一段时间来构建首页UI了,一举两得。缓存定位&首页预请求美团外卖App冷启动过程中一个重要的串行流程就是:首页定位–>首页请求–>首页渲染过程,这三个操作占了整个首页加载时间的77%左右,所以想要缩短冷启动时间,就一定要从这三点出发进行优化。之前串行操作流程如下:优化后的设计,在发起定位的同时,使用客户端缓存定位,进行首页数据的预请求,使定位和请求并行进行。然后当用户真实定位成功后,判断真实定位是否命中缓存定位,如果命中,则刚才的预请求数据有效,这样可以节省大概40%的时间首页加载时间,效果非常明显;如果未命中,则弃用预请求数据,重新请求。九、数据监控Time Profiler和Caesium火焰图都只能在线下分析App在单台设备中的耗时操作,局限性比较大,无法在线上监控App在用户设备上的表现。外卖App使用公司内部自研的Metrics性能监控系统,长期监控App的性能指标,帮助我们掌握App在线上各种环境下的真实表现,并为技术优化项目提供可靠的数据支持。Metrics监控的核心指标之一,就是冷启动时间。冷启动开始&结束时间节点结束时间点:结束时间比较好确定,我们可以将首页某些视图元素的展示作为首页加载完成的标志。开始时间点:一般情况下,我们都是在main()之后才开始接管App,但以main()函数作为冷启动起始点显然不合适,因为这样无法统计到T1时间段。那么,起始时间如何确定呢?目前业界常见的有两种方法,一是以可执行文件中任意一个类的+load方法的执行时间作为起始点;二是分析dylib的依赖关系,找到叶子节点的dylib,然后以其中某个类的+load方法的执行时间作为起始点。根据Dyld对dylib的加载顺序,后者的时机更早。但是这两种方法获取的起始点都只在Initializers阶段,而Initializers之前的时长都没有被计入。Metrics则另辟蹊径,以App的进程创建时间(即exec函数执行时间)作为冷启动的起始时间。因为系统允许我们通过sysctl函数获得进程的有关信息,其中就包括进程创建的时间戳。#import <sys/sysctl.h>#import <mach/mach.h>+ (BOOL)processInfoForPID:(int)pid procInfo:(struct kinfo_proc)procInfo{ int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid}; size_t size = sizeof(*procInfo); return sysctl(cmd, sizeof(cmd)/sizeof(*cmd), procInfo, &size, NULL, 0) == 0;}+ (NSTimeInterval)processStartTime{ struct kinfo_proc kProcInfo; if ([self processInfoForPID:[[NSProcessInfo processInfo] processIdentifier] procInfo:&kProcInfo]) { return kProcInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + kProcInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0; } else { NSAssert(NO, @“无法取得进程的信息”); return 0; }}进程创建的时机非常早。经过实验,在一个新建的空白App中,进程创建时间比叶子节点dylib中的+load方法执行时间早12ms,比main函数的执行时间早13ms(实验设备:iPhone 7 Plus (iOS 12.0)、Xcode 10.0、Release 模式)。外卖App线上的数据则更加明显,同样的机型(iPhone 7 Plus)和系统版本(iOS 12.0),进程创建时间比叶子节点dylib中的+load方法执行时间早688ms。而在全部机型和系统版本中,这一数据则是878ms。冷启动过程时间节点我们也在App冷启动过程中的所有关键节点打上一连串测速点,Metrics会记录下测速点的名称,及其距离进程创建时间的时长。我们没有采用自动打点的方式,是因为外卖App的冷启动过程十分复杂,而自动打点无法做到如此细致,并不实用。另外,Metrics记录的是时间轴上以进程创建时间为原点的一组顺序的时间点,而不是一组时间段,是因为顺序的时间点可以计算任意两个时间点之间的距离,即可以将时间点处理成时间段。但是,一组时间段可能无法还原为顺序的时间点,因为时间段之间可能并不是首尾相接的,特别是对于异步执行或者多线程的情况。在测速完毕后,Metrics会统一将所有测速点上报到后台。下图是美团外卖App 6.10版本的部分过程节点监控数据截图:Metrics还会由后台对数据做聚合计算,得到冷启动总时长和各个测速点时长的50分位数、90分位数和95分位数的统计数据,这样我们就能从宏观上对冷启动时长分布情况有所了解。下图中横轴为时长,纵轴为上报的样本数。十、总结对于快速迭代的App,随着业务复杂度的增加,冷启动时长会不可避免的增加。冷启动流程也是一个比较复杂的过程,当遇到冷启动性能瓶颈时,我们可以根据App自身的特点,配合工具的使用,从多方面、多角度进行优化。同时,优化冷启动存量问题只是冷启动治理的第一步,因为冷启动性能问题并不是一日造成的,也不能简单的通过一次优化工作就能解决,我们需要通过合理的设计、规范的约束,来有效地管控性能问题的增量,并通过持续的线上监控来及时发现并修正性能问题,这样才能够长期保证良好的App冷启动体验。作者简介郭赛,美团点评资深工程师。2015年加入美团,目前作为外卖iOS团队主力开发,负责移动端业务开发,业务类基础设施的建设与维护。徐宏,美团点评资深工程师。2016年加入美团,目前作为外卖iOS团队主力开发,负责移动端APM性能监控,高可用基础设施支撑相关推进工作。招聘美团外卖长期招聘Android、iOS、FE高级/资深工程师和技术专家,Base北京、上海、成都,欢迎有兴趣的同学投递简历到chenhang03@meituan.com。 ...

December 7, 2018 · 1 min · jiezi

Xcode 10 升级导致项目报错的常见问题

前不久,伴随着iOS 12版本的发布,苹果升级了Xcode 版本,最新的版本是Xcode 10.1。不过升级后经常会遇到各种编译问题,下面就给大家总结一些常见的错误(我项目遇到的)。library not found for -libstdc++报错的详细信息如: library not found for -lstdc++.6.0.9 。报错的原因是因为xocde 10后这个libstd++.6.0.9库已经废弃了,解决方法也简单,要么删除之前的依赖脚本,要么下载libstd++.6.0.9添加到项目中。然后,依次选择TARGETS—>Build Phases—>Link Binary With Libraries添加libstd++.6.0.9。说明:如果大家找不到libstdc++,可以到下面的地址下载点击下载directory not found for option问题描述:iOS “directory not found for option ‘-L/Users/…/Pods/build/Debug-iphoneos/…“解决方法: 在Target-Build Settings中找到Search Paths-Library Search Paths,删除掉在警告中所示的该路径,然后clean 、编译 即可。script phase “[CP]Copy Pods Resources报错信息如下:解决方法是删除对应的路径文件:在Target-Build Phases — [CP]Copy Pods Resources — Output Files 下的 ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH} 删除

November 29, 2018 · 1 min · jiezi

Texture 布局篇

Texture 拥有自己的一套成熟布局方案,虽然学习成本略高,但至少比原生的 AutoLayout 写起来舒服,重点是性能远好于 AutoLayout ,Texture 文档上也指出了这套布局方案的的优点:Fast: As fast as manual layout code and significantly faster than Auto LayoutAsynchronous & Concurrent: Layouts can be computed on background threads so user interactions are not interrupted.Declarative: Layouts are declared with immutable data structures. This makes layout code easier to develop, document, code review, test, debug, profile, and maintain.Cacheable: Layout results are immutable data structures so they can be precomputed in the background and cached to increase user perceived performance.Extensible: Easy to share code between classes.首先这套布局都是基于 Texture 组件的,所以当遇到要使用原生控件时,通过用 block 的方式包装一个原生组件再合适不过了,例如:ASDisplayNode *animationImageNode = [[ASDisplayNode alloc] initWithViewBlock:^UIView * _Nonnull{ FLAnimatedImageView *animationImageView = [[FLAnimatedImageView alloc] init]; animationImageView.layer.cornerRadius = 2.0f; animationImageView.clipsToBounds = YES; return animationImageView;}];[self addSubnode:animationImageNode];self.animationImageNode = animationImageNode;ASDisplayNode 在初始化之后会检查是否有子视图,如果有就会调用- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize方法进行布局,所以对视图进行布局需要重写这个方法。看一个例子:- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ ASInsetLayoutSpec *inset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:_childNode]; return insetLayout;}_childNode 相对于父视图边距都为 0,也就是AutoLayout中 top bottom left right 都为 0。—————————–父视图—————————-| ————————-_childNode——————— || | | || | | || ————————— ————————— |————————————————————–可以看到layoutSpecThatFits:方法返回的必须是 ASLayoutSpec, ASInsetLayoutSpec 是它的子类之一,下面是所有的子类及其关系:ASLayoutSpecASAbsoluteLayoutSpec // 绝对布局ASBackgroundLayoutSpec // 背景布局ASInsetLayoutSpec // 边距布局ASOverlayLayoutSpec // 覆盖布局ASRatioLayoutSpec // 比例布局ASRelativeLayoutSpec // 顶点布局ASCenterLayoutSpec // 居中布局ASStackLayoutSpec // 盒子布局ASWrapperLayoutSpec // 填充布局ASCornerLayoutSpec // 角标布局_ASAbsoluteLayoutSpec使用方法和原生的绝对布局类似- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ self.childNode.style.layoutPosition = CGPointMake(100, 100); self.childNode.style.preferredLayoutSize = ASLayoutSizeMake(ASDimensionMake(100), ASDimensionMake(100)); ASAbsoluteLayoutSpec *absoluteLayout = [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[self.childNode]]; return absoluteLayout;}值得提的是:ASAbsoluteLayoutSpec 一般情况都会通过 ASOverlayLayoutSpec 或 ASOverlayLayoutSpec 着陆,因为只有上述两种布局才能保留 ASAbsoluteLayoutSpec 绝对布局的事实。举个例子当视图中只有一个控件需要用的是 ASAbsoluteLayoutSpec 布局,而其他控件布局用的是 ASStackLayoutSpec(后面会介绍),那么一旦 absoluteLayout 被加入到 ASStackLayoutSpec 也就失去它原本的布局的意义。ASOverlayLayoutSpec *contentLayout = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:stackLayout overlay:absoluteLayout];不过官方文档明确指出应该尽量少用这种布局方式:Absolute layouts are less flexible and harder to maintain than other types of layouts._ASBackgroundLayoutSpec- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ ASBackgroundLayoutSpec *backgroundLayout = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:self.childNodeB background:self.childNodeA]; return backgroundLayout;}把childNodeA 做为 childNodeB 的背景,也就是 childNodeB 在上层,要注意的是 ASBackgroundLayoutSpec 事实上根本不会改变视图的层级关系,比如:ASDisplayNode *childNodeB = [[ASDisplayNode alloc] init];childNodeB.backgroundColor = [UIColor blueColor];[self addSubnode:childNodeB];self.childNodeB = childNodeB;ASDisplayNode *childNodeA = [[ASDisplayNode alloc] init];childNodeA.backgroundColor = [UIColor redColor];[self addSubnode:childNodeA];self.childNodeA = childNodeA;那么即使使用上面的布局方式,childNodeB 依然在下层。_ASInsetLayoutSpec比较常用的一个类,看图应该能一目了然(图片来自于官方文档)- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ ASInsetLayoutSpec *inset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:_childNode]; return insetLayout;}_childNode 相对于父视图边距都为 0,相当于填充整个父视图。它和之后会说到的ASOverlayLayoutSpec 实际上更多的用来组合两个 Element 而已。ASOverlayLayoutSpec参考 ASBackgroundLayoutSpec_ASRatioLayoutSpec(图片来自于官方文档)也是比较常用的一个类,作用是设置自身的高宽比,例如设置正方形的视图- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ ASRatioLayoutSpec *ratioLayout = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:1.0f child:self.childNodeA]; return ratioLayout;}ASRelativeLayoutSpec把它称为顶点布局可能有点不恰当,实际上它可以把视图布局在:左上、左下、右上、右下四个顶点以外,还可以设置成居中布局。- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ self.childNodeA.style.preferredSize = CGSizeMake(100, 100); ASRelativeLayoutSpec *relativeLayout = [ASRelativeLayoutSpec relativePositionLayoutSpecWithHorizontalPosition:ASRelativeLayoutSpecPositionEnd verticalPosition:ASRelativeLayoutSpecPositionStart sizingOption:ASRelativeLayoutSpecSizingOptionDefault child:self.childNodeA]; return relativeLayout;}上面的例子就是把 childNodeA 显示在右上角。ASCenterLayoutSpec绝大多数情况下用来居中显示视图- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ self.childNodeA.style.preferredSize = CGSizeMake(100, 100); ASCenterLayoutSpec *relativeLayout = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionDefault child:self.childNodeA]; return relativeLayout;}ASStackLayoutSpec可以说这是最常用的类,而且相对于其他类来说在功能上是最接近于 AutoLayout 的。之所以称之为盒子布局是因为它和 CSS 中 Flexbox 很相似,关于 Flexbox 的可以看下阮一峰的这篇文章。先看一个例子:- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ self.childNodeA.style.preferredSize = CGSizeMake(100, 100); self.childNodeB.style.preferredSize = CGSizeMake(200, 200); ASStackLayoutSpec *stackLayout = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical spacing:12 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStart children:@[self.childNodeA, self.childNodeB]]; return stackLayout;}简单的说明下各个参数的作用:direction:主轴的方向,有两个可选值:纵向:ASStackLayoutDirectionVertical横向:ASStackLayoutDirectionHorizontalspacing: 主轴上视图排列的间距,比如有四个视图,那么它们之间的存在三个间距值都应该是spacingjustifyContent: 主轴上的排列方式,有五个可选值:ASStackLayoutJustifyContentStart 从前往后排列ASStackLayoutJustifyContentCenter 居中排列ASStackLayoutJustifyContentEnd 从后往前排列ASStackLayoutJustifyContentSpaceBetween 间隔排列,两端无间隔ASStackLayoutJustifyContentSpaceAround 间隔排列,两端有间隔alignItems: 交叉轴上的排列方式,有五个可选值:ASStackLayoutAlignItemsStart 从前往后排列ASStackLayoutAlignItemsEnd 从后往前排列ASStackLayoutAlignItemsCenter 居中排列ASStackLayoutAlignItemsStretch 拉伸排列ASStackLayoutAlignItemsBaselineFirst 以第一个文字元素基线排列(主轴是横向才可用)ASStackLayoutAlignItemsBaselineLast 以最后一个文字元素基线排列(主轴是横向才可用)children: 包含的视图。数组内元素顺序同样代表着布局时排列的顺序,所以需要注意主轴的方向设置尤为重要,如果主轴设置的是 ASStackLayoutDirectionVertical, 那么 justifyContent 各个参数的意义就是:ASStackLayoutJustifyContentStart 从上往下排列ASStackLayoutJustifyContentCenter 居中排列ASStackLayoutJustifyContentEnd 从下往上排列ASStackLayoutJustifyContentSpaceBetween 间隔排列,两端无间隔ASStackLayoutJustifyContentSpaceAround 间隔排列,两端有间隔alignItems 就是:ASStackLayoutAlignItemsStart 从左往右排列ASStackLayoutAlignItemsEnd 从右往左排列ASStackLayoutAlignItemsCenter 居中排列ASStackLayoutAlignItemsStretch 拉伸排列ASStackLayoutAlignItemsBaselineFirst 无效ASStackLayoutAlignItemsBaselineLast 无效对于子视图间距不一样的布局方法,后面实战中会讲到。ASWrapperLayoutSpec填充整个视图- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ ASWrapperLayoutSpec *wrapperLayout = [ASWrapperLayoutSpec wrapperWithLayoutElement:self.childNodeA]; return wrapperLayout;}ASCornerLayoutSpec顾名思义 ASCornerLayoutSpec 适用于类似于角标的布局override func layoutSpecThatFits( constrainedSize: ASSizeRange) -> ASLayoutSpec{ let cornerSpec = ASCornerLayoutSpec(child: avatarNode, corner: badgeNode, location: .topRight) cornerSpec.offset = CGPoint(x: -3, y: 3)}最需要注意的是offset是控件的Center的偏移布局实战案例一简单的文件覆盖在图片上,文字居中。- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ ASWrapperLayoutSpec *wrapperLayout = [ASWrapperLayoutSpec wrapperWithLayoutElement:self.coverImageNode]; ASCenterLayoutSpec *centerSpec = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionDefault child:self.textNode]; ASOverlayLayoutSpec *overSpec = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:wrapperLayout overlay:centerSpec]; return overSpec;}ASWrapperLayoutSpec 把图片铺满整个视图ASCenterLayoutSpec 把文字居中显示ASOverlayLayoutSpec 把文字覆盖到图片上注意第三步就是之前提到的 ASOverlayLayoutSpec/ASBackgroundLayoutSpec 的作用:用于组合两个 Element。案例二这个是轻芒阅读(豌豆荚一览) APP 内 AppSo 频道 Cell 的布局,应该也是比较典型的布局之一。为了方便理解先给各个元素定一下名称,从上至下,从左往右分别是:coverImageNode // 大图titleNode // 标题subTitleNode // 副标题dateTextNode // 发布时间shareImageNode // 分享图标shareNumberNode // 分享数量likeImageNode // 喜欢图标likeNumberNode // 喜欢数量- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{ self.shareImageNode.style.preferredSize = CGSizeMake(15, 15); self.likeImageNode.style.preferredSize = CGSizeMake(15, 15); ASStackLayoutSpec *likeLayout = [ASStackLayoutSpec horizontalStackLayoutSpec]; likeLayout.spacing = 4.0; likeLayout.justifyContent = ASStackLayoutJustifyContentStart; likeLayout.alignItems = ASStackLayoutAlignItemsCenter; likeLayout.children = @[self.likeImageNode, self.likeNumberNode]; ASStackLayoutSpec *shareLayout = [ASStackLayoutSpec horizontalStackLayoutSpec]; shareLayout.spacing = 4.0; shareLayout.justifyContent = ASStackLayoutJustifyContentStart; shareLayout.alignItems = ASStackLayoutAlignItemsCenter; shareLayout.children = @[self.shareImageNode, self.shareNumberNode]; ASStackLayoutSpec *otherLayout = [ASStackLayoutSpec horizontalStackLayoutSpec]; otherLayout.spacing = 12.0; otherLayout.justifyContent = ASStackLayoutJustifyContentStart; otherLayout.alignItems = ASStackLayoutAlignItemsCenter; otherLayout.children = @[likeLayout, shareLayout]; ASStackLayoutSpec *bottomLayout = [ASStackLayoutSpec horizontalStackLayoutSpec]; bottomLayout.justifyContent = ASStackLayoutJustifyContentSpaceBetween; bottomLayout.alignItems = ASStackLayoutAlignItemsCenter; bottomLayout.children = @[self.dateTextNode, otherLayout]; self.titleNode.style.spacingBefore = 12.0f; self.subTitleNode.style.spacingBefore = 16.0f; self.subTitleNode.style.spacingAfter = 20.0f; ASRatioLayoutSpec *rationLayout = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:0.5 child:self.coverImageNode]; ASStackLayoutSpec *contentLayout = [ASStackLayoutSpec horizontalStackLayoutSpec]; contentLayout.justifyContent = ASStackLayoutJustifyContentStart; contentLayout.alignItems = ASStackLayoutAlignItemsStretch; contentLayout.children = @[ rationLayout, self.titleNode, self.subTitleNode, bottomLayout ]; ASInsetLayoutSpec *insetLayout = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(16, 16, 16, 16) child:contentLayout]; return insetLayout;}下面详细解释下布局,不过首先要明确的是,Texture 的这套布局方式遵守从里到外的布局原则,使用起来才会得心应手。根据布局的原则,首先利用 ASStackLayoutSpec 布局 分享图标 和 分享数量、 喜欢图标 和 喜欢数量。还是通过 ASStackLayoutSpec 包装第一步的两个的布局得到 otherLayout 布局对象。依然是 ASStackLayoutSpec 包装otherLayout和 发布时间。注意这里设置横向的排列方式 ASStackLayoutJustifyContentSpaceBetween已到达两端布局的目的,最终返回 bottomLayout。由于 大图 是网络图片,对于 Cell 来说,子视图的布局必能能决定其高度(Cell 宽度是默认等于 TableNode 的宽度),所以这里必须设置 大图 的高度,ASRatioLayoutSpec 设置了图片的高宽比。接下来布局应该就是 大图、标题、副标题、bottomLayout 的一个纵向布局,可以发现这里的视图间距并不相同,这时候 spacingBefore 和 spacingAfter 就会很有用,它们用来分别设置元素在主轴上的前后间距。self.titleNode.style.spacingBefore = 12.0f; 意思就是 标题 相对于 大图 间距为 12。最后通过一个 ASInsetLayoutSpec 设置一个边距。可以看到不仅是 Node,ASLayoutSpec 本身也可以作为布局元素,这是因为只要是遵守了 <ASLayoutElement> 协议的对象都可以作为布局元素。案例三 override func layoutSpecThatFits( constrainedSize: ASSizeRange) -> ASLayoutSpec { self.node1.style.preferredSize = CGSize(width: constrainedSize.max.width, height: 136) self.node2.style.preferredSize = CGSize(width: 58, height: 25) self.node2.style.layoutPosition = CGPoint(x: 14.0, y: 95.0) self.node3.style.height = ASDimensionMake(37.0) self.node4.style.preferredSize = CGSize(width: 80, height: 20) self.node5.style.preferredSize = CGSize(width: 80, height: 20) self.node4.style.spacingBefore = 14.0 self.node5.style.spacingAfter = 14.0 let absoluteLayout = ASAbsoluteLayoutSpec(children: [self.node2]) let overlyLayout = ASOverlayLayoutSpec(child: self.node1, overlay: absoluteLayout) let insetLayout = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(0, 14, 0, 14), child: self.node3) insetLayout.style.spacingBefore = 13.0 insetLayout.style.spacingAfter = 25.0 let bottomLayout = ASStackLayoutSpec.horizontal() bottomLayout.justifyContent = .spaceBetween bottomLayout.alignItems = .start bottomLayout.children = [self.node4, self.node5] bottomLayout.style.spacingAfter = 10.0// bottomLayout.style.width = ASDimensionMake(constrainedSize.max.width) let stackLayout = ASStackLayoutSpec.vertical() stackLayout.justifyContent = .start stackLayout.alignItems = .stretch stackLayout.children = [overlyLayout, insetLayout, bottomLayout] return stackLayout }为了演示 ASAbsoluteLayoutSpec 的使用,这里 node3 我们用 ASAbsoluteLayoutSpec 布局。接下来说下要点:node 和 layoutSpec 都可以设置 style 属性,因为它们都准守 ASLayoutElement 协议当 spaceBetween 没有达到两端对齐的效果,尝试设置当前 layoutSpec 的 width(如注释)或它的上一级布局对象的 alignItems,在例子中就是 stackLayout.alignItems = .stretchASAbsoluteLayoutSpec 必须有落点(除非是只有绝对布局),例子中 ASAbsoluteLayoutSpec 着落点就在 ASOverlayLayoutSpec案例四此案例主要为了演示 flexGrow 的用法,先介绍下 flexGrow 的作用(来自于简书九彩拼盘)该属性来设置,当父元素的宽度大于所有子元素的宽度的和时(即父元素会有剩余空间),子元素如何分配父元素的剩余空间。flex-grow的默认值为0,意思是该元素不索取父元素的剩余空间,如果值大于0,表示索取。值越大,索取的越厉害。举个例子:父元素宽400px,有两子元素:A和B。A宽为100px,B宽为200px,则空余空间为 400-(100+200)= 100px。如果A,B都不索取剩余空间,则有100px的空余空间。如果A索取剩余空间:设置flex-grow为1,B不索取。则最终A的大小为 自身宽度(100px)+ 剩余空间的宽度(100px)= 200px如果A,B都设索取剩余空间,A设置flex-grow为1,B设置flex-grow为2。则最终A的大小为 自身宽度(100px)+ A获得的剩余空间的宽度(100px (1/(1+2))),最终B的大小为 自身宽度(200px)+ B获得的剩余空间的宽度(100px (2/(1+2))) override func layoutSpecThatFits( constrainedSize: ASSizeRange) -> ASLayoutSpec { self.node1.style.height = ASDimensionMake(20.0) var imageLayoutArray = ASLayoutElement [self.node2, self.node3, self.node4].forEach { (node) in let layout = ASRatioLayoutSpec(ratio: 2.0/3.0, child: node) layout.style.flexGrow = 1 // 相当于宽度相等 imageLayoutArray.append(layout) } let imageLayout = ASStackLayoutSpec.horizontal() imageLayout.justifyContent = .start imageLayout.alignItems = .start imageLayout.spacing = 14.0 imageLayout.children = imageLayoutArray let contentLayout = ASStackLayoutSpec.vertical() contentLayout.justifyContent = .start contentLayout.alignItems = .stretch contentLayout.spacing = 22.0 contentLayout.children = [self.node1, imageLayout] return ASInsetLayoutSpec(insets: UIEdgeInsetsMake(22.0, 16.0, 22.0, 16.0), child: contentLayout) }在这个案例中 node2、node3、node4 的宽度的总和小于父元素的宽度,所以为了达到宽度相同只需要设置三者的 flexGrow 相同就行(都为1),再通过 ASRatioLayoutSpec 固定各自的宽高比,那么对于这个三个控件来说最终的宽度是确定的。案例四此案例主要为了演示 flexShrink 的用法,同样还来自于简书九彩拼盘关于 flexShrink 的介绍该属性来设置,当父元素的宽度小于所有子元素的宽度的和时(即子元素会超出父元素),子元素如何缩小自己的宽度的。flex-shrink的默认值为1,当父元素的宽度小于所有子元素的宽度的和时,子元素的宽度会减小。值越大,减小的越厉害。如果值为0,表示不减小。举个例子:父元素宽400px,有两子元素:A和B。A宽为200px,B宽为300px。则A,B总共超出父元素的宽度为(200+300)- 400 = 100px。如果A,B都不减小宽度,即都设置flex-shrink为0,则会有100px的宽度超出父元素。如果A不减小宽度:设置flex-shrink为0,B减小。则最终B的大小为 自身宽度(300px)- 总共超出父元素的宽度(100px)= 200px如果A,B都减小宽度,A设置flex-shirk为3,B设置flex-shirk为2。则最终A的大小为 自身宽度(200px)- A减小的宽度(100px (200px 3/(200 3 + 300 2))) = 150px,最终B的大小为 自身宽度(300px)- B减小的宽度(100px (300px 2/(200 3 + 300 2))) = 250px目前关于该属性最常见还是用于对文本的宽度限制,在上图中 textNode 和 displayNode 是两端对齐,而且需要限制文本的最大宽度,这时候设置 flexShrink 是最方便的。override func layoutSpecThatFits( constrainedSize: ASSizeRange) -> ASLayoutSpec { self.displayNode.style.preferredSize = CGSize(width: 42.0, height: 18.0) self.textNode.style.flexShrink = 1 let contentLayout = ASStackLayoutSpec.horizontal() contentLayout.justifyContent = .spaceBetween contentLayout.alignItems = .start contentLayout.children = [self.textNode, self.displayNode] let insetLayout = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(16.0, 16.0, 16.0, 16.0), child: contentLayout) return insetLayout }随便提一下的是如果 ASTextNode 出现莫名的文本截断问题,可以用 ASTextNode2 代替。案例五还算比较典型的例子override func layoutSpecThatFits( constrainedSize: ASSizeRange) -> ASLayoutSpec { let otherLayout = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(10.0, 10.0, CGFloat(Float.infinity), CGFloat(Float.infinity)), child: topLeftNode) let contentLayout = ASOverlayLayoutSpec(child: coverImageNode, overlay: otherLayout) return contentLayout }利用 ASInsetLayoutSpec 是最好的解决方案,值得注意的是对于红色控件只需要设置向上和向左的间距,那么其他方向的可以用 CGFloat(Float.infinity) 代替,并不需要给出具体数值。 ...

November 23, 2018 · 5 min · jiezi

深入理解苹果系统(Unicode)字符串的排序方法

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由iminder发表于云+社区专栏Unicode编码我们知道计算机是不能直接处理文本的,而是和数字打交道。因此,为了表示文本,就建立了一个字符到数字的映射表,叫做编码。最著名的字符编码就是ASCII了,它使用7-bit来表示应用字母表以及数字和其他字符。这对于英语来说是够用了,但是对于其他语言,这个7-bit就不能满足条件了,因为字符远远超过了7-bit所能表示的最大个数。因此1987年,来自几个大的科技公司的工程师开始合作开发一种致力于能在全世界的所有书写系统中都能通用的字符编码系统,并与1991年10发布了Unicode的1.0.0标准。2018年6月发布了Unicode的11.0版本。这里就不再对Unicode做过多的介绍,值得注意的是,在iOS开发中,常使用的的NSString是基于Unicode-16来开发的,这是因为当时开发这个的时候Unicode标准还是以16bit固定长度来编码,这就导致使用上的一些坑,建议大家阅读下这篇文章:NSString and Unicode。#UCA和CLDR:最常用到的排序标准介绍完Unicode编码之后,我们就可以来介绍UCA(Unicode Collation Algorithm)和CLDR(Common Locale Data Repository)了,因为苹果的NSString的介绍文档里有这么一句话:Localized string comparisons are based on the Unicode Collation Algorithm, as tailored for different languages by CLDR (Common Locale Data Repository). Both are projects of the Unicode Consortium. Unicode is a registered trademark of Unicode, Inc.说白了,苹果系统的NSString字符串排序是基于UCA的,并且在不同语言下,经过CLDR来裁剪的。UCA(Unicode Collation Algorithm)UCA的介绍官方文档介绍在这里:UCA介绍。其中第一句话就写的很清楚,Collation is the general term for the process and function of determining the sorting order of strings of characters.对字符串排序的过程就是Collation,UCA就是Unicode表示的字符串进行排序的规则,制定这个规则的原因是不同语种对字符串的排序规则要求是不一样的,比如,德国、法国和瑞士对相同的字符排序的规则是不一样的,甚至在同一个语言下比如中文,多音字这种在不同组合里,排序的先后顺序也是不一样的。差异化举例因此可以想象,UCA指定的规则比较复杂。感兴趣的可以读下前面贴的UCA介绍,里面有具体的排序规则介绍。CLDR(Common Locale Data Repository)CLDR的官方文档在这里:CLDR介绍。CLDR是一堆语言数据仓库,为软件提供各种世界语言版本提供了基础,目前在使用CLDR的公司有:Apple (macOS, iOS, watchOS, tvOS, and several applications; Apple Mobile Device Support and iTunes for Windows; …) Google (Web Search, Chrome, Android, Adwords, Google+, Google Maps, Blogger, Google Analytics, …) IBM (DB2, Lotus, Websphere, Tivoli, Rational, AIX, i/OS, z/OS,…) Microsoft (Windows, Office, Visual Studio, …)其他公司:ABAS Software, Adobe, Amazon (Kindle), Amdocs, Apache, Appian, Argonne National Laboratory, Avaya, Babel (Pocoo library), BAE Systems Geospatial eXploitation Products, BEA, BluePhoenix Solutions, BMC Software, Boost, BroadJump, Business Objects, caris, CERN, Debian Linux, Dell, Eclipse, eBay, EMC Corporation, ESRI, Firebird RDBMS, FreeBSD, Gentoo Linux, GroundWork Open Source, GTK+, Harman/Becker Automotive Systems GmbH, HP, Hyperion, Inktomi, Innodata Isogen, Informatica, Intel, Interlogics, IONA, IXOS, Jikes, jQuery, Library of Congress, Mathworks, Mozilla, Netezza, OpenOffice, Oracle (Solaris, Java), Lawson Software, Leica Geosystems GIS & Mapping LLC, Mandrake Linux, OCLC, Perl, Progress Software, Python, QNX, Rogue Wave, SAP, Shutterstock, SIL, SPSS, Software AG, SuSE, Symantec, Teradata (NCR), ToolAware, Trend Micro, Twitter, Virage, webMethods, Wikimedia Foundation (Wikipedia), Wine, WMS Gaming, XyEnterprise, Yahoo!, Yelp对于不同区域(local),可以找到不同的数据CLDR,结合UCA对字符串进行排序,就做到了不同语言下的本地化排序。可以去 http://cldr.unicode.org/ 下载最新的CLDR库,后面将会用到里面的一些内容。字符分类与排序规则字符分类与Unicode码点值排序Unicode把所有的字符分为两类:common charaters 包括空格,标点,通用符号,货币符号,数字等。script charaters 包括拉丁字母,希腊字母,汉字等。 这样经过分类,便于把一类字符统一集中在一起。通常情况下,我们是通过unicode 的UTF-16码点值逐个进行比较大小的来进行排序的。NSArray *rawArray = @[@“爱你”, @“一生一世”,@“㊀”, @“上”,@"㊤",@"",@"",@"..",@“123”,@"@",@“AA”,@“abc”,@“abb”];//1. 默认排序方式NSArray defaultedSortedArray = [rawArray sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { return [obj1 compare:obj2 options:NSCaseInsensitiveSearch];}];__block NSMutableArray codeUnits = [NSMutableArray array];[defaultedSortedArray enumerateObjectsUsingBlock:^(NSString _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { [codeUnits addObject:@([obj characterAtIndex:0])];}];NSLog(@“默认Unicode码点值排序 %@ 对应的各个字符串的首字符码点值是 %@”, [defaultedSortedArray descriptionWithLocale:cnLocal], codeUnits);输出结果是排序结果 .. 123 @ AA abb abc ㊀ ㊤ 一生一世 上 爱你 对应的各个字符串的首字符码点值是 46 49 64 65 97 97 956 1103 12928 12964 19968 19978 29233 我们常用的各种字符的码点值范围是:0-9 U+0030 - U0039a-z U+0061 - U+007AA-Z U+0041 - U+005A 具体可通过:unicode-table查询。UCA 默认排序在我们前面下载的文件CLDR库有个/common/uca/allkeys_CLDR.txt文件,它表示我们指定locale为“en”或者说是默认的排序规则。它的格式是0000 ; [.0000.0000.0000] # <NULL>0001 ; [.0000.0000.0000] # <START OF HEADING>0002 ; [.0000.0000.0000] # <START OF TEXT>0003 ; [.0000.0000.0000] # <END OF TEXT>分号前的值表示码点,分号后中括号里面的值表示UCA算法权重,用.号来区分,Unicode字符就是按照这个规则从上到下排序。NSLocale enLocale = [[NSLocale alloc] initWithLocaleIdentifier:@“en”];defaultedSortedArray = [rawArray sortedArrayUsingComparator:^NSComparisonResult(NSString _Nonnull obj1, NSString Nonnull obj2) { return [obj1 compare:obj2 options:0 range:NSMakeRange(0, obj1.length) locale:enLocale];}];NSLog(@“默认排序规则或者指定地区为locale后的排序结果是 %@”, [defaultedSortedArray descriptionWithLocale:cnLocal]);排序结果是默认排序规则或者指定地区为en后的排序结果是 .. (ch (en @ 123 AA abb abc ㊀ 一生一世 上 ㊤ 爱你 这种排序依次为符号,数字,英文/汉字等script charaters。CLDR调整后的排序在下载的CLDR文件中,有个common/bcp47/collation.xml文件,列出了可选的排序方式,有standard,pinyin, stroke(笔画排序)等。排序可选方式那如何确定各个区域语言下,该使用哪种排序规则呢,我们可以看到common/collation/文件夹下,有很多标记语言LDML文件,这些文件就是表示在不同区域语言下,采用的排序规则。我们打开zh.xml,这个就是我们简体中文的排序规则,可以看到,里面默认采用的排序是pinyin排序,并且在开头还写了各个声调字母的排序先后顺序。首先按照pinyin声调的先后顺序进行排序,即zh.xml底下列出的先后顺序进行排序。如果是在同一行的汉字,则按照笔画由少到多的顺序进行排序。如果还不能区分大小,就按照kRSUnicode (偏旁索引的方式,按照康熙字典的定义)的先后顺序进行排序。假如我们指定区域为zh_CN,则对于字符串中出现的中文则排在其他语言字符串前面。其他script charater则按照allkeys_CLDR.txt的顺序进行进行排序。值得注意的是,中文由于多音字,在这里不一定能够完全按照我们的习惯排序正确,比如“重逢(chong feng)”就没有第一个拼音chong去排,而是按照zhong来排列的。默认排序规则或者指定地区为zh_CN后的排序结果是 .. (ch (en @ 0124 123 艾你 爱你 産 上 ㊤ ㊀ 一生一世 重逢 重要 aa AA abb 默认排序规则或者指定地区为ru_CN后的排序结果是 .. (ch (en @ 0124 123 aa AA abb ㊀ 一生一世 上 ㊤ 爱你 産 艾你 重要 重逢 至此,我们大致讲清楚了几种排序规则。苹果系统的排序前面我们已经说了,苹果系统的NSString排序是UCA和CLDR规则的。NSString提供了很多的排序方法,但最终,所有的都是调用了compare:options:range:locale:来进行处理,只是传入的参数不同。可以在NSString.swift 中查看具体的实现。这么多排序方法中,其中之一是localizedStandardCompare:, 这个方法是苹果系统推荐的,在给用户展示的列表数据的名字或者其他字符串进行排序时所使用的方法。我们看到,它的内部实现是 public func localizedStandardCompare( string: String) -> ComparisonResult { return compare(string, options: [.caseInsensitive, .numeric, .widthInsensitive, .forcedOrdering], range: NSRange(location: 0, length: length), locale: Locale.current._bridgeToObjectiveC()) }其中用到的四个Options参数是NSCaseInsensitiveSearch //大小写不敏感NSNumericSearch //对字符串中出现的数字字符进行数字化的大小比较,比如Foo2.txt < Foo7.txt < Foo25.txtNSWidthInsensitiveSearch //忽略宽度,按照实际表示的意思来对比,如’a’ = UFF41NSForcedOrderingSearch //强制返回Ascending或者Descending,和NSCaseInsensitiveSearch结合起来就是例如"aaa" > “AAA"并且指定了当前的区域locale作为参数,这就相当于指定使用CLDR进行排序,如果是在手机上,这个方法的调用和系统当前的区域设置是有很大关系的,这和我们代码中设置locale是一个道理。我们可以这样理解,调用这个方法得到的结果和在iOS Files中文件名选择按照名称排序得到的结果是一样的。在iOS中,当我们的区域设置为中国时,排序顺序就是 标点符号等特殊符号>数字>中文>英文等其他。区域设置成中文后的排序自此,对localizedStandardCompare:的使用,大家应该比较清楚了。数字的比较这里单独把数字字符串的比较列出来,是因为一些人对这里比较迷惑。由于localizedStandardCompare:中有使用NSNumericSearch选项,这里简单来说,就是假如目前两个字符串是相等的,两者都出现了数字,则分别从两者种取出这段数字进行数字化来比较大小,按照数字大小排序。为了验证这里的逻辑,我看了下CFString.c中CFStringCompareWithOptionsAndLocale这个方法的实现,这个就是compare实际调用的的比较方法。其中关于数字大小比较的代码如下:if (numerically && ((0 == strBuf1Len) && (str1Char <= ‘9’) && (str1Char >= ‘0’)) && ((0 == strBuf2Len) && (str2Char <= ‘9’) && (str2Char >= ‘0’))) { // If both are not ASCII digits, then don’t do numerical comparison here uint64_t intValue1 = 0, intValue2 = 0; // !!! Doesn’t work if numbers are > max uint64_t CFIndex str1NumRangeIndex = str1Index; CFIndex str2NumRangeIndex = str2Index; do { intValue1 = (intValue1 * 10) + (str1Char - ‘0’); str1Char = CFStringGetCharacterFromInlineBuffer(&inlineBuf1, ++str1Index); } while ((str1Char <= ‘9’) && (str1Char >= ‘0’)); do { intValue2 = intValue2 * 10 + (str2Char - ‘0’); str2Char = CFStringGetCharacterFromInlineBuffer(&inlineBuf2, ++str2Index); } while ((str2Char <= ‘9’) && (str2Char >= ‘0’)); if (intValue1 == intValue2) { if (forceOrdering && (kCFCompareEqualTo == compareResult) && ((str1Index - str1NumRangeIndex) != (str2Index - str2NumRangeIndex))) { compareResult = (((str1Index - str1NumRangeIndex) < (str2Index - str2NumRangeIndex)) ? kCFCompareLessThan : kCFCompareGreaterThan); numericEquivalence = true; forcedIndex1 = str1NumRangeIndex; forcedIndex2 = str2NumRangeIndex; } continue; } else if (intValue1 < intValue2) { if (freeLocale && locale) { CFRelease(locale); } return kCFCompareLessThan; } else { if (freeLocale && locale) { CFRelease(locale); } return kCFCompareGreaterThan; } }这段代码的含义就是,如果两个字符串都是以数字开始(也可能是字符串前面都相等,当前从数字部分开始比较),则取出两个字符串的数字,按照数字大小进行对比。如果数字能够比较出大小,则直接返回两个字符串的大小关系,不再对后面的字符串进行对比。比如“0123aaa” 和“1bbbbbbbbb”,就直接返回“0123aaa”大于“1bbbbbbbbb”。当然,这里取出的数字可能超出了uint64_t表示的最大值,但是这种概率很低,在我们的名称排序中,很难遇到这么长的数字进行比较的。明白这个规则后,大家对字符串中出现的数字在进行排序时应该比较理解了。下面的名字排序是对着的。综述本文主要讲述由localizedStandardCompare:这个苹果系统方法所引发的对排序规则的深入研究,简单来说,设置中选择区域为中国时,排序顺序为 标点符号等特殊符号>数字>中文>英文等其他。中文本身是按照pinyin排序的,只是由于多音字的关系,不能够做到100%按照中文习惯来排序,会有些无法正确排序的问题,但大体已经符合我们的习惯了。参考https://zh.wikipedia.org/wiki…https://developer.apple.com/l...https://www.objc.io/issues/9-...http://unicode.org/reports/tr10/https://www.cnblogs.com/huahu...https://raw.githubusercontent...http://cldr.unicode.org/相关阅读【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...

November 20, 2018 · 4 min · jiezi

uni-app官方教程学习手记

背景介绍大概在今年的十月份左右,我了解到Dcloud推出了uni-app。当时下载了一个Hbuilder X,下载了官方提供的hello示例教程。经过一番努力,在云端打包成功了。当时这个软件还不够完善,用iphone真机模拟运行时,还会存在中文乱码问题。我还特意提交了一个bug。当时觉得这个框架真的好用。早先开发过混合app,也就是在webview下内嵌html5页面,经常会出现卡顿的和性能问题。这个解决方案DCloud本身也提供了,就是mui。但是uni-app是不一样的,一套代码,可以完美的运行在安卓、苹果、小程序甚至未来会支持H5页面。想想都觉得蛮兴奋的。然后我并没有动手去做任何项目,毕竟是新项目还需要发展一段时间。再有就是前端水太深,不能瞎折腾,时刻保持关注就行。一旦有需求,随时能最快上手就是最好的。这两天刚好朋友有个项目让我接手。仔细了解了下项目情况,发现项目刚开始做,由一个前端人员采用mui框架做的。做了不到一半页面就跑路了。这个项目没有任何的安卓或者苹果的开发人员,朋友跟我也没有任何相关的客户端开发经验。我这么一听,一下子想起来uni-app了。这就是个机会啊,既能接私活挣钱,又能学习新技术积累经验,更重要的是,一旦这个项目做成,就意味着自己不仅仅只会html、css、js了。以后可以吹牛逼说,我丫的客户端IOS和Android都能搞定~ 想想以后求职面试向面试官吹牛,我就有点小激动~~ 扯偏了 :)学习手记甭管多高深的框架,要想学习就得老老实实的去看官网。我又去看了一下官网,发现才隔了不到两个月,更新的内容还是蛮多的。更新得越频繁,说明框架越有活力,也就更值得我去折腾一下。大概的看了下文档之后,心中的第一个疑问就冒出来了。既然mui和uni-app都是DCloud出品,mui能直接平滑过渡到uni-app不? 官方给出的答案是否定的。如果已有5+或mui App、wap2app、原生App,是无法迁移到uni-app的。然后我就想起来,之前的webview存在的性能卡顿问题,看到官网给出的答案,我就放心了。也就下定决心,将朋友的mui项目改写成uni-app的。对于Hybrid方案,uni-app比普通基于webview的Hybrid方案体验更好,包括比DCloud之前的mui体验更好。对比纯原生渲染的方案,体验差不多,但易用性和生态完整度上uni-app明显胜出(uni-app自身功能组件丰富,并且小程序的周边丰富生态都可以用于跨平台开发)。心中的顾虑都解除了,那就开始学习这个框架吧。以下内容基本都来源于官方网站,仅此记录自己学习过程官方给出了一个视频:https://ke.qq.com/course/343370,老老实实地认真看过了视频,加上之前运行过demo,心中有了个大概。光看视频是不行的,感觉像是明白了,等到一动手操作的时候,就会发现哪里忘记或者记错了。所以我新建了一个仓库在腾讯云开发者平台,因为目前腾讯云是免费的,不限制创建项目数量,所以可以随便折腾。新建项目时注意事项HBuilder X提倡使用快捷键开发,所以尽量使用快捷键。这点在视频中有提到。我觉得还是非常好的一款编辑器。如果是练习项目,可以选择hello 模版。也可以在hello模版基础上二次开发。新建空白的项目需要拷贝uni.css和uni.js,保存到common目录。照着视频做,直接拷贝,非常方便新建的项目需要修改内容页面的标题,打开pages.json将文件中的navigationBarTitleText修改成自己的标题即可。使用代码进行编辑视频中随便敲了一下键盘,噼里啪啦的出现了一堆的代码,感觉还是不错的。应该是内置了一个代码块。所以去官网查了一下,果然nice。内置了很多的代码块。代码块分为Tag代码块、JS代码块。使用代码块直接创建组件模板新闻列表页 index.vue视频中就是一个新闻列表页,而列表页也就是一个列表而已。在index.vue中编写如下代码。v-for表示要循环的语句,其中的news是在js部分中的data定义的属性,表示新闻列表。v-for中的item表示一个列表项,也就是一个新闻;index表示列表的下标。@tap表示绑定点击事件。因为是在移动端,还是不要写@click了。click事件在移动端会有300ms的延迟:data-postid表示绑定一个动态的数据,而postid表示这个动态的数据属性是这个名字。如果想直接输出数据中的内容,通过{{}}两对大括号将数据内容包裹在里面即可。例如{{item.title}}视频中特别强调了声明data属性时要注意,必须声明为返回一个初始数据对象的函数。只需要更新最新版本的HBuilder X 新建页面的时候就会自动生成。编写js代码的时候,编辑器会自动用eslint对代码进行检查。可以通过工具-插件配置-eslint-vue查看和修改配置项。onLoad是页面的生命周期。uni-app 完整支持 Vue 实例的生命周期,同时还支持应用生命周期及页面生命周期uni.request是发起请求,直接通过代码块敲代码会特别快。另外这个接口跟ajax还是有些不太一样的。例如发送给服务器的数据都是string类型的。具体可以查看文档发起网络请求。关于项目目录、开发规范一定要遵守,直接通过官网学习即可。开发规范<template> <view class=“content”> <view class=“uni-list”> <view class=“uni-list-cell” hover-class=“uni-list-cell-hover” v-for="(item,index) in news" :key=“index” @tap=“opennews” :data-postid=“item.post_id”> <view class=“uni-media-list”> <image class=“uni-media-list-logo” :src=“item.author_avatar”></image> <view class=“uni-media-list-body”> <view class=“uni-media-list-text-top”>{{item.title}}</view> <view class=“uni-media-list-text-bottom uni-ellipsis”>{{item.created_at}}</view> </view> </view> </view> </view> </view></template><script> export default { data() { return { news: [] }; }, onLoad:function(){ uni.request({ url: ‘https://unidemo.dcloud.net.cn/api/news', method: ‘GET’, data: {}, success: res => { this.news = res.data; }, fail: () => {}, complete: () => {} }); }, methods:{ opennews(e){ uni.navigateTo({ url: ‘../news/news?postid=’+e.currentTarget.dataset.postid }); } } }</script><style> .uni-media-list-body{height: auto;} .uni-media-list-text-top{line-height: 1.6em;}</style>这个列表页面就算完成了。通过整体代码,可以发现采用vue这种开发模式,代码如此的简洁,总共才不到50行的代码。相比较于传统的jQuery方式,不知道要方便多少倍。通过代码,再一次鼓舞我继续下去,将项目修改成uni-app项目,不管工作量有多么大,也不管坑有多深了。新闻详情页 news.vue新闻详情页,超级简单,全部代码才区区40行。就可以比较好的展示页面了。因为新闻页面一般都包含<p></p>标签等富文本内容,所以使用uni-app提供的内置组件rich-text来实现。另外需要注意的就是,在页面的onLoad函数中,接到的参数e,实际上就是在页面index.vue传过来的参数。这种页面之间传参的方式非常方便。不用写任何多余的代码。<template> <view class=“wrap”> <view class=“title”> {{title}} </view> <view class=“content”> <rich-text :nodes=“content”></rich-text> </view> </view></template><script> export default { data() { return { title: ‘’, content: ’’ }; }, onLoad:function(e){ uni.request({ url: ‘https://unidemo.dcloud.net.cn/api/news/36kr/'+ e.postid, method: ‘GET’, data: {}, success: res => { this.title = res.data.title; this.content = res.data.content; }, fail: () => {}, complete: () => {} }); } }</script><style> .wrap{padding: 10upx 2%;width: 96%;flex-wrap: wrap;} .title{line-height: 2em;font-weight: bold;font-size: 40upx;} .content{line-height: 2em;}</style>运行和调试项目uni-app调试是在微信开发者工具之中调试的。所以本地一定要按照这个软件。之后ctrl+R,在微信中运行就可以调试了。开发App程序和微信小程序都需要在微信开发者工具之中调试。运行与调试介绍我觉得还是非常方便的,因为首先在HBuilder X 编辑代码之后,按下ctrl+s,会自动编译,然后就会自动刷新微信开发者工具。这就好像是前端开发中会使用自动刷新工具一样。以前我开发的时候还会专门去用一些工具去做到实时刷新,然后可以双屏幕开发。但是我发现没有那个第三方软件做得比较好的,所以每次在浏览器里我还是需要手动刷新。但是这个HBuilder X 内置的实时刷新,是非常好用的。另外就是编译的时候会对代码进行检查,如果有错误会直接在控制台报错。最后就是发布安卓包和苹果包了。因为我没有申请相应的开发者证书,不能进行本地打包。这里还是要对HBuilder X 再点一个赞的。它在软件内部集成了一个打包工具,支持本地打包和云打包。如果使用云打包还可以使用Dcloud 公用的开发者证书进行测试,或者使用自己的开发者证书。非常非常的方便。我将自己做的第一个新闻列表、新闻详情程序打包,安装到了安卓手机上测试了下,效果非常棒。因为我的苹果手机没有越狱,我也不想对自己手机越狱,就没有测试苹果系统下效果。用Mock模拟虚拟数据。在看这个项目之前,我一直没有使用过Mock数据,进行开发。主要是我没有接触过。但是刚好在接这个项目的前一天,我知道了还有Mock数据这个东西,然后就学习了一下。我才发现自己是有多么的懒惰,没有学习这个技能。Mock数据简直是前端开发的神器啊。在实际项目开发中,经常是前端开发完成了,后端数据还没有到。等后端数据到了,会发现接口跟当初预定的接口不一致了,等各种意想不到的问题。尤其是在等接口的过程中,我以前是自己写假数据。但是当后端接口数据提供之后,会发现自己写的假数据有问题,然后就影响到我的代码了。经常搞得自己焦头烂额。Mock数据就是为了解决这个痛点的。通过对比学习,我决定使用Easy-Mock。使用方法和注意事项可以参考数据模拟神器 easy-mock 正式开源之所以采用这种平台化的Mock数据,是因为我没有花时间去看怎么在本地搭建一个测试服务。直接就拿来主义了。总结通过这个新闻列表的程序,快速上手了uni-app,完成了一套代码,多端运行。按照官网给出的说法是,学习成本非常低的,但对比我个人,我觉得还是有成本的,至少要理解其中的概念,才能更愉快的上手项目。需要熟练掌握至少以下知识点Vue的语法,至少要知道如何创建文件、怎样绑定数据、怎样通讯、如何绑定事件、怎样通过绑定数据刷新页面。我虽然没有做过vue的项目,但是好在我有看过vue文档,并练习了官网的例子。否则,在运行这个项目之前我得花不少时间去vue官网学习vue微信小程序相关知识。同样的,如果完全没有看过微信小程序相关知识,同样会遇到问题,尤其是后续开发会用到微信小程序APIcss3、flex布局、ES6、打包、发布、模块化开发等等。相关的知识点,都需要学习、强化。可以发现,通过uni-app,就将目前主流的技术全都链接起来了。这样的好处是非常多的。对我个人而言,可以学习新框架、开发IOS和Android APP项目、串联起来目前主流的技术栈,积累经验。另外最重要的就是,通过这个过程,还能挣一笔零花钱~ 不仅学习了知识,积累了项目经验,还有伙食费,真是好处多多啊~目前已经上手了这个框架,下一步就应该考虑将mui项目,修改成uni-app项目了。希望这个过程是幸福并快乐的。目前项目是180多个html页面,希望采用vue之后,页面数量能减少一半。(完) ...

November 15, 2018 · 1 min · jiezi

iOS开发必会的坐标系探究

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由落影发表于云+社区专栏前言app在渲染视图时,需要在坐标系中指定绘制区域。 这个概念看似乎简单,事实并非如此。When an app draws something in iOS, it has to locate the drawn content in a two-dimensional space defined by a coordinate system. This notion might seem straightforward at first glance, but it isn’t.正文我们先从一段最简单的代码入手,在drawRect中显示一个普通的UILabel; 为了方便判断,我把整个view的背景设置成黑色:- (void)drawRect:(CGRect)rect { [super drawRect:rect]; CGContextRef context = UIGraphicsGetCurrentContext(); NSLog(@“CGContext default CTM matrix %@”, NSStringFromCGAffineTransform(CGContextGetCTM(context))); UILabel *testLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 28)]; testLabel.text = @“测试文本”; testLabel.font = [UIFont systemFontOfSize:14]; testLabel.textColor = [UIColor whiteColor]; [testLabel.layer renderInContext:context];}这段代码首先创建一个UILabel,然后设置文本,显示到屏幕上,没有修改坐标。 所以按照UILabel.layer默认的坐标(0, 0),在左上角进行了绘制。UILabel绘制接着,我们尝试使用CoreText来渲染一段文本。- (void)drawRect:(CGRect)rect { [super drawRect:rect]; CGContextRef context = UIGraphicsGetCurrentContext(); NSLog(@“CGContext default matrix %@”, NSStringFromCGAffineTransform(CGContextGetCTM(context))); NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:@“测试文本” attributes:@{ NSForegroundColorAttributeName:[UIColor whiteColor], NSFontAttributeName:[UIFont systemFontOfSize:14], }]; CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef) attrStr); // 根据富文本创建排版类CTFramesetterRef UIBezierPath * bezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 100, 20)]; CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), bezierPath.CGPath, NULL); // 创建排版数据 CTFrameDraw(frameRef, context);}首先用NSString创建一个富文本,然后根据富文本创建CTFramesetterRef,结合CGRect生成的UIBezierPath,我们得到CTFrameRef,最终渲染到屏幕上。 但是结果与上文不一致:文字是上下颠倒。CoreText的文本绘制从这个不同的现象开始,我们来理解iOS的坐标系。坐标系概念在iOS中绘制图形必须在一个二维的坐标系中进行,但在iOS系统中存在多个坐标系,常需要处理一些坐标系的转换。 先介绍一个图形上下文(graphics context)的概念,比如说我们常用的CGContext就是Quartz 2D的上下文。图形上下文包含绘制所需的信息,比如颜色、线宽、字体等。用我们在Windows常用的画图来参考,当我们使用画笔????在白板中写字时,图形上下文就是画笔的属性设置、白板大小、画笔位置等等。iOS中,每个图形上下文都会有三种坐标: 1、绘制坐标系(也叫用户坐标系),我们平时绘制所用的坐标系; 2、视图(view)坐标系,固定左上角为原点(0,0)的view坐标系; 3、物理坐标系,物理屏幕中的坐标系,同样是固定左上角为原点;根据我们绘制的目标不同(屏幕、位图、PDF等),会有多个context;Quartz常见的绘制目标不同context的绘制坐标系各不相同,比如说UIKit的坐标系为左上角原点的坐标系,CoreGraphics的坐标系为左下角为原点的坐标系;CoreGraphics坐标系和UIKit坐标系的转换CoreText基于CoreGraphics,所以坐标系也是CoreGraphics的坐标系。 我们回顾下上文提到的两个渲染结果,我们产生如下疑问: UIGraphicsGetCurrentContext返回的是CGContext,代表着是左下角为原点的坐标系,用UILabel(UIKit坐标系)可以直接renderInContext,并且“测”字对应为UILabel的(0,0)位置,是在左上角? 当用CoreText渲染时,坐标是(0,0),但是渲染的结果是在左上角,并不是在左下角;并且文字是上下颠倒的。 为了探究这个问题,我在代码中加入了一行log: NSLog(@“CGContext default matrix %@”, NSStringFromCGAffineTransform(CGContextGetCTM(context))); 其结果是CGContext default matrix [2, 0, 0, -2, 0, 200]; CGContextGetCTM返回是CGAffineTransform仿射变换矩阵: 一个二维坐标系上的点p,可以表达为(x, y, 1),乘以变换的矩阵,如下:把结果相乘,得到下面的关系此时,我们再来看看打印的结果[2, 0, 0, -2, 0, 200],可以化简为 x’ = 2x, y’ = 200 - 2y 因为渲染的view高度为100,所以这个坐标转换相当于把原点在左下角(0,100)的坐标系,转换为原点在左上角(0,0)的坐标系!通常我们都会使用UIKit进行渲染,所以iOS系统在drawRect返回CGContext的时候,默认帮我们进行了一次变换,以方便开发者直接用UIKit坐标系进行渲染。我们尝试对系统添加的坐标变换进行还原: 先进行CGContextTranslateCTM(context, 0, self.bounds.size.height); 对于x’ = 2x, y’ = 200 - 2y,我们使得x=x,y=y+100;(self.bounds.size.height=100) 于是有x’ = 2x, y’ = 200-2(y+100) = -2y; 再进行CGContextScaleCTM(context, 1.0, -1.0); 对于x’ = 2x, y’ = -2y,我们使得x=x, y=-y; 于是有 x’=2x, y’ = -2(-y) = 2y;- (void)drawRect:(CGRect)rect { [super drawRect:rect]; CGContextRef context = UIGraphicsGetCurrentContext(); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0); NSLog(@“CGContext default matrix %@”, NSStringFromCGAffineTransform(CGContextGetCTM(context))); NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:@“测试文本” attributes:@{ NSForegroundColorAttributeName:[UIColor whiteColor], NSFontAttributeName:[UIFont systemFontOfSize:14], }]; CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef) attrStr); // 根据富文本创建排版类CTFramesetterRef UIBezierPath * bezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 100, 20)]; CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), bezierPath.CGPath, NULL); // 创建排版数据 CTFrameDraw(frameRef, context);}通过log也可以看出来CGContext default matrix [2, 0, -0, 2, 0, 0]; 最终结果如下,文本从左下角开始渲染,并且没有出现上下颠倒的情况。 这时我们产生新的困扰: 用CoreText渲染文字的上下颠倒现象解决,但是修改后的坐标系UIKit无法正常使用,如何兼容两种坐标系? iOS可以使用CGContextSaveGState()方法暂存context状态,然后在CoreText绘制完后通过CGContextRestoreGState ()可以恢复context的变换。- (void)drawRect:(CGRect)rect { [super drawRect:rect]; CGContextRef context = UIGraphicsGetCurrentContext(); NSLog(@“CGContext default matrix %@”, NSStringFromCGAffineTransform(CGContextGetCTM(context))); CGContextSaveGState(context); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0); NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:@“测试文本” attributes:@{ NSForegroundColorAttributeName:[UIColor whiteColor], NSFontAttributeName:[UIFont systemFontOfSize:14], }]; CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef) attrStr); // 根据富文本创建排版类CTFramesetterRef UIBezierPath * bezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 100, 20)]; CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), bezierPath.CGPath, NULL); // 创建排版数据 CTFrameDraw(frameRef, context); CGContextRestoreGState(context); NSLog(@“CGContext default CTM matrix %@”, NSStringFromCGAffineTransform(CGContextGetCTM(context))); UILabel *testLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 20)]; testLabel.text = @“测试文本”; testLabel.font = [UIFont systemFontOfSize:14]; testLabel.textColor = [UIColor whiteColor]; [testLabel.layer renderInContext:context];}渲染结果如下,控制台输出的两个matrix都是[2, 0, 0, -2, 0, 200]; 遇到的问题1、UILabel.layer在drawContext的时候frame失效初始化UILabel时设定了frame,但是没有生效。 UILabel *testLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 100, 28)]; 这是因为frame是在上一层view中坐标的偏移,在renderInContext中坐标起点与frame无关,所以需要修改的是bounds属性: testLabel.layer.bounds = CGRectMake(50, 50, 100, 28);2、renderInContext和drawInContext的选择在把UILabel.layer渲染到context的时候,应该采用drawInContext还是renderInContext?虽然这两个方法都可以生效,但是根据画线部分的内容来判断,还是采用了renderInContext,并且问题1就是由这里的一句Renders in the coordinate space of the layer,定位到问题所在。3、如何理解CoreGraphics坐标系不一致后,会出现绘制结果异常?我的理解方法是,我们可以先不考虑坐标系变换的情况。 如下图,上半部分是普通的渲染结果,可以很容易的想象; 接下来是增加坐标变换后,坐标系变成原点在左上角的顶点,相当于按照下图的虚线进行了一次垂直的翻转。也可以按照坐标系变换的方式去理解,将左下角原点的坐标系相对y轴做一次垂直翻转,然后向上平移height的高度,这样得到左上角原点的坐标系。附录Drawing and Printing Guide for iOS Quartz 2D Programming Guide相关阅读【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...

November 13, 2018 · 2 min · jiezi

手拉手带你极速构建漂亮的跨平台(iOS/Android)移动应用 ✿ 环境搭建

上篇文章带大家认识了 Flutter ,想必大家已迫不及待的想练练手,所以要行动起来,现在这篇文章就带您搭建一个 Flutter 运行及开发环境。文章详情可查阅我的博客 https://h.lishaoy.net ,欢迎大家访问。安装 Flutter SDK想要在本地电脑上运行 Flutter ,需要安装 Flutter SDK 才可以运行, SDK 里面有一些用于创建、构建、测试和编译应用程序的命令行工具等,这些在开发的时候会用到。首先,我们有 2 种方法获取 SDK可以到 下载 Flutter SDK 到本地电脑可以用 git clone 命令下载到本地电脑git clone -b master https://github.com/flutter/flutter.git其次,把下载下来的 Flutter SDK 解压,放到系统的某个目录,比如我是放到: /Applications/flutter ,如图:配置环境变量配置环境变量的目的是为了让 Flutter SDK 命令行工具在全局范围都起作用,以便开发使用。首先,您可以用编辑器打开主目录下的 .bash_profile,或者用 vi 命令编辑,我习惯用 vi 命令,如下vi $HOME/.bash_profile新增以下配置export PATH=$PATH:/Applications/flutter/binexport PUB_HOSTED_URL=https://pub.flutter-io.cnexport FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn Tips: 第一行 export PATH=$PATH:/Applications/flutter/bin 中的 /Applications/flutter/bin 就是刚才下载的 Flutter SDK 解压后放在本地电脑的目录,您要根据自己操作更改为自己电脑对应的目录。第二、三行为解决国内下载或更新资源慢的国内镜像,配置这个下载或更新资源会快一些。再执行 source $HOME/.bash_profile 命令刷新当前命令行窗口,或者关掉当前命令行窗口重新打开,效果一样source $HOME/.bash_profile再执行 flutter –help,来测试环境变量是否配置成功,如图: Tips: 如果你使用的是 zsh,需要在 ~/.zshrc 文件中添加:source ~/.bash_profile ,否则 flutter 命令将无法运行。配置 iOS 开发环境想用 Flutter 为 iOS 平台开发应用,需要安装 Xcode,我们可以去苹果应用商店下载。安装好 Xcode 后,你需要打开一次 Xcode 同意许可协议(会提示),或者执行 sudo xcodebuild -license 同意许可协议。然后执行 open -a Simulator 命令,就可以打开一个模拟器,来运行和测试 Flutter 程序,如图配置 Android 开发环境想用 Flutter 为 Android 平台开发应用,需要下载安装 Android Studio。安装好 Android Studio 后,启动它,首次启动会安装最新的 Android SDK ,但是你可能会遇到这样的问题,如图:如果遇到这个问题应该就是网络问题(需要科学上网),点 Setup Proxy 来设置代理,如图:如一切正常,就会提示你需要下载一些东西,如图点击 Finish 按钮后就会下载安装以上列表的东西,下载安装完 SDK 后,如图:需要我们打开一个项目,我们可以用刚才已经配置好的 Flutter SDK 的命令行创建一个 Flutter 项目,如执行以下命令cd ~/desktopflutter create new_flutter命令执行完成后,在桌面就会生成一个 Flutter 项目,再用 Android Studio 打开,项目打开后会提示安装 Flutter 插件和依赖 Dart 语言插件 ,安装完之后我们可以去创建一个模拟器。打开 Tools>AVD Manager ,点击 Create Virtual Device… 来创建一个模拟器,选择一个设备,点击 Next,如图为模拟器选择一个系统镜像(我选择的是第一个),点击 Download ,下载完成后,点击 Next 后,如图最后,在模拟性能这里选择 Hardware - GLES 2.0 启动硬件加速,点击 Finish 完成配置编辑器前面我们已经配置好了 Flutter SDK 、iOS 模拟器 、Android 模拟器 ,最后我们还需要配置一下编辑器,当然您可以选择 Android Studio 或者 VS Code,这里我选择的是轻量级的 VS Code。如对 VS Code 不是很熟悉,可参考我之前写的 VS Code 编辑技巧打开终端进入我们刚才新建的 Flutter 项目cd new_flutter再用 VS Code 打开项目code ./打开项目之后 ⌘ - ⇧ - X ,打开扩展,安装 Flutter 插件,如图完成之后,打开项目目录 lib->main.dart 文件, VS Code 会自动提示你安装 Dart 语言扩展包。运行项目现在,所有的准备工作都完成了,就可以开发、测试或运行项目了,在上面我们用 Flutter create 命令创建的 Flutter 项目,自带一个计数器的小功能,我们可以运行看看效果首先,您需要执行 flutter doctor 来检查一下环境是否正常如上图第二项提示 Android license status unknown. 意思是 Android 协议没安装好,可以执行以下命令,来解决问题flutter doctor –android-licenses如上图第三项是 iOS 真机的检查项,可以按照提示操作>如上图第四项是 Java 的编辑器检查,可不用理会,如你没有安装 IDEA 也不会有这个提示其实在我另一台电脑上全部都配置好了 ???? ,如图最后,在 VS Code 编辑器里按 F5 后,会让你选择模拟器来运行 Flutter 程序,如图这个是分别在 iOS 和 Android 运行 Flutter 的效果,如图运行 Flutter 案例现在所有的都准备好了,您可以去我的 GitHub 上下载上篇文章中的案例代码,也可以 git clonecd $HOME/Desktop #进到桌面git clone https://github.com/persilee/flutter_pro.git #下载案例cd flutter_pro #进入案例目录flutter packages get #获取依赖包code ./ #用 VS Code 打开完成以上步骤后,在 VS Code 按 F5 选择模拟器,查看运行效果,如图好的,大功告成,这篇到处为止,下篇将手拉手带大家完成一个实操小案例 。 ...

November 12, 2018 · 2 min · jiezi

Category 特性在 iOS 组件化中的应用与管控

背景iOS Category功能简介Category 是 Objective-C 2.0之后添加的语言特性。Category 就是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。在 Objective-C(iOS 的开发语言,下文用 OC 代替)中的具体体现为:实例(类)方法、属性和协议。除了引用中提到的添加方法,Category 还有很多优势,比如将一个类的实现拆分开放在不同的文件内,以及可以声明私有方法,甚至可以模拟多继承等操作,具体可参考官方文档Category。若 Category 添加的方法是基类已经存在的,则会覆盖基类的同名方法。本文将要提到的组件间通信都是基于这个特性实现的,在本文的最后则会提到对覆盖风险的管控。组件通信的背景随着移动互联网的快速发展,不断迭代的移动端工程往往面临着耦合严重、维护效率低、开发不够敏捷等常见问题,因此越来越多的公司开始推行“组件化”,通过解耦重组组件来提高并行开发效率。但是大多数团队口中的“组件化”就是把代码分库,主工程使用 CocoaPods 工具把各个子库的版本号聚合起来。但能合理的把组件分层,并且有一整套工具链支撑发版与集成的公司较少,导致开发效率很难有明显地提升。处理好各个组件之间的通信与解耦一直都是组件化的难点。诸如组件之间的 Podfile 相互显式依赖,以及各种联合发版等问题,若处理不当可能会引发“灾难”性的后果。目前做到 ViewController (指iOS中的页面,下文用VC代替)级别解耦的团队较多,维护一套 mapping 关系并使用 scheme 进行跳转,但是目前仍然无法做到更细粒度的解耦通信,依然满足不了部分业务的需求。实际业务案例例1:外卖的首页的商家列表(WMPageKit),在进入一个商家(WMRestaurantKit)选择5件商品返回到首页的时候,对应的商家cell需要显示已选商品“5”。例2:搜索结果(WMSearchKit)跳转到商超的容器页(WMSupermarketKit),需要传递一个通用Domain(也有的说法叫模型、Model、Entity、Object等等,下文统一用Domain表示)。例3:做一键下单需求(WMPageKit),需要调用下单功能的一个方法(WMOrderKit)入参是一个订单相关 Domain 和一个 VC,不需要返回值。这几种场景基本涵盖了组件通信所需的的基本功能,那么怎样才可以实现最优雅的解决方案?组件通信的探索模型分析对于上文的实际业务案例,很容易想到的应对方案有三种,第一是拷贝共同依赖代码,第二是直接依赖,第三是下沉公共依赖。对于方案一,会维护多份冗余代码,逻辑更新后代码不同步,显然是不可取的。对于方案二,对于调用方来说,会引入较多无用依赖,且可能造成组件间的循环依赖问题,导致组件无法发布。对于方案三,其实是可行解,但是开发成本较大。对于下沉出来的组件来说,其实很难找到一个明确的定位,最终沦为多个组件的“大杂烩”依赖,从而导致严重的维护性问题。那如何解决这个问题呢?根据面向对象设计的五大原则之一的“依赖倒置原则”(Dependency Inversion Principle),高层次的模块不应该依赖于低层次的模块,两者(的实现)都应该依赖于抽象接口。推广到组件间的关系处理,对于组件间的调用和被调用方,从本质上来说,我们也需要尽量避免它们的直接依赖,而希望它们依赖一个公共的抽象层,通过架构工具来管理和使用这个抽象层。这样我们就可以在解除组件间在构建时不必要的依赖,从而优雅地实现组件间的通讯。业界现有方案的几大方向实践依赖倒置原则的方案有很多,在 iOS 侧,OC 语言和 Foundation 库给我们提供了数个可用于抽象的语言工具。在这一节我们将对其中部分实践进行分析。1.使用依赖注入代表作品有 Objection 和 Typhoon,两者都是 OC 中的依赖注入框架,前者轻量级,后者较重并支持 Swift。比较具有通用性的方法是使用「协议」 <-> 「类」绑定的方式,对于要注入的对象会有对应的 Protocol 进行约束,会经常看到一些RegisterClass:ForProtocol:和classFromProtocol的代码。在需要使用注入对象时,用框架提供的接口以协议作为入参从容器中获得初始化后的所需对象。也可以在 Register 的时候直接注册一段 Block-Code,这个代码块用来初始化自己,作为id类型的返回值返回,可以支持一些编译检查来确保对应代码被编译。美团内推行将一些运行时加载的操作前移至编译时,比如将各项注册从 +load 改为在编译期使用__attribute((used,section("__DATA,key"))) 写入 mach-O 文件 Data 的 Segment 中来减少冷启动的时间消耗。因此,该方案的局限性在于:代码块存取的性能消耗较大,并且协议与类的绑定关系的维护需要花费更多的时间成本。2.基于SPI机制全称是 Service Provider Interfaces,代表作品是 ServiceLoader。实现过程大致是:A库与B库之间无依赖,但都依赖于P平台。把B库内的一个接口I下沉到平台层(“平台层”也叫做“通用能力层”,下文统一用平台层表示),入参和返回值的类型需要平台层包含,接口I的实现放在B库里(因为实现在B库,所以实现里可以正常引用B库的元素)。然后A库通过P平台的这个接口I来实现功能。A可以调用的到接口I,但是在B的库中进行实现。在A库需要通过一个接口I实例化出一个对象,使用ServiceLoader.load(接口,key),通过注册过的key使用反射找到这个接口imp的文件路径然后得到这个实例对象调用对应接口。这个操作在安卓中使用较为广泛,大致相当于用反射操作来替代一次了 import 这样的耦合引用。但实际上iOS中若使用反射来实现功能则完全不必这么麻烦。关于反射,Java可以实现类似于ClassFromString的功能,但是无法直接使用 MethodFromString的功能。并且ClassFromString也是通过字符串map到这个类的文件路径,类似于 com.waimai.home.searchImp,从而可以获得类型然后实例化,而OC的反射是通过消息机制实现。3.基于通知中心之前和一个做读书类App的同学交流,发现行业内有些公司的团队在使用 NotificationCenter 进行一些解耦的通信,因为通知中心本身支持传递对象,并且通知中心的功能也原生支持同步执行,所以也可以达到目的。通知中心在iOS 9之后有一次比较大的升级,将通知支持了 request 和 response 的处理逻辑,并支持获取到通知的发送者。比以往的通知群发但不感知发送者和是否收到,进步了很多。字符串的约定也可以理解为一个简化的协议,可设置成宏或常量放在平台层进行统一的维护。比较明显的缺陷是开发的统一范式难以约束,风格迥异,且字符串相较于接口而言还是难以管理。4.使用objc_msgSend这是iOS原生消息机制中最万能的方法,编写时会有一些硬编码。核心代码如下:id s = ((id(*)(id, SEL))objc_msgSend)(ClassName,@selector(methodName)); 这种方法的特点是即插即用,在开发者能100%确定整条调用链没问题的时候,可以快速实现功能。此方案的缺陷在于编写十分随意,检查和校验的逻辑还不够,满屏的强转。对于 int、Integer、NSNumber 这样的很容易发生类型转换错误,结果虽然不报错,但数字会有错误。方案对比接下来,我们对这几个大方向进行一些性能对比。考虑到在公司内的实际用法与限制,可能比常规方法增加了若干步骤,结果也可能会与常规裸测存在一定的偏差。例如依赖注入常用做法是存在单例(内存)里,但是我们为了优化冷启动时间都写入 mach-O 文件 Data 的 Segment 里了,所以在我们的统计口径下存取时间会相对较长。// 为了不暴露类名将业务属性用“some”代替,并隐藏初始化、循环100W次、差值计算等代码,关键操作代码如下// 存取注入对象xxConfig = [[WMSomeGlueCore sharedInstance] createObjectForProtocol:@protocol(WMSomeProtocol)];// 通知发送[[NSNotificationCenter defaultCenter]postNotificationName:@“nixx” object:nil];// 原生接口调用a = [WMSomeClass class];// 反射调用b = objc_getClass(“WMSomeClass”);运行结果显示如下:可以看出原生的接口调用明显是最高效的用法,反射的时长比原生要多一个数量级,不过100W次也就是多了几十毫秒,还在可以接受的范围之内。通知发送相比之下性能就很低了,存取注入对象更低。当然除了性能消耗外,还有很多不好量化的维度,包括规范约束、功能性、代码量、可读性等,笔者按照实际场景客观评价给出对比的分值。下面,我们用五种维度的能力值图来对比每一种方案优缺点:各维度的的评分考虑到了一定的实际场景,可能和常规结果稍有偏差。已经做了转化,看图面积越大越优。可读性的维度越长代表可读性越高,代码量的维度越长代表代码成本越少。如图2所示,可以看出上图的四种方式或多或少都存在一些缺点:依赖注入是因为美团的实际场景问题,所以在性能消耗上存在明显的短板,并且代码量和可读性都不突出,规范约束这里是亮点。SPI机制的范围图很大,但使用了反射,并且代码开发成本较高,实践上来看,对协议管理有一定要求。通知中心看上去挺方便,但发送与接收大多成对出现,还附带绑定方法或者Block,代码量并不少。而msgsend功能强大,代码量也少,但是在规范约束和可读性上几乎为零。综合看来 SPI 和 objc_msgSend 两者的特点比较明显,很有潜力,如果针对这两种方案分别进行一定程度的完善,应该可以实现一个综合评分更高的方案。从现有方案中完善或衍生出的方案5.使用Category+NSInvocation此方案从 objc_msgSend 演化而来。NSInvocation 的调用方式的底层还是会使用到 objc_msgSend,但是通过一些方法签名和返回值类型校验,可以解决很多类型规范相关的问题,并且这种方式没有繁琐的注册步骤,任何一次新接口的添加,都可以直接在低层的库中进行完成。为了更进一步限制调用者能够调用的接口,创建一些 Category 来提供接口,内部包装下层接口,把返回值和入参都限制实际的类型。业界比较接近的例子有 casatwy 的 CTMediator。6.原生CategoryCoverOrigin方式此方案从 SPI 方式演化而来。两个的共同点是都在平台层提供接口供业务方调用,不同点是此方式完全规避了各种硬编码。而且 CategoryCoverOrigin 是一个思想,没有任何框架代码,可以说 OC 的 Runtime 就是这个方案的框架支撑。此方案的核心操作是在基类里汇总所有业务接口,在上层的业务库中创建基类的 Category 中对声明的接口进行覆盖。整个过程没有任何硬编码与反射。演化出的这两种方案能力评估如下(绿色部分),图中也贴了和演化前方案(桔色部分)的对比:上文对这两种方案描述的非常概括,可能有同学会对能力评估存在质疑。接下来会分别进行详解的介绍,并描述在实际操作值得注意的细节。这两种方案组合成了外卖内部的组件通信框架 WMScheduler。WMScheduler组件通信外卖的 WMScheduler 主要是通过对 Category 特性的运用来实现组件间通信,实际操作中有两种的应用方案:Category+NSInvocation 和 Category CoverOrigin。1.Category+NSInvocation方案方案简介:这个方案将其对 NSInvocation 功能容错封装、参数判断、类型转换的代码写在下层,提供简易万能的接口。并在上层创建通信调度器类提供常用接口,在调度器的的 Category 里扩展特定业务的专用接口。所有的上层接口均有规范约束,这些规范接口的内部会调用下层的简易万能接口即可通过NSInvocation 相关的硬编码操作调用任何方法。UML图:如图3-1所示,代码的核心在 WMSchedulerCore 类,其包含了基于 NSInvocation 对 target 与 method 的操作、对参数的处理(包括对象,基本数据类型,NULL类型)、对异常的处理等等,最终开放了简洁的万能接口,接口参数有 target、method、parameters等等,然后内部帮我们完成调用。但这个接口并不是让上层业务直接进行调用,而是需要创建一个 WMSchedule r的 Category,在这个 Category 中编写规范的接口(前缀、入参类型、返回值类型都是确定的)。值得一提的是,提供业务专用接口的 Category 没有以 WMSchedulerCore 为基类,而是以 WMScheduler 为基类。看似多此一举,实际上是为了做权限的隔离。上层业务只能访问到 WMScheduler.h 及其 Category 的规范接口。并不能访问到 WMSchedulerCore.h 提供的“万能但不规范”接口。例如:在UML图中可以看到 外界只可以调用到wms_getOrderCountWithPoiid(规范接口),并不能使用wm_excuteInstance Method(万能接口)。为了更好地理解实际使用,笔者贴一个组件调用周期的完整代码:如图3-2,在这种方案下,“B库调用A库方法”的需求只需要改两个仓库的代码,需要改动的文件标了下划线,请仔细看下示例代码。示例代码:平台(通用功能)库三个文件:①// WMScheduler+AKit.h#import “WMScheduler.h”@interface WMScheduler(AKit)/** * 通过商家id查到当前购物车已选e的小红点数量 * @param poiid 商家id * @return 实际的小红点数量 */+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber )poiID;@end②// WMScheduler+AKit.m#import “WMSchedulerCore.h”#import “WMScheduler+AKit.h”#import “NSObject+WMScheduler.h”@implementation WMScheduler (AKit)+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber )poiID{ if (nil == poiid) { return 0; }#pragma clang diagnostic push#pragma clang diagnostic ignored “-Wundeclared-selector” id singleton = [wm_scheduler_getClass(“WMXXXSingleton”) wm_executeMethod:@selector(sharedInstance)]; NSNumber orderFoodCount = [singleton wm_executeMethod:@selector(calculateOrderedFoodCountWithPoiID:) params:@[poiID]]; return orderFoodCount == nil ? 0 : [orderFoodCount integerValue];#pragma clang diagnostic pop}@end③// WMSchedulerInterfaceList.h#ifndef WMSchedulerInterfaceList_h#define WMSchedulerInterfaceList_h// 这个文件会被加到上层业务的pch里,所以下文不用import本文件#import “WMScheduler.h”#import “WMScheduler+AKit.h”#endif / WMSchedulerInterfaceList_h /BKit (调用方)一个文件:// WMHomeVC.m@interface WMHomeVC () <UITableViewDataSource, UITableViewDelegate>@end@implementation WMHomeVC… NSUInteger foodCount = [WMScheduler wms_getOrderedFoodCountWithPoiID:currentPoi.poiID]; NSLog(@"%ld",foodCount);…@end代码分析:上文四个文件完成了一次跨组件的调用,在 WMScheduler+AKit.m 中的第30、31行,调用的都是AKit(提供方)的现有方法,因为 WMSchedulerCore 提供了 NSInvocation 的调用方式,所以可以直接向上调用。WMScheduler+AKit 中提供的接口就是上文说的“规范接口”,这个接口在WMHomeVC(调用方)调用时和调用本仓库内的OC方法,并没有区别。延伸思考:上文的例子中入参和返回值都是基本数据类型,Domain 也是支持的,前提是这个 Domain 是放在平台库的。我们可以将工程中的 Domain 分为BO(Business Object)、VO(View Object)与TO(Transfer Object),VO 经常出现在 view 和 cell,BO一般仅在各业务子库内部使用,这个TO则是需要放在平台库是用于各个组件间的通信的通用模型。例如:通用 PoiDomain,通用 OrderDomain,通用 AddressDomain 等等。这些称为 TO 的 Domain 可以作为规范接口的入参类型或返回值类型。在实际业务场景中,跳转页面时传递 Domain 的需求也是一个老生常谈的问题,大多数页面级跳转框架仅支持传递基本数据类型(也有 trick 的方式传 Domain 内存地址但很不优雅)。在有了上文支持的能力,我们可以在规范接口内通过万能接口获取目标页面的VC,并调用其某个属性的 set 方法将我们想传递的Domain赋值过去,然后将这个 VC 对象作为返回值返回。调用方获得这个 VC 后在当前的导航栈内push即可。上文代码中我们用 WMScheduler 调用了 Akit 的一个名为calculateOrderedFoodCount WithPoiID:的方法。那么有个争议点:在组件通信需要调用某方法时,是允许直接调用现有方法,还是复制一份加上前缀标注此方法专门用于提供组件通信? 前者的问题点在于现有方法可能会被修改,扩充参数会直接导致调用方找不到方法,Method 字符串的不会编译报错(上文平台代码 WMScheduler+AKit.m 中第31行)。后者的问题在于大大增加了开发成本。权衡后我们还是使用了前者,加了些特殊处理,若现有方法被修改了,则会在isReponseForSelector这里检查出来,并走到 else 的断言及时发现。阶段总结:Category+NSInvocation 方案的优点是便捷,因为 Category 的专用接口放在平台库,以后有除了 BKit 以外的其他调用方也可以直接调用,还有更多强大的功能。但是,不优雅的地方我们也列举一下:当这个跨组件方法内部的代码行数比较多时,会写很多硬编码。硬编码method字符串,在现有方法被修改时,编译检测不报错(只能靠断言约束)。下层库向上调用的设计会被诟病。接下来介绍的 CategoryCoverOrigin 的方案,可以解决这三个问题。2.CategoryCoverOrigin方案方案简介:首先说明下这个方案和 NSInvocation 没有任何关系,此方案与上一方案也是完全不同的两个概念,不要将上一个方案的思维带到这里。此方案的思路是在平台层的 WMScheduler.h 提供接口方法,接口的实现只写空实现或者兜底实现(兜底实现中可根据业务场景在 Debug 环境下增加 toast 提示或断言),上层库的提供方实现接口方法并通过 Category 的特性,在运行时进行对基类同名方法的替换。调用方则正常调用平台层提供的接口。在 CategoryCoverOrigin 的方案中 WMScheduler 的 Category 在提供方仓库内部,因此业务逻辑的依赖可以在仓库内部使用常规的OC调用。UML图:从图4-1可以看出,WMScheduler 的 Category 被移到了业务仓库,并且 WMScheduler 中有所有接口的全集。为了更好地理解 CategoryCover 实际应用,笔者再贴一个此方案下的完整完整代码:如图4-2,在这种方案下,“B库调用A库方法”的需求需要修改三个仓库的代码,但除了这四个编辑的文件,没有其他任何的依赖了,请仔细看下代码示例。示例代码:平台(通用功能库)两个文件①// WMScheduler.h@interface WMScheduler : NSObject// 这个文件是所有组件通信方法的汇总#pragma mark - AKit / * 通过商家id查到当前购物车已选e的小红点数量 * @param poiid 商家id * @return 实际的小红点数量 */+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber *)poiID;#pragma mark - CKit// …#pragma mark - DKit// …@end②// WMScheduler.m#import “WMScheduler.h”@implementation WMScheduler#pragma mark - Akit+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber *)poiID{ return 0; // 这个.m里只要求一个空实现 作为兜底方案。}#pragma mark - Ckit// …#pragma mark - Dkit// …@endAKit(提供方)一个 Category 文件:// WMScheduler+AKit.m#import “WMScheduler.h”#import “WMAKitBusinessManager.h”#import “WMXXXSingleton.h” // 直接导入了很多AKit相关的业务文件,因为本身就在AKit仓库内@implementation WMScheduler (AKit)// 这个宏可以屏蔽分类覆盖基类方法的警告#pragma clang diagnostic push#pragma clang diagnostic ignored “-Wobjc-protocol-method-implementation”// 在平台层写过的方法,这边是是自动补全的+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber *)poiID{ if (nil == poiid) { return 0; } // 所有AKIT相关的类都能直接接口调用,不需要任何硬编码,可以和之前的写法对比下。 WMXXXSingleton *singleton = [WMXXXSingleton sharedInstance]; NSNumber *orderFoodCount = [singleton calculateOrderedFoodCountWithPoiID:poiID]; return orderFoodCount == nil ? 0 : [orderFoodCount integerValue];}#pragma clang diagnostic pop@endBKit(调用方) 一个文件写法不变:// WMHomeVC.m@interface WMHomeVC () <UITableViewDataSource, UITableViewDelegate>@end@implementation WMHomeVC… NSUInteger *foodCount = [WMScheduler wms_getOrderedFoodCountWithPoiID:currentPoi.poiID]; NSLog(@"%ld",foodCount);…@end代码分析:CategoryCoverOrigin 的方式,平台库用 WMScheduler.h 文件存放所有的组件通信接口的汇总,各个仓库用注释隔开,并在.m文件中编写了空实现。功能代码编写在服务提供方仓库的 WMScheduler+AKit.m,看这个文件的17、18行业务逻辑是使用常规 OC 接口调用。在运行时此Category的方法会覆盖 WMScheduler.h 基类中的同名方法,从而达到目的。CategoryCoverOrigin 方式不需要其他功能类的支撑。延伸思考:如果业务库很多,方法很多,会不会出现 WMScheduler.h 爆炸? 目前我们的工程跨组件调用的实际场景不是很多,所以汇总在一个文件了,如果满屏都是跨组件调用的工程,则需要思考业务架构与模块划分是否合理这一问题。当然,如果真出现 WMScheduler.h 爆炸的情况,完全可以将各个业务的接口移至自己Category 的.h文件中,然后创建一个 WMSchedulerInterfaceList 文件统一 import 这些 Category。两种方案的选择刚才我们对于 Category+NSInvocation 和 CategoryCoverOrigin 两种方式都做了详细的介绍,我们再整理一下两者的优缺点对比:Category+NSInvocationCategoryCover优点只改两个仓库,流程上的时间成本更少可以实现url调用方法(scheme://target/method:?para=x)无任何硬编码,常规OC接口调用除了接口声明、分类覆盖、调用,没有其他多余代码不存在下层调用上层的场景缺点功能复杂时硬编码写法成本较大下层调上层,上层业务改变时会影响平台接口不能使用url调用方法新增接口时需改动三个仓库,稍有麻烦。(当接口已存在时,两种方式都只需修改一处)笔者更建议使用 CategoryCoverOrigin 的无硬编码的方案,当然具体也要看项目的实际场景,从而做出最优的选择。更多建议关于组件对外提供的接口,我们更倾向于借鉴 SPI 的思想,作为一个 Kit 哪些功能是需要对外公开的?提供哪些服务给其他方解耦调用?建议主动开放核心方法,尽量减少“用到才补”的场景。例如全局购物车就需要“提供获取小红点数量的方法”,商家中心就需要提供“根据字符串 id 得到整个 Poi 的 Domain”的接口服务。需要考虑到抽象能力,提供更有泛用性的接口。比如“获取到了最低满减价格后拼接成一个文案返回字符串” 这个方法,就没有“获取到了最低满减价格” 这个方法具备泛用性。Category 风险管控先举两个发生过的案例1. 2017年10月 一个关于NSDate重复覆盖的问题当时美团平台有 NSDate+MTAddition 类,在外卖侧有 NSDate+WMAddition 类。前者 NSDate+MTAddition 之前就有方法 getCurrentTimestamp,返回的时间戳是秒。后者 NSDate+WMAddition 在一次需求中也增加了 getCurrentTimestamp 方法,但是为了和其他平台统一口径返回值使用了毫秒。在正常的加载顺序中外卖类比平台类要晚,因此在外卖的测试中没有发现问题。但集成到 imeituan 主项目之后,原先其他业务方调用这个返回“秒”的方法,就被外卖测的返回“毫秒”的同名方法给覆盖了,出现接口错误和UI错乱等问题。2. 2018年3月 一个WMScheduler组件通信遇到的问题在外卖侧有订单组件和商家容器组件,这两个组件的联系是十分紧密的,有的功能放在两个仓库任意一个中都说的通。因此出现了了两个仓库写了同名方法的场景。在 WMScheduler+Restaurant 和 WMScheduler+Order 两个仓库都添加了方法 -(void)wms_enterGlobalCartPageFromPage:,在运行中这两处有一处被覆盖。在有一次 Bug 解决中,给其中一处增加了异常处理的代码,恰巧增加的这处先加载,就被后加载的同名方法覆盖了,这就导致了异常处理代码不生效的问题。那么使用 CategoryCover 的方式是不是很不安全? NO!只要弄清其中的规律,风险点都是完全可以管控的,接下来,我们来分析 Category 的覆盖原理。Category 方法覆盖原理1) Category 的方法没有“完全替换掉”原来类已经有的方法,也就是说如果 Category 和原来类都有methodA,那么 Category 附加完成之后,类的方法列表里会有两个 methodA。2) Category 方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的 Category 的方法会“覆盖”掉原来类的同名方法,这是因为运行过程中,我们在查找方法的时候会顺着方法列表的顺序去查找,它只要一找到对应名字的方法,就会罢休^_^,殊不知后面可能还有一样名字的方法。Category 在运行期进行决议,而基类的类是在编译期进行决议,因此分类中,方法的加载顺序一定在基类之后。美团曾经有一篇技术博客深入分析了 Category,并且从编译器和源码的角度对分类覆盖操作进行详细解析:深入理解Objective-C:Category根据方法覆盖的原理,我们可以分析出哪些操作比较安全,哪些存在风险,并针对性地进行管理。接下来,我们就介绍美团 Category 管理相关的一些工作。Category 方法管理由于历史原因,不管是什么样的管理规则,都无法直接“一刀切”。所以针对现状,我们将整个管理环节先拆分为“数据”、“场景”、 “策略”三部分。其中数据层负责发现异常数据,所有策略公用一个数据层。针对 Category 方法的数据获取,我们有如下几种方式:根据优缺点的分析,再考虑到美团已经彻底实现了“组件化”的工程,所以对 Category 的管控最好放在集成阶段以后进行。我们最终选择了使用 linkmap 进行数据获取,具体方法我们将在下文进行介绍。策略部分则针对不同的场景异常进行控制,主要的开发工作位于我们的组件化 CI 系统上,即之前介绍过的 Hyperloop 系统。Hyperloop 本身即提供了包括白名单,发布集成流程管理等一系列策略功能,我们只需要将工具进行关联开发即可。我们开发的数据层作为一个独立组件,最终也是运行在 Hyperloop 上。根据场景细分的策略如下表所示(需要注意的是,表中有的场景实际不存在,只是为了思考的严谨列出):我们在前文描述的 CategoryCoverOrigin 的组件通信方案的管控体现在第2点。风险管控中提到的两个案例的管控主要体现在第4点。Category 数据获取原理上一章节,我们提到了采用 linkmap 分析的方式进行 Category 数据获取。在这一章节内,我们详细介绍下做法。启用 linkmap首先,linkmap 生成功能是默认关闭的,我们需要在 build settings 内手动打开开关并配置存储路径。对于美团工程和美团外卖工程来说,每次正式构建后产生的 linkmap,我们还会通过内部的美团云存储工具进行持久化的存储,保证后续的可追溯。linkmap 组成若要解析 linkmap,首先需要了解 linkmap 的组成。如名称所示,linkmap 文件生成于代码链接之后,主要由4个部分组成:基本信息、Object files 表、Sections 表和 Symbols 表。前两行是基本信息,包括链接完成的二进制路径和架构。如果一个工程内有多个最终产物(如 Watch App 或 Extension),则经过配置后,每一个产物的每一种架构都会生成一份 linkmap。# Path: /var/folders/tk/xmlx38_x605127f0fhhp_n1r0000gn/T/d20180828-59923-v4pjhg/output-sandbox/DerivedData/Build/Intermediates.noindex/ArchiveIntermediates/imeituan/InstallationBuildProductsLocation/Applications/imeituan.app/imeituan# Arch: arm64第二部分的 Object files,列举了链接所用到的所有的目标文件,包括代码编译出来的,静态链接库内的和动态链接库(如系统库),并且给每一个目标文件分配了一个 file id。# Object files:[ 0] linker synthesized[ 1] dtrace[ 2] /var/folders/tk/xmlx38_x605127f0fhhp_n1r0000gn/T/d20180828-59923-v4pjhg/output-sandbox/DerivedData/Build/Intermediates.noindex/ArchiveIntermediates/imeituan/IntermediateBuildFilesPath/imeituan.build/DailyBuild-iphoneos/imeituan.build/Objects-normal/arm64/main.o……[ 26] /private/var/folders/tk/xmlx38_x605127f0fhhp_n1r0000gn/T/d20180828-59923-v4pjhg/repo-sandbox/imeituan/Pods/AFNetworking/bin/libAFNetworking.a(AFHTTPRequestOperation.o)……[25919] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.3.sdk/usr/lib/libobjc.tbd[25920] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.3.sdk/usr/lib/libSystem.tbd第三部分的 Sections,记录了所有的 Section,以及它们所属的 Segment 和大小等信息。# Sections:# Address Size Segment Section0x100004450 0x07A8A8D0 __TEXT __text……0x109EA52C0 0x002580A0 __DATA __objc_data0x10A0FD360 0x001D8570 __DATA __data0x10A2D58D0 0x0000B960 __DATA __objc_k_kylin……0x10BFE4E5D 0x004CBE63 __RODATA __objc_methname0x10C4B0CC0 0x000D560B __RODATA __objc_classname第四部分的 Symbols 是重头戏,列举了所有符号的信息,包括所属的 object file、大小等。符号除了我们关注的 OC 的方法、类名、协议名等,也包含 block、literal string 等,可以供其他需求分析进行使用。# Symbols:# Address Size File Name0x1000045B8 0x00000060 [ 2] ___llvm_gcov_writeout0x100004618 0x00000028 [ 2] ___llvm_gcov_flush0x100004640 0x00000014 [ 2] ___llvm_gcov_init0x100004654 0x00000014 [ 2] ___llvm_gcov_init.40x100004668 0x00000014 [ 2] ___llvm_gcov_init.60x10000467C 0x0000015C [ 3] _main……0x10002F56C 0x00000028 [ 38] -[UIButton(_AFNetworking) af_imageRequestOperationForState:]0x10002F594 0x0000002C [ 38] -[UIButton(_AFNetworking) af_setImageRequestOperation:forState:]0x10002F5C0 0x00000028 [ 38] -[UIButton(_AFNetworking) af_backgroundImageRequestOperationForState:]0x10002F5E8 0x0000002C [ 38] -[UIButton(_AFNetworking) af_setBackgroundImageRequestOperation:forState:]0x10002F614 0x0000006C [ 38] +[UIButton(AFNetworking) sharedImageCache]0x10002F680 0x00000010 [ 38] +[UIButton(AFNetworking) setSharedImageCache:]0x10002F690 0x00000084 [ 38] -[UIButton(AFNetworking) imageResponseSerializer]……linkmap 数据化根据上文的分析,在理解了 linkmap 的格式后,通过简单的文本分析即可提取数据。由于美团内部 iOS 开发工具链统一采用 Ruby,所以 linkmap 分析也采用 Ruby 开发,整个解析器被封装成一个 Ruby Gem。具体实施上,处于通用性考虑,我们的 linkmap 解析工具分为解析、模型、解析器三层,每一层都可以单独进行扩展。对于 Category 分析器来说,link map parser 解析指定 linkmap,生成通用模型的实例。从实例中获取 symbol 类,将名字中有“()”的符号过滤出来,即为 Category 方法。接下来只要按照方法名聚合,如果超过1个则肯定有 Category 方法冲突的情况。按照上一节中分析的场景,分析其具体冲突类型,提供结论输出给 Hyperloop。具体对外接口可以直接参考我们的工具测试用例。最后该 Gem 会直接被 Hyperloop 使用。 it ‘should return a map with keys for method name and classify’ do @parser = LinkmapParser::Parser.new @file_path = ‘spec/fixtures/imeituan-LinkMap-normal-arm64.txt’ @analyze_result_with_classification = @parser.parse @file_path expect(@analyze_result_with_classification.class).to eq(Hash) # Category 方法互相冲突 symbol = @analyze_result_with_classification["-[NSDate isEqualToDateDay:]"] expect(symbol.class).to eq(Hash) expect(symbol[:type]).to eq([LinkmapParser::CategoryConflictType::CONFLICT]) expect(symbol[:detail].class).to eq(Array) expect(symbol[:detail].count).to eq(3) # Category 方法覆盖原方法 symbol = @analyze_result_with_classification["-[UGCReviewManager setCommonConfig:]"] expect(symbol.class).to eq(Hash) expect(symbol[:type]).to eq([LinkmapParser::CategoryConflictType::REPLACE]) expect(symbol[:detail].class).to eq(Array) expect(symbol[:detail].count).to eq(2) endCategory 方法管理总结1. 风险管理对于任何语法工具,都是有利有弊的。所以除了发掘它们在实际场景中的应用,也要时刻对它们可能带来的风险保持警惕,并选择合适的工具和时机来管理风险。而 Xcode 本身提供了不少的工具和时机,可以供我们分析构建过程和产物。若是在日常工作中遇到一些坑,不妨从构建期工具的角度去考虑管理。比如本文内提到的 linkmap,不仅可以用于 Category 分析,还可以用于二进制大小分析、组件信息管理等。投入一定资源在相关工具开发上,往往可以获得事半功倍的效果。2. 代码规范回到 Category 的使用,除了工具上的管控,我们也有相应的代码规范,从源头管理风险。如我们在规范中要求所有的 Category 方法都使用前缀,降低无意冲突的可能。并且我们也计划把“使用前缀”做成管控之一。3. 后续规划1.覆盖系统方法检查 由于目前在管控体系内暂时没有引入系统符号表,所以无法对覆盖系统方法的行为进行分析和拦截。我们计划后续和 Crash 分析系统打通符号表体系,提早发现对系统库的不当覆盖。2.工具复用 当前的管控系统仅针对美团外卖和美团 App,未来计划推广到其他 App。由于有 Hyperloop,事情在技术上并没有太大的难度。 从工具本身的角度看,我们有计划在合适的时机对数据层代码进行开源,希望能对更多的开发有所帮助。总结在这篇文章中,我们从具体的业务场景入手,总结了组件间调用的通用模型,并对常用的解耦方案进行了分析对比,最终选择了目前最适合我们业务场景的方案。即通过 Category 覆盖的方式实现了依赖倒置,将构建时依赖延后到了运行时,达到我们预期的解耦目标。同时针对该方案潜在的问题,通过 linkmap 工具管控的方式进行规避。另外,我们在模型设计时也提到,组件间解耦其实在 iOS 侧有多种方案选择。对于其他的方案实践,我们也会陆续和大家分享。希望我们的工作能对大家的 iOS 开发组件间解耦工作有所启发。作者简介尚先,美团资深工程师。2015年加入美团,目前作为美团外卖 iOS 端平台化虚拟小组组长,主要负责业务架构、持续集成和工程化相关工作。同时也是移动端领域新技术的爱好者,负责多项新技术在外卖业务落地中的难点攻关,目前个人拥有七项国家发明专利。泽响,美团技术专家,2014年加入美团,先后负责过公司 iOS 持续集成体系建设,美团 iOS 端平台业务,美团 iOS 端基础业务等工作。目前作为美团移动平台架构平台组 Team Leader,主要负责美团 App 平台架构、组件化、研发流程优化和部分基础设施建设,致力于提升平台上全业务的研发效率与质量。招聘信息美团外卖长期招聘 iOS、Android、FE 高级/资深工程师和技术专家,Base 北京、上海、成都,欢迎有兴趣的同学投递简历到 chenhang03@meituan.com。 ...

November 9, 2018 · 4 min · jiezi

浅谈easy-mock 最好的备胎没有之一

引言 今天我们来聊聊Mock,随着互联网发展,这两年前后端分离的开发模式兴起,Mock也从以住的幕后走上了台面,让更多的人而得知,以前传统的开发方式Mock大多局限在后端人员接触较多一些。 Mock已经是老生常谈了,网上一搜索就很多,各位前辈们都讲的很到位,但今天我只讲它——easy-mock。 为什么会突然来聊它,这个就说来话长了,个人简介里就说过,专注于分享工作中遇到的坑,但这一次不是我的坑,来源于QQ群友(# 如果您有想知道的故事,而正好我也会,那么就由我为您讲出来吧,欢迎留言哦 # ),请看下图:这里是@IT·平头哥联盟,我是首席填坑官—苏南,用心分享 做有温度的攻城狮。什么是Mock 什么是Mock?? Mock其实就是真实数据存在之前,即调试期间的代替品,是个虚拟的存在,用人话讲它就是个备胎,如女生长的好看,追她的人多,但又不是很满意但也不拒绝,在自己心仪的小哥哥出现之前,一直吊着你????!如何Mock数据?不要告诉我 new 一个哦,对象可以 new,备胎可new不出来呢????;方法一:最low的方式将 Mock 数据写在代码里、json文件里;方法二:利用 Charles 、Fiddler等代理工具,将 URL 映射到本地文件;方法三:本地起 Mock Server,即mockjs,有点麻烦每次修改了后还要重启服务,nodemon能解决,但开的东西多了,电脑卡出翔,维护也麻烦;方法四:规范些的公司自己已经集成了一套mock数据体系;重点来了:easy-mock一个在线 Mock 平台,活儿好又性感是你备胎的最佳选择。当然优秀的你可能还有很多其他的方式,欢迎补充。//mock 基本使用示例import Mock from “mockjs”;Mock.mock({ “code”: 0, “message”: “请求成功”, “data|20”: [{ “name”: “@cname”,//cname 中文,name 英文 “userId”: “@id”, “lastDate”: “@datetime” }]})什么是easy-mock,能给我们带来什么?Easy Mock 是一个可视化,并且能快速生成 模拟数据 的持久化服务,Easy Mock 支持基于 Swagger 创建项目,以节省手动创建接口的时间;简单点说:Easy Mock就是一个在线创建mock的服务平台,帮你省去你 配置、安装、起服务、维护、多人协作Mock数据不互通等一系列繁琐的操作, 它能在不用1秒钟的时间内给你所要的一切,呼之即来、挥之即去的2018最优秀备胎没有之一,完全不用担心负任何责任哦。更多优点它在等你去发现哦……深入浅出 - 简介就跟人一样长的再好看,了解过后才懂,一样东西也是如何,谁用谁知道;Easy Mock支持团队协作,也可以是个人项目,以下以个人项目为例,与多人协作没有区别,只是少了成员邀请;深入浅出 - Mock语法回顾@ip -> 随机输出一个ip;@id -> 随机输出长度18的字符,不接受参数;“array|1-10” -> 随机输出1-10长度的数组,也可以直接是固定长度;“object|2” -> 输出一个两个key值的对象,"@image()" 返回一个占位图url,支持size, background, foreground, format, text;等等,这里就不再一一介绍。深入浅出 - 创建一个接口它的写法,跟Mock.js一模一样,上面代码已经展示过,更多示例使用Easy Mock创建一个接口,请看下图:深入浅出 - 高阶用法 Function在线编辑,它也能支持 function ,是不是很优秀,能获取到全部请求头,可以让我们像写在js里一样写逻辑,写运算,当然它肯定是还有很多局限性的,如并不支持ES6,有一点需要注意的是 function 里要写传出Mock对象,不能直接@…,来看示例:对象描述MockMock 对象_req.url获得请求 url 地址_req.method获取请求方法_req.params获取 url 参数对象_req.querystring获取查询参数字符串(url中?后面的部分),不包含 ?_req.query将查询参数字符串进行解析并以对象的形式返回,如果没有查询参数字字符串则返回一个空对象_req.body当 post 请求以 x-www-form-urlencoded 方式提交时,我们可以拿到请求的参数对象…_req.cookies、ip、host等等,我只是一个代码的搬运,更详细请看这里//简单模拟登录,根据用户传入的参数,返回不同逻辑数据{ defaultName:function({_req}){ return _req.query.name; }, code: function({_req}){ return this.defaultName ? 0 : -97; }, message: function({_req}) { return this.defaultName ? “登录成功” : “参数错误”; }, data: function({req,Mock}){ return this.defaultName ? { token: Mock.mock("@guid()"), userId: Mock.mock("@id(5)"), cname: Mock.mock("@cname()"), name: Mock.mock("@name()"), avatar: Mock.mock("@image(200x100, #FF6600)"), other:"@IT·平头哥联盟-首席填坑官∙苏南 带你再谈Mock数据之easy-mock" }:{} }}深入浅出 - 在线调试再优秀的工程师写出的代码也一样要测试,没有人敢说自己的代码无Bug,Easy Mock 它是真的懂你的,已经为你准备好了,接口编写好后,立马就能让你测试,是不是觉得很棒棒呢??如果是你自己本地写mock数据,你需要重启服务、手动写参数、可能还需要整个测试页,知道你已经非常饥渴迫切的想要知道,具体的调试方式了:看不清吗??已经为你备好在线调试链接,支持POST、GET、PUT等方式,因gif图加载比较大,就不一一演示了结尾: 天下无不散之宴席,又到说再见的时候了,以上就是今天苏南为大家带来的分享,您GET到了吗?Easy Mock更多强大之处自己去折腾吧,#用心分享 做有温度的攻城狮#,希望今天的分享能给您带来些许成长,如果觉得不错记得点个赞哦,,顺便关注下方公众号就更棒了呢,每周为您推最新分享????????。更多文章:immutability因React官方出镜之使用总结分享!小程序项目之做完项目老板给我加了6k薪资~小程序项目之填坑小记面试踩过的坑,都在这里了~你应该做的前端性能优化之总结大全!如何给localStorage设置一个过期时间?动画一点点 - 如何用CSS3画出懂你的3D魔方?动画一点点 - 手把手教你如何绘制一辆会跑车SVG Sprites Icon的使用技巧作者:苏南 - 首席填坑官链接:https://blog.csdn.net/weixin…交流群:912594095、公众号:honeyBadger8本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。 ...

November 7, 2018 · 1 min · jiezi

iOS导航栏样式方案梳理

1.背景在iOS开发中每个页面都有可能被个性化设计,但如果页面是以push方式进行管理,那么多个视图控制器共享一个导航栏,导航栏的适配显示就是一个问题。因此需基于系统导航进一步调整和修改才能满足需求。本文参考下面两篇博客进行分析梳理。Kenshin Cui’s Blog美团技术团队2.关注点页面样式自定义(包括隐藏或显示导航栏)之后,关注点如下:导航栏内容Title和Item容易编码维护。页面过渡导航栏内容渐变动画(参见系统导航效果)。页面过渡导航栏背景颜色变化不突兀。支持滑动手势pop。3.导航配置导航栏透明self.navigationBar.isTranslucent = true //需要开启半透明self.navigationBar.setBackgroundImage(UIImage(), for: .default)self.navigationBar.shadowImage = UIImage()导航栏隐藏// 导航栏显示(含animated,否则页面有无导航切换可能会突变,在手势pop时最明显) self.navigationController?.setNavigationBarHidden(true, animated: true)导航栏颜色导航栏半透明开启:既然开启半透明一般是想用模糊效果的,因此明显应使用下列第①种:// ① 半透明开启,此种方式设置颜色有明显模糊效果,展开图层树UINavigatuionBar -> background视图 -> UIVisualEffectView -> UIVisualEffectBackdropView, 发现进行UIVisualEffectBackdropView颜色变化(箭头代表内部子视图),但是因为UIVisualEffectView是模糊控制视图,因此会有模糊效果显现出来self.navigationController?.navigationBar.backgroundColor = UIColor.kcRed// ② 半透明开启,此种方式设置颜色没有模糊效果,展开图层树UINavigationBar ->background视图 -> imageView, 发现imageView颜色变化(箭头代表内部子视图)self.navigationController?.navigationBar.setBackgroundImage(UIImage(color:UIColor.kcRed), for: .default)// ③ 半透明开启,此种方式设置颜色有轻微模糊感,但不如第一种那样明显,展开图层树UINavigatuionBar -> background视图 -> UIVisualEffectView -> _UIVisualEffectSubview,发现_UIVisualEffectSubview颜色变化(箭头代表内部子视图),因为UIVisualEffectView视模糊控制视图,因此会有模糊效果显现出来self.navigationController?.navigationBar.barTintColor = UIColor.kcRed导航栏半透明关闭:建议采用第②种// ① 半透明关闭,此种方式不能设置导航栏背景颜色,展开图层树发现设置backgroundcolor仅仅影响UINavigationBar的颜色,但是UINavigationBar有一个background子视图(默认白色)遮盖了设置的颜色self.navigationController?.navigationBar.backgroundColor = UIColor.kcRed// ② 半透明关闭,此种方式可以设置导航栏颜色,展开图层树UINavigationBar ->background视图 -> imageView,发现imageView颜色变化(箭头代表内部子视图)self.navigationController?.navigationBar.setBackgroundImage(UIImage(color:UIColor.kcRed), for: .default)// ③ 半透明关闭,此种方式可以设置导航栏颜色。展开图层树发现是设置UINavigationBar的子视图background的颜色,但根据API语义(barTintColor)明显不是设置背景专属,可能会影响内部子视图颜色,因此一般不建议采用此种方法来设置背景色self.navigationController?.navigationBar.barTintColor = UIColor.kcRed隐藏导航栏下线 // 展开图层树发现黑线是一个高度为0.33的imageView(iphoneX显示),图层树UINavigationBar ->background视图 -> imageView,颜色为透明度0.3的黑色, self.navigationBar.shadowImage = UIImage()3.方案讨论方案一方案说明:用系统导航栏,且导航栏颜色控制仅仅在每个视图控制器viewWillAppear中进行配置,透明导航栏也可以使用颜色控制。当然也可根据需要部分页面隐藏导航栏。存在问题:此方案过于简单,页面过渡和手势滑动时导航栏颜色效果变化突兀。样例:参见KenshinCui博客的名为原始方式的方案(见其博客代码示例的Demo1)。关注点:不满足关注点3,页面过渡导航栏背景颜色变化突兀。方案二方案说明:隐藏系统导航栏,切换不同颜色的导航条则只需要隐藏用这个方法隐藏导航条然后自定义一个UINavigationBar增加到导航条的位置(添加一个假的导航条)。不过这种方式的由于隐藏了导航条,那么侧滑返回手势也会消失。透明导航条直接隐藏导航条。存在问题:①需要自己添加UINavigationBar。②由于隐藏了系统的导航栏,造成侧滑手势丢失。解决方式是重新设置当前控制器的interactivePopGestureRecognizer.delegate=self,但是多次push、pop会出现界面错乱操作失效的问题(解决方式就是在适当的时候禁用侧滑或者禁止手势shouldReceiveTouch)。样例:参见KenshinCui博客的方案1(见其博客代码示例的Demo2)。关注点:由于需要添UINavigationBar所以不满足关注点1;此方案导航栏内容和背景随视图渐进平移,背景不突兀,不满足关注点2,但满足关注点3;对于关注点4需要控制好手势的响应。此方案实现起来复杂,并且导航栏原生的特殊效果没有(自适应调整滚动视图、 iOS 11的大标题特效等),但此方案并没有突兀点,不影响需求的话可以采用。方案三方案说明:系统导航栏透明,自定义导航栏背景视图,将系统原有导航栏的背景设置为透明色,同时在每个 ViewController上添加一个View或者 NavigationBar来充当我们实际看到的导航栏,每个ViewController同样只需要关心自身的样式即可。当然也可根据需要部分页面隐藏导航栏。存在问题:基本上满足需求,但和系统原生比较起来,需要自己实现半透明效果,另外可在转场过程中通过self.transitionCoordinator?.animateAlongsideTransition设置navigationBar透明度。样例:参见KenshinCui博客的方案2(见其博客代码示例的Demo3)。关注点:基本满足所列4个关注点。方案四方案说明:隐藏导航栏,每个页面包含一个NavigationController ,每个页面有2个ViewController和一个NavigationController,一个ViewController交给所属导航管理页面跳转,且其子视图为NavigationController(寄宿到另一个ViewController)。我们具体细节内容布局在导航内层那个ViewController。存在问题:视图结构复杂,过渡时导航内容的没动画,手势处理需谨慎(面临两个导航)。样例:网传网易云音乐是这样的关注点:看起来和方案二相似,更好的满足关注点1。不满足关注点2。满足关注点3,如果手势处理好可满足关注点4。相对每个自身页面而言,导航栏的原生特殊效果可以通过内层NavigationController达到。方案五方案说明:使用系统导航栏,页面过渡添加Fake Bar在转场的过程中隐藏原有的导航栏并添加假的 NavigationBar,当转场结束后删除假的 NavigationBar 并恢复原有的导航栏,这一过程可以通过 Swizzle 的方式完成,而每个 ViewController 只需要关心自身的样式即可。当然也可根据需要部分页面隐藏导航栏。存在问题:但在解决 Bug 的时候,Swizzle 这种方式无疑会增加解决问题的时间成本和学习成本。样例:美团关注点:不满足关注点2,其它满足。4.推荐方案优先推荐方案3,简单易用;方案3为避免出乱子,需要良好的团队代码规范和完善的技术文档来做辅助。如果旧项目并且历史问题较多采用方案5。方案2和方案4满足需求的情况下也可选用,但这两个方案较复杂。 ...

October 30, 2018 · 1 min · jiezi

微信小程序中的ios兼容性问题

记录下在微信小程序中遇到的一些兼容性问题,ios兼容性1.ios中input的placeholder属性字体不居中对placeholder设置line-height及font-size对input设置高度2.ios中滚动卡顿设置-webkit-overflow-scrolling:touch;3.微信小程序中解决ios中new Date() 时间格式不兼容在实现倒计时,根据后台返回的时间格式转换时,后台返回了时间格式为”2018-11-12 11:12:11”,然后利用new Date() 转换时,ios中无法展示,安卓中显示正常let time = ‘2018-12-10 11:11:11’;let temporaryTime1 = new Date(time);this.setData({ timeRemain1: temporaryTime1,})/* 利用正则表达式替换时间中的”-”为”/”即可 */let time = ‘2018-12-10 11:11:11’;let temporaryTime = new Date(time.replace(/-/g,’/’));let temporaryTime1 = new Date(time);this.setData({ timeRemain: temporaryTime, timeRemain1: temporaryTime1, })4. 微信小程序scroll-view隐藏滚动条方法在wxss里加入以下代码:::-webkit-scrollbar{width: 0;height: 0;color: transparent;}暂时遇到的兼容性就是这么多,会持续更新,若大家有遇到,可在评论区告知下,感谢正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断ios和Android及PC端实现文字的省略号纯css实现瀑布流(multi-column多列及flex布局)

October 30, 2018 · 1 min · jiezi

基于weex的有赞无线开发框架

出于对开发效率和动态化的要求,无线端的开发框架也一直在更新,从 Hybrid、结构化 Native View、React Native、Weex,再到现在正在大受关注的 Flutter。什么样的框架才是适合自己的团队?不仅要有技术追求,而且要考虑实际业务需要。最近,有赞移动选择了 weex 作为无线开发框架,搭建了从开发、Debug、构建、发布、数据一个闭环的流程。本文将对此进行分享。一、什么是 weexWeex 是阿里巴巴开源的一套构建高性能、可扩展的原生应用跨平台开发方案。首先总结一下 weex 的特点:页面的开发目前支持Rax和VueWeex 也不是只支持 Vue 和 Rax,你也可以把自己喜欢的前端框架集成到 Weex 中,有一个文档扩展前端框架描述了如何实现,但是这个过程仍然非常复杂和棘手,你需要了解关于 js-native 之间通信和原生渲染引擎的许多底层细节。一次编写,三端(Android、iOS、前端)运行前提是都集成了 weex sdk,另外视觉表现做不到完全一样,有的会有一些差异,需要做一下适配。所以写 weex 页面的时候,如果支持三端,便需要在三端都进行自测。UI 的绘制通过 native 的组件,JavaScript 逻辑在 JS 引擎里运行,两者通过 JavaScriptCore 通信weex 里使用组件都需要在 native 端注册,这样 weex 里才可以使用,运行的时候通过注册时记录的 map 进行查找。weex sdk 内置注册了一些基础的组件,包括 list、text、input 等。WXJSCoreBridge 封装了 JavaScriptCore 实现 native 和 js 之间的通信。支持 Native 扩展可以将 native 的 UI 组件封装成 component,将 native 的逻辑代码封装成 module。从而在 weex 里可以进行使用。这里的 natiev UI 组件包括 modal、webview、image 等,这里的 native 逻辑代码包括 storage、network 等。每个 weex 页面会被打包成一个 js 文件,weex sdk 将 js 文件渲染成一个 viewweex 的打包通过 webpack,将每个页面打包成独立的一个 js 文件,weex sdk 会将 js 进行解析,将 UI 部分绘制成一个 view, 再绑定 view 的事件与 js 代码绑定。二、为什么要使用weex进行无线开发1. 效率问题1)开发的人力成本如果不算 web 端,一个页面本来需要 Android 和 iOS 2 个人开发;使用 weex 后只需要 1 个开发页面。2)开发的编译速度随着项目渐渐变得庞大,Android 项目一次编译需要 2-3 分钟,机器不好的还需要 10 分钟,iOS 可能会快一点,也需要 1-2 分钟。使用 weex 后,界面修改,只需要十几秒。3)测试效率提测之后,发现 bug,修复完成,测试总需要重新下载一个包进行安装;使用 weex 后,跟原生无关的 bug,只要测试重启 App 就可以进行验证。2. 动态化weex 页面最后打包完是一个 js 文件,只要能做到动态下发 JavaScript,那便可以实现动态化,可以热修复,甚至可以热部署,完全替换或者新增页面。3. 成熟度在 2016 年阿里双十一中,Weex 在阿里双十一会场中的覆盖率接近 99%,页面数量接近 2000,覆盖了包括主会场、分会场、分分会场、人群会场在内几乎所有的阿里双十一会场业务。阿里双十一主会场秒开率97%,全部会场页面达到 93%。2016 年 12 月 15 日,阿里巴巴宣布将移动开源项目 Weex 捐赠给 Apache 基金会开始孵化。2017 年,weex 在阿里业务里增长如下图,来自 WeexConf 2018。4. 接入成本经过实践,一个移动端开发,一周时间就可以开始进行使用 weex 进行业务开发。三、如何使用 weex 进行无线开发weex 其实是一套方案,各个流程很多东西需要自己建设,把它建设得让小伙伴可以以较小成本开始使用 weex,把它建设得融入已有的系统。这方面,我们目前做了下面这几个方面,还任重道远。1. 开发工具 zweex-toolkit这是一个脚手架工具,基于 weex 官方的 weex-toolkit,用于新建 weex 工程,目前只支持 vue。随着页面的增多,业务的复杂,工程会慢慢变得庞大,每次运行的时候如果全部页面都运行起来比较慢。为了解决这个问题,使用 zweex-toolkit 创建建的工程模板支持运行的时候,支持只运行指定目录下的页面,只要在 npm start 后加上参数即可,如:npm run start hi,helloworld这样就表示只运行 hi 目录下和 helloworld 下的页面。另外,我们支持:新增页面zweex page开启调试zweex debug2. ZanWeex SDK 的实现官方 weex sdk 做的事情,就是输入一个 js 文件,然后返回一个view。考虑到每个应用的路由和个性化的需要,这一点,ZanWeex SDK 没有做其他工作,也还是返回了一个view,业务方可以根据自己的需要将view添加到自己想要展示的地方。ZanWeex SDK 做的事情主要有如下几方面:1)支持下发配置,支持动态化,可以完成整个页面的替换weex 页面打包后的结果是一个 js 文件,所以可以进行下发进行动态更新,那么就需要有一份配置,来关联页面路由和 js 文件的关系,于是我们设计了这样的数据结构:h5:页面路由地址,可以直接使用发布平台生成的 h5 地址js:打包后的 js 文件地址version:支持的最低 App 版本,因为新页面如果需要 native 扩展,那就需要发布新版本进行支持md5:为了校验完整性,我们在配置里添加每个 js 文件的 md5。2)支持多模块独立配置,互不影响 一个App里会有多个模块,每个模块可能由独立的团队进行负责,所以为了减少耦合,我们将配置独立,每个模块可以独立管理自己的配置,独立接入weex,不依赖于宿主App。3)预加载页面模板,支持页面模板缓存和配置缓存如果没有缓存,每次都从服务端拉取页面模板,那么是不可能达到秒开的,跟没有做缓存的H5页面就区别不大了。我们SDK会预加载页面模板到本地,打开过的页面会缓存到内存。这样渲染的时间就更接近原生的渲染时间了。4)支持开发时的hot reloading,前端开发般的体验如果没有hot reloading,那么每次修改完页面,都得退出页面重新进入。为了省去这个操作,hot reloading是必须的。weex 工程里本地开发时候,通过webpack-dev-server来启动一个websocket,zan weex sdk 打开一个weex页面后,去与它建立连接。webpack-dev-server将工程的编译状态发送给ZanWeex SDK,当接收到渲染完成的指令时,就重新渲染页面,从而达到 hot reloading的目的。5)支持页面的适配,提供环境变量ZanWeex SDK 会提供以下四个变量共 weex 页面使用,方便完成页面配置。容器的高度:weex.config.yzenv.viewHeight容器的宽度:weex.config.yzenv.viewWidth状态栏高度:weex.config.yzenv.statusBarHeight底部栏高度(针对iPhone X,其他为0):weex.config.yzenv.bottomHeight6)开发阶段日志的查看在开发阶段,weex sdk 源码里输出的日志以及 js 里通过 console.log 输出的日志,还有 js 运行的报错,都只能通过 XCode 和 Android Studio 进行查看。这对于一个只了解一端的开发人员是非常不方便的。于是我们做了一个入口,在打开 weex 页面的时候,会显示该入口,点击即可查看所输出的日志。7)参数传递正向传参:从 A 页面跳转到 B 页面,参数传递是开发过程肯定会遇见的一个场景。SDK 对外提供的渲染接口 renderByH5 的参数包括 url,params,data。业务方进行渲染的时候,可以将参数直接跟在 url 后面,或者通过 params、data 传入,不同方式,取的方式也不一样:url 后面的参数,会传入 data,weex 页面里直接在 data 里定义参数就会自动赋值;params的参数,在 weex 页面里可以通过 weex.config.name 来获取;data 传入的参数,获取方式同第一种。反向传参:从 B 页面返回到 A 页面的时候,携带参数返回也是很常见的一个场景。SDK 提供了统一的存储类 ZParamStorage 来临时存储参数。页面 B 要返回的时候先把数据存入存储区,A 页面显示的时候再从存储区获取,然后清空存储区。非跳转的参数传递:weex 页面之间,可以采用 BroadcastChannel 进行传参,weex 与 native 之间的传递可以通过自己封装 Module 进行实现。3. 页面的开发前面有提到,weex 的页面目前可以采用 vue 或者 Rax 编写。对于 Vue 和 Rax 的语法这里不做陈述。这里主要总结了容易在实际开发中卡住小伙伴的几个问题。1)如何判断一个页面是否用 weex 来实现?可以认为所有的新页面都可以采取 weex 来开发,区别在于这个页面使用的 native 能力有多少。可以通过自定义 Module 来调用 native 的能力,通过自定义 component 来使用 native 的组件;2)什么时候需要自定义 Module?需要原生的能力的时候,比如:要调用系统选择图片的接口调用打电话、发短信的功能打开其他应用调用已有的业务逻辑,比如:加密、解密逻辑登录逻辑3)什么时候需要自定义 component?如果一个组件已经使用 native 实现,为了保持统一一致,那么可以将原有的组件封装成 component如果一个组件不能使用 weex 实现,比如地图组件、超长图显示等4)多个弹层的布局如何实现?weex 页面渲染的层级,是从上而下的,越在下面的布局,显示越上层。所以要作为弹层的布局,就把它放到最下面。5)页面的动画如何实现?官方 weex sdk 已经封装了 animation 的 module 可以直接使用,复杂的动画可以使用 BindingX 实现。6)weex 的代码如何复用?代码都可以抽离出组件。作为一个 UI 组件,抽离成一个组件,向外暴露属性参数和事件接口;作为独立的 js 函数,抽离成一个 js 供其他页面引入;css 样式也可以抽离成一个 css 文件,供其他页面引入;如果包含多个组件形式,可以通过 mixins 来引入。4. 构建和打包平台我们开发了以项目为单位的构建平台:每个项目可以添加多个分支,可以是不同仓库的分支。因为一个项目有可能是跨团队跨模块的,但是需要一起发布。构建通过 webpack 构建,构建之后,支持发布线下存储和线上 cdn我们还开发了以应用为单位的 weex 发布平台:这里的应用是一个抽象概念,不是传统的“应用”,可以理解成模块业务方可以在构建平台构建完成后,一键跳转到发布平台进行发布,除了需要第一次填写最低支持的版本号,其他均无需操作。发布平台支持灰度发布、全量发布和回滚。发布平台会展示 weex 在端上的使用情况,渲染时间、渲染错误、下载时间等四、遇到的问题以及解决方案在开发过程中,很多问题,可以通过阅读源码来解决,比如:使用 iconfont 的时候,是否已支持缓存?答:已支持,包括内存缓存和文件缓存,内存缓存使用 familyname 来做 key,文件缓存使用 md5(url) 来做本地文件名module实现的函数能不能返回参数?答:module 的函数氛围 UIThread 和 JSThread,JSThread 对于 js 线程来说是同步的,支持直接返回参数;UIThread 对于 JS 线程来说是异步的,不支持直接返回参数,只能使用 callback另外,很多常见的问题,我们已经在 ZanWeexSDK 进行了解决,包括实现动态化、多模块的支持、缓存管理、Hot Reloading、日志查看、页面适配、参数传递等。此外,还会有一些常见的问题,在此罗列一下:配置的更新机制是怎样的?更新失败,如何打开 weex 页面?答: 配置的更新接口开放给业务方调用,由业务方决定什么时候调用更新接口;SDK 里做了三种处理,来尽量保证配置可以更新成功:1)配置接口拉取失败后,会有三次重试;2)网络从无网变成有网时,sdk 会检查配置是否已拉取,如果未拉取就主动拉取3)允许业务方内置配置和 js 文件,当拉取失败后,SDK里会从内置配置里读取配置的版本管理是怎样的?答:配置每次发布的时候,都会指定该发布支持的 App 最低版本号。每次请求,会携带 App 版本号,服务端只会返回符合该版本号的最新配置。支持不支持屏幕旋转?答:答案是支持的。旋转之后,屏幕变成了横屏,weex 就按照横屏的尺寸来渲染,问题是只要你写的页面符合这种变化就可以了,跟 native 来实现页面没有什么区别。五、未来还要继续做的事情组件库的建设性能统计,比如帧率、内存、CPU配置和js文件的增量更新、推送更新降级处理 ...

October 30, 2018 · 2 min · jiezi

iOS系统中导航栏的转场解决方案与最佳实践

背景目前,开源社区和业界内已经存在一些 iOS 导航栏转场的解决方案,但对于历史包袱沉重的美团 App 而言,这些解决方案并不完美。有的方案不能满足复杂的页面跳转场景,有的方案迁移成本较大,为此我们提出了一套解决方案并开发了相应的转场库,目前该转场库已经成为美团点评多个 App 的基础组件之一。在美团 App 开发的早期,涉及到导航栏样式改变的需求时,经常会遇到转场效果不佳或者与预期样式不符的“小问题”。在业务体量较小的情况下,为了满足快速的业务迭代,通常会使用硬编码的方式来解决这一类“小问题”。但随着美团 App 业务的高速发展,这种硬编码的方式遇到了以下的挑战:业务模块的不断增加,导致使用硬编码方式编写的代码维护成本增加,代码质量迅速下降。大型 App 的路由系统使得页面间的跳转变得更加自由和灵活,也使得导航栏相关的问题激增,不但增加了问题的排查难度,还降低了整体的开发效率。App 中的导航栏属于各个业务方的公用资源,由于缺乏相应的约束机制和最佳实践,导致业务方之间的代码耦合程度不断增加。从各个角度来看,硬编码的方式已经不能很好的解决此类问题,美团 App 需要一个更加合理、更加持久、更加简单易行的解决方案来处理导航栏转场问题。本文将从导航栏的概念入手,通过讲解转场过程中的状态管理、转换时机和样式变化等内容,引出了在大型应用中导航栏转场的三种常见解决方案,并对美团点评的解决方案进行剖析。重新认识导航栏导航栏里的 MVC在 iOS 系统中, 苹果公司不仅建议开发者遵循 MVC 开发框架,在它们的代码里也可以看到 MVC 的影子,导航栏组件的构成就是一个类似 MVC 的结构,让我们先看看下面这张图:在这张图里,我们可以将 UINavigationController 看做是 C,UINavigationBar 看做是 V,而 UIViewController 和 UINavigationItem 组成的 Stack 可以看做是 M。这里要说明的是,每个 UIViewController 都有一个属于自己的 UINavigationItem,也就是说它们是一一对应的。UINavigationController 通过驱动 Stack 中的 UIViewController 的变化来实现 View 层级的变化,也就是 UINavigationBar 的改变。而 UINavigationBar 样式的数据就存储在 UIViewController 的 UINavigationItem 中。这也就是为什么我们在代码里只要设置 self.navigationItem 的相关属性就可以改变 UINavigationBar 的样式。很多时候,国内的开发者会将 UINavigationBar 和 UINavigationController 混在一起叫导航栏,这样的做法不仅增加了开发者之间的沟通成本,也容易导致误解。毕竟它们是两个完全不一样的东西。所以本文为了更好的阐明问题,会采用英文区分不同的概念,当需要描述笼统的导航栏概念时,会使用导航栏组件一词。通过这一节的回顾,我们应该明确了 NavigationItem、ViewController、NavigationBar 和 NavigationController 在 MVC 框架下的角色。下面我们会重新梳理一下导航栏的生命周期和各个相关方法的调用顺序。导航栏组件的生命周期大家可以通过下图获得更为直观的感受,进而了解到导航栏组件在 push 过程中各个方法的调用顺序。值得注意的地方有两点:第一个是 UINavigationController 作为 UINavigationBar 的代理,在没有特殊需求的情况下,不应该修改其代理方法,这里是通过符号断点获取它们的调用顺序。如果我们创建了一个自定义的导航栏组件系统,它的调用顺序可能会与此不同。第二个是用虚线圈起来的方法,它们也有可能不被调用,这与 ViewController 里的布局代码相关,假设跳转到新页面后,新旧页面中的控件位置会发生变化,或者由于数据改变驱动了控件之间的约束关系发生变化,这就会带来新一轮的布局,进而触发 viewWillLayoutSubview 和 viewDidLayoutSubview 这两个方法。当然,具体的调用顺序会与业务代码紧密相关,如果我们发现顺序有所不同,也不必惊慌。下面这张图展示了导航栏在 pop 过程中各个方法的调用顺序:除了上面说到的两点,pop 过程中还需要注意一点,那就是从 B 返回到 A 的过程中,A 视图控制器的 viewDidLoad 方法并不会被调用。关于这个问题,只要提醒一下,大多数人都会反应过来是为什么。不过在实际开发过程中,总会有人忘记这一点。通过这两个图,我们已经基本了解了导航栏组件的生命周期和相关方法的调用顺序,这也是后面章节的理论基础。导航栏组件的改变与革新导航栏组件在 iOS 11 发布时,获得了重大更新,这个更新可不是增加了一个大标题样式(Large Title Display Mode)那么简单,需要注意的地方大概有两点:导航栏全面支持 Auto Layout 且 NavigationBar 的层级发生了明显的改变,关于这一点可以阅读 UIBarButtonItem 在 iOS 11 上的改变及应对方案 。由于引进了 Safe Area 等概念,topLayoutGuide 和 bottomLayoutGuide 等属性会逐渐废弃,虽然变化不大,但如果我们的导航栏在转场过程中总是出现视图上下移动的现象,不妨从这个方面思考一下,如果想深究可以查看 WWDC 2017 Session 412。导航栏组件到底怎么了?经常有人说 iOS 的原生导航栏组件不好使用,抱怨主要集中在导航栏组件的状态管理和控件的布局问题上。控件的布局问题随着 iOS 11 的到来已经变得相对容易处理了不少,但导航栏组件的状态管理仍然让开发者头疼不已。可能已经有朋友在思考导航栏组件的状态管理到底是什么东西?不要着急,下面的章节就会做相关的介绍。导航栏的状态管理虽然导航栏组件的 push 和 pop 动画给人一种每次操作后都会创建一遍导航栏组件的错觉,但实际上这些 ViewController 都是由一个 NavigationController 所管理,所以你看到的 NavigationBar 是唯一的。在 NavigationController 的 Stack 存储结构下,每当 Stack 中的 ViewController 修改了导航栏,势必会影响其他 ViewController 展示的效果。例如下图所示的场景,如果 NavigationBar 原先的颜色是绿色,但之后进入 Stack 里的 ViewController 将 NavigationBar 颜色修改为紫色后,在此之后 push 的 ViewController 会从默认的绿色变为紫色,直到有新的 ViewController 修改导航栏颜色才会发生变化。虽然在 push 过程中,NavigationBar 的变化听起来合情合理,但如果你在 NavigationBar 为绿色的 ViewController 里设置不当的话,那么当你 pop 回这个 ViewController 时,NavigationBar 可就不一定是绿色了,它还会保持为紫色的状态。通过这个例子,我们大概会意识到在导航栏里的 Stack 中,每个 ViewController 都可以永久的影响导航栏样式,这种全局性的变化要求我们在实际开发中必须坚持“谁修改,谁复原”的原则,否则就会造成导航栏状态的混乱。这不仅仅是样式上的混乱,在一些极端状况下,还有可能会引起 Stack 混乱,进而造成 Crash 的情况。导航栏样式转换的时机我们刚才提到了“谁修改,谁复原”的原则,但何时修改,何时复原呢?对于那些存储在 Stack 中的 ViewController 而言,它其实就是在不断的经历 appear 和 disappear 的过程,结合 ViewController 的生命周期来看,viewWillAppear: 和 viewWillDisappear: 是两个完美的时间节点,但很多人却对这两个方法的调用存在疑惑。苹果公司在它的 API 文档中专门用了一段文字来解答大家的疑惑,这段文字的标题为《Handling View-Related Notifications》,在这里我们直接引用原文:When the visibility of its views changes, a view controller automatically calls its own methods so that subclasses can respond to the change. Use a method like viewWillAppear: to prepare your views to appear onscreen, and use the viewWillDisappear: to save changes or other state information. Use other methods to make appropriate changes.Figure 1 shows the possible visible states for a view controller’s views and the state transitions that can occur. Not all ‘will’ callback methods are paired with only a ‘did’ callback method. You need to ensure that if you start a process in a ‘will’ callback method, you end the process in both the corresponding ‘did’ and the opposite ‘will’ callback method.这里很好的解释了所有的 will 系列方法和 did 系列方法的对应关系,同时也给我们吃了一个定心丸,那就是在 appearing 和 disappearing 状态之间会由 will 系列方法进行衔接,避免了状态中断。这对于连续 push 或者连续 pop 的情况是及其重要的,否则我们无法做到 “谁修改,谁复原”的原则。通常来说,如果只是一个简单的导航栏样式变化,我们的代码结构大体会如下所示:- (void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; // MARK: change the navigationbar style }- (void)viewWillDisappear:(BOOL)animated{ [super viewWillDisappear:animated]; // MARK: restore the navigationbar style}现在,我们明确了修改时机,接下来要明确的就是导航栏的样式会进行怎样的变化。导航栏的样式变化对于不同 ViewController 之间的导航栏样式变化,大多可以总结为两种情况:导航栏的显示与否导航栏的颜色变化导航栏的显示与否对于显示与否的问题,可以在上一节提到的两个方法里调用 setNavigationBarHidden:animated: 方法,这里需要提醒的有两点:在导航栏转场的过程中,不要天真的以为 setNavigationBarHidden: 和 setNavigationBarHidden:animated: 的效果是一样的,直接使用 setNavigationBarHidden: 会造成导航栏转场过程中的闪现、背景错乱等问题,这一现象在使用手势驱动转场的场景中十分常见,所以正确的方式是使用带有 animated 参数的 API。在 push 和 pop 的方法里也会带有 animated 参数,尽量保证与 setNavigationBarHidden:animated: 中的 animated 参数一致。导航栏的颜色变化颜色变化的问题就稍微复杂一些,在 iOS 7 后,导航栏增加了 translucent 效果,这使得导航栏背景色的变化出现了两种情况:translucent 属性值为 YES 的前提下,更改导航栏的背景色。translucent 属性值为 NO 的前提下,更改导航栏的背景色。对于第一种情况,我们需要调用 UINavigationBar 的 setBackgroundColor: 方法。对于第二种情况我们需要调用 UINavigationBar 的 setBackgroundImage:forBarMetrics: 方法。对于第二种情况,这里有三点需要提示:在设置透明效果时,我们通常可以直接设置一个 [UIImage new] 创建的对象,无须创建一个颜色为透明色的图片。在使用 setBackgroundImage:forBarMetrics: 方法的过程中,如果图像里存在 alpha 值小于 1.0 的像素点,则 translucent 的值为 YES,反之为 NO。也就是说,如果我们真的想让导航栏变成纯色且没有 translucent 效果,请保证所有像素点的 alpha 值等于 1。如果设置了一个完全不透明的图片且强行将 NavigationBar 的 translucent 属性设置为 YES 的话,系统会自动修正这个图片并为它添加一个透明度,用于模拟 translucent 效果。如果我们使用了一个带有透明效果的图片且导航栏的 translucent 效果为 NO 的话,那么系统会在这个带有透明效果的图片背后,添加一个不透明的纯色图片用于整体效果的合成。这个纯色图片的颜色取决于 barStyle 属性,当属性为 UIBarStyleBlack 时为黑色,当属性为 UIBarStyleDefault 时为白色,如果我们设置了 barTintColor,则以设置的颜色为基准。分清楚 transparent,translucent,opaque,alpha 和 opacity 也挺重要在刚接触导航栏 API 时,许多人经常会把文档里的这些英文词搞混,也不太明白带有这些词的变量为什么有的是布尔型,有的是浮点型,总之一切都让人很困惑。在这里将做了一个总结,这对于理解 Apple 的 API 设计原则十分有帮助。transparent, translucent, opaque 三个词经常会用在一起,它用于描述物体的透光强度,为了让大家更好的理解这三个词,这里做了三个比喻:transparent 是指透明,就好比我们可以透过一面干净的玻璃清楚的看到外面的风景。translucent 是指半透明,就好比我们可以透过一面有点磨砂效果的塑料墙看外面的风景,不能说看不见,但我们肯定看不清。opaque 是指不透明,就好比我们透过一个堵石墙是看不见任何外面的东西,眼前看到的只有这面墙。这三个词更多的是用来表述一种状态,不需要量化,所以这与这三个词相关的属性,一般都是 BOOL 类型。alpha 和 opacity 经常会在一起使用,它要表示的就是透明度,在 Web 端这两个属性有着明显的区别。在 Web 端里,opacity 是设定整个元素的透明值,而 alpha 一般是放在颜色设置里面,所以我们可以做到对特定对元素的某个属性设定 alpha,比如背景、边框、文字等。div { width: 100px; height: 100px; background: rgba(0,0,0,0.5); border: 1px solid #000000; opacity: 0.5;}这一概念同样适用于 iOS 里的概念,比如我们可以通过 alpha 通道单独的去设置 backgroudColor、borderColor,它们互不影响,且有着独立的 alpha 通道,我们也可以通过 opacity 统一设置整个 view 的透明度。但与 Web 端不一致的是,iOS 里面的 view 不光拥有独立的 alpha 属性,同时也是基于 CALayer,所以我们可以看到任意 UIView 对象下面都会有一个 layer 的属性,用于表明 CALayer 对象。view 的 alpha 属性与 layer 里面的 opacity 属性是一个相等的关系,需要注意的是 view 上的 alpha 属性是 Web 端并不具备的一个能力,所以笔者认为:在 iOS 中去说 alpha 时,要区分是在说 view 上的属性,还是在说颜色通道里的 alpha。由于这两个词都是在描述程度,所以我们看到它们都是 CGFloat 类型:转场过程中需要注意的问题和细节说完了导航栏的转场时机和转场方式,其实大体上你已经能处理好不同样式间的转换,但还有一些细节需要你去考虑,下面我们来说说其中需要你关注的两点。translucent 属性带来的布局改变translucent 会影响导航栏组件里 ViewController 的 View 布局,这里需要大家理清 5 个 API 的使用场景:edgesForExtendedLayoutextendedLayoutIncluedsOpaqueBarsautomaticallyAdjustScrollViewInsetscontentInsetAdjustmentBehavioradditionalSafeAreaInsets前三个 API 是 iOS 11 之前的 API,它们之间的区别和联系在 Stack Overflow 上有一个比较精彩的回答 - Explaining difference between automaticallyAdjustsScrollViewInsets, extendedLayoutIncludesOpaqueBars, edgesForExtendedLayout in iOS7,我在这里就不做详细阐述,总结一下它的观点就是:如果我们先定义一个 UINavigationController,它里面包含了多个 UIViewController,每个 UIViewController 里面包含一个 UIView 对象:那么 edgesForExtendedLayout 是为了解决 UIViewController 与 UINavigationController 的对齐问题,它会影响 UIViewController 的实际大小,例如 edgesForExtendedLayout 的值为 UIRectEdgeAll 时,UIViewController 会占据整个屏幕的大小。当 UIView 是一个 UIScrollView 类或者子类时,automaticallyAdjustsScrollViewInsets 是为了调整这个 UIScrollView 与 UINavigationController 的对齐问题,这个属性并不会调整 UIViewController 的大小。对于 UIView 是一个 UIScrollView 类或者子类且导航栏的背景色是不透明的状态时,我们会发现使用 edgesForExtendedLayout 来调整 UIViewController 的大小是无效的,这时候你必须使用 extendedLayoutIncludesOpaqueBars 来调整 UIViewController 的大小,可以认为 extendedLayoutIncludesOpaqueBars 是基于 automaticallyAdjustsScrollViewInsets 诞生的,这也是为什么经常会看到这两个 API 会同时使用。这些调整布局的 API 背后是一套基于 topLayoutGuide 和 bottomLayoutGuide 的计算而已,在 iOS 11 后,Apple 提出了 Safe Area 的概念,将原先分裂开来的 topLayoutGuide 和 bottomLayoutGuide 整合到一个统一的 LayoutGuide 中,也就是所谓的 Safe Area,这个改变看起来似乎不是很大,但它的出现确实方便了开发者。如果想对 Safe Area 带来的改变有更全面的认识,十分推荐阅读 Rosberry 的工程师 Evgeny Mikhaylov 在 Medium 上的文章 iOS Safe Area,这篇文章基本涵盖了 iOS 11 中所有与 Safe Area 相关的 API 并给出了真正合理的解释。这里只说一下 contentInsetAdjustmentBehavior 和 additionalSafeAreaInsets 两个 API。对于 contentInsetAdjustmentBehavior 属性而言,它的诞生也意味着 automaticallyAdjustsScrollViewInsets 属性的失效,所以我们在那些已经适配了 iOS 11 的工程里能看到如下类似的代码:if (@available(iOS 11.0, *)) { self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;} else { self.automaticallyAdjustsScrollViewInsets = NO;}此处的代码片段只是一个示例,并不适用所有的业务场景,这里需要着重说明几个问题:关于 contentInsetAdjustmentBehavior 中的 UIScrollViewContentInsetAdjustmentAutomatic 的说明一直很“模糊”,通过 Evgeny Mikhaylov 的文章,我们可以了解到他在大多数情况下会与 UIScrollViewContentInsetAdjustmentScrollableAxes 一致,当且仅当满足以下所有条件时才会与 UIScrollViewContentInsetAdjustmentAlways 相似:UIScrollView 类型的视图在水平轴方向是可滚动的,垂直轴是不可滚动的。ViewController 视图里的第一个子控件是 UIScrollView 类型的视图。ViewController 是 navigation 或者 tab 类型控制器的子视图控制器。启用 automaticallyAdjustsScrollViewInsets。iOS 11 后,通过 contentInset 属性获取的偏移量与 iOS 10 之前的表现形式并不一致,需要获取 adjustedContentInset 属性才能保证与之前的 contentInset 属性一致,这样的改变需要我们在代码里对不同的版本进行适配。对于 additionalSafeAreaInsets 而言,如果系统提供的这几种行为并不能满足我们的布局要求,开发者还可以考虑使用 additionalSafeAreaInsets 属性做调整,这样的设定使得开发者可以更加灵活,更加自由的调整视图的布局。backIndicator 上的动画苹果提供了许多修改导航栏组件样式的 API,有关于布局的,有关于样式的,也有关于动画的。backIndicatorImage 和 backIndicatorTransitionMaskImage 就是其中的两个 API。backIndicatorImage 和 backIndicatorTransitionMaskImage 操作的是 NavigationBar 里返回按钮的图片,也就是下图红色圆圈所标注的区域。想要成功的自定义返回按钮的图标样式,我们需要同时设置这两个 API ,从字面上来看,它们一个是返回图片本身,另一个是返回图片在转场时用到的 mask 图片,看起来不怎么难,我们写一段代码试试效果:self.navigationController.navigationBar.backIndicatorImage = [UIImage imageNamed:@“backArrow”];self.navigationController.navigationBar.backIndicatorTransitionMaskImage = [UIImage imageNamed:@“backArrowMask”];代码里的图片如下所示:也许大多数人在这里会都认为,mask 图片会遮挡住文字使其在遇到返回按钮右边缘的时候就消失。但实际的运行效果是怎么样子的呢?我们来看一下:在上面的图片中,我们可以看到返回按钮的文字从返回按钮的图片下面穿过并且文字被图片所遮挡,这种动画看起来十分奇怪,这是无法接受的。我们需要做点修改:self.navigationController.navigationBar.backIndicatorImage = [UIImage imageNamed:@“backArrow”];self.navigationController.navigationBar.backIndicatorTransitionMaskImage = [UIImage imageNamed:@“backArrow”];这一次我们将 backIndicatorTransitionMaskImage 改为 indicatorImage 所用的图片。到这里,可能大多数人都会好奇,这代码也能行?让我们看下它实际的效果:在上面的图中,我们看到文字在到达图片的右边缘时就从下方穿过并被完全遮盖住了,这种动画效果虽然比上面好一些,但仍然有改进的空间,不过这里我们先不继续优化了,我们先来讨论一下它们背后的运作原理。iOS 系统会将 indicatorImage 中不透明的颜色绘制成返回按钮的图标, indicatorTransitionMaskImage 与 indicatorImage 的作用不同。indicatorTransitionMaskImage 将自身不透明的区域像 mask 一样作用在 indicatorImage 上,这样就保证了返回按钮中的文字像左移动时,文字只出现在被 mask 的区域,也就是 indicatorTransitionMaskImage 中不透明的区域。掌握了原理,我们来解释下刚才的两种现象:在第一种实现中,我们提供的 indicatorTransitionMaskImage 覆盖了整个返回按钮的图标,所以我们在转场过程中可以清晰的看到返回按钮的文字。在第二种实现中,我们使用 indicatorImage 作为 indicatorTransitionMaskImage,记住文字是只能出现在 indicatorTransitionMaskImage 里不透明的区域,所以显然返回按钮中的文字会在图标的最右边就已经被遮挡住了,因为那片区域是透明的。那么前面提到的进一步优化指的是什么呢?让我们来看一下下面这个示例图,为了更好的区分,我们将 indicatorTransitionMaskImage 用红色进行标注。黑色仍然是 indicatorImage。按照刚才介绍的原理,我们应该可以理解,现在文字只会出现在红色区域,那么它的实际效果是什么样子的呢,我们可以看下图:现在,一个完美的返回动画,诞生啦!此节所用的部分效果图出自 Ray Wenderlich 的文章 UIAppearance Tutorial: Getting Started导航栏的跳转或许可以这么玩儿…前两章的铺垫就是为了这一章的内容,所以现在让我们开始今天的大餐吧。这样真的好么?刚才我们说了两个页面间 NavigationBar 的样式变化需要在各自的 viewWillAppear: 和 viewWillDisappear: 中进行设置。那么问题就来了:这样的设置会带来什么问题呢?试想一下,当我们的页面会跳到不同的地方时,我们是不是要在 viewWillAppear: 和 viewWillDisappear: 方法里面写上一堆的判断呢?如果应用里还有 router 系统的话,那么页面间的跳转将变得更加不可预知,这时候又该如何在 viewWillAppear: 和 viewWillDisappear: 里做判断呢?现在我们的问题就来了,如何让导航栏的转场更加灵活且相互独立呢?常见的解决方案如下所示:重新实现一个类似 UINavigationController 的容器类视图管理器,这个容器类视图管理器做好不同 ViewController 间的导航栏样式转换工作,而每个 ViewController 只需要关心自身的样式即可。将系统原有导航栏的背景设置为透明色,同时在每个 ViewController 上添加一个 View 或者 NavigationBar 来充当我们实际看到的导航栏,每个 ViewController 同样只需要关心自身的样式即可。在转场的过程中隐藏原有的导航栏并添加假的 NavigationBar,当转场结束后删除假的 NavigationBar 并恢复原有的导航栏,这一过程可以通过 Swizzle 的方式完成,而每个 ViewController 只需要关心自身的样式即可。这三种方案各有优劣,我们在网上也可以看到很多关于它们的讨论。例如方案一,虽然看起来工作量大且难度高,但是这个工作一旦完成,我们就会将处理导航栏转场的主动权牢牢抓在手里。但这个方案的一个弊端就是,如果苹果修改了导航栏的整体风格,就好比 iOS 11 的大标题特效,那么工作量就来了。对于方案二而言,虽然看起来简单易用,但这需要一个良好的继承关系,如果整个工程里的继承关系混乱或者是历史包袱比较重,后续的维护就像“打补丁”一样,另外这个方案也需要良好的团队代码规范和完善的技术文档来做辅助。对于方案三而言,它不需要所谓的继承关系,使用起来也相对简单,这对于那些继承关系和历史包袱比较重的工程而言,这一个不错的解决方案,但在解决 Bug 的时候,Swizzle 这种方式无疑会增加解决问题的时间成本和学习成本。我们的解决方案在美团 App 的早期,各个业务方都想充分利用导航栏的能力,但对于导航栏的状态维护缺乏理解与关注,随着业务方的增加和代码量的上升,与导航栏相关的问题逐渐暴露出来,此时我们才意识到这个问题的严重性。大型 App 的导航栏问题就像一个典型的“公地悲剧”问题。在软件行业,公用代码的所有权可以被视作“公地”,因为不注重长期需求而容易遭到消耗。如果开发人员倾向于交付“价值”,而以可维护性和可理解性为代价,那么这个问题就特别普遍了。如果是这种情况,每次代码修改将大大减少其总体质量,最终导致软件的不可维护。所以解决这个问题的核心在于:明确公用代码的所有权,并在开发期施加约束。明确公用代码的所有权,可以理解为将导航栏相关的组件抽离成一个单独的组件,并交由特定的团队维护。而在开发期施加约束,则意味着我们要提供一套完整的解决方案让各个业务方遵守。这一节我们会以美团内部的解决方案为例,讲解如何实现一个流畅的导航栏跳转过程和相关使用方法。设计理念使用者只用关心当前 ViewController 的 NavigationBar 样式,而不用在 push 或者 pop 的时候去处理 NavigationBar 样式。举个例子来说,当从 A 页面 push 到 B 页面的时候,转场库会保存 A 页面的导航栏样式,当 pop 回去后就会还原成以前的样式,因此我们不用考虑 pop 后导航栏样式会改变的情况,同时我们也不必考虑 push 后的情况,因为这个是页面 B 本身需要考虑的。使用方法转场库的使用十分简单,我们不需要 import 任何头文件,因为它在底层通过 Method Swizzling 进行了处理,只需要在使用的时候遵循下面 4 点即可:当需要改变导航栏样式的时候,在视图控制器的 viewDidLoad 或者 viewWillAppear: 方法里去设置导航栏样式。用 setBackgroundImage:forBarMetrics: 方法和 shadowImage 属性去修改导航栏的背景样式。不要在 viewWillDisappear: 里添加针对导航栏样式修改的代码。不要随意修改 translucent 属性,包括隐式的修改和显示的修改。隐式修改是指使用 setBackgroundImage:forBarMetrics: 方法时,如果 image 里的像素点没有 alpha 通道或者 alpha 全部等于 1 会使得 translucent 变为 NO 或者 nil。基本原理以上,我们讲完了设计理念和使用方法,那么我们来看看美团的转场库到底做了什么?从大方向上来看,美团使用的是前面所说的第三种方案,不过它也有一些自己独特的地方,为了更好的让大家理解整个过程,我们设计这样一个场景,从页面 A push 到页面 B,结合之前探讨过的方法调用顺序,我们可以知道几个核心方法的调用顺序大致如下:页面 A 的 pushViewController:animated:页面 B 的 viewDidLoad or viewWillAppear:页面 B 的 viewWillLayoutSubviews页面 B 的 viewDidAppear:在 push 过程的开始,转场库会在页面 A 自身的 view 上添加一个与导航栏一模一样的 NavigationBar 并将真的导航栏隐藏。之后这个假的导航栏会一直存在页面 A 上,用于保留 A 离开时的导航栏样式。等到页面 B 调用 viewDidLoad 或者 viewWillAppear: 的时候,开发者在这里自行设置真的导航栏样式。转场库在这里会对页面布局做一些修正和辅助操作,但不会影响导航栏的样式。等到页面 B 调用 viewWillLayoutSubviews 的时候,转场库会在页面 B 自身的 view 上添加一个与真的导航栏一模一样的 NavigationBar,同时将真的导航栏隐藏。此时不论真的导航栏,还是假的导航栏都已经与 viewDidLoad 或者 viewWillAppear: 里设置的一样的。当然,这一步也可以放在 viewWillAppear: 里并在 dispatch main queue 的下一个 runloop 中处理。等到页面 B 调用 viewDidAppear: 的时候,转场库会将假的导航栏样式设置到真的导航栏中,并将假的导航栏从视图层级中移除,最终将真的导航栏显示出来。为了让大家更好地理解上面的内容,请参考下图:说完了 push 过程,我们再来说一下从页面 B pop 回页面 A 的过程,几个核心方法的调用顺序如下:页面 B 的 popViewControllerAnimated:页面 A 的 viewWillAppear:页面 A 的 viewDidAppear:在 pop 过程的开始,转场库会在页面 B 自身的 view 上添加一个与导航栏一模一样的 NavigationBar 并将真的导航栏隐藏,虽然这个假的导航栏会一直存在于页面 B 上,但它自身会随着页面 B 的 dealloc 而消亡。等到页面 A 调用 viewWillAppear: 的时候,开发者在这里自行设置真的导航栏样式。当然我们也可以不设置,因为这时候页面 A 还持有一个假的导航栏,这里还保留着我们之前在 viewDidLoad 里写的导航栏样式。等到页面 A 调用 viewDidAppear: 的时候,转场库会将假的导航栏样式设置到真的导航栏中,并将假的导航栏从视图层级中移除,最终将真的导航栏显示出来。同样,我们可以参考下面的图来理解上面所说的内容:现在,大家应该对我们美团的解决方案有了一定的认识,但在实际开发过程中,还需要考虑一些布局和适配的问题。最佳实践在维护这套转场方案的时间里,我们总结了一些此类方案的最佳实践。判断导航栏问题的基本准则如果发现导航栏在转场过程中出现了样式错乱,可以遵循以下几点基本原则:检查相应 ViewController 里是否有修改其他 ViewController 导航栏样式的行为,如果有,请做调整。保证所有对导航栏样式变化的操作出现在 viewDidLoad 和 viewWillAppear: 中,如果在 viewWillDisappear: 等方法里出现了对导航栏的样式修改的操作,如果有,请做调整。检查是否有改动 translucent 属性,包括显示修改和隐式修改,如果有,请做调整。只关心当前页面的样式永远记住每个 ViewController 只用关心自己的样式,设置的时机点在 viewWillAppear: 或者 viewDidLoad 里。透明样式导航栏的正确设置方法如果需要一个透明效果的导航栏,可以使用如下代码实现:[self.navigationController.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];self.navigationController.navigationBar.shadowImage = [UIImage new]; 导航栏的颜色渐变效果如果需要导航栏实现随滚动改变整体 alpha 值的效果,可以通过改变 setBackgroundImage:forBarMetrics: 方法里 image 的 alpha 值来达到目标,这里一般是使用监听 scrollView.contentOffset 的手段来做。请避免直接修改 NavigationBar 的 alpha 值。还有一点需要注意的是,在页面转场的过程中,也会触发 contentOffset 的变化,所以请尽量在 disappear 的时候取消监听。否则会容易出现导航栏透明度的变化。导航栏背景图片的规范请避免背景图里的像素点没有 alpha 通道或者 alpha 全部等于 1,容易触发 translucent 的隐式改变。如果真的要隐藏导航栏如果我们需要隐藏导航栏,请保证所有的 ViewController 能坚持如下原则:每个 ViewController 只需要关心当前页面下的导航栏是否被隐藏。在 viewWillAppear: 中,统一设置导航栏的隐藏状态。使用 setNavigationBarHidden:animated: 方法,而不是 setNavigationBarHidden:。转场动画与导航栏隐藏动画的一致性如果在转场的过程中还会显示或者隐藏导航栏的话,请保证两个方法的动画参数一致。- (void)viewWillAppear:(BOOL)animated{ [self.navigationController setNavigationBarHidden:YES animated:animated];}viewWillAppear: 里的 animated 参数是受 push 和 pop 方法里 animated 参数影响。导航栏固有的系统问题目前已知的有两个系统问题如下:当前后两个 ViewController 的导航栏都处于隐藏状态,然后在后一个 ViewController 中使用返回手势 pop 到一半时取消,再连续 push 多个页面时会造成导航栏的 Stack 混乱或者 Crash。当页面的层级结构大体如下所示时,在红色导航栏的 Stack 中,返回手势会大概率的出现跨层级的跳转,多次后会导致整个导航栏的 Stack 错乱或者 Crash。导航栏内置组件的布局规范导航栏里的组件布局在 iOS 11 后发生了改变,原有的一些解决方案已经失效,这些内容不在本篇文章的讨论范围之内,推荐阅读UIBarButtonItem 在 iOS 11 上的改变及应对方案,这篇文章详细的解释了 iOS 11 里的变化和可行的应对方案。总结本文涉及内容较多,从 iOS 系统下的导航栏概念到大型应用里的最佳实践,这里我们总结一下整篇文章的核心内容:理解导航栏组件的结构和相关方法的生命周期。导航栏组件的结构留有 MVC 架构的影子,在解决问题时,要去相应的层级处理。转场问题的关键点是方法的调用顺序,所以了解生命周期是解决此类问题的基础。状态管理,转换时机和样式变化是导航栏里常见问题的三种表现形式,遇到实际问题时需要区分清楚。状态管理要坚持“谁修改,谁复原”的原则。转换时机的设定要做到连续可执行。样式变化的核心点是导航栏的显示与否与颜色变化。为了更好的配合大型应用里的路由系统,导航栏转场的常见解决方案有三种,各有利弊,需要根据自身的业务场景和历史包袱做取舍。解决方案1:自定义导航栏组件。解决方案2:在原有导航栏组件里添加 Fake Bar。解决方案3:在导航栏转场过程中添加 Fake Bar。美团在实际开发过程中采用了第三种方案,并给出了适合美团 App 的最佳实践。特别感谢莫洲骐在此项目里的贡献与付出。参考链接UIAppearance Tutorial: Getting StartedKMNavigationBarTransition作者简介思琦,美团点评 iOS 工程师。2016 年加入美团,负责美团平台的业务开发及 UI 组件的维护工作。招聘美团平台诚招 iOS、Android、FE 高级/资深工程师和技术专家,Base 北京、上海、成都,欢迎有兴趣的同学投递简历到zhangsiqi04@meituan.com。 ...

October 26, 2018 · 6 min · jiezi

快速搭建一个“微视”类短视频 App

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由腾讯云视频发表于云+社区专栏关注公众号“腾讯云视频”,一键获取 技术干货 | 优惠活动 | 视频方案“爱就像蓝天白云晴空万里,突然暴风雨……”偷偷在上班期间看微视里美丽的小姐姐,不巧被老大当场抓包“看来还是给你安排的工作太少了,这样吧,竟然你那么喜欢看微视,那就给你三天时间搭建出类似微视的短视频APP,要搭建不起来,这个月的绩效奖金就取消!”我的天!除了绝望还是绝望!人们常说人生如戏,戏如人生,这句话怕是要改成,人生如歌,歌如人生。三天的时间搭建短视频APP,唯有寻求度娘的帮助,网上的解决方案有千万种,最后选择了腾讯云的解决方案,从接入过程到最后的呈现效果都很赞,接下来带大家了解如何从零开始快速搭建一个类似““微视”类的短视频APP。集成独立的短视频功能如果您需要在您的 App 中集成短视频的部分功能,请参考如下四个步骤: 01step1: 申请测试License1.1 登录腾讯云官网,进入 短视频控制台,填写相应的信息,其中 Package Name 是填 Android 的包名,Bundle Id 是填 iOS 的 bundleId。1.2 创建后生成 License 信息,其中 Key 和 LicenceUrl 是要在集成的 SDK 中用到的。 02step2: 下载 SDK 并导入工程2.1 到 SDK 下载页下载对应的 iOS 和 Android 的 SDK2.2 按照指引完成工程配置iOS 平台拷贝 SDK 文件,添加 Framework,添加 -ObjC,引用头文件,短视频发布功能集成。具体详细内容参考 iOS 工程配置。Android 平台有 jar 包和 aar 两种集成方式选择使用,具体详细内容参考和 Android 工程配置。2.3 最后一定不要忘记验证一下配置是否正确: iOS 平台在 ViewController.m 开头引用 SDK:@import TXLiteAVSDK_UGC;在 viewDidLoad 方法中添加代码:- (void)viewDidLoad { [super viewDidLoad]; // 打印SDK的版本信息 NSLog(@“SDK Version = %@”, [TXLiveBase getSDKVersionStr]);}如果前面各个步骤都操作正确的话,HelloSDK 工程就可以顺利编译通过。在 Debug 模式下运行 App,Xcode 的 Console 窗格会打印出 SDK 的版本信息。2017-09-26 16:16:15.767 HelloSDK[17929:7488566] SDK Version = 3.4.1761Android 平台在 MainActivity.java 中引用 SDK 的 class:import com.tencent.rtmp.TXLiveBase;在 onCreate 中调用 getSDKVersioin 接口获取版本号:String sdkver = TXLiveBase.getSDKVersionStr();Log.d(“liteavsdk”, “liteav sdk version is : " + sdkver);如果前面各步骤都操作正确,demo 工程将顺利编译通过,运行之后将在 logcat 中看到如下 log 信息:09-26 19:30:36.547 19577-19577/ D/liteavsdk: liteav sdk version is : 3.9.2794至此,工程配置完成。03step3: 集成 License工程配置完之后,需要集成短视频 License 才能使用基础功能。第一步中已经拿到了Key和LicenceUrl,只需要一行代码配置就可以完成。iOS 平台在您的应用中使用短视频功能之前(建议在 AppDelegate 中)把拿到的 key 和 url 设置到下面接口中[TXUGCBase setLicenceURL:url key:key];Android 平台在您的应用中使用短视频功能之前(建议在 application 中)把拿到的 key 和 url 设置到下面接口中TXUGCBase.getInstance().setLicence(context, url, key);另外需要注意的是:1.您可以选择是否打包 license 到应用中:如果不选择打包,SDK 第一次使用需要访问网络;如果选择打包,iOS 把 TXUgcSDK.licence(名称要正确)拷贝到 App 中即可;Android 把 TXUgcSDK.licence(名称要正确)放到 asset 根目录下即可。2.当您的 license 过期了,可以登录腾讯云点播控制台进行续费,SDK 会自动续期,不需要您的应用做任何操作3.如果您的 license 校验失败,您可以调用下面代码来查看 license 信息是否填写错误。// iOS NSLog(@”%@", [TXUGCBase getLicenceInfo]);// AndroidTXUGCBase.getInstance().getLicenceInfo();04step4: 集成独立功能如果您希望在自己的 App 中添加部分短视频功能(比如录制和上传),那么可以参考如下的功能集成文档,完成指定功能的嵌入:接入功能iOS 平台Android工程配置XCodeAndroid Studio视频录制APIAPI视频编辑APIAPI视频拼接APIAPI视频上传APIAPI视频播放 APIAPI动效变脸APIAPI上述就是本文根据腾讯云官方文档指引,以Android和iOS平台为例总结的快速搭建短视频APP服务的主要步骤,如有遇到其他问题,欢迎留言。问答短视频UGSV中,短视频怎么编辑 ?相关阅读相较于传统视频,短视频开发主要有哪些特点一对一直播系统搭建,主要应用于什么样的行业领域短视频APP开发——主要功能分析 【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...

October 26, 2018 · 1 min · jiezi

iOS Push详述,了解一下?

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由WeTest质量开放平台团队发表于云+社区专栏作者:陈裕发, 腾讯系统测试工程师商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。原文链接:http://wetest.qq.com/lab/view/380.htmlWeTest 导读本文主要对iOS Push的在线push、本地push及离线(远程)push进行梳理,介绍了相关逻辑,测试时要注意的要点以及相关工具。小小的Push背后蕴藏着大大的逻辑!Push种类一、在线push在线push:当用户在线(APP在前台)时,收到的状态栏的消息提醒,称为在线push。这个功能与苹果系统无关,是我们自己的APP开发的一种功能,该push与设置中是否打开“通知”无关。这里以iOS Qzone为例,当APP在前台时,自己发的说说被点赞了,收到的在线push如下:1.png Qzone在线push 二、离线(远程)push离线push:当APP在离线(kill掉进程、切到后台、锁屏)时,收到的消息提醒,称为离线push。离线push是需要经过苹果的APNs服务器才可以推送到某台设备的某个APP上的,这是和本地push的本质区别。push与设置中是否打开“通知”有关。这里最简单的以大家常用的手机QQ为例,当APP在后台、锁屏或者被kiil了进程时,收到了消息:2.png 离线push 1、静默push静默push用的场景不较少,这里只做简要介绍。首先我们看看离线(远程)push与静默push的区别:普通离线(远程)push:收到推送后(有文字有声音),点开通知,进入APP后,才执行– (void)application:(UIApplication didReceiveRemoteNotification:(NSDictionary fetchCompletionHandler:(void result))handler )application )userInfo (^)(UIBackgroundFetchResult静默push:收到推送(没有文字没有声音),不用点开通知,不用打开APP,就能执行(void)application:(UIApplication )application)userInfo didReceiveRemoteNotification:(NSDictionary fetchCompletionHandler:(void (^)(UIBackgroundFetchResultresult))handler,用户完全感觉不到。所以静默push又被我们称做 Background Remote Notification(后台远程推送)。静默推送是在iOS7之后推出的一种推送方式。它与其他推送的区别在于允许应用收到通知后在后台(background)状态下运行一段代码,可用于从服务器获取内容更新。三、本地push本地push:本地推送和远程推送的功能是一样的,都是要提醒用户去做某些事情。但是和远程推送不同的就是本地推送是不需要设备联网的,而远程推送是必需要设备联网的,因为只有联网状态下,才能和苹果的APNs服务器建立长连接,从而推送消息。本地推送是由App自己设定的,并且发送给安装此App的这台设备,属于一对一的对应关系。比较典型的应用是闹钟类似的场景。该push与设置中是否打开“通知”有关。最容易看到本地push的场景,可以直接在手机设置一个计时器,计时器时间到了就会弹出本地push:3.png 本地push4.png由于本地push原理和作用相对于在线push和离线push都更为简单明了,下文主要介绍在线push和离线push。本地push实现一、 iOS10以前本地push弹出方式试验过iOS10以前的本地push方法在iOS10+的系统也能使用,不过可能有些参数不生效。1、立即展示( iOS10以前)本地push稍微简单,有两种方式可以调用,一种是presentLocalNotificationNow方法,立即展示本地push:5.png2、延迟展示( iOS10以前)另一种是用scheduleLocalNotification方法按计划来弹本地推送:6.png如果使用这种方法,需要对推送的时间进行设置,举个例子,设为5秒后:7.png二、设置本地push内容( iOS10以前)8.png其中alertBody是消息内容锁屏与不锁屏时效果如下:9.png 本地push效果applicationIconBadgeNumber是消息数量,我们可以看到这里设置为66:10.png 消息数三、处理本地push ( iOS10以前)1、 App没有启动情况下处理本地push这种情况下,当点击通知时,会启动App,而在App中,开发人员可以通过实现AppDelegate中的方法:- (BOOL)application:(UIApplication)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions,然后从lauchOptions中获取App启动的原因,若是因为本地通知,则可以App启动时对App做对应的操作,比方说跳转到某个画面等等。11.png2、App运行在后台及前台上面的2种情况的处理基本一致, 不同点只有当运行再后台的时候,会有弹窗提示用户另外一个App有通知,对于本地通知单的处理都是通过AppDelegate的方法:- (void)application:(UIApplication )application didReceiveLocalNotification:(UILocalNotification *)notification来处理的。12.png四、iOS10以后本地push弹出方式iOS10以后,本地通知可以由使用 UNUserNotificationCenter来管理。创建方法:13.png接下来需要需创建一个包含待通知内容的 UNMutableNotificationContent 对象:14.png在iOS上可以通过以下几种触发器来触发本地push:● UNCalendarNotificationTrigger 传送本地通知的日期和时间。● UNTimeIntervalNotificationTrigger 传递本地通知之前必须过期的时间。● UNLocationNotificationTrigger 用户必须达到的地理位置才能提供本地通知。● UNPushNotificationTrigger 表示通知是从Apple推送通知服务发送的对象。假如以时间间隔(TimeInterval)来触发,则设置触发器代码为:15.png推送本地push的代码为:16.png在线、离线(远程)push流程一、在线push流程在线push相对简单,因为是内部实现,具体流程如上面所示。1、判断app是否在线此处可以根据APP自身的后台策略如上一次与后台交互的时间等方法来判断APP是否在线或者离线。认为在线,会发送在线push,否则,发送离线push。2、在线push特点● 在线push有以下几个特点:● 不需要经过苹果APNs。● 需要自己实现长链接。● 代码在app内部实现。二、离线(远程)push流程17.png离线push流程主要流程为:● 服务器端将消息先发送到苹果的APNs● 由苹果的APNs将消息推送到客户的设备端● 由iOS系统将接收到的消息传递给相应的App。简而言之离线push是苹果系统的行为,与app状态无关,能够直接推送到指定手机的指定app。在进一步了解离线push前,我们有必要先了解几个名词。1、离线push名词解释—APNsAPNs:Apple Push Notification service(苹果推送通知服务)。APNs主要用于以下场景:当用户主动杀掉 APP,或者 APP 进入后台超过约定时长时,APP会被kill,这样保障了前台 APP 的流畅性,也延长了手机的使用时长,获得了较好的用户体验,但是这也意味着,服务器无法主动和用户交互(如推送实时消息等),所以苹果推出了 APNs,允许设备和服务器分别与苹果的推送通知服务器保持长连接状态。关于APNs的更新有以下几点:● iOS 8以后,APNs推送的字节是2k,iOS8以前是256字节● iOS 9以后APNs支持HTTP/2协议栈,优化长连接,具有标准的HTTP返回和管道复用技术● iOS 10以后,推送的字节是4k,APNs可根据推送消息的唯一标示符查询某条消息是否被用户阅读,可更新某一推送消息,而不用发重读的多条消息关于APNs更全面的介绍可以看官方文档:https://developer.apple.com/l…—payload什么是payload?对于每一条发送给APNs的推送消息,都包含一个payload,通常是组成了一个JSON的Dictionary,这其中必不可少的是aps属性,它对应的value也是一个Dictionary,包含一些但不限于以下内容:标题、副标题、内容、附件、category等,如18.png—device token什么是device token?我们看一下官方的简介:device token: APNs uses device tokens to identify each unique app and device combination. It also uses them to authenticate the routing of remote notifications sent to a device.(device token是APNs用于区分识别每个iOS设备和设备上不同app的一个标识符,还可以用于APNs通过它将推送消息路由到指定设备上)即:device token里包含了device id和bundle id的信息,但是device id和bundle id不会确定唯一的device token。但是,这里有个坑,查资料得知,iOS8及之前的iOS系统,对于同一部手机,如果卸载后重装APP的话,device token是不会变的,在token变了以后,老的token,就被认为是无效了,苹果不会对这部分无效的token推送。但是,对iOS9及以后的iOS系统,对于同一部手机,卸载后重装APP的device token是会发生变化的,而且老的token不会无效,还可以正常推送,这应该是苹果的一个bug,但是苹果也没有修复这个问题,所以这个需要开发者自己来解决,否则容易出现一个app收到多个push的问题。官方的说法是:To protect user privacy, do not use device tokens to identify user devices. Device tokens change when the user updates the operating system and when a device’s data and settings are erased. As a result, apps should always request the current device token at launch time.(即此举为了保护用户隐私,device token会在更新系统、擦除设置重置后变化,在一定时间后会过期)2、离线push详细流程知道了以上概念后我们重新来看一下离线(远程)push的详细流程:19.png 离线push详细流程1) 首先是应用程序注册消息推送。2) iOS跟APNS Server要deviceToken。应用程序接受deviceToken。3) 应用程序将deviceToken发送给PUSH服务端程序。4) 服务端程序向APNS服务发送消息。5) APNS服务将消息发送给iPhone应用程序。值得注意的是,当由于用户反复卸载重装程序(虽然概率很小)等原因导致多个device Token指向同一台设备的同一个app,又把多个device Token发给APNs时,用户就会收到多条push。苹果APNs是不会对多个device Token是否指向同一台设备的同一个app做校验的,所以需要后台来做去重等处理保证用户不会收到多条push。三、对离线(远程)push的响应1、iOS 7以上对离线(远程)push时的响应iOS 7以上关于接受离线push有两个函数20.png那么这两个函数有什么区别呢?其实这两个方法都是用来处理离线push的。差别就是,如果app在前台是收到离线(远程)push,那么就会调用21.png相对的,如果在后台或者杀进程情况下,点击收到的离线push,那么就会调用,如果没有实现22.png则会调用23.png若实现了前者,就只调用前者。2、iOS 10以上对离线(远程)push的响应iOS10对push的处理主要增加了两个方法24.png其中前者是对APP在前台时收到push时的处理,后者是点击push进入APP执行的函数。用得比较多的是后者,我们可以举个例子,点击push进入APP后如何获取push的消息、角标、标题等内容:25.pngiOS 10关于push的一些新特性iOS10新增的UserNotifications框架,主要有了这样几方面的更新:● 用UserNotifications框架替换了原先与通知相关的接口,通知文字可分为title、subtitle和body三部分,通知可携带附件● 系统在展示通知之前,可以唤起app附带的service extension,并且允许它改动通知的内容● 用户在对通知右滑查看、下拉或者3d touch的时候,通知会展开,展开后页面的布局可以由app附带的content extension来决定一、push的多样性iOS10以前的push只有文字,甚至没有标题。iOS10以后的push更加多样化,可以有主标题,副标题,甚至还有附件,这里以我司的腾讯新闻为例(有标题,内容,和附件):26.png 腾讯新闻push3D touch点入详情以后:27.png 腾讯新闻push详情这里我们惊奇的发现,除了可以携带图片这样的附件、push还能展开详情以外,进入详情以后,下面还多了“打开”、“收藏”、“不感兴趣”这些选项,这里就涉及到以下iOS10的新特性。二、push携带附件因为payload有大小限制,所以如果remote notification想要携带附件,那么payload上只能带上如附件下载地址之类的信息,等通知到达客户端后由service extension下载附件到本地,然后在初始化UNNotificationAttachment对象时传入附件在本地的URL。28.png初始化UNNotificationAttachment对象时,可以传入option参数。这里的option参数可以强制指定附件的类型,可以选择是否展示缩略图,以及缩略图截取自附件的哪一帧、哪一部分。目前iOS10通知只将几种格式的图片、音频和视频作为附件,附件的大小也有一定限制,具体可以看官方文档中的限制说明。关于附件的更加详细的说明,可以参考官方文档:https://developer.apple.com/d…三、携带action的通知上面提到的“打开”、“收藏”、“不感兴趣”这些选项其实就是push携带的action,其实从iOS8开始,通知已经可以携带action了。而在iOS10中,通知的action被放在了更明显的位置,与action相关的接口也有了很大变化。决定一个通知应该有哪些action呢?在payload中,这是由category字段决定的。如果我们希望一个通知能携带若干个action,我们就需要将若干个action和一个category绑定起来。通知到达前端后,系统会根据category的名字来决定要给这个通知展示哪些action:29.png怎么得知用户选了哪个action并做出相应操作呢?这需要给UNUserNotificationCenter指定一个delegate:30.png然后在delegate的类中实现31.png方法:通过response.notification.request.content.categoryIdentifier和response.actionIdentifier就可以得知用户选择的action了。四、改变push内容这里主要讲应用的比较多的离线(远程)push的改变push方法1、改变本地push内容本地push,只要request的id一样,那么就可以更新推送:更新的例子:31.png32.png此外,还有删除所有推送等,都在UNUserNotificationCenter.h中实现。2、改变离线(远程)push内容目前远程push只支持更新push内容,更新需要通过新的字段apps-collapse-id来作为唯一标示。方法是在HTTP/2 请求头中使用相同的apns-collapse-id,这样收到同样的apns-collapse-id的push时,push内容便会更新。使用场景:比较容易理解的一个场景就是球赛比分,比如现在是1:0,如果变成1:1的话,只需要刷新原来的新闻,这样用户就不会因为同一场比赛收到多条push。五、两个extension有两个与push相关的extension,可能我们会好奇这两个extension有什么不同,为什么需要两个?它们分别实现什么功能呢?33.png push相关extension1、notification service extension给app添加notification service extension后,系统会在收到通知后唤醒它,并允许它修改通知的内容,之后再展示这个通知。service extension只对remote notification起作用,local notification是无法唤起它的。如果想要让系统唤起service extension的话,payload必须符合这样几个条件:1) 必须增加mutable-content字段并为1,这表示允许客户端修改这个通知:payload(举例)如下:34.png2)这个通知必须展示一个alert,如果只是一个修改badge的通知的话,是不会唤起service extension的3)静默推送是不能唤起service extension的,所以payload中不能有”content-available” : 1字段所以,通过这个notification service extension,你可以在接收到推送之后、展示推送之前处理一些事情,比如说更新一下推送内容,或者在后台做一些其他事情。2、notification content extension另一项notification content extension用于完全自定义推送展开后的视图。上面腾讯新闻的展开后的视图就是通过这个notification content extension实现的。依然以腾讯新闻为例子:35.png 展开界面这里Notification Content Extension大展拳脚的地方,在这里可以自定义绘制不同的内容,将希望展现给用户的额外信息可以加载这里。下半部分的notification action的实现就是在上面提到的“携带action的通知”。测试要点36.pngQ&AQ:离线push,支持角标(badge)在本地角标数值上+1这样的操作吗?A:不支持。如果是自己实现push服务的话,需要自己的后台将角标值badge发送个APNs服务器,有些APP使用第三方push SDK除外。Q:如果重复收到离线push,可能是什么情况?A:1)iOS9之后卸载重装后生成新的deviceToken,后台对多个deviceToken都发送了push2)后台对注销了的账号也发送了push。总而言之一般是后台的逻辑出现了问题,而不是APNs服务器出现问题。Q:直接卸载APP,还能收到离线push吗?A:不会收到。直接卸载APP,虽然后台不知道APP被卸载了,仍然会对之前的账号发送push,但是由于手机上没有对应APP,所以并不会收到push。Q:为什么有时候全新安装APP就立马有红点角标?A:这是因为卸载该APP时有红点角标。每个 APP 的角标都是存在 iOS 手机系统里的,开发无法修改,所以此时卸载前有角标,重新安装也会有角标。但是,APP 卸载之后超过一天的时间再重装,那么角标就会被系统清空,届时也不会有新安装的 APP 就有角标的情况存在。相关工具Knuff离线push工具下载链接:https://github.com/KnuffApp/K…使用方法也比较简单37.png比如我的payload输入如下:38.png得到的应该是有“Knuff测试”文字,和角标数变为999,我们可以看下结果,与预料是一致的:39.png 预期结果有了这个工具也更加方便了我们的iOS push的调试。参考资料iOS推送之远程推送(iOS Notification Of Remote Notification):https://www.jianshu.com/p/4b9…相关阅读系统负载能力浅析搭建公众号自动回复功能【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...

October 26, 2018 · 2 min · jiezi

iOS开发手册

1.工程文件结构所有的文件应放在工程中的项目目录下。项目文件和物理文件需保持一致。Xcode创建的任何组(group)都必须在文件系统中有映射。项目文件不仅可以按照业务类型分组,也可以根据功能分组。2.代码格式规范2.1 代码注释格式文件注释:采用Xcode自动生成的注释格式。//// AppDelegate.h// 项目名称//// Created by 开发者姓名 on 2018/6/8.// Copyright © 2018年 公司名称. All rights reserved.//import注释:如果有一个以上的import语句,对这些语句进行分组,每个分组的注释是可选的。// Framework#import <UIKit/UIKit.h>// Model#import “WTUser.h”// View#import “WTView.h"方法注释:Xcode8之后快捷键自动生成(option + command + /)。/**<#Description#>@param application <#application description#>@param launchOptions <#launchOptions description#>@return <#return value description#>*/- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;代码块注释:单行的用 “// + 空格” 开头, 多行用“/ /”。2.2 代码结构与排版声明文件:方法顺序和实现文件的顺序保持一致,根据需要用”#pragma mark -“将方法分组。实现文件:必须用”#pragma mark -“将方法分组。分组前后优先级:Lifecycle方法 > Public方法 > UI方法 > Data方法 > Event方法 > Private方法(逻辑处理等) > Delegate方法 > 部分Override方法 > Setter方法 > Getter方法。#pragma mark - Lifecycle- (instancetype)init {}- (void)viewDidLoad {}- (void)viewWillAppear:(BOOL)animated {}- (void)viewDidAppear:(BOOL)animated {}- (void)viewWillDisappear:(BOOL)animated {}- (void)viewDidDisappear:(BOOL)animated {}- (void)didReceiveMemoryWarning {}- (void)dealloc {}#pragma mark - Public- (void)refreshData {}#pragma mark - UI- (void)initSubViews {}#pragma mark - Data- (void)initData {}- (void)constructData {}#pragma mark - Event- (void)clickButton:(UIButton *)button {}#pragma mark - Private- (CGFloat)calculateHeight {}#pragma mark - UIScrollViewDelegate- (void)scrollViewDidScroll:(UIScrollView *)scrollView {}#pragma mark - Override- (BOOL)needNavigationBar {}#pragma mark - Setter- (void)setWindow:(UIWindow *)window {}#pragma mark - Getter- (UIWindow *)window {}变量:优先使用属性声明而非变量声明,注意属性修饰符、变量类型、变量之间的间隔。@property (strong, nonatomic) UIWindow *window;点语法:应始终使用点语法来访问和修改属性。间距要求如下:一个缩进使用四个空格。在”-“或者”+“号之后应该有一个空格,方法的大括号和其它大括号始终和声明在同一行开始,在新的一行结束,另外方法之间应该空一行。- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary )launchOptions { if (door.isClosed) { // Do something } else { // Do something } return YES;}长度要求如下:每行代码的长度不应该超过100个字符。单个函数或方法的实现代码控制在50行以内。单个文件里的代码行数控制在500600行之内。3.代码命名规范3.1 代码命名基础最好是既清晰又简短,但不要为简短丧失清晰性,并使用驼峰命名法。名称通常不缩写,即使名称很长也要拼写完全(禁止拼音),然而可使用少数非常常见的缩写,部分举例如下:常用缩写词含义常用缩写词含义appapplicationmaxmaximumaltalternateminminimumcalccalculatemsgmessageallocallocterectrectangledeallocdealloctemsgmessageinitinitializetemptemporaryintintegerfuncfunction由于Cocoa(Objective-C)没有C++一样的命名空间机制,需添加前缀(公司名首字母)防止命名冲突,前缀使用2个字符(以下统称项目前缀)。常见的单词略写:ASCII,PDF,HTTP,XML,URL,JPG,GIF,PNG,RGB等3.2 类和协议命名类名应明确该类的功能,并且要有项目前缀防止命名冲突。协议组合一组方法作为一个类的部分接口使用, 用类名作为协议名,例如:NSObject。协议仅仅组合一组方法而不关联具体类,这种协议的命名应采用动名词形式(ing)。委托形式的协议命名为类名加上Delegate,例如:UIScrollViewDelegate。3.3 变量和属性命名变量名应前置下划线“”,属性名没有下划线。属性本质上是存取方法setter/getter,可进行重写(注意内存管理)。@property (strong, nonatomic) UIWindow *window;- (void)setWindow:(UIWindow *)window;- (UIWindow *)window;可以适当的对setter/getter进行别名设置。@property(nonatomic,getter=isUserInteractionEnabled) BOOL userInteractionEnabled;3.4 方法和函数命名方法名和函数名一般不需要前缀,但函数(C语言形式)作为全局作用域的时候最好加上项目前缀。 表示行为的方法名称以动词开头,但不要使用do/does等无实际意义的助动词。参数前面的单词要能够描述该参数,并且参数名最好能用描述该参数的单词命名。- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;方法中多个参数可以使用适当的介词进行连接。// 后续多个参数使用with- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;// 添加适当介词能够使方法的含义更明确- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;// 第一个参数用了with,后面的参数不使用with- (instancetype)initWithImage:(nullable UIImage *)image highlightedImage:(nullable UIImage *)highlightedImage;只有在方法返回多个值的时候使用get单词进行明确。- (void)getLineDash:(nullable CGFloat *)pattern count:(nullable NSInteger *)count phase:(nullable CGFloat *)phase;方法返回某个对象实例。// 类方法创建对象+ (instancetype)buttonWithType:(UIButtonType)buttonType;// 单例命名+ (UIApplication *)sharedApplication; 委托或代理方法命名第一个参数最好能相关某个对象。- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;私有方法不要以下划线““开头,因为系统私有方法保留此方式。自定义方法和系统方法重名,建议在方法开头加前缀”xx_methodName“。3.5 常量和宏的命名const常量外部声明:在Objective-C文件中优先采用FOUNDATION_EXTERN和UIKIT_EXTERN,而非C语言中的extern。const常量采用驼峰命名原则。const常量根据作用域适当加上前缀(含项目前缀):可供外部使用需加上相应的类名或者模块前缀,仅文件内部使用需要加上小写字母“k”.宏定义每个字母采用大写,单词之间用下划线“”间隔。宏定义也可根据作用范围加上适当前缀,避免命名冲突。3.6 枚举的命名使用枚举来定义一组相关的整数常量,增强代码的可读性。枚举可根据作用域添加前缀(含项目前缀),格式:[相关类名或功能模块名] + [描述] + [状态]。建议优先采用Objective-C的声明NS_ENUM和NS_OPTIONS,少采用C语言形式的enum等枚举声明.枚举定义时需指定None状态,并且其rawValue一般为起始值0。// NS_ENUMtypedef NS_ENUM(NSInteger, UIStatusBarAnimation) { UIStatusBarAnimationNone = 0, UIStatusBarAnimationFade = 1, UIStatusBarAnimationSlide = 2,}typedef NS_OPTIONS(NSUInteger, UIRemoteNotificationType) { UIRemoteNotificationTypeNone = 0, UIRemoteNotificationTypeBadge = 1 << 0, UIRemoteNotificationTypeSound = 1 << 1, UIRemoteNotificationTypeAlert = 1 << 2, UIRemoteNotificationTypeNewsstandContentAvailability = 1 << 3,}3.7 通知命名外部声明:在Objective-C文件中优先采用FOUNDATION_EXTERN和UIKIT_EXTERN,而非C语言中的extern。通知的命名一般都是跨文件使用的,需添加项目前缀。// [相关联类名或者功能模块名] + [will/Did](可选) + [描述] + NotificationUIApplicationDidEnterBackgroundNotification UIApplicationWillEnterForegroundNotification 3.8 类型别名命名根据作用域添加前缀(含项目前缀),格式:[类名或功能模块名] + [描述]。4.文件资源命名规范资源文件命名也需加上项目前缀。资源文件名全小写,单词之间用下划线“”间隔。资源文件命名格式:[项目前缀] + [业务] + [文件名]图片文件命名格式:[项目前缀] + [业务] + [类型] + [状态]。// 常见类型:logol,icon,img// 常见状态:normal,selected,highlightUIImage *image = [UIImage imageNamed:@“wt_setting_icon_normal”];5.代码警告处理注意警告问题的隐蔽性,因此最好修复警告。警告类型的查看步骤:选中警告 -> 右键Reveal in Log(不编译Reveal in Log是灰色的,因此先编译) ->查看方括号的内容如果需要忽略警告,建议优先代码push或者pop处理。#pragma clang diagnostic push#pragma clang diagnostic ignored “-Warc-retain-cycles”// 造成警告的代码#pragma clang diagnostic pop如果警告数量过大,检查警告类型以及必要性,可xcode配置忽略此类型警告。步骤:选中工程 -> TARGETS -> Build Settings -> Other Warning Flags。忽略单个和全局配置稍有差别,如下举例:push/pop Other Warning Flags-Wformat —-> -Wno-format -Wunused-variable —-> -Wno-unused-variable -Wundeclared-selector —-> -Wno-undeclared-selector -Wint-conversion —-> -Wno-int-conversion也可以在pch等大范围作用域的头文件中添加代码来忽略后续警告:#pragma clang diagnostic ignored “警告名称” 。6.外部库文件引入库文件引入最好把警告处理掉。库文件引入优先采用CocoaPods引入,并且指定版本号。源文件方式需引入文件到工程目录下。源文件方式需注意有无版本说明信息(可能在README文件中,也可能在某个.h头文件中,又或者有Version文件)没有时需在库文件目录下新增版本说明文件,7.代码版本管理版本管理工具:svn 或 git。svn文件管理配置:目录/.subversion打开config文件配置global-ignore。git文件管理配置:.gitignore文件记录了被git忽略的文件,作用于本仓库,常见语法如下:井号(#)用来添加注释用的,比如 “#注释”。build/ : 星号()是通配符,build/则是要说明要忽略 build 文件夹下的所有内容。.pbxuser : 表示要忽略后缀名为.pbxuser的文件。!default.pbxuser : 感叹号(!)是取反的意思,.pbxuser 表示忽略所有后缀名为.pbxuser的文件,如果加上!default.pbxuser则表示,除了default.pbxuse忽略其它后缀名为pbxuse的文件。提交信息规范:BUG类型为“Fix + [BUG编号] + [BUG描述]”。任务类型为“Done + [任务编号] + [任务描述]”。任务中间态为“Doing + [任务编号] + [任务描述]”。引入类库为“import + [类库名]”。8.构建和分发手动构建:Xcode界面化构建注、xcodebuild终端命令构建。自动化构建:Jenkins+Fastlane、xcodebuild脚本执行 。内测分发渠道:fir.im、蒲公英等。线上分发渠道:AppStore。 ...

October 23, 2018 · 2 min · jiezi

我理解的 iOS 与 Android 的区别

事实上在讲清楚这个问题之前,必须知道一个所有人都无法拒绝的常识,即:对大多数人来说 iOS 绝对比 Android 好用。本文试着从使用者的角度出发谈谈自己对两个手机端操作系统的理解与认识iOS 为何好用决定一个操作系统好用与否的原因有很多,系统的功能健全、用户体验、稳定性、应用程序及其周边生态等。iOS 操作系统从本质上讲是一个闭源、高度统一且集中式的操作系统,这个集中可以理解为中心化的。回顾 iOS 发展历你会感觉这种高度集中的程度甚至达到了「集权」的程度,苹果对自己的操作系统有着几乎所有的控制权,这就使得 iOS 系统无论是 UI 交互、设计风格还是应用生态都天然的具备了近乎完美的一致性。当你手里拿着最新款的 iPhone 的时候你会觉得她简直就是一件艺术品。深层次上这就使得苹果的产品在审美上可以 主动的 影响用户,而不是被动的接受用户的反馈大家都知道 iOS 的生态好,但是到底好在哪里?我认为好在两方面其一 应用生态,一个应用从前期的设计、中期的开发、后期的运营苹果都提供了很多解决方案。设计时有 HIG,开发时有 Xcode 集成开发环境,运营商业化的时候有应用内购买、Apple Pay、Searchads 等一系列的解决方案,所以我们看到很多独立开发者做的应用都非常优秀甚至超过了某些商业公司。细心的话你会发现上面提到的这些似乎在 Android 平台也有一些影子。但实际上差别还是很大的,这就好比程序员写的代码示例和生产环境跑的应用程序对比起来完全不是一个量级的概念其二 系统生态,这一点可能很多人并没在意,但是却非常重要。操作系统做为所有上层应用的基础其重要性不言而喻,据说 iOS 12 安装率已达到了 50%,iOS 11 使用了一个月的时间才达到的水平 iOS 用了 20 天内就完成了。由此可见其高效的更新迭代周期。在软件开发的过程中有重要的一个概念就是软件的生命周期。其实软件和人是一样的,人有生老病死,软件有增删查改。好的软件应该保持这种平衡。老旧的部分该删除的就应该删除,欠缺的部分该添加添加应该升级升级。可是大多时候人们都只喜欢添加加功能。程序员应该非常清楚这一点,维护或者兼容老系统是非常痛苦的,但是在苹果却能把 iOS 系统维护的相当完备。当然这不仅仅在于 iOS 的集中性,也在于苹果对自己产品的营销手段及用户心理的预期控制Android 为何零乱Android 令人印象深刻的当属国内的 应用市场,国外的应用市场并没有国内这么泛滥,国外的应用市场要么是第三方的那种综合类下载站要么就是有能力做手机的厂商。手机厂商都想建立属于自己的闭环系统,但是实际上只有 Google 才能从根本上做到闭环,手机厂商本质上只是一层 ROM 或者 UI,就是上面提到的应用生态。事实上很多公司连自己的应用生态都做不好。Android 系统本质上讲是一个开源、自由的操作系统。这是它的基因,但是开源与自由有的时候会被人滥用,甚至利用。自由的基因决定了 Android 会提供给用户几乎所有的选择和路径。你喜欢的不喜欢的、想要的不想要的它统统都有,你不得不自己做出选择。但是当手机做为一种工具的时候,很多用户并不需要一个瑞士军刀,它们只想要一个简单好用的螺丝刀就够了。对于那些挑剔的特殊用户 Android 再适合不过了,你可以把 Android 看成一个微型计算机,你可以像玩树莓派一样去定制它,Web 服务器、智能家居、私有云等等,但这些都要建立在你有能力的基础上Android 零乱的原因还有一点就是用户的需求总是多样的,而 Andoird 本身的使命就是满足他们。比如就会有人看到 iPhone 的刘海觉得很别致,自己可能换不了 iPhone,然后就会理所当然的认为 Android 也应该有,因为这是 潮流既然提到了 iPhone,那也应该聊下 Google 的 Pixel。Pixel 最近刚出 3 系统,刘海居然是默认的设置,大家已经有很多吐槽了。自然不必多讲。事实上 Pixel 系列手机让我看到了很多的闪光点,Google 现在似乎也慢慢明白了自己做手机的方向,在 Pixel 3 的发布会上多次提到了 End-to-End 的摄像头的概念,它可以从一张照片里面分析出里面的建筑、人物,进而知道和人物相关的东西,比如它的年龄、性别、穿着。有了这些信息后他就可以做一些真正有用的推荐。如果你是 Google 的重度用户的话你会发现近两年 Google 的一些新的技术/软件正在成体系的发展,这是在苹果系统上看不到的。举个例子,iOS 的 Siri 和 Google 的 Assistant。一方面 Google Assistant 的智能程度好过 Siri 几条街,一方面你会觉得 Google Assistant 真的可以做到一些有用的事情而不只是一个玩具。比如 接到来电识别出来是骚扰电话,assistant 就可以帮你「礼貌」回绝了Google 做 Android 手机可以很好的解决 Android 的一些问题,但是由于 Google 本身是一家技术基因的公司,它所有的软件都会以功能为主。设计和用户体验不会是它的强项,这就使得 Google 做的手机用起来总是有一种不爽的感觉。比如就滚动这个操作,iOS 的交互处理的非常好,不仅仅界面操作流畅,而且响应快,该停的时候立即停下来。反观 Android,虽然流畅度上可能很多用户已经体会不出来与 iOS 之间的差异了,但是使用 Android 总让你感觉该顺滑的时候顺滑,不该顺滑的时候也很「顺滑」。还有 Pixel 3 上展示的和 iPhone 对比夜间成像的问题,Pixel 可以做到把夜晚照得和白天一样,大多数情况下是有好处的,但是假如用户就想拍张夜晚的照片那就比较尴尬了吧结语事实上很多时候大家讨论 iOS 与 Android 好坏只是在说其中某一点,因为这些点能打动用户。但是很诡异的是人们总是喜欢以贬低对方的不足来突出自己的优势,因为没有人甘于承认自己花钱买来的东西不如别人的博客原文 ...

October 23, 2018 · 1 min · jiezi

h5 与原生 app 交互的原理

h5 与原生 app 交互的原理现在移动端 web 应用,很多时候都需要与原生 app 进行交互、沟通(运行在 webview 中),比如微信的 jssdk,通过 window.wx 对象调用一些原生 app 的功能。所以,这次就来捋一捋 h5 与原生 app 交互的原理。h5 与原生 app 的交互,本质上说,就是两种调用:app 调用 h5 的代码h5 调用 app 的代码1. app 调用 h5 的代码因为 app 是宿主,可以直接访问 h5,所以这种调用比较简单,就是在 h5 中曝露一些全局对象(包括方法),然后在原生 app 中调用这些对象。javascriptwindow.sdk = { double = value => value * 2, triple = value => value * 3,};androidwebview.evaluateJavascript(‘window.sdk.double(10)’, new ValueCallback<String>() { @Override public void onReceiveValue(String s) { // 20 }});iosNSString *func = @“window.sdk.double(10)";NSString *str = [webview stringByEvaluatingJavaScriptFromString:func]; // 202. h5 调用 app 的代码因为 h5 不能直接访问宿主 app,所以这种调用就相对复杂一点。这种调用常用有两种方式:由 app 向 h5 注入一个全局 js 对象,然后在 h5 直接访问这个对象由 h5 发起一个自定义协议请求,app 拦截这个请求后,再由 app 调用 h5 中的回调函数2.1 由 app 向 h5 注入一个全局 js 对象这种方式沟通机制简单,比较好理解,并且对于 h5 来说,没有新的东西,所以是比较推荐的一种方式。但这种方式可能存在安全隐患,详细查看 你不知道的 Android WebView 使用漏洞。androidwebview.addJavascriptInterface(new Object() { @JavascriptInterface public int double(value) { return value * 2; } @JavascriptInterface public int triple(value) { return value * 3; }}, “appSdk”);iosNSString *scripts = @“window.appSdk = {double: value => value * 2, triple: value => value * 3}”;[webview stringByEvaluatingJavaScriptFromString:scripts];javascriptwindow.appSdk.double(10); // 202.2 由 h5 发起一个自定义协议请求这种方式要稍复杂一点,因为需要自定义协议,这对很多前端开发者来说是比较新的东西。所以一般不推荐这种方式,可以作为第一种方式的补充。大致需要以下几个步骤:由 app 自定义协议,比如 sdk://action?params在 h5 定义好回调函数,比如 window.bridge = {getDouble: value => {}, getTriple: value => {}}由 h5 发起一个自定义协议请求,比如 location.href = ‘sdk://double?value=10’app 拦截这个请求后,进行相应的操作,获取返回值由 app 调用 h5 中的回调函数,比如 window.bridge.getDouble(20);javascriptwindow.bridge = { getDouble: value => { // 20 }, getTriple: value => { // more }};location.href = ‘sdk://double?value=10’;androidwebview.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { // 判断如果 url 是 sdk:// 打头的就拦截掉 // 然后从 url sdk://action?params 中取出 action 与params Uri uri = Uri.parse(url); if ( uri.getScheme().equals(“sdk”)) { // 比如 action = double, params = value=10 webview.evaluateJavascript(‘window.bridge.getDouble(20)’); return true; } return super.shouldOverrideUrlLoading(view, url); }});ios- (BOOL)webview:(UIWebView *)webview shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { // 判断如果 url 是 sdk:// 打头的就拦截掉 // 然后从 url sdk://action?params 中取出 action 与params NSString *urlStr = request.URL.absoluteString; if ([urlStr hasPrefix:@“sdk://”]) { // 比如 action = double, params = value=10 NSString *func = @“window.bridge.getDouble(20)”; [webview stringByEvaluatingJavaScriptFromString:func]; return NO; } return YES;}后续更多博客,查看 https://github.com/senntyou/blogs作者:深予之 (@senntyou)版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证) ...

October 22, 2018 · 2 min · jiezi

小程序点赞收藏功能

一.收藏focusFavoriteTab: function (e) { var that = this; var isFocus = that.data.isFocus; console.log(“isFocus”, isFocus) var itemId = that.data.itemId; if (isFocus) { favoriteService.cancel(that, itemId).then((res) => { wx.showToast({ title: “取消收藏”, icon: ‘success’, duration: 1000 }); this.setData({ isFocus: false, }); }) } else { favoriteService.add(that, itemId).then((res) => { wx.showToast({ title: res.data == 1 ? “收藏成功” : “收藏失败”, icon: ‘success’, duration: 1000 }); this.setData({ isFocus: true, }); }); } },二.点赞focuslikeTab: function (e) { var that = this; var isLike = that.data.isLike; let itemDetail = this.data.itemDetail var itemId = that.data.itemId; if (isLike) { itemService.cancellike(that, itemId).then((res) => { wx.showToast({ title: “点赞取消”, icon: ‘success’, duration: 1000 }); itemDetail.liked–; this.setData({ itemDetail:itemDetail }); this.setData({ isLike: false, }); }) } else { itemService.addlike(that, itemId).then((res) => { wx.showToast({ title: res.data == 1 ? “点赞成功” : “点赞失败”, icon: ‘success’, duration: 1000 }); itemDetail.liked++; this.setData({ itemDetail: itemDetail }); this.setData({ isLike: true, }); }); } }, ...

September 7, 2018 · 1 min · jiezi

关于iOS 11.x微信连wifi流程中,在Portal页无法拉起微信问题的简单记录

标题挺长,踩过坑的应该看的明白。不过限于目前所做产品流程的限制,我并没有解决掉这个问题,只是简单说一下相应的思路。iOS的系统浏览器是Safari,用于Portal认证的则是CNA(Captive Network Assistant),二者的区别在于前者可以打开wachat:这种私有协议头网址,后者无法打开并且限制很多,比如无法使用alert()、无法正常使用window.open()(只能做跳转)等等。问题的症结在于在新版的CNA中是不认wechat:这样的私有协议头的,所以自然也就拉不起来微信。解决时需要引导用户点击a标签<a target="_system"></a>触发Safari,然后再在Safari拉起微信就行了。我目前的portal触发逻辑是,客户端连到wifi上回触发landing,首先返回码设定为401用于触发客户端的portal页面,同时判断客户端UA,如果是部分安卓或iOS就渲染landing实体页(landing.ejs),页面的title和body均为“Success”以作为iOS欺骗(并且会加快从连接到弹出portal的响应时间);js部分,ios是直接打开认证URL,针对部分安卓则是加了判断document.visibilityState == ‘visible’时触发跳转的事件,用来解决不弹portal的问题。但由于点击按钮之后就直接进到js拉微信认证的流程了(少一步引导拉起微信),所以其实需要部分变更产品流程才行(这个版本暂时没戏)。参考链接:iOS: Open a Welcome Page in Safari, not CNA微信连WI-Fi解决ios无法呼出微信

September 1, 2018 · 1 min · jiezi