opengl源码 实现无缝切换图片过场_手把手讲解 Android hook技术实现一键换肤
前言
產(chǎn)品大佬又提需求啦,要求app里面的圖表要實(shí)現(xiàn)白天黑夜模式的切換,以滿足不同光線下都能保證足夠的圖表清晰度. 怎么辦?可能解決的辦法很多,你可以給圖表view增加一個(gè)toggle方法,參數(shù)String,day/night,然后切換之后postInvalidate 刷新重繪.
OK,可行,但是這種方式切換白天黑夜,只是單個(gè)View中有效,那么如果哪天產(chǎn)品又要另一個(gè)View換膚,難道我要一個(gè)一個(gè)去寫toggle么?未免太low了.
那么能不能要實(shí)現(xiàn)一個(gè)全app內(nèi)的一鍵換膚?一勞永逸~~~
正文大綱
什么是一鍵換膚
界面上哪些東西是可以換膚的
利用HOOK技術(shù)實(shí)現(xiàn)優(yōu)雅的“一鍵換膚"
相關(guān)android源碼一覽
? 5. "全app一鍵換膚" Demo源碼詳解
1、什么是一鍵換膚
所謂"一鍵",就是通過"一個(gè)"接口的調(diào)用,就能實(shí)現(xiàn)全app范圍內(nèi)的所有資源文件的替換.包括 文本,顏色,圖片等。
一些換膚實(shí)現(xiàn)方式的對(duì)比:
方案1: 自定義View中,要換膚,那如同引言中所述,toggle方法,invalidate重繪。弊端:換膚范圍僅限于這個(gè)View.
方案2:給靜態(tài)變量賦值,然后重啟Activity. 如果一個(gè)Activity內(nèi)用靜態(tài)變量定義了兩種色系,那么確實(shí)是可以通過關(guān)閉Activity,再啟動(dòng)的方式,實(shí)現(xiàn) 貌似換膚的效果(其實(shí)是重新啟動(dòng)了Activity)弊端:太low,而且很浪費(fèi)資源
也許還有其他方案吧,View重繪,重啟Activity,都能實(shí)現(xiàn),但是仍然不是最優(yōu)雅的方案,那么,有沒有一種方案,能夠?qū)崿F(xiàn)全app內(nèi)的換膚效果,又不會(huì)像重啟 Activity 這樣浪費(fèi)資源呢?請(qǐng)看下圖:
這個(gè)動(dòng)態(tài)圖中,首先看到的是Activity1,點(diǎn)擊換膚,可直接更換界面上的background,圖片的src,還有textView的textColor,跳轉(zhuǎn)Activity2之后的textView顏色,在我換膚之前,和換膚之后,是不同的。換膚的過程我并沒有啟動(dòng)另外的Activity,界面也沒有閃爍。我在Activity1里面換膚,直接影響了Activity2的textView字體顏色。
既然給出了效果,那么肯定要給出Demo,不然太沒誠(chéng)意,嘿嘿嘿。
github地址奉上:https://github.com/18598925736/HookSkinDemoFromHank
2、界面上哪些東西是可以換膚的
上面的換膚動(dòng)態(tài)圖,我換了ImageView,換了background,換了TextView的字體顏色,那么到底哪些東西可以換?
答案其實(shí)就一句話: 我們項(xiàng)目代碼里面 res目錄下的所有東西,幾乎都可以被替換。
(為什么說幾乎?因?yàn)橐恍╆鹘顷戈沟臇|西我沒有時(shí)間一個(gè)一個(gè)去試驗(yàn)….囧)
具體而言就是如下這些:
動(dòng)畫
背景圖片
字體
字體顏色
字體大小
音頻
視頻
3、 利用HOOK技術(shù)實(shí)現(xiàn)優(yōu)雅的“一鍵換膚"
什么是hook?
如題,我是用hook實(shí)現(xiàn)一鍵換膚。那么什么是hook?
hook,鉤子. 安卓中的hook技術(shù),其實(shí)是一個(gè)抽象概念:對(duì)系統(tǒng)源碼的代碼邏輯進(jìn)行"劫持",插入自己的邏輯,然后放行。注意:hook可能頻繁使用java反射機(jī)制···
"一鍵換膚"中的hook思路
1 、"劫持"系統(tǒng)創(chuàng)建View的過程,我們自己來創(chuàng)建View
系統(tǒng)原本自己存在創(chuàng)建View的邏輯,我們要了解這部分代碼,以便為我所用。
2、收集我們需要換膚的View(用自定義view屬性來標(biāo)記一個(gè)view是否支持一鍵換膚),保存到變量中,劫持了系統(tǒng)創(chuàng)建view的邏輯之后,我們要把支持換膚的這些view保存起來。
3、加載外部資源包,調(diào)用接口進(jìn)行換膚,外部資源包是.apk后綴的一個(gè)文件,是通過gradle打包形成的。里面包含需要換膚的資源文件,但是必須保證,要換的資源文件,和原工程里面的文件名完全相同。
4、 相關(guān)android源碼一覽
1、Activity 的 setContentView(R.layout.XXX) 到底在做什么?
回顧我們寫app的習(xí)慣,創(chuàng)建Activity,寫xxx.xml,在Activity里面setContentView(R.layout.xxx) 。 我們寫的是xml,最終呈現(xiàn)出來的是一個(gè)一個(gè)的界面上的UI控件,那么setContentView到底做了什么事,使得XML里面的內(nèi)容,變成了UI控件呢?
請(qǐng)看下圖:
源碼索引:setContentView(R.layout.activity_main); ?->getDelegate().setContentView(layoutResID);
OK,這里暴露出了兩個(gè)方法,getDelegate()和setContentView()。
先看getDelegate:
這里返回了一個(gè)AppCompatDelegate對(duì)象,跟蹤到AppCompatDelegate內(nèi)部,閱讀源碼,可以得出一個(gè)結(jié)論:AppCompatDelegate 是替Activity生成View對(duì)象的委托類,它提供了一系列setContentView方法,在Activity中加入U(xiǎn)I控件。
2、那它的AppCompatDelegate的setContentView方法又做了什么?
找到setContentView的具體過程:
那么就進(jìn)入下一個(gè)環(huán)節(jié):LayoutInflater又做了什么?
LayoutInflater這個(gè)類是怎么把layout.xml的 變成TextView對(duì)象的?
我們知道,我們傳入的是int,是xxx.xml這個(gè)布局文件,在R文件里面的對(duì)應(yīng)int值。LayoutInflater拿到了這個(gè)int之后,又干了什么事呢?
一路索引進(jìn)去:會(huì)發(fā)現(xiàn)這個(gè)方法:
發(fā)現(xiàn)一個(gè)關(guān)鍵方法:CreateViewFromTag,tag是指的什么?其實(shí)就是 xml里面 的標(biāo)簽頭:里的TextView。
跟蹤進(jìn)去:
View?createViewFromTag(View?parent,?String?name,?Context?context,?AttributeSet?attrs,boolean?ignoreThemeAttr)?{????????if?(name.equals("view"))?{
????????????name?=?attrs.getAttributeValue(null,?"class");
????????}
????????//?Apply?a?theme?wrapper,?if?allowed?and?one?is?specified.
????????if?(!ignoreThemeAttr)?{
????????????final?TypedArray?ta?=?context.obtainStyledAttributes(attrs,?ATTRS_THEME);
????????????final?int?themeResId?=?ta.getResourceId(0,?0);
????????????if?(themeResId?!=?0)?{
????????????????context?=?new?ContextThemeWrapper(context,?themeResId);
????????????}
????????????ta.recycle();
????????}
????????if?(name.equals(TAG_1995))?{
????????????//?Let's?party?like?it's?1995!
????????????return?new?BlinkLayout(context,?attrs);
????????}
????????try?{
????????????View?view;
????????????if?(mFactory2?!=?null)?{
????????????????view?=?mFactory2.onCreateView(parent,?name,?context,?attrs);
????????????}?else?if?(mFactory?!=?null)?{
????????????????view?=?mFactory.onCreateView(name,?context,?attrs);
????????????}?else?{
????????????????view?=?null;
????????????}
????????????if?(view?==?null?&&?mPrivateFactory?!=?null)?{
????????????????view?=?mPrivateFactory.onCreateView(parent,?name,?context,?attrs);
????????????}
????????????if?(view?==?null)?{
????????????????final?Object?lastContext?=?mConstructorArgs[0];
????????????????mConstructorArgs[0]?=?context;
????????????????try?{
????????????????????if?(-1?==?name.indexOf('.'))?{
????????????????????????view?=?onCreateView(parent,?name,?attrs);
????????????????????}?else?{
????????????????????????view?=?createView(name,?null,?attrs);
????????????????????}
????????????????}?finally?{
????????????????????mConstructorArgs[0]?=?lastContext;
????????????????}
????????????}
????????????return?view;
????????}?catch?(InflateException?e)?{
????????????throw?e;
????????}?catch?(ClassNotFoundException?e)?{
????????????final?InflateException?ie?=?new?InflateException(attrs.getPositionDescription()
????????????????????+?":?Error?inflating?class?"?+?name,?e);
????????????ie.setStackTrace(EMPTY_STACK_TRACE);
????????????throw?ie;
????????}?catch?(Exception?e)?{
????????????final?InflateException?ie?=?new?InflateException(attrs.getPositionDescription()
????????????????????+?":?Error?inflating?class?"?+?name,?e);
????????????ie.setStackTrace(EMPTY_STACK_TRACE);
????????????throw?ie;
????????}
????}
這個(gè)方法有5個(gè)參數(shù),意義分別是:
View parent 父組件
String name ?xml標(biāo)簽名
Context context ? 上下文
AttributeSet attrs view屬性
boolean ignoreThemeAttr 是否忽略theme屬性
并且在這里,發(fā)現(xiàn)一段關(guān)鍵代碼:
?if?(mFactory2?!=?null)?{????????????????view?=?mFactory2.onCreateView(parent,?name,?context,?attrs);
????????????}?else?if?(mFactory?!=?null)?{
????????????????view?=?mFactory.onCreateView(name,?context,?attrs);
????????????}?else?{
????????????????view?=?null;
????????????}
實(shí)際上,可能有人要問了,你怎么知道這邊是走的哪一個(gè)if分支呢?
方法:新創(chuàng)建一個(gè)Project,跟蹤MainActivity onCreate里面setContentView()一路找到這段代碼debug你會(huì)發(fā)現(xiàn):
答案很明確了,系統(tǒng)在默認(rèn)情況下就會(huì)走Factory2的onCreateView(),應(yīng)該有人好奇:這個(gè)mFactory2對(duì)象是哪來的?是什么時(shí)候set進(jìn)去的,答案如下:
這時(shí),getDelegate()得到的對(duì)象,和 LayoutInflater里面mFactory2其實(shí)是同一個(gè)對(duì)象。
那么繼續(xù)跟蹤,一直到:AppCompatViewInflater 類:
這邊利用了大量的switch case來進(jìn)行系統(tǒng)控件的創(chuàng)建,例如:TextView
@NonNull????protected?AppCompatTextView?createTextView(Context?context,?AttributeSet?attrs)?{
????????return?new?AppCompatTextView(context,?attrs);
????}
都是new 出來一個(gè)具有兼容特性的TextView,返回出去。
但是,使用過switch的人都知道,這種case形式的分支,無法涵蓋所有的類型怎么辦呢?這里switch之后,view仍然可能是null。所以,switch之后,谷歌大佬加了一個(gè)if,但是很詭異,這段代碼并未進(jìn)入if,因?yàn)??originalContext != context并不滿足….具體原因我也沒查出來,(;′д`)ゞ
????????????//?If?the?original?context?does?not?equal?our?themed?context,?then?we?need?to?manually
????????????//?inflate?it?using?the?name?so?that?android:theme?takes?effect.
????????????view?=?createViewFromTag(context,?name,?attrs);
????????}
然而,這里的補(bǔ)救措施沒有執(zhí)行,那自然有地方有另外的補(bǔ)救措施,回到之前的LayoutInflater的下面這段代碼:
?if?(mFactory2?!=?null)?{????????????????view?=?mFactory2.onCreateView(parent,?name,?context,?attrs);
????????????}?else?if?(mFactory?!=?null)?{
????????????????view?=?mFactory.onCreateView(name,?context,?attrs);
????????????}?else?{
????????????????view?=?null;
????????????}
這段代碼的下面,如果view是空,補(bǔ)救措施如下:
?if?(view?==?null)?{????????????????final?Object?lastContext?=?mConstructorArgs[0];
????????????????mConstructorArgs[0]?=?context;
????????????????try?{
????????????????????if?(-1?==?name.indexOf('.'))?{//包含.說明這不是權(quán)限定名的類名
????????????????????????view?=?onCreateView(parent,?name,?attrs);
????????????????????}?else?{//權(quán)限定名走這里
????????????????????????view?=?createView(name,?null,?attrs);
????????????????????}
????????????????}?finally?{
????????????????????mConstructorArgs[0]?=?lastContext;
????????????????}
????????????}
這里的兩個(gè)方法onCreateView(parent, name, attrs)和createView(name, null, attrs);都最終索引到:
這么一大段好像有點(diǎn)讓人害怕。其實(shí)真正需要關(guān)注的,就是反射的代碼,最后的newInstance()。
OK,Activity上那些豐富多彩的View的來源,就說到這里。
4、app中資源文件大管家 Resources / AssetManager 是怎么工作的
從我們的終極目的出發(fā):我們要做的是“換膚”,如果我們拿到了要換膚的View,可以對(duì)他們進(jìn)行setXXX屬性來改變UI,那么屬性值從哪里來?
界面元素豐富多彩,但是這些View,都是用資源文件來進(jìn)行 "裝扮"出來的,資源文件大致可以分為:
圖片,文字,顏色,聲音視頻,字體等。如果我們控制了資源文件,那么是不是有能力對(duì)界面元素進(jìn)行set某某屬性來進(jìn)行“再裝扮”呢? 當(dāng)然,這是可行的。因?yàn)?#xff0c;我們平時(shí)拿到一個(gè)TextView,就能對(duì)它進(jìn)行setTextColor,這種操作,在view還存活的時(shí)候,都可以進(jìn)行操作,并且這種操作,并不會(huì)造成Activity的重啟。
這些資源文件,有一個(gè)統(tǒng)一的大管家。可能有人說是R.java文件,它里面統(tǒng)籌了所有的資源文件int值.沒錯(cuò),但是這個(gè)R文件是如何產(chǎn)生作用的呢? 答案:Resources.
一張圖說明一切:
5、 "全app一鍵換膚" Demo源碼詳解(戳這里獲得源碼)
項(xiàng)目工程結(jié)構(gòu):
關(guān)鍵類 SkinFactory
SkinFactory類, 繼承LayoutInflater.Factory2 ,它的實(shí)例,會(huì)負(fù)責(zé)創(chuàng)建View,收集 支持換膚的view
public?class?SkinFactory?implements?LayoutInflater.Factory2?{????private?AppCompatDelegate?mDelegate;//預(yù)定義一個(gè)委托類,它負(fù)責(zé)按照系統(tǒng)的原有邏輯來創(chuàng)建view
????private?List?listCacheSkinView?=?new?ArrayList<>();//我自定義的list,緩存所有可以換膚的View對(duì)象/**
?????*?給外部提供一個(gè)set方法
?????*
?????*?@param?mDelegate
?????*/public?void?setDelegate(AppCompatDelegate?mDelegate)?{this.mDelegate?=?mDelegate;
????}/**
?????*?Factory2?是繼承Factory的,所以,我們這次是主要重寫Factory的onCreateView邏輯,就不必理會(huì)Factory的重寫方法了
?????*
?????*?@param?name
?????*?@param?context
?????*?@param?attrs
?????*?@return
?????*/@Overridepublic?View?onCreateView(String?name,?Context?context,?AttributeSet?attrs)?{return?null;
????}/**
?????*?@param?parent
?????*?@param?name
?????*?@param?context
?????*?@param?attrs
?????*?@return
?????*/@Overridepublic?View?onCreateView(View?parent,?String?name,?Context?context,?AttributeSet?attrs)?{//?TODO:?關(guān)鍵點(diǎn)1:執(zhí)行系統(tǒng)代碼里的創(chuàng)建View的過程,我們只是想加入自己的思想,并不是要全盤接管
????????View?view?=?mDelegate.createView(parent,?name,?context,?attrs);//系統(tǒng)創(chuàng)建出來的時(shí)候有可能為空,你問為啥?請(qǐng)全文搜索?“標(biāo)記標(biāo)記,因?yàn)椤?你會(huì)找到你要的答案if?(view?==?null)?{//萬一系統(tǒng)創(chuàng)建出來是空,那么我們來補(bǔ)救try?{if?(-1?==?name.indexOf('.'))?{//不包含.?說明不帶包名,那么我們幫他加上包名
????????????????????view?=?createViewByPrefix(context,?name,?prefixs,?attrs);
????????????????}?else?{//包含.?說明?是權(quán)限定名的view?name,
????????????????????view?=?createViewByPrefix(context,?name,?null,?attrs);
????????????????}
????????????}?catch?(Exception?e)?{
????????????????e.printStackTrace();
????????????}
????????}//TODO:?關(guān)鍵點(diǎn)2?收集需要換膚的View
????????collectSkinView(context,?attrs,?view);return?view;
????}/**
?????*?TODO:?收集需要換膚的控件
?????*?收集的方式是:通過自定義屬性isSupport,從創(chuàng)建出來的很多View中,找到支持換膚的那些,保存到map中
?????*/private?void?collectSkinView(Context?context,?AttributeSet?attrs,?View?view)?{//?獲取我們自己定義的屬性
????????TypedArray?a?=?context.obtainStyledAttributes(attrs,?R.styleable.Skinable);boolean?isSupport?=?a.getBoolean(R.styleable.Skinable_isSupport,?false);if?(isSupport)?{//找到支持換膚的viewfinal?int?Len?=?attrs.getAttributeCount();
????????????HashMap?attrMap?=?new?HashMap<>();for?(int?i?=?0;?i?//遍歷所有屬性
????????????????String?attrName?=?attrs.getAttributeName(i);
????????????????String?attrValue?=?attrs.getAttributeValue(i);
????????????????attrMap.put(attrName,?attrValue);//全部存起來
????????????}
????????????SkinView?skinView?=?new?SkinView();
????????????skinView.view?=?view;
????????????skinView.attrsMap?=?attrMap;
????????????listCacheSkinView.add(skinView);//將可換膚的view,放到listCacheSkinView中
????????}
????}/**
?????*?公開給外界的換膚入口
?????*/public?void?changeSkin()?{for?(SkinView?skinView?:?listCacheSkinView)?{
????????????skinView.changeSkin();
????????}
????}static?class?SkinView?{
????????View?view;
????????HashMap?attrsMap;/**
?????????*?真正的換膚操作
?????????*/public?void?changeSkin()?{if?(!TextUtils.isEmpty(attrsMap.get("background")))?{//屬性名,例如,這個(gè)background,text,textColor....int?bgId?=?Integer.parseInt(attrsMap.get("background").substring(1));//屬性值,R.id.XXX?,int類型,//?這個(gè)值,在app的一次運(yùn)行中,不會(huì)發(fā)生變化
????????????????String?attrType?=?view.getResources().getResourceTypeName(bgId);?//?屬性類別:比如?drawable?,colorif?(TextUtils.equals(attrType,?"drawable"))?{//區(qū)分drawable和color
????????????????????view.setBackgroundDrawable(SkinEngine.getInstance().getDrawable(bgId));//加載外部資源管理器,拿到外部資源的drawable
????????????????}?else?if?(TextUtils.equals(attrType,?"color"))?{
????????????????????view.setBackgroundColor(SkinEngine.getInstance().getColor(bgId));
????????????????}
????????????}if?(view?instanceof?TextView)?{if?(!TextUtils.isEmpty(attrsMap.get("textColor")))?{int?textColorId?=?Integer.parseInt(attrsMap.get("textColor").substring(1));
????????????????????((TextView)?view).setTextColor(SkinEngine.getInstance().getColor(textColorId));
????????????????}
????????????}//那么如果是自定義組件呢if?(view?instanceof?ZeroView)?{//那么這樣一個(gè)對(duì)象,要換膚,就要寫針對(duì)性的方法了,每一個(gè)控件需要用什么樣的方式去換,尤其是那種,自定義的屬性,怎么去set,//?這就對(duì)開發(fā)人員要求比較高了,而且這個(gè)換膚接口還要暴露給?自定義View的開發(fā)人員,他們?nèi)ザx//?....
????????????}
????????}
????}/**
?????*?所謂hook,要懂源碼,懂了之后再劫持系統(tǒng)邏輯,加入自己的邏輯。
?????*?那么,既然懂了,系統(tǒng)的有些代碼,直接拿過來用,也無可厚非。
?????*///*******************************下面一大片,都是從源碼里面抄過來的,并不是我自主設(shè)計(jì)******************************//?你問我抄的哪里的?到?AppCompatViewInflater類源碼里面去搜索:view?=?createViewFromTag(context,?name,?attrs);static?final?Class>[]?mConstructorSignature?=?new?Class[]{Context.class,?AttributeSet.class};//final?Object[]?mConstructorArgs?=?new?Object[2];//View的構(gòu)造函數(shù)的2個(gè)"實(shí)"參對(duì)象private?static?final?HashMap>?sConstructorMap?=?new?HashMap>();//用映射,將View的反射構(gòu)造函數(shù)都存起來static?final?String[]?prefixs?=?new?String[]{//安卓里面控件的包名,就這么3種,這個(gè)變量是為了下面代碼里,反射創(chuàng)建類的class而預(yù)備的"android.widget.","android.view.","android.webkit."
????};/**
?????*?反射創(chuàng)建View
?????*
?????*?@param?context
?????*?@param?name
?????*?@param?prefixs
?????*?@param?attrs
?????*?@return
?????*/private?final?View?createViewByPrefix(Context?context,?String?name,?String[]?prefixs,?AttributeSet?attrs)?{
????????Constructor?extends?View>?constructor?=?sConstructorMap.get(name);
????????Class?extends?View>?clazz?=?null;if?(constructor?==?null)?{try?{if?(prefixs?!=?null?&&?prefixs.length?>?0)?{for?(String?prefix?:?prefixs)?{
????????????????????????clazz?=?context.getClassLoader().loadClass(
????????????????????????????????prefix?!=?null???(prefix?+?name)?:?name).asSubclass(View.class);//控件if?(clazz?!=?null)?break;
????????????????????}
????????????????}?else?{if?(clazz?==?null)?{
????????????????????????clazz?=?context.getClassLoader().loadClass(name).asSubclass(View.class);
????????????????????}
????????????????}if?(clazz?==?null)?{return?null;
????????????????}
????????????????constructor?=?clazz.getConstructor(mConstructorSignature);//拿到?構(gòu)造方法,
????????????}?catch?(Exception?e)?{
????????????????e.printStackTrace();return?null;
????????????}
????????????constructor.setAccessible(true);//
????????????sConstructorMap.put(name,?constructor);//然后緩存起來,下次再用,就直接從內(nèi)存中去取
????????}
????????Object[]?args?=?mConstructorArgs;
????????args[1]?=?attrs;try?{//通過反射創(chuàng)建View對(duì)象final?View?view?=?constructor.newInstance(args);//執(zhí)行構(gòu)造函數(shù),拿到View對(duì)象return?view;
????????}?catch?(Exception?e)?{
????????????e.printStackTrace();
????????}return?null;
????}//**********************************************************************************************
}
關(guān)鍵類 SkinEngine
public?class?SkinEngine?{????//單例
????private?final?static?SkinEngine?instance?=?new?SkinEngine();
????public?static?SkinEngine?getInstance()?{
????????return?instance;
????}
????private?SkinEngine()?{
????}
????public?void?init(Context?context)?{
????????mContext?=?context.getApplicationContext();
????????//使用application的目的是,如果萬一傳進(jìn)來的是Activity對(duì)象
????????//那么它被靜態(tài)對(duì)象instance所持有,這個(gè)Activity就無法釋放了
????}
????private?Resources?mOutResource;//?TODO:?資源管理器
????private?Context?mContext;//上下文
????private?String?mOutPkgName;//?TODO:?外部資源包的packageName
????/**
?????*?TODO:?加載外部資源包
?????*/
????public?void?load(final?String?path)?{//path?是外部傳入的apk文件名
????????File?file?=?new?File(path);
????????if?(!file.exists())?{
????????????return;
????????}
????????//取得PackageManager引用
????????PackageManager?mPm?=?mContext.getPackageManager();
????????//“檢索在包歸檔文件中定義的應(yīng)用程序包的總體信息”,說人話,外界傳入了一個(gè)apk的文件路徑,這個(gè)方法,拿到這個(gè)apk的包信息,這個(gè)包信息包含什么?
????????PackageInfo?mInfo?=?mPm.getPackageArchiveInfo(path,?PackageManager.GET_ACTIVITIES);
????????mOutPkgName?=?mInfo.packageName;//先把包名存起來
????????AssetManager?assetManager;//資源管理器
????????try?{
????????????//TODO:?關(guān)鍵技術(shù)點(diǎn)3?通過反射獲取AssetManager?用來加載外面的資源包
????????????assetManager?=?AssetManager.class.newInstance();//反射創(chuàng)建AssetManager對(duì)象,為何要反射?使用反射,是因?yàn)樗@個(gè)類內(nèi)部的addAssetPath方法是hide狀態(tài)
????????????//addAssetPath方法可以加載外部的資源包
????????????Method?addAssetPath?=?assetManager.getClass().getMethod("addAssetPath",?String.class);//為什么要反射執(zhí)行這個(gè)方法?因?yàn)樗莌ide的,不直接對(duì)外開放,只能反射調(diào)用
????????????addAssetPath.invoke(assetManager,?path);//反射執(zhí)行方法
????????????mOutResource?=?new?Resources(assetManager,//參數(shù)1,資源管理器
????????????????????mContext.getResources().getDisplayMetrics(),//這個(gè)好像是屏幕參數(shù)
????????????????????mContext.getResources().getConfiguration());//資源配置
????????????//最終創(chuàng)建出一個(gè)?"外部資源包"mOutResource?,它的存在,就是要讓我們的app有能力加載外部的資源文件
????????}?catch?(Exception?e)?{
????????????e.printStackTrace();
????????}
????}
????/**
?????*?提供外部資源包里面的顏色
?????*?@param?resId
?????*?@return
?????*/
????public?int?getColor(int?resId)?{
????????if?(mOutResource?==?null)?{
????????????return?resId;
????????}
????????String?resName?=?mOutResource.getResourceEntryName(resId);
????????int?outResId?=?mOutResource.getIdentifier(resName,?"color",?mOutPkgName);
????????if?(outResId?==?0)?{
????????????return?resId;
????????}
????????return?mOutResource.getColor(outResId);
????}
????/**
?????*?提供外部資源包里的圖片資源
?????*?@param?resId
?????*?@return
?????*/
????public?Drawable?getDrawable(int?resId)?{//獲取圖片
????????if?(mOutResource?==?null)?{
????????????return?ContextCompat.getDrawable(mContext,?resId);
????????}
????????String?resName?=?mOutResource.getResourceEntryName(resId);
????????int?outResId?=?mOutResource.getIdentifier(resName,?"drawable",?mOutPkgName);
????????if?(outResId?==?0)?{
????????????return?ContextCompat.getDrawable(mContext,?resId);
????????}
????????return?mOutResource.getDrawable(outResId);
????}
????//.....?這里還可以提供外部資源包里的String,font等等等,只不過要手動(dòng)寫代碼來實(shí)現(xiàn)getXX方法
}
關(guān)鍵類的調(diào)用方式:
1、 初始化"換膚引擎"
public?class?MyApp?extends?Application?{????@Override
????public?void?onCreate()?{
????????super.onCreate();
????????//初始化換膚引擎
????????SkinEngine.getInstance().init(this);
????}
}
2、劫持 系統(tǒng)創(chuàng)建view的過程
public?class?BaseActivity?extends?AppCompatActivity?{????...
????@Override
????protected?void?onCreate(Bundle?savedInstanceState)?{
????????//?TODO:?關(guān)鍵點(diǎn)1:hook(劫持)系統(tǒng)創(chuàng)建view的過程
????????if?(ifAllowChangeSkin)?{
????????????mSkinFactory?=?new?SkinFactory();
????????????mSkinFactory.setDelegate(getDelegate());
????????????LayoutInflater?layoutInflater?=?LayoutInflater.from(this);
????????????layoutInflater.setFactory2(mSkinFactory);//劫持系統(tǒng)源碼邏輯
????????}
????????super.onCreate(savedInstanceState);
????}
3、 執(zhí)行換膚操作
protected?void?changeSkin(String?path)?{????????if?(ifAllowChangeSkin)?{
????????????File?skinFile?=?new?File(Environment.getExternalStorageDirectory(),?path);
????????????SkinEngine.getInstance().load(skinFile.getAbsolutePath());//加載外部資源包
????????????mSkinFactory.changeSkin();//執(zhí)行換膚操作
????????????mCurrentSkin?=?path;
????????}
????}
效果展示:
注意事項(xiàng):
1、 皮膚包skin_plugin module,里面,只提供需要換膚的資源即可,不需要換膚的資源,還有src目錄下的源碼
(只是刪掉java源碼文件,不要?jiǎng)h目錄結(jié)構(gòu)啊….(●′?`●)),不要放在這里,無端增大皮膚包的體積.
2、 皮膚包 skin_plugin module的gradle sdk版本最好和app module的保持完全一致,否則無法保證不會(huì)出現(xiàn)奇葩問題.
3、 用皮膚包skin_plugin module打包生成的apk文件,常規(guī)來說,是放在手機(jī)內(nèi)存里面,然后由app module內(nèi)的代碼去加載。至于是手機(jī)內(nèi)存里面的哪個(gè)位置,那就見仁見智了. 我是使用的mumu模擬器,我放在了最外層的根目錄下面,然后讀取這個(gè)位置的代碼是:File skinFile = new File(Environment.getExternalStorageDirectory(), "skin.apk");
4、上圖中,打了兩個(gè)皮膚包,要注意:打兩個(gè)皮膚包運(yùn)行demo,打之前,一定要記得替換drawable圖片資源為同名文件,以及
不然切換沒有效果。
結(jié)語
hook技術(shù)是安卓高級(jí)層次的技能,學(xué)起來并不簡(jiǎn)單,demo里面的注釋我自認(rèn)為寫的很清楚了,如果還有不懂的,歡迎留言評(píng)論。讀源碼也并不是這么輕松的事,可是還是那句話,太簡(jiǎn)單的東西,不值錢,有高難度才有高回報(bào)。為了百萬年薪,fighting!
作者:波瀾步驚
鏈接:https://www.jianshu.com/p/4c8d46f58c4f
本文經(jīng)作者授權(quán)推送。
---完---
閱讀推薦:
反對(duì)996的人,就是對(duì)于社會(huì)價(jià)值創(chuàng)造理解不夠徹底?
Android百度地圖軌跡回放
Android開發(fā)一年,你是不是還做著拖拽改樣的活?
?2019 隨手點(diǎn)好看 年薪上百萬!總結(jié)
以上是生活随笔為你收集整理的opengl源码 实现无缝切换图片过场_手把手讲解 Android hook技术实现一键换肤的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python怎么添加列_如何将列添加到D
- 下一篇: python idle撤回上一条命令_找