Swift UIlabel 的高级用法,文本显示表情,图片

可以定义一个UIlabel的扩展代码如下class CPSPFMarkLabel: UILabel { convenience init(text: String) { self.init(frame:CGRect.zero) let markLabelText = " 清明时节雨纷纷☺" + text self.textAlignment = .center self.textColor = UIConstants.AppFontColor self.font = UIConstants.DefaultFont15 self.backgroundColor = UIConstants.AppWhiteColor let markAttribute = NSMutableAttributedString(string: markLabelText) markAttribute.addAttribute(NSAttributedStringKey.foregroundColor, value:UIConstants.AppRedColor, range: NSRange(location: 6,length: text.count)) //以上是富文本显示 let arkattch = NSTextAttachment() //定义一个attachment markattch.image = UIImage(named: “image_timing”)//初始化图片 markattch.bounds = CGRect(x: 0, y: -1, width: 15, height: 15) //初始化图片的 bounds let markattchStr = NSAttributedString(attachment: markattch) // 将attachment 加入富文本中 markAttribute.insert(markattchStr, at: 0)// 将markattchStr 加入原有文字的某个位置 self.attributedText = markAttribute }} ...

January 23, 2019 · 1 min · jiezi

用最新的 Alamofire(swift 4.1) (带参数)post方法上传图片到服务器

用最新的 Alamofire(swift 4.1) (带参数)上传图片到服务器1,准备参数let image = UIImage(named: “xxx”)//将图片转化为JPEG类型的data 后面的参数是压缩比例let jpegImage = UIImageJPEGRepresentation(image!, 0.5)//要传的参数(比如我们带用户的加密uid) let uid = [“uid” : user.uid] let ecodeUid = EncryptionHelper.getParamsString(uid as [String : AnyObject]) //将参赛转化为datalet ecodeData = ecodeUid.data(using: .utf8)2,开始上传//全部代码如下Alamofire.upload(multipartFormData: { (multipartFormData) in multipartFormData.append(ecodeData!, withName: “data”) multipartFormData.append(jpegImage!, withName: “avatar”, fileName: “avatar”+".jpeg", mimeType: “image/jpeg”) }, to: “https://www.ka5188.com/app/api/v1/user/uploadImg" , encodingCompletion: { encodingResult in ws!.hideLoading() switch encodingResult { case .success(let upload, _, _): upload.responseJSON { response in if let data = response.data { let responseJson = JSON(data: data) if responseJson[“status”].intValue == 1 { //上传成功,刷新当前头像 } } else { let msg = responseJson[“msg”].stringValue self.showMessage(msg) } } } case .failure(let encodingError): log.debug(encodingError) self.showMessage(“上传图片失败”) } })说明和注意点//需要注意的地方 multipartFormData.append(ecodeData!, withName: “data”)//第一个参数"ecodeData”,就是我们加密的二进制uid/第二个参数"data"服务端以这个字段名获取加密的uid(个人以为应该用"uid")multipartFormData.append(jpegImage!, withName: “avatar”, fileName: “avatar”+".jpeg", mimeType: “image/jpeg”)//带一个参数"jpegImage",就是我们加密的二进制图片流//带二个参数"withName",后台通过西字段来获取图片//带三个参数"fileName",后台检图片类型的,主要是后缀名//带四个参数"mimeType",传输的文件类型。注 : 1,开始由于第二个参数和第三个参数弄混了,导制上传失败 2,由于第三个参数没有加后缀".jpeg",导致后台输出(文件格式不支持) 3,第三个参数mimeType 传入类型是图片 下面给出官方解释,还有我们后台php部分代码官方解释如下后端代码 ...

January 22, 2019 · 1 min · jiezi

iOS个人中心渐变动画、微信对话框、标签选择器、自定义导航栏、短信验证输入框等源码

iOS精选源码简单的个人中心页面-自定义导航栏并予以渐变动画程序员取悦女票的正确姿势—Tip1(iOS美容篇)iOS 前台重启应用和清除角标的问题微信原生提醒对话框3.0JHLikeButton - 有趣的点赞动画,抖音点赞动画WKWebView交互LeeTagView2.0<一行代码集成标签选择器>CRBoxInputView-短信验证码输入框自定义导航栏和标签栏iOS优质博客iOS开发之线程间的MachPort通信与子线程中的Notification转发如题,今天的博客我们就来记录一下iOS开发中使用MachPort来实现线程间的通信,然后使用该知识点来转发子线程中所发出的Notification。简单的说,MachPort的工作方式其实是将NSMachPort的对象添加到一个线程所对应的RunLoop中,并给NSMachPort对象设置相应的代理。在其他线程中调用该… 阅读原文基于MQTT和ProtocolBuffer的推送系统(iOS)由于业务需求,需要实现实时获取服务端更新的数据功能,基于这个需求,进行调研及技术方案的实施,最终决定采用MQTT +ProtocolBuffer基于长连接的数据实时推送的方案;具体实现方案见本文;本文包括三个部分:1.技术选型 2.技术方案实践 3.未来优化方向技术选型在调研过程中,发现需求功能可以使用推送来… 阅读原文[[性能优化]DateFormatter深度优化探索](http://www.code4app.com/blog-…前言在iOS开发中,对日期进行格式化处理通常有三个步骤:创建DateFormatter对象设置日期格式使用DateFormatter对象对日期进行处理我们通过创建单例对象的方式对创建DateFormatter对象,设置日期格式两个步骤进行了缓存,将方法耗时降低为不缓存的方案的10%左右,但是这种优化方法受制于DateF… 阅读原文[[App探索]JSBox中幽灵触发器的实现原理探索](http://www.code4app.com/blog-…前言幽灵触发器是钟颖大神的JSBox中的一个功能,在app进程被杀死的情况下,也可以将通知固定在通知栏,即便用户点击清除,也能马上再弹出,永远不消失,除非用户关闭App的通知权限或者卸载App,才可以消失。这个功能确实比较有意思,而且钟颖大神在介绍视频里有提到是目前JSBox独有的,说明实现得非常巧妙,自己研究的话还是… 阅读原文iOS可视化动态绘制八种排序过程(Swift版)前面几篇博客都是关于排序的,在之前陆陆续续发布的博客中,我们先后介绍了冒泡排序、选择排序、插入排序、希尔排序、堆排序、归并排序以及快速排序。俗话说的好,做事儿要善始善终,本篇博客就算是对之前那几篇博客的总结了。而本篇博客的示例Demo也是在之前那些博客Demo的基础上做的,也算是集成了各种排序的方法,然后给出了可视化的… 阅读原文更多源码更多博文

January 16, 2019 · 1 min · jiezi

在iOS-Swift项目中集成CppJieba分词

背景在垃圾短信过滤应用 SMSFilters 中,需要使用 Jieba 分词库来対短信进行分词,然后使用 TF-IDF 来进行处理` 分词库是 C++ 写的,这就意味着需要在Swift中集成 C++ 库。在官方文档 “Using Swift with Cocoa and Objective-C” 中,Apple只是介绍了怎么将 Swift 代码跟 Objective-C 代码做整合,但是没有提C++,后来在官方文档中看到了这样一段话:You cannot import C++ code directly into Swift. Instead, create an Objective-C or C wrapper for C++ code.也就是不能直接导入 C++ 代码,但是可以使用 Objective-C 或者 C 对 C++ 进行封装。所以项目中使用 Objective-C 做封装,然后在 Swift 中调用,下面就是这个过程的实践,Demo 代码见 SwiftJiebaDemo。整合过程分成三步:引入C++文件;用 Objective-C 封装;在 Swift 中 调用 Objective-C;引入C++文件Demo中使用的是"结巴"中文分词的 C++ 版本 yanyiwu/cppjieba。将其中的 include/cppjieba 和依赖 limonp 合并,并加入 dict 中的 hmm_model 和 jiaba.dict 作为基础数据,并暴露 JiebaInit 和 JiebaCut 接口://// Segmentor.cpp// iosjieba//// Created by yanyiwu on 14/12/24.// Copyright (c) 2014年 yanyiwu. All rights reserved.//#include “Segmentor.h”#include <iostream>using namespace cppjieba;cppjieba::MixSegment * globalSegmentor;void JiebaInit(const string& dictPath, const string& hmmPath, const string& userDictPath){ if(globalSegmentor == NULL) { globalSegmentor = new MixSegment(dictPath, hmmPath, userDictPath); } cout << FILE << LINE << endl;}void JiebaCut(const string& sentence, vector<string>& words){ assert(globalSegmentor); globalSegmentor->Cut(sentence, words); cout << FILE << LINE << endl; cout << words << endl;}以及//// Segmentor.h// iosjieba//// Created by yanyiwu on 14/12/24.// Copyright (c) 2014年 yanyiwu. All rights reserved.//#ifndef iosjieba__Segmentor#define iosjieba__Segmentor#include <stdio.h>#include “cppjieba/MixSegment.hpp”#include <string>#include <vector>extern cppjieba::MixSegment * globalSegmentor;void JiebaInit(const std::string& dictPath, const std::string& hmmPath, const std::string& userDictPath);void JiebaCut(const std::string& sentence, std::vector<std::string>& words);#endif /* defined(iosjieba__Segmentor) */目录如下:$ tree iosjiebaiosjieba├── Segmentor.cpp├── Segmentor.h├── cppjieba│ ├── DictTrie.hpp│ ├── FullSegment.hpp│ ├── HMMModel.hpp│ ├── HMMSegment.hpp│ ├── Jieba.hpp│ ├── KeywordExtractor.hpp│ ├── MPSegment.hpp│ ├── MixSegment.hpp│ ├── PosTagger.hpp│ ├── PreFilter.hpp│ ├── QuerySegment.hpp│ ├── SegmentBase.hpp│ ├── SegmentTagged.hpp│ ├── TextRankExtractor.hpp│ ├── Trie.hpp│ ├── Unicode.hpp│ └── limonp│ ├── ArgvContext.hpp│ ├── BlockingQueue.hpp│ ├── BoundedBlockingQueue.hpp│ ├── BoundedQueue.hpp│ ├── Closure.hpp│ ├── Colors.hpp│ ├── Condition.hpp│ ├── Config.hpp│ ├── FileLock.hpp│ ├── ForcePublic.hpp│ ├── LocalVector.hpp│ ├── Logging.hpp│ ├── Md5.hpp│ ├── MutexLock.hpp│ ├── NonCopyable.hpp│ ├── StdExtension.hpp│ ├── StringUtil.hpp│ ├── Thread.hpp│ └── ThreadPool.hpp└── iosjieba.bundle └── dict ├── hmm_model.utf8 ├── jieba.dict.small.utf8 └── user.dict.utf8接下来开始在项目中集成。首先创建一个空项目 iOSJiebaDemo,将 iosjieba 加入项目中。单页应用SwiftJiebaDemo添加 SwiftJiebaDemo添加 iosjieba:见代码: https://github.com/qiwihui/Sw...C++ 到 Objective-C 封装这个过程是将 C++ 的接口进行 Objective-C 封装,向 Swift 暴露。这个封装只暴露了 objcJiebaInit 和 objcJiebaCut 两个接口。//// iosjiebaWrapper.h// SMSFilters//// Created by Qiwihui on 1/14/19.// Copyright © 2019 qiwihui. All rights reserved.//#import <Foundation/Foundation.h>@interface JiebaWrapper : NSObject- (void) objcJiebaInit: (NSString *) dictPath forPath: (NSString *) hmmPath forDictPath: (NSString *) userDictPath;- (void) objcJiebaCut: (NSString *) sentence toWords: (NSMutableArray *) words;@end//// iosjiebaWrapper.mm// iOSJiebaTest//// Created by Qiwihui on 1/14/19.// Copyright © 2019 Qiwihui. All rights reserved.//#import <Foundation/Foundation.h>#import “iosjiebaWrapper.h”#include “Segmentor.h”@implementation JiebaWrapper- (void) objcJiebaInit: (NSString *) dictPath forPath: (NSString *) hmmPath forDictPath: (NSString *) userDictPath { const char *cDictPath = [dictPath UTF8String]; const char *cHmmPath = [hmmPath UTF8String]; const char *cUserDictPath = [userDictPath UTF8String]; JiebaInit(cDictPath, cHmmPath, cUserDictPath); }- (void) objcJiebaCut: (NSString ) sentence toWords: (NSMutableArray ) words { const char cSentence = [sentence UTF8String]; std::vector<std::string> wordsList; for (int i = 0; i < [words count];i++) { wordsList.push_back(wordsList[i]); } JiebaCut(cSentence, wordsList); [words removeAllObjects]; std::for_each(wordsList.begin(), wordsList.end(), [&words](std::string str) { id nsstr = [NSString stringWithUTF8String:str.c_str()]; [words addObject:nsstr]; });}@end见代码: https://github.com/qiwihui/Sw...Objective-C 到 Swift在 Swift 中调用 Objecttive-C 的接口,这个在官方文档和许多博客中都有详细介绍。加入 {project_name}-Bridging-Header.h 头文件,即 SwiftJiebaDemo_Bridging_Header_h,引入之前封装的头文件,并在 Targets -> Build Settings -> Objective-C Bridging Header 中设置头文件路径 SwiftJiebaDemo/SwiftJiebaDemo_Bridging_Header_h。//// SwiftJiebaDemo-Bridging-Header.h// SwiftJiebaDemo//// Created by Qiwihui on 1/15/19.// Copyright © 2019 Qiwihui. All rights reserved.//#ifndef SwiftJiebaDemo_Bridging_Header_h#define SwiftJiebaDemo_Bridging_Header_h#import “iosjiebaWrapper.h”#endif / SwiftJiebaDemo_Bridging_Header_h */将使用到 C++ 的 Objective-C 文件修改为 Objective-C++ 文件,即 将 .m 改为 .mm: iosjiebaWrapper.m 改为 iosjiebaWrapper.mm。见代码:https://github.com/qiwihui/Sw…使用使用时需要先初始化 Jiaba分词,然后再进行分词。class Classifier { init() { let dictPath = Bundle.main.resourcePath!+"/iosjieba.bundle/dict/jieba.dict.small.utf8" let hmmPath = Bundle.main.resourcePath!+"/iosjieba.bundle/dict/hmm_model.utf8" let userDictPath = Bundle.main.resourcePath!+"/iosjieba.bundle/dict/user.dict.utf8" JiebaWrapper().objcJiebaInit(dictPath, forPath: hmmPath, forDictPath: userDictPath); } func tokenize(_ message:String) -> [String] { print(“tokenize…”) let words = NSMutableArray() JiebaWrapper().objcJiebaCut(message, toWords: words) return words as! [String] }}控制台输出结果:可以看到,测试用例 小明硕士毕业于中国科学院计算所,后在日本京都大学深造 经过分词后为〔拼音〕[“小明”, “硕士”, “毕业”, “于”, “中国科学院”, “计算所”, “,”, “后”, “在”, “日本”, “京都大学”, “深造”],完成集成。见代码: https://github.com/qiwihui/Sw…遇到的问题由于自己对于编译链接原理不了解,以及是 iOS 开发初学,因此上面的这个过程中遇到了很多问题,耗时两周才解决,故将遇到的一些问题记录于此,以便日后。“cassert” file not found将 .m 改为 .mm 即可。compiler not finding <tr1/unordered_map>设置 C++ Standard Library 为 LLVM libc++参考: mac c++ compiler not finding <tr1/unordered_map>warning: include path for stdlibc++ headers not found; pass ‘-std=libc++’ on the command line to use the libc++ standard library instead [-Wstdlibcxx-not-found]Build Setting -> C++ Standard Library -> libstdc++ 修改为 Build Setting -> C++ Standard Library -> libc++use of unresolved identifier这个问题在于向项目中加入文件时,Target Membership 设置不正确导致。需要将对于使用到的 Target 都勾上。相关参考: Understanding The “Use of Unresolved Identifier” Error In Xcode参考SwiftArchitect 对问题 “Can I have Swift, Objective-C, C and C++ files in the same Xcode project?” 的回答SwiftArchitect 对问题 “Can I mix Swift with C++? Like the Objective - C .mm files” 的回答在Swift代码中整合C++类库 ...

January 16, 2019 · 4 min · jiezi

中国上海,极客生涯: 一个开发者的 2018, 2019

总结 2018技术进步:做项目涨知识,没有什么比来个难一点的任务,更快了。为了解决他,又是翻书、又是 google , 有时候还发 stackOverFlow ,寻找各种可能的技术,各种场景,综合记忆理解。解决一个,对一类问题的解决套路印象,都挺深。工程师,本来就是解决问题的。做不出来,也别忘了他。说不定哪天,灵光一闪项目简单,怎么办?笔者 iOS ,用 Objective-C 需求容易实现,有时间做做代码优化,翻一下三方库源代码。譬如:AFN 的 AFURLSessionManager 文件中,有一个 _AFURLSessionTaskSwizzling 类。他在 + load 中做了两个方法交换。笔者的工程,用不到网络会话任务继续和网络会话任务挂起这两个通知,就把 _AFURLSessionTaskSwizzling 类的相关代码删了。做启动时间优化的时候,翻到这里有一个 + load 又譬如:SDW 的 HTTPS 挑战,笔者项目网络图片方面,不需要 ATS ,就去掉了SDWebImageDownloaderOperation类和SDWebImageDownloader类 NSURLSessionTaskDelegate 的 ATS 方法- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler 不见得删了好,处理掉更加贴近自己的项目。删了,可以让自己离底层近一点。(笔者独立开发,有这个条件)…..写博客,下笔有益写博客,写一下的好笔者写博客中,发现技术点,本来是很懵懂的,写着写着就通了一些。写一下,既然自己是真的这样思考的,写下思路。下次加载,就快了很多。写博客的好处,就是不太会自欺欺人。自己都骗不过去,蒙谁( 后来又发现,文如其人知识点,东一下,下一下的。完全看心情 )已经写了几篇,就把写给我自己看的完善和严谨化了一些,有一些赞,挺高兴的。感觉大体有两种写法:第一种是 cookbook.要做什么事情,( 需要长什么样的功能 ) ,怎么设计的,( 弄几个概念出来 ),怎么实现的,写到这里,差不多原理和基本的 demo 都清楚了。再处理一下异常情况和边界条件。套路就是,为什么这样做,怎么做,需要什么还有一种,感觉像是念经。有什么东西,这些是做什么的,东搞西搞,搞出来了。( 在学校上课,这种情况,略常见一些 )第一种,感觉比较友好,原理充沛一些,需要准备的时间比较长,对技术点把握的比较好。第二种,感觉仓促了一些。读起来滞涩。比如,有博客里面的代码,不太好 run 起来。看到了,也能感觉是这么个意思。我努力加大第一种的比例,减少第二种的看博客,开卷有益有公司的老法师说: “中文博客不要看,如果不能分辨好坏"很有道理,要学就学最强的。英文资料大法好,可能英文技术世界积累深厚,现在发展的相当成熟了 ( 不必要的事情,比较少 )中文技术博客,发展中,完善中,有小坑看了一些,感觉做好两点,即可首先是,如果一个老法师说什么技术方向不可能,他往往是错的。如果一个老法师说什么技术方向是可能的,他往往是对的。技术途径是丰富多彩的,编程语言本来就是人设计出来解决问题。一般方法是很多的,取舍的余地比较大。其次是,有博客中,有一种情绪。( 常见的是 BS 新手,新手一般脾气高于技术。线下操作,有一定的危险性。本文建议尊重每一个人 )取出我们关心的技术,就好了。老法师的世界,笔者不怎么懂StackOverFlow国内的,还有 segmentFault.借别人的需求,磨练自己的技术。回答出来一些别人的问题,对方采纳。笔者没能总结出来,什么规律。感觉当时,有状态,有灵感这几天,发现别人的需求,NaN职业发展每一份工作,一般都有困境期和舒适期。困境期,各种展现价值,图表现,求生存。不断的拼命完成任务舒适期,就是温水区,稳定下来了,升职又看不到光。好一点的,会把舒适期转变为积累期,给自己持续的刺激,成长。感觉进入了套路的轮回,温水中折腾,看到了社会上的能力似乎有成百成千种笔者惨淡经营中,加薪靠跳槽展望 2019=缺什么,就补什么。想要什么,努力呗技术和知识,不能用来改造自己,改变自己的生活,技术世界,似乎没有了十二分的精彩途径: 自求,才能多福希望技术上,有大的突破,写出来,分享出来,水到渠成的事技术聚会笔者参加技术聚会挺多的,上海地区挺上档次的有,谷歌开发者日感觉其宣讲的技术,有点着力于通俗易懂,传播福祉。参加这个好,跟电影里面的一样,坚定了技术信念。类似的技术主题聚会,有嘉宾主讲,去开拓下眼界,好笔者,听了就忘。惭愧惭愧听多了,感觉冷静了很多。人外,人真多推荐: 饿科技、携程技术(微信公众)技术闲聊,有 Linux , 参加了一下,好像不够大胆和进取感觉反反复复那几句,很安全另一半现状,我妈强烈建议,我减肥,以免孤独一生愿景: 情投意合,心心相印有什么比两颗心,在一起,还要好。女性本来就温柔、善良,唤醒她物质,感觉似乎好像,不是很重要。颜值,不需要。聪明,不需要。正直,还有人喜欢,厉害了真实,还不丑陋,强认识大佬,感觉不怎么需要大佬也不容易啊, ( 有写博客,求啊球 )人家也是很努力,得来的。大佬盛名之下,上面的那个老法师说: “没那么难"羡慕人家吃鸡,不如自己努力有幸认识了几位大佬,感觉生活中,普通人。刚开始从众,后来自己的生活,都搞不定。人家的私生活,兴趣无群里看看消息,笔者就满足了找大佬请教?大佬博客写得好,先看十遍关于标题:上海地区的极客,就像江水里面的鲫鱼一样,数不胜数希望多我一个本文描述下: 一个普通的 iOS 开发者看到的,以及相关理解偏见多,发小号写了很多,阵痛的困惑 ...

January 15, 2019 · 1 min · jiezi

O3(OzoneWalletIOS)项目

The main repo for the O3 wallet on iOS.OzoneWalletIOS:https://github.com/CityOfZion…neo-swift:https://github.com/CityOfZion…neo-wallet-address-go:https://github.com/apisit/neo…下载 OzoneWalletIOS 项目下载 OzoneWalletIOS 项目到本地:下载 OzoneWalletIOS 项目用 Xcode 打开项目并运行:运行出错缺少文件运行出错,发现缺少文件。打开项目目录,发现有这两个文件 Cartfile、Cartfile.resolved:Cartfile 文件该项目是用 Carthage 管理 iOS 依赖库的,安装使用方法看这里,如果安装不成功请下载 Releases 版本进行安装。打开终端,cd 到该项目目录,运行如下命令:$ carthage update –platform iOS$ carthage update –platform iOS 命令缺少的文件已下载完成命令运行成功之后,再次运行项目:info.plist 文件错误发现 info.plist 文件读取错误,无法打开。我的解决方法是,新建一个 Project,然后把新工程里的 info.plist 文件拷贝到该项目里替换掉,然后再次运行:Reason image not found 错误在 Finder 里前往文件夹 ~/Library/Developer/Xcode/DerivedData ,删除缓存目录下的文件 :删除缓存目录下的文件再次运行,发现还是同样的错误,后来发现新加的一个库文件 AutoInsetter 没有加进去:*** Building scheme “AutoInsetter” in AutoInsetter.xcworkspace添加库文件 1添加库文件 2添加好后再次运行:“networkKey” 出错这个错误不知道怎么分析,全局搜索了一下“ networkKey”,发现是跟用户数据有关,后来整体看了一下项目,然后把项目的 Main Interface 改了一下:修改 Main Interface再一次运行:Info.plist 文件缺少 Fabric 字段 1Info.plist 文件缺少 Fabric 字段 2开启 Background Modes:开启 Background Modes向 Info.plist 文件添加 Fabric 字段,包含一个 String 类型的 APIKey 和一个 Array 类型的 Kits:QQ20180131-144207@2x.png好了,再次运行:模拟器运行界面终于运行成功了!!!/(ㄒoㄒ)/~~AppDelegate.swiftChannel.framework 大概是跟推送通知有关的库:func setupChannel() { //O3 Development on Channel app_gUHDmimXT8oXRSpJvCxrz5DZvUisko_mliB61uda9iY Channel.setup(withApplicationId: “app_gUHDmimXT8oXRSpJvCxrz5DZvUisko_mliB61uda9iY”) }设置应用的外观样式:func setupApperances() { UIBarButtonItem.appearance().setTitleTextAttributes([ NSAttributedStringKey.font: ThemeManager.barButtonItemFont, NSAttributedStringKey.foregroundColor: UserDefaultsManager.theme.primaryColor], for: .normal) UINavigationBar.appearance().largeTitleTextAttributes = [ NSAttributedStringKey.foregroundColor: UserDefaultsManager.theme.textColor, NSAttributedStringKey.font: UIFont(name: “Avenir-Heavy”, size: 32) as Any] }创建用户的默认配置:func registerDefaults() { let userDefaultsDefaults: [String: Any] = [ “networkKey”: “main”, “usedDefaultSeedKey”: false, “selectedThemeKey”: Theme.light.rawValue ] UserDefaults.standard.register(defaults: userDefaultsDefaults) }开启网络状态监测:let alertController = UIAlertController(title: “Uh oh! There is no internet connection. ????”, message: nil, preferredStyle: .alert) @objc func reachabilityChanged(_ note: Notification) { switch reachability.connection { case .wifi: print(“Reachable via WiFi”) alertController.dismiss(animated: true, completion: nil) case .cellular: print(“Reachable via cellular”) alertController.dismiss(animated: true, completion: nil) case .none: print(“Network not reachable”) UIApplication.shared.keyWindow?.rootViewController?.presentFromEmbedded(alertController, animated: true, completion: nil) } } let reachability = Reachability()! func setupReachability() { NotificationCenter.default.addObserver(self, selector: #selector(reachabilityChanged(:)), name: .reachabilityChanged, object: nil) do { try reachability.startNotifier() } catch { print(“could not start reachability notifier”) } }检查本地是否存在钱包,如果存在就将主窗口设置为登录到本地钱包的界面:func application( application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { Fabric.with([Crashlytics.self]) self.registerDefaults() self.setupChannel() self.setupApperances() self.setupReachability() //check if there is an existing wallet in keychain //if so, present LoginToCurrentWalletViewController let walletExists = UserDefaultsManager.o3WalletAddress != nil if walletExists { let login = UIStoryboard(name: “Onboarding”, bundle: nil).instantiateViewController(withIdentifier: “LoginToCurrentWalletViewController”) if let window = self.window { window.rootViewController = login } } return true }Onboarding.storyboardOnboardingHomeViewController.swift总结欢迎留言讨论,有错误请指出,谢谢!【联系我(QQ:3500229193)或者加入社群,请戳这里!】参考链接OzoneWalletIOS:https://github.com/CityOfZion…neo-swift:https://github.com/CityOfZion…neo-wallet-address-go:https://github.com/apisit/neo…更新日志2018.02.02 第一次更新2018.02.23 第二次更新2018.08.07 第三次更新作者:AlleniCoder链接:https://www.jianshu.com/p/85a…來源:简书简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。 ...

January 9, 2019 · 2 min · jiezi

iOS筛选菜单、分段选择器、导航栏、悬浮窗、转场动画、启动视频等源码

iOS精选源码APP启动视频自定义按钮,图片可调整图文间距SPButton一款定制性极高的轮播图,可自定义轮播图Item的样式(或只…iOS 筛选菜单分段选择器仿微信导航栏的实现,让你的导航栏过渡平滑,赏心悦目。iOS仿微信的悬浮窗,自定义转场动画,使用超级简单<br/>iOS优质博客2018 我的技术进阶之路窗外雨夹着雪,2018 的冬天似乎格外寒冷。国内经济下行,资金链断裂、人员优化、冻结 HC 等消息不绝于耳,2018 的冬天被一层阴霾笼罩,互联网从业人员一度人心惶惶。大潮退去,才知道谁在裸泳,这是说企业,也是说个人。毫无疑问的是,互联网从业人员的竞争更加激烈了,作为一名普通的开发者,在没有更好的机遇时,首先应该想的是… 阅读原文iOS开发之UIRefreshControl使用踩坑问题描述接上一个话题,实现了TabBar的点击刷新以后,开始继续写完成功能,刷新UITableView,于是考虑到iOS 10以后,UIScrollView已经有UIRefreshControl的属性了,干脆用自带的写。于是就有了如下的代码:添加UIRefreshControl到UITableView上去UIRefre… 阅读原文iOS开发-WKWebView与JS的交互iOS8以后,Apple公司退出了WKWebView,对比之前的UIWebView不论是处理速度还是内存性能,都有了大幅度的提升!那么下面我就分享一下WKWebView与JS的交互.首先使用WKWebView.你需要导入WebKit #import然后初始化一个WKWebView,设置代理,并且执行代理的方法.在网页加… 阅读原文iOS内存管理的那些事儿-原理及实现为什么要写这篇文章最近在做内存优化相关的问题,趁着这个机会把内存相关知识捋一捋。虽然现在语言设计的趋势之一就是让程序员不在关心内存管理这件事。但是作为一名程序开发,如果因为语言这个特性,而忽略这方面的知识的话,那是非常不可取的,不懂这方面知识,遇到问题会让我们知其然还不知其所以然。因为内存设计的知识比较多,因此我把他做… 阅读原文<br/>更多源码更多博文<br/>

January 9, 2019 · 1 min · jiezi

Git 使用总结-常用命令 (一)

Git 常用命令//初始化代码仓库$ git init//添加当前文件夹下所有文件 $ git add .//提交代码$ git commit -m ‘修改信息’// 将远程代码拷贝到本地$ git clone [远程库地址]// 关联远程仓库$ git remote add origin [远程库地址]// 查看本地分支$ git branch// 分支创建并切换到该分支$ git checkout -b [分支名] 实例 :git checkout -b Dev // 分支创建并切换到该分支 同步本地分支到远程分支 ,确保远程分支有该分支名$ git checkout -b origin/[远程分支名]// 删除 本地分支 注意不要在该分支上操作,切换到其他分支操作$ git branch -D [本地分支名]// 将本地分支删除后 同步git push –delete origin [本地分支名]//提交记录查看$ git log//会退到某个版本号 版本号可以通过’版本记录’ 查看 $ git reset –hard [版本号名称]/组件化中Git常用*****/// 组件快速创建$ pod lib create [组件名]// 本地验证 $ pod lib lint // 本地验证 忽略警告$ pod lib lint –allow-warnings// 远程验证$ pod spec lint// 远程验证 忽略警告$ pod spec lint –allow-warnings// 查看本地索引库$ pod repo// 向本地端提交索引库 也就是把.spec 提交到本地创建的索引库中$ pod repo push [本地索引库名称] xxxx.spec// 忽略警告$ pod repo push [本地索引库名称] xxxx.spec –allow-warnings// 移除本地索引库$ pod repo remove [本地索引库名称]// 移除cocopod 索引文件$ rm ~/Library/Caches/CocoaPods/search_index.json// 打tag$ git tag -a ‘版本号’ -m ‘描述信息’// 推送taggit push –tags ...

January 9, 2019 · 1 min · jiezi

浅谈SLAM的回环检测技术

什么是回环检测?在讲解回环检测前,我们先来了解下回环的概念。在视觉SLAM问题中,位姿的估计往往是一个递推的过程,即由上一帧位姿解算当前帧位姿,因此其中的误差便这样一帧一帧的传递下去,也就是我们所说的累计误差。我们的位姿约束都是与上一帧建立的,第五帧的位姿误差中便已经积累了前面四个约束中的误差。但如果我们发现第五帧位姿不一定要由第四帧推出来,还可以由第二帧推算出来,显然这样计算误差会小很多,因为只存在两个约束的误差了。像这样与之前的某一帧建立位姿约束关系就叫做回环。回环通过减少约束数,起到了减小累计误差的作用。那么我们怎么知道可以由第二帧推算第五帧位姿呢?也许第一帧、第三帧也可以呢。确实,我们之所以用前一帧递推下一帧位姿,因为这两帧足够近,肯定可以建立两帧的约束,但是距离较远的两帧就不一定可以建立这样的约束关系了。找出可以建立这种位姿约束的历史帧,就是回环检测。回环检测的意义有了前端的视觉里程计及后端优化的SLAM系统,似乎已经比较好用了。但其在提高实时性的同时精度却降低了,一旦精度降低,又会面临长时间累计误差的问题,特别是像ORB-SLAM那样只做局部地图优化的方案。我们该如何平衡这个矛盾呢?我们不妨先思考下,在一个陌生的环境中,我们人类是如何进行环境地图的建立?在局部区域,人不断的移动从而在脑海中建造增量式地图,时间长了大部分人也分不清东南西北了,与起始点的关系又如何。假如人正巧回到了之前路过的位置,在对环境足够敏感的情况下,他就能发现这个事实,从而修正自己之前对方位的判断。我们说,此时检测到了一个回环,显然,人可以通过面前看到的景象与脑海中残缺的印象来对比从而检测到回环的,对于SLAM来说也可以通过对比当前帧与过去关键帧的相似度,如相似度超过某一阀值时就可以被认为是检测到回环。现在,问题的关键就在于如何判断两帧图片的相似度。最直观的做法是特征匹配,比较匹配的数量是否足够多。但由于特征匹配非常耗时,回环检测需要与过去所有关键帧匹配,这个运算量是绝对无法承受的。因此,有人提出了词袋模型,用来加速特征匹配。什么是词袋模型?词袋模型就是把特征看成是一个个单词,通过比较两张图片中单词的一致性,来判断两张图片是否属于同一场景。为了能够把特征归类为单词,我们需要训练一个字典。所谓的字典就是包含了所有可能的单词的集合,为了提高通用性,需要使用海量的数据训练。字典的训练其实是一个聚类的过程。假设所有图片中共提取了10,000,000个特征,可以使用K-means方法把它们聚成100,000个单词。但是,如果只是用这100,000个单词来匹配的话效率还是太低,因为每个特征需要比较100,000次才能找到自己对应的单词。为了提高效率,字典在训练的过程中构建了一个k个分支,深度为d的树,如下图所示。直观上看,上层结点提供了粗分类,下层结点提供了细分类,直到叶子结点。利用这个树,就可以将时间复杂度降低到对数级别,大大加速了特征匹配。使用DBoW3库训练及使用词典DBoW3库为我们提供了非常方便的训练词典和使用词典的方法。训练词典时,只需要把所有训练用的图片的描述符传给DBoW3::Vocabulary的create方法就可以了。训练好的词袋模型保存在vocabulary.yml.gz文件中。接下来,使用训练好的词袋模型对图片计算相似性评分。DBoW3为我们提供了两种计算相似性的方式,第一种是直接对两张图片比较;第二种是把图片集构造成一个数据库,再与另一张图片比较。005QQQghzy7oCR8cSOZ31&690可以看出,图片越相似,评分越接近1。我们可以根据这个评分来判断两张图片是否是同一场景。但是直接给定一个绝对的阈值并不合适。通常,如果当前帧与之前某帧的相似度超过当前帧与上一个关键帧相似度的3倍,就认为可能存在回环。不过,这种做法要求关键帧之间的相似性不能太高,否则无法检测出回环。

January 8, 2019 · 1 min · jiezi

[App探索]JSBox中幽灵触发器的实现原理探索

前言幽灵触发器是钟颖大神的JSBox中的一个功能,在app进程被杀死的情况下,也可以将通知固定在通知栏,即便用户点击清除,也能马上再弹出,永远不消失,除非用户关闭App的通知权限或者卸载App,才可以消失。这个功能确实比较有意思,而且钟颖大神在介绍视频里有提到是目前JSBox独有的,说明实现得非常巧妙,自己研究的话还是很难想到的,非常值得学习,而且当你了解它的实现原理的话,会发现其实可以做很多其他的事情。当某天产品经理对App推送点击率不满意时,可以向她祭出这件大杀器(哈哈,开玩笑的,无线推送这种功能其实苹果很不推荐,因为确实有可能会被一些不良App采用,然后无限推送,让用户反感)。以下内容仅供学习讨论,JSBox是一个很强大的App,有很多值得学习的地方,强烈推荐大家去购买使用。简短的效果视频完整的介绍视频https://weibo.com/tv/v/G79vjv…:1f37179499e39dbc8a7472897b9e056c从2分6秒开始探索历程因为没有可以用来砸壳的越狱手机,而且PP助手也没有JSBox的包,一开始是去搜幽灵触发器,无限通知的实现,发现没找到答案,stackoverflow上的开发者倒是对无限通知比较感兴趣,问答比较多,但是没有人给出答案,基本上也是说因为苹果不希望开发者用这种功能去骚扰用户。所以只能自己阅读通知文档,查资料来尝试实现了。难道是使用时间间隔触发器UNTimeIntervalNotificationTrigger来实现的吗?因为看通知清除了还是一个接一个得出现,很自然就能想到是通过绕过苹果的检测,去改UNTimeIntervalNotificationTrigger的timeInterval属性来实现的,所以写出了一下代码:UNTimeIntervalNotificationTrigger timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1.0f repeats:YES];UNMutableNotificationContent content = [[UNMutableNotificationContent alloc] init];content.title = @“推送标题”;UNNotificationRequest request = [UNNotificationRequest requestWithIdentifier:@“requestIdentifier” content:content trigger:timeTrigger];[center addNotificationRequest:request withCompletionHandler:nil];通过传入创建时间间隔为1s的实际间隔触发器来实现,运行后,第一个通知能正常显示出来,清除第一个通知后,显示第二个通知时,app崩溃了,时间间隔不能小于60s。UserNotificationsDemo[14895:860379] *** Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ’time interval must be at least 60 if repeating’ First throw call stack:(0x1ae2a3ea0 0x1ad475a40 0x1ae1b9c1c 0x1aeca7140 0x1b8738d0c 0x1b8738bdc 0x102d508ac 0x1db487658 0x1dad09a18 0x1dad09720 0x1dad0e8e0 0x1dad0f840 0x1dad0e798 0x1dad13684 0x1db057090 0x1b0cd96e4 0x1030ccdc8 0x1030d0a10 0x1b0d17a9c 0x1b0d17728 0x1b0d17d44 0x1ae2341cc 0x1ae23414c 0x1ae233a30 0x1ae22e8fc 0x1ae22e1cc 0x1b04a5584 0x1db471054 0x102d517f0 0x1adceebb4)libc++abi.dylib: terminating with uncaught exception of type NSExceptiontimeInterval是只读属性,看来苹果早有防范@property (NS_NONATOMIC_IOSONLY, readonly) NSTimeInterval timeInterval;但是这年头,还能活着做iOS开发的谁没还不会用KVC呀,所以很自然得就能想到使用KVC来改UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1.0f repeats:YES];UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];content.title = @“推送标题”;UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@“requestIdentifier” content:content trigger:timeTrigger];[timeTrigger setValue:@1 forKey:@“timeInterval”];[center addNotificationRequest:request withCompletionHandler:nil];而且我打断点看,确实改成功了,但是,很快,当我把第一个通知清除时,手机变成这样了有那么一刻,我心里很慌,我一定好好做人,不去改苹果爸爸的只读属性了。苹果是在显示第二个通知的时候才去判断的,而我们的代码只能控制到将通知请求request添加到UNUserNotificationCenter这一步,所以不太好绕过。难道是使用地点触发器UNLocationNotificationTrigger来实现的吗?UNLocationNotificationTrigger可以通过判断用户进入某一区域,离开某一区域时触发通知,但是我去看了一下设置里面的权限,发现只使用这个功能的时候JSBox并没有请求定位的权限,所以应该不是根据地点触发的。继续阅读文档然后我就去钟颖大神的JSBox社区仔细查看开发者文档,查看关于通知触发相关的api,结果发现不是通过repeats字段,而是通过renew这个字段来决定是否需要重复创建通知的,所以很有可能不是通过时间触发器来实现的,是通过自己写代码去创建一个通知,然后将通知进行发送。在大部分iOS开发同学心中(包括我之前也是这么认为的),普遍都认为当app处于运行状态时,这样的实现方案自然没有问题,因为我们可以获取到通知展示,用户对通知操作的回调。当app处于未运行状态时,除非用户点击通知唤醒app,我们无法获取到操作的回调,但其实在iOS 10以后,苹果公开的UserNotifications框架,允许开发者通过实现UNUserNotificationCenter的代理方法,来处理用户对通知的各种点击操作。具体可以看苹果的这篇文章Handling Notifications and Notification-Related Actions,翻译其中主要的一段:你可以通过实现UNUserNotificationCenter的代理方法,来处理用户对通知的各种点击操作。当用户对通知进行某种操作时,系统会在后台启动你的app并且调用UNUserNotificationCenter的代理对象实现的userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:方法,参数response中会包含用户进行的操作的actionIdentifier,即便是系统定义的通知操作也是一样,当用户对通知点击取消或者点击打开唤醒App,系统也会上报这些操作。核心就是这个方法// The method will be called on the delegate when the user responded to the notification by opening the application, dismissing the notification or choosing a UNNotificationAction. The delegate must be set before the application returns from application:didFinishLaunchingWithOptions:.- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __OSX_AVAILABLE(10.14) __TVOS_PROHIBITED;所以我就写了一个demo来实现这个功能,核心代码如下:AppDelegate.m- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0]; [self applyPushNotificationAuthorization:application];//请求发送通知授权 [self addNotificationAction];//添加自定义通知操作扩展 return YES;}//请求发送通知授权- (void)applyPushNotificationAuthorization:(UIApplication *)application{ if (([[[UIDevice currentDevice] systemVersion] floatValue] >= 10.0)) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; center.delegate = self; [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) { if (!error && granted) { NSLog(@“注册成功”); }else{ NSLog(@“注册失败”); } }]; [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { NSLog(@“settings========%@",settings); }]; } else if (([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)){ [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound ) categories:nil]]; } [application registerForRemoteNotifications];}//添加自定义通知操作扩展- (void)addNotificationAction { UNNotificationAction *openAction = [UNNotificationAction actionWithIdentifier:@“NotificationForeverCategory.action.look” title:@“打开App” options:UNNotificationActionOptionForeground]; UNNotificationAction *cancelAction = [UNNotificationAction actionWithIdentifier:@“NotificationForeverCategory.action.cancel” title:@“取消” options:UNNotificationActionOptionDestructive]; UNNotificationCategory *notificationCategory = [UNNotificationCategory categoryWithIdentifier:@“NotificationForeverCategory” actions:@[openAction, cancelAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction]; [[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:[NSSet setWithObject:notificationCategory]];}# pragma mark UNUserNotificationCenterDelegate//app处于前台时,通知即将展示时的回调方法,不实现会导致通知显示不了- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler{ completionHandler(UNNotificationPresentationOptionBadge| UNNotificationPresentationOptionSound| UNNotificationPresentationOptionAlert);}//app处于后台或者未运行状态时,用户点击操作的回调- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler { [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0]; if ([response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) {//点击系统的清除按钮 UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.0001f repeats:NO]; UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = @“App探索-NotFound”; content.body = @"[App探索]JSBox中幽灵触发器的实现原理探索”; content.badge = @1; content.categoryIdentifier = @“NotificationForeverCategory”; UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:response.notification.request.identifier content:content trigger:timeTrigger]; [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:nil]; } completionHandler();}- (void)applicationWillResignActive:(UIApplication *)application { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.}- (void)applicationDidEnterBackground:(UIApplication *)application { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.}- (void)applicationWillEnterForeground:(UIApplication *)application { // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.}- (void)applicationDidBecomeActive:(UIApplication *)application { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.}- (void)applicationWillTerminate:(UIApplication *)application { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.}ViewController.m- (void)viewDidLoad { [super viewDidLoad]; UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; [button addTarget:self action:@selector(sendNotification) forControlEvents:UIControlEventTouchUpInside]; [button setTitle:@“发送一个3s后显示的通知” forState:UIControlStateNormal]; button.frame = CGRectMake(0, 200, [UIScreen mainScreen].bounds.size.width, 100); [self.view addSubview:button];}//发送一个通知- (void)sendNotification { UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:3.0f repeats:NO]; UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = @“App探索-NotFound”; content.body = @"[App探索]JSBox中幽灵触发器的实现原理探索"; content.badge = @1; content.categoryIdentifier = @“NotificationForeverCategory”; UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@“requestIdentifier” content:content trigger:timeTrigger]; UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center addNotificationRequest:request withCompletionHandler: nil];}必须在didFinishLaunchingWithOptions的方法返回前设置通知中心的代理,这个文档里面都有提及,大家都知道,但是有两个文档里面未曾提及的难点需要注意:隐藏关卡一 必须给通知添加自定义的通知操作1.必须给通知添加自定义的通知操作,并且给发送的通知指定自定义的通知操作的categoryIdentifier,这样系统在用户对通知进行操作时才会调用这个代理方法,- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler自定义通知操作是用户长按通知,下方弹出的actionSheet,在我们的Demo中,是“打开App”和“取消”两个操作,其实不添加这些自定义操作的话,系统的这些“管理”,”“查看”,“清除”也是有的,但是当用户点击“清除”时,我们的代理方法didReceiveNotificationResponse就不会被调用了,文档里面没有提及这个,我也是试了好久才试出来的。隐藏关卡二 必须使用上一个通知的requestIdentifier当用户点击“清除”按钮时,即便app处于未运行状态,系统也会在后台运行我们的app,并且执行didReceiveNotificationResponse这个代理方法,在这个方法里面我们会创建一个UNNotificationRequest,把他添加到通知中心去,然后通知会展示出来。但是系统好像对于在app正常运行时添加的UNNotificationRequest跟在didReceiveNotificationResponse方法里添加的UNNotificationRequest做了区分,后者在被用户点击“清除”按钮后,app不会收到didReceiveNotificationResponse回调方法,可能系统也是考虑到开发者可能会利用这个机制去实现无限通知的功能。所以我在创建UNNotificationRequest时,使用的identifier是前一个通知的identifier,这也是实现无限通知的最巧妙的地方,可能很多开发者是知道实现这个代理方法来接受用户点击“清除”的回调,然后做一些通知上报,隔一段时间再次发送通知事情,但是再次创建并发送的通知在被点击“清除”时已经不会再执行didReceiveNotificationResponse回调了。 UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:response.notification.request.identifier content:content trigger:timeTrigger];扩展如果我们做的是效率工具类型的App,利用这个功能做一些固定通知之类的功能,如果我们做的是一些资讯类的App,可以做一些不定间隔推送的功能,而不需要每次用户点击“清除”后,将用户操作通过网络请求上报给服务器,然后服务器根据情况给用户发推送。更多的玩法有待我们探索。Demo https://github.com/577528249/...Demo 演示Gif写文章太耗费时间了,可以的话,求大家给我点个关注吧,会定期写原创文章,谢谢了! ...

January 2, 2019 · 3 min · jiezi

通过视图控制器容器和子视图控制器避免庞大的视图控制器

首发于:【译】通过视图控制器容器和子视图控制器避免庞大的视图控制器通过视图控制器容器和子视图控制器避免庞大的视图控制器视图控制器容器和子视图控制器图解View Controller 是一个提供基本构建块的组件,在 iOS 开发中我们以它为基础构建应用。在 Apple MVC 世界中,它作为 View 和 Model 的中间人,在两者之间充当协调者的角色。它以观察者控制器开始,响应模型更改、更新视图、使用目标操作从视图中接受用户交互、然后更新模型。Apple MVC 图解(Apple 公司提供)作为一名 iOS 开发者,很多次我们将面临处理庞大的 View Controller 问题,即便我们使用了像 MVVM、MVP 或 VIPER 这样的架构。某些时刻,View Controller 在一个屏幕上承担了太多职责。这违反了 SRP(单一职责原则),在模块之间形成了强度耦合,并使得重用和测试每个组件变得异常困难。我们可以将下面的应用截图作为示例。你可以看到在一个屏幕上至少存在 3 种职责:显示电影列表;显示可以选择应用于电影列表的过滤列表;清除所选过滤器的选项。如果我们准备使用单一的 View Controller 来构建此屏幕,由于它在一个 view controller 中承担了过多职责,因此可以保证这个 view controller 将变得非常庞大和臃肿。我们如何解决这个问题呢?其中一个解决方案是使用 View Controller 容器和子 View Controller。以下是使用该方案的好处:将电影列表封装到 MovieListViewController 中,它只负责显示电影列表并对 Movie 模型中的更改做出响应。如果我们只想显示没有过滤器的电影列表,我们也可以在另一个屏幕中重用它。将过滤器中的列表和选择逻辑封装到 FilterListViewController 中,它单独负责显示和过滤器的选择。当用户选择和取消选择时,我们可以使用委托与父 View Controller 进行通信。将主 View Controller 缩减为一个 ContainerViewController,它只负责将选中的过滤器从过滤列表应用到 MovieListViewController 中的 Movie 模型。它还设置布局并将子 view controller 添加到容器视图中。你可以在下面的 GitHub 代码仓库中查看完整的项目源代码。alfianlosari/Filter-MVC-iOS使用 Storyboard 来布置 View Controller使用 Storyboard 来布置 View ControllerContainerViewController:View Controller 容器提供了 2 个容器视图,用于将子 View Controller 嵌入到水平 UIStackView 中。它还提供了单个 UIButton 来清空所选的过滤器。它还嵌入在充当初始 View Controller 的 UINavigationController 中。FilterListMovieController:它是 UITableViewController 的子类,具有分类样式和一个用来显示过滤器名称的标准单元格。它还分配了 Storyboard ID,因此可以通过编程的方式在 ContainerViewController 中对它进行实例化。MovieListViewController:它是 UITableViewController 的子类,具有 Plain 样式和一个用来显示 Movie 属性的小标题单元格。它还跟 FilterListViewController 一样分配了 Storyboard ID。电影列表 View Controller此 view controller 负责显示作为实例公开属性的 Movie 模型列表。我们使用 Swift 的 didSet 属性观察器来响应模型的更改,然后重新加载 UITableView。单元格使用默认小标题样式 UITableViewCellStyle 来显示电影的标题、持续时间、评级和流派。import UIKitstruct Movie { let title: String let genre: String let duration: TimeInterval let rating: Float}class MovieListViewController: UITableViewController { var movies = Movie { didSet { tableView.reloadData() } } let formatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() formatter.allowedUnits = [.hour, .minute] formatter.unitsStyle = .abbreviated formatter.maximumUnitCount = 1 return formatter }() override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return movies.count } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: “Cell”, for: indexPath) let movie = movies[indexPath.row] cell.textLabel?.text = movie.title cell.detailTextLabel?.text = “(formatter.string(from: movie.duration) ?? “”), (movie.genre.capitalized), rating: (movie.rating)” return cell }}过滤器列表 View Controller过滤器列表在 3 个单独的部分中显示 MovieFilter 枚举:流派、评级和持续时间。MovieFilter 枚举本身符合 Hashable 协议,因此可以使用每个枚举及其属性的哈希值存储在唯一集合中。过滤器的选择存储在包含 MovieFilter 的 Set 的实例属性下。要与其他对象通信,通过 FilterListControllerDelegate 使用委托模式,委托有三个方法需要实现:选择一个过滤器。取消选择一个过滤器。清空所有已选择过滤器。import UIKitenum MovieFilter: Hashable { case genre(code: String, name: String) case duration(duration: TimeInterval, name: String) case rating(value: Float, name: String) var hashValue: Int { switch self { case .genre(let code, let name): return “(code)-(name)".hashValue case .rating(let value, let name): return “(value)-(name)".hashValue case .duration(let duration, let name): return “(duration)-(name)".hashValue } }}protocol FilterListViewControllerDelegate: class { func filterListViewController(_ controller: FilterListViewController, didSelect filter: MovieFilter) func filterListViewController(_ controller: FilterListViewController, didDeselect filter: MovieFilter) func filterListViewControllerDidClearFilters(controller: FilterListViewController)}class FilterListViewController: UITableViewController { let filters = MovieFilter.defaultFilters weak var delegate: FilterListViewControllerDelegate? var selectedFilters: Set<MovieFilter> = [] override func viewDidLoad() { super.viewDidLoad() } func clearFilter() { selectedFilters.removeAll() delegate?.filterListViewControllerDidClearFilters(controller: self) tableView.reloadData() } override func numberOfSections(in tableView: UITableView) -> Int { return filters.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return filters[section].filters.count } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return filters[section].title } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) let filter = filters[indexPath.section].filters[indexPath.row] if selectedFilters.contains(filter) { selectedFilters.remove(filter) delegate?.filterListViewController(self, didDeselect: filter) } else { selectedFilters.insert(filter) delegate?.filterListViewController(self, didSelect: filter) } tableView.reloadRows(at: [indexPath], with: .automatic) } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: “Cell”, for: indexPath) let filter = filters[indexPath.section].filters[indexPath.row] switch filter { case .genre(, let name): cell.textLabel?.text = name case .rating(, let name): cell.textLabel?.text = name case .duration(, let name): cell.textLabel?.text = name } if selectedFilters.contains(filter) { cell.accessoryType = .checkmark } else { cell.accessoryType = .none } return cell }}在容器 View Controller 中集成在 ContainerViewController 中,我们有以下几个实例属性:FilterListContainerView 和 MovieListContainerView: 用于添加子 view controller 的容器视图。FilterListViewController 和 MovieListViewController:使用 Storyboard ID 实例化的影片列表和筛选器列表 view controller 的引用。movie:使用默认硬编码的电影实例的 Movie 数组。当 viewDidLoad 被调用时,我们调用该方法来设置子 View Controller。以下是它要执行的几项任务:使用 Storyboard ID 实例化 FilterListViewController 和 MovieListViewController;将它们分配给实例属性;将 MovieListViewController 分配给 movies 数组;将 ContainerViewController 指定为 FilterListViewController 的委托,以便它可以响应过滤器选择;设置子视图框架并使用扩展帮助方法将它们添加为子 View Controller。对于 FilterListViewControllerDelegate 的实现,当选择或取消选择过滤器时,将针对每个类型、评级和持续时间过滤默认的电影数据。然后,过滤器的结果将分配给 MovieListViewController 的 movies 属性。要取消选择所有过滤器,它只会分配默认的电影数据。import UIKitclass ContainerViewController: UIViewController { @IBOutlet weak var filterListContainerView: UIView! @IBOutlet weak var movieListContainerView: UIView! var filterListVC: FilterListViewController! var movieListVC: MovieListViewController! let movies = Movie.defaultMovies override func viewDidLoad() { super.viewDidLoad() setupChildViewControllers() } private func setupChildViewControllers() { let storyboard = UIStoryboard(name: “Main”, bundle: nil) let filterListVC = storyboard.instantiateViewController(withIdentifier: “FilterListViewController”) as! FilterListViewController addChild(childController: filterListVC, to: filterListContainerView) self.filterListVC = filterListVC self.filterListVC.delegate = self let movieListVC = storyboard.instantiateViewController(withIdentifier: “MovieListViewController”) as! MovieListViewController movieListVC.movies = movies addChild(childController: movieListVC, to: movieListContainerView) self.movieListVC = movieListVC } @IBAction func clearFilterTapped( sender: Any) { filterListVC.clearFilter() } private func filterMovies(moviesFilter: [MovieFilter]) { movieListVC.movies = movies .filter(with: moviesFilter.genreFilters) .filter(with: moviesFilter.ratingFilters) .filter(with: moviesFilter.durationFilters) }}extension ContainerViewController: FilterListViewControllerDelegate { func filterListViewController(_ controller: FilterListViewController, didSelect filter: MovieFilter) { filterMovies(moviesFilter: Array(controller.selectedFilters)) } func filterListViewController(_ controller: FilterListViewController, didDeselect filter: MovieFilter) { filterMovies(moviesFilter: Array(controller.selectedFilters)) } func filterListViewControllerDidClearFilters(controller: FilterListViewController) { movieListVC.movies = Movie.defaultMovies }}结论通过研究示例项目。我们可以看到在我们的应用中使用 View Controller 容器和子 View Controller 的好处。我们可以将单个 View Controller 的职责划分为单独的 View Controller,它们只具有单一职责(SRP)。我们还需要确保子 View Controller 对其父级没有任何依赖。为了让子 View Controller 与父级进行通信,我们可以使用委托模式。该方法还提供了模块松耦合的优点,这可以为每个组件带来更好的可重用性和可测试性。随着我们的应用变得更大、更复杂,该方法确实有助于我们扩展它。让我们继续学习????,祝你圣诞快乐????,新年快乐????!继续使用 Swift 和 Cocoa !!????在社交平台上关注我们:Facebook: facebook.com/AppCodamobile/Twitter: twitter.com/AppCodaMobileInstagram: instagram.com/AppCodadotcom ...

December 29, 2018 · 4 min · jiezi

iOS雪花动画、音频图、新闻界面框架、2048游戏、二维码条形码扫码生成等源码

iOS精选源码粒子雪花与烟花的动画iOS 2048游戏JHSoundWaveView - 简单地声波图、音波图一个可快速集成的新闻详情界面框架,类似今日头条,腾讯新闻二维码/条形码扫描及扫描与输入形式切换和二维码的生成页面多tableView滑动悬停数值加减小控件(购物车、商品类数值更改)iOS国际化语言切换iOS优质博客新手也看得懂的 iOS Runtime 教程关于 Runtime ,网上已经有很多很好的文章,写得很详尽。本篇主要是从新手的角度出发,逐步介绍 Runtime 的原理、常用方法、应用场景等。一、Runtime 是什么在 C 语言中,将代码转换为可执行程序,一般要经历三个步骤,即编译、链接、运行。在链接的时候,对象的类型、方法的实现就已经确定好了。而在 Objec… 阅读原文iOS可视化动态绘制连通图(Swift版)今天的博客我们有易到难大致分为三个部分。第一部分我们会画出相应的图,并该图是可以对每个点进行拖动的,在拖动的过程中,我们对其进行重绘。第二部分会取消拖动,使用UIView自带的动画来让其自己变换,当然本部分你也可以使用Timer或者GCD的TimerSource让其运动。第三部分则是第二部分的升级,再第二部分的基础上我… 阅读原文iOS 架构组件:让你的 TableView 优雅起来GitHub 地址:https://github.com/indulgeIn/…、传统方式的弊端UITableView是出场率极高的视图组件,开发者通过实现和协议方法来配置布局逻辑,面向协议设计模式在苹果的代码设计中很常见,它能适应大部分的业务场景且足够灵活。这种方式优点很多,比如某一时… 阅读原文iOS 关于全面屏适配的方案及UI在不同尺寸下适配方案前言全面屏刚出时,网上有说反人类。但过去这么久了,趋于技术的进步或看久了,大家也都慢慢习惯了(只是笔者还是买不起全面屏)。官方适配中文版文档也出来了。回想起刚开始适配全面屏用了一种暴力、并不优雅的方法,以至于后来出了XS(MAX)和XR后出了bug。所以选择一种可靠的、优雅的方案是很有必要的。如今网上关于探讨适配全面屏… 阅读原文iOS Charles 抓包 https 实战并篡改返回数据没需求?No!不想拦截某个软件的接口数据瞧瞧到底干了啥?是否遇到想把返回数据更改下,来测试临界值情况,得找个后端来一起调试下?发个 Post 请求调试只能一步步来码?这些何曾不是 iOS 开发中面临的!只需要拥有它 Charles 抓包,一切都帮你搞定。 Charles 那么iOS … 阅读原文更多源码更多博文

December 26, 2018 · 1 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

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

【基本功】深入剖析Swift性能优化

简介2014年,苹果公司在WWDC上发布Swift这一新的编程语言。经过几年的发展,Swift已经成为iOS开发语言的“中流砥柱”,Swift提供了非常灵活的高级别特性,例如协议、闭包、泛型等,并且Swift还进一步开发了强大的SIL(Swift Intermediate Language)用于对编译器进行优化,使得Swift相比Objective-C运行更快性能更优,Swift内部如何实现性能的优化,我们本文就进行一下解读,希望能对大家有所启发和帮助。针对Swift性能提升这一问题,我们可以从概念上拆分为两个部分:编译器:Swift编译器进行的性能优化,从阶段分为编译期和运行期,内容分为时间优化和空间优化。开发者:通过使用合适的数据结构和关键字,帮助编译器获取更多信息,进行优化。下面我们将从这两个角度切入,对Swift性能优化进行分析。通过了解编译器对不同数据结构处理的内部实现,来选择最合适的算法机制,并利用编译器的优化特性,编写高性能的程序。理解Swift的性能理解Swift的性能,首先要清楚Swift的数据结构,组件关系和编译运行方式。数据结构Swift的数据结构可以大体拆分为:Class,Struct,Enum。组件关系组件关系可以分为:inheritance,protocols,generics。方法分派方式方法分派方式可以分为Static dispatch和Dynamic dispatch。要在开发中提高Swift性能,需要开发者去了解这几种数据结构和组件关系以及它们的内部实现,从而通过选择最合适的抽象机制来提升性能。首先我们对于性能标准进行一个概念陈述,性能标准涵盖三个标准:AllocationReference countingMethod dispatch接下来,我们会分别对这几个指标进行说明。Allocation内存分配可以分为堆区栈区,在栈的内存分配速度要高于堆,结构体和类在堆栈分配是不同的。Stack基本数据类型和结构体默认在栈区,栈区内存是连续的,通过出栈入栈进行分配和销毁,速度很快,高于堆区。我们通过一些例子进行说明://示例 1// Allocation// Structstruct Point { var x, y:Double func draw() { … }}let point1 = Point(x:0, y:0) //进行point1初始化,开辟栈内存var point2 = point1 //初始化point2,拷贝point1内容,开辟新内存point2.x = 5 //对point2的操作不会影响point1// use point1// use point2以上结构体的内存是在栈区分配的,内部的变量也是内联在栈区。将point1赋值给point2实际操作是在栈区进行了一份拷贝,产生了新的内存消耗point2,这使得point1和point2是完全独立的两个实例,它们之间的操作互不影响。在使用point1和point2之后,会进行销毁。Heap高级的数据结构,比如类,分配在堆区。初始化时查找没有使用的内存块,销毁时再从内存块中清除。因为堆区可能存在多线程的操作问题,为了保证线程安全,需要进行加锁操作,因此也是一种性能消耗。// Allocation// Classclass Point { var x, y:Double func draw() { … }}let point1 = Point(x:0, y:0) //在堆区分配内存,栈区只是存储地址指针let point2 = point1 //不产生新的实例,而是对point2增加对堆区内存引用的指针point2.x = 5 //因为point1和point2是一个实例,所以point1的值也会被修改// use point1// use point2以上我们初始化了一个Class类型,在栈区分配一块内存,但是和结构体直接在栈内存储数值不同,我们只在栈区存储了对象的指针,指针指向的对象的内存是分配在堆区的。需要注意的是,为了管理对象内存,在堆区初始化时,除了分配属性内存(这里是Double类型的x,y),还会有额外的两个字段,分别是type和refCount,这个包含了type,refCount和实际属性的结构被称为blue box。内存分配总结从初始化角度,Class相比Struct需要在堆区分配内存,进行内存管理,使用了指针,有更强大的特性,但是性能较低。优化方式:对于频繁操作(比如通信软件的内容气泡展示),尽量使用Struct替代Class,因为栈内存分配更快,更安全,操作更快。Reference countingSwift通过引用计数管理堆对象内存,当引用计数为0时,Swift确认没有对象再引用该内存,所以将内存释放。对于引用计数的管理是一个非常高频的间接操作,并且需要考虑线程安全,使得引用计数的操作需要较高的性能消耗。对于基本数据类型的Struct来说,没有堆内存分配和引用计数的管理,性能更高更安全,但是对于复杂的结构体,如:// Reference Counting// Struct containing referencesstruct Label { var text:String var font:UIFont func draw() { … }}let label1 = Label(text:“Hi”, font:font) //栈区包含了存储在堆区的指针let label2 = label1 //label2产生新的指针,和label1一样指向同样的string和font地址// use label1// use label2这里看到,包含了引用的结构体相比Class,需要管理双倍的引用计数。每次将结构体作为参数传递给方法或者进行直接拷贝时,都会出现多份引用计数。下图可以比较直观的理解:备注:包含引用类型的结构体出现Copy的处理方式Class在拷贝时的处理方式:引用计数总结Class在堆区分配内存,需要使用引用计数器进行内存管理。基本类型的Struct在栈区分配内存,无引用计数管理。包含强类型的Struct通过指针管理在堆区的属性,对结构体的拷贝会创建新的栈内存,创建多份引用的指针,Class只会有一份。优化方式在使用结构体时:通过使用精确类型,例如UUID替代String(UUID字节长度固定128字节,而不是String任意长度),这样就可以进行内存内联,在栈内存储UUID,我们知道,栈内存管理更快更安全,并且不需要引用计数。Enum替代String,在栈内管理内存,无引用计数,并且从语法上对于开发者更友好。Method Dispatch我们之前在Static dispatch VS Dynamic dispatch中提到过,能够在编译期确定执行方法的方式叫做静态分派Static dispatch,无法在编译期确定,只能在运行时去确定执行方法的分派方式叫做动态分派Dynamic dispatch。Static dispatch更快,而且静态分派可以进行内联等进一步的优化,使得执行更快速,性能更高。但是对于多态的情况,我们不能在编译期确定最终的类型,这里就用到了Dynamic dispatch动态分派。动态分派的实现是,每种类型都会创建一张表,表内是一个包含了方法指针的数组。动态分派更灵活,但是因为有查表和跳转的操作,并且因为很多特点对于编译器来说并不明确,所以相当于block了编译器的一些后期优化。所以速度慢于Static dispatch。下面看一段多态代码,以及分析实现方式://引用语义实现的多态class Drawable { func draw() {} }class Point :Drawable { var x, y:Double override func draw() { … }}class Line :Drawable { var x1, y1, x2, y2:Double override func draw() { … }}var drawables:[Drawable]for d in drawables { d.draw()}Method Dispatch总结Class默认使用Dynamic dispatch,因为在编译期几乎每个环节的信息都无法确定,所以阻碍了编译器的优化,比如inline和whole module inline。使用Static dispatch代替Dynamic dispatch提升性能我们知道Static dispatch快于Dynamic dispatch,如何在开发中去尽可能使用Static dispatch。inheritance constraints继承约束我们可以使用final关键字去修饰Class,以此生成的Final class,使用Static dispatch。access control访问控制private关键字修饰,使得方法或属性只对当前类可见。编译器会对方法进行Static dispatch。编译器可以通过whole module optimization检查继承关系,对某些没有标记final的类通过计算,如果能在编译期确定执行的方法,则使用Static dispatch。Struct默认使用Static dispatch。Swift快于OC的一个关键是可以消解动态分派。总结Swift提供了更灵活的Struct,用以在内存、引用计数、方法分派等角度去进行性能的优化,在正确的时机选择正确的数据结构,可以使我们的代码性能更快更安全。延伸你可能会问Struct如何实现多态呢?答案是protocol oriented programming。以上分析了影响性能的几个标准,那么不同的算法机制Class,Protocol Types和Generic code,它们在这三方面的表现如何,Protocol Type和Generic code分别是怎么实现的呢?我们带着这个问题看下去。Protocol Type这里我们会讨论Protocol Type如何存储和拷贝变量,以及方法分派是如何实现的。不通过继承或者引用语义的多态:protocol Drawable { func draw() }struct Point :Drawable { var x, y:Double func draw() { … }}struct Line :Drawable { var x1, y1, x2, y2:Double func draw() { … }}var drawables:[Drawable] //遵守了Drawable协议的类型集合,可能是point或者linefor d in drawables { d.draw()}以上通过Protocol Type实现多态,几个类之间没有继承关系,故不能按照惯例借助V-Table实现动态分派。如果想了解Vtable和Witness table实现,可以进行点击查看,这里不做细节说明。因为Point和Line的尺寸不同,数组存储数据实现一致性存储,使用了Existential Container。查找正确的执行方法则使用了 Protoloc Witness Table 。Existential ContainerExistential Container是一种特殊的内存布局方式,用于管理遵守了相同协议的数据类型Protocol Type,这些数据类型因为不共享同一继承关系(这是V-Table实现的前提),并且内存空间尺寸不同,使用Existential Container进行管理,使其具有存储的一致性。结构如下:三个词大小的valueBuffer这里介绍一下valueBuffer结构,valueBuffer有三个词,每个词包含8个字节,存储的可能是值,也可能是对象的指针。对于small value(空间小于valueBuffer),直接存储在valueBuffer的地址内, inline valueBuffer,无额外堆内存初始化。当值的数量大于3个属性即large value,或者总尺寸超过valueBuffer的占位,就会在堆区开辟内存,将其存储在堆区,valueBuffer存储内存指针。value witness table的引用因为Protocol Type的类型不同,内存空间,初始化方法等都不相同,为了对Protocol Type生命周期进行专项管理,用到了Value Witness Table。protocol witness table的引用管理Protocol Type的方法分派。内存分布如下:1. payload_data_0 = 0x0000000000000004,2. payload_data_1 = 0x0000000000000000,3. payload_data_2 = 0x0000000000000000,4. instance_type = 0x000000010d6dc408 ExistentialContainers`type metadata for ExistentialContainers.Car,5. protocol_witness_0 = 0x000000010d6dc1c0 ExistentialContainers protocol witness table for ExistentialContainers.Car:ExistentialContainers.Drivable in ExistentialContainersProtocol Witness Table(PWT)为了实现Class多态也就是引用语义多态,需要V-Table来实现,但是V-Table的前提是具有同一个父类即共享相同的继承关系,但是对于Protocol Type来说,并不具备此特征,故为了支持Struct的多态,需要用到protocol oriented programming机制,也就是借助Protocol Witness Table来实现(细节可以点击Vtable和witness table实现,每个结构体会创造PWT表,内部包含指针,指向方法具体实现)。Value Witness Table(VWT)用于管理任意值的初始化、拷贝、销毁。Value Witness Table的结构如上,是用于管理遵守了协议的Protocol Type实例的初始化,拷贝,内存消减和销毁的。Value Witness Table在SIL中还可以拆分为%relative_vwtable和%absolute_vwtable,我们这里先不做展开。Value Witness Table和Protocol Witness Table通过分工,去管理Protocol Type实例的内存管理(初始化,拷贝,销毁)和方法调用。我们来借助具体的示例进行进一步了解:// Protocol Types// The Existential Container in actionfunc drawACopy(local :Drawable) { local.draw()}let val :Drawable = Point()drawACopy(val)在Swift编译器中,通过Existential Container实现的伪代码如下:// Protocol Types// The Existential Container in actionfunc drawACopy(local :Drawable) { local.draw()}let val :Drawable = Point()drawACopy(val)//existential container的伪代码结构struct ExistContDrawable { var valueBuffer:(Int, Int, Int) var vwt:ValueWitnessTable var pwt:DrawableProtocolWitnessTable}// drawACopy方法生成的伪代码func drawACopy(val:ExistContDrawable) { //将existential container传入 var local = ExistContDrawable() //初始化container let vwt = val.vwt //获取value witness table,用于管理生命周期 let pwt = val.pwt //获取protocol witness table,用于进行方法分派 local.type = type local.pwt = pwt vwt.allocateBufferAndCopyValue(&local, val) //vwt进行生命周期管理,初始化或者拷贝 pwt.draw(vwt.projectBuffer(&local)) //pwt查找方法,这里说一下projectBuffer,因为不同类型在内存中是不同的(small value内联在栈内,large value初始化在堆内,栈持有指针),所以方法的确定也是和类型相关的,我们知道,查找方法时是通过当前对象的地址,通过一定的位移去查找方法地址。 vwt.destructAndDeallocateBuffer(temp) //vwt进行生命周期管理,销毁内存}Protocol Type 存储属性我们知道,Swift中Class的实例和属性都存储在堆区,Struct实例在栈区,如果包含指针属性则存储在堆区,Protocol Type如何存储属性?Small Number通过Existential Container内联实现,大数存在堆区。如何处理Copy呢?Protocol大数的Copy优化在出现Copy情况时:let aLine = Line(1.0, 1.0, 1.0, 3.0)let pair = Pair(aLine, aLine)let copy = pair会将新的Exsitential Container的valueBuffer指向同一个value即创建指针引用,但是如果要改变值怎么办?我们知道Struct值的修改和Class不同,Copy是不应该影响原实例的值的。这里用到了一个技术叫做Indirect Storage With Copy-On-Write,即优先使用内存指针。通过提高内存指针的使用,来降低堆区内存的初始化。降低内存消耗。在需要修改值的时候,会先检测引用计数检测,如果有大于1的引用计数,则开辟新内存,创建新的实例。在对内容进行变更的时候,会开启一块新的内存,伪代码如下:class LineStorage { var x1, y1, x2, y2:Double }struct Line :Drawable { var storage :LineStorage init() { storage = LineStorage(Point(), Point()) } func draw() { … } mutating func move() { if !isUniquelyReferencedNonObjc(&storage) { //如何存在多份引用,则开启新内存,否则直接修改 storage = LineStorage(storage) } storage。start = … }}这样实现的目的:通过多份指针去引用同一份地址的成本远远低于开辟多份堆内存。以下对比图:Protocol Type多态总结支持Protocol Type的动态多态(Dynamic Polymorphism)行为。通过使用Witness Table和Existential Container来实现。对于大数的拷贝可以通过Indirect Storage间接存储来进行优化。说到动态多态Dynamic Polymorphism,我们就要问了,什么是静态多态Static Polymorphism,看看下面示例:// Drawing a copyprotocol Drawable { func draw()}func drawACopy(local :Drawable) { local.draw()}let line = Line()drawACopy(line)// …let point = Point()drawACopy(point)这种情况我们就可以用到泛型Generic code来实现,进行进一步优化。泛型我们接下来会讨论泛型属性的存储方式和泛型方法是如何分派的。泛型和Protocol Type的区别在于:泛型支持的是静态多态。每个调用上下文只有一种类型。查看下面的示例,foo和bar方法是同一种类型。在调用链中会通过类型降级进行类型取代。对于以下示例:func foo<T:Drawable>(local :T) { bar(local)}func bar<T:Drawable>(local:T) { … }let point = Point()foo(point)分析方法foo和bar的调用过程://调用过程foo(point)–>foo<T = Point>(point) //在方法执行时,Swift将泛型T绑定为调用方使用的具体类型,这里为Point bar(local) –>bar<T = Point>(local) //在调用内部bar方法时,会使用foo已经绑定的变量类型Point,可以看到,泛型T在这里已经被降级,通过类型Point进行取代泛型方法调用的具体实现为:同一种类型的任何实例,都共享同样的实现,即使用同一个Protocol Witness Table。使用Protocol/Value Witness Table。每个调用上下文只有一种类型:这里没有使用Existential Container, 而是将Protocol/Value Witness Table作为调用方的额外参数进行传递。变量初始化和方法调用,都使用传入的VWT和PWT来执行。看到这里,我们并不觉得泛型比Protocol Type有什么更快的特性,泛型如何更快呢?静态多态前提下可以进行进一步的优化,称为特定泛型优化。泛型特化静态多态:在调用站中只有一种类型Swift使用只有一种类型的特点,来进行类型降级取代。类型降级后,产生特定类型的方法为泛型的每个类型创造对应的方法这时候你可能会问,那每一种类型都产生一个新的方法,代码空间岂不爆炸?静态多态下进行特定优化specialization因为是静态多态。所以可以进行很强大的优化,比如进行内联实现,并且通过获取上下文来进行更进一步的优化。从而降低方法数量。优化后可以更精确和具体。例如:func min<T:Comparable>(x:T, y:T) -> T { return y < x ? y : x}从普通的泛型展开如下,因为要支持所有类型的min方法,所以需要对泛型类型进行计算,包括初始化地址、内存分配、生命周期管理等。除了对value的操作,还要对方法进行操作。这是一个非常复杂庞大的工程。func min<T:Comparable>(x:T, y:T, FTable:FunctionTable) -> T { let xCopy = FTable.copy(x) let yCopy = FTable.copy(y) let m = FTable.lessThan(yCopy, xCopy) ? y :x FTable.release(x) FTable.release(y) return m}在确定入参类型时,比如Int,编译器可以通过泛型特化,进行类型取代(Type Substitute),优化为:func min<Int>(x:Int, y:Int) -> Int { return y < x ? y :x}泛型特化specilization是何时发生的?在使用特定优化时,调用方需要进行类型推断,这里需要知晓类型的上下文,例如类型的定义和内部方法实现。如果调用方和类型是单独编译的,就无法在调用方推断类型的内部实行,就无法使用特定优化,保证这些代码一起进行编译,这里就用到了whole module optimization。而whole module optimization是对于调用方和被调用方的方法在不同文件时,对其进行泛型特化优化的前提。泛型进一步优化特定泛型的进一步优化:// Pairs in our program using generic typesstruct Pair<T :Drawable> { init(_ f:T, _ s:T) { first = f ; second = s } var first:T var second:T}let pairOfLines = Pair(Line(), Line())// …let pairOfPoint = Pair(Point(), Point())在用到多种泛型,且确定泛型类型不会在运行时修改时,就可以对成对泛型的使用进行进一步优化。优化的方式是将泛型的内存分配由指针指定,变为内存内联,不再有额外的堆初始化消耗。请注意,因为进行了存储内联,已经确定了泛型特定类型的内存分布,泛型的内存内联不能存储不同类型。所以再次强调此种优化只适用于在运行时不会修改泛型类型,即不能同时支持一个方法中包含line和point两种类型。whole module optimizationwhole module optimization是用于Swift编译器的优化机制。可以通过-whole-module-optimization (或 -wmo)进行打开。在XCode 8之后默认打开。 Swift Package Manager在release模式默认使用whole module optimization。module是多个文件集合。编译器在对源文件进行语法分析之后,会对其进行优化,生成机器码并输出目标文件,之后链接器联合所有的目标文件生成共享库或可执行文件。whole module optimization通过跨函数优化,可以进行内联等优化操作,对于泛型,可以通过获取类型的具体实现来进行推断优化,进行类型降级方法内联,删除多余方法等操作。全模块优化的优势编译器掌握所有方法的实现,可以进行内联和泛型特化等优化,通过计算所有方法的引用,移除多余的引用计数操作。通过知晓所有的非公共方法,如果这写方法没有被使用,就可以对其进行消除。如何降低编译时间和全模块优化相反的是文件优化,即对单个文件进行编译。这样的好处在于可以并行执行,并且对于没有修改的文件不会再次编译。缺点在于编译器无法获知全貌,无法进行深度优化。下面我们分析下全模块优化如何避免没修改的文件再次编译。编译器内部运行过程分为:语法分析,类型检查,SIL优化,LLVM后端处理。语法分析和类型检查一般很快,SIL优化执行了重要的Swift特定优化,例如泛型特化和方法内联等,该过程大概占用整个编译时间的三分之一。LLVM后端执行占用了大部分的编译时间,用于运行降级优化和生成代码。进行全模块优化后,SIL优化会将模块再次拆分为多个部分,LLVM后端通过多线程对这些拆分模块进行处理,对于没有修改的部分,不会进行再处理。这样就避免了修改一小部分,整个大模块进行LLVM后端的再次执行,除此外,使用多线程并行操作也会缩短处理时间。扩展:Swift的隐藏“Bug”Swift因为方法分派机制问题,所以在设计和优化后,会产生和我们常规理解不太一致的结果,这当然不能算Bug。但是还是要单独进行说明,避免在开发过程中,因为对机制的掌握不足,造成预期和执行出入导致的问题。Message dispatch我们通过上面说明结合Static dispatch VS Dynamic dispatch对方法分派方式有了了解。这里需要对Objective-C的方法分派方式进行说明。熟悉OC的人都知道,OC采用了运行时机制使用obj_msgSend发送消息,runtime非常的灵活,我们不仅可以对方法调用采用swizzling,对于对象也可以通过isa-swizzling来扩展功能,应用场景有我们常用的hook和大家熟知的KVO。大家在使用Swift进行开发时都会问,Swift是否可以使用OC的运行时和消息转发机制呢?答案是可以。Swift可以通过关键字dynamic对方法进行标记,这样就会告诉编译器,此方法使用的是OC的运行时机制。注意:我们常见的关键字@ObjC并不会改变Swift原有的方法分派机制,关键字@ObjC的作用只是告诉编译器,该段代码对于OC可见。总结来说,Swift通过dynamic关键字的扩展后,一共包含三种方法分派方式:Static dispatch,Table dispatch和Message dispatch。下表为不同的数据结构在不同情况下采取的分派方式:如果在开发过程中,错误的混合了这几种分派方式,就可能出现Bug,以下我们对这些Bug进行分析:SR-584此情况是在子类的extension中重载父类方法时,出现和预期不同的行为。class Base:NSObject { var directProperty:String { return “This is Base” } var indirectProperty:String { return directProperty }}class Sub:Base { }extension Sub { override var directProperty:String { return “This is Sub” }}执行以下代码,直接调用没有问题:Base().directProperty // “This is Base”Sub().directProperty // “This is Sub”间接调用结果和预期不同:Base()。indirectProperty // “This is Base”Sub()。indirectProperty // expected “this is Sub”,but is “This is Base” <- Unexpected!在Base.directProperty前添加dynamic关键字就可以获得"this is Sub"的结果。Swift在extension 文档中说明,不能在extension中重载已经存在的方法。“Extensions can add new functionality to a type, but they cannot override existing functionality.”会出现警告:Cannot override a non-dynamic class declaration from an extension。出现这个问题的原因是,NSObject的extension是使用的Message dispatch,而Initial Declaration使用的是Table dispath(查看上图 Swift Dispatch Method)。extension重载的方法添加在了Message dispatch内,没有修改虚函数表,虚函数表内还是父类的方法,故会执行父类方法。想在extension重载方法,需要标明dynamic来使用Message dispatch。SR-103协议的扩展内实现的方法,无法被遵守类的子类重载:protocol Greetable { func sayHi()}extension Greetable { func sayHi() { print(“Hello”) }}func greetings(greeter:Greetable) { greeter.sayHi()}现在定义一个遵守了协议的类Person。遵守协议类的子类LoudPerson:class Person:Greetable {}class LoudPerson:Person { func sayHi() { print(“sub”) }}执行下面代码结果为:var sub:LoudPerson = LoudPerson()sub.sayHi() //sub不符合预期的代码:var sub:Person = LoudPerson()sub.sayHi() //HellO <-使用了protocol的默认实现注意,在子类LoudPerson中没有出现override关键字。可以理解为LoudPerson并没有成功注册Greetable在Witness table的方法。所以对于声明为Person实际为LoudPerson的实例,会在编译器通过Person去查找,Person没有实现协议方法,则不产生Witness table,sayHi方法是直接调用的。解决办法是在base类内实现协议方法,无需实现也要提供默认方法。或者将基类标记为final来避免继承。进一步通过示例去理解:// Defined protocol。protocol A { func a() -> Int}extension A { func a() -> Int { return 0 }}// A class doesn’t have implement of the function。class B:A {}class C:B { func a() -> Int { return 1 }}// A class has implement of the function。class D:A { func a() -> Int { return 1 }}class E:D { override func a() -> Int { return 2 }}// Failure cases。B().a() // 0C().a() // 1(C() as A).a() // 0 # We thought return 1。 // Success cases。D().a() // 1(D() as A).a() // 1E().a() // 2(E() as A).a() // 2其他我们知道Class extension使用的是Static Dispatch:class MyClass {}extension MyClass { func extensionMethod() {}} class SubClass:MyClass { override func extensionMethod() {}}以上代码会出现错误,提示Declarations in extensions can not be overridden yet。总结影响程序的性能标准有三种:初始化方式, 引用指针和方法分派。文中对比了两种数据结构:Struct和Class的在不同标准下的性能表现。Swift相比OC和其它语言强化了结构体的能力,所以在了解以上性能表现的前提下,通过利用结构体可以有效提升性能。在此基础上,我们还介绍了功能强大的结构体的类:Protocol Type和Generic。并且介绍了它们如何支持多态以及通过使用有条件限制的泛型如何让程序更快。参考资料swift memorylayoutwitness table videoprotocol types pdfprotocol and value oriented programming in UIKit apps videooptimizing swift performancewhole module optimizaitonincreasing performance by reducing dynamic dispatchprotocols generics existential containerprotocols and genericswhy swift is swiftswift method dispatchswift extensionuniversal dynamic dispatch for method callscompiler performance.mdstructures and classes作者简介亚男,美团点评iOS工程师。2017年加入美团点评,负责专业版餐饮管家开发,研究编译器原理。目前正积极推动Swift组件化建设。招聘信息我们餐饮生态技术部是一个技术氛围活跃,大牛聚集的地方。新到店紧握真正的大规模SaaS实战机会,多租户、数据、安全、开放平台等全方位的挑战。业务领域复杂技术挑战多,技术和业务能力迅速提升,最重要的是,加入我们,你将实现真正通过代码来改变行业的梦想。我们欢迎各端人才加入,Java优先。感兴趣的同学赶紧发送简历至 zhaoyanan02@meituan.com,我们期待你的到来。 ...

November 2, 2018 · 5 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