性能优化之存储优化

一、安卓4种存储方式

安卓系统4种基本的存储方式

  • 使用SharedPreferences存储数据
  • 文件存储数据
  • SQLite数据库存储数据
  • 使用ContentProvider存储数据
image.png

1、 使用SharedPreferences存储数据

推荐阅读官方文档

  • 安卓轻量级的存储类,使用简单,速度快
  • 常用于存储配置、状态信息,如onSaveInstanceState
  • 主要保存类型有:boolean,int,float,long和String。以Key-Value键值对形式存储
  • 存储位置在:/data/data/<包名>/shared_prefs目录下。
  • 处理方式

以XML形式保存,xml 处理时Dalvik会通过自带底层的本地XML Parser解析,比如XMLpull方式,这样对于内存资源占用比较好。

相关API和参数

1、getSharedPreferences(String name, int mode)

获取SharedPreferences实例,name 要操作的xml文件名,modec参数具体如下:

Context.MODE_PRIVATE: 默认状态,只能由创建该文件的应用程序调用,即为私有的。
Context.MODE_APPEND :如果改文件已经存在,就将数据写入,而不是抹掉现有文件的末尾。
Context.MODE_WORLD_READABLE: 允许所有其他应用程序读取和创建文件的权限。
Context.MODE_WORLD_WRITEABLE: 允许所有其他应用程序写入、访问、创建文件的权限。

2、SharedPreferences 主要重要方法

Editor edit() //获取Editor对象
getXXX() //获取数据

3、Editor主要重要方法

SharedPreferences.Editor clear():清空SharedPreferences里所有数据
SharedPreferences.Editor putXxx(String key , xxx value): 向SharedPreferences存入指定key对应的数据,其中xxx 可以是boolean,float,int等各种基本类型据
SharedPreferences.Editor remove(): 删除SharedPreferences中指定key对应的数据项
boolean commit(): 当Editor编辑完成后,使用该方法同步提交修改
void apply();当Editor编辑完成后,使用该方法异步提交修改

原本只有commit()这一种提交方法,但因为这是一个同步的方法,若数据量大会堵塞UI线程,所以Google后来折腾出了apply()这么一个异步的提交方法,因此推荐使用apply方法进行提交


image.png
实现步骤
数据存储
  • 根据Context.getSharedPreferences(String name, int mode)方法来获取SharedPreferences实例
  • 调用edit()获取Editor对象
  • 通过Editor存储key-value键值对数据
  • 调用Editor对象的apply/commit方法提交数据,完成写入
      SharedPreferences sp = ctx.getSharedPreferences("lock", MODE_PRIVATE);
        //存入数据
        Editor editor = sp.edit();
        editor.putString("STRING_KEY", "string");
        editor.putInt("INT_KEY", 0);
        editor.putBoolean("BOOLEAN_KEY", true);
        editor.apply();

代码执行后,会在/data/data/<包名>/shared_prefs目录下生产lock.xml文件。

数据读取
  • 根据Context.getSharedPreferences(String name, int mode)方法来获取SharedPreferences实例
  • 调用getXXX读取数据
          //步骤1:创建一个SharedPreferences接口对象
                SharedPreferences read = getSharedPreferences("lock", MODE_WORLD_READABLE);
                //步骤2:获取文件中的值
                String value = read.getString(STRING_KEY, "");
SharedPreferences对象与SQLite数据库相比

优点:操作简单,方便。因为免去了创建数据库、建表、写sql语句的麻烦
缺点:只能存储boolean,int,float,long和String五种简单的数据类型,进行简单的存取,无法按条件查询

2 、文件存储数据

  • 适用于读写大量的流式数据,如媒体文件(图片,音视频等)和其他网络传输内容
  • 缺点是更新数据麻烦
image.png

首先弄清楚内存、内部存储、外部存储、机身存储(内置存储)。

