利他才能利己


  • 首页

  • 标签

  • 归档

  • 搜索

iOS APNs: 本地推送

发表于 2017-08-06 | 分类于 iOS |

概括

iOS APNs: 远程推送 说过远程推送(RemotePush).

今天说说本地推送, 本地推送也就是平时所说的 LocalPush.

该系列博客:

  • iOS APNs: 远程推送
  • iOS APNs: 本地推送(本篇)
  • iOS APNs: 处理数据
  • iOS APNs: 静默推送
  • iOS APNs: Background Fetch

业务场景

在具体实现之前, 先说一下业务场景.

假如你的 APP 具有 IM(即时通讯) 功能, 这个时候, 你可以根据连接来判断用户是否在线, 如果不在线, 可以使用远程推送将推送内容告知用户.如果在线, 可以通过 IM 把内容告诉 APP, APP 收到这个消息后, 可以使用本地推送告知给用户.

据说 APNs 每天要处理的推送在亿级别, 所以如果可以使用 LocalPush 完成的业务, 我建议大家还是不要使用远程推送, 况且苹果并不一定保证远程推送一定成功, 如果网络或者 APNs 压力大, 推送也会延时.

给苹果减少点压力吧, 哈哈!

再说一个业务场景, 你的 APP 在后台运行时间快到要被系统挂起的时候了, 你可以发送一个 LocalPush 来提示用户或者刺激用户, 再次将 APP 拉回前台运行. 这样可以保证 APP 可以正常运行了.

上面说的第二个业务场景, 是今天例子的基础, 你也可以根据自己的实际业务场景来使用 LocalPush 功能.

LocalPush 简介

LocalPush 允许 APP 向用户发送通知, 对于用户来说, 就跟远程推送是一样的, 基本没有感知, 开发者也不希望用户有感知.

推送效果图:

1

注意: 如果你的 APP 在前台, 发送 LocalPush 是不会要上述效果的.
如果在前台, 可以使用自定义的弹框来提示用户.

LocalPush 同样需要用户授权推送的权限, 否则也无法发送成功.这个跟远程推送是一样的.

另外, LocalPush 需要 APP 在后台没有被挂起的情况下, 才能发送, 否则无法启用.

发送 LocalPush 的一个好处是不需要用户连接网络, 这个是区别于远程推送的, 因为远程推送必须要求用户连接网络的.很多单机游戏或者弱联网的游戏, 发送的推送都是 LocalPush, 而非远程推送.

在阅读下面内容之前, 建议大家先看看 iOS 后台模式 这篇文章.本篇也是基于这个为基础的.

实现

这个示例, 实现的场景如下:

用户打开 APP, 授权了推送的权限, 用户试玩一会之后, 将 APP 退到了后台, APP 监听退到后台之后, 延时一定时间发送 LocalPush 给用户.

AppDelegate.m 中使用后台模式

关于 MZBackgroundTask 的实现, 附录给出.

1
2
3
4
- (void)applicationDidEnterBackground:(UIApplication *)application {

[[MZBackgroundTask sharedTask] startTask];
}

ViewController.m

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
- (void)viewDidLoad {

[super viewDidLoad];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}
- (void)onDidEnterBackground:(NSNotification *)notification {

MZLOG(@"App Background. Enter onDidEnterBackground.");

// 等待 6s 后, 这个时间可以根据具体情况去修改, 这里只是模拟
int delta = 6;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delta * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

MZLOG(@"App Background. Enter onDidEnterBackground diapatch.");

if (UIApplicationStateBackground == [UIApplication sharedApplication].applicationState) {
UILocalNotification *notification = [UILocalNotification new];
notification.alertBody = @"走, 去high吧!";
notification.soundName = UILocalNotificationDefaultSoundName;
// 应用图标上面显示的数字
notification.applicationIconBadgeNumber = notification.applicationIconBadgeNumber + 1;
// 可以自定义数据
notification.userInfo = @{@"user_info_key": @"user_info_value_json_str"};

[[UIApplication sharedApplication] scheduleLocalNotification:notification];
}
else {
// 显示自定义弹框等
}
});
}

将 APP 点击 Home 键退到后台, 6s 后可以看到推送的效果.

点击推送的提示框, 再次打开了 APP, 会执行 AppDelegate 中的方法, 如下:

1
2
3
4
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {

MZLOG(@"App push. notification: %@", notification);
}

打印出来的 notification, 如下图所示:

1

其中, user_info 是我们自定义的数据部分.

附录

MZBackgroundTask 实现

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#import "MZBackgroundTask.h"

@implementation MZBackgroundTask

+ (instancetype)sharedTask {

static MZBackgroundTask *_task;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_task = [[self alloc] init];
});

return _task;
}

- (void)startTask {

if (![self _checkSupportBackgroundTask]) {
MZLOG(@"BackgroundTask. Current device don't support backgroundTask.");
return;
}

UIApplication *application = [UIApplication sharedApplication];

__block UIBackgroundTaskIdentifier taskId;

/// 申请后台执行
/// 注意: 在iOS7和该版本前,后台可以用下面的的方式在后台存活5-10分钟,在iOS8及后,最多存活3分钟
{
taskId = [application beginBackgroundTaskWithName:NSStringFromClass([self class]) expirationHandler:^{

MZLOG(@"BackgroundTask. BackgroundTask is Over. The remained time: %f", application.backgroundTimeRemaining);

[application endBackgroundTask:taskId];

taskId = UIBackgroundTaskInvalid;
}];
}

if (UIBackgroundTaskInvalid == taskId) {

MZLOG(@"BackgroundTask. Apply backgroundTask failed.");

return;
}

/// 可以监控后台任务剩余的时间, 针对业务可以去处理
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

while (true) {
// 剩余可以后台执行的时间
NSTimeInterval remainedTime = application.backgroundTimeRemaining;
MZLOG(@"BackgroundTask. The remained time: %f", remainedTime);

if (remainedTime < 2) {
// 可以告诉其他业务, 后台任务即将结束了
break;
}

// 睡眠(延时)1s
[NSThread sleepForTimeInterval:1.f];
}

/// 这里可以做一些清除工作
{
// clean up
}

[application endBackgroundTask:taskId];

taskId = UIBackgroundTaskInvalid;
});
}
}

#pragma mark - Private.

/**
* 当前设备是否支持后台任务.
*
* @return YES, 支持后台任务. 否则, 不支持后台任务.
*/
- (BOOL)_checkSupportBackgroundTask {

SEL sel = @selector(isMultitaskingSupported);
BOOL supportBTask = [[UIDevice currentDevice] respondsToSelector:sel];

return supportBTask;
}

iOS APNs: 远程推送

发表于 2017-08-06 | 分类于 iOS |

概要

本篇博文, 你首先需要知道的内容:

1.了解过 Push.
2.阅读过 [iOS 后台模式] 这篇文章.
3.申请过苹果证书或者知道如何申请和制作证书.
4.iOS 开发基本知识.

你在这篇博文将会学到:

1.Push 的发展历程.
2.开源推送工具 NWPusher 的使用.
3.Push 的基本原理.

简介

APNs, 苹果推送通知服务. 全称是: Apple Push Notification Service.

推送指的是由 APNs 服务器、ProviderService、iOS 系统、App 构成的通讯系统,也是移动互联网与传统的 Web 最明显不同的.

官方有比较详细的文档介绍, 可以戳这里 官方文档 查看.

苹果的文档写的确实好, 但是作为程序员, 最重要的还是要结合理论去实践.

本篇结合自己的实践和对推送的理解, 跟大家分享一下推送相关的知识.

