Android 以jar包方式共享资源注意事项

    最近的一个项目是一个Android系统的系统应用的重构开发,项目中有很多个应用,这些 应用有许多相同的界面和交互;另外,这一套应用的界面可能会需要经常调整来适配不同的客户需求。为了减少开发和维护的工作量,我把这些应用的资源统一起来 一起维护,相同的资源不需要维护2份,并且适配新资源(图片、多国语言等)工作量也能做到最小,毕竟,人力资源是有限的。

    为了实现这个功能,我尝试了使用jar包的方式来共享资源,过程中遇到了一些问题,现在把这些问题归纳成四点,记录在这里,希望能帮到跟我有同样需求的人。这四点分别是:

一. 以lib工程方式静态共享资源;

二. Android不支持jar包中的资源的访问;

三. 第三方发布的开发包带有资源时的处理方式;

四. 为什么Android系统资源包Android.jar中的资源可以被访问。

    本文的Demo代码位于http://download.csdn.net/detail/romantic_energy/8598171,  有需要的朋友自己去下载。


一. 以lib工程方式静态共享资源

    把应用中的所有资源都放到了一个Android lib工程中(project->property->Android选项中把 Is Labrary勾选中),假设这个工程名为ResLib,它包含2个图片drawable:ok_n.png、ok_d.png , 一个xml drawable:selector_ok.xml ,内容为:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/ok_p" android:state_pressed="true" />
    <item android:drawable="@drawable/ok_n" />
</selector>

    再建一个apk工程,名为app1,它包含一个图片drawable ic_launcher.png,一个layout activity_main.xml, 在app1工程的属性中选择Android,点击Library选项框的add...按钮, 选中ResLib作为app1的依赖工程。在activity_main.xml中增加一个按钮:

<Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/textView1"
        android:layout_below="@+id/textView1"
        android:layout_marginLeft="41dp"
        android:layout_marginTop="75dp"
        android:text="Button"
        android:background="@drawable/selector_ok"/>

按钮中用到了ResLib中的drawable。

编译ResLib,然后再编译app1, 发现没问题, app1正确引用到了ResLib中的drawableselector_ok

    分别观察ResLib、app1中自动生成的R.java文件,发现总共有3个R.java文件,分别是:

1. 位于ResLib中的com.example.reslib.R.java;

2. 位于app1中的com.example.reslib.R.java;

3. 位于app1中的com.example.app1.R.java;

查看R.java 中的drawable类,发现

1. 位于ResLib中的com.example.reslib.R.java中的是:

public static final class drawable {
        public static int ok_n=0x7f020000;
        public static int ok_p=0x7f020001;
        public static int selector_ok=0x7f020002;
    }

2. 位于app1中的com.example.reslib.R.java是:

public static final class drawable {
        public static final int ok_n = 0x7f020001;
        public static final int ok_p = 0x7f020002;
        public static final int selector_ok = 0x7f020003;
    }

3. 位于app1中的com.example.app1.R.java是:

 public static final class drawable {
        public static final int ic_launcher=0x7f020000;
        public static final int ok_n=0x7f020001;
        public static final int ok_p=0x7f020002;
        public static final int selector_ok=0x7f020003;
    }

这里有2个需要注意的地方:

A. 变量的类型: 位于ResLib中的R.java中的drawable类型是public static int, 没有final,说明它不是常量, 可以在运行时变化,从设计上说,不能用这个值来索引资源,因为它可能在运行时被修改; 位于app1中dR.java中的drawable类型public static final int,是常量,可以用来索引资源。

B. 变量的值:以标注为红色的selector_ok为例,位于ResLib中的R.java中的selector_ok值为0x7f020002位于app1中dR.java中的selector_ok值为0x7f020003; 如果在app1中直接使用位于ResLib中的R.java中的selector_ok来获取资源,其实获取的是ok_p, 因为ok_p的值是0x7f020002

    再来把app1工程生成的apk包解压缩,看看里面都有些什么drawable,发现里面包含了3个图片drawableic_launcher.pngok_n.png、ok_d.png , 一个xml drawable:selector_ok.xml, 也就是说app1的apk包把所有在ResLib中存在的drawable资源都拷到了apk包中。

    最后我们通过aapt命令来查看一下app1.apk中的资源索引表,从命令行进入到app1.apk所在的目录,输入以下命令:aapt d resources app1.apk > out.txt, 查看out.txt, 可以看到以下内容:

Package Groups (1)
Package Group 0 id=127 packageCount=1 name=com.example.app1
  Package 0 id=127 name=com.example.app1 typeCount=8
    type 0 configCount=0 entryCount=0
    type 1 configCount=1 entryCount=4
      spec resource 0x7f020000 com.example.app1:drawable/ic_launcher: flags=0x00000000
      spec resource 0x7f020001 com.example.app1:drawable/ok_n: flags=0x00000000
      spec resource 0x7f020002 com.example.app1:drawable/ok_p: flags=0x00000000
      spec resource 0x7f020003 com.example.app1:drawable/selector_ok: flags=0x00000000

    我们看到app1中的资源索引表中是含有ok_nok_p、selector_ok资源的索引的,并且索引值跟app1中的R.java中的值一样。

由上可知:app1不会直接引用ResLib中的资源,不会使用位于ResLib中的R.java文件,而是把属于ResLib中的资源拷贝到了自己的apk中,把这些资源当作自己的资源,再重新生成资源ID和R.java文件,然后像访问自己的资源一样去访问原本属于ResLib中的资源。


二. Android不支持jar包中的资源的访问;

    区别于第一点,这里的jar包是指不带源码工程的jar包, lib工程默认生成的jar包中是只包含.class文件,不带资源文件的, 如果你想你的jar包中包含资源文件,需要使用Eclipse的导出(export)功能,将资源文件一起导出到jar包中,但是请注意:以这种方式导出的资源文件不仅引用jar包的apk工程无法使用,连jar包中的代码也无法使用。

    下面来看看,为什么Android中jar包中的资源文件无法访问。

    先来做个实验,看看使用使用Eclipse的导出(export)功能导出的ResLib中的资源id,与直接工程依赖的ResLib中的id,是否一样。

    我们在之前的ResLib工程中增加一个ResLibTest.java文件, 包名com\example\reslib,内容为:

package com.example.reslib;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.Log;
public class ResLibTest {
    public ResLibTest() {        
        Log.i("ResLibTest", "id: ok_n = 0x" + Integer.toHexString(R.drawable.ok_n));
        Log.i("ResLibTest", "id: ok_p = 0x" + Integer.toHexString(R.drawable.ok_p));
        Log.i("ResLibTest", "id: selector_ok = 0x" + Integer.toHexString(R.drawable.selector_ok));
    }   
    public Drawable get_ok_n(Context context) {
        return context.getResources().getDrawable(R.drawable.ok_n);
    }
}

    使用Eclipse中的导出功能,把ResLib的代码和资源导出到名为ResLib.jar的jar包中,导出方式是: 右击ResLib工程->Export->Java->JAR file  点击Next,将ResLib的以下内容导出:

Android 以jar包方式共享资源注意事项_第1张图片

    注意不要把manifest.xml导出,否则引用时会报错:有2个manifest.xml。


    任然在app1中已代码工程方式引用ResLib,并在app1的MainActivity类中的onCreate函数中增加以下代码:

        ResLibTest test = new ResLibTest();        
        TextView view1 = (TextView)findViewById(R.id.textView1);
        view1.setBackground(test.get_ok_n(this));

    以此查看资源id和test.get_ok_n()函数返回的drawable。

    再建一个app2的apk工程,只包含ic_launcher.png一个drawable,把之前导出的ResLib.jar包拷贝到工程的libs目录下,同样在app2的MainActivity类中的onCreate函数中增加以下代码:

        ResLibTest test = new ResLibTest();        
        TextView view1 = (TextView)findViewById(R.id.textView1);
        view1.setBackground(test.get_ok_n(this));

    以此查看资源id和test.get_ok_n()函数返回的drawable。   

    运行app1,得到以下打印信息:

        id: ok_n = 0x7f020001
        id: ok_p = 0x7f020002
        id: selector_ok = 0x7f020003

    并且textView1显示的背景是ok_n.png。

    运行app2,得到以下打印信息:

        id: ok_n = 0x7f020000
        id: ok_p = 0x7f020001
        id: selector_ok = 0x7f020002
    并且textView1显示的背景是ic_launcher.png。   

    由上可知,使用导出包ResLib.jar时,得到的资源id与直接工程依赖时得到的id并不同,并且使用导出包ResLib.jar时,访问到了错误的资源。

    Android中一般资源的访问方式是:    context.getResources().getDrawable(R.drawable.ok_n);

    说明资源是和Context、ResourceManager类关联在一起的,而导出的jar包无法构造出类似的关联类,Android本身也没有提供类似的访问机制,所以我们无法以正常的方式来访问jar包中的资源。

    stackoverflow上有2个问题是关于这个的, 分别是:

