作者: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

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

简介

提到Xposed框架时,人们总会用到一个词“神器”。
是的,安装Xposed后,我们似乎脑洞大开,以前不能干的事件,现在都能干了。
对此,我的理解是:hook android,hook everything

Xposed框架是什么???

官方对此的解释是这样的:
“Xposed是一个适用于Android的框架。基于这个框架开发的模块可以改变系统和app应用的行为,而不需要修改APK。这是一个很棒的特性,意味着Xposed模块可以不经过任何修改,安装在各种不同的ROM上。Xposed模块可以很容易的开启和关闭。你只需要激活或者禁用Xposed模块,然后重启手机即可。”

在手机发烧友的眼中,Xposed是这样子的:

修改手机主题,权限控制,阻止广告,禁用各种APP滥用权限,微信,游戏等相关的各种外挂…

在开发者的眼中,Xposed是这样子的:

渗透测试,测试数据构造,环境监控,动态埋点,热补丁,自动化录制…

关于Xposed框架的基本原理以及更多介绍,请参考文末链接,或者自行百度。

风险声明

在安装Xposed框架之前,我必须把风险告诉你:

  1. 软变砖
  2. 无限重启

简单解释下:

  1. 软砖: 手机能启动,但是进不去桌面
  2. 硬砖/黑砖: 手机在按电源键,或者连接电脑没反应,一直黑屏。
  3. 软砖可以救 硬砖只能修。
  4. 无限重启: 就是手机快要进入桌面的时候,又自动重启。周而复始,无限重启。

根据官方的警示和网友的反馈, 三星的手机,以及索尼,戴尔的部分手机 容易导致以上风险。

安装

Xposed框架的安装需要经过root,安装第三方Recovery,安装Xposed框架,安装Xposed Installer等几个步骤。这些步骤都是依次进行的,任何步骤的失败,都会导致Xposed框架的安装过程中止。

因此,建议在 国际国内的主流Android机型 上进行安装。

Root

根据我的个人实践,这里我推荐使用 KingRoot 这款工具进行Root。

官方网址: https://kingroot.net/?myLocale=zh_CN

Root之前,我建议你查询下,你的机型是否被支持: https://kingroot.net/model

TWRP

对于Android 5.0以上的手机,官方提示,必须要先刷入第三方Recovery, 比如: TWRP

官方网址: https://twrp.me/

刷机之前,请先查询下,你的机型是否被支持:
https://twrp.me/Devices/

以Nexus 5 为例, 网站有详细的操作指南。https://twrp.me/devices/lgnexus5.html

当然有些非主流手机,也可以在相关论坛找到TWRP的修改版本。

比如我的手机,中兴 Blade A1(C880U) 16G 灵动白 移动4G手机 双卡双待

我就是参考:中兴小鲜3中兴Blade a1移动版全网通版本TWRP刷写教程@root

按照 TWRP for ZTE Blade Apex 2 强行刷入的。

刷机完成后,重启可以进入Recovery界面。

Xposed Framework

下载

Xposed Framework下载地址:http://dl-xda.xposed.info/framework/

其中,sdk21,sdk22,sdk23,分别对应Android 5.0,5.1, 6.0.
根据,手机ROM版本和处理器类型选择Xposed Framework刷机包。

比如,中兴Blade a1移动版(5.1, arm64),我选择了刷机包xposed-v86-sdk22-arm64.zip 和卸载包xposed-uninstaller-20150831-arm64.zip

下载之后,将这两个压缩包,拷贝到SD卡根目录下。

安装

  1. 重启手机,进入Recovery界面。(adb reboot recovery)
  2. 选择【安装刷机包】进入下级页面,选择【从SD卡选择ZIP文件】
  3. 在SD卡根目录找到Xposed Framework刷机包(xposed-v86-sdk22-arm64.zip),并选择。
  4. 滑动底部的滑动条,确认刷入,等待提示刷机完成。
  5. 重启手机,等待进入桌面。

卸载

如果刷入Xposed Framework刷机包之后,无限重启,进不去桌面怎么办?
那就按照下面提示,卸载掉Xposed Framework。

  1. 重启手机,进入Recovery界面。(adb reboot recovery)
  2. 选择【安装刷机包】进入下级页面,选择【从SD卡选择ZIP文件】
  3. 在SD卡根目录找到Xposed Framework卸载刷机包(xposed-uninstaller-20150831-arm64.zip),并选择。
  4. 滑动底部的滑动条,确认刷入,等待提示刷机完成。
  5. 重启手机,等待进入桌面。

Xposed Installer

这是一个管理Xposed模块的官方应用。通过它,你可以随时禁用或者启用Xposed模块,然后重启手机。

对于Android 5.0以上的手机,请前往XDA论坛主题贴下载附件 XposedInstaller_3.0_alpha4.apk,并安装。

下载地址:http://forum.xda-developers.com/showthread.php?t=3034811

如果你看到以下界面,恭喜你,Xposed Framework安装完成。

FAQ

  1. Xposed FAQ / Known issues
  2. Xposed in zhihu
  3. Xposed in Stackoverflow

参考

  1. Xposed 官网
  2. Xposed XDA论坛
  3. [OFFICIAL] Xposed for Lollipop/Marshmallow [Android 5.0/5.1/6.0, v86, 2016/07/08]
  4. Xposed framework 作者rovo89 原文(xda)介绍大译
  5. Xposed:不得不说的 Android 神器
  6. Android 系统上的 Xposed 框架中都有哪些值得推荐的模块?
  7. xposed模块整理
  8. 基于Xposed修改微信运动步数
  9. 用黑客思维做测试——神器 Xposed 框架介绍
  10. 安卓注入框架Xposed分析与简单应用
  11. Xposed框架初体验

