之前的文章中,对SharedPreferences的基本使用进行了介绍。同时也提到了,SharedPreferences的功能并不是为了解决跨进程通信,且也不支持跨进程。实际上并非如此,谷歌官方只是不推荐也不建议我们在跨进程场景中使用它,但是我们依然有办法在不同的进程中通过SharedPreferences共享数据。主要要利用到Context类的createPackageContext(String packageName, int flags)方法,这个方法,可以在应用中,创建其他包中应用的上下文(也就是context),进而借助这个上下文来对其他包中的资源甚至代码进行访问。

依旧是通过Demo来介绍相关代码并演示效果,我们通过在一个应用中写入SharedPreferences,并在另外一个应用中读取写入的值来进行演示。

其中负责写入的应用我们直接利用SharedPreferences牛刀小试一文中已经写好的Demo(需要稍加修改),另外再新建一个应用AccessPreferences来负责读取。

先上一下演示效果吧:

getSharedPreferences 跨应用 sharedpreferences跨进程_Android

 

我们在AccessPreferences应用中提供一个按钮,点击就会去读取SharedPreferencesDemo中写入的值,具体代码如下:

package com.itachi.android.accesspreferences;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import java.util.List;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private Button mButtonGetData;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mButtonGetData = (Button) findViewById(R.id.button_get_data);
        mButtonGetData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                getDataFromOtherApplication("com.itachi.android.sharedpreferencesdemo", MainActivity.this, "SharedPreferencesDemo");
            }
        });
    }

    private void getDataFromOtherApplication(String pkg, Context context, String prefsName) {
        Context otherContext = null;
        getPkgsNames();
        try {
            // 通过createPackageContext方法创建了应用包"com.itachi.android.sharedpreferencesdemo"的上下文(context)
            // 并利用其获取到了其包内的SharedPreferences
            // 其中CONTEXT_IGNORE_SECURITY这个flag用于忽略安全限制,使得无论如何都可以创建对应包的上下文
            // 如果不仅需要访问包内的文件,还需要调用包内的方法,还需要额外利用CONTEXT_INCLUDE_CODE这个flag,加上这个flag,我们甚至可以跨进程的调用其他包内的方法
            otherContext = context.createPackageContext(pkg, CONTEXT_IGNORE_SECURITY);
            SharedPreferences sharedPreferences = otherContext.getSharedPreferences(prefsName, MODE_PRIVATE);
            String username = sharedPreferences.getString("Username", "Null");
            int age = sharedPreferences.getInt("Age", -1);
            Toast.makeText(this, "Username:" + username + ", Age:" + age, Toast.LENGTH_SHORT).show();
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
    }

    // 一开始总是读不到"com.itachi.android.sharedpreferencesdemo" 于是就写了这个方法来遍历展示本应用能够访问到的包名
    private void getPkgsNames() {
        PackageManager pm = getPackageManager();
        List<PackageInfo> list = pm.getInstalledPackages(PackageManager.GET_ACTIVITIES);
        for (PackageInfo info : list) {
            Log.d(TAG, info.packageName);
        }
    }
}

对于SharedPreferencesDemo这个应用,我们需要对其稍加改动,由于我们在创建SharedPreferences时需要指定创建文件的mode,如果此mode为MODE_PRIVATE,则其他的进程无法读取到这个文件的内容,在Android早起版本(Android 4.4之前),还有MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE两种mode,由这两种mode创建的文件对于系统中的其他进程都是可见的(可读、可写),处于安全因素考虑,在Android 4.4上废弃了,如果现在使用这两种mode,会抛出SecurityException,在ContextImpl类中,这部分代码如下:

private void checkMode(int mode) {
        if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
            if ((mode & MODE_WORLD_READABLE) != 0) {
                throw new SecurityException("MODE_WORLD_READABLE no longer supported");
            }
            if ((mode & MODE_WORLD_WRITEABLE) != 0) {
                throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
            }
        }
    }

So,怎么解决这个问题?Android还为我们留了一个mode,MODE_MULTI_PROCESS用于创建一个多进程可访问的文件,关于这个flag,谷歌的官方注释如下:

/**
     * SharedPreference loading flag: when set, the file on disk will
     * be checked for modification even if the shared preferences
     * instance is already loaded in this process.  This behavior is
     * sometimes desired in cases where the application has multiple
     * processes, all writing to the same SharedPreferences file.
     * Generally there are better forms of communication between
     * processes, though.
     *
     * <p>This was the legacy (but undocumented) behavior in and
     * before Gingerbread (Android 2.3) and this flag is implied when
     * targeting such releases.  For applications targeting SDK
     * versions <em>greater than</em> Android 2.3, this flag must be
     * explicitly set if desired.
     *
     * @see #getSharedPreferences
     *
     * @deprecated MODE_MULTI_PROCESS does not work reliably in
     * some versions of Android, and furthermore does not provide any
     * mechanism for reconciling concurrent modifications across
     * processes.  Applications should not attempt to use it.  Instead,
     * they should use an explicit cross-process data management
     * approach such as {@link android.content.ContentProvider ContentProvider}.
     */
    @Deprecated
    public static final int MODE_MULTI_PROCESS = 0x0004;

