Android基礎夯實--你瞭解Handler有多少?

来源:https://www.cnblogs.com/ryanleee/archive/2018/01/05/8204450.html
-Advertisement-
Play Games

Handler是用來結合線程的消息隊列來發送、處理“Message對象”和“Runnable對象”的工具。每一個Handler實例之後會關聯一個線程和該線程的消息隊列。當你創建一個Handler的時候,從這時開始,它就會自動關聯到所在的線程/消息隊列,然後它就會陸續把Message/Runnalbe... ...


概述

對於剛入門的同學來說,往往都會對Handler比較迷茫,到底Handler是個什麼樣的東西。當然,可能對於一些有工作經驗的工程師來說,他們也不一定能很準確地描述,我們來看下API的介紹。

Handler是用來結合線程的消息隊列來發送、處理“Message對象”和“Runnable對象”的工具。每一個Handler實例之後會關聯一個線程和該線程的消息隊列。當你創建一個Handler的時候,從這時開始,它就會自動關聯到所在的線程/消息隊列,然後它就會陸續把Message/Runnalbe分發到消息隊列,併在它們出隊的時候處理掉。

從官方文檔中,我們不難找出其中的關鍵詞,就是“線程”。我們都知道,一個涉及到網路操作,耗時操作等的Android應用,都離不開多線程操作,然而,如果這時我們允許併發更新UI,那麼最終導致控制項的狀態都是不可確定的。所以,我們可以通過對控制項進行加鎖,在不需要用時解鎖,這是一個解決方案之一,但最後很容易造成線程阻塞,效率會非常差。所以,谷歌採用了只允許在主線程更新UI,所以作為線程通信橋梁的Handler也就應運而生了。

Looper、MessageQueue、Message、Handler的關係

講到Handler,肯定離不開Looper、MessageQueue、Message這三者和Handler之間的關係,下麵簡略地帶過,詳細自己可以查閱相關資料,或者查看源碼,這樣更方便大家深入學習。

Looper

每一個線程只有一個Looper,每個線程在初始化Looper之後,然後Looper會維護好該線程的消息隊列,用來存放Handler發送的Message,並處理消息隊列出隊的Message。它的特點是它跟它的線程是綁定的,處理消息也是在Looper所在的線程去處理,所以當我們在主線程創建Handler時,它就會跟主線程唯一的Looper綁定,從而我們使用Handler在子線程發消息時,最終也是在主線程處理,達到了非同步的效果。

那麼就會有人問,為什麼我們使用Handler的時候從來都不需要創建Looper呢?這是因為在主線程中,ActivityThread預設會把Looper初始化好,prepare以後,當前線程就會變成一個Looper線程。

MessageQueue

MessageQueue是一個消息隊列,用來存放Handler發送的消息。每個線程最多只有一個MessageQueue。MessageQueue通常都是由Looper來管理,而主線程創建時,會創建一個預設的Looper對象,而Looper對象的創建,將自動創建一個MessageQueue。其他非主線程,不會自動創建Looper。

Message

消息對象,就是MessageQueue裡面存放的對象,一個MessageQueu可以包括多個Message。當我們需要發送一個Message時,我們一般不建議使用new Message()的形式來創建,更推薦使用Message.obtain()來獲取Message實例,因為在Message類裡面定義了一個消息池,當消息池裡存在未使用的消息時,便返回,如果沒有未使用的消息,則通過new的方式創建返回,所以使用Message.obtain()的方式來獲取實例可以大大減少當有大量Message對象而產生的垃圾回收問題。

四者關係總體如下(如有不對的地方,謝謝指出)
image

Handler的主要用途

  1. 推送未來某個時間點將要執行的Message或者Runnable到消息隊列。
  2. 在子線程把需要在另一個線程執行的操作加入到消息隊列中去。

廢話不多說,通過舉例來說明Handler的兩個主要用途。

1. 推送未來某個時間點將要執行的Message或者Runnable到消息隊列

實例:通過Handler配合Message或者Runnable實現倒計時

  • 首先看一下效果圖

image

  • 方法一,通過Handler + Message的方式實現倒計時。代碼如下:
public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding mBinding;

    private Handler mHandler ;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        //設置監聽事件
        mBinding.clickBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //通過Handler + Message的方式實現倒計時
                for (int i = 1; i <= 10; i++) {
                    Message message = Message.obtain(mHandler);
                    message.what = 10 - i;
                    mHandler.sendMessageDelayed(message, 1000 * i); //通過延遲發送消息,每隔一秒發送一條消息
                }
            }
        });

        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                mBinding.time.setText(msg.what + "");   //在handleMessage中處理消息隊列中的消息
            }
        };
    }
}

