之前的文章中,对SharedPreferences的基本使用进行了介绍。同时也提到了,SharedPreferences的功能并不是为了解决跨进程通信,且也不支持跨进程。实际上并非如此,谷歌官方只是不推荐也不建议我们在跨进程场景中使用它,但是我们依然有办法在不同的进程中通过SharedPreferences共享数据。主要要利用到Context类的createPackageContext(String packageName, int flags)方法,这个方法,可以在应用中,创建其他包中应用的上下文(也就是context),进而借助这个上下文来对其他包中的资源甚至代码进行访问。
依旧是通过Demo来介绍相关代码并演示效果,我们通过在一个应用中写入SharedPreferences,并在另外一个应用中读取写入的值来进行演示。
其中负责写入的应用我们直接利用SharedPreferences牛刀小试一文中已经写好的Demo(需要稍加修改),另外再新建一个应用AccessPreferences来负责读取。
先上一下演示效果吧:
我们在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的跨进程之旅,就先到这里吧。