美团实习| 周记(四)

本周知识清单如下:

  • Android Lint工具
  • SimpleDateFormat
  • 线程池之Executors、ThreadPoolExecutor
  • 定时任务之Timer、ScheduledThreadPoolExecutor
  • OnTouchListener、OnClickListener的冲突
  • 一点小感悟

1.Android Lint工具

项目一期需求开发告一段落,为了让代码更好,需要进一步去优化代码结构和质量。

a.是Android Studio提供的代码扫描分析工具,在不运行程序或者写任何测试用例的情况下,帮助发现代码结构和质量问题,并提供一些解决方案。

b.工作流程:根据预先配置的检测标准检查项目的源文件,发现潜在bug和可优化代码,并将扫描结果显示在控制台或者Android Studio中的Event Log里,流程图如下:

其中,图中几个名词含义:

  • App Source Files:包括Java代码、XML代码、icon及ProGuard配置文件等
  • lint.xml:Lint检测的执行标准配置文件
  • lint Tool:静态代码扫描工具,可运行在命令行或者Android Studio中
  • Correctness:正确性,如硬编码、使用过时API等
  • Usability :可用性,如不在文本字段上指定输入的类型等
  • Security: 安全性,如WebView中允许使用JavaScriptInterface等
  • Accessibility :可达性,如图片没有使用contentDescription等。
  • Performance: 性能,如静态引用、循环引用等
  • Internationalization:国际化,如直接使用汉字、没有使用资源引用等

c.使用方法:这里直接用Android Studio的GUI操作

step1:Lint使用路径为『工具栏 -> Analyze -> Inspect Code…

step2:设定要检测的源文件

默认选项是Whole project(整个项目),还可以选择Custom scope自定义文件,有多个选项:Project Files(所有项目文件)、Project Production Files(项目的代码文件)、Project Test Files(项目的测试文件)、Open Files(当前打开的文件)、Module 'app'(主要的ap 模块)、Current File(当前文件)。

当然,还能选择特定的文件,选择右侧的'...'

点击左上角'+'添加一个检查范围,可选项有Local(只能当前项目使用)和Shared(其他项目也可使用)。

这里选择Shared,会提示起规则名。

根据右侧四个按钮的提示:Include(包括当前文件夹内的文件,但不包括其子文件夹)、Include Recursively(包括当前文件夹,及其子文件夹内所有的文件夹)、Exclude(移除当前文件夹,但不包括子文件夹)、Exclude Recursively(移除当前文件夹、及其所有子文件夹),来制定规则。

这里对app选择了Include Recursively,可看到该文件夹及其子文件夹所有文件都被选中了,并且变成了绿色,右上角提示总共有385个文件夹要扫描。

step3:确认完成,等待检测结果

结果显示在底部Inspection对话框,可以看到,除了之前提及的六个,还有Class structure(类结构,如全局变量可替换为局部变量等)、Code maturity issues(代码成熟度问题,如使用弃用的方法等)、Code style issues(代码风格问题,如没必要的符号等)、Compiler issues(编译器问题,如集合缺少泛型符号)、Control flow issues(控制流问题,如去掉没必要的if/else语句)、Data flow issues(数据流问题,如无用的局部变量)、Declaration redundancy(声明冗余,如方法返回值可为void等)、Error handling(错误处理,如异常处理等)、Spelling(拼写)等等,只要打开相应的选项卡,就可以在右侧对话框看到具体描述和问题所在了。

推荐阅读Lint常见的问题及解决方案


2.SimpleDateFormat

a.SimpleDateFormat是一个用于格式化和分析数据的具体类。继承关系如下:

Java.lang.Object
   |
   +----java.text.Format
           |
           +----java.text.DateFormat
                   |
                   +----java.text.SimpleDateFormat

b.常用方法:

先用其构造函数SimpleDateFormat(String str)构造一个格式化日期的格式,再通过它的format(Date date)将指定的日期对象格式化为指定格式的字符串。

假设格式化日期的形式为yyyyy-MM-dd hh:mm:ss a E,含义:

  • yyyyy:年,可匹配如 : 2018。Y和y都表示年。
  • MM:月,可匹配如 : 07。
  • dd:日,可匹配如13。
  • hh:时,可匹配如08。注意:大写H表示24进制计时,小写h表示12进制计时。
  • mm:分,可匹配如23。
  • ss:秒,可匹配如18。
  • a:上/下午,上午为AM、下午为PM。
  • E:星期,可匹配如Fri。