Push 发展历程

iOS 历经很多版本, 一直在优化或者说是进化推送相关的内容, 无论是从实用性和技术上来看, 推送是至关重要的.

看一下 push 发展历程

  • iOS 3 - 引入推送通知 UIApplication 的 registerForRemoteNotificationTypes 与 UIApplicationDelegate 的 application(:didRegisterForRemoteNotificationsWithDeviceToken:),application(:didReceiveRemoteNotification:)

  • iOS 4 - 引入本地通知 scheduleLocalNotification,presentLocalNotificationNow:, application(_:didReceive:)

  • iOS 5 - 加入通知中心页面

  • iOS 6 - 通知中心页面与 iCloud 同步

  • iOS 7 - 后台静默推送 application(_:didReceiveRemoteNotification:fetchCompletionHandle:)

  • iOS 8 - 重新设计 notification 权限请求,Actionable 通知 registerUserNotificationSettings(:),UIUserNotificationAction 与 UIUserNotificationCategory,application(:handleActionWithIdentifier:forRemoteNotification:completionHandler:) 等

  • iOS 9 - Text Input action,基于 HTTP/2 的推送请求 UIUserNotificationActionBehavior,全新的 Provider API 等

  • iOS 10- 支持Images, GIFs, Audio and Video类型, 并且有 Notification Service Extension 与 Notification Content Extension,可以实现推送数据在展示前进行下载更新、定制通知 UI, 并且统一了通知类型,具有时间间隔通知、地理位置通知和日历通知.

该系列博客共分为几个部分:

  • iOS APNs: 远程推送(本篇)
  • iOS APNs: 本地推送
  • iOS APNs: 处理数据
  • iOS APNs: 静默推送
  • iOS APNs: Background Fetch

如果需要完整源码的, 可以通过邮件联系我(veritman@126.com), 后续完成后会上传到 github.

原理

先看官方的流程图, 如下所示:

1

该流程图, 主要说明的是自己 业务服务器(Provider) 推送消息到用户(Client APP) 的流程.

注意: 这里说的以及本文后面说的 业务服务器, 统一指的是可以向 APNs 发送推送消息的服务器.

下面这张图是比较完整的一张流程图, 自己画的, 凑合看吧!

1

大概流程, 我详细说一下.

1.iOS 设备启动后连接网络, 会与苹果服务器建立一个安全的长连接.
这个是系统维护的, 这也是推送的关键.
2.用户打开 app, 授权了推送通知的权限.
3.授权成功后, APNs 会将 deviceToken 返回给 iOS 终端.
4.终端将该 deviceToken 返回给指定的 APP.
5.APP 拿到 deviceToken 上传给我们自己的业务服务器.
6.业务服务器向 APNs 发送推送请求, 带上 deviceToken.
7.APNs 推送内容到指定的 iOS 终端.
8.iOS 终端将内容推送给用户.

关于 deviceToken 后面会讲.

关于 deviceToken

这里简单介绍一下 deviceToken.

deviceToken 是 NSData 类型的数据, 是苹果服务器根据 设备,证书等信息和一定算法生成的.
需要将这个 deviceToken 传送给我们的服务器端, 这样一个用户对象就绑定了一个 deviceToken.
当需要给用户推送消息, 通过自己的业务服务器, 找用户对应的 deviceToken 和要发送的推送内容, 发送到苹果的 APNs, 然后 APNs 将消息推送到该 deviceToken 对应的手机上.

关于 deviceToken 是否可变的问题

网上有些人说, deviceToken 是可变的, 有些人说, deviceToken 是不可变的, 我也不知道他们到底有没有实践过, 今天我要告诉大家的是, deviceToken 是可变的.

如卸载重装 app, deviceToken 会变. 我的设备 iphone6, iOS10.3.

下面是我实验的数据:

1
2
3
4
第一次安装运行得到的 deviceToken

<4e0f2928 5be0700c="" 296bf7f1="" 3b0837e4="" bc9da6d1="" 9fdb672e="" f87446be="" 1c098431="">
卸载后, 第一次安装运行得到的 deviceToken

这说明, deviceToken 是可变的.

代码实现

上面基本都是一些理论知识, 下面结合代码, 具体实现.

工程配置

在写代码之前, 需要配置一下工程.关于如何创建带有 push 功能的苹果证书的操作, 大家自行去网上学习, 这里不赘述.

不过这里要提醒一点, 创建的证书一定要和自己项目的 appid 保持一致, 否则无法推送.

这个 Demo 工程的名字是 MZPush.

安装好证书, 打开工程, 并能让工程识别到.

切换到 Target, 点击 Capabilities 选项.

步骤1: 配置 Push, 打开开关即可.

1

配置后台模式, 打开开关, 选中Remote notifications.

1

不过这一步, 不是必须的, 如果你不配置, 工程会有警告.我建议是选上, 在后面的博文中, 再仔细说说这个东西的好处.

警告信息如下:

1
2
You've implemented -[<UIApplicationDelegate> application:didReceiveRemoteNotification:fetchCompletionHandler:], 
but you still need to add "remote-notification" to the list of your supported UIBackgroundModes in your Info.plist.

步骤2: 配置完这一步后,在 Info.plist 中可以看到多了一项内容:

1

步骤3: 配置工程最小兼容版本

1

因为我要兼容 iOS7, 所以在 Xcode8 中, 自己手动改为了 7.0.

步骤4: 关闭 Bitcode(可选操作)

1

步骤5: 配置支持 HTTP(可选操作)

在 Info.plist 中, 添加如下:

1

用户授权

询问用户授权的实现

该实现兼容了 iOS7.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (void)applyPushPermission {

UIApplication *application = [UIApplication sharedApplication];

if (MZSysVersion <= 7.0) {
UIRemoteNotificationType nType = UIRemoteNotificationTypeAlert|UIRemoteNotificationTypeBadge|UIRemoteNotificationTypeSound;
[application registerForRemoteNotificationTypes:nType];
}
else {
UIUserNotificationType nType = UIUserNotificationTypeBadge|UIUserNotificationTypeSound|UIUserNotificationTypeAlert;
UIUserNotificationSettings *nSettings = [UIUserNotificationSettings settingsForTypes:nType categories:nil];
[application registerUserNotificationSettings:nSettings];
}
}

关于用户授权, 分几种情况来看待.

情景一. 用户不允许 APP 推送, 即不授权.

Appdelegate 代理调用

1
2
3
4
5
6
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {

MZLOG(@"App push. notificationSettings: %@", notificationSettings);

[application registerForRemoteNotifications];
}
1
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error

error 信息大概如下:

1
2
Error Domain=NSCocoaErrorDomain Code=3000 "未找到应用程序的“aps-environment”的授权字符串"
UserInfo={NSLocalizedDescription=未找到应用程序的“aps-environment”的授权字符串}

情景二. 用户允许授权了, 又分为两种情况

1.使用具有 push 功能的证书

一定要有带有 push 功能的证书.

代理调用

1
2
3
4
5
6
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {

MZLOG(@"App push. notificationSettings: %@", notificationSettings);

[application registerForRemoteNotifications];
}

这个方法大概在上面回调 1s 后才会调用.

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {

MZLOG(@"App push. deviceToken: %@", deviceToken);

// 格式化该数据
NSString *deviceTokenStr = [NSString stringWithFormat:@"%@", deviceToken];
deviceTokenStr = [deviceTokenStr stringByReplacingOccurrencesOfString:@"<" withString:@""];
deviceTokenStr = [deviceTokenStr stringByReplacingOccurrencesOfString:@">" withString:@""];
deviceTokenStr = [deviceTokenStr stringByReplacingOccurrencesOfString:@" " withString:@""];
MZLOG(@"App push. deviceToken string: %@", deviceTokenStr);

// 可以上传该 token 到自己的业务服务器
}