其實代碼不用怎麼解釋,都比較通俗易懂,但是這裡用到了DataBiding,可能沒用過的同學看起來有點奇怪,但其實反而簡略了代碼,有一定基礎的同學看起來都不會有太大壓力,所以不做太多解釋。通過這個小程式,作者希望大家可以瞭解到Handler的一個作用就是,在主線程中,可以通過Handler來處理一些有順序的操作,讓它們在固定的時間點被執行。

  • 方法二,通過Handler + Runnable的方式實現倒計時。代碼如下:
public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding mBinding;
    private Handler mHandler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        //設置監聽事件
        mBinding.clickBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                for (int i = 1; i <= 10; i++) {
                    final int fadedSecond = i;
                    //每延遲一秒,發送一個Runnable對象
                    mHandler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mBinding.time.setText((10 - fadedSecond) + "");
                        }
                    }, 1000 * i);
                }
            }
        });
    }
}

方法二也是通過代碼讓大家加深Handler處理有序事件的用途,之所以分開Runnable和Message兩種方法來實現,是因為很多人都搞不清楚為什麼Handler可以推送Runnable和Message兩種對象。其實,無論Handler將Runnable還是Message加入MessageQueue,最終都只是將Message加入到MessageQueue。只要大家看一下源碼就可以知道,Handler的post Runnable對象這個方法只是對post Message進行了一層封裝,所以最終我們都是通過Handler推送了一個Message罷了,至於為什麼會分開兩種方法,下文會給大家詳說究竟。下麵再來看看Handler的第二個主要用途。

2. 在子線程把需要在另一個線程執行的操作加入到消息隊列中去

實例:通過Handler + Message來實現子線程載入圖片,在UI線程顯示圖片

  • 效果圖如下

image

  • 代碼如下(佈局代碼也不放出來了)

public class ThreadActivity extends AppCompatActivity implements View.OnClickListener {
    private ActivityThreadBinding mBinding = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_thread);
        // 設置點擊事件
        mBinding.clickBtn.setOnClickListener(this);
        mBinding.resetBtn.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            // 響應load按鈕
            case R.id.clickBtn:
                // 開啟一個線程
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        // 在Runnable中進行網路讀取操作,返回bitmap
                        final Bitmap bitmap = loadPicFromInternet();
                        // 在子線程中實例化Handler同樣是可以的,只要在構造函數的參數中傳入主線程的Looper即可
                        Handler handler = new Handler(Looper.getMainLooper());
                        // 通過Handler的post Runnable到UI線程的MessageQueue中去即可
                        handler.post(new Runnable() {
                            @Override
                            public void run() {
                                // 在MessageQueue出隊該Runnable時進行的操作
                                mBinding.photo.setImageBitmap(bitmap);
                            }
                        });
                    }
                }).start();
                break;
            case R.id.resetBtn:
                mBinding.photo.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.mipmap.default_pic));
                break;
        }
    }

    /***
     * HttpUrlConnection載入圖片,不多說
     * @return
     */
    public Bitmap loadPicFromInternet() {
        Bitmap bitmap = null;
        int respondCode = 0;
        InputStream is = null;
        try {
            URL url = new URL("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1421494343,3838991329&fm=23&gp=0.jpg");
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(10 * 1000);
            connection.setReadTimeout(5 * 1000);
            connection.connect();
            respondCode = connection.getResponseCode();
            if (respondCode == 200) {
                is = connection.getInputStream();
                bitmap = BitmapFactory.decodeStream(is);
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
            Toast.makeText(getApplicationContext(), "訪問失敗", Toast.LENGTH_SHORT).show();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return bitmap;
    }
}

Handler推送Message和Runnable的區別

在上文我們通過用Handler推送Message和Runnable實現相同的倒計時效果,這裡我們就說一下Post(Runnable)和SendMessage(Message)的區別。

首先我們看看post方法和sendMessage方法的源碼:

    public final boolean post(Runnable r)
    {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }
    public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }

可見,兩個方法都是通過調用sendMessageDelayed方法實現的,所以可以知道它們的底層邏輯是一致的。

