Android-動(dòng)態(tài)加載插件資源,皮膚包的實(shí)現(xiàn)原理

原創(chuàng)-轉(zhuǎn)載請(qǐng)注明出處

Android動(dòng)態(tài)加載插件資源

最近在看app的換膚功能。簡(jiǎn)單的來說就是動(dòng)態(tài)讀取插件apk中的資源手负,需要進(jìn)行換膚的控件所用到的資源在主apk和插件apk中各維護(hù)了一份宁赤,且資源名稱相同鹃两。
插件聽起來高大上犁苏,但其實(shí)就是一個(gè)apk文件场绿。所以我們所要做的啊犬,就是怎么樣能讓插件中的資源加載進(jìn)本地灼擂,并且讀取到。

Resource的創(chuàng)建

在app內(nèi)部加載資源使用的是context.getResources(),context中g(shù)etResources()方法是一個(gè)抽象方法觉至,具體的實(shí)現(xiàn)在ContextImpl類中剔应。

Resources resources = packageInfo.getResources(mainThread);

參數(shù)packageInfo指向的是一個(gè)LoadedApk對(duì)象,這個(gè)LoadedApk對(duì)象描述的是當(dāng)前正在啟動(dòng)的Activity組所屬的Apk语御。

進(jìn)入到LoadedApk的getResources(mainThread)方法

public Resources getResources(ActivityThread mainThread) {
        if (mResources == null) {
            mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
                    mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
        }
        return mResources;
    }

LoadedApk類的成員函數(shù)getResources首先檢查其成員變量mResources的值是否等于null峻贮。如果不等于的話,那么就會(huì)將它所指向的一個(gè)Resources對(duì)象返回給調(diào)用者应闯,否則的話纤控,就會(huì)調(diào)用參數(shù)mainThread的成員函數(shù)getTopLevelResources來獲得這個(gè)Resources對(duì)象,然后再返回給調(diào)用者碉纺。 mainThread指向一個(gè)ActivityThread對(duì)象船万。

public final class ActivityThread {  
    ......  
    final HashMap<ResourcesKey, WeakReference<Resources> > mActiveResources  
            = new HashMap<ResourcesKey, WeakReference<Resources> >();  
      
    Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {  
        ResourcesKey key = new ResourcesKey(resDir, compInfo.applicationScale);  
        Resources r;  
        synchronized (mPackages) {  
            ......  
            WeakReference<Resources> wr = mActiveResources.get(key);  
            r = wr != null ? wr.get() : null;  
            ......  
            if (r != null && r.getAssets().isUpToDate()) {  
                ......  
                return r;  
            }  
        }  
        ......  
        AssetManager assets = new AssetManager();  
        if (assets.addAssetPath(resDir) == 0) {  
            return null;  
        }  
        ......  
        r = new Resources(assets, metrics, getConfiguration(), compInfo);  
        ......  
        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;  
        }  
    }  
} 

在其中創(chuàng)建了AssertManager對(duì)象,assets.addAssetPath(resDir)這句話的意思是把資源目錄里的資源都加載到AssetManager對(duì)象中 如果我們把一個(gè)未安裝的apk的路徑傳給這個(gè)方法骨田,那么apk中的資源就被加載到AssetManager對(duì)象里面了唬涧。但它是一個(gè)隱藏方法,需要反射調(diào)用盛撑。有了AssertManager對(duì)象就能創(chuàng)建Resources對(duì)象了。

AssetManager介紹

Provides access to an application's raw asset files; see Resources for the way most applications will want to retrieve their resource data. This class presents a lower-level API that allows you to open and read raw files that have been bundled with the application as a simple stream of bytes.

AssetManager提供了應(yīng)用的原始資源捧搞,通過它可以讓應(yīng)用程序檢索他們的資源數(shù)據(jù)抵卫。
在ResourcesImpl類中存在AssetManager的引用mAsset.舉個(gè)例子看下Resources怎么通過AssetManager加載數(shù)據(jù).看下Resources的getString()方法

