Android換膚技術總結

原文出處:
http://blog.zhaiyifan.cn/2015/09/10/Android%E6%8D%A2%E8%82%A4%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93/java

背景

縱觀如今各類Android app,其換膚需求能夠歸爲android

  • 白天/黑夜主題切換(或者別的名字,一般2套),如同花順/自選股/每天動聽等,UI表現爲一個switcher。git

  • 多種主題切換,一般爲會員特權,如QQ/QQ空間。github

對於第一種來講,目測應該是直接經過本地theme來作的,即全部圖片/顏色的資源都在apk裏面打包了。
而對於第二種,則相對複雜一些,因爲做爲一種線上服務,可能上架新皮膚,且那麼多皮膚包放在apk裏面實在太佔體積了,因此皮膚資源會在選擇後再進行下載,也就不能直接使用android的那套theme。數組

技術方案

內部資源加載方案和動態下載資源下載兩種。
動態下載能夠稱爲一種黑科技了,由於每每須要hack系統的一些方法,因此在部分機型和新的API上有時候可能有坑,但相對好處則不少app

  • 圖片/色值等資源因爲是後臺下發的,能夠隨時更新工具

  • APK體積減少性能

  • 對應用開發者來講,換膚幾乎是透明的,不須要關心有幾套皮膚字體

  • 能夠做爲增值服務賣錢!!優化

內部資源加載方案

內部資源加載都是經過android自己那套theme來作的,相對業務開發來講工做量更大(須要定義attr和theme),不一樣方案相似地都是在BaseActivity裏面作setTheme,差異主要在解決如下2個問題的策略:

  • setTheme後如何實時刷新,而不用從新建立頁面(尤爲是listview裏面的item)。

  • 哪些view須要刷新,刷新什麼(背景?字體顏色?ImageView的src?)。

自定義view

MultipleTheme
作自定義view是爲了在setTheme後會去當即刷新,更新頁面UI對應資源(如TextView替換背景圖和文字顏色),在上述項目中,則是經過對rootView進行遍歷,對全部實現了ColorUiInterface的view/viewgroup進行setTheme操做來實現即便刷新的。
顯然這樣過重了,須要把應用內的各類view/viewgroup進行替換。
手動綁定view和要改變的資源類型

Colorful
這個…咱們看看用法吧…

ViewGroupSetter listViewSetter = new ViewGroupSetter(mNewsListView);
// 綁定ListView的Item View中的news_title視圖,在換膚時修改它的text_color屬性
listViewSetter.childViewTextColor(R.id.news_title, R.attr.text_color);

// 構建Colorful對象來綁定View與屬性的對象關係
mColorful = new Colorful.Builder(this)
        .backgroundDrawable(R.id.root_view, R.attr.root_view_bg)
        // 設置view的背景圖片
        .backgroundColor(R.id.change_btn, R.attr.btn_bg)
        // 設置背景色
        .textColor(R.id.textview, R.attr.text_color)
        .setter(listViewSetter) // 手動設置setter
        .create(); // 設置文本顏色

我就是想換個皮膚,還得在activity裏本身去設置要改變哪一個view的什麼屬性,對應哪一個attribute?是否是成本過高了?並且activity的邏輯也很容易被弄得亂七八糟。

動態資源加載方案

resource替換

開源項目可參照Android-Skin-Loader
即覆蓋application的getResource方法,優先加載本地皮膚包文件夾下的資源包,對於性能問題,能夠經過attribute或者資源名稱規範(如須要換膚則用skin_開頭)來優化,從而不對不換膚的資源進行額外開銷。
能夠重點關注該項目中的SkinInflaterFactory和SkinManager(實現了本身的getColor、getDrawable方法)。
不過因爲Android 5.1源碼裏,getDrawable方法的實現被修改了,因此會致使沒法跟膚的問題(實際上是loadDrawable被修改了,連參數都改了,相似的內部API大改在5.1上還不少)。
4.4的源碼中Resources.java:

public Drawable getDrawable(int id) throws NotFoundException {
    TypedValue value;
    synchronized (mAccessLock) {
        value = mTmpValue;
        if (value == null) {
            value = new TypedValue();
        } else {
            mTmpValue = null;
        }
        getValue(id, value, true);
    }
    // 實際資源經過loadDrawable方法加載
    Drawable res = loadDrawable(value, id);
    synchronized (mAccessLock) {
        if (mTmpValue == null) {
            mTmpValue = value;
        }
    }
    return res;
}

