移动周分享-第61期

CloudKit初探 - 刘康

什么是CloudKit

  • iCloud
  • Documents
  • iCloud Drive
  • CloudKit

CloudKit 基础对象类型

  • CKContainer: Containers 就像应用运行的沙盒一样,一个应用只能访问自己沙盒中的内容而不能访问其他应用的。Containers 就是最外层容器,每个应用有且仅有一个属于自己的 container。

  • CKDatabase: Database 即数据库,私有数据库用来存储敏感信息,比如说用户的性别年龄等,用户只能访问自己的私有数据库。应用也有一个公开的数据库来存储公共信息,例如你在构建一个基于地理位置的应用,那么地理位置信息就应该存储在公共数据库里以便所有用户都能访问到。

  • CKRecord: 即数据库中的一条数据记录。CloudKit 使用 record 通过 key-value 结构来存储结构化数据。关于键值存储,目前值的架构支持 NSString、NSNumber、NSData、NSDate、CLLocation,和 CKReference、CKAsset(这两个下面我们会说明),以及存储以上数据类型的数组。

  • CKRecordZone: Record 不是以零散的方式存在于 database 之中的,它们位于 record zones 里。每个应用都有一个 default record zone,你也可以有自定义的 record zone。

  • CKRecordIdentifier: 是一条 record 的唯一标识,用于确定该 record 在数据库中的唯一位置。

  • CKReference: Reference 一种引用关系。以地理位置签到应用为例,每个地理位置可以包含很多用户在该位置的签到,那么位置与签到之间就形成了这样一种包含式的从属关系。

  • CKAsset: 即资源文件,例如二进制文件。还是以签到应用为例,用户签到时可能还包含一张照片,那么这张照片就会以 asset 形式存储起来。

有哪些Api(Diary Demo)

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
func updateRecord(diary: Diary, record: CKRecord) {

record.setObject(diary.content, forKey: "Content")

record.setObject(diary.created_at, forKey: "Created_at")

if let location = diary.location {
record.setObject(location, forKey: "Location")
}

if let title = diary.title {
record.setObject(title, forKey: "Title")
}

record.setObject(diary.id, forKey: "id")

privateDB.saveRecord(record, completionHandler: { (newDiary, error) -> Void in

debugPrint("Diary Updated")

if let error = error {
debugPrint("error \(error.description)")
}

})
}

1
2
3
4
5
func deleteCloudRecord(record: CKRecord) {
privateDB.deleteRecordWithID(record.recordID) { (recordID, error) -> Void in
print("Delete \(recordID?.recordName) \(error?.description)")
}
}

查询 -> 修改 -> 更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func fetchCloudRecordWithTitle(title: String , complete: ([CKRecord]?) -> Void) {

let predicate = NSPredicate(format: "Title == %@", title)

let query = CKQuery(recordType: "Diary",
predicate: predicate )

privateDB.performQuery(query, inZoneWithID: nil) {
results, error in
if let results = results {
complete(results)
} else {
complete(nil)
}
}
}

碰到的问题(踩到的坑)

  1. Saving CloudKit Record “Not Authenticated” (9/1002)“ ”This request requires an authenticated account
    原因:使用模拟器或者设备没有启用iCloud
  2. Couldn't get container configuration from the server for container
    原因:container’s identifier错误,可能是使用默认值,也可能是与代码里初始化container时的identifier不一致;
    另外,需要与开发者中心Certificates, Identifiers & Profiles的iCloud Containers保持一致。需要5分钟左右才可以使用
  3. Invalid Arguments(12/2015); server message = Field '___recordID' is not marked queryable
    原因:未设置Metadata Indexes.需要前往CloudKit Dashboard -> Record Types -> Metedata Indexes设置

收费

等不及要试试 CloudKit 了?它能让你从编写服务端代码、监控服务器压力、长期维护大量的 CDN、租用服务器等等等等的事情中解脱出来。等等!CloudKit 怎么收费呢,会很贵吗?答案是:免费。目前苹果允许你使用 CloudKit 存储 10 GB 资源,100 M 数据库存储,每天 2 GB 流量;当你的用户数量增加的时候,这些免费额度也相应地增加到 1 PB 存储、10 TB 数据库存储,以及每天 200 TB 流量。

参考

https://developer.apple.com/library/ios/documentation/DataManagement/Conceptual/CloudKitQuickStart/EnablingiCloudandConfiguringCloudKit/EnablingiCloudandConfiguringCloudKit.html#//apple_ref/doc/uid/TP40014987-CH2-SW1

https://icloud.developer.apple.com/dashboard

http://nshipster.cn/cloudkit/

@(iOS)[universal links, 通用链接]

  • 通过唯一的网址, 就可以链接一个特定的视图到你的 APP 里面, 不需要特别的 schema
  • 不再需要JavaScript检查平台,跳转也不需要js去做特定处理
  • 比scheme更灵活更安全的匹配URL跳转

注意:不能使用模拟器调试

image

工作原理:When the app is installed, the system downloads and verifies the site association file for each of its associated domains. If the verification is successful, the app is associated with the domain.

Configure your file server

  • 我们的根路径配置文件
  • 根路径配置有效性验证
    要求如下:

    Alright! So you have your signed apple-app-site-association file. Now you just need to configure your file server to host this for you. There are a few caveats:

  • It must be sent with the header ‘application/pkcs7-mime’
  • It must be sent from the endpoint youdomain.com/apple-app-site-association
  • It must return a 200 http code.

We set up the one for all Branch integrated apps using our Node+Express link servers. Here’s the code we used in case that’s helpful:

1
2
3
4
5
var aasa = fs.readFileSync(__dirname + '/static/apple-app-site-association');
app.get('/apple-app-site-association', function(req, res, next) {
res.set('Content-Type', 'application/pkcs7-mime');
res.status(200).send(aasa);
});

可以从原有的scheme过渡过来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
[self handleRouting:url];
return YES;
}

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler {
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
[self handleRouting:userActivity.webpageURL];
}
return YES;
}

- (void)handleRouting:(NSURL *)url {
....
}

视频优先推荐
https://developer.apple.com/videos/play/wwdc2015/509/

优秀博客
https://blog.branch.io/how-to-setup-universal-links-to-deep-link-on-apple-ios-9

https://blog.branch.io/ios-9.2-deep-linking-guide-transitioning-to-universal-links

http://blog.hokolinks.com/how-to-implement-apple-universal-links-on-ios-9/

可能出现的bug(巨坑,也可能是配置问题)
http://stackoverflow.com/questions/32751225/ios9-universal-links-does-not-work

结合实例介绍 nginx 配置 - 曾铭

默认配置

DNS, nginx, tomcat, application

https://api-pre.51offer.com/mobile/user/login

DNS: api-pre.51offer.com(host) -> IP(nginx)

nginx: IP(nginx):80 -> IP(tomcat):8080

tomcat: /mobile

application: /user/login (controller, function)

看 nginx 配置

两个问题

https://api-pre.51offer.com/xyz.html
https://api-pre.51offer.com/apple-site-app-config

线上 fixbug

http://www.51offer.com/aboutus/server.html?in_app=1

api.51offer.com

https://api.51offer.com/aboutus/server.html?in_app=1
https://api.51offer.com/aboutus/protocol.html?in_app=1

移动周分享-第60期

微信-支付宝-银联支付对比 - 王胜

微信支付

业务流程

pay-weixin-timeline

支付宝

业务流程

pay-alipay-timeline

银联

业务流程

pay-unionpay-timeline

结论

支付平台 流程图 文档 接口设计 demo 测试相关
微信 ♥♥♥♥♥ ♥♥♥♥♥ ♥♥♥♥♥ ♥♥♥ ♥♥♥♥
支付宝 ♥♥♥♥ ♥♥♥ ♥♥♥♥ ♥♥ ♥♥
银联 ♥♥♥ ♥♥ ♥♥ ♥♥ ♥♥♥♥

说明:

  • 路程图

    从全面性和完整性看,微信 > 支付宝 > 银联

  • 文档:

    微信更简洁,清晰,完整;其他两家还是相对传统的风格,尤其是银联

  • 接口设计:

    微信的接口设计更简洁灵活易用,支付宝次之,银联的字段说明另类。

  • demo:

    微信demo还算清楚,但是美中不足项目编码非utf-8,还得用Notepad++才正确编码显示;支付宝和银联的demo看起来晕。

  • 测试:

    微信的测试环境是通过添加沙箱sandbox路径实现;另外,微信提供了测试用例,赞一个。支付宝没有看到有测试环境。银联在sdk的API参数中,提供了环境参数设置。

iOS响应者链与hit-testing - 刘康

今天我们来探索一下,当我们点击微信扫一扫就能打开扫码视图,当我们点击屏幕的时候。背后发生了些什么?

当用户通过以上方式触发一个事件时,会将相应的事件对象添加到UIApplication的事件队列中。UIApplication会循环的从队列中拿出第一个事件来处理。首先将该事件分发给UIApplication的主窗口对象(KeyWindow),然后由主窗口决定如何将事件交给最合适的响应者(UIResponder)来处理取决于事件的类型。这里主要分两种情况:

触摸事件

UIApplication通过一个触摸检测来决定最合适来处理该事件的响应者,一般情况下,这个响应者是UIView对象。
对于触摸事件,window对象会尝试着首先将事件传递给触摸事件发生点得View。这个View被视为“命中测试view” (hit-test view)。

手势和远程控制事件

UIApplication寻找UIWindow中的第一响应者。找到第一响应者(The First Responder)后,会将该事件对象派发给该响应者以便处理。

事件传递响应链

最终所有的事件响应路径都是为了去寻找那个能够响应并处理该事件的对象。因此,UIkit会首先发送该事件给最适合处理该事件的对象。对于触摸事件,这个最适合处理的对象就是 hit-test view既“命中测试view”,并且对于其它事件,这个对象就是“第一响应者”。

命中测试返回触摸事件发生点的view

系统检测到手指触摸(Touch)操作时会将其放入当前活动Application的事件队列,UIApplication会从事件队列中取出触摸事件并传递给key window处理,window对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图:

为了去阐明这个过程,假设用户触摸 view E 如图所示。iOS会以这样的顺序去寻找命中测试view通过检测所有的子view.
Alt text

  1. 触摸点是否在view A的边界之内,如果是它会检测子视图B和C.

  2. 触摸点不在view B的边界之内,但是它在view C的边界之内,因此它就去检测C的子视图D和E。

  3. 触摸事件不在view D的边界之内,但是在view E的边界之内。view E是整个包含触摸事件的view层级中最底端的view,因此view E就名正言顺的成为了“命中测试view”。

例外
不满足一下三个条件的Responder是不能接收触摸事件的:

  • 不允许交互:userInteractionEnabled = NO
  • 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
  • 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。

注意点
一个触摸事件对象将会被关联于命中测试view的整个生命周期,即使这个触摸后来移到了view的边界之外。

实例

  1. 在此例子中button,scrollview同为topView的子视图,但scrollview覆盖在button之上,这样在在button上的触摸操作返回的hit-test view为scrollview,button无法响应,可以修改topView的hitTest:withEvent:方法如下:
1
2
3
4
5
6
7
8
9
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
CGPoint buttonPoint = [_checkedButton convertPoint:point fromView:self];
if ([_checkedButton pointInside:buttonPoint withEvent:event]) {
return _checkedButton;
}
return result;
}
  1. 放弃自身响应,让子视图去响应事件:
1
2
3
4
5
6
7
8
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
//当事件是传递给此View内部的子View时,让子View自己捕获事件,如果是传递给此View自己时,放弃事件捕获
UIView* __tmpView = [super hitTest:point withEvent:event];
if (__tmpView == self) {
return nil;
}
return __tmpView;
}
  1. 扩大按钮可点击区域:
1
2
3
4
5
6
7
8
9
- (UIView*)hitTest:(CGPoint) point withEvent:(UIEvent*) event
{
CGRect rect = [self enlargedRect];
if (CGRectEqualToRect(rect, self.bounds))
{
return [super hitTest:point withEvent:event];
}
return CGRectContainsPoint(rect, point) ? self : nil;
}

加速库在数学运算中的使用 - 杨志平

@(iOS)[加速|Accelerate]

除了加减乘除外还有好多好多数学运算需要我们处理,但我们很多都没有用到,感觉low爆了

Apple:加速框架文档

Any time you’ve got to make some numbers happen, it’s probably worth it to consider using Accelerate

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 模拟随机数据
var doubles = (0...10000).map {_ in Double(arc4random()%10000)}

// 求和
// 常见的加法求和
let reduceSum = doubles.reduce(0) { $0+$1 }
// Accelerate 封装
let accSum = sum(doubles)

// 求最大值(最小值也一样)
let maxOfArr = max(doubles)
let maxOfArr2 = doubles.sort(>).first

