【Android】深入Binder攔截

来源:https://www.cnblogs.com/iofomo/Undeclared/17957871
-Advertisement-
Play Games

☞ Github ☜ ☞ Gitee ☜ 說明 Binder作為Android系統跨進程通信的核心機制。網上也有很多深度講解該機制的文章,如: Android跨進程通信詳解Binder機制原理 Android系統核心機制Binder【系列】 這些文章和系統源碼可以很好幫助我們理解Binder的實現原 ...


☞ Github ☜  ☞ Gitee ☜

說明

Binder作為Android系統跨進程通信的核心機制。網上也有很多深度講解該機制的文章,如:

這些文章和系統源碼可以很好幫助我們理解Binder的實現原理和設計理念,為攔截做準備。藉助Binder攔截可以我們可以擴展出那些能力呢:

  1. 虛擬化的能力,多年前就出現的應用免安裝運行類產品如:VirtualApp/DroidPlugin/平行空間/雙開大師/應用分身等。
  2. 測試驗證的能力,通常為Framework層功能開發。
  3. 檢測第三方SDK或模塊系統服務調用訪問情況(特別是敏感API調用)。
  4. 逆向分析應用底層服務介面調用實現。
  5. 第三方ROM擴展Framework服務。

現有方案

一直以來實時分析和攔截進程的Binder通信是通過Java層的AIDL介面代理來實現的。藉助於Android系統Binder服務介面設計的規範,上層的介面均繼承於IBinder

如一下為代理目標對象的所有的介面API的方法:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;

private static void getInterface(Class<?> cls, final HashSet<Class<?>> ss) {
    Class<?>[] ii;
    do {
        ii = cls.getInterfaces();
        for (final Class<?> i : ii) {
            if (ss.add(i)) {
                getInterface(i, ss);
            }
        }
        cls = cls.getSuperclass();
    } while (cls != null);
}

private static Class<?>[] getInterfaces(Class<?> cls) {
    final HashSet<Class<?>> ss = new LinkedHashSet<Class<?>>();
    getInterface(cls, ss);
    if (0 < ss.size()) {
        return ss.toArray(new Class<?>[ss.size()]);
    }
    return null;
}

public static Object createProxy(Object org, InvocationHandler cb) {
    try {
        Class<?> cls = org.getClass();
        Class<?>[] cc = getInterfaces(cls);
        return Proxy.newProxyInstance(cls.getClassLoader(), cc, cb);
    } catch (Throwable e) {
        Logger.e(e);
    } finally {
        // TODO release fix proxy name
    }
    return null;
}

1、對於已經生成的Binder服務對象,在應用進程可參與實現邏輯之前就已經緩存了,我們需要找到並且進行替換(AMS、PMS、WMS等),如AMS在Android 8.0之後的緩存如下:

// source code: http://aospxref.com/android-9.0.0_r61/xref/frameworks/base/core/java/android/app/ActivityManager.java
package android.app;

public class ActivityManager {

    public static IActivityManager getService() {
        return IActivityManagerSingleton.get();
    }

    private static final Singleton<IActivityManager> IActivityManagerSingleton =
            new Singleton<IActivityManager>() {
                @Override
                protected IActivityManager create() {
                    final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
                    final IActivityManager am = IActivityManager.Stub.asInterface(b);
                    return am;
                }
            };
}

因此我們需要找到並且替換它,如:

Object obj;
if (Build.VERSION.SDK_INT < 26) {// <= 7.0
    obj = ReflectUtils.getStaticFieldValue("android.app.ActivityManagerNative", "gDefault");
} else {// 8.0 <=
    obj = ReflectUtils.getStaticFieldValue("android.app.ActivityManager", "IActivityManagerSingleton");
}
Object inst = ReflectUtils.getFieldValue(obj, "mInstance");
ReflectUtils.setFieldValue(obj, "mInstance", createProxy(inst));

2、對於後續運行過程中才獲取的Binder服務,則需要代理ServiceManager,源碼如下:

// source code: http://aospxref.com/android-9.0.0_r61/xref/frameworks/base/core/java/android/os/ServiceManager.java
package android.os;