public String getString(@StringRes int id) throws NotFoundException {
        return getText(id).toString();
    }
    
    
public CharSequence getText(@StringRes int id) throws NotFoundException {
        CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
        if (res != null) {
            return res;
        }
        throw new NotFoundException("String resource ID #0x"
                                    + Integer.toHexString(id));
    }

Resources將資源id傳給了AssetManager的getResourceText方法狮荔。從AssetManager中返回了資源數(shù)據(jù)。有興趣大家可以深入研究一下介粘,這里不做過多介紹殖氏。 接下來我們寫一個(gè)小demo看看更換皮膚包的簡(jiǎn)單實(shí)現(xiàn)。

Demo

首先如果只加載本地皮膚包(帶有皮膚資源的apk)的時(shí)候姻采,我們將皮膚包放入assets文件夾內(nèi)雅采,再在初始化的時(shí)候加載進(jìn)sdcard中。如果要下載皮膚包慨亲,則直接下載進(jìn)sdcard指定目錄中婚瓜。我們現(xiàn)在只做本地皮膚包的更換。

先進(jìn)行初始化的操作:

//先定義全局的名稱和存儲(chǔ)路徑
 private static final String APK_NAME = "sample.apk";
 private static final String APK_DIR = Environment.
            getExternalStorageDirectory() + File.separator + APK_NAME;
    
public void init(Context context) {

        File pluginFile = new File(APK_DIR);
        if (pluginFile.exists()) {
            pluginFile.delete();
        }

        InputStream is = null;
        FileOutputStream fos = null;

        try {
            is = context.getAssets().open(APK_NAME);
            fos = new FileOutputStream(APK_DIR);
            int bytes;
            byte[] byteArr = new byte[1024 * 4];
            while ((bytes = is.read(byteArr, 0, 1024 * 4)) != -1) {
                fos.write(byteArr, 0, bytes);
            }
            PackageManager mPm = context.getPackageManager();
            PackageInfo mInfo = mPm.getPackageArchiveInfo(APK_DIR, PackageManager.GET_ACTIVITIES);
            mSkinPackageName = mInfo.packageName;

            AssetManager assetManager = AssetManager.class.newInstance();
            Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
            method.invoke(assetManager, pluginFile.getAbsolutePath());

            mSuperResources = context.getResources();
            mResources = new Resources(assetManager, mSuperResources.getDisplayMetrics(),
                    mSuperResources.getConfiguration());

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }   
    

讀取assets下的sample.apk刑棵,將其放進(jìn)sdcard中巴刻。通過反射創(chuàng)建AssetManager,并調(diào)用addAssetPath方法,將apk的路徑傳入AssetManager中蛉签。再new 一個(gè)Resources對(duì)象胡陪,傳入上步生成的AssetManager對(duì)象,這時(shí)就拿到了皮膚包apk的Resources對(duì)象碍舍。初始化完成柠座。

接下來在布局中放入一個(gè)TextView,動(dòng)態(tài)替換TextView控件用到的資源。

<TextView
        android:id="@+id/tv_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/app_name"
        android:textSize="20sp"
        android:layout_centerHorizontal="true"
        android:textColor="@color/colorPrimaryDark"
        android:background="@mipmap/ic_launcher"
        />

代碼實(shí)現(xiàn)

btnLoad = (Button) findViewById(R.id.btn_load);
        tvName = (TextView) findViewById(R.id.tv_name);
        btnLoad.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                tvName.setBackground(ResourceManager.getInstance()
                        .loadMipmapResource(R.mipmap.ic_launcher));

                tvName.setText(ResourceManager.getInstance()
                        .loadStringResource(R.string.app_name));

                tvName.setTextColor(ResourceManager.getInstance()
                        .loadColorResource(R.color.colorPrimaryDark));

            }
        });

