首頁 > 易卦

Context都沒弄明白,還怎麼做Android開發?

作者:由 視聽觀察室 發表于 易卦日期:2023-01-30

建立程式組什麼意思

大家好,我是老K,一個在網際網路行業深耕10年+的技術草根,從基礎碼農到管理一路摸爬滾打,極具傳奇色彩的進階歷程,有著極其充實的技術沉澱和豐富的網際網路閱歷,透過自身所見所學分享知識,傳遞最主流前沿的技術,希望對讀者有所幫助,彼此在成長共同進步,堅持每天各類乾貨,希望大家多點贊、關注支援!

Activity mActivity =new Activity()

作為Android開發者,不知道你有沒有思考過這個問題,Activity可以new嗎?Android的應用程式開發採用JAVA語言,Activity本質上也是一個物件,那上面的寫法有什麼問題呢?估計很多人說不清道不明。Android程式不像Java程式一樣,隨便建立一個類,寫個main()方法就能執行,Android應用模型是基於元件的應用設計模式,元件的執行要有一個完整的Android工程環境,在這個環境下,Activity、Service等系統元件才能夠正常工作,而這些元件並不能採用普通的Java物件建立方式,new一下就能建立例項了,而是要有它們各自的上下文環境,也就是我們這裡討論的Context。可以這樣講,Context是維持Android程式中各元件能夠正常工作的一個核心功能類。

** Context到底是什麼**

Context的中文翻譯為:語境; 上下文; 背景; 環境,在開發中我們經常說稱之為“上下文”,那麼這個“上下文”到底是指什麼意思呢?在語文中,我們可以理解為語境,在程式中,我們可以理解為當前物件在程式中所處的一個環境,一個與系統互動的過程。比如微信聊天,此時的“環境”是指聊天的介面以及相關的資料請求與傳輸,Context在載入資源、啟動Activity、獲取系統服務、建立View等操作都要參與。

那Context到底是什麼呢?一個Activity就是一個Context,一個Service也是一個Context。Android程式設計師把“場景”抽象為Context類,他們認為使用者和作業系統的每一次互動都是一個場景,比如打電話、發簡訊,這些都是一個有介面的場景,還有一些沒有介面的場景,比如後臺執行的服務(Service)。一個應用程式可以認為是一個工作環境,使用者在這個環境中會切換到不同的場景,這就像一個前臺秘書,她可能需要接待客人,可能要列印檔案,還可能要接聽客戶電話,而這些就稱之為不同的場景,前臺秘書可以稱之為一個應用程式。

如何生動形象的理解Context

上面的概念中採用了通俗的理解方式,將Context理解為“上下文”或者“場景”,如果你仍然覺得很抽象,不好理解。在這裡我給出一個可能不是很恰當的比喻,希望有助於大家的理解:一個Android應用程式,可以理解為一部電影或者一部電視劇,Activity,Service,Broadcast Receiver,Content Provider這四大元件就好比是這部戲裡的四個主角:胡歌,霍建華,詩詩,Baby。

他們是由劇組(系統)一開始就定好了的,整部戲就是由這四位主演領銜擔綱的,所以這四位主角並不是大街上隨隨便便拉個人(new 一個物件)都能演的。有了演員當然也得有攝像機拍攝啊,他們必須透過鏡頭(Context)才能將戲傳遞給觀眾,這也就正對應說四大元件(四位主角)必須工作在Context環境下(攝像機鏡頭)。那Button,TextView,LinearLayout這些控制元件呢,就好比是這部戲裡的配角或者說群眾演員,他們顯然沒有這麼重用,隨便一個路人甲路人乙都能演(可以new一個物件),但是他們也必須要面對鏡頭(工作在Context環境下),所以

Button mButton=new Button(Context)

是可以的。雖然不很恰當,但還是很容易理解的,希望有幫助。

原始碼中的Context

/**

* Interface to global information about an application environment。 This is

* an abstract class whose implementation is provided by

* the Android system。 It

* allows access to application-specific resources and classes, as well as

* up-calls for application-level operations such as launching activities,

* broadcasting and receiving intents, etc。

*/

public

abstract

class

Context

{

/**

* File creation mode: the default mode, where the created file can only

* be accessed by the calling application (or all applications sharing the

* same user ID)。

* @see #MODE_WORLD_READABLE

* @see #MODE_WORLD_WRITEABLE

*/

public

static

final

int

MODE_PRIVATE

=

0x0000

public

static

final

int

MODE_WORLD_WRITEABLE

=

0x0002

public

static

final

int

MODE_APPEND

=

0x8000

public

static

final

int

MODE_MULTI_PROCESS

=

0x0004

}

