在Gradle中运行单元测试很简单。一般情况下,如果有一个单元测试CASE执行失败,也会导致构建失败。但是我们却不能立刻在控制台看到为什么这个单元测试失败。我们需要先打开生成的HTML格式的测试报告。现在我们还有其他办法。

首先,我们创建一个下面的Gradle build文件:

1
2
3
4
5
6
7
8
9
10
// File: build.gradle
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
testCompile 'junit:junit:[4,)'
}

然后,我们使用下面的Junit单元测试。注意,这个单元测试一直会失败,因为这就是我们想要的CASE场景。

1
2
3
4
5
6
7
8
9
10
11
12
// File: src/test/java/com/mrhaki/gradle/SampleTest.java
package com.mrhaki.gradle;
import org.junit.*;
public class SampleTest {
@Test public void sample() {
Assert.assertEquals("Gradle is gr8", "Gradle is great");
}
}

我们通过执行test任务,来运行单元测试。如果我们运行这个任务,我们发现控制台显示哪一行单元测试失败了,但是我们却看不到单元测试失败的具体原因:

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
$ gradle test
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
com.mrhaki.gradle.SampleTest > sample FAILED
org.junit.ComparisonFailure at SampleTest.java:8
1 test completed, 1 failed
:test FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/testlogging/build/reports/tests/index.html
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
BUILD FAILED
Total time: 4.904 secs

我们可以再次运行test任务,但是现在我们通过添加命令行选项-i 或者 –info来设置Gradle日志级别到info。现在我们可以在终端看到单元测试失败的具体原因了。

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
$ gradle test -i
Starting Build
Settings evaluated using empty settings script.
Projects loaded. Root project using build file
...
Successfully started process 'Gradle Worker 1'
Gradle Worker 1 executing tests.
Gradle Worker 1 finished executing tests.
com.mrhaki.gradle.SampleTest > sample FAILED
org.junit.ComparisonFailure: expected:<gradle is gr[8]> but was:<gradle is gr[eat]>
at org.junit.Assert.assertEquals(Assert.java:115)
at org.junit.Assert.assertEquals(Assert.java:144)
at com.mrhaki.gradle.SampleTest.sample(SampleTest.java:8)
Process 'Gradle Worker 1' finished with exit value 0 (state: SUCCEEDED)
1 test completed, 1 failed
Finished generating test XML results (0.025 secs)
Generating HTML test report...
Finished generating test html results (0.027 secs)
:test FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/testlogging/build/reports/tests/index.html
* Try:
Run with --stacktrace option to get the stack trace. Run with --debug option to get more log output.
BUILD FAILED
Total time: 5.117 secs

但是这样做仍然会产生很多干扰的日志。最好的办法就是通过配置test任务来自定义单元测试日志级别。我们可以配置不同的日志级别。为了获取单元测试失败的具体原因我们可以仅仅将exceptionFormat设置为full。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// File: build.gradle
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
testCompile 'junit:junit:[4,)'
}
test {
testLogging {
exceptionFormat = 'full'
}
}

我们可以再次运行test任务,并且使用正常的日志级别,但是这次,我们依然能够看到单元测试失败的具体原因,而没有其他的干扰日志。

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
$ gradle test
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test
com.mrhaki.gradle.SampleTest > sample FAILED
org.junit.ComparisonFailure: expected:<gradle is gr[8]> but was:<gradle is gr[eat]>
at org.junit.Assert.assertEquals(Assert.java:115)
at org.junit.Assert.assertEquals(Assert.java:144)
at com.mrhaki.gradle.SampleTest.sample(SampleTest.java:8)
1 test completed, 1 failed
:test FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/gradle/testlogging/build/reports/tests/index.html
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
BUILD FAILED
Total time: 5.906 secs

实例使用 Gradle 1.6 编写。

联系方式

sn0wdr1am

作者:snowdream
Email:yanghui1986527#gmail.com
Github: https://github.com/snowdream
QQ 群: 529327615
原文地址:https://snowdream.github.io/blog/2016/11/27/gradle-issues-with-android/

现在,大家都慢慢开始习惯使用Android Studio来开发Android应用了。
在使用过程中,难免碰到一些Gralde相关的使用问题。下面总结我收集整理的一些碰到的问题,以及解决方案:

1. 问题: Maven源仓库不可用,或者访问,下载非常慢。

