学更好的别人,
做更好的自己。
——《微卡智享》
本文长度为4578字,预计阅读7分钟
前言
在开发初期,当Android端嵌入在硬件中,并且本地数据库单机业务逻辑挺多,往往要分析数据是否处理正常,需要直接从数据库中查看,这时我们一般都是将数据库拷贝到PC端后查看分析,在虚拟机中可以实现直接拷贝,但是真机无法直接访问Android端data/data/包名/databases的数据库路径,所以做了一个小Demo,通过网络将本地数据库文件传到PC端。
实现效果
Q1
为什么要做这个东西?
文章开头也说过,开发初期做业务测试的时候,往往查询功能还没做完,需要看数据库中业务逻辑做的是否对,数据是否正常,所以需要在数据库中查询。
主要我最近开发的是在硬件设备,装的Android平板控制,要求在断网情况下单机也能运行,所以基本的业务逻辑包括数据的保存都在本地处理,后台定时通讯上传数据,除了文章开头说的开发初期可以方便传上来数据库来分析,后期也是想通过这个方式实现本地的数据库备份。于是就有了这篇文章和Demo,文章最后还是会列出源码地址,想研究的小伙伴也可直接下载。
实现方式
微卡智享
上图做了一个简单的流程设计图,还是很简单的,中间的数据库文件传输采用NanoMsg通讯,C#端用用的Nuget包中的NNanoMsg,Android端采用的我自己封装的VNanoMsg。有关NanoMsg相关的文章可以看《NanoMsg框架|NanoMsg的简介》及相关的一系列文章。
Android:Room+LiveEventBus+VNanoMsg
allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
//VNanoMsg通讯库
implementation 'com.github.Vaccae:VNanoMsg:1.0.4'
//LiveEventBus
api 'io.github.jeremyliao:live-event-bus-x:1.8.0'
//Room
def room_version = "2.2.5"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
C#:NNanoMsg
要在Nuget包中添加NNanoMsg
上图中是Android端Demo的所有类文件,主要多是的Room的类,像实体的创建,Dao的使用,还有数据库的创建等。
01
Room数据库创建
BaseDao
首先定义了一个BaseDao,这样创建Dao时直接继承自BaseDao不用再写Insert,update,delete的函数了。
package com.vaccae.roomdemo.bean
import androidx.room.*
@Dao
interface BaseDao<T> {
@Transaction
@Insert
fun add(vararg arr:T)
@Transaction
@Insert
fun add(arr:ArrayList<T>)
@Transaction
@Update
fun upd(vararg arr:T)
@Transaction
@Update
fun upd(arr:ArrayList<T>)
@Transaction
@Delete
fun del(vararg arr:T)
@Transaction
@Delete
fun del(arr:ArrayList<T>)
}
Productitem
这里直接用productitem这个文件,里面@Entity和@Dao都创建在一起了
package com.vaccae.roomdemo.bean
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Query
@Entity(tableName = "Body", primaryKeys = ["Code", "BarCode"])
class ProductItem {
@ColumnInfo(name = "Code")
lateinit var code: String
@ColumnInfo(name = "BarCode")
lateinit var barcode: String
@ColumnInfo(name = "Qty")
var qty = 0
}
@Dao
interface ProductItemDao : BaseDao<ProductItem> {
@Query("select * from Body")
fun getAll(): List<ProductItem>
}
AppDataBase
AppDataBase是数据库的整个创建,数据库升级都在里面,其中可以通过DbUtil类调用实现,里面的testdb是数据库名,可以外部直接定义。
package com.vaccae.roomdemo.bean
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
/**
* 作者:Vaccae
* 邮箱:3657447@qq.com
* 创建时间:2020-04-14 14:29
* 功能模块说明:
*/
@Database(entities = [Product::class,ProductItem::class], version = 2)
abstract class AppDataBase : RoomDatabase() {
abstract fun ProductDao(): ProductDao
abstract fun ProductItemDao():ProductItemDao
}
class DbUtil {
//数据库升级
var migration1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
val sql="CREATE TABLE if not exists Body(Code TEXT NOT NULL ," +
"BarCode TEXT NOT NULL,Qty INTEGER NOT NULL,PRIMARY KEY(Code,BarCode))"
database.execSQL(sql)
}
}
//创建单例
private var INSTANCE: AppDataBase? = null
fun getDatabase(context: Context): AppDataBase {
if (INSTANCE == null) {
synchronized(lock = AppDataBase::class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.applicationContext,
AppDataBase::class.java, "testdb"
)
.allowMainThreadQueries()//允许在主线程查询数据
.addMigrations(migration1_2)//数据库升级时执行
.fallbackToDestructiveMigration()
.build()
}
}
}
return INSTANCE!!
}
}
外部的调用方式
private fun CreateProductItem() {
//定义明细列表
val itemlist = ArrayList<ProductItem>()
//加载AppDataBase
val db = DbUtil().getDatabase(this);
//显示所有Product的明细
val list = db.ProductDao().getAll()
list.forEach {
for (i in 1..3) {
val item = ProductItem()
item.code = it.code
item.barcode = it.code + i.toString()
item.qty = 1
itemlist.add(item)
}
}
db.ProductItemDao().add(itemlist)
//显示明细
val getlist = db.ProductItemDao().getAll()
tvshow.text = ""
getlist.forEach {
tvshow.append(
it.code + " " + it.barcode
+ " " + it.qty + "\r\n"
)
}
}
02
获取本机IP地址
Demo是用Android端作为通讯的服务器,所以需要获取到本机的IP地址,用于VNanoMsg绑定服务端口,所以写了一个获取本地IP地址的类PhoneAdrUtil。
package com.vaccae.roomdemo
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.wifi.WifiManager
import android.os.Build
import androidx.annotation.RequiresApi
import java.net.Inet4Address
import java.net.InetAddress
import java.net.NetworkInterface
import java.util.*
/**
* 作者:Vaccae
* 邮箱:3657447@qq.com
* 创建时间:13:14
* 功能模块说明:
*/
class PhoneAdrUtil {
companion object {
fun getIpAdr(context: Context): String? {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT < 23) {
val networiinfo = cm.activeNetworkInfo
networiinfo?.let {
if (it.type == ConnectivityManager.TYPE_WIFI) {
return getWIfiIpAdr(context)
} else if (it.type == ConnectivityManager.TYPE_MOBILE) {
return getMobileIpAdr()
}
}
} else {
val network = cm.activeNetwork
network?.let { it ->
val networkCapabilities = cm.getNetworkCapabilities(it)
networkCapabilities?.let { item ->
if (item.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
return getWIfiIpAdr(context)
}else if (item.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
return getMobileIpAdr()
}
}
}
}
return null
}
private fun getMobileIpAdr():String {
var ipstr = ""
val en: Enumeration<NetworkInterface> = NetworkInterface.getNetworkInterfaces()
while (en.hasMoreElements()) {
val intf: NetworkInterface = en.nextElement()
val enumIpAddr: Enumeration<InetAddress> = intf.inetAddresses
while (enumIpAddr.hasMoreElements()) {
val inetAddress: InetAddress = enumIpAddr.nextElement()
if (!inetAddress.isLoopbackAddress && inetAddress is Inet4Address) {
ipstr = inetAddress.hostAddress.toString()
return ipstr
}
}
}
return ipstr
}
private fun getWIfiIpAdr(context: Context):String{
val wifiManager =
context.getSystemService(Context.WIFI_SERVICE) as WifiManager
val wifiinfo = wifiManager.connectionInfo
return ChangeIP2String(wifiinfo.ipAddress)
}
private fun ChangeIP2String(ip: Int): String {
return "" + (ip and 0xFF) + "." +
((ip shr 8) and 0xFF) + "." +
((ip shr 16) and 0xFF) + "." +
(ip shr 24 and 0xFF);
}
}
}
03
VNanoMsg数据通讯
不用VNanoMsg可以自己写Socket通讯或是别的,我自己用NanoMsg主要原因是轻量,方便,也有多种模式,像订阅,消息队列等。这次用的Pair模式是是一对一的,服务端和客户端谁先启动都可以,不用像传统的Socket必须服务端先启动,客户端再连接,而且Pair模式下send是不阻塞,recv是阻塞的,并且通讯时多大的包recv可以一次性全部接收完,Demo中我就是把文件整个读完后一起send的,然后一个Recv全部接收完了,完全不用自己去写循环读取和判断是否接收完等。
package com.vaccae.roomdemo
import android.R.attr
import com.jeremyliao.liveeventbus.LiveEventBus
import com.vaccae.vnanomsg.NNPAIR
import kotlinx.coroutines.*
import java.io.File
import java.io.FileInputStream
import java.lang.Exception
import android.R.attr.path
import android.R.string.no
/**
* 作者:Vaccae
* 邮箱:3657447@qq.com
* 创建时间:09:31
* 功能模块说明:
*/
object VNanoNNPairUtils {
private var mNNPAIR: NNPAIR? = null
private var isOpenListen = false;
fun IsRecvListen(): Boolean {
return isOpenListen
}
fun getInstance(): VNanoNNPairUtils {
mNNPAIR ?: run {
synchronized(VNanoNNPairUtils::class.java) {
mNNPAIR = NNPAIR()
}
}
return VNanoNNPairUtils
}
fun Bind(ipadr: String): VNanoNNPairUtils {
mNNPAIR?.let {
//var ipstr = "tcp://192.168.10.155:8157"
it.bind(ipadr)
}
return VNanoNNPairUtils
}
fun UnBind() {
mNNPAIR?.let {
it.shutdownbind()
}
}
private fun byteMerger(bt1: ByteArray, bt2: ByteArray): ByteArray {
val bt3 = ByteArray(bt1.size + bt2.size)
System.arraycopy(bt1, 0, bt3, 0, bt1.size)
System.arraycopy(bt2, 0, bt3, bt1.size, bt2.size)
return bt3
}
fun Send(file: File) {
mNNPAIR?.let {
var filebytearray = ByteArray(0)
var len = 0;
var byteArray = ByteArray(1024)
val inputStream: FileInputStream = FileInputStream(file)
//判断是否读到文件末尾
while (inputStream.read(byteArray).also { len = it } != -1) {
//将文件循环写入fielbytearray
filebytearray = byteMerger(filebytearray, byteArray)
}
it.send(filebytearray)
}
}
fun Send(byte: ByteArray) {
mNNPAIR?.let { it.send(byte) }
}
fun StartRecvListen() {
mNNPAIR?.let {
isOpenListen = true;
val recvScope = CoroutineScope(Job())
recvScope.launch {
try {
withContext(Dispatchers.IO) {
while (isOpenListen) {
delay(50)
val recvstr = it.recv()
recvstr?.let {
LiveEventBus.get("NNPair", String::class.java)
.postOrderly(it)
}
}
}
} catch (e: Exception) {
throw e
}
}
}
}
fun StopRecvListen() {
isOpenListen = false;
}
}
客户端比较简单,就是输入IP地址进行发送接收,然后保存到本地即可。
01
FileHelper文件保存
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace nanomsgclient
{
public class FileHelper
{
/// <summary>
/// 将文件转换成byte[]数组
/// </summary>
/// <param name="fileUrl">文件路径文件名称</param>
/// <returns>byte[]数组</returns>
public static byte[] FileToByte(string fileUrl)
{
try
{
using (FileStream fs = new FileStream(fileUrl, FileMode.Open, FileAccess.Read))
{
byte[] byteArray = new byte[fs.Length];
fs.Read(byteArray, 0, byteArray.Length);
return byteArray;
}
}
catch
{
return null;
}
}
/// <summary>
/// 将byte[]数组保存成文件
/// </summary>
/// <param name="byteArray">byte[]数组</param>
/// <param name="fileName">保存至硬盘的文件路径</param>
/// <returns></returns>
public static bool ByteToFile(byte[] byteArray, string fileName)
{
bool result = false;
try
{
using (FileStream fs = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write))
{
fs.Write(byteArray, 0, byteArray.Length);
result = true;
}
}
catch
{
result = false;
}
return result;
}
}
}
02
Form界面
简单的窗体布局,整个代码也写到了一起
using nanomsgclient;
using NNanomsg.Protocols;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace NanoRecvDataBase
{
public partial class Form1 : Form
{
private PairSocket pairSocket = null;
public Form1()
{
InitializeComponent();
CheckForIllegalCrossThreadCalls = false;
_tbMsg = tbMsg;
}
#region 文本框操作
//定义文本框
private static TextBox _tbMsg;
//定义Action
private Action<string> TextShowAction = new Action<string>(TextShow);
//定义更新UI函数
private static void TextShow(string sMsg)
{
//当文本行数大于500后清空
if (_tbMsg.Lines.Length > 500)
{
_tbMsg.Clear();
}
string ShowMsg = DateTime.Now + " " + sMsg + "\r\n";
_tbMsg.AppendText(ShowMsg);
//让文本框获取焦点
_tbMsg.Focus();
//设置光标的位置到文本尾
_tbMsg.Select(_tbMsg.TextLength, 0);
//滚动到控件光标处
_tbMsg.ScrollToCaret();
}
#endregion
private void btnRecv_Click(object sender, EventArgs e)
{
try
{
if (pairSocket == null)
{
pairSocket = new PairSocket();
var ipadr = tbipadr.Text;
TextShow("要连接的IP地址为:" + ipadr);
pairSocket.Connect(ipadr);
}
var res = new Task<string>(() =>
{
pairSocket.Send(Encoding.UTF8.GetBytes("getdbnames"));
while (true)
{
Thread.Sleep(50);
//接收数据
byte[] buffer = pairSocket.Receive();
if (buffer != null)
{
string recvstr = Encoding.UTF8.GetString(buffer);
return recvstr;
}
}
});
res.Start();
var getdbnum = res.Result;
var dbnames = getdbnum.Split('#');
TextShow("接收到数据库文件个数:" + dbnames.Length);
var resfile = new Task<String>(() =>
{
for (int i = 0; i < dbnames.Length; ++i)
{
string filename = dbnames[i];
pairSocket.Send(Encoding.UTF8.GetBytes("#" + filename));
while (true)
{
Thread.Sleep(50);
//接收数据
byte[] buffer = pairSocket.Receive();
if (buffer != null)
{
var pathfile = "D:\\DataBase\\" + filename;
FileHelper.ByteToFile(buffer, pathfile);
TextShow(pathfile + "文件传输成功");
break;
}
}
}
return "传输完成";
});
resfile.Start();
TextShow(resfile.Result);
}
catch (Exception ex)
{
TextShow(ex.Message);
}
}
}
}
以上就是一个简单的Android将本地Sqlite数据库传输到PC端的程序就实现了。
https://github.com/Vaccae/TransAndroidSqliteDBDemo.git