android+maven问题记录

参考材料

  • https://code.google.com/p/maven-android-plugin/wiki/GettingStarted
  • http://books.sonatype.com/mvnref-book/reference/android-dev.html
  • http://www.ikoding.com/build-android-project-with-maven/
  • https://github.com/mosabua/maven-android-sdk-deployer
  • http://rgladwell.github.io/m2e-android/
  • http://wiki.eclipse.org/M2E_plugin_execution_not_covered

前提条件

  • JDK 1.6+
  • Android SDK r21.1+
  • Maven 3.1.1+
  • Set environment variable ANDROID_HOME to the path of your installed Android SDK and add $ANDROID_HOME/tools as well as $ANDROID_HOME/platform-tools to your $PATH. (or on Windows %ANDROID_HOME%\tools and %ANDROID_HOME%\platform-tools)

特别注意maven的版本号

maven配置

请设置环境变量M2_HOME,并把settings.xml放到M2_HOME/conf中去。

eclipse配置

对于eclipse来说,除了要maven插件,还需要m2e-android插件。

dependency中support-v4的版本号只有很旧r7

其实除了support-v4,像android也有类似的问题。有一种解决方案是采用maven-android-sdk-deployer。 我测试过之后,发现这个解决方案虽然可行,但实际上比较麻烦。我直接在公司内的代理仓库上安装了新版本的。

Plugin execution not covered by lifecycle configuration

pom.xml很可能出现下面的错误提示:

Plugin execution not covered by lifecycle configuration: 
 com.jayway.maven.plugins.android.generation2:android-maven-plugin:3.8.2:consume-aar 
 (execution: default-consume-aar, phase: compile)

虽然不影响编译,但是很怪,可以通过下面的配置进行排除:

<pluginManagement>
	<plugins>
		<plugin>
			<groupId>org.eclipse.m2e</groupId>
			<artifactId>lifecycle-mapping</artifactId>
			<version>1.0.0</version>
			<configuration>
				<lifecycleMappingMetadata>
					<pluginExecutions>
						<pluginExecution>
							<pluginExecutionFilter>
								<groupId>com.jayway.maven.plugins.android.generation2</groupId>
								<artifactId>android-maven-plugin</artifactId>
								<versionRange>3.8.2</versionRange>
								<goals>
									<goal>manifest-update</goal>
									<goal>generate-sources</goal>
									<goal>proguard</goal>
									<goal>consume-aar</goal>
								</goals>
							</pluginExecutionFilter>
							<action>
								<ignore />
							</action>
						</pluginExecution>
					</pluginExecutions>
				</lifecycleMappingMetadata>
			</configuration>
		</plugin>
	</plugins>
</pluginManagement>

可以参考M2E_plugin_execution_not_covered

OutOfMemory或创建不了虚拟机

有时候会出现内存溢出或创建不了虚拟机的错误。考虑设置内存大小

<plugin>
	<groupId>com.jayway.maven.plugins.android.generation2</groupId>
	<artifactId>android-maven-plugin</artifactId>
	<configuration>
		<dex>
			<jvmArguments>
				<jvmArgument>-Xms256m</jvmArgument>
				<jvmArgument>-Xmx512m</jvmArgument>
			</jvmArguments>
		</dex>
	</configuration>
</plugin>

出现maven打包太慢的情况

经过测量,在dex成classes.dex的阶段比较慢,dx工具有提供一些参数进行优化.

  • incremental 增量打包,开发阶段可以开启,可以比较明显的缩短打包时间
  • optimize 是否优化classes.dex,开发阶段可以关闭
<dex>
  <incremental>true</incremental>
  <optimize>false</optimize>
</dex>

libpng error: Not a PNG file

如果直接把jpg格式换个名字,变成png,编译会报下面的错误,导致后面编译的.9图片也出问题(混淆问题的原因)

[INFO] libpng error: Not a PNG file
[INFO] ERROR: Failure processing PNG image E:\projects\G3ESOP\ESOP-Hubei2\res\drawable-xhdpi\more_about_pic1.png

‘build.plugins.plugin.version’ is missing

[WARN] 'build.plugins.plugin.version' is missing fororg.apache.maven.plugins:maven.compiler.plugin
It is highly recommended to fix these problems because they threaten the stability of your build.
For this reason, future Maven versions might no longer support building such malformed projects.

很简单,给maven.compiler.plugin这个插件添加version属性。 其实所有引用的插件都应该指定版本,不然都会有类似的提示。

关于编码

对于源码的编码格式和编译版本,应该进行指定:

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-compiler-plugin</artifactId>
	<version>3.1</version>
	<configuration>
		<source>1.6</source>
		<target>1.6</target>
		<encoding>UTF8</encoding>
	</configuration>
</plugin>

对于资源处理的话,可能出现下面的提示:

[WARNING] Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform dependent!

