本文介绍 Android 不同系统及图片资源的常见适配问题。
compileSdkVersion, targetSdkVersion, minSdkVersion, buildToolsVersion
- minSdkVersion :应用支持的Android最低版本,若系统版本高于该版本则无法安装;
- buildToolsVersion:使用哪个版本的build工具,一般build版本会随着android版本的发布而发布,所以一般选取最新的sdk版本;
- compileSdkVersion:用什么版本的sdk编译程序,只在编译期起作用,另外support的版本要和这个版本一致,不然会出错。建议使用最新的Android系统版本;
- targetSdkVersion :targetSdkVersion 是 Android 系统提供前向兼容的主要手段。这是什么意思呢?随着 Android 系统的升级,某个系统的 API 或者模块的行为可能会发生改变,但是为了保证老 APK 的行为还是和以前兼容。只要 APK 的 targetSdkVersion 不变,即使这个 APK 安装在新 Android 系统上,其行为还是保持老的系统上的行为,这样就保证了系统对老应用的前向兼容性;
- 举个例子:比如某个api在Android5.0前后有改动,应用运行时系统会检测targetSdkVersion版本,如果小于21,就以旧版api功能运行,如果大于21,就以新版功能运行;
- 若targetSdkVersion(24) > 系统版本(6.0):如果没动态申请权限就会崩溃;
- 系统版本(6.0)> targetSdkVersion(22):如果没动态申请权限则不会崩溃,按照老版本方式运行;
- 系统是如何做到的呢?很容易想到,其实就是判断,也就是说发生这种兼容行为的变化时,一般都会在原来的地方保存新旧两种逻辑,并通过 if-else 方法来判断执行哪种逻辑。判断条件就是应用设置的targetSdkVersion版本,比如系统源码AlarmManger.java中就有如下逻辑:
private final boolean mAlwaysExact;
AlarmManager(IAlarmManager service, Context ctx) {
mService = service;
final int sdkVersion = ctx.getApplicationInfo().targetSdkVersion;
mAlwaysExact = (sdkVersion < Build.VERSION_CODES.KITKAT);
}
- 所以说修改了 APK 的 targetSdkVersion 行为会发生变化,或者说修改 targetSdkVersion 需要做完整的测试。
support包作用
- Android Support Library 可以分为两类:兼容库和组件库
- 兼容库
- 为老版本系统提供新API。说白了就是,让老版本的系统也能用上新版本才有的功能;
- 如果就这么一味的维护这个support库,也是不太可能的,毕竟新系统不断更新,这个库就会不断增大,因此support也有很多版本,比如v7.xx 或 v4.xx,其中v7依赖了v4,另外也需要开发者根据自己当前工程的编译版本来考虑使用什么版本的support库,这也是上面提到的“support版本要和compileSdkVersion版本对应”的原因。
- 组件库
- Android在推出新版本的时候往往也会推出一些新的组件,这些组件自身并没有调用底层的framework的API,因此在旧版本系统上进行兼容就显得很方便,进行开发时只需引入相应的组件依赖即可。Android Support Library提供了诸如v7-recyclerview,v7-cardview,v7-gridlayout等更小更灵活的组件库。
Android 高版本API方法在低版本系统上的兼容性处理
- Android 版本更替,新的版本新的方法带来许多便利,但无法在低版本系统上运行,如果兼容性处理不恰当,APP在低版本系统上,运行时将会crash。
- 举例:根据给出路径,获取此路径所在分区的总空间大小。
- 获取文件系统用量情况,在API level 9及其以上的系统,可直接调用File对象的相关方法getTotalSpace() 即可, 但是在API level 8 以下系统File对象并不存在此方法。
- 如果minSdkVersion设置为8,那么build时候会报以下错误:
Call requires API level 9 (current min is 8) - 为了编译可以通过,可以添加 @SuppressLint("NewApi") 或者 @TargeApi(9)。
用@TargeApi($API_LEVEL)显式表明方法的API level要求,而不是@SuppressLint("NewApi"); - 但是这样只是能编译通过,到了API level8的系统运行,将会引发 java.lang.NoSuchMethodError。
- 正确的做法
- 判断运行时版本,在低版本系统不调用此方法,同时为了保证功能的完整性,需要提供低版本功能实现
/**
* Returns the total size in bytes of the partition containing this path.
* Returns 0 if this path does not exist.
*
* @param path
* @return -1 means path is null, 0 means path is not exist.
*/
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
// using @TargeApi instead of @SuppressLint("NewApi")
@SuppressWarnings("deprecation")
public static long getTotalSpace(File path) {
if (path == null) {
return -1;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return path.getTotalSpace();
}
// implements getTotalSpace() in API lower than GINGERBREAD
else {
if (!path.exists()) {
return 0;
} else {
final StatFs stats = new StatFs(path.getPath());
// Using deprecated method in low API level system,
// add @SuppressWarnings("description") to suppress the warning
return (long) stats.getBlockSize() * (long) stats.getBlockCount();
}
}
}
- 总结:在使用高于minSdkVersion API level的方法需要:
- 编译期:用@TargeApi($API_LEVEL) 使可以编译通过(不建议使用@SuppressLint("NewApi"));
- 运行期:判断API level; 仅在足够高、有此方法的API level系统中,调用此方法;保证功能完整性,保证低API版本通过其他方法提供功能实现。
图片(drawable)适配问题
基本原则:先查找和屏幕密度最匹配的目录,如果没有,则依次向高密度目录查找,如果查到最高也没有,则查找 drawable-nodpi 目录(该目录无论设备密度如何,系统都不会缩放此目录中的资源),如果还是没有,再依次像低密度目录查找;
常见问题
- 放错目录会怎样?图片大小?内存大小?
- 视觉给的两倍图 @2x 和三倍图 @3x 如何使用?
具体实例
我们以具体的实例分析该问题。实例:一个本该放在 hdpi(对应设备 dpi:240) 的图片,被放在了 xxhdpi(对应设备 dpi:480) 目录,图片还是在 dpi 为 240 的设备上显示时,大小是放大还是缩小?放大或缩小几倍?内存占用是增大还是减小?增大或者减小几倍?
- 大小变化:放在了 xxhdpi 目录,该图片被认为是为高密度设备需要的,现在要显示在低密度设备上,图片会被缩小。至于缩小倍数,可以直接用 480/240=2 得到,当然也可以使用“缩放因子”得到:官方提供的六种通用密度对应着设备的逻辑密度(是以 mdpi 即 dpi 为 160 为基线的),逻辑密度(即缩放因子 density)计算公式为:density=dpi/160。因此 hdpi 的缩放因子是 1.5,xxhdpi 的缩放因子是 3,因此大小缩小了 2 倍;
想要说明的是,设备真实的像素密度(dpi)并不一定就是 240 或者 480,官方提供的六种通用密度本身就是一个范围,ldpi 对应的是 0~120,mdpi 对应的是 120~160,hdpi对应的是 160~240 等等。那个一个设备的真实缩放大小可以这样计算: scale = 设备 dpi / 图片所在 drawable 的(最大) dpi。
- 内存变化:
先说一个常见误区:我们在电脑上看到的 png(或jpg) 格式的图片,png(jpg) 只是这张图片的容器,它们是经过相对应的压缩算法将原图每个像素点信息转换用另一种数据格式表示,以此达到压缩目的,减少图片文件大小。而当这张图片被加载到内存时,会先解析图片文件本身的数据格式,然后还原为位图,也就是 Bitmap 对象,Bitmap 的大小取决于像素点的数据格式以及分辨率。所以二者是两码事。其实这里也可以直接明确一个结论:图片的不同格式:png 或者 jpg 对于图片所占用的内存大小其实并没有影响。
首先要了解图片内存占用计算公式:内存大小 = 分辨率 x 每个像素点大小,其中分辨率是图片加载到内存后像素点的总个数(宽度 x 高度),每个像素的大小取决于使用的数据格式,如 ARGB_8888 格式就会占用 4 个字节。这里特别说明了,分辨率是图片加载到内存后的像素点个数,并不是加载前的像素数,二者关系为:加载后宽度/高度 = 加载前宽度/高度 x (设备 dpi / 图片所在 drawable 的dpi)。因此答案也就一目了然了,新的宽高都减少了1/2,因此总内存大小减小了1/4。
但是如果图片位于磁盘或者 asset 目录呢?其实就不会有任何影响了,无论什么设备,直接按照原图加载前像素大小计算即可。当然,其中奥秘可以参考 Bitmap、BitmapFactory 相关源码,会发现只有 decodeResource() 方法内部会根据 dpi 进行分辨率的转换,其他 decodeXXX() 就没有了。