征服Android面試官路漫漫(三):從原始碼深扒一下四大元件和 Context

南方吳彥祖_藍斯發表於2020-11-03

目錄

  • 什麼是 Context?
  • 四大元件和 Context
  • Application 和 Context
  • 為什麼 Application 的 Context 不可以建立 Dialog ?
  • 未完待續...

文章開頭,先來看一段程式碼:

public class ContextActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_context);
        Log.e("context", "getApplication in Activity: " + getApplication().getClass().getName());
        Log.e("context", "getApplicationContext in Activity: " + getApplicationContext().getClass().getName());
        Log.e("context", "getBaseContext in Activity: " + getBaseContext().getClass().getName());
        startService(new Intent(this,ContextService.class));
    }}

你能準確的說出這三行列印語句的執行結果嗎?如果不能,你需要認真閱讀這篇文章。

什麼是 Context ?

Context 是一個抽象類。既然是抽象類,那麼它就代表了一類具體物件的通用特徵。先來看一下 Context 的類圖:

征服Android面試官路漫漫(三):從原始碼深扒一下四大元件和 Context

其中看到了我們很熟悉的  ActivityServiceApplication,這些都是  Context 的具體實現類,也就是說 Context 抽象了這些類的通用特徵和功能:

  • 獲取系統資源, getResources()getAssets() 等
  • 啟動各種系統元件
  • 獲取系統服務
  • ......

這些與系統環境息息相關的功能都是由 Context 提供的,所以一般將其稱為 上下文,它其實就是對當前執行環境的具體描述,為系統元件的正常執行提供必要的環境和資源。

在上面的類圖中,可能有兩個讀者比較陌生的類, ContextWraaper 和 ContextImpl

ContextImpl 很好理解,它就是 Context 的具體實現類。Context 類中的所有抽象方法都是在 ContextImpl 中實現的。

class ContextImpl extends Context {
    ......
   @Override
    public AssetManager getAssets() {
        return getResources().getAssets();
    }
    @Override
    public Resources getResources() {
        return mResources;
    }
    @Override
    public PackageManager getPackageManager() {
        if (mPackageManager != null) {
            return mPackageManager;
        }
        IPackageManager pm = ActivityThread.getPackageManager();
        if (pm != null) {
            // Doesn't matter if we make more than one instance.
            return (mPackageManager = new ApplicationPackageManager(this, pm));
        }
        return null;
    }
    @Override
    public ContentResolver getContentResolver() {
        return mContentResolver;
    }
    @Override
    public Looper getMainLooper() {
        return mMainThread.getLooper();
    }
    ......}

ContextWraaper 其實也很簡單,直接看它的實現程式碼:

public class ContextWrapper extends Context {
    @UnsupportedAppUsage
    Context mBase;
    public ContextWrapper(Context base) {
        mBase = base;
    }
    
    /**
     * 在這個方法中給 mBase 賦值
     */
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }
    public Context getBaseContext() {
        return mBase;
    }
    @Override
    public AssetManager getAssets() {
        return mBase.getAssets();
    }
    @Override
    public Resources getResources() {
        return mBase.getResources();
    }
    ......}

這是一個典型的  裝飾者模式,也叫做  修飾模式,以下來自維基百科:

修飾模式,是物件導向程式設計領域中,一種動態地往一個類中新增新的行為的設計模式。就功能而言,修飾模式相比生成子類更為靈活,這樣可以給某個物件而不是整個類新增一些功能。

透過使用修飾模式,可以在執行時擴充一個類的功能。原理是:增加一個修飾類包裹原來的類,包裹的方式一般是透過在將原來的物件作為修飾類的建構函式的引數。裝飾類實現新的功能,但是,在不需要用到新功能的地方,它可以直接呼叫原來的類中的方法。修飾類必須和原來的類有相同的介面。

修飾模式是類繼承的另外一種選擇。類繼承在編譯時候增加行為,而裝飾模式是在執行時增加行為。

