編輯:關於Android編程
上一篇講了內存緩存,這一篇就緊接著講一下磁盤緩存DiskLruCache.
官方文檔:
https://developer.android.com/training/displaying-bitmaps/cache-bitmap.html#memory-cache
內存緩存雖然對於快速訪問最近浏覽過的圖片很有用,但是我們不能總想著將圖片全部緩存在內存。因為內存的大小畢竟是有限制的,像GridView這樣的具有較大數據集的組件可以輕松地填充內存緩存。還有就是我們的應用程序可能會被其他任務(如電話呼叫)中斷,而在後台可能會被終止並且內存緩存被破壞。一旦用戶恢復,我們的應用程序必須再次加載每個圖像。在這些情況下,可以使用磁盤緩存來持久存儲圖片,這樣如果應用內存釋放了,再次打開用的時候,就不用去網絡上加載了,直接去外部存儲設備獲取,這樣節約時間,也節省流量。當然,從磁盤獲取圖片比從內存加載更慢,應該在後台線程中完成,因為磁盤讀取時間可能是不可預測的。
注意:如果更頻繁地訪問緩存的圖像,例如在圖像庫應用程序中,ContentProvider可能更適合存儲緩存的圖像。
內存緩存google提供了LruCache,而磁盤緩存,google推薦使用DiskLruCache,由於DiskLruCache並不是由Google官方編寫的,所以這個類並沒有被包含在Android API當中,我們需要將這個類從網上下載下來,然後手動添加到項目當中。DiskLruCache的源碼在Google Source上,地址如下:
https://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java
我上傳的是是從googleSource直接拷貝下來的,有兩個地方需要修改一下
1.注釋掉System.logW...相關的代碼。2.將StrictLineReader替換為BufferedInputStream
修改後的就是guolin博主上傳的。
下載好了源碼之後,只需要在項目中新建一個libcore.io包,然後將DiskLruCache.Java文件復制到這個包中即可
關於DiskLruCache的源碼分析,可以參考:
http://blog.csdn.net/lmj623565791/article/details/47251585
下面就寫個demo,來演示一下如何使用DiskLruCache:
demo做的事情就是去網絡上加載一張圖片,第一次加載,磁盤中肯定是沒有緩存的,所以從網絡上取回來後,再存入磁盤,下次加載該圖片則直接從磁盤獲取。
先分步解釋一下代碼,最後再貼全部代碼:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); imageView = (ImageView) findViewById(R.id.imageView); // Initialize disk cache on background thread File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR); Log.d(TAG,"diskcacheDir="+cacheDir.getAbsolutePath()); new InitDiskCacheTask().execute(cacheDir); }
/** * 初始化DiskLruCache */ class InitDiskCacheTask extends AsyncTask第一步是另起線程去初始化DiskLruCache,{ @Override protected Void doInBackground(File... params) { synchronized (mDiskCacheLock) { File cacheDir = params[0]; try { mDiskLruCache = DiskLruCache.open(cacheDir,getAppVersion(),1,DISK_CACHE_SIZE); mDiskCacheStarting = false; // Finished initialization mDiskCacheLock.notifyAll(); // Wake any waiting threads } catch (IOException e) { e.printStackTrace(); } } return null; } }
磁盤緩存首先確定好存儲的路徑:
// Creates a unique subdirectory of the designated app cache directory. Tries to use external // but if not mounted, falls back on internal storage. private File getDiskCacheDir(Context context, String uniqueName) { // Check if media is mounted or storage is built-in, if so, try and use external cache dir // otherwise use internal cache dir final String cachePath = Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !isExternalStorageRemovable() ? getExternalCacheDir().getPath() : context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName); }
cj.com.disklrucache就是應用的包名,disccache就是自己設置的緩存子目錄。
選擇在這個位置有兩點好處:第一,這是存儲在SD卡上的,因此即使緩存再多的數據也不會對手機的內置存儲空間有任何影響,只要SD卡空間足夠就行。第二,這個路徑被Android系統認定為應用程序的緩存路徑,當程序被卸載的時候,這裡的數據也會一起被清除掉,這樣就不會出現刪除程序之後手機上還有很多殘留數據的問題。
當然如果沒有sd卡的話,就存儲在/cache/目錄下。
mDiskLruCache = DiskLruCache.open(cacheDir,getAppVersion(),1,DISK_CACHE_SIZE);創建DiskLruCache需要四個參數,分別是緩存路徑,應用版本,每個key對應的值的個數(這裡一般寫一個),緩存最大容量(單位字節)。
初始化完DiskLruCache,會在緩存目錄創建一個文件名為journal文件,journal文件是DiskLruCache的一個日志文件,程序對緩存數據的操作記錄都存放在這個文件中,那就看一下這個文件的內容:
首先看前五行:
libcore.io.DiskLruCache
第二行DiskLruCache的版本號,源碼中為常量1
第三行為你的app的版本號,自己傳入指定的
第四行指每個key對應幾個文件,一般為1自己傳入指定的
第五行,空行
這就是文件頭。
第6開始就是你緩存數據的記錄了,這裡我只操作一張圖片,所以只有關於一個key記錄
以DIRTY前綴開始的,後面緊跟著緩存圖片的key.我這裡用hashcode值作為key了。
以CLEAN前綴開始的,表示上面key值代表的圖片緩存成成功了,後面緊跟的數字的就是緩存的字節大小.對比一下(看上面兩張圖片大小差不多326656 , 326700)
以READ前綴開始的,表示從緩存中獲取了一次該key對應的緩存數據
以REMOVE前綴開始的,表示刪除了該key對應的緩存數據
接著往下看,
第一次點擊load button去網絡上加載圖片:
public void click(View v){
Log.d(TAG,"click");
switch (v.getId()){
case R.id.button:
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(IMAGE_URL);
break;
case R.id.delete_btn:
try {
mDiskLruCache.remove(String.valueOf(IMAGE_URL.hashCode()));
} catch (IOException e) {
e.printStackTrace();
}
break;
default:
break;
}
}
網絡請求操作放在子線程去做:
class BitmapWorkerTask extends AsyncTask {
private final WeakReference imageViewReference;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(String... params) {
Log.d(TAG,"path="+params[0]);
String key = String.valueOf(params[0].hashCode());
// Check disk cache in background thread
Bitmap bitmap = getBitmapFromDiskCache(key);
if (bitmap == null) {
Log.d(TAG,"Not found in disk cache");
//去網絡下載
bitmap = downLoadImage(params[0]);
if(bitmap != null){
Log.d(TAG,"downLoadImage success");
/**
* 將改圖片存入disk cache
*/
if(addBitmapToCache(key,bitmap)){
Log.d(TAG,"add to disk cache success");
}else{
Log.d(TAG,"add to disk cache failure");
}
return bitmap;
}
Log.d(TAG,"downLoadImage failure");
return null;
}
Log.d(TAG,"found in disk cache");
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
Log.d(TAG,"onPostExecute bitmap="+bitmap);
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
下載之前首先去看緩存裡面有木有:
/**
*
* @param key
* @return
*/
public Bitmap getBitmapFromDiskCache(String key) {
Log.d(TAG,"getBitmapFromDiskCache");
synchronized (mDiskCacheLock) {
// Wait while disk cache is started from background thread
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
Log.d(TAG,"getBitmapFromDiskCache snapshot="+snapshot);
// if(snapshot != null){
// InputStream inputStream = snapshot.getInputStream(0);
// Log.d(TAG,"getBitmapFromDiskCache inputStream="+inputStream);
// }
return snapshot != null ? decodeStream(snapshot.getInputStream(0)):null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
獲取緩存的圖片,與內存緩存相比過程稍微復雜點,中間使用了DiskLruCache.Snapshot對象,然後Snapshot獲取不是圖片,而是輸入流,所以還需解碼成圖片。參數傳入0,與之前一個key對應一個值,是對應的。
第一次請求圖片,緩存沒有,所以要去下載:
/**
*
* @param path
* @return
*/
private Bitmap downLoadImage(String path){
Log.d(TAG,"downLoadImage");
HttpURLConnection conn = null;
try {
conn = createConnection(path);
int redirectCount = 0;//重定向的次數
//>=300重定向相關的
while (conn.getResponseCode() / 100 == 3 && redirectCount < MAX_REDIRECT_COUNT) {
conn = createConnection(conn.getHeaderField("Location"));
redirectCount++;
}
return decodeStream(conn.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}finally {
if (conn != null) {
conn.disconnect();
}
}
return null;
}
/**
*
* @param url
* @return
* @throws IOException
*/
protected HttpURLConnection createConnection(String url) throws IOException {
String encode = Uri.encode(url, ALLOWED_URI_CHARS);//
Log.d(TAG,"encodeURL=="+encode);
URL u = new URL(encode);
HttpURLConnection conn = (HttpURLConnection) u.openConnection();
conn.setConnectTimeout(DEFAULT_HTTP_CONNECT_TIMEOUT);//設置連接主機超時(單位:毫秒)
conn.setReadTimeout(DEFAULT_HTTP_READ_TIMEOUT);//設置從主機讀取數據超時(單位:毫秒)
return conn;
}
這裡就是簡單使用HttpUrlConnection去下載了,這裡添加有關重定向的處理,這裡下載圖片就不多解釋了
下載完圖片後,需要將圖片存入disk
/**
*
* @param key
* @param bitmap
*/
public boolean addBitmapToCache(String key, Bitmap bitmap) {
Log.d(TAG,"addInputStreamToCache");
DiskLruCache.Editor editor = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
synchronized (mDiskCacheLock) {
try {
InputStream inputStream = Bitmap2IS(bitmap);
editor = mDiskLruCache.edit(key);
OutputStream outputStream = editor.newOutputStream(0);
in = new BufferedInputStream(inputStream,1024);
out = new BufferedOutputStream(outputStream,1024);
int b;
while ((b = in.read())!=-1){
out.write(b);
}
editor.commit();
mDiskLruCache.flush();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
}
}
DiskLruCache存數據是使用DiskLruCache.Editor,比起內存緩存還是稍微復雜點,Editor記得要執行commit()函數
處理的步驟就這些,
下面是全部代碼:
package cj.com.disklrucache;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.URL;
import static android.os.Environment.isExternalStorageRemovable;
public class MainActivity extends AppCompatActivity {
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "disccache";
private static final String TAG = "disklrucache";
private static final String IMAGE_URL = "http://inthecheesefactory.com/uploads/source/glidepicasso/cover.jpg";
private static final int MAX_REDIRECT_COUNT = 5;
private static final String ALLOWED_URI_CHARS = "@#&=*+-_.,:!?()/~'%";
private static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 5 * 1000; // milliseconds
private static final int DEFAULT_HTTP_READ_TIMEOUT = 20 * 1000; // milliseconds
private final Object mDiskCacheLock = new Object();
private DiskLruCache mDiskLruCache;
private boolean mDiskCacheStarting = true;
private ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = (ImageView) findViewById(R.id.imageView);
// Initialize disk cache on background thread
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
Log.d(TAG,"diskcacheDir="+cacheDir.getAbsolutePath());
new InitDiskCacheTask().execute(cacheDir);
}
public void click(View v){
Log.d(TAG,"click");
switch (v.getId()){
case R.id.button:
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(IMAGE_URL);
break;
case R.id.delete_btn:
try {
mDiskLruCache.remove(String.valueOf(IMAGE_URL.hashCode()));
} catch (IOException e) {
e.printStackTrace();
}
break;
default:
break;
}
}
/**
*
* @param path
* @return
*/
private Bitmap downLoadImage(String path){
Log.d(TAG,"downLoadImage");
HttpURLConnection conn = null;
try {
conn = createConnection(path);
int redirectCount = 0;//重定向的次數
//>=300重定向相關的
while (conn.getResponseCode() / 100 == 3 && redirectCount < MAX_REDIRECT_COUNT) {
conn = createConnection(conn.getHeaderField("Location"));
redirectCount++;
}
return decodeStream(conn.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}finally {
if (conn != null) {
conn.disconnect();
}
}
return null;
}
/**
*
* @param bm
* @return
*/
private InputStream Bitmap2IS(Bitmap bm){
Log.d(TAG,"Bitmap2IS");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bm.compress(Bitmap.CompressFormat.JPEG, 100, baos);
InputStream sbs = new ByteArrayInputStream(baos.toByteArray());
return sbs;
}
/**
*
* @param url
* @return
* @throws IOException
*/
protected HttpURLConnection createConnection(String url) throws IOException {
String encode = Uri.encode(url, ALLOWED_URI_CHARS);//
Log.d(TAG,"encodeURL=="+encode);
URL u = new URL(encode);
HttpURLConnection conn = (HttpURLConnection) u.openConnection();
conn.setConnectTimeout(DEFAULT_HTTP_CONNECT_TIMEOUT);//設置連接主機超時(單位:毫秒)
conn.setReadTimeout(DEFAULT_HTTP_READ_TIMEOUT);//設置從主機讀取數據超時(單位:毫秒)
return conn;
}
/**
*
* @param key
* @param bitmap
*/
public boolean addBitmapToCache(String key, Bitmap bitmap) {
Log.d(TAG,"addInputStreamToCache");
DiskLruCache.Editor editor = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
synchronized (mDiskCacheLock) {
try {
InputStream inputStream = Bitmap2IS(bitmap);
editor = mDiskLruCache.edit(key);
OutputStream outputStream = editor.newOutputStream(0);
in = new BufferedInputStream(inputStream,1024);
out = new BufferedOutputStream(outputStream,1024);
int b;
while ((b = in.read())!=-1){
out.write(b);
}
editor.commit();
mDiskLruCache.flush();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
}
}
/**
*
* @param key
* @return
*/
public Bitmap getBitmapFromDiskCache(String key) {
Log.d(TAG,"getBitmapFromDiskCache");
synchronized (mDiskCacheLock) {
// Wait while disk cache is started from background thread
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
Log.d(TAG,"getBitmapFromDiskCache snapshot="+snapshot);
if(snapshot != null){
InputStream inputStream = snapshot.getInputStream(0);
Log.d(TAG,"getBitmapFromDiskCache inputStream="+inputStream);
}
return snapshot != null ? decodeStream(snapshot.getInputStream(0)):null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
/**
*
* @param inputStream
* @return
*/
private Bitmap decodeStream(InputStream inputStream){
return inputStream != null ? BitmapFactory.decodeStream(inputStream):null;
//BitmapFactory.decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts)
}
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
private File getDiskCacheDir(Context context, String uniqueName) {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir().getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
private int getAppVersion(){
try {
PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), 0);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
/**
* 初始化DiskLruCache
*/
class InitDiskCacheTask extends AsyncTask {
@Override
protected Void doInBackground(File... params) {
synchronized (mDiskCacheLock) {
File cacheDir = params[0];
try {
mDiskLruCache = DiskLruCache.open(cacheDir,getAppVersion(),1,DISK_CACHE_SIZE);
mDiskCacheStarting = false; // Finished initialization
mDiskCacheLock.notifyAll(); // Wake any waiting threads
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
/**
*
*/
class BitmapWorkerTask extends AsyncTask {
private final WeakReference imageViewReference;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(String... params) {
Log.d(TAG,"path="+params[0]);
String key = String.valueOf(params[0].hashCode());
// Check disk cache in background thread
Bitmap bitmap = getBitmapFromDiskCache(key);
if (bitmap == null) {
Log.d(TAG,"Not found in disk cache");
//去網絡下載
bitmap = downLoadImage(params[0]);
if(bitmap != null){
Log.d(TAG,"downLoadImage success");
/**
* 將改圖片存入disk cache
*/
if(addBitmapToCache(key,bitmap)){
Log.d(TAG,"add to disk cache success");
}else{
Log.d(TAG,"add to disk cache failure");
}
return bitmap;
}
Log.d(TAG,"downLoadImage failure");
return null;
}
Log.d(TAG,"found in disk cache");
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
Log.d(TAG,"onPostExecute bitmap="+bitmap);
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
}
看一下log:
D/disklrucache: click
D/disklrucache: path=http://inthecheesefactory.com/uploads/source/glidepicasso/cover.jpg
D/disklrucache: getBitmapFromDiskCache
D/disklrucache: getBitmapFromDiskCache snapshot=null
D/disklrucache: Not found in disk cache
D/disklrucache: downLoadImage
D/disklrucache: encodeURL==http://inthecheesefactory.com/uploads/source/glidepicasso/cover.jpg
D/disklrucache: encodeURL==https://inthecheesefactory.com/uploads/source/glidepicasso/cover.jpg
D/disklrucache: downLoadImage success
D/disklrucache: addInputStreamToCache
D/disklrucache: Bitmap2IS
D/disklrucache: add to disk cache success
D/disklrucache: onPostExecute bitmap=android.graphics.Bitmap@3396b564
D/disklrucache: click
D/disklrucache: path=http://inthecheesefactory.com/uploads/source/glidepicasso/cover.jpg
D/disklrucache: getBitmapFromDiskCache
D/disklrucache: getBitmapFromDiskCache snapshot=cj.com.disklrucache.DiskLruCache$Snapshot@1f9bd793
D/disklrucache: getBitmapFromDiskCache inputStream=java.io.FileInputStream@3fbbc3d0
D/disklrucache: found in disk cache
D/disklrucache: onPostExecute bitmap=android.graphics.Bitmap@1b7befc9
D/disklrucache: click
D/disklrucache: click
D/disklrucache: path=http://inthecheesefactory.com/uploads/source/glidepicasso/cover.jpg
D/disklrucache: getBitmapFromDiskCache
D/disklrucache: getBitmapFromDiskCache snapshot=null
D/disklrucache: Not found in disk cache
D/disklrucache: downLoadImage
D/disklrucache: encodeURL==http://inthecheesefactory.com/uploads/source/glidepicasso/cover.jpg
D/disklrucache: encodeURL==https://inthecheesefactory.com/uploads/source/glidepicasso/cover.jpg
D/disklrucache: downLoadImage success
D/disklrucache: addInputStreamToCache
D/disklrucache: Bitmap2IS
D/disklrucache: add to disk cache success
D/disklrucache: onPostExecute bitmap=android.graphics.Bitmap@354a86ce
Application terminated.
具體動作就是,先點擊load button 這時會去網上下載,再次點擊load button 就會去緩存裡去獲取,然後點擊delete button 刪除了緩存,最後再點load button 又會去網上下載。
日志記錄(上面那張圖記錄的)。
關於DiskLruCache還有一些其他API:
1.size()
這個方法會返回當前緩存路徑下所有緩存數據的總字節數,以byte為單位,如果應用程序中需要在界面上顯示當前緩存數據的總大小,就可以通過調用這個方法計算出來。
2.flush()
這個方法用於將內存中的操作記錄同步到日志文件(也就是journal文件)當中。這個方法非常重要,因為DiskLruCache能夠正常工作的前提就是要依賴於journal文件中的內容。並不是每次寫入緩存都要調用一次flush()方法的,頻繁地調用並不會帶來任何好處,只會額外增加同步journal文件的時間。比較標准的做法就是在Activity的onPause()方法中去調用一次flush()方法就可以了。
3.close()
這個方法用於將DiskLruCache關閉掉,是和open()方法對應的一個方法。關閉掉了之後就不能再調用DiskLruCache中任何操作緩存數據的方法,通常只應該在Activity的onDestroy()方法中去調用close()方法。
最近在做項目,小組幾個回了家。界面暫時沒人做,用到自定義對話框只能臨時去學。現在把對話框的相關整理。 +
這篇繼續解決上一篇遺留下來的問題:點擊條目顯示具體的知乎日報信息怎麼實現?很簡單,讓ContentActivity的recylerView響應點擊事件便可。先來看看我的代
好久沒有寫博客了,最近都在忙。有時候即使是有時間也會很懶,就會想玩一玩,放松放松!一直都沒有什麼時間更新我這個菜鳥的博客了。不過今天不一樣,我要給大家講講怎麼實現許多ap
最近項目中,有個需求就是要禁止ViewPager滑動事件,我們看下360手機助手的界面,風格就類似這樣的 大家如果使用過360手機助手就會