作者:snowdream
Email:yanghui1986527#gmail.com
Github: https://github.com/snowdream
QQ 群: 529327615
原文地址:https://snowdream.github.io/blog/2016/08/24/android-incremental-update-solutions-flowchart/

今天,我对Android 应用增量升级方案的流程进行了一个梳理,简单画了一个流程图。

流程图源文件: [update.graphml]

请使用yEd工具打开浏览。https://www.yworks.com/products/yed/download

作者:snowdream
Email:yanghui1986527#gmail.com
QQ 群: 529327615
原文地址:https://snowdream.github.io/blog/2016/08/23/android-incremental-update-solutions/

名词解释

全量升级

每次下载完整的新安装包,进行覆盖安装。

增量升级

将新安装包和已经安装的旧安装包进行比对,生成一个差分升级包(Patch包)。用户下载patch包后,和已经安装的旧安装包进行合并,生成新安装包,再进行覆盖安装。

背景

在早期的Android应用开发中,由于android应用普遍比较小,因此,普遍采用了全量升级方案。简单粗暴,却行之有效。
但是,随着Android的发展,Android应用功能越来越多,体积越来越大,再综合以下几个因素考虑,全量升级方案逐渐无法满足我们的需求。

  1. 在国内,随着2G,3G,4G的逐步演进,手机网络越来越快,但有一点事实仍然没有改变:流量很贵,非常不够用。(这个因素不适合WIFI用户和土豪用户) 以北京移动为例,最基础套餐,5元30M流量。而最新的微信APK安装包35M。也就是说,如果你选了最基础套餐,你一个月内使用全量升级方案,升级一次微信,流量都不够用。
  2. 在敏捷开发大行其道的今天,开发者希望尽快将新开发的功能推送到用户面前,并及时得到用户的反馈。恨不能三天一小版,一周一大版。

综合以上因素,开发者必须为用户考虑:省流量,省流量,省流量。

显然,全量升级这种土豪做法已经不再适用,于是,增量升级应运而生。

增量升级原理

首先,两句话简单概括增量升级原理:

  1. 服务端通过比对最新升级包,和当前应用包,生成差分升级包;
  2. 客户端将差分升级包和当前应用包合并,生成最新升级包。

接下来,简单介绍下增量升级的原理:

  1. 首先,客户端获取当前应用的Version Code和应用APK文件的MD5值,发送给服务器;
  2. 服务器根据既定策略,给用户返回更新包信息。
    1. 通过MD5值没有查询到旧有APK应用信息,返回全量升级包网址,全量升级包MD5值;
    2. 需要返回patch包,但还没有生成patch包时,后台去生成patch包,并返回全量升级包网址,全量升级包MD5值;
    3. 需要返回patch包,并且已经生成patch包时,返回patch包网址,patch包MD5值,全量升级包网址,全量升级包MD5值;
    4. 不需要返回patch包,则返回全量升级包网址,全量升级包MD5值;
  3. 客户端根据返回信息进行更新操作。
    1. 如果只有全量升级包相关信息,则下载全量升级包,并在校验MD5值后,安装更新包;
    2. 如果有差分升级包(patch包),则下载差分升级包。校验差分升级包的MD5值。如果校验失败,走上面一个步骤。如果校验成功,则将差分升级包和当前版本的APK进行合并操作,生成新的应用包。校验新的应用包MD5值。校验通过,这安装这个生成的新应用包。如果校验失败,则走上面一个步骤。

以上只是简单介绍了增量升级的原理,实际应用中还需要细化,考虑更多的场景。

注意: 下载过程中,必须支持断点续传策略。防止网络不畅时,不断重试,造成的流量浪费。

增量升级方案

增量升级方案的核心就是使用Diff/Patch算法来对新旧APK进行diff/patch操作。
目前主流的Diff/Patch算法是bsdiff/bspatch,来自:http://www.daemonology.net/bsdiff/

另外,我了解到,国内有人开源了另外一种Diff/Patch算法,名字叫做HDiffPatch,来自:https://github.com/sisong/HDiffPatch

据说,比bsdiff/bspatch更高效呢?详见《HDiff1.0和BSDiff4.3的对比测试》

我将bsdiff/bspatch和HDiffPatch,使用jni封装成so库,供android调用。项目地址: https://github.com/snowdream/android-diffpatch
在封装HDiffPatch过程中遇到问题,得到作者@sisong的支持和帮助,在此表示感谢。

bsdiff/bspatch和HDiffPatch算法都是开源的,服务端可以根据源文件来进行编译集成。
这里我主要在Android客户端的角度,介绍下bsdiff/bspatch和HDiffPatch怎么使用。

BsDiffPatch

  1. 在build.gradle文件中自定义jnilib目录

    1
    2
    3
    4
    5
    sourceSets {
    main {
    jniLibs.srcDirs = ['libs']
    }
    }
  2. app/libs/armeabi-v7a/libbsdiffpatch.so 拷贝到你的工程对应目录下。

  3. app/src/main/java/com/github/snowdream/bsdiffpatchapp/src/main/java/com/github/snowdream/diffpatch 拷贝到你的工程对应目录下,包名和文件名都不能改变。
  4. 在Java文件中参考以下代码进行调用。
    1
    2
    3
    4
    5
    6
    IDiffPatch bsDiffPatch = new BSDiffPatch();
    bsDiffPatch.init(getApplicationContext());
    //diff
    bsDiffPatch.diff(oldFilePath, newFilePath, diffFilePath);
    //patch
    bsDiffPatch.patch(oldFilePath, diffFilePath, gennewFilePath);