在 didRegisterForRemoteNotificationsWithDeviceToken 方法中可以得到 deviceToken 信息:

1

注意: 代码中将 NSData 的 deviceToken 转换为了 NSString 类型的数据类型.

2.使用一般的证书, 没有 push 功能的证书

这种情况和 情景一 一样.

推送

万事俱备, 只欠东风了.

今天没有准备搭建一个自己的业务服务器去推送, 可以使用推送工具来替代.

安装 NWPusher 这个工具, 可以进行推送测试.

安装成功后, 打开这个 Mac APP, 填写相关信息.

点击 push 即可推送.

1

在 Appdelegate 中的代理回调中, 可以打印推送内容.

1
2
3
4
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {

MZLOG(@"App push. userInfo: %@", userInfo);
}

推送的内容, 如下图所示.

1

1
2
3
4
5
6
7
{
aps = {
alert = "Testing.. (6)";
badge = 1;
sound = default;
};
}

推荐

1.活久见的重构 - iOS 10 UserNotifications 框架解析

2.国内 90%以上的 iOS 开发者,对 APNs 的认识都是错的

iOS 后台模式

发表于 2017-07-30 | 分类于 iOS |

应用状态

来自苹果开发者文档 The App Life Cycle 的图.

1

下面这张图说明了应用程序的状态, 如下所示:

1

具体的说一下这5中状态:

1.Not running

未运行, 程序没启动或者被系统被用户杀死

2.Inactive

未激活, 程序在前台运行,不过没有接收到事件.
在没有事件处理情况下程序通常停留在这个状态.

3.Active

激活, 程序在前台运行而且接收到了事件.
这也是前台的一个正常的模式

4.Backgroud

后台, 程序在后台而且能执行代码,大多数程序进入这个状态后会在在这个状态上停留一会.
时间到之后会进入挂起状态(Suspended). 有的程序经过特殊的请求后可以长期处于 Backgroud 状态.

5.Suspended

挂起, 程序在后台不能执行代码.
系统会自动把程序变成这个状态而且不会发出通知.
当挂起时, 程序还是停留在内存中的, 当系统内存低时, 系统就把挂起的程序清除掉, 为前台程序提供更多的内存.

关于 Backgroud 状态, 是我们今天要说的重点部分.

多任务介绍

iOS 的多任务是在 iOS4 的时候被引入的,在此之前 iOS 的 APP 都是按下 Home 键就被干掉了.
iOS4 虽然引入了后台和多任务,但是实际上是伪多任务,一般的 APP 后台并不能执行自己的代码,只有少数几类服务在通过注册后可以真正在后台运行,并且在提交到 AppStore 的时候也会被严格审核是否有越权行为,这种限制主要是出于对于设备的续航和安全两方面进行的考虑.之后经过iOS5 和 iOS6 的逐渐发展,后台能运行的服务的种类虽然出现了增加,但是 iOS 后台的本质并没有变化.
在iOS7之前,系统所接受的应用多任务可以大致分为几种:

  • 后台完成某些花费时间的特定任务.
  • 后台播放音乐等.
  • 位置服务.
  • IP电话(VOIP).
  • Newsstand.

iOS7 后台任务申请的最长时间 10分钟.
iOS8+ 后台任务申请最长时间 3分钟.

示例: APP 退到后台会被挂起

今天跟大家分享的是一般应用如何在后台延长生命周期的知识, 关于其他特殊的 App 如上面提到的5中情况, 不是今天讨论的重点.

我们先看一个例子, 例子很简单.

这里要说明一下概念, 直接锁屏和点击 Home 键, 都会导致应用处于后台模式, 这里为了说明问题, 统一点
击 Home 作为代名词.

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
- (void)viewDidLoad {

[super viewDidLoad];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}

- (void)onDidEnterBackground:(NSNotification *)notification {

MZLOG(@"App Background. Enter onDidEnterBackground.");

int delta = 1;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delta * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

MZLOG(@"App Background. Enter onDidEnterBackground diapatch.");

UILocalNotification *notification = [UILocalNotification new];
notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:3];
notification.alertBody = @"走, 去high吧!";
notification.soundName = UILocalNotificationDefaultSoundName;
// 可以自定义数据
notification.userInfo = @{@"user_info_key": @"user_info_value_json_str"};

[[UIApplication sharedApplication] scheduleLocalNotification:notification];
});
}

UIApplicationDidEnterBackgroundNotification 可以监听到用户将 APP 退到后台.

当 APP 退到后台, 会调用 onDidEnterBackground 这个方法.

在 onDidEnterBackground 这个方法中, 我故意延时执行代码, 这里使用的是 dispatch_after.

编译运行这个工程, 运行成功后, 可以点击 Home 键将应用退到后台.

可以在 Xcode 的控制台看到 App Background. Enter onDidEnterBackground. 的打印信息, 但是迟迟不见 dispatch_after 里面的代码执行.

这里说明, APP 退到后台后被系统挂起了.

另外一个例子就是使用 NSTimer, 在 APP 退到后台后, 也会被终止.

完整例子, 一会在文章后台附录给出.

通过后台模式延长 APP 运行

上面的例子充分说明了, 在我们没有做任何处理的情况下, iOS 系统在 APP 退到后台的情况下, 会被系统挂起, 从而终止 APP 的代码行为.

下面通过实例, 来开启后台任务, 让 APP 尽可能的延长声明周期.

在工程的基础上, 新建一个文件 MZBackgroundTask

1
2
3
4
@interface MZBackgroundTask : NSObject
+ (instancetype)sharedTask;
- (void)startTask;
@end

具体实现, 只给出关键代码.

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
- (void)startTask {

if (![self _checkSupportBackgroundTask]) {

MZLOG(@"BackgroundTask. Current device don't support backgroundTask.");
return;
}

UIApplication *application = [UIApplication sharedApplication];

__block UIBackgroundTaskIdentifier taskId;

/// 申请后台执行
/// 注意: 在iOS7和该版本前,后台可以用下面的的方式在后台存活5-10分钟,在iOS8及后,最多存活3分钟
{
taskId = [application beginBackgroundTaskWithName:NSStringFromClass([self class]) expirationHandler:^{

MZLOG(@"BackgroundTask. BackgroundTask is Over. The remained time: %f", application.backgroundTimeRemaining);

[application endBackgroundTask:taskId];

taskId = UIBackgroundTaskInvalid;
}];
}

if (UIBackgroundTaskInvalid == taskId) {

MZLOG(@"BackgroundTask. Apply backgroundTask failed.");

return;
}

/// 可以监控后台任务剩余的时间, 针对业务可以去处理
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

__block NSTimeInterval remainedTime;

while (true) {

// 剩余可以后台执行的时间
dispatch_async(dispatch_get_main_queue(), ^{

// application.backgroundTimeRemaining 必须在主线程获取
remainedTime = application.backgroundTimeRemaining;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

MZLOG(@"BackgroundTask. The remained time: %f", remainedTime);

if (remainedTime < 10) {

// 可以告诉其他业务, 后台申请的时间即将结束了
}

if (remainedTime < 2) {

/// 这里可以做一些清除工作
{
// clean up
}

[application endBackgroundTask:taskId];

taskId = UIBackgroundTaskInvalid;

return;
}

// 睡眠(延时)1s
[NSThread sleepForTimeInterval:1.f];
});
});
}
});
}
}