这个应该设置成UTF-8,如下所示:

			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-resources-plugin</artifactId>
				<version>2.6</version>
				<configuration>
					<encoding>UTF-8</encoding>
				</configuration>
			</plugin>

maven-jarsigner-plugin对带特殊字符的口令的处理

这个弄了好久,最后发现得把密码用引用引起来。切记切记。

后来,发现在linux这样又不能支持。所以只能用profile解决。

部分代码在jdk7中编译后dex出错

参考jdk7编译的bug记录,暂时只用jdk6编译

jdk6不支持android-19的proguard

原因是android-19的API实现了一些jdk7的特性,在proguard会找不到这些api。 由于和上一个问题有些冲突,暂时不考虑proguard。后续考虑考虑上jdk7。

如何添加.so支持

例如下面的百度地图SDK,需要加入一个so文件,在百度SDK里边是这样调用的:

System.loadLibrary("BaiduMapSDK_v2_3_1");

如果要用maven集成的话,可以用下面的配置(已经部署到代理仓库):

<dependency>
	<groupId>com.baidu</groupId>
	<artifactId>libBaiduMapSDK_v2_3_1</artifactId>
	<version>2.3.1</version>
	<classifier>armeabi</classifier>
	<scope>runtime</scope>
	<type>so</type>
</dependency>

如何转换成eclipse项目

项目目录中只有pom.xml,如果要导入eclipse的话,可以考虑使用下面的命令生成.project和.classpath文件

mvn eclipse:eclipse

生成之后可能会有M2_REPO变量找不到的问题,可以在eclipse中通过window>Preferences>Maven>Installations>Add进行添加maven安装位置。

否则的话,可以按以下方法添加M2_REPO: Window > Preferences > Java > Build Path > Classpath Variables 新增一个M2_REPO变量指向你maven本地仓库。

常用命令

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不会删除已有部署和应用数据

考察对类加载的理解(答案篇)

独立进程篇

首先需要知道类加载器是怎么回事? 在Java里边,类加载器就是用来加载类的,然后才是执行代码。
Java里边默认有启动类加载器(boot),扩展类加载器(ext),和应用类加载器(app)。
其中boot就是用来加载最开始的虚拟机和最基本的java类,ext是用来加载一些扩展类,默认是在jre/lib/ext目录下的。
最后一个才是你真正会用到的。cp参数就是用来指定应用类加载器找类的地方,java.ext.dirs就是用来指定 扩展类加载器找类的地方。 这三种类加载器是有层级关系的,类似于继承关系(父子关系)。

另外一个需要知道的概念是,类加载器在找类的时候,默认是采用双亲委派机制的。
例如应用类加载器加载类的时候,会先叫ext去加载,ext就会叫boot去加载,只有加载不到才尝试自己去加载。
这种做法是有个重要的因素,就是为了安全性考虑。

最后需要知道的是,TestServlet.class.getClassLoader()获取到的时候,加载TestServlet类所使用的实际的类加载器。

回到上面的问题。

第一个很简单,指定了应用类加载器加载的路径,所以main方法能找到,TestServlet也能找到,config.properties也能被应用类加载器找到。 启动是正常的。

第二个问题,TestServlet是被扩展类加载器加载的,所以通过它来找config.properties会找不到(在应用类加载器中才能被加载到)。

第三个问题,调整目录后,这个时候扩展类加载器的加载路径上也有config.properties,所以启动也会正常。

现在调整了代码,换成了Thread.currentThread().getContextClassLoader()的实现。
这个是有一点不一样的,上下文类加载器默认就是应用类加载器(如果通过代码进行修改的话)。 上下文类加载器还是一个很有用的技术, 可以用在JDBC这种SPI(Service Provider Interface)接口与实现分离的技术上,有兴趣可以去找找资料。

在这种情况下,后面的三个命令都是能够正常的,因为config.properties能够被正常识别的。

再说一点,用eclipse可以运行的程序,用命令行不一定可以,这点必须要认准最后的启动参数,通过这个来确认。 我们这边写独立进程的时候,贪方便喜欢用java.ext.dirs这个虚拟机参数,但这个有时候会有奇怪的问题。 大家要注意区分这个参数和cp参数的区别,这样就分析有思路,找问题很happy。

Web应用服务器篇

像tomcat这种应用服务器,本身也是一个java程序,但它可以把我们放上去的各个web应用隔离开来。 你没法调用其他web应用中的类,看上去好像是完全不相干的。这种技巧就充分利用了自定义类加载器的功能。 像tomcat会对每个应用单独定义一个类加载器(继承应用类加载器),并且修改双亲委派机制。 而是采用先从应用中的lib目录、classes目录尝试加载,没找到才到上面去找。(像was这种就跟复杂了,加载顺序也是可选的)

