深入理解 Android 中的各種 Context

来源:https://www.cnblogs.com/linhaostudy/archive/2020/02/22/12344127.html
-Advertisement-
Play Games

前言 網上關於 Context 的文章也已經有不少了,比如值得參考的有: "Android Context完全解析,你所不知道的Context的各種細節" "Android Context 到底是什麼?" 但看了一下,發現還有值得討論的地方,比如這個等式: Context個數 = Service 個 ...


前言

網上關於 Context 的文章也已經有不少了,比如值得參考的有:

Android Context完全解析,你所不知道的Context的各種細節

Android Context 到底是什麼?

但看了一下,發現還有值得討論的地方,比如這個等式:

Context個數 = Service 個數 + Activity 個數 + 1

老實說,我不明白這個等式有什麼意義,而且還是錯的。首先多進程情況下,Application 對象就不止一個;其次,Activity、Service、Application 繼承自 ContextWrapper,它們自己就是一個 Context,裡面又有一個 Base Context;最後,還有各種 outer context、display context 什麼的,這部分沒深入研究過,但 Context 的數量絕對大於上述等式的兩倍了。

上面這部分算一個討論,下麵正式進入正題。

Context 家族

Context 本身是一個抽象類,主要實現類為 ContextImpl,另外有子類 ContextWrapper 和 ContextThemeWrapper,這兩個子類都是 Context 的代理類,主要區別是 ContextThemeWrapper 有自己的主題資源。它們繼承關係如下:

image

Context 有什麼用?

如果要弄清楚 “某個類有什麼用” 這樣的問題,其實很簡單,看一下它提供了什麼介面就知道了,下麵列舉一些主要的:

/**
* Interface to global information about an application environment.  This is
* an abstract class whose implementation is provided by
* the Android system.  It
* allows access to application-specific resources and classes, as well as
* up-calls for application-level operations such as launching activities,
* broadcasting and receiving intents, etc.
*/
public abstract class Context {
    
    // 四大組件相關
    public abstract void startActivity(@RequiresPermission Intent intent);
    public abstract void sendBroadcast(@RequiresPermission Intent intent);
    public abstract Intent registerReceiver(@Nullable BroadcastReceiver receiver,
                                            IntentFilter filter);
    public abstract void unregisterReceiver(BroadcastReceiver receiver);
    public abstract ComponentName startService(Intent service);
    public abstract boolean stopService(Intent service);
    public abstract boolean bindService(@RequiresPermission Intent service,
            @NonNull ServiceConnection conn, @BindServiceFlags int flags);
    public abstract void unbindService(@NonNull ServiceConnection conn);
    public abstract ContentResolver getContentResolver();
    
    // 獲取系統/應用資源
    public abstract AssetManager getAssets();
    public abstract Resources getResources();
    public abstract PackageManager getPackageManager();
    public abstract Context getApplicationContext();
    public abstract ClassLoader getClassLoader();
    public final @Nullable <T> T getSystemService(@NonNull Class<T> serviceClass) { ... }
    
    public final String getString(@StringRes int resId) { ... }
    public final int getColor(@ColorRes int id) { ... }
    public final Drawable getDrawable(@DrawableRes int id) { ... }
    public abstract Resources.Theme getTheme();
    public abstract void setTheme(@StyleRes int resid);
    public final TypedArray obtainStyledAttributes(@StyleableRes int[] attrs) { ... }
    
    // 獲取應用相關信息
    public abstract ApplicationInfo getApplicationInfo();
    public abstract String getPackageName();
    public abstract Looper getMainLooper();
    public abstract int checkPermission(@NonNull String permission, int pid, int uid);
    
    // 文件相關
    public abstract File getSharedPreferencesPath(String name);
    public abstract File getDataDir();
    public abstract boolean deleteFile(String name);
    public abstract File getExternalFilesDir(@Nullable String type);
    public abstract File getCacheDir();
    ...
    public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);
    public abstract boolean deleteSharedPreferences(String name);
    
    // 資料庫相關
    public abstract SQLiteDatabase openOrCreateDatabase(...);
    public abstract boolean deleteDatabase(String name);
    public abstract File getDatabasePath(String name);
    ...
    
    // 其它
    public void registerComponentCallbacks(ComponentCallbacks callback) { ... }
    public void unregisterComponentCallbacks(ComponentCallbacks callback) { ... }
    ...
}