public final class ServiceManager {
    private static final String TAG = "ServiceManager";

    private static IServiceManager sServiceManager;
}

因此我們的代理如下:

Class<?> cls = ReflectUtils.findClass("android.os.ServiceManager");
Object org = ReflectUtils.getStaticFieldValue(cls, "sServiceManager");
Object pxy = new createProxy(org);
if (null != pxy) {
    ReflectUtils.setStaticFieldValue(getGlobalClass(), "sServiceManager", pxy);
}

這樣每次在第一次訪問該服務時,就會調用IServiceManager中的getService的方法,而該方法已經被我們代理攔截,我們可以通過參數可以識別當前獲取的是哪個服務,然後將獲取的服務對象代理後在繼續返回即可。

但是:這樣的方案並不能攔截進程中所有的Binder服務。我們面臨幾大問題:

  1. 首先,Android源碼越來越龐大,瞭解所有的服務工作量很大,因此有哪些服務已經被緩存排查非常困難。

  2. 其次,廠商越來越鐘情於擴展自定義服務,這些服務不開源,識別和適配更加耗時。

  3. 再次,有一部分服務只有native實現,並不能通過Java層的介面代理進行攔截(如:Sensor/Audio/Video/Camera服務等)。

    // source code: http://aospxref.com/android-13.0.0_r3/xref/frameworks/av/camera/ICamera.cpp
    
    class BpCamera: public BpInterface<ICamera>
    {
    public:
        explicit BpCamera(const sp<IBinder>& impl)
            : BpInterface<ICamera>(impl)
        {
        }
      
        // start recording mode, must call setPreviewTarget first
        status_t startRecording()
        {
            ALOGV("startRecording");
            Parcel data, reply;
            data.writeInterfaceToken(ICamera::getInterfaceDescriptor());
            remote()->transact(START_RECORDING, data, &reply);
            return reply.readInt32();
        }
    }
    

新方案:基於底層攔截

原理

我們都知道Binder在應用進程運行原理如下圖:

不管是Java層還是native層的介面調用,最後都會通過ioctl函數訪問共用記憶體空間,達到跨進程訪問數據交換的目的。因此我們只要攔截ioctl函數,即可完成對所有Binder通信數據的攔截。底層攔截有以下優勢:

1)可以攔截所有的Binder通信。

2)底層攔截穩定,高相容性。從Android 4.xAndroid 14,近10年的系統版本演進,涉及到Binder底層通信適配僅兩次;一次是支持64位進程(當時需要同時相容32位和64位進程訪問Binder服務)。另一次是華為鴻蒙系統的誕生,華為ROMBinder通信協議中增加了新的標識欄位。

要解決的問題

如何攔截

C/C++層的函數攔截,並不像Java層一樣系統提供了較為穩定的代理工具,在這裡不是我們本期討論的重點,可以直接採用網上開源的Hook框架:

如何過濾

ioctl函數為系統底層設備訪問函數,調用及其頻繁,而Binder通信調用只是其中調用者之一,因此需要快速識別非Binder通信調用,不影響程式性能。

函數定義:

#include <sys/ioctl.h>

int ioctl(int fildes, unsigned long request, ...);

request的參數定義:

// source code: http://aospxref.com/android-14.0.0_r2/xref/bionic/libc/kernel/uapi/linux/android/binder.h
#define BINDER_WRITE_READ _IOWR('b', 1, struct binder_write_read)
#define BINDER_SET_IDLE_TIMEOUT _IOW('b', 3, __s64)
#define BINDER_SET_MAX_THREADS _IOW('b', 5, __u32)
#define BINDER_SET_IDLE_PRIORITY _IOW('b', 6, __s32)
#define BINDER_SET_CONTEXT_MGR _IOW('b', 7, __s32)
#define BINDER_THREAD_EXIT _IOW('b', 8, __s32)
#define BINDER_VERSION _IOWR('b', 9, struct binder_version)
#define BINDER_GET_NODE_DEBUG_INFO _IOWR('b', 11, struct binder_node_debug_info)
#define BINDER_GET_NODE_INFO_FOR_REF _IOWR('b', 12, struct binder_node_info_for_ref)
#define BINDER_SET_CONTEXT_MGR_EXT _IOW('b', 13, struct flat_binder_object)
#define BINDER_FREEZE _IOW('b', 14, struct binder_freeze_info)
#define BINDER_GET_FROZEN_INFO _IOWR('b', 15, struct binder_frozen_status_info)
#define BINDER_ENABLE_ONEWAY_SPAM_DETECTION _IOW('b', 16, __u32)
#define BINDER_GET_EXTENDED_ERROR _IOWR('b', 17, struct binder_extended_error)

