/   今日科技快讯   /

为推动自动驾驶技术的发展和应用,北京市近期发布最新的自动驾驶车辆道路测试管理实施细则,首次允许自动驾驶车辆,进行载人和载物测试。

/   作者简介   /

本篇文章来自小学生不小的投稿,分享了他自己开发维护的Android邮件框架EmailKit,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

小学生不小的博客地址:

https://www.jianshu.com/u/1225fe6bdfc1

/   开始   /

先说一下这个框架的来历,我大一时练习写一个程序需要用到发送邮件验证码的功能,当时直接使用JavaMail来把邮件验证码从客户端中发送出去。

后来无心插柳,大一的暑假把这个功能写成工具类上传到GitHub,第一个版本的功能很简陋,就一个发送邮件功能。后来我利用课余时间开发和维护,经过一年多的快速迭代,这个工具类慢慢地演变成一个框架,框架也逐渐变得稳定了,春华秋实。

/   EmailKit的简介   /

EmailKit for Android是以JavaMail类库为基础进行封装的框架,它比JavaMail更简单易用,在使用它开发电子邮件客户端时,还能避免对电子邮件协议不熟悉的烦恼。目前EmailKit支持的电子邮件协议有SMTP和IMAP,它支持的功能有发送邮件,下载附件、获取文件夹列表、读取邮件、加载邮件、同步邮件,对邮件消息的移动,删除,保存到草稿箱等操作,同时支持邮箱的新邮件消息推送(需要邮件服务器支持相关命令),邮件搜索等功能。

项目地址:

https://github.com/mailhu/emailkit

效果图

使用EmailKit框架设计的邮箱客户端程序的效果图,示例程序的代码可以到这个项目的仓库中获取,由于时间的原因,我只完成基础的功能,希望谅解。

/   安装引入   /

步骤一、将JitPack存储库添加到根目录的build.gradle中:

allprojects {repositories {...maven { url 'https://jitpack.io' }}
}

步骤二、在项目的app模块下的build.gradle里加:

dependencies {implementation 'com.github.mailhu:emailkit:4.2.1'
}

注:因为该库内部使用了Java 8新特性,可能你的项目依赖该框架在构建时出现如下错误:

Invoke-customs are only supported starting with Android O (--min-api 26)

你可以在项目的app模块下的build.gradle里加添如下代码:

android {...compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}
}

在AndroidManifest.xml文件中添加相关权限。

<!--获取联网权限-->
<uses-permission android:name="android.permission.INTERNET"/>
<!--获取读写存储空间权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

/   功能的使用   /

初始化

使用EmailKit的功能前,请在你自定义的Application类中对它进行初始化。

EmailKit.initialize(this);

配置邮件服务器参数

创建一个EmailKit.Config对象,并使用该对象设置邮件服务器相关的参数。还有就是框架的每一个功能都会使用到该对象的。

快速配置:目前只支持QQ邮箱、Foxmail、腾讯企业邮(EXMAIL)、Outlook、yeah.net邮箱、163邮箱、126邮箱。只需通过setMailType()方法选择对应的邮箱类型即可。

EmailKit.Config config = new Email.Config().setMailType(Email.MailType.FOXMAIL)   //选择邮箱类型.setAccount("from@foxmail.com")        //发件人的邮箱.setPassword("password");              //发件人邮箱的密码或者授权码

自定义配置:若在快速配置中没有找对你所需的邮箱类型,你也可以自行填写你使用的邮件服务器的主机地址和端口。

EmailKit.Config config = new Email.Config().setSMTP("smtp.qq.com", 465, true)  //设置SMTP服务器主机地址、端口和是否开启ssl.setIMAP("imap.qq.com", 993, true)  //设置IMAP服务器主机地址、端口和是否开启ssl.setAccount("from@qq.com")          //发件人的邮箱.setPassword("password");           //发件人邮箱的密码或者授权码

使用自定义配置注意事项:1.了解邮件服务器端口号是否需要开启SSL加密;2.了解邮箱登录是使用密码还是授权码;3.是否需要在邮箱的官方平台上开启IMAP/SMTP服务

验证服务器配置参数

已创建好EmailKit.Config对象并设置了邮件服务器的相关参数后,如果你要验证设置的参数是否正确,这时可以使用框架提供的验证方法,这个方法相当于登录验证功能。

