使用Cycript修改微信红包数额


这里使用的是非越狱手机、pp助手(https://pan.baidu.com/s/1cxgFdn7RNXC2IwnEitk9RA, 密码:nbva)用来下载越狱包、及安装MonkeyDev

由于是非越狱手机,所以需要借助pp助手下载一个微信越狱包,那为什么要安装MonkeyDev呢?

MonkeyDev是一个非越狱开发集成神器

  • 可以使用Xcode开发CaptainHook Tweak、Logos Tweak 和 Command-line Tool,在越狱机器开发插件,这是原来iOSOpenDev功能的迁移和改进
  • 只需拖入一个砸壳应用,自动集成class-dump、restore-symbol、Reveal、Cycript和注入的动态库并重签名安装到非越狱机器
  • 支持调试自己编写的动态库和第三方App
  • 支持通过CocoaPods第三方应用集成SDK以及非越狱插件,简单来说就是通过CocoaPods搭建了一个非越狱插件商店

安装:

1.安装最新的theos

sudo git clone --recursive https://github.com/theos/theos.git /opt/theos

2.安装ldid

brew install ldid

3.选择指定Xcode进行安装

sudo xcode-select -s /Applications/Xcode-beta.app

sudo /bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/AloneMonkey/MonkeyDev/master/bin/md-install)"
  • 默认安装的xcode为xcode-select -p

4.卸载

sudo /bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/AloneMonkey/MonkeyDev/master/bin/md-uninstall)"

使用:

1.新建一个monkeyDev工程

2.将微信越狱包拖到此文件下然后真机运行

3.启动成功后进入Cycript环境并修改

使用cycript -r 网络IP地址:端口号 进行cycript环境,手机需和Mac处于同一网络下,端口号可以自行修改

这个命令可以写成一个脚本,将这个脚本放在指定目录,并且在.bash_profile中做好配置,以后就不用每次输入

隐藏状态栏

自定义弹框

设置推送通知个数

查看当前程序所有的UILabel

修改红包数额

如果想修改某个控件的值,可以使用choose方法,也可以直接通过Xcode查看层次结构找到该控件的地址,然后再做修改


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

配置Cycript环境前常用工具的安装配置


首先,这些安装工具并非必须的,仅为提升效率使用,以简洁为主

Alfred, 密码:fnsh

pp_mac, 密码:nbva

go2Shell, 密码:v31a

Alfred安装

打开安装包,将Alfred 3拖入程序,打开Alfred 3 KG,点击PATCH弹窗对话框,选择Alfred 3,如果弹出需安装Xcode_select,需要安装,然后点击save,会提示saved successfully,即破解版安装成功

然而,安装成功后打开会提示文件损坏,是因为这并非使用的App Store下载的,需打开系统偏好设设置->安全性与隐私->选择任何来源即可。如果没有任何来源选项,需设置sudo spctl --master-disable

默认使用option+space会唤起Alfred,打开偏好设置,点击Web Search, 可以配置常用app的唤起方式,默认都是一些国外的网站。如果Alfred没有购买或者破解,这个设置是无效的。

可以点击Add Custom Search自己添加一些自定义的搜索设置

只需要在对应的网站上搜索,记下网址,并把具体的搜索内容改为{query}就可以找到我们自定义搜索设置中所需要的Search URL信息了。可以加一些常用的网站设置

另外,Alfred如果需要支持iTerm2需要做额外的设置

添加的脚本内容是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
on alfred_script(q)  
if application "iTerm2" is running or application "iTerm" is running then
run script "
on run {q}
tell application \":Applications:iTerm.app\"
activate
try
select first window
set onlywindow to false
on error
create window with default profile
select first window
set onlywindow to true
end try
tell current session of the first window
if onlywindow is false then
tell split vertically with default profile
write text q
end tell
end if
end tell
end tell
end run
" with parameters {q}
else
run script "
on run {q}
tell application \":Applications:iTerm.app\"
activate
try
select first window
on error
create window with default profile
select first window
end try
tell the first window
tell current session to write text q
end tell
end tell
end run
" with parameters {q}
end if
end alfred_script
iTerm2安装

下载地址

下载的是压缩文件,解压后是执行程序文件,你可以直接双击,或者直接将它拖到 Applications 目录下

者你可以直接使用 Homebrew 进行安装:

brew cask install iterm2

iTerm2 最常用的主题是 Solarized Dark theme下载地址

下载的是压缩文件,解压一下,然后打开 iTerm2偏好设置界面,然后Profiles -> Colors -> Color Presets -> Import,选择刚才解压的solarized->iterm2-colors-solarized->Solarized Dark.itermcolors文件,导入成功,最后选择Solarized Dark 主题,就可以了

配置 Oh My Zsh

Oh My Zsh 是对主题的进一步扩展,一键安装:

sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

安装好之后,需要把 Zsh 设置为当前用户的默认 Shell,这样新建标签的时候才会使用 Zsh,Mac默认使用的是bash

chsh -s /bin/zsh

注意,bash默认加载的文件是.bash_profile,而zsh默认.zshrc,相关配置也是在这两个文件中

这两个都是隐藏文件,Mac上显示隐藏文件的快捷键是shift+commond+. ,不显示隐藏文件再敲一次快捷键即可

然后编辑vim ~/.zshrc,将主题设置为ZSH_THEME="agnoster"

agnoster是比较常用的 zsh 主题之一,你可以挑选你喜欢的主题,zsh 主题列表下载

使用上面的主题,需要 Meslo 字体支持,要不然会出现乱码的情况,字体下载地址

下载好之后,直接在 Mac OS 中安装即可。

然后打开 iTerm2偏好设置,然后Profiles -> Text -> Font -> Chanage Font,选择 Meslo LG M Regular for Powerline 字体

选择什么样的字体和大小可以随意设置

下面设置声明高亮

使用Homebrew安装

brew install zsh-syntax-highlighting

安装成功之后,编辑vim ~/.zshrc文件,在最后一行增加下面配置:

source /usr/local/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

再之后可以添加自动建议填充功能,这个功能比较实用,可以快读的敲出命令

配置步骤,先克隆zsh-autosuggestions项目,到指定目录:

git clone https://github.com/zsh-users/zsh-autosuggestions ~/.oh-my-zsh/custom/plugins/zsh-autosuggestions

然后编辑vim ~/.zshrc文件,找到plugins配置,增加zsh-autosuggestions插件。

iTerm还可以额外设置很多很多功能,可自行百度

下面列举一些iTerm2的快捷命令

命令 说明
command + t 新建标签
command + w 关闭标签
command + 数字 command + 左右方向键 切换标签
command + enter 切换全屏
command + f 查找
command + d 垂直分屏
command + shift + d 水平分屏
command + option + 方向键 command + [ 或 command + ] 切换屏幕
command + ; 查看历史命令
command + shift + h 查看剪贴板历史
ctrl + u 清除当前行
ctrl + l 清屏
ctrl + a 到行首
ctrl + e 到行尾
ctrl + f/b 前进后退
ctrl + p 上一条命令
ctrl + r 搜索命令历史

注意,如果使用zsh想完全兼容bash,可以在.zshrc文件最后加上一句source ~/.bash_profile即可

goshell安装

打开安装包,要按住commond再拖动go2shell到finder中去,Mac10.10之后需要按住commond键

安装完打开选择iTerm2