對應的源碼:

// source code: http://aospxref.com/android-14.0.0_r2/xref/frameworks/native/libs/binder/IPCThreadState.cpp
void IPCThreadState::threadDestructor(void *st) {
	ioctl(self->mProcess->mDriverFD, BINDER_THREAD_EXIT, 0);
}

status_t IPCThreadState::getProcessFreezeInfo(pid_t pid, uint32_t *sync_received, uint32_t *async_received) {
    return ioctl(self()->mProcess->mDriverFD, BINDER_GET_FROZEN_INFO, &info);
}

status_t IPCThreadState::freeze(pid_t pid, bool enable, uint32_t timeout_ms) {
    return ioctl(self()->mProcess->mDriverFD, BINDER_FREEZE, &info) < 0);
}

void IPCThreadState::logExtendedError() {
    ioctl(self()->mProcess->mDriverFD, BINDER_GET_EXTENDED_ERROR, &ee) < 0);
}

status_t IPCThreadState::talkWithDriver(bool doReceive) {
    // 實際Binder調用通信
    return ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr);
}

快速過濾:

static int ioctl_hook(int fd, int cmd, void* arg) {
    if (cmd != BINDER_WRITE_READ || !arg || g_ioctl_disabled) {
        return g_ioctl_func(fd, cmd, arg);
    }
}
如何解析

目標源碼:http://aospxref.com/android-14.0.0_r2/xref/frameworks/native/libs/binder

重點解析發送(即BC_TRANSACTIONBC_REPLY)和接收(即BR_TRANSACTIONBR_REPLY)的類型數據。

如何修改數據

修改數據分為以下幾種:

1)修複調用時參數數據。

2)修複調用後返回的結果數據。

如果數據修複不改變當前數據的長度,只是內容的變化,則可以直接通過地址進行修改。否則需要創建新的記憶體進行修改後將新的數據地址設置到BINDER_WRITE_READ結構的buffer成員。此時處理好記憶體的釋放問題。

3)直接攔截本次調用。

為了保障穩定性,不打斷Binder的調用流程(通常這也是攔截和逆向方案保障穩定的最重要原則之一)。我們可以將目標函數code修改成父類處理的通用方法,然後通過修複調用的返回值即可完成攔截。

方案實現

數據解析

Binder調用數據結構如下:

解析bwr

bwrbinder_write_read,從源碼中瞭解到ioctlBINDER_WRITE_READ類型的arg數據結構為:

struct binder_write_read {
  // 調用時傳入的數據
 	binder_size_t write_size;// call data
 	binder_size_t write_consumed;// call data
 	binder_uintptr_t write_buffer;// call data
  
	// 結果返回數據
 	binder_size_t read_size;// recv data
 	binder_size_t read_consumed;// recv data
 	binder_uintptr_t read_buffer;// recv data
};

不管是傳入還是返回的數據,都是一組BC命令或BR命令,也就是說一次調用上層會打包幾個命令一起傳遞。因此我們需要通過迴圈來找到我們的命令。

void binder_find_for_bc(struct binder_write_read& bwr) {
    binder_uintptr_t cmds = bwr.write_buffer;
    binder_uintptr_t end = cmds + (binder_uintptr_t)bwr.write_size;

    binder_txn_st* txn = NULL;
    while (0 < cmds && cmds < end && !txn) {
        // 由於每次Binder通信數據量的限制,Binder設計每次調用有且僅包含一個有效的參數命令,因此只要找到即可,其他類型則直接跳過忽略
        cmds = binder_parse_cmds_bc(cmds, txn);
    }
}

