Android developers are no strangers to Applications. Sometimes to avoid memory leaks, it is common to use the provided getApplicationContext() instead of using the Context directly to ensure that you get an application-level Context. But this time, as usual, the Application is null, what happened?

The rollover

Let’s start by reviewing the code in question.

In order to avoid memory leaks, the following code is used in the Jar package provided externally without thinking:

private DemoManager(Context context){
    mContext = context.getApplicationContext();
    if(DEBUG){ mContext.getPackageName(); . }}Copy the code

A seemingly common way of writing this Jar package crashes when it is applied in a project: McOntext.getpackagename () has a null pointer exception.

When I saw the crash happened here, IT was really a little unexpected, but I didn’t have time to think about it, so I changed the code to this for the time being.

private DemoManager(Context context){
    mContext = context.getApplicationContext();
    if(null == mContext){
        mContext = context;
    }
    if(DEBUG){ mContext.getPackageName(); . }}Copy the code

In hindsight, I had time to figure it out, because it was a little subversive as an Android veteran.

The Application Context should always be created first, so why do you have the Context but not the Application?

What’s going on

I tried to write a minimal Demo to reproduce, but failed. Later, I found that such a problem would not occur in general, because the App running this time is special.

The actual code adds the custom ContentProvider to the TelephonyProvider App and uses the Jar package in query(). The com.Android. phone system process on which TelephonyProvider App depends will start first, and then TelephonyProvider will be loaded into the process. Surprisingly, for TelephonyProvider App, its Application is always null, not its own Application, let alone Phone Application.

Therefore, Demo needs to use the above features to reproduce. For example, make two apps. One is the App for querying ContentProvider: Query App; Another is the App for ContentProvider: Provider App.

  1. Query App and Provider App are in the same process. Run the Android :process=”XXX” command to specify the process
  2. Query App starts first, and calls Provider App to Query (The null ApplicationContext has nothing to do with the Query App calling Query)

I didn’t notice the TelephonyProvider and Phone process feature at first, so the DEMO won’t reproduce anyway.

Let’s take a closer look at FW.

Why can’t Provider apps that share processes get applications?

That’s not how it works

So first of all, where does Context come from in ContentProvider?

