xray的博客


  • 首页

  • 归档

  • 关于

  • 分类

  • 标签

Android自动化测试工具MonkeyRnnner

发表于 2016-08-26

MonkeyRunner测试环境配置

  1. android-sdk

    下载android-sdk,配置android-sdk环境变量(当然也包括java环境,配置JAVA_HOME环境变量)

  2. 配置python环境

    MonkeyRunner基于python环境运行,最好下载2.x版的python,因为我测试代码中用的是2.x的python

配置好后输入monkeyrunner命令如果能进入monkeyrunner的命令交互模式证明安装成功

如图:

MonkeyRunner简介

MonkeyRunner是什么东西是猴子派来的救兵吗,确实是!MonkeyRunner是Android sdk中一个用于测试的工具,通过它,可以用一个python脚本程序去安装一个android的apk包,运行它,向它发送模拟点击,截取它的用户界面。

下面就是一段很简单的python脚本,通过调用MonkeyRunner的api实现app的自动安装,卸载,截图,打开某个页面。

具体每句代码的作用已经在注释中了

test_install.py

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

#!/usr/bin/env python

# encoding: utf-8



from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice, MonkeyImage

# 该目录是需要改成你自己的目录,将要测试的apk文件放进去,同时产生的截图文件也会保存到该目录下

dir = "/home/cfp/test/"



# 连接手机设备

device = MonkeyRunner.waitForConnection()



# 截图

result = device.takeSnapshot()

# 将截图保存到文件

result.writeToFile(dir + '1.png','png')



# 卸载APP 参数是旺财谷app的包名

device.removePackage('com.yitoudai.wcg')

print ('Uninstall Success!')



# 暂停5秒

MonkeyRunner.sleep(5)



# 截图

result = device.takeSnapshot()

result.writeToFile(dir + '2.png','png')



# 安装新的APP

device.installPackage(dir + 'wcg.apk')

print ('Install Success!')



# 截图

result = device.takeSnapshot()

result.writeToFile( dir + '3.png','png')



# 跳转到主页

device.startActivity(component="com.yitoudai.wcg/.ui.MainActivity")



#暂停目前正在运行的程序指定的秒数

#MonkeyRunner.sleep(秒数,浮点数)

MonkeyRunner.sleep(5)



#获取设备的屏蔽缓冲区,产生了整个显示器的屏蔽捕获。(截图)

result=device.takeSnapshot()

#返回一个MonkeyImage对象(点阵图包装),我们可以用以下命令将图保存到文件

result.writeToFile(dir +'4.png','png')

然后在终端中用MonkeyRunner运行test_install.py脚本,就可以看到效果了

1
2

monkeyrunner test_install.py

除了上面脚本文件中用到的命令,还有一些常用的脚本命令

  • 重启设备
1
2

device.reboot()
  • 点击屏幕,前两个参数是坐标,第三个是点击事件
1
2

device.touch(100, 100, 'DOWN_AND_UP')
  • 在设备上弹出提示信息
1
2

MonkeyRunner.alert("Hello World")
  • 向编辑区域输入文本“hello”
1
2

device.type('hello')

下面想特别说一下

1
2

device.startActivity()这个方法

其实官方文档完整的形式是

1
2

void startActivity ( string uri, string action, string data, string mimetype, iterable categories dictionary extras,component component, iterable flags)

上面的栗子,是跳转到一个不需要传参数的页面,但是app中很多页面是需要传参数的,否则这个页面进去会显示不正常,比如标详情页,需要传入标id

那怎么传呢,其实很简短,因为传的是键值对,对应python中的词典。可以这样写:

1
2
3
4
5
6

extra={}

extra['deal_id'] = '2334'

device.startActivity(extras = extra, component="com.yitoudai.wcg/.ui.financial.DealDetailActivity")

上面的代码就是跳转到标详情页的实现,标id是2334

MonkeyRunner 录制和回放脚本

用上面的方法写脚本测试确实很麻烦,而且要求测试对android项目必须的熟悉。有没有更naocan一点的方法呢,回答是yes,它也是android sdk中的一个工具MonkeyRecorder

运行MonkeyRecorder需要两个脚本:

  • 录制脚本:recorder.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21


#!/usr/bin/env monkeyrunner
# Copyright 2010, The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from com.android.monkeyrunner import MonkeyRunner as mr
from com.android.monkeyrunner.recorder import MonkeyRecorder as recorder
device = mr.waitForConnection()
recorder.start(device)
  • 回放脚本:recorder_playback.py
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

#!/usr/bin/env monkeyrunner
# Copyright 2010, The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import sys
from com.android.monkeyrunner import MonkeyRunner
from com.android.monkeyrunner import MonkeyRunner as mr

# The format of the file we are parsing is very carfeully constructed.
# Each line corresponds to a single command. The line is split into 2
# parts with a | character. Text to the left of the pipe denotes
# which command to run. The text to the right of the pipe is a python
# dictionary (it can be evaled into existence) that specifies the
# arguments for the command. In most cases, this directly maps to the
# keyword argument dictionary that could be passed to the underlying
# command.