dump數據如下:

write_buffer:0xb400007107d1d400, write_consumed:68, write_size:68
00000000:  00 63 40 40 14 00 00 00  00 00 00 00 00 00 00 00  .c@@............
00000010:  00 00 00 00 01 00 00 00  12 00 00 00 00 00 00 00  ................
00000020:  00 00 00 00 54 00 00 00  00 00 00 00 00 00 00 00  ....T...........
00000030:  00 00 00 00 00 4d 3a ac  70 00 00 b4 00 00 00 00  .....M:.p.......
00000040:  00 00 00 00                                       ....
BR_NOOP: 0x720c
BR_TRANSACTION_COMPLETE: 0x7206
BR_REPLY: 0
解析txn

txnbinder_transaction_data,Binder方法調用的方法參數信息定義如下:

struct binder_transaction_data {
 union {
     __u32 handle;
     binder_uintptr_t ptr;
 } target;// 目標服務句柄,server端使用

 binder_uintptr_t cookie;// 緩存的Binder進行訪問
 __u32 code;//方法編號

 __u32 flags;// 標識,如是否為 oneway
 __s32 sender_pid;
 __u32 sender_euid;
 binder_size_t data_size;// 數據長度
 binder_size_t offsets_size;// 若包含對象,則對象數據大小
  
 union {
     struct {
         binder_uintptr_t buffer;// Binder方法參數值地址
         binder_uintptr_t offsets;// Binder方法參數對象數據地址
     } ptr;
     __u8 buf[8];
 } data;
};

dumo數據如下:

Trace   : target:       1   cookie:       0   code:      23   flags:   0x12(READ REPLY)
Trace   :    pid:       0      uid:       0   size:     196    offs:8
Trace   : 00000000:  00 00 00 80 ff ff ff ff  54 53 59 53 1c 00 00 00  ........TSYS....
Trace   : 00000010:  61 00 6e 00 64 00 72 00  6f 00 69 00 64 00 2e 00  a.n.d.r.o.i.d...
Trace   : 00000020:  61 00 70 00 70 00 2e 00  49 00 41 00 63 00 74 00  a.p.p...I.A.c.t.
Trace   : 00000030:  69 00 76 00 69 00 74 00  79 00 4d 00 61 00 6e 00  i.v.i.t.y.M.a.n.
Trace   : 00000040:  61 00 67 00 65 00 72 00  00 00 00 00 85 2a 62 73  a.g.e.r......*bs
Trace   : 00000050:  13 01 00 00 00 38 dd 2a  71 00 00 b4 00 05 e9 31  .....8.*q......1
Trace   : 00000060:  71 00 00 b4 01 00 00 0c  1a 00 00 00 63 00 6f 00  q...........c.o.
Trace   : 00000070:  6d 00 2e 00 69 00 66 00  6d 00 61 00 2e 00 74 00  m...i.f.m.a...t.
Trace   : 00000080:  72 00 61 00 6e 00 73 00  65 00 63 00 2e 00 63 00  r.a.n.s.e.c...c.
Trace   : 00000090:  6f 00 6e 00 74 00 61 00  69 00 6e 00 65 00 72 00  o.n.t.a.i.n.e.r.
Trace   : 000000a0:  00 00 00 00 08 00 00 00  73 00 65 00 74 00 74 00  ........s.e.t.t.
Trace   : 000000b0:  69 00 6e 00 67 00 73 00  00 00 00 00 00 00 00 00  i.n.g.s.........
Trace   : 000000c0:  01 00 00 00                                       ....
Trace   : binder object offs:0x4c  type:0x73622a85  flags:0x113  ptr:0x2add3800  cookie:0x31e90500
解析服務名

Binder通信數據頭如下,即可解析出目標服務名:

void find_server_name(const binder_txn_st* txn) {
		const int32_t* ptr = reinterpret_cast<const int32_t*>(txn->data.ptr.buffer);
  	++ ptr;// skip strict model
    if (29 <= sdkVersion()) ++ ptr;// 10.0 <=, skip flags(ff ff ff ff)
		
  	int32_t nameLen = *ptr;
    const uint16_t* name16 = (const uint16_t*)(ptr+1);
}
解析方法名

Binder通信數據中標識該服務方法的參數是txn->codeAIDL定義類在編譯後會為每個方法自動生成靜態的方法。

如定義的Binder介面方法為:

interface IDemo {
  void test();
  void test2();
}

則編譯後生成的類為:

class IDemo$Stub {
   void test();
   void test2();
   
  static final int TRANSACTION_test = 1;
  static final int TRANSACTION_test2 = 2;
}

因此我們可以通過反射的方式,找到服務名對應的類所有靜態成員變數,然後找到與code值相等的成員即為此方法。

這裡可能需要解決私有API的限制解除問題。

// 可直接使用工程工具類
TstClassPrinter.printStubByCodes("android.app.IActivityManager", 13, 16, 67);

日誌輸出如下:

解析數據

首先需要藉助數據封裝類Parcel

// souce code:
// http://aospxref.com/android-14.0.0_r2/xref/frameworks/native/include/binder/Parcel.h
// http://aospxref.com/android-14.0.0_r2/xref/frameworks/native/libs/binder/Parcel.cpp

藉助該類可以解析一些比較簡單的數據,快速的找到目標內容。而對於比較複雜的數據,如參數值為Intent,該參數類型嵌套了多層的Parcelable成員,因此在native層通過Parcel來解析,相容性比較差。因此我們選擇通過回調到Java層來解析,修改後再格式化為nativebuffer數據。這裡需要處理好Javanative層的數據交換問題,以及回收。

native層:

// 創建
jobject obtain(JNIEnv* env) {
    jclass jcls = env->FindClass("android/os/Parcel");
    jmethodID method = env->GetStaticMethodID(jcls, "obtain", "()Landroid/os/Parcel;");
    if (!method) return NULL;

    mParcelObj = env->CallStaticObjectMethod(jcls, method);
    if (!mParcelObj) return NULL;

    if (0 < mUparcel->dataSize()) {
        method = env->GetMethodID(sParcelClass, "setDataPosition", "(I)V");
        if (method) {
            unmarshall(env, mUparcel->data(), mUparcel->dataSize());
            env->CallVoidMethod(mParcelObj, method, mUparcel->dataPosition());
        }
    }

    return mParcelObj;
}

// 回收
void recycle(JNIEnv* env) {
    jclass jcls = env->FindClass("android/os/Parcel");
    jmethodID method = env->GetMethodID(jcls, "recycle", "()V");
    if (method) {
        env->CallVoidMethod(mParcelObj, method);
    }
    if (mParcelObj) {
        env->DeleteLocalRef(mParcelObj);
    }
    mParcelObj = NULL;
}

Java層:

public static void clearHttpLink(Parcel p/*IN*/, Parcel q/*OUT*/) {
    try {
        Intent ii = Intent.CREATOR.createFromParcel(pp);
		// TODO something ...
      	
        // write new data
        q.appendFrom(p, p.dataPosition(), p.dataAvail());
    } catch (Throwable e) {
        e.printStackTrace();
    }
}

數據攔截

Binder的數據解析和列印不會改變原數據內容,因此相對簡單,如果要對數據進行修改,則相對複雜一些。修複的數據需要替換原數據,因此需要進行如下操作。

1、數據替換。

txn中方法參數的數據指針指向新創建的數據區。


int binder_replace_txn_for_br(binder_txn_st *txn, ParcelEx* reply, binder_size_t _pos) {
    size_t size = reply->ipcDataSize();
    uint8_t* repData = (uint8_t*)malloc(size + txn->offsets_size);
    memcpy(repData, reply->data(), size);
    if (0 < txn->offsets_size) {
        binder_replace_objects(txn, repData, _pos, ((int)size) - ((int)(txn->data_size)));
    }

    txn->data.ptr.buffer = reinterpret_cast<uintptr_t>(repData);
    txn->data_size = size;
    return 0;
}