1、内存:Ram是计算机中重要的部件之一,它是与CPU沟通的桥梁。计算机中所有程序运行都是在内存中进行,所以说它是用于计算机运行时的,它不是用来存储数据的。
2、机身存储:也就是Rom,4.4以前只有内部存储,4.4以后包含内部存储和外部存储。
3、机身外部存储、外置SD卡都是安卓系统的外部存储

也就是说外部存储可以是机身外部存储和SD卡。

推荐阅读官方文档:文件保存应用安卓位置

内部存储和外部存储的区别

  • 内部存储,保存的文件只有自己应用才能访问
  • 内部存储,在应用卸载时,系统会从内部存储中删除所有应用程序的文件。
  • 外部存储,是可以移除的,所以使用外部存储,需要判断是否已挂载

应用的配置文件可以放内部存储,因为不需要访问限制,需要分享给其他应用的文件则放在外部存储。

应用默认安装到内存存储,可以在清单文件中指定android:installLocation属性来使应用安卓到外部存储空间

存储.png
  • 机身内存存储包含:持久存储和缓存
  • 机身外部存储包含:公共存储(所有应用都可以使用),私有存储。而私有存储包含持久存储和缓存,私有存储是应用私有的,当应用卸载,所有文件也跟着删除。

1、内部存储

方法 说明 路径
getFilesDir 返回File表示应用程序的内部目录。 /data/data/包名/files
getCacheDir 返回File表示应用程序临时缓存文件的内部目录。 /data/data/包名/cache

A、持续化存储

可以使用File()或者调用openFileOutput()以获取FileOutputStream

public FileOutputStream openFileOutput(String name, int mode) 的 mode

  • MODE_PRIVATE:其成为您应用的私密状态
  • MODE_WORLD_READABLE以及 MODE_WORLD_WRITEABLE:已弃用,可以用内容提供者来实现这方面功能

部分代码:

    // 调用openFileOutput()以获取FileOutputStream 写入内部目录中的文件的内容
    FileOutputStream outputStream = this.openFileOutput("test.txt", Context.MODE_PRIVATE);

    //或者在目录中创建新文件
    File file = new File(getFilesDir(), "test.txt");
    outputStream = new FileOutputStream(file);

保存结果:


image.png

B、非持续化存储(缓存)

  • 确保在不再需要时删除每个文件,并对在任何给定时间使用的内存量(例如1MB)实施合理的大小限制。

  • 系统存储空间不足,则可能会在没有警告的情况下删除缓存文件。

目录获取 路径
getCacheDir /data/data/包名/cache
getCodeCacheDir /data/data/包名/code_cache
文件获取方式一:

public static File createTempFile(String prefix, String suffix,File directory)

  • prefix:前缀,suffix:后缀,directory:目录

  • createTempFile获取的文件,为确保唯一性,会在prefix后面增加一串数据。

部分代码:

File file = File.createTempFile("testCache", ".txt", getCacheDir());

保存结果:


image.png
文件获取方式二:

在目录中创建新文件

部分代码

File file = new File(getFilesDir(), "test.txt");

结果


image.png

2、外部存储

  • 分为公共存储和私有存储
  • 需要请求存储权限和验证存储是否可用

注意事项:外部文件并非始终可用的,建议将重要的文件存储在内部存储。

请求存储权限

<manifest ... > 
<uses-permission android:name = “android.permission.WRITE_EXTERNAL_STORAGE” />     ...
 </ manifest>
      

注意:

  1. 请求写权限,会隐式拥有读取外部存储的权限READ_EXTERNAL_STORAGE
  2. 从Android 4.4(API级别19)开始,getExternalFilesDir()-does 访问 不需要READ_EXTERNAL_STORAGE 或WRITE_EXTERNAL_STORAGE 权限。、

验证外部存储是否可用

通过调用查询外部存储状态getExternalStorageState()。如果返回状态为MEDIA_MOUNTED,则可以读取和写入文件。如果是MEDIA_MOUNTED_READ_ONLY,则只能读取文件。

/* Checks if external storage is available for read and write */
public boolean isExternalStorageWritable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }
    return false;
}

/* Checks if external storage is available to at least read */
public boolean isExternalStorageReadable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}