public interface ComponentCallbacks {
    void onConfigurationChanged(Configuration newConfig);
    void onLowMemory();
}

結合註釋,可以發現,Context 就相當於 Application 的大管家,主要負責:

  1. 四大組件的交互,包括啟動 Activity、Broadcast、Service,獲取 ContentResolver 等
  2. 獲取系統/應用資源,包括 AssetManager、PackageManager、Resources、System Service 以及 color、string、drawable 等
  3. 文件,包括獲取緩存文件夾、刪除文件、SharedPreference 相關等
  4. 資料庫(SQLite)相關,包括打開資料庫、刪除資料庫、獲取資料庫路徑等
  5. 其它輔助功能,比如設置 ComponentCallbacks,即監聽配置信息改變、記憶體不足等事件的發生

ContextImpl 、ContextWrapper、ContextThemeWrapper 有什麼區別?

ContextWrapper

先看 ContextWrapper:

/**
* Proxying implementation of Context that simply delegates all of its calls to
* another Context.  Can be subclassed to modify behavior without changing
* the original Context.
*/
public class ContextWrapper extends Context {
    // 註意這個成員
    Context mBase; 

    public ContextWrapper(Context base) {
        mBase = base;
    }
    
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }
    
    // 這就是經常讓人產生疑惑的  Base Context 了
    public Context getBaseContext() {
        return mBase;
    }

    // 下麵這些方法全都直接通過 mBase 完成
    @Override
    public AssetManager getAssets() {
        return mBase.getAssets();
    }

    @Override
    public Resources getResources() {
        return mBase.getResources();
    }

    @Override
    public PackageManager getPackageManager() {
        return mBase.getPackageManager();
    }
    
    ...
    
}

可以看到,ContextWrapper 實際上就是 Context 的代理類而已,所有的操作都是通過內部成員 mBase 完成的,另外,Activity、Service 的 getBaseContext 返回的就是這個 mBase。

ContextThemeWrapper

接著看 ContextThemeWrapper,這個類的代碼並不多,主要看 Resource 和 Theme 相關的:

/**
* A context wrapper that allows you to modify or replace the theme of the
* wrapped context.
*/
public class ContextThemeWrapper extends ContextWrapper {
    private int mThemeResource;
    private Resources.Theme mTheme;
    private LayoutInflater mInflater;
    private Configuration mOverrideConfiguration;
    private Resources mResources;

    public ContextThemeWrapper() {
        super(null);
    }

    public ContextThemeWrapper(Context base, @StyleRes int themeResId) {
        super(base);
        mThemeResource = themeResId;
    }

