威立山

记录心路历程

0%

JVM内存结构

《深入理解Java虚拟机(第2版)》中的描述是下面这个样子的:
jvm
JVM的内存结构大概分为:

  • 堆(Heap):线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。
  • 方法区(Method Area):线程共享。存储类信息、常量、静态变量、即时编译器编译后的代码。
  • 方法栈(JVM Stack):线程私有。存储局部变量表、操作栈、动态链接、方法出口,对象指针。
  • 本地方法栈(Native Method Stack):线程私有。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。
  • 程序计数器(Program Counter Register):线程私有。有些文章也翻译成PC寄存器(PC Register),同一个东西。它可以看作是当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。

​ 堆的作用是存放对象实例和数组。从结构上来分,可以分为新生代和老年代。而新生代又可以分为Eden 空间、From Survivor 空间(s0)、To Survivor 空间(s1)。 所有新生成的对象首先都是放在新生代的。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象,而复制到老年代的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。

  • 控制参数
    -Xms设置堆的最小空间大小。-Xmx设置堆的最大空间大小。-XX:NewSize设置新生代最小空间大 小。-XX:MaxNewSize设置新生代最小空间大小。
  • 垃圾回收
    此区域是垃圾回收的主要操作区域。
  • 异常情况
    如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常

方法区

​ 方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。

​ 很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。在Java8中永生代彻底消失了。

  • 控制参数

    -XX:PermSize 设置最小空间 -XX:MaxPermSize 设置最大空间。

  • 垃圾回收

    对此区域会涉及但是很少进行垃圾回收。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意。

  • 异常情况

    根据Java 虚拟机规范的规定, 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError。

方法栈

​ 每个线程会有一个私有的栈。每个线程中方法的调用又会在本栈中创建一个栈帧。在方法栈中会存放编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

  • 控制参数

-Xss控制每个线程栈的大小。

  • 异常情况

在Java 虚拟机规范中,对这个区域规定了两种异常状况:

- StackOverflowError: 异常线程请求的栈深度大于虚拟机所允许的深度时抛出;

- OutOfMemoryError 异常: 虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出。

本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其

区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则

是为虚拟机使用到的Native 方法服务。

  • 控制参数

    在Sun JDK中本地方法栈和方法栈是同一个,因此也可以用-Xss控制每个线程的大小。

  • 异常情况

    与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。

程序计数器

它的作用可以看做是当前线程所执行的字节码的行号指示器。

  • 异常情况

此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。

常见内存溢出错误

有了对内存结构清晰的认识,就可以帮助我们理解不同的OutOfMemoryErrors,下面列举一些比较常见的内存溢出错误,通过查看冒号“:”后面的提示信息,基本上就能断定是JVM运行时数据的哪个区域出现了问题。

Exception in thread “main”: java.lang.OutOfMemoryError: Java heap space

原因:对象不能被分配到堆内存中。

Exception in thread “main”: java.lang.OutOfMemoryError: PermGen space

原因:类或者方法不能被加载到老年代。它可能出现在一个程序加载很多类的时候,比如引用了很多第三方的库。

Exception in thread “main”: java.lang.OutOfMemoryError: Requested array size exceeds VM limit

原因:创建的数组大于堆内存的空间。

参考

JVM内存结构和Java内存模型

函数响应式编程FRP

  • FRP是基于异步事件流进行编程的一种编程范式。针对离散事件序列进行有效的封装,利用函数式编程的思想,满足响应式编程的需要。
  • ReactiveCocoa是Github在制作Github客户端时开源的一个副产物,缩写为RAC。它是Objective-C语言下FRP思想的一个优秀实例,后续版本也支持了Swift语言。
  • Swift语言的推出为iOS界的函数式编程爱好者迎来了曙光。著名的FRP开源库Rx系列也新增了RxSwift,保持其接口与ReactiveX.net、RxJava、RxJS接口保持一致。

RxSwift

  • 核心概念就是一个观察者(Observer)订阅一个可观察序列(Observable)。观察者对 Observable 发射的数据或数据序列作出响应。
  • 举个例子——你就是观察者,你正在统计某地方的交通情况。每通过一辆车,你就记一下车牌号和车的型号等等。这个时候这一辆辆车就是序列,你对经过的车进行记录就是对序列的响应。
  • 我们的应用也是这样。再看 Button 的点击这个情景。我们把用户一次又一次的点击 Button 看做是序列,通过调用 subscribe 来订阅这个点击事件,每次点击都会发射一个数据,作为订阅者的 subscribe 收到这个这个数据进行某些响应。
  • 序列存在三种情况:发射数据(Next)、遇到问题(Error)、发射完成(Completed)。

有限序列和无限序列

  • 有这样一个序列 –1–2–3–4–5–6–| ,它发送了按顺序发送了 1 2 3 4 5 6 这些值后,就终止了,就是发射完成。这是一个有限序列。

  • 也有可能因为某些特殊的情况,遇到某些错误(比如数据解析错误),–1–2–3–4–X ,因为解析错误意外终止。

  • 也有无限序列,比如用户点击 Button ,这个就属于无限序列:—tap-tap——-tap—> 当然这里的无限不是真的无限,是不确定用户什么时候会点击,不确定是否会继续点击。所以这是一个无限序列,每当用户点击,序列就发射一个值。

