如何解决写入放大效应导致的内存问题

Posted 嘴巴吃糖了

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何解决写入放大效应导致的内存问题相关的知识,希望对你有一定的参考价值。

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日志可以发现内存使用情况是否存在异常。

作者:狼窝山下的青年
链接:https://juejin.cn/post/7202164243612532792

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。


相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集

二、源码解析合集


三、开源框架合集


欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

以上是关于如何解决写入放大效应导致的内存问题的主要内容,如果未能解决你的问题,请参考以下文章

spring cloud熔断器Hystrix

springcloud分布式微服务:熔断器Hystrix

SpringCloud微服务架构之断路器,如何解决微服务中的雪崩效应?

解决android中EditText导致的内存泄漏问题

服务熔断(HystrixTurbine)

容错机制和熔断(Hystrix)