安装goshell可能会遇到一些问题,比如文件夹右上角显示问好之类的,这时候要点击uninstall然后再重新install


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

配置Cycript环境及部分常用命令


Cycript 是一个能够理解Objective-C语法的javascript解释器,让开发者在命令行下和应用交互,在运行时查看和修改应用,一般用于动态调试应用

1.到官网下载并拖到具体位置即可

2.打开.bash_profile做相应配置

如果使用的是iTerm2和on my zsh组合,又没有设置兼容bash,则需要在.zshrc文件下配置

3.在终端或iTerm2输入cycript验证是否配置成功

使用commond+D退出,我在公司的电脑上验证通过,但是在自己的电脑上却报错

1
2
3
dyld: Library not loaded: /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/libruby.2.0.0.dylib
Referenced from: /Users/devzkn/Downloads/cycript_0.9.594/Cycript.lib/cycript-apl
Reason: image not found

这个错误是因为ruby版本太高导致

解决方法:

  • 查看电脑ruby版本,我的版本是2.3
    • cd /System/Library/Frameworks/Ruby.framework/Versions/
    • ls
  • 关闭系统的SIP

    • 在 OS X El Capitan 中有一个跟安全相关的模式叫 SIP(System Integrity Protection ),它禁止让软件以 root 身份来在 Mac 上运行,在升级到 OS X 10.11 中或许你就会看到部分应用程序被禁用了,这些或许是你通过终端或者第三方软件源安装。对于大多数用户来说,这种安全设置很方便,但是也有些开发者或者高级 Mac 用户不需要这样的设置
    • 电脑重启按住command+R,进入恢复模式
    • 打开终端,输入csrutil disable,重启
    • 如果想打开SIP,重复上两步,命令改为csrutil enable
  • 直接复制一份,改为2.0即可

    • sudo mkdir -p /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/

    • sudo ln -s /System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/lib/libruby.2.3.0.dylib /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/libruby.2.0.0.dylib

    • 根据每个人ruby版本不同,将上面第二条命令的/System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/lib/libruby.2.3.0.dylib中的2.3改成本机的ruby版本。
      这里不是降级ruby,只是复制一份2.0的ruby的dylib,让cycript运行起来