常用方法

  • never:创建一个不被终结,也不发送任何事件

  • empty:创建一个空序列,只发送一个completed事件

  • just:创建一个只有一个元素的序列

  • of:创建一个包括若干元素的序列

  • map :就是用你指定的方法去变换每一个值,这里非常类似 Swift 中的 map
    map

  • flatMap:投射一个源序列的每个元素到一个新的序列,并平整的合并成一个target序列。对新序列发送新值会添加到目标序列(flatMap会和每个创建的新序列保持联系)

    flatMap

  • flatMapLatest:只接受最后的序列,不订阅之前的新序列
    flatMapLatest

#RxCocoa

  • RxCocoa是一个独立的库(虽然和RxSwift打包一起使用),可以在iOS,MacOS,AppleTV等平台使用。它对UIKit和Cocoa的控件进行了封装,提供更加丰富的功能。

  • 例如:UILabel,UITextField等

相关知识

  • 绑定(苹果不公开他们的绑定系统(Cocoa Bindings),mac上的绑定非常的先进,并且和苹果提供的类耦合)
  • 在RxCocoa提供更加简单的方法,仅仅依赖库中的几个类型。简单的理解绑定是单向的数据流,简化了在应用内的数据流
  • 最基本的方法bindTo(_:)绑定一个序列到另一个元素,接受者必须遵循ObserverType
  • Subjects 是极其重要,主要作用:和UI控件一起工作的时候(UILabel设置新值,取值)
  • 可以理解 bindTo(_:)是 subscribe()的一个定制版本
  • flatMapLatest:好处是在Rx中可观察序列是可以重复使用的实体,正确的建模,可以使一个漫长,难以阅读的一次性观察者,变成一个多用途的且易于理解的观察者。
  • 可以用不同的subcription处理单个参数,映射要显示的值

写在前面的话

实际应用:在公司的海外项目中,在App中嵌入YouTube网页,当进入一个视频详情页时,视频会自动播放。而需求是不要自动播放,让用户点击我们自定义的播放按钮把视频投到电视上播放。下面让我们一起学习下如何拦截网络请求。

URL Loading System

URL Loading System
如图所示,URL Loading System是iOS一系列网络请求类的集合,包括已经过期不用的NSConnection和现在流行的NSURLSession,还包括一些请求认证的类,一个sessionConfig的类,还有关于处理请求缓存的类等,当然还包括我们要说的这个NSURLProtocol类。

NSURLProtocol

NSURLProtocol可以让我们拦截程序中的一切网络请求,因为NSURLProtocol是一个虚基类,所以不能直接使用它,要想使用它就必须自定义一个类成为他的子类,然后实现他里面的必须实现的一些方法。主要进行如下拦截处理:

  • 自定义请求 和 响应
  • 过滤掉某些请求不让其发起、以及修改
  • 提供 自定义的全局缓存 逻辑
  • 重定向 网络请求
  • 提供 HTTP Mocking (方便前期测试)

如何拦截网络请求

注册自定义的URLProtocol子类

  • 在appDelegate中,注册自己拦截请求的URLProtocol子类;相对应的也有unregistClass方法,不让某个子类起作用
1
2
3
4
5
6
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

[NSURLProtocol registerClass:[WSURLProtocol class]];

return YES;
}

子类必须实现的方法

  • + (BOOL)canInitWithRequest:(NSURLRequest *)request
    每次有一个请求的时候都会调用这个方法,在这个方法里面判断这个请求是否需要被处理拦截,如果返回YES就代表这个request需要被处理,反之就是不需要被处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
//判断是否处理过,防止死循环
if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {
return NO;
}
//只拦截http和https请求
NSString *scheme = [[request URL] scheme];
if ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
[scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) {
return YES;
}

return NO;
}
  • + (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request 通常该方法你可以简单的直接返回request,但也可以在这里修改request,比如添加header,修改host等,并返回一个新的request,这是个抽象方法,子类必须实现。
1
2
3
4
5
6
+ (NSURLRequest *)canonicalRequestForRequest:	(NSURLRequest *)request {
NSMutableURLRequest *request = [request mutableCopy];
//把访问百度的request改为访问Google了
request.URL = [NSURL URLWithString:@"http://www.google.com"];
return request;
}
  • + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b 可用来使用缓存数据结束此次网络请求

  • - (void)startLoading 开始请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)startLoading {
NSMutableURLRequest *request = [self.request mutableCopy];
// 标记request已处理
[NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];

//使用NSURLSession继续把重定向的request发送出去
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];

NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];

NSURLSessionDataTask *task = [session dataTaskWithRequest:request];