EmailKit.auth(config, new EmailKit.GetAuthCallback() {@Overridepublic void onSuccess() {Log.i(TAG, "配置成功!");}@Overridepublic void onFailure(String errMsg) {Log.i(TAG, errMsg);}
});

发送一封简单的邮件

创建一个Draft对象,设置好发件人昵称、收件人的邮箱地址、邮件主题和正文,然后使用SMTPService对象中的send()方法发送这封邮件,发送成功后这封邮件会自动保存到邮箱的“已发送”文件夹。Draft类的更多用法在文档中有详细的介绍哦!

//写邮件
Draft draft = new Draft().setNickname("小学生")          //发件人昵称.setTo("to@qq.com")             //收件人.setSubject("这是一封测试邮件")  //主题.setText("Hello world!");       //正文//发送邮件
EmailKit.useSMTPService(config).send(draft, new EmailKit.GetSendCallback() {@Overridepublic void onSuccess() {Log.i(TAG, "发送成功!");}@Overridepublic void onFailure(String errMsg) {Log.i(TAG, errMsg);}});

获取邮箱的文件夹列表

什么是邮箱的文件夹?就是邮箱中的“收件箱”、“垃圾箱”、“已发送”等功能即可看作邮箱的文件夹,每个文件夹都放着许多邮件消息。我们可通过IMAPService对象中的getDefaultFolderList()方法来获取邮箱中的文件夹列表。

EmailKit.useIMAPService(config).getDefaultFolderList(new EmailKit.GetFolderListCallback() {@Overridepublic void onSuccess(List<String> folderList) {Log.i(TAG, Arrays.toString(folderList.toArray()));}@Overridepublic void onFailure(String errMsg) {Log.i(TAG, errMsg);}});

使用Foxmail邮箱、QQ邮箱、腾讯企业邮所获取到的邮箱文件夹列表如下:

[其他文件夹, INBOX, Sent Messages, Drafts, Deleted Messages, Junk]

获取到文件夹列表的全部文件夹名称后,可以使用IMAPService对象的getFolder()方法获取对应文件夹对象。而getInbox()方法和getDraftBox()方法可以直接获取收件箱和草稿箱的文件夹对象。Inbox类和DraftBox类是子类,继承父类Folder类。

//使用IMAP服务
IMAPService service = EmailKit.useIMAPService(config);//获取收件箱
Folder inbox1 = service.getFolder("INBOX");
//获取草稿箱
Folder drafts = service.getFolder("Drafts");
//获取垃圾箱
Folder junk = service.getFolder("Junk");//获取收件箱
Inbox inbox = service.getInbox();
//获取草稿箱(可能有未知Bug,但目前还没发现)
DraftBox draftBox = service.getDraftBox();

加载邮件

加载邮件的方法一般用于邮箱的消息列表中,它每次拉取的邮件消息数量的区间为[0, 20],自带分页加载功能。如果邮箱是第一次加载邮件消息,只需传入一个小于0的参数给load()方法,它会自动拉取最新且最多20条消息,然后把拉取到的消息的邮件头信息的内容保存到SQLite中即可,当你需要加载下一分页消息时,你只需把参数改为本地所缓存的消息中uid最小的那个值。

EmailKit.useIMAPService(config).getInbox().load(-1, new EmailKit.GetLoadCallback() {@Overridepublic void onSuccess(List<Message> msgList) {for (Message msg : msgList) {Log.i(TAG, "uid:" + msg.getUID());Log.i(TAG, "主题:" + msg.getSubject());Log.i(TAG, "时间:" + msg.getSentDate().getText());Log.i(TAG, "发件人:" + msg.getSender().getAddress());Log.i(TAG, "是否已读:" + msg.getFlags().isRead());Log.i(TAG, "---------------------------------------");}}@Overridepublic void onFailure(String errMsg) {Log.i(TAG, errMsg);}});

操控台上打印出我收件箱最新20条消息的比较主要的邮件头信息,这里我展示其中的两条。

