Android App升級非常好用的工具類(應用內升級),及相關可能踩到的坑
App升級一般有兩種方式: 第一種,是在App內部升級,自己寫下載程式碼,一種是調到第三方瀏覽器中,讓瀏覽器下載本應用升級(之前文章有講過,連結地址:https://blog.csdn.net/wolfking0608/article/details/79619472)
下面重點介紹應用內部升級
升級工具類如下:
public class UpdateService extends Service { public static final String TAG = "UpdateService"; public static final String ACTION = "me.shenfan.UPDATE_APP"; public static final String STATUS = "status"; public static final String PROGRESS = "progress"; public static boolean DEBUG = true; //下載大小通知頻率 public static final int UPDATE_NUMBER_SIZE = 1; public static final int DEFAULT_RES_ID = -1; public static final int UPDATE_PROGRESS_STATUS = 0; public static final int UPDATE_ERROR_STATUS = -1; public static final int UPDATE_SUCCESS_STATUS = 1; //params private static final String URL = "downloadUrl"; private static final String ICO_RES_ID = "icoResId"; private static final String ICO_SMALL_RES_ID = "icoSmallResId"; private static final String UPDATE_PROGRESS = "updateProgress"; private static final String STORE_DIR = "storeDir"; private static final String DOWNLOAD_NOTIFICATION_FLAG = "downloadNotificationFlag"; private static final String DOWNLOAD_SUCCESS_NOTIFICATION_FLAG = "downloadSuccessNotificationFlag"; private static final String DOWNLOAD_ERROR_NOTIFICATION_FLAG = "downloadErrorNotificationFlag"; private static final String IS_SEND_BROADCAST = "isSendBroadcast"; private String downloadUrl; private int icoResId; //default app ico private int icoSmallResId; private int updateProgress; //update notification progress when it add number private String storeDir; //default sdcard/Android/package/update private int downloadNotificationFlag; private int downloadSuccessNotificationFlag; private int downloadErrorNotificationFlag; private boolean isSendBroadcast; private UpdateProgressListener updateProgressListener; private LocalBinder localBinder = new LocalBinder(); /** * Class used for the client Binder. */ public class LocalBinder extends Binder { /** * set update progress call back * @param listener */ public void setUpdateProgressListener(UpdateProgressListener listener){ UpdateService.this.setUpdateProgressListener(listener); } } private boolean startDownload;//開始下載 private int lastProgressNumber; private NotificationCompat.Builder builder; private NotificationManager manager; private int notifyId; private String appName; private LocalBroadcastManager localBroadcastManager; private Intent localIntent; private DownloadApk downloadApkTask; /** * whether debug */ public static void debug(){ DEBUG = true; } private static Intent installIntent(String path){ Intent intent = new Intent(Intent.ACTION_VIEW); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //特別特別 注意這裡的包名,必須和你應用的包名保持一致,不然下載了也會安裝不了,程式閃退!!!
Uri contentUri = FileProvider.getUriForFile(BaseApplication.app, "com.wcyq.gangrong.FileProvider", new File(path)); intent.setDataAndType(contentUri, "application/vnd.android.package-archive"); }else { Uri uri = Uri.fromFile(new File(path)); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setDataAndType(uri, "application/vnd.android.package-archive"); } return intent; } private static Intent webLauncher(String downloadUrl){ Uri download = Uri.parse(downloadUrl); Intent intent = new Intent(Intent.ACTION_VIEW, download); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); return intent; } private static String getSaveFileName(String downloadUrl) { if (downloadUrl == null || TextUtils.isEmpty(downloadUrl)) { return "noName.apk"; } return downloadUrl.substring(downloadUrl.lastIndexOf("/")); } private static File getDownloadDir(UpdateService service){ File downloadDir = null; if (Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED)) { if (service.storeDir != null){ downloadDir = new File(Environment.getExternalStorageDirectory(), service.storeDir); }else { downloadDir = new File(service.getExternalCacheDir(), "update"); } } else { downloadDir = new File(service.getCacheDir(), "update"); } if (!downloadDir.exists()) { downloadDir.mkdirs(); } return downloadDir; } @Override public void onCreate() { super.onCreate(); appName = getApplicationName(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (!startDownload && intent != null){ startDownload = true; downloadUrl = intent.getStringExtra(URL); icoResId = intent.getIntExtra(ICO_RES_ID, DEFAULT_RES_ID); icoSmallResId = intent.getIntExtra(ICO_SMALL_RES_ID, DEFAULT_RES_ID); storeDir = intent.getStringExtra(STORE_DIR); updateProgress = intent.getIntExtra(UPDATE_PROGRESS, UPDATE_NUMBER_SIZE); downloadNotificationFlag = intent.getIntExtra(DOWNLOAD_NOTIFICATION_FLAG, 0); downloadErrorNotificationFlag = intent.getIntExtra(DOWNLOAD_ERROR_NOTIFICATION_FLAG, 0); downloadSuccessNotificationFlag = intent.getIntExtra(DOWNLOAD_SUCCESS_NOTIFICATION_FLAG, 0); isSendBroadcast = intent.getBooleanExtra(IS_SEND_BROADCAST, false); if (DEBUG){ Log.d(TAG, "downloadUrl: " + downloadUrl); Log.d(TAG, "icoResId: " + icoResId); Log.d(TAG, "icoSmallResId: " + icoSmallResId); Log.d(TAG, "storeDir: " + storeDir); Log.d(TAG, "updateProgress: " + updateProgress); Log.d(TAG, "downloadNotificationFlag: " + downloadNotificationFlag); Log.d(TAG, "downloadErrorNotificationFlag: " + downloadErrorNotificationFlag); Log.d(TAG, "downloadSuccessNotificationFlag: " + downloadSuccessNotificationFlag); Log.d(TAG, "isSendBroadcast: " + isSendBroadcast); } notifyId = startId; buildNotification(); buildBroadcast(); downloadApkTask = new DownloadApk(this); downloadApkTask.execute(downloadUrl); } return super.onStartCommand(intent, flags, startId); } @Nullable @Override public IBinder onBind(Intent intent) { return localBinder; } @Override public boolean onUnbind(Intent intent) { return true; } public void setUpdateProgressListener(UpdateProgressListener updateProgressListener) { this.updateProgressListener = updateProgressListener; } @Override public void onDestroy() { if (downloadApkTask != null){ downloadApkTask.cancel(true); } if (updateProgressListener != null){ updateProgressListener = null; } localIntent = null; builder = null; super.onDestroy(); } public String getApplicationName() { PackageManager packageManager = null; ApplicationInfo applicationInfo = null; try { packageManager = getApplicationContext().getPackageManager(); applicationInfo = packageManager.getApplicationInfo(getPackageName(), 0); } catch (PackageManager.NameNotFoundException e) { applicationInfo = null; } String applicationName = (String) packageManager.getApplicationLabel(applicationInfo); return applicationName; } private void buildBroadcast(){ if (!isSendBroadcast){ return; } localBroadcastManager = LocalBroadcastManager.getInstance(this); localIntent = new Intent(ACTION); } private void sendLocalBroadcast(int status, int progress){ if (!isSendBroadcast || localIntent == null){ return; } localIntent.putExtra(STATUS, status); localIntent.putExtra(PROGRESS, progress); localBroadcastManager.sendBroadcast(localIntent); } private void buildNotification(){ manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); builder = new NotificationCompat.Builder(this); builder.setContentTitle(getString(R.string.update_app_model_prepare, appName)) .setWhen(System.currentTimeMillis()) .setProgress(100, 1, false) .setSmallIcon(icoSmallResId) .setLargeIcon(BitmapFactory.decodeResource( getResources(), icoResId)) .setDefaults(downloadNotificationFlag); manager.notify(notifyId, builder.build()); } private void start(){ builder.setContentTitle(appName); builder.setContentText(getString(R.string.update_app_model_prepare, 1)); manager.notify(notifyId, builder.build()); sendLocalBroadcast(UPDATE_PROGRESS_STATUS, 1); if (updateProgressListener != null){ updateProgressListener.start(); } } /** * * @param progress download percent , max 100 */ private void update(int progress){ if (progress - lastProgressNumber > updateProgress){ lastProgressNumber = progress; builder.setProgress(100, progress, false); builder.setContentText(getString(R.string.update_app_model_progress, progress, "%")); manager.notify(notifyId, builder.build()); sendLocalBroadcast(UPDATE_PROGRESS_STATUS, progress); if (updateProgressListener != null){ updateProgressListener.update(progress); } } } private void success(String path) { builder.setProgress(0, 0, false); builder.setContentText(getString(R.string.update_app_model_success)); Intent i = installIntent(path); PendingIntent intent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(intent); builder.setDefaults(downloadSuccessNotificationFlag); Notification n = builder.build(); n.contentIntent = intent; manager.notify(notifyId, n); sendLocalBroadcast(UPDATE_SUCCESS_STATUS, 100); if (updateProgressListener != null){ updateProgressListener.success(); } startActivity(i); stopSelf(); } private void error(){ Intent i = webLauncher(downloadUrl); PendingIntent intent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentText(getString(R.string.update_app_model_error)); builder.setContentIntent(intent); builder.setProgress(0, 0, false); builder.setDefaults(downloadErrorNotificationFlag); Notification n = builder.build(); n.contentIntent = intent; manager.notify(notifyId, n); sendLocalBroadcast(UPDATE_ERROR_STATUS, -1); if (updateProgressListener != null){ updateProgressListener.error(); } stopSelf(); } private static class DownloadApk extends AsyncTask<String, Integer, String> { private WeakReference<UpdateService> updateServiceWeakReference; public DownloadApk(UpdateService service){ updateServiceWeakReference = new WeakReference<>(service); } @Override protected void onPreExecute() { super.onPreExecute(); UpdateService service = updateServiceWeakReference.get(); if (service != null){ service.start(); } } @Override protected String doInBackground(String... params) { final String downloadUrl = params[0]; final File file = new File(UpdateService.getDownloadDir(updateServiceWeakReference.get()), UpdateService.getSaveFileName(downloadUrl)); if (DEBUG){ Log.d(TAG, "download url is " + downloadUrl); Log.d(TAG, "download apk cache at " + file.getAbsolutePath()); } File dir = file.getParentFile(); if (!dir.exists()){ dir.mkdirs(); } HttpURLConnection httpConnection = null; InputStream is = null; FileOutputStream fos = null; int updateTotalSize = 0; java.net.URL url; try { url = new URL(downloadUrl); httpConnection = (HttpURLConnection) url.openConnection(); httpConnection.setConnectTimeout(20000); httpConnection.setReadTimeout(20000); if (DEBUG){ Log.d(TAG, "download status code: " + httpConnection.getResponseCode()); } if (httpConnection.getResponseCode() != 200) { return null; } updateTotalSize = httpConnection.getContentLength(); if (file.exists()) { if (updateTotalSize == file.length()) { // 下載完成 return file.getAbsolutePath(); } else { file.delete(); } } file.createNewFile(); is = httpConnection.getInputStream(); fos = new FileOutputStream(file, false); byte buffer[] = new byte[4096]; int readSize = 0; int currentSize = 0; while ((readSize = is.read(buffer)) > 0) { fos.write(buffer, 0, readSize); currentSize += readSize; publishProgress((currentSize * 100 / updateTotalSize)); } // download success } catch (Exception e) { e.printStackTrace(); return null; } finally { if (httpConnection != null) { httpConnection.disconnect(); } if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } return file.getAbsolutePath(); } @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); if (DEBUG){ Log.d(TAG, "current progress is " + values[0]); } UpdateService service = updateServiceWeakReference.get(); if (service != null){ service.update(values[0]); } } @Override protected void onPostExecute(String s) { super.onPostExecute(s); UpdateService service = updateServiceWeakReference.get(); if (service != null){ if (s != null){ service.success(s); }else { service.error(); } } } } /** * a builder class helper use UpdateService */ public static class Builder{ private String downloadUrl; private int icoResId = DEFAULT_RES_ID; //default app ico private int icoSmallResId = DEFAULT_RES_ID; private int updateProgress = UPDATE_NUMBER_SIZE; //update notification progress when it add number private String storeDir; //default sdcard/Android/package/update private int downloadNotificationFlag; private int downloadSuccessNotificationFlag; private int downloadErrorNotificationFlag; private boolean isSendBroadcast; protected Builder(String downloadUrl){ this.downloadUrl = downloadUrl; } public static Builder create(String downloadUrl){ if (downloadUrl == null) { throw new NullPointerException("downloadUrl == null"); } return new Builder(downloadUrl); } public String getDownloadUrl() { return downloadUrl; } public int getIcoResId() { return icoResId; } public Builder setIcoResId(int icoResId) { this.icoResId = icoResId; return this; } public int getIcoSmallResId() { return icoSmallResId; } public Builder setIcoSmallResId(int icoSmallResId) { this.icoSmallResId = icoSmallResId; return this; } public int getUpdateProgress() { return updateProgress; } public Builder setUpdateProgress(int updateProgress) { if (updateProgress < 1){ throw new IllegalArgumentException("updateProgress < 1"); } this.updateProgress = updateProgress; return this; } public String getStoreDir() { return storeDir; } public Builder setStoreDir(String storeDir) {//資料夾名 GangGang this.storeDir = storeDir; return this; } public int getDownloadNotificationFlag() { return downloadNotificationFlag; } public Builder setDownloadNotificationFlag(int downloadNotificationFlag) { this.downloadNotificationFlag = downloadNotificationFlag; return this; } public int getDownloadSuccessNotificationFlag() { return downloadSuccessNotificationFlag; } public Builder setDownloadSuccessNotificationFlag(int downloadSuccessNotificationFlag) {//標記等於-1 this.downloadSuccessNotificationFlag = downloadSuccessNotificationFlag; return this; } public int getDownloadErrorNotificationFlag() { return downloadErrorNotificationFlag; } public Builder setDownloadErrorNotificationFlag(int downloadErrorNotificationFlag) {//標記-1 this.downloadErrorNotificationFlag = downloadErrorNotificationFlag; return this; } public boolean isSendBroadcast() { return isSendBroadcast; } public Builder setIsSendBroadcast(boolean isSendBroadcast) { this.isSendBroadcast = isSendBroadcast; return this; } public Builder build(Context context){ if (context == null){ throw new NullPointerException("context == null"); } Intent intent = new Intent();//Intent { cmp=com.wcyq.gangrong/.utils.UpdateService } intent.setClass(context, UpdateService.class); intent.putExtra(URL, downloadUrl); if (icoResId == DEFAULT_RES_ID){//進來了 icoResId = getIcon(context); } if (icoSmallResId == DEFAULT_RES_ID){//進來了 icoSmallResId = icoResId; } intent.putExtra(ICO_RES_ID, icoResId); intent.putExtra(STORE_DIR, storeDir); intent.putExtra(ICO_SMALL_RES_ID, icoSmallResId); intent.putExtra(UPDATE_PROGRESS, updateProgress); intent.putExtra(DOWNLOAD_NOTIFICATION_FLAG, downloadNotificationFlag); intent.putExtra(DOWNLOAD_SUCCESS_NOTIFICATION_FLAG, downloadSuccessNotificationFlag); intent.putExtra(DOWNLOAD_ERROR_NOTIFICATION_FLAG, downloadErrorNotificationFlag); intent.putExtra(IS_SEND_BROADCAST, isSendBroadcast); context.startService(intent); return this; } private int getIcon(Context context){ final PackageManager packageManager = context.getPackageManager(); ApplicationInfo appInfo = null; try { appInfo = packageManager.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } if (appInfo != null){ return appInfo.icon; } return 0; } }
使用方法:
private void requestAppVersion() { RequestParams params = new RequestParams(); addRequestHeader(params); BaseApplication.getInstance().httpRequest.xPostjson(mContext, params, Constant.BASE_HTTP + ContantUrl.getVersionUpDate, new RequestResultJsonCallBack() { @Override public void onSucess(String result) { Logger.e(TAG, "requestAppVersion-------" + result); NewBaseBean info = Constant.getPerson(result, NewBaseBean.class); if (info.getCode() == Constant.RETURN_SUCCESS__STATE_CODE) { UpdataAppBean bean = Constant.getPerson(result, UpdataAppBean.class); List<UpdataAppBean.DataBean.ListBean> list = bean.getData().getList(); if (list != null && list.size() > 0) { listBean = list.get(0); String name = listBean.getName(); if (!name.equals(ContantUrl.SERVER_VERSION_NAME)) { final String url = listBean.getUrl(); String text = mContext.getResources().getString(R.string.check_upgrade); UpgradeDialog upgrade = new UpgradeDialog(MainActivity.this, text, new UpgradeDialog.OnClickconfirmListener() { @Override public void confirm() { if (!checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) && !checkPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_PERMISSION_MAIN); return; } UpdateService.Builder.create(url).setStoreDir(ContantUrl.AppFile).setDownloadSuccessNotificationFlag(Notification.DEFAULT_ALL).setDownloadErrorNotificationFlag(Notification.DEFAULT_ALL).build(mContext); Toast.makeText(mContext, "正在後臺下載", Toast.LENGTH_LONG).show(); //通過瀏覽器去下載APK // InstallUtils.installAPKWithBrower(mContext, url); } }); upgrade.show(); upgrade.setDetail(listBean.getDescription()); } } } else { Logger.e(TAG, info.getMessage()); } } @Override public void onFailure(int errorCode, String errorMsg) { showErrorLogger(TAG, errorCode, errorMsg); } }); } public boolean checkPermission(@NonNull String permission) { return ActivityCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED; } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_PERMISSION_MAIN) {//許可權走的是這裡 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { UpdateService.Builder.create(listBean.getUrl()).setStoreDir(ContantUrl.AppFile).setDownloadSuccessNotificationFlag(Notification.DEFAULT_ALL).setDownloadErrorNotificationFlag(Notification.DEFAULT_ALL).build(mContext); Toast.makeText(mContext, "正在後臺下載", Toast.LENGTH_LONG).show(); //刪除apk檔案(獲取許可權之後) FileUtils.deleteFile(new File(ContantUrl.absolutePath + File.separator + ContantUrl.AppFile + "/GangGang_release-1.0.apk")); } else { ToastUtil.show(mContext, "許可權被禁止,無法下載檔案"); } } }
為了大家檢視方便,沒有使用分層處理. UpgradeDialog升級app內容的彈框,很好寫,可以自己寫一個,工具類可以照抄,但是邏輯程式碼僅給各位參考.
特別注意:
1. 這裡適配了Android6.0以上的許可權, 在清單檔案中要註冊一個內容提供者,否者你的許可權申請不下來.
程式碼如下:rc_file_path.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_files" path="."/>
</paths>
清單檔案中要註冊
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/rc_file_path"/>
</provider>
2. 可能你很久沒有用服務了,服務也是需要註冊的,否者你發現許可權成功了,提示正在下載,就是通知欄裡面就是沒有下載的進度條,你可能懷疑是不是樓上這個教你的傢伙,傻逼了? 其實是你自己沒有註冊服務,自己犯二了.
3. App下載進度條出來了,但是安裝到快100%的時候,媽的,程式閃退了 就是安裝不了
請你在複製的時候,檢視一下,上面的工具類, 有一行我標了紅色了 ,你程式的包名一定要填寫上去!!!.因為你複製的是我Demo的包名.FileProvider.所以下載的時候發現不一樣,能給你安裝成功就他媽見鬼了.
4. 你下載App成功了,也進入下載頁面了,麻痺的就是安裝apk安裝不上去,這是什麼鬼? 檢查了程式碼也沒有錯啊,自己發現自己找了半天原因還是沒有找出來問題來? 怎麼辦?算了,我還是告訴你吧.你檢查下,你的app和簽名和你手機你的app的簽名是否一致,兩者的包名是否一致,如果不一致,是不會覆蓋安裝的,這個與你的版本沒毛關係.所以你安裝不上去
5.如果上面的4點你都做到了,程式碼也檢查了,還是出現問題,那隻能說明你人品太差了,自己好好檢查和分析原因debug吧! 以上遇到的坑希望能夠幫到你,如果能夠幫到你,並節約你開發的時間,請給我點個贊,follow我一下,謝謝!
相關推薦
Android App升級非常好用的工具類(應用內升級),及相關可能踩到的坑
App升級一般有兩種方式: 第一種,是在App內部升級,自己寫下載程式碼,一種是調到第三方瀏覽器中,讓瀏覽器下載本應用升級(之前文章有講過,連結地址:https://blog.csdn.net/wolfking0608/article/details/79619472)下面重
Android 獲取建立各種儲存路徑工具類(內建許可權獲取)
前言 最近工作比較輕,從專案中總結抽象出一些工具類,利人利己,歡迎交流完善~ 概念介紹 1、內部儲存 Internal Storage: 注意內部儲存不是記憶體。內部儲存位於系統中很特殊的一個位置,如果你想將檔案儲存於內部儲存中,那麼檔案預設
Android動態許可權申請工具類非常好用包含9組危險許可權
先看下動態許可權的工具類:package com.xiayiye.yhsh.permissionsdemo; import android.Manifest; import android.app.Activity; import android.app.AlertDia
更改沉浸式狀態列非常好用的一個工具類 StatusBarUtil
1.設定狀態列顏色 StatusBarUtil.setColor(Activity activity, int color) 2.設定狀態列半透明 StatusBarUtil.setTranslucent(Activity activity, int statusBarAlpha)
Android探索之旅(第十篇) 推薦幾款非常好用的Bug除錯工具
首推 騰訊Bugly - 一種愉悅的開發方式是一款非常方便幫組開發者實時的檢測App的異常及應用統計,還有更加強大的應用更新及熱修復,讓你的App 6飛起 官網地址:https://bugly.qq
PHP非常好用的分頁類
onf pan url pre ++ reg fig cti wal 分頁類: <?php /* * ********************************************* * @類名: page * @參數: $myde_tota
非常好用的CSS clip-path polygon工具
win .com blog cli polygon .cn image too mage http://betravis.github.io/shape-tools/polygon-drawing/ 非常好用的CSS clip-path polygon工具
pytorch:一個非常好用的工具檔案
在pytorch中去寫訓練函式和測試函式是一件重複的事,因此可以寫成一個總的訓練檔案。 from datetime import datetime import torch import torch.nn.functional as F from torch import nn from
非常好用的android特效
各種幫助類彙總:https://github.com/Blankj/AndroidUtilCode 常用的 iOS 風格 dialog 和 meterial design 風格的 dialog:https://github.com/glassLake/Dialo
dimens-Android非常簡單非常好用的螢幕適配
為什麼要進行Android螢幕適配? 關於為什麼要進行Android螢幕適配,什麼是dp、dpi這些概念我就不去一一講解了,網上很多文章。這裡我推薦幾篇講的比較好的: Android螢幕適配全攻略(最權威的官方適配指導) Android 螢幕適配:最全面的解決方案 Andr
python中非常好用的資料庫管理工具dataset
dataset對於操作JSON檔案、NoSQL非常好用。 官方文件:http://dataset.readthedocs.io/en/latest/ 補充: 連線mysql資料庫: db = dataset.connect('mysql://username:[ema
讓我們開發一個非常好用的捲簾工具
版權宣告:未經作者允許不得轉載,此外掛不得用於商業用途。 目錄 開發環境 外掛開發 __init__.py map_swipe_plugin.py map_swipe_tool.py active deactivate canvasPressEvent can
非常好用 世界上最快最好的視訊壓縮轉換工具(精品)
測試了20多款 各種視訊壓縮 擷取軟體 真的是這款最快 快好用 效果也好!!!最快的視訊轉換壓縮工具。WisMencoder 能夠把您的電腦上的所有視訊格式,包括avi,mpg,rmvb,wmv,mp4,mov,dat等格式以最快的速度和最高的質量轉換為AVI格式。速度和質量都
幾款 Windows 系統 非常好用的常用工具軟體
1、驅動更新 :Driver Boost Free Driver Boost是國外老牌優化軟體廠商iobit推出的驅動更新/備份工具,相比於國內的主流驅動更新軟體,Driver Boost不捆綁推廣軟體、無廣告,一鍵輕鬆更新所有驅動和常用的執行庫(DirectX、Visua
yum升級CURL到最新版本的方法,非常好用
首先,先為你的伺服器獲取最新匹配的源:http://mirror.city-fan.org/ftp/contrib/yum-repo/ # 安裝新版libcurl的yum源rpm -ivh http://mirror.city-fan.org/ftp/contrib/y
【非常好用的雙視窗管理工具】Fenetre Mac破解版
fenetre mac破解版是mac平臺上一款非常好用的雙視窗管理工具,可以幫助您在Mac桌面上的選單欄位置保留一個包含視訊,文件,照片或網站的視窗,讓您可以在工作時觀看Netflix,編碼時聽會議,邊看教程實時操作等等,非常強大! fenetre mac破解版軟體介紹
一個SQL SERVER查詢分析器非常好用的工具
使用效果: 1. 1.非常智慧的提示,使用起來其智慧、方便性,不比PL/SQL差; 2.功能非常強大,可見其“選項”面板; 3.適合SQL2000、SQL2005等。 如有需要的朋友請到:下載 安裝步驟: 1.解壓 2.雙擊 3.安
外掛,非常好用的請求REST的工具restclient,和非常好用的請求http的工具HttpRequester,
firefox外掛,非常棒,有圖有真相, restclient chrome://restclient/content/restclient.html HttpRequester 在firef
發現一個非常好用的扒站工具IDM
作為一個前端,看見一個好的網站會忍不住有想趴下全站來研究的衝動,今天就分享一個非常非常好用的扒站工具,免費使用的,無需費用:Internet Download Manager (IDM) 安裝好之後可以選擇中文版本,然後其他的都是傻瓜式操作。很簡單的。這個軟體有30天
非常好用的記憶體檢測工具
推薦理由:準確、快速大家比較熟悉的記憶體檢測工具有MemTest、微軟記憶體檢測(Microsoft Memory Diagnostic)等。但就本人的使用經歷來說,微軟的記憶體檢測工具更實用一些。下載地址:http://www.box.net/shared/03b7xbvk