`
ydbc
  • 浏览: 711148 次
  • 性别: Icon_minigender_1
  • 来自: 大连
文章分类
社区版块
存档分类
最新评论

Android应用程序框架层和系统运行库层日志系统源代码分析

 
阅读更多

在开发Android应用程序时,少不了使用Log来监控和调试程序的执行。在上一篇文章Android日志系统驱动程序Logger源代码分析中,我们分析了驱动程序Logger的源代码,在前面的文章浅谈Android系统开发中Log的使用一文,我们也简单介绍在应用程序中使Log的方法,在这篇文章中,我们将详细介绍Android应用程序框架层和系统运行库存层日志系统的源代码,使得我们可以更好地理解Android的日志系统的实现。

我们在Android应用程序,一般是调用应用程序框架层的Java接口(android.util.Log)来使用日志系统,这个Java接口通过JNI方法和系统运行库最终调用内核驱动程序Logger把Log写到内核空间中。按照这个调用过程,我们一步步介绍Android应用程序框架层日志系统的源代码。学习完这个过程之后,我们可以很好地理解Android系统的架构,即应用程序层(Application)的接口是如何一步一步地调用到内核空间的。

一. 应用程序框架层日志系统Java接口的实现。

浅谈Android系统开发中Log的使用一文中,我们曾经介绍过Android应用程序框架层日志系统的源代码接口。这里,为了描述方便和文章的完整性,我们重新贴一下这部份的代码,在frameworks/base/core/java/android/util/Log.java文件中,实现日志系统的Java接口:

  1. ................................................
  2. publicfinalclassLog{
  3. ................................................
  4. /**
  5. *Priorityconstantfortheprintlnmethod;useLog.v.
  6. */
  7. publicstaticfinalintVERBOSE=2;
  8. /**
  9. *Priorityconstantfortheprintlnmethod;useLog.d.
  10. */
  11. publicstaticfinalintDEBUG=3;
  12. /**
  13. *Priorityconstantfortheprintlnmethod;useLog.i.
  14. */
  15. publicstaticfinalintINFO=4;
  16. /**
  17. *Priorityconstantfortheprintlnmethod;useLog.w.
  18. */
  19. publicstaticfinalintWARN=5;
  20. /**
  21. *Priorityconstantfortheprintlnmethod;useLog.e.
  22. */
  23. publicstaticfinalintERROR=6;
  24. /**
  25. *Priorityconstantfortheprintlnmethod.
  26. */
  27. publicstaticfinalintASSERT=7;
  28. .....................................................
  29. publicstaticintv(Stringtag,Stringmsg){
  30. returnprintln_native(LOG_ID_MAIN,VERBOSE,tag,msg);
  31. }
  32. publicstaticintv(Stringtag,Stringmsg,Throwabletr){
  33. returnprintln_native(LOG_ID_MAIN,VERBOSE,tag,msg+'\n'+getStackTraceString(tr));
  34. }
  35. publicstaticintd(Stringtag,Stringmsg){
  36. returnprintln_native(LOG_ID_MAIN,DEBUG,tag,msg);
  37. }
  38. publicstaticintd(Stringtag,Stringmsg,Throwabletr){
  39. returnprintln_native(LOG_ID_MAIN,DEBUG,tag,msg+'\n'+getStackTraceString(tr));
  40. }
  41. publicstaticinti(Stringtag,Stringmsg){
  42. returnprintln_native(LOG_ID_MAIN,INFO,tag,msg);
  43. }
  44. publicstaticinti(Stringtag,Stringmsg,Throwabletr){
  45. returnprintln_native(LOG_ID_MAIN,INFO,tag,msg+'\n'+getStackTraceString(tr));
  46. }
  47. publicstaticintw(Stringtag,Stringmsg){
  48. returnprintln_native(LOG_ID_MAIN,WARN,tag,msg);
  49. }
  50. publicstaticintw(Stringtag,Stringmsg,Throwabletr){
  51. returnprintln_native(LOG_ID_MAIN,WARN,tag,msg+'\n'+getStackTraceString(tr));
  52. }
  53. publicstaticintw(Stringtag,Throwabletr){
  54. returnprintln_native(LOG_ID_MAIN,WARN,tag,getStackTraceString(tr));
  55. }
  56. publicstaticinte(Stringtag,Stringmsg){
  57. returnprintln_native(LOG_ID_MAIN,ERROR,tag,msg);
  58. }
  59. publicstaticinte(Stringtag,Stringmsg,Throwabletr){
  60. returnprintln_native(LOG_ID_MAIN,ERROR,tag,msg+'\n'+getStackTraceString(tr));
  61. }
  62. ..................................................................
  63. /**@hide*/publicstaticnativeintLOG_ID_MAIN=0;
  64. /**@hide*/publicstaticnativeintLOG_ID_RADIO=1;
  65. /**@hide*/publicstaticnativeintLOG_ID_EVENTS=2;
  66. /**@hide*/publicstaticnativeintLOG_ID_SYSTEM=3;
  67. /**@hide*/publicstaticnativeintprintln_native(intbufID,
  68. intpriority,Stringtag,Stringmsg);
  69. }
定义了2~7一共6个日志优先级别ID和4个日志缓冲区ID。回忆一下Android日志系统驱动程序Logger源代码分析一文,在Logger驱动程序模块中,定义了log_main、log_events和log_radio三个日志缓冲区,分别对应三个设备文件/dev/log/main、/dev/log/events和/dev/log/radio。这里的4个日志缓冲区的前面3个ID就是对应这三个设备文件的文件描述符了,在下面的章节中,我们将看到这三个文件描述符是如何创建的。在下载下来的Android内核源代码中,第4个日志缓冲区LOG_ID_SYSTEM并没有对应的设备文件,在这种情况下,它和LOG_ID_MAIN对应同一个缓冲区ID,在下面的章节中,我们同样可以看到这两个ID是如何对应到同一个设备文件的。

在整个Log接口中,最关键的地方声明了println_native本地方法,所有的Log接口都是通过调用这个本地方法来实现Log的定入。下面我们就继续分析这个本地方法println_native。

二.应用程序框架层日志系统JNI方法的实现。

在frameworks/base/core/jni/android_util_Log.cpp文件中,实现JNI方法println_native:

  1. /*//device/libs/android_runtime/android_util_Log.cpp
  2. **
  3. **Copyright2006,TheAndroidOpenSourceProject
  4. **
  5. **LicensedundertheApacheLicense,Version2.0(the"License");
  6. **youmaynotusethisfileexceptincompliancewiththeLicense.
  7. **YoumayobtainacopyoftheLicenseat
  8. **
  9. **http://www.apache.org/licenses/LICENSE-2.0
  10. **
  11. **Unlessrequiredbyapplicablelaworagreedtoinwriting,software
  12. **distributedundertheLicenseisdistributedonan"ASIS"BASIS,
  13. **WITHOUTWARRANTIESORCONDITIONSOFANYKIND,eitherexpressorimplied.
  14. **SeetheLicenseforthespecificlanguagegoverningpermissionsand
  15. **limitationsundertheLicense.
  16. */
  17. #defineLOG_NAMESPACE"log.tag."
  18. #defineLOG_TAG"Log_println"
  19. #include<assert.h>
  20. #include<cutils/properties.h>
  21. #include<utils/Log.h>
  22. #include<utils/String8.h>
  23. #include"jni.h"
  24. #include"utils/misc.h"
  25. #include"android_runtime/AndroidRuntime.h"
  26. #defineMIN(a,b)((a<b)?a:b)
  27. namespaceandroid{
  28. structlevels_t{
  29. jintverbose;
  30. jintdebug;
  31. jintinfo;
  32. jintwarn;
  33. jinterror;
  34. jintassert;
  35. };
  36. staticlevels_tlevels;
  37. staticinttoLevel(constchar*value)
  38. {
  39. switch(value[0]){
  40. case'V':returnlevels.verbose;
  41. case'D':returnlevels.debug;
  42. case'I':returnlevels.info;
  43. case'W':returnlevels.warn;
  44. case'E':returnlevels.error;
  45. case'A':returnlevels.assert;
  46. case'S':return-1;//SUPPRESS
  47. }
  48. returnlevels.info;
  49. }
  50. staticjbooleanandroid_util_Log_isLoggable(JNIEnv*env,jobjectclazz,jstringtag,jintlevel)
  51. {
  52. #ifndefHAVE_ANDROID_OS
  53. returnfalse;
  54. #else/*HAVE_ANDROID_OS*/
  55. intlen;
  56. charkey[PROPERTY_KEY_MAX];
  57. charbuf[PROPERTY_VALUE_MAX];
  58. if(tag==NULL){
  59. returnfalse;
  60. }
  61. jbooleanresult=false;
  62. constchar*chars=env->GetStringUTFChars(tag,NULL);
  63. if((strlen(chars)+sizeof(LOG_NAMESPACE))>PROPERTY_KEY_MAX){
  64. jclassclazz=env->FindClass("java/lang/IllegalArgumentException");
  65. charbuf2[200];
  66. snprintf(buf2,sizeof(buf2),"Logtag\"%s\"exceedslimitof%dcharacters\n",
  67. chars,PROPERTY_KEY_MAX-sizeof(LOG_NAMESPACE));
  68. //releasethechars!
  69. env->ReleaseStringUTFChars(tag,chars);
  70. env->ThrowNew(clazz,buf2);
  71. returnfalse;
  72. }else{
  73. strncpy(key,LOG_NAMESPACE,sizeof(LOG_NAMESPACE)-1);
  74. strcpy(key+sizeof(LOG_NAMESPACE)-1,chars);
  75. }
  76. env->ReleaseStringUTFChars(tag,chars);
  77. len=property_get(key,buf,"");
  78. intlogLevel=toLevel(buf);
  79. return(logLevel>=0&&level>=logLevel)?true:false;
  80. #endif/*HAVE_ANDROID_OS*/
  81. }
  82. /*
  83. *Inclassandroid.util.Log:
  84. *publicstaticnativeintprintln_native(intbuffer,intpriority,Stringtag,Stringmsg)
  85. */
  86. staticjintandroid_util_Log_println_native(JNIEnv*env,jobjectclazz,
  87. jintbufID,jintpriority,jstringtagObj,jstringmsgObj)
  88. {
  89. constchar*tag=NULL;
  90. constchar*msg=NULL;
  91. if(msgObj==NULL){
  92. jclassnpeClazz;
  93. npeClazz=env->FindClass("java/lang/NullPointerException");
  94. assert(npeClazz!=NULL);
  95. env->ThrowNew(npeClazz,"printlnneedsamessage");
  96. return-1;
  97. }
  98. if(bufID<0||bufID>=LOG_ID_MAX){
  99. jclassnpeClazz;
  100. npeClazz=env->FindClass("java/lang/NullPointerException");
  101. assert(npeClazz!=NULL);
  102. env->ThrowNew(npeClazz,"badbufID");
  103. return-1;
  104. }
  105. if(tagObj!=NULL)
  106. tag=env->GetStringUTFChars(tagObj,NULL);
  107. msg=env->GetStringUTFChars(msgObj,NULL);
  108. intres=__android_log_buf_write(bufID,(android_LogPriority)priority,tag,msg);
  109. if(tag!=NULL)
  110. env->ReleaseStringUTFChars(tagObj,tag);
  111. env->ReleaseStringUTFChars(msgObj,msg);
  112. returnres;
  113. }
  114. /*
  115. *JNIregistration.
  116. */
  117. staticJNINativeMethodgMethods[]={
  118. /*name,signature,funcPtr*/
  119. {"isLoggable","(Ljava/lang/String;I)Z",(void*)android_util_Log_isLoggable},
  120. {"println_native","(IILjava/lang/String;Ljava/lang/String;)I",(void*)android_util_Log_println_native},
  121. };
  122. intregister_android_util_Log(JNIEnv*env)
  123. {
  124. jclassclazz=env->FindClass("android/util/Log");
  125. if(clazz==NULL){
  126. LOGE("Can'tfindandroid/util/Log");
  127. return-1;
  128. }
  129. levels.verbose=env->GetStaticIntField(clazz,env->GetStaticFieldID(clazz,"VERBOSE","I"));
  130. levels.debug=env->GetStaticIntField(clazz,env->GetStaticFieldID(clazz,"DEBUG","I"));
  131. levels.info=env->GetStaticIntField(clazz,env->GetStaticFieldID(clazz,"INFO","I"));
  132. levels.warn=env->GetStaticIntField(clazz,env->GetStaticFieldID(clazz,"WARN","I"));
  133. levels.error=env->GetStaticIntField(clazz,env->GetStaticFieldID(clazz,"ERROR","I"));
  134. levels.assert=env->GetStaticIntField(clazz,env->GetStaticFieldID(clazz,"ASSERT","I"));
  135. returnAndroidRuntime::registerNativeMethods(env,"android/util/Log",gMethods,NELEM(gMethods));
  136. }
  137. };//namespaceandroid
在gMethods变量中,定义了println_native本地方法对应的函数调用是android_util_Log_println_native。在android_util_Log_println_native函数中,通过了各项参数验证正确后,就调用运行时库函数__android_log_buf_write来实现Log的写入操作。__android_log_buf_write函实实现在liblog库中,它有4个参数,分别缓冲区ID、优先级别ID、Tag字符串和Msg字符串。下面运行时库liblog中的__android_log_buf_write的实现。

三.系统运行库层日志系统的实现。

在系统运行库层liblog库的实现中,内容比较多,这里,我们只关注日志写入操作__android_log_buf_write的相关实现:

  1. int__android_log_buf_write(intbufID,intprio,constchar*tag,constchar*msg)
  2. {
  3. structiovecvec[3];
  4. if(!tag)
  5. tag="";
  6. /*XXX:Thisneedstogo!*/
  7. if(!strcmp(tag,"HTC_RIL")||
  8. !strncmp(tag,"RIL",3)||/*Anylogtagwith"RIL"astheprefix*/
  9. !strcmp(tag,"AT")||
  10. !strcmp(tag,"GSM")||
  11. !strcmp(tag,"STK")||
  12. !strcmp(tag,"CDMA")||
  13. !strcmp(tag,"PHONE")||
  14. !strcmp(tag,"SMS"))
  15. bufID=LOG_ID_RADIO;
  16. vec[0].iov_base=(unsignedchar*)&prio;
  17. vec[0].iov_len=1;
  18. vec[1].iov_base=(void*)tag;
  19. vec[1].iov_len=strlen(tag)+1;
  20. vec[2].iov_base=(void*)msg;
  21. vec[2].iov_len=strlen(msg)+1;
  22. returnwrite_to_log(bufID,vec,3);
  23. }

函数首先是检查传进来的tag参数是否是为HTC_RIL、RIL、AT、GSM、STK、CDMA、PHONE和SMS中的一个,如果是,就无条件地使用ID为LOG_ID_RADIO的日志缓冲区作为写入缓冲区,接着,把传进来的参数prio、tag和msg分别存放在一个向量数组中,调用write_to_log函数来进入下一步操作。write_to_log是一个函数指针,定义在文件开始的位置上:

  1. staticint__write_to_log_init(log_id_t,structiovec*vec,size_tnr);
  2. staticint(*write_to_log)(log_id_t,structiovec*vec,size_tnr)=__write_to_log_init;
并且初始化为__write_to_log_init函数:

  1. staticint__write_to_log_init(log_id_tlog_id,structiovec*vec,size_tnr)
  2. {
  3. #ifdefHAVE_PTHREADS
  4. pthread_mutex_lock(&log_init_lock);
  5. #endif
  6. if(write_to_log==__write_to_log_init){
  7. log_fds[LOG_ID_MAIN]=log_open("/dev/"LOGGER_LOG_MAIN,O_WRONLY);
  8. log_fds[LOG_ID_RADIO]=log_open("/dev/"LOGGER_LOG_RADIO,O_WRONLY);
  9. log_fds[LOG_ID_EVENTS]=log_open("/dev/"LOGGER_LOG_EVENTS,O_WRONLY);
  10. log_fds[LOG_ID_SYSTEM]=log_open("/dev/"LOGGER_LOG_SYSTEM,O_WRONLY);
  11. write_to_log=__write_to_log_kernel;
  12. if(log_fds[LOG_ID_MAIN]<0||log_fds[LOG_ID_RADIO]<0||
  13. log_fds[LOG_ID_EVENTS]<0){
  14. log_close(log_fds[LOG_ID_MAIN]);
  15. log_close(log_fds[LOG_ID_RADIO]);
  16. log_close(log_fds[LOG_ID_EVENTS]);
  17. log_fds[LOG_ID_MAIN]=-1;
  18. log_fds[LOG_ID_RADIO]=-1;
  19. log_fds[LOG_ID_EVENTS]=-1;
  20. write_to_log=__write_to_log_null;
  21. }
  22. if(log_fds[LOG_ID_SYSTEM]<0){
  23. log_fds[LOG_ID_SYSTEM]=log_fds[LOG_ID_MAIN];
  24. }
  25. }
  26. #ifdefHAVE_PTHREADS
  27. pthread_mutex_unlock(&log_init_lock);
  28. #endif
  29. returnwrite_to_log(log_id,vec,nr);
  30. }
这里我们可以看到,如果是第一次调write_to_log函数,write_to_log == __write_to_log_init判断语句就会true,于是执行log_open函数打开设备文件,并把文件描述符保存在log_fds数组中。如果打开/dev/LOGGER_LOG_SYSTEM文件失败,即log_fds[LOG_ID_SYSTEM] < 0,就把log_fds[LOG_ID_SYSTEM]设置为log_fds[LOG_ID_MAIN],这就是我们上面描述的如果不存在ID为LOG_ID_SYSTEM的日志缓冲区,就把LOG_ID_SYSTEM设置为和LOG_ID_MAIN对应的日志缓冲区了。LOGGER_LOG_MAIN、LOGGER_LOG_RADIO、LOGGER_LOG_EVENTS和LOGGER_LOG_SYSTEM四个宏定义在system/core/include/cutils/logger.h文件中:

  1. #defineLOGGER_LOG_MAIN"log/main"
  2. #defineLOGGER_LOG_RADIO"log/radio"
  3. #defineLOGGER_LOG_EVENTS"log/events"
  4. #defineLOGGER_LOG_SYSTEM"log/system"
接着,把write_to_log函数指针指向__write_to_log_kernel函数:

  1. staticint__write_to_log_kernel(log_id_tlog_id,structiovec*vec,size_tnr)
  2. {
  3. ssize_tret;
  4. intlog_fd;
  5. if(/*(int)log_id>=0&&*/(int)log_id<(int)LOG_ID_MAX){
  6. log_fd=log_fds[(int)log_id];
  7. }else{
  8. returnEBADF;
  9. }
  10. do{
  11. ret=log_writev(log_fd,vec,nr);
  12. }while(ret<0&&errno==EINTR);
  13. returnret;
  14. }
函数调用log_writev来实现Log的写入,注意,这里通过一个循环来写入Log,直到写入成功为止。这里log_writev是一个宏,在文件开始的地方定义为:

  1. #ifFAKE_LOG_DEVICE
  2. //Thiswillbedefinedwhenbuildingforthehost.
  3. #definelog_open(pathname,flags)fakeLogOpen(pathname,flags)
  4. #definelog_writev(filedes,vector,count)fakeLogWritev(filedes,vector,count)
  5. #definelog_close(filedes)fakeLogClose(filedes)
  6. #else
  7. #definelog_open(pathname,flags)open(pathname,flags)
  8. #definelog_writev(filedes,vector,count)writev(filedes,vector,count)
  9. #definelog_close(filedes)close(filedes)
  10. #endif
这里,我们看到,一般情况下,log_writev就是writev了,这是个常见的批量文件写入函数,就不多说了。

至些,整个调用过程就结束了。总结一下,首先是从应用程序层调用应用程序框架层的Java接口,应用程序框架层的Java接口通过调用本层的JNI方法进入到系统运行库层的C接口,系统运行库层的C接口通过设备文件来访问内核空间层的Logger驱动程序。这是一个典型的调用过程,很好地诠释Android的系统架构,希望读者好好领会。




http://blog.csdn.net/luoshengyang/article/details/6598703

分享到:
评论

相关推荐

    Android系统源代码情景分析 / 罗升阳著

    在内容上,《Android系统源代码情景分析(含CD光盘1张)》结合使用情景,全面、深入、细致地分析了Android系统的源代码,涉及到Linux内核层、硬件抽象层(HAL)、运行时库层(Runtime)、应用程序框架层...

    Android系统源代码情景分析[罗升阳著]pdf完整清晰版

    在内容上,本书结合使用情景,全面、深入、细致地分析Android系统的源代码,涉及到Linux内核层、硬件抽象层(HAL)、运行时库层(Runtime)、应用程序框架层(Application Framework)以及应用程序层(Application)。...

    Android系统源代码情景分析 [罗升阳 著] [高清版 带书签 840页]

    在内容上,本书结合使用情景,全面、深入、细致地分析Android系统的源代码,涉及到Linux内核层、硬件抽象层(HAL)、运行时库层(Runtime)、应用程序框架层(Application Framework)以及应用程序层(Application)。...

    《Android系统源代码情景分析》 PDF

    在内容上,本书结合使用情景,全面、深入、细致地分析Android系统的源代码,涉及到Linux内核层、硬件抽象层(HAL)、运行时库层(Runtime)、应用程序框架层(Application Framework)以及应用程序层(Application)。...

    罗升阳-Android系统源代码情景分析(带书签完整高清版).pdf

    在内容上,本书结合使用情景,全面、深入、细致地分析Android系统的源代码,涉及到Linux内核层、硬件抽象层(HAL)、运行时库层(Runtime)、应用程序框架层(Application Framework)以及应用程序层(Application)。...

    Android系统源代码情景分析 【罗升阳】

    在内容上,本书结合使用情景,全面、深入、细致地分析Android系统的源代码,涉及到Linux内核层、硬件抽象层(HAL)、运行时库层(Runtime)、应用程序框架层(Application Framework)以及应用程序层(Application)。...

    Android系统源代码情景分析 PDF 完整版 ZIP.002(二个压缩包)

    在内容上,本书结合使用情景,全面、深入、细致地分析Android系统的源代码,涉及到Linux内核层、硬件抽象层(HAL)、运行时库层(Runtime)、应用程序框架层(Application Framework)以及应用程序层(Application)。...

    《Android系统源代码情景分析》

    《Android系统源代码情景分析》随书光盘内容(源代码) 目录如下: 第1篇 初识Android系统 第1章 准备知识 1.1 Linux内核参考书籍 1.2 Android应用程序参考书籍 1.3 下载、编译和运行Android源代码 ...

    android 情景分析源代码

    在内容上,本书结合使用情景,全面、深入、细致地分析了Android系统的源代码,涉及到Linux内核层、硬件抽象层(HAL)、运行时库层(Runtime)、应用程序框架层(Application Framework)以及应用程序层(Application...

    Android系统源代码情景分析-罗升阳-源码

    《Android系统源代码情景分析》随书光盘内容(源代码) 目录如下: 第1篇 初识Android系统 第1章 准备知识 1.1 Linux内核参考书籍 1.2 Android应用程序参考书籍 1.3 下载、编译和运行Android源代码 1.3.1 下载...

    Android系统源代码情景分析 PDF 完整版 ZIP.001(二个压缩包)

    在内容上,本书结合使用情景,全面、深入、细致地分析Android系统的源代码,涉及到Linux内核层、硬件抽象层(HAL)、运行时库层(Runtime)、应用程序框架层(Application Framework)以及应用程序层(Application)。...

    Android系统源代码情景分析光盘

    Android系统源代码情景分析光盘资料 目录 第1篇初识Android系统 第1章 准备知识................................................................ 2 1.1 Linux内核参考书籍......................................

    新版Android开发教程.rar

    将会支持 Google 可能发布的手机操作系统或者应用软件,共同开发名为 Android 的开放源代码的移动 系 统。 谷歌早在 2002 年就进入了移动领域,可是由于目前的手机操作系统企业和手机企业相对封闭,提高了 行业的...

    Android技术内幕.系统卷 pdf

    1.1.4 android系统开发(移植)和应用开发 /11 1.2 获取和编译android的源码 /13 1.2.1 环境配置 /13 1.2.2 获取android源码 /14 1.2.3 编译android的源码及其工具包 /16 1.2.4 运行android系统 /21 1.3 开发...

    android开发资料大全

    android程序编写及调试新手入门 大家一起学Android(Windows篇) android入门与提高必看指南 Android入门逆引手册 Android开发指南中文版、创意设计 【Android系统原理与开发要点详解】/底层 应用 框架 Android核心...

    JAVA上百实例源码以及开源项目源代码

     基于JAVA的UDP服务器模型源代码,内含UDP服务器端模型和UDP客户端模型两个小程序,向JAVA初学者演示UDP C/S结构的原理。 简单聊天软件CS模式 2个目标文件 一个简单的CS模式的聊天软件,用socket实现,比较简单。 ...

    【Android】 Android开发工具之Crash日志打印.zip

    跨平台开发工具如Xamarin、React Native和Flutter,让开发者使用一种语言或框架编写可以在多个操作系统上运行的应用程序。 文档编写与API管理: 文档生成工具可以自动生成代码注释文档,便于团队内外理解和使用...

    【Android】 Android开发工具之http请求日志打印.zip

    跨平台开发工具如Xamarin、React Native和Flutter,让开发者使用一种语言或框架编写可以在多个操作系统上运行的应用程序。 文档编写与API管理: 文档生成工具可以自动生成代码注释文档,便于团队内外理解和使用...

Global site tag (gtag.js) - Google Analytics