原始碼中的註釋是這麼來解釋Context的:Context提供了關於應用環境全域性資訊的介面。它是一個抽象類,它的執行被Android系統所提供。它允許獲取以應用為特徵的資源和型別,是一個統領一些資源(應用程式環境變數等)的上下文。就是說,它描述一個應用程式環境的資訊(即上下文);是一個抽象類,Android提供了該抽象類的具體實現類;透過它我們可以獲取應用程式的資源和類(包括應用級別操作,如啟動Activity,發廣播,接受Intent等)。既然上面Context是一個抽象類,那麼肯定有他的實現類咯,我們在Context的原始碼中透過IDE可以檢視到他的子類最終可以得到如下關係圖:

Context都沒弄明白,還怎麼做Android開發?

Context。png

Context類本身是一個純abstract類,它有兩個具體的實現子類:ContextImpl和ContextWrapper。其中ContextWrapper類,如其名所言,這只是一個包裝而已,ContextWrapper建構函式中必須包含一個真正的Context引用,同時ContextWrapper中提供了attachBaseContext()用於給ContextWrapper物件中指定真正的Context物件,呼叫ContextWrapper的方法都會被轉向其所包含的真正的Context物件。

ContextThemeWrapper類,如其名所言,其內部包含了與主題(Theme)相關的介面,這裡所說的主題就是指在AndroidManifest。xml中透過android:theme為Application元素或者Activity元素指定的主題。當然,只有Activity才需要主題,Service是不需要主題的,因為Service是沒有介面的後臺場景,所以Service直接繼承於ContextWrapper,Application同理。而ContextImpl類則真正實現了Context中的所以函式,應用程式中所呼叫的各種Context類的方法,其實現均來自於該類。

一句話總結:Context的兩個子類分工明確,其中ContextImpl是Context的具體實現類,ContextWrapper是Context的包裝類。Activity,Application,Service雖都繼承自ContextWrapper(Activity繼承自ContextWrapper的子類ContextThemeWrapper),但它們初始化的過程中都會建立ContextImpl物件,由ContextImpl實現Context中的方法。

一個應用程式有幾個Context

其實這個問題本身並沒有什麼意義,關鍵還是在於對Context的理解,從上面的關係圖我們已經可以得出答案了,在應用程式中Context的具體實現子類就是:Activity,Service,Application。那麼

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

。當然如果你足夠細心,可能會有疑問:我們常說四大元件,這裡怎麼只有Activity,Service持有Context,那Broadcast Receiver,Content Provider呢?Broadcast Receiver,Content Provider並不是Context的子類,他們所持有的Context都是其他地方傳過去的,所以並不計入Context總數。上面的關係圖也從另外一個側面告訴我們Context類在整個Android系統中的地位是多麼的崇高,因為很顯然Activity,Service,Application都是其子類,其地位和作用不言而喻。

Context能幹什麼

Context到底可以實現哪些功能呢?這個就實在是太多了,彈出Toast、啟動Activity、啟動Service、傳送廣播、操作資料庫等等都需要用到Context。

TextView

tv

=

new

TextView

getContext

());

ListAdapter

adapter

=

new

SimpleCursorAdapter

getApplicationContext

(), 。。。);

AudioManager

am

=

AudioManager

getContext

()。

getSystemService

Context

AUDIO_SERVICE

);

()。

getSharedPreferences

name

mode

);

()。

getContentResolver

()。

query

uri

, 。。。);

getContext

()。

getResources

()。

getDisplayMetrics

()。

widthPixels

*

5

/

8

getContext

()。

startActivity

intent

);

getContext

()。

startService

intent

);

getContext

()。

sendBroadcast

intent

);

Context作用域

雖然Context神通廣大,但並不是隨便拿到一個Context例項就可以為所欲為,它的使用還是有一些規則限制的。由於Context的具體例項是由ContextImpl類去實現的,因此在絕大多數場景下,Activity、Service和Application這三種類型的Context都是可以通用的。不過有幾種場景比較特殊,比如啟動Activity,還有彈出Dialog。出於安全原因的考慮,Android是不允許Activity或Dialog憑空出現的,一個Activity的啟動必須要建立在另一個Activity的基礎之上,也就是以此形成的返回棧。而Dialog則必須在一個Activity上面彈出(除非是System Alert型別的Dialog),因此在這種場景下,我們只能使用Activity型別的Context,否則將會出錯。

Context都沒弄明白,還怎麼做Android開發?

Context作用域。png

從上圖我們可以發現Activity所持有的Context的作用域最廣,無所不能。因為Activity繼承自ContextThemeWrapper,而Application和Service繼承自ContextWrapper,很顯然ContextThemeWrapper在ContextWrapper的基礎上又做了一些操作使得Activity變得更強大,這裡我就不再貼原始碼給大家分析了,有興趣的童鞋可以自己查查原始碼。上圖中的YES和NO我也不再做過多的解釋了,這裡我說一下上圖中Application和Service所不推薦的兩種使用情況。

