轻量级log日志框架

898 阅读12分钟

获取当前堆栈信息

private StackTraceElement getCurrentStackTrace() {
        StackTraceElement[] trace = Thread.currentThread().getStackTrace();
        int stackOffset = -1;
        for (int i = 5; i < trace.length; i++) {
            StackTraceElement e = trace[i];
            if (LcLog.class.equals(Logger.class) && i < trace.length - 1 && trace[i + 1].getClassName()
                    .equals(Logger.class.getName())) {
                continue;
            }
            if (e.getClassName().equals(LcLog.class.getName())) {
                stackOffset = ++i;
            }
        }

        return stackOffset != -1 ? trace[stackOffset] : null;
    }

一、日志存储(Log4a原理分析)

日志的收集一直有个痛点,就是性能与日志完整性无法兼得。 要实现高性能的日志收集,势必要使用大量内存,先将日志写入内存中,然后在合适的时机将内存里的日志写入到文件系统中(flush), 如果在 flush 之前用户强杀了进程,那么内存里的内容会因此而丢失。 日志实时写入文件可以保证日志的完整性,但是写文件是 IO 操作,涉及到用户态与内核态的切换,而且这种开销是开启线程都无法避免的,也就是说即使开启一个新线程实时写入也是相对耗时的。

那么,如何才能既减少读写文件次数,又防止断电造成内存数据丢失? 日志首先会写入到 mmap 文件映射内存中,基于 mmap 的特性,即使用户强杀了进程,日志文件也不会丢失

  • mmap 是什么

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

public class LogBuffer {

    private static final String TAG = "LogBuffer";

    private long ptr = 0;
    private String logPath;
    private String bufferPath;
    private int bufferSize;
    private boolean compress;

    //缓存文件的路径,缓存文件的大小,日志的路径
    public LogBuffer(String bufferPath, int capacity, String logPath, boolean compress) {
        this.bufferPath = bufferPath;
        this.bufferSize = capacity;
        this.logPath = logPath;
        this.compress = compress;
        try {
            ptr = initNative(bufferPath, capacity, logPath, compress);
        } catch (Exception e) {
            Log.e(TAG, "LogBuffer Initialization Exception", e);
        }
    }

    public void changeLogPath(String logPath) {
        if (ptr != 0) {
            try {
                changeLogPathNative(ptr, logPath);
                this.logPath = logPath;
            } catch (Exception e) {
                Log.e(TAG, e.getMessage(), e);
            }
        }
    }

    public boolean isCompress() {
        return compress;
    }

    public String getLogPath() {
        return logPath;
    }

    public String getBufferPath() {
        return bufferPath;
    }

    public int getBufferSize() {
        return bufferSize;
    }

    public void write(String log) {
        if (ptr != 0) {
            try {
                writeNative(ptr, log);
            } catch (Exception e) {
                Log.e(TAG, e.getMessage(), e);
            }
        }
    }

    public void flushAsync() {
        if (ptr != 0) {
            try {
                flushAsyncNative(ptr);
            } catch (Exception e) {
                Log.e(TAG, e.getMessage(), e);
            }
        }
    }

    public void release() {
        if (ptr != 0) {
            try {
                releaseNative(ptr);
            } catch (Exception e) {
                Log.e(TAG, e.getMessage(), e);
            }
            ptr = 0;
        }
    }

    static {
        System.loadLibrary("log4a-lib");
    }

    private native static long initNative(String bufferPath, int capacity, String logPath, boolean compress);

    private native void writeNative(long ptr, String log);

    private native void flushAsyncNative(long ptr);

    private native void releaseNative(long ptr);

    private native void changeLogPathNative(long ptr, String logPath);

}
  • 初始化方法(initNative)
  • 写文件(writeNative)
  • 异步刷新(flushAsyncNative)
  • 释放资源(releaseNative)

初始化(initNative)

private native static long initNative(String bufferPath, int capacity, String logPath, boolean compress);

initNative 接受3个参数,分别是缓存文件的路径,缓存文件的大小,日志的路径,返回参数为 native 层的一个对象指针

