android图片:多选相册的实现
上一篇文章简单介绍了图片的加载,但是实际业务中,加载图片的需求会比这复杂得多,例如这篇博客要讲的多选相册的实现,会涉及到下面几个问题:
1、 取得图片路径:这里涉及到contentprovider,不是本文重点,这里只提供代码,不做详细解释。
2、 耗时操作:相册图片的读取是从硬盘读取的,这是一个耗时操作,不能直接在ui主线程操作,应该另起线程,可以使用AsyncTask来加载图片。
3、 并发性:相册有大量图片,通常我们用gridview来显示,同时会用viewholder来复用view,但是由于我们的图片加载是在线程中并发操作的,快速滑动gridview时,会使得同一个view,同时有多个task在加载图片,会导致图片错位和view一直在变换图片,而且图片加载效率非常低(如果没看明白,不用着急,下面有例子展示)。
4、 图片缓存:为了提高效率,我们应该对图片做缓存,加载图片时,先从缓存读取,读取不到再去硬盘读取。一方面内存读取效率高,另一方面减少重复操作(硬盘读取时,我们是先做压缩,再读取)。
下面一一解决上面的问题。
1、取得相册图片路径。
public static List<ImageModel> getImages(Context context){List<ImageModel> list = new ArrayList<ImageModel>();ContentResolver contentResolver = context.getContentResolver();Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;String[] projection = {MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA,};String sortOrder = MediaStore.Images.Media.DATE_ADDED + " desc";Cursor cursor = contentResolver.query(uri, projection, null, null, sortOrder);int iId = cursor.getColumnIndex(MediaStore.Images.Media._ID);int iPath = cursor.getColumnIndex(MediaStore.Images.Media.DATA);cursor.moveToFirst();while (!cursor.isAfterLast()) {String id = cursor.getString(iId);String path = cursor.getString(iPath);ImageModel imageModel = new ImageModel(id,path);list.add(imageModel);cursor.moveToNext();}cursor.close();return list;
}<strong>
</strong>
其中ImageModel为图片类:
public class ImageModel {private String id;//图片idprivate String path;//路径private Boolean isChecked = false;//是否被选中public ImageModel(String id, String path, Boolean isChecked) {this.id = id;this.path = path;this.isChecked = isChecked;}public ImageModel(String id, String path) {this.id = id;this.path = path;}public String getId() {return id;}public void setId(String id) {this.id = id;}public String getPath() {return path;}public void setPath(String path) {this.path = path;}public Boolean getIsChecked() {return isChecked;}public void setIsChecked(Boolean isChecked) {this.isChecked = isChecked;}
}
别忘了在AndroidManifest.xml中加上权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
2、使用AsyncTask加载图片
由于从硬盘读取照片是耗时操作,我们不能直接在ui主线程里面去操作,这里用AsyncTask来进行图片读取。
class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {private String mPath;private final WeakReference<ImageView> imageViewReference;public BitmapWorkerTask(String path, ImageView imageView) {mPath = path;imageViewReference = new WeakReference<ImageView>(imageView);}// Decode image in background.@Overrideprotected BitmapDrawable doInBackground(String... params) {BitmapDrawable drawable = null;Bitmap bitmap = decodeBitmapFromDisk(mPath, mImageWidth, mImageHeight);//Bitmap转换成BitmapDrawableif (bitmap != null) {drawable = new BitmapDrawable(mResources, bitmap);}return drawable;}@Overrideprotected void onPostExecute(BitmapDrawable value) {if (imageViewReference != null && value != null) {final ImageView imageView = imageViewReference.get();if (imageView != null) {imageView.setImageDrawable(value);}}}
}
从上面的代码中可以看出,我们在构造函数中把图片路径和要显示图片的ImageView引入进来,图片读取并压缩完成后,ImageView显示该图片。这里我们并没有直接强引用ImageView,而是使用了弱引用(WeakReference),原因在于读取图片是耗时操作,有可能在图片未读取完成时,我们的ImageView已经被划出屏幕,这时候如果AsyncTask仍持有ImageView的强引用,那会阻止垃圾回收机制回收该ImageView,使用弱引用就不会阻止垃圾回收机制回收该ImageView,可以有效避免OOM。
定义好task后,我们可以这么来使用:
public void loadImage(String path, ImageView imageView) {BitmapWorkerTask task = new BitmapWorkerTask(path,imageView);task.execute();
}
上面定义的AsyncTask中,decodeBitmapFromDisk(mPath, mImageWidth,mImageHeight)是压缩图片并读取出来的方法。
/*** 根据路径从硬盘中读取图片* @param path 图片路径* @param reqWidth 请求宽度(显示宽度)* @param reqHeight 请求高度(显示高度)* @return 图片Bitmap*/
public Bitmap decodeBitmapFromDisk(String path, int reqWidth, int reqHeight) {BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;BitmapFactory.decodeFile(path, options);//初始压缩比例options.inSampleSize = calculateBitmapSize(options, reqWidth, reqHeight);options.inJustDecodeBounds = false;Bitmap bmp = BitmapFactory.decodeFile(path, options);return bmp;
}/*** 计算压缩率* @param options* @param reqWidth* @param reqHeight* @return*/
public static int calculateBitmapSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {// Raw height and width of imagefinal int height = options.outHeight;final int width = options.outWidth;int inSampleSize = 1;if (height > reqHeight || width > reqWidth) {final int halfHeight = height / 2;final int halfWidth = width / 2;// Calculate the largest inSampleSize value that is a power of 2 and keeps both// height and width larger than the requested height and width.while ((halfHeight / inSampleSize) > reqHeight&& (halfWidth / inSampleSize) > reqWidth) {inSampleSize *= 2;}}return inSampleSize;
}
calculateBitmapSize这个方法的主要功能就是根据要显示的图片大小,计算压缩率,压缩原始图片并读出压缩后的图片,在上一篇博客里面有详细介绍。
接下来就是在gridview中,定义adapter,把图片显示出来,具体代码这里不贴出来,不是这篇博客的主要内容,后面我会把源码上传,博客中没有贴出来的代码,可以在源码中查看。
下面看我们做到这一步之后的效果。
可以看到效果很不流畅,滑动屏幕时,ImageView显示的图片一直在变换,原因一开始就讲过了,这是并发导致的。复用ImageView导致同个ImageView对应了多个AsyncTask,每个AsyncTask完成时都会改变ImageView显示的图片。而且AsyncTask完成顺序是不确定的,所以也会导致图片错位,本来应该显示1位置的图片的ImageView结果显示的21位置的图片。
3、处理并发性
要解决上面的问题,我们就应该让一个ImgeView只对应一个AsyncTask,当有新的AsyncTask进入时,先看ImgeView上是否有AsyncTask正在执行,如果有,则取消该AsyncTask,然后把新的AsyncTask加入进来,这样不止解决了图片错位问题,同时也减少了没必要的AsyncTask,提高了加载效率。
定义一个持有AsyncTask弱引用的BitmapDrawable类
static class AsyncDrawable extends BitmapDrawable {private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;public AsyncDrawable(Resources res, Bitmap bitmap,BitmapWorkerTask bitmapWorkerTask) {super(res, bitmap);bitmapWorkerTaskReference =new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);}public BitmapWorkerTask getBitmapWorkerTask() {return bitmapWorkerTaskReference.get();}
}
然后用imageView.setImageDrawable(asyncDrawable)把ImageView和AsyncDrawable绑定,这样就可以把ImageView与AsyncTask对应起来。
为什么使用弱引用我们上面讲了,为什么AsyncTask要持有ImageView的引用我们上面也讲了,那么这里为什么要ImageView持有AsyncTask的引用呢?
ImageView持有AsyncTask的引用,就可以通过ImageView找到其当前对应的AsyncTask,如果有新的AsyncTask进来,先比较是否和当前的AsyncTask一样,如果一样,则不把新的AsyncTask加入,如果不一样,先把当前对应的AsyncTask取消,再把新的AsyncTask与ImageView对应起来。
这里还有一个问题,为什么不和前面AsyncTask持有ImageView弱引用一样,也在ImageView构造函数中让ImageView持有AsyncTask的弱引用就行,不用拐弯抹角的让ImageDrable持有AsyncTask的弱引用。这里要注意一下,我们的ImageView是复用的,也就是一般情况下,ImageView只构造了一次,如果ImageView直接持有AsyncTask的弱引用,那么只会持有ImageView刚构造时的那一个,而不会随着界面的滑动而更新AsyncTask。但是界面滑动时,ImageView的setImageDrawable方法却随着被触发,所以这里在ImageDrawable中持有AsyncTask的弱引用,然后ImageView通过getImageDrawable获得ImageDrawable,再通过ImageDrawable获得AsyncTask。
修改loadImage方法
public void loadImage(String path, ImageView imageView) {if (path == null || path.equals("")) {return;}BitmapDrawable bitmapDrawable = null;if (bitmapDrawable != null) {imageView.setImageDrawable(bitmapDrawable);} else if (cancelPotentialWork(path,imageView)) {final BitmapWorkerTask task = new BitmapWorkerTask(path,imageView);final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, mLoadingBitmap, task);imageView.setImageDrawable(asyncDrawable);task.execute();}
}public static boolean cancelPotentialWork(String path, ImageView imageView) {final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);if (bitmapWorkerTask != null) {final String bitmapData = bitmapWorkerTask.mPath;// If bitmapData is not yet set or it differs from the new dataif (bitmapData == null|| !bitmapData.equals(path)) {// Cancel previous taskbitmapWorkerTask.cancel(true);} else {// The same work is already in progressreturn false;}}// No task associated with the ImageView, or an existing task was cancelledreturn true;
}private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {if (imageView != null) {final Drawable drawable = imageView.getDrawable();if (drawable instanceof AsyncDrawable) {final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;return asyncDrawable.getBitmapWorkerTask();}}return null;
}
修改BitmapWorkerTask 方法
class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {private String mPath;private final WeakReference<ImageView> imageViewReference;public BitmapWorkerTask(String path, ImageView imageView) {mPath = path;imageViewReference = new WeakReference<ImageView>(imageView);}// Decode image in background.@Overrideprotected BitmapDrawable doInBackground(String... params) {BitmapDrawable drawable = null;Bitmap bitmap = decodeBitmapFromDisk(mPath, mImageWidth, mImageHeight);//Bitmap转换成BitmapDrawableif (bitmap != null) {drawable = new BitmapDrawable(mResources, bitmap);}return drawable;}@Overrideprotected void onPostExecute(BitmapDrawable value) {if (isCancelled()) {value = null;}if (imageViewReference != null && value != null) {final ImageView imageView = imageViewReference.get();final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);if (this == bitmapWorkerTask && imageView != null) {imageView.setImageDrawable(value);}}}
}
再看效果
已经运行比较流畅了。
4、图片缓存
图片缓存,老的做法是使用SoftReference 或者WeakReference bitmap缓存,但是不推荐使用这种方式。因为从Android 2.3 (API Level 9) 开始,垃圾回收开始强制的回收掉soft/weak 引用从而导致这些缓存没有任何效率的提升。另外,在 Android 3.0 (API Level 11)之前,这些缓存的Bitmap数据保存在底层内存(nativememory)中,并且达到预定条件后也不会释放这些对象,从而可能导致程序超过内存限制并崩溃(OOM)。
现在常用的做法是使用LRU算法来缓存图片,把最近使用到的Bitmap对象用强引用保存起来(保存到LinkedHashMap中),当缓存数量达到预定的值的时候,把不经常使用的对象删除。Android提供了LruCache类(在API 4之前可以使用SupportLibrary 中的类),里面封装了LRU算法,因此我们不需要自己实现,只要分配好内存空间就可以。
定义ImageCache类用于图片缓存。
public class ImageCache {private LruCache<String, BitmapDrawable> mMemoryCache;public ImageCache() {// 获取应用最大内存final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//用最大内存的1/4来缓存图片final int cacheSize = maxMemory / 4;mMemoryCache = new LruCache<String, BitmapDrawable>(cacheSize) {/*** Measure item size in kilobytes rather than units which is more practical* for a bitmap cache*/@Overrideprotected int sizeOf(String key, BitmapDrawable value) {Bitmap bitmap = value.getBitmap();return bitmap.getByteCount() / 1024;}};}/*** Adds a bitmap to both memory and disk cache.* @param data Unique identifier for the bitmap to store* @param value The bitmap drawable to store*/public void addBitmapToMemCache(String data, BitmapDrawable value) {if (data == null || value == null) {return;}// Add to memory cacheif (mMemoryCache != null) {mMemoryCache.put(data, value);}}/*** Get from memory cache.** @param data Unique identifier for which item to get* @return The bitmap drawable if found in cache, null otherwise*/public BitmapDrawable getBitmapFromMemCache(String data) {BitmapDrawable memValue = null;if (mMemoryCache != null) {memValue = mMemoryCache.get(data);}return memValue;}}
接下来就是修改获取图片的方法:先从内存缓存查找图片,找不到再开启task去硬盘读取。
public void loadImage(String path, ImageView imageView) {if (path == null || path.equals("")) {return;}BitmapDrawable bitmapDrawable = null;//先从缓存读取if(mImageCache != null){bitmapDrawable = mImageCache.getBitmapFromMemCache(path);}//读取不到再开启任务去硬盘读取if (bitmapDrawable != null) {imageView.setImageDrawable(bitmapDrawable);} else if (cancelPotentialWork(path,imageView)) {final BitmapWorkerTask task = new BitmapWorkerTask(path,imageView);final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, mLoadingBitmap, task);imageView.setImageDrawable(asyncDrawable);task.execute();}
}
task中读取到图片之后,同时把该图片加入到缓存中
class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {private String mPath;private final WeakReference<ImageView> imageViewReference;public BitmapWorkerTask(String path, ImageView imageView) {mPath = path;imageViewReference = new WeakReference<ImageView>(imageView);}// Decode image in background.@Overrideprotected BitmapDrawable doInBackground(String... params) {BitmapDrawable drawable = null;Bitmap bitmap = decodeBitmapFromDisk(mPath, mImageWidth, mImageHeight);//Bitmap转换成BitmapDrawableif (bitmap != null) {drawable = new BitmapDrawable(mResources, bitmap);//缓存if(mImageCache!=null){mImageCache.addBitmapToMemCache(mPath, drawable);}}return drawable;}@Overrideprotected void onPostExecute(BitmapDrawable value) {if (isCancelled()) {value = null;}if (imageViewReference != null && value != null) {final ImageView imageView = imageViewReference.get();final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);if (this == bitmapWorkerTask && imageView != null) {imageView.setImageDrawable(value);}}}
}
再看效果
会发现第一次加载完图片之后,再往回滑动查看时,图片很快就显示出来。做到这一步其实就已经可以了,不过还可以继续优化。
Android 3.0 (API level 11)之后, BitmapFactory.Options提供了一个属性 inBitmap,该属性使得Bitmap解码器去尝试重用已有的bitmap,这样就可以减少内存的分配和释放,提高效率。
需要注意的是,在Android4.4之前,重用的bitmap大小必须一样,4.4之后,新申请的Bitmap大小必须小于或者等于已经赋值过的Bitmap大小,所以实际上这个属性4.4之后的作用才比较明显。
当bitmap从LruCache被移出时,将移出的bitmap以软引用的形式放进HashSet,用于后面的重用。
private LruCache<String, BitmapDrawable> mMemoryCache;
private Set<SoftReference<Bitmap>> mReusableBitmaps;
if (Utils.hasHoneycomb()) {mReusableBitmaps = Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}// 获取应用最大内存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//用最大内存的1/4来缓存图片
final int cacheSize = maxMemory / 4;
mMemoryCache = new LruCache<String, BitmapDrawable>(cacheSize) {/*** Measure item size in kilobytes rather than units which is more practical* for a bitmap cache*/@Overrideprotected int sizeOf(String key, BitmapDrawable value) {Bitmap bitmap = value.getBitmap();return bitmap.getByteCount() / 1024;}/*** Notify the removed entry that is no longer being cached*/@Overrideprotected void entryRemoved(boolean evicted, String key,BitmapDrawable oldValue, BitmapDrawable newValue) {// The removed entry is a standard BitmapDrawableif (Utils.hasHoneycomb()) {// We're running on Honeycomb or later, so add the bitmap// to a SoftReference set for possible use with inBitmap latermReusableBitmaps.add(new SoftReference<Bitmap>(oldValue.getBitmap()));}}
};
在解码的时候,尝试使用 inBitmap。
public Bitmap decodeBitmapFromDisk(String path, int reqWidth, int reqHeight) {// BEGIN_INCLUDE (read_bitmap_dimensions)// First decode with inJustDecodeBounds=true to check dimensionsfinal BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;BitmapFactory.decodeFile(path, options);// Calculate inSampleSizeoptions.inSampleSize = calculateBitmapSize(options, reqWidth, reqHeight);// END_INCLUDE (read_bitmap_dimensions)// If we're running on Honeycomb or newer, try to use inBitmapif (Utils.hasHoneycomb()) {addInBitmapOptions(options);}// Decode bitmap with inSampleSize setoptions.inJustDecodeBounds = false;return BitmapFactory.decodeFile(path, options);
}private void addInBitmapOptions(BitmapFactory.Options options) {// inBitmap only works with mutable bitmaps so force the decoder to// return mutable bitmaps.options.inMutable = true;if (mImageCache != null) {// Try and find a bitmap to use for inBitmapBitmap inBitmap = mImageCache.getBitmapFromReusableSet(options);if (inBitmap != null) {options.inBitmap = inBitmap;}}
}
下面的方法从ReusableSet查找是否有可以重用的inBitmap
/*** @param options - BitmapFactory.Options with out* options populated* @return Bitmap that case be used for inBitmap*/
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {Bitmap bitmap = null;if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {synchronized (mReusableBitmaps) {final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator();Bitmap item;while (iterator.hasNext()) {item = iterator.next().get();if (null != item && item.isMutable()) {// Check to see it the item can be used for inBitmapif (canUseForInBitmap(item, options)) {bitmap = item;// Remove from reusable set so it can't be used againiterator.remove();break;}} else {// Remove from the set if the reference has been cleared.iterator.remove();}}}}return bitmap;}
最后,下面方法确定候选位图尺寸是否满足inBitmap
/*** @param candidate - Bitmap to check* @param targetOptions - Options that have the out* value populated* @return true if <code>candidate</code> can be used for inBitmap re-use with* <code>targetOptions</code>*/
@TargetApi(Build.VERSION_CODES.KITKAT)
private static boolean canUseForInBitmap(Bitmap candidate, BitmapFactory.Options targetOptions) {if (!Utils.hasKitKat()) {// On earlier versions, the dimensions must match exactly and the inSampleSize must be 1return candidate.getWidth() == targetOptions.outWidth&& candidate.getHeight() == targetOptions.outHeight&& targetOptions.inSampleSize == 1;}// From Android 4.4 (KitKat) onward we can re-use if the byte size of the new bitmap// is smaller than the reusable bitmap candidate allocation byte count.int width = targetOptions.outWidth / targetOptions.inSampleSize;int height = targetOptions.outHeight / targetOptions.inSampleSize;int byteCount = width * height * getBytesPerPixel(candidate.getConfig());return byteCount <= candidate.getAllocationByteCount();}/*** Return the byte usage per pixel of a bitmap based on its configuration.* @param config The bitmap configuration.* @return The byte usage per pixel.*/
private static int getBytesPerPixel(Bitmap.Config config) {if (config == Bitmap.Config.ARGB_8888) {return 4;} else if (config == Bitmap.Config.RGB_565) {return 2;} else if (config == Bitmap.Config.ARGB_4444) {return 2;} else if (config == Bitmap.Config.ALPHA_8) {return 1;}return 1;
}
自此就把本地相册图片加载到我们自己定义的gridview中,这个gridview要怎么设计,单选,多选,混合选,就随各位喜欢了。
其他更细致的优化都在源码里面
以上内容参考自官方文档。
android图片:多选相册的实现相关推荐
- Android图片保存到相册失败
Android之图片保存的一些坑 生成图片的路径: // 该目录下得文件不会被相册扫描到,所以不要用这个String rootDir = context.getExternalFilesDir(nul ...
- Android 图片多选Hor,Android水平滚动laggy
HorizontalScrollView不使用管理列表内存的适配器,因此它无法处理繁重(图像,自定义视图等)列表. 您可以使用此水平ListView http://www.dev-smart.co ...
- Android 图片选择对话框,通过本地相册或照相机获得图片,可单选或多选,单选可设置是否裁剪
AndroidPickPhotoDialog 项目地址: wanliyang1990/AndroidPickPhotoDialog 简介:Android 图片选择对话框,通过本地相册或照相机获得图片, ...
- Android:支持多选的本地相册
前段时间在做一个动态发布功能,需要用到图片上传.一开始直接调用的系统相册和相机,由于系统相机不支持多选,就花点时间做了个本地相册,在此开源下. 先上截图,依次为选择相册界面.相册详情界面.查看图片大图 ...
- 获取android的拍照和自定义多选相册
获取系统的相机功能拍照这个不难,但是需要注意的是,拍照返回后的照片如果没有指定存储的路径,那么系统将自动保存到sd卡中,得到的是拍完照的缩略图,会失帧,显示有些模糊,所以在调用系统相机拍完照后我们要指 ...
- Android拍照及从相册选择图片传详解(终极版)
Android 拍照及从相册选择图片传详解 先上图 新知识点速览 URI(统一资源标识符)是标识逻辑或物理资源的字符序列,与URL类似,也是一串字符.通过使用位置,名称或两者来标识Internet上的 ...
- Android 拍照、从相册选择图片
在做Android图片上传功能的时候,获取图片的途径一般都有两种:拍照.从相册选择. 一.拍照 调用相机拍照有两种方法: 直接返回图片. 在调用相机的时候,传入uri,拍照后通过该uri来获取图片. ...
- android图片保存形式,Android应用开发之Android ScrollView截图和图片保存到相册的方式...
本文将带你了解Android应用开发之Android ScrollView截图和图片保存到相册的方式,希望本文对大家学Android有所帮助. 1.1首先来看你一种截取屏幕,这种代码有缺陷,只能截取一 ...
- Zxing图片识别 从相册选二维码图片解析总结
Zxing图片识别 从相册选取二维码图片进行解析总结 在Zxing扫描识别和图片识别的解析对象是相同的 本文分三个步骤: 1 获取相册的照片 2 解析二维码图片 3 返回结果 1) 获取相册照片 go ...
最新文章
- Ubuntu中Samba的安装配置和使用[图文]
- android uri获取参数,android-无法从深度链接获取Uri数据
- 在word中插入目录
- arcgis jsapi接口入门系列(6):样式
- centOS 6 和centOS 7 防火墙指令
- python二分法查找程序_Python程序查找地板划分
- Linux磁盘分区/格式化/挂载目录
- linux下部署tomcat的备忘
- 微软人工智能-服务和 API
- 各地“十四五”规划促智能网联新发展 | 政策解读系列
- 计算机颜色更换,如何给证件照换底色;怎么快速更换证件照底色
- 杭电计算机学硕还是专硕就业好,19计算机考研选学硕还是专硕?
- Python获取对象所占内存大小方法
- URP——后期处理特效——通道混合器Channel Mixer
- 清华大学计算机系刘斌,queueing刘斌,男,工学博士 ,清华大学计算机科学与技...
- jQuery实现手机号码的验证
- Mysql跨库跨表复制数据
- eBay卖家用WorldFirst将PayPal美元提现国内银行教程!
- linux浏览器无法下载,红芯浏览器目前已经无法正常下载到
- 前端包管理器的依赖管理原理