    public ContextThemeWrapper(Context base, Resources.Theme theme) {
        super(base);
        mTheme = theme;
    }

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
    }

    // 在 Recource 初始化之前,傳入配置信息
    public void applyOverrideConfiguration(Configuration overrideConfiguration) {
        if (mResources != null) {
            throw new IllegalStateException(...);
        }
        if (mOverrideConfiguration != null) {
            throw new IllegalStateException(...);
        }
        mOverrideConfiguration = new Configuration(overrideConfiguration);
    }

    public Configuration getOverrideConfiguration() {
        return mOverrideConfiguration;
    }

    // 沒有重寫 setResource,即 setResource 行為和父類一樣
    @Override
    public Resources getResources() {
        return getResourcesInternal();
    }

    private Resources getResourcesInternal() {
        if (mResources == null) {
            if (mOverrideConfiguration == null) {
                mResources = super.getResources();
            } else {
                // 根據配置信息初始化 Resource
                // 註意,這裡創建了另一個和 Base Context 不同的 Resource
                final Context resContext = createConfigurationContext(mOverrideConfiguration);
                mResources = resContext.getResources();
            }
        }
        return mResources;
    }

    @Override
    public void setTheme(int resid) {
        if (mThemeResource != resid) {
            mThemeResource = resid;
            initializeTheme();
        }
    }
    
    private void initializeTheme() {
        final boolean first = mTheme == null;
        if (first) {
            // 根據 Resource 獲取 Theme
            mTheme = getResources().newTheme();
            // 複製內容
            final Resources.Theme theme = getBaseContext().getTheme();
            if (theme != null) {
                mTheme.setTo(theme);
            }
        }
        onApplyThemeResource(mTheme, mThemeResource, first);
    }

    protected void onApplyThemeResource(Resources.Theme theme, int resId, boolean first) {
        theme.applyStyle(resId, true);
    }

    @Override
    public Resources.Theme getTheme() {
        // 只會初始化一次
        if (mTheme != null) {
            return mTheme;
        }

        mThemeResource = Resources.selectDefaultTheme(mThemeResource,
                getApplicationInfo().targetSdkVersion);
        initializeTheme();

        return mTheme;
    }

    ...

}

結合註釋及源碼,可以發現,相比 ContextWrapper,ContextThemeWrapper 有自己的另外 Resource 以及 Theme 成員,並且可以傳入配置信息以初始化自己的 Resource 及 Theme。即 Resource 以及 Theme 相關的行為不再是直接調用 mBase 的方法了,也就說,ContextThemeWrapper 和它的 mBase 成員在 Resource 以及 Theme 相關的行為上是不同的。

ContextImpl

下麵看一下 ContextImpl 有關 Theme 以及 Resource 的部分,以分析它和 ContextThemeWrapper 的區別:

/**
* Common implementation of Context API, which provides the base
* context object for Activity and other application components.
*/
class ContextImpl extends Context {

    private int mThemeResource = 0;
    private Resources.Theme mTheme = null;
    private @NonNull Resources mResources;
    
    // 用於創建 Activity Context
    static ContextImpl createActivityContext(...) {
        ContextImpl context = new ContextImpl(...);
        context.setResources(resourcesManager.createBaseActivityResources(...));
        return context;
    }
    
    // 用於創建 Application Context、Service Context
    static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
        ContextImpl context = new ContextImpl(...);
        context.setResources(packageInfo.getResources());
        return context;
    }
    
    private static Resources createResources(...) {
        return ResourcesManager.getInstance().getResources(...);
    }

    // ContextThemeWrapper 沒有重寫父類的 setResources
    // 因此會調用 mBase 的 setResources,即和 ContextImpl 的行為一樣
    void setResources(Resources r) {
        if (r instanceof CompatResources) {
            ((CompatResources) r).setContext(this);
        }
        mResources = r;
    }
    
    @Override
    public Resources getResources() {
        return mResources;
    }
    
    
    /* ---------- 主題相關 ------------ */
    
    @Override
    public void setTheme(int resId) {
        synchronized (mSync) {
            if (mThemeResource != resId) {
                mThemeResource = resId;
                initializeTheme();
            }
        }
    }
    
    // 直接創建一個 Themem 對象,相比 ContextThemeWrapper,少了一部分內容
    private void initializeTheme() {
        if (mTheme == null) {
            mTheme = mResources.newTheme();
        }
        mTheme.applyStyle(mThemeResource, true);
    }

    @Override
    public Resources.Theme getTheme() {
        synchronized (mSync) {
            // 和 ContextThemeWrapper 基本一樣
            if (mTheme != null) {
                return mTheme;
            }

            mThemeResource = Resources.selectDefaultTheme(mThemeResource,
                    getOuterContext().getApplicationInfo().targetSdkVersion);
            initializeTheme();

            return mTheme;
        }
    }

}

從代碼中可以看出,ContextImpl 和 ContextThemeWrapper 最大的區別就是沒有一個 Configuration 而已,其它的行為大致是一樣的。另外,ContextImpl 可以用於創建 Activity、Service 以及 Application 的 mBase 成員,這個 Base Context 時除了參數不同,它們的 Resource 也不同。需要註意的是,createActivityContext 等方法中 setResource 是 mBase 自己調用的,Activity、Service 以及 Application 本身並沒有執行 setResource。

