Android 获取资源的过程分析

来源:互联网 发布:java web工作流开发 编辑:程序博客网 时间:2024/05/29 08:11

Android获取Resources有两种方法,第一种是通过Context,第二种是通过PackageManager。
1. 通过Context获取
在以往的程序开发时,大家经常使用getResources.getXXX()方法获取XML文件中定义的资源,比如getDrawable()、getString()、getBoolean()等。这些是怎么访问的呢?流程是怎么样的呢?
首先看看getResources()方法。该方法是Context类成员函数,一般是在Activity对象或者Service对象中调用,因为Activity和Service本质上是一个Context,而真正的实现Context接口的是ContextImpl类。
ContextImpl对象是在ActivityThread类中创建的。所以getResources()方法实际上是调用到ContextImpl类中的getResouces()方法,在ContextImpl类中,返回的是其内部变量mResouces变量,而对该变量的赋值是在init()方法中,在创建contextImpl对象后,一般会调用init()方法对ContextImpl对象的内部变量初始化,其中就包括初始化mResources变量。

final void init(ActivityThread.Packageinfo packageInfo,Ibinder activityToken ,ActivityThread mainThread,Resources container){    mPackageInfo=packageInfo;    mResources=mPackagesInfo.getResources(mainThread); }

以上代码中,mResouces又是调用mPackagesInfo的getResouces()方法进行赋值。而一个应用程序中多个contextImpl对象实际上是共享一个PackageInfo对象,这就意味着多个ContextImpl对象中的mResource
变量实际上也只有同一个Resource对象。
PackageInfo的getResources()方法的代码如下:

public Resources getResouces(ActivityThread mainThread){   if(mResouces==null){      mResouces=mainThread.getToplevelResources(mResDir,this);   }   return mResource;}

以上代码中参数中,参数变量指的是ActivityThread对象,一个应用程序中只有一个该对象,其getTopLevelResource()方法的语义得到本应用程序的对应的资源对象。
变量mActivieResources对象保存内部保存了该应用程序所使用到的所有Resource对象,其类型为HashMap

ResourcesKey key=new ResourcesKey(reDir,cominfo.applicationScale);

即用reDir和另一个变量构造,reDir变量的含义是资源文件的所在路径,实际上是指Apk所在的路径,比如可以是:data/app/com.haii.android.client.apk,该文件对应的data/dalvik-cache目录为:data@app@com.haii.android.client.apk@class.dex文件,这两个文件是一个应用程序安装成功后自动生成的。
所以,如果一个应用程序没有访问该程序以为的文件资源,那么mActivieResources变量中就仅有一个Resources对象。这也从侧面说明,mActivieResources内部可能包含多个Resources对象,条件是必须有不同的ResourcesKey,也就是必须有不同的redir,这就意味着一个程序可以访问另一个APK文件,读取其中的资源,这个结论非常有意义,简单的想法就是,可以设计成一种框架,不同的资源对应不同的APK,然后主应用程序可以读取不同的资源文件,从而可以实现系统的主题切换。
接上面,如果mActivieResources对象中还没包含所要的Resources对象,那么,就重新建立一个Resources对象,建立方法如下:

AssetManager assets=new AssetManager();if(assets.addAssetPath(resDir)==0){    return null;}DisplayMetric=getDisplayMetricsLocked(false);r=new Resoureces(assets,metrics,getConfiguretion(),comInfo);

这里可以发现,构造Resoureces对象需要先构造一个AssetManager对象,然后把这个对象作为Resouces构造函数的参数即可。AsetManager是个什么东西呢?
在应用程序开发时,曾经使用该对象,但是构造该对象的方法使用Resources对象的getAssets()方法,并没有使用构造函数,实际上getAssets()所获得AssetManager对象正是这里创建的,AssetManager其实并不只是访问项目的res/assets目录下面的资源,而是访问res下所有的资源。
AssetManager类中的几个关键函数都是native实现的。以上代码的中的addAssetpath(resDir)非常关键,它为所创建的AssetManager对象添加了一个资源路径,剩下的事情就有AssetManager内部完成了,内部会从指定的路径处获取任何资源。
AssetManger的构造函数如下:

public AssetManager(){    synchronized(this){        if(DEBUG_REFS){            mnumRefs=0;            incRefsLocked(this.hasCode());        }        init();        if(localLOGV) Log.v(TAG,"new asset");        ensureSystemAssets();    }}

该构造函数有两个重要的方法,一个是init(),另一个是ensureSystemAssets(),这两个方法都是native实现的。init()的作用是初始化AssetManager的内部环境变量,而初始化过程的一个关键人物是把Framework中的资源路径添加到该AssetManager对象中去,其native代码如下,该代码见android_util_AssetManager.cpp文件。

Static void android_content_AssetManager_init(JNIEnv* env,jobject clazz){    AssetManager * am= new AssetManager();    if(am==null){        doThrow(env,"java/lang/OutOfMemoryError");    }    am-addDefaultAssets();    env->SetIntfield(clazz,gAssetManagerOffsets.mObject,(jint)am);}