所以通常我们会有共享库的概念,在tomcat中对应的就是tomcat_home/lib这个目录(老版本的话还有更多目录可选)。 把一些第三方库放到这里,可以减少加载类的数量,从而减少内存占用。

这里要说明的是,类可以被不同的类加载器加载,虽然是在同一个jvm里边,但是是被当成不同的类(唯一标识是类加载器+类名)。

现在回到问题。虽然一开始就有人告诉我们,servlet是属于单例运行的。但是在这里有点小变化。

第一个问题,这个应该最常规的做法了,appa和appb是不相干的,所以访问appb,输出的是”1 1”,因为两个类是通过不同的类加载器加载的(就是说不一样的类),肯定生成的servlet实例是不一样的。

第二个问题,这种是共享库的做法,所以实际上他们使用的是同一个类,但是对于不同的app,用的是不同的servlet实例。 所以会出现静态变量有影响,当实例变量是独立的。所以最后会输出”3 1”。所以有维护静态变量的话,使用共享库是有不一样的。

第三个问题,这种其实在生产中也很常见,上新程序的时候就可能变成这样了。其实这个跟第一种情况是一样的。 不过,在was上加载顺序是可选的,所以情况也可能变成第二种情况。

后话

这里讲解的只是皮毛,有兴趣的童鞋,可以google更多资料和书籍,加以研究。

考察对类加载的理解(问题篇)

类加载和程序运行是有些关系的,不妨来测试一下。
难度:中级

独立进程篇

假设有下面的类文件:

// Main.java
package com.github.mccxj.test;

public class Main {
  public static void main(String[] args){
    new TestServlet().test();
  }
}

// TestServlet.Java
package com.github.mccxj.test;

public class TestServlet { 
  public void test() {
    InputStream is = TestServlet.class.getClassLoader().getResourceAsStream("config.properties");
    if(is == null){
      throw new RuntimeException("couId not found config.properties");
    }
  }
}

假设目录结构是这样的,其中jar下面的表示是在jar包里边的内容:

test
    -lib
        -test.jar
          -com/github/mccxj/test/Main.class
    -main.jar
        -com/github/mccxj/test/TestServlet.class
        -config.properties

请问:

  1. 执行java -cp main.jar;lib/test.jar com.github.mccxj.test.Main会出错么?
  2. 执行java -cp main.jar -Djava.ext.dirs=./lib com.github.mccxj.test.Main结果是怎样?

继续调整目录结果如下:

test
    -lib
        -test.jar
            -com/github/mccxj/test/Main.class
        -main.jar
            -com/github/mccxj/test/TestServlet.class
            -config.properties

再请问

  1. 执行java -Djava.ext.dirs=./lib com.github.mccxj.test.Main结果是怎样?

继续调整一下TestServlet的代码:

// TestServlet.Java
package com.github.mccxj.test;

public class TestServlet { 
  public void test() {
-    InputStream is = TestServlet.class.getClassLoader().getResourceAsStream("config.properties");
+    InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("config.properties");
    if(is == null){
      throw new RuntimeException("couId not found config.properties");
    }
  }
}

把目录结构恢复成:

test
    -lib
        -test.jar
          -com/github/mccxj/test/Main.class
    -main.jar
        -com/github/mccxj/test/TestServlet.class
        -config.properties

请问:

  1. 执行java -cp main.jar;lib/test.jar com.github.mccxj.test.Main会出错么?
  2. 执行java -cp main.jar -Djava.ext.dirs=./lib com.github.mccxj.test.Main结果又是怎样?

最后调整目录结果如下:

test
    -lib
        -test.jar
            -com/github/mccxj/test/Main.class
        -main.jar
            -com/github/mccxj/test/TestServlet.class
            -config.properties
  1. 执行java -Djava.ext.dirs=./lib com.github.mccxj.test.Main结果是怎样?

Web应用服务器篇

下面的例子,以tomcat为例。 假设有下面的Servlet文件,并打包成test.jar:

// TestServlet.java
package com.github.mccxj.test;

public class TestServlet extends HttpServlet {
    private static Atomiclnteger al = new AtomicInteger();
    private Atomiclnteger a2 = new AtomicInteger();

    @Override
    public void service(ServletRequest arg0, ServletResponse arg1) throws Servlet Exception, IOException {
        System.out.printIn(String.valueOf(al.incrementAndGet()));
        System.out.printIn(String.valueOf(a2.incrementAndGet()));
    }
}

并部署两个应用程序appa、appb,在他们的WEB_INF/web.xml添加了下面的内容

<servlet>
  <servlet-name>test</servlet-name>
  <display-name>test servlet</display-name>
  <servlet-class>com.huawei.test.TestServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>test</servlet-name>
  <url-pattern>/test</url-pattern>
</servlet-mapping>

