zoukankan      html  css  js  c++  java
  • Android动态资源加载原理和应用

    动态加载资源原理

    通常我们调用getResources()方法获取资源文件

    public Resources getResources() {
        return mResources;
    }
    mResources是在创建ContextImp对象后的init方法里面创建的

    mResources = mPackageInfo.getResources(mainThread);
    调用了LoadedApk的getResources方法
    public Resources getResources(ActivityThread mainThread) {
        if (mResources == null) {
            mResources = mainThread.getTopLevelResources(mResDir,
                    Display.DEFAULT_DISPLAY, null, this);
        }
        return mResources;
    }
    又调用到了ActivityThread类的getTopLevelResources方法

    Resources getTopLevelResources(String resDir, int displayId, Configuration overrideConfiguration, CompatibilityInfo compInfo) {
        ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, compInfo.applicationScale, compInfo.isThemeable);
        Resources r;
        synchronized (mPackages) {
            // ...
            WeakReference<Resources> wr = mActiveResources.get(key);
            r = wr != null ? wr.get() : null;
            if (r != null && r.getAssets().isUpToDate()) {
                if (false) {
                    Slog.w(TAG, "Returning cached resources " + r + " " + resDir
                            + ": appScale=" + r.getCompatibilityInfo().applicationScale);
                }
                return r;
            }
        }
        
        AssetManager assets = new AssetManager();
        assets.setThemeSupport(compInfo.isThemeable);
        if (assets.addAssetPath(resDir) == 0) {
            return null;
        }
    
        // ...
    
        r = new Resources(assets, dm, config, compInfo);
        if (false) {
            Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
                    + r.getConfiguration() + " appScale="
                    + r.getCompatibilityInfo().applicationScale);
        }
    
        synchronized (mPackages) {
            WeakReference<Resources> wr = mActiveResources.get(key);
            Resources existing = wr != null ? wr.get() : null;
            if (existing != null && existing.getAssets().isUpToDate()) {
                // Someone else already created the resources while we were
                // unlocked; go ahead and use theirs.
                r.getAssets().close();
                return existing;
            }
            
            // XXX need to remove entries when weak references go away
            mActiveResources.put(key, new WeakReference<Resources>(r));
            return r;
        }
    }
    ResourcesKey使用resDir和其他参数来构造,这里主要是resDir参数,表明资源文件所在的路径。也就是APK程序所在路径。

    ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, compInfo.applicationScale, compInfo.isThemeable);
    上面代码的主要逻辑是获取Resources对象,从一个Map变量mActiveResources获取,这个Map维护了ResourcesKey和WeakReference<Resources>的对应关系。如果不存在就创建它,并且添加到Map中。

    因此只要这个Map中包含多个指向不同资源路径的Resources对象或者说我们有指向不同路径的资源的Resources对象,就可以访问多个路径的资源,即有实现访问其他APK文件中的资源的可能。

    创建Resources对象的主要逻辑为

    AssetManager assets = new AssetManager();
    assets.setThemeSupport(compInfo.isThemeable);
        if (assets.addAssetPath(resDir) == 0) {
            return null;
    }
     r = new Resources(assets, dm, config, compInfo);
    首先创建AssetManager对象,然后用其创建Resources对象。我们以前使用getAssets方法读取assets文件夹中的文件,其实他就是在这里创建的。

    AssetManager的构造函数:

    public AssetManager() {
        synchronized (this) {
            if (DEBUG_REFS) {
                mNumRefs = 0;
                incRefsLocked(this.hashCode());
            }
            init();
            if (localLOGV) Log.v(TAG, "New asset manager: " + this);
            ensureSystemAssets();
        }
    }
    init()函数也是一个native函数,其native代码在android_util_AssetManager.cpp中

    static void android_content_AssetManager_init(JNIEnv* env, jobject clazz)
    {
        AssetManager* am = new AssetManager();
        if (am == NULL) {
            jniThrowException(env, "java/lang/OutOfMemoryError", "");
            return;
        }
        // 将Framework的资源文件添加到AssertManager对象的路径中。
        am->addDefaultAssets();
    
        ALOGV("Created AssetManager %p for Java object %p
    ", am, clazz);
        env->SetIntField(clazz, gAssetManagerOffsets.mObject, (jint)am);
    }
    
    bool AssetManager::addDefaultAssets()
    {
    	// /system
        const char* root = getenv("ANDROID_ROOT");
        LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");
    
        String8 path(root);
        // kSystemAssets定义为static const char* kSystemAssets = "framework/framework-res.apk";
        // 因此,path为/system/framework/framework-res.apk,framework对应的资源文件
        path.appendPath(kSystemAssets);
    
        return addAssetPath(path, NULL);
    }
    到此为止,在创建AssetManager的时候完成了添加framework资源,然后添加本应用的资源路径,即调用addAssetPath方法

    /**
     * Add an additional set of assets to the asset manager.  This can be
     * either a directory or ZIP file.  Not for use by applications.  Returns
     * the cookie of the added asset, or 0 on failure.
     * {@hide}
     */
    public native final int addAssetPath(String path);
    也是一个native方法,其native代码在android_util_AssetManager.cpp中

    static jint android_content_AssetManager_addAssetPath(JNIEnv* env, jobject clazz, jstring path)
    {
        ScopedUtfChars path8(env, path);
        if (path8.c_str() == NULL) {
            return 0;
        }
        AssetManager* am = assetManagerForJavaObject(env, clazz);
        if (am == NULL) {
            return 0;
        }
    
        void* cookie;
        // 在native代码中完成添加资源路径的工作
        bool res = am->addAssetPath(String8(path8.c_str()), &cookie);
    
        return (res) ? (jint)cookie : 0;
    }
    可以看到,Resources对象的内部AssetManager对象包含了framework的资源还包含了应用程序本身的资源,因此这也就是为什么能使用getResources函数获得的resources对象来访问系统资源和本应用资源的原因。

    受此过程的提醒,我们是不是可以自己创建一个Resources对象,让它的包含我们指定路径的资源,就可以实现访问其他的资源了呢?答案是肯定的,利用这个思想可以实现资源的动态加载,换肤、换主题等功能都可以利用这种方法实现。

    于是,主要思想就是创建一个AssetManager对象,利用addAssetPath函数添加指定的路径,用其创建一个Resources对象,使用该Resources对象获取该路径下的资源。

    需要注意的是addAssetPath函数是hide的,可以使用反射调用。

    public void loadRes(String path){
    	try {
    		assetManager = AssetManager.class.newInstance();
    		Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
    		addAssetPath.invoke(assetManager, path);
    	} catch (Exception e) {
    	}
    	resources = new Resources(assetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
    	// 也可以根据资源获取主题
    }
    
    这里的参数path就是APK文件的路径,可以通过以下方式获取

    getPackageManager().getApplicationInfo("xxx", 0).sourceDir;
    并且还可以重写Context的getResources方法,getAsset方法,提高代码的一致性。

    @Override
    public Resources getResources() {
    	return resources == null ? super.getResources() : resources;
    }
    @Override
    public AssetManager getAssets() {
    	return assetManager == null ? super.getAssets() : assetManager;
    }
    于是在加载了资源之后就可以通过该Resources对象获取对应路径下面的资源了。

    动态加载资源

    两种不同风格的按钮,默认的是本应用提供的资源,还有一种作为另一个单独的插件APK程序存放在手机的其他路径中,当选择不同的风格时加载不同的图片资源。


    插件APK仅仅包含了一些资源文件。

    宿主程序的代码具体如下

    private AssetManager assetManager;
    	private Resources resources;
    	private RadioGroup rg;
    	private ImageView iv;
    
    	@Override
    	protected void onCreate(Bundle savedInstanceState) {
    		super.onCreate(savedInstanceState);
    		setContentView(R.layout.activity_main);
    		iv = (ImageView) findViewById(R.id.iv);
    		rg = (RadioGroup) findViewById(R.id.rg);
    		rg.setOnCheckedChangeListener(new OnCheckedChangeListener() {
    			@Override
    			public void onCheckedChanged(RadioGroup group, int checkedId) {
    				switch (checkedId) {
    				case R.id.default_skin:
    					assetManager = null;
    					resources = null;
    					iv.setImageDrawable(getResources().getDrawable(R.drawable.ic_launcher));
    					break;
    				case R.id.skin1:
    			        String dexPath = "";
    			        try {
    						dexPath = getPackageManager().getApplicationInfo("com.example.plugin", 0).sourceDir;
    					} catch (NameNotFoundException e) {
    						e.printStackTrace();
    					}
    					loadRes(dexPath);
    					// 由于重写了getResources方法,因此这时返回的是我们自己维护的Resources对象,因此可以访问到他的编号id的资源
    					iv.setImageDrawable(getResources().getDrawable(0x7f020000));
    					break;
    				}
    			}
    		});
    	}
    	
    	public void loadRes(String path){	
    		try {
    			assetManager = AssetManager.class.newInstance();
    			Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
    			addAssetPath.invoke(assetManager, path);
    		} catch (Exception e) {
    		}
    		resources = new Resources(assetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
    	}
    	
    	
    	@Override
    	public Resources getResources() {
    		return resources == null ? super.getResources() : resources;
    	}
    	
    	@Override
    	public AssetManager getAssets() {
    		return assetManager == null ? super.getAssets() : assetManager;
    	}
    可以查到,插件APK中的额ic_launcher图片的id为0x7f020000,于是可以通过该id值获取到对应的资源

    public static final int ic_launcher=0x7f020000;
    当然这样的耦合性太高了,可以用来说明原理,但看起来不是很直观,因为这个id只有查看了插件APK的代码才知道,因此可以让插件APK提供返回这个id的函数,由宿主APK来调用,具体可以通过反射也可以通过接口。

    插件APK提供getImageId函数获取图片资源的id

    public class Plugin {
    	public static int getImageId() {
    		return R.drawable.ic_launcher;
    	}
    }
    这样在加载完资源后,可以调用以下方法来获取该图片资源

    private void setImage(String dexPath) {
    		DexClassLoader loader = new DexClassLoader(dexPath, getApplicationInfo().dataDir, null, this.getClass().getClassLoader());
    		try {
    			Class<?> clazz = loader.loadClass("com.example.plugin.Plugin");
    			Method getImageId = clazz.getMethod("getImageId");
    			int ic_launcher = (int) getImageId.invoke(clazz);
    			iv.setImageDrawable(getResources().getDrawable(ic_launcher));
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}

    插件管理的一种方式

    对于每个插件,在AndroidManifest.xml中声明一个空的Activity,并添加他的action,比如:

    <activity
                android:name=".plugin" >
                <intent-filter>
                    <action android:name="android.intent.plugin" />
                </intent-filter>
            </activity>
    这样在宿主程序中就可以查到对应的插件,以供选择加载。

    PackageManager pm = getPackageManager();
    List<ResolveInfo> resolveinfos = pm.queryIntentActivities(intent, 0);
    ActivityInfo activityInfo = resolveinfos.get(i).activityInfo;
    dexPaths.add(activityInfo.applicationInfo.sourceDir);
    效果:


    宿主程序的代码

    private AssetManager assetManager;
    private Resources resources;
    private LinearLayout ll;
    private ImageView iv;
    private Button btn;
    private List<String> dexPaths = new ArrayList<String>();
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    	super.onCreate(savedInstanceState);
    	setContentView(R.layout.activity_main);
    	iv = (ImageView) findViewById(R.id.iv);
    	ll = (LinearLayout) findViewById(R.id.ll);
    	btn = (Button) findViewById(R.id.btn);
    	btn.setOnClickListener(new OnClickListener() {
    		@Override
    		public void onClick(View v) {
    			resources = null;
    			iv.setImageDrawable(getResources().getDrawable(R.drawable.ic_launcher));
    		}
    	});
    	
    	Intent intent = new Intent("android.intent.plugin");
    	PackageManager pm = getPackageManager();
    	final List<ResolveInfo> resolveinfos = pm.queryIntentActivities(intent, 0);
    	for (int i = 0; i < resolveinfos.size(); i++) {
    		final ActivityInfo activityInfo = resolveinfos.get(i).activityInfo;
    		dexPaths.add(activityInfo.applicationInfo.sourceDir);
    		// 根据查询到的插件数添加按钮
    		final Button btn = new Button(this);
    		btn.setText("风格" +(i+1));
    		btn.setTag(i);
    		ll.addView(btn, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT));
    		btn.setOnClickListener(new OnClickListener() {
    			
    			@Override
    			public void onClick(View v) {
    				int index = (Integer)btn.getTag();
    				String dexPath = dexPaths.get(index);
    				loadRes(dexPath);
    				setImage(resolveinfos.get(index).activityInfo);
    				
    			}
    		});
    	}
    }
    
    private void setImage(ActivityInfo activityInfo) {
    	DexClassLoader loader = new DexClassLoader(activityInfo.applicationInfo.sourceDir, getApplicationInfo().dataDir, null, this.getClass().getClassLoader());
    		try {
    			Class<?> clazz = loader.loadClass(activityInfo.packageName + ".Plugin");
    			Method getImageId = clazz.getMethod("getImageId");
    			int ic_launcher = (int) getImageId.invoke(clazz);
    			iv.setImageDrawable(getResources().getDrawable(ic_launcher));
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    }
    
    public void loadRes(String path) {
    		try {
    			assetManager = AssetManager.class.newInstance();
    			Method addAssetPath = AssetManager.class.getMethod("addAssetPath",
    					String.class);
    			addAssetPath.invoke(assetManager, path);
    		} catch (Exception e) {
    			e.printStackTrace();
    		} 
    	resources = new Resources(assetManager, super.getResources()
    			.getDisplayMetrics(), super.getResources().getConfiguration());
    }
    
    @Override
    public Resources getResources() {
    	return resources == null ? super.getResources() : resources;
    }
    
    @Override
    public AssetManager getAssets() {
    	return assetManager == null ? super.getAssets() : assetManager;
    }
    两个插件程序:

    com.example.plugin

        |-- Plugin.java

    com.example.plugin2

        |-- Plugin.java

    Plugin类的内容一样,为提供给宿主程序反射调用的类

    注册空的activity

    <activity
          android:name=".plugin"
          android:label="@string/name" >
          <intent-filter>
                <action android:name="android.intent.plugin" />
          </intent-filter>
    </activity>

    代码点此下载

  • 相关阅读:
    Java多线程之赛跑游戏(含生成exe文件)
    JavaSE之绘制菱形
    JavaSE项目之员工收录系统
    深度解析continue,break和return
    如何查看yum安装路径
    转载 linux umount 时出现device is busy 的处理方法--fuser
    linux安装扩展总结
    linux 编译安装amqp
    vmware 实现linux目录映射window本地目录
    yaf学习之——生成yaf示例框架
  • 原文地址:https://www.cnblogs.com/qhyuan1992/p/5385266.html
Copyright © 2011-2022 走看看