當有幾個相互獨立的功能需要擴充時,這個區別就變得很重要。在有些物件導向的程式語言中,類不能在執行時被建立,通常在設計的時候也不能預測到有哪幾種功能組合。這就意味著要為每一種組合建立一個新類。相反,修飾模式是面向執行時候的物件例項的,這樣就可以在執行時根據需要進行組合。一個修飾模式的示例是JAVA裡的Java I/O Streams的實現。

Context 是基本的抽象類,無論是實現類,還是裝飾類,都直接或間接的實現它。ContextImpl 是 Context 的直接實現類,但各個元件並不是直接繼承 ContextImpl,而是透過裝飾類 ContextWrapper 來持有 ContextImpl。這是為什麼呢?對於 Activity 和 Service 來說,它們都需要系統上下文執行環境,但它們又是不同的。Activity 需要顯示到前臺,它有頁面,它需要主題,於是有了繼承自 ContextWrapper 的  ContextThemeWrapper,擴充套件了功能,給 Activity 提供了主題。同時,Activity、Service、Application 這些具體元件本身又擴充套件出了不同的生命週期功能。

所以,裝飾器模式透過組合和擴充套件裝飾類,來給不同的具體物件提供了不同的功能擴充套件。

ActivityServiceApplication 最終都是繼承自裝飾類  ContextWrapper , ContextWrapper 透過  attachBaseContext() 方法來獲取實際做事的  ContextImpl 物件。所以這些元件的建立過程中,一定會在某一時機呼叫  attachBaseContext() 方法對  mBase 物件進行賦值,讓我們從原始碼裡面找找答案。

四大元件和 Context

Activity 和 Context

先說  Activity,Activity 的啟動過程極其複雜,我們就直接從  ActivityThread 的  performLaunchActivity() 方法看起。

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ActivityInfo aInfo = r.activityInfo;
        if (r.packageInfo == null) {
            // 1. 獲取 LoadedApk 物件
            r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                    Context.CONTEXT_INCLUDE_CODE);
        }
        ......
        // 2. 建立 ContextImpl 物件
        ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            // 3. 反射建立 Activity 物件
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            ......
        } catch (Exception e) {
            ......
        }
        try {
            // 4. 建立 Application 物件
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);
       
            if (activity != null) {
                ......
                appContext.setOuterContext(activity);
                // 5. 繫結 activity
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);
                ......
                int theme = r.activityInfo.getThemeResource();
                if (theme != 0) {
                    // 設定主題
                    activity.setTheme(theme);
                }
                // 6. 回撥 onCreate()
                if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }
                ......
                r.activity = activity;
            }
            r.setState(ON_CREATE);
            mActivities.put(r.token, r);
        } catch (SuperNotCalledException e) {
            throw e;
        } catch (Exception e) {
           ......
        }
        return activity;
    }

整理一下大致的執行流程:

  1. 獲取 LoadedApk 物件,表示載入過的 Apk ,通常一個 App 對應著一個 LoadedApk
  2. 透過  createBaseContextForActivity() 方法建立 ContextImpl 物件
  3. 反射建立 Activity 物件
  4. 建立 Application 物件,這裡也是用的反射。如果開發者沒有宣告自己的 Application 的話,就是預設的  androoid.app.Application
  5. 呼叫  activity.attach() ,這個方法很重要,後面詳細說
  6. 回撥  onCreate()

接著就是 Activity 正常的生命週期流程了。

重點看一下  createBaseContextForActivity() 方法和  attach() 方法。

    private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
    
        ContextImpl appContext = ContextImpl.createActivityContext(
                this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);
        ......
        return appContext;
    }

呼叫了  ContextImpl.createActivityContext() 方法。

   static ContextImpl createActivityContext(ActivityThread mainThread,
            LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
            Configuration overrideConfiguration) {
        ......
        // 建立 ContextImpl 物件
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,
                activityToken, null, 0, classLoader);
        ......
        final ResourcesManager resourcesManager = ResourcesManager.getInstance();
        context.setResources(resourcesManager.createBaseActivityResources(activityToken,
                packageInfo.getResDir(),
                splitDirs,
                packageInfo.getOverlayDirs(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                classLoader));
        context.mDisplay = resourcesManager.getAdjustedDisplay(displayId,
                context.getResources());
        return context;
    }