A、公共存储

使用getExternalStoragePublicDirectory()方法获取File表示外部存储上相应目录

public static File getExternalStoragePublicDirectory(String type)

type有:DIRECTORY_MUSIC、DIRECTORY_PODCASTS、DIRECTORY_RINGTONES、DIRECTORY_ALARMS、DIRECTORY_NOTIFICATIONS、DIRECTORY_PICTURES、DIRECTORY_MOVIES、DIRECTORY_DOWNLOADS、DIRECTORY_DCIM、DIRECTORY_DOCUMENTS

注意事项:正确传递type参数很重要,确保系统正确处理文件。例如,保存在的文件 DIRECTORY_RINGTONES被系统媒体扫描仪分类为铃声而不是音乐。

部分代码:

File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),albumName);

结果


image.png

B、私有存储

  • 私有存储,只能自己访问,别的程序不能访问。
  • 程序卸载的时候,会将外部私有存储文件也删除。
a、持续化存储

部分代码

File file = new File(getExternalFilesDir(DOWNLOAD_SERVICE), "test.txt");

效果:


image.png
b、非持续化存储(缓存)

文件保存部分代码

File file = new File(getExternalCacheDir(), "test.txt");

效果


image.png

还可以通过调用getExternalFilesDir()并向其传递一个名称来指定自己喜欢的目录类型,从而获取仅由您的应用程序使用的目录。
部分代码

file = new File(getExternalFilesDir(DOWNLOAD_SERVICE), "test111");

效果


image.png

文件存储目录如下:

目录获取 说明 路径
getDataDir 内部存储目录 /data/data/包名
getFilesDir 内部存储下的files目录 /data/data/包名/files
getCacheDir 内部存储下的cache目录 /data/data/包名/cache
getCodeCacheDir 内部存储下的code_cache目录 /data/data/包名/code_cache
getNoBackupFilesDir 内部存储下的no_backup目录 /data/data/包名/no_backup
getExternalMediaDirs()[0] 外部media存储目录 /storage/emulated/0/Android/media/包名
getObbDir 外部obb存储目录 /storage/emulated/0/Android/obb/包名
getExternalCacheDir 外部存储下的cache目录 /storage/emulated/0/Android/data/包名/cache
getExternalFilesDir(DOWNLOAD_SERVICE) 外部存储下的files/download目录 /storage/emulated/0/Android/data/包名/files/download
Environment.getDataDirectory data目录 /data
Environment.getRootDirectory system目录 /system
Environment.getDownloadCacheDirectory data/cache目录 /data/cache
Environment.getExternalStorageDirectory 外部存储目录 /storage/emulated/0
Environment.getExternalStoragePublicDirectory(XXX) 外部公共存储目录 /storage/emulated/0/XXX

三、补充

1、在多个存储位置之间选择

外部存储有机身外部存储和SD存储

这意味着该设备有两个不同的外部存储目录,因此您需要选择在将“私有”文件写入外部存储时使用哪个目录。

  • 从Android 4.4(API级别19)开始,您可以通过调用访问这两个位置getExternalFilesDirs(),该位置 返回一个File包含每个存储位置条目的数组。阵列中的第一个条目被视为主要外部存储,您应该使用该位置,除非它已满或不可用。
  • 如果您的应用支持Android 4.3及更低版本,则应使用支持库的静态方法ContextCompat.getExternalFilesDirs()。这总是返回一个File数组,但是如果设备运行的是Android 4.3及更低版本,那么它只包含一个主外部存储条目(如果有第二个存储位置,则无法在Android 4.3及更低版本上访问它)。
image.png

推荐阅读:彻底搞懂Android文件存储---内部存储,外部存储以及各种存储路径解惑

2、删除文件和查询可用空间

1、删除文件

myFile.delete ();

或者

myContext.deleteFile (fileName );

2、查询可用空间

用File类的getFreeSpacegetTotalSpace和方法

  • public long getFreeSpace() 返回指定的分区中未分配的字节数。
  • public long getTotalSpace () 返回指定的分区的大小。
