赞
踩
想必我们做 Android App 开发的应该对 gradle 都不太陌生,因为 Android Studio 的帮助,Android 工程师使用 gradle 的门槛不算太高,基本的配置都大同小异,只要在 Android Studio 默认生成的 build.gradle 中稍加修改,都能满足项目要求。但是深入细致的了解 gradle 的基本知识,还是能够帮助我们更优雅地实现项目的配置工作。有些场景 gradle 甚至能帮助我们完成一些业务上的需要。这节课我们就来了解一下 gradle 那些需要掌握的基本知识。
Task(任务)可以理解为 gradle 的执行单元,gradle 通过执行一个个 Task 来完成整个项目构建工作。
我们可以在 build.gradle 中使用关键字 task 来自定义一个 Task。比如创建 build.gradle 文件,并添加 task,如图所示:
上图中定义了一个简单的 Task A,然后在终端中使用以下命令行执行此 Task,即可看到打印结果,如图所示:
从结果中可以看出,打印日志是在 gradle 的配置(Configure)阶段执行的。gradle 的构建生命周期包含 3 部分:初始化阶段、配置阶段、执行阶段。在 task A 中添加 doFirst 闭包,如下所示:
再次执行 gradle A,打印结果如下所示:
gradle 在运行期会执行所有 task 的配置语句,然后执行指定的 Task。
gradle 中的 Task 可以通过 dependsOn 来指定它依赖另一个 Task,如下所示:
在 build.gradle 中,我新加了一个 Task B,并通过 dependsOn 关键字指定 task B 依赖于 task A。 在命令行中执行“gradle B”,结果如下所示:
可以看出虽然我们只是执行了 task B,但是因为依赖关系的存在,task A 也会被执行。
gradle 会在配置 Configure 阶段,确定依赖关系。对于 Android 项目来说即为执行各个 module 下的 build.gradle 文件,这样各个 build.gradle 文件中的 task 的依赖关系就被确认下来了,而这个依赖关系的确定就是在 Configuration 阶段。
我们可以在 build.gradle 中使用 def 关键字,自定义方法,比如以下代码中自定义了 getDate 方法,并在 task 中使用此方法。
通过 gradle 命令执行上述 task,结果如下:
自定义 task 时,还可以使用系统提供的各种显示 task 来完成相应的任务。具体就是使用关键字 type 来指定使用的是哪一个 task。
比如我在当前目录下新建 2 个文件夹:src 和 dst。目录如下:
然后在 src 中创建文件 Demo.java,代码如下
最后当前路径结构如下:
修改 build.gradle,新添加一个 task copy 如下:
然后在命令行中执行“gradle copy”,运行结束后,重新查看当前目录结构如下:
可以看出 Demo.java 被拷贝了一份到 dst 目录中。
除了 Copy 之外,还有很多其他显示的 task 可用,比如我们可以通过自定义 task 实现编译 Demo.java 并将编译后的 .class 输出到某一特定路径,具体实现如下所示:
解释说明:
通过 type: JavaCompile 指定是编译 Java 类的 task;
source 指定需要编译类的文件路径;
include 指定需要编译哪一个 Java 类;
destinationDir 指定编译之后,生成 .class 文件的保存路径。
最后命令行中执行“gradle compile”,查看目录结构如下:
在 Android 中每个 module 就对应着一个 project,gradle 在编译时期会为每一个 project 创建一个 Project 对象用来构建项目。这一过程是在初始化阶段,通过解析 settings.gradle 中的配置来创建相应的 Project。
上图 settings.gradle 中导入了 3 个 project,但是实际上还会有一个根 project,使用 ./gradlew project 查看,如下所示:
我们可以在根 project 中统筹管理所有的子 project,具体在 LagouGradle 路径下的 build.gradle 中进行设置,如下所示:
这样写的好处是项目中所有 module 的配置都统一写在一个地方,统筹管理。比如经常会在主项目的 build.gradle 中添加包过滤,解决依赖冲突,如下所示:
随着项目越来越大,工程中的 module 越来越多,依赖的三方库也越来越多。一般情况下我们会在一个集中的地方统一管理这些三方库的版本。比如像谷歌官方推荐的使用 ext 变量,在根 module 的 build.gradle 中,使用 ext 集中声明各种三方库的版本,如下所示:
然后在子 module 中,引用这些版本信息。
但是这种写法有点小瑕疵:不支持 AS 的自动补充功能,也无法使用代码自动跟踪,因此可以考虑使用 buildSrc。
buildSrc 是 Android 项目中一个比较特殊的 project,在 buildSrc 中可以编写 Groovy 语言,但是现在谷歌越来也推荐使用 Kotlin 来编写编译语句。
先在根路径下创建目录 buildSrc,结构如下:
注意:这个工程的只能有一个,并且名字必须为 buildSrc。
创建好之后,在 buildSrc 中创建 build.gradle.kts 文件,并添加 Kotlin 插件。
编译工程有可能会报错,如下所示:
只要添加 repositories { jcenter() } 仓库即可。
接下来在 buildSrc 中创建 src/main/java 目录,并在此目录下创建 Dependencies.kt(名字可随意取)。在 Dependencies.kt 中创建两个 object,分别用来管理工程中的版本信息以及依赖库。
我们可以在 Versions 中添加各种项目中可能会引用到的版本。
然后在 Deps 中引用 Versions 中的变量。
最后我们就可以在各个 module 中的 build.gradle 中直接使用 Deps 中的变量用来声明依赖,比如在 app module 的 build.gradle 中添加如下依赖。
上图中分别是使用 buildSrc 前后的对比,并且在使用 Deps 的过程中,studio 会给出自动提示,如下:
![在这里插入图片描述](https://img-blog.csdnimg.cn/976ced6e0877447490c7ab56f366cb71.gif#pic_center)这节课主要介绍了 gradle 构建中的 task 和 project。
task 与大部分的开发者开发是最为紧密的,它是 gradle 构建的基本单元。我们每次编译工程时,Android studio 会在控制台打印出执行的 task 名称,类似下图中的格式。
我们也可以自定义 task 实现相同的构建需求。
project 对应的项目中的 module,每个 module 中包含一个 build.gradle,每个 build.gradle 都会被 gradle 编译成 Project 字节码。我们在 build.gradle 中所写的所有逻辑,实际上最终都会被映射成此 Project 字节码内的实现逻辑。
我们经常在 Android MVP 架构中的 Presenter 层做一些耗时操作,比如请求网络数据等。然后根据请求后的结果刷新 View。但是如果按返回结束 Activity,而 Presenter 依然在执行耗时操作,那么就有可能造成内存泄漏,严重时甚至会造成程序崩溃,因为 Presenter 中的 View 已经变为 null。为了解决这个问题,我们需要将 Activity 的某些生命周期方法与 Presenter 保持一致。
LifeCycle 的使用很简单,Activity 通过继承 AppCompatActivity 会自动继承来自父类 ComponentActivity 的方法 getLifeCycle。具体使用如图所示:
onStateChanged 方法会在 Activity 的生命周期发生变化时被触发,比如打开 LoginActivity 时就会显示如下日志:
在 LoginActivity 中按下返回键,则打印如下日志:
LifeCycle 还提供了注解的方式供使用,因此我们可以很容易地创建一个接口 IPresenter 类,在这个接口中声明对各种 Activity 生命周期的回调,如图所示:
上图中 IPresenter 接口通过注解的方式,将 Activity 的生命周期绑定到相应的方法上,我们只要在 BasePresenter 中实现上述方法,并在方法中做数据绑定与取消的操作即可,具体如图所示:
注意:上图中的代码存在一点问题,使用了 Android 中的 Log 来打印日志信息。严格来说在 Presenter 层应该禁止出现任何 Android 中的类,这里为了快速演示效果,所以直接使用 Log 打印日志。
接下来只要再修改 LoginActivity,将 BasePresenter 注册到 LifeCycle 中即可,如图所示:
重新打开 LoginActivity,显示日志结果如图所示:
关闭 LoginActivity,显示日志如图所示:
可以看出当 Activity 执行 onDestroy 时,BasePresenter 的 onDestroy 方法也会被执行。
在 LoginActivity 方法中有 login 方法,此方法会执行 BasePresenter 中的 login 方法,如图所示:
在 BasePresenter 的 login 方法中,模拟执行了一段耗时操作。如果在 Activity onDestroy 时,BasePresenter 还没有处理完耗时操作,则会造成内存泄漏。解决办法就是在 BasePresenter 的 onDestroy 方法中停止正在执行的耗时操作,如图所示:
并不是所有的 Activity 的生命周期都需要通知 Presenter。举一个例子,新增的需求是根据 GPS 定位,展示用户的位置。但是为了节省电量,有可能会在灭屏之后,解绑定 GPS 定位的接收事件。
如果使用 MVP 架构,需要有一个 TrackingActivity 实现 MVP 的接口 TrackingView,并在生命周期方法中调用 presenter 的相应方法,如图所示:
TrackingPresenter 是 presenter 层的实现,内部实现了 GPS 定位的监听事件,并分别在 resume 和 stop 方法中绑定和解绑定 GPS。如图所示:
上述写法是常规的 MVP 写法,但是存在 2 个问题:
GpsTracker 实际的控制周期是跟 Activity 有关的,因为亮屏和灭屏事件是在 Activity 中接收的,中间多了一层 Presenter 层其实是多余的。
从重构的角度看,TrackingPresenter 其实违反了职责单一原则(Single Responsibility),因为 Presenter 层的主要作用是用来刷新 View,但是上述代码中 TrackingPresenter 还负责对 GpsTracker 进行管理。
这种情况下,我们可以将 GpsTracker 初始化在 Activity 中,将 GpsTracker 的绑定与解绑定都在 Activity 中管理。最后将 GpsTracker 传给 TrackingPresenter 执行业务上的逻辑,具体实现如下:
TrackingActivity 中对 tracker 进行管理,并且根据 Presenter 层的逻辑处理,回调 showCurrentPosition 方法。
TrackingPresenter 只负责对 GPS 事件的监听,并根据结果刷新 View。
这样 View 层和 Presenter 层的职责单一原则就完成了,在完成实际需求的前提下,也丝毫不影响 Presenter 层的单元测试。当然,并没有绝对正确或者错误的架构,说到底代码具体要怎样写,功能具体应该怎样实现,最终还是要看实际业务场景。
这节课我主要对 MVP 架构中 Presenter 层的使用做了 2 点优化介绍:
如何支持 Presenter 的生命周期,使其在 Activity 被销毁时也能取消相应的耗时请求。
合理使用 Presenter 的生命周期,Activity 中所有的方法都委托给 Presenter 来处理是不合理的,这样会造成 Presenter 层极其庞大,也难以维护,有时也会违反职责单一原则。
我们在项目中经常会打印各种 Log 信息,用来查看程序运行中的详细情况。因此打印日志俨然已经成了程序开发工作中的一部分,而设计一个合理的 LogUtils 也成了一个好的程序员的必选条件之一。
有时候为了调试方便,我们甚至会将用户的账号、密码、余额等信息也打印到控制台,但是如果这部分 Log 信息也出现在线上版本中,那用户的私密信息,或者程序相关核心实现都会被暴露;除此之外,打印日志的代码并不属于业务需求的必要代码,复杂的 Log 信息还会造成一定的性能损耗,所以这部分代码都不应该出现在线上版本的 App 中。因此我们需要设置一个开关来控制是否打印 Log 日志,只有在 Debug 版本才会打开此开关,如图所示:
通常情况下我们会使用 BuildConfig.DEBUG 来作为是否要打印日志的开关。但是使用这个变量具有一定的局限性。比如现场突然发现一个异常现象,而我们需要现场抓取异常的日志信息加以分析。因为是 release 版本,所有不会有任何 log 信息被打印。因此这个开关的设置最好具有一定的灵活性,比如可以再加一层 System Property 的设置,如图所示:
上述代码打印结果如下所示:
使用 System Property 的好处是一旦设置之后,即使重启 App,System Property 中的变量依旧是设置之后的值,与 Android 中的 SharedPreference 非常相似。开发者只要定义好通过何种方式将这种属性打开即可,建议仿照 Android 系统设置中的“开发者选项”来实现,当用户快速连续点击某 item 时,才将此属性打开。
另外,我们还可以通过 ProGuard 在打包阶段清除某些 Log 日志、打印代码,具体规则如下:
有时候我们需要将部分 log 日志以文件的形式保存在手机磁盘中,因此我们还需要设置开关,控制日志是打印在控制台还是保存到文件中。如下所示:
因为涉及文件的写操作,所以最好是在子线程中完成日志的保存。因此在 LogUtils 中可以使用线程池控制子线程完成日志保存,如下所示:
如果 LogUtils 中的开关较多,再加上还有其他配置项,比如日志保存为文件的路径等。这种情况可以使用一个全局的 Config 来配置 LogUtils 中所有的配置项,如下所示:
我们经常会处理一些特殊格式的数据,比如 JSON、XML。为了打印这部分数据,还需要在 LogUtils 类中做一些格式转换的操作:
如果感觉自己封装一个 LogUtils 类比较麻烦,或者没有好的实现思路,那就不妨尝试使用行内内已经成熟的 log 日志库。
XLog
XLog 是比较常用的打印日志开源库,GitHub 地址参考 XLog github。XLog 基本囊括了我们上文介绍的所有功能:
全局配置或基于单条日志的配置;
支持打印任意对象以及可自定义的对象格式化器;
支持打印数组;
支持打印无限长的日志(没有 4K 字符的限制);
XML 和 JSON 格式化输出;
线程信息(线程名等,可自定义);
调用栈信息(可配置的调用栈深度,调用栈信息包括类名、方法名、文件名和行号);
支持日志拦截器;
保存日志文件(文件名和自动备份策略可灵活配置)。
XLog 使用比较简单,先调用 init 方法进行初始化,最好是在 Application 中。
然后就可以直接调用 Xlog 的静态方法打印相应日志即可:
也可以在打印日志时,添加局部的配置信息:
打印结果类似下图所示:
可以看出,除了打印日志的类和方法,XLog 还能打印线程信息以及调用栈信息。
这节课主要介绍了项目开发中对 LogUtils 类的配置,因为项目中会在很多地方打印各种日志信息,所以为了方便统一管理,我们应该将所有日志打印的工作都集中到一个 Utils 类中。LogUtils 应该对外部提供相应的开关,用来设置是否需要打印日志,以及打印日志的通道,如果是将日志保存在文件中等耗时操作,还应该考虑在子线程中完成。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。