static jlong initNative(JNIEnv *env, jclass type, jstring buffer_path_,
           jint capacity, jstring log_path_, jboolean compress_) {
    const char *buffer_path = env->GetStringUTFChars(buffer_path_, 0);
    const char *log_path = env->GetStringUTFChars(log_path_, 0);
    size_t buffer_size = static_cast<size_t>(capacity);
    int buffer_fd = open(buffer_path, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
    // buffer 的第一个字节会用于存储日志路径名称长度,后面紧跟日志路径,之后才是日志信息
    if (fileFlush == nullptr) {
        fileFlush = new AsyncFileFlush();
    }
    // 加上头占用的大小
    buffer_size = buffer_size + LogBufferHeader::calculateHeaderLen(strlen(log_path));
    //拿到缓存文件映射为内存后的地址 *buffer_ptr, 通过这个指针操作内存就相当于读写缓存文件了。
    char *buffer_ptr = openMMap(buffer_fd, buffer_size);
    bool map_buffer = true;
    //如果打开 mmap 失败,则降级使用内存缓存
    if(buffer_ptr == nullptr) {
        buffer_ptr = new char[buffer_size];
        map_buffer = false;
    }
    LogBuffer* logBuffer = new LogBuffer(buffer_ptr, buffer_size);
    logBuffer->setAsyncFileFlush(fileFlush);
    //将buffer内的数据清0, 并写入日志文件路径
    logBuffer->initData((char *) log_path, strlen(log_path), compress_);
    //将 LogBuffer 的指针返回给 Java 层。
    logBuffer->map_buffer = map_buffer;

    env->ReleaseStringUTFChars(buffer_path_, buffer_path);
    env->ReleaseStringUTFChars(log_path_, log_path);
    return reinterpret_cast<long>(logBuffer);
}
  1. 首先分别打开缓存文件和日志文件
  2. 然后初始化 AsyncFileFlush ,该类会以异步的方式将文件刷新到日志文件中。
  3. 之后调用 openMMap 拿到缓存文件映射为内存后的地址 *buffer_ptr, 通过这个指针操作内存就相当于读写缓存文件了。 如果 mmap 开启失败则回退到使用普通内存缓存。
  4. 接着初始化 native 层的 LogBuffer。
  5. 最后将 LogBuffer 的指针返回给 Java 层。
openMMap 函数
static char* openMMap(int buffer_fd, size_t buffer_size) {
    char* map_ptr = nullptr;
    if (buffer_fd != -1) {
        // 写脏数据
        writeDirtyLogToFile(buffer_fd);
        // 根据 buffer size 调整 buffer 文件大小
        ftruncate(buffer_fd, static_cast<int>(buffer_size));
        lseek(buffer_fd, 0, SEEK_SET);
        map_ptr = (char *) mmap(0, buffer_size, PROT_WRITE | PROT_READ, MAP_SHARED, buffer_fd, 0);
        if (map_ptr == MAP_FAILED) {
            map_ptr = nullptr;
        }
    }
    return map_ptr;
}
  1. 回写上次因断电(泛指,包括强杀进程)来不及写到日志文件中的脏数据
  2. 根据 buffer size, 使用 ftruncate 调整 buffer 文件大小
  3. 使用 mmap 创建文件内存映射
writeDirtyLogToFile
static void writeDirtyLogToFile(int buffer_fd) {
    struct stat fileInfo;
    if(fstat(buffer_fd, &fileInfo) >= 0) {
        size_t buffered_size = static_cast<size_t>(fileInfo.st_size);
        if(buffered_size > 0) {
            char *buffer_ptr_tmp = (char *) mmap(0, buffered_size, PROT_WRITE | PROT_READ, MAP_SHARED, buffer_fd, 0);
            if (buffer_ptr_tmp != MAP_FAILED) {
                LogBuffer *tmp = new LogBuffer(buffer_ptr_tmp, buffered_size);
                size_t data_size = tmp -> length();
                if (data_size > 0) {
                    tmp -> async_flush(fileFlush, tmp);
                } else {
                    delete tmp;
                }
            }
        }
    }
}
  1. 拿到原文件大小,如果原文件中有内容则开始写入
  2. 使用 mmap 映射文件内存,初始化 LogBuffer,AsyncFileFlush
  3. 异步回写脏数据 tmp.async_flush(&tmpFlush)

写文件(writeNative)

static void writeNative(JNIEnv *env, jobject instance, jlong ptr,
            jstring log_) {
    const char *log = env->GetStringUTFChars(log_, 0);
    jsize log_len = env->GetStringUTFLength(log_);
    LogBuffer* logBuffer = reinterpret_cast<LogBuffer*>(ptr);
    // 缓存写不下时异步刷新
    if (log_len >= logBuffer->emptySize()) {
        logBuffer->async_flush(fileFlush);
    }
    logBuffer->append(log, (size_t)log_len);
    env->ReleaseStringUTFChars(log_, log);
}

先会判断缓存够不够写入新日志,如果不够写入,会调用 async_flush 将原缓存异步写到日志文件中,这个方法会清空原有的缓存,最后将新的日志写入到缓存中中。

AsyncFileFlush.h:
class AsyncFileFlush {

public:
    AsyncFileFlush();
    ~AsyncFileFlush();
    bool async_flush(FlushBuffer* flushBuffer);
    void stopFlush();

private:
    void async_log_thread();
    ssize_t flush(FlushBuffer* flushBuffer);

    bool exit = false;
    std::vector<FlushBuffer*> async_buffer;
    std::thread async_thread;
    std::condition_variable async_condition;
    std::mutex async_mtx;
};

实现其实就是一个生产消费模型,有一个线程读取数组内待写入的日志,如果有日志就写入,没有就等待。外部写入时唤醒写入线程写入。

AsyncFileFlush
void AsyncFileFlush::async_log_thread() {
    while (true) {
        std::unique_lock<std::mutex> lck_async_log_thread(async_mtx);
        while (!async_buffer.empty()) {
            FlushBuffer* data = async_buffer.back();
            async_buffer.pop_back();
            flush(data);
        }
        if (exit) {
            return;
        }
        async_condition.wait(lck_async_log_thread);
    }
}

ssize_t AsyncFileFlush::flush(FlushBuffer* flushBuffer) {
    ssize_t written = 0;
    FILE* log_file = flushBuffer->logFile();
    if(log_file != nullptr && flushBuffer->length() > 0) {
        written = fwrite(flushBuffer->ptr(), flushBuffer->length(), 1, log_file);
        fflush(log_file);
    }
    delete flushBuffer;
    return written;
}

bool AsyncFileFlush::async_flush(FlushBuffer* flushBuffer) {
    std::unique_lock<std::mutex> lck_async_flush(async_mtx);
    if (exit) {
        delete flushBuffer;
        return false;
    }
    async_buffer.push_back(flushBuffer);
    async_condition.notify_all();
    return true;
}

void AsyncFileFlush::stopFlush() {
    exit = true;
    async_condition.notify_all();
    async_thread.join();
}

在构造方法里启动了一个消费者线程,在析构函数中停止。在写文件线程中是一个循环,读取数组中的日志内容并调用 flush 写入日志文件中,最后等待。 async_flush 中会将日志内容放入数组中并通知写入线程开始工作。

LogBuffer.h
class LogBuffer {
public:
    LogBuffer(char* ptr, size_t capacity);
    ~LogBuffer();

    void initData(const char *log_path);
    char* dataCopy();
    size_t dataSize();
    size_t append(const char* log);
    void clear();
    void release();
    size_t emptySize();
    char *getLogPath();
    bool async_flush(AsyncFileFlush *fileFlush);

public:
    bool map_buffer = true;

private:
    char* const buffer_ptr = nullptr;
    char* data_ptr = nullptr;
    char* write_ptr = nullptr;

    size_t buffer_size = 0;
    std::recursive_mutex log_mtx;

};

需要注意的是最后的三个指针,buffer_ptr,data_ptr,write_ptr。 buffer_ptr 是缓存文件映射内存的指针,前面说过缓存文件的格式为:第一个字节会用于存储日志路径名称长度,后面紧跟日志路径,之后才是日志信息 data_ptr 指向的就是日志内容开始的地方。write_ptr 指向的是目前写内存的起始位置。知道这几个指针的作用之后就容易理解了。

LogBuffer::LogBuffer(char *ptr, size_t buffer_size):
        buffer_ptr(ptr),
        buffer_size(buffer_size) {
    data_ptr = buffer_ptr + 1 + buffer_ptr[0];
    write_ptr = data_ptr + strlen(data_ptr);
}
LogBuffer::~LogBuffer() {
    release();
}
void LogBuffer::initData(const char *log_path) {
    std::lock_guard lck_release(log_mtx);
    memset(buffer_ptr, '\0', buffer_size);
    size_t log_path_len = strlen(log_path);
    buffer_ptr[0] = static_cast(log_path_len);
    memcpy(buffer_ptr + 1, log_path, log_path_len);
    data_ptr = buffer_ptr + 1 + log_path_len;
    write_ptr = data_ptr;
}
void LogBuffer::release() {
    std::lock_guard lck_release(log_mtx);
    if(map_buffer) {
        munmap(buffer_ptr, buffer_size);
    } else {
        delete[] buffer_ptr;
    }
}

其中 data_ptr = buffer_ptr + 1 + buffer_ptr[0]; 还是那句话:第一个字节会用于存储日志路径名称长度,后面紧跟日志路径,之后才是日志信息。稍微理解一下就明白了。initData 函数就是初始化 buffer 的数据格式。 write_ptr 为 data_ptr 加上原内容的长度实现之后写入追加的目的。 析构函数中释放资源,释放资源有两种情况,一种是使用 mmap 缓存的,一种是 mmap 开启失败使用普通内存缓存的。

size_t LogBuffer::dataSize() {
    return write_ptr - data_ptr;
}
size_t LogBuffer::emptySize() {
    return buffer_size - (write_ptr - buffer_ptr);
}
char *LogBuffer::dataCopy() {
    size_t str_len = dataSize() + 1;  //'\0'
    char* data = new char[str_len];
    memcpy(data, data_ptr, str_len);
    data[str_len - 1] = '\0';
    return data;
}
void LogBuffer::clear() {
    std::lock_guard lck_clear(log_mtx);
    write_ptr = data_ptr;
    memset(write_ptr, '\0', emptySize());
}
char *LogBuffer::getLogPath() {
    size_t path_len = static_cast(buffer_ptr[0]);
    char* file_path = nullptr;
    if(path_len > 0) {
        file_path = new char[path_len + 1];
        memcpy(file_path, buffer_ptr + 1, path_len);
        file_path[path_len] = '\0';
    }
    return file_path;
}

数据大小与空闲空间大小通过指针就能很快计算出来。数据拷贝通过 memcpy 操作指针来实现拷贝到新数组中,清空缓存使用 memset 将内存置0。使用 getLogPath 取得保存在缓存头的日志路径。所有操作都是针对内存地址实现的,方便且高效。

下面是新增日志信息:

size_t LogBuffer::append(const char *log) {
    std::lock_guard lck_append(log_mtx);
    size_t len = strlen(log);
    size_t freeSize = emptySize();
    size_t writeSize = len <= freeSize ? len : freeSize;
    memcpy(write_ptr, log, writeSize);
    write_ptr += writeSize;
    return writeSize;
}

计算新传入的日志长度,再计算缓存空闲空间,通过 memcpy 直接写入到 buffer 内存中,最后移动写指针 write_ptr 并返回写入长度。在日志缓存不够写入的时候,会造成写入的数据不完整,调用层需要自行判断写入长度与实际长度来重新写入未写入的数据。

当日志缓存写满之后,应该调用刷新缓存:

bool LogBuffer::async_flush(AsyncFileFlush *fileFlush) {
    std::lock_guard lck_clear(log_mtx);
    if (dataSize() > 0) {
        char *data = dataCopy();
        if(fileFlush->async_flush(data)) {
            clear();
            return true;
        } else {
            delete[] data;
            return false;
        }
    }
    return false;
}

刷新缓存会取出缓存的内容,再调用 AsyncFileFlush 异步写入到日志文件中,然后清理缓存空间供新日志写入。

二、JavaMail 发送邮件

compile 'com.sun.mail:android-mail:1.6.0'
compile 'com.sun.mail:android-activation:1.6.0'

pop3 smtp imap 协议

  • pop3:用于接收电子邮件的标准协议;
  • smtp:简单邮件传输协议,用于发送电子邮件的传输协议;
  • imap:互联网消息协议,是 POP3 的替代协议。

常用邮箱服务商协议地址和端口

163邮箱

163邮箱-设置-pop3

qq邮箱

qq邮箱-设置-pop3

发送邮箱步骤

    executorService.execute(new Runnable() {
            @Override
            public void run() {
                createProperties(emailProperty);
                createSession(configuration);
                createMimeMessage(emailMessage);
                transportMessage(configuration,emailProperty);
            }
        });
1、定义property
if (type.equals("163"))
            return new EmailProperty("smtp.163.com", 25, 465);
        else if (type.equals("qq"))
            return new EmailProperty("smtp.qq.com", 25, 465);
        else if (type.equals("sina"))
            return new EmailProperty("smtp.sina.com", 25, 465);
  • createProperties
createProperties(EmailProperty emailProperty) {
        mProperties = new Properties();
        //邮件的协议 pop3 smtp imap
        mProperties.put("mail.transport.protocol", "smtp");
        //== The default host name of the mail server for both Stores and Transports. Used if the mail.xxx.host property isn't set.
        //如果 mail.xxx.host 未设置的时候取它的值
        //mProperties.put("mail.host", MAIL_HOST);
        //== The default user name to use when connecting to the mail server. Used if the mail.protocol.user property isn't set.
        //如果 mail.xxx.user 未设置的时候取它的值
        //mProperties.put("mail.user", FROM_USER);
        mProperties.put("mail.smtp.host", emailProperty.getEmailHost());
        mProperties.put("mail.smtp.ssl.trust", emailProperty.getEmailHost());
        mProperties.put("mail.smtp.port", emailProperty.getEmailHostPort());
        mProperties.put("mail.smtp.user", mUserAddress);//登录邮件服务器的用户名
        //mProperties.put("mail.smtp.class", "mail.smtp.class");
        //qq 邮箱必须要有这个 否则报 530 错误
        mProperties.put("mail.smtp.starttls.enable", true);
        // 开启debug调试
        //        mProperties.put("mail.debug", mIsDebug);
        // 发件人地址,针对服务器来说的  mail.from / mail.smtp.from 邮件被退回(bounced)等时,被退到的邮箱地址 可以设置成 fromAddress
        //服务使用的地址,用来设置邮件的 return 地址。缺省是Message.getFrom()或InternetAddress.getLocalAddress()。mail.user / mail.smtp.user 会优先使用
        mProperties.put("mail.from", mUserAddress);
        // mProperties.put("mail.mime.address.strict", true);//严格
        // mProperties.put("mail.store.protocol", "mail.store.protocol");
        // 发送服务器需要身份验证
        mProperties.put("mail.smtp.auth", true);

        //使用SSL安全连接  java 1.8 有问题
       /* mProperties.put("mail.smtp.socketFactory.class","javax.net.ssl.SSLSocketFactory");
        mProperties.put("mail.smtp.socketFactory.fallback", "false");
        mProperties.put("mail.smtp.socketFactory.port", mEmailProtocol.getEmailHostPortSsl());*/
    }

2、创建 Session
createSession(final EmailConfiguration configuration) {
        //getDefaultInstance 会复用Properties
        //mSession = Session.getDefaultInstance(props, null);
        //mSession = Session.getInstance(props, null);
        //和 mProperties.put("mail.debug", true); 功能一致
        // mSession.setDebug(true);
        mSession = Session.getInstance(mProperties, new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(configuration.getUserName(), configuration.getAuthCode());
            }
        });
    }