http://stackoverflow.com/questions/9087096/packaging-drawable-resources-with-a-jar
http://stackoverflow.com/questions/9868546/android-how-to-export-jar-with-resources

    另外,Android文档中也有相关的介绍:http://tools.android.com/recent/buildchangesinrevision14

    但其实jar包中的资源文件还是可以被访问到的,只不过是被当作一般的文件io流,需要自己去解析,这并不是一个完整的方案,会引出许多其它的问题,所以实际意义不大。这个只给出图片文件的访问方式,在ResLibTest类中加入以下函数:

public Drawable get_ok_n_2(Context context) {        
        Bitmap bitmap = null;
        BitmapDrawable drawable;
        InputStream iStream = getClass().getClassLoader()
                .getResourceAsStream("res/drawable/ok_n.png");
        Log.i("ResLibTest", "iStream = " + iStream);
        if(iStream != null) {
            bitmap = BitmapFactory.decodeStream(iStream);
            drawable = new BitmapDrawable(context.getResources(),bitmap);    
            return drawable;
        }
        return null;
    }

导出ResLib后,使用get_ok_n_2能得到正确的drawable。这里有3点值得注意:

A. getClass().getClassLoader().getResourceAsStream 和 getClass().getResourceAsStream都能获取到io流,ClassLoader版本的getResourceAsStream不能访问"/res/drawable/ok_n.png", 注意前面的斜杠,Class版本的getResourceAsStream可以。并且Class版本的函数调用的也是ClassLoader中的函数。

B. 在app2中可以以以下方式访问图片:

ResLibTest test = new ResLibTest();   
InputStream iStream = test.getClass().getResourceAsStream("res/drawable/ok_n.png");

C. iStream的打印结果是:

iStream = libcore.net.url.JarURLConnectionImpl$JarURLConnectionInputStream@41ade240

    当以getResourceAsStream方式来获取xml文件时,xml需要全部自己解析之后再根据解析结果去加载相应的资源文件,实际应用中不具备实际意义,这里就不深究了。

    getResourceAsStream方式加载的文件属于java类加载器提供的功能,Andriod并没有为获取jar包中的资源提供任何便利的方法,所以得出的结论是:Android不支持jar包中的资源的访问


三. 第三方发布的开发包带有资源时的处理方式;

    一般是类文件导出成jar包,和资源文件分开,一起提供给客户。客户端的apk把资源打包到自己的apk中,此时jar包中的类不能再以资源id来访问资源,而是使用由apk层传过来的Context对象加上资源路径来访问。如下:

public int getDrawableID(Context context, String strPath) {
        return context.getResources().getIdentifier(strPath,
                "drawable", context.getPackageName());
    }

    网上关于这种方式的论述有很多,这里不再赘述。


四. 为什么Android系统资源包Android.jar中的资源可以被访问;

    这个是最困扰我的一个问题。这要从android资源编译打包、系统资源引用方式方面说起。以下2篇博文对这个问题有些论述:   

