前言:

加载并显示gif是App常见的一个功能,像加载普通图片一样,大体应该包含以下几项功能:

1、自动下载GIF到本地文件作为缓存,第二次加载同一个url的图片不需要下载第二遍

2、由于GIF往往较大,要显示圆形的进度条提示下载进度

3、在GIF完全下载完之前,先显示GIF的第一帧图像进行占位,完全下载完毕之后自动播放动画。

4、两个不同的页面加载同一张GIF,两个页面的加载进度应该一致

5、支持ViewPager同时加载多个GIF动图

效果演示:

实现思路:

1、关于下载和磁盘缓存:

我这里使用HttpConnection根据url进行下载,在下载之前先将url字符串使用16位MD5进行转换,让下载的文件名为url的MD5码,然后以4096字节为单位,使用ByteStremBuffer进行边读边写,防止下载过程中内存溢出,而且不时的向磁盘写入还可以帮助实现GIF第一帧占位的效果。

2、关于进度指示:

我这里使用了一个圆形的第三方Progress Bar和一个TextView实现,由于在下载过程中以4096为缓冲,所以每下载4096字节就会更新一次进度UI。文件总大小由http返回报文的头部的Content-length返回,通过已下载大小除以这个length得出下载百分比。

3、关于不同页面的下载同步:

用户在首页会看到一个gif,这时候点击图片可以跳进大图页继续这个gif的下载,用户在首页的下载进度到带到大图页来,不能让用户下载两遍,也不能在大图页打开一个才下载了一半的图像。

首先在下载开始之前,建立一个MD5.tmp的文件用来存储下载内容,在下载完毕之后将.tmp文件名后缀去掉,这样通过文件系统检索一个GIF是否已被下载的时候,没有下载完成的图片就不会被检索出来。

如果有一个url已经开始了一次下载,这时候又有一个下载请求同一个url,此时会将请求的imageView,textView和progressBar使用一个WeakReference引用起来,防止内存泄漏,然后把这三个空间添加到一个HashMap里去,这个HashMap的key是url,value就是这些控件的弱引用组成的list。当下载线程更新进度或完成的时候,会从这个HashMap中根据url取出所有和这张gif有关的控件,然后把这些控件统一的更新状态,这样就可以保证不同页面的控件的进度相同,也避免了一个文件下载多次的情况。

4、关于使用GIF的第一帧进行下载占位:

GIF的显示使用了github上的开源项目:android-gif-drawable,地址:https://github.com/koral--/android-gif-drawable。是一个非常优秀的框架,其内部使用c语言编写了一些效率非常高的执行代码。

这个框架的可以直接根据输入流进行加载,也就是说不用等gif文件完全下载完毕就可以显示已经下载完毕的内容,甚至可以向浏览器那样一行像素一行像素的进行加载,十分好用。

根据框架的这个特性,只需要将还没有下载好的文件直接传到Drawable里,让道gifImageView中显示即可,并且在这之前要判断能否拿到第一帧,然后设置播放选项为暂停。

5、关于VIewPager的使用

在ViewPager的Adapter使用的时候遇到了很多麻烦,主要是由于ViewPager的缓存机制引起的,会引起显示重复,无控件显示等等问题,要解决在ViewPager中的使用,并让GifImageView和普通ImageView一起在ViewPager中和平共处,需要先研究好ViewPager的缓存机制。在这里我是先根据所有图片数量生成同等多的imageView放在一个数组里,然后ViewPager切换到哪张就从数组里拿出哪张放到ViewPager的Container里。GIfImageVIew也是这样,不过是放在另一个数组里,根据position取得相应的GIFImageView,然后用container来add,这里对于add过一遍的GIfImageView会报异常,通过catch解决。

具体代码:

加载工具类:

import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;import com.imaginato.qravedconsumer.task.AlxMultiTask;
import com.lidroid.xutils.HttpUtils;
import com.pnikosis.materialishprogress.ProgressWheel;
import com.qraved.app.R;import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;import pl.droidsonroids.gif.GifDrawable;
import pl.droidsonroids.gif.GifImageView;/*** Created by Alex on 2016/6/16.*/
public class AlxGifHelper {public static class ProgressViews{public ProgressViews(WeakReference<gifimageview> gifImageViewWeakReference, WeakReference<progresswheel> progressWheelWeakReference, WeakReference<textview> textViewWeakReference,int displayWidth) {this.gifImageViewWeakReference = gifImageViewWeakReference;this.progressWheelWeakReference = progressWheelWeakReference;this.textViewWeakReference = textViewWeakReference;this.displayWidth = displayWidth;}public WeakReference<gifimageview> gifImageViewWeakReference;//gif显示控件public WeakReference<progresswheel> progressWheelWeakReference;//用来装饰的圆形进度条public WeakReference<textview> textViewWeakReference;//用来显示当前进度的文本框public int displayWidth;//imageView的控件宽度}public static ConcurrentHashMap<string,arraylist<progressviews>> memoryCache;//防止同一个gif文件建立多个下载线程,url和imageView是一对多的关系,如果一个imageView建立了一次下载,那么其他请求这个url的imageView不需要重新开启一次新的下载,这几个imageView同时回调//为了防止内存泄漏,这个一对多的关系均使用LRU缓存/*** 通过本地缓存或联网加载一张GIF图片* @param url* @param gifView*/public static void displayImage(final String url, GifImageView gifView, ProgressWheel progressBar , TextView tvProgress, int displayWidth){//首先查询一下这个gif是否已被缓存String md5Url = getMd5(url);String path = gifView.getContext().getCacheDir().getAbsolutePath()+"/"+md5Url;//带.tmp后缀的是没有下载完成的,用于加载第一帧,不带tmp后缀是下载完成的,//这样做的目的是为了防止一个图片正在下载的时候,另一个请求相同url的imageView使用未下载完毕的文件显示一半图像JLogUtils.i("AlexGIF","gif图片的缓存路径是"+path);final File cacheFile = new File(path);if(cacheFile.exists()){//如果本地已经有了这个gif的缓存JLogUtils.i("AlexGIF","本图片有缓存");if(displayImage(cacheFile,gifView,displayWidth)) {//如果本地缓存读取失败就重新联网下载if (progressBar != null) progressBar.setVisibility(View.GONE);if (tvProgress!=null)tvProgress.setVisibility(View.GONE);return;}}//为了防止activity被finish了但是还有很多gif还没有加载完成,导致activity没有及时被内存回收导致内存泄漏,这里使用弱引用final WeakReference<gifimageview> imageViewWait= new WeakReference<gifimageview>(gifView);final WeakReference<progresswheel> progressBarWait= new WeakReference<progresswheel>(progressBar);final WeakReference<textview> textViewWait= new WeakReference<textview>(tvProgress);if(gifView.getId()!= R.id.gif_photo_view)gifView.setImageResource(R.drawable.qraved_bg_default);//设置没有下载完成前的默认图片if(memoryCache!=null && memoryCache.get(url)!=null){//如果以前有别的imageView加载过JLogUtils.i("AlexGIF","以前有别的ImageView申请加载过该gif"+url);//可以借用以前的下载进度,不需要新建一个下载线程了memoryCache.get(url).add(new ProgressViews(imageViewWait,progressBarWait,textViewWait,displayWidth));return;}if(memoryCache==null)memoryCache = new ConcurrentHashMap<>();if(memoryCache.get(url)==null)memoryCache.put(url,new ArrayList<progressviews>());//将现在申请加载的这个imageView放到缓存里,防止重复加载memoryCache.get(url).add(new ProgressViews(imageViewWait,progressBarWait,textViewWait,displayWidth));final HttpUtils http = new HttpUtils();// 下载图片startDownLoad(url, new File(cacheFile.getAbsolutePath()+".tmp"), new DownLoadTask() {@Overridepublic void onStart() {JLogUtils.i("AlexGIF","下载GIF开始");ProgressWheel progressBar = progressBarWait.get();TextView tvProgress = textViewWait.get();if(progressBar!=null){progressBar.setVisibility(View.VISIBLE);progressBar.setProgress(0);if(tvProgress==null)return;tvProgress.setVisibility(View.VISIBLE);tvProgress.setText("1%");}}@Overridepublic void onLoading(long total, long current) {int progress = 0;//得到要下载文件的大小,是通过http报文的header的Content-Length获得的,如果获取不到就是-1if(total>0)progress = (int)(current*100/total);JLogUtils.i("AlexGIF","下载gif的进度是"+progress+"%"+"    现在大小"+current+"   总大小"+total);ArrayList<progressviews> viewses = memoryCache.get(url);if(viewses ==null)return;JLogUtils.i("AlexGIF","该gif的请求数量是"+viewses.size());for(ProgressViews vs : viewses){//遍历所有的进度条,修改同一个url请求的进度显示ProgressWheel progressBar = vs.progressWheelWeakReference.get();if(progressBar!=null){progressBar.setProgress((float)progress/100f);if(total==-1)progressBar.setProgress(20);//如果获取不到大小,就让进度条一直转}TextView tvProgress = vs.textViewWeakReference.get();if(tvProgress != null)tvProgress.setText(progress+"%");}//显示第一帧直到全部下载完之后开始动画getFirstPicOfGIF(new File(cacheFile.getAbsolutePath()+".tmp"),vs.gifImageViewWeakReference.get());}public void onSuccess(File file) {if(file==null)return;String path = file.getAbsolutePath();if(path==null || path.length()<5)return;File downloadFile = new File(path);File renameFile = new File(path.substring(0,path.length()-4));if(path.endsWith(".tmp"))downloadFile.renameTo(renameFile);//将.tmp后缀去掉Log.i("AlexGIF","下载GIf成功,文件路径是"+path+" 重命名之后是"+renameFile.getAbsolutePath());if(memoryCache==null)return;ArrayList<progressviews> viewArr = memoryCache.get(url);if(viewArr==null || viewArr.size()==0)return;for(ProgressViews ws:viewArr){//遍历所有的进度条和imageView,同时修改所有请求同一个url的进度//显示imageViewGifImageView gifImageView = ws.gifImageViewWeakReference.get();if (gifImageView!=null)displayImage(renameFile,gifImageView,ws.displayWidth);//修改进度条TextView tvProgress = ws.textViewWeakReference.get();ProgressWheel progressBar = ws.progressWheelWeakReference.get();if(progressBar!=null)progressBar.setVisibility(View.GONE);if(tvProgress!=null)tvProgress.setVisibility(View.GONE);}JLogUtils.i("AlexGIF",url+"的imageView已经全部加载完毕,共有"+viewArr.size()+"个");memoryCache.remove(url);//这个url的全部关联imageView都已经显示完毕,清除缓存记录}@Overridepublic void onFailure(Throwable e) {Log.i("Alex","下载gif图片出现异常",e);TextView tvProgress = textViewWait.get();ProgressWheel progressBar = progressBarWait.get();if(progressBar!=null)progressBar.setVisibility(View.GONE);if(tvProgress!=null)tvProgress.setText("image download failed");if(memoryCache!=null)memoryCache.remove(url);//下载失败移除所有的弱引用}});}/*** 通过本地文件显示GIF文件* @param localFile 本地的文件指针* @param gifImageView* displayWidth imageView控件的宽度,用于根据gif的实际高度重设控件的高度来保证完整显示,传0表示不缩放gif的大小,显示原始尺寸*/public static boolean displayImage(File localFile,GifImageView gifImageView,int displayWidth){if(localFile==null || gifImageView==null)return false;JLogUtils.i("AlexGIF","准备加载gif"+localFile.getAbsolutePath()+"显示宽度为"+displayWidth);GifDrawable gifFrom;try {gifFrom = new GifDrawable(localFile);int raw_height = gifFrom.getIntrinsicHeight();int raw_width = gifFrom.getIntrinsicWidth();JLogUtils.i("AlexGIF","图片原始height是"+raw_height+"  图片原始宽是:"+raw_width);if(gifImageView.getScaleType() != ImageView.ScaleType.CENTER_CROP && gifImageView.getScaleType()!= ImageView.ScaleType.FIT_XY){//如果大小应该自适应的话进入该方法(也就是wrap content),不然高度不会自动变化if(raw_width<1 || raw_height<1)return false;int imageViewWidth = displayWidth;if(imageViewWidth < 1)imageViewWidth = raw_width;//当传来的控件宽度不大对的时候,就显示gif的原始大小int imageViewHeight = imageViewWidth*raw_height/raw_width;JLogUtils.i("AlexGIF","缩放完的gif是"+imageViewWidth+" X "+imageViewHeight);ViewGroup.LayoutParams params = gifImageView.getLayoutParams();if(params!=null){params.height = imageViewHeight;params.width = imageViewWidth;}}else {JLogUtils.i("AlexGIF","按照固定大小进行显示");}gifImageView.setImageDrawable(gifFrom);return true;} catch (IOException e) {JLogUtils.i("AlexGIF","显示gif出现异常",e);return false;}}/*** 用于获取一个String的md5值* @param str* @return*/public static String getMd5(String str) {if(str==null || str.length()<1)return "no_image.gif";MessageDigest md5 = null;try {md5 = MessageDigest.getInstance("MD5");byte[] bs = md5.digest(str.getBytes());StringBuilder sb = new StringBuilder(40);for(byte x:bs) {if((x & 0xff)>>4 == 0) {sb.append("0").append(Integer.toHexString(x & 0xff));} else {sb.append(Integer.toHexString(x & 0xff));}}if(sb.length()<24)return sb.toString();return sb.toString().substring(8,24);//为了提高磁盘的查找文件速度,让文件名为16位} catch (NoSuchAlgorithmException e) {JLogUtils.i("Alex","MD5加密失败");return "no_image.gif";}}public static abstract class DownLoadTask{abstract void onStart();abstract void onLoading(long total, long current);abstract void onSuccess(File target);abstract void onFailure(Throwable e);boolean isCanceled;}/*** 开启下载任务到线程池里,防止多并发线程过多* @param uri* @param targetFile* @param task*/public static void startDownLoad(final String uri, final File targetFile, final DownLoadTask task){final Handler handler = new Handler();new AlxMultiTask<void,void,void>(){//开启一个多线程池,大小为cpu数量+1@Overrideprotected Void doInBackground(Void... params) {task.onStart();downloadToStream(uri,targetFile,task,handler);return null;}}.executeDependSDK();}/*** 通过httpconnection下载一个文件,使用普通的IO接口进行读写* @param uri* @param targetFile* @param task* @return*/public static long downloadToStream(String uri, final File targetFile, final DownLoadTask task, Handler handler) {if (task == null || task.isCanceled) return -1;HttpURLConnection httpURLConnection = null;BufferedInputStream bis = null;OutputStream outputStream = null;long result = -1;long fileLen = 0;long currCount = 0;try {try {final URL url = new URL(uri);outputStream = new FileOutputStream(targetFile);httpURLConnection = (HttpURLConnection) url.openConnection();httpURLConnection.setConnectTimeout(20000);httpURLConnection.setReadTimeout(10000);final int responseCode = httpURLConnection.getResponseCode();if (HttpURLConnection.HTTP_OK == responseCode) {bis = new BufferedInputStream(httpURLConnection.getInputStream());result = httpURLConnection.getExpiration();result = result < System.currentTimeMillis() ? System.currentTimeMillis() + 40000 : result;fileLen = httpURLConnection.getContentLength();//这里通过http报文的header Content-Length来获取gif的总大小,需要服务器提前把header写好} else {Log.e("Alex","downloadToStream -> responseCode ==> " + responseCode);return -1;}} catch (final Exception ex) {handler.post(new Runnable() {@Overridepublic void run() {task.onFailure(ex);}});return -1;}if (task.isCanceled) return -1;byte[] buffer = new byte[4096];//每4k更新进度一次int len = 0;BufferedOutputStream out = new BufferedOutputStream(outputStream);while ((len = bis.read(buffer)) != -1) {out.write(buffer, 0, len);currCount += len;if (task.isCanceled) return -1;final long finalFileLen = fileLen;final long finalCurrCount = currCount;handler.post(new Runnable() {@Overridepublic void run() {task.onLoading(finalFileLen, finalCurrCount);}});}out.flush();handler.post(new Runnable() {@Overridepublic void run() {task.onSuccess(targetFile);}});} catch (Throwable e) {result = -1;task.onFailure(e);} finally {if (bis != null) {try {bis.close();} catch (final Throwable e) {handler.post(new Runnable() {@Overridepublic void run() {task.onFailure(e);}});}}}return result;}/*** 加载gif的第一帧图像,用于下载完成前占位* @param gifFile* @param imageView*/public static void getFirstPicOfGIF(File gifFile,GifImageView imageView){if(imageView==null)return;if(imageView.getTag(R.style.AppTheme) instanceof Integer)return;//之前已经显示过第一帧了,就不用再显示了try {GifDrawable gifFromFile = new GifDrawable(gifFile);boolean canSeekForward = gifFromFile.canSeekForward();if(!canSeekForward)return;JLogUtils.i("AlexGIF","是否能显示第一帧图片"+canSeekForward);//下面是一些其他有用的信息
//            int frames = gifFromFile.getNumberOfFrames();
//            JLogUtils.i("AlexGIF","已经下载完多少帧"+frames);
//            int bytecount = gifFromFile.getFrameByteCount();
//            JLogUtils.i("AlexGIF","一帧至少多少字节"+bytecount);
//            long memoryCost = gifFromFile.getAllocationByteCount();
//            JLogUtils.i("AlexGIF","内存开销是"+memoryCost);gifFromFile.seekToFrame(0);gifFromFile.pause();//静止在该帧imageView.setImageDrawable(gifFromFile);imageView.setTag(R.style.AppTheme,1);//标记该imageView已经显示过第一帧了} catch (IOException e) {JLogUtils.i("AlexGIF","获取gif信息出现异常",e);}}
}
</void,void,void></progressviews></progressviews></progressviews></textview></textview></progresswheel></progresswheel></gifimageview></gifimageview></string,arraylist<progressviews></textview></progresswheel></gifimageview></textview></progresswheel></gifimageview>

线程池:

android.os.AsyncTask;
import android.os.Build;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** Created by Alex on 2016/4/19.* 用于替换系统自带的AsynTask,使用自己的多线程池,执行一些比较复杂的工作,比如select photos,这里用的是缓存线程池,也可以用和cpu数相等的定长线程池以提高性能*/
public abstract class AlxMultiTask<params, result=""> extends AsyncTask<params, result=""> {private static ExecutorService photosThreadPool;//用于加载大图的线程池private final int CPU_COUNT = Runtime.getRuntime().availableProcessors();private final int CORE_POOL_SIZE = CPU_COUNT + 1;public void executeDependSDK(Params...params){if(photosThreadPool==null)photosThreadPool = Executors.newFixedThreadPool(CORE_POOL_SIZE);if(Build.VERSION.SDK_INT<11) super.execute(params);else super.executeOnExecutor(photosThreadPool,params);}}
</params,></params,>

ViewPager Adpater的写法(截取)

public class PhotoImageViewPageAdapter extends PagerAdapter {@Overridepublic Object instantiateItem(ViewGroup container, int position) {String imageUrl = "https://xxx.com/sdf/xxx.gif";JLogUtils.i("AlexGIF","当前图片->"+imageUrl);if(imageUrl.endsWith(".gif")){//如果是gif动图JLogUtils.i("AlexGIF","现在是gif大图");View rl_gif = LayoutInflater.from(activity).inflate(R.layout.layout_photo_loading_gif_imageview, null);//这种方式容易导致内存泄漏GifImageView gifImageView = (GifImageView) rl_gif.findViewById(R.id.gif_photo_view);ProgressWheel progressWheel = (ProgressWheel) rl_gif.findViewById(R.id.progress_wheel);CustomTextView tv_progress = (CustomTextView) rl_gif.findViewById(R.id.tv_progress);AlxGifHelper.displayImage(imageUrl,gifImageView,progressWheel,tv_progress,0);//最后一个参数传0表示不缩放gif的大小,显示原始尺寸try {container.addView(rl_gif);//这里要注意由于container是一个复用的控件,所以频繁的addView会导致多张相同的图片重叠,必须予以处置}catch (Exception e){JLogUtils.i("AlexGIF","父控件重复!!!!,这里出现异常很正常",e);}return rl_gif;//这里有个大坑,千万不能return container,但是在return之前必须addView}}return container;}
}

布局文件

<!--?xml version="1.0" encoding="utf-8"?-->
<relativelayout android:id="@+id/rl_gif" android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical" xmlns:android="https://schemas.android.com/apk/res/android" xmlns:wheel="https://schemas.android.com/apk/res-auto"><pl.droidsonroids.gif.gifimageview android:id="@+id/gif_photo_view" android:layout_centerhorizontal="true" android:layout_centervertical="true" android:layout_height="match_parent" android:layout_width="match_parent"><textview android:id="@+id/tv_progress" android:layout_centerhorizontal="true" android:layout_centervertical="true" android:layout_height="wrap_content" android:layout_width="wrap_content" android:text="2%" android:textcolor="@color/white" android:textsize="15sp"><com.pnikosis.materialishprogress.progresswheel android:id="@+id/progress_wheel" android:layout_centerhorizontal="true" android:layout_centervertical="true" android:layout_gravity="center" android:layout_height="60dp" android:layout_width="60dp" wheel:matprog_barcolor="#5097DA" wheel:matprog_progressindeterminate="true"></com.pnikosis.materialishprogress.progresswheel></textview></pl.droidsonroids.gif.gifimageview></relativelayout>

中间的ProgressBar使用了一个第三方库

dependencies {compile 'com.pnikosis:materialish-progress:1.7'
}

Android加载网络GIF完整解决方案相关推荐