裝飾類 ContextWrapper 真正需要的 ContextImpl 物件現在已經建立出來了,但是還沒有繫結到 Activity 。繼續看  Activity.attach() 方法,注意 attach() 方法的第一個引數就是剛剛建立出來的 ContextImpl 物件。

    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        // 回撥 attachBaseContext()
        attachBaseContext(context);
        ......
        // 建立 PhoneWindow
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ......    
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        ......
    }

你對  attachBaseContext() 方法應該還有印象。ContextWrapper 正是透過這個方法給  mBase 物件賦值,拿到真正的 ContextImpl 物件。到這裡,整個邏輯就通順了。

注意  attach() 方法中的  setWindowManager() 方法中的  mToken 引數,這決定了 Application Context 無法建立和顯示 Dialog 。後續會進行詳細分析。

再回頭看看文章開頭的問題。

Log.e("context", "getApplication in Activity: " + getApplication().getClass().getName());Log.e("context", "getApplicationContext in Activity: " + getApplicationContext().getClass().getName());Log.e("context", "getBaseContext in Activity: " + getBaseContext().getClass().getName());

第一個  getApplication() ,看下原始碼就知道了:

public final Application getApplication() {
    return mApplication;}

getApplication() 返回的是當前的 Application 物件。開發者沒有宣告自己實現的 Application 的話,就是系統預設的  android.app.Application

第二個  getApplicationContext(),它並不是 Activity 中的方法,而是 ContextWrapper 的。直接看原始碼:

@Overridepublic Context getApplicationContext() {
    return mBase.getApplicationContext();}

呼叫的是  ContextImpl.getApplicationContext() 。

@Overridepublic Context getApplicationContext() {
    return (mPackageInfo != null) ?
        mPackageInfo.getApplication() : mMainThread.getApplication();}

所以返回的同樣是 Application 物件。

第三個, getBaseContext() ,同樣是 ContextWrapper 中的方法:

public Context getBaseContext() {
    return mBase;}

所以這裡返回的是 ContextImpl 物件。

最後的列印語句是:

E/context: getApplication in Activity: luyao.android.App
E/context: getApplicationContext in Activity: luyao.android.App
E/context: getBaseContext in Activity: android.app.ContextImpl

關於 Activity 就說這麼多了。下面來看看 Service 。

Service 和 Context

Service 其實和 Activity 的整體流程基本一致,建立服務的主要邏輯在  ActivityThread.handleCreateService() 方法中。這裡我就不貼原始碼了,簡單敘述一下:

  1. 建立 LoadedApk 物件
  2. 反射建立 Service 物件
  3. 呼叫 ContextImpl.createAppCntext() 建立 ContextImpl 物件
  4. 建立 Application 物件
  5. 呼叫 service.attach() 進行繫結
  6. 回撥 service 的 onCreate() 方法

直接看一下  Service.attach() 方法:

    public final void attach(
            Context context,
            ActivityThread thread, String className, IBinder token,
            Application application, Object activityManager) {
        attachBaseContext(context);
        ......
    }

又看到了熟悉的  attachBaseContext() 方法。

Activity 和  Service 都是繼承自  ContextWrapper 的,最後都是透過  attachBaseContext() 對 ContextImpl 型別的  mBase 賦值。而  ContentProvider 和  BroadcastReceiver 都沒有繼承 Context,所以它們獲取 Context 的方式會有一點不一樣。

ContentProvider 和 Context

先來看  ContentProvider,建立 Provider 的邏輯在  Activity.installProvider() 方法中:

    private ContentProviderHolder installProvider(Context context,
            ContentProviderHolder holder, ProviderInfo info,
            boolean noisy, boolean noReleaseNeeded, boolean stable) {
        ContentProvider localProvider = null;
        IContentProvider provider;
     
        // 建立 LoadedApk 和 ContextImpl
        c = context.createPackageContext(ai.packageName,Context.CONTEXT_INCLUDE_CODE);
               
        try {
            ......
            // 建立 ContentProvider
            localProvider = packageInfo.getAppFactory()
                    .instantiateProvider(cl, info.name);
            provider = localProvider.getIContentProvider();
            ......
            // 繫結 Context
            localProvider.attachInfo(c, info);
        } catch (java.lang.Exception e) {
            ......
        }
      
        ......
        return retHolder;}