HDiffPatch

  1. 在build.gradle文件中自定义jnilib目录

    1
    2
    3
    4
    5
    sourceSets {
    main {
    jniLibs.srcDirs = ['libs']
    }
    }
  2. app/libs/armeabi-v7a/libhdiffpatch.so 拷贝到你的工程对应目录下。

  3. app/src/main/java/com/github/snowdream/hdiffpatchapp/src/main/java/com/github/snowdream/diffpatch 拷贝到你的工程对应目录下,包名和文件名都不能改变。
  4. 在Java文件中参考以下代码进行调用。
    1
    2
    3
    4
    5
    6
    IDiffPatch hDiffPatch = new HDiffPatch();
    hDiffPatch.init(getApplicationContext());
    //diff
    hDiffPatch.diff(oldFilePath, newFilePath, diffFilePath);
    //patch
    hDiffPatch.patch(oldFilePath, diffFilePath, gennewFilePath);

BsDiffPatch vs HDiffPatch

这里我选择高德地图Android客户端的两个版本来进行测试。

  • 测试来源:http://www.autonavi.com/
  • 测试版本: Amap_Android_V7.7.4.2128_GuanWang.apk 和 Amap_Android_V7.3.0.2036_GuanWang.apk (注:版本跨度大,差异大)
  • 对比算法: BsDiffPatch vs HDiffPatch
  • 测试结果:(详见下图)
    1. BsDiffPatch生成的patch包稍小。
    2. 两者的diff操作都非常耗资源,耗时间,无法忍受。(当然diff操作一般在服务端进行)
    3. 两者的patch操作都比较快。通过大概五次测试,BsDiffPatch的patch操作需要13s左右,而HDiffPatch的patch操作仅仅需要1s左右。

以上结果仅供参考。

  • 测试结论:
    1. BsDiffPatch应用更广泛
    2. HDiffPatch看起来更高效

扩展

以上算是比较成熟的增量升级方案了,但是仔细想想,可能还存在一些问题:

  1. 由于多渠道,多版本造成非常多Patch包
  2. bs diff/patch算法性能和内存开销太高
    第一个问题可以通过服务器策略进行限制。比如,只有最新版5个版本内的升级采用增量升级,其他的仍然采用全量升级。
    据说,还有一种更强大的算法,可以解决以上问题。大家有兴趣的话,可以自己去了解。
    crsync-基于rsync rolling算法的文件增量更新.md

参考

  1. 友盟增量更新的原理是什么
  2. Android应用增量更新库(Smart App Updates)
  3. Android实现应用的增量更新\升级
  4. https://github.com/smuyyh/IncrementallyUpdate
  5. 浅析android应用增量升级
  6. https://github.com/cundong/SmartAppUpdates
  7. crsync-基于rsync rolling算法的文件增量更新.md
  8. https://github.com/sisong/HDiffPatch
  9. http://www.daemonology.net/bsdiff/
  10. https://github.com/snowdream/android-diffpatch

作者:snowdream
Email:yanghui1986527#gmail.com
QQ 群: 529327615
原文地址:https://snowdream.github.io/blog/2016/08/13/android-develop-with-kotlin/

目标

本文旨在引导开发者使用Kotlin来开发Android应用。

至于Kotlin语言的语法和教程等,不在本文讨论范围,请参考以下官网文档和网上的开发教程。

  1. kotlin-android
  2. 《Kotlin for android Developers》中文翻译
  3. Kotlin-in-Chinese
  4. Kotlin 官方参考文档 中文版
  5. Kotlin 官方文档中文翻译版

简介

名词解释

Kotlin

Kotlin 是一个基于 JVM 的新的编程语言,由 JetBrains 开发。
Kotlin可以编译成Java字节码,也可以编译成JavaScript,方便在没有JVM的设备上运行。
JetBrains,作为目前广受欢迎的Java IDE IntelliJ 的提供商,在 Apache 许可下已经开源其Kotlin 编程语言。

官方网站:http://kotlinlang.org/

Github仓库: https://github.com/JetBrains/kotlin

教程

本节介绍如何使用Kotlin开发android应用。

以下几点需要谨记:

  1. 所有Kotlin类文件,以.kt为后缀。
  2. Kotlin的源码目录规则和默认的是一样的。分别放在src/main/kotlin, src/test/kotlin, src/androidTest/kotlin 和任意的src/${buildVariant}/kotlin。

Kotlin and Java

