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