作者: Lefe_x
ReactiveX
(Reactive Extensions
)是通过可观察的流实现异步编程的一种API
,它结合了观察者模式、迭代器模式和函数式编程的精华。RxSwift
是ReactiveX
编程思想的一种实现,几乎每一种语言都会有那么一个Rx[xxxx]
框架,比如 RxJava
,RxJS
等。Rx 可以概括为,对某些数据流(很广,可以是一些事件等)进行处理,使其变成可观察对象(Observable
)序列,这样观察者(observer
)就可以订阅这些序列【观察者模式】。然而对于订阅者来说(observer
)某些选项(items
)并不是自己需要的(需要过滤),某些选项(items
)需要转换才能达到自己的目的【操作符 filter
, map
等】。为了提升用户体验,或其它目的,有些操作需要放到特定的线程去执行,比如 UI 操作需要放到主线程,这就涉及到了调度器【调度器 Scheduler
】。所以Rx可以这样概括,Rx = Observables + LINQ + Schedulers
,其中 LINQ(Language Integrated Query)
语言集成查询,比如那些操作符号。
下图是 RxSwift
的简单的应用,从数组中找出 Lefe_x
并显示到 Label
上。
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global().async {
self.from()
}
}
func from() {
Observable.from(["Lefe", "Lefe_x", "lefex", "wsy", "Rx"])
.subscribeOn(MainScheduler.instance)
.filter({ (text) -> Bool in
return text == "Lefe_x"
})
.map({ (text) -> String in
return "我的新浪微博是: " + text
})
.subscribe(onNext: { [weak self] (text) in
self?.nickNameLabel.text = text
})
.disposed(by: disposeBag)
}
流程如下图所示:
在React Native
项目中引入realm
后,在编译阶段,网络状况不好的时候会卡在realm模块下载的地方。现象是如果在终端用react-native run-ios
,则会卡在Download realm-sync-cocoa-${version}.tar.xz
,如果是Xcode,则会卡在Build ${ProjectName}: RealmJS
这一块。
这主要是因为realm的库太大(realm-sync-cocoa-${version}.tar.xz
包大概是50+M)。所以我们可以考虑把包先下载到本地,以减少每次编译的时间。
- 我们可以从
realm-cocoa
的build.sh文件中找到realm-sync-cocoa-${version}.tar.xz
的下载地址(注:${version}是具体的版本号),下载文件。 - 在命令行输入
getconf DARWIN_USER_TEMP_DIR
,获取文件下载的临时目录 - 将下载的文件拷贝到临时目录
- 这样基本就OK。
如果还不行,则可以考虑以下操作:
- 修改下载的脚本代码了。找到
node_modules/realm/scripts/download_realm.js
; - 找到
download()
函数,将其中的saveFile()
函数作如下修改;
function saveFile() {
if (process.stdout.isTTY) {
printProgress(response.body, parseInt(response.headers.get('Content-Length')), archive);
} else {
console.log(`Downloading ${archive}`);
}
return new Promise((resolve) => {
// const file = fs.createWriteStream(destination);
// response.body.pipe(file).once('finish', () => file.close(resolve));
resolve();
}).then(() => fs.utimes(destination, lastModified, lastModified));
}
最后运行react-native run-ios
。
参考
之前分享了可以用dispatch_queue_set_specific
和dispatch_get_specific
来判断代码是否运行在主队列上。除了这种方法外,还可以使用dispatch_queue_get_label
获取当前队列的label
,与主队列的label
比较,如图代码所示:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%d", [self isMainQueue]); // 1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"%d", [self isMainQueue]); // 0
});
}
- (BOOL)isMainQueue {
return strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0;
}
主队列的label
是com.apple.main-thread
。当然也可以用这种方式来判断其它队列。全局队列的label
与其QOS
类型相关,label
值主要有以下几个:
com.apple.root.user-interactive-qos //
com.apple.root.user-initiated-qos // DISPATCH_QUEUE_PRIORITY_HIGH
com.apple.root.default-qos // DISPATCH_QUEUE_PRIORITY_DEFAULT
com.apple.root.utility-qos // DISPATCH_QUEUE_PRIORITY_LOW
com.apple.root.background-qos // DISPATCH_QUEUE_PRIORITY_BACKGROUND
我们可以使用dispatch_get_global_queue(qos_class_self(), 0)
来获取代码所在的的全局队列,进而获取其label
值。
参考
作者: Vong_HUST
通常情况下,使用 Instruments
做性能分析,看调用栈时,是能够直接看到对应方法调用堆栈的,而不是一串的地址,但这个前提是 Xcode 用编译过这个应用。如果是用同事或者打包服务器上的安装包做性能分析时,就无法看到对应的方法调用堆栈信息了,只能看到一串地址。这个时候可以通过导入 dSYM
的方式来查看对应的方法调用堆栈,方式如下(见图):打开 Instruments
-> 运行一遍你的应用
-> 停止
-> File
-> Symbols
-> dSYM Path : Locate
即可。如下图所示:
但是有些时候可能应用中包含 extension
,则直接 locate
会提示无法定位到 dSYM 文件
The specified path didn't locate a dSYM for any of the selected libraries.
这个时候需要把你的 dSYM.zip
解压,右键显示其内容将对应的 .app.dSYM
拷出,然后 locate
到这个拷出的 dSYM
文件即可或者按照下图操作也是可行的。具体流程见图:
作者: Lefe_x
使用 Pod 的同学经常会遇到 "error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation."
错误,如下图所示。其实是 [CP] Check Pods Manifest.lock
这个脚本所起的作用。
Pod 中有 Manifest.lock
和 Podfile.lock
这两个文件,只要这两个文件的内容不一样就会报错上面这个错误。Podfile.lock
是大家共用的文件(用来保证我们每个人的Pod库版本一样),而 Manifest.lock
是本地的文件(自己用)。下图中这个脚本正是做这样的事情。
diff "${PODS_PODFILE_DIR_PATH}/Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null
if [ $? != 0 ] ; then
# print error to STDERR
echo "error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation." >&2
exit 1
fi
# This output is used by Xcode 'outputs' to avoid re-running this script phase.
echo "SUCCESS" > "${SCRIPT_OUTPUT_FILE_0}"
解释下这个脚本
shell
脚本总是以:#!/bin/bash
或者 #!/bin/sh
开头,它主要告诉系统执行这个文件需要哪个解释器,进入 /bin
目录下可以看到 bash 和 sh 解释器;
diff
命令:判断两个文件的不同,比如diff /Users/lefe/Desktop/project/Kmart/Podfile.lock /Users/lefe/Desktop/project/Kmart/pods/Manifest.lock >~/Desktop/shell.log
比较两个文件的不同,并重定向到shell.log
文件中;>
重定向符号,可以把输出命令输出到某个文件中而不是控制台;echo
是脚本的输出,相当于printf
;exit 1
退出,有了这个命令 Xcode 就会报错,你可以在 Xcode 中新建一个脚本,试试下面这个脚本:
echo "This is a test shell created by Lefe_x"
exit 1
-
$?:
指上条命令执行的结果,也就是diff
执行的结果; -
下面是 shell 中的 if 语句:
if 条件 ; then
fi
如何在终端执行脚本
假如有个叫 podlgsk.sh
的脚本,只要给予它执行权限(chmod +x podlgsk.sh
),注意只需要给一次执行权限就行,下次运行脚本时就不需要给予执行权限了,然后直接 ./podlgsk.sh
即可。
小结
其实 Shell
脚本就是把一些 linux
命令组合到一起,经过一些处理(比如:if 判断,循环等)后所形成的文件。(不知道总结的是否恰当)
作者: Vong_HUST
众所周知,对于一些多边形的绘制,我们可以使用 CAShapeLayer
配合 UIBezierPath
,然后再用这个 layer
给 View
做 mask
即可。但是一种情况是不行的,对于 UIVisualEffectView
,iOS10 之前 self.blurView.layer.mask = someShapeLayer
这一句是 ok 的,但是 iOS10 之后,这样设将无效,而应该使用 self.blurView.maskView = maskView
。具体代码如下图所示:
详细解释可参考这里
作者: iOS_OneByte
iOS 中 UITableView
中有一种比较常用的样式 UITableViewStyleGrouped
。有时我们要隐去 section
头尾的话,经常实现如下:
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 0.1f;
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
return 0.1f;
}
如果只实现这2段代码的话,在 iOS 11
之前是不会出现问题的,但 iOS 11
之后需要同时实现如下:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
return nil;
}
-(UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
{
return nil;
}
Base SDK
路径设置: Xcode
< Build Settings
< Base SDK
Base SDK
:指得是当前编译应用的和构建 .ipa
的 SDK
的版本,并且手机的 SDK
版本是向前兼容。
作者: Lefe_x
在微信小程序中,开发 UI 功能模块,绝对比的上 iOS 原生开发。它已经提供了一套成熟的数据绑定方式,而不需要引入其它库。而在 iOS 中我们同样可以做数据绑定,然而需要花费一定的时间来学习一些数据绑定的框架,比如 RAC,RxSwift。这里主要以 RxSwift
为例说明。在小程序中,假如实现一个功能,点击按钮修改昵称,可以这样实现:
Page({
data: {
nickName: 'Lefe_x'
},
changeNickNameAction: function () {
this.setData ({
nickName: 'Lefe'
})
}
})
直接修改数据源 data
中的 nickName
即可让新的昵称(Lefe)显示到控件上。看看具体的数据绑定实现:
<view class="main-container">
{{nickName}}
修改昵称
</view>
nickName
与一个 Text
的控件绑定到了一起,这样只要 nickName
发生改变,就会显示到 Text
控件上。而在 iOS 中我们可以使用 RxSwift
做数据绑定。比如:当 nickNameTextField
输入内容后,将显示在 nickNameLabel
上。
class MiniViewController: UIViewController {
let disposeBag = DisposeBag()
@IBOutlet weak var nickNameTextField: UITextField!
@IBOutlet weak var nickNameLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
nickNameTextField.rx.text.bind(to: nickNameLabel.rx.text).disposed(by: disposeBag)
}
}
作者: Vong_HUST
在新建一个 ViewController
文件时,如果同时也勾选了 Also create XIB file
,那么ViewController *viewController = [[ViewController alloc] init]
得到的是和 initWithNibName:bundle:
初始化得到的 UI 一致的。原因是 init
方法最终都会调到指定构造器 initWithNibName:bundle:
,但此时 nibName
为 nil
,而且没有 VC 复写 loadView
的情况下,则系统会有一套自己的寻找机制来看是否有对应的 xib 文件,如果有,则加载 xib 文件。具体可参考苹果官方文档的解释:
在手机端开发 web
页面时,有时候我们可能需要删除一些 cookie
值。JS 删除 cookie
主要是将 cookie
的 expires
属性设置为一个早于当前时间的值。不过如果 cookie
是 HttpOnly
的话,表示这个 cookie
值不能通过非 HTTP
方式来访问,而无法通过 JS 来访问,document.cookie
获取不到,所以也无法通过 JS 来删除这样的 cookie
值。
不过除了通过 server
端来处理外,我们也可以借助 native
端来执行删除操作。iOS 端使用 NSHTTPCookie
对象来表示一个 cookie
,并通过 NSHTTPCookieStorage
来管理当前应用的所有 cookie
,包括 HttpOnly/secure
类型的 cookie
。所有想要删除 cookie
,只需要依据给定的条件(如域、path等属性)来找出对应的 NSHTTPCookie
对象,并删除就行。以下代码是清空当前应用程序所有 cookie
的操作:
NSArray<NSHTTPCookie *> *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;
for (NSHTTPCookie *cookie in cookies) {
[[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie];
}
当然,如果要在 web
端触发操作,还需要提供 Hybrid
接口。
想在程序运行时判断一个子类是否实现了父类的某个方法,可以比较两个类对应方法的指针是否相等。以 -init
方法为例,如下代码所示:
@interface Test: NSObject
@end
@implementation Test
@end
BOOL check() {
SEL sel = @selector(init);
IMP baseInit = [NSObject instanceMethodForSelector:sel];
IMP testInit = [Test instanceMethodForSelector:sel];
return baseInit == testInit;
}
NSLog(@"%@", check() ? @"true" : @"false"); // true
按照 Objective-C
查找方法的规则,对象会先在自己的方法列表中查找,如果有,则调用;如果没有,则向上去父类里面查找,依此一直到 NSObject
。
需要注意的是,如果是多层继承体系,即使某个子类没有实现某个方法,也不能确定这个子类和某个父类是同一个实现,如下代码所示:
@interface Base: NSObject
@end
@implementation Base
- (id)init {
return [super init];
}
@end
@interface Test: Base
@end
@implementation Test
@end
BOOL check() {
SEL sel = @selector(init);
IMP baseInit = [NSObject instanceMethodForSelector:sel];
IMP testInit = [Test instanceMethodForSelector:sel];
return baseInit == testInit;
}
NSLog(@"%@", check() ? @"true" : @"false"); // false
ParsifalC童鞋之前也分享过相关的内容,提供了几种方法,具体可以参考这里
作者: 高老师很忙
Git
的 Stash
功能很方便的帮我们存一些临时代码,如果不小心把 Stash
代码删除了如何恢复呢?使用 git fsck --lost-found
查看我们最近删除的 Stash
;找到你需要恢复的 dangling commit
(只需关注 dangling commit
);再使用 git merge
命令即可恢复。如下两图所示:
作者: iOS_OneByte
iOS 经常定义类似如下的输出宏:
#ifdef DEBUG
#define ZJLog(fmt, ...) NSLog((fmt), ##__VA_ARGS__)
#else
#define ZJLog(...)
#endif
但是大部分人可能只是遵循国际惯例,并不知道如果直接使用 NSLog
的危害或者如何去查看别人家的应用输出,其实很简单,只是 Xcode
隐藏的很深,路径如下
Xcode8 - Window - Devices
Xcode9 - Window - Devices And Simulators
如下图所示:
这个利用好的话,对于调试自己或者查(po)看(jie)别人家的应用都很有用,效果如下图所示:
作者: Vong_HUST
首先 podfile.lock
和 podfile
必须加入版本控制。
install
并不是第一次创建 podfile
时运行一次,后面就不再使用了。install
命令不仅在初始时使用,在新增或删除 repo
时也需要运行。每次添加或删除 repo
后应该执行 install
命令,这样其它的 repo
不会更新。update
仅仅在只需更新某一个 repo
或所有时才使用。每次执行 install
时,会将每个 repo
的版本信息写入到 podfile.lock
,已存在于 podfile.lock
的 repo
不会被更新只会下载指定版本,不在 podfile.lock
中的 repo
将会搜索与 podfile
里面对应 repo
匹配的版本。即使某个 repo
指定了版本,如 pod 'A', '1.0.0'
,最好也是不要使用 update
,因为 repo A
可能有依赖,如果此时使用 update
会更新其依赖。
参考:
作者: 高老师很忙
在调试推送、Today
、3DTouch
等唤起测试 App 的时候(此时App未启动),我们通常 Run
的方式是不行的,因为 Run
后 App
就启动了,不满足调试环境。Xcode
为我们提供了 Attach
的方式进行调试,使用起来也是超简单的。操作方式如下:
前提:已经加了断点(比如 application: didFinishLaunchingWithOptions:
方法里加断点)
-
Attach
之前需要把测试App
的进程杀掉(如果不杀掉进程,这种方式是无法断点调试的); -
选择你要
Attach
的测试App
,有两种方式:在Debug
下拉菜单下面有Attach to Process
选项(直接选择你的测试App
,如图1)和Attach to Process by PID or Name
选项(输入名称,如图2);
- 用推送、
Today
、3DTouch
等方式唤起,就大功告成了,如下图所示
作者: Lefe_x
用过 iTerm
后,我再也不想使用系统自带的终端了。 iTerm
是终端的一个替代品,而且开源免费。它支持非常棒的特性:
- 设置一个可爱的背景图
Preference -> Profiles -> Window -> Background Image
,如下图所示;
-
Hotkey Window
,让终端立刻出现在你的面前。比如当你使用Xcode
正在编码时,这时需要执行Pod install
,使用快捷键command+i
(需要设置) 直接调出iTerm
; -
选择即复制,当你选择终端中的文本后,它会自动复制;
-
点击可跳转,如下图中,按住
command
并点击/Users/apple/Desktop/project/lefeDemo
就会跳转到这个文件夹下。 按住command
并点击www.baidu.com
,就会在浏览器中打开www.baidu.com
;
-
自动提示你所输过的命令,这样可以快速输入你所敲过的命令;
-
快照功能,可以回退到某一时刻你所输过的命令;
想了解 iTerm 更多特性,可以查看 官网
作者: Lefe_x
Your terminal never felt this good before.
上一条 iOS知识小集
主要说了下使用 iTerm
可以改善我们的终端,评论中 @CrespoXiao 和 @struggleend 提到了 iTerm + ZSH
使用起来更方便。搜了下 ZSH
,发现 Star 数 62476 个,确实是好东西,和大家分享下,目前使用时间不长,姑且总结几条我认为比较好的,有熟悉的同学在评论中分享给大家吧 🤝,相信同学们不会吝啬的。安装看这里 。Oh My Zsh
它是基于 zsh 命令行的一个扩展工具集。
提示:根目录下有 ~/.zshrc
文件,可以修改配置,比如主题,插件等。
- 设置一个主题,如果你有洁癖,选择一个你认为比较好的主题 ;
- 使用插件,比如有 Git 插件,当记不住命令的时候,按 Tab 键可以提示,比如
git p [tab]
将会提示,如下图所示;
- 支持简写,比如省略 cd,回到上级目录直接输入:.. ;
ls *.jpeg
显示当前目录下所有 jpeg 文件;cd <TAB>
简显示当前目录下的文件,继续按 可以选择你所要进入的目录;git <UP>
可以找到最近输入的 Git 命令;
想了解 ZSH
更多特性,可以查看 官网
作者: Vong_HUST
熟悉 CoreData
的都知道 NSFetchedResultsController
这个类,与 tableView
和 collectionView
结合起来使用非常方便。
但是这个类在 iOS10
中有个比较坑爹的地方,如果使用的是下图的方式初始化
并且传入的 cacheName
不为 nil
,那么在 iOS10
下面可能会产生各种莫名其妙的崩溃。详细原因如下图描述:
简单翻译一下就是:修改了 vc
的 fetchRequest
之后,context
的 save
操作会打开一个或多个文件描述符,当打开的文件描述符数目超过255
(真机)后,后续的资源加载都会导致崩溃。
当初遇到这个问题的时候,整个应用都处于随机崩溃的情况,看日志都是加载 storyboard
或者 xib
等资源时的崩溃,但是死活复现不出来,看崩溃日志也找不到原因。然后无意间发现 Xcode console
中一直不断输出如下警告信息:
(NSFetchedResultsController): couldn't read cache file to update store info timestamps
然后 google 了一下,找到了一个相同的问题,并且有完整的复现 Demo
及复现步骤,然后全局代码搜索果然发现有一个地方初始化的时候传入了 cacheName
,将其置 nil
后就再也没有那段 warning
同时上线之后也没有再收到资源加载的崩溃了。
所以 Xcode
的 warning
还是值的注意的。附上这个 radar
的链接,里面包含 Demo
地址。
今天再去复现的时候,试了两台 iOS10
的设备,Xcode 9.1
下 iPhone5S iOS10.2
这个 bug 已经不存在了,但是 iPhone7 iOS10.0
还是存在的。