Android Context完全解析,你所不知道的Context的各種細節

来源:https://www.cnblogs.com/linhaostudy/archive/2020/02/16/12318475.html
-Advertisement-
Play Games

前幾篇文章,我也是費勁心思寫了一個ListView系列的三部曲,雖然在內容上可以說是絕對的精華,但是很多朋友都表示看不懂。好吧,這個系列不僅是把大家給難倒了,也確實是把我給難倒了,之前為了寫瀑布流ListView的Demo就寫了大半個月的時間。那麼本篇文章我們就講點輕鬆的東西,不去分析那麼複雜的源碼 ...


前幾篇文章,我也是費勁心思寫了一個ListView系列的三部曲,雖然在內容上可以說是絕對的精華,但是很多朋友都表示看不懂。好吧,這個系列不僅是把大家給難倒了,也確實是把我給難倒了,之前為了寫瀑布流ListView的Demo就寫了大半個月的時間。那麼本篇文章我們就講點輕鬆的東西,不去分析那麼複雜的源碼了,而是來談一談大家都熟知的Context。

Context相信所有的Android開發人員基本上每天都在接觸,因為它太常見了。但是這並不代表Context沒有什麼東西好講的,實際上Context有太多小的細節並不被大家所關註,那麼今天我們就來學習一下那些你所不知道的細節。

Context類型

我們知道,Android應用都是使用Java語言來編寫的,那麼大家可以思考一下,一個Android程式和一個Java程式,他們最大的區別在哪裡?劃分界限又是什麼呢?其實簡單點分析,Android程式不像Java程式一樣,隨便創建一個類,寫個main()方法就能跑了,而是要有一個完整的Android工程環境,在這個環境下,我們有像Activity、Service、BroadcastReceiver等系統組件,而這些組件並不是像一個普通的Java對象new一下就能創建實例的了,而是要有它們各自的上下文環境,也就是我們這裡討論的Context。可以這樣講,Context是維持Android程式中各組件能夠正常工作的一個核心功能類。

下麵我們來看一下Context的繼承結構:

image

Context的繼承結構還是稍微有點複雜的,可以看到,直系子類有兩個,一個是ContextWrapper,一個是ContextImpl。那麼從名字上就可以看出,ContextWrapper是上下文功能的封裝類,而ContextImpl則是上下文功能的實現類。而ContextWrapper又有三個直接的子類,ContextThemeWrapper、Service和Application。其中,ContextThemeWrapper是一個帶主題的封裝類,而它有一個直接子類就是Activity。

那麼在這裡我們至少看到了幾個所比較熟悉的面孔,Activity、Service、還有Application。由此,其實我們就已經可以得出結論了,Context一共有三種類型,分別是Application、Activity和Service。這三個類雖然分別各種承擔著不同的作用,但它們都屬於Context的一種,而它們具體Context的功能則是由ContextImpl類去實現的。

那麼Context到底可以實現哪些功能呢?這個就實在是太多了,彈出Toast、啟動Activity、啟動Service、發送廣播、操作資料庫等等等等都需要用到Context。由於Context的具體能力是由ContextImpl類去實現的,因此在絕大多數場景下,Activity、Service和Application這三種類型的Context都是可以通用的。不過有幾種場景比較特殊,比如啟動Activity,還有彈出Dialog。。出於安全原因的考慮,Android是不允許Activity或Dialog憑空出現的,一個Activity的啟動必須要建立在另一個Activity的基礎之上,也就是以此形成的返回棧。而Dialog則必須在一個Activity上面彈出(除非是System Alert類型的Dialog),因此在這種場景下,我們只能使用Activity類型的Context,否則將會出錯。

Context數量

那麼一個應用程式中到底有多少個Context呢?其實根據上面的Context類型我們就已經可以得出答案了。Context一共有Application、Activity和Service三種類型,因此一個應用程式中Context數量的計算公式就可以這樣寫:

Context數量 = Activity數量 + Service數量 + 1