使用Kotlin来开发android,需要经过以下几个步骤进行配置。
1.在项目根目录下的build.gradle文件中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
buildscript {
ext.kotlin_version = '1.0.1-2'

repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

2.在模块目录下的build.gradle文件中添加以下代码:

1
2
3
4
5
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

3.配置完成,你可以在src/main/kotlin目录下愉快地使用Kotlin来写Android应用了。

实例展示:

  1. https://github.com/JetBrains/kotlin-examples
  2. https://github.com/snowdream/test/tree/master/android/kotlin/HelloWorld

Java 2 Kotlin

上面是手动给android项目增加kotlin支持。
其实还有一种自动转换的方法,也可以添加kotlin支持。

  1. 通过菜单“ Help | Find Action”或者快捷键“Ctrl+Shift+A”调出动作查询窗口
  2. 输入”Configure Kotlin in Project”,回车,按照提示操作,即可添加Kotlin配置。
  3. 重复第一步,调出动作查询窗口。输入“Convert Java File to Kotlin File”。即可将现有的Java文件自动转换成Kotlin文件。当然,如果只想转换某一个java文件,方法就是,打开改Java文件,然后选择菜单“ Code | Convert Java File to Kotlin File”,即可将当前打开的Java文件自动转换成Kotlin文件。
  4. 转换完成。

总结

根据Kotlin官网描述,Kotlin是一种适用于JVM,Android
根据个人的开发实践,总结出使用Kotlin开发Android应用的优缺点:

优点

  1. 和Java相比,更简洁,更安全。
  2. 和Java无缝集成,官网宣称kotlin可以100%和java混合使用。
  3. 由jetbrains推出,Idea可以更好的进行支持。

缺点

  1. 会将支持kotlin的相关jar包打散,打包到apk中。这部分内容最终会给apk增加700k左右的大小。这个和前面的groovy相比,情况要好很多,勉强还是可以接受的。
  2. 和java相比,使用Kotlin的开发者还太少。
  3. 诞生时间较晚,有待时间的检验。

结论

  1. 使用Kotlin是可以更快,更有效地开发Android应用的。
  2. 在应用于生产实践之前,还需要更多的评估,包括稳定性,运行效率,耗电量,兼容性,研发的接受程度等。

参考

  1. Kotlin名词解释
  2. Kotlin官网
  3. kotlins-android-roadmap
  4. Getting started with Android and Kotlin
  5. Kotlin Android Extensions
  6. kotlin-examples
  7. HelloWorld
  8. 如何评价 Kotlin 语言
  9. Kotlin:Android世界的Swift
  10. 初见Kotlin
  11. Android开发中使用kotlin你遇到过哪些坑?

作者:snowdream
Email:yanghui1986527#gmail.com
QQ 群: 529327615
原文地址:https://snowdream.github.io/blog/2016/08/12/android-develop-with-groovy/

目标

本文旨在引导开发者使用Groovy来开发Android应用。

简介

名词解释

Groovy

Groovy是一种基于JVM(Java虚拟机)的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码。由于其运行在 JVM 上的特性,Groovy 可以使用其他 Java 语言编写的库。

官方网站:http://www.groovy-lang.org/

Github仓库:https://github.com/apache/groovy

Gradle

Gradle是一个开源的自动化构建工具,主要用于持续发布(Continuous Delivery)。
Gradle主要基于Java和Groovy开发,用于支持多种开发语言和开发平台的自动化构建,包括Java, Scala, Android, C/C++, 和 Groovy, 并且可以无缝集成在Eclipse, IntelliJ, and Jenkins等开发工具和持续集成服务中。

官方网站:https://gradle.org/

Github仓库:https://github.com/gradle/gradle

Groovy with Android

groovy-android-gradle-plugin是一款由groovy推出的插件,主要用于支持使用groovy来开发android应用。

Github仓库: groovy-android-gradle-plugin

实例展示:
https://github.com/snowdream/test/tree/master/android/groovy/HelloWorld

教程

本节介绍使用Groovy开发android应用,主要分两种:
一种是只使用Groovy来开发,另外一种是混合使用Groovy和Java来开发。

至于Groovy语言的语法和教程等,不在本文讨论范围,请参考官网文档和网上的开发教程。

以下几点需要谨记:

  1. 所有Groovy类文件,以.groovy为后缀。
  2. 任何新建的Groovy类,请在类的头部加上注解:@CompileStatic .具体原因请移步:Melix’s blog here for more technical details
  3. Groovy的源码目录规则和默认的是一样的。分别放在src/main/groovy, src/test/groovy, src/androidTest/groovy 和任意的src/${buildVariant}/groovy。
  4. Groovy的源码目录可以在build.gradle文件中自定义,定义规则如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    androidGroovy {
    sourceSets {
    main {
    groovy {
    srcDirs += 'src/main/java'
    }
    }
    }
    }

为了让Android Studio识别这些自定义目录为源码目录,可能还需要在android插件的sourceSets中再添加一遍。

Groovy

仅仅使用Groovy来开发android,步骤比较简单。
1.在项目根目录下的build.gradle文件中添加以下代码:

1
2
3
4
5
6
7
8
9
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.2'
classpath 'org.codehaus.groovy:groovy-android-gradle-plugin:1.0.0'
}
}

2.在模块目录下的build.gradle文件中添加以下代码:

1
2
3
4
apply plugin: 'groovyx.android'
dependencies {
compile 'org.codehaus.groovy:groovy:2.4.6:grooid'
}

3.配置完成,你可以在src/main/groovy目录下愉快地使用Groovy来写Android应用了。

实例展示:https://github.com/groovy/groovy-android-gradle-plugin/tree/master/groovy-android-sample-app

Groovy and Java

仅仅使用Groovy来开发android,步骤也比较简单。

1.这一步,和上面的一样。
2.在模块目录下的build.gradle文件中添加以下代码:

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
apply plugin: 'groovyx.android'


androidGroovy {
skipJavaC = true

sourceSets {
main {
groovy {
srcDirs += 'src/main/java'
}
}
}

options {
configure(groovyOptions) {
encoding = 'UTF-8'
forkOptions.jvmArgs = ['-noverify'] // maybe necessary if you use Google Play Services
javaAnnotationProcessing = true
}
sourceCompatibility = '1.7' // as of 0.3.9 these are automatically set based off the android plugin's
targetCompatibility = '1.7'
}
}

dependencies {
compile 'org.codehaus.groovy:groovy:2.4.6:grooid'
}

3.配置完成,你既可以在src/main/groovy下写Groovy类,也可以在src/main/java下写Java类。

实例展示:https://github.com/snowdream/test/tree/master/android/groovy/HelloWorld

注意事项

1.引用使用groovy开发的lib时,需要排除groovy的jar包。

例如:引用groovy-xml库,操作如下:

1
2
3
compile ('org.codehaus.groovy:groovy-xml:2.4.3') {
exclude group: 'org.codehaus.groovy'
}

2.Groovy编译选项在androidGroovy块下的options块进行编写。

1
2
3
4
5
6
7
8
9
10
androidGroovy {
options {
configure(groovyOptions) {
encoding = 'UTF-8'
forkOptions.jvmArgs = ['-noverify'] // maybe necessary if you use Google Play Services
}
sourceCompatibility = '1.7' // as of 0.3.9 these are automatically set based off the android plugin's
targetCompatibility = '1.7'
}
}

3.如果需要在java文件中引用Groovy文件内容,需要将所有源码文件使用groovyc来编译,而不要通过javac编译。

1
2
3
androidGroovy {
skipJavaC = true
}

4.如果需要用到注解(annoation),还需要作如下设置:

1
2
3
4
5
6
7
androidGroovy {
options {
configure(groovyOptions) {
javaAnnotationProcessing = true
}
}
}

注: 这一步,首先要确保第三步已经设置skipJavaC = true.

5.如果需要用到Data Binding,需要用到一个插件https://bitbucket.org/hvisser/android-apt

更多配置,请参考: groovy-android-gradle-plugin

总结

根据个人的开发实践,总结出使用Groovy开发Android应用的优缺点:

优点

  1. 引入Groovy的诸多特性,包括闭包,函数式编程,静态编译,DSL等。
  2. 和Java无缝集成,可以平滑过渡。

缺点

  1. 会将支持Groovy的相关jar包打散,打包到apk中。这部分内容最终会给apk增加2~3M的大小。这个目前看来是硬伤,希望以后能够精简一下。
  2. 会将一些License文件打包到apk中,这个可以通过android开发插件的packagingOptions进行过滤,问题不大。
  3. 和java相比,使用Groovy的开发者还太少。
  4. 中文文档少,主要是官方英文文档。

结论

  1. 使用Groovy是可以开发Android应用的。
  2. 在应用于生产实践之前,还需要更多的评估,包括稳定性,运行效率,耗电量,兼容性,研发的接受程度等。

参考

  1. Groovy现在可运行于Android平台
  2. groovy-android-gradle-plugin
  3. groovy-android-helloworld
  4. Groovy官网
  5. Groovy名词解释
  6. Java之外,选择Scala还是Groovy?
  7. http://www.groovy-lang.org/
  8. https://gradle.org/

大家在用Gradle开发Android项目的时候,想必都知道构建过程是由一个一个的任务(Task)组成的。
那么项目中到底有那些Task呢?

1
gradle tasks --all

执行上面的命令,你会看到项目中定义的所有Task。

这些Task是怎样依赖的,构建过程中又是怎样的流程?

在实践过程中,我找到两个gradle插件,可以帮助我们实现流程的可视化。

gradle-task-tree

gradle-task-tree可以将项目下的task依赖关系以树的形式,输出到终端。

引入

1
2
3
4
5
6
7
8
9
10
11
12
buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "gradle.plugin.com.dorongold.plugins:task-tree:1.2.2"
}
}

apply plugin: "com.dorongold.task-tree"

使用

gradle taskTree –no-repeat

输出

结果会打印在终端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
:build
+--- :assemble
| \--- :jar
| \--- :classes
| +--- :compileJava
| \--- :processResources
\--- :check
\--- :test
+--- :classes
| +--- :compileJava
| \--- :processResources
\--- :testClasses
+--- :compileTestJava
| \--- :classes
| +--- :compileJava
| \--- :processResources
\--- :processTestResources

gradle-visteg

gradle-visteg可以将项目下的task依赖关系以图的形式,输出到文件。
默认是输出到.dot文件中,通过Graphviz工具可以将.dot文件转换成.png文件。

引入

1
2
3
4
5
6
7
8
9
10
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'cz.malohlava:visteg:1.0.0'
}
}

apply plugin: 'cz.malohlava.visteg'

配置

1
2
3
4
5
6
7
8
9
10
11
visteg {
enabled = true
colouredNodes = true
colouredEdges = true
destination = 'build/reports/visteg.dot'
exporter = 'dot'
colorscheme = 'spectral11'
nodeShape = 'box'
startNodeShape = 'hexagon'
endNodeShape = 'doubleoctagon'
}

详细配置请参考:https://github.com/mmalohlava/gradle-visteg

使用

执行正常的task。
默认会生成build/reports/visteg.dot文件。
在ubuntu下,可以通过xdot,直接打开该文件。

通过以下命令可以转换成png图片

1
2
cd build/reports/
dot -Tpng ./visteg.dot -o ./visteg.dot.png

输出

task流程图

参考

  1. https://github.com/dorongold/gradle-task-tree
  2. https://github.com/mmalohlava/gradle-visteg

本文旨在从实践出发,引导开发者在Android项目中进行Mock单元测试。

什么是单元测试

单元测试由一组独立的测试构成,每个测试针对软件中的一个单独的程序单元。单元测试并非检查程序单元之间是否能够合作良好,而是检查单个程序单元行为是否正确。

为什么要进行单元测试

在敏捷开发大行其道的今天,由于时间紧,任务重,过分依赖测试工程师以及下列原因,导致单元测试不被重视,在开发流程中处于一个可有可无的尴尬境地。

  1. 浪费的时间太多
  2. 软件开发人员不应参与单元测试
  3. 我是很棒的程序员,不需要进行单元测试
  4. 不管怎样,集成测试将会抓住所有的Bug
  5. 单元测试效率不高

