自动编译组件

目前Android开发的主流开发工具是 Eclipse 和 IDEA

目前主流的自动化打包工具时 ant,maven,gradle。

maven工具中有自己的依赖仓库维护,很多开源支持包在上面都有维护(国内的除外)

gradle是近年来发展起来自动化构建应用,解决 ant 构建上的繁琐代码,并且也支持读取maven的配置形式,依赖maven的支持包结构

好了,平时你们使用 eclipse 发布的时候,不要说你没用过自动构建,eclipse 的 android项目是用ant的构建方式。如果你要加渠道发布,你就需要自己写 ant 的脚本。

maven自动构建,目前大多数用在 javaweb 项目,安卓项目用的不多。

eclipse 的构建应用大多使用 ant,maven,gradle也有相关支持。Android的项目默认使用 ant 进行构建

在 IDEA 中,可以使用 ant 方式构建 android应用,也能使用 gradle 方式构建,目前主流支持都是 gradle 方式。

IDEA 默认使用 gradle 工具做 android 的构建程序,你试试在新建应用的时候选择 Application Module 而不是 Gradle:Android Module 看看,你会发现,工程项目和 eclipse 没什么区别。而且包含 ant 的脚本文件。(如下图)

Gradle Module原理

IDEA 支持 ant,maven,gradle 工具来构建引用。目前 Android 应用在 as下编码基本使用 gradle 进行构建,本章将基于这个思路讲解那些内容属于 IDEA,那些内容属于 gradle 。

首先,我们建立一个 Gradle:Android Module 来看看 Gradle 项目结构。

得到的目录结构如下:

其中根目录是一个project,下面的app目录是其中一个module。

目录文件 作用
.gradle gradle项目产生文件(自动编译工具产生的文件)
.idea IDEA项目文件(开发工具产生的文件)
app 其中一个module,复用父项目的设置,可与父项目拥有相同的配置文件
build 自动构建时生成文件的地方
gradle 自动完成gradle环境支持文件夹
.gitignore git源码管理文件
build.gradle gradle 项目自动编译的配置文件
gradle.properties gradle 运行环境配置文件
gradlew 自动完成 gradle 环境的linux mac 脚本,配合gradle 文件夹使用
gradlew.bat 自动完成 gradle 环境的windows 脚本,配合gradle 文件夹使用
local.properties Android SDK NDK 环境路径配置
*.iml IDEA 项目文件
setting.gradle gradle 项目的子项目包含文件
  1. .gradle .idea 是在分别在 gradle ,IDEA 运行时候会生成的文件,一般这样的文件也不会纳入源代码管理之中。
  2. app文件夹,是其中一个module,里面的文件内容与父类差不多,若没有定义,则在项目中使用父类的设置(意思就是,里面也能包含build.gradle、gradle.properties、setting.gradle 等相关gradle文件,怎么理解?其实每一层都是一个module,整个项目是一个大的 module 而已)
  3. gradle 文件夹,用于保存gradle 下载路径的配置文件位置,用于没有gradle环境的环境初始化使用
  4. build.gradle 项目的编译环境配置,比如制定项目依赖的lib包。
  5. gradle.properties 配置gradle运行环境的文件,比如配置gradle运行模式,运行时jvm虚拟机的大小
  6. gradlew && gradlew.bat 代替gradle 命令实现自动完成gradle环境搭建。配合gradle文件夹的内容,会降到IDEA如何搭配gradlew使用。
  7. local.properties 配置android NDK,SDK的地方,恩,非android项目可能没有这个文件,这个路径根据不同想电脑不同,一般也不会纳入源代码管理之中,一般可以写一个local.properties.simple 文件,告知需要修改该文件名并写上本地SDK NDK 路径。simple文件纳入源码管理之中。
  8. setting.gradle 子项目包含文件,声明当前目录下含有什么module,当然你的app底下加上这样的文件,也能继续在app底下加module。和我第点说的,整个project就是一个大的module,每个module下面还能包含相应的module。如果你理解这个了,其实app目录单独作为一个项目管理也是可以的,,把相应的配置文件配上而已,相当于主目录应用 android 的gradle plugin (下一点会说到这个)
  9. gitignore 该文件是源码管理的配置文件,不在该文讲解。

    既然gradle 是多 module形式,那么我们来看看 setting.gradle 配置的内容

    从上面目录的配置文件内容来看,整个project也算是一个module,如果改module底下还有module,就可以通过setting.gradle配置进来,使得该module底下的gradle,从app module底下可以看出,module最少要含有 build.gradle文件,这个module的项目编译文件,该module依赖什么插件对该目录进行编译都在此配置,比如android与android-library,其他内容可继承父类的