# Lookup table to map command strings to functions that implement that
# command.
CMD_MAP = {
'TOUCH': lambda dev, arg: dev.touch(**arg),
'DRAG': lambda dev, arg: dev.drag(**arg),
'PRESS': lambda dev, arg: dev.press(**arg),
'TYPE': lambda dev, arg: dev.type(**arg),
'WAIT': lambda dev, arg: MonkeyRunner.sleep(**arg)
}
device= mr.waitForConnection(1,"emulator-5556")
# Process a single file for the specified device.
def process_file(fp, device):
for line in fp:
(cmd, rest) = line.split('|')
try:
# Parse the pydict
rest = eval(rest)
except:
print 'unable to parse options'
continue

if cmd not in CMD_MAP:
print 'unknown command: ' + cmd
continue

CMD_MAP[cmd](device, rest)


def main():
file = sys.argv[1]
fp = open(file, 'r')

device = MonkeyRunner.waitForConnection()

process_file(fp, device)
fp.close();


if __name__ == '__main__':
main()

当执行

1
2

monkeyrunner recorder.py

会出现下面的窗口

该窗口的功能:

  1. 可以自动显示手机当前的界面

  2. 自动刷新手机的最新状态

  3. 点击手机界面可以对手机进行操作,同时反应到真机,而且会在右侧插入操作脚本

4.

- wait: 用来插入下一次操作的时间间隔

- Press a Button: 用来确定需要点击的按钮,包括menu、home、serach,以及对按钮的press、down、up属性

- Type Something: 用来输入内容到输入框

- Fling: 用来进行拖动操作, 可以向上、下、左、右,以及范围的操作

- Export Actions: 用来导出脚本

- Refresh Display: 用来刷新手机界面

录制的脚本如下:

recorder_action.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

TOUCH|{'x':698,'y':1540,'type':'downAndUp',}

TOUCH|{'x':162,'y':1836,'type':'downAndUp',}

TOUCH|{'x':266,'y':1836,'type':'downAndUp',}

TOUCH|{'x':563,'y':1668,'type':'downAndUp',}

TYPE|{'message':'13310011006',}

TOUCH|{'x':317,'y':424,'type':'downAndUp',}

TYPE|{'message':'aaa111222',}

TOUCH|{'x':1002,'y':152,'type':'downAndUp',}

TOUCH|{'x':982,'y':1692,'type':'downAndUp',}

TOUCH|{'x':587,'y':1736,'type':'downAndUp',}

比如上边的脚本是我记录的登陆操作,我们要回放上面的步骤,只需要执行一下上面的recorder_playback.py脚本

1
2

monkeyrunner recorder_playback.py recorder_action.py

第二个脚本就是录制的脚本文件。

MonkeyRunner的测试类型

  1. 多设备控制

  2. 功能测试:例如:你给一个输入框提供值,然后观察输出结果的截屏

  3. 回归测试:可以将测试接截屏和正确的结果相比较。MonkeyRunner有个模块提供了该功能。

总结

要真正在生产环境中使用MonkeyRunner,需要测试能编写python脚本代码,并且对andrfoid的项目有一定的了解,当然前期可以让开发去编写测试代码,但是,测试至少也的能看懂。上面的内用还处于demo的级别,还需要许多工作去做.

路漫漫其修远兮,吾将上下而求索…

Android反编译总结

发表于 2016-05-26

工欲善其事必先利其器

前言

Android反编译需要用到一些工具,使用这些工具可以大大的提高我们的工作效率。当然使用工具是一把双刃剑,一方面工具使我们的工作更方便,但另一方面工具把一些原理封装了起来,不利于我们的学习。所以使用工具的时候最好对工具的原理有一定的了解。

jadx

github地址,jadx是一个将android 的dex文件解码为java的工具。并且有GUI,用起来非常的方便,具体的使用可以看官方的文档。

apktool

有了上面的工具,可以很方便的将apk的源码进行反编译,让我们了解代码的逻辑。但是这还远远不够,比如我们想干点坏事,将apk反编译后,做点手脚,并把它重新打包发布出去,要实现这样的功能,光上面的工具已经达不到我们的要求了。我们需要另外一个更加强大的工具-apktool. apktool可以将dex文件反编译成smali文件。如果对smali语法熟悉,就可以在smali中修改app的逻辑。其实apktool这个工具集成了另外一个开源的项目baksmali,(因为apktool集成了baksmail, 假如baksmail修复了bug,需要一段时间才能集成的apktool中,所以我们也可以直接使用baksmail)

smali语法

为了达到我们对一个apk做手脚的目的,需要了解一种新的语言smali, smail语言类似与汇编语言,如果对汇编比较熟悉的学习起来会比较容易。

实战