startTask 开启后台任务.

在 AppDelegate 中, 调用这个方法.

1
2
3
4
- (void)applicationDidEnterBackground:(UIApplication *)application {

[[MZBackgroundTask sharedTask] startTask];
}

再次运行工程, 可以根据日志看出, 之前的 dispatch_after 和 timer 可以运行了, 并且可以运行3分钟(180s).

根据 backgroundTimeRemaining 这个属性, 可以看出具体的后台可执行的剩余时间.

1

注意: 我测试的时候使用的是 iOS10 设备.

后记

除了苹果规定的几种类型(如定位, 录音, VOIP 等)的应用外, 其他 APP 想申请更多的后台驻留时间, 就需要一些 旁门左道 的方法了.

苹果对后台操作做了这么多限制, 也是从用户的角度出发, 如安全, 省电, 省流量等.

比如, 在后台播放没有声音的音乐.

再比如, 申请定位服务的权限, 这样也可以保持 APP 在后台不被挂起.
但是, 依照苹果一贯的审核做法来看,如果声明了需要某项后台权限,你却没有相关实现的话,会直接被拒掉的.

这些是技术上的实现, 我没有推荐大家这么干, 现在苹果审核比以前还要严格, 大家还是悠着点干吧.如果你的 APP 不需要上架到 AppStore 的话, 就尽情的放纵吧…

推荐博文

1.iOS实现无限后台background的方法

2.WWDC 2013 Session笔记 - iOS7中的多任务

iOS 在后台的时候如果不使用后台模式, socket 也会被系统关闭连接, 比如我们使用的 IM 功能.
使用后台模式后, 向系统申请的时间(3分钟内), socket 还是没有被关闭的, 除非断网或者被路由器给断开了, 在申请的这段时间内, socekt 还是可以使用的, 如果想持续的保持 socket 连接, 就需要去了解一下 VOIP Socket 相关的知识了, 实践过后, 分享给大家.

附录

下面是具体的实现代码.

ViewController.m

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
56
57
58
59
60
61
@interface ViewController ()

@property (nonatomic, strong) NSTimer *timer;

@end


@implementation ViewController

- (void)viewDidLoad {

[super viewDidLoad];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];

// 每隔一秒执行一次
_timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(onTimerDidRun:)
userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

#pragma mark - Callback.

- (void)onTimerDidRun:(id)sender {

MZLOG(@"App Background. Timer Running.");
}

- (void)onDidEnterBackground:(NSNotification *)notification {

MZLOG(@"App Background. Enter onDidEnterBackground.");

int delta = 1;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delta * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

MZLOG(@"App Background. Enter onDidEnterBackground diapatch.");

UILocalNotification *notification = [UILocalNotification new];
notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:3];
notification.alertBody = @"走, 去high吧!";
notification.soundName = UILocalNotificationDefaultSoundName;
// 可以自定义数据
notification.userInfo = @{@"user_info_key": @"user_info_value_json_str"};

[[UIApplication sharedApplication] scheduleLocalNotification:notification];
});
}

#pragma mark - SetupViews.

- (void)_setupViews {

self.view.backgroundColor = [UIColor purpleColor];

}

MZBackgroundTask.m

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#import "MZBackgroundTask.h"

@implementation MZBackgroundTask

+ (instancetype)sharedTask {

static MZBackgroundTask *_task;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_task = [[self alloc] init];
});

return _task;
}

- (void)startTask {

if (![self _checkSupportBackgroundTask]) {

MZLOG(@"BackgroundTask. Current device don't support backgroundTask.");
return;
}

UIApplication *application = [UIApplication sharedApplication];

__block UIBackgroundTaskIdentifier taskId;

/// 申请后台执行
/// 注意: 在iOS7和该版本前,后台可以用下面的的方式在后台存活5-10分钟,在iOS8及后,最多存活3分钟
{
taskId = [application beginBackgroundTaskWithName:NSStringFromClass([self class]) expirationHandler:^{

MZLOG(@"BackgroundTask. BackgroundTask is Over. The remained time: %f", application.backgroundTimeRemaining);

[application endBackgroundTask:taskId];

taskId = UIBackgroundTaskInvalid;
}];
}

if (UIBackgroundTaskInvalid == taskId) {

MZLOG(@"BackgroundTask. Apply backgroundTask failed.");

return;
}

/// 可以监控后台任务剩余的时间, 针对业务可以去处理
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

__block NSTimeInterval remainedTime;

while (true) {

// 剩余可以后台执行的时间
dispatch_async(dispatch_get_main_queue(), ^{

// application.backgroundTimeRemaining 必须在主线程获取
remainedTime = application.backgroundTimeRemaining;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

MZLOG(@"BackgroundTask. The remained time: %f", remainedTime);

if (remainedTime < 10) {

// 可以告诉其他业务, 后台申请的时间即将结束了
}

if (remainedTime < 2) {

/// 这里可以做一些清除工作
{
// clean up
}

[application endBackgroundTask:taskId];

taskId = UIBackgroundTaskInvalid;

return;
}

// 睡眠(延时)1s
[NSThread sleepForTimeInterval:1.f];
});
});
}
});
}
}

#pragma mark - Private.

/**
* 当前设备是否支持后台任务.
*
* @return YES, 支持后台任务. 否则, 不支持后台任务.
*/
- (BOOL)_checkSupportBackgroundTask {

SEL sel = @selector(isMultitaskingSupported);
BOOL supportBTask = [[UIDevice currentDevice] respondsToSelector:sel];

return supportBTask;
}

使用 Charles

发表于 2017-07-23 | 分类于 Tools |

概要

Charles 是 Mac 上面比较好用的抓包工具.

在使用微信登录 SDK 的时候, 我想看看微信请求的内容, 以及其授权过程, 发现只要连接 Charles, 跳转到微信之后无法打开微信的授权登录页面, 提示网络不可用.

经过研究, 是需要使用 Charles 配置 HTTPS, 配置完成后, 就可以随心所欲了.

所以, 今天跟大家分享一下 Charles 使用的两个点:

  • Charles 的 HTTPS 配置.
  • 使用 Charles 模拟(慢)网络.

工具和系统

  • iPhone: iOS 10.3.
  • Mac OS: 10.12.5.
  • Charles: 4.0.2.

抓取 HTTPS 请求

1.Mac 安装证书

打开 Charles, 在 Help 选项中选择 SSL Proxying, 如下图所示:

1

2.Mac 信任证书

打开 Keychain Access (钥匙串), 找到刚才安装的证书, 信任证书, 如图:

1

3.给 iPhone 安装证书

打开 Charles, 在 Help 选项中选择 SSL Proxying, 操作如图:

1

在弹出的对话框中, 可以看到 chls.pro/ssl

1

手机设置代理, 连接 Charles

一定要记住: 手机一定要设置代理, 否则安装证书会失败.

打开手机的 Safari 浏览器, 输入 chls.pro/ssl 这个地址.

1

设置信任证书, 如下图:

1

1

模拟任意网络

Charles 给我们提供了可以模拟任何网速的网络环境的条件.

1

打开 Throttle Settings, 然后 Enable Throttling

1

可以看到很多类型的网络供你选择.

Add Preset 可以自己自定义一个网络环境.

这个功能非常实用, 可以让我们模拟慢网络环境, 更好地调试自己的程序.

遇到麻烦

1.抓取 HTTPS 的请求出现错误

错误日志如下:

1
2
3
SSLHandshake: Received fatal alert: unknown_ca
Charles Proxy will even offer a helpful suggestion:
You may need to configure your browser or application to trust the Charles Root Certificate. See SSL Proxying in the Help menu.