分析: 由于主要的仓库Maven,Jcenter都在国外,以及众所周知的原因,这些Maven仓库可能比较慢。而我们从新建/导入工程开始,就要和Maven源仓库进行交互,因此,这可能导致我们导入或者打开工程非常的缓慢。

解决方案

  1. 优先使用国内Maven仓库镜像。推荐:阿里云Maven仓库镜像

    1
    maven { url “http://maven.aliyun.com/mvn/repository/“ }
  2. 使用http协议访问Maven仓库。使用下面的仓库来替换默认的 jcenter() 和 mavenCentral()

    1
    2
    jcenter { url “http://jcenter.bintray.com/"}
    maven { url “http://repo1.maven.org/maven2"}
  3. 如果你的项目不需要频繁从Maven仓库更新/下载组件,那么强烈建议你打开offline模式。

2. 问题: Android Studio导入工程非常慢,甚至卡死在导入阶段。

分析:Android Studio导入工程,默认会采用Gradle Wrapper的方式。也就是当你在本地没有下载过相应版本的Gradle,在导入工程前,就会去尝试下载相应版本的Gradle。

关于Gradle Wrapper的配置文件,在项目根目录下gradle/wrapper下。通常是两个文件:gradle-wrapper.jar和gradle-wrapper.properties

我们来看看gradle-wrapper.properties文件:

1
2
3
4
5
6
#Mon Dec 28 10:00:20 PST 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

没错,如果你之前没有通过Gradle Wrapper方式下载过这个版本Gradle,那么你导入工程后,第一件事情就是通过distributionUrl去下载gradle。然而,我们从这个来自Gradle官方的下载地址下载是非常慢的。

解决方案
1.搭建局域网HTTP服务器,镜像Gradle主流版本安装包。
参考:腾讯的方案:http://android-mirror.bugly.qq.com:8080/gradle/

2.在项目根目录build.gradle配置一个gradle wrapper任务。

1
2
3
4
task wrapper(type: Wrapper) {
gradleVersion = '3.1'
distributionUrl = "http://android-mirror.bugly.qq.com:8080/gradle/gradle-${gradleVersion}-bin.zip"
}

其中:gradleVersion对应gradle版本号,而distributionUrl则是对应版本的Gradle安装包下载地址。

3.在根目录下执行gradle wrapper 命令,然后将根目录下的gradle目录提交至git仓库。

3. 问题: 导入工程后,在Gradle Sync阶段卡死。

具体表现为:你以前已经在Android Studio中打开该工程,但是当你升级了本地Gradle版本,又或者使用Intellij IDEA打开,你会发现导入很慢。即使进去主界面,Gradle Sync也会处于假死状态,好像一直在加载什么。。。

分析: 不明

解决方案:根据网上的建议,排除以上原因之后,你可以这么干:删除工程下的.gradle文件夹,然后重新导入工程。

作者:snowdream
Email:yanghui1986527#gmail.com
Github: https://github.com/snowdream
QQ 群: 529327615
原文地址:https://snowdream.github.io/blog/2016/10/27/wifi-adb-without-usb/

简介

大家在开发调试Android应用的时候,都需要使用USB连接电脑和测试手机。
那么如何通过WIFI来连接电脑和测试手机呢?

通常的做法是这样:
安装idea插件AndroidWiFiADB。通过这个插件,只需要用USB连接一次电脑和测试手机,之后就可以只通过WIFI来连接了。

那么有没有方法,完全通过WIFI来进行连接呢?
下面就要介绍这样一款工具:WIFIADB

前提

Android手机需要Root

安装

WIFIADBIntelliJPlugin

在Idea/Android studio插件安装对话框输入“WIFI ADB ULTIMATE”,安装插件,然后重启IDE。
WIFIADBIntelliJPlugin

WIFIADBAndroid

  1. 点击,下载安装WIFIADBANDROID-RELEASE-1.0.APK
  2. 测试手机连接wifi
  3. 启动应用,点击主界面中间的圆形按钮。如果启动成功,底部会提示IP地址和端口号。
    WIFIADBAndroid

运行

在Idea/Android studio界面右侧,点击标签““WIFI ADB ULTIMATE””,输入IP地址和端口号,点击“Connect”即可。

WIFI ADB ULTIMATEWIFI ADB ULTIMATE

参考

  1. WIFIADB
  2. AndroidWiFiADB
  3. ADBWIFI

作者:snowdream
Email:yanghui1986527#gmail.com
Github: https://github.com/snowdream
QQ 群: 529327615
原文地址:https://snowdream.github.io/blog/2016/10/26/gradle-goodness-check-task-dependencies/

翻译自: http://mrhaki.blogspot.com/2014/11/gradle-goodness-check-task-dependencies.html

我们可以运行Gradle的任务,但是不实际执行动作。这就是所谓的模拟运行。我们可以通过模拟运行来查看,我们定义的任务依赖关系是否正确。因为,我们在进行模拟运行时,我们可以看到所有的任务和任务依赖关系输出日志。

例如,我们定义了一个简单的build文件,包含三个任务和一些任务依赖关系:

1
2
3
4
5
6
7
8
9
def printTaskNameAction = {
println "Running ${it.name}"
}
task first << printTaskNameAction
task second(dependsOn: first) << printTaskNameAction
task third(dependsOn: [first, second]) << printTaskNameAction

我们通过添加命令行选项 -m or –dry-run来进行模拟运行。因此,我们这样执行第三个任务:

1
2
3
4
5
6
7
8
9
$ gradle -m third
:first SKIPPED
:second SKIPPED
:third SKIPPED
BUILD SUCCESSFUL
Total time: 2.242 secs
$

我们在输出中可以看到,没有任何任务是实际被执行的,在输出日志中显示SKIPPED,但是我们可以看到所有被执行任务的名字。

作者:snowdream
Email:yanghui1986527#gmail.com
Github: https://github.com/snowdream
QQ 群: 529327615
原文地址:https://snowdream.github.io/blog/2016/10/26/gradle-goodness-changing-name-of/

翻译自: http://mrhaki.blogspot.com/2014/10/gradle-goodness-changing-name-of.html

Gradle默认使用build.gradle作为默认的配置文件文件名。如果我们在build.gradle文件中编写代码,那么我们在运行任务的时候,不需要指定build文件名。我们也可以不使用build.gradle,而用另外的文件名来创建build配置文件。例如:我们可以在一个名字为sample.gradle的文件中编写代码。为了运行这个文件中的任务,我们需要在命令行,使用选项 -b 或者 –build-file,后面指定build文件名。但是,我们可以改变项目设置,并且设置项目的默认build文件名。这样设置,我们不需要再使用前面的命令行选项。

假设我们现在有一个build文件,sample.gradle:

1
2
3
4
5
6
// File: sample.gradle
task sample(description: 'Sample task') << {
println 'Sample task'
}
defaultTasks 'sample'

为了运行sample任务,我们可以使用命令行选项 -b 或者 –build-file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ gradle -b sample.gradle
:sample
Sample task
BUILD SUCCESSFUL
Total time: 3.168 secs
$ gradle --build-file sample.gradle
:sample
Sample task
BUILD SUCCESSFUL
Total time: 2.148 secs
$

我们也可以改变项目默认的build文件名。首先,我们在项目根目录下创建项目设置文件settings.gradle。 在settings.gradle文件中,我们给rootProject修改属性值buildFileName

1
2
3
// File: settings.gradle
// Change default build file name for this project.
rootProject.buildFileName = 'sample.gradle'

现在,我们执行sample.gradle中的任务,不再需要使用命令行选项-b 或者 –build-file了。

1
2
3
4
5
6
7
8
$ gradle
:sample
Sample task
BUILD SUCCESSFUL
Total time: 3.312 secs
$

作者:snowdream
Email:yanghui1986527#gmail.com
Github: https://github.com/snowdream
QQ 群: 529327615
原文地址:https://snowdream.github.io/blog/2016/10/22/android-optimalize-series-log/

简介

在Android应用开发过程中,通过Log类输出日志是一种很重要的调试手段。
大家对于Log类的使用,一般会形成几点共识:

  1. 在Debug模式下打印日志,在Release模式下不打印日志
  2. 避免滥用Log类进行输出日志。因为这样可能造成日志刷屏,淹没真正有用的日志。
  3. 封装Log类,以提供同时输出日志到文件等功能

具体细化为以下几点建议:

  1. 禁用System.out.println
    Android应用中,一般通过封装过的Log类来输出日志,方便控制。而System.out.println是标准的Java输出方法,使用不当,可能造成Release模式下输出日志的结果。
  2. 禁用e.printStackTrace
    禁用理由同上
    建议通过封装过的Log类来输出异常堆栈信息

  3. Debug模式下,通过一个静态变量,控制日志的显示隐藏。
    我一般习惯直接使用BuildConfig.DEBUG,当然,你也可以自己定义一个。

1
2
3
4
5
6
7
private static final boolean isDebug = BuildConfig.DEBUG;
public static void i(String tag, String msg) {
if (isDebug) {
Log.i(tag, msg);
}
}

4.Release模式下,通过Proguard配置来移除日志
在Proguard配置文件中,确保没有添加 –dontoptimize选项 来禁用优化的前提下,
添加以下代码:

1
2
3
4
5
6
7
8
9
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** e(...);
public static *** i(...);
public static *** v(...);
public static *** println(...);
public static *** w(...);
public static *** wtf(...);
}

那么,是否我们按照上面的做,就真的一劳永逸呢?
我的脑海中浮现出几个相关问题:

  1. Proguard配置中添加的配置,真的可以在Release模式下,移除日志吗?
  2. 如果我们用的是封装过的Log工具类,应该怎么配置?
  3. 移除日志后,原来在日志方法中的拼接字符串参数,是否还会申请/占用内存?

本着大胆假设,小心求证的原则,下面我们通过实践来探索上面的问题答案。

本文基于以下项目进行测试实践:
https://github.com/snowdream/test/tree/master/android/test/logtest
反编译工具:JD-GUI

验证Proguard配置清理日志的有效性

CASE

1
2
3
4
Log.i(TAG,"这样使用,得到的LOGTAG的值就是DroidSettings," +
"然而并非如此,当DroidSettings这个类进行了混淆之后,类名变成了类似a,b,c这样的名称," +
"LOGTAG则不再是DroidSettings这个值了。这样可能造成的问题就是,内部混淆有日志的包,我们去过滤DroidSettings " +
"却永远得不到任何信息。");

在添加上述Proguard配置前后,编译打包Release模式的正式包,使用JD-GUI进行反编译,对比上述代码的编译后代码。

结果

添加配置前
[case1-a]

添加配置后
[case1-b]

结论

通过比对结果,我们可以得出结论:
通过添加Proguard配置,可以在Release模式下,移除掉日志。

验证封装过的Log工具类,是否有必要进行而外配置

CASE

1
2
3
4
LogUtil.i(TAG,"这样使用,得到的LOGTAG的值就是DroidSettings," +
"然而并非如此,当DroidSettings这个类进行了混淆之后,类名变成了类似a,b,c这样的名称," +
"LOGTAG则不再是DroidSettings这个值了。这样可能造成的问题就是,内部混淆有日志的包,我们去过滤DroidSettings " +
"却永远得不到任何信息。");

在添加上述Proguard配置前后,编译打包Release模式的正式包,使用JD-GUI进行反编译,对比上述代码的编译后代码。

结果

添加配置前
[case2-a]

添加配置后
[case2-b]

结论

通过比对结果,我们可以得出结论:
在这种简单封装的情况下,我们不需要额外的配置,也可以将封装过的Log工具类调用日志一起移除。

当然,实际使用过程中,可能封装更复杂。为了保险起见,可以也添加上Log工具类的配置。示例如下:

1
2
3
4
5
6
7
-assumenosideeffects class com.github.snowdream.logtest.LogUtil {
public static *** d(...);
public static *** e(...);
public static *** i(...);
public static *** v(...);
public static *** w(...);
}

验证移除日志后,字符串拼接是否还存在?

CASE

1
2
3
4
5
6
7
8
9
Log.i(TAG,"这样使用,得到的LOGTAG的值就是DroidSettings," +
"然而并非如此,当DroidSettings这个类进行了混淆之后,类名变成了类似a,b,c这样的名称," +
"LOGTAG则不再是DroidSettings这个值了。这样可能造成的问题就是,内部混淆有日志的包,我们去过滤DroidSettings " +
"却永远得不到任何信息。");
Log.i(TAG, "这样使用,得到的LOGTAG的值就是DroidSettings," +
"然而并非如此,当DroidSettings这个类进行了混淆之后,类名变成了类似a,b,c这样的名称," +
"LOGTAG则不再是DroidSettings这个值了。这样可能造成的问题就是,内部混淆有日志的包,我们去过滤DroidSettings " +
"却永远得不到任何信息。" + index ++);

上面代码的区别是:
前面是简单的字符串相加,而后面是字符串和变量的相加
在添加上述Proguard配置的前提下,分别针对以上两段代码,编译打包Release模式的正式包,使用JD-GUI进行反编译,对比上述代码的编译后代码。

结果

简单字符串相加
[case1-b]

字符串和变量相加
[case3.png]

结论

通过比对结果,我们可以得出结论:
如果只是简单字符串相加,是会彻底移除的,并且字符串拼接也不见了,不会占用内存。
而如果是字符串和变量相加,日志会移除,但是字符串拼接还在,还会占用内存。

验证日志中使用函返回值的情况

CASE

1
2
3
4
5
6
7
8
9
10
11
LogUtil.i(TAG, getMessage());
LogUtil.i(TAG, "FROM FUNCTION " + getMessage());
private String getMessage() {
return "这样使用,得到的LOGTAG的值就是DroidSettings," +
"然而并非如此,当DroidSettings这个类进行了混淆之后,类名变成了类似a,b,c这样的名称," +
"LOGTAG则不再是DroidSettings这个值了。这样可能造成的问题就是,内部混淆有日志的包,我们去过滤DroidSettings " +
"却永远得不到任何信息。";
}

上面代码的区别是:
前面是直接使用函数返回值,而后面是字符串和函数返回值的相加
在添加上述Proguard配置的前提下,分别针对以上两段代码,编译打包Release模式的正式包,使用JD-GUI进行反编译,对比上述代码的编译后代码。

结果

直接使用函数返回值
[case1-b]

字符串和函数返回值相加
[case4.png]

结论

通过比对结果,我们可以得出结论:
以上两种场景下,日志移除,拼接字符串不在了,也不会占用内存。

经过大量实践后的结论

如果你以为上面就是全部真相的话,就错了。
经过大量的测试实践,实际上真相更复杂。

以下是开启Proguard前提下,各种情况下的测试结论:

  1. Log.i(简单字符串)
  2. Log.i(局部变量)
  3. Log.i(成员变量)
  4. Log.i(简单字符串+局部变量)
    以上四种情况,日志被彻底移除,不会额外增加内存。
  5. Log.i(简单字符串+成员变量)
    日志被移除,但是字符串拼接会存在,并占用内存。
  6. Log.i(成员函数) 其中,成员函数返回值为: 简单字符串
  7. Log.i(成员函数) 其中,成员函数返回值为: 简单字符串+局部变量
    以上两种情况,日志被彻底移除,不会额外增加内存。
  8. Log.i(成员函数) 其中,成员函数返回值为: 简单字符串+成员变量
    日志被移除,但是字符串拼接会存在,并占用内存。

注:以上所有情况,参数都是指第二个或者后面的参数。第一个参数,我都使用了静态成员变量:
private static final String TAG = MainActivity.class.getSimpleName();

优化建议

  1. 确保没有开启 –dontoptimize选项的前提下,添加Proguard优化日志配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    -assumenosideeffects class android.util.Log {
    public static *** d(...);
    public static *** e(...);
    public static *** i(...);
    public static *** v(...);
    public static *** println(...);
    public static *** w(...);
    public static *** wtf(...);
    }
  2. 针对这种情况“Log.i(成员函数) 其中,成员函数返回值为: 简单字符串+成员变量”
    目前并没有办法规避,不建议这么使用。

  3. 针对这种情况”Log.i(简单字符串+成员变量)”
    我们的解决方案是,在封装的Log工具类方法中,使用变长参数。
    下面是一个简单的示例:
    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
    package com.github.snowdream.logtest;
    import android.text.TextUtils;
    import android.util.Log;
    /**
    * Created by snowdream on 16-10-22.
    */
    public class LogUtil {
    private static final boolean isDebug = BuildConfig.DEBUG;
    public static void i(String tag, String... args) {
    if (isDebug) {
    Log.i(tag, getLog(tag,args));
    }
    }
    public static void d(String tag, String... args) {
    if (isDebug) {
    Log.i(tag, getLog(tag,args));
    }
    }
    public static void v(String tag, String... args) {
    if (isDebug) {
    Log.i(tag, getLog(tag,args));
    }
    }
    public static void w(String tag, String... args) {
    if (isDebug) {
    Log.i(tag, getLog(tag,args));
    }
    }
    public static void e(String tag, String... args) {
    if (isDebug) {
    Log.i(tag, getLog(tag,args));
    }
    }
    private static String getLog(String tag, String... args){
    StringBuilder builder = new StringBuilder();
    for (String arg : args){
    if (TextUtils.isEmpty(arg)) continue;
    builder.append(arg);
    }
    return builder.toString();
    }
    }

参考

  1. 如何安全地打印日志
  2. 关于Android Log的一些思考
  3. Androrid应用打包release版时关闭log日志输出

作者:snowdream
Email:yanghui1986527#gmail.com
Github: https://github.com/snowdream
QQ 群: 529327615
原文地址:https://snowdream.github.io/blog/2016/10/20/mobile-app-develop-flow/

众所周知,移动应用一般都是按照预定周期进行迭代的,比如:一月发三版,两周发一版。

每一个迭代周期内,都会有产品,UI,研发,测试的逐步介入。

各种角色之间可能会有不同的依赖关系,他们并不能完全并行的工作。

本文主要从研发环节,以一个Android RD的视角,探索移动应用的开发流程。

名词解释

API

本文主要泛指服务端

SDK

本文主要泛指本地模块端。在模块化的背景下,移动应用被分隔成不同的模块。这些模块有可能是独立的,也有可能依赖于服务端(API)。

Client

本文主要泛指客户端

开发流程

串行开发

[串行开发流程图]

串行开发比较好理解,就是每个环节依次进行,每个环节都给出完整,可用的输出,提供给下一个环节,直至最后给测试。

优点

每个环节给出的输出都是经过充分测试,可用的成果。

缺点

每个环节要等上一个环节完成开发,之后,再进行开发。

结果往往是,下游环节的同学,在迭代初期,开发进程被阻塞,迭代后期,因为工期紧,而没日没夜的加班。

浪费时间和人力。

并行开发

[并行开发流程图]

并行开发就是API,SDK,Client各类开发人员约定接口/协议,同时进行开发。

至少分为两个阶段:

第一阶段:
上游环节和下游环节约定好接口/协议,并提供有接口但是无实现的依赖成果,输出给下游环节。

下游环节根据这些进行并行开发。

第二阶段:
上游环节完成接口/协议的实现,经过充分测试,输出完整的成果。

等待下游环节完成开发时,进行一次连接调试,确保业务流程完整,功能正常。

优点

各个环节约定好接口/协议之后,可以并行开发。

要求

这个需要各个环节的人员,进行及时,有效的沟通。

总结

根据之前的项目实践,我比较偏向于并行开发。

当然,对于移动应用开发流程的理解,大家见仁见智,欢迎讨论。

作者:snowdream
Email:yanghui1986527#gmail.com
Github: https://github.com/snowdream
QQ 群: 529327615
原文地址:https://snowdream.github.io/blog/2016/10/17/android-genymotion-install-and-settings/

注: 由snowdream收集整理

简介

Genymotion是一款基于x86架构的Android模拟器,由于系统启动速度,应用运行速度远远快于Android SDK自带模拟器而受到广泛应用。

优缺点

优点

  • 系统启动速度快
  • 应用运行速度快
  • 跨平台
  • IDE支持

缺点

  • 与真机相比,无法支持一些硬件相关的传感器特性等
  • 由于市场上大部分应用都是基于ARM架构来编译的,因此,与默认模拟器,真机相比,对于包含仅支持ARM架构的so的应用,默认不支持。

注:基于x86架构的模拟器/真机,兼容ARM指令有两个解决方案:

  1. 对于x86真机,x86处理器已经能够基本兼容ARM指令了。参考《涨姿势!x86处理器兼容ARM架构App的秘密》
  2. 对于Genymotion模拟器,则通过安装ARM_Translation_Android来进行兼容。

安装Genymotion

安装步骤

  1. 安装虚拟机VirtualBox

  2. 注册Genymotion帐号

  3. 登录,下载并安装Genymotion

安装指南

详细安装步骤,请参考以下文章:

  1. Installation
  2. Genymotion安装方法
[Genymotion效果图]

安装ARM_Translation_Android系列包

由于genymotion是基于x86的,而大部分应用都是基于ARM的,因此,我们需要安装一个ARM_Translation_Android系列包来增强兼容性。

安装步骤

  1. 点击下载ARM_Translation_Android系列包
  2. 将下载的zip包,拖进Genymotion模拟器窗口,按照提示安装
  3. 安装成功后,重启Genymotion模拟器即可。

安装指南

  1. Genymotion with Google Play Services
  2. Use ARM Translation on 5.x image
  3. Use ARM Translation on 6.x image

注:以上步骤,便可满足大部分的开发测试需求。以下的步骤,都是可选步骤。

下面是安装微信的效果

[wechat]

安装Google Apps

  1. 根据平台,android版本等选择不同的安装包,下载。
    http://opengapps.org/
    https://github.com/opengapps/opengapps
  2. 将下载的zip包,拖进Genymotion模拟器窗口,按照提示安装
  3. 安装成功后,重启Genymotion模拟器即可。

安装Xposed

  1. 根据平台,android版本等选择不同的安装包,下载。
    http://dl-xda.xposed.info/framework/
    其中,sdk21,sdk22,sdk23,分别对应Android 5.0,5.1, 6.0.
  2. 将下载的zip包,拖进Genymotion模拟器窗口,按照提示安装
  3. 安装成功后,重启Genymotion模拟器即可。
  4. 对于Android 5.0以上的手机,请前往XDA论坛主题贴下载附件 XposedInstaller_3.0_alpha4.apk,并安装。
    下载地址:http://forum.xda-developers.com/showthread.php?t=3034811
    如果你看到以下界面,恭喜你,Xposed Framework安装完成。
[xposed3]

参考

  1. Android模拟器Genymotion
  2. Genymotion安装方法
  3. 快到极致的 Android 模拟器Genymotion
  4. Genymotion那点事儿
  5. Xposed 官网
  6. Xposed XDA论坛
  7. [OFFICIAL] Xposed for Lollipop/Marshmallow [Android 5.0/5.1/6.0, v86, 2016/10/16]
  8. Xposed框架的安装
  9. Genymotion with Google Play Services
  10. Use ARM Translation on 5.x image
  11. Use ARM Translation on 6.x image

作者:snowdream
Email:yanghui1986527#gmail.com
Github: https://github.com/snowdream
QQ 群: 529327615
原文地址:https://snowdream.github.io/blog/2016/10/11/android-secret-codes/

注: 本文根据《Android-SecretCodes》翻译整理。

什么是暗码?

在android系统中,暗码就是类似这种样式的字符串: *#*#<code>#*#*

如果这样的系统暗码执行,系统会触发下面的方法:(来自 AOSP Android Open Source Project)

1
2
3
4
5
6
7
8
9
10
11
static private boolean handleSecretCode(Context context, String input) {
int len = input.length();
if (len > 8 && input.startsWith("*#*#") && input.endsWith("#*#*")) {
Intent intent = new Intent(TelephonyIntents.SECRET_CODE_ACTION,
Uri.parse("android_secret_code://" + input.substring(4, len - 4)));
context.sendBroadcast(intent);
return true;
}
return false;
}

如何运行暗码?

有两种方式可以执行运行暗码:

直接在手机电话拨号界面输入暗码,例如:*#*#123456789#*#*

或者直接在代码中进行调用。

1
2
3
4
String secretCode = "123456789";
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:*#*#" + secretCode + "#*#*"));
startActivity(intent);
1
2
3
4
5
String secretCode = "123456789";
String action = "android.provider.Telephony.SECRET_CODE";
Uri uri = Uri.parse("android_secret_code://" + secretCode);
Intent intent = new Intent(action, uri);
sendBroadcast(intent);

如何创建自定义暗码?

在你的应用AndroidManifest.xml文件中,增加以下代码,用来定义手机暗码。

不管什么时候暗码 *#*#123456789#*#* 被触发,你都将收到对应的广播。

1
2
3
4
5
6
<receiver android:name=".MySecretCodeReceiver">
<intent-filter>
<action android:name="android.provider.Telephony.SECRET_CODE" />
<data android:scheme="android_secret_code" android:host="123456789" />
</intent-filter>
</receiver>

参考

  1. Android Secret Codes
  2. 话说android系统之暗码

作者:snowdream
Email:yanghui1986527#gmail.com
Github: https://github.com/snowdream
QQ 群: 529327615
原文地址:https://snowdream.github.io/blog/2016/09/02/android-develop-xposed-module/

注: 根据Development tutorial 整理完成

创建Android项目

如果准备从零开始创建Xposed模块,首先应该创建一个Android应用工程。

引入 Xposed Framework API

app/build.gradle文件中声明Xposed Framework API 的jar包依赖。

1
2
3
4
5
6
7
8
9
repositories {
jcenter();
}
dependencies {
provided 'de.robv.android.xposed:api:82'
//如果需要引入文档,方便查看的话
provided 'de.robv.android.xposed:api:82:sources'
}

说明:

  1. 请留意,这个82是Xposed Framework API的版本号,叫做xposedminversion。
  2. xposedminversion可以在这里进行查询:
    https://bintray.com/rovo89/de.robv.android.xposed/api
  3. Xposed Framework API文档请参考:http://api.xposed.info/reference/packages.html

修改AndroidManifest.xml

在AndroidManifest.xml文件中添加以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.robv.android.xposed.mods.tutorial"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="15" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="Easy example which makes the status bar clock red and adds a smiley" />
<meta-data
android:name="xposedminversion"
android:value="53" />
</application>
</manifest>

说明:

  1. xposedmodule: 一般设置为true,表示这是一个xposed模块
  2. xposeddescription: 一句话描述该模块的用途,可以引用string.xml中的字符串
  3. xposedminversion: 没错,这个就是上面提到的xposedminversion。我理解为要求支持的Xposed Framework最低版本。

模块实现

创建一个或者几个类,并实现IXposedHookLoadPackage,IXposedHookZygoteInit或者其他IXposedMod的子接口。

1
2
3
4
5
6
7
8
9
10
11
package de.robv.android.xposed.mods.tutorial;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
public class Tutorial implements IXposedHookLoadPackage {
public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
XposedBridge.log("Loaded app: " + lpparam.packageName);
}
}

注: XposedBridge.log会将日志输出到logcat,并写入日志文件”/data/data/de.robv.android.xposed.installer/log/debug.log”.

好了,现在可以开始Hook了。
大部分的Hook工作,主要通过XposedHelpers类的一些辅助函数来实现。比如:findAndHookMethod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package de.robv.android.xposed.mods.tutorial;
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
public class Tutorial implements IXposedHookLoadPackage {
public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
if (!lpparam.packageName.equals("com.android.systemui"))
return;
findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "updateClock", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
// this will be called before the clock was updated by the original method
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
// this will be called after the clock was updated by the original method
}
});
}
}