但是,post方法的底層調用sendMessageDelayed的時候,卻是通過getPostMessage(r)來將Runnable對象來轉為Message,我們點進方getPostMessage()法可以看到:

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

其實,最終runnable最終也是轉化為一個Message,而這個Message只有一個被賦值的成員變數,就是Runnable的回調函數,也就是說,這個Message在進入MessageQueue之後,它只是一個“動作”,即我們Runnbale的run方法裡面的操作。

要知道,我們的Message類可是有很多參數的,所以你可以理解為它是一個非常豐富的JavaBean,可以看看它的成員變數:

  • public int what;
  • public int arg1;
  • public int arg2;
  • public Object obj;
  • ...

那麼講到這裡,大家也應該有所理解為什麼Google工程師為什麼會封裝這兩種方法,我總結如為:為了更方便開發者根據不同需要進行調用。當我們需要傳輸很多數據時,我們可以使用sendMessage來實現,因為通過給Message的不同成員變數賦值可以封裝成數據非常豐富的對象,從而進行傳輸;當我們只需要進行一個動作時,直接使用Runnable,在run方法中實現動作內容即可。當然我們也可以通過Message.obtain(Handler h, Runnable callback)來傳入callback介面,但這樣看起來就沒有post(Ruannable callback)那麼直觀。

API

API是我們學習最好的文檔,所以我也簡要跟大家學習一下,其實大家認真看我上面的介紹加上自己親手實踐,Handler的API大家都可以隨便翻閱了。

構造函數

  • Handler()
  • Handler(Handler.Callback callback):傳入一個實現的Handler.Callback介面,介面只需要實現handleMessage方法。
  • Handler(Looper looper):將Handler關聯到任意一個線程的Looper,在實現子線程之間通信可以用到。
  • Handler(Looper looper, Handler.Callback callback)

主要方法

  • void dispatchMessage (Message msg)

一般情況下不會使用,因為它的底層實現其實是作為處理系統消息的一個方法,如果真要用,效果和sendMessage(Message m)效果一樣。

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            // 如果有Runnbale,則直接執行它的run方法
            handleCallback(msg);
        } else {
            //如果有實現自己的callback介面
            if (mCallback != null) {
                //執行callback的handleMessage方法
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            //否則執行自身的handleMessage方法
            handleMessage(msg);
        }
    }
    
    private static void handleCallback(Message message) {
        message.callback.run();
    }
  • void dump (Printer pw, String prefix)

主要Debug時使用的一個方法,dump函數只是使用了Printer對象進行了列印,列印出Handler以及Looper和Queue中的一些信息,源碼如下:

    public final void dump(Printer pw, String prefix) {
        pw.println(prefix + this + " @ " + SystemClock.uptimeMillis());
        // 如果Looper為空,輸出Looper沒有初始化
        if (mLooper == null) {
            pw.println(prefix + "looper uninitialized");
        } else {
            // 否則調用Looper的dump方法,Looper的dump方法也是
            mLooper.dump(pw, prefix + "  ");
        }
    }

通過測試用例大家會瞭解得更清晰:

        //測試代碼
        Printer pw = new LogPrinter(Log.ERROR, "MyTag");
        mHandler.dump(pw, "prefix");

結果:
image

  • Looper getLooper ()

拿到Handler相關聯的Looper

  • String getMessageName (Message message)

獲取Message的名字,預設名字為message.what的值。

  • void handleMessage (Message msg)

處理消息。

  • boolean hasMessages (int what)

判斷是否有Message的what值為參數what。

  • boolean hasMessages (int what, Object object)

判斷是否有Message的what值為參數what,obj值為參數object。

  • Message obtainMessage (int what, Object obj)

從消息池中拿到一個消息並賦值what和obj,其他重載函數同理。

  • boolean post (Runnable r)

將Runnable對象加入MessageQueue。

  • boolean post (Runnable r)

將Runnbale加入到消息隊列的隊首。但是官方不推薦這麼做,因為很容易打亂隊列順序。

  • boolean postAtTime (Runnable r, Object token, long uptimeMillis)

在某個時間點執行Runnable r。

  • boolean postDelayed (Runnable r, long delayMillis)

當前時間延遲delayMillis個毫秒後執行Runnable r。

  • void removeCallbacks (Runnable r, Object token)

移除MessageQueue中的所有Runnable對象。

  • void removeCallbacksAndMessages (Object token)

移除MessageQueue中的所有Runnable和Message對象。

  • void removeMessages (int what)

