IOS- 框架模式(简述 MVC,MVP,MVVM 和 VIPER)

MV(X)系列概要
做iOS开发也有一段时间了,*近闲暇之余总结了一下几个iOS框架,每个人对架构和设计模式都有不同的理解,在此记录下我的一些小见解,仅供参考,欢迎批评指正。

当今我们已经有很架构设计模式方面的选择:

A.MVC

B.MVP

C.MVVM

D.VIPER

前三种设计模式都把一个应用中的实体分为以下三类:

A: Models–负责主要的数据或者操作数据的数据访问层,可以想象 Perspn 和 PersonDataProvider 类。

B: Views–负责展示层(GUI),对于iOS环境可以联想一下以 UI 开头的所有类。

C: Controller/Presenter/ViewModel–负责协调 Model 和 View,通常根据用户在View上的动作在Model上作出对应的更改,同时将更改的信息返回到View上.

MVC的过去
在我们探讨Apple的MVC模式之前,我们来看下传统的MVC模式。

在这里,View并没有任何界限,仅仅是简单的在Controller中呈现出Model的变化。想象一下,就像网页一样,在点击了跳转到某个其他页面的连接之后就会完全的重新加载页面。尽管在iOS平台上实现这这种MVC模式是没有任何难度的,但是它并不会为我们解决架构问题带来任何裨益。因为它本身也是,三个实体间相互都有通信,而且是紧密耦合的。这很显然会大大降低了三者的复用性,而这正是我们不愿意看到的。

苹果推荐的MVC–愿景
控制器来作为视图和模型中间的中转站,这样视图和模型之间互相就没有直接联系了。这样的话,控制器的可复用性就变得*低,但是这个对于我们来说也是可接受的,因为我们需要有一个地方来放那些不方便放在模型中的复杂业务逻辑。

理论上来讲,这种结构看起来非常直接,但是是不是觉得有点不对劲?甚至听到过有人叫 MVC 为重控制器模式。此外,对于 iOS 开发者来说,给控制器减轻负担已经成为一个重要的话题。为什么苹果会采用仅仅改进过一点点的传统 MVC 模式呢?

现实(Reality)

Cocoa MVC 鼓励你写重控制器是因为它们互相之间在视图的生命周期中互相牵扯,以至于很难将它们分开。虽然你也可能有一些办法把一些业务逻辑和数据转模型的工作放到 Model 中来,但是对于把负担分摊到 View 上却没有什么办法,大多数情况中,View 的所有功能就是给 Controller 发送动作(action),而 Controller 则会成为所有东西的代理或者数据源,并且通常会发送或者取消网络请求等等等等,你可以尽情想象。

评估:

(1) 划分(Distribution)–View 和 Model 确实是分开了,但是View 和 Controller 紧紧的联系在一起
(2)可测试性–由于功能划分的不好,你可能只能测试你的 Model
(3)易用性–与其他模式相比代码量*小,另外,每个人都对他很熟悉,即使是一个不是非常有经验的开发者也能进行维护

就开发速度而言,Cocoa MVC 是*好的架构模式。

MVP
Cocoa MVC’s promises delivered

它看上去是不是非常像 Apple’s MVC ?是的,确实很像,并且叫做MVP(Passive View variant)。但是等一下,这意味着 Apple’s MVC 实质上就是 MVP 吗?不是的,还记得 View 是紧紧和 Controller 联系在一起的吧,而在 MVP 中,作为中转站的 Presenter 与视图控制器的生命周期没有任何关联,并且 View 很容易被模拟,所以在 Presenter 中没有任何页面布局的代码,但是 Presenter 有责任通过数据和状态来更新 View。

MVP 的特点:

(1) 划分(distribution)–大部分的任务都被划分到 Presenter 和 Model 中,而 View不太灵活(例子中的 Model 也不太灵活)
(2) 可测试性–非常出色,我们可以通过 View 来测试大部分的业务逻辑
(3)易用性–在我们那个不切实际的小例子里,MVP 的理念是非常清晰的,但是代码量是 MVC 模式的两倍

MVP 在 iOS 中使用意味着非常好的可测试和非常多的代码

MVVM
概述

引用自iOS应用架构谈:
http://www.cocoachina.com/ios/20150525/11919.html

MVVM的出现主要是为了解决在开发过程中Controller越来越庞大的问题,变得难以维护,所以MVVM把数据加工的任务从Controller中解放了出来,使得Controller只需要专注于数据调配的工作,ViewModel则去负责数据加工并通过通知机制让View响应ViewModel的改变。

MVVM是基于胖Model的架构思路建立的,然后在胖Model中拆出两部分:Model和ViewModel。ViewModel本质上算是Model层(因为是胖Model里面分出来的一部分),所以View并不适合直接持有ViewModel,因为ViewModel有可能并不是只服务于特定的一个View,使用更加松散的绑定关系能够降低ViewModel和View之间的耦合度。

还有一个让人很容易忽略的问题,大部分国内外资料阐述MVVM的时候都是这样排布的:
View<->ViewModel <->Model
1
造成了MVVM不需要Controller的错觉,现在似乎发展成业界开始出现MVVM是不需要Controller的声音了。其实MVVM是一定需要Controller的参与的,虽然MVVM在一定程度上弱化了Controller的存在感,并且给Controller做了减负瘦身(这也是MVVM的主要目的)。但是,这并不代表MVVM中不需要Controller,MMVC和MVVM他们之间的关系应该是这样:

View <-> C <-> ViewModel <->Model
1
所以使用MVVM之后,就不需要Controller的说法是不正确的。严格来说MVVM其实是MVCVM。从中可以得知,Controller夹在View和ViewModel之间做的其中一个主要事情就是将View和ViewModel进行绑定。在逻辑上,Controller知道应当展示哪个View,Controller也知道应当使用哪个ViewModel,然而View和ViewModel它们之间是互相不知道的,所以Controller就负责控制他们的绑定关系,所以叫Controller/控制器就是这个原因。

前面讲了那么多,其实归根结底就是一句话:在MVC的基础上,把C拆出一个ViewModel专门负责数据处理的事情,就是MVVM。然后,为了让View和ViewModel之间能够有比较松散的绑定关系,于是我们使用ReactiveCocoa,KVO,Notification,block,delegate和target-action都可以用来做数据通信,从而来实现绑定,但都不如ReactiveCocoa提供的RACSignal来的优雅,如果不用ReactiveCocoa,绑定关系可能就做不到那么松散那么好,但并不影响它还是MVVM。

MVVM(View-ViewManger-C-ViewModel-Model)

View – 用来呈现用户界面
ViewManger – 用来处理View的常规事件,负责管理View
Controller – 负责ViewManger和ViewModel之间的绑定,负责控制器本身的生命周期。
ViewModel – 存放各种业务逻辑和网络请求
Model – 用来呈现数据

这种设计的目的是保持View和Model的高度纯洁,提高可扩展性和复用度。在日常开发中,ViewModel是为了拆分Controller业务逻辑而存在的,所以ViewModel需要提供公共的服务接口,以便为Controller提供数据。而ViewManger的作用相当于一个小管家,帮助Controller来分别管理每个subView,ViewManger负责接管来自View的事件,也负责接收来自Controller的模型数据,而View进行自己所负责的视图数据绑定工作。Controller则是*后的大家长,负责将ViewModel和ViewManger进行绑定,进行数据转发工作。把合适的数据模型分发给合适的视图管理者。

日常开发中,往往一个视图页面交由一个控制器进行管理,而一个页面上又有N个小的子页面,这就要求我们来对这些视图进行合适的分层处理,拆分视图,将这些视图进行封装,将复杂View抽象成独立的类,不必暴露出具体的实现细节。这样做的好处是,简化应用层的层级复杂度,同时也方便进行管理,视图结构就会变得很清晰。子视图具体的内部事件,可通过代理模式或者Block交由ViewManger处理,因为视图是可以复用的,而其中的事件响应代码往往是根据不同的业务是有差异的。所以可能会有下面两种情况出现:

– View很纯洁,需要复用View,若业务逻辑变化则切换ViewManger。
– ViewManger也比较纯洁,若业务逻辑不变,而View需要大变,则切换View即可,保证View中的protocol或者block一致即可<*好是通过协议提前规范好>。

这样就实现了互相的封装,两者之间只通过protocol或者block进行交流通信,降低了代码的耦合度。尽量使用protocol和category来制定对象之间的通信规范,来降低代码的侵入性。

这样的架构设计,就像一条生产线,ViewModel进行数据的采集和加工,Controller则进行数据的装配和转发工作,ViewManger进行接收转发分配来的数据,从而进行负责View的展示工作和管理View的事件。这样,不管哪个环节,都是可以更换的,同时也提高了复用性。

架构讲解

以上图做为讲解demo,*然很简单,但是也能够很好的阐述了,理解思想才是*重要的。
首先我们来拆分这个页面,*个为控制器。暂且命名为MyController,上面有两个直接子视图,按钮MyBtn和页面比较复杂的子视图MyView,MyView中有MyViewBtn1和MyViewBtn2还有一个MyViewLabel视图。
具体结构如下:

MyController
MyBtn
MyView
MyViewBtn1
MyViewBtn2
MyViewLabel
界面分析完了,现在可以进行代码的架构工作了。
首先需要建立一个ViewModel,使它能够源源不断的进行数据的生产,并提供数据给MyController;然后建立一个ViewManger负责管理MyView,当然,Model模型数据必不可少。这些工作完成之后,代码结构变为:

Controller – – 存放MyController
ViewModel – – 存放MyViewModel
View – – 存放MyView
ViewManger – – 存放MyViewManger
Model – – 存放MyModel

控制器中的代码结构如下图:

当用户点击MyBtn按钮触发动作时,控制器就会就将ViewMode中加载的数据模型转发分配给ViewManger中的回调函数- (void)smk_viewMangerWithModel:(NSDictionary * (^) ( ))dictBlock接收。

// 两种消息传递方式,开发时任选其一即可
– (void)smk_viewMangerWithSubView:(UIView *)subView {

__weak typeof(self.thirdView) weakThirdView = self.thirdView;
__weak typeof(self) weakSelf = self;

// btnClickBlock
weakThirdView.btnClickBlock = ^() {
[weakSelf smk_viewMangerWithHandleOfSubView:weakThirdView info:@”click”];
};
}

// 两种消息传递方式,开发时任选其一即可
– (void)smk_view:(__kindof UIView *)view withEvents:(NSDictionary *)events {

NSLog(@”———-%@”, events);

if ([[events allKeys] containsObject:@”jump”]) {
FirstVC *firstVC = [UIViewController svv_viewControllerWithStoryBoardName:@”Main” identifier:@”FirstVCID”];
[view.sui_currentVC.navigationController pushViewController:firstVC animated:YES];
}

}

其中,MyViewModel中的加载代码如下,如上所述,它的工作就是分解以前控制器做的一些事情。

– (void)smk_viewModelWithGetDataSuccessHandler:(void (^)())successHandler {
// 博客中省略,查看详细请参考demo
}

– (instancetype)getRandomData {
if (self.smk_dataArrayList.count > 0) {
u_int32_t index = arc4random_uniform((u_int32_t)self.smk_dataArrayList.count);
return self.smk_dataArrayList[index];
}
return nil;
}
MyViewManger中的代码如下,它实现了MVVMViewMangerProtocol协议的三个方法:

// 此方法用来接收处理来自所管理View的一些事件。
– (void)smk_viewMangerWithSubView:(UIView *)subView;
// 此方法将view的父视图传递过来,用来布局当前View
– (void)smk_viewMangerWithSuperView:(UIView *)superView;
// 根据所传入的view和info信息分别实现具体的方法
– (void)smk_viewMangerWithHandleOfSubView:(UIView *)subView info:(NSString *)info;
// 两种消息传递方式,开发时任选其一即可
– (void)smk_viewMangerWithSubView:(UIView *)subView {

__weak typeof(self.thirdView) weakThirdView = self.thirdView;
__weak typeof(self) weakSelf = self;

// btnClickBlock
weakThirdView.btnClickBlock = ^() {
[weakSelf smk_viewMangerWithHandleOfSubView:weakThirdView info:@”click”];
};
}

// 两种消息传递方式,开发时任选其一即可 —> 视图delegate
– (void)smk_view:(__kindof UIView *)view withEvents:(NSDictionary *)events {

NSLog(@”———-%@”, events);

if ([[events allKeys] containsObject:@”jump”]) {
FirstVC *firstVC = [UIViewController svv_viewControllerWithStoryBoardName:@”Main” identifier:@”FirstVCID”];
[view.sui_currentVC.navigationController pushViewController:firstVC animated:YES];
}

}

– (void)smk_viewMangerWithSuperView:(UIView *)superView {
self.thirdView.frame = CGRectMake(0, 66, [UIScreen mainScreen].bounds.size.width, 200);
[superView addSubview:self.thirdView];
}

– (void)smk_viewMangerWithHandleOfSubView:(UIView *)view info:(NSString *)info {

if ([info isEqualToString:@”click”]) {
[view configureViewWithCustomObj:self.smk_model];
}
}
MyView中的代码如下,主要是负责管理自身的内部控件视图,并根据业务逻辑需要定义了一些基本事件,通过交给ViewManger来实现:

– (IBAction)testBtnClick:(UIButton *)sender {

if (self.btnClickBlock) {
self.btnClickBlock();
}
}

– (IBAction)jumpOtherVC:(UIButton *)sender {

if (self.delegate && [self.delegate respondsToSelector:@selector(smk_view:withEvents:)]) {
[self.delegate smk_view:self withEvents:@{@”jump”: @”vc”}];
}
}

// 根据传入的model配置需要显示的内容
– (void)configureViewWithCustomObj:(id)obj {
if (!obj) return;
ThirdModel *thirdModel = (ThirdModel *)obj;
self.testLabel.text = thirdModel.title;
}

这样把各个部分区分开来,是不是感觉代码结构十分清晰了呢,当然可以根据个人习惯来进行修改,代码实现因人而异,但是思想确是互通的。把合适的业务逻辑交给*合适的对象去处理实现,只需要遵守这么一个基本原则就可以了。

至于是否采用更轻量级的ViewController做法,即 通过将各个 protocol 的实现挪到 ViewController 之外,来为 ViewController 瘦身 ,众口不一。以UITableView为例,我的做法是:

如果只是在页面上进行简单的展示,并不设计负责的业务逻辑时,会将UITableViewDelegate与UITableViewDataSource单独放到一个Handler钟进行处理,抽象出tableHander,由MVVMTableDataDelegate进行负责管理;
当然,事实上,实际开发中,每个tableView页面都很复杂,有很多逻辑要处理,这时候只能考虑将protocol重新请回Controller中了,因为View层与ViewController层本身是持有与被持有的依赖关系,所以任何类作为ViewController的类内实例来实现协议回调,实际上都是在跨层调用,所以,随着时间和业务逻辑的愈来愈复杂,就注定要以额外的接口为代价,换言之,ViewController 的内聚性变差了。
总之,具体情况具体分析,采用*合适的方式来处理应对不同的问题。兵来将挡,水来土掩。本文的相关Demo见github,实现的功能并不复杂,仅供参考,欢迎补充。

项目传送门:https://github.com/lovemo/MVVMFramework
VIPER
从乐高玩具的搭建经验转换到 iOS app 的设计

VIPER是我们*后一个要介绍的架构,它不是MV(X)系列的架构。

到现在为止,我们应该都觉得职责划分的颗粒度还是不错的。在 VIPER 中对于职责的划分提出了另一种方式,这次我们有五层:

Interactor–包括和数据相关的业务逻辑(Entities)或者网络请求,比如创建entities 类的对象或者把它们从服务器中抓取出来。为了达到这些目的你会用到通常会被看做外部依赖而不被看做 VIPER 单元的一些服务(Services)和管理者(Managers)
Presenter–包括 UI 相关(UIKit 之外)的一些业务逻辑,调用 Interactor 中的一些方法
Entities–纯粹的数据对象,并非是数据访问层,数据访问是 Interactor 层的任务
Router–负责 VIPER 模块之间的切换
它的特性:

(1)划分(distribution)–毫无疑问,在层次职责划分方面,VIPER 是*棒的
(2)可测试性–理所当然的,非常好的层次划分带来好的可测试性
(3)易用性–想你想的那样,上述两个方面都牺牲(系统)可维护性的,你需要编写许多的仅有少量功能的接口

IOS-关于App Transport Security相关说明及适配

iOS9中新增App Transport Security(简称ATS)特性, 主要使到原来请求的时候用到的HTTP,都转向TLS1.2协议进行传输。这也意味着所有的HTTP协议都强制使用了HTTPS协议进行传输。原文如下:

App Transport Security

App Transport Security (ATS) enforces best practices in the secure connections between an app and its back end. ATS prevents accidental disclosure, provides secure default behavior, and is easy to adopt; it is also on by default in iOS 9 and OS X v10.11. You should adopt ATS as soon as possible, regardless of whether you’re creating a new app or updating an existing one.
If you’re developing a new app, you should use HTTPS exclusively. If you have an existing app, you should use HTTPS as much as you can right now, and create a plan for migrating the rest of your app as soon as possible. In addition, your communication through higher-level APIs needs to be encrypted using TLS version 1.2 with forward secrecy. If you try to make a connection that doesn’t follow this requirement, an error is thrown. If your app needs to make a request to an insecure domain, you have to specify this domain in your app’s Info.plist file.

如果我们在iOS9下直接进行HTTP请求是会收到如下错误提示:

App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app’s Info.plist file.

新特性要求App内访问的网络必须使用HTTPS协议,具体内容大家可以自行度娘,

我们这里只是说明一下如何关闭该特性,让其可以访问http开头的网址.

关闭很简单,打开项目中的info.plist文件,在其中添加一个字典类型的项目App Transport Security Settings,然后在其中添加一个key:Allow Arbitrary Loads,其值为YES.如下所示:

<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
1
2
3
4
5
这段配置中的NSAppTransportSecurity是ATS配置的根节点,配置了节点表示告诉系统要走自定义的ATS设置。而NSAllowsAritraryLoads节点则是控制是否禁用ATS特性,设置YES就是禁用ATS功能。

如果想要设置某个域名禁用ATS可以在plist文件中加以下代码:

<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>your domain</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>

 

 

NSIncludesSubdomains设置为YES表示百度的子级域名都使用相同设置。

NSExceptionRequiresForwardSecrecy为NO由于网址不支持ForwardSecrecy,因此屏蔽掉改功能。

NSExceptionAllowInsecureHTTPLoads设置为YES,则表示允许访问没有证书或者是自签名、过期、主机名不匹配的证书引发的错误的域名(这里检查过百度的证书貌似没有什么问题,但是还是需要设置此项才允许访问)。

