Android Debug Database原理简析

写在前面:本文大约有2.5k字,可能需要一刻钟阅读时间。

1、Android Debug Database方式与其他方式查看/修改数据库?

Android在开发调试过程中,查看/修改app的数据库是比较麻烦的,一般有以下几种方式:

  • 将手机app中的SQLite数据库pull到电脑,通过电脑端的软件(如SQLite Expert Professional)打开这个数据库,可以执行相关的CRUD语句,然后push到手机app中。
  • Root手机,在手机上安装RE文件管理器,进入应用程序的包下,找到app数据库的文件,然后再查看数据库(亲测:好像不能修改,以文本方式编辑后保存会失败)。
  • Android Studio有相关的插件,方便操作,但是有的需要收费,使用起来也不是很爽。

总之,以上几种方式是一般开发者使用的,操作稍显麻烦!但是我们呢,可以使用Android Debug Database就比较方便了,直接在浏览器输入手机ip地址+端口号就可以对数据库进行CRUD,并且是实时生效的!浏览器页面如下截图:

2、Android Debug Database能做什么?

我们看github上,作者如是说:

  • 可以查看app应用中所有的数据库及其数据
  • 可以查看app应用中所有的shared preferences及其数据
  • 可以使用sql语句对数据库进行CRUD
  • 可以直接对数据库进行CRUD
  • 可以直接对shared preferences进行编辑
  • 可以将数据库下载下来
  • ...

3、Android Debug Database使用?

(1)在app的build.gradle中添加如下依赖:

debugImplementation 'com.amitshekhar.android:debug-db:1.0.4'

(2)运行app,查看logcat并找到如下信息,在浏览器中打开地址即可:

注:这个浏览器可以是电脑上的浏览器——和手机连在同一个局域网/同一个wifi,或者直接在手机浏览器打开也可以!

4、Android Debug Database原理简析

注:话说在前面,这一部分主要涉及源码分析,可能会比较繁琐一些。

虽然这个工具用的很爽,但是不知道你有没有想过如下问题:

  • 为什么要用局域网?用互联网有什么不好的吗?
  • 为什么在gradle文件里implementation一下相应的库就可以直接使用,不需要额外的初始化和配置?
  • 为什么要使用浏览器?
  • 浏览器端的数据和手机端的数据是怎样交互的?

那接下来我们就一起看看Android Debug Database的源码,分析一下这个过程,达到更好的理解和使用这个工具。

(1)为什么要用局域网?用互联网有什么不好的吗?

其实这个原因很简单的,我们看到浏览器中输入的地址是,手机端ip+app设置的port,只有在局域网或者手机本身可以访问这个地址,而互联网是访问不到的。另外想想,如果互联网上可以修改,那岂不是很可怕!(比如,当你的app正在运行,而远在千里之外的另一人修改了你app的数据库,那很可能你的app就会崩溃或者数据泄露等等)

(2)为什么在gradle文件里implementation一下相应的库就可以直接使用,不需要额外的初始化和配置?

这个就不得不说一下这些开源库的优秀做法了,使用android四大组件之一ContentProvider初始化library。

首先,平常引用一些第三方库时,一般需要在Application中初始化一下并且传一个Context进去,但是如果忘记初始化就会出现NullPointerException,或者如果需要初始化很多库时,代码就变得比较庞大了。所以通过ContentProvider初始化第三方库是值得采取的一种方式。

实现方式如下:

  • debug-db的AndroidManifest中注册一个ContentProvider:
<application>
    <provider
        android:authorities="${applicationId}.DebugDBInitProvider"
        android:exported="false"
        android:enabled="true"
        android:name=".DebugDBInitProvider" />
</application>

注:要设置一个authorities,这个authorities相当于ContentProvider的标识,是不能重复的。为了保证不重复,最好不要硬编码,而是使用这种方式:${applicationId}。

  • DebugDBInitProvider继承自ContentProvider,就只是在onCreate方法里对DebugDB进行初始化。代码如下:
package com.amitshekhar;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;