// loadDrawable會去preload的LongSparseArray裏面查找
/*package*/ Drawable loadDrawable(TypedValue value, int id)
        throws NotFoundException {

    if (TRACE_FOR_PRELOAD) {
        // Log only framework resources
        if ((id >>> 24) == 0x1) {
            final String name = getResourceName(id);
            if (name != null) android.util.Log.d("PreloadDrawable", name);
        }
    }

    boolean isColorDrawable = false;
    if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
            value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
        isColorDrawable = true;
    }
    final long key = isColorDrawable ? value.data :
            (((long) value.assetCookie) << 32) | value.data;

    Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key);

    if (dr != null) {
        return dr;
    }
    ...
    ...
    return dr;
}

而5.1代碼裏Resources.java:

// 能夠看到,方法參數裏面加上了Theme
public Drawable getDrawable(int id, @Nullable Theme theme) throws NotFoundException {
    TypedValue value;
    synchronized (mAccessLock) {
        value = mTmpValue;
        if (value == null) {
            value = new TypedValue();
        } else {
            mTmpValue = null;
        }
        getValue(id, value, true);
    }
    final Drawable res = loadDrawable(value, id, theme);
    synchronized (mAccessLock) {
        if (mTmpValue == null) {
            mTmpValue = value;
        }
    }
    return res;
}

/*package*/ Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {
    if (TRACE_FOR_PRELOAD) {
        // Log only framework resources
        if ((id >>> 24) == 0x1) {
            final String name = getResourceName(id);
            if (name != null) {
                Log.d("PreloadDrawable", name);
            }
        }
    }

    final boolean isColorDrawable;
    final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches;
    final long key;
    if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
            && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
        isColorDrawable = true;
        caches = mColorDrawableCache;
        key = value.data;
    } else {
        isColorDrawable = false;
        caches = mDrawableCache;
        key = (((long) value.assetCookie) << 32) | value.data;
    }

    // First, check whether we have a cached version of this drawable
    // that was inflated against the specified theme.
    if (!mPreloading) {
        final Drawable cachedDrawable = getCachedDrawable(caches, key, theme);
        if (cachedDrawable != null) {
            return cachedDrawable;
        }
    }

方法名字都改了

Hack Resources internally

黑科技方法,直接對Resources進行hack,Resources.java:

// Information about preloaded resources.  Note that they are not
// protected by a lock, because while preloading in zygote we are all
// single-threaded, and after that these are immutable.
private static final LongSparseArray<Drawable.ConstantState>[] sPreloadedDrawables;
private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables
        = new LongSparseArray<Drawable.ConstantState>();
private static final LongSparseArray<ColorStateList> sPreloadedColorStateLists
        = new LongSparseArray<ColorStateList>();

直接對Resources裏面的這三個LongSparseArray進行替換,因爲apk運行時的資源都是從這三個數組裏面加載的,因此只要採用interceptor模式:

public class DrawablePreloadInterceptor extends LongSparseArray<Drawable.ConstantState>

本身實現一個LongSparseArray,並經過反射set回去,就能實現換膚,具體getDrawable等方法裏是怎麼取preload數組的,能夠本身看Resources的源碼。

等等,就這麼簡單?,NONO,少年你太天真了,怎麼去加載xml,9patch的padding怎麼更新,怎麼打包/加載自定義的皮膚包,drawable的狀態怎麼刷新,等等。這些都是你須要考慮的,在存在插件的app中,還須要考慮是否會互相覆蓋resource id的問題,進而須要修改apt,把resource id按位放在2個range。
手Q和獨立版QQ空間使用的是這種方案,效果挺好。

總結

儘管動態加載方案比較黑科技,可能由於系統API的更改而出問題,但相對來所
好處有

  • 靈活性高,後臺能夠隨時更新皮膚包

  • 相對透明,開發者幾乎不用關心有幾套皮膚,不用去定義各類theme和attr,甚至連皮膚包的打包都- - 能夠交給設計或者專門的同窗

  • apk體積節省
    存在的問題

  • 沒有完善的開源項目,若是咱們採用動態加載的第二種方案,須要的項目功能包括:

  • 自定義皮膚包結構

  • 換膚引擎,加載皮膚包資源並load,實時刷新。

  • 皮膚包打包工具

  • 對各類rom的兼容

若是有這麼一個項目的話,就一勞永逸了,有興趣的同窗能夠聯繫一下,你們一塊兒搞一搞。

內部加載方案大同小異,主要解決的都是即時刷新的問題,然而從目前的一些開源項目來看,仍然沒有特別簡便的方案。讓我選的話,我寧願讓界面從新建立,好比重啓activity,或者remove全部view再添加回來。

相關文章
相關標籤/搜索