一、前言
今天我们来看一下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有着不同签名的应用的,会出现安装失败。
分析的好累呀~~,跪求点赞啦啦~~
更多内容:点击这里
关注微信公众号,最新Android技术实时推送