您的位置:首页 > 移动开发 > Android开发

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

2016-03-18 09:59 525 查看
 最近的一个项目是一个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个图片drawable:ic_launcher.png、ok_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_n、ok_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的以下内容导出:



    注意不要把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