• 主页
  • 系列总集
  • OpenCV
  • CMake
  • iOS
  • Java
  • 前端
所有文章 关于我

  • 主页
  • 系列总集
  • OpenCV
  • CMake
  • iOS
  • Java
  • 前端

设计模式的思考(一):MVC

2016-02-14

MVC模式是什么

MVC全称Model-View-Controller

  1. Model:用于管理数据
  2. View:用于展示数据
  3. Controller:用于管理事件

根据唐巧的文章介绍,MVC的概念最早出现在二十世纪八十年代的施乐帕克实验室中,就是传说中乔布斯抄袭的那个实验室。其本质是尽量高复用View和Model.

  1. View: 你如果抽象得好,那么一个 App 的动画效果可以很方便地移植到别的 App 上
  2. Model: 用来存储业务的数据的,如果做得好,它也可以方便地复用。
  3. Controller: 因为一个界面逻辑对应一个Controller,所以很难被复用

当意识到Controller很难被复用,那么设计的理念就清晰了,容易或者说需要复用的放入View和Model,Controller里尽量不要放需要复用的代码,因为Controller本身不便于复用.

Xcode中的MFC

根据苹果推荐的MFC建立方法

  1. Model: Xcode并没有推荐的,一般使用自建一个基于NSObject的类来当作Model
  2. View: Xcode推荐使用的View是xib和Storyboard,两者实质是XML文件
  3. Controller: Xcode最常见的就是ViewController类,用于控制View文件的声明周期和事件

在Xcode推荐的方式里,一般使用@IBOutlet进行关联View和Controller.但是由于好多人不喜欢使用xib和Storyboard来书写View,加上遇到批量更改或者同一管理时,使用代码更加方便,所以导致实际情况是View的内容和ViewController的内容混合在了一起.

Xcode中Model的常用方法

由于Model是自建的一个NSObject类,一般使用属性来保存数据

@property (nonatomic, copy) NSString *theme;

然后会自定义以下方法用于模型的初始化

- (instancetype)initWithDict:(NSDictionary *)dict;
+ (instancetype)createWithDict:(NSDictionary *)dict;

第一个方法是通过字典初始化模型的实例方法

- (instancetype)initWithDict:(NSDictionary *)dict {
    self = [super init];
    if (self) {
        self.theme = [dict valueForKey:@"THEME"];
    }
    return self;
}

第二个是使用字典初始化模型的类方法

+ (instancetype)createWithDict:(NSDictionary *)dict {
    return [[self alloc]initWithDict:dict];
}

如果需要对模型进行深拷贝,需要实现NSCopy协议

- (id)copyWithZone:(NSZone *)zone {
    CustomModel *newItem = [[CustomModel allocWithZone:zone] init];
    newItem.theme = self.theme;
    return newItem;
}

还可以实现逆转换方法,用于转化成通用的Dictionary或者Array

+ (NSDictionary *)convertDictFromModel:(CustomModel *)model {
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    [dict setValue:model.theme forKey:@"THEME"];
    return dict;
}

View和Controller混合在一起造成的问题

由于纯代码完成View造成了ViewController.m这个文件中会有一大部分代码是界面,这部分代码完成了本来应该用Xib和Stroyboard完成的工作,所以代码量的增加,会导致Controller显得特别臃肿,不利于代码Review和审查

为了解决这个问题,才会有人提出了MVVM这个新的的设计模式,以及我目前更喜欢的另外一种模式来解决.

MVC的理解

MVC的误区

我个人对于MVC最早的理解,认为MVC应该是完全分离的,像圈地一样把不同的东西放在不同的地方,甚至认为只要我有独立的数据管理机制,就是Model,所以我把所有界面之间的数据都通过字典和数组储存和传递.

MVC误区带来的问题

之所以我通过数组和字典进行储存数据和传递,基于的考虑是字典和数组不管在任何界面都是可以被解析的,不用加载一个自己的Class进行解析.

但是这引起了很多问题:

  1. NSDictionary和NSArray本身是不可变的,一旦init就不能对其内存进行修改
  2. 使用Key-Value键值对调用,很容易写错Key导致数据出错
  3. 每个界面都要自己解析Dictionary是什么

虽然以上三个问题都可以通过技术手段避免,但是这明显进入了为了解决一个简单的问题,引入了更复杂问题的怪圈.而最关键的问题是,使用Dictionary会影响到MVC三个模块之间的联动.

MVC的核心:联动

但是只所以被称为设计模式,其实MVC三个部分之间并不是说要做到去耦合,完全独立的关系.MVC之间应该是一种联动关系,最终为了是通过不同的方式来展示信息(数据),如下图.

Figure1

  1. Model可以作为Controller的一个属性,通过**setModel:**方法进行控制,每当Model数据被Set时就会引起Controller对View的控制

  2. 而用户对View的改变,会通过Action作用于Controller,在Action触发的函数里实际改变(Change)Model的数据.

而这一切联动,都是围绕着信息,所以说最终用户和计算机之间交流的本质就是信息.

误区不能联动举例

  • 需求

