Android 組件化最佳實踐 ARetrofit 原理

来源:https://www.cnblogs.com/vivotech/archive/2019/08/06/11283627.html
-Advertisement-
Play Games

ARetrofit 是一款針對Android組件之間通信的路由框架,實現快速組件化開發的利器。本文主要講述 ARetrofit 實現的原理。 ...


本文首發於 vivo互聯網技術 微信公眾號 https://mp.weixin.qq.com/s/TXFt7ymgQXLJyBOJL8F6xg
作者:朱壹飛

ARetrofit 是一款針對Android組件之間通信的路由框架,實現快速組件化開發的利器。本文主要講述 ARetrofit 實現的原理。

簡介

ARetrofit 是一款針對Android組件之間通信的路由框架,實現快速組件化開發的利器。

源碼鏈接:https://github.com/yifei8/ARetrofit

組件化架構 APP Demo, ARetrofit 使用實例:https://github.com/yifei8/HappyNote

組件化

Android組件化已經不是一個新鮮的概念了,出來了已經有很長一段時間了,大家可以自行Google,可以看到一堆相關的文章。

簡單的來說,所謂的組件就是Android Studio中的Module,每一個Module都遵循高內聚的原則,通過ARetrofit 來實現無耦合的代碼結構,如下圖:

每一個 Module 可單獨作為一個 project 運行,而打包到整體時 Module 之間的通信通過 ARetrofit 完成。

ARetrofit 原理

講原理之前,我想先說說為什麼要ARetrofit。開發ARetrofit 這個項目的思路來源其實是 Retrofit,Retrofit 是Square公司開發的一款針對 Android 網路請求的框架,這裡不對Retrofit展開來講。主要是 Retrofit 框架使用非常多的設計模式,可以說 Retrofit 這個開源項目將Java的設計模式運用到了極致,當然最終提供的API也是非常簡潔的。如此簡潔的API,使得我們APP中的網路模塊實現變得非常輕鬆,並且維護起來也很舒服。因此我覺得有必要將Android組件之間的通信也變得輕鬆,使用者可以優雅的通過簡潔的API就可以實現通信,更重要的是維護起來也非常的舒服。

ARetrofit 基本原理可以簡化為下圖所示:

1.通過註解聲明需要通信的Activity/Fragment或者Class

2.每一個module通過annotationProcessor在編譯時生成待註入的RouteInject的實現類和AInterceptorInject的實現類。

這一步在執行app[build]時會輸出日誌,可以直觀的看到,如下圖所示:

註: AInjecton::Compiler >>> Apt interceptor Processor start... <<<
註: AInjecton::Compiler enclosindClass = null
註: AInjecton::Compiler value = 3
註: AInjecton::Compiler auto generate class = com$$sjtu$$yifei$$eCGVmTMvXG$$AInterceptorInject
註: AInjecton::Compiler add path= 3 and class= LoginInterceptor
....
註: AInjecton::Compiler >>> Apt route Processor start... <<<
註: AInjecton::Compiler enclosindClass = null
註: AInjecton::Compiler value = /login-module/ILoginProviderImpl
註: AInjecton::Compiler enclosindClass = null
註: AInjecton::Compiler value = /login-module/LoginActivity
註: AInjecton::Compiler enclosindClass = null
註: AInjecton::Compiler value = /login-module/Test2Activity
註: AInjecton::Compiler enclosindClass = null
註: AInjecton::Compiler value = /login-module/TestFragment
註: AInjecton::Compiler auto generate class = com$$sjtu$$yifei$$VWpdxWEuUx$$RouteInject
註: AInjecton::Compiler add path= /login-module/TestFragment and class= null
註: AInjecton::Compiler add path= /login-module/LoginActivity and class= null
註: AInjecton::Compiler add path= /login-module/Test2Activity and class= null
註: AInjecton::Compiler add path= /login-module/ILoginProviderImpl and class= null
註: AInjecton::Compiler >>> Apt route Processor succeed <<<

3.將編譯時生成的類註入到RouterRegister中,這個類主要用於維護路由表和攔截器,對應的[build]日誌如下:

TransformPluginLaunch >>> ========== Transform scan start ===========
TransformPluginLaunch >>> ========== Transform scan end cost 0.238 secs and start inserting ===========
TransformPluginLaunch >>> Inserting code to jar >> /Users/yifei/as_workspace/ARetrofit/app/build/intermediates/transforms/TransformPluginLaunch/release/8.jar
TransformPluginLaunch >>> to class >> com/sjtu/yifei/route/RouteRegister.class
InjectClassVisitor >>> inject to class:
InjectClassVisitor >>> com/sjtu/yifei/route/RouteRegister{
InjectClassVisitor >>>        public *** init() {
InjectClassVisitor >>>            register("com.sjtu.yifei.FBQWNfbTpY.com$$sjtu$$yifei$$FBQWNfbTpY$$RouteInject")
InjectClassVisitor >>>            register("com.sjtu.yifei.klBxerzbYV.com$$sjtu$$yifei$$klBxerzbYV$$RouteInject")
InjectClassVisitor >>>            register("com.sjtu.yifei.JmhcMMUhkR.com$$sjtu$$yifei$$JmhcMMUhkR$$RouteInject")
InjectClassVisitor >>>            register("com.sjtu.yifei.fpyxYyTCRm.com$$sjtu$$yifei$$fpyxYyTCRm$$AInterceptorInject")
InjectClassVisitor >>>        }
InjectClassVisitor >>> }
TransformPluginLaunch >>> ========== Transform insert cost 0.017 secs end ===========

4.Routerfit.register(Class<T> service) 這一步主要是通過動態代理模式實現介面中聲明的服務。

前面講的是整體的框架設計思想,便於讀者從全局的覺得來理解ARetrofit的框架的架構。接下來,將待大家個個擊破上面提到的annotationProcessor、 transform在項目中如何使用,以及動態代理、攔截器功能的實現等細節。

一、annotationProcessor生成代碼

annotationProcessor(註解處理器)是javac內置的一個用於編譯時掃描和處理註解(Annotation)的工具。簡單的說,在源代碼編譯階段,通過註解處理器,我們可以獲取源文件內註解(Annotation)相關內容。Android Gradle 2.2 及以上版本提供annotationProcessor的插件。

在ARetrofit中annotationProcessor對應的module是auto-complier,在使用annotationProcessor之前首先需要聲明好註解。關於註解不太瞭解或者遺忘的同學可直接參考我之前寫的Java註解這篇文章,本項目中聲明的註解在auto-annotation這個module中,主要有:

  • @Extra 路由參數

  • @Flags intent flags

  • @Go 路由路徑key

  • @Interceptor 聲明自定義攔截器

  • @RequestCode 路由參數

  • @Route路由

  • @Uri

  • @IMethod 用於標記註冊代碼將插入到此方法中(transform中使用)

  • @Inject 用於標記需要被註入類,最近都將插入到標記了#com.sjtu.yifei.annotation.IMethod的方法中(transform中使用)

創建自定義的註解處理器,具體使用方法可參考利用註解動態生成代碼,本項目中的註解處理器如下所示:

//這是用來註冊註解處理器要處理的源代碼版本。
@SupportedSourceVersion(SourceVersion.RELEASE_8)
//這個註解用來註冊註解處理器要處理的註解類型。有效值為完全限定名(就是帶所在包名和路徑的類全名
@SupportedAnnotationTypes({ANNOTATION_ROUTE, ANNOTATION_GO})
//來註解這個處理器,可以自動生成配置信息
@AutoService(Processor.class)
public class IProcessor extends AbstractProcessor {
     
}

生成代碼的關鍵部分在GenerateAInterceptorInjectImpl 和 GenerateRouteInjectImpl中,以下貼出關鍵代碼:

public void generateAInterceptorInjectImpl(String pkName) {
        try {
            String name = pkName.replace(".",DECOLLATOR) + SUFFIX;
            logger.info(String.format("auto generate class = %s", name));
            TypeSpec.Builder builder = TypeSpec.classBuilder(name)
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(Inject.class)
                    .addSuperinterface(AInterceptorInject.class);
 
            ClassName hashMap = ClassName.get("java.util", "HashMap");
 
            //Map<String, Class<?>>
            TypeName wildcard = WildcardTypeName.subtypeOf(Object.class);
            TypeName classOfAny = ParameterizedTypeName.get(ClassName.get(Class.class), wildcard);
            TypeName string = ClassName.get(Integer.class);
 
            TypeName map = ParameterizedTypeName.get(ClassName.get(Map.class), string, classOfAny);
 
            MethodSpec.Builder injectBuilder = MethodSpec.methodBuilder("getAInterceptors")
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(Override.class)
                    .returns(map)
                    .addStatement("$T interceptorMap = new $T<>()", map, hashMap);
 
            for (Map.Entry<Integer, ClassName> entry : interceptorMap.entrySet()) {
                logger.info("add path= " + entry.getKey() + " and class= " + entry.getValue().simpleName());
                injectBuilder.addStatement("interceptorMap.put($L, $T.class)", entry.getKey(), entry.getValue());
            }
            injectBuilder.addStatement("return interceptorMap");
 
            builder.addMethod(injectBuilder.build());
 
            JavaFile javaFile = JavaFile.builder(pkName, builder.build())
                    .build();
            javaFile.writeTo(filer);
 
        } catch (Exception e) {
            e.printStackTrace();
        }
 
    }
 
