Android
发展和历史Android
是由Android
公司创造的手机操作系统,公司创始人是Andy Rubin
,后来被Google
收购,Google
于2007年11月发布了Android 1.0
手机操作系统,在2009年发布了Android 1.5
,此后Android
发展迅速。目前Android
已经超出类手机操作系统的范畴,已经被广泛应用于TV、手表以及各种可穿戴设备等等。
Android
平台架构Android
系统的底层建立在Linux
上,采用一种称为软件叠层的方式进行构建,这种方式使得层与层之间相互分离,保证了层与层之间的低耦合。
Android
体系结构如下(图源):
可以看到主要由6部分组成:
App
层Java API
框架层C/C++
库Android
运行时HAL
)Linux
内核下面进行详细说明。
App
层包含一系列核心App
(电话拨号应用、电子邮件客户端、日历、相机等),这些应用程序通常使用Java
编写。
Java API
框架层Java API
框架提供了大量API
供开发者使用,主要在这一层的上面进行App
的开发。
C/C++
库Android
包含一套被不同组件所使用的C/C++
库的集合,一般来说是通过Java API
框架层去调用这些原生的C/C++
库。
一些简单的原生C/C++
库如下:
WebKit
:一个Web
浏览器引擎,为Android
浏览器提供支持,也为WebView
提供支持OpenMAX
:开放媒体加速层,目的在于使用统一的接口,加速处理大量多媒体资料Libc
:一个从BSD
系统诞生的标准C系统库,并为嵌入式Linux
调整过Media Framework
:基于PacketVideo
的OpenCORE
,支持播放和录制许多流行的音频和视频格式SGL
:底层的2D图形引擎OpenGL ES
:基于OpenGL ES API
实现的3D系统,可以使用硬件3D加速和软件3D加速SQLite
:供所有应用使用的功能强大的轻量级关系数据库Android
运行时Android
运行时由两部分组成:
Android
核心库:核心库提供了Java
语言核心库所能使用的绝大部分功能ART
:负责运行Android
应用程序硬件抽象层(HAL
)提供了对Linux
内核驱动的封装,可以向上提供驱动音频、蓝牙、摄像头、传感器等设备的编程接口,向下可以隐藏底层的实现细节。
Android
把对硬件的支持分为两层:
Linux
内核中,值提供简单的硬件访问控制逻辑,开源Linux
内核Android
是基于Linux
的,Linux
内核提供了安全性、内存管理、进程管理、网络协议栈和驱动模型等核心服务,也是系统硬件和软件叠层之间的抽象层。
Gradle
Gradle
简介Gradle
是Android Studio
采用的构建工具,Gradle
与Ant
、Maven
相比,优势如下:
Gradle
支持Ant
、Maven
的构建操作Gradle
提供了强大的依赖管理Gradle
使用Groovy
编写构建文件,构建文件的功能更加灵活以目前最新的7.5.1
版本为例,下载之后解压会发现如下目录:
bin
:包含Gradle
的命令docs
:包含用户手册、DSL
参考文档、API
文档lib
:包含Gradle
核心,以及依赖的JAR
包init.d
:初始化脚本,以.gradle
结尾,比如test.gradle
,构建时会执行,该文件夹默认为空src
:Gralde
源码Gradle
解压后会有bin
文件夹,其中包含gradle
和gradle.bat
,根据系统的不同选择其中一个即可运行。
运行如果没有指定参数,会在当前目录下搜索build.gradle
,如果想让其他文件作为构建文件,可以使用-b
/--buildfile
参数。
使用Gradle
的关键就是编写构建文件,构建文件的主要作用就是定义构建项目的各种任务和属性。每个任务可以包含多个动作,Gradle
每次可运行多个任务。
一个典型的Gradle
项目层次结构如下:
root:项目根目录,存放全部资源
--src:源文件+资源文件
----main:存放与项目相关的源文件和资源
------java:Java源文件
------resources:项目相关资源
----test:存放与测试相关的源文件和资源
------java:测试源文件
------resources:测试相关资源
--build:存放编译后的class文件,与src具有对应关系
--libs:存放第三方JAR包
--build.gradle:Gradle构建文件
如果使用gradle
命令构建项目,项目根目录就会多出一个.gradle
文件夹,存放的是Gradle
构建信息,一般不需要手动修改。
Gradle
构建文件本质上是一个Groovy
源文件,该文件完全符合Groovy
语法。Gradle
采用领域对象模型的概念来组织构建文件,在整个构建文件中涉及如下API
:
Project
:代表项目,通常一份构建文件代表一个项目,Project
包含大量属性和方法TaskContainer
:任务容器,每个Project
都会维护一个TaskContainer
类型的tasks
属性,Project
和TaskContainer
具有一一对应的关系Task
:代表要执行的一个任务,允许指定它依赖的任务以及任务的类型,也可以通过configure()
配置任务,还提供了doFirst
、doLast
方法来添加Action
Gradle
构建文件结构如下:
Task1:
--Action1
--Action2
--Action3
Task2:
--Action1
--Action2
--Action3
Task3:
--Action1
--Action2
--Action3
Task
创建Task
创建有两种方式:
Project
的task()
TaskContainer
的create()
无论哪一种方式都可以为Task
指定如下属性:
dependsOn
:指定该Task
所依赖的其他Task
type
:指定该Task
的类型Task
一个简单的Task
示例如下:
task hello1{
println "配置的第一个Task"
}
然后通过gradle hello1
可以看到输出如下:
Gradle
构建过程Gradle
是一种声明式的构建工具,使用Gradle
构建时,并不是直接按顺序执行build.gradle
中的内容,构建过程可以分为两个阶段:
Gradle
读取build.gradle
的全部内容来配置Project
和Task
Task
在创建Task
时传入的代码用于配制该Task
,因此上面的代码在配置阶段输出(Configure project
),而不是在运行阶段输出。
Action
添加可以通过doFirst
/doLast
为Task
添加Action
,代码示例如下:
task hello2{
println "配置的第二个Task"
doLast{
for(i in 0..<5){
println i
}
}
doFirst{
def s = "test str"
println "str is:$s"
}
}
运行结果:
可以看到,Gradle
构建过程分为配置和运行,配置阶段会配置整个Project
和所有Task
,而运行阶段会运行对应参数指定的Task
。
Project
对象带有一个TaskContainer
类型的tasks
属性,可以在构建文件中通过它的create
方法来创建task
,示例如下:
tasks.create(name:'showTasks'){
doLast{
println 'tasks属性的类型: ${tasks.class}'
tasks.each{ e->
println e
}
}
}
通过gradle showTasks
输出如下:
在创建Task
的时候可以通过dependsOn
属性指定该Task
锁依赖的Task
,同时也可以通过type
指定Task
的类型(如果不指定默认是DefaultTask
类)。示例如下:
tasks.create(name:'task3',dependsOn:'hello2',type:Copy){
from 'src.txt'
into 'targetDir'
}
上面定义了一个叫task3
的Task
,依赖于hello2
,类型为Copy
(完成文件复制),from
指定了复制的源文件,into
指定了复制的目录位置(如果没有目录会新建)。
通过gradle tasks
运行发现会首先运行hello2
任务,同时会自动新建了into
指定的文件夹,其中的内容是src.txt
。
另一方面,使用Project
的task
方法同样可以指定type
以及dependsOn
属性,示例代码如下:
plugins {
id 'java'
}
task compile(type:JavaCompile){
source=fileTree('src/main/java')
classpath=sourceSets.main.compileClasspath
destinationDir=file('build/classes/main')
options.fork=true
options.incremental=true
}
task run(type:JavaExec,dependsOn:'compile'){
classpath=sourceSets.main.runtimeClasspath
main='Main'
}
上面的代码首先应用了java
插件,然后定义了compile
和run
任务,其中后者依赖于前者。compile
任务类型为JavaCompile
(详细属性说明可查看官方文档),其中:
source
指定了源代码路径classpath
指定类路径destinationDir
指定了编译后的字节码文件保存位置options.fork
表示是否编译之前创建一个新的进程,默认为false
options.incremental
表示开启增量编译而run
的类型是JavaExec
(文档说明此处),指定了类路径以及主类。
注意在运行之前先在当前与build.gradle
同级的文件夹下创建src/main/java/Main.java
,里面内容示例如下:
public class Main{
public static void main(String[] args){
System.out.println("Hello world");
}
}
然后可以直接gradle run
运行。
但是笔者在运行的时候报错如下:
> Task :compileJava FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileJava'.
> Could not find tools.jar.
笔者环境是m1 Mac+JDK8
,解决方法是通过
/usr/libexec/java_home -V
查看JDK
路径,一般会有两个,其中一个在lib
下带有tools.jar
,而另一个没有,只需要使用cp
将tools.jar
复制到另一个没有的lib
下即可。
修复之后,任务正常运行,输出如下:
Gradle
允许为Project
和Task
指定或添加属性,也就是可以指定内置属性的属性值,也可以添加新的属性。
Project
/Task
本身具有内置属性,可以直接在构建文件制定属性值,示例:
version = 1.0
description = 'Project 描述'
task showProps{
description = 'Task 描述'
doLast{
println version
println description
println project.description
}
}
输出如下:
在task
外的就是为Project
指定的属性,在task
内的就是为该task
指定的属性。
Project
常用属性:
name
:项目名字path
:项目绝对路径description
:项目描述信息buildDir
:项目构建结果存放路径version
:项目版本号ext
添加属性Gradle
中实现了ExtensionAware
接口的API
都可以通过ext
添加属性,而Project
和Task
都实现了ExtensionAware
,可以通过ext
为其添加属性,示例:
ext.prop1='project prop1'
ext.prop2='project prop2'
ext{
prop3='project prop3'
prop4='project prop4'
}
task showExtProps{
ext.prop1='task prop1'
ext.prop2='task prop2'
ext{
prop3='task prop3'
prop4='task prop4'
}
doLast{
println prop1
println prop2
println prop3
println prop4
println project.prop1
println project.prop2
println project.prop3
println project.prop4
}
}
通过gradle showExtProps
输出如下:
-P
添加属性执行gradle
命令可以通过-P
添加项目属性,示例:
task showCmdProps{
doLast{
println("系统显卡:${graphics}")
println("系统显卡:${project.graphics}")
}
}
如果直接执行gradle showCmdProps
会报错,提示找不到该属性。
需要使用-P
指定属性,执行gradle -P graphics=RTX4090Ti showCmdProps
后,输出如下:
JVM
参数添加属性执行gradle
时可以通过-D
设置JVM
参数从而添加项目属性,示例:
task showJVMProps{
doLast{
println("JVM参数添加的属性:${p1}")
}
}
执行gradle -D org.gradle.project.p1=111 showJVMProps
后,输出如下:
对于一些执行时间长的任务,如果每次执行该任务时没有发生改变,那么就没有必要执行该任务。因此Gradle
引入了增量式构建,如果任务的执行和前一次执行比较没有改变,Gradle
不会重复执行该任务。
Gradle
通过任务的输入和输出去判断任务有没有改变。Task
使用如下属性表示输入和输出:
inputs
:代表任务输入,是一个TaskInputs
类型的对象ouputs
:代表任务输出,是一个TaskOutputs
类型对象两者都支持设置文件、文件集、目录、属性等,只要它们没有发生改变,Gradle
就认为该任务的输入和输出没有改变。
示例代码:
task incrementalBuild{
def source = fileTree('source')
def dest = file('dist.txt')
inputs.dir source
outputs.file dest
doLast{
dest.withPrintWriter{ writer->
source.each{ s->
writer.write(s.text)
}
}
}
}
其中输入的部分指定了目录是source
,输出部分指定了文件为dist.txt
,只要两者没有发生改变,那么就认为任务没有发生改变,就不会执行任务。下面是两次执行任务的结果:
可以看到第二次并没有执行,只是更新到最新。
为了简化开发人员从头开始编写每一个Task
以及属性,Gradle
提供了插件机制。
开发插件其实很简单,无非就是在插件中预先定义大量的任务类型、任务、属性,然后开发人员只需要在build.gradle
中使用apply plugin
应用插件即可。
应用插件就相当于引入了该插件包含的所有任务类型、任务和属性,这样Gradle
就能执行插件中预先定义好的任务。
比如之前的java
插件,引入之后,可以通过gradle tasks --all
查看引入的任务:
可以看到java
插件分成几类添加了很多的任务,另外,该插件还定义了大量属性,比如:
sourceCompatibility
用于指定编译源文件时使用的JDK
版本archivesBaseName
指定了打包生成的JAR
包文件名示例代码:
plugins{
id 'java'
id 'application'
}
sourceSets{
utils
main{
compileClasspath=compileClasspath+files(utils.output.classesDirs)
}
}
compileJava.dependsOn compileUtilsJava
mainClassName='Main'
run.classpath=sourceSets.main.runtimeClasspath+files(sourceSets.utils.output.classesDirs)
首先应用了java
以及application
插件,两个插件都定义了大量的属性以及任务。然后在其中的sourceSets
添加了一个叫utils
自定义的依赖,并在main
中将utils
编译生成的字节码添加到编译时的类路径中。接着通过配置compileJava
的依赖任务compileUtilsJava
(该任务是自动生成的,一个sourceSet
对应了三个任务,比如此处的是utils
,则会自动生成compileUtilsJava
、processUtilsResources
、utilsClasses
三个任务)。
最后两行是application
插件定义的属性,mainClassName
指定运行的主类,run.classpath
指定运行时的类路径。
测试项目代码结构如下:
代码:
package utils;
public class Utils{
public static void print(){
System.out.println("utils test");
}
}
import utils.Utils;
public class Main{
public static void main(String[] args){
Utils.print();
}
}
运行gradle run
可以看到输出如下:
通过Gradle
配置依赖需要两步:
配置仓库在repositories
中配置即可,例如:
repositories{
// Maven默认中央仓库
mavenCentral()
// 通过URL自定义远程仓库或者本地仓库
maven{
// 定义远程仓库
url "http://repo2.maven.org/maven2/"
// 定义本地仓库
url "/xxx/xx/xxx"
}
}
然后可以通过配置组引入依赖。配置组的概念是由于项目编译时可能依赖一组JAR
,而运行的时候又依赖另一组JAR
,测试的时候可能又依赖另一组JAR
,因此可以通过不同的组配置不同的依赖。
Gradle
可以使用configurations
来配置组,例如:
configurations{
testConfig
}
定义配置组后就可以引入JAR
包,Gradle
使用dependencies
来配置JAR
包,配置方式与Maven
相同,指定group
、name
、version
即可。示例代码:
dependencies{
testConfig 'org.apache.commons:commons-lang3:3.12.0'
}
另外,在引入之后,如果需要提供额外的配置,可以使用闭包,示例如下:
testConfig('org.apache.commons:commons-lang3:3.12.0'){
// 额外配置
}
如果需要添加多个JAR
包,可以使用数组的形式:
dependencies{
testConfig 'org.apache.commons:commons-lang3:3.12.0',
'commons-io:commons-io:2.11.0'
}
定义好依赖后,可以在任务中使用该依赖,示例任务如下:
task showDependency{
doLast{
println configurations.testConfig.asPath
}
}
执行任务之后就会输出依赖包在本地的路径,笔者输出是在Gradle
的缓存目录下。
在实际开发中,通常不需要自己配置依赖组,应用java
插件后,默认的依赖组有:
implementation
:源代码依赖的组,最常用的一个依赖组compileOnly
:源代码编译时才依赖的组runtimeOnly
:源代码运行时才依赖的组testImplementation
:测试代码依赖的组testCompileOnly
:测试代码编译时依赖的组testRuntimeOnly
:测试代码运行时依赖的组archives
:打包时依赖的组自定义任务就是一个实现Task
接口的类,该接口定义了大量的抽象方法,因此一般自定义任务都会继承DefaultTask
基类,自定义任务的累可以自定义多个方法,这些方法可作为Action
使用。
自定义任务的Groovy
可以直接定义在build.gradle
中,或者定义在Groovy
源文件中。示例:
class HelloWorldTask extends DefaultTask{
@Internal
def message = '测试str'
@TaskAction
def test(){
println "test str: $message"
}
def info(){
println "info: $message"
}
}
task hello(type:HelloWorldTask){
doLast{
info()
}
}
task hello1(type:HelloWorldTask){
message = "测试 hello1"
}
上面定义了一个HelloWorldTask
的任务类,自定义了一个message
属性,且使用了@TaskAction
修饰了test()
方法作为Action
。执行gradle hello hello1
输出如下:
当需要定义大量的自定义任务时,直接写在build.gradle
显然不是一个好办法,更好的办法是存放在buildSrc
目录中。
buildSrc
相当于另一个Gradle
目录,该目录存放自定义任务的源代码。比如例子中的目录结构如下:
build.gradle
buildSrc
--src
----main
------groovy
--------com
----------company
------------TestTask.groovy
其中TestTask.groovy
定义如下:
package com.company
import org.gradle.api.*;
import org.gradle.api.tasks.*;
class TestTask extends DefaultTask{
@Internal
File file = new File('dist.txt')
@TaskAction
def show(){
println file.text
}
@TaskAction
def multiShow(){
println "==========="
println file.text
println "==========="
}
}
build.gradle
如下:
task show(type:com.company.TestTask)
task show1(type:com.company.TestTask){
file = file("dist1.txt")
}
运行后会发现会输出文件的内容。
自定义插件其实就是实现一个Plugin
接口的类,实现该接口要求必须实现apply()
方法。插件的本质,就是为Project
定义多个属性和任务,这样引入插件后即可直接使用这些属性和任务。示例:
class FirstPlugin implements Plugin<Project>{
void apply(Project project){
project.extensions.create("user",User)
project.task('showName'){
doLast{
println '用户名:'+project.user.name
}
}
project.tasks.create('showPass'){
doLast{
println '密码:'+project.user.pass
}
}
}
}
class User{
String name='test username'
String pass='test password'
}
apply plugin:FirstPlugin
上面定义了一个叫FirstPlugin
的插件,该插件重写了其中的apply()
方法,通过project.extensions
定义了一个扩展属性,同时定义了两个任务。
运行结果如下:
如果想独立定义插件,类似任务,需要放在buildSrc
下。示例目录结构如下:
build.gradle
buildSrc
--build.gradle
--src
----main
------groovy
--------com
----------company
------------Item.groovy
------------ItemPlugin.groovy
根build.gradle
如下:
plugins{
id 'item-plugin'
}
item.name = 'tets name'
item.price = 12345
item.discount = 0.5
buildSrc
下文件内容如下:
//build.gradle
plugins{
id 'java-gradle-plugin'
}
gradlePlugin{
plugins{
itemPlugin{
id='item-plugin'
implementationClass='com.company.ItemPlugin'
}
}
}
//Item.groovy
class Item{
String name = 'name'
double price = 10
double discount = 1.0
}
//ItemPlugin.groovy
package com.company
import org.gradle.api.*;
class ItemPlugin implements Plugin<Project>{
void apply(Project project){
project.extensions.create("item",Item)
project.task('showItem'){
doLast{
println '商品名:'+project.item.name
println '商品销售价:'+project.item.price*project.item.discount
}
}
}
}
运行结果:
Android
环境搭建Android Studio
安装安装就略过了,直接到官网下载安装即可。
下载之后,准备好SDK
、AVD
等。
打开Android Studio
选择创建应用:
接着设置包名并且选择位置即可:
然后等待依赖导入完成就可以直接运行了。
Android
应用结构分析Android
项目结构分析app
目录代表一个模块,是一个典型的Gradle
项目,目录树如下:
app
--build
--libs
--src
----androidTest
----main
------java
--------com/xxx/xxx/
------res
--------drawable
--------layout
--------mipmap-xxx
--------values
------AndroidManifest.xml
----test
--build.gradle
--gitignore
各部分含义如下:
build
:存放的项目构建结果libs
:存放第三方依赖库src
:源代码和资源目录build.gradle
:项目的构建文件androidTest
:Android
测试项目main
:java
目录保存Java
源文件(如果是Kotlin
源文件则存放在kotlin
中)res
:存放各种资源文件,比如layout
存放布局文件,values
存放各种XML
格式的资源文件,比如字符串资源文件strings.xml
、颜色资源文件colors.xml
,drawable
存放Drawable
资源,比如drawable-ldpi
等,与drawable
对应的还有一个叫mipmap
的目录,用于保存应用程序启动图标以及系统保留的Drawable
资源AndroidManifest.xml
:系统清单文件,用于控制应用的名称、图标、访问权限等整体属性R.java
R.java
位于app/build/outputs/apk/debug/app-debug.apk
中:
打开apk
中的dex
文件(dex
是为Dalvik
设计的一种格式),并打开对应的包路径即可以看到R.java
。
R.java
是由AAPT
工具根据应用中的资源文件自动生成的,规则如下:
R
类的一个内部类,比如界面布局资源对应layout
内部类,字符串资源对应string
内部类public static final int
类型字段AndroidManifest.xml
AndroidManifest.xml
清单文件是每个Android
项目所必须的,是整个Android
应用的全局扫描文件,说明了该应用的名称、所使用的图标以及包含的组件等。
一般包含如下信息:
Activity
和View
Activity
是Android
应用中负责与用户交互的组件,View
是所有UI
控件、容器控件的基类,View
组件需要放到容器中,或者使用Activity
将它显示出来。显示可以通过Activity
的setContentView
方法。
Service
Service
也代表一个单独的Android
组件,通常位于后台运行,一般不需要与用户交互。Service
组件通常用于为其他组件提供后台服务或监控其他组件的运行状态。
BroadcastReceiver
BroadcastReceiver
是Android
应用中另一个重要的组件,代表广播消息接收器。类似于事件编程中的监听器,与普通监听器不同的是,普通事件监听器监听的是程序中的对象,BroadcastReceiver
监听的是其他组件,相当于一个全局的事件监听器。
使用时,实现BroadcastReceiver
子类即可,并重写onReceive
即可,其他组件通过sendBroadcast
/sendStickyBroadcast
/setOrderedBroadcast
发送广播消息。实现了之后可以通过Context.registReceiver()
或在AndroidManifest.xml
中注册。
ContentProvider
ContentProvider
可用于为跨应用数据交换,实现时,需要实现如下方法:
insert()
:插入数据delete()
:删除数据update()
:更新数据query()
:查询数据一般需要配合ContentResolver
使用,一个应用使用ContentProvider
暴露数据,另一个使用ContentResolver
访问数据。
Intent
和IntentFilter
Intent
可启动应用中的另一个Activity
,也可以启动一个Service
,还可以发送广播消息触发BroadcastReceiver
。也就是说,Activity
、Service
、BroadcastReceiver
之间的通信都以Intent
作为载体,Intent
封装了当前组件需要启动或触发的目标组件信息。
Intent
可以分为隐式以及显式Intent
,显式Intent
明确指定了需要启动或触发的组件类名,隐式Intent
只是指定启动或触发的组件应满足怎么的条件。对于隐式Intent
,需要依靠IntentFilter
来声明自己所满足的条件。
Android
使用包名作为唯一的标识,如果安装包名相同的应用,后面安装的会覆盖前面的应用。签名主要有两个作用:
Android Studio
签名根据菜单中的Build->Generate Signed Build/APK
即可创建签名apk
:
选择APK
:
如果没有创建数字证书,可以选择Create new...
:
随便输入即可:
完成后点击下一步,选择debug
或release
:
key store
keytool -genkeypair -alias key -keyalg RSA -validity 400 -keystore key.jks
使用apksigner
签名:
apksigner sign --ks key.jks --ks-key-alias key --out sign.apk app-debug.apk
签完之后就会输出sign.apk
。
本文大部分摘自《疯狂Android讲义(第四版)》,作者李刚。