下面就举个例子,演示一下怎么利用apktool解包,修改smali文件,重新打包,签名的过程。

首先新建个项目,主要一个MainActivity,里面只有一个字符串,我的目的就是通过解包apk,在smali语法中注入日志打印语句,把这个字符串打印出来:

1
2
3
4
5
6
7
8
9
10
11

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String hello = "Hello World";

}
}

将项目打包出来的apk(debug包和release包都可以)拷贝到apktool的同级目录

目录

然后将apk包用apktool,反编译,命令为:

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

cfp@cfp:~/tools/android_tools/apktool$ ./apktool d app-release.apk

I: Using Apktool 2.1.1 on app-release.apk

I: Loading resource table...

I: Decoding AndroidManifest.xml with resources...

I: Loading resource table from file: /home/cfp/apktool/framework/1.apk

I: Regular manifest package...

I: Decoding file-resources...

I: Decoding values */* XMLs...

I: Baksmaling classes.dex...

I: Copying assets and libs...

I: Copying unknown files...

I: Copying original files...

cfp@cfp:~/tools/android_tools/apktool$

反编译后多了个app-release目录,反编译的文件就都在里边了,下面的任务就是找到MainActivity,在里边注入日志打印语言,将里边的字符串输出。

1
2
3
4
5
6

cfp@cfp:~/tools/android_tools/apktool$ ls

apktool app-release debug.keystore sign.jar

apktool.jar app-release.apk signapk.jar

反编译后的目录:

1
2
3
4

cfp@cfp:~/tools/android_tools/apktool/app-release$ ls

AndroidManifest.xml apktool.yml original res smali

所有的smali文件都在smali目录下,我们找到MainActivity.smali文件

1
2
3
4
5
6
7
8
9
10

cfp@cfp:~/tools/android_tools/apktool/app-release/smali/com/xray/smali$ ls

BuildConfig.smali R$bool.smali R$id.smali R.smali

MainActivity.smali R$color.smali R$integer.smali R$string.smali

R$anim.smali R$dimen.smali R$layout.smali R$styleable.smali

R$attr.smali R$drawable.smali R$mipmap.smali R$style.smali

MainActivity.smali文件中的代码:

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

.class public Lcom/xray/smali/MainActivity; #class的名字

.super Landroid/support/v7/app/AppCompatActivity; #这个类的父类

.source "MainActivity.java" #这个类的java文件名





# direct methods

.method public constructor <init>()V #这个类的构造方法

.locals 0



.prologue

.line 7 #行号

invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V #调用父类的构造方法



return-void #返回空

.end method





# virtual methods

.method protected onCreate(Landroid/os/Bundle;)V #onCreate方法

.locals 2

.param p1, "savedInstanceState" # Landroid/os/Bundle; #方法的参数



.prologue

.line 11

invoke-super {p0, p1}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V 

#父类的构造方法





.line 12

const v1, 0x7f040019



invoke-virtual {p0, v1}, Lcom/xray/smali/MainActivity;->setContentView(I)V #调用serContentView方法



.line 13

const-string v0, "Hello World" #字符串Hello World,保存在寄存器v0中



.line 15

.local v0, "hello":Ljava/lang/String; #变量hello ,类型为String

return-void

.end method

前面说了smali语言有点像汇编,通过上面的注释大概能了解这个smali文件的大概意思,要想输出Hello World,我么可以加上调用Log的smali语句。

1
2
3
4

const-string v1, "TAG"

invoke-static {v1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

修改后的MainActivity.smali文件变成:

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

.class public Lcom/xray/smali/MainActivity; #class的名字

.super Landroid/support/v7/app/AppCompatActivity; #这个类的父类

.source "MainActivity.java" #这个类的java文件名





# direct methods

.method public constructor <init>()V #这个类的构造方法

.locals 0



.prologue

.line 7 #行号

invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V #调用父类的构造方法



return-void #返回空

.end method





# virtual methods

.method protected onCreate(Landroid/os/Bundle;)V #onCreate方法

.locals 2

.param p1, "savedInstanceState" # Landroid/os/Bundle; #方法的参数



.prologue

.line 11

invoke-super {p0, p1}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V 

#父类的构造方法





.line 12

const v1, 0x7f040019



invoke-virtual {p0, v1}, Lcom/xray/smali/MainActivity;->setContentView(I)V #调用serContentView方法



.line 13

const-string v0, "Hello World" #字符串Hello World,保存在寄存器v0中



.line 15

.local v0, "hello":Ljava/lang/String; #变量hello ,类型为String

const-string v1, "TAG"

invoke-static {v1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

return-void

.end method

然后重新打包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

cfp@cfp:~/tools/android_tools/apktool$ ./apktool b app-release

I: Using Apktool 2.1.1

I: Checking whether sources has changed...

I: Smaling smali folder into classes.dex...

I: Checking whether resources has changed...

I: Building resources...

I: Building apk file...

I: Copying unknown files/dir...

新的apk包位于

1
2

app-release/dist

这个apk包是不能直接安装的,需要用签名工具签名,可以使用另外一个工具sign.jar,命令为:

1
2

cfp@cfp:~/tools/android_tools/apktool$ java -jar sign.jar app-release.apk

签名后的新包叫app-release.s.apk,直接安装就可以打印Hello World的日志了。



1
2

05-25 22:56:25.725 17277-17277/com.xray.smali D/TAG: Hello World

参考文献

  1. smali语法

  2. http://drops.wooyun.org/papers/6045

Retrofit介绍

发表于 2016-04-25

前言

Retrofit是Square开发的一个用于网络请求的开源库,内部封装了okhttp,并且和RxAndroid完美的兼容,使得Android的开发效率增加不少的同时也使代码变得清晰易读。

Gradle 依赖

retrofit可以很方便的使用Maven和Gradle依赖,在1.x时retrofit默认是没有引入okhttp作为http client,需要手动的依赖。但是2.0版本已经将okhttp作为retrofit的默认http client,引入retrofit2只需要在gradle中配置

1
2

compile 'com.squareup.retrofit2:retrofit:2.0.0'

如果你不想使用retrofit2中自带的okhttp,你也可以导入你自己的okhttp,为了避免导入冲突可以按下面的依赖:

1
2
3
4
5
6

compile ('com.squareup.retrofit2:retrofit:2.0.0') {

exclude module: 'okhttp'
}
compile 'com.squareup.okhttp3:okhttp:3.2.0'

retrofit2默认没有导入gson,需要gson作为转换器:

1
2

compile 'com.squareup.retrofit2:converter-gson:2.0.0'

与RxAndroid使用需要依赖:

1
2
3

compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0'
compile 'io.reactivex:rxandroid:1.0.1'

请求参数注解说明

@Query @QueryMap

Http Get请求参数:

1
2
3

@GET("group/users")
Call<List<User>> groupList(@Query("id") int groupId);

等同于

1
2

@GET("group/users?id=groupId")

多个请求参数

1
2
3
4


@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);

@Field

用于Post方式传递参数,需要在请求接口方法上添加@FormUrlEncoded,表示以表单的方式传递参数

1
2
3
4

@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

@Path

用于URL占位符

1
2
3

@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId);

@Header

添加http header

1
2
3

@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)

@Body

请求体,对象会被自动转化成Json格式

1
2
3
4


@POST("users/new")
Call<User> createUser(@Body User user);

拦截器Interceptor

retrofit2默认的集成了okhttp, okhttp可以设置拦截器,比如个请求添加统一的Header

1
2
3
4
5
6
7
8
9
10
11
12

httpClient.addNetworkInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {

Request request = chain.request();
Request.Builder requestBuilder = request.newBuilder().addHeader("Accept", "application/json");
Request request1 = requestBuilder.build();

return chain.proceed(request);
}
});

Post多个参数提交

如果有很多默认的参数需要每次添加时:

1
2
3
4
5
6
7
8
9

@FormUrlEncoded
@POST("/feedback")
Call<ResponseBody> sendFeedbackSimple(
@Field("osName") String osName,
@Field("osVersion") int osVersion,
@Field("device") String device,
@Field("message") String message,
@Field("userIsATalker") Boolean userIsATalker);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

private void sendFeedbackFormSimple(@NonNull String message) {
// create the service to make the call, see first Retrofit blog post
FeedbackService taskService = ServiceGenerator.create(FeedbackService.class);

// create flag if message is especially long
boolean userIsATalker = (message.length() > 200);

Call<ResponseBody> call = taskService.sendFeedbackSimple(
"Android",
android.os.Build.VERSION.SDK_INT,
Build.MODEL,
message,
userIsATalker
);

call.enqueue(new Callback<ResponseBody>() {
...
});
}

可以写成下面的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

@POST("/feedback")
Call<ResponseBody> sendFeedbackConstant(@Body UserFeedback feedbackObject);


public class UserFeedback {

private String osName = "Android";
private int osVersion = android.os.Build.VERSION.SDK_INT;
private String device = Build.MODEL;
private String message;
private boolean userIsATalker;

public UserFeedback(String message) {
this.message = message;
this.userIsATalker = (message.length() > 200);
}

// getters & setters
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12



private void sendFeedbackFormAdvanced(@NonNull String message) {
FeedbackService taskService = ServiceGenerator.create(FeedbackService.class);

Call<ResponseBody> call = taskService.sendFeedbackConstant(new UserFeedback(message));

call.enqueue(new Callback<ResponseBody>() {
...
});
}

Retrofit2 + RxAndroid 封装和使用

首先创建一个工厂类根据不通的api interface 创建其实现。这个解释起来比较抽象,可以看下面的代码:

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

/**
* Created by cfp on 16-4-18.
*/
public class RetrofitFactory {


public static final String GITHUB_HTTP_URL = "https://api.github.com";

public static final String OTHER_HTTP_URL = "https://yoursite";


//默认超时时间
private final static int DEFAULT_TIMEOUT = 5;

private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder();

private static Retrofit.Builder githubBuilder = new Retrofit.Builder()
.baseUrl(GITHUB_HTTP_URL)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create());


