Android前向兼容的几个问题
2011-11-21 05:45 onm 阅读(352) 评论(0) 编辑 收藏 举报之前写过一个屏幕分辨率兼容性的Blog《Android学习小结(五)——Android手机屏幕那点事》。这次说的是Platform Version前向兼容的一些问题。
最近使用了一下Android Compatibility Package(最新版本r4改名叫做了Support Package),地址:http://developer.android.com/sdk/compatibility-library.html。这个Package主要是提供了较早平台版本不支持的一些API。目的是为了使程序员较少关注平台版本,而写出前向兼容的程序。比如http://code.google.com/p/iosched/这个应用就是使用的Compatibility Package。
Compatibility Package主要是加入了平板平台引入的一些API的支持,如Fragment,Loader等,我最近就把玩了一下Fragment和Loader,感觉很有意思。
话接上次的Blog《SimpleTouchImageView一个支持缩放平移及多点缩放的显示图片的Activity》,这个由于用到了多点API,我们知道API 4及其之前的版本是不支持多点的。所以我声明的minSdkVersion为7(Android 2.1)。这样运行低于Android 2.1版本的手机,将无法安装该应用。
但是,有个想法是让>=Android 2.1的机器使用多点缩放的Feature,而让低于这个版本的只使用单点缩放。这就需要程序能前向兼容。
对于Android应用,为了实现前向兼容,需要在AndroidManifest.xml文件中声明,然后使用高版本的SDK进行编译。这样编译期不会出现错误,可能的错误会在运行时出现(这个稍后说)。
例如,这样的声明则目前全部平台的设备都可以安装。
对于targetSdkVersion还需要仔细品味它的含义,之前看过一篇Blog讲解的比较到位,现在找不到了,只好粘贴一下官方文档。
With this attribute set, the application says that it is able to run on older versions (down to minSdkVersion), but was explicitly tested to work with the version specified here. Specifying this target version allows the platform to disable compatibility settings that are not required for the target version (which may otherwise be turned on in order to maintain forward-compatibility) or enable newer features that are not available to older applications. This does not mean that you can program different features for different versions of the platform—it simply informs the platform that you have tested against the target version and the platform should not perform any extra work to maintain forward-compatibility with the target version.
然后,可以通过使用反射来解决,就是通过反射判断是否有这个方法,如果有,则通过反射调用该方法,没有则放弃,这样可以保证较早版本的程序没有这个feature且不会崩溃,具体可以参看《Backward Compatibility for Applications》
因为以前的一个项目里做过一次api版本兼容的东西。具体是Camera的自动对焦的问题,由于我需要知道手机是否支持自动对焦这个功能,通过查看Reference知道,Camera.Parameters类的getFocusMode()方法可以检测,但是它是Since API Level 5,由于项目时序要从API Level 3开始支持的,于是通过使用反射来解决,就是通过反射判断是否有这个方法,如果有,则通过反射调用该方法,得知是否支持自动对焦,如果没有则实在没有办法。但这样至少高版本的手机可以有这个Feature。如果我们直接基于Android 1.5,也就是API Level 3来写应用,那么高版本的也无法应用这个Feature。
虽然这样,有了前面的经验,对于多点缩放前向兼容的这个feature,我还是觉得实现起来过于麻烦,几乎无法实现。反射自然是可以实现这种需求,但是反射调用过于麻烦,且多点缩放Feature涉及较多高版本方法,所以需要大量反射调用。
于是想直接判断运行时的平台版本,然后高于需求版本,就调用这个方法。这种想法应该是最朴素的想法,其实早在先前的自动对焦功能判断的项目里,就尝试过这种做法,但是结果显然是不行的,不然我就那么做了,但是我那时候还不知道为什么不行。
我还是打算再尝试一下,因为我突然想到看看Android Compatibility包是怎么实现的兼容。看了部分源码后,我发现它并没有使用反射,其实实现原理就是大概是通过Wrapper,它根据不同的运行平台写了不同的IMPL(实现)。
但是我之前为什么不行呢,于是我做了几个实验,遇到了很多疑问,于是我照例跑到Stack Overflow上去咨询,问了两个问题,地址分别如下:《android compatibility. I am confused when using Build.VERSION_CODES》,《why running android 2.3.4 and android 1.6 emulator throw different exception?》
没有得到太多答案,但是也还是解决了我部分的困惑,整理如下。
前面说道判断运行设备的平台版本,怎么判呢,可以通过Build.VERSION.SDK_INT得知运行环境SDK版本号。
得到版本号之后就是要做判断,然后执行不同代码。比如判断如下:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
Log.d(TAG, "Build.VERSION_CODES: " + Build.VERSION_CODES.ICE_CREAM_SANDWICH);
}
然后我用SDK 4.0编译,编译成功,然后安装。可以正常运行。但是问题是,我的设备是运行在低于ICE_CREAM_SANDWICH平台版本的,我的运行时是怎么可能正常执行Build.VERSION_CODES.ICE_CREAM_SANDWICH这个代码呢?我的运行时是没有ICE_CREAM_SANDWICH这个概念的,通过别人的解答,我知道了Java对于原生类型常量的处理,Java编译器会把这种类型的值直接编译到字节码(bytecode)里面,也就是对于ICE_CREAM_SANDWICH来说,这段代码编译后是:
if (Build.VERSION.SDK_INT >= 14) {
Log.d(TAG, "Build.VERSION_CODES: " + 14);
}
其实只是数字而已。
然后的问题是如下一段代码:(测试工程地址:https://docs.google.com/open?id=0Bz9fUdx-R6g9MjhhYzg1ZjQtZTRlMC00NDk0LWI4ZDktMDEyNzFjZmE5ZDAz)
private ScaleGestureDetector mScaleGestureDetector;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR_MR1) {
mScaleGestureDetector = new ScaleGestureDetector(this, new MyOnScaleGestureListener());
}
其中ScaleGestureDetector不是使用的SDK中的实现,而是我根据SDK实现更改过得自己的类。这是一个重点。
我在Android 2.3.4和 Android 1.6上都测试了,并且都运行正常。但是当我使用SDK中实现的ScaleGestureDetector类时,Android 2.3.4正常运行,Android 1.6上崩溃,抛出java.lang.VerifyError异常。
但是ScaleGestureDetector这个类里都调用了Android 1.6平台上没有提供的API,于是我困惑了。
然后我又测试了一段代码:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Fragment f = new Fragment();
}
(说明:我并不是想使用Fragment类,只是测试高于API Level 10的API)
然后我分别在Android 2.3.4和 Android 1.6上运行,根据前面的测试预想是都会抛出java.lang.VerifyError异常,但是结果又令我迷惑了。
在Android 1.6上抛出java.lang.VerifyError异常,但是在Android 2.3.4上却抛出了java.lang.NoClassDefFoundError异常。
通过查阅文档得知java.lang.VerifyError和java.lang.NoClassDefFoundError都继承自LinkageError(LinkageError is the superclass of all error classes that occur when loading and linking class files)
LinkageError说是发生在类文件加载和链接时。具体到刚才的两个异常,分别是:
java.lang.VerifyError:Thrown when the VM notices that an attempt is made to load a class which does not pass the class verification phase.
java.lang.NoClassDefFoundError:Thrown when the VM is unable to locate a class which it has been asked to load.
通过Stack Overflow上朋友的指引,我找到了这个,类加载过程(地址:http://www.particle.kth.se/~lindsey/JavaCourse/Book/Part1/Supplements/Chapter04/classLoaders.html):
The procedure followed to obtain and install a class goes as follows:
Loading includes the following tasks:
locate the bytecode file in a disk file, JAR, URL address, etc.
read it into the JVM's method area on the heap.
parse the class data into sections for constants, methods, fields, etc..
create an instance of java.lang.Class to represent the class.
Linking proceeds through these 3 steps
verification - check that the basic structure and form of the class is proper. Run bytecode verification.
preparation - allocate memory for the class data such as static variables.
resolution - convert the symbolic references in the class file, such as variable names, to direct references.
Initialization
The static variables and constants must be set to either default or user assigned values. The initialization code is collected into a special method called that is invoke by the JVM.
Particular JVM implementations may vary the order of these steps somewhat. For example, a class might be loaded and cached to speed the processing before it is actually needed. Bytecode verification can be done all at once after the class is first read in or it could be done "on the fly" on individual instructions as they execute.
前面的解惑之门的钥匙我认为就是类加载的问题。我的想法是调用系统ScaleGestureDetector类的时候,Android 1.6平台会找不到这个类,然后出现java.lang.VerifyError,但是我还不甚清楚具体发生在类加载的哪个阶段,看前面的文档貌似是说发生在类文件验证阶段。而使用自己的类的时候,由于调用的不是Framework的类文件,所以不会出现加载验证问题,最后的Android 1.6和Android 2.3.4扔出的异常不一样,貌似是他们的运行环境不一样,也就是VM的实现可能有所差异。
目前还不是十分了解具体原因,给出完美的解释,待后续完善更新。
最后我完成了SimpleTouchImageView的前向兼容工作,minSdkVersion从4开始,这样在不支持多点的手机上可以使用单点按钮缩放,支持多点的手机上使用多点缩放。
更新:
我在Android源码目录里找到了dalvik的文档,里面有如下一段话,大抵能解释一些问题。
In early versions of Dalvik (as found in Android 1.6 and earlier), the verifier simply regarded all problems as immediately fatal. This generally worked, but in some cases the VM was rejecting classes because of bits of code that were never used. The VerifyError itself was sometimes difficult to decipher, because it was thrown during verification rather than at the point where the problem was first noticed during execution.
The current version uses a variation of approach #1. The dexopt command works the way it did before, leaving the code untouched and flagging fully-correct classes as "pre-verified". When the VM loads a class that didn't pass pre-verification, the verifier is invoked. If a "deferrable" problem is detected, a modifiable copy of the instructions in the problematic method is made. In that copy, the troubled instruction is replaced with an "always throw" opcode, and verification continues.