ATS是在iOS 9.0 和 OS X v10.11版本中增加的特性,使用iOS 9.0或者OS X v10.11的SDK版本(或更新的SDK)进行编译应用时会默认启动ATS。则需要对ATS进行配置。如果使用iOS 9.0或者OS X v10.11之前的SDK版本编译的应用默认是禁止ATS的,因此不会影响应用的网络连接方面的功能(即使在iOS 9.0的机子上跑也是不影响的)。

其实ATS并不单单针对HTTP进行了限制,对HTTPS也有一定的要求,以百度的地址为例(注:举该栗子的时候百度是还没符合ATS的要求的,现在百度已经支持ATS),如果在App中请求https://www.baidu.com的话,是会收到如下的错误信息:

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)

查阅了一下官方资料:(https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW33),发现HTTPS的请求需要满足下面的要求:

Requirements for Connecting Using ATS

With ATS fully enabled, your app’s HTTP connections must use HTTPS and must satisfy the following security requirements:

The server certificate must meet at least one of the following trust requirements:
Issued by a certificate authority (CA) whose root certificate is incorporated into the operating system
Issued by a trusted root CA and installed by the user or a system administrator
The negotiated Transport Layer Security version must be TLS 1.2
The negotiated TLS connection cipher suite must support forward secrecy (FS) and be one of the following:

TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA

The leaf server certificate must be signed with one of the following types of keys:
Rivest-Shamir-Adleman (RSA) key with a length of at least 2048 bits
Elliptic-Curve Cryptography (ECC) key with a size of at least 256 bits
In addition, the leaf server certificate hashing algorithm must be Secure Hash Algorithm 2 (SHA-2) with a digest length of at least 256 (that is, SHA-256 or greater).

根据原文描述,首先颁发给服务器证书的证书机构(CA)的根证书必须是内置于操作系统(哪些根证书被信任可以查看https://support.apple.com/zh-cn/HT205205,或者在你的机子的设置-通用-关于本机*下面的“进一步了解被信任的证书”中查看)或者受用户或者系统管理员信任并安装到操作系统上的。而且必须要基于TLS 1.2版本协议。再来就是连接的加密方式要提供Forward Secrecy(FS正向保密,感兴趣的筒子可以看看这个https://vincent.bernat.im/en/blog/2011-ssl-perfect-forward-secrecy.html),文档中罗列出了支持的加密算法(上面的原文中有说明,我把它独立抽出来放到下面表格中查看)。*后就是证书至少要使用一个SHA256的指纹与任一个2048位或者更高位的RSA密钥,或者是256位或者更高位的ECC密钥。如果不符合其中一项,请求将被中断并返回nil。

下面举例说明:
用浏览器打开一个链接,然后点击左侧的小锁头,点击详细信息,查看证书和算法等信息。

点击证书信息,查看颁发给它证书的CA的根证书,如图:

 

%title插图%num%title插图%num

Secure Connection
The connection to this site is encrypted and authenticated using a strong protocol (TLS 1.2), a strong key exchange (ECDHE_RSA with P-256), and a strong cipher (AES_128_GCM).

可以看到【带 RSA 加密的 SHA-256 ( 1.2.840.113549.1.1.11 )】使用了TLS 1.2版本协议,符合上面所说的TLS版本的约定。

*后,说到如何诊断一个URL是否支持ATS,这里给大家介绍一些nscurl这个命令行工具,这个工具是OS X v10.11上新增的,主要用于诊断ATS带来的连接问题,利用它可以在命令行中直接检测一个URL地址是否支持ATS。其用法如下:

/usr/bin/nscurl –ats-diagnostics [–verbose] URL

URL - 表示用来诊断的网址
verbose - 该选项将会为每次的连接包含更多信息,包括使用到Info.plist中的哪些key和对应的值也会列出来。

输出的结果都是Pass的了,那证明你查看的链接是支持ATS的。

例,检测阿里云官网www.aliyun.com

> /usr/bin/nscurl –ats-diagnostics –verbose https://www.aliyun.com

Starting ATS Diagnostics

Configuring ATS Info.plist keys and displaying the result of HTTPS loads to https://www.aliyun.com.
A test will “PASS” if URLSession:task:didCompleteWithError: returns a nil error.
================================================================================

Default ATS Secure Connection

ATS Default Connection
ATS Dictionary:
{
}
Result : PASS

================================================================================

Allowing Arbitrary Loads


Allow All Loads
ATS Dictionary:
{
NSAllowsArbitraryLoads = true;
}
Result : PASS

================================================================================

Configuring TLS exceptions for www.aliyun.com


TLSv1.2
ATS Dictionary:
{
NSExceptionDomains = {
“www.aliyun.com” = {
NSExceptionMinimumTLSVersion = “TLSv1.2”;
};
};
}
Result : PASS


TLSv1.1
ATS Dictionary:
{
NSExceptionDomains = {
“www.aliyun.com” = {
NSExceptionMinimumTLSVersion = “TLSv1.1”;
};
};
}
Result : PASS


TLSv1.0
ATS Dictionary:
{
NSExceptionDomains = {
“www.aliyun.com” = {
NSExceptionMinimumTLSVersion = “TLSv1.0”;
};
};
}
Result : PASS

================================================================================

Configuring PFS exceptions for www.aliyun.com


Disabling Perfect Forward Secrecy
ATS Dictionary:
{
NSExceptionDomains = {
“www.aliyun.com” = {
NSExceptionRequiresForwardSecrecy = false;
};
};
}
Result : PASS

================================================================================

Configuring PFS exceptions and allowing insecure HTTP for www.aliyun.com


Disabling Perfect Forward Secrecy and Allowing Insecure HTTP
ATS Dictionary:
{
NSExceptionDomains = {
“www.aliyun.com” = {
NSExceptionAllowsInsecureHTTPLoads = true;
NSExceptionRequiresForwardSecrecy = false;
};
};
}
Result : PASS

================================================================================

Configuring TLS exceptions with PFS disabled for www.aliyun.com


TLSv1.2 with PFS disabled
ATS Dictionary:
{
NSExceptionDomains = {
“www.aliyun.com” = {
NSExceptionMinimumTLSVersion = “TLSv1.2”;
NSExceptionRequiresForwardSecrecy = false;
};
};
}
Result : PASS


TLSv1.1 with PFS disabled
ATS Dictionary:
{
NSExceptionDomains = {
“www.aliyun.com” = {
NSExceptionMinimumTLSVersion = “TLSv1.1”;
NSExceptionRequiresForwardSecrecy = false;
};
};
}
Result : PASS


TLSv1.0 with PFS disabled
ATS Dictionary:
{
NSExceptionDomains = {
“www.aliyun.com” = {
NSExceptionMinimumTLSVersion = “TLSv1.0”;
NSExceptionRequiresForwardSecrecy = false;
};
};
}
Result : PASS

================================================================================

Configuring TLS exceptions with PFS disabled and insecure HTTP allowed for www.aliyun.com


TLSv1.2 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
NSExceptionDomains = {
“www.aliyun.com” = {
NSExceptionAllowsInsecureHTTPLoads = true;
NSExceptionMinimumTLSVersion = “TLSv1.2”;
NSExceptionRequiresForwardSecrecy = false;
};
};
}
Result : PASS


TLSv1.1 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
NSExceptionDomains = {
“www.aliyun.com” = {
NSExceptionAllowsInsecureHTTPLoads = true;
NSExceptionMinimumTLSVersion = “TLSv1.1”;
NSExceptionRequiresForwardSecrecy = false;
};
};
}
Result : PASS


TLSv1.0 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
NSExceptionDomains = {
“www.aliyun.com” = {
NSExceptionAllowsInsecureHTTPLoads = true;
NSExceptionMinimumTLSVersion = “TLSv1.0”;
NSExceptionRequiresForwardSecrecy = false;
};
};
}
Result : PASS

================================================================================

如果出现FAIL情况,请看以下两点:
1.签名算法–》带 RSA 加密的 SHA-256 ( 1.2.840.113549.1.1.11 )
2.Secure Connection
The connection to this site is encrypted and authenticated using a strong protocol (TLS 1.2), a strong key exchange (ECDHE_RSA with P-256), and a strong cipher (AES_128_GCM).

看以上两点是否匹配。

IOS-SDWebImage 底层实现原理以及面试题相关问题的学习链接

推荐几个我学习SDWebImage的链接>

SDWebImage实现详解:
http://www.cnblogs.com/YYSheng/articles/4735609.html

有关请求相同地址图片的下载问题(SDWebIMage底层原理的实现):
http://www.jianshu.com/p/e00854ab5567

SDWebImage的底层介绍以及——需要了解和掌握的要点(包括面试题,图片格式,以及GIF的播放):
http://blog.csdn.net/u013087513/article/details/49402427

SDWebImage内部实现过程:
http://blog.csdn.net/ab20514/article/details/49479505

SDWebImageDownloader中线程安全:
http://liuyuxiao.cc/index.php/2016/05/30/sdwebimagedownloader-thread-safe/

SDWebImage 加载图片的流程
入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。
进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:.
先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。
SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。
如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。
根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。
如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。
如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。
共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。
图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。
connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。
connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。
图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,*好也在这里完成,效率会好很多。
在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。
imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。
通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。
将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。
SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
SDWI 也提供了 UIButton+WebCache 和 MKAnnotationView+WebCache,方便使用。
SDWebImagePrefetcher 可以预先下载图片,方便后续使用
SDWebImage库的作用
通过对UIImageView的类别扩展来实现异步加载替换图片的工作。