File dir = getExternalCacheDir();
Log.e(TAG,"TotalSpace="+dir.getTotalSpace());
Log.e(TAG,"FreeSpace="+dir.getFreeSpace());

3、卸载应用会删除哪些文件?

  • 您在内部存储上保存的所有文件。
  • 使用保存外部存储的所有文件getExternalFilesDir()。

但是,我们应手动删除getCacheDir()定期创建的所有缓存文件, 并定期删除不再需要的其他文件。

4、java mkdir()和mkdirs()区别

mkdirs()可以建立多级文件夹, mkdir()只会建立一级的文件夹, 如下:

new File("/tmp/one/two/three").mkdirs();
执行后, 会建立tmp/one/two/three四级目录

new File("/tmp/one/two/three").mkdir();
则不会建立任何目录, 因为找不到/tmp/one/two目录, 结果返回false

3、SQLite

(1)SQLite介绍

SQLite是一个轻量级的数据库,不是一个C/S结构的数据库引擎,而是被集成到用户程序中。这个库也被动态链接,应用程序中直接调用相关API来使用SQLite功能,比跨进程通通信更有效率。SQLite将整个数据库作为一个单独的、可跨平台使用的文件存储在主机中

SQLite 是在世界上最广泛部署的 SQL 数据库引擎。像Android、IOS等移动操作系统中的数据库实现都是用SQLite

优点:

  • 无服务器、零配置、事务性。
  • 存储在一个单一磁盘文件中的一个完整数据库
  • 数据库文件可以在不同字节顺序的机器自由共享
  • 支持数据库大小2TB
  • 足够小,全部源代码250KB
  • 对数据操作快
  • 开源

特点:轻量级、独立性、隔离性、跨平台、多语言接口、线程安全性

优势:

  • 多线程访问数据、
  • 需要事务处理、
  • 应用需要处理变化复杂的数据结构、
  • 数据库对于创建它们的包套件是私有的。
    结构
image.png

1.接口(Interface)

接口由SQLite C API组成,也就是说不管是程序、脚本语言还是库文件,最终都是通过它与SQLite交互的(我们通常用得较多的ODBC/JDBC最后也会转化为相应C API的调用)。

2.编译器(Compiler)

在编译器中,分词器(Tokenizer)和分析器(Parser)对SQL进行语法检查,然后把它转化为底层能更方便处理的分层的数据结构---语法树,然后把语法树传给代码生成器(code generator)进行处理。而代码生成器根据它生成一种针对SQLite的汇编代码,最后由虚拟机(Virtual Machine)执行。

3.虚拟机(Virtual Machine)

架构中最核心的部分是虚拟机,或者叫做虚拟数据库引擎(Virtual Database Engine,VDBE)。它和Java虚拟机相似,解释执行字节代码。VDBE的字节代码由128个操作码(opcodes)构成,它们主要集中在数据库操作。它的每一条指令都用来完成特定的数据库操作(比如打开一个表的游标)或者为这些操作栈空间的准备(比如压入参数)。总之,所有的这些指令都是为了满足SQL命令的要求(关于VM,后面会做详细介绍)。

4.后端(Back-End)

后端由B-树(B-tree),页缓存(page cache,pager)和操作系统接口(即系统调用)构成。B-tree和page cache共同对数据进行管理。B-tree的主要功能就是索引,它维护着各个页面之间的复杂的关系,便于快速找到所需数据。而pager的主要作用就是通过OS接口在B-tree和Disk之间传递页面。

参考:

SQLite剖析之体系结构

(2)SQL语言学习

参考:

SQLite 教程

(3)SQListeOpenHelper

是一个数据库创建和版本管理的帮助类

SQListeOpenHelper子类需实现onCreate和onUpgrade方法

getWritableDatabase() / getReadableDatabase() 第一次被调用时才会进行数据库创建 / 打开

/**
 * 创建数据库子类,继承自SQLiteOpenHelper类
 * 需 复写 onCreate()、onUpgrade()
 */