那么单元测试是否正的可有可无呢?No! No! No!

  1. 作为android客户端研发,在一个开发周期内,你负责的需求需要Web服务(API),和本地代码(JNI,Native Code)的支持,而你们的工作是同时进行的。
  2. 你的需求开发完成了,但是由于需要在特定条件下才能触发,而这些条件在开发过程中很难去模拟,导致需求无法在所有场景下进行充分测试。举个例子,假设你在室内开发一个地图导航的Android应用,你需要在导航过程中,前方出现车祸,积水,施工等多种状况,怎么办?
  3. 总结你过去的BUG,你会发现有些你以为写的很完善的逻辑,却在最后被发现有场景未覆盖,或者逻辑错误等问题。
  4. 测试工程师给你报了一个BUG,你改完提交了,但是之后由于Merge失误导致代码丢失,或者其他人的修改导致你的BUG再次出现。直到测试工程师再次发现该BUG,并再次给你提出。
  5. 你的开发进度很快,但是开发完成后,你会被BUG淹没。你持续不断的修改BUG,持续不断的加班,直至发布版本,身心俱疲。
  6. 以前明明很正常的功能,在本次开发周期内,突然不能正常使用了。

如果你也经常碰到以上问题,或者困扰,那么你需要持续不断的对项目进行单元测试

Android单元测试简介

Android的单元测试分为两大类:

1.Instrumentation

通过Android系统的Instrumentation测试框架,我们可以编写测试代码,并且打包成APK,运行在Android手机上。

优点: 逼真
缺点: 很慢

代表框架:JUnit(Android自带),espresso

2.JUnit / Mock

通过JUnit,以及第三方测试框架,我们可以编写测试代码,生成class文件,直接运行在JVM虚拟机中。

优点: 很快。使用简单,方便。
缺点: 不够逼真。比如有些硬件相关的问题,无法通过这些测试出来。

代表框架: JUnit(标准),Robolectric, mockito, powermock

Android最佳Mock单元测试方案

我通过对比前辈们对各种单元测试框架的实践,总结出Android最佳Mock单元测试方案: Junit + Mockito + Powermock.(自己认证的…)

Junit + Mockito + Powermock 简介

众所周知,Junit是一个简单的单元测试框架。
Mockito,则是一个简单的用于Mock的单元测试框架。

那么为什么还需要Powermock呢?
EasyMock和Mockito等框架,对static, final, private方法均是不能mock的。
这些框架普遍是通过创建Proxy的方式来实现的mock。 而PowerMock是使用CGLib来操纵字节码而实现的mock,所以它能实现对上面方法的mock。

Junit + Mockito + Powermock 引入

由于PowerMock对Mockito有较强依赖,因此需要按照以下表格采用对应的版本。

Mockito PowerMock
2.0.0-beta - 2.0.42-beta 1.6.5+
1.10.8 - 1.10.x 1.6.2+
1.9.5-rc1 - 1.9.5 1.5.0 - 1.5.6
1.9.0-rc1 & 1.9.0 1.4.10 - 1.4.12
1.8.5 1.3.9 - 1.4.9
1.8.4 1.3.7 & 1.3.8
1.8.3 1.3.6
1.8.1 & 1.8.2 1.3.5
1.8 1.3
1.7 1.2.5

建议方案:
在项目依赖文件build.gradle中添加以下依赖。

1
2
3
4
5
6
7
testCompile 'junit:junit:4.11'
// required if you want to use Mockito for unit tests
testCompile 'org.mockito:mockito-core:1.9.5'
// required if you want to use Powermock for unit tests
testCompile 'org.powermock:powermock-module-junit4:1.5.6'
testCompile 'org.powermock:powermock-module-junit4-rule:1.5.6'
testCompile 'org.powermock:powermock-api-mockito:1.5.6'

Junit + Mockito + Powermock 配置

  1. 默认的测试代码位置
    对于通过gradle构建的android项目,在默认的项目结构中,Instrumentation的测试代码放在
    src/androidTest/ 目录,而JUnit / Mock的测试代码放在 src/test/ 目录。
  2. 自定义测试代码位置
    有些项目是由Eclipse构建迁移到由Gradle构建,需要自定义测试代码位置。
    举个例子,androidTest和test目录都在项目的根文件夹下。我们需要这样配置:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    android {  
    sourceSets {
    test {
    java.srcDir 'test'
    }
    androidTest {
    java.srcDir 'androidTest'
    }
    }
    }

如果在单元测试中遇到类似”Method … not mocked.”的问题,请添加以下设置:

1
2
3
4
5
6
android {
// ...
testOptions {
unitTests.returnDefaultValues = true
}
}

Junit + Mockito + Powermock 使用

强烈建议你熟读以下内容,来熟悉Junit + Mockito + Powermock的使用。

  1. Mockito 中文文档 ( 2.0.26 beta )
  2. Mockito reference documentation
  3. powermock wiki
  4. Unit tests with Mockito - Tutorial

下面通过举例来简单说明Junit + Mockito + Powermock 使用,更多详情清参考Demo项目:
https://github.com/snowdream/test/tree/master/android/test/mocktest

源码: https://github.com/snowdream/test/blob/master/android/test/mocktest/app/src/main/java/snowdream/github/com/mocktest/Calc.java

测试代码:https://github.com/snowdream/test/blob/master/android/test/mocktest/app/src/test/java/snowdream/github/com/mocktest/CalcUnitTest.java

1.验证某些行为,主要是验证某些函数是否被调用,以及被调用的具体次数。

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
//using mock
mockedList.add("once");

mockedList.add("twice");
mockedList.add("twice");

mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");

//following two verifications work exactly the same - times(1) is used by default
// 下面的两个验证函数效果一样,因为verify默认验证的就是times(1)
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");

