转自:http://blog.csdn.net/donotwarry/article/details/6931307

前不久接到个任务,在我们的app里面添加更新模块,在之前的版本中,我们的更新都是直接通过浏览器下载apk包来安装更新的,我想各位很大一部分应用的更新方法都是这样,因为它简单、方便,但是他也有许多不好的地方,比如需要用户跳转到浏览器页面、下载不可控、网络不好的情况的下失败无法续传,退出浏览器就无法接着下了等。。

于是我们这个更新模块的需求就来了

1.下载后台进行,退出我们应用下载任务依旧能继续执行操作

2.下载文件支持断点续传

3.下载任务支持没有安装sdcard时也可下载更新

4.notify栏提示操作

对几个需求稍作分析,解决方法如下:

1.下载更新的线程放到一个service中去,service的好处是不易被系统回收,而且也容易操作。我们需要先在AndroidMainfest.xml文件中去注册这个service

[html]view plaincopy
  1. <service android:name="com.dj.app.UpdateService"/>

2.断点续传,请求头中有个重要的参数range,代表的意思的我要取的数据的范围,这个需要服务器支持,默认情况都是支持的。我的思路是下载前先获取文件包大小,然后检查是否已经有下载好的部分了,没有就从头开始,有就接着下。

[java]view plaincopy
  1. request.addHeader("Range", "bytes=" + downLength + "-");

为了支持断点续传,我将下载好的文件版本号、文件长度都以prefrence的形似保存下来,下次更新如果还是这个版本就接着下,如果又有更新的了就删掉重下。

[java]view plaincopy
  1. private void checkTemFile() {
  2. existTemFileVersionCode = preferences().getInt(
  3. UPDATE_FILE_VERSIONCODE, 0);
  4. if (newestVersionCode == existTemFileVersionCode
  5. || newestVersionCode == TEST) {
  6. File temFile = new File(context.getFilesDir(),
  7. Integer.valueOf(newestVersionCode) + ".apk");
  8. if (!temFile.exists()) {
  9. saveLogFile(newestVersionCode, 0);
  10. }
  11. } else {
  12. deleteApkFile(existTemFileVersionCode);
  13. saveLogFile(newestVersionCode, 0);
  14. }
  15. }

3.无sdcard时也能下载,那只能将apk包下载到系统内存中

[java]view plaincopy
  1. context.getFilesDir();

这样创建的文件在/data/data/应用包名/files

伪代码:

[java]view plaincopy
  1. if (downLength > 0) {//接着上次的下载
  2. outStream = context.openFileOutput(Integer.valueOf(newestVersionCode) + ".apk",
  3. Context.MODE_APPEND+ Context.MODE_WORLD_READABLE);
  4. } else {//从头开始下载
  5. outStream = context.openFileOutput(Integer.valueOf(newestVersionCode) + ".apk",Context.MODE_WORLD_READABLE);
  6. }

4.notify,我设定了一个更新notify的线程专门去观察下载线程的进度,每一分钟更新一次notify中的进度条。

[java]view plaincopy
  1. new Thread() {
  2. public void run() {
  3. try {
  4. boolean notFinish = true;
  5. while (notFinish) {
  6. Thread.sleep(1000);
  7. notFinish = false;
  8. if (downloadThread == null) {
  9. break;
  10. }
  11. if (!downloadThread.downFinish) {
  12. notFinish = true;
  13. }
  14. downloadThread.showNotification();
  15. }
  16. } catch (Exception e) {
  17. e.printStackTrace();
  18. } finally {
  19. stopSelf();
  20. }
  21. };
  22. }.start();

downloadThread就是我的下载线程了因为要对不同进度时有不同的控制,这个可以通过notification.contentIntent来进行设定,比如100%的时候,我想要用户点击通知栏,即可进行安装,则应该这样做