注:根据名称不难发现,beforeHookedMethod/afterHookedMethod会在被Hook函数之前/之后执行。

关于使用Xposed来进行Hook的更多知识,这里就不展开了。大家可以参考以下两篇文章:

  1. Helpers
  2. android-hook框架xposed篇

声明实现

在assets目录下创建一个空文件,命名为xposed_init。
在这个文件中,每一行记录一个类的完整路径,来声明实现类。
在这里,我们声明 “de.robv.android.xposed.mods.tutorial.Tutorial”

好了,到这里,一个简单的Xposed模块应用项目就构建好了。

模块安装与使用

  1. 将这个工程,编译,打包,安装到已经支持Xposed的手机中。
  2. 打开Xposed Installer应用,切换到模块界面,你可以看到你开发的Xposed模块。
  3. 通过勾选/取消,来启用/禁用模块。然后,重启手机,进行生效。

模块发布

Xposed模块开发完成后,你可以按照以下步骤发布分享。

  1. 你首先需要一个XDA论坛帐号。如果没有,请前往论坛注册:
    http://forum.xda-developers.com/
  2. 使用XDA论坛帐号,登录xposed官网,按照操作提示进行发布。
    http://repo.xposed.info/

Xposed模块开发优势和不足

优势

  1. 功能强大,既可以修改系统应用,也可以修改其他应用。hook android,hook everything.
  2. 使用灵活,既可以针对一款应用进行Hook,也可以针对所有应用进行Hook。

不足

  1. 无法调试。只能通过打印日志进行跟踪。(例如:XposedBridge.log)
  2. 无法即时生效。启用/禁用模块,你需要重启手机。
  3. multidex支持不足。详见Multidex support

Xposed模块开发实例

Xposed-Keylogger的基础上,我稍作修改,制作了一个Xposed模块开发实例。
这个模块的作用就是监听键盘按键,记录所有你设置到EditText控件的字符串。

建议

Xposed是如此的强大,因此,建议重视手机安全的用户,坚决不要root,不要安装xposed,发烧友,土豪随意。

参考

  1. Xposed 官网
  2. Xposed XDA论坛
  3. Development tutorial
  4. Helpers
  5. Replacing resources
  6. Using the Xposed Framework API
  7. DingDingUnrecalled
  8. FakeXX
  9. WechatLuckyMoney
  10. Xposed Framework API
  11. Xposed Framework API in bintray
  12. android-hook框架xposed篇
  13. JustTrustMe

Fork me on GitHub