2019-11-30 20:35:54.148 uid:1087
2019-11-30 20:35:54.148 主题:万网域名续费通知
2019-11-30 20:35:54.148 时间:2019年11月27日 11:18
2019-11-30 20:35:54.148 发件人:system@notice.aliyun.com
2019-11-30 20:35:54.148 是否已读:true
2019-11-30 20:35:54.148 ---------------------------------------
2019-11-30 20:35:54.148 uid:1085
2019-11-30 20:35:54.148 主题:阿里云ECS续费成功提醒
2019-11-30 20:35:54.148 时间:2019年11月26日 04:35
2019-11-30 20:35:54.148 发件人:system@notice.aliyun.com
2019-11-30 20:35:54.149 是否已读:true
2019-11-30 20:35:54.149 ---------------------------------------

你还可以加载垃圾箱的邮件消息,只需通过垃圾箱的文件夹名称来获取垃圾箱的对象,腾讯系邮箱的垃圾箱名称为“Junk”,而网易系邮箱的垃圾箱名称为“垃圾箱”。一般来说,好像所有邮箱的收件箱名称都是“INBOX”。

EmailKit.useIMAPService(config).getFolder("Junk").load(-1, new EmailKit.GetLoadCallback() {@Overridepublic void onSuccess(List<Message> msgList) {for (Message msg : msgList) {Log.i(TAG, "uid:" + msg.getUID());Log.i(TAG, "主题:" + msg.getSubject());Log.i(TAG, "时间:" + msg.getSentDate().getText());Log.i(TAG, "发件人:" + msg.getSender().getAddress());Log.i(TAG, "是否已读:" + msg.getFlags().isRead());Log.i(TAG, "---------------------------------------");}}@Overridepublic void onFailure(String errMsg) {Log.i(TAG, errMsg);}});

同步邮件

同步方法用于拉取邮件消息列表的新邮件和检查本地是否有邮件需要删除。使用时把本地缓存的全部消息的uid插入到数组中,然后数组作为参数传给sync()方法。