private static Retrofit.Builder otherBuilder = new Retrofit.Builder()
.baseUrl(OTHER_HTTP_URL)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create());

/**
* 为了防止一个类里放置所有的api,可以根据程序的功能分成不通的模块,把他们存在map中
*/
private static Map<Class<?>, Object> githubApiMap = new HashMap<Class<?>, Object>();
private static Map<Class<?>, Object> otherApiMap = new HashMap<Class<?>, Object>();


/**
* 默认的server为github
*
* @param instanceClass
* @param <S>
* @return
*/
public synchronized static <S> S getInstance(Class<S> instanceClass) {

return getInstance(GITHUB_HTTP_URL, instanceClass);
}


public RetrofitFactory() {
}

public synchronized static <S> S getInstance(String server, Class<S> instanceClass) {

final Retrofit retrofit;

switch (server) {

case GITHUB_HTTP_URL:

if (githubApiMap.containsKey(instanceClass)) {

return (S) githubApiMap.get(instanceClass);
} else {
//添加拦截器,可以给请求添加header
httpClient.addNetworkInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {

Request origin = chain.request();
Request.Builder requestBuilder = origin.newBuilder()
.addHeader("devicetype", "ANDROID");
Request request = requestBuilder.build();

return chain.proceed(request);
}
});
retrofit = githubBuilder.client(httpClient.build()).build();
S instance = retrofit.create(instanceClass);
githubApiMap.put(instanceClass, instance);
return instance;


}
case OTHER_HTTP_URL:

if (otherApiMap.containsKey(instanceClass)) {

return (S) githubApiMap.get(instanceClass);
} else {

retrofit = githubBuilder.client(httpClient.build()).build();
S instance = retrofit.create(instanceClass);
otherApiMap.put(instanceClass, instance);
return instance;
}

default:
return null;

}

}
}

另外就是网络请求的抽象回调类,继承自Subscriber

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

/**
* 网络请求的回调抽象类
* Created by cfp on 16-5-24.
*/
public abstract class AbsCallBackSubscriber<T> extends Subscriber<T> {
@Override
public void onCompleted() {

//解除注册
if (this.isUnsubscribed()){

this.unsubscribe();
}
onFinish();
}

@Override
public void onError(Throwable e) {
// TODO: 16-5-25 一些错误处理
}

@Override
public void onNext(T t) {
onSuccess(t);
}

public abstract void onFinish();
public abstract void onSuccess(T t);

}

最后是真正的网络请求部分,如果有需求某次请求的错误信息需要单独处理,可以复写

AbsCallBackSubscriber中的onError方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

RetrofitFactory.getInstance(GithubApi.class).getFollowings(username)
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new AbsCallBackSubscriber<List<UserInfo>>() {
@Override
public void onFinish() {

}

@Override
public void onSuccess(List<UserInfo> userInfos) {

}
});

最后是接口的interface

1
2
3
4
5
6
7
8
9
10
11

/**
* Created by cfp on 16-4-18.
*/
public interface GithubApi {

@GET("/users/{username}")
Observable<UserInfo> getUserInfo(@Path("username") String username);
@GET("/users/{username}/following")
Observable<List<UserInfo>> getFollowings(@Path("username") String username);
}

总结

retrofit2 + reAndroid简单的流程用法差不多介绍了,也做了个简单的封装,至于更高级的用法,还需要继续的研究,也希望大家多多交流,指出不足共同进步^v^。

引用

  1. http://www.loongwind.com/archives/242.html

  2. http://xiuweikang.github.io/2016/03/17/Retrofit%E5%88%86%E4%BA%AB/

  3. https://futurestud.io/blog/retrofit-2-upgrade-guide-from-1-9

2016年书单

发表于 2016-03-23

读完

  • Pro-Git 作者是github的创始人

  • 协议森林用很直白但却很形象的文字帮助读者了解网络协议,读过这一系列文章后,你可以找到参考书籍,继续深入学习。

  • Python快速教程 文中代码主要基于python2.7.

在读

  • TCP/IP详解 卷一:协议 不多说了该领域的经典

  • App研发录 进阶必备

  • Android开发艺术探索 侧重于Android知识的体系化和体统工作机制的分析。

  • 命令行的艺术  步入大神的行列

想读

Base64编码解析

发表于 2016-03-09

Base64原理简介

Base64是一种基于64个可打印字符来表示二进制数据的表示方法,严格来讲并不是加密方法,仅仅是表示方法。由于每6bit为一个单元。三个字节有24个bit,对应于4个Base64单元

Base64索引表