// 平均值,哈哈大数据统计,可以测试准确率
let meanValue = mean(doubles)
let meanValue2 = doubles.reduce(0) { $0 + $1/Double(doubles.count) }
meanValue2

// 向量加减乘积
let vector1 = [2,4,5] as [Double]
let vector2 = [3,5,2] as [Double]
let sumArrs = add(vector1, y: vector2)

耗时上对比是不是reduce,map等系统的高阶函数被“加速库”秒了,但使用上貌似reduce,map是比较灵活的

1
2
let newReduceSum = (0...1000).reduce(0) { $0+$1 }
newReduceSum

其他计算的一点的应用

函数混合示例: 使用中文变量😄

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
let 函数点阵密度 = 64

let 频率1 = 4.0
let 相位1 = 0.0
let 幅度1 = 1.0
let 正弦函数1 = (0..<函数点阵密度).map {
幅度1 * sin(2.0 * M_PI / Double(函数点阵密度) * Double($0) * 频率1 + 相位1)
}

let 频率2 = 1.0
let 相位2 = M_PI / 2.0
let 幅度2 = 2.0
let 正弦函数2 = (0..<函数点阵密度).map {
幅度2 * sin(2.0 * M_PI / Double(函数点阵密度) * Double($0) * 频率2 + 相位2)
}

let 频率3 = 10.0
let 相位3 = M_PI / 3.0
let 幅度3 = 4.0
let 正弦函数3 = (0..<函数点阵密度).map {
幅度3 * sin(2.0 * M_PI / Double(函数点阵密度) * Double($0) * 频率3 + 相位3)
}

let 新函数1 = add(正弦函数1, y: 正弦函数2)
let 新函数2 = add(新函数1, y: 正弦函数3)

// Xcode 分栏查看图形排布,尤其是新函数的图形
新函数1.forEach { XCPlaygroundPage.currentPage.captureValue($0, withIdentifier:"新函数1") }
新函数2.forEach { XCPlaygroundPage.currentPage.captureValue($0, withIdentifier:"新函数2") }

正弦函数2.forEach { XCPlaygroundPage.currentPage.captureValue($0, withIdentifier:"正弦函数2") }

正弦函数1.forEach { XCPlaygroundPage.currentPage.captureValue($0, withIdentifier:"正弦函数1") }

傅里叶变换通俗篇讲解

1
2
3
// 查看图像发现‘新函数2’左右有三对波峰,得出它由三个正弦波组成(可对应得出振幅、频率及相位)
let 快速傅里叶转换 = fft(新函数2)
快速傅里叶转换.forEach { XCPlaygroundPage.currentPage.captureValue($0, withIdentifier:"快速傅里叶转换") }

矩阵计算

很多图像处理是根据矩阵做处理的,像素越大,处理性能要求越高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 简单矩阵示例

// ⎛ 1 2 ⎞ ⎛ 3 2 ⎞ ⎛ 5 6
// ⎢ ⎟ * ⎢ ⎟ = ⎢ ⎟
// ⎝ 3 -4 ⎠ ⎝ 1 2 ⎠ ⎝ 5 -2

let A = Matrix([[1, 2], [3, -4]])
let B = Matrix([[3, 2], [1, 2]])
let C = A * B


// 利用逆矩阵求解
// ⎛ 1 1 ⎞ ⎛ 3 ⎞ ⎛ 2
// ⎢ ⎟ * CC = ⎢ ⎟ CC = ⎢ ⎟
// ⎝ 1 -1 ⎠ ⎝ 1 ⎠ ⎝ 1

let AA = Matrix([[1, 1], [1, -1]])
let BB = Matrix([[3], [1]])
let CC = inv(AA) * BB

应用的加速库函数

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
import Accelerate

public func sum(x: [Double]) -> Double {
var result: Double = 0.0
vDSP_sveD(x, 1, &result, vDSP_Length(x.count))

return result
}

public func max(x: [Double]) -> Double {
var result: Double = 0.0
vDSP_maxvD(x, 1, &result, vDSP_Length(x.count))

return result
}

public func mean(x: [Double]) -> Double {
var result: Double = 0.0
vDSP_meanvD(x, 1, &result, vDSP_Length(x.count))

return result
}

public func add(x: [Double], y: [Double]) -> [Double] {
var results = [Double](y)
cblas_daxpy(Int32(x.count), 1.0, x, 1, &results, 1)

return results
}

public func fft(input: [Double]) -> [Double] {
var real = [Double](input)
var imaginary = [Double](count: input.count, repeatedValue: 0.0)
var splitComplex = DSPDoubleSplitComplex(realp: &real, imagp: &imaginary)

let length = vDSP_Length(floor(log2(Float(input.count))))
let radix = FFTRadix(kFFTRadix2)
let weights = vDSP_create_fftsetupD(length, radix)
vDSP_fft_zipD(weights, &splitComplex, 1, length, FFTDirection(FFT_FORWARD))

var magnitudes = [Double](count: input.count, repeatedValue: 0.0)
vDSP_zvmagsD(&splitComplex, 1, &magnitudes, 1, vDSP_Length(input.count))

var normalizedMagnitudes = [Double](count: input.count, repeatedValue: 0.0)
vDSP_vsmulD(sqrt(magnitudes), 1, [2.0 / Double(input.count)], &normalizedMagnitudes, 1, vDSP_Length(input.count))

vDSP_destroy_fftsetupD(weights)

return normalizedMagnitudes
}

移动周分享-第59期

闲聊前后端分离 - 曾铭

  • 现状与问题
  • 看看历史
  • 看看方案
  • 一些启示

如何优雅的自定义Xib - 刘康

先创建一个UIView文件和一个Xib文件,在Xib文件里设置如下:

  • size: Freeform
  • Status Bar: None

使用-init(frame: CGRect)而不是-awakeFromNib()

为了能在Storyboard中使用自定义view,需要实现initWithCoder

1
2
3
4
5
6
7
8
9
override init(frame: CGRect) {
super.init(frame: frame)
setupXib()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupXib()
}

加载Xib关键代码

1
2
3
4
5
6
func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: "WeiboHeader", bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}

设置Xib文件的Files Owner,而不是设置View的Custom Class

如果需要UIView和Xib文件建立控件属性关联,是设置Xib文件的File`s Owner,而不是设置View的Custom Class

Image show
设置了File`s Owner就可以像往常一样拖线了!

在Storyboard中使用Xib

1.在SB中加入一个UIView,将其Class设置成WeiboHeader
2.使用@IBInspectable

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
@IBInspectable var background: UIImage? {
get {
return backgroundImageView.image
}
set {
backgroundImageView.image = newValue
}
}

@IBInspectable var avatar: UIImage? {
get {
return avatarImageView.image
}
set {
avatarImageView.image = newValue
}
}

@IBInspectable var name: String? {
get {
return nameLabel.text
}
set {
nameLabel.text = newValue
}
}

添加了@IBInspectable之后,就可以像系统自带的控件一样设置属性了~

单元测试 - 吴明

  • 注:基于小创分享的总结

明确概念

  • 单元测试 or 集成测试:
  • 单元测试在维基百科是这样定义的
1
单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

集成测试在维基百科的定义

1
集成测试,整合测试又称组装测试,即对程序模块采用一次性或增殖方式组装起来,对系统的接口进行正确性检验的测试工作。整合测试一般在单元测试之后、系统测试之前进行。实践表明,有时模块虽然可以单独工作,但是并不能保证组装起来也可以同时工作

有没有理解?那我们在借鉴下《单元测试的艺术》对单元测试和集成测试的定义:

1
一个单元测试时一段自动化的代码,这段代码调用被测试的工作单元,之后对这个单元的单个最终结果的某些假设进行检验。单元测试几乎都是用单元测试框架编写。单元测试容易编写,能快速运行。单元测试可靠,可读,并且可维护。只要产品代码不发生变化,单元测试的结果是稳定的。
1
集成测试是对一个工作单元进行的测试,这个测试对被测试的工作单元没有完全控制,并使用该单元的一个或者多个真实依赖物,例如时间,网络,数据库,线程或随机数产生器等。

现在应该了解吧,没有了解不要紧,我们再通过他们关系比对
那单元测试和集成测试的关系是怎么样了?
单元测试和集成测试
链接

优点

  • 保证代码质量,防止bug或尽早发现bug的作用
  • 改善代码的设计,节约开发调试时间
  • 大大减少重构中手动验证正确性的时间。
  • 在写单元测试的时候也能发现方法乃至系统结构设计的不合理

  • 前面都是官方文字,现在讲点实在的。单元测试就是测试方法,方法我们又分为两种

    • 有返回值方法
    • 无返回值方法
  • 有返回值方法,我们测试这个方法直接通过测试这个方法返回的值是否跟你预期一样。我们就以一个登陆的demo来解析吧

    • 环境

      • IDE:Android Studio
      • Java测试库:junit,在build.gradle配置

        1
        testCompile 'junit:junit:4.12'

目录结构

1
2
3
4
5
6
  public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

这个是新近一个Android新建的项目,生成的单元测试,直接通过点击方法的运行就能看的结果了。

代码

运行结果

代码

错误运行结果
实际开发中我们也是通过get()获取指定值比较这个应该比较容易理解。重点就是第二个了

  • 无返回值方法,我们只能通过方法内某个对象的方法是否被调用并且调用参数一样。这里就要用到mock,mock其实就是虚拟一个对象,然后根据这个对象方法判断是否被调用,mock目前比较成熟的框架是Mockito,所有我们这里要引用Mockito框架
1
2
3
4
5
6
7
8
9
10
11
12
dependencies {    
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2',
{
exclude group: 'com.android.support', module: 'support-annotations' })

compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha4'
//以下测试框架
testCompile "junit:junit:4.12"
//mockito
testCompile "org.mockito:mockito-core:1.+"}

先看代码这个是一个简单的login代码
Activity

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
 public class MainActivity extends AppCompatActivity implements View.OnClickListener, ILoginView {

private EditText mUserNameView;
private EditText mPassWordView;
private LoginPresenter mPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.login).setOnClickListener(this);
mUserNameView = (EditText) findViewById(R.id.username);
mPassWordView = (EditText) findViewById(R.id.password);
mPresenter = new LoginPresenter(this, HttpClient.getInstance());
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.login:
String username = mUserNameView.getText().toString();
String password = mPassWordView.getText().toString();
if (TextUtils.isEmpty(username)) {
Toast.makeText(this, "用户名不能为空", Toast.LENGTH_LONG).show();
return;
} else if (TextUtils.isEmpty(password)) {
Toast.makeText(this, "密码不能为空", Toast.LENGTH_LONG).show();
return;
}
mPresenter.login(username, password);
break;
}
}

@Override
public void startHomeView() {
Toast.makeText(this, "跳转主页", Toast.LENGTH_LONG).show();
}

@Override
public void onError() {
Toast.makeText(this, "密码错误", Toast.LENGTH_LONG).show();
}
}

ILoginView接口

1
2
3
4
5
6
7
8
9
10
11
 public interface ILoginView {
/**
* 跳转主页
*/

void startHomeView();

/**
* 错误
*/

void onError();
}

HttpClient

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
public class HttpClient {
public static HttpClient mClient;

public static HttpClient getInstance() {
if (mClient == null) {
synchronized (HttpClient.class) {
if (mClient == null) {
mClient = new HttpClient();
}
}
}
return mClient;
}

public interface Callback {
void onSuccess(String phone);

void onError(String error);
}

public void login(String username, String password, Callback callback) {
//为了简单,没有实际操作网络请求只是简单判断返回结果
if (username.length() < 5) {
callback.onError("用户名太短");
} else {
callback.onSuccess("110110");
}

}
}

LoginPresenter

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
public class LoginPresenter {
private ILoginView mLoginView;
private HttpClient mClient;

public LoginPresenter(@NonNull ILoginView mLoginView,@NonNull HttpClient client) {
this.mLoginView = mLoginView;
this.mClient = client;
}

public void login(String username, String password) {
mClient.login(username, password, new HttpClient.Callback() {
@Override
public void onSuccess(String phone) {
mLoginView.startHomeView();
}

@Override
public void onError(String error) {
mLoginView.onError();
}
});
}

public String getDevice() {
return "nexus6p";
}
}

单元测试,mock出来的对象一定要set到这个LoginPresenter对象中,要不然报错org.mockito.exceptions.misusing.NotAMockException

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
public class LoginPresenterTest {    
private LoginPresenter mPresenter;
private ILoginView mLoginView;
private HttpClient mClient;
//初始化,每测试一个方法都会掉这个
@Before
public void setUp() throws Exception {
mLoginView = Mockito.mock(ILoginView.class);
mClient = Mockito.mock(HttpClient.class);
mPresenter = new LoginPresenter(mLoginView, mClient);
}
@Test
public void login() throws Exception {
//无返回值方法
mPresenter.login("123456", "123456");
Mockito.verify(mClient).login("123456", "123456", null);
}
@Test
public void loginTest() throws Exception {
//无返回值方法
mPresenter.loginTest("123456", "123456");
Mockito.verify(mClient).login("123456", "123456", null);
}
@Test
public void getDevice() throws Exception {
//有返回值方法"
Assert.assertEquals(mPresenter.getDevice(), "nexus6p");
}
}