//exact number of invocations verification
// 验证具体的执行次数
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");

//verification using never(). never() is an alias to times(0)
// 使用never()进行验证,never相当于times(0)
verify(mockedList, never()).add("never happened");

//verification using atLeast()/atMost()
// 使用atLeast()/atMost()
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("five times");
verify(mockedList, atMost(5)).add("three times");

2.验证执行顺序,主要验证某些函数是否按照预定顺序执行。

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
// A. Single mock whose methods must be invoked in a particular order
// A. 验证mock一个对象的函数执行顺序
List singleMock = mock(List.class);

//using a single mock
singleMock.add("was added first");
singleMock.add("was added second");

//create an inOrder verifier for a single mock
// 为该mock对象创建一个inOrder对象
InOrder inOrder = inOrder(singleMock);

//following will make sure that add is first called with "was added first, then with "was added second"
// 确保add函数首先执行的是add("was added first"),然后才是add("was added second")
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");

// B. Multiple mocks that must be used in a particular order
// B .验证多个mock对象的函数执行顺序
List firstMock = mock(List.class);
List secondMock = mock(List.class);

//using mocks
firstMock.add("was called first");
secondMock.add("was called second");

//create inOrder object passing any mocks that need to be verified in order
// 为这两个Mock对象创建inOrder对象
InOrder inOrder = inOrder(firstMock, secondMock);

//following will make sure that firstMock was called before secondMock
// 验证它们的执行顺序
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");

// Oh, and A + B can be mixed together at will

3.使用powermock必须使用两个annotation:

1
2
3
4
5
@RunWith(PowerMockRunner.class)
@PrepareForTest({Calc.class})
public class CalcUnitTest {
}
//PrepareForTest 后面要加准备被mock或stub的类,单个class直接()起来即可,多个用{},并用逗号隔开。

4.测试公开成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testPublicField() {
assertEquals(mCalc.mPublicField, 0);
assertEquals(mCalc.mPublicFinalField, 0);
assertEquals(Calc.mPublicStaticField, 0);
assertEquals(Calc.mPublicStaticFinalField, 0);

mCalc.mPublicField = 1;
Calc.mPublicStaticField = 2;

assertEquals(mCalc.mPublicField, 1);
assertEquals(mCalc.mPublicFinalField, 0);
assertEquals(Calc.mPublicStaticField, 2);
}

5.测试公开成员方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testAddPublicMethod() {
//when
when(mCalc.addPublic(anyInt(), anyInt()))
.thenReturn(0)
.thenReturn(1)
.thenReturn(2)
.thenReturn(3)
.thenReturn(4)
.thenReturn(5);

//call method
for (int i = 0; i < 6; i++) {

//verify
assertEquals(mCalc.addPublic(i, i), i);
}

//verify
verify(mCalc, times(6)).addPublic(anyInt(), anyInt());
verify(mCalc, atLeast(1)).addPublic(anyInt(), anyInt());
verify(mCalc, atLeastOnce()).addPublic(anyInt(), anyInt());
verify(mCalc, atMost(6)).addPublic(anyInt(), anyInt());
}

6.测试公开无返回值成员方法

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testAddPublicVoidMethod() {
//when
doNothing().when(mCalc).voidPublic(anyInt(), anyInt());

mCalc.voidPublic(anyInt(), anyInt());
mCalc.voidPublic(anyInt(), anyInt());

verify(mCalc, atLeastOnce()).voidPublic(anyInt(), anyInt());
verify(mCalc, atLeast(2)).voidPublic(anyInt(), anyInt());
}

7.测试公开静态成员方法

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

@Test
public void testAddPublicStaicMethod() throws Exception {
PowerMockito.mockStatic(Calc.class);

PowerMockito.when(Calc.class, "addPublicStatic", anyInt(), anyInt())
.thenReturn(0)
.thenReturn(1)
.thenReturn(2)
.thenReturn(3)
.thenReturn(4)
.thenReturn(5);


//call method
for (int i = 0; i < 6; i++) {

//verify
assertEquals(Calc.addPublicStatic(i, i), i);
}


//verify static
PowerMockito.verifyStatic(times(6));
}

8.测试私有成员变量
Powermock提供了一个Whitebox的class,可以方便的绕开权限限制,可以get/set private属性,实现注入。也可以调用private方法。也可以处理static的属性/方法,根据不同需求选择不同参数的方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testPrivateField() throws IllegalAccessException {
PowerMockito.mockStatic(Calc.class);

assertEquals(Whitebox.getField(Calc.class, "mPrivateField").getInt(mCalc), 0);
assertEquals(Whitebox.getField(Calc.class, "mPrivateFinalField").getInt(mCalc), 0);
assertEquals(Whitebox.getField(Calc.class, "mPrivateStaticField").getInt(null), 0);
assertEquals(Whitebox.getField(Calc.class, "mPrivateStaticFinalField").getInt(null), 0);


Whitebox.setInternalState(mCalc, "mPrivateField", 1);
Whitebox.setInternalState(Calc.class, "mPrivateStaticField", 1, Calc.class);

assertEquals(Whitebox.getField(Calc.class, "mPrivateField").getInt(mCalc), 1);
assertEquals(Whitebox.getField(Calc.class, "mPrivateFinalField").getInt(mCalc), 0);
assertEquals(Whitebox.getField(Calc.class, "mPrivateStaticField").getInt(null), 1);
assertEquals(Whitebox.getField(Calc.class, "mPrivateStaticFinalField").getInt(null), 0);
}

