聊聊Android中的ContextImpl

stormWen發表於2019-03-03

說起這個ContextImpl.可能有些同學不太熟悉,但說起Context,我想都認識它吧,上下文,也可以說是代表一種所在的場景,由於Context只是一個抽象類,而抽象類必定是有一個具體的實現類的,另外還有ContextThemeWrapper和ContextWrapper,不過這些都是Context的子類而已,他們是以裝飾模式而存在的一種關係,簡單說下裝飾模式,裝飾模式是常用的設計模式之一,一般情況下如果需要動態地給一個物件新增一些額外的職責但又不想增加子類,那麼就可以用到裝飾模式了,如果單純增加功能來說,Decorator模式相比生成子類更為靈活,該模式以對客 戶端透明的方式擴充套件物件的功能,下面舉一個簡單的例子說明一下,首先一個人,有吃飯的功能介面

程式碼如下:

Component

 public interface Person {
 
    void eat();
}
複製程式碼

ConcreteComponent

public class Man implements Person {

    public void eat() {
        System.out.println("男人在吃飯");
    }
}
複製程式碼

Decorator(這個類是關鍵,實現了介面並且有真實物件的引用)

public abstract class Decorator implements Person {

    protected Person person;
    
    public Decorator(Person person){
        this.person=person;
    }
    
    public void eat() {
        person.eat();
    }
}
複製程式碼

下面的就是具體的新增額外功能的了具體子類,比如有些人吃飯前先洗手或者吃完飯後洗碗之類,當然具體什麼事情根據業務決定

public class ManDecoratorA extends Decorator {

    public ManDecoratorA(Person person) {
        super(person);
    }
    public void eat() {
        wash();
        super.eat();
    }

    private void wash() {
        System.out.println("飯前先洗洗手");
    }
}
public class ManDecoratorB extends Decorator {
    
    public ManDecoratorB(Person person) {
        super(person);
    }
    public void eat() {
        super.eat();
        washDishes();
    }
    
    private void washDishes(){
          System.out.println("吃完飯後洗碗");
    }
}
複製程式碼

測試的結果為:

 public static void main(String[] args) {
        Person person=new Man();
        ManDecoratorA md1=new ManDecoratorA(person);
        ManDecoratorB md2=new ManDecoratorB(person);
        md1.eat();
        System.out.println("===============");
        md2.eat();
    }
複製程式碼

執行結果為:

聊聊Android中的ContextImpl

可以看到在吃飯前和吃飯後做了一些事情,這就達到了不增加子類而又可以新增一些額外功能的作用

回到ContextImpl和Context,其實也是一樣的,這個ContextImpl相當於程式碼中的Man,而Context相當於Person,只是一個介面,一個抽象類,但本質都是一樣,而Decorator相當於ContextWrapper,那麼具體的抽象實現類為什麼呢,在Android中Activity和Service就是類似程式碼中的ManDecoratorA和ManDecoratorB了,只不過Activity有介面,自然就有Theme主題,因此對應的是ContextThemeWrapper,現在已經對裝飾模式理解的更深了吧。

現在回到ContextImpl本身來,ContextImpl作為Context的抽象類,實現了所有的方法,我們常見的getResources(),getAssets(),getApplication()等等的具體實現都是在ContextImpl的,下面是具體的一些程式碼

 @Override
    public ContentResolver getContentResolver() {
        return mContentResolver;
    }

    @Override
    public Looper getMainLooper() {
        return mMainThread.getLooper();
    }

    @Override
    public Context getApplicationContext() {
        return (mPackageInfo != null) ?
                mPackageInfo.getApplication() : mMainThread.getApplication();
    }
複製程式碼

可以看到我們平常開發所呼叫的方法的實現都是在這裡完成,作為一名android開發者,我們應該多去研究一些原始碼之類的,這樣在出問題的時候可以根據原始碼找出問題的具體所在,ContextImpl在主執行緒ActivityThread通過傳入主執行緒物件建立了一個系統的ContextImpl,下面是程式碼:

 public ContextImpl getSystemContext() {
        synchronized (this) {
            if (mSystemContext == null) {
                mSystemContext = ContextImpl.createSystemContext(this);
            }
            return mSystemContext;
        }
    }
複製程式碼

這個是在ActivityThread裡面完成的,ActivityThread是Android應用程式的主執行緒環境,關於對ActivityThread的分析,可以看我的另一篇文章
關於Android主執行緒(ActivityThread)原始碼分析以及一些特殊問題的非常規方法
大家有空可以去看看,實際上Activity和Service中的Context也是通過ContextImpl來的,大家有時間可以去看看主執行緒的原始碼,好了,我們知道了ContextImpl裡面的方法了,下面說一個具體的不太常見的需求.曾經有個需求,要求使用者在解除安裝之後再重新安裝進入的時候能夠讀取一些配置資訊,當初服務端要求這個客戶端自己去實現,有同學說了這個本身不難,很簡單啊,用SharePreference就可以搞定,恩,是很簡單,可是需求說了再解除安裝之後重新進入的時候需要讀取出來原來的配置,解除安裝之後整個應用程式的目錄都不見了,所有資料都消失了,配置檔案哪裡來呢?,我們知道,SharePreference是儲存在一個叫做shared_prefs目錄下面的,這個目錄隨著程式解除安裝也會被刪掉,也就是說解除安裝之後,儲存在原來預設的儲存就會全部消失,那應該怎麼辦呢,其實只需要修改原來的路徑改為自定義的路徑就好,比如放在外部SD卡或者其他地方,這樣程式解除安裝的時候就不會刪除這些自定義的目錄了,從而可以在安裝再次進入的時候讀取出來,看起來這個方法可以的

ContextImpl裡面有一個欄位mPreferencesDir,這個檔案目錄就是儲存了SharePreference路徑的,我們只需要修改這個為我們自定義的路徑就好了,由於ContextImpl是一個隱藏類,我們需要使用反射去實現,隨我走一波吧,下面是具體的程式碼:

try {
            Class<?> clazz=Class.forName("android.app.ContextImpl");
            Method method=clazz.getDeclaredMethod("getImpl", Context.class);
            method.setAccessible(true);
            Object mContextImpl=method.invoke(null,this);
            //獲取ContextImpl的例項
            Log.d("[app]","mContextImpl="+mContextImpl);
            Field mPreferencesDir=clazz.getDeclaredField("mPreferencesDir");
            mPreferencesDir.setAccessible(true);
            //我們自定義的目錄假設在SD卡, 其他目錄也是一樣的
            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
                File file=new File(Environment.getExternalStorageDirectory(),"new_shared_pres");
                if (!file.exists()){
                    file.mkdirs();
                }
                mPreferencesDir.set(mContextImpl,file);
                Log.d("[app]","修改sp路徑成功");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        
    下面是具體的執行結果:
    12-08 17:28:42.811 12404-12404/com.example.hotfixdemo D/[app]: mContextImpl=android.app.ContextImpl@db6fc37
12-08 17:28:42.818 12404-12404/com.example.hotfixdemo D/[app]: 修改sp路徑成功
複製程式碼

OK,已經成功修改為自定義的路徑了,這樣就達到目的了。

實際上,除了SP的路徑,ContextImpl裡面還有很多類似這樣的路徑,一樣是可以通過類似的手段修改的,達到一些特定的目的,比如360的外掛框架也是Hook了ContextImpl類的資料庫路徑,達到載入的目的,大家有空可以去看看,Java層的Hook基本以反射和動態代理為主,這兩方面的內容,有時間再寫,今天就寫到這裡,感謝大家閱讀。

相關文章