写在前面:本文大约有2.5k字,可能需要一刻钟阅读时间。
Android在开发调试过程中,查看/修改app的数据库是比较麻烦的,一般有以下几种方式:
总之,以上几种方式是一般开发者使用的,操作稍显麻烦!但是我们呢,可以使用Android Debug Database就比较方便了,直接在浏览器输入手机ip地址+端口号就可以对数据库进行CRUD,并且是实时生效的!浏览器页面如下截图:
我们看github上,作者如是说:
(1)在app的build.gradle中添加如下依赖:
debugImplementation 'com.amitshekhar.android:debug-db:1.0.4'
(2)运行app,查看logcat并找到如下信息,在浏览器中打开地址即可:
注:这个浏览器可以是电脑上的浏览器——和手机连在同一个局域网/同一个wifi,或者直接在手机浏览器打开也可以!
注:话说在前面,这一部分主要涉及源码分析,可能会比较繁琐一些。
虽然这个工具用的很爽,但是不知道你有没有想过如下问题:
那接下来我们就一起看看Android Debug Database的源码,分析一下这个过程,达到更好的理解和使用这个工具。
(1)为什么要用局域网?用互联网有什么不好的吗?
其实这个原因很简单的,我们看到浏览器中输入的地址是,手机端ip+app设置的port,只有在局域网或者手机本身可以访问这个地址,而互联网是访问不到的。另外想想,如果互联网上可以修改,那岂不是很可怕!(比如,当你的app正在运行,而远在千里之外的另一人修改了你app的数据库,那很可能你的app就会崩溃或者数据泄露等等)
(2)为什么在gradle文件里implementation一下相应的库就可以直接使用,不需要额外的初始化和配置?
这个就不得不说一下这些开源库的优秀做法了,使用android四大组件之一ContentProvider初始化library。
首先,平常引用一些第三方库时,一般需要在Application中初始化一下并且传一个Context进去,但是如果忘记初始化就会出现NullPointerException,或者如果需要初始化很多库时,代码就变得比较庞大了。所以通过ContentProvider初始化第三方库是值得采取的一种方式。
实现方式如下:
注:要设置一个authorities,这个authorities相当于ContentProvider的标识,是不能重复的。为了保证不重复,最好不要硬编码,而是使用这种方式:${applicationId}。
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" ? ' ' : "";
if(dbName.indexOf("journal") == -1 && dbName.indexOf("-wal") == -1 && dbName.indexOf("-shm") == -1){
$("#db-list").append("" + dbName + dbAttribute + "");
if(!isSelectionDone){
isSelectionDone = true;
$('#db-list').find('a').trigger('click');
}
}
}
}});
}
在app.js文件中我们可以看到,文件开头就请求getDBList,在getDBList使用ajax异步请求数据(即浏览器端请求手机端的数据),最终会走到手机端的getDBListResponse()方法,完成浏览器端和手机端的完美交互!
【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/