4.cycript部分常用命令

  • 获取项目单例: UIApp或者[uiapplication sharedApplication]
  • 格式化: .toString()
  • 根据地址获取对象: #内存地址
  • 打印当前页面view层级: UIApp.keyWindow.recursiveDescription().toString()
  • 获取下一个响应者: [#内存地址 nextResponder]
  • 查找指定类型: choose(UILabel)
  • 获取指定对象的所有属性: [#内存地址 _ivarDescription].toString()
  • 查看安装的进程: ps -e |grep /var/mobile*
  • 查看架构层级: [[UIApp keyWindow] _autolayoutTrace].toString()

如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

RunTime探究


RunTime,即运行时,是基于C语言的一套API, 也被称为iOS系统的黑魔法,在开发中占据举足轻重的作用

这里以objc_msgSend即消息发送,作为切入点

新建一个项目

控制器中调用Person方法

Person * p = [[Person alloc] init];
[p eat];

当然还可以写成

Person * p = [Person alloc];
p = [p init];
[p performSelector:@selector(eat)];

OC中调用方法,其实就是向调用对象发送消息,即iOS的消息机制,每个对象中其实都有一张映射表,这里的@selector其实是方法标识SEL,根据这个方法标识会找到方法实现的指针,即IMP,再根据IMP地址值找到具体的代码实现,SELIMP是一一对应的,上面代码如果使用RunTime应该怎么写?

要先导入头文件#import <objc/message.h>,并且在Build Settings中设置

这是因为自Xcode5之后,🍎不建议使用底层函数,需要手动开启

Person *p = objc_msgSend(Person.class, @selector(alloc));
p = objc_msgSend(p, @selector(init));
objc_msgSend(p, @selector(eat));

这么写其实还可以再深入一点

Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
p = objc_msgSend(p, sel_registerName("init"));
objc_msgSend(p, sel_registerName("eat"));

objc_getClass其实就相当于OC中的NSClassFromStringsel_registerName相当于OC中的NSSelectorFromString

其实也可以借助我们的Clang编译器查看我们底层代码的实现

可以新建一个macOS的命令行项目,在main.m文件中依然调用上面代码,使用clang -rewrite-objc main.m会生成一个main.cpp文件,这个文件下会找到最终的实现代码

我们在objc/message.hobjc/runtime文件中可以看到,系统给我们提供了众多接口,下面列举部分,仅做记录使用

// 获取实例对象的所属的类
Class object_getClass(id obj);

// 设置实例对象的所属的类
Class object_setClass(id obj, Class cls);

// 获取实例对象的所属类的类名
const char *object_getClassName(id obj);

// 返回指向给定对象分配的任何额外字节的指针
void *object_getIndexedIvars(id obj);

// 获取实例对象的成员变量
id object_getIvar(id obj, Ivar ivar);

// 设置实例对象的成员变量
 void object_setIvar(id obj, Ivar ivar, id value);

// 修改类实例的实例变量的值
Ivar object_setInstanceVariable(id obj, const char *name, void     *value);

// 获取对象实例变量的值
Ivar object_getInstanceVariable(id obj, const char *name, void     **outValue);

// 返回指定类的元类
id objc_getMetaClass(const char *name);

// 返回指定类的类定义
id objc_lookUpClass(const char *name);

// 返回实例对象的类
id objc_getClass(const char *name);

// 获取已注册的类定义的列表
int objc_getClassList(Class *buffer, int bufferCount);

// 创建并返回一个指向所有已注册类的指针列表
Class *objc_copyClassList(unsigned int *outCount);

// 获取类的类名
const char *class_getName(Class cls);

// 是否是元类
BOOL class_isMetaClass(Class cls);

// 获取类的父类
Class class_getSuperclass(Class cls);

// 设置新类的父类
Class class_setSuperclass(Class cls, Class newSuper);

// 类的版本信息
int class_getVersion(Class cls);

// 设置类的版本信息
void class_setVersion(Class cls, int version);

// 获取该类实例对象大小
size_t class_getInstanceSize(Class cls);

// 获取类中指定名称实例对象的信息
Ivar class_getInstanceVariable(Class cls, const char *name);

// 获取类成员变量的信息
Ivar class_getClassVariable(Class cls, const char *name);

// 获取整个成员变量列表
Ivar *class_copyIvarList(Class cls, unsigned int *outCount);

// 获取实例方法.
Method class_getInstanceMethod(Class cls, SEL name);

// 获取类方法.
Method class_getClassMethod(Class cls, SEL name);

// 返回方法的具体实现
IMP class_getMethodImplementation(Class cls, SEL name);

// 返回方法的具体实现
IMP class_getMethodImplementation_stret(Class cls, SEL name);

// 检查类是否响应指定的消息.
BOOL class_respondsToSelector(Class cls, SEL sel);

// 获取类方法列表.
Method *class_copyMethodList(Class cls, unsigned int *outCount);

// 检查类是否实现了指定协议类的方法.
BOOL class_conformsToProtocol(Class cls, Protocol *protocol);

// 返回类遵守的协议列表.
Protocol * __unsafe_unretained *class_copyProtocolList(Class cls,     unsigned int *outCount);

// 获取指定的属性
objc_property_t class_getProperty(Class cls, const char *name);

// 获取属性列表
objc_property_t *class_copyPropertyList(Class cls, unsigned int     *outCount);

// 创建实例对象
id class_createInstance(Class cls, size_t extraBytes);

// 在指定位置创建类实例
id objc_constructInstance(Class cls, void *bytes);

// 销毁类实例
void *objc_destructInstance(id obj);

// 创建一个新类和元类  (ClassPair:包含类和元类)
Class objc_allocateClassPair(Class superclass, const char *name,     size_t extraBytes);

// 在应用中注册由objc_allocateClassPair创建的类
void objc_registerClassPair(Class cls);

// 复制一份类
Class objc_duplicateClass(Class original, const char *name,size_t     extraBytes);

// 销毁一个类及其相关联的类
void objc_disposeClassPair(Class cls);

// 添加某个类的方法
BOOL class_addMethod(Class cls, SEL name, IMP imp,const char         *types);

// 替换某个类的方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp,const char     *types);

// 添加某个类的变量,这个方法只能在objc_allocateClassPair函数与objc_registerClassPair之间调用
BOOL class_addIvar(Class cls, const char *name, size_t size,         uint8_t alignment, const char *types);

// 添加某个类的协议
BOOL class_addProtocol(Class cls, Protocol *protocol);

// 添加某个类的属性
 BOOL class_addProperty(Class cls, const char *name, const         objc_property_attribute_t *attributes, unsigned int attributeCount);

// 替换某个类的属性
void class_replaceProperty(Class cls, const char *name, const     objc_property_attribute_t *attributes, unsigned int attributeCount);

// 通过方法名返回方法
SEL method_getName(Method m);

// 获取方法的实现地址
IMP method_getImplementation(Method m);

// 获取方法参数列表
unsigned int method_getNumberOfArguments(Method m);

// 修改方法实现
IMP method_setImplementation(Method m, IMP imp) ;

// 方法交换
void method_exchangeImplementations(Method m1, Method m2) ;

// 运行时给分类添加/删除属性
void objc_setAssociatedObject(id object, const void *key, id     value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);

具体到开发中,我们可以利用一些开放给我们的接口来实现相应的功能,下面列举几个案例

未完待续…

RunTimeDemo下载


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

RunLoop探究


RunLoop,即运行循环,你非要叫它跑圈,也不是不可以

这哥们到底是什么?

我们知道,一个程序的入口函数,也就是在main.m文件中

程序启动首先就会走到这里,这里有个自动释放池,一般在创建大量临时变量的时候会手动创建释放池,这里将整个程序运行期间都置于这个释放池内,程序运行结束之时,自动释放池结束,内存回收。这个自动释放池内,就一句代码,这是什么意思?进入UIApplicationMain函数内可以发现,这个函数返回值是int型。那我们换种写法

运行起来,会发现第十六行并不会执行,代码停留在了第十五行,不会再继续往下执行,也就是说UIApplicationMain函数一直没有返回。我们的程序一直是在主线程上执行的,我们打印下看看

NSLog(@"thread=%@",[NSThread currentThread]);

2018-03-30 22:53:12.821131+0800 RunLoopDemo[74265:4182972] thread=<NSThread: 0x604000066180>{number = 1, name = main}

的确,当前就是在主线程,这是必然的。整个项目运行期间,主线程都不会退出,一旦退出,程序肯定就挂了。究其原因,是因为UIApplicationMain函数内开启了一个死循环,用来保住主线程的生命,程序处于运行状态,使得整个项目运行期间,主线程都不会退出。这个死循环,其实就是RunLoop,RunLopp就是个死循环。这个默认启动的RunLoop是跟主线程相关联的。RunLoop这么设计,它的作用是什么呢?

  • 保证线程不退出,即程序的持续运行
  • 监听事件,询问是否有事件发生,从事件队列里取出事件并处理(网络、触摸、时钟等事件)
  • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息

那就写个代码玩一下试试看

- (void)viewDidLoad {
    [super viewDidLoad];

    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0     target:self selector:@selector(timerMethod) userInfo:nil     repeats:YES];
}

- (void)timerMethod {
    static int a = 0;
    a++;
    NSLog(@"a=%zd",a);
}

一个很简单的时钟事件,这个时候你会发现方法并没有走。上面已经提到,RunLoop用来监听事件,需要将timer加到RunLoop中去

[[NSRunLoop currentRunLoop] addTimer:timer     forMode:NSDefaultRunLoopMode];

注意RunLoop无法手动创建,只能获取,不过其实第一次获取就是创建。这里面有个mode属性,暂时写默认,这时候运行程序,正常运行。这时候我们往控制器上拖个textView, 运行

问题出来了,启动后时钟事件正常运行,但是一旦拖动textView,时钟事件就停止打印,不拖动textView,时钟事件又正常运行。这是什么情况?

上面我们将timer丢给RunLoop的时候,使用的模式modeNSDefaultRunLoopMode默认模式。

iOS中有两套API来访问和使用RunLoop对象,分别是OC语言中的Foundation框架(NSRunLoop)和C语言中的Core Foundation框架(CFRunLoopRef),NSRunLoop是基于CFRunLoopRef的一层OC包装

RunLoop文档

CFRunLoopRef文档

RunLoop其实共五种模式

  • NSDefaultRunLoopMode
  • UITrackingRunLoopMode
  • NSRunLoopCommonModes
  • NSConnectionReplyMode
  • NSModalPanelRunLoopMode

NSDefaultRunLoopMode模式就是RunLoop的默认模式,而UITrackingRunLoopMode模式则是UI模式,UI模式只能被触摸事件唤醒,所以优先级最高。这里第三种模式NSRunLoopCommonModes严格来说不是一种模式,而是一种占位模式,既能起到默认模式的效果又能起到UI模式的效果。第四种和第五种模式分别是在刚启动进入App时进入的第一个Mode,启动后就不再使用,和接受系统时间的内部Mode,作为开发者这两种模式基本不会使用到,

Core Foundation中关于RunLoop有五个类

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

CFRunLoopRef对应的就是NSRunLoop,CFRunLoopModeRef代表上面说到的RunLoop的几种运行模式,CFRunLoopSourceRef是事件源,或者说是输入源,现在事件源分为两种,非基于PortSource0和基于PortSource1,基于Port是指 通过内核和其他线程通信,接收、分发系统事件,CFRunLoopTimerRef是基于事件的触发器,相当于NSTimer,CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变,可以监听的时间点有以下几个

  • kCFrunLoopEntry (1UL << 0) 即将进入Loop
  • kCFRunLoopBeforeTimers (1UL << 1) 即将处理Timer
  • kCFRunLoopBeforeSources (1UL << 2) 即将处理Source
  • kCFRunLoopBeforeWaiting (1UL << 5) 即将进入休眠
  • kCFRunLoopAfterWaiting (1UL << 6) 刚从休眠中唤醒
  • kCFRunLoopExit (1UL << 7) 即将退出Loop
  • kCFRunLoopAllActivities 所有状态

上面我们是把时钟加到默认模式下,现在我们加在UI模式看下效果

[[NSRunLoop currentRunLoop] addTimer:timer     forMode:UITrackingRunLoopMode];

再次运行程序

问题又出来了,当我们拖到textView时,时钟事件可以正常打印,但是一旦停止就不再打印了。这是因为,时钟是加到RunLoopUI模式下的,而UI模式只有在触摸屏幕的时候才会唤醒,而没有触摸屏幕的时候RunLoop则处于休眠状态,所以才会出现此问题。怎么才能解决这个问题呢?将这个时钟既加到默认模式下,又加到UI模式下

 [[NSRunLoop currentRunLoop] addTimer:timer     forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer     forMode:UITrackingRunLoopMode];

这要添加两次,而🍎肯定考虑到了这点,所以有了NSRunLoopCommonModes这种占位模式

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

再次运行,会发现上述问题都得到了解决,拖拽不再影响时钟方法的执行了。简单画个图

这是🍎官网的图

上面提到,RunLoop的作用之一就是监听事件并执行,事件可以有很多种,比如时钟事件、监听事件、网络事件等等。UI模式下的这些事件必须通过触摸才能唤醒RunLoop去执行,执行完之后RunLoop就处于休眠状态,循环往复。而默认模式下,只要有这些事件,就会及时通知RunLoop去执行。

继续深究,如果这个事件是个耗时操作会怎样?我们在timerMethod这个时间下模拟一下

[NSThread sleepForTimeInterval:1.0];

我让它每次睡个2秒钟再执行

问题又出来了,你会发现UI被卡住了,因为当前是在主线程上执行,每次都强迫它睡2秒,这绝壁会卡住。既然这样,那就让这个耗时操作放在子线程去执行

- (void)viewDidLoad {
    [super viewDidLoad];
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
          NSLog(@"111");
    }];
    [thread start];
}