9.测试私有成员方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testAddPrivateMethod() throws Exception {
PowerMockito.mockStatic(Calc.class);

//when
PowerMockito.when(mCalc,"addPrivate",anyInt(),anyInt())
.thenReturn(0)
.thenReturn(1)
.thenReturn(2)
.thenReturn(3)
.thenReturn(4)
.thenReturn(5);

//call method
for (int i = 0; i < 6; i++) {

//verify
assertEquals(Whitebox.invokeMethod(mCalc,"addPrivate",i,i), i);
}

//verify static
PowerMockito.verifyPrivate(mCalc,times(6)).invoke("addPrivate",anyInt(),anyInt());
PowerMockito.verifyPrivate(mCalc,atLeast(1)).invoke("addPrivate",anyInt(),anyInt());
}

10.测试私有静态成员方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testAddPrivateStaicMethod() throws Exception {
PowerMockito.mockStatic(Calc.class);

PowerMockito.when(Calc.class, "addPrivateStatic", anyInt(), anyInt())
.thenReturn(0)
.thenReturn(1)
.thenReturn(2)
.thenReturn(3)
.thenReturn(4)
.thenReturn(5);


//call method
for (int i = 0; i < 6; i++) {

//verify
assertEquals(Whitebox.invokeMethod(Calc.class,"addPrivateStatic",i, i), i);
}


//verify static
PowerMockito.verifyStatic(times(6));
}

通过以上介绍,相信你对Android项目的Mock单元测试有一定的了解。
如果你有任何相关疑问,请通过以下方式联系我:

Email:yanghui1986527#gmail.com
QQ 群: 529327615

参考

  1. 详细讲解单元测试的内容
  2. 浅谈单元测试的意义
  3. 敏捷开发之测试
  4. Unit testing support
  5. junit4
  6. mockito
  7. powermock
  8. mocktest
  9. 在Android Studio中进行单元测试和UI测试
  10. whats-the-best-mock-framework-for-java
  11. mock测试
  12. 使用PowerMock来Mock静态函数
  13. PowerMock介绍
  14. Sharing code between unit tests and instrumentation tests on Android
  15. Unit tests with Mockito - Tutorial
  16. Android单元测试之Mockito浅析
  17. Mockito 简明教程
  18. mockito简单教程

一. Gradle源仓库(repositories)是什么东西,有什么用?

Gradle 源仓库(repositories)实际上复用了Maven的 源仓库(repositories)。

源仓库,主要用于托管项目构建输出和依赖组件的一个软件仓库。

这样说可能太抽象了。。。

举个例子:
开发者A,通过一个Android 库项目,构建出一个依赖aar包,然后上传至源仓库。
开发者B,通过在一个Android应用项目的build.gradle文件中声明依赖开发者A的库项目。构建过程中,gradle会自动去源仓库拉取改aar依赖包到本地,参与构建。(本地有缓存机制。如果上次拉取了,这次会使用本地缓存。)

二. 默认的仓库不可用?

对于Android应用,以前默认的官方仓库是mavenCentral(),后来修改成了jcenter()。

这两个官方仓库默认是通过https来访问的。很可能在国内无法正常使用。

如果你无法正常从源仓库拉取依赖包,有几个办法:
1.改用http访问
maven { url “http://oss.sonatype.org/content/repositories/snapshots" }
jcenter { url “http://jcenter.bintray.com/"}
maven { url “http://repo1.maven.org/maven2"}
maven { url “https://jitpack.io" }
其中,
jcenter { url “http://jcenter.bintray.com/"} 替换 jcenter()
maven { url “http://repo1.maven.org/maven2"} 替换 mavenCentral()
如果你需要用到snapshots包,建议添加 maven { url “http://oss.sonatype.org/content/repositories/snapshots" }
而另外一些包被托管在jitpack.io网站,你可以通过maven { url “https://jitpack.io" }仓库进行访问。

2.采用国内Maven仓库镜像
最早是oschina做了maven镜像,不过很遗憾,稳定性太差,目前处于基本不可用的状态。
目前的做法是:
推荐阿里云的maven镜像:
maven { url ‘http://maven.aliyun.com/mvn/repository/' }

3.通过vpn或者其他工具fq

三. Maven 搜索查询

比如:android插件
classpath ‘com.android.tools.build:gradle:1.3.1’
有哪些版本,最新版本号是多少?

你可以通过以下Maven网站/镜像进行查询。
http://maven.aliyun.com/nexus/#welcome
http://search.maven.org/
https://oss.sonatype.org/

接上一篇 Android项目持续集成实践之Gitlab CI.

在我看来,.gitlab-ci.yml 配置还是有些复杂,写的脚本还是有点多,有没有办法更精简一点呢?

有,那就是Android环境Docker化。(注:对Docker感兴趣的同学,请参考这本书《Docker —— 从入门到实践》)。

我在这本书的指导下封装了一个包含Android开发环境的Docker镜像。

  1. https://github.com/snowdream/docker-android
  2. https://hub.docker.com/r/snowdream/docker-android/

现在有了合适的Docker镜像,.gitlab-ci.yml 将会变得非常简单:

1
2
3
4
5
6
7
8
image: snowdream/docker-android:1.0

build:
script:
- gradle assembleRelease
artifacts:
paths:
- app/build/outputs/

第一行的意思是,采用标签为1.0,名称为snowdream/docker-android的Docker镜像,用于本工程的CI环境。

是不是很简单呢?

详细的构建过程日志太长,我就不贴了。链接如下:
https://gitlab.com/snowdream/Citest/builds/2155883

如果你在使用过程中,碰到什么问题,可以通过以下方式联系我:

  • Email:yanghui1986527#gmail.com
  • QQ Group: 529327615

Fork me on GitHub