[java]view plaincopy
  1. notification.tickerText = "下载完成";
  2. notification.when = System.currentTimeMillis();
  3. Intent notificationIntent = new Intent();
  4. notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  5. notificationIntent
  6. .setAction(android.content.Intent.ACTION_VIEW);
  7. String type = "application/vnd.android.package-archive";
  8. notificationIntent.setDataAndType(
  9. Uri.parse("file:///data/data/"
  10. + context.getPackageName()
  11. + "/files/"
  12. + (Integer.valueOf(newestVersionCode)
  13. .toString()) + ".apk"), type);
  14. notification.contentView = rv;
  15. notification.flags = Notification.FLAG_AUTO_CANCEL;
  16. notification.contentView
  17. .setProgressBar(
  18. R.id.update_notification_progressbar, 100,
  19. p, false);
  20. notification.contentView.setTextViewText(
  21. R.id.update_notification_progresstext, p + "%");
  22. notification.contentView.setTextViewText(
  23. R.id.update_notification_title, "下载完成,点击安装");
  24. PendingIntent contentIntent = PendingIntent.getActivity(
  25. context, 0, notificationIntent, 0);
  26. notification.contentIntent = contentIntent;

下面我将下载线程完整的代码贴出来

[java]view plaincopy
  1. class DownloadThread extends Thread {
  2. private static final String UPDATE_FILE_VERSIONCODE = "updateTemFileVersionCode";
  3. private static final String UPDATE_FILE_LENGTH = "updateTemFileLength";
  4. private static final String TEST_UPDATE_FILE_LENGTH = "testupdatefilelength";
  5. private static final int BUFFER_SIZE = 1024;
  6. private static final int NETWORK_CONNECTION_TIMEOUT = 15000;
  7. private static final int NETWORK_SO_TIMEOUT = 15000;
  8. private int newestVersionCode, existTemFileVersionCode;
  9. private String downUrl;
  10. private int fileLength = Integer.MAX_VALUE;
  11. private int downLength;
  12. private boolean downFinish;
  13. private static final int CHECK_FAILED = -1;
  14. private static final int CHECK_SUCCESS = 0;
  15. private static final int CHECK_RUNNING = 1;
  16. private int checkStatus;
  17. private boolean isChecking = false;
  18. private Context context;
  19. private boolean isPercentZeroRunning = false;
  20. private boolean stop;
  21. private Object block = new Object();
  22. private boolean receiverRegistered = false;
  23. private NotificationManager mNM;
  24. private RemoteViews rv;
  25. public DownloadThread(Context context, String downUrl, int versionCode) {
  26. super("DownloadThread");
  27. this.downUrl = downUrl;
  28. this.newestVersionCode = versionCode;
  29. this.context = context;
  30. this.mNM = (NotificationManager) context
  31. .getSystemService(Context.NOTIFICATION_SERVICE);
  32. this.rv = new RemoteViews(context.getPackageName(), R.layout.notify);
  33. this.downFinish = false;
  34. }
  35. private void checkTemFile() {
  36. existTemFileVersionCode = preferences().getInt(
  37. UPDATE_FILE_VERSIONCODE, 0);
  38. if (newestVersionCode == existTemFileVersionCode
  39. || newestVersionCode == TEST) {
  40. File temFile = new File(context.getFilesDir(),
  41. Integer.valueOf(newestVersionCode) + ".apk");
  42. if (!temFile.exists()) {
  43. saveLogFile(newestVersionCode, 0);
  44. }
  45. } else {
  46. deleteApkFile(existTemFileVersionCode);
  47. saveLogFile(newestVersionCode, 0);
  48. }
  49. }
  50. private void deleteApkFile(int existVersionCode) {
  51. File temFile = new File(context.getFilesDir(),
  52. Integer.valueOf(existVersionCode) + ".apk");
  53. if (temFile.exists()) {
  54. temFile.delete();
  55. }
  56. }
  57. private SharedPreferences preferences() {
  58. return context.getSharedPreferences(context.getPackageName(),
  59. Context.MODE_WORLD_READABLE | Context.MODE_WORLD_WRITEABLE);
  60. }
  61. private void saveLogFile(int versionCode, int downloadLength) {
  62. SharedPreferences.Editor edit = preferences().edit();
  63. if (versionCode == TEST) {
  64. edit.putInt(TEST_UPDATE_FILE_LENGTH, downloadLength);
  65. } else {
  66. edit.putInt(UPDATE_FILE_VERSIONCODE, versionCode);
  67. edit.putInt(UPDATE_FILE_LENGTH, downloadLength);
  68. }
  69. edit.commit();
  70. }
  71. @Override
  72. public void run() {
  73. checkTemFile();
  74. this.stop = false;
  75. while (!downFinish) {
  76. Log.i(TAG, "download thread start : while()");
  77. if (newestVersionCode == TEST) {
  78. downLength = preferences().getInt(TEST_UPDATE_FILE_LENGTH,
  79. 0);
  80. } else {
  81. downLength = preferences().getInt(UPDATE_FILE_LENGTH, 0);
  82. }
  83. InputStream is = null;
  84. FileOutputStream outStream = null;
  85. try {
  86. // check the network
  87. ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
  88. NetworkInfo ni = cm.getActiveNetworkInfo();
  89. boolean con = ni == null ? false : ni
  90. .isConnectedOrConnecting();
  91. synchronized (block) {
  92. if (!con) {
  93. context.registerReceiver(
  94. receiver,
  95. new IntentFilter(
  96. ConnectivityManager.CONNECTIVITY_ACTION));
  97. receiverRegistered = true;
  98. try {
  99. Log.i(TAG, "network is not ok : block.wait()");
  100. block.wait();
  101. } catch (InterruptedException e1) {
  102. }
  103. }
  104. }
  105. if (fileLength == Integer.MAX_VALUE) {
  106. URL url = new URL(downUrl);
  107. HttpURLConnection conn = (HttpURLConnection) url
  108. .openConnection();
  109. if (conn.getResponseCode() / 100 == 2
  110. && conn.getContentLength() != -1) {
  111. fileLength = conn.getContentLength();
  112. Log.i(TAG, "getContentLength == " + fileLength);
  113. } else {
  114. Thread.sleep(5000);
  115. Log.i(TAG, "getContentLength failed : retry in 5 second later");
  116. continue;
  117. }
  118. }
  119. HttpClient httpClient = new DefaultHttpClient();
  120. HttpGet request = new HttpGet(downUrl);
  121. request.addHeader("Range", "bytes=" + downLength + "-");
  122. if (downLength < fileLength) {
  123. HttpHost proxy = HttpBase.globalProxy();
  124. HttpParams httpParams = request.getParams();
  125. HttpConnectionParams.setConnectionTimeout(httpParams,
  126. NETWORK_CONNECTION_TIMEOUT);
  127. HttpConnectionParams.setSoTimeout(httpParams,
  128. NETWORK_SO_TIMEOUT);
  129. ConnRouteParams.setDefaultProxy(request.getParams(),
  130. proxy);
  131. HttpResponse response = httpClient.execute(request);
  132. Log.i(TAG, "getContent's response status == "
  133. + response.getStatusLine().getStatusCode());
  134. if (response.getStatusLine().getStatusCode() / 100 != 2) {
  135. continue;
  136. }
  137. HttpEntity entity = response.getEntity();
  138. is = entity.getContent();
  139. byte[] buffer = new byte[BUFFER_SIZE];
  140. int offset = 0;
  141. if (downLength > 0) {
  142. outStream = context
  143. .openFileOutput(
  144. Integer.valueOf(newestVersionCode)
  145. + ".apk",
  146. Context.MODE_APPEND
  147. + Context.MODE_WORLD_READABLE);
  148. } else {
  149. outStream = context
  150. .openFileOutput(
  151. Integer.valueOf(newestVersionCode)
  152. + ".apk",
  153. Context.MODE_WORLD_READABLE);
  154. }
  155. while ((offset = is.read(buffer, 0, BUFFER_SIZE)) != -1
  156. && !stop) {
  157. outStream.write(buffer, 0, offset);
  158. downLength += offset;
  159. }
  160. }
  161. if (downLength == fileLength) {
  162. File apkFile = new File(context.getFilesDir(),
  163. Integer.valueOf(newestVersionCode) + ".apk");
  164. if (isApkFileOK(apkFile)) {
  165. checkStatus = CHECK_SUCCESS;
  166. } else {
  167. deleteApkFile(newestVersionCode);
  168. saveLogFile(existTemFileVersionCode, 0);
  169. checkStatus = CHECK_FAILED;
  170. }
  171. this.downFinish = true;
  172. }
  173. } catch (Exception e) {
  174. } finally {
  175. saveLogFile(newestVersionCode, downLength);
  176. if (receiverRegistered) {
  177. context.unregisterReceiver(receiver);
  178. receiverRegistered = false;
  179. }
  180. if (stop || downFinish) {
  181. break;
  182. }
  183. try {
  184. if (outStream != null) {
  185. outStream.close();
  186. }
  187. if (is != null) {
  188. is.close();
  189. }
  190. } catch (IOException e) {
  191. }
  192. }
  193. }
  194. }
  195. @Override
  196. public void interrupt() {
  197. synchronized (block) {
  198. Log.i(TAG, "block.notify()");
  199. block.notify();
  200. }
  201. }
  202. private void cancel() {
  203. stop = true;
  204. mNM.cancel(MOOD_NOTIFICATIONS);
  205. }
  206. private boolean isApkFileOK(File file) {
  207. checkStatus = CHECK_RUNNING;
  208. // first check the file header
  209. /*if (file.isDirectory() || !file.canRead() || file.length() < 4) {
  210. return false;
  211. }
  212. DataInputStream in = null;
  213. try {
  214. in = new DataInputStream(new BufferedInputStream(
  215. new FileInputStream(file)));
  216. int test = in.readInt();
  217. if (test != 0x504b0304)
  218. return false;
  219. } catch (IOException e) {
  220. return false;
  221. } finally {
  222. try {
  223. in.close();
  224. } catch (IOException e) {
  225. }
  226. }*/
  227. // second unZip file to check(without saving)
  228. boolean result = unzip(file);
  229. isChecking = false;
  230. return result;
  231. }
  232. private boolean unzip(File unZipFile) {
  233. boolean succeed = true;
  234. ZipInputStream zin = null;
  235. ZipEntry entry = null;
  236. try {
  237. zin = new ZipInputStream(new FileInputStream(unZipFile));
  238. boolean first = true;
  239. while (true) {
  240. if ((entry = zin.getNextEntry()) == null) {
  241. if (first)
  242. succeed = false;
  243. break;
  244. }
  245. first = false;
  246. if (entry.isDirectory()) {
  247. zin.closeEntry();
  248. continue;
  249. }
  250. if (!entry.isDirectory()) {
  251. byte[] b = new byte[1024];
  252. @SuppressWarnings("unused")
  253. int len = 0;
  254. while ((len = zin.read(b)) != -1) {
  255. }
  256. zin.closeEntry();
  257. }
  258. }
  259. } catch (IOException e) {
  260. succeed = false;
  261. } finally {
  262. if (null != zin) {
  263. try {
  264. zin.close();
  265. } catch (IOException e) {
  266. }
  267. }
  268. }
  269. return succeed;
  270. }
  271. public void showNotification() {
  272. float result = (float) downLength / (float) fileLength;
  273. int p = (int) (result * 100);
  274. if (p == 0 && isPercentZeroRunning || p == 100 && isChecking) {
  275. return;
  276. } else if (p != 0) {
  277. isPercentZeroRunning = false;
  278. }
  279. Notification notification = new Notification(R.drawable.icon, null,
  280. 0);
  281. if (p == 100) {
  282. if (checkStatus == CHECK_RUNNING) {
  283. notification.tickerText = "开始检查下载文件";
  284. notification.when = System.currentTimeMillis();
  285. notification.flags = notification.flags
  286. | Notification.FLAG_ONGOING_EVENT
  287. | Notification.FLAG_NO_CLEAR;
  288. notification.contentView = rv;
  289. notification.contentView.setProgressBar(
  290. R.id.update_notification_progressbar, 100, p, true);
  291. notification.contentView.setTextViewText(
  292. R.id.update_notification_title, "正在检查下载文件...");
  293. PendingIntent contentIntent = PendingIntent.getActivity(
  294. context, 0, null, 0);
  295. notification.contentIntent = contentIntent;
  296. isChecking = true;
  297. } else if (checkStatus == CHECK_FAILED) {
  298. notification.tickerText = "文件验证失败!";
  299. notification.when = System.currentTimeMillis();
  300. notification.flags = notification.flags
  301. | Notification.FLAG_AUTO_CANCEL;
  302. notification.contentView = rv;
  303. notification.contentView.setProgressBar(
  304. R.id.update_notification_progressbar, 100, p, false);
  305. notification.contentView.setTextViewText(
  306. R.id.update_notification_title, "文件验证失败,请重新下载");
  307. Intent notificationIntent = new Intent(context,
  308. UpdateService.class);
  309. notificationIntent.setAction(ACTION_STOP);
  310. PendingIntent contentIntent = PendingIntent.getService(
  311. context, 0, notificationIntent, 0);
  312. notification.contentIntent = contentIntent;
  313. } else {
  314. notification.tickerText = "下载完成";
  315. notification.when = System.currentTimeMillis();
  316. Intent notificationIntent = new Intent();
  317. notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  318. notificationIntent
  319. .setAction(android.content.Intent.ACTION_VIEW);
  320. String type = "application/vnd.android.package-archive";
  321. notificationIntent.setDataAndType(
  322. Uri.parse("file:///data/data/"
  323. + context.getPackageName()
  324. + "/files/"
  325. + (Integer.valueOf(newestVersionCode)
  326. .toString()) + ".apk"), type);
  327. notification.contentView = rv;
  328. notification.flags = Notification.FLAG_AUTO_CANCEL;
  329. notification.contentView
  330. .setProgressBar(
  331. R.id.update_notification_progressbar, 100,
  332. p, false);
  333. notification.contentView.setTextViewText(
  334. R.id.update_notification_progresstext, p + "%");
  335. notification.contentView.setTextViewText(
  336. R.id.update_notification_title, "下载完成,点击安装");
  337. PendingIntent contentIntent = PendingIntent.getActivity(
  338. context, 0, notificationIntent, 0);
  339. notification.contentIntent = contentIntent;
  340. }
  341. } else if (p == 0) {
  342. notification.tickerText = "准备下载";
  343. notification.when = System.currentTimeMillis();
  344. Intent notificationIntent = new Intent(context,
  345. UpdateService.class);
  346. notificationIntent.setAction(ACTION_STOP);
  347. notification.flags = notification.flags
  348. | Notification.FLAG_ONGOING_EVENT;
  349. notification.contentView = rv;
  350. notification.contentView.setProgressBar(
  351. R.id.update_notification_progressbar, 100, p, true);
  352. notification.contentView.setTextViewText(
  353. R.id.update_notification_title, "正在准备下载(点击取消)");
  354. PendingIntent contentIntent = PendingIntent.getService(context,
  355. 0, notificationIntent, 0);
  356. notification.contentIntent = contentIntent;
  357. isPercentZeroRunning = true;
  358. } else {
  359. notification.tickerText = "开始下载";
  360. notification.when = System.currentTimeMillis();
  361. Intent notificationIntent = new Intent(context,
  362. UpdateService.class);
  363. notificationIntent.setAction(ACTION_STOP);
  364. notification.contentView = rv;
  365. notification.flags = notification.flags
  366. | Notification.FLAG_ONGOING_EVENT;
  367. notification.contentView.setProgressBar(
  368. R.id.update_notification_progressbar, 100, p, false);
  369. notification.contentView.setTextViewText(
  370. R.id.update_notification_progresstext, p + "%");
  371. notification.contentView.setTextViewText(
  372. R.id.update_notification_title, "正在下载(点击取消)");
  373. PendingIntent contentIntent = PendingIntent.getService(context,
  374. 0, notificationIntent, 0);
  375. notification.contentIntent = contentIntent;
  376. }
  377. mNM.notify(MOOD_NOTIFICATIONS, notification);
  378. }
  379. }