public class MySQListOpenHelper extends SQLiteOpenHelper {

    public static final String USER_TABLE_NAME = "user";
    public static final String JOB_TABLE_NAME = "job";
    //数据库版本号
    private static Integer Version = 1;

    public MySQListOpenHelper(Context context,String name,int version)
    {
        this(context,name,null,version);
    }

    public MySQListOpenHelper(Context context,String name)
    {
        this(context, name, Version);
    }

    public MySQListOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    /**
     * 数据库第1次创建时 则会调用
     * 即 第1次调用 getWritableDatabase() / getReadableDatabase()时调用
     **作用:创建数据库 表 & 初始化数据
     * SQLite数据库创建支持的数据类型: 整型数据、字符串类型、日期类型、二进制
     */
    @Override
    public void onCreate(SQLiteDatabase db) {
        // 创建数据库1张表
        // 通过execSQL()执行SQL语句
        String sql = "create table "+USER_TABLE_NAME+"(id int primary key,name varchar(200))";
        String sql1 = "create table "+JOB_TABLE_NAME+"(id int primary key,name varchar(200))";
        db.execSQL(sql);
        db.execSQL(sql1);

        // 注:数据库实际上是没被创建 / 打开的(因该方法还没调用)
        // 直到getWritableDatabase() / getReadableDatabase() 第一次被调用时才会进行创建 / 打开
    }

