本示例介绍在Android平台下通过HTTP协议实现断点续传下载。

我们编写的是Andorid的HTTP协议多线程断点下载应用程序。直接使用单线程下载HTTP文件对我们来说是一件非常简单的事。那么,多线程断点需要什么功能?

1.多线程下载,

2.支持断点。

使用多线程的好处:使用多线程下载会提升文件下载的速度。那么多线程下载文件的过程是: 

(1)首先获得下载文件的长度,然后设置本地文件的长度。

HttpURLConnection.getContentLength();//获取下载文件的长度

RandomAccessFile file = new RandomAccessFile("QQWubiSetup.exe","rwd");

file.setLength(filesize);//设置本地文件的长度

  (2)根据文件长度和线程数计算每条线程下载的数据长度和下载位置。

     如:文件的长度为6M,线程数为3,那么,每条线程下载的数据长度为2M,每条线程开始下载的位置如下图所示。

   例如10M大小,使用3个线程来下载,

       线程下载的数据长度   (10%3 == 0 ? 10/3:10/3+1) ,第1,2个线程下载长度是4M,第三个线程下载长度为2M

        下载开始位置:线程id*每条线程下载的数据长度 = ?

下载结束位置:(线程id+1)*每条线程下载的数据长度-1=?

  (3)使用Http的Range头字段指定每条线程从文件的什么位置开始下载,下载到什么位置为止,

        如:指定从文件的2M位置开始下载,下载到位置(4M-1byte)为止

           代码如下:HttpURLConnection.setRequestProperty("Range", "bytes=2097152-4194303");

  (4)保存文件,使用RandomAccessFile类指定每条线程从本地文件的什么位置开始写入数据。

RandomAccessFile threadfile = new RandomAccessFile("QQWubiSetup.exe ","rwd");

threadfile.seek(2097152);//从文件的什么位置开始写入数据

程序结构如下图所示:

 

string.xml文件中代码:

<?xml version="1.0" encoding="utf-8"?> <resources>

<string name="hello">Hello World, MainActivity!</string>

<string name="app_name">Android网络多线程断点下载</string>

<string name="path">下载路径</string>

<string name="downloadbutton">下载</string>

<string name="sdcarderror">SDCard不存在或者写保护</string>

<string name="success">下载完成</string>

<string name="error">下载失败</string>

</resources>

main.xml文件中代码:

<?xml version="1.0" encoding="utf-8"?>    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"   android:layout_width="fill_parent"    android:layout_height="fill_parent">     <!-- 下载路径 -->

<TextView

android:layout_width="fill_parent"

android:layout_height="wrap_content"

android:text="@string/path"/>

<EditText

android:id="@+id/path"

android:text="http://www.winrar.com.cn/download/wrar380sc.exe"

android:layout_width="fill_parent"

android:layout_height="wrap_content">

</EditText>

<!-- 下载按钮 -->

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="@string/downloadbutton"

android:id="@+id/button"/>

<!-- 进度条 -->

<ProgressBar

android:layout_width="fill_parent"

android:layout_height="20dip"

style="?android:attr/progressBarStyleHorizontal"

android:id="@+id/downloadbar" />

<TextView

android:layout_width="fill_parent"

android:layout_height="wrap_content"

android:gravity="center"

android:id="@+id/resultView" />

</LinearLayout>

AndroidManifest.xml文件中代码:

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"   package="com.android.downloader"   android:versionCode="1"      android:versionName="1.0">

<uses-sdk android:minSdkVersion="8" />

<application android:icon="@drawable/icon" android:label="@string/app_name">

<activity android:name=".MainActivity"   android:label="@string/app_name">

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

</application>

<!-- 在SDCard中创建与删除文件权限 -->

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

<!-- 往SDCard写入数据权限 -->

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

<!-- 访问internet权限 -->

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

</manifest>

MainActivity中代码:

package com.android.downloader; import java.io.File;

import com.android.network.DownloadProgressListener;

import com.android.network.FileDownloader;

import android.app.Activity;

import android.os.Bundle;

import android.os.Environment;

import android.os.Handler;

import android.os.Message;

import android.view.View;

