7 in depth understanding of ContentProvider

Posted by wickning1 on Fri, 24 Dec 2021 22:04:49 +0100

Chapter 7 in depth understanding of ContentProvider

7.1 general

This chapter focuses on the implementation of ContentProvider, SQLite, Cursor query and close functions and the ContentResolver openAssetFileDescriptor function. In order to help readers further understand the knowledge points of this chapter, the author specially selects four analysis routes.

  • Article 1: through mediastore.com as a client process Images. The static function query of media class is used to query the Image related information in MediaProvider as the entry point to analyze how the system creates and starts MediaProvider. This analysis route focuses on the interaction between client processes, ActivityManagerService and MediaProvider processes.
  • Second: follow the first analysis path, but shift the focus to the analysis of how SQLiteDatabase creates the database. In addition, this route will also introduce SQLite.
  • Article 3: we will focus on the implementation details of Cursor query and close functions.
  • Article 4: the implementation of the ContentResolver openAssetFileDescriptor function will be analyzed.

Stop gossiping and start this analysis journey immediately.

7.2 startup and creation of mediaprovider

The first, second and third analysis routes will take the following example as a reference.

MediaProvider client example

void QueryImage(Context context){
  //① Get ContentResolver object
 ContentResolver cr = context.getContentResover();
  Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
  //② Query database
  Cursorcursor = MediaStore.Images.Media.query(cr,uri,null);
 cursor.moveToFirst();//③ Move cursor to head
  ......//Fetch dataset from cursor
  cursor.close();//④ Close cursor
}

Let's introduce this example first: the target ContentProvider of the client (that is, the process running this example) query is MediaProvider, which runs in the process android.process.media. It is assumed that the target process has not been started at this time.

This section focuses on:

  • How is the MediaProvider process created? How was MediaProvider created?
  • Through what does the client interact with the MediaProvider located in the target process?

Let's first look at the first key function getContentResolver.

7.2. 1. Analysis of getContentResolver function of context

According to the introduction of Context in Chapter 6, the getContentResolver of Context will eventually call the getContentResolver function of the ContextImpl object it represents. Here, you can directly see the code of ContextImpl.

ContextImpl.java::getContentResolver

public ContentResolver getContentResolver() {
   return mContentResolver;
}

This function directly returns mContentResolver. This variable is created during ContextImpl initialization. The relevant code is as follows:

ContextImpl.java::init

final void init(LoadedApk packageInfo, IBinder activityToken,
       ActivityThreadmainThread, Resources container,String basePackageName) {
   ......
  mMainThread = mainThread;//mainThread points to the actitivitythread object
  //The real type of mContentResolver is ApplicationContentResolver
  mContentResolver = new ApplicationContentResolver(this, mainThread);
   ......
 }

From the above code, the real type of mContentResolver is ApplicationContentResolver, which is an internal class defined by ContextImpl and inherits the ContentResolver.

The getContentResolver function is relatively simple, so this is the analysis. Let's look at the second key point.

Tip: for convenience of writing, the ContentProvider is abbreviated as CP and the ContentResolver is abbreviated as CR.

7.2. 2 MediaStore. Image. Analysis of query function in media

The second key point is the MediaStore called in the MediaProvider client example Image. query function of media. MediaStore is a commonly used class in multimedia development. It defines internal classes specifically for different multimedia information such as image, Audio and Video to help client developers better interact with MediaProvider. These classes and their relationships are shown in Figure 7-1.

Figure 7-1 MediaStore class diagram

As can be seen from Figure 7-1, MediaStore defines many internal classes. We focus on Image as one of the internal classes, including:

  • MediaColumns defines the database fields used by all media related database tables, while ImageColumns defines the database fields for Image alone.

  • The Image class defines an internal class called Media, which is used to query Image related information, At the same time, the Image class also defines an internal class named Thumbnails to query the information of Thumbnails related to Image (on the Android platform, there are two sources of Thumbnails, one is Image and the other is Video, so Image defines an internal class named Thumbnails, and Video also defines an internal class named Thumbnails).

Tip: the MediaStore class is complex, mainly because it defines some classes with the same name. Readers should read the code carefully.

Let's look at image The code of the query function of media is very simple, as shown below:

MediaStore.java::Image.Media.query

public static final class Media implementsImageColumns {
   public static final Cursor query(ContentResolvercr,Uri uri,
                                          String[]projection) {
   //Directly call the query function of ContentResolver
   return cr.query(uri, projection, null, null,DEFAULT_SORT_ORDER);
 }

Image. The query function of media directly calls the query function of ContentResolver. Although the real type of cr is ApplicationContentResolver, this function is implemented by its base class ContentResolver.

It is suggested that programmers pursuing efficiency may have a slight criticism about the implementation of the above code, because Image The query function of media basically doesn't do any meaningful work. If the client directly calls the cr.query function, The query here increases the cost of a function call and return (i.e. the input / output of parameters during the call and return of Image.Media query). However, the encapsulation of Image.Media will make the program clearer and easier to read (compared with the query directly using ContentResolver, code readers know that the query function of Image.Media should be related to Image, otherwise they need to parse the uri parameter to determine what the query information is). The code is clear and easy to read and runs efficiently. It is often a bear's paw and fish in software development. The opposition between them will be reflected incisively and vividly in this chapter . The author suggests that readers should make a choice in combination with the specific situation in the actual development.

1. query function analysis of contentresolver

ContentResolver.java::query

public final Cursor query(Uri uri, String[]projection,
           String selection, String[] selectionArgs, String sortOrder) {
   //Call the acquireProvider function with uri as the parameter, and the function is also implemented by ContentResolver
  IContentProvider provider = acquireProvider(uri);
  //Note: the following will interact with the ContentProvider, and relevant knowledge will be analyzed in section 7.4
   ......
 }

The query of ContentResolver will call acquireProvider, which is defined in the ContentResolver class. The code is as follows:

ContentResolver.java::query

public final IContentProvider acquireProvider(Uriuri) {
  if(!SCHEME_CONTENT.equals(uri.getScheme())) return null;
  Stringauth = uri.getAuthority();
  if (auth!= null) {
     //acquireProvider is an abstract function implemented by a subclass of ContentResolver. In this case, the function
     //Will be implemented by ApplicationContentResolver. uri.getAuthority will return a representative target
     //The name of the ContentProvider
      return acquireProvider(mContext, uri.getAuthority());
  }
  return null;
 }

As described above, acquireProvider is implemented by a subclass of ContentResolver. In this example, the function is defined by ApplicationContentResolver. The code is as follows:

ContextImpl.java::acquireProvider

protected IContentProvider acquireProvider(Contextcontext, String name) {
   //mMainThread refers to the ActivityThread object representing the main thread of the application process. Each application process has only one
   //ActivityThread object
    return mMainThread.acquireProvider(context,name);
 }

As shown in the above code, the acquireProvider function of ActivityThread will be called finally. I hope it will not be subcontracted layer by layer.

2. Analysis of acquireProvider function of actitvitythread

The code of acquireProvider function of ActivityThread is as follows:

ActivityThread.java::acquireProvider

public final IContentProvideracquireProvider(Context c, String name) {
  //① It is important to call the getProvider function. See analysis below
  IContentProvider provider = getProvider(c,name);
  ......
  IBinderjBinder = provider.asBinder();
  synchronized(mProviderMap) {
     //The client process saves the ContentProvider information used by the process to the mProviderRefCountMap,
     //Its main function is related to reference counting and resource release. Readers can ignore it for the time being
     ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
      if(prc== null)
          mProviderRefCountMap.put(jBinder, newProviderRefCount(1));
     else   prc.count++;
   }
 return provider;
}

Call getProvider inside acquireProvider to get an IContentProvider type object. This function is very important. Its code is:

ActivityThread.java::getProvider

private IContentProvider getProvider(Contextcontext, String name) {
  /*
   Query whether the application process has saved the existing object used to communicate with the remote ContentProvider.
   Here, we know that the type of existing is IContentProvider, but IContentProvider is a
   An interface, so what is the real type of existing? We'll reveal it later
  */
 IContentProvider existing = getExistingProvider(context, name);
  if(existing != null) return existing;//If existing exists, it returns directly
 
 IActivityManager.ContentProviderHolder holder = null;
  try {
      //If existing does not exist, you need to query AMS, and the return value type is ContentProviderHolder
      holder= ActivityManagerNative.getDefault().getContentProvider(
               getApplicationThread(), name);
  }......
   //Note: remember that the following function call is in the client process
 IContentProvider prov = installProvider(context, holder.provider,
               holder.info, true);
   ......
  return prov;
 }

The headache in the above code is the new data types, such as IContentProvider and ContentProviderHolder. First, analyze the getContentProvider of AMS.

3. Analysis of getContentProvider function of AMS

The function of getContentProvider is mainly realized by getContentProviderImpl function, so it can be analyzed directly here.

(1) getContentProviderImpl starts the target process
The getContentProviderImpl function is long and can be viewed in segments. Let's analyze the following code first.

ActivityManagerService.java::getContentProviderImpl

 private final ContentProviderHolder getContentProviderImpl(
                          IApplicationThread caller, String name) {
 ContentProviderRecord cpr;
 ProviderInfo cpi = null;
 synchronized(this) {
    ProcessRecord r = null;
     if(caller != null) {
          r= getRecordForAppLocked(caller);
          if (r == null)......//If the caller information cannot be queried, a SecurityException is thrown
      }// If (caller! = null) judge end
    //The name parameter specifies the authority representing the target ContentProvider for the calling process
    cpr =mProvidersByName.get(name);
    //If cpr is not empty, it indicates that the ContentProvider has been registered in AMS
    booleanproviderRunning = cpr != null;
    if(providerRunning){
      ......//If the ContentProvider already exists, it will be processed accordingly, and the relevant contents can be read by yourself
    }
   //If the process corresponding to the target ContentProvider has not been started
   if(!providerRunning) {
       try {
           //Query PKMS to get the specified ProviderInfo information
           cpi = AppGlobals.getPackageManager().resolveContentProvider(
                   name,STOCK_PM_FLAGS |
                   PackageManager.GET_URI_PERMISSION_PATTERNS);
        }......
        String msg;
      //Permission check is not discussed here
      if((msg=checkContentProviderPermissionLocked(cpi, r)) != null)
         throw new SecurityException(msg);
     /*
      If system_ The server has not been started, and the ContentProvider does not run in the system_server
      Content provider is not allowed to start at this time. Readers still remember which ContentProvider runs in
     system_server In progress? The answer is SettingsProvider
    */
     .......
    ComponentName comp = new ComponentName(cpi.packageName, cpi.name);
     cpr =mProvidersByClass.get(comp);
     finalboolean firstClass = cpr == null;
     //When starting the corresponding process of MediaProvider for the first time, firstClass must be true
     if (firstClass) {
         try {
             //Query PKMS to get the Application information where MediaProvider is located
              ApplicationInfoai =
                      AppGlobals.getPackageManager().getApplicationInfo(
                          cpi.applicationInfo.packageName, STOCK_PM_FLAGS);
             if (ai == null) return null;
            //Save ContentProvider information through ContentProviderRecord in AMS, similar to
            //ActivityRecord, BroadcastRecord, etc
             cpr = new ContentProviderRecord(cpi, ai,comp);
         }......
    }// if(firstClass) judge the end

The logic of the above code is relatively simple, mainly to create a ContentProviderRecord object for the target ContentProvider (i.e. MediaProvider). Combined with the knowledge in Chapter 6, AMS has designed corresponding data structures for the four components, such as ActivityRecord, BroadcastRecord, etc.

Next, look at getContentProviderImpl. The next step is to start the target process:

ActivityManagerService.java::getContentProviderImpl

   /*
   canRunHere The function is used to determine whether the target CP can run in the process corresponding to r (that is, the process of the caller)
   The internal judgment of this function is as follows:
   (info.multiprocess|| info.processName.equals(app.processName))
    && (uid == Process.SYSTEM_UID || uid == app.info.uid)
    For this example, MediaProvider cannot run in a client process
   */
    if (r !=null && cpr.canRunHere(r))  returncpr;
    finalint N = mLaunchingProviders.size();
    ......//Find out whether the process corresponding to the target ContentProvider is starting
    //If i is greater than or equal to N, it indicates that the information of the target process is not in mLaunchingProviders
    if (i>= N) {
       finallong origId = Binder.clearCallingIdentity();
       ......
       //Call the startProcessLocked function to create the target process
      ProcessRecord proc = startProcessLocked(cpi.processName,
                         cpr.appInfo, false, 0,"content provider",
                         newComponentName(cpi.applicationInfo.packageName,
                          cpi.name), false);
       if(proc == null)return null;
      cpr.launchingApp = proc;
       //Save the process information to mLaunchingProviders
      mLaunchingProviders.add(cpr);
    }
 
     if(firstClass) mProvidersByClass.put(comp, cpr);
    mProvidersByName.put(name, cpr);
     /*
       The following function will establish a close relationship between the client process and the target CP process, that is, when the target CP process dies,
        AMS The client processes will be found and kill ed according to the relationship established by this function. In section 7.2.3
        There is an explanation for this function
     */
     incProviderCount(r, cpr);
     if(cpr.launchingApp == null) return null;
     try {
          cpr.wait();//Wait for the current process to start
      } ......
   }// synchronized(this) end
  return cpr;
}

Through the analysis of the above code, it is found that getContentProviderImpl will wait for an event. If you want to read it, you can understand that you must be waiting for the target process to start and create a MediaProvider. This part of the work of the target process is expressed in professional terms, that is, publishing the target ContentProvider (that is, the MediaProvider in this example).

(2) Creation of MediaProvider
According to the introduction in Chapter 6, the first important thing to be done after the target process starts is to call the attachApplication function of AMS. The main function of this function is completed by attachApplicationLocked. Let's review the code.

ActivityManagerService.java::attachApplicationLocked

private final booleanattachApplicationLocked(IApplicationThread thread,
           int pid) {
   ......
   //Query the CP information running in the process through PKMS and save it to mProvidersByClass
   Listproviders = normalMode ?
               generateApplicationProvidersLocked(app) : null;
  //Call the bindApplication function of the target application process, where the providers information is passed to the target process
   thread.bindApplication(processName,appInfo, providers,
                             app.instrumentationClass, profileFile,
                              ......);
   ......
}

Let's take another look at the implementation of the target process bindApplication. Its internal will finally be processed through the handleBindApplication function. Let's review the relevant code.

ActivtyThread.java::handleBindApplication]

private void handleBindApplication(AppBindDatadata) {
   ......
   if(!data.restrictedBackupMode){
       List<ProviderInfo> providers = data.providers;
        if(providers != null) {
           //Call installContentProviders to install
           installContentProviders(app, providers);
           ......
       }
   }
   ......
}

The ProviderInfo list passed from AMS will be processed by the installContentProviders of the target process, and the relevant codes are as follows:

ActivtyThread.java::installContentProviders

private void installContentProviders(Contextcontext, List<ProviderInfo>providers) {
	final ArrayList<IActivityManager.ContentProviderHolder> results =
                newArrayList<IActivityManager.ContentProviderHolder>();
	Iterator<ProviderInfo> i = providers.iterator();
	while(i.hasNext()) {
    //① Also call installProvider. Note that the second parameter passed by this function is null
    IContentProvider cp = installProvider(context, null, cpi, false);
    if (cp!= null) {
        IActivityManager.ContentProviderHolder cph =
                         newIActivityManager.ContentProviderHolder(cpi);
        cph.provider = cp;
        results.add(cph);//Add information to the results array
        ......//Create reference count
        }
    }//The while loop ends
    try {  //② Call publishContentProviders of AMS to publish ContentProviders
           ActivityManagerNative.getDefault().publishContentProviders(
               getApplicationThread(), results);
        } ......
 }

The above code lists two key points, namely:

  • Call installProvider to get an object of IContentProvider type.
  • Call publishContentProviders of AMS to publish the ContentProvider running by this process. This function is left for later analysis

Before continuing the analysis, I would like to emphasize installProvider in particular. This function is called in both the client process (remember the comment in the acquireProvider function of ActivityThread in section 7.2.2?) and the target process (that is, the process where MediaProvider is located here). Compared with the call of the client process, there is only one obvious difference:

  • When the client process calls the installProvider function, the second parameter of the function is not null.
  • When the target process calls the installProvider function, the second parameter of the function is hard coded as null.

We were on 6.2 3 after analyzing the installProvider function, combined with the introduction there, we can see that installProvider is a general function. It will be called in the end regardless of whether the client uses the remote CP or the CP installed and running on it by the target process, but the parameters are different.

Look at the installProvider function. Its code is as follows:

ActivityThread.java::installProvider

private IContentProvider installProvider(Contextcontext,
           IContentProvider provider, ProviderInfo info, boolean noisy) {
  ContentProvider localProvider = null;
   if(provider == null) {//For the target process
       Context c = null;
        ApplicationInfo ai = info.applicationInfo;
         if(context.getPackageName().equals(ai.packageName)) {
               c = context;
           }......//This part of the code has been in 6.2 Section 3 has been analyzed, and its purpose is to get the correct results
          //Context is used to load Java bytecode
         try{
            final java.lang.ClassLoader cl = c.getClassLoader();
            //Create MediaProvider instance through Java reflection mechanism
            localProvider = (ContentProvider)cl.
                                     loadClass(info.name).newInstance();
            //Note the following code
            provider = localProvider.getIContentProvider();
            }
     } elseif (localLOGV) {
           Slog.v(TAG, "Installing external provider " + info.authority +": "
                   + info.name);
    }// if(provider == null) judge end
    /*
     As can be seen from the above code, this function does not have a null value when the provider is not null (that is, when called by the client)
     What special treatment
    */
    ......
     /*
      Reference counting, setting deathreceiver and other related operations. In 6.2 In the second subheading of section 3
      It has been said that it is useless for the target process to set deathreceiver for the CP instance in its own process, because they are in the same process
      In the process, how can I receive my obituary message? However, if the client process is the CP setting of the target process
     DeathReceipient Does it work again? Think about it carefully
    */
    return provider;//The final returned object is IContentProvider type. What is it?
 }

It can be seen from the code that the installProvider finally returns an IContentProvider type object. For the target process, the object is obtained by calling the getIContentProvider function of the CP instance object (MediaProvider in this case). For the client process, the object is passed in by the second parameter of installProvider. What is the IContentProvider?

(3) The true face of IContentProvider
To clarify IContentProvider, first look at the class diagram of ContentProvider family, as shown in Figure 7-2.

Figure 7-2 class diagram of ContentProvider

Figure 7-2 reveals the true face of IContentProvider, as follows:

  • Each ContentProvider instance has an mTransport member of type Transport.
  • The Transport class derives from ContentProviderNative. As shown in Figure 7-2, ContentProviderNative derives from Binder class and implements IContentProvider interface. Combined with the previous code, IContentProvider will be the interface between the client process and the target process, that is, the target process uses the Bn Transport of IContentProvider, while the client uses the Bp Transport of IContentProvider. Its type is ContentProviderProxy (defined in ContentProviderNative.java).

How does the client interact with the target CP process through the IContentProvider query function? The process is as follows:

  • The CP client obtains the Bp end of IContentProvider (the actual type is ContentProviderProxy), calls its query function, packages the parameter information inside the function, and passes it to Transport (it is the Bn end of IContentProvider).
  • The onTransact function of Transport will call the query function of Transport, and the query function of Transport will call the query function defined by the subclass of ContentProvider (that is, the query function of MediaProvider).

For a series of calling functions of the target process, you might as well take a look at the query function of Transport, whose code is:

ContentProvider.java::Transport.query

public Cursor query(Uri uri, String[] projection,
               String selection, String[] selectionArgs, String sortOrder) {
  enforceReadPermission(uri);
  //Transport is the internal class of ContentProvider. The query function of ContentProvider will be called here
   //In this example, the query function is implemented by MediaProvider, so it will eventually call MediaProvider's query
   returnContentProvider.this.query(uri, projection, selection,
                   selectionArgs, sortOrder);
 }

Make sure that there is only one instance of the target ContentProvider, that is, only one MediaProvider object. Although the query function of Transport calls the query function of the base class ContentProvider, according to the principle of object-oriented polymorphism, the function is finally implemented by its subclass (MediaProvider in this case).

Knowing IContentProvider means knowing the interactive interface between the client process and the target process.

Continue our analysis. At this time, the target process needs to publish the MediaProvider information through AMS.

(4) AMS pulishContentProviders analysis
To publish the CP information of the target process, you need to use the pulishContentProviders function of AMS, and its code is as follows:

ActivityManagerService.java::publishContentProviders

public final voidpublishContentProviders(IApplicationThread caller,
         List<ContentProviderHolder> providers) {
   ......
  synchronized(this) {
     finalProcessRecord r = getRecordForAppLocked(caller);
     finallong origId = Binder.clearCallingIdentity();
 
     final int N = providers.size();
     for (int i=0; i<N; i++) {
       ContentProviderHolder src = providers.get(i);
       ContentProviderRecord dst = r.pubProviders.get(src.info.name);
       if(dst != null) {
           ......//Save relevant information to mProviderByClass and mProvidersByName respectively
           int NL = mLaunchingProviders.size();
         ...... //The target process has started, remove it from mLaunchingProviders
         synchronized (dst) {
            dst.provider = src.provider;//Save information in dst
            dst.proc = r;
           //Trigger the client process that is still wait ing in getContentProvider
            dst.notifyAll();
          }
       updateOomAdjLocked(r);//Adjust the oom of the target process_ Adj and other related parameters
     }// If (DST! = null) judge end
   ......
   }
}

At this point, the client process will return from getContentProvider and call the installProvider function. According to the previous analysis, when the client process calls installProvider, its second parameter is not null, that is, the client process has obtained the IContentProvider Bp end object that can directly interact with the target process from AMS. After that, the client can directly use the object to make a request to the target process.

7.2. 3. Mediaprovider startup and creation summary

Review the entire MediaProvider startup and creation process, as shown in Figure 7-3.

Figure 7-3 startup and creation process of mediaprovider

The whole process is relatively simple. When analyzing, readers only need to pay attention to the difference between the installProvider function when called in the target process and the client process. Here again:

  • When the target process calls installProvider, the second parameter passed is null, which makes the internal create the target CP instance through the Java reflection mechanism.
  • When the client calls installProvider, its second parameter has been obtained by querying AMS. The real work of this function is just reference count control and setting obituary receiving object.

So far, IContentProvider, the communication channel between the client process and the target process, has appeared. In addition, the client process and the target CP also establish a very close relationship. The consequence of this relationship is that once the target CP process dies, AMS will kill the related client processes. Review the relevant knowledge points:

  • The establishment of the relationship is accomplished by calling incProviderCount in the AMS getContentProviderImpl function. The establishment of the relationship is marked by ContentProviderRecorder preserving the ProcessRecord information of the client process.
  • Once the CP process dies, AMS can find all client processes using the CP according to the client information saved in the ContentProviderRecorder, and then kill them.

Can the client undo this tight relationship? The answer is yes, but it has something to do with whether Cursor is turned off. Here is a brief description of the process:

  • When Cursor is closed, the releaseProvider of ContextImpl will be called. According to the previous introduction, it will eventually call the releaseProvider function of ActivityThread.
  • The releaseProvider function of ActivityThread will cause completeRemoveProvider to be called. Internally, judge whether to call the removeContentProvider of AMS according to the reference count of the CP.
  • The removeContentProvider of AMS will delete the information of this client process in the corresponding ContentProviderRecord. In this way, the close relationship between the client process and the target CP process will disappear.

So far, the first analysis route in this chapter has been introduced.

It is suggested that readers may feel that this route is a supplement and continuation of Chapter 6. However, although the target process is created and started by AMS, and the publishing of ContentProvider also needs to interact with AMS, for ContentProvider, we pay more attention to the interaction between the client and the ContentProvider instance in the target process. In fact, after the client obtains the IContentProvider Bp object, it can directly interact with the CP instance of the target process, and there is no need to use AMS. Therefore, the author puts this route into this chapter for analysis.

7.3 SQLite create database analysis

As the repository of media information in Android multimedia system, MediaProvider uses SQLite database to manage multimedia related data information in the system. As the second analysis route, the goal of this section is to analyze how MediaProvider uses SQLite to create a database. At the same time, it will also introduce some knowledge points related to SQLite.

Let's first look at the famous SQLite and the SQLiteDatabase family of the Java layer.

7.3.1 SQLite and SQLiteDatabase family

1. SQLite goes to battle with light equipment

SQLite is a lightweight database. Compared with SQL server or Oracle DB that the author came into contact with before, it is like the difference between ants and elephants. How light is it? The author summarizes two characteristics of SQLite:

  • From the code point of view, all the functions of SQLite are implemented in SQLite3 C, and the header file SQLite3 H defines the API s it supports. Where SQLite3 C file contains about 120000 lines of code, which is equivalent to a medium to small-scale program.
  • From the user's point of view, SQLite will generate a libsqlite So, the size is only more than 300 KB.

SQLite is really light enough, but this "light" is not the "light" in the title of this section. Why? Here we first introduce readers to an Android native program example developed directly using SQLite API. The binary executable program finally compiled by the example is called sqlitetest.

Note that the SQLite API mentioned later in this book specifically refers to libsqlite API of Native layer provided by so.

The code of the Android native program example developed using SQLite API is as follows:

SqliteTest.cpp::libsqlite example

#include <unistd.h>
#include <sqlite3. h> / / contains sqlite API header files. Here, 3 is the version number of SQLite
#include <stdlib.h>

#define LOG_TAG "SQLITE_TEST" / / define the label of the logcat output of the process
#include <utils/Log.h>
#ifndef NULL
   #defineNULL (0)
#endif
//Declare the path to the database file
#define DB_PATH"/mnt/sdcard/sqlite3test.db"
/*
   Declare a global SQLite handle. Developers do not need to know the specific content of the data structure, as long as they know that it represents the user
   A connection relationship with the database. In the future, all operations on a specific database need to pass in the corresponding SQLite handle
*/
static sqlite3* g_pDBHandle = NULL;
/*
   Define a macro to detect the return value of SQLite API call. If value is not equal to expectValue, print a warning,
   And exit the program. Note that after the process exits, the system will automatically reclaim the allocated memory resources. For such a simple example, the reader can
    Don't be critical. Among them, SQLite3_ The errmsg function prints error messages
*/
#define CHECK_DB_ERR(value,expectValue) \
do \
{ \
   if(value!= expectValue)\
   {\
    LOGE("Sqlite db fail:%s",g_pDBHandle==NULL?"db not \
           connected":sqlite3_errmsg(g_pDBHandle));\
    exit(0);\
   }\
}while(0)

int main(int argc, char* argv[])
{
  LOGD("Delete old DB file");
  unlink(DB_PATH);//Delete the old database file first
  LOGD("Create new DB file");
   /*
    Call sqlite3_open creates a database and saves the connection environment with the database in the global SQLite handle
     g_pDBHandle Medium, later operation g_pDBHandle is the operation dB_ Database corresponding to path
    */
   int ret =sqlite3_open(DB_PATH,&g_pDBHandle);
  CHECK_DB_ERR(ret,SQLITE_OK);
  
  LOGD("Create Table personal_info:");
   /*
    Define macro TABLE_PERSONAL_INFO is used to describe the SQL statement used to create a table for the database of this example,
    Readers who are not familiar with SQL statements can learn relevant knowledge first. From the definition of this macro, a macro named
   personal_info The table has four columns. The first column is the primary key and the type is INTEGER (INTEGER in SQLite),
    The value will increase automatically every time a row of data is added; The second column name is "name", and the data type is string (TEXT in SQLite);
     The third column is named "age", and the data type is integer; The fourth column is named "sex" and the data type is string
   */
   #defineTABLE_PERSONAL_INFO  \
          "CREATETABLE personal_info" \
           "(ID INTEGER primary keyautoincrement," \
           "nameTEXT," \
           "age INTEGER,"\
           "sex TEXT"\
           ")"
   //Print table_ PERSONAL_ SQL statement corresponding to info
  LOGD("\t%s\n",TABLE_PERSONAL_INFO);
  //Call sqlite3_exec executes the previous SQL statement
   ret =sqlite3_exec(g_pDBHandle,TABLE_PERSONAL_INFO,NULL,NULL,NULL);
  CHECK_DB_ERR(ret,SQLITE_OK);
  
   /*
    Define the SQL statement used to insert a row of data. Note the question mark in the last row, which indicates the need to insert data
   Before binding to a specific value. The SQL statement corresponding to inserting a database is a standard INSERT command
   */
  LOGD("Insert one personal info into personal_info table");
   #defineINSERT_PERSONAL_INFO  \
  "INSERT INTO personal_info"\
  "(name,age,sex)"\
  "VALUES"\
  "(?,?,?)"   //Notice the question mark in this line
  LOGD("\t%s\n",INSERT_PERSONAL_INFO);
   //sqlite3_stmt is a very important structure in SQLite, which represents an SQL statement
   sqlite3_stmt* pstmt = NULL;
   /*
    Call sqlite3_prepare initializes pstmt and combines it with INSERT_PERSONAL_INFO binding.
    That is, if pstmt is executed, insert will be executed_ PERSONAL_ Info statement
   */
   ret =sqlite3_prepare(g_pDBHandle,INSERT_PERSONAL_INFO,-1,&pstmt,NULL);
  CHECK_DB_ERR(ret,SQLITE_OK);
  /*
    Call sqlite3_bind_xxx binds a specific value to the corresponding question mark in the pstmt. The second parameter of the function is used to
    Specify which question mark
  */
   ret =sqlite3_bind_text(pstmt,1,"dengfanping",-1,SQLITE_STATIC);
   ret =sqlite3_bind_int(pstmt,2,30);
   ret =sqlite3_bind_text(pstmt,3,"male",-1,SQLITE_STATIC);
   //Call sqlite3_step executes the corresponding SQL statement. If the function is executed successfully, our personal_info
   //A new record will be added in the table, and the corresponding value is (1, dengfanping, 30, male)
   ret =sqlite3_step(pstmt);
  CHECK_DB_ERR(ret,SQLITE_DONE);
   //Call sqlite3_finalize destroys the SQL statement
   ret =sqlite3_finalize(pstmt);
   //Next, you will query the age value of the person whose name is "dengfanping" from the table
  LOGD("select dengfanping's age from personal_info table");
   pstmt =NULL;
   /*
    Reinitialize the pstmt and compare it with the SQL statement "SELECT age FROM personal_info WHERE name"
     = ?"binding
   */
   ret =sqlite3_prepare(g_pDBHandle,"SELECT age FROM personal_info WHERE name
                                              =? ",-1,&pstmt,NULL);
  CHECK_DB_ERR(ret,SQLITE_OK);  
   /*
    The first question mark in the binding pstmt is the string "dengfanping", and the final SQL statement is
    SELECTage FROM personal_info WHERE name = 'dengfanping'
   */
   ret =sqlite3_bind_text(pstmt,1,"dengfanping",-1,SQLITE_STATIC);
  CHECK_DB_ERR(ret,SQLITE_OK);
   //Execute this query statement
 while(true)//Traverse the result set in a loop
 {
    ret =sqlite3_step(pstmt);
   if(ret ==SQLITE_ROW)
   {
      //Take out the first column from the result set (because only age is selected when executing SELECT, there is only one column in the final result),
      //Call sqlite3_column_int returns the value of the first row of the first column (starting from 0) of the result set
      intmyage = sqlite3_column_int(pstmt, 0);
      LOGD("Gotdengfanping's age: %d\n",myage);
   }
   else //If ret is another value, exit the loop
     break;
  }

   else  LOGD("Find nothing\n"); //SELECT execution failed
   ret =sqlite3_finalize(pstmt);//Destroy pstmt
  if(g_pDBHandle)
  {
     LOGD("CloseDB");
    //Call sqlite3_close close the database connection and release g_pDBHandle
   sqlite3_close(g_pDBHandle);
   g_pDBHandle = NULL;
   }
  return 0;
}

It can be found from the above example code that the use of SQLite API mainly focuses on the following points:

  • Creates an sqlite3 instance representing the specified database.
  • Create SQLite3 representing an SQL statement_ For stmt instance, SQLite3 should be called first during use_ Prepare binds it to a string representing an SQL statement. If the string contains a wildcard '?', You need to pass SQLite3 later_ bind_ The XXX function binds specific values for wildcards to generate a complete SQL statement. Finally, SQLite3 is called_ Step executes this statement.
  • If it is a query command (i.e. SELECT command), you need to call sqlite3_step function to traverse the result set, and get the value of a specified column in a row in the result set through sqlite3_column_xx and other functions.
  • Finally, sqlite3 needs to be called_ Finalize and sqlite3_close to release sqlite3_ The resources occupied by stmt instances and sqlite3 instances.

The use of SQLite API is very simple and clear. However, it is a pity that the shortcut brought by this simplicity is only available to those Native layer program developers. For Java programmers, they can only use the classes and related APIs provided by the SQLiteDatabase family encapsulated by Android on top of the SQLite API. In my mind, there is only one word "amazing" for the evaluation of this package. Considering the stability and scalability of the architecture and system, Android has designed object-oriented packaging and decoupling on the SQLite API, and finally presented a large and complex SQLiteDatabase family in front of you, It has as many as 61 members (see the files in the frameworks/base/core/java/android/database directory).

Now readers should understand the true meaning of "light" in the title of this section "SQLite light". In the subsequent analysis process, we mainly deal with SQLiteDatabase family. With the in-depth analysis, readers can gradually see and understand the "massiness" of SQLiteDatabase.

2. Introduction to sqlitedatabase family

Figure 7-4 shows several important members of the SQLiteDatabase family.

Figure 7-4 some members of sqlitdatabase family

The related classes in Figure 7-4 are described as follows:

  • SQLiteOpenHelper is a Utility class for developers to create and manage databases.

  • SQLiteQueryBuilder is a help class that helps developers create SQL statements.

  • SQLiteDatabase represents SQLite database, which internally encapsulates a sqlite3 instance of the Native layer.

  • Android provides three classes SQLiteProgram, SQLiteQuery and SQLiteStatement to describe information related to SQL statements. As can be seen from figure 7-4, SQLiteProgram is a base class, which provides some API s for parameter binding. SQLiteQuery is mainly used for query operations, SQLiteStatement is used for operations other than query (according to the SDK, if SQLiteStatement is used for query, the returned result set can only be 1 row * 1 column). Note that in these three classes, the base class SQLiteProgram will store a variable pointing to the sqlite3_stmt instance of the Native layer, but the assignment of this member variable is related to another class SQLiteComplieSql hidden from developers. From this point of view, you can Consider the Native layer SQLite3_ The encapsulation of stmt instances is done by SQLiteComplieSql. This knowledge can be seen in the later analysis.

  • Sqlitecloseable is used to control the life cycle of instances of some classes in the SQLiteDatabase family, such as SQLiteDatabase instance and SQLiteQuery instance. Before each use of these instance objects, you need to call acquireReference to increase the reference count. After use, you need to call releasereference to reduce the reference count.

Remind readers how they feel after seeing these members of the SQLiteDatabase family? Do you think it will take some time to really understand them?

Let's look at how MediaProvider uses SQLite database, focusing on how SQLite database is created.

7.3.2 MediaProvider create database analysis

In MediaProvider, the attach function triggers the database, and its code is as follows:

MediaProvider::attach

private Uri attachVolume(String volume) {
  Contextcontext = getContext();
 DatabaseHelper db;
  if(INTERNAL_VOLUME.equals(volume)) {
        ......//Database for internal storage
     } elseif (EXTERNAL_VOLUME.equals(volume)) {
       ......
        String dbName = "external-" +Integer.toHexString(volumeID) + ".db";
        //① Construct a DatabaseHelper object
       db =new DatabaseHelper(context, dbName, false,
                                   false, mObjectRemovedCallback);
       ......//Omit irrelevant content
    }......
 
  if(!db.mInternal) {
     //② Call the getWritableDatabase function of DatabaseHelper, and the type of return value of this function is
   //SQLiteDatabase, that is, the object representing the SQLite database
    createDefaultFolders(db.getWritableDatabase());
     .......
  }
  ......
}

Two key points are listed in the above code:

  • Construct a DatabaseHelper object.
  • Call the getWritableDatabase function of the DatabaseHelper object to get a SQLiteDatabase object representing the SQLite database.

1. DatabaseHelper analysis

DatabaseHelper is an internal class of MediaProvider. It derives from SQLiteOpenHelper. Its constructor code is as follows:

(1) DatabaseHelper constructor analysis

MediaProvider.java::DatabaseHelper

public DatabaseHelper(Context context, Stringname, boolean internal,
                    boolean earlyUpgrade,
                   SQLiteDatabase.CustomFunction objectRemovedCallback) {
      //Focus on the constructor of its base class
      super(context, name, null,DATABASE_VERSION);
     mContext = context;
      mName= name;
     mInternal = internal;
     mEarlyUpgrade = earlyUpgrade;
     mObjectRemovedCallback = objectRemovedCallback;
 }

SQLiteOpenHelper is the base class of DatabaseHelper, and its constructor code is as follows:

SQLiteOpenHelper.java::SQLiteOpenHelper

public SQLiteOpenHelper(Context context, Stringname, CursorFactory factory,
                             int version) {
  //Call another constructor, noting that it creates a new default error handling object
 this(context, name, factory, version, newDefaultDatabaseErrorHandler());
}
public SQLiteOpenHelper(Context context, Stringname, CursorFactory factory,
            int version, DatabaseErrorHandlererrorHandler) {
    ......
    mContext= context;
    mName =name;
   //Seeing the word "factory", readers should think of the factory pattern in the design pattern. In this case, the variable is null
    mFactory= factory;
   mNewVersion = version;
    mErrorHandler= errorHandler;
 }

The above functions are relatively simple, but they contain a more profound design concept, as follows:

From the constructor of SQLiteOpenHelper, the database object corresponding to MediaProvider (i.e. SQLiteDatabase instance) is not created in this function. When was the SQLiteDatabase instance representing the database created?

The so-called lazy creation method is used here, that is, the SQLiteDatabase instance is really created when it is used for the first time, that is, the second key function getWritableDatabase in this example.

Before analyzing the getWritableDatabase function, let's introduce some knowledge about delay creation.

Delay creation or delay initialization, the so-called "heavy" resources (such as those with large memory or long creation time), is a common strategy [①] in system development and design. When using this strategy, developers not only "care about everything" during resource creation, but also on resource release "Be cautious. Reference counting technology is generally used to control resource release.

Combined with the previous introduction to SQLiteDatabase, it is found that the SQLiteDatabase framework does not simply map the SQLite API to the Java layer, but has a lot of more detailed considerations. For example, in this framework, the lazy creation method is used for resource creation, and SQLiteClosable is used for resource release to control the life cycle.

It is suggested that although this framework is more perfect and extensible, it is much more complex to use it than to directly use SQLite API. Therefore, in the development process, whether to use this framework should be comprehensively considered according to the actual situation. For example, when developing the company's DLNA solution, the author directly used SQLite API instead of this framework.

(2) getWritableDatabase function analysis
Now let's look at the code for getWritableDatabase.

SQLiteDatabase.java::getWritableDatabase

public synchronized SQLiteDatabasegetWritableDatabase() {
  if(mDatabase != null) {
    //The first time this function is called, the mDatabase has not been created. Future calls will directly return the created mDatabase
   }

   boolean success = false;
  SQLiteDatabase db = null;
   if (mDatabase != null) mDatabase.lock();
   try {
        mIsInitializing = true;
        if(mName == null) {
            db = SQLiteDatabase.create(null);
         }else {
            //① Call openOrCreateDatabase of Context to create a database
            db = mContext.openOrCreateDatabase(mName, 0,
                                                 mFactory, mErrorHandler);
        }

     int version = db.getVersion();
     if(version != mNewVersion) {
         db.beginTransaction();
          try {
               if (version == 0) {
              /*
              If the database is created for the first time (that is, the corresponding database file does not exist), the implementation of the subclass is called
              onCreate Function. The onCreate function implemented by the subclass will complete database table creation and other operations. Readers may wish
              View the onCreate function implemented by MediaProviderDatabaseHelper
             */
                 onCreate(db);
               } else {
                  //If the version number read from the database file is inconsistent with the version number set by MediaProvider,
                  //Call onDowngrade or onUpgrade implemented by the subclass for corresponding processing
                  if (version > mNewVersion)
                    onDowngrade(db, version,mNewVersion);
                  else
                    onUpgrade(db, version,mNewVersion);
                }
              db.setVersion(mNewVersion);
             db.setTransactionSuccessful();
       }finally {
          db.endTransaction();
       }
   }// If (version! = mnewversion) end of judgment
  onOpen(db);//Call the onOpen function implemented by the subclass
   success =true;
   return db;
  }......

As can be seen from the above code, the SQLiteDatabase object representing the database is created by context openOrCreateDatabase. This function is analyzed in a separate section below.

2. ContextImpl openOrCreateDatabase analysis

(1) openOrCreateDatabase function analysis
I believe that readers can accurately locate the real implementation of openOrCreateDatabase function, which is in contextimpl In Java, the code is as follows:

ContextImpl.java::openOrCreateDatabase

public SQLiteDatabase openOrCreateDatabase(Stringname, int mode,
               CursorFactory factory,DatabaseErrorHandler errorHandler) {
  File f =validateFilePath(name, true);
  //Call the static function openOrCreateDatabase of SQLiteDatabase to create the database
 SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(f.getPath(),
                                             factory, errorHandler);
 setFilePermissionsFromMode(f.getPath(), mode, 0);
  return db;
 }

SQLiteDatabase.java::openDatabase

public static SQLiteDatabase openDatabase(Stringpath, CursorFactory
            factory, int flags,DatabaseErrorHandlererrorHandler) {
    //Calling openDatabase to create an SQLiteDatabase instance is really subcontracting at all levels
   SQLiteDatabase sqliteDatabase = openDatabase(path, factory,
                                              flags, errorHandler,(short) 0);
    if(sBlockSize == 0)  sBlockSize = newStatFs("/data").getBlockSize();
     //Set some parameters for this SQLiteDatabase instance. These contents are related to the characteristics of SQLite itself, which is not included in this book
    //If you want to discuss this in depth, interested readers may wish to refer to the materials provided on SQLite's official website
    sqliteDatabase.setPageSize(sBlockSize);
    sqliteDatabase.setJournalMode(path, "TRUNCATE");
    synchronized(mActiveDatabases) {
           mActiveDatabases.add(
                     new WeakReference<SQLiteDatabase>(sqliteDatabase));
     }
    return sqliteDatabase;
 }

openDatabase will actually create an SQLiteDatabase instance, and its related code is:

SqliteDatabase.java::openDatabase

private static SQLiteDatabase openDatabase(Stringpath, CursorFactory factory,
                             int flags,DatabaseErrorHandler errorHandler,
                             shortconnectionNum) {
   //Construct an SQLiteDatabase instance
  SQLiteDatabase db = new SQLiteDatabase(path, factory, flags,errorHandler,
                                                 connectionNum);
   try {
        db.dbopen(path, flags);//Open the database. dbopen is a native function
        db.setLocale(Locale.getDefault());//Set Locale
        ......
         return db;
    }......
 }

In fact, openDatabase has basically done two things: creating an instance of SQLiteDatabase and calling the dbopen function of that instance.

(2) Analysis of SQLiteDatabase constructor and dbopen function
First look at the constructor of SQLitedDatabase. The code is as follows:

SQLitedDatabase.java::SQLiteDatabase

private SQLiteDatabase(String path, CursorFactoryfactory, int flags,
           DatabaseErrorHandler errorHandler, short connectionNum) {
  setMaxSqlCacheSize(DEFAULT_SQL_CACHE_SIZE);
   mFlags =flags;
   mPath = path;
   mFactory= factory;
   mPrograms= new WeakHashMap<SQLiteClosable,Object>();
   // config_ The cursorwindowsize value is 2048, so the limit value obtained below should be 8MB
   int limit= Resources.getSystem().getInteger(
               com.android.internal.R.integer.config_cursorWindowSize)
                * 1024 * 4;
  native_setSqliteSoftHeapLimit(limit);
 }

As mentioned earlier, the SQLiteDatabase object of the Java layer will be bound with a sqlite3 instance of the Native layer. From the above code, it can be found that the binding work is not carried out in the constructor. In fact, this work is completed by the dbopen function, and the relevant codes are as follows:

android_database_SQLiteDatabase.cpp::dbopen

static void dbopen(JNIEnv* env, jobject object,jstring pathString, jint flags)
{
    int err;
    sqlite3* handle = NULL;
   sqlite3_stmt * statement = NULL;
    charconst * path8 = env->GetStringUTFChars(pathString, NULL);
    intsqliteFlags;
   registerLoggingFunc(path8);
    if(flags & CREATE_IF_NECESSARY) {
       sqliteFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
    } ......
    //Call sqlite3_open_v2 function to create database, sqlite3_open_v2 and SQLite3 in the example_ Open similar
    //handle is used to store newly created instances of sqlite3 * type
    err =sqlite3_open_v2(path8, &handle, sqliteFlags, NULL);
    ......
   sqlite3_soft_heap_limit(sSqliteSoftHeapLimit);
    err =sqlite3_busy_timeout(handle, 1000 /* ms */);
    ......
    //Android has also made some special customizations on the native SQLite, and the relevant contents will be analyzed at the end of this section
    err =register_android_functions(handle, UTF16_STORAGE);
    //Save the handle in the SQLiteDatabase object of the Java layer so that the SQLiteDatabase instance of the Java layer
    //It is bound to a sqlite3 instance of the Native layer
   env->SetIntField(object, offset_db_handle, (int) handle);
    handle =NULL;  // The caller owns the handle now.
 
done:
    if(path8 != NULL) env->ReleaseStringUTFChars(pathString, path8);
    if(statement != NULL) sqlite3_finalize(statement);
    if(handle != NULL) sqlite3_close(handle);
}

From the above code, we can see that the dbopen function is actually used to obtain an sqlite3 instance of the Native layer. In addition, Android also sets some platform related functions for SQLite, which will be analyzed later.

3. Introduction to sqlitecompiledsql

As mentioned earlier, the Native layer SQLite3_ The encapsulation of stmt instance is completed by the class SQLiteCompileSql that is not exposed to developers. Because of its secrecy, it is not listed in Figure 7-4. Now let's uncover its mystery. Its code is as follows:

SQLiteCompliteSql.java::SQLiteCompiledSql

SQLiteCompiledSql(SQLiteDatabase db, String sql) {
   db.verifyDbIsOpen();
   db.verifyLockOwner();
   mDatabase = db;
    mSqlStmt= sql;
    ......
    nHandle= db.mNativeHandle;
   native_compile(sql);//Call native_compile function, the code is as follows
  }

android_database_SQLiteCompliteSql.cpp::native_compile

static void native_compile(JNIEnv* env, jobjectobject, jstring sqlString)
{
   compile(env, object, GET_HANDLE(env, object), sqlString);
}
//Let's look at the implementation of compile
sqlite3_stmt * compile(JNIEnv* env, jobjectobject,
                       sqlite3 * handle,jstring sqlString)
{
    int err;
    jcharconst * sql;
    jsizesqlLen;
   sqlite3_stmt * statement = GET_STATEMENT(env, object);
 
   if(statement != NULL) ....//Release the previous sqlite3_stmt instance
    sql = env->GetStringChars(sqlString,NULL);
    sqlLen =env->GetStringLength(sqlString);
   //Call sqlite3_prepare16_v2 gets an sqlite3_stmt instance
    err =sqlite3_prepare16_v2(handle, sql, sqlLen * 2, &statement, NULL);
   env->ReleaseStringChars(sqlString, sql);
 
    if (err== SQLITE_OK) {
        //Save to SQLiteCompliteSql object of Java layer
       env->SetIntField(object, gStatementField, (int)statement);
       return statement;
    } ......
}

After the compile function is executed, a SQLite3 bound with an SQL statement is generated_ The stmt instance is bound to the SQLiteCompileSql object of the Java layer.

4. Introduction to Android SQLite custom functions

This section will introduce some functions customized by Android on SQLite. Everything has to start with SQL triggers.

(1) Trigger introduction
Trigger is a common term in database development technology. Its essence is very simple, that is, some operations that the database needs to perform when specific things happen on the specified table. It's still a little vague? Let's look at a trigger set by MediaProvider:

db.execSQL("CREATE TRIGGER IF NOT EXISTSimages_cleanup DELETE ON images " +
            "BEGIN " +
            "DELETE FROM thumbnails WHERE image_id = old._id;" +
            "SELECT _DELETE_FILE(old._data);" +
            "END");

What does the above SQL statement mean?

  • CREATE TRIGGER IF NOT EXITSimages_cleanup: if there is no definition, it is called images_ A trigger named images is created for cleanup_ Trigger for cleanup.
  • DELETE ON images: sets the trigger condition of the trigger. Obviously, this trigger will be triggered when we perform a delete operation on the images table.

The actions to be executed by the trigger are defined between BEGIN and END. As you can see from the previous code, it will perform two operations:

  • Delete the corresponding information in the thumbnails table. Why delete thumbnails? Because the information of the original image no longer exists, it's useless to keep it.
  • Execute_ DELETE_FILE function, whose parameter is old data. In terms of name, the function of this function should be to delete files. Why delete this file? The reason is also very simple. There is no such information in the database. Why do you still keep pictures! In addition, if the files are not deleted, they will be found again in the next media scan.

Hint_ DELETE_FILE operation has brought great trouble to the author and colleagues, because I didn't know this trigger at first. As a result, all the test files that were hard to download were deleted. In addition, due to the design defect of MediaProvider itself, the database information will be deleted by mistake when frequently hanging / unloading SD cards (this defect can only be avoided as far as possible and cannot be completely eradicated). As a result, the entity file will also be deleted.

One might be surprised by this_ DELETE_ Who set the file function? The answer is in the register mentioned earlier_ android_ Functions.

(2) Introduction to register_android_functions
register_android_functions is called in dbopen, and its code is as follows:

sqlite3_android.cpp::register_android_functions

//When dbopen calls it, the second parameter is set to 0
extern "C" intregister_android_functions(sqlite3 * handle, int utf16Storage)
{
    int err;
   UErrorCode status = U_ZERO_ERROR;
   UCollator * collator = ucol_open(NULL, &status);
    ......
    if(utf16Storage) {
       err =sqlite3_exec(handle, "PRAGMA encoding = 'UTF-16'", 0, 0, 0);
      ......
    } else {
        //sqlite3_create_collation_xx defines a text comparison function for sorting. Readers can read it by themselves
      //SQLite official documentation for more detailed instructions
       err = sqlite3_create_collation_v2(handle,"UNICODE",
                SQLITE_UTF8, collator, collate8,
               (void(*)(void*))localized_collator_dtor);
    }
 
   /*
     Call sqlite3_create_function creates a function called "PHONE_NUMBERS_EQUAL",
     The third parameter 2 indicates that the function has two parameters, SQLITE_UTF8 indicates that the string is encoded as UTF8,
     phone_numbers_equal It is the function pointer corresponding to the function, that is, the function that will be executed. be careful
     "PHONE_NUMBERS_EQUAL"Is the function name used in the SQL statement, phone_numbers_equal is Native
     Function corresponding to layer
  */
    err =sqlite3_create_function(
       handle, "PHONE_NUMBERS_EQUAL", 2,
       SQLITE_UTF8, NULL, phone_numbers_equal, NULL, NULL);
    ......
    //Register_ DELETE_ The function corresponding to file is delete_file
    err =sqlite3_create_function(handle, "_DELETE_FILE", 1, SQLITE_UTF8,
                                 NULL, delete_file, NULL, NULL);
    if (err!= SQLITE_OK) {
       return err;
    }
 
#if ENABLE_ANDROID_LOG
    err =sqlite3_create_function(handle, "_LOG", 1, SQLITE_UTF8,
                                    NULL, android_log,NULL, NULL);
    ......
#endif
   ......//Some functions related to PHONE
    return SQLITE_OK;
}

register_android_functions registers some functions customized on the Android platform. Come and see_ delete_file related delete_file function, whose code is:

Sqlite3_android.cpp::delete_file

static void delete_file(sqlite3_context * context,int argc,
                                          sqlite3_value** argv)
{
    if (argc!= 1) {
       sqlite3_result_int(context, 0);
       return;
    }
    //Take the first parameter from argv, which is the trigger call_ DELETE_ Passed on file
    charconst * path = (char const *)sqlite3_value_text(argv[0]);
    ......
   /*
     Android4.0 After that, the system supports multiple storage spaces (many tablets have a large internal storage space).
     For compatibility, the environment variable EXTERNAL_STORAGE also points to the sd card's Mount directory, while other storage devices are not
    The mount directory is controlled by SECCONDARY_STORAGE indicates that each mount directory is separated by a colon.
    The following code is used to judge_ DELETE_ Is the file path passed by the file function correct
   */
    boolgood_path = false;
    charconst * external_storage = getenv("EXTERNAL_STORAGE");
    if(external_storage && strncmp(external_storage,
                                     path,strlen(external_storage)) == 0) {
       good_path = true;
    } else {
        charconst * secondary_paths = getenv("SECONDARY_STORAGE");
       while (secondary_paths && secondary_paths[0]) {
           const char* colon = strchr(secondary_paths, ':');
           int length = (colon ? colon - secondary_paths :
                                              strlen(secondary_paths));
           if (strncmp(secondary_paths, path, length) == 0) {
               good_path = true;
           }
           secondary_paths += length;
            while (*secondary_paths == ':')secondary_paths++;
        }
    }
 
    if(!good_path) {
       sqlite3_result_null(context);
       return;
    }
    //Call unlink to delete the file
    int err= unlink(path);
    if (err!= -1) {
       sqlite3_result_int(context, 1);//Set return value
    } else {
      sqlite3_result_int(context, 0);
    }
}

7.3. 3 Analysis and summary of sqlitedatabase creating database

This section introduces SQLite and SQLiteDatabase of Java layer with the database created by MediaProvider as the entry. Among them, you should focus on the sample code in SQLiteTest, through which you can master the usage of SQLite API. On this basis, the SQLiteDatabase family is also introduced, and the related codes of database creation in MediaProvider are analyzed.

The knowledge points involved in this section are not complex. Readers may wish to review 7.3 1 to deepen the understanding of SQLite and SQLiteDatabase.

It is recommended to start with SQLiteDatabase, which will cost a lot of learning. Therefore, you can try to encapsulate a lightweight C++SQLite library by yourself. Through this attempt, you can learn and master how to design a good framework.

7.4 implementation analysis of cursor query

This section will analyze another complex knowledge point in the ContentProvider, namely the implementation of query. Starting from the query function of ContentResolver, the code is as follows:

ContentResolver.java::query

/*
   Pay attention to the parameters in query. The SQL statements obtained after their combination are as follows (the statements in square brackets are the comments added by the author)
   SELECT projection Specified column name [use '*' if projection is null]
   FROM Table name [set by target CP according to uri parameter] WHERE
   (selection)[If there are wildcards in the selection, the specific parameters are specified by selectionArgs]
    ORDERYEDBY sortOrder
*/
public final Cursor query(Uri uri, String[]projection,
           String selection, String[] selectionArgs, String sortOrder) {
   /*
    According to the previous analysis, the real type of the provider returned by the following function is ContentProviderProxy,
     The real type of the corresponding Bn end object is the internal class Transport of ContentProvider. The query is executed this time
    The target CP is MediaProvider
   */
  IContentProvider provider = acquireProvider(uri);
  //Look at the code below
   try {
           long startTime = SystemClock.uptimeMillis();
           //① Call the query function of the remote process
           Cursor qCursor = provider.query(uri, projection,
                              selection, selectionArgs, sortOrder);
           if (qCursor == null) {
               //If the returned result is null, release the provider. This function is used in 7.2.1 Described in Section 3
               releaseProvider(provider);
               return null;
           }
           //② Calculate the number of data items contained in the query result, and save the result in the internal variable of qCursor
           qCursor.getCount();
           long durationMillis = SystemClock.uptimeMillis() - startTime;
          //③ Finally, a cursor object is returned to the client, and its real data type is CursorWrapperInner
           return new CursorWrapperInner(qCursor, provider);
 }

The above code reveals the workflow of the ContentResolver query function:

  • Call the query function of the remote CP to return an object qCursor of type Cursor.
  • The function finally returns an object of type CursorWrapperInner to the client.

Two new data types, Cursor and CursorWrappperInner, seriously interfere with our thinking. Regardless of them, let's first analyze several key point functions listed in the above code. The first key function to be analyzed is query.

7.4. 1 extract query key points

1. Key points of client query

According to the previous analysis habit, when a Binder call is encountered, it will be immediately transferred to the service side (i.e. Bn side) for analysis, but this idea does not work in the query function. Why? Look at the query function at IContentProvider Bp side, which is defined in ContentProviderProxy. The code is as follows:

ContentProviderNative.java::ContentProviderProxy.query

public Cursor query(Uri url, String[] projection,String selection,
           String[] selectionArgs, String sortOrder) throws RemoteException {
  //① Construct a BulkCursorToCursorAdaptor object
 BulkCursorToCursorAdaptor adaptor = new BulkCursorToCursorAdaptor();
  Parceldata = Parcel.obtain();
  Parcel reply = Parcel.obtain();
  try {
      data.writeInterfaceToken(IContentProvider.descriptor);
      ......//Package the parameters into the data request package
      //②adaptor.getObserver() returns an object of IContentObserver type and returns it
     //It is also packaged into the data request package. The knowledge related to ContentObserver will be reserved for Chapter 8 for further analysis
      data.writeStrongBinder(adaptor.getObserver().asBinder());
       //Send a request to the remote Bn end
      mRemote.transact(IContentProvider.QUERY_TRANSACTION, data, reply, 0);
       DatabaseUtils.readExceptionFromParcel(reply);
      //③ Get an object of type IBulkCursor from the reply package
       IBulkCursor bulkCursor =
                     BulkCursorNative.asInterface(reply.readStrongBinder());
       if(bulkCursor != null) {
           int rowCount = reply.readInt();
           int idColumnPosition = reply.readInt();
            boolean wantsAllOnMoveCalls = reply.readInt() != 0;
            //④ Call the initialize function of the adaptor
            adaptor.initialize(bulkCursor, rowCount,
                          idColumnPosition, wantsAllOnMoveCalls);
           } ......
           return adaptor;
        } ......
       finally {
           data.recycle();
           reply.recycle();
        }
    }

From the above code, we can see that there are also many articles in the ContentProviderProxy query function, in which a total of four key points are listed. The most troublesome is the two new classes BulkCursorToCursorAdaptor and IBulkCursor. There is no rush to analyze them here.

We go to the server to extract the key points of the query function.

2. Server query key points

According to the introduction of Binder in Chapter 2, the request from the client is processed in onTransact on the Bn side. The code is as follows:

ContentProviderNative.java::onTransact

public boolean onTransact(int code, Parcel data, Parcelreply, int flags)
                                throws RemoteException {
  try {
       switch (code) {
        case QUERY_TRANSACTION://Processing query requests
         {
         data.enforceInterface(IContentProvider.descriptor);
         Uri url = Uri.CREATOR.createFromParcel(data);
         ......//Extract parameters from request package
          //⑤ Create the Bp side of ContentObserver Binder communication
         IContentObserver observer = IContentObserver.Stub.asInterface(
                           data.readStrongBinder());
          //⑥ Call the query function implemented by MediaProvider. If Cursor is an interface class, then the variable
        //What is the real type of cursor?
         Cursor cursor = query(url, projection, selection, selectionArgs,
                                      sortOrder);
          if(cursor != null) {
           //⑦ Create an object of type CursorToBulkCursorAdaptor
               CursorToBulkCursorAdaptor adaptor = new
                         CursorToBulkCursorAdaptor(
                                cursor,observer, getProviderName());
                final IBinder binder = adaptor.asBinder();
                //⑧ This function returns the number of record items contained in the result set. It looks very insignificant, but it is very critical
                final int count = adaptor.count();
                //Returns the index position of the column named "_id" in the result set, which is automatically added by the database when creating the table
                final int index = BulkCursorToCursorAdaptor.findRowIdColumnIndex(
                              adaptor.getColumnNames());
                //wantsAllOnMoveCalls is generally false. Readers can analyze it after reading this chapter
                final boolean wantsAllOnMoveCalls =
                                    adaptor.getWantsAllOnMoveCalls();
                 reply.writeNoException();
                //Write the information of the binder to the reply package
                reply.writeStrongBinder(binder);
                reply.writeInt(count);//Returns the number of record item rows contained in the result set to the client
                reply.writeInt(index);
                reply.writeInt(wantsAllOnMoveCalls ? 1 : 0);
           }......
        return true;
       }......//Handling of other situations
     ......
}

Corresponding to the client, the query processing on the server is also complex, and the obstacles are still several new data types.

Before clearing these obstacles, you should first summarize the key points of query calls on the client and server.

3. Extract summary of query key points

We extract the call key points at both ends of query, as shown in Figure 7-5.

Figure 7-5 key points of query call

Let's summarize the obstacles mentioned earlier, which are:

  • BulkCursorToCursorAdaptor created by the client and IBulkCursor returned after query from the server.
  • The CursorToCursorAdaptor created by the server and the cursorreturned from the subclass query function.

In terms of name, these classes are related to Cursor, so it is necessary to find out what the Cursor returned by MediaProviderquery is.

Note that figure 7-5 borrows the UML sequence diagram to show the query call sequence, in which the ContentProvider box and the mediaprovider box represent the same object. In addition, the calling function number in Figure 7-5 does not exactly correspond to the key function call number in the code.

7.4. 2 query analysis of mediaprovider

This section analyzes the MediaProvider query function. The focus of this analysis is not the business logic of the MediaProvider itself, but to find out what the Cursor returned by the query function is. The code is as follows:

MediaProvider.java::query

public Cursor query(Uri uri, String[]projectionIn, String selection,
           String[] selectionArgs, String sort) {
   int table= URI_MATCHER.match(uri);
   ......
  //Take out the corresponding DatabaseHelper object according to the uri, and the MediaProvider aims at the media files and in the internal storage
  //Two databases are created for media files in external storage (i.e. SD card)
 DatabaseHelper database = getDatabaseForUri(uri);
  //We're on 7.3 Section 2 analyzes the brother getWritableDatabase function of getReadableDatabase function
 SQLiteDatabase db = database.getReadableDatabase();
  //Create a SQLiteQueryBuilder object to facilitate developers to write SQL statements
 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   ......//Set the parameters of qb, for example, call the setTables function to set the target Table for this query
  //① Call the query function of SQLiteQueryBuilder
  Cursor c =qb.query(db, projectionIn, selection,
               combine(prependArgs, selectionArgs), groupBy, null, sort, limit);

  if (c !=null) {
    //2 call the setNotificationUri function of the Cursor object, which is similar to ContentObserver
   //Relevant contents will be left to Chapter 8 for further analysis
    c.setNotificationUri(getContext().getContentResolver(), uri);
 }
  return c;
}

The two key points listed in the above code are:

  • Call the query function of SQLiteQueryBuilder to get an object of Cursor type.
  • Call the setNotificationUri function of the Cursor type object. By name, it sets the notification URI for the object. The contents related to ContentObserver will be analyzed in Chapter 8.

Look at the query function of SQLiteQueryBuilder.

1. query function analysis of sqlitequerybuilder

SQLiteQueryBuilder.java::query

public Cursor query(SQLiteDatabase db, String[]projectionIn,
           String selection, String[] selectionArgs, String groupBy,
           String having, String sortOrder, String limit) {
  ......
  //Call the buildQuery function to get the string of the corresponding SQL statement
  String sql= buildQuery(
               projectionIn, selection, groupBy, having,
               sortOrder, limit);
   /*
    Call the rawQueryWithFactory function of SQLiteDatbase. mFactory is SQLiteQueryBuilder
    The initial value of the member variable is null, and it is not set in this example, so mFactory is null
   */
   returndb.rawQueryWithFactory(
               mFactory, sql, selectionArgs,
               SQLiteDatabase.findEditTable(mTables));
  }

SQLiteDatabase.java::rawQueryWithFactory

public Cursor rawQueryWithFactory(
    CursorFactory cursorFactory, String sql, String[] selectionArgs,
     StringeditTable) {
  verifyDbIsOpen();
  BlockGuard.getThreadPolicy().onReadFromDisk();
 
   //The concept of connection pool is often encountered in database development, and its purpose is to cache heavy resources. Interested readers may wish to make their own decisions
   //Take a look at the getDbConnection function
  SQLiteDatabase db = getDbConnection(sql);
   //Create a SQLiteDirectCursorDriver object
  SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(
                                     db, sql,editTable);
   Cursorcursor = null;
   try {
         //Call the query function of SQLiteCursorDriver
         cursor = driver.query(
                   cursorFactory != null ? cursorFactory : mFactory,
                   selectionArgs);
        }finally {
           releaseDbConnection(db);
   }
  return cursor;

A new type, SQLiteCursorDriver, appears in the above code. The cursor variable is the return value of its query function.

(1) SQLiteCursorDriver query function analysis
The code of SQLiteCursorDriverquery function is as follows:

SQLiteCursorDriver.java::query

public Cursor query(CursorFactory factory,String[] selectionArgs) {
   //In this case, factory is empty
  SQLiteQuery query = null;
   try {
       mDatabase.lock(mSql);
       mDatabase.closePendingStatements();
        //① Construct a SQLiteQuery object
       query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs);
       //Originally, the real type of the last returned cursor object is SQLiteCursor
       if(factory == null) {//② Construct a SQLiteCursor object
            mCursor = new SQLiteCursor(this, mEditTable, query);
        }else {
             mCursor =factory.newCursor(mDatabase, this,
                                mEditTable, query);
        }
 
       mQuery = query;
       query = null;
       return mCursor;//The actual type of the cursor object returned is SQLiteCursor
        }finally {
         if (query != null) query.close();
        mDatabase.unlock();
      }
 }

The main function of the query function of SQLiteCursor driver is to create a SQLiteQuery instance and a SQLiteCursor instance. So far, we have finally figured out that the real type of the cursor object returned by MediaProvider's query is SQLiteCursor.

Note that the MediaProvider query here refers to the key call numbered 8 in Figure 7-5.

Let's see why SQLiteQuery and SQLiteCursor are sacred.

(2) SQLiteQuery introduction
SQLiteQuery is introduced in Figure 7-4. It stores information related to query (i.e. SELECT command of SQL). The constructor code is as follows:

SQLiteQuery.java:: constructor

SQLiteQuery(SQLiteDatabase db, String query, intoffsetIndex,
             String[] bindArgs) {
   //Note that in this example, offsetIndex is 0, and the meaning of offsetIndex will be explained later
   super(db,query);//Call base class constructor
  mOffsetIndex = offsetIndex;
  bindAllArgsAsStrings(bindArgs);
  }

The base class of SQLiteQuery is SQLiteProgram, and its code is as follows:

SQLiteProgram.java:: constructor

SQLiteProgram(SQLiteDatabase db, String sql) {
    this(db,sql, null, true);//Call another constructor, paying attention to the parameters passed
  }
SQLiteProgram(SQLiteDatabase db, String sql,Object[] bindArgs,
           boolean compileFlag) {
   //In this example, bindArgs is null and compileFlag is true
   mSql =sql.trim();
   //Returns the type of the SQL statement, and the query statement returns state_ Select type
   int n =DatabaseUtils.getSqlStatementType(mSql);
   switch(n) {
       caseDatabaseUtils.STATEMENT_UPDATE:
          mStatementType = n | STATEMENT_CACHEABLE;
          break;
       caseDatabaseUtils.STATEMENT_SELECT:
          /*
          mStatementType The member variable is used to indicate the type of the SQL statement. If the SQL statement
           It's state_ Select type, mStatementType will set state_ CACHEABLE
           Sign. This flag indicates that this object will be cached to avoid reconstruction when the same SELECT command is executed again
           An object
         */
          mStatementType = n | STATEMENT_CACHEABLE |
                        STATEMENT_USE_POOLED_CONN;
               break;
       ......//Handling of other situations
       default:
           mStatementType = n;
    }
  db.acquireReference();//Increase reference count
  db.addSQLiteClosable(this);
   mDatabase= db;
   nHandle =db.mNativeHandle;//This mnatehandle corresponds to an SQLite3 instance
    if(bindArgs != null) ......//Binding parameters
   //complieAndBindAllArgs will bind an SQLite3 to this object_ Stmt instance, pointer to the native layer object
   //Saved in nStatement member variable
    if(compileFlag)    compileAndbindAllArgs();
 }

Look at the compileAndbindAllArgs function. Its code is:

SQLiteProgram.java::compileAndbindAllArgs

void compileAndbindAllArgs() {
   ......
      //If the object is not connected to SQLite3 of the native layer_ If the stmt instance is bound, the compileSql function is called
        if(nStatement == 0)  compileSql();
       ......//Binding parameters
        for(int index : mBindArgs.keySet()) {
           Object value = mBindArgs.get(index);
           if (value == null) {
               native_bind_null(index);
           }......//Bind other types of data
       }
 }

The compileSql function binds the SQLiteQuery object of the Java layer to a Native sqlite3_stmt instance. According to the previous analysis, this binding is implemented through the SQLiteCompileSql object, and the relevant codes are as follows:

SQLiteProgram.java::compileSql

private void compileSql() {
   //If mStatementType is not set to state_ Cacheable flag, one is created each time
   // SQLiteCompiledSql object. According to 7.3 According to the analysis in Section 2, subtitle 3, the object will be truly connected with the native layer
  //sqlite_stmt instance binding
   if((mStatementType & STATEMENT_CACHEABLE) == 0) {
         mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql);
         nStatement = mCompiledSql.nStatement;
         return;
   }
   //Query whether SQLiteCompiledSql that meets the requirements of the SQL statement has been cached from the SQLiteDatabase object
   //object
  mCompiledSql = mDatabase.getCompiledStatementForSql(mSql);
   if(mCompiledSql == null) {
       //Create a new SQLiteCompiledSql object and save it to mDatabase
      mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql);
      mCompiledSql.acquire();
      mDatabase.addToCompiledQueries(mSql, mCompiledSql);
   } ......
   //Save Native layer sqlite3_stmt instance pointer to nStatement member variable
   nStatement= mCompiledSql.nStatement;
}

Through the above analysis, it can be found that SQLiteQuery will be combined with a SQLite3 representing the SELECT command_ Stmt instance binding. At the same time, in order to reduce the creation of SQLite3_ The SQLiteDatabase framework also caches the corresponding SQL statement and the corresponding SQLiteCompiledSql object. If the same SELECT statement is executed next time, the system will directly fetch the SQLiteCompiledSql object saved before, so that SQLite3 does not need to be recreated_ Stmt instance.

Consider that the SQLiteDatabase framework obviously considers more issues than using the SQLite API directly. Do you think of these problems when you encapsulate the C++ SQLite library?

(3) SQLiteCursor analysis
Let's look at SQLiteCursor class. The constructor code is as follows:

SQLiteCursor.java:: constructor

public SQLiteCursor(SQLiteCursorDriver driver,String editTable,
                      SQLiteQuery query) {
   ......
   mDriver =driver;
  mEditTable = editTable;
  mColumnNameMap = null;
   mQuery =query;//Save this SQLiteQuery object
 
  query.mDatabase.lock(query.mSql);
   try {
       //Get the number of columns contained in the result set obtained by this query execution
       intcolumnCount = mQuery.columnCountLocked();
      mColumns = new String[columnCount];
        for(int i = 0; i < columnCount; i++) {
            String columnName = mQuery.columnNameLocked(i);
           //Save column name
            mColumns[i] = columnName;
            if ("_id".equals(columnName)) {
                   mRowIdColumnIndex = i;//Save the index position of the "_id" column in the result set
               }
            }
        }finally {
           query.mDatabase.unlock();
        }
}

SQLiteCursor is relatively simple and will not be detailed here.

2. Cursor analysis

So far, we have known the real type of the Cursor object returned by MediaProvider query. Now, we can finally invite the Cursor family to the stage, as shown in Figure 7-6.

Figure 7-6 Cursor family

Figure 7-6 contains many elements and complex knowledge points. Therefore, the following explanations must be read carefully.

Pass 7.3.1 According to the sqlitest example in Section 1, The results of query (i.e. SELECT command) are bound with a Native sqlite_stmt instance. Developers can get their desired results by traversing the sqlite_stmt instance (for example, call sqlite3_step to traverse the rows in the result set, and then get the value of the specified column through sqlite3_column_xxx). The query result set can be used through a special term in database development technology - Cursor (Cursor) to traverse and obtain. In the upper left part of figure 7-6, there are classes related to Cursor, including interface class Cursor and CrossProcessCursor, abstract class AbstractCursor, AbstractWindowCursor, and real implementation class sqlitecursor. According to the previous analysis, sqlitecursor stores a SQLiteQuery object bound with sqlite3_stmt instance, so the reader can simplify Simply regard sqlitecursor as a Cursor object that already contains the query result set, although the SQL statement has not been actually executed at this time.

As mentioned above, SQLiteCursor is a cursor object that already contains the result set. From the perspective of process, the result set of query still belongs to the process where MediaProvider is located, and this query request is initiated by the client, so there must be a way to pass the result set in MediaProvider to the client process. The technology used for data transmission is very simple, which is the familiar shared memory technology. The SQLite API does not provide relevant functions, but the SQLiteDatabase framework encapsulates cross process data transfer, and finally obtains the CursorWindow class in the upper left part of figure 7-6. The comments in the code clearly indicate the role of CursorWindow: it is "A buffer containing multiplecursor rows".

Knowing CursorWindow, I believe readers can guess the general process of data transmission in query.

MediaProvider stores the data in the result set into the shared memory of CursorWindow, and then the client can take it out of the shared memory.

The above process description is correct, but the actual process is not so simple, because SQLiteDatabase framework wants the client to see not shared memory, but a cursor object representing the result set, just as the client queries the database in the process. Due to this requirement [②], Android constructs the class family in the lower right corner of figure 7-6.

The two most important classes are CursorToBulkCursorAdaptor and BulkCursorToCursorAdaptor. From the name, they adopt the adapter pattern in the design pattern; From the perspective of inheritance relationship, these two classes will participate in cross process Binder communication (in which the BulkCursorToCursorAdaptor used by the client communicates with the CursorToBulkCursorAdaptor in the process where the MediaProvider is located through mBulkCursor). The most important of these two classes is the onMove function, which will be analyzed later.

In addition, the upper right corner of figure 7-6 shows the derivation of the CursorWrapperInner class. The CursorWrapperInner class is the type of cursor object that the ContentResolver query function finally returns to the client. The purpose of CursorWrapperInner should be to expand the function of CursorToBulkCursorAdaptor class.

The Cursor family is a little complicated. The author thinks that the current architecture design of Cursor is over designed, which will not only lead to many difficulties in our analysis, but also cause some loss to the running efficiency of the actual code.

Let's focus on cross process data transmission.

7.4. 3. Query key point analysis

This section will analyze the key points in the query function in the following order:

  • Firstly, the CursorToBulkCursorAdaptor and its count function on the server are introduced.
  • CursorWindow, a key class for sharing data across processes.
  • The BulkCursorToCursorAdaptor of the client and its initialize function, as well as the CursorWrapperInner class returned to the client

1. CursorToBulkCursorAdaptor function analysis

(1) Constructor analysis
The code of CursorToBulkCursorAdaptor constructor is as follows:

CursorToBulkCursorAdaptor.java:: constructor

public CursorToBulkCursorAdaptor(Cursor cursor,IContentObserver observer,
           String providerName) {
   //The real type of the cursor variable passed in is SQLiteCursor, which is CrossProcessCursor
   if(cursor instanceof CrossProcessCursor) {
       mCursor = (CrossProcessCursor)cursor;
   } else {
       mCursor = new CrossProcessCursorWrapper(cursor);
   }
  mProviderName = providerName;
 synchronized (mLock) {//It is related to ContentObserver. We will analyze it later
    createAndRegisterObserverProxyLocked(observer);
   }
 }

The constructor of CursorToBulkCursorAdaptor is very simple and will not be detailed here. Let's look at the next important function, CursorToBulkCursorAdaptor count. This function returns the number of rows contained in the query result set.

(2) count function analysis
The code of count function is as follows:

CursorToBulkCursorAdaptor.java::count

public int count() {
  synchronized (mLock) {
     throwIfCursorIsClosed();//If the mCursor is closed, throw an exception
    //The true type of the mCursor variable of CursorToBulkCursorAdaptor is SQLiteCursor
     return mCursor.getCount();
     }
 }

count will eventually call the getCount function of SQLiteCursor with the following code:

SQLiteCursor.java::getCount

public int getCount() {
  if (mCount== NO_COUNT) {//NO_COUNT is - 1, and the if condition is met when calling for the first time
       fillWindow(0);//Key function
  }
  return mCount;
 }

The getCount function calls a very important function, fillWindow. As the name suggests, readers can guess its function: saving the result data to the shared memory of CursorWindow.

The following is a separate section to analyze the knowledge points related to CursorWindow.

2. CursorWindow analysis

CursorWindow is created from the call to fillWindow in the previous code. The code of fillWindow is as follows:

SQLiteCurosr.java::fillWindow

private void fillWindow(int startPos) {
   //① If CursorWinodow already exists, clear it; otherwise, create a new one
   //CursorWinodow object
  clearOrCreateLocalWindow(getDatabase().getPath());
  mWindow.setStartPosition(startPos);
   //② getQuery returns a SQLiteQuery object whose fillWindow function will be called here
   int count= getQuery().fillWindow(mWindow);
   if (startPos == 0) {
        mCount= count;
   } ......
 }

Let's first look at the clearorcreatelocal window function.

(1) Analysis of clearorcreatelocal window function

SQLiteCursor.java::clearOrCreateLocalWindow

protected void clearOrCreateLocalWindow(Stringname) {
   if(mWindow == null) {
      mWindow = new CursorWindow(name, true);//Create a CursorWindow object
    }else  mWindow.clear();//Clear the information in CursorWindow
 }

The code of CursorWindow constructor is as follows:

CursorWindow.java::CursorWindow

public CursorWindow(String name, booleanlocalWindow) {
  mStartPos= 0;//The starting row position of this query, such as the results of rows 10 to 100 in the database table,
                //The starting line is 10
  /*
   Call the nativeCreate function and pay attention to the parameters passed, where sCursorWindowSize is 2MB and localWindow is 2MB
   Is true. sCursorWindowSize is a static variable whose value is taken from frameworks/base/core/res/res
  /values/config.xml Config defined in_ Cursorwindowsize variable, the value is 2048KB, and
  sCursorWindow On this basis, it is expanded 1024 times, and the final result is 2MB
  */
  mWindowPtr= nativeCreate(name, sCursorWindowSize, localWindow);
 mCloseGuard.open("close");
 recordNewWindow(Binder.getCallingPid(), mWindowPtr);
}

nativeCreate is a native function, which is really implemented in Android_ database_ CursorWindow. In CPP, the code is as follows:

android_database_CursorWindow.cpp::nativeCreate

static jint nativeCreate(JNIEnv* env, jclassclazz,
       jstring nameObj, jint cursorWindowSize, jboolean localOnly) {
   String8name;
   if(nameObj) {
       const char* nameStr = env->GetStringUTFChars(nameObj, NULL);
       name.setTo(nameStr);
       env->ReleaseStringUTFChars(nameObj, nameStr);
    }
   ......
   CursorWindow* window;
    //Create a CursorWindow object of the Native layer
    status_tstatus = CursorWindow::create(name, cursorWindowSize,
                                          localOnly,&window);
   ......
    return reinterpret_cast<jint>(window);//Convert pointer to jint type
}

Take another look at the create function of Native CursorWindow. Its code is as follows:

CursorWindow.cpp::create

status_t CursorWindow::create(const String8&name, size_t size, bool localOnly,
       CursorWindow** outCursorWindow) {
    String8ashmemName("CursorWindow: ");
   ashmemName.append(name);
   ashmemName.append(localOnly ? " (local)" : "(remote)");
 
    status_tresult;
    //Create shared memory and call ashmem provided by Android platform_ create_ Region function
    intashmemFd = ashmem_create_region(ashmemName.string(), size);
    if(ashmemFd < 0) {
       result = -errno;
    } else {
       result = ashmem_set_prot_region(ashmemFd, PROT_READ | PROT_WRITE);
        if(result >= 0) {
           //Map the shared memory to get an address, and the data variable points to the starting position of the address
           void* data = ::mmap(NULL, size, PROT_READ | PROT_WRITE,
                                       MAP_SHARED,ashmemFd, 0);
            ......
            result= ashmem_set_prot_region(ashmemFd, PROT_READ);
            if (result >= 0) {
                   //Create a CursorWindow object
                   CursorWindow* window = new CursorWindow(name, ashmemFd,
                                               data, size, false);
                   result = window->clear();
                    if (!result) {
                       *outCursorWindow = window;
                        return OK;//Created successfully
                   }
           }......//Error handling
    }
    return result;
}

As can be seen from the above code, the create function of CursorWindow will construct a Native CursorWindow object. Finally, the CursorWindow object of the Java layer will be bound to the Native CursorWindow object.

The creation of CursorWindow involves the knowledge of shared memory. Readers can query or read 7.2 of Volume I online Section 2.

So far, the shared memory for carrying data has been created, but we have not executed the SQL SELECT statement. This is done by SQLiteQuery's fillWindow function.

(2) SQLiteQuery fillWindow analysis
As mentioned earlier, SQLiteQuery saves SQLite3 of a Native layer_ Stmt instance, does its fillWindow function fill the result information into the CursorWindow after executing the SQL statement? It can be verified by the following code.

SQLiteQuery.java::fillWindow

int fillWindow(CursorWindow window) {
 mDatabase.lock(mSql);
  longtimeStart = SystemClock.uptimeMillis();
  try {
       acquireReference();//Increase reference count once
        try {
             window.acquireReference();
             /*
                Call the nativeFillWindow function to complete the function. Where nHandle points to the of the Native layer
                sqlite3 Instance, nStatement points to SQLite3 of the Native layer_ Stmt instance,
                window.mWindowPtr Point to the CursorWindow instance of the Native layer,
                 This function finally returns the number of record items in the result set obtained after the execution of this SQL statement.
                mOffsetIndex The parameters are explained below
             */
              int numRows = nativeFillWindow(nHandle,
                             nStatement,window.mWindowPtr,
                             window.getStartPosition(), mOffsetIndex);
               mDatabase.logTimeStat(mSql, timeStart);
               return numRows;
           }...... finally {
               window.releaseReference();
           }
        }finally {
           releaseReference();
           mDatabase.unlock();
        }
    }

The OFFSET index is related to the OFFSET parameter of the SQL statement, which can be recognized through an SQL statement.

SELECT * FROM IMAGES LIMIT 10 OFFSET 1
//The above SQL statement means to query 10 records from the IMAGES table. The starting position of the 10 records starts from the first one.
//That is to query records 1 to 11

Let's look at the implementation function of nativeFillWindow. Its code is:

android_database_SQLiteQuery.cpp::nativeFillWindow

static jint nativeFillWindow(JNIEnv* env, jclassclazz, jint databasePtr,
        jintstatementPtr, jint windowPtr, jint startPos, jint offsetParam) {
    //Take out the instance of the Native layer
    sqlite3*database = reinterpret_cast<sqlite3*>(databasePtr);
   sqlite3_stmt* statement =reinterpret_cast<sqlite3_stmt*>(statementPtr);
   CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
 
    if(offsetParam > 0) {
       //If the OFFSET of the query is set, the starting row needs to be bound., According to the following settings, readers can
      //Infer SQL statements that do not bind specific values? The answer is:
      //SELECT* FROM TABLE OFFSET, where offsetParam indicates the number of wildcards,
     //startPos is used to bind to this wildcard
        interr = sqlite3_bind_int(statement, offsetParam, startPos);
    }  ......
 
   //Calculate the number of columns of the result set returned by this query
   int numColumns =sqlite3_column_count(statement);
    //Save the results of SQL execution to the CursorWindow object
   status_tstatus = window->setNumColumns(numColumns);
   ......

    intretryCount = 0;
    inttotalRows = 0;
    intaddedRows = 0;
    boolwindowFull = false;
    boolgotException = false;
   //Traverse all results
    constbool countAllRows = (startPos == 0);
   //Note the following loop, which will traverse the result set of SQL and save the data to the CursorWindow object
    while(!gotException && (!windowFull || countAllRows)) {
        interr = sqlite3_step(statement);
        if(err == SQLITE_ROW) {
          retryCount = 0;
           totalRows += 1;
          //The windowFull variable is used to indicate whether CursorWindow has enough memory. From the previous introduction,
          //A CursorWindow allocates only 2MB of shared memory space
          if (startPos >= totalRows || windowFull) {
               continue;
           }
           //Allocate a row of space in shared memory to store this row of data
           status = window->allocRow();
           if (status) {
              windowFull = true;// CursorWindow has no space
               continue;
           }
           for (int i = 0; i < numColumns; i++) {
                //Gets the value of each column in this row of record items
               int type = sqlite3_column_type(statement, i);
               if (type == SQLITE_TEXT) {
                     //If a string is stored in this column, it is taken out and passed through CursorWindow
                    //The putString function is saved to shared memory
                     const char* text =reinterpret_cast<const char*>(
                           sqlite3_column_text(statement, i));
                     size_t sizeIncludingNull =sqlite3_column_bytes(statement, i)
                                                    + 1;
                   status = window->putString(addedRows, i, text,
                                                    sizeIncludingNull);
                   if (status) {
                        windowFull = true;
                        break;//CursorWindow does not have enough space
                   }
               } ......Processing other data types
           }
           if (windowFull || gotException) {
               window->freeLastRow();
           } else {
               addedRows += 1;
           }
        }else if (err == SQLITE_DONE) {
            ......//All rows in the result set are traversed
           break;
        }else if (err == SQLITE_LOCKED || err == SQLITE_BUSY) {
            //If the database is being locked due to other operations, an attempt will be made here to wait for some time
           if (retryCount > 50) {//Wait up to 50 times for 1 second each time
               throw_sqlite3_exception(env,database, "retrycount exceeded");
               gotException = true;
           } else {
               usleep(1000);
               retryCount++;
           }
       }......
    }
    //Reset sqlite3_stmt instance for next use
   sqlite3_reset(statement);
    ......//Returns the number of rows in the result set
    return countAllRows ? totalRows : 0;
}

It can be confirmed from the above code that the fillWindow function fills the execution results of SQL statements into the shared memory of CursorWindow. If you are interested, you might as well study how CursorWindow saves the result information.

It is suggested that one of the things I often do in network development is to serialize some custom class instance objects into a piece of memory, and then send the contents of this memory to a remote process through socket, and the remote process will deserialize the received data to get an instance object. In this way, the remote process gets an instance object from the sender. Readers may wish to learn about serialization / deserialization by themselves.

(3) CursorWindow analysis summary
This section introduces readers to the knowledge points related to CursorWindow. In fact, CursorWindow encapsulates a piece of shared memory. In addition, we also saw how to fill the result set obtained after executing the SELECT statement into this shared memory. However, this memory only belongs to the server process. Only after the client process obtains this memory can the client really obtain the results after executing the SELECT. So, when does the client have to hit this memory? Let's go back to the client process.

3. BulkCursorToCursorAdaptor and CursorWrapperInner analysis

The work of the client is to first create BulkCursorToCursorAdaptor, and then call the initialize function of BulkCursorToCursorAdaptor according to the result of remote query.

BulkCursorToCursorAdaptor.java

public final class BulkCursorToCursorAdaptorextends AbstractWindowedCursor {
    privatestatic final String TAG = "BulkCursor";
    //The mObserverBridge is related to the content OBERVER. We will leave it to section 7.5 for further analysis
    privateSelfContentObserver mObserverBridge = new SelfContentObserver(this);
    privateIBulkCursor mBulkCursor;
    privateint mCount;
    privateString[] mColumns;
    privateboolean mWantsAllOnMoveCalls;
  //initialize function
    public void initialize(IBulkCursor bulkCursor, int count, int idIndex,
           boolean wantsAllOnMoeCalls) {
       mBulkCursor = bulkCursor;
       mColumns = null;
       mCount = count;
       mRowIdColumnIndex = idIndex;
       mWantsAllOnMoveCalls = wantsAllOnMoveCalls;//The value is false
    }
   ......
}

It can be seen from the above code that bulkcursor tocursoradaptor simply saves the information from the remote end without any special operations. It seems that the client process does not share memory during the execution of the above code. Will this work be done by CursorWrapperInner? Look at the CursorWrapperInner class of the object returned to the client by ContentResolver query. The code is also relatively simple.

ContentResolver.java::CursorWrapperInner

private final class CursorWrapperInner extendsCursorWrapper {
       private final IContentProvider mContentProvider;
       public static final String TAG="CursorWrapperInner";
       /*
        CloseGuard Class is an auxiliary class provided by Android dalvik virtual machine to help developers judge
         Whether the instance object of the class that uses it is close d. For example, suppose there is a CursorWrapperInner
          Object. When there is no place to refer to it, its finalize function will be called. If not called before
         CursorWrapperInner close function, then finalize the warnIsOpen function of CloseGuard
         A warning message will be printed: "a resource was acquired at attached stack trace but never."
         released.See java.io.Closeable for informationon avoiding resource
         leaks.".  Interested readers can study the CloseGuard class by themselves
       */
       private final CloseGuard mCloseGuard = CloseGuard.get();
       private boolean mProviderReleased;
       CursorWrapperInner(Cursor cursor, IContentProvider icp) {
           super(cursor);//Call the constructor of the base class, and the cursor variable will be saved in mCursor
           mContentProvider = icp;
           mCloseGuard.open("close");
        }
    ......
}

The constructor of CursorWrapperInner also does not get shared memory. Don't worry. Let's see the results after executing query.

Client through image The media query function will get a Cursor object of CursorWrapperInner type. Of course, the client does not know such important details. It only knows that it uses the interface class Cursor. According to the previous analysis, the client can interact with the CursorToBulkCursorAdaptor of the server through this Cursor object, that is, the Binder communication channel between processes has been opened. But at this time, the client has not got the crucial shared memory, that is, the data channel between processes has not been opened. So, when did the data channel get through?

The time when the data channel is opened is related to lazy creation, that is, it is opened only when it is used.

4. moveToFirst function analysis

According to the previous analysis, the client starts from image The real type of the cursor object obtained by the media query function is CursorWrapperInner. The use of cursor object has a feature that it must first call the function of its move family. This family includes functions such as moveToFirst and moveToLast. Why do you have to call them? To analyze the most common moveToFirst function, which is actually implemented by the base class CursorWrapper of CursorWrapperInner. The code is as follows:

CursorWrapper.java::moveToFirst

 public boolean moveToFirst() {
   //mCursor points to BulkCursorToCursorAdaptor
   return mCursor.moveToFirst();
}

The real type of mCursor member variable is BulkCursorToCursorAdaptor, but its moveToFirst function is the implementation of AbstractCursor, the ancestor of this class. The code is as follows:

AbstractCursor.java::moveToFirst

 publicfinal boolean moveToFirst() {
    return moveToPosition(0);//Call moveToPosition to see the function directly
}

//moveToPosition analysis, whose parameter position indicates which row the cursor will be moved to
public final boolean moveToPosition(int position){
   //getCount returns the number of rows in the result set. This value has been calculated and returned by the server when building the Binder communication channel
   //To the client
   final int count = getCount();
   //The mPos variable records the position of the current cursor. The initial value of the variable is - 1
   if(position >= count) {
         mPos = count;
         return false;
   }
   if(position < 0) {
        mPos = -1;
         returnfalse;
   }
    if(position == mPos)  return true;
    //onMove function is an abstract function implemented by subclasses
    booleanresult = onMove(mPos, position);
    if(result == false) mPos = -1;
     else {
           mPos = position;
           if (mRowIdColumnIndex != -1) {
               mCurrentRowID = Long.valueOf(getLong(mRowIdColumnIndex));
           }
        }
    return result;
 }

In the above code, moveToPosition will call the onMove function implemented by the subclass. In this example, the subclass is BulkCursorToCursorAdaptor. Next, look at its onMove function.

(1) Analysis of onMove function of BulkCursorToCursorAdaptor

BulkCursorToCursorAdaptor.java::onMove

public boolean onMove(int oldPosition, intnewPosition) {
 throwIfCursorIsClosed();
  try {
     //The type of mWindow is CursorWindow. When this function is called for the first time, mWindow is null
      if(mWindow == null
            ||newPosition < mWindow.getStartPosition()
            || newPosition >= mWindow.getStartPosition()+
                                mWindow.getNumRows()){
            /*
              mBulkCurosr It is used to communicate with IBulkCursor Bn on the server, and its getWindow function
              An object of type CursorWindow will be returned. That is, after calling the getWindow function,
              The client process gets a CursorWindow. From then on, the data channel between the client and the server will be closed
              Got through
            */
            setWindow(mBulkCursor.getWindow(newPosition));
        }else if (mWantsAllOnMoveCalls) {
            mBulkCursor.onMove(newPosition);
          }
        } ......
     if (mWindow== null)  return false;
     return true;
 }

The key function for establishing data channel is getWindow of IBulkCurosr. For the client, the type of IBulkCursor Bp object is BulkCursorProxy. Its getWindow function is described below.

(2) Analysis of getWindow function of BulkCursorProxy

BulkCursorNative.java::BulkCursorProxy:getWindow

public CursorWindow getWindow(int startPos) throwsRemoteException
 {
    Parceldata = Parcel.obtain();
    Parcelreply = Parcel.obtain();
    try {
         data.writeInterfaceToken(IBulkCursor.descriptor);
         data.writeInt(startPos);
         mRemote.transact(GET_CURSOR_WINDOW_TRANSACTION, data, reply, 0);
         DatabaseUtils.readExceptionFromParcel(reply);
         CursorWindow window = null;
          if(reply.readInt() == 1) {
             /*
              Construct a local CursorWindow object according to the server reply package, and readers can study it by themselves
              newFromParcel Function, which internally calls the nativeCreateFromParcel function to create
              A Native CursorWindow object. The whole process is the deserialization process mentioned earlier by the author
              */
               window = CursorWindow.newFromParcel(reply);
          }
           return window;
        } ......
 }

Let's look at the getWindow function on the Bn side of ibulkcursor. The real type of this Bn side object is CursorToBulkCursorAdaptor.

(3) Analysis of getWindow function of CursorToBulkCursorAdaptor

CursorToBulkCursorAdaptor.java::getWindow

public CursorWindow getWindow(int startPos) {
 synchronized (mLock) {
    throwIfCursorIsClosed();
     CursorWindow window;
      //mCursor is the value returned by MediaProvider query. Its real type is SQLiteCursor, which meets the following requirements
     //The following if conditions
     if(mCursor instanceof AbstractWindowedCursor) {
          AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor)mCursor;
          //For this example, SQLiteCursor has been bound to a CursorWindow, so the value of window
         //Not empty
          window = windowedCursor.getWindow();
           if (window == null) {
                 window = new CursorWindow(mProviderName, false);
                 windowedCursor.setWindow(window);
            }
              //Call the moveToPosition function of SQLiteCursor. This function has been analyzed before. The / / call of onMove function will be triggered inside it. Here will be the onMove function of SQLiteCursor
               mCursor.moveToPosition(startPos);
           } else {
              ......
           }
           if (window != null) {
               window.acquireReference();
           }
           return window;
        }
 }

The CursorWindow object returned by the server is the CursorWindow object created in the count function, which already contains the query results of this query.

In addition, before passing the CursorWindow of the server to the client, the system will call the writeToParcel function of CursorWindow for serialization. Readers can read the writeToParcel of CursorWindow and its native implementation of the nativeWriteToParcel function.

(4) Analysis of movetoposition function of SQLiteCursor
This function is implemented by the base class AbstractCursor of SQLiteCursor. We have seen its code before. Its main internal work is to call the AbstractCursor subclass (SQLiteCursor itself here) to implement the onMove function. Therefore, you can directly look at the onMove function of SQLiteCursor.

SQLiteCursor.java::onMove

public boolean onMove(int oldPosition, intnewPosition) {
   if(mWindow == null || newPosition < mWindow.getStartPosition() ||
       newPosition >= (mWindow.getStartPosition() +
                                                       mWindow.getNumRows())) {
           fillWindow(newPosition);
     }
     return true;
 }

The if judgment in the above code is very important. The specific explanation is as follows:

  • When mWindow is empty, that is, the server does not create CursorWindow (of course, in this case, CursorWindow has already been created during query), fillWindow needs to be called. Clearorcreatelocuwindow will be called inside this function. If CursorWindow does not exist, a CursorWindow object will be created. If it already exists, the information of CursorWindow object will be cleared.

  • fillWindow also needs to be called when newPosition is less than the starting position of CursorWindow obtained from the previous query, or newPosition is greater than the maximum row position of CursorWindow obtained from the previous query. Because CursorWindow already exists at this time, clearorcreatelocal window will call its clear function to clear the previously saved information.

  • When fillWindow is called, the SQL statement is executed to get the correct result set. For example, if 90 records (i.e. 10100 records) starting from line 10 are set during the last query execution, if the new query specifies to start from line 0 or 101, you need to refill fillWindow, that is, fill the new results into CursorWindow. If the number of rows of the new query is between 10100, you don't need to call fillWindow again.

This is some optimization processing done by the server for query, that is, when the CursorWindow already contains the required data, there is no need to query again. Logically, the client should make a similar judgment to avoid making unnecessary Binder requests. Let's look back at the onMove function of the client BulkCursorToCursorAdaptor.

BulkCursorToCursorAdaptor.java::onMove

public boolean onMove(int oldPosition, intnewPosition) {
 throwIfCursorIsClosed();
  try {
      //Similarly, the client does the corresponding optimization. If the if condition is not met, the client does not need to call
    //mBulkCurosr's getWindow function, so that the server will not receive the corresponding Binder request
      if(mWindow == null
            ||newPosition < mWindow.getStartPosition()
            || newPosition >=mWindow.getStartPosition() +
                                 mWindow.getNumRows()){
            setWindow(mBulkCursor.getWindow(newPosition));
    )
 ......
}

(5) moveToFirst function analysis summary
moveToFirst and related sibling functions (such as moveToLast and move, etc.) aims to move the cursor position to the specified line. Through the above code analysis, we find that its work is far more than moving the cursor position. For clients that do not have CursorWindow, moveToFirst will cause the client to deserialize the CursorWindow information from the server, so as to reduce the number between the client and the server According to the channel, it is really established.

7.4. 4. Analysis and summary of cursor query implementation

The knowledge points in this section are the most difficult except for ActivityManagerService. When we look back on this analysis journey, we may have the following perceptions:

The difficulty of AMS is reflected in its game rules; The difficulty of query is reflected in its layers of encapsulation (including the classes it creates and the derivative relationship between them). In essence, the work of query is very simple, that is, copying data to shared memory. The content of this work is not very difficult, because it has neither complex game rules nor high technical threshold.

Why does query and even SQLiteDatabase involve so many classes? This question can only be answered by the designer, but I think there must be more room for optimization.

7.5 implementation analysis of cursor close function

Although most Java programmers generally do not understand actively recycling resources (including memory) as well as C/C + + programmers, for heavy resources such as Cursor (it not only occupies a file descriptor, but also shares a 2MB of memory). In the process of programming, Java programmers must show that they call the close function to release these resources. In their daily work, the author has encountered countless times that the Monkey test fails because the Cursor is not closed.

In addition, some colleagues have asked the author that although it is not shown to call the close function, there is no place for this object to refer to it again. According to the rules of Java garbage collection mechanism, the object will be recycled after a certain time. Since the Cursor itself has been recycled, why the resources it contains (referring to the Cursor window) have not been recycled? This issue will be discussed at the end of this section.

It is suggested that if you do not pay attention to resource recovery when writing code and find the leakage point when there is a problem at last, it will greatly increase the cost of software development. Although Java provides a garbage collection mechanism, I hope readers will not lose their awareness of resource recycling.

Let's analyze the Cursor close function, starting with the client.

7.5. 1. Analysis of client close

The real type of the cursor object obtained by the client is CursorWrapperInner, and the code of its close function is as follows:

ContentResolver.java::CursorWrapperInner.close

public void close() {
  super.close();//Call the close function of the base class
   //Revoke the intimacy between the client process and the target CP process
  ContentResolver.this.releaseProvider(mContentProvider);
  mProviderReleased = true;

   if(mCloseGuard != null) mCloseGuard.close();
 }

Let's look at the close function of CursorWrapper, the base class of CursorWrapperInner. Readers should be reminded that the function analysis in the following text will frequently change from base class to subclass and from subclass to base class. The reason for this situation is the result of encapsulating classes too much.

CursorWrapper.java::close

public void close() {
  mCursor.close();  //mCursor points to BulkCursorToCursorAdaptor
 }

The code of BulkCursorToCursorAdaptorclose is as follows:

BulkCurosrToCursorAdaptor.java::close

public void close() {
 super.close();//① Call the close function of the base class again, which will release the CursorWindow resource created locally
  if(mBulkCursor != null) {
      try {
            mBulkCursor.close();//② Call the close function of the remote object
       }......
      finally {
               mBulkCursor = null;
           }
      }
 }

For the Cursor close function, the author pays more attention to how the contained CursorWindow resources are released. According to the comments in the above code, the close function of the base class called by bulkcurorsrtocursoradapter will release CursorWindow.

Next, let's look at super Close this function. This close is implemented by AbstractCursor, the parent class of AbstractWindowedCursor, the parent class of bulkcurorstocursoradaptor. Its code is as follows:

AbstractCursor.java::close

public void close() {
   mClosed =true;
  mContentObservable.unregisterAll();
  onDeactivateOrClose();//Call the onDeactivateOrClose function implemented by the subclass
 }

onDeactivateOrClose is implemented by AbstractWindowedCursor, a subclass of AbstractCursor. The code is as follows:

AbstractWindowedCursor.java::onDeactivateOrClose

protected void onDeactivateOrClose() {
    //It also calls the onDeactivateOrClose function of the base class, AbstractCursor
   super.onDeactivateOrClose();
   closeWindow();//Release CursorWindow resource
}

The calls involved in the close function appear so repeatedly in the derived tree, which is helpless -! Similar practices will be encountered later. Readers must keep calm.

AbstractWindowedCursor.java::close

protected void closeWindow() {
  if(mWindow != null) {
      mWindow.close();//Call the close function of CurosrWindow

      mWindow = null;

    }

}

CursorWindow derives from SQLiteClosable. According to the previous introduction, releasing the resources represented by SQLiteClosable will be controlled by reference count. How is this realized? Look at the code:

CursorWindow.java::close]

public void close() {
   releaseReference();//Decrease the reference count once

}

releaseReference is implemented by the base class SQLiteClosable of CursorWindow, and its code is as follows:

SQLiteClosable.java::releaseReference]

public void releaseReference() {
   boolean refCountIsZero = false;
   synchronized(this) {
       refCountIsZero = --mReferenceCount == 0;
   }
   if(refCountIsZero) {//When the reference count is reduced to 0, the resource can be really released
       onAllReferencesReleased();//Call the onAllReferencesReleased function implemented by the subclass
    }
 }

The resource release is completed by onAllReferencesReleased implemented by the subclass. For CursorWindow, the code of this function is as follows:

CursorWindow.java::onAllReferencesReleased

protected void onAllReferencesReleased() {
  dispose();//Call dispose

}
private void dispose() {
  if(mCloseGuard != null) {
      mCloseGuard.close();
   }
  if(mWindowPtr != 0) {
      recordClosingOfWindow(mWindowPtr);
       //Call the nativedisposite function, which will release the CursorWindow object of the Native layer
       //At this point, the shared memory obtained by the client is completely released
      nativeDispose(mWindowPtr);
      mWindowPtr = 0;
   }
}

So far, the close function of the client cursor object has been analyzed. I don't know what readers think after reading the whole process.

7.5. 2 Analysis of server close

The close function of the server is triggered because the client sends a Binder request through the IBulkCurosrclose function. The Bn end of IBulkCurosr is the CursorToBulkCursorAdaptor of the target CP process. The code of its close function is as follows:

CursorToBulkCursorAdaptor.java::close

public void close() {
  synchronized (mLock) {
        disposeLocked();
     }
}

CursorToBulkCursorAdaptor.java::disposeLocked

private void disposeLocked() {
   if (mCursor != null) {
        //Log off ContentObserver and leave the relevant knowledge to Chapter 8 for further analysis
        unregisterObserverProxyLocked();
       mCursor.close();//Call the close function of SQLiteCursor
       mCursor = null;
  }
   closeWindowForNonWindowedCursorLocked();
 }

The code of SQLiteCurosr's close function is as follows:

SQLiteCursor.java::close

public void close() {
  //Like the client, first call the close of AbstractCursor, and finally trigger AbstractWindowedCursor
  //Ondectivateorclose function, where the CursorWindow on the server ends
 super.close();
 synchronized (this) {
      mQuery.close();//Call the close of SQLiteQuery, and SQLite3 will be released internally_ Stmt instance
      //Call the cursorClose function of SQLiteDirectCursorDriver
      mDriver.cursorClosed();
    }
 }

At this point, the close function of the server is analyzed. The content is simple and does not need to be detailed.

Now let's answer the question raised at the beginning of this section. If the close function calling the cursor object is not displayed, will the close function be called when the object is garbage collected? Let's answer this question with code.

7.5. 3 Analysis of finalize function

The finalize function of the cursor object will be called before it is recycled. Look at the finalize function of CursorWrapperInner. The code is as follows:

ContentResolver.java::CursorWrapperInner.finalize

protected void finalize() throws Throwable {
  try {
       if(mCloseGuard != null) {
           mCloseGuard.warnIfOpen();//Print a warning
        }
        if(!mProviderReleased && mContentProvider != null) {
            ContentResolver.this.releaseProvider(mContentProvider);
        }
       //In addition to printing a warning, the above code does not call the close function
     }finally {
          super.finalize();//Call finalize of the base class. Will it have any special processing?
      }
 }

Unfortunately, we have high hopes for super The finalize function does not do any special processing. Is there no place to deal with CursorWindow resources? The answer to this question is as follows:

  • The CursorWindow resources held by the client will be recycled when the object is finalized. Readers can view the finalize function of CursorWindow.
  • As analyzed earlier, the close function of the server is triggered by bulkcurorsrtocursoradapter calling IBulkCursor close function. However, bulkcurorsrtocursoradapter does not implement the finalize function, so when bulkcurorsrtocursoradapter is recycled, it will not trigger the release of Cursor on the server. Therefore, if the client does not display the call close, the resources of the server process cannot be released.

It is suggested that when analyzing the failure cases of Monkey test, the author found that the process problems are all caused by Android process. In media. According to the analysis of finalize, the root of the problem lies in the client. Because there are many MediaProvider clients (including Music, Gallery3D, Video, etc.), every time this problem occurs, all MediaProvider client developers need to assist in the investigation.

7.5. 4 Summary of cursor close function

The Cursor close function does not involve any difficult knowledge points. I hope that the knowledge points introduced in this section can help readers establish the awareness of recycling resources in time. Although a few more lines of code are written, it is indispensable to maintain the stability of the whole system.

In addition, through the analysis of the close function, readers also see the disadvantages of over encapsulation, that is, repeatedly looking for the implementation of the corresponding function in the derived tree. Although this is not intentional by the code designer, it is easy to happen when there are too many inheritance relationships. At present, the author has not found a perfect solution. Readers are welcome to participate in the discussion.

7.6 analysis of contentresolveopenassetfiledescriptor function

Through the analysis of Cursor query, it can be seen that the client process can obtain information from the target CP process like querying the local database. However, this approach also has its limitations:

  • The client can only obtain data according to the organization of the result set, and the organization of the result set is determinant, that is, the client must move the cursor to the specified row to obtain the value of the column of interest. In real life, not all information can be organized into rows and columns.
  • The amount of data obtained by query query is very limited. The analysis shows that the shared memory used to carry data is only 2MB. For data with a large amount of data, it is obviously inappropriate to obtain it through query.

Considering the limitations of query, ContentProvider also supports another more direct data transmission mode, which I call "file stream mode". Because in this way, the client will get an object similar to a file descriptor, and then create the corresponding input or output stream object on it. In this way, the client can interact with the CP process through them.

Let's analyze how this function is implemented. First, introduce the function used by the client, that is, the openAssetFileDescriptor of ContentResolver.

7.6. 1. Client call analysis of openassetfiledescriptor

ContentResolver.java::openAssetFileDescriptor

public final AssetFileDescriptoropenAssetFileDescriptor(Uri uri,
           String mode) throws FileNotFoundException {
    //openAssetFileDescriptor is a generic function that supports three scheme type URI s. see
    //Explanation below

   Stringscheme = uri.getScheme();
   if(SCHEME_ANDROID_RESOURCE.equals(scheme)) {
        if(!"r".equals(mode)) {
              throw new FileNotFoundException("Can't write resources: " +uri);
        }
       //Create resource file input stream
      OpenResourceIdResult r = getResourceId(uri);
        try{
             return r.r.openRawResourceFd(r.id);
         } ......
        }else if (SCHEME_FILE.equals(scheme)) {
           //Create a normal file input stream
           ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
                   new File(uri.getPath()), modeToMode(uri, mode));
           return new AssetFileDescriptor(pfd, 0, -1);
        }else {
           if ("r".equals(mode)) {
                //We focus on the analysis of this function, which is used to read the data of the target ContentProvider
               return openTypedAssetFileDescriptor(uri, "*/*", null);
           } ......//For the support of other modes, please study this part by yourself after learning this chapter
        }
    }

As described in the above code, openAssetFileDescriptor is a general-purpose function that supports three sheme type URI s.

  • SCHEME_ANDROID_RESOURCE: the string is expressed as "android.resource". It can read the resources encapsulated in the APK package (actually a compressed file). Suppose a test.ogg file is stored in the res/raw directory of the application process, and the resulting resource id is expressed by R.raw.tet. If the application process wants to read this resource, the created URI is "Android resource://com.package.name/R.raw.test ”. Readers may as well try.

  • SCHEME_FILE: the string is expressed as "file". It can read ordinary files.

  • URI s other than the above two scheme s: what data corresponds to this resource needs to be interpreted by the target CP.

Next, analyze the opentypedasassetfiledescriptor function called in the third sheme type.

1. Opentypedasassetfiledescriptor function analysis

ContentResolver.java::openTypedAssetFileDescriptor

public final AssetFileDescriptoropenTypedAssetFileDescriptor(Uri uri,
           String mimeType, Bundle opts) throws FileNotFoundException {
    //Establish a channel to interact with the target CP process. Do readers remember the real type of provider here?
    //Its real type is ContentProviderProxy
   IContentProvider provider = acquireProvider(uri);
    try {
           //① Call the opentypedasassetfile function of the remote CP to return the value
          //The type is AssetFileDescriptor. The value of the parameter passed here is: mimeType = "* / *"
          //opts=null
           AssetFileDescriptor fd = provider.openTypedAssetFile(uri,
                                          mimeType, opts);
           ......
           //② Create a variable of type ParcelFileDescriptor
           ParcelFileDescriptor pfd = new ParcelFileDescriptorInner(
                   fd.getParcelFileDescriptor(), provider);
           provider = null;
           //③ Another variable of type AssetFileDescriptor is created as the return value
           return new AssetFileDescriptor(pfd, fd.getStartOffset(),
                   fd.getDeclaredLength());
        }......
         finally {
           if (provider != null) {
               releaseProvider(provider);
           }
        }
    }

In the above code, what hinders our thinking is still the emerging class. They should be resolved before analyzing the key calls in the code.

2. Introduction to filedescriptor family

The class family map involved in this section is shown in Figure 7-7.

Figure 7-7 profile of filedescriptor family

The content in Figure 7-7 is relatively simple and needs only a brief introduction.

  • FileDescriptor class is a standard class of Java, which encapsulates file descriptors. Each file opened by the process has a corresponding file descriptor. In Native language development, it is represented by an int variable.

  • As the local resource of the process, if you want to pass the file descriptor to other processes across the process boundary, you need to use the inter process sharing technology. On the Android platform, the designer encapsulates a ParcelFileDescriptor class. This class implements the Parcel interface and naturally supports the functions of serialization and deserialization. As can be seen from figure 7-7, a ParcelFileDescriptor points to a file descriptor through mfiledescriptor.

  • AssetFileDescriptor also implements the Parcel interface, which internally points to a ParcelFileDescriptor object through the mFd member variable. It can be seen from here that AssetFileDescriptor is a further encapsulation and extension of the ParcelFileDescriptor class. In fact, according to the description of AssetFileDescritpor in the SDK document, its function is to read the specified resource data from AssetManager (which will be introduced later when analyzing resource management).

Tips: briefly introduce the reader to the knowledge related to AssetFileDescriptor. It is used to read the resource data specified in the APK package. Take the test For example, if Ogg is read through AssetFileDescriptor, its mFd member points to a ParcelFileDescriptor object. Regardless of whether the object crosses the process boundary or not, it represents a file after all. Assuming that this file is an APK package, the mStartOffset variable of AssetFileDescriptor is used to indicate test The starting position of Ogg in the APK package, such as 100 bytes. mLength is used to indicate test The length of Ogg is assumed to be 1000 bytes. It can be seen from the above introduction that the APK file stores test in the space from 100 bytes to 1100 bytes Ogg data. In this way, the AssetFileDescriptor can set test The Ogg data is read from the APK package.

Let's look at the opentypedasassetfile function of the ContentProvider.

7.6. 2 Analysis of opentypedasassetfile function of ContentProvider

ContentProvider.java::openTypedAssetFile

public AssetFileDescriptor openTypedAssetFile(Uriuri, String mimeTypeFilter,
                      Bundle opts) throws FileNotFoundException {
    //This example satisfies the following if conditions
    if("*/*".equals(mimeTypeFilter))
         return openAssetFile(uri, "r");//The code of this function is shown below


    StringbaseType = getType(uri);
    if(baseType != null &&
               ClipDescription.compareMimeTypes(baseType, mimeTypeFilter)) {
          return openAssetFile(uri, "r");
    }
   throw newFileNotFoundException("Can't open " +
                uri + " as type " + mimeTypeFilter);
 }

ContentProvider.java::openAssetFile

public AssetFileDescriptor openAssetFile(Uri uri,String mode)
           throws FileNotFoundException {
   //openFile is implemented by subclasses. Take MediaProvider as an example
    ParcelFileDescriptor fd = openFile(uri, mode);
   //Get an AssetFileDescriptor object according to fd returned by openFile
    returnfd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
 }

The openFile function implemented by MediaProvider is analyzed below.

1. MediaProvideropenFile analysis

MediaProvider.java::openFile

public ParcelFileDescriptor openFile(Uri uri,String mode)
           throws FileNotFoundException {
  ParcelFileDescriptor pfd = null;
   //Assume that the URI meets the following if condition, that is, what the client wants to read is the information of the Album to which a music file belongs
   if(URI_MATCHER.match(uri) == AUDIO_ALBUMART_FILE_ID) {
       DatabaseHelper database = getDatabaseForUri(uri);
        ......
       SQLiteDatabase db = database.getReadableDatabase();
       ......
       SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        //Get the name of the music file specified by the client_ id value
        intsongid = Integer.parseInt(uri.getPathSegments().get(3));
       qb.setTables("audio_meta");
       qb.appendWhere("_id=" + songid);
       Cursor c = qb.query(db,
                   new String [] {
                       MediaStore.Audio.Media.DATA,
                       MediaStore.Audio.Media.ALBUM_ID },
                   null, null, null, null, null);
        if(c.moveToFirst()) {
             String audiopath = c.getString(0);
             //Get the album to which the music belongs_ ID value
             int albumid = c.getInt(1);
             //Notice that the ALBUMART_ in the following function is called The URI will point to album_art table
             Uri newUri = ContentUris.withAppendedId(ALBUMART_URI, albumid);
             try {
                   //Call the openFileHelper function implemented by ContentProvider. Note that pfd
                   //The type is ParcelFileDescriptor
                   pfd = openFileHelper(newUri, mode);
               } ......
           }
           c.close();
           return pfd;
        }
    ......
}

In the above code, the MediaProvider will first pass the name of the music file specified by the client_ id to query its album information. Here is an example for the reader, as shown in Figure 7-8.

Figure 7-8 audio_meta content display

The SQL statement set in Figure 7-8 is select_ id,album_id,_ data from audio_ Meta, the resulting set contains: the first column of music files_ ID value, and the second column returns the album to which the music file belongs_ ID value, and the third column returns the file storage path of the corresponding song.

The above code constructs a new URI variable before calling the openFileHelper function. According to the comments in the code, it will query the album_ For the art table, take another example, as shown in Figure 7-9.

Figure 7-9 album_art content display

In Figure 7-9, the first column of the result set is the thumbnail file storage path of the album artist, and the second column is the album artist_ ID value. Therefore, the file to be opened is the corresponding album_ Thumbnail of ID. Let's look at the code of openFileHelper.

2. Analysis of contentprovideropenfilehelper function

ContentProvider.java::openFileHelper

protected final ParcelFileDescriptoropenFileHelper(Uri uri,
           String mode) throws FileNotFoundException {
  //Gets the file path of the thumbnail
  Cursor c =query(uri, new String[]{"_data"}, null, null, null);
  int count= (c != null) ? c.getCount() : 0;
  if (count!= 1) {
     ......//An album_id can only correspond to one thumbnail file
  }

  c.moveToFirst();
   int i =c.getColumnIndex("_data");
   Stringpath = (i >= 0 ? c.getString(i) : null);
   c.close();
    if (path == null)
           throw new FileNotFoundException("Column _data not found.");
    intmodeBits = ContentResolver.modeToMode(uri, mode);
    //When creating a ParcelFileDescriptor object, a file will be opened internally to get the
   //A FileDescriptor object, and then create a ParcelFileDescriptor object. In fact
   //This is to set the value of the member variable mFileDescriptor in the ParcelFileDescriptor
    return ParcelFileDescriptor.open(new File(path), modeBits);
 }

So far, the server has opened the specified file. Then, how is the file descriptor of the server passed to the client? Let's answer this question in a separate section.

7.6. Discussion on transferring file descriptors across processes

Before answering the questions in the previous section, I wonder if the reader has thought about the following questions:

What is the purpose of cross process transfer of file descriptors?

As an example of reading the thumbnail of a music album shown in the above section, the answer to the question is to enable the client to read the thumbnail file of the album. Why doesn't the client get the file storage path of the corresponding album thumbnail first, and then open the file directly, but it takes so much trouble? There are two reasons:

  • For security reasons, MediaProvider does not want clients to bypass it to directly read files on storage devices. In addition, the client must additionally declare the read-write permission of the relevant storage device before directly reading the files on it.
  • Although this example is for an actual file, from the perspective of scalability, we hope that the client can use a more general interface through which the data of the actual file and the data from the network can be read, and the user of this interface does not need to care where the data comes from.

Tip: in fact, there are more reasons. Readers might as well try to expand their thinking on the basis of the above two reasons.

1. Serialize the ParcelFileDescriptor

OK, continue to discuss the situation of this example. Now the server has opened a thumbnail file and obtained a file descriptor object FileDescriptor. This file is opened by the server. How to make the client open this file? According to the previous analysis, the client will not and should not open the file itself through the file path. What should I do?

Never mind. The Binder driver supports the transfer of file descriptors across processes. Let's first look at the serialization function writeToParcel of the ParcelFileDescriptor. The code is as follows:

ParcelFileDescriptor.java::writeToParcel

public void writeToParcel(Parcel out, int flags) {
   //Directly write the FileDescriptor object pointed to by mFileDescriptor to the Parcel package
  out.writeFileDescriptor(mFileDescriptor);
   if((flags&PARCELABLE_WRITE_RETURN_VALUE) != 0 && !mClosed) {
        try {
               close();
           }......
        }
    }

The writeFileDescriptor of Parcel is a native function. The code is as follows:

android_util_Binder.cpp::android_os_Parcel_writeFileDescriptor

static voidandroid_os_Parcel_writeFileDescriptor(JNIEnv* env,
                           jobject clazz,jobject object)
{
    Parcel*parcel = parcelForJavaObject(env, clazz);
    if(parcel != NULL) {
        //First call jnigetfdffromfiledescriptor to get the file from the Java layer FileDescriptor object
       //Take out the corresponding file descriptor. In the Native layer, the file descriptor is an int integer
       //And then call the writeDupFileDescriptor function of the Native parcel object.
       const status_t err =
               parcel->writeDupFileDescriptor(
                                   jniGetFDFromFileDescriptor(env, object));
        if(err != NO_ERROR) {
           signalExceptionForError(env, clazz, err);
        }
    }
}

The writeDupFileDescriptor code of the Native Parcel class is as follows:

Parcel.cpp::writeDupFileDescriptor

status_t Parcel::writeDupFileDescriptor(int fd)
{
    return writeFileDescriptor(dup(fd), true);
}

//Let's look directly at the writeFileDescriptor function

status_t Parcel::writeFileDescriptor(int fd, booltakeOwnership)
{
   flat_binder_object obj;
    obj.type= BINDER_TYPE_FD;
   obj.flags = 0x7f | FLAT_BINDER_FLAG_ACCEPTS_FDS;
   obj.handle = fd; //Pass the file descriptor opened by MediaProvider to Binder protocol
   obj.cookie = (void*) (takeOwnership ? 1 : 0);
    return writeObject(obj, true);
}

It can be seen from the above code that the serialization process of ParcelFileDescriptor is to take out the file descriptor of its internal corresponding file and store it in a Binder driven flat_binder_object object. The object is eventually sent to the Binder driver.

2. Deserialize the ParcelFileDescriptor

Assuming that the client process receives a reply from the server, what the client needs to do is to construct a new ParcelFileDescriptor according to the reply package of the server. We focus on the anti serialization of file descriptors, where the function called is Parcel's readFileDescriptor, which is as follows:

ParcelFileDescriptor.java::readFileDescriptor

public final ParcelFileDescriptorreadFileDescriptor() {
  //Returns a FileDescriptor object from internalReadFileDescriptor
 FileDescriptor fd = internalReadFileDescriptor();
  //Construct a ParcelFileDescriptor object, and the file corresponding to this object is the thumbnail file opened by the server
  return fd!= null ? new ParcelFileDescriptor(fd) : null;

internalReadFileDescriptor is a native function. Its implementation code is as follows:

android_util_Binder.cpp::android_os_Parcel_readFileDescriptor

static jobjectandroid_os_Parcel_readFileDescriptor(JNIEnv* env, jobject clazz)
{
    Parcel*parcel = parcelForJavaObject(env, clazz);
    if(parcel != NULL) {
        //Call the readFileDescriptor of Parcel to get a file descriptor
        intfd = parcel->readFileDescriptor();
        if(fd < 0) return NULL;
        fd =dup(fd);//Call dup to copy the file descriptor
        if(fd < 0) return NULL;
        //Call jniCreateFileDescriptor to return a Java layer FileDescriptor object
       return jniCreateFileDescriptor(env, fd);
    }
    return NULL;
}

Look at the readFileDescriptor function of Parcel. The code is as follows:

Parcel.cpp::readFileDescriptor

int Parcel::readFileDescriptor() const
{
    constflat_binder_object* flat = readObject(true);
    if(flat) {
       switch (flat->type) {
           case BINDER_TYPE_FD:
               //When the server sends a reply packet, the handle variable points to fd. When the client receives the reply packet,
              //Get fd from handle again. Is this fd the other fd?
               return flat->handle;
       }       
    }
    return BAD_TYPE;
}

The author mentioned a profound question in the above code: is this fd the other fd? The real meaning of this question is:

  • The server opens a file and gets an fd. Note that fd is an integer. On the server side, this fd does correspond to an open file.
  • The client also gets an integer value. Does it correspond to a file?

If the client gets an integer value, it is considered that it has got a file, which is a bit hasty. In the above code, we found that the client did create a FileDescriptor object based on the integer value received. So, how can we know that this integer value must represent a file in the client?

The ultimate answer to this question is in the Binder driver code. Look at its binder_transaction function.

3. Processing of Binder driver for file descriptor transfer

binder.c::binder_transaction

static void binder_transaction(struct binder_proc*proc,
                      structbinder_thread *thread,
                      structbinder_transaction_data *tr, int reply)
   ......
   switch(fp->type) {
    case BINDER_TYPE_FD: {
            int target_fd;
            struct file *file;
            if (reply) {
             ......
            //The Binder driver finds the representative file of the file in the kernel according to the fd returned by the server, and its data type is
            //struct file
            file = fget(fp->handle);
            ......
            //target_proc is the client process, task_ get_ unused_ fd_ The flags function is used from the client
           //Find an idle integer value in the process and use it as the file descriptor of the client process
            target_fd = task_get_unused_fd_flags(target_proc,
                                           O_CLOEXEC);
            ......
            //Bind the file descriptor of the client process to the file object representing the file
            task_fd_install(target_proc, target_fd, file);
            fp->handle = target_fd;
     }break;
   ......//Other processing
}

The truth comes out! Originally, the Binder driver opened the corresponding file instead of the client, so it is now certain that the integer value received by the client really represents a file.

4. In depth discussion

When studying this code, the author once asked his colleagues in his team such a question: on the Linux platform, what is the way to make two processes share the data of the same file? I have received the following answers:

  • Two processes open the same file. As discussed earlier, this method has poor security and scalability, which is not the way we want.
  • Through the relationship between parent and child processes, file redirection technology is used. Due to the close relationship between the two processes, this implementation method has poor expansibility and is not what we want.
  • Jump out of the limit for two processes to open the same file. Create a pipeline between the two processes, then the server reads the file data and writes it to the pipeline, and then the client process obtains the data from the pipeline. This approach is different from the openasset filedescriptor described earlier.

In the absence of Binder driver support, it is difficult to transfer file descriptors across processes on Linux platform. From the above three answers, the most extensible is the third way, that is, pipes are used as communication means between processes. However, for the Android platform, this method is obviously not as efficient as the existing implementation of openAssetFileDescriptor. The reason lies in the characteristics of the pipeline itself.

The server must start a thread separately to continuously write data to the pipeline, That is, the entire data flow is driven by the write end (although when the pipeline has no space, if the read end does not read the data, the write end can no longer write the data, but if the write end does not write the data, the read end must not read the data. Based on this understanding, the author believes that the driving force of data flow in the pipeline should be at the write end).

Android provides pipeline support for ContentProvider after 3.0. Let's look at the related functions.

ContentProvider.java::openPipeHelper

public <T> ParcelFileDescriptoropenPipeHelper(final Uri uri,
            finalString mimeType, final Bundle opts,
           final T args, final PipeDataWriter<T> func)
           throws FileNotFoundException {
        try {
           //Create pipe
           final ParcelFileDescriptor[] fds = ParcelFileDescriptor.
                                              createPipe();
          //Construct an AsyncTask object
           AsyncTask<Object, Object, Object> task = new
                                    AsyncTask<Object,Object, Object>() {
               @Override
               protected Object doInBackground(Object... params) {
                  //Write data to the write end of the pipeline. If there is no write operation of the background thread, the client will not write anyway
                  //I can't read the data
                   func.writeDataToPipe(fds[1],uri, mimeType, opts, args);
                   try {
                        fds[1].close();
                   } ......
                   return null;
               }
           };
          //AsyncTask.THREAD_POOL_EXECUTOR is a thread pool and the doInBackground of task
          //The function will run in a thread in the thread pool
           task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
                                      (Object[])null);
           return fds[0];//Return the reader to the client
        } ......
 }

From the above code, it can be seen that the overhead of pipeline is indeed greater than that of the client directly obtaining the file descriptor.

7.6. 4. Analysis and summary of openassetfiledescriptor function

This section discusses the second data sharing method provided by ContentProvider, namely file stream method. In this way, the client can obtain the desired data from the target CP.

This section covers a wide range of topics, from the Java layer to the driver layer. I hope readers can seriously understand it and be sure to make clear the principle.

7.7 learning guidance of this chapter

This chapter discusses ContentProvider in depth. In contrast, the startup and creation of ContentProvider are less difficult and important, while the use of SQLite, data sharing and transmission between processes are much more complex. It is suggested that after reading this chapter, readers can further study the following issues on this basis:

  • How does the client process undo its close relationship with the target CP process.
  • Try to encapsulate a lightweight, object-oriented SQLite class library.
  • For the knowledge related to serialization and deserialization, it is best to write some simple examples for practice.
  • Java programmers deeply read the book "high quality Java programming" and establish a good awareness of resource management and recycling. It is better to read the reference book "Pattern Oriented Software Architecture Volume 3: patterns for resource management" mentioned in the footnote.

7.8 summary of this chapter

This chapter has a more in-depth and detailed discussion around ContentProvider. Firstly, it introduces how the target ContentProvider process starts and creates a ContentProvider instance; Then it introduces SQLite and SQLiteDatabase family; The last three sections focus on the implementation of Cursor's query and close functions and the cross process transfer of file descriptors.

[①] in fact, this is a broad design pattern. Readers can refer to the Book Pattern Oriented Software Architecture Volume 3: patterns for resource management to deepen their understanding.
[②] there should be other design considerations here. I hope readers can participate in the discussion.

Topics: Android