在完成一个读取本机号码做成TableView的过程中,可以通过点击Cell改变当前对象为选中或非选中状态

  • 问题1: NSDictionary无法改变

由于NSDictionary一旦init就无法改变,所以当根据动作不同改变名为”Selected”的Key值时,无法改变,这个问题可以通过NSMutableDictionary解决,但是还存在问题2

  • 问题2: 无法通过Set函数联动

解决了问题一之后,在滑动TableViewCell,由于存在TableViewCell复用

  1. 在第一行被选中,把索引第一的NSMutableDictionary的”Selected”的Key值改变
  2. 但是Cell再复用后又从选中后的View样式变回了默认的未选中的View样式
  3. 产生的原因是复用出的Cell并没有接受到Selected这个动作.

如果要使用字典和View联动,还需要自己写一套联动方法,并没有直接通过读取自身属性Model的值快.就犯了为了解决一个问题引入另外一个问题的悖论.

  • 结论

综上所述,使用自定义的基于NSObject的Model更利于理解和使用,虽然Array和Dictionary有较好的通用性,但是并不方便.

  • iOS
  • Architecture
  • Tutorial

展开全文 >>

设计模式的思考(二):MVVM

2016-02-14

MVVM模式是什么

根据唐巧的文章和Objective-C.io介绍,MVVM是最早于2005年被微软的 WPF 和 Silverlight 的架构师 John Gossman 提出,并且应用在微软的软件开发中.当时 MVC 已经被提出了 20 多年了,可见两者出现的年代差别有多大.

下面祭出一张微软大神的手绘图

Figure01

MVVM的目的是什么

在上一篇中MFC模式中提出的问题是,ViewController层过于臃肿,Objective-C.io中提出,使用MVVM可以很好的解决这个问题.

MVVM的示例

由于我本身并不经常使用MVVM的设计模式,所以并没有很有心得的代码,我们以Objective-C.io中的代码举例

首先定义一个Person

@interface Person : NSObject

- (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate;

@property (nonatomic, readonly) NSString *salutation;
@property (nonatomic, readonly) NSString *firstName;
@property (nonatomic, readonly) NSString *lastName;
@property (nonatomic, readonly) NSDate *birthdate;

@end

MVC中使用一个VC展示它

- (void)viewDidLoad {
    [super viewDidLoad];

    if (self.model.salutation.length > 0) {
        self.nameLabel.text = [NSString stringWithFormat:@"%@ %@ %@", self.model.salutation, self.model.firstName, self.model.lastName];
    } else {
        self.nameLabel.text = [NSString stringWithFormat:@"%@ %@", self.model.firstName, self.model.lastName];
    }

    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
    self.birthdateLabel.text = [dateFormatter stringFromDate:model.birthdate];
}

MVVM: 创建一个ViewModel

像MVC中Format这个逻辑,完全可以脱离VC,从而减少ViewController中的代码逻辑.

@interface PersonViewModel : NSObject

- (instancetype)initWithPerson:(Person *)person;

@property (nonatomic, readonly) Person *person;

@property (nonatomic, readonly) NSString *nameText;
@property (nonatomic, readonly) NSString *birthdateText;

@end

Person在ViewModel中是作为一个属性,而ViewModel本身有VC中要展示的数据nameText与birthdateText

@implementation PersonViewModel

- (instancetype)initWithPerson:(Person *)person {
    self = [super init];
    if (!self) return nil;

    _person = person;
    if (person.salutation.length > 0) {
        _nameText = [NSString stringWithFormat:@"%@ %@ %@", self.person.salutation, self.person.firstName, self.person.lastName];
    } else {
        _nameText = [NSString stringWithFormat:@"%@ %@", self.person.firstName, self.person.lastName];
    }

    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
    _birthdateText = [dateFormatter stringFromDate:person.birthdate];

    return self;
}

@end

MVVM: 轻量化的ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.nameLabel.text = self.viewModel.nameText;
    self.birthdateLabel.text = self.viewModel.birthdateText;
}

MVVM模式的实质

实质就是用一个新的Model,称之为ViewModel,包含了原本MVC中的Model,然后把对数据的逻辑处理和部分业务逻辑,放入这个Model中.ViewController只负责可视化已经处理完善的数据.

MVVM本身也是每一个ViewController对应一个ViewModel,但是对于MVC来讲,其具备了一定性质的ViewModel复用的可能

MVVM的缺点

好多人推崇MVVM,但是并不代表它没有缺点,任何设计模式都有缺陷,唐巧提到MVVM的作者 John Gossman 的批评,应该值得注意

第一点:数据绑定使得 Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。

第二点:对于过大的项目,数据绑定需要花费更多的内存。

  • iOS
  • Architecture
  • Tutorial

展开全文 >>

设计模式的思考(三):局部单例

2016-02-14

局部单例模式

除了上文提到的MVVM模式之外,为了解决纯代码MVC中View和ViewController代码冗余问题,我个人还采取过一种局部单例的设计模式.