import android.widget.Button;

import android.widget.EditText;

import android.widget.ProgressBar;

import android.widget.TextView;

import android.widget.Toast;

public class MainActivity extends Activity {

private EditText downloadpathText;

private TextView resultView;

private ProgressBar progressBar;

/**

* 当Handler被创建会关联到创建它的当前线程的消息队列,该类用于往消息队列发送消息

* 消息队列中的消息由当前线程内部进行处理

*/

private Handler handler = new Handler(){

@Override

public void handleMessage(Message msg) {

switch (msg.what) {

case 1:

progressBar.setProgress(msg.getData().getInt("size"));

float num = (float)progressBar.getProgress()/(float)progressBar.getMax();

int result = (int)(num*100);

resultView.setText(result+ "%");

if(progressBar.getProgress()==progressBar.getMax()){

Toast.makeText(MainActivity.this, R.string.success, 1).show();

}

break;

case -1:

Toast.makeText(MainActivity.this, R.string.error, 1).show();

break;

}

}

};

/** Called when the activity is first created. */

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

downloadpathText = (EditText) this.findViewById(R.id.path);

progressBar = (ProgressBar) this.findViewById(R.id.downloadbar);

resultView = (TextView) this.findViewById(R.id.resultView);

Button button = (Button) this.findViewById(R.id.button);

button.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

// TODO Auto-generated method stub

String path = downloadpathText.getText().toString();

System.out.println(Environment.getExternalStorageState()+"------"+Environment.MEDIA_MOUNTED);

if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){

download(path, Environment.getExternalStorageDirectory());

}else{

Toast.makeText(MainActivity.this, R.string.sdcarderror, 1).show();

}

}

});

}

/**

* 主线程(UI线程)

* 对于显示控件的界面更新只是由UI线程负责,如果是在非UI线程更新控件的属性值,更新后的显示界面不会反映到屏幕上

* @param path

* @param savedir

*/

private void download(final String path, final File savedir) {

new Thread(new Runnable() {

@Override

public void run() {

FileDownloader loader = new FileDownloader(MainActivity.this, path, savedir, 3);

progressBar.setMax(loader.getFileSize());//设置进度条的最大刻度为文件的长度

try {

loader.download(new DownloadProgressListener() {

@Override

public void onDownloadSize(int size) {//实时获知文件已经下载的数据长度

Message msg = new Message();

msg.what = 1;

msg.getData().putInt("size", size);

handler.sendMessage(msg);//发送消息

}

});

} catch (Exception e) {

handler.obtainMessage(-1).sendToTarget();

}

}

}).start();

} }

DBOpenHelper中代码:

package com.android.service; import android.content.Context;

import android.database.sqlite.SQLiteDatabase;

import android.database.sqlite.SQLiteOpenHelper;

public class DBOpenHelper extends SQLiteOpenHelper {

private static final String DBNAME = "down.db";

private static final int VERSION = 1;

public DBOpenHelper(Context context) {

super(context, DBNAME, null, VERSION);

}

@Override

public void onCreate(SQLiteDatabase db) {

db.execSQL("CREATE TABLE IF NOT EXISTS filedownlog (id integer primary key autoincrement, downpath varchar(100), threadid INTEGER, downlength INTEGER)");

}

@Override

public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

db.execSQL("DROP TABLE IF EXISTS filedownlog");

onCreate(db);

}

}

FileService中代码:

package com.android.service; import java.util.HashMap;

import java.util.Map;

import android.content.Context;

import android.database.Cursor;

import android.database.sqlite.SQLiteDatabase;

