Android AIDL淺析及非同步使用

来源:https://www.cnblogs.com/cspecialy/archive/2018/05/20/9062384.html
-Advertisement-
Play Games

AIDL:Android Interface Definition Language,即 Android 介面定義語言。 AIDL 是什麼 Android 系統中的進程之間不能共用記憶體,因此,需要提供一些機制在不同進程之間進行數據通信。 為了使其他的應用程式也可以訪問本應用程式提供的服務,Andro ...


AIDL:Android Interface Definition Language,即 Android 介面定義語言。

AIDL 是什麼

Android 系統中的進程之間不能共用記憶體,因此,需要提供一些機制在不同進程之間進行數據通信。

為了使其他的應用程式也可以訪問本應用程式提供的服務,Android 系統採用了遠程過程調用(Remote Procedure Call,RPC)方式來實現。與很多其他的基於 RPC 的解決方案一樣,Android 使用一種介面定義語言(Interface Definition Language,IDL)來公開服務的介面。我們知道 Android 四大組件中的 3 種(Activity、BroadcastReceiver和ContentProvider)都可以進行跨進程訪問,另外一種 Android 組件 Service 同樣可以。因此,可以將這種可以跨進程訪問的服務稱為 AIDL(Android Interface Definition Language)服務。

在介紹 AIDL 的使用以及其它特性前,我們先來瞭解下 AIDL 的核心——Binder

Android 的 Binder 機制淺析

看過一些關於 Binder 的文章,總得來說 Binder 機制的底層實現很複雜,相當複雜,要完全搞清楚,得花大量的時間。從某種角度來說,個人覺得,對於 Binder,我們只需要瞭解其上層原理以及使用方法即可。

直觀來看,從代碼的角度來說,Binder 是 Android 系統源碼中的一個類,它實現了 IBinder 介面;從 IPC 角度來說,Binder 是 Android 中的一種跨進程通信方式;從 Android Framework 角度來講,Binder 是 ServiceManager 連接各種 Manager(ActivityManager、WindowManager 等等)和相應 ManagerService 的橋梁;從 Android 應用層來說,Binder 是客戶端和服務端進行通信的媒介,當 bindService 的時候,服務端會返回一個包含了服務端業務調用的 Binder 對象,通過這個 Binder 對象,客戶端就可以和服務端進行通信,這裡的服務包括普通服務和基於 AIDL 的服務。

接下來,我們通過一個 AIDL 示例,來分析 Binder 的工作機制。在工程目錄中新建一個名為 aidl 的 package,然後新建 Book.Java、Book.aidl(創建此文件時,as 會提示已存在,需要先用其它命令,創建成功後再重命名為 Book.aidl )和 IBookManager.aidl,代碼如下:

// Book.java
package com.cy.ipcsample.aidl;

import android.os.Parcel;
import android.os.Parcelable;

/**
 * 數據類
 * @author cspecialy
 * @version v1.0.0
 * @date 2018/5/14 21:38
 */
public class Book implements Parcelable {

    public int bookId;
    public String bookName;

    public Book(int bookId, String bookName) {
        this.bookId = bookId;
        this.bookName = bookName;
    }

    protected Book(Parcel in) {
        bookId = in.readInt();
        bookName = in.readString();
    }