// frameworks/base/core/java/android/app/ActivityThread.java
private ContentProviderHolder installProvider(Context context...) {
    ContentProvider localProvider = null;
    IContentProvider provider;
    if (holder == null || holder.provider == null) {
        Context c = null;
        ApplicationInfo ai = info.applicationInfo;
        if (context.getPackageName().equals(ai.packageName)) {
            // If the Provider App is a separate process, the context takes the Application parameters passed in
            c = context;
        } else if(mInitialApplication ! =null &&
                mInitialApplication.getPackageName().equals(ai.packageName)) {
            c = mInitialApplication;
        } else {
            try {
               // Instead call createPackageContext to create a specific Contextc = context.createPackageContext(ai.packageName, Context.CONTEXT_INCLUDE_CODE); }... }...if(info.splitName ! =null) {
                try {
                    c = c.createContextForSplit(info.splitName);
                } catch (NameNotFoundException e) {
                    throw newRuntimeException(e); }}if(info.attributionTags ! =null && info.attributionTags.length > 0) {
                final String attributionTag = info.attributionTags[0];
                c = c.createAttributionContext(attributionTag);
            }

            try {
                // Where c is the actual Context passed to the ContentProviderlocalProvider.attachInfo(c, info); . }}... }Copy the code

The Context passed to the ContentProvider can be created in several ways. If the Query App is not the same as the Provider App’s packageName, the Provider App cannot use the Query App’s Application directly. In createPackageContextAsUser entrance.

@Override
public Context createPackageContextAsUser(String packageName, int flags, UserHandle user)
            throws NameNotFoundException {
    // The LoadedApk constructor is called
    // LoadedApk holds Application instances that are null by defaultLoadedApk pi = mMainThread.getPackageInfo(packageName, mResources.getCompatibilityInfo(), flags | CONTEXT_REGISTER_PACKAGE, user.getIdentifier()); . }Copy the code

Instance will create your own LoadedApk createPackageContextAsUser (), and held LoadedApk instances of Application by default is null. So if there is no opportunity to assign an Application later, the Provider App will always get an empty Application.

Context#getApplicationContext; Context#getApplicationContext;

// frameworks/base/core/java/android/app/ContextImpl.java
public Context getApplicationContext(a) {
    return(mPackageInfo ! =null)? mPackageInfo.getApplication() : mMainThread.getApplication(); }Copy the code

You can see that there are two sources:

  1. MPackageInfo: LoadedApk. Generally, the Application is obtained from this instance
  2. MMainThread: mInitialApplication is initialized when the ActivityThread is attached. It is unlikely to be null.

The problem is that the Application held in LoadedApk is empty.

LoadedApk holds Application instances that are created and assigned in makeApplication(), so we need to further analyze the call source of makeApplication().

A search revealed the following key calls to ActivityThread:

  • HandleBindApplication () : Creates an Application instance when the process starts cold, which is the Query App Application in this case
  • PerformLaunchActivity () : When the Activity is started
  • HandleCreateService () : When a Service is started
  • HandleReceiver () : When a broadcast is received

All four components, except ContentProvider, execute makeApplication().

// frameworks/base/core/java/android/app/LoadedApk.java
public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {.../ / create the Applicationapp = mActivityThread.mInstrumentation.newApplication( cl, appClass, appContext); . mActivityThread.mAllApplications.add(app); mApplication = app;if(instrumentation ! =null) {
        try {
            / / call application # onCreate ()instrumentation.callApplicationOnCreate(app); . } } Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);return app;
}
Copy the code

Give it a try

After the above code analysis, can not help but have the following conjecture:

  1. If getApplicationContext is null, does that mean the Application in the Provider app will not be created? Add the following Log to duplicate it and find that Application#onCreate() is not actually called when the problem occurs.
public class ProviderApplication extends Application {
    @Override
    public void onCreate(a) {
        super.onCreate();
        android.util.Log.e("ProviderApplication"."onCreate"); }}Copy the code
  1. As mentioned above, makeApplication() will be called when the three major components of Service, Activity and Receiver are started. Therefore, if I start a Service in the Provider App, will there be no problem?

    Yes, the following Log shows that both apps share the same process. The Application instance is available only after manually starting the Service.

    The Demo information is added as follows:

    • Query App, package name com.zxg.testcode

    • Provider App, package name: Com. ZXG. Queryproviderdemo, start the Service offers ProviderService, Application for ProviderApplication, ContentProvider to QueryProvider

2022-04-01 15:14:41.126 18687-18687/com.zxg.testcode E/QueryProvider: Query // getContext() is android.app.ContextImpl@869d7cf 2022-04-01 15:14:41.127 18687-18687/com.zxg.testcode E/QueryProvider: GetContext () is android.app.ContextImpl@869d7cf // and getApplicationContext() is null 2022-04-01 15:14:41.127 18687-18687/com.zxg.testcode E/QueryProvider: Context is null // Start a service manually, ProviderApplication created and called onCreate 2022-04-01 15:14:46.378 18687-18687/com.zxg.testcode E/ProviderApplication created and called onCreate 2022-04-01 15:14:46.378 18687-18687/com.zxg. Application 2022-04-01 15:14:46.380 18687-18687/com.zxg.testcode E/ProviderService: OnStartCommand ApplicationContext is com. ZXG. Queryproviderdemo. ProviderApplication @ 472 f1c7 / / Query App Query the 2022-04-01 for the second time 15:14:49. 564, 18687-18687 / com. ZXG. Testcode E/QueryProvider: Query 2022-04-01 15:14:49.564 18687-18687/com.zxg.testcode E/QueryProvider: Application 2022-04-01 15:14:49.564 getContext() is android.app.ContextImpl@869d7cf 18687-18687/com.zxg.testcode E/QueryProvider: context is com.zxg.queryproviderdemo.ProviderApplication@472f1c7Copy the code

the end

If the App processes that provide the ContentProvider are shared, be aware that their lifecycle callbacks may fail to retrieve the Application instance. Of course, this is a rare case, and if you do, consider whether the Context instance meets your needs, with the necessary Null checks.