  1. Android加载网络图片资源

    Android加载网络图片资源 获取图片资源 public static Bitmap getBitmap(String path) throws IOException {URL url = new ...

  2. android 获取网络视频资源,Android 加载网络视频(url地址)第三方框架简用

    正好最近做项目有个需要加载网络视频技术,就找了找,碰到了这个开源的框架.主要还是网络请求下来的视频地址,然后简单的,赋一下值,非常OK.不过限自己练习用毕竟看需求的嘛,不多介绍复制用吧. 先看看效果大 ...

  3. android 加载网络bitmap图片 oom 简书_Android常见问题--ImageView加载图片OOM

    开发中给ImageView加载一个高质量图片时,APP抛出了"Canvas: trying to draw too large(840253440bytes) bitmap."的异 ...

  4. android加载网络gif图片不显示不出来的,android显示网络gif图片

    这功能源自负责app中要加一个显示gif广告图功能. android自带控件不支持gif图片,网上很多通过扩展ImageView或View来实现支持gif图片,但在android4.0后,需要关闭硬件 ...

  5. android如何添加gif,Android加载Gif和ImageView的通用解决方案:android-gif-drawable(1)...

     Android加载Gif和ImageView的通用解决方案:android-gif-drawable(1) Android自己的ImageView或者View不能直接加载运行Gif图片,如果要在 ...

  6. gif android 点击 加载,android 加载显示gif图片的解决方案

    使用方法: 1-把GifView.jar加入你的项目. 2-在xml中配置GifView的基本属性,GifView继承自View类,和Button.ImageView一样是一个UI控件.如: andr ...

  7. 【走过巨坑】android studio对于jni调用及运行闪退无法加载库的问题解决方案

    [走过巨坑]android studio对于jni调用及运行闪退无法加载库的问题解决方案 参考文章: (1)[走过巨坑]android studio对于jni调用及运行闪退无法加载库的问题解决方案 ( ...

  8. android webView加载网络视频

    之前,我写过webView加载本地网页的博客,今天,就写写webView加载网络视频的内容. 一.加载网页 1.WebView用来显示网页,使用必须时刻注意我们需要添加网络权限 <uses-pe ...

  9. android加载网页pdf,android 一行代码搞定加载网络 pdf 文件

    之前写过一篇Android打开本地pdf文件的文章,最后总结的时候说,后面一定要拓展库,让其也能打开网络的的pdf文件.今天终于可以兑现承诺了.frok一份代码github.com/JoanZapat ...

  10. OKhttp加载网络上的图片

    使用OKhttp只需要做到五个步骤即可,下面通过一个Demo来看看OKhttp的强大之处(最下面有完整代码和详细注释): 一.关联OKhttp框架,添加网络权限 1.jar包准备 官方介绍页面有链接位 ...

最新文章

  1. poj 2892---Tunnel Warfare(线段树单点更新、区间合并)
  2. DbHelperSQL 判断数据库表结构公用方法
  3. 4.36域名重定向4.37用户认证4.38Nginx访问日志4.39日志不记录静态文件4.40日志切割...
  4. 并发编程-12线程安全策略之常见的线程不安全类
  5. C#学员信息管理试题
  6. The Moving Points
  7. python程序执行时间_用于在Python中查找程序执行时间的程序
  8. 深度学习加持的工业AI质检
  9. 应用数仓ODBC前,这些问题你需要先了解一下
  10. android大智慧安装目录,大智慧新一代目录文件结构
  11. 怎么使用PHPMailer实现邮件的发送??
  12. 【转】android开发必看资源URL
  13. matlab直接解超越方程函数,用matlab 解超越方程~
  14. 开独立网店需要哪些步骤
  15. 连接交换机太讲究,很多网工总弄不明白
  16. 我的QT Creator学习笔记(二十一)——Qt国际化和Qt插件
  17. 用java实现的文本编辑器可以媲美windows自带的编辑器吗?(功能全,超详细)
  18. python编程学习笔记⑦-1函数
  19. 1024程序员节,我被喷上了热搜!
  20. 用Shell脚本实现自动从NewSmth.net的MyPhoto版下载照片

热门文章

  1. 内网穿透详细教程——远程项目测试(免费使用)
  2. web前端行业调研报告_web前端开发述职报告
  3. 【C++程序设计语言A视频教程 全12讲 中科院】【下载链接】
  4. Java调用发微博API_编写调用新浪微博API的Java程序来发送微博
  5. java分享微博_Connect/sharing - 微博API
  6. AI和计算机会议,CCF推荐人工智能领域的会议和期刊
  7. ae遮罩路径图形扭曲插件BAO Boa
  8. UI自动化测试工具的设计
  9. Caffe 的深度学习训练全过程
  10. VS2010对话框中使用MSChart控件