最後在  ContentProvider.attachInfo() 方法中進行了 ContextImpl 的賦值操作。

    private void attachInfo(Context context, ProviderInfo info, boolean testing) {
        if (mContext == null) {
            // 給 mContext 賦值
            mContext = context;
            ......
            // 回撥 onCreate()
            ContentProvider.this.onCreate();
        }
    }

這樣 ContentProvider 也能拿到 Context 物件了。

BroadcastReceiver 和 Context

最後就是 BroadcastReceiver 了,對應  ActivityThread.handleReceiver()方法:

    private void handleReceiver(ReceiverData data) {
        ......
        // 建立 LoadedApk 物件
        LoadedApk packageInfo = getPackageInfoNoCheck(
                data.info.applicationInfo, data.compatInfo);
        Application app;
        BroadcastReceiver receiver;
        ContextImpl context;
        try {
            // 建立 Application 物件
            app = packageInfo.makeApplication(false, mInstrumentation);
            // 建立 ContextImpl 物件
            context = (ContextImpl) app.getBaseContext();
           
            ......
            // 建立 BroadcastReceiver 物件
            receiver = packageInfo.getAppFactory()
                    .instantiateReceiver(cl, data.info.name, data.intent);
        } catch (Exception e) {
            ......
        }
        try {
            ......
            // 回撥 onReceive()
            receiver.onReceive(context.getReceiverRestrictedContext(),
                    data.intent);
        } catch (Exception e) {
          ......
        } finally {
            sCurrentBroadcastIntent.set(null);
        }
        ......
    }

大多數步驟和 Activity 還是類似的,只是到最後回撥  onReceive() 方法的時候,才會把 ContextImpl 物件傳過去。注意,這裡並不是直接返回原生的 ContextImpl 物件,而是呼叫  context.getReceiverRestrictedContext()返回一個  受限制 的  ReceiverRestrictedContext,你無法使用這個 Context 物件啟動 Service 。

這不正是  裝飾者模式 的體現?想給廣播的 Context 物件加點限制,那就再來一個裝飾類  ReceiverRestrictedContext ,它繼承了  ContextWrapper , 重寫部分方法以限制應用場景。 透過增加和組合裝飾類,而不是增加子類,來實現功能擴充套件。

Application 和 Context

四大元件說完了,別忘了  Application 也是  Context 的間接子類。

Application 的建立時機得從應用程式的建立開始說起。Zygote 程式在接收到客戶端請求建立應用程式的 socket 請求之後,會 fork 出子程式,並反射呼叫 ActivityThread 的靜態 main() 方法。接著是 AMS 和客戶端的一系列 Binder 呼叫以及 Handler 通訊,最終主執行緒在收到  BIND_APPLICATION 訊息之後回撥  handleBindApplication() 方法,到這裡就是我們需要的邏輯了:

private void handleBindApplication(AppBindData data){
    ......
    // 獲取 ContextImpl
    final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
    ......
    // 建立 Application 物件
    app = data.info.makeApplication(data.restrictedBackupMode, null);
    ......
    // 呼叫 Application 的 onCreate() 方法
    mInstrumentation.callApplicationOnCreate(app);}

你可能會疑惑怎麼沒有回撥  attBaseContext() 方法,別急,看看  LoadedApk.makeApplication() 方法是如何建立 Application 的。

public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {
    ......
    // 建立 ContextImpl
    ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
    // 反射建立 Application
    app = mActivityThread.mInstrumentation.newApplication(
            cl, appClass, appContext);
    appContext.setOuterContext(app);}

透過  Instrumentation.newApplication() 方法建立 Application 。

    public Application newApplication(ClassLoader cl, String className, Context context)
            throws InstantiationException, IllegalAccessException, 
            ClassNotFoundException {
        // 反射建立
        Application app = getFactory(context.getPackageName())
                .instantiateApplication(cl, className);
        // 重點
        app.attach(context);
        return app;
    }