上面的1代表著Application的數量,因為一個應用程式中可以有多個Activity和多個Service,但是只能有一個Application。

Application Context的設計

基本上每一個應用程式都會有一個自己的Application,並讓它繼承自系統的Application類,然後在自己的Application類中去封裝一些通用的操作。其實這並不是Google所推薦的一種做法,因為這樣我們只是把Application當成了一個通用工具類來使用的,而實際上使用一個簡單的單例類也可以實現同樣的功能。但是根據我的觀察,有太多的項目都是這樣使用Application的。當然這種做法也並沒有什麼副作用,只是說明還是有不少人對於Application理解的還有些欠缺。那麼這裡我們先來對Application的設計進行分析,講一些大家所不知道的細節,然後再看一下平時使用Application的問題。

首先新建一個MyApplication並讓它繼承自Application,然後在AndroidManifest.xml文件中對MyApplication進行指定,如下所示:

<application
    android:name=".MyApplication"
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
    ......
</application>

指定完成後,當我們的程式啟動時Android系統就會創建一個MyApplication的實例,如果這裡不指定的話就會預設創建一個Application的實例。

public class MainActivity extends Activity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyApplication myApp = (MyApplication) getApplication();
        Log.d("TAG", "getApplication is " + myApp);
    }
    
}

可以看到,代碼很簡單,只需要調用getApplication()方法就能拿到我們自定義的Application的實例了,列印結果如下所示:

image

那麼除了getApplication()方法,其實還有一個getApplicationContext()方法,這兩個方法看上去好像有點關聯,那麼它們的區別是什麼呢?我們將代碼修改一下:

public class MainActivity extends Activity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyApplication myApp = (MyApplication) getApplication();
        Log.d("TAG", "getApplication is " + myApp);
        Context appContext = getApplicationContext();
        Log.d("TAG", "getApplicationContext is " + appContext);
    }
    
}

同樣,我們把getApplicationContext()的結果列印了出來,現在重新運行代碼,結果如下圖所示:

image

咦?好像列印出的結果是一樣的呀,連後面的記憶體地址都是相同的,看來它們是同一個對象。其實這個結果也很好理解,因為前面已經說過了,Application本身就是一個Context,所以這裡獲取getApplicationContext()得到的結果就是MyApplication本身的實例。

那麼有的朋友可能就會問了,既然這兩個方法得到的結果都是相同的,那麼Android為什麼要提供兩個功能重覆的方法呢?實際上這兩個方法在作用域上有比較大的區別。getApplication()方法的語義性非常強,一看就知道是用來獲取Application實例的,但是這個方法只有在Activity和Service中才能調用的到。那麼也許在絕大多數情況下我們都是在Activity或者Service中使用Application的,但是如果在一些其它的場景,比如BroadcastReceiver中也想獲得Application的實例,這時就可以藉助getApplicationContext()方法了,如下所示:

public class MyReceiver extends BroadcastReceiver {
 
    @Override
    public void onReceive(Context context, Intent intent) {
        MyApplication myApp = (MyApplication) context.getApplicationContext();
        Log.d("TAG", "myApp is " + myApp);
    }
 
}

也就是說,getApplicationContext()方法的作用域會更廣一些,任何一個Context的實例,只要調用getApplicationContext()方法都可以拿到我們的Application對象。

那麼更加細心的朋友會發現,除了這兩個方法之外,其實還有一個getBaseContext()方法,這個baseContext又是什麼東西呢?我們還是通過列印的方式來驗證一下:

image

哦?這次得到的是不同的對象了,getBaseContext()方法得到的是一個ContextImpl對象。這個ContextImpl是不是感覺有點似曾相識?回去看一下Context的繼承結構圖吧,ContextImpl正是上下文功能的實現類。也就是說像Application、Activity這樣的類其實並不會去具體實現Context的功能,而僅僅是做了一層介面封裝而已,Context的具體功能都是由ContextImpl類去完成的。那麼這樣的設計到底是怎麼實現的呢?我們還是來看一下源碼吧。因為Application、Activity、Service都是直接或間接繼承自ContextWrapper的,我們就直接看ContextWrapper的源碼,如下所示:

/**
 * Proxying implementation of Context that simply delegates all of its calls to
 * another Context.  Can be subclassed to modify behavior without changing
 * the original Context.
 */
public class ContextWrapper extends Context {
    Context mBase;
    
    /**
     * Set the base context for this ContextWrapper.  All calls will then be
     * delegated to the base context.  Throws
     * IllegalStateException if a base context has already been set.
     * 
     * @param base The new base context for this wrapper.
     */
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }
 
    /**
     * @return the base context as set by the constructor or setBaseContext
     */
    public Context getBaseContext() {
        return mBase;
    }
 
    @Override
    public AssetManager getAssets() {
        return mBase.getAssets();
    }
 
    @Override
    public Resources getResources() {
        return mBase.getResources();
    }
 
    @Override
    public ContentResolver getContentResolver() {
        return mBase.getContentResolver();
    }
 
    @Override
    public Looper getMainLooper() {
        return mBase.getMainLooper();
    }
    
    @Override
    public Context getApplicationContext() {
        return mBase.getApplicationContext();
    }
 
    @Override
    public String getPackageName() {
        return mBase.getPackageName();
    }
 
    @Override
    public void startActivity(Intent intent) {
        mBase.startActivity(intent);
    }
    
    @Override
    public void sendBroadcast(Intent intent) {
        mBase.sendBroadcast(intent);
    }
 
    @Override
    public Intent registerReceiver(
        BroadcastReceiver receiver, IntentFilter filter) {
        return mBase.registerReceiver(receiver, filter);
    }
 
    @Override
    public void unregisterReceiver(BroadcastReceiver receiver) {
        mBase.unregisterReceiver(receiver);
    }
 
    @Override
    public ComponentName startService(Intent service) {
        return mBase.startService(service);
    }
 
    @Override
    public boolean stopService(Intent name) {
        return mBase.stopService(name);
    }
 
    @Override
    public boolean bindService(Intent service, ServiceConnection conn,
            int flags) {
        return mBase.bindService(service, conn, flags);
    }
 
    @Override
    public void unbindService(ServiceConnection conn) {
        mBase.unbindService(conn);
    }
 
    @Override
    public Object getSystemService(String name) {
        return mBase.getSystemService(name);
    }
 
    ......
}

由於ContextWrapper中的方法還是非常多的,我就進行了一些篩選,只貼出來了部分方法。那麼上面的這些方法相信大家都是非常熟悉的,getResources()、getPackageName()、getSystemService()等等都是我們經常要用到的方法。那麼所有這些方法的實現又是什麼樣的呢?其實所有ContextWrapper中方法的實現都非常統一,就是調用了mBase對象中對應當前方法名的方法。

那麼這個mBase對象又是什麼呢?我們來看第16行的attachBaseContext()方法,這個方法中傳入了一個base參數,並把這個參數賦值給了mBase對象。而attachBaseContext()方法其實是由系統來調用的,它會把ContextImpl對象作為參數傳遞到attachBaseContext()方法當中,從而賦值給mBase對象,之後ContextWrapper中的所有方法其實都是通過這種委托的機制交由ContextImpl去具體實現的,所以說ContextImpl是上下文功能的實現類是非常準確的。

那麼另外再看一下我們剛剛列印的getBaseContext()方法,在第26行。這個方法只有一行代碼,就是返回了mBase對象而已,而mBase對象其實就是ContextImpl對象,因此剛纔的列印結果也得到了印證。

使用Application的問題

雖說Application的用法確實非常簡單,但是我們平時的開發工作當中也著實存在著不少Application誤用的場景,那麼今天就來看一看有哪些比較容易犯錯的地方是我們應該註意的。

Application是Context的其中一種類型,那麼是否就意味著,只要是Application的實例,就能隨時使用Context的各種方法呢?我們來做個實驗試一下就知道了:

public class MyApplication extends Application {
    
    public MyApplication() {
        String packageName = getPackageName();
        Log.d("TAG", "package name is " + packageName);
    }
    
}

這是一個非常簡單的自定義Application,我們在MyApplication的構造方法當中獲取了當前應用程式的包名,並列印出來。獲取包名使用了getPackageName()方法,這個方法就是由Context提供的。那麼上面的代碼能正常運行嗎?跑一下就知道了,你將會看到如下所示的結果:

image

應用程式一啟動就立刻崩潰了,報的是一個空指針異常。看起來好像挺簡單的一段代碼,怎麼就會成空指針了呢?但是如果你嘗試把代碼改成下麵的寫法,就會發現一切正常了:

public class MyApplication extends Application {
    
    @Override
    public void onCreate() {
        super.onCreate();
        String packageName = getPackageName();
        Log.d("TAG", "package name is " + packageName);
    }
    
}

運行結果如下所示:

image

在構造方法中調用Context的方法就會崩潰,在onCreate()方法中調用Context的方法就一切正常,那麼這兩個方法之間到底發生了什麼事情呢?我們重新回顧一下ContextWrapper類的源碼,ContextWrapper中有一個attachBaseContext()方法,這個方法會將傳入的一個Context參數賦值給mBase對象,之後mBase對象就有值了。而我們又知道,所有Context的方法都是調用這個mBase對象的同名方法,那麼也就是說如果在mBase對象還沒賦值的情況下就去調用Context中的任何一個方法時,就會出現空指針異常,上面的代碼就是這種情況。Application中方法的執行順序如下圖所示:

image

Application中在onCreate()方法里去初始化各種全局的變數數據是一種比較推薦的做法,但是如果你想把初始化的時間點提前到極致,也可以去重寫attachBaseContext()方法,如下所示:

public class MyApplication extends Application {
    
    @Override
    protected void attachBaseContext(Context base) {
        // 在這裡調用Context的方法會崩潰
        super.attachBaseContext(base);
        // 在這裡可以正常調用Context的方法
    }
    
}

以上是我們平時在使用Application時需要註意的一個點,下麵再來介紹另外一種非常普遍的Application誤用情況。

其實Android官方並不太推薦我們使用自定義的Application,基本上只有需要做一些全局初始化的時候可能才需要用到自定義Application,官方文檔描述如下:

image

但是就我的觀察而言,現在自定義Application的使用情況基本上可以達到100%了,也就是我們平時自己寫測試demo的時候可能不會使用,正式的項目幾乎全部都會使用自定義Application。可是使用歸使用,有不少項目對自定義Application的用法並不到位,正如官方文檔中所表述的一樣,多數項目只是把自定義Application當成了一個通用工具類,而這個功能並不需要藉助Application來實現,使用單例可能是一種更加標準的方式。

不過自定義Application也並沒有什麼副作用,它和單例模式二選一都可以實現同樣的功能,但是我見過有一些項目,會把自定義Application和單例模式混合到一起使用,這就讓人大跌眼鏡了。一個非常典型的例子如下所示:

public class MyApplication extends Application {
    
    private static MyApplication app;
    
    public static MyApplication getInstance() {
        if (app == null) {
            app = new MyApplication();
        }
        return app;
    }
    
}

就像單例模式一樣,這裡提供了一個getInstance()方法,用於獲取MyApplication的實例,有了這個實例之後,就可以調用MyApplication中的各種工具方法了。

但是這種寫法對嗎?這種寫法是大錯特錯!因為我們知道Application是屬於系統組件,系統組件的實例是要由系統來去創建的,如果這裡我們自己去new一個MyApplication的實例,它就只是一個普通的Java對象而已,而不具備任何Context的能力。有很多人向我反饋使用 LitePal 時發生了空指針錯誤其實都是由於這個原因,因為你提供給LitePal的只是一個普通的Java對象,它無法通過這個對象來進行Context操作。

