Android6.0/7.0系統(tǒng)---使用Tomcat服務(wù)器進(jìn)行版本檢測(cè)更新

前言


自己寫(xiě)簡(jiǎn)書(shū)記錄知識(shí)與看別人的簡(jiǎn)書(shū)饲握、博客去學(xué)習(xí)有很大的區(qū)別,需要斟酌每一行代碼的編寫(xiě)蚕键,認(rèn)真的總結(jié)在編寫(xiě)代碼過(guò)程中遇到的問(wèn)題救欧,避免以及提醒其他android開(kāi)發(fā)者同樣的問(wèn)題。

其中的艱辛只有自己體會(huì)锣光,但同時(shí)也受益匪淺笆怠。Android的開(kāi)發(fā)學(xué)習(xí)就是在不斷的總結(jié)、思考中誊爹,在不斷的分享中才能得到進(jìn)步蹬刷。致那些無(wú)私奉獻(xiàn)瓢捉、不辭辛苦的開(kāi)源開(kāi)發(fā)者,博主办成,答主泡态,簡(jiǎn)主。予人樂(lè)迂卢,其樂(lè)無(wú)窮某弦。

代碼無(wú)可期,夢(mèng)想尤可違而克。但使志常存靶壮,亦復(fù)為君啟。
PS:自己總結(jié)的一句話

本篇內(nèi)容較多员萍,涵蓋的知識(shí)點(diǎn)較廣腾降,建議收藏學(xué)習(xí)。


Android6.0/7.0系統(tǒng)軟件版本使用Tomcat服務(wù)器進(jìn)行版本檢測(cè)更新


版本的升級(jí)檢測(cè)作為每個(gè)上線APP必備的功能碎绎,其重要性不言而喻部宿。很多的開(kāi)發(fā)者苦于不懂后端的開(kāi)發(fā)们何,無(wú)法進(jìn)行本地的版本測(cè)試借浊、升級(jí)的操作专肪。本篇將重點(diǎn)講述在Android6.0/7.0及以上系統(tǒng)版本中拇厢,如何利用Tomcat服務(wù)器進(jìn)行本地的軟件版本的檢測(cè)椭岩、更新竟趾。在上一篇中已經(jīng)講述如何利用Tomcat搭建本地服務(wù)器冯事,還沒(méi)有瀏覽這一篇的請(qǐng)自行前往: Android版本更新(一)---Tomcat服務(wù)器安裝配置及問(wèn)題解決宿接。鑒于Android系統(tǒng)的開(kāi)源性赘淮、資源的廣泛性,以下內(nèi)容中也會(huì)引用到其他開(kāi)發(fā)者的相關(guān)博客睦霎、簡(jiǎn)書(shū)梢卸,如有侵權(quán)還請(qǐng)告知,并及時(shí)修改副女。
本章節(jié)將通過(guò)以下幾個(gè)模塊來(lái)介紹如何進(jìn)行進(jìn)行版本的檢測(cè)蛤高、更新:

  • 服務(wù)器端的代碼構(gòu)建
  • 本地軟件版本的獲取
  • 對(duì)比服務(wù)器端版本彈出升級(jí)對(duì)話框
  • 下載并安裝最新版本的軟件

軟件版本檢測(cè)、更新的程序流程框圖:

Paste_Image.png

最終的軟件版本升級(jí)演示如下:

動(dòng)態(tài)展示.gif

一碑幅、服務(wù)器端代碼構(gòu)建


服務(wù)器端的代碼的構(gòu)建比較簡(jiǎn)單戴陡,只需要?jiǎng)?chuàng)建一個(gè)json格式的文件(保存的字符標(biāo)準(zhǔn)選擇為UTF-8,否則在后續(xù)的Gson解析的時(shí)候中文為亂碼)沟涨,如updateinfo.json恤批,具體代碼如下:

{
    "versionName":"2.0",
    "versionCode":2,
    "des":"這是升級(jí)后的版本",
    "apkUrl":"http://172.26.0.1:8181/app-release.apk"
}

其文件放置的位置為,tomcat根目錄下裹赴,主要包含兩個(gè)文件updateinfo.json和app-release.apk喜庞;
updateinfo.json---包含升級(jí)的相關(guān)信息
versionName:版本名诀浪,可用于在軟件中顯示的版本名稱;
versionCode:版本號(hào)延都,用于本地versionCode與服務(wù)器端的對(duì)比雷猪;
des:版本升級(jí)的一些信息;
apkUrl:即升級(jí)的軟件安裝包app-release.apk所在的服務(wù)器端的位置連接晰房;
app-release.apk---待升級(jí)的版本
以上兩個(gè)文件可以僅作為測(cè)試使用求摇,可以根據(jù)需求自行設(shè)置內(nèi)容及文件名。

Paste_Image.png

打開(kāi)Tomcat嫉你,并通過(guò)cmd的ipconfig獲取本地的ip地址

Paste_Image.png

然后在瀏覽器輸入: http://172.26.0.1:8181/updateinfo.json月帝,即可顯示我們的服務(wù)器的升級(jí)信息。
注意:這個(gè)是我本地的IP地址幽污,請(qǐng)注意修改為你本地的IP地址嚷辅,至于端口號(hào)為什么是8181,在上一篇中有說(shuō)到距误,這個(gè)是由于本地的8080端口被占用簸搞,瀏覽器中的中文出現(xiàn)亂碼,這個(gè)是瀏覽器的問(wèn)題准潭,不用理會(huì)趁俊。

Paste_Image.png

二、本地軟件版本的獲取


在項(xiàng)目project的AndroidManifest.xml中設(shè)置當(dāng)前軟件的版本信息versionCode和versionName

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

同時(shí)需要module的defaultConfig配置versionCode和versionName

Paste_Image.png

構(gòu)建工具類刑然,來(lái)獲取本地的versionCode和versionName