Gradle 与 IDEA 的关联性

上面介绍了gradle项目的目录结构,以及module的模式,接下来,了解一下IDEA如何与gradle项目关联上来的。

idea的项目,在建立或者导入的时候,就已经确定他是基于什么自动构建工具的项目。新建的时候,使用 gradle androiw module 说明他是 gradle 自动构建的项目,那么导入的时候也是一样。我们看看下面的项目导入图,在选择项目地址之后,我们看到一下的内容。

如果你的项目里面包含了gradle的相关文件,就可以选择 import project form external model 导入IDEA了,如果项目只有源码,没有包含自动构建的相关信息,你只能选择 Create project form existing sources 了,让他生成自动构建工具需要的文件。

你可能会想,想gradle里面的依赖包,我不知道有哪些,怎么办。好吧,你私下里就骂骂作者吧,上传了源码不上传自动构建相关的文件,导致项目依赖不完整,还得找资料自己完善,所以这里也希望广大开源代码爱好者,在分享你的作品的时候,希望能够分享完整的项目信息,别只有源码,让人跑不起来项目(牢骚说多了)。

那么下一步是什么呢:

恩,是不是似曾相识?你没看错,在新建项目的时候也有这个选择,这个是选择自动编译的工具的方式。

如果你到官方下载 gradle 绿色包,解压到某个目录,你可以使用 gradle distribution,并设定 Gradle home 目录,这样 IDEA 构建编译项目的时候,就使用你设定的gradle版本进行。

说到这里,我觉得该说说 use default gradle wrapper 记得新建项目的时候也是使用 use default gradle warpper,这时候,上面目录说到的三个文件。

  • gradle/
  • gradlew
  • gradlew.bat

如果使用这种方法,IDEA会调用项目根目录 gradlew 或者 gradlew.bat (根据linux,windows,osx自动选型)代替原生的 gradle 方法做自动构建。

这两个文件做了什么事情呢:

  1. 解析 gradle/wrapper/gradle-wrapper.properties 文件,获取项目需要的 gradle 版本下载地址
  2. 判断本地用户目录下的 ./gradle 目录下是否存在该版本,不存在该版本,走第3点,存在走第4点
  3. 下载 gradle-wrapper.properties 指定版本,并解压到用户目录的下 ./gradle 文件下
  4. 利用 ./gradle 目录下对应的版本的 gradle 进行相应自动编译操作

看了上面的原理,应该明白了,如果你自己下载 gradle 让idea 导入项目的时候使用。那么其他人就不知道你使用什么版本的gradle版本进行自动编译,如果使用 项目目录自带的 gradlew 的话,gradlew 就会自动完善 gradle 的安装,若需要更新 gradle 的版本,只需要修改 gradle/wrapper/gradle-wrapper.properties 文件内的下载链接即可。而且gradlew的版本和 android 版本是需要适配的,在自己电脑维护需求不同版本的 gradlew 也是一个麻烦的事情。

而且这样的好处也有一个,当你在新电脑上下载你的源码进行编译时,你完全可以不依赖IDE开发工具,直接在项目目录下使用 ./gradlew build 即可对源码进行编译,因为它会自动下载 gradle 进行调用,可以使得新电脑较快完成项目开发环境适配(对网络依赖较强,希望带上vpn,这就是为什么你们在新建项目时需要去下载gradle,还比较慢的原因,我们一起来 f-u-c-k-g-f-w)

当然,导入项目需要能够选上 use default gradle wrapper 的前提是存在上面一个gradle文件夹与gradlew、gradew.bat

既然可以选择使用 gradlew 来管理 gradle 或者手动指定 gradle 工具,那么已经存在的项目如何更改?

这个问题,我曾在 idea 13 版本上有找到,但是在 idea 14 上面没找到相应的变更设置。谁要是找到了,记得留言告知我一声。