小結

  1. ContextWrapper、ContextThemeWrapper 都是 Context 的代理類,二者的區別在於 ContextThemeWrapper 有自己的 Theme 以及 Resource,並且 Resource 可以傳入自己的配置初始化

  2. ContextImpl 是 Context 的主要實現類,Activity、Service 和 Application 的 Base Context 都是由它創建的,即 ContextWrapper 代理的就是 ContextImpl 對象本身

  3. ContextImpl 和 ContextThemeWrapper 的主要區別是, ContextThemeWrapper 有 Configuration 對象,Resource 可以根據這個對象來初始化

  4. Service 和 Application 使用同一個 Recource,和 Activity 使用的 Resource 不同

Activity Context、Service Context、Application Context、Base Context 有什麼區別?

Activity Context

先看 Activity,Activity 在啟動時,最終會執行 ActivityThread 的 performLaunchActivitiy:

public final class ActivityThread {

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ...
        // 這個 Context 將會作為 Activity 的 Base Context
        ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        try {
            ClassLoader cl = appContext.getClassLoader();
            // 創建 Activity
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
        } catch (Exception e) {
            ...
        }

        try {
            // 創建 Application
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);

            if (activity != null) {
                // 初始化 Activity,註意參數 appContext
                activity.attach(appContext, ...);
                ...
            }

        } catch (...) {
            ...
        }

        return activity;
    }
    
    private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
        ContextImpl appContext = ContextImpl.createActivityContext(...);
        ...
    }
    
}

可以看到,Activity 的 Base Context 就是上面分析過的 ContextImpl 的 createActivityContext 創建的。

同時,Service 的 Base Context 的創建過程和 Application 一樣,調用的都是 ContextImpl 的 createAppContext,即 Service Context 和 Application Context 的 Resource 是相同的。因此這裡跳過 Service,下麵看一下 Application Context。

Application Context

在上面 ActivityThread 的 performLaunchActivity 方法中,可以看到一個 makeApplication 的調用,它是 LoaedApk 的方法:

public final class LoadedApk {

    private Application mApplication;

    public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {
        if (mApplication != null) {
            return mApplication;
        }

        Application app = null;

        try {
            // 創建 Base Context
            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            // 創建 Application 並設置 Base Context
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
            appContext.setOuterContext(app);
        } catch (Exception e) {
            ...
        }
        // Application 創建成功,賦值給 mApplication
        mApplication = app;
        
        ...
        
        return app;
    }
    
    // 獲取 mApplication
    Application getApplication() {
        return mApplication;
    }
    
}
public class Application extends ContextWrapper implements ComponentCallbacks2 {
    /* package */ final void attach(Context context) {
        // 調用父類的 attachBaseContext 以設置 mBase
        attachBaseContext(context); 
    }
}

可以看到,Instrumentation 是使用反射的方法創建 Application 對象,創建完畢後,會執行 Application 的 attach 方法設置 mBase 成員。

Application 及其 Base Context 的創建過程我們瞭解了,接下來看一下 getApplicationContext 的實現:

class ContextImpl extends Context {

    ActivityThread mMainThread;
    LoadedApk mPackageInfo;
    
    @Override
    public Context getApplicationContext() {
        return (mPackageInfo != null) ?
                mPackageInfo.getApplication() : mMainThread.getApplication();
    }
    
}

從代碼中可以看出,getApplicationContext 的返回值可能有兩個:第一個是 LoadedApk 的 getApplication 方法,這個方法的返回值就是剛剛創建的 Application 對象;第二個是 ActivityThread 的 getApplication 方法:

public final class ActivityThread {

    Application mInitialApplication;
    
    public Application getApplication() {
        return mInitialApplication;
    }
    
    public static ActivityThread systemMain() {
        // 創建 ActivityThread
        ActivityThread thread = new ActivityThread();
        thread.attach(true);
        return thread;
    }
    