那麼如果真的想要提供一個獲取MyApplication實例的方法,比較標準的寫法又是什麼樣的呢?其實這裡我們只需謹記一點,Application全局只有一個,它本身就已經是單例了,無需再用單例模式去為它做多重實例保護了,代碼如下所示:

public class MyApplication extends Application {
    
    private static MyApplication app;
    
    public static MyApplication getInstance() {
        return app;
    }
    
    @Override
    public void onCreate() {
        super.onCreate();
        app = this;
    }
    
}

getInstance()方法可以照常提供,但是裡面不要做任何邏輯判斷,直接返回app對象就可以了,而app對象又是什麼呢?在onCreate()方法中我們將app對象賦值成this,this就是當前Application的實例,那麼app也就是當前Application的實例了。

好了,關於Context的介紹就到這裡吧,內容還是比較簡單易懂的,希望大家通過這篇文章可以理解Context更多的細節,並且不要去犯使用Context時的一些低級錯誤。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1 DHCP伺服器簡介 DHCP(Dynamic Host Configuration Protocol),動態主機配置協議,DHCP 協議主要是用來自動為區域網中的客戶機分配TCP/IP 信息的網路協議,並完成每台客戶機的TCP/IP 協議配置。當我們將區域網中客戶機IP地址設置為動態獲取方式時, ...
  • 實驗環境:centos7 註:因為本次實驗在同一臺伺服器上,Apache與Nginx同為80埠,所以改Apache埠為60 1 配置Nginx伺服器: 編輯Nginx配置文件,寫入以下內容 location ~ \.php$ { 所有以.php結尾的文件,前面\代表轉義 proxy_pass h ...
  • 很多人都有寫博客的習慣,奈何國內的博客網站正在一家家地關閉與重整,部分博客網站也充斥著太多的廣告,使用體驗非常不好。對於愛寫博客的朋友來說,其實還有一個更好的選擇,那就是自己搭建一個博客。 ...
  • 前言 年過30惶惶不安,又逢疫情,還是不斷學習,強化自己的能力。hadoop的視頻和書籍在15年的時候就看過,但是一直沒動手實踐過,要知道技術不經過實戰,一點提升也沒有。因此下定決心邊學邊做,希望能有所收穫。 軟體版本介紹 virtualbox 6.1 centos7 hadoop 3.2.1 jd ...
  • 1 查詢指定欄位 在 employee 表找出所有員工的姓名、性別和電子郵箱。 SELECT 表示查詢,隨後列出需要返回的欄位,欄位間逗號分隔 FROM 表示要從哪個表中進行查詢 分號為語句結束符 這種查詢表中指定欄位的操作在關係運算中被稱為投影(Projection) 使用 SELECT 子句進行 ...
  • 1、概述 (1)鎖的定義 鎖是電腦協調多個進程或線程併發訪問某一資源的機制。 在資料庫中,除了傳統的計算資源(如CPU、RAM、IO等)的爭用以外,數據也是一種供需要用戶共用的資源。如何保證數據併發訪問的一致性、有效性是所有資料庫必須解決的一個問題,鎖衝突也是影響資料庫併發訪問性能的一個重要因素。 ...
  • 該文為《 MySQL 實戰 45 講》的學習筆記,感謝查看,如有錯誤,歡迎指正 一、查詢和更新上的區別 這兩類索引在查詢能力上是沒差別的,主要考慮的是對更新性能的影響。建議儘量選擇普通索引。 1.1 MySQL 的查詢操作 普通索引 查找到第一個滿足條件的記錄後,繼續向後遍歷,直到第一個不滿足條件的 ...
  • 隱式類型轉換簡介 通常ORACLE資料庫存在顯式類型轉換(Explicit Datatype Conversion)和隱式類型轉換(Implicit Datatype Conversion)兩種類型轉換方式。如果進行比較或運算的兩個值的數據類型不同時(源數據的類型與目標數據的類型),而且此時又沒有轉... ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...