一、前言

今天我们来看一下Android中一个众人熟悉的一个属性:shareUserId,关于这个属性可能大家都很熟悉了,最近在开发项目,用到了这个属性,虽然知道一点知识,但是感觉还是有些迷糊,所以就写篇文章来深入研究一下。

关于Android中的sharedUserId的概念这里就简单介绍一下:

Android给每个APK进程分配一个单独的空间,manifest中的userid就是对应一个分配的Linux用户ID,并且为它创建一个沙箱,以防止影

响其他应用程序(或者其他应用程序影响它)。用户ID 在应用程序安装到设备中时被分配,并且在这个设备中保持它的永久性。

通常,不同的APK会具有不同的userId,因此运行时属于不同的进程中,而不同进程中的资源是不共享的,在保障了程序运行的稳定。然后在有些时候,我们自己开发了多个APK并且需要他们之间互相共享资源,那么就需要通过设置shareUserId来实现这一目的。

通过Shared User id,拥有同一个User id的多个APK可以配置成运行在同一个进程中.所以默认就是可以互相访问任意数据. 也可以配置成运行成不同的进程, 同时可以访问其他APK的数据目录下的数据库和文件.就像访问本程序的数据一样。

用法也很简单:

在需要共享资源的项目的每个AndroidMainfest.xml中添加shareuserId的标签。
android:sharedUserId="com.example"
id名自由设置,但必须保证每个项目都使用了相同的sharedUserId。一个mainfest只能有一个Shareuserid标签。


二、问题延伸

我们今天先来看一个场景:Android中一个App如何能够访问到其他App的信息和资源?

这个可能很多人感觉是两个App之间的通信,其实不是,比如我们在早期遇到支付宝有一个快捷支付,那么我们会看到手机中会安装两个app,一个是支付宝app,一个是快捷支付app,那么在开启快捷支付的时候,就会调用快捷支付app等,大家可能会想到现在有一个比较流行的技术叫做插件开发,的确如此,这个我在之前的文章也有说过,不清楚的同学可以点击这里:Android中插件开发篇

但是我们今天不说这个插件怎么搞,今天就来看看如何在一个app中去访问另外一个app的代码和资源等信息?

在说这个知识点之前,我们需要了解的一个知识点,就是我们可以通过一个包名来得到对应的Context的全局变量,可以直接使用Context的一个静态方法:createPackageContext

关于这个方法其实很简单,他有两个参数:

第一个参数:需要构造出来Context的包名字符串

第二个参数:构造出来的Context的开启模式

下面我们可以直接使用一个例子来看看效果:

首先我们弄一个插件工程:ShareUserIdPlugin

这个工程很简单,我们编译安装运行即可。


在弄一个宿主工程:ShareUserIdHost


这里有一个核心方法,我们首先通过插件工程的包名:cn.wjdiankong.shareuseridplugin;创建出一个Context对象。

这里看到第二参数有两个模式:

Context.CONTEXT_INCLUDE_CODE:这个标志是在我们需要执行插件中的某段代码需要加上的值。

CONTEXT_IGNORE_SECURITY:这个标志是必须的,是忽视安全性,如果没有这个值的话,那么我们访问什么都是失败的。

得到了Context变量之后,我们下面就可以通过反射来执行代码和获取资源了,这里需要注意的是,一定要先拿到Context对应的ClassLoader,然后才能加载对应的类,ClassLoader一定是Context的,是插件工程中的类加载器。


下面我们运行结果看看:


运行成功了啦~~是不是很简单呢。

下面如果我们把CONTEXT_INCLUDE_CODE去掉,在运行:


发现报错了,找不到指定的类。所以如果想运行代码的话,这个值一定要加上。


我们再把CONTEXT_IGNORE_SECURITY去掉,运行结果:


看到了,爆出了安全错误,所以要想构造成功Context出来,必须要加上这个值。


三、步入正题