3、创建 MimeMessage 实例对象 ,封装邮件信息
try {
            // MimeMessage 邮件类
            MimeMessage msg = new MimeMessage(session);
            // !!!设置发件人地址,针对邮件来说的,是邮件类的属性,收件人邮箱里可以看到的,知道邮件是由谁发的
            // msg.setFrom(new InternetAddress("创业软件" + "<" + fromAddress + ">"));
            msg.setFrom(addressAndName);
            //多个收信人地址 逗号隔开 可以用 Address InternetAddress 封装
            // msg.addRecipients(Message.RecipientType.TO, TO_EMAIL);
            msg.setRecipients(Message.RecipientType.TO, toAddresses);
            //抄送 (抄送一份给发件人,降低 163之类的 报 554 错误(垃圾邮件,屏蔽问题) 的概率)
            msg.addRecipient(Message.RecipientType.CC, addressAndName);
            if (ccAddresses != null) {
                msg.addRecipients(Message.RecipientType.CC, ccAddresses);
            }
            if (bccAddresses != null) {
                //密送
                msg.addRecipients(Message.RecipientType.BCC, bccAddresses);
            }
             msg.setSubject(title, "UTF-8");
            // msg.setSubject(title);//设置主题
            msg.setSentDate(new Date());//设置时间

            //纯文本 msg.setText(content);//设置内容 可以直接使用 setContent 替代,兼容 html代码
            //html  msg.setContent(htmlText, "text/html;charset=UTF-8");
            // msg.setContent("<span style='color:red;'>html邮件测试...</span>","text/html;charset=gbk");
         /*   msg.setContent(
                    "<body><div style='width: 1000px;height: 300px;margin: 0px auto;margin-bottom:20px;border:1px solid #92B0DD;background-color: #FFFFFf;'><h3>这是系统自动发送的邮件,请勿回复!</h3><br/>"+
                            content+"</div></body>",
                    "text/html;charset=UTF-8");*/
            //mixed 混合
            MimeMultipart multipart = new MimeMultipart(SUBTYPE_MIXED);
            // 附件部分
            BodyPart fileBodyPart = createFileBodyPart(files);
            // 创建图文部分
            BodyPart contentBodyPart = createContentBodyPart(htmlText, imageFiless);

            //
            if (fileBodyPart != null) {
                multipart.addBodyPart(fileBodyPart);
            }

            if (contentBodyPart != null) {
                multipart.addBodyPart(contentBodyPart);
            }
            msg.setContent(multipart);
            // 设置回复人 ( 收件人回复此邮件时,不设置就是默认收件人 )
            // msg.setReplyTo();
            // 设置优先级(1:紧急   3:普通    5:低)
            // msg.setHeader("X-Priority", "1");
            // 要求阅读回执(收件人阅读邮件时会提示回复发件人,表明邮件已收到,并已阅读)
            //msg.setHeader("Disposition-Notification-To", fromAddress.toString());

            msg.saveChanges();

        } catch (MessagingException mex) {
            Log.e(TAG, "getMimeMessage:" + mex.getMessage());
            Log.e(TAG, "getMimeMessage:" + mex.getNextException());
        }
