第三章 存储

文章目录

  • 第三章 存储
    • (一)Android 5种保存数据的方法/数据持久化
      • (1)方法1:SharedPreferences 用户偏好设置
        • 1.1)简介——Android最简单数据存储方式
        • 1.2)基本使用
        • 1.3)工具类——对SharedPreferences功能进行封装
        • 1.4)数据的存储位置与格式
        • 1.5)性能优化
        • 1.6)设置数据文件的访问权限
        • 1.7)SharedPreferences源码分析
          • (1.7.1)SP存储格式
          • (1.7.2)SP如何读取数据
          • (1.7.3)SP如何写入数据
          • (1.7.4)SharedPreferences有同步锁,是线程安全的。
      • (2)方法2:文件存储
        • (2.1)Android中内部存储,外部存储
          • (2.1.1)内部存储、外部存储及Ram,Rom,扩展存储概念
          • (2.1.2)一张图解释
        • (2.2)不同版本Android路径与获取方式
          • (2.2.1)内部存储路径
          • (2.2.2)外部存储路径
          • (2.2.3)其他存储路径
          • (2.2.4)存储路径总结
        • (2.3)文件存储方式
          • (2.3.1)内部存储
          • (2.3.2)SD卡存储
      • (3)方法3:SQLite数据库存储
        • (3.1)简介
        • (3.2)特点
        • (3.3)SQLiteDatabase使用
        • (3.4)数据库的优化
          • 1、数据库性能上
          • 2、数据库设计上
        • (3.5)数据库框架对比和源码分析
          • 1、常见数据库框架
          • 2、greenDao简介
          • 3、greenDao使用
          • 4、自己封装一个数据库存储框架?
        • (3.6)数据库升级(数据库数据迁移)
      • (4)方法4:ContentProvider
        • (4.1)简介
        • (4.2)重要的类
        • (4.3)使用
      • (5)方法5:网络存储
    • (二)Android保存网络图片到系统相册
      • (1)基本流程
      • (2)源码解析
        • (2.1)确定存储路径
        • (2.2)获取外部存储权限
        • (2.3)确定外部存储状态
        • (2.4)确定文件名
        • (2.5)保存到文件中
        • (2.6)发送广播,通知系统扫描保存后的文件
        • (2.7)大图/多图的异步保存
        • (2.8)完整代码

第三章 存储

(一)Android 5种保存数据的方法/数据持久化

(1)方法1:SharedPreferences 用户偏好设置

1.1)简介——Android最简单数据存储方式

SharedPreferences是Android平台上一个轻量级的存储类,主要是保存一些常用的配置比如窗口状态。
(1)它提供了Android平台常规的Long长 整形、Int整形、String字符串型的保存。
(2)数据存储类型为key-value对。
(3)使用SharedPreferences保存数据,其背后是用xml文件存放数据,文件 存放在/data/data/ < package name > /shared_prefs目录下。
(4)是一种轻量级存储类,常用于保存一些常用的配置比如窗口状态。之所以说SharedPreference是一种轻量级的存储方式,是因为它在创建的时候会把整个文件全部加载进内存。
案例:登录成功后,再次启动,显示上次登录的用户名和密码

1.2)基本使用

SharedPreferences作为Android存储数据方式之一,主要特点是:
1、只支持Java基本数据类型,不支持自定义数据类型;
2、应用内数据共享;
3、使用简单.
存数据 putString

protected void onStop() {
    //获取一个文件名为test、权限为private的xml文件的SharedPreferences对象
    SharedPreferences sharedPreferences = getSharedPreferences("test", MODE_PRIVATE);
    //得到SharedPreferences.Editor对象,并保存key-value键值对数据到该对象中
    SharedPreferences.Editor editor = sharedPreferences.edit();
    editor.putString("username", et_username.getText().toString().trim());
    editor.putString("password", et_password.getText().toString().trim());
    //commit提交数据,保存key-value对到文件中
    editor.commit();
    super.onStop();
}

注意:sp.edit()每次都会返回一个新的Editor对象,Editor的实现类EditorImpl里面会有一个缓存的Map,最后commit的时候先将缓存里面的Map写入内存中的Map,然后将内存中的Map写进XML文件中。
故一次存储对应一个Editor。
取数据getString

protected void onCreate() {
    et_username = (EditText) findViewById(R.id.et_username);
    et_password = (EditText) findViewById(R.id.et_password);
    SharedPreferences sharedPreferences = this.getSharedPreferences("test", MODE_PRIVATE);
    et_username.setText(sharedPreferences.getString("username",""));
    et_password.setText(sharedPreferences.getString("password",""));
}

commit和apply的区别

  • apply没有返回值而commit返回boolean表明修改是否提交成功
  • apply是将修改数据原子提交到内存, 而后异步真正提交到硬件磁盘,;而commit是同步的提交到硬件磁盘,会阻塞调用它的线程。因此,在多个并发的提交commit的时候,他们会等待正在处理的commit保存到磁盘后在操作,从而降低了效率。而apply只是原子的提交到内容,后面有调用apply的函数的将会直接覆盖前面的内存数据,这样从一定程度上提高了很多效率。
  • apply方法不会提示任何失败的提示。由于在一个进程中,sharedPreference是单实例,一般不会出现并发冲突,如果对提交的结果不关心的话,建议使用apply,当然需要确保提交成功且有后续操作的话,还是需要用commit的。
  • SharedPreferences还提供一个监听接口可以监听SharedPreferences的键值变化,需要监控键值变化的可以用registerOnSharedPreferenceChangeListener添加监听器。
public interface SharedPreferences {
    /**
     * Interface definition for a callback to be invoked when a shared
     * preference is changed.
     */
    public interface OnSharedPreferenceChangeListener {
        void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }

存取复杂类型的数据
如果要用 SharedPreferences 存取复杂的数据类型(类,图像等),就需要对这些数据进行编码。通常会将复杂类型的数据转换成Base64编码,然后将转换后的数据以字符串的形式保存在XML文件中。
实例:
使用 SharedPreferences 保存Product类的一个对象和一张图片。
原理:
使用Base64把Product对象和图片进行编码成字符串后,然后通过 SharedPreferences 把转换后的字符串保存到xml文件中,在需要使用该对象或者图片时,通过Base64把从 SharedPreferences 获取的字符串解码成对象或者图片再使用。
保存对象

	SharedPreferences sharedPreferences = getSharedPreferences("base64", MODE_PRIVATE);
	SharedPreferences.Editor editor = sharedPreferences.edit();
	Product product = new Product();
	product.setId(et_prod_id.getText().toString().trim());
	product.setName(et_prod_name.getText().toString().trim());
	ByteArrayOutputStream baos = new ByteArrayOutputStream();
	ObjectOutputStream oos = new ObjectOutputStream(baos);
	oos.writeObject(product);
	String base64Product = Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT);
	editor.putString("product", base64Product);

保存图片

	ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
    ((BitmapDrawable) getResources().getDrawable(R.drawable.lanbojini)).getBitmap().compress(Bitmap.CompressFormat.JPEG, 100, baos2);
    String imageBase64 = Base64.encodeToString(baos2.toByteArray(), Base64.DEFAULT);
    editor.putString("productImg", imageBase64);
    editor.commit();

	baos.close();
	oos.close();

回显

					//获取对象
                    et_prod_id = (EditText) findViewById(R.id.et_prod_id);
                    et_prod_name = (EditText) findViewById(R.id.et_prod_name);
                    SharedPreferences sharedPreferences = getSharedPreferences("base64", MODE_PRIVATE);
                    String productString = sharedPreferences.getString("product", "");
                    byte[] base64Product = Base64.decode(productString, Base64.DEFAULT);
                    ByteArrayInputStream bais = new ByteArrayInputStream(base64Product);
                    ObjectInputStream ois = new ObjectInputStream(bais);
                    Product product = (Product) ois.readObject();
                    et_prod_id.setText(product.getId());
                    et_prod_name.setText(product.getName());

                    //获取图片
                    iv_prod_img = (ImageView) findViewById(R.id.iv_prod_img);
                    byte[] imagByte = Base64.decode(sharedPreferences.getString("productImg",""), Base64.DEFAULT);
                    ByteArrayInputStream bais2 = new ByteArrayInputStream(imagByte);
                    iv_prod_img.setImageDrawable(Drawable.createFromStream(bais2,  "imagByte"));

base64.xml



    /9j/4AAQSkZJRgABAQAAAQABAAD/2wBDABALDA4MChAOoOjM9PDkz......
    
    rO0ABXNyACNjbHViLmxldGdldC50ZXN0c2hhGJqb......
    

1.3)工具类——对SharedPreferences功能进行封装

public class ShareUtils {

    private String path = "share_data";

    private static ShareUtils prefUtils;
    private final SharedPreferences sp;


    public ShareUtils(Context context) {
        sp = context.getSharedPreferences(path, Context.MODE_PRIVATE);
    }

    public static ShareUtils getInstance(Context context) {
        if (prefUtils == null) {
            synchronized (ShareUtils.class) {
                if (prefUtils == null) {
                    prefUtils = new ShareUtils(context);
                }
            }
        }
        return prefUtils;
    }


    public void setPath(String path) {
        this.path = path;
    }

    public void putBoolean(String key, boolean value) {
        sp.edit().putBoolean(key, value).apply();
    }

    public boolean getBoolean(String key, boolean defValue) {
        return sp.getBoolean(key, defValue);
    }

    public void putString(String key, String value) {
        sp.edit().putString(key, value).apply();
    }

    public String getString(String key, String defValue) {
        return sp.getString(key, defValue);
    }

    public void putInt(String key, int value) {
        sp.edit().putInt(key, value).apply();
    }

    public int getInt(String key, int defValue) {
        return sp.getInt(key, defValue);
    }

    public void remove(String key) {
        sp.edit().remove(key).apply();
    }

    public void clear() {
        if (sp != null)
            sp.edit().clear().apply();
    }
}

1.4)数据的存储位置与格式

实际上,SharedPreferences 将数据文件写在了手机内存私有的目录中该app的文件夹下。
可以通过DDMS的【File Explorer】找到data\data\程序包名\shared_prefs目录(如果使用真机测试,必须保存已root,否则因为权限问题无法进入data目录),发现test.xml 文件。导出文件并查看




    小明

1.5)性能优化