整个设计由以下部分组成

  1. Model: 用于管理数据,同MVC
  2. ViewController: 兼顾View和Controller功能
  3. View: 一些独立的View,如Cell等
  4. ModuleSingleton: 关键!一个局部单例,负责模块功能和模块数据

基本思想(哲⛢学)

任何事物都分型和质

  • 型: 就是看到的东西,程序本身并不需要界面甚至模型,这些都是为了质而存在的
  • 质: 就是事物的本质,程序本身是为了完成一些功能,完成功能需要的条件只是信息

那么型存在的理由是什么呢?也就是为了方便信息交流给人看和使用的.所以说MVC也好,MVVM也好,都没有把功能从Controller里分离出来,每个ViewController.m文件都有自己的功能,各自独立.为了减少Controller层的冗余,并不一定非要把View拆出去,也可以拆除对于用户和程序进行交流不必要的部分,也就是质.

模式分析

1. 业务分析

第一次实际操作的工作是设计一个支付工具的登录注册模块,首先我们来分析登录模块需要什么:

  1. 账户
  2. 登录密码
  3. 验证码

然后分析注册模块需要用到什么:

  1. 账户
  2. 验证码
  3. 登录密码
  4. 支付密码
  5. 密保问题
  6. 密保答案
  7. 身份证

根据以上分析,如果不是计算机,而是一个真实的人,比如我去银行柜台办理开户,我需要告诉柜员我要办理什么业务(登录or注册),然后给他必须提供的信息,然后柜员告诉我办理好了没有.只要我信息提供的足够全,柜员给我的结果应该只有两个,办理好了(YES)或没有办理好(NO).

我们假设这个柜员一次只能办理一个业务(实际上也是这样),你给出信息,执行业务,反馈结果,你给出第二次信息,执行第二个业务,反馈第二次结果,如此循环下去.

2. 模块单例数据池

经过业务分析,我们需要建立一个数据池,由于登录和注册有部分可以进行复用,所以我们的数据池包括

  1. 账户
  2. 验证码
  3. 登录密码
  4. 支付密码
  5. 密保问题
  6. 密保答案
  7. 身份证

数据池用于从外部接收数据

3. 模块单例业务池

所谓业务池,就是我这个模块能干什么.比如登录注册模块,一定会有登录和注册.

4. 模块单例业务逻辑

就如同步骤1中分析的,发起业务的我们只关心业务有没有办好,并不关心业务具体需要怎么办,就拿登录来的流程举例

  1. 接收账户密码
  2. 检查账户是不是正确(比如为一个手机号)
  3. 密码是不是符合要求
  4. 网络是不是正常
  5. 是否有验证码的需求
  6. 进行登录

而注册的流程就更为复杂

  1. 接收账户密码
  2. 检查账户是不是符合规范
  3. 登录密码是不是符合规范
  4. 支付密码是不是符合规范
  5. 密保问题是不是可以正常获取
  6. 密保答案是不是符合规范
  7. 身份证号是不是符合规范
  8. 网络是否正常
  9. 进行注册开户

从以上分析结合步骤3中的业务池来看,除去接收账户密码与进行登录/开户注册步骤之外,中间的步骤对于业务请求者应该是封闭的,可以单独在模块内部完成的.

5. 模块单例的成型

根据步骤1-4的分析,我们已经可以建立一个脱离了界面,本身就可以完成登录注册功能的模块,这个模块的特性是:

  1. 有一个数据池用于接收数据
  2. 有一个业务池用于接收指令并反馈
  3. 内部可以独立完成当前业务所有功能
  4. 单例声明周期可以被控制

这四个特征的1-3已经在前四步分析中进行归纳,第四点可以参考我的单例文章

模块的单例代码解析

ModuleSingleton.h

typedef void(^ModuleSingletonCompletion)(BOOL success, NSString *code, NSString *message);

@interface ModuleSingleton : NSObject

#pragma mark - ModuleSingleton Life Controller
+ (instancetype)sharedInstance;//开启单例
+ (void)haltSharedInstance;//关闭单例

#pragma mark - ModuleSingleton Data Manager //数据管理
- (void)cleanDataPool;//清除所有数据
- (void)cleanDataPoolExceptAccount;//清除除去帐号外的数据

#pragma mark - DataPool //数据池
@property (nonatomic, copy) NSString *account;
@property (nonatomic, copy) NSString *verifyCode;
@property (nonatomic, copy) NSString *loginPassword;
@property (nonatomic, copy) NSString *payPassword;
@property (nonatomic, copy) NSString *secQuestion;
@property (nonatomic, copy) NSString *secAnswer;
@property (nonatomic, copy) NSString *IDCardNo;

#pragma mark - FunctionPool //方法池
- (void)loginWithPasswordCompletion:(ModuleSingletonCompletion)completion;
- (void)registerOpenAccountCompletion:(ModuleSingletonCompletion)completion;

@end

代码解析

根据头文件可以知道暴露在外的只有方法和数据,这个模块就像一个柜员一样,你只需要告诉它必要的信息就可以,然后下达指令就可以了.