从注释中可以看到,谷歌官方说明了这个flag在多进程下不是很可靠,并且不会提供任何的并发修改机制,不建议开发者使用它,且对于多进程而言,有更好的跨进程通信方式(例如bundle,message,aidl等)。

所以在SharedPreferencesDemo应用中,我们需要将创建SharedPreferences的mode改为MODE_MULTI_PROCESS以便于我们能够跨进程访问它,如下:

package com.itachi.android.sharedpreferencesdemo;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import com.itachi.android.sharedpreferencesdemo.util.SharedPreferencesUtils;

import java.util.List;

public class SharedPreferencesActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "SharedPreferencesActivtiy";
    private static final String CUSTOM_PREFERENCES_NAME = "CustomPreferences";

    private EditText mUsername;
    private EditText mAge;
    private Button mWriteToApplications;
    private Button mWriteToActivitys;
    private Button mWriteToCustom;
    private Button mToOtherActivity;
    private Button mGetPkgsList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_shared_preferences);
        mUsername = findViewById(R.id.user_name);
        mAge = findViewById(R.id.user_age);
        mWriteToApplications = findViewById(R.id.button_write_to_applications_preferences);
        mWriteToActivitys = findViewById(R.id.button_write_to_current_activitys_preferences);
        mWriteToCustom = findViewById(R.id.button_write_to_custom_preferences);
        mToOtherActivity = findViewById(R.id.to_other_activity);
        mGetPkgsList = findViewById(R.id.get_pkg_list);
        mWriteToApplications.setOnClickListener(this);
        mWriteToActivitys.setOnClickListener(this);
        mWriteToCustom.setOnClickListener(this);
        mToOtherActivity.setOnClickListener(this);
        mGetPkgsList.setOnClickListener(this);
    }

    private void writeToPreferences(SharedPreferences preferences) {
        SharedPreferences.Editor editor = preferences.edit();
        String username = mUsername.getText().toString();
        int age = Integer.valueOf(TextUtils.isEmpty(mAge.getText()) ? "-1" : mAge.getText().toString());
        editor.putString("Username", username);
        editor.putInt("Age", age);
        editor.commit();
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button_write_to_applications_preferences:
                // 将mode改为MODE_MULTI_PROCESS
                writeToPreferences(SharedPreferencesUtils.getCurrentApplicationSharedPreferences(this, MODE_MULTI_PROCESS));
                break;
            case R.id.button_write_to_current_activitys_preferences:
                writeToPreferences(SharedPreferencesUtils.getCurrentActivityPreferences(this, MODE_PRIVATE));
                break;
            case R.id.button_write_to_custom_preferences:
                writeToPreferences(SharedPreferencesUtils.getPreferencesByName(this, CUSTOM_PREFERENCES_NAME, MODE_PRIVATE));
                break;
            case R.id.to_other_activity:
                Intent intent = new Intent(this, OtherActivity.class);
                startActivity(intent);
                break;
            case R.id.get_pkg_list:
                PackageManager pm = getPackageManager();
                List<PackageInfo> list = pm.getInstalledPackages(PackageManager.GET_ACTIVITIES);
                for (PackageInfo info : list) {
                    Log.d(TAG, info.packageName);
                }
            default:
                break;
        }
    }
}

除此之外,在写这个Demo的时候我还遇到了一个问题,就是在AccessPreferences应用中一直无法获取到"com.itachi.android.sharedpreferencesdemo"这个应用包,捣鼓了好久。最后才发现,是由于Android的沙箱(SandBox)机制导致,大概介绍一下吧,这个机制会将不同的应用隔离开,Android的每个应用,系统都会为它分配一个虚拟机,每个虚拟机都对应了Linux系统中的一个进程,系统为每个应用分配一个UID,对于一个应用内的文件系统,只有拥有这个UID的应用才能够访问。虽然我没有真正的去看createPackageContext这个方法的底层实现,不过大致也能看出,应该是通过反射去对应的包中找到对应的class文件,并将其加载到本应用的内存中,因此需要访问到不同包下的文件,因此为了能够顺利的找到这个包中的文件,我们还需要将两个应用的UID配置成一样,这个就需要在应用的manifest文件中进行修改,具体如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.itachi.android.sharedpreferencesdemo"
    android:sharedUserId="com.itachi.android">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.SharedPreferencesDemo">
        <activity android:name=".OtherActivity"></activity>
        <activity android:name=".SharedPreferencesActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

我们通过为应用指定sharedUserId,来使得不同的应用之间能够互相访问文件系统,才能够使得createPackageContext方法能够正常的创建其他应用的上下文对象。

另外,如果你在manifest文件中申明了sharedUserId,那么就需要为这个应用添加签名,直接通过Android Studio的"Run 'app'"按钮无法将应用安装到手机,这也说明了对sharedUserId的使用,需要校验签名,其实想想也对,如果两个应用之前仅仅只需要在manifest指明相同的sharedUserId就能够访问其文件系统,那么也太不安全了,任何人都能够通过相同的sharedUserId来对你的代码和资源进行利用,因此需要加上签名来进行身份确认。

最后,关于SharedPreferences,既然谷歌官方都已经说了它为了解决跨进程通信的问题,也不推荐使用它作为跨进程通信的方式,并且也不支持多进程同步的读写保障,毕竟有很多其他更好的方式来进行跨进程通信。

关于SharedPreferences的跨进程之旅,就先到这里吧。