[task resume];
}

  • -(void)stopLoading 停止请求的方法,也是要实现的。

    NSURLProtocolClient

    如果我们使用UIWebView发送一个request,拦截以后当我们使用NSURLSession发出了request,那么这个request的response是无法回到这个UIWebView的,因为可以理解成不是同一个地方发出的request,这个response只能有session来处理,那我们怎么才能让这个response回到刚开始的UIWebView呢?

    NSURLProtocolClient就可以看做是URL Loading System,我们把response告诉client,也就是URL Loading System,让他来继续处理这个response,因为一切都是基于URL Loading System发生的,所以把response交给他,他会自动处理这个response回到webView。

    每一个NSURLProtocol的子类都有一个client对象来处理请求得到的response。其实下面这些写法都是差不多固定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
[self.client URLProtocol:self didFailWithError:error];
} else {
[self.client URLProtocolDidFinishLoading:self];
}
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];

completionHandler(NSURLSessionResponseAllow);
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
completionHandler(proposedResponse);
}

总结

  • 防止死循环 [NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];
  • NSURLProtocol 可以拦截 NSURLConnection、NSURLSession、UIwebview 的请求,但是不能拦截WKwebview 和CFNetwork的请求
  • AFNetworking 3.0 以后版本拦截不到请求,session的创建方式不同,默认不起作用,解决方法

参考

田腾飞的博客
幻想乡

写在前面的话

之前一直觉得Apple TV 是个神奇的东西,迷一样的事物。国内基本见不到它的影子,而公司要做一个Apple TV app,作为一名”资深”iOS开发者,怎么能不兴奋呢😝
在使用Apple TV之后,感觉真的比国内安卓系统的智能电视要惊艳很多,遥控器都那么与众不同!操作方便,也赏心悦目;缺点是不能看各个电视频道

前期调研

  • 相关资料很少,国内基本没有啥资料,只能看英文资料啦!推荐一本书籍《tvOS Apprentice》入门
  • 使用Swift进行开发,国外资料基本都用Swift写,赶紧上船…

讲点有趣的

UIView的focus特效

  • 在电视屏幕上你看到App里展示的界面,大部分都是UIImageView呈现的,图片比文字更能吸引眼球。
    ImageView Focus
    看到没有,但你用手指在遥控器触摸区打转时,获得焦点的图片会被放大,随手指旋转并带有眩光的视觉!这样的效果,只需设置UIImageView的自带属性:adjustsImageWhenAncestorFocused即可,真棒!!!

  • UIView 有个属性canBecomeFocused可以设置是否能获得焦点状态,设置为true,然后重写didUpdateFocus()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {

if context.nextFocusedView == self { // 获得焦点
self.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
self.layer.shadowColor = UIColor.black.alpha(0.36).cgColor
self.layer.shadowOffset = CGSize(width: 0, height: 30)
//设置阴影,模糊效果的半径
self.layer.shadowRadius = 50
self.layer.shadowOpacity = 1.0
}else{ //失去焦点
self.transform = CGAffineTransform.identity
self.layer.shadowOpacity = 0.0
}
}

下面再看一个有意思的
表情

UIImageView的animationImages可以设置一组图片,然后让图片动起来~
可是当你把一组表情图片设置给animationImages,ImageView获得焦点时,表情并没有动啊! 解决方法:将UIImageView加到一个UIView上,当view获得焦点时,view调整transformscale,并让imageView开始startAnimating;失去焦点时,将view的transform还原,imageViewstopAnimating

如何控制焦点

Focus Engine(焦点引擎)

  • 我们可以将Focus视为聚光灯,将用户的注意力引导到屏幕上的特定元素。视图可以突出显示或聚焦,用户可以使用遥控器将焦点移动到应用的不同元素。
  • 当用户在遥控器的触摸面上滑动时,焦点引擎将朝着移动焦点的下一个视图的方向移动。如果它“看到”一个,它将Focus放在那里,如果没有看到它,它不会移动Focus。

例如:下面简单的3个按钮应用程序,当右上方的按钮处于焦点状态,用户向下滑动,想滑动到左下方按钮,因为焦点引擎看不到任何视图,所以Focus不会移动。
Focus示意图

这时候该 UIFocusGuide 登场了

UIFocusGuide

UIFocusGuide :是不可见的布局指导,可帮助Focus Engine知道将Focus移动到哪里。
我们把FocusGuide放到右下方,效果就像下图
FocusGuide

代码

首先,我们添加焦点指南的视图。

1
2
3
4
5
6
7
8
9
// Create the Focus Guide and add it to the view
var focusGuide = UIFocusGuide()
view.addLayoutGuide(focusGuide)

// Anchor the Focus Guide
focusGuide.widthAnchor.constraintEqualToAnchor(topRightButton.widthAnchor).active = true
focusGuide.heightAnchor.constraintEqualToAnchor(bottomLeftButton.heightAnchor).active = true
focusGuide.topAnchor.constraintEqualToAnchor(bottomLeftButton.topAnchor).active = true
focusGuide.leftAnchor.constraintEqualToAnchor(topRightButton.leftAnchor).active = true