    public static final Creator<Book> CREATOR = new Creator<Book>() {
        @Override
        public Book createFromParcel(Parcel in) {
            return new Book(in);
        }

        @Override
        public Book[] newArray(int size) {
            return new Book[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeInt(bookId);
        parcel.writeString(bookName);
    }

    @Override
    public String toString() {
        return "Book{" +
                "bookId=" + bookId +
                ", bookName='" + bookName + '\'' +
                '}';
    }
}
1 // Book.aidl
2 package com.cy.ipcsample.aidl;
3 
4 parcelable Book;/
 1 // IBookManager.aidl
 2 package com.cy.ipcsample.aidl;
 3 
 4 import com.cy.ipcsample.aidl.Book;
 5 import com.cy.ipcsample.aidl.IOnNewBookArrivedListener;
 6 
 7 interface IBookManager {
 8     List<Book> getBookList();
 9     void addBook(in Book book);
10     void registerListener(IOnNewBookArrivedListener listener);
11     void unRegisterListener(IOnNewBookArrivedListener listener);
12 }
創建好三個文件之後,編譯一下,as 會在app/build/generated/source/aidl/debug 目錄下的 com.cy.ipcsample 包中生成一個名為 IBookManager.Java 的類,如下圖所示:

 

這是系統生成的 Binder 類,接下來我們要利用這個類來分析 Binder 的工作原理。其代碼如下:(生成的代碼格式很亂,可以格式化代碼之後看)

  1 /*
  2  * This file is auto-generated.  DO NOT MODIFY.
  3  * Original file: G:\\Android\\Github\\Bugly-Android-Demo\\sample\\ipcsample\\src\\main\\aidl\\com\\cy\\ipcsample
  4  * \\aidl\\IBookManager.aidl
  5  */
  6 package com.cy.ipcsample.aidl;
  7 
  8 public interface IBookManager extends android.os.IInterface {
  9     public java.util.List<com.cy.ipcsample.aidl.Book> getBookList() throws android.os.RemoteException;
 10 
 11     public void addBook(com.cy.ipcsample.aidl.Book book) throws android.os.RemoteException;
 12 
 13     /** Local-side IPC implementation stub class. */
 14     public static abstract class Stub extends android.os.Binder implements com.cy.ipcsample.aidl.IBookManager {
 15         static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
 16         static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
 17         private static final java.lang.String DESCRIPTOR = "com.cy.ipcsample.aidl.IBookManager";
 18 
 19         /** Construct the stub at attach it to the interface. */
 20         public Stub() {
 21             this.attachInterface(this, DESCRIPTOR);
 22         }
 23 
 24         /**
 25          * Cast an IBinder object into an com.cy.ipcsample.aidl.IBookManager interface,
 26          * generating a proxy if needed.
 27          */
 28         public static com.cy.ipcsample.aidl.IBookManager asInterface(android.os.IBinder obj) {
 29             if ((obj == null)) {
 30                 return null;
 31             }
 32             android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
 33             if (((iin != null) && (iin instanceof com.cy.ipcsample.aidl.IBookManager))) {
 34                 return ((com.cy.ipcsample.aidl.IBookManager) iin);
 35             }
 36             return new com.cy.ipcsample.aidl.IBookManager.Stub.Proxy(obj);
 37         }
 38 
 39         @Override
 40         public android.os.IBinder asBinder() {
 41             return this;
 42         }
 43 
 44         @Override
 45         public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws 
 46                 android.os.RemoteException {
 47             switch (code) {
 48                 case INTERFACE_TRANSACTION: {
 49                     reply.writeString(DESCRIPTOR);
 50                     return true;
 51                 }
 52                 case TRANSACTION_getBookList: {
 53                     data.enforceInterface(DESCRIPTOR);
 54                     java.util.List<com.cy.ipcsample.aidl.Book> _result = this.getBookList();
 55                     reply.writeNoException();
 56                     reply.writeTypedList(_result);
 57                     return true;
 58                 }
 59                 case TRANSACTION_addBook: {
 60                     data.enforceInterface(DESCRIPTOR);
 61                     com.cy.ipcsample.aidl.Book _arg0;
 62                     if ((0 != data.readInt())) {
 63                         _arg0 = com.cy.ipcsample.aidl.Book.CREATOR.createFromParcel(data);
 64                     } else {
 65                         _arg0 = null;
 66                     }
 67                     this.addBook(_arg0);
 68                     reply.writeNoException();
 69                     return true;
 70                 }
 71             }
 72             return super.onTransact(code, data, reply, flags);
 73         }
 74 
 75         private static class Proxy implements com.cy.ipcsample.aidl.IBookManager {
 76             private android.os.IBinder mRemote;
 77 
 78             Proxy(android.os.IBinder remote) {
 79                 mRemote = remote;
 80             }
 81 
 82             public java.lang.String getInterfaceDescriptor() {
 83                 return DESCRIPTOR;
 84             }            @Override
 85             public android.os.IBinder asBinder() {
 86                 return mRemote;
 87             }
 88 
 89             @Override
 90             public java.util.List<com.cy.ipcsample.aidl.Book> getBookList() throws android.os.RemoteException {
 91                 android.os.Parcel _data = android.os.Parcel.obtain();
 92                 android.os.Parcel _reply = android.os.Parcel.obtain();
 93                 java.util.List<com.cy.ipcsample.aidl.Book> _result;
 94                 try {
 95                     _data.writeInterfaceToken(DESCRIPTOR);
 96                     mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
 97                     _reply.readException();
 98                     _result = _reply.createTypedArrayList(com.cy.ipcsample.aidl.Book.CREATOR);
 99                 } finally {
100                     _reply.recycle();
101                     _data.recycle();
102                 }
103                 return _result;
104             }
105 
106             @Override
107             public void addBook(com.cy.ipcsample.aidl.Book book) throws android.os.RemoteException {
108                 android.os.Parcel _data = android.os.Parcel.obtain();
109                 android.os.Parcel _reply = android.os.Parcel.obtain();
110                 try {
111                     _data.writeInterfaceToken(DESCRIPTOR);
112                     if ((book != null)) {
113                         _data.writeInt(1);
114                         book.writeToParcel(_data, 0);
115                     } else {
116                         _data.writeInt(0);
117                     }
118                     mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
119                     _reply.readException();
120                 } finally {
121                     _reply.recycle();
122                     _data.recycle();
123                 }
124             }
125 
126 
127         }
128     }
129 }

可見,系統為我們生成了一個 IBookManager 介面,它繼承了 IInterface 這個介面,所以這裡要註意下,所有可以在 Binder 中傳輸的介面,都需要繼承 IInterface 介面。

接下來分析下,該類的工作機制。仔細看,可以發現,該類主要分成三個部分:

  • 定義自身方法( getBookList 方法和 addBook 方法);
  • 內部靜態類-Stub,該類繼承 Binder,同時實現 IBookManager 介面
  • Stub的內部代理類-Proxy,也實現 IBookManager 介面

第一部分我們不用管它,主要看 Stub 類和 Proxy 類。在 Stub 中,首先聲明瞭兩個用於標識 IBookManager 方法的整型變數,這兩個變數用於標識在 transact 過程中客戶端所請求的是哪個方法。接著,是 asInterface 方法,該方法用於將服務端的 Binder 對象轉換成客戶端所需的 AIDL 介面類型的對象,該方法通過調用 Binder 的 queryLocalInterface 方法,判斷客戶端和服務端是否處於同一進程,如果客戶端、服務端處於同一進程,那麼此方法直接返回服務端的 Stub,否則,返回 Stub 的代理對象 Proxy。queryLocalInterface 實現如下:

/**
 * Use information supplied to attachInterface() to return the
 * associated IInterface if it matches the requested
 * descriptor.
 */
public @Nullable IInterface queryLocalInterface(@NonNull String descriptor) {
    if (mDescriptor.equals(descriptor)) {
        return mOwner;
    }
    return null;
}

mOwner 是在 Stub 構造函數中傳進去的 this 參數。

接下來是 Stub 的代理類 Stub.Proxy,由上面的分析可知,Stub.Proxy 是運行在客戶端的(由 asInterface 方法返回給客戶端的對象),Stub.Proxy對象創建後,持有服務端的 Binder 對象,用於客戶端請求時調用服務端方法進行遠程調用。客戶端在向服務端發起請求時,調用 Stub.Proxy 的相應方法,Stub.Proxy 方法的流程如下:

  • 首先創建該方法所需要的輸入型 Parcel 對象 _data、輸出型對象 _reply 和返回值對象(如果有);
  • 然後把該方法的參數信息寫入 _data 中(如果有);
  • 接著調用 transact 方法進行 RPC(遠程過程調用)請求,同時當前線程掛起;
  • 然後在 transact 方法通過服務端的 Binder 對象調用服務端的 onTransact 方法,即 Stub 中的 onTransact 方法;
  • onTransact 方法返回後,當前線程繼續執行,並從 _reply 中取出 RPC 過程返回的結果;
  • 最後返回 _reply 中的數據(如果客戶端請求方法需要返回值)。

以上,就是系統生成的 IBookManager 的工作過程,需要註意下,服務端的 onTransact 方法是運行在 Binder 線程池中的。由於 IBookManager.Stub 類繼承 Binder,所以上述分析即 Binder 的工作機制,簡單總結下:

  • 在客戶端和服務端連接時(一般通過 bindService 方法),服務端通過 asInterface 方法給客戶端返回一個 IInterface 介面類型的對象(如果客戶端和服務端在同一個進程,則返回服務端的 Binder 對象,否則返回 服務端 Binder 對象的代理);
  • 客戶端通過該對象向服務端進行請求,如果客戶端和服務端不在同一個進程,則通過該對象所代理的 Binder 對象進行 RPC 請求,否則,直接通過 Binder 對象調用服務端相應方法。

或者參考下圖理解下:

 

由此可見,Binder 在 AIDL 中承載著重要的職能,是 AIDL 的核心,理解了 Binder 的工作機制,其實在很多方面都很有用。

AIDL 的使用

一套 AIDL 服務搭建的步驟如下:

  • 創建 .aidl 文件,系統生成相應的繼承 IInterface 的介面類(暴露給客戶端的介面)。
  • 創建一個 Service(服務端),實現 .aidl 文件中的介面。
  • 創建客戶端,綁定服務端的 Service。
  • 客戶端綁定服務端成功後,將服務端返回的 Binder 對象轉成 AIDL 介面所屬的 IInterface 類型。調用 AIDL 中的方法,實現和服務端的通信。

接下來,我們使用上面的 IBookManager 來實現 AIDL。

  1. 創建 .aidl 文件
    直接使用上面創建好的 Book.aidl、IBookManager.aidl 文件即可
  2. 創建服務端
    創建一個 Service,命名為 BookManagerService,代碼如下:

     1 package com.cy.ipcsample.aidl
     2 
     3 import android.app.Service
     4 import android.content.Intent
     5 import android.os.IBinder
     6 import android.os.RemoteCallbackList
     7 import android.util.Log
     8 import java.util.concurrent.CopyOnWriteArrayList
     9 
    10 class BookManagerService : Service() {
    11     private val TAG = "BookManagerService"
    12 
    13     private val mBookList = CopyOnWriteArrayList<Book>()
    14     private val mListenerList = RemoteCallbackList<IOnNewBookArrivedListener>()
    15 
    16     /**
    17      * 實現 AIDL 介面的 Binder 對象,客戶端綁定服務端時,直接返回此 Binder 對象
    18      */
    19     private val mBinder = object : IBookManager.Stub() {
    20 
    21         override fun getBookList(): MutableList<Book> {
    22             return mBookList
    23         }
    24 
    25         override fun addBook(book: Book?) {
    26             mBookList.add(book)
    27         }
    28 
    29     }
    30 
    31     override fun onBind(intent: Intent): IBinder {
    32         return mBinder
    33     }
    34 
    35     override fun onCreate() {
    36         super.onCreate()
    37 
    38         // 創建兩本圖書
    39         mBookList.add(Book(1, "Android"))
    40         mBookList.add(Book(2, "iOS"))
    41     }
    42 }

    然後在 AndroidManifest 中註冊 Service,註意啟動多進程:

    <service
        android:name=".aidl.BookManagerService"
        android:process="com.cy.ipcsample.bookManagerService">
    </service>
  3. 客戶端綁定服務端的 Service
    客戶端的創建,直接使用 Activity 即可,綁定遠程服務的代碼如下:

    private val mConnection = object : ServiceConnection {
    
        override fun onServiceDisconnected(name: ComponentName?) {
        }
    
        /**
         * 連接遠程服務成功的回調
         */
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        }
    
    }
    
