手把手系列————手机卫士(1)


手把手教你实现一款手机卫士。在今天的项目里面,你会实现一下几个小目标。

  1. 热门开源库的使用,xUtils3(下载文件),okHttp3(发送网络请求),Gson(处理Gson数据),permissionsdispatcher(动态权限的适配)的使用。
  2. handler的使用。
  3. 打包的流程
  4. 实现了一个本地端向服务器检查应用版本是否有更新并且下载,自动跳转安装的功能。兼容到8.0版本
  5. 以及综合考虑了用户的各种奇葩操作的相应的处理。

Splash页面功能

顾名思义,SplashActivity页面的含义就是启动时候的界面,一般来说, 用于广告位,或者检测应用是否在服务器上有更新,如果有的话就直接下载下来,并且安装。这样用户就可以获得最新版本的应用。接下来我们就要实现这个具体的功能。

效果展示

流程图

下面的流程图就很好的展示我们即将要做的事情。

添加用到的第三方库

1
2
3
4
5
6
compile 'com.squareup.okhttp3:okhttp:3.4.1'
compile 'com.google.code.gson:gson:2.7'
compile 'org.xutils:xutils:3.5.0'
compile('com.github.hotchemi:permissionsdispatcher:2.4.0')
annotationProcessor 'com.github.hotchemi:permissionsdispatcher-processor:2.4.0'

在这里需要值得一提的是permissionsdispatcher三方库的也就是最后两行代码的引入,并不是直接粘贴,而是通过插架加载的,ctrl+ shift+s 打开控制面板,输入Plugins,然后点击中间的按钮,输入permissionsdispatcher,就会看到这个插件了,点击安装即可(可能需要重启studio,我忘了)

1
2
//添加permissionsdispatcher依赖,重新构建一下项目就行了。
Code——>Generat....->addPermissionDispatcherdependence

这样的话,我们就添加完毕了。

activity_splash.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<TextView
android:id="@+id/version_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:shadowColor="#ff0"
android:shadowDx="1"
android:shadowDy="1"
android:shadowRadius="10"
android:text="版本名称"
android:textColor="#fff"
android:textSize="20sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/version_name"
android:id="@+id/app_name"
android:layout_centerHorizontal="true"
android:text="手机卫士"
android:textColor="#fff"
android:textSize="30sp"/>
<ProgressBar
style="?android:attr/progressDrawable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/version_name"
android:layout_centerHorizontal="true"
/>

布局使用的RelativeLayout,为什么代码要贴出来呢,是因为有一个非常重要的点,叫做版本名称的这个textview,必须放在最前面,因为在编译的时候,一旦@id/version_name这个值没有被先编译的话,就会报错。找不到该值。

android:shadowDx="1"    
android:shadowDy="1"
android:shadowRadius="10"

这三个属性决定了版本名称的阴影效果。

Activity去头操作&保留高版本主题

第一种方式

1
2
3
4
5
1
android.support.v7.app.ActionBar actionBar = getSupportActionBar();
if(actionBar!=null){
actionBar.hide();
}

得到getSupportActionBar的实例,然后进行判断,如果存在就隐藏。

第二种方式

1
2
3
4
5
6
7
8
9
10
11
<!--
在manifest清单中修改theme
去掉头的第一种方法
android:theme="@style/Theme.AppCompat.NoActionBar" 追踪进去看实现
<item name="windowNoTitle">true</item>
然后添加进我们自己的appTheme中
android:theme="@style/AppTheme"
在AppTheme里面去掉ActionBar又保留样式
<item name="windowNoTitle">true</item>
-->
android:theme="@style/AppTheme"

二者的区别

区别:

  • 在oncreate里面隐藏的话,下一个Activity里,同样存在
  • 在manifest中隐藏的话,所有的anctivity中都不可见,因为是在根节点隐藏