/**
 * Created by amitshekhar on 16/11/16.
 */

public class DebugDBInitProvider extends ContentProvider {

    public DebugDBInitProvider() {
    }

    @Override
    public boolean onCreate() {
        DebugDB.initialize(getContext());
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        return null;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public void attachInfo(Context context, ProviderInfo providerInfo) {
        if (providerInfo == null) {
            throw new NullPointerException("DebugDBInitProvider ProviderInfo cannot be null.");
        }
        // So if the authorities equal the library internal ones, the developer forgot to set his applicationId
        if ("com.amitshekhar.DebugDBInitProvider".equals(providerInfo.authority)) {
            throw new IllegalStateException("Incorrect provider authority in manifest. Most likely due to a "
                    + "missing applicationId variable in application\'s build.gradle.");
        }
        super.attachInfo(context, providerInfo);
    }

}

原理:我们都知道,ContentProvider的onCreate的调用时机介于Application的attachBaseContext和onCreate之间(即:ContentProvider的onCreate要先于Application的onCreate而执行),把初始化的逻辑放到库内部,让调用方完全不需要在Application里去进行初始化了,十分方便。

坏处:不过这种方法的坏处就是,因为所有的ContentProvider都是运行在主线程中,也就意味着所有的初始化都会在主线程完成。如果你希望要异步的初始化一些库,那么可以选择还是手动地在某个地方进行初始化。

(3)为什么要使用浏览器?

其实这里的浏览器只是一个中间介质,像将数据库导出到电脑的文件也需要工具打开,或者re文件管理器都是,浏览器只是更方便我们进行操作的一种方式。

(4)浏览器端的数据和手机端的数据是怎样交互的?

A、交互流程如下:

B、对于手机端,初始化时DebugDB给app开启了一个线程clientServer,不断的处理浏览器发过来的请求(Socket形式),包括解析浏览器发过来的route、处理数据库请求、发送处理结果给浏览器:

public static void initialize(Context context) {
    int portNumber;

    try {
        portNumber = Integer.valueOf(context.getString(R.string.PORT_NUMBER));
    } catch (NumberFormatException ex) {
        Log.e(TAG, "PORT_NUMBER should be integer", ex);
        portNumber = DEFAULT_PORT;
        Log.i(TAG, "Using Default port : " + DEFAULT_PORT);
    }

    clientServer = new ClientServer(context, portNumber);
    clientServer.start();
    addressLog = NetworkUtils.getAddressLog(context, portNumber);
    Log.d(TAG, addressLog);
}

a、portNumber默认端口是8080,如果要修改,可以在app build.gradle文件下buildTypes 内添加如下内容(8081就是可以修改的端口号):

debug {
    resValue("string", "PORT_NUMBER", "8081")
}

b、clientServer.start();就在一直循环接收socket:

@Override
public void run() {
    try {
        mServerSocket = new ServerSocket(mPort);
        while (mIsRunning) {
            Socket socket = mServerSocket.accept();
            mRequestHandler.handle(socket);
            socket.close();
        }
    } catch (SocketException e) {
        // The server was stopped; ignore.
    } catch (IOException e) {
        Log.e(TAG, "Web server error.", e);
    } catch (Exception ignore) {
        Log.e(TAG, "Exception.", ignore);
    }
}

c、我们可以看到,在logcat打印的那句日志,就是在这里生成的:

public static String getAddressLog(Context context, int port) {
    WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
    int ipAddress = wifiManager.getConnectionInfo().getIpAddress();
    @SuppressLint("DefaultLocale")
    final String formattedIpAddress = String.format("%d.%d.%d.%d",
            (ipAddress & 0xff),
            (ipAddress >> 8 & 0xff),
            (ipAddress >> 16 & 0xff),
            (ipAddress >> 24 & 0xff));
    return "Open http://" + formattedIpAddress + ":" + port + " in your browser";
}

d、mRequestHandler.handle(socket);下面是处理浏览器请求的主要逻辑,route是对socket进行解析后得到的(当浏览器进行CRUD时,这里就能接到请求,进行解析并真正的对数据库进行CRUD):

if (route.startsWith("getDbList")) {
    final String response = getDBListResponse();
    bytes = response.getBytes();
} else if (route.startsWith("getAllDataFromTheTable")) {
    final String response = getAllDataFromTheTableResponse(route);
    bytes = response.getBytes();
} else if (route.startsWith("getTableList")) {
    final String response = getTableListResponse(route);
    bytes = response.getBytes();
} else if (route.startsWith("addTableData")) {
    final String response = addTableDataAndGetResponse(route);
    bytes = response.getBytes();
} else if (route.startsWith("updateTableData")) {
    final String response = updateTableDataAndGetResponse(route);
    bytes = response.getBytes();
} else if (route.startsWith("deleteTableData")) {
    final String response = deleteTableDataAndGetResponse(route);
    bytes = response.getBytes();
} else if (route.startsWith("query")) {
    final String response = executeQueryAndGetResponse(route);
    bytes = response.getBytes();
} else if (route.startsWith("downloadDb")) {
    bytes = Utils.getDatabase(mSelectedDatabase, mDatabaseFiles);
} else {
    bytes = Utils.loadContent(route, mAssets);
}

e、getDBListResponse()方法应该会最先执行(原因在后面部分会提到),它主要是获取到app内部单个或多个数据库的名称、路径和密码,并返回给浏览器端。然后再根据浏览器端的请求操作进行对应的逻辑处理。至于更深入的代码部分,我就不在此处贴出了,有兴趣的小伙伴可以自行下载源码分析!

C、对于浏览器端,我们知道,在源码assets文件夹下,存放着对应的html网页和js、css等文件,它们就承担着和用户交互、与手机端数据库交互等操作,文件列表如下图所示:

index.xml就是主页,而且也只有这一个页面!也就是在浏览器中输入地址后,展示的页面。

我们看到,html页面中主要是布局设计,然后是js脚本,就是app.js文件,如下:

$( document ).ready(function() {
            getDBList();
            $("#query").keypress(function(e){
                if(e.which == 13) {
                    queryFunction();
                }
            });
...
        });

...