/**
* 獲取本地的versionCode和versionName
*/
public class VersionGetUtil {
    private static final String TAG = "VersionGetUtil";
    /**
     * 獲取版本名
     */
    public static String getVersionName(Context context){
        //PackageManager,可以獲取清單中的所有信息
        PackageManager manager = context.getPackageManager();
        //getPackageName(),獲取當(dāng)前程序的包名
        try {
            //獲取包中的信息
            PackageInfo info = manager.getPackageInfo(context.getPackageName(),0);
            String versionName = info.versionName;//版本名寺擂,是需要在APP中顯示的
            Log.i(TAG, "getVersion: name" + versionName);
            return versionName;
        }catch (PackageManager.NameNotFoundException e){
            e.printStackTrace();
            Log.e("VersionGetUtil","can not get current Version Name");
        }
        //如果出現(xiàn)異常拋出null
        return null;
    }
    /**
     * 獲取版本號(hào)
     */
    public static int getVersionCode(Context context){
        //PackageManager,可以獲取清單中的所有信息
        PackageManager manager = context.getPackageManager();
        //getPackageName(),獲取當(dāng)前程序的包名
        try {
            //獲取包中的信息
            PackageInfo info = manager.getPackageInfo(context.getPackageName(),0);
            int versionCode = info.versionCode;//版本號(hào),用于判斷是否為最新版本
            Log.i(TAG, "getVersion: code" + versionCode);
            return versionCode;
        }catch (PackageManager.NameNotFoundException e){
            e.printStackTrace();
            Log.e("VersionGetUtil","can not get current Version Code");
        }
        //如果出現(xiàn)異常拋出0
        return 0;
    }
}

三泼掠、對(duì)比服務(wù)器端版本彈出升級(jí)對(duì)話框等操作


首先根據(jù)服務(wù)器端的json格式的版本升級(jí)數(shù)據(jù)怔软,封裝成一個(gè)實(shí)體類VersionInfoEntity,便于后續(xù)使用OkHttp請(qǐng)求的時(shí)候解析择镇,可以使用GsonFormat快速的實(shí)現(xiàn)實(shí)體類的生成挡逼,可以參考: 插件GsonFormat快速生成JSon實(shí)體類

public class VersionInfoEntity {

    /**
     * versionName : 2.0
     * versionCode : 2
     * des : 這是升級(jí)后的版本
     * apkUrl : http://172.26.0.1:8181/app-release.apk
     */

    private String versionName;
    private int versionCode;
    private String des;
    private String apkUrl;

    public String getVersionName() {
        return versionName;
    }

    public void setVersionName(String versionName) {
        this.versionName = versionName;
    }

    public int getVersionCode() {
        return versionCode;
    }

    public void setVersionCode(int versionCode) {
        this.versionCode = versionCode;
    }

    public String getDes() {
        return des;
    }

    public void setDes(String des) {
        this.des = des;
    }

    public String getApkUrl() {
        return apkUrl;
    }

    public void setApkUrl(String apkUrl) {
        this.apkUrl = apkUrl;
    }
}

軟件版本的升級(jí)腻豌,做法很簡(jiǎn)單:通過(guò)獲取本地的版本號(hào)或者版本名與服務(wù)器端的版本號(hào)或者版本名進(jìn)行對(duì)比家坎,不一致則提示升級(jí)等操作,這里就只講述版本號(hào)對(duì)比吝梅∈瑁考慮到其內(nèi)部的流程比較多,且為了后續(xù)的維護(hù)憔涉,我的做法是封裝成一個(gè)工具類订框,具體代碼如下:
封裝的工具類使用到了OkHttp,handler消息處理兜叨,對(duì)于各個(gè)函數(shù)的使用和關(guān)鍵代碼都有注釋穿扳,很容易理解衩侥,就不作一一說(shuō)明,可以參考流程圖對(duì)比代碼矛物,這樣看起來(lái)就會(huì)一目了然茫死。

public class VersionUpdateUtil extends Activity{
    private static final String TAG = VersionUpdateUtil.class.getSimpleName();
    /**
     * 服務(wù)器端保存版本更新信息的地址
     */
    private static final String server_url = "http://172.26.0.1:8181/updateinfo.json";

    /**
     * 用于構(gòu)造函數(shù)
     * versionCode: 本地版本號(hào)
     */
    private Activity activity;
    private int currentVersionCode;

    /**
     * 版本更新實(shí)體類,包括版本號(hào)履羞,版本名稱峦萎,描述,下載地址
     */
    private VersionInfoEntity versionInfoEntity;
    /**
     * 服務(wù)器版本號(hào),并初始化
     */
    private static int serverVersionCode = 1;

    /**
     * 聲明okhttp客戶端忆首,并設(shè)置讀/寫(xiě)/連接超時(shí)
     */
    private OkHttpClient client = new OkHttpClient.Builder()
            .readTimeout(10, TimeUnit.SECONDS) //設(shè)置讀超時(shí)
            .writeTimeout(10, TimeUnit.SECONDS) //設(shè)置寫(xiě)超時(shí)
            .connectTimeout(10, TimeUnit.SECONDS) //設(shè)置連接超時(shí)范圍
            .build();
    /**
     * 忽略版本爱榔,保存服務(wù)器端版本號(hào)到sp中
     */
    private SharedPreferences sharedPreferences = null;
    private SharedPreferences.Editor editor = null;
    private static int savedVersion = 1;

    /**
     * 進(jìn)度更新dialog
     */
    private ProgressDialog progressDialog;

    /**
     * 請(qǐng)求碼,用于動(dòng)態(tài)權(quán)限設(shè)置的回調(diào)
     */
    private static final int REQUEST_CODE = 1;