然后,要使焦点指南直接聚焦到左下方的按钮,我们设置焦点指南的preferredFocusView属性; 这是“焦点指南”将引导焦点的view。

1
2
// Set preferred focus
focusGuide.preferredFocusedView = bottomLeftButton

调试

如果它们没有起作用,如何调试”焦点指南”呢?
苹果提供两种方式:

  1. Quick Look(快速查看)
  2. _whyIsThisViewNotFocusable

快速查看
Quick Look是一款用于调试Focus Engine和Focus Guide问题的可视化工具。
要使用它,你必须在方法中得到一个中断didUpdateFocusInContext(context, withAnimationCoordinator coordinator); 每次焦点被移动时都会被调用。然后,在Xcode左下角的Varibles视图中,突出显示context变量,然后单击底部的眼睛图标(或按空格键)。

whyIsThisViewNotFocusable
苹果还提供了(非常描述性地命名)LLDB命令,_whyIsThisViewNotFocusable称为另一个Focus调试工具。

要使用此命令,您需要在代码中的任何位置(或者甚至暂停它)中断中断,并_whyIsThisViewNotFocusable在LLDB命令行调试器中运行命令,如下所示:

1
[yourView _whyIsThisViewNotFocusable]

Swift命令是:

1
po yourView.performSelector(Selector("_whyIsThisViewNotFocusable"))

第三个方法Visual Focus Guides
Github地址

再吐槽一下

给图片加圆角

1
2
self.bgImageV.layer.masksToBounds = true
self.bgImageV.layer.cornerRadius = 10

TVOS上可没这么简单,这么做的后果是当图片在焦点状态时,图片放大的部分会被裁剪,这太坑了~😢

1
2
3
4
5
6
7
8
9
// Kingfisher库的生成圆角方法
///下载网络图片
let processor = RoundCornerImageProcessor(cornerRadius: 6)
ImageView.kf.setImage(with: model?.image, placeholder: nil, options: [.processor(processor)])

///本地图片
let image = UIImage.createImage(color: UIColor("#908F8F"), cornerRadius: 0, size: CGSize(width: 10, height: 10))
//fit 一定要是UIImageView的size大小
ImageView.image = image.kf.image(withRoundRadius: 6, fit: ImageView.size)

为啥在TVApp不能给UIImageView设置圆角呢?苹果怎么考虑的?可能是个bug!

参考

Debugging Focus in tvOS

iOS应用内购买IAP

最近苹果针对iOS应用软件开发者的指引政策进行了修改,,第一次明确指出用户支付小费或者打赏,必须通过苹果的支付渠道,也就是不能绕开苹果iOS管理机制。微信在iOS版本取消了打赏功能,知乎则是向苹果低了头…
苹果在相关文档中提到,如果开发者想在iOS软件提供下述功能(比如用户订阅、游戏内虚拟货币、获取高端会员内容、或者提供完整的功能版本等),则开发者必须使用软件内购买。

本文主要以自动订阅类型产品为主,其他类型产品为辅

准备

In-App Purchase, 简称IAP,允许在iOS app与macOS app中出售商品。

  • 新建一个app,然后添加App 内购买项目,主要分四种内购买项目类型。
  1. 消耗品(Consumable products):会越用越少的,比如游戏内金币等。
  2. 不可消耗品(Non-consumable products):比如游戏中跑车,简单来说就是一次购买,终身可用(用户可随时从App Store restore)
  3. 自动更新订阅品(Auto-renewable subscriptions):和不可消耗品的不同点是有失效时间。比如一整年的付费周刊。在这种模式下,开发者定期投递内容,用户在订阅期内随时可以访问这些内容。订阅快要过期时,系统将自动更新订阅。
  4. 非自动更新订阅品(Non-renewable subscriptions):一般使用场景是从用户从IAP购买后,购买信息存放在自己的开发者服务器上。失效日期/可用是由开发者服务器自行控制的,而非由App Store控制,这一点与自动更新订阅品有差异。
  • 添加APP内购买项目
    内购项目

  • 设置税务和银行卡信息

    银行卡

  • 添加沙盒测试账号
    沙盒测试账号