        function getDBList() {

            $.ajax({url: "getDbList", success: function(result){

                result = JSON.parse(result);
                var dbList = result.rows;
                $('#db-list').empty();
                var isSelectionDone = false;
                for(var count = 0; count < dbList.length; count++){
                    var dbName = dbList[count][0];
                    var isEncrypted = dbList[count][1];
                    var isDownloadable = dbList[count][2];
                    var dbAttribute = isEncrypted == "true" ? ' <span class="glyphicon glyphicon-lock" aria-hidden="true" style="color:blue"></span>' : "";
                    if(dbName.indexOf("journal") == -1 && dbName.indexOf("-wal") == -1 && dbName.indexOf("-shm") == -1){
                        $("#db-list").append("<a href='#' id=" + dbName + " class='list-group-item' onClick='openDatabaseAndGetTableList(\""+ dbName + "\", \""+ isDownloadable + "\");'>" + dbName + dbAttribute + "</a>");
                        if(!isSelectionDone){
                            isSelectionDone = true;
                            $('#db-list').find('a').trigger('click');
                        }
                    }
                }

            }});

        }

在app.js文件中我们可以看到,文件开头就请求getDBList,在getDBList使用ajax异步请求数据(即浏览器端请求手机端的数据),最终会走到手机端的getDBListResponse()方法,完成浏览器端和手机端的完美交互!

5、参考:

【1】https://github.com/amitshekhariitbhu/Android-Debug-Database

【2】https://www.jianshu.com/p/89ccae3e590b

【3】http://zjutkz.net/2017/09/11/%E4%B8%80%E4%B8%AA%E5%B0%8F%E6%8A%80%E5%B7%A7%E2%80%94%E2%80%94%E4%BD%BF%E7%94%A8ContentProvider%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BD%A0%E7%9A%84Library/

猜你喜欢

转载自blog.csdn.net/Agg_bin/article/details/86495897