在导入项目或者新建项目的时候,idea 会根据 build.gradle 文件更新 *.iml 项目文件,有时候,你会发现,新增一个jar包,但是无法读取jar包内容,因为对于 gradle 来说,jar包属于项目外依赖,包括maven拓展包,都是属于项目外依赖,需要修改build.gradle 文件,当你加入新的 jar包,或者添加了 maven支持包,在idea上面都会提示需要进行同步

恩,上图你看到的,是我模拟添加一个jar包之后,随便加了一个空格,文件上提示需要进行 gradle 项目与 idea项目文件同步,点击 sync now 之后,idea 会根据 gradle 文件重新更新 .idea 目录以及 *.iml 文件,让idea 可以识别引入的 资源。

这也就是为什么有人说加入一个jar包确没有自动提示,而重新打开idea之后就能够提示使用jar内的方法了,因为重新打开idea,开发工具会重新同步 build.gradle 的内容

Gradle Android Plugin Version与 Gradle Version对应

gradle只是一个自动化编译工具,它需要以来插件来识别这是什么项目,用什么方式去编译的。我们来看看 build.gradle 与 app/build.gradle 的设置看看。

 

不知道你们会不会奇怪,在app里面的 build.gradle 中,缺少了 buildscript 与 allproject 的设置,恩,没错,我删掉了,因为这个设置是可以继承父项目的 build.gradle

其中父类的整个项目需要依赖插件 com.android.tools.build:gradle:0.14.2 最后是版本号。(目前AS的版本号已经是1.0.0了)

这个插件的版本号与gradle调用编译时是有依赖关系的,插件的版本越高,需要更多gradle的新特性,新的gradle特新就需要新版本的gradle才能支持。

这时候,回到上一节的内容,如果要使用android项目自动编译的新特性,如果选择不同的gradle指向方式,那么你就要做不同的处理

  • 下载不同版本的gradle对不同的项目的不同版本做插件与gradle的对应维护,如果其他人使用你的项目,你还要告知他使用什么版本以上的gradle才能使用这个插件。
  • 把对应关系一次弄好之后,更新gradle/wrapper/gradle-wrapper.properties下载地址,利用gradlew自动使用相应版本的gradle,这样gradlew版本的需求就跟着源码管理一直保留到其他人的电脑商。

具体的插件依赖,可点击这里

目录格式

先来看看eclipse的完整目录与IDEA的完整目录结构(当然,IDEA看的目录时基于module的目录设置的,而不是根据总项目的目录设置的)

eclipse目录

IDEA(AS)目录

按照Android开发的目录,区分为以下的目录格式:

目录类型 Eclipse IDEA(Android Studio)
代码根目录 / src/main
adil文件 /src [代码根目录]/aidl
java文件 /src [代码根目录]/java
assets文件 /assets [代码根目录]/assets
jni文件 /jni [代码根目录]/jni
jnilibs文件 /libs [代码根目录]/jniLibs
res文件 /res [代码根目录]/res
AndroidMainfest.xml AndroidMainfest.xml [代码根目录]/AndroidMainfest.xml

上面所展示的内容就是目前Android用到的所有资源文件类型。为什么要把项目根目录列出来?这个到后面渠道包的时候需要用到。可是也可能也会问到,为什么项目根目录会有两级。src 第一级我的理解是项目源码相关都在这里,第二级,认为是主项目源码在 main 目录,根据系统完善性,应该针对主项目添加测试项目的源码,所以新的代码里面在 main 目录同级的地方会有 tests 目录,用于测试项目的目录源码维护。如果有渠道,还可以为渠道包新建项目目录去坐项目自定义。

 
 
1
2
3
4
1. 其中adil是跨进程通信使用的
2. jni文件夹是存放dnk编译的c或者cpp文件
3. jnilibs文件,就是平时jni接入使用的 *.so库。需要里面是需要包含平台文件夹的。入下图所示
 

当我们要从eclipse里面转移到as的时候,是可以通过gradle来从新定义以上路径的。在module/build.gradle文件里面有这么一段设置默认设置,如果按照缺省,可以不写。

 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