使用StoreKit API

  1. 获取产品列表,展现的产品可以后台配置(Consumable可消耗商品举例)

    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
    // MARK: - FETCH AVAILABLE IAP PRODUCTS
    func fetchAvailableProducts() {
    Toast(text: "fetching available products").show()
    // Put here your IAP Products ID's
    let productIdentifiers = NSSet(objects:
    COINS_PRODUCT_ID,PREMIUM_PRODUCT_ID
    )

    productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set<String>)
    productsRequest.delegate = self
    productsRequest.start()
    }

    // MARK: - REQUEST IAP PRODUCTS
    func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) {
    if (response.products.count > 0) {
    Toast(text: "fetch \(response.products.count) product success").show()

    iapProducts = response.products

    // 1st IAP Product (Consumable) ------------------------------------
    let firstProduct = response.products[0] as SKProduct

    // Get its price from iTunes Connect
    let numberFormatter = NumberFormatter()
    numberFormatter.formatterBehavior = .behavior10_4
    numberFormatter.numberStyle = .currency
    numberFormatter.locale = firstProduct.priceLocale
    let price1Str = numberFormatter.string(from: firstProduct.price)

    // Show its description
    consumableLabel.text = firstProduct.localizedDescription + "\nfor just \(price1Str!)"
    }
    }

  2. 购买产品,首先检查设备是否支持内购买,然后将交易加入支付队列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // MARK: - MAKE PURCHASE OF A PRODUCT
    func canMakePurchases() -> Bool { return SKPaymentQueue.canMakePayments() }
    func purchaseMyProduct(product: SKProduct) {
    if self.canMakePurchases() {
    Toast(text: "begin purchase for \(product.localizedTitle)").show()

    let payment = SKPayment(product: product)
    SKPaymentQueue.default().add(self)
    SKPaymentQueue.default().add(payment)

    print("PRODUCT TO PURCHASE: \(product.productIdentifier)")
    productID = product.productIdentifier


    // IAP Purchases dsabled on the Device
    } else {
    UIAlertView(title: "IAP Tutorial",
    message: "Purchases are disabled in your device!",
    delegate: nil, cancelButtonTitle: "OK").show()
    }
    }
  3. 处理支付结果

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
44
45
46
47
48
49
50
51
52
53
54
55
// MARK:- IAP PAYMENT QUEUE
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction:AnyObject in transactions {
if let trans = transaction as? SKPaymentTransaction {
let msg = self.descFromTransactionState(trans.transactionState);
if(msg != nil){
Toast(text: msg).show()
}

switch trans.transactionState {

case .purchased:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)

guard let receiptUrl = Bundle.main.appStoreReceiptURL else {
SKPaymentQueue.default()
return
}

let receipt = try? Data.init(contentsOf: receiptUrl)

//自动订阅类型需要验证receipt 发到 apple
guard let receiptBase64String = receipt?.base64EncodedString() else {
return
}
let dict = ["receipt-data": receiptBase64String]
let url:URL? = URL(string:"https://sandbox.itunes.apple.com/verifyReceipt")
var request = URLRequest.init(url: url!)
request.httpMethod = "POST"
let data = try? JSONSerialization.data(withJSONObject: dict, options: JSONSerialization.WritingOptions.prettyPrinted)
request.httpBody = data

URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
if(data != nil){
let JSONObj = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments)
let str = String(data: data!, encoding: .utf8)
print(str!)
print(JSONObj!)
}

}).resume()

break

case .failed:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
break
case .restored:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
break

default: break
}}}
}

使用SwiftyStoreKit库简化调用API

  • SwiftyStoreKit对StoreKit的api进行封装,调用起来更加方便。具体api请参考SwiftyStoreKit Github
  • 需要注意的购买商品时api 区分原子性和非原子性;原子性是购买成功后,购买的内容能立即传递给用户界面,而非原子性是指购买的内容需要从服务器下载,需要下载完毕,才调用完成交易的api来结束交易
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1", quantity: 1, atomically: false) { result in
switch result {
case .success(let product):
// fetch content from your server, then:
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
print("Purchase Success: \(product.productId)")
case .error(let error):
switch error.code {
case .unknown: print("Unknown error. Please contact support")
case .clientInvalid: print("Not allowed to make the payment")
case .paymentCancelled: break
case .paymentInvalid: print("The purchase identifier was invalid")
case .paymentNotAllowed: print("The device is not allowed to make the payment")
case .storeProductNotAvailable: print("The product is not available in the current storefront")
case .cloudServicePermissionDenied: print("Access to cloud service information is not allowed")
case .cloudServiceNetworkConnectionFailed: print("Could not connect to the network")
case .cloudServiceRevoked: print("User has revoked permission to use this cloud service")
}
}
}

自动订阅类型总结

  1. 注册在AppDelegate中didFinishLaunchingWithOptions中,注册交易观察者,这样app就可以收到支付队列的通知(自动续订时会被调用)
  2. 沙盒测试周期会被加速,订阅周期为1周,在沙盒环境下只需3分钟,1个月只需5分钟…苹果考虑的挺周到的
  3. 一个沙盒测试账号一天只能被更新订阅5次,测试自动订阅时尤其注意,可以使用多个测试账号(当时我以为没能自动订阅呢)
  4. 客户端先将receipt发给服务端,服务端去验证receipt 有时返回 210007 状态码,意味着这个receipt是测试环境的,但发送到了正式环境https://buy.itunes.apple.com/verifyReceip。而且审核时,苹果人员也是用沙盒环境测试,如何切换正式和测试环境呢?解决方案:服务端先走正式环境,如果返回21007就再走沙盒环境去验证。
  5. 关于用户退款,需要服务端定期去向App Store去校验receipt,在返回来的数据中检查是否有 cancellation_date 字段,如果有值,则购买不成立。

