暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

实现Android本地Sqlite数据库网络传输到PC端

微卡智享 2021-09-23
264

学更好的别人,

做更好的自己。

——《微卡智享》





本文长度为4578,预计阅读7分钟




前言



在开发初期,当Android端嵌入在硬件中,并且本地数据库单机业务逻辑挺多,往往要分析数据是否处理正常,需要直接从数据库中查看,这时我们一般都是将数据库拷贝到PC端后查看分析,在虚拟机中可以实现直接拷贝,但是真机无法直接访问Android端data/data/包名/databases的数据库路径,所以做了一个小Demo,通过网络将本地数据库文件传到PC端。





实现效果








Q1

为什么要做这个东西?

文章开头也说过,开发初期做业务测试的时候,往往查询功能还没做完,需要看数据库中业务逻辑做的是否对,数据是否正常,所以需要在数据库中查询。



主要我最近开发的是在硬件设备,装的Android平板控制,要求在断网情况下单机也能运行,所以基本的业务逻辑包括数据的保存都在本地处理,后台定时通讯上传数据,除了文章开头说的开发初期可以方便传上来数据库来分析,后期也是想通过这个方式实现本地的数据库备份。于是就有了这篇文章和Demo,文章最后还是会列出源码地址,想研究的小伙伴也可直接下载。



实现方式

微卡智享


流程设计


上图做了一个简单的流程设计图,还是很简单的,中间的数据库文件传输采用NanoMsg通讯,C#端用用的Nuget包中的NNanoMsg,Android端采用的我自己封装的VNanoMsg。有关NanoMsg相关的文章可以看《NanoMsg框架|NanoMsg的简介》及相关的一系列文章。


Demo中使用的库

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


    上图中是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;
                }




                }


                C#桌面端Demo


                客户端比较简单,就是输入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




                    扫描二维码

                    获取更多精彩

                    微卡智享




                    「 往期文章 」


                    Android通讯库VNanoMsg的1.0.4发布

                    Android使用BaseSectionQuickAdapter动态生成不规则宫格

                    Android之任务调度WorkManager和JobSchedule的使用




                    文章转载自微卡智享,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论