public void generateRouteInjectImpl(String pkName) {
        try {
            String name = pkName.replace(".",DECOLLATOR) + SUFFIX;
            logger.info(String.format("auto generate class = %s", name));
            TypeSpec.Builder builder = TypeSpec.classBuilder(name)
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(Inject.class)
                    .addSuperinterface(RouteInject.class);
 
            ClassName hashMap = ClassName.get("java.util", "HashMap");
 
            //Map<String, String>
            TypeName wildcard = WildcardTypeName.subtypeOf(Object.class);
            TypeName classOfAny = ParameterizedTypeName.get(ClassName.get(Class.class), wildcard);
            TypeName string = ClassName.get(String.class);
 
            TypeName map = ParameterizedTypeName.get(ClassName.get(Map.class), string, classOfAny);
 
            MethodSpec.Builder injectBuilder = MethodSpec.methodBuilder("getRouteMap")
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(Override.class)
                    .returns(map)
                    .addStatement("$T routMap = new $T<>()", map, hashMap);
 
            for (Map.Entry<String, ClassName> entry : routMap.entrySet()) {
                logger.info("add path= " + entry.getKey() + " and class= " + entry.getValue().enclosingClassName());
                injectBuilder.addStatement("routMap.put($S, $T.class)", entry.getKey(), entry.getValue());
            }
            injectBuilder.addStatement("return routMap");
 
            builder.addMethod(injectBuilder.build());
 
            JavaFile javaFile = JavaFile.builder(pkName, builder.build())
                    .build();
            javaFile.writeTo(filer);
 
        } catch (Exception e) {
            e.printStackTrace();
        }
 
    }

二、Transform

Android Gradle 工具在 1.5.0 版本後提供了 Transfrom API, 允許第三方 Plugin在打包dex文件之前的編譯過程中操作 .class 文件。這一部分面向高級Android工程師的,面向位元組碼編程,普通工程師可不做瞭解。

寫到這裡也許有人會有這樣一個疑問,既然annotationProcessor這麼好用為什麼還有Transform面向位元組碼註入呢?這裡需要解釋以下,annotationProcessor具有局限性,annotationProcessor只能掃描當前module下的代碼,且對於第三方的jar、aar文件都掃描不到。而Transform就沒有這樣的局限性,在打包dex文件之前的編譯過程中操作.class 文件。

關於Transfrom API在Android Studio中如何使用可以參考Transform API — a real world example,順便提供一下位元組碼指令方便我們讀懂ASM。

本項目中的Transform插件在AInject中,實現源碼TransformPluginLaunch如下,貼出關鍵部分:

/**
 *
 * 標準transform的格式,一般實現transform可以直接拷貝一份重命名即可
 *
 * 兩處todo實現自己的位元組碼增強/優化操作
 */
class TransformPluginLaunch extends Transform implements Plugin<Project> {
 
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
  
        //todo step1: 先掃描
        transformInvocation.inputs.each {
            TransformInput input ->
                input.jarInputs.each { JarInput jarInput ->
                   ...
                }
 
                input.directoryInputs.each { DirectoryInput directoryInput ->
                    //處理完輸入文件之後,要把輸出給下一個任務
                  ...
                }
        }
        
        //todo step2: ...完成代碼註入
        if (InjectInfo.get().injectToClass != null) {
          ...
        }
   
    }
 
    /**
     * 掃描jar包
     * @param jarFile
     */
    static void scanJar(File jarFile, File destFile) {
         
    }
 
    /**
     * 掃描文件
     * @param file
     */
    static void scanFile(File file, File dest) {
       ...
    }
}

註入代碼一般分為兩個步驟:

  • 第一步:掃描
    這一部分主要是掃描的內容有:
    註入類和方法的信息,是AutoRegisterContract的實現類和其中@IMethod,@Inject的方法。
    待註入類的和方法信息,是RouteInject 和 AInterceptorInject實現類且被@Inject註解的。

  • 第二步:註入
    以上掃描的結果,將待註入類註入到註入類的過程。這一過程面向ASM操作,可參考位元組碼指令來讀懂以下的關鍵註入代碼:

class InjectClassVisitor extends ClassVisitor {
...
    class InjectMethodAdapter extends MethodVisitor {
 
        InjectMethodAdapter(MethodVisitor mv) {
            super(Opcodes.ASM5, mv)
        }
 
        @Override
        void visitInsn(int opcode) {
            Log.e(TAG, "inject to class:")
            Log.e(TAG, own + "{")
            Log.e(TAG, "       public *** " + InjectInfo.get().injectToMethodName + "() {")
            if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
                InjectInfo.get().injectClasses.each { injectClass ->
                    injectClass = injectClass.replace('/', '.')
                    Log.e(TAG, "           " + method + "(\"" + injectClass + "\")")
                    mv.visitVarInsn(Opcodes.ALOAD, 0)
                    mv.visitLdcInsn(injectClass)
                    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, own, method, "(Ljava/lang/String;)V", false)
                }
            }
            Log.e(TAG, "       }")
            Log.e(TAG, "}")
            super.visitInsn(opcode)
        }
...
    }
...
}

三、動態代理

定義:為其它對象提供一種代理以控制對這個對象的訪問控制;在某些情況下,客戶不想或者不能直接引用另一個對象,這時候代理對象可以在客戶端和目標對象之間起到中介的作用。

Routerfit.register(Class<T> service) 這裡就是採用動態代理的模式,使得ARetrofit的API非常簡潔,使用者可以優雅定義出路由介面。關於動態代理的學習難度相對來說還比較小,想瞭解的同學可以參考這篇文章java動態代理。

本項目相關源碼:

public final class Routerfit {
...
      private <T> T create(final Class<T> service) {
        RouterUtil.validateServiceInterface(service);
        return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable {
                // If the method is a method from Object then defer to normal invocation.
                if (method.getDeclaringClass() == Object.class) {
                    return method.invoke(this, args);
                }
                ServiceMethod<Object> serviceMethod = (ServiceMethod<Object>) loadServiceMethod(method, args);
                if (!TextUtils.isEmpty(serviceMethod.uristring)) {
                    Call<T> call = (Call<T>) new ActivityCall(serviceMethod);
                    return call.execute();
                }
                try {
                    if (serviceMethod.clazz == null) {
                        throw new RouteNotFoundException("There is no route match the path \"" + serviceMethod.routerPath + "\"");
                    }
                } catch (RouteNotFoundException e) {
                    Toast.makeText(ActivityLifecycleMonitor.getApp(), e.getMessage(), Toast.LENGTH_SHORT).show();
                    e.printStackTrace();
                }
                if (RouterUtil.isSpecificClass(serviceMethod.clazz, Activity.class)) {
                    Call<T> call = (Call<T>) new ActivityCall(serviceMethod);
                    return call.execute();
                } else if (RouterUtil.isSpecificClass(serviceMethod.clazz, Fragment.class)
                        || RouterUtil.isSpecificClass(serviceMethod.clazz, android.app.Fragment.class)) {
                    Call<T> call = new FragmentCall(serviceMethod);
                    return call.execute();
                } else if (serviceMethod.clazz != null) {
                    Call<T> call = new IProviderCall<>(serviceMethod);
                    return call.execute();
                }
 
                if (serviceMethod.returnType != null) {
                    if (serviceMethod.returnType == Integer.TYPE) {
                        return -1;
                    } else if (serviceMethod.returnType == Boolean.TYPE) {
                        return false;
                    } else if (serviceMethod.returnType == Long.TYPE) {
                        return 0L;
                    } else if (serviceMethod.returnType == Double.TYPE) {
                        return 0.0d;
                    } else if (serviceMethod.returnType == Float.TYPE) {
                        return 0.0f;
                    } else if (serviceMethod.returnType == Void.TYPE) {
                        return null;
                    } else if (serviceMethod.returnType == Byte.TYPE) {
                        return (byte)0;
                    } else if (serviceMethod.returnType == Short.TYPE) {
                        return (short)0;
                    } else if (serviceMethod.returnType == Character.TYPE) {
                        return null;
                    }
                }
                return null;
            }
        });
    }
...
}

這裡ServiceMethod是一個非常重要的類,使用了外觀模式,主要用於解析方法中的被註解所有信息並保存起來。

四、攔截器鏈實現

本項目中的攔截器鏈設計,使得使用者可以非常優雅的處理業務邏輯。如下:

@Interceptor(priority = 3)
public class LoginInterceptor implements AInterceptor {
 