欢迎指正😝

参考

In-App Purchases in iOS With Swift 3
苹果文档 About In-App Purchase
苹果文档 验证Receipt

Swift中值类型和引用类型

本文主要介绍值类型、以及使用Struct作为数据模型的注意事项

值类型(value type)

值类型:每次分配给变量/常量或者作为参数传递到函数时,都会重新创建(复制)一个新的实例。

Swift 中的所有的内建类型都是值类型,不仅包括了传统意义像 Int,Double这些,甚至连 String,Array 以及 Dictionary 都是值类型的(与OC不同), swift中所有的Struct 和 enum 都是值类型,值类型每次初始化以后,将它分配或者传递时,实际上是分配或传递了它的一个拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Point {
var x : Int
var y : Int
init(x:Int, y:Int) {
self.x = x
self.y = y
}
}

var aPoint = Point(x: 10, y: 10)
var bPoint = aPoint
bPoint.x = 5
print(aPoint)
print(bPoint)

打印结果

Point(x: 10, y: 10)

Point(x: 5, y: 10)

总结

  1. 赋值给新变量,会产生一个单独的、没有数据共享的新实例.
  2. 值类型在复制时,会将存储在其中的值类型一并进行复制,而对于其中的引用类型的话,则只复制一份引用。
  3. 复制时机,是在值类型中的内容发生改变时!值类型每次复制会增加额外开销,Swift将这种开销控制在最小的范围内,在没有必要复制时,复制是不会发生的。(≧▽≦)/