4、 获得 Transport 实例对象 ,发送邮件
transportMessage(EmailConfiguration configuration, EmailProperty emailProperty) {
        if (mMimeMessage == null) {
            LcLog.e("transportMessage:mMimeMessage==null ");
            return;
        }
        try {
            //Transport 邮件发送器
            //   PasswordAuthentication 同样也是设置 USERNAME 和 PASSWORD         Transport.send(msg, USERNAME, PASSWORD);
            //###  Transport.send(msg, USERNAME, AUTH_CODE);
            //###   Transport.send(msg);
            // Transport.send 如果有问题 554 错误  用这个
            Transport transport = mSession.getTransport();
            transport.connect(emailProperty.getEmailHost(), configuration.getFromEmail(), configuration.getAuthCode());
            //            transport.connect();
            transport.sendMessage(mMimeMessage, mMimeMessage.getAllRecipients());
            transport.close();
        } catch (MessagingException e) {
            LcLog.e("transportMessage:" + e.getMessage());
        }

    }

发送邮件

        EmailConfiguration emailConfiguration = new EmailConfiguration.Builder()
                .setUserName("***")
                .setAuthCode("****")
                .setFromEmail("***t@163.com")
                .setFromName("d***@163.com")
                .setToEmail("***@qq.com")
                .setPlatform("163")
                .build();
        LcLog.sendMail(emailConfiguration);
        
        
    public void send(final EmailConfiguration configuration) {
        if (configuration == null) {
            LcLog.e("send: EmailConfiguration is null,please initial EmailConfiguration");
            return;
        }
        File file = new File(Environment.getExternalStorageDirectory() + "/lc_log.txt");
        reWriteFile(file);
        final EmailProperty emailProperty = new DeafultEmailProperty(configuration.getPlatform()).createEmailProperty();
        EmailMessage emailMessage = null;
        try {
            emailMessage = EmailMessage.newBuilder()
                    .setTitle("错误日志")
                    .setContent("错误日志在lc_log附件中,请下载查看")
                    .setFiles(new File[]{file})
                    .setTOAddresses(new Address[]{new InternetAddress(configuration.getToEmail())})
                    .build();
        } catch (AddressException e) {
            e.printStackTrace();
        }
        send(configuration, emailProperty, emailMessage);
    }