public class FileService {

private DBOpenHelper openHelper;

public FileService(Context context) {

openHelper = new DBOpenHelper(context);

}

/**

* 获取每条线程已经下载的文件长度

* @param path

* @return

*/

public Map<Integer, Integer> getData(String path){

SQLiteDatabase db = openHelper.getReadableDatabase();

Cursor cursor = db.rawQuery("select threadid, downlength from filedownlog where downpath=?", new String[]{path});

Map<Integer, Integer> data = new HashMap<Integer, Integer>();

while(cursor.moveToNext()){

data.put(cursor.getInt(0), cursor.getInt(1));

}

cursor.close();

db.close();

return data;

}

/**

* 保存每条线程已经下载的文件长度

* @param path

* @param map

*/

public void save(String path,  Map<Integer, Integer> map){//int threadid, int position

SQLiteDatabase db = openHelper.getWritableDatabase();

db.beginTransaction();

try{

for(Map.Entry<Integer, Integer> entry : map.entrySet()){

db.execSQL("insert into filedownlog(downpath, threadid, downlength) values(?,?,?)",

new Object[]{path, entry.getKey(), entry.getValue()});

}

db.setTransactionSuccessful();

}finally{

db.endTransaction();

}

db.close();

}

/**

* 实时更新每条线程已经下载的文件长度

* @param path

* @param map

*/

public void update(String path, Map<Integer, Integer> map){

SQLiteDatabase db = openHelper.getWritableDatabase();

db.beginTransaction();

try{

for(Map.Entry<Integer, Integer> entry : map.entrySet()){

db.execSQL("update filedownlog set downlength=? where downpath=? and threadid=?",

new Object[]{entry.getValue(), path, entry.getKey()});

}

db.setTransactionSuccessful();

}finally{

db.endTransaction();

}

db.close();

}

/**

* 当文件下载完成后,删除对应的下载记录

* @param path

*/

public void delete(String path){

SQLiteDatabase db = openHelper.getWritableDatabase();

db.execSQL("delete from filedownlog where downpath=?", new Object[]{path});

db.close();

} }

DownloadProgressListener中代码:

package com.android.network;

public interface DownloadProgressListener {

public void onDownloadSize(int size);

}

FileDownloader中代码:

package com.android.network; import java.io.File;

import java.io.RandomAccessFile;

import java.net.HttpURLConnection;

import java.net.URL;

import java.util.LinkedHashMap;

import java.util.Map;

import java.util.UUID;

import java.util.concurrent.ConcurrentHashMap;

import java.util.regex.Matcher;

import java.util.regex.Pattern;

import com.android.service.FileService;

import android.content.Context;

import android.util.Log;