- (void)timerMethod {
    NSLog(@"222");
    [NSThread sleepForTimeInterval:2.0];
    static int a = 0;
    a++;
}

这时候运行会发现111尽管打印了,但是222没有打印,这说明timerMethod方法并没有执行,这是什么原因?这时候我们自定义一个FN_Thread

@implementation FN_Thread
- (void)dealloc {
    NSLog(@"dealloc");
}

NSThread替换成FN_Thread

 FN_Thread *thread = [[FN_Thread alloc] initWithBlock:^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        NSLog(@"111");
    }];
[thread start];

再次运行,会发现打印完111,接着走进了FN_Thread中的dealloc方法,这说明这个线程对象thread被释放了,既然被释放了,那这个线程上的timeMethod方法自然就不会执行了。那我们强引用这个线程对象试一下,加上对应代码

@property (nonatomic, strong) FN_Thread *thread;

_thread = thread;

再次运行,会发现FN_Thread类中的dealloc方法不走了,但是timeMethod方法依然没有执行。线程对象没有释放,当前线程上的时钟方法又没有走,这又是什么原因?再加一个方法看一下

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"thread=%@",_thread);
    [_thread start];
}

运行,触摸屏幕的时候再次开启这个线程,开启前打印一下

这个线程对象还在,但是开启会崩

这是因为,这个线程已经挂了,已经被释放了,肯定无法被开启,但是这个OC对象thread还在,因为被我们强引用了。强引用线程对象,并不会阻止线程的释放。所以会出现上述情况。事实上,子线程block内的代码执行完线程就会被释放,要想线程不被释放。就必须让线程有执行不完的任务。那我们可以手动让线程一直在执行,在block内加如下代码

while (true) {     }

这时候会发现程序可以正常运行了。但这并不是正确的解决方法。正确的姿势应该是将事件加到RunLoop中,然后开启它,因为


每个线程都对应一个RunLoop,但是除了主线程,所有子线程的RunLoop默认都没有开启,需要手动开启

NSLog(@"111");代码前加上

[[NSRunLoop currentRunLoop] run];

再次运行,会发现时钟方法timeMethod终于可以正常运行了,但是你会发现NSLog(@"111");这句代码又没有走了?这是因为NSRunLoop是个死循环,下面的代码根本不会执行。好在,RunLoop还提供给了一个指定线程运行时间的方法[[NSRunLoop currentRunLoop] runUntilDate:];,毕竟一开启就停不下来,这操作很难接得住

这里就让线程存活指定时间感受一下

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];

再次运行,一切都很明了了

这里如果想强制退出子线程,使用[NSThead exit],不过并不建议这么做

有必要强调一下,如果在主线程调用[NSThead exit]会怎样?

你会发现程序并不会挂掉,但是主线程卡死了,子线程仍然在继续。其实在CPU眼里,主线程和其他子线程一样,一样可以强退

iOS里有个peformSelector方法,我们看一下

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    
    FN_Thread *thread = [[FN_Thread alloc] initWithBlock:^{
        NSLog(@"touch");
    }];
    [thread start];
    [self performSelector:@selector(performMethod) onThread:thread withObject:nil     waitUntilDone:NO];
}

- (void)performMethod {
    NSLog(@"performMethod");
}

这时候你会发现performMethod方法根本就没走,performSelector就是线程间的通信,performMethod是在子线程thread上执行的,thread线程的block代码走完线程就挂了,所以不会走进performMethod方法,需要加上下面这句代码保住子线程的生命

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];

这里的performSelector事件就是上面提到的RunLoop处理的Source事件源。如果是performSelectorOnMainThread方法就不需要开启RunLoop,因为主线程的RunLoop在程序启动之时就已经默认开启了

NSTimer作为定时器有时候并不是十分精准的,可能会受到RunLoop的运行模式的影响,再加上其本身的精度有限, 可以使用GCD的定时器作为代替,精度更高,且不会受到RunLoop的影响,因为其是基于C语言的

int count = 0;
- (void)GCDTimer {
    // 获得队列
    dispatch_queue_t queue = dispatch_get_main_queue(); //     dispatch_get_global_queue(0, 0);
    // 创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    self.timer = timer;
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC);
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(timer, start, interval, 0);
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"count=%zd",count);
        count++;
    });
    // 启动定时器
    dispatch_resume(timer);
    // 取消定时器
    //dispatch_cancel(self.timer);
}

上面提到了RunLoop中的TimerSource,还有一个Observer,监听者,是用来监听RunLoop所处的状态,OC中无法创建,只能使用C语言

- (void)addOberser {
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"activity = %zd", activity);
    });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer,     kCFRunLoopDefaultMode);
    CFRelease(observer);
}

可以看到RunLoop的运行逻辑,每次都是从睡眠中唤醒,处理一些事件,最后处于休眠状态。监听状态变化的block中可以根据自己需求定义一些处理事件

在项目中经常会遇到加载图片的需求,现在一般都使用SDWebImage库,如果不使用这个库怎么处理?

主要逻辑就是将一个个图片下载任务放入队列,下载成功存入内存缓存,然后写入沙盒,下次再次显示时先从内存缓存里获取,获取不到则从沙盒获取,依然获取不到则进行下载任务。其实使用SDWebImage库也依然会进行这些操作。

如果,这些下载的图片非常的大,分辨率比较高,你会发现这时候滑动的时候会有明显卡顿,是因为即便下载是在子线程,但是在渲染的时候图片会解压缩,这是在主线程进行的,是个耗时的操作,图片分辨率高的情况下卡顿尤其明显。这些下载下来的图片是在一次RunLoop循环的时候渲染显示,如果一次RunLooop循环只处理一张图片,卡顿就会得到有效解决,这需要用到Observer来监听RunLoop的状态,RunLoop在处理完事件会进入休眠状态,那就每次监听在RunLoop进入休眠状态前处理这些图片,直到没有图片处理然后进入休眠状态

RunLoopDemo