三、bugly集成

    implementation 'com.tencent.bugly:crashreport:3.1.0'
    implementation 'com.tencent.bugly:nativecrashreport:3.7.1'
 CrashReport.UserStrategy strategy = new CrashReport.UserStrategy(context);
        if (TextUtils.isEmpty(logConfiguration.getBuglyAppId())){
            throw new NullPointerException("LogConfiguration.getBuglyAppId is null,has not set");
        }
        if (TextUtils.isEmpty(logConfiguration.getBuglyAppVersion())){
            Log.i("lklogger","LogConfiguration.getBuglyAppVersion is null,has not set");
        }else {
            strategy.setAppVersion(logConfiguration.getBuglyAppVersion());
        }
        BuglyLog.setCache(12 * 1024);
        CrashReport.initCrashReport(context, logConfiguration.getBuglyAppId(), false, strategy);

出错可以在bugly跟踪日志

四、生成jar,aar,生成maven链接 github链接

jar

module中build.gradle添加

task makeJar(type: Copy) {
    //删除存在的
    delete 'build/libs/lklogger.jar'
    //设置拷贝的文件
    from('build/intermediates/packaged-classes/release/')
    //打进jar包后的文件目录
    into('build/libs/')
    //将classes.jar放入build/libs/目录下
    //include ,exclude参数来设置过滤
    //(我们只关心classes.jar这个文件)
    include('classes.jar')
    //重命名
    rename('classes.jar', 'lklogger.jar')
}