以上代码首先创建一个AssetManager对象,这个是C++类,然后调用am->addDefaultAsset()方法,该方法的作用是把Framework中的资源文件添加到AssetManager对象的路径中,最后调用setIntfield把C++创建的AssetManager对象的引用保存到java端的mObject变量中,该变量可以在java端的AssetManager类中找到,其类型是int。
addDefaultAssets()的代码如下,见AssetManager.cpp文件

bool AssetManager::addDefaultAssets(){    const char* root=getenv("ANDROID_ROOT");    Strings path(root);    path.appenPath(kSystemAssets);    return addAssetpath(path,NULL);}

该函数中首先获取Android的根目录,getenv()是一个Linux系统调用。用户同样可以使用一下终端命令获取该值,如下:

adb shellecho $ANDROID_ROOT

获得根目录之后,在与kSystemAssets路径进行组合,该变量的定义如下:

static const char* kSystemAssets="framework/framework-res.apk";

所以最终获得的路径文件名称是/system/framework/framework-res.apk,这正是Framework对应的资源文件。
再看看ensureSystemAssets()。该方法仅仅是在Framework启动时调用,因为mSystem是一个static变量,该变量在Zygote启动时已经赋值,所以以后所有应用程序运行时该值都不为空,所以该方法如同虚设。
因为应用程序中的Resource对象内部的AssetManager对象除了包含应用本身的资源路径外,还包含了Framework的资源路径,这就是应用程序使用本地Resources对象就可以访问系统的资源原因。比如

Resoureces res=getResources();res.getDrawable(android.R.drawable.ic_menu.add);

在AssetManager.cpp文件中,当时用getXXX(int id)访问资源时,如果id值小于0x1000 0000时,AssetManager会认为要访问系统资源,因为aapt在对系统资源进行编译的时,所有的资源id都会被编译为小于该值的一个int值,而当访问应用程序对应资源时,id值都大于0x7000 0000。
创建好了Resources对象后,就把该对象缓存到mActiviteResource变量中,以以后继续使用。这就是访问Resources内部整个流程。

总结为:
ContextImpl.getResources()->ActivityThread.packageinfo.getResources()->ActivityThread.getTopLevelResources()->AssetManager.cpp(system/framework/framework-res.apk data/app/*.apk)

2.通过PackageManager获取
该方法主要用于访问其他程序的资源,其典型的就是应用切换主题,但这种切换仅限应用程序的内部,而不是整个系统。
使用PackageManager获取Resources对象的代码如下:
其中getPackageManager()用于返回一个PackageManager对象,该对象是一个本地对象,但是对象内的方法一般是调用远程PackageManagerService。其代码如下:

@Overridepublic PackageManager getPackageManager(){    if(mPackageManager!=null){        retur mPackageManager;    }    IPackageManager pm=ActivityThread.getPackageManager();    if(pm!=null){        return (mPackage=new ApplicationPackageManager(this,pm));    }    return null;}

PackageManager本身是一个abstract类,以上代码可以看出,真正实现这个类是ApplicationPackageManager类。该类的构造函数包含了远程服务的一个引用,即IpackageManager,该对象是通过调用getPackageManager()金泰方法获取的,这种获取远程服务的方法和大多数和获取远程服务的调用类似,其代码如下:

public static IPackageManager getPackageManager(){   if(sPackagemanager!=null){        return sPackageManager;   }   IBinder b=ServiceManager.getService("package");   sPackageManager=IPackageManager.stub.asInterface(b);     return sPackageManager;}

获得了PackageManager对象后,接着调用getResourceForApplication()方法,该方法的代码在contextImpl.ApplicationManager子类中,其代码如下:

@Overridepublic Resources getResoucesForApplication(ApplicationInfo app)throw NameNotFoundException{    if(app.packageName.equals("system")){        return mContext.mMainThread.getSystemContext().getResources();    }    Resources r=mContext.mMainThread.getTopLevelResources(app.uid=Process.myUid()?app.sourceDir:app.pulicSourceDir,mContext.mPackageInfo);    if(r!=null){        return r;    }    throw new NameNotFoundException("unable to open "+app.pulicSource);}

以上代码内部调用了mMainthread.getTopLevelResources(),这又回到了上一节通过获取Resources对象的过程,注意这里面的参数,其含义是:如果目标资源程序和当前程序是同一个uid,那么就使用目标程序sourceDir作为路径,否则就使用目标程序的publicSourceDir目录,该目录可以在目标程序的AndroidManifest.xml文件制定,在大多数情况下,目标程序和当前程序都不属于同一个uid,因此,多为publicSourceDir,该值在默认情况和sourceDir相同的。
当进入mainThread.getTopLevelResources()方法中后,全局ActivityThread对象就会在mActivieResources变量保存了一个新的Resources对象,其键值对应目标应用程序的包名。

可总结为
ContextImpl.getPackageManager()->ContextImpl.ApplicationPackageManager(getResourceForApplication 目标程序包名)->ActivityThread.getTopLevelResources(目标程序包名)。

原创粉丝点击