LoadManyPictures

LoadBigImage


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

GCD探究


要了解GCD(Grand Central Dispatch),先从最基本的一些概念作为切入点

1.概念理解

进程

只指在系统中正在运行的一个应用程序,比如同时打开了Xcode和Sourcetree, 系统会分别启动两个进程,Mac上有个活动监视器,可以查看已打开的进程并可以杀死进程。每个进程之间是独立的,均运行在其专用且受保护的内存空间内


线程

进程想要执行任务,必须得有线程,每个进程至少要有一条线程,一个进程所有的任务都在线程间执行


线程的串行

一个线程中任务的执行是串行的,如果要在一个线程中执行多个任务,那么只能一个一个的按顺序执行这些任务,也就是说,在同一时间内,一个线程只能执行一个任务


多线程

一个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务,多线程可以提升程序的工作效率。其实,在同一时间,CPU只能处理一条线程,只有一条线程在工作,多线程并发,只是CPU在快速的在多条线程之间调度。如果同时执行的线程过多,反而会拖慢程序的执行效率,因为切换线程也同样需要消耗资源,每条线程被调度的频次也会降低,所以多线程也并非是越多越好


多线程的优缺点

可以适当提高程序的执行效率和资源利用率。但是创建线程也是有开销的,以iOS为例,成本主要包括:内核数据结构(大约1kb),栈空间(子线程512KB,主线程1MB,也可以通过setStackSize设置,但必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的时间。如果开启大量线程,会降低程序性能,线程越多,CPU在地调度线程上的开销就越大,也有可能会导致程序设计更加复杂,比如线程之间通信、数据共享等


主线程

iOS程序运行后,默认会开启一条线程,称为主线程或UI线程,用来显示刷新UI界面,处理一些点击、滚动或拖拽等UI事件,但是尽量不要将比较耗时的操作放到主线程,否则会卡主主线程,影响UI流畅度

2.多线程实现

技术方案 简介 语言 线程声明周期 使用频率
pthread 1.一套通用的多线程API
2.适用于Unix\Linux\Windows等系统
3.跨平台\可移植
C 程序员管理 几乎不用
NSThread 1.使用更加面向对象
2.简单易用,可直接操作县城对象
OC 程序员负责创建 偶尔使用
GCD 1.旨在替代NSThread等等多核技术
2.充分利用设备的多核
C 自动管理 经常使用
NSOperation 1.基于GCD
2.比GCD多了一些更简单的实用功能
3.使用更加面向对象
OC 自动管理 经常使用
  • pthread

在查看CPU的使用上也是可以直观看到是有多条线程在同时执行

  • NSThread

一个NSThread对象就代表一条线程,线程一旦死亡不能再开启任务

线程状态

多线程的安全隐患

多个线程访问同一块资源可能发生安全隐患

这个时候就需要加锁,即互斥锁

锁定一段代码只能使用一把锁,使用多把锁匙无效的,互斥锁能有效防止因多线程抢夺同一块资源而造成的数据安全问题,但是却需要消耗大量的CPU资源,注意,在主线程加锁是没有意义的

OC在定义属性的时候有nonatomicatomic,即非原子属性和原子属性,默认是原子属性。原子属性,为setter方法加锁,非原子属性则不会加锁 。但是虽然atomic线程安全,却需要消耗大量的资源。nonatomatic非线程安全,适合内存小的移动设备。所以我们iOS开发,建议所有属性都声明为nonatomatic,在需要线程安全的地方再手动加锁

在一个进程中中,线程往往不是孤立存在的,多个线程之间经常需要通信,经典的图片下载显示就需要线程间的通信,线程间通信通常可以使用performSelector,还可以使用NSPortNSMessagePortNSMachPort等端口,不过使用不多

  • GCD

Grand Central Dispatch,很⑥的中枢调度器,纯C语言,提供了很多非常强大的函数

GCD的优势

  • GCD是🍎公司为多核的并行运算提出的解决方案

  • GCD会自动利用更多的CPU内核(双核、四核)

  • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)

  • 攻城狮只需要告诉GCD要执行什么任务,不需要编写任何线程管理代码

GCD的两个核心概念

  • 任务: 执行什么操作

  • 队列: 用来存放任务

GCD的使用

  • 定制任务

    • 确定想做的事情
  • 将任务添加到队列中

    • GCD会自动将队列中的任务取出,放到对应的线程中执行

    • 任务的取出遵循队列的FIFO原则:先进先出,后进后出

GCD中有两个用来执行任务的常用函数

  • 同步 diapatch_sync(dispatch_queue_t queue, dispatch_block_t block)

    • queue: 队列

    • block: 任务

  • 异步 dispatch_async(dispatch_queue_t queue,dispatch_block_t block)

注意,同步只能在当前线程中执行任务,不具备开启新线程的能力
异步可以在新的线程中执行任务,具备开启新线程的能力

GCD的队列类型

  • 并发队列(Concurrent Dispatch Queue

    • 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)

    • 并发功能只有在异步(dispatch_async)函数下才有效

  • 串行队列

    • 让任务一个接着一个的执行(一个任务执行完毕后,再执行下一个任务)

上面提到的几个术语容易混淆:同步、异步、并发、串行

  • 同步和异步主要影响:能不能开启新的线程
    同步: 只是在当前线程中执行任务,不具备开启新线程的能力

    异步: 可以在新的线程中执行任务,具备开启新线程的能力

  • 并发和串行的主要影响: 任务的执行方式

    • 并发:多个任务并发(同时)执行

    • 串行:一个任务执行完毕后,再执行下一个任务

GCD的各种队列

并发队列 手动创建的串行队列 主队列
同步(sync) 没有开启新线程
串行执行任务
同左 同左
异步 (async) 有开启新线程
并发执行任务
有开启新线程
串行执行任务
没有开启新线程
串行执行任务

注意,使用sync函数往当前串行队列中添加任务,会卡住当前串行队列

线程间通信

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEF    AULT, 0), ^{
        dispatch_async(dispatch_get_main_queue(), ^{
        });
//        dispatch_sync(dispatch_get_main_queue(), ^{
//        });
        NSLog(@"");
    });

GCD中常用函数

dispatch_barrier_async(dispatch_queue_t queue,dispatch_block_t block)

在前面任务结束后他才执行,而且它后面的任务等它执行完成后才会执行, 这个queue不能是全局的并发地列,需手动创建

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<#delayInSeconds#> * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ })

延迟执行, 不一定非要放到主线程

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});

一次性代码,该函数能保证某段代码在程序运行过程中只被执行一次

// .h
#define FNSingletonH + (instancetype)sharedInstance;

// .m
#define FNSingletonM \
static id _instace; \
 \
+ (instancetype)allocWithZone:(struct _NSZone *)zone \
{ \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
        _instace = [super allocWithZone:zone]; \
    }); \
    return _instace; \
} \
 \
+ (instancetype)sharedInstance \
{ \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
        _instace = [[self alloc] init]; \
    }); \
    return _instace; \
} \
 \
- (id)copyWithZone:(NSZone *)zone \
{ \
    return _instace; \
}

常用于单例模式,可以保证在程序运行过程中,一个类只有一个实例,而且该实例易于供外界访问,从而方便控制了实例个数又节约系统资源