    private File file = null;
    /**
     * handler消息的處理
     */
    private static final int UPDATE_YES = 1;
    private static final int UPDATE_NO = 2;
    private static final int IO_ERROR = 3;
    private static final int SHOW_DIALOG = 4;
    private static final int UPDATE_IGNORE = 5;
    private static final int UPDATE_PROGRESS = 6;
    private static final int UPDATE_INSTALL = 7;
    private static final int UPDATE_ADD_PERMISSION = 8;
    private static final int NEWEST_VERSION = 9;

    final Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case UPDATE_YES:
                    Log.i(TAG, "handleMessage: 需要更新");
                    //從服務(wù)器端獲取apk的下載網(wǎng)址
                    downLoadApk(versionInfoEntity.getApkUrl());
                    break;
                case UPDATE_NO:
                    Log.w(TAG, "handleMessage: 不需要更新");
                    break;
                case UPDATE_IGNORE:
                    Toast.makeText(activity,"用戶忽略了該版本", Toast.LENGTH_SHORT).show();
                    saveNewestVersion(serverVersionCode);
                    break;
                case IO_ERROR:
                    Log.e(TAG, "handleMessage: IO異常");
                    break;
                case SHOW_DIALOG:
                    Log.i(TAG, "handleMessage: 彈出更新對(duì)話框");
                    showUpdateDialog();
                    break;
                case UPDATE_PROGRESS:
                    int progress = msg.arg1;
                    progressDialog.setProgress(progress);
                    break;
                case UPDATE_INSTALL:
                    Log.i(TAG, "handleMessage: file: " + file);
                    installApk(activity,file);
                    break;
                case UPDATE_ADD_PERMISSION:
                    Toast.makeText(activity,"需要添加權(quán)限,請(qǐng)點(diǎn)擊允許", Toast.LENGTH_SHORT).show();
                    break;
                case NEWEST_VERSION:
                    Toast.makeText(activity,"已經(jīng)是最新版本糙及!", Toast.LENGTH_SHORT).show();
                    break;
            }
        }
    };

    /**
     * 構(gòu)造函數(shù)
     *
     * @param currentVersionCode 本地版本號(hào)
     * @param activity
     */
    public VersionUpdateUtil(int currentVersionCode, Activity activity) {
        this.activity = activity;
        this.currentVersionCode = currentVersionCode;
    }

    /**
     * 獲取服務(wù)器版本號(hào)
     */
    public void getServerVersionCode() {
        //構(gòu)造request详幽,并設(shè)置request參數(shù)
        final Request request = new Request.Builder()
                .url(server_url)
                .build();

        //請(qǐng)求調(diào)度,異步get請(qǐng)求
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                handler.sendEmptyMessage(IO_ERROR);
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {

                String result = response.body().string();
                Log.i(TAG, "當(dāng)前響應(yīng)的結(jié)果:" + result);

                //利用gson解析服務(wù)器端的數(shù)據(jù),并將數(shù)據(jù)保存到VersionInfoEntity實(shí)體類中
                Gson gson = new Gson();
                versionInfoEntity = gson.fromJson(result, VersionInfoEntity.class);

                //獲取服務(wù)器端的版本號(hào)與本地的服務(wù)端版本號(hào)作對(duì)比
                serverVersionCode = versionInfoEntity.getVersionCode();

                Log.i(TAG, "服務(wù)器端版本: " + serverVersionCode);
                Log.i(TAG, "本地版本: " + currentVersionCode);
                Log.i(TAG, "sp保存的版本:" + savedVersion);
                /**
                 * 版本更新的判斷與是否執(zhí)行了忽略版本的操作
                 */
                if (serverVersionCode > currentVersionCode) {
                    if (serverVersionCode == savedVersion){
                        Log.i(TAG, "onResponse: 用戶選擇了忽略該版本");
                        handler.sendEmptyMessage(UPDATE_IGNORE);
                    }else {
                        handler.sendEmptyMessage(SHOW_DIALOG);
                    }

                } else {
                    System.out.println("無(wú)最新版本");
                    handler.sendEmptyMessage(NEWEST_VERSION);
                }
            }
        });
    }

    /**
     * 彈出對(duì)話框浸锨,讓用戶判斷是否需要更新版本
     */
    private void showUpdateDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(activity);
        builder.setTitle("監(jiān)測(cè)到新版本");
        builder.setMessage(versionInfoEntity.getDes());
        builder.setPositiveButton("確定更新", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int i) {
                handler.sendEmptyMessage(UPDATE_YES);
            }
        });
        builder.setNeutralButton("忽略版本", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int i) {
                handler.sendEmptyMessage(UPDATE_IGNORE);
            }
        });
        builder.setNegativeButton("暫不更新", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int i) {
                handler.sendEmptyMessage(UPDATE_NO);
                dialog.dismiss();
            }
        });
        builder.create().show();
    }

    /**
     * 從服務(wù)器下載新版本的APK
     */
    private void downLoadApk(final String downLoadApkUrl) {
        //創(chuàng)建進(jìn)度對(duì)話框
        createProgressDialog();

        //請(qǐng)求服務(wù)器端的apk
        final Request request = new Request.Builder()
                .url(downLoadApkUrl)
                .build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                handler.sendEmptyMessage(IO_ERROR);
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                /**
                 * android6.0系統(tǒng)后增加運(yùn)行時(shí)權(quán)限唇聘,需要?jiǎng)討B(tài)添加內(nèi)存卡讀取權(quán)限
                 */
                if (Build.VERSION.SDK_INT >= 23) {
                    int permission = ContextCompat.checkSelfPermission(activity, android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
                    if (permission != PackageManager.PERMISSION_GRANTED) {
                        progressDialog.dismiss();
                        ActivityCompat.requestPermissions(activity, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
                        Log.w(TAG, "checkWriteStoragePermission: 無(wú)此權(quán)限,需要添加");
                        handler.sendEmptyMessage(UPDATE_ADD_PERMISSION);
                        return;
                    } else {
                        downApkFlie(response);
                        if (progressDialog != null && progressDialog.isShowing()){
                            progressDialog.dismiss();
                        }
                        handler.sendEmptyMessage(UPDATE_INSTALL);

                    }
                } else {
                    downApkFlie(response);
                    if (progressDialog != null && progressDialog.isShowing()){
                        progressDialog.dismiss();
                    }
                    handler.sendEmptyMessage(UPDATE_INSTALL);
                }

            }
        });
    }


    /**
     * 忽略當(dāng)前服務(wù)器端的版本
     * @param versionCode
     */
    private void saveNewestVersion(int versionCode) {
        sharedPreferences = activity.getSharedPreferences("ignore_ServerVersionCode", Activity.MODE_PRIVATE);
        editor = sharedPreferences.edit();

        editor.putInt("ignore_ServerVersionCode", versionCode);
        editor.commit();

        savedVersion = sharedPreferences.getInt("ignore_ServerVersionCode",versionCode);
    }

    private void createProgressDialog() {
        progressDialog = new ProgressDialog(activity);
        progressDialog.setMax(100);
        progressDialog.setCancelable(false);
        progressDialog.setMessage("正在下載");
        progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        progressDialog.show();
    }
    
    /**
     * OkHttp請(qǐng)求的結(jié)果
     *
     * @param response
     */
    private void downApkFlie(Response response) {
        InputStream is = null;
        FileOutputStream fos = null;
        byte[] buf = new byte[1024];//每次讀取1K的數(shù)據(jù)

        int len = 0;
        long sum = 0;
        int progress = 0;

        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            file = new File(Environment.getExternalStorageDirectory(), "test.apk");
            try {
                if (file.exists()) {
                    file.delete();
                } else {
                    file.createNewFile();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            long total = response.body().contentLength();
            Log.i(TAG, "downApkFlie: total---" + total);

            is = response.body().byteStream();

            //捕捉是否動(dòng)態(tài)分配讀寫(xiě)內(nèi)存權(quán)限異常
            try {
                fos = new FileOutputStream(file);
                //捕捉輸入流讀取異常
                try {
                    /**
                     * read(),從輸入流中讀取數(shù)據(jù)的下一個(gè)字節(jié)柱搜,返回0~255范圍內(nèi)的字節(jié)值迟郎,如果已經(jīng)到達(dá)
                     * 流末尾而沒(méi)有可用的字節(jié),則返回-1
                     */
                    while ((len = is.read(buf)) != -1) {
                        fos.write(buf, 0, len);//write(byte[]b, off, int len), 將指定的byte數(shù)組中從偏移量off開(kāi)始的len個(gè)字節(jié)寫(xiě)入此輸出流
                        sum += len;
                        progress = (int) (sum * 1.0f / total * 100);
                        Log.d("h_bl", "progress=" + progress);
                        //更新進(jìn)度
                        Message msg = handler.obtainMessage();
                        msg.what = UPDATE_PROGRESS;
                        msg.arg1 = progress;
                        handler.sendMessage(msg);
                    }
                    fos.flush();//徹底完成輸出并清空緩存區(qū)
                    Log.i(TAG, "downApkFlie: 下載完畢");
                } catch (IOException e) {
                    handler.sendEmptyMessage(IO_ERROR);
                }

            } catch (FileNotFoundException e) {
                e.printStackTrace();
                Log.e(TAG, "downApkFlie: 下載失敗");
            } finally {
                //清空f(shuō)ile輸入輸出流
                try {
                    if (is != null) {
                        is.close();//關(guān)閉輸入流
                    }
                    if (fos != null) {
                        fos.close();//關(guān)閉輸出流
                    }
                } catch (IOException e) {
                    handler.sendEmptyMessage(IO_ERROR);
                }
            }
        }
    }

    /**
     * 安裝新版本APK
     */
    protected void installApk(Activity activity, File file) {
        if (activity == null || !file.exists()){
            return;
        }

        Intent intent = new Intent(Intent.ACTION_VIEW);
        // 由于沒(méi)有在Activity環(huán)境下啟動(dòng)Activity,設(shè)置下面的標(biāo)簽
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //aandroid N的權(quán)限問(wèn)題
            //賦予臨時(shí)權(quán)限
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            //通過(guò)provider生成uri
            Uri contentUri = FileProvider.getUriForFile(activity, "com.example.test.fileprovider", file);//注意修改com.example.test為自己的包名
            intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
        }
        activity.startActivity(intent);
    }
}