可以看到,我的下载是包裹在一个while循环中的,假如没有下载完成,我会一直重复这个循环,可以注意到,我在取数据的时候有个标志位stop

[java]view plaincopy
  1. while ((offset = is.read(buffer, 0, BUFFER_SIZE)) != -1
  2. && !stop) {
  3. outStream.write(buffer, 0, offset);
  4. downLength += offset;
  5. }

有了这个就可以在外面控制强制停止下载。

在下载开始阶段我最先做的时就是检查网络情况,如果没有网络,我就使用一个block让这个线程阻塞掉,有人会问那什么时候恢复呢?我在service里面加了个广播

[java]view plaincopy
  1. private final BroadcastReceiver receiver = new BroadcastReceiver() {
  2. @Override
  3. public void onReceive(Context context, Intent intent) {
  4. if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent
  5. .getAction())) {
  6. NetworkInfo info = (NetworkInfo) intent
  7. .getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
  8. boolean hasConnectivity = (info != null && info.isConnected()) ? true
  9. : false;
  10. if (hasConnectivity && downloadThread != null) {
  11. downloadThread.interrupt();
  12. }
  13. }
  14. }
  15. };

可以监听系统网络情况,如果连上了,就调用interrupt()来唤醒线程。这样做的好处时,在没有网络时,这个线程不会无限循环的去获取数据。