    private void attach(boolean system) {
        mSystemThread = system;
        if (!system) {
            ...
        } else {
            try {
                mInstrumentation = new Instrumentation();
                // 註意參數 getSystemContext().mPackageInfo
                ContextImpl context = ContextImpl.createAppContext(
                        this, getSystemContext().mPackageInfo);
                // 創建 Application
                mInitialApplication = context.mPackageInfo.makeApplication(true, null);
                mInitialApplication.onCreate();
            } catch (Exception e) {
                ...
            }
        }
        ...
    }
    
}

ActivityThread 中的 mInitialApplication 是在 systemMain 方法執行時創建的,而這個方法又是 SystemServer 啟動時調用的,結合參數 getSystemContext().mPackageInfo,因此個人推測 mInitialApplication 對應的是系統的某個 apk,即系統級別的 Application,但具體是不是這樣,目前還沒有深入研究過,有興趣的可以自己研究。

為什麼不推薦使用 Base Context?

一般情況下,使用代理而不直接使用某個對象,目的可能有兩個:

  1. 定製自己的行為
  2. 不影響原對象

其中 Servcie 和 Application 的父類 ContextWrapper 完全沒有自定義的行為,而 Activity 的父類 ContextThemeWrapper 則自定義了 Resource 以及 Theme 的相關行為,因此,個人理解:

  1. 對於 Service 和 Application 而言,不推薦使用 Base Context,是擔心用戶修改了 Base Context 而導致錯誤的發生
  2. 對於 Activity 而言,除了擔心用戶的修改之外,Base Context 和 Activity 本身對於 Reource 以及 Theme 的相關行為是不同的(如果應用了 Configuration 的話),使用 Base Context 可能會出現無法預期的現象

對於 Activity 的 getResource 問題,我寫了一份代碼來驗證:

public class MainActivity extends AppCompatActivity {

    private Configuration mOverrideConfiguration;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.i(TAG, "getResources: " + getResources() + ", getBaseContext().getResources():"
                + getBaseContext().getResources());
    }
    
    // 因為 Android 會在 onCreate 之前自動調用 getResource
    // 因此需要在這裡執行 applyOverrideConfiguration
    @Override
    public Resources getResources() {
        if (mOverrideConfiguration == null) {
            mOverrideConfiguration = new Configuration();
            applyOverrideConfiguration(mOverrideConfiguration);
        }
        return super.getResources();
    }
    
}

輸出(我用的是小米手機):

getResources: android.content.res.MiuiResources@3c660a7, 
getBaseContext().getResources():android.content.res.MiuiResources@5143954

可以看到,就像源碼顯示的那樣,應用了 Configuration 之後,Activity 的 getResource 方法返回的和 getBaseContext().getResources() 方法返回的不是同一個對象

小結

  1. Activity、Service 和 Application 的 Base Context 都是由 ContextImpl 創建的,且創建的都是 ContextImpl 對象,即它們都是 ContextImpl 的代理類
  2. getApplicationContext 返回的就是 Application 對象本身,一般情況下它對應的是應用本身的 Application 對象,但也可能是系統的某個 Application
  3. 對於 Service 和 Application 而言,不推薦使用 Base Context,是擔心用戶修改了 Base Context 而導致錯誤的發生
  4. 對於 Activity 而言,除了擔心用戶的修改之外,Base Context 和 Activity 本身對於 Reource 以及 Theme 的相關行為是不同的(如果應用了 Configuration 的話),使用 Base Context 可能會出現無法預期的現象

總結

Context 的繼承關係如下:

image

Context 相當於 Application 的大管家,主要負責:

  1. 四大組件的交互,包括啟動 Activity、Broadcast、Service,獲取 ContentResolver 等
  2. 獲取系統/應用資源,包括 AssetManager、PackageManager、Resources、System Service 以及 color、string、drawable 等
  3. 文件,包括獲取緩存文件夾、刪除文件、SharedPreference 相關等
  4. 資料庫(SQLite)相關,包括打開資料庫、刪除資料庫、獲取資料庫路徑等
  5. 其它輔助功能,比如設置 ComponentCallbacks,即監聽配置信息改變、記憶體不足等事件的發生