也就是说在ViewController中,界面本身并不和逻辑或者网络层对接,所有的功能都在单例中进行统一的对接管理,ViewController的.m文件只负责可视化的展示和视觉逻辑.

最后啰嗦一遍:

业务逻辑(比如例子中的检查)和网络通讯全部由ModuleSingleton单例处理,ViewController只负责可视化,没有可视化,Module本身也可以完成工作

VC中调用登录代码

[[ModuleSingleton shareInstance] setAccount:@"15010XXXXXX"];
[[ModuleSingleton shareInstance] setLoginPassword:@"**********"];
[[ModuleSingleton shareInstance] loginWithPasswordCompletion:^(BOOL success, NSString *code, NSString *message) {
    if(success) { }else {}
    }];

ModuleSingleton.m

static ModuleSingleton *_sharedInstance = nil;
static dispatch_once_t _onceToken;

NSString *const CheckPassed = @"CheckPassed";
NSString *const CheckErrorCode = @"-999999";

@implementation ModuleSingleton

#pragma mark - ModuleSingleton Life Controller

+ (instancetype)sharedInstance{
    dispatch_once(&_onceToken, ^{
        _sharedInstance = [[BPLoginManager alloc] initSharedInstance];
    });
    return _sharedInstance;
}

+ (void)haltSharedInstance {
    if (_sharedInstance) {
        _sharedInstance = nil;
        _onceToken = 0;
    }
}

- (void)dealloc {
    NSLog(@"ModuleSingleton SharedInstance Did Halted, Address: %p ",self);
}

- (instancetype)initSharedInstance {
    if (_sharedInstance) {
        return self;
    }
    self = [super init];
    if (self) {
        [self cleanDataPool];
    }
    return self;
}

- (instancetype)init {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"ModuleSingleton must accessed by shareInstance" userInfo:nil];
}

#pragma mark - ModuleSingleton Data Manager //数据管理

- (void)cleanDataPool {
    [self cleanDataPoolWithAccount:YES];
}

- (void)cleanDataPoolExceptAccount {
    [self cleanDataPoolWithAccount:NO];
}

- (void)cleanDataPoolWithAccount:(BOOL)withAccount {
    if(withAccount) {
        account = @"";
    }
    verifyCode = @"";
    loginPassword = @"";
    payPassword = @"";
    secQuestion = @"";
    secAnswer = @"";
    IDCardNo = @"";
}

#pragma mark - FunctionPool //方法池

- (void)loginWithPasswordCompletion:(ModuleSingletonCompletion)completion {
    //检查发送数据
    NSString *checkResult = [self checkLoginWithPassword];
    if (![checkResult isEqualToString:CheckPassed]) {
        completion(NO,CheckErrorCode,checkResult);
        return;
    }
    //登录的网络层
    [NetworkModuleService loginWithAccount:_account loginPassword:_loginPassword verifyCode:_verifycode block:^(ServiceResponse *response) {
        if (response.isSuccess) {
            completion(YES,response.code,@"登陆成功");
        }
        else {
            completion(NO,response.code,response.message);
        }
    }];
}

- (void)registerOpenAccountCompletion:(ModuleSingletonCompletion)completion {
    //检查发送数据
    NSString *checkResult = [self checkRegisterOpenAccount];
    if (![checkResult isEqualToString:CheckPassed]) {
        completion(NO,CheckErrorCode,checkResult);
        return;
    }
    //注册的网络层
    [NetworkModuleService openAccount:_account loginPwd:_loginPassword payPwd:_payPassword idNo:_IDCardNo securityQuestion:_secQuestion securityAnswer:_secAnswer block:^(ServiceResponse *response) {
        if (response.isSuccess) {
            completion(YES,response.code,@"注册成功");
        }
        else {
            completion(NO,response.code,response.message);
        }
    }];
}

#pragma mark - CheckPool //检查池

- (NSString *)checkAccount {
    if ([_account isEqualToString:@""]) {
        return @"请输入手机号";
    }
    return CheckPassed;
}//检查账号

- (NSString *)checkVerifyCode {
    return CheckPassed;
}

- (NSString *)checkLoginPassword {
    return CheckPassed;
}

- (NSString *)checkPayPassword {
    return CheckPassed;
}

- (NSString *)checkSecQuestion {
    return CheckPassed;
}

- (NSString *)checkSecAnswer {
    return CheckPassed;
}

- (NSString *)checkIDCardNo {
    return CheckPassed;
}

- (NSString *)checkLoginWithPassword {
    NSString *checkResult;
    checkResult = [self checkAccount];
    if (![checkResult isEqualToString:CheckPassed]) {
        return checkResult;
    }
    checkResult = [self checkLoginPassword];
    if (![checkResult isEqualToString:CheckPassed]) {
        return checkResult;
    }
    return CheckPassed;
}

