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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介:

 在开发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.  
  3. public final class Log {  
  4.  
  5. ................................................  
  6.  
  7.     /**  
  8.      * Priority constant for the println method; use Log.v.  
  9.          */  
  10.     public static final int VERBOSE = 2;  
  11.  
  12.     /**  
  13.      * Priority constant for the println method; use Log.d.  
  14.          */  
  15.     public static final int DEBUG = 3;  
  16.  
  17.     /**  
  18.      * Priority constant for the println method; use Log.i.  
  19.          */  
  20.     public static final int INFO = 4;  
  21.  
  22.     /**  
  23.      * Priority constant for the println method; use Log.w.  
  24.          */  
  25.     public static final int WARN = 5;  
  26.  
  27.     /**  
  28.      * Priority constant for the println method; use Log.e.  
  29.          */  
  30.     public static final int ERROR = 6;  
  31.  
  32.     /**  
  33.      * Priority constant for the println method.  
  34.          */  
  35.     public static final int ASSERT = 7;  
  36.  
  37. .....................................................  
  38.  
  39.     public static int v(String tag, String msg) {  
  40.         return println_native(LOG_ID_MAIN, VERBOSE, tag, msg);  
  41.     }  
  42.  
  43.     public static int v(String tag, String msg, Throwable tr) {  
  44.         return println_native(LOG_ID_MAIN, VERBOSE, tag, msg + '\n' + getStackTraceString(tr));  
  45.     }  
  46.  
  47.     public static int d(String tag, String msg) {  
  48.         return println_native(LOG_ID_MAIN, DEBUG, tag, msg);  
  49.     }  
  50.  
  51.     public static int d(String tag, String msg, Throwable tr) {  
  52.         return println_native(LOG_ID_MAIN, DEBUG, tag, msg + '\n' + getStackTraceString(tr));  
  53.     }  
  54.  
  55.     public static int i(String tag, String msg) {  
  56.         return println_native(LOG_ID_MAIN, INFO, tag, msg);  
  57.     }  
  58.  
  59.     public static int i(String tag, String msg, Throwable tr) {  
  60.         return println_native(LOG_ID_MAIN, INFO, tag, msg + '\n' + getStackTraceString(tr));  
  61.     }  
  62.  
  63.     public static int w(String tag, String msg) {  
  64.         return println_native(LOG_ID_MAIN, WARN, tag, msg);  
  65.     }  
  66.  
  67.     public static int w(String tag, String msg, Throwable tr) {  
  68.         return println_native(LOG_ID_MAIN, WARN, tag, msg + '\n' + getStackTraceString(tr));  
  69.     }  
  70.  
  71.     public static int w(String tag, Throwable tr) {  
  72.         return println_native(LOG_ID_MAIN, WARN, tag, getStackTraceString(tr));  
  73.     }  
  74.       
  75.     public static int e(String tag, String msg) {  
  76.         return println_native(LOG_ID_MAIN, ERROR, tag, msg);  
  77.     }  
  78.  
  79.     public static int e(String tag, String msg, Throwable tr) {  
  80.         return println_native(LOG_ID_MAIN, ERROR, tag, msg + '\n' + getStackTraceString(tr));  
  81.     }  
  82.  
  83. ..................................................................  
  84.     /** @hide */ public static native int LOG_ID_MAIN = 0;  
  85.     /** @hide */ public static native int LOG_ID_RADIO = 1;  
  86.     /** @hide */ public static native int LOG_ID_EVENTS = 2;  
  87.     /** @hide */ public static native int LOG_ID_SYSTEM = 3;  
  88.  
  89.     /** @hide */ public static native int println_native(int bufID,  
  90.         int priority, String tag, String msg);  

  定义了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. ** Copyright 2006, The Android Open Source Project  
  4. **  
  5. ** Licensed under the Apache License, Version 2.0 (the "License");   
  6. ** you may not use this file except in compliance with the License.   
  7. ** You may obtain a copy of the License at   
  8. **  
  9. **     http://www.apache.org/licenses/LICENSE-2.0   
  10. **  
  11. ** Unless required by applicable law or agreed to in writing, software   
  12. ** distributed under the License is distributed on an "AS IS" BASIS,   
  13. ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   
  14. ** See the License for the specific language governing permissions and   
  15. ** limitations under the License.  
  16. */  
  17.  
  18. #define LOG_NAMESPACE "log.tag." 
  19. #define LOG_TAG "Log_println" 
  20.  
  21. #include <assert.h>  
  22. #include <cutils/properties.h>  
  23. #include <utils/Log.h>  
  24. #include <utils/String8.h>  
  25.  
  26. #include "jni.h" 
  27. #include "utils/misc.h" 
  28. #include "android_runtime/AndroidRuntime.h" 
  29.  
  30. #define MIN(a,b) ((a<b)?a:b)  
  31.  
  32. namespace android {  
  33.  
  34. struct levels_t {  
  35.     jint verbose;  
  36.     jint debug;  
  37.     jint info;  
  38.     jint warn;  
  39.     jint error;  
  40.     jint assert;  
  41. };  
  42. static levels_t levels;  
  43.  
  44. static int toLevel(const char* value)   
  45. {  
  46.     switch (value[0]) {  
  47.         case 'V'return levels.verbose;  
  48.         case 'D'return levels.debug;  
  49.         case 'I'return levels.info;  
  50.         case 'W'return levels.warn;  
  51.         case 'E'return levels.error;  
  52.         case 'A'return levels.assert;  
  53.         case 'S'return -1; // SUPPRESS  
  54.     }  
  55.     return levels.info;  
  56. }  
  57.  
  58. static jboolean android_util_Log_isLoggable(JNIEnv* env, jobject clazz, jstring tag, jint level)  
  59. {  
  60. #ifndef HAVE_ANDROID_OS  
  61.     return false;  
  62. #else /* HAVE_ANDROID_OS */  
  63.     int len;  
  64.     char key[PROPERTY_KEY_MAX];  
  65.     char buf[PROPERTY_VALUE_MAX];  
  66.  
  67.     if (tag == NULL) {  
  68.         return false;  
  69.     }  
  70.       
  71.     jboolean result = false;  
  72.       
  73.     const char* chars = env->GetStringUTFChars(tag, NULL);  
  74.  
  75.     if ((strlen(chars)+sizeof(LOG_NAMESPACE)) > PROPERTY_KEY_MAX) {  
  76.         jclass clazz = env->FindClass("java/lang/IllegalArgumentException");  
  77.         char buf2[200];  
  78.         snprintf(buf2, sizeof(buf2), "Log tag \"%s\" exceeds limit of %d characters\n",  
  79.                 chars, PROPERTY_KEY_MAX - sizeof(LOG_NAMESPACE));  
  80.  
  81.         // release the chars!  
  82.         env->ReleaseStringUTFChars(tag, chars);  
  83.  
  84.         env->ThrowNew(clazz, buf2);  
  85.         return false;  
  86.     } else {  
  87.         strncpy(key, LOG_NAMESPACE, sizeof(LOG_NAMESPACE)-1);  
  88.         strcpy(key + sizeof(LOG_NAMESPACE) - 1, chars);  
  89.     }  
  90.       
  91.     env->ReleaseStringUTFChars(tag, chars);  
  92.  
  93.     len = property_get(key, buf, "");  
  94.     int logLevel = toLevel(buf);  
  95.     return (logLevel >= 0 && level >= logLevel) ? true : false;  
  96. #endif /* HAVE_ANDROID_OS */  
  97. }  
  98.  
  99. /*  
  100.  * In class android.util.Log:  
  101.  *  public static native int println_native(int buffer, int priority, String tag, String msg)  
  102.  */  
  103. static jint android_util_Log_println_native(JNIEnv* env, jobject clazz,  
  104.         jint bufID, jint priority, jstring tagObj, jstring msgObj)  
  105. {  
  106.     const char* tag = NULL;  
  107.     const char* msg = NULL;  
  108.  
  109.     if (msgObj == NULL) {  
  110.         jclass npeClazz;  
  111.  
  112.         npeClazz = env->FindClass("java/lang/NullPointerException");  
  113.         assert(npeClazz != NULL);  
  114.  
  115.         env->ThrowNew(npeClazz, "println needs a message");  
  116.         return -1;  
  117.     }  
  118.  
  119.     if (bufID < 0 || bufID >= LOG_ID_MAX) {  
  120.         jclass npeClazz;  
  121.  
  122.         npeClazz = env->FindClass("java/lang/NullPointerException");  
  123.         assert(npeClazz != NULL);  
  124.  
  125.         env->ThrowNew(npeClazz, "bad bufID");  
  126.         return -1;  
  127.     }  
  128.  
  129.     if (tagObj != NULL)  
  130.         tag = env->GetStringUTFChars(tagObj, NULL);  
  131.     msg = env->GetStringUTFChars(msgObj, NULL);  
  132.  
  133.     int res = __android_log_buf_write(bufID, (android_LogPriority)priority, tag, msg);  
  134.  
  135.     if (tag != NULL)  
  136.         env->ReleaseStringUTFChars(tagObj, tag);  
  137.     env->ReleaseStringUTFChars(msgObj, msg);  
  138.  
  139.     return res;  
  140. }  
  141.  
  142. /*  
  143.  * JNI registration.  
  144.  */  
  145. static JNINativeMethod gMethods[] = {  
  146.     /* name, signature, funcPtr */  
  147.     { "isLoggable",      "(Ljava/lang/String;I)Z", (void*) android_util_Log_isLoggable },  
  148.     { "println_native",  "(IILjava/lang/String;Ljava/lang/String;)I", (void*) android_util_Log_println_native },  
  149. };  
  150.  
  151. int register_android_util_Log(JNIEnv* env)  
  152. {  
  153.     jclass clazz = env->FindClass("android/util/Log");  
  154.  
  155.     if (clazz == NULL) {  
  156.         LOGE("Can't find android/util/Log");  
  157.         return -1;  
  158.     }  
  159.       
  160.     levels.verbose = env->GetStaticIntField(clazz, env->GetStaticFieldID(clazz, "VERBOSE""I"));  
  161.     levels.debug = env->GetStaticIntField(clazz, env->GetStaticFieldID(clazz, "DEBUG""I"));  
  162.     levels.info = env->GetStaticIntField(clazz, env->GetStaticFieldID(clazz, "INFO""I"));  
  163.     levels.warn = env->GetStaticIntField(clazz, env->GetStaticFieldID(clazz, "WARN""I"));  
  164.     levels.error = env->GetStaticIntField(clazz, env->GetStaticFieldID(clazz, "ERROR""I"));  
  165.     levels.assert = env->GetStaticIntField(clazz, env->GetStaticFieldID(clazz, "ASSERT""I"));  
  166.                   
  167.     return AndroidRuntime::registerNativeMethods(env, "android/util/Log", gMethods, NELEM(gMethods));  
  168. }  
  169.  
  170. }; // namespace android 

   在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(int bufID, int prio, const char *tag, const char *msg)  
  2. {  
  3.     struct iovec vec[3];  
  4.  
  5.     if (!tag)  
  6.         tag = "";  
  7.  
  8.     /* XXX: This needs to go! */  
  9.     if (!strcmp(tag, "HTC_RIL") ||  
  10.         !strncmp(tag, "RIL", 3) || /* Any log tag with "RIL" as the prefix */  
  11.         !strcmp(tag, "AT") ||  
  12.         !strcmp(tag, "GSM") ||  
  13.         !strcmp(tag, "STK") ||  
  14.         !strcmp(tag, "CDMA") ||  
  15.         !strcmp(tag, "PHONE") ||  
  16.         !strcmp(tag, "SMS"))  
  17.             bufID = LOG_ID_RADIO;  
  18.  
  19.     vec[0].iov_base   = (unsigned char *) &prio;  
  20.     vec[0].iov_len    = 1;  
  21.     vec[1].iov_base   = (void *) tag;  
  22.     vec[1].iov_len    = strlen(tag) + 1;  
  23.     vec[2].iov_base   = (void *) msg;  
  24.     vec[2].iov_len    = strlen(msg) + 1;  
  25.  
  26.     return write_to_log(bufID, vec, 3);  

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

 

 
 
  1. static int __write_to_log_init(log_id_t, struct iovec *vec, size_t nr);  
  2. static int (*write_to_log)(log_id_t, struct iovec *vec, size_t nr) = __write_to_log_init; 

  并且初始化为__write_to_log_init函数:

 

 
 
  1. static int __write_to_log_init(log_id_t log_id, struct iovec *vec, size_t nr)  
  2. {  
  3. #ifdef HAVE_PTHREADS  
  4.     pthread_mutex_lock(&log_init_lock);  
  5. #endif  
  6.  
  7.     if (write_to_log == __write_to_log_init) {  
  8.         log_fds[LOG_ID_MAIN] = log_open("/dev/"LOGGER_LOG_MAIN, O_WRONLY);  
  9.         log_fds[LOG_ID_RADIO] = log_open("/dev/"LOGGER_LOG_RADIO, O_WRONLY);  
  10.         log_fds[LOG_ID_EVENTS] = log_open("/dev/"LOGGER_LOG_EVENTS, O_WRONLY);  
  11.         log_fds[LOG_ID_SYSTEM] = log_open("/dev/"LOGGER_LOG_SYSTEM, O_WRONLY);  
  12.  
  13.         write_to_log = __write_to_log_kernel;  
  14.  
  15.         if (log_fds[LOG_ID_MAIN] < 0 || log_fds[LOG_ID_RADIO] < 0 ||  
  16.                 log_fds[LOG_ID_EVENTS] < 0) {  
  17.             log_close(log_fds[LOG_ID_MAIN]);  
  18.             log_close(log_fds[LOG_ID_RADIO]);  
  19.             log_close(log_fds[LOG_ID_EVENTS]);  
  20.             log_fds[LOG_ID_MAIN] = -1;  
  21.             log_fds[LOG_ID_RADIO] = -1;  
  22.             log_fds[LOG_ID_EVENTS] = -1;  
  23.             write_to_log = __write_to_log_null;  
  24.         }  
  25.  
  26.         if (log_fds[LOG_ID_SYSTEM] < 0) {  
  27.             log_fds[LOG_ID_SYSTEM] = log_fds[LOG_ID_MAIN];  
  28.         }  
  29.     }  
  30.  
  31. #ifdef HAVE_PTHREADS  
  32.     pthread_mutex_unlock(&log_init_lock);  
  33. #endif  
  34.  
  35.     return write_to_log(log_id, vec, nr);  

这里我们可以看到,如果是第一次调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. #define LOGGER_LOG_MAIN     "log/main" 
  2. #define LOGGER_LOG_RADIO    "log/radio" 
  3. #define LOGGER_LOG_EVENTS   "log/events" 
  4. #define LOGGER_LOG_SYSTEM   "log/system" 

  接着,把write_to_log函数指针指向__write_to_log_kernel函数:

 

 
 
  1. static int __write_to_log_kernel(log_id_t log_id, struct iovec *vec, size_t nr)  
  2. {  
  3.     ssize_t ret;  
  4.     int log_fd;  
  5.  
  6.     if (/*(int)log_id >= 0 &&*/ (int)log_id < (int)LOG_ID_MAX) {  
  7.         log_fd = log_fds[(int)log_id];  
  8.     } else {  
  9.         return EBADF;  
  10.     }  
  11.  
  12.     do {  
  13.         ret = log_writev(log_fd, vec, nr);  
  14.     } while (ret < 0 && errno == EINTR);  
  15.  
  16.     return ret;  

   函数调用log_writev来实现Log的写入,注意,这里通过一个循环来写入Log,直到写入成功为止。这里log_writev是一个宏,在文件开始的地方定义为:

 

 
 
  1. #if FAKE_LOG_DEVICE  
  2. // This will be defined when building for the host.  
  3. #define log_open(pathname, flags) fakeLogOpen(pathname, flags)  
  4. #define log_writev(filedes, vector, count) fakeLogWritev(filedes, vector, count)  
  5. #define log_close(filedes) fakeLogClose(filedes)  
  6. #else 
  7. #define log_open(pathname, flags) open(pathname, flags)  
  8. #define log_writev(filedes, vector, count) writev(filedes, vector, count)  
  9. #define log_close(filedes) close(filedes)  
  10. #endif 

  这里,我们看到,一般情况下,log_writev就是writev了,这是个常见的批量文件写入函数,就不多说了。
 

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





本文转自 Luoshengyang 51CTO博客,原文链接:http://blog.51cto.com/shyluo/964521,如需转载请自行联系原作者

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
15天前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
41 14
|
18天前
|
Java Linux 数据库
探索安卓开发:打造你的第一款应用
在数字时代的浪潮中,每个人都有机会成为创意的实现者。本文将带你走进安卓开发的奇妙世界,通过浅显易懂的语言和实际代码示例,引导你从零开始构建自己的第一款安卓应用。无论你是编程新手还是希望拓展技术的开发者,这篇文章都将为你打开一扇门,让你的创意和技术一起飞扬。
|
16天前
|
搜索推荐 前端开发 测试技术
打造个性化安卓应用:从设计到开发的全面指南
在这个数字时代,拥有一个定制的移动应用不仅是一种趋势,更是个人或企业品牌的重要延伸。本文将引导你通过一系列简单易懂的步骤,从构思你的应用理念开始,直至实现一个功能齐全的安卓应用。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你提供必要的工具和知识,帮助你将创意转化为现实。
|
16天前
|
Java Android开发 开发者
探索安卓开发:构建你的第一个“Hello World”应用
在安卓开发的浩瀚海洋中,每个新手都渴望扬帆起航。本文将作为你的指南针,引领你通过创建一个简单的“Hello World”应用,迈出安卓开发的第一步。我们将一起搭建开发环境、了解基本概念,并编写第一行代码。就像印度圣雄甘地所说:“你必须成为你希望在世界上看到的改变。”让我们一起开始这段旅程,成为我们想要见到的开发者吧!
24 0
|
存储 Java 开发工具
【Android应用开发】 Android 崩溃日志 本地存储 与 远程保存(二)
【Android应用开发】 Android 崩溃日志 本地存储 与 远程保存(二)
606 0
【Android应用开发】 Android 崩溃日志 本地存储 与 远程保存(二)
|
存储 Android开发
【Android应用开发】 Android 崩溃日志 本地存储 与 远程保存(一)
【Android应用开发】 Android 崩溃日志 本地存储 与 远程保存(一)
671 0
|
28天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
1月前
|
缓存 前端开发 Android开发
安卓开发中的自定义视图:从零到英雄
【10月更文挑战第42天】 在安卓的世界里,自定义视图是一块画布,让开发者能够绘制出独一无二的界面体验。本文将带你走进自定义视图的大门,通过深入浅出的方式,让你从零基础到能够独立设计并实现复杂的自定义组件。我们将探索自定义视图的核心概念、实现步骤,以及如何优化你的视图以提高性能和兼容性。准备好了吗?让我们开始这段创造性的旅程吧!
26 1
|
1月前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
15天前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
40 19
下一篇
DataWorks