最后在这个版本发布后因为代码一些bug导致,如果网络数据获取有问题,则用户下载下来的安装包就会解析错误,而且这是个死胡同,除非去手动下载个好的安装包来装,否则我们软件会一直提示,一直完成,但是一直装不上。。。哎,我们用户可是百万级啊,这个bug很致命,还好当时做了备用方案,可以由服务器控制客户端更新方法,于是改为以前的更新。

这也是我在上面的代码中下载完成时加入了最后一步,校验安装包的过程。我们都知道apk就是一个zip文件,通常对一个zip文件的校验,最简单的是校验文件头是不是0x504b0304,但是这只是文件格式的判断,加入是文件内容字节损坏还是查不出来,只能通过去unzip这个文件来捕获异常。在上面unzip(File unZipFile)方法中,我尝试去解压软件,对ZipInputStream流我没做任何处理,仅仅是看这个解压过程是否正常,以此判断这个zip文件是否正常,暂时也没想到更好的办法。

[java]view plaincopy
  1. private boolean unzip(File unZipFile) {
  2. boolean succeed = true;
  3. ZipInputStream zin = null;
  4. ZipEntry entry = null;
  5. try {
  6. zin = new ZipInputStream(new FileInputStream(unZipFile));
  7. boolean first = true;
  8. while (true) {
  9. if ((entry = zin.getNextEntry()) == null) {
  10. if (first)
  11. succeed = false;
  12. break;
  13. }
  14. first = false;
  15. if (entry.isDirectory()) {
  16. zin.closeEntry();
  17. continue;
  18. }
  19. if (!entry.isDirectory()) {
  20. byte[] b = new byte[1024];
  21. @SuppressWarnings("unused")
  22. int len = 0;
  23. while ((len = zin.read(b)) != -1) {
  24. }
  25. zin.closeEntry();
  26. }
  27. }
  28. } catch (IOException e) {
  29. succeed = false;
  30. } finally {
  31. if (null != zin) {
  32. try {
  33. zin.close();
  34. } catch (IOException e) {
  35. }
  36. }
  37. }
  38. return succeed;
  39. }