好了,到这里我们就介绍了如何通过包名构造一个Context变量出来,然后执行对应的代码和获取资源。那么这个我们看到工程中貌似没有用到shareUserId这个属性呢?那这个和我们今天要介绍的知识点有什么关系吗?其实没什么关系,上面的例子只能说是做一个简单的引子,那有些同学可能困惑了,为何都没有使用shareUserId属性,这两件事还可以做呢?那岂不是很不安全?其实我们在接触过逆向知识的时候会发现,关于Android中的一个App中的代码和资源说的直白点其实没有安全性可言,比如,我想获取一个一个app中的指定资源,可以使用反编译或者直接解压apk就可以得到,想看到app中的一段代码的含义或者执行结果,反编译也可以做到,所以说这个说的直白点关于代码和资源在Android中其实没什么安全性可说。有办法可以去搞定的。

当然我们在后面可以用这种构造Context的方式,去实现我们想要的一些功能,比如我们知道了一个app的资源名或者是方法名,想直接在我的工程中用,那么可以使用这种方式就可以啦,不过这个还是很不靠谱的,当然也是一种方式,比如A应用实现了一个很复杂的一个方法,我自己的应用和他没任何关系,但是也需要这个方法,那么可以直接使用这种方式去调用即可。但是前提是A应用安装了。当然正规公司的app都不会这么傻逼的去做的,其实我们在研究逆向app的时候可能会用到哦~~


那么说了这么多,shareUserId的属性的最大作用是什么呢?

前面都说了,Android中每个app都对应一个uid,每个uid都有自己的一个沙箱,这是基于安全考虑的,那么说到沙箱,我们会想到的是data/data/XXXX/目录下面的所有数据,因为我们知道这个目录下面的所有数据是一个应用私有的,一般情况下其他应用是没有权限访问的,当然root之后是另外情况,这里就不多说了。这里只看没有root的情况,下面我们在来看一个场景:

A应用和B应用都是一家公司的,现在想在A应用中能够拿到B引用存储的一些值,那么这时候该怎么办呢?

这时候就需要用到了shareUserId属性了,但是这里我们在介绍shareUserId属性前,我们先来看一个简单的例子:

还是使用上面的两个工程:

ShareUserIdPlugin中的MainActivity.java代码如下:


这里很简单,我们使用SharedPreferences来存储一个密码,注意模式是:Context.MODE_PRIVATE,关于这里,有很多种模式,后面会详细介绍。


下面在来看一下宿主工程中的代码,获取密码。

运行宿主工程结果:


我们看到运行结果打印出来了几个值,我先不管其他的,看到最后pwd的值是默认值,那说明我们宿主工程中获取插件工程中的密码失败了。

我们在去看看插件工程中那个shareperference的xml文件的权限:


这里使用root了之后查看的:-rw-rw----

关于这个值,不了解的同学可以网上去看一些资料:

Linux文件权限你分开三段来看:
首位代表是目录还是文件,一般不用管,后面的三段每段3位,r代表可读,w代表可写,x代表可执行,第一段是代表文件所属的用户对它的权限,第二段是所属用户组的用户对它的权限,第三段是其他用户对它的权限。
第一段:rw- ,所属用户(比如是root)对这个文件可读可写
第二段:rw- ,所属用户组用户,对这个文件可读可写
第三段:--- ,其他用户对这个文件什么都干不了

那么从上面的分析可以看出来,这个文件对于其他用户(不同uid的)访问是失败的。所以我们获取密码失败。

那么这个xml的权限在哪里设置的呢?其实就是在插件工程中的那个创建SharedPreferences的时候:

其实Context提供了几种模式:

1、Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆 盖原文件的内容,如果想把新写入的内容追加到原文件中。可以使用Context.MODE_APPEND
2、Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件。
3、Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件。
MODE_WORLD_READABLE:表示当前文件可以被其他应用读取;
MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入


我们可以查看源码ContextImpl.java