- (NSString *)checkRegisterOpenAccount {
    NSString *checkResult;
    checkResult = [self checkAccount];
    if (![checkResult isEqualToString:CheckPassed]) {
        return checkResult;
    }
    checkResult = [self checkLoginPassword];
    if (![checkResult isEqualToString:CheckPassed]) {
        return checkResult;
    }
    checkResult = [self checkPayPassword];
    if (![checkResult isEqualToString:CheckPassed]) {
        return checkResult;
    }
    checkResult = [self checkSecQuestion];
    if (![checkResult isEqualToString:CheckPassed]) {
        return checkResult;
    }
    checkResult = [self checkSecAnswer];
    if (![checkResult isEqualToString:CheckPassed]) {
        return checkResult;
    }
    checkResult = [self checkIDCardNo];
    if (![checkResult isEqualToString:CheckPassed]) {
        return checkResult;
    }
    return CheckPassed;
}

@end

设计隐患

通常来讲单例都是伴随着整个App的生命周期,不推荐释放的,因为会造成数据的不安全,所以使用这个模式存在一种隐患,虽然在举例的代码中并没有发生.

假设存在一种异步情况用来获取某个账户的密保问题

NSString *question = @"";
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    //某个函数通过单例函数获取密保问题
    question = [self getSecurityQuestionByAccount:[ModuleSingleton shareInstance].account];//第一个Module
    dispatch_async(dispatch_get_main_queue(), ^{
        //然后放回单例
        [[ModuleSingleton shareInstance] setSecQuestion:question];//被释放后重新生成的Module
    });
});

如果在执行dispatch_async(dispatch_get_main_queue(), ^{})之前,ModuleSingleton被释放了,那么函数内部的setSecQuestion将会把question赋给一个全新的ModuleSingleton,而不是第一个ModuleSingleton.

也就是说不恰当的使用,会造成数据的混乱,这个问题是文章单例模式的滥用让我发现的.

  • iOS
  • Architecture
  • Singleton
  • Tutorial

展开全文 >>

GitHub上PullRequest有冲突怎么办

2016-02-01

GitHub上的Conflict的产生

一般Conflict的产生,都是几个人同时在改一个仓库的代码,特别是Fork来的,没有进行同步导致的

Google的关键词

可以Google Github Update Forked来查找

如何同步Forked

命令行是肯定可以的,但是一般大家都喜欢用Desktop嘛

  1. 先到DeskTop的Reporsitory的 Repository Settings里
  2. 更改Primary Remote Repository的地址为Forked来的地址
  3. 然后进行Sync,此时会提示有冲突
  4. 然后到Uncommitted Changes里看哪些文件冲突了
  5. 改Primary Remote Repository为自己Fork的仓库地址
  6. 然后用Editer(比如Xcode)改掉Conflict,想留哪里的把,另一部分删掉就可以了
  7. 然后去查看自己Pull Request就没有冲突了
  • Git
  • Tips

展开全文 >>

MethodSwizzling方法hook函数

2016-02-01

MethodSwizzling是什么

由于OC是一种动态语言,基于消息机制的函数.执行代码的时候,函数并不是在编译阶段就确定了的,是动态绑定的,他们的绑定关系是通过一张映射表来确定的.

与动态绑定对应的是C的静态绑定,会在后文举例说明.

那么既然函数是动态绑定的,我们能不能通过一些方法动态改变这些绑定关系,答案就是MethodSwizzling.它属于C语言,利用的是Runtime机制.

MethodSwizzling怎么用

动态改变这个映射基本分为3个,交换,修改,设置新方法

  1. 利用 method_exchangeImplementations 来交换2个方法的实现
  2. 利用 class_replaceMethod 来修改类的实现
  3. 利用 method_setImplementation 来直接设置某个方法实现

我们以常见的交换举例,先准备两个函数

- (void)oringinalLog {
    NSLog(@"Oringinal Method");
}

- (void)swizzlingLog {
    NSLog(@"Swizzling Method");
}

函数前后的映射关系如下图

Figure2

然后我们进行测试

- (void)touchUpInsideButton {
    NSLog(@"First Log:");
    [self oringinalLog];
    [self swizzlingLog];
    //Method Swizzling Test
    Method oringinal = class_getInstanceMethod([self class], @selector(oringinalLog));
    Method swizzling = class_getInstanceMethod([self class], @selector(swizzlingLog));
    method_exchangeImplementations(oringinal, swizzling);

    NSLog(@"Second Log:");
    [self oringinalLog];
    [self swizzlingLog];
}

观察输出日志

2016-02-02 10:49:09.664 Learn[1098:619493] First Log:
2016-02-02 10:49:09.665 Learn[1098:619493] Oringinal Method
2016-02-02 10:49:09.665 Learn[1098:619493] Swizzling Method
2016-02-02 10:49:09.665 Learn[1098:619493] Second Log:
2016-02-02 10:49:09.666 Learn[1098:619493] Swizzling Method//替换完成
2016-02-02 10:49:09.666 Learn[1098:619493] Oringinal Method

可以发现第二次打印方法已经被互换,而且一旦执行过Exchange方法后,映射表是一直被交换的,和函数作用域无关,使用另外一个按键测试

- (void)touchUpInsideOtherButton {
    NSLog(@"OtherButton Log:");
    [self oringinalLog];
    [self swizzlingLog];
}

