高效加载图片(上)

如果要挖井,就要挖到水出为止。


本讲内容:高效加载图片



一、加载分辨率高的大图片

我们在编写Android程序的时候经常要用到许多图片,一般这些图片都会大于我们程序所需要的大小。比如说系统图片库里展示的图片大都是用手机摄像头拍出来的,这些图片的分辨率会比我们手机屏幕的分辨率高得多。当我们读取一个Bitmap图片的时候,注意不要去加载不需要的分辨率。譬如在一个很小的ImageView上显示一张高分辨率的图片不会带来任何视觉上的好处,但却会占用我们相当多宝贵的内存。注意:将一张图片解析成一个Bitmap对象时所占用的内存并不是这个图片在硬盘中的大小,可能一张图片只有100k你觉得它并不大,但是读取到内存当中是按照像素点来算的,比如这张图片是1500*1000像素,使用的ARGB_8888颜色类型,那么每个像素点就会占用4个字节,总内存就是1500*1000*4字节,也就是5.7M。

我们编写的应用程序都是有一定内存限制的,程序占用了过高的内存就容易出现OOM(OutOfMemory)异常。我们可以通过下面的代码看出每个应用程序最高可用内存(与手机有关)是多少。

int maxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
Log.d("TAG", "Max memory is " + maxMemory + "KB"); 
因此在展示高分辨率图片的时候,最好先将图片进行压缩。压缩后的图片大小应该和用来展示它的控件大小相近。



二、对一张大图片进行适当的压缩,让它能够以最佳大小显示的同时,还能防止OOM的出现。

BitmapFactory类提供了多个解析方法用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFile()方法,网络上的图片可以使用decodeStream()方法,资源文件中的图片可以使用decodeResource()方法。这些方法会尝试为已经构建的bitmap分配内存,这时就会很容易导致OOM出现。为此每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。如下代码所示:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeResource(getResources(), R.id.img,options);
int imageHeight=options.outHeight;
int imageWidth=options.outWidth;
String imageType=options.outMimeType;
为了避免OOM异常,最好在解析每张图片的时候都先检查一下图片的大小,除非你非常信任图片的来源,保证这些图片都不会超出你程序的可用内存。

现在图片的大小已经知道了,我们就可以决定是把整张图片加载到内存中还是加载一个压缩版的图片到内存中。譬如,你的ImageView只有128*128像素的大小,只是为了显示一张缩略图,这时候把一张1024*1024像素的图片完全加载到内存中显然是不值得的。我们可以通过设置BitmapFactory.Options中inSampleSize的值来实现对图片进行压缩。比如我们有一张2048*1536像素的图片,将inSampleSize的值设置为4,就可以把这张图片压缩成512*384像素。原本加载这张图片需要占用13M的内存,压缩后就只需要占用0.75M了(假设图片是ARGB_8888类型,即每个像素点占用4个字节)。下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值:

public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { 
		// 源图片的高度和宽度  
		final int height=options.outHeight;
		final int width=options.outWidth;
		int inSampleSize=1;
		if(height>reqHeight||width>reqWidth){
			// 计算出实际宽高和目标宽高的比率  
			final int heightRatio=Math.round((float)height/(float)reqHeight);
			final int widthRatio=Math.round((float)width/(float)reqWidth);
			// 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高 一定都会大于等于目标的宽和高。  
			inSampleSize=heightRatio < widthRatio ? heightRatio : widthRatio;
		}
		return inSampleSize;
	}
使用这个方法,首先你要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。代码如下所示:

public static Bitmap decodeBitmapFromResource(Resources res,int resId,int reqWidth,int reqHeight) { 
		// 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
		BitmapFactory.Options options = new BitmapFactory.Options();
		options.inJustDecodeBounds=true;
		BitmapFactory.decodeResource(res, resId, options); 
		// 调用上面定义的方法计算inSampleSize值  
	       options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); 
	       // 使用获取到的inSampleSize值再次解析图片  
	       options.inJustDecodeBounds = false;    
	       return BitmapFactory.decodeResource(res, resId, options);
	}


上面我们使用到图片的id,但是我们只知道图片的名字,怎么获得图片的id呢?代码如下:

         /**
	 * 通过图片的名称获取图片的资源id的方法 
	 */
	public int getResource(String imageName) {
		Context context= getBaseContext();
		int resId = getResources().getIdentifier(imageName, "drawable",context.getPackageName());
		return resId;
	}

下面的代码非常简单地将任意一张图片压缩成128*128的缩略图,并在ImageView上展示。

