数据存储
存储方式 | 说明 |
---|---|
SharedPreferences | 在键值对中存储私有原始数据 |
内部存储 | 在设备内存中存储私有数据 |
外部存储 | 在共享的外部存储中存储公共数据 |
SQLite 数据库 | 在私有数据库中存储结构化数据 |
SharedPreferences
SharedPreferences采用key-value(键值对)形式, 主要用于轻量级的数据存储, 尤其适合保存应用的配置参数, 但不建议使用SharedPreferences来存储大规模的数据, 可能会降低性能.
SharedPreferences采用xml文件格式来保存数据, 该文件所在目录位于/data/data/<package name>/shared_prefs
,如:
1 |
|
从Android N开始, 创建的SP文件模式, 不允许MODE_WORLD_READABLE
和MODE_WORLD_WRITEABLE
模块, 否则会直接抛出异常SecurityException。MODE_MULTI_PROCESS
这种多进程的方式也是Google不推荐的方式, 后续同样会不再支持。
当设置MODE_MULTI_PROCESS模式, 则每次getSharedPreferences过程, 会检查SP文件上次修改时间和文件大小, 一旦所有修改则会重新从磁盘加载文件.
获取方式
getPreferences
Activity.getPreferences(mode): 以当前Activity的类名作为SP的文件名. 即xxxActivity.xmlActivity.java
1 | public SharedPreferences getPreferences(int mode) { |
getDefaultSharedPreferences
PreferenceManager.getDefaultSharedPreferences(Context): 以包名加上_preferences作为文件名, 以MODE_PRIVATE模式创建SP文件. 即packgeName_preferences.xml.
1 | public static SharedPreferences getDefaultSharedPreferences(Context context) { |
getSharedPreferences
直接调用Context.getSharedPreferences(name, mode),所有的方法最终都是调用到如下方法:
1 | class ContextImpl extends Context { |
架构
SharedPreferences与Editor只是两个接口. SharedPreferencesImpl和EditorImpl分别实现了对应接口. 另外, ContextImpl记录着SharedPreferences的重要数据。
putxxx()
操作把数据写入到EditorImpl.mModified;
apply()/commit()
操作先调用commitToMemory(`, 将数据同步到SharedPreferencesImpl的mMap, 并保存到MemoryCommitResult的mapToWriteToDisk,再调用enqueueDiskWrite(), 写入到磁盘文件; 先之前把原有数据保存到.bak为后缀的文件,用于在写磁盘的过程出现任何异常可恢复数据;
getxxx()
操作从SharedPreferencesImpl.mMap读取数据.
apply / commit
- apply没有返回值, commit有返回值能知道修改是否提交成功
- apply是将修改提交到内存,再异步提交到磁盘文件,而commit是同步的提交到磁盘文件
- 多并发的提交commit时,需等待正在处理的commit数据更新到磁盘文件后才会继续往下执行,从而降低效率; 而apply只是原子更新到内存,后调用apply函数会直接覆盖前面内存数据,从一定程度上提高很多效率。
SharedPreference源码
注意
- 强烈建议不要在sp里面存储特别大的key/value,有助于减少卡顿/anr
- 不要高频地使用apply,尽可能地批量提交
- 不要使用MODE_MULTI_PROCESS
- 高频写操作的key与高频读操作的key可以适当地拆分文件,由于减少同步锁竞争
- 不要连续多次edit(),应该获取一次获取edit(),然后多次执行putxxx(),减少内存波动
第三方-数据库
GreenDao相关
关系注解
在greenDAO,实体使用@to-one
或@to-many
关联起来
@ToOne
一对一
1 |
|
1 |
|
外键约束语句:
constraint FK_name foreign key(customerId) references Customer(id);
@ToOne
定义到另一个实体对象的关系,应在持有目标实体对象的字段上使用该注解。
如一个Order对一个Customer,应在实体Order的customer字段使用@ToOne
在实体内部,需要定义一个属性来指向目标实体的ID,也就是定义外键(如Order实体的customerId),
使用@ToOne的joinProperty参数来指明外键
如果改变了外键属性(customerId),下次调用getter(getCustomer())时会更新实体;如果设置了新的实体(setCustomer()),外键也会更新
第一次调用to-one关系的getter方法(getCustomer())时会延迟加载,在随后的调用会立即返回之前加载的对象。
@ToMany
一对多@ToMany
定义一对多关系(一对一个其他实体的集合),使用@ToMany
的属性代表目标实体的List,集合里的对象都必须至少有一个属性指向拥有@ToMany
的实体
referencedJoinProperty指定目标实体的外键
1 | @Entity |
1 |
|
joinProperties parameter
对于更复杂的关系,可以指定一个@joinproperty
注释列表,每个@joinproperty
需要原始实体中的源属性和目标实体中的引用属性。
1 | @Entity |
1 |
|
@JoinEntity
多对多
如果两个实体是多对多的关系,那么需要第三张表(表示两个实体关系的表)
例如学生与课程是多对多关系,需要一个选课表来转成两个一对多的关系(学生与选课是一对多,课程与选课是一对多) 所以一定要配合 @ToMany
一起使用
1 | @Entity |
1 |
|
1 |
|
获取和更新To-many关系
To-many在首次请求时会延迟加载,之后,相关的实体缓存到源实体的内部List对象中。随后的访问不再查询数据库,而是直接从缓存中获取。所以当数据库更新to-many关系后,需要手动更新to-many的list缓存。
1 | // 插入新的实体之前获取to-many list,否则新实体可能在list中出现两次 |
同样,可以删掉关联的实体
1 | List<Order> orders = customer.getOrders(); |
当添加、更新或删除很多关联实体时可以使用reset方法来清掉缓存,然后调用getter方法时会重新查询
1 | // clear any cached list of related orders |
基础属性注解
@Id
主键,选择使用Long,必须是大写的 L 可以通过@Id(autoincrement = true)
设置自增长,引用一下官方文档的说明
Currently, entities must have a long or Long property as their primary key. This is recommended practice for Android and SQLite.
To work around this, define your key property as an additional property, but create a unique index for it:
1 |
|
@Property
为该属性映射的列设置一个非默认的名称,默认是将单词大写,用下划线分割单词,如属性名customName对应列名CUSTOM_NAME
1 | @Property(nameInDb = "USERNAME") |
@NotNull
表明这个列非空,通常使用@NotNull
标记基本类型(long,int,short,byte)
,然而可使用包装类型(Long, Integer, Short, Byte)
使其可空
1 |
|
@Transient
表明此字段不存储到数据库中,用于不需要持久化的字段,比如临时状态
1 |
|
实体注解
@Entity
实体注解,为greendao指明这是一个需要映射到数据库的实体类
1 | @Entity( |
索引注解
@Index
为相应的列创建索引
name 如果不想使用greenDAO为该属性生成的默认索引名称,可通过name设置
unique 给索引添加唯一性约束
1 | @Index(unique = true) |
@Unique
为相应列添加唯一约束,注意,SQLite会隐式地为该列创建索引
1 | @Unique |
LitePal相关笔记
使用步骤
添加依赖
1 | // 添加jitPack仓库使用 |
在src/main/assets下新建 litepal.xml
1 |
|
实体类 bean 需要 extends DataSupport;另外还需要加个主键
1 | private int id; |
存取调用
1 | public static UserBean cust = null;//新的用户实体类 |
网上说 DataSupport 被弃用,可用 LitePalSupport 来替代
MMKV
Internal和External Storage
现在大部分设备只有一个存储区域,即内置存储,分成 Internal 和 External 两个分区。
区别 | Internal storage | External storage |
---|---|---|
可见性 | 一直可见 | 不总是可见,mount了就可见,remove了就不可见 |
访问权限 | 保存在这里的文件默认只有对应app才能访问 | 其他app共享 |
卸载表现 | 保存在此的与app相关的文件会被删除 | 保存在这里的文件除了在getExternalFilesDir()和getExternalCacheDir()文件夹之外,都会保留 |
权限 | 无需 | 需 WRITE_EXTERNAL_STORAGE权限 (注意:getExternalFilesDir()和getExternalCacheDir()这两个路径无需权限) |
适用条件 | app私有数据 | 存放的数据不需要访问限制,为了分享数据或允许其他用户访问 |
通过Context.getExternalFilesDir()方法可以获取到 SDCard/Android/data/应用包名/files/ 目录,一般放一些长时间保存的数据【设置->应用->应用详情里面的“清除数据”】
通过Context.getExternalCacheDir()方法可以获取到 SDCard/Android/data/应用包名/cache/目录,一般存放临时缓存数据【设置->应用->应用详情里面的与“清除缓存”】
一些路径的标准写法
1 | Environment.getExternalStorageDirectory() = /mnt/sdcard |
file_path.xml
Android中有以下几种用于访问存储设备的路径
Context.getExternalFilesDir()
:获取外部应用程序存储空间中的应用程序特定的目录。应用程序在该目录中创建的文件只能该应用程序访问,别的应用程序无法访问。这个路径通常用于存储应用程序相关的私人数据,如:文件缓存、用户数据等。通常,应用程序被卸载时,这个目录内容也会被删除。Context.getExternalCacheDir()
:获取外部应用程序存储空间中的应用程序特定的缓存目录。应用程序通常在这个目录中存储临时文件,如图片缓存等。这个目录下的文件也只能由该应用程序访问。Environment.getExternalStorageDirectory()
:获取外部存储设备的根目录,通常是/sdcard
或/storage/emulated/0
。该目录是存储设备的根目录,所有应用程序皆可访问该目录,但使用时应该注意权限,因为该目录中的文件可能是其它应用程序使用的。在 Android 10 及以上,使用此方法无法直接访问存储设备的根目录,需要改用其他方式进行访问。Context.getExternalMediaDirs()
:获取外部存储设备中相对应用程序的媒体文件目录。媒体文件包括图片、视频、音频等,应用程序可以在该目录中对媒体文件进行保存和访问。这个目录是与应用程序私有相关联的,其他应用程序无法访问该目录中的文件。需要权限android.permission.WRITE_EXTERNAL_STORAGE
和android.permission.READ_EXTERNAL_STORAGE
。
1 | <!--上面两个一般不用,可能在某个版本中就会被废弃--> |
<root-path>
:在应用程序中访问存储设备的根目录。供 fileProvider 使用。需要注意的是,这种方法在 Android 10 及以上版本已被弃用。
调用:
1 | //Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);<external-files-path name="hm_external_files" path="Pictures" /> |
存储相关问题
Android 也需要防止 SQL 注入
背景:
在移动端发现一个用户无法登陆的 bug,经排查,发现是不规范的 Sql 语句以及没做 SQL 攻击的防止导致的。
出现的原因及分析
大多数情况下,我们不需要考虑移动端 SQL 攻击,因为 Android SDK提供的 query/insert/update/delete 等 API 是安全的,但是execSql 这个 API 却不是安全的,尽量别使用这个 API
由于我们的代码,原先的数据存储是靠C++的sqlite,而没借助Android的Sdk来做的。因此,原先一些SQL可能存在问题。
例子
1 | String userName = ""; |
类似上面的代码,我想 大家的项目里或许也有。看上去,这个没有任何问题,但是却存在sql攻击,当我把userName这个字符串的内容换成,带特殊字符的,如 guolei’ 的时候,这个拼接出来的sql语句就有问题了,回报如下错误。
1 | Process: com.guolei.sqlinsert, PID: 22520 |
这是因为单引号 ‘ 使sql语句发生了截断,在sql语句中,要用连续两个单引号去表达一个单引号字符。这仅仅是一个例子。有兴趣的,大家去了解SQL攻击相关的东西吧。