Context Application 使用總結 MD


Markdown版本筆記 我的GitHub首頁 我的博客 我的微信 我的郵箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

Context Application 使用總結 MD


目錄

Application 使用總結

Application 簡介

官方文檔 中的描述:

Application 是用來維護應用程序全局狀態[maintain global application state]的基礎類。你可以通過在 AndroidManifest.xml 文件的<application>標簽中指出他的android:name屬性的方式提供自己的實現。在創建應用程序/包的進程時,Application類或Application類的子類在任何其他類之前實例化。

注意:通常不需要子類化Application。在大多數情況下,靜態單例[static singletons]可以以更模塊化的方式[more modular way]提供相同的功能。如果您的單例需要全局上下文(例如注冊廣播接收器),則在調用單例的getInstance()方法時將Context.getApplicationContext()作為Context參數。

Android系統會為每個程序運行時創建一個Application類的對象且僅創建一個,所以Application可以說是單例模式的一個類。且 Application 對象的生命周期是整個程序中最長的,它的生命周期就等於這個程序的生命周期。因為它是全局唯一的,所以在不同的Activity、Service中獲得的對象都是同一個對象。所以通過 Application 來進行一些數據傳遞、數據共享、數據緩存等操作。

Application和Activity,Service一樣是Android框架的一個系統組件,當android程序啟動時系統會創建一個 Application 對象,用來存儲系統的一些信息。通常我們是不需要指定一個Application的,這時系統會自動幫我們創建,如果需要創建自己的Application,也很簡單,創建一個類繼承 Application並在manifest的application標簽中進行注冊,只需要給Application標簽增加個name屬性把自己的 Application 的名字定入即可。

通過反射方式獲取當前應用的 Application

沒有自定義Application時,當前應用的Application為android.app.Application
自定義Application時,當前應用的Application為你所設置的 Application

public class App {
    private static Application application;

    private App() {
    }

    //通過反射方式獲取當前應用的Application
    @SuppressLint("PrivateApi")
    public static Application get() {
        if (application == null) {
            try {
                Class<?> clazz = Class.forName("android.app.ActivityThread");
                Field field = clazz.getDeclaredField("sCurrentActivityThread");
                field.setAccessible(true);
                Object object = field.get(null);//得到ActivityThread的對象,雖然是隱藏的,但已經指向了內存的堆地址
                Method method = clazz.getDeclaredMethod("getApplication");
                method.setAccessible(true);
                application = (Application) method.invoke(object);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
        //沒有自定義時為【android.app.Application】,自定義時為你設置的Application
        Log.i("bqt", "當前應用的Application為:" + application.getClass().getName());
        return application;
    }
}

Application 的生命周期方法

  • onCreate:在應用程序創建的時候被調用,可以實現這個這個方法來創建和實例化任何應用程序狀態變量或共享資源。還可以在這個方法里面得到 Application 的單例。
  • onTerminate:當終止應用程序對象時調用,不保證一定被調用,當程序是被內核終止以便為其他應用程序釋放資源,那么將不會提醒,並且不調用應用程序的對象的onTerminate方法而直接終止進程。
  • onLowMemory:當系統資源匱乏的時候,我們可以在這里可以釋放額外的內存。這個方法一般只會在后台進程已經結束,但前台應用程序還是缺少內存時調用。可以重寫這個方法來清空緩存或者釋放不必要的資源。
  • onConfigurationChanged:重寫此方法可以監聽APP一些配置信息的改變事件(如屏幕旋轉等),當配置信息改變的時候會調用這個方法。與 Activity 不同,配置改變時,應用程序對象不會被終止和重啟。如果應用程序使用的值依賴於特定的配置,則重寫這個方法來加載這些值,或者在應用程序級處理配置值的改變。
  • onTrimMemory(int level):系統用這個方法提醒APP釋放一些緩存了,如圖片緩存,數據緩存之類的。里有傳入一個int類型的參數level,它告訴APP們內存不足的嚴重性(越高越嚴重)

關於onLowMemory
按我的理解就是,當APP處於前台時,但是所有后台程序都被kill光了,但是還是內存不足時,系統就會調用這個方法告訴APP:兄弟輪到你了。我們可以在這個方法里面釋放一些不重要的資源,來保證到時候內存足夠而讓APP進程不被系統殺掉,或者提醒用戶清一下垃圾,讓內存清一點空位出來,我的手機老是這樣提示我,不知道是不是這個方法惹的禍。

注冊全局的 ActivityLifecycleCallbacks

registerActivityLifecycleCallbacksunregisterActivityLifecycleCallbacks 這兩個方法用於注冊或者注銷對APP內所有Activity的生命周期監聽,當APP內Activity的生命周期發生變化的時候就會調用ActivityLifecycleCallbacks里面的方法:

registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
   @Override
   public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}

