如何将位图缓存到本地内存中

39

为了我的 10,000 分,我决定在这个很酷的网站上贡献一些东西:一种在本地内存上缓存位图的机制。

背景

Android 设备每个应用程序可用的内存非常有限-堆范围从 16MB 到 128MB 不等,具体取决于 各种参数

如果超过此限制,就会发生 OOM 错误,当使用位图时,这种错误可能会多次发生。

许多时候,应用程序可能需要克服这些限制,在巨大的位图上执行重操作或仅将它们存储供以后使用,这时你需要

我想出的方法是使用一个简单的 Java 类,可以使这些目的变得更加容易。

它使用 JNI 来存储位图数据,并能够在需要时恢复它。

为了支持类的多个实例,我不得不使用一个我找到的技巧(点击此处)。

重要提示

  • 数据仍然存储在 RAM 上,因此如果设备没有足够的 RAM,应用程序可能会被杀死。

  • 请尽快释放内存。这不仅是为了避免内存泄漏,还为了避免成为系统优先杀死的应用程序之一,一旦您的应用程序进入后台。

  • 如果不想忘记释放内存,可以每次恢复位图时释放它,或使该类实现Closable接口。

  • 作为一项安全措施,我在 finalize() 方法中自动释放其本机内存,但不要让它负责这项工作。这太危险了。我还制定了一个规则,当发生这种情况时,将其写入日志记录。

  • 工作原理是将整个数据复制到JNI对象中,为了恢复,它会从头创建位图并将数据放入其中。

  • 使用和还原的位图格式为ARGB_8888。当然,你可以将其更改为任何你想要的格式,只需不要忘记更改代码...

  • 大位图可能需要花费较长时间来存储和恢复,因此最好在后台线程上执行。

  • 这不是一个完整的OOM解决方案,但可以帮助一些问题。例如,你可以与自己的LruCache一起使用它,同时避免使用堆内存来缓存自身。

  • 代码仅用于存储和还原。如果需要执行某些操作,则需要进行研究。openCV 可能是答案,但如果你想执行一些基本操作,可以自己实现(这里 是使用JNI旋转大型图像的示例)。如果你知道其他替代方案,请让我知道这里

  • 希望对某些人有用。请留下你的评论。

    另外,如果你发现代码问题或有改进建议,请让我知道。


    更好的解决方案

    如果您希望在JNI端执行更多操作,可以使用我制作的这篇文章。它基于我在此处编写的代码,但允许你执行更多操作,并且你可以轻松添加自己的操作。


    @Akanksha 你试过这个示例吗?它是否有效?如果有效,将我制作的项目转换为Android库,然后从您的原始项目中使用它。如果仍无法正常工作,请将整个代码复制到您的项目中。我不介意 :) 另外,我该如何添加Apache许可证信息? - android developer
    @Akanksha,你可以随心所欲地做任何事情,这是你的应用程序... - android developer
    我正在询问如何将身份验证集成到我的应用程序中的过程。 - Akanksha Rathore
    @Akanksha,你想让我制作一个使用我的项目的示例项目吗?也许这样更好,这样我就可以将示例与库分开,从而使库成为最少的文件。 - android developer
    @Akanksha 好的,我已经将库与示例分开了。现在应该更加清晰明了。感谢您的建议。您可以查看它并确认它能够正常工作:https://github.com/AndroidDeveloperLB/AndroidJniBitmapOperations - android developer
    显示剩余2条评论
    2个回答

    15

    说明

    这个示例代码展示了如何存储两个不同的位图(虽然它们很小,但只是一个演示),回收原始的Java位图,并稍后将它们恢复为Java实例并使用它们。

    正如你所猜测的那样,布局有两个ImageView。我没有将其包含在代码中,因为这是相当明显的。

    请记得根据需要更改代码到你自己的包中,否则事情将无法正常工作。

    MainActivity.java - 如何使用:

    package com.example.jnibitmapstoragetest;
    ...
    public class MainActivity extends Activity
      {
      @Override
      protected void onCreate(final Bundle savedInstanceState)
        {
        super.onCreate(savedInstanceState);
        //
        Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher);
        final JniBitmapHolder bitmapHolder=new JniBitmapHolder(bitmap);
        bitmap.recycle();
        //
        Bitmap bitmap2=BitmapFactory.decodeResource(getResources(),android.R.drawable.sym_action_call);
        final JniBitmapHolder bitmapHolder2=new JniBitmapHolder(bitmap2);
        bitmap2.recycle();
        //
        setContentView(R.layout.activity_main);
          {
          bitmap=bitmapHolder.getBitmapAndFree();
          final ImageView imageView=(ImageView)findViewById(R.id.imageView1);
          imageView.setImageBitmap(bitmap);
          }
          {
          bitmap2=bitmapHolder2.getBitmapAndFree();
          final ImageView imageView=(ImageView)findViewById(R.id.imageView2);
          imageView.setImageBitmap(bitmap2);
          }
        }
      }
    

    JniBitmapHolder.java - JNI和JAVA之间的“桥梁”:

    package com.example.jnibitmapstoragetest;
    ...
    public class JniBitmapHolder
      {
      ByteBuffer _handler =null;
      static
        {
        System.loadLibrary("JniBitmapStorageTest");
        }
    
      private native ByteBuffer jniStoreBitmapData(Bitmap bitmap);
    
      private native Bitmap jniGetBitmapFromStoredBitmapData(ByteBuffer handler);
    
      private native void jniFreeBitmapData(ByteBuffer handler);
    
      public JniBitmapHolder()
        {}
    
      public JniBitmapHolder(final Bitmap bitmap)
        {
        storeBitmap(bitmap);
        }
    
      public void storeBitmap(final Bitmap bitmap)
        {
        if(_handler!=null)
          freeBitmap();
        _handler=jniStoreBitmapData(bitmap);
        }
    
      public Bitmap getBitmap()
        {
        if(_handler==null)
          return null;
        return jniGetBitmapFromStoredBitmapData(_handler);
        }
    
      public Bitmap getBitmapAndFree()
        {
        final Bitmap bitmap=getBitmap();
        freeBitmap();
        return bitmap;
        }
    
      public void freeBitmap()
        {
        if(_handler==null)
          return;
        jniFreeBitmapData(_handler);
        _handler=null;
        }
    
      @Override
      protected void finalize() throws Throwable
        {
        super.finalize();
        if(_handler==null)
          return;
        Log.w("DEBUG","JNI bitmap wasn't freed nicely.please rememeber to free the bitmap as soon as you can");
        freeBitmap();
        }
      }
    

    Android.mk - JNI的属性文件:

    LOCAL_PATH := $(call my-dir)
    
    include $(CLEAR_VARS)
    
    LOCAL_MODULE    := JniBitmapStorageTest
    LOCAL_SRC_FILES := JniBitmapStorageTest.cpp
    LOCAL_LDLIBS := -llog
    LOCAL_LDFLAGS += -ljnigraphics
    
    include $(BUILD_SHARED_LIBRARY)
    APP_OPTIM := debug
    LOCAL_CFLAGS := -g
    

    JniBitmapStorageTest.cpp - 这里是“神奇”的东西:

    #include <jni.h>
    #include <jni.h>
    #include <android/log.h>
    #include <stdio.h>
    #include <android/bitmap.h>
    #include <cstring>
    #include <unistd.h>
    
    #define  LOG_TAG    "DEBUG"
    #define  LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
    #define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
    
    extern "C"
      {
      JNIEXPORT jobject JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniStoreBitmapData(JNIEnv * env, jobject obj, jobject bitmap);
      JNIEXPORT jobject JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniGetBitmapFromStoredBitmapData(JNIEnv * env, jobject obj, jobject handle);
      JNIEXPORT void JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniFreeBitmapData(JNIEnv * env, jobject obj, jobject handle);
      }
    
    class JniBitmap
      {
      public:
        uint32_t* _storedBitmapPixels;
        AndroidBitmapInfo _bitmapInfo;
        JniBitmap()
          {
          _storedBitmapPixels = NULL;
          }
      };
    
    JNIEXPORT void JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniFreeBitmapData(JNIEnv * env, jobject obj, jobject handle)
      {
      JniBitmap* jniBitmap = (JniBitmap*) env->GetDirectBufferAddress(handle);
      if (jniBitmap->_storedBitmapPixels == NULL)
        return;
      delete[] jniBitmap->_storedBitmapPixels;
      jniBitmap->_storedBitmapPixels = NULL;
      delete jniBitmap;
      }
    
    JNIEXPORT jobject JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniGetBitmapFromStoredBitmapData(JNIEnv * env, jobject obj, jobject handle)
      {
      JniBitmap* jniBitmap = (JniBitmap*) env->GetDirectBufferAddress(handle);
      if (jniBitmap->_storedBitmapPixels == NULL)
        {
        LOGD("no bitmap data was stored. returning null...");
        return NULL;
        }
      //
      //creating a new bitmap to put the pixels into it - using Bitmap Bitmap.createBitmap (int width, int height, Bitmap.Config config) :
      //
      //LOGD("creating new bitmap...");
      jclass bitmapCls = env->FindClass("android/graphics/Bitmap");
      jmethodID createBitmapFunction = env->GetStaticMethodID(bitmapCls, "createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
      jstring configName = env->NewStringUTF("ARGB_8888");
      jclass bitmapConfigClass = env->FindClass("android/graphics/Bitmap$Config");
      jmethodID valueOfBitmapConfigFunction = env->GetStaticMethodID(bitmapConfigClass, "valueOf", "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");
      jobject bitmapConfig = env->CallStaticObjectMethod(bitmapConfigClass, valueOfBitmapConfigFunction, configName);
      jobject newBitmap = env->CallStaticObjectMethod(bitmapCls, createBitmapFunction, jniBitmap->_bitmapInfo.height, jniBitmap->_bitmapInfo.width, bitmapConfig);
      //
      // putting the pixels into the new bitmap:
      //
      int ret;
      void* bitmapPixels;
      if ((ret = AndroidBitmap_lockPixels(env, newBitmap, &bitmapPixels)) < 0)
        {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
        return NULL;
        }
      uint32_t* newBitmapPixels = (uint32_t*) bitmapPixels;
      int pixelsCount = jniBitmap->_bitmapInfo.height * jniBitmap->_bitmapInfo.width;
      memcpy(newBitmapPixels, jniBitmap->_storedBitmapPixels, sizeof(uint32_t) * pixelsCount);
      AndroidBitmap_unlockPixels(env, newBitmap);
      //LOGD("returning the new bitmap");
      return newBitmap;
      }
    
    JNIEXPORT jobject JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniStoreBitmapData(JNIEnv * env, jobject obj, jobject bitmap)
      {
      AndroidBitmapInfo bitmapInfo;
      uint32_t* storedBitmapPixels = NULL;
      //LOGD("reading bitmap info...");
      int ret;
      if ((ret = AndroidBitmap_getInfo(env, bitmap, &bitmapInfo)) < 0)
        {
        LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
        return NULL;
        }
      LOGD("width:%d height:%d stride:%d", bitmapInfo.width, bitmapInfo.height, bitmapInfo.stride);
      if (bitmapInfo.format != ANDROID_BITMAP_FORMAT_RGBA_8888)
        {
        LOGE("Bitmap format is not RGBA_8888!");
        return NULL;
        }
      //
      //read pixels of bitmap into native memory :
      //
      //LOGD("reading bitmap pixels...");
      void* bitmapPixels;
      if ((ret = AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels)) < 0)
        {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
        return NULL;
        }
      uint32_t* src = (uint32_t*) bitmapPixels;
      storedBitmapPixels = new uint32_t[bitmapInfo.height * bitmapInfo.width];
      int pixelsCount = bitmapInfo.height * bitmapInfo.width;
      memcpy(storedBitmapPixels, src, sizeof(uint32_t) * pixelsCount);
      AndroidBitmap_unlockPixels(env, bitmap);
      JniBitmap *jniBitmap = new JniBitmap();
      jniBitmap->_bitmapInfo = bitmapInfo;
      jniBitmap->_storedBitmapPixels = storedBitmapPixels;
      return env->NewDirectByteBuffer(jniBitmap, 0);
      }
    

    是的,Github项目运行良好。因此,我想将其与我的项目一起使用,并尝试了两种方法:将示例项目转换为库并在Android标签中包含它。编译时没有显示任何错误,但在运行时出现ExceptionInInitializerError异常。我认为问题在于System.loadLibrary("JniBitmapOperations");或者可能是包名的问题。我从未使用过ndk。如何实现它? - Akanksha Rathore
    你需要进行一些研究。请在您开始的地方继续评论,而不是多个地方。然而,您是正确的,包很重要,因为C/C++端的代码假定了它们的路径。 - android developer
    谢谢回复。如果我将您的项目作为库项目并将其包含在我的应用程序中,我需要更改什么,例如包名称或路径? - Akanksha Rathore
    这是您第一次导入Android库,还是您在特定的库上遇到了问题?如果是这个库有问题,您可以将其文件复制到您的项目中,在所有文件(特别是C/C++文件)中更改包路径,然后它应该可以正常工作。 - android developer
    @androiddeveloper 在将图像保存在本地内存后,当我尝试获取该图像并将其保存到SD卡时,它返回给我一个完全没有显示的黑色图像。你有什么线索吗? - AndroidLearner
    显示剩余3条评论

    2
    如果您只想在堆之外缓存位图,一个更简单的解决方案是使用分段内存。这是它的 Gist(完整代码如下)。您可以将其用于除了位图之外的 Parcelable 实例。像这样使用它:
    private final CachedParcelable<Bitmap> cache = new CachedParcelable<>(Bitmap.CREATOR);
    
    cache.put(bitmap);
    bitmap = cache.get();
    cache.close();
    

    public final class CachedParcelable<T extends Parcelable> implements AutoCloseable {
        private final Parcelable.Creator<T> creator;
        private Parcel cache;
    
        public CachedParcelable(Parcelable.Creator<T> creator) {
            this.creator = creator;
        }
    
        public synchronized T get() {
            if (cache == null) return null;
            try {
                cache.setDataPosition(0);
                return creator.createFromParcel(cache);
            } catch (BadParcelableException e) {
                //
            } catch (RuntimeException e) {
                if (creator != Bitmap.CREATOR) throw e;
            }
            return null;
        }
    
        public synchronized void put(T value) {
            if (cache != null) cache.recycle();
            if (value == null) {
                cache = null;
                return;
            }
            try {
                cache = Parcel.obtain();
                value.writeToParcel(cache, 0);
            } catch (RuntimeException e) {
                if (creator != Bitmap.CREATOR) throw e;
            }
        }
    
        @Override
        public synchronized void close() {
            if (cache != null) {
                cache.recycle();
                cache = null;
            }
        }
    }
    

    1
    是的,Parcel 使用本地的 malloc 堆。 - Nuno Cruces
    有趣。但是当位图在那里时,你能做些什么,比如我的JNI解决方案?还是只能缓存它? - android developer
    1
    不,它是不透明的。但是如果你的唯一目标是将(几个)大位图缓存在堆之外,那么这是我所知道的最简单的解决方案。 - Nuno Cruces
    1
    我的意思是,在数据被打包的时候是无法访问的。我不确定有多少个。我用它来缓存一些非常大的位图。 - Nuno Cruces
    我认为现在所有大于X的原始数组都有自己的堆空间。这是GC知道的事情,但不需要扫描(没有对象引用)。位图在内部使用int[],因此它们也会进入那里。 - Nuno Cruces
    显示剩余6条评论

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接