观察日志,可以发现开始没有被替换,后来被替换了

2016-02-02 10:58:11.551 Learn[1149:728289] OtherButton Log:
2016-02-02 10:58:11.551 Learn[1149:728289] Oringinal Method//第一次按没有被替换
2016-02-02 10:58:11.552 Learn[1149:728289] Swizzling Method
2016-02-02 10:58:13.315 Learn[1149:728289] First Log:
2016-02-02 10:58:13.315 Learn[1149:728289] Oringinal Method
2016-02-02 10:58:13.315 Learn[1149:728289] Swizzling Method
2016-02-02 10:58:13.316 Learn[1149:728289] Second Log:
2016-02-02 10:58:13.316 Learn[1149:728289] Swizzling Method
2016-02-02 10:58:13.317 Learn[1149:728289] Oringinal Method//此时Exchange结束
2016-02-02 10:58:14.584 Learn[1149:728289] OtherButton Log:
2016-02-02 10:58:14.584 Learn[1149:728289] Swizzling Method//再次点击,已经被替换
2016-02-02 10:58:14.584 Learn[1149:728289] Oringinal Method

用MethodSwizzling来Hook的递归假象

像刚刚上文那种已经实现了的两个方法,互相交换来使用,很少会遇到这种需求,多数需求还是用自己的函数替代系统的函数,完成一些功能性的插入.

比如我想让每次调用NSString的lowercaseString都把小写化的字符输出一下.由于这个函数是系统的,我们无法进入函数内部修改.

@interface NSString (HookCategory)
- (NSString *)hooked_lowcaseString;
@end

@implementation NSString (HookCategory)
- (NSString *)hooked_lowcaseString {
    NSString *lowercase = [self hooked_lowcaseString];
    NSLog(@"%@",lowercase);
    return lowercase;
}
@end

上文一眼看上去,在hooked_lowcaseString内部调用了hooked_lowcaseString岂不是产生了递归.但是由于是要配合Exchange使用的,并不会产生

我们来看测试代码

NSString *tempStr = @"ABC";
Method oringinal = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swizzling = class_getInstanceMethod([NSString class], @selector(hooked_lowcaseString));
method_exchangeImplementations(oringinal, swizzling);
tempStr = [tempStr lowercaseString];//此处已经调用的是hooked_lowcaseString

进入hooked_lowcaseString函数内部

- (NSString *)hooked_lowcaseString {
    //此处调用的是系统lowercaseString,所以不会递归
    NSString *lowercase = [self hooked_lowcaseString];
    NSLog(@"%@",lowercase);
    return lowercase;
}

什么是动态绑定

所谓动态绑定就是和静态绑定对应的,因为OC是C的超集,所以这里通过C来解释,首先定义两个函数.

#import <stdio.h>

void printHello() {
    printf("Hello\n");
}

void printGoodbye(){
    printf("Goodbye\n");
}

这种就是静态绑定,编译器编译结束就知道这里回执行printHello和printGoodbye函数,所以直接把指针指向了这两个函数的所在地

void doTheThing(int type) {
    if(type == 0) {
        printHello();
    }else {
        printGoodbye();
    }
    return 0;
}

这种就是动态绑定,编译器只是指导这里会声明一个func函数,func函数具体干什么,要等到运行时根据type的值,来让func指向Hello和Goodbye其中一个,所以我们只需要控制type值,就让func有了多种可能性

void doTheThing(int type) {
    void (*func)();
    if(type == 0) {
        func = printHello();
    }else {
        func = printGoodbye();
    }
    func();
    return 0;
}

OC的动态函数机制

消息调用成功的情况

由于OC是动态绑定机制,所以每个**[类活实例 函数名]**的调用,都是将某个字符串(函数名的Selector)发送到某个地址(可以是类名,也可以是某个实例的内存地址).所以才会有类方法(classMethod)和实例方法(InstanceMethod).

那么对这个字符串(函数名),OC会做哪些处理呢?首先会调用底层的C函数

void objc_msgSend(id self, SEL cmd, ...)

这个函数是个多参数函数,第一个是你传来的地址,第二个就是消息的selector,后边可以附加参数,对于一些特殊情况还有

  1. objc_msgSend_stret 发送的消息返回一个结构体
  2. objc_msgSend_fpret 发送的消息返回一个浮点数Float
  3. objc_msgSendSuper 发送消息给超类就是[super message:parameter]

这是可以正常发送的函数状态,如果遍历了所有的super还没有找对应这个消息的函数,就会抛出异常

这个是由NSObject的**doesNotRecognizeSelector:**抛出的异常

消息调用不成功

消息调用不成功会有三个函数来尝试补救这个问题,三个消息的策略分别是

  1. 卧槽,这个没见过啊,我看看能不能自己处理掉
  2. fuck处理不掉啊,算了交给备胎看他行不行
  3. 备胎说,尼玛,我也搞不定啊,丢回系统找别人吧

Figure1

动态方法解析

//二选一,尝试在内部捕获这个函数并进行一定的处理
+ (BOOL)resolveInstanceMethod:(SEL)selector;
+ (BOOL)resolveClassMethod:(SEL)selector;