移除所有what值得Message對象。

  • boolean sendEmptyMessage (int what)

直接拿到一個空的消息,並賦值what,然後發送到MessageQueue。

  • boolean sendMessageDelayed (Message msg, long delayMillis)

在延遲delayMillis毫秒之後發送一個Message到MessageQueue。

Handler引發的記憶體泄漏

在上面的例子中,為了展示方便,我都沒有考慮記憶體泄漏的情況,但是在實際開發中,如果不考慮代碼的安全性的話,尤其當一個項目到達了一定的規模之後,那麼對於代碼的維護和系統的調試都是非常困難的。而Handler的記憶體泄漏在Android中也是一個非常經典的案例。

詳細可以參考:How to Leak a Context: Handlers & Inner Classes

或參考翻譯文:Android中Handler引起的記憶體泄露

通常我們都會在一個Activity內部定義一個Handler的內部類:

public class MainActivity extends AppCompatActivity {

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                 ...
            }
        }
    };
    
        @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);


        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                ...
            }
        }, 1000000);

    }
}

(1)外部類Activity中定義了一個非靜態內部類Handler,非靜態內部類預設持有對外部類的引用。如果外部Activity突然關閉了,但是MessageQueue中的消息還沒處理完,那麼Handler就會一直持有對外部Activty的引用,垃圾回收器無法回收Activity,從而導致記憶體泄漏。

(2) 如上代碼,在postDelayed中,我們在參數中傳入一個非靜態內部類Runnable,這同樣會造成記憶體泄漏,假如此時關閉了Activity,那麼垃圾回收器在接下來的1000000ms內都無法回收Activity,造成記憶體泄漏。

解決方案:

(1) 將非靜態內部類Handler和Runnable轉為靜態內部類,因為非靜態內部類(匿名內部類)都會預設持有對外部類的強引用。

(2) 改成靜態內部類後,對外部類的引用設為弱引用,因為在垃圾回收時,會自動將弱引用的對象回收。

避免記憶體泄漏的例子:

public class HandlerActivity extends AppCompatActivity {

    private final MyHandler mHandler = new MyHandler(this);

    private static final Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            // 操作
        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fourth);

        mHandler.postDelayed(mRunnable, 1000*10);
        
        finish();   
    }


    private static class MyHandler extends Handler {
        WeakReference<HandlerActivity> mWeakActivity;

        public MyHandler(HandlerActivity activity) {
            this.mWeakActivity = new WeakReference<HandlerActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            final HandlerActivity mActivity = mWeakActivity.get();
            if (mActivity != null) {
                // 處理消息
            }
        }
    }

}

HandlerThread

思考一下,假如我們需要同時下載A和B,下載A需要6s,下載B需要5s,在它們下載完成後Toast信息出來即可,此時HandlerThread便是一種解決方式之一。那麼HandlerThread到底是什麼?

  • HandlerThread就是一種線程。
  • HandlerThread和普通的Thread之間的區別就是HandlerThread在創建的時候會提供自己該線程的Looper對象。

所以,如果大家瞭解清楚了我前面所講的Looper、Message、Handler、MessageQueue的關係的話,這裡就很清楚HandlerThread是什麼東西了。大家都知道,我們在Actvity創建時系統會自動幫我們初始化好主線程的Looper,然後這個Looper就會管理主線程的消息隊列。但是在我們創建子線程時,系統並不會幫我們創建子線程的Looper,需要我們自己手動創建,如下:

    new Thread(){
        @Override
        public void run() {
            super.run();
            Looper.prepare();
            Handler mHandler = new Handler(Looper.myLooper());
            Looper.loop();
        }
    }.start();

所以HandlerThread就在內部幫我們封裝了Looper的創建過程,從源碼可以看到,HandlerThread集成於Thread,然後覆寫run方法,進行Looper的創建,從而通過getLooper方法暴露出該線程的Looper對象

public class HandlerThread extends Thread {
    int mPriority;
    int mTid = -1;
    Looper mLooper;
    
    ...
    
    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }
    
    public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        
        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }
    
    ...
}    

所以通過HandlerThread,我們可以輕鬆創建一個包含了Looper的子線程:

final HandlerThread mHandlerThread = new HandlerThread("HandlerThread");

mHandlerThread.start();

Handler mHandler = new Handler(mHandlerThread.getLooper());

使用HandlerThread同時下載A和B的Demo:

image

代碼:

public class HandlerThreadActivity extends AppCompatActivity {
    private TextView tv_A, tv_B;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_thread);

        tv_A = (TextView) findViewById(R.id.txt_dlA);
        tv_B = (TextView) findViewById(R.id.txt_dlB);

        final Handler mainHandler = new Handler();

        final HandlerThread downloadAThread = new HandlerThread("downloadAThread");
        downloadAThread.start();
        Handler downloadAHandler = new Handler(downloadAThread.getLooper());

        // 通過postDelayed模擬耗時操作
        downloadAHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(), "下載A完成", Toast.LENGTH_SHORT).show();
                mainHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        tv_A.setText("A任務已經下載完成");
                    }
                });
            }
        }, 1000 * 5);


        final HandlerThread downloadBThread = new HandlerThread("downloadBThread");
        downloadBThread.start();
        Handler downloadBHandler = new Handler(downloadBThread.getLooper());

        // 通過postDelayed模擬耗時操作
        downloadBHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(), "下載B完成", Toast.LENGTH_SHORT).show();
                mainHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        tv_B.setText("B任務已經下載完成");
                    }
                });

            }
        }, 1000 * 7);
    }
}

總結

由於Android的UI更新只能在主線程,所以Handler是Android中一套非常重要的更新UI線程機制,雖然在很多框架的幫助下我們可以減少了很多Handler的代碼編寫,但實際上很多框架的底層實現都是通過Handler來更新UI的,所以可見掌握好Handler對我們來說是多麼重要,所以這也是很多面試官在面試中的高頻考點之一。雖然Handler對開發者來說是一個非常方便的存在,但是我們也不能否認它也是存在缺點的,如處理不當,Handler所造成的的記憶體泄漏對開發者來說也是一個非常頭疼的難題。


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

-Advertisement-
Play Games
更多相關文章
  • 1.1 MHA簡介 1.1.1 MHA軟體介紹 MHA(Master High Availability)目前在MySQL高可用方面是一個相對成熟的解決方案,它由日本DeNA公司youshimaton(現就職於Facebook公司)開發,是一套優秀的作為MySQL高可用性環境下故障切換和主從提升的高 ...
  • 工作中有個excel,其中一列是一組數字。數字的含義是商品的商品碼。商品的狀態有3種,1,2,3.需要連接到後臺的oracle資料庫,查詢商品的狀態,然後從這個excel表的商品碼中篩選出1或者2的。然後保存在另外一個excel里。 整個代碼是: #coding:utf-8import xlrdim ...
  • 大家好,今天給大家帶來一個最近很火的問題,就是英特爾漏洞問題。今年年初,英特爾被曝出其處理器存在一個底層設計缺陷,而要解決這一晶元級漏洞問題,必須得重新設計Windows、Linux內核系統。據瞭解,此次被曝出的英特爾晶元漏洞,無法通過微代碼更新進行彌補,需要與操作系統研發公司一起修補。而此次受到影 ...
  • iOS雙滑塊選擇器 《SDRangeSliderView》 https://github.com/qddnovo/SDRangeSliderView 你不知道我是一個憤怒的控制項 實現了通用性和便利性 假如生活欺騙了你 你就去嘲笑 賈乃亮 羽凡 綠 ...
  • 性能優化界面和業務邏輯之間事件交互小程式調用nativeNative回調小程式圖片源文件優化渲染優化----------------------------------------------------------------------------------------------------... ...
  • 在Android開發中,在儲存少量的數據時,個人感覺SharedPreferences是最好的選擇,SharedPreferences是以鍵值對的方式進行儲存,支持boolean,int,float,long,String 以及Set<String>,使用方法如下: 先在類中進行聲明: 在onCre ...
  • 小程式目前是越來越火爆了,我們公司我算是比較早研究小程式的,做了一個小程式二維碼生成器,希望能夠對大家有用. 支持小程式參數二維碼,場景二維碼,小程式美化,參數二維碼統計 https://weixin.hotapp.cn 如果還有什麼新的需求,可以可以留言給我,或者加QQ群。 ...
  • 心靈雞湯:天下事有難易乎,為之,則難者亦易矣;不為,則易者亦難矣。 摘要 當你已經掌握了Tween Animation之後,再來看Frame Animation,你就會頓悟,喔,原來Frame Animation簡單多了,那麼恭喜你,你已經在Animation這條路上走得越來越遠了,當你花十來分鐘認 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...