运行看结果

结果
可以看出login方法有问题,这个错误是正常的,就是我要的结果,可是loginTest方法运行正常,最主要的是他们callback为null,login方法执行的callback不是null,所有login方法执行的时候验证出来就是不一致的。

移动周分享-第58期

Java Annotation - 王胜

Annotation是什么

维基百科: A form of syntactic metadaa that can be added to Java source code. 也就是说,Annotation的引入是为了从Java语言层面上,为Java源代码提供元数据的支持。参见维基百科

Annotation的用途

表象,替代之前JDK1.4开发中,大量繁琐的配置项,Annotation的出现其实可以极大简化配置文件的数量和需要关注配置的内容。但其实,注解带来的益处远不至于此。

Annotation的分类

  • 文档标注型

    主要是@Documented,用以标注是否在javadoc中

  • 编译检查型

    主要在编译过程中,给Java编译器若干指令,检查Java代码中是否存在若干问题, 改变编译器的动作或者行为,通过Annotation的使用,可以调整和控制编译器的使用以及让编译器提供关于代码的更多的检查和验证。主要有:@Override,@SuppressWarning

  • 代码分析型

    此类Annotation是在我们开发中使用最多的,主要是通过Annotation提供给代码更多的额外特性和设置,在Java运行过程中发挥作用。常见的是在Spring或者Hibernate等框架中出现的@Controller,@Service,@Bean, @Table, @Enitty等等.

自定义生命周期为Runtime类型Annotation

Runtime的处理主要依赖于反射的接口,在字节码中寻找Annotation的接口和输入参数,提取其内容和数值。大部分的情况下是基于代理模式,动态生成相应的代理类实例,然后通过代理类,调用相应的InvocationHandler,在InvocationHandler之内完成Annotation所要执行的动作;然后再继续调用原来的方法,继续执行。

用户在定义Runtime类型的Annotation时,需要的步骤:

  • 定义Annotation
  • 定义Annotation的处理器类『通过Reflect、InvocationHandler、Proxy.newProxyInstance()来处理具体的逻辑』
  • 选择合适的调用Annotation的时机和切入点。

示例

custom-annotation-demo.png

