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