主要用到的对象:
1、UIImageView (WebCache)类别,入口封装,实现读取图片完成后的回调
2、SDWebImageManager,对图片进行管理的中转站,记录那些图片正在读取。
向下层读取Cache(调用SDImageCache),或者向网络读取对象(调用SDWebImageDownloader) 。
实现SDImageCache和SDWebImageDownloader的回调。
3、SDImageCache,根据URL的MD5摘要对图片进行存储和读取(实现存在内存中或者存在硬盘上两种实现)
实现图片和内存清理工作。
4、SDWebImageDownloader,根据URL向网络读取数据(实现部分读取和全部读取后再通知回调两种方式)

其他类:
SDWebImageDecoder,异步对图像进行了一次解压⋯⋯

解惑
1、SDImageCache是怎么做数据管理的?
SDImageCache分两个部分,一个是内存层面的,一个是硬盘层面的。
内存层面的相当是个缓存器,以Key-Value的形式存储图片。当内存不够的时候会清除所有缓存图片。
用搜索文件系统的方式做管理,文件替换方式是以时间为单位,剔除时间大于一周的图片文件。
当SDWebImageManager向SDImageCache要资源时,先搜索内存层面的数据,如果有直接返回,没有的话去访问磁盘,将图片从磁盘读取出来,然后做Decoder,将图片对象放到内存层面做备份,再返回调用层。

2、为啥必须做Decoder?
通过这个博客:http://www.cocoanetics.com/2011/10/avoiding-image-decompression-sickness/
现在明白了,由于UIImage的imageWithData函数是每次画图的时候才将Data解压成ARGB的图像,
所以在每次画图的时候,会有一个解压操作,这样效率很低,但是只有瞬时的内存需求。
为了提高效率通过SDWebImageDecoder将包装在Data下的资源解压,然后画在另外一张图片上,这样这张新图片就不再需要重复解压了。

这种做法是典型的空间换时间的做法。

SDWebImage是一个图片缓存的框架。相较于AFNetworking集成的UIImageView+AFNetworking.h,对于图片的缓存实际应用的是NSURLCache自带的cache机制。

NSURLCache每次都要把缓存的raw data 再转化为UIImage,就带来了数据处理和内存方面的更多操作。

SDWebImage的缓存由SDImageCache类来实现,这是一个单例类,该类负责处理内存缓存及一个可选的磁盘缓存,其中磁盘缓存的写操作是异步的,这样就不会对UI操作造成影响。此外还提供了若干属性和接口来配置和操作缓存对象。包含以下功能:
1.提供UIImageView的一个分类,以支持网络图片的加载与缓存管理
2.一个异步的图片加载器
3.一个异步的内存+磁盘图片缓存
4.支持GIF图片
5.支持WebP图片
6.后台图片解压缩处理
7.确保同一个URL的图片不被下载多次
8.确保虚假的URL不会被反复加载
9.确保下载及缓存时,主线程不被阻塞

SDWebImage底层实现原理:
SDWebImage有沙盒缓存机制,主要由三块组成
1.内存图片缓存
2.内存操作缓存
3.磁盘沙盒缓存

SDWebImage的大部分工作是由缓存对象SDImageCache和异步下载器管理对象SDWebImageManager来完成的。SDWebImage的图片下载是由SDWebImageDownloader这个类来实现的,它是一个异步下载管理器,下载过程中增加了对图片加载做了优化的处理。而真正实现图片下载的是自定义的一个Operation操作,将该操作加入到下载管理器的操作队列downloadQueue中,Operation操作依赖系统提供的NSURLConnection类实现图片的下载。
SDWebImage提供了对图片缓存的支持,而该功能是由SDImageCache类来完成的。该类负责处理内存缓存及一个可选的磁盘缓存。内存缓存的处理是使用NSCache对象来实现的。NSCache是一个类似于集合的容器。它存储key-value对,这一点类似于NSDictionary类,用搜索文件系统的方式做管理,文件替换方式是以时间为单位。我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。
磁盘缓存的处理则是使用NSFileManager对象来实现的。图片存储的位置是位于Cache文件夹。另外,SDImageCache还定义了一个串行队列,来异步存储图片。
当SDWebImageManager向SDImageCache要资源时,先搜索内存层面的数据,如果有直接返回,没有的话去访问磁盘,将图片从磁盘读取出来,然后做Decoder,将图片对象放到内存层面做备份,再返回调用层。使用Decoder 是因为UIImage的imageWithData函数是每次画图的时候才将Data解压成ARGB的图像,
所以在每次画图的时候,会有一个解压操作,这样效率很低,但是只有瞬时的内存需求。
为了提高效率通过SDWebImageDecoder将包装在Data下的资源解压,然后画在另外一张图片上,这样这张新图片就不再需要重复解压了。是典型的空间换时间的做法。

SDWebImage的原理
1.使用

-(void)sd_setImageWithURL:(NSURL )url placeholderImage:(UIImage )placeholder options:(SDWebImageOptions)options;

会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。

2.进入 SDWebImageManager

-downloadWithURL:options:progress:completed:

交给 SDImageCache 从缓存查找图片是否已经下载。

3.先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,取缓存,没有从- (UIImage )diskImageForKey:(NSString )key去磁盘缓存中去查找,根据 URLKey 在硬盘缓存目录下尝试读取图片文件。在磁盘缓存中找到后,同时更新置内存缓存中(如果空闲内存过小,会先清空内存缓存),有回调则调用doneBlock回调。

4.找到了就从SDWebImageQueryCompletedBlock到 UIImageView+WebCache 等前端展示图片。

5.如果从硬盘缓存目录读取不到图片,说明不存在该图片,需要下载图片,共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。 图片下载由 NSURLSession 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。

6.URLSession:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。数据下载完成后交给 SDWebImageDecoder 做图片解码处理。

7.图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,*好也在这里完成,效率会好很多。

8.在主线程 SDWebImageDownloaderCompletedBlock里处理解码完成后的操作。回调给需要的地方展示图片。

9.从SDWebImageDownloaderProgressBlock 回调给 SDWebImageManager 告知图片下载信息。

10.将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。

11.SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。

12.SDWebImage 也提供了 UIButton+WebCache 和 MKAnnotationView+WebCache方便使用。 SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

SDWebImage 使用
常用到的对象:
1、UIImageView (WebCache)类别,入口封装,实现读取图片完成后的回调。
2、SDWebImageManager,对图片进行管理的中转站,记录那些图片正在读取。
向下层读取Cache(调用SDImageCache),或者向网络读取对象(调用SDWebImageDownloader) 。
实现SDImageCache和SDWebImageDownloader的回调。
3、SDImageCache,根据URL的MD5摘要对图片进行存储和读取(实现存在内存中或者存在硬盘上两种实现)
实现图片和内存清理工作。
4、SDWebImageDownloader,根据URL向网络读取数据(实现部分读取和全部读取后再通知回调两种方式)
5、SDWebImageDecoder,异步对图像进行了一次解压
使用:

[self.imageView sd_setImageWithURL:self.imageURL
placeholderImage:nil
options:SDWebImageProgressiveDownload
progress:^(NSInteger receivedSize, NSInteger expectedSize) {

}
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {

}];

调用setImageWithURL:方法的时候,SDWebImage自动做很多事,当你需要在某一具体时刻做事情的时候,你可以覆盖这些方法。比如在下载某个图片的过程中要响应一个事件,就覆盖这个方法:

//这个方法是下载imagePath2的时候响应
SDWebImageManager *manager = [SDWebImageManager sharedManager];

[manager downloadImageWithURL:imagePath2 options:SDWebImageRetryFailed progress:^(NSInteger receivedSize, NSInteger expectedSize) {

NSLog(@”显示当前进度”);

} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {

NSLog(@”下载完成”);
}];

 

基本代码:
使用SDWebImageManager类:可以进行一些异步加载的工作。

SDWebImageManager *manager = [SDWebImageManager sharedManager];
UIImage *cachedImage = [manager imageWithURL:url]; // 将需要缓存的图片加载进来
if (cachedImage) {
// 如果Cache命中,则直接利用缓存的图片进行有关操作
// Use the cached image immediatly
} else {
// 如果Cache没有命中,则去下载指定网络位置的图片,并且给出一个委托方法
// Start an async download
[manager downloadWithURL:url delegate:self];
}

当然你的类要实现SDWebImageManagerDelegate协议,并且要实现协议的webImageManager:didFinishWithImage:方法。

// 当下载完成后,调用回调方法,使下载的图片显示
– (void)webImageManager:(SDWebImageManager *)imageManager didFinishWithImage:(UIImage *)image {
// Do something with the downloaded image
}

独立的异步图像下载
可能会单独用到异步图片下载,则一定要用downloaderWithURL:delegate:来建立一个SDWebImageDownloader实例。

downloader = [SDWebImageDownloader downloaderWithURL:url delegate:self];

这样SDWebImageDownloaderDelegate协议的方法imageDownloader:didFinishWithImage:被调用时下载会立即开始并完成。
独立的异步图像缓存
SDImageCache类提供一个创建空缓存的实例,并用方法imageForKey:来寻找当前缓存。

UIImage *myCachedImage = [[SDImageCache sharedImageCache] imageFromKey:myCacheKey];

存储一个图像到缓存是使用方法storeImage: forKey:

[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];

默认情况下,图像将被存储在内存缓存和磁盘缓存中。如果仅仅是想内存缓存中,要使用storeImage:forKey:toDisk:方法的第三个参数带一负值
来替代。
SDWebImage 源码分析示例
SDWebImageDownloader类
SDWebImageDownloaderOptions定义:

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
SDWebImageDownloaderLowPriority = 1 << 0,
SDWebImageDownloaderProgressiveDownload = 1 << 1,