ok,這時(shí)候我們看下ResourceManager.getInstance().loadMipmapResource(R.mipmap.ic_launcher)的實(shí)現(xiàn)片橡;

    public Drawable loadMipmapResource(int resId){
        return mResources.getDrawable(findTrueResId(resId,"mipmap"));
    }

    /**
     * 找到插件app中rescource的真正id
     * @param resId 主app中的資源id
     */
    private int findTrueResId(int resId,String defType){
        String entryName = mSuperResources.getResourceEntryName(resId);
        Log.e(TAG, "entryName " + entryName);
        String resourceName = mSuperResources.getResourceName(resId);
        Log.e(TAG, "resourceName " + resourceName);
        int trueResId = mResources.getIdentifier(entryName, defType,mSkinPackageName);
        Log.e(TAG, "trueResId " + trueResId);

        return trueResId;
    }

上面的代碼,mSuperResources是當(dāng)前apk的Resources對(duì)象妈经,通過getResourceEntryName(resId),拿到resId對(duì)應(yīng)的名稱.
還有一個(gè)方法锻全,getResourceName狂塘,這個(gè)和getResourceEntryName的區(qū)別在于,getResourceName拿到的全名包括包名鳄厌,getResourceEntryName拿到的是簡(jiǎn)短名稱.這里我們使用getResourceEntryName方法荞胡。
調(diào)用皮膚包的Resources對(duì)象的getIdentifier方法,會(huì)返回資源在皮膚包中的真實(shí)id了嚎,將真實(shí)id拿到后泪漂,就可以調(diào)用getDrawable(trueId)來加載資源了。來看下打印出來的日志歪泳。

xyz.ibat.pluginsloader E/DONG: entryName ic_launcher
xyz.ibat.pluginsloader E/DONG: resourceName xyz.ibat.pluginsloader:mipmap/ic_launcher
xyz.ibat.pluginsloader E/DONG: trueResId 2130903047

加載string和color和上述方法原理相同萝勤。我們來看下最終效果。

換膚前:


Android動(dòng)態(tài)加載資源-換膚前

換膚后:

Android動(dòng)態(tài)加載資源-換膚后

喜歡的話就點(diǎn)個(gè)贊吧呐伞,每個(gè)贊都是我前進(jìn)的動(dòng)力~

參考資料

Android應(yīng)用程序資源管理器(Asset Manager)的創(chuàng)建過程分析
Android源碼分析-資源加載機(jī)制

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末敌卓,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子伶氢,更是在濱河造成了極大的恐慌趟径,老刑警劉巖瘪吏,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蜗巧,居然都是意外死亡掌眠,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門幕屹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蓝丙,“玉大人,你說我怎么就攤上這事望拖∶斐荆” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵靠娱,是天一觀的道長(zhǎng)沧烈。 經(jīng)常有香客問我,道長(zhǎng)像云,這世上最難降的妖魔是什么锌雀? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮迅诬,結(jié)果婚禮上腋逆,老公的妹妹穿的比我還像新娘。我一直安慰自己侈贷,他們只是感情好惩歉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著俏蛮,像睡著了一般撑蚌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搏屑,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天争涌,我揣著相機(jī)與錄音,去河邊找鬼辣恋。 笑死亮垫,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的伟骨。 我是一名探鬼主播饮潦,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼携狭!你這毒婦竟也來了继蜡?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎稀并,沒想到半個(gè)月后鲫剿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡稻轨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了雕凹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片殴俱。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖枚抵,靈堂內(nèi)的尸體忽然破棺而出线欲,到底是詐尸還是另有隱情,我是刑警寧澤汽摹,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布李丰,位于F島的核電站,受9級(jí)特大地震影響逼泣,放射性物質(zhì)發(fā)生泄漏趴泌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一拉庶、第九天 我趴在偏房一處隱蔽的房頂上張望嗜憔。 院中可真熱鬧,春花似錦氏仗、人聲如沸吉捶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)呐舔。三九已至,卻和暖如春慷蠕,著一層夾襖步出監(jiān)牢的瞬間珊拼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工砌们, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留杆麸,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓浪感,卻偏偏與公主長(zhǎng)得像昔头,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子影兽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容