sourceSets {
    main.setRoot('src/main')
    main {
        manifest.srcFile '[mainRoot]/AndroidManifest.xml'
        java.srcDirs = ['[mainRoot]/java']
        resources.srcDirs = ['[mainRoot]/java']
        aidl.srcDirs = ['[mainRoot]/aidl']
        res.srcDirs = ['[mainRoot]/res']
        assets.srcDirs = ['[mainRoot]/assets']
        jni.srcDirs=['[mainRoot]/jni']
        jniLibs.srcDirs = ['[mainRoot]/jniLibs']
    }
 

所示需要适应eclipse的目录格式,可以写成:

 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
sourceSets {
    //main.setRoot('src/main')    因为下面的路径全部被定义了,所以这个方法已经不起作用了。
    main {
        manifest.srcFile 'AndroidManifest.xml'
        java.srcDirs = ['src']
        resources.srcDirs = ['src']
        aidl.srcDirs = ['src']
        res.srcDirs = ['res']
        assets.srcDirs = ['assets']
        jni.srcDirs=['jni']
        jniLibs.srcDirs = ['libs']
    }
 

你一定奇怪,为什么libs 的jar包没有目录呢?没错,还记得我上面写过的,对于 gradle android 项目来说,jar包和library支持,属于android项目的外部支持,通通由 Gradle 配置文件去管理。上图的最后一段说明这一切。

Jar支持、Library支持、仓库插件

说到 library ,不得不说说项目类型。

在项目根目录,我们引入了 android 插件:com.android.tools.build:gradle:0.14.2

我们具体看上图 build.gradle 的第一行代码:

 
 
1
2
apply plugin: 'com.android.library'
 

这是说明这个 module 项目说明 android-library 项目,我看过其他的项目,gradle module 是可以依赖多个 plugin 让这个项目成为多种类型的项目。

如果是一个普通android项目,会是这样的代码:

 
 
1
2
apply plugin: 'com.android.applition'
 

那么 android 项目 gradle 中依赖 jar libs 又三种方法:

 
 
1
2
3
4
5
6
7
8
9
10
11
#1 依赖项目相对路径的jar包,当然,你可以换成全路径
compile files('libs/something_local.jar')
#或者依赖libs目录下的所有jar包
compile fileTree(dir: 'libs', include: ['*.jar'])
 
#2 依赖maven仓库中的支持包(目前很多好的都在maven进行管理,比如 v4,v7支持包)
compile 'com.android.support:appcompat-v7:20.0.0'
 
#3 依赖其他library module
compile project(':jiechic-library')
 

这么一看,我这个 jiechic-library 其实就是一个module,与app同级,使用的是 apply plugin: ‘com.android.library’ 形式的一个安卓 library 。

有时候,你可能新加入一个

很多开源仓库不懂怎么加?maven插件添加不在这篇文章的讨论范围了。你在搜搜可否?

编译过程及渠道模式

ant 的的编译目录基于当前工作目录进行,如果你需要自定义渠道,你需要编写 ant 脚本代码,去替换当前目录的文件,而且当前目录的文件你还需要进行保存一次。若出错了,你还得在本地目录进行恢复。

gradle 的编译方式,是根据基础项目内容,以及渠道信息 ,将相关代码文件拷贝合并到 build 目录下,然后在build 目录下进行编译。在此设计小文件多文件的平凡拷贝更新,则正常编译速度让很多人觉得,IDEA 开发比 Eclipse 的卡,其实这也是他实现的方式造成。

但是 gradle 项目却提供了很多eclipse 不方便提供的功能。比如渠道模式

比如渠道模式:

在build.gradle 中有这样的设定 productFlavors 这样的设定,当改设定存在,则 main 主代码渠道不在进行打包,全部依赖渠道进行编译安装调试

看到我的 productFlavors 中定义了两个版本,一个是线上测试餐饮版,一个是线上测试大众版。你看了,可能举得这么定义没什么用,你看到大括号里面了么?? applicationID,恩,没错,这就是可以重新定义他的包名,在这括号里面能够重新定义defaultConfig里面的所有配置。每个渠道的版本号都能单独维护。不需要你写ant脚本去替换。而且每个渠道也是同时可以编译 debug 和 release 版本

再来看下图:

记得在目录详解的目录上,设定的是 main 渠道的目录,恩,没错,现在我设定的是渠道的目录。这个怎么说?还记得我设定主目录的设定代码么?

 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
sourceSets {
    main.setRoot('src/main')
    main {
        manifest.srcFile '[mainRoot]/AndroidManifest.xml'
        java.srcDirs = ['[mainRoot]/java']
        resources.srcDirs = ['[mainRoot]/java']
        aidl.srcDirs = ['[mainRoot]/aidl']
        res.srcDirs = ['[mainRoot]/res']
        assets.srcDirs = ['[mainRoot]/assets']
        jni.srcDirs=['[mainRoot]/jni']
        jniLibs.srcDirs = ['[mainRoot]/jniLibs']
    }
 

没错,你看到的,所有android相关的文件,都在某个root下面。

那么,我们渠道上面设置的的路径也是基于如此路径进行文件分类的。

恩,没错,设定了渠道的内容,编译渠道的时候,不同的渠道就会依据主项目,然后替换自己的内容。 比如上图的String,你需要替换的只要写入你需要替换的String name,这么value的值就会在这个渠道就会替换过来。包括drawable等二进制文件,他会进行替换操作,比如说,你给应用宝打包的应用启动欢迎页是应用宝的,给其他应用商城的是其他图片,就可完全放置在渠道文件夹内。

如何检查是否合并?检查 build 文件内的合并内容,具体的不细说,你应该能找到,比如某个渠道替换的文件,找到那个渠道相应的文件查看即可。如果时Value的值,那就查看value文件内对应的name是否已经变更成功。

说到这个,不得不提,你有时候双击报错信息,打开了某个文件,或者某个图片(.9图较多),想对其进行修改,你发现它是只读状态,不可修改,恩,没错,这个编译文件经过 gradle 拷贝到build 相应渠道目录下才开始进行编译,在那个目录下的文件都是只读状态,并非你的源代码文件。你可以根据提示信息找到你自己的源文件进行修改。

你可能会问,这么多渠道包,那我要调试,究竟默认使用哪个渠道呢?看下图:

恩,你没看错,在项目下面,有个选择编译版本的地方。。这样,你想调试那个渠道的版本都可以轻松进行了。

提速编译

gradle 在编译时候有些东西跟java虚拟机相关,记得一开头的项目文件 gradle.properties 么?这个是设定 gradle 在运行时的编译环境。

在渠道编译的时候,默认情况下,一个渠道会启用一个java 虚拟机进行编译,在java 虚拟机启动,关闭的过程是非常耗时的,gradle 提供了守护进程的模式进行编译,从头到尾就使用一个java 虚拟机(jvm)

至于什么是守护进程模式,资料比较多,我就不解析了,可看这里这里

同时,根据你自己的电脑内存,你还可以定义虚拟机的内存数大小,这个其实会关乎你电脑卡不卡的问题,电脑内存太小,编译虚拟机内存太多,也许你切换个环境都会觉得举步维艰吧。

很多教程,都写编译打包版本,使用命令

 
 
1
2
./gradlew build
 

可是,都不会告诉你,更好的提高效率。官方提供release 版本与debug版本的区别编译

一般我们要编译所有渠道的 release版本,会使用如下命令:

 
 
1
2
3
4
./gradlew assembleRelease
#or
./gradlew aR
 

你肯定会问,如果我基于一个稳定版本,新增一个渠道,我只想打包一个渠道,怎么办?之行下面的命令看看

 
 
1
2
./gradlew task
 

这会列表所有 gradle task。task的概念我就不解释了,很多gradle教程已经说了。

我只要打包 onlinetestcatering 渠道,只要之行这样的命令就可以了。

 
 
1
2
./gradlew task assembleOnlinetestcateringRelease
 

IDEA 及 Gradle 使用总结的更多相关文章

  1. Gradle配置APK自动签名完整流程

    转载请注明出处:http://www.cnblogs.com/LT5505/p/6256683.html 一.生成签名 1.命令行生成签名,输入命令keytool -genkey -v -keysto ...

  2. gradle学习笔记(1)

    1. 安装     (1) 下载最新gradle压缩包,解压到某处.地址是:Gradle web site:     (2) 添加环境变量:             1) 变量名:GRADLE_HOM ...

  3. Gradle 实现 Android 多渠道定制化打包

    Gradle 实现 Android 多渠道定制化打包 版权声明:本文为博主原创文章,未经博主允许不得转载. 最近在项目中遇到需要实现 Apk 多渠道.定制化打包, Google .百度查找了一些资料, ...

  4. 解决 Could not find com.android.tools.build:gradle 问题

    今天拉同事最新的代码,编译时老是报如下错误: Error:Could not find com.android.tools.build:gradle:2.2.0.Searched in the fol ...

  5. React Native Android gradle下载慢问题解决

    很多人会遇到 初次运行 react-native run android的时候 gradle下载极慢,甚至会失败的问题 如下图 实际上这个问题好解决的 首先 把对应版本的gradle下载到本地任意一个 ...

  6. Android studio使用gradle动态构建APP(不同的包,不同的icon、label)

    最近有个需求,需要做两个功能相似的APP,大部分代码是一样的,只是界面不一样,以前要维护两套代码,比较麻烦,最近在网上找资料,发现可以用gradle使用同一套代码构建两个APP.下面介绍使用方法: 首 ...

  7. 对Maven、gradle、svn、spring 3.0 fragment、git的想法

    1.Maven Maven可以构建项目,采用pom方式配置主项目和其他需要引用的项目.同时可结合spring3.0的新特性web  fragment. 从现实出发,特别是对于管理不到位,程序员整体素质 ...

  8. 项目自动化建构工具gradle 入门1——输出helloWorld

    先来一个简单的例子,4个步骤: 1.进入D:\work\gradle\java 目录  ,您电脑没这目录? 那辛苦自己一级一级建立起来吧 新建文件build.gradle,文件内容是: apply p ...

  9. 用IntelliJ IDEA创建Gradle项目简单入门

    Gradle和Maven一样,是Java用得最多的构建工具之一,在Maven之前,解决jar包引用的问题真是令人抓狂,有了Maven后日子就好过起来了,而现在又有了Gradle,Maven有的功能它都 ...

  10. 通过Gradle为APK瘦身

    引言:在过去几年中,APK 文件的大小曾急剧增长态势.一般来说,其原因如下:Android开发者获取了更多的依赖库,添加了更多的密度,Apps 增加了更多的功能.但实际上我们应该让APKs 尽可能的小 ...

随机推荐

  1. vs2008快捷键极其技巧

    vs2008快捷键极其技巧 1. 工具: Microsoft Visual Studio 2008 Version 9.0.21022.8 RTM Microsoft .NET Framework V ...

  2. Cenots 7 Configure static IP

    For example: # cd /etc/sysconfig/ifcfg-enp3s0 # cat ifcfg-enp3s0 TYPE=EthernetBOOTPROTO=staticIPADDR ...

  3. PostgreSQL 区域设置

    安装PostgreSQL 10.3 windows版本时区域请选择"default locale",安装成功后输入命令: show lc_ctype; show lc_collat ...

  4. ASP.NET Core学习总结(2)

    public class ControllerActionInvoker : ResourceInvoker, IActionInvoker 我们知道,ControllerActionInvoker实 ...

  5. UWP开发入门(十)——通过继承来扩展ListView

    本篇之所以起这样一个名字,是因为重点并非如何自定义控件,不涉及创建CustomControl和UserControl使用的Template和XAML概念.而是通过继承的方法来扩展一个现有的类,在继承的 ...

  6. C博客第01次作业---顺序,分支结构

    1.本章学习总结 1.1 思维导图 1.2本章学习体会及代码量学习体会 1.2.1学习体会 经过了这一周的学习,从一开始对C语言一无所知,到现在能够写出基本的代码,感到非常开心. 学习C语言也并非想象 ...

  7. python中的 小数据池 is 和 ==

    1. 小数据池 一种数据缓存机制,也被称为驻留机制 小数据池针对的是:整数 , 字符 , 布尔值 .其他的数据类型不存在驻留机制 在python中对 -5 到256之间的整数会被驻留在内存中, 将一定 ...

  8. http与https通信

    HTTP协议 http协议与https协议的区别 GET请求和POST请求的说明与比较 发送GET和POST请求(使用NSURLSession)

  9. SQL语句优化 (一) (52)

    优化SQL语句的一般步骤 1 通过show status命令了解各种SQL的执行频率. 格式:mysql> show [session|global]status; 其中:session(默认) ...

  10. 弹性盒子模型display:flex

    1.div上下左右居中 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&qu ...