public class FileDownloader {

private static final String TAG = "FileDownloader";

private Context context;

private FileService fileService;

/* 已下载文件长度 */

private int downloadSize = 0;

/* 原始文件长度 */

private int fileSize = 0;

/* 线程数 */

private DownloadThread[] threads;

/* 本地保存文件 */

private File saveFile;

/* 缓存各线程下载的长度*/

private Map<Integer, Integer> data = new ConcurrentHashMap<Integer, Integer>();

/* 每条线程下载的长度 */

private int block;

/* 下载路径  */

private String downloadUrl;

/**

* 获取线程数

*/

public int getThreadSize() {

return threads.length;

}

/**

* 获取文件大小

* @return

*/

public int getFileSize() {

return fileSize;

}

/**

* 累计已下载大小

* @param size

*/

protected synchronized void append(int size) {

downloadSize += size;

}

/**

* 更新指定线程最后下载的位置

* @param threadId 线程id

* @param pos 最后下载的位置

*/

protected synchronized void update(int threadId, int pos) {

this.data.put(threadId, pos);

this.fileService.update(this.downloadUrl, this.data);

}

/**

* 构建文件下载器

* @param downloadUrl 下载路径

* @param fileSaveDir 文件保存目录

* @param threadNum 下载线程数

*/

public FileDownloader(Context context, String downloadUrl, File fileSaveDir, int threadNum) {

try {

this.context = context;

this.downloadUrl = downloadUrl;

fileService = new FileService(this.context);

URL url = new URL(this.downloadUrl);

if(!fileSaveDir.exists()) fileSaveDir.mkdirs();

this.threads = new DownloadThread[threadNum];

HttpURLConnection conn = (HttpURLConnection) url.openConnection();

conn.setConnectTimeout(5*1000);

conn.setRequestMethod("GET");

conn.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*");

conn.setRequestProperty("Accept-Language", "zh-CN");

conn.setRequestProperty("Referer", downloadUrl);

conn.setRequestProperty("Charset", "UTF-8");

conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");

conn.setRequestProperty("Connection", "Keep-Alive");

conn.connect();

printResponseHeader(conn);

if (conn.getResponseCode()==200) {

this.fileSize = conn.getContentLength();//根据响应获取文件大小

if (this.fileSize <= 0) throw new RuntimeException("Unkown file size ");

String filename = getFileName(conn);//获取文件名称

this.saveFile = new File(fileSaveDir, filename);//构建保存文件

Map<Integer, Integer> logdata = fileService.getData(downloadUrl);//获取下载记录

if(logdata.size()>0){//如果存在下载记录

for(Map.Entry<Integer, Integer> entry : logdata.entrySet())

data.put(entry.getKey(), entry.getValue());//把各条线程已经下载的数据长度放入data中

}

if(this.data.size()==this.threads.length){//下面计算所有线程已经下载的数据长度

for (int i = 0; i < this.threads.length; i++) {

this.downloadSize += this.data.get(i+1);

}

print("已经下载的长度"+ this.downloadSize);

}

//计算每条线程下载的数据长度

this.block = (this.fileSize % this.threads.length)==0? this.fileSize / this.threads.length : this.fileSize / this.threads.length + 1;

}else{

throw new RuntimeException("server no response ");

}

} catch (Exception e) {

print(e.toString());

throw new RuntimeException("don't connection this url");

}

}

/**

* 获取文件名

* @param conn

* @return

*/

private String getFileName(HttpURLConnection conn) {

String filename = this.downloadUrl.substring(this.downloadUrl.lastIndexOf('/') + 1);

if(filename==null || "".equals(filename.trim())){//如果获取不到文件名称

for (int i = 0;; i++) {

String mine = conn.getHeaderField(i);

if (mine == null) break;

if("content-disposition".equals(conn.getHeaderFieldKey(i).toLowerCase())){

Matcher m = Pattern.compile(".*filename=(.*)").matcher(mine.toLowerCase());

if(m.find()) return m.group(1);

}

}

filename = UUID.randomUUID()+ ".tmp";//默认取一个文件名

}

return filename;

}

/**

*  开始下载文件

* @param listener 监听下载数量的变化,如果不需要了解实时下载的数量,可以设置为null

* @return 已下载文件大小

* @throws Exception

*/

public int download(DownloadProgressListener listener) throws Exception{

try {

RandomAccessFile randOut = new RandomAccessFile(this.saveFile, "rw");

if(this.fileSize>0) randOut.setLength(this.fileSize);

randOut.close();

URL url = new URL(this.downloadUrl);

if(this.data.size() != this.threads.length){

this.data.clear();

for (int i = 0; i < this.threads.length; i++) {

this.data.put(i+1, 0);//初始化每条线程已经下载的数据长度为0

}

}

for (int i = 0; i < this.threads.length; i++) {//开启线程进行下载

int downLength = this.data.get(i+1);

if(downLength < this.block && this.downloadSize<this.fileSize){//判断线程是否已经完成下载,否则继续下载

this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i+1), i+1);

this.threads[i].setPriority(7);

this.threads[i].start();

}else{

this.threads[i] = null;

}

}

this.fileService.save(this.downloadUrl, this.data);

boolean notFinish = true;//下载未完成

while (notFinish) {// 循环判断所有线程是否完成下载

Thread.sleep(900);

notFinish = false;//假定全部线程下载完成

for (int i = 0; i < this.threads.length; i++){

if (this.threads[i] != null && !this.threads[i].isFinish()) {//如果发现线程未完成下载

notFinish = true;//设置标志为下载没有完成

if(this.threads[i].getDownLength() == -1){//如果下载失败,再重新下载

this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i+1), i+1);

this.threads[i].setPriority(7);

this.threads[i].start();

}

}

}

if(listener!=null) listener.onDownloadSize(this.downloadSize);//通知目前已经下载完成的数据长度

}

fileService.delete(this.downloadUrl);

} catch (Exception e) {

print(e.toString());

throw new Exception("file download fail");

}

return this.downloadSize;

}

/**

* 获取Http响应头字段

* @param http

* @return

*/