在以上封裝的工具類中聪蘸,遇到了很多問(wèn)題宪肖,重點(diǎn)講述兩個(gè)問(wèn)題:

  1. 動(dòng)態(tài)權(quán)限;
  2. Android7.0安裝apk健爬;

3.1 Android6.0動(dòng)態(tài)權(quán)限

在上述工具類VersionInfoEntity.class中匈庭,在確定更新后,進(jìn)入到downLoadApk()浑劳,從服務(wù)器端下載最新的apk。起初夭拌,我在Androidmanifest.xml中定義了以下權(quán)限

<!--相關(guān)權(quán)限-->
<!-- 權(quán)限設(shè)置 -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<!-- 在SD卡中創(chuàng)建和刪除文件權(quán)限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.MOUNT_FORMAT_FILESYSTEMS"/>
<!-- 向SD卡中寫(xiě)入東西權(quán)限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>-->

<!-- 藍(lán)牙 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />

<!-- 網(wǎng)絡(luò)權(quán)限 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />

這個(gè)習(xí)慣很不好魔熏,其實(shí)很多的權(quán)限,可能就用不到鸽扁,但是經(jīng)常怕遺忘蒜绽,就全部拷貝過(guò)來(lái)了,那么問(wèn)題就來(lái)了桶现,在編譯運(yùn)行的時(shí)候躲雅,打印以下log:

Paste_Image.png

FileNotFoundException:/storage/emulated/0/test.apk,(Permission denied)

沒(méi)有權(quán)限?可是我明明已經(jīng)給與寫(xiě)內(nèi)存卡的權(quán)限了啊骡和,仔細(xì)地查詢了一些資料相赁,以及在android開(kāi)發(fā)者官方: developer

Android6.0為了保護(hù)用戶的隱私相寇,將一些權(quán)限的申請(qǐng)放在了應(yīng)用運(yùn)行的時(shí)候去申請(qǐng),如內(nèi)存卡的讀寫(xiě)權(quán)限钮科。在以前的版本中唤衫,開(kāi)發(fā)人員只需要在AndroidManifest.xml中設(shè)置即可,如

<!-- 向SD卡中寫(xiě)入東西權(quán)限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

安裝應(yīng)用的時(shí)候可以在設(shè)置的應(yīng)用信息中看到绵脯,如應(yīng)用需要獲得***權(quán)限佳励,用戶點(diǎn)擊后就可以設(shè)置相應(yīng)的權(quán)限,如允許蛆挫、拒絕等赃承。但是存在這一的一個(gè)問(wèn)題,如一款應(yīng)用APP只需要電話悴侵、短信的權(quán)限瞧剖,但是在開(kāi)發(fā)的過(guò)程中,開(kāi)發(fā)者為了省事畜挨,請(qǐng)求了全部的權(quán)限筒繁,這就可能導(dǎo)致侵犯了用戶隱私的權(quán)限請(qǐng)求,在用戶安裝了這款A(yù)PP后巴元,才發(fā)現(xiàn)拍照毡咏、讀取內(nèi)存、網(wǎng)絡(luò)等權(quán)限被打開(kāi)了逮刨,有可能導(dǎo)致一些隱私數(shù)據(jù)被剽竊呕缭。而Google官網(wǎng)為了避免用戶的數(shù)據(jù)被剽竊,在Android6.0版本后加入了動(dòng)態(tài)權(quán)限的的申請(qǐng)修己。 對(duì)于我這種喜歡全部權(quán)限都給予的人也是一種約束恢总。

這些動(dòng)態(tài)的權(quán)限在需要的時(shí)候才需要用戶動(dòng)態(tài)申請(qǐng),比如在上面所說(shuō)的APP中睬愤,如果需要用到拍照的功能片仿,需要在使用的地方通過(guò)代碼請(qǐng)求打開(kāi)拍照權(quán)限的方式動(dòng)態(tài)的去請(qǐng)求這個(gè)拍照權(quán)限。

以下是危險(xiǎn)權(quán)限尤辱,有組的概念砂豌,如果一個(gè)權(quán)限組內(nèi)的某個(gè)權(quán)限被獲取了,那么這個(gè)組中剩余的權(quán)限也會(huì)被自動(dòng)獲取光督。而這些權(quán)限需要?jiǎng)討B(tài)的去申請(qǐng)阳距,可以理解為,動(dòng)態(tài)的申請(qǐng)了

permission:android.permission.WRITE_EXTERNAL_STORAGE

group:android.permission-group.STORAGE结借,組中的其他權(quán)限也將自動(dòng)獲得筐摘。

以下是一些比較危險(xiǎn)的權(quán)限,需要去動(dòng)態(tài)的申請(qǐng):

//聯(lián)系人

group:android.permission-group.CONTACTS
permission:android.permission.WRITE_CONTACTS
permission:android.permission.GET_ACCOUNTS
permission:android.permission.READ_CONTACTS

//電話
group:android.permission-group.PHONE
permission:android.permission.READ_CALL_LOG
permission:android.permission.READ_PHONE_STATE
permission:android.permission.CALL_PHONE
permission:android.permission.WRITE_CALL_LOG
permission:android.permission.USE_SIP
permission:android.permission.PROCESS_OUTGOING_CALLS
permission:com.android.voicemail.permission.ADD_VOICEMAIL

//日歷
group:android.permission-group.CALENDAR
permission:android.permission.READ_CALENDAR
permission:android.permission.WRITE_CALENDAR

//相機(jī)
group:android.permission-group.CAMERA
permission:android.permission.CAMERA

//傳感器
group:android.permission-group.SENSORS
permission:android.permission.BODY_SENSORS

//定位
group:android.permission-group.LOCATION
permission:android.permission.ACCESS_FINE_LOCATION
permission:android.permission.ACCESS_COARSE_LOCATION

//內(nèi)存卡
group:android.permission-group.STORAGE
permission:android.permission.READ_EXTERNAL_STORAGE
permission:android.permission.WRITE_EXTERNAL_STORAGE

//耳機(jī)
group:android.permission-group.MICROPHONE
permission:android.permission.RECORD_AUDIO

//SMS
group:android.permission-group.SMS
permission:android.permission.READ_SMS
permission:android.permission.RECEIVE_WAP_PUSH
permission:android.permission.RECEIVE_MMS
permission:android.permission.RECEIVE_SMS
permission:android.permission.SEND_SMS
permission:android.permission.READ_CELL_BROADCASTS

因此將從服務(wù)器端下載apk這一塊需要用到動(dòng)態(tài)權(quán)限的地方特殊的解釋一下:代碼如下:

/**
 * android6.0系統(tǒng)后增加運(yùn)行時(shí)權(quán)限,需要?jiǎng)討B(tài)添加內(nèi)存卡讀取權(quán)限
 */