   @Override
   public void onActivityDestroyed(Activity activity) {}

   @Override
   public void onActivityStarted(Activity activity) {}

   @Override
   public void onActivityResumed(Activity activity) {
       Log.i("bqt", "當前Activity" + "(" + activity.getClass().getSimpleName() + ".java" + ":" + 30 + ")");
   }

   @Override
   public void onActivityPaused(Activity activity) {}

   @Override
   public void onActivityStopped(Activity activity) {}

   @Override
   public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
});

Application 中的其他注冊方法

registerComponentCallbacks 和 unregisterComponentCallbacks 方法

用於注冊和注銷 ComponentCallbacks2 回調接口,里面的回調方法前面已經介紹過,看名字就知道。
Context 類也有這兩個方法,但是 Context 類的方法只可以使用 ComponentCallbacks,比 ComponentCallbacks2 少了一個 onTrimMemory() 回調。

registerComponentCallbacks(new ComponentCallbacks2() {
   @Override
   public void onTrimMemory(int level) { //ComponentCallbacks2 比 ComponentCallbacks 多了這個回調方法
      Log.i("bqt", "【onTrimMemory】" + level);
   }

   @Override
   public void onConfigurationChanged(Configuration newConfig) {
      Log.i("bqt", "【onConfigurationChanged】");
   }

   @Override
   public void onLowMemory() {
      Log.i("bqt", "【onLowMemory】");
   }
});

registerOnProvideAssistDataListener 和 unregisterOnProvideAssistDataListener 方法

API18 以上的方法,網上關於這兩個方法的介紹很少,幾乎沒有,在官網上的介紹是這樣的:

This is called when the user is requesting an assist, to build a full ACTION_ASSIST Intent with all of the context of the current application.

好像是當用戶請求幫助的時候會調用這個方法,然后會啟動一個 ACTION_ASSIST 的 Intent。什么時候才是用戶請求幫助呢?

StackOverflow 里有的人說是長按 Home 鍵,外國的機子會跳出 Google Now 這個助手,至於國內的機子...。然后嘗試了一下用下面的代碼來發送一個ACTION_ASSIST來看看有什么效果:

context.startActivity(new Intent(ACTION_ASSIST));

結果打開了我手機上UC瀏覽器的語音搜索功能。最后還是搞不懂這個方法什么時候會回調,如果有知道的請告知,謝謝!

使用 Application 實現數據全局共享

目前基本上每一個應用程序都會寫一個自己的Application,然后在自己的Application類中去封裝一些通用的操作。其實這並不是Google所推薦的一種做法,因為這樣我們只是把Application當成了一個通用工具類來使用的,而實際上使用一個簡單的單例類也可以實現同樣的功能。當然這種做法也並沒有什么副作用,只是說明還是有不少人對於Application理解的還有些欠缺。

可以在以下場景中使用 Application 實現數據全局共享:

  • 可以設置一些全局的共享常量,如一些TAG,枚舉值等。
  • 可以設置一些全局使用的共享變量數據,如一個全局的Handler等等。但是要注意,這里緩存的變量數據的作用周期只在APP的生命周期,如果APP因為內存不足而結束的話,再開啟這些數據就會消失,所以這里只能存儲一些不重要的數據來使數據全APP共享,想要儲存重要數據的話需要SharePreference、數據庫或者文件存儲等這些持久化本地存儲方式。
  • 可以設置一些靜態方法來讓其他類調用,來使用Application里面的全局變量,如實現APP一鍵退出功能時候會用到。

Application 方法執行順序