线程安全:
在Swift中,Array、String、Dictionary都是值类型。它们的使用方式类似C语言中得int,每一个实例都有一份数据。你不需要进行显示的复制操作去防止数据在你不知情的情况下被修改。更重要的是,你可以跨线程进行传参而不需要考虑同步的问题,因为传递值类型很安全。(Swift官方博客

引用类型(Reference Types)

引用类型:引用类型初始化后,无论是分配给变量还是常量,或是通过参数传递给函数,都将是同一个实例对象。
NSObject再熟悉不过了,当在分配或传递是都是引用,都指向那个原始对象。不再多说了~

Struct数据模型

处理数据结构时有时会用Struct进行处理,使用 Struct 来代替 Class 作为数据模型有很多好处。值类型是非常有优势的:

  • 安全性 :因为 Struct 是用值类型传递的,它们没有引用计数。
  • 内存:由于他们没有引用数,他们不会因为循环引用导致内存泄漏。
  • 速度:值类型通常来说是以栈的形式分配的,而不是用堆。因此他们比 Class 要快很多,真的很多!在 StackOverflow 给 Struct 和 Class 进行相同操作的表现做了基准测试,Struct 比 Class 要快九百倍。
  • 拷贝:在 Objective-C 里拷贝一个对象,你必须选用正确的拷贝类型(深拷贝、浅拷贝),这是非常烦人的,值类型的拷贝则非常轻松!
  • 线程安全:值类型是自动线程安全的。无论你从哪个线程去访问你的 Struct ,都非常简单。

Struct的缺点:

  • OC无法调用Swift的Struct,当你的项目的代码是 Swift 和 Objective-C 混合开发时,你会发现在 Objective-C 的代码里无法调用 Swift 的 Struct。因为要在 Objective-C 里调用 Swift 代码的话,对象需要继承于 NSObject。
  • 不能继承,继承是面向对象编程的四大特性之一,没有继承就不能抽象一个统一模型
  • Struct 不能被序列化成 NSData 对象。某些时候会把数据存在 NSUserDefaults 里,而Struct 转化成NSData,技术可行但坑多

参考:

王巍Swift Tips

SwiftGG

  • 如果想要成功,那就要努力;光努力还不够,还要坚持,如果坚持不了,那就不要抱怨…

  • 自己有多想成功?当你想成功的欲望强烈的像想要呼吸一样,成功才更有可能…

  • “人生没有彩排,每一天都是现场直播”。偶尔会想,如果人生真如一场电子游戏,玩坏了可以选择重来,生活会变成什么样子?正因为时光流逝一去不复返,每一天都不可追回,所以更要珍惜每一寸光阴,孝敬父母、疼爱孩子、体贴爱人、善待朋友!

  • 我想要纯粹的生活
    想要奋斗的喜悦
    想要健康的体魄
    想要一个有趣的灵魂
    二十六的我,仍然渴望出发…

UIWebView与JavaScript交互

基本的概念

  • JavaScriptCore是封装了JavaScript和Objective-C桥接的Objective-C API,只要用很少的代码,就可以做到JavaScript调用Objective-C,或者Objective-C调用JavaScript。
  • JSValue: 代表一个JavaScript实体,一个JSValue可以表示很多JavaScript原始类型例如boolean, integers, doubles,甚至包括对象和函数。
  • JSContext: 代表JavaScript的运行环境,你需要用JSContext来执行JavaScript代码。所有的JSValue都是捆绑在一个JSContext上的.
  • JSExport: 这是一个协议,可以用这个协议来将原生对象导出给JavaScript,这样原生对象的属性或方法就成为了JavaScript的属性或方法,非常神奇。

OC调用JS的方法

采用stringByEvaluatingJavaScriptFromString写入JS代码,调用JS的方法
采用系统框架<JavaScriptCore/JavaScriptCore.h>的JSContext +evaluateScript调用JS代码

1
2
3
4
5
6
// 1.UIWebView执行JS代码
[self.webView stringByEvaluatingJavaScriptFromString:@"showName('Willi')"];
// 2.JavaScriptCore执行JS
JSContext *context = [[JSContext alloc] init];
NSString *jsCode = [NSString stringWithFormat:@"alert(\"我是OC里面的js方法\")"];
[context evaluateScript:jsCode];

JS调用OC的方法

  1. 采用传统的方法,加载WebView的时候截取URL的方式
  2. 利用苹果系统框架<JavaScriptCore/JavaScriptCore.h>的JSContext的block方式 或者 JSExport协议方式
1
2
3
4
5
6
7
8
9
//注册printAandB方法
context[@"printAandB"] = ^(NSString *A ,NSString *B) {
NSLog(@"%@,%@",A,B);
};

//自定义协议,并且协议遵守<JSExport>协议
@protocol WuKongJSExport <JSExport>
JSExportAs(Invoke, - (void)invokeKey:(NSString *)key value:(NSString *)value);
@end

WKWebView与JavaScript的交互

WKWebView

WKWebView是苹果在iOS 8中引入的新组件,目的是给出一个新的高性能的WebView解决方案,摆脱过去 UIWebView的老、旧、笨重,特别是内存占用量巨大的问题。

OC调用JS的方法

  • 原生调用JavaScript的代码需要在页面加载完成之后,就是在 - webView:didFinishNavigation:代理方法里面
1
2
3
[webView evaluateJavaScript:@"showAlert('奏是一个弹框')" completionHandler:^(id item, NSError * _Nullable error) {
// Block中处理是否通过了或者执行JS错误的代码
}];

JS调用OC的方法

  • JavaScript的配置
    JavaScript调用Native的方法就需要前端和Native的小伙伴们配合了,需要前端的小伙伴在JS的方法中调用:
    window.webkit.messageHandlers.NativeMethod.postMessage("就是一个消息啊");

  • Native App的代码配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 // 创建配置
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 创建UserContentController(提供JavaScript向webView发送消息的方法)
WKUserContentController* userContent = [[WKUserContentController alloc] init];
// 添加消息处理,注意:self指代的对象需要遵守WKScriptMessageHandler协议,结束时需要移除
[userContent addScriptMessageHandler:self name:@"NativeMethod"];
// 将UserConttentController设置到配置文件
config.userContentController = userContent;
// 高端的自定义配置创建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:config];
// 设置访问的URL
NSURL *url = [NSURL URLWithString:@"http://www.jianshu.com"];
// 根据URL创建请求
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// WKWebView加载请求
[webView loadRequest:request];
// 将WKWebView添加到视图
[self.view addSubview:webView];

可以看到,添加消息处理的handler的name,就是JavaScript中调用时候的NativeMethod,这两个要保持一致。请把URL换成你自己的。
配置当前ViewController为MessageHandler,需要服从WKScriptMessageHandler协议,如果出现警告⚠️,请检查是否服从了这个协议。
注意!注意!注意:上面将当前ViewController设置为MessageHandler之后需要在当前ViewController销毁前将其移除,否则会造成内存泄漏。

1
[webView.configuration.userContentController removeScriptMessageHandlerForName:@"NativeMethod"];

现在万事俱备,只欠东风了。可以看到WKScriptMessageHandler的协议里面只有一个方法,就是:- userContentController:didReceiveScriptMessage:

相信聪明的你已经猜到了。是的,就是在这个代理方法里面操作:如果JavaScript执行已经写好的:window.webkit.messageHandlers.NativeMethod.postMessage(“就是一个消息啊”);这行代码,这个代理方法就会走,并且会有个WKScriptMessage的对象,这个WKScriptMessage对象有个name属性,拿到之后你会发现,就是我们注册的NativeMethod这个字符串,这时候你就可以手动调用Native的方法了。如果有多个方法需要调用的话怎么办,看到JavaScript中postMessage()方法有一个参数了没有,可以根据这里的参数来区分调用原生App的哪个方法。

1
2
3
4
5
6
7
8
9
10
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
// 判断是否是调用原生的
if ([@"NativeMethod" isEqualToString:message.name]) {
// 判断message的内容,然后做相应的操作
if ([@"close" isEqualToString:message.body]) {

}
}
}

网页支持滑动返回

WKWebView比UIWebView有很多优势,在WKWebView中通过设置一个属性 allowsBackForwardNavigationGestures 就可以实现滑动返回,而UIWebView需要写很多代码来达到滑动返回效果。

