3级缓存

上篇文章简单地介绍了listview的使用和优化,都是一些常见的优化技巧。但是listview优化还有一些重要的问题,那就是图片加载,异步加载的优化,因为图片占用内存较大,listview在滑动过程中很容易产生OOM的现象,下面我来给大家解释一下图片异步加载的优化思路。

总的来说有一下几个优化思路:

1,对Imageview使用setTag()方法来解决图片错位问题,这个Tag中设置的是图片的url,然后在加载的时候取得这个url和要加载那position中的url对比,如果不相同就加载,相同就是复用以前的就不加载了

2,对于要加载的图片资源,先在内存缓存中找(原始的方法是使用SoftRefrence,最新的方法是使用android提供的Lrucache),如果找不到,则在本地缓存(可以使用DiskLrucache类)中找(也就是读取原先下载过的本地图片),还找不到,就开启异步线程去下载图片,下载以后,保存在本地,内存缓存也保留一份引用

3,在异步线程中,先测量需要的图片大小,按比例缩放

4,使用一个Map保存异步线程的引用,key->value为url->AsyncTask,这样可以避免已经开启了线程去加载图片,但是还没有加载完时,又重复开启线程去加载图片的情况

5,在快速滑动的时候不加载图片,暂停所有图片加载线程,一旦停下来,结束不可见图片的加载线程,继续可见图片的加载线程


下面都是我摘取的网上的一些例子,我分别介绍它们来说明上述的优化思路

第一个例子:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public class MemoryCache {  
  2.   
  3.     private static final String TAG = "MemoryCache";  
  4.     // 放入缓存时是个同步操作  
  5.     // LinkedHashMap构造方法的最后一个参数true代表这个map里的元素将按照最近使用次数由少到多排列,即LRU  
  6.     // 这样的好处是如果要将缓存中的元素替换,则先遍历出最近最少使用的元素来替换以提高效率  
  7.     private Map<String, Bitmap> cache = Collections  
  8.             .synchronizedMap(new LinkedHashMap<String, Bitmap>(101.5f, true));  
  9.     // 缓存中图片所占用的字节,初始0,将通过此变量严格控制缓存所占用的堆内存  
  10.     private long size = 0;// current allocated size  
  11.     // 缓存只能占用的最大堆内存  
  12.     private long limit = 1000000;// max memory in bytes  
  13.   
  14.     public MemoryCache() {  
  15.         // use 25% of available heap size  
  16.         setLimit(Runtime.getRuntime().maxMemory() / 4);  
  17.     }  
  18.   
  19.     public void setLimit(long new_limit) {   
  20.         limit = new_limit;  
  21.         Log.i(TAG, "MemoryCache will use up to " + limit / 1024. / 1024. + "MB");  
  22.     }  
  23.   
  24.     public Bitmap get(String id) {  
  25.         try {  
  26.             if (!cache.containsKey(id))  
  27.                 return null;  
  28.             return cache.get(id);  
  29.         } catch (NullPointerException ex) {  
  30.             return null;  
  31.         }  
  32.     }  
  33.   
  34.     public void put(String id, Bitmap bitmap) {  
  35.         try {  
  36.             if (cache.containsKey(id))  
  37.                 size -= getSizeInBytes(cache.get(id));  
  38.             cache.put(id, bitmap);  
  39.             size += getSizeInBytes(bitmap);  
  40.             checkSize();  
  41.         } catch (Throwable th) {  
  42.             th.printStackTrace();  
  43.         }  
  44.     }  
  45.   
  46.     /** 
  47.      * 严格控制堆内存,如果超过将首先替换最近最少使用的那个图片缓存 
  48.      *  
  49.      */  
  50.     private void checkSize() {  
  51.         Log.i(TAG, "cache size=" + size + " length=" + cache.size());  
  52.         if (size > limit) {  
  53.             // 先遍历最近最少使用的元素  
  54.             Iterator<Entry<String, Bitmap>> iter = cache.entrySet().iterator();  
  55.             while (iter.hasNext()) {  
  56.                 Entry<String, Bitmap> entry = iter.next();  
  57.                 size -= getSizeInBytes(entry.getValue());  
  58.                 iter.remove();  
  59.                 if (size <= limit)  
  60.                     break;  
  61.             }  
  62.             Log.i(TAG, "Clean cache. New size " + cache.size());  
  63.         }  
  64.     }  
  65.   
  66.     public void clear() {  
  67.         cache.clear();  
  68.     }  
  69.   
  70.     /** 
  71.      * 图片占用的内存 
  72.      *  
  73.      * @param bitmap 
  74.      * @return 
  75.      */  
  76.     long getSizeInBytes(Bitmap bitmap) {  
  77.         if (bitmap == null)  
  78.             return 0;  
  79.         return bitmap.getRowBytes() * bitmap.getHeight();  
  80.     }  
  81. }  