这里获取一个SharedPreferencesImpl对象,这个对象是实现了SharedPreferences接口的。这里我们看到采用了缓存机制,将xml的名字和sp对象一一对应起来,所以我们可以得知,一个app中,最好简化xml的个数,尽量将值都定义到一个xml中,减少内存占用。

我们在看看SharedPreferencesImpl.java类源码:


有一个全局变量存储了mode值,再看看mMode在哪里用到了:


在writeToFile这个方法中用到了,这个方法其实后面会分析的,就是SP将内存中的值保存到磁盘中。

然后再看看ContextImpl的setFilePermissionsFromMode方法:


好了,到这里,我们可以看到,通过传递进来的mode值,来设置文件的权限。

那么代码看完了,下面我们在改一下插件工程中的那个创建sp的代码:

Context.MODE_WORLD_READABLE|Context.MODE_WORLD_WRITEABLE 为读写模式


再来测试一下:


看到这里取出来密码了,成功了,关于空指针后面会详细介绍的,这里先不管了。我们再来看一下sp的xml文件权限:


看到了,其他用户是可以进行读写操作的了,所以取出来的密码是成功的了。

到这里我们就弄清楚了Context提供的那几个创建sp文件的几种模式的区别,所以我们这里也可以看到,这个模式很重要,对于安全性来说,不过这个默认模式就是private的,也是挺好的。


补充:

第一:不需要root来查看sp文件的权限

前面我们看到我们是使用root之后查看文件的权限的,其实还有一种方式,不root也是可以的,那就是run-as命令,关于这个命令不熟的同学可以自行google了,这个命令的作用是:可以查看指定包名应用的data目录下面的数据,也就是只能查看data/data/XXX/目录下面的内容,而且他的局限性也很大,只有debug模式下才能起作用,下面我们来看看怎么使用:

run-as 需要查看内容的应用包名


是不是这里也是可以查看的,但是他只能在debug下面才能使用,比如我们现在用它去查看非debug的应用:


看到了吧,很蛋疼,非debug模式还不能用。好吧,不过这里只是做了一个知识点的补充,记住有这个命令,在debug环境下也是蛮有用的。


第二:关于上面日志中的异常是怎么回事?

我们回去看看宿主工程中,用反射去访问了SP内部的一些变量值。为什么访问这些呢?源于我之前调试一个bug,但是这里引出来了一些问题,下面就来分析一下。


为了分析,这里我们还是需要去看SharePreferencesImpl源码:


代码逻辑不是很复杂,首先创建备份文件,然后加载xml内容到内存的map对象,用于后面的getXXX方法直接获取值,提高效率,然后将解析之后的map赋值给全局的map对象,如果解析出来的map为空,那么就直接赋值一个空数据的map。最后一行代码很重要,就是需要唤醒其他所有的wait地方,看完这段代码我们就可以很好理解上面的异常崩溃了:


首先文件是可读的,所以进入到了if语句中,开始解析xml到内存中,但是这时候需要注意的是,解析工作实在子线程中工作的,但是我们去访问全局map是在主线程做的,那么这时候解析还没有完成,那么只能获取到null值了,所以抛出一个空指针,但是后面我们使用getString方法的时候,可以获取到正确值了

下面我们来看看getString的源码:


看看awaitLoadedLocked方法:


这个方法什么都没干,就是wait住了,等待唤醒,这个也就和上面的那个notifyAll方法对应起来了。

那么既然都分析到这里了,我们干脆再来看一下常用的commit和apply两个方法吧:

commit方法:


这里主要就连个方法,首先来看看commitToMemory方法,这个是整理提交前的map数据结构,用于写到文件前的操作准备

整理好了内存中的数据,开始写入到磁盘中了,其实commit从内存写文件是在当前调运线程中直接执行的。那我们再来看看这个写内存到磁盘方法中真正的写方法writeToFile:


分析完了commit方法,我们总结一下:

如果用commit()方法提交数据,其过程是先把数据更新到内存,然后在当前线程中写文件操作,提交完成返回提交状态


接下来继续看apply方法:


这里也是调用了enqueueDiskWrite方法:


其实这个方法是commit和apply公用的,主要用isFromSyncCommit来进行区分的,postWriteRunnalbe==null就是commit方式。如果不为null的话,就是apply方式。

总结一下apply方法:

如果用的是apply()方法提交数据,首先也是写到内存,接着在一个新线程中异步写文件,然后没有返回值。

其实这里算是分析完了SharePreferences的源码,我们可以总结如下:

1、SharedPreferences在实例化时首先会从sdcard异步读文件,然后缓存在内存中;接下来的读操作都是内存缓存操作而不是文件操作。
2、在SharedPreferences的Editor中如果用commit()方法提交数据,其过程是先把数据更新到内存,然后在当前线程中写文件操作,提交完成返回提交状态;如果用的是apply()方法提交数据,首先也是写到内存,接着在一个新线程中异步写文件,然后没有返回值。

3、由于上面分析了,在写操作commit时有三级锁操作,所以效率很低,所以当我们一次有多个修改写操作时等都批量put完了再一次提交确认,这样可以提高效率。


上面算是开了一个小差,顺道分析了一下SharePreferences的源码,下面来说正题了,我们在上面的例子已经知道了,通过设置Context的文件创建模式来设置安全性。那么现在如果我们想让A应用访问到B应用的数据,我们可以这么做:把B应用创建模式改成可读模式的,那么A应用就可以操作了,那么这就有一个问题,A应用可以访问了,其他应用也可以访问了,这样所有的应用都可以访问B应用的沙盒数据了,太危险了,所以要用另外的一种方式,那么这时候就要用到shareUserId属性了,我们只需要将B应用创建方式还是private的,然后A应用和B应用公用一个uid即可,我们下面就来修改一下代码,还是上面的那两个工程,修改他们的AndroidManifest.xml,添加shareUserId即可。


这时候,我们发现把ShareUserIdPlugin中的模式改成private的,A应用任然可以访问数据了,其实也好理解,他们两个的uid都相同了,A的文件就是B的,B的就是A的了,他们两个没有沙盒的概念了,数据也是透明的了。

所以这里我们就看到了,使用shareUserId可以达到多个应用之间的数据透明性互相访问。


那么问题来了,假如现在我手机没有root,想访问某个应用的沙盒数据,我把自己的应用修改成和他一样的shareUserId即可。

注意:这里有一个误点,就是这里所有的修改的前提是这个应用的AndroidManifest.xml本身就定义了这个属性,然后我们可以反编译看到这个值,把我们自己的shareUserId改成他的就可以了,但是如果这个应用本身没有这个属性,那么这里就没有办法的,为什么呢,如果要添加,那就是另外一条路了,就是逆向,修改AndroidManifest.xml之后,还需要从新打包在验证,但是这时候没必要了,我们也知道有时候回编译还是很艰难的,如果都能回编译了,那都不需要这些工作了,所以这里需要注意的一个前提

那么修改之后是不是真的可以呢?

答案是肯定不可以的,如果可以的话,那google也太傻比了,其实Android系统中有一个限制,就是说如果多个应用的uid相同的话,那么他们的apk签名必须一致,不然是安装失败的,如下错误:


我们可以查看PackageManagerService.java源码:


看到了,这里会作比较的,不过这里我们在深入看一下这个方法的调用链:


在scanPackageLI方法中调用的verifySignaturesLP方法,那么scanPackageLI方法在哪调用的呢?继续跟踪:


在这里,这里其实是一个文件监听类AppDirObserver:


这里会监听/data/app目录,如果有新的文件增加,就会调用scanPackageLI方法,然后在调用verifySignaturesLP方法来进行验证apk文件信息。同时我们也发现了,系统的安装和卸载apk的广播也是在这里发送的。果然这里的知识点还是很多的。

