OOM讲课内容:图片缓存

2024-01-22 23:58
文章标签 图片 内容 缓存 讲课 oom

本文主要是介绍OOM讲课内容:图片缓存,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

OOM讲课内容

 

OOM现象:

05:15:04.764: ERROR/dalvikvm-heap(264): 3528000-byte external allocation too large for … 

05:15:04.764: ERROR/(264): VM won’t let us allocate 3528000 bytes 

05:15:04.764: DEBUG/skia(264): — decoder->decode returned false 

05:15:04.774: DEBUG/AndroidRuntime(264): Shutting down VM

这几句的意思是,我们的程序申请需要3528000byte太大了,虚拟机不同意给我们,虚拟机shut down自杀了。这个现象,比较常见在需要用到很多图片或者要用很大图片的APP开发中。

OOM(Out Of Memory)错误,什么是 OOM

通俗讲就是当我们的APP需要申请一块内存用来装图片的时候,系统觉得我们的APP所使用的内存已经够多了,不同意给我们的APP更多的内存,即使手机系统里还有1G空余的内存,然后系统抛出OOM,程序弹框shut down

为什么有OOMOOM的必然性!

因为android系统app的每个进程或者每个虚拟机有个最大内存限制,如果申请的内存资源超过了这个限制,系统就会抛出OOM错误。跟整个设备的剩余内存没太大关系。比如比较早的android系统一个虚拟机最多16M内存,当一个app启动后,虚拟机不停的申请内存资源用来装载图片,当超过内存上限时就OOM

Android系统APP内存限制怎么确定的?

AndroidAPP内存组成:

APP内存由dalvik内存和 native内存2部分组成,dalvik也就是 java堆,创建的对象就是在这里分配的,而native是通过 c/c++ 方式申请的内存,Bitmap就是以这种方式分配的(android3.0 以后,系统都默认是通过dalvik分配的,native作为堆来管理)。这2部分加起来不能超过 android 对单个进程、虚拟机的内存限制。

每个手机的内存限制大小是多少?

ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

 activityManager.getMemoryClass();

以上方法会返回以M为单位的数字,不同的系统平台或设备上的值都不太一样,比如:HTC G7默认 24MGalaxy 36Memulator-2.3 24M,等等。我的moto xt681 42M3.0系统的设备默认是48M

上面取到是虚拟机的最大内存资源。而对于heap堆的大小限制,可以查看/system/build.prop文件。

dalvik.vm.heapstartsize=5m

dalvik.vm.heapgrowthlimit=48m

dalvik.vm.heapsize=256m 

heapsize参数表示单个进程heap可用的最大内存,但如果存在如下参数:

dalvik.vm.heapgrowthlimit=48m表示单个进程heap内存被限定在48m,即程序运行过程中实际只能使用48m内存

为什么android系统设定APP的内存限制?

1,要使开发者内存使用更为合理。限制每个应用的可用内存上限,可以防止某些应用程序恶意或者无意使用过多的内存,而导致其他应用无法正常运行。Android是有多进程的,如果一个进程(也就是一个应用)耗费过多的内存,其他的应用无法运行了。 因为有了限制,使得开发者必须好好利用有限的资源,优化资源的使用。

2,即使有万千图片、千万数据需要使用到,但是特定时刻需要展示给用户看的总是有限,因为设备的屏幕显示就那么大,上面可以放的信息就是很有限的。大部分信息都是出于准备显示状态,所以没必要给予太多heap内存。也就是出现OOM现象,绝大部分原因是我们的程序设计上有问题,需要优化。比如可以通过时间换空间,不停的加载要用的图片,不停的回收不用的图片,把大图片解析到适合手机屏幕大小的图片等。

3android上的APP使用独立虚拟机,每开一个应用就会打开至少一个独立的虚拟机。这样可以避免虚拟机崩溃导致整个系统崩溃,同时代价就是需要浪费更多内存。这些设计确保了android的稳定性

不是androidgc会自动回收资源么,为什么还会OOM

Android不是用gc会自动回收资源么,为什么app的哪些不用的资源不回收呢?