public static Map<String, String> getHttpResponseHeader(HttpURLConnection http) {

Map<String, String> header = new LinkedHashMap<String, String>();

for (int i = 0;; i++) {

String mine = http.getHeaderField(i);

if (mine == null) break;

header.put(http.getHeaderFieldKey(i), mine);

}

return header;

}

/**

* 打印Http头字段

* @param http

*/

public static void printResponseHeader(HttpURLConnection http){

Map<String, String> header = getHttpResponseHeader(http);

for(Map.Entry<String, String> entry : header.entrySet()){

String key = entry.getKey()!=null ? entry.getKey()+ ":" : "";

print(key+ entry.getValue());

}

}

private static void print(String msg){

Log.i(TAG, msg);

}

}

DownloadThread 中代码:

package com.android.network;import java.io.File;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL; import android.util.Log; public class DownloadThread extends Thread {
private static final String TAG = "DownloadThread";
private File saveFile;
private URL downUrl;
private int block; /* 下载开始位置 */
private int threadId = -1;
private int downLength;
private boolean finish = false;
private FileDownloader downloader; public DownloadThread(FileDownloader downloader, URL downUrl, File saveFile, int block, int downLength, int threadId) {
this.downUrl = downUrl;
this.saveFile = saveFile;
this.block = block;
this.downloader = downloader;
this.threadId = threadId;
this.downLength = downLength;
} @Override
public void run() {
if(downLength < block){//未下载完成
try {
HttpURLConnection http = (HttpURLConnection) downUrl.openConnection();
http.setConnectTimeout(5 * 1000);
http.setRequestMethod("GET");
http.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*");
http.setRequestProperty("Accept-Language", "zh-CN");
http.setRequestProperty("Referer", downUrl.toString());
http.setRequestProperty("Charset", "UTF-8");
int startPos = block * (threadId - 1) + downLength;//开始位置
int endPos = block * threadId -1;//结束位置
http.setRequestProperty("Range", "bytes=" + startPos + "-"+ endPos);//设置获取实体数据的范围
http.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
http.setRequestProperty("Connection", "Keep-Alive"); InputStream inStream = http.getInputStream();
byte[] buffer = new byte[1024];
int offset = 0;
print("Thread " + this.threadId + " start download from position "+ startPos);
RandomAccessFile threadfile = new RandomAccessFile(this.saveFile, "rwd");
threadfile.seek(startPos); while ((offset = inStream.read(buffer, 0, 1024)) != -1) {
threadfile.write(buffer, 0, offset);
downLength += offset;
downloader.update(this.threadId, downLength);
downloader.append(offset);
} threadfile.close();
inStream.close();
print("Thread " + this.threadId + " download finish");
this.finish = true;
} catch (Exception e) {
this.downLength = -1;
print("Thread "+ this.threadId+ ":"+ e);
}
}
} private static void print(String msg){
Log.i(TAG, msg);
} /**
* 下载是否完成
* @return
*/
public boolean isFinish() {
return finish;
} /**
* 已经下载的内容大小
* @return 如果返回值为-1,代表下载失败
*/
public long getDownLength() {
return downLength;
}}

运行效果如下

最后,希望转载的朋友能够尊重作者的劳动成果,加上转载地址:http://www.cnblogs.com/hanyonglu/archive/2012/02/20/2358801.html 谢谢。

示例源码:点击下载

完毕。^_^

