Android专项功能剖析中剖析过,Android的功能优化根本分为两个部分,即

  • 资源类
  • 交互类

重点整治也在资源类功能中,Android设备是一个移动式便携设备的代表,此类设备对内存的要求是十分高的,虽然现在的手机内存发展现已很好了,可是内存问题在开发中依然面临着严峻的调整。

之前也剖析过内存写入扩大问题,其直接导致的结果便是磁盘I/O的耗时会产生剧烈的波动,导致运用卡顿今日就内存写入扩大做一个深化的剖析,列举一些在开发中经常遇到的场景。

内存写入扩大问题

Android内存写入扩大问题(Memory Write Amplification,MWA)是指在进行写入操作时,因为数据存储在闪存中的特别结构,导致实践写入的数据比要写入的数据更多,然后导致内存空间的糟蹋和闪存寿数的缩短。

简略来讲,闪存中的存储单元是以“页”为单位进行读写操作的。当需求对某个页中的某个数据进行写入时,因为闪存的特别结构,需求先将整个页读入到内存中,然后对需求写入的数据进行修正,最终再将整个页写回到闪存中。这个过程中,实践上写入了整个页,而不仅仅是要修正的数据,这便是内存写入扩大问题。

这类问题会导致的几个比较官方的问题便是:

  1. 内存空间糟蹋:实践写入的数据比要写入的数据更多,会占用更多的内存空间。
  2. 闪存寿数缩短:闪存的寿数是由它能够承受的擦除次数决定的,内存写入扩大会导致实践写入的数据量添加,然后缩短闪存的寿数。
  3. 功能下降:内存写入扩大会添加读写操作的次数,然后下降读写功能。

当然,当下不会存在特别严重的的内存写入扩大问题,闪存厂商为了处理内存写入扩大问题,通常会采用一些优化策略,例如多级缓存、写入扩大算法等。

所以咱们首要针对第三条进行剖析。

举个比如

(闪存中存储单元是以“页”为单位进行读写操作的,而每个页的巨细通常为4KB或8KB等。)

假如需求将一个1KB的数据写入到一个4KB的页中,实践上会将整个4KB的页读入内存中,将需求修正的1KB数据写入到内存中,然后将整个4KB的页写回到闪存中。这就导致了3KB的内存空间被糟蹋,然后下降了存储效率。

换句话说便是,

当需求将一个小于闪存页巨细的数据写入到闪存中时,因为闪存存储单元的特别结构,通常会将整个闪存页读入内存中,然后将修正后的整个闪存页从头写回闪存中,这样就会导致内存写入扩大问题。

Android 中常见的会导致写入扩大效应产生的写法总结

  1. 运用 SQLite 数据库进行操作时,假如对表中的某个字段进行修正,通常会导致整行数据被读入内存中进行修正,然后从头写回到数据库中,这就会导致内存写入扩大问题。
  2. 运用SharedPreferences进行操作时,修正其间一个key所对应的value值会将整个SharedPreferences文件读入内存中,然后修正对应的value值,最终将整个文件从头写回磁盘中,相同也会导致内存写入扩大问题。
  3. 在运用文件操作时,假如需求修正文件中的一个小块数据,可是文件体系以块为单位进行读写操作,因此需求将整个块读入内存中进行修正,然后再将整个块从头写回文件中,这就会导致内存写入扩大问题。
  4. 运用 Bitmap 操作时,假如对一个 Bitmap 进行修正,例如旋转或缩放等操作,通常会导致整个 Bitmap 目标从头分配内存空间,然后导致内存写入扩大问题。

用 SQLite 数据库进行操作时导致内存写入扩大问题

问题

假设有一个用户信息表,其间包含用户的姓名和手机号码两个字段。现在需求更新某个用户的手机号码,能够运用以下代码完成:

ContentValues values = new ContentValues();
values.put("phone", newPhone);
String whereClause = "name=?";
String[] whereArgs = new String[]{name};
db.update("user", values, whereClause, whereArgs);

这段代码会将整行数据读入内存中进行修正,然后从头写回数据库中,假如数据表十分大,就会导致内存写入扩大问题。

处理办法

能够运用 SQLite 的 REPLACE INTO 句子,该句子能够直接更新指定字段,而不需求将整行数据读入内存中:

String sql = "REPLACE INTO user(name, phone) VALUES (?, ?)";
db.execSQL(sql, new String[]{name, newPhone});

运用SharedPreferences进行操作时导致内存写入扩大问题

问题

假设有一个存储用户登录信息的SharedPreferences文件,其间包含用户名和暗码两个字段。现在需求更新暗码,能够运用以下代码完成:

SharedPreferences sp = context.getSharedPreferences("user_login", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString("password", newPassword);
editor.apply();

将整个SharedPreferences文件读入内存中,然后修正暗码值,最终将整个文件从头写回磁盘中,导致了内存写入扩大问题。

处理办法

能够运用FileOutputStream对应的FileChannel进行文件操作,这样能够将修正后的值直接写入文件,而不需求读取整个SharedPreferences文件

File file = new File(context.getFilesDir(), "user_login.xml");
try (FileOutputStream fos = new FileOutputStream(file);
     FileChannel channel = fos.getChannel()) {
    String xml = "<map><string name=\"username\">" + username + "</string><string name=\"password\">" + newPassword + "</string></map>";
    ByteBuffer buffer = ByteBuffer.wrap(xml.getBytes());
    channel.write(buffer);
} catch (IOException e) {
    e.printStackTrace();
}

在运用文件操作时导致内存写入扩大问题

问题

假设有一个记载用户日志的文件,每条日志记载占用256字节,现在需求修正其间一条记载的内容,能够运用以下代码完成:

RandomAccessFile raf = new RandomAccessFile("user.log", "rw");
byte[] buffer = new byte[256];
raf.seek(recordOffset);
raf.read(buffer);
// 修正buffer中对应记载的内容
raf.seek(recordOffset);
raf.write(buffer);
raf.close();

这段代码会将整个256字节块读入内存中进行修正,然后从头写回文件中,导致了内存写入扩大问题。

处理办法

能够运用内存映射文件的办法来防止内存写入扩大问题。例如,能够运用MappedByteBuffer对文件进行操作,该类能够将文件映射到内存中,并且只有在需求写回磁盘时才会进行写操作。以下是修正用户日志记载的示例代码

RandomAccessFile raf = new RandomAccessFile("user.log", "rw");
FileChannel channel = raf.getChannel();
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, recordOffset, 256);
// 修正map中对应记载的内容
map.force(); // 强制将修正写回磁盘
channel.close();
raf.close();

文件读写时还可能遇到读写次数过多问题,这个我在Bitmap 解码优化中提到过,能够看看,十分有优化价值。

在运用Bitmap进行操作时导致内存写入扩大问题

问题

假设有一个需求处理很多图片的运用,现在需求对一张图片进行裁剪操作,能够运用以下代码完成:

Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
Bitmap croppedBitmap = Bitmap.createBitmap(bitmap, x, y, width, height);
FileOutputStream fos = new FileOutputStream(outputPath);
croppedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos);
fos.flush();
fos.close();

这段代码会将整张图片读入内存中进行裁剪操作,然后将裁剪后的图片从头写回磁盘中,导致了内存写入扩大问题。

处理办法

运用BitmapRegionDecoder对图片进行裁剪,该类能够只解码出需求的图片区域,并且不会将整张图片读入内存中。以下是运用BitmapRegionDecoder完成裁剪的示例代码:

InputStream is = new FileInputStream(imagePath);
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
Bitmap croppedBitmap = decoder.decodeRegion(new Rect(x, y, x + width, y + height), null);
FileOutputStream fos = new FileOutputStream(outputPath);
croppedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos);
fos.flush();
fos.close();

这段代码会运用BitmapRegionDecoder只解码出需求的图片区域,然后将裁剪后的图片写回磁盘中,防止了内存写入扩大问题。

在运用IO流进行操作时导致内存写入扩大问题

问题

假设有一个需求将数据写入文件的运用,现在需求写入一个大文件,能够运用以下代码完成:

File inputFile = new File(inputPath);
File outputFile = new File(outputPath);
FileInputStream fis = new FileInputStream(inputFile);
FileOutputStream fos = new FileOutputStream(outputFile);
byte[] buffer = new byte[1024 * 1024]; // 1MB buffer
int len;
while ((len = fis.read(buffer)) != -1) {
    fos.write(buffer, 0, len);
}
fis.close();
fos.flush();
fos.close();

这段代码会将大文件分成若干个1MB巨细的块,然后将每个块读入内存中进行写入操作,导致了内存写入扩大问题。