Androidgc会按照特定的算法回收程序不用的内存资源,避免app的内存申请越积越多。但是Gc一般回收的资源是哪些无主的对象内存或者软引用的资源,或者更软的引用资源,比如:

Bitmap bt= BitmapFactory.decodeResource(this.getResources(), R.drawable.splash);

使用bt…   //此时的图片资源是强应用,是有主的资源。

bt=null;

此时这个图片资源就是无主的了,gc心情好的时候就会去回收它。

Bitmap bt= BitmapFactory.decodeResource(this.getResources(), R.drawable.splash);

SoftReference< Bitmap > SoftRef=new SoftReference< Bitmap >(bt); 

bt=null;

其他代码.。当程序申请很多内存资源时,gc有可能会释放SoftRef引用的这个图片内存。

bt=SoftRef.get(); 此时可能得到的是null;需要从新加载图片。当然这也说明了用软引用图片资源的好处,就是gc会自动根据需要释放资源,一定程度上避免OOM

TIPS:编程要养成的习惯,不用的对象置null。其实更好是,不用的图片直接recycle。因为通过置null,让gc来回收,有时候还是会来不及。

For Android specific we should use the 'recycle' method rather than 'gc', because 'recycle' will free the memory at the same time, but calling 'gc' doesn't guaranty to run and free the memory for same time(if it is not too critical, we should not call gc in our code) and results can very every time.
One more thing using 'recycle' is faster than the 'gc' and it improves the performance.

怎么查看APP内存分配情况?

1通过DDMS中的Heap选项卡监视内存情况:

Heap视图中部有一个叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。

data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量。

如果代码中存在没有释放对象引用的情况,则data objectTotal Size值在每次GC后不会有明显的回落,随着操作次数的增多Total Size的值会越来越大,
  直到到达一个上限后导致进程被kill掉。

2,在APP可以通过Runtime类的totalMemory() ,freeMemory() 两个方法获取VM的一些内存信息,如:

Runtime.getRuntime().freeMemory();

Runtime.getRuntime().totalMemory ();

3adb shell dumpsys meminfo com.android.demo

避免OOM的几个注意点:

1,适当调整图像大小,因为手机屏幕尺寸有限,分配给图像的显示区域有限,尤其对于超大图片,加载自网络或者sd卡,图片文件体积达到几M或者十几M的:

加载到内存前,先算出该bitmap的大小,然后通过适当调节采样率使得加载的图片刚好、或稍大即可在手机屏幕上显示就满意了:

BitmapFactory.Options opts = new BitmapFactory.Options();  

        opts.inJustDecodeBounds = true;  

        BitmapFactory.decodeFile(imageFile, opts);  //此时不加载实际图片,只获取到图片的宽高,大致可以通过宽度*高度*4来估算图片大小。

        opts.inSampleSize = computeSampleSize(opts, minSideLength, maxNumOfPixels);  // Android提供了一种动态计算的方法computeSampleSize

        opts.inJustDecodeBounds = false;  

        try {  

            return BitmapFactory.decodeFile(imageFile, opts);  

        } catch (OutOfMemoryError err) {  

        }  

2,在ListViewGallery等控件中一次性加载大量图片时,只加载屏幕显示的资源,尚未显示的不加载,移出屏幕的资源及时释放,可以采用强引用+软引用2级缓存方式,提高加载性能。

3,缓存图像到内存,采用软引用缓存到内存,而不是在每次使用的时候都从新加载到内存;

4,采用低内存占用量的编码方式,比如Bitmap.Config.ARGB_4444Bitmap.Config.ARGB_8888更省内存;

5,及时回收图像,如果引用了大量Bitmap对象,而应用又不需要同时显示所有图片,可以将暂时用不到的Bitmap对象及时回收掉。对于一些明确知道图片使用情况的场景可以主动recycle。比如:

App的启动splash画面上的图片资源,使用完就recycle;对于帧动画,可以加载一张,画一张,释放一张。