2、修正對象指針。

如果傳入的參數包含Binder對象,如register方法的Observe。因此修複的數據可能導致偏移的地址前移或者後移,因此需要重新計算偏移,如:

void replaceObjects(binder_txn_st *txn, uint8_t* objData, binder_size_t _pos, int _off) {
    binder_size_t* offs = reinterpret_cast<binder_size_t*>(txn->data.ptr.offsets);
    unsigned count = txn->offsets_size / sizeof(binder_size_t);

    while (0 < count--) {
        if (0 != memcmp(objData + (int)(*offs), (uint8_t*)txn->data.ptr.buffer + (int)(*offs), sizeof(binder_size_t))) {
            *offs += _off;
        }
        ++ offs;
    }
}

3、記憶體釋放。

需要保存原地址A和新的地址AA的映射關係到自定義的記憶體池中。

Binder通信命令出現BC_FREE_BUFFERBR_FREE_BUFFER時,則通過該命令要釋放的AA地址,然後從記憶體池找到與之對應A的地址,並設置回去讓上層繼續釋放,完成記憶體使用的閉環。

case BC_FREE_BUFFER:
{
    uintptr_t* buffPtr = (uintptr_t *)cmd;
    uintptr_t ptr = MemPool::detach(*buffPtr);
    if (__UNLIKELY(0 != ptr)) {
        *buffPtr = ptr;// set origin buffer
    }
    cmd += sizeof(uintptr_t);// move to next command
}   break;

附:

如果你有需要,可以直接使用我們已經封裝好的SDK來實現相應的功能,該項目已經開源,可以直接使用,參考【集成文檔】


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