1

这种错误一般是出现在 iOS10 上面.

请在关于本机里面再设置一次 证书信任设置, 即可.

1

2.系统升级后, 设置的 HTTPS 抓包失效

按照上述方法, 重新设置一次即可.

Cocos2d-js: 在 iOS 上运行

发表于 2017-07-23 | 分类于 Game |

在我的博文 Cocos2d-js: 首次在 Mac 上面运行 这篇文章中, 介绍了 Cocos2d 相关的知识, 并且使用Cocos2dx 创建了一个 JS 的简单游戏项目.

今天跟大家分享一下, 如何在 ios 设备上面运行一个 JS 游戏项目.

该游戏项目(贪吃蛇)来自网络资源, 只供演示使用, 不提供任何关于游戏的资源.

当初的想当然

在 Mac 上面 so easy 的编译并运行了 JS 游戏, 我想在 iOS 设备上应该也很简单的就可以运行起来, 但是我发现我简单不加思考的想法, 是行不通的.

博文 Cocos2d-js: 首次在 Mac 上面运行 在 FireFox 浏览器上面可以直接运行 index.html, 但是在 Chrome 浏览器上面就没那么顺利.

想到这里, 我就受到了启发, 莫非 iOS 设备上面需要自己搭建一套 Web Server?

搭建 Web Server

既然想到了这一步, 接下来就是找方案实施了.

CocoaHTTPServer 这个可以满足我的需求, 于是, 我就直接拿来使用了.

项目中, 我使用了 Cocoapods 来管理第三方库.在 podfile 中直接添加下面的代码:

1
pod 'CocoaHTTPServer', '~> 2.3'

然后 pod install 即可.

1.在 ViewController 中导入头文件

1
2
3
4
5
6
7
8
9
10
11
#import <HTTPServer.h>
ViewController 声明部分

@interface ViewController () <WKNavigationDelegate>
@property (nonatomic, strong) WKWebView *webView;
@property (nonatomic, strong) HTTPServer *localHttpServer;
@property (nonatomic, strong) WKWebViewConfiguration *wbConfig;
//用于调试的 UILabel
@property (nonatomic, strong) IBOutlet UILabel *loadingLb;
@property (nonatomic, assign) BOOL startServerSuccess;
@end

2.配置 Web Server

核心实现如下:

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
 - (void)_configLocalHttpServer
{
NSString *webPath = [[NSBundle mainBundle] pathForResource:@"crazySnake" ofType:nil];
_localHttpServer = [[HTTPServer alloc] init];
[_localHttpServer setType:@"_http.tcp"];

NSFileManager *fileManager = [[NSFileManager alloc] init];
NSLog(@"%@", webPath);

if (![fileManager fileExistsAtPath:webPath]) {
NSLog(@"File path error!");
}
else {
NSString *webLocalPath = webPath;
[_localHttpServer setDocumentRoot:webLocalPath];
NSLog(@"webLocalPath:%@", webLocalPath);
[self _startWebServer];
}
}
- (void)_startWebServer
{
self.loadingLb.hidden = NO;

NSError *error;
if ([_localHttpServer start:&error]) {
NSLog(@"Started HTTP Server on port %hu", [_localHttpServer listeningPort]);
self.port = [NSString stringWithFormat:@"%d", [_localHttpServer listeningPort]];

self.loadingLb.text = @"Start Server Successfully.";

_startServerSuccess = YES;
}
else {
NSLog(@"Error starting HTTP Server: %@", error);

self.loadingLb.text = @"Start Server failed.";

_startServerSuccess = NO;
}
}

_configLocalHttpServer 是配置 HTTPServer 的方法, _startWebServer 是开启 Web Server 的方法.

这样调用 _configLocalHttpServer 就可以开启在设备上面开启一个 Web Server 了.

Xcode 导入 JS 游戏项目

首先看一下我的工程和资源目录结构, 如图:

1

仔细看一下, crazySnake 这个文件夹是蓝色的, 不是黄色的.

这里特别注意, 导入这个 crazySnake 文件夹的时候, Options 一定要选择 Create folder reference.

为什么要这么做?

细心的朋友可以发现, 使用 Create folder reference 导入的文件夹, 在项目打包生成的 archive 的文件中(自己可以解压看)是可以看到 crazySnake 这个文件夹的, 如果不是这种方式, 而是使用 Create groups 方式(在 Xcode 中显示黄色), 在 archive 中是看不到 crazySnake 这个文件夹的, 而是将里面的文件打散放在 archive 的根目录下面了.

总之, 一句话, Create folder reference 方式导入的文件夹在打包后还是原来的文件夹, 不会将文件夹里面的文件打散.

试想一下, 如果多加入几个这样的目录, 都打散在根目录下面, 我们就不好管理这个文件夹里面的文件了.

配置 WKWebView

这里我使用了 WKWebView 而不是 UIWebView 作为加载 HTML 的容器.
当然, 你也可以使用 UIWebView.

主要的核心代码如下:

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
- (void)viewDidLoad
{
[super viewDidLoad];

self.navigationController.navigationBarHidden = YES;

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.loadingLb.text = @"Config server...";
[self _configLocalHttpServer];
});

/// 增加的调式方法: 可以重新启动 web server.
{
SEL sel = @selector(_configLocalHttpServer);
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self
action:sel];
[self.loadingLb addGestureRecognizer:gesture];
self.loadingLb.userInteractionEnabled = YES;
}
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];

// 配置 WKWebView
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

_wbConfig = [[WKWebViewConfiguration alloc] init];
self.wbConfig.userContentController = [[WKUserContentController alloc] init];

_webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:self.wbConfig];

_webView.frame = self.view.bounds;
_webView.scrollView.showsHorizontalScrollIndicator = NO;
_webView.scrollView.showsVerticalScrollIndicator = NO;

[self.view addSubview:self.webView];
self.webView.frame = self.view.bounds;
self.webView.navigationDelegate = self;

if (self.startServerSuccess) {
self.loadingLb.hidden = YES;

NSString *gameUrl = [[NSBundle mainBundle] pathForResource:@"index"
ofType:@"html"
inDirectory:@"crazySnake"];
NSURL *url = [NSURL fileURLWithPath:gameUrl];

url = [NSURL URLWithString:[NSString stringWithFormat:@"http://127.0.0.1:%@/index.html", self.port]];
[self.webView loadRequest:[NSURLRequest requestWithURL:url]];
}
});
}

代码很简单, 就是使用 WKWebView 加载一个 HTML 文件并启动 Web Server.

运行项目

接下来, 就是见证奇迹的时刻了…

上面配置了 Web Server 和 加载 html 的 WKWebView, 现在可以直接运行项目看效果了.

无论你是在模拟器还是在真机上面都可以运行这个 JS 游戏项目了.

1

写在最后

上面的例子本人亲自实践过的, 给大家提供了一个实现思路, 算是抛砖引玉.

如果想做好这个模式, 还需要很多工作要做, 这里列出来给大家分享一下.

1.游戏资源包管理和下载.
2.游戏中需要和 Native 的交互逻辑.
3.数据加密.
4.移动端游戏本身的加载优化.

如果你有更好的方案和想法, 我很乐意邮件 (veryitman@126.com) 与你沟通, 非常感谢!

代码示例我放在了 GitHub, 点击 MZMWPlay 前往下载体验.

Android: Chrome 调试 WEB 页面

发表于 2017-06-01 | 分类于 Android |

引言

在 Mac OS 上面通过 Safari 调试 iOS 设备或者模拟器的 WEB 页面, 比较方便.

