上一篇文章简单介绍了图片的加载,但是实际业务中,加载图片的需求会比这复杂得多,例如这篇博客要讲的多选相册的实现,会涉及到下面几个问题:

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图片:多选相册的实现相关推荐

  1. Android图片保存到相册失败

    Android之图片保存的一些坑 生成图片的路径: // 该目录下得文件不会被相册扫描到,所以不要用这个String rootDir = context.getExternalFilesDir(nul ...

  2. Android 图片多选Hor,Android水平滚动laggy

    Horizo​​ntalScrollView不使用管理列表内存的适配器,因此它无法处理繁重(图像,自定义视图等)列表. 您可以使用此水平ListView http://www.dev-smart.co ...

  3. Android 图片选择对话框,通过本地相册或照相机获得图片,可单选或多选,单选可设置是否裁剪

    AndroidPickPhotoDialog 项目地址: wanliyang1990/AndroidPickPhotoDialog 简介:Android 图片选择对话框,通过本地相册或照相机获得图片, ...

  4. Android:支持多选的本地相册

    前段时间在做一个动态发布功能,需要用到图片上传.一开始直接调用的系统相册和相机,由于系统相机不支持多选,就花点时间做了个本地相册,在此开源下. 先上截图,依次为选择相册界面.相册详情界面.查看图片大图 ...

  5. 获取android的拍照和自定义多选相册

    获取系统的相机功能拍照这个不难,但是需要注意的是,拍照返回后的照片如果没有指定存储的路径,那么系统将自动保存到sd卡中,得到的是拍完照的缩略图,会失帧,显示有些模糊,所以在调用系统相机拍完照后我们要指 ...

  6. Android拍照及从相册选择图片传详解(终极版)

    Android 拍照及从相册选择图片传详解 先上图 新知识点速览 URI(统一资源标识符)是标识逻辑或物理资源的字符序列,与URL类似,也是一串字符.通过使用位置,名称或两者来标识Internet上的 ...

  7. Android 拍照、从相册选择图片

    在做Android图片上传功能的时候,获取图片的途径一般都有两种:拍照.从相册选择. 一.拍照 调用相机拍照有两种方法: 直接返回图片. 在调用相机的时候,传入uri,拍照后通过该uri来获取图片. ...

  8. android图片保存形式,Android应用开发之Android ScrollView截图和图片保存到相册的方式...

    本文将带你了解Android应用开发之Android ScrollView截图和图片保存到相册的方式,希望本文对大家学Android有所帮助. 1.1首先来看你一种截取屏幕,这种代码有缺陷,只能截取一 ...

  9. Zxing图片识别 从相册选二维码图片解析总结

    Zxing图片识别 从相册选取二维码图片进行解析总结 在Zxing扫描识别和图片识别的解析对象是相同的 本文分三个步骤: 1 获取相册的照片 2 解析二维码图片 3 返回结果 1) 获取相册照片 go ...

最新文章

  1. Ubuntu中Samba的安装配置和使用[图文]
  2. android uri获取参数,android-无法从深度链接获取Uri数据
  3. 在word中插入目录
  4. arcgis jsapi接口入门系列(6):样式
  5. centOS 6 和centOS 7 防火墙指令
  6. python二分法查找程序_Python程序查找地板划分
  7. Linux磁盘分区/格式化/挂载目录
  8. linux下部署tomcat的备忘
  9. 微软人工智能-服务和 API
  10. 各地“十四五”规划促智能网联新发展 | 政策解读系列
  11. 计算机颜色更换,如何给证件照换底色;怎么快速更换证件照底色
  12. 杭电计算机学硕还是专硕就业好,19计算机考研选学硕还是专硕?
  13. Python获取对象所占内存大小方法
  14. URP——后期处理特效——通道混合器Channel Mixer
  15. 清华大学计算机系刘斌,queueing刘斌,男,工学博士 ,清华大学计算机科学与技...
  16. jQuery实现手机号码的验证
  17. Mysql跨库跨表复制数据
  18. eBay卖家用WorldFirst将PayPal美元提现国内银行教程!
  19. linux浏览器无法下载,红芯浏览器目前已经无法正常下载到
  20. 前端包管理器的依赖管理原理

热门文章

  1. 佳沛奇异果猕猴桃扫盲
  2. 1276 不浪费原料的汉堡制作方案
  3. 服务器开通网站来宾帐户,IIS 增加Internet来宾用户权限
  4. 151202storyboard中, 设置子控件和父控件的高宽比
  5. 使用Mozilla Thunderbird 创建ics日历文件
  6. 牛逼!这届WWDC依旧展现了那个让你无法复制的苹果!
  7. python 异常检测算法_吴恩达机器学习中文版笔记:异常检测(Anomaly Detection)
  8. 用python写一个PDF翻译软件
  9. excel 日期选择器_Excel日期选择器工具
  10. Android - 手机下载的缓存视频在文件管理怎么找不到?