6,不要在循环中创建过多的本地变量; 慎用static,用static来修饰成员变量时,该变量就属于该类,而不是该类的实例,它的生命周期是很长的。如果用它来引用一些资源耗费过多的实例,这时就要谨慎对待了。

public class ClassName {  

     private static Context mContext;  

     //省略  

}  

如果将Activity赋值到mContext的话。即使该Activity已经onDestroy,由于仍有对象保存它的引用,因此该Activity依然不会被释放。

7,自定义堆内存分配大小,优化Dalvik虚拟机的堆内存分配;

App避免OOM的几种方式

 1,直接nullrecycle

对于app里使用的大量图片,采用方式:使用时加载,不显示时直接置nullrecycle

这样处理是个好习惯,基本上可以杜绝OOM。但是缺憾是代码多了,可能会忘记某些资源recycle。而且有些情况下会出现特定的图片反复加载、释放、再加载等,低效率的事情。

2,简单通过SoftReference引用方式管理图片资源

建个SoftReference的hashmap

使用图片时先查询这个hashmap是否有SoftReference,SoftReference里的图片是否空;

如果空就加载图片到SoftReference并加入hashmap

无需在代码里显式的处理图片的回收和释放,gc会自动处理资源的释放。

这种方式处理起来简单实用,能一定程度上避免前一种方法反复加载释放的低效率。但还不够优化。

3,强引用+软引用二级缓存 + SD卡三级

Android示范程序ImageDownloader.java,使用了一个二级缓存机制。就是有一个数据结构中直接持有解码成功的Bitmap对象引用,同时使用一个二级缓存数据结构保持淘汰的Bitmap对象的SoftReference对象,由于SoftReference对象的特殊性,系统会在需要内存的时候首先将SoftReference对象持有的对象释放掉,也就是说当VM发现可用内存比较少了需要触发GC的时候,就会优先将二级缓存中的Bitmap回收,而保有一级缓存中的Bitmap对象用于显示。

其实这个解决方案最为关键的一点是使用了一个比较合适的数据结构,那就是LinkedHashMap类型来进行一级缓存Bitmap的容器,由于LinkedHashMap的特殊性,我们可以控制其内部存储对象的个数并且将不再使用的对象从容器中移除,放到SoftReference二级缓存里,我们可以在一级缓存中一直保存最近被访问到的Bitmap对象,而已经被访问过的图片在LinkedHashMap的容量超过我们预设值时将会把容器中存在时间最长的对象移除,这个时候我们可以将被移除出LinkedHashMap中的对象存放至二级缓存容器中,而二级缓存中对象的管理就交给系统来做了,当系统需要GC时就会首先回收二级缓存容器中的Bitmap对象了。

在获取图片对象的时候先从一级缓存容器中查找,如果有对应对象并可用直接返回,如果没有的话从二级缓存中查找对应的SoftReference对象,判断SoftReference对象持有的Bitmap是否可用,可用直接返回,否则返回空。如果2级缓存都找不到图片,就直接加载图片资源。

private static final int HARD_CACHE_CAPACITY = 16; 

// Hard cache, with a fixed maximum capacity and a life duration 

private static final HashMap<String, Bitmap> sHardBitmapCache = new LinkedHashMap<String, Bitmap>(HARD_CACHE_CAPACITY, 0.75f, true) { 

private static final long serialVersionUID = -57738079457331894L; 

@Override

protected boolean removeEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest) { 

if (size() > HARD_CACHE_CAPACITY) { 

sSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue())); 

return true; 

} else

return false; 

}; 

// Soft cache for bitmap kicked out of hard cache 

private final static ConcurrentHashMap<String, SoftReference<Bitmap>> sSoftBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>(HARD_CACHE_CAPACITY); 