    /**
     * 复写onUpgrade()
     * 调用时刻:当数据库升级时则自动调用(即 数据库版本 发生变化时)
     * 作用:更新数据库表结构
     * 注:创建SQLiteOpenHelper子类对象时,必须传入一个version参数,该参数 = 当前数据库版本, 若该版本高于之前版本, 就调用onUpgrade()
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 参数说明:
        // db : 数据库
        // oldVersion : 旧版本数据库
        // newVersion : 新版本数据库

        // 使用 SQL的ALTER语句
        String sql = "alter table user add sex varchar(8)";
        db.execSQL(sql);
    }
}

因为SQLite对多线程支持并不完善,所以需要用一个单例来管理SQListeOpenHelper,以保证线程安全。

package com.example.lenovo.mpplication.data.provider;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class BDManager {

    private static SQLiteOpenHelper mMySQListOpenHelper;
    private static SQLiteDatabase mDB;

    private static SQLiteOpenHelper getDatabaseHelper(Context context, String name) {
        if (null == mMySQListOpenHelper) {
            mMySQListOpenHelper = new MySQListOpenHelper(context, name)
        }
        return mMySQListOpenHelper;
    }

    public static synchronized SQLiteDatabase getDB(Context context, String name) {
        if (null == mDB) {
            mDB = getDatabaseHelper(context, name).getReadableDatabase();
        }
        return mDB;
    }

    public static synchronized void initDB(Context context, String name) {
        getDB(context, name);
    }
}

(4)SQLiteDatabase

SQLiteDatabase提供了基本的增删改查方法,可以执行SQL命令,执行其他常见数据库管理任务的方法。

常用方法:

close() // 关闭数据库
(Cursor) rawQuery(String sql, String[] selectionArgs) //运行一个预置的SQL语句,返回带游标的数据集(与上面的语句最大的区别 = 防止SQL注入)
(int) delete(String table,String whereClause,String[] whereArgs) // 删除数据行
(long) insert(String table,String nullColumnHack,ContentValues values) // 添加数据行
(int) update(String table, ContentValues values, String whereClause, String[] whereArgs) // 更新数据行
(void) execSQL(String sql) // 执行一个SQL语句,可以是一个select or 其他sql语句
(Cursor) query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) // 查询指定的数据表返回一个带游标的数据集。
// 各参数说明:
// table:表名称 ,colums:列名称数组 ,selection:条件子句,相当于where ,selectionArgs:条件语句的参数数组
// groupBy:分组 , having:分组条件 , orderBy:排序类 , limit:分页查询的限制 , Cursor:返回值,相当于结果集ResultSet

注意:对SQLiteDatabase数据库操作需统一到同一个线程队列管理,而业务层使用缓存同步,避免多线程操作数据库导致不同步和死锁问题

参考:

5、ContentProvider

定义:内容提供者,是 Android 四大组件之一

作用:对外共享数据

使用ContentProvider的好处是统一了数据访问方式,实际上是对SQListOpenHelper的进一步封装,通过Uri映射来判断要操作数据库哪个表

(1)统一资源标识符(URI)

image.png
  • schema:固定为content://
  • authority:ContentProvider的唯一标记,调用者可以通过这个找到它
  • path:要操作的数据库表
  • id:表中的某个记 录
// 设置URI
Uri uri = Uri.parse("content://com.carson.provider/User/1") 
// 上述URI指向的资源是:名为 `com.carson.provider`的`ContentProvider` 中表名 为`User` 中的 `id`为1的数据

// 特别注意:URI模式存在匹配通配符* & #

// *:匹配任意长度的任何有效字符的字符串
// 以下的URI 表示 匹配provider的任何内容
content://com.example.app.provider/* 
// #:匹配任意长度的数字字符的字符串
// 以下的URI 表示 匹配provider中的table表的所有行
content://com.example.app.provider/table/# 

(2)ContentResolver

通过Uri来定位注册到系统的ContentProvider,找到ContentProvider之后通过ContentResolver来操作对应的数据库

获取方式:Context.getContentResolver()

常用方法:insert、query、delete、update等待

例如:读取联系人

(3)自定义ContentProvider

ContentProvider实际上是对SQLiteOpenHelper的进一步封装,通过Uri映射要判断选择需要操作的数据库中哪个表,并进行增删查改。

重写insert、query、update、delete、getType方法


public class MyProvider extends ContentProvider {
    private SQLiteDatabase db;
    public static final String AUTOHORITY = "cn.scu.myprovider";
    public static final int User_Code = 1;
    public static final int Job_Code = 2;
    private static final UriMatcher mUriMatcher;

    static {
        mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        // 初始化
        mUriMatcher.addURI(AUTOHORITY,"user",User_Code);
        mUriMatcher.addURI(AUTOHORITY,"job",Job_Code);
        // 若URI资源路径 = content://cn.scu.myprovider/user ,则返回注册码User_Code
        // 若URI资源路径 = content://cn.scu.myprovider/job ,则返回注册码Job_Code
    }

    /**
     * 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
     */
    private String getTableName(Uri uri){
        String tableName = null;
        switch (mUriMatcher.match(uri)) {
            case User_Code:
                tableName = MySQListOpenHelper.USER_TABLE_NAME;
                break;
            case Job_Code:
                tableName = MySQListOpenHelper.JOB_TABLE_NAME;
                break;
        }
        return tableName;
    }
    @Override
    public boolean onCreate() {
        // 在ContentProvider创建时对数据库进行初始化
        // 运行在主线程,故不能做耗时操作,此处仅作展示
        SQLiteOpenHelper mDbHelper= new MySQListOpenHelper(getContext(),"test_person");
        db = mDbHelper.getWritableDatabase();
        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return db.query(getTableName(uri),projection,selection,selectionArgs,null,null,sortOrder,null);
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        // 得到数据类型,即返回当前 Url 所代表数据的MIME类型
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        db.insert(getTableName(uri),null,values);
        // 当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
        getContext().getContentResolver().notifyChange(uri, null);
        return uri;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }
}

UriMatcher

作用:

  • 在ContentProvider 中注册URI
  • 根据 URI 匹配 ContentProvider 中对应的数据表

ContentObserver

定义:内容观察者

作用:观察 Uri引起 ContentProvider 中的数据变化 & 通知外界(即访问该数据访问者)

// 步骤1:注册内容观察者ContentObserver
    getContentResolver().registerContentObserver(uri);
    // 通过ContentResolver类进行注册,并指定需要观察的URI

// 步骤2:当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
    public class UserContentProvider extends ContentProvider { 
      public Uri insert(Uri uri, ContentValues values) { 
      db.insert("user", "userid", values); 
      getContext().getContentResolver().notifyChange(uri, null); 
      // 通知访问者
   } 
}