mImageView.setImageBitmap(decodeBitmapFromResource(getResources(), getResource("image01"), 128, 128));


三、 使用图片缓存技术

当你需要在界面上加载一大堆图片的时候,(比如使用ListView, GridView 或者 ViewPager 这样的组件),屏幕上显示的图片可以通过滑动屏幕等事件不断地增加,最终导致OOM。为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,你需要想办法去避免这个情况的发生。

这个时候,使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。下面我们就来看一看如何使用内存缓存技术来对图片进行缓存,从而让你的应用程序在加载很多图片的时候可以提高响应速度和流畅性。


内存缓存技术最核心的类是LruCache (此类在android-support-v4的包中提供) 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。

根据你的程序选择一个合适的缓存大小给LruCache。如果一个太小的缓存空间,有可能造成图片频繁地被释放和重新加载,这并没有好处。而一个太大的缓存空间,则有可能还是会引起 java.lang.OutOfMemory 的异常。


下面是一个使用 LruCache 来缓存图片的例子:

private LruCache<String, Bitmap> mMemoryCache;  

protected void onCreate(Bundle savedInstanceState) {  
    // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。   LruCache通过构造函数传入缓存值,以KB为单位。  
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);  
    // 使用最大可用内存值的1/8作为缓存的大小。  
    int cacheSize = maxMemory / 8;  
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {  
        @Override  
        protected int sizeOf(String key, Bitmap bitmap) {  
            // 重写此方法来衡量每张图片的大小,默认返回图片大小kb 
            return bitmap.getByteCount() / 1024;  
        }  
    };  
}  
  
/**
 * 将一张图片存储到LruCache中。
 * @param key    LruCache的键,这里传入图片的URL地址。
 * @param bitmap LruCache的键,这里传入从网络上下载的Bitmap对象。
 */
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {  
    if (getBitmapFromMemCache(key) == null) {  
        mMemoryCache.put(key, bitmap);  
    }  
}  
  
/**
 * 从LruCache中获取一张图片,如果不存在就返回null。
 * @param key  LruCache的键,这里传入图片的URL地址。
 * @return  对应传入键的Bitmap对象,或者null。
 */
public Bitmap getBitmapFromMemCache(String key) {  
    return mMemoryCache.get(key);  
} 
使用了系统分配给应用程序的八分之一内存来作为缓存大小。 当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片。


注意:使用LruCache的目的 不是避免OOM 而是为了加快加载的速度,避免反复从网络 或者磁盘获取图片,内存缓存的技术不但不能节省内存反而会消耗部分内存


示例一: 照片墙

  

下面是res/layout/activity_main.xml 布局文件:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <GridView
        android:id="@+id/id_photo_wall"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:horizontalSpacing="3dp"
        android:numColumns="3"
        android:verticalSpacing="3dp" 
        android:layout_centerInParent="true"/>

</RelativeLayout>

下面是res/layout/grid_item.xml 布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ImageView
        android:id="@+id/id_photo"
        android:layout_width="match_parent"
        android:layout_height="120dp"
        android:scaleType="centerCrop" />

</RelativeLayout>

下面是网址中下载图片

public class Images {
	public final static String[] imageUrls=new String[]{
		"https://img-my.csdn.net/uploads/201309/01/1378037235_3453.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037235_9280.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037234_3539.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037234_6318.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037194_2965.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037193_1687.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037193_1286.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037192_8379.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037178_9374.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037177_1254.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037177_6203.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037152_6352.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037151_9565.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037151_7904.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037148_7104.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037129_8825.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037128_5291.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037128_3531.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037127_1085.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037095_7515.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037094_8001.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037093_7168.jpg",  
        "https://img-my.csdn.net/uploads/201309/01/1378037091_4950.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949643_6410.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949642_6939.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949630_4505.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949630_4593.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949629_7309.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949629_8247.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949615_1986.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949614_8482.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949614_3743.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949614_4199.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949599_3416.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949599_5269.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949598_7858.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949598_9982.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949578_2770.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949578_8744.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949577_5210.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949577_1998.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949482_8813.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949481_6577.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949480_4490.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949455_6792.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949455_6345.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949442_4553.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949441_8987.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949441_5454.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949454_6367.jpg",  
        "https://img-my.csdn.net/uploads/201308/31/1377949442_4562.jpg"
	};
}

新建PhotoWallAdapter做为GridView的适配器

public class PhotoWallAdapter extends ArrayAdapter<String> implements OnScrollListener{
	//记录所有正在下载或等待下载的任务。
	private Set<BitmapWorkerTask> taskCollection;