public Bitmap getBitmap(String id) { 

// First try the hard reference cache 

synchronized (sHardBitmapCache) { 

final Bitmap bitmap = sHardBitmapCache.get(id); 

if (bitmap != null) { 

// Bitmap found in hard cache 

// Move element to first position, so that it is removed last 

sHardBitmapCache.remove(id); 

sHardBitmapCache.put(id, bitmap); 

return bitmap; 

else{

// Then try the soft reference cache 

SoftReference<Bitmap> bitmapReference = sSoftBitmapCache.get(id); 

if (bitmapReference != null) { 

final Bitmap bitmap = bitmapReference.get(); 

if (bitmap != null) { 

// Bitmap found in soft cache 

return bitmap; 

} else 

// Soft reference has been Garbage Collected 

sSoftBitmapCache.remove(id); 

}

}

return null; 

public void putBitmap(String id, Bitmap bitmap) { 

synchronized (sHardBitmapCache) { 

if (sHardBitmapCache != null) { 

sHardBitmapCache.put(id, bitmap); 

}

4LruCache +sd的缓存方式 软引用

LruCache 类特别合适用来caching bitmaps;

private LruCache mMemoryCache; 

@Override 

protected void onCreate(Bundle savedInstanceState) {     ... 

    // Get memory class of this device, exceeding this amount will throw an 

    // OutOfMemory exception. 

    final int memClass = ((ActivityManager) context.getSystemService( 

            Context.ACTIVITY_SERVICE)).getMemoryClass();  

    // Use 1/8th of the available memory for this memory cache. 

    final int cacheSize = 1024 * 1024 * memClass / 8;  

    mMemoryCache = new LruCache(cacheSize) { 

        @Override 

        protected int sizeOf(String key, Bitmap bitmap) { 

            // The cache size will be measured in bytes rather than number of items. 

            return bitmap.getByteCount(); 

        } 

    }; 

    ... 

}  

public void addBitmapToMemoryCache(String key, Bitmap bitmap) { 

    if (getBitmapFromMemCache(key) == null) { 

        mMemoryCache.put(key, bitmap); 

    } 

}  

public Bitmap getBitmapFromMemCache(String key) { 

    return mMemoryCache.get(key); 

}

当加载位图到ImageView时,LruCache会先被检查是否存在这张图片。如果找到有,它会被用来立即更新 ImageView 组件,否则一个后台线程则被触发去处理这张图片。

public void loadBitmap(int resId, ImageView imageView) { 

    final String imageKey = String.valueOf(resId);  

    final Bitmap bitmap = getBitmapFromMemCache(imageKey); 

    if (bitmap != null) { 

        mImageView.setImageBitmap(bitmap); 

    } else {         mImageView.setImageResource(R.drawable.image_placeholder); //默认图片

        BitmapWorkerTask task = new BitmapWorkerTask(mImageView); 

        task.execute(resId); 

    } 

上面的程序中 BitmapWorkerTask 也需要做添加到内存Cache中的动作:

class BitmapWorkerTask extends AsyncTask { 

    ... 

    // Decode image in background. 

    @Override 

    protected Bitmap doInBackground(Integer... params) { 

        final Bitmap bitmap = decodeSampledBitmapFromResource( 

                getResources(), params[0], 100, 100)); 

        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); 

        return bitmap; 

    } 

    ... 

Use a Disk Cache [使用磁盘缓存]

private DiskLruCache mDiskCache; 

private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB 

private static final String DISK_CACHE_SUBDIR = "thumbnails"; 

 @Override 

protected void onCreate(Bundle savedInstanceState) { 

    ... 

    // Initialize memory cache 

    ... 

    File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR); 

    mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE); 

    ... 

}  

class BitmapWorkerTask extends AsyncTask { 

    ... 

    // Decode image in background. 

    @Override 

    protected Bitmap doInBackground(Integer... params) { 

        final String imageKey = String.valueOf(params[0]);  

        // Check disk cache in background thread 

        Bitmap bitmap = getBitmapFromDiskCache(imageKey);  

        if (bitmap == null) { // Not found in disk cache 

            // Process as normal 

            final Bitmap bitmap = decodeSampledBitmapFromResource( 

                    getResources(), params[0], 100, 100)); 

        }  

        // Add final bitmap to caches 

        addBitmapToCache(String.valueOf(imageKey, bitmap);  

        return bitmap; 

    } 

    ... 

}  

public void addBitmapToCache(String key, Bitmap bitmap) { 

    // Add to memory cache as before 

    if (getBitmapFromMemCache(key) == null) { 

        mMemoryCache.put(key, bitmap); 

    }  

    // Also add to disk cache 

    if (!mDiskCache.containsKey(key)) { 

        mDiskCache.put(key, bitmap); 

    } 

}  

public Bitmap getBitmapFromDiskCache(String key) { 

    return mDiskCache.get(key); 

}  

// Creates a unique subdirectory of the designated app cache directory. Tries to use external 

// but if not mounted, falls back on internal storage. 

public static File getCacheDir(Context context, String uniqueName) { 

    // Check if media is mounted or storage is built-in, if so, try and use external cache dir 

    // otherwise use internal cache dir 

    final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED 

            || !Environment.isExternalStorageRemovable() ? 

                    context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();  

    return new File(cachePath + File.separator + uniqueName); 

两种场景下的图片加载建议:

1,网络下载大量图片

比如微博客户端:

多线程异步网络下载图片,小图直接用LRUcache+softref+sd卡,大图按需下载;

 2,对于需要展现非常多条目信息的listviewgridview等的情况

adaptergetview函数里有个convertView参数,告知你是否有课利旧的view对象。如果不使用利旧convertView的话,每次调用getView时每次都会重新创建View,这样之前的View可能还没有销毁,加之不断的新建View势必会造成内存泄露。同时利旧convertView时,里面原有的图片等资源就会变成无主的了。

推介使用convertView+静态类ViewHolder

在这里,官方给出了解释

提升Adapter的两种方法

重用缓存convertView传递给getView()方法来避免填充不必要的视图

使用ViewHolder模式来避免没有必要的调用findViewById():因为太多的findViewById也会影响性能

ViewHolder类的作用

ViewHolder模式通过getView()方法返回的视图的标签(Tag)中存储一个数据结构,这个数据结构包含了指向我们要绑定数据的视图的引用,从而避免每次调用getView()的时候调用findViewById()

远远超过限制的内存分配方式有两种 :

1是从本机代码分配内存 。使用NDK(本地开发工具包)和JNI,它可能从C级(如的malloc / free或新建/删除)分配内存,这样的分配是不计入对24 MB的限制 。这是真的,从本机代码分配内存是为从Java方便,但它可以被用来存储在RAM中的数据(即使图像数据)的一些大金额 。

2使用OpenGL的纹理-纹理内存不计入限制 ,要查看您的应用程序确实分配多少内存可以使用android.os.Debug.getNativeHeapAllocatedSize( ),可以使用上面介绍的两种技术的Nexus之一,我可以轻松地为一个单一的前台进程分配300MB - 10倍以上的默认24 MB的限制 ,从上面来看使用navtive代码分配内存是不在24MB的限制内的(开放的GL的质地也是使用navtive代码分配内存的) 。

但是,这2个方法有个风险就是,本地堆分配内存超过系统可用内存限制的话,通常都是直接崩溃。



2013-11-6 更新:

今天遇到了OOM错误,很常见的一个问题,第一次修复这个问题,好好的学习一下吧。
看看大家的实现方式基本都差不多:
http://blog.csdn.net/android_tutor/article/details/8099918

http://chjmars.iteye.com/blog/1157137

http://mobile.51cto.com/abased-410796.htm

这里有十个文章分享如何优化android内存。
http://mobile.51cto.com/android-410883.htm

这篇文章还是值得好好看看的,对优化讲了很多哦
http://blog.sina.com.cn/s/blog_7501670601014dcj.html



这篇关于OOM讲课内容:图片缓存的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/634630

相关文章

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

缓存雪崩问题

缓存雪崩是缓存中大量key失效后当高并发到来时导致大量请求到数据库,瞬间耗尽数据库资源,导致数据库无法使用。 解决方案: 1、使用锁进行控制 2、对同一类型信息的key设置不同的过期时间 3、缓存预热 1. 什么是缓存雪崩 缓存雪崩是指在短时间内,大量缓存数据同时失效,导致所有请求直接涌向数据库,瞬间增加数据库的负载压力,可能导致数据库性能下降甚至崩溃。这种情况往往发生在缓存中大量 k

两个月冲刺软考——访问位与修改位的题型(淘汰哪一页);内聚的类型;关于码制的知识点;地址映射的相关内容

1.访问位与修改位的题型(淘汰哪一页) 访问位:为1时表示在内存期间被访问过,为0时表示未被访问;修改位:为1时表示该页面自从被装入内存后被修改过,为0时表示未修改过。 置换页面时,最先置换访问位和修改位为00的,其次是01(没被访问但被修改过)的,之后是10(被访问了但没被修改过),最后是11。 2.内聚的类型 功能内聚:完成一个单一功能,各个部分协同工作,缺一不可。 顺序内聚:

Android 10.0 mtk平板camera2横屏预览旋转90度横屏拍照图片旋转90度功能实现

1.前言 在10.0的系统rom定制化开发中,在进行一些平板等默认横屏的设备开发的过程中,需要在进入camera2的 时候,默认预览图像也是需要横屏显示的,在上一篇已经实现了横屏预览功能,然后发现横屏预览后,拍照保存的图片 依然是竖屏的,所以说同样需要将图片也保存为横屏图标了,所以就需要看下mtk的camera2的相关横屏保存图片功能, 如何实现实现横屏保存图片功能 如图所示: 2.mtk

Spring MVC 图片上传

引入需要的包 <dependency><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId><version>1.1</version></dependency><dependency><groupId>commons-io</groupId><artifactId>commons-

Prompt - 将图片的表格转换成Markdown

Prompt - 将图片的表格转换成Markdown 0. 引言1. 提示词2. 原始版本 0. 引言 最近尝试将图片中的表格转换成Markdown格式,需要不断条件和优化提示词。记录一下调整好的提示词,以后在继续优化迭代。 1. 提示词 英文版本: You are an AI assistant tasked with extracting the content of

Redis中使用布隆过滤器解决缓存穿透问题

一、缓存穿透(失效)问题 缓存穿透是指查询一个一定不存在的数据,由于缓存中没有命中,会去数据库中查询,而数据库中也没有该数据,并且每次查询都不会命中缓存,从而每次请求都直接打到了数据库上,这会给数据库带来巨大压力。 二、布隆过滤器原理 布隆过滤器(Bloom Filter)是一种空间效率很高的随机数据结构,它利用多个不同的哈希函数将一个元素映射到一个位数组中的多个位置,并将这些位置的值置

STL经典案例(四)——实验室预约综合管理系统(项目涉及知识点很全面,内容有点多,耐心看完会有收获的!)

项目干货满满,内容有点过多,看起来可能会有点卡。系统提示读完超过俩小时,建议分多篇发布,我觉得分篇就不完整了,失去了这个项目的灵魂 一、需求分析 高校实验室预约管理系统包括三种不同身份:管理员、实验室教师、学生 管理员:给学生和实验室教师创建账号并分发 实验室教师:审核学生的预约申请 学生:申请使用实验室 高校实验室包括:超景深实验室(可容纳10人)、大数据实验室(可容纳20人)、物联网实验

研究人员在RSA大会上演示利用恶意JPEG图片入侵企业内网

安全研究人员Marcus Murray在正在旧金山举行的RSA大会上公布了一种利用恶意JPEG图片入侵企业网络内部Windows服务器的新方法。  攻击流程及漏洞分析 最近,安全专家兼渗透测试员Marcus Murray发现了一种利用恶意JPEG图片来攻击Windows服务器的新方法,利用该方法还可以在目标网络中进行特权提升。几天前,在旧金山举行的RSA大会上,该Marcus现场展示了攻击流程,

恶意PNG:隐藏在图片中的“恶魔”

&lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/bffb187dc3546c6c5c6b8aa18b34b962.jpeg&quot; title=&quot;214201hhuuhubsuyuukbfy_meitu_1_meitu_2.jpg&quot;/&gt;&lt;/strong&gt;&lt;/span&gt;&lt;