项目最低支持iOS7,所以只能使用UIKit中的UIWebView,而新的WKWebView是支持iOS8+

  • WKWebView的内存开销要比UIWebView小很多
  • 拥有高达60FPS滚动刷新率及内置手势
  • 支持了更多的HTML5特性
  • html页面和WKWebView交互更方便
  • Safari相同的JavaScript引擎
  • 提供常用的属性,如加载网页进度的属性estimatedProgress

实现思路:在UIWebView的Delegate的shouldStart代理方法中,把当前页面截一张图,把这张截图和对应的Url保存到一个数组中,在WebView上加一个UIPanGestureRecognizer拖拽手势,在手势代理方法中监听状态的变化,当手指滑动时,会把当前的截图和上一页截图,根据手势的移动的位置,做随手指移动的动画。然后重新请求上一个网页
测试过程中发现,有时滑动好使有时不好使,最后了解到这个Pan手势和UIScrollview的Pan手势会冲突

  1. [webpageView.scrollView.panGestureRecognizer requireGestureRecognizerToFail:_swipePanGesture]; 设置先识别滑动返回,在识别滚动的手势,偶尔会造成某些网站网页,不能滚动。

  2. 设置手势代理方法 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer 返回YES,允许手势能同时识别。完美解决手势冲突


欢迎指正😝

资料

WKWebView使用
WKWeb​View(AFN作者)
自己动手打造基于 WKWebView 的混合开发框架收藏一下

介绍

网络抓包是个基础技能,对于网络协议的掌握有一定的要求。iOS上实现网络抓包可以用Charles(针对http和https),tcpdump(快速分析网络包),和Wireshare。和tcpdump相比,Wireshark提供丰富的GUI交互,而且能分析所有的网络协议。

  1. 首先安装软件 官网下载,目前最新版本v2.2.7

打开主界面
Wireshark主界面
Wireshark在第一个界面就把当前系统所包含的网卡列出来了,直接点击任何一项就可以开始监听通过该网卡的所有网络流量。

RVI接口创建和管理

当我们把iPhone通过usb连接macbook时,Wireshark并不能直接监听通过iPhone的网络流量,需要通过一个系统程序在我们的Mac系统上,建立一个映射到iPhone的虚拟网卡

  1. 通过数据线连接好iOS设备,拿到设备的UDID(Xcode或iTunes都行)
  2. terminal输入命令,格式是rvictl -s 设备UDID

执行命令之后Wireshark能立即识别新增加的rvi0网卡,也就是上图中高亮的部分,双击rvi0这一项,Wireshare即进入如下界面开始监听iPhone设备上的所有流量。

  • Android手机,可以下载个PacketCaptrue软件,用于抓包

如果出现下图错误,请用在终端用 sudo wireshark 命令启动
error

网络包结构

经典的TCP/IP五层结构:

  • 应用层
  • 传输层
  • 网络层
  • 数据链路层
  • 物理层

应用层是最内层的payload,除了应用层这一层之外,其他层都是用自己这一层的协议header+所包含那一层的payload。可以用如下公式表示:
TCP Layer = TCP Header + Application Payload
IP Layer = IP Header + TCP Payload

使用Filter过滤包

使用Wireshark和使用Charles最大的区别在于,Charles只捕获HTTP流量,而Wireshark捕捉的是经过目标网卡所有的流量,流量包可以在几秒内膨胀到难以阅读的数量,所以此时我们需要使用Filter来做包的过滤,Filter规则定的越细,剔除掉的干扰信息就越多,分析起来就越快。

Wireshark的Filter分为两种,一种为Capture Filter,另一种是Display Filter。

  • Capture Filter出现在初始界面,在网卡列表的上方有个输入框,允许我们输入capture filter,一旦输入了特定的capture规则,Wireshark就只捕获符合该规则的流量包了。

  • Display Filter出现在流量监控界面,在工具栏的下方有个输入框,允许我们输入display filter,display filter只是从界面上过滤掉不符合规则的包,Wireshark实际上还是监听了这些包,一旦去掉display filter,所有的包又会出现在同一界面。
    Display Filter

语法规则可以查看Wireshark官方文档

流量跟踪

Wireshark默认情况下将不同网络连接的流量都混在一起展示,即使给不同协议的包上色之后,要单独查看某个特定连接的流量依然不怎么方便,我们可以通过Wireshark提供的两种方式来实现这个目标。

Follow Stream

当我们选中某个包之后,右键弹出的菜单里,有个选项允许我们将当前包所属于的完整流量单独列出来,如
TCP追踪流

Flow Graph

Flow Graph可以通过菜单Statistics->Flow Graph来生成,这样我们可以得到另一种形式的流量呈现

和Follow Stream不同的是我们获取到的是完整的流量,iPhone手机IP地址发出的流向多个服务器的网络流量,包括DNS解析和SSL安全握手等。当然我们也可以在上图中下方的操作区域做进一步的过滤,可以使用Display Filter做进一步的流量定位。