关于具体如何使用 Safari 来调试 iOS 设备, 可以参考 [Safari 的开发者模式] 这篇文章.

自从踏上开发的道路, 电脑上就安装了 Chrome 浏览器, 并且一直保持着更新, Chrome 以其简单设计和高效的用法吸引了大批开发者.

使用 Chrome 调试 Android 设备的 WEB 页面, 易如反掌.

Chrome 调试条件

使用这项技能的条件如下:

1.Android 设备系统最低为4.4, 也就是 android-19

2.Chrome 最好使用最新版本的, 以前的版本不支持, 最低兼容版本目前尚不清楚.

3.配置 Android WebView, 使其支持 Debug 和使能 JavaScript 功能.

4.设备和 PC 通过 USB 连接正常, 且支持 USB 调试的.

我目前使用的设备和 Chrome 版本信息如下:

Android 设备: 5.1.1版本
Chrome: Version 58.0.3029.110 (64-bit)

我们在地址栏中输入:

1
chrome://inspect

就可以打开设备列表界面.
1

可以看到我的 Android 设备已经显示出来了.

下面举个例子说明该调试工具的使用.

简单的 Android 工程示例

该工程只有一个 Activity, 使用 WebView 加载本地 HTML 文件.

1. 创建 assets 目录

工程切换到 Project Files 视图
1

这样就可以在 src/main 目录下新建了 assets 文件夹了.

2. 创建 HTML 文件

该 HTML 文件, 命名为 local.html, 放在 assets 目录下面.

local.html 源文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<body>
<button onclick="myFunction()">Click me</button>
<p id="demo"></p>
<p>A function is triggered when the button is clicked.</p>
<script>
function myFunction() {
console.log('js console.');
}
</script>
</body>
</html>

3.在 Activity 中配置 WebView.

Activity.java 源文件

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
public class MZWebPage extends AppCompatActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mzweb_page);
webView = (WebView) this.findViewById(R.id.ui_web_view);
// 设置 WebView 的 Debug 为可用状态
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
WebSettings settings = webView.getSettings();

// 设置 JavaScript 可以使用, 否则在 HTML 中无法调用 JavaScript 代码
settings.setJavaScriptEnabled(true);
// 本地文件
String url = "file:///android_asset/local.html";
webView.loadUrl(url);
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
return super.shouldOverrideUrlLoading(view, request);
}
});
// 设置渲染视图
webView.setWebChromeClient(new WebChromeClient());
}
}

对应的 xml 布局文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="demo.droidsdk.near.idreamsky.com.nearsdk.MZWebPage">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/ui_web_view">
</WebView>
</LinearLayout>

4. 运行程序

在 Chrome 中输入 chrome://inspect 打开, 可以看到正在运行的 local.html.
1

点击 inspect, 进入调试界面.

在调试界面, 点击页面的 Click me, 按钮, 对应的会有 log 输出.
1

5.调试出错的 HTML

修改一下 local.html 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<body>
<button onclick="myFunction()">Click me</button>
<p id="demo"></p>
<p>A function is triggered when the button is clicked.</p>
<script>
function myFunction() {
console.log('js console.' + var_error);
}
</script>
</body>
</html>

这里可以看出 var_error 是一个不存在的变量.

重新运行, 点击 Click me, 可以看到右侧的 error:
1

关于 Chrome 开发者工具的更多使用, 可以参考官方文档 Chrome 开发者工具.

保持你的好习惯

发表于 2017-05-29 | 分类于 随笔 |

1

明天就是传统的端午节了, 提前祝大家端午安康!

今天老婆让我帮忙把粽叶子洗洗, 说要包粽子, 我听话地把刚买来的粽叶子洗了个干净, 那都是带有绿色的叶子, 生机勃勃, 让我从指尖中感到无比的温馨和无尽的希望.

小的时候, 我很享受妈妈为我们包粽子的过程, 说实话, 我不是很喜欢吃粽子, 但是特别喜欢粽子锅里面的煮鸡蛋, 特别美味.

老家的粽子没有什么特色, 就是简简单单的糯米被包在叶子中, 然后放在锅里慢慢的煮, 熟了之后, 蘸点白糖就可以吃了.

到了南方之后, 我才发现世界上原来有各种各样的粽子, 红枣的, 绿豆的, 猪肉馅的, 鸡肉馅的…等等!

这些都是偶发的回忆, 和今天的推文没有关系, 不信, 你接着往下看.

养成阅读的习惯

我不是一个聪明的孩子, 直到小学三年级, 我才真正的开始明白学习到底是怎么一回事, 用老家人的话说算是开窍了.

后来我很顺利的上了初中, 家里的大部分收入要靠庄家, 一年四季能够风调雨顺的话, 也能过个不错的年, 至少能把学费给交上.

初中二年级的时候, 我辍学了, 也许是命运的安排吧, 当时自己也没有多想, 毅然决定下学去寻找另一种人生.

于是, 我选择了理发这个行业. 和师傅在风风雨雨中度过了一年多的时间, 在当时的农村, 不上学有门手艺也算是对自己和父母有个交代.

我的内心仍然渴望去上学, 虽然不知道为什么要去上学也不知道上学后能干什么, 但是心里面总是觉得自己不能放弃学业.在理发的一年多时间里, 我没有放弃过学习.
冥冥之中自有安排, 妈妈有次问我: “还想去上学不?”, 我想都没想就情不自禁的点头了, 妈妈又说:”我就知道你没有放弃, 去继续上学吧, 不要留下遗憾!”.当时的我甭提有多高兴了.

再次回到校园的我比以前更加努力和拼命了, 更加懂得如何珍惜这来之不易的学习机会了.

直到大学毕业, 参加工作以及有了孩子后, 我也能保持自我学习和自我激励的状态, 保持好的阅读习惯, 让我受益终身.

有次孩子早晨起床忙完后, 就开始坐在那里看书, 有好几次晚上, 我也发现他洗完澡后, 坐在那里看书, 或者是做手工, 或是画画.

我感到很好奇, 决定找个机会问问他.

有一个周末的上午, 早餐过后, 我就把他拉到身边, 和他一起看儿童读物, 问他:”宝贝, 你为什么喜欢看书呢?”, 他说:”爸爸, 是你影响了我, 你喜欢看书, 我也喜欢看书.”, 看着他一副天真的样子, 我真的不敢相信几岁的孩子能说出这样的话, 但我内心感到无比的欣慰.原来好的习惯会传染给自己身边的人, 并且能让你感到自豪.

坚持写博客

写博客或者是生活日记, 确实很”浪费”时间, 甚至会占用你任何休息的时间, 写博客偶尔也会让你抓耳挠腮, 感叹:”书到用时方恨少”!

所以, 读, 写不能分家, 更应该是将二者紧密的联合在一起.
多阅读多写, 才会让你更加的通透, 当然了, 我只是喜欢写, 写的质量也很一般, 只是为了满足内心的那一点点对知识的渴望.

写的多了, 你就更加愿意去思考了, 思考多了, 很多事情你就看开了, 对你的生活和工作都有好处.

从09年到现在我一直坚持写博客, 刚开始也是无从下笔, 不知道自己到底该写一写什么东西, 写了之后, 是不是别人看了会嘲笑我, 诸如此类的担心太多太多, 后来我也想明白了, 你写的再好也有人比你写得好, 也会有人说你写的不好.那我何必去计较这么些个事儿了, 只管动手写吧!

然后, 就在博客网站上面耕耘, 技术的, 吐槽的, 生活的我都喜欢写, 不管你喜不喜欢看, 它都在那里, 哈哈!