// 步骤3:解除观察者
 getContentResolver().unregisterContentObserver(uri);
    // 同样需要通过ContentResolver类进行解除

Android:关于ContentProvider的知识都在这里了!

二、存储优化

1、序列化

Serializable、Parcelable

Serializable是java序列化接口,Parcelable是安卓序列化接口

  • Serializable会产生大量的临时变量,序列化过程会消耗更多内存,造成频繁的GC
  • Parcelable不能将数据存储在磁盘中(因为Parcel本质是为了更好的实现对象在IPC间传递,不是一个通用的序列化机制)

Gson

Gson实现JSON的序列化和反序列化

优点:执行速度快,内存使用效率高

主要方法是fromJson和toJson

Nano Proto Buffers

FlatBuffers

FlatBuffers是一个高效的跨平台序列化类库,是Google开发的,是为了应用在游戏开发,以及其他注重性能的应用上。比JSON快得多。

https://www.oschina.net/news/75092/android-flatbuffers-json

注意:为了避免序列化带来的性能问题,可以考虑使用SharedPefrence或者sqlite来存储数据,避免先把复杂数据进行序列化的操作。

2、SharedPerences优化

Editor的commit和apply方法中,commit是同步写入,apply是异步写入

以下方式可提升性能

  • 不需要返回值使用apply
  • 在批量操作,先获取一个editor,进行批量操作,然后调用apply
  • 避免频繁读写SharedPerences(因为commitToMemory会锁定SharedPerences对象,put和getEditor会锁定Editor对象,写入磁盘会锁定一个写入锁)

3、数据库优化

SQliteStatement

作用:提高性能,也解决SQL注入问题

    /**
     * 使用SQLiteStatement的executeInsert方法插入数据
     * @return 返回执行所需要的时间
     */
    public long insertStatement()
    {
        long start = System.currentTimeMillis();
        for(User user:users){
        SQLiteStatement statement=    db.compileStatement(sql_insert.toString());
        statement.bindString(1, user.getName());
        statement.bindLong(2, user.getGender());
        statement.bindLong(3, user.getAge());
        statement.bindString(4, user.getPhoneNumber());
        statement.bindString(5, user.getAddress());
        statement.executeInsert();
        }
        long end = System.currentTimeMillis();
        return end - start;
    }
}

使用事务

特征:原子提交、性能好

使用方式

db.beginTransaction();
xxxx….
db.setTransactionSuccessful();
db.endTransaction();

在endTransaction之前调用setTransactionSuccessful将事务标记成功,从beginTransaction开始的操作都会被提交,没有调用setTransactionSuccessful则回滚事务

使用索引

作用:提升查找效率

缺点:

  • 插入、更新、删除数据慢
  • 增加数据库大小

以下场景不建议使用索引

  • 表较小
  • 大量null值列
  • 频繁插入、删除、更新
  • 频繁操作列

异步线程管理对数据库的操作

数据库操作是耗时的,异步线程管理可以提高性能,保证数据的同步避免死锁。

提升查询性能

影响查询性能方面

  • 查询数据量大小
  • 排序复杂度
  • 查询数据的列数

数据库第三方框架

OrmLiteGreenDaoLitePalafinal

  • OrmLite、LitePal、afinal根据反射进行数据库的各个具体操作
  • GreenDao不是根据反射,而是直接使用人工生产业务需要的DAO和Model文件,避免因反射带来的性能损耗

参考:

安卓开发进阶从小工到专家

android应用性能优化最佳实践

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,208评论 6 524
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,502评论 3 405
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,496评论 0 370
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,176评论 1 302
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,185评论 6 401
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,630评论 1 316
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,992评论 3 431
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,973评论 0 280
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,510评论 1 325
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,546评论 3 347
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,659评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,250评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,990评论 3 340
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,421评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,569评论 1 277
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,238评论 3 382
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,732评论 2 366

推荐阅读更多精彩内容