getVersionName()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
/**
* 获取版本名称
*
* @return 应用版本名称 返回null 代表有异常
*/
private String getVersionName() {
//1.得到包管理者对象PackageManager
PackageManager manager = getPackageManager();
try {
//2.传入app的包名和flag(值为0),得到当前包的基本信息
PackageInfo packageInfo = manager.getPackageInfo(this.getPackageName(), 0);
//返回包的versionName,即是Module:app里面的versionName
return packageInfo.versionName;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

注意返回值和传入的参数的含义

getVersionCode()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
3
/**
* 获取版本号
*
* @return 应用版本名称 返回0 代表有异常
*/
private int getVersionCode() {
//1.包管理者对象PackageManager
PackageManager manager = getPackageManager();
try {
PackageInfo packageInfo = manager.getPackageInfo(this.getPackageName(), 0);
return packageInfo.versionCode;
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}

注意返回值和传入的参数的含义。

initData()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
4
private void initData() {
//1.应用名称
String versionName = getVersionName();
//2.给textView赋值
tv_versionName.setText("版本名称:" + versionName);
//3.检测本地版本号和服务器版本号是否相同,不相同说明有新版本,后台更新
mVersionCode = getVersionCode();
// 4.获得服务端的版本号
// http://ogtmd8elu.bkt.clouddn.com/update_mobilesafe.json
// 包含更新版本的名称
// 新版本的描述信息
// 获得版本号
// 下载地址
// "versionName":"2.0",
// "versionCode":"2"
// "versionDes":"2.0版本发布了,狂拽酷炫吊炸天"
// "downloadUrl":"http://yanhui.site"(替换成下载地址)
checkVersion();
}

这里的话,我们就已经拿到了,本地包下的版本号和版本名字。接下来要分析的 checkVersion()就是splash代码的重点。

checkVersion();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
5
//进行耗时操作必须开启一个新的子线程,不然会造成ANR异常
final long startTime = System.currentTimeMillis(); //开始的时间戳
final Message message = Message.obtain(); //初始化message对象
String address = "http://ogtmd8elu.bkt.clouddn.com/MobilesafeUpdate.json";//版本更新的json数据
HttpUtils.sendOkHttpRequest(address, new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(new Runnable() {
@Override
public void run() {
//切换回主线程弹出toast
ToastUtils.showShort(SplashActivity.this, "更新数据失败,请检查网络");
}
});
message.what = ENTER_HOME;//赋值给message.what
try {
Thread.sleep(2000);//睡两秒再发送消息体验好很多
} catch (InterruptedException e1) {
e1.printStackTrace();
}
mHandler.sendMessage(message);//发送消息
}

看上述代码,我使用了HttpUtils.sendOkHttpRequest()方法,发起网络的请求,以上是onFailure方法要走的逻辑。

  1. 首先切换到主线程,然后弹出一个土司告诉用户,是否是网络了出了问题。
  2. 把常量ENTER_HOME赋值给message.what
  3. 延时2s后,发送message。

这么可虑,既然走到了onFailure方法,说明请求失败了,那么我们就应该进入到程序的HomeActivity因此我们使用handler发送消息。

ToastUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
7
/**
* Created by Archer on 2017/9/16.
* <p>
* 功能描述:
* 一个弹出土司的工具类
*/
public class ToastUtils {
public static void showShort(Context context,String text){
Toast.makeText(context,text,Toast.LENGTH_SHORT).show();
}
public static void showLong(Context context,String text){
Toast.makeText(context,text,Toast.LENGTH_LONG).show();
}
}

这就是一个很简单的封装,就不赘述了。

HttpUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
6
/**
* Created by Archer on 2017/9/16.
* <p>
* 功能描述:封装一个发起网络请求的一个工具类,
*/
public class HttpUtils {
public static void sendOkHttpRequest(String address,okhttp3.Callback callback){
OkHttpClient client= new OkHttpClient();
Request request= new Request.Builder().url(address).build();
client.newCall(request).enqueue(callback);
}

这就是现在最流行的网络请求开源库okhttp3的基本用法,第一个参数传入一个url,第二个传入一个callback回调函数,

  1. 得到OkHttpClient的实例
  2. 根据address得到一个请求Request
  3. 通过 OkHttpClient的实例,调用newCall方法,加入队列中。

现在再回来头上面的代码就是很清晰的吧,这个callBack里面实现了onFailure和onResponse方法,相信onFailure方法你已经明白是怎么回事了,接下来当然是要分析onResponse方法了。

服务器的Json数据

这是我放在服务器上的json数据,通过下面的这个网址就能访问到。

http://ogtmd8elu.bkt.clouddn.com/MobilesafeUpdate.json

我们模拟了app与服务器的交互。请求成功的话,就返回如下json数据。

1
2
3
4
5
6
{
"versionName":"2.0",
"versionCode":"2",
"versionDes":"2.0版本发布了,狂拽酷炫吊炸天",
"downloadUrl":"http://ogtmd8elu.bkt.clouddn.com/mobilesafe2.0.apk"
}

可以打开浏览器,输入adress地址,得到以上数据,很清楚了吧,意思是我们的服务器告诉我们,出现了新的版本,是2.0的版本,并且给我们了一个下载的最新版本的地址。

onResponse()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
8
@Override
public void onResponse(Call call, Response response) throws IOException {
String jsonResponseText = response.body().string();//将返回的response对象转化成String
Log.d(TAG, "onResponse: " + jsonResponseText);
int versionCloudCode = parseJsonObject(jsonResponseText);//解析json返回服务器上最新版本值
if (mVersionCode < versionCloudCode) {//本地的小于服务器的
message.what = UPDATE_VERSION; //给message一个消息,更新版本
} else {
message.what = ENTER_HOME; //进入主页面
}
long endtime = System.currentTimeMillis();
if (endtime - startTime < 4000) { //保证执行4s,体验好
try {
Thread.sleep(4000 - (endtime - startTime));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mHandler.sendMessage(message);//发送消息
}

上述代码非常容易理解,请求数据呈贡以后,callback给我回调的onResponse方法,reponse对象就是回调成功的返回对象类型,因此我们需要拿到它的body的string。这里注意,是string不是toString。这样的话,我们拿到了String类型的json数据jsonResponseText,因此,我们下一步调用的parseJsonObject(jsonResponseText)方法,就是把这个jsonResponseText进行解析得到服务器的版本号,然后与我们本地的版本号进行比较,如果本地版本号小于服务器的,那么给 message.what赋值一个UPDATE_VERSION的常量,由handler进行更新。否则的话(不需要更新,本地的已经是新版本了),ENTER_HOME赋值给message.what,进入程序的主界面。最终都是通过Handler发送message,通过handleMessage()进行操作。

parseJsonObject()

下面我们就要对服务器回调回来的json进行解析。

Update类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
9
package site.yanhui.mobilesafe.gson;
/**
* Created by Archer on 2017/9/16.
* <p>
* 功能描述:
* GsonBean,用来传入更新的基本信息。Json实体类
*/
public class Update {
/**
* versionName : 2.0
* versionCode : 2
* versionDes : 2.0版本发布了,狂拽酷炫吊炸天
* downloadUrl : http://yanhui.site
*/
private String versionName;
private String versionCode;
private String versionDes;
private String downloadUrl;
public String getVersionName() {
return versionName;
}
public void setVersionName(String versionName) {
this.versionName = versionName;
}
public String getVersionCode() {
return versionCode;
}
public void setVersionCode(String versionCode) {
this.versionCode = versionCode;
}
public String getVersionDes() {
return versionDes;
}
public void setVersionDes(String versionDes) {
this.versionDes = versionDes;
}
public String getDownloadUrl() {
return downloadUrl;
}
public void setDownloadUrl(String downloadUrl) {
this.downloadUrl = downloadUrl;
}
}

google 开源的Gson库,最牛的一点就是能过能够在返回的jsonString和jsonObject,jsonArray中建立起上述这种映射关系,并且非常容易使用。实体类准备好了。接下来开始解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
10
/**
* 解析拿到的jsonObject对象
*
* @param jsonData 传入json返回的String
*/
private int parseJsonObject(String jsonData) {
Gson gson = new Gson();
Update update = gson.fromJson(jsonData, Update.class);
Log.d(TAG, "parseJsonObject: " + update.getVersionDes());
Log.d(TAG, "parseJsonObject: " + update.getVersionName());
Log.d(TAG, "parseJsonObject: " + update.getVersionCode());
Log.d(TAG, "parseJsonObject: " + update.getDownloadUrl());
des = update.getVersionDes();
updateDownloadUrl = update.getDownloadUrl();
return Integer.parseInt(update.getVersionCode());
}

首先得到一个Gson对象,然后调用fromJson方法,传入到处理的json String和我们刚刚新建的实体类的class,只需要一行代码,你就可以通过调用update的get系列的方法,得到数据,这种感觉很爽吧。你也许可能会觉得,这有什么稀奇的,服务器返回的不过是一个简单jsonObject而已。使用jsonObject的解析方式也很简单,你说的没错,但是如果我们返回的是一个非常复杂的json数据的话,使用Gson就会变得非常方便和不容易出错了。这是下一个项目要介绍的内容了。先搞定这个吧,万里长征才开始了第一步。

可以打个log看一看,数据拿到没有,同时我们被得到的这个描述复制给了des,新版本apk下载地址赋值给了updateDownloadUrl ,最终把得到的服务器的版本号return了过去。样子我们就拿到了,可以进行比较了。

为什么需要handler

先简单解释一下,这个handler到底是什么情况。考虑以下的情况,现在我们的app需要更新UI界面,但是在更新UI界面时所需要的数据,却是要经过耗时操作,这该怎么处理呢。

主线程直接处理:

最直接的处理就在主线程中写耗时操作,但是很快就会发现问题,就是一旦耗时操作的时间超过(4s,还是5s)就会出现一个ARN(Application Not Responding)异常,应用没有响应,就会弹出一个对话框要求用户选择,可以选择等待或者是结束应用。这样子的话,用户的体验就太糟糕了。

子线程中处理:

显然第一种方式的处理非常糟糕,那么我们想,是否可以开启这个子线程专门处理UI更细呢?但是很快你就会发现,在子线程中是无法更新UI的(报异常),换句话中,就是当我们在更新UI的时候,必须在主线程里面,但是又不能有耗时操作(会造成ANR异常),那么最终的处理方式就是一种折中的机制。开启一个子线程,完成相关耗时操作,得到数据(数据就算准备好了),然后通知主线程去更新UI,这样就可以实现了嘛,因为数据都准备好了,就能马上更新了。

你会问,如何通知。这就是handler的作用了,Android我们提供了完整的一套上述情况的解决方式,handler就是其中的重要方式之一,实际上其他的几种方式都是对handler机制的一种封装。所以学好handler是非常重要的,在这里我就不讲handler的原理了,我会单开一篇专门研究handler。这里先讲handler在此项目中的具体使用。

handleMessage(Message msg)

相信在之前的方法中,你会先看到一个Message.obtain的Message初始化,然后无论是onFailure还是onResponse的方法中,我们把常量值如果在onResponse方法中,就把UPDATE_VERSION赋值给message.what,如果是onFailure方法中,就把ENTER_HOME赋值给message.what,这两个都市一个int的常量值,最后都使用了

1
mHandler.sendMessage(message);//发送消息

发送出去,接下来再又handler处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
11
private static final String TAG = "SplashActivity";
private static final int UPDATE_VERSION = 100;
private static final int ENTER_HOME = 101;
private TextView tv_versionName;
private int mVersionCode;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case UPDATE_VERSION:
showUpdateDialog();//启动更新的对话框
break;
case ENTER_HOME:
enterHome(); //进入主界面
}
}
};
private String des; //服务器得到的新版本描述
private String updateDownloadUrl; //新版本apk的下载地址

所以之前我们使用的mHandler的sendMessage方法,最终就是在这里得到了处理。因为我们是用switch语句判断赋值给massage.what携带的值,然后进行相应的操作。这就非常清楚了。

handler使用小结

1
2
3
final Message message = Message.obtain(); //初始化message对象
message.what 赋值
handler.sendMessage(message)

第一步调用Message.obtain(),这个方法在内部执行的就是不断的从线程池里面查询是否有message.what被赋值,如果有就捕捉到,然后最后使用handler发送出去。

enterHome()

我们先看enterHome方法.

1
2
3
4
5
6
7
8
9
10
12
/**
* 进入Home界面
*/
private void enterHome() {
Intent intent = new Intent(SplashActivity.this, HomeActivity.class);
startActivity(intent);
finish();//结束当前app
}

非常简单,从启动界面,跳转到主界面,并结束当前的activity。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
13
/**
* 弹出对话框更新界面
*/
private void showUpdateDialog() {
//弹出对话框,是依赖于activity存在的
final AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setIcon(R.drawable.ic_launcher); //设置左上角图标
alertDialog.setTitle("版本更新");
alertDialog.setMessage(des); //设置从服务器拿到的新版本描述
alertDialog.setPositiveButton("立即更新", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//申请权限
SplashActivityPermissionsDispatcher.downNewApkWithCheck(SplashActivity.this);
}
});
alertDialog.setNegativeButton("取消更新", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
enterHome();
}
});
// alertDialog.setCancelable(false);//不能被取消
//弹出软件升级界面 点击back按钮同样能进入主界面
alertDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
enterHome(); //进入主界面
dialog.dismiss();//使得dialog消失
}
});
alertDialog.show();//展现出dialog
}