我把博客当做自己的朋友, 心情好亦或是不好的时候, 我都会向他倾诉, 希望自己写下的东西或多或少能帮助一些人, 这也许是我坚持写博客的最大动力了.

如果你也喜欢写博客或者是准备写博客, 就开始动手写吧, 不要再犹豫了.

最后

这个世界上, 没多少人喜欢听别人啰嗦, 即使听了你的啰嗦, 也不一定会付出实践, 即使你是长者或者是智者, 况且我只是一介平民.

但是我还是要啰嗦一句:”保持持续学习的习惯, 会消耗你的勤奋, 但会让你受益终生”, 信不信由你!

Safari 的开发者模式

发表于 2017-05-15 | 分类于 iOS |

设置 Safari

1.打开 Safari 的 Preference
1

2.切换到 Advanced 选项
1

将 Show Develop menu in menu bar 选中.

可以看到 Safari 多了 Develop 选项
1

设置设备或者模拟器

在 iphone或者模拟器 中设置 Safari

打开 Safari 的高级选项

1

打开 Web 检查器

1

上面设置完毕后, 运行你的程序, 在 Safari 的 Develop 中可以看到对应设备, 从而可以调试 HTML 页面了.
1

在调试窗口中,可以看到当前正在加载网页的各种信息,如源码、请求头、图片、加载的资源与脚本、控制台输出等.

也可以直接修改网页的 CSS 样式和布局等进行修改,而不用重新运行 App.
1

使用 Safari 的开发者模式, 可以帮助我们调试前端页面, 解决一些问题, 比较方便.

iOS: 自定义 UIWebView 和 WKWebView 的 UserAgent

发表于 2017-05-14 | 分类于 iOS |

导言

在 iOS8, 苹果推出了 WKWebView, 目的很明显: 取代 UIWebView.

随着时间的推移, 越来越多的应用开始最低版本支持为 iOS8, 大家也纷纷的开始使用 WKWebView 组件.

在 iOS10.x 版本上, UIWebView 的表现不尽如人意, 在我们的后台日志和用户反馈, 存在很多莫名其妙的 crash 以及卡顿现象, 内存飙升等问题. 针对不同的 JS 游戏引擎, UIWebView 表现也不够好, 所以是时候使用 WKWebView 了.

但是话又说回来, 如果你最低版本支持 iOS8, 也不能完全放弃 UIWebView, WKWebView 有一部分 API 只有 iOS9+ 才有, 如果你的应用在 iOS8 运行而使用 iOS9 的 API, 肯定会 crash.

例如:

1.清除缓存.
2.设置 user-agent.

今天要说的是关于设置自定义 UIWebView 和 WKWebView 的 UserAgent 问题.

WKWebView UserAgent

默认的 UserAgent

使用下面的代码, 可以输出 WKWebView 的默认 UserAgent.

1
2
3
4
5
6
7
8
9
10
- (void)viewDidload 
{
[self.wkWebView evaluateJavaScript:@"navigator.appName" completionHandler:^(id __nullable appName, NSError * __nullable error) {
NSLog(@"navigator.appName: %@", appName);
}];

[self.wkWebView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id __nullable userAgent, NSError * __nullable error) {
NSLog(@"navigator.userAgent: %@", userAgent);
}];
}

navigator.appName 无论在 iOS8, iOS9 还是 iOS10, 输出结果都是一致的.

navigator.userAgent 在不同的 iOS 系统上面输出结果略有不同.

1
2
3
4
5
6
// iOS 8.3
// Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12F70
// iOS 9.0
// Mozilla/5.0 (iPhone; CPU iPhone OS 9_0 like Mac OS X) AppleWebKit/601.1.32 (KHTML, like Gecko) Mobile/13A4254v
// iOS 10.x
//Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Mobile/14E269

自定义 UserAgent

修改默认的 UserAgent, 需要使用下面的方法.

1
self.wkWebView.customUserAgent = @"CustomUserAgent"

这样, 就改变了其默认的 UserAgent 值了.

特别需要注意: customUserAgent 是 iOS9 之后才有的字段.

1
2
3
/*! @abstract The custom user agent string or nil if no custom user agent string has been set.
*/
@property (nullable, nonatomic, copy) NSString *customUserAgent API_AVAILABLE(macosx(10.11), ios(9.0));

所以在 iOS8.x, 如果你想改变 UserAgent, 还是要使用 UIWebView.

HTML 使用 UserAgent

HTML 中可以根据自定义的 UserAgent, 做出不同的行为, 如自定的 UserAgent 可以包括应用的版本号, HTML 可以根据不同的版本来做版本区分等逻辑.

在 HTML 中, 可以使用 JS 来获取 UserAgent 的值.

Demo.html

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
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>user-agent</title>
<script type="text/javascript">
function getUserAgent() {
var ug = navigator.userAgent;
document.getElementById("mySpan").innerHTML = ug;
}
</script>
</head>
<body>
<h1>显示 user-agent</h1>
<p><span style="font-size:30px" id="mySpan">..</span></p>
<p><input type="button" value="点击显示 user-agent" onclick="getUserAgent()" class="button"/></p>
</body>

<style>
.button {
background-color: #4C0950;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 40px;
margin: 20px 200px 100px 300px;
cursor: pointer;
border-radius: 15;
}
</style>
</html>

使用 WKWebView 加载该 Demo.html 文件, 效果图如下:
1

UIWebView UserAgent

默认的 UserAgent

使用下面的代码, 可以输出 UIWebView 的默认 UserAgent.

1
2
3
4
5
NSString *appName = [self.webView stringByEvaluatingJavaScriptFromString:@"navigator.appName"];
NSLog(@"navigator.appName: %@", appName);