-Advertisement-
Play Games
更多相關文章
  • Nginx採用虛擬目錄的方式代理IIS站點 起因 背景 由於IIS出現了某種不可知的問題,H5APP的部署從IIS改為Nginx。 H5APP的Nginx的部署比較簡單,直接修改官方的實例即可 但是之前H5站點中有一個虛擬目錄用於客戶單點登錄認證,所以需要在Nginx中添加對應的虛擬目錄,但是單點認 ...
  • 使用STM32CubeMX軟體配置STM32F407開發板上串口USART1進行DMA傳輸數據,然後實現與實驗STM32CubeMX教程9 USART/UART 非同步通信相同的目標 ...
  • 文件系統結構 unix的文件系統相關知識 unix將可用的磁碟空間劃分為兩種主要類型的區域:inode區域和數據區域。 unix為每個文件分配一個inode,其中保存文件的關鍵元數據,如文件的stat屬性和指向文件數據塊的指針。 數據區域中的空間會被分成大小相同的數據塊(就像記憶體管理中的分頁)。數據 ...
  • 1月9日,計世資訊(CCW Research)發佈《2022-2023年中國信創資料庫行業市場研究報告》(以下簡稱“報告”),從產品技術能力和市場及戰略能力兩個維度對我國主要資料庫產品服務商進行競爭力分析。其中,中國電信天翼雲憑藉其產品豐富的管理功能、靈活的部署架構,位列雲資料庫產品領域領導者象限。 ...
  • 作者:俊達 引言 MySQL是MySQL安裝包預設的客戶端,該客戶端程式通常位於二進位安裝包的bin目錄中,或者通過rpm安裝包安裝mysql-community-client,是資料庫管理系統的重要組成部分。MySQL客戶端不僅僅是一個簡單的軟體工具,更是連接用戶與資料庫之間的橋梁,對於有效地使用 ...
  • 作者:櫰木 環境準備 本次使用到的二進位軟體包目錄為:系統初始化前提是操作系統已完成安裝、各個主機之間網路互通,系統常用命令已安裝,本預設這些前提條件已具備,不在闡述。 1 主機環境初始化 安裝centos系統完成後需要對主機進行初始化配置和驗證工作,在所有主機上(hd1.dtstack.com-h ...
  • 摘要 隨著任務數量、任務類型需求不斷增長,對我們的數據開發平臺提出了更高的要求。本文主要分享我們將調度引擎升級到 Apache DolphinScheduler 的實踐經驗,以及對數據開發平臺的一些思考。 1. 背景 首先介紹下我們的大數據平臺架構: 數據計算層承接了全公司的數據開發需求,負責運行各 ...
  • 一、背景 為瞭解決應卡頓,分析耗時。 二、原理 Looper中的loop方法: public static void loop() { ... for (;;) { ... // This must be in a local variable, in case a UI event sets th ...
一周排行
    -Advertisement-
    Play Games
  • 在C#中使用SQL Server實現事務的ACID(原子性、一致性、隔離性、持久性)屬性和使用資料庫鎖(悲觀鎖和樂觀鎖)時,你可以通過ADO.NET的SqlConnection和SqlTransaction類來實現。下麵是一些示例和概念說明。 實現ACID事務 ACID屬性是事務處理的四個基本特征, ...
  • 我們在《SqlSugar開發框架》中,Winform界面開發部分往往也用到了自定義的用戶控制項,對應一些特殊的界面或者常用到的一些局部界面內容,我們可以使用自定義的用戶控制項來提高界面的統一性,同時也增強了使用的便利性。如我們Winform界面中用到的分頁控制項、附件顯示內容、以及一些公司、部門、菜單的下... ...
  • 在本篇教程中,我們學習瞭如何在 Taurus.MVC WebMVC 中進行數據綁定操作。我們還學習瞭如何使用 ${屬性名稱} CMS 語法來綁定頁面上的元素與 Model 中的屬性。通過這些步驟,我們成功實現了一個簡單的數據綁定示例。 ...
  • 是在MVVM中用來傳遞消息的一種方式。它是在MVVMLight框架中提供的一個實現了IMessenger介面的類,可以用來在ViewModel之間、ViewModel和View之間傳遞消息。 Send 接受一個泛型參數,表示要發送的消息內容。 Register 方法用於註冊某個對象接收消息。 pub ...
  • 概述:在WPF中,通過EventHandler可實現基礎和高級的UI更新方式。基礎用法涉及在類中定義事件,併在UI中訂閱以執行更新操作。高級用法藉助Dispatcher類,確保在非UI線程上執行操作後,通過UI線程更新界面。這兩種方法提供了靈活而可靠的UI更新機制。 在WPF(Windows Pre ...
  • 概述:本文介紹了在C#程式開發中如何利用自定義擴展方法測量代碼執行時間。通過使用簡單的Action委托,開發者可以輕鬆獲取代碼塊的執行時間,幫助優化性能、驗證演算法效率以及監控系統性能。這種通用方法提供了一種便捷而有效的方式,有助於提高開發效率和代碼質量。 在軟體開發中,瞭解代碼執行時間是優化程式性能 ...
  • 概述:Cron表達式是一種強大的定時任務調度工具,通過配置不同欄位實現靈活的時間規定。在.NET中,Quartz庫提供了簡便的方式配置Cron表達式,實現精準的定時任務調度。這種靈活性和可擴展性使得開發者能夠根據需求輕鬆地制定和管理定時任務,例如每天備份系統日誌或其他重要操作。 Cron表達式詳解 ...
  • 概述:.NET提供多種定時器,如System.Windows.Forms.Timer適用於UI,System.Web.UI.Timer用於Web,System.Diagnostics.Timer用於性能監控,System.Threading.Timer和System.Timers.Timer用於一般 ...
  • 問題背景 有同事聯繫我說,在生產環境上,訪問不了我負責的common服務,然後我去檢查common服務的health endpoint, 沒問題,然後我問了下異常,timeout導致的System.OperationCanceledException。那大概率是客戶端的問題,會不會是埠耗盡,用ne ...
  • 前言: 在本篇 Taurus.MVC WebMVC 入門開發教程的第四篇文章中, 我們將學習如何實現數據列表的綁定,通過使用 List<Model> 來展示多個數據項。 我們將繼續使用 Taurus.Mvc 命名空間,同時探討如何在視圖中綁定並顯示一個 Model 列表。 步驟1:創建 Model ...