这么说可能稍微有点抽象,举个例子就明白了,比如 “Man” 这个单词由3个字节,也就是24bit组成,按我们前面说的如果用Base64编码的话,由4个Base64单元组成。具体可以看下面的图

所以”Man”这个单词用Base64编码的结果为”TWFu”

但是我们举得这个例子有点特殊,“Man”正好有3个字节,24bit正好转成4个Base64, 如果转码的数据不是3的倍数。

最后会多长1个或2个字节,那么可以使用下面的方法处理:先使用0字节值在末尾补足,使其能够被3整除,然后再进行base64的编码。在编码后的base64文本后加上一个或两个‘=’号,代表补足的字节数。也就是说,当最后剩余一个八位字节(一个byte)时,最后一个6位的base64字节块有四位是0值,最后附加上两个等号;如果最后剩余两个八位字节(2个byte)时,最后一个6位的base字节块有两位是0值,最后附加一个等号。

编码

  • 在终端命令行中编字符串
1
2

echo "hello" | base64

注:在Ubuntu上使用 echo “hello” | base64 时或出现多编码字符的情况,这个是因为UTF-8编码的原因

从指定的文件file中读取数据,编码为base64字符串输出。

1
2

base64 file

从标准输入中读取已经进行base64编码的内容,解码输出。

1
base64 -d

Php笔记-基础语法

发表于 2016-03-09

变量

php 的变量必须用$开头

1
$myAge

switch语法

switch有两种语法:

  • 正常的语法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$myAge = 25;
switch($myAge){
case 20:
print "your are age is 20";
break;
case 24:
print "your are age is 24";
break;
case 25:
print "your are age is 25";
break;
default:
print "your are age is not match";
break;

}
  • sugar语法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$myAge = 25;
switch($myAge):
case 20:
print "your are age is 20";
break;
case 24:
print "your are age is 24";
break;
case 25:
print "your are age is 25";
break;
default:
print "your are age is not match";
break;
endswitch;
?>

数组

1
$array = array("cheng", "fang", "peng");

获取数组的值有两种方法

  • 通过[]
1
$array[2];
  • 通过{}
1
$array{2}

数组中删除元素

1
unset(array[2]);
1
2
3
4
5
6
7
8
9
10
<?php
$languages = array("HTML/CSS",
"JavaScript", "PHP", "Python", "Ruby");

unset($languages[3]);

foreach($languages as $lang) {
print "<p>$lang</p>";
}
/>

for循环

1
2
3
4
5
6
<?php
// 输出前五个偶数
for ($i = 2; $i < 11; $i = $i + 2) {
echo $i;
}
?>

foreach循环

foreach循环是迭代对象里的每一个元素。

1
2
3
4
5
6
7
8
9
10
11
<?php
$langs = array("JavaScript",
"HTML/CSS", "PHP",
"Python", "Ruby");

foreach ($langs as $lang) {
echo "<li>$lang</li>";
}

unset($lang);
?>

while 循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$headCount = 0;
$flipCount = 0;
while ($headCount < 3) {
$flip = rand(0,1);
$flipCount ++;
if ($flip){
$headCount ++;
echo "<div class=\"coin\">H</div>";
}
else {
$headCount = 0;
echo "<div class=\"coin\">T</div>";
}
}
echo "<p>It took {$flipCount} flips!</p>";
?>

endwhile的使用

1
2
3
4
5
6
7
8
9
10
11
<?php
$isLoop = true;
$index = 0;
while($isLoop):
if($index > 10){
$isLoop = false;
}
echo $index;
$index++;
endwhile;
?>

do-while的使用

先执行do中的代码,然后再执行while循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$flipCount = 0;
do {
$flip = rand(0,1);
$flipCount ++;
if ($flip){
echo "<div class=\"coin\">H</div>";
}
else {
echo "<div class=\"coin\">T</div>";
}
} while ($flip);
$verb = "were";
$last = "flips";
if ($flipCount == 1) {
$verb = "was";
$last = "flip";
}
echo "<p>There {$verb} {$flipCount} {$last}!</p>";
?>

函数

字符串内建函数

  • strlen 返回字符串的长度
1
2
3
4
<?php
$name = "chengfangpeng";
print strlen($name);
?>
  • substr 截取字符串函数,返回截取到的子字符串
1
2
3
4
<?php
$name = "chengfangpeng";
print substr($name, 5, 10);
?>
  • strtoupper 将字符串转为大写
1
2
3
4
5
<?php

$name = "chengfangpeng";
print strtoupper($name);
?>
  • strtolower 将字符串转为小写
1
2
3
4
<?php
$name = "CHENGFANGPENG";
print strtolower($name);
?>
  • strpos 找出字符串中某个子字符串的位置,如果没有所给的字符串,返回false,否则返回子字符串在原字符串中的位置。
1
2
3
4
5
6
7
8
9
<?php

$name = "chengfangpeng";
print strpos($name, "fang");