Android app更新模块相关推荐

  1. 编写Android app更新模块遇到的问题分析与总结

    前不久接到个任务,在我们的app里面添加更新模块,在之前的版本中,我们的更新都是直接通过浏览器下载apk包来安装更新的,我想各位很大一部分应用的更新方法都是这样,因为它简单.方便,但是他也有许多不好的 ...

  2. android app安装,Android App更新安装APK

    原标题:Android App更新安装APK 概要 一般地, Android App 都会被要求在App内进行软件更新提示, 让用户下载apk文件, 然后更新安装新版本, 一般过程如下: 检测是否有新 ...

  3. Android APP更新下载工具类——简单封装DownloadManager

    几乎所有APP都包含了检查更新功能,更新下载功能的实现方式常用的有两种:1.使用App网络框架的文件下载请求:2.使用自带的DownloadManager类:本文介绍第二种,简单封装一下Downloa ...

  4. Android app更新适配安卓10、11版本

    Android app内部更新适配安卓10.11版本 前言 ​ App内部更新现在基本每个app中都有,由于安卓各大应用市场不统一,不像Ios那样只有一个应用商城.并且现在安卓已经更新到11版本了,中 ...

  5. android软件更新模块实现的技术和方法,Android APK签名原理及方法

    一 Android签名机制及原理 Android系统在安装APK的时候,首先会检验APK的签名,如果发现签名文件不存在或者校验签名失败,则会拒绝安装,所以应用程序在发布之前一定要进行签名.给APK签名 ...

  6. android app更新弹窗,应用弹窗“此应用专为旧版Android打造,因此可能无法正常运行...”的原因...

    Android P上,有的应用打开时,会弹出对话框,内容:"此应用专为旧版Android打造,因此可能无法正常运行.请尝试检查更新或与开发者联系".用户会感到困惑,真正的原因是什么 ...

  7. android app 自动更新,AndroidUpdateDemo

    Android课程-App更新策略 @(Android) 第一节 课程介绍 概述 App更新是应用当中很常见的一个功能,基本上联网的app都应该具备这样的功能,对于更新迭代比较快速的产品,应用更新升级 ...

  8. android 强制更新流程图,AndroidUpdateDemo

    Android课程-App更新策略 @(Android) 第一节 课程介绍 概述 App更新是应用当中很常见的一个功能,基本上联网的app都应该具备这样的功能,对于更新迭代比较快速的产品,应用更新升级 ...

  9. Android增量更新框架

    Android增量更新框架 框架介绍 功能简介 简易效果图 增量更新配置 快速使用 Api详解 项目地址 框架介绍 功能简介 Android App更新框架,包含增量更新.多线程下载等功能.一句代码链 ...