    // 綁定遠程服務
    bindService(Intent(this, BookManagerService::class.java),
            mConnection, Context.BIND_AUTO_CREATE)
  4. 與服務端通信
    與伺服器綁定成功後,首先在 ServiceConnection 的回調中,將服務端返回的 Binder 對象轉換成 AIDL 介面所屬的對象,就可以調用相應方法和服務端通信了,代碼如下:

    // 將服務端返回的 Binder 對象轉換成 IBookManager 對象
    val bookManager = IBookManager.Stub.asInterface(service)
    
    // 與服務端通信
    try {
        // 獲取圖書列表
        val list = bookManager.bookList
        Log.i(TAG, "query book list, list type: ${list.javaClass.canonicalName}")
        Log.i(TAG, "query book list: $list")
    
        // 添加一本圖書
        val book = Book(3, "Android開發藝術探索")
        bookManager.addBook(book)
        Log.i(TAG, "add book: $book")
    
        // 獲取圖書列表
        val newList = bookManager.bookList
        Log.i(TAG, "query book list: $newList")
    } catch (e: RemoteException) {
        e.printStackTrace()
    }

    代碼中,我們先查詢了服務端的圖書列表,接著向服務端添加一本書Android藝術開發探索,然後再次查詢看是否添加成功。運行下看 log,如下圖所示:

    可見,運行結果和預期結果一致。完整的客戶端代碼如下:
     1 package com.cy.ipcsample.aidl
     2 
     3 import android.content.ComponentName
     4 import android.content.Context
     5 import android.content.Intent
     6 import android.content.ServiceConnection
     7 import android.os.Bundle
     8 import android.os.IBinder
     9 import android.os.RemoteException
    10 import android.support.v7.app.AppCompatActivity
    11 import android.util.Log
    12 import com.cy.ipcsample.R
    13 
    14 class BookManagerActivity : AppCompatActivity() {
    15 
    16     private val TAG = "BookManagerActivity"
    17 
    18     private val mConnection = object : ServiceConnection {
    19 
    20         override fun onServiceDisconnected(name: ComponentName?) {
    21             Log.d(TAG, "binder died.")
    22         }
    23 
    24         /**
    25          * 連接遠程服務成功的回調
    26          */
    27         override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
    28             // 將服務端返回的 Binder 對象轉換成 IBookManager 對象
    29             val bookManager = IBookManager.Stub.asInterface(service)
    30 
    31             // 與服務端通信
    32             try {
    33                 // 獲取圖書列表
    34                 val list = bookManager.bookList
    35                 Log.i(TAG, "query book list, list type: ${list.javaClass.canonicalName}")
    36                 Log.i(TAG, "query book list: $list")
    37 
    38                 // 添加一本圖書
    39                 val book = Book(3, "Android開發藝術探索")
    40                 bookManager.addBook(book)
    41                 Log.i(TAG, "add book: $book")
    42 
    43                 // 獲取圖書列表
    44                 val newList = bookManager.bookList
    45                 Log.i(TAG, "query book list: $newList")
    46             } catch (e: RemoteException) {
    47                 e.printStackTrace()
    48             }
    49         }
    50 
    51     }
    52 
    53     override fun onCreate(savedInstanceState: Bundle?) {
    54         super.onCreate(savedInstanceState)
    55         setContentView(R.layout.activity_book_manager)
    56 
    57         // 綁定遠程服務
    58         bindService(Intent(this, BookManagerService::class.java),
    59                 mConnection, Context.BIND_AUTO_CREATE)
    60     }
    61 
    62     override fun onDestroy() {
    63         unbindService(mConnection)
    64         super.onDestroy()
    65     }
    66 }

