日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 运维知识 > Android >内容正文

Android

opengl源码 实现无缝切换图片过场_手把手讲解 Android hook技术实现一键换肤

發(fā)布時(shí)間:2025/3/21 Android 19 豆豆
生活随笔 收集整理的這篇文章主要介紹了 opengl源码 实现无缝切换图片过场_手把手讲解 Android hook技术实现一键换肤 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

前言

產(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?(view?==?null?&&?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)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。