    private static final String TAG = "LoginInterceptor";
    @Override
    public void intercept(final Chain chain) {
        //Test2Activity 需要登錄
        if ("/login-module/Test2Activity".equalsIgnoreCase(chain.path())) {
            Routerfit.register(RouteService.class).launchLoginActivity(new ActivityCallback() {
                @Override
                public void onActivityResult(int i, Object data) {
                    if (i == Routerfit.RESULT_OK) {//登錄成功後繼續執行
                        Toast.makeText(ActivityLifecycleMonitor.getTopActivityOrApp(), "登錄成功", Toast.LENGTH_LONG).show();
                        chain.proceed();
                    } else {
                        Toast.makeText(ActivityLifecycleMonitor.getTopActivityOrApp(), "登錄取消/失敗", Toast.LENGTH_LONG).show();
                    }
                }
            });
        } else {
            chain.proceed();
        }
    }
 
}

這一部分實現的思想是參考了okhttp中的攔截器,這裡使用了java設計模式責任鏈模式,具體實現歡迎閱讀源碼。

總結

基本上讀完本文可以對 ARetrofit 的核心原理有了很清晰的理解.簡單來說 ARetrofit 通過 annotationProcessor 在編譯時獲取路由相關內容,通過 ASM 實現了可跨模塊獲取對象,最終通過動態代理實現面向切麵編程(AOP)。

ARetrofit 相對於其他同類型的路由框架來說,其優點是提供了更加簡潔的 API,其中高階用法對開發者提供了更加靈活擴展方式,開發者還可以結合 RxJava 完成複雜的業務場景。具體可以參考 ARetrofit 的基本用法,以及 Issues。

————  參考資料   ————

  1. Java註解:https://www.jianshu.com/p/ef1146a771b5

  2. 利用註解動態生成代碼:https://blog.csdn.net/Gaugamela/article/details/79694302

  3. Transform API — a real world example:https://medium.com/grandcentrix/transform-api-a-real-world-example-cfd49990d3e1

更多內容敬請關註 vivo 互聯網技術 微信公眾號

註:轉載文章請先與微信號:labs2020 聯繫。


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

-Advertisement-
Play Games
更多相關文章
  • 恢復內容開始 Windows下備份mysql 第一步 編寫腳本 --user 用戶名 --password 密碼 --host 地址 --port 埠 --default-character-set 字元編碼 --all-databases 備份整個資料庫 (單單備份一個庫可用--database ...
  • 前言 由於最近在學習node+express,學習到持久化存儲章節需要連接mongodb資料庫,然後之前也有試過安裝mongodb但是失敗了,這次就找了很多資料,終於安裝完成了,故此記錄下來安裝步驟,提供給有需要的人. 安裝流程 1. 下載mongodb安裝包 官網地址:https://www.mo ...
  • Redis事務 事務提供了一種"將多個命令打包,一次性提交並按順序執行"的機制,提交後在事務執行中不會中斷。只有在執行完所有命令後才會繼續執行來自其他客戶的消息。 Redis中的使用 Redis通過multi,exec,discard,watch實現事務功能。 1. multi:開始事務 2. ex ...
  • ==註:wm_concat(str1) 11g 後不支持使用== LISTAGG函數用法 ...
  • mysql資料庫相關流程圖/原理圖 1.mysql主從複製原理圖 mysql主從複製原理是大廠後端的高頻面試題,瞭解mysql主從複製原理非常有必要。 主從複製原理,簡言之,就三步曲,如下: 主資料庫有個bin-log二進位文件,紀錄了所有增刪改Sql語句。(binlog線程) 從資料庫把主資料庫的 ...
  • 1、結構化查詢語言——SQL,關係型資料庫通信的標準語言; 2、關係型資料庫:表的邏輯單元組成,這些表在內部彼此關聯,組成了關係型資料庫; 3、SQL會話:用戶用SQL命令語句與關係型資料庫進行交互時發生的事情。當用戶與資料庫建立鏈接時,會話就建立了,當用戶與資料庫斷開連接時,會話就結束了。 con ...
  • Android 屬性動畫初戰,通過屬性動畫實現類似於美團外賣購物車消失顯示的動效。 ...
  • 前言 有時候我們的應用需要系統級的許可權來實現一些功能(如靜默安裝),這時候需要給應用打上系統簽名,常規操作打包apk,解壓apk,刪除META INF中CERT.RSA和 CERT.SF,然後壓縮,用系統簽名工具簽名,一頓操作後可能十分鐘過去了,實在太過繁瑣,因此我們做了一些簡化,用gradle+s ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...