到此,一個簡單的 AIDL 示例就完成了,當然,AIDL 的使用遠沒有那麼簡單,還有很多情景需要考慮的,比如:客戶端需要隨時服務端在狀態變化時同時客戶端,類似觀察這模式,那麼訂閱與反訂閱怎麼實現;Binder 意外死亡,怎麼重連等等,更多內容,可以參考《Android藝術開發探索》電子書下載

AIDL 的非同步調用

AIDL 的調用過程是同步還是非同步的?

這個問題其實看這一節的標題大家都知道了,AIDL 的調用過程是同步的。同時,上面分析 Binder 的機制時,也提到了,客戶端進行遠程 RPC 請求時,線程會掛起,等待結果,由此也可知,AIDL 的調用過程是同步的,下麵來驗證下。

首先,在服務端的 BookManagerService 中實現的 Binder 對象的 getBookList 方法添加延時執行,如下圖所示:

 

然後,在客戶端的 BookManagerActivity 中添加一個按鈕,點擊按鈕時調用服務端 Binder 的 getBookList 做 RPC 請求。代碼如下:

 

運行後,連續點擊按鈕,結果如下圖所示:

由圖所知,連續點擊按鈕過後一段時間,出現了無響應錯誤( ANR ),由此可知,客戶端向服務端做 RPC 請求時,是同步的,也就是說:AIDL 的調用過程是同步的

 