自定义一个@OnClick注解,通过在方法前面加入这个注解,从而为指定的组件动态添加事件监听方法。

  • 定义OnClick注解

    1
    2
    3
    4
    5
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface OnClick {
    String value();
    }
  • 定义OnClickProxyFactory来处理动态绑定方法的逻辑

    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
    public class OnClickProxyFactory {

    /**
    * 处理OnClick注解
    *
    * @param target 包含OnClick注解的目标对象
    */

    public static void handleOnClickAnnotation(Object target) {
    try {
    Class<?> clsTarget = target.getClass();
    // 检索目标对象的所有方法, 如果含有OnClick注解, 则为注解中声明的属性对象动态添加AddActionListener方法
    for (Method method : clsTarget.getDeclaredMethods()) {
    OnClick onClickAnnotation = method.getAnnotation(OnClick.class);
    if (onClickAnnotation != null) {
    Field field = clsTarget.getDeclaredField(onClickAnnotation.value());
    field.setAccessible(true);
    autoAddActionlistener(field.get(target), getActionListenerProxy(target, method));
    }
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    /**
    * 为目标对象添加addActionListener方法, 该方法的参数ActionListener为ActionListener代理对象
    * @param target 被添加ActionListener方法的事件源对象
    * @param actionListenerProxy ActionListener代理对象
    * @throws NoSuchMethodException
    * @throws InvocationTargetException
    * @throws IllegalAccessException
    */

    private static void autoAddActionlistener(Object target, Object actionListenerProxy) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    // 获取JButton的addActionListener方法对象
    Method methodAddActionListener = target.getClass().getMethod("addActionListener", ActionListener.class);
    // 为JButton对象添加addActionListener方法
    methodAddActionListener.invoke(target, actionListenerProxy);
    }

    /**
    * 获取ActionListener的代理对象
    * @param target 指定的代理对象
    * @param targetMethod 指定的代理对象的方法
    * @return
    */

    public static Object getActionListenerProxy(final Object target, final Method targetMethod) {
    InvocationHandler handler = new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    return targetMethod.invoke(target);
    }
    };
    return Proxy.newProxyInstance(target.getClass().getClassLoader(), new Class[] {ActionListener.class}, handler);
    }
    }
  • 在UI初始化时,调用注解的解析和动态处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class UI extends JFrame{
    // 省略......
    public UI() {
    // 省略......

    // 通过OnClickProxyFactory工厂来处理OnClick注解
    OnClickProxyFactory.handleOnClickAnnotation(this);
    }

    @OnClick("btnBlue")
    public void setBlueBackground() {
    panel.setBackground(Color.BLUE);
    }

    @OnClick("btnRed")
    public void setRedBackground() {
    panel.setBackground(Color.RED);
    }

    @OnClick("btnSayHello")
    public void sayHello() {
    JOptionPane.showConfirmDialog(null, "Hello World!", "Tips", JOptionPane.YES_OPTION);
    }

具体的示例源码,移步 Github-helloJavaSE

参考

Xcode MarkDown的代码文档 ( for swift )

@(iOS)[Markdown, Document]

markdown在swift中的应用

goals

  • 描述各个属性、函数和类的真正用途
  • 高亮函数的输入和输出(参数和返回值)
  • 几个月后还能清晰地记得每个函数属性是为了什么
  • 使用工具制作具有专业外观的使用手册(比如:使用 Jazzy
  • Xcode 里写的代码文档能被预览

markdown grammar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#text#:文本标题

**text**:使文本具有加粗的效果

*text*:使文本具有斜体的效果

* text:使文本成为一个无序列表的元素,值得注意的是,有个 * 后面需要有一个空格。同样,可以使用 + 或 - 实现这个的功能

1.text:使文本成为一个有序列表的元素

[linked text](http://some-url.com):使文本成为可以点击的超链接

![image show](http://www.appcoda.com/wp-content/uploads/2016/05/t52_3_help_inspector1.png):可以显示图片

> text:创建一个块引用。

使用 4 个空格或 1 个 tab 来缩进所写的代码块,等价于 HTML 中的 \\ 标签。可以继续使用 4 个空格或 1 个 tab 来添加另一个缩进

如果不想使用空格或 tab 的话,可以使用 ` 。比如, `var myProperty` 会显示成 var myProperty

另一种创建代码块的方法是添加 4 个 `,并从下一行开始写具体的代码,最后添加 4 个 ` 表示结束

反斜杠修饰 Markdown 的特殊字符就可以避免 Markdown 语法的解析了。比如, \**this\** 就不会产生加粗的效果

注释区域: 3 个斜线(///)或以下面的形式开头:

1
2
3
/**

*/

Case
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
It calculates and returns the outcome of the division of the two parameters.

## Important Notes ##
1. Both parameters are **double** numbers.
2. For a proper result the second parameter *must be other than 0*.
3. If the second parameter is 0 then the function will return nil.

*/

func performDivisionnumber1: Double, number2: Double) -> Double! {
if number2 != 0 {
return number1 / number2
}
else {
return nil
}
}

case image

quick look

关键词

  • Parameter
  • Returns
  • Remark
  • SeeAlso
  • Precondiction
  • Requires
  • Todo
  • Version
  • Author
  • Note
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
/**
Another complicated function.

- Parameter fullname: The fullname that will be broken into its parts.
- Returns: A *tuple* with the first and last name.

- Remark:
There's a counterpart function that concatenates the first and last name into a full name.

- SeeAlso: `createFullName(_:lastname:)`

- Precondition: `fullname` should not be nil.
- Requires: Both first and last name should be parts of the full name, separated with a *space character*.

- Todo: Support middle name in the next version.

- Warning: A wonderful **crash** will be the result of a `nil` argument.

- Version: 1.1

- Author: Myself Only

- Note: Too much documentation for such a small function.
*/

func breakFullNamefullname: String) -> (firstname: String, lastname: String) {
let fullnameInPieces = fullname.componentsSeparatedByString(" "
return (fullnameInPieces[0], fullnameInPieces[1])
}

全关键字

Jazzy 自动产生代码文档

Jazzy 是一款可以为 Swift 和 Objective-C 代码产生具有 Apple 风格的代码文档工具。

效果如下

jazzy 效果

下面以Alamofire为例子:

jazzy —help 查看帮助

  • cd Alamofire 的项目path
  • jazzy —output /Users/xcodeyang/Desktop/jazzy_document

参考博客地址

Android App共享文件Uri不能为file:// - 吴明

  • 先看异常信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
E/StrictMode: null
java.lang.Throwable: file:// Uri exposed through Intent.getData()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1757)
at android.net.Uri.checkFileUriExposed(Uri.java:2346)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8045)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1506)
at android.app.Activity.startActivityForResult(Activity.java:3930)
at android.app.Activity.startActivityForResult(Activity.java:3890)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:843)
at android.app.Activity.startActivity(Activity.java:4213)
at android.support.v4.app.ActivityCompatJB.startActivity(ActivityCompatJB.java:26)
at android.support.v4.app.ActivityCompat.startActivity(ActivityCompat.java:133)
at com.horizon.offer.mail.maildetail.impl.ImageAnnexWrapper.openFile(ImageAnnexWrapper.java:26)
at com.horizon.offer.mail.maildetail.MailDetailActivity.openAnnexFile(MailDetailActivity.java:184)
at com.horizon.offer.mail.maildetail.adapter.MailAnnexAdapter$MailAnnexViewHolder$1.onClick(MailAnnexAdapter.java:75)
at android.view.View.performClick(View.java:5204)
at android.view.View$PerformClick.run(View.java:21153)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

在StrictMode(严格)模式下,App之间共享资源报的异常。

  • 出现这个异常的代码
1
2
3
4
5
6
7
8
public void openFile(@NonNull Activity activity, @NonNull File file) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
/**异常这行代码**/
intent.setDataAndType(Uri.fromFile(file), "image/*");
ActivityCompat.startActivity(activity, intent, null);
}

打印

1
Uri.fromFile(file)

输出信息

1
Uri uri=file:///storage/emulated/0/download/9text.jpg

谓词 - 刘康

  • NSPredicate是Foundation框架中的一个类。
  • 作用:指定数据被获取和过滤的方式。提供了类似于自然语言一样定义一个集合被搜寻的逻辑条件。

为了证明NSPredicate的强大功能,先写一个Person类,做准备工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person: NSObject {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
super.init()
}
override var description: String {
return "name: \(self.name), age: \(self.age)"
}
}

var colleagues = NSMutableArray()
colleagues.addObject(Person(name: "Arthur", age: 45))
colleagues.addObject(Person(name: "Michael", age: 23))
colleagues.addObject(Person(name: "Kenny", age: 25))
colleagues.addObject(Person(name: "Bella", age: 24))
colleagues.addObject(Person(name: "Vincent", age: 36))
colleagues.addObject(Person(name: "Adolph", age: 39))

基本使用

找出年龄为24岁的人:

1
2
let predicateByAge = NSPredicate(format: "age == 24")
let result = colleagues.filteredArrayUsingPredicate(predicateByAge)

参数可以传入:

1
2
3
let age = NSNumber(int: 25)
let predicateByPassAge = NSPredicate(format: "age == %@", age)
let result1 = colleagues.filteredArrayUsingPredicate(predicateByPassAge)

也可传入要对比的属性,这里是age. 属性的key:

1
2
let pridicateByAge1 = NSPredicate(format: "%K == %@", "age", NSNumber(int: 36))
let result2 = colleagues.filteredArrayUsingPredicate(pridicateByAge1)

指定通配的变量,这里用24来替代age:

1
2
let pridicateByAge2 = NSPredicate (format: "age == $age")
let result3 = colleagues.filteredArrayUsingPredicate(pridicateByAge2.predicateWithSubstitutionVariables(["age": NSNumber(int: 24)]))

语法小结

  • 使用%@对应数字,字符串,日期的替代值
  • 使用%K对应要比较的属性,也就是KVC中的key
  • 使用$变量名来表示通配的变量,然后predicateWithSubstitutionVariables来决定具体的变量值

基本比较

找出年龄大于40岁的同事:

1
2
let predicateAgeOver40 = NSPredicate(format: "age > 40")
let boss = colleagues.filteredArrayUsingPredicate(predicateAgeOver40)

找出年龄在22岁~35岁的人:

1
2
3
4
let minAge = NSNumber(int: 22)
let maxAge = NSNumber(int: 35)
let predicateByAge3 = NSPredicate(format: "age BETWEEN {%@, %@}", minAge, maxAge)
let result4 = colleagues.filteredArrayUsingPredicate(predicateByAge3)

语法小结

  • > 大于
  • >= 大于等于
  • < 小于
  • <= 小于等于
  • == 等于
  • != 或者 <> 不等于
  • BETWEEN 介于两者之间,包括上下限

复合比较

  • && 或者AND 逻辑与
  • || 或者 OR 逻辑或
  • !或者NOT 逻辑非
1
2
let predicateByCompare = NSPredicate(format: "age < 30 OR age >= 40")
let result5 = colleagues.filteredArrayUsingPredicate(predicateByCompare)

字符串比较

  • BEGINSWITH 左边表达式以右边表达式开头
  • CONTAINS 左边表达式包含右边表达式
  • ENDSWITH 左边表达式以右边表达式结尾
  • LIKE 左边表达式和右边表达式相似(简单的正则表达式匹配,?匹配一个字符,*匹配0个或者多个字符)
  • MATCHES 可以实现较为复杂的正则表达式匹配
  • 用方括号加cd来不区分大小写和变音符号
  • IN 左边的表达式在右边的集合里

找出名字以“A”开头的同事:

1
2
let pridivateByName1 = NSPredicate(format: "name BEGINSWITH %@", "A")
let result6 = colleagues.filteredArrayUsingPredicate(pridivateByName1)

名字里包含in,不区分大小写,并且年龄大于等于24:

1
2
let pridivateByName2 = NSPredicate(format: "name CONTAINS %@ && age >= %@", "in", NSNumber(int: 24))
let result7 = colleagues.filteredArrayUsingPredicate(pridivateByName2)

复合正则表达式T[a-z]*k:

1
2
let privatedivateByName3 = NSPredicate(format: "name MATCHES 'T[a-z]*k'")
let result8 = colleagues.filteredArrayUsingPredicate(privatedivateByName3)

名字是两者中的一个:

1
2
let privatedivateByName4 = NSPredicate(format: "name IN {'Bella', 'Jack Tomphon'}")
let result9 = colleagues.filteredArrayUsingPredicate(privatedivateByName4)

基于Block的谓词

基于Block能够灵活的定制谓词,这里简单的Block定义age > 24:

1
2
3
4
5
6
7
8
9
10
let blockPredicate = NSPredicate { (person: AnyObject!, _) -> Bool in
var result = false
if let castResult = person as? Person {
if castResult.age > 24 {
result = true
}
}
return result
}
let result10 = colleagues.filteredArrayUsingPredicate(blockPredicate)

Array使用谓词参考:

StackOverFlow

移动周分享-第57期

Java Runtime 动态代理 - 王胜

使用过Spring的朋友应该对AOP『Aspected Oriented Programe』很熟悉,那么是否被里面的前置增强、后置增强、环绕增强所震撼呢?有没有想过这背后的技术实现呢?其实JDK的动态代理就能实现。下面做一个简单的示例,来阐述如何在Runtime时,动态增强原有的方法。

前提环境

假设我们有一个Waiter类,里面含有两个方法:

  • welcome() // 欢迎光临!
  • bye() // 谢谢惠顾!
1
2
3
4
欢迎光临!
谢谢惠顾!

Process finished with exit code 0

后来发现欢迎顾客和送别顾客不够礼貌,需要在欢迎之前说『您好!』,送别顾客后说『祝您愉快!』。但我们不希望动原有的业务类Waiter,那么如何实现呢?

实现步骤

  1. 创建一个业务对象『必须要有接口』, 执行业务方法

    1
    2
    3
    4
    public interface Receptor {
    void welcome();
    void bye();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
      public class Waiter implements Receptor{
    @Override
    public void welcome() {
    System.out.println("欢迎光临!");
    }

    @Override
    public void bye() {
    System.out.println("谢谢惠顾!");
    }
    }
  2. 创建一个具有公用代码的对象,并将这些公用代码声明成方法

    1
    2
    3
    4
    5
    6
    7
    8
    public class PoliteWords {
    public void sayHello() {
    System.out.println("您好!");
    }
    public void sayHappy() {
    System.out.println("祝您愉快!");
    }
    }
  3. 创建一个可以使用公用代码对象的对象,这个对象可以对要被处理的目标对象实施方法拦截,执行公用代码对象中的方法。借助JDK提供的API『InvocationHandler』

    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
    public class ReceptorHandler implements InvocationHandler {
    // 省略...

    /**
    * 重写invoke方法,从而达到增强目标对象的目的
    *
    * @param proxy 增强后的对象
    * @param method 目标对象上正在被调用的方法
    * @param args 目标对象上正在被调用的方法所传递的参数
    * @return 目标对象正在被调用的方法执行的结果
    * @throws Throwable
    */

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object result;
    if (method.getName().equals("welcome")) {// welcome之前sayHello
    interceptor.sayHello();
    result = method.invoke(target, args);
    } else if (method.getName().equals("bye")) {// bye之后sayHappy
    result = method.invoke(target, args);
    interceptor.sayHappy();
    } else {
    result = method.invoke(target, args);
    }
    return result;
    }
    }
  4. 创建一个可以获取增强了功能之后对象的代理工厂,工厂的作用就是返回增强后的代理Proxy, 他的类型应该和目标对象的接口类型相同。

    1
    2
    3
    4
    5
    6
    7
    8
    public class ReceptorProxyFactory {
    public static Object getProxy(Object target) {
    ReceptorHandler handler = new ReceptorHandler();
    handler.setTarget(target);

    return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler);
    }
    }
  5. 编写测试类(可选)

    1
    2
    3
    4
    5
    6
    7
    8
    public class TestProxy {
    public static void main(String[] args) {
    Waiter target = new Waiter();
    Receptor receptor = (Receptor) ReceptorProxyFactory.getProxy(target);
    receptor.welcome();
    receptor.bye();
    }
    }

具体的示例源码,移步 Github-helloJavaSE

执行结果

1
2
3
4
5
6
您好!
欢迎光临!
谢谢惠顾!
祝您愉快!

Process finished with exit code 0

注意: JDK动态代理只能代理接口类型的对象,并且返回接口类型。如果要增强那些没有接口的对象,JDK动态代理不能实现,要依赖 CGLIB 技术。

Android转场动画

  • startActivity()

    1
    2
    3
    	Intent intent = new Intent(MainActivity.this,OtherActivity.class);
    startActivity(intent);
    overridePendingTransition(R.anim.push_up_in,R.anim.push_up_out);
  • ActivityCompat

1
ActivityCompat.startActivity((Activity) context, intent, null);
1
2
3
4
5
6
7
public static void startActivity(Activity activity, Intent intent, @Nullable Bundle options) {
if (Build.VERSION.SDK_INT >= 16) {
ActivityCompatJB.startActivity(activity, intent, options);
} else {
activity.startActivity(intent);
}
}
  • Android 4.1(API16)以下转场动画
1
2
3
AIntent intent = new Intent(MainActivity.this,OtherActivity.class);
startActivity(intent);
overridePendingTransition(R.anim.push_up_in,R.anim.push_up_out);
  • Android 4.1(API16)以上转场动画

    • ActivityOptions
      • Android 4.1(API16)提供了一个新类ActivityOptions,用来实现Activity的切换动画。
    • ActivityOptions常用使用

      • makeClipRevealAnimation(View source, int startX, int startY, int width, int height)

        1
        2
        3
        Added in API level 23
        ````
        - makeCustomAnimation(Context context, int enterResId, int exitResId)

        Added in API level 16

        1
        - makeScaleUpAnimation(View source, int startX, int startY, int width, int height)

        Added in API level 16

        1
        - makeSceneTransitionAnimation(Activity activity, Pair...<View, String> sharedElements)

        Added in API level 21

        1
        - makeSceneTransitionAnimation(Activity activity, View sharedElement, String sharedElementName)

        Added in API level 21

        1
        - makeTaskLaunchBehind()

        Added in API level 21

        1
        - makeThumbnailScaleUpAnimation(View source, Bitmap thumbnail, int startX, int startY)

        Added in API level 16

        1
        2
        3
        	
        - ActivityOptions兼容包:ActivityOptionsCompat
        - 自定义动画:makeCustomAnimation

      public static ActivityOptions makeCustomAnimation(Context context,

      int enterResId, int exitResId) {
      

      return makeCustomAnimation(context, enterResId, exitResId, null, null);

      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      		
      enterResId:开启的动画
      exitResId:退出的动画

      - makeThumbnailAspectScaleDownAnimation
      - makeClipRevealAnimation
      - Android API19以上使用
      - makeScaleUpAnimation
      - 放大动画,从指定view的指定位置开始放大
      - makeSceneTransitionAnimation
      - 共享view放大效果,仅支持API>=21
      - 底层实现动画:什么动画实现?

      ActivityTransitionState

      1
      	

      public void startExitOutTransition(Activity activity, Bundle options) {
      if (!activity.getWindow().hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)) {

      return;
      

      }
      ActivityOptions activityOptions = new ActivityOptions(options);
      mEnterTransitionCoordinator = null;
      //是否转场动画
      if (activityOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) {

      int key = activityOptions.getExitCoordinatorKey();
      int index = mExitTransitionCoordinators.indexOfKey(key);
      if (index >= 0) {
          mCalledExitCoordinator = mExitTransitionCoordinators.valueAt(index).get();
          mExitTransitionCoordinators.removeAt(index);
          if (mCalledExitCoordinator != null) {
              mExitingFrom = mCalledExitCoordinator.getAcceptedNames();
              mExitingTo = mCalledExitCoordinator.getMappedNames();
              mExitingToView = mCalledExitCoordinator.copyMappedViews();
              mCalledExitCoordinator.startExit();
          }
      }
      

      }
      }

      1
      2
      3
      4
      5
      	- Transition解析
      - [Android最新动画框架完全解析(二)——Transitions Framework](http://blog.csdn.net/l664675249/article/details/50195847#t0)
      - [Android最新动画框架完全解析(一)—— Animator(Property Animation)](http://blog.csdn.net/l664675249/article/details/50204503)
      - FragmentTransaction动画
      - setCustomAnimations(int enter, int exit, int popEnter, int popExit)

      Added in API level 13

      1
      - setCustomAnimations(int enter, int exit)

      Added in API level 11

      1
      - setTransition(int transit)

      Added in API level 1
      TRANSIT_NONE, TRANSIT_FRAGMENT_OPEN, or TRANSIT_FRAGMENT_CLOSE

      1
      - 底层源码:什么动画实现?

      BackStackState

      1
      	

      public void run() {
      if (FragmentManagerImpl.DEBUG) Log.v(TAG, “Run: “ + this);

      if (mAddToBackStack) {

      if (mIndex < 0) {
          throw new IllegalStateException("addToBackStack() called after commit()");
      }
      

      }

      bumpBackStackNesting(1);

      TransitionState state = null;
      SparseArray firstOutFragments = null;
      SparseArray lastInFragments = null;
      if (SUPPORTS_TRANSITIONS && mManager.mCurState >= Fragment.CREATED) {

      firstOutFragments = new SparseArray<Fragment>();
      lastInFragments = new SparseArray<Fragment>();
      
      calculateFragments(firstOutFragments, lastInFragments);
      
      state = beginTransition(firstOutFragments, lastInFragments, false);
      

      }

      int transitionStyle = state != null ? 0 : mTransitionStyle;
      int transition = state != null ? 0 : mTransition;
      Op op = mHead;
      while (op != null) {

      int enterAnim = state != null ? 0 : op.enterAnim;
      int exitAnim = state != null ? 0 : op.exitAnim;
      switch (op.cmd) {
          case OP_ADD: {
              Fragment f = op.fragment;
              f.mNextAnim = enterAnim;
              mManager.addFragment(f, false);
          } break;
          case OP_REPLACE: {
              Fragment f = op.fragment;
              int containerId = f.mContainerId;
              if (mManager.mAdded != null) {
                  for (int i = mManager.mAdded.size() - 1; i >= 0; i--) {
                      Fragment old = mManager.mAdded.get(i);
                      if (FragmentManagerImpl.DEBUG) Log.v(TAG,
                              "OP_REPLACE: adding=" + f + " old=" + old);
                      if (old.mContainerId == containerId) {
                          if (old == f) {
                              op.fragment = f = null;
                          } else {
                              if (op.removed == null) {
                                  op.removed = new ArrayList<Fragment>();
                              }
                              op.removed.add(old);
                              old.mNextAnim = exitAnim;
                              if (mAddToBackStack) {
                                  old.mBackStackNesting += 1;
                                  if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of "
                                          + old + " to " + old.mBackStackNesting);
                              }
                              mManager.removeFragment(old, transition, transitionStyle);
                          }
                      }
                  }
              }
              if (f != null) {
                  f.mNextAnim = enterAnim;
                  mManager.addFragment(f, false);
              }
          } break;
          case OP_REMOVE: {
              Fragment f = op.fragment;
              f.mNextAnim = exitAnim;
              mManager.removeFragment(f, transition, transitionStyle);
          } break;
          case OP_HIDE: {
              Fragment f = op.fragment;
              f.mNextAnim = exitAnim;
              mManager.hideFragment(f, transition, transitionStyle);
          } break;
          case OP_SHOW: {
              Fragment f = op.fragment;
              f.mNextAnim = enterAnim;
              mManager.showFragment(f, transition, transitionStyle);
          } break;
          case OP_DETACH: {
              Fragment f = op.fragment;
              f.mNextAnim = exitAnim;
              mManager.detachFragment(f, transition, transitionStyle);
          } break;
          case OP_ATTACH: {
              Fragment f = op.fragment;
              f.mNextAnim = enterAnim;
              mManager.attachFragment(f, transition, transitionStyle);
          } break;
          default: {
              throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
          }
      }
      op = op.next;
      

      }

      mManager.moveToState(mManager.mCurState, transition, transitionStyle, true);

      if (mAddToBackStack) {

      mManager.addBackStackState(this);
      

      }
      }
      ````

    • 为什么不建议用FragmentTransaction动画
    • Android动画

移动周分享-第56期

RunTime简介及实践 - 刘康

前言

今年年初的面试中,基本上超过了60%的公司会问RunTime的一些用法。
在很多开源项目中用到了RunTime的方法,使我们理解起来比较吃力。
RunTime确实很强大,借助它可以实现很酷的功能。比如JSPatch
我本身对于RunTime的理解也没有很深入,所以这里讨论的主要是RunTime的具体应用场景。此番探索希望能起到抛砖引玉的作用,大家可以将RunTime应用起来。

简介

  • RunTime简称运行时。是一套底层的C语言API,包含很多强大实用的C语言数据类型和C语言函数,平时我们编写的ObjC代码,底层都是基于runtime实现的。ObjC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ***********发送消息***********
Person* p = [[Person alloc] init];

// 调用对象方法
[p eat];
// 本质是向对象发送消息: objc_msgSend(p, @selector(eat));

// 调用类方法的方式有两种
// 第一种通过类名调用
[Person eat];
// 第二种通过类对象调用
[[Person class] eat];
// 用类名调用类方法,底层会自动把类名转换成类对象调用
// 本质:让类对象发送消息:objc_msgSend([Person class], @selector(eat));
  • 对于C语言,函数的调用在编译的时候会决定调用哪个函数。
  • 对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
  • 事实证明:
    • 在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。
    • 在编译阶段,C语言调用未实现的函数就会报错。

实践

一. 方法交换
  • 开发使用场景:系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。
    • 方式一:继承系统的类,重写方法.
    • 方式二:使用runtime,交换方法.

例如,有需求如下:
给imageNamed方法提供功能,每次加载图片就判断图片是否加载成功。

runtime实现思路:

  1. 写一个UIImage分类,在分类中定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
  2. 交换imageNamed和imageWithName的实现,就能调用imageWithName,间接调用imageWithName的实现。
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
@implementation UIImage (image)

+ (void)load
{
// 交换方法

// 获取imageWithName方法地址
Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));

// 获取imageWithName方法地址
Method imageName = class_getClassMethod(self, @selector(imageNamed:));

// 交换方法地址,相当于交换实现方式
method_exchangeImplementations(imageWithName, imageName);
}

// 既能加载图片又能打印
+ (instancetype)imageWithName:(NSString *)name
{
// 这里调用imageWithName,相当于调用imageName
UIImage *image = [self imageWithName:name];

if (image == nil) {
NSLog(@"加载空的图片");
}

return image;
}

@end
二. 动态添加方法
  • 开发使用场景:如果一个类,方法非常多,加载类到内存的时候也比较耗费资源,因为需要给每个方法生成映射表。可以使用动态给某个类添加方法解决。
  • 面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。

例如,给person类增加walk的功能, 默认person,没有实现walk方法,通过performSelector调用会报错。但当我动态添加了方法调用[p performSelector:@selector(walk)]就不会报错了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 默认方法都有两个隐式参数
void walk(id self, SEL sel)
{
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}

// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
// 这儿可以用来判断,未实现的方法是不是我们想要动态添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(walk)) {
// 动态添加walk方法

// 第一个参数:给哪个类添加方法
// 第二个参数:添加方法的方法编号
// 第三个参数:添加方法的函数实现(函数地址)
// 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
class_addMethod(self, sel, walk, "v@:");

}
return [super resolveInstanceMethod:sel];
}
三. 给分类添加属性
  • 原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。

例如:给NSObject添加一个name属性。
思路:在NSObject中,setName方法中设置关联值,name方法中通过取出关联值。达到添加属性的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义关联的key
static const char *key = "name";
@implementation NSObject (Property)
- (NSString *)name
{
// 根据关联的key,获取关联的值。
return objc_getAssociatedObject(self, key);
}

- (void)setName:(NSString *)name
{
// 第一个参数:给哪个对象添加关联
// 第二个参数:关联的key,通过这个key获取
// 第三个参数:关联的value
// 第四个参数:关联的策略
objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
四. 更便捷的archive/unarchive

需求:实现对象的归档和解档
思路:利用Runtime实现快速遍历对象的所有属性,减少大量类似于self.property=[aDecoder decodeObjectForKey:type];以及[aCoder encodeObject:self.property forKey:type]这种代码。

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
- (void)encodeWithCoder:(NSCoder *)encoder{
//归档存储自定义对象
unsigned int count = 0;
//获得指向该类所有属性的指针
objc_property_t *properties = class_copyPropertyList([Person class], &count);
for (int i =0; i < count; i ++) {
//获得
objc_property_t property = properties[i];
//根据objc_property_t获得其属性的名称--->C语言的字符串
const char *name = property_getName(property);
NSString *key = [NSString stringWithUTF8String:name];
// 编码每个属性,利用kVC取出每个属性对应的数值
[encoder encodeObject:[self valueForKeyPath:key] forKey:key];
}
}

- (instancetype)initWithCoder:(NSCoder *)decoder{
//归档存储自定义对象
unsigned int count = 0;
//获得指向该类所有属性的指针
objc_property_t *properties = class_copyPropertyList([Person class], &count);
for (int i =0; i < count; i ++) {
objc_property_t property = properties[i];
//根据objc_property_t获得其属性的名称--->C语言的字符串
const char *name = property_getName(property);
NSString *key = [NSString stringWithUTF8String:name];
//解码每个属性,利用kVC取出每个属性对应的数值
[self setValue:[decoder decodeObjectForKey:key] forKeyPath:key];
}
return self;
}
五. 字典转模型

需求:能不能自动根据一个字典,生成对应的属性。

  • 思路一:KVC。通过KVC方法setValuesForKeysWithDictionary可以进行转换。
    • 弊端:必须保证模型中的属性和字典中的key一一对应。
      • 如果不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]报key找不到的错。
      • 分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
      • 解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖, 就能继续使用KVC,字典转模型了。

思路二:RunTime。利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。

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
108
109
110
111
112
113
114
115
116
117
118
119
120
@implementation NSObject (Model)

+ (instancetype)modelWithDict:(NSDictionary *)dict
{
// 思路:遍历模型中所有属性-》使用运行时

// 0.创建对应的对象
id objc = [[self alloc] init];

// 1.利用runtime给对象中的成员属性赋值

// class_copyIvarList:获取类中的所有成员属性
// Ivar:成员属性的意思
// 第一个参数:表示获取哪个类中的成员属性
// 第二个参数:表示这个类有多少成员属性,传入一个Int变量地址,会自动给这个变量赋值
// 返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
/* 类似下面这种写法

Ivar ivar;
Ivar ivar1;
Ivar ivar2;
// 定义一个ivar的数组a
Ivar a[] = {ivar,ivar1,ivar2};

// 用一个Ivar *指针指向数组第一个元素
Ivar *ivarList = a;

// 根据指针访问数组第一个元素
ivarList[0];

*/

unsigned int count;

// 获取类中的所有成员属性
Ivar *ivarList = class_copyIvarList(self, &count);

for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员属性
Ivar ivar = ivarList[i];

// 获取成员属性名
NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];

// 处理成员属性名->字典中的key
// 从第一个角标开始截取
NSString *key = [name substringFromIndex:1];

// 根据成员属性名去字典中查找对应的value
id value = dict[key];

// 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型
// 判断下value是否是字典
if ([value isKindOfClass:[NSDictionary class]]) {
// 字典转模型
// 获取模型的类对象,调用modelWithDict
// 模型的类名已知,就是成员属性的类型

// 获取成员属性类型
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 生成的是这种@"@\"User\"" 类型 -》 @"User" 在OC字符串中 \" -> ",\是转义的意思,不占用字符
// 裁剪类型字符串
NSRange range = [type rangeOfString:@"\""];

type = [type substringFromIndex:range.location + range.length];

range = [type rangeOfString:@"\""];

// 裁剪到哪个角标,不包括当前角标
type = [type substringToIndex:range.location];

// 根据字符串类名生成类对象
Class modelClass = NSClassFromString(type);

if (modelClass) { // 有对应的模型才需要转

// 把字典转模型
value = [modelClass modelWithDict:value];
}

}

// 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
// 判断值是否是数组
if ([value isKindOfClass:[NSArray class]]) {
// 判断对应类有没有实现字典数组转模型数组的协议
if ([self respondsToSelector:@selector(arrayContainModelClass)]) {

// 转换成id类型,就能调用任何对象的方法
id idSelf = self;

// 获取数组中字典对应的模型
NSString *type = [idSelf arrayContainModelClass][key];

// 生成模型
Class classModel = NSClassFromString(type);
NSMutableArray *arrM = [NSMutableArray array];
// 遍历字典数组,生成模型数组
for (NSDictionary *dict in value) {
// 字典转模型
id model = [classModel modelWithDict:dict];
[arrM addObject:model];
}

// 把模型数组赋值给value
value = arrM;

}
}

if (value) { // 有值,才需要给模型的属性赋值
// 利用KVC给模型中的属性赋值
[objc setValue:value forKey:key];
}

}

return objc;
}

@end

Swift Runtime

  • 纯Swift的类,不能通过runtime获取到属性与方法。比如tuple:纯Swift类的函数调用已经不再是OC的运行时发消息objc_msgsend,而是类似C++的vtable,在编译时就确定了调用什么函数,所以runtime获取不到。
  • 继承于NSObject的类依然拥有动态性,所以可以拿的到。
  • @objc
    • @objc是用来将Swift的API导出给OC与OC runtime使用的,如果你继承NSObject的类,将会被自动的加入这个标识。
  • dynamic
    • 加了@objc标识的方法、属性都无法保证都会被运行时调用,因为Swift会做静态优化。要想完全被动态调用就要使用dynamic修饰词了。使用这个标识也会隐形的加入@objc。这也就解释了为什么上边VC中的方法无法被替换了,被Swift优化成静态调用了,而ViewDidAppear本身为OC的方法,拥有动态特性,所以我们加入dynamic关键字

小结

  • Runtime是运行时特性 ,方法调用的本质是给对象发消息:objc_msgSend
  • class_getClassMethod: 获取类方法地址
  • class_getInstanceMethod: 获取实例方法地址
  • method_exchangeImplementations:交换方法的实现
  • + (BOOL)resolveInstanceMethod:(SEL)sel: 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
  • class_addMethod:给实例添加方法
  • objc_setAssociatedObject:给对象设置关联值
  • objc_getAssociatedObject:获取对象的关联值
  • class_copyPropertyList:获得指向该类所有属性的指针
  • property_getName:取出属性指针对应的名称,返回字符串(char*)
  • class_copyIvarList:获取指向该类所有成员属性指针
  • ivar_getName:取出成员属性对应的名称

参考

Runtime简介: http://www.jianshu.com/p/94657b7d31d0
Swift Runtime: http://www.jianshu.com/p/9c36a5b7820a
MJExtension: https://github.com/CoderMJLee/MJExtension

SourceTree rebase 用法 - 曾铭

push with --rebase

push 前调整本地的 commits tree

  • Add some commits append to the branch
  • Right-click on a commit and hit Rebase children of <commit-id> interactively...
  • Try reorder or squash or edit message or delete
  • Click OK
  • Enjoy the history tree

注意:

  • 仅适用于 local branch 还没有 push 的情况!!!
  • push 时不要使用 -f 参数

参考

附带一提

  • 对于同一个 repo, 本地 clone 一份即可, 用 branchs 切换不同状态. 否则每个 repo 都要 sync( pull&push ) 很麻烦
  • 控制写代码的节奏, 半小时整理思路, 小步提交(起来走走喝杯水), 善用 amend last commitrebase, 便于 reviewer 理解

Firebase - 王胜

Firebase是一个帮助你快速开发高质量的应用程序,增强你的用户基础,并提高盈利能力的移动开发平台。Firebase有很多互补性的功能组成,你可以根据具体需求,找到适用的功能模块搭配使用。

功能介绍

总体功能预览:

all-feature-preview

分析功能

Firebase的核心功能是Firebases分析,一个免费且没有限制的分析解决方案。通过一个统一的Dashboard看板,可以查看用户的行为以及属性的定量分析。同时支持iOS和Android平台:

  • 最多可以无限制报告500个时间类型,每一个时间类型可支持25个属性
  • 统一的dashbaord查看用户行为以及跨网络性能分析
  • 人口分布,包括年龄、性别以及位置信息等
  • 提供可导出的BigQuery自定义查询

Develop

构建更好的应用程序,并预留接口给开发者。节省关键的开发时间,产出高质量、无bug的应用程序。

  • 云端消息
    提供可靠的跨平台的消息分发和接受通道
  • 认证
    提供健壮的认证机制
  • 实时数据
    实时存储和同步应用数据
  • 存储
    便捷的文件存储
  • 寄主
    快速分发网络内容
  • 远程配置
    可远程自定义应用的配置信息
  • 测试Lab
    提供云端测试功能
  • Crash报告
    保证应用的稳定性

Grow

在恰当的时机,培养并吸引合适的用户。促进潜在用户的增长。

  • 通知
    恰当的时机吸引用户
  • App索引
    驱动有效的搜索流量到你的App
  • 动态链接
    应用内发送动态链接,引导用户到正确的地方
  • 邀请
    引导用户分享你的应用程序
  • AdWords广告
    通过Google搜索获取用户

Earn

通过向全球用户展示吸引的广告赚取到收入。

  • AdMob
    通过吸引性的广告盈利

开发使用

前置条件

  • 一个运行Google Play服务9.0.0以上版本的Android设备
  • 通过 Android SDK Manager 获取Google Play services
  • Android Studio 1.5或者更高版本
  • 一个Android Studio项目和包名

注意事项:
Android Studio低于2.2版本中的Instant Run存在Firebase Analytics和特定事件保护不兼容问题,官方建议禁用 Instant run或者升级Android Studio到2.2预览版。

创建Firebase项目

  • 访问 控制台,创建一个Firebase项目。
    firebase-create-projec
  • 添加Firebase到应用程序
    选择项目 -> 项目设置 -> 将Firebase添加到您的Android/iOS/网页应用,按照向导一步一步完成配置。
    firebase-console-dashboard
    firebase-add-to-app
  • 选择需要的功能模块,加入项目中
    firebase-feature-libs-list

具体的开发文档,参见官网

移动周分享-第55期

Android MVVM开发模式 - 王胜

MVVM(Model-View-ViewModel),它最初是在2005年由微软提出的一个被证明可用的概念。在Google I/O 2015上,伴随着Android M预览版发布的Data Binding使Android原生开发也支持了MVVM框架。

mvvm-workflow

下面还是通过天气的示例来演示MVVM框架的运用,效果如图:
weather-app-screenshot

目录结构

android-mvvm

开启Data Binding功能

Android Studio中,在需要开启Data Binding功能的Module中的build.gradle文件中加入以下内容

1
2
3
4
// 开启Data Binding功能
dataBinding {
enabled true
}

Model层 Weather

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 Weather {
private String date;//日期
private String textDay;//白天天气
private String textNight;//夜晚天气
private String high;//最高气温
private String low;//最低气温

public String getDisplay() {
StringBuilder sb = new StringBuilder();
sb.append(date);
sb.append("\t");
sb.append("白天:");
sb.append(textDay);
sb.append(", ");
sb.append("夜晚:");
sb.append(textNight);
sb.append(", ");
sb.append("最高气温:");
sb.append(high);
sb.append(", ");
sb.append("最低气温:");
sb.append(low);
return sb.toString();
}

// setter and getter
......
}

View层

  • activity_weather_mvvm.xml

    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
      <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textStyle="bold"
    android:textSize="22sp"
    android:layout_marginLeft="@dimen/activity_horizontal_margin"
    android:text="上海未来3天的天气"/>

    <ListView
    android:id="@+id/lv"
    android:layout_marginTop="@dimen/activity_horizontal_margin"
    android:visibility="gone"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    <LinearLayout
    android:id="@+id/ll_loading"
    android:layout_marginTop="100dp"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">


    <ProgressBar
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    style="?android:attr/progressBarStyleLarge"
    android:layout_gravity="center_horizontal"/>

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:textSize="20sp"
    android:text="@string/loading" />

    </LinearLayout>
    </LinearLayout>
    </layout>
  • WeatherActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class WeatherActivity extends AppCompatActivity implements WeatherViewModel.DataListener {
    private ActivityWeatherMvvmBinding binding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtil.setContentView(this, R.layout.activity_weather_mvvm);
    new WeatherViewModel(this).getWeatherList();
    }

    @Override
    public void onWeatherChanged(boolean occurError, List<Weather> data) {
    binding.llLoading.setVisibility(View.GONE);
    if (occurError) {
    Toast.makeText(WeatherActivity.this, "Get data failed!", Toast.LENGTH_LONG).show();
    } else {
    binding.lv.setAdapter(new WeatherAdapter(data));
    binding.lv.setVisibility(View.VISIBLE);
    }
    }
    }
  • weather_item.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
    <variable name="weather" type="org.freedom.androidpatterndemo.mvvm.model.Weather"/>
    </data>
    <TextView
    android:id="@+id/tv_weather"
    android:paddingLeft="16dp"
    android:paddingRight="16dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textSize="18sp"
    android:text="@{weather.getDisplay}"/>

    </layout>
  • WeatherAdapter

    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
    public class WeatherAdapter extends BaseAdapter {
    private List<Weather> data;

    public WeatherAdapter(List<Weather> data) {
    this.data = data;
    }

    @Override
    public int getCount() {
    return data == null ? 0 : data.size();
    }

    @Override
    public Weather getItem(int position) {
    return data.get(position);
    }

    @Override
    public long getItemId(int id) {
    return id;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup viewGroup) {
    ViewHolder holder;
    if (convertView == null) {
    holder = new ViewHolder((WeatherItemBinding) DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()),
    R.layout.weather_item, viewGroup, false));
    convertView = holder.binding.tvWeather;
    convertView.setTag(holder);
    } else {
    holder = (ViewHolder) convertView.getTag();
    }
    holder.binding.setWeather(getItem(position));
    return convertView;
    }

    class ViewHolder {
    WeatherItemBinding binding;
    public ViewHolder(WeatherItemBinding binding) {
    this.binding = binding;
    }
    }
    }
  • WeatherViewModer

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
/**
* View model for WeatherActivity
*
* 从维基百科文章中可以看到:
*
* view model是一个抽象的view,它对外暴露公有的属性和命令。
*
* Created by wangsheng on 16/5/12.
*/

public class WeatherViewModel {

private DataListener dataListener;

public WeatherViewModel(DataListener dataListener) {
this.dataListener = dataListener;
}

/**
* 通过网络请求获取天气列表数据
*/

public void getWeatherList() {
HttpEngine.get(Constants.API_WEATHER, new HttpEngine.JSONRequestCallback() {
@Override
public void onFailed(Call call, Exception e) {
if (call.request().url().url().toString().equals(Constants.API_WEATHER)) {
if (dataListener != null) {
dataListener.onWeatherChanged(true, null);
}
}
}

@Override
public void onSuccess(Call call, JSONObject jsonResult) {
if (call.request().url().url().toString().equals(Constants.API_WEATHER)) {
try {
JSONArray jsonArray = jsonResult.getJSONArray("results").getJSONObject(0).getJSONArray("daily");
Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
List<Weather> data = gson.fromJson(jsonArray.toString(), new TypeToken<List<Weather>>(){}.getType());
if (dataListener != null) {
dataListener.onWeatherChanged(false, data);
}
} catch (JSONException e) {
e.printStackTrace();
if (dataListener != null) {
dataListener.onWeatherChanged(true, null);
}
}
}
}
});
}

public interface DataListener {
void onWeatherChanged(boolean occurError, List<Weather> data);
}
}

总结

通过上面可以看出,ViewModer代替了原来的Presenter,不过相比于MVP,MVVM的Data Binding框架做了数据和View的自动绑定功能,省去了手动findViewById以及拿到组件后set数据的繁琐步骤。

从计算机为什么用补码存储数据,衍生到存储单元数据溢出 - 李仙鹏

引言

先说几句屁话,觉得啰嗦可以忽略跳过这段屁话。
俗话说:眼看他起高楼,眼看他宴宾客,眼看他楼塌了。我想这句话放在我们做技术的,也很合适——基础不牢,地动山摇。
尽管我们很多人不是做基础开发的,但是操作系统、数据结构和算法、计算机网络、设计模式……这些IT领域的基础性学科,对于我们来说其实挺重要的,比如做优化、跨语言学习、工程框架搭建……。基础虽然不能决定眼前,但是能够决定我们在这条路上走多远。框架那么多、特效那么多,真心没有兴趣一个个都摸一遍,所以偶尔回过头去翻看这些基础性的东西是挺有意思的。

  • 是否还记得在学操作系统的时候,很困惑计算机为什么要用补码存储数据,而不是用我们人更容易理解的原码来进行存储呢?

关于这个问题,相信很多教这门课的老师以及工作多年的coder也解释不清,甚至不知道这个概念。

  • 本文将尝试从理性结合感性的角度去说明计算机为什么用补码存储数据,当我们明白这个问题后,那么,我们就可以去理解另一个衍生问题——数据溢出。我们先来看一段关于数据溢出的Java代码片:
1
2
3
4
5
6
7
/*char为无符号数,16位存储,表示范围是0~2^16-1(即0~65535)*/
char ch = Character.MAX_VALUE; // ch为65535
ch += (char) 1; // 加1后,引起数据溢出,则ch为0

/*int位有符号数,32位存储,表示范围是-2^31~2^31-1(即-2147483648~2147483647)*/
int i = Integer.MAX_VALUE; // i为2147483647
i += 1; // 加1后,引起数据溢出,则i为-2147483648
  • 至于上述代码片的执行结果为什么会这样,暂时不解释,希望通过文章循序渐进的过程来说明溢出的问题。

fuck概念

  • 计算机用二进制来表示数据,这个大家应该都了解(不了解的找块板砖拍死自己算了)。
  • 没有特殊说明,本文都以4位存储单元来说明
  • 下面几个小节会提到一些关键概念,不要对这些概念恐慌,这些概念会结合例子或者对比的形式,尽量以通俗简洁的文字来说明,保证人人都能看的懂

加法器

  • 计算机只有加法器没有减法器,两个数的减法运算会被计算机转换为加法运算。(先埋个伏笔——通过补码进行表示,即可将减法运算转换为加法运算)

模、补数

  • 在日常生活中,有许多化减为加点例子。我们以最平常的钟表为例,时针逆时针拨x(0<x<12)格和时针顺时针拨12-x格,效果是相同的。比如,时针从10点调整到5点有以下两种方法:

    1. 时针逆时针拨5格,相当于做减法:10 -5 = 5
    2. 时针顺时针拨7(即12 - 5)格,相当于做加法:10 + 7 = 12 +5 = 5(MOD=12)
  • 总结,x + (MOD - x) = MOD就是模,x和MOD - x就是一对“互补”的数,即原数x的补数为MOD - x或者原数MOD - x的补数为x。通过对钟表拨时针的例子可以发现,用补数(7)代替原数(5),可把减法转变为加法(出现的进位就是模,进位舍弃)。

二进制数的模,先来看下两个个例子(此处我们忽略符号):

  1. 2位存储所能表示的最大数是11(10进制:3 = 2^2 - 1),比他大1的是11 + 1 = 100(10进制:4 = 2^2),那么这个100则是2位存储所能表示的所有数据的模。
  2. 4位存储所能表示的最大数是1111(10进制:15 = 2^4 - 1),比他大1的数是1111 + 1 = 10000(10进制:16 = 2^4),那么这个10000则是4位存储所能表示的所有数据的模。

通过对上面两个例子可以推论:一个二进制数的最高位位数用n表示,那么该二进制数的模就是2^n

原码、反码、补码

  • 先来看一张国内外教材对比的表(出自《计算机教育》2015年第10期的文章——《原码、反码和补码的教学讨论》)

国内外教材对比.png

  • 给定一个有符号数x,来对比下国外和国内教材对原码、反码、补码的表示:

    • 国外教材
      • sign and magnitude representation(原码):最高位位符号位(0表示正数,1表示负数),剩余位(数据位)为x的大小。
      • ones’ complement representation(反码):如果x为正数,则是其二进制表示;如果x为负数,则是其对应正数的bit complement/bitwise NOT(按位取反)——执行每一位逻辑否定的一元操作。可用公式表示为:
        • [x]反 = (2^n - 1) - |X|(其中n为将符号位算在内的位数,|X|为绝对值)
      • two’s complement representation(补码):如果x为正数,则是其二进制表示;如果x为负数,则是其对应正数的二的补(所有位取反后加1)。可用公式表示为:
        • [x]补 = (2^n) - |X| = [x]反 + 1(其中n为将符号位算在内的位数,|X|为绝对值)
    • 国内教材
      • 原码:最高位为符号位,剩余位(数据位)为x的绝对值。
      • 反码:如果x为正数,则与原码相同;如果x为负数,符号位保持不变,数据位取反。
      • 补码:如果x为正数,则与原码相同;如果x为负数,符号位保持不变,数据位取反,然后加1(若符号位有进位,则舍弃进位)。
  • 对比国内外教材的表述,是否发现高下立现:

    • 国内教材画蛇添足,并且容易引起误解:
      1. 原码是反码和补码的基础,反码和补码由原码转化而来
      2. 原码、反码和补码的符号位相同
    • 国外教材,则非常通俗:
      1. 求解一个数的反码和补码,根本不需要知道原码,直接通过它们的两个对应公式即可,甚至可以说原码与反码和补码没有半毛钱关系,反倒是反码和补码存在关系——补码 = 反码 + 1
      2. 原码的出发点是符号的表示(符号位),即用0表示正数,用1表示负数;反码和补码的出发点是减法的运算,即用两个正数的加法取代两个数的减法

狗日的国内教材和翻译,真是误人子弟啊

计算机为什么用补码存储数据

  • 上面铺垫了这门久,终于要进入第一个正题——计算机为什么用补码存储数据。为了不引起混淆,我们就以国外教材对于原码、反码和补码的表示法来进行说明。简单起见,以4位存储表示有符号数为例,通过原码、反码和补码的表示法来生成一张表:
有符号数(十进制) sign and magnitude representation(原码) ones’ complement representation(反码),[x]反 = (2^n - 1) - \X\ two’s complement representation(补码),[x]补 = (2^n) - \X\
+7 0111 表示方式不变 表示方式不变
+6 0110 表示方式不变 表示方式不变
+5 0101 表示方式不变 表示方式不变
+4 0100 表示方式不变 表示方式不变
+3 0011 表示方式不变 表示方式不变
+2 0010 表示方式不变 表示方式不变
+1 0001 表示方式不变 表示方式不变
+0 0000 表示方式不变 表示方式不变
-0 1000 1111 0000(求解过程:[x]补 = 2^n - \x\ = 2^4 - \-0\ = 2^4 - (+0),使用二进制则为10000 - 0000 = 10000,超过4位(有进位),那么舍弃进位1,最终结果就是0000)
-1 1001 1110 1111
-2 1010 1101 1110
-3 1011 1100 1101
-4 1100 1011 1100
-5 1101 1010 1011
-6 1110 1001 1010
-7 1111 1000 1001
-8 超出4个bit所能表达范围 超出4个bit所能表达范围 1000
备注 零重码,二进制存在两种表示方法:0000和1000 零重码,二进制存在两种表示方法:0000和1111 零无重码,同时解决了原码和反码不能表示-8的问题
  • 通过上述表格,可以很自然的总结出一个结论:补码表示法(two’s complement representation)可以防止0的机器数重码,同时又解决了原码和反码无法表示-8的问题,这样就极大的简化了计算机的硬件设计。

  • 结合之前提到的时钟例子,我们把补码表示法(two’s complement representation)所表示的四位存储单元,按照从0000到1111递增的方式,均匀的分布在时钟的表盘上。于是,我们就可以得到下面这张图(图片来自于这里):

two's complement wheel

  • OK,继续以时钟的方式来观察上图:

    • 顺时针方向位加法,逆时针方向为减法
    • 模为2^n:在1111处顺时针拨一格,就到了0000。用数学的方式,即1111 + 1 = 10000,进位舍弃则结果为0000,那么四位存储的模就是10000(2^4)
    • 减法转换为加法:3 - 1 = 3 + (-1) = 0011 + 1111 = 0010,眼尖的人可能会说0011 + 1111明明等于10010,怎么会是0010?还记得之前提过的最高位进位舍弃嘛,因此对于4位存储来说,进位舍弃后就是0010 = 2。

      若减法不转换为加法,那么3 -1 = 0011 - 0001 = 0010 = 2

    • 数据溢出:当0111(7)加1时,按照我们人的思维来说,应该结果为8,但是对于机器来说则不是,0111(7)是四位存储所能表示的最大数,它是无法表示01111(8)的,这个时候我们就说数据溢出了。那么数据溢出该怎么办呢?很简单,机器的思考方式显然和我们人脑不一样,机器按照上面环形图的方式,由于0111(7)加1是顺时针造成的数据溢出,那么我们可以把机器的操作想象成在0111(7)处顺时针拨了一格,我们再去对照下环形图发现这时候指向了1000(-8)。

      把这个过程想象成拨时针就OK了,对于1000(-8)减1也是同样道理

  • 至此,我们完全可以总结一下,并解答计算机为什么用补码存储数据:

    1. 计算机只有加法器没有减法器,两个数的减法运算会被计算机转换为加法运算,而补码正好能够解决减法转换为加法的问题
    2. 防止机器发生零重码,同时解决了原码和反码不能表示-8的问题,这样极大的简化了计算机的硬件设计
    3. 以循环的方式解决数据溢出的问题

从补码的角度解答代码片中的数据溢出

  • 既然已经知道了计算机为什么用补码存储数据,那我们就可以回过头去消灭文章开头的数据溢出的代码片了。由于代码片中ch和i的问题是一样的,那我们就选择ch来进行分析,另一个留给你们分析。

  • 在Java中,char为无符号数,16位存储,表示范围是0~2^16-1(即0~65535)。

    1. 首先,我们按照0000 0000 0000 0000到1111 1111 1111 1111递增的方式,均匀的分布在时钟的表盘上,图就不画了,自己在脑中想象一下或者画个草稿。
    2. 然后,找出数据溢出点,通过观察char环形图可以发现数据溢出点是0(0000 0000 0000 0000)和65535(1111 1111 1111 1111)
    3. 最后,我们的ch = 65535 + 1,那么很显然发生了数据溢出,按照拨时针的方式就可以得出ch = 0
  • Perfect,是否解答了当初学操作系统和编程的时候,困扰你们很久的问题。送给大家一句话:有些概念可能当时不理解,但是随着经验多累积和回顾的多了,自然而然就理解了。

贴出我看的关于补码的文章链接,有几篇中文文章对于某些知识点可能说错了,切记要带着批判的观点去看:

移动周分享-第54期

自动化 ssh 授权笔记 - 曾铭

每次 ssh user@host 登录目标机器都要输密码是件很烦的事,特别是经常访问多台主机的情况。 最近写自动化脚本时碰到要自动做机器间 ssh 验证,碰到一些问题记录下来备忘。

分析

  • 通过 ssh-keygen -t rsa -b 4096 -C "your_email@example.com" 命令生成本地机器的私钥和公钥: ~/.ssh/id_rsa and ~/.ssh/id_rsa.pub
  • 授权就是将将本地机器的公钥加入到目标机器的 ~/.ssh/authorized_keys 中供验证用
  • 本地 ~/.ssh/known_hosts 中信任目标机器的公钥指纹

自动做 ssh-key 授权

1
2
3
4
# copy local ssh-key to remote 对应分析中的第二步
cat ~/.ssh/id_rsa.pub | ssh root@192.168.37.110 "cat >> ~/.ssh/authorized_keys"
# add host to known_hosts 对应分析中的第三步
ssh-keyscan -t rsa "192.168.37.110" >> ~/.ssh/known_hosts

或者这两条命令可以精简为如下一条

1
2
brew install ssh-copy-id # just for mac
ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.37.110

还有剩下的一个难题是还需要手动输入密码,这个怎么自动化呢?

1
2
3
# just for mac
brew install https://raw.githubusercontent.com/kadwanev/bigboybrew/master/Library/Formula/sshpass.rb
sshpass -p "PASSWORD" ssh-copy-id -i ~/.ssh/id_rsa.pub -o StrictHostKeyChecking=no root@192.168.37.110

或者使用 Fabric 等远程执行 run(“cmd”)

1
2
# tips 执行 sudo 不需要手动输入密码
echo "PASSWORD" | sudo -S CMD

用 iterm2 做 terminal 管理

参考:

枚举的特殊用法 - 杨志平

源于项目中的应用太广了,但是对swift的很多简单特性不熟。之前那个动画的阶段使用到enum有遇到困难暂时绕开它了

当时的目标就是外部直接赋值操作

如下动画的enum示例:

为了省事,目前直接使用死的文案放在这里

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
enum AnimationPeriod: UInt {
// 动画执行的五个阶段
case Start,First,Second,Third,End

func description() -> String {
switch self {
case .Start, .First: return "正在提取学校最新\n录取条件"
case .Second: return "正在与学校进行匹配"
case .Third, .End: return "正在根据匹配结果\n生成选校方案"
}
}

func duration() -> NSTimeInterval {
switch self {
case .Start: return 0.8
case .First: return 1
case .Second: return 2
case .Third: return 0.5
case .End: return 0.25
}
}
}

extension AnimationPeriod {
mutating func next() {
switch self {
case .Start: self = .First
case .First: self = .Second
case .Second: self = .Third
case .Third: self = .End
default: self = .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
enum FamilyType {

case Father(age: Int)
case Mother(age: Int)
case Sister(age: Int)

func gift() -> String {
switch(self) {
case .Sister(let age):
if age > 15 {
return "iphone"
} else {
return "toy"
}
case .Mother(let age) where age < 40:
return "cloth"
default:
return "book"
}
}
}

let someone = FamilyType.Sister(age: 11)
let somebody = FamilyType.Mother(age: 40)
// swift的枚举技巧
let someoneGift = someone.gift() // print toy
let somebodyGift = somebody.gift() // book

那我们要取值的有

  • 方法一
1
2
3
4
5
6
7
switch someone {
case .Father(let age):
age
case .Sister(let age):
age
default:()
}
  • 方法二
1
2
3
if case .Sister(let age) = someone {
age
}

枚举的嵌套

1
2
3
4
5
6
7
8
9
10
11
enum Colleague {
enum Weight: Int {
case Light
case Mid
case Heavy
}
case iOS(weight: Weight)
case Android(weight: Weight)
case HTML(weight: Weight)
}
let woodenHelmet = Colleague.iOS(weight: .Mid)

枚举的成员变量

之前就一直想要使用枚举做到外部动态对枚举赋值储存操作,
那成员变量用法如下

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
// 根据自身赋值
enum Device {
case iPad, iPhone
var year: Int {
switch self {
case iPhone: return 2007
case iPad: return 2010
}
}
}

// set get 方法对于枚举的成员变量是无效的,允许get调用但set不可执行
enum Book {
case Story, News
var year: Int {
set{
year = newValue
}
get{
return self.year
}
}
}

let myIpad = Device.iPad
myIpad.year

var storyBook = Book.Story
//storyBook.year = 2010 // set 方法此处报错
//storyBook.year // get 无效

初始化,感觉然并软

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum AppleDevice {
case iMac(price:Int)
case iPod(price:Int)
case iPhone(price:Int)
init (costMoney: Int) {
if costMoney > 10000 {
self = .iMac(price: costMoney)
} else if costMoney > 2500 {
self = .iPhone(price: costMoney)
} else {
self = .iPod(price: costMoney)
}
}
}

let myDevice = AppleDevice(costMoney: 6000)

元组的使用

多个参数的介入时,可以使用元组,此处常规使用报错暂时无法解决,
使用func函数赋值倒是没有出错。why??

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum HumanHabit {
case Reading
case PlayGame
case Traveling
}
typealias HumanInfo = (age: Int, name: String, habit: HumanHabit)

func selectRAM(humanInfo: HumanInfo) -> HumanInfo {return (age: 32, name: humanInfo.name, habit: humanInfo.habit)}
func selectCPU(humanInfo: HumanInfo) -> HumanInfo {return (age: humanInfo.age, name: "3.2GHZ", habit: humanInfo.habit)}
func selectGPU(humanInfo: HumanInfo) -> HumanInfo {return (age: humanInfo.age, name: "3.2GHZ", habit: .Reading)}

enum Desktop {
case Cube(HumanInfo)
case Tower(HumanInfo)
case Rack(HumanInfo)
}

let aTower = Desktop.Tower(selectGPU(selectCPU(selectRAM((0, "", .Traveling) as HumanInfo))))

移动周分享-第53期

JSPatch初探 - 刘康

什么是JSPatch

JSPatch是一个开源项目,最初是腾讯@bang的个人项目,诞生于2015年5月。它能够使用JavaScript调用Objective-C的原生接口,从而动态植入代码来替换旧代码,以实现修复线上bug。

JSPatch平台

JSPatch 需要使用者有一个后台可以下发和管理脚本,并且需要处理传输安全等部署工作。如果自己搭建后台下发 JSPatch 脚本,可以直接使用 github 上的代码。JSPatch 平台提供了脚本后台托管,版本管理,保证传输安全等功能,无需搭建一个后台,无需关心部署操作,只需引入一个 SDK 即可立即使用 JSPatch。

JSPatch实现原理

基本原理

Objective-C是动态语言,具有运行时特性,该特性可通过类名称和方法名的字符串获取该类和该方法,并实例化和调用:

1
2
3
4
Class class = NSClassFromString(@"DataManager");
id dataManager = [[class alloc] init];
SEL selector = NSSelectorFromString(@"saveData");
[dataManager performSelector:selector];

也可以替换某个类的方法为新的实现:

1
2
static void mySaveData(id slf, SEL sel) { }
class_replaceMethod(class, selector, mySaveData, "");

新注册一个类,为类添加方法:

1
2
3
Class cls = objc_allocateClassPair([NSObject class], "MyObject", 0);
objc_registerClassPair(cls);
class_addMethod(cls, selector, implement, typedesc);
Javascript调用

可以用Javascript对象定义一个Objective-C类:

1
2
3
4
{
__isCls: 1,
__clsName: "UIView"
}

在OC执行JS脚本前,通过正则把所有方法调用都改成调用 __c() 函数,再执行这个JS脚本,做到了消息转发机制:

1
2
3
UIView.alloc().init()
->
UIView.__c('alloc')().__c('init')()
互传消息
  • JavaScriptCore: 通过此framework实现消息互传
  • JSContext: JS代码的执行环境。JS通过调用 JSContext 定义的方法把数据传给OC,OC通过返回值传会给JS。

集成步骤

通过JSPatch平台。

  • 创建App,获得AppKey
  • 下载SDK并导入到项目
  • 设置App Transport Security Settings
  • 添加依赖框架:libz.dylibJavaScriptCore.framework
  • 添加代码:
1
2
[JSPatch startWithAppKey:@"c08857f72a4e5f7b"];
[JSPatch sync]; //检查更新

如何使用

在 JSPatch 平台的规范里,JS脚本的文件名必须是 main.js
直接在main.js中添加JavaScript代码!
详细使用请见Github Wiki

测试

  • 在项目中添加main.js文件
  • 添加代码
  • didFinishLaunchingWithOptions添加一行:
1
[JSPatch testScriptInBundle];
  • Run!

在平台中添加JS脚本

  1. 在创建的应用中添加版本
  2. 在添加的版本中上传main.js
    上传后,对应版本的App会请求下载此 脚本并保存在本地,以后App每次启动都会执行。
    亲测第一次并不会执行!!!

部署安全策略

使用 JSPatch 有两个安全问题:

  • 传输安全:JS 脚本可以调用任意 OC 方法,权限非常大,若被中间人攻击替换代码,会造成较大的危害。
  • 执行安全:下发的 JS 脚本灵活度大,相当于一次小型更新,若未进行充分测试,可能会出现 crash 等情况对 APP 稳定性造成影响。

针对传输安全问题,作者的建议是对下发的JS代码进行RSA校验,而执行安全,除了在发布JS脚本之前做好周全测试之外,还需要建立回退机制,实现方式是后台下发命令,让 APP 在下次启动时不执行 JSPatch 脚本即可。但这里能回退的前提是 APP 可以接收到后台下发的回退命令,若因为下发的脚本导致 APP 启动即时 crash,这个回退命令也会接收不到。所以建议再加一层防启动 crash 的机制,APP 在连续启动即 crash 后,下次启动不再执行脚本文件。

Swift项目中使用JSPatch

使用 defineClass() 覆盖 Swift 类时,类名应为 项目名.原类名:

1
defineClass('demo.ViewController', {})

需要注意的是:

  1. 只支持调用继承自 NSObject 的 Swift 类;
  2. 继承自 NSObject 的 Swift 类,其继承自父类的方法和属性可以在 JS 调用,其他自定义方法和属性同样需要加 dynamic 关键字才行;
  3. 若方法的参数/属性类型为 Swift 特有的(如 Character / Tuple),则此方法和属性无法通过 JS 调用。

不会JavaScript?

JSPatch Convertor


参考:

JSPatch平台

Github Wiki

移动周分享-第52期

Swift 的 Playground 学习

我们一开始知道 Playground,也知道然后查看一些变量及变量的历史记录。偶尔还知道可以做视图甚至动画展示但是,现在,你特么看到的是 MarkDown 语法,有没有。

参考文献

使用优势

  • 快速学习swift
  • 快速测试代码效果
  • 验证API
  • 富文本注释

如下所有案例使用Xcode7下加载playground文件,可预览效果
文件下载链接

变量

至上而下执行,显示变量的当前值及历史记录的变化。

1
2
3
4
5
6
7
8
9
var name = "杨志"
name += "平"
// playground下预览

var graph = 0.0
for i in 0...100 {
graph = sin(Double(i)/10.0)
// playground下预览
}

UI视图

简单显示UI的基础元素

1
2
3
4
5
6
7
8
9
10
11
let redView = UIView(frame:CGRectMake(0,0,100,100))
redView.backgroundColor = UIColor.redColor()
redView.layer.cornerRadius = 20
redView.layer.borderWidth = 3
redView.layer.borderColor = UIColor.whiteColor().CGColor

let circle = UIView(frame:CGRectMake(25,25,50,50))
circle.backgroundColor = UIColor.yellowColor()
circle.layer.cornerRadius = 80
redView.addSubview(circle)
// playground下预览

重头戏

动画展示 & 网络请求

这里需要对 XCPlayground 有一点了解,好像还对SpriteKit做了支持文章连接

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
// 这个库需要import
import XCPlayground

let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
containerView.backgroundColor = UIColor.yellowColor()
XCPlaygroundPage.currentPage.liveView = containerView

let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
view.center = CGPoint(x: containerView.center.x, y: containerView.center.y-100)
view.backgroundColor = UIColor.redColor()
containerView.addSubview(view)

// playground下预览
UIView.animateWithDuration(3, delay: 0, usingSpringWithDamping: 0.1, initialSpringVelocity: 6, options: UIViewAnimationOptions.CurveEaseInOut, animations: {
view.center.y += 100
}, completion:{ (animation: Bool) in
view.center.y -= 100
}
)


// 异步操作:网络在家图片
// 旧接口:XCPSetExecutionShouldContinueIndefinitely(true)
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

let url = NSURL(string: "https://avatars1.githubusercontent.com/u/5317671?v=3&s=460")!
let task = NSURLSession.sharedSession().dataTaskWithURL(url) {
data, _, _ in
let image = UIImage(data: data!)
// playground下预览
}
task.resume()


// 快速检查API接口
let url2 = NSURL(string: "http://www.test.51offer.com/mobile/abroad/query_school_by_name?name=%E4%B8%80")!
let task2 = NSURLSession.sharedSession().dataTaskWithURL(url2) {
data, _, _ in
let str = String(NSString(data: data!, encoding: NSUTF8StringEncoding))
// playground下预览
}
task2.resume()

结构介绍

  • sources

    我们直接在 Playground 上面写代码,然后编译器会实时编译我们代码,并将结果显示出来。但是效率很低,source的作用就可以发挥出来

  • resources

    可以作为sandbox使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 直接使用PicButton 这个class.
// 注意点:PicButton的类及初始化方法必须是public的

let btn = PicButton(frame: CGRectMake(0,0,200,100))
// playground下预览


// 如上述:在resources中放入jpg图片,加载本地资源

if let path = NSBundle.mainBundle().pathForResource("swift-playground", ofType: "jpg") {
let image = UIImage(contentsOfFile:path)
let imageView = UIImageView(image: image)
// playground下预览
}

Android APP开发设计模式 - 王胜

MVC(Standard Android)

  • 目录结构

    android-mvc

    • Weather

      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 Weather {
      private String date;//日期
      private String textDay;//白天天气
      private String textNight;//夜晚天气
      private String high;//最高气温
      private String low;//最低气温

      public String getDisplay() {
      StringBuilder sb = new StringBuilder();
      sb.append(date);
      sb.append("\t");
      sb.append("白天:");
      sb.append(textDay);
      sb.append(", ");
      sb.append("夜晚:");
      sb.append(textNight);
      sb.append(", ");
      sb.append("最高气温:");
      sb.append(high);
      sb.append(", ");
      sb.append("最低气温:");
      sb.append(low);
      return sb.toString();
      }

      // setter and getter
      ......
      }
  • WeatherActivity

    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
      public class WeatherActivity extends AppCompatActivity {
    private ListView lv;
    private LinearLayout llLoading;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_weather);
    lv = (ListView) findViewById(R.id.lv);
    llLoading = (LinearLayout) findViewById(R.id.ll_loading);
    loadData();
    }

    private void loadData() {
    HttpEngine.get(Constants.API_WEATHER, new HttpEngine.JSONRequestCallback() {
    @Override
    public void onFailed(Call call, Exception e) {
    if (call.request().url().url().toString().equals(Constants.API_WEATHER)) {
    Log.e(WeatherActivity.class.getSimpleName(), e.getMessage());
    Toast.makeText(WeatherActivity.this, "Get data failed!", Toast.LENGTH_LONG).show();
    llLoading.setVisibility(View.GONE);
    }
    }

    @Override
    public void onSuccess(Call call, JSONObject jsonResult) {
    if (call.request().url().url().toString().equals(Constants.API_WEATHER)) {
    try {
    JSONArray jsonArray = jsonResult.getJSONArray("results").getJSONObject(0).getJSONArray("daily");
    Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
    final List<Weather> data = gson.fromJson(jsonArray.toString(), new TypeToken<List<Weather>>(){}.getType());
    WeatherAdapter adapter = new WeatherAdapter(WeatherActivity.this, data);
    lv.setAdapter(adapter);
    llLoading.setVisibility(View.GONE);
    lv.setVisibility(View.VISIBLE);
    } catch (JSONException e) {
    Log.e(WeatherActivity.class.getSimpleName(), e.getMessage());
    Toast.makeText(WeatherActivity.this, "Get data failed!", Toast.LENGTH_LONG).show();
    }
    }
    }
    });
    }
    }

MVP

  • 目录结构

    android-mvp

    • Weather 同上
    • IWeatherView && WeatherActivity

      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
        public interface IWeatherView {
      /**
      * 刷新天气列表
      * @param data
      */

      void refreshWeatherList(List<Weather> data);

      /**
      * 显示错误信息
      * @param e
      */

      void showErrorInfo(Exception e);
      }

      public class WeatherActivity extends AppCompatActivity implements IWeatherView{
      private ListView lv;
      private LinearLayout llLoading;
      private WeatherPresenter mWeatherPresenter;

      @Override
      protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_weather);
      lv = (ListView) findViewById(R.id.lv);
      llLoading = (LinearLayout) findViewById(R.id.ll_loading);
      mWeatherPresenter = new WeatherPresenter(this);
      mWeatherPresenter.getWeatherList();
      }

      @Override
      public void refreshWeatherList(List<Weather> data) {
      WeatherAdapter adapter = new WeatherAdapter(WeatherActivity.this, data);
      lv.setAdapter(adapter);
      llLoading.setVisibility(View.GONE);
      lv.setVisibility(View.VISIBLE);
      }

      @Override
      public void showErrorInfo(Exception e) {
      Log.e(WeatherActivity.class.getSimpleName(), e.getMessage());
      Toast.makeText(WeatherActivity.this, "Get data failed!", Toast.LENGTH_LONG).show();
      llLoading.setVisibility(View.GONE);
      }
      }
    • WeatherPresenter

      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
        public class WeatherPresenter {
      private IWeatherView mWeatherView;

      public WeatherPresenter(@NonNull IWeatherView view) {
      this.mWeatherView = view;
      }

      /**
      * 通过网络请求获取天气列表数据
      */

      public void getWeatherList() {
      HttpEngine.get(Constants.API_WEATHER, new HttpEngine.JSONRequestCallback() {
      @Override
      public void onFailed(Call call, Exception e) {
      if (call.request().url().url().toString().equals(Constants.API_WEATHER)) {
      mWeatherView.showErrorInfo(e);
      }
      }

      @Override
      public void onSuccess(Call call, JSONObject jsonResult) {
      if (call.request().url().url().toString().equals(Constants.API_WEATHER)) {
      try {
      JSONArray jsonArray = jsonResult.getJSONArray("results").getJSONObject(0).getJSONArray("daily");
      Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
      List<Weather> data = gson.fromJson(jsonArray.toString(), new TypeToken<List<Weather>>(){}.getType());
      mWeatherView.refreshWeatherList(data);
      } catch (JSONException e) {
      mWeatherView.showErrorInfo(e);
      }
      }
      }
      });
      }
      }