ContextWrapper、ContextThemeWrapper、ContextImpl 的區別:

  1. ContextWrapper、ContextThemeWrapper 都是 Context 的代理類,二者的區別在於 ContextThemeWrapper 有自己的 Theme 以及 Resource,並且 Resource 可以傳入自己的配置初始化
  2. ContextImpl 是 Context 的主要實現類,Activity、Service 和 Application 的 Base Context 都是由它創建的,即 ContextWrapper 代理的就是 ContextImpl 對象本身
  3. ContextImpl 和 ContextThemeWrapper 的主要區別是, ContextThemeWrapper 有 Configuration 對象,Resource 可以根據這個對象來初始化

Activity Context、Service Context、Application Context、Base Context 的區別:

  1. Activity、Service 和 Application 的 Base Context 都是由 ContextImpl 創建的,且創建的都是 ContextImpl 對象,即它們都是 ContextImpl 的代理類
  2. Service 和 Application 使用同一個 Recource,和 Activity 使用的 Resource 不同
  3. getApplicationContext 返回的就是 Application 對象本身,一般情況下它對應的是應用本身的 Application 對象,但也可能是系統的某個 Application

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

-Advertisement-
Play Games
更多相關文章
  • 一、導讀 愛奇藝的社交業務“泡泡”,擁有日活用戶6千萬+,後臺系統每日高峰期間介面QPS可以達到80K+,與視頻業務的主要區別是泡泡業務更多地引入了與用戶互動相關的數據,讀、寫的量均很大。無論是龐大的數據量,還是相對較高的QPS,使得我們在絕大多數場景下都依賴於高可靠、高性能、以及存儲量巨大的線上緩 ...
  • 一、理想與現實 Apache Flink 是一個分散式流批一體化的開源平臺。Flink 的核心是一個提供數據分發、通信以及自動容錯的流計算引擎。Flink 在流計算之上構建批處理,並且原生的支持迭代計算,記憶體管理以及程式優化。 實時計算(Alibaba Cloud Realtime Compute, ...
  • 概述 MySQL經過多年的發展已然成為最流行的資料庫,廣泛用於互聯網行業,並逐步向各個傳統行業滲透。之所以流行,一方面是其優秀的高併發事務處理的能力,另一方面也得益於MySQL豐富的生態。MySQL在處理OLTP場景下的短查詢效果很好,但對於複雜大查詢則能力有限。最直接一點就是,對於一個SQL語句, ...
  • CREATE PRoc [名字] { @參數 數據類型, @參數 數據類型 OUTPUT[輸入] } AS begin select INSERT UPDATE (SQL) end --基本語句快 --以上是語句庫 --先看看不帶參數的吧 他跟方法一樣 可以帶參數也可以不帶參數(當然我沒用過幾次不帶 ...
  • 在下載了Qualcomm的Hexagon SDK 351版本之後,想跑裡面的examples,然後參照文檔的說,比如在 examples/common/sobel3x3_v60 目錄下麵,先執行了SDK根目錄下麵的 setup_sdk_env.cmd ,然後執行 編譯命令,結果編譯錯誤,輸出如下錯誤 ...
  • Align的作用是為了設置子child的對齊方式,一般作為其他控制項的一個參數。 構造函數 const Align({ Key key, this.alignment = Alignment.center, this.widthFactor, this.heightFactor, Widget chi ...
  • Waiting for another flutter command to release the startup lock… 異常解決 平時我們在開發flutter過程中,在執行flutter packages get 命令 或者 flutter packages upgrade之後, 經常遇到 ...
  • 轉載請標明出處:https:////www.cnblogs.com/tangZH/p/12343786.html APT 是Annotation Processing Tool 的簡稱。 它是註解處理器,在處理Annotation時可以根據源文件中的Annotation生成額外的源文件和其它的文件( ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...