View的Measure&和Layout

在自定义view的时候比较重要的就是三个方法。

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec)
  • onLayout(boolean changed, int left, int top, int right, int bottom)
  • onDraw(Canvas canvas)

onMeasure

onMeasure方法是在view的measure方法中调用的。主要是控制控件的尺寸。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

......

if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

......

继续看onMeasure的实现,一直到最后一系列的调用就是给mMeasuredWidth和mMeasuredHeight赋值。

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

1
2
3
4
5
6
7
8
9
10
11
12
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight){
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;

measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

插播一段:因为在调用measure方法的时候传入的两个参数widthMeasureSpec和heightMeasureSpec都是通过MeasureSpec构造出来的,因此这个类可以简单了解下。

这个measureSpec是一个32位int类型的数据,前两位表示mode,后30位表示size。因为他的掩码是11后面加30个零。例如getMode,掩码和measureSpec进行与运算,得到的一定是measureSpec的前两位,即mode值。也就是说根据mode值来设置返回不同的size。

1
2
3
4
5
6
7
8
9
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}

public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}

因此如果复写onMeasure方法,能改动的变量也只有重新设置mMeasuredHeight和mMeasuredWidth。也就是调用setMeasuredDimension这个方法,将我们需要的height和width传进去即可。

onLayout

onLayout方法一样也是在layout()方法中调用的,作用是控制子控件的布局。因此view的onLayout方法是空的,因为只有ViewGroup才有子控件。不过ViewGroup的onLayout方法也只是个抽象类,实现的地方还是在ViewGroup的子类中。

1
2
@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

例如LinearLayout的onLayout方法,根据orientation进行不同的布局排版。

1
2
3
4
5
6
7
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}

之前碰到一个坑就是在vertical的LinearLayout情况下gravity的top和bottom不生效,当时理解为因为纵向线形布局的高度都是根据子view来决定的,这边看下源码印象更深刻,以vertical为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for (int i = 0; i < count; i++) {

......

switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;

case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;

case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}

......

}

在layoutVertical中对子view的排版只有RIGHT和LEFT两个属性,并没有对TOP和BOTTOM处理。

其余还有些padding和margin等balabala各种属性要考虑到其中就不赘述了。

自定义Layout属性

在学习写自定义Layout的时候,顺便练习了下自定义属性,主要以下几步

  • 新建attrs.xml,declare-styleable的name是自定义的布局名称,attr的name和format都自己定义

    1
    2
    3
    4
    5
    6
    7
    <resources>

    <declare-styleable name="MyLayout">
    <attr name="isVertical" format="boolean" />
    </declare-styleable>

    </resources>
  • 在布局文件中设置该属性

    1
    2
    3
    4
    5
    6
    <MyLayout 
    .......
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:isVertical="true"
    tools:context=".MainActivity">
  • 在该类初始化的时候获取属性值

    1
    2
    3
    var isVertical: Boolean = false
    var t = context?.obtainStyledAttributes(attrs, R.styleable.MyLayout)
    isVertical = t!!.getBoolean(R.styleable.MyLayout_isVertical, false)

Constructor&Destructor

构造函数

构造函数的定义就不写了,直接看下例子,这里的构造函数中初始化了number这个变量。

插播一段:这里使用了ifndef和define来防止多重包含。即使多个文件include了这个头文件也只会包含一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef Stock00_h
#define Stock00_h

class Stock {
private:
int number_;
public:
void display();
void set(int);
Stock(int);
};

void Stock::display()
{
std::cout << number_ << std::endl;
}
Stock::Stock(int number)
{
number_ = number;
}
#endif /* Stock00_h */

如何使用:

1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
Stock stock1(100); //隐式
Stock stock2 = Stock(100); //显式
Stock stock3 {100}; //类似初始化列表的情况
stock1.display();
return 0;
}

Warning:

  • 如果该类定义了构造函数,那么当实例化该类的时候则必须调用它的构造方法
  • 构造函数的参数名不能与类成员相同。一种做法是在数据成员前面加上m_前缀,还有一种做法是在成员名称后面加后缀_。

析构函数

析构函数是和构造函数对应的,一个是创建过程中调用,析构函数则是在对象销毁时调用的,不过大部分情况都是由系统自己调用,很少有显式调用的情况。析构函数不包含任何参数。

  • 静态存储类对象:程序结束时调用。
  • 自动存储类对象:函数结束时调用。
  • new创建的对象:调用delete来释放内存时调用。

const成员函数

如果定义了一个const的类,调用该类的非const方法则会报错,因为不能保证调用的对象不被修改:

1
2
3
4
5
6
7
const Stock stock1 {100};
stock1.display();

void Stock::display()
{
std::cout << number_ << std::endl;
}

解决方式在函数最后加上const关键字:

1
2
3
4
5
6
void display() const;