makeJar.dependsOn(build)

aar
一、不包含第三方依赖的打包

将会在module的build/outputs/aar目录下生成 debug 和 release 两个版本的 aar包

二、包含第三方依赖的打包

1、library module中build.gradle中添加

apply plugin: 'maven'

// 省略其他配置

uploadArchives{
    repositories.mavenDeployer{
        // Ubuntu本地仓库路径, Windows 为(url:"file://D://***/***/***/")
        repository(url:"file:/home/pen/develop/other/")
        // 唯一标识
        pom.groupId = "com.lpen"
        // 项目名称
        pom.artifactId = "libraryB"
        // 版本号
        pom.version = "1.0"
    }
}

2、Android Studio右侧的Gradle面板,双击 module下面的 Tasks/upload/uploadArchives

3、引用

// 添加到 root 的 build.gradle
maven{
    url 'file:/home/pen/develop/other/'
}

// 添加到 app 的 build.gradle,注意名字规则和上面配置本地仓库之间的关联
implementation 'com.lpen:libraryB:1.0'
三、线上maven
repositories {
    jcenter()

    maven {
        url project.ext.config.mavenRepositorySettings.projectRepository
    }
    maven {
        url project.ext.config.mavenRepositorySettings.snapshotRepository
    }
}

uploadArchives {
    repositories.mavenDeployer {
        repository(url: project.ext.config.mavenRepositorySettings.projectRepository) {
            authentication(userName: project.ext.config.mavenRepositorySettings.user, password: project.ext.config.mavenRepositorySettings.password)
        }

        snapshotRepository(url: project.ext.config.mavenRepositorySettings.snapshotRepository) {
            authentication(userName: project.ext.config.mavenRepositorySettings.user, password: project.ext.config.mavenRepositorySettings.password)
        }

        pom.version = project.ext.config.moduleSettings.versionName
        pom.artifactId = project.ext.config.mavenLibraySettings.artifactId
        pom.groupId = project.ext.config.mavenLibraySettings.groupId
        pom.name = project.ext.config.mavenLibraySettings.libName
        pom.packaging = 'aar'
    }
}

def setConfigFromExternalParams() {
    def config = project.ext.config

    if (project.hasProperty('V_CODE')) {
        config.moduleSettings.versionCode = Integer.parseInt(V_CODE)
    } else {
    }

    if (project.hasProperty('V_NAME')) {
        config.moduleSettings.versionName = V_NAME
    } else {
    }

    println(project.name + ": ext.config: ")
    println(new groovy.json.JsonBuilder(config).toPrettyString())
}
生成github链接
1、代码上传github
2、release版本
3、jitpack 复制github链接 get it!

感谢: 高性能日志框架