大家应该听说过Servlet是单例的概念,也可能听过Web应用服务器有共享类的机制。那么,请问:

  1. 现在把test.jar扔到appa和appb的WEB_INF/lib目录中,启动tomcat,先访问/appa/test两次,再访问/appb/test, 此时会输出什么?
  2. 继续把test.jar都移除掉,只添加到TOMCAT_HOME/lib目录中,启动tomcat,先访问/appa/test两次,再访问/appb/test, 此时会输出什么?
  3. 最后把test.jar拷贝一份到appa的WEB_INF/lib目录中,启动tomcat,先访问/appa/test两次,再访问/appb/test, 此时会输出什么?

对UC文章《实时监控Android设备网络包》的补充

补充的内容,主要是一些细节的问题,备忘.

编译netcat

android上自己好像带了一个,不过也可以自己编译一个。 我这里使用cygwin来编译的,首先去下载源码。

# cygwin
cd /cygdrive/d/
mkdir -p netcat/toolchain

export NDK=/cygdrive/d/android-ndk-r8e
/cygdrive/d/android-ndk-r8e/build/tools/make-standalone-toolchain.sh --platform=android-8 --install-dir=netcat/toolchain
export PATH='pwd'/netcat/toolchain/bin:$PATH
export CC=arm-linux-androideabi-gcc
export RANLIB=arm-linux-androideabi-ranlib
export AR=arm-linux-and roideabi-ar
export LD=arm-linux-androideabi-ld

# 开始编译源码
cd netcat-0.7.1/
./configure —host=arm-linux
make

# 用file进行检测一下
file src/netcat
src/netcat: ELF 32-bit LSB executable, ARM, version1 (SYSV), ...

# 发到android上去
adb push src/netcat /data/local/netcat
adb shell chmod 777 /data/local/netcat

tcpdump的使用

如果只是监听所有的包,可以用下面的:

adb shell "tcpdump -n -s 0 -w - | nc -I -p 11233"

如果先监听端口的话,又想转发的话,写法有点特别。

adb shell "tcpdump -X -n -s 0 -w - port 5000 | nc -l -p 11233"

另外,如果像我这样,有cygwin的话,就已经有nc命令了,可以像下面一样进行转发。

adb forward tcp:11333 tcp:11233 && nc -v 127.0.0.1 | /cygdriver/d/Wireshark/Wireshark.exe -k -S -i -

由于MANIFEST.MF不规范导致程序无法启动的问题

昨晚发现某个jar程序启动不了,包类没找到。 这个是由于我增加了一个新的jar包,并且依赖于xwork.jar。 所以在build.xml里边增加这个jar包。

		<fatjar.build output="ibossproc.jar">
      ...
	    <fatjar.jarsource file="${buildlib}/jsse.jar" relpath=""/>
	    <fatjar.jarsource file="${buildlib}/cipher14.jar" relpath=""/>
	    <fatjar.jarsource file="${buildlib}/xwork-2.0.4.jar" relpath=""/>
    </fatjar.build>

不过还是出错,出错信息挺诡异的(用星号对某些信息进行屏蔽):

mccxj@XXX:/work/procs/log> cat *
线程 "main" 中发生异常java.lang.NoClassDefFoundError: com.*.*.*.*Entry
Caused by: java.lang.ClassNotFoundException: com.*.*.*.*Entry
        at java.net.URLClassLoader.findClass(URLClassLoader.java:421)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:652)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:346)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:618)
Could not find the main class: com.*.*.*.*Entry.  Program will exit.

这个类是程序里边的代码,应该是存在的。把jar解压了,也的确看到jar包里边有这个类。

于是把以前能跑的jar拿来进行对比,发现META-INF/MANIFEST.MF稍稍有点不同,新的在最后多了2个空行。 简单调整了一下打包脚本(用的一个fatjar.jar的工具),最后两行进行调整顺序,发现就可以了。

		<fatjar.build output="xx.jar">
      ...
	    <fatjar.jarsource file="${buildlib}/jsse.jar" relpath=""/>
	    <fatjar.jarsource file="${buildlib}/xwork-2.0.4.jar" relpath=""/>
	    <fatjar.jarsource file="${buildlib}/cipher14.jar" relpath=""/>
    </fatjar.build>

一开始感觉是fatjar.jar的bug,不过回过头来看,其实是cipher14.jar这个包的问题。
因为这个包的META-INF/MANIFEST.MF的最后一样是个问号,根据规范文档, 这个是不规范的。如:

Manifest-Version: 1.0

?

而fatjar会把所有的MANIFEST.MF进行合并,但是奇怪的地方就在这里了。如果问号是在最后一样,能够正常启动。
如果问号后面还有内容,就启动不了。所以调整顺序之后,能够正常启动。
后来,我把cipher14.jar的MANIFEST.MF中的问号去掉,不需要调整顺序也是可以正常的。

这个包应该是第三方厂商提供的,又踩地雷了。