// 默认情况下请求不使用NSURLCache,如果设置该选项,则以默认的缓存策略来使用NSURLCache
SDWebImageDownloaderUseNSURLCache = 1 << 2,

// 如果从NSURLCache缓存中读取图片,则使用nil作为参数来调用完成block
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,

// 在iOS 4+系统上,允许程序进入后台后继续下载图片。该操作通过向系统申请额外的时间来完成后台下载。如果后台任务终止,则操作会被取消
SDWebImageDownloaderContinueInBackground = 1 << 4,

// 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES来处理存储在NSHTTPCookieStore中的cookie
SDWebImageDownloaderHandleCookies = 1 << 5,

// 允许不受信任的SSL证书。主要用于测试目的。
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,

// 将图片下载放到高优先级队列中
SDWebImageDownloaderHighPriority = 1 << 7,
};

 

下载顺序:

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
// 以队列的方式,按照先进先出的顺序下载。这是默认的下载顺序
SDWebImageDownloaderFIFOExecutionOrder,
// 以栈的方式,按照后进先出的顺序下载。
SDWebImageDownloaderLIFOExecutionOrder
};

 

每个下载操作都定义了回调操作,如下载进度回调,下载完成回调,头部过滤等,这些回调操作是以block形式来呈现;每个下载操作的下载进度回调和下载完成回调,这两个回调稍后将保存在下载管理器的URLCallbacks字典中,key为URL,value为一个数组,数组里面又存放一个保存了下载进度回调和完成回调代码块的字典。这个字典数组同时也保证了同一张图片只会被下载一次。

// 下载进度
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
// 下载完成
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
// Header过滤
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);

 

为了保证URLCallbacks操作(添加、删除)的线程安全性,SDWebImageDownloader将这些操作作为一个个任务放到barrierQueue队列中,并设置屏障来确保同一时间只有一个线程操作URLCallbacks属性。