Android工程编译过程:
http://www.cnblogs.com/devinzhang/archive/2011/12/20/2294686.html
Android应用程序资源的编译和打包过程分析
http://blog.csdn.net/luoshengyang/article/details/8744683
    首先,来看看 Android.jar 中有些什么内容。用解压工具打开Android.jar包,可以看到以下内容:

    这其中包括了Android Framework层的类库、res中包含了系统资源、android目录下的R类中包含了系统资源对应的id、resources.arsc是资源索引表。
    Android程序编译时会先使用AAPT(Android Asset Packaging Tool)资源编译工具编译资源,这个工具也能查看jar包或者apk包中的资源id及其对应的资源名称的对应关系,事实上这个对应关系存储在resources.arsc文件中。现在我们使用AAPT命令来查看一下android-17中的android.jar包中的资源id索引情况。在命令行中进入到SDK的platforms\android-17目录下,输入以下命令 :aapt d resources android.jar > android_jar_res.txt,可以得到一个记录了id、名字相互索引的android_jar_res.txt文件,部分内容如下:
Package Groups (1)
Package Group 0 id=1 packageCount=1 name=android
  Package 0 id=1 name=android typeCount=20
    type 0 configCount=1 entryCount=1112
      spec resource 0x01010000 android:attr/theme: flags=0x40000000
      spec resource 0x01010001 android:attr/label: flags=0x40000000
      spec resource 0x01010002 android:attr/icon: flags=0x40000000
      spec resource 0x01010003 android:attr/name: flags=0x40000000
      spec resource 0x01010004 android:attr/manageSpaceActivity: flags=0x40000000
      spec resource 0x01010005 android:attr/allowClearUserData: flags=0x40000000
      spec resource 0x01010006 android:attr/permission: flags=0x40000000
    可见Android.jar包中确实包含了系统资源id及其名字的索引关系。

    Android app编译时会执行aapt资源编译命令,使用命令行编译的命令如下:
     aapt p -f -m -J gen -S res -I ~/android-sdk-linux/platforms/android-17/android.jar -M AndroidManifest.xml
    其中的-I命令的解释是:  -I  add an existing package to base include set, 即添加一个现有的包作为基础引入包。查看 aapt 的源码可知aapt命令执行时会解析这个包,然后就可以使用解析出来的id了,亦即系统资源id!所以在Android app编译时,app中引用的系统资源id能被识别并正确的引用。

    Android app虽然引用了系统资源,但其apk包中并不包含系统资源拷贝(这点从apk包的大小就可以看出来),而是在运行时加载了系统资源包,从而通过系统资源id访问到了系统资源。这个系统资源却并不是Androd.jar,而是:/system/framework/framework-res.apk。这个apk在应用程序启动时由AssetManager加载,具体加载过程可以查看老罗的博文: Android应用程序资源管理器(Asset Manager)的创建过程分析 。
    app编译时根据Android.jar包已经确定好了系统资源id,但是运行时加载的却是framework-res.apk,所以Android.jar和framework-res.apk应该有某种意义上的对应关系。 我们使用adb命令,把位于虚拟机的 /system/framework/framework-res.apk 文件pull到PC上,然后用
aapt d resources framework-res.apk > framework-res.txt
命令得到 记录系统id、名字索引关系的文件 framework-res.txt 经过与 android.jar包产生的 android_jar_res.txt 对比发现, 他们的id、名字索引关系是一样的!可知使用Android.jar中的id在 framework-res.apk 中不会访问到错误的资源!这也是为什么 所以应用程序即使不包含图片资源也能显示美观的界面 ,并且 同一个app安装到不同的Android系统中可以表现为不同的形式,因为运行时动态加载嘛!

在此 总结一下 为什么Android系统资源包Android.jar中的资源可以被访问
1. app引入了系统资源,这些系统资源及其id和名字的索引包含在Android.jar包中。
2. app编译时会执行aapt资源编译打包命令,aapt资源编译打包命令的-I 参数,引入了Android.jar,所以app在编译的时候,系统资源id能被识别。
3. apk包中只包含了对系统资源id的索引,并不包含真正的资源,否则apk包不会那么小。
4. apk在运行时加载的系统资源其实包含在/system/framework/framework-res.apk包中,这个包的资源索引表跟Android.jar包相同,在apk运行时由framework层中的 AssertManager自动加载,app需要引用系统资源时,通过使用编译时固定的id到framework-res.apk包中查找。
    所以 Android.jar中的资源可以被访问 其实是个假象, app只是应用了位于其中的资源id及索引, 这一步是在编译时就完成了。真正的资源访问是在运行时 framework-res.apk包中查找 的。


你可能感兴趣的:(Android 以jar包方式共享资源注意事项)