(1)不适宜存储尺寸很大的数据
SharedPreference是一种轻量级的存储方式,在创建的时候会把整个文件加载进内存,如果sp文件较大,则会导致以下几个严重问题:

  • 第一次从sp中获取值的时候,有可能阻塞主线程,使界面卡顿、掉帧。
  • 解析sp的时候会产生大量的临时对象,导致频繁GC,引起界面卡顿。
  • 这些key和value会永远存在于内存之中,占用大量内存。

原因1:使用getString()时需要等待sp加载完毕。
下面是默认的sp实现SharedPreferenceImpl这个类的getString函数:

public String getString(String key, @Nullable String defValue) {
    synchronized (this) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

继续看看这个awaitLoadedLocked:

private void awaitLoadedLocked() {
    while (!mLoaded) {
    //锁,主线程等待sp加载完毕
        try {
            wait();
        } catch (InterruptedException unused) {
        }
    }
}

原因2:sp加载的大对象,会永远保存在内存中,不会释放。getSharedPreference会把所有的sp放到一个静态变量里缓存。

private ArrayMap getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    ArrayMap packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}

这个static的sSharedPrefsCache,它保存了你所有使用的sp,然后sp里面有一个成员mMap保存了所有的键值对;这样,你程序中使用到的那些个sp永远就呆在内存中。
所以,如果要存取很大的数据或者复杂的数据,一般使用文件存储、SQLite数据库等技术。
(2)不适宜存储JSON等特殊符号很多的数据
JSON或者HTML格式存放在sp里面的时候,需要转义,这样会带来很多 & 这种特殊符号,sp在解析碰到这个特殊符号的时候会进行特殊的处理,引发额外的字符串拼接以及函数调用开销。
如果这种场景应该直接使用JSON配置文件。
(3)多次edit多次commit/apply

  • 多次edit
    每次edit都会创建一个Editor对象,额外占用内存;
  • 多次apply
    每次apply会将一个带有await的写入任务加入写入队列,这个写入队列交给一个只有单个线程的线程池去执行;如果有很多写入任务,则当App执行onStop时,需要等写入线程执行完才能停止。
public void apply() {
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };
//把一个带有await的runnable添加进了QueueWork类的一个队列
    QueuedWork.add(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            public void run() {
                awaitCommit.run();
                QueuedWork.remove(awaitCommit);
            }
        };
//把这个写入任务通过enqueueDiskWrite丢给了一个只有单个线程的线程池执行。
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    notifyListeners(mcr);
}

在Activity Stop的时候,看ActivityThread类的handleStopActivity

private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {

    // 省略无关。。
    // Make sure any pending writes are now committed.
    if (!r.isPreHoneycomb()) {
        QueuedWork.waitToFinish();
        //waitToFinish()等待写入进程
    }

    // 省略无关。。
}
  • 多次commit
    commit是直接在当前线程写入,如果写入多次,会导致界面卡顿、掉帧。

(4)不要用SP跨进程通信
一般用ContentProvider。
sp有一个可以提供「跨进程」功能的FLAG——MODE_MULTI_PROCESS。保证了在API 11以前的系统上,如果sp已经被读取进内存,再次获取这个sp的时候,如果有这个flag,会重新读一遍文件。官方不推荐使用。并且未来也不会维护。
(5)优化建议

  • 不要存放大的key和value,会引起界面卡、频繁GC、占用内存等等。 毫不相关的配置项就不要放在在一起,文件越大读取越慢。
  • 读取频繁的key和不易变动的key尽量不要放在一起,影响速度,如果整个文件很小,那么忽略吧,为了这点性能添加维护成本得不偿失。
  • 不要多次edit和apply,尽量批量修改一次提交,多次apply会阻塞主线程。
  • 尽量不要存放JSON和HTML,这种场景请直接使用JSON。
  • SharedPreference无法进行跨进程通信,MODE_MULTI_PROCESS只是保证了在API
    11以前的系统上,如果sp已经被读取进内存,再次获取这个SharedPreference的时候,如果有这个flag,会重新读一遍文件,仅此而已。

1.6)设置数据文件的访问权限

SharedPreferences sharedPreferences = this.getSharedPreferences("test", MODE_PRIVATE);

其中getSharedPreferences方法第二个参数就是对文件权限的描述。
这个参数有四个可选值:

权限 解释
Activity.MODE_PRIVATE 表示该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容
Activity.MODE_APPEND 也是私有数据,新写入的内容会追加到原文件中
Activity.MODE_WORLD_READABLE 表示当前文件可以被其他应用读取
Activity.MODE_WORLD_WRITEABLE 表示当前文件可以被其他应用写入

1.7)SharedPreferences源码分析

(1.7.1)SP存储格式

1.1)ContextImpl.getSharedPreferences根据当前应用名称获取ArrayMap(存储sp容器),并根据文件名获取SharedPreferencesImpl对象(实现SharedPreferences接口)。

public class ContextImpl{
//静态存储类,缓存所有应用的SP容器,该容器key对应应用名称,value则为每个应用存储所有sp的容器(ArrayMap)
private static ArrayMap> sSharedPrefs;

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        if (sSharedPrefs == null) {
            //如果静态对象不存在,直接创建一个Map,以便后期用于保存sp
            sSharedPrefs = new ArrayMap>();
        }
        //获取当前应用包名
        final String packageName = getPackageName();
        //从保存sp的容器中通过包名查找当前应用所有sp;每个app的所有sp都是保存在ArrayMap中,
        ArrayMap packagePrefs = sSharedPrefs.get(packageName);

        if (packagePrefs == null) {

            //如果从sp容器没找到保存当前应用sp的ArrayMap直接创建一个
            packagePrefs = new ArrayMap();

            //将创建的对象保存到sp容器中
            sSharedPrefs.put(packageName, packagePrefs);
        }

        //从当前应用的sp容器中通过文件名去查找sp
        sp = packagePrefs.get(name);
        if (sp == null) {
            //如果没找到,直接创建一个文件名以name命名的xml文件
            File prefsFile = getSharedPrefsFile(name);
            //此处极为关键,该构造器是读取文件操作
            sp = new SharedPreferencesImpl(prefsFile, mode);
            //将创建sp对象保存到当前应用sp容器中
            packagePrefs.put(name, sp);
            return sp;
        }
    }
    return sp;
}

1.2)用SP存储的静态变量键值数据在内存中是一直存在(文件存储),通过SharedPreferencesImpl构造器开启一个线程对文件进行读取
SharedPreferencesImpl是SharedPreferences接口具体实现类,一个name对应一个SharedPreferencesImpl,一个应用程序中根据name的不同会有多个SharedPreferencesImpl。SharedPreferencesImpl主要是对文件进行操作。

final class SharedPreferencesImpl implements SharedPreferences {

    SharedPreferencesImpl(File file, int mode) {
        //给类成员变量赋值
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;

        //开启一个线程读取文件
        startLoadFromDisk();
    }

    private static File makeBackupFile(File prefsFile) {//文件备份
        return new File(prefsFile.getPath() + ".bak");
    }

    private void startLoadFromDisk() {
        synchronized (this) {//使用同步代码代码块,对mloader进行赋值
            mLoaded = false;
        }

        new Thread("SharedPreferencesImpl-load") {//开启线程读取文件
            public void run() {
                synchronized (SharedPreferencesImpl.this) {
                    loadFromDiskLocked();
                }
            }
        }.start();
    }

    private void loadFromDiskLocked() {
        //如果文件已经加载完毕直接返回
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }

        Map map = null;
        StructStat stat = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                //读取文件
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16*1024);

                    //使用XmlUtils工具类读取xml文件数据
                    map = XmlUtils.readMapXml(str);
                }(Exception e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
        }

        //修改文件加载完成标志
        mLoaded = true;
        if (map != null) {
            mMap = map;//如果有数据,将数据已经赋值给类成员变量mMap(将从文件读取的数据赋值给mMap)
            mStatTimestamp = stat.st_mtime;//记录文件上次修改时间
            mStatSize = stat.st_size;//记录文件大小(字节单位)
        } else {
            //没有数据直接创建一个hashmap对象
            mMap = new HashMap();
        }

        //此处非常关键是为了通知其他线程文件已读取完毕,你们可以执行读/写操作了
        notifyAll();
    }
}
(1.7.2)SP如何读取数据
@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (this) {
        //此处会阻塞当前线程,直到文件加载完毕,第一次使用的时候可能会阻塞主线程
        awaitLoadedLocked();
        //从类成员变量mMap中直接读取数据,没有直接返回默认值
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}
(1.7.3)SP如何写入数据

sp写入数据:
1、
3.1)edit()方法

public Editor edit() {
    synchronized (this) {
        awaitLoadedLocked();//如果文件未加载完毕,会一直阻塞当前线程,直到加载完成为止
    }
    return new EditorImpl();
}

3.2)EditorImpl()方法

public final class EditorImpl implements Editor {
    private final Map mModified = Maps.newHashMap();
    private boolean mClear = false;

    public Editor putString(String key, @Nullable String value) {
        synchronized (this) {
            mModified.put(key, value); //暂时放入内存
            return this;
        }
    }

    public Editor remove(String key) {
        synchronized (this) {
            //注意此处并没有执行删除操作,而是将其对应key的value设置了当前this
            //commitToMemory方法中会对此做特殊处理
            mModified.put(key, this);
            return this;
        }
    }
    public Editor clear() {
        synchronized (this) {
            mClear = true;//设置标志位
            return this;
        }
    }

    public boolean commit() {
        //第一步  commitToMemory方法可以理解为对SP中的mMap对象同步到最新数据状态
        MemoryCommitResult mcr = commitToMemory();
        //第二步 写文件;注意第二个参数为null,写文件操作会运行在当前线程
        SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
        try {
            mcr.writtenToDiskLatch.await();
        } catch (InterruptedException e) {
            return false;
        }
        //第三步 通知监听器数据改变
        notifyListeners(mcr);
        //第四步 返回写操作状态
        return mcr.writeToDiskResult;
    }
        ...
}

3.3)commit方法第一步commitToMemory,对SP中的mMap对象同步到最新数据状态

class MemoryCommitResult {
    public boolean changesMade;  // any keys different?
    public List keysModified;  // may be null
    public Set listeners;  // may be null
    public Map mapToWriteToDisk;}

private MemoryCommitResult commitToMemory() {
//将sp数据同步到最新状态并返回mcr对象
    MemoryCommitResult mcr = new MemoryCommitResult();
    synchronized (SharedPreferencesImpl.this) {
        mcr.mapToWriteToDisk = mMap;
        synchronized (this) {
            //如果调用了clear方法,判断sp中mMap是否为空,不为空直接清空数据,并设置标志位,表示数据有改变; 此操作只会清空上次数据,不会对清空本次数据
            if (mClear) {
                if (!mMap.isEmpty()) {
                    mcr.changesMade = true;
                    mMap.clear();
                }
                mClear = false;
            }
            //遍历需要修改数据的键值集合
            for (Map.Entry e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                //如果对应的值为它自己EditorImpl或为null
                if (v == this || v == null) {
                    //如果sp的数据结合没有该键值对,直接进入下一个循环
                    if (!mMap.containsKey(k)) {
                        continue;
                    }
                    mMap.remove(k);//有该键值集合直接删除,因为remove方法其实存放的就是EditorImpl对象
                } else {
                    if (mMap.containsKey(k)) {//将要写入的键值数据,同步到sp的mMap中
                        Object existingValue = mMap.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mMap.put(k, v);
                }
                //标志数据发生改变
                mcr.changesMade = true;
                if (hasListeners) {
                    mcr.keysModified.add(k);
                }
            }
            //结尾清空EditorImpl中mModified数据
            mModified.clear();
        }
    }
    return mcr;
}

3.4)commit方法第二步enqueueDiskWrite:写文件,第二个参数为null,写文件操作会运行在当前线程

private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
//将mcr对象中数据写入文件
    final Runnable writeToDiskRunnable = new Runnable() {
        public void run() {
            synchronized (mWritingToDiskLock) {
                writeToFile(mcr);//持有mWritingToDiskLock锁后,执行写文件操作
            }
            synchronized (SharedPreferencesImpl.this) {
                //写操作数减一
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                //文件写入操作完成后,执行后续动作;apply方法会执行到次
                postWriteRunnable.run();
            }
        }
    };
    //判断是不是调用commit方法
    final boolean isFromSyncCommit = (postWriteRunnable == null);
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (SharedPreferencesImpl.this) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();//如果是commit方法,直接在当前线程执行;可以看出如果当前是UI线程,会阻塞UI线程,引起界面卡顿
            return;
        }
    }
    //如果不是commit是apply方法,writeToDiskRunnable任务会被提交到一个单个线程的线程池中执行
    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

private void writeToFile(MemoryCommitResult mcr) {
    if (mFile.exists()) {
        //如果文件存在且数据未发生改变,没有必要执行无用的写入操作,直接通知并返回即可
        if (!mcr.changesMade) {
            mcr.setDiskWriteResult(true);
            return;
        }
        //文件存在且备份文件不存在
        if (!mBackupFile.exists()) {
            //尝试将文件设置为备份文件(重命名)(此时只有备份文件,mFile对应的文件已经没有了);如果操作不成功,通知并返回;因为下面到操作是写入数据到mFile中,所以我们先要备份数据,这样即使写入数据失败时,我们还可以使用备份数据
            if (!mFile.renameTo(mBackupFile)) {
                Log.e(TAG, "Couldn't rename file " + mFile
                        + " to backup file " + mBackupFile);
                mcr.setDiskWriteResult(false);
                return;
            }
        } else {
            //如果备份文件存在,直接删除mFile,因为接下来要重新写入mFile了
            mFile.delete();
        }
    }
    try {
        //创建一个文件输出流,用于写入数据到mFile中
        FileOutputStream str = createFileOutputStream(mFile);
        if (str == null) {
            //创建失败直接返回
            mcr.setDiskWriteResult(false);
            return;
        }
        //写入数据到mFile中
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        //同步到硬盘中
        FileUtils.sync(str);
        //关闭流,释放资源
        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
        try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (this) {
                //更新文件属性
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            }
        } catch (ErrnoException e) {
        }
        mBackupFile.delete();//写入数据成功,则删除备份文件;因为所有数据都写入mFile中
        mcr.setDiskWriteResult(true);
        return;
    } catch (XmlPullParserException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    }
    if (mFile.exists()) {//写入过程中出现异常则删除mFile,下次可以直接从备份文件中读取
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }
    mcr.setDiskWriteResult(false);
}

3.5)commit方法第三步notifyListeners(mcr):通知监听器数据改变

private void notifyListeners(final MemoryCommitResult mcr) {
    //注意监听器的回调方法都是在UI线程执行的
    if (mcr.listeners == null || mcr.keysModified == null ||
            mcr.keysModified.size() == 0) {
        return;
    }
    if (Looper.myLooper() == Looper.getMainLooper()) {
        for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
            final String key = mcr.keysModified.get(i);
            for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
                if (listener != null) {
                    listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
                }
            }
        }
    } else {
        ActivityThread.sMainThreadHandler.post(new Runnable() {
            public void run() {
                notifyListeners(mcr);
            }
        });
    }
}

3.6)commit方法第四步return mcr.writeToDiskResult:返回写操作状态

3.7)补:apply方法
apply和commit主要区别就是apply的写文件操作会在一个线程中执行,不会阻塞UI线程

public void apply() {
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();//写文件操作线程
                        } catch (InterruptedException ignored) {
                        }
                    }
                };

            //将awaitCommit任务提交到一个队列中
            QueuedWork.add(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    public void run() {
                        //写文件操作完成后会执行此2行代码(如果写操作未结束,当前页面已经销毁写操作的数据会丢失?这个切入点主要看QueuedWork的waitToFinish方法何时调用就明白了,有兴趣同学可以自己研究下)
                        awaitCommit.run();
                        //移除任务
                        QueuedWork.remove(awaitCommit);
                    }
                };

            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

            // Okay to notify the listeners before it's hit disk
            // because the listeners should always get the same
            // SharedPreferences instance back, which has the
            // changes reflected in memory.
            notifyListeners(mcr);
        }
(1.7.4)SharedPreferences有同步锁,是线程安全的。

SharedPreferences读取数据都使用awaitLoadedLocked同步锁,故是线程安全的。

(2)方法2:文件存储

(2.1)Android中内部存储,外部存储

(2.1.1)内部存储、外部存储及Ram,Rom,扩展存储概念

内部存储(InternalStorage)
1、概念
注意内部存储不是内存。内部存储位于系统中很特殊的一个位置,如果你想将文件存储于内部存储中,那么文件默认只能被你的应用访问到,且一个应用所创建的所有文件都在和应用包名相同的目录下。也就是说应用创建于内部存储的文件,与这个应用是关联起来的。当一个应用卸载之后,内部存储中的这些文件也被删除。从技术上来讲如果你在创建内部存储文件的时候将文件属性设置成可读,其他app能够访问自己应用的数据,前提是他知道你这个应用的包名,如果一个文件的属性是私有(private),那么即使知道包名其他应用也无法访问。 内部存储空间十分有限,因而显得可贵,另外,它也是系统本身和系统应用程序主要的数据存储所在地,一旦内部存储空间耗尽,手机也就无法使用了。所以对于内部存储空间,我们要尽量避免使用。Shared Preferences和SQLite数据库都是存储在内部存储空间上的。内部存储一般用Context来获取和操作。

2、特点

  • 始终可用
  • 只有你的应用可以访问这里保存的文件
  • 用户卸载应用时候,会删除应用在这里存放的文件
  • 不希望用户或其他应用访问的那些文件信息,可以放到这里。

3、访问内部存储的API方法

  • Environment.getDataDirectory()
  • getFilesDir().getAbsolutePath()
  • getCacheDir().getAbsolutePath()
  • getDir(“myFile”, MODE_PRIVATE).getAbsolutePath()

外部存储(ExternalStorage)
1、概念
最容易混淆的是外部存储,因为老的Android系统的跟新的Android系统是有差别的。因为在4.4(API19)以前的手机上确实是这样的,手机自身带的存储卡就是内部存储,而扩展的SD卡就是外部存储。但是从4.4的系统开始,很多的中高端机器都将自己的机身存储扩展到了8G以上,比如有的人的手机是16G的,有的人的手机是32G的,也就是说4.4系统及以上的手机将机身存储存储(手机自身带的存储叫做机身存储)在概念上分成了”内部存储internal” 和”外部存储external” 两部分。若4.4系统及以上的手机插入SD卡(也是外部存储),为了区别机身存储的外部存储和SD卡的外部存储,在4.4以后的系统中,API提供了一种方式遍历手机的外部存储路径:

File[] files;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    files = getExternalFilesDirs(Environment.MEDIA_MOUNTED);
    for(File file:files){
        Log.e("main",file);
    }
}

如果你的手机插了SD卡的话,那么它打印的路径就有两条了:
/storage/emulated/0/Android/data/packname/files/mounted
/storage/B3E4-1711/Android/data/packname/files/mounted
其中/storage/emulated/0目录就是机身存储的外部存储路径 ,而/storage/B3E4-1711/就是SD卡的路径

2、特点

  • 并非始终可用。用户可采用USB存储设备的形式装载外部存储,并且可拆卸
  • 全局可读。因此这里存放的文件可以不受你的控制被读取。
  • 当用户卸载应用时候,系统会卸载你存放在通过调用getExternalFilesDir()获取的目录里的文件。
  • 外部存储适用于存放那些无需访问限制,以及你希望共享给其他应用或允许用户使用电脑访问的文件。

3、访问外部存储的API方法
1、Environment.getExternalStorageDirectory().getAbsolutePath()
2、Environment.getExternalStoragePublicDirectory(“”).getAbsolutePath()
3、getExternalFilesDir(“”).getAbsolutePath()
4、getExternalCacheDir().getAbsolutePath()

内存、机身存储(内置存储)、Ram,Rom,以及扩展存储(TF卡)
内存
我们在英文中称作memory,内存是计算机中重要的部件之一,它是与CPU进行沟通的桥梁。计算机中所有程序的运行都是在内存中进行的,所以说它是用于计算机运行时的,它不是用来存储数据的。
机身存储(内置存储)
机身存储是指手机自身携带的存储空间,出厂时就已经有了,4.4以前机身存储就是内部存储,外置SD卡就是外部存储,我们通过getDataDirectory就可以获取内置存储根路径,通过getExternalStorageDirectory就可以获取外置SD卡根路径。4.4及以后机身存储包含了内部存储和外部存储,其中通过getExternalStorageDirectory获取的是机身存储的外部存储,而外置SD卡我们则需要通过getExternalDirs遍历来获取了。
Ram,Rom,以及扩展存储(TF卡)
从图中我们可以看到,一个手机里面有内存,手机内置存储,以及SD卡, 它们分别是Ram,Rom,以及TF卡,这三种卡的性能,材质及价格都不一样,都有各自的用处。

(2.1.2)一张图解释

第三章 存储_第1张图片

(2.2)不同版本Android路径与获取方式

(2.2.1)内部存储路径

第三章 存储_第2张图片
后三个方法包含包名,是属于某个应用的内部存储。

(2.2.2)外部存储路径

第三章 存储_第3张图片
其中方法7和方法8如果在4.4以前的系统中getExternalFilesDir(“”)和getExternalCacheDir()将返回null,如果是4.4及以上的系统才会返回上面的结果,也即4.4以前的系统没插SD卡的话,就没有外部存储,它的SD卡就等于外部存储;而4.4及以后的系统外部存储包括两部分,getExternalFilesDir(“”)和getExternalCacheDir()获取的是机身存储的外部存储部分,也即4.4及以后的系统你不插SD卡,它也有外部存储,既然getExternalFilesDir(“”)和getExternalCacheDir()获取的是机身存储的外部存储部分,那么怎么获取SD卡的存储路径呢,还是通过上面提到的getExternalFilesDirs(Environment.MEDIA_MOUNTED)方法来获取了

(2.2.3)其他存储路径

Environment.getDownloadCacheDirectory() = /cache
Environment.getRootDirectory() = /system

(2.2.4)存储路径总结
  • /data目录下的文件物理上存放在我们通常所说的内部存储里面
  • /storage目录下的文件物理上存放在我们通常所说的外部存储里面
  • /system用于存放系统文件
  • /cache用于存放一些缓存文件,物理上它们也是存放在内部存储里面的

getFilesDir().getAbsolutePath()和getCacheDir().getAbsolutePath()和getExternalFilesDir(“”).getAbsolutePath()区别
getFilesDir().getAbsolutePath():
路径:/data/user/0/packname/files
getFilesDir获取的是files目录,存放普通数据(log数据,json型数据等)。
getCacheDir().getAbsolutePath():
路径:/data/user/0/packname/cache
getCacheDir获取的是cache目录,存放缓冲数据。
/data/user/0/packname/目录(系统创建App专属文件):
第三章 存储_第4张图片
cache下存放缓存数据,databases下存放使用SQLite存储的数据,files下存放普通数据(log数据,json型数据等),shared_prefs下存放使用SharedPreference存放的数据。这些文件夹都是由系统创建的。

getExternalFilesDir(“”).getAbsolutePath():
路径:/storage/emulated/0/Android/data/packname/files
放在外部存储中App专属文件中,一般开发人员使用外部存储的App专属文件。因为内部存储的App专属文件一般给系统使用,且内存较小。

(2.3)文件存储方式

(2.3.1)内部存储

应用私有存储文件:/data/data//files/目录下,应用删除时,即清空该目录。
//通过context对象获取私有目录:context.getFileDir().getPath()
是一个应用程序的私有目录,只有当前应用程序有权限访问读写,其他应用无权限访问。一些安全性要求比较高的数据存放在该目录,一般用来存放size比较小的数据。

在使用openFileOutput方法打开文件以写入数据时,需要指定打开模式。默认为零,即MODE_PRIVATE。不同的模式对应的的含义如下:
openFileOutput方法打开文件时的模式:

常量 含义
MODE_PRIVATE 默认模式,文件只可以被调用该方法的应用程序访问
MODE_APPEND 如果文件已存在就向该文件的末尾继续写入数据,而不是覆盖原来的数据。
MODE_WORLD_READABLE 赋予所有的应用程序对该文件读的权限。
MODE_WORLD_WRITEABLE 赋予所有的应用程序对该文件写的权限。

1、读取数据

public String read() {
    try {
        FileInputStream inStream = context.openFileInput(mFileName);
        byte[] buffer = new byte[1024];
        int hasRead = 0;
        StringBuilder sb = new StringBuilder();
        while ((hasRead = inStream.read(buffer)) != -1) {
            sb.append(new String(buffer, 0, hasRead));
        }
        inStream.close();
        return sb.toString();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

2、写入数据

public void write(String msg) {
    try {
        FileOutputStream fos = context.openFileOutput(mFileName, MODE_APPEND);
        fos.write(msg.getBytes());
        fos.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3、使用

FileCacheUtil.getInstance(getApplicationContext()).write("hello world");//写文件
String result = FileCacheUtil.getInstance(getApplicationContext()).read();//读文件
(2.3.2)SD卡存储

1、申请权限



2、读取数据

    public static byte[] loadDataFromSDCard(String fileAbsolutePath) {
        if (isSDCardMounted()) {
            BufferedInputStream bis = null;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            try {
                bis = new BufferedInputStream(
                        new FileInputStream(fileAbsolutePath));
                byte[] buffer = new byte[1024 * 8];
                int len = 0;
                while ((len = bis.read(buffer)) != -1) {
                    baos.write(buffer, 0, len);
                    baos.flush();
                }
                return baos.toByteArray();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (bis != null) {
                        bis.close();
                    }
                    if (baos != null) {
                        baos.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    public static String getSDCardCacheDir(Context context) {
        return context.getExternalCacheDir().getPath();
    }

3、写入数据

if (isSDCardMounted()) {//如果SD卡处于挂载状态
        BufferedOutputStream bos = null;
        File fileDir =Environment.getExternalStoragePublicDirectory(type);//type为9大共有目录的type
        File file = new File(fileDir, fileName);
        bos = new BufferedOutputStream(new FileOutputStream(file));
        bos.write(data);
        bos.flush();

第三章 存储_第5张图片
4、使用

boolean isSave = SDUtils.saveFileToExternalCacheDir(getApplicationContext(), msg.getBytes(), fileName);  //保存的地址  //storage/sdcard0/Android/data/packageName/cache/text.txt
byte[] bytes = SDUtils.loadDataFromSDCard("/storage/sdcard0/Android/data/com.allens.test/files/text1.txt");

(3)方法3:SQLite数据库存储

(3.1)简介

android 内置的数据库,是遵守ACID的关联式数据库管理系统,非常轻量级,占用资源非常低(可能只有几百K内存)。最后会生成 /data/data/n你的应用包名/databases/数据库名字.db
sqlite每个数据库都是以单个文件的形式存在,这些数据都是以B-Tree的数据结构形式存储在磁盘上。同时sqlite更改数据的时候默认一条语句就是一个事务,有多少条数据就有多少次磁盘操作。
另外还有一个很重要的就是sqlite的共享锁和独享锁机制,sqlite在可以写数据库之前,它必须先读这个数据库,看它是否已经存在了.为了从数据库文件读取,第一步是获得一个数据库文件的共享锁。一个“共享”锁允许多个数据库联接在同一时刻从这个数据库文件中读取信息。“共享”锁将不允许其他联接针对此数据库进行写操作。
在修改一个数据库之前,SQLite首先得拥有一个针对数据库文件的“Reserved”锁。Reserved锁类似于共享锁,它们都允许其他数据库连接读取信息。单个Reserved 锁能够与其他进程的多个共享锁一起协作。然后一个数据库文件同时只能存在一个Reserved 。因此只能有一个进程在某一时刻尝试去写一个数据库文件。然后将此锁提升成一个独享锁,一个临界锁允许其他所有已经取得共享锁的进程从数据库文件中继续读取数据。但是它会阻止新的共享锁的生成。也就说,临界锁将会防止因大量连续的读操作而无法获得写入的机会。所以从sqlite本身的机制看来事务的方式去提交数据,本身是多线程乃至多进程数据安全的,但是android在并发写的时候还是会爆出数据库锁住的异常,我们在开发过程中需要尽量避免。

(3.2)特点

1、轻量级
SQLite和C/S模式的数据库软件不同,它是进程内的数据库引擎,因此不存在数据库的客户端和服务器。使用SQLite一般只需要带上它的一个动态 库,就可以享受它的全部功能。而且那个动态库的尺寸也挺小,以版本3.6.11为例,Windows下487KB、Linux下347KB。
2、不需要"安装"
SQLite的核心引擎本身不依赖第三方的软件,使用它也不需要"安装"。是系统自带的数据库
3、单一文件
数据库中所有的信息(比如表、视图等)都包含在一个文件内。这个文件可以自由复制到其它目录或其它机器上。
4、跨平台/可移植性
除了主流操作系统 windows,linux之后,SQLite还支持其它一些不常用的操作系统。
5、弱类型的字段
同一列中的数据可以是不同类型,包括NULL,VARCHAR,CHAR,INTERGER,REAL,TEXT,BLOB,DATA,TIME
6、开源

(3.3)SQLiteDatabase使用

1、创建一个类继承SQLiteOpenHelper
SQLiteOpenHelper是SQLIteDatabase一个辅助类,用于生成一个数据库,并对数据库版本进行管理。
当在程序当中调用这个类的方法getWritableDatabase()或者 getReadableDatabase()方法的时候,如果当时没有数据,那么Android系统就会自动生成一个数据库。 SQLiteOpenHelper 是一个抽象类,我们通常需要继承它,并且实现里面的3个函数:
1.onCreate(SQLiteDatabase)
在数据库第一次生成的时候会调用这个方法,也就是说,只有在创建数据库的时候才会调用,当然也有一些其它的情况,一般我们在这个方法里边生成数据库表。

  • onUpgrade(SQLiteDatabase,int,int)
    当数据库需要升级的时候,Android系统会主动的调用这个方法。一般我们在这个方法里边删除数据表,并建立新的数据表,当然是否还需要做其他的操作,完全取决于应用的需求。
  • onOpen(SQLiteDatabase):
    这是当打开数据库时的回调函数,一般在程序中不是很常使用。
public class DBHelper extends SQLiteOpenHelper {
    public DBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
    //构造函数
        super(context, name, factory, version);
    }
    @Override
    public void onCreate(SQLiteDatabase db) {
    //第一次创建数据库,调用该方法
    //execSQL用于执行SQL语句
        db.execSQL(T_UserInfo);
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    //更新数据库时,调用该方法
    }
    //使用者登录信息
    private static final String T_UserInfo =
            "create table T_UserInfo("
                    + "id varchar,"//id
                    + "account varchar,"//手动输入的账号
                    + "Name varchar,"//账号姓名
                    + "pwd varchar)";//pwd
}

2、得到SQLiteDatabase

public class DBUtil {
    public static SQLiteDatabase db(Context context) {
    //创建一个DBHelper并得到一个可写的SQLiteDatabase对象
        return new DBHelper(context, "text.db", null, 1).getWritableDatabase();
    }
}

3、使用数据库进行增删改查

db.insert("T_UserInfo",null,values);//增
db.delete("T_UserInfo","id = ?",new String[]{"1"});//删
db.update("T_UserInfo",values,"name = ?",new String[]{"allens"});//改
        Cursor cursor = db.query("T_UserInfo", new String[]{"id", "account", "name", "pwd"}, null, null, null, null, null);//查
        while (cursor.moveToNext()) {
        Log.e("TAG", "id--->" + cursor.getString(0));
        Log.e("TAG", "account--->" + cursor.getString(1));
        Log.e("TAG", "name--->" + cursor.getString(2));
        Log.e("TAG", "pwd--->" + cursor.getString(3));
        }

4、数据库更新

// 数据库版本的更新,由原来的1变为2
DBHelper dbHelper = new DBHelper(context.this,"text.db",null,2);
SQLiteDatabase db =dbHelper.getReadableDatabase();

(3.4)数据库的优化

1、数据库性能上

1.1 批量事务插入,提升数据插入的性能由于sqlite默认每次插入都是事务,需要对文件进行读写,那么减少事务次数就能简书磁盘读写次数从而获得性能提升。
1.2 单条sql优于多条sql实测发现,对于几十条sql插入当你替换成单条sql时性能有所提升
1.3 读和写操作是互斥的,写操作过程中可以休眠让读操作进行由于第一步所说的多数据事务插入,从而会导致插入时间增长那么也会影响数据展示的速度,所以可以在插入过程中休眠操作,以便给读操作流出时间展示数据。
1.4 使用索引适当的索引的好处是让读取变快,当然带来的影响就是数据插入修改的时间增加,因为还得维护索引的变化。
1.5 使用联合索引过多的索引同时也会减慢读取的速度
1.6 勿使用过多索引
1.7 增加查询条件当你只要一条数据时增加limit 1,大大的加快了速度
1.8 提前将字段的index映射好减少getColumnIndex的时间,可以缩短一半的时间

2、数据库设计上

2.1 通过冗余换取查询速度
2.2 减少数据来提升查询速度比如下拉操作时,先清除旧数据,再插入新数据保证数据库中的数据总量小,提升查询速度。
2.3 避免大数据多表的联合查询

(3.5)数据库框架对比和源码分析

1、常见数据库框架
  • LitePal 注解+反射 5K 最简单
    简介:
    LitePal通过LitePal.xml文件获取数据库的名称、版本号以及表,然后自动创建数据库和表,以及表数据类型和非空约束等。
    要执行增删改查操作的数据model都会继承DataSupport,最后将查询得到的数据转换成List并返回。
    LitePal不管是创建数据库、表还是执行增删改查,都是根据Model的类名和属性名,每次都需要进行反射拼装,然后调用Android原生的数据库操作,或者直接执行sql语句,实现相应的功能
    特点:
    1、根据反射进行数据库的各项操作(速度比GreenDAO要慢很多很多)
    2、采用对象关系映射(ORM)的模式
    3、很“轻”,jar包只有100k不到
    4、使用起来比较简单
    5、支持直接用sql原始语句实现查询的api方法
  • GreenDao 智能代码生成 10K+ 首先
    简介:
    greenDAO与其他常见的ORM框架不同,其原理不是根据反射进行数据库的各项操作,而是一开始就人工生成业务需要的Model和DAO文件,业务中可以直接调用相应的DAO文件进行数据库操作,从而避免了因反射带来的性能损耗和效率低下。
    以查询为例,其首先是创建数据库,然后在SQLiteOpenHelper.onCreate方法中根据已生成的model创建所有的表,而db.query其实就是Android原生的查询操作,只不过参数是经过DAO文件处理过的,无需手动匹配。
    由于需要人工生成model和DAO文件,所以greenDAO的配置就略显复杂。
    优点:
    效率高,速度快,文件较小,占用更少的内存,操作实体灵活
    缺点:
    学习成本较高。
  • OrmLite 注解+反射 1.5K 通用
    简介:
    基于注解和反射的的方式,导致ormlite性能有着一定的损失(运行时注解其实也是利用了反射的原理)
    OrmLite 不是 Android 平台专用的ORM框架,它是Java ORM。支持JDBC连接,Spring以及Android平台。语法中广泛使用了运行时注解。
    优点:
    文档较全面,社区活跃,有好的维护,使用简单,易上手。
    缺点:
    基于反射,效率较低(GreenDAO比OrmLite要快几乎4.5倍)
  • DBFlow 5K
    相关代码通过编译时注解生成,不会导致性能瓶颈。
    功能特性比较丰富,文档较完善。
    数据库版本升级和数据迁移有较简便的解决方案,支持集成SQLCipher加密,支持Content Provider Generation。
  • Realm 10K-
    针对NoSQL(对关系型数据库引入ORM,实现了对象型数据库)
    基于C++编写,直接运行在你的设备硬件上(不需要被解释),因此运行很快。
    Realm是一个直接在手机,平板电脑或可穿戴设备中运行的移动数据库。 此存储库包含Realm的Java版本的源代码,该版本目前仅在Android上运行。
    特征:
    移动优先:Realm是第一个直接在手机、平板电脑和可穿戴设备内部运行的数据库。
    简单:数据直接作为对象公开,并且可以通过代码查询,从而消除了对ORM性能和维护问题的需求。 此外,我们努力将我们的API保持在极少数类上:我们的大多数用户直观地选择它[pick it up intuitively],在几分钟内启动并运行简单的应用程序。
    现代[Modern]:Realm支持简单的线程安全,关系和加密[relationships & encryption]。
    快速:Realm在常见操作上比原始SQLite更快,同时保持极其丰富的功能集。
2、greenDao简介

greenDAO是一种Android数据库ORM(object/relational mapping 对象关系映射)框架,与OrmLite、ActiveOrm、LitePal等数据库相比,单位时间内可以插入、更新和查询更多的数据,而且提供了大量的灵活通用接口。是目前最主流的安卓数据库操作框架。
ORM:面向对象编程把所有实体看成对象(object),关系型数据库则是采用实体之间的关系(relation)连接数据。
简单说,ORM 就是通过实例对象的语法,完成关系型数据库的操作的技术,是"对象-关系映射"(Object/Relational Mapping) 的缩写。可以理解成,映射(Map)是一种对应关系,用面向对象的方式来处理关系型数据库,用于将一张表按照统一规则映射成一个java实体类,对表的操作可以转换成对实体对象的操作。

数据库的表(table) --> 类(class)
记录(record,行数据)–> 对象(object)
字段(field)–> 对象的属性(attribute)

GreenDao的ORM体现图:
第三章 存储_第6张图片
GreenDao特点:
优点:
1、开发简单,将对象模型转化为SQL语句。只需要掌握api,不需要书写复杂的sql语句。
2、面对一个复杂的程序时,sql语句大量的硬编码,会让代码难以维护,ORM框架能让代码结构更清晰,更容易维护。
缺点:
1、虽然ORM框架开发简单,但需要掌握很多东西。
2、对于一些复杂的数据库操作(如多表关联),ORM语法会很复杂,直接使用SQL语句比较清晰直接。

3、greenDao使用

3.1)在build.grade中配置插件信息并引入greenDao依赖包
1、project的gradle文件引入greenDao插件

apply plugin: 'org.greenrobot.greendao'
//设置脚本运行环境(app启动模块)
buildscript {
    repositories {
    //支持java依赖库管理(maven/ivy),用于项目依赖
        mavenCentral()
    }
    dependencies {
    //依赖包定义。支持maven/ivy,远程,本地库,也支持单文件
        classpath 'org.greenrobot:greendao-gradle-plugin:3.2.2'
    }
}

2、module的gradle文件添加greenDao插件

apply plugin: 'com.android.application'
//1、声明添加的插件类型
apply plugin: 'org.greenrobot.greendao'
android {
    ...
    //自定义Greendao版本和生成路径
    greendao{
    	//数据库版本号,数据库修改后一定要修改版本,否则会报错no such table
        schemaVersion 1
        //greenDAO生成的DAOMaster和DaoSession的位置,自定义生成数据库文件的目录
        targetGenDir 'src/main/java'
    }
}
dependencies {
    ...
    //添加依赖库
    compile 'org.greenrobot:greendao:3.1.0'
}

3.2)数据库初始化
(1)创建一个实体类,实体类添加@Entity注解

@Entity
public class Song {
    @Id(autoincrement = true)
    private Long id;
    private String songName;
    private Integer songId;
    private String songDesc;
    private String cover;
    private Integer singerCode;
    private String singerName;
    private String createTime;
    private String updateTime;
}

自增长的id类型一定是Long/long类型,否则会报错
(2)build(Build->Make Project)项目,获得操作类DaoSession和管理类DaoMaster
会自动生成一些数据库相关类,这些类在build.gradle里设置的目录下,而且实体类里面也会自动生成get/set方法。
有多少@Entity注释的实体类就会生成对少相关的XXDao类,XXDao类里提供对实体类对应的表单的CRUD的操作方法,即ORM里提供以面向对象的方式来处理关系型数据库,不需要写sql语句。
第三章 存储_第7张图片
3.3)数据库基本操作
操作类DaoSession,默认数据库表存储在内存中。见DaoSessionManager:

public class DaoSessionManager {
	//数据库表单存储路径
    //private final String DB_NAME = "android.db";默认存储在内存中
    private final String DB_NAME = "AndroidDevelopment/nc/miss08/database";修改存储路径为手机本地目录
    private DaoMaster daoMaster;
    private DaoSession daoSession;

    private DaoSessionManager() {
    }

    public static DaoSessionManager mInstance = new DaoSessionManager();

    public static DaoSessionManager getInstace() {

        return mInstance;
    }

    public DaoMaster getDaoMaster(Context mContext) {

        DaoMaster.DevOpenHelper mHelper = new DaoMaster
                                   .DevOpenHelper(mContext, DB_NAME, null);
        daoMaster = new DaoMaster(mHelper.getWritableDatabase());
        return daoMaster;
    }

    public DaoSession getDaoSession(Context mContext) {

        if (daoSession == null) {

            if (daoMaster == null) {
                getDaoMaster(mContext);
            }
            daoSession = daoMaster.newSession();
        }
        return daoSession;
    }
}

可以通过DaoSession获取实体类Song所对应的表单操作类SongDao并进行简单的CRUD操作。可见进行增删改查操作不需要写相应的sql语句,只需要调用songDao相关的API。

//获取Song这张表的操作类SongDao
DaoSession daoSession = DaoSessionManager.getInstace()
                                         .getDaoSession(getApplicationContext());
SongDao songDao = daoSession.getSongDao();
//创建一个对象
Song song = new Song();
song.setSingerCode(111);
//增加
songDao.insert(song);
//改
song.setSingerName("miss08");
songDao.update(song);
//查
Song query = songDao.queryBuilder().where(SongDao.Properties.SingerCode.eq(111))
        .list().get(0);
//删
songDao.delete(song);

4、源码分析
4.1)数据库版本升级
不更新数据库表单,只增加数据库版本号,会发现数据库所有数据被清空。
在基本数据库操作里,Sqlite的数据库更新是在SQLiteOpenHelper里的onUpgrade里进行的,在GreenDao框架里肯定会继承它来定制自己框架的需求,在DaoMaster里我们找到了GreenDao用来处理版本升级的类DevOpenHelper。

 /** WARNING: Drops all table on Upgrade! Use only during development. */
    public static class DevOpenHelper extends OpenHelper {
         
        ...
        @Override       //数据库版本升级会触发这个方法
        public void onUpgrade(Database db, int oldVersion, int newVersion) {
            dropAllTables(db, true);
            onCreate(db);
        }
    }

在其onUpgrade方法里会通过dropAllTables方法删除项目里所有的数据库,通过所有表单操作类XXXDao来删除所有表单。

 /** Drops underlying database table using DAOs. */
    public static void dropAllTables(Database db, boolean ifExists) {
       //项目里所有实体类操作Dao,都会在此删除表单,当前项目里只有一张表
        SongDao.dropTable(db, ifExists);
    }

    //找到SongDao的dropTable方法,就是执行一条删除表单的sql语句
    public static void dropTable(Database db, boolean ifExists) {
        String sql = "DROP TABLE " + (ifExists ? "IF EXISTS " : "") + "\"SONG\"";
        db.execSQL(sql);
    }

删除完成后,调用onCreate(db)方法创建所有表单,onCreate方法存在于DaoMaster里的OpenHelper类里,它是上面DevOpenHelper的父类

    public static abstract class OpenHelper extends DatabaseOpenHelper {
        ...
        @Override    //数据库创建时触发的方法
        public void onCreate(Database db) {
            Log.i("greenDAO"
            , "Creating tables for schema version " + SCHEMA_VERSION);
            createAllTables(db, false);
        }
    }

    //所有Dao都在此创建
    public static void createAllTables(Database db, boolean ifNotExists) {
        SongDao.createTable(db, ifNotExists);
    }

故,GreenDao默认在版本升级时会删除所有表单然后再创建,如果用户想自己控制版本升级的情况,就需要自己实现OpenHelper。
项目每次编译运行时,DaoMaster里的内容都会恢复成默认状态,所以不要在DaoMaster的DevOpenHelper里进行业务操作。
greenDAO有别于其他通过反射机制实现的ORM框架,greenDAO需要一个Java工程事先生成需要的文件,而在每一个DAO文件中都已经自动组装好创建和删除数据表的sql语句。

/** Creates the underlying database table. */
public static void createTable(SQLiteDatabase db, boolean ifNotExists) {
    String constraint = ifNotExists? "IF NOT EXISTS ": "";
    db.execSQL("CREATE TABLE " + constraint + "\\"NOTE\\" (" + //
            "\\"_id\\" INTEGER PRIMARY KEY AUTOINCREMENT ," + // 0: id
            "\\"TEXT\\" TEXT NOT NULL ," + // 1: text
            "\\"COMMENT\\" TEXT," + // 2: comment
            "\\"DATE\\" INTEGER);"); // 3: date
}
/** Drops the underlying database table. */
public static void dropTable(SQLiteDatabase db, boolean ifExists) {
    String sql = "DROP TABLE " + (ifExists ? "IF EXISTS " : "") + "\\"NOTE\\"";
    db.execSQL(sql);
}

这里会销毁之前的那张表然后重新创建一张新表,所以表里原有数据是不会存在的,这里只有传入的XXXDao.class做删除和重建的工作,其他表单是不会受影响的。
4.2)源码解析
1、GreenDao常见的类
第三章 存储_第8张图片
2、DaoGenerator
通过给定的模板Schema和路径生成相应的实体类和Dao相关的类。greenDAO有别于其他通过反射机制实现的ORM框架,greenDAO需要一个Java工程事先生成需要的文件,这个Java工程便是DaoGenerator。Schema的关系图如下:
第三章 存储_第9张图片

public class PatrolcheckDaoGenerator extends libDaoGenerator {
    //生成数据库管理文件路径
    public final static String OUTPATH = "D:\\androidxm\\githubme\\greenDaoMaster\\javaProject\\src\\main\\java";
    
    public final static String PACKAGENAME = "com.example.javaproject.greendao";

    public static void main(String[] args) throws Exception {
        PatrolcheckDaoGenerator mPatrolcheckDaoGenerator = new PatrolcheckDaoGenerator();
        Schema schema = new Schema(16, PACKAGENAME);
        mPatrolcheckDaoGenerator.addAll(schema);

        new DaoGenerator().generateAll(schema, OUTPATH);

    }

    @Override
    public void addAll(Schema schema) {
        super.addAll(schema);
        addListDat(schema);
        addUerDat(schema);
    }


    private void addListDat(Schema schema) {
        Entity tb = schema.addEntity("ListData");
        tb.addIntProperty("detail_level");
        tb.addStringProperty("display_name");
        tb.addStringProperty("parent_id");
        tb.addStringProperty("user_id");
        tb.addStringProperty("user_id1");
        tb.addStringProperty("user_id2");
    }
    private void addUerDat(Schema schema) {
        Entity tb = schema.addEntity("CommonCheckType");
        tb.addIdProperty();
        tb.addStringProperty("user_name");
        tb.addStringProperty("check_parent_name");
        tb.addIntProperty("check_parent_id");
        tb.addStringProperty("check_name");
        tb.addIntProperty("check_id");
        tb.addIntProperty("check_count");

    }

}

DaoGenerator里生成实体类、Daos和DaoMaster与DaoSeesion的方法generateAll

/** Generates all entities and DAOs for the given schema. */
public void generateAll(Schema schema, String outDir, String outDirEntity
, String outDirTest) throws Exception {

    List entities = schema.getEntities();
    for (Entity entity : entities) {
        generate(templateDao, outDirFile, entity.getJavaPackageDao()
           , entity.getClassNameDao(), schema, entity);
    }
    generate(templateDaoMaster, outDirFile, schema.getDefaultJavaPackageDao(),
            schema.getPrefix() + "DaoMaster", schema, null);
    generate(templateDaoSession, outDirFile, schema.getDefaultJavaPackageDao(),
            schema.getPrefix() + "DaoSession", schema, null);
}

3、DaoMaster:插件生成的daos的最顶层
注释:Master of DAO:knows all DAOs.
(1)包含项目所有表单的创建工作createAllTables和升级处理(先dropAllTables再createAllTables)

public static void createAllTables(Database db, boolean ifNotExists) {
    SingerDao.createTable(db, ifNotExists);
    MenuInfoDao.createTable(db, ifNotExists);
    SongDao.createTable(db, ifNotExists);
}

public static void dropAllTables(Database db, boolean ifExists) {
    SingerDao.dropTable(db, ifExists);
    MenuInfoDao.dropTable(db, ifExists);
    SongDao.dropTable(db, ifExists);
}

(2)DaoSession的创建方法,创建时会传入daoConfigMap,这个daoConfigMap是项目所有表单操作类XXXDao的class对象缓存,它是在创建DaoMaster时创建和存入class的。

public DaoSession newSession() {
    return new DaoSession(db, IdentityScopeType.Session, daoConfigMap);
}

(3)DaoMaster的构造方法,通过registerDaoClass方法把当前项目里所有的XXXDao的class对象缓存起来(这里不是实例对象,是class对象,一般用于反射)。

public DaoMaster(SQLiteDatabase db) {
    this(new StandardDatabase(db));
}

public DaoMaster(Database db) {
    super(db, SCHEMA_VERSION);
    registerDaoClass(SingerDao.class);
    registerDaoClass(MenuInfoDao.class);
    registerDaoClass(SongDao.class);
}

在DaoMaster的父类中AbstractDaoMaster缓存起来,
protected final Map>, DaoConfig> daoConfigMap;

protected void registerDaoClass(Class> daoClass) {
    DaoConfig daoConfig = new DaoConfig(db, daoClass);
    daoConfigMap.put(daoClass, daoConfig);
}

registerDaoClass方法缓存数据前,会创建一个DaoConfig,DaoConfig里面存储了很多Dao的基本数据,如下

    public final Database db;
    public final String tablename;
    public final Property[] properties;     

    public final String[] allColumns;
    public final String[] pkColumns;
    public final String[] nonPkColumns;

再看看DaoConfig的这个构造方法,通过reflectProperties方法获取到Property数组。Property里的数据描述了映射到数据库里列的属性。用于创建查询构建器使用的对象(包含所有select查询用到的条件操作)。

public DaoConfig(Database db, Class> daoClass) {
    this.db = db;
    try {
        this.tablename = (String) daoClass.getField("TABLENAME").get(null);
        Property[] properties = reflectProperties(daoClass);

        allColumns = new String[properties.length];

        List pkColumnList = new ArrayList();
        List nonPkColumnList = new ArrayList();
        Property lastPkProperty = null;
        for (int i = 0; i < properties.length; i++) {
            Property property = properties[i];
            String name = property.columnName;
            allColumns[i] = name;
            if (property.primaryKey) {
                pkColumnList.add(name);
                lastPkProperty = property;
            } else {
                nonPkColumnList.add(name);
            }
        }
    }
}

reflectProperties这个方法里,通过反射获取AbstractDao类的内部类Properties里所有的静态字段和public字段,然后再通过field.get(null)获取所有字段的属性值,存储这些属性值。

private static Property[] reflectProperties(Class> 
daoClass) throws ClassNotFoundException, IllegalArgumentException
, IllegalAccessException {
    Class propertiesClass = Class.forName(daoClass.getName() + "$Properties");
    Field[] fields = propertiesClass.getDeclaredFields();

    ArrayList propertyList = new ArrayList();
    final int modifierMask = Modifier.STATIC | Modifier.PUBLIC;
    for (Field field : fields) {
        if ((field.getModifiers() & modifierMask) == modifierMask) {
            Object fieldValue = field.get(null);
            if (fieldValue instanceof Property) {
                propertyList.add((Property) fieldValue);
            }
        }
    }
}

以SingerDao为例,看看其内部类的字段和属性:属性值就是一个Property对象,包含实体类的字段名,对应的数据库里的列名,以及类型

public class SingerDao extends AbstractDao {
    public static class Properties {
        public final static Property Id = new Property(0, Long.class
                              , "id", true, "_id");
        public final static Property SingerCode = new Property(1, String.class
                              , "singerCode", false, "SINGER_CODE");
        public final static Property SingerName = new Property(2, String.class
                              , "singerName", false, "SINGER_NAME");
        public final static Property SingerDesc = new Property(3, String.class
                              , "singerDesc", false, "SINGER_DESC");
    }
}

4、DaoSession:在构造里创建出XXXDao的实例对象。

public DaoSession(Database db, IdentityScopeType type, Map>, DaoConfig> daoConfigMap) {
    super(db);
    //首先从传入的daoConfigMap里获取DaoConfig,通过原型模式创建DaoConfig对象
    singerDaoConfig = daoConfigMap.get(SingerDao.class).clone();
    singerDaoConfig.initIdentityScope(type);
    menuInfoDaoConfig = daoConfigMap.get(MenuInfoDao.class).clone();
    menuInfoDaoConfig.initIdentityScope(type);
    songDaoConfig = daoConfigMap.get(SongDao.class).clone();
    songDaoConfig.initIdentityScope(type);
	
	//创建XXXDao的实例,DaoConfig对象做为Dao的构参传入
    singerDao = new SingerDao(singerDaoConfig, this);
    menuInfoDao = new MenuInfoDao(menuInfoDaoConfig, this);
    songDao = new SongDao(songDaoConfig, this);
    
	//在DaoSession的父类里用Map, AbstractDao>集合缓存实例类和它的操作类对象
    registerDao(Singer.class, singerDao);
    registerDao(MenuInfo.class, menuInfoDao);
    registerDao(Song.class, songDao);
}

DaoSession的父类AbstractDaoSession,在AbstractDaoSession提供基本增删改查的方法,但是数据库真正的执行者确是AbstractDao,从上面的缓存里获取的AbstractDao。

    public  long insert(T entity) {
        @SuppressWarnings("unchecked")
        AbstractDao dao = (AbstractDao) getDao(entity.getClass());
        return dao.insert(entity);
    }

    public  long insertOrReplace(T entity) {
        @SuppressWarnings("unchecked")
        AbstractDao dao = (AbstractDao) getDao(entity.getClass());
        return dao.insertOrReplace(entity);
    }

    public  void update(T entity) {
        @SuppressWarnings("unchecked")
        AbstractDao dao = (AbstractDao) getDao(entity.getClass());
        dao.update(entity);
    }

    public  void delete(T entity) {
        @SuppressWarnings("unchecked")
        AbstractDao dao = (AbstractDao) getDao(entity.getClass());
        dao.delete(entity);
    }

    public  List queryRaw(Class entityClass, String where
                  , String... selectionArgs) {
        @SuppressWarnings("unchecked")
        AbstractDao dao = (AbstractDao) getDao(entityClass);
        return dao.queryRaw(where, selectionArgs);
    }

    public  QueryBuilder queryBuilder(Class entityClass) {
        @SuppressWarnings("unchecked")
        AbstractDao dao = (AbstractDao) getDao(entityClass);
        return dao.queryBuilder();
    }

5、XyzDao 数据库真实操作类:提供所有数据库操作方式,通过查询语句分析操作过程。
GreenDao查询语句

singerDao.queryBuilder().where(Properties.SingerCode.eq("111")).orderAsc(Properties.SINGER_NAME).list();

对比sqlite查询

1、db.rawQuery(sql)
   select * from SINGER where SINGER_CODE = ‘111’ order by SINGER_NAME asc;

2、db.query(...)
   query(String table, String[] columns, String selection,String[] selectionArgs
                    , String groupBy, String having,String orderBy, String limit)
   
   query("Singer",null,"SINGER_CODE = ?",new String[]{"111"},"null","null"
                    ,"SINGER_NAME asc")

下面分析GreenDao的查询,在XXXDao的父类AbstractDao里找到queryBuilder方法,该方法里会创建一个QueryBuilder对象,我们来看看QueryBuilder的描述:使用约束条件和参数来构造实体对象的查询,而不使用SQL语句(QueryBuilder 为我们生成SQL语句)
QueryBuilder的where方法,使用一个类WhereCondition来收集所有的条件,这个在下面的build方法里会被用到。

public QueryBuilder where(WhereCondition cond, WhereCondition... condMore) {
    whereCollector.add(cond, condMore);
    return this;
}

QueryBuilder类的orderAsc方法,实际会调用下面这个方法,分析见里面注释。

private void orderAscOrDesc(String ascOrDescWithLeadingSpace, Property... properties) 
{
    for (Property property : properties) {

        //获取StringBuilder对象拼接字符串,没有就创建,存在就append(",")
        checkOrderBuilder();
        //首先判断排序的字段属于Singer表里字段,然后拼装该columnName
        append(orderBuilder, property);
        //字段是String类型并且stringOrderCollation不为空,
        if (String.class.equals(property.type) && stringOrderCollation != null) {
            //拼接stringOrderCollation =  " COLLATE NOCASE"
            orderBuilder.append(stringOrderCollation);
        }
        //拼装ASC
        orderBuilder.append(ascOrDescWithLeadingSpace);
    }
}

最终会创建一个orderBuilder,里面包含“SINGER_NAME asc”

最后看QueryBuilder类的list方法,list方法里先执行build方法,然后执行build方法返回对象的list方法。

public List list() {
    return build().list();
}

先找到build方法,build里会做很多拼接工作。

public Query build() {

    //创建一个StringBuilder对象,拼接工作下面分析
    StringBuilder builder = createSelectBuilder();

    //判断是否有LIMIT和OFFSET条件,如果有则拼接上
    int limitPosition = checkAddLimit(builder);
    int offsetPosition = checkAddOffset(builder);

    String sql = builder.toString();
    checkLog(sql);

    //返回一个Query对象,查询操作返回的结果(实体类或游标),values是position的值
    return Query.create(dao, sql, values.toArray(), limitPosition, offsetPosition);
}

重点看看上面build方法里执行的createSelectBuilder方法,他会拼接返回完整的用于查询条件的select语句

private StringBuilder createSelectBuilder() {

    //创建一个StringBuilder,拼接“SELECT FROM tablename”
    String select = SqlUtils.createSqlSelect(dao.getTablename(), tablePrefix
    , dao.getAllColumns(), distinct);
    StringBuilder builder = new StringBuilder(select);

    //如果上面的whereCollector条件集合有值,拼接“WHERE”和条件
    appendJoinsAndWheres(builder, tablePrefix);

    //orderBuilder如果存在则拼接“ORDER BY ”和orderBuilder,这个在上面分析过
    if (orderBuilder != null && orderBuilder.length() > 0) {
        builder.append(" ORDER BY ").append(orderBuilder);
    }

    //返回最终拼装的StringBuilder
    return builder;
}

再看最后的list方法调用,这个方法在类Query里,找到了我们最熟悉的Sqlite操作dao.getDatabase().rawQuery,查询传入我们上面拼接的sql语句和传入的参数值,得到游标,轮询就能返回结果。

public List list() {
    checkThread();
    Cursor cursor = dao.getDatabase().rawQuery(sql, parameters);
    return daoAccess.loadAllAndCloseCursor(cursor);
}

XXXDao对数据库的操作最终还是Sqlite里对sql语句的操作。体现了ORM面向对象操作和关系数据表操作的映射,所以要想熟练使用GreenDao对数据库的操作,必须得先掌握SQLite里对数据库的操作和常用的SQL语句的书写。

4、自己封装一个数据库存储框架?

其实这些库满足我们平时的开发需求足够,但是也有这些库办不到的,因此我们需要自己进行封装,我们可以利用Java高阶知识反射,泛型,注解等等封装一个内存优化良好,使用优雅的库,这要看你的思维啦!
(1)通过反射(+注解)获取字段名和属性值进行映射
(2)对表结构缓存

(3.6)数据库升级(数据库数据迁移)

数据库表的设计往往不是一开始就非常完美,可能在应用版本开发迭代中,表的结构也需要调整,这时候就涉及到数据库升级的问题了。数据库升级,主要有以下这几种情况:
1、增加表
2、删除表
3、修改表 (1)增加表字段(2)删除表字段
增加表和删除表问题不大,因为它们都没有涉及到数据的迁移问题,增加表只是在原来的基础上CRTATE TABLE,而删除表就是对历史数据不需要了,那只要DROP TABLE即可。那么修改表呢?需要对数据迁移进行处理。
主要步骤是:
1、编写数据迁移业务
2、在onUpgrade函数中升级
步骤1:数据迁移
基本思想:(1)将现有表命名为临时表(2)创建新表(3)将临时表的数据导入新表(4)删除临时表
(1)命名为临时表

 			//Rename table
            String tempTableName = tableName + "_temp";
            String sql = "ALTER TABLE "+tableName+" RENAME TO "+tempTableName;
            db.execSQL(sql);

(2)创建新表

			//Create table
            try {
                sql = TableUtils.getCreateTableStatements(cs, clazz).get(0);
                db.execSQL(sql);
            } catch (Exception e) {
                e.printStackTrace();
                TableUtils.createTable(cs, clazz);
            }

(3)导入数据

           //Load data
            String columns;
            if(type == OPERATION_TYPE.ADD){
            //添加字段
                columns = Arrays.toString(getColumnNames(db,tempTableName)).replace("[","").replace("]","");
            }else if(type == OPERATION_TYPE.DELETE){
            //删除字段
                columns = Arrays.toString(getColumnNames(db,tableName)).replace("[","").replace("]", "");
            }else {
                throw new IllegalArgumentException("OPERATION_TYPE error");
            }
            sql = "INSERT INTO "+tableName +
                    " ("+ columns+") "+
                    " SELECT "+ columns+" FROM "+tempTableName;
            db.execSQL(sql);

(4)删除临时表

			//Drop temp table
            sql = "DROP TABLE IF EXISTS "+tempTableName;
            db.execSQL(sql);

步骤2:在onUpgrade中进行逻辑判断
考虑逐级升级和跨级升级两种模式

 	@Override
    public void onUpgrade(SQLiteDatabase db, ConnectionSource connectionSource, int oldVersion, int newVersion) {
        if(oldVersion < 2){
            DatabaseUtil.upgradeTable(db,connectionSource,A.class,DatabaseUtil.OPERATION_TYPE.ADD);
            DatabaseUtil.upgradeTable(db,connectionSource,B.class,DatabaseUtil.OPERATION_TYPE.DELETE);
        }
        if(oldVersion < 3){
            DatabaseUtil.upgradeTable(db,connectionSource,C.class,DatabaseUtil.OPERATION_TYPE.ADD);
        }
        onCreate(db,connectionSource);
    }

(4)方法4:ContentProvider

(4.1)简介

作为Android四大组件之一,ContentProvider一般为存储和获取数据提供统一的接口,可以在不同的应用程序之间共享数据。它有2个主要作用:
1.封装。对数据进行封装,提供统一的接口。
2.提供一种跨进程数据共享的方式。

(4.2)重要的类

1、ContentProvider内容提供者
2、ContentResolver内容接受者
3、URI统一资源定位符,唯一标识ContentProvider所提供的数据(资源)
uri的组成:
在这里插入图片描述
A:标准前缀
用来说明一个Content Provider控制这些数据,无法改变的;content://
B:URI 的标识(包名+类名)
用于唯一标识这个ContentProvider,外部调用者可以根据这个标识来找到它。它定义了是哪个Content Provider提供这些数据。对于第三方应用程序,为了保证URI标识的唯一性,它必须是一个完整的、小写的类名。
这个标识在 元素的authorities属性中说明:一般是定义该ContentProvider的包.类的名称
C:路径(path)(数据库名)
通俗的讲就是你要操作的数据库中表的名字,或者你也可以自己定义,记得在使用的时候保持一致就可以了;content://com.bing.provider.myprovider/tablename
D:记录的ID
如果URI中包含表示需要获取的记录的ID;则就返回该id对应的数据,如果没有ID,就表示返回全部; content://com.bing.provider.myprovider/tablename/# #表示数据id。

(4.3)使用

1、基本使用

//获取手机联系人
Cursor cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null, null, null, null);
while (cursor.moveToNext()) {
          String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
          String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
          Log.e("TAG", displayName + "," + number);//联系人姓名 + 手机号
          }

2、自定义ContentProvider——UriMatcher类用于匹配Uri

//常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码
    UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//如果match()方法匹配content://com.bing.procvide.personprovider/person路径,返回匹配码为1
sMatcher.addURI("com.bing.procvide.personprovider", "person", 1);//添加需要匹配uri,如果匹配就会返回匹配码
//如果match()方法匹配content://com.bing.provider.personprovider/person/230路径,返回匹配码为2
        sMatcher.addURI("com.bing.provider.personprovider", "person/#", 2);//#号为通配符

        switch (sMatcher.match(Uri.parse("content://com.ljq.provider.personprovider/person/10"))) {
        case 1
        break;
        case 2
        long id = ContentUris.parseId(uri);// 从uri中取出id
        break;
default://不匹配
        break;
        }

3、数据共享
3.1)清单文件


3.2)application标签目录




3.3)获取Provider提供参数

Uri uri = Uri.parse("content://com.allens.test.MyProvider/T_UserInfo");// 对外的URI  之前说的 URI 主成的部分

Cursor cursor = getContentResolver().query(uri, new String[]{"id", "account", "name", "pwd"}, null, null, null);
    while (cursor.moveToNext()) {
            Log.e("TAG", "id--->" + cursor.getString(0));
            Log.e("TAG", "account--->" + cursor.getString(1));
            Log.e("TAG", "name--->" + cursor.getString(2));
            Log.e("TAG", "pwd--->" + cursor.getString(3));
            }

(5)方法5:网络存储

与后台交互,将数据存储在后台数据库中

(二)Android保存网络图片到系统相册

(1)基本流程

下载图片,并再系统相册中显示:
1、图片处理
对图片的下载,图片到Bitmap对象的转换,Bitmap对象的格式转换和压缩
2、图片保存
确定存储路径->获取外部存储权限->确定外部存储状态->确定文件名->保存到文件中->发送广播,通知系统扫描保存后的文件

(2)源码解析

(2.1)确定存储路径

1、内部存储/data/data/packageName/(不采用)
一个应用对内部存储的所有访问都被限制在这个文件夹中,也就是说Android应用只能在该目录中读取,创建,修改文件。对该目录之外的其他内部存储中的目录都没有任何操作的权限。
因此,如果将图片保存在内部存储中,只能被应用自身读取,其他应用均无法读取。如果需要让系统图库,相册或其他应用能够找到保存的图片,必须将图片保存到外部存储中。
2、外部存储
(1)/storage/emulated/0/Android/data/packageName/(不采用)
这个路径同样只能被应用自身读取,其他应用不能访问。因此,也不能将图片保存在这个目录中。
(2)/storage/emulated/0/packageName/image/(采用)
除外部存储的/Android目录之外的其他目录一般都是可以被其他应用访问的。目前,大多数应用都会在外部存储的根路径下建立一个类似包名的多层目录,以存储需要共享的文件。
获取外部存储路径:由于Android系统的碎片化问题,不同设备上外部存储的路径很可能会不同,因此,不能直接使用/storage/emulated/0/作为外部存储的根路径。 Android SDK中 Environment类 提供了getExternalStorageDirectory()方法来获取外部存储的根路径。

Environment.getExternalStorageDirectory().getAbsolutePath();// /storage/emulated/0
String dir = Environment.getExternalStorageDirectory().getAbsolutePath() + "/tencent/MicroMsg/WeiXin/";

(2.2)获取外部存储权限


(2.3)确定外部存储状态

由于外部存储需要被挂载,也可以被卸载,在写入文件之前,需要先判断外部存储的状态是否正常。只有状态正常情况下才可以执行保存文件的操作。
挂载(mounting)是指由操作系统使一个存储设备(诸如硬盘、CD-ROM或共享资源)上的计算机文件和目录可供用户通过计算机的文件系统访问的一个过程。

	//获取内部存储状态
    String state = Environment.getExternalStorageState();
	//如果状态不是mounted,无法读写
	if (!state.equals(Environment.MEDIA_MOUNTED)) {
        return;
        }

(2.4)确定文件名

保存的图片文件名可以由应用根据自身需要自行确定,一般来说需要有一个命名规则,然后根据命名规则计算得到文件名。 常用:
(1)时间命名
根据保存图片的当前系统时间来对图片命名。

Calendar now = new GregorianCalendar();
SimpleDateFormat simpleDate = new SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault());
String fileName = simpleDate.format(now.getTime());

(2)文件URL命名
每张网络图片都有一个对应的图片URL,可以根据图片的URL来对图片命名。

(2.5)保存到文件中

try {
        File file = new File(dir + fileName + ".jpg");
        FileOutputStream out = new FileOutputStream(file);
        mBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);//将Bitmap压缩到一个文件输出流
        out.flush();
        out.close();
        } catch (Exception e) {
        e.printStackTrace();
        }

(2.6)发送广播,通知系统扫描保存后的文件

将Bitmap对象保存成外部存储中的一个jpg格式的文件。为了让其他应用能够知道图片文件被创建,必须通知MediaProvider服务将新建的文件添加到图片数据库中。
Android系统中常驻一个MediaProvider服务,对应的进程名为android.process.media,此服务用来管理本机上的媒体文件,提供媒体管理服务。在系统开机或者收到外部存储的挂载消息后,MediaProvider会调用MediaScanner,MediaScanner会扫描外部存储中的所有文件,根据文件类型的后缀将文件信息保存到对应的数据库中,供其他APP使用。
MediaScannerReceiver是一个广播接收者,当它接收到特定的广播请求后,就会去扫描指定的文件,并根据文件信息将其添加到数据库中。当图片文件被创建后,就可以发送广播给MediaScannerReceiver,通知其扫描新建的图片文件。

//保存图片后发送广播通知更新系统图库(将图片保存在系统图库)
Uri uri = Uri.fromFile(file);
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));

(2.7)大图/多图的异步保存

保存图片文件时,如果图片很大,或需要同时保存多张图片时,就需要较多的时间。为了避免阻塞UI线程,出现帧率下降或ANR,通常需要将图片保存操作放到线程中去执行。当图片保存完毕后通过sendMessage()方法通知UI线程保存结果。

(2.8)完整代码

1、保存Bitmap到本地指定路径下
2、通过广播,通知系统相册图库刷新数据

public class ImgUtils {
    //保存文件到指定路径
    public static boolean saveImageToGallery(Context context, Bitmap bmp) {
        // 首先保存图片
        String storePath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "dearxy";
        File appDir = new File(storePath);
        if (!appDir.exists()) {
            appDir.mkdir();
        }
        String fileName = System.currentTimeMillis() + ".jpg";
        File file = new File(appDir, fileName);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            //通过io流的方式来压缩保存图片
            boolean isSuccess = bmp.compress(Bitmap.CompressFormat.JPEG, 60, fos);
            fos.flush();
            fos.close();

            //把文件插入到系统图库
            //MediaStore.Images.Media.insertImage(context.getContentResolver(), file.getAbsolutePath(), fileName, null);

            //保存图片后发送广播通知更新数据库
            Uri uri = Uri.fromFile(file);
            context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
            if (isSuccess) {
                return true;
            } else {
                return false;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
}

你可能感兴趣的:(Android学习之旅)