备援接收者

//把这个消息的接收者发送给另外一个对象
//这个对象不是super的一员,因为此时已经遍历了super找不到对应
- (id)forwardingTargetForSelector:(SEL)selector;

完整消息转发

//这里把SEL的target,message的selector字符串本身,parameters重新包装成一个NSInvocation
//然后把这个NSInvocation交还给系统重新进行一次派发(msssage-dispatch system)
- (void)forwardInvocation:(NSInvocation *)invocation;

所以在自定义的Class里的时候,我们可以通过重构这三个函数,来编写对应未知消息的处理方式.

参考文献

[1]http://blog.csdn.net/yiyaaixuexi/article/details/9374411

[2]Effective Objective-C 2.0

  • iOS
  • C
  • Reflect
  • Tutorial

展开全文 >>

如何主动闪退让别人用自己的接口函数

2016-02-01

为什么要主动闪退

比如自己写了一个Class,然后开放了一个**initWithTitle:**的接口,但是总有像我一样的挫B不看头文件就开始写程序,程序运行起来不正常查半天还不知道为啥,所以干脆主动闪退告诉别人

小子! 用我写的类

如何主动闪退

@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"提示信息" userInfo:nil];

通过丢出一个异常,来让App闪退,第一个参数Name,是一些系统定义的常量字符串

/***************    Generic Exception names        ***************/

FOUNDATION_EXPORT NSString * const NSGenericException;
FOUNDATION_EXPORT NSString * const NSRangeException;
FOUNDATION_EXPORT NSString * const NSInvalidArgumentException;
FOUNDATION_EXPORT NSString * const NSInternalInconsistencyException;

FOUNDATION_EXPORT NSString * const NSMallocException;

FOUNDATION_EXPORT NSString * const NSObjectInaccessibleException;
FOUNDATION_EXPORT NSString * const NSObjectNotAvailableException;
FOUNDATION_EXPORT NSString * const NSDestinationInvalidException;

FOUNDATION_EXPORT NSString * const NSPortTimeoutException;
FOUNDATION_EXPORT NSString * const NSInvalidSendPortException;
FOUNDATION_EXPORT NSString * const NSInvalidReceivePortException;
FOUNDATION_EXPORT NSString * const NSPortSendException;
FOUNDATION_EXPORT NSString * const NSPortReceiveException;

FOUNDATION_EXPORT NSString * const NSOldStyleException;

如何配合使用

可以重写最终初始化方法**initWithFrame:和initWithCoder:**来进行条件检查,不满足就闪退!

  • iOS
  • IDE
  • Compiler
  • Tips

展开全文 >>

控制台调试(lldb)快速入门

2016-01-29

控制台调试干嘛用

除了用Xcode控制断点\打印变量之外,控制台调试最大的作用就是可以注入代码

基本操作

在断点暂停的状态下,控制台内可以输入

  1. n (next下一步)
  2. s (step进入函数)
  3. c (continue继续执行)
  4. p (print打印某个指针)
  5. po (print object打印某个指针指向的内容)

例如

po self.view

注入代码控制变量值

控制台调试最重要的就是注入代码

  • e (expression执行函数)

例如

BOOL value = YES; //break point
if (value) {
    NSLog(@"%tu",value);
}

比如在上述代码中,value如果正常运行,始终为YES,我们可以通过注入代码让其改变

(lldb) po value
YES
(lldb) e value = NO
(BOOL) $4 = NO
(lldb) po value
NO

可以动态改变某个值,从而帮助调试

注入代码控制UI

我们还可以通过注入代码指向某个内存,或者变量,执行一些函数,如先改变背景色,然后通过**[CATransaction flush]**刷新就可以看到效果.

记住声明变量的时候带”$”号,以及执行函数时的返回值(void)等

(lldb) e id $myView = self.view
(lldb) po $myView
<UIView: 0x7faecae7dfe0; frame = (0 267; 375 400); autoresize = W+H; layer = <CALayer: 0x7faec95bf200>>

(lldb) po self.view
<UIView: 0x7faecae7dfe0; frame = (0 267; 375 400); autoresize = W+H; layer = <CALayer: 0x7faec95bf200>>

(lldb) e id $youView = (id)0x7faecae7dfe0
(lldb) po $youView
<UIView: 0x7faecae7dfe0; frame = (0 267; 375 400); autoresize = W+H; layer = <CALayer: 0x7faec95bf200>>

(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]
(lldb) e (void)[CATransaction flush]

控制当前函数返回值

控制台还可以通过指令控制返回值,即使进入了汇编的界面也可以,比如有一些.a静态库的函数,Step进入函数后,直接是汇编界面,此时可以直接控制返回

(lldb) thread return NO

更多的可以查看参考文献

参考文献

[1]http://objccn.io/issue-19-2/

  • iOS
  • IDE
  • Tutorial

展开全文 >>

HTML5和Native应用交互的基本原理

2016-01-28

HTML5应用和Native交互的桥梁