通过上面的分析,我们就知道了,Android中是不允许相同的uid的不同签名的应用。

那么我们上面的猜想就是失败的。及时改成目标应用相同的shareUserId,也是安装不成功的。


四、知识梳理

1、我们知道如何通过包名来构建一个Context,同时需要注意两种模式:

Context.CONTEXT_INCLUDE_CODE和Context.CONTEXT_IGNORE_SECURITY

构造完成之后,我们可以访问资源和执行一些模块代码,这些其实不算是一个应用的沙盒概念了,所以不会牵扯到shareUserId的知识点。

2、我们在实验A应用去访问B应用的SharedPreferences中的值时,发现创建sp的xml有几种模式:

Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容,如果想把新写入的内容追加到原文件中。可以使用Context.MODE_APPEND
Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件。
Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件。

这三种模式的区别,我们最保险的操作就是设置成private的,不过默认也是这种模式

3、我们通过分析SharedPreferences的源码,知道这三种模式对应的就是设置xml文件的访问权限,同时我们顺便分析了commit,apply,getXXX等方法的实现,也算是对SP的更深入的理解了。其实SharedPreferences内部为了高效率,会第一次加载xml内容到内存中的map中,每次getXXX数据的时候,都是直接从map中取,每次保存数据,是首先保存到内存的map中,调用commit和apply方法只有在将数据写入到磁盘中的区别。apply是异步的没有返回值,commit是同步的有返回值

4、我们再次实验使用shareUserId属性来做到多个应用之间的数据共享和透明性,同时我们也做了一个猜想就是把自己的shareUserId修改成和目标应用相同来访问目标应用的数据,但是这个猜想是错误的,因为我们通过分析PackageManagerService源码知道,Android中是不允许相同的shareUserId的应用有着不同的签名文件的,会出现安装失败的情况。


五、遗留的问题

关于文件创建还有一种模式:Context.MODE_MULTI_PROCESS,这个模式其实我们知道是用来多进程访问的,这里关于源码就不在分析了,在ContextImpl.java中的getSharedPreferences方法中会做一次多进程的数据刷新加载操作:


不过这个方法已经废弃了,google建议还是使用ContentProvider比较靠谱,同样,上面的Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE这两种模式也是被废弃了,也算是google为了增强安全性考虑吧。


六、总结

这篇文章就介绍了使用sharedUserId属性,来实现我们想要的应用数据共享效果,但是引出来的知识点有点多,所以说的就有点多了,不过我们就记住一点:

在创建文件时,一定要设置成Context.MODE_PRIVATE或者是Context.MODE_APPEND模式,为了做到应用的数据共享可以考虑shareUserId属性。同时Android中是不允许相同的sharedUserId有着不同签名的应用的,会出现安装失败。

分析的好累呀~~,跪求点赞啦啦~~


更多详细内容可以 点击这里

PS: 关注微信,最新Android技术实时推送




更多相关文章

  1. 浅谈Java中Collections.sort对List排序的两种方法
  2. NPM 和webpack 的基础使用
  3. Python list sort方法的具体使用
  4. 【阿里云镜像】使用阿里巴巴DNS镜像源——DNS配置教程
  5. python list.sort()根据多个关键字排序的方法实现
  6. 2014非常好用的开源Android测试工具
  7. Android的多媒体框架OpenCore介绍
  8. Android(安卓)Studio下的目录结构
  9. android依赖工程 java build path android工程导出jar

随机推荐

  1. Android.StructureOfAndroidSourceCodeRo
  2. Android http文件上传-本地+服务器一条龙
  3. android 多个activity 共用一套广播时,写
  4. Android获取sdcard信息
  5. Android 优化
  6. Android 创建与解析XML(三)—— Sax方式(源
  7. android crash自动化分析
  8. android 监听网络连接状态的改变
  9. Android软键盘的弹出与隐藏
  10. Android ListView 局部刷新