处理办法

能够运用FileChannel进行文件操作,该类能够将文件映射到内存中,并且支撑直接对内存进行操作,而不需求将数据读入内存中。以下是运用FileChannel完成文件写入的示例代码:

File inputFile = new File(inputPath);
File outputFile = new File(outputPath);
FileInputStream fis = new FileInputStream(inputFile);
FileOutputStream fos = new FileOutputStream(outputFile);
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); // 1MB buffer
while (inChannel.read(buffer) != -1) {
    buffer.flip();
    outChannel.write(buffer);
    buffer.clear();
}
inChannel.close();
outChannel.force(true); // 强制将修正写回磁盘
outChannel.close();
}
}

在运用数据库进行操作时导致内存写入扩大问题

问题

假设有一个需求运用SQLite数据库进行操作的运用,现在需求刺进很多数据到数据库中,能够运用以下代码完成:

SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
try {
    for (int i = 0; i < count; i++) {
        ContentValues values = new ContentValues();
        values.put("column1", value1);
        values.put("column2", value2);
        values.put("column3", value3);
        db.insert("table", null, values);
    }
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}

这段代码会将很多数据刺进到数据库中,导致了内存写入扩大问题

处理办法

能够运用SQLiteDatabase的insertWithOnConflict()办法刺进多条数据,该办法能够将多条数据刺进到数据库中,而不需求将一切数据读入内存中。以下是运用insertWithOnConflict()办法刺进多条数据的示例代码:

SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
try {
    for (int i = 0; i < count; i++) {
        ContentValues values = new ContentValues();
        values.put("column1", value1);
        values.put("column2", value2);
        values.put("column3", value3);
        db.insertWithOnConflict("table", null, values, SQLiteDatabase.CONFLICT_REPLACE);
    }
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}

运用insertWithOnConflict()办法将多条数据刺进到数据库中,防止了内存写入扩大问题。

其他问题

  • 在运用反射进行操作时,频繁地创立Class目标
  • 在运用多个线程进行操作时,运用同步锁进行线程同步
  • 在运用很多字符串进行操作时,将多个字符串拼接为一个字符串
  • 在运用很多字符串进行操作时导致内存写入扩大问题(循环拼接等)
  • 在运用网络通信进行操作时导致内存写入扩大问题(网络中获取到的数据悉数读入内存中,然后再写入到输出流中就会导致此问题呈现,能够运用Java的NIO的FileChannel 将数据从输入流直接写到输出流中,当然网络在Android中现已不需求咱们过分关怀了)
  • 其他问题(遇到或听到会弥补)

总结

咱们有没有发现,这类问题都是平常写代码中遇到的问题,并且有的问题还是高频呈现的,并且咱们都知道这个问题,就像最初第一次听到内存颤动时咱们都很懵逼,知其然后又感觉很简略一样,仅仅叫法不同而已,在移动功能要求日益严厉的今日,在占面试一多半技能点的今日,咱们还是要注重起来的。在Android专项功能剖析中剖析过各种工具的运用办法,这篇文章出自对微信功能剖析一书的总结,咱们有兴趣能够自己研读,在这里在简略提一下,咱们已然知道这类问题了,那一定要掌握这类问题的衡量和定位规范,以及怎么发现这类问题(其实这类问题更多是经历发现,内存问题存在都不是剑拔弩张的,都是日积月累的),咱们经常用的有:

写入扩大问题最直观的体现是运用程序在运行过程中占用的内存过高,可能导致体系内存不足,然后影响运用程序的功能和稳定性。一般来说,咱们能够通过以下几种办法来衡量和定位这个问题:

  1. 运用内存剖析工具,如Android Profiler、MAT等,来监控运用程序的内存运用情况,并查看内存运用情况是否存在反常。

  2. 在运用程序运行过程中,运用Log输出运用程序的内存运用情况,如可用内存、已用内存等,以便在后期进行剖析。

  3. 运用dumpsys指令来获取运用程序的内存运用情况,如:

    adb shell dumpsys meminfo<package_name>
    

    这条指令能够输出运用程序当时的内存运用情况,包括堆内存、Native堆内存、Dalvik堆内存等信息。

  4. 运用GC日志来剖析内存运用情况,GC日志能够记载运用程序中的内存分配、回收等信息,通过剖析GC日志能够发现内存运用情况是否存在反常。