if (Build.VERSION.SDK_INT >= 23) {
        int permission = ContextCompat.checkSelfPermission(activity, android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
        if (permission != PackageManager.PERMISSION_GRANTED) {
            progressDialog.dismiss();
            ActivityCompat.requestPermissions(activity, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
            Log.w(TAG, "checkWriteStoragePermission: 無(wú)此權(quán)限咖熟,需要添加");
            handler.sendEmptyMessage(UPDATE_ADD_PERMISSION);
            return;
        } else {
            downApkFlie(response);
            if (progressDialog != null && progressDialog.isShowing()){
                progressDialog.dismiss();
            }
            handler.sendEmptyMessage(UPDATE_INSTALL);

        }
    } else {
        downApkFlie(response);
        if (progressDialog != null && progressDialog.isShowing()){
            progressDialog.dismiss();
        }
        handler.sendEmptyMessage(UPDATE_INSTALL);
    }            
  • 檢查權(quán)限

ContextCompat.checkSelfPermission(Context context, String permission)圃酵;

  • 有權(quán)限: PackageManager.PERMISSION_GRANTED
  • 無(wú)權(quán)限: PackageManager.PERMISSION_DENIED

當(dāng)應(yīng)用需要用到危險(xiǎn)權(quán)限時(shí),在執(zhí)行權(quán)限相關(guān)代碼前球恤,使用該方法判斷是否擁有指定的權(quán)限辜昵。有權(quán)限,則繼續(xù)執(zhí)行設(shè)計(jì)需要權(quán)限的代碼咽斧;無(wú)權(quán)限堪置,則向用戶請(qǐng)求授予權(quán)限。

  • 解釋權(quán)限

ActivityCompat.shouldShowRequestPermissionRationale(Activity activity, String permission)

判斷是否有必要向用戶解釋為什么要這項(xiàng)權(quán)限张惹。如果應(yīng)用第一次請(qǐng)求過(guò)此權(quán)限舀锨,但是被用戶拒絕了,則之后調(diào)用該方法將返回 true宛逗,此時(shí)就有必要向用戶詳細(xì)說(shuō)明需要此權(quán)限的原因
備注:如果應(yīng)用第一次請(qǐng)求此權(quán)限時(shí)被用戶拒絕坎匿,第二次再請(qǐng)求此權(quán)限時(shí),用戶勾選了權(quán)限請(qǐng)求對(duì)話框的“不再詢問(wèn)”雷激,則此方法返回 false替蔬。如果設(shè)備規(guī)范禁止應(yīng)用擁有該權(quán)限,此方法也返回 false屎暇。

  • 請(qǐng)求權(quán)限

ActivityCompat.requestPermissions(Activity activity, String[] permissions, int requestCode)

當(dāng)檢測(cè)到應(yīng)用沒(méi)有指定的權(quán)限時(shí)承桥,調(diào)用此方法向用戶請(qǐng)求權(quán)限。調(diào)用此方法將彈出權(quán)限請(qǐng)求對(duì)話框詢問(wèn)用戶 “允許” 或 “拒絕” 指定的權(quán)限根悼。

  1. 權(quán)限參數(shù)傳入的是數(shù)組凶异,可以調(diào)用該方法一次請(qǐng)求多個(gè)權(quán)限;傳入的權(quán)限數(shù)組參數(shù)以單個(gè)具體權(quán)限為單位挤巡,但彈框詢問(wèn)用戶授權(quán)時(shí)矿卑,屬于同一權(quán)限組的權(quán)限將自動(dòng)合并詢問(wèn)授權(quán)一次;
  2. 請(qǐng)求的權(quán)限必須事先在 AndroidManifest.xml 中有聲明母廷,否則調(diào)用此方法請(qǐng)求時(shí)瀑晒,將不彈框徘意,而是直接返回“拒絕”的結(jié)果轩褐;
  3. 第一次請(qǐng)求權(quán)限時(shí)椎咧,用戶點(diǎn)擊了“拒絕”,第二次再請(qǐng)求該權(quán)限時(shí)蟋座,對(duì)話框?qū)⒊霈F(xiàn)“不再詢問(wèn)”復(fù)選框脚牍,如果用戶勾選了“不再詢問(wèn)”并點(diǎn)擊了“拒絕”诸狭,則之后再請(qǐng)求此權(quán)限組時(shí)將不彈框,而是直接返回“拒絕”的結(jié)果芹彬。
  • 處理結(jié)果
    請(qǐng)求權(quán)限的結(jié)果返回和接收一個(gè)Activity的返回類似舒帮,重寫(xiě) FragmentActivity 或 (v4) Fragment 中的 onRequestPermissionsResult(...) 方法陡叠。
    /**
     * 處理權(quán)限請(qǐng)求結(jié)果
     *
     * @param requestCode
     *          請(qǐng)求權(quán)限時(shí)傳入的請(qǐng)求碼枉阵,用于區(qū)別是哪一次請(qǐng)求的
     *
     * @param permissions
     *          所請(qǐng)求的所有權(quán)限的數(shù)組岭妖,
     *          例如String permissions = new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
     *                           android.Manifest.permission.CALL_PHONE};
     *
     * @param grantResults
     *          權(quán)限授予結(jié)果昵慌,和 permissions 數(shù)組參數(shù)中的權(quán)限一一對(duì)應(yīng),
     *          例已卷,WRITE_EXTERNAL_STORAGE和CALL_PHONE兩個(gè)元素值為兩種情況侧蘸,如下:
     *          授予: PackageManager.PERMISSION_GRANTED
     *          拒絕: PackageManager.PERMISSION_DENIED
     *          可能的結(jié)果有幾種鹉梨,{true,true},{true,false},{false,true},{false,false}
     *          如果針對(duì)某一個(gè)直接采用數(shù)組下標(biāo)來(lái)判斷存皂,例如WRITE_EXTERNAL_STORAGE,則為grantResults[0],代表其權(quán)限值
     *
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CODE && grantResults[0] == PackageManager.PERMISSION_GRANTED){
            Log.i(TAG, "onRequestPermissionsResult: ++++");
        }
    }

3.2 android7.0安裝APK

在apk下載后它改,進(jìn)入安裝階段出現(xiàn)以下error

FATAL EXCEPTION: main
Process: com.example.test.release, PID: 19544
android.os.FileUriExposedException: file:///storage/emulated/0/test.apk exposed beyond app through Intent.getData()

搜尋了一些資料:Android7.0行為變更央拖,這是由于Android7.0執(zhí)行了“StrictMode API 政策禁”以及“私有目錄被限制訪問(wèn)”的原因鲜戒,隨著Android版本越來(lái)越高袍啡,Google對(duì)于用戶隱私的保護(hù)力度也越來(lái)越大境输∮毕担可以用FileProvider來(lái)解決這一問(wèn)題嘁扼。

在應(yīng)用間共享文件
對(duì)于面向 Android 7.0 的應(yīng)用趁啸,Android 框架執(zhí)行的 StrictMode API 政策禁止在您的應(yīng)用外部公開(kāi) file:// URI不傅。如果一項(xiàng)包含文件 URI 的 intent 離開(kāi)您的應(yīng)用访娶,則應(yīng)用出現(xiàn)故障崖疤,并出現(xiàn) FileUriExposedException 異常。
要在應(yīng)用間共享文件叮趴,您應(yīng)發(fā)送一項(xiàng) content:// URI眯亦,并授予 URI 臨時(shí)訪問(wèn)權(quán)限搔驼。進(jìn)行此授權(quán)的最簡(jiǎn)單方式是使用 FileProvider 類舌涨。如需了解有關(guān)權(quán)限和共享文件的詳細(xì)信息囊嘉,請(qǐng)參閱共享文件革为。

” StrictMode API 政策” 是指禁止向你的應(yīng)用外公開(kāi) file:// URI震檩。 如果一項(xiàng)包含文件 file:// URI類型 的 Intent 離開(kāi)你的應(yīng)用抛虏,應(yīng)用失敗迂猴,并出現(xiàn) FileUriExposedException 異常沸毁。

現(xiàn)在我們就來(lái)一步一步的解決這個(gè)問(wèn)題息尺。

3.2.1 AndroidManifest.xml清單文件中注冊(cè)provider

provider也是Android四大組件之一掷倔,可以簡(jiǎn)單把它理解為向外提供數(shù)據(jù)的組件,參考: FileProvider API:在項(xiàng)目的AndroidManifest.xml中注冊(cè)勒葱。具體配置代碼如下:

<application>
...
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.example.test.fileprovider"
    android:grantUriPermissions="true"
    android:exported="false"
    >
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>
...
</application>

其中

android:authorities:組件標(biāo)識(shí)凛虽,這個(gè)屬性的com.example.test為你本項(xiàng)目的包名凯旋,可以在mainfest中找到,用于避免和其它應(yīng)用發(fā)生沖突

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

android:resource糠聪,指的是當(dāng)前組件引用 res/xml/file_paths.xml 這個(gè)文件舰蟆,通過(guò)<meta-data>標(biāo)簽將上面的filepath添加到provider當(dāng)中

通過(guò)閱讀API身害,放置<paths>元素和子元素到項(xiàng)目中的xml文件塌鸯,需自行創(chuàng)建

Paste_Image.png

新建的file_paths.xml的代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="test" path="."/>
</paths>

通過(guò)閱讀API:

<files-path/>代表的根目錄:Context.getFilesDir()

<external-path/>代表的根目錄: Environment.getExternalStorageDirectory()

<cache-path/>代表的根目錄: getCacheDir()

在上述代碼中:

path="."代表的是根目錄丙猬,即你可以向其它的應(yīng)用共享根目錄及其子目錄下任何一個(gè)文件了淮悼。如果使用path="download",那么得到的目錄為“/storage/emulated/0/download”袜腥,只允許向其他應(yīng)用共享download目錄及其子目錄內(nèi)的文件羹令。

在完成以上配置后福侈,使用到的完整的安裝apk的函數(shù)如下:

/**
     * 安裝新版本APK
     */
    protected void installApk(Activity activity, File file) {
        if (activity == null || !file.exists()){
            return;
        }

        Intent intent = new Intent(Intent.ACTION_VIEW);
        // 由于沒(méi)有在Activity環(huán)境下啟動(dòng)Activity,設(shè)置下面的標(biāo)簽
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //aandroid N的權(quán)限問(wèn)題
            //賦予臨時(shí)權(quán)限
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            //通過(guò)provider生成uri
            Uri contentUri = FileProvider.getUriForFile(activity, "com.example.test.fileprovider", file);//注意修改com.example.test為自己的包名
            intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
        }
        activity.startActivity(intent);
    }