if(strpos($name, "Hello") === false){
print "no match string";
}
?>

数学内建函数

  • round 将一个float类型的数,四舍五入为整形,如果再传入保留位数参数可以转化成保留小数位的float类型。
1
2
3
4
<?php
print round(M_PI); //3
print round(M_PI, 3);//3.142
?>
1
2
3
4
5
6
7
//使用rand,strlen,substr函数随机打印你姓名中的一个字母
<?php

$name = "chengfangpeng";
$rIndex = rand(0, strlen($name));
print substr($name, $rIndex, 1);
?>

Ubuntu安装Fiddler

发表于 2016-02-21

前言

Fiddler 是Windows上一个非常强大的抓包工具,其实在Ubuntu上也是可以安装的,具体的方法如下:

安装mono

fiddler是基于.net开发,所以安装fidder前需要安装.net framework。而.net framework 只能运行在windows,针对linux和mac操作系统,可通过安装mono framework 来支持fiddler运行。
安装命令:

1
sudo apt-get install  mono-complete

下载Fiddler

下载地址
下载Fiddler for Mono Current Linux build
解压

运行Fiddler

进入解压目录直接运行

1
./fiddler.exe

就可以使用了。

Ubuntu截图工具

发表于 2016-01-10

使用Ubuntu已近有一段时间了,但是有时候工作中需要截图,之前用的是win qq的截图,但是大家都懂得,qq在Ubuntu 体验差的要死,还是多年以前的版本。实在不是很方便。于是就找了一下Ubuntu上好的截图方法
方法一:其实Ubuntu是自带截图工具的(注:我用的是Ubuntu 14.04)在/usr/share/applications 中可以看到一个类似于照相机图标的应用

Screenshot

打开之后出现一个窗口你可以选择截取整个屏幕,或者是一个窗口,再或者某个区域,当然系统默认有使用Sreenshot的快捷键 分别为PrtSc, Alt + PrtSc, Shift + PrtSc但是系统自带的没有编辑图片的功能,比如你想模糊一些私密信息,加一些备注,系统的Screenshot就无能为力了。
方法二:Shutter
Shutter是一个第三方的截图软件需要我们手动的安装,方法很简单:

1
sudo apt-get install shutter

安装完之后在终端中输入

1
cfp@cfp:~$ shutter -s

就可以像qq那样截图了
当然了shutter 配置了对图片的编辑功能,可以添加备注等等,非常的强大,具体安装后自己体验一下。

另外就是像使用qq的截图功能一样,添加一个快捷键 Ctrl + Alt +A

具体步骤:

  1. 打开系统设置
  2. 打开 Keyboard 键盘设置
  3. 选择快捷键
  4. 自定义快捷键
  5. 添加快捷键
  6. 编辑快捷键名称和打开命令
  7. 右键设置快捷键 Ctrl + Alt +A

步骤

好,这样在Ubuntu上也可以噬无忌惮的截图了!

Ubuntu科学上网服务器搭建

发表于 2016-01-10

前言

受天朝GFW的影响,包括大Google在内的很多境外网站访问不了,所以天朝的程序猿大多都练就了一种叫”翻墙”的功夫.从自由门,配置host,到goAgent,再到现在各色各样的vpn,翻墙的姿势也随着时代的发展发生着变化。

工具

代理服务器:搬瓦工(我租的是每月5刀的,1T的流量,对于个人用绰绰有余了)
系统:Ubuntu 14.04
代理工具:shadowsocks (项目的原作者去年被请去喝茶,所以停止更新了,但是相信一个shadowsocks倒下去,成千上万的shadowsocks会站起来)

配置服务器

1.先到搬瓦工租个云服务器,类似国内的阿里云,腾讯云。

这里写图片描述

之后就是购买的一些流程,比如让你选择服务器的位置(我选的是洛杉矶)还有就是些你的用户信息,支付的时候意外的发现搬瓦工居然支持支付宝,这一点很方便。

这里写图片描述

这里写图片描述

填写一些用户信息

支付环节

支付成功后到Client Area->Service->MyService->KiWiControl Panel
就可以看到你服务器的配置信息了

这里写图片描述

这里写图片描述

这里写图片描述

之后的工作就是在服务器上安装shadowsocks.
通过终端ssh到服务器,在Ubuntu上安装shadowsocks比较方便,shadowsocks使用python写的(当然也有其他版本的,例如:go语言的),所以需要一个管理python包的工具PIP,安装PIP的命令如下:

1
~# apt–get install python–gevent python–pip

然后可以直接安装 shadowsocks 了

1
~# pip install shadowsocks

然后就是配置shadowsocks,自己写个配置文件/etc/shadowsocks.json

1
2
3
4
5
6
7
8
9
{
"server":"你服务器的ip",
"server_port":8388,
"password":"密码",
"timeout":300,
"method":"aes-256-cfb",
"fast_open":false,
"workers": 1
}