long[] uids = new long[]{1, 2, 3, 4, 5, 6}; EmailKit.useIMAPService(config).getInbox().sync(uids, new EmailKit.GetSyncCallback() {@Overridepublic void onSuccess(List<Message> newMsgList, long[] deletedUID) {//新邮件for (Message msg : newMsgList) {Log.i(TAG, "uid:" + msg.getUID());Log.i(TAG, "主题:" + msg.getSubject());Log.i(TAG, "时间:" + msg.getSentDate().getText());Log.i(TAG, "发件人:" + msg.getSender().getAddress());Log.i(TAG, "是否已读:" + msg.getFlags().isRead());Log.i(TAG, "---------------------------------------");}//本地需要删除的邮件的uidLog.i(TAG, Arrays.toString(deletedUID));}@Overridepublic void onFailure(String errMsg) {Log.i(TAG, errMsg);}});

获取一封邮件的内容

如何获取一封邮件的内容?框架提供getMsg()方法可以让你获取到邮件消息的全部内容。为了设计框架的Message类我花了挺多心思和时间,因为JavaMail提供的Message类,可能让很多开发者hold不住(对不起,是真的hold不住.png),所以我特意封装了一层由EmailKit提供的Message类,它更简单,更易用和更高效。

EmailKit.useIMAPService(config).getInbox().getMsg(1064, new EmailKit.GetMsgCallback() {@Overridepublic void onSuccess(Message msg) {Log.i(TAG, "uid:" + msg.getUID());Log.i(TAG, "主题:" + msg.getSubject());Log.i(TAG, "时间:" + msg.getSentDate().getText());Log.i(TAG, "发件人:" + msg.getSender().getAddress());Log.i(TAG, "发件人昵称:" + msg.getSender().getNickname());Log.i(TAG, "------------------------------------------------");Log.i(TAG, "正文类型:" + msg.getContent().getMainBody().getType());Log.i(TAG, "正文内容:" + msg.getContent().getMainBody().getText());Log.i(TAG, "------------------------------------------------");List<Message.Content.Attachment> attachments = msg.getContent().getAttachmentList();for (Message.Content.Attachment attachment : attachments) {Log.i(TAG, "附件名称:" + attachment.getFilename());Log.i(TAG, "附件大小:" + attachment.getSize() + " byte");Log.i(TAG, "附件类型:" + attachment.getType());Log.i(TAG, "附件是否已下载:" + attachment.getFile().exists());Log.i(TAG, "------------------------------------------------");}}@Overridepublic void onFailure(String errMsg) {Log.i(TAG, errMsg);}});

EmailKit提供的Message类的特性:获取邮件正文数据和附件数据是使用懒加载,即拿到Message对象时,头信息的数据也可以即时拿到了,而邮件正文数据和附件数据只有调用相关的API才会异步获取。这么设计是受限于JavaMail的性能限制,上面提到的加载和同步建议只拉取邮件的头信息数据,原因是如果加载和同步时就拉取邮件正文数据和附件数据,会导致拉取数据耗时长,流量消耗大。

下面是在操控台上打印的一封邮件内容,可以观察到打印“正文类型”的时间和“发件人昵称”的时间间隔,还有打印“附件名称”的时间和“发件人昵称”的时间间隔,按此推论,加载和同步只拉取邮件的头信息应该可以节省很多时间。

2019-11-30 21:28:34.226 uid:1064
2019-11-30 21:28:34.226 主题:附件测试
2019-11-30 21:28:34.226 时间:2019年11月23日 13:08
2019-11-30 21:28:34.226 发件人:zguanhu@126.com
2019-11-30 21:28:34.226 发件人昵称:zguanhu
2019-11-30 21:28:34.226 ------------------------------------------------
2019-11-30 21:28:34.367 正文类型:text/html
2019-11-30 21:28:34.411 正文内容:<div dir="ltr">哈哈哈<br></div><div dir="ltr"><br></div><div dir="ltr"><br></div><div dir="ltr"><br></div><div class="wps_signature">发自我的小米手机</div>
2019-11-30 21:28:34.411 ------------------------------------------------
2019-11-30 21:28:34.420 附件名称:-1263222bf178296f.png
2019-11-30 21:28:34.420 附件大小:473594 byte
2019-11-30 21:28:34.420 附件类型:image/png
2019-11-30 21:28:34.421 附件是否已下载:false
2019-11-30 21:28:34.421 ------------------------------------------------

查看邮件内容时,如果这封邮件带有附件,无论哪个邮件客户端都不会直接就下载到本地,需要用户来确认是否下载,而且下载成功后,如果本地已存在该附件,则不会再次下载。EmailKit为了满足这个需求也提供了相关的接口,下面展示如何获取本地的附件文件和从邮件服务器上下载附件。

//获取本地的附件文件
File localFile = attachment.getFile();
//判断一下附件文件是否存在
if (localFile.exists()) {//存在,打印附件的路径Log.i(TAG, localFile.getPath());
} else {//不存在,先下载,再打印附件的路径attachment.download(file -> Log.i(TAG, file.getPath()));
}

当附件已下载到本地,可以调用系统提供的打开方式来打开相应的附件,框架提供的getType()方法得到的是MIME格式的类型,直接可以使用,这样无需开发者在使用文件后缀名来获取相关的文件格式类型了。你也可以使用腾讯X5浏览服务来打开附件,同时显示邮件正文内容也可以使用X5浏览服务来代替WebView控件。

//设置类型和Uri对象
String type = attachment.getType();
Uri uri = Uri.fromFile(attachment.getFile());
//选择打开附件的方式
Intent intent = new Intent().setAction(Intent.ACTION_VIEW).setDataAndType(uri, type);
startActivity(intent);

因篇幅有限,文本只简单介绍框架的一些基础功能,需要了解更多详细的功能使用,可以移步到GitHub上查阅Wiki文档哦!

/   邮件协议与框架的设计   /

SMTP与IMAP的介绍

SMTP的全称是“Simple Mail Transfer Protocol”,即简单邮件传输协议。它是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。SMTP 服务器就是遵循 SMTP 协议的发送邮件服务器。

IMAP(Internet Mail Access Protocol)以前称作交互邮件访问协议(Interactive Mail Access Protocol),是一个应用层协议。IMAP是斯坦福大学在1986年开发的一种邮件获取协议。它的主要作用是邮件客户端可以通过这种协议从邮件服务器上获取邮件的信息,下载邮件等。

EmailKit的核心功能

EmailKit是一个邮件客户端核心框架,在JavaMail的基础上进行封装和设计,只提供关于SMTP和IMAP的邮件通信服务,它并不是一个完整的邮件客户端程序,它只关注邮件的网络通信,与UI解耦,也不提供数据缓存(附件除外)。使用EmailKit框架设计的邮件客户端相当于下图所示的用户代理。

为什么支持IMAP而不支持POP3?

EmailKit早期支持IMAP和POP3,后因框架的快速迭代,框架无法对POP3作出合理的兼容,只能对其舍弃。例如:通过IMAP可以从服务器中获取到邮件是否已读的状态,而用POP3则不行,POP3要实现这个功能需要从客户端的逻辑实现,这样又会涉及到数据持久化的问题,所以在4.0版本后不再支持POP3。

为什么框架不做数据缓存?

EmailKit获取到的邮件消息需要使用SQLite这种关系型数据库来缓存。关于使用SQLite缓存的问题,我承认自己写不好,同时也希望框架不要过度封装,所以框架内部不做数据缓存。EmailKit的Demo使用LitePal来完成数据的缓存也很简单和高效,当然,你也可以使用其他的SQLite操作框架。但是附件的文件缓存这种文件IO流操作是由框架内部完成,开发者可以使用框架默认的附件目录路径或者自定义附件的目录路径。

人性化设计

设计一:平常我们使用的QQ邮箱客户端中,打开登录界面,会显示一些主流邮箱类型,你选择对应的邮箱,只需输入账号密码即可登录,无需在配置邮件服务器相关的参数,所以框架的快速配置功能就是借鉴了一个实现,虽然技术含量不高,但是它做到了“Don't make me think”。

设计二:邮件正文的提取,有一些比较特殊的情况,同样正文内容的一封邮件数据里却包含了一份text/plain格式的内容和一份text/html格式的内容,例如Apple公司的广告邮件,Twitter和Google等公司的邮件就会出现“一式两份”的情况,如果不做分离的话,就会出现内容显示重复的情况,所以框架内部解析邮件正文的做法是优先返回text/html格式的正文。正常情况,一般就一种格式的正文。

设计三:线程池和服务器连接对象。因为Android的程序不允许在UI线程中进行网络请求,所以框架的内部每次网络请求都是在子线程中进行,早期框架功能较少的时候,简单粗暴直接使用new Thread()开启线程,后来功能增多,野线程会出现线程抢占的情况,就引入了线程池来管理线程,每次网络请求完成在回调前都会切换回UI线程。另外框架内部会对连接服务器的对象进行管理,让连接对象可以多次复用。我想起刚学习Android开发时,使用第三方SDK的某个功能,它内部的网络请求完成后回调,我更新UI,程序就崩溃了,当时把我这小白给整懵了,后来才知道子线程不能直接更新UI。

设计四:同步和加载,Javamail并没有提供同步与加载的功能,所以只能在框架的层面设计这两个功能。以加载功能为例,每次使用加载方法只需传入本地客户端最小的uid,它自然加载20条比该uid值更小的消息。那问题来了,如果邮箱存在几百封邮件,框架内部是如何最快确认下一轮需要加载的邮件的起始位置?核心代码如下:

/*** 算法功能:在元素值递增的数组中查找刚比目标uid小的uid的下标* uid表 = [1, 2, 3, 4, 5, 6, 8, 9, 10]* 下标值: [0, 1, 2, 3, 4, 5, 6, 7, 8 ]** 假设目标uid = 11,刚比目标uid小的uid = 10,该uid的index = 8* 假设目标uid = 10,刚比目标uid小的uid = 9,该uid的index = 7* 假设目标uid = 7,刚比目标uid小的uid = 6,该uid的index = 5* 假设目标uid = 0,刚比目标uid小的uid不存在,返回值 = -1*/
private static int searchIndex(IMAPFolder folder, Message[] msgList, long uid) {for (int low = 0, high = msgList.length - 1, last = high, mid; low <= high; ) {mid = (low + high) / 2;if (folder.getUID(msgList[mid]) > uid) {high = mid - 1;if (high >= 0 && folder.getUID(msgList[high]) < uid)return high;} else if (folder.getUID(msgList[mid]) < uid) {low = mid + 1;if (low == last && folder.getUID(msgList[low]) < uid)return low;} else {return mid - 1;}}return -1;
}

JavaMail获取邮件的uid需要使用IMAPFolder对象的getUID()方法,每使用一次该方法相当于一次网络请求,这样就会产生“请求与响应”的耗时。使用顺序查找来获取uid的下标这种方案可以直接忽略了,但服务器的邮件uid是自增的,有序的,这符合折半查找的条件,使用折半查找大大提高查找的效率,但是折半查找完成后如果没有找到对应的uid,返回值为-1。

但实际情况,有可能客户端A缓存了“消息7”,而在客户端B却删除了“消息7”,客户端B的操作结果同步到服务器上,所以服务器也会删除“消息7”,假设客户端A缓存的最小uid为7,然后拿着这个uid去加载。这种情况使用折半查找结束后并没有找到下一轮加载的起始位置。但可以修改一下折半查找的算法,修改后的算法如上面的代码所示,让新的算法可以实现,在一组有序递增的数列中,指定任意一个数值n,都能找到一个刚好比数值n小的数的下标,若没有找到,则说明数值n比数列中的任意一个元素的值都小。这是我想到的其中一种方案,不排除还有更好的算法可以实现这个功能。

设计五:更简单的邮件消息类,与发信相关的Draft类和与读信相关的Message类已经是十分简单了,如果使用JavaMail原生的API来发送邮件和读取邮件,你还需要了解MIME协议。简单来说早期SMTP只支持传输7位ASCII码的邮件,后来的邮件内容图文并茂,就提出使用MIME来扩充SMTP,MIME让邮件内容支持图片,音频,视频等文件,甚至支持内容为非英文的邮件通过SMTP发送。开发框架那时,我刚开始写解析邮件消息时遇到很多问题从JavaMail的API层面比较难理解透彻,后来使用Foxmail客户端来查看邮件源码,同时查阅计算机网络协议的书籍,不断踩坑,推理,编码,调试,最后逐渐完成邮件内容的解析提取。下面展示的是使用Foxmail客户端来查看一封比较简单的邮件源码:

/   题外话   /

再聊一些题外话和知识,Foxmail的是张小龙前辈独立开发的,这点挺令人佩服的。在一些计算机网络的书籍在邮件协议那部分会提到早期版本的Foxmail提供一种“特快专递”的功能(现在找不到了),就是Foxmail客户端直接利用SMTP把邮件发送到接收方的邮件服务器,这就省去了在发送方邮件服务器中的排队等待时间。下面是我通过自己的理解来画的工作原理图(不保证完全正确):

但书上并没有详细写到它的工作原理,我的推理(不保证完全正确)应该是Foxmail通过收件人的邮件地址截取出域名后,通过DNS服务器查找相应的MX记录,然后解析出收件人的SMTP服务器名称,获取到收件人的SMTP服务器名称,Foxmail客户端直接与该SMTP服务器建立连接发送邮件。

当我画完邮件消息传递图时,我看着图中表达的内容似曾相识。后来想起很久之前从微信后台团队的公众号中看过一篇《从0到1:微信后台系统的演进之路》的文章,里面提到的部分内容大致如下:

微信起初定位是一个通讯工具,作为通讯工具最核心的功能是收发消息。微信团队源于广硏团队,消息模型跟邮箱的邮件模型也很有渊源,都是存储转发。

图1展示了这一消息模型,消息被发出后,会先在后台临时存储;为使接收者能更快接收到消息,会推送消息通知给接收者;最后客户端主动到服务器收取消息。

好,题外话就这么多,不多讲......

/   结语   /

开发框架的过程意识到基础知识的重要性,同时这一过程也让自己有所收获。大二的时候计划开发邮箱客户并开源核心功能的源码,当时了解到Android平台已经有k-9 Mail这样优秀的开源邮箱,我的心态是“底层的仰望”,只抱着尝试的心态,就把当初的的工具类重构成邮箱客户端的核心框架,不断迭代和重构。大三时框架的功能逐渐完成,可我已经没时间完成邮箱客户端开发的计划。大学时光差不多过了八分之五了,现在考虑着找实习的问题,心里一直向往着与产品相关工作岗位,向往的事当然要有所准备,付诸行动。

最后这个项目欢迎大家star或fork,学习与使用。

推荐阅读:

使用揭露动画,让你的应用特效更进一步!

作者说这是初级Android工程师的面经?吓到我了!

面试官问你:自定义View跟绘制流程懂吗?帮你搞定面试官

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

我大一的时候就写出了一个Android邮件框架相关推荐

  1. Laravel php 框架的使用写出第一个hello world,Laravel 入门配置

    Laravel 第一次使用,如何写出第一个hello world Laravel php 框架第一次接触,遇到一些困难,在这里记录一下,有需要的童鞋可以看一下 从github上下载下来最新版,地址如下 ...

  2. 【初识C语言】如何写出第一个C语言代码

    如何写代码? 1.写出主函数(main函数) 如何执行?-c语言是从主函数的第一行开始执行的 所以c语言代码中得有mian函数-入口 printf -库函数-在屏幕上打印信息 printf 的使用,也 ...

  3. 快应用之先写出第一个hello world

    快应用简介 快应用是各大手机厂商联合制定的,类似于微信小程序都是采用css+js前端开发,不同于微信的是,微信小程序依附在微信上,而快应用是可以再各大安卓应用市场上搜索直接打开,无须安装.还可以直接生 ...

  4. 写出我的第一个框架:迷你版Spring MVC

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:张丰哲 https://www.jianshu.com/p/ ...

  5. 【手写系列】写出我的第一个框架:迷你版Spring MVC

    你没有看错标题,今天,我将实现我人生中第一个框架,^_^ 前期准备 我这里要写的是一个迷你版的Spring MVC,我将在一个干净的web工程开始开发,不引入Spring,完全通过JDK来实现. 我们 ...

  6. 自己写的第一个android 游戏《是男人就下100层》

    自己开发的第一个android 游戏<是男人就下100层>,注意是安卓游戏,不是IPhone的.截图如下:            这个有游戏是用重力感应来控制的,所以要晃动手机来控制人物移 ...

  7. android搬家iphone,苹果又出了一个 Android 应用,帮你搬家去 iPhone

    今年前两个季度,苹果卖出了破纪录的 iPhone.它的 CEO 库克年初曾经说过,大部分 iPhone 6 用户之前都是 Android 用户. 现在苹果出了一个新应用Move to iOS,让 An ...

  8. 用 Python 写出了一个 Gameboy 模拟器

    点击上方"编程派",选择设为"设为星标" 优质文章,第一时间送达! 感觉用 Atari 游戏研究人工智能有点「不够接地气」?现在我们可以使用 Gameboy 模 ...

  9. python写计算机模拟器_用 Python 写出了一个 Gameboy 模拟器

    点击上方"编程派",选择设为"设为星标" 优质文章,第一时间送达! 感觉用 Atari 游戏研究人工智能有点「不够接地气」?现在我们可以使用 Gameboy 模 ...

最新文章

  1. ubuntu /boot 空间清理
  2. linux上最好用的sh --zsh
  3. FPGA中建立时间和保持时间不满足如何解决
  4. 用什么来代替switch_一根转动的圆筒能有什么用?可以用它来代替机翼、船帆
  5. 驼峰设计 PPT设计网站
  6. 【错误解决】[Maven] cannot be opened because it does not exist错误[文件无法编译到target目录下的解决方法]...
  7. matlab 三维图像配准,[转载]Matlab实现多种图像配准(转)
  8. facebook.com_如何降低电子商务的Facebook CPM
  9. SQL Server 索引和表体系结构(三)
  10. 常用中后台交互设计控件使用场景与规范总结
  11. tinymce vue 部分工具不显示_工具栏图标未在tinymce(4.0.1)文本编辑器中显示
  12. 不忘初芯 NEC发布系列工程显示新品解决方案
  13. 省级应急指挥平台建设方案
  14. C++实现设计模式——Builder模式
  15. 100道接口测试面试题收好了!【建议收藏】
  16. oracle有rtf函数,Delphi中对Oracle存取RTF文档(作者:苏涌)
  17. Docker 容器中添加字体
  18. VUE小需求——旋转小图标
  19. 算法:如何判断两颗二叉树是否相等
  20. ARGIS利用计算器对属性表数据进行编号

热门文章

  1. linux的免费虚拟机,Win10下的Linux+非虚拟机+非双系统+可靠教程+免费
  2. 如何评价项目计划的可行性?
  3. 设计模式(11)代理模式The Proxy Pattern - 1 - 远程代理rmi
  4. 《C语言程序设计》 游戏五子棋
  5. Bootstrap系列之导航
  6. 公司创建初期,用哪个企业邮箱好?
  7. 微分中值定理之拉格朗日中值定理
  8. 通用绩效考核系统问题列表
  9. 直销现状:从奖金分配政策看直销
  10. 【加密芯片】加密芯片——ATSHA204A的使用