http://www.ikoding.com/build-android-project-with-maven/
之前一直在做WEB前端项目,前段时间接手第一个Android项目,拿到代码之后,先试着run起来再说,导入eclipse,一堆错误,设置classpath依赖,折腾半天,还是编译错误,于是联系项目接口人,得知他有一个Android库项目没有提交到SVN,晕。。。
对于习惯使用Maven管理Java项目的我来说,自然想到能否用Maven构建Android项目呢?于是开始Google、百度,发现已经有前人做过这样的实践了,不过在使用过程中还是遇到不少问题,后面经过各种努力终于能比较顺地使用了,这篇文章对如何使用Maven构建Android项目作了简要总结。如果你和我一样饱受项目依赖管理的折磨,和我一样讨厌项目打包发布时的繁琐,希望能通过Maven让这一切自动化完成。那么,这篇文章或许对你有用。
1. 环境搭建
- JDK与Android SDK安装
做Android开发,这里无需多说,但安装完成后需要正确设置JAVA_HOME、CLASSPATH、ANDROID_HOME等环境变量。其中ANDROID_HOME为Android SDK安装的根目录。并将%ANDROID_HOME%\tools和%ANDROID_HOME%\platform-tools值添加到Path变量中。
- Maven安装
这里无需多说,下载安装Maven并正确设置环境变量即可。
- IDE支持
- Eclipse
大多数人都在使用Eclipse开发Android应用,如果你用Eclipse做Android开发,推荐下载eclipse-with-m2e-and-adt,该版本已经安装了ADT、m2e、m2e-android等重要插件,支持在Eclipse中使用Maven进行Android应用开发。当时为了安装这些插件费了好大劲,所以,如果你看到这里,可以直接下载它,不用像我一样去做那些没有意义又浪费时间的事情。
- IntelliJ IDEA
做Java开发,你不能不知道的神器,完美支持使用Maven构建Android应用,强烈推荐。即使是装了插件的Eclipse对使用Maven构建Android应用仍然支持不好。如果你是Eclipse的信徒,你也可以试试它,如果你能习惯它,你一定会被它的强大所吸引。
- NetBeans IDE
NetBeans也支持Android开发,但没怎么了解,用的人应该也比较少。
2. 项目构建
以下是我的项目中使用的pom文件,因为涉及保密,部分地方做过修改,但整体结构没有改变,可以清楚地说明问题。
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ikoding.android</groupId>
<artifactId>android-app-quickstart</artifactId>
<version>1.0.0</version>
<packaging>apk</packaging>
<name>Android Application Quick Satrt</name>
<dependencies>
<dependency>
<groupId>com.google.android</groupId>
<artifactId>android</artifactId>
<version>4.1.1.4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.android</groupId>
<artifactId>support-v4</artifactId>
<version>r7</version>
</dependency>
<dependency>
<groupId>org.apache.james</groupId>
<artifactId>apache-mime4j</artifactId>
<version>0.5</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.2.2</version>
</dependency>
<dependency>
<groupId>com.ikoding.android</groupId>
<artifactId>android-base</artifactId>
<version>2.0.0</version>
<type>apklib</type>
</dependency>
</dependencies>
<properties>
<keystore.filename>app-quickstart.keystore</keystore.filename>
<keystore.storepass>2013@ikoding</keystore.storepass>
<keystore.keypass>2013@ikoding</keystore.keypass>
<keystore.alias>ikoding-android-app</keystore.alias>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<finalName>${project.artifactId}-${project.version}-${manifest.metadata.id}</finalName>
<sourceDirectory>src</sourceDirectory>
<resources>
<resource>
<directory>.</directory>
<filtering>true</filtering>
<targetPath>../filtered-resources</targetPath>
<includes>
<include>AndroidManifest.xml</include>
</includes>
</resource>
<resource>
<directory>resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*</include>
</includes>
<excludes>
<exclude>**/env-*.properties</exclude>
</excludes>
</resource>
</resources>
<pluginManagement>
<plugins>
<plugin>
<groupId>com.jayway.maven.plugins.android.generation2</groupId>
<artifactId>android-maven-plugin</artifactId>
<version>3.5.0</version>
<extensions>true</extensions>
<executions>
<execution>
<id>run</id>
<goals>
<goal>deploy</goal>
<goal>run</goal>
</goals>
<phase>install</phase>
</execution>
</executions>
<configuration>
<proguardConfig>proguard.cfg</proguardConfig>
<proguardSkip>${project.build.proguardSkip}</proguardSkip>
<manifestDebuggable>${manifest.debuggable}</manifestDebuggable>
<androidManifestFile>target/filtered-resources/AndroidManifest.xml</androidManifestFile>
<release>${project.build.release}</release>
<run>
<debug>${project.build.debug}</debug>
</run>
<runDebug>${project.build.runDebug}</runDebug>
<sign>
<debug>${project.build.sign.debug}</debug>
</sign>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>com.jayway.maven.plugins.android.generation2</groupId>
<artifactId>android-maven-plugin</artifactId>
<configuration>
<sdk>
<platform>15</platform>
</sdk>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>keytool-maven-plugin</artifactId>
<version>1.2</version>
<!--<executions>-->
<!--<execution>-->
<!--<goals>-->
<!--<goal>clean</goal>-->
<!--<goal>generateKeyPair</goal>-->
<!--</goals>-->
<!--<phase>generate-resources</phase>-->
<!--</execution>-->
<!--</executions>-->
<configuration>
<keystore>${keystore.filename}</keystore>
<storepass>${keystore.storepass}</storepass>
<keypass>${keystore.keypass}</keypass>
<alias>${keystore.alias}</alias>
<dname>CN=iKoding, OU=iKoding, O=iKoding, C=CN</dname>
<sigalg>SHA1withDSA</sigalg>
<validity>10000</validity>
<keyalg>DSA</keyalg>
<keysize>1024</keysize>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
<encoding>UTF8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.9</version>
<configuration>
<projectnatures>
<projectnature>org.eclipse.m2e.core.maven2Nature</projectnature>
<projectnature>com.android.ide.eclipse.adt.AndroidNature</projectnature>
<projectnature>org.eclipse.jdt.core.javanature</projectnature>
</projectnatures>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<phase>initialize</phase>
<goals>
<goal>resources</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>debug</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<filters>
<filter>resources/env-debug.properties</filter>
</filters>
</build>
<properties>
<project.build.debug>true</project.build.debug>
<project.build.runDebug>false</project.build.runDebug>
<project.build.proguardSkip>true</project.build.proguardSkip>
<project.build.release>false</project.build.release>
<project.build.sign.debug>true</project.build.sign.debug>
<manifest.debuggable>true</manifest.debuggable>
</properties>
</profile>
<profile>
<id>release</id>
<properties>
<project.build.debug>false</project.build.debug>
<project.build.runDebug>false</project.build.runDebug>
<project.build.proguardSkip>false</project.build.proguardSkip>
<project.build.release>true</project.build.release>
<project.build.sign.debug>false</project.build.sign.debug>
<manifest.debuggable>false</manifest.debuggable>
</properties>
<build>
<filters>
<filter>resources/env-release.properties</filter>
</filters>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jarsigner-plugin</artifactId>
<version>1.2</version>
<executions>
<execution>
<id>sign</id>
<goals>
<goal>sign</goal>
</goals>
<phase>package</phase>
<inherited>true</inherited>
<configuration>
<includes>
<include>${project.build.outputDirectory}/*.apk</include>
</includes>
<keystore>${keystore.filename}</keystore>
<storepass>${keystore.storepass}</storepass>
<keypass>${keystore.keypass}</keypass>
<alias>${keystore.alias}</alias>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<!-- 渠道profiles -->
<profile>
<id>channel-test</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<manifest.metadata.id>test</manifest.metadata.id>
<manifest.metadata.channel>test</manifest.metadata.channel>
</properties>
</profile>
<profile>
<id>channel-91</id>
<properties>
<manifest.metadata.id>91-market</manifest.metadata.id>
<manifest.metadata.channel>91 market</manifest.metadata.channel>
</properties>
</profile>
<profile>
<id>channel-yingyonghui</id>
<properties>
<manifest.metadata.id>yingyonghui-market</manifest.metadata.id>
<manifest.metadata.channel>yingyonghui market</manifest.metadata.channel>
</properties>
</profile>
<profile>
<id>channel-tongbutui</id>
<properties>
<manifest.metadata.id>tongbutui-market</manifest.metadata.id>
<manifest.metadata.channel>tongbutui market</manifest.metadata.channel>
</properties>
</profile>
<profile>
<id>channel-tengxun</id>
<properties>
<manifest.metadata.id>tengxun-market</manifest.metadata.id>
<manifest.metadata.channel>tengxun market</manifest.metadata.channel>
</properties>
</profile>
<profile>
<id>channel-anzhi</id>
<properties>
<manifest.metadata.id>anzhi-market</manifest.metadata.id>
<manifest.metadata.channel>anzhi market</manifest.metadata.channel>
</properties>
</profile>
<profile>
<id>channel-gfan</id>
<properties>
<manifest.metadata.id>gfan</manifest.metadata.id>
<manifest.metadata.channel>gfan</manifest.metadata.channel>
</properties>
</profile>
</profiles>
</project>
以上的pom文件基本上涉及到了一个Android应用构建过程中的各个方面,以下针对其中比较重要的一些点作简要说明。
- 依赖管理
这也是我们使用Maven构建Android项目的重要原因之一,值得注意的是以下部分:
<groupId>com.ikoding.android</groupId>
<artifactId>android-base</artifactId>
<version>2.0.0</version>
<type>apklib</type>
</dependency>
以上依赖的type为apklib,这就是我们项目中的Library Project,此外,我们还可以使用Maven管理Native库,亦即我们项目中所依赖的那些so库文件。
- 资源过滤
项目中总会有一些和环境相关的配置,比如线下测试环境和线上环境的配置可能不一样,为此我们在pom中分别定义了id为debug和release两个profile,并使用不同的filter进行资源过滤。
- 生成keystore
我们使用了keytool-maven-plugin来生成签名时所需的keystore文件,我在properties中定义了生成keystore文件所需的信息,如下所示:
<keystore.filename>app-quickstart.keystore</keystore.filename>
<keystore.storepass>2013@ikoding</keystore.storepass>
<keystore.keypass>2013@ikoding</keystore.keypass>
<keystore.alias>ikoding-android-app</keystore.alias>
</properties>
可以运行mvn keytool:generateKeyPair命令来生成keystore文件,之后在应用正式打包发布时候就会使用该keystore文件进行签名。
- 生成渠道包
项目最终发布时需要根据渠道号生成不同的渠道包,我们可以利用Maven的资源过滤对AndroidManifest.xml文件中的渠道信息进行替换,例如我们上面的pom文件对Manifest文件中${manifest.metadata.channel}的渠道信息进行了替换,还有一些其他信息如Manifest文件的debuggable属性也可以针对不同配置进行替换,比如我们在日常开发中需要将该值设为true,而在项目正式发布时需要将该值设为false,我们以上的pom文件中就定义了多个针对不同渠道的profile。
- 代码混淆
项目正式发布时需要对代码进行混淆处理,我们只需在android-maven-plugin配置中指定proguardConfig文件,比如我们在上面的pom中指定proguard文件为proguard.cfg,项目构建过程中会自动运行代码混淆,并且可以通过proguardSkip属性指定是否运行代码混淆任务,这样我们就可以在日常开发调试过程中关闭混淆,而在项目发布时开启混淆。
- 签名
我们使用了maven-jarsigner-plugin对apk文件进行签名,签名使用的证书就是之前生成的keystore文件。
- 调试
我在使用Maven构建Android项目之初遇到的问题就是无法进行断点调试,这显然会降低开发效率,后来发现可以通过android-maven-plugin的runDebug属性开启调试,这样可以在应用程序启动时连接debugger进行调试。
- IDE支持
再次回到这个话题上,在导入eclipse之前,你需要先运行mvn eclipse:eclipse命令,生成eclipse项目文件,因为eclipse对android-maven-plugin支持不好。
3. 常用命令
以下是使用Maven构建Android项目中常用的一些命令,你可以根据需要选择合适的命令。
- mvn clean package
打包,但不部署。
- mvn clean install
打包,部署并运行。
- mvn clean package android:redeploy android:run
这个命令通常用于手机上已经安装了要部署的应用,但签名不同,所以我们打包的同时使用redeploy命令将现有应用删除并重新部署,最后使用run命令运行应用。
- mvn android:redeploy android:run
不打包,将已生成的包重新部署并运行。
- mvn android:deploy android:run
部署并运行已生成的包,与redeploy不同的是,deploy不会删除已有部署和应用数据。
4. 总结
使用Maven构建Android应用很好地解决了依赖管理的问题,而且我们也可以将很多繁琐的任务自动化完成。在使用过程中主要的问题是Eclipse的m2e-android插件支持不够理想,而IntelliJ IDEA在这方便显得非常强大,让我得以将Maven和IDE的优势结合在一起。你可以在此基础上开发出一些常用类型项目的maven archetype,这样你就可以更快的开始一个新的项目。
5. 坚持?改变?
事实上我所在的团队成员最初都在使用Eclipse进行开发,后来在我的一再“忽悠”下,大家都接受了这种新的开发方式,并逐渐转向IntelliJ IDEA。这需要你有做出改变的勇气,毕竟改变是痛苦的,没有充分的理由就很难说服别人改变,但当你适应后就会有新的收获。
附件
下面是Android Library Project的pom文件,它比文中的Application Project的pom文件看起来要简单很多:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ikoding.android</groupId>
<artifactId>android-lib-quickstart</artifactId>
<version>1.0.0</version>
<packaging>apklib</packaging>
<name>Android Library Project Quick Start</name>
<dependencies>
<dependency>
<groupId>com.google.android</groupId>
<artifactId>android</artifactId>
<version>4.1.1.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<sourceDirectory>src</sourceDirectory>
<pluginManagement>
<plugins>
<plugin>
<groupId>com.jayway.maven.plugins.android.generation2</groupId>
<artifactId>android-maven-plugin</artifactId>
<version>3.5.0</version>
<extensions>true</extensions>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>com.jayway.maven.plugins.android.generation2</groupId>
<artifactId>android-maven-plugin</artifactId>
<configuration>
<sdk>
<platform>15</platform>
</sdk>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.9</version>
<configuration>
<projectnatures>
<projectnature>org.eclipse.m2e.core.maven2Nature</projectnature>
<projectnature>com.android.ide.eclipse.adt.AndroidNature</projectnature>
<projectnature>org.eclipse.jdt.core.javanature</projectnature>
</projectnatures>
</configuration>
</plugin>
</plugins>
</build>
</project>