dispatch_apply(<#size_t iterations#>, <#dispatch_queue_t      _Nonnull queue#>, <#^(size_t)block#>)

快速迭代遍历

 dispatch_group_t group = dispatch_group_create(); 
 dispatch_queue_t queue =     dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_group_async(group, queue, ^{
   });
   dispatch_group_async(group, queue, ^{
 });
dispatch_group_notify(group, queue, ^{
   });

队列组,等二个异步操作都执行完毕后,再回到指定线程操作



未完待续…


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

复杂UITableView的相关优化


仅做记录使用,会比较粗糙,不过多探讨!

iOS开发中,UITableView存在于几乎每一个应用中。不是那么复杂的UITableView使用常规写法没有任何问题,但是对于复杂业务一个UITableView中存在几十个不同的UITableViewCell,再使用常规写法简直就是灾难,有些写法性能上并无问题,但是优雅度不够,维护性较差。以下从几点阐述,为了方便,多是图片

UITableVIewCell的创建

  1. 每个cell对应一个viewModel

2.每个section对应一个viewModel

UITableViewCell上的UITextField

  1. 提前创建cell,为当前cell上的textField指定好下一个响应者

2.遍历获取所有可响应键盘的textField

    #import "UIView+NextKeyBoard.h"

@implementation UIView (NextKeyBoard)

/** 获取当前视图所有可以响应键盘的textField或textView */
- (NSArray *)lvdeepResponderViews {
    NSMutableArray<UIView*> *textFields = [[NSMutableArray alloc] init];
    CGFloat height = self.frame.size.height;
    if (height > 1 && self.hidden == NO) {
        for (UIView *textField in self.subviews) {
            if ((textField) && [textField lvcanBecomeFirstResponder] && [textField isUserInteractionEnabled] && ![textField isHidden] && [textField alpha] != 0.0) {
                [textFields addObject:textField];
            }

            if (textField.subviews.count && [textField isUserInteractionEnabled] && ![textField isHidden] && [textField alpha]!= 0.0) {
                [textFields addObjectsFromArray:[textField lvdeepResponderViews]];
            }
        }
    }

    // 按照位置y排序 如果y相等 则按照x的大小排序
    return [textFields sortedArrayUsingComparator:^NSComparisonResult(UIView *view1, UIView *view2) {

        CGRect frame1 = [view1 convertRect:view1.bounds toView:self];
        CGRect frame2 = [view2 convertRect:view2.bounds toView:self];

        CGFloat x1 = CGRectGetMinX(frame1);
        CGFloat y1 = CGRectGetMinY(frame1);
        CGFloat x2 = CGRectGetMinX(frame2);
        CGFloat y2 = CGRectGetMinY(frame2);

        if (y1 < y2) {
            return NSOrderedAscending;
        } else if (y1 > y2) {
            return NSOrderedDescending;
        } else if (x1 < x2) {
            return NSOrderedAscending;
        } else if (x1 > x2) {
            return NSOrderedDescending;
        } else {
            return NSOrderedSame;
        }
    }];
    return textFields;
}

/** 判断当前的输入控件是否可以有输入响应的事件 */
- (BOOL)lvcanBecomeFirstResponder {
    BOOL canBecomeFirstResponder = NO;
    if ([self isKindOfClass:[UITextField class]]) {
        canBecomeFirstResponder = [(UITextField*)self isEnabled];
    } else if ([self isKindOfClass:[UITextView class]]) {
        canBecomeFirstResponder = [(UITextView*)self isEditable];
    }

    if (canBecomeFirstResponder) {
        canBecomeFirstResponder = ([self isUserInteractionEnabled] && ![self isHidden] && [self alpha] != 0.0 && ![self isAlertViewTextField]  && ![self isSearchBarTextField]);
    }

    return canBecomeFirstResponder;
}

- (BOOL)isAlertViewTextField {
    UIResponder *alertViewController = [self lvviewController];
    BOOL isAlertViewTextField = NO;
    while (alertViewController && isAlertViewTextField == NO) {
        if ([alertViewController isKindOfClass:[UIAlertController class]]) {
            isAlertViewTextField = YES;
            break;
        }

        alertViewController = [alertViewController nextResponder];
    }
    return isAlertViewTextField;
}

- (BOOL)isSearchBarTextField {
    UIResponder *searchBar = [self nextResponder];
    BOOL isSearchBarTextField = NO;
    while (searchBar && isSearchBarTextField == NO) {
        if ([searchBar isKindOfClass:[UISearchBar class]]) {
            isSearchBarTextField = YES;
            break;
        } else if ([searchBar isKindOfClass:[UIViewController class]]) {
            break;
        }

        searchBar = [searchBar nextResponder];
    }
    return isSearchBarTextField;
}

- (UIViewController *)lvviewController {
    UIResponder *nextResponder =  self;
    do {
        nextResponder = [nextResponder nextResponder];
        if ([nextResponder isKindOfClass:[UIViewController class]]) {
            return (UIViewController*)nextResponder;
        }
    } while (nextResponder != nil);
    return nil;
}

@end

UITableviewCell高度计算

  1. 提前创建好的cell直接获取cell的高度 (适用于相同样式cell数量相对较少的情况,对应上面的每个section对应一个viewModel情况)

  2. 通过协议或者上图展示的viewModel,提前计算好cell的高度并缓存

UITableviewCell布局

  1. 每个section对应一个viewModel情况

2.每个cell对应一个model对象

UITableviewCell上事件响应

1.delegate,block,RacSubject

2.使用响应者链ResponderChain

内存问题

以上


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

2017


有人问我,2017年你过得还好吗

什么叫好,什么叫不好?这要看怎么比了,如果跟过去的16年15年比,我敢说不好吗

不好意思,我敢!确实不好

早上闹钟准时响起,忘了关了,反手一个动作,熟练干脆,再醒时已快中午了

打开微信,打开QQ(原谅我还在使用QQ,在下是个念旧的人), 都在狂晒十八岁的照片,晒之前可能还要思考哪张照片看起来没那么傻,哪张看起来更青春,晒个照片徒增不少烦恼

我就好了,毕竟还得两年才能到十八岁,嘴角不经意上扬了一下,不知道有没有四十五度

挺好的,虽然一个个都像是在给葬爱家族开年会,稚嫩青涩但又不失朝气,像是刚出炉的蛋糕,即使不小心被人用指头摁下去,也会很快恢复到之前光滑无痕的模样

这盛世美颜,难以置信

时至年末,又到了坐下来清算flag的时候了

尽管,我们总是身体力行,坚定且执着的反复验证着同一个真理:当初立下的flag,只是用来方便日后推倒的

想起曾经斗志满满、正气凛然的在朋友圈立下的那些flag,有没有觉得现在脸上有些疼

没错,就是这个感觉。立flag的时候总是有种舍我其谁的兴奋感油然而生,立的好的话可能感觉还有点秒不而言,与我而言,通常帅不过三秒

其实在朋友圈打嘴炮立flag这事儿,不必太在意,毕竟生活里总是需要大大小小的仪式感和目标来约束放纵的自己

年轻时我立下了无数flag,就如同我说过想戒熬夜一样多,正是这些flag的存在才让我成为更好现在的自己

“这次考试怎么也要拿个第一名,再不济也要前三”

“大学非清北不上,再不济也要985、211”

“我要连拿四年奖学金,这不是开玩笑”

“今年要再看十本纸质书,读书使我快乐”

“我要开始炫‘腹’,练出八块腹肌,让运动成为一种习惯,健身成为一种日常”

“我要让余太不仅垂涎与我的才华,更要拜倒在我的厨艺之下”

“世界这么大,我要去看看,我要带余太去浪漫的土耳其,去看更广阔的世界”

“这个双十一我就看看什么都不买”

“教师资格证、银行从业资格证、证券资格证,一证在手,天下我有”

“每天专业技能学习,谁也不能阻止我学习的脚步”

“每天早点睡,早起两小时”

“今年先挣它一个亿,目标不在小,在于积累”

….

这哪是立flag啊,有些就是在白日做梦

鲁迅说过:“一千个人有一千个flag,倒或者不倒,它都在那里,见证你的酸甜苦辣”

不管曾经立下的flag,是依然屹立在风中,还是已经飘然倒下,但不可否认的是一直在进步,只是离预期目标还有点远而已

既然有些东西必须要经历,又何必畏手畏脚?都是有故事的人,谁能输给谁

想我17年立的几个flag,简单、易实现、没难度。预计存款一万,结果负债九万, 还差十万就达标了,差距不是很大

讲道理,不必在意那些打脸的flag,毕竟真正的勇士,敢于直面打脸的flag,履立屡破,屡破屡立

新的一年,我的flag很简单

过得比17年好就行

如果没做到,明年我还得这么立

看起来都一片歌舞升平,腾讯新闻很合时机的推送了三分钟的2017大事件

既有”中国首艘国产航母下水”、”国产大飞机C919成功首飞”等振奋国人的成就,又有”杭州保姆纵火案”、”江哥案”等让人扼腕叹息的悲剧

每年的这个时候,我觉得有篇文章都值得我们反复诵读,永远致敬

原文刊登于1999年的《南方周末》,题为《总有一种力量让你泪流满面》,别说我没告诉你

登过顶峰方知初心可贵,有过低谷更知人间冷暖

没有谁能留住时间,唯有努力才不会让光阴虚度

再见2017

您好2018


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

ReactiveCocoa探究-编程思想


谈到ReactiveCocoa(RAC),说它比较火似乎也没有那么火,一些大厂用的也并不多,说它不火貌似又经常看到或听到一些鼓吹RAC的文章和言论。尽管它很强大,但是也并没有很普及,即便它很强大!

在了解ReactiveCocoa前,先简单粗略回顾一下block的几种常用用法及几种常见的编程思想。

block常用用法

1. block作为对象的属性

新建一个Single工程,创建一个Person类,在Person类中声明一个block。系统有个内置的block代码块

注意,在非ARC环境下block需要使用copy声明,使用copy的原因,是把block从栈区拷贝到堆区,因为栈区中的变量出了作用域之后就会被销毁,无法在全局使用,所以应该把栈区的属性拷贝到堆区中全局共享,这样就不会被销毁了,在MRC手动管理的就是堆区,不需要系统管理,MRC环境必须使用copy把变量拷贝到全局的堆区。而如果在ARC环境下就可以不使用copy修饰,因为ARC下的属性本来就在堆区。所以现在在默认是ARC环境下依然沿袭了之前的使用方式copy,使用strong也没有问题。

在Person类中声明一个block

@property (nonatomic, strong) void(^myBlock)(void);

在控制器中持有一个Person对象,并声明一个block接受Person的这个block属性

self.p = [[Person alloc] init];

void(^testBlock)(void) = ^() {

 NSLog(@"block as params");

    };

self.p.myBlock = testBlock

在点击屏幕的时候实现这个block

self.p.myBlock();

2.block作为方法的参数

同样在Person类中声明一个带有block参数的方法并实现

- (void)eat:(void(^)(NSString *someThing))block;

- (void)eat:(void(^)(NSString * someThing))block {

block(@"delicious");

}

在控制器中调用,其中block中的已知数据类型的参数是Person对象提供给外界使用的,AFNetworing中返回的block结果就是如此。

[self.p eat:^(NSString *someThing) {
       NSLog(@"eat %@",someThing);
   }];

3.作为方法的返回值

block作为方法的返回值这种用法开发中其实并不常用,但是一些第三方,最典型的常用约束库
Masonry库就将block作为方法的返回值来使用。

同样在Person中声明一个run方法并实现,将block作为方法的返回值

- (void(^)(int m))run;

- (void(^)(int m))run {
    return ^(int m) {
        NSLog(@"run %d",m);
    };
}

在控制器找那个可以这样调用

- (void)methodTree {
    void(^block)(int m) =  self.p.run;
    block(100);
}

并没需要一个block先接收再实现,可以直接调用

[self.p run](100);

有没有觉得这种调用怪怪的,其实就是调用方法返回一个block再调用 这个block,block的调用方式就是block()

有没有更简单的写法呢?答案是有!

self.p.run(100);

为什么可以使用点语法调用?

在OC中只有形如get类型的方法才可以使用点语法点出来,否则是能使用形如[self run]这种加空格的调用方法。而这种将block作为返回值的方法语法类型和get方法相同,所以可以使用点语法调用,但是如果将block作为方法的返回值,而且这个方法又有其他参数,是无法通过点语法调用的,因为系统判断这不是get方法,get方法是没有参数的。

Demo地址

编程思想

了解几种常规的编程思想,如链式编程,响应式编程,函数式编程等。

1. 链式编程思想

链式编程就是可以通过”点”语法,将需要执行的代码块连续的书写下去,使得代码简单易读,书写方便。典型的使用链式编程思想的有Masonry库,通过分析Mansory库的使用机制来了解一下链式编程。

同样新建一个Single工程,添加一个View

UIView *redView = [[UIView alloc] init];

redView.backgroundColor = [UIColor redColor];

[self.view addSubview:redView];

[redView mas_makeConstraints:^(MASConstraintMaker *make) {

    make.left.top.equalTo(@100);

    make.right.bottom.equalTo(@-100);

}];

上面的MASConstraintMaker,我们称之为约束制造者,这里面就将block作为方法的参数来使用了,为什么对redView添加约束,而block里面使用的确是约束制造者呢?

点进mas_makeConstraints这个方法看一看

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

首先这个方法关掉了老式的自动布局self.translatesAutoresizingMaskIntoConstraints = NO;,防止和Autolayout冲突,然后创建一个约束制造者并返回。而在创建这个约束制造者的时候将self作为参数传入,[[MASConstraintMaker alloc] initWithView:self];,而这个self就是redView,即这个方法的调用者。到这个方法里面看一下

- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;

    self.view = view;
    self.constraints = NSMutableArray.new;

    return self;
}