c.使用注意:

在阿里开发手册中一段关于SimpleDateFormat的规范:

【强制】SimpleDateFormat是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils工具类。

原因分析:在SimpleDateFormat内部持有一个Calendar对象的引用,如果把SimpleDateFormat定义为static,可能存在多个Thread同时共享它,即共享对这个Calendar的引用。在高并发情况下,易出现幻读成员变量的现象。

//解决方案一:定义为局部变量
private static final String FORMAT = "yyyy-MM-dd HH:mm:ss";
    public String getFormat(Date date){
        SimpleDateFormat sdf = new SimpleDateFormat(FORMAT);
        return sdf.format(date);
//解决方案二:加锁
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public void getFormat(){
        synchronized (sdf){
        sdf.format(new Date());
        ….;
    }
//解决方案三:使用ThreadLocal使得每个线程都有自己的SimpleDateFormat对象
private static final ThreadLocal<DateFormat> df = newThreadLocal<DateFormat>() {     
       @Override      
       protected DateFormatinitialValue() {
        return newSimpleDateFormat("yyyy-MM-dd");     
      } 
 }; 

推荐阅读SimpleDateFormat线程不安全及解决办法


3.线程池之Executors、ThreadPoolExecutor

a.Executors的类型:

//创建一个单一线程池:线程以队列顺序来执行。
ExecutorService threadPool = Executors.newSingleThreadExecutor();
//创建一个定长线程池,超出的线程会在队列中等待。
ExecutorService threadPool = Executors.newFixedThreadPool(2);
//创建一个定长线程池,支持定时及周期性任务执行。 
ExecutorService threadPool = Executors.newScheduledThreadPool(3);
//创建一个无界线程池:可进行线程自动回收,可存放线程数量最大值为Integer.MAX_VALUE
ExecutorService threadPool = Executors.newCachedThreadPool();

b.在阿里开发手册中说明了Executors各个方法的弊端

  • newFixedThreadPoolnewSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
  • newCachedThreadPoolnewScheduledThreadPool:主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

c.解决方式:改用ThreadPoolExecutor创建线程池,便于明确线程池的运行规则,规避资源耗尽的风险。

其中,ExecutorThreadPoolExecutorScheduledExecutorServiceScheduledThreadPoolExecutor关系如下:

java.util.concurrent.Executor : 负责线程的使用与调度的根接口 
  |–ExecutorService:Executor的子接口,线程池的主要接口 
      |–ThreadPoolExecutor:ExecutorService的实现类 
      |–ScheduledExecutorService:ExecutorService的子接口,负责线程的调度 
          |–ScheduledThreadPoolExecutor:继承了ThreadPoolExecutor实现了ScheduledExecutorService

d.ThreadPoolExecutor的构造函数:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

其中,各个参数的具体含义详见要点提炼|开发艺术之线程,以及有关拒绝策略

然后根据具体需求来创建ThreadPoolExecutor对象,并提供一系列参数来配置线程池即可。而不是直接用有默认配置的Executors创建,事实上看过源码也知道Executors底层也是通过ThreadPoolExecutor去实现的。

推荐阅读JDK 源码解析--Executors、ExecutorService、ThreadPoolExecutor 线程池


4.定时任务之Timer、ScheduledThreadPoolExecutor

a.Timer常见使用:

//延迟2s后执行timer定时器内的任务
Timer  timer = new Timer();
timer.schedule(new TimerTask() {
      @Override
      public void run() {
          //do something

       }
}, 2000);
//延迟2s后执行timer定时器内的任务,之后每隔1s执行一次
Timer  timer = new Timer();
timer.schedule(new TimerTask() {
      @Override
      public void run() {
          //do something

       }
}, 2000,1000);

b.Timer管理延时任务的缺陷:

  • Timer内部只创建了一个线程,因此如果存在多个任务,且任务时间过长,超过了两个任务的间隔时间,会影响其他任务的执行。
  • Timer创建的线程没有处理异常,因此当多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行。

解决办法:JDK5.0后推荐使用ScheduledThreadPoolExecutor代替Timer,顾名思义,ScheduledThreadPoolExecutor内部重用线程池,使用了多线程,使得单个任务的执行不会影响其他线程。

c.ScheduledThreadPoolExecutor的常见使用:

//延迟2s后执行timer定时器内的任务,之后每隔1s执行一次,单位为毫秒
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);//创建大小为2的线程池
executor.scheduleAtFixedRate(new Runnable() {
       @Override
       public void run() {
           //do something
       }
}, 2000, 1000, TimeUnit.MILLISECONDS);

其中,间隔单位的参数有:

  • TimeUnit.MILLISECONDS :毫秒
  • TimeUnit.SECONDS :秒
  • TimeUnit.MINUTES :分钟
  • TimeUnit.HOURS :小时
  • TimeUnit.DAYS:天

推荐阅读深入理解Java线程池:ScheduledThreadPoolExecutor


5.OnTouchListener、OnClickListener的冲突

a.优先度onTouch()>onTouchEvent()>onClick()

//setOnTouchListener需要重写onTouch()
bt.setOnTouchListener(new View.OnTouchListener() {
       @Override
       public boolean onTouch(View v, MotionEvent event) {
            //do something
       return false;
       }
});
//setOnClickListener需要重写onClick()
bt.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View v) {
            //do something
       }
});
@Override
public boolean onTouchEvent(MotionEvent event) {
        //do something
        return super.onTouchEvent(event);
}