重點就在  Application.attach() 方法。

final void attach(Context context) {
    attachBaseContext(context);
    mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;}

在這裡呼叫了  attachBaseContext() 方法進行賦值,也驗證了  attachBaseContext() 的確比  onCreate() 先呼叫。

為什麼 Application 的 Context 不可以建立 Dialog ?

使用 Application 的 Context 建立 Dialog 並顯示,會報如下錯誤:

Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
    at android.view.ViewRootImpl.setView(ViewRootImpl.java:951)
    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:387)
    at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:96)
    at android.app.Dialog.show(Dialog.java:344)

注意錯誤資訊  token null is not valid ,還記得文章前面說到 Activity 和 Context 的時候,有這麼一段程式碼:

mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);

注意其中的  mToken 引數不為 null ,是不是就說明了 Application 的 token 引數為空呢?

本來準備接著說說這個問題,但可能造成文章篇幅過長,所以 之後會單獨來嘮嘮這個問題。

如何進階Android?

有些東西你不僅要懂,而且要能夠很好地表達出來,能夠讓面試官認可你的理解,例如Handler機制,這個是面試必問之題。有些晦澀的點,或許它只活在面試當中,實際工作當中你壓根不會用到它,但是你要知道它是什麼東西。

對於程式設計師來說,要學習的知識內容、技術有太多太多,要想不被環境淘汰就只有不斷提升自己, 從來都是我們去適應環境,而不是環境來適應我們!

最後我在這裡分享一下這段時間從朋友,大佬那裡收集到的一些 2019-2020BAT 面試真題解析,裡面內容很多也很系統,包含了很多內容: Android 基礎、Java 基礎、Android 原始碼相關分析、常見的一些原理性問題等等,可以很好地幫助我們深刻理解Android相關知識點的原理以及面試相關知識。

1、確定好方向,梳理成長路線圖

不用多說,相信大家都有一個共識:無論什麼行業,最牛逼的人肯定是站在金字塔端的人。所以,想做一個牛逼的程式設計師,那麼就要讓自己站的更高,成為技術大牛並不是一朝一夕的事情,需要時間的沉澱和技術的積累。

關於這一點,在我當時確立好Android方向時,就已經開始梳理自己的成長路線了,包括技術要怎麼系統地去學習,都列得非常詳細。

征服Android面試官路漫漫(三):從原始碼深扒一下四大元件和 Context

知識梳理完之後,就需要進行查漏補缺,所以針對這些知識點,我手頭上也準備了不少的電子書和筆記,這些筆記將各個知識點進行了完美的總結。

2、透過原始碼來系統性地學習

只要是程式設計師,不管是Java還是Android,如果不去閱讀原始碼,只看API文件,那就只是停留於皮毛,這對我們知識體系的建立和完備以及實戰技術的提升都是不利的。

真正最能鍛鍊能力的便是直接去閱讀原始碼,不僅限於閱讀各大系統原始碼,還包括各種優秀的開源庫。

征服Android面試官路漫漫(三):從原始碼深扒一下四大元件和 Context
《486頁超全面Android開發相關原始碼精編解析》

3、閱讀前輩的一些技術筆記

征服Android面試官路漫漫(三):從原始碼深扒一下四大元件和 Context
《960全網最全Android開發筆記》

4、刷題備戰,直通大廠

歷時半年,我們整理了這份市面上最全面的安卓面試題解析大全
包含了騰訊、百度、小米、阿里、樂視、美團、58、360、新浪、搜狐等一線網際網路公司面試被問到的題目。熟悉本文中列出的知識點會大大增加透過前兩輪技術面試的機率。

如何使用它?

1.可以透過目錄索引直接翻看需要的知識點,查漏補缺。
2.五角星數表示面試問到的頻率,代表重要推薦指數

征服Android面試官路漫漫(三):從原始碼深扒一下四大元件和 Context
《379頁Android開發面試寶典》

以上內容均放在了開源專案: github  中已收錄,大家可以自行獲取。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2731944/,如需轉載,請註明出處,否則將追究法律責任。

相關文章