也可以使用SoftReference,代码会简单很多,但是我推荐上面的方法。
[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public class MemoryCache {  
  2.       
  3.     private Map<String, SoftReference<Bitmap>> cache = Collections  
  4.             .synchronizedMap(new HashMap<String, SoftReference<Bitmap>>());  
  5.   
  6.     public Bitmap get(String id) {  
  7.         if (!cache.containsKey(id))  
  8.             return null;  
  9.         SoftReference<Bitmap> ref = cache.get(id);  
  10.         return ref.get();  
  11.     }  
  12.   
  13.     public void put(String id, Bitmap bitmap) {  
  14.         cache.put(id, new SoftReference<Bitmap>(bitmap));  
  15.     }  
  16.   
  17.     public void clear() {  
  18.         cache.clear();  
  19.     }  
  20.   
  21. }  

下面是文件缓存类的代码FileCache.java:
[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public class FileCache {  
  2.   
  3.     private File cacheDir;  
  4.   
  5.     public FileCache(Context context) {  
  6.         // 如果有SD卡则在SD卡中建一个LazyList的目录存放缓存的图片  
  7.         // 没有SD卡就放在系统的缓存目录中  
  8.         if (android.os.Environment.getExternalStorageState().equals(  
  9.                 android.os.Environment.MEDIA_MOUNTED))  
  10.             cacheDir = new File(  
  11.                     android.os.Environment.getExternalStorageDirectory(),  
  12.                     "LazyList");  
  13.         else  
  14.             cacheDir = context.getCacheDir();  
  15.         if (!cacheDir.exists())  
  16.             cacheDir.mkdirs();  
  17.     }  
  18.   
  19.     public File getFile(String url) {  
  20.         // 将url的hashCode作为缓存的文件名  
  21.         String filename = String.valueOf(url.hashCode());  
  22.         // Another possible solution  
  23.         // String filename = URLEncoder.encode(url);  
  24.         File f = new File(cacheDir, filename);  
  25.         return f;  
  26.   
  27.     }  
  28.   
  29.     public void clear() {  
  30.         File[] files = cacheDir.listFiles();  
  31.         if (files == null)  
  32.             return;  
  33.         for (File f : files)  
  34.             f.delete();  
  35.     }  
  36.   
  37. }  

最后最重要的加载图片的类,ImageLoader.java:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public class ImageLoader {  
  2.   
  3.     MemoryCache memoryCache = new MemoryCache();  
  4.     FileCache fileCache;  
  5.     private Map<ImageView, String> imageViews = Collections  
  6.             .synchronizedMap(new WeakHashMap<ImageView, String>());  
  7.     // 线程池  
  8.     ExecutorService executorService;  
  9.   
  10.     public ImageLoader(Context context) {  
  11.         fileCache = new FileCache(context);  
  12.         executorService = Executors.newFixedThreadPool(5);  
  13.     }  
  14.   
  15.     // 当进入listview时默认的图片,可换成你自己的默认图片  
  16.     final int stub_id = R.drawable.stub;  
  17.   
  18.     // 最主要的方法  
  19.     public void DisplayImage(String url, ImageView imageView) {  
  20.         imageViews.put(imageView, url);  
  21.         // 先从内存缓存中查找  
  22.   
  23.         Bitmap bitmap = memoryCache.get(url);  
  24.         if (bitmap != null)  
  25.             imageView.setImageBitmap(bitmap);  
  26.         else {  
  27.             // 若没有的话则开启新线程加载图片  
  28.             queuePhoto(url, imageView);  
  29.             imageView.setImageResource(stub_id);  
  30.         }  
  31.     }  
  32.   
  33.     private void queuePhoto(String url, ImageView imageView) {  
  34.         PhotoToLoad p = new PhotoToLoad(url, imageView);  
  35.         executorService.submit(new PhotosLoader(p));  
  36.     }  
  37.   
  38.     private Bitmap getBitmap(String url) {  
  39.         File f = fileCache.getFile(url);  
  40.   
  41.         // 先从文件缓存中查找是否有  
  42.         Bitmap b = decodeFile(f);  
  43.         if (b != null)  
  44.             return b;  
  45.   
  46.         // 最后从指定的url中下载图片  
  47.         try {  
  48.             Bitmap bitmap = null;  
  49.             URL imageUrl = new URL(url);  
  50.             HttpURLConnection conn = (HttpURLConnection) imageUrl  
  51.                     .openConnection();  
  52.             conn.setConnectTimeout(30000);  
  53.             conn.setReadTimeout(30000);  
  54.             conn.setInstanceFollowRedirects(true);  
  55.             InputStream is = conn.getInputStream();  
  56.             OutputStream os = new FileOutputStream(f);  
  57.             CopyStream(is, os);  
  58.             os.close();  
  59.             bitmap = decodeFile(f);  
  60.             return bitmap;  
  61.         } catch (Exception ex) {  
  62.             ex.printStackTrace();  
  63.             return null;  
  64.         }  
  65.     }  
  66.   
  67.     // decode这个图片并且按比例缩放以减少内存消耗,虚拟机对每张图片的缓存大小也是有限制的  
  68.     private Bitmap decodeFile(File f) {  
  69.         try {  
  70.             // decode image size  
  71.             BitmapFactory.Options o = new BitmapFactory.Options();  
  72.             o.inJustDecodeBounds = true;  
  73.             BitmapFactory.decodeStream(new FileInputStream(f), null, o);  
  74.   
  75.             // Find the correct scale value. It should be the power of 2.  
  76.             final int REQUIRED_SIZE = 70;  
  77.             int width_tmp = o.outWidth, height_tmp = o.outHeight;  
  78.             int scale = 1;  
  79.             while (true) {  
  80.                 if (width_tmp / 2 < REQUIRED_SIZE  
  81.                         || height_tmp / 2 < REQUIRED_SIZE)  
  82.                     break;  
  83.                 width_tmp /= 2;  
  84.                 height_tmp /= 2;  
  85.                 scale *= 2;  
  86.             }  
  87.   
  88.             // decode with inSampleSize  
  89.             BitmapFactory.Options o2 = new BitmapFactory.Options();  
  90.             o2.inSampleSize = scale;  
  91.             return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);  
  92.         } catch (FileNotFoundException e) {  
  93.         }  
  94.         return null;  
  95.     }  
  96.   
  97.     // Task for the queue  
  98.     private class PhotoToLoad {  
  99.         public String url;  
  100.         public ImageView imageView;  
  101.   
  102.         public PhotoToLoad(String u, ImageView i) {  
  103.             url = u;  
  104.             imageView = i;  
  105.         }  
  106.     }  
  107.   
  108.     class PhotosLoader implements Runnable {  
  109.         PhotoToLoad photoToLoad;  
  110.   
  111.         PhotosLoader(PhotoToLoad photoToLoad) {  
  112.             this.photoToLoad = photoToLoad;  
  113.         }  
  114.   
  115.         @Override  
  116.         public void run() {  
  117.             if (imageViewReused(photoToLoad))  
  118.                 return;  
  119.             Bitmap bmp = getBitmap(photoToLoad.url);  
  120.             memoryCache.put(photoToLoad.url, bmp);  
  121.             if (imageViewReused(photoToLoad))  
  122.                 return;  
  123.             BitmapDisplayer bd = new BitmapDisplayer(bmp, photoToLoad);  
  124.             // 更新的操作放在UI线程中  
  125.             Activity a = (Activity) photoToLoad.imageView.getContext();  
  126.             a.runOnUiThread(bd);  
  127.         }  
  128.     }  
  129.   
  130.     /** 
  131.      * 防止图片错位 
  132.      *  
  133.      * @param photoToLoad 
  134.      * @return 
  135.      */  
  136.     boolean imageViewReused(PhotoToLoad photoToLoad) {  
  137.         String tag = imageViews.get(photoToLoad.imageView);  
  138.         if (tag == null || !tag.equals(photoToLoad.url))  
  139.             return true;  
  140.         return false;  
  141.     }  
  142.   
  143.     // 用于在UI线程中更新界面  
  144.     class BitmapDisplayer implements Runnable {  
  145.         Bitmap bitmap;  
  146.         PhotoToLoad photoToLoad;  
  147.   
  148.         public BitmapDisplayer(Bitmap b, PhotoToLoad p) {  
  149.             bitmap = b;  
  150.             photoToLoad = p;  
  151.         }  
  152.   
  153.         public void run() {  
  154.             if (imageViewReused(photoToLoad))  
  155.                 return;  
  156.             if (bitmap != null)  
  157.                 photoToLoad.imageView.setImageBitmap(bitmap);  
  158.             else  
  159.                 photoToLoad.imageView.setImageResource(stub_id);  
  160.         }  
  161.     }  
  162.   
  163.     public void clearCache() {  
  164.         memoryCache.clear();  
  165.         fileCache.clear();  
  166.     }  
  167.   
  168.     public static void CopyStream(InputStream is, OutputStream os) {  
  169.         final int buffer_size = 1024;  
  170.         try {  
  171.             byte[] bytes = new byte[buffer_size];  
  172.             for (;;) {  
  173.                 int count = is.read(bytes, 0, buffer_size);  
  174.                 if (count == -1)  
  175.                     break;  
  176.                 os.write(bytes, 0, count);  
  177.             }  
  178.         } catch (Exception ex) {  
  179.         }  
  180.     }  
  181. }  


上面代码的思路是这样的,首先是一个MemoryCache类,用来缓存图片应用到内存。这个类包含一个Collectiosn.synchronizedMap(new LinkedHashMap<String,Bitmap>(10,1.5f,true))对象,这个对象就是用来保存url和对应的bitmap的,也就是缓存,最后一个参数设置为true的原因,是代表这个map里的元素将按照最近使用次数由少到多排列,即LRU。这样的好处是如果要将缓存中的元素替换,则先遍历出最近最少使用的元素来替换以提高效率 。

另外设置一个缓存的最大值limit,和一个初始值size=0。每次添加图片缓存,Size就增加相应大小,如果增加以后大小超过limit,就遍历LinkedHashMap清楚使用次数最少的缓存,同时减小size值,直到size<limit。

作者还举了一个使用SoftReference的例子,这样做的好处是android会自动替我们回收适当的bitmap缓存。

接下来是文件缓存,如果有SD卡则在SD卡中建一个LazyList的目录存放缓存的图片,没有SD卡就放在系统的缓存目录中,将url的hashCode作为缓存的文件名。这个类只是根据url名创建并返回了一个File类,没有真正的缓存图片,图片缓存在ImageLoader类中,不过这个类要获取FileCache返回的File来做FileOutputStream的目的地.

最后是负责的ImageLoader,这个类有一个线程池,用于管理下载线程。另外有一个WeakHashMap<ImageView, String>用于保存imageview引用和记录Tag,用于图片更新。它先检查缓存,没有则开启一个线程去下载,下载以后图片保存到缓存(内存,文件),然后缩放图像比例,返回一个合适大小的bitmap,最后开启一个线程去跟新UI(方式是imagview.getContext()获取对应的context,然后context调用runOnUIThread()方法)。

另外,在下载线程开启前,图片下载完成后,跟新UI前,都通过WeakHashMap<ImageView, String>获取下载图片的Tag与对应要设置图片imageview的tag比较,防止图片错位。

上述代码完成了基本的优化思路,甚至使用了一个自己定义的缓存类MemoryCache,使管理变得更加清晰,同时有文件缓存,也通过imagview->url的方式避免了图片错位,还开启了异步线程下载图片,但是又开启了一个UI线程去跟新UI。

缺点是开启了UI线程去更新UI,浪费了资源,其实这个可以使用定义一个回调接口实现。另外也没有考虑到重复开启下载线程的问题。


第二个例子:

先贴上主方法的代码:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. package cn.wangmeng.test;  
  2.   
  3. import java.io.IOException;  
  4. import java.io.InputStream;  
  5. import java.lang.ref.SoftReference;  
  6. import java.net.MalformedURLException;  
  7. import java.net.URL;  
  8. import java.util.HashMap;  
  9.   
  10. import android.graphics.drawable.Drawable;  
  11. import android.os.Handler;  
  12. import android.os.Message;  
  13.   
  14. public class AsyncImageLoader {  
  15.   
  16.      private HashMap<String, SoftReference<Drawable>> imageCache;  
  17.         
  18.          public AsyncImageLoader() {  
  19.              imageCache = new HashMap<String, SoftReference<Drawable>>();  
  20.          }  
  21.         
  22.          public Drawable loadDrawable(final String imageUrl, final ImageCallback imageCallback) {  
  23.              if (imageCache.containsKey(imageUrl)) {  
  24.                  SoftReference<Drawable> softReference = imageCache.get(imageUrl);  
  25.                  Drawable drawable = softReference.get();  
  26.                  if (drawable != null) {  
  27.                      return drawable;  
  28.                  }  
  29.              }  
  30.              final Handler handler = new Handler() {  
  31.                  public void handleMessage(Message message) {  
  32.                      imageCallback.imageLoaded((Drawable) message.obj, imageUrl);  
  33.                  }  
  34.              };  
  35.              new Thread() {  
  36.                  @Override  
  37.                  public void run() {  
  38.                      Drawable drawable = loadImageFromUrl(imageUrl);  
  39.                      imageCache.put(imageUrl, new SoftReference<Drawable>(drawable));  
  40.                      Message message = handler.obtainMessage(0, drawable);  
  41.                      handler.sendMessage(message);  
  42.                  }  
  43.              }.start();  
  44.              return null;  
  45.          }  
  46.         
  47.         public static Drawable loadImageFromUrl(String url) {  
  48.             URL m;  
  49.             InputStream i = null;  
  50.             try {  
  51.                 m = new URL(url);  
  52.                 i = (InputStream) m.getContent();  
  53.             } catch (MalformedURLException e1) {  
  54.                 e1.printStackTrace();  
  55.             } catch (IOException e) {  
  56.                 e.printStackTrace();  
  57.             }  
  58.             Drawable d = Drawable.createFromStream(i, "src");  
  59.             return d;  
  60.         }  
  61.         
  62.          public interface ImageCallback {  
  63.              public void imageLoaded(Drawable imageDrawable, String imageUrl);  
  64.          }  
  65.   
  66. }  

以上代码是实现异步获取图片的主方法,SoftReference是软引用,是为了更好的为了系统回收变量,重复的URL直接返回已有的资源,实现回调函数,让数据成功后,更新到UI线程。 
几个辅助类文件:
[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. package cn.wangmeng.test;  
  2.   
  3. public class ImageAndText {  
  4.         private String imageUrl;  
  5.         private String text;  
  6.   
  7.         public ImageAndText(String imageUrl, String text) {  
  8.             this.imageUrl = imageUrl;  
  9.             this.text = text;  
  10.         }  
  11.         public String getImageUrl() {  
  12.             return imageUrl;  
  13.         }  
  14.         public String getText() {  
  15.             return text;  
  16.         }  
  17. }  

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. package cn.wangmeng.test;  
  2.   
  3. import android.view.View;  
  4. import android.widget.ImageView;  
  5. import android.widget.TextView;  
  6.   
  7. public class ViewCache {  
  8.   
  9.         private View baseView;  
  10.         private TextView textView;  
  11.         private ImageView imageView;  
  12.   
  13.         public ViewCache(View baseView) {  
  14.             this.baseView = baseView;  
  15.         }  
  16.   
  17.         public TextView getTextView() {  
  18.             if (textView == null) {  
  19.                 textView = (TextView) baseView.findViewById(R.id.text);  
  20.             }  
  21.             return textView;  
  22.         }  
  23.   
  24.         public ImageView getImageView() {  
  25.             if (imageView == null) {  
  26.                 imageView = (ImageView) baseView.findViewById(R.id.image);  
  27.             }  
  28.             return imageView;  
  29.         }  
  30.   
  31. }  

ViewCache是辅助获取adapter的子元素布局


[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. package cn.wangmeng.test;  
  2.   
  3. import java.util.List;  
  4.   
  5. import cn.wangmeng.test.AsyncImageLoader.ImageCallback;  
  6.   
  7. import android.app.Activity;  
  8. import android.graphics.drawable.Drawable;  
  9. import android.view.LayoutInflater;  
  10. import android.view.View;  
  11. import android.view.ViewGroup;  
  12. import android.widget.ArrayAdapter;  
  13. import android.widget.ImageView;  
  14. import android.widget.ListView;  
  15. import android.widget.TextView;  
  16.   
  17. public class ImageAndTextListAdapter extends ArrayAdapter<ImageAndText> {  
  18.   
  19.         private ListView listView;  
  20.         private AsyncImageLoader asyncImageLoader;  
  21.   
  22.         public ImageAndTextListAdapter(Activity activity, List<ImageAndText> imageAndTexts, ListView listView) {  
  23.             super(activity, 0, imageAndTexts);  
  24.             this.listView = listView;  
  25.             asyncImageLoader = new AsyncImageLoader();  
  26.         }  
  27.   
  28.         public View getView(int position, View convertView, ViewGroup parent) {  
  29.             Activity activity = (Activity) getContext();  
  30.   
  31.             // Inflate the views from XML  
  32.             View rowView = convertView;  
  33.             ViewCache viewCache;  
  34.             if (rowView == null) {  
  35.                 LayoutInflater inflater = activity.getLayoutInflater();  
  36.                 rowView = inflater.inflate(R.layout.image_and_text_row, null);  
  37.                 viewCache = new ViewCache(rowView);  
  38.                 rowView.setTag(viewCache);  
  39.             } else {  
  40.                 viewCache = (ViewCache) rowView.getTag();  
  41.             }  
  42.             ImageAndText imageAndText = getItem(position);  
  43.   
  44.             // Load the image and set it on the ImageView  
  45.             String imageUrl = imageAndText.getImageUrl();  
  46.             ImageView imageView = viewCache.getImageView();  
  47.             imageView.setTag(imageUrl);  
  48.             Drawable cachedImage = asyncImageLoader.loadDrawable(imageUrl, new ImageCallback() {  
  49.                 public void imageLoaded(Drawable imageDrawable, String imageUrl) {  
  50.                     ImageView imageViewByTag = (ImageView) listView.findViewWithTag(imageUrl);  
  51.                     if (imageViewByTag != null) {  
  52.                         imageViewByTag.setImageDrawable(imageDrawable);  
  53.                     }  
  54.                 }  
  55.             });  
  56.             if (cachedImage == null) {  
  57.                 imageView.setImageResource(R.drawable.default_image);  
  58.             }else{  
  59.                 imageView.setImageDrawable(cachedImage);  
  60.             }  
  61.             // Set the text on the TextView  
  62.             TextView textView = viewCache.getTextView();  
  63.             textView.setText(imageAndText.getText());  
  64.   
  65.             return rowView;  
  66.         }  
  67.   
  68. }  

上述代码的思路是这样的:AsyncImageLoader类里面,使用了一个HashMap<String, SoftReference<Drawable>>用来缓存,然后有一个异步下载线程,还有一个方法内部的handler,线程下载完成后,会发消息给handler,然后handler调用回调接口imageCallback的imageLoaded()方法,这个方法是在adapter里面实现的,所以也就是在主线程跟新UI了。

而ViewCache类的作用其实就是ViewHolder,ImageAndText是一个bean类。

在adapter中,使用mageView.setTag(imageUrl)为imageview提供一个唯一标识Url,所以先图片下载完成以后,imageCallback的imageLoaded()方法中,就可以调用listview的findViewWithTag(imageUrl)来找到对应的imageview,从而不用担心错误的问题,这个方法比较巧妙。

缺点是没有实现文件缓存,另外也没有解决出现多个线程下载同一张图片的问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值