最新文章

  1. python中的encode()和decode()函数_python里面的encode和decode函数
  2. 手机可以阅读html吗,手机文档html能删除吗
  3. python itertools模块实现排列组合
  4. SSRF(服务端请求伪造)
  5. [python opencv 计算机视觉零基础到实战] 十、图片效果毛玻璃
  6. POJ-3624 Charm Bracelet dp
  7. oracle 修改2个表,oracle学习笔记2:创建修改表
  8. 足不出户带你体验专业实验室,技术实现不在话下
  9. python几行代码识别验证码_Python有多强?文字识别(验证码识别)只需三行代码!...
  10. RMAN 目录管理维护
  11. w10自动删除文件怎么关了_回收站删除的文件怎么恢复?
  12. 正则表达式学习笔记002--星号的应用
  13. 浮动元素横排居中显示及浏览器兼容性处理
  14. mysql数据库用doc命令,myMySQL数据库怎么使用dos命令安装? MySQL数据库使用教程
  15. IDEA 导入cordova3.5工程目录注意事项
  16. win7系统蓝牙功能怎么开启
  17. GIMP 2.10.24 图片切片
  18. 行至青鸟 | 为学习保驾护航的“教学管理”
  19. 路由工作原理+DHCP+静态路由配置
  20. Python实现对比两个Excel数据内容并标出不同

热门文章

  1. linux+字体设置推荐,linux字体设置从入门到精通(入门级)
  2. H3CTE认证的说明
  3. lnmp 配置nginx 实现内网/本地域名
  4. matlab8灰色系统与新陈代谢预测
  5. tensorflow教程中的mnist数据下载脚本
  6. 国内外火控计算机发展水平,基于国产软硬件平台的火控计算机
  7. GeeksforGeeks 智力题
  8. 一个redis集群的管理工具
  9. 智能配送应用的简单介绍
  10. LPL比赛数据可视化,完成这个项目,用尽了我的所有Python知识