	//图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。
	private LruCache<String, Bitmap> mMemoryCache;
	
	//GridView的实例
	private GridView mPhotoWall;
	//第一张可见图片的下标
	private int mFirstVisibleItem;
	//一屏有多少张图片可见
	private int mVisibleItemCount;
	//记录是否刚打开程序,用于解决进入程序不滚动屏幕,不会下载图片的问题。
	private boolean isFirstEnter = true;
	
	public PhotoWallAdapter(Context context, int textViewResourceId,String[] objects,GridView photoWall) {
		super(context, textViewResourceId, objects);
		mPhotoWall=photoWall;
		taskCollection=new HashSet<BitmapWorkerTask>();
		// 获取应用程序最大可用内存
		int maxMemory=(int) Runtime.getRuntime().maxMemory();
		int cacheSize = maxMemory / 8;
		// 设置图片缓存大小为程序最大可用内存的1/8
		mMemoryCache=new LruCache<String,Bitmap>(cacheSize){
			protected int sizeOf(String key, Bitmap bitmap) {
				return bitmap.getByteCount();
			}
		};
		mPhotoWall.setOnScrollListener(this);
	}

	public View getView(int position, View convertView, ViewGroup parent) {
		final String url=getItem(position);
		View view;
		if(convertView==null){
			view = LayoutInflater.from(getContext()).inflate(R.layout.grid_item, null);
		}else{
			view=convertView;
		}
		final ImageView photo=(ImageView) view.findViewById(R.id.id_photo);
		// 给ImageView设置一个Tag,保证异步加载图片时不会乱序
		photo.setTag(url);
		setImageView(url, photo);
		return view;
	}
	
	/**
	 * 给ImageView设置图片。首先从LruCache中取出图片的缓存,设置到ImageView上。如果LruCache中没有该图片的缓存,就给ImageView设置一张默认图片。
	 * 
	 * @param imageUrl    图片的URL地址,用于作为LruCache的键。
	 * @param imageView   用于显示图片的控件。
	 */
	private void setImageView(String imageUrl,ImageView imageView){
		Bitmap bitmap=getBitmapFromMemoryCache(imageUrl);
		if(bitmap!=null){
			imageView.setImageBitmap(bitmap);
		}else{
			imageView.setImageResource(R.drawable.empty_photo);
		}
	}
	
	/**
	 * 将一张图片存储到LruCache中。
	 * @param key    LruCache的键,这里传入图片的URL地址。
	 * @param bitmap LruCache的键,这里传入从网络上下载的Bitmap对象。
	 */
	public void addBitmapToMemoryCache(String key,Bitmap bitmap){
		if(getBitmapFromMemoryCache(key)==null){
			mMemoryCache.put(key, bitmap);
		}
	}
	
	/**
	 * 从LruCache中获取一张图片,如果不存在就返回null。
	 * @param key  LruCache的键,这里传入图片的URL地址。
	 * @return  对应传入键的Bitmap对象,或者null。
	 */
	public Bitmap getBitmapFromMemoryCache(String key){
		return mMemoryCache.get(key);
	}
	
	/**
	 * 取消所有正在下载或等待下载的任务。
	 */
	public void cancelAllTasds(){
		if(taskCollection!=null){
			for (BitmapWorkerTask task : taskCollection) {
				task.cancel(false);
			}
		}
	}
	