加入在自定義 Application 的構造方法中添加如下代碼:

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

應用程序一啟動就立刻崩潰了,報的是一個空指針異常。如果你嘗試把代碼改成下面的寫法,就會發現一切正常了:

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

在構造方法中調用Context的方法就會崩潰,在onCreate()方法中調用Context的方法就一切正常,那么這兩個方法之間到底發生了什么事情呢?

我們查看ContextWrapper類的源碼可知道,ContextWrapper中有一個attachBaseContext()方法,這個方法會將傳入的一個Context參數賦值給mBase對象,之后mBase對象就有值了。

而我們又知道,所有Context的方法都是調用這個mBase對象的同名方法,那么也就是說如果在mBase對象還沒賦值的情況下就去調用Context中的任何一個方法時,就會出現空指針異常,上面的代碼就是這種情況。

Application中方法的執行順序為:

構造方法 --> attachBaseContext --> onCreate

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

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

Context 使用總結

Context 簡介

Context字面意思上下文,或者叫做場景,也就是用戶與操作系統操作的一個過程,比如你打電話,場景包括電話程序對應的界面,以及隱藏在背后的數據

源碼中是這么來解釋Context的:

  • 提供關於應用環境全局信息的接口。
  • 它是一個抽象類,它的實現由Android系統所提供。
  • 它允許訪問特定於應用程序的資源和類,以及對應用程序級操作的up-calls,例如啟動Activity,廣播和接收意圖等。

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.

Context 的繼承結構

Context類本身是一個abstract類,Context的直系子類有兩個,一個是ContextWrapper,一個是ContextImpl。從名字上就可以看出,ContextWrapper是上下文功能的封裝類,而ContextImpl則是上下文功能的實現類

ContextWrapper構造函數中必須包含一個真正的Context引用,同時ContextWrapper中提供了attachBaseContext用於給ContextWrapper對象中指定真正的Context對象,調用ContextWrapper的方法都會被轉向其所包含的真正的Context對象。

ContextWrapper有三個直接的子類,ContextThemeWrapper、Service和Application。其中,ContextThemeWrapper是一個帶主題Theme(即在清單文件中指定的android:theme)的封裝類,而它有一個直接子類就是Activity

ContextImpl類真正實現了Context中的所以函數,應用程序中所調用的各種Context類的方法,其實現均來自於該類。ContextImpl在IDE中時找不到的,這是屬於保護文件在frameworks\base\core\java\android\app目錄中。

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

ContextWrapper 類的結構

ContextWrapper只是對Context類的一種封裝,它的構造函數包含了一個真正的Context引用,即ContextImpl對象,ContextImpl正是上下文功能的實現類

也就是說像Application、Activity這樣的類其實並不會去具體實現Context的功能,而僅僅是做了一層接口封裝而已,Context的具體功能都是由ContextImpl類去完成的。

部分源碼:

public class ContextWrapper extends Context { 
    Context mBase; //其實就是系統創建的 ContextImpl,Context的具體功能都是由ContextImpl類去完成的
    protected void attachBaseContext(Context base) { 
        if (mBase != null)  throw new IllegalStateException("Base context already set"); 
        mBase = base; 
    }

    public Context getBaseContext() { 
        return mBase; //得到的是一個ContextImpl對象
    }

    //*************************************** 常用的功能都是調用的 mBase 的同名方法  ***************************************
    @Override 
    public AssetManager getAssets() { 
        return mBase.getAssets(); 
    }
    @Override
    public Resources getResources() { 
        return mBase.getResources(); 
    } 
} 

其實所有ContextWrapper中方法的實現都非常統一,就是調用了mBase對象中對應當前方法名的方法。

那么這個mBase對象又是什么呢?

我們來看attachBaseContext()方法,這個方法中傳入了一個base參數,並把這個參數賦值給了mBase對象。

attachBaseContext()方法其實是由系統來調用的,它會把ContextImpl對象作為參數傳遞到attachBaseContext()方法當中,從而賦值給mBase對象,之后ContextWrapper中的所有方法其實都是通過這種委托的機制交由ContextImpl去具體實現的,所以說ContextImpl是上下文功能的實現類是非常准確的。