void Stock::display() const
{
std::cout << number_ << std::endl;
}

this指针

之前在尝试写成员变量的get/set方法时,发现在c++中this并不代表一个类,而是一个类的指针。并且可以表示隐式的访问,

1
2
3
4
5
6
7
8
9
10
stock1.topval(stock2);

Stock & Stock::topval(Stock &s)
{
if (this->number_ > s.number_) {
return *this;
} else {
return s;
}
}

可以看到在这个函数中,this表示stock1。

基于C++ Primer plus第十章

Matrix&Camera

最近在学习自定义view,先将学到了东西记录一下。

Matrix

Matrix就是矩阵的意思,具体的数学原理已经还给老师了,因此只是记录使用方式。
由于矩阵并不满足交换律,即:
AB ≠ BA
因此矩阵的方法都是成对的,用pre或者post作为前缀表示前乘或者后乘,这个前和后的意思就是将初始矩阵放在前还是后的意思。
使用方法:

1
2
3
4
5
Matrix matrix = new Matrix();
matrix.preRotate(45);
matrix.postTranslate(200, 0);
canvas.setMatrix(matrix);
canvas.drawBitmap(bitmap, 100, 100, paint);

Matrix的强大之处在于可以完成一切的形变,而canvas只支持一部分的形变。并且Matrix可以根据前乘和后乘(绝对不是根据简单的pre和post)来决定形变的顺序。

Camera

Camera顾名思义就是照相机,这是一个在三维坐标轴上处于z轴的一个相机,他的工作原理就是将三维模型投影在canvas,也就是我们实际看到的图形。插播一段它的坐标系。

View的坐标系:以屏幕左上方为原点。x轴左负右正,y轴上负下正。

Camera的坐标系:以屏幕左上方为原点,x轴左负右正,y轴上正下负,z轴内正外负。

如果我们直接调用camera的rotate方法的话,假设让它绕x轴旋转30度,由于camera是始终处于z轴上的,因此如果这时候投影的话,那么投影出来的图形并不是我们想要的效果,而是不对称且右边拉长的结果。解决办法就是在旋转之前先将图片移到原点。之后再移回来,不过代码中canvas的移动要反过来写,因为它是反向执行的。

1
2
3
4
camera.rotateX(30);
canvas.translate(centerX, centerY);
camera.applyToCanvas(canvas);
canvas.translate(-centerX, -centerY);

其余的一些调用api的就不写了,等用的时候一查就有了。

LinearLayout设置gravity不生效

最近在学习Transition动画的时候,有个动画是将button从左上移到右下,代码其实很简单。

1
2
3
4
TransitionManager.beginDelayedTransition(transitionContainer, ChangeBounds().setPathMotion(ArcMotion()).setDuration(1000))
var params = button_view.layoutParams as FrameLayout.LayoutParams
params.gravity = Gravity.BOTTOM or Gravity.RIGHT
button_view.layoutParams = params

这边使用的是FrameLayout,最开始使用LinearLayout的时候修改gravity始终无效。后来意识到因为LinearLayout是或横向或纵向的布局,因此如果当它处于横向的情况时,只有top和bottom是有效的,因为横向的布局都依赖于子控件的个数。反之如果是纵向布局,只有right和left是有效的。

区分tablet和mobile

最近项目上UE有个需求是增加一个按钮切换横竖屏,当然很容易想到使用

1
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

传入的值一般有如下,按字面意思理解即可:

1
2
3
4
SCREEN_ORIENTATION_PORTRAIT
SCREEN_ORIENTATION_LANDSCAPE
SCREEN_ORIENTATION_REVERSE_PORTRAIT
SCREEN_ORIENTATION_LANDSCAPE

但是这样设置有个问题,那就是手动设置了横竖屏之后系统的AutoRotate失效了。有一种workaround就是手动去处理转屏事件。加一个内部类继承OrientationEventListener。只要底层重力传感器的值发生变化就会回调这个类中的onOrientationChanged方法。之后根据上报的orientation值和一些flag进行横竖屏处理。

1
2
3
4
5
6
7
8
9
10
11
private class IncallOrientationListener extends OrientationEventListener {
public IncallOrientationListener(Context context) {
super(context);
}

@Override
public void onOrientationChanged(int orientation) {
//do anything what we want
orientationUtils.onOrientationChanged(getApplicationContext(), orientation);
}
}

然而在上一次GT的时候发现平板下横竖屏全反了,原因是平板是横着的时候oreintation为0,这时候如果设置SCREEN_ORIENTATION_PORTRAIT就会有问题,当转屏之后又设置了SCREEN_ORIENTATION_LANDSCAPE。也就是说对于横着的平板和手机相比应该走完全相反的逻辑。那么就需要区分平板和手机了,官方似乎没有相关文档,但是根据以前我处理分屏下布局的bug,系统应该是根据长宽的大小比较来决定是使用port文件夹还是land文件夹下的布局。这个在stackoverflow上的workaround

1
2
3
public static boolean isTablet(Context context) {
return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE;
}

其实我个人认为通过长宽来判断应该设置横屏还是竖屏更合理一点。因为有的平板就是放大版的手机,这种平板其实就应该走手机的逻辑。用WindowManagerService来获取屏幕的宽高。

1
2
3
4
5
6
7
WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics metrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metrics);
int heightPixels = metrics.heightPixels;
Log.d(TAG, "heightPixels :" + heightPixels);
int widthPixels = metrics.widthPixels;
Log.d(TAG, "widthPixels :" + widthPixels);

1
2
3
Result:
heightPixels :1440 widthPixels :2560 //横屏
heightPixels :2560 widthPixels :1440 //竖屏

inline&reference

C++内联函数

传统函数:当我们调用函数的时候,其实是将该指令的内存地址保存下来,并将函数参数复制到堆栈。当函数运行完之后再跳回去。这样来回跳跃会加大开销。因此还有另一种函数,叫做内联函数。

内联函数:简单来说就是将函数的代码替换函数调用,这样就不用跳到其他地方执行,但是如果有10个地方调用了这个函数,那么就会有10个函数代码的拷贝。

  • 在函数声明前加上inline
  • 在函数定义前加上inline

Warning: 如果内联函数过大或者调用了自己(不支持递归),那么编译器不会将其作为内联函数。并不是所有的编译器启用了这个特性。

内联与宏

inline关键字是C++新增的特性。C语言则使用#define。但C语言中时、是通过文本替换而不是传递参数。

1
2
3
#define SQUARE(X) X*X
a = SQUARE(5 + 1);
// a = 5 + 1 * 5 + 1 not a = 6 * 6

引用变量

基本概念:引用是已定义的变量的别名。使用&符号表示引用。

1
2
int i;
int & another = i;//alias for i

int&表示指向int的引用,上述表示i和another指向相同的值和内存单元。如果改变了another,那么i也会改变。
引用其实就是另一个变量的完美复制品,除了名称不一样其他一模一样,表现在指针中则是:

1
int & i = rats;

等同于

1
int * const pr = &rats;

引用和指针最大的区别就是引用在声明的时候必须初始化。

引用用作函数参数

也就是将某种类型的引用作为参数传入函数。在代码方面,如果使用value传递的话会有一个变量的copy,而引用的则不会有两个变量,只会有两个名称。之后在函数中不管怎么处理都只是在操作这个copy而没有改变他的原始值。

如果我们定义了一个函数的参数是引用,那么则必须传一个变量进去,传入表达式则是不规范的。例如:

1
2
void square(int & i);
square(x + 3);// INVALID

当然有的编译器不会报错,只是弹一个warning出来,那是因为如果实参和引用参数不匹配的话就会生成临时变量。并且只有当参数是const引用时才会这么做。
如果参数是const类型,那么在以下情况会生成临时变量

  • 实参的类型正确,但不是左值。
  • 实参的类型不正确,但可以转换为正确的类型。

左值其实就是指处于等号左边的值,那么什么样的值可以放等号左边呢,自然是变量,引用等等。像字面常量,表达式则都属于非左值。如下例:

1
2
3
4
double refcube(const double & ra)
{
return ra * ra * ra;
}

1
2
3
4
5
double side = 3.0;
long edge = 5L
double c1 = refcube(edge);//临时变量,因为edge类型与参数不匹配
double c2 = refcube(7.0);//临时变量,非左值,字面常量
double c3 = refcube(side + 3.0);//临时变量,非左值,表达式

何时使用引用参数

  • 函数中的数据对象需要被修改
  • 数据较大时需要提高程序的运行速度。

第二个原因有点类似于指针,那么什么时候使用指针,什么时候使用引用呢:

  • 如果数据对象很小,则按值传递。
  • 如果数据对象是数组,那么使用指针,同时也是唯一的选择。
  • 如果是较大的数据结构则使用指针或引用。
  • 如果是类对象,则使用const引用。

默认参数

也很简单,就是在函数定义的时候给参数一个默认值。

1
char * open(int n = 1)

但是有个规矩那就是必须从右向左添加默认值。

函数重载

其实就是同一个函数,但是参数类型或者数量不同叫做函数重载。在java中是必须严格按照参数类型或者数量来调用函数的,但是在c++中如果调用的函数不与任何函数原型匹配,那么就会尝试使用标准类型转换强制进行匹配。

1
2
3
4
5
6
7
8
9
10
void test(int i = 1);

int main(int argc, const char * argv[]) {
test(2.2);
return 0;
}
void test (int i)
{
cout << i << endl;
}

1
Result:2

而关于const也可以用于重载函数,不过只有在修饰的参数是引用或者指针的时候才可以。

函数模版

简单来说就是使用泛型来定义函数,编译器会按照模版创建对应的函数,例如有个函数的作用是交换传入的两个参数,那么不管传入是int还是double都可以使用同一个函数模版。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <typename T>
void Swap(T &a,T &b);
using namespace std;

int main(int argc, const char * argv[]) {
int i = 10;
int j = 30;
Swap(i,j);
cout << i << endl;
cout << j << endl;
double d1 = 2.2;
double d2 = 4.4;
Swap(d1, d2);
cout << d1 << endl;
cout << d2 << endl;
return 0;
}
template <typename T>
void Swap(T &a,T &b) {
T temp;
temp = a;
a = b;
b = temp;
}

函数与数组

单个数组作为函数的参数

C++中将数组名看成是第一个元素的地址。如下:

1
2
int cookies[8] = {1,2,4,8,16,32,64,128};
cout << cookies;

1
0x7ffeefbff540

但是该规矩有一个特例,就是在函数原型中,int *arr和int arr[]表示同一个意思。如下:

1
int sum_arr(int *arr,int n);

1
int sum_arr(int arr[],int n);

也就是说在这个特例中将数组作为了参数,其实是将数组第一个元素的地址传递给了函数,而并没有传递数组的内容。

总结:传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。其实传递数组时也传递了一个值,这个值被赋予给了一个新变量,也即是地址。
这么做的好处是能节省开支,如果该数组很大,那么要对它进行拷贝则费时费力。

Warning :为将数组类型和元素数量告诉数组处理函数,请通过两个不同的参数来传递它们:

1
2
void fillArray(int arr[], int size); //good
void fillArray(int arr[size]); //bad

如果在某个函数中,所传入的数组不希望被修改,则可以使用const关键字修饰该数组。这表示指针arr指向的是常量数据,在该函数中不能使用ar修改该数据,当然,这并不是说原始数组必须是常量。如下:

1
void show_array(const int arr[], int n);

多个数组作为函数的参数

实际上我们也可以通过传入两个地址来表示数组在这两个地址的区间内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using namespace std;
int sum_arr(int *start,int *end);
int main(int argc, const char * argv[]) {
int cookies[8] = {1,2,4,8,16,32,64,128};
int total = sum_arr(cookies, cookies + 8);
int wrongTotal = sum_arr(cookies, cookies + 9);
cout << total;
cout << endl;
cout << wrongTotal;

}
int sum_arr(int start[], int end[]){
int sum = 0;
int *pt;
for (pt = start; pt != end; pt++) {
sum = sum + *pt;
}
return sum;
}

1
2
3
Result:
255
-272632185

即使超出数组也不会报错,因为c++中只在初始化的时候检查数组是否越界。

函数和字符串

将字符串作为参数来传递的时候,同样传递的是第一个字符的地址,字符串有内置的结束字符。因此想要判断字符串是否读取完毕则可以使用*str是否为0。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using namespace std;
int count(char * str);
int main(int argc, const char * argv[]) {
char * str = "xingxing";
int g = count(str);
cout << g;
}
int count(char * str)
{
int count = 0;
while (*str) {
if (*str == 'g') {
count++;
}
str++;
}
return count;
}

函数和结构体

函数中使用结构体比较简单,将结构体当作一个对象来使用就可以了。不赘述。

函数指针

C++中函数也有地址,函数的地址是存储其机器语言代码的内存的开始地址,有时候会将函数地址作为参数传给另一个函数。

获取函数地址

使用函数名且不带任何参数就是使用函数的地址。例如test()是一个函数,那么test就是该函数的地址。

1
2
show(test()) //传递test函数的返回值
show(test) //传递test函数的地址

声明函数指针

声明一个函数指针就是将该函数原型中的函数名替换为指针名即可。

1
2
3
int test(int); //原型
int (*pf)(int); //函数指针声明
pf = test; //函数指针赋值

如果使用该函数指针来调用函数的话则可以这样:

1
2
pf(5);
(*pf)(5);

C++PrimerPlus习题

7.6

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
void Fill_array(double [], int size);
void Show_array(double [], int size);
void Reverse_array(double [], int size);

using namespace std;

int main(int argc, const char * argv[]) {
int max = 5;
double array[max];
Fill_array(array, max);
Show_array(array,max);
Reverse_array(array, max);
Show_array(array, max);
return 0;
}
void Fill_array(double arr[], int size)
{
for (int i = 0; i < size; i++) {
cout << "Enter double :";
cin >> arr[i];
}
}

void Show_array(double arr[], int size)
{
for (int i = 0; i < size; i++) {
cout << arr[i];
cout << endl;
}
}

void Reverse_array(double arr[], int size)
{
for (int i = 0; i < size / 2 ; i++) {
double tmp = arr[i];
arr[i] = arr[size - i -1];
arr[size - i -1] = tmp;
}
}

7.10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
double calculate(double,double,double (*pf)(double,double));
double add(double,double);
int main(int argc, const char * argv[]) {
double d = calculate(2.2,2.8,add);
cout << d << endl;
return 0;
}

double add(double x,double y)
{
return x + y;
}

double calculate(double i,double j,double (*pf)(double,double))
{
return pf(i,j);
}



基于C++ Primer plus第七章。

Singleton

Object

在Kotlin中,object是一个自带单例模式的数据类型,而在java中object是所有类的父类。如下是object分别在java和kotlin的代码:

1
2
3
4
object ExampleObject {
fun example() {
}
}

1
2
3
4
5
6
7
8
9
10
11
public final class ExampleObject {
public static final ExampleObject INSTANCE;

public final void example() {
}

static {
ExampleObject var0 = new ExampleObject();
INSTANCE = var0;
}
}

可以看到,kotlin中的object是对应java中的静态单例模式。因此如果想在kotlin中写出单例模式,可以直接使用object关键字即可。同时因为使用的是static实现单例模式,因此也保证了线程安全。后续会介绍更好的写法。

Java Singleton

这里其实对java中使用static来实现单例模式不是很清晰,主要的问题在于为什么static修饰的对象指向的是同一个内存地址。即使我这么写

1
2
3
4
5
6
7
8
9
10
public class Single {
public static Single single = new Single();
}

public static void main(String[] args) {
Single single = Single.single;
System.out.println(single);
Single single1 = Single.single;
System.out.println(single1);
}

Result:

1
2
Single@2b193f2d
Single@2b193f2d

静态单例模式还有另一种方式,区别在于只有调用getInstance方法时才会去实例化Single对象。

1
2
3
4
5
6
7
8
9
10
public class Single {

private static class SingleHoleder {
private static Single single = new Single();
}

public static Single getInstance() {
return SingleHoleder.single;
}
}

这边static关键字能实现单例模式的主要原因在于被static修饰的变量或对象具有唯一性。那么为什么具有唯一性呢,这就得查看static的原理了。

Static关键字

根据定义,被static所修饰的变量有且只有该变量的一份拷贝,并且能在类中公用。换句话说是一个全局的变量。静态变量在加载类的时候就已经分配好了内存,因此该变量是属于类的而不是某个new出来的对象的。在往下到jvm还未了解到,本文关于static到此结束。

更好的单例模式

1
2
3
4
5
6
7
8
9
10
public class Singleton private constructor() {
init { println("This ($this) is a singleton") }

private object Holder { val INSTANCE = Singleton() }

companion object {
val instance: Singleton by lazy { Holder.INSTANCE }
}
var b:String? = null
}
  • 将构造方法私有化,其他地方无法实例化该类。
  • 使用lazy关键字延迟instance的实例化,优化性能。
  • 使用companion关键字实现java中static的效果。

kotlin中的companion

由于kotlin中object默认是单例模式,因此没有static关键字,那么如果我们想要全局变量该怎么做呢,答案就是companion。

1
2
3
4
5
6
7
8
9
10
class Test private constructor() {
companion object {
val i = 1
val instance = Test()
}
}

fun main(args: Array<String>) {
println(Test.i)
}

1
Result : 1

这样就实现了static的效果。再来看一下decompile后的代码

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 final class Test {
private static final int i = 1;
@NotNull
private static final Test instance = new Test();
public static final Test.Companion Companion = new Test.Companion((DefaultConstructorMarker)null);

public static final class Companion {
public final int getI() {
return Test.i;
}

@NotNull
public final Test getInstance() {
return Test.instance;
}

private Companion() {
}

// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}

可以看到变量i被static所修饰。并且companion是对所有类公开的,因此适合用来实现单例模式。

Warning:一个类中只能有一个被companion修饰的object。

带参数的单例

Android中经常需要传递context,如果一个单例需要context作为参数的话用object是不行的,因为object不允许传递参数。因此可以借鉴Android中的LocalBroadcastManager.getInstance(this).sendBroadcast(intent)

1
2
3
4
5
6
7
8
9
companion object {
private var INSTANCE: RingerManager? = null
fun getInstance(context: Context): RingerManager {
if (INSTANCE == null) {
INSTANCE = RingerManager(context)
}
return INSTANCE!!
}
}

模板单例

在这个模板单例中还用到了synchronized来保证线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
open class SingletonHolder<out T: Any, in A>(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
@Volatile private var instance: T? = null

fun getInstance(arg: A): T {
val i = instance
if (i != null) {
return i
}

return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg)
instance = created
creator = null
created
}
}
}
}

用了模板之后不用每个单例都写一个类似的代码,可以复用。

1
2
companion object : SingletonHolder<CoreFramework, Context>(::CoreFramework)
CoreFramework.getInstance(context).doStuff()

参考文档:

https://antonioleiva.com/objects-kotlin/

https://medium.com/@adinugroho/singleton-in-kotlin-502f80fd8a63

https://www.journaldev.com/18662/kotlin-singleton-companion-object

https://medium.com/@BladeCoder/kotlin-singletons-with-argument-194ef06edd9e

Pointer Note

基本概念

指针也是一种变量,特殊之处在于它存储的是变量的地址,如下:

1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
int i = 6;
//assign the address of i to pointer
int *pointer = &i;
int *pointer1 = new int;
return 0;
}

操作符*被叫做间接值或者解除应用

Warning: 一定要在对指针使用*之前,将指针初始化为一个确定的、适当的地址

运算符new

在C中,使用malloc来分配内存,在c++中则使用new关键字。
为一个数据对象获得并制定分配内存的通用格式:
_typeName * pointer_name = new typeName;_

一旦我们不需要该指针,应该使用delete关键字将该指针释放,new和delete必须成对出现,否则会发生memory leak。

1
2
3
int *ps = new int;
...
delete ps;

这将释放ps指针所指向的内存,但不会删除ps本身,也就是说可以继续将ps指向其他地址。

动态数组

使用new关键字来创建动态数组,并且返回的是该数组第一个元素的地址,如果想要获得第二个元素的地址,则需要将该指针加1,如下:

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, const char * argv[]) {
// insert code here...
int *array = new int[10];
array[0] = 1;
array[1] = 3;
cout << *array;
cout << endl;
array = array + 1;
cout << *array;
cout << endl;
return 0;
}

Result:

1
2
1
3

动态结构

结构体同样可以使用new关键字。不同的是无法使用句点运算符,取而代之的是->运算符,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct things {
int price;
int sum;
};
int main(int argc, const char * argv[]) {
// insert code here...
things t = {3,18};
things * tpt = &t;
//things * tpt = new things;
cout << tpt->price;
cout << endl;
return 0;
}

Result:

1
3

指针与字符串

一般来说如果给cout提供一个指针的话,他将打印地址,但是如果指针的类型为char ,那么cout的指针其实指向的是char的第一个字符的地址,并且会依次往后直到结束符。如下:

1
2
3
4
5
6
7
8
9
10
int main(int argc, const char * argv[]) {
char * c = "dog";
cout << "c :" << c;
cout<< endl;
cout <<"*c :" << *c;
cout << endl;
cout <<"第一个字符的地址 :" << (int*)c;
cout << endl;
return 0;
}

1
2
3
c :dog
*c :d
第一个字符的地址 :0x100001edc

管理内存的三种方式

  • 自动存储
    在函数内部定义的常规变量使用自动存储空间,被称为自动变量,也就是说在函数执行完成之后会自动销毁。自动变量存储在栈中,采用先进后出的形式。
  • 静态存储
    变量成为静态的方式有两种,一是在函数外面定义;二是使用static关键字。
  • 动态存储
    使用new和delete管理内存,它们管理了一个内存池,被成为自由存储空间或堆,这个内存池和静态比变量以及自动变量的内存是分开的。

目前为C++ Primer Plus的第四章。

Container

Basic Concept

容器是用来存储对象的,通常有两种

  • Collection: 一个独立的序列,例如List,Set等
  • Map:一种键-值对.

    List、Set、Map的区别

    Now check the different result on the demo.
    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
    public class PrintingContainers {  
    public static void main(String[] args) {
    System.out.println(fill(new ArrayList<>()));
    System.out.println(fill(new LinkedList<>()));
    System.out.println(fill(new HashSet<>()));
    System.out.println(fill(new TreeSet<>()));
    System.out.println(fill(new LinkedHashSet<>()));

    System.out.println(fill(new HashMap<>()));
    System.out.println(fill(new TreeMap<>()));
    System.out.println(fill(new LinkedHashMap<>()));
    }

    static Collection fill(Collection<String> collection) {
    collection.add("dog");
    collection.add("cat");
    collection.add("rat");
    collection.add("dog");
    return collection;
    }

    static Map fill(Map<String, String> map) {
    map.put("rat", "Tom");
    map.put("cat", "Jerry");
    return map;
    }
    }

Result :

1
2
3
4
5
6
7
8
[dog, cat, rat, dog]
[dog, cat, rat, dog]
[rat, cat, dog]
[cat, dog, rat]
[dog, cat, rat]
{rat=Tom, cat=Jerry}
{cat=Jerry, rat=Tom}
{rat=Tom, cat=Jerry}

List

  • ArrayList: 适合查询,不适合插入和删除元素。
  • LinkedList : 按照添加的顺序存储元素,常用于频繁的插入和删除元素。

Practice For List: Create a single linked List.User iterator to insert or remove elements

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class SingleListDemo {  
public static void main(String[] args) {
SList<String> list = new SList<>();
SListIterator iterator = list.iterator();
iterator.insert("wang");
System.out.println(list);
iterator.insert("shen");
System.out.println(list);
iterator.insert("xing");
System.out.println(list);

SListIterator iterator1 = list.iterator();
iterator1.remove();
System.out.println(list);
}
}

class SList<E> {
Link<E> head = new Link<>(null);

public SListIterator iterator() {
return new SListIterator<E>(head);
}

@Override
public String toString() {
if (head.next == null) {
return "[]";
} else {
System.out.print("[");
StringBuilder stringBuilder = new StringBuilder();
SListIterator iterator = this.iterator();
while (iterator.hasNext()) {
stringBuilder.append(iterator.next() + (iterator.hasNext() ? "," : ""));
}
return stringBuilder + "]";
}
}
}

class Link<E> {
E e;
Link<E> next;
public Link(E e) {
this.e = e;
}
public Link(E e, Link<E> next) {
this.e = e;
this.next = next;
}
@Override
public String toString() {
return e != null ? e.toString() : null;
}
}

class SListIterator<E> {
Link<E> current;

public SListIterator(Link<E> current) {
this.current = current;
}

public boolean hasNext() {
return current.next != null;
}

public void insert(E e) {
current.next = new Link<>(e);
current = current.next;
}

public void remove() {
if (current.next != null)
current.next = current.next.next;
}

public Link<E> next() {
current = current.next;
return current;
}
}

Set

  • HashSet: 获取元素最快的一种
  • TreeSet: 降序的方式存储元素
  • LinkedHashSet : 根据添加的顺序存储元素
  • SortedSet: 由TreeSet实现的接口

Practice:Create a SortedSet ,Using LinkerList as an underlying implementation

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
public class MySortedSetDemo {  
public static void main(String[] args) {
MySortedSet<Integer> integerMySortedSet = new MySortedSet<>();
integerMySortedSet.add(6);
integerMySortedSet.add(8);
integerMySortedSet.add(2);
integerMySortedSet.add(5);
integerMySortedSet.add(2);
integerMySortedSet.add(7);
System.out.println(integerMySortedSet);
MySortedSet<String> stringMySortedSet = new MySortedSet<>();
stringMySortedSet.add("wang");
stringMySortedSet.add("shen");
stringMySortedSet.add("xing");
stringMySortedSet.add("is");
stringMySortedSet.add("good");
stringMySortedSet.add("is");
stringMySortedSet.add("cool");
System.out.println(stringMySortedSet);
}
}
class MySortedSet<E> extends LinkedList<E> {
public int compare(E e1, E e2) {
System.out.println(e1.hashCode());
System.out.println(e2.hashCode());
int result = e1.hashCode() - e2.hashCode();
return Integer.compare(result, 0);
}
public boolean add(E e) {
if (!this.contains(e)) {
Iterator<E> iterator = this.iterator();
int index = 0;
while (iterator.hasNext()) {
if (compare(iterator.next(), e) < 1) {
index++;
}
}
add(index, e);
return true;
}
return false;
}
}

Map

  • HashMap: 最快的找到元素的方式
  • TreeMap:降序方式存储,且是唯一带有submap方法的Map,可以返回一个子树
  • LinkerHashMap: 按照添加的顺序存储

Something about Array.asList

1
2
3
List<Integer> list = Arrays.asList(1,2,3);  
list.add(4);
//Runtime error Because the array cannot be resized

Let’s check the source code of Array.asList. It will return a fixed-size list by the specific array.

1
2
3
public static <T> List<T> asList(T... a) {  
return new ArrayList<>(a);
}

散列与散列码

由于线性搜索的速度相当慢,因此在HashMap中采用了散列码来取代线性搜索。它是一个代表对象的int值。hashCode()属于Object中的方法,因此适用于所有的java对象。HashMap就是通过hashCode来进行查询的。下面是将Groundhog和Prediction联系起来的demo:

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
public class Prediction {
private static Random random = new Random(47);
private boolean shadow = random.nextDouble() > 0.5;
@Override
public String toString() {
if (shadow) {
return "Six more ";
} else {
return "early spring";
}
}
}
public class Groundhog {
protected int number;

public Groundhog(int number) {
this.number = number;
}

@Override
public String toString() {
return "Groundhog #" + number;
}
}
public static <T extends Groundhog> void detectSpring(Class<T> type) throws Exception {
Constructor<T> ghog = type.getConstructor(int.class);
Map<Groundhog, Prediction> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
map.put(ghog.newInstance(i), new Prediction());
}
System.out.println("map = " + map);
Groundhog gh = ghog.newInstance(3);
if (map.containsKey(gh)) {
System.out.println(map.get(gh));
}
}

结果是找不到该key,原因很简单,这两个根本不是同一个类当然是查不到的,而它的查找方式就是在一个node中查找是否包含某个对象的hashcode,object的默认hashcode是和地址相关的。而想要让他们联系起来则需要覆写hashcode方法和equals方法。修改GroundDog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Groundhog {
protected int number;

public Groundhog(int number) {
this.number = number;
}

@Override
public String toString() {
return "Groundhog #" + number;
}

@Override
public boolean equals(Object o) {
return number == ((Groundhog) o).number;
}

@Override
public int hashCode() {
return number;
}
}

那么为什么也要覆写equals方法呢,因为HashMap中覆写了equals方法,是根据key来判断是否相同的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final boolean equals(Object var1) {
if (var1 == this) {
return true;
} else {
if (var1 instanceof Entry) {
Entry var2 = (Entry)var1;
if (Objects.equals(this.key, var2.getKey()) && Objects.equals(this.value, var2.getValue())) {
return true;
}
}

return false;
}
}

创建一个新的Map:

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
54
55
56
57
58
59
60
public class SlowMap<K, V> extends AbstractMap<K, V> {
private List<K> keys = new ArrayList<>();
private List<V> values = new ArrayList<>();

public V put(K key, V value) {
V oldValue = get(key);
if (!keys.contains(key)) {
keys.add(key);
values.add(value);
} else {
values.set(keys.indexOf(value), value);
}
return oldValue;
}

public V get(Object key) {
if (!keys.contains(key))
return null;
else
return values.get(keys.indexOf(key));
}

@Override
public Set<Map.Entry<K, V>> entrySet() {
Set<Map.Entry<K, V>> set = new HashSet<>();
Iterator<K> ki = keys.iterator();
Iterator<V> vi = values.iterator();
while (ki.hasNext()) {
set.add(new MapEntry<K, V>(ki.next(), vi.next()));
}
return null;
}
}

class MapEntry<K, V> implements Map.Entry<K, V> {
private K key;
private V value;

public MapEntry(K key, V value) {
this.key = key;
this.value = value;
}

@Override
public K getKey() {
return key;
}

@Override
public V getValue() {
return value;
}

@Override
public V setValue(V v) {
V oldValue = value;
value = v;
return oldValue;
}
}

这个简陋的map问题在于对键的保存是线性,只能挨个匹配,这样是非常慢的,那么HashMap是如何优化的呢,其实HashMap的数据结构是数组加单向链表的形式。将hashcode的hash值(就是一个转换的方法)作取模运算,结果作为数组的索引值index,而数组的元素是单项链表,挨个add。当我们需要get的时候则先通过hashcode的hash值作取模运算得到index。然后再在取得链表中挨个配对。简单实现:

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
54
55
56
57
58
SimpleHashMap.java
public class SimpleHashMap<K, V> extends AbstractMap<K, V> {
static final int SIZE = 997;
LinkedList<MapEntry<K, V>>[] buckets = new LinkedList[SIZE];

@Override
public Set<Map.Entry<K, V>> entrySet() {
Set<Map.Entry<K, V>> set = new HashSet<>();
for (LinkedList<MapEntry<K, V>> bucket : buckets) {
if (bucket == null) continue;
for (MapEntry<K, V> pair : bucket) {
set.add(pair);
}
}
return set;
}

public V put(K key, V value) {
V oldValue = null;
int index = Math.abs(key.hashCode()) % SIZE;
if (buckets[index] == null) {
buckets[index] = new LinkedList<>();
}
MapEntry<K, V> pair = new MapEntry<>(key, value);
LinkedList<MapEntry<K, V>> bucket = buckets[index];
ListIterator<MapEntry<K, V>> iterator = bucket.listIterator();
boolean hasfound = false;
while (iterator.hasNext()) {
MapEntry<K, V> iPair = iterator.next();
if (iPair.getKey().equals(key)) {
oldValue = iPair.getValue();
hasfound = true;
break;
}
}
if (!hasfound) {
buckets[index].add(new MapEntry<>(key, value));
}
return oldValue;

}

public V get(Object key) {
int index = Math.abs(key.hashCode()) % 997;
for (MapEntry<K, V> iPair : buckets[index]) {
if (iPair.getKey() == key)
return iPair.getValue();
}
return null;
}

public static void main(String[] args) {
SimpleHashMap<String, String> map = new SimpleHashMap<>();
map.put("key0", "value0");
map.put("key1", "value1");
System.out.println(map);
}
}

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
MapEntry.java
public class MapEntry<K, V> implements Map.Entry<K, V> {
private K key;
private V value;

public MapEntry(K key, V value) {
this.key = key;
this.value = value;
}

@Override
public K getKey() {
return key;
}

@Override
public V getValue() {
return value;
}

@Override
public V setValue(V v) {
V oldValue = value;
value = v;
return oldValue;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof MapEntry)) return false;
MapEntry me = (MapEntry) o;
return (key == null ?
me.getKey() == null : key.equals(me.getKey())) &&
(value == null ?
me.getValue() == null : value.equals(me.getValue()));
}

@Override
public int hashCode() {
return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode());
}
}