可见这个约束制造者将view作为属性绑定起来了,到[constraintMaker install];这个方法里面
可以看到就是我们不愿意写的各种
[NSLayoutConstraint constraintWithItem: attribute: relatedBy: toItem: attribute: multiplier: constant:]

通过上面流程我们知道mas_makeConstraints的执行流程

1.创建约束制造者MASConstraintMaker,并且绑定控件,生成一个保存所有约束的数组

2.执行mas_makeConstraints传入的Block

3.让约束制造者安装约束!

3.1 清空之前的所有约束

3.2 遍历约束数组,一个一个安装

接下来看一下block中的代码make.left.top.equalTo(@100);,这里使用的即是链式编程,我们发现make.left后依然可以.top,.right,为什么可以这样写?点进去可以看到

- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

这是一个get方法,上文有提到OC中只有形如get方法才可以使用点语法点出来。而这个get方法返回的是一个MASConstraint,你会发现MASConstraint类中这些全是get方法,而返回值全是调用者本身MASConstraint

随便点进去一个get方法

- (MASConstraint *)addConstraintWithLayoutAttribute:    (NSLayoutAttribute)layoutAttribute {
    NSAssert(!self.hasLayoutRelation, @"Attributes should be     chained before defining the constraint relation");

    return [self.delegate constraint:self     addConstraintWithLayoutAttribute:layoutAttribute];
}

