sharedUserId
Android中给每一个apk的进程分配了一个单独的userid,不同的userid拥有不同的进程,不同的userid也使得两个进行的资源进行了隔离,当需要共享两个apk的data/data/目录下或/data/user_de/0/包名/下资源时,将两个apk的userid需要设置为相同,即可达到共享资源的目的,或者将相同userid的两个apk运行在同一个进程时,也可以共享资源,下面主要针对相同uid配置位不同apk时的情况。
使用方式
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.shared_user_a"
android:sharedUserId="com.example.shareduser">
</manifest>
通过设置两个apk为相同的sharedUserId,即使两个应用运行在不同的进程,也可以相互访问文件,图片等静态资源,当设置两个apk为相同的sharedUserId时,两个apk的签名需要相同。
访问(/data/data/app包名)缓存目录下的文件
android中,每个apk会有自己的缓存目录,或者是data/data/包名/或者data/user_de/0/包名/(该目录可以在加锁的时候访问),不同的apk是没有办法相互访问数据的,但是通过设置相同的userId之后(相同uid的apk需要签名相同),两个apk是可以相互访问缓存数据的。
App A会在onCreate的时候在自己的缓存目录下创建文件,同时我们给textview设置了监听,点击可以获取文本中的内容。
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WriteSettings("app a write it");
mTextView = findViewById(R.id.sharedText);
mTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mTextView.setText(ReadSettings(MainActivity.this));
}
});
}
public void WriteSettings(String data) {
FileOutputStream fOut = null;
OutputStreamWriter osw = null;
try {
fOut = openFileOutput("settings.txt", MODE_PRIVATE);
osw = new OutputStreamWriter(fOut);
osw.write(data);
osw.flush();
Log.i(TAG, "WriteSettings: success");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (osw != null) {
osw.close();
}
if (fOut != null) {
fOut.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String ReadSettings(Context context) {
FileInputStream fIn = null;
InputStreamReader isr = null;
char[] inputBuffer = new char[255];
String data = null;
try {
fIn = context.openFileInput("settings.txt");
isr = new InputStreamReader(fIn);
isr.read(inputBuffer);
data = new String(inputBuffer);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (isr != null) {
isr.close();
}
if (fIn != null) {
fIn.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return data;
}
}
APP B会在onCreate的时候首先读取App A缓存目录下的该文件,然后给该目录下的文件中写入内容,
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = findViewById(R.id.sharedText);
try {
//获取程序A的context,通过这个context我们可以访问到A程序的缓存目录下的文件,以及获取程序A的资源
Context context = this.createPackageContext(
"com.example.shared_user_a", Context.CONTEXT_IGNORE_SECURITY);
String msg = ReadSettings(context);
mTextView.setText(msg);
WriteSettings(context, "app b modified");
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
public String ReadSettings(Context context) {
FileInputStream fIn = null;
InputStreamReader isr = null;
char[] inputBuffer = new char[255];
String data = null;
try {
//此处调用并没有区别,但context此时是从程序A里面获取的
fIn = context.openFileInput("settings.txt");
isr = new InputStreamReader(fIn);
isr.read(inputBuffer);
data = new String(inputBuffer);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (isr != null) {
isr.close();
}
if (fIn != null) {
fIn.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return data;
}
public void WriteSettings(Context context, String data) {
FileOutputStream fOut = null;
OutputStreamWriter osw = null;
try {
fOut = context.openFileOutput("settings.txt", MODE_PRIVATE);
//此处调用并没有区别,但context此时是从程序A里面获取的
osw = new OutputStreamWriter(fOut);
osw.write(data);
osw.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (osw != null) {
osw.close();
}
if (fOut != null) {
fOut.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
运行App A后,运行App b,可以看到App b读取到了App a缓存目录下的文件内容,然后点击App a界面上的黑色文本,可以看到获取到了App b写入的内容。
如果没有为两个程序添加相同的shareduserid,App b是没有办法获取到另一个apk缓存目录下面的文件的,会报权限错误。
获取另一个进程的sp文件
通过设置相同的sharedUserId我们可以通过一个apk去访问另一个apk的sp文件下面的资源:
AppA,每次点击往sp中写入新的值:
findViewById(R.id.changeTextInSp).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//程序A中
SharedPreferences sp = MainActivity.this.getSharedPreferences("sp", MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString("Rkey", "App a put " + mCount++ + " to sp");
editor.commit();
}
});
AppB每次读取sp中的值:
// MODE_MULTI_PROCESS会重新加载文件
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "onCreate: start read sp");
// MODE_MULTI_PROCESS会重新加载文件
SharedPreferences sp = finalPackageContextA.getSharedPreferences("sp", Context.MODE_MULTI_PROCESS);
String value = sp.getString("Rkey", "");
Log.i(TAG, "onCreate:Rkey value is" + value);
textView.setText(value);
}
});
效果,每次AppA改变sp中的值,AppB中可以重新读取到该文件中的值:
在读取sp文件的时候,需要将mode设置为Context.MODE_MULTI_PROCESS,这样才会重新读取,否则会获取全局cache,查看代码:
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
checkMode(mode);
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
if (isCredentialProtectedStorage()
&& !getSystemService(UserManager.class)
.isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
throw new IllegalStateException("SharedPreferences in credential encrypted "
+ "storage are not available until after user is unlocked");
}
}
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// If somebody else (some other process) changed the prefs
// file behind our back, we reload it. This has been the
// historical (if undocumented) behavior.
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
每次在getSharedPreferences()的时候,首先会去取全局的cache,没有cache则直接读取后返回,有cache读取cache,且如果mode为MODE_MULTI_PROCESS会去重新加载文件内容,这最然保证了读取内容的实时性,但是这也意味着重新加载文件带来的额外的消耗,同时Google不建议使用MODE_MULTI_PROCESS,推荐使用ContentProvider,因此这种读取方式也不建议使用,只是知道就好。
获取另一个apk的res目录下的资源
我们可以通过使用createPackageContext()接口创建另一个应用的context,通过这个context就可以 访问另一个程序res下面的资源,自己测试这里并不需要两个apk拥有相同的sharedUserId。
于是自己有个疑问就是难道一个程序知道另一个程序的包名就可以获取另一个程序的资源吗?这里难道没有什么安全校验吗?希望广大博友知道该问题的话可以给予解答
在App A的drawable目录下放置一个kite.png的图片,同时在strings中定义一个字符串,app_A_define_str,在测试的时候我们去掉App b的sharedUserId,部分代码如下:
...
Resources resources = finalPackageContextA.getResources();
int drawableRes = resources.getIdentifier("kite", "drawable", "com.example.shared_user_a");
int str = resources.getIdentifier("app_A_define_str", "string", "com.example.shared_user_a");
mTextView.setText(resources.getString(str));
mTextView.setBackground(resources.getDrawable(drawableRes));
...
App b成功获取到App a中定义的资源和字符串
一个apk中调用另一个apk中的代码
AppA中我们定义一个类:
public class ResUtil {
private static final String TAG = "ResUtil";
public ResUtil() {
}
public void invokeAppACode() {
Log.i(TAG, "invokeAppACode: I am app a");
}
}
AppB中我们通过反射访问该类中的方法:
private void invokeAppA() {
Context AooAContext = null;
try {
AooAContext = this.createPackageContext("com.example.shared_user_a"
, Context.CONTEXT_INCLUDE_CODE
| Context.CONTEXT_IGNORE_SECURITY);
} catch (PackageManager.NameNotFoundException e) {
Log.i(TAG, "invokeAppA: NameNotFoundException");
}
ClassLoader loader = AooAContext.getClassLoader();
Class<?> clazz = null;
try {
clazz = loader.loadClass("com.example.shared_user_a.ResUtil");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
Object a = clazz.getConstructor().newInstance();
Method appACode = clazz.getMethod("invokeAppACode");
appACode.invoke(a);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
查看AppB进程中的日志,
com.example.shared_user_b I/ResUtil: invokeAppACode: I am app a
AppB进程成功调用了AppA中的代码,同样这里自己有个疑问就是一个apk调用另一个apk的代码不需要权限什么的吗,这里没有使用相同的sharedUid也是可以访问到的,如果有哪位博友知道的话希望能不解答下。
参考:
https://www.jianshu.com/p/19f021d2b2a8
https://blog.csdn.net/zy_jibai/article/details/84930231