特別需要注意的是:需要注意的是:修改com.example.test為自己的包名。


四伟墙、測(cè)試

  • 測(cè)試手機(jī):三星S7 edge
  • 服務(wù)器:Tomcat:http://172.26.0.1:8181/updateinfo.json
  • 軟件版本:1.0
  • 服務(wù)端軟件版本:2.0
  • 更改內(nèi)容:主界面新增加一個(gè)textView“這是升級(jí)后的版本”戳葵。

4.1 修改AndroidManifest.xml及build.gradle配置文件

AndroidManifest.xml修改為:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.test"
    android:versionName="2.0"
    android:versionCode="2">

build.gradle文件修改為:

defaultConfig {
    applicationId "com.example.test"
    minSdkVersion 19
    targetSdkVersion 25
    versionCode 2
    versionName "2.0"
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

4.2 主界面activity_main.xml

新增textView2:“這是升級(jí)后的版本”

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.test.MainActivity">
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="檢查更新" />
    <TextView
        android:id="@+id/test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:textSize="30sp"
        android:textStyle="bold"
        android:textColor="@color/colorAccent"
        android:layout_centerInParent="true" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/test"
        android:textSize="30sp"
        android:textStyle="bold"
        android:textColor="@color/colorAccent"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="49dp"
        android:text="這是升級(jí)后的版本" />

</RelativeLayout>

4.3 MainActivity.java

主界面就一個(gè)button用于執(zhí)行版本檢測(cè)

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    private int currentVersionCode;
    private String currentVersionName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //獲取本地版本號(hào)和版本名
        currentVersionCode = VersionGetUtil.getVersionCode(MainActivity.this);
        currentVersionName = VersionGetUtil.getVersionName(MainActivity.this);
        Log.i(TAG, "onCreate: 版本號(hào):" + currentVersionCode + ",版本名:" + currentVersionName);

        final VersionUpdateUtil updateUtil = new VersionUpdateUtil(currentVersionCode,MainActivity.this);
              Button button = (Button)findViewById(R.id.button);
                button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.i(TAG,"開(kāi)始執(zhí)行版本判斷");
                updateUtil.getServerVersionCode();
            }
        });
        TextView textView = (TextView)findViewById(R.id.test);
        textView.setText("當(dāng)前版本: " + currentVersionName);
    }
}