HTML5应用本身就是个网页,以一个index.html为入口.那么和Native引用交互的较量就是WebView

WebView交互方法

Native对H5进行交互,通过函数发送JS代码

[WebView stringByEvaluatingJavaScriptFromString:@"control.setSequencePlay()"];

H5对Native进行交互,通过发送Request,并且Request被WebView的协议接收Request字符串

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

交互的基本原理

交互的过程,Native生成一段JS代码,然后执行,H5则通过和Native的开发人员约定一套规则(协议?),通过Request请求发送,发送的过程中,被桥梁WebViewDelegate拦截下来.然后分析字符串,执行本地的函数

  • iOS
  • JavaScript
  • Tutorial

展开全文 >>

OC中的偏门语句TryCatchAssert

2016-01-28

Try…Catch…

为什么OC中很少用Try Catch,根据知乎热心网友回答

因为try catch无法捕获UncaughtException
而oc中大部分crash如:内存溢出、野指针等都是无法捕获的
而能捕获的只是像数组越界之类(这真心需要catch么?),所以try catch对于oc来说,比较鸡肋。

Assert

OC中的NSAssert()是一个宏,用于开发阶段调试程序中的Bug,如果发布不去掉的话,有可能会影响到程序的性能。

#define NSAssert(condition, desc)

condition是条件表达式,值为YES或NO;desc为异常描述,通常为NSString。

当conditon为YES时程序继续运行,为NO时,则抛出带有desc描述的异常信息。NSAssert()可以出现在程序的任何一个位置

如何一键除去NSAssert

在Build Settings菜单,找到Preprocessor Macros项,Preprocessor Macros项下面有一个选择,用于程序生成配置:Debug版和Release版。选择 Release项,设置NS_BLOCK_ASSERTIONS,不进行断言检查。

然后只需要在Run的时候选择为Release,就不会进行断言.对于Archive 而言,默认的生成配置就是Release。

[1]https://www.zhihu.com/question/21248079/answer/27037365
[2]http://blog.csdn.net/univcore/article/details/16859263

  • iOS
  • IDE
  • C
  • Tips

展开全文 >>

Xcode插件与装逼利器

2016-01-28

Xcode插件管理器

一直都是Github上手动搜插件,最近看了唐巧的书,唐老师不愧老司机,收获很大,其中有个Xcode插件管理器十分好用

  • Alcatraz

装逼利器

除了常用的插件之外,我还特别中意两个装逼利器,其中一个自己还Commit了一些代码,但是人家还没接受,心里凉凉哒

  1. 火花特效
  2. Miku跳舞

Miku跳舞加入播放控制和音乐源控制,如果人家没接受,可以去我的Git下我Fork的

Miku跳舞

自己改写这个插件还学了挺多,比如JS比如HTML5,比如Xcode插件开发

  • iOS
  • IDE
  • Tips

展开全文 >>

&laquo; Prev1…3435363738…45Next &raquo;
© 2021 Alan Li
Hexo Theme Yilia by Litten
  • 所有文章
  • 关于我

tag:

  • iOS
  • Java
  • Collection
  • Python
  • Shell
  • CMake
  • Memory
  • JavaScript
  • Architecture
  • AnchorPoint
  • Android
  • Web
  • Annotation
  • AFNetworking
  • Window
  • ViewController
  • AutoLayout
  • Dozer
  • CoreAnimation
  • Cycle Retain
  • Block
  • UI
  • IDE
  • FrontEnd
  • CSS
  • Category
  • TableViewCell
  • Security
  • Net
  • JSP
  • Spring
  • C
  • MyBatis
  • Date
  • React
  • GCD
  • UITouch
  • Gesture
  • UIControl
  • Git
  • HTML
  • HTTPS
  • HTTP
  • Servlet
  • Server
  • DataBase
  • MySQL
  • Linux
  • Tutorial
  • Ajax
  • Type
  • JQuery
  • JSON
  • Exception
  • Parameter
  • Reflect
  • Thread
  • Sort
  • KVO
  • MKMapKit
  • Overlay
  • Maven
  • Configure
  • Tips
  • Transaction
  • Swift
  • NavigationBar
  • Nginx
  • Runtime
  • OpenCV
  • Property
  • Playground
  • Protocol
  • Redux
  • ScrollView
  • Session
  • Cookie
  • Shiro
  • Error
  • Singleton
  • RegEx
  • StackView
  • StatusBar
  • Base64
  • Socket
  • TCP
  • IP
  • TextField
  • CALayer
  • UILabel
  • View
  • Animation
  • Xcode
  • Hexo
  • Terminal
  • OC
  • Device
  • Log
  • Image
  • JUnit
  • Oval
  • Archive
  • XSS
  • Compiler
  • Aspect
  • Responder
  • Class
  • FireWall
  • RetainCount
  • Const
  • Frame
  • String
  • Symbols
  • Framework
  • CocoaPods
  • Unity
  • Message
  • Button
  • AuthorizationStatus
  • Struct
  • XCTest
  • NSNotification
  • Contact

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

我写的,大概率是错的。。。。。