NSString *userAgent = [self.webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
NSLog(@"navigator.userAgent %@", userAgent);

打印输出结果和 WKWebView 的结果一致.

设置自定的 UserAgent

设置一个 key 为 @”UserAgent” 的本地存储即可.

1
2
3
4
5
6
7
[[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent":@"CustomUserAgent-UIWebView"}];

NSString *appName = [self.webView stringByEvaluatingJavaScriptFromString:@"navigator.appName"];
NSLog(@"navigator.appName: %@", appName);

NSString *userAgent = [self.webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
NSLog(@"navigator.userAgent %@", userAgent);

这样就改变了 UIWebView 的默认 UserAgent 了.

小结

应用最低版本支持 iOS7/8 的开发者们, 可以考虑 UIWebView 和 WKWebView 并存的开发模式. iOS9及以上的开发者你们, 可以完全放弃 UIWebView, 大胆的拥抱 WKWebView 了.

移植 UIWebView 到 WKWebView 的成本不算大.
如果你现在有时间, 可以开始着手做这些事了.前端和客户端需要考虑之前交互那部分的逻辑, 即 JS 调用 Native 或者 Native 调用 JS 的需要做些调整, 因为 WKWebView 的调用方式和 UIWebView 不一样了, 前端页面要考虑版本兼容性.

新的 WKWebView 虽然存在一些坑, 但是使用它亦是大势所趋, 正所谓 存在即合理, 你没有理由拒绝它.

后续, 我会写一写在 WKWebView 上面遇到的一些问题, 分享给大家.

之前我也写过一篇文章, 是关于 UIWebView 缓存的, 不妨, 你也瞅瞅 [iOS: 聊聊 UIWebView 缓存].

Cocos2d-js: 首次在 Mac 上面运行

发表于 2017-05-14 | 分类于 Game |

Cocos2d-js 介绍

如果你是刚接触 Cocos2d-js 开发的人, 一定会被 Cocos2d-js 和 Cocos2d-HTML5 以及 Cocos2d-x 搞得晕头转向.

下面简单介绍一下 Cocos2d-js 的前世今生.

目前 Cocos2d-x 支持使用 C++、Lua、Javascript 语言来进行开发,且内置 JavaScript 引擎,通过 C++ 解析 Javascript 去执行.

Cocos2d-HTML5 是使用 JavaScript 进行开发,最终运行在浏览器里的.

v3.0 后,Cocos2d-HTML5 和 Cocos2d-x JSBinding 被合到了一起,称作 Cocos2d-JS.

与 Cocos2d-HTML5 不同的是,Cocos2d-js 开发的程序不仅能运行在浏览器里,还能编译运行在 Mac OSX, Windows, iOS, Android 平台上.

从 Cocos2d-x v3.7 版本之后, js 版本被合并到 Cocos2d-x 中了.

具体可以看 Release Note 这个说明.

这个版本的发布日期是在2015年7月21.

原来的 Cocos2d-js 版本在 [GitHub] 上面已经停止更新了.

说明如下:

2/27/2016 - This repository is no longer active.
Cocos2d-JS was merged with Cocos2d-x starting at version 3.7.

可以看出, Cocos2d-js 原来独立的项目被合并到了 Cocos2d-x 中了.

现在使用 Cocos2d-x 不仅可以创建 cpp 项目和 lua 项目, 也可以创建 js 项目了.

官方有说明的:

Cocos2d-JS is Cocos2d-x engine’s JavaScript version that includes Cocos2d-html5 and Cocos2d-x JavaScript Bindings.

至于什么是 Cocos2d-x JavaScript Bindings, 大家自行 google, 这里不赘述.

关系图大概如下:
1

Cocos2d-x 框架图
1

搭建 Cocos2d-js 开发环境

1.下载 Cocos2d-x

可以在官网下载最新的 Release 包, 点击 下载地址, 我下载的最新包是 cocos2d-x-3.15.

下载解压即可, 这里我把 cocos2d-x-3.15 修改为 cocos2d-x.

设置环境变量

在 cocos2d-x 目录, 执行

1
python setup.py

执行完毕后, 可以看到 ‘~/.bash_profile` 的内容

1
2
3
4
5
6
# Add environment variable COCOS_CONSOLE_ROOT for cocos2d-x
export COCOS_CONSOLE_ROOT=/Users/mark/workspace/mzProjs/c2dx/cocos2d-x/tools/cocos2d-console/bin
export PATH=$COCOS_CONSOLE_ROOT:$PATH
# Add environment variable COCOS_TEMPLATES_ROOT for cocos2d-x
export COCOS_TEMPLATES_ROOT=/Users/mark/workspace/mzProjs/c2dx/cocos2d-x/templates
export PATH=$COCOS_TEMPLATES_ROOT:$PATH

这里, 没有看到我的 ANT_ROOT 路径, 其实该脚本智能的找到 /usr/local/Cellar/ant/1.9.7/bin, 但是我不想使用这个 ant, 想使用自己下载的 ant, 修改该文件如下:

1
2
3
4
5
6
7
8
9
# Add environment variable COCOS_CONSOLE_ROOT for cocos2d-x
export COCOS_CONSOLE_ROOT=/Users/mark/workspace/mzProjs/c2dx/cocos2d-x/tools/cocos2d-console/bin
export PATH=$COCOS_CONSOLE_ROOT:$PATH
# Add environment variable ANT_ROOT for cocos2d-x
export ANT_ROOT=/Users/mark/developer/apache/apache-ant-1.10.1/bin
export PATH=$ANT_ROOT:$PATH
# Add environment variable COCOS_TEMPLATES_ROOT for cocos2d-x
export COCOS_TEMPLATES_ROOT=/Users/mark/workspace/mzProjs/c2dx/cocos2d-x/templates
export PATH=$COCOS_TEMPLATES_ROOT:$PATH

最后执行:

1
source ~/.bash_profile

使环境变量立即生效.

创建 js 工程

cd 到任意目录, 执行:

1
cocos new HelloJs -l js -d .

new HelloJs 表示工程名字为 HelloJs
-l js 表示创建 js 工程.
-d . 表示在当前路径创建该工程

可以看到下面创建工程的过程:

1
2
3
4
5
6
7
8
9
Copy template into /Users/mark/workspace/mzProjs/c2dx/works/HelloJs
Copying directory from cocos root directory...
Copying files from template directory...
Copying Cocos2d-x files...
Rename project name from 'HelloJavascript' to 'HelloJs'
Replace the project name from 'HelloJavascript' to 'HelloJs'
Replace the project package name from 'org.cocos2dx.hellojavascript' to 'org.cocos2dx.HelloJs'
Replace the Mac bundle id from 'org.cocos2dx.hellojavascript' to 'org.cocos2dx.HelloJs'
Replace the iOS bundle id from 'org.cocos2dx.hellojavascript' to 'org.cocos2dx.HelloJs'

工程目录:
1

工程发布

将上面的工程编译发布.

编译发布:

1
cocos compile -p web -m release

-p web 表示编译 web 平台.
-m release 表示编译为 release 模式.

编译输出:

1
2
3
4
5
6
7
8
Building mode: release
running: '/Users/mark/developer/apache/apache-ant-1.10.1/bin/ant -f /Users/mark/workspace/mzProjs/c2dx/works/HelloJs/publish/html5/build.xml'
Buildfile: /Users/mark/workspace/mzProjs/c2dx/works/HelloJs/publish/html5/build.xml
compile:
[jscomp] Compiling 156 file(s) with 42 extern(s)
[jscomp] 0 error(s), 0 warning(s)
BUILD SUCCESSFUL
Total time: 8 seconds

如果你在这一步编译报错, 或者执行不下去, 一般都是没有在环境变量中配置好 ant.

编程成功后, 会多出 publish 目录:
1

运行

执行 cocos run 命令来在系统默认的浏览器上面运行.

cd 到工程目录, 执行:

1
cocos run -p web -s . --port 9009

这样会帮我们在本地启动一个 webServer 来运行示例, 直接打开了默认使用的浏览器.

-s: 表示当前需要执行的源码路径
–port: 指定端口, 如果不指定端口, 默认在 8000 端口执行.

更多 cocos run 命令可以, 使用下面的命令来查看帮助:

1
cocos run --help

本地运行 index.html 的问题

你可以用火狐浏览器(FireFox)打开本地工程目录中的 index.html, 即可看到运行效果了.
FireFox 应该是在本地启动了一个 WebServer 来运行.

在 chrome 中直接运行 index.html, 无法看到最终效果, 一直卡在 loading 界面.

打开 inspect, 可以看到报错信息:

1
2
XMLHttpRequest cannot load file:/xx/publish/html5/project.json.
Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

网上有人讨论过过问题, 说是 chrome 跨域不支持 file 协议,那么即使你允许 chrome 跨域支持, 直接运行本地的 index.html 还是报错:

1
game.min.js:54 Uncaught TypeError: Cannot read property 'modules' of null

在 MacOS 上面允许 chrome 跨域访问文件的方法:

1
open /Applications/Google\ Chrome.app --args --allow-file-access-from-files

可以看出, 在这方面, FireFox 做的比 chrome 好.

更新

更新时间 2017-10-15

在微信公众号上面看到 一招教你辨别 Cocos、Cocos2d、Cocos2d-x 这篇文章, 大家可以看看, 对 cocos 会有更加深入的了解.

<1…151617…20>

193 日志
16 分类
163 标签
© 2024 veryitman