那么另外再看一下getBaseContext()方法,這個方法就是返回了mBase對象而已,而mBase對象其實就是ContextImpl對象。

Context 的應用場景

由於Context的具體實例是由ContextImpl類去實現的,因此在絕大多數場景下,在絕大多數場景下,Activity、Service和Application這三種類型的Context都是可以通用的。不過有幾種場景比較特殊,比如啟動Activity,還有彈出Dialog。

出於安全原因的考慮,Android是不允許Activity或Dialog憑空出現的,一個Activity的啟動必須要建立在另一個Activity的基礎之上,也就是以此形成的返回棧

而Dialog則必須在一個Activity上面彈出,除非是System Alert類型的Dialog,因此在這種場景下,我們只能使用Activity類型的Context,否則將會出錯。

大家注意看到有一些NO上添加了一些數字,其實這些從能力上來說是YES,但是不建議使用:

  • 數字1:啟動Activity在這些類中是可以的,但是需要創建一個新的task
  • 數字2:在這些類中去layout inflate是合法的,但是會使用系統默認的主題樣式,如果你自定義了某些樣式可能不會被使用
  • 數字3:在receiver為null時允許,在4.2或以上的版本中,用於獲取黏性廣播的當前值(可以無視)

注:ContentProvider、BroadcastReceiver之所以在上述表格中,是因為在其內部方法中都有一個context用於使用。

這里重點看下Activity和Application,可以看到,和UI相關的方法基本都不建議或者不可使用Application,並且,前三個操作基本不可能在Application中出現。

如何獲取 Context

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

  • View.getContext,返回當前View對象的Context對象,通常是當前正在展示的Activity對象。
  • Activity.getApplicationContext,獲取當前Activity所在的(應用)進程的Context對象,通常我們使用Context對象時,要優先考慮這個全局的進程Context。
  • Activity.this,返回當前的Activity實例,如果是UI控件需要使用Activity作為Context對象,但是默認的Toast實際上使用ApplicationContext也可以。
  • ContextWrapper.getBaseContext(),用來獲取一個ContextWrapper進行裝飾之前的Context,可以使用這個方法,這個方法在實際開發中使用並不多,也不建議使用。

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); 
    }
} 

結果如下圖所示:

看來它們是同一個對象。其實這個結果也很好理解,因為Application本身就是一個Context,所以這里獲取getApplicationContext()得到的結果就是MyApplication本身的實例。

那么,為什么要提供兩個功能重復的方法呢?
其實僅僅是因為Activity和Service提供了這個方法:

//Activity和Service的源碼
private Application mApplication; 

/** Return the application that owns this activity. */
public final Application getApplication() {
    return mApplication;
}

getApplicationContext()ContextWrapper中提供的方法,任何一個Context的實例,比如BroadcastReceiver,只要調用getApplicationContext()方法都可以拿到我們的Application對象:

Context mBase;

@Override
public Context getApplicationContext() {
    return mBase.getApplicationContext();
}

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 A 被銷毀掉,但因為它的引用還存在於一個 Singleton 中,就不可能被 GC 掉,這樣就導致了內存泄漏。

靜態對象持有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會將自身設置到Drawable的callback上去,因為View是實現了Drawable.Callback接口,這樣當Drawable需要刷新的時候,可以調用這個Callback,然后通知View重新繪制該Drawable。所以引用的正確順序應該Drawable->View->Context,因為被static修飾的mDrawable是常駐內存的,MainActivity是它的間接引用,MainActivity被銷毀時,也不能被GC掉,所以造成內存泄漏。

正確使用Context
一般Context造成的內存泄漏,幾乎都是當Context銷毀的時候,卻因為被引用導致銷毀失敗,而Application的Context對象可以理解為隨着進程存在的,所以我們總結出使用Context的正確姿勢:

  • 當Application的Context能搞定的情況下,並且生命周期長的對象,優先使用Application的Context。
  • 不要讓生命周期長於Activity的對象持有到Activity的引用。
  • 盡量不要在Activity中使用非靜態內部類,因為非靜態內部類會隱式持有外部類實例的引用;如果使用靜態內部類,將外部實例引用作為弱引用持有。

2018-9-8


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM