算法之优先队列 PriorityQueue解决Top K 问题

转自:https://www.jianshu.com/p/a4a1984fc4ff

解决方法:
维护一个大小为 K 的小顶堆,依次将数据放入堆中,当堆的大小满了的时候,只需要将堆顶元素与下一个数比较:如果大于堆顶元素,则将当前的堆顶元素抛弃,并将该元素插入堆中。遍历完全部数据,Top K 的元素也自然都在堆里面了。
在这里插入图片描述

当然,如果是求前 K 个最小的数,只需要改为大顶堆即可
在这里插入图片描述
将数据插入堆 95 大于 20,进行替换 95 下沉,维持小顶堆
对于海量数据,我们不需要一次性将全部数据取出来,可以一次只取一部分,因为我们只需要将数据一个个拿来与堆顶比较。
在这里插入图片描述
另外还有一个优势就是对于动态数组,我们可以一直都维护一个 K 大小的小顶堆,当有数据被添加到集合中时,我们就直接拿它与堆顶的元素对比。这样,无论任何时候需要查询当前的前 K 大数据,我们都可以里立刻返回给他。

整个操作中,遍历数组需要 O(n) 的时间复杂度,一次堆化操作需要 O(logK),加起来就是 O(nlogK) 的复杂度,换个角度来看,如果 K 远小于 n 的话, O(nlogK) 其实就接近于 O(n) 了,甚至会更快,因此也是十分高效的。

最后,对于 Java,我们可以直接使用优先队列 PriorityQueue 来实现一个小顶堆,这里给个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static List<Integer> solutionByHeap(int[] input, int k) {
List<Integer> list = new ArrayList<>();
if (k > input.length || k == 0) {
return list;
}
Queue<Integer> queue = new PriorityQueue<>();
for (int num : input) {
if (queue.size() < k) {
queue.add(num);
} else if (queue.peek() < num) {
queue.poll();
queue.add(num);
}
}
while (k-- > 0) {
list.add(queue.poll());
}
return list;
}

Java中Lambda表达式解析

在大部分开发者看来,Lambda表达式只是一种语法糖,简化了书写匿名内部类的写法。实际上Lambda表达式并不仅仅是匿名内部类的语法糖,JVM内部是通过invokedynamic指令来实现Lambda表达式的,与内部类的实现有很大的差异。

本文主要讲解以下知识点:

一、函数式接口

二、Lambda表达式与匿名内部类

三、Lambda实现原理

一、函数式接口

众所周知Javascript具有一个强大的特性:闭包。Java中最接近闭包概念的东西就是lambda表达式了,而Lambda为Java添加了缺失函数式编程的特点。所以什么是函数是接口呢?

函数式接口需满足以下两个条件:

  1. 它是接口
  2. 这个接口有且仅有一个抽象方法

例如我们常用的:Runnable、View.OnClickListener、Comparable等都是函数式接口,因为它们都只有一个方法,而且都是抽象的。虽然只有一个抽象方法,是不是就意味着只能有一个方法呢?实际并不是,虽然有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

嗯?!Java接口中难道还可以定义非抽象方法么?平时我们的接口大概长这样:

public interface IdiomSubmitListener {
    void verifyResult(String result);
    void onSuceess();
}

那接口的非抽象方法是啥?原来在JDK 1.8 对于接口而言具有以下新特性:
接口可以定义非抽象方法,但必须使用default或者staic关键字来修饰
具体细节点可以参考 JAVA 8新特性 允许接口定义非抽象方法 快速入门案例

如果一个接口符合函数式接口的定义,那么我们就可以在该接口上面声明FunctionalInterface注解,用来表示该接口是一个函数式接口,并按照函数式接口的规范在编译的时候对该接口进行检查。

当然如果某个接口只有一个抽象方法,但我们并没有给该接口声明FunctionalInterface注解,那么编译器依旧会将该接口看做是函数式接口。

那Lambda表达式跟函数式接口又有什么关联呢?
在JDK 1.8中,Lambda表达式是对象,而不是函数,它们必须依附于一类特别的对象类型–函数式接口。

因此可以说 在JDK 1.8中,Lambda表达式就是一个函数式接口的实例。
所以如果一个实例是函数式接口的实例,那么该对象就可以用Lambda表达式来表示

二、Lambda表达式与匿名内部类

我们知道代码IDE如果是在JDK1.8的环境下,使用匿名内部类作为一个参数传入到方法中,编译器会提示我们:Anonymous new Runnable() can be replaced with lambda,匿名内部类XXX可以替换为lambda表达式。

如下所示,匿名内部类 Runnable是一个函数式接口的实例,所以我们可以用lambda表达式来将之替换,从而将代码变得更加简洁。

在这里插入图片描述
那么我们是否就认为:Lambda表达式只是为匿名内部类中提供的一种语法糖,他们有什么区别呢?底层原理是完全一样的呢?

他们主要区别如下:

1、关键字this。匿名内部类的this指向匿名类,而Lambda表达式的this指向被Lambda包围的外部类

2、编译方式。Java编译器将Lambda表达式编译成类的私有方法,使用Java7的invokedynamic字节码动态绑定这个方法。而匿名内部类将编译成外部类$数字编号的新类。这也造成第1点关键字this指向不同地方的原因。

三、Lambda实现原理

我们知道如果使用匿名内部类,编译期间会生成一个外部类$数字编号的类,如图所示:

在这里插入图片描述

而如果使用Lambda表达式进行编译后并没有生成新类。
在这里插入图片描述

我们对Lambda表达式生成的class文件使用:javap -p -v Test.class 进行反编译生成如下内容,为便于观察,删除了一些无用内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class wang.julis.jwbase.basecompact.Test

Constant pool:
#1 = Methodref #9.#18 // java/lang/Object."<init>":()V
{
public wang.julis.jwbase.basecompact.Test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 12: 0

private void testLambda();
descriptor: ()V
flags: (0x0002) ACC_PRIVATE
Code:
stack=3, locals=1, args_size=1
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: pop
13: return
LineNumberTable:
line 14: 0
line 18: 13

private static void lambda$testLambda$0();
descriptor: ()V
flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String lambda
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 15: 0
line 16: 8
}
SourceFile: "Test.java"
InnerClasses:
public static final #50= #49 of #53; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #21 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#22 ()V
#23 REF_invokeStatic wang/julis/jwbase/basecompact/Test.lambda$testLambda$0:()V
#22 ()V

从反编译的结果我们可以看到:

1、编译期间自动生成私有静态类lambda$testLambda$0而这里面就就是lambda的具体实现逻辑

2、使用invokedynamic去执行lambda表达式 关于invokedynamic命令具体细节可以参考: 08 | JVM是怎么实现invokedynamic的?(上)

3、lambda表达式编译后并没有生成外部类$数字编号的类

总结:

1、函数式接口:有且仅有一个抽象方法,可以用非抽象方法1.8后支持

2、匿名内部类的this指向匿名类,而Lambda表达式的this指向被Lambda包围的外部类

3、lambda表达式编译后不会生成外部类$数字编号的类

4、Java编译器将Lambda表达式编译成类的私有方法,使用Java7的invokedynamic字节码动态绑定这个方法。

参考:
1、《深入探索Android热修复技术原理》2.3.8章节
2、Java8 lambda表达式、函数式接口、方法引用

Android骨架屏效果的实现与原理解析

0、前言

大家在使用淘宝的时候,如下图所示有遇到这样的效果,其会只展示一部分骨架大致图,等数据加载完毕之后再展示真正的页面数据。与菊花图相比起来,这样的实现能更好的提升用户的体验,这种效果称做:Skeleton Screen Loading,中文叫做骨架屏

1、骨架屏的实现方式

在现在主流的骨架屏实现效果中有两种方式:

这些开源库中,自己比较喜欢今天Skeleton这个开源库,总结了有如下一些优缺点:

优点:

  1. 代码方案实现及使用方式简单,通过替换View和Adapter实现效果,使用Builder设计模式来构造。
  2. 代码耦合程度不高。没有复杂的设计模式,使得代码结构清晰明了。
  3. 骨架屏的效果使用相对于较灵活,可以对整个布局实现骨架屏效果,也可以对单一View实现骨架屏效果。

缺点:

  1. 需要对每个骨架屏效果单独写一套xml布局。
  2. 使用的removeView和addView对 原有布局的view进行替换,存在一定的风险性
  3. 必须清晰的知道所bind的View类型,存在一定的类型转化问题。
  4. 依赖了shimmerlayout第三方库

2、Skeleton解读

一、Skeleton的使用方式

展示骨架屏效果:

View rootView = findViewById(R.id.rootView);
skeletonScreen = Skeleton.bind(rootView)
           .load(R.layout.activity_view_skeleton)//骨架屏UI
           .duration(1000)//动画时间,以毫秒为单位
           .shimmer(true)//是否开启动画
           .color(R.color.shimmer_color)//shimmer的颜色
           .angle(30)//shimmer的倾斜角度
           .show();

关闭骨架屏效果并展示原有View:

skeletonScreen.hide()

流程:

**1. 选择需要替换的目标view

  1. 将骨架效果xml与目标view进行绑定
  2. 添加一些效果属性,比如:动画时间、是否开启展示动画、动画颜色等
  3. 在合适的实际关闭骨架屏效果**

二、Skeleton源码实现

Skeleton提供两个绑定方法,分别绑定普通View与RecyclerView,分别返回对应的Builder

1
2
3
4
5
6
7
8
public class Skeleton {
public static RecyclerViewSkeletonScreen.Builder bind(RecyclerView recyclerView) {
return new RecyclerViewSkeletonScreen.Builder(recyclerView);
}
public static ViewSkeletonScreen.Builder bind(View view) {
return new ViewSkeletonScreen.Builder(view);
}
}

我们首先来看看如何实现与普通View绑定,构造方法中传入目标View,并对shimmer动画效果设置默认的颜色,在Builder里面我们可以看到各种相关参数的设定。

1
2
3
4
public Builder(View view) {
this.mView = view;
this.mShimmerColor = ContextCompat.getColor(mView.getContext(), R.color.shimmer_color);
}

接下来再到show的步骤,主要实现还是由ViewSkeletonScreen来实现

1
2
3
4
5
public ViewSkeletonScreen show() {
ViewSkeletonScreen skeletonScreen = new ViewSkeletonScreen(this);
skeletonScreen.show();
return skeletonScreen;
}

其中ViewSkeletonScreen与绑定的RecyclerViewSkeletonScreen都实现了SkeletonScreen接口,SkeletonScreen有两个接口方法分别是

void show();
void hide();

对于ViewSkeletonScreen.show()进入源码,这里出现一个比较重要的类ViewReplacer,等下再进行解析,通过show的源码清楚的知道逻辑:
1、生成骨架效果View
2、利用生成的View替换目标View。

其中生成骨架效果View阶段主要还是通过LayoutInflater去加载传入mSkeletonResID

1
2
3
4
5
6
7
@Override
public void show() {
View skeletonLoadingView = generateSkeletonLoadingView();
if (skeletonLoadingView != null) {
mViewReplacer.replace(skeletonLoadingView);
}
}

接下来主要讲解ViewReplacer类,其构造方法传入目标View

1
2
3
4
5
6
public ViewReplacer(View sourceView) {
mSourceView = sourceView;
mSourceViewLayoutParams = mSourceView.getLayoutParams();
mCurrentView = mSourceView;
mSourceViewId = mSourceView.getId();
}

其比较重要的方法有两个:replace()restore() 这两个方法分别为SkeletonScreen 的show()和hide()的最终实现,首先看replace()方法,有两个方法重载,分别传入targetViewResID或者targetView,最终还是会走到replace(View targetView)中。
其主要逻辑为:

**1. 判断所替换的View和骨架屏效果View是否为同一个View

  1. remove掉在父布局中的目标View
  2. 将骨架屏效果View添加到目标View的父布局中**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void replace(int targetViewResID) {
if (mTargetViewResID == targetViewResID) {
return;
}
if (init()) {
mTargetViewResID = targetViewResID;
replace(LayoutInflater.from(mSourceView.getContext()).inflate(mTargetViewResID, mSourceParentView, false));
}
}

public void replace(View targetView) {
if (mCurrentView == targetView) {
return;
}
if (targetView.getParent() != null) {
((ViewGroup) targetView.getParent()).removeView(targetView);
}
if (init()) {
mTargetView = targetView;
mSourceParentView.removeView(mCurrentView);
mTargetView.setId(mSourceViewId);
mSourceParentView.addView(mTargetView, mSourceViewIndexInParent, mSourceViewLayoutParams);
mCurrentView = mTargetView;
}
}

在执行添加到目标View的父布局中,有执行一个init方法,主要做两件事:

**1. 获取目标View的父View

  1. 找到目标View在父View 中的位置索引,为之后添加骨架屏View到父View中做铺垫**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private boolean init() {
if (mSourceParentView == null) {
mSourceParentView = (ViewGroup) mSourceView.getParent();
if (mSourceParentView == null) {
Log.e(TAG, "the source view have not attach to any view");
return false;
}
int count = mSourceParentView.getChildCount();
for (int index = 0; index < count; index++) {
if (mSourceView == mSourceParentView.getChildAt(index)) {
mSourceViewIndexInParent = index;
break;
}
}
}
return true;
}

至此对普通View的骨架屏效果实现流程已经完全梳理完成,那对于RecyclerView呢?其实两者实现逻辑差不多,主要有两个差异:

  1. RecyclerViewSkeletonScreen的Builder中,相比ViewSkeletonScreen多了一个adapter()方法,传入目标RecyclerViewAdapter
  2. 在show的时候对目标RecyclerView的adapter进行替换,使用骨架屏效果的adapter。hide的时候恢复为原先的Adapter

3、总结

  1. Skeleton的原理主要是通过替换目标View和RecyclerView的Adapter
  2. 在Skeleton的使用过程中最需要关心的两个问题是:show()和hide()的时机
  3. 对于整个页面的骨架屏效果实现,个人推荐在布局中添加一个全屏的空View盖在原先内容上
  4. 注意一些异常情况下的hide(),要不然整个页面就“假死”状态了。

参考:
https://juejin.im/post/5c789a4ce51d457c042d3b31

Android 插件化之ClassLoader

0、前言:

插件化要解决的三个核心问题:类加载、资源加载、组件生命周期管理。

在Android插件化中其原理实际是 Java ClassLoader的原理,此博文主要对Android插件化中类加载中的DexClassLoader做总结,便于之后对Android插件化的理解学习。

Android的Dalvik虚拟机和Java虚拟机的运行原理相同都是将对应的java类加载在内存中运行。而Java虚拟机是加载class文件,也可以将一段二进制流通过defineClass方法生产Class进行加载。Dalvik虚拟机加载的dex文件。dex文件是Android对与Class文件做的优化,以便于提高手机的性能。可以想象dex为class文件的一个压缩文件。dex在Android中的加载和class在jvm中的相同都是基于双亲委派模型,都是调用ClassLoader的loadClass方法加载类。

1、DexClassLoader和PathClassLoader区别

Android 也有自己的 ClassLoader,分为 DexClassLoaderPathClassLoader,这两者有什么区别和关联呢?

阅读源码可以看到两者的构造方法分别为:

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

可以发现DexClassLoaderPathClassLoader 多一个参数String optimizedDirectory,那这个参数具体做什么的呢?继续查看源码我们可以知道optimizedDirectory是用来缓存我们需要加载的dex文件的,并创建一个DexFile对象,如果它为null,那么会直接使用dex文件原有的路径来创建DexFile 对象,其具体体现在如下代码区域:

private static DexFile loadDexFile(File file, File optimizedDirectory)
        throws IOException {
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

因此两者区别在于 PathClassLoader 不能直接从 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已经安装过的 apk。而 DexClassLoader 可以加载外部的 apk、jar 或 dex文件,并且会在指定的 outpath 路径存放其 dex 文件。所以在插件化中我们使用DexClassLoader来加载class的,接下来讲解DexClassLoader的用法。

2、DexClassLoader用法

其构造方法为:

DexClassLoader(
    String dexPath, 
       String optimizedDirectory, 
    String librarySearchPath,
     ClassLoader parent)

dexPath:被解压的apk路径,不能为空。
optimizedDirectory:解压后的.dex文件的存储路径,不能为空。这个路径强烈建议使用应用程序的私有路径,不要放到sdcard上,否则代码容易被注入攻击。
libraryPath:os库的存放路径,可以为空,若有os库,必须填写。
parent:父亲加载器,一般为context.getClassLoader(),使用当前上下文的类加载器。

接下来讲解具体使用流程:

1、新建一个名为plugin的project,其中新建一个Bean类,只有一个方法getName()返回一个字符串“My App”,然后对plugin这个工程打包为apk,将apk放在主工程的asser目录中。
在这里插入图片描述
2、构造Classloader

File extractFile = getFileStreamPath("app-debug.apk");
String dexPath = extractFile.getPath();
File fileRelease = getDir("dex", 0);
ClassLoader classLoader = new DexClassLoader(dexPath, fileRelease.getAbsolutePath(), null, getClassLoader());

3、利用构造好的Classloader反射调用插件类中的方法

Class mLoadClassBean;
try {
      mLoadClassBean = classLoader.loadClass("com.example.plugin.Bean");
      Object beanObject = mLoadClassBean.newInstance();
      Method getNameMethod = mLoadClassBean.getMethod("getName");
      getNameMethod.setAccessible(true);
      String name = (String) getNameMethod.invoke(beanObject);
      Log.e("julis", name);
  } catch(Exception e) {
      e.printStackTrace();
  }

成功打印出结果:
在这里插入图片描述

参考:

https://www.jianshu.com/p/4b4f1fa6633c

https://www.jianshu.com/p/53aa2de20cf8

https://cloud.tencent.com/developer/article/1071815

忙碌中求生活-记录23岁生日

此时周六晚七点半,刚吃了一碗自己做的番茄鸡蛋面,将浸泡了很久的银耳原料装入了电饭煲内,静待熟时。回到自己的房间,窗外不断传来来往的车轱辘声,好像在告诉我他们很忙吧。看到书桌上的日历,还沉浸在六月份,我可能也很忙吧,都忘记将它带入七月份。

来杭州一年有余,从一名大三学生暑假实习,到大四学生实习,再到应届毕业生签订转正合同,从而成为一名正式的“社会人儿”。时间过得真的很快吧,一周周一下子就没有了。进入了七月份,明显感觉比之前更忙了,或者说是因为自己身份的转变,导致肩上的责任变得不太一样了吧。因为工作节奏的改变,有时候也变得有些麻木吧,日记有时候总会忘记写,写字这件事感觉也变得有些奢侈起来,日语学习的节奏好像变得慢了起来。但是时间的脚步并不因为你的忙碌而停下来。

我最近在读《时间简史》,讲述了从认知革命到农业革命再到人类文化的融合统一再到科学革命,作者用通俗的话语讲完了整个人类历史,真的很值得阅读。其中有一章节引发了我的思考,作者认为:农业革命是史上最大的骗局。因为在长达250万里里人类都靠狩猎或者采集果实为生,而到大约一万年前全然改变,从日升到日落,人类忙着对植物的培育,一心认为这样就能得到更多的水果、谷物和肉类,使得人类生活能够变得更加容易。然而事实上是人变得越来越辛苦。人类每次决定多做一点事情(比如用锄头来耕地,而不是直接将种子撒在地里面),我们认为这样没错,这样会使我们的收成更好一点,有了更好的收成,就不用更多地去担心荒年的问题了,不用挨饿了。工作努力一点,生活也能过得好一点。不过这都是理想的状态。

人们确实工作得更努力也更辛苦,但没想到大家的孩子也更多了,人口慢慢地增加了,所得到的食物也就变得少了,生存压力也就更大了,资源也开始变得稀缺,而引发各种低问题。可是为什么他们不赶快放弃农耕回到原始的采集社会?原因在于,所有的改变都是必须点滴积累,经过许多代,才能改变社会,等到那个时候,已经没有人记得过去的生活方式和现在有什么不一样了,也没得选了。采用了农耕生活,村落的人口从100人到了110人,难道会有10个人自愿挨饿,好让其他人回到过去的美好时光?但这已经无法回头。于是人类付出了更多,但得到的却变得没有以前那样多。

我们都各自为生活变得轻松而努力,但是事实上我们过得并不轻松。人的欲望永远也满足不了,今天拥有了这个,明天还想拥有更好的。现在,我们随手可以发送一条信息,传到地球另一方,而他立马能够回你。我们确实省下了很多时间和麻烦,但生活真的更轻松了么?我们以为省了时间,然而我们其实是把生活的步调调成了过去的10倍,于是我们整天忙忙碌碌、焦躁不安。

这是原书作者对农业革命是史上最大的骗局的论证吧,我再赞同不过了。每每与我的爸妈打电话,我都会有很大的感触,他们总是在忙碌,他们每天都在拼命的挣钱,可是日子真的好起来了么?生活真的轻松了么?不久前,在家族微信群里面,我妈拍了一张我爸的照片放在群里,我爸双手背后,露出一脸笑意,背景好像是他们住那里的一个普通的建筑吧。想表现出: 你看我和你妈在玩,我们多开心啊。 当我看到这张照片的时候,我心里五谷杂粮。

一方面是 我看到我爸的头发,愈发的白了,白了大多半了。记得上一次有这感触的时候,还是我大二的时候,那次我从学校回家,他到车站来接我,我坐他后面,我发现他的白头已经有很多很多了,那时我差点哭了出来。爸妈真的老了,可我还没有给他们带来好的生活,我曾告诉他们说:等我实习你们就回家吧,不要工作了,我能养活我自己,以后也能养活你们。可是他们并不,他们还是在工作,烈日下,每每打电话跟我说太阳是有多么多大温度是有多么高,我心里都很难受。可是他们总会说一句话:“这都是为了你以后更轻松一点”。此时我却不知道该说些什么。

另一方面是 我看着我妈拍的那张照片,真的很糊,不是我妈不会拍照,也不是她眼神不好,真的是她的手机像素不好。我的爸妈,辛苦了大半辈子,其实也有很多积蓄了,虽然没有大城市里的那样多,但在老家里也算是稍微有一些钱的,可是他们却舍不得给自己多花一分钱,能将就用则用。每次电话,我都给他们讲让他们对自己好一点,我说 你们到底挣钱是为了做什么?给他们讲了很多道理,可是他们也总是会一句:“这都是为了你以后更轻松一点”。而我也只能强忍着,因为我现在还没有足够的能力,心里暗暗发誓:我会让他们过上轻松地日子。

我其实一直都在思考一个问题:人到底活在这个世界上是为了什么?科学家无法解释,这是一个哲学问题,没有人知道正确的答案。我们只是沧海中的一粟,如果掀不起波浪,那么就好好感受海的浩瀚吧。忙忙碌碌,短视频以及直播的崛起,可能真的是因为都市生活节奏变得太快,于是都在夹缝中去寻找那一丝丝快感,其实可以做的很多吧,摘自网上“试着每天自己为自己做美味的饭菜,试着经常联络一下家人好友,试着拾起丢下很久的小说,试着用心养一颗植物,试着在空气清新的清晨去跑步,试着约一下自己暗恋已久的女孩…”。看看足球比赛,看看电影,多出去走走,再忙也不要忘记生活吧。

快九点了,删删写写,也不道该放一些什么在日志上,那就这样吧。
祝自己二十三岁生日快乐

记录两张此时自己二十三岁的照片,没出门没刮胡子没收拾,一张沧桑一张微笑,生活亦如此吧。

xxx.jpg
xxx.jpg 隐私保护

—-二零一九年六月十一

单例模式的设计

我们都知道单例模式很简单,大概是这样:

1
2
3
4
5
6
7
8
9
10
//单线程单例模式实现
public class Singleton {
private static Singleton instance=null;
public static Singleton getInstance() {
if(null==instance){
instance = new Singleton();
}
return instance;
}
}

但是呢,在多线程条件下getInstance()并不是一个原子操作。由于代码没有使用任何同步机制,因此该线程可能会出现线程交错的情形:在instance还是null的时候,如果两个线程同时执行到 if(null==instance)那么会创建两个实例,从而违背了初衷。于是通过简单加锁来解决这种问题:

1
2
3
4
5
6
7
8
9
10
11
12
//简单加锁实现单例模式
public class Singleton {
private static Singleton instance=null;
public static Singleton getInstance() {
synchronized (Singleton.class){//加入synchronized同步
if(null==instance){
instance = new Singleton();
}
return instance;
}
}
}

这种方式实现单例模式固然安全,但意味着每次调用 getInstance()都会申请锁,为了避免开销,我们想到了另一种办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//基于双重检查锁定的错误单例模式实现
public class Singleton {
private static Singleton instance=null;
public static Singleton getInstance() {
if(null==instance){//先检查是否为null,再执行之上的代码
synchronized (Singleton.class){
if(null==instance){
instance = new Singleton();
}
}
}
return instance;
}
}

通过这种方法,虽然第一次检查对变量instance的访问没有加锁从而使竞态仍然可能存在,它似乎避免了锁的开销又保障了线程的安全。然后对 instance = new Singleton();进行伪代码独立子操作:

1
2
3
obj=allocate(Singleton.class);//1、分配对象所需的存储空间
invokeConstructor(obj);//2、初始化obj的引用对象
instance=obj;//3、将对象引用写入共享变量

由于重排序的规则,临界区内的操作可以再临界区内重排序,因此JIT编译器可能将上述子操作重排序为:1->3->2,即在初始化对象之前将对象引用写入实例变量instace。由于锁对有序性的保障是有条件的,而操作1读取intance变量的时候并没有加锁,因此重排序是对1操作是有影响的:该线程可能看到一个未初始化(或者为初始化完毕)的实例,即intance不为null。于是该线程直接就直接返回这个instance变量所引用的实例,而实例可能是未初始化完毕的,这就是可能导致程序出错。明白问题的原因之后,解决方法也不难想到了:只需将instance变量加入volatile修饰则可。于是代码变成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//基于双重检查锁定的正确单例模式实现
public class Singleton {
private static volatile Singleton instance=null;//加入volatile修饰
public static Singleton getInstance() {
if(null==instance){
synchronized (Singleton.class){
if(null==instance){
instance = new Singleton();
}
}
}
return instance;
}
}

到此为止,才正确实现安全的“单例模式”。

参考:《黄文海-Java多线程编程实战指南(核心篇)》

Android监听截屏事件之媒体读取的探索

最近做了一个需求:监听用户截屏,然后生成相关海报。
参考了Android 截屏事件监听的文章,大致思路是:a

1、利用ContentObserver用来监听指定Uri的所有资源变化,当媒体库中有相关图片新增的时候,则发送相关的通知。

2、得到回调的Uri后,借助ContentResolver在媒体数据库中查询最后一条数据

3、对数据做一些过滤。比如短时间重复截屏的情况以及其他App也插入了媒体文件等情况做处理。

不过有一些适配性的问题:

1、截屏后读取文件数据库后获取到件的绝对路径后,利用“screenshot”等关键字判断是否是截屏图片,并不能适配所有手机截屏的命名规则,以及其他应用同时间产生带有“screenshot”等关键词的文件也会有问题。

2、在某些型号手机中(现遇到Vivo)从数据库中读取的文件并不是获取到的最新的截屏文件,而且其他目录的文件,这里就有些难以理解了,所以今天取探究一下媒体数据库的读取。

其中ContentObserver如下代码所示:

` /**
 * 媒体内容观察者(观察媒体数据库的改变)
 */
private class MediaContentObserver extends ContentObserver {
    private Uri mContentUri;
    public MediaContentObserver(Uri contentUri, Handler handler) {
        super(handler);
        mContentUri = contentUri;
    }
    [@Override](https://my.oschina.net/u/1162528)
    public void onChange(boolean selfChange) {
        super.onChange(selfChange);
        handleMediaContentChange(mContentUri);
    }
}`

其中获取最后一次更新的媒体文件时的代码(为便于查看 删除了判空处理代码):

 /**
 * 处理媒体数据库的内容改变
 */
private void handleMediaContentChange(Uri contentUri) {
    Cursor cursor = null;
    /** 读取媒体数据库时需要读取的列 */
    private static final String[] MEDIA_PROJECTIONS =  {
        MediaStore.Images.ImageColumns.DATA,
        MediaStore.Images.ImageColumns.DATE_TAKEN };
    try {
        // 数据改变时查询数据库中最后加入的一条数据
        cursor = mContext.getContentResolver().query(
                contentUri,
                 MEDIA_PROJECTIONS,
                null,
                null,
                MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1"
        );
        // 获取各列的索引
        int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
        int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);

        // 获取行数据
        String data = cursor.getString(dataIndex);
        long dateTaken = cursor.getLong(dateTakenIndex);

        // 处理获取到的第一行数据
        handleMediaRowData(data, dateTaken);
}

这次的目的主要探究的是从数据库获取相关信息的过程

1、Android 的多媒体如何存储?

Android的多媒体文件主要存储在 /data/data/com.android.providers.media/databases 目录下,该目录下有连个db文件:

内部存储数据库文件:internal.db

存储卡数据库:external-XXXX.db

媒体文件的操作主要是围绕着这两个数据库来进行,这两个数据库的结构是一样的。

这两个数据库包含这些表:
album_art 、audio 、search 、album_info 、audio_genres、 searchhelpertitle、albums、 audio_genres_map、 thumbnails、
android_metadata、 audio_meta、 video、artist_info 、audio_playlists 、videothumbnails、artists 、audio_playlists_map、
artists_albums_map 、images

2、表的结构
对于Images表:主要存储images信息。表结构如下:

CREATE TABLE images (
_id INTEGER PRIMARY KEY, 
_data TEXT,
_size INTEGER,
_display_name TEXT,
mime_type TEXT,
title TEXT, 
date_added INTEGER, 
date_modified INTEGER,
description TEXT,
picasa_id TEXT,
isprivate INTEGER,
latitude DOUBLE, 
longitude DOUBLE, 
datetaken INTEGER, 
orientation INTEGER, 
mini_thumb_magic INTEGER, 
bucket_id TEXT, 
bucket_display_name TEXT );

`

各字段所表示意思,如图所示:

图片来自:Android MediaProvider数据库模式说明

所以在截屏监听数据的时候所读取的数据库返回值,分别为:

_data :图片据对路径

datetaken:取子EXIF照片拍摄事件,空的话为文件修改时间

private static final String[] MEDIA_PROJECTIONS =  {
      MediaStore.Images.ImageColumns.DATA,
      MediaStore.Images.ImageColumns.DATE_TAKEN };

在查询过程中构造的数据库代码为:

public final Cursor query (Uri uri, 
    String[] projection,
    String selection, 
    String[] selectionArgs, 
    String sortOrder)

`
其中对应的构造参数官方解释为:

uri The URI, using the content:// scheme, for the content to retrieve.

projection A list of which columns to return. Passing null will return all columns, which is inefficient.

selection A filter declaring which rows to return, formatted as an SQL WHERE clause (excluding the WHERE itself). Passing null will return all rows for the given URI.

selectionArgs You may include ?s in selection, which will be replaced by the values from selectionArgs, in the order that they appear in the selection. The values will be bound as Strings.

sortOrder How to order the rows, formatted as an SQL ORDER BY clause (excluding the ORDER BY itself). Passing null will use the default sort order, which may be unordered.


所以参数依次为:
所要查找的目标、所要的返回值、条件限制(类似sql中where)、匹配项、排序规则

所以这里的查询就显而易见了:获取最新图片数据库下data和datatoken列的数据

cursor = mContext.getContentResolver().query(
              contentUri,
              MEDIA_PROJECTIONS,
              null,
              null,
              MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1"
      );

然而…并不能解释vivo手机为什么查找出来不是最新截图的图片的问题

Android在子线程中创建Handler为什么会抛出异常?

复习一下消息机制,如下代码:

new Thread() {
        Handler handler = null;
        [@Override](https://my.oschina.net/u/1162528)
        public void run() {
            handler = new Handler();
        }
    }.start();

如果执行会抛出异常:

Can't create handler inside thread Thread.currentThread() that has not called Looper.prepare()

这是为什么呢?

我们进入Handler的构造方法

    public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread " + Thread.currentThread()
                    + " that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

从上面的代码我们很清新的可以知道mLooper 为null,那么就会抛出这样的异常,那么mLooper 为什么会为空呢?这是因为在线程中的Looper还未被创建,所以在Looper.myLooper()中sThreadLocal.get()就会返回null。

我们知道 Handler的作用是处理消息,将消息传递给MessageQueue,而MessageQueue存在于Looper中,如果没有Looper那么就没有MessageQueue,所以创建Handler时,Looper不能够为空。

所以以上代码可以进行一个修改:

private void test() {
    new Thread() {
        Handler handler = null;
        [@Override](https://my.oschina.net/u/1162528)
        public void run() {
            Looper.prepare();
            handler = new Handler();
            Looper.loop();
        }
    }.start();
}

其中 Looper.prepare();为当前线程创建Looper并绑定在ThreadLocal中
Looper.loop();执行消息循环,这样子 Handler就能够正常工作了。

写在我即将毕业旅行前

忙碌的一天,又要到下班的时候了,一天好似很忙,其实回过头来仔细想想,一天好像并没有做太多的事情,然而确实是这样子度过一天了,就像我们的青春岁月,看样子并没有经历什么,但它就这样匆匆过去了,亦忧伤,亦憧憬。

还有两周就要回校参加毕业典礼了,向公司请了十天假,算上端午以及周末,大概有半个多月属于自己的日子吧,也算是大学最后一点还是以学生身份存在的时间了。早在几个月前我都在筹划这次出去游玩了,因为我想出去走走,想出去看看。纵观整个大学生涯,自己也算是去了不少地方了,屋子里车票、机票已经收纳了一沓了。看着每一张车票的起点与终点以及行程时间,还能想起那时候发生过的事,或许有些事还在记忆里面,或许可能存在我的日记里面,或许我什么也记不得了。

2015年8月15日 绵阳-上海:距离大学开学还有一个月了,父亲陪我度过了整个高三,待我拿到通知书后,我与父亲来到上海。父亲是一个节俭的人,买了绿皮车,将近两千多公里的路程,三十多个小时的行程,脚没有办法舒展开,时间长了别的难受。当看到那些没有座位的那些人要不停地给过路人让路的时候,我觉得自己也挺幸运的,至少我还有一个属于自己的位置,不需要为别人让路。在这煎熬的行程中,我并没有去抱怨,我只是一直在想,我以后一定要努力,一定不会让我和我父亲再受这样的遭遇。

2015年9月14日 上海-温州:马上就要大学开学了。离开了父母,将一个人去面对新的生活了,路过杭州的时候,车窗外一片开阔的平原,让我看到极具江南特色的景象:鳞次栉比的房屋伴着小河流,来来往往的车辆川流不息,很憧憬那样“小桥流水人家”的生活,这大概是对杭州的第一印象吧。

2016年1月22日 温州-成都:大学寒假第一次回家,运气很好,我和同学抢到了卧铺票,有了一个较舒服的乘车环境。与我通行的还有很多大学川籍同学,因此一路上也变得并不孤独,时而和他们“摆龙门阵”,累了就回床铺上面去休息一下,我记得我当时带了一本《浪潮之巅》。

2017年7月18日 温州-太原:第一次坐飞机,带着许多憧憬出发,但让人失望的是我错过了那一天的飞机,原本安排的好好的行程,一下子把我的计划打乱了。这时候我真的很迷茫,我不知道该去哪里,我也不知道该做什么,整个脑子是一片空白,在街上走走,最后回学校见到了精神支柱。然后去了太原平遥古城,喜欢太原老大爷那种街边悠然下着象棋的生活。

2017年7月10日 嘉兴-福州:又是一次暑假,我去嘉兴看望我爸,然后我爸他们提议去福州找我的大姑爷家玩,这一次我和我的父亲快吵了一架,因为我想让他买一张动车票,可父亲执着的只买绿皮车票,他觉得能节约钱,可是我觉得时间才是最重要的,那时候我没有一点收入,我说服不了他,绿皮车从晚上八点开到第二天早上八点,整个一晚我没有怎么睡,感慨太多,我也告诉我自己要努力。当时也写过一篇随笔:2017年710随笔 于嘉兴-福州列车

2017年9月02日 温州-杭州:这一次算是第一次以异地恋的身份去见了那时喜欢的人,一路忐忑。记得那时候带的是一本《小王子》,但是感觉没有看太懂,好像过了天真的年纪,已经看不太明白小王子的天真了。

2018年8月23日 杭州-太原: 或许真的是和太原有缘分,时隔一年再次去太原,这次我并没有错过,过了一年,变得成熟些了,这时候我已经在参加实习了。这次是代表整个学校唯一一只队伍进电子商务全国总决赛,压力与动力并存吧。

2019年2月14日 德阳-成都:这是最近一次从家出发去杭州在成都中转,在德阳站中因为高铁晚点,我遇到了一个女孩子,或许这是读大学以后认识的第一个非本大学的四川女孩子吧,在她回头的那一下,我感觉我好像心动了一下。或许是缘分,我和她是同一辆车,在德阳到成都只有短短半个小时的行车时间,居然车晚点将近两个小时,于是和她就聊啊,聊啊,从小学说到初中高中,从高中说到大学,再从大学说到实习。原来她也和我一样是大四的学生,原来我和她大学同学是初中高中同学,真的是缘分吧。第一次是多么的希望列车晚点的时间能够再长一点。只可惜,我本将心照明月,奈何明月照沟渠。

……

2019年6月10日 杭州-青岛、烟台、威海、大连? 或许是在被公司同事的鼓励下:现在有时间多出去玩玩吧,实习一天也没有多少钱,等以后正式工作了,有钱也没有时间了,趁现在,多出去走走吧。感谢芳姐姐对我的支助,让我有机会去计划这次旅行。

这一次,我是第一次一个人的旅行,我想在这炎热的夏天里,走出屋子,踏上行程。去坐一次轮渡、我还想再使用学生证享受一次学生特权、然后去看看海、去吹吹风、去看看更多的天空、去认识的人并说:“很高兴认识你”。也算是对整个大学青春岁月画上一个句号吧。
那接下来的一路上又会发生什么故事呢?

最后以王小波的《黄金时代》结尾吧:那一天我二十一岁,在我一生的黄金时代,我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云,后来我才知道,生活就是个缓慢受锤的过程,人一天天老下去,奢望也一天天消逝,最后变得像挨了锤的牛一样。可是我过二十一岁生日时没有预见到这一点。我觉得自己会永远生猛下去,什么也锤不了我。

我还年轻,我还可以到处走走,我还有很多想要去憧憬的、盼望的……

–于2019.06.04 7:24 即将下班回家

95后,毕业半年,你过得好吗?

原帖:https://www.zhihu.com/question/307640916/answer/686748493
95后,毕业半年,你过得好吗?
亲爱的95后,毕业两年,你过得怎么样了?充实或是空虚,甜蜜还是苦涩,热爱生活还是生无可恋,期待写下你的故事。

96年 刚参加完学校的毕业答辩回来,离毕业也不远了吧。从去年暑假开始就在杭州实习了,最开始是做php后端,后来转到了安卓开发。实习了差不多快一年了,就要成“资深实习生”了。说实话我感觉我过得很充实吧,依然热爱生活。实习期间租房+伙食费+路费,如果不干别的什么事,一个月请假天数在两三天的话,每个月的生活是过得还算是比较舒服的,不太愁。实习之后就第一个月问家里要了一下房租和押金,以及驾校的学费。可以说完全独立了吧。拿到公司的offer是10几k,还是蛮期待拿毕业证转正的时候吧:
96年 刚参加完学校的毕业答辩回来,离毕业也不远了吧。从去年暑假开始就在杭州实习了,最开始是做php后端,后来转到了安卓开发。实习了差不多快一年了,就要成“资深实习生”了。说实话我感觉我过得很充实吧,依然热爱生活。实习期间租房+伙食费+路费,如果不干别的什么事,一个月请假天数在两三天的话,每个月的生活是过得还算是比较舒服的,不太愁。实习之后就第一个月问家里要了一下房租和押金,以及驾校的学费。可以说完全独立了吧。拿到公司的offer是10几k,还是蛮期待拿毕业证转正的时候吧。

在这里我好想讲讲近一年的实习生活,过得日子也算是充满酸甜苦辣吧。从刚开始从公司实习开始讲吧:当时的我好像什么都会,php也会、python、java、安卓啥都会一样,经过被阿里面试打击后,可以看此帖子:https://www.zhihu.com/question/268713348/answer/352195054

我稍微收敛了一点了吧,最后进入了杭州一家互联网公司,刚开始是干php的,干了一个多月一点,感觉并没有学到多少东西,做的业务比较多,因为我属于“实习”的状态吧,安排的任务也比较少,所以每天的日子过得也很“悠闲”,每天干完就回家了,大概下午六点多就走了……这前面的php实习期间算是伏笔吧。之后由于刚好公司内部有个安卓实习的位置,好像是没有招到合适的吧,最开始面试的时候说了我什么都会,所以有幸被调到了安卓组里面。我发现好像每天的需求做起来还是那么简单,所以每天依然走得特别早吧,还有点沾沾自喜的样子,然而回到家并没有继续学习相关知识,要不就和女朋友( broken up.)漫步钱塘江边,要么就是回到家里写写字看看其他类型的书,反正过得很舒服吧。

直到有一天我被“伤自尊”了,一度自我感觉良好的我被组内的同事叫去看一个问题,我看了很久也看不出来,因为很多Java基础我都有些快忘了吧,基础不太扎实,我对代码的深度理解也有问题,我只停留在用的阶段,我不懂其原理,我只会使用,我连他们的源码都没有看过。

后来被他鞭策道:你现在确实很厉害,比我当时刚出大学的时候厉害多了,但是我觉得我一点比你做的好,那就是我一直坚持在学习。你每天回去那么早没有学习,你在做什么?你现在对你自己的定位有些问题,现在你虽然觉得这些需求能做,但是你知道这些需求都是最简单的,工作难度都是最低的,我们为什么不把那些高难的任务给你?是因为你现在能力还不够,现在给你简单任务就是想让你多一些时间去学习,让你尽早能够跟上团队的步伐。实话给你说:你很菜!你真的很菜……你师傅可能不太好给你讲所以我才给你讲这些,如果你现在的状态,你永远只能做那些最简单的任务,可能连之后的校招offer都拿不到,就算侥幸8月份能拿到校招offer,也许你之后能侥幸转正,但是你不坚持学习的话迟早会被行业淘汰……

这位前辈的话一直在我脑海里面印象深刻,我是一个不服输的人,在被鞭策后,我真的不服气,我也不认输,当时他叫我周末会去看一看”EventBus”的源码,然后周一向他汇报一下看的结果。周末连续两天都在看其源码,说实话看起来真的很痛苦,因为自己以前都是直接用,不会去关心它内部的逻辑,但是依然坚持看了两天,做了很多笔记,把它内部实现逻辑也搞明白了。忐忑不安地等到了周一,我向他汇报,我给他讲述了内部原理以及源码解读,他给我提了几个问题,我答上了一半多一点,我正沾沾自喜时,他给我说:如果给你这次评价满分一百分的话,我最多给你打20分。

!!!!20分,我的天!我当时一下子整个人就不好了,甚至都想去反驳他了,可是他之后给我讲得东西,让我打消了这个念头,我确实菜。我确实认识到了自己的不足,对源代码的解读真的还不够,Java基础也不太扎实,包括我的师傅也这样认为,我是一个不服输的人,不服气。我向我师傅请教了,大概给自己定了一个短期的学习计划:先把Java基础过一遍,再过一遍Android基础过一把,再开始去理解安卓深度的东西。从此开始,我感觉我开始暴走了。

我每天都背一两本书回去学习,在地铁上有时候位置空我也会拿出来继续学习,偶尔还是会找女朋友去玩,但是当我11点钟回到家,我还是会拿出书来继续学习。然后第二天我会和我的师傅讲我昨天学了什么,然后讲出我的疑问。我的师傅真的是一个很耐心的人,他会给我仔细地讲解,包括平时的问题,如果我向组内请教问:1+1等于几? 他们会告诉我等于2。然而我的师傅会告诉我 加法口诀表,甚至是乘法口诀表。那时候我每天都不曾忘记学习,每天都会去研究,那一段时间我真的压力很大,经常性的失眠,心跳特别快,后面也去看过一次医生说是“心悸”。当然那也是在压力下人做出的一些极端反应,所幸的是我坚持了过来。八月中,HR把我叫了过去,我还以为她又要批判我了,因为最开始被团队的人给鞭策了之后,她也找过我谈话了:你如果还继续现在的状态,可能之后的校招offer都不会发给你。这一次不一样了,她给我讲了我转正后的待遇,什么期权还要公积金啥啥啥的,然后讲了工资多少多少,其实工资是有点超乎我预期的。她讲完之后,我真的笑了,真的开心了,我忍不住笑了起来。她问:开心吗?我一直点头。或许这是对努力之后最好的回报吧!那一刻真的,心里有太多说不出来,打心地的开心。

拿到校招offer我还是坚持学习吧,只是强度没有之前那样强了,但是依然坚持每天要学习。现在我觉得每天每周过得生活都特别的充实吧,我们不是996。公司9点上班,弹性打卡9个小时,也就是说早上9点钟打卡,下午6点钟就可以走了,但是大部分人并不是6点钟走。现在自己每天早上都会比较期盼去公司,因为每次做需求,我都并不把他当做我的“工作”在做,而我认为那是我的兴趣爱好。我喜欢敲代码的感觉,我喜欢那种自己去设计思路想法,最后打磨出来产品的过程。而我们做出的成果会是被几百万用户所使用,肩上也有一种责任感吧。然后也带来了成就感,比如公司的宣传片或者展示公告大牌上面印着app的应用界面图,会看到里面有一部分是我做得,我是多么的开心呀。一次室友的朋友过来了,我们聊着天,后来聊到它妈妈也在用我们做得app,它给我说它妈妈怎么怎么这个App,我说啊,这里就是我做的呀。哇!厉害咯。我对工作保持乐观态度,因为我可以向厉害的前辈们请教问题,向他们学习,我每天都感觉自己有收获,所以在公司让我感觉过得很开心,没有太大的压力,每天都在进步。

当然回到家了,我也有该做的事,我也有我的兴趣,大概每天7.30左右下班回家,回到家快8.30了。我也有自己追求,我在学日语,打算能在明年去一趟日本,感受一下不同的文化。日语学累了,我会继续学习相关专业的书籍,或者写写字,我比较喜欢写字,虽然没有很飘逸炫酷,但是我觉得写完之后看起来特别的舒服,这也算是一种成就感把,大概学到10.30收拾洗漱,大概11点左右上床了,我会继续看一些相关的书籍,比如最近在看《红楼梦》,厚厚的一本,渐渐地书签也跑到了中间的位置吧。

至于周末,我可能比较放松吧,周末我会学着去做做菜,不再想吃外卖了,真的难吃且贵。

有时候也会去绿城主场看看绿城踢球吧,虽然心里支持的四川队,但是远在他乡没有办法了。

最后附上自己毕业答辩后拍的照片,算是对自己充实的大学生活的怀念吧。

总之,我觉得自己每天都过得很充实吧,很热爱自己的生活。

愿每一个你们的生活都过得幸福。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×