• 在前面两篇文章Android日志系统驱动程序Logger源代码分析和Android应用程序框架层和系统运行库层日志系统源代码中,介绍了Android内核空间层、系统运行库层和应用程序框架层日志系统相关的源代码,其中,后一篇文章...

            在前面两篇文章Android日志系统驱动程序Logger源代码分析Android应用程序框架层和系统运行库层日志系统源代码中,介绍了Android内核空间层、系统运行库层和应用程序框架层日志系统相关的源代码,其中,后一篇文章着重介绍了日志的写入操作。为了描述完整性,这篇文章着重介绍日志的读取操作,这就是我们在开发Android应用程序时,经常要用到日志查看工具Logcat了。

    《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

            Logcat工具内置在Android系统中,可以在主机上通过adb logcat命令来查看模拟机上日志信息。Logcat工具的用法很丰富,因此,源代码也比较多,本文并不打算完整地介绍整个Logcat工具的源代码,主要是介绍Logcat读取日志的主线,即从打开日志设备文件到读取日志设备文件的日志记录到输出日志记录的主要过程,希望能起到一个抛砖引玉的作用。

            Logcat工具源代码位于system/core/logcat目录下,只有一个源代码文件logcat.cpp,编译后生成的可执行文件位于out/target/product/generic/system/bin目录下,在模拟机中,可以在/system/bin目录下看到logcat工具。下面我们就分段来阅读logcat.cpp源代码文件。

            一.  Logcat工具的相关数据结构。

            这些数据结构是用来保存从日志设备文件读出来的日志记录:

    struct queued_entry_t {
        union {
            unsigned char buf[LOGGER_ENTRY_MAX_LEN + 1] __attribute__((aligned(4)));
            struct logger_entry entry __attribute__((aligned(4)));
        };
        queued_entry_t* next;
    
        queued_entry_t() {
            next = NULL;
        }
    };
    
    struct log_device_t {
        char* device;
        bool binary;
        int fd;
        bool printed;
        char label;
    
        queued_entry_t* queue;
        log_device_t* next;
    
        log_device_t(char* d, bool b, char l) {
            device = d;
            binary = b;
            label = l;
            queue = NULL;
            next = NULL;
            printed = false;
        }
    
        void enqueue(queued_entry_t* entry) {
            if (this->queue == NULL) {
                this->queue = entry;
            } else {
                queued_entry_t** e = &this->queue;
                while (*e && cmp(entry, *e) >= 0) {
                    e = &((*e)->next);
                }
                entry->next = *e;
                *e = entry;
            }
        }
    };
            其中,宏LOGGER_ENTRY_MAX_LEN和struct logger_entry定义在system/core/include/cutils/logger.h文件中,在Android应用程序框架层和系统运行库层日志系统源代码分析一文有提到,为了方便描述,这里列出这个宏和结构体的定义:

    struct logger_entry {
    	__u16		len;	/* length of the payload */
    	__u16		__pad;	/* no matter what, we get 2 bytes of padding */
    	__s32		pid;	/* generating process's pid */
    	__s32		tid;	/* generating process's tid */
    	__s32		sec;	/* seconds since Epoch */
    	__s32		nsec;	/* nanoseconds */
    	char		msg[0];	/* the entry's payload */
    };
    
    #define LOGGER_ENTRY_MAX_LEN		(4*1024)
            从结构体struct queued_entry_t和struct log_device_t的定义可以看出,每一个log_device_t都包含有一个queued_entry_t队列,queued_entry_t就是对应从日志设备文件读取出来的一条日志记录了,而log_device_t则是对应一个日志设备文件上下文。在Android日志系统驱动程序Logger源代码分析一文中,我们曾提到,Android日志系统有三个日志设备文件,分别是/dev/log/main、/dev/log/events和/dev/log/radio。

            每个日志设备上下文通过其next成员指针连接起来,每个设备文件上下文的日志记录也是通过next指针连接起来。日志记录队例是按时间戳从小到大排列的,这个log_device_t::enqueue函数可以看出,当要插入一条日志记录的时候,先队列头开始查找,直到找到一个时间戳比当前要插入的日志记录的时间戳大的日志记录的位置,然后插入当前日志记录。比较函数cmp的定义如下:

    static int cmp(queued_entry_t* a, queued_entry_t* b) {
        int n = a->entry.sec - b->entry.sec;
        if (n != 0) {
            return n;
        }
        return a->entry.nsec - b->entry.nsec;
    }
            为什么日志记录要按照时间戳从小到大排序呢?原来,Logcat在使用时,可以指定一个参数-t <count>,可以指定只显示最新count条记录,超过count的记录将被丢弃,在这里的实现中,就是要把排在队列前面的多余日记记录丢弃了,因为排在前面的日志记录是最旧的,默认是显示所有的日志记录。在下面的代码中,我们还会继续分析这个过程。
            二. 打开日志设备文件。

            Logcat工具的入口函数main,打开日志设备文件和一些初始化的工作也是在这里进行。main函数的内容也比较多,前面的逻辑都是解析命令行参数。这里假设我们使用logcat工具时,不带任何参数。这不会影响我们分析logcat读取日志的主线,有兴趣的读取可以自行分析解析命令行参数的逻辑。

            分析完命令行参数以后,就开始要创建日志设备文件上下文结构体struct log_device_t了:

        if (!devices) {
            devices = new log_device_t(strdup("/dev/"LOGGER_LOG_MAIN), false, 'm');
            android::g_devCount = 1;
            int accessmode =
                      (mode & O_RDONLY) ? R_OK : 0
                    | (mode & O_WRONLY) ? W_OK : 0;
            // only add this if it's available
            if (0 == access("/dev/"LOGGER_LOG_SYSTEM, accessmode)) {
                devices->next = new log_device_t(strdup("/dev/"LOGGER_LOG_SYSTEM), false, 's');
                android::g_devCount++;
            }
        }

            由于我们假设使用logcat时,不带任何命令行参数,这里的devices变量为NULL,因此,就会默认创建/dev/log/main设备上下文结构体,如果存在/dev/log/system设备文件,也会一并创建。宏LOGGER_LOG_MAIN和LOGGER_LOG_SYSTEM也是定义在system/core/include/cutils/logger.h文件中:

    #define LOGGER_LOG_MAIN		"log/main"
    #define LOGGER_LOG_SYSTEM	"log/system"
            我们在Android日志系统驱动程序Logger源代码分析一文中看到,在Android日志系统驱动程序Logger中,默认是不创建/dev/log/system设备文件的。

            往下看,调用setupOutput()函数来初始化输出文件:

        android::setupOutput();
            setupOutput()函数定义如下:

    static void setupOutput()
    {
    
        if (g_outputFileName == NULL) {
            g_outFD = STDOUT_FILENO;
    
        } else {
            struct stat statbuf;
    
            g_outFD = openLogFile (g_outputFileName);
    
            if (g_outFD < 0) {
                perror ("couldn't open output file");
                exit(-1);
            }
    
            fstat(g_outFD, &statbuf);
    
            g_outByteCount = statbuf.st_size;
        }
    }

            如果我们在执行logcat命令时,指定了-f  <filename>选项,日志内容就输出到filename文件中,否则,就输出到标准输出控制台去了。

            再接下来,就是打开日志设备文件了:

        dev = devices;
        while (dev) {
            dev->fd = open(dev->device, mode);
            if (dev->fd < 0) {
                fprintf(stderr, "Unable to open log device '%s': %s\n",
                    dev->device, strerror(errno));
                exit(EXIT_FAILURE);
            }
    
            if (clearLog) {
                int ret;
                ret = android::clearLog(dev->fd);
                if (ret) {
                    perror("ioctl");
                    exit(EXIT_FAILURE);
                }
            }
    
            if (getLogSize) {
                int size, readable;
    
                size = android::getLogSize(dev->fd);
                if (size < 0) {
                    perror("ioctl");
                    exit(EXIT_FAILURE);
                }
    
                readable = android::getLogReadableSize(dev->fd);
                if (readable < 0) {
                    perror("ioctl");
                    exit(EXIT_FAILURE);
                }
    
                printf("%s: ring buffer is %dKb (%dKb consumed), "
                       "max entry is %db, max payload is %db\n", dev->device,
                       size / 1024, readable / 1024,
                       (int) LOGGER_ENTRY_MAX_LEN, (int) LOGGER_ENTRY_MAX_PAYLOAD);
            }
    
            dev = dev->next;
        }
            如果执行logcat命令的目的是清空日志,即clearLog为true,则调用android::clearLog函数来执行清空日志操作:

    static int clearLog(int logfd)
    {
        return ioctl(logfd, LOGGER_FLUSH_LOG);
    }
            这里是通过标准的文件函数ioctl函数来执行日志清空操作,具体可以参考logger驱动程序的实现。

            如果执行logcat命令的目的是获取日志内存缓冲区的大小,即getLogSize为true,通过调用android::getLogSize函数实现:

    /* returns the total size of the log's ring buffer */
    static int getLogSize(int logfd)
    {
        return ioctl(logfd, LOGGER_GET_LOG_BUF_SIZE);
    }
            如果为负数,即size < 0,就表示出错了,退出程序。

            接着验证日志缓冲区可读内容的大小,即调用android::getLogReadableSize函数:

    /* returns the readable size of the log's ring buffer (that is, amount of the log consumed) */
    static int getLogReadableSize(int logfd)
    {
        return ioctl(logfd, LOGGER_GET_LOG_LEN);
    }
            如果返回负数,即readable < 0,也表示出错了,退出程序。

            接下去的printf语句,就是输出日志缓冲区的大小以及可读日志的大小到控制台去了。

            继续看下看代码,如果执行logcat命令的目的是清空日志或者获取日志的大小信息,则现在就完成使命了,可以退出程序了:

        if (getLogSize) {
            return 0;
        }
        if (clearLog) {
            return 0;
        }
            否则,就要开始读取设备文件的日志记录了:

        android::readLogLines(devices);
           至此日志设备文件就打开并且初始化好了,下面,我们继续分析从日志设备文件读取日志记录的操作,即readLogLines函数。

           三. 读取日志设备文件。

           读取日志设备文件内容的函数是readLogLines函数:

    static void readLogLines(log_device_t* devices)
    {
        log_device_t* dev;
        int max = 0;
        int ret;
        int queued_lines = 0;
        bool sleep = true;
    
        int result;
        fd_set readset;
    
        for (dev=devices; dev; dev = dev->next) {
            if (dev->fd > max) {
                max = dev->fd;
            }
        }
    
        while (1) {
            do {
                timeval timeout = { 0, 5000 /* 5ms */ }; // If we oversleep it's ok, i.e. ignore EINTR.
                FD_ZERO(&readset);
                for (dev=devices; dev; dev = dev->next) {
                    FD_SET(dev->fd, &readset);
                }
                result = select(max + 1, &readset, NULL, NULL, sleep ? NULL : &timeout);
            } while (result == -1 && errno == EINTR);
    
            if (result >= 0) {
                for (dev=devices; dev; dev = dev->next) {
                    if (FD_ISSET(dev->fd, &readset)) {
                        queued_entry_t* entry = new queued_entry_t();
                        /* NOTE: driver guarantees we read exactly one full entry */
                        ret = read(dev->fd, entry->buf, LOGGER_ENTRY_MAX_LEN);
                        if (ret < 0) {
                            if (errno == EINTR) {
                                delete entry;
                                goto next;
                            }
                            if (errno == EAGAIN) {
                                delete entry;
                                break;
                            }
                            perror("logcat read");
                            exit(EXIT_FAILURE);
                        }
                        else if (!ret) {
                            fprintf(stderr, "read: Unexpected EOF!\n");
                            exit(EXIT_FAILURE);
                        }
    
                        entry->entry.msg[entry->entry.len] = '\0';
    
                        dev->enqueue(entry);
                        ++queued_lines;
                    }
                }
    
                if (result == 0) {
                    // we did our short timeout trick and there's nothing new
                    // print everything we have and wait for more data
                    sleep = true;
                    while (true) {
                        chooseFirst(devices, &dev);
                        if (dev == NULL) {
                            break;
                        }
                        if (g_tail_lines == 0 || queued_lines <= g_tail_lines) {
                            printNextEntry(dev);
                        } else {
                            skipNextEntry(dev);
                        }
                        --queued_lines;
                    }
    
                    // the caller requested to just dump the log and exit
                    if (g_nonblock) {
                        exit(0);
                    }
                } else {
                    // print all that aren't the last in their list
                    sleep = false;
                    while (g_tail_lines == 0 || queued_lines > g_tail_lines) {
                        chooseFirst(devices, &dev);
                        if (dev == NULL || dev->queue->next == NULL) {
                            break;
                        }
                        if (g_tail_lines == 0) {
                            printNextEntry(dev);
                        } else {
                            skipNextEntry(dev);
                        }
                        --queued_lines;
                    }
                }
            }
    next:
            ;
        }
    }
            由于可能同时打开了多个日志设备文件,这里使用select函数来同时监控哪个文件当前可读:

        do {
            timeval timeout = { 0, 5000 /* 5ms */ }; // If we oversleep it's ok, i.e. ignore EINTR.
            FD_ZERO(&readset);
            for (dev=devices; dev; dev = dev->next) {
                FD_SET(dev->fd, &readset);
            }
            result = select(max + 1, &readset, NULL, NULL, sleep ? NULL : &timeout);
        } while (result == -1 && errno == EINTR);
           如果result >= 0,就表示有日志设备文件可读或者超时。接着,用一个for语句检查哪个设备文件可读,即FD_ISSET(dev->fd, &readset)是否为true,如果为true,表明可读,就要进一步通过read函数将日志读出,注意,每次只读出一条日志记录:

    	    for (dev=devices; dev; dev = dev->next) {
                    if (FD_ISSET(dev->fd, &readset)) {
                        queued_entry_t* entry = new queued_entry_t();
                        /* NOTE: driver guarantees we read exactly one full entry */
                        ret = read(dev->fd, entry->buf, LOGGER_ENTRY_MAX_LEN);
                        if (ret < 0) {
                            if (errno == EINTR) {
                                delete entry;
                                goto next;
                            }
                            if (errno == EAGAIN) {
                                delete entry;
                                break;
                            }
                            perror("logcat read");
                            exit(EXIT_FAILURE);
                        }
                        else if (!ret) {
                            fprintf(stderr, "read: Unexpected EOF!\n");
                            exit(EXIT_FAILURE);
                        }
    
                        entry->entry.msg[entry->entry.len] = '\0';
    
                        dev->enqueue(entry);
                        ++queued_lines;
                    }
                }
            调用read函数之前,先创建一个日志记录项entry,接着调用read函数将日志读到entry->buf中,最后调用dev->enqueue(entry)将日志记录加入到日志队例中去。同时,把当前的日志记录数保存在queued_lines变量中。

            继续进一步处理日志:

                if (result == 0) {
                    // we did our short timeout trick and there's nothing new
                    // print everything we have and wait for more data
                    sleep = true;
                    while (true) {
                        chooseFirst(devices, &dev);
                        if (dev == NULL) {
                            break;
                        }
                        if (g_tail_lines == 0 || queued_lines <= g_tail_lines) {
                            printNextEntry(dev);
                        } else {
                            skipNextEntry(dev);
                        }
                        --queued_lines;
                    }
    
                    // the caller requested to just dump the log and exit
                    if (g_nonblock) {
                        exit(0);
                    }
                } else {
                    // print all that aren't the last in their list
                    sleep = false;
                    while (g_tail_lines == 0 || queued_lines > g_tail_lines) {
                        chooseFirst(devices, &dev);
                        if (dev == NULL || dev->queue->next == NULL) {
                            break;
                        }
                        if (g_tail_lines == 0) {
                            printNextEntry(dev);
                        } else {
                            skipNextEntry(dev);
                        }
                        --queued_lines;
                    }
                }
            如果result == 0,表明是等待超时了,目前没有新的日志可读,这时候就要先处理之前已经读出来的日志。调用chooseFirst选择日志队列不为空,且日志队列中的第一个日志记录的时间戳为最小的设备,即先输出最旧的日志:

    static void chooseFirst(log_device_t* dev, log_device_t** firstdev) {
        for (*firstdev = NULL; dev != NULL; dev = dev->next) {
            if (dev->queue != NULL && (*firstdev == NULL || cmp(dev->queue, (*firstdev)->queue) < 0)) {
                *firstdev = dev;
            }
        }
    }
           如果存在这样的日志设备,接着判断日志记录是应该丢弃还是输出。前面我们说过,如果执行logcat命令时,指定了参数-t <count>,那么就会只显示最新的count条记录,其它的旧记录将被丢弃:

        if (g_tail_lines == 0 || queued_lines <= g_tail_lines) {
             printNextEntry(dev);
        } else {
             skipNextEntry(dev);
        }
             g_tail_lines表示显示最新记录的条数,如果为0,就表示全部显示。如果g_tail_lines == 0或者queued_lines <= g_tail_lines,就表示这条日志记录应该输出,否则就要丢弃了。每处理完一条日志记录,queued_lines就减1,这样,最新的g_tail_lines就可以输出出来了。

            如果result > 0,表明有新的日志可读,这时候的处理方式与result == 0的情况不同,因为这时候还有新的日志可读,所以就不能先急着处理之前已经读出来的日志。这里,分两种情况考虑,如果能设置了只显示最新的g_tail_lines条记录,并且当前已经读出来的日志记录条数已经超过g_tail_lines,就要丢弃,剩下的先不处理,等到下次再来处理;如果没有设备显示最新的g_tail_lines条记录,即g_tail_lines == 0,这种情况就和result  == 0的情况处理方式一样,先处理所有已经读出的日志记录,再进入下一次循环。希望读者可以好好体会这段代码:

        while (g_tail_lines == 0 || queued_lines > g_tail_lines) {
             chooseFirst(devices, &dev);
             if (dev == NULL || dev->queue->next == NULL) {
                  break;
             }
             if (g_tail_lines == 0) {
                  printNextEntry(dev);
             } else {
                  skipNextEntry(dev);
             }
             --queued_lines;
        }
            丢弃日志记录的函数skipNextEntry实现如下:

    static void skipNextEntry(log_device_t* dev) {
        maybePrintStart(dev);
        queued_entry_t* entry = dev->queue;
        dev->queue = entry->next;
        delete entry;
    }
            这里只是简单地跳过日志队列头,这样就把最旧的日志丢弃了。

            printNextEntry函数处理日志输出,下一节中继续分析。

            四. 输出日志设备文件的内容。

            从前面的分析中看出,最终日志设备文件内容的输出是通过printNextEntry函数进行的:

    static void printNextEntry(log_device_t* dev) {
        maybePrintStart(dev);
        if (g_printBinary) {
            printBinary(&dev->queue->entry);
        } else {
            processBuffer(dev, &dev->queue->entry);
        }
        skipNextEntry(dev);
    }
            g_printBinary为true时,以二进制方式输出日志内容到指定的文件中:

    void printBinary(struct logger_entry *buf)
    {
        size_t size = sizeof(logger_entry) + buf->len;
        int ret;
        
        do {
            ret = write(g_outFD, buf, size);
        } while (ret < 0 && errno == EINTR);
    }
           我们关注g_printBinary为false的情况,调用processBuffer进一步处理:

    static void processBuffer(log_device_t* dev, struct logger_entry *buf)
    {
        int bytesWritten = 0;
        int err;
        AndroidLogEntry entry;
        char binaryMsgBuf[1024];
    
        if (dev->binary) {
            err = android_log_processBinaryLogBuffer(buf, &entry, g_eventTagMap,
                    binaryMsgBuf, sizeof(binaryMsgBuf));
            //printf(">>> pri=%d len=%d msg='%s'\n",
            //    entry.priority, entry.messageLen, entry.message);
        } else {
            err = android_log_processLogBuffer(buf, &entry);
        }
        if (err < 0) {
            goto error;
        }
    
        if (android_log_shouldPrintLine(g_logformat, entry.tag, entry.priority)) {
            if (false && g_devCount > 1) {
                binaryMsgBuf[0] = dev->label;
                binaryMsgBuf[1] = ' ';
                bytesWritten = write(g_outFD, binaryMsgBuf, 2);
                if (bytesWritten < 0) {
                    perror("output error");
                    exit(-1);
                }
            }
    
            bytesWritten = android_log_printLogLine(g_logformat, g_outFD, &entry);
    
            if (bytesWritten < 0) {
                perror("output error");
                exit(-1);
            }
        }
    
        g_outByteCount += bytesWritten;
    
        if (g_logRotateSizeKBytes > 0 
            && (g_outByteCount / 1024) >= g_logRotateSizeKBytes
        ) {
            rotateLogs();
        }
    
    error:
        //fprintf (stderr, "Error processing record\n");
        return;
    }

            当dev->binary为true,日志记录项是二进制形式,不同于我们在Android日志系统驱动程序Logger源代码分析一文中提到的常规格式:

            struct logger_entry | priority | tag | msg
            这里我们不关注这种情况,有兴趣的读者可以自已分析,android_log_processBinaryLogBuffer函数定义在system/core/liblog/logprint.c文件中,它的作用是将一条二进制形式的日志记录转换为ASCII形式,并保存在entry参数中,它的原型为:

    /**
     * Convert a binary log entry to ASCII form.
     *
     * For convenience we mimic the processLogBuffer API.  There is no
     * pre-defined output length for the binary data, since we're free to format
     * it however we choose, which means we can't really use a fixed-size buffer
     * here.
     */
    int android_log_processBinaryLogBuffer(struct logger_entry *buf,
        AndroidLogEntry *entry, const EventTagMap* map, char* messageBuf,
        int messageBufLen);
            通常情况下,dev->binary为false,调用android_log_processLogBuffer函数将日志记录由logger_entry格式转换为AndroidLogEntry格式。logger_entry格式在在Android日志系统驱动程序Logger源代码分析一文中已经有详细描述,这里不述;AndroidLogEntry结构体定义在system/core/include/cutils/logprint.h中:

    typedef struct AndroidLogEntry_t {
        time_t tv_sec;
        long tv_nsec;
        android_LogPriority priority;
        pid_t pid;
        pthread_t tid;
        const char * tag;
        size_t messageLen;
        const char * message;
    } AndroidLogEntry;
    
            android_LogPriority是一个枚举类型,定义在system/core/include/android/log.h文件中:

    /*
     * Android log priority values, in ascending priority order.
     */
    typedef enum android_LogPriority {
        ANDROID_LOG_UNKNOWN = 0,
        ANDROID_LOG_DEFAULT,    /* only for SetMinPriority() */
        ANDROID_LOG_VERBOSE,
        ANDROID_LOG_DEBUG,
        ANDROID_LOG_INFO,
        ANDROID_LOG_WARN,
        ANDROID_LOG_ERROR,
        ANDROID_LOG_FATAL,
        ANDROID_LOG_SILENT,     /* only for SetMinPriority(); must be last */
    } android_LogPriority;
    
            android_log_processLogBuffer定义在system/core/liblog/logprint.c文件中:

    /**
     * Splits a wire-format buffer into an AndroidLogEntry
     * entry allocated by caller. Pointers will point directly into buf
     *
     * Returns 0 on success and -1 on invalid wire format (entry will be
     * in unspecified state)
     */
    int android_log_processLogBuffer(struct logger_entry *buf,
                                     AndroidLogEntry *entry)
    {
        size_t tag_len;
    
        entry->tv_sec = buf->sec;
        entry->tv_nsec = buf->nsec;
        entry->priority = buf->msg[0];
        entry->pid = buf->pid;
        entry->tid = buf->tid;
        entry->tag = buf->msg + 1;
        tag_len = strlen(entry->tag);
        entry->messageLen = buf->len - tag_len - 3;
        entry->message = entry->tag + tag_len + 1;
    
        return 0;
    }
            结合logger_entry结构体中日志项的格式定义(struct logger_entry | priority | tag | msg),这个函数很直观,不再累述。

            调用完android_log_processLogBuffer函数后,日志记录的具体信息就保存在本地变量entry中了,接着调用android_log_shouldPrintLine函数来判断这条日志记录是否应该输出。

            在分析android_log_shouldPrintLine函数之前,我们先了解数据结构AndroidLogFormat,这个结构体定义在system/core/liblog/logprint.c文件中:

    struct AndroidLogFormat_t {
        android_LogPriority global_pri;
        FilterInfo *filters;
        AndroidLogPrintFormat format;
    };
            AndroidLogPrintFormat也是定义在system/core/liblog/logprint.c文件中:

    typedef struct FilterInfo_t {
        char *mTag;
        android_LogPriority mPri;
        struct FilterInfo_t *p_next;
    } FilterInfo;
            因此,可以看出,AndroidLogFormat结构体定义了日志过滤规范。在logcat.c文件中,定义了变量

    static AndroidLogFormat * g_logformat;
           这个变量是在main函数里面进行分配的:

    g_logformat = android_log_format_new();
           在main函数里面,在分析logcat命令行参数时,会将g_logformat进行初始化,有兴趣的读者可以自行分析。

           回到android_log_shouldPrintLine函数中,它定义在system/core/liblog/logprint.c文件中:

    /**
     * returns 1 if this log line should be printed based on its priority
     * and tag, and 0 if it should not
     */
    int android_log_shouldPrintLine (
            AndroidLogFormat *p_format, const char *tag, android_LogPriority pri)
    {
        return pri >= filterPriForTag(p_format, tag);
    }
           这个函数判断在p_format中根据tag值,找到对应的pri值,如果返回来的pri值小于等于参数传进来的pri值,那么就表示这条日志记录可以输出。我们来看filterPriForTag函数的实现:

    static android_LogPriority filterPriForTag(
            AndroidLogFormat *p_format, const char *tag)
    {
        FilterInfo *p_curFilter;
    
        for (p_curFilter = p_format->filters
                ; p_curFilter != NULL
                ; p_curFilter = p_curFilter->p_next
        ) {
            if (0 == strcmp(tag, p_curFilter->mTag)) {
                if (p_curFilter->mPri == ANDROID_LOG_DEFAULT) {
                    return p_format->global_pri;
                } else {
                    return p_curFilter->mPri;
                }
            }
        }
    
        return p_format->global_pri;
    }
            如果在p_format中找到与tag值对应的filter,并且该filter的mPri不等于ANDROID_LOG_DEFAULT,那么就返回该filter的成员变量mPri的值;其它情况下,返回p_format->global_pri的值。

            回到processBuffer函数中,如果执行完android_log_shouldPrintLine函数后,表明当前日志记录应当输出,则调用android_log_printLogLine函数来输出日志记录到文件fd中, 这个函数也是定义在system/core/liblog/logprint.c文件中:

    int android_log_printLogLine(
        AndroidLogFormat *p_format,
        int fd,
        const AndroidLogEntry *entry)
    {
        int ret;
        char defaultBuffer[512];
        char *outBuffer = NULL;
        size_t totalLen;
    
        outBuffer = android_log_formatLogLine(p_format, defaultBuffer,
                sizeof(defaultBuffer), entry, &totalLen);
    
        if (!outBuffer)
            return -1;
    
        do {
            ret = write(fd, outBuffer, totalLen);
        } while (ret < 0 && errno == EINTR);
    
        if (ret < 0) {
            fprintf(stderr, "+++ LOG: write failed (errno=%d)\n", errno);
            ret = 0;
            goto done;
        }
    
        if (((size_t)ret) < totalLen) {
            fprintf(stderr, "+++ LOG: write partial (%d of %d)\n", ret,
                    (int)totalLen);
            goto done;
        }
    
    done:
        if (outBuffer != defaultBuffer) {
            free(outBuffer);
        }
    
        return ret;
    }
            这个函数的作用就是把AndroidLogEntry格式的日志记录按照指定的格式AndroidLogFormat进行输出了,这里,不再进一步分析这个函数。

            processBuffer函数的最后,还有一个rotateLogs的操作:

    static void rotateLogs()
    {
        int err;
    
        // Can't rotate logs if we're not outputting to a file
        if (g_outputFileName == NULL) {
            return;
        }
    
        close(g_outFD);
    
        for (int i = g_maxRotatedLogs ; i > 0 ; i--) {
            char *file0, *file1;
    
            asprintf(&file1, "%s.%d", g_outputFileName, i);
    
            if (i - 1 == 0) {
                asprintf(&file0, "%s", g_outputFileName);
            } else {
                asprintf(&file0, "%s.%d", g_outputFileName, i - 1);
            }
    
            err = rename (file0, file1);
    
            if (err < 0 && errno != ENOENT) {
                perror("while rotating log files");
            }
    
            free(file1);
            free(file0);
        }
    
        g_outFD = openLogFile (g_outputFileName);
    
        if (g_outFD < 0) {
            perror ("couldn't open output file");
            exit(-1);
        }
    
        g_outByteCount = 0;
    
    }
            这个函数只有在执行logcat命令时,指定了-f <filename>参数时,即g_outputFileName不为NULL时才起作用。它的作用是在将日志记录循环输出到一组文件中。例如,指定-f参数为logfile,g_maxRotatedLogs为3,则这组文件分别为:

            logfile,logfile.1,logfile.2,logfile.3

            当当前输入到logfile文件的日志记录大小g_outByteCount大于等于g_logRotateSizeKBytes时,就要将logfile.2的内容移至logfile.3中,同时将logfile.1的内容移至logfile.2中,同时logfle的内容移至logfile.1中,再重新打开logfile文件进入后续输入。这样做的作用是不至于使得日志文件变得越来越来大,以至于占用过多的磁盘空间,而是只在磁盘上保存一定量的最新的日志记录。这样,旧的日志记录就会可能被新的日志记录所覆盖。

            至此,关于Android日志系统源代码,我们就完整地分析完了,其中包括位于内核空间的驱动程序Logger源代码分析,还有位于应用程序框架层和系统运行库层的日志写入操作接口源代码分析和用于日志读取的工具Logcat源代码分析,希望能够帮助读者对Android的日志系统有一个清晰的认识。

    老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

    展开全文
  • 为了有利于项目维护、增强代码可读性、提升 Code Review 效率以及规范团队安卓开发,故提出以下安卓开发规范。

    前言

    为了有利于项目维护、增强代码可读性、提升 Code Review 效率以及规范团队安卓开发,故提出以下安卓开发规范。

    一、Android Studio 规范

    1. 尽量使用最新的稳定版 Android Studio 进行开发;
    2. 编码格式统一为 UTF-8
    3. 编辑完 .java、.kt、.xml 等文件后一定要 格式化,格式化,格式化(如果团队有公共的样式包,那就遵循它,否则统一使用 AS 默认模板即可,Mac 下可以使用快捷键 cmd + alt + L 进行代码格式化,Window 下可以使用快捷键 ctrl + alt + L 进行代码格式化);
    4. 删除多余的 import,减少警告出现,Mac 下可以使用快捷键 ctrl + alt + O 进行 import 优化,Window 下可以使用快捷键 ctrl + alt + O 进行 import 优化;

    二、Kotlin命名

    代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。正确的英文拼写和语法可以让阅读者易于理解,避免歧义。

    注意:即使纯拼音命名方式也要避免采用。但 alibabataobaoyoukuhangzhou 等国际通用的名称,可视同英文。

    2.1 包名

    包名全部小写,连续的单词只是简单地连接起来,不使用下划线,采用反域名命名规则,全部使用小写字母。一级包名是顶级域名,通常为 comedugovnetorg 等,二级包名为公司名,三级包名根据应用进行命名,后面就是对包名的划分了,关于包名的划分,推荐采用 PBF(按功能分包 Package By Feature)。

    // BAD
    com.RayWenderlich.funky_widget
    // GOOD
    com.raywenderlich.funkywidget
    

    2.2 类名

    类名通常是名词或名词短语,接口名称有时可能是形容词或形容词短语。现在还没有特定的规则或行之有效的约定来命名注解类型。

    名词,采用大驼峰命名法,尽量避免缩写,除非该缩写是众所周知的, 比如 HTML、URL,如果类名称中包含单词缩写,则单词缩写的每个字母均应大写。

    测试类的命名以它要测试的类的名称开始,以 Test 结束。例如:HashTestHashIntegrationTest

    接口(interface):命名规则与类一样采用大驼峰命名法,多以 able 或 ible 结尾,如 interface Runnableinterface Accessible;或者以 I 为前缀。

    描述 示例
    Activity 类 以Activity 为后缀标识 欢迎页面类 WelcomeActivity
    Adapter 类 以Adapter 为后缀标识 新闻详情适配器 NewsDetailAdapter
    解析类 以Parser 为后缀标识 首页解析类 HomePosterParser
    工具方法类 以Util、Tool、Manager 为后缀标识 线程池管理类:ThreadPoolManager,日志工具类:LogUtil,网络请求工具类:HttpTool
    数据库类 以 DBHelper 后缀标识 新闻数据库:NewsDBHelper
    Service 类 以 Service 为后缀标识 时间服务 TimeService 用户组件服务IUserService
    BroadcastReceiver 类 以 Receiver 为后缀标识 推送接收 JPushReceiver
    ContentProvider 类 以 Provider 为后缀标识 ShareProvider
    自定义的共享基础类 以 Base 为前缀 BaseActivity, BaseFragment

    上面提到工具类以 Util 和 Tool 为后缀,那么 Util 和 Tool 的区别是什么?Util 是无业务逻辑的,Tool 是有业务逻辑的。比如 HttpUtil 只是包含了基本网络请求,而 HttpTool 中包含了项目的一些配置,如在每个请求增加 token 。也可以这么说,HttpUtil 可以跨项目使用,而 HttpTool 只能在该项目中使用。

    2.3 方法名

    方法名都以 lowerCamelCase 风格编写。

    方法名通常是动词或动词短语。

    方法 说明
    initXX() 初始化相关方法,如初始化布局 initView()
    isXX(), checkXX() 方法返回值为 boolean 型
    handleXX(), processXX() 对数据进行处理的方法
    displayXX(), showXX() 弹出提示框和提示信息
    resetXX() 重置数据
    clearXX() 清除数据
    drawXX() 绘制数据或效果相关的
    setXX() 设置某个属性值
    getXX() 返回某个值或单个对象
    listXX() 返回多个对象
    countXX() 返回统计值
    saveXX(), insertXX() 保存或插入数据
    removeXX(), deleteXX() 移除数据或者视图等,如 removeView()
    updateXX() 更新数据

    2.4 常量名

    常量名命名模式为 CONSTANT_CASE,全部字母大写,用下划线分隔单词。那到底什么算是一个常量?

    每个常量都是一个 static final 字段,但不是所有 static final 字段都是常量。在决定一个字段是否是一个常量时,得考虑它是否真的感觉像是一个常量。例如,如果观测任何一个该实例的状态是可变的,则它几乎肯定不会是一个常量。只是永远不打算改变的对象一般是不够的,它要真的一直不变才能将它示为常量。

    // Constants
    static final int NUMBER = 5;
    static final ImmutableListNAMES = ImmutableList.of("Ed", "Ann");
    static final Joiner COMMA_JOINER = Joiner.on(','); // because Joiner is immutable
    static final SomeMutableType[] EMPTY_ARRAY = {};
    enum SomeEnum { ENUM_CONSTANT }
    
    // Not constants
    static String nonFinal = "non-final";
    final String nonStatic = "non-static";
    static final SetmutableCollection = new HashSet();
    static final ImmutableSetmutableElements = ImmutableSet.of(mutable);
    static final Logger logger = Logger.getLogger(MyClass.getName());
    static final String[] nonEmptyArray = {"these", "can", "change"};
    

    Android SDK 中的很多类都用到了键值对函数,比如 SharedPreferencesBundleIntent,所以,即便是一个小应用,我们最终也不得不编写大量的字符串常量。

    当时用到这些类的时候,我们 必须 将它们的键定义为 const val 字段,并遵循以下指示作为前缀。

    字段名前缀
    SharedPreferences PREF_
    Bundle BUNDLE_
    Fragment Arguments ARGUMENT_
    Intent Extra EXTRA_
    Intent Action ACTION_
    save instance state SAVED_

    虽然 Fragment.getArguments()onSaveInstanceState 也是使用的 Bundle ,但为了区分不同的使用场景,所以定义了不同的前缀。

    2.5 非常量字段名

    非常量字段名以 lowerCamelCase 风格的基础上改造为如下风格:基本结构为 scope{Type0}VariableName{Type1}type0VariableName{Type1}variableName{Type1}

    说明:{} 中的内容为可选。

    注意:所有的 VO(值对象)统一采用标准的 lowerCamelCase 风格编写,所有的 DTO(数据传输对象)就按照接口文档中定义的字段名编写。

    2.5.1 scope(范围)

    非公有,非静态字段命名以 m 开头。

    静态字段命名以 s 开头。

    其他字段以小写字母开头。

    例如:

    public class MyClass {
        public int publicField;
        private static MyClass sSingleton;
        int mPackagePrivate;
        private int mPrivate;
        protected int mProtected;
    }
    

    使用 1 个字符前缀来表示作用范围,1 个字符的前缀必须小写,前缀后面是由表意性强的一个单词或多个单词组成的名字,而且每个单词的首写字母大写,其它字母小写,这样保证了对变量名能够进行正确的断句。

    通过IDE 自动生成get 、set 和构造函数的时候,这个没有任何实际意义的m前缀会被包含到变量名称当中去,显得很low也很容易影响可读性。在 AS 中,Settings -> Editor -> Code Style -> Java -> Code Generation 中,Field Name prefix 设置 m,Static Field Name prefix 设置 s。这样 AS 就可以识别了,自动生成方法的时候会去掉 s 或 m。

    2.5.2 Type0(控件类型)

    考虑到 Android 众多的 UI 控件,为避免控件和普通成员变量混淆以及更好地表达意思,所有用来表示控件的成员变量统一加上控件缩写作为前缀(具体见附录 [UI 控件缩写表](#UI 控件缩写表))。

    例如:mIvAvatarrvBooksflContainer

    2.5.3 VariableName(变量名)

    变量名中可能会出现量词,我们需要创建统一的量词,它们更容易理解,也更容易搜索。

    例如:mFirstBookmPreBookcurBook

    量词列表 量词后缀说明
    First 一组变量中的第一个
    Last 一组变量中的最后一个
    Next 一组变量中的下一个
    Pre 一组变量中的上一个
    Cur 一组变量中的当前变量

    2.5.4 Type1(数据类型)

    对于表示集合或者数组的非常量字段名,我们可以添加后缀来增强字段的可读性,比如:

    集合添加如下后缀:List、Map、Set。

    数组添加如下后缀:Arr。

    例如:mIvAvatarListuserArrfirstNameSet

    注意:如果数据类型不确定的话,比如表示的是很多书,那么使用其复数形式来表示也可,例如 mBooks

    2.6 参数名

    参数名以 lowerCamelCase 风格编写。

    2.7 局部变量名

    局部变量名以 lowerCamelCase 风格编写,比起其它类型的名称,局部变量名可以有更为宽松的缩写。

    虽然缩写更宽松,但还是要避免用单字符进行命名,除了临时变量和循环变量。

    即使局部变量是 final 和不可改变的,也不应该把它示为常量,自然也不能用常量的规则去命名它。

    2.8 临时变量

    临时变量通常被取名为 ijkmn,它们一般用于整型;cde,它们一般用于字符型。 如:for (int i = 0; i < len; i++)

    2.9 泛型变量名

    类型变量可用以下两种风格之一进行命名:

    1. 单个的大写字母,后面可以跟一个数字(如:E, T, X, T2)。
    2. 以类命名方式(参考类名),后面加个大写的 T(如:RequestT, FooBarT)。

    2.10 id命名

    插件 apply plugin: 'kotlin-android-extensions',可以在代码中通过 xml 定义的控件 id 直接操作控件,而不用写 findViewById ,所以这里控件的命名直接使用 lowerCamelCase 风格编写。

    命名规则:view 缩写{模块名}逻辑名,例如: btnMainSearchbtnBack

    三、Kotlin代码样式

    3.1 类型推断

    在可能的情况下,应选择使用类型推断代替显式声明实际的类型。

    // BAD
    val something: MyType = MyType()
    val meaningOfLife: Int = 42
    // GOOD
    val something = MyType()
    val meaningOfLife = 42
    

    3.2 分号

    应该尽可能避免在Kotlin中使用分号。

    // BAD
    val horseGiftedByTrojans = true;
    if (horseGiftedByTrojans) {
        bringHorseIntoWalledCity();
    }
    // GOOD
    val horseGiftedByTrojans = true
    if (horseGiftedByTrojans) {
        bringHorseIntoWalledCity()
    }
    

    3.3 声明When

    如果应该以相同的方式处理它们,请使用逗号分隔。始终包括else情况。

    // BAD
    when (anInput) {
        1 -> doSomethingForCaseOne()
        2 -> doSomethingForCaseOneOrTwo()
        3 -> doSomethingForCaseThree()
    }
    // GOOD
    when (anInput) {
        1, 2 -> doSomethingForCaseOneOrTwo()
        3 -> doSomethingForCaseThree()
        else -> println("No case satisfied")
    }
    

    3.4 可空类型

    避免使用 !! ,减少出现空指针的情况。

    // BAD
    editText!!.setText("foo")
    tvTitle.setText(title!!)
    // GOOD
    editText?.setText("foo")
    tvTitle.setText(title ?: "") or title?.let { tvTitle.setText(it) }
    

    3.5 语言

    使用美式英语 ??拼写。

    // BAD
    val colourName = "red"
    // GOOD
    val colorName = "red"
    

    3.6 大括号

    左大括号不单独占一行,与其前面的代码位于同一行:

    class MyClass {
        fun func(): Int {
            if (something) {
                // ...
            } else if (somethingElse) {
                // ...
            } else {
                // ...
            }
        }
    }
    

    我们需要在条件语句周围添加大括号。例外情况:如果整个条件语句(条件和主体)适合放在同一行,那么您可以(但不是必须)将其全部放在一行上。例如,我们接受以下样式:

    if (condition) {
        body()
    }
    

    同样也接受以下样式:

    if (condition) body()
    

    但不接受以下样式:

    if (condition)
        body()  // bad!
    

    3.7 编写简短方法

    在可行的情况下,尽量编写短小精炼的方法。有些情况下较长的方法是恰当的,因此对方法的代码长度没有做出硬性限制。如果某个方法的代码超出 40 行,请考虑是否可以在不破坏程序结构的前提下对其拆解,一个方法最好只做一件事情。

    3.8 类成员的顺序

    这并没有唯一的正确解决方案,但如果都使用一致的顺序将会提高代码的可读性,推荐使用如下排序:

    1. 常量
    2. 字段(public -> protected -> private)
    3. 构造函数
    4. 重写函数和回调 (在Android 中,应该将生命周期的函数放在前面)
    5. 公有函数
    6. 私有函数
    7. 内部类或接口
    class MainActivity : Activity() {
    
        companion object {
            private const val TAG = "tag"
        }
    
        var updateTime = 0L
        protected var mContent = ""
        private  var mTitle = ""
        private lateinit var mTextViewTitle: TextView
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            ...
        }
    
        fun setTitle(title:String){
            mTitle = title
        }
    
        private fun setUpView(){
            ...
        }
    
        inner class AnInnerClass{
           
        }
    }
    

    如果类继承于 Android 组件(例如 ActivityFragment),那么把重写函数按照他们的生命周期进行排序是一个非常好的习惯,例如,Activity 实现了 onCreate()onDestroy()onPause()onResume(),它的正确排序如下所示:

    class MainActivity : Activity() {
        // Order matches Activity lifecycle
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
        }
    
        override fun onResume() {
            super.onResume()
    
        }
    
        override fun onPause() {
            super.onPause()
    
        }
    
        override fun onDestroy() {
            super.onDestroy()
           
        }
    }
    

    3.9 函数参数的排序

    在 Android 开发过程中,Context 在函数参数中是再常见不过的了,我们最好把 Context 作为其第一个参数。

    把回调接口或函数应该作为其最后一个参数。

    // Context always goes first
    fun loadUser(context: Context, userId: Int): User
    
    // Callbacks or funcation always go last
    fun loadUserAsync(context: Context, userId: Int, callback: UserCallback)
    fun loadUserAsync(context: Context, userId: Int, action: (User) -> Unit)
    

    3.10 字符串常量的命名和值

    Android SDK 中的很多类都用到了键值对函数,比如 SharedPreferencesBundleIntent,所以,即便是一个小应用,我们最终也不得不编写大量的字符串常量。

    当时用到这些类的时候,我们 必须 将它们的键定义为 static final 字段,并遵循以下指示作为前缀。

    字段名前缀
    SharedPreferences PREF_
    Bundle BUNDLE_
    Fragment Arguments ARGUMENT_
    Intent Extra EXTRA_
    Intent Action ACTION_
    onSaveInstanceState SAVED_

    说明:虽然 Fragment.getArguments()onSaveInstanceState 也是 Bundle ,但因为这是 Bundle 的常用用法,所以特意为此定义一个不同的前缀。

    3.11 行长限制

    代码中每一行文本的长度都应该不超过 100 个字符。虽然关于此规则存在很多争论,但最终决定仍是以 100 个字符为上限,如果行长超过了 100(AS 窗口右侧的竖线就是设置的行宽末尾 ),我们通常有两种方法来缩减行长。

    • 提取一个局部变量或方法(最好)。
    • 使用换行符将一行换成多行。

    不过存在以下例外情况:

    • 如果备注行包含长度超过 100 个字符的示例命令或文字网址,那么为了便于剪切和粘贴,该行可以超过 100 个字符。
    • 导入语句行可以超出此限制,因为用户很少会看到它们(这也简化了工具编写流程)。

    3.11.1 换行策略

    这没有一个准确的解决方案来决定如何换行,通常不同的解决方案都是有效的,但是有一些规则可以应用于常见的情况。

    3.11.2 操作符的换行

    除赋值操作符之外,我们把换行符放在操作符之前,例如:

    int longName = anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne
            + theFinalOne;
    

    赋值操作符的换行我们放在其后,例如:

    int longName =
            anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne + theFinalOne;
    

    3.11.3 函数链的换行

    当同一行中调用多个函数时(比如使用构建器时),对每个函数的调用应该在新的一行中,我们把换行符插入在 . 之前。

    例如:

    Picasso.with(context).load("https://blankj.com/images/avatar.jpg").into(ivAvatar);
    

    我们应该使用如下规则:

    Picasso.with(context)
            .load("https://blankj.com/images/avatar.jpg")
            .into(ivAvatar);
    

    3.11.4 多参数的换行

    当一个方法有很多参数或者参数很长的时候,我们应该在每个 , 后面进行换行。

    比如:

    loadPicture(context, "https://blankj.com/images/avatar.jpg", ivAvatar, "Avatar of the user", clickListener);
    

    我们应该使用如下规则:

    loadPicture(context,
            "https://blankj.com/images/avatar.jpg",
            ivAvatar,
            "Avatar of the user",
            clickListener);
    

    3.11.5 RxJava 链式的换行

    RxJava 的每个操作符都需要换新行,并且把换行符插入在 . 之前。

    例如:

    public Observable<Location> syncLocations() {
        return mDatabaseHelper.getAllLocations()
                .concatMap(new Func1<Location, Observable<? extends Location>>() {
                    @Override
                     public Observable<? extends Location> call(Location location) {
                         return mRetrofitService.getLocation(location.id);
                     }
                })
                .retry(new Func2<Integer, Throwable, Boolean>() {
                     @Override
                     public Boolean call(Integer numRetries, Throwable throwable) {
                         return throwable instanceof RetrofitError;
                     }
                });
    }
    

    四、Android资源文件

    资源文件命名为全部小写,采用下划线命名法,资源文件需带module名作为前缀。

    4.1 动画资源文件(anim/ 和 animator/)

    安卓主要包含属性动画和视图动画,其视图动画包括补间动画和逐帧动画。属性动画文件需要放在 res/animator/ 目录下,视图动画文件需放在 res/anim/ 目录下。

    命名规则:module_逻辑名称

    例如:common_refresh_progress.xml

    如果是普通的补间动画或者属性动画,可采用:动画类型_方向 的命名方式。

    例如:

    名称 说明
    module_fade_in 淡入
    module_fade_out 淡出
    module_push_down_in 从下方推入
    module_push_down_out 从下方推出
    module_push_left 推向左方
    module_slide_in_from_top 从头部滑动进入
    module_zoom_enter 变形进入
    module_slide_in 滑动进入
    module_shrink_to_middle 中间缩小

    4.2 颜色资源文件(color/)

    命名规则:module_类型_逻辑名称

    例如:module_sel_btn_font.xml

    颜色资源也可以放于 res/drawable/ 目录,引用时则用 @drawable 来引用,但不推荐这么做,最好还是把两者分开。

    4.3 图片资源文件(drawable/ 和 mipmap/)

    res/drawable/ 目录下放的是位图文件(.png、.9.png、.jpg、.gif)或编译为可绘制对象资源子类型的 XML 文件,而 res/mipmap/ 目录下放的是不同密度的启动图标,所以 res/mipmap/ 只用于存放启动图标,其余图片资源文件都应该放到 res/drawable/ 目录下。

    命名规则:module_类型_逻辑名称

    例如:user_btn_logout.png

    大分辨率图片(单维度超过 1000)建议统一放在 xxhdpi 目录下管理,否则将导致占用内存成倍数增加 。

    如果有多种形态,如按钮选择器:module_sel_btn_xx.xml,采用如下命名:

    名称 说明
    module_sel_btn_xx 作用在 btn_xx 上的 selector
    module_btn_xx_normal 默认状态效果
    module_btn_xx_pressed state_pressed 点击效果
    module_btn_xx_focused state_focused 聚焦效果
    module_btn_xx_disabled state_enabled 不可用效果
    module_btn_xx_checked state_checked 选中效果
    module_btn_xx_selected state_selected 选中效果
    module_btn_xx_hovered state_hovered 悬停效果
    module_btn_xx_checkable state_checkable 可选效果
    module_btn_xx_activated state_activated 激活效果
    module_btn_xx_window_focused state_window_focused 窗口聚焦效果

    注意:使用 Android Studio 的插件 SelectorChapek 可以快速生成 selector,前提是命名要规范。

    4.4 布局资源文件(layout/)

    按照对应类型增加前缀:

    类型 前缀
    Activity module_activity
    Fragment module_fragment
    Dialog module_dialog
    include module_include
    ListView module_list_item
    RecyclerView module_recycle_item
    GridView module_grid_item

    Activity/Fragment/Dialog的类名不需要增加module前缀,直接为每一个module分配一个package,比如:a.b.c.user、a.b.c.launch,所有的代码都放在该package下。

    4.5 菜单资源文件(menu/)

    命名规则:module_逻辑名称

    4.6 values 资源文件(values/)

    values/ 资源文件下的文件都以 s 结尾,如 attrs.xmlcolors.xmldimens.xml,起作用的不是文件名称,而是 <resources> 标签下的各种标签,比如 <style> 决定样式,<color> 决定颜色,所以,可以把一个大的 xml 文件分割成多个小的文件,比如可以有多个 style 文件,如 styles.xmlstyles_home.xmlstyles_item_details.xmlstyles_forms.xml

    一个 Module 的资源文件都在 res 目录下,如果一个 Module 涉及到的页面很多,资源文件很多,不方便管理,可以多配置几个资源文件目录,这样我们可以对每个模块的资源都进行具体分类。

    方法很简单,配置我们的app文件夹下的 build.gradle 文件,比如:

    android {
        ...
        sourceSets {
            main {
                res.srcDirs('src/main/res', 'src/main/res_core', 'src/main/res_sub')
            }
        }
    }
    

    配置完之后,sync project 一下就成功了。

    4.6.1 colors.xml

    <color>name 命名使用下划线命名法,在你的 colors.xml 文件中应该只是映射颜色的名称一个 ARGB 值,而没有其它的。不要使用它为不同的按钮来定义 ARGB 值。

    例如,不要像下面这样做:

      <resources>
          <color name="button_foreground">#FFFFFF</color>
          <color name="button_background">#2A91BD</color>
          <color name="comment_background_inactive">#5F5F5F</color>
          <color name="comment_background_active">#939393</color>
          <color name="comment_foreground">#FFFFFF</color>
          <color name="comment_foreground_important">#FF9D2F</color>
          ...
          <color name="comment_shadow">#323232</color>
    

    使用这种格式,会非常容易重复定义 ARGB 值,而且如果应用要改变基色的话会非常困难。同时,这些定义是跟一些环境关联起来的,如 button 或者 comment,应该放到一个按钮风格中,而不是在 colors.xml 文件中。

    相反,应该这样做:

      <resources>
    
          <!-- grayscale -->
          <color name="white"     >#FFFFFF</color>
          <color name="gray_light">#DBDBDB</color>
          <color name="gray"      >#939393</color>
          <color name="gray_dark" >#5F5F5F</color>
          <color name="black"     >#323232</color>
    
          <!-- basic colors -->
          <color name="green">#27D34D</color>
          <color name="blue">#2A91BD</color>
          <color name="orange">#FF9D2F</color>
          <color name="red">#FF432F</color>
    
      </resources>
    复制代码
    

    向应用设计者那里要这个调色板,名称不需要跟 "green""blue" 等等相同。"brand_primary""brand_secondary""brand_negative" 这样的名字也是完全可以接受的。像这样规范的颜色很容易修改或重构,会使应用一共使用了多少种不同的颜色变得非常清晰。通常一个具有审美价值的 UI 来说,减少使用颜色的种类是非常重要的。

    注意:如果某些颜色和主题有关,那就单独写一个 colors_theme.xml

    4.6.2 dimens.xml

    像对待 colors.xml 一样对待 dimens.xml 文件,与定义颜色调色板一样,你同时也应该定义一个空隙间隔和字体大小的“调色板”。 一个好的例子,如下所示:

    <resources>
    
        <!-- font sizes -->
        <dimen name="font_22">22sp</dimen>
        <dimen name="font_18">18sp</dimen>
        <dimen name="font_15">15sp</dimen>
        <dimen name="font_12">12sp</dimen>
    
        <!-- typical spacing between two views -->
        <dimen name="spacing_40">40dp</dimen>
        <dimen name="spacing_24">24dp</dimen>
        <dimen name="spacing_14">14dp</dimen>
        <dimen name="spacing_10">10dp</dimen>
        <dimen name="spacing_4">4dp</dimen>
    
        <!-- typical sizes of views -->
        <dimen name="button_height_60">60dp</dimen>
        <dimen name="button_height_40">40dp</dimen>
        <dimen name="button_height_32">32dp</dimen>
    
    </resources>
    复制代码
    

    布局时在写 marginspaddings 时,你应该使用 spacing_xx 尺寸格式来布局,而不是像对待 string 字符串一样直接写值,像这样规范的尺寸很容易修改或重构,会使应用所有用到的尺寸一目了然。 这样写会非常有感觉,会使组织和改变风格或布局非常容易。

    4.6.3 strings.xml

    <string>name 命名使用下划线命名法,采用以下规则:module_逻辑名称,这样方便同一个界面的所有 string 都放到一起,方便查找。

    减少复用字符串,每个组件维护自己的,不要嫌麻烦。因为一些语言同样一个意思在不同的语境下,会用到不同的单词。

    使用字符串格式化替代字符串拼接。

    4.6.4 styles.xml

    <style> 的 name 命名使用大驼峰命名法,几乎每个项目都需要适当的使用 styles.xml 文件,因为对于一个视图来说,有一个重复的外观是很常见的,将所有的外观细节属性(colors、padding、font)放在 styles.xml 文件中。 在应用中对于大多数文本内容,最起码你应该有一个通用的 styles.xml 文件,例如:

    <style name="ContentText">
        <item name="android:textSize">@dimen/font_normal</item>
        <item name="android:textColor">@color/basic_black</item>
    </style>
    

    应用到 TextView 中:

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/price"
        style="@style/ContentText"/>
    

    或许你需要为按钮控件做同样的事情,不要停止在那里,将一组相关的和重复 android:xxxx 的属性放到一个通用的 <style>中。

    五、Android基本组件

    1. Activity 间的数据通信,对于数据量比较大的,避免使用 Intent + Parcelable 的方式传递大数据(binder transaction缓存为 1MB),可能导致 OOM。可以考虑 EventBus 等替代方案,以免造成 TransactionTooLargeException。

    2. Activity#onSaveInstanceState()方法不是 Activity 生命周期方法,也不保证一定会被调用。它是用来在 Activity 被意外销毁时保存 UI 状态的,只能用于保存临时性数据,例如 UI 控件的属性等,不能跟数据的持久化存储混为一谈。持久化存储应该在 Activity#onPause()/onStop()中实行。

    3. 避免在 Service#onStartCommand()/onBind()方法中执行耗时操作,如果确 实有需求,应改用 IntentService 或采用其他异步机制完成。

    4. 避免在 BroadcastReceiver#onReceive()中执行耗时操作,如果有耗时工作, 应该创建 IntentService 完成,而不应该在 BroadcastReceiver 内创建子线程去做。

    5. 对于只用于应用内的广播,优先使用 LocalBroadcastManager 来进行注册 和发送,LocalBroadcastManager 安全性更好,同时拥有更高的运行效率。

    6. 添 加 Fragment 时 , 确 保 FragmentTransaction#commit() 在 Activity#onPostResume()或者 FragmentActivity#onResumeFragments()内调用。 不要随意使用 FragmentTransaction#commitAllowingStateLoss()来代替,任何 commitAllowingStateLoss()的使用必须经过 code review,确保无负面影响。

    7. 不要在 Activity#onDestroy()内执行释放资源的工作,例如一些工作线程的 销毁和停止,因为 onDestroy()执行的时机可能较晚。可根据实际需要,在 Activity#onPause()/onStop()中结合 isFinishing()的判断来执行。

    8. 当前 Activity 的 onPause 方法执行结束后才会创建(onCreate)或恢复 (onRestart)别的 Activity,所以在 onPause 方法中不适合做耗时较长的工作,这 会影响到页面之间的跳转效率。

    9. Activity 或者 Fragment 中动态注册 BroadCastReceiver 时,registerReceiver() 和 unregisterReceiver()要成对出现。

    10. Android的最小兼容版本到17.关于Android不同系统版本的市场占比情况详解

    11. ActivityFragment里面有许多重复的操作以及操作步骤,所以我们都需要提供一个BaseActivityBaseFragment,让所有的ActivityFragment` 都继承这个基类。

    12. 必须支持界面被系统回收,用户再次打开App的时候能够恢复用户当时的使用状态,分析出具体在哪些情况下需要,哪些情况下不需要。例如弹窗,恢复弹窗上显示的数据,恢复弹窗的按钮的点击事件。

    13. 使用ArrayMap、ArraySet、SparseArray替换HashMap<T, T>、HashMap<int, T> 和 HashSet<T>

    14. 自定义控件的自定义属性,必须使用控件名称作为前缀,否则容易造成同名属性。

    15. 新建线程时,必须通过线程池提供(AsyncTask 或者 ThreadPoolExecutor或者其他形式自定义的线程池),不允许在应用中自行显式创建线程 。

    16. 子线程中不能更新界面,更新界面必须在主线程中进行,网络操作不能在主线程中调用 。

    17. 不要在非 UI 线程中初始化 ViewStub,否则会返回 null。

    18. 任何时候不要硬编码文件路径,请使用 Android 文件系统 API 访问。

      示例:Android 应用提供内部和外部存储,分别用于存放应用自身数据以及应用产生的用
      户数据。可以通过相关 API 接口获取对应的目录,进行文件操作。

      android.os.Environment#getExternalStorageDirectory()
      android.os.Environment#getExternalStoragePublicDirectory()
      android.content.Context#getFilesDir()
      android.content.Context#getCacheDir
      
    19. 当使用外部存储时,必须检查外部存储的可用性 。

    20. SharedPreference 中只能存储简单数据类型(int、 boolean、 String 等),复杂数据类型建议使用文件、数据库等其他方式存储 。

    21. SharedPreference 提 交 数 据 时 , 尽 量 使 用 Editor#apply() , 而 非Editor#commit()。一般来讲,仅当需要确定提交结果,并据此有后续操作时,才使用 Editor#commit()。

    22. 多线程操作写入数据库时,需要使用事务,以免出现同步问题 。

    23. 执行 SQL 语句时,应使用 SQLiteDatabase#insert()、 update()、 delete(),不要使用SQLiteDatabase#execSQL(),以免 SQL 注入风险。

    24. 不要通过 Msg 传递大的对象,会导致内存问题。

    25. 不能在 Activity 没有完全显示时显示 PopupWindow 和 Dialog。

    26. 不要在 Android 的 Application 对象中缓存数据。基础组件之间的数据共享请使用 Intent 等机制,也可使用 SharedPreferences 等数据持久化机制。

    27. 使用 Toast 时,建议定义一个全局的 Toast 对象,这样可以避免连续显示Toast 时不能取消上一次 Toast 消息的情况(如果你有连续弹出 Toast 的情况,避免使用 Toast.makeText)。

    六、代码

    1. 使用Gson中的 @SerializedName 将服务器端返回数据字段不符合命名规范的转换为符合规范的命名,分离服务器和客户端字段的硬绑定。
    2. 多用组合,少用继承。
    3. 当一个类有多个构造函数,或是多个同名函数,这些函数应该按顺序出现在一起,中间不要放进其它函数。
    4. 提取方法,去除重复代码。对于必要的工具类抽取也很重要,这在以后的项目中是可以重用的。
    5. 通过引入事件总线,如:EventBusAndroidEventBusRxBus,它允许我们在 DataLayer 中发送事件,以便 ViewLayer 中的多个组件都能够订阅到这些事件,减少回调。
    6. 尽可能使用局部变量。
    7. 及时关闭流。
    8. 不要在循环中使用 try…catch…,应该把其放在最外层。
    9. 使用带缓冲的输入输出流进行 IO 操作。
    10. 尽量在合适的场合使用单例;
    11. 使用单例可以减轻加载的负担、缩短加载的时间、提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面:
      1. 控制资源的使用,通过线程同步来控制资源的并发访问。
      2. 控制实例的产生,以达到节约资源的目的。
      3. 控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。
    12. 使用 AS 自带的 Lint 来优化代码结构(右键 module、目录或者文件,选择 Analyze -> Inspect Code)。
    13. Kotlin 和 Java 是两种语言,在使用Kotlin的时候应该使用Kotlin中的特性,而不是固执的使用Java特性。比如:对于变量的赋值,如果仅仅是赋值,那么应该直接开放变量权限;如果涉及到一系列后续操作,那么使用方法。

    七、UI与布局

    1. 使用 start 和 end 替代 left 和 right,使布局能适应 RightRoLeft布局。
    2. 合理布局,有效运用 <merge><ViewStub><include> 标签。比如content 是一个 FrameLayout ,所以这里简单布局是不需要外层ViewGroup,直接使用 <merge> 来作为布局文件的根ViewGroup就可以了;
    3. 所有布局都需要考虑到大屏幕和小屏幕显示的问题,尤其是对大屏幕一个页面就可以展示完,而对于小屏幕不能展示完,需要增加 <ScrollView> 让页面能够滚动。目前一般情况下最小屏幕是 1280 * 720。
    4. 字体单位使用 dp 设置字体而不是 sp。
    5. 对于设计图上按钮大小小于 24dp * 24dp,应该使用 padding 将按钮的实际大小控制在大于等于 24dp * 24dp。
    6. 使用 ConstraintLayout 替换 RelativeLayout。
    7. 不要过分依赖通用资源,否则会导致修改困难,该拆分的就拆分,不要在乎资源内容是一样的。
    8. 所有的文本按钮,还是使用 Button,不要使用 TextView ,毕竟系统对 Button 有样式渲染。同理对于图片按钮,应该使用 ImageButton 而不是 ImageView。
    9. 使用Space控件占据不显示内容的空间。
    10. ScrollView 内部嵌套有 ListView 或 RecycleView 等,注意要考虑到ScrollView默认位置不是最顶部的情况。
    11. 考虑过渡绘制,不要直接在整个布局增加背景颜色,考虑是否在布局的一部分设置背景颜色就可以达到UI效果。

    八、安全

    1. 将 android:allowbackup 属性必须设置为 false,阻止应用数据被导出。说明:android:allowBackup 原本是 Android 提供的 adb 调试功能,如果设置为 true, 可以导出应用数据备份并在任意设备上恢复。这对应用安全性和用户数据隐私构成 极大威胁,所以必须设置为 false,防止数据泄露。

    2. 在 SDK 支持的情况下,Android 应用必须使用 V2 签名,这将对 APK 文件的修改做更多的保护。

    3. 所有的 Android 基本组件(Activity、Service、BroadcastReceiver、ContentProvider 等)都不应在没有严格权限控制的情况下,将 android:exported 设 置为 true。

    4. 确保应用发布版本的 android:debuggable 属性设置为 false。

    5. 在 Android 4.2(API Level 17)及以上,对安全性要求较高的应用可在 Activity中,对 Activity 所关联的 Window 应用 WindowManager.LayoutParams.FLAG_SECURE,防止被截屏、录屏。但要注意的是,一个 Activity 关联的 Window 可能不止一个,如果使用了 Dialog / DialogFragment 等控件弹出对话框,它们本身也会创建一个新的 Window,也一样需要保护。

    九、注释

    注释的功能是辅助程序员理解逻辑和业务,不是每一行都需要注释。一个好的命名就可以反应该字段或者函数的意思,没有必要多此一举增加注释。在具体的业务逻辑或者关键代码处,添加注释就可。

    9.1 类注释

    每个类完成后应该有作者姓名的注释,对自己的代码负责。

    /**
     * <pre>
     *     author : XiaoYangZi
     *     time   : 2018/04/23
     *     desc   : xxxx 描述
     *     version: 1.0
     * </pre>
     */
    class WelcomeActivity {
        ...
    }
    

    具体可以在 AS 中自己配制,进入 Settings -> Editor -> File and Code Templates -> Includes -> File Header,输入

    /**
     * <pre>
     *     author : ${USER} //这里也可以写死,比如说 author : XiaoYangZi
     *     time   : ${YEAR}/${MONTH}/${DAY}
     *     desc   :
     *     version: 1.0
     * </pre>
     */
    

    这样便可在每次新建类的时候自动加上该头注释。

    9.2 函数注释

    有选择性的书写。

    /**
     * 一句话功能描述
     * 功能详细描述
     * @param [参数1] [参数1说明]
     * @param [参数2] [参数2说明]
     * @return [返回类型说明]
     * @exception/throws [异常类型] [异常说明]
     * @deprecated [是否废弃]
     */
    

    9.3 代码块注释

    写出代码的目的,而不是行为,行为可以通过具体的代码来判断。

    十、测试

    十一、Git使用

    12.1 分支

    master

    主分支,不轻易改动,新版本上线后,将新版本代码合并到该分支

    develop:

    团队人员很少的时候,所有的开发人员都在这个分支进行开发。

    团队人员超过一个规模,每个开发人员在自己分支(dev_xx)上开发,等开发到一定阶段,将自己的代码合并到develop分支上。

    fix_version:

    修复Bug分支,修复对应版本的bug。

    refactor_xx:

    重构分支,区别于修改Bug分支,重构不一定会在下一个新版本中上线。

    12.2 提交

    • feat: 新功能(feature)

    • fix: 修复bug

    • docs: 仅文档修改(documentation)

    • style: 不影响代码含义的变化(空白,格式化,缺少分号等)

    • refactor:重构(既不是新增功能,也不是修改bug的变动)

    • chore: 构建过程、辅助工具、编辑器配置的变动

    • perf: 改进性能的代码更改

    • test: 添加缺失测试或更正现有测试

    • build: 影响构建系统或外部依赖关系的更改(示例范围:gulp,broccoli,npm)

    • ci: 更改我们的持续集成文件和脚本(示例范围:Travis,Circle,BrowserStack,SauceLabs)

    示例:

    fix(首页):修复缓存异常
    feat(用户):新增修改用户头像的功能
    

    12.3 代码评审

    在两个及两个以上开发人员的项目中,应该进行代码评审,检查代码风格和是否有潜在的BUG。

    12.4其他

    在Android Studio中设置好代码注释、模版或者其他配置文件后,导出settings.jar 包,可实现团队或换电脑时一键统一配置。

    https://www.jb51.net/softjc/445324.html

    附录

    UI 控件缩写表

    名称 缩写
    Button btn
    CheckBox cb
    EditText et
    FrameLayout fl
    GridView gv
    ImageButton ib
    ImageView iv
    LinearLayout ll
    ListView lv
    ProgressBar pb
    RadioButtion rb
    RecyclerView rv
    RelativeLayout rl
    ScrollView sv
    SeekBar sb
    Spinner spn
    TextView tv
    ToggleButton tb
    VideoView vv
    WebView wv

    常见的英文单词缩写表

    名称 缩写
    average avg
    background bg(主要用于布局和子布局的背景)
    buffer buf
    control ctrl
    current cur
    default def
    delete del
    document doc
    error err
    escape esc
    icon ic(主要用在 App 的图标)
    increment inc
    information info
    initial init
    image img
    Internationalization I18N
    length len
    library lib
    message msg
    password pwd
    position pos
    previous pre
    selector sel(主要用于某一 view 多种状态,不仅包括 ListView 中的 selector,还包括按钮的 selector)
    server srv
    string str
    temporary tmp
    window win

    程序中使用单词缩写原则:不要用缩写,除非该缩写是约定俗成的。

    扩展阅读 :Git使用规范(Android版)

    参考

    Android 开发规范(完结版)

    Android Studio 下对资源进行分包

    不可不知的 Android strings.xml 那些事

    kotlin-style-guide

    Android Studio中导出设置共享配置

    阿里巴巴Android开发手册

    阿里巴巴Java开发手册

    展开全文
  • 我的简书同步发布:从Android代码中来记忆23种设计模式 相信大家都曾经下定决心把23种设计模式牢记于心,每次看完之后过一段时间又忘记了~,又得回去看,脑子里唯一依稀记得的是少数设计模式的大致的定义。其实,...

    我的简书同步发布:从Android代码中来记忆23种设计模式

    相信大家都曾经下定决心把23种设计模式牢记于心,每次看完之后过一段时间又忘记了~,又得回去看,脑子里唯一依稀记得的是少数设计模式的大致的定义。其实,网上很多文章讲得都非常好,我也曾经去看过各种文章。也曾一直苦恼这些难以永久记下的设计模式,直到我接触到了《Android源码设计模式解析与实战》——何红辉与关爱明著,发现原来其实我们在Android中都接触过这些设计模式,只是我们不知道而已。既然我们都接触过,我们只需一一对号入座,对设计模式的记忆就不用死记硬背了!这里自愿无偿做个广告,《Android源码设计模式解析与实战》这本书真心不错,每个Android程序员最好都去翻翻…正如你所想的那样,本文是从这本书中的总结,相信你也会跟我一样,从中获益。

    面向对象的六大原则

    首先,我们为什么要学习设计模式。主要是这些模式是前人总结的经验,使用这些模式能让我们的程序更健壮、更稳定、容易扩展等等优点。在编写面向对象程序时,我们需要遵循以下6个原则,能让我们的程序维护起来更轻松~(当然还有其它好处)。

    1 单一职责原则
    单一原则很简单,就是将一组相关性很高的函数、数据封装到一个类中。换句话说,一个类应该有职责单一。

    2 开闭原则

    开闭原则理解起来也不复杂,就是一个类应该对于扩展是开放的,但是对于修改是封闭的。我们知道,在开放的app或者是系统中,经常需要升级、维护等,这就要对原来的代码进行修改,可是修改时容易破坏原有的系统,甚至带来一些新的难以发现的BUG。因此,我们在一开始编写代码时,就应该注意尽量通过扩展的方式实现新的功能,而不是通过修改已有的代码实现。

    3 里氏替换原则

    里氏替换原则的定义为:所有引用基类的地方必须能透明地使用其子类对象。定义看起来很抽象,其实,很容易理解,本质上就是说,要好好利用继承和多态。简单地说,就是以父类的形式声明的变量(或形参),赋值为任何继承于这个父类的子类后不影响程序的执行。看一组代码你就明白这个原则了:

    //窗口类
    public class Window(){
        public void show(View child){
            child.draw();
        }
    }
    public abstract class View(){
        public abstract void draw();
        public void measure(int widht,int height){
            //测量视图大小
        }
    }
    public class Button extends View{
        public void draw(){
            //绘制按钮
        }
    }
    
    public class TextView extends View{
        public void draw(){
            //绘制文本
        }
    }

    Window 类中show函数需要传入View,并且调用View对象的draw函数。而每个继承于View的子对象都有draw的实现,不存在继承于View但是却没实现draw函数的子类(abstract方法必须实现)。我们在抽象类设计之时就运用到了里氏替换原则。

    4 依赖倒置原则

    依赖倒置主要是实现解耦,使得高层次的模块不依赖于低层次模块的具体实现细节。怎么去理解它呢,我们需要知道几个关键点:

    (1)高层模块不应该依赖底层模块(具体实现),二者都应该依赖其抽象(抽象类或接口)
    (2)抽象不应该依赖细节(废话,抽象类跟接口肯定不依赖具体的实现了)
    (3)细节应该依赖于抽象(同样废话,具体实现类肯定要依赖其继承的抽象类或接口)

    其实,在我们用的Java语言中,抽象就是指接口或者抽象类,二者都是不能直接被实例化;细节就是实现类,实现接口或者继承抽象类而产生的类,就是细节。使用Java语言描述就简单了:就是各个模块之间相互传递的参数声明为抽象类型,而不是声明为具体的实现类;

    5 接口隔离原则

    接口隔离原则定义:类之间的依赖关系应该建立在最小的接口上。其原则是将非常庞大的、臃肿的接口拆分成更小的更具体的接口。

    6 迪米特原则

    描述的原则:一个对象应该对其他的对象有最少的了解。什么意思呢?就是说一个类应该对自己调用的类知道的最少。还是不懂?其实简单来说:假设类A实现了某个功能,类B需要调用类A的去执行这个功能,那么类A应该只暴露一个函数给类B,这个函数表示是实现这个功能的函数,而不是让类A把实现这个功能的所有细分的函数暴露给B。

    开始学设计模式

    学习了上面的六大原则之后,提前做了预热。现在开始,一起学习设计模式吧~

    1 单例模式

    单例模式可以说是最容易理解的模式了,也是应用最广的模式之一,先看看定义吧。

    定义:确保单例类只有一个实例,并且这个单例类提供一个函数接口让其他类获取到这个唯一的实例。

    什么时候需要使用单例模式呢:如果某个类,创建时需要消耗很多资源,即new出这个类的代价很大;或者是这个类占用很多内存,如果创建太多这个类实例会导致内存占用太多。

    关于单例模式,虽然很简单,无需过多的解释,但是这里还要提个醒,其实单例模式里面有很多坑。我们去会会单例模式。最简单的单例模式如下:

    public class Singleton{
        private static Singleton instance;
        //将默认的构造函数私有化,防止其他类手动new
        private Singleton(){};
        public static Singleton getInstance(){
            if(instance==null)
                instance=new Singleton();
             return instatnce;
        }
    }

    如果是单线程下的系统,这么写肯定没问题。可是如果是多线程环境呢?这代码明显不是线程安全的,存在隐患:某个线程拿到的instance可能是null,可能你会想,这有什么难得,直接在getInstance()函数上加sychronized关键字不就好了。可是你想过没有,每次调用getInstance()时都要执行同步,这带来没必要的性能上的消耗。注意,在方法上加sychronized关键字时,一个线程访问这个方法时,其他线程无法同时访问这个类其他sychronized方法。的我们看看另外一种实现:

    public class Singleton{
        private static Singleton instance;
        //将默认的构造函数私有化,防止其他类手动new
        private Singleton(){};
        public static Singleton getInstance(){
            if(instance==null){
                sychronized(Singleton.class){
                    if(instance==null)
                        instance=new Singleton();
                }
            }
            return instatnce;
        }
    }
    

    为什么需要2次判断是否为空呢?第一次判断是为了避免不必要的同步,第二次判断是确保在此之前没有其他线程进入到sychronized块创建了新实例。这段代码看上去非常完美,但是,,,却有隐患!问题出现在哪呢?主要是在instance=new Singleton();这段代码上。这段代码会编译成多条指令,大致上做了3件事:

    (1)给Singleton实例分配内存
    (2)调用Singleton()构造函数,初始化成员字段
    (3)将instance对象指向分配的内存(此时instance就不是null啦~)

    上面的(2)和(3)的顺序无法得到保证的,也就是说,JVM可能先初始化实例字段再把instance指向具体的内存实例,也可能先把instance指向内存实例再对实例进行初始化成员字段。考虑这种情况:一开始,第一个线程执行instance=new Singleton();这句时,JVM先指向一个堆地址,而此时,又来了一个线程2,它发现instance不是null,就直接拿去用了,但是堆里面对单例对象的初始化并没有完成,最终出现错误~ 。
    看看另外一种方式:

    public class Singleton{
        private volatile static Singleton instance;
        //将默认的构造函数私有化,防止其他类手动new
        private Singleton(){};
        public static Singleton getInstance(){
            if(instance==null){
                sychronized(Singleton.class){
                    if(instance==null)
                        instance=new Singleton();
                }
            }
            return instatnce;
        }
    }

    相比前面的代码,这里只是对instance变量加了一个volatile关键字volatile关键字的作用是:线程每次使用到被volatile关键字修饰的变量时,都会去堆里拿最新的数据。换句话说,就是每次使用instance时,保证了instance是最新的。注意:volatile关键字并不能解决并发的问题,关于volatile请查看其它相关文章。但是volatile能解决我们这里的问题。

    那么在安卓中哪些地方用到了单例模式呢?其实,我们在调用系统服务时拿到的Binder对象就是个单例。比如:

    //获取WindowManager服务引用
    WindowManager wm = (WindowManager)getSystemService(getApplication().WINDOW_SERVICE);  
    

    其内部是通过单例的方式返回的,由于单例模式较简单,这里不去深究。

    2 Builder模式

    Builder模式是什么情况呢?我不想去提它的定义,因为他的定义:将一个复杂对象的构造与它的表示分离,使得同样的构造过程可以创建不同的表示。好吧,我还是提了。但是看了这个定义并没有什么luan用。我们看看具体在什么情况下用到Builder模式:主要是在创建某个对象时,需要设定很多的参数(通过setter方法),但是这些参数必须按照某个顺序设定,或者是设置步骤不同会得到不同结果。举个非常简单的例子:

    public class MyData{
        private int id;
        private String num; 
        public void Test(){
    
        } 
        public void setId(int id){
            this.id=id;
        }
    
        public void setNum(String num){
            this.num=num+"id";
        }
    
    
    }

    当然了,没有人会这么去写代码。这里只是举例子,或者是有时候很多参数有这种类似的依赖关系时,通过构造函数未免太多参数了。回到主题,就是如果是上面的代码,该怎么办呢?你可能会说,那还不简单,先调用setId函数,再调用setNum函数。是的,没错。可是,万一你一不小心先调用了setNum呢?这是比较简单的示例,如果是比较复杂的,有很多变量之间依赖的关系,那你每次都得小心翼翼的把各个函数的执行步骤写正确。
    我们看看Builder模式是怎么去做的:

    public class MyBuilder{
        private int id;
        private String num;
        public MyData build(){
            MyData d=new MyData();
            d.setId(id);
            d.setNum(num);
            return t;
        }
        public MyBuilder setId(int id){
            this.id=id;
            return this;
        }
        public MyBuilder setNum(String num){
            this.num=num;
            return this;
        }
    
    }
    
    public class Test{
        public static void  main(String[] args){
            MyData d=new MyBuilder().setId(10).setNum("hc").build();
        }
    
    }
    

    注意到,Builer类的setter函数都会返回自身的引用this,这主要是用于链式调用,这也是Builder设计模式中的一个很明显的特征。

    Android中用过的代码来记忆

    记忆我这个例子没啥意义,我们前面说过,要通过Android中用过的代码来记忆,这样才可以不用死记硬背。那么在Android中哪里用到了Builder设计模式呢?哈哈~在创建对话框时,是不是跟上面有点类似呢?

    AlertDialog.Builer builder=new AlertDialog.Builder(context);
    builder.setIcon(R.drawable.icon)
        .setTitle("title")
        .setMessage("message")
        .setPositiveButton("Button1", 
            new DialogInterface.OnclickListener(){
                public void onClick(DialogInterface dialog,int whichButton){
                    setTitle("click");
                }   
            })
        .create()
        .show();

    这里的create()函数就想到上面代码中的build函数。看到这里是不是在内心中默默的把Builder设计模式拿下了?你并不用死记硬背~

    3 原型模式

    原型设计模式非常简单,就是将一个对象进行拷贝。对于类A实例a,要对a进行拷贝,就是创建一个跟a一样的类型A的实例b,然后将a的属性全部复制到b。
    什么时候会用到原型模式呢?我个人认为,可以在类的属性特别多,但是又要经常对类进行拷贝的时候可以用原型模式,这样代码比较简洁,而且比较方便。

    另外要注意的是,还有深拷贝和浅拷贝。深拷贝就是把对象里面的引用的对象也要拷贝一份新的对象,并将这个新的引用对象作为拷贝的对象引用。说的比较绕哈~,举个例子,假设A类中有B类的引用b,现在需要对A类实例进行拷贝,那么深拷贝就是,先对b进行一次拷贝得到nb,然后把nb作为A类拷贝的对象的引用,如此一层一层迭代拷贝,把所有的引用都拷贝结束。浅拷贝则不是。

    原型模式比较简单,看看Android怎么运用原型模式:

    Uri uri=Uri.parse("smsto:10086");
    Intent shareIntent=new Intent(Intent.ACTION_SENDTO,uri);
    
    //克隆副本
    Intent intent=(Intetn)shareIntent.clone();
    startActivity(intent);
    

    或许我们平时不会这么去写,但是Intent对象确实提供了原型模式的函数clone()

    4 工厂方法模式

    定义:定义一个创建对象的接口,让子类决定实例化哪个类
    先看一个例子:

    public abstract class Product{
        public abstract void method();
    } 
    
    public class ConcreteProductA extends Prodect{
        public void method(){
            System.out.println("我是产品A!");
        }
    }
    
    public class ConcreteProductB extends Prodect{
        public void method(){
            System.out.println("我是产品B!");
        }
    }
    public  abstract class Factory{
        public abstract Product createProduct();
    }
    
    public class MyFactory extends Factory{
    
        public Product createProduct(){
            return new ConcreteProductA();
        }
    }

    看到上面的代码,是不是觉得工厂模式很简单呢?还可以通过传参的方式,让MyFactory的createProduct方法根据传入的参数决定是创建ConcreteProductA还是ConcreteProductB。

    同样的,我们不希望记住这个例子,而是通过Android中的代码来记忆:
    其实,在getSystemService方法中就是用到了工厂模式,他就是根据传入的参数决定创建哪个对象,当然了,由于返回的都是以单例模式存在的对象,因此不用new了,直接把单例返回就好。

    public Object getSystemService(String name) {
        if (getBaseContext() == null) {
            throw new IllegalStateException("System services not available to Activities before onCreate()");
        }
        //........
        if (WINDOW_SERVICE.equals(name)) {
             return mWindowManager;
        } else if (SEARCH_SERVICE.equals(name)) {
            ensureSearchManager();
            return mSearchManager;
        }
        //.......
        return super.getSystemService(name);
      }
    

    5 抽象工厂模式

    抽象工厂模式:为创建一组相关或者是相互依赖的对象提供一个接口,而不需要制定他们的具体类
    看个例子吧,将它跟工厂方法模式做个对比:

    public abstract class AbstractProductA{
        public abstract void method();
    }
    public abstract class AbstractProdectB{
        public abstract void method();
    }
    
    public class ConcreteProductA1 extends AbstractProductA{
        public void method(){
            System.out.println("具体产品A1的方法!");
        }
    }
    public class ConcreteProductA2 extends AbstractProductA{
        public void method(){
            System.out.println("具体产品A2的方法!");
        }
    }
    public class ConcreteProductB1 extends AbstractProductB{
        public void method(){
            System.out.println("具体产品B1的方法!");
        }
    }
    public class ConcreteProductB2 extends AbstractProductB{
        public void method(){
            System.out.println("具体产品B2的方法!");
        }
    }
    
    public abstract class AbstractFactory{
        public abstract AbstractProductA createProductA();
    
        public abstract AbstractProductB createProductB();
    }
    
    public  class ConcreteFactory1 extends AbstractFactory{
        public  AbstractProductA createProductA(){
            return new ConcreteProductA1();
        }
    
        public  AbstractProductB createProductB(){
            return new ConcreteProductB1();
        }
    }
    
    public  class ConcreteFactory2 extends AbstractFactory{
        public  AbstractProductA createProductA(){
            return new ConcreteProductA2();
        }
    
        public  AbstractProductB createProductB(){
            return new ConcreteProductB2();
        }
    }

    其实Android源码中对抽象工厂出现的比较少,好在抽象工厂方法并不复杂,很容易记住,我们可以从Service中去理解,Service的onBind方法可以看成是一个工厂方法,从framework角度来看Service,可以看成是一个具体的工厂,这相当于一个抽象工厂方法模式的雏形。

     public class BaseService extends Service{
        @Nullable
        @Override
        public IBinder onBind(Intent intent){
            return new Binder();
        }
    
    }

    6 策略模式

    定义:有一系列的算法,将每个算法封装起来(每个算法可以封装到不同的类中),各个算法之间可以替换,策略模式让算法独立于使用它的客户而独立变化。

    举个例子来理解吧,比如,你现在又很多排序算法:冒泡、希尔、归并、选择等等。我们要根据实际情况来选择使用哪种算法,有一种常见的方法是,通过if…else或者case…等条件判断语句来选择。但是这个类的维护成本会变高,维护时也容易发生错误。

    如何使用策略模式呢,我不打算写示例代码了,简单描述一下,就将前面说的算法选择进行描述。我们可以定义一个算法抽象类AbstractAlgorithm,这个类定义一个抽象方法sort()。每个具体的排序算法去继承AbstractAlgorithm类并重写sort()实现排序。在需要使用排序的类Client类中,添加一个setAlgorithm(AbstractAlgorithm al);方法将算法设置进去,每次Client需要排序而是就调用al.sort()。

    不知道简单描述能不能让你理解~

    看看Android中哪里出现了策略模式,其中在属性动画中使用时间插值器的时候就用到了。在使用动画时,你可以选择线性插值器LinearInterpolator、加速减速插值器AccelerateDecelerateInterpolator、减速插值器DecelerateInterpolator以及自定义的插值器。这些插值器都是实现根据时间流逝的百分比来计算出当前属性值改变的百分比。通过根据需要选择不同的插值器,实现不同的动画效果。这些比较好理解,就不去粘贴Android源码了。

    7 状态模式

    状态模式中,行为是由状态来决定的,不同状态下有不同行为。状态模式和策略模式的结构几乎是一模一样的,主要是他们表达的目的和本质是不同。状态模式的行为是平行的、不可替换的,策略模式的行为是彼此独立可相互替换的。
    举个例子把,比如电视,电视有2个状态,一个是开机,一个是关机,开机时可以切换频道,关机时切换频道不做任何响应。

    public interface TvState{
        public void nextChannerl();
        public void prevChannerl();
        public void turnUp();
        public void turnDown();
    }
    
    public class PowerOffState implements TvState{
        public void nextChannel(){}
        public void prevChannel(){}
        public void turnUp(){}
        public void turnDown(){}
    
    }
    
    
    public class PowerOnState implements TvState{
        public void nextChannel(){
            System.out.println("下一频道");
        }
        public void prevChannel(){
            System.out.println("上一频道");
        }
        public void turnUp(){
            System.out.println("调高音量");
        }
        public void turnDown(){
            System.out.println("调低音量"); 
        }
    
    }
    
    public interface PowerController{
        public void powerOn();
        public void powerOff();
    }
    
    public class TvController implements PowerController{
        TvState mTvState;
        public void setTvState(TvStete tvState){
            mTvState=tvState;
        }
        public void powerOn(){
            setTvState(new PowerOnState());
            System.out.println("开机啦");
        }
        public void powerOff(){
            setTvState(new PowerOffState());
            System.out.println("关机啦");
        }
        public void nextChannel(){
            mTvState.nextChannel();
        }
        public void prevChannel(){
            mTvState.prevChannel();
        }
        public void turnUp(){
            mTvState.turnUp();
        }
        public void turnDown(){
            mTvState.turnDown();
        }
    
    }
    
    
    public class Client{
        public static void main(String[] args){
            TvController tvController=new TvController();
            tvController.powerOn();
            tvController.nextChannel();
            tvController.turnUp();
    
            tvController.powerOff();
            //调高音量,此时不会生效
            tvController.turnUp();
        }
    
    
    }

    在Android源码中,哪里有用到状态模式呢?其实很多地方用到了,举一个地方例子,就是WIFI管理模块。当WIFI开启时,自动扫描周围的接入点,然后以列表的形式展示;当wifi关闭时则清空。这里wifi管理模块就是根据不同的状态执行不同的行为。由于代码太多,我就不手打敲入了~我们只要知道大致Android里面在哪里用到了以及大概是怎么用的就好。

    8 责任链模式

    定义:使多个对象都有机会处理请求,从而避免请求的发送者和接受者直接的耦合关系,将这些对象连成一条链,并沿这条链传递该请求,直到有对象处理它为止。

    相信聪明的你很容易理解吧,基本不需要例子来解释了,直接进如到Android源码中哪里用到了责任链:在Android处理点击事件时,父View先接收到点击事件,如果父View不处理则交给子View,依次往下传递~

    9 解释器模式

    定义:给定一个语言,定义它的语法,并定义一个解释器,这个解释器用于解析语言。

    从定义中看起来比较抽象,其实,很简单,很容易理解!就是相当于自定义一个格式的文件,然后去解析它。不用理解的那么复杂!

    我们看看Android中哪里用到了,从我们第一次学Android时就知道,四大组件需要在AndroidManifest.xml中定义,其实AndroidManifest.xml就定义了<Activity><Service>等标签(语句)的属性以及其子标签,规定了具体的使用(语法),通过PackageManagerService(解释器)进行解析。

    10 命令模式

    定义:命令模式将每个请求封装成一个对象,从而让用户使用不同的请求把客户端参数化;将请求进行排队或者记录请求日志,以及支持可撤销操作。

    举个例子来理解:当我们点击“关机”命令,系统会执行一系列操作,比如暂停事件处理、保存系统配置、结束程序进程、调用内核命令关闭计算机等等,这些命令封装从不同的对象,然后放入到队列中一个个去执行,还可以提供撤销操作。

    那么Android中哪里用到了命令模式呢?在framework层还真不多。但是在底层却用到了,一个比较典型的例子就是在Android事件机制中,底层逻辑对事件的转发处理。每次的按键事件会被封装成NotifyKeyArgs对象。通过InputDispatcher封装具体的事件操作。

    11 观察者模式

    定义:定义了对象之间的一对多的关系,其实就是1对n,当“1”发生变化时,“n”全部得到通知,并更新。

    观察者模式一个比较经典的应用就是:订阅——发布系统。很容易理解,发布消息时,将消息发送给每个订阅者。我们常用的微信公众号就是典型,当我们关注某个公众号时,每当公众号推送消息时,我们就会去接收到消息,当然了,每个订阅(关注)公众号的的人都能接收到公众号推送的消息。

    那么Android哪里用到了观察者模式呢?我们看看ListView的适配器,有个函数notifyDataSetChanged()函数,这个函数其实就是通知ListView的每个Item,数据源发生了变化,请各位Item重新刷新一下。

    12 备忘录模式

    备忘录模式定义:在不破坏封闭的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态,这样,以后就可将对象恢复到原先保存的状态中。

    其实就是相当于一个提前备份,一旦出现啥意外,能够恢复。像我们平时用的word软件,意外关闭了,它能帮我们恢复。其实就是它自动帮我们备份过。

    那么Android哪里用到了备忘录模式呢?ActivityonSaveInstanceStateonRestoreInstanceState就是用到了备忘录模式,分别用于保存和恢复。

    13 迭代器模式

    迭代器模式定义:提供一种方法顺序访问一个容器对象中的各个元素,而不需要暴露该对象的内部表示。

    相信熟悉Java的你肯定知道,Java中就有迭代器Iterator类,本质上说,它就是用迭代器模式。

    按照惯例,看看Android中哪里用到了迭代器模式,Android源码中,最典型的就是Cursor用到了迭代器模式,当我们使用SQLiteDatabasequery方法时,返回的就是Cursor对象,通过如下方式去遍历:

    cursor.moveToFirst();
    do{
    //cursor.getXXX(int);
    }while(cursor.moveToNext);

    14 模板方法模式

    定义:定义一个操作中的算法框架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定的步骤。

    不用解释太多,感觉越解释越糊涂,直接拿Android中的源码来说事!

    我们知道,启动一个Activity过程非常复杂,如果让开发者每次自己去调用启动Activity过程无疑是一场噩梦。好在启动Activity大部分代码时不同的,但是有很多地方需要开发者定制。也就是说,整体算法框架是相同的,但是将一些步骤延迟到子类中,比如ActivityonCreateonStart等等。这样子类不用改变整体启动Activity过程即可重定义某些具体的操作了~。

    15 访问者模式

    定义:封装一些作用于某种数据结构中各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。

    访问者模式是23种设计模式中最复杂的一个,但他的使用率并不高,大部分情况下,我们不需要使用访问者模式,少数特定的场景才需要。

    Android中运用访问者模式,其实主要是在编译期注解中,编译期注解核心原理依赖APT(Annotation Processing Tools),著名的开源库比如ButterKnife、Dagger、Retrofit都是基于APT。APT的详细使用这里不提,后面我会写关于APT相关的文章,敬请期待~

    16 中介者模式

    定义:中介者模式包装了一系列对象相互作用的方式,使得这些对象不必相互明显调用,从而使他们可以轻松耦合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用保证这些作用可以彼此独立的变化,中介者模式将多对多的相互作用转为一对多的相互作用。

    什么时候用中介者模式呢?其实,中介者对象是将系统从网状结构转为以调停者为中心的星型结构。

    举个简单的例子,一台电脑包括:CPU、内存、显卡、IO设备。其实,要启动一台计算机,有了CPU和内存就够了。当然,如果你需要连接显示器显示画面,那就得加显卡,如果你需要存储数据,那就要IO设备,但是这并不是最重要的,它们只是分割开来的普通零件而已,我们需要一样东西把这些零件整合起来,变成一个完整体,这个东西就是主板。主板就是起到中介者的作用,任何两个模块之间的通信都会经过主板协调。

    那么Android中那些地方用到了中介者模式呢?在Binder机制中,就用到了中介者模式,对Binder不是很熟悉的童鞋请参考我的《 简单明了,彻底地理解Binder》。我们知道系统启动时,各种系统服务会向ServiceManager提交注册,即ServiceManager持有各种系统服务的引用 ,当我们需要获取系统的Service时,比如ActivityManagerWindowManager等(它们都是Binder),首先是向ServiceManager查询指定标示符对应的Binder,再由ServiceManager返回Binder的引用。并且客户端和服务端之间的通信是通过Binder驱动来实现,这里的ServiceManagerBinder驱动就是中介者。

    17 代理模式

    定义:为其他类提供一种代理以控制这个对象的访问。
    其实代理模式我们平时用的也比较多,其实比较好理解,就是当我们需要对一个对象进行访问时,我们不直接对这个对象进行访问,而是访问这个类的代理类,代理类能帮我们执行我们想要的操作。代理模式比较容易理解,既然你来看这篇文章相信你对代理模式不陌生。

    我们直接看看代理模式在Android中的应用,如果你查看AIDL生成的代码就知道,它会根据当前的线程判断是否要跨进程访问,如果不需要跨进程就直接返回实例,如果需要跨进程则返回一个代理,这个代理干什么事情呢?我们在《 简单明了,彻底地理解Binder》提到,在跨进程通信时,需要把参数写入到Parcelable对象,然后再执行transact函数,我们要写的代码挺多的。AIDL通过生成一个代理类,代理类中自动帮我们写好这些操作。

    18 组合模式

    定义:将对象组成成树形结构,以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

    上面的定义不太好理解,我们直接从Android中用到的组合模式说起。我们知道,Android中View的结构是树形结构,每个ViewGroup包含一系列的View,而ViewGroup本身又是View。这是Android中非常典型的组合模式。

    19 适配器模式

    定义:把一个类的接口变换成客户端所期待的另一个接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。

    其实适配器模式很容易理解,我们在Android开发时也经常用到。比较典型的有ListView和RecyclerView。为什么ListView需要使用适配器呢?主要是,ListView只关心它的每个ItemView,而不关心这个ItemView具体显示的是什么。而我们的数据源存放的是要显示的内容,它保存了每一个ItemView要显示的内容。ListView和数据源之间没有任何关系,这时候,需要通过适配器,适配器提供getView方法给ListView使用,每次ListView只需提供位置信息给getView函数,然后getView函数根据位置信息向数据源获取对应的数据,根据数据返回不同的View。

    20 装饰模式

    定义:动态的给一个对象添加额外的智者,就增加功能来说,装饰模式比子类继承的方式更灵活。
    通过简单代码来理解装饰模式:

    public abstract class Component{
        public abstract void operate();
    }
    
    public class ConcreteComponent extends Component{
        public void operate(){
            //具体的实现
        }
    
    }
    
    public class Decorator{
        private Component component;
        public Decorator(Component component){
            this.component=component;
        }
        public void operate(){
            operateA();
            component.operate();
            operateB();
        }
        public void operateA(){
            //具体操作
        }
        public void operateB(){
            //具体操作
        }
    }

    那么在Android哪里出现了装饰模式呢?我们平时经常用到Context类,但是其实Context类只是个抽象类,具体实现是ContextImpl,那么谁是ContextImpl的装饰类呢?我们知道Activity是个Context,但是Activity 并不是继承于Context,而是继承于ContextThremeWrapper.而ContextThremeWrapper继承于ContextWrapper,ContextWrapper继承Context.说了这么多,跟装饰模式有啥关系?主要是引入ContextWrapper这个类。ContextWrapper内部有个Context引用mContext,并且ContextWrapper中对Context的每个方法都有实现,在实现中调用的就是mContext相同的方法。

    21 享元模式

    定义:使用享元对象有效地支持大量的细粒度对象。

    享元模式我们平时接触真的很多,比如Java中的常量池,线程池等。主要是为了重用对象。

    在Android哪里用到了享元模式呢?线程通信中的Message,每次我们获取Message时调用Message.obtain()其实就是从消息池中取出可重复使用的消息,避免产生大量的Message对象。

    22 外观模式

    定义:要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。

    怎么理解呢,举个例子,我们在启动计算机时,只需按一下开关键,无需关系里面的磁盘、内存、cpu、电源等等这些如何工作,我们只关心他们帮我启动好了就行。实际上,由于里面的线路太复杂,我们也没办法去具体了解内部电路如何工作。主机提供唯一一个接口“开关键”给用户就好。

    那么Android哪里使用到了外观模式呢?依然回到Context,Android内部有很多复杂的功能比如startActivty、sendBroadcast、bindService等等,这些功能内部的实现非常复杂,如果你看了源码你就能感受得到,但是我们无需关心它内部实现了什么,我们只关心它帮我们启动Activity,帮我们发送了一条广播,绑定了Activity等等就够了。

    23 桥接模式

    定义:将抽象部分与实现部分分离,使他们独立地进行变化。
    其实就是,一个类存在两个维度的变化,且这两个维度都需要进行扩展。

    在Android中桥接模式用的很多,举个例子,对于一个View来说,它有两个维度的变化,一个是它的描述比如Button、TextView等等他们是View的描述维度上的变化,另一个维度就是将View真正绘制到屏幕上,这跟Display、HardwareLayer和Canvas有关。这两个维度可以看成是桥接模式的应用。

    24 MVC、MVP、MVVP模式

    MVC
    全称为Model-View-Controller,也就是模型-视图-控制器。MVC结构如下图所示:
    MVC

    在Android中对MVC的应用很经典,我们的布局文件如main.xml就是对应View层,本地的数据库数据或者是网络下载的数据就是对应Model层,而Activity对应Controller层。

    MVP
    MVP全称为Model View Presenter,目前MVP在Android应用开发中越来越重要了,它的结构图如下:
    MVP
    它降低了View与Model之间的耦合。彻底将View与Model分离。MVP不是一种标准化的模式,它由很多种实现。

    MVVM

    全称是Mode View ViewModel,它的结构如下所示:

    MVVM
    我们在使用ListView时,会自定义一个ViewHolder,在RecyclerView中是必须使用ViewHolder,这主要是提高性能,因为不需要每次去调用findViewById来获取View。其实ViewHolder就是个ViewModel。

    展开全文
  • 在大家的支持和鼓励下,《Android系统源代码情景分析》一书得以出版了,老罗在此首先谢过大家了。本书的内容来源于博客的文章,经过大半年的整理之后,形成了初稿。在正式出版之前,又经过了三次排版以及修订,最终...

            在大家的支持和鼓励下,《Android系统源代码情景分析》一书得以出版了,老罗在此首先谢过大家了。本书的内容来源于博客的文章,经过大半年的整理之后,形成了初稿。在正式出版之前,又经过了三次排版以及修订,最终得到终稿。然而,老罗深知,书中的内容并不尽完美,除了错误之外总还会有许多不尽人意的地方,因此,欢迎广大读者以及国内外的专家给老罗指出,以便改进。为了达到此目的,老罗特别在此列出该书有错误的地方。

    《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

            现在暂时将书中出现的错误划分为三类,第一类是笔误,第二类是表达问题,第三类是技术性错误,分别使用I、II和III来表示,错误所在的页码使用字母P来表示。

            1. P3,倒数第2行(III):sudo add-apt-repository ppa:ferramrobert/java。由于License问题(http://askubuntu.com/questions/109209/sun-java6-plugin-has-no-installation-candidate),官方的Sun JDK6不能在Ubuntu上发布,因此,现在从下载源ferramrobert已经下载不到JDK6来安装了,可以通过修改安装源来解决这个问题,如下所示:

           A. sudo add-apt-repository "deb http://us.archive.ubuntu.com/ubuntu/ hardy multiverse"

           B. sudo apt-get update

           C. sudo apt-get install sun-java6-jre sun-java6-plugin

           D. sudo apt-get install sun-java6-jdk

           如果在安装过程中,碰到有依赖包未安装,就先把依赖包安装上去就行了。感谢网友@偏左和@大桥++指出,2012-11-05。

           如果这样还不能安装成功,那就只有自己手动安装了,官方JDK下载地址:http://www.oracle.com/technetwork/java/javase/downloads/index.html

           更多的环境配置信息,可以参考官方文档:http://source.android.com/source/initializing.html

           2. P12页,倒数第1个自然段(I):重新生成的Android系统镜像文件ssystem.img位于out/target/product/generic目录中。这句话里面的ssystem.img应该改为system.img。感谢网友@lewisgre指出,2015-05-19。

           3. P20,第278行代码(III):temp = device_create(freg_class, NULL, dev, "%s", FREG_DEVICE_FILE_NAME)。这个函数调用的参数写错了,不过歪打正着,能正常编译以及工作,应该将第四个参数设置为NULL,即为:temp = device_create(freg_class, NULL, dev, NULL, "%s", FREG_DEVICE_FILE_NAME)

           函数device_create的原型为:struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)

           第四个参数drvdata表示一个私有数据,它可以为任意值或者NULL,第五个参数是一个格式化字符串,用来描述设备名称,最后是一个可变参数列表,是配合第五个参数使用的。在上述错误的调用中,参数drvdata的值等于“%s”,而参数fmt的值等于FREG_DEVICE_FILE_NAME,即“freg”。由于没有可变参数列表,并且参数fmt的值不带有%s或者%d之类的格式化符号,因此,这里设置的设备名称就等于“freg”。

           感谢网友@insoonior的指出,2012-11-14。

           4. P20,第297行代码:printk(KERN_ALERT"Succedded to initialize freg device.\n")。这行代码中的Succedded应改为Succeeded。感谢网友@Five_Cent_Nicol指出,2013-02-28。

           5. P26,顺数第二段第2行(I):这些动态链接库文件的命令需要符合一定的规范。这句话中的命令应该改为命名。感谢网友@迷死人的东东指出,2012-12-07。

           6. P30,第一段第4行(I):它的第一个成员变量的类型为freg_device_t。这里话里面的freg_device_t应改为hw_device_t。感谢网友@hustljh指出,2014-09-19。

           7. P33,顺数第二段(I):USER@MACHINE:~/Android$ mmm ./hardware/libhardware/freg。应改为:USER@MACHINE:~/Android$ mmm ./hardware/libhardware/modules/freg。感谢网友@jltxgcy指出,2014-01-06。

           8. P36页,倒数第4个自然段(I):如果不修改设备文件/def/freg的访问权限。这句话里面的/def/freg应该改为/dev/freg。感谢网友@Lasting泉指出,2015-03-05。

           9. P53,lightpointer.cpp的第16行(I):printf("Destory LightClass Object.")。单词Destory拼写错误,应为Destroy。感谢网友@hengbo12345指出,2012-10-26。

           10. P69,第118行代码(I):printf("\nTest Froever Class: \n")。应改为:Forever。相应地,P71页的输出:“Test Froever Class:” 也改为Forever。感谢网友@Coding人生指出,2014-05-05。

           11. P78,倒数第一段第1行和第2行(I):在分析这个函数之前,我们首先介绍三个结构体变量log_main、log_events和log_radio,它们的类型均为struct logger。这句话中的struct logger应改为struct logger_log。感谢网友@迷死人的东东指出,2012-12-11。

           12. P146,顺数第三段第1行(I):这些工作项有可能属于一个进程,也有会可能属于一个进程中的某一个线程。这句话中的也有会可能应改为也有可能。感谢网友@zhouaijia8指出,2013-04-07。 

           13. P147,倒数第9行(I):即将一个Binder实体对象的成员变量work的值设置为BINDER_WORKD_NODE。短语BINDER_WORKD_NODE中间的单词WORKD拼写错误,多了一个字母D,应为BINDER_WORK_NODE。感谢网友@brucechan1973指出,2012-10-30。

           14. P155,最后四段(III):对结构体binder_transaction的成员变量from_parentto_parent的描述有偏差。应改为:

            ---------------------------

            成员变量from_parent和to_parent分别描述一个事务所依赖的另外一个事务,以及目标线程下一个需要处理的事务。假设线程A发起了一个事务T1,需要由线程B来处理;线程B在处理事务T1时,又需要线程C先处理事务T2;线程C在处理事务T2时,又需要线程A先处理事务T3。这样,事务T1就依赖于事务T2,而事务T2又依赖于事务T3,它们的关系如下:

    T1->from_parent = T2; T2->from_parent = T1;
    T2->from_parent = T3; T3->from_parent = T2;
             对于线程A来说,它需要处理的事务有两个,分别是T1和T3,它首先要处理事务T3,然后才能处理事务T1,因此,事务T1和T3的关系如下:
    T3->to_parent = T1;
            考虑这样一个情景:如果线程C在发起事务T3给线程A所属的进程来处理时,Binder驱动程序选择了该进程的另外一个线程D来处理该事务,这时候会出现什么情况呢?这时候线程A就会处于空闲等待状态,什么也不能做,因为它必须要等线程D处理完成事务T3后,它才可以继续执行事务T1。在这种情况下,与其让线程A闲着,还不如把事务T3交给它来处理,这样线程D就可以去处理其他事务,提高了进程的并发性。

            现在,关键的问题又来了——Binder驱动程序在分发事务T3给目标进程处理时,它是如何知道线程A属于目标进程,并且正在等待事务T3的处理结果的?当线程B在处理事务T2时,就会将事务T2放在其事务堆栈transaction_stack的最前端。这样当线程B发起事务T3给线程C处理时,Binder驱动程序就可以沿着线程B的事务堆栈transaction_stack向下遍历,直到发现事务T3的目标进程等于事务T1的目标进程时,它就知道线程A正在等待事务T3的处理结果了。当线程C在处理事务T2时,就会将事务T2放在其事务堆栈transaction_stack的最前端。这样当线程C发起事务T3给线程A所属的进程处理时,Binder驱动程序就可以沿着线程C的事务堆栈transaction_stack向下遍历,即沿着事务T2的成员变量from_parent向下遍历,最后就会发现事务T3的目标进程等于事务T1的目标进程,并且事务T1是由线程A发起来的,这时候它就知道线程A正在等待事务T3的处理结果了。

            --------------------------

            PS:结构体binder_transaction的成员变量from_parent和to_parent可以结合P256的第31行到第40行代码块以及P267的第77行到第79行的代码块来理解。这是个比较严重的技术性错误,由此造成读者的疑惑和费解,老罗先道歉了,同时,非常感谢网友@hongbog_cd指出,2012-11-14。

            15. P155,最后一段第4行和第5行(I):即沿着事务T2的成员变量from、parent向下遍历,最后就会发现事务T3的目标进程等于事务T1的目标进程。这里话里面的from、parent应改为from_parent目标进程应改为源进程。感谢网友@albert1017diu指出,2014-08-06。 

            16. P161页,第2个自然段(I):其中,命令协议代码BR_INCREFS和BR_DECREFS分别用来增加和减少一个Service组件的弱引用计数;而命令协议代码BR_ACQUIRE和BR_RELEASE分别用来增加和减少一个Service组件的强引用计数。这句话里面的命令协议代码应该改为返回协议代码。感谢网友@zlp1992指出,2015-09-13。

            17. P172,顺数第一段第2行和第3行(I):在将内核缓冲区new_buffer加入到目标进程proc的空闲内缓冲区红黑树中之前。这句话中的空闲内缓冲区应改为空闲内核缓冲区。感谢网友@zhouaijia8指出,2013-04-07。

            18. P225,顺数第二段文字后面的目录结构(I):~/Android/frameworks/base/cmdcmd后面少了一个s,应改为cmds。感谢网友@zhouaijia8指出,2013-04-09。

            19. P240,倒数第二段,P696,倒数第五段(III):这个内核缓冲区的大小被Binder库设置为1016Kb、第5行代码创建的匿名共享内存块的大小就为16Kb。这两句话要表达的单位是千字节,应该使用KB来表示,Kb里面的bbit的意思,这里使用不当。感谢网友@hongbog_cd指出,2012-11-17。

            20. P242,倒数第一段最后3行(III):接下来第5行到第8行代码就会在列表mHandleToObject的第N到第(handle+1-N)个位置上分别插入一个handle_entry结构体,最后第11行就可以将与句柄值handle对应的handle_entry结构体返回给调用者。(handle+1-N)描述是要插入的handle_entry结构体的个数,因此,这句话要表达的意思其实是从第N个位置开始,插入(handle+1-N)个handle_entry结构体到列表mHandleToObject中,因此,这句话里面的第N到第(handle+1-N)个位置应该改为第N到第handle个位置。感谢网友@hongbog_cd指出,2012-11-19。

            21. P243,顺数第一段第1行(I):回到ProcessState类的成员函数getContextObject中。这句话中的getContextObject应改为getStrongProxyForHandle。感谢网友@zhouaijia8指出,2013-04-09。

            22. P244,顺数第三段第1行和第2行(I):Service进程在启动时,会首先将它里面的Service组件注册到Service Manager中。这句话开头的Service应改为Server。感谢网友@zhouaijia8指出,2013-04-09。

            23. P252,图5-23(I):binder: FregService->localBinder();cookie: FregService->getWeakRefs()。写反了,应改为:binder:FregService->getWeakRefs();cookie: FregService->localBinder()。感谢网友@jltxgcy指出,2014-05-07。

            24. P270,顺数第一段第2行和第3行(I):它等同于在前面5.1.1小节中介绍的结构体flat_binder_objecflat_binder_objec后面少了一个t,应该改为flat_binder_object。感谢网友@hongbog_cd指出,2012-11-19。

            25. P276,顺数第四段第1行(I):回到函数svcmgr_handler中。这句中的svcmgr_handler应改为do_add_service。感谢网友@zhouaijia8指出,2013-04-10。

            26. P279页,第4个自然段(I):第7行将binder_write_read结构体bwr的输出缓冲区write_buffer设置为由参数data所描述的一块用户空间缓冲区。这句话里面的输出缓冲区应该改为输入缓冲区。感谢网友@jianghu1059指出,2015-09-18。

            27. P329,顺数第四段第2行(I):当这些小块的内存处理解锁状态时。这句话中的处理应该改为处于。感谢网友@nanfeng5651指出,2012-12-13。

            28. P340,倒数第五段第2行和第3行(I):如果是,那么就5行就调用函数lru_del将它从全局列表ashmem_lru_list中删除。这句话中的就5行应改为第5行。感谢网友@zhouaijia8指出,2013-04-11。

            29. P350,顺数第二段和倒数第一段(I):IMemoryBase类定义了MemoryHeapBase服务接口、并且实现了IMemoryBase接口的四个成员函数。这两句话中的IMemoryBase拼写错误,应改为IMemoryHeap。感谢网友@sulliy指出,2012-11-06。

            30. P378,顺数第3行(I):IMemoryFile接口定义了两个成员函数getFileDescriptor和setValue。短语IMemoryFile写错了,应改为IMemoryService。感谢网友@hongbog_cd指出,2012-11-14。

            31. P399,顺数第2行和第4行(I):ManActivity组件。短语ManActivity中的单词Man拼写错误,应改为MainActivity。感谢网友@herodie指出,2012-11-01。

            32. P400,第二段和第三段(I):action = "android.intent.action.Main"、要启动的Activity组件的Action名称和Category名称分别为"android.intent.action.Main"和"android.intent.category.LAUNCHER"。单词Main应全部大写MAIN。感谢网友@herodie指出,2012-11-01。

            33. P418,Step 21(I):ActivityStack.resumeTopActivityLokced。这里话里面的Lokced应改为Locked。感谢网友@SunShinXin指出,2014-12-31。

            34. P420,第16行代码(III):app = new ProcessRecordLocked(null, info, processName)。这里是调用成员函数newProcessRecordLocked来创建一个ProcessRecord对象,而不是直接创建一个ProcessRecordLocked对象。相应地,接下来的一段描述文字“第16行就会根据指定的名称以及用户ID来创建一个ProcessRecordLocked对象”中的ProcessRecordLocked应改为ProcessRecord。感谢网友@android迷指出,2012-12-05。

            35. P497,顺数第二段第1行和第2行(I):第8行到第28行代码在LoadedApk类的mReceivers中检查是否存在一个以广播接收者c为关键字的ReceiverDispatcher对象rd。这句话中的广播接收者c应改为为广播接收者r。感谢网友@nanfeng5651指出,2012-12-19。

            36. P513,顺数第一段第2行和第3行(I):那么第11行就将ActivityManagerService类的成员变量mBroadcastsScheduled的值设置为true。这句话中的true应改为false。感谢网友@zhouaijia8指出,2013-04-17。

            37. P528,顺数第一段最后两行(I):其中,前者用来描述一个vdn.shy.luo.article数据集合即一个博客文章集合,后者用来描述一个vdn.shy.luo.article数据,即一个博客文章条目。这句话中的两个vdn.shy.luo.article应改为vnd.shy.luo.article,分号改为逗号。感谢网友@nanfeng5651指出,2012-12-20。

            38. P528,倒数第一段前面两行(I):其中,DB_TABLE和DB_VERSION用来描述这个SQLite数据库的名称和版本号。这句话中的DB_TABLE应该改为DB_NAME。感谢网友@nanfeng5651指出,2012-12-20。

            39. P539,倒数第一段(I):ArticlesAdapter类的员函数getArticleById和getArticleByPos分别根据一个博客文章的ID值和位置值从ArticlesProvider组件获得指定的博客文章条目。这句话中的员函数应改为成员函数。感谢网友@zhouaijia8指出,2013-04-17。

            40. P560,图10-9(I):左上角的ApplicationThread。应改为:ActivityThread。感谢网友@hongbog_cd指出,2014-01-23。

            41. P573,顺数第二段第3行(I):最后就可以通过这个接口来访问ArtivlesProviders组件中的博客文章信息。这句话中的ArtivlesProviders应改为ArticlesProviders。感谢网友@zhouaijia8指出,2013-04-17。

            42. P573,顺数第三段第1行和第2行(I):我们继续分析MainActivity组件访问运行在另外一个应用程序进程中的ArtivlesProvider组件的博客文章信息的过程。这句话中的ArtivlesProviders应改为ArticlesProviders。感谢网友@zhouaijia8指出,2013-04-17。

            43. P588,倒数第三段第1行(I):参数memobj指向了一个Java层的Binder代理对象。这句话中的memobj应改为memObj。感谢网友@nanfeng5651指出,2012-12-24。

            44. P693,倒数第二段倒数第1行和第2行(I):将前面所创建的WindowState对象win保存在Window管理服务Window ManagerService的成员变量mWindows所描述的一个应用程序窗口列表中。这句话中的Window ManagerService有一个多余的空格,改为WindowManagerService。感谢网友@nanfeng5651指出,2012-12-27。

            45. P713,倒数第三段第1行(I):参数sacnKey和keyCode保存的分别是当前所发生的键盘事件所对应的扫描码和键盘码。这句话中的sacnKey应改为scanCode。感谢网友@nanfeng5651指出,2012-12-28。

            46. P716,顺数第四段第1行(I):第一种情况是在将一个新发生的键盘事件添加到待分键盘事件队列之前。这句话中的待分键盘事件队列应改为待分发键盘事件队列。感谢网友@nanfeng5651指出,2012-12-28。

            47. P730,倒数第四段第2行和第3行(I):第30行代码获得它的一个引用,并且保存在变量inputHandlerObjGlobal中。这句话中的inputHandlerObjGlobal应改为inputHandlerObjLocal。感谢网友@nanfeng5651指出,2013-01-03。

            48. P768,倒数第一段(II):HandlerThread类的成员函数quit的实现如下所示。这段话描述有误,改为:如前所示,HandlerThread类的成员函数quit首先获得前面在子线程中所创建的一个Looper对象,然后再调用这个Looper对象的成员函数quit来退出子线程。Looper类的成员函数quit的实现如下所示。感谢网友@nanfeng5651指出,2013-01-04。

            49. P771,倒数第五段(I):最后,参数workerQueue和threadFactory分别用来描述一个ThreadPoolExecutor线程池的工作任务队列和线程创建工厂。这句话中的workerQueue应改为workQueue。感谢网友@nanfeng5651指出,2013-01-04。

            50. P807,顺数第二段第3行和第4行(I):在这种情况下,参数pkg所描述的一个应用程序所获得的资源权访问权限就与它所共享的Linux用户所具有的资源权访问权限相同。这句话中的资源权访问权限应改为资源访问权限。感谢网友@nanfeng5651指出,2013-01-05。

            51. P812,顺数第三段第2行和第3行(I):如果不存在,那么第22行代码就会将文件/data/system/packages.xml重命令为/data/system/packages-backup.xml。这句话中的重命令应改为重命名。感谢网友@nanfeng5651指出,2013-01-05。

            52. P828,倒数第二段第1行(I):这一步执行完成之后,这回到前面的Step 10中。这句话中的这回应改为返回。感谢网友@nanfeng5651指出,2013-01-05。

    老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

         39. P539,倒数第一段(I):ArticlesAdapter类的员函数getArticleById和getArticleByPos分别根据一个博客文章的ID值和位置值从ArticlesProvider组件获得指定的博客文章条目。这句话中的员函数应改为成员函数。感谢网友@zhouaijia8指出,2013-04-17。
         52. P279页,第4个自然段(I):第7行将binder_write_read结构体bwr的输出缓冲区write_buffer设置为由参数data所描述的一块用户空间缓冲区。这句话里面的输出缓冲区应该改为输入缓冲区。感谢网友@jianghu1059指出,2015-09-18。
         53. P52,顺数第一段第一行(I):sp类也是一个模块类。这句话中的模块类应改为模板类。感谢网友@码农小c指出,2016-05-25。
         54. P63,顺数第一段第一行(I):wp类是一个模块类。这句话中的模块类应改为模板类。感谢网友@码农小c指出,2016-05-25。
    展开全文
  • 在进行UI布局的时候,可能经常会用到 android:gravity 和 android:layout_Gravity 这两个属性。 关于这两个属性的区别,网上已经有很多人进行了说明,这边再简单说一下。 (资料来自网络) LinearLayout有两个非常...

    在进行UI布局的时候,可能经常会用到 android:gravity  和 android:layout_Gravity 这两个属性。

     

    关于这两个属性的区别,网上已经有很多人进行了说明,这边再简单说一下。 (资料来自网络)

     

     

     


     

    LinearLayout有两个非常相似的属性:

    android:gravity与android:layout_gravity。

     

    他们的区别在于:

     

    android:gravity 属性是对该view中内容的限定.比如一个button 上面的text. 你可以设置该text 相对于view的靠左,靠右等位置.

    android:layout_gravity是用来设置该view相对与父view 的位置.比如一个button 在linearlayout里,你想把该button放在linearlayout里靠左、靠右等位置就可以通过该属性设置. 

     

    即android:gravity用于设置View中内容相对于View组件的对齐方式,而android:layout_gravity用于设置View组件相对于Container的对齐方式。

     

    原理跟android:paddingLeft、android:layout_marginLeft有点类似。如果在按钮上同时设置这两个属性。

    android:paddingLeft="30px"  按钮上设置的内容离按钮左边边界30个像素
    android:layout_marginLeft="30px"  整个按钮离左边设置的内容30个像素

     


    下面回到正题, 我们可以通过设置android:gravity="center"来让EditText中的文字在EditText组件中居中显示;同时我们设置EditText的android:layout_gravity="right"来让EditText组件在LinearLayout中居右显示。看下效果:

     

     

     

    正如我们所看到的,在EditText中,其中的文字已经居中显示了,而EditText组件自己也对齐到了LinearLayout的右侧。

     

    附上布局文件:

     

     

     

     


     

    那么上面是通过布局文件的方式来设置的。,相信大家都曾写过,那么如何通过Java代码来设置组件的位置呢?

     

    依然考虑实现上述效果。

     

    通过查看SDK,发现有一个setGravity方法, 顾名思义, 这个应该就是用来设置Button组件中文字的对齐方式的方法了。

    仔细找了一圈,没有发现setLayoutgravity方法, 有点失望。 不过想想也对, 如果这边有了这个方法, 将Button放在不支持Layout_Gravity属性的Container中如何是好! 

     

    于是想到, 这个属性有可能在Layout中 , 于是仔细看了看LinearLayout 的 LayoutParams, 果然有所发现, 里面有一个 gravity 属性,相信这个就是用来设置组件相对于容器本身的位置了,没错,应该就是他了。

     

    实践后发现,如果如此, 附上代码,各位自己看下。

     

     

     

    代码比较简单,但是发现它们还是花了我一点时间的。

     

     

     

    或者这样也可以:

     

     

     

    好了,效果图就不上了,跟上面的一样。 就讲这么多。

     

    另外,要设置在RelativeLayout中的位置时使用addRule方法,如下:

    params = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            params.addRule(RelativeLayout.CENTER_IN_PARENT);
            mContainer.addView(progress,params);

    如果觉得本文对您有帮助, 还请留言支持一下, 非常感谢!

     

    展开全文
  • 上文介绍了Android应用程序的启动过程,即应用程序默认Activity的启动过程,一般来说,这种默认Activity是在新的进程和任务中启动的;本文将继续分析在应用程序内部启动非默认Activity的过程的源代码,这种非默认...
  • 突然想要在android一个消消乐的代码,在此之前没有系统地学过java的面向对象,也没有任何android相关知识,不过还是会一点C++。8月初开始搭建环境,在这上面花了相当的时间,然后看了一些视频和电子书,对...
  • 《第一行代码——Android》已经上市快一个月了,目前销售情况还算良好,也是特别感谢众多朋友的支持。其实一本书如果想要卖的好,除了内容必须要给力之外,封面的设计也是至关重要的,而本书的封面无疑是在充实的...
  • 通常我们程序,都是在项目计划的压力下完成的,此时完成的代码可以完成具体业务逻辑,但是性能一定是最优化的。一般来说,优秀的程序员在代码之后都会不断的对代码进行重构。重构的好处有很,其中一点,...
  • 通常在APP开发中可避免要涉及登陆/注册xml布局文件的编码实现,这些Android APP登陆/注册XML布局文件代码倒不是很难,但是要在xml布局写代码实现UI设计要求的各种颜色、大小、字体、间距、圆角等等细节设计要求...
  • 重复的代码一直都是可维护性的大敌,重构的重要任务之一也就是要去除掉重复的代码,有效的减少重复代码,可以大大提高软件的扩展性。 在Android开发中,很容易产生重复的代码。因为Android是组件,模板式开发,每...
  • 但是这并代表Context没有什么东西好讲的,实际上Context有太小的细节并被大家所关注,那么今天我们就来学习一下那些你所知道的细节。我们知道,Android应用都是使用Java语言来编写的,那么大家可以思考一下...
  • Android组件化方案

    2017-11-20 17:17:14
    每个工程师都要熟悉如此之代码,将很难进行多人协作开发,而且Android项目在编译代码的时候电脑会非常卡,又因为单一工程下代码耦合严重,每修改一处代码后都要重新编译打包测试,导致非常耗时。
  • 转自:http://www.ituring.com.cn/article/177180Java...在谈Java时,我们通常是指Java SE,因为只有这个版本包含虚拟机和编译器。首先,Java代码会被编译成称为字节码的中间格式。当字节码在目标电脑上运行时,虚
  • 在教你写Android网络框架之基本架构一文中我们已经介绍了SimpleNet网络框架的基本结构,今天我们就开始从代码的角度来开始切入该网络框架的实现,在剖析的同时我们会分析设计思路,以及为什么要这样做,这样做的好处...
  • 我们在编写Android程序的时候经常要用到许多图片,不同图片总是会有不同的形状、不同的大小,但在大多数情况下,这些图片都会大于我们程序所需要的大小。比如说系统图片库里展示的图片大都是用手机摄像头拍出来的,...
  • 转自:http://androidperformance.com/ ... ...为了使垃圾回收器可以正常释放程序所占用的内存,在编写代码的时候就一定要注意尽量避免出现内存泄漏的情况(通常都是由于全局成员变量持有对象引用所导
  • 顺便把源码代码整理出来,弄成一个完整的可以运行的apk,今天上午就整理了一下,才发现,源码处理的逻辑一些,考虑的情况,比如开机后接收一个广播,然后从数据库中取时间和当前时间对比,设置闹钟,当时区改变...
  • 最近在搞NDK 开发,使用 android studio 编写C/C++语言 ,但是一直没有代码提示功能。这就很蛋疼了。我的android studio 版本是3.2.0。 然后网上搜了很的资料,什么 创建项目的时候 勾选 Inculde C++ support、 ...
  • 编辑推荐:稀土掘金,这是一个针对技术开发者的一个应用,你可以在掘金上获取最新最优质的技术干货,仅仅是Android知识、前端、后端以至于产品和设计都有涉猎,想成为全栈工程师的朋友不要错过! 原文:Code ...
1 2 3 4 5 ... 20
收藏数 141,230
精华内容 56,492