server: 你自己服务器的ip
server_port:给shadowsocks分配的端口,默认是8388
password: 你设置的shadowsocks 密码
timeout:超时时间,默认是300秒
method: 加密方法,默认是aes-256-cfb
fast_open: 在Linux3.7+可以使用TCP_FASTOPEN
workers: number of workers (应该是客户端账户的个数,没理解明白)

之后通过下面的命令就可以启动shadowsocks了

1
ssserver -c /etc/shadowsocks.json -d start

这样服务端的任务就已经完成了

当然了我们也可以把shadowsocks设置为开机自起
把上面的命令配置到/etc/rc.local 中就可以了

这里写图片描述

这样,即使重启服务器,shadowsocks也会自动启动

到这里服务端已经大功告成!!!


客户端配置

windows系统:需要shadowsocks客户端,配置如图(我用的是Ubuntu的图,windows界面类似)

这里写图片描述

之后需要配合浏览器的代理服务器功能.

如果利用Chrome插件Proxy SwitchySharp,注意一定要选择SOCKS5

所有客户端的下载地址

~
结束
–
配置比较简单,网上的资料一大堆,最后吐槽一下Baidu最近的血友病事件,一个卖假药,一个研究量子计算机,这就是Baidu和Google的差距。

Android属性动画

发表于 2015-10-29

前言

Property Animation是Android3.0引入的一种功能强大的动画系统。它除了可以给普通的View添加动画效果外,还可以给对象添加效果。另外,Property Animation与Tween Animation一个最大的区别是Property Animation更改是对象的实际属性,而后者只是View的绘制效果,比如一个Button实现一个移动的动画效果,如果使用Tween Animation 的话,Button 的点击位置并不会随着动画的移动效果儿移动。换句话说在新位置Button是可能没有点击事件的。使用Property Animation可以设置下面的一些动画特性:

  • Duration: 动画之间的间隔

  • Time interpolation: 属性值的变化方式,可以表示为动画的事件函数,例如线性动画,加速动画等等。

  • Repeat count and behavior: 动画的重复次数和重复方式。

  • Animation sets: 动画集合

  • Frame refresh delay: 帧刷新间隔,默认是10ms,但具体的速度依赖于系统的繁忙程度。

属性动画的工作原理

图1描绘了一个假象的对象x属性的动画,它给出了该对象在屏幕水平方法的位置,在40ms内改对象移动了40个像素。每10ms记录一次对象移动的像素,这个动画是的Time interpolation是liearinterpolation,表明动画是以恒定的速度移动的。

图1

在Property Animation中,ValueAnimator是其核心类,它记录了动画自身的一些属性值。图2是其工作流程:

图2

动画在整个过程中,会根据我们当初设置的TimeInterpolator和TypeEvaluator的计算方式计算出不通的属性值,从而不断的改变属性值的大小,就会产生各式各样的动画效果。

下面就通过一个实例理解一下什么是TimeInterpolator和TypeEvaluator.

  • TimeInterpolator

TimeInterpolator翻译过来是插值器,插值器定义了动画变化中的属性变化规则,它根据时间比例因子计算出一个插值因子,那么什么是时间比例因子呢,简单讲就是:

1
2

时间比例因子 = 动画已执行的时间 / 动画执行的总时间

而插值因子,用于设定动画是线性变化,还是非线性变化等千万中变化,你可以通过实现TimeInterpolator来实现自己的插值器(在>sdk22可以继承抽象类BaseInterpolator)。Android中默认的定义很多的插值器:

  1. AccelerateDecelerateInterpolator

    在开始和结束时速度较慢

  2. AccelerateInterpolator

    加速变化

  3. LinearInterpolator

    匀速变化

更多的插值器,可以在这里查看.

1
2
3
4
5
6
7
8
9
10
11

//自定义插值器
class MyInterpolator implements TimeInterpolator{


@Override
public float getInterpolation(float input) {
//自定义的规则
return 0;
}
}
  • TypeEvaluator

TypeEvaluator是根据插值因子去计算属性值,Android默认可以识别的类型为int, float和颜色,分别是

IntEvalutor, FloatEvalutor, ArgbEvaluator.

  1. IntEvalutor

    计算整数型

  2. FloatEvalutor

    计算浮点型

  3. ArgbEvaluator

    计算颜色属性

1
2
3
4
5
6
7
8
9
10
11
12
13



//自定义TypeEvaluator
class MyTypeEvaluator implements TypeEvaluator<Ball>{


@Override
public Ball evaluate(float fraction, Ball startValue, Ball endValue) {
//自己的规则
return null;
}
}

下面以三个小球的旋转效果为例,了解一下属性动画的整个实现过程。

效果图

源码可以看这里

<i class="fa fa-angle-left"></i>123<i class="fa fa-angle-right"></i>

chengfangpeng

冲出地球,移民宇宙

24 日志
8 标签
© 2019 chengfangpeng
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4