b.Warning:当对一个控件使用setOnTouchListener() 或对自定义控件重写onTouchEvent()会出现警告:

If a View that overrides onTouchEvent or uses an OnTouchListener does not also implement performClick and call it when clicks are detected, the View may not handle accessibility actions properly. Logic handling the click actions should ideally be placed in View#performClick as some accessibility services invoke performClick when a click action should occur.

大意是:如果重写了view的onTouchEvent方法或者设置了OnTouchListener,但没有实现performClick并在检测到点击时调用它,view可能无法正确处理辅助操作。理想情况下,处理点击操作的逻辑应放在View#performClick中,因为某些辅助功能服务会在发生单击操作时调用performClick。

c.原因onClick()会通过performClick()完成点击事件的,而在onTouch()onTouchEvent()ACTION_UP过程中会启用一个新的线程来调用performClick(),因而可能会屏蔽掉onClick()中设置的事件。

d.解决办法

  • 如果是使用setOnTouchListener(),那么就在重写onTouch()ACTION_UP情况下调用performClick()
bt.setOnTouchListener(new View.OnTouchListener() {
      @Override
      public boolean onTouch(View v, MotionEvent event) {
           switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        break;
                    case MotionEvent.ACTION_MOVE:
                        break;
                    case MotionEvent.ACTION_UP:
                        //以下为添加内容
                        button.performClick();
                        break;
            }
            return false;//如果返回true,由于优先级较高,后面onTouchEvent、onClick将不会被触发
          }
     });
  • 如果是重写onTouchEvent(),那么就在ACTION_UP情况下调用performClick()
@Override
public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    break;
                case MotionEvent.ACTION_CANCEL:
                    break;
                //以下为添加内容
                case MotionEvent.ACTION_UP:
                    performClick();
                    break;
            }
        return true;
    }

6.一点小感悟

这周的北京莫名的凉快,从上周末开始就断断续续的下了好几场雨,相比于烈日,还是喜欢清爽的雨吧。工作的节奏也似乎跟着淅沥沥的雨慢了下来,后台也才跟着上线,距离下一期的开发还有些时日,也就空出一段难得不被人打扰的日子,做了些对上一期代码质量和性能评价总结等收尾的事情,在这种时候发现到的问题反倒让自己学的更多、成长的更快。

分享一波开发流程,自己也参与到了大部分的过程,虽然没有写很厉害的代码,但是能体验一波完整的开发流程也是收获颇多呢!以及不得不知的互联网职位缩写含义

开发流程图

转眼也来北京一个多月了,除了头几天不太适应北京夏日的高温、以及从未见过如此拥挤的地铁之外,好像就没有特别的感概,时而恍惚以为自己仍在那个美丽的滨海城市大连了,大连在我的眼里也是如此的繁华和热闹。

不过很奇怪的是,貌似不少人还以为春秋招只和应届生有关系,听闻我未毕业就实习都一副吃惊的样子。事实上,现在一年一度的春招,为了能在秋招前提前锁定一批优秀人才,企业的重心反而是在面向大三/研二学生的暑期实习上,这就是为什么暑期实习的招聘流程和秋招几乎一致,要求也是较高的。

可能得益于学校有意识的培养,身边有很多同学都在北京各大互联网公司实习,还有一些老同学在北京读书,又遇上那么多的可爱的小伙伴,所以只不过换了个地方继续生活,有何谈孤单呢!

窗外一景

推荐阅读更多精彩内容