4.4 測(cè)試

這個(gè)是重中之重,請(qǐng)務(wù)必保持手機(jī)與Tomcat處于同一局域網(wǎng)下邦投,可以采用360隨身wifi尼摹,否則會(huì)出現(xiàn)無(wú)法請(qǐng)求的錯(cuò)誤剂娄,直接提示IO異常阅懦。原因可以參考:Android真機(jī)連接本地部署的Tomcat問(wèn)題耳胎,將最新版本的APK放到Tomcat中怕午,開(kāi)始手機(jī)端的測(cè)試工作郁惜。具體的測(cè)試請(qǐng)參考視頻兆蕉。


五虎韵、總結(jié)

經(jīng)過(guò)連續(xù)幾天的摸索與整理缸废,總算把版本更新的代碼企量、文檔整理完畢梁钾,受益匪淺姆泻。當(dāng)然還存在著很多的不足:

  • 沒(méi)有提示移動(dòng)網(wǎng)絡(luò)環(huán)境及wifi環(huán)境的下載提示;
  • 沒(méi)有對(duì)動(dòng)態(tài)申請(qǐng)權(quán)限做進(jìn)一步的處理孝凌,如點(diǎn)擊“拒絕”蟀架,“不在詢問(wèn)”片拍,的處理捌省;
  • 沒(méi)有進(jìn)行自動(dòng)檢測(cè)版本的設(shè)置纲缓;
  • 類的封裝祝高,代碼不夠精簡(jiǎn)工闺;
  • SD卡外掛判斷還需要進(jìn)一步的整合斤寂;

知識(shí)的梳理遍搞,語(yǔ)言的整理溪猿,流程化的處理诊县,還需要進(jìn)一步的完善依痊,每天進(jìn)步一點(diǎn)點(diǎn)胸嘁。也希望各位developer共同探討,對(duì)于以上出現(xiàn)的各個(gè)問(wèn)題群井,希望不吝賜教。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末荐吉,一起剝皮案震驚了整個(gè)濱河市稍坯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌枪向,老刑警劉巖秘蛔,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件深员,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡绣的,警方通過(guò)查閱死者的電腦和手機(jī)屡江,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)罢洲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)惹苗,“玉大人鸽粉,你說(shuō)我怎么就攤上這事触机±苁祝” “怎么了蔬胯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵氛濒,是天一觀的道長(zhǎng)京景。 經(jīng)常有香客問(wèn)我确徙,道長(zhǎng)鄙皇,這世上最難降的妖魔是什么伴逸? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任违柏,我火速辦了婚禮香椎,結(jié)果婚禮上漱竖,老公的妹妹穿的比我還像新娘。我一直安慰自己畜伐,他們只是感情好馍惹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般万矾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上良狈,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天后添,我揣著相機(jī)與錄音,去河邊找鬼薪丁。 笑死遇西,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的严嗜。 我是一名探鬼主播粱檀,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼漫玄!你這毒婦竟也來(lái)了茄蚯?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤睦优,失蹤者是張志新(化名)和其女友劉穎渗常,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體汗盘,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡皱碘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了衡未。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡家凯,死狀恐怖缓醋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情绊诲,我是刑警寧澤送粱,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站掂之,受9級(jí)特大地震影響抗俄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜世舰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一动雹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧跟压,春花似錦胰蝠、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)躲庄。三九已至,卻和暖如春钾虐,著一層夾襖步出監(jiān)牢的瞬間噪窘,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工效扫, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留倔监,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓荡短,卻偏偏與公主長(zhǎng)得像丐枉,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子掘托,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,071評(píng)論 25 707
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理瘦锹,服務(wù)發(fā)現(xiàn),斷路器闪盔,智...
    卡卡羅2017閱讀 134,651評(píng)論 18 139
  • 因不勝抑郁癥折磨泪掀,而自行結(jié)束生命的男明星喬任梁听绳,近日他生前最好的朋友陳喬恩,在喬任梁追悼會(huì)舉行當(dāng)日异赫,發(fā)表了一篇長(zhǎng)微...
    破布鞋開(kāi)口笑閱讀 350評(píng)論 0 0
  • 2017年3月14日 我曾經(jīng)一萬(wàn)次在腦海中幻想過(guò)我成為記者的模樣椅挣,胸前掛著讓我從踏入大學(xué)校園就向往的記者證,背著象...
    果子__你好閱讀 2,230評(píng)論 0 1
  • 嗨量九,我是文案妞,你是哪一位呢?今天過(guò)的好嗎? 每晚22:00颂碧,想說(shuō)情話給你聽(tīng)荠列,讓我們一起卸下負(fù)累,休憩心靈…… 今...
    文案妞閱讀 443評(píng)論 0 1