andoid 多线程断点下载的更多相关文章

  1. iOS开发网络篇—大文件的多线程断点下载

    http://www.cnblogs.com/wendingding/p/3947550.html iOS开发网络篇—多线程断点下载 说明:本文介绍多线程断点下载.项目中使用了苹果自带的类,实现了同时 ...

  2. iOS开发网络请求——大文件的多线程断点下载

    iOS开发中网络请求技术已经是移动app必备技术,而网络中文件传输就是其中重点了.网络文件传输对移动客户端而言主要分为文件的上传和下载.作为开发者从技术角度会将文件分为小文件和大文件.小文件因为文件大 ...

  3. iOS开发网络篇—大文件的多线程断点下载(转)

    http://www.cnblogs.com/wendingding/p/3947550.html   iOS开发网络篇—多线程断点下载 说明:本文介绍多线程断点下载.项目中使用了苹果自带的类,实现了 ...

  4. 33、多线程断点下载的实现&界面的更新

    <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android=&quo ...

  5. iOS开发网络篇—多线程断点下载

    iOS开发网络篇—多线程断点下载 说明:本文介绍多线程断点下载.项目中使用了苹果自带的类,实现了同时开启多条线程下载一个较大的文件.因为实现过程较为复杂,所以下面贴出完整的代码. 实现思路:下载开始, ...

  6. Android(java)学习笔记216:多线程断点下载的原理(Android实现)

    之前在Android(java)学习笔记215中,我们从JavaSE的角度去实现了多线程断点下载,下面从Android角度实现这个断点下载: 1.新建一个Android工程: (1)其中我们先实现布局 ...

  7. 我的Android进阶之旅------>Android基于HTTP协议的多线程断点下载器的实现

    一.首先写这篇文章之前,要了解实现该Android多线程断点下载器的几个知识点 1.多线程下载的原理,如下图所示 注意:由于Android移动设备和PC机的处理器还是不能相比,所以开辟的子线程建议不要 ...

  8. Android(java)学习笔记159:多线程断点下载的原理(Android实现)

    之前在Android(java)学习笔记215中,我们从JavaSE的角度去实现了多线程断点下载,下面从Android角度实现这个断点下载: 1. 新建一个Android工程: (1)其中我们先实现布 ...

  9. java多线程断点下载原理(代码实例演示)

    原文:http://www.open-open.com/lib/view/open1423214229232.html 其实多线程断点下载原理,很简单的,那么我们就来先了解下,如何实现多线程的断点下载 ...

随机推荐

  1. stuff for xml path

    SumOrg=stuff((select '/'+User_Org from V_RubricInfoRefer t where t.RubricID=V_RubricInfoRefer.Rubric ...

  2. android eclipse 报error loading /system/media/audio/ xxx 错的解决办法。

    只针对 报错..error   loading /system/media/audio/ xxx.ogg 一步操作 解决烦恼..把 模拟器声音 关了..所有的错 都没了. 包括 关闭按键声音,触摸声音 ...

  3. Graphics 小记

    1.切图 drowg.DrawImage(productImg1, new System.Drawing.Rectangle(30, 30, 300, 300), new System.Drawing ...

  4. SharePoint 2013备份方法整理

    关于SharePoint备份 SharePoint的备份是一个数据副本,主要用于在系统出现故障后还原和恢复该数据. 备份的工具主要有以下几种(写的不全,欢迎补充.) SharePoint管理中心的备份 ...

  5. Echarts+WPF

    C# using System; using System.Collections.Generic; using System.IO; using System.Linq; using System. ...

  6. Office Diary(工作日记本)

    Office Diary 是一款免费.绿色小巧的写日记和收集文档资料的软件,界面效仿Office Word ,支持Word中常用的文字编辑排版功能,可以作为Microsoft Office产品系列的强 ...

  7. Apple开启双重认证过程

    1.准备 1.1 AppleID账号.密码 1.2 打算用于接收开启双重认证的十一位手机号 1.3 AppleID账号密保问题 2.操作步骤: 2.1 打开设置 2.2 点击个人账户头像 注意:当前有 ...

  8. Mac 安装 mysqlclient

    尝试在虚拟环境下通过 pip 安装: pip install mysqlclient 然后报错:OSError: mysql_config not found 找到官方文档 https://githu ...

  9. 流水的算法,铁打的损失函数/MLE

    机器学习算法可以说是不少的,如果死记硬背的话,只能当时记得推导过程和步骤,过一段时间就又想不起来了,只能依稀记得一些影子.所以,应该找到算法的一些通用的方法来理解算法的思路以及推导过程. 我认为,最大 ...

  10. vue可视化图表 基于Echarts封装好的v-charts简介

    **vue可视化图表 基于Echarts封装好的v-charts** 近期公司又一个新的需求,要做一个订单和销售额统计的项目,需要用到可视化图表来更直观的展示数据.首先我想到的是Echarts,众所周 ...