首先我们先使用了AlertDialog.Builder构造,构造出了一个dialog实例,然后对其进行赋值,设置图标,标题,从服务器上拿到的版本描述(这里走的是成功拿到服务器数据的回调)。点击取消的时候,我们就认为是用户取消了更新,那么当然是要进入主界面了。在这里值得一提的是setOnCancelListener方法,这个方法的含义是,比如说弹出更新对话框了,用户既没有点击立即更新,也没有点击取消更新,而是直接点击了返回键,如果没有这个方法的话,就会卡在splash界面,体验非常差,所以要设置一下这个方法,让程序能正常运行起来,进入主界面,并让dialog消失。

动态权限适配(PermissionDispatcher)

Android提升了安全性以后,比如一些敏感权限,在这里用到的写入sd卡的权限,就必须得由用户授予权限,才可以调用。因此我们用到一个三方库PermissionDispatcher,之前的依赖相信你已经完成了。

1
2
//主菜单
Code->generate..->Generate Runtime Permission

填写需要到权限的方法,需要的理由,权限拒绝以后的方法,点击不再询问的方法。

初始化xUtils3

添加完依赖以后,还需要对其进行初始化

下载新的apk文件(xUtils3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
void downNewApk() {
ToastUtils.showShort(SplashActivity.this, "正在下载.....");
//1.判断sd卡是否挂载
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
//获得sd路径
final String path = Environment.getExternalStorageDirectory().getAbsolutePath()
+ File.separator + "mobilesafe1.apk";//指定存储路径(sd存储绝对路径)和存储的名称
String url = updateDownloadUrl; //将服务器上获得下载新apk的url赋值给url进行发起请求
RequestParams params = new RequestParams(url);//封装成param
params.setSaveFilePath(path);//设置存储下载文件的位置
// params.setAutoRename(true);//自动把名字改成服务器上的app名称,会覆盖,但是一旦重复下载就会多出来。
x.http().post(params, new org.xutils.common.Callback.CommonCallback<File>() {
@Override
public void onSuccess(File result) {
Log.i(TAG, "onSuccess: ");
//获得的result就是下载获得的文件
//提升权限
String path1 = result.getAbsolutePath();
Log.d(TAG, "onSuccess: "+path1);
setPermission(path1);
installApk(SplashActivity.this, result);
}

我们已经获得权限了,接下来就是要下载新的apk文件,注释的参数已经写的非常清楚了

野望 wechat
欢迎订阅我的个人微信公众号:yewang1993