	/**
	 * 加载Bitmap对象。此方法会在LruCache中检查所有屏幕中可见的ImageView的Bitmap对象,
	 * 如果发现任何一个ImageView的Bitmap对象不在缓存中,就会开启异步线程去下载图片。
	 * 
	 * @param firstVisibleItem   第一个可见的ImageView的下标
	 * @param visibleItemCount   屏幕中总共可见的元素数
	 */
	private void loadBitmaps(int firstVisibleItem,int visibleItemCount){
		try {
			for(int i=firstVisibleItem;i<firstVisibleItem+visibleItemCount;i++){
				String imageUrl=Images.imageUrls[i];
				Bitmap bitmap=getBitmapFromMemoryCache(imageUrl);
				if(bitmap==null){
					BitmapWorkerTask task=new BitmapWorkerTask();
					taskCollection.add(task);
					task.execute(imageUrl);
				}else{
					ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl);
					if(imageView!=null && bitmap!=null){
						imageView.setImageBitmap(bitmap);
					}
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 异步下载图片的任务。
	 */
	class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap>{
		private String imageUrl; // 图片的URL地址

		protected Bitmap doInBackground(String... params) {
			imageUrl=params[0];
			// 在后台开始下载图片
			Bitmap bitmap=downloadBitmap(imageUrl);
			if(bitmap!=null){
				// 图片下载完成后缓存到LrcCache中
				addBitmapToMemoryCache(imageUrl, bitmap);
			}
			return bitmap;
		}
		
		protected void onPostExecute(Bitmap bitmap) {
			super.onPostExecute(bitmap);
			// 根据Tag找到相应的ImageView控件,将下载好的图片显示出来。
			ImageView img=(ImageView) mPhotoWall.findViewWithTag(imageUrl);
			if(img!=null && bitmap!=null){
				img.setImageBitmap(bitmap);
			}
		}
		
		/**
		 * 建立HTTP请求,并获取Bitmap对象。
		 * @param imageUrl   图片的URL地址
		 * @return    解析后的Bitmap对象
		 */
		private Bitmap downloadBitmap(String imageUrl){
			Bitmap bitmap=null;
			HttpURLConnection con = null;
			
			try {
				URL url=new URL(imageUrl);
				con=(HttpURLConnection) url.openConnection();
				con.setConnectTimeout(5*1000);
				con.setReadTimeout(10*1000);
				con.setDoInput(true);
				con.setDoOutput(true);
				bitmap=BitmapFactory.decodeStream(con.getInputStream());
			} catch (Exception e) {
				e.printStackTrace();
			}finally{
				if(con!=null){
					con.disconnect();
				}
			}
			return bitmap;
		}
		
	}

	public void onScrollStateChanged(AbsListView view, int scrollState) {
		// 仅当GridView静止时才去下载图片,GridView滑动时取消所有正在下载的任务
		if(scrollState==SCROLL_STATE_IDLE){
			loadBitmaps(mFirstVisibleItem, mVisibleItemCount);
		}else{
			cancelAllTasds();
		}
	}

	public void onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount) {
		mFirstVisibleItem = firstVisibleItem;
		mVisibleItemCount = visibleItemCount;
		// 下载的任务应该由onScrollStateChanged里调用,但首次进入程序时onScrollStateChanged并不会调用,
		// 因此在这里为首次进入程序开启下载任务。
		if (isFirstEnter && visibleItemCount > 0) {
			loadBitmaps(firstVisibleItem, visibleItemCount);
			isFirstEnter = false;
		}
	}

}
首先在PhotoWallAdapter的构造函数中,我们初始化了LruCache类,并设置了最大缓存容量为程序最大可用内存的1/8,接下来又为GridView注册了一个滚动监听器。然后在getView()方法中,我们为每个ImageView设置了一个唯一的Tag,这个Tag的作用是为了后面能够准确地找回这个ImageView,不然异步加载图片会出现乱序的情况。之后调用了setImageView()方法为ImageView设置一张图片,这个方法首先会从LruCache缓存中查找是否已经缓存了这张图片,如果成功找到则将缓存中的图片显示在ImageView上,否则就显示一张默认的空图片。在onScrollStateChanged()方法中,我们对GridView的滚动状态进行了判断,如果当前GridView是静止的,则调用loadBitmaps()方法去下载图片,如果GridView正在滚动,则取消掉所有下载任务,这样可以保证GridView滚动的流畅性。在loadBitmaps()方法中,我们为屏幕上所有可见的GridView子元素开启了一个线程去执行下载任务,下载成功后将图片存储到LruCache当中,然后通过Tag找到相应的ImageView控件,把下载好的图片显示出来。

由于我们使用了LruCache来缓存图片,所以不需要担心内存溢出的情况,当LruCache中存储图片的总大小达到容量上限的时候,会自动把最近最少使用的图片从缓存中移除。


下面是MainActivity.java主界面文件:

public class MainActivity extends Activity {
	/** 
     * 用于展示照片墙的GridView 
     */  
    private GridView mPhotoWall; 
    
    /** 
     * GridView的适配器 
     */  
    private PhotoWallAdapter adapter;
    
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mPhotoWall = (GridView) findViewById(R.id.id_photo_wall);  
        adapter = new PhotoWallAdapter(this, 0, Images.imageUrls, mPhotoWall);  
        mPhotoWall.setAdapter(adapter);  
	}
	
	protected void onDestroy() {
		super.onDestroy();
		// 退出程序时结束所有的下载任务  
        adapter.cancelAllTasds();  
	}
}


注册

<uses-permission android:name="android.permission.INTERNET" />  


注:本文章参考郭婶的博客做的笔记,地址:http://blog.csdn.net/guolin_blog/article/details/9316683

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值