– (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {

// 1. 以dispatch_barrier_sync操作来保证同一时间只有一个线程能对URLCallbacks进行操作
dispatch_barrier_sync(self.barrierQueue, ^{

// 2. 处理同一URL的同步下载请求的单个下载
});
}

下载请求的管理都是放在downloadImageWithURL:options:progress:completed:方法里面来处理的,该方法调用了上面所提到的addProgressCallback:andCompletedBlock:forURL:createCallback:方法来将请求的信息存入管理器中,同时在创建回调的block中创建新的操作,配置之后将其放入downloadQueue操作队列中,*后方法返回新创建的操作。

– (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {

[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{

}

下载操作的超时时间可以通过downloadTimeout属性来设置,默认值为15秒。
SDWebImage定义了一个协议,即 SDWebImageOperation 作为图片下载操作的基础协议。它只声明了一个cancel方法,用于取消操作。每个图片的下载都是一个Operation操作。SDWebImage自定义了一个Operation类,即 SDWebImageDownloaderOperation ,它继承自NSOperation,并采用了SDWebImageOperation协议。除了继承而来的方法,该类只向外暴露了一个方法,initWithRequest:options:progress:completed:cancelled:。对于图片的下载,SDWebImageDownloaderOperation完全依赖于URL加载系统中的NSURLSession。具体看代码源码分析.

– (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
1
方法的主要任务是接收数据。每次接收到数据时,都会用现有的数据创建一个CGImageSourceRef对象以做处理。在首次获取到数据时(width+height==0)会从这些包含图像信息的数据中取出图像的长、宽、方向等信息以备使用。而后在图片下载完成之前,会使用CGImageSourceRef对象创建一个图片对象,经过缩放、解压缩操作后生成一个UIImage对象供完成回调使用。当然,在这个方法中还需要处理的就是进度信息。如果我们有设置进度回调的话,就调用这个进度回调以处理当前图片的下载进度。
缩放操作可以查看SDWebImageCompat文件中的SDScaledImageForKey函数;解压缩操作可以查看SDWebImageDecoder文件+decodedImageWithImage方法。在下载完成或下载失败后,需要停止当前线程的run loop,清除连接,并抛出下载停止的通知。如果下载成功,则会处理完整的图片数据,对其进行适当的缩放与解压缩操作,以提供给完成回调使用。

IOS-WKWebView的使用以及Cookie的处理

其实和UIWebView的用法没什么区别,但是WKWebView相对于UIWebView强大了很多,内存的消耗相对少了,所提供的接口也丰富了。

WKWebView的代理方法:

WKNavigationDelegate代理的使用:

该代理提供的方法,可以用来追踪加载过程(页面开始加载、加载完成、加载失败)、决定是否执行跳转。
// 页面开始加载时调用
– (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation;
// 当内容开始返回时调用
– (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation;
// 页面加载完成之后调用
– (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;
// 页面加载失败时调用
– (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation;

页面跳转的代理方法有三种,分为(收到跳转与决定是否跳转两种)

// 接收到服务器跳转请求之后调用
– (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation;
// 在收到响应后,决定是否跳转
– (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
// 在发送请求之前,决定是否跳转
– (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

代理方法的展开:

#pragma mark – WKNavigationDelegate –
/**
* 页面开始加载时调用
*
* @param webView 实现该代理的webview
* @param navigation 当前navigation
*/
– (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
NSLog(@”%s”, __FUNCTION__);
}

/**
* 当内容开始返回时调用
*
* @param webView 实现该代理的webview
* @param navigation 当前navigation
*/
– (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
NSLog(@”%s”, __FUNCTION__);
}

/**
* 页面加载完成之后调用
*
* @param webView 实现该代理的webview
* @param navigation 当前navigation
*/
– (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
self.progressView.alpha = 0;
NSLog(@”%s”, __FUNCTION__);
}

/**
* 加载失败时调用
*
* @param webView 实现该代理的webview
* @param navigation 当前navigation
* @param error 错误
*/
– (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error
{
NSLog(@”%s”, __FUNCTION__);
}

/**
* 接收到服务器跳转请求之后调用
*
* @param webView 实现该代理的webview
* @param navigation 当前navigation
*/
– (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {

NSLog(@”%s”, __FUNCTION__);
}

/**
* 在收到响应后,决定是否跳转
*
* @param webView 实现该代理的webview
* @param navigationResponse 当前navigation
* @param decisionHandler 是否跳转block
*/
– (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {

NSString *clickUrl = [navigationResponse.response.URL absoluteString];
self.barView.addressLabel.text = clickUrl;
NSLog(@”%@”, navigationResponse.response.URL);
decisionHandler(WKNavigationResponsePolicyAllow);
}

/**
* 在发送请求之前,决定是否跳转
*
* @param webView 实现该代理的webview
* @param navigationAction 当前navigation
* @param decisionHandler 是否调转block
*/
– (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {

decisionHandler(WKNavigationActionPolicyAllow);
}

 

WKUIDelegate代理的使用:

// 创建一个新的WebView
– (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures;

剩下三个代理方法全都是与界面弹出提示框相关的,针对于web界面的三种提示框(警告框、确认框、输入框)分别对应三种代理方法。下面只举了警告框的例子。

#pragma mark – WKUIDelegate

/**
* web界面中有弹出警告框时调用
*
* @param webView 实现该代理的webview
* @param message 警告框中的内容
* @param frame 主窗口
* @param completionHandler 警告框消失调用
*/
– (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {

UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@”温馨提示” message:message preferredStyle:(UIAlertControllerStyleAlert)];
UIAlertAction *trueAction = [UIAlertAction actionWithTitle:@”确定” style:(UIAlertActionStyleCancel) handler:^(UIAlertAction *action) {

}];
[alertController addAction:trueAction];
[self presentViewController:alertController animated:YES completion:nil];

completionHandler(YES);
}

– (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler {

}

 

WKScriptMessageHandler代理的使用:

这个协议中包含一个必须实现的方法,这个方法是提高App与web端交互的关键,它可以直接将接收到的JS脚本转为OC或Swift对象。(当然,在UIWebView也可以通过“曲线救国”的方式与web进行交互,著名的Cordova框架就是这种机制)。

#pragma mark – WKScriptMessageHandler
– (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {

NSLog(@”%@”, message);
}

– (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

 

 

WKWebView加载JS:

// 图片缩放的js代码
NSString *js = @”var count = document.images.length;for (var i = 0; i < count; i++) {var image = document.images[i];image.style.width=320;};window.alert(‘找到’ + count + ‘张图’);”;
// 根据JS字符串初始化WKUserScript对象
WKUserScript *script = [[WKUserScript alloc] initWithSource:js injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
// 根据生成的WKUserScript对象,初始化WKWebViewConfiguration
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
[config.userContentController addUserScript:script];
_webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
[_webView loadHTMLString:@””baseURL:nil];
[self.view addSubview:_webView];

iOS开发-关于iOS11适配的一些坑

安全区域(Safe Area)
在iOS11以前, 自定义UIViewController时, 从来没有考虑过topLayoutGuide和bottomLayoutGuide这两个属性.但是在iOS11出来之后,这两个属性被废弃,取而代之的就是safeArea的概念,safeArea是描述你的视图部分不被任何内容遮挡的方法。 它提供两种方式:safeAreaInsets或safeAreaLayoutGuide来提供给你safeArea的参照值,即 insets 或者 layout guide。 safeArea区域如图所示:

iPhone X图
在除了iPhone X以外的型号手机上, UINavigationController 就是 topLayoutGuide区域. UITabBarController就是bottomLayoutGuide.但是这两个属性在iOS11已经被取代了. 取而代之的就是safeArea区域(iPhone X图区域4部分)

当我们自定义一个UIViewController, 内部嵌套了UINavigationController 或者UITabBarController的时候. 运行程序会出现以下错误:

图1-1
原因是少了:

具体是为什么,没有细研究.不过解决办法如下:

图1-2
因为我是创建的空项目所以只有LaunchScreen.storyboard..如果你的项目里还有Main.storyboard,那么 你需要把第四个勾勾也去掉.

iPhone X 竖屏UINavigationController和UITabBarController尺寸问题

iPhone X竖屏
区域一: 高度30

区域二:高度14

区域三:高度44

区域五:高度49

区域六:高度14

区域七:高度20

iOS横屏UITabBarController
更新iOS11以后, UITabBarController上的图标文字由原来的垂直放置, 变成水平放置

iPhone X 横屏UINavigationController和UITabBarController尺寸问题

iPhone X横屏
iPhone X 横竖屏在尺寸上有很多细微的差别.

UINavigationController
我们打印UINavigationController navigationBar的时候发现他的高度为44.也就是上图绿色区域.但是在实际显示的时候确实红色区域,高度为32.整个高度向上12.也就是蓝色区域.

UITabBarController
我们打印UITabBarController tabBar的时候发现他的高度为32,就是橘色区域,红色区域为功能区,高度为21.我们是何止tabBar颜色的时,包括橘色区和红色区.

控制大标题的显示
待更新

屏幕未充满?底部tabbar留白
解决方案: 准备一张尺寸:1125 * 2436的启动图片, 添加到Assret中

iOS11导航栏自定义按钮偏移问题
解决方案: 通过改变按钮的 contentEdgeInsets和imageEdgeInsets的值成功改变了按钮的偏移问题,单独设置contentEdgeInsets也可达到一定的效果。

UITableview UICollectionView MJRefresh下拉刷新错乱
解决方案:iOS11弃用了automaticallyAdjustsScrollViewInsets属性,新增contentInsetAdjustmentBehavior来替代它

IOS 微信端 视频自动播放问题解决方法

问题描述:由于浏览器限制,视频不能够自动播放,需要用户手势触发才可以。

解决方法:通过微信浏览器提供的WeixinJSBridgeReady做桥接后就可以绕过这一限制。

上面链接中的操作比较繁琐,在此给出简化版本(亲测可用):

function doPlay(){
WeixinJSBridge.invoke(‘getNetworkType’, {}, function (e) {
var $video1 = $(“#video1”)
var $video2 = $(“#video2”)
$video1[0].play()
$video2[0].play()
})
}

if (window.WeixinJSBridge) {
doPlay()
} else {
document.addEventListener(“WeixinJSBridgeReady”, function(){
doPlay()
}, false);
}
需要注意的是,监听WeixinJSBridgeReady事件后,回调函数里需要调用一下invoke,在invoke中操作视频才可以生效。

另外,页面不需要引入jweixin-1.0.0.js,微信浏览器会自带api。

VUE前端界面在iOS中遇到的坑

一、滑动效果卡顿问题解决方案
在需要滑动的位置加上如下css代码:
-webkit-overflow-scrolling:touch;
以此,界面滑动卡顿问题得以解决,但是,这一行代码会带来一个副作用:
1、在滑动界面之中使用的position:fixed 无法固定下来,会随着界面进行一起滚动
解决方法:将使用的position:fixed(头部导航)写在滑动部位外部,在使用*对定位进行布局,以此解决问题
2、vue中使用v-if导致的界面初始化之后无法滑动
解决方法:将v-if改成v-show进行展示,解决界面进入之后*次不能滑动的问题

二、界面点反应慢、延时问题解决方案
经过排查,导致问题的原因是iOS嵌套界面之后,界面点击效果会自动产生一个300毫秒的延时,解决方案:
vue 引入 fastclick(npm install fastclick -S)
然后再进行引入 FastClick.attach(document.body);
问题解决
注:在引入fastclick之后,会有一个副作用:input输入框需要连续点击两次或者长按才能获取焦点,解决方法详见个人文章:
vue引入fastClick导致的输入框点击无响应问题

三、按钮点击之后会产生灰色图层解决方案
目前未发现前端有效的解决方案,本次是让iOS进行单独的处理

四、关于new Date()转换时间在iOS中不生效问题
前端界面使用new Date(‘2018-12-20 23:59:59’).getTime(),放到iOS中后发现,没有正常转换成时间戳,经过查找相关资料发现,
iOS不支持该种时间格式的转换,将代码写成:
new Date(‘2018-12-20T23:59:59’).getTime()
即可获取到正确的时间戳

五、关于部分拷贝方法在iOS中不生效问题
前端有时候需要用到拷贝部分内容到剪切板中,但是在使用时发现,部分网上提供的拷贝方法在Android中正常使用,但是在iOS中无法进行拷贝,经过试验发现一种通用的拷贝方法,如下:
/**

将内容拷贝到剪切板中
@param copyText 需要拷贝的内容
*/
function copyContent(message) { // text: 要复制的内容, callback: 回调
var input = document.createElement(“input”);
input.value = message;
document.body.appendChild(input);
input.select();
input.setSelectionRange(0, input.value.length);
document.execCommand(‘Copy’);
document.body.removeChild(input);
}
该方法亲测在Android以及iOS中都有效
六、关于input输入框在iOS中获取到焦点之后界面上移无法回落问题
在联合移动端开发过程中,会遇到input输入框获取到焦点之后,软键盘自动顶起界面,但是失去焦点之后无法回落的问题,解决方法如下,添加如下代码:
const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop || 0;
window.scrollTo(0, Math.max(scrollHeight – 1, 0));

七、关于移动端定时器运行过程中,界面点击/滑动会造成定时器卡顿的现象
原因:界面点击时,iOS会有300毫秒延时阻塞,同时界面滑动时,也会造成定时器方法的阻塞
解决方法:
1.使用fastClick结局300毫秒延时问题,参考文章 第二点
2.定时器运转期间,使用css或者js禁止界面滚动

ISO7816 调试心得

ISO7816是一种接触式的读卡协议,PSAM、SAM、手机的SIM卡都是按照这个协议定制的。协议分为2个部分,复位和数据的传输。本文档主要介绍自己的理解和在调试的过程,具体协议部分的内容需要翻看ISO7816的协议文档。本文重点是带着数据对协议进行分析。适合刚接触ISO7816的人。加深对协议的理解。

一、复位

上电复位或者手动给卡复位之后,卡会发出一段数据,这段数据是代表卡本身的一些信息,如发送的格式是什么样子的,卡支持什么样的协议,卡片的号码等等。复位的结构如下:

 

TS…………………………………………表示正向或者反向约定,强制性在

T0…………………………………………格式字符,强制性

TA(i) TB(i) TC(i) TD(i)…………… 接口字符,可选的

T1 T2…Tk…………………………………历史字符,可选的

Tck……………………………………… 检测字符,有条件的

 

*个数据是TS,我个人理解是大端和小端的问题,*个数据永远是“0X3B”或者是“0X3F”,其中“0X3B”代表正向约定,“0X3F”代表反向约定,在我见到的卡片里面,只见过“0X3B”,还没有见过“0X3F”。如果*个数据不是这两个值,那么代表接收的时序有问题,需要查看驱动或者硬件是否有问题。

 

第二个数据是T0,这个数据比较重要,他决定了下面的数据代表什么意思。也就是说,从第三个数据开始,每个数据代表什么意思是不确定的。T0的结构如下。

b8

msb

b7

b6

b5

b4

b3

b2

b1

lsb

Y(1)

K

图1 — T0编码

Y(1)……接口字节存在的标记

b5=1时TA(1)存在

b6=1时TB(1)存在

b7=1时TC(1)存在

b8=1时TD(1)存在

K……历史字节的数目,从0到15

由图1 可知,第三个数据代表什么意思,是由T0的高4位决定的。T0的低4位代表是历史字节的数目,这个后面在介绍。

 

第三个数据是有T0决定的,下面我就举一段例子,来说明。下面是我读取到的一张卡的数据。

1

0x3b

2

0x69

3

0x00

4

0x00

5

0x45

6

0x53

7

0x41

8

0x4d

9

0x10

10

0xd3

11

0x4c

12

0x8a

13

0xe6

上面这段数据是我抓取的一张T=0协议卡的复位数据。*个数据是“0x3B”,代表TS,是正向约定,就是小端的意思。第二个数据是0X69,代表T0,二进制“01101001”,则带表下面第三个数据是TB1,因为其b6位是1,那么顺下去,b7位也是1,则第四个数据就代表TC1,所以说TA(i) TB(i) TC(i) TD(i)是可选字符(i代表1,2,3,4、、、由于可能存在好多组TA、TB、TC、TD,所以用i来表示),也就是说他们不一定都存在,会根据T0和TD(i)来决定。至于TA(i) TB(i) TC(i) TD(i)是干什么用的,我们下面会介绍。这组数据由于T0的b8位由于没有TD1,所以就没有TA2、TB2、TC2、TD2、、、、、以及以后的数据,那么,剩下的数据代表什么意思的,剩下的从第5个开始到第13个,代表的是历史字符,也就是T1 T2…Tk,我们上面说了,历史字符的个数是由T0的低4位决定的,我们这组数据中,T0为0X69,低4位是9,则说明有9个历史数据,而剩下的数据正好是9个,由于*后一个字符Tck是有条件的,也就是说不一定存在,在T=0的卡中,一般是不存在Tck的。分析到这里,这组数据和我们的复位协议也都对上了。简单的总结一下,这组数据代表了这张卡是正向约定的卡,存在TB1和TC1,有9个历史字符。那么这张卡到底是T=0的卡还是T=1的卡,根据现在的情报还没办法分析出来。下面开始介绍TA(i) TB(i) TC(i) TD(i)。

 

TA(1)

— FI,位b8到b5 上的时钟率转换因子的引用。

— DI,位b4到b7上波特率校正因子的引用。

这位和etu有关系,总之是和两个数据的传输间隔时间相关。这位确定了卡支持的传输速率。

 

TB(1)  b8=0代码处

— II,位b7 b6上*大编程电流的引用。

— PI1,位b5到b1上编程电压的值。

注:接口设备可以忽略TB(1)的位b8。

这一位我也没用到过。所以这里也就不介绍了。

 

TC(1)代码(见8.5.3)

— N,计算八位额外保护时间的引用。

—  这一位我也没用到过。所以这里也就不介绍了。

 

下面是重点TD1

字节TD1由两部分组成。

— 位b8到b5构成Y2);每个等于1的位指明接口字节的存在。

— 位b4到b1构成8.2中定义的参数T的值。

 

b8

msb

b7

b6

b5

b4

b3

b2

b1

lsb

Y(2)

T

图2 — TD(1)编码

Y(2)……接口字节存在的标记

b5=1时TA(2)存在

b6=1时TB(2)存在

b7=1时TC(2)存在

b8=1时TD(2)存在

T……协议参考和/或接口字节限制符

TD1和T0差不多,高4为代表了TA(2)TB(2) TC(2) TD(2)是否存在,低4为代表了这张卡支出什么协议,参数T的定义如下:

——T=0        异步半双工字符传输协议 在第8章中说明。

——T=1        异步半双工块传输协议 在第9章中说明。

——T=2和T=3    保留用于将来的全双工操作。

——T=4        保留用于增强的异步半双工字符传输协议。

——T=5到T=13   保留待未来使用。

——T=14       未由ISO/IEC JTC1 SC17标准化的传输协议

——T=15       不属于传输协议,仅指明了全程接口字节的类型(见8.4.3.2)

TD2、TD3的高四位定义和TD1差不多,类推一下就好了,低4位目前我没用过,反正我判断是什么协议的卡只判断TD1的低4位,TD2、TD3的低4位我直接忽略了。如果TD1不存在,则代表了该卡支持的是T=0的卡。

TA2、TB2、TC2我也没有用到,所以没有仔细研究过,如果想要了解就需要看协议的相关文档。

TA3 也是比较重要的一个值,他确定了IFSC的大小,IFSC后面会介绍,这里只要记住有这个东西,并且比较重要就好了。

TB3的值和CWT和BWT,这两个值关系到了数据传输的时间,具体看协议,我也没有了解过。

TC3的值也比较重要,他决定T=1的检验位采用什么方式,第1位是0,则采用LRC校验,第1位是1,则采用CRC校验。如果没有TC3,则代表采用LRC校验。

接下来的数据是历史字符,具体代表什么意思需要看卡片。我也没研究过。

还有*后一个数据,Tck—校验字符

TCK具有一个检验复位应答期间所发送数据完整性的值。TCK的值应使从T0到包括TCK在内的所有字

由于T=0的卡没有校验,所以没有该值,在T=1的卡中,会正常返回该值。

 

下面我给出一组T=1卡的复位数据,进行分析。

序号

分析

1

0x3b

TS,正向约定

2

0xff

T0,有TA1、TB1、TC1、TD1,历史数据为15个

3

0x18

TA1控制了传输的速率,详细看下面的介绍

4

0x00

TB1 我也不知道具体什么意思

5

0xff

TC1 我也不知道具体什么意思

6

0x81

TD1,有TD2,卡支持T=1协议

7

0x31

TD2,有TA3和TB3,后四位好像不是代表支持的协议

8

0xfe

TA3,代表IFSC的值为254

9

0x45

TB3 我也不知道具体什么意思

10

0x65

T1

11

0x63

T2

12

0x0d

T3

13

0xoc

T4

14

0x76

T5

15

0x01

T6

16

0x56

T7

17

0x00

T8

18

0x0d

T9

19

0x92

T10

20

0x94

T11

21

0x03

T12

22

0x00

T13

23

0x07

T14

24

0x30

T15

25

0x0a

Tck,*后的校验位

 

二、数据传输

1、 传输速度

卡的传输速度除了有卡本身的决定外,还由CLK的时钟决定。在ISO7816协议中规定,CLK的时钟必须1Mhz-5Mhz,CLK的时钟是由读卡器决定的,而DATA的位持续时间是由CLK和卡本身决定的,所以说,卡的传输速度是由读卡器和卡共同决定。这里有一个叫做etu的东西,我把他理解成为每一位的持续时间。在复位期间,etu的计算如下:

etu =372 / f

其中F即为CLK的时钟,假如CLK的时钟为3.6M,则

etu =372 / 3600000 = 103us

那么,在CLK为3.6M的情况下,波特率为

1 / (372 / 3600000)= 9677

“STM32 ISO7816 智能卡”的DEMO中,其CLK的时钟输出就是3.6M,所以其波特率为9677。而我在调试过程中,将STM32的主频修改为8M,那么,CLK无论怎么分频都无法输出3.6M,所以我只能将CLK设置为2M,那么相应的波特率也改为了5376。

而复位的信息中包含了时钟率转换因子(FI)和波特率矫正因子(DI),那么etu的计算就变成了:

etu =FI / (DI * f)

FI和DI由TA决定,一般情况下,FI=372,DI=1,所以,一般情况下,复位完成后无需重新设置波特率,如果有遇到特殊的卡,在复位完成之后还需要对波特率进行重新设置。

2、 传输协议

卡支持不同的协议,传输方式也不同,目前*常见的就是T=0的卡和T=1的卡,他们的传输都是通过APDU格式进行传输,协议的内容我就不介绍了。

其中T=0叫字符传输,T=1叫块传输。说白了就是T=0的卡没有校验,你不知道还剩下多少个数据。也不知道传输过来的数据对不对。T=1的卡就不一样了,分为头域、信息域、尾域。其中,头域代表了传输的地址、命令以及信息域的长度,信息域代表的意思和头域有关,如果发送的是I块,那么信息域就是APUD数据,如果发送的S块,那么信息域代表了控制信息,R块的信息域为0,我们上面说的IFSC指的就是信息域的大小,刚刚那组数据中,IFSC的值为254,那么代表传输过程中,信息域*大的长度就是254。尾域的作用就是校验接收的数据是否正确。

T=0的卡百度也有很多现成的代码,直接搜“STM32 ISO7816 智能卡 代码”应该就能找得到。可以做一些参考。T=1的卡百度代码较少。可以在美信的官网上找一下智能卡相关的内容,应该可以找得到源码,或者在下面的连接下载也可以。

 

https://www.maximintegrated.com/cn/design/tools/appnotes/4200/an4200_sw.zip

 

这个代码支持T=0和T=1的卡,可以用作参考。

 

另外需要注意的就是,T=1的卡复位完成之后,必须先发一个S块,应该是规定好的。

 

 

 

下面给出一组T=0的卡传输过程中的数据分析。

序号

收发

分析

1

0x00

CLA 指令类别 根据卡的文档决定

2

0x84

INS 指令类别 0x84代表获取随机数

3

0x00

P1

4

0x00

P2

5

0x04

LE(没有LC,则没有DATA),希望得到4个数据

6

0x84

INS,卡回应INS,则代表相应了这个命令

7

0x86

DATA1

8

0x91

DATA2

9

0xd3

DATA3

10

0x48

DATA4

11

0x90

SW1 和SW2一起表示正常结束

12

0x00

 

 

这支持一个简单的测试卡是否可以正常通信的命令。该支持的意思是获取4个随机数。其实每个代表什么意思,还需要看卡的文档。

 

下面给出一组T=1的卡传输过程中的数据分析,举一个I块的例子。

序号

收发

分析

1

0x00

NAD 节点地址,一般都是0

2

0x00

PCB 协议控制字节 b0=1代表是I块,b7=0代表是第1组数据,b6=0代表是*后一个块,剩下的字节保留

3

0x05

LEN 长度,代表信息域的长度为5

4

0x00

信息域,其实内容和T=0一样的

CLA 指令类别 根据卡的文档决定

5

0x84

INS 指令类别 0x84代表获取随机数

6

0x00

P1

7

0x00

P2

8

0x04

LE(没有LC,则没有DATA),希望得到4个数据

9

0x85

EDC 校验位,前面所有数据的异或值

10

0x00

NAD 节点地址,和发送的一样

11

0x00

PCB 协议控制字节 b0=1代表是I块,b7=0代表是第1组数据,b6=0代表是*后一个块,剩下的字节保留

12

0x20

LEN 长度,代表信息域的长度为2

13

0x67

SW1 0x67代表长度错误

14

0x00

 

15

0x1e

EDC 校验位,前面所有数据的异或值

 

我特意选了一组带有错误值,发送的数据和T=0的卡一样,是一个希望得到4个随机数的指令,而返回的只有SW1和SW2,没有DATA的值,SW1为0X67代表数据的长度错误,后来才发现,我手里的这张T=1的卡只支持获取8个随机数的指令,所以,把LE改成8之后,接收的数据就正确了。

c语言代码 linux 关机_Linux关机和重启命令

说到关机和重启,很多人认为,重要的服务器(比如银行的服务器、电信的服务器)如果重启了,则会造成大范围的灾难。笔者在这里解释一下。

首先,就算是银行或电信的服务器,也不是不需要维护,而是依靠备份服务器代替。其次,每个人的经验都是和自己的技术成长环境息息相关的。比如笔者是游戏运维出身,而游戏又是数据为王,所以一切操作的目的就是保证数据的可靠和安全。这时,有计划的重启远比意外岩机造成的损失要小得多,所以定义重启是游戏运维的重要手段。

shutdown命令

在早期的 Linux 系统中,应该尽量使用 shutdown 命令来进行关机和重启。因为在那时的 Linux 中,只有 shutdown 命令在关机或重启之前会正确地中止进程及服务,所以我们一直认为 shutdown 才是*安全的关机与重启命令。

而在现在的系统中,一些其他的命令(如 reboot)也会正确地中止进程及服务,但我们仍建议使用 shutdown 命令来进行关机和重启。

shutdown 命令的基本信息如下。

命令名称:shutdown。

英文原意:bring the system down。

所在路径:/sbin/shutdown。

执行权限:超级用户。

功能描述:关机和重启

命令格式

[root@localhost ~]# shutdown [选项] 时间 [警告信息]

选项:

-c:取消已经执行的 shutdown 命令;

-h:关机;

-r:重启;

【例 1】重启与定时重启。

先来看看如何使用 shutdown 命令进行重启:

[root@localhost ~]# shutdown -r now

#重启, now是现在重启的意思

[root@localhost ~]# shutdown -r 05:30

#指定时间重启,但会占用前台终端

[root@localhost ~]# shutdown -r 05:30 &

#把定义重启命令放入后台,&是后台的意思

[root@localhost ~]# shutdown -c

//取消定时重启

[root@localhost ~]# shutdown -r +10

#10分钟之后重启

【例 2】关机和定时关机。

[root@localhost ~]# shutdown -h now

#现在关机

[root@localhost ~]# shutdown -h 05:30

#指定时间关机

reboot命令

在现在的系统中,reboot 命令也是安全的,而且不需要加入过多的选项。

[root@localhost ~]# reboot

#重启

halt和poweroff命令

这两个都是关机命令,直接执行即可。

[root@localhost ~】# halt

#关机

[root@localhost ~】# poweroff

#关机

init命令

init 是修改 Linux 运行级别的命令,也可以用于关机和重启。

[root@localhost~]# init 0

#关机,也就是调用系统的 0 级别

[root@localhost ~】# init 6

#重启,也就是调用系统的 6 级别

一行代码搞定 Python 日志!

一行代码搞定 Python 日志!

 

写了这么多年的 Python ,我一直都是使用 Python 自带的 logging 模块来记录日志,每次需要写一些配置将日志输出到不同的位置,设置不同日志输出格式,或者将日志进行分文件和压缩等。这个日志模块没什么问题,直到我无意中发现了一个神器,我才发觉原来记日志可以这么简单的!这个神器就是 loguru 。

安装
这个库的安装方式很简单,直接使用 pip 就可以,我使用 Python 3 版本,安装命令如下:

pip3 install loguru
小试牛刀
安装完毕之后,我们就可以使用了,*简单的使用方式:

from loguru import logger

logger.debug(‘this is a debug message’)

无需任何配置,即取即用。上例是打印一条 debug 级别的日志,输出结果如下:

2021-03-16 22:17:23.640 | DEBUG    | __main__:<module>:8 – this is a debug message

这条输出日志信息包含了日期、时间、日志级别、日志代码行数以及日志内容信息。可以说*基本的内容都囊括了,当然你还可以打印 warning、info、error、critical、success 等级别。输出的日志在 console 中还带有高亮颜色,并且每个级别的日志颜色不一样,简直不要太酷!

日志文件
写文件
在loguru中,输出日志文件只需要一个 add() 函数即可:

logger.add(‘hello.log’)

logger.debug(‘i am in log file’)

这时候,在 console 中会正常打印日志信息,在同级目录下会生成一个日志文件 hello.log ,我们打开日志文件,可以看到内容如下:

2021-03-16 21:20:31.460 | DEBUG    | __main__:<module>:12 – i am in log file

当然,我们还可以加一些参数,来指定文件中日志输出的格式、级别:

log = logger.add(‘world.log’, format=”{time} | {level} | {message}”, level=”INFO”)

logger.debug(‘i am debug message’)
logger.info(‘i am info message’)

对应的文件输出信息如下:

2021-03-16T22:47:53.226998+0800 | INFO | i am info message

我们设置了文件只记录 info 级别的信息,所以 debug 级别的日志信息并没有写入日志文件。

我们也可以给日志文件名称加信息:

logger.add(‘hello_{time}.log’)

上面的代码运行后,会生成一个带时间的日志文件。

停止写入文件
当我们不再需要将日志写入文件时,我们随时可以停止:

id = logger.add(‘world.log’, format=”{time} | {level} | {message}”, level=”INFO”)
logger.info(‘this is a info message’)
logger.remove(id)
logger.info(‘this is another info message’)

add() 方法会返回一个日志文件的 id ,当我们需要停止写入信息时,我们使用 remove() 方法,传入 id ,即可。上面代码运行后,日志文件记录的信息如下:

2021-03-16T22:47:53.227389+0800 | INFO | this is a info message

在调用 remove() 方法后,其后面的日志信息并没有写入日志文件中。

滚动记录日志文件
我们可以配置 rotation 参数,来指定日志文件的生成方式,跟通常的日志记录一样,我们可以设置按照文件大小、时间、日期等来指定生成策略。

# 超过200M就新生成一个文件
logger.add(“size.log”, rotation=”200 MB”)
# 每天中午12点生成一个新文件
logger.add(“time.log”, rotation=”12:00″)
# 一周生成一个新文件
logger.add(“size.log”, rotation=”1 week”)

指定日志文件的有效期
我们还可以通过 retention 参数来指定日志文件的保留时长:

logger.add(“file.log”, retention=”30 days”)

通过上面的配置,可以指定日志文件*多保留30天,30天之前的日志文件就会被清理掉。

配置压缩文件
为了节省空间,我们可能存在压缩日志文件的需求,这个 loguru 也可以实现:

logger.add(“file.log”, compression=”zip”)

通过上面的配置,我们指定了日志文件的压缩格式为 zip 。

异常捕获
loguru 不仅可以记录日志,还可以捕获异常信息,这个 可以帮助我们更好地追溯错误原因。

在 loguru 模块中,我们通常有两种异常捕获方式:通过 catch 装饰器捕获和通过 exception 方法捕获。

catch 装饰器捕获异常
我们来看一个例子:

@logger.catch
def a_function(x):
return 1 / x

a_function(0)

输出信息如下:

021-03-16 23:10:28.124 | ERROR    | __main__:<module>:32 – An error has been caught in function ‘<module>’, process ‘MainProcess’ (25939), thread ‘MainThread’ (140735895298944):
Traceback (most recent call last):
File “/Users/cxhuan/Library/Application Support/JetBrains/IntelliJIdea2020.3/plugins/python/helpers/pydev/pydevconsole.py”, line 483, in <module>
pydevconsole.start_client(host, port)
│            │            │     └ 62146
│            │            └ ‘127.0.0.1’
│            └ <function start_client at 0x10fd596a8>
└ <module ‘pydevconsole’ from ‘/Users/cxhuan/Library/Application Support/JetBrains/IntelliJIdea2020.3/plugins/python/helpers/py…

……

> File “/Users/cxhuan/Documents/python_workspace/mypy/loguru/logurustudy.py”, line 32, in <module>
a_function(0)
└ <function a_function at 0x11021e620>
File “/Users/cxhuan/Documents/python_workspace/mypy/loguru/logurustudy.py”, line 30, in a_function
return 1 / x
└ 0
ZeroDivisionError: division by zero
上面的代码中,我特意造了一个 1 除以 0 的异常,我们可以看到日志输出信息非常详细,将每一步调用的错误信息都详细的列出来,并且还把参数的值也打印出来了,还有非常直观的指向性,简直是异常分析神器!

exception 方法捕获异常
我们直接看例子:

def b_function1(x):
try:
return 1 / x
except ZeroDivisionError:
logger.exception(“exception!!!”)

b_function1(0)

运行上面代码,输出信息如下:

2021-03-16 23:16:07.602 | ERROR    | __main__:b_function1:40 – exception!!!
Traceback (most recent call last):
File “/Users/cxhuan/Library/Application Support/JetBrains/IntelliJIdea2020.3/plugins/python/helpers/pydev/pydevconsole.py”, line 483, in <module>
pydevconsole.start_client(host, port)
│            │            │     └ 62254
│            │            └ ‘127.0.0.1’
│            └ <function start_client at 0x118d216a8>
└ <module ‘pydevconsole’ from ‘/Users/cxhuan/Library/Application Support/JetBrains/IntelliJIdea2020.3/plugins/python/helpers/py…
File “/Users/cxhuan/Library/Application Support/JetBrains/IntelliJIdea2020.3/plugins/python/helpers/pydev/pydevconsole.py”, line 411, in start_client
process_exec_queue(interpreter)
│                  └ <_pydev_bundle.pydev_ipython_console.InterpreterInterface object at 0x118d36240>
└ <function process_exec_queue at
0x118d21400>

……

File “/Users/cxhuan/Documents/python_workspace/mypy/loguru/logurustudy.py”, line 42, in <module>
b_function1(0)
└ <function b_function1 at 0x11913b598>
> File “/Users/cxhuan/Documents/python_workspace/mypy/loguru/logurustudy.py”, line 38, in b_function1
return 1 / x
└ 0
ZeroDivisionError: division by zero
同样地,也是很详细和直观地打印了错误详情。