AIDL 的調用過程是同步的,當我們需要服務端做耗時操作時,肯定是不能使用同步調用的,否則輕者影響用戶體驗,重者直接 ANR 或者應用崩潰。那麼如何使 AIDL 的調用過程是非同步的呢?

其實也很簡單,只需要把調用放到非 UI 線程即可,如果要對調用的返回做 UI 更新的話,再通過 Handler 處理即可。如下圖所示:

 

本文參考

  • 《Android開發藝術探索》 第二章

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • EXEC GenerateData '同一資料庫下的表名' ...
  • 運行環境:一拖一分散式集群+客戶端 mysql和hive安裝在客戶機上 問題:在客戶機終端啟動hive時出現如下問題: 目前還沒有找出是什麼原因! 解決辦法: 參照http://dblab.xmu.edu.cn/blog/install-hive/#more-996其中,hive-site.xml的 ...
  • 上一篇我們說了關於自排如果主鍵是0的問題,在這裡我搞清楚了原因,導致這種情況是因為在SQL中對自排設置了初始值: 從這裡可以看到這兩個變數一個是自增的初始值,一個是增量,這裡都是1,所以在設置自增的時候會把那個欄位原來存在的所有0變成從1開始的步長為1的等差數列。 但是這個數值是可以被修改的(不過在 ...
  • 用戶的許可權來自系統許可權和對象許可權 一、系統許可權 3個索引許可權 5個存儲過程許可權, 4個角色許可權 5個序列許可權 登錄資料庫許可權 表空間許可權 類型許可權 視圖許可權 表許可權 觸發器 備份資料庫 二、對象許可權 具體表的操作許可權: 具體存儲過程執行許可權 表空間 限制修改的列 收回許可權 三、其它方面 角色有哪些權 ...
  • 看了很多關於索引的博客,講的大同小異。但是始終沒有讓我明白關於索引的一些概念,如B Tree索引,Hash索引,唯一索引....或許有很多人和我一樣,沒搞清楚概念就開始研究B Tree,B+Tree等結構,導致在面試的時候答非所問! 索引是什麼? 索引是幫助MySQL高效獲取數據的數據結構。 索引能 ...
  • 一、什麼是分詞器? 分詞器,是將用戶輸入的一段文本,分析成符合邏輯的一種工具。到目前為止呢,分詞器沒有辦法做到完全的符合人們的要求。和我們有關的分詞器有英文的和中文的分詞器:輸入文本-關鍵詞切分-去停用詞-形態還原-轉為小寫中文的分詞器分為: 單子分詞 例:中國人 分成中,國,人 二分法人詞 例:中 ...
  • 群集準備工作 個人電腦 記憶體12G,處理器 AMD A6-3650CPU主頻2.6GHz 虛擬機 VMware Workstation 12 資料庫 sql server 2008 r2 三台虛擬伺服器 windows server 2008 r2 enterprise , 記憶體分配2g,使用網路橋 ...
  • MySQL不允許遠程登錄,所以遠程登錄失敗了,解決方法如下: 執行FLUSH PRIVILEGES; ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...