可以发现都是通过代理来实现,最终返回的是一个约束MASConstraint,并添加到约束数组中,这也就可以解释了为什么make.left后依然可以.top,.right继续点出来,事实上只要你愿意,你可以一直这么写下去,因为返回值都是这些方法的调用者。而这也是链式编程最重要的思想特点,即:

方法的返回值必须要有方法调用者

为什么是 “要有方法调用者”而不是“要是方法的调用者”呢?

我们接着看make.left.top.equalTo(@100)中的equalTo方法

- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute,     NSLayoutRelationEqual);
    };
}

可见,这个equalTo方法返回的不是方法调用者,而是一个返回值为方法调用者的block。而我们在调用equalTo方法的时候紧跟着后面加了“(100)”,也就是说调用了这个返回的block,bclok执行后返回的又是一个方法调用者MASConstraint。也就是说,只要你愿意,在make.left.top.equalTo(@100)之后,你依然可以继续调用get方法,形如

make.left.top.equalTo(@100).right.bottom.equalTo(@-100);

只是在equalTo之后继续调用get方法,后面的约束值会覆盖掉前面的约束值。所以,理论上可以,然而并不应该这么做。

那如果我们自己也想玩一个具备链式编程特点的类该怎么写呢?记住,链式编程,方法返回值必须要有方法调用者。然后可以参考Masonary库粗略玩一下。
  1. 先创建一个计算管理器吧CalculateManager,定义简单的加减乘除方法并逐一趋实现,并声明一个属性去保存计算结果, 这些方法都有一个共同的特点,就是返回值是一个block,而block的返回值是这个类本身,这就确保了在调用方法后依然可以继续调用。
@interface CalculateManager : NSObject

@property(assign,nonatomic)double result;

+ (instancetype)sharedInstance;

-(CalculateManager *(^)(float value))add;

-(CalculateManager *(^)(float value))minus;

-(CalculateManager *(^)(float value))multi;

-(CalculateManager *(^)(float value))divis;

@end

@implementation CalculateManager

    + (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    static CalculateManager *mgr = nil;
    dispatch_once(&onceToken, ^{
        mgr = [[self alloc] init];
    });
    return mgr;
}

-(CalculateManager * (^)(float value))add{
    return ^(float value){
        _result += value;
        return self;
    };
}

-(CalculateManager *(^)(float value))minus {
    return ^(float value){
        _result -= value;
        return self;
    };
}

-(CalculateManager *(^)(float value))multi {
    return ^(float value){
        _result *= value;
        return self;
    };
}

-(CalculateManager *(^)(float value))divis {
    return ^(float value){
        _result /= value;
        return self;
    };
}

2.接下来你会发现已经可以使用链式编程了

CalculateManager *manger = [[CalculateManager alloc] init];
double result = manger.add(3).minus(3).result;

只要你愿意,依然可以在返回结果前继续点下去。但是你会发现在返回结果后没法继续调用方法了。而且这种调用依然需要创建实例,和Masonry形式依然不同,那就继续改进。在使用Masonry时你会发现任何继承自UIView的控件都可以调用mas_makeConstraints或其他方法,那说明这绝壁是个UIView的分类。仿照此思路,我们创建一个NSObject的分类,使得NSObject任何子类实例都可以调用。

 @interface NSObject (Calculate)

- (double)fn_calculate:(void(^)(CalculateManager * mgr))block;

@end

@implementation NSObject (Calculate)

- (double)fn_calculate:(void(^)(CalculateManager * mgr))block {
    CalculateManager * mgr = [CalculateManager sharedInstance];
    block(mgr);
    return mgr.result;
}

上面这个方法的参数是参数为CalculateManager的block,返回结果为CalculateManager的属性值。现在就可以这么使用了:

double result = [self fn_calculate:^(CalculateManager *mgr) {
        mgr.add(10).add(10).divis(3).multi(5.8).minus(2.2);
    }];

@end

只要你愿意,你可以一直点下去。这种写法其实在OC中非常少见,但是像Masonry一些第三方库都使用了这种思想。

Demo下载

函数编程思想(FP)

函数编程是把操作尽量写成一系列嵌套的函数或者方法调用。每个方法必须要有返回值,把函数或者block当作参数。

继续上面的例子,在CalculateManager中声明并实现一个方法

- (instancetype)manager:(double(^)(double))block;


- (instancetype)manager:(double(^)(double))block {
    _result = block(_result);
        return self;
}

这个方法参数是一个block,这个block又有返回值和参数,方法的返回值又是自己的一个实例。这里方法的返回值完全可以不是自己的实例,而是一个具体的结果。但是如果是一个具体的结果,如果这个类中有很多其他的属性,那对应的就相应有很多雷同的方法,所以返回一个实例,调用的时候可以通过这个实例去获得对应的属性值。

CalculateManager * mgr = [[CalculateManager alloc] init];
    double h_Result = [[mgr manager:^double(double parameter) {
       parameter += 10;
          parameter *= 2;
          return parameter;
   }] result];
   NSLog(@"%f",h_Result);

响应式编程思想(RP)

响应式编程不需要考虑调用顺序,只需要考虑结果。 iOS中最具代表性的就是KVO的使用。看一下简单实用。

先创建一个Person类,声明一个name属性,监听Personname属性

你会发现没什么毛病。如果这时候,我们把name属性变成一个成员变量,再去使用箭头函数方式去修改name的值

这个时候你会发现并没有走进监听函数。这和上面正常使用的时候有什么区别呢?那就是修改name属性的时候,成员变量不会调用set方法, 而使用点语法总是会调用set函数。有理由相信,使用KVO的时候重写了属性的set方法。

怎么可以重写set方法?

  1. 可以使用分类的形式重写
  2. 可以使用子类去重写

想想看,苹果有没有可能去使用分类的形式去重写? 绝壁不可能!因为如果分类中去重写属性的set方法,则原有类中即便重写了该属性的set方法也不会调用,会优先使用分类中的方法。而我们在日常开发中,经常会去重写set方法去做一些其他操作。所以只可能是第二种情况,使用子类去重写了set方法。那过程应该是怎样的呢?

  1. 自定义被监听类的子类
  2. 重写被监听属性的set方法,在内部调用super恢复父类的做法,通知观察者

还有一个问题? 如何让外界调用自定义子类的方法?所以还有最重要的一点

iOS中有个isa指针, 修改当前对象的isa指针,指向自定义的子类!

验证一下猜测,查看一下在走进监听方法后对象的isa指针指向。

知道了KVO的内部机制,那我们自己就可以写一个简单的KVO

以上面为例,监听Person类的name属性,创建一个NSObject的分类,添加监听方法并实现,最后再使用添加的这个方法监听即可。

注意在使用objc_msgSend时需要在Build Setting中将Enable Strict Checking of objc_msgSend Calls设置为NO

Demo下载


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

本站总访问量 本文总阅读量