1:如果我們用ApplicationContext去啟動一個LaunchMode為standard的Activity的時候會報錯

android。util。AndroidRuntimeException: Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag。 Is this really what you want?

這是因為非Activity型別的Context並沒有所謂的任務棧,所以待啟動的Activity就找不到棧了。解決這個問題的方法就是為待啟動的Activity指定FLAG_ACTIVITY_NEW_TASK標記位,這樣啟動的時候就為它建立一個新的任務棧,而此時Activity是以singleTask模式啟動的。所有這種用Application啟動Activity的方式不推薦使用,Service同Application。

2:在Application和Service中去layout inflate也是合法的,但是會使用系統預設的主題樣式,如果你自定義了某些樣式可能不會被使用。所以這種方式也不推薦使用。

一句話總結:凡是跟UI相關的,都應該使用Activity做為Context來處理;其他的一些操作,Service,Activity,Application等例項都可以,當然了,注意Context引用的持有,防止記憶體洩漏。

如何獲取Context

通常我們想要獲取Context物件,主要有以下四種方法

1:View。getContext,返回當前View物件的Context物件,通常是當前正在展示的Activity物件。

2:Activity。getApplicationContext,獲取當前Activity所在的(應用)程序的Context物件,通常我們使用Context物件時,要優先考慮這個全域性的程序Context。

3:ContextWrapper。getBaseContext():用來獲取一個ContextWrapper進行裝飾之前的Context,可以使用這個方法,這個方法在實際開發中使用並不多,也不建議使用。

4:Activity。this 返回當前的Activity例項,如果是UI控制元件需要使用Activity作為Context物件,但是預設的Toast實際上使用ApplicationContext也可以。

getApplication()和getApplicationContext()

上面說到獲取當前Application物件用getApplicationContext,不知道你有沒有聯想到getApplication(),這兩個方法有什麼區別?相信這個問題會難倒不少開發者。

Context都沒弄明白,還怎麼做Android開發?

getApplication()&getApplicationContext()。png

程式是不會騙人的,我們透過上面的程式碼,列印得出兩者的記憶體地址都是相同的,看來它們是同一個物件。其實這個結果也很好理解,因為前面已經說過了,Application本身就是一個Context,所以這裡獲取getApplicationContext()得到的結果就是Application本身的例項。

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

publicclassMyReceiverextendsBroadcastReceiver

{

@Override

publicvoidonReceive

Contextcontext

Intentintent

){

ApplicationmyApp

=

Application

context

();

}

}

Context引起的記憶體洩露

但Context並不能隨便亂用,用的不好有可能會引起記憶體洩露的問題,下面就示例兩種錯誤的引用方式。

錯誤的單例模式

public

class

Singleton

{

private

static

Singleton

instance

private

Context

mContext

private

Singleton

Context

context

) {

this

mContext

=

context

}

public

static

Singleton

getInstance

Context

context

) {

if

instance

==

null

) {

instance

=

new

Singleton

context

);

}

return

instance

}

}

這是一個非執行緒安全的單例模式,instance作為靜態物件,其生命週期要長於普通的物件,其中也包含Activity,假如Activity A去getInstance獲得instance物件,傳入this,常駐記憶體的Singleton儲存了你傳入的Activity A物件,並一直持有,即使Activity被銷燬掉,但因為它的引用還存在於一個Singleton中,就不可能被GC掉,這樣就導致了記憶體洩漏。

View持有Activity引用

public

class

MainActivity

extends

Activity

{

private

static

Drawable

mDrawable

@Override

protected

void

onCreate

Bundle

saveInstanceState

) {

super

onCreate

saveInstanceState

);

setContentView

R

layout

activity_main

);

ImageView

iv

=

new

ImageView

this

);

mDrawable

=

getResources

()。

getDrawable

R

drawable

ic_launcher

);

iv

setImageDrawable

mDrawable

);

}

}

有一個靜態的Drawable物件當ImageView設定這個Drawable時,ImageView儲存了mDrawable的引用,而ImageView傳入的this是MainActivity的mContext,因為被static修飾的mDrawable是常駐記憶體的,MainActivity是它的間接引用,MainActivity被銷燬時,也不能被GC掉,所以造成記憶體洩漏。

正確使用Context

一般Context造成的記憶體洩漏,幾乎都是當Context銷燬的時候,卻因為被引用導致銷燬失敗,而Application的Context物件可以理解為隨著程序存在的,所以我們總結出使用Context的正確姿勢:

1:當Application的Context能搞定的情況下,並且生命週期長的物件,優先使用Application的Context。

2:不要讓生命週期長於Activity的物件持有到Activity的引用。

3:儘量不要在Activity中使用非靜態內部類,因為非靜態內部類會隱式持有外部類例項的引用,如果使用靜態內部類,將外部例項引用作為弱引用持有。