注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

300行代码实现循环滚动控件

序言在业务中需要显示一个循环滚动的控件,内容可以循环滚动,可以自动滚动,手指触摸的时候会暂停。 由于目前的方案都是基于ViewPager或者RecycleView的。还需要实现Adapter,需要拦截各种事件。使用成本比较高。于是我就自定义了一个控件实现该功能...
继续阅读 »

序言

在业务中需要显示一个循环滚动的控件,内容可以循环滚动,可以自动滚动,手指触摸的时候会暂停。 由于目前的方案都是基于ViewPager或者RecycleView的。还需要实现Adapter,需要拦截各种事件。使用成本比较高。于是我就自定义了一个控件实现该功能,

使用

使用起来很简单。把需要显示的控件放置在其中就行。就和普通的HorizontalScrollView用法一样。 不过子控件必须要LoopLinearLayout 在这里插入图片描述

效果

  • 1.支持左右循环滚动
  • 2.支持自动滚动
  • 3.支持点击事件
  • 4.触摸暂停
  • 5.支持惯性滚动
  • 6.一共不到300行代码,逻辑简单易于扩展

在这里插入图片描述

原理

通过继承自HorizontalScrollView实现,重新onOverScrolled 和 scrollTo 方法在调用supper方法之前,对是否到达边界进行判断,如果到达就调用LoopLinearLayout.changeItemsToRight() 方法对内容重新摆放。

摆放使用的是 child.layout() 的方法,没有性能问题。摆放完成以后,对scrollX进行重新赋值。

需要注意的是在HorizontalScrollView中有一个负责惯性滚动的OverScroller 在这里插入图片描述 但是在调用其fling方法之前会设置maxX这导致无法滚动到控件内容之外。所以使用反射修改了这个类。拦截了fling方法 在这里插入图片描述

而动画的时长设置的是滚动一个LoopScrollView宽度需要的时间。还有就是无限循环的动画需要在 onDetachedFromWindow中移除,避免内存泄漏

源码

LoopLinearLayout

package com.example.myapplication;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import androidx.annotation.Nullable;

import java.util.List;

/**
* Created by zhuguohui
* Date: 2021/11/30
* Time: 10:46
* Desc:
*/
public class LoopLinearLayout extends LinearLayout {
public LoopLinearLayout(Context context) {
this(context, null);
}

public LoopLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}


public void changeItemsToRight(List<View> moveItems, int offset) {

int offset2 = 0;
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
if (!moveItems.contains(childAt)) {
MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();
offset2 += childAt.getWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
childAt.layout(childAt.getLeft() - offset, childAt.getTop(), childAt.getRight() - offset, childAt.getBottom());
}
}
for(View view:moveItems){
view.layout(view.getLeft()+offset2,view.getTop(),view.getRight()+offset2,view.getBottom());
}
}
public void changeItemsToLeft(List<View> moveItems, int offset) {

int offset2 = 0;
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
if (!moveItems.contains(childAt)) {
MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();
offset2 += childAt.getWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
childAt.layout(childAt.getLeft() + offset, childAt.getTop(), childAt.getRight() + offset, childAt.getBottom());
}
}
for(View view:moveItems){
view.layout(view.getLeft()-offset2,view.getTop(),view.getRight()-offset2,view.getBottom());
}
}


}

LoopScrollView

package com.example.myapplication;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.HorizontalScrollView;
import android.widget.OverScroller;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

public class LoopScrollView extends HorizontalScrollView {

private LoopScroller loopScroller;
private ValueAnimator animator;

public LoopScrollView(Context context) {
this(context, null);
}

public LoopScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
setOverScrollMode(OVER_SCROLL_ALWAYS);
try {
@SuppressLint("DiscouragedPrivateApi")
Field field =HorizontalScrollView.class.getDeclaredField("mScroller");
field.setAccessible(true);
loopScroller = new LoopScroller(getContext());
field.set(this, loopScroller);

} catch (Exception e) {
e.printStackTrace();
}

}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if(changed||animator==null){
buildAnimation();
}
}

private void buildAnimation() {
if(animator!=null){
animator.cancel();
animator=null;
}
animator = ValueAnimator.ofInt(getWidth() - getPaddingRight() - getPaddingLeft());
animator.setDuration(5*1000);
animator.setRepeatCount(-1);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
int lastValue;
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value= (int) animation.getAnimatedValue();
int scrollByX=value-lastValue;
// Log.i("zzz","scroll by x="+scrollByX);
scrollByX=Math.max(0,scrollByX);
if(userUp) {
scrollBy(scrollByX, 0);
}
lastValue=value;
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
}

@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
}

});
animator.start();
}

static class LoopScroller extends OverScroller{
public LoopScroller(Context context) {
super(context);
}

@Override
public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) {
super.fling(startX, startY, velocityX, velocityY, Integer.MIN_VALUE,Integer.MAX_VALUE, minY, maxY, 0, overY);
}
}




@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if(animator!=null){
animator.cancel();
animator.removeAllListeners();
animator = null;
}
}

@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
if (userUp) {
//scroller再滚动
scrollX=loopScroller.getCurrX();
int detailX = scrollX - lastScrollX;
lastScrollX = scrollX;
if(detailX==0){
return;
}
scrollX = detailX + getScrollX();

}
int moveTo = moveItem(scrollX,clampedX);

super.onOverScrolled(moveTo, scrollY, false, clampedY);
}

boolean userUp = true;
int lastScrollX = 0;

@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
userUp = true;
lastScrollX = getScrollX();
} else {
userUp = false;
}
return super.onTouchEvent(ev);
}
@Override
public void scrollTo(int x, int y) {
int scrollTo = moveItem(x, false);
super.scrollTo(scrollTo, y);
}


private int moveItem(int scrollX, boolean clampedX) {

int toScrollX = scrollX;

if (getChildCount() > 0) {
if (!canScroll(scrollX,clampedX)) {
boolean toLeft=scrollX<=0;
int mWidth=getWidth()-getPaddingLeft()-getPaddingRight();
//无法向右滚动了,将屏幕外的item,移动到后面
List<View> needRemoveViewList = new ArrayList<>();
LoopLinearLayout group = (LoopLinearLayout) getChildAt(0);
int removeItemsWidth = 0;
boolean needRemove = false;
for (int i = group.getChildCount() - 1; i >= 0; i--) {
View itemView = group.getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) itemView.getLayoutParams();
if(toLeft){
int itemLeft = itemView.getLeft() - params.leftMargin;
if (itemLeft >= mWidth) {
//表示之后的控件都需要移除
needRemove = true;
}
}else{
int itemRight = itemView.getRight() + params.rightMargin;
if (itemRight <= scrollX) {
//表示之后的控件都需要移除
needRemove = true;
}
}

if (needRemove) {
int itemWidth = itemView.getWidth() + params.rightMargin + params.leftMargin;
removeItemsWidth += itemWidth;
needRemoveViewList.add(0,itemView);
}
needRemove=false;
}
if(!toLeft){
group.changeItemsToRight(needRemoveViewList,removeItemsWidth);
toScrollX -=removeItemsWidth;
}else{
group.changeItemsToLeft(needRemoveViewList,removeItemsWidth);
toScrollX +=removeItemsWidth;
}

}

}
return Math.max(0, toScrollX);
}

private boolean canScroll(int scrollX, boolean clampedX) {
if(scrollX<0){
return false;
}
if(scrollX==0&&clampedX){
//表示向左划不动了
return false;
}
View child = getChildAt(0);
if (child != null) {
int childWidth = child.getWidth();
return getWidth() + scrollX < childWidth + getPaddingLeft() + getPaddingRight();
}
return false;
}
}

最后所有的功能只依赖上述两个类,关于动画的时长写死在类中的,没有抽成方法。有需要的自己去改吧。


作者:solo_99
链接:https://juejin.cn/post/7036262704482353160
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

synchronized 的实现原理

synchronized 的使用 锁代码块(锁对象可指定,可为this、XXX.class、全局变量) 锁普通方法(锁对象是this,即该类实例本身) 锁静态方法(锁对象是该类,即XXX.class) 锁代码块 public class Sync { ...
继续阅读 »

synchronized 的使用



  • 锁代码块(锁对象可指定,可为this、XXX.class、全局变量)

  • 锁普通方法(锁对象是this,即该类实例本身)

  • 锁静态方法(锁对象是该类,即XXX.class)


锁代码块


public class Sync {

private int a = 0;

public void add(){
synchronized (this){
System.out.println("a values "+ ++a);
}
}

}

反编译之后的


public add()V
TRYCATCHBLOCK L0 L1 L2 null
TRYCATCHBLOCK L2 L3 L2 null
ALOAD 0
DUP
ASTORE 1
MONITORENTER
L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "a values "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
DUP
GETFIELD com/arrom/webview/Sync.a : I
ICONST_1
IADD
DUP_X1
PUTFIELD com/arrom/webview/Sync.a : I
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
ALOAD 1
MONITOREXIT
L1
GOTO L4
L2
ASTORE 2
ALOAD 1
MONITOREXIT
L3
ALOAD 2
ATHROW
L4
RETURN
MAXSTACK = 5
MAXLOCALS = 3
}

由反编译结果可以看出:synchronized代码块主要是靠MONITORENTERMONITOREXIT这两个原语来实现同步的。当线程进入MONITORENTER获得执行代码的权利时,其他线程就不能执行里面的代码,直到锁Owner线程执行MONITOREXIT释放锁后,其他线程才可以竞争获取锁。


MONITORENTER

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权.



  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。


第2点就涉及到了可重入锁,意思就是说当一个线程已经获取一个锁时,它可以再获取无数次,从代码的角度上将就是有无数个相同的synchronized语句块嵌套在一起。在进入时,monitor的进入数+1;退出时就-1,直到为0的时候才可以被其他线程竞争获取。


MONITOREXIT

执行MONITOREXIT的线程必须是objectref所对应的monitor的所有者。


指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。


锁普通方法


public class Sync {

private int a = 0;

public synchronized void add(){
System.out.println("a values "+ ++a);
}

}

反编译之后并没有monitorenter和monitorexit,但是常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:


当方法调用时会检查方法的ACC_SYNCHRONIZED之后才能执行方法体,方法执行完后再释放monitor。


在方法执行期间,其他任何线程都无法再获得同一个monitor对象。这种方式与语句块没什么本质区别,都是通过竞争monitor的方式实现的。只不过这种方式是隐式的实现方法。


MONITORENTER和ACC_SYNCHRONIZED只是起标志作用,并无实质操作。


锁静态方法



private static int a = 0;

public synchronized static void add(){
System.out.println("a values "+ ++a);
}

常量池中用ACC_STATIC标志了这是一个静态方法,然后用ACC_SYNCHRONIZED标志位提醒线程去竞争monitor。由于静态方法是属于类级别的方法(即不用创建对象就可以被调用),所以这是一个类级别(XXX.class)的锁,即竞争某个类的monitor。


锁的竞争过程


image.png



  • (1)、多个线程请求锁,首先进入Contention List,它可以接纳所有请求线程,而且是一个后进先出(LIFO)的虚拟队列,通过结点Node和next指针构造。

  • (2)(3)、ContentionList会被线程并发访问,EntryList为了降低线程对ContentionList队尾的争用而构造出来。当Owner释放锁时,会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head结点)为Ready Thread,也就是说某个时刻最多只有一个线程正在竞争锁。

  • (4)、Owner并不是直接把锁交给OnDeck线程,而是将竞争锁的权利交给OnDeck(将锁释放了),然后让OnDeck自己去竞争。竞争成功后,OnDeck线程就变成Owner;否则继续留在EntryList的队头。

  • (5)(6)、当线程调用wait方法被阻塞时,进入WaitSet;当其他线程调用notifyAll()(notify())方法后,阻塞队列的(某个)线程就会进入EntryList中。


      处于ContetionList、EntryList、WaitSet的线程均处于阻塞状态。而线程被阻塞涉及到用户态与内核态的切换(Liunx),系统切换严重影响锁的性能。解决这个问题的办法就是自旋。自旋就是线程不断进行内部循环,即for循环什么也不做,防止线程wait()阻塞,在自旋过程中不断尝试获取锁,如果自旋期间,Owner刚好释放锁,此时自旋线程就可以去竞争锁。如果自旋了一段时间还没获取到锁,那没办法,只能调用wait()阻塞了。 

为什么自旋了一段时间后又调用wait()方法呢?因为自旋是要消耗CPU的,而且还有线程上下文切换,因为CPU还可以调度线程,只不过执行的是空的for循环罢了。 

对自旋锁周期的选择上,HotSpot认为最佳时间应是一个线程上下文切换的时间,但目前并没有做到。 

所以,synchronized是什么时候进行自旋的?答案是在进入ContetionList之前,因为它自旋一定时间后还没获取锁,最后它只好在ContetionList中阻塞等待了。


对象头


对象头(Object Header)包括两部分信息。


一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。


对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额 外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。


image.png


另外一部分是类型指针,即是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经过对象本身。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。


为了减少锁释放带来的消耗,锁有一个升级的机制,从轻到重依次是:无锁状态 ——> 偏向锁 ——> 轻量级锁 ——>重量级锁。


偏向锁


在无其它线程与它竞争的情况下,持有偏向锁的线程永远也不需要同步。


它的加锁过程很简单:线程访问同步代码块时检查偏向锁中线程ID是否指向自己,如果是表明该线程已获得锁;否则,检测偏向锁标记是否为1,不是的话则CAS竞争锁,如果是就将对象头中线程ID指向自己。


当存在线程竞争锁时,偏向锁才会撤销,转而升级为轻量级锁。而这个撤销过程则需要有一个全局安全点(即这个时间点上没有正在执行的字节码)


image.png


在撤销锁的时候,栈中对象头的Mark Word要么偏向于其他线程,要么恢复到无锁或者轻量级锁。



  • 优点:加锁和解锁无需额外消耗

  • 缺点:锁进化时会带来额外锁撤销的消耗

  • 适用场景:只有一个线程访问同步代码块


轻量级锁


image.png



  • 优点:竞争的线程不阻塞,也就是不涉及到用户态与内核态的切换(Liunx),减少系统切换锁带来的开销

  • 缺点:如果长时间竞争不到锁,自旋会消耗CPU

  • 适用场景:追求响应时间、同步块执行速度非常快


重量级锁


它是传统意义上的锁,通过互斥量来实现同步,线程阻塞,等待Owner释放锁唤醒。




  • 优点:线程竞争不自旋,不消耗CPU




  • 缺点:线程阻塞,响应时间慢




  • 适用场景:追求吞吐量、同步块执行时间较长


作者:深圳_Arrom
链接:https://juejin.cn/post/7036354740342489118
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Jetpack-Lifecycle

1.AndroidX 的ComponentActivity 实现了LifecycleOwner接口,ComponentActivity 的子类会重写LifecycleOwner的接口方法,以便得到我们使用的lifecycle对象,lifecycle 是在Com...
继续阅读 »

图片.png


1.AndroidX 的ComponentActivity 实现了LifecycleOwner接口,ComponentActivity 的子类会重写LifecycleOwner的接口方法,以便得到我们使用的lifecycle对象,lifecycle 是在ComponentActivity 中创建的lifecycleRegistry对象;


2.使用的时候,通过lifecycleRegistry对象addObserver的方式注册LifecycleObserver,当生命周期变化的时候,会回调LifecycleObserver的onStateChanged, 里面的Lifecycle.Event能监听到当前Activity不同生命周期的变化


原理:


1.lifecycle注册的时候,会初始化observer的状态State,初始状态是 initial或者destroy, 将observer和state封装成一个类作为map的value值, observer作为key;


2.addObserver还有一个状态对齐,等会讲


3.当宿主activity或者fragment生命周期发生变化的时候,会分发当前的生命周期事件,转换成对应的mState,


3.1 和当前map中最早添加的observer的state进行比较,如果mState小于 state的值, 说明当前执行的是OnResume->onPause->onStop->onDestroy中的某一个环节, 遍历当前的map,将map的state向下更新为当前的生命周期状态,并调用observer的onStateChange方法;


3.2 和当前map中最后添加的observer的state进行比较,如果mState大于 state的值, 说明当前执行的是onCreate->onStart->onResume中的某一个环节, 遍历当前的map,将map的state向上更新为当前的生命周期状态,并调用observer的onStateChange方法;


3.3 状态对齐, 比如:当我们在生命周期的onStop方法中去addObserver时,此时添加到map中的state是inital状态, 实际上当前的生命周期是onStop,对应的是Created状态, 此时需要将map中小于Created的状态更新成Created状态,因为是upEvent, 所以回调的event会有onCreate


小结: 1.创建一个state保存到map; 2.等生命周期变化时,更新state的值,回调onStateChanged方法,达到监控生命周期的作用


作者:朱络嗒
链接:https://juejin.cn/post/7036552818668339208
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Glide数据输入输出

基础概念 在正式开始之前先明确一些概念 Glide输入: 我们日常在使用Glide的时候,通过load可以加载不同的资源类型例如文件,字符串等待。 我们把load的不同类型称为不同的输入。 Glide输出: Glide RequestManager提供了许多...
继续阅读 »

基础概念


在正式开始之前先明确一些概念


Glide输入: 我们日常在使用Glide的时候,通过load可以加载不同的资源类型例如文件,字符串等待。


requestManager多个load重载.png


我们把load的不同类型称为不同的输入。


Glide输出: Glide RequestManager提供了许多的as重载方法,


GlideAs方法.png


通过不同的as我们可以指定不同的输出类型。


ModelLoader: 是一个泛型接口,最直观的翻译是模型加载器。ModelLoader标记了它能够加载什么类型的数据,以及加载后返回什么样的数据类型。注意这里说说的返回的数据类型并不是我们想要的输出。ModelLoader定义如下


public interface ModelLoader<Model, Data> {
 class LoadData<Data> {
     //数据加载的key
   public final Key sourceKey;
   public final List<Key> alternateKeys;
     //获取数据的接口,对应获取不同类型的数据实现
   public final DataFetcher<Data> fetcher;

   public LoadData(@NonNull Key sourceKey, @NonNull DataFetcher<Data> fetcher) {
     this(sourceKey, Collections.<Key>emptyList(), fetcher);
  }

   public LoadData(@NonNull Key sourceKey, @NonNull List<Key> alternateKeys,
       @NonNull DataFetcher<Data> fetcher) {
     this.sourceKey = Preconditions.checkNotNull(sourceKey);
     this.alternateKeys = Preconditions.checkNotNull(alternateKeys);
     this.fetcher = Preconditions.checkNotNull(fetcher);
  }
}

   //创建LoadData 对象
 @Nullable
 LoadData<Data> buildLoadData(@NonNull Model model, int width, int height,
     @NonNull Options options);
   //判断当前的ModelLoader是否能够处理这个model
 boolean handles(@NonNull Model model);
}

DataFetcher: 用于进行数据加载,不同的类型有不同的DataFetcher


SourceGenerator远程数据加载过程


@Override
public boolean startNext() {
 //...
 boolean started = false;
 while (!started && hasNextModelLoader()) {
   loadData = helper.getLoadData().get(loadDataListIndex++);
   if (loadData != null
       && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
       || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
     started = true;
     loadData.fetcher.loadData(helper.getPriority(), this);
  }
}
 return started;
}

代码流程:


通过DecodeHelper获取LoadData,遍历每一个LoadData;


查看当前LoadData加载出来的数据能不能,转换成我们想要的输出数据,如果可以的话就是用当前loadData进行数据加载。


DecodeHelpe#getLoadData()


List<LoadData<?>> getLoadData() {
 if (!isLoadDataSet) {
   isLoadDataSet = true;
   loadData.clear();
     //此处的model就是我们 通过调用load传递进来的参数 即输入
   List<ModelLoader<Object, ?>> modelLoaders = glideContext.getRegistry().getModelLoaders(model);
   //noinspection ForLoopReplaceableByForEach to improve perf
   for (int i = 0, size = modelLoaders.size(); i < size; i++) {
     ModelLoader<Object, ?> modelLoader = modelLoaders.get(i);
     //通过modelLoader 构建loadData
     LoadData<?> current =
         modelLoader.buildLoadData(model, width, height, options);
     if (current != null) {
       loadData.add(current);
    }
  }
}
 return loadData;
}

ModelLoaderRegistry#getModelLoaders


getModelLoaders()实现的位置在ModelLoaderRegistry#getModelLoaders


public <A> List<ModelLoader<A, ?>> getModelLoaders(@NonNull A model) {
   //获取对应的modelLoader
 List<ModelLoader<A, ?>> modelLoaders = getModelLoadersForClass(getClass(model));
 int size = modelLoaders.size();
 boolean isEmpty = true;
 List<ModelLoader<A, ?>> filteredLoaders = Collections.emptyList();
 //noinspection ForLoopReplaceableByForEach to improve perf
 for (int i = 0; i < size; i++) {
   ModelLoader<A, ?> loader = modelLoaders.get(i);
     //判断对应的modelLoader是否有能力处理对应的model
   if (loader.handles(model)) {
     if (isEmpty) {
       filteredLoaders = new ArrayList<>(size - i);
       isEmpty = false;
    }
     filteredLoaders.add(loader);
  }
}
 return filteredLoaders;
}

getModelLoadersForClass主要是通过MultiModelLoaderFactory#build。然后MultiModelLoaderFactory会遍历所有已经注册的ModelLoader,只要当前的model是已经注册model的子类或者对应的实现,那么就会把对应的ModelLoader添加到待返回的集合中。


DecodeHelper#hasLoadPath


boolean hasLoadPath(Class<?> dataClass) {
   return getLoadPath(dataClass) != null;
}

<Data> LoadPath<Data, ?, Transcode> getLoadPath(Class<Data> dataClass) {
 return glideContext.getRegistry().getLoadPath(dataClass, resourceClass, transcodeClass);
}

可以看到hasLoadPath代码其实非常简单,就是获取一个LoadPath集合。获取的时候传递了三个参数 DataFetcher加载出来的数据类型dataClass,resourceClass ,transcodeClass


getLoadPath参数


对于resourceClass ,transcodeClass在DecodeHelper定义如下:


private Class<?> resourceClass;
private Class<Transcode> transcodeClass;

他们在init方法中进行初始化,经过层层代码的流转我们发现最终的参数初始化来自于RequestBuilder#obtainRequest


private Request obtainRequest(
     Target<TranscodeType> target,
     RequestListener<TranscodeType> targetListener,
     BaseRequestOptions<?> requestOptions,
     RequestCoordinator requestCoordinator,
     TransitionOptions<?, ? super TranscodeType> transitionOptions,
     Priority priority,
     int overrideWidth,
     int overrideHeight,
     Executor callbackExecutor) {
   return SingleRequest.obtain(
       context,
       glideContext,
       model,
       //该参数会在调用as系列方法后初始化,指向的是我们想要的输出类型。
       transcodeClass,
       //指向的是RequestBuilder 自身
       requestOptions,
       overrideWidth,
       overrideHeight,
       priority,
       target,
       targetListener,
       requestListeners,
       requestCoordinator,
       glideContext.getEngine(),
       transitionOptions.getTransitionFactory(),
       callbackExecutor);
}

而RequestOptions#getResourceClass返回的resourceClass默认情况下返回的是Object,而在asBitmap和asGifDrawable会做其它的转换。


private static final RequestOptions DECODE_TYPE_BITMAP = decodeTypeOf(Bitmap.class).lock();
private static final RequestOptions DECODE_TYPE_GIF = decodeTypeOf(GifDrawable.class).lock();

@NonNull
 @CheckResult
 public RequestBuilder<Bitmap> asBitmap() {
   return as(Bitmap.class).apply(DECODE_TYPE_BITMAP);
}
 public RequestBuilder<GifDrawable> asGif() {
   return as(GifDrawable.class).apply(DECODE_TYPE_GIF);
}

getLoadPath执行过程


getLoadPath最终会调用Registry#getLoadPath


@Nullable
public <Data, TResource, Transcode> LoadPath<Data, TResource, Transcode> getLoadPath(
   @NonNull Class<Data> dataClass, @NonNull Class<TResource> resourceClass,
   @NonNull Class<Transcode> transcodeClass) {
   //先获取DecodePath  
 List<DecodePath<Data, TResource, Transcode>> decodePaths =
       getDecodePaths(dataClass, resourceClass, transcodeClass);
   if (decodePaths.isEmpty()) {
     result = null;
  } else {
     result =
         new LoadPath<>(
             dataClass, resourceClass, transcodeClass, decodePaths, throwableListPool);
  }
   loadPathCache.put(dataClass, resourceClass, transcodeClass, result);
 return result;
}

private <Data, TResource, Transcode> List<DecodePath<Data, TResource, Transcode>> getDecodePaths(
     @NonNull Class<Data> dataClass, @NonNull Class<TResource> resourceClass,
     @NonNull Class<Transcode> transcodeClass) {
   List<DecodePath<Data, TResource, Transcode>> decodePaths = new ArrayList<>();
   //遍历所有资源解码器,获取能够解析当前输入dataClass的解码器
   List<Class<TResource>> registeredResourceClasses =
       decoderRegistry.getResourceClasses(dataClass, resourceClass);
   for (Class<TResource> registeredResourceClass : registeredResourceClasses) {
       //获取能够解析当前输入dataClass且将数据转变成我们想要的transcodeClass类型的转换类
     List<Class<Transcode>> registeredTranscodeClasses =
         transcoderRegistry.getTranscodeClasses(registeredResourceClass, transcodeClass);

     for (Class<Transcode> registeredTranscodeClass : registeredTranscodeClasses) {
//获取对应的所有解码器
       List<ResourceDecoder<Data, TResource>> decoders =
           decoderRegistry.getDecoders(dataClass, registeredResourceClass);
       //转换类
       ResourceTranscoder<TResource, Transcode> transcoder =
           transcoderRegistry.get(registeredResourceClass, registeredTranscodeClass);
       @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
       DecodePath<Data, TResource, Transcode> path =
           new DecodePath<>(dataClass, registeredResourceClass, registeredTranscodeClass,
               decoders, transcoder, throwableListPool);
       decodePaths.add(path);
    }
  }
   return decodePaths;
}

整个过程涉及到两个关键类LoadPath DecodePath。


LoadPath 由数据类型datacalss 和 DecodePath组成


DecodePath 由数据类型dataclass 解码器 ResourceDecoder 集合 和资源转换 ResourceTranscoder 构成。总体上而言 一个LoadPath的存在代表着可能存在一条路径能够将ModelLoader加载出来的data解码转换成我们指定的数据类型。


DocodeJob数据解码的过程


Glide DecodeJob 的工作过程我们知道SourceGenerator在数据加载完成之后如果允许缓存原始数据会再次执行SourceGenerator#startNext将加载的数据进行缓存,然后通过DataCacheGenerator从缓存文件中获取。最终获取数据成功后会调用DocodeJob#onDataFetcherReady


作者:小小小小小鹿
链接:https://juejin.cn/post/7036389645168410655
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

大厂面试Kafka,一定会问到的幂等性

01 幂等性如此重要 Kafka作为分布式MQ,大量用于分布式系统中,如消息推送系统、业务平台系统(如结算平台),就拿结算来说,业务方作为上游把数据打到结算平台,如果一份数据被计算、处理了多次,产生的后果将会特别严重。 02 哪些因素影响幂等性...
继续阅读 »

01 幂等性如此重要


Kafka作为分布式MQ,大量用于分布式系统中,如消息推送系统、业务平台系统(如结算平台),就拿结算来说,业务方作为上游把数据打到结算平台,如果一份数据被计算、处理了多次,产生的后果将会特别严重。

02 哪些因素影响幂等性


使用Kafka时,需要保证exactly-once语义。要知道在分布式系统中,出现网络分区是不可避免的,如果kafka broker 在回复ack时,出现网络故障或者是full gc导致ack timeout,producer将会重发,如何保证producer重试时不造成重复or乱序?又或者producer 挂了,新的producer并没有old producer的状态数据,这个时候如何保证幂等?即使Kafka 发送消息满足了幂等,consumer拉取到消息后,把消息交给线程池workers,workers线程对message的处理可能包含异步操作,又会出现以下情况:


  • 先commit,再执行业务逻辑:提交成功,处理失败 。造成丢失




  • 先执行业务逻辑,再commit:提交失败,执行成功。造成重复执行




  • 先执行业务逻辑,再commit:提交成功,异步执行fail。造成丢失




本文将针对以上问题作出讨论


03 Kafka保证发送幂等性


       针对以上的问题,kafka在0.11版新增了幂等型producer和事务型producer。前者解决了单会话幂等性等问题,后者解决了多会话幂等性。


单会话幂等性


为解决producer重试引起的乱序和重复。Kafka增加了pid和seq。Producer中每个RecordBatch都有一个单调递增的seq; Broker上每个tp也会维护pid-seq的映射,并且每Commit都会更新lastSeq。这样recordBatch到来时,broker会先检查RecordBatch再保存数据:如果batch中 baseSeq(第一条消息的seq)比Broker维护的序号(lastSeq)大1,则保存数据,否则不保存(inSequence方法)。

ProducerStateManager.scala


    private def maybeValidateAppend(producerEpoch: Short, firstSeq: Int, offset: Long): Unit = {

    validationType match {

    case ValidationType.None =>

    case ValidationType.EpochOnly =>

    checkProducerEpoch(producerEpoch, offset)

    case ValidationType.Full =>

    checkProducerEpoch(producerEpoch, offset)

    checkSequence(producerEpoch, firstSeq, offset)

    }

    }

    private def checkSequence(producerEpoch: Short, appendFirstSeq: Int, offset: Long): Unit = {

    if (producerEpoch != updatedEntry.producerEpoch) {

    if (appendFirstSeq != 0) {

    if (updatedEntry.producerEpoch != RecordBatch.NO_PRODUCER_EPOCH) {

    throw new OutOfOrderSequenceException(s"Invalid sequence number for new epoch at offset $offset in " +

    s"partition $topicPartition: $producerEpoch (request epoch), $appendFirstSeq (seq. number)")

    } else {

    throw new UnknownProducerIdException(s"Found no record of producerId=$producerId on the broker at offset $offset" +

    s"in partition $topicPartition. It is possible that the last message with the producerId=$producerId has " +

    "been removed due to hitting the retention limit.")

    }

    }

    } else {

    val currentLastSeq = if (!updatedEntry.isEmpty)

    updatedEntry.lastSeq

    else if (producerEpoch == currentEntry.producerEpoch)

    currentEntry.lastSeq

    else

    RecordBatch.NO_SEQUENCE

    if (currentLastSeq == RecordBatch.NO_SEQUENCE && appendFirstSeq != 0) {

ne throw mew UnknownProducerIdException(s"Local producer state matches expected epoch $producerEpoch " +

    s"for producerId=$producerId at offset $offset in partition $topicPartition, but the next expected " +

    "sequence number is not known.")

    } else if (!inSequence(currentLastSeq, appendFirstSeq)) {

    throw new OutOfOrderSequenceException(s"Out of order sequence number for producerId $producerId at " +

    s"offset $offset in partition $topicPartition: $appendFirstSeq (incoming seq. number), " +

    s"$currentLastSeq (current end sequence number)")

    }

    }

    }

    private def inSequence(lastSeq: Int, nextSeq: Int): Boolean = {

    nextSeq == lastSeq + 1L || (nextSeq == 0 && lastSeq == Int.MaxValue)

    }



引申:Kafka producer 对有序性做了哪些处理


假设我们有5个请求,batch1、batch2、batch3、batch4、batch5;如果只有batch2 ack failed,3、4、5都保存了,那2将会随下次batch重发而造成重复。我们可以设置max.in.flight.requests.per.connection=1(客户端在单个连接上能够发送的未响应请求的个数)来解决乱序,但降低了系统吞吐。
新版本kafka设置enable.idempotence=true后能够动态调整max-in-flight-request。正常情况下max.in.flight.requests.per.connection 大于1。当重试请求到来且时,batch 会根据 seq重新添加到队列的合适位置,并把max.in.flight.requests.per.connection设为1, 这样它 前面的 batch序号都比它小,只有前面的都发完了,它才能发。

    private void insertInSequenceOrder(Deque<ProducerBatch> deque, ProducerBatch batch) {

    // When we are requeing and have enabled idempotence, the reenqueued batch must always have a sequence.

    if (batch.baseSequence() == RecordBatch.NO_SEQUENCE)

    throw new IllegalStateException("Trying to re-enqueue a batch which doesn't have a sequence even " +

    "though idempotency is enabled.");

    if (transactionManager.nextBatchBySequence(batch.topicPartition) == null)

    throw new IllegalStateException("We are re-enqueueing a batch which is not tracked as part of the in flight " +

    "requests. batch.topicPartition: " + batch.topicPartition + "; batch.baseSequence: " + batch.baseSequence());

    ProducerBatch firstBatchInQueue = deque.peekFirst();

    if (firstBatchInQueue != null && firstBatchInQueue.hasSequence() && firstBatchInQueue.baseSequence() < batch.baseSequence()) {

    List<ProducerBatch> orderedBatches = new ArrayList<>();

    while (deque.peekFirst() != null && deque.peekFirst().hasSequence() && deque.peekFirst().baseSequence() < batch.baseSequence())

    orderedBatches.add(deque.pollFirst());

    log.debug("Reordered incoming batch with sequence {} for partition {}. It was placed in the queue at " +

    "position {}", batch.baseSequence(), batch.topicPartition, orderedBatches.size())

    deque.addFirst(batch);

    // Now we have to re insert the previously queued batches in the right order.

    for (int i = orderedBatches.size() - 1; i >= 0; --i) {

    deque.addFirst(orderedBatches.get(i));

    }

    // At this point, the incoming batch has been queued in the correct place according to its sequence.

    } else {

    deque.addFirst(batch);

    }

    }


多会话幂等性


在单会话幂等性中介绍,kafka通过引入pid和seq来实现单会话幂等性,但正是引入了pid,当应用重启时,新的producer并没有old producer的状态数据。可能重复保存。

Kafka事务通过隔离机制来实现多会话幂等性


kafka事务引入了transactionId 和Epoch,设置transactional.id后,一个transactionId只对应一个pid, 且Server 端会记录最新的 Epoch 值。这样有新的producer初始化时,会向TransactionCoordinator发送InitPIDRequest请求, TransactionCoordinator 已经有了这个 transactionId对应的 meta,会返回之前分配的 PID,并把 Epoch 自增 1 返回,这样当old
producer恢复过来请求操作时,将被认为是无效producer抛出异常。     如果没有开启事务,TransactionCoordinator会为新的producer返回new pid,这样就起不到隔离效果,因此无法实现多会话幂等。

    private def maybeValidateAppend(producerEpoch: Short, firstSeq: Int, offset: Long): Unit = {

    validationType match {

    case ValidationType.None =>

    case ValidationType.EpochOnly =>

    checkProducerEpoch(producerEpoch, offset)

    case ValidationType.Full => //开始事务,执行这个判断

    checkProducerEpoch(producerEpoch, offset)

    checkSequence(producerEpoch, firstSeq, offset)

    }

    }

    private def checkProducerEpoch(producerEpoch: Short, offset: Long): Unit = {

    if (producerEpoch < updatedEntry.producerEpoch) {

    throw new ProducerFencedException(s"Producer's epoch at offset $offset is no longer valid in " +

    s"partition $topicPartition: $producerEpoch (request epoch), ${updatedEntry.producerEpoch} (current epoch)")

    }

    }


04 Consumer端幂等性


如上所述,consumer拉取到消息后,把消息交给线程池workers,workers对message的handle可能包含异步操作,又会出现以下情况:


  • 先commit,再执行业务逻辑:提交成功,处理失败 。造成丢失




  • 先执行业务逻辑,再commit:提交失败,执行成功。造成重复执行




  • 先执行业务逻辑,再commit:提交成功,异步执行fail。造成丢失




对此我们常用的方法时,works取到消息后先执行如下code:


    if(cache.contain(msgId)){

    // cache中包含msgId,已经处理过

    continue;

    }else {

    lock.lock();

    cache.put(msgId,timeout);

    commitSync();

    lock.unLock();

    }

    // 后续完成所有操作后,删除cache中的msgId,只要msgId存在cache中,就认为已经处理过。Note:需要给cache设置有消息


作者:WeCoding
链接:https://juejin.cn/post/6844903909153062926
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Toast必须在UI(主)线程使用?

背景 依稀记得,从最开始干Android这一行就经常听到有人说:toast(吐司)不能在子线程调用显示,只能在UI(主)线程调用展示。 非常惭愧的是,我之前也这么认为,并且这个问题也一直没有深究。 直至前两天我的朋友 “林小海” 同学说toast不能在子线程中...
继续阅读 »

背景


依稀记得,从最开始干Android这一行就经常听到有人说:toast(吐司)不能在子线程调用显示,只能在UI(主)线程调用展示。


非常惭愧的是,我之前也这么认为,并且这个问题也一直没有深究。


直至前两天我的朋友 “林小海” 同学说toast不能在子线程中显示,这句话使我突然想起了点什么。


我觉得我有必要证明、并且纠正一下。


toast不能在子线程调用展示的结论真的是谬论~


疑点


前两天在说到这个toast的时候一瞬间对于只能在UI线程中调用展示的说法产生了两个疑点:




  1. 在子线程更新UI一般都会有以下报错提示:


    Only the original thread that created a view hierarchy can touch its views.


    但是,我们在子线程直接toast的话,报错的提示如下:


    Can't toast on a thread that has not called Looper.prepare()


    明显,两个报错信息是不一样的,从toast这条来看的话是指不能在一个没有调用Looper.prepare()的线程里面进行toast,字面意思上有说是不能在子线程更新UI吗?No,没有!这也就有了下面第2点的写法。




  2. 曾见过一种在子线程使用toast的用法如下(正是那时候没去深究这个问题):




        new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(MainActivity.this.getApplicationContext(),"SHOW",Toast.LENGTH_SHORT).show();
Looper.loop();
}
}).start();

关于Looper这个东西,我想大家都很熟悉了,我就不多说looper这块了,下面主要分析一下为什么这样的写法就可以在子线程进行toast了呢?


并且Looper.loop()这个函数调用后是会阻塞轮循的,这种写法是会导致线程没有及时销毁,在toast完之后我特意给大家用如下代码展示一下这个线程的状态:


    Log.d("Wepon", "isAlive:"+t[0].isAlive());
Log.d("Wepon", "state:" + t[0].getState());

D/Wepon: isAlive:true
D/Wepon: state:RUNNABLE

可以看到,线程还活着,没有销毁掉。当然,这种代码里面如果想要退出looper的循环以达到线程可以正常销毁的话是可以使用looper.quit相关的函数的,但是这个调用quit的时机却是不好把握的。


下面将通过Toast相关的源码来分析一下为什么会出现上面的情况?


源码分析


Read the fuck source code.


1.分析Toast.makeText()方法


首先看我们的调用Toast.makeText,makeText这个函数的源码:


    // 这里一般是我们外部调用Toast.makeText(this, "xxxxx", Toast.LENGTH_SHORT)会进入的方法。
// 然后会调用下面的函数。
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}

/**
* Make a standard toast to display using the specified looper.
* If looper is null, Looper.myLooper() is used. // 1. 注意这一句话
* @hide
*/
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {

// 2. 构造toast实例,有传入looper,此处looper为null
Toast result = new Toast(context, looper);

LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);

result.mNextView = v;
result.mDuration = duration;

return result;
}

从上面的源码中看第1点注释,looper为null的时候会调用Looper.myLooper(),这个方法的作用是取我们线程里面的looper对象,这个调用是在Toast的构造函数里面发生的,看我们的Toast构造函数:


2.分析Toast构造函数


    /**
* Constructs an empty Toast object. If looper is null, Looper.myLooper() is used.
* @hide
*/
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
// 1.此处创建一个TN的实例,传入looper,接下来主要分析一下这个TN类
mTN = new TN(context.getPackageName(), looper);
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(
com.android.internal.R.integer.config_toastDefaultGravity);
}

TN的构造函数如下,删除了一部分不重要代码:


TN(String packageName, @Nullable Looper looper) {
// .....
// ..... 省略部分源码,这
// .....

// 重点
// 2.判断looper == null,这里我们从上面传入的时候就是null,所以会进到里面去。
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
// 3.然后会调用Looper.myLooper这个函数,也就是会从ThreadLocal<Looper> sThreadLocal 去获取当前线程的looper。
// 如果ThreadLocal这个不太清楚的可以先去看看handler源码分析相关的内容了解一下。
looper = Looper.myLooper();
if (looper == null) {
// 4.这就是报错信息的根源点了!!
// 没有获取到当前线程的looper的话,就会抛出这个异常。
// 所以分析到这里,就可以明白为什么在子线程直接toast会抛出这个异常
// 而在子线程中创建了looper就不会抛出异常了。
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
// 5.这里不重点讲toast是如何展示出来的源码了,主要都在TN这个类里面,
// Toast与TN中间有涉及aidl跨进程的调用,这些可以看看源码。
// 大致就是:我们的show方法实际是会往这个looper里面放入message的,
// looper.loop()会阻塞、轮循,
// 当looper里面有Message的时候会将message取出来,
// 然后会通过handler的handleMessage来处理。
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
// .... 省略代码
case SHOW: // 显示,与WindowManager有关,这部分源码不做说明了,可以自己看看,就在TN类里面。
case HIDE: // 隐藏
case CANCEL: // 取消
}
}
};
}

总结


从第1点可以看到会创建TN的实例,并传入looper,此时的looper还是null。


进入TN的构造函数可以看到会有looper是否为null的判断,并且在当looper为null时,会从当前线程去获取looper(第3点,Looper.myLooper()),如果还是获取不到,刚会抛出我们开头说的这个异常信息:Can't toast on a thread that has not called Looper.prepare()。


而有同学会误会只能在UI线程toast的原因是:UI(主)线程在刚创建的时候就有创建looper的实例了,在主线程toast的话,会通过Looper.myLooper()获取到对应的looper,所以不会抛出异常信息。


而不能直接在子线程程中toast的原因是:子线程中没有创建looper的话,去通过Looper.myLooper()获取到的为null,就会throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");


另外,两个点说明一下:



  1. Looper.prepare() 是创建一个looper并通过ThreadLocal跟当前线程关联上,也就是通过sThreadLocal.set(new Looper(quitAllowed));

  2. Looper.loop()是开启轮循,有消息就会处理,没有的话就会阻塞。


综上,“Toast必须在UI(主)线程使用”这个说法是不对滴!,以后千万不要再说toast只能在UI线程显示啦.....


作者:ywp0919
链接:https://juejin.cn/post/6844904103538065422
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

手把手教你用Flutter搭建属于自己的个人博客

Flutter 2.0以来已经稳定支持web的开发,现在来教大家使用Flutter搭建一个个人的博客网站,使用Github提供的Actions、gh-pages服务,毕竟一时白票一时爽,一直白嫖一直爽。 1. 使用AndoridStuido创建一个Flutte...
继续阅读 »

Flutter 2.0以来已经稳定支持web的开发,现在来教大家使用Flutter搭建一个个人的博客网站,使用Github提供的Actions、gh-pages服务,毕竟一时白票一时爽,一直白嫖一直爽。


1. 使用AndoridStuido创建一个Flutter项目


Dingtalk_20210512195651.jpg


2. Github注册一个账号,并且创建一个Repository


Dingtalk_20210512195915.jpg


3. 上传创建的Flutter项目到这个Repository的master分支中


4. 获取Github的access token


新建一个access token
Dingtalk_20210512200242.jpg
保存token,等下要用


Dingtalk_20210512200516.jpg


5. 配置Actions secrets,name随便填写,value填入刚刚获取的token


Dingtalk_20210512200640.jpg


6.配置Actions


Dingtalk_20210512200843.jpg


Dingtalk_20210512201013.jpg


需要填写的规则


name: Flutter Web
on:
push:
branches:
- master
jobs:
build:
name: Build Web
env:
my_secret: ${{secrets.commit_secret}}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: subosito/flutter-action@v1
with:
channel: 'dev'
- run: flutter pub get
- run: flutter build web --release
- run: |
cd build/web
git init
git config --global user.email aaa
git config --global user.name bbb
git status
git remote add origin https://${{secrets.commit_secret}}@github.com/xxx/yyy.git
git checkout -b gh-pages
git add --all
git commit -m "update"
git push origin gh-pages -f

aaa-你的邮箱 bbb替-你的名称 xxx-你的git名字 yyy为-Repository名字


然后我们每次提交修改到master上时,Actions都会自动帮我们打包web到gh-pages分支上,完成Actions后,我们可以查看flutter构建的博客网站,一般网址为https://你的git名字.github.io/Repository名字/。


这里记得注意的是需要修改web目录下index.html中


<base href="/">
修改为Repository的名字
<base href="/flutter_blog/">

不然在打开网页的时候会找不到资源。


作者:gstory
链接:https://juejin.cn/post/6961399282872107015
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android 关键字高亮

前言项目中经常会遇到需要对关键字加特殊色值显示,不管是搜索内容还是列表关键字展示,对于特殊文字或者词组高亮是一种很常见的需求,Android 没有自带这样的工具或者组件提供,但是我们可以自己实现一个这样的工具类,用到的地方直接调用就好了。文字高亮所谓文字高亮,...
继续阅读 »

前言

项目中经常会遇到需要对关键字加特殊色值显示,不管是搜索内容还是列表关键字展示,对于特殊文字或者词组高亮是一种很常见的需求,Android 没有自带这样的工具或者组件提供,但是我们可以自己实现一个这样的工具类,用到的地方直接调用就好了。

文字高亮

所谓文字高亮,其实就是针对某个字符做特殊颜色显示,下面列举几种常见的实现方式

一、通过加载Html标签,显示高亮

Android 的TextView 可以加载带Html标签的段落,方法:

textView.setText(Html.fromHtml("<font color='red' size='24'>Hello World</font>"));

那么要高亮显示关键字,就可以这样实现,把需要高亮显示的关键字,通过这样的方式,组合起来就好了,例如:

textView.setText(“这是我的第一个安卓项目” + Html.fromHtml("<font color='red' size='24'>Hello World</font>"));

二、通过SpannableString来实现文本高亮

先来简单了解下SpannableString

SpannableString的基本使用代码示例:

//设置Url地址连接
private void addUrlSpan() {
SpannableString spanString = new SpannableString("超链接");
URLSpan span = new URLSpan("tel:0123456789");
spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.append(spanString);
}

//设置字体背景的颜色
private void addBackColorSpan() {
SpannableString spanString = new SpannableString("文字背景颜色");
BackgroundColorSpan span = new BackgroundColorSpan(Color.YELLOW);
spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.append(spanString);
}
//设置字体的颜色
private void addForeColorSpan() {
SpannableString spanString = new SpannableString("文字前景颜色");
ForegroundColorSpan span = new ForegroundColorSpan(Color.BLUE);
spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.append(spanString);
}
//设置字体的大小
private void addFontSpan() {
SpannableString spanString = new SpannableString("36号字体");
AbsoluteSizeSpan span = new AbsoluteSizeSpan(36);
spanString.setSpan(span, 0, 5, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.append(spanString);
}

以上是比较常用的,还有其他例如设置字体加粗,下划线,删除线等,都可以实现

我们这里主要用到给字体设置背景色,通过正则表达式匹配关键字,设置段落中匹配到的关键字高亮

/***
* 指定关键字高亮 字符串整体高亮
* @param originString 原字符串
* @param keyWords 高亮字符串
* @param highLightColor 高亮色值
* @return 高亮后的字符串
*/
public static SpannableString getHighLightWord(String originString, String keyWords, int highLightColor) {
SpannableString originSpannableString = new SpannableString(originString);
if (!TextUtils.isEmpty(keyWords)) {
Pattern pattern = Pattern.compile(keyWords);
Matcher matcher = pattern.matcher(originSpannableString);
while (matcher.find()) {
int startIndex = matcher.start();
int endIndex = matcher.end();
originSpannableString.setSpan(new ForegroundColorSpan(highLightColor), startIndex, endIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return originSpannableString;
}

在扩展一下,可以支持关键字,关键词拆分显示

类似:测试1234测1234试(测试为高亮字,实现测试/测/试分别高亮)

/***
* 指定关键字高亮 支持分段高亮
* @param originString
* @param keyWords
* @param highLightColor
* @return
*/
public static SpannableString getHighLightWords(String originString, String keyWords, int highLightColor) {
SpannableString originSpannableString = new SpannableString(originString);
if (!TextUtils.isEmpty(keyWords)) {
for (int i = 0; i < keyWords.length(); i++) {
Pattern p = Pattern.compile(String.valueOf(keyWords.charAt(i)));
Matcher m = p.matcher(originSpannableString);
while (m.find()) {
int start = m.start();
int end = m.end();
originSpannableString.setSpan(new ForegroundColorSpan(highLightColor), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
return originSpannableString;
}

字符可以,那么数组呢,是不是也可以实现了?

/***
* 指定关键字数组高亮
* @param originString 原字符串
* @param keyWords 高亮字符串数组
* @param highLightColor 高亮色值
* @return 高亮后的字符串
*/
public static SpannableString getHighLightWordsArray(String originString, String[] keyWords, int highLightColor) {
SpannableString originSpannableString = new SpannableString(originString);
if (keyWords != null && keyWords.length > 0) {
for (int i = 0; i < keyWords.length; i++) {
Pattern p = Pattern.compile(keyWords[i]);
Matcher m = p.matcher(originSpannableString);
while (m.find()) {
int start = m.start();
int end = m.end();
originSpannableString.setSpan(new ForegroundColorSpan(highLightColor), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
return originSpannableString;
}

总结

这样不管来什么需求,是不是都可以满足了,随便产品经理提,要什么给什么

收起阅读 »

聊一聊Android开发利器之adb

学无止境,有一技旁身,至少不至于孤陋寡闻。adb的全称为Android Debug Bridge,通过adb可以方便我们调试Android程序。作为移动端开发相关的同学,掌握所需要的adb操作命令是非常必须的,就把平时工作中用得相对比较多的adb命令做个梳理。...
继续阅读 »

学无止境,有一技旁身,至少不至于孤陋寡闻。

adb的全称为Android Debug Bridge,通过adb可以方便我们调试Android程序。作为移动端开发相关的同学,掌握所需要的adb操作命令是非常必须的,就把平时工作中用得相对比较多的adb命令做个梳理。(日常中把adb操作命令搭配shell alias使用起来更方便)

ADB常用命令

1.启动/停止adb server命令

adb start-server  //启动命令
adb kill-server //停止命令

2. 通过adb查看设备相关信息

  1. 查询已连接设备/模拟器
    adb devices
  2. 查看手机型号
    adb shell getprop ro.product.model
  3. 查看电池状况
    adb shell dumpsys battery
  4. 查看屏幕分辨率
    adb shell wm size
  5. 查看屏幕密度
    adb shell wm density
  6. 查看显示屏参数
    adb shell dumpsys window displays
  7. 查看Android系统版本
    adb shell getprop ro.build.version.release
  8. 查看CPU信息
    adb shell cat /proc/cpuinfo
  9. 查看手机CPU架构
    adb shell getprop ro.product.cpu.abi
  10. 查看内存信息
    adb shell cat /proc/meminfo

3. 通过adb连接设备命令

adb [-d|-e|-s ]
如果只有一个设备/模拟器连接时,可以省略掉 [-d|-e|-s ] 这一部分,直接使用 adb即可 。 如果有多个设备/模拟器连接,则需要为命令指定目标设备。

参数含义
-d指定当前唯一通过 USB 连接的 Android 设备为命令目标
-e指定当前唯一运行的模拟器为命令目标
-s <serialNumber>指定相应 serialNumber 号的设备/模拟器为命令目标
在多个设备/模拟器连接的情况下较常用的是-s参数,serialNumber 可以通过adb devices命令获取。如:
$ adb devices
List of devices attached
cfxxxxxx device
emulator-5554 device
10.xxx.xxx.x:5555 device

输出里的 cfxxxxxxemulator-5554 和 10.xxx.xxx.x:5555 即为 serialNumber。 比如这时想指定 cfxxxxxx 这个设备来运行 adb 命令 获取屏幕分辨率:

adb -s cfxxxxxx shell wm size

安装应用:

adb -s cfxxxxxx install hello.apk

遇到多设备/模拟器的情况均使用这几个参数为命令指定目标设备。

4. 通过adb在设备上操作应用相关

  1. 安装 APK

    adb install [-rtsdg] <apk_path>

    参数:
    adb install 后面可以跟一些可选参数来控制安装 APK 的行为,可用参数及含义如下:

    参数含义
    -r允许覆盖安装
    -t允许安装 AndroidManifest.xml 里 application 指定 android:testOnly="true" 的应用
    -s将应用安装到 sdcard
    -d允许降级覆盖安装
    -g授予所有运行时权限
  2. 卸载应用

    adb uninstall [-k] <packagename>

    <packagename> 表示应用的包名,-k 参数可选,表示卸载应用但保留数据和缓存目录。

    adb uninstall com.vic.dynamicview
  3. 强制停止应用

    adb shell am force-stop <packagename>

    命令示例:

    adb shell am force-stop com.vic.dynamicview
  4. 调起对应的Activity

    adb shell am start [options] <INTENT>

    例如:

    adb shell am start -n com.vic.dynamicview/.MainActivity --es "params" "hello, world"

    表示调起 com.vic.dynamicview/.MainActivity 并传给它 string 数据键值对 params - hello, world。

  5. 查看前台 Activity

    adb shell dumpsys activity activities | grep ResumedActivity

    查看activity堆栈信息: adb shell dumpsys activity

    ACTIVITY MANAGER PENDING INTENTS (adb shell dumpsys activity intents)
    ...
    ACTIVITY MANAGER BROADCAST STATE (adb shell dumpsys activity broadcasts)
    ...
    ACTIVITY MANAGER CONTENT PROVIDERS (adb shell dumpsys activity providers)
    ...
    ACTIVITY MANAGER SERVICES (adb shell dumpsys activity services)
    ...
    ACTIVITY MANAGER ACTIVITIES (adb shell dumpsys activity activities)
    ...
    ACTIVITY MANAGER RUNNING PROCESSES (adb shell dumpsys activity processes)
    ...
  6. 打开系统设置:
    adb shell am start -n com.android.settings/com.android.settings.Settings

  7. 打开开发者选项:
    adb shell am start -a com.android.settings.APPLICATION_DEVELOPMENT_SETTINGS

  8. 进入WiFi设置
    adb shell am start -a android.settings.WIRELESS_SETTINGS

  9. 重启系统
    adb reboot

5. 通过adb操作日志相关

  1. logcathelp帮助信息
    adb logcat --help 可以查看logcat帮助信息
    adb logcat 命令格式: adb logcat [选项] [过滤项], 其中 选项 和 过滤项 在 中括号 [] 中, 说明这是可选的;

  2. 输出日志信息到文件:
    ">"输出 :
    ">" 后面跟着要输出的日志文件, 可以将 logcat 日志输出到文件中, 使用 adb logcat > log 命令, 使用 more log 命令查看日志信息;
    如:adb logcat > ~/logdebug.log

  3. 输出指定标签内容:
    "-s"选项 : 设置默认的过滤器, 如 我们想要输出 "System.out" 标签的信息, 就可以使用 adb logcat -s System.out 命令;

  4. 清空日志缓存信息:
    使用 adb logcat -c 命令, 可以将之前的日志信息清空, 重新开始输出日志信息;

  5. 输出缓存日志:
    使用 adb logcat -d 命令, 输出命令, 之后退出命令, 不会进行阻塞;

  6. 输出最近的日志:
    使用 adb logcat -t 5 命令, 可以输出最近的5行日志, 并且不会阻塞;

  7. 日志过滤:
    注意:在windows上不能使用grep关键字,可以用findstr代替grep.

    • 过滤固定字符串:
      adb logcat | grep logtag
      adb logcat | grep -i logtag #忽略大小写。
      adb logcat | grep logtag > ~/result.log #将过滤后的日志输出到文件
      adb logcat | grep --color=auto -i logtag #设置匹配字符串颜色。

    • 使用正则表达式匹配
      adb logcat | grep "^..Activity"

ADB其他命令

1. 清除应用数据与缓存

adb shell pm clear <packagename>

<packagename> 表示应用名包,这条命令的效果相当于在设置里的应用信息界面点击了「清除缓存」和「清除数据」。

adb shell pm clear com.xxx.xxx

2. 与应用交互操作

主要是使用 am <command> 命令,常用的 <command> 如下:

command用途
start [options] <INTENT>启动 <INTENT> 指定的 Activity
startservice [options] <INTENT>启动 <INTENT> 指定的 Service
broadcast [options] <INTENT>发送 <INTENT> 指定的广播
force-stop <packagename>停止 <packagename> 相关的进程

<INTENT> 参数很灵活,和写 Android 程序时代码里的 Intent 相对应。

用于决定 intent 对象的选项如下:

参数含义
-a <ACTION>指定 action,比如 android.intent.action.VIEW
-c <CATEGORY>指定 category,比如 android.intent.category.APP_CONTACTS
-n <COMPONENT>指定完整 component 名,用于明确指定启动哪个 Activity,如 com.example.app/.ExampleActivity

<INTENT> 里还能带数据,就像写代码时的 Bundle 一样:

参数含义
--esn <EXTRA_KEY>null 值(只有 key 名)
-e--es <EXTRA_KEY> <EXTRA_STRING_VALUE>`
--ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE>boolean 值
--ei <EXTRA_KEY> <EXTRA_INT_VALUE>integer 值
--el <EXTRA_KEY> <EXTRA_LONG_VALUE>long 值
--ef <EXTRA_KEY> <EXTRA_FLOAT_VALUE>float 值
--eu <EXTRA_KEY> <EXTRA_URI_VALUE>URI
--ecn <EXTRA_KEY> <EXTRA_COMPONENT_NAME_VALUE>component name
--eia <EXTRA_KEY> <EXTRA_INT_VALUE>[,<EXTRA_INT_VALUE...]integer 数组
--ela <EXTRA_KEY> <EXTRA_LONG_VALUE>[,<EXTRA_LONG_VALUE...]long 数组
  1. 调起Activity

    adb shell am start [options] <INTENT>

    例如:

    adb shell am start -n com.cc.test/.MainActivity --es "params" "hello, world"

    表示调起 com.cc.test/.MainActivity 并传给它 string 数据键值对 params - hello, world。

  2. 调起Service

    adb shell am startservice [options] <INTENT>

    例如:

    adb shell am startservice -n com.tencent.mm/.plugin.accountsync.model.AccountAuthenticatorService
  3. 发送广播

    adb shell am broadcast [options] <INTENT>

    可以向所有组件广播,也可以只向指定组件广播。 例如,向所有组件广播 BOOT_COMPLETED:

    adb shell am broadcast -a android.intent.action.BOOT_COMPLETED

    又例如,只向 com.cc.test/.BootCompletedReceiver 广播 BOOT_COMPLETED:

    adb shell am broadcast -a android.intent.action.BOOT_COMPLETED -n com.cc.test/.BootCompletedReceiver
  4. 撤销应用程序的权限

    1. 向应用授予权限。只能授予应用程序声明的可选权限
    adb shell pm grant <packagename> <PACKAGE_PERMISSION>

    例如:adb -d shell pm grant packageName android.permission.BATTERY_STATS

    1. 取消应用授权
    adb shell pm revoke <packagename> <PACKAGE_PERMISSION>

3. 模拟按键/输入

Usage: input [<source>] <command> [<arg>...]

The sources are:
mouse
keyboard
joystick
touchnavigation
touchpad
trackball
stylus
dpad
gesture
touchscreen
gamepad

The commands and default sources are:
text <string> (Default: touchscreen)
keyevent [--longpress] <key code number or name> ... (Default: keyboard)
tap <x> <y> (Default: touchscreen)
swipe <x1> <y1> <x2> <y2> [duration(ms)] (Default: touchscreen)
press (Default: trackball)
roll <dx> <dy> (Default: trackball)

比如模拟点击://在屏幕上点击坐标点x=50 y=250的位置。

adb shell input tap 50 250

结合shell alias使用adb

shell终端的别名只是命令的简写,有类似键盘快捷键的效果。如果你经常执行某个长长的命令,可以给它起一个简短的化名。使用alias命令列出所有定义的别名。
你可以在~/.bashrc(.zshrc)文件中直接定义别名如alias logRunActivity="adb shell dumpsys activity activities | grep 'Run*'",也可以新创建一个文件如.byterc, 然后在当前shell对应的文件中.bashrc或者.zshrc 中增加source ~/.byterc,重新source配置,使得配置生效,即可使别名全局生效。使用别名可以节省时间、提高工作效率。

如何添加别名alias

下面在MAC环境采用新建文件形式添加别名,步骤如下:

  1. 新建.byterc 文件
    • 如果已经新建,直接打开
      open ~/.byterc
    • 没有新建,则新建后打开
      新建: touch ~/.byterc
      打开:open ~/.byterc
  2. 在.zshrc中添加source ~/.byterc
  3. 在打开的.byterc文件中定义别名
    alias logRunActivity="adb shell dumpsys activity activities | grep 'Run*'"
    Android同学应该知道作用就是查看当前设备运行的Activity信息
  4. 重新source配置,使得配置生效
    $ source ~/.byterc
    如果不是新建文件,直接使用.bashrc或者.zshrc ,直接source对应的配置即可,如:$ source ~/.zshrc .
  5. 此时在命令行中直接执行logRunActivity 即可查看当前设备运行的Activity信息。

注意: 可使用$ alias查看当前有设置哪些别名操作。


收起阅读 »

必学必知的自定义View基础

前言自定义View原理是Android开发者必须了解的基础;在了解自定义View之前,你需要有一定的知识储备;本文将全面解析关于自定义View中的所有知识基础。目录1. 视图定义即日常说的View,具体表现为显示在屏幕上的各种视图控件,如TextView、Li...
继续阅读 »

前言

  • 自定义View原理是Android开发者必须了解的基础;
  • 在了解自定义View之前,你需要有一定的知识储备;
  • 本文将全面解析关于自定义View中的所有知识基础。

目录

示意图


1. 视图定义

即日常说的View,具体表现为显示在屏幕上的各种视图控件,如TextView、LinearLayout等。


2. 视图分类

视图View主要分为两类:

  • 单一视图:即一个View、不包含子View,如TextView
  • 视图组,即多个View组成的ViewGroup、包含子View,如LinearLayout

Android中的UI组件都由View、ViewGroup共同组成。


3. 视图类简介

  • 视图的核心类是:View类
  • View类是Android中各种组件的基类,如View是ViewGroup基类
  • View的构造函数:共有4个,具体如下:

自定义View必须重写至少一个构造函数:

// 构造函数1
// 调用场景:View是在Java代码里面new的
public CarsonView(Context context) {
super(context);
}

// 构造函数2
// 调用场景:View是在.xml里声明的
// 自定义属性是从AttributeSet参数传进来的
public CarsonView(Context context, AttributeSet attrs) {
super(context, attrs);
}

// 构造函数3
// 应用场景:View有style属性时
// 一般是在第二个构造函数里主动调用;不会自动调用
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

// 构造函数4
// 应用场景:View有style属性时、API21之后才使用
// 一般是在第二个构造函数里主动调用;不会自动调用
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}

更加具体的使用请看:深入理解View的构造函数和 理解View的构造函数


4. 视图结构

  • 对于包含子View的视图组(ViewGroup),结构是树形结构
  • ViewGroup下可能有多个ViewGroup或View,如下图:

这里需要特别注意的是:在View的绘制过程中,永远都是从View树结构的根节点开始(即从树的顶端开始),一层一层、一个个分支地自上而下遍历进行(即树形递归),最终计算整个View树中各个View,从而最终确定整个View树的相关属性。


5. Android坐标系

Android的坐标系定义为:

  • 屏幕的左上角为坐标原点
  • 向右为x轴增大方向
  • 向下为y轴增大方向

具体如下图:

注:区别于一般的数学坐标系

两者坐标系的区别


6. View位置(坐标)描述

视图的位置由四个顶点决定,如图1-3所示的A、B、C、D。

视图的位置是相对于父控件而言的,四个顶点的位置描述分别由四个与父控件相关的值决定:

  • 顶部(Top):视图上边界到父控件上边界的距离;
  • 左边(Left):视图左边界到父控件左边界的距离;
  • 右边(Right):视图右边界到父控件左边界的距离;
  • 底部(Bottom):视图下边界到父控件上边界的距离。

具体如图1-4所示。

可根据视图位置的左上顶点、右下顶点进行记忆:

  • 顶部(Top):视图左上顶点到父控件上边界的距离;
  • 左边(Left):视图左上顶点到父控件左边界的距离;
  • 右边(Right):视图右下顶点到父控件左边界的距离;
  • 底部(Bottom):视图右下顶点到父控件上边界的距离。

7. 位置获取方式

视图的位置获取是通过View.getXXX()方法进行获取。

获取顶部距离(Top):getTop()
获取左边距离(Left):getLeft()
获取右边距离(Right):getRight()
获取底部距离(Bottom):getBottom()
  • 与MotionEvent中 get() getRaw()的区别
//get() :触摸点相对于其所在组件坐标系的坐标
event.getX();
event.getY();

//getRaw() :触摸点相对于屏幕默认坐标系的坐标
event.getRawX();
event.getRawY();

具体如下图:

get() 和 getRaw() 的区别


8. 角度(angle)& 弧度(radian)

  • 自定义View实际上是将一些简单的形状通过计算,从而组合到一起形成的效果。

这会涉及到画布的相关操作(旋转)、正余弦函数计算等,即会涉及到角度(angle)与弧度(radian)的相关知识。

  • 角度和弧度都是描述角的一种度量单位,区别如下图::

角度和弧度区别

在默认的屏幕坐标系中角度增大方向为顺时针。

屏幕坐标系角度增大方向

注:在常见的数学坐标系中角度增大方向为逆时针


9. 颜色相关

Android中的颜色相关内容包括颜色模式,创建颜色的方式,以及颜色的混合模式等。

9.1 颜色模式

Android支持的颜色模式主要包括:

  • ARGB8888:四通道高精度(32位)
  • ARGB4444:四通道低精度(16位)
  • RGB565:Android屏幕默认模式(16位)
  • Alpha8:仅有透明通道(8位)

这里需要特别注意的是:

  • 字母:表示通道类型;
  • 数值:表示该类型用多少位二进制来描述;
  • 示例说明:ARGB8888,表示有四个通道(ARGB);每个对应的通道均用8位来描述。

以ARGB8888为例介绍颜色定义:

ARGB88888

9.2 颜色定义

主要分为xml定义 / java定义。

/**
* 定义方式1:xml
* 在/res/values/color.xml文件中定义
*/
<?xml version="1.0" encoding="utf-8"?>
<resources>
//定义了红色(没有alpha(透明)通道)
<color name="red">#ff0000</color>
//定义了蓝色(没有alpha(透明)通道)
<color name="green">#00ff00</color>
</resources>

// 在xml文件中以”#“开头定义颜色,后面跟十六进制的值,有如下几种定义方式:
#f00 //低精度 - 不带透明通道红色
#af00 //低精度 - 带透明通道红色
#ff0000 //高精度 - 不带透明通道红色
#aaff0000 //高精度 - 带透明通道红色

/**
* 定义方式2:Java
*/
// 使用Color类定义颜色
int color = Color.GRAY; //灰色

// Color类使用ARGB值表示
int color = Color.argb(127, 255, 0, 0); //半透明红色
int color = 0xaaff0000; //带有透明度的红色

9.3 颜色引用

主要分为xml定义 / java定义。

/**
* 引用方式1:xml
*/
// 1. 在style文件中引用
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/red</item>
</style>
// 2. 在layout文件中引用
android:background="@color/red"
// 3. 在layout文件中创建并使用颜色
android:background="#ff0000"

/**
* 引用方式2:Java
*/
//方法1
int color = getResources().getColor(R.color.mycolor);

//方法2(API 23及以上)
int color = getColor(R.color.myColor);

9.4 取色工具

  • 颜色都是用RGB值定义的,而我们一般是无法直观的知道自己需要颜色的值,需要借用取色工具直接从图片或者其他地方获取颜色的RGB值。
  • 有时候一些简单的颜色选取就不用去麻烦UI了,开发者自己去选取效率更高
  • 这里,取色工具我强推Markman:一款设计师用于标注的工具,主要用于尺寸标注、字体大小标注、颜色标注,而且使用简单。本人强烈推荐!




收起阅读 »

CoordinatorLayout与AppBarLayout。置顶悬停,二级悬停,类似京东、淘宝等二级悬停。

类似京东、淘宝等二级悬停。 参考+实践 一、惯例先上效果图 二、GitHub 代码地址,欢迎指正https://github.com/MNXP/SlideTop 三、XML布局主要用到的控件 1、PullRefreshLayout (借用这位大神的ht...
继续阅读 »

类似京东、淘宝等二级悬停。
参考+实践




一、惯例先上效果图


效果图


二、GitHub

三、XML布局主要用到的控件


  1、PullRefreshLayout (借用这位大神的https://github.com/genius158/PullRefreshLayout)
2、CoordinatorLayout
3、AppBarLayout

四、实现

1、布局的实现



  需要注意的几点:
1)AppBarLayout 设置 behavior 需要自己定义,为以后拦截事件用
app:layout_behavior=".weight.MyBehavior"
2)AppBarLayout 第一个子view,就是需要滑动消失的布局,设置
app:layout_scrollFlags="scroll|exitUntilCollapsed"
scroll 滚动,exitUntilCollapsed 可以在置顶后有阴影效果
3)最外层RecyclerView(也可以是各种带滑动的view,也可以是ViewPager实现分页) 设置
app:layout_behavior="@string/appbar_scrolling_view_behavior"


xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#ffffff"
android:orientation="vertical">

android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="置顶滑动"
android:textColor="@color/black"
android:textSize="20sp" />
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true"
android:background="#dddddd"/>


android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:prl_pullDownMaxDistance="300dp"
app:prl_twinkEnable="true">

android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/home_top_view"
android:orientation="vertical">

android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ffffff"
app:layout_behavior=".weight.MyBehavior">

android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
android:layout_width="match_parent"
android:layout_height="150dp"
android:src="@mipmap/home_c"/>
android:id="@+id/top_img_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>


android:id="@+id/home_tab_container_layout"
android:layout_width="match_parent"
android:layout_height="55dp"
android:gravity="center"
android:orientation="horizontal">
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layout_marginLeft="20dp"
android:textSize="15sp"
android:textColor="#222222"
android:text="悬停标题"/>

android:id="@+id/filter_layout"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="20dp"
android:scaleType="fitXY"
android:src="@mipmap/home_icon" />




android:id="@+id/bottom_img_rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />








2、首先解决PullRefreshLayout 与 CoordinatorLayout(依靠AppBarLayout来处理)滑动冲突


通过AppBarLayout监听addOnOffsetChangedListener获取CoordinatorLayout是否滑动到顶部,
设置PullRefreshLayout是否可以上拉刷新

   // 记录AppBar滚动距离
appBarLayout.addOnOffsetChangedListener(View::setTag);
homeRefreshLayout.setOnTargetScrollCheckListener(new PullRefreshLayout.OnTargetScrollCheckListener() {
@Override
public boolean onScrollUpAbleCheck() {
// 根据AppBar滚动的距离来设置RefreshLayout是否可以下拉刷新
int appbarOffset = ((appBarLayout.getTag() instanceof Integer)) ? (int) appBarLayout.getTag() : 0;
return appbarOffset != 0;
}

@Override
public boolean onScrollDownAbleCheck() {
return true;
}
});

3、启用AppBarLayout滑动(不设置也可以,但有的时候会滑动有问题)


注意📢:要在数据加载之后设置,不然不起作用

CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
MyBehavior behavior = (MyBehavior) params.getBehavior();
try {
if (behavior!=null){
behavior.setDragCallback(new AppBarLayout.Behavior.DragCallback() {
@Override
public boolean canDrag(@NonNull AppBarLayout appBarLayout) {
isFirstData = true;
//为了启用折叠工具栏的滚动
return true;
}
});
}

} catch (Exception e) {

}

4、自以为很完美了┭┮﹏┭┮,但是遇到一个小问题


  问题?
滑动置顶之后,滑动下面的recyclerview,使得recyclerview不是显示第一个item,松开手,
然后向下滑动“悬停标题”,发现可以向下滑动,🤩,是bug的味道。如下图

BUG的味道


下面就开始解决


 解决思路就是根据下面的RecyclerView滑动,设置AppBarLayout是否可以滑动,
(1) 设置监听RecyclerView第一个完整item
(2) 根据Position来设置behavior.setCanMove(position<1);
(3) MyBehavior实现是否可以滑动
上代码

// (1)设置监听RecyclerView第一个完整item
bottomRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == SCROLL_STATE_IDLE) {
if (bottomRv != null && bottomRv.getLayoutManager() instanceof LinearLayoutManager) {
LinearLayoutManager layoutManager = (LinearLayoutManager) bottomRv.getLayoutManager();
if (layoutManager != null) {
// 根据滑动item设置置顶是否可以滑动
int firstCompletelyVisible = layoutManager.findFirstCompletelyVisibleItemPosition();
initAppbar(firstCompletelyVisible);
}
}
}
}
});
//根据Position来设置behavior.setCanMove(position<1);
private boolean isFirstData;
private int oldPosition = -2;
public void initAppbar(int position) {
if (oldPosition == position){
return;
}
oldPosition = position;
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
MyBehavior behavior = (MyBehavior) params.getBehavior();
try {
if (behavior!=null){
if (position == -1){
behavior.setDragCallback(new AppBarLayout.Behavior.DragCallback() {
@Override
public boolean canDrag(@NonNull AppBarLayout appBarLayout) {
isFirstData = true;
//为了启用折叠工具栏的滚动
return true;
}
});
}else {
// 置顶后,如果recyclerview不是第一个item,禁止工具栏滑动
behavior.setCanMove(position<1);
}
}

} catch (Exception e) {
}
}
// (3) MyBehavior实现是否可以滑动
@Override
public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull AppBarLayout child, @NonNull MotionEvent ev) {
if (!canMove && ev.getAction() == MotionEvent.ACTION_DOWN){
return false;
}
return super.onInterceptTouchEvent(parent, child, ev);
}

public void setCanMove(boolean canMove){
this.canMove = canMove;
}

5、完整代码


Activity代码

  public class MainActivity extends AppCompatActivity {
private RecyclerView topRv;
private RecyclerView bottomRv;
private AppBarLayout appBarLayout;
private PullRefreshLayout homeRefreshLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
appBarLayout = findViewById(R.id.app_bar_layout);
homeRefreshLayout = findViewById(R.id.swipe_refresh_layout);
topRv = findViewById(R.id.top_img_rv);
bottomRv = findViewById(R.id.bottom_img_rv);
topRv.setLayoutManager(new LinearLayoutManager(this));
bottomRv.setLayoutManager(new LinearLayoutManager(this));
initView();
initData();
}
private void initView() {
StoreHouseHeader header = new StoreHouseHeader(this);
header.setPadding(0, 20, 0, 20);
header.initWithString("XIANGPAN");
header.setTextColor(0xFF222222);

homeRefreshLayout.setHeaderView(header);

homeRefreshLayout.setOnRefreshListener(new PullRefreshLayout.OnRefreshListenerAdapter() {
@Override
public void onRefresh() {
initData();
checkHandler.sendEmptyMessageDelayed(0,2000);
}
});
// 记录AppBar滚动距离
appBarLayout.addOnOffsetChangedListener(View::setTag);

homeRefreshLayout.setOnTargetScrollCheckListener(new PullRefreshLayout.OnTargetScrollCheckListener() {
@Override
public boolean onScrollUpAbleCheck() {
// 根据AppBar滚动的距离来设置RefreshLayout是否可以下拉刷新
int appbarOffset = ((appBarLayout.getTag() instanceof Integer)) ? (int) appBarLayout.getTag() : 0;
return appbarOffset != 0;
}

@Override
public boolean onScrollDownAbleCheck() {
return true;
}
});

}
private void initData() {
initTop();
initBottom();
if (!isFirstData) {
initAppbar(-1);
}

appBarLayout.setExpanded(true, false);
}

private void initBottom() {
PhotoAdapter bottomAdapter = new PhotoAdapter();
bottomRv.setAdapter(bottomAdapter);
bottomAdapter.setDataList(10);
bottomRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == SCROLL_STATE_IDLE) {
if (bottomRv != null && bottomRv.getLayoutManager() instanceof LinearLayoutManager) {
LinearLayoutManager layoutManager = (LinearLayoutManager) bottomRv.getLayoutManager();
if (layoutManager != null) {
// 根据滑动item设置置顶是否可以滑动
int firstCompletelyVisible = layoutManager.findFirstCompletelyVisibleItemPosition();
initAppbar(firstCompletelyVisible);
}
}
}
}
});
}

private void initTop() {
PhotoAdapter topAdapter = new PhotoAdapter();
topRv.setAdapter(topAdapter);
topAdapter.setDataList(4);
}
private boolean isFirstData;
private int oldPosition = -2;
public void initAppbar(int position) {
if (oldPosition == position){
return;
}
oldPosition = position;
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
MyBehavior behavior = (MyBehavior) params.getBehavior();
try {
if (behavior!=null){
if (position == -1){
behavior.setDragCallback(new AppBarLayout.Behavior.DragCallback() {
@Override
public boolean canDrag(@NonNull AppBarLayout appBarLayout) {
isFirstData = true;
//为了启用折叠工具栏的滚动
return true;
}
});
}else {
// 置顶后,如果recyclerview不是第一个item,禁止工具栏滑动
behavior.setCanMove(position<1);
}
}

} catch (Exception e) {

}

}
public Handler checkHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
//模拟网络请求结束,去除刷新效果
if (homeRefreshLayout != null) {
homeRefreshLayout.refreshComplete();
}
}
};
}

MyBehavior代码

public class MyBehavior extends AppBarLayout.Behavior {


private boolean canMove = true;

public MyBehavior() {

}

public MyBehavior(Context context, AttributeSet attrs) {

super(context, attrs);
}

@Override
public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull AppBarLayout child, @NonNull MotionEvent ev) {
if (!canMove && ev.getAction() == MotionEvent.ACTION_DOWN){
return false;
}
return super.onInterceptTouchEvent(parent, child, ev);
}

public void setCanMove(boolean canMove){
this.canMove = canMove;
}

public boolean isCanMove() {
return canMove;
}
}



以上就是全部内容,待完善,以后会更新。如有建议和意见,请及时沟通。


作者:_xiangpan
链接:https://juejin.cn/post/7018453099794825252
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

HashMap有何特别之处,为什么java面试从不缺席?

涉及知识点 看过java面试经验分享的小伙伴或者经历过准备过校招面试的小伙伴应该都曾经被Hashmap给支配过,即使是社招HashMap也仍然是高频考点,那么究竟为什么大家都喜欢问HashMap,其中包含了哪些知识点? 首先从生产的角度来说,HashMap是...
继续阅读 »

涉及知识点


看过java面试经验分享的小伙伴或者经历过准备过校招面试的小伙伴应该都曾经被Hashmap给支配过,即使是社招HashMap也仍然是高频考点,那么究竟为什么大家都喜欢问HashMap,其中包含了哪些知识点?



  • 首先从生产的角度来说,HashMap是我们在生产过程中最常用的集合之一,如果完全不懂它的原理,很难发挥出它的优势甚至会造成线上bug

  • 从数据结构和算法的角度,HashMap涉及到了数组,链表,红黑树,以及三者相互转化的过程,以及位运算等丰富的数据结构和算法内容。


所以HashMap就成为了面试的高频考点,这些过程都清楚,说明数据结构和算法的基础不会太差。


HashMap基本知识


JDK1.8之前数据结构是数组+链表
JDK1.8以后采用数组+链表+红黑树


image.png


其中,当数组元素超过64个且数组单个位置存储的链表元素个数超过8个时,会进化成红黑树,红黑树的引进时为了加快查找效率


同样,当数组单个位置存储的链表元素个数少于6个时,又会退化成链表


HashMap重要的类属性


其实JDK的源码都有非常详细的注释,但还是翻译一下吧,如下:



  • 初始容量大小为16


/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16


  • 最大容量为2^30


/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;


  • 默认的负载因子为0.75


负载因子之所以选择是基于时间和空间平衡的结果选择的参数,时间和空间的平衡是指既不浪费太多的空间,又不用频繁地进行扩容


/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;


  • 链表元素数量进化成红黑树的阈值为8,数组元素大于64时,链表元素超过8就会进化为红黑树


/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;

至于为什么选8,在之前有一段很长的注释里面有说明,可以看一下原文如下:


 * Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins. In
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million

大概意思就是:如果 hashCode的分布离散良好的话,那么红黑树是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,注释中给我们展示了1-8长度的具体命中概率,当长度为8的时候,概率概率仅为0.00000006,这么小的概率,HashMap的红黑树转换几乎不会发生



  • 红黑树元素少于6个就退化成链表


至于为什么是6个不是7个是为了避免频繁在树与链表之间变换,如果是7,加一个元素就需要进化成红黑树,删一个元素就需要退化成链表


/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;


  • 链表树能进化成树时最小的数组长度,只有数组长度打到这个值链表才可能进化成树


/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;

重要的内部类


链表Node<K,V>


定义的链表Node类,next有木有很熟悉,单向链表
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }

public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}

红黑树TreeNode<K,V>


/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}

/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}

HashMap的put方法


实际上调用的是putVal方法


/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

接下来看看putVal方法


/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

putVal的流程如下:


HashMap的put方法执行过程.png


HashMap的resize扩容方法(重要)


先翻译一下这个方法的官方注释:
初始化或者将原来的容量翻倍,如果为空,则分配初始容量,否则,进行容量翻倍,原来存在的元素要么留在原来的位置上,要么在新的数组中向后移动原来数组容量的大小


/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//数组原来不为空
if (oldCap > 0) {
// 超过最大值就不再扩充了,就只好随你碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 没超过最大值,就扩充为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//初始化,申明HashMap的时候指定了初始容量
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//初始化,申明HashMap的时候没有指定初始容量
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;

//扩容之后的新数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

整个扩容的流程如下:


HashMap的resize过程.png


链表重新整理的过程


可能会将原来一个链表拆分为两个链表,判断当前链表元素是否需要移动到新的链表的依据是:
计算(e.hash & oldCap) == 0?
true,不需要移动到另一个链表,我们用头结点为loHead的链表将这些元素串起来
false,元素需要移动到另一个新的链表,我们用头结点为hiHead的链表将这些元素串起来
(tips:其实lo和hi是low和high的缩写,这里用的是两个双指针来进行链表的拆分整理)
以数组从容量为16扩容到32为例,位于下标为1的链表扩容之后如图所示:一部分链表仍然保留在下标为1的位置,另一部分则迁移至下标为1+16的位置。


HashMap扩容链表重新整理.png
在链表重新整理的过程中,同一个链表的元素的相对顺序不会被改变


红黑树重新整理的过程


看了一下TreeNode的继承关系,TreeNdoe也是继承Node的子类,红黑树重新整理的过程也和链表相似,因为它其实也维护了一个链表的结构
拆分可能会将原来一个红黑树拆分为两个链表,或者一个红黑树一个链表,判断当前链表元素是否需要移动到新的链表的依据是:
计算(e.hash & oldCap) == 0?
true,不需要移动,仍然在原来的位置,我们用头结点为loHead的链表将这些元素串起来,如果元素的个数不小于6,则继续维护成红黑树,否则为链表
false,元素需要移动到另一个新的位置,我们用头结点为hiHead的链表将这些元素串起来,如果元素的个数不小于6,则继续维护成红黑树,否则为链表


HashMap的TreeNode.png


HashMap线程安全性


HashMap中存在的线程安全问题:


1.HashMap在读取Hash槽首元素的时候读取的是工作内存中引用所指向的对象,并发情况下,其他线程修改的值并不能被及时读取到。


2.HashMap在插入新元素的时候,主要会进行两次判断:


2.1 第一次是根据键的hash判断当前hash槽是否被占用,如果没有就放入当前插入对象。并发情况下,如果A线程判断该槽未被占用,在执行写入操作时时间片耗尽。此时线程B也执行获取hash(恰巧和A线程的对象发生hash碰撞)判断该槽未被占用,继而直接插入该对象。然后A线程被CPU重新调度继续执行写入操作,就会将线程B的数据覆盖。(注:此处也有可见性问题)


2.2 第二次是同一个hash槽内,因为HashMap特性是保持key值唯一,所以会判断当前欲插入key是否存在,存在就会覆盖。与上文类似,并发情况下,如果线程A判断最后一个节点仍未发现重复key那么会把以当前对象构建节点挂在链表或者红黑树上,如果线程B在A判断操作和写操作之间,进行了判断和写操作,也会发生数据覆盖。


除此之外扩容也会发生类似的并发问题。还有size的问题,感觉其实高并发情况下这个size的准确性可以让步性能。


参考文章


【1】tech.meituan.com/2016/06/24/…

【2】blog.csdn.net/weixin_3143…


作者:ForeverKobe
链接:https://juejin.cn/post/7033392478753390629
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

为什么 JakeWharton 建议:App 只要用到一个 Activity ?

安卓开发大神级人物 JakeWharton 前不久在接受采访时提出一个颇具争议而又没有给出原因的建议:一个 App 只需要一个 Activity ,你可以使用 Fragments,只是别用 Fragments 回退栈。 针对这一言论,有关 JakeWharto...
继续阅读 »

安卓开发大神级人物 JakeWharton 前不久在接受采访时提出一个颇具争议而又没有给出原因的建议:一个 App 只需要一个 Activity ,你可以使用 Fragments,只是别用 Fragments 回退栈。


针对这一言论,有关 JakeWharton 建议的背后原因的一个提问迅速在 Reddit 国外网站的安卓开发频道引发热评。



众多安卓开发人员纷纷提出自己的见解。其中获赞最高的一条,甚至得到 JakeWharton 本人的亲自赞评。



我们来看看这条回答都提到了哪些内容,对 Activity 和 Fragment 之间的爱恨情仇有何独到的见解,凭什么能得到 JakeWharton 本尊的青睐有加。




因为 Activity 是一个程序入口。你可以将其视为 app 的一个 main 函数。站在用户的立场上,通常你进入 app 的方式可能包括以下几种:




  • launcher 桌面程序(main 函数入口);




  • 来自参数化 main 函数入口的通知栏,并且导航到 app 的指定位置;




  • 如果你做的是一个相机应用,那么需要处理图片请求的 intents;




  • 如果你做的是一个社交产品,那么需要处理 share 请求的 intents;




差不多类似这些场景。


但是,如果你真的不用分享和来自应用的 intents 的话,并且唯一的程序入口就是 launcher 桌面,别为每一个页面创建一个新的入口。这样做其实没有意义。为什么没有意义?因为这种场景下,进程死掉后 launcher 能够启动任何你应用中的 Activity 页面。


Fragments 是处理生命周期事件的视图控制器,并且非常不错。然而,Fragments 回退栈简直垃圾;回退栈变化监听器总是不正常地被调用( 1 次 transaction 三次调用?),并且不告诉你调用什么,而在恢复事务时也不知道哪些 fragments 是可用的。


你可以给事务添加 tag 标签,然后从栈中弹出操作,但是仅仅是一个 main -> Events -> Details(id=123) 的操作流程就相当繁琐了。


同样的,一旦你将一个 Fragment 放进回退栈中,我个人不知道它的生命周期开始做什么。我曾经遇到过一个后台中的 fragment 被调用四次 onCreateView() 方法,我甚至不知道究竟怎么了。而没有位于回退栈中的 Fragments 是可以被预见的。它们的动画支持有点古怪,但至少它们还能使用。


所以如果你想知道哪些 Fragments 是你能够操作的并且哪些 views 是你正在展示的并且能够在你自己的导航状态控制之中,那么你应该自己处理导航操作。把“应用逻辑”抽象化到一个 presenter(亦枫注:MVP 模式)中听起来来很棒,但是你是不是脱离了应用视图层里面的真实情况?



但是单一 activity 的优势是什么?



更简单的生命周期处理(例如,当 app 进入后台时,你只需要处理 onStop 方法),更少错误空间,和更多控制。同样的,你可以移动视图层外面的导航状态到 domain 层,或者至少到 presenter 中。不需要太多 view.navigateToDetail(songId) 之类的东西,你只需要在你的 presenter 或者 ViewModel 或者无论哪些时髦的用法中使用 backstack.goTo(SongKey.create(songId)) 就行。借助一个合适的库,当你到了 onResume 时它会自动将这些导航调用加入队列,并且不会致使 fragment 事务发生崩溃,非常得好。


尽管 Google 给出的案例也在用 commitAllowingStateLoss(),我有使用 commitNow() 的动画爱好。在我看来,单个 activity 能够看得见的好处就是,页面间共享 views 的能力,取代通过使用 <include 标签在 18 个布局文件重复视图。其他当然是更简单的导航操作。




以上便是深得 JakeWharton 大神心意的一条回答。话虽如此,但是系统 Fragment 存在的未解之谜或者说出乎你意料的坑实在太多。如果一定要在多 activity 部分 fragments 和单 activity 多 fragments 之间选择的话,我想不只是我,很多人还是毫不犹豫地选择前者。


更多讨论内容,参见:


http://www.reddit.com/r/androidde…


作者:亦枫
链接:https://juejin.cn/post/6844903521209286663
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

termux 安卓神器

今年春节在家的时候,手头没有电脑,但是想用电脑写下代码,于是乎我找到这一款termux神器,可以把安卓手机当作一台小型的服务器来使用。利用5年前已经淘汰的安卓手机,插上适配器,这样我就可以无休止的跑我的脚本了。termux 安装在termux官网上看到最新的版...
继续阅读 »

今年春节在家的时候,手头没有电脑,但是想用电脑写下代码,于是乎我找到这一款termux神器,可以把安卓手机当作一台小型的服务器来使用。利用5年前已经淘汰的安卓手机,插上适配器,这样我就可以无休止的跑我的脚本了。

termux 安装

在termux官网上看到最新的版本,必须要安装在Android 7以上的手机,我的魅族手机经过我的一番折腾只能升级到安卓6,不能安装最新的termux,还好旧的版本0.73支持Android 5以上。  安装包下载后,就是常规的apk安装了。

termux 环境配置

termux支持sshd,所有我们不用在旧手机上进行操作,我们可以在自己新安卓手机上通过ssh连接到旧手机上,前提是termux要启动sshd.

#安装openssh
pkg install openssh
#安装后,启动sshd
sshd
#设置用户密码
passwd
#登陆用户名
whoami
#查看局域网内ip
ifconfig|grep inet

我们在另外一台手机上安卓juicessh,一款安卓的ssh客户端,由于termux的sshd端口跟我们平时使用的22端是不一样的,所以在ssh到termux采用的是8022端口,通过刚才我们查看的用户名登陆,输入我们刚刚设置的密码,就能完成ssh手机登陆了。

现在我们就能愉快的采用termux进行学习了。

vim 练习

#安装vim
pkg install vim
vim hello.txt

 这就跟我们配置在服务器用vim一样,我们就可以练习下vim的快捷键

安装nodejs

#安装nodejs
pkg install nodejs
node -v
pkg install npm
npm -v

安装软件的时候,会有点慢,但是千万不要切换镜像,因为我们的版本比较旧,国内的源没有搬运旧的,所以切换后会导致安装软件出现各自问题,这是卸载安装5次的体验。清华源网站已经提示了镜像仅适用于 Android 7.0 (API 24) 及以上版本,旧版本系统使用本镜像可能导致程序错误。

安装好nodejs后,我们可以做更多有趣的事情,运行node脚本,node server服务器,个人网站等等,甚至可以做一些定时任务,比如定时发天气预告邮件等。


作者:chiv
链接:https://juejin.cn/post/6930531076607737870
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

flutter 线上apm监控 远程日志 emas_tlog

emas_tlog ali emas tlog 阿里巴巴flutter版本 远程日志 TLog 介绍 远程日志服务提供远程手机日志拉取功能,解决移动App线上异常排查困难的问题。 远程日志服务支持Android/iOS应用类型。 产品架构 1.移动App集成...
继续阅读 »

emas_tlog


ali emas tlog


阿里巴巴flutter版本 远程日志 TLog


介绍


远程日志服务提供远程手机日志拉取功能,解决移动App线上异常排查困难的问题。
远程日志服务支持Android/iOS应用类型。


产品架构


在这里插入图片描述
1.移动App集成SDK
2.远程日志服务通过心跳探测识别已安装App的移动终端,并进行日志拉取配置。
3.远程日志服务拉取指定移动终端App的用户日志,并对拉取任务进行管理。
4.远程日志服务查看已从终端设备拉取至控制台的用户日志。


官网地址:官网地址


本项目是根据官方来制作的flutter版本


快速开始


#####flutter配置:


  emas_tlog: ^0.0.1

初始化:


方法的声明:
static void init(String appKey,String appSecret,String rsaPublicKey,
String appKeyIos,String appSecretIos,String rsaPublicKeyIos,
{String androidChannel = "line",String userNick = "NoLogin",ApmLogType? type,bool debug = true}){
// xxxxxx
}
方法的调用:
EmasTlog.init("**", "**", "**",
"**","**","**",
androidChannel :"HEHE",userNick: "lalala2");

参数说明:
appKey
appSecret
rsaPublicKey
appKeyIosiOSemas.appKey
appSecretIosiOSemas.appSecret
rsaPublicKeyIosiOSappmonitor.tlog.rsaSecret
androidChannel: 渠道 (iOS指定 App Store
userNick: 用户昵称说明: 默认值NoLogin
type(*日志上传类型,注:iOS若不传,默认是I
debug(底层运行日志答应)true测试环境开启 false正式环境 关闭(iOS可不传)


*星号标记说明


V:可拉取所有级别的日志。(iOS无此类型)


D:可拉取DEBUG/INFO/WARN/ERROR级别的日志。


I:可拉取INFO/WARN/ERROR级别的日志。


W:可拉取WARN/ERROR级别的日志。


E:可拉取ERROR级别的日志。


日常使用:


方法的声明:
static void init(String appKey,String appSecret,String rsaPublicKey,
String appKeyIos,String appSecretIos,String rsaPublicKeyIos,
{String androidChannel = "line",String userNick = "NoLogin",ApmLogType? type,bool debug = true}){
// xxxxx
}

方法的调用:
EmasTlog.log(ApmLogType.I, "tag2_1231231",module: "hehe2_flutter",tag: "tag_hehe2");
EmasTlog.log(ApmLogType.V, "tag2_1231231",module: "hehe2_flutter",tag: "tag_hehe2");
EmasTlog.log(ApmLogType.W, "tag2_1231231",module: "hehe2_flutter",tag: "tag_hehe2");
EmasTlog.log(ApmLogType.E, "tag2_1231231",module: "hehe2_flutter",tag: "tag_hehe2");
EmasTlog.log(ApmLogType.D, "tag2_1231231",module: "hehe2_flutter",tag: "tag_hehe2");
EmasTlog.log(ApmLogType.I, "tag2_1231231",module: "hehe2_flutter",tag: "tag_hehe2");

说明:
module为模块业务,可以为空


    EmasTlog.comment(); 主动上传日志
EmasTlog.updateNickName(name) 修改用户名(用于登录切换用户)

#####Android配置:
1、在根项目Android目录build.gradle配置如下代码



ext {
tlog = [
openUtdid : true
]
}


说明:
如果项目编译期报类似如下错误



Duplicate class com.ta.utdid2.a.a.a found in modules jetified-alicloud-android-utdid-2.5.1-proguard (com.aliyun.ams:alicloud-android-utdid:2.5.1-proguard) and jetified-utdid-1.5.2.1 (com.umeng.umsdk:utdid:1.5.2.1)


则代码需要调整为



ext {
tlog = [
openUtdid : false
]
}


配置展示:



ext {
tlog = [
openUtdid : true
]
}

buildscript {
ext.kotlin_version = '1.3.50'
repositories {
google()
jcenter()
}

dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
jcenter()
}
}


app AndroidManifest配置:



<manifest **
xmlns:tools="http://schemas.android.com/tools"
** >

<application
**
tools:replace="android:label">


说明需要 配置 tools:replace="android:label"
#####iOS 的配置说明
1、在Flutter项目的iOS端的Podfile中添加如下索引库地址:


# alicloud
source "https://github.com/CocoaPods/Specs.git"
source "https://github.com/aliyun/aliyun-specs.git"

2、在Flutter项目的iOS端的info.plist文件中添加如下代码:


<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key>
<array>
<string>_dartobservatory._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Main</string>

3、在iOS端项目Build Setting中,将Allow Non-modular Includes In Framework Modules设置为YES

收起阅读 »

你怎么总是能写出两三千行的controller类?

你一定经常见到一个两三千行的 controller 类,类之所以发展成如此庞大,有如下原因: 长函数太多 类里面有特别多的字段和函数 量变引起质变,可能每个函数都很短小,但数量太多 1 程序的modularity 你思考过为什么你不会把all code写到...
继续阅读 »

你一定经常见到一个两三千行的 controller 类,类之所以发展成如此庞大,有如下原因:



  • 长函数太多

  • 类里面有特别多的字段和函数


量变引起质变,可能每个函数都很短小,但数量太多


1 程序的modularity


你思考过为什么你不会把all code写到一个文件?因为你的潜意识里明白:



  • 相同的功能模块无法复用

  • 复杂度远超出个人理解极限


一个人理解的东西是有限的,在国内互联网敏捷开发环境下,更没有人能熟悉所有代码细节。


解决复杂的最有效方案就是分而治之。所以,各种程序设计语言都有自己的模块划分(modularity)方案:



  • 从最初的按文件划分

  • 到后来使用OO按类划分


开发者面对的不再是细节,而是模块,模块数量显然远比细节数量少,理解成本大大降低,开发效率也提高了,再也不用 996, 每天都能和妹纸多聊几句了。


modularity,本质就是分解问题,其背后原因,就是个人理解能力有限。



说这么多我都懂,那到底怎么把大类拆成小类?



2 大类是怎么来的?


2.1 职责不单一


最容易产生大类的原因


CR一段代码:


该类持有大类的典型特征,包含一坨字段:这些字段都缺一不可吗?



  • userId、name、nickname等应该是一个用户的基本信息

  • email、phoneNumber 也算是和用户相关联


很多应用都提供使用邮箱或手机号登录方式,所以,这些信息放在这里,也能理解



  • authorType,作者类型,表示作者是签约作者还是普通作者,签约作者可设置作品的付费信息,但普通作者无此权限

  • authorReviewStatus,作者审核状态,作者成为签约作者,需要有一个申请审核的过程,该状态字段就是审核状态

  • editorType,编辑类型,编辑可以是主编,也可以是小编,权限不同


这还不是 User 类的全部。但只看这些内容就能看出问题:



  • 普通用户既不是作者,也不是编辑


作者和编辑这些相关字段,对普通用户无意义



  • 对那些成为作者的用户,编辑的信息意义不大


因为作者不能成为编辑。编辑也不会成为作者,作者信息对成为编辑的用户无意义


总有一些信息对一部分人毫无意义,但对另一部分人又必需。出现该问题的症结在于只有“一个”用户类。


普通用户、作者、编辑,三种不同角色,来自不同业务方,关心的是不同内容。仅因为它们都是同一系统的用户,就把它们都放到一个用户类,导致任何业务方的需求变动,都会反复修改该类,严重违反单一职责原则
所以破题的关键就是职责拆分。


虽然这是一个类,但它把不同角色关心的东西都放在一起,就愈发得臃肿了。


只需将不同信息拆分即可:


public class User {
private long userId;
private String name;
private String nickname;
private String email;
private String phoneNumber;
...
}

public class Author {
private long userId;
private AuthorType authorType;
private ReviewStatus authorReviewStatus;
...
}

public class Editor {
private long userId;
private EditorType editorType;
...
}

拆出 Author、Editor 两个类,将和作者、编辑相关的字段分别移至这两个类里。
这俩类分别有个 userId 字段,用于关联该角色和具体用户。


2.2 字段未分组


有时觉得有些字段确实都属于某个类,结果就是,这个类还是很大。


之前拆分后的新 User 类:


public class User {
private long userId;
private String name;
private String nickname;
private String email;
private String phoneNumber;
...
}

这些字段应该都算用户信息的一部分。但依然也不算是个小类,因为该类里的字段并不属于同一种类型的信息。
如,userId、name、nickname算是用户的基本信息,而 email、phoneNumber 则属于用户的联系方式。


需求角度看,基本信息是那种一旦确定一般就不变的内容,而联系方式则会根据实际情况调整,如绑定各种社交账号。把这些信息都放到一个类里面,类稳定程度就差点。


据此,可将 User 类的字段分组:


public class User {
private long userId;
private String name;
private String nickname;
private Contact contact;
...
}

public class Contact {
private String email;
private String phoneNumber;
...
}

引入一个 Contact 类(联系方式),把 email 和 phoneNumber 放了进去,后面再有任何关于联系方式的调整就都可以放在这个类里面。
此次调整,把不同信息重新组合,但每个类都比原来要小。


前后两次拆分到底有何不同?



  • 前面是根据职责,拆分出不同实体

  • 后面是将字段做了分组,用类把不同的信息分别封装


大类拆解成小类,本质上是个设计工作,依据单一职责设计原则。


若把大类都拆成小类,类的数量就会增多,那人们理解的成本是不是也会增加呢?
这也是很多人不拆分大类的借口。


各种程序设计语言中,本就有如包、命名空间等机制,将各种类组合在一起。在你不需要展开细节时,面对的是一个类的集合。
再进一步,还有各种程序库把这些打包出来的东西再进一步打包,让我们只要面对简单的接口,而不必关心各种细节。


软件正这样层层封装构建出来的。


作者:JavaEdge在掘金
链接:https://juejin.cn/post/7031442685152100360
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

大红大紫的 Golang 真的是后端开发中的万能药吗?

前言 城外的人想进去,城里的人想出来。-- 钱钟书《围城》 随着容器编排(Container Orchestration)、微服务(Micro Services)、云技术(Cloud Technology)等在 IT 行业不断盛行,2009 年诞生于 Go...
继续阅读 »

前言



城外的人想进去,城里的人想出来。-- 钱钟书《围城》



随着容器编排(Container Orchestration)、微服务(Micro Services)、云技术(Cloud Technology)等在 IT 行业不断盛行,2009 年诞生于 Google 的 Golang(Go 语言,简称 Go)越来越受到软件工程师的欢迎和追捧,成为如今炙手可热的后端编程语言。在用 Golang 开发的软件项目列表中,有 Docker(容器技术)、Kubernetes(容器编排)这样的颠覆整个 IT 行业的明星级产品,也有像 Prometheus(监控系统)、Etcd(分布式存储)、InfluxDB(时序数据库)这样的强大实用的知名项目。当然,Go 语言的应用领域也绝不局限于容器和分布式系统。如今很多大型互联网企业在大量使用 Golang 构建后端 Web 应用,例如今日头条、京东、七牛云等;长期被 Python 统治的框架爬虫领域也因为简单而易用的爬虫框架 Colly 的崛起而不断受到 Golang 的挑战。Golang 已经成为了如今大多数软件工程师最想学习的编程语言。下图是 HackerRank 在 2020 年调查程序员技能的相关结果。


hackerrank-survey-2020


那么,**Go 语言真的是后端开发人员的救命良药呢?它是否能够有效提高程序员们的技术实力和开发效率,从而帮助他们在职场上更进一步呢?Go 语言真的值得我们花大量时间深入学习么?**本文将详细介绍 Golang 的语言特点以及它的优缺点和适用场景,带着上述几个疑问,为读者分析 Go 语言的各个方面,以帮助初入 IT 行业的程序员以及对 Go 感兴趣的开发者进一步了解这个热门语言。


Golang 简介


golang


Golang 诞生于互联网巨头 Google,而这并不是一个巧合。我们都知道,Google 有一个 20% 做业余项目(Side Project)的企业文化,允许工程师们能够在轻松的环境下创造一些具有颠覆性创新的产品。而 Golang 也正是在这 20% 时间中不断孵化出来。Go 语言的创始者也是 IT 界内大名鼎鼎的行业领袖,包括 Unix 核心团队成员 Rob Pike、C 语言作者 Ken Thompson、V8 引擎核心贡献者 Robert Griesemer。Go 语言被大众所熟知还是源于容器技术 Docker 在 2014 年被开源后的爆发式发展。之后,Go 语言因为其简单的语法以及迅猛的编译速度受到大量开发者的追捧,也诞生了很多优秀的项目,例如 Kubernetes。


Go 语言相对于其他传统热门编程语言来说,有很多优点,特别是其高效编译速度天然并发特性,让其成为快速开发分布式应用的首选语言。Go 语言是静态类型语言,也就是说 Go 语言跟 Java、C# 一样需要编译,而且有完备的类型系统,可以有效减少因类型不一致导致的代码质量问题。因此,Go 语言非常适合构建对稳定性灵活性均有要求的大型 IT 系统,这也是很多大型互联网公司用 Golang 重构老代码的重要原因:传统的静态 OOP 语言(例如 Java、C#)稳定性高但缺乏灵活性;而动态语言(例如 PHP、Python、Ruby、Node.js)灵活性强但缺乏稳定性。因此,“熊掌和鱼兼得” 的 Golang,受到开发者们的追捧是自然而然的事情,毕竟,“天下苦 Java/PHP/Python/Ruby 们久矣“。


不过,Go 语言并不是没有缺点。用辩证法的思维方式可以推测,Golang 的一些突出特性将成为它的双刃剑。例如,Golang 语法简单的优势特点将限制它处理复杂问题的能力。尤其是 Go 语言缺乏泛型(Generics)的问题,导致它构建通用框架的复杂度大增。虽然这个突出问题在 2.0 版本很可能会有效解决,但这也反映出来明星编程语言也会有缺点。当然,Go 的缺点还不止于此,Go 语言使用者还会吐槽其啰嗦的错误处理方式(Error Handling)、缺少严格约束的鸭子类型(Duck Typing)、日期格式问题等。下面,我们将从 Golang 语言特点开始,由浅入深多维度深入分析 Golang 的优缺点以及项目适用场景。


语言特点


简洁的语法特征


Go 语言的语法非常简单,至少在变量声明、结构体声明、函数定义等方面显得非常简洁。


变量的声明不像 Java 或 C 那样啰嗦,在 Golang 中可以用 := 这个语法来声明新变量。例如下面这个例子,当你直接使用 := 来定义变量时,Go 会自动将赋值对象的类型声明为赋值来源的类型,这节省了大量的代码。


func main() {
valInt := 1 // 自动推断 int 类型
valStr := "hello" // 自动推断为 string 类型
valBool := false // 自动推断为 bool 类型
}

Golang 还有很多帮你节省代码的地方。你可以发现 Go 中不会强制要求用 new 这个关键词来生成某个类(Class)的新实例(Instance)。而且,对于公共和私有属性(变量和方法)的约定不再使用传统的 publicprivate 关键词,而是直接用属性变量首字母的大小写来区分。下面一些例子可以帮助读者理解这些特点。


// 定义一个 struct 类
type SomeClass struct {
PublicVariable string // 公共变量
privateVariable string // 私有变量
}

// 公共方法
func (c *SomeClass) PublicMethod() (result string) {
return "This can be called by external modules"
}

// 私有方法
func (c *SomeClass) privateMethod() (result string) {
return "This can only be called in SomeClass"
}

func main() {
// 生成实例
someInstance := SomeClass{
PublicVariable: "hello",
privateVariable: "world",
}
}

如果你用 Java 来实现上述这个例子,可能会看到冗长的 .java 类文件,例如这样。


// SomeClass.java
public SomeClass {
public String PublicVariable; // 公共变量
private String privateVariable; // 私有变量

// 构造函数
public SomeClass(String val1, String val2) {
this.PublicVariable = val1;
this.privateVariable = val2;
}

// 公共方法
public String PublicMethod() {
return "This can be called by external modules";
}

// 私有方法
public String privateMethod() {
return "This can only be called in SomeClass";
}
}

...

// Application.java
public Application {
public static void main(String[] args) {
// 生成实例
SomeClass someInstance = new SomeClass("hello", "world");
}
}

可以看到,在 Java 代码中除了容易看花眼的多层花括号以外,还充斥着大量的 publicprivatestaticthis 等修饰用的关键词,显得异常啰嗦;而 Golang 代码中则靠简单的约定,例如首字母大小写,避免了很多重复性的修饰词。当然,Java 和 Go 在类型系统上还是有一些区别的,这也导致 Go 在处理复杂问题显得有些力不从心,这是后话,后面再讨论。总之,结论就是 Go 的语法在静态类型编程语言中非常简洁。


内置并发编程


Go 语言之所以成为分布式应用的首选,除了它性能强大以外,其最主要的原因就是它天然的并发编程。这个并发编程特性主要来自于 Golang 中的协程(Goroutine)和通道(Channel)。下面是使用协程的一个例子。


func asyncTask() {
fmt.Printf("This is an asynchronized task")
}

func syncTask() {
fmt.Printf("This is a synchronized task")
}

func main() {
go asyncTask() // 异步执行,不阻塞
syncTask() // 同步执行,阻塞
go asyncTask() // 等待前面 syncTask 完成之后,再异步执行,不阻塞
}

可以看到,关键词 go 加函数调用可以让其作为一个异步函数执行,不会阻塞后面的代码。而如果不加 go 关键词,则会被当成是同步代码执行。如果读者熟悉 JavaScript 中的 async/awaitPromise 语法,甚至是 Java、Python 中的多线程异步编程,你会发现它们跟 Go 异步编程的简单程度不是一个量级的!


异步函数,也就是协程之间的通信可以用 Go 语言特有的通道来实现。下面是关于通道的一个例子。


func longTask(signal chan int) {
// 不带参数的 for
// 相当于 while 循环
for {
// 接收 signal 通道传值
v := <- signal

// 如果接收值为 1,停止循环
if v == 1 {
break
}

time.Sleep(1 * Second)
}
}

func main() {
// 声明通道
sig := make(chan int)

// 异步调用 longTask
go longTask(sig)

// 等待 1 秒钟
time.Sleep(1 * time.Second)

// 向通道 sig 传值
sig <- 1

// 然后 longTask 会接收 sig 传值,终止循环
}

面向接口编程


Go 语言不是严格的面向对象编程(OOP),它采用的是面向接口编程(IOP),是相对于 OOP 更先进的编程模式。作为 OOP 体系的一部分,IOP 更加强调规则和约束,以及接口类型方法的约定,从而让开发人员尽可能的关注更抽象的程序逻辑,而不是在更细节的实现方式上浪费时间。很多大型项目采用的都是 IOP 的编程模式。如果想了解更多面向接口编程,请查看 “码之道” 个人技术博客的往期文章《为什么说 TypeScript 是开发大型前端项目的必备语言》,其中有关于面向接口编程的详细讲解。


Go 语言跟 TypeScript 一样,也是采用鸭子类型的方式来校验接口继承。下面这个例子可以描述 Go 语言的鸭子类型特性。


// 定义 Animal 接口
interface Animal {
Eat() // 声明 Eat 方法
Move() // 声明 Move 方法
}

// ==== 定义 Dog Start ====
// 定义 Dog 类
type Dog struct {
}

// 实现 Eat 方法
func (d *Dog) Eat() {
fmt.Printf("Eating bones")
}

// 实现 Move 方法
func (d *Dog) Move() {
fmt.Printf("Moving with four legs")
}
// ==== 定义 Dog End ====

// ==== 定义 Human Start ====
// 定义 Human 类
type Human struct {
}

// 实现 Eat 方法
func (h *Human) Eat() {
fmt.Printf("Eating rice")
}

// 实现 Move 方法
func (h *Human) Move() {
fmt.Printf("Moving with two legs")
}
// ==== 定义 Human End ====

可以看到,虽然 Go 语言可以定义接口,但跟 Java 不同的是,Go 语言中没有显示声明接口实现(Implementation)的关键词修饰语法。在 Go 语言中,如果要继承一个接口,你只需要在结构体中实现该接口声明的所有方法。这样,对于 Go 编译器来说你定义的类就相当于继承了该接口。在这个例子中,我们规定,只要既能吃(Eat)又能活动(Move)的东西就是动物(Animal)。而狗(Dog)和人(Human)恰巧都可以吃和动,因此它们都被算作动物。这种依靠实现方法匹配度的继承方式,就是鸭子类型:如果一个动物看起来像鸭子,叫起来也像鸭子,那它一定是鸭子。这种鸭子类型相对于传统 OOP 编程语言显得更灵活。但是,后面我们会讨论到,这种编程方式会带来一些麻烦。


错误处理


Go 语言的错误处理是臭名昭著的啰嗦。这里先给一个简单例子。


package main

import "fmt"

func isValid(text string) (valid bool, err error){
if text == "" {
return false, error("text cannot be empty")
}
return text == "valid text", nil
}

func validateForm(form map[string]string) (res bool, err error) {
for _, text := range form {
valid, err := isValid(text)
if err != nil {
return false, err
}
if !valid {
return false, nil
}
}
return true, nil
}

func submitForm(form map[string]string) (err error) {
if res, err := validateForm(form); err != nil || !res {
return error("submit error")
}
fmt.Printf("submitted")
return nil
}

func main() {
form := map[string]string{
"field1": "",
"field2": "invalid text",
"field2": "valid text",
}
if err := submitForm(form); err != nil {
panic(err)
}
}

虽然上面整个代码是虚构的,但可以从中看出,Go 代码中充斥着 if err := ...; err != nil { ... } 之类的错误判断语句。这是因为 Go 语言要求开发者自己管理错误,也就是在函数中的错误需要显式抛出来,否则 Go 程序不会做任何错误处理。因为 Go 没有传统编程语言的 try/catch 针对错误处理的语法,所以在错误管理上缺少灵活度,导致了 “err 满天飞” 的局面。


不过,辩证法则告诉我们,这种做法也是有好处的。第一,它强制要求 Go 语言开发者从代码层面来规范错误的管理方式,这驱使开发者写出更健壮的代码;第二,这种显式返回错误的方式避免了 “try/catch 一把梭”,因为这种 “一时爽” 的做法很可能导致 Bug 无法准确定位,从而产生很多不可预测的问题;第三,由于没有 try/catch 的括号或额外的代码块,Go 程序代码整体看起来更清爽,可读性较强。


其他


Go 语言肯定还有很多其他特性,但笔者认为以上的特性是 Go 语言中比较有特色的,是区分度比较强的特性。Go 语言其他一些特性还包括但不限于如下内容。



  • 编译迅速

  • 跨平台

  • defer 延迟执行

  • select/case 通道选择

  • 直接编译成可执行程序

  • 非常规依赖管理(可以直接引用 Github 仓库作为依赖,例如 import "github.com/crawlab-team/go-trace"

  • 非常规日期格式(格式为 "2006-01-02 15:04:05",你没看错,据说这就是 Golang 的创始时间!)


优缺点概述


前面介绍了 Go 的很多语言特性,想必读者已经对 Golang 有了一些基本的了解。其中的一些语言特性也暗示了它相对于其他编程语言的优缺点。Go 语言虽然现在很火,在称赞并拥抱 Golang 的同时,不得不了解它的一些缺点。


这里笔者不打算长篇大论的解析 Go 语言的优劣,而是将其中相关的一些事实列举出来,读者可以自行判断。以下是笔者总结的 Golang 语言特性的不完整优缺点对比列表。














































特性 优点 缺点
语法简单 提升开发效率,节省时间 难以处理一些复杂的工程问题
天然支持并发 极大减少异步编程的难度,提高开发效率 不熟悉通道和协程的开发者会有一些学习成本
类型系统
  • Go 语言是静态类型,相对于动态类型语言更稳定和可预测

  • IOP 鸭子类型比严格的 OOP 语言更简洁



    • 没有继承、抽象、静态、动态等特性

    • 缺少泛型,导致灵活性降低

    • 难以快速构建复杂通用的框架或工具


    错误处理 强制约束错误管理,避免 “try/catch 一把梭” 啰嗦的错误处理代码,充斥着 if err := ...
    编译迅速 这绝对是一个优点 怎么可能是缺点?
    非常规依赖管理

    • 可以直接引用发布到 Github 上的仓库作为模块依赖引用,省去了依赖托管的官方网站

    • 可以随时在 Github 上发布 Go 语言编写的第三方模块

    • 自由的依赖发布意味着 Golang 的生态发展将不受官方依赖托管网站的限制


    严重依赖 Github,在 Github 上搜索 Go 语言模块相对不精准
    非常规日期格式 按照 6-1-2-3-4-5(2006-01-02 15:04:05),相对来说比较好记 对于已经习惯了 yyyy-MM-dd HH:mm:ss 格式的开发者来说非常不习惯

    其实,每一个特性在某种情境下都有其相应的优势和劣势,不能一概而论。就像 Go 语言采用的静态类型和面向接口编程,既不缺少类型约束,也不像严格 OOP 那样冗长繁杂,是介于动态语言和传统静态类型 OOP 语言之间的现代编程语言。这个定位在提升 Golang 开发效率的同时,也阉割了不少必要 OOP 语法特性,从而缺乏快速构建通用工程框架的能力(这里不是说 Go 无法构建通用框架,而是它没有 Java、C# 这么容易)。另外,Go 语言 “奇葩” 的错误处理规范,让 Go 开发者们又爱又恨:可以开发出更健壮的应用,但同时也牺牲了一部分代码的简洁性。要知道,Go 语言的设计理念是为了 “大道至简”,因此才会在追求高性能的同时设计得尽可能简单。


    无可否认的是,Go 语言内置的并发支持是非常近年来非常创新的特性,这也是它被分布式系统广泛采用的重要原因。同时,它相对于动辄编译十几分钟的 Java 来说是非常快的。此外,Go 语言没有因为语法简单而牺牲了稳定性;相反,它从简单的约束规范了整个 Go 项目代码风格。因此,**“快”(Fast)、“简”(Concise)、“稳”(Robust)**是 Go 语言的设计目的。我们在对学习 Golang 的过程中不能无脑的接纳它的一切,而是应该根据它自身的特性判断在实际项目应用中的情况。


    适用场景


    经过前文关于 Golang 各个维度的讨论,我们可以得出结论:Go 语言并不是后端开发的万能药。在实际开发工作中,开发者应该避免在任何情况下无脑使用 Golang 作为后端开发语言。相反,工程师在决定技术选型之前应该全面了解候选技术(语言、框架或架构)的方方面面,包括候选技术与业务需求的切合度,与开发团队的融合度,以及其学习、开发、时间成本等因素。笔者在学习了包括前后端的一些编程语言之后,发现它们各自有各自的优势,也有相应的劣势。如果一门编程语言能广为人知,那它绝对不会是一门糟糕语言。因此,笔者不会断言 “XXX 是世界上最好的语言“,而是给读者分享个人关于特定应用场景下技术选型的思路。当然,本文是针对 Go 语言的技术文,接下来笔者将分享一下个人认为 Golang 最适合的应用场景。


    分布式应用


    Golang 是非常适合在分布式应用场景下开发的。分布式应用的主要目的是尽可能多的利用计算资源和网络带宽,以求最大化系统的整体性能和效率,其中重要的需求功能就是并发(Concurrency)。而 Go 是支持高并发异步编程方面的佼佼者。前面已经提到,Go 语言内置了协程(Goroutine)通道(Channel)两大并发特性,这使后端开发者进行异步编程变得非常容易。Golang 中还内置了sync,包含 Mutex(互斥锁)、WaitGroup(等待组)、Pool(临时对象池)等接口,帮助开发者在并发编程中能更安全的掌控 Go 程序的并发行为。Golang 还有很多分布式应用开发工具,例如分布式储存系统(Etcd、SeaweedFS)、RPC 库(gRPC、Thrift)、主流数据库 SDK(mongo-driver、gnorm、redigo)等。这些都可以帮助开发者有效的构建分布式应用。


    网络爬虫


    稍微了解网络爬虫的开发者应该会听说过 Scrapy,再不济也是 Python。市面上关于 Python 网络爬虫的技术书籍数不胜数,例如崔庆才的 《Python 3 网络开发实战》 和韦世东的《Python 3 网络爬虫宝典 用 Python 编写的高性能爬虫框架 Scrapy》,自发布以来一直是爬虫工程师的首选。


    不过,由于近期 Go 语言的迅速发展,越来越多的爬虫工程师注意到用 Golang 开发网路爬虫的巨大优势。其中,用 Go 语言编写的 Colly 爬虫框架,如今在 Github 上已经有 13k+ 标星。其简洁的 API 以及高效的采集速度,吸引了很多爬虫工程师,占据了爬虫界一哥 Scrapy 的部分份额。前面已经提到,Go 语言内置的并发特性让严重依赖网络带宽的爬虫程序更加高效,很大的提高了数据采集效率。另外,Go 语言作为静态语言,相对于动态语言 Python 来说有更好的约束下,因此健壮性和稳定性都更好。


    后端 API


    Golang 有很多优秀的后端框架,它们大部分都非常完备的支持了现代后端系统的各种功能需求:RESTful API、路由、中间件、配置、鉴权等模块。而且用 Golang 写的后端应用性能很高,通常有非常快的响应速度。笔者曾经在开源爬虫管理平台 Crawlab 中用 Golang 重构了 Python 的后端 API,响应速度从之前的几百毫秒优化到了几十毫秒甚至是几毫秒,用实践证明 Go 语言在后端性能方面全面碾压动态语言。Go 语言中比较知名的后端框架有 GinBeegoEchoIris


    当然,这里并不是说用 Golang 写后端就完全是一个正确的选择。笔者在工作中会用到 Java 和 C#,用了各自的主流框架(SpringBoot 和 .Net Core)之后,发现这两门传统 OOP 语言虽然语法啰嗦,但它们的语法特性很丰富,特别是泛型,能够轻松应对一些逻辑复杂、重复性高的业务需求。因此,笔者认为在考虑用 Go 来编写后端 API 时候,可以提前调研一下 Java 或 C#,它们在写后端业务功能方面做得非常棒。


    总结


    本篇文章从 Go 语言的主要语法特性入手,循序渐进分析了 Go 语言作为后端编程语言的优点和缺点,以及其在实际软件项目开发中的试用场景。笔者认为 Go 语言与其他语言的主要区别在于语法简洁天然支持并发面向接口编程错误处理等方面,并且对各个语言特性在正反两方面进行了分析。最后,笔者根据之前的分析内容,得出了 Go 语言作为后端开发编程语言的适用场景,也就是分布式应用网络爬虫以及后端API。当然,Go 语言的实际应用领域还不限于此。实际上,不少知名数据库都是用 Golang 开发的,例如时序数据库 Prometheus 和 InfluxDB、以及有 NewSQL 之称的 TiDB。此外,在机器学习方面,Go 语言也有一定的优势,只是目前来说,Google 因为 Swift 跟 TensorFlow 的意向合作,似乎还没有大力推广 Go 在机器学习方面的应用,不过一些潜在的开源项目已经涌现出来,例如 GoLearn、GoML、Gorgonia 等。


    在理解 Go 语言的优势和适用场景的同时,我们必须意识到 Go 语言并不是全能的。它相较于其他一些主流框架来说也有一些缺点。开发者在准备采用 Go 作为实际工作开发语言的时候,需要全面了解其语言特性,从而做出最合理的技术选型。就像打网球一样,不仅需要掌握正反手,还要会发球、高压球、截击球等技术动作,这样才能把网球打好。


    作者:MarvinZhang
    链接:https://juejin.cn/post/6943524790636544036
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    写给前端工程师的 Flutter 教程

    最爱折腾的就是前端工程师了,从 jQuery 折腾到 AngularJs,再折腾到 Vue、React。 最爱跨端的也是前端工程师,从 phonegap,折腾到 React Native,这不又折腾到了 Flutter。 图啥? 低成本地为用户带来更优秀的用户...
    继续阅读 »

    最爱折腾的就是前端工程师了,从 jQuery 折腾到 AngularJs,再折腾到 Vue、React。 最爱跨端的也是前端工程师,从 phonegap,折腾到 React Native,这不又折腾到了 Flutter。


    图啥?


    低成本地为用户带来更优秀的用户体验


    目前来说Flutter可能是其中最优秀的一种方案了。


    Flutter 是什么?



    Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.



    Flutter是由原 Google Chrome 团队成员,利用 Chrome 2D 渲染引擎,然后精简 CSS 布局演变而来。


    Flutter 架构


    或者更详细的版本




    • Flutter 在各个原生的平台中,使用自己的 C++的引擎渲染界面,没有使用 webview,也不像 RN、NativeScript 一样使用系统的组件。简单来说平台只是给 Flutter 提供一个画布。

    • 界面使用 Dart 语言开发,貌似唯一支持 JIT,和 AOT 模式的强类型语言。

    • 写法非常的现代,声明式,组件化,Composition > inheritance,响应式……就是现在前端流行的这一套 😄

    • 一套代码搞定所有平台。


    Flutter 为什么快?Flutter 相比 RN 的优势在哪里?


    从架构中实际上已经能看出 Flutter 为什么快,至少相比之前的当红炸子鸡 React Native 快的原因了。



    • Skia 引擎,Chrome, Chrome OS,Android,Firefox,Firefox OS 都以此作为渲染引擎。

    • Dart 语言可以 AOT 编译成 ARM Code,让布局以及业务代码运行的最快,而且 Dart 的 GC 针对 Flutter 频繁销毁创建 Widget 做了专门的优化。

    • CSS 的的子集 Flex like 的布局方式,保留强大表现能力的同时,也保留了性能。

    • Flutter 业务书写的 Widget 在渲染之前 diff 转化成 Render Object,对,就像 React 中的 Virtual DOM,以此来确保开发体验和性能。


    而相比 React Native:



    • RN 使用 JavaScript 来运行业务代码,然后 JS Bridge 的方式调用平台相关组件,性能比有损失,甚至平台不同 js 引擎都不一样。

    • RN 使用平台组件,行为一致性会有打折,或者说,开发者需要处理更多平台相关的问题。


    而具体两者的性能测试,可以看这里,结论是 Flutter,在 CPU,FPS,内存稳定上均优于 ReactNative。


    Dart 语言


    在开始 Flutter 之前,我们需要先了解下 Dart 语言……


    Dart 是由 Google 开发,最初是想作为 JavaScript 替代语言,但是失败沉寂之后,作为 Flutter 独有开发语言又焕发了第二春 😂。


    实际上即使到了 2.0,Dart 语法和 JavaScriptFlutter非常的相像。单线程,Event Loop……


    Dart Event Loop模型


    当然作为一篇写给前端工程师的教程,我在这里只想写写 JavaScript 中暂时没有的,Dart 中更为省心,也更“甜”的东西。



    • 不会飘的this

    • 强类型,当然前端现在有了 TypeScript 😬

    • 强大方便的操作符号:

      • ?. 方便安全的foo?.bar取值,如果 foo 为null,那么取值为null

      • ?? condition ? expr1 : expr2 可以简写为expr1 ?? expr2

      • =和其他符号的组合: *=~/=&=|= ……

      • 级联操作符(Cascade notation ..)




    // 想想这样省了多少变量声明
    querySelect('#button')
    ..text ="Confirm"
    ..classes.add('important')
    ..onClick.listen((e) => window.alert('Confirmed'))

    甚至可以重写操作符


    class Vector {
    final int x, y;

    Vector(this.x, this.y);

    Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
    Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

    // Operator == and hashCode not shown. For details, see note below.
    // ···
    }

    void main() {
    final v = Vector(2, 3);
    final w = Vector(2, 2);

    assert(v + w == Vector(4, 5));
    assert(v - w == Vector(0, 1));
    }

    注:重写==,也需要重写 Object hashCodegetter


    class Person {
    final String firstName, lastName;

    Person(this.firstName, this.lastName);

    // Override hashCode using strategy from Effective Java,
    // Chapter 11.
    @override
    int get hashCode {
    int result = 17;
    result = 37 * result + firstName.hashCode;
    result = 37 * result + lastName.hashCode;
    return result;
    }

    // You should generally implement operator == if you
    // override hashCode.
    @override
    bool operator ==(dynamic other) {
    if (other is! Person) return false;
    Person person = other;
    return (person.firstName == firstName &&
    person.lastName == lastName);
    }
    }

    void main() {
    var p1 = Person('Bob', 'Smith');
    var p2 = Person('Bob', 'Smith');
    var p3 = 'not a person';
    assert(p1.hashCode == p2.hashCode);
    assert(p1 == p2);
    assert(p1 != p3);
    }

    这点在 diff 对象的时候尤其有用。


    lsolate


    Dart 运行在独立隔离的 iSolate 中就类似 JavaScript 一样,单线程事件驱动,但是 Dart 也开放了创建其他 isolate,充分利用 CPU 的多和能力。


    loadData() async {
       // 通过spawn新建一个isolate,并绑定静态方法
       ReceivePort receivePort =ReceivePort();
       await Isolate.spawn(dataLoader, receivePort.sendPort);
       
       // 获取新isolate的监听port
       SendPort sendPort = await receivePort.first;
       // 调用sendReceive自定义方法
    List dataList = await sendReceive(sendPort, 'https://hicc.me/posts');
       print('dataList $dataList');
    }

    // isolate的绑定方法
    static dataLoader(SendPort sendPort) async{
       // 创建监听port,并将sendPort传给外界用来调用
       ReceivePort receivePort =ReceivePort();
       sendPort.send(receivePort.sendPort);
       
       // 监听外界调用
       await for (var msg in receivePort) {
         String requestURL =msg[0];
         SendPort callbackPort =msg[1];
       
         Client client = Client();
         Response response = await client.get(requestURL);
         List dataList = json.decode(response.body);
         // 回调返回值给调用者
         callbackPort.send(dataList);
      }    
    }

    // 创建自己的监听port,并且向新isolate发送消息
    Future sendReceive(SendPort sendPort, String url) {
       ReceivePort receivePort =ReceivePort();
       sendPort.send([url, receivePort.sendPort]);
       // 接收到返回值,返回给调用者
       return receivePort.first;
    }

    当然 Flutter 中封装了compute,可以方便的使用,譬如在其它 isolate 中解析大的 json


    Dart UI as Code


    在这里单独提出来的意义在于,从 React 开始,到 Flutter,到最近的 Apple SwiftUI,Android Jetpack Compose 声明式组件写法越发流行,Web 前端使用 JSX 来让开发者更方便的书写,而 Flutter,SwiftUI 则直接从优化语言本身着手。


    函数类的命名参数


    void test({@required int age,String name}) {
    print(name);
    print(age);
    }
    // 解决函数调用时候,参数不明确的问题
    test(name:"hicc",age: 30)

    // 这样对于组件的使用尤为方便
    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(),
    body: Container(),
    floatingActionButton:FloatingActionButton()
    );
    }
    }

    大杀器:Collection If 和 Collection For


    // collection If
    Widget build(BuildContext context) {
    return Row(
    children: [
    IconButton(icon: Icon(Icons.menu)),
    Expanded(child: title),
    if (!isAndroid)
    IconButton(icon: Icon(Icons.search)),
    ],
    );
    }
    // Collect For
    var command = [
    engineDartPath,
    frontendServer,
    for (var root in fileSystemRoots) "--filesystem-root=$root",
    for (var entryPoint in entryPoints)
    if (fileExists("lib/$entryPoint.json")) "lib/$entryPoint",
    mainPath
    ];

    更多 Dart 2.3 对此的优化看这里


    Flutter 怎么写


    到这里终于到正题了,如果熟悉 web 前端,熟悉 React 的话,你会对下面要讲的异常的熟悉。


    UI=F(state)


    Flutter App 的一切从lib/main.dart文件的 main 函数开始:


    import 'package:flutter/material.dart';

    void main() => runApp(MyApp());

    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Welcome to Flutter',
    home: Scaffold(
    appBar: AppBar(
    title: Text('Welcome to Flutter'),
    ),
    body: Center(
    child: Text('Hello World'),
    ),
    ),
    );
    }
    }

    Dart 类 build 方法返回的便是 Widget,在 Flutter 中一切都是 Widget,包括但不限于



    • 结构性元素,menu,button 等

    • 样式类元素,font,color 等

    • 布局类元素,padding,margin 等

    • 导航

    • 手势


    Widget 是 Dart 中特殊的类,通过实例化(Dart 中new 是可选的)相互嵌套,你的这个 App 就是形如下图的一颗组件树(Dart 入口函数的概念,main.dart -> main())。


    Flutter Widget Tree


    Widget 布局


    上说过 Flutter 布局思路来自 CSS,而 Flutter 中一切皆 Widget,因此整体布局也很简单:



    • 容器组件 Container

      • decoration 装饰属性,设置背景色,背景图,边框,圆角,阴影和渐变等

      • margin

      • padding

      • alignment

      • width

      • height



    • Padding,Center

    • Row,Column,Flex

    • Wrap, Flow 流式布局

    • stack, z 轴布局

    • ……


    更多可以看这里


    Flutter 中 Widget 可以分为三类,形如 React 中“展示组件”、“容器组件”,“context”。


    StatelessWidget


    这个就是 Flutter 中的“展示组件”,自身不保存状态,外部参数变化就销毁重新创建。Flutter 建议尽量使用无状态的组件。


    StatefulWidget


    状态组件就是类似于 React 中的“容器组件”了,Flutter 中状态组件写法会稍微不一样。


    class Counter extends StatefulWidget {
    // This class is the configuration for the state. It holds the
    // values (in this case nothing) provided by the parent and used by the build
    // method of the State. Fields in a Widget subclass are always marked "final".

    @override
    _CounterState createState() => _CounterState();
    }

    class _CounterState extends State {
    int _counter = 0;

    void _increment() {
    setState(() {
    // This call to setState tells the Flutter framework that
    // something has changed in this State, which causes it to rerun
    // the build method below so that the display can reflect the
    // updated values. If you change _counter without calling
    // setState(), then the build method won't be called again,
    // and so nothing would appear to happen.
    _counter++;
    });
    }

    @override
    Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance
    // as done by the _increment method above.
    // The Flutter framework has been optimized to make rerunning
    // build methods fast, so that you can just rebuild anything that
    // needs updating rather than having to individually change
    // instances of widgets.
    return Row(
    children: [
    RaisedButton(
    onPressed: _increment,
    child: Text('Increment'),
    ),
    Text('Count: $_counter'),
    ],
    );
    }
    }

    可以看到 Flutter 中直接使用了和 React 中同名的setState方法,不过不会有变量合并的东西,当然也有生命周期


    Flutter StatefulWidget 声明周期


    可以看到一个有状态的组件需要两个 Class,这样写的原因在于,Flutter 中 Widget 都是 immmutable 的,状态组件的状态保存在 State 中,组件仍然每次重新创建,Widget 在这里只是一种对组件的描述,Flutter 会 diff 转换成 Element,然后转换成 RenderObject 才渲染。


    Flutter render object


    Flutter Widget 更多的渲染流程可以看这里


    实际上 Widget 只是作为组件结构一种描述,还可以带来的好处是,你可以更方便的做一些主题性的组件, Flutter 官方提供的Material Components widgetsCupertino (iOS-style) widgets质量就相当高,再配合 Flutter 亚秒级的Hot Reload,开发体验可以说挺不错的。




    State Management


    setState()可以很方便的管理组件内的数据,但是 Flutter 中状态同样是从上往下流转的,因此也会遇到和 React 中同样的问题,如果组件树太深,逐层状态创建就显得很麻烦了,更不要说代码的易读和易维护性了。


    InheritedWidget


    同样 Flutter 也有个context一样的东西,那就是InheritedWidget,使用起来也很简单。


    class GlobalData extends InheritedWidget {
    final int count;
    GlobalData({Key key, this.count,Widget child}):super(key:key,child:child);

    @override
    bool updateShouldNotify(GlobalData oldWidget) {
    return oldWidget.count != count;
    }

    static GlobalData of(BuildContext context) => context.inheritFromWidgetOfExactType(GlobalData);
    }

    class MyApp extends StatelessWidget {
    // This widget is the root of your application.
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
    primarySwatch: Colors.blue,
    ),
    home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
    }
    }

    class MyHomePage extends StatefulWidget {
    MyHomePage({Key key, this.title}) : super(key: key);

    final String title;

    @override
    _MyHomePageState createState() => _MyHomePageState();
    }

    class _MyHomePageState extends State {
    int _counter = 0;

    void _incrementCounter() {
    _counter++;
    });
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text(widget.title),
    ),
    body: GlobalData(
    count: _counter,
    child: Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
    Text(
    'You have pushed the button this many times:',
    ),
    Text(
    '$_counter',
    style: Theme.of(context).textTheme.display1,
    ),
    Body(),
    Body2()
    ],
    ),
    ),
    ),
    floatingActionButton: FloatingActionButton(
    onPressed: _incrementCounter,
    tooltip: 'Increment',
    child: Icon(Icons.add),
    ),
    );
    }
    }

    class Body extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    GlobalData globalData = GlobalData.of(context);
    return Text(globalData.count.toString());
    }
    }

    class Body2 extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    // TODO: implement build
    GlobalData globalData = GlobalData.of(context);
    return Text(globalData.count.toString());
    }

    具体实现原理可以参考这里,不过 Google 封装了一个更为上层的库provider,具体使用可以看这里


    BlOC


    BlOC是 Flutter team 提出建议的另一种更高级的数据组织方式,也是我最中意的方式。简单来说:


    Bloc = InheritedWidget + RxDart(Stream)


    Dart 语言中内置了 Steam,Stream ~= Observable,配合RxDart, 然后加上StreamBuilder会是一种异常强大和自由的模式。


    class GlobalData extends InheritedWidget {
    final int count;
    final Stream timeInterval$ = new Stream.periodic(Duration(seconds: 10)).map((time) => new DateTime.now().toString());
    GlobalData({Key key, this.count,Widget child}):super(key:key,child:child);

    @override
    bool updateShouldNotify(GlobalData oldWidget) {
    return oldWidget.count != count;
    }

    static GlobalData of(BuildContext context) => context.inheritFromWidgetOfExactType(GlobalData);

    }

    class TimerView extends StatelessWidget {

    @override
    Widget build(BuildContext context) {
    GlobalData globalData = GlobalData.of(context);
    return StreamBuilder(
    stream: globalData.timeInterval$,
    builder: (context, snapshot) {
    return Text(snapshot?.data ?? '');
    }
    );
    }
    }

    当然 Bloc 的问题在于



    • 学习成本略高,Rx 的概念要吃透,不然你会抓狂

    • 自由带来的问题是,可能代码不如 Redux 类的规整。


    顺便,今年 Apple 也拥抱了响应式,Combine(Rx like) + SwiftUI 也基本等于 Bloc 了。


    所以,Rx 还是要赶紧学起来 😬


    除去 Bloc,Flutter 中还是可以使用其他的方案,譬如:



    展开来说现在的前端开发使用强大的框架页面组装已经不是难点了。开发的难点在于如何组合富交互所需的数据,也就是上面图中的state部分。


    更具体来说,是怎么优雅,高效,易维护地处理短暂数据(ephemeral state)setState()和需要共享的 App State 的问题,这是个工程性的问题,但往往也是日常开发最难的事情了,引用 Redux 作者 Dan 的一句:



    “The rule of thumb is:Do whatever is less awkward.”



    到这里,主要的部分已经讲完了,有这些已经可以开发出一个不错的 App 了。剩下的就当成一个 bonus 吧。




    测试


    Flutter debugger,测试都是出场自带,用起来也不难。


    // 测试在/test/目录下面
    void main() {

    testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(MyApp());

    // Verify that our counter starts at 0.
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Tap the '+' icon and trigger a frame.
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    // Verify that our counter has incremented.
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
    });
    }

    包管理,资源管理


    类似与 JavaScript 的 npm,Flutter,也就是 Dart 也有自己的包仓库。不过项目包的依赖使用 yaml 文件来描述:


    name: app
    description: A new Flutter project.
    version: 1.0.0+1

    environment:
    sdk: ">=2.1.0 <3.0.0"

    dependencies:
    flutter:
    sdk: flutter

    cupertino_icons: ^0.1.2

    生命周期


    移动应用总归需要应用级别的生命周期,flutter 中使用生命周期钩子,也非常的简单:


    class MyApp extends StatefulWidget {
    @override
    _MyAppState createState() => new _MyAppState();
    }

    class _MyAppState extends State with WidgetsBindingObserver {
    @override
    void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    }

    @override
    void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
    }

    @override
    void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
    case AppLifecycleState.inactive:
    print('AppLifecycleState.inactive');
    break;
    case AppLifecycleState.paused:
    print('AppLifecycleState.paused');
    break;
    case AppLifecycleState.resumed:
    print('AppLifecycleState.resumed');
    break;
    case AppLifecycleState.suspending:
    print('AppLifecycleState.suspending');
    break;
    }
    super.didChangeAppLifecycleState(state);
    }

    @override
    Widget build(BuildContext context) {
    return Container();
    }
    }

    使用原生能力


    和 ReactNative 类似,Flutter 也是使用类似事件的机制来使用平台相关能力。


    Flutter platform channels


    Flutter Web, Flutter Desktop


    这些还在开发当中,鉴于对 Dart 喜欢,以及对 Flutter 性能的乐观,这些倒是很值得期待。


    Flutter web 架构


    还记得平台只是给 Flutter 提供一个画布么,Flutter Desktop 未来更是可以大有可为 😄,相关可以看这里


    最后每种方案,每种技术都有优缺点,甚至技术的架构决定了,有些缺陷可能永远都没法改进,所以 🤔


    作者:小刀c
    链接:https://juejin.cn/post/6844903906800041998
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Flutter | 求求你们了,切换 Widget 的时候加上动画吧

    平时我们在切换 Widget 的时候是怎样的呢?有没有动画效果?是不是直接改变了一个 Widget?类似于这样的:如果是的话,那么今天所说的 Widget,绝对符合你的口味。那如何在 Flutter 当中切换 Widget 的时候加上特效?完成这样的效果?An...
    继续阅读 »

    平时我们在切换 Widget 的时候是怎样的呢?

    有没有动画效果?是不是直接改变了一个 Widget?

    类似于这样的:

    如果是的话,那么今天所说的 Widget,绝对符合你的口味。

    那如何在 Flutter 当中切换 Widget 的时候加上特效?完成这样的效果?

    AnimatedSwitcher 了解一下。

    AnimatedSwitcher

    官方介绍

    话不多说,功能我们已经了解,再来看一下官方的介绍:

    A widget that by default does a FadeTransition between a new widget and the widget previously set on the AnimatedSwitcher as a child.

    If they are swapped fast enough (i.e. before duration elapses), more than one previous child can exist and be transitioning out while the newest one is transitioning in.

    If the "new" child is the same widget type and key as the "old" child, but with different parameters, then AnimatedSwitcher will not do a transition between them, since as far as the framework is concerned, they are the same widget and the existing widget can be updated with the new parameters. To force the transition to occur, set a Key on each child widget that you wish to be considered unique (typically a ValueKey on the widget data that distinguishes this child from the others).

    大致意思就是:

    默认情况下是执行透明度的动画。

    如果交换速度足够快,则存在多个子级,但是在新子级传入的时候将它移除。

    如果新 Widget 和 旧 Widget 的类型和键相同,但是参数不同,那么也不会进行转换。如果想要进行转换,那么要添加一个 Key。

    构造函数

    再来看构造函数,来确定如何使用:

    const AnimatedSwitcher({
    Key key,
    this.child,
    @required this.duration,
    this.reverseDuration,
    this.switchInCurve = Curves.linear,
    this.switchOutCurve = Curves.linear,
    this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder,
    this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder,
    }) : assert(duration != null),
    assert(switchInCurve != null),
    assert(switchOutCurve != null),
    assert(transitionBuilder != null),
    assert(layoutBuilder != null),
    super(key: key);
    复制代

    来解释一下每个参数:

    1. child:不用多说
    2. duration:动画持续时间
    3. reverseDuration:从新的 Widget 到旧的 Widget 动画持续时间,如果不设置则为 duration 的值
    4. switchInCurve:动画效果
    5. switchOutCurve:同上
    6. transitionBuilder:设置一个新的转换动画
    7. layoutBuilder:包装新旧 Widget 的组件,默认是一个 Stack

    其中必要参数就是一个 duration,那既然知道如何使用了,那就开撸。

    简单例子

    前面我们看的图,就是在对 AppBar上的 actions 进行操作,

    其实这个例子在实际开发当中经常存在,肯定要删除一些东西的嘛,然后选中了以后批量删除。

    那这里也不多说,直接上代码,然后解释:

    class _AnimatedSwitcherPageState extends State<AnimatedSwitcherPage> {
    IconData _actionIcon = Icons.delete;

    @override
    void initState() {
    super.initState();
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text('AnimatedSwitcherPage'),
    actions: <Widget>[
    AnimatedSwitcher(
    transitionBuilder: (child, anim){
    return ScaleTransition(child: child,scale: anim);
    },
    duration: Duration(milliseconds: 300),
    child: IconButton(
    key: ValueKey(_actionIcon),
    icon: Icon(_actionIcon),
    onPressed: () {
    setState(() {
    if (_actionIcon == Icons.delete)
    _actionIcon = Icons.done;
    else
    _actionIcon = Icons.delete;
    });
    }),
    )
    ],
    ),
    body: Container());
    }
    }
    复制代

    我们定义的是一个 StatefulWidget,因为在切换 Widget 的时候要调用 setState()

    下面来说一下整个流程:

    1. 首先定义好我们初始化的 Icon的数据为 Icons.delete
    2. 在 AppBar 的 actions 里面加入 AnimatedSwitcher
    3. 设置 transitionBuilder 为 缩放动画 ScaleTransition
    4. 给 AnimatedSwitcher 的 child 为 IconButton
    5. 因为前面官方文档说过,如果 Widget 类型一样,只是数据不一样,那么想要动画,就必须添加 Key。
    6. 所以我们给 IconButton 添加了一个 ValueKey,值就为定义好的 IconData
    7. 最后在点击事件中切换两个 Icon 就完成了

    最后再看一下效果:

    总结

    使用该控件最应该注意的点就是 Key 的问题,一定要记住:

    如果新 Widget 和 旧 Widget 的类型和键相同,但是参数不同,那么也不会进行转换。如果想要进行转换,那么要添加一个 Key。

    完整代码已经传至GitHub:github.com/wanglu1209/…


    作者:Flutter笔记
    链接:https://juejin.cn/post/6844903897912311821
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    苍老师的 "码" 是怎么打上的

    --OpenCV初体验,Swift和C++混编 文档更新说明 2017年10月27日 v1.0 初稿 2017年10月28日 v1.1 添加Objective-C++编译方法 ...
    继续阅读 »

    --OpenCV初体验,Swift和C++混编


    文档更新说明



    • 2017年10月27日 v1.0 初稿

    • 2017年10月28日 v1.1 添加Objective-C++编译方法



    提到OpenCV,相信大多数人都听说过,应用领域非常广泛,使用C++开发,天生具有跨平台的优势,我们学习一次,就可以在各个平台使用,这个还是很具有诱惑力的
    本文主要记录我第一次使用OpenCV,在iOS开发平台上面搭建开发环境,并且实现一个简单的马赛克功能
    开发环境:Swift4XCode 9.0



    1、什么是OpenCV?



    • 由英特尔公司于1999年发起并参与开发,至今已有18年历史

    • OpenCV的全称是Open Source Computer Vision Library

    • 是一个跨平台开源计算机视觉库,可用于开发实时的图像处理计算机视觉以及模式识别程序。

    • 支持C/C++JavaPythonOCSwiftRuby等等语言

    • 支持WindowsAndroidMaemoFreeBSDOpenBSDiOSLinuxMac OS


    2、难点,思路



    • 由于我们使用的是Swift,由于目前还不能在Swift中使用C++的类,所以我们得想一个方法,在Swift中调用C++的类

    • 其实方法很简单,Swift天生具有跟Objective-C++混编的能力,而Objective-C++里面是可以直接使用C++的类的,上面的问题也就解决了


    swift-c++handle


    3、马赛克原理



    • 其实把图片的像素密度调低,就可以出现马赛克效果了

    • 开始做马赛克之前,需要定一个马赛克的级别,表示原图中每几个像素变成新图里面的一个像素

    • 取一小块区域左上角的一个像素,并把这个像素填充到整个小区域内

    • 如下图,左边是原图,右边是经过变换之后的图,假设马赛克级别为3,每个数字表示的区域就是处理的一个小单元,取这个最小单元左上角的颜色,填充整个小单元就OK了


    马赛克原理


    4、开动工程


    4.1、搭建c++和swift混编环境



    我们首先要搭建一个c++的环境,然后才能进行c++的开发,而c++环境可以通过iostream里面的cout函数验证




    1. 首先,我们使用xCode新建一个swiftiOS项目

    2. 在工程内,新建一个Objective-C类,继承NSObject,这里会自动提示我们是否为项目添加桥接文件,选择添加即可(桥接文件是用来向Swift暴露Objective-C方法的)


    3. 因为我们要使用Objective-C++,而把Objective-C转成Objective-C++的方法有两种



      • .m文件的后缀名改为.mm,xCode就会自动识别我们的代码为Objective-C++了(xCode会通过后缀名自动识别源文件类型)


      • 选中要修改的.m文件,在右边的Type属性修改成:Objective-C++ Source(也可以手动指定源文件类型)







    4. 在刚才的.mm文件中,添加一个测试方法,在这里测试一下C++环境是否搭建成功


      #import "MyUtil.h"
      #import <iostream> // 记得导入iostrem头文件

      using namespace std;

      @implementation MyUtil

      + (void)testCpp {
      cout << "Hello Swift and Cpp" << endl;
      }


    5. 在前面xCode自动创建的桥接文件中暴露我们的测试方法头文件




    6. Swift中调用测试方法,控制台输出 "Hello Swift and Cpp" 就正常了


      import UIKit

      class ViewController: UIViewController {
      override func viewDidLoad() {
      super.viewDidLoad()
      // 测试方法
      MyUtil.testCpp()
      }
      }


    4.3、导入OpenCV动态库



    iOS开发中导入OpenCV的库其实非常简单,直接拖拽到工程文件就行了




    1. 首先去OpenCV官网下载我们需要的framework,下载地址:opencv.org/releases.ht…,选择最新版本的iOS pack即可


    2. 下载下来之后解压,然后拖拽到我们的工程目录,设置如下图




    3. 设置我们的工程链接OpenCV动态库




    4. build一下,确认不会报错




    4.4、实现马赛克函数



    接下来就是干代码的时候了





    1. 首先要在.m文件中,导入OpenCV的头文件,导入头文件之后代码如下,这里有几个坑要注意:



      • 不要在.h文件中去导入OpenCV的相关头文件,否则会报错,错误信息: Core.hpp header must be compiled as C++,看到这个问题,赶紧把头文件移动到.m文件中去

      • 还有就是OpenCV的头文件最好放在#import <UIKit/UIKit.h>之前,否则也会报一个错误: enum { NO, FEATHER, MULTI_BAND }; Expected identifier


      //导入OpenCV框架 最好放在Foundation.h UIKit.h之前
      //核心头文件
      #import <opencv2/opencv.hpp>
      //对iOS支持
      #import <opencv2/imgcodecs/ios.h>
      //导入矩阵帮助类
      #import <opencv2/highgui.hpp>
      #import <opencv2/core/types.hpp>

      #import "MyUtil.h"
      #import <iostream>

      using namespace std;
      using namespace cv;


    2. 实现马赛克函数


      +(UIImage*)opencvImage:(UIImage*)image level:(int)level{
      //实现功能
      //第一步:将iOS图片->OpenCV图片(Mat矩阵)
      Mat mat_image_src;
      UIImageToMat(image, mat_image_src);

      //第二步:确定宽高
      int width = mat_image_src.cols;
      int height = mat_image_src.rows;

      //在OpenCV里面,必须要先把ARGB的颜色空间转换成RGB的,否则处理会失败(官方例程里面,每次处理都会有这个操作)
      //ARGB->RGB
      Mat mat_image_dst;
      cvtColor(mat_image_src, mat_image_dst, CV_RGBA2RGB, 3);

      //为了不影响原始图片,克隆一张保存
      Mat mat_image_clone = mat_image_dst.clone();

      //第三步:马赛克处理
      int xMax = width - level;
      int yMax = height - level;

      for (int y = 0; y <= yMax; y += level) {
      for (int x = 0; x <= xMax; x += level) {
      //让整个矩形区域颜色值保持一致
      //mat_image_clone.at<Vec3b>(i, j)->像素点(颜色值组成->多个)->ARGB->数组
      //mat_image_clone.at<Vec3b>(i, j)[0]->R值
      //mat_image_clone.at<Vec3b>(i, j)[1]->G值
      //mat_image_clone.at<Vec3b>(i, j)[2]->B值
      Scalar scalar = Scalar(
      mat_image_clone.at<Vec3b>(y, x)[0],
      mat_image_clone.at<Vec3b>(y, x)[1],
      mat_image_clone.at<Vec3b>(y, x)[2]);

      //取出要处理的矩形区域
      Rect2i mosaicRect = Rect2i(x, y, level, level);
      Mat roi = mat_image_dst(mosaicRect);

      //将前面处理的小区域拷贝到要处理的区域
      //CV_8UC3的含义
      //CV_:表示框架命名空间
      //8表示:32位色->ARGB->8位 = 1字节 -> 4个字节
      //U: 无符号类型
      //C分析:char类型
      //3表示:3个通道->RGB
      Mat roiCopy = Mat(mosaicRect.size(), CV_8UC3, scalar);
      roiCopy.copyTo(roi);
      }
      }

      //第四步:将OpenCV图片->iOS图片
      return MatToUIImage(mat_image_dst);
      }


    4.5、在swift中调用马赛克函数



    函数已经实现了,接下来就是在Swift中调用了





    1. 为了便于测试,我们在storyboard中搭一个简单的界面,在按钮中切换马赛克图片和原图,界面如下:
      苍井空




    2. 在按钮点击事件中调用上面的马赛克函数即可


      @IBOutlet weak var imageView: UIImageView!
      /// 显示原图按钮
      @IBAction func origImageBtnClick(_ sender: Any) {
      imageView.image = UIImage(named: "pic.jpg")
      }

      /// 显示马赛克图片
      @IBAction func mosaicImageBtnClick(_ sender: Any) {
      guard let origImage = imageView.image else {
      return
      }

      let mosaicImage = MyUtil.opencvImage(origImage, level: 20)
      imageView.image = mosaicImage
      }


    3. 效果如下,左边的是原图,右边的是马赛克之后的图片,就这样,苍老师的码就打上去啦~





    5、后记


    对于C++,很多人并不陌生,不过我想对于iOS开发者来说,用过C++的童鞋并不多吧,我一直很崇拜那些C++大神,因为通过C++,我们可以很方便的实现跨平台开发,就我们今天的马赛克代码来说,移植到安卓平台,里面的东西也只需要做很小部分的修改,就可以非常完美的适配(当然,安卓的开发环境么有iOS这么简单),所以,掌握和使用C++的性价比还是很高的。


    完整代码已经上传到github: github.com/fengqiangbo…,不过移除了OpenCV.framework,因为太多传不上去,欢迎大家给Star

    作者:加勒比海带王
    链接:https://juejin.cn/post/6844903506944458760
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Jetpack Compose 动画初步了解和使用

    Animatable compose 使用 Animatable 来实现动画效果,Animatable 可以理解为一个可以作为动画属性的 Value 持有者。当它持有的 Value 通过 animateTo 更新时,可以自动以动画的形式对这一过程进行演变。与传...
    继续阅读 »

    Animatable


    compose 使用 Animatable 来实现动画效果,Animatable 可以理解为一个可以作为动画属性的 Value 持有者。当它持有的 Value 通过 animateTo 更新时,可以自动以动画的形式对这一过程进行演变。与传统基于 View 实现的动画不同, 其内部使用协程计算动画的中间过程,所以触发函数 animateTo() 是用suspend 这大大保障了动画运行时的性能。基本的使用方式:


    @Composable
    fun Demo() {
    var flag by remember { mutableStateOf(false) }
    Box(
    Modifier
    .fillMaxSize()
    .background(Color.DarkGray),
    contentAlignment = Alignment.Center
    ) {
    val animate = remember { Animatable(32.dp, Dp.VectorConverter) }
    // 通过协程触发 animateTo()
    LaunchedEffect(key1 = flag) {
    animate.animateTo(if (flag) 32.dp else 144.dp)
    }
    Row(
    Modifier
    .size(animate.value) // size 在 animate 中取值
    .background(Color.Magenta)
    .clickable { flag = !flag }
    ) {}
    }
    }

    首先看 Animatable :


    androidx.compose.animation.core.Animatable

    public constructor Animatable<T, V : AnimationVector>(
    initialValue: T,
    typeConverter: TwoWayConverter<T, V>,
    visibilityThreshold: T?
    )


    • initialValue 很好理解,作为它的初始值传入,所谓的 Value 持有者持有的就是它。

    • typeConverter 是用来统一动画行为,可以做属性动画的值都通过这个converter 把不同类型的值都转化成 Float 进行动画计算,与对应的 AnimationVector 进行互相转化。

    • visibilityThreshold 判断动画逐渐变为目标值得阈值,可空,暂且按下不表。


    详细了解一下其中的 TwoWayConverter


    /**
    * [TwoWayConverter] class contains the definition on how to convert from an arbitrary type [T]
    * to a [AnimationVector], and convert the [AnimationVector] back to the type [T]. This allows
    * animations to run on any type of objects, e.g. position, rectangle, color, etc.
    */
    interface TwoWayConverter<T, V : AnimationVector> {
    /**
    * Defines how a type [T] should be converted to a Vector type (i.e. [AnimationVector1D],
    * [AnimationVector2D], [AnimationVector3D] or [AnimationVector4D], depends on the dimensions of
    * type T).
    */
    val convertToVector: (T) -> V
    /**
    * Defines how to convert a Vector type (i.e. [AnimationVector1D], [AnimationVector2D],
    * [AnimationVector3D] or [AnimationVector4D], depends on the dimensions of type T) back to type
    * [T].
    */
    val convertFromVector: (V) -> T
    }

    TwoWayConverter 用于定义如何把任意类型的值与可供动画使用的 AnimationVector 之前互相转化的方法,这样通过对它的封装就可以进行对任意属性类型做统一的动画计算。同时,根据动画所需的维度数据返回对应维度的封装 AnimationVectorXD ,这里所说的XD 是指数据维度的个数。例如:



    • androidx.compose.ui.unit.Dp 值转化为 AnimationVector 只有一个维度,也就是它的 value ,所以转化为与之对应的 AnimationVector1D

    • androidx.compose.ui.geometry.Size 中包含两个维度的数据:widthheight , 所以对转化为 AnimationVector2D

    • androidx.compose.ui.geometry.Rect 中包含四个数据维度:lefttoprightbottom,对应 AnimationVector4D


    同时,Compose 还对常用与动画的对象非常贴心的做了默认实现:



    • Float.Companion.VectorConverter: TwoWayConverter<Float, AnimationVector1D>

    • Int.Companion.VectorConverter: TwoWayConverter<Int, AnimationVector1D>

    • Rect.Companion.VectorConverter: TwoWayConverter<Rect, AnimationVector4D>

    • Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>

    • DpOffset.Companion.VectorConverter: TwoWayConverter<DpOffset, AnimationVector2D>

    • Size.Companion.VectorConverter: TwoWayConverter<Size, AnimationVector2D>

    • Offset.Companion.VectorConverter: TwoWayConverter<Offset, AnimationVector2D>

    • IntOffset.Companion.VectorConverter: TwoWayConverter<IntOffset, AnimationVector2D>

    • IntSize.Companion.VectorConverter: TwoWayConverter<IntSize, AnimationVector2D>


    至此,Animatable 有了初始值, 也有了值类型与对应动画数据的转换方式,那么只需要一个目标值,就满足触发动画的条件了。又因为动画数据的计算在协程中进行,那么我们此时只需在协程中触发 animateTo() 就可以了:


    // 通过协程触发 animateTo()
    LaunchedEffect(key = flag) {
    animate.animateTo(if (flag) 32.dp else 144.dp)
    }

    注意此处的协程 CoroutineScope 是通过 Composable 函数 LaunchedEffect 提供的,该函数内部实现了对于 composer 的优化,同时通过 remember 函数缓存状态,所以不会由于 recompose 的主动或被动调用而多次执行。


    AnimationSpec


    AnimationSpec 顾名思义支持对动画定义规范,以此实现自定义动画。


    查看 animateTo 函数的定义可以发现其第二个参数可以设置 animationSpec,它有一个默认的实现 defaultSpringSpec ,所以上面的例子中没有明确指定 animationSpec


    androidx.compose.animation.core.Animatable 
    public final suspend fun animateTo(
    targetValue: T,
    animationSpec: AnimationSpec<T> = defaultSpringSpec,
    initialVelocity: T = velocity,
    block: (Animatable<T, V>.() → Unit)? = null
    ): AnimationResult<T, V>

    spring


    defaultSpringSpec 是一个通过 spring 创建的基于弹簧的物理特性的动画:


    androidx.compose.animation.core AnimationSpec.kt 
    @Stable
    public fun <T> spring(
    dampingRatio: Float = Spring.DampingRatioNoBouncy,
    stiffness: Float = Spring.StiffnessMedium,
    visibilityThreshold: T? = null
    ): SpringSpec<T>

    val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = spring(
    dampingRatio = Spring.DampingRatioHighBouncy,
    stiffness = Spring.StiffnessMedium
    )
    )

    spring 接受两个参数,dampingRatiostiffness 。前者定义弹簧的弹性,默认值为 Spring.DampingRatioNoBouncy 。后者定义弹簧向 targetVaule 移动的速度。 默认值为 Spring.StiffnessMedium。基于物理特性的 spring 无法设置 duration。具体效果参考下图:


    animation-spring.gif


    tween


    androidx.compose.animation.core AnimationSpec.kt 
    @Stable
    public fun <T> tween(
    durationMillis: Int = DefaultDurationMillis,
    delayMillis: Int = 0,
    easing: Easing = FastOutSlowInEasing
    ): TweenSpec<T>

    tween 在指定的 durationMillis 内使用缓和曲线在起始值和结束值之间添加动画效果。动画曲线通过 Easing 添加。


    val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = tween(
    durationMillis = 300,
    delayMillis = 50,
    easing = LinearOutSlowInEasing
    )
    )

    keyframes


    keyframes 会根据在动画时长内的不同时间戳中指定的快照值添加动画效果。在任何给定时间,动画值都将插值到两个关键帧值之间。对于其中每个关键帧,可以指定 Easing 来确定插值曲线:


    val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = keyframes {
    durationMillis = 375
    0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
    0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
    0.4f at 75 // ms
    0.4f at 225 // ms
    }
    )

    snapTo(targetValue: T)


    androidx.compose.animation.core.Animatable
    public final suspend fun snapTo(
    targetValue: T
    ): Unit

    Animatable 还提供了一个 snapTo(targetValue) 的函数,这个函数允许直接设置它内部持有的 value 值,此过程不会产生任何动画,正在进行的动画也会被取消,某些场景可能需要动画开始前有一个初始值,可以使用此函数。


    一种更方便的使用方式:animate*AsState


    设置某一个属性的目标值,当对应属性值发生变化后,自动触发动画,过度到对应值。



    This Composable function is overloaded for different parameter types such as Float, Color, Offset, etc. When the provided targetValue is changed, the animation will run automatically. If there is already an animation in-flight when targetValue changes, the on-going animation will adjust course to animate towards the new target value.



    compose 提供了这几个覆盖基本场景的函数:



    • animateFloatAsState

    • animateDpAsState

    • animateSizeAsState

    • animateOffsetAsState

    • animateRectAsState

    • animateIntAsState

    • animateIntOffsetAsState

    • animateIntSizeAsState


    @Composable
    fun Demo() {
    var flag by remember { mutableStateOf(false) }
    Box(
    Modifier.fillMaxSize().background(Color.DarkGray),
    contentAlignment = Alignment.Center
    ) {

    val size by animateDpAsState(if (flag) 32.dp else 96.dp) { valueOnAnimateEnd ->
    // 可以设置动画结束的监听函数,回调动画结束时对应属性的目标值
    Log.i(TAG, "size animate finished with $valueOnAnimateEnd")
    }
    Row(
    Modifier
    .size(size)
    .background(Color.Magenta)
    .clickable { flag = !flag }
    ) {}
    }
    }

    演示效果:


    demo_animateDpAsState.gif


    作为 compose 动画的最基本操作,与我们平时使用动画的方式不太一样,你会发现你能影响动画的核心只能是选一个属性和一个目标值。甚至连属性的初始值都不能预设,动画的时长没有办法干预。


    深入一点点


    查看 animateDpState() 函数的实现:


    /**
    * ... ...
    *
    * [animateDpAsState] returns a [State] object. The value of the state object will continuously be
    * updated by the animation until the animation finishes.
    *
    * Note, [animateDpAsState] cannot be canceled/stopped without removing this composable function
    * from the tree. See [Animatable] for cancelable animations.
    *
    * @sample androidx.compose.animation.core.samples.DpAnimationSample
    *
    * @param targetValue Target value of the animation
    * @param animationSpec The animation that will be used to change the value through time. Physics animation will be used by default.
    * @param finishedListener An optional end listener to get notified when the animation is finished.
    * @return A [State] object, the value of which is updated by animation.
    */
    @Composable
    fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    finishedListener: ((Dp) -> Unit)? = null
    ): State<Dp> {
    return animateValueAsState(
    targetValue,
    Dp.VectorConverter,
    animationSpec,
    finishedListener = finishedListener
    )
    }

    查看 animateIntAsState 的实现:


    /**
    * ... ...
    *
    * [animateIntAsState] returns a [State] object. The value of the state object will continuously be
    * updated by the animation until the animation finishes.
    *
    * Note, [animateIntAsState] cannot be canceled/stopped without removing this composable function
    * from the tree. See [Animatable] for cancelable animations.
    *
    * @param targetValue Target value of the animation
    * @param animationSpec The animation that will be used to change the value through time. Physics
    * animation will be used by default.
    * @param finishedListener An optional end listener to get notified when the animation is finished.
    * @return A [State] object, the value of which is updated by animation.
    */
    @Composable
    fun animateIntAsState(
    targetValue: Int,
    animationSpec: AnimationSpec<Int> = intDefaultSpring,
    finishedListener: ((Int) -> Unit)? = null
    ): State<Int> {
    return animateValueAsState(
    targetValue, Int.VectorConverter, animationSpec, finishedListener = finishedListener
    )
    }




    • targetValue是某一个以Dp为单位的属性的目标值,顾名思义就是你希望这个属性变化为某一个具体的值;




    • animationSpec 该属性的值如何跟随时间的变化而变化,有默认实现;




    • finishedListener 动画结束函数,可空;




    • anumate*AsState 系列函数的实现都很相似,统一在内部调用了 animateValueAsState(...)




    这是一个基于 State 的实现,联系 Compose 中对于数据的封装和订阅方式,可以理解为当程序的某一个行为触发动画启动后,compose 会自主启动,并根据时间来计算对应的属性应该是什么值,再通过 State 返回,Composable 函数在一次次 recompose 行为中不断通过 State 获取到该属性的最新值,并刷新到界面上,知道这个值变化到目标值状态,更新也就结束了。也就是动画结束。


    继续深入 animateValueAsState 的实现:


    @Composable
    fun <T, V : AnimationVector> animateValueAsState(
    targetValue: T,
    typeConverter: TwoWayConverter<T, V>,
    animationSpec: AnimationSpec<T> = remember {
    spring(visibilityThreshold = visibilityThreshold)
    },
    visibilityThreshold: T? = null,
    finishedListener: ((T) -> Unit)? = null
    ): State<T> {

    val animatable = remember { Animatable(targetValue, typeConverter) }
    val listener by rememberUpdatedState(finishedListener)
    val animSpec by rememberUpdatedState(animationSpec)

    ... ...

    return animatable.asState()
    }

    你会发现其内部其实还是使用 Animatable 来实现。anumate*AsState 虽然基于 Animatable ,不但没有扩充 Animatable 的用法,反而还有了局限,怎会如此?个人认为 animate*AsState 是专门为确定性的简单使用场景进行的封装,这些场景有明确的状态变化,需要做动画的值也不会很复杂,在这些场景中如果能极为方便的快速定义动画,也会是一种非常实用的设计,即使场景变得复杂,再用 Animatable 兜底也能满足需求。


    updateTransition


    在实际的使用场景中,很多情况下的动画设计都不是单一参数可以完成的,比如大小变化的同时对颜色进行过渡、大小与圆角同时变化,形状与颜色同时变化等。这些情况需要组合多个动画同时进行:


    @Composable
    fun Demo() {
    var flag by remember { mutableStateOf(false) }
    Box(
    Modifier
    .fillMaxSize()
    .background(Color.DarkGray),
    contentAlignment = Alignment.Center
    ) {

    val size by animateDpAsState(if (flag) 32.dp else 96.dp) { valueOnAnimateEnd ->
    // 可以设置动画结束的监听函数,回调动画结束时对应属性的目标值
    Log.i(TAG, "size animate finished with $valueOnAnimateEnd")
    }
    val color by animateColorAsState(
    targetValue = if (flag) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
    )
    Row(
    Modifier
    .size(size)
    .background(color)
    .clickable { flag = !flag }
    ) {}
    }
    }

    但是上面的实现存在一个问题,就是每一个属性值的动画过程都是单独计算的,同时每个属性动画也都要考单独的状态进行管理,这显然在性能上是有浪费的,而却也很不方便。这种情况可以引入 Transition 来进行动画的统一管理:


    @Composable
    fun Demo() {
    var flag by remember { mutableStateOf(false) }
    Box(
    Modifier
    .fillMaxSize()
    .background(Color.DarkGray),
    contentAlignment = Alignment.Center
    ) {
    val transition = updateTransition(flag)
    val size = transition.animateDp { if (it) 32.dp else 96.dp }
    val color = transition.animateColor { if (it) MaterialTheme.colors.primary else MaterialTheme.colors.secondary }
    Row(
    Modifier
    .size(size.value)
    .background(color.value)
    .clickable { flag = !flag }
    ) {}
    }
    }

    这样当 flag 触发 transition 状态改变时,sizecolor 的值就可以同时在 transition 内部进行计算,性能又节省了亿点点🤏🏻


    @Composable
    fun <T> updateTransition(
    targetState: T,
    label: String? = null
    ): Transition<T> {
    val transition = remember { Transition(targetState, label = label) }
    transition.animateTo(targetState)
    DisposableEffect(transition) {
    onDispose {
    // Clean up on the way out, to ensure the observers are not stuck in an in-between
    // state.
    transition.onTransitionEnd()
    }
    }
    return transition
    }

    updateTransition 可以创建并保存状态,其内部使用 remember 实现。


    作者:User_20211124110632
    链接:https://juejin.cn/post/7034107134551785503
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    kotlin 进阶教程:核心概念

    1 空安全 // ? 操作符,?: Elvis 操作符 val length = b?.length ?: -1 // 安全类型转换 val code = res.code as? Int // StringsKt val code = res.code?.t...
    继续阅读 »

    1 空安全


    // ? 操作符,?: Elvis 操作符
    val length = b?.length ?: -1
    // 安全类型转换
    val code = res.code as? Int
    // StringsKt
    val code = res.code?.toIntOrNull()
    // CollectionsKt
    val list1: List<Int?> = listOf(1, 2, 3, null)
    val list2 = listOf(1, 2, 3)
    val a = list2.getOrNull(5)
    // 这里注意 null 不等于 false
    if(a?.hasB == false) {}

    2 内联函数



    使用 inline 操作符标记的函数,函数内代码会编译到调用处。


    // kotlin
    val list = listOf("a", "b", "c", null)
    list.getOrElse(4) { "d" }?.let {
    println(it)
    }

    // Decompile,getOrElse 方法会内联到调用处
    List list = CollectionsKt.listOf(new String[]{"a", "b", "c", (String)null});
    byte var3 = 4;
    Object var10000;
    if (var3 <= CollectionsKt.getLastIndex(list)) {
    var10000 = list.get(var3);
    } else {
    var10000 = "d";
    }

    String var9 = (String)var10000;
    if (var9 != null) {
    String var2 = var9;
    System.out.print(var2);
    }

    noline: 禁用内联,用于标记参数,被标记的参数不会参与内联。


    // kotlin
    inline fun sync(lock: Lock, block1: () -> Unit, noinline block2: () -> Unit) {}

    // Decompile,block1 会内联到调用处,但是 block2 会生成函数对象并生成调用
    Function0 block2$iv = (Function0)null.INSTANCE;
    ...
    block2.invoke()


    @kotlin.internal.InlineOnly: kotlin 内部注解,这个注解仅用于内联函数,用于防止 java 类调用(原理是编译时会把这个函数标记为 private,内联对于 java 类来说没有意义)。


    如果扩展函数的方法参数包含高阶函数,需要加上内联。


    非局部返回:
    lambda 表达式内部是禁止使用裸 return 的,因为 lambda 表达式不能使包含它的函数返回。但如果 lambda 表达式传给的函数是内联的,那么该 return 也可以内联,所以它是允许的,这种返回称为非局部返回。
    但是可以通过 crossinline 修饰符标记内联函数的表达式参数禁止非局部返回。


    public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
    ReadWriteProperty<Any?, T> =
    object : ObservableProperty<T>(initialValue) {
    override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
    }

    3 泛型



    (1) 基本用法


    class A<T> {
    }
    fun <T> T.toString(): String {
    }
    // 约束上界
    class Collection<T : Number, R : CharSequence> : Iterable<T> {
    }
    fun <T : Iterable> T.toString() {
    }
    // 多重约束
    fun <T> T.eat() where T : Animal, T : Fly {
    }


    (2) 类型擦除
    为了兼容 java 1.5 以前的版本,带不带泛型编译出来的字节码都是一样的,泛型的特性是通过编译器类型检查和强制类型转换等方式实现的,所以 java 的泛型是伪泛型。
    虽然运行时会擦除泛型,但也是有办法拿到的。


    (javaClass.genericSuperclass as? ParameterizedType)
    ?.actualTypeArguments
    ?.getOrNull(0)
    ?: Any::class.java

    fastjsonTypeReferencegsonTypeToken 都是用这种方式来获取泛型的。


    // fastjson 
    HttpResult<PrivacyInfo> httpResult = JSON.parseObject(
    json,
    new TypeReference<HttpResult<PrivacyInfo>>() {
    }
    );
    // gson
    Type type = new TypeToken<ArrayList<JsonObject>>() {
    }.getType();
    ArrayList<JsonObject> srcJsonArray = new Gson().fromJson(sourceJson, type);


    Reified 关键字
    在 kotlin 里,reified 关键字可以让泛型能够在运行时被获取到。reified 关键字必须结合内联函数一起用。


    // fastjson
    inline fun <reified T : Any> parseObject(json: String) {
    JSON.parseObject(json, T::class.java)
    }
    // gson
    inline fun <reified T : Any> fromJson(json: String) {
    Gson().fromJson(json, T::class.java)
    }
    // 获取 bundle 中的 Serializable
    inline fun <reified T> Bundle?.getSerializableOrNull(key: String): T? {
    return this?.getSerializable(key) as? T
    }
    // start activity
    inline fun <reified T : Context> Context.startActivity() {
    startActivity(Intent(this, T::class.java))
    }


    (3) 协变(out)和逆变(in)


    javaList 是不变的,下面的操作不被允许。


    List<String> strList = new ArrayList<>();
    List<Object> objList = strList;


    但是 kotlinList 是协变的,可以做这个操作。


    public interface List<out E> : Collection<E> { ... }
    val strList = arrayListOf<String>()
    val anyList: List<Any> = strList

    注意这里赋值之后 anyList 的类型还是 List<Any> , 如果往里添加数据,那个获取的时候就没法用 String 接收了,这是类型不安全的,所以协变是不允许写入的,是只读的。在 kotlin 中用 out 表示协变,用 out 声明的参数类型不能作为方法的参数类型,只能作为返回类型,可以理解成“生产者”。相反的,kotlin 中用 in 表示逆变,只能写入,不能读取,用 in 声明的参数类型不能作为返回类型,只能用于方法参数类型,可以理解成 “消费者”。


    注意 kotlin 中的泛型通配符 * 也是协变的。


    4 高阶函数



    高阶函数: 将函数用作参数或返回值的函数。


    写了个 test 方法,涵盖了常见的高阶函数用法。


    val block4 = binding?.title?.test(
    block1 = { numer ->
    setText(R.string.app_name)
    println(numer)
    },
    block2 = { numer, checked ->
    "$numer : $checked"
    },
    block3 = {
    toIntOrNull() ?: 0
    }
    )
    block4?.invoke(2)

    fun <T: View, R> T.test(
    block1: T.(Int) -> Unit,
    block2: ((Int, Boolean) -> String)? = null,
    block3: String.() -> R
    ): (Int) -> Unit {
    block1(1)
    block2?.invoke(2, false)
    "5".block3()
    return { number ->
    println(number)
    }
    }

    5 作用域函数


    // with,用于共用的场景
    with(View.OnClickListener {
    it.setBackgroundColor(Color.WHITE)
    }) {
    tvTitle.setOnClickListener(this)
    tvExpireDate.setOnClickListener(this)
    }

    // apply,得到值后会修改这个值的属性
    return CodeLoginFragment().apply {
    arguments = Bundle().apply {
    putString(AppConstants.INFO_EYES_EVENT_ID_FROM, eventFrom)
    }
    }

    // also,得到值后还会继续用这个值
    tvTitle = view.findViewById<TextView?>(R.id.tvTitle).also {
    displayTag(it)
    }

    // run,用于需要拿内部的属性的场景
    tvTitle?.run {
    text = "test"
    visibility = View.VISIBLE
    }

    // let,用于使用它自己的场景
    tvTitle?.let {
    handleTitle(it)
    }

    fun <T> setListener(listenr: T.() -> Unit) {
    }

    6 集合


    list.reversed().filterNotNull()
    .filter {
    it % 2 != 0
    }
    .map {
    listOf(it, it * 2)
    }
    .flatMap {
    it.asSequence()
    }.onEach {
    println(it)
    }.sortedByDescending {
    it
    }
    .forEach {
    println(it)
    }

    7 操作符重载



    重载(overload)操作符的函数都需要使用 operator 标记,如果重载的操作符被重写(override),可以省略 operator 修饰符。
    这里列几个比较常用的。


    索引访问操作符:


    a[i, j] => a.get(i, j)
    a[i] = b => a.set(i, b)


    注意 i、j 不一定是数字,也可以是 String 等任意类型。


    public interface List<out E> : Collection<E> {
    public operator fun get(index: Int): E
    }
    public interface MutableList<E> : List<E>, MutableCollection<E> {
    public operator fun set(index: Int, element: E): E
    }


    调用操作符:
    invoke 是调用操作符函数名,调用操作符函数可以写成函数调用表达式。


    val a = {}
    a() => a.invoke()
    a(i, j) => a.invoke(i, j)


    变量 block: (Int) -> Unit 调用的时候可以写成 block.invoke(2),也可以写成 block(2),原因是重载了 invoke 函数:


    public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
    }


    getValuesetValueprovideDelegate 操作符:
    用于委托属性,变量的 get() 方法会委托给委托对象的 getValue 操作符函数,相对应的变量的 set() 方法会委托给委托对象的 setValue 操作符函数。


    class A(var name: String? = null) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String? = name
    operator fun setValue(thisRef: Any?, property: KProperty<*>, name: String?) {
    this.name = name
    }
    }
    // 翻译
    var b by A() =>
    val a = A()
    var b:String?
    get() = a.getValue(this, ::b)
    set(value) = a.setValue(this, ::b, value)


    表达式 ::b 求值为 KProperty 类型的属性对象。



    跟前面的操作符函数有所区别的是,这两个操作符函数的参数格式都是严格要求的,一个类中的函数格式符合特定要求才可以被当做委托对象。


    provideDelegate 主要用于对委托对象通用处理,比如多个变量用了同一个委托对象时需要验证变量名的场景。


    var b by ALoader()

    class A(var name: String? = null) : ReadWriteProperty<Any?, String?>{
    override fun getValue(thisRef: Any?, property: KProperty<*>): String? {
    return name
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) {
    this.name = value
    }
    }

    class ALoader : PropertyDelegateProvider<Any?, A> {
    override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>) : A {
    property.run {
    when {
    isConst -> {}
    isLateinit -> {}
    isFinal -> {}
    isSuspend -> {}
    !property.name.startsWith("m") -> {}
    }
    }
    return A()
    }
    }

    // 翻译
    var b by ALoader() =>
    val a = ALoader().provideDelegate(this, this::b)
    var b: String?
    get() = a.getValue(this, ::b)
    set(value) = a.setValue(this, ::b, value)

    8 委托


    8.1 委托模式




    // 单例
    companion object {
    @JvmStatic
    val instance by lazy { FeedManager() }
    }

    // 委托实现多继承
    interface BaseA {
    fun printA()
    }

    interface BaseB {
    fun printB()
    }

    class BaseAImpl(val x: Int) : BaseA {
    override fun printA() {
    print(x)
    }
    }

    class BaseBImpl() : BaseB {
    override fun printB() {
    print("printB")
    }
    }

    class Derived(a: BaseA, b: BaseB) : BaseA by a, BaseB by b {
    override fun printB() {
    print("world")
    }
    }

    fun main() {
    val a = BaseAImpl(10)
    val b = BaseBImpl()
    Derived(a, b).printB()
    }

    // 输出:world



    这里 Derived 类相当于同时继承了 BaseAImplBaseBImpl 类,并且重写了 printB() 方法。
    在实际开发中,一个接口有多个实现,如果想复用某个类的实现,可以使用委托的形式。
    还有一种场景是,一个接口有多个实现,需要动态选择某个类的实现:


    interface IWebView {
    fun load()
    }

    // SDK 内部 SystemWebView
    class SystemWebView : IWebView {
    override fun load() {
    ...
    }

    fun stopLoading() {
    ...
    }
    }

    // SDK 内部 X5WebView
    class X5WebView : IWebView {
    override fun load() {
    ...
    }

    fun stopLoading() {
    ...
    }
    }

    abstract class IWebViewAdapter(webview: IWebView) : IWebView by webview{
    abstract fun stopLoading()
    }

    class SystemWebViewAdapter(private val webview: SystemWebView) : IWebViewAdapter(webview){
    override fun stopLoading() {
    webview.stopLoading()
    }
    }

    class X5WebViewAdapter(private val webview: X5WebView) : IWebViewAdapter(webview){
    override fun stopLoading() {
    webview.stopLoading()
    }
    }


    8.2 委托属性



    格式:


    import kotlin.reflect.KProperty

    public interface ReadOnlyProperty<in R, out T> {
    public operator fun getValue(thisRef: R, property: KProperty<*>): T
    }

    public interface ReadWriteProperty<in R, T> {
    public operator fun getValue(thisRef: R, property: KProperty<*>): T

    public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
    }

    public fun interface PropertyDelegateProvider<in T, out D> {
    public operator fun provideDelegate(thisRef: T, property: KProperty<*>): D
    }

    自定义委托对象, getValue 方法的参数跟上面完全一致即可,返回值类型必须是属性类型;setValue 方法的前两个参数跟上面完全一致即可,第三个参数类型必须是属性类型;provideDelegate 方法的参数跟上卖弄完全一致即可,返回值类型必须是属性类型。
    ReadOnlyPropertyReadWritePropertyPropertyDelegateProvider 都是 kotlin 标准库里的类,需要自定义委托对象时直接继承他们会更方便。


    9 怎么写单例?



    不用 object 的写法可能是:


    // 不带参单例
    class A {
    companion object {
    @JvmStatic
    val instance by lazy { A() }
    }
    }

    // 带参的单例,不推荐
    class Helper private constructor(val context: Context) {

    companion object {
    @Volatile
    private var instance: Helper? = null

    @JvmStatic
    fun getInstance(context: Context?): Helper? {
    if (instance == null && context != null) {
    synchronized(Helper::class.java) {
    if (instance == null) {
    instance = Helper(context.applicationContext)
    }
    }
    }
    return instance
    }
    }
    }

    先说带参的单例,首先不推荐写带参数的单例,因为单例就是全局共用,初始化一次之后保持不变,需要的参数应该在第一次使用前设置好(比如通过 by lazy{ A().apply { ... } }),或者单例内部拿应用内全局的参数,然后上例中 context 作为静态变量,Android Studio 会直接报黄色警告,这是个内存泄漏。context 可以设置一个全局的 applicationContext 变量获取。


    然后上面不带参的单例可以
    直接用 object 代替或者直接不用 object 封装,写在文件顶层,可以对比下编译后的代码:


    // kotlin
    object A{
    fun testA(){}
    }

    // 编译后:
    public final class A {
    @NotNull
    public static final A INSTANCE;

    public final void testA() {
    }

    private A() {
    }

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


    // kotlin
    var a = "s"
    fun testB(a: String){
    print(a)
    }

    // 编译后:
    public final class TKt {
    @NotNull
    private static String a = "s";

    @NotNull
    public static final String getA() {
    return a;
    }

    public static final void setA(@NotNull String var0) {
    Intrinsics.checkNotNullParameter(var0, "<set-?>");
    a = var0;
    }

    public static final void testB(@NotNull String a) {
    Intrinsics.checkNotNullParameter(a, "a");
    boolean var1 = false;
    System.out.print(a);
    }
    }

    可以发现,直接文件顶层写,不会创建对象,都是静态方法,如果方法少且评估不需要封装(主要看调用的时候是否需要方便识别哪个对象的方法)可以直接写在文件顶层。


    同理,伴生对象也尽量非必要不创建。


    // kotlin
    class A {
    companion object {
    const val TAG = "A"

    @JvmStatic
    fun newInstance() = A()
    }
    }

    // 编译后
    public final class A {
    @NotNull
    public static final String TAG = "A";
    @NotNull
    public static final A.Companion Companion = new A.Companion((DefaultConstructorMarker)null);

    @JvmStatic
    @NotNull
    public static final A newInstance() {
    return Companion.newInstance();
    }

    public static final class Companion {
    @JvmStatic
    @NotNull
    public final A newInstance() {
    return new A();
    }

    private Companion() {
    }

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

    可以发现,伴生对象会创建一个对象(废话...),知道这个很重要,因为如果伴生对象里没有函数,只有常量,那还有必要创建这个对象吗?函数也只是为了 newInstance 这种方法调用的时候看起来统一一点,如果是别的方法,完全可以写在类所在文件的顶层。


    作者:流沙三七
    链接:https://juejin.cn/post/7034110955571609607
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    别再问我 new 字符串创建了几个对象了!我来证明给你看!

    我想所有 Java 程序员都曾被这个 new String 的问题困扰过,这是一道高频的 Java 面试题,但可惜的是网上众说纷纭,竟然找不到标准的答案。有人说创建了 1 个对象,也有人说创建了 2 个对象,还有人说可能创建了 1 个或 2 个对象,但谁都没有...
    继续阅读 »

    我想所有 Java 程序员都曾被这个 new String 的问题困扰过,这是一道高频的 Java 面试题,但可惜的是网上众说纷纭,竟然找不到标准的答案。有人说创建了 1 个对象,也有人说创建了 2 个对象,还有人说可能创建了 1 个或 2 个对象,但谁都没有拿出干掉对方的证据,这就让我们这帮吃瓜群众们陷入了两难之中,不知道到底该信谁得。


    但是今天,老王就斗胆和大家聊聊这个话题,顺便再拿出点证据


    以目前的情况来看,关于 new String("xxx") 创建对象个数的答案有 3 种:



    1. 有人说创建了 1 个对象;

    2. 有人说创建了 2 个对象;

    3. 有人说创建了 1 个或 2 个对象。


    而出现多个答案的关键争议点在「字符串常量池」上,有的说 new 字符串的方式会在常量池创建一个字符串对象,有人说 new 字符串的时候并不会去字符串常量池创建对象,而是在调用 intern() 方法时,才会去字符串常量池检测并创建字符串。


    那我们就先来说说这个「字符串常量池」。


    字符串常量池


    字符串的分配和其他的对象分配一样,需要耗费高昂的时间和空间为代价,如果需要大量频繁的创建字符串,会极大程度地影响程序的性能,因此 JVM 为了提高性能和减少内存开销引入了字符串常量池(Constant Pool Table)的概念。


    字符串常量池相当于给字符串开辟一个常量池空间类似于缓存区,对于直接赋值的字符串(String s="xxx")来说,在每次创建字符串时优先使用已经存在字符串常量池的字符串,如果字符串常量池没有相关的字符串,会先在字符串常量池中创建该字符串,然后将引用地址返回变量,如下图所示:


    字符串常量池示意图.png


    以上说法可以通过如下代码进行证明:


    public class StringExample {
    public static void main(String[] args) {
    String s1 = "Java";
    String s2 = "Java";
    System.out.println(s1 == s2);
    }
    }

    以上程序的执行结果为:true,说明变量 s1 和变量 s2 指向的是同一个地址。


    在这里我们顺便说一下字符串常量池的再不同 JDK 版本的变化。


    这里,顺便送大家一份经典学习资料,我把大学和工作中用的经典电子书库(包含数据结构、操作系统、C++/C、网络经典、前端编程经典、Java相关、程序员认知、职场发展)、面试找工作的资料汇总都打包放在这。



    戳这里直接获取:


    计算机经典必读书单(含下载方式)


    Java 入门到精通含面试最全资料包(含下载方式)


    常量池的内存布局


    JDK 1.7 之后把永生代换成的元空间,把字符串常量池从方法区移到了 Java 堆上


    JDK 1.7 内存布局如下图所示:


    JDK 1.7 内存布局.png


    JDK 1.8 内存布局如下图所示:


    JDK 1.8 内存布局.png


    JDK 1.8 与 JDK 1.7 最大的区别是 JDK 1.8 将永久代取消,并设立了元空间。官方给的说明是由于永久代内存经常不够用或发生内存泄露,会爆出 java.lang.OutOfMemoryError: PermGen 的异常,所以把将永久区废弃而改用元空间了,改为了使用本地内存空间,官网解释详情:openjdk.java.net/jeps/122


    答案解密


    认为 new 方式创建了 1 个对象的人认为,new String 只是在堆上创建了一个对象,只有在使用 intern() 时才去常量池中查找并创建字符串。


    认为 new 方式创建了 2 个对象的人认为,new String 会在堆上创建一个对象,并且在字符串常量池中也创建一个字符串。


    认为 new 方式有可能创建 1 个或 2 个对象的人认为,new String 会先去常量池中判断有没有此字符串,如果有则只在堆上创建一个字符串并且指向常量池中的字符串,如果常量池中没有此字符串,则会创建 2 个对象,先在常量池中新建此字符串,然后把此引用返回给堆上的对象,如下图所示:


    new 字符串常量池.png


    老王认为正确的答案:创建 1 个或者 2 个对象


    技术论证


    解铃还须系铃人,回到问题的那个争议点上,new String 到底会不会在常量池中创建字符呢?我们通过反编译下面这段代码就可以得出正确的结论,代码如下:


    public class StringExample {
    public static void main(String[] args) {
    String s1 = new String("javaer-wang");
    String s2 = "wang-javaer";
    String s3 = "wang-javaer";
    }
    }

    首先我们使用 javac StringExample.java 编译代码,然后我们再使用 javap -v StringExample 查看编译的结果,相关信息如下:


    Classfile /Users/admin/github/blog-example/blog-example/src/main/java/com/example/StringExample.class
    Last modified 2020年4月16日; size 401 bytes
    SHA-256 checksum 89833a7365ef2930ac1bc3d7b88dcc5162da4b98996eaac397940d8997c94d8e
    Compiled from "StringExample.java"
    public class com.example.StringExample
    minor version: 0
    major version: 58
    flags: (0x0021) ACC_PUBLIC, ACC_SUPER
    this_class: #16 // com/example/StringExample
    super_class: #2 // java/lang/Object
    interfaces: 0, fields: 0, methods: 2, attributes: 1
    Constant pool:
    #1 = Methodref #2.#3 // java/lang/Object."<init>":()V
    #2 = Class #4 // java/lang/Object
    #3 = NameAndType #5:#6 // "<init>":()V
    #4 = Utf8 java/lang/Object
    #5 = Utf8 <init>
    #6 = Utf8 ()V
    #7 = Class #8 // java/lang/String
    #8 = Utf8 java/lang/String
    #9 = String #10 // javaer-wang
    #10 = Utf8 javaer-wang
    #11 = Methodref #7.#12 // java/lang/String."<init>":(Ljava/lang/String;)V
    #12 = NameAndType #5:#13 // "<init>":(Ljava/lang/String;)V
    #13 = Utf8 (Ljava/lang/String;)V
    #14 = String #15 // wang-javaer
    #15 = Utf8 wang-javaer
    #16 = Class #17 // com/example/StringExample
    #17 = Utf8 com/example/StringExample
    #18 = Utf8 Code
    #19 = Utf8 LineNumberTable
    #20 = Utf8 main
    #21 = Utf8 ([Ljava/lang/String;)V
    #22 = Utf8 SourceFile
    #23 = Utf8 StringExample.java
    {
    public com.example.StringExample();
    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 3: 0

    public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
    stack=3, locals=4, args_size=1
    0: new #7 // class java/lang/String
    3: dup
    4: ldc #9 // String javaer-wang
    6: invokespecial #11 // Method java/lang/String."<init>":(Ljava/lang/String;)V
    9: astore_1
    10: ldc #14 // String wang-javaer
    12: astore_2
    13: ldc #14 // String wang-javaer
    15: astore_3
    16: return
    LineNumberTable:
    line 5: 0
    line 6: 10
    line 7: 13
    line 8: 16
    }
    SourceFile: "StringExample.java"


    备注:以上代码的运行也编译环境为 jdk1.8.0_101。



    其中 Constant pool 表示字符串常量池,我们在字符串编译期的字符串常量池中找到了我们 String s1 = new String("javaer-wang"); 定义的“javaer-wang”字符,在信息 #10 = Utf8 javaer-wang 可以看出,也就是在编译期 new 方式创建的字符串就会被放入到编译期的字符串常量池中,也就是说 new String 的方式会首先去判断字符串常量池,如果没有就会新建字符串那么就会创建 2 个对象,如果已经存在就只会在堆中创建一个对象指向字符串常量池中的字符串。


    那么问题来了,以下这段代码的执行结果为 true 还是 false?


    String s1 = new String("javaer-wang");
    String s2 = new String("javaer-wang");
    System.out.println(s1 == s2);

    既然 new String 会在常量池中创建字符串,那么执行的结果就应该是 true 了。其实并不是,这里对比的变量 s1 和 s2 堆上地址,因为堆上的地址是不同的,所以结果一定是 false,如下图所示:


    字符串引用.png


    从图中可以看出 s1 和 s2 的引用一定是相同的,而 s3 和 s4 的引用是不同的,对应的程序代码如下:


    public static void main(String[] args) {
    String s1 = "Java";
    String s2 = "Java";
    String s3 = new String("Java");
    String s4 = new String("Java");
    System.out.println(s1 == s2);
    System.out.println(s3 == s4);
    }

    程序执行的结果也符合预期:



    true false



    扩展知识


    我们知道 String 是 final 修饰的,也就是说一定被赋值就不能被修改了。但编译器除了有字符串常量池的优化之外,还会对编译期可以确认的字符串进行优化,例如以下代码:


    public static void main(String[] args) {
    String s1 = "abc";
    String s2 = "ab" + "c";
    String s3 = "a" + "b" + "c";
    System.out.println(s1 == s2);
    System.out.println(s1 == s3);
    }

    按照 String 不能被修改的思想来看,s2 应该会在字符串常量池创建两个字符串“ab”和“c”,s3 会创建三个字符串,他们的引用对比结果也一定是 false,但其实不是,他们的结果都是 true,这是编译器优化的功劳。


    同样我们使用 javac StringExample.java 先编译代码,再使用 javap -c StringExample 命令查看编译的代码如下:


    警告: 文件 ./StringExample.class 不包含类 StringExample
    Compiled from "StringExample.java"
    public class com.example.StringExample {
    public com.example.StringExample();
    Code:
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4: return

    public static void main(java.lang.String[]);
    Code:
    0: ldc #7 // String abc
    2: astore_1
    3: ldc #7 // String abc
    5: astore_2
    6: ldc #7 // String abc
    8: astore_3
    9: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
    12: aload_1
    13: aload_2
    14: if_acmpne 21
    17: iconst_1
    18: goto 22
    21: iconst_0
    22: invokevirtual #15 // Method java/io/PrintStream.println:(Z)V
    25: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
    28: aload_1
    29: aload_3
    30: if_acmpne 37
    33: iconst_1
    34: goto 38
    37: iconst_0
    38: invokevirtual #15 // Method java/io/PrintStream.println:(Z)V
    41: return
    }

    从 Code 3、6 可以看出字符串都被编译器优化成了字符串“abc”了。




    总结


    本文我们通过 javap -v XXX 的方式查看编译的代码发现 new String 首次会在字符串常量池中创建此字符串,那也就是说,通过 new 创建字符串的方式可能会创建 1 个或 2 个对象,如果常量池中已经存在此字符串只会在堆上创建一个变量,并指向字符串常量池中的值,如果字符串常量池中没有相关的字符,会先创建字符串在返回此字符串的引用给堆空间的变量。我们还将了字符串常量池在 JDK 1.7 和 JDK 1.8 的变化以及编译器对确定字符串的优化,希望能帮你正在的理解字符串的比较。


    最后的话 原创不易,本篇近 3000 的文字描述,以及大量精美的图片,耗费了作者大概 5 个多小时的时间,写作是一件很酷,并且能帮助他人的事,作者希望一直能坚持下去。如果觉得有用,请随手点击一个赞吧,谢谢


    作者:码农出击666
    链接:https://juejin.cn/post/7033653572898127879
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    一步一步搭建Flutter开发架子-Tabbar

    一点点搭建一个架构,架构对于开发比较重要,有固定的模式,第一不容易产生bug,并且有利于对于项目以及开发架构的理解。 对于一个app,常见的架构一般是底部有Tabbar形式,或者采用抽屉的形式,底部Tabbar大部分app都是平铺的,中间有一块凸出来的形式。普...
    继续阅读 »

    一点点搭建一个架构,架构对于开发比较重要,有固定的模式,第一不容易产生bug,并且有利于对于项目以及开发架构的理解。 对于一个app,常见的架构一般是底部有Tabbar形式,或者采用抽屉的形式,底部Tabbar大部分app都是平铺的,中间有一块凸出来的形式。

    普通Tabbar

     比较简单代码直接贴出来了

     @override
    Widget build(BuildContext context) {
    return Scaffold(
    bottomNavigationBar: normalBottomBar(),
    );
    }
    normalBottomBar() {
    return BottomNavigationBar(
    // 底部导航
    items: <BottomNavigationBarItem>[
    BottomNavigationBarItem(
    icon: Icon(Icons.home),
    label: 'Home',
    activeIcon: Icon(Icons.access_alarm)),
    BottomNavigationBarItem(icon: Icon(Icons.search), label: 'search'),
    BottomNavigationBarItem(icon: Icon(Icons.people), label: 'mine'),
    ],
    currentIndex: _selectedIndex,
    fixedColor: Colors.blue,
    elevation: 10, // default: 8.0
    type: BottomNavigationBarType.fixed,
    iconSize: 30,
    selectedFontSize: 12, // 默认是14,未选择是14
    onTap: _onItemTapped,
    );
    }
    _onItemTapped(int index) {
    setState(() {
    _selectedIndex = index;
    });
    }

    中间凸出的Tabbar

    官方的Material风格库中存在这种效果,不过个人感觉跟现在流行的风格有点不太匹配,所以封装了一个接近于现在流行风格的Tabbar。 刚开始的效果是这样的:  凸出的按钮封装了一下的代码片段:

    import 'dart:math';

    import 'package:flutter/material.dart';

    class CenterNavigationItem extends StatefulWidget {
    CenterNavigationItem({Key key, this.onTap}) : super(key: key);

    final Function onTap;

    @override
    _CenterNavigationItemState createState() => _CenterNavigationItemState();
    }

    class _CenterNavigationItemState extends State<CenterNavigationItem> {
    @override
    Widget build(BuildContext context) {
    return Stack(
    children: [
    // CustomPaint(size: Size(76, 76), painter: MyPainter()),
    Container(
    width: 76,
    height: 76,
    padding: EdgeInsets.all(8),
    decoration: BoxDecoration(
    color: Color.fromRGBO(250, 250, 250, 1),
    borderRadius: BorderRadius.circular(38)),
    child: FloatingActionButton(
    child: Icon(Icons.add),
    // child: TextField(),
    tooltip: '测试', // 长按弹出提示
    onPressed: () {
    widget.onTap();
    }),
    ),
    ],
    );
    }
    }
    // 主页面引用:
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
    floatingActionButton: CenterNavigationItem(
    onTap: () {
    setState(() {
    _selectedIndex = 1;
    });
    },
    ),
    bottomNavigationBar: normalBottomBar(),
    );
    }

    感觉还是差点意思, 在于底部线与线直接连接的不够平滑 

    带有动画效果的Tabbar

    继续改造一下的想法就是通过CustomPainter去画一个半圆。效果如下:  代码片段:

    class MyPainter extends CustomPainter {
    @override
    paint(Canvas canvas, Size size) {
    Paint paint = Paint()
    ..isAntiAlias = false
    ..color = Colors.green
    ..strokeCap = StrokeCap.round
    ..strokeWidth = 0
    ..style = PaintingStyle.stroke;
    print(pi);
    canvas.drawArc(
    new Rect.fromCircle(center: Offset(38, 38), radius: size.width / 2),
    pi,
    2 * pi * 0.5,
    false,
    paint);
    }

    @override
    bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
    }
    }

    在凸出的按钮封装的代码中添加:

    class _CenterNavigationItemState extends State<CenterNavigationItem> {
    @override
    Widget build(BuildContext context) {
    return Stack(
    children: [
    CustomPaint(size: Size(76, 76), painter: MyPainter()),
    class _CenterNavigationItemState extends State<CenterNavigationItem> {
    @override
    Widget build(BuildContext context) {
    return Stack(
    children: [
    // 这个位置加入!!!!!!!!!!
    CustomPaint(size: Size(76, 76), painter: MyPainter()),
    Container(
    width: 76,
    height: 76,
    padding: EdgeInsets.all(8),
    decoration: BoxDecoration(
    color: Color.fromRGBO(250, 250, 250, 1),
    borderRadius: BorderRadius.circular(38)),
    child: FloatingActionButton(
    child: Icon(Icons.add),
    // child: TextField(),
    tooltip: '测试', // 长按弹出提示
    onPressed: () {
    widget.onTap();
    }),
    ),
    ],
    );
    }
    }
    ],
    );
    }
    }

    最终就是这个效果。还有就是也不能闭门造车,上网搜搜大家都是怎么去构建Tabbar的。其中在github上发现一个加入了动画的开源代码。Motion-Tab-Bar。分析了一波代码,看了一眼效果如下: 

    稍加修改了一下,留着以后项目可能用的上。over~欢迎讨论

    one more thing...

    • 1, 路由管理,
    • 2, 国际化管理,
    • 3, 数据持久化管理,
    • 4, 响应式管理方法


    作者:一天清晨
    链接:https://juejin.cn/post/6933496106827546632
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    一步一步搭建Flutter开发架子-国际化,路由,本地化,响应式

    接上一篇文章,这篇文章主要介绍,路由管理,国际化管理,响应式管理方法,数据持久化管理。还是先看看大神么们都是怎么写的,从中学习一下。看到又一个比较好用的库getx,方便简介,基本上都包含今天要封装的内容,那就用起来吧。ps:有人可能会有想法说是应该自己写,总用...
    继续阅读 »

    接上一篇文章,这篇文章主要介绍,路由管理,国际化管理,响应式管理方法,数据持久化管理。还是先看看大神么们都是怎么写的,从中学习一下。看到又一个比较好用的库getx,方便简介,基本上都包含今天要封装的内容,那就用起来吧。ps:有人可能会有想法说是应该自己写,总用第三方的库,遇到问题不好处理,有点道理,换个想法如果自己没达到那个水平也可以先使用第三方库,好好看看大神的源码,来提升自己。总之所当面看待吧。


    引入GetX


    在pubspec.yaml文件中加入


    dependencies:
    get: ^3.24.0

    在需要使用的文件中引入


    import 'package:get/get.dart'

    在main.dart中使用GetMaterialApp替换MaterialApp


     return GetMaterialApp(
    enableLog: false,
    debugShowCheckedModeBanner: false,
    defaultTransition: Transition.rightToLeftWithFade,
    theme: ThemeData(
    primarySwatch: Colors.orange,
    visualDensity: VisualDensity.adaptivePlatformDensity,
    ),
    home: TabBarPage());

    路由管理


    比较喜欢这个的原因: 不需要获取上下文context直接跳转页面,代码很简洁,并且支持别名路由跳转
    效果


    不带参数颇通跳转


    Get.to(OtherPage())

    带参数跳转


    Get.to(OtherPage(id:''))

    无返回跳转


    比如在登录成功之后的跳转,不能够再返回到登录页面


    Get.off(OtherPage(id:''))

    跳转到Tabbar页面


    比如在商品的详情页面直接跳转到购物车页面,一般购物车页面在Tabbar上。


    Get.off(TabbarPage(currentIndex: 1));

    别名跳转


    这种情况大家可以去看下GetX的文档,这里就不介绍了。因为我不打算在项目里面坐这种跳转。ps:纯个人原因


    SnackBars,Dialogs,BottomSheets使用


    GetX中,我们也可以不获取上下文context进行跳用SnackBars,Dialogs, BottomSheets使用


    SnackBars


    效果:


     Get.snackbar(
    "Hey i'm a Get SnackBar!",
    "It's unbelievable! I'm using SnackBar without context, without boilerplate, without Scaffold, it is something truly amazing!",
    icon: Icon(Icons.alarm),
    shouldIconPulse: true,
    barBlur: 20,
    isDismissible: true,
    duration: Duration(seconds: 3),
    backgroundColor: Colors.red);

    具体的属性,大家可以点击进去看下源码配置


    Dialogs


    效果:


     Get.defaultDialog(
    onConfirm: () => print("Ok"),
    buttonColor: Colors.white,
    middleText: "Dialog made in 3 lines of code");


    也可以弹出自定义的组件


    Get.dialog(YourDialogWidget());

    BottomSheets


    效果:


    Get.bottomSheet(Container(
    decoration: BoxDecoration(color: Colors.red),
    child: Wrap(
    children: <Widget>[
    ListTile(
    leading: Icon(Icons.music_note),
    title: Text('Music'),
    onTap: () {}),
    ListTile(
    leading: Icon(Icons.videocam),
    title: Text('Video'),
    onTap: () {},
    ),
    ],
    ),
    ));

    以上是简单的用法,我们可以新建哥utils文件夹,封装成一个工具类去调用这个方法。


    国际化管理


    目前涉及到2中模式,



    • 根据系统语言设置国际化

    • 在应用内设置国际化显示


    首先创建一个Languages.dart文件 简单的写了一个hello的中英文含义


    import 'package:get/get.dart';
    class Languages extends Translations {
    @override
    Map<String, Map<String, String>> get keys => {
    'zh_CN': {
    'hello': '你好 世界',
    },
    'en_US': {
    'hello': 'Hallo Welt',
    }
    };
    }

    在main.dart中加入代码:


    return GetMaterialApp(
    translations: Languages(), // 你的翻译
    locale: Locale('zh', 'CN'), // 将会按照此处指定的语言翻译
    fallbackLocale: Locale('en', 'US'), // 添加一个回调语言选项,以备上面指定的语言翻译不存在
    );

    显示只需要加入如下就ok。很简单


    Text('hello'.tr)


    跟随系统语言


    ui.window.locale 获取当前系统语言,设置本地语言


    GetMaterialApp(
    translations: Languages(), // 你的翻译
    locale: ui.window.locale, // 将会按照此处指定的语言翻译
    fallbackLocale: Locale('en', 'US'), // 添加一个回调语言选项,以备上面指定的语言翻译 不存在
    ......
    )

    在应用内设置国际化显示


    更新文字显示为中文如下:


    var locale = Locale('zh', 'CN');
    Get.updateLocale(locale);

    多写两句用RadioListTile实现一下效果


    RadioListTile(
    value: 'chinese',
    groupValue: _selected,
    title: Text('中文'),
    subtitle: Text('中文'),
    selected: _selected == 'chinese',
    onChanged: (type) {
    var locale = Locale('zh', 'CN');
    Get.updateLocale(locale);
    setState(() {
    _selected = type;
    });
    }),
    RadioListTile(
    value: 'english',
    groupValue: _selected,
    title: Text('英文'),
    subtitle: Text('英文'),
    selected: _selected == 'english',
    onChanged: (type) {
    var locale = Locale('en', 'US');
    Get.updateLocale(locale);
    setState(() {
    _selected = type;
    });
    },
    ),

    这是本人测试使用用的代码。看一下效果


    响应式管理方法


    GetX举例是一个计数器的例子,已经很容易理解了,作用就是不用在引入过多的状态管理的库,比如provide之类的。用法差不多。更简洁。还是记录一下,方便以后查看用


    class Controller extends GetxController{
    var count = 0.obs;
    increment() => count++;
    }

    lass Home extends StatelessWidget {

    @override
    Widget build(context) {

    // 使用Get.put()实例化你的类,使其对当下的所有子路由可用。
    final Controller c = Get.put(Controller());

    return Scaffold(
    // 使用Obx(()=>每当改变计数时,就更新Text()。
    appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),

    // 用一个简单的Get.to()即可代替Navigator.push那8行,无需上下文!
    body: Center(child: RaisedButton(
    child: Text("Go to Other"), onPressed: () => Get.to(Other()))),
    floatingActionButton:
    FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment));
    }
    }

    class Other extends StatelessWidget {
    // 你可以让Get找到一个正在被其他页面使用的Controller,并将它返回给你。
    final Controller c = Get.find();

    @override
    Widget build(context){
    // 访问更新后的计数变量
    return Scaffold(body: Center(child: Text("${c.count}")));
    }
    }

    这块还没有在程序中使用,但是状态管理在程序中使用还是很方便的,比如更改用户信息,登录。购物车逻辑中都可以使用


    数据持久化管理


    这个地方引入了第二个第三方库


      flustars: ^0.3.3
    # https://github.com/Sky24n/sp_util
    # sp_util分拆成单独的库,可以直接引用
    sp_util: ^1.0.1

    用起来也很方便感觉不错。
    为了之后方便使用,现定义一个Global.dart文件,做初始化操作


    import 'package:flustars/flustars.dart';
    class Global {
    static Future initSqutil() async => await SpUtil.getInstance();
    }
    在main方法中调用:
    Global.initSqutil();

    接下来进行存储数据以及获取数据的方法,类型包括:字符串,布尔值,对象,数组
    举个例子:


    存数据
    SpUtil.putString( 'login', '登录了',);
    取数据
    SpUtil.getString('login',defValue: '');

    额外提的一点就是存储对象类型数组,分两种形式,getObjList, getObjectList方法


     类似泛型的结构, 可以进行转换
    List<Map> dataList = SpUtil.getObjList('cityMap', (v) => v);
    返回一个Map数组
    List<Map> dataList = SpUtil.getObjectList('cityMap');

    这个地方可以配合国际化语言切换时使用。比如每次改变语言进行存储。然后每次打开app进行,获取初始化。


    one more things...



    • 网络请求

    • 页面不同状态展示封装

    作者:一天清晨
    链接:https://juejin.cn/post/6934572753601167374
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    通过协程简化Activity之间的通信

    假设我们有这样一个常用的场景:有两个Activity,第一个Activity展示一段文本点击“编辑”按钮启动第二个Activity,并把这段文本当做参数传递到第二个Activity在第二个Activity编辑这个字符串编辑完成后点击保存将结果返回到第一个Act...
    继续阅读 »

    假设我们有这样一个常用的场景:

    • 有两个Activity,第一个Activity展示一段文本
    • 点击“编辑”按钮启动第二个Activity,并把这段文本当做参数传递到第二个Activity
    • 在第二个Activity编辑这个字符串
    • 编辑完成后点击保存将结果返回到第一个Activity
    • 第一个Activity展示修改后的字符串

    如下图:

    这是一个非常简单和常见的场景,我们一般通过 startActivityForResult 的方式传递参数,并在 onActivityResult 接收编辑后的结果,代码也很简单,如下:


    //第一个Activity启动编辑Activity
    btnEditByTradition.setOnClickListener {
    val content = tvContent.text.toString().trim()
    val intent = Intent(this, EditActivity::class.java).apply {
    putExtra(EditActivity.ARG_TAG_CONTENT, content)
    }
    startActivityForResult(intent, REQUEST_CODE_EDIT)
    }
    //EditActivity回传编辑后的结果
    btnSave.setOnClickListener {
    val newContent = etContent.text.toString().trim()
    setResult(RESULT_OK, Intent().apply {
    putExtra(RESULT_TAG_NEW_CONTENT, newContent)
    })
    finish()
    }
    //第一个Activity中接受编辑后的结果,并展示
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    when (requestCode) {
    REQUEST_CODE_EDIT -> {
    if (resultCode == RESULT_OK && data != null) {
    val newContent = data.getStringExtra(EditActivity.RESULT_TAG_NEW_CONTENT)
    tvContent.text = newContent
    }
    }
    else -> super.onActivityResult(requestCode, resultCode, data)
    }
    }

    那这种方式有什么缺点呢?

    1. 代码分散,可读性差
    2. 封装不彻底,调用方需要到EditActivity才能知道需要传递什么参数,类型是什么,key是什么
    3. 调用方需要知道EditActivity是如何返回的参数类型和key是什么才能正确解析
    4. 约束性差,各种常量的定义(REQUEST_CODE,PARAM_KEY等),若项目管理不严谨,重复定义,导致后期重构和维护比较麻烦

    那有没有一种方式能解决上面的缺点呢?我们期望的是:

    1. 一个对外提供某些功能的Activity应该有足够的封装性,调用者像调用普通方法一样,一行代码即可完成调用
    2. 方法的参数列表就是调用本服务需要传递的参数(参数数量,参数类型,是否必须)
    3. 方法的返回参数就是本服务的返回结果
    4. 提供服务的Activity像一个组件一样,能对外提供功能都是以一个个方法的形式体现

    通过Kotlin 协程和一个不可见的Fragment来实现。


    btnEditByCoroutine.setOnClickListener {
    GlobalScope.launch {
    val content = tvContent.text.toString().trim()

    // 调用EditActivity的 editContent 方法
    // content为要编辑的内容
    // editContent 即为编辑后的结果
    val newContent = EditActivity.editContent(this@MainActivity, content)

    if (!newContent.isNullOrBlank()) {
    tvContent.text = newContent
    }
    }
    }


    通过上面的代码,我们看到,通过一个方法即可完成调用,基本实现了上文提到的期望。 那 editContent 方法内部是如何实现的呢?看如下代码:


    /**
    * 对指定的文本进行编辑
    * @param content 要编辑的文本
    *
    * @return 可空 不为null 表示编辑后的内容 为null表示用户取消了编辑
    */

    @JvmStatic
    suspend fun editContent(activity: FragmentActivity, content: String): String? =
    suspendCoroutine { continuation ->
    val editFragment = BaseSingleFragment().apply {
    intentGenerator = {
    Intent(it, EditActivity::class.java).apply {
    putExtra(ARG_TAG_CONTENT, content)
    }
    }
    resultParser = { resultCode, data ->
    if (resultCode == RESULT_OK && data != null) {
    val result = data.getStringExtra(RESULT_TAG_NEW_CONTENT)
    continuation.resume(result)
    } else {
    continuation.resume(null)
    }
    removeFromActivity(activity.supportFragmentManager)
    }
    }
    editFragment.addToActivity(activity.supportFragmentManager)
    }

    这里需要借助一个“BaseSingleFragment”来实现,这是因为我不能违背 ActivityManagerService 的规则,依然需要通过 startActivityForResult 和 onActivityResult 来实现,所以我们这里通过一个不可见(没有界面)的 Fragment ,将这个过程封装起来,代码如下:


    class BaseSingleFragment : Fragment() {


    /**
    * 生成启动对应Activity的Intent,因为指定要启动的Activity,如何启动,传递参数,所以由具体的使用位置来实现这个Intent
    *
    * 使用者必须实现这个lambda,否则直接抛出一个异常
    */

    var intentGenerator: ((context: Context) -> Intent) = {
    throw RuntimeException("you should provide a intent here to start activity")
    }

    /**
    * 解析目标Activity返回的结果,有具体实现者解析,并回传
    *
    * 使用者必须实现这个lambda,否则直接抛出一个异常
    */

    var resultParser: (resultCode: Int, data: Intent?) -> Unit = { resultCode, data ->
    throw RuntimeException("you should parse result data yourself")
    }

    companion object {
    const val REQUEST_CODE_GET_RESULT = 100
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val context = requireContext()
    startActivityForResult(intentGenerator.invoke(context), REQUEST_CODE_GET_RESULT)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_CODE_GET_RESULT) {
    resultParser.invoke(resultCode, data)
    } else {
    super.onActivityResult(requestCode, resultCode, data)
    }
    }


    /**
    * add current fragment to FragmentManager
    */

    fun addToActivity(fragmentManager: FragmentManager) {
    fragmentManager.beginTransaction().add(this, this::class.simpleName)
    .commitAllowingStateLoss()
    }

    /**
    * remove current fragment from FragmentManager
    */

    fun removeFromActivity(fragmentManager: FragmentManager) {
    fragmentManager.beginTransaction().remove(this).commitAllowingStateLoss()
    }
    }
    当然,这是一个 suspend 方法,java是不支持协程的,而现实情况是,很多项目都有中途集成Kotlin的,有很多遗留的java代码,对于这种情况,我们需要提供相应的java实现吗?The answer is no. Java 代码同样可以调用 suspend 方法,调用方式如下:
    btnEditByCoroutine.setOnClickListener((view) -> {
    String content = tvContent.getText().toString().trim();
    EditActivity.editContent(MainActivityJava.this, content, new Continuation<String>() {
    @NotNull
    @Override
    public CoroutineContext getContext() {
    return EmptyCoroutineContext.INSTANCE;
    }

    @Override
    public void resumeWith(@NotNull Object o) {
    String newContent = (String) o;
    if (!TextUtils.isEmpty(content)) {
    tvContent.setText(newContent);
    }
    }
    });
    });

    虽然是通过回调的方式,在resumeWith方法中来接受结果,但也是比 startActivityForResult 的方式要好的多。

    Perfect!!!

    这种实现方式的灵感是来源于 RxPermission 对权限申请流程的实现,在此对 RxPermission 表达感谢。 另外 Glide 3.X 版本对图片加载任务的启动,暂停,和取消和Activity的和生命周期绑定也是通过向FragmentManager中添加了一个隐藏的Fragment来实现的。 这个demo的代码在

    CourtineTest GitHub ,

    原文链接:https://juejin.cn/post/7033598140766912549?utm_source=gold_browser_extension

    收起阅读 »

    屏幕旋转切换机制详解

    前言 屏幕旋转的机制; 默认情况下,当用户手机的重力感应器打开后,旋转屏幕方向,会导致当前activity发生onDestroy-> onCreate,这样会重新构造当前activity和界面布局,如果在Camera界面,则表现为卡顿或者黑屏一段时间; 今天...
    继续阅读 »

    前言



    屏幕旋转的机制;


    默认情况下,当用户手机的重力感应器打开后,旋转屏幕方向,会导致当前activity发生onDestroy-> onCreate,这样会重新构造当前activity和界面布局,如果在Camera界面,则表现为卡顿或者黑屏一段时间;


    今天就介绍下平面旋转方面的知识点;



    一、screenOrientation属性说明


    android:screenOrientation属性说明:



    • unspecified,默认值,由系统决定,不同手机可能不一致

    • landscape,强制横屏显示,只有一个方向

    • portrait,强制竖屏显,只有一个方向

    • behind,与前一个activity方向相同

    • sensor,根据物理传感器方向转动,用户90度、180度、270度旋转手机方向,activity都更着变化,会重启activity(无论系统是否设置为自动转屏,activity页面都会跟随传感器而转屏)

    • sensorLandscape,横屏旋转,就是可以上下旋转,有两个方向,不会重启activity

    • sensorPortrait,竖屏旋转,就是可以上下旋转,有两个方向,不会重启activity

    • nosensor,旋转设备时候,界面不会跟着旋转。初始化界面方向由系统控制(无论系统是否设置为自动转屏,activity页面都不会转屏)

    • user,用户当前设置的方向

    • reverseLandscape,与正常的横向方向相反显示(反向横屏)

    • reversePortrait,与正常的纵向方向相反显示(反向竖屏)(我设置没用)


    二、屏幕旋转详解


    1、配置文件设置



    • AndroidManifest.xml设置;

    • 横屏或者竖屏是被直接定死,旋转方向不会变化,只有一个方向(意思是旋转180°也不会改变),当然就不会在手机旋转的时候重启activity;


      
    
                android:name=".test1"

                android:screenOrientation="landscape" />

           
                android:name=".test2"

                android:screenOrientation="portrait" />

    2、代码设置


    调用setRequestedOrientation()函数,其效果就是和在


    AndroidManifest中设置一样的,当前方向和设置的方向不一致的时候,会重启activity,一致的话不会重启;


    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);//横屏设置

    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);//竖屏设置

    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);//默认设置

    注意点:


    不想activity被重启,可以在AndroidManifest中加上android:configChanges(orientation|screenSize这两个一定要加上)


     
    
                android:name=".MainActivity"

                android:screenOrientation="sensor"

                android:configChanges="keyboardHidden|orientation|screenSize">

    3、监听屏幕旋转变化


    重写onConfigurationChanged方法


        @Override

        public void onConfigurationChanged(Configuration newConfig) {

            super.onConfigurationChanged(newConfig);

            Log.d(TAG, "onConfigurationChanged");

        }

    这个方法将会在屏幕旋转变化时调用,可以在这里做出我们在屏幕变化时想要的操作,并且不会重启activity。但它只能一次旋转90度,如果一下子旋转180度,onConfigurationChanged函数不会被调用;


    4、自定义旋转监听设置


    想更加完美,更加完全的掌控监听屏幕旋转变化,就的自定义旋转监听


    (1)创建一个类继承OrientationEventListener


    (2)开启和关闭监听


    可以在 activity 中创建MyOrientationDetector 类的对象,注意,监听的开启的关闭,是由该类的父类的 enable() 和 disable() 方法实现的;


    因此,可以在activity的 onResume() 中调用MyOrientationDetector 对象的 enable方法,在 onPause() 中调用MyOrientationDetector 对象的 disable方法来完车功能;


    (3)监测指定的屏幕旋转角度


    MyOrientationDetector类的onOrientationChanged 参数orientation是一个从0~359的变量,如果只希望处理四个方向,加一个判断即可:


     OrientationEventListener mOrientationListener;

        @Override

        public void onCreate(Bundle savedInstanceState) {

            super.onCreate(savedInstanceState);

            setContentView(R.layout.main);

            mOrientationListener = new OrientationEventListener(this,

                SensorManager.SENSOR_DELAY_NORMAL) {

                @Override

                public void onOrientationChanged(int orientation) {

                if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {

        return;  //手机平放时,检测不到有效的角度

    }

    //只检测是否有四个角度的改变

    if (orientation > 350 || orientation < 10) { //0度

        orientation = 0;

    } else if (orientation > 80 && orientation < 100) { //90度

        orientation = 90;

    } else if (orientation > 170 && orientation < 190) { //180度

        orientation = 180;

    } else if (orientation > 260 && orientation < 280) { //270度

        orientation = 270;

    } else {

        return;

    }

    Log.v(DEBUG_TAG,"Orientation changed to " + orientation);

                }

            };

           if (mOrientationListener.canDetectOrientation()) {

               Log.v(DEBUG_TAG, "Can detect orientation");

               mOrientationListener.enable();

           } else {

               Log.v(DEBUG_TAG, "Cannot detect orientation");

               mOrientationListener.disable();

           }

        }

        @Override

        protected void onDestroy() {

            super.onDestroy();

            mOrientationListener.disable();

        }

    总结


    快年底了,很多人都要找工作或者写毕业设计,有不懂就发私信给我,或许可以给你点帮助建议;


    我们一起努力进步;


    作者:Android开发编程
    链接:https://juejin.cn/post/7033665120962084901
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    为什么 MySQL 不推荐使用 join?

     1. 对于 mysql,不推荐使用子查询和 join 是因为本身 join 的效率就是硬伤,一旦数据量很大效率就很难保证,强烈推荐分别根据索引 单表取数据,然后在程序里面做 join,merge 数据。   2. 子查询就更别用了,效率太差,执行子查询时,M...
    继续阅读 »

     1. 对于 mysql,不推荐使用子查询和 join 是因为本身 join 的效率就是硬伤,一旦数据量很大效率就很难保证,强烈推荐分别根据索引


    单表取数据,然后在程序里面做 join,merge 数据。


      2. 子查询就更别用了,效率太差,执行子查询时,MYSQL 需要创建临时表,查询完毕后再删除这些临时表,所以,子查询的速度会


    受到一定的影响,这里多了一个创建和销毁临时表的过程。


      3. 如果是 JOIN 的话,它是走嵌套查询的。小表驱动大表,且通过索引字段进行关联。如果表记录比较少的话,还是 OK 的。大的话


    业务逻辑中可以控制处理。


      4. 数据库是最底层的,瓶颈往往是数据库。建议数据库只是作为数据 store 的工具,而不要添加业务上去。


      让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。如果关联中的某个表发生了变化,那么就无法使用查


    询缓存了,而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了。


      将查询分解后,执行单个查询可以减少锁的竞争。


      在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。


      查询本身效率也可能会有所提升。查询 id 集的时候,使用 IN()代替关联查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的关联要更高效。


      可以减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需


    要重复地访问一部分数据。从这点看,这样的重构还可能会减少网络和内存的消艳。


      更进一步,这样做相当于在应用中实现了哈希关联,而不是使用 MySQL 的嵌套循环关联。某些场景哈希关联的效率要高很多。


    当应用能够方便地缓存单个查询的结果的时候


    当可以将数据分布到不同的 MySQL 服务器上的时候


    当能够使用 IN()的方式代替关联查询的时候


    并发场景多,DB 查询频繁,需要分库分表


    1.DB 承担的业务压力大,能减少负担就减少。当表处于百万级别后,join 导致性能下降;


    2. 分布式的分库分表。这种时候是不建议跨库 join 的。目前 mysql 的分布式中间件,跨库 join 表现不良。


    3. 修改表的 schema,单表查询的修改比较容易,join 写的 sql 语句要修改,不容易发现,成本比较大,当系统比较大时,不好维护。


      在业务层,单表查询出数据后,作为条件给下一个单表查询。也就是子查询。 会担心子查询出来的结果集太多。mysql 对 in 的数量没有限制,但是


      mysql 限制整条 sql 语句的大小。通过调整参数 max_allowed_packet ,可以修改一条 sql 的最大值。建议在业务上做好处理,限制一次查询出来的结果集是能接受的。


      关联查询的好处时候可以做分页,可以用副表的字段做查询条件,在查询的时候,将副表匹配到的字段作为结果集,用主表去 in 它,但是问题来了,如果匹配到的数据量太大就不行了,也会导致返回的分页记录跟实际的不一样,解决的方法可以交给前端,一次性查询,让前端分批显示就可以了,这种解决方案的前提是数据量不太,因为 sql 本身长度有限。


    作者:Java小咖秀
    链接:https://juejin.cn/post/6948227472232022053
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    我的Android开发之旅(一):BaseActivity的浅入之简单封装 Toolbar

    为什么要写BaseAcivity 我们都知道在做Android应用开发的时候都需要创建一个Activity,但很多时候我们的程序有多个界面并且每个界面都有相似的内容(例如:Toolbar、DrawerLayout)和后台的操作有共同的方法,这个时候我们写一个B...
    继续阅读 »

    为什么要写BaseAcivity


    我们都知道在做Android应用开发的时候都需要创建一个Activity,但很多时候我们的程序有多个界面并且每个界面都有相似的内容(例如:Toolbar、DrawerLayout)和后台的操作有共同的方法,这个时候我们写一个BaseActivity作为每一个Activity的基类,统一管理程序中的每个Activity。


    一行代码实现 Toolbar 效果


    activity_main.xml 的代码


    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:gravity="center"
    android:background="@android:color/holo_blue_light">

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="我是MainActivity的界面"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

    </LinearLayout>

    MainAcitvity.java 的代码


    public class MainActivity extends BaseActivity {

    @Override
    protected int getContentView() {
    return R.layout.activity_main;
    }
    }

    在这里插入图片描述

    在上面的 activity_main.xml 中可以看出,父布局设置了背景颜色和里面只有一个TextView,并没图中的Toolbar。那到底是为什么呢?其实细心观察的小伙伴们会发现,怎么MainActivity中的代码和平常不一样呢?onCreate()方法呢?别急,我们重头开始!


    “少啰嗦,先看东西”




    • 创建 BaseActivity


      在项目创建后,我们可以看到AndroidStudio自动帮我们生成了MainActivity.java和activity_main.xml文件,然后我们再创建一个新的Activity,命名为BaseActivity。

      在这里插入图片描述




    • 修改 activity_base.xml 文件


      接着打开activity_base.xml文件,把父布局的ConstraintLayout换成垂直的LinearLayout(其实也可以不换的,主要是我喜欢用LinearLayout),并在里面添加两个元素Toolbar和FrameLayout。



      注意:由于Toolbar代替 ActionBar,所以先把 ActionBar 去掉,我们通过设置 Application 的 theme 来隐藏,这样项目中所有的界面的 ActionBar 就都隐藏了。





    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".BaseActivity"
    android:orientation="vertical">

    <androidx.appcompat.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="?attr/colorPrimary"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

    <FrameLayout
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    </FrameLayout>

    </LinearLayout>


    • 修改 BaseActivity.java 文件


      接下来打开 BaseActivity.java 文件,让 BaseActivity 继承 AppCompatActivity ,修改代码如下



      注意:protected abstract int getContentView(); 是一个抽象方法,所以我们要将 BaseActivity 修改成抽象类。为什么要修改成抽象类呢?原因很简单,因为一个类里是不允许有一个抽象方法的,如要有抽象方法,那这个类就必须是抽象类。那可能你又会问,为什么要用抽象方法呢?(你是十万个为什么吗?哪来的那么多为什么)因为我们想让其他 Activity 的界面显示到 BaseActivity 中,那这个方法是必须要实现的,如果设置成普通的方法的话,我们很有可能在写代码的时候忘记了调用了这个方法,导致界面不显示。所以我们得用抽象方法,这样每个 Activity 继承这个 BaseActivity 的时候就必须覆写 getContentView() 这个方法。





    public abstract class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_base);
    initView();
    }

    private void initView() {
    // 绑定控件
    Toolbar toolbar = findViewById(R.id.toolbar);
    FrameLayout container = findViewById(R.id.container);
    // 初始化设置Toolbar
    toolbar.setTitle("我是BaseActivity的Toolbar");
    setSupportActionBar(toolbar);
    // 将继承了BaseActivity的布局文件解析到 container 中,这样 BaseActivity 就能显示 MainActivity 的布局文件了
    LayoutInflater.from(this).inflate(getContentView(), container);
    }

    /**
    * 获取要显示内容的布局文件的资源id
    *
    * @return 显示的内容界面的资源id
    */
    protected abstract int getContentView();

    }

    • 修改 activity_main.xml 文件
      打开 activity_main.xml 文件,然后我们将父布局的背景颜色修改一下,方便我们辨别到底是 MainActivity 的布局文件还是 BaseActivity 的布局文件。再添加添加一个 TextView ,原因也是和修改背景颜色是一样的。


    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:gravity="center"
    android:background="@android:color/holo_blue_light">

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="我是MainActivity的界面"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

    </LinearLayout>

    • 修改 MainActivity.java 文件
      让 MainActivity 继承 BaseActivity 并覆写 getContentView() 方法,然后删除onCreate()方法。通过 getContentView() 方法返回当前的布局资源ID给 BaseActivity,让 BaseActivity 加载布局文件。


    public class MainActivity extends BaseActivity {

    @Override
    protected int getContentView() {
    return R.layout.activity_main;
    }
    }


    • 运行项目


      现在你运行一下项目,我们并没有在 MainActivity 的布局中添加 ToolBar,但是运行出来的效果是 Toolbar 已经存在了。

      在这里插入图片描述

      现在就能做到用一行代码实现 Toolbar 的效果。那你现在可能就会有疑问,如果我像对 Toolbar 修改标题和添加按钮呢?其实也简单,我们继续往下看。




    • 修改标题
      我们在 BaseActivity 中再添加一个抽象方法,并在初始化 Toolbar 那一处调用我们写的这个抽象方法。




    public abstract class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_base);
    initView();
    }

    private void initView() {
    // 绑定控件
    Toolbar toolbar = findViewById(R.id.toolbar);
    FrameLayout container = findViewById(R.id.container);
    // 初始化设置Toolbar
    toolbar.setTitle(setTitle());
    setSupportActionBar(toolbar);
    // 将继承了BaseActivity的布局文件解析到 container 中,这样 BaseActivity 就能显示 MainActivity 的布局文件了
    LayoutInflater.from(this).inflate(getContentView(), container);
    }

    /**
    * 获取要显示内容的布局文件的资源id
    *
    * @return 显示的内容界面的资源id
    */
    protected abstract int getContentView();

    /**
    * 设置标题
    *
    * @return 要显示的标题名称
    */
    protected abstract String setTitle();

    }

    • 修改 MainActivity.java
      我们还是像刚才一样覆写 setTitle() 方法,并在返回值输入我们想要显示的标题


    public class MainActivity extends BaseActivity {

    @Override
    protected int getContentView() {
    return R.layout.activity_main;
    }

    @Override
    protected String setTitle() {
    return "我是MainActivity的标题";
    }
    }

    这个时候再运行以下你的程序就会出现你设置的标题了。

    在这里插入图片描述
    那么现在你可能又会问了,如果我想对 Toolbar 添加一个返回按钮,并能对他进行操作应该怎么办?(我不想写了,你也别问了!)其实很简单,在 BaseActivity 里自定义一个接口,在子类中设置这个接口的实例就行。




    • 显示返回按钮
      我们先给 Toolbar 显示返回按钮,通过 getSupportActionBar() 得到 ActionBar 的实例,再调用 ActionBar 的 setDisplayHomeAsUpEnabled(true) 方法让返回按钮显示出来。



      Toolbar 最左侧的按钮是叫做HomeAsUp,默认是隐藏的,并且它的图标是一个返回箭头。还有一种 setNavigationcon() 方法也能设置图标,具体可以查找 Toolbar 的文档说明





    private void initView() {
    // 绑定控件
    Toolbar toolbar = findViewById(R.id.toolbar);
    FrameLayout container = findViewById(R.id.container);
    // 初始化设置Toolbar
    toolbar.setTitle(setTitle());
    setSupportActionBar(toolbar);
    // 将继承了BaseActivity的布局文件解析到 container 中,这样 BaseActivity 就能显示 MainActivity 的布局文件了
    LayoutInflater.from(this).inflate(getContentView(), container);
    // 显示返回按钮
    ActionBar actionBar = getSupportActionBar();
    if (actionBar != null) {
    actionBar.setDisplayHomeAsUpEnabled(true);
    }
    // 初始化
    init();
    }


    • 自己定义一个接口并声明
      这里我就用截图显示代码片段

      在这里插入图片描述




    • 设置监听事件
      打开 MainActivity.java 文件,覆写 init() 方法,并调用父类的 setBackOnClickListener() 方法。



      这里我用了 lambda 表达式,这是 java 8 才支持的,默认项目是不支持的,你在 build.gradle 中需要声明一下。





    	@Override
    protected void init() {
    setBackOnClickListener(() ->
    Toast.makeText(this, "点击了一下返回按钮", Toast.LENGTH_SHORT).show()
    );
    }

    • 运行app
      在这里插入图片描述


    最后


    相信你对 BaseActivity 有了一些简单的了解了,具体如何使用还是得看你的项目,不是项目里就一定要写 BaseActivity 和所有 Activity 都要继承 BaseActivity ,我只是将我所理解的 BaseActivity 和大家分享一下。可能你看完了这一篇文章发现还是没能理解,在这里我想说声抱歉,可能有些地方讲的不够通俗易懂或是讲解有误,还请您多多指教,我会虚心接受并及时改正(就算是讲错了我也不会改)。



    Demo的Github地址:
    github.com/lmx0206/Bas…
    Demo的Gitee地址:
    gitee.com/Leungmx/Bas…


    作者:梁大侠
    链接:https://juejin.cn/post/6844904146659704846
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Android 包大小优化实践

    android减少包大小是非常必要的,在性能,转换率等等都有益处,而常用的包大小优化Google已经给出了一些方案,再加上市面上的一些美团方案,微信方案、抖音方案等等,下面就说一下我们在包大小优化做的努力。 1、使用AAB模式 google play现在强制所...
    继续阅读 »

    android减少包大小是非常必要的,在性能,转换率等等都有益处,而常用的包大小优化Google已经给出了一些方案,再加上市面上的一些美团方案,微信方案、抖音方案等等,下面就说一下我们在包大小优化做的努力。


    1、使用AAB模式


    google play现在强制所有上传的应用都使用aab,Google Play 会使用您的 AAB 针对每种设备配置生成并提供经过优化的 APK,因此只会下载特定设备所需的代码和资源来运行您的应用。假如一个AAB是90MB在google play上下载耗费的流量可能也就50MB,但是这种方案对性能上没有任何的影响只是减少了下载流量可能会增加一些转换率。具体文档可以参考官方文档。这里有必要说一下AAB还有更多又去的玩法比如使用AAB实现插件化(对模块拆分还是非常有帮助的),对不同地区实现不同的业务然后使用google play进行分发


    2、使用AGP配置来减少包大小(链接


    使用lint本地检测无用资源或者开启shrinkResources


    使用lint本地检查无用资源


    1、点击AS上的Analyze菜单按钮,选择Run Inspection by Name 如下图


    image.png
    2、会出现一个弹窗, 输入unused resources


    image.png
    3、会弹出“inspaction scope”选择窗口,选择检查的范围,一般选择整个项目或模块。“inspaction scope”窗口下面还可以设置文件过滤,选择好后点ok就开始检查了


    image.png
    4、下面的输出栏会输出没有用的资源文件。


    image.png
    5、删除无用资源


    开启shrinkResources


    android {
        ...
        buildTypes {
            release {
                shrinkResources true
                minifyEnabled true
                proguardFiles
                    getDefaultProguardFile('proguard-android-optimize.txt'),
                    'proguard-rules.pro'
            }
        }
    }
    复制代码

    此配置必须和代码压缩一起使用才有效果,如果说要保留某些资源,假如插件化里面宿主里面放了某个资源需要给很多个插件使用,这个时候就需要保留此资源那么就需要做如下配置:
    在项目的res目录下新建一个创建一个包含 <resources> 标记的 XML 文件,tools:keep 属性中指定每个要保留的资源,在 tools:discard 属性中指定每个要舍弃的资源。例如:


    <?xml version="1.0" encoding="utf-8"?>
    <resources xmlns:tools="http://schemas.android.com/tools"
        tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
        tools:discard="@layout/unused2" />
    复制代码

    缩减、混淆处理和代码优化功能


    在AGP 3.4.0以上版本R8是默认编辑器,把代码编译成android所需要的dex文件,但是代码的缩减、混淆和代码优化是默认关闭的,建议在release版本应用将此功能打开可以有效的减少包体积。上面提到的shrinkResources也是必须和此功能一起使用才有效,关于R8更多的配置可以详见官网文档
    如果要保留一些类就要在proguard-rules.pro 配置具体proguard 配置规则可以查看手册
    这里proguard特点说一下他是累加的,假如A moudle 依赖B moudle,那么到最后proguard 规则就是A moudle 的配置+B moudle的配置


    移除无用so库


    现在市面上的手机cpu指令基本上就两种了 armeabi-v7a 和 arm64-v8a,x86可以不考虑了很少有手机在用了,所以就可以通过gradle配置来只依赖v7a和v8a,例如:


    android {
    ...
    defaultConfig {
    ...
    ndk {
    // Specifies the ABI configurations of your native
    // libraries Gradle should build and package with your APK.
    abiFilters 'armeabi-v7a','arm64-v8a'
    }
    }

    }
    复制代码

    但是这样还是会很大,可以减少arm64-v8a指令集,不管你的手机cpu是v7还是v8都可以运行v7的so。
    如果说你的应用有多个变种,比如一个是上线到google play的aab一个是在国内上线的apk这样你可以使用不同变种依赖不同的指令集


    android {
    ...
    defaultConfig {
    ...
    flavorDimensions "market"
    productFlavors {

    //上google play市场
    gp {
    dimension "market"
    ndk.abiFilters "armeabi-v7a", "arm64-v8a"
    }

    //中国版本
    cn {
    dimension "market"
    ndk.abiFilters "armeabi-v7a"}
    }
    }
    复制代码

    3、美团进阶方案


    美团的这种方案主要是提到了zip压缩。dex优化,R Field的优化,我主要是用了R Field的优化。主要说下这个R field的优化这个点。这篇文章写的比较早用的是java代码插桩的方式进行处理的,现在其实AGP就可以完成一样的处理。
    先说下原理为什么R文件会导致包大小变大,假如你的项目结构如下:


    image.png


    lib1_R= lib1_R


    lib2_R= lib2_R


    lib3_R= lib3_R


    biz1_R = lib1_R + lib2_R + lib3_R + (自己的R)


    biz2_R = lib2_R + lib3_R + (自己的R)


    app_R = lib1_R + lib2_R + lib3_R + biz1_R + biz2_R + (自己的R)


    app_R因为是final的所以如果开启java优化也就是混淆会被shrink掉,但是其他moudle的R不是final而且引用也不是直接使用id的值来引用的:


    这是app moudle 下的MainActivity中setcontent对应的字节码:


    WeChata234b76e355e3899a7be1119de8a8880.png


    这是子moudle Activity中同样setContent对应的字节码:


    WeChat71e04d6bc8f6153355d695db68318246.png


    发现什么不同了吗?就是子moudle中是R的变量引用而非常量,在打包过程中aapt会将这个变量统一赋值来防止id冲突,而如果你的项目特别复杂子moudle特别多的话那么各个R类的就会特别大,但是这部分是必须的吗?好像有方法来解决


    R类内联解决方法一:


    就好像美团方案里面这种方法利用插桩在aapt分配了id后将对应的R变量的引用修改成对应的id值这样就可以把原有的R类删除掉(这个方案中的插桩插件是自己写的其实市面上有很多插桩三方库,字节的bytex,滴滴的Booster这些都可以直接使用)


    R类内联解决方法二:


    升级AGP版本到4.1以上
    image.png


    这是AGP 4.1版本的升级说明截图,他帮助咱们做了上面美团方案的插桩替换的一系列动作,R类内联是非常有必要的我们的app做了R类的内联以后apk大小减少了百分之十。这部分收益必须是在R类没有被混淆的时候keep住的前提下才可以,如果keep住了R类这部分收益就没办法了,可以在主moudle的proguard-rules.-printconfiguration "build/outputs/mapping/configuration.txt"(其实此文件的路径随便写都可以)来查看是不是添加了keep R类。如果是自己工程里面添加了keep R类就直接删了就好但是如果是三方的aar怎么办呢?


        tasks.each {
    if (it.name.startsWith("mini") && it.name.endsWith("R8")) {
    def f = it.configurationFiles.filter { File ff ->
    if (ff.exists()) {
    !ff.text.contains("-keep public class **.R\$*")
    } else {
    false
    }
    }
    it.configurationFiles.setFrom(f.files)
    }
    }
    }
    复制代码

    可以在app moudle 下的build.gradle中添加如下代码,会自动的将keep R类排除在外


    4、资源压缩


    在apk中res的资源占了很大的一部分,这部分如果可以被减少那么对减少apk size也有很大的收益,在android中主要的资源是图片,图片有几种格式jpg,png,webp同一个图片应该webp是最小的,所以可以将图片从png转成webp这样apk size不就变小了。如果是一两张图片还好可以在线转然后直接丢到工程里面但是如果所有的图片都要转呢?


    抖音团队给出了一个无入侵的解决方案,但是我并没有完全使用他这种方案因为需要hook如果说android系统版本更新hook点就要非常小心,搞过插件化的都知道这是永远的痛,但是有什么更好的方法吗?其实是有的


    需要先明确个概念就是android中所有的res资源都是会合并的,但是如果在不同的moudle包含了同样的res怎么办呢?答案是合并,合并的规则为参考官网总而言之就是主moudle会优先依赖的moudle,根据这个特点是不是可以将所有的图片都转成webp到主moudle的res目录中呢?


    在我们的项目中使用了zoom通过apk大小分析看到zoom相关的图片是最大的所以这里就拿zoom为例。


    先讲下操作思路,自定义gradle插件,添加png转webp task将png转为webp,我这里用的是webp官方的转换工具libwebp,其实还有其他工具。


    下个问题就是怎么获取当前工程中全部的资源呢?答案是通过AGP 中android 对象下的 ApplicationVariant中的 getAllRawAndroidResources()此方法可以把当前变种所有的资源都获取到,具体代码如下:


    Set<File> getAllResPath(Project project) {
    def extension = project.getExtensions().getByName("android")
    Set<File> allRes = new HashSet<>()
    if (project.getPlugins().hasPlugin("com.android.application")) {
    extension.applicationVariants.each {
    allRes.addAll(it.allRawAndroidResources)
    }
    return allRes
    }
    return null
    }
    复制代码

    获取到所有的资源以后就可以一层层的遍历找到非webp的图片然后使用libwebp转为webp


    5、插件化


    插件化是一个非常好的减少包大小的方式,将一些无关紧要不常用的moudle改成插件,然后发布的时候只发布宿主,到用户使用到对应的模块时候下载对应的插件,市面上有很多插件化方案,大家可以对比选用,这个我们也在进行中ing。


    参考链接:


    美团方案


    网易大前端团队实践


    抖音瘦身实践


    作者:Android_阿飞
    链接:https://juejin.cn/post/7031471769248595975
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    面试官:一千万数据,怎么快速查询?

    sql
    前言 面试官: 来说说,一千万的数据,你是怎么查询的?B哥:直接分页查询,使用limit分页。面试官:有实操过吗?B哥:肯定有呀 此刻献上一首《凉凉》 也许有些人没遇过上千万数据量的表,也不清楚查询上千万数据量的时候会发生什么。 今天就来带大家实操一下,这次...
    继续阅读 »

    前言



    • 面试官: 来说说,一千万的数据,你是怎么查询的?
    • B哥:直接分页查询,使用limit分页。
    • 面试官:有实操过吗?
    • B哥:肯定有呀

    此刻献上一首《凉凉》


    也许有些人没遇过上千万数据量的表,也不清楚查询上千万数据量的时候会发生什么。


    今天就来带大家实操一下,这次是基于MySQL 5.7.26做测试


    准备数据


    没有一千万的数据怎么办?


    创建呗


    代码创建一千万?那是不可能的,太慢了,可能真的要跑一天。可以采用数据库脚本执行速度快很多。


    创建表

    CREATE TABLE `user_operation_log`  (
      `id` int(11NOT NULL AUTO_INCREMENT,
      `user_id` varchar(64CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `ip` varchar(20CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `op_data` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr1` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr2` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr3` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr4` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr5` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr6` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr7` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr8` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr9` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr10` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr11` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr12` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`USING BTREE
    ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

    创建数据脚本

    采用批量插入,效率会快很多,而且每1000条数就commit,数据量太大,也会导致批量插入效率慢


    DELIMITER ;;
    CREATE PROCEDURE batch_insert_log()
    BEGIN
      DECLARE i INT DEFAULT 1;
      DECLARE userId INT DEFAULT 10000000;
     set @execSql = 'INSERT INTO `test`.`user_operation_log`(`user_id`, `ip`, `op_data`, `attr1`, `attr2`, `attr3`, `attr4`, `attr5`, `attr6`, `attr7`, `attr8`, `attr9`, `attr10`, `attr11`, `attr12`) VALUES';
     set @execData = '';
      WHILE i<=10000000 DO
       set @attr = "'测试很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长的属性'";
      set @execData = concat(@execData"(", userId + i, ", '10.0.69.175', '用户登录操作'"","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr")");
      if i % 1000 = 0
      then
         set @stmtSql = concat(@execSql@execData,";");
        prepare stmt from @stmtSql;
        execute stmt;
        DEALLOCATE prepare stmt;
        commit;
        set @execData = "";
       else
         set @execData = concat(@execData",");
       end if;
      SET i=i+1;
      END WHILE;

    END;;
    DELIMITER ;

    开始测试



    哥的电脑配置比较低:win10 标压渣渣i5 读写约500MB的SSD



    由于配置低,本次测试只准备了3148000条数据,占用了磁盘5G(还没建索引的情况下),跑了38min,电脑配置好的同学,可以插入多点数据测试


    SELECT count(1FROM `user_operation_log`

    返回结果:3148000


    三次查询时间分别为:



    • 14060 ms
    • 13755 ms
    • 13447 ms

    普通分页查询


    MySQL 支持 LIMIT 语句来选取指定的条数数据, Oracle 可以使用 ROWNUM 来选取。


    MySQL分页查询语法如下:


    SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset


    • 第一个参数指定第一个返回记录行的偏移量
    • 第二个参数指定返回记录行的最大数目

    下面我们开始测试查询结果:


    SELECT * FROM `user_operation_log` LIMIT 1000010

    查询3次时间分别为:



    • 59 ms
    • 49 ms
    • 50 ms

    这样看起来速度还行,不过是本地数据库,速度自然快点。


    换个角度来测试


    相同偏移量,不同数据量

    SELECT * FROM `user_operation_log` LIMIT 1000010
    SELECT * FROM `user_operation_log` LIMIT 10000100
    SELECT * FROM `user_operation_log` LIMIT 100001000
    SELECT * FROM `user_operation_log` LIMIT 1000010000
    SELECT * FROM `user_operation_log` LIMIT 10000100000
    SELECT * FROM `user_operation_log` LIMIT 100001000000

    查询时间如下:

















































    数量 第一次 第二次 第三次
    10条 53ms 52ms 47ms
    100条 50ms 60ms 55ms
    1000条 61ms 74ms 60ms
    10000条 164ms 180ms 217ms
    100000条 1609ms 1741ms 1764ms
    1000000条 16219ms 16889ms 17081ms

    从上面结果可以得出结束:数据量越大,花费时间越长


    相同数据量,不同偏移量

    SELECT * FROM `user_operation_log` LIMIT 100100
    SELECT * FROM `user_operation_log` LIMIT 1000100
    SELECT * FROM `user_operation_log` LIMIT 10000100
    SELECT * FROM `user_operation_log` LIMIT 100000100
    SELECT * FROM `user_operation_log` LIMIT 1000000100










































    偏移量 第一次 第二次 第三次
    100 36ms 40ms 36ms
    1000 31ms 38ms 32ms
    10000 53ms 48ms 51ms
    100000 622ms 576ms 627ms
    1000000 4891ms 5076ms 4856ms

    从上面结果可以得出结束:偏移量越大,花费时间越长


    SELECT * FROM `user_operation_log` LIMIT 100100
    SELECT idattr FROM `user_operation_log` LIMIT 100100

    如何优化


    既然我们经过上面一番的折腾,也得出了结论,针对上面两个问题:偏移大、数据量大,我们分别着手优化


    优化偏移量大问题


    采用子查询方式

    我们可以先定位偏移位置的 id,然后再查询数据


    SELECT * FROM `user_operation_log` LIMIT 100000010

    SELECT id FROM `user_operation_log` LIMIT 10000001

    SELECT * FROM `user_operation_log` WHERE id >= (SELECT id FROM `user_operation_log` LIMIT 10000001LIMIT 10

    查询结果如下:































    sql 花费时间
    第一条 4818ms
    第二条(无索引情况下) 4329ms
    第二条(有索引情况下) 199ms
    第三条(无索引情况下) 4319ms
    第三条(有索引情况下) 201ms

    从上面结果得出结论:



    • 第一条花费的时间最大,第三条比第一条稍微好点
    • 子查询使用索引速度更快

    缺点:只适用于id递增的情况


    id非递增的情况可以使用以下写法,但这种缺点是分页查询只能放在子查询里面


    注意:某些 mysql 版本不支持在 in 子句中使用 limit,所以采用了多个嵌套select


    SELECT * FROM `user_operation_log` WHERE id IN (SELECT t.id FROM (SELECT id FROM `user_operation_log` LIMIT 100000010AS t)

    采用 id 限定方式

    这种方法要求更高些,id必须是连续递增,而且还得计算id的范围,然后使用 between,sql如下


    SELECT * FROM `user_operation_log` WHERE id between 1000000 AND 1000100 LIMIT 100

    SELECT * FROM `user_operation_log` WHERE id >= 1000000 LIMIT 100

    查询结果如下:



















    sql 花费时间
    第一条 22ms
    第二条 21ms

    从结果可以看出这种方式非常快


    注意:这里的 LIMIT 是限制了条数,没有采用偏移量


    优化数据量大问题


    返回结果的数据量也会直接影响速度


    SELECT * FROM `user_operation_log` LIMIT 11000000

    SELECT id FROM `user_operation_log` LIMIT 11000000

    SELECT id, user_id, ip, op_data, attr1, attr2, attr3, attr4, attr5, attr6, attr7, attr8, attr9, attr10, attr11, attr12 FROM `user_operation_log` LIMIT 11000000

    查询结果如下:























    sql 花费时间
    第一条 15676ms
    第二条 7298ms
    第三条 15960ms

    从结果可以看出减少不需要的列,查询效率也可以得到明显提升


    第一条和第三条查询速度差不多,这时候你肯定会吐槽,那我还写那么多字段干啥呢,直接 * 不就完事了


    注意本人的 MySQL 服务器和客户端是在同一台机器上,所以查询数据相差不多,有条件的同学可以测测客户端与MySQL分开


    SELECT * 它不香吗?

    在这里顺便补充一下为什么要禁止 SELECT *。难道简单无脑,它不香吗?


    主要两点:



    1. 用 "SELECT * " 数据库需要解析更多的对象、字段、权限、属性等相关内容,在 SQL 语句复杂,硬解析较多的情况下,会对数据库造成沉重的负担。
    2. 增大网络开销,* 有时会误带上如log、IconMD5之类的无用且大文本字段,数据传输size会几何增涨。特别是MySQL和应用程序不在同一台机器,这种开销非常明显。

    结束


    最后还是希望大家自己去实操一下,肯定还可以收获更多,欢迎留言!!


    创建脚本我给你正好了,你还在等什么!!!


    再奉上我之前 MySQL 如何优化


    作者:Owater
    链接:https://juejin.cn/post/6863668253898735629
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Android 基础架构组面试题 | 面试

    SDK相关面试的时候我觉得哦,这些sdk有任意其实你研究的比较深入就行了,应该能在面试中表现的很好了。还有就是个人建议最好还是在单一方向研究的更深入一点,别的只要大概知道干什么的就行了。配置中心以及灰度测试app必备工具之一,配置中心主要负责的就是动态化的配置...
    继续阅读 »

    SDK相关

    面试的时候我觉得哦,这些sdk有任意其实你研究的比较深入就行了,应该能在面试中表现的很好了。还有就是个人建议最好还是在单一方向研究的更深入一点,别的只要大概知道干什么的就行了。

    1. 配置中心以及灰度测试

    app必备工具之一,配置中心主要负责的就是动态化的配置,比如文本展示类似这些的。sdk提供方需要负责的是提供动态更新能力,这里有个差异化更新,只更新dif部分,还有就是流量优化等等需要开发同学考虑的。然后可以考虑下存储性能方面的提升等。

    而abtest也是app必备工具之一了,动态的下发实验策略,之后开发同学可以切换实验的页面。另外主要需要考虑灰度结果计算,分桶以及版本过滤白名单等等。这里只是一个简单的介绍不展开,因为我只是一个使用方。

    1. 调试组件

    个人还是更推荐滴滴的Dokit,功能点比较多而且接入相对来说比较简单。而且提供了很多给开发同学定制的能力,可以在debug情况下增加很多业务相关的测试功能,方便测试同学,核心还是浮窗太方便了。

    当然很多实验性的预研功能等其实都可以直接接在这里,然后在测试环境下充分展开,之后在进行线上灰度方案。还有一些具有风险的hook操作,个人也比较建议放在debug组件上。

    1. 性能监控框架

    这部分有几个不同的方面,首先是异常崩溃方面的,另外则是性能监控方面的,但是他们整体是划分在一起的,都属于线上性能监控体系的。

    Crash相关的,可以从爱奇艺的xCrash学起。包含了崩溃日志,ANR以及native crash,因为版本适配的问题ANR在高版本上已经不是这么好捞了,还有就是native crash相关的。是一个非常牛逼的库了。

    而线上的性能监控框架可以从腾讯的Matrix学起,以前有两篇文章介绍的内容也都是和Matrix相关的, Matrix首页上也有介绍,比如fps,卡顿,IO,电池,内存等等方面的监控。其中卡顿监控涉及到的就是方法前后插桩,同时要有函数的mapping表,插桩部分整体来说比较简单感觉。

    另外关于线上内存相关的,推荐各位可以学习下快手的koom, 对于hprof的压缩比例听说能达到70%,也能完成线上的数据回捞以及监控等等,是一个非常屌的框架。下面给大家一个抄答案的方式。字节也有一个类似的原理其实也差不多。

    主进程发现内存到达阈值的时候,用leakcanary的方案,通过shark fork进程内存,之后生成hrop。由于hrop文件相对较大,所以我们需要对于我们所要分析的内容进行筛选,可以通过xhook,之后对hrop的写入操作进行hook,当发现写入内容的类型符合我们的需要的情况下才进行写入。

    而当我们要做线上日志回捞的情况,需要对hprof 进行压缩,具体算法可以参考koom/raphel,有提供对应的压缩算法。

    最后线上回捞机制就是基于一个指令,回捞线上符合标准的用户的文件操作,这个自行设计。

    其实上述几个库都还是有一个本质相关的东西,那么就是plthook,这个上面三个库应该都有对其的使用,之前是爱奇艺的xhook,现在是字节的bhook, 这个大佬也是我的偶像之一了,非常离谱了算是。

    Android 性能采集之Fps,Memory,Cpu 和 Android IO监控

    最近已经不咋写这部分相关了,所以也就没有深挖,但是后续可能会有一篇关于phtead hook相关的,也是之前matrix更新的一个新东西,还在测试环境灰度阶段。

    1. 基础网络组件

    虽然核心可能还是三方网络库,但是因为基本所有公司都对网络方面有调整和改动,以及解析器等方面的优化,其实可以挖的东西也还是蛮多的。

    应付面试的同学可以看看Android网络优化方案。当然还是要具体问题具体分析,毕竟头疼医头,脚疼医脚对吧。

    之前和另外一个朋友聊了下,其实很多厂对json解析这部分有优化调整,通过apt之后更换原生成原生的解析方式,加快反序列化速度的都是可以考虑考虑的。

    1. 埋点框架

    其实这个应该要放在更前面一点的,数据上报数据分析啥的其实都还是蛮重要的。

    这部分因为我完全没写过哦,所以我压根不咋会,但是如果你会的话,面试的时候展开说说,可以帮助你不少。

    另外还需要有线上的异常用户数据回捞系统,方便开发同学主动去把线上有异常的用户的日志给收集回来。

    但是有些刁钻的页面曝光监控啦,自动化埋点啥的其实还是写过一点的,有兴趣的可以翻翻历史,还有github 上还有demo。

    AndroidAutoTrack demo工程

    1. 启动相关

    通过DAG(有向无环图)的方式将sdk的初始化拆解成一个个task,之后理顺依赖关系,让他们能按照固定的顺序向下执行。

    核心需要处理的是依赖关系,比如说其实埋点库依赖于网络库初始化,然后APM相关的则依赖于埋点库和配置中心abtest等等,这样的依赖关系需要开发同学去理顺的。

    另外就是把sdk的粒度打的细碎一点,更容易观察每个sdk任务的耗时情况,之后增加task阈值告警,超过某个加载速度就通知到相应的同学改一下。

    多线程是能优化掉一部分,但是也需要避免频繁线程调度。还有就是我个人觉得这些启动相关的东西因为都无法使用sdk级别的灰度,所以改动最好慎重一点。出发点始终都是好的,但是还是结果导向吧。

    启动优化的核心,我个人始终坚持的就是延迟才能优化。开发人员很难做到优化代码执行的复杂度,执行时间之类的。尽人事听天命,玄学代码。

    1. 中间件(图片 日志 存储 基础信息)

    这部分没啥,最好是对第三方库有一层隔离的思维,但是这个隔离也需要对应的同学对于程序设计方面有很好的思维,说起来简单,其实也蛮复杂的。

    这里就不展开了,感觉面试也很少会问的很细。

    1. 第三方sdk大杂烩(偏中台方向)

    基本一个app现在都有啥分享啦,推送啦,支付啦,账号体系啦,webview,jsbridge等等服务于应用内的一些sdk,这些东西就比较偏向于业务。

    有兴趣的可以看看之前写的两篇关于sdk设计相关的。

    活学活用责任链 SDK开发的一点点心得 Android厂商推送Plugin化

    1. 其他方面

    大公司可能都会有些动态化方案的考虑,比如插件化啊动态化之类的。这部分在下确实不行,我就不展开了啊。

    编译相关

    1. 描述下android编译流程

    基架很容易碰到的面试题,以前简单的描述写过。聊聊Android编译流程

    虽然是几年前的知识点了,但是还是要拆开高低版本的agp做比较的。所以这部分可以回答下,基本这题就能简单的拿下了。

    1. Gradle 生命周期

    简单的说下就是buildSrc先编译,之后是根目录的settings.gradle, 根build.gradle,最后才是module build

    网上一堆,你自己翻一番就知道了。

    1. apt是编译中哪个阶段

    APT解析的是java 抽象语法树(AST),属于javac的一部分流程。大概流程:.java -> AST -> .class

    聊聊AbstractProcessor和Java编译流程

    1. Dex和class有什么区别

    链接传送门

    Class与dex的区别

    1)虚拟机: class用jvm执行,dex用dvm执行

    2)文档: class中冗余信息多,dex会去除冗余信息,包含所有类,查找方便,适合手机端

    JVM与DVM

    1)JVM基于栈(使用栈帧,内存),DVM基于寄存器,速度更快,适合手机端

    2)JVM执行Class字节码,DVM执行DEX

    3)JVM只能有一个实例,一个应用启动运行在一个DVM

    DVM与ART

    1)DVM:每次运行应用都需要一次编译,效率降低。JIT

    2)ART:Android5.0以上默认为ART,系统会在进程安装后进行一次预编译,将代码转为机器语言存在本地,这样在每次运行时不用再进行编译,提高启动效率;。 AOP & JIT

    1. Transform是如何被执行的

    Transform 在编译过程中会被封装成Task 依赖其他编译流程的Task执行。

    image.png

    1. Transform和其他系统Transform执行的顺序

    其实这个题目已经是个过期了,后面对这些都合并整合了,而且最新版的api也做了替换,要不然考虑下回怼下面试官?

    Transform和Task之间有关?

    1. 如何监控编译速度变慢问题
    ./gradlew xxxxx -- scan

    之后会生成一个gradle的网页,填写下你的邮箱就好了。

    另外一个相对来说比较简单了。通过gradle原生提供的listener进行就行了。


    // 耗时统计kt化
    class TimingsListener : TaskExecutionListener, BuildListener {
    private var startTime: Long = 0L
    private var timings = linkedMapOf<String, Long>()


    override fun beforeExecute(task: Task) {
    startTime = System.nanoTime()
    }

    override fun afterExecute(task: Task, state: TaskState) {
    val ms = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS)
    task.path
    timings[task.path] = ms
    project.logger.warn("${task.path} took ${ms}ms")
    }

    override fun buildFinished(result: BuildResult) {
    project.logger.warn("Task timings:")
    timings.forEach {
    if (it.value >= 50) {
    project.logger.warn("${it.key} cos ms ${it.value}\n")
    }
    }
    }

    override fun buildStarted(gradle: Gradle) {

    }

    override fun settingsEvaluated(settings: Settings) {
    }

    override fun projectsLoaded(gradle: Gradle) {

    }

    override fun projectsEvaluated(gradle: Gradle) {

    }

    }

    gradle.addListener(TimingsListener())

    1. Gradle中如何给一个Task前后插入别的任务

    最简单的可以考虑直接获取到Task实例,之后在after和before插入一些你所需要的代码。

    另外一个就是通过dependOn前置和finalizedBy挂载一个任务 mustAfter

    Gradle 使用指南 -- Gradle Task

    1. ksp APT Transform的区别

    ksp 是kotlin专门独立的ast语法树

    apt 是java 的ast语法树

    transform是 agp 专门修改字节码的一个方法。

    反杀时刻AsmClassVisitorFactory,可以看看我之前写的那篇文章。

    1. Transform上的编译优化能做哪些?

    虽然是个即将过期的api,但是大家对他的改动还是都比较多的。

    首先肯定是需要完成增量编译的,具体的可以参考我的demo工程。记住,所有的transfrom都要全量。

    另外可以考虑多线程优化,将转化操作移动到子线程内,建议使用gradle内部的共享线程。

    参考agp最新做法,抽象出一个新的interface,之后通过spi串联,之后将asm链式调用。我的文章也介绍过,具体的点在哪里自己盘算。

    现在准备好告别Transform了吗

    1. aar 源码切换插件原理

    这个前几天刚介绍过,原理和方案业内都差不多,mulite-repo应该都需要这个东西的。我的版本也比较简陋,大厂内部肯定都会有些魔改的。

    相对来说功能肯定会更丰富,更全面一点。

    aar和源码切换插件Plus

    1. 你们有哪些保证代码质量的手段

    最简单的方式还是通过静态扫描+pipline 处理,之后在合并mr之前进行一次拦截。

    静态扫描方式比较多,下面给大家简单的介绍下

    阿里的sonar 但是对kt的支持很糟糕,因为阿里使用,所以有很多现成的规则可以使用,但是如果从0-1接入,你可能会直接放弃。

    原生的lint,可以基于原生提供的lint api,对其进行开发,支持种类也多,基本上算是一个非常优秀的方案了,但是由于文档资料较少,对于开发的要求可能会较高。

    AndroidLint

    1. 如何对第三方的依赖做静态检查?

    魔高一尺道高一丈。lint还是能解决这个问题的。

    Tree Api+ClassScanner = 识别三方隐私权限调用

    1. R.java code too large 解决方案

    又是一个过期的问题,尽早升级agp版本,让R8帮你解决这个问题,R文件完全可以内联的。

    或者用别的AGP插件的R inline也可以解决这个问题。

    1. R inline 你需要注意些什么?

    预扫描,先收集调用的信息,之后在进行替换。还有javac 的时候可能就因为文件过大,直接挂掉了。

    1. 一个类替换父类 比如所有activity实现类替换baseactivity

    class node 直接替换 superName ,想起了之前另外一个问题,感觉主要是要对构造函数进行修改,否则也会出异常。

    1. R8 D8 以及混淆相关的,还有R8除了混淆还能干些什么? 混淆规则有没有碰到什么奇怪的问题?

    D8Dx的区别,主要涉及到编译速度以及编译产物的体积,包体积大概小11%。

    R8 则是变更了整个编译流程的,其中我觉得最微妙的就是java8 lambda相关的,脱糖前后的差别还是比较大的。同时R8也少了很多之前的Transform。

    R8的混淆部分,混淆除了能增加代码阅读难度意外,更多的是对于代码优化方面的。 比如无效代码优化, 同时也删除代码等等都可以做。

    1. 编译的时候有没有碰到javac的常量优化

    javac会将静态常量直接优化成具体的数值。但是尤其是多模块场景下尤其容易出现异常,看起来是个实际的常量引用,但是产物上却是一个具体的常量值了。

    其他部分

    组件化相关

    不仅仅要聊到路由,还需要聊下业务仓库的设计,如何避免两个模块之间相互相互引用导致的环问题。

    另外就是路由的apt aop的部分都可以深入的聊一下。

    如果只聊路由的话,你就只说了一个字符串匹配规则,非常无聊了。

    路由跳转

    路由跳转只是一小部分,其核心原理就是字符串匹配,之后筛选出符合逻辑的页面进行跳转。

    另外就是拦截器的设计,同步异步拦截器两种完全不同的写法。

    其原理基于apt+transform ,apt负责生成模块德 路由表,而transform则负责将各个模块的路由表进行收集。

    服务发现

    类似路由表,但是维护的是一个基于键值的类构造。ab之间当有相互依赖的情况下,可以通过基于接口编程的方式进行调整,互相只依赖抽象的接口,之后实现类在内部,通过注册的机制。之后在实际的使用地方用服务发现的机制寻找。

    虚拟机部分

    很多人会觉得虚拟机这部分都是硬八股,比较无聊。但是其实有时候我们碰到的一些字节码相关的问题就和这部分基础姿势相关了。

    虽然用的比较少,但是也不是一个硬八股,比hashmap好玩太多了。

    依赖注入

    和服务发现类似,也是拿来解决不同模块间的依赖问题。可以使用hilt,依赖注入的好处就是连构造的这部分工作也有di完成了,而且构造能力更多样。可以多参数构造。

    总结

    其实以当前来说安卓的整个体系相对来说很复杂,第三方库以及源代码量都比较大,并不是要求每个同学都对这些有一个良好的掌握,但是大体上应该了解的还是需要了解的。

    面试造火箭可不是浪得虚名啊,但是鸡架可能还是需要使用到其中一些奇奇怪怪的黑科技的。

    好了胡扯结束了,今天的文章就到此为止了。

    原文链接:https://juejin.cn/post/7032625978023084062?utm_source=gold_browser_extension

    收起阅读 »

    你用过HandlerThread么?

    前言我们都用过Handler,也很熟悉怎么使用Handler,肯定也知道Handler使用过程中的注意事项,那就是内存泄漏,也知道大部分内存泄漏都是因为静态变量引用的问题。Handler是一个内部类,非static内部类或者匿名内部类都会持有外部类的引用。如果...
    继续阅读 »

    前言

    我们都用过Handler,也很熟悉怎么使用Handler,肯定也知道Handler使用过程中的注意事项,那就是内存泄漏,也知道大部分内存泄漏都是因为静态变量引用的问题。Handler是一个内部类,非static内部类或者匿名内部类都会持有外部类的引用。如果此时Activty退出了, handler持有他的引用,则这个Activity 并不会被销毁,其实还是在内存中,所以就造成了内存泄漏 (Memory Leak) 的问题。怎么解决这个问题,网上都有很成熟的文章和技术实现,这里不再累赘,这期主要讲下Handler的另一种使用方式HandlerThread。

    一、HandlerThread的本质

    HandlerThread 本质上就是一个普通Thread。

    Handler完成两个线程通信的代码中,需要调用Looper.prepare() 为一个线程开启一个消息循环,默认情况下Android中新诞生的线程是没有开启消息循环的。(主线程除外,主线程系统会自动为其创建Looper对象,开启消息循环。) Looper对象通过MessageQueue来存放消息和事件。一个线程只能有一个Looper,对应一个MessageQueue。 然后通过Looper.loop() 让Looper开始工作,从消息队列里取消息,处理消息。

    所以要使用Handler完成线程之间的通信,首先需要调用Looper.prepare() 为该线程开启消息循环,然后创建Handle,然后调用 Looper.loop() 开始工作。这都是很常规的流程。

    而HandlerThread 帮我们做好了这些事情,它内部建立了Looper。

    二、HandlerThread 用法

    public class OtherActivity extends AppCompatActivity {
    private static final String TAG = "OtherActivity";
    private Handler handler1;
    private Handler handler2;
    private HandlerThread handlerThread1;
    private HandlerThread handlerThread2;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_other);

    // 创建HandlerThread
    handlerThread1 = new HandlerThread("handle-thread-1");
    handlerThread2 = new HandlerThread("handle-thread-2");
    // 开启HandleThread
    handlerThread1.start();
    handlerThread2.start();

    handler1 = new Handler(handlerThread1.getLooper()) {
    @Override
    public void handleMessage(@NonNull Message msg) {
    super.handleMessage(msg);
    Log.i(TAG, "handleMessage: ThreadName = " + Thread.currentThread().getName()
    + " msg.what = " + msg.what);
    }
    };

    handler2 = new Handler(handlerThread2.getLooper()) {
    @Override
    public void handleMessage(@NonNull Message msg) {
    super.handleMessage(msg);
    Log.i(TAG, "handleMessage: ThreadName = " + Thread.currentThread().getName()
    + " msg.what = " + msg.what);
    }
    };

    handler2.sendEmptyMessage(2);
    handler1.sendEmptyMessage(5);
    }

    // 释放资源
    @Override
    protected void onDestroy() {
    super.onDestroy();
    handlerThread1.quit();
    handlerThread2.quitSafely();
    }
    }

    HandlerThread创建 Looper 并执行 loop() 的线程在任务结束的时候,需要手动调用 quit。否则,线程将由于 loop() 的轮询一直处于可运行状态,CPU 资源无法释放。更有可能因为 Thread 作为 GC Root 持有超出生命周期的实例引发内存泄漏。

    官方使用 quitSafely() 去终止 Looper,原因是其只会剔除执行时刻晚于 当前调用时刻 的 Message。这样可以保证 quitSafely 调用的那刻,满足执行时间条件的 Message 继续保留在队列中,在都执行完毕才退出轮询。

    那么主线程需要 quit 吗?其实不需要,在内存不足的时候 App 由 AMS 直接回收进程。因为主线程极为重要,承载着 ContentProvider、Activity、Service 等组件生命周期的管理,即便某个组件结束了,它仍有继续存在去调度其他组件的必要! 换言之,ActivityThread 的作用域超过了这些组件,不该由这些组件去处理它的结束。比如,Activity destroy 了,ActivityThread 仍然要处理其他 Activity 或 Service 等组件的事务,不能结束。

    HandlerThread 的在Android中的用处

    Android本身就是一个巨大的消息处理机,ActivityThread类是Android APP进程的初始类,它的main函数是这个APP进程的入口。APP进程中UI事件的执行代码段都是由ActivityThread提供的。也就是说,主线程实例是存在的,只是创建它的代码我们不可见。ActivityThread的main函数就是在这个主线程里被执行的。

    public final class ActivityThread {

    //...
    private static ActivityThread sCurrentActivityThread;
    public static ActivityThread currentActivityThread() {
    return sCurrentActivityThread;
    }
    private void attach(boolean system) {
    sCurrentActivityThread = this;
    //...
    }
    public static void main(String[] args) {
    //....

    // 创建Looper和MessageQueue对象,用于处理主线程的消息
    Looper.prepareMainLooper();

    // 创建ActivityThread对象
    ActivityThread thread = new ActivityThread();

    // 建立Binder通道 (创建新线程)
    thread.attach(false);

    Looper.loop(); //消息循环运行
    throw new RuntimeException("Main thread loop unexpectedly exited");
    }
    }

    Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施即可。

    总结

    管它呢,用就好了,封装好的东西干嘛不用了,可以减少我们开发过程中bug,提示开发效率,多好的一件事啊,用起来就对了,哈哈

    原文链接:https://juejin.cn/post/7032649435133771807?utm_source=gold_browser_extension

    收起阅读 »

    android 展示PDF文件

    PDF
     注:此方式展示pdf文件会增加apk大小3-4m左右 建议使用x5的webview进行加载pdf文件(可扩展) 1. 加入此依赖 implementation 'com.github.barteksc:android-pdf-viewer:3....
    继续阅读 »
    •  注:此方式展示pdf文件会增加apk大小3-4m左右 建议使用x5的webview进行加载pdf文件(可扩展)


    1. 加入此依赖



    implementation 'com.github.barteksc:android-pdf-viewer:3.2.0-beta.1'



    2. 简单介绍


    此篇文章主要还是将pdf文件进行下载到本sd目录下,之后转为file文件,交给pdfview进行展示,具体的展示pdf文件可进入pdfview源码中进行查看


    https://github.com/barteksc/AndroidPdfViewer

    3. 开始操作



    public class PDF2Activity extends AppCompatActivity {

    private PDFView pdfView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_p_d_f2);
    pdfView = findViewById(R.id.pdfView);

    download("xxx.pdf");

    }

    private void download(String url) {
    DownloadUtil.download(url, getCacheDir() + "/temp.pdf", new DownloadUtil.OnDownloadListener() {
    @Override
    public void onDownloadSuccess(final String path) {
    Log.d("MainActivity", "onDownloadSuccess: " + path);
    runOnUiThread(new Runnable() {
    @Override
    public void run() {
    preView(path);
    }
    });
    }

    @Override
    public void onDownloading(int progress) {
    Log.d("MainActivity", "onDownloading: " + progress);
    }

    @Override
    public void onDownloadFailed(String msg) {
    Log.d("MainActivity", "onDownloadFailed: " + msg);
    }
    });
    }

    private void preView(String path) {
    File file = new File(path);
    //这里只是作为一个file文件进行展示 还有其他的办法进行展示
    pdfView.fromFile(file)
    .enableSwipe(true) // allows to block changing pages using swipe
    .swipeHorizontal(false)
    .enableDoubletap(true)
    .defaultPage(0)
    // allows to draw something on the current page, usually visible in the middle of the screen
    .enableAnnotationRendering(false) // render annotations (such as comments, colors or forms)
    .password(null)
    .scrollHandle(null)
    .enableAntialiasing(true) // improve rendering a little bit on low-res screens
    // spacing between pages in dp. To define spacing color, set view background
    .spacing(0)
    .load();
    }


    3.1 okhttp



    implementation("com.squareup.okhttp3:okhttp:4.6.0")



    4.DownLoadUtils


    public class DownloadUtil {

    public static void download(final String url, final String saveFile, final OnDownloadListener listener) {
    Request request = new Request.Builder().url(url).build();
    new OkHttpClient().newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
    listener.onDownloadFailed(e.getMessage());
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
    InputStream is = null;
    byte[] buf = new byte[2048];
    int len;
    FileOutputStream fos = null;
    try {
    is = response.body().byteStream();
    long total = response.body().contentLength();
    File file = new File(saveFile);
    fos = new FileOutputStream(file);
    long sum = 0;
    while ((len = is.read(buf)) != -1) {
    fos.write(buf, 0, len);
    sum += len;
    int progress = (int) (sum * 1.0f / total * 100);
    listener.onDownloading(progress);
    }
    fos.flush();
    listener.onDownloadSuccess(file.getAbsolutePath());
    } catch (Exception e) {
    listener.onDownloadFailed(e.getMessage());
    } finally {
    try {
    if (is != null)
    is.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    try {
    if (fos != null)
    fos.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
    });
    }

    public interface OnDownloadListener {
    void onDownloadSuccess(String path);

    void onDownloading(int progress);

    void onDownloadFailed(String msg);
    }
    }

    作者:你的头上有犄角
    链接:https://juejin.cn/post/6915269769138552840
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    进来看看是不是你想要的效果,Android吸顶效果,并有着ViewPager左右切换

    老规矩,先上图,看看是不是你想要的 美团: 来一个图形分析 接下来我要写一个简单示例,先分析一下布局,见下图,最外层是NestedScrollView,之后嵌套一个LinearLayout头部,中间TabLayout选择器,底部一个ViewPager Vi...
    继续阅读 »

    老规矩,先上图,看看是不是你想要的


    美团:
    美团




    来一个图形分析


    接下来我要写一个简单示例,先分析一下布局,见下图,最外层是NestedScrollView,之后嵌套一个LinearLayout头部,中间TabLayout选择器,底部一个ViewPager
    ViewPager高度需要动态控制,看自己的需求了,如果是美团那种效果,就是
    ViewPager高度 = NestedScrollView高度 - TabLayout高度
    在这里插入图片描述




    话不多说,代码实现


    接下来我写一个例子,如果按照普通控件的嵌套方式来实现,那么肯定存在滑动冲突,会出现RecyclerView先进行滑动其次才是ScrollView滑动,那么就需要先重写NestedScrollView控件,用于控制最大的滑动距离,当达到最大滑动距离,再分发给RecyclerView滑动!




    NestedScrollView重写


    需要继承自NestedScrollView并重写onStartNestedScroll和onNestedPreScroll方法,如下


    package com.cyn.mt

    import android.content.Context
    import android.util.AttributeSet
    import android.view.View
    import androidx.core.view.NestedScrollingParent2
    import androidx.core.widget.NestedScrollView

    /**
    * @author cyn
    */

    class CoordinatorScrollview : NestedScrollView, NestedScrollingParent2 {
    private var maxScrollY = 0

    constructor(context: Context?) : super(context!!)
    constructor(context: Context?, attrs: AttributeSet?) : super(
    context!!,
    attrs
    )

    constructor(
    context: Context?,
    attrs: AttributeSet?,
    defStyleAttr: Int
    ) : super(context!!, attrs, defStyleAttr)

    override fun onStartNestedScroll(
    child: View,
    target: View,
    axes: Int,
    type: Int
    )
    : Boolean {
    return true
    }

    /**
    * 设置最大滑动距离
    *
    * @param maxScrollY 最大滑动距离
    */

    fun setMaxScrollY(maxScrollY: Int) {
    this.maxScrollY = maxScrollY
    }

    /**
    * @param target 触发嵌套滑动的View
    * @param dx 表示 View 本次 x 方向的滚动的总距离
    * @param dy 表示 View 本次 y 方向的滚动的总距离
    * @param consumed 表示父布局消费的水平和垂直距离
    * @param type 触发滑动事件的类型
    */

    override fun onNestedPreScroll(
    target: View,
    dx: Int,
    dy: Int,
    consumed: IntArray,
    type: Int
    )
    {
    if (dy > 0 && scrollY < maxScrollY) {
    scrollBy(0, dy)
    consumed[1] = dy
    }
    }
    }

    布局文件


    我按照美团的布局大体写出这样的布局

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <!--titleBar-->
    <LinearLayout
    android:id="@+id/titleBar"
    android:layout_width="match_parent"
    android:layout_height="45dp"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:paddingLeft="18dp"
    android:paddingRight="18dp">

    <EditText
    android:layout_width="0dp"
    android:layout_height="35dp"
    android:layout_marginEnd="12dp"
    android:layout_marginRight="12dp"
    android:layout_weight="1"
    android:background="@drawable/edit_style"
    android:paddingLeft="12dp"
    android:paddingRight="12dp" />

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="35dp"
    android:background="@drawable/button_style"
    android:gravity="center"
    android:paddingLeft="15dp"
    android:paddingRight="15dp"
    android:text="搜索"
    android:textColor="#333333"
    android:textStyle="bold" />

    </LinearLayout>

    <!--coordinatorScrollView-->
    <com.cyn.mt.CoordinatorScrollview
    android:id="@+id/coordinatorScrollView"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <!--相当于分析图中头部的LinearLayout,模拟动态添加的情况-->
    <LinearLayout
    android:id="@+id/titleLinerLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical" />

    <!--相当于分析图中红色标记处TabLayout-->
    <com.google.android.material.tabs.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

    <!--相当于分析图中绿色标记处ViewPager,代码中动态设置高度-->
    <androidx.viewpager.widget.ViewPager
    android:id="@+id/viewPager"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

    </LinearLayout>

    </com.cyn.mt.CoordinatorScrollview>

    </LinearLayout>



    Fragment


    加入,在Fragment中放入RecyclerView,提供给ViewPager使用,这里代码就不贴了,可以直接下源码!源码在文章末尾!




    主要代码(重点来了)


    coordinatorScrollView最大滑动距离即是titleLinerLayout的高度,所以实现titleLinerLayout的post方法,来监听titleLinerLayout的高度,由于这一块布局常常是通过网络请求后加载,所以,网络请求完毕后要再次实现post设置coordinatorScrollView最大滑动距离,如第80行代码和第90行代码,在这里,我并不推荐使用多次回调监听的方法!使用post只用调用一次,如果使用多次监听View变化的方法,应该在最后一次网络请求完毕后将此监听事件remove掉!


    package com.cyn.mt

    import android.content.res.Resources
    import android.os.Bundle
    import android.os.Handler
    import android.util.DisplayMetrics
    import android.view.LayoutInflater.from
    import android.view.View
    import androidx.appcompat.app.AppCompatActivity
    import androidx.fragment.app.Fragment
    import kotlinx.android.synthetic.main.activity_main.*
    import kotlinx.android.synthetic.main.title_layout.view.*


    class MainActivity : AppCompatActivity() {

    //屏幕宽
    var screenWidth = 0

    //屏幕高
    var screenHeight = 0

    //tabLayout的文本和图片
    private val tabTextData = arrayOf("常用药品", "夜间送药", "隐形眼镜", "成人用品", "医疗器械", "全部商家")
    private val tabIconData = arrayOf(
    R.mipmap.tab_icon,
    R.mipmap.tab_icon,
    R.mipmap.tab_icon,
    R.mipmap.tab_icon,
    R.mipmap.tab_icon,
    R.mipmap.tab_icon
    )
    private var fragmentData = mutableListOf()


    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    initView()
    initData()
    }

    private fun initView() {

    //获取屏幕宽高
    val resources: Resources = this.resources
    val dm: DisplayMetrics = resources.displayMetrics
    screenWidth = dm.widthPixels
    screenHeight = dm.heightPixels

    //状态栏沉浸
    StatusBarUtil.immersive(this)

    //titleBar填充
    StatusBarUtil.setPaddingSmart(this, titleBar)

    //状态栏字体颜色设置为黑色
    StatusBarUtil.darkMode(this)

    //动态设置ViewPager高度
    coordinatorScrollView.post {
    val layoutParams = viewPager.layoutParams
    layoutParams.width = screenWidth
    layoutParams.height = coordinatorScrollView.height - tabLayout.height
    viewPager.layoutParams = layoutParams
    }

    }

    private fun initData() {

    //我模拟在头部动态添加三个布局,就用图片代替了,要设置的图片高度都是我提前算好的,根据屏幕的比例来计算的
    val titleView1 = getTitleView(screenWidth * 0.42F, R.mipmap.title1)
    val titleView2 = getTitleView(screenWidth * 0.262F, R.mipmap.title2)
    titleLinerLayout.addView(titleView1)
    titleLinerLayout.addView(titleView2)

    //设置最大滑动距离
    titleLinerLayout.post {
    coordinatorScrollView.setMaxScrollY(titleLinerLayout.height)
    }

    //用于请求网络后动态添加子布局
    Handler().postDelayed({
    val titleView3 = getTitleView(screenWidth * 0.589F, R.mipmap.title3)
    titleLinerLayout.addView(titleView3)

    //再次设置最大滑动距离
    titleLinerLayout.post {
    coordinatorScrollView.setMaxScrollY(titleLinerLayout.height)
    }

    }, 200)

    //添加TabLayout
    for (i in tabTextData.indices) {
    tabLayout.addTab(tabLayout.newTab())
    tabLayout.getTabAt(i)!!.setText(tabTextData[i]).setIcon(tabIconData[i])

    //添加Fragment
    fragmentData.add(TestFragment.newInstance(tabTextData[i]))
    }

    //Fragment ViewPager
    viewPager.adapter = ViewPagerAdapter(supportFragmentManager, fragmentData)

    //TabLayout关联ViewPager
    tabLayout.setupWithViewPager(viewPager)

    //设置TabLayout数据
    for (i in tabTextData.indices) {
    tabLayout.getTabAt(i)!!.setText(tabTextData[i]).setIcon(tabIconData[i])
    }
    }

    /**
    * 获取一个title布局
    * 我这里就用三张图片模拟的
    *
    * @height 要设置的图片高度
    */

    private fun getTitleView(height: Float, res: Int): View {
    val inflate = from(this).inflate(R.layout.title_layout, null, false)
    val layoutParams = inflate.titleImage.layoutParams
    layoutParams.width = screenWidth
    layoutParams.height = height.toInt()
    inflate.titleImage.setImageResource(res)
    return inflate
    }
    }



    最终效果


    在这里插入图片描述
    至此结束!




    源码资源


    下面3个链接均可下载源码



    源码Github(推荐):github.com/ThirdGoddes…




    源码CodeChina:codechina.csdn.net/qq_40881680…




    源码下载:download.csdn.net/download/qq…


    作者:第三女神程忆难
    链接:https://juejin.cn/post/6950099129104072740
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    图解:什么是AVL树?

    引子上一次我给大家介绍了什么是二叉搜索树,但是由于二叉搜索树查询效率的不稳定性,所以很少运用在实际的场景中,所以我们伟大的前人就对二叉搜索树进行了改良,发明了AVL树。AVL树是一种自平衡二叉搜索树,因为AVL树任意节点的左右子树高度差的绝对值不超过1,所以A...
    继续阅读 »

    引子

    上一次我给大家介绍了什么是二叉搜索树,但是由于二叉搜索树查询效率的不稳定性,所以很少运用在实际的场景中,所以我们伟大的前人就对二叉搜索树进行了改良,发明了AVL树。

    AVL树是一种自平衡二叉搜索树,因为AVL树任意节点的左右子树高度差的绝对值不超过1,所以AVL树又被称为高度平衡树。

    AVL树本质上是一棵带有平衡条件的二叉搜索树,它满足二叉搜索树的基本特性,所以本次主要介绍AVL树怎么自平衡,也就是理解它的旋转过程。

    二叉搜索树特性忘了的小伙伴可以看之前的文章:搞定二叉搜索树,9图足矣!同时我也将基本性质给大家再回顾一遍:

    1. 若它的左子树不为空,则左子树上所有节点的值均小于根节点的值。

    2. 若它的右子树不为空,则右子树上所有节点的值均大于根节点的值。

    3. 它的左、右子树也分别为二叉搜索树。

    平衡条件:每个节点的左右子树的高度差的绝对值不超过1。

    我们将每个节点的左右子树的高度差的绝对值又叫做平衡因子。

    AVL树的旋转行为一般是在插入和删除过程中才发生的,因为插入过程中的旋转相比于删除过程的旋转而言更加简单和直观,所以我给大家图解一下AVL树的插入过程。

    插入过程

    最开始的时候为空树,没有任何节点,所以我们直接用数据构造一个节点插入就好了,比如第一个要插入的数据为18。

    第一个节点插入完成,开始插入第二个节点,假如数据为20。

    插入第三个节点数据为14。

    第四个节点数据为16。从根节点位置开始比较并寻找16的对应插入位置。

    第五个要插入的数据为12。还是一样,从树的根节点出发,根据二叉搜索树的特性向下寻找到对应的位置。

    此时插入一个数据11,根据搜索树的性质,我们不难找到它的对应插入位置,但是当我们插入11这个节点之后就不满足AVL树的平衡条件了。

    此时相当于18的左子树高了,右子树矮了,所以我们应该进行一次右单旋,右单旋使左子树被提起来,右子树被拉下去,相当于左子树变矮了,右子树变高了,所以一次旋转之后,又满足平衡条件了。

    简单分析上图的旋转过程:**因为左子树被提上去了,所以14成为了新的根节点,而18被拉到了14右子树的位置,又因为14这个节点原来有右子节点为16,所以18与16旋转之后的位置就冲突了,但是因为16小于18,**所以这个时候根据二叉搜索树的特性,将16调整到18的左子树中去,因为旋转之后的18这个节点的左子树是没有节点的,所以16可以直接挂到18的左边,如果18的左子树有节点,那么还需要根据二叉搜索树的性质去将16与18左子树中的节点比较大小,直到确定新的位置。

    经过上面的分析我们可以知道:如果新插入的节点插入到根节点较高左子树的左侧,则需要进行一次右单旋,我们一般将这种情况简单记为左左情况,第一个左说的是较高左子树的左,第二个左说的是新节点插入到较高左子树的左侧。

    分析完了左左的情况,我想小伙伴们不难推出右右的情况(第一个右说的是较高右子树的右,第二个右说的是新节点插入到较高右子树的右侧),就是一次左单旋,这里就不一步一步地分析右右的情况了,因为它和左左是对称的。给大家画个图,聪明的你一眼就可以学会!

    现在两种单旋的情况已经讲完了,分别是左左和右右,还剩下两种单旋的情况,不过别慌,因为双旋比你想象中的简单,而且同样,双旋也是两种对称的情况,实际上我们只剩下一种情况需要分析了,所以,加油,弄懂了的话,面试的时候就完全不用慌了!

    双旋

    我们假设当前的AVL树为下图。

    这个时候我们新插入一个节点,数据为15,根据搜索树的性质,我们找到15对应的位置并插入,如图

    我们此时再次计算每个节点的平衡因子,发现根节点18的平衡因子为2,超过了1,不满足平衡条件,所以需要对他进行旋转。

    我们将刚才需要进行右单旋的左左情况和现在的这种情况放在一起对比一下,聪明的你一定发现,当前的情况相比于左左的情况只是插入的位置不同而已,左左情况插入的节点在根节点18较高左子树的左侧,而当前这种情况插入节点是在根节点18较高左子树的右侧,我们将它称为左右情况。

    那么可能正看到这里的你可能不禁会想:这不跟刚才左左差不多嘛,直接右单旋不就完事了。真的是这样吗?让我们来一次右单旋看看再说。

    简单分析该右单旋:**节点14上提变成新的根节点,18下拉变成根节点的右子树,又因为当前根节点14原来有右子树16,所以18与16位置冲突,**比较18与16大小之后,发现18大于16,根据搜索树的性质,将以16为根节点的子树调整到18的左子树,因为18的左子树目前为空,所以以16为根的子树直接挂在18的左侧,若18的左子树不为空,则需要根据搜索树的性质继续进行比较,直到找到合适的挂载位置。

    既然一次右单旋不行,那么我们应该怎么办呢?答案就是进行一次双旋,一次双旋可以拆分成两次单旋,对于当前这种不平衡条件,我们可以先进行一次左单旋,再进行一次右单旋,之后就可以将树调整成满足平衡条件的AVL树了,话不多说,图解一下。

    简单分析左右双旋先对虚线框内的子树进行左单旋,则16上提变成子树的新根,以14为根节点的子树下拉,调整到16的左子树,此时发现16的左子树为15,与14这棵子树冲突,所以根据搜索树规则进行调整,将15挂载到以14为根节点子树的右子树,从而完成一次左单旋,之后再对整棵树进行一次右单旋,节点16上提成为新的根节点,18下拉变成根节点的右子树,因为之前16没有右子树,所以以18为根节点的子树直接挂载到16的右子树,从而完成右旋。

    同样,对于左右情况的对称情况右左情况我就不给大家分析了,还是将图解送给大家,相信聪明的你一看就会!

    到此为止,我将AVL树的四种旋转情况都给大家介绍了一遍,仔细想想,其实不止这四种情况需要旋转,严格意义上来说有八种情况需要旋转,比如之前介绍的左左情况吧,我们说左左就是将新的节点插入到了根节点较高左子树的左侧,这个左侧其实细分一下又有两种情况,只不过这两种情况实际可以合成一种情况来看,也就是新的节点插入到左侧的时候可以成为它父亲节点的左孩子,也可以成为它父亲节点的右孩子,那么这样的话就是相当于两种情况了,简单画个图看一下吧。

    就是这样上图这样,每个新插入的节点都可以是它父亲节点的左孩子或者右孩子,这取决于新插入数据的大小,比如11就是12的左孩子,13就是12的右孩子,这两种情况都属于左左情况,也就是说他们本质上是一样的,都插在了节点18较高左子树的左侧。

    那么这样看来这四种旋转情况严格上看都可以多分出一种情况,变成八种情况。

    后话

    emmm…这样看来AVL树确实解决了二叉搜索树可能不平衡的缺陷,补足了性能上不稳定的缺陷,但是细细想来AVL树的效率其实不是很好,这里说的不是查询效率,而是插入与删除效率,上面所说的这四大种八小种情况还是很容易命中的,那么这样的话就需要花费大量的时间去进行旋转调整,我的天,这样也太难搞了!

    不过聪明的前人早就为我们想好了更加利于实际用途的搜索树,在现实场景中AVL树和二叉搜索树一样,基本上用不到,我们接下来要讲的这种二叉类的搜索树才是我们经常应用的,相信见多识广的你一定猜到了它的名字,对,就是它,大名鼎鼎的红黑树!我们下次来盘他!

    鄙人才疏学浅,若有任何差错,还望各位海涵,不吝指教!

    喜欢本文的少侠们,欢迎关注公众号雷子的编程江湖,修炼更多武林秘籍。

    一键三连是中华民族的当代美德!


    作者:雷子的编程江湖
    链接:https://juejin.cn/post/6886103714818424846
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    恕我直言,我怀疑你并不会用 Java 枚举

    开门见山地说吧,enum(枚举)是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,默认继承自 java.lang.Enum。 为了证明这一点,我们来新建一个枚举 PlayerType: public enum PlayerType { TE...
    继续阅读 »

    开门见山地说吧,enum(枚举)是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,默认继承自 java.lang.Enum。


    为了证明这一点,我们来新建一个枚举 PlayerType:


    public enum PlayerType {
    TENNIS,
    FOOTBALL,
    BASKETBALL
    }

    两个关键字带一个类名,还有大括号,以及三个大写的单词,但没看到继承 Enum 类啊?别着急,心急吃不了热豆腐啊。使用 JAD 查看一下反编译后的字节码,就一清二楚了。


    public final class PlayerType extends Enum
    {

    public static PlayerType[] values()
    {
    return (PlayerType[])$VALUES.clone();
    }

    public static PlayerType valueOf(String name)
    {
    return (PlayerType)Enum.valueOf(com/cmower/baeldung/enum1/PlayerType, name);
    }

    private PlayerType(String s, int i)
    {
    super(s, i);
    }

    public static final PlayerType TENNIS;
    public static final PlayerType FOOTBALL;
    public static final PlayerType BASKETBALL;
    private static final PlayerType $VALUES[];

    static
    {
    TENNIS = new PlayerType("TENNIS", 0);
    FOOTBALL = new PlayerType("FOOTBALL", 1);
    BASKETBALL = new PlayerType("BASKETBALL", 2);
    $VALUES = (new PlayerType[] {
    TENNIS, FOOTBALL, BASKETBALL
    });
    }
    }

    看到没?PlayerType 类是 final 的,并且继承自 Enum 类。这些工作我们程序员没做,编译器帮我们悄悄地做了。此外,它还附带几个有用静态方法,比如说 values()valueOf(String name)


    01、内部枚举


    好的,小伙伴们应该已经清楚枚举长什么样子了吧?既然枚举是一种特殊的类,那它其实是可以定义在一个类的内部的,这样它的作用域就可以限定于这个外部类中使用。


    public class Player {
    private PlayerType type;
    public enum PlayerType {
    TENNIS,
    FOOTBALL,
    BASKETBALL
    }

    public boolean isBasketballPlayer() {
    return getType() == PlayerType.BASKETBALL;
    }

    public PlayerType getType() {
    return type;
    }

    public void setType(PlayerType type) {
    this.type = type;
    }
    }

    PlayerType 就相当于 Player 的内部类,isBasketballPlayer() 方法用来判断运动员是否是一个篮球运动员。


    由于枚举是 final 的,可以确保在 Java 虚拟机中仅有一个常量对象(可以参照反编译后的静态代码块「static 关键字带大括号的那部分代码」),所以我们可以很安全地使用“==”运算符来比较两个枚举是否相等,参照 isBasketballPlayer() 方法。


    那为什么不使用 equals() 方法判断呢?


    if(player.getType().equals(Player.PlayerType.BASKETBALL)){};
    if(player.getType() == Player.PlayerType.BASKETBALL){};

    “==”运算符比较的时候,如果两个对象都为 null,并不会发生 NullPointerException,而 equals() 方法则会。


    另外, “==”运算符会在编译时进行检查,如果两侧的类型不匹配,会提示错误,而 equals() 方法则不会。



    02、枚举可用于 switch 语句


    这个我在之前的一篇我去的文章中详细地说明过了,感兴趣的小伙伴可以点击链接跳转过去看一下。


    switch (playerType) {
    case TENNIS:
    return "网球运动员费德勒";
    case FOOTBALL:
    return "足球运动员C罗";
    case BASKETBALL:
    return "篮球运动员詹姆斯";
    case UNKNOWN:
    throw new IllegalArgumentException("未知");
    default:
    throw new IllegalArgumentException(
    "运动员类型: " + playerType);

    }

    03、枚举可以有构造方法


    如果枚举中需要包含更多信息的话,可以为其添加一些字段,比如下面示例中的 name,此时需要为枚举添加一个带参的构造方法,这样就可以在定义枚举时添加对应的名称了。


    public enum PlayerType {
    TENNIS("网球"),
    FOOTBALL("足球"),
    BASKETBALL("篮球");

    private String name;

    PlayerType(String name) {
    this.name = name;
    }
    }

    04、EnumSet


    EnumSet 是一个专门针对枚举类型的 Set 接口的实现类,它是处理枚举类型数据的一把利器,非常高效(内部实现是位向量,我也搞不懂)。


    因为 EnumSet 是一个抽象类,所以创建 EnumSet 时不能使用 new 关键字。不过,EnumSet 提供了很多有用的静态工厂方法:



    下面的示例中使用 noneOf() 创建了一个空的 PlayerType 的 EnumSet;使用 allOf() 创建了一个包含所有 PlayerType 的 EnumSet。


    public class EnumSetTest {
    public enum PlayerType {
    TENNIS,
    FOOTBALL,
    BASKETBALL
    }

    public static void main(String[] args) {
    EnumSet<PlayerType> enumSetNone = EnumSet.noneOf(PlayerType.class);
    System.out.println(enumSetNone);

    EnumSet<PlayerType> enumSetAll = EnumSet.allOf(PlayerType.class);
    System.out.println(enumSetAll);
    }
    }

    程序输出结果如下所示:


    []
    [TENNIS, FOOTBALL, BASKETBALL]

    有了 EnumSet 后,就可以使用 Set 的一些方法了:



    05、EnumMap


    EnumMap 是一个专门针对枚举类型的 Map 接口的实现类,它可以将枚举常量作为键来使用。EnumMap 的效率比 HashMap 还要高,可以直接通过数组下标(枚举的 ordinal 值)访问到元素。


    和 EnumSet 不同,EnumMap 不是一个抽象类,所以创建 EnumMap 时可以使用 new 关键字:


    EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);

    有了 EnumMap 对象后就可以使用 Map 的一些方法了:



    和 HashMap 的使用方法大致相同,来看下面的例子:


    EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);
    enumMap.put(PlayerType.BASKETBALL,"篮球运动员");
    enumMap.put(PlayerType.FOOTBALL,"足球运动员");
    enumMap.put(PlayerType.TENNIS,"网球运动员");
    System.out.println(enumMap);

    System.out.println(enumMap.get(PlayerType.BASKETBALL));
    System.out.println(enumMap.containsKey(PlayerType.BASKETBALL));
    System.out.println(enumMap.remove(PlayerType.BASKETBALL));

    程序输出结果如下所示:


    {TENNIS=网球运动员, FOOTBALL=足球运动员, BASKETBALL=篮球运动员}
    篮球运动员
    true
    篮球运动员

    06、单例


    通常情况下,实现一个单例并非易事,不信,来看下面这段代码


    public class Singleton {  
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
    if (singleton == null) {
    synchronized (Singleton.class) {
    if (singleton == null) {
    singleton = new Singleton();
    }
    }
    }
    return singleton;
    }
    }

    但枚举的出现,让代码量减少到极致:


    public enum EasySingleton{
    INSTANCE;
    }

    完事了,真的超级短,有没有?枚举默认实现了 Serializable 接口,因此 Java 虚拟机可以保证该类为单例,这与传统的实现方式不大相同。传统方式中,我们必须确保单例在反序列化期间不能创建任何新实例。


    07、枚举可与数据库交互


    我们可以配合 Mybatis 将数据库字段转换为枚举类型。现在假设有一个数据库字段 check_type 的类型如下:


    `check_type` int(1) DEFAULT NULL COMMENT '检查类型(1:未通过、2:通过)',

    它对应的枚举类型为 CheckType,代码如下:


    public enum CheckType {
    NO_PASS(0, "未通过"), PASS(1, "通过");
    private int key;

    private String text;

    private CheckType(int key, String text) {
    this.key = key;
    this.text = text;
    }

    public int getKey() {
    return key;
    }

    public String getText() {
    return text;
    }

    private static HashMap<Integer,CheckType> map = new HashMap<Integer,CheckType>();
    static {
    for(CheckType d : CheckType.values()){
    map.put(d.key, d);
    }
    }

    public static CheckType parse(Integer index) {
    if(map.containsKey(index)){
    return map.get(index);
    }
    return null;
    }
    }

    1)CheckType 添加了构造方法,还有两个字段,key 为 int 型,text 为 String 型。


    2)CheckType 中有一个public static CheckType parse(Integer index)方法,可将一个 Integer 通过 key 的匹配转化为枚举类型。


    那么现在,我们可以在 Mybatis 的配置文件中使用 typeHandler 将数据库字段转化为枚举类型。


    <resultMap id="CheckLog" type="com.entity.CheckLog">
    <id property="id" column="id"/>
    <result property="checkType" column="check_type" typeHandler="com.CheckTypeHandler"></result>
    </resultMap>

    其中 checkType 字段对应的类如下:


    public class CheckLog implements Serializable {

    private String id;
    private CheckType checkType;

    public String getId() {
    return id;
    }

    public void setId(String id) {
    this.id = id;
    }

    public CheckType getCheckType() {
    return checkType;
    }

    public void setCheckType(CheckType checkType) {
    this.checkType = checkType;
    }
    }

    CheckTypeHandler 转换器的类源码如下:


    public class CheckTypeHandler extends BaseTypeHandler<CheckType> {

    @Override
    public CheckType getNullableResult(ResultSet rs, String index) throws SQLException {
    return CheckType.parse(rs.getInt(index));
    }

    @Override
    public CheckType getNullableResult(ResultSet rs, int index) throws SQLException {
    return CheckType.parse(rs.getInt(index));
    }

    @Override
    public CheckType getNullableResult(CallableStatement cs, int index) throws SQLException {
    return CheckType.parse(cs.getInt(index));
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int index, CheckType val, JdbcType arg3) throws SQLException {
    ps.setInt(index, val.getKey());
    }
    }

    CheckTypeHandler 的核心功能就是调用 CheckType 枚举类的 parse() 方法对数据库字段进行转换。



    恕我直言,这篇文章看完后,我觉得小伙伴们肯定会用 Java 枚举了,如果还不会,就过来砍我!


    作者:沉默王二
    链接:https://juejin.cn/post/6844904182948823053
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    程序员如何优雅的挣零花钱?

    前言虽然程序员有女朋友的不多(误),但是开销往往都不小。 VPS、域名、Mac上那一堆的收费软件、还有Apple每年更新的那些设备,经常都是肾不够用的节奏。 幸好作为程序员,我们有更多挣钱的姿势。 有同学该嚷了:不就是做私单嘛。 ...
    继续阅读 »

    前言

    虽然程序员有女朋友的不多(误),但是开销往往都不小。


    VPS、域名、Mac上那一堆的收费软件、还有Apple每年更新的那些设备,经常都是肾不够用的节奏。


    幸好作为程序员,我们有更多挣钱的姿势。


    有同学该嚷了:不就是做私单嘛。


    对,但是也不太对。做私单的确是一个简单直接方式,但在我看来,私单的投入产出比很差,并不是最优的。


    但既然提到了,就先说说吧。

    关于

    本文作者:easychen 


    GitHub地址:https://github.com/easychen




    私单


    远程外包


    最理想的单子还是直接接海外的项目,比如freelance.com等网站。一方面是因为挣的是美刀比较划算,之前看到像给WordPress写支付+发送注册码这种大家一个周末就能做完的项目,也可以到200~300美刀;另一方面是在国外接单子比较隐蔽。


    常用国外网站:



    (由ahui132同学补充)



    本段由tvvocold同学贡献。国内也有一个软件众包平台 CODING 码市 。 码市基于云计算技术搭建的云端软件开发平台 Coding.net 作为沟通和监管工具,快速连接开发者与需求方,旨在通过云端众包的方式提高软件交付的效率。码市作为第三方监管平台,会对所有项目进行审核以保证项目需求的明确性,并提供付款担保,让开发者只要按时完成项目开发即可获取酬劳。你可以 在这里 看到开发者对码市的评价。



    当然,猪八戒这种站我就不多说了,不太适合专业程序员去自贬身价。


    按需雇用


    按需雇用是近几年新兴的私单方式,开发者在业余时间直接到雇主公司驻场办公,按时薪领取报酬。这种方式省去了网络沟通的低效率,也避免了和雇主的讨价还价,适合怕麻烦的程序员。


    拉勾大鲲





    大鲲 由拉勾网推出,考虑到拉勾上三十多万的招聘方,大鲲不缺雇主,这是其他独立平台相对弱势的地方。


    实现网





    实现网的价格也很不错,但是我强烈建议大家不要在介绍中透漏实名和真实的公司部门信息,因为这实在太高调了。有同学说,这是我的周末时间啊,我爱怎么用就怎么用,公司还能告我怎么的? 虽然很多公司的劳动合同里边并不禁止做兼职,但在网上如此高调的干私活,简直就是在挑衅HR:「我工作不饱和」、「公司加班不够多」… 再想象下你一边和产品经理说这个需求做不完,一边自己却有时间做私单的样子。你自己要是老板也不愿提拔这样的人吧。


    (我这几天重新去看了下,人才页面已经不再显示姓名了,只用使用头像。这样只要在工作经历介绍里边注意一点,就可以避免上述问题了。)


    程序员客栈





    不太熟悉,但国内按需雇用的网站不多,写出来供大家参考。


    Side Project


    比起做私单,做一个Side Project会更划算。


    Side Project的好处是你只需要对特定领域进行持续投入,就可以在很长时间获得收入。这可以让你的知识都在一棵树上分支生长,从而形成良好的知识结构,而不是变成一瓶外包万金油。


    思路有两种:


    一种是做小而美的,针对一个细分领域开发一个功能型应用,然后放到市场上去卖;


    另一种是做大而全的基础应用(想想WordPress),方便别人在上边直接添加代码,定制成自己想要的应用。


    前一种做起来比较快,但需要自己去做一些销售工作;后一种通常是开源/免费+收费模式,推广起来更简单。


    有同学会说,我写的 Side Project 就是卖不掉啊。项目方向的选取的确是比较有技巧的,但简单粗暴的解决方案就是找一个现在卖得非常好、但是产品和技术却不怎样的项目,做一个只要一半价格的竞品。


    比如 Mac 下有一个非常有名的写作软件,叫 Ulysses 。我试用了一下非常不错,但就是贵,283 RMB。后来看到了 Mweb ,光是免费的 Lite 版覆盖了 Ulysses 的主功能,完整版也才98RMB,几乎没有思考就买下来了。


    做咨询


    专家平台


    如果你在技术圈子里边小有名气,或者在某一个业务上特别精通,那么通过做咨询来挣钱是一种更轻松的方式。和人在咖啡厅聊几个小时,几百上千块钱就到手了。


    国内这方面的产品,我知道的有下边几个:




    • 在行: 这个是果壳旗下的,做得比较早,内容是全行业的,所以上边技术向的反而不多。




    • 缘创派: 缘创派的轻合伙栏目,主要面向创业者,适合喜欢感受创业氛围的技术专家们。




    • 极牛: 你可以认为这是一个程序员版本的「在行」,我浏览了下,虽然被约次数比在行要低不少,但专业性比较强,期望他们能尽快的推广开来吧。




    • 知加:这个项目是我参与的,面向程序员,类似「分答」的付费语音问答,刚开始内测,上边有一些硅谷科技公司的同学。感兴趣的可以看看。




    做咨询虽然也是实名的,但和私活是完全不同的。咨询的时间短,不会影响到正常的休息,更不会影响上班;而且大部分公司是鼓励技术交流的,所以大家的接受度都很高。


    付费社群


    除了APP外,我觉得收费群也是可以做的。比如可以搞一个技术创业群,找一些创业成功的同学、做投资的同学、做法务的同学,面向想创业的同学开放,每人收个几百块的年费。然后你在创业过程中遇到的问题,都可以有人解答,不会觉得是孤零零的一个人。如果遇到了问题,群里的人可以解答;如果没遇到问题,那不是更好么。有种卖保险的感觉,哈哈哈。


    比较好用的工具是 知识星球 也就是之前的小密圈。这个工具比较适合交流和讨论,长文比较痛苦。可以发布到其他地方,然后粘贴回来。





    另一个靠谱的工具大概是微博的 V+ 会员。说它靠谱主要是它在微博上,所以等于整合了 「内容分发」→ 「新粉丝获取」 → 「付费用户转化」 的整个流程。


    PS:交流型付费社群的一个比较难处理的事情是,很难平衡免费的粉丝和付费的社群之间的关系,所以我最后的选择是付费类的提供整块的内容,比如整理成册的小书、录制的实战视频等;而日常零碎的资料分享还是放到微博这种公开免费的平台。


    写文章


    投稿


    很多同学喜欢写技术博客,其实把文章投给一些网站是有稿费的。比如InfoQ,他们家喜欢收3000~4000字的深度技术文章;稿费是千字150。虽然不算太多,但一篇长文的稿费也够买个入门级的Cherry键盘了。我喜欢InfoQ的地方是他们的版权要求上比较宽松。文章在他们网站发布后,你可以再发布到自己博客的;而且文章可以用于出书,只要标明原发于InfoQ即可。


    更详细的说明见这里:http://www.infoq.com/cn/article-guidelines



    微博的@rambone同学补充到,文章还可以发到微博、微信、简书等支持打赏的平台。考虑到简书CEO及其官博对程序员的奇葩态度,个人建议是换个咱程序员自己的平台写文章。



    出版


    顺便说一句,比起写文章,其实通过传统发行渠道出书并不怎么挣钱,我之前到手的版税是8%,如果通过网络等渠道销售,数字会更低。出电子书收益会好一些,我之前写过一篇文章专门介绍:《如何通过互联网出版一本小书》


    以前一直写图文为主的书,用Markdown非常不错;但最近开始写技术教程类的书,发现Markdown不太够用了,最主要的问题有 ① 不支持视频标签,尤其是本地视频方案 ② 代码高亮什么的很麻烦 ③ 也没有footer note、文内说明区域的预置。


    这里要向大家严重推荐Asciidoc,你可以把它看成一个增强版的Markdown,预置了非常多的常用格式,而且GitBook直接就支持这个格式(只要把.md 搞成 .adoc 就好),Atom也有实时预览插件。用了一段时间,非常喜欢。


    付费文集


    最近一年有不少的付费文集产品出现,可以把它看成传统出版的一个网络版。一般是写作十篇以内的系列文章,定价为传统计算机书的一半到三分之一。付费文集产品通常是独家授权,所以在选择平台方面一定要慎重,不然一个好作品可能就坑掉了。


    掘金小册





    小册是由掘金推出的付费文集产品。我是小册的第一批作者,一路用下来还是很不错的。文章格式直接采用 Markdown , 发布以后可以实时更新,保证内容的新鲜度,非常的方便。小册的一般定价在19~29,通用内容销量好的能过千,细分内容基本也能过百。挣零花钱的话,是个非常不错的选择。


    达人课





    达人课是 GitChat 旗下的付费文集产品,现在应该已经合并到 CSDN 了。GitChat 的用户群不算大,但付费意愿还可以,大概因为内容就没有免费的🤣。之前我上课的时候是提交完成以后的文档给编辑,由编辑同学手动上架。感觉比较麻烦,尤其是修改错别字什么的。


    小专栏





    这个平台不熟……写到这里仅供参考。


    教学视频



    微博的@瓜瓜射门啦同学给了自己应聘程序教学网站讲师的经验:应聘程序教学网站讲师,出视频+作业教程,平台按小时支付,这个不知道算不算挣零花钱,我算了一下去年,一年大概出 20 个小时视频,拿到手是不到 6 万的样子,平时就是周末花时间弄下。



    在线教育起来以后,录制教学视频也可以赚钱了。关于录制在线课程的收益,一直不为广大程序员所知。但最近和51CTO学院 和 网易云课堂 的同学聊天,才发现一个优秀的40~60节的微专业课程,一年的收益比得上一线城市高级总监的收入。难怪最近做培训的人这么多😂


    渠道和分成


    大部分的平台合同有保密协议,所以不能对外讲。但网易云课堂和Udemy在公开的讲师注册协议中写明了分成,所以这里说一下。


    网易云课堂


    网易的课分三类:




    • 独立上架:等于网易提供平台(视频上传管理、用户管理、支付系统等),由你自己来负责营销。这个分成比例在 9:1 ,平台收取 10% 的技术服务费。我觉得非常划算。




    • 精品课:由网易帮你推广,但需要和他们签订独立的合同,会收取更多的分成。最麻烦的是,通常是独家授权。一旦签署了,就不能在其他平台上架课程了。




    • 微专业:这个是网易自己规划的课程体系,从课程的策划阶段就需要和他们深度沟通。也是网易推广力度最大、收益最大的一类课程。




    方糖全栈课就放在网易平台上,觉得好的地方如下:




    • 支付渠道相对全,还支持花呗,这样对于我这种高价课就非常重要。苹果应用内购买课程会渠道费用会被苹果扣掉30%,好想关掉 🤣




    • 自带推广系统,愿意的话可以用来做课程代理系统。




    Udemy


    相比之下 Udemy 就很贵了,分成是 5:5 ;支付上国内用户只能通过信用卡或者银行卡绑 paypal 支付。但可以把课程推向全球。(但我英文还不能讲课🙃)


    腾讯课堂没用过,欢迎熟悉的同学 PR 。


    小课和大课


    我个人喜欢把视频分成小课和大课两种。因为视频虽然看起来时间短,但实际上要做好的话,背后要消耗的时间、要投入精力还是非常多的。大课动不动就是几十上百个课时,绝大部分上班的程序员都没有时间来录制。所以挣零花钱更适合做小课,这种课一般就几个小时,剪辑成 10 个左右的小课时,价格在几十百来块。如果是自己专业纯熟的领域,一个长假就可以搞定。


    表现形式


    在课程的表现形式上,我个人更喜欢designcode.io这种图文+视频的模式,一方面是学习者可以快速的翻阅迅速跳过自己已经学会的知识;另一方面,会多出来 微博头条文章、微信公众号、知乎和简书专栏这些长文推广渠道。





    当然,内容本身才是最核心的。现在那么多的免费视频,为什么要来买你的收费版?


    其实现在绝大部分教学视频,往往都真的只是教学,和现实世界我们遇到的问题截然不同。里边都是一堆简化后的假项目,为了教学而刻意设计的。


    这里和大家分享一个我之前想操作的想法。


    就是在自己决定开始做一个开源项目后,用录屏软件把整个过程完完整整的录下来。开源的屏幕录制工具OBS,1920*1080的屏幕录成FLV格式,一个小时只需要1G,一个T的移动硬盘可以录制上千小时,对一个中型项目来说也足够了。


    等项目做完,就开源放到GitHub,让大家先用起来。等迭代稳定后,再从录制的全量视频中剪辑出一系列的教程,整理出一系列的文章,放到网站上做收费课程。


    这样做有几个好处:




    • 保证所有遇到的问题都是真实的,不是想象出来的,学习过这个课程的人,可以独立的将整个项目完整的实现。




    • 没有特意的录制过程,所以教程其实是软件开发的副产品,投入产出比更高。




    • 如果你的软件的确写得好,那么用过你软件的人可以成为你的客户或者推荐员。




    后续


    今年我录制方糖全栈课的时候就采用了上边这个思路,效果还不错,不过有几个小问题:




    • 连续性。录着视频写代码总会有一种潜在焦虑,平时经常写一会儿休息一会儿,录像时就会留下大段的空白,有点浪费空间。当然这个主要是心理上的。




    • 录音。录音的问题更大一些。因为一个长期项目很难一直处于一个安静的环境,另外基础课录制可能需要大量的讲解,几个小时写下来嗓子哑了 🤣 。最后的解决方式是剪辑的时候重新配音,不过需要注意音画同步问题。




    软件


    如果是没有太多界面切换的课程,那可以使用keynote自带的录音。在其他环境里边的演示的视频可以直接粘贴到keynote里面来播放。


    但是当你有很多的外部界面的时候,就需要录屏了。mac上可以直接用quicktime来录制。文件,新建里边选 record screen就好。


    我录全栈课的时候,因为会在三个操作系统上录一些界面,所以我选择了obs。虽然这个工具主打的是直播,但实际上它的录制功能也还是挺不错的。


    剪辑的话,用mac的imovie基本就够了,主要用到的功能就是分割片段,然后把不要的删掉。音频去下底噪。部分等待时间过长的片段加速下。当然adobe家的也行,就是贵。


    硬件


    硬件上边,最好买一个用来支撑话筒的支架。不要用手直接握着话筒来录,这样就不会有电流声(或者很小)。外接声卡我用的是 XOX , 在 Mac 下边效果挺好,但不知道为啥在 Windows 上回声比较大(当然也可能是系统设置的原因)。


    内部推荐和猎头推荐


    如果你在BAT等一流互联网公司工作,如果你有一帮志同道合的程序员朋友,那么基本上每隔几个月你应该就会遇到有想换工作的同事和朋友,所以千万别错过你挣推荐费的大好时机。


    一般来讲,公司内部推荐的钱会少一些,我见过的3000~6000的居多。但因为是自己公司,会比较靠谱,所以风险小一些。经常给公司推荐人才,还会提升老大多你的好感度,能优先就优先吧。


    比起内部推荐,猎头推荐的推荐费则会多很多。一个30万年薪的程序员,成功入职后差不多可以拿到1万RMB的推荐费。但猎头渠道的问题在于对简历质量要求非常高,有知名公司背景的才容易成单;回款周期又特别长,一般要入职过了试用期以后才能拿到全部推荐费,得小半年。


    小结


    学会挣钱是一件非常重要的事情,它会让你了解商业是如何运作的,帮助你理解公司的产品逻辑、以及为你可能的技术创业打下坚实的基础。


    所以我鼓励大家多去挣零花钱,最好各种姿势都都试试,会有意想不到的乐趣。如果你有更好的挣零花钱技能,欢迎发PR过来,我会挑不错的合并进去 :)


    作者:巧摄ONE里
    链接:https://juejin.cn/post/6844903597700808711
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    协程调度器详解

    协程和线程的差异目的差异线程的目的是提高CPU资源使用率, 使多个任务得以并行的运行,是为了服务于机器的.协程的目的是为了让多个任务之间更好的协作,主要体现在代码逻辑上,是为了服务开发者 (能提升资源的利用率, 但并不是原始目的)调度差异线程的调度是系统完成的...
    继续阅读 »

    协程和线程的差异

    目的差异

    • 线程的目的是提高CPU资源使用率, 使多个任务得以并行的运行,是为了服务于机器的.
    • 协程的目的是为了让多个任务之间更好的协作,主要体现在代码逻辑上,是为了服务开发者 (能提升资源的利用率, 但并不是原始目的)

    调度差异

    • 线程的调度是系统完成的,一般是抢占式的,根据优先级来分配
    • 协程的调度是开发者根据程序逻辑指定好的,在不同的时期把资源合理的分配给不同的任务.

    协程与线程的关系

    协程并不是取代线程,而且抽象于线程之上,线程是被分割的CPU资源,协程是组织好的代码流程,协程需要线程来承载运行,线程是协程的资源

    协程的核心竞争力

    简化异步并发任务。

    协程上下文 CoroutineContext

    • 协程总是运行在一些以 CoroutineContext 类型为代表的上下文中 ,协程上下文是各种不同元素的集合
    • 集合内部的元素Element是根据key去对应(Map特点),但是不允许重复(Set特点)
    • Element之间可以通过+号进行组合
    • Element有如下四类,共同组成了CoroutineContext
      • Job:协程的唯一标识,用来控制协程的生命周期(new、active、completing、completed、cancelling、cancelled)
      • CoroutineDispatcher:指定协程运行的线程(IO、Default、Main、Unconfined)
      • CoroutineName: 指定协程的名称,默认为coroutine
      • CoroutineExceptionHandler: 指定协程的异常处理器,用来处理未捕获的异常

    它们的关系如图所示:

    CoroutineDispatcher 作用

    • 用于指定协程的运行线程
    • kotlin已经内置了CoroutineDispatcher的4个实现,分别为 Dispatchers的Default、IO、Main、Unconfined字段

    public actual object Dispatchers {

    @JvmStatic
    public actual val Default: CoroutineDispatcher = createDefaultDispatcher()

    @JvmStatic
    public val IO: CoroutineDispatcher = DefaultScheduler.IO

    @JvmStatic
    public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined

    @JvmStatic
    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
    }

    Dispatchers.Default

    Default根据useCoroutinesScheduler属性(默认为true) 去获取对应的线程池

    • DefaultScheduler :Kotlin内部自己实现的线程池逻辑
    • CommonPool:Java类库中的Executor实现的线程池逻辑
    internal actual fun createDefaultDispatcher(): CoroutineDispatcher =
    if (useCoroutinesScheduler) DefaultScheduler else CommonPool
    internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
    .....
    }

    open class ExperimentalCoroutineDispatcher(
    private val corePoolSize: Int,
    private val maxPoolSize: Int,
    private val idleWorkerKeepAliveNs: Long,
    private val schedulerName: String = "CoroutineScheduler"
    ) : ExecutorCoroutineDispatcher() {
    constructor(
    corePoolSize: Int = CORE_POOL_SIZE,
    maxPoolSize: Int = MAX_POOL_SIZE,
    schedulerName: String = DEFAULT_SCHEDULER_NAME
    ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)

    ......
    }
    //java类库中的Executor实现线程池逻辑
    internal object CommonPool : ExecutorCoroutineDispatcher() {}

    如果想使用java类库中的线程池该如何使用呢?也就是修改useCoroutinesScheduler属性为false

    internal const val COROUTINES_SCHEDULER_PROPERTY_NAME = "kotlinx.coroutines.scheduler"

    internal val useCoroutinesScheduler = systemProp(COROUTINES_SCHEDULER_PROPERTY_NAME).let { value ->
    when (value) {
    null, "", "on" -> true
    "off" -> false
    else -> error("System property '$COROUTINES_SCHEDULER_PROPERTY_NAME' has unrecognized value '$value'")
    }
    }

    internal actual fun systemProp(
    propertyName: String
    ): String? =
    try {
    //获取系统属性
    System.getProperty(propertyName)
    } catch (e: SecurityException) {
    null
    }

    从源码中可以看到,使用过获取系统属性拿到的值, 那我们就可以通过修改系统属性 去改变useCoroutinesScheduler的值, 具体修改方法为

     val properties = Properties()
    properties["kotlinx.coroutines.scheduler"] = "off"
    System.setProperties(properties)

    DefaultScheduler的主要实现都在其父类 ExperimentalCoroutineDispatcher 中

    open class ExperimentalCoroutineDispatcher(
    private val corePoolSize: Int,
    private val maxPoolSize: Int,
    private val idleWorkerKeepAliveNs: Long,
    private val schedulerName: String = "CoroutineScheduler"
    ) : ExecutorCoroutineDispatcher() {
    public constructor(
    corePoolSize: Int = CORE_POOL_SIZE,
    maxPoolSize: Int = MAX_POOL_SIZE,
    schedulerName: String = DEFAULT_SCHEDULER_NAME
    ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)

    constructor(
    corePoolSize: Int = CORE_POOL_SIZE,
    maxPoolSize: Int = MAX_POOL_SIZE
    ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS)

    override val executor: Executor
    get() = coroutineScheduler

    private var coroutineScheduler = createScheduler()

    //创建CoroutineScheduler实例
    private fun createScheduler() = CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)

    override val executor: Executorget() = coroutineScheduler

    override fun dispatch(context: CoroutineContext, block: Runnable): Unit =
    try {
    //dispatch方法委托到CoroutineScheduler的dispatch方法
    coroutineScheduler.dispatch(block)
    } catch (e: RejectedExecutionException) {
    ....
    }

    override fun dispatchYield(context: CoroutineContext, block: Runnable): Unit =
    try {
    //dispatchYield方法委托到CoroutineScheduler的dispatchYield方法
    coroutineScheduler.dispatch(block, tailDispatch = true)
    } catch (e: RejectedExecutionException) {
    ...
    }

    internal fun dispatchWithContext(block: Runnable, context: TaskContext, tailDispatch: Boolean) {
    try {
    //dispatchWithContext方法委托到CoroutineScheduler的dispatchWithContext方法
    coroutineScheduler.dispatch(block, context, tailDispatch)
    } catch (e: RejectedExecutionException) {
    ....
    }
    }
    override fun close(): Unit = coroutineScheduler.close()
    //实现请求阻塞
    public fun blocking(parallelism: Int = BLOCKING_DEFAULT_PARALLELISM): CoroutineDispatcher {
    require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
    return LimitingDispatcher(this, parallelism, null, TASK_PROBABLY_BLOCKING)
    }
    //实现并发数量限制
    public fun limited(parallelism: Int): CoroutineDispatcher {
    require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
    require(parallelism <= corePoolSize) { "Expected parallelism level lesser than core pool size ($corePoolSize), but have $parallelism" }
    return LimitingDispatcher(this, parallelism, null, TASK_NON_BLOCKING)
    }

    ....
    }

    实现请求数量限制是调用 LimitingDispatcher 类,其类实现为

    private class LimitingDispatcher(
    private val dispatcher: ExperimentalCoroutineDispatcher,
    private val parallelism: Int,
    private val name: String?,
    override val taskMode: Int
    ) : ExecutorCoroutineDispatcher(), TaskContext, Executor {
    //同步阻塞队列
    private val queue = ConcurrentLinkedQueue<Runnable>()
    //cas计数
    private val inFlightTasks = atomic(0)

    override fun dispatch(context: CoroutineContext, block: Runnable) = dispatch(block, false)

    private fun dispatch(block: Runnable, tailDispatch: Boolean) {
    var taskToSchedule = block
    while (true) {

    if (inFlight <= parallelism) {
    //LimitingDispatcher的dispatch方法委托给了DefaultScheduler的dispatchWithContext方法
    dispatcher.dispatchWithContext(taskToSchedule, this, tailDispatch)
    return
    }
    ......
    }
    }
    }

    Dispatchers.IO

    先看下 Dispatchers.IO 的定义

        /**
    *This dispatcher shares threads with a [Default][Dispatchers.Default] dispatcher, so using
    * `withContext(Dispatchers.IO) { ... }` does not lead to an actual switching to another thread &mdash;
    * typically execution continues in the same thread.
    */

    @JvmStatic
    public val IO: CoroutineDispatcher = DefaultScheduler.IO


    Internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
    val IO = blocking(systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)))

    ......

    }

    IO在DefaultScheduler中的实现 是调用blacking()方法,而blacking()方法最终实现是LimitingDispatcher类, 所以 从源码可以看出 Dispatchers.Default和IO 是在同一个线程中运行的,也就是共用相同的线程池。

    而Default和IO 都是共享CoroutineScheduler线程池 ,kotlin内部实现了一套线程池两种调度策略,主要是通过dispatch方法中的Mode区分的

    TypeMode
    DefaultNON_BLOCKING
    IOPROBABLY_BLOCKING
    internal enum class TaskMode {

    //执行CPU密集型任务
    NON_BLOCKING,

    //执行IO密集型任务
    PROBABLY_BLOCKING,
    }
    fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, tailDispatch: Boolean = false) {
    ......
    if (task.mode == TaskMode.NON_BLOCKING) {
    signalCpuWork() //Dispatchers.Default
    } else {
    signalBlockingWork() // Dispatchers.IO
    }
    }

    Type处理策略适合场景特点
    Default1、CoroutineScheduler最多有corePoolSize个线程被创建; 2、corePoolSize它的取值为max(2, CPU核心数),即它会尽量的等于CPU核心数复杂计算、视频解码等1、CPU密集型任务特点会消耗大量的CPU资源。2、因为线程本身也有栈等空间,同时线程过多,频繁的线程切换带来的消耗也会影响线程池的性能4.对于CPU密集型任务,线程池并发线程数等于CPU核心数才能让CPU的执行效率最大化
    IO1、创建线程数不能大于maxPoolSize ,公式:max(corePoolSize, min(CPU核心数 * 128, 2^21 - 2))。网络请求、IO操作等1、IO密集型 执行任务时CPU会处于闲置状态,任务不会消耗大量的CPU资源。 2.线程执行IO密集型任务时大多数处于阻塞状态,处于阻塞状态的线程是不占用CPU的执行时间。3.Dispatchers.IO构造时通过LimitingDispatcher默认限制了最大线程并发数parallelism为max(64, CPU核心数),剩余的任务被放进队列中等待。

    Dispatchers.Unconfined

    任务执行在默认的启动线程。之后由调用resume的线程决定恢复协程的线程

    internal object Unconfined : CoroutineDispatcher() {
    //为false为不需要dispatch
    override fun isDispatchNeeded(context: CoroutineContext): Boolean = false

    override fun dispatch(context: CoroutineContext, block: Runnable) {
    // 只有当调用yield方法时,Unconfined的dispatch方法才会被调用
    // yield() 表示当前协程让出自己所在的线程给其他协程运行
    val yieldContext = context[YieldContext]
    if (yieldContext != null) {
    yieldContext.dispatcherWasUnconfined = true
    return
    }
    throw UnsupportedOperationException("Dispatchers.Unconfined.dispatch function can only be used by the yield function. " +
    "If you wrap Unconfined dispatcher in your code, make sure you properly delegate " +
    "isDispatchNeeded and dispatch calls.")
    }
    }

    每一个协程都有对应的Continuation实例,其中的resumeWith用于协程的恢复,存在于DispatchedContinuation

    public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    ......

    public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
    DispatchedContinuation(this, continuation)

    ......

    }

    重点看resumeWith的实现以及类委托

    internal class DispatchedContinuation<in T>(
    @JvmField val dispatcher: CoroutineDispatcher,
    @JvmField val continuation: Continuation<T>//协程suspend挂起方法产生的Continuation
    ) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation {
    .....
    override fun resumeWith(result: Result<T>) {
    val context = continuation.context
    val state = result.toState()
    if (dispatcher.isDispatchNeeded(context)) {
    _state = state
    resumeMode = MODE_ATOMIC
    dispatcher.dispatch(context, this)
    } else {
    executeUnconfined(state, MODE_ATOMIC) {
    withCoroutineContext(this.context, countOrElement) {
    continuation.resumeWith(result)
    }
    }
    }
    }
    ....
    }

    通过isDispatchNeeded(是否需要dispatch,Unconfined=false,default,IO=true)判断做不同处理

    • true:调用协程的CoroutineDispatcher的dispatch方法
    • false:调用executeUnconfined方法
    private inline fun DispatchedContinuation<*>.executeUnconfined(
    contState: Any?, mode: Int, doYield: Boolean = false,
    block: () -> Unit
    ): Boolean {
    assert { mode != MODE_UNINITIALIZED }
    val eventLoop = ThreadLocalEventLoop.eventLoop
    if (doYield && eventLoop.isUnconfinedQueueEmpty) return false
    return if (eventLoop.isUnconfinedLoopActive) {
    _state = contState
    resumeMode = mode
    eventLoop.dispatchUnconfined(this)
    true
    } else {
    runUnconfinedEventLoop(eventLoop, block = block)
    false
    }
    }

    从threadlocal中取出eventLoop(eventLoop和当前线程相关的),判断是否在执行Unconfined任务

    1. 如果在执行则调用EventLoop的dispatchUnconfined方法把Unconfined任务放进EventLoop中
    2. 如果没有在执行则直接执行
    internal inline fun DispatchedTask<*>.runUnconfinedEventLoop(
    eventLoop: EventLoop,
    block: () -> Unit
    ) {
    eventLoop.incrementUseCount(unconfined = true)
    try {
    block()
    while (true) {
    if (!eventLoop.processUnconfinedEvent()) break
    }
    } catch (e: Throwable) {
    handleFatalException(e, null)
    } finally {
    eventLoop.decrementUseCount(unconfined = true)
    }
    }

    1. 执行block()代码块,即上文提到的resumeWith()
    2. 调用processUnconfinedEvent()方法实现执行剩余的Unconfined任务,知道全部执行完毕跳出循环

    EventLoop是存放与threadlocal,所以是跟当前线程相关联的,而EventLoop也是CoroutineDispatcher的一个子类

    internal abstract class EventLoop : CoroutineDispatcher() {
    .....
    //双端队列实现存放Unconfined任务
    private var unconfinedQueue: ArrayQueue<DispatchedTask<*>>? = null
    //从队列的头部移出Unconfined任务执行
    public fun processUnconfinedEvent(): Boolean {
    val queue = unconfinedQueue ?: return false
    val task = queue.removeFirstOrNull() ?: return false
    task.run()
    return true
    }
    //把Unconfined任务放进队列的尾部
    public fun dispatchUnconfined(task: DispatchedTask<*>) {
    val queue = unconfinedQueue ?:
    ArrayQueue<DispatchedTask<*>>().also { unconfinedQueue = it }
    queue.addLast(task)
    }
    .....
    }

    内部通过双端队列实现存放Unconfined任务

    1. EventLoop的dispatchUnconfined方法用于把Unconfined任务放进队列的尾部
    2. rocessUnconfinedEvent方法用于从队列的头部移出Unconfined任务执行

    Dispatchers.Main

    kotlin在JVM上的实现 Android就需要引入kotlinx-coroutines-android库,它里面有Android对应的Dispatchers.Main实现,

       public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher

    @JvmField
    val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()

    private fun loadMainDispatcher(): MainCoroutineDispatcher {
    return try {
    val factories = if (FAST_SERVICE_LOADER_ENABLED) {
    FastServiceLoader.loadMainDispatcherFactory()
    } else {
    // We are explicitly using the
    // `ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()`
    // form of the ServiceLoader call to enable R8 optimization when compiled on Android.
    ServiceLoader.load(
    MainDispatcherFactory::class.java,
    MainDispatcherFactory::class.java.classLoader
    ).iterator().asSequence().toList()
    }
    factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
    ?: MissingMainCoroutineDispatcher(null)
    } catch (e: Throwable) {
    // Service loader can throw an exception as well
    MissingMainCoroutineDispatcher(e)
    }
    }

    internal fun loadMainDispatcherFactory(): List<MainDispatcherFactory> {
    val clz = MainDispatcherFactory::class.java
    if (!ANDROID_DETECTED) {
    return load(clz, clz.classLoader)
    }

    return try {
    val result = ArrayList<MainDispatcherFactory>(2)
    createInstanceOf(clz, "kotlinx.coroutines.android.AndroidDispatcherFactory")?.apply { result.add(this) }
    createInstanceOf(clz, "kotlinx.coroutines.test.internal.TestMainDispatcherFactory")?.apply { result.add(this) }
    result
    } catch (e: Throwable) {
    // Fallback to the regular SL in case of any unexpected exception
    load(clz, clz.classLoader)
    }
    }

    通过反射获取AndroidDispatcherFactory 然后根据加载的优先级 去创建Dispatcher

    internal class AndroidDispatcherFactory : MainDispatcherFactory {

    override fun createDispatcher(allFactories: List<MainDispatcherFactory>) =
    HandlerContext(Looper.getMainLooper().asHandler(async = true), "Main")

    override fun hintOnError(): String? = "For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used"

    override val loadPriority: Int
    get() = Int.MAX_VALUE / 2
    }
    internal class HandlerContext private constructor(
    private val handler: Handler,
    private val name: String?,
    private val invokeImmediately: Boolean
    ) : HandlerDispatcher(), Delay {

    public constructor(
    handler: Handler,
    name: String? = null
    ) : this(handler, name, false)

    ......

    override fun dispatch(context: CoroutineContext, block: Runnable) {
    handler.post(block)
    }

    ......
    }

    而createDispatcher调用HandlerContext 类 通过调用Looper.getMainLooper()获取handler ,最终通过handler来实现在主线程中运行

    Dispatchers.Main 其实就是把任务通过Handler运行在Android的主线程

    收起阅读 »

    Android C++系列:Linux文件IO操作

    1.1 read/writeread函数从打开的设备或文件中读取数据。#include ssize_t read(int fd, void *buf, size_t count); //返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调r...
    继续阅读 »

    1.1 read/write

    read函数从打开的设备或文件中读取数据。

    #include 
    ssize_t read(int fd, void *buf, size_t count);
    //返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0

    参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读 写位置向后移。

    注意这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写 位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置。比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一 个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是 1。

    注意返回值类型是ssize_t,表示有符号的size_t,这样既可以返回正的字节数、0(表 示到达文件末尾)也可以返回负值-1(表示出错)。

    read函数返回时,返回值说明了buf中 前多少个字节是刚读上来的。有些情况下,实际读到的字节数(返回值)会小于请求读的字节数count,例如:

    • 读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个 字节而请求读100个字节,则read返回30,下次read将返回0。

    • 从终端设备读,通常以行为单位,读到换行符就返回了。

    • 从网络读,根据不同的传输层协议和内核缓存机制,返回值可能小于请求的字节数,后面socket编程部分会详细讲解。

    write函数向打开的设备或文件中写数据。

    #include 
    ssize_t write(int fd, const void *buf, size_t count);
    返回值:成功返回写入的字节数,出错返回-1并设置errno

    写常规文件时,write的返回值通常等于请求写的字节数count,而向终端设备或网络写则不一定。

    1.2 阻塞和非阻塞

    读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。

    现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况:

    • 正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
    • 就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另 一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进 程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程 的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时 要兼顾用户体验,不能让和用户交互的进程响应太慢。

    下面这个小程序从终端读数据再写回终端。

    1.2.1 阻塞读终端

    #include  #include 
    int main(void) {
    char buf[10];
    int n;
    n = read(STDIN_FILENO, buf, 10);
    if (n < 0) {
    perror("read STDIN_FILENO");
    exit(1);
    }
    write(STDOUT_FILENO, buf, n);
    return 0;
    }

    执行结果如下:

    $ ./a.out hello(回车)
    hello
    $ ./a.out
    hello world(回车) hello worl$ d
    bash: d: command not found

    第一次执行a.out的结果很正常,而第二次执行的过程有点特殊,现在分析一下:

    Shell进程创建a.out进程,a.out进程开始执行,而shell进程睡眠等待a.out进程退出。

    a.out调用read时睡眠等待,直到终端设备输入了换行符才从read返回,read只读走10 个字符,剩下的字符仍然保存在内核的终端设备输入缓冲区中。

    a.out进程打印并退出,这时shell进程恢复运行,Shell继续从终端读取用户输入的命令,于是读走了终端设备输入缓冲区中剩下的字符d和换行符,把它当成一条命令解释执 行,结果发现执行不了,没有d这个命令。

    如果在open一个设备时指定了O_NONBLOCK标志,read/write就不会阻塞。以read为例, 如果设备暂时没有数据可读就返回-1,同时置errno为EWOULDBLOCK(或者EAGAIN,这两个宏定义的值相同),表示本来应该阻塞在这里(would block,虚拟语气),事实上并没 有阻塞而是直接返回错误,调用者应该试着再读一次(again)。这种行为方式称为轮询 (Poll),调用者只是查询一下,而不是阻塞在这里死等,这样可以同时监视多个设备:

    while(1) { 
    非阻塞read(设备1);
    if(设备1有数据到达)
    处理数据;
    非阻塞read(设备2);
    if(设备2有数据到达)
    处理数据; ...
    }

    如果read(设备1)是阻塞的,那么只要设备1没有数据到达就会一直阻塞在设备1的read 调用上,即使设备2有数据到达也不能处理,使用非阻塞I/O就可以避免设备2得不到及时处 理。

    非阻塞I/O有一个缺点,如果所有设备都一直没有数据到达,调用者需要反复查询做无用功,如果阻塞在那里,操作系统可以调度别的进程执行,就不会做无用功了。在使用非阻塞I/O时,通常不会在一个while循环中一直不停地查询(这称为Tight Loop),而是每延迟 等待一会儿来查询一下,以免做太多无用功,在延迟等待的时候可以调度其它进程执行。

    while(1) { 
    非阻塞read(设备1);
    if(设备1有数据到达)
    处理数据;
    非阻塞read(设备2);
    if(设备2有数据到达)
    处理数据;
    ...
    sleep(n);
    }

    这样做的问题是,设备1有数据到达时可能不能及时处理,最长需延迟n秒才能处理,而且反复查询还是做了很多无用功。以后要学习的select(2)函数可以阻塞地同时监视多个设 备,还可以设定阻塞等待的超时时间,从而圆满地解决了这个问题。

    以下是一个非阻塞I/O的例子。目前我们学过的可能引起阻塞的设备只有终端,所以我们用终端来做这个实验。程序开始执行时在0、1、2文件描述符上自动打开的文件就是终 端,但是没有O_NONBLOCK标志。所以就像例 28.2 “阻塞读终端”一样,读标准输入是阻塞 的。我们可以重新打开一遍设备文件/dev/tty(表示当前终端),在打开时指定O_NONBLOCK 标志。

    1.2.2 非阻塞读终端

    #include  
    #include
    #include
    #include
    #include
    #define MSG_TRY "try again\n"
    int main(void) {
    char buf[10];
    int fd, n;
    fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
    if(fd<0) {
    perror("open /dev/tty");
    exit(1);
    }
    tryagain:
    n = read(fd, buf, 10);
    if (n < 0) {
    if (errno == EAGAIN) {
    sleep(1);
    write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
    goto tryagain;
    }
    perror("read /dev/tty");
    exit(1);
    }
    write(STDOUT_FILENO, buf, n); close(fd);
    return 0;
    }

    以下是用非阻塞I/O实现等待超时的例子。既保证了超时退出的逻辑又保证了有数据到达时处理延迟较小。

    1.2.3 非阻塞读终端和等待超时

    #include  
    #include
    #include
    #include
    #include
    #define MSG_TRY "try again\n"
    #define MSG_TIMEOUT "timeout\n"
    int main(void) {
    char buf[10];
    int fd, n, i;
    fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
    if(fd<0) {
    perror("open /dev/tty");
    exit(1);
    }
    for(i=0; i<5; i++) {
    n = read(fd, buf, 10);
    if(n>=0)
    break;
    if(errno!=EAGAIN) {
    perror("read /dev/tty");
    exit(1);
    }
    sleep(1);
    write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
    }
    if(i==5)
    write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
    else
    write(STDOUT_FILENO, buf, n);
    close(fd);
    return 0;
    }

    1.3 lseek

    每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节就会将读写位置往后移多少个字节。但是有一个例外,如果以O_APPEND方 式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。lseek 和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。

     #include 
    #include
    off_t lseek(int fd, off_t offset, int whence);

    参数offset和whence的含义和fseek函数完全相同。只不过第一个参数换成了文件描述符。和fseek一样,偏移量允许超过文件末尾,这种情况下对该文件的下一次写操作将延长 文件,中间空洞的部分读出来都是0。

    若lseek成功执行,则返回新的偏移量,因此可用以下方法确定一个打开文件的当前偏 移量:

    off_t currpos;
    currpos = lseek(fd, 0, SEEK_CUR);

    这种方法也可用来确定文件或设备是否可以设置偏移量,常规文件都可以设置偏移量, 而设备一般是不可以设置偏移量的。如果设备不支持lseek,则lseek返回-1,并将errno 设置为ESPIPE。注意fseek和lseek在返回值上有细微的差别,fseek成功时返回0失败时返 回-1,要返回当前偏移量需调用ftell,而lseek成功时返回当前偏移量失败时返回-1。

    1.4 fcntl

    先前我们以read终端设备为例介绍了非阻塞I/O,为什么我们不直接对STDIN_FILENO做 非阻塞read,而要重新open一遍/dev/tty呢?因为STDIN_FILENO在程序启动时已经被自动 打开了,而我们需要在调用open时指定O_NONBLOCK标志。这里介绍另外一种办法,可以用 fcntl函数改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志(这 些标志称为File Status Flag),而不必重新open文件。

    #include  #include 
    int fcntl(int fd, int cmd);
    int fcntl(int fd, int cmd, long arg);
    int fcntl(int fd, int cmd, struct flock *lock);

    这个函数和open一样,也是用可变参数实现的,可变参数的类型和个数取决于前面的 cmd参数。下面的例子使用F_GETFL和F_SETFL这两种fcntl命令改变STDIN_FILENO的属性,加 上O_NONBLOCK选项,实现和例 28.3 “非阻塞读终端”同样的功能。

    1.4.1 用fcntl改变File Status Flag

    #include  
    #include
    #include
    #include
    #include
    #define MSG_TRY "try again\n"
    int main(void) {
    char buf[10];
    int n;
    int flags;
    flags = fcntl(STDIN_FILENO, F_GETFL); flags |= O_NONBLOCK;
    if (fcntl(STDIN_FILENO, F_SETFL, flags) == -1) {
    perror("fcntl");
    exit(1);
    }
    tryagain:
    n = read(STDIN_FILENO, buf, 10);
    if (n < 0) {
    if (errno == EAGAIN) {
    sleep(1);
    write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
    goto tryagain;
    }
    perror("read stdin");
    exit(1);
    }
    write(STDOUT_FILENO, buf, n);
    return 0;
    }

    1.5 ioctl

    ioctl用于向设备发控制和配置命令,有些命令也需要读写一些数据,但这些数据是 不能用read/write读写的,称为Out-of-band数据。也就是说,read/write读写的数据是 in-band数据,是I/O操作的主体,而ioctl命令传送的是控制信息,其中的数据是辅助的数 据。例如,在串口线上收发数据通过read/write操作,而串口的波特率、校验位、停止位通 过ioctl设置,A/D转换的结果通过read读取,而A/D转换的精度和工作频率通过ioctl设置。

    #include 
    int ioctl(int d, int request, ...);

    d是某个设备的文件描述符。request是ioctl的命令,可变参数取决于request,通常是 一个指向变量或结构体的指针。若出错则返回-1,若成功则返回其他值,返回值也是取决于 request。

    以下程序使用TIOCGWINSZ命令获得终端设备的窗口大小。

    #include  
    #include
    #include
    #include
    int main(void) {
    struct winsize size;
    if (isatty(STDOUT_FILENO) == 0)
    exit(1);
    if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)<0) {
    perror("ioctl TIOCGWINSZ error");
    exit(1);
    }
    printf("%d rows, %d columns\n", size.ws_row, size.ws_col);
    return 0;
    }

    在图形界面的终端里多次改变终端窗口的大小并运行该程序,观察结果。

    1.6 总结

    本文介绍了read/write的系统调用,以及阻塞、非阻塞相关的概念以及配置方式,等待超时方式。还介绍了lseek、fcntl、ioctl文件操作相关的系统调用。

    原文链接:https://juejin.cn/post/7031846767289499685?utm_source=gold_browser_extension

    收起阅读 »

    Android App 卡顿分析

    Android App 反应卡顿,从技术上将就是UI 渲染慢。 UI渲染是从您的应用程序生成一个框架并将其显示在屏幕上的行为。 为了确保用户与您的应用程序的交互顺利,您的应用程序应该在16ms内渲染帧数达到每秒60帧(为什么60fps?)。 如果您的应用程序因...
    继续阅读 »

    Android App 反应卡顿,从技术上将就是UI 渲染慢。


    UI渲染是从您的应用程序生成一个框架并将其显示在屏幕上的行为。 为了确保用户与您的应用程序的交互顺利,您的应用程序应该在16ms内渲染帧数达到每秒60帧(为什么60fps?)。 如果您的应用程序因UI渲染速度缓慢而受到影响,那么系统将被迫跳过帧,用户将感觉到您的应用程序中出现卡顿。 我们把这个叫做jank


    本篇文章主要介绍 Android 开发中的部分知识点,通过阅读本篇文章,您将收获以下内容:



    1.UI 渲染简介
    2.识别Jank
    3.Fix Jank
    4.引起Jank 通用问题举例



    1.UI 渲染简介


    为了帮助您提高应用程序质量,Android会自动监视您的应用程序是否有空,并在Android生命危险仪表板中显示信息。 有关如何收集数据的信息,请参阅Play Console文档。


    如果您的应用程序出现问题,本页提供诊断和解决问题的指导。


    Android生命危险仪表板和Android系统会跟踪使用UI Toolkit的应用程序的渲染时间统计信息(应用程序的用户可见部分是从CanvasView hierarchy绘制的)。


    如果您的应用程序不使用UI Toolkit,就像使用VulkanUnityUnrealOpenGL构建的应用程序一样,则在Android Vitals仪表板中不提供时间统计信息。


    您可以通过运行
    adb shell dumpsys gfxinfo <package name>
    来确定您的设备是否正在记录您的应用的渲染时间指标。


    2.识别Jank


    在您的应用程序中定位引起jank的代码可能很困难。 本部分介绍了三种识别jank的方法:



    • 1.Visual inspection


    通过视觉检查,您可以在几分钟内快速浏览应用程序中的所有用例use-cases,但不能提供与Systrace相同的详细信息。



    • 2.Systrace


    Systrace提供了更多的细节,但是如果你运行Systrace来处理应用程序中的所有用例,那么就会被大量的数据淹没,难以分析。



    • 3.Custom performance monitoring


    Visual inspectionSystrace都会在你的本地设备上检测到。


    如果不能在本地设备上重现,则可以构建自定义性能监视器Custom performance monitoring,以测量在现场运行的设备上应用的特定部分。


    ##1. Visual inspection


    目视检查可以帮助您识别正在生产结果的使用案例。 要执行视觉检查,请打开您的应用程序并手动检查应用程序的不同部分,然后查看非常粗糙的UI。 以下是进行目视检查时的一些提示:



    • 1.运行release 版本


    运行您release应用程序的版本(或至少不可调试)的版本。ART运行时为了支持调试功能而禁用了一些重要的优化,所以确保你正在寻找类似于用户将看到的东西。





      1. 开启GPU渲染




    开启步骤:
    Settings -->Developer options -->Profile GPU rending


    开启配置文件GPU渲染,会在屏幕上显示条形图,可以快速直观地显示相对于每帧16毫秒基准测试渲染UI窗口帧所花费的时间。
    每个条都有着色的组件映射到渲染管道中的一个舞台,所以你可以看到哪个部分花费的时间最长。
    例如,如果框架花费大量时间处理输入,则应该查看处理用户输入的应用程序代码。


    开启 GPU 渲染效果图





      1. 留意特殊组件




    有一些组件,如RecyclerView,是Jank普遍的来源。 如果您的应用程序使用这些组件,那么运行应用程序的这些部分是一个好idea





      1. App 冷启动导致




    有时候,只有当应用程序从冷启动启动(Clod start)时,才能复制jank



    • 5.低内存情况下jank 比较容易出现


    一旦你发现产生jank的用例,你可能会有一个很好的想法是什么导致你的应用程序的结果。 但是,如果您需要更多信息,则可以使用Systrace进一步深入研究。


    ##2. Systrace


    Systrace是一个显示整个设备在做什么的工具,并且它可以用于识别应用程序中的JankSystrace的系统开销很小,所以在仪器使用过程中你会感受到app卡顿的存在。


    Systrace记录跟踪,同时在设备上执行janky用例。 有关如何使用Systrace的说明,请参阅Systrace演练。 systrace被进程和线程分解。 在Systrace中查找应用程序的过程,应该如图所示。


    Systrace分析应用程序


    上面3个标注点解释



    1. 当卡顿时,会有掉帧发生,如上图1所示


    Systrace显示何时绘制每个框架,并对每个框架进行颜色编码以突出显示较慢的渲染时间。 这可以帮助您查找比视觉检查更准确的单个janky框架。 有关更多信息,请参阅Inspecting Frames.




    1. 掉帧提示,如上图 2所示
      Systrace检测应用程序中的问题,并在各个框架和警报面板中显示警报。 警报中的以下指示是您的最佳选择。




    2. systrace timeline 如上图3 所示




    Android框架和库的一部分(如RecyclerView)包含跟踪标记。 因此,systrace时间线会显示何时在UI线程上执行这些方法,以及执行多长时间。


    如果systrace没有向您显示有关长时间使用UI线程工作的详细信息,则需要使用Android CPU Profiler来记录采样或检测的方法跟踪。 一般来说,method方法痕迹不适合用于识别排队,因为由于开销太大而产生假jank,并且无法看到线程何时被阻塞。 但是,method方法跟踪可以帮助您识别应用中花费最多时间的方法。 在识别这些方法后,add Trace markers a
    标记并重新运行systrace,以查看这些方法是否引起混乱。
    当记录systrace时,每个跟踪标记(执行的开始 Trace.beginSection();和结束Trace.endSection();对)会增加大约10μs的开销。 为了避免假Jank结局,不要将追踪标记添加到在一帧中被称为几十次的方法中,或者短于200us左右。


    如需获取更多内容,请查看Systrace详解


    ##3. Custom performance monitoring


    如果您无法在本地设备上再现突发事件,则可以在您的应用中构建自定义性能监控,以帮助识别现场设备上的突发源。


    为此,请使用FrameMetricsAggregator从应用程序的特定部分收集帧渲染时间,并使用Firebase性能监控记录和分析数据。


    要了解更多信息,请参阅使用Use Firebase Performance Monitoring with Android Vitals.


    #3.Fix Jank


    为了解决这个问题,请检查哪些帧在16.7ms内没有完成,并寻找出错的地方。Record View#draw在一些帧中抽取异常长度,或者可能是Layout? 查看下面4这些问题的常见来源,以及其他问题。


    为了避免乱码,长时间运行的任务应该在UI线程之外异步运行。 一定要注意你的代码正在运行在哪个线程上,并且在向主线程发布不重要的任务时要小心。


    如果您的应用程序有一个复杂而重要的主UI(可能是中央滚动列表),请考虑编写可自动检测缓慢渲染时间的测试测试,并经常运行测试以防止出现回归。 有关更多信息,请参阅自动化性能测试代码实验室。


    #4.引起Jank 通用问题举例


    以下部分解释了应用程序中常见Jank问题 的来源,以及解决这些问题的最佳方案。


    滑动 List


    ListView和特别是RecyclerView通常用于复杂的滚动列表,这些列表最容易被忽略。 他们都包含Systrace标记,所以你可以使用Systrace来弄清楚他们是否有助于在你的应用程序jank。 一定要传递命令行参数-a <your-package-name>来获取RecyclerView中的跟踪部分(以及添加的任何跟踪标记)以显示出来。 如果可用,请遵循systrace输出中生成的警报的指导。 在Systrace里面,你可以点击RecyclerView-traced部分查看RecyclerView正在做的工作的解释。


    RecyclerView: notifyDataSetChanged


    如果您看到RecyclerView中的每个项目在一个框架中被反弹(并因此重新布局和重新绘制),请确保您没有调用notifyDataSetChanged()setAdapter(Adapter)swapAdapter(Adapter,boolean)为小更新。 这些方法表示整个列表内容已经改变,并且将在Systrace中显示为RV FullInvalidate。 而是在内容更改或添加时使用SortedListDiffUtil生成最小更新。


    例如,考虑从服务器接收新闻内容列表的新版本的应用程序。 当您将该信息发布到适配器时,可以调用notifyDataSetChanged(),如下所示:


    RecyclerView: notifyDataSetChanged

    但是这带来了一个很大的缺点 - 如果它是一个微不足道的变化(也许单个项目添加到顶部),RecyclerView不知道 - 它被告知放弃所有的缓存项目状态,因此需要重新绑定一切。


    最好使用DiffUtil,它将为您计算和分配最小的更新。

    DiffUtil 使用


    只需将您的MyCallback定义为DiffUtil.Callback实现,以通知DiffUtil如何检查您的列表。


    RecyclerView: Nested RecyclerViews


    嵌套RecyclerView是很常见的,特别是水平滚动列表的垂直列表(如Play Store主页上的应用程序的网格)。 这可以很好的工作,但也有很多意外四处移动。 如果在第一次向下滚动页面时看到很多内部项目膨胀,则可能需要检查是否在内部(水平)RecyclerViews之间共享RecyclerView.RecycledViewPools


    默认情况下,每个RecyclerView将拥有自己的物品池。 如果在屏幕上同时显示一打itemViews,那么当itemViews不能被不同的水平列表共享的时候,如果所有的行都显示了相似类型的视图,那么这是有问题的。


    RecyclerView: Nested RecyclerViews


    如果要进一步优化,还可以在内部RecyclerViewLinearLayoutManager上调用setInitialPrefetchItemCount(int)
    例如,如果您总是在一行中可见3.5项,请调用innerLLM.setInitialItemPrefetchCount(4);. 这将告诉RecyclerView,当一个水平行即将出现在屏幕上时,如果UI线程上有空闲时间,它应该尝试预取内部的项目


    RecyclerView: Too much inflation / Create taking too long


    UI线程则处于闲置状态下,RecyclerView中的预取功能应该有助于在大多数情况下通过提前完成工作来解决inflation Layout的成本问题。
    如果您在一帧中看到inflation Layout(而不是标记为RV Prefetch的部分),请确保您正在测试最近的设备(Prefetch目前仅在Android 5.0 API Level 21及更高版本上支持),并使用最近版本的Support Library.


    如果经常看到inflation Layout导致屏幕上出现新的Jank,验证出问题,请移除多余的ViewRecyclerView内容中的视图类型越少,当新的项目类型出现在屏幕上时,需要完成的inflation Layout就越少。


    如果可能的话,将视图类型合并到合理的位置 - 如果只有图标,颜色或文本块在类型之间改变,则可以在绑定时间进行更改,并避免inflation Layout(同时减少应用程序的内存占用)。


    如果您的视图类型看起来还不错,请考虑减少inflation Layout的成本。减少不必要的容器和结构视图可以帮助 - 考虑使用ConstraintLayout构建itemView,这可以很容易地减少结构视图。如果你想真正优化性能,你的项目层次结构是简单的,并且你不需要复杂的themingstyle的功能,请考虑自己调用构造函数 - 但请注意,它往往是不值得的损失的简单性和功能的权衡XML。


    RecyclerView: Bind taking too long


    绑定(即onBindViewHolder(VH,int))应该是非常简单的,除了最复杂的项目之外的所有项目都要花费少于一毫秒的时间。 它只需从adapter's的内部项目数据中获取POJO项目,然后在ViewHolder中的视图上调用setter。 如果RV OnBindView需要很长时间,请确认您在绑定代码中做了最少的工作。


    如果您使用简单的POJO对象来保存适配器中的数据,则可以完全避免使用Data Binding l
    库来将绑定代码写入onBindViewHolder


    RecyclerView or ListView: layout / draw taking too long


    有关绘制和布局的问题,请参阅 Layout and Rendering Performance.


    ListView: Inflation


    如果你不小心,ListView很容易会被意外回收。 如果每次屏幕显示项目时都看到inflation Layout,请检查Adapter.getView()的实现是否正在使用,重新绑定并返回convertView参数。 如果你的getView()实现总是inflation Layout,你的应用程序将无法从ListView中获得回收的好处。 你的getView()的结构几乎总是类似于下面的实现:


    复用 convertView


    Layout performance


    如果Systrace显示Choreographer#doFrame的布局部分工作太多,或者工作频繁,这意味着您遇到了布局性能问题。 您的应用的布局性能取决于View层次结构的哪个部分具有更改布局参数或输入。


    Layout performance: Cost


    如果段长度超过几毫秒,则可能是针对RelativeLayoutsweighted-LinearLayouts.
    的最差嵌套性能。


    这些布局中的每一个都可以触发其子项的多个measure/layout传递,因此嵌套它们会导致嵌套深度上的O(n ^ 2)行为。 请尝试避免使用RelativeLayoutLinearLayoutweight特征,除了层次结构的最低叶节点之外的所有特征。 有几种方法可以做到这一点:



    • 优化View结构

    • 使用自定义View

    • 尝试转换到ConstraintLayout,它提供了类似的功能,并且没有性能上的缺陷。


    Layout performance: Frequency


    当新内容出现在屏幕上时,将会发生新的Layout,例如,当一个新项目在RecyclerView中滚动查看时。 如果在每个框架上都发生重要的布局,则可能是在布局上进行动画处理,这很可能导致丢帧。 通常,动画应该在View的绘图属性(例如setTranslationX / Y / Z()setRotation(),setAlpha()等)上运行。 这些都可以比Layout属性(如填充或边距)更好地更改。 通常通过调用触发invalidate()setter,然后在下一帧中绘制(Canvas),来更改视图的绘制属性。 这将重新记录无效的视图的绘图操作,并且通常也比布局好得多。


    Rendering performance渲染性能


    Android UI在两个阶段工作 - 在UI线程Record View#draw,在RenderThread上绘制DrawFrame。 第一次运行在每个无效的View上绘制(Canvas),并可能调用自定义视图或代码。 第二个在本地RenderThread上运行,但是将根据Record View#draw阶段生成的工作进行操作。


    Rendering performance: UI Thread


    如果Record View#draw需要很长时间,则通常是在UI线程上绘制位图的情况。 绘制位图需要使用CPU渲染,一般应该避免在主线程中绘制。 您可以使用Android CPU分析器的方法跟踪来查看这是否是问题。


    绘制位图通常是在应用程序想要在显示位图之前修饰位图的时候完成的。 有时候像装饰圆角的装饰:

    绘制圆角图片

    如果这是您在UI线程上所做的工作,则可以在后台的解码线程上执行此操作。 在这样的一些情况下,你甚至可以在绘制时做这个工作,所以如果你的Drawable或View代码看起来像这样:
    性能差代码


    可以将上面代码优化为如下:


    优化性能的代码


    请注意,这通常也可以用于后台保护(在位图顶部绘制渐变)和图像过滤(使用ColorMatrixColorFilter),以及修改位图的其他两种常见操作。


    如果由于其他原因(可能将其用作缓存)绘制到位图,则尝试绘制直接传递到ViewDrawable的硬件加速硬件,如有必要,可考虑使用LAYER_TYPE_HARDWARE调用setLayerType()来缓存复杂的渲染 输出,并仍然利用GPU渲染。


    Rendering performance: RenderThread


    一些canvas操作是便小的消耗,但触发RenderThread昂贵的计算。 Systrace通常会通知这些。


    Canvas.saveLayer()


    避免Canvas.saveLayer() - 它可以触发昂贵的,未缓存的,离屏渲染每一帧。 尽管Android 6.0的性能得到了提高(当进行优化以避免GPU上的渲染目标切换时),但是如果可能的话,避免使用这个昂贵的API仍然是好事,或者至少确保您通过CLIP_TO_LAYER_SAVE_FLAG(或者调用一个变体 不带标志)。


    Animating large Paths


    当硬件加速Canvas传递给Views时,Canvas.drawPath()被调用,Android首先在CPU上绘制这些路径,然后将它们上传到GPU。 如果路径较大,请避免逐帧编辑,以便高速缓存和绘制。 drawPoints(),drawLines()和drawRect / Circle / Oval / RoundRect()更有效率 - 即使最终使用更多的绘制调用,最好使用它们。


    Canvas.clipPath


    clipPath(Path)触发了昂贵的裁剪行为,通常应该避免。 如果可能,选择绘制形状,而不是剪裁到非矩形。 它性能更好,支持抗锯齿。 例如,下面的clipPath调用:


    Canvas.clipPath


    Bitmap uploads


    Android将位图显示为OpenGL纹理,并且首次在一帧中显示位图时,将其上传到GPU。您可以在Systrace中将此视为上传宽度x高度纹理。这可能需要几个毫秒(见下图),但是有必要用GPU显示图像。

    位图绘制


    如果这些花费很长时间,请首先检查轨迹中的宽度和高度数字。确保正在显示的位图不比显示的屏幕区域大得多。如果是,则浪费上传时间和内存。通常位图加载库提供了简单的方法来请求适当大小的位图。


    Android 7.0中,位图加载代码(通常由库完成)可以在需要之前调用prepareToDraw()来及早触发上传。这样上传发生的早,而RenderThread空闲。这可以在解码之后完成,也可以在将位图绑定到View时进行,只要知道位图即可。理想情况下,你的位图加载库会为你做这个,但是如果你正在管理你自己的,或者想确保你没有在新设备上点击上传,你可以在你自己的代码中调用prepareToDraw()


    Thread scheduling delays


    线程调度程序是Android操作系统的一部分,负责决定系统中哪些线程应该运行,何时运行以及运行多长时间。 有时候,因为你的应用程序的UI线程被阻塞或者没有运行,就会发生JankSystrace使用不同的颜色来指示线程正在Sleep(灰色)Runnable(蓝色:可以运行,但调度程序还没有选择它运行)正在运行(绿色)中断(红色或橙色)。 这对于调试线程调度延迟导致的Jank`问题非常有用。



    注意:
    旧版本的Android更频繁地遇到不是应用程序故障的调度问题。 在这方面进行了不断的改进,所以考虑在最近的操作系统版本上更多的调试线程调度问题,在这些版本中,被调度的线程更可能是应用程序的错误。



    线程渲染过程


    UI线程RenderThread预计不会运行时,有框架的一部分。 例如,UI线程RenderThreadsyncFrameState正在运行并且上传位图时被阻塞 - 这是因为RenderThread可以安全地复制UI线程所使用的数据。 另一个例子是,RenderThread在使用IPC时可以被阻塞:在帧的开始处获取缓冲区,从中查询信息,或者通过eglSwapBuffers将缓冲区传回给合成器。


    在您的应用程序的执行中经常会有很长时间的暂停,这些都是由Android上的进程间通信(IPC)机制进行的。 在最近的Android版本中,这是UI线程停止运行的最常见原因之一。 一般来说,修正是为了避免调用函数来调用binder; 如果这是不可避免的,那么应该缓存该值,或将工作移动到后台线程。 随着代码库变得越来越大,如果不小心的话,通过调用一些低级别的方法,很容易意外地添加了一个binder调用,但是使用跟踪来发现和修复它们也是很容易的。


    如果您有绑定事务,则可以使用以下adb命令来捕获其调用堆栈:

    adb 命令捕获堆栈信息


    有时像getRefreshRate()这样的无害的表面调用可能会触发绑定事务,并在频繁调用时导致严重的问题。 定期跟踪可以帮助您快速找到并解决这些问题。


    UI Thread sleeping due to binder transactions in a RV fling


    如果你没有看到绑定Activity,但仍然没有看到你的UI线程运行,请确保你没有等待来自另一个线程的锁定或其他操作。 通常,UI线程不应该等待来自其他线程的结果 - 其他线程应该向其发布信息post message.


    Object allocation and garbage collection


    对象分配和垃圾回收(GC)已经成为一个问题,因为ARTAndroid 5.0中默认运行时引入的,但是仍然有可能通过这些额外的工作来减轻你的线程负担。 对于每秒钟不会发生多次的罕见事件(如用户单击按钮)进行分配是很好的做法,但要记住,每次分配都需要付出一定的代价。 如果它处于一个频繁调用的紧密循环中,请考虑避免分配来减轻GC上的负载。


    Systrace会告诉你GC是否频繁运行,Android Memory Profiler可以显示你的分配来自哪里。 如果你可以避免分配,特别是在紧密的循环中,你应该没有问题。

    shows a 94ms GC on the HeapTaskDaemon thread


    在最新版本的Android上,GC通常在名为HeapTaskDaemon的后台线程上运行。 请注意,大量的分配可能意味着更多的CPU资源花费在GC上.



    至此,本篇已结束,如有不对的地方,欢迎您的建议与指正。同时期待您的关注,感谢您的阅读,谢谢!


    作者:程序员Android
    链接:https://juejin.cn/post/6844903912395243528
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Android组件化基础

    前言 公司包含三大业务线,每条业务线都有独立的app。功能模块难免会有重合~举个栗子,直播功能本来只在业务线A使用,但是由于业务拓展,现在业务线B和C也需要使用直播功能。这时候就有必要将直播功能做成一个独立的直播组件供三条业务线使用。 构思 既然要将直播做成组...
    继续阅读 »

    前言


    公司包含三大业务线,每条业务线都有独立的app。功能模块难免会有重合~举个栗子,直播功能本来只在业务线A使用,但是由于业务拓展,现在业务线B和C也需要使用直播功能。这时候就有必要将直播功能做成一个独立的直播组件供三条业务线使用。


    构思


    既然要将直播做成组件,需要考虑哪些方面呢?



    1. 既可独立运行,单独测试该组件功能;也可作为sdk,被其他项目使用

    2. 统一管理:部署到私有化仓库,其他项目可配置引用


    基础实践


    全局控制配置


    在gradle.properties中的配置可以在项目中直接使用


    # 是否作为module使用
    isModule=true

    build.gradle的配置



    1. 配置android构建插件


    if(isModule.toBoolean()){
    // lib
    apply plugin: 'com.android.library'
    }else{
    // 独立运行的app
    apply plugin: 'com.android.application'
    }


    1. 禁用applicationId配置


    作为library不能带有配置,否则编译会报错:Library projects cannot set applicationId. applicationId is set to 'com.example.live' in default config.


    android {
    ...
    defaultConfig {
    if(!isModule.toBoolean()){
    applicationId "com.example.live"
    }
    ...
    }

    AndroidManifest.xml的配置


    1. 独立运行


    为了可独立运行,需要配置application和启动Activity


    // 正常模板
    <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.TestAndroidManifest">
    <activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter>
    <action android:name="android.intent.action.MAIN" />

    <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    </activity>
    </application>

    2. module形式使用


    假如被其他项目作为组件使用,则需要修改application和启动入口配置


    // 去除application不必要的属性配置
    <application>
    // 去除intent-filter
    <activity
    android:name=".MainActivity"
    android:exported="true">
    </activity>
    </application>

    这里有两个问题:



    1. application里边的属性配置可以不去掉吗?


    其实在编译后,所有module的AndroidManifest会被合并到一起,假如相同属性配置不同会报错


    Manifest merger failed : Attribute application@name value=(com.example.moduledemo.MainApplication) from AndroidManifest.xml:7:9-40
    is also present at [:live] AndroidManifest.xml:11:9-56 value=(com.example.live.LiveApplication).
    Suggestion: add 'tools:replace="android:name"' to <application> element at AndroidManifest.xml:6:5-23:19 to override.

    这里我分别给和app-module和live-module指定了自定义appliation,提示合并失败了,解决方案需要通过在app-module配置tools:replace="android:name"。这里通过不同配置然后rebuild查看下输出的AndroidManifest.xml文件可以总结以下规律:



    • 假如只有一个module配置了自定义application,则直接使用该application

    • 假如每个module都配置了自定义application,则需要解决冲突。解决后会使用最后编译的那个module的application(举个例子:demo中,app-module依赖于live-module,假如都配置了自定义application,因为app后编译,所以最后会使用app-module里边定义的)



    1. activity里边的intent-filter可以不去掉吗?


    合并.png


    看到合并后的文件,里边包含了两个包含启动信息的activity。安装app时你会发现在桌面会有两个启动图标,并且点击他们的行为是一致的:打开第一个配置了MAIN和LAUNCHER的activity。因此是没有必要保留该配置的。


    3. 动态配置AndroidManifest


    根据上述的分析发现,作为module使用和独立app运行,相应的AndroidManifest.xml也需要相应的进行调整。那我们就有必要根据配置来配置使用不同的AndroidManifest文件了



    1. 在live-module增加用于sdk的AndroidManifest.xml


    module.png
    3. 在live-module的build.gradle配置动态引用不同的AndroidManifest.xml


    android {
    ...
    sourceSets {
    main {
    if(isModule){
    manifest.srcFile 'src/main/module/AndroidManifest.xml'
    }else{
    manifest.srcFile 'src/main/AndroidManifest.xml'
    }
    }
    }
    }

    总结


    至此,你已经可以通过修改gradle.properties里边的liModule来控制是否以library的形式使用live组件了。这里可以思考个问题,假如我们项目中有好几个类似于live这样的组件,是否每个组件都需要做这么繁琐的配置呢?能否将这些配置抽出来,统一管理?


    优化


    1. 抽取独立app构建脚本


    在项目根目录创建一个common_app_build.gradle


    apply plugin: 'com.android.application'

    android {
    compileSdk 31
    defaultConfig {
    minSdk 21
    targetSdk 31
    versionCode 1
    versionName "1.0"

    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
    release {
    minifyEnabled false
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
    }
    compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
    }

    sourceSets {
    main {
    manifest.srcFile 'src/main/AndroidManifest.xml'
    }
    }
    }

    2. 抽取构建library脚本


    在项目根目录创建一个common_library_build.gradle


    apply plugin: 'com.android.library'

    android {
    compileSdk 31
    defaultConfig {
    minSdk 21
    targetSdk 31
    versionCode 1
    versionName "1.0"

    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
    release {
    minifyEnabled false
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
    }
    compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
    }

    sourceSets {
    main {
    manifest.srcFile 'src/main/module/AndroidManifest.xml'
    }
    }
    }

    3. 在创建一个的course module(用于验证)


    4. 修改live和course两个module的build.gradle


    下边以live module为例


    // 直接通过配置引用不同的gradle文件,前边涉及的配置都可以去掉
    if (isModule.toBoolean()) {
    apply from: '../common_library_build.gradle'
    } else {
    apply from: '../common_app_build.gradle'
    }

    android {
    defaultConfig {
    if(!isModule.toBoolean()){
    applicationId "com.example.live"
    }
    }
    }

    后续类似的组件只需要进行简单的配置,即可实现第一点的构思


    module发布


    这里以live module为例进行实践,# google文档:使用 Maven Publish 插件


    发布live module到本地仓库


    再live module的build.gradle增加以下配置


    afterEvaluate {
    publishing {
    repositories {
    maven {
    url uri("../repo")
    }
    }
    publications {
    maven(MavenPublication) {
    from components.release
    groupId "com.example.live"
    artifactId "modulelive"
    version "1.0.0"
    }
    }
    }
    }

    上述配置,指定将live发布到 项目/repo/ 目录下。sync完成后,会在live出现publish task


    maven.png


    双击publish,即会在repo生成相应的aar文件


    aar.png


    配置根build.gradle


    为了可以使用repo里边的aar,需要增加配置


    buildscript {
    repositories {
    ...
    maven {
    url('repo')
    }
    }
    ...
    }

    app中使用:配置build.gradle


    dependencies {
    ...
    // 不直接引用project
    // api project(':live')
    // 改为该配置
    implementation 'com.example.live:modulelive:1.0.0'
    ...
    }

    重新rebuild就可以正常使用到live组件。


    发布到远程仓库


    因为不同业务线项目环境不同,发布到本地项目目录下,使用比较不方便吗。所以可以考虑将组件发布到公司内部的私有仓库,供所有项目组使用:


    publishing {
    ...
    repositories {
    maven {
    // 仓库地址
    url = "http://...."
    // 仓库用户名及密码
    credentials {
    username ''
    password ''
    }
    }
    }
    }

    总结


    上述主要是讲述了Android组件化的一些基础以及如何发布组件的一些流程。当然,组件化的内容不止这些内容,包括:



    • 组件间通信

    • 组件间跳转

    • 组件化混淆

    • 组件资源冲突

    • .....


    这些方面都是在进行组件化设计需要思考与处理的~后续逐渐完善这块的内容


    gitee:Demo地址


    作者:king_nina
    链接:https://juejin.cn/post/7028842367960481806
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Android加载长图方案

    背景介绍 在某些特定场景下,我们需要考虑加载长图的需求,比如加载一幅《清明上河图》,这个好像有点过分了,那就加载1/2的《清明上河图》吧... 那TMD还不是一样道理。 言归正传说一下我这边遇到的情况,之前有图片或大图的模块是划分为H5来实现的,现在需求变更划...
    继续阅读 »

    背景介绍


    在某些特定场景下,我们需要考虑加载长图的需求,比如加载一幅《清明上河图》,这个好像有点过分了,那就加载1/2的《清明上河图》吧... 那TMD还不是一样道理。


    言归正传说一下我这边遇到的情况,之前有图片或大图的模块是划分为H5来实现的,现在需求变更划分为原生开发,那么问题就来了。


    图片尺寸为


    image.png


    图片大小为


    image.png


    这一刻我是懵逼的,哪个端图片上传的时候没限制尺寸和压缩?mdzz,
    吐槽归吐槽,还是要撸起袖子解决加载长图大图的问题。
    先提供几个技术方案来对比一下:


    方案1:WebView加载渲染

    因为图片本身也是一个URL地址,也是被WebView渲染,并且支持缩放。这是一种实现方案,遇到几M的大图WebView也是会崩溃Crash,所以这种投机的方式并不推荐。


    方案2:BitmapRegionDecoder

    分片加载,使用系统BitmapRegionDecoder去加载本地的图片,调用bitmapRegionDecoder.decodeRegion解析图片的矩形区域,返回bitmap,最终显示在ImageView上。这种方案需要手动处理滑动、缩放手势,网络图片还要处理缓存策略等问题。实现方式比较繁琐也不是很推荐。


    方案3:SubsamplingScaleImageView

    一款封装BitmapRegionDecoder的三方库,已经处理了滑动,缩放手势。我们可以考虑选择这个库来进行加载长图,但是官方上的Demo示例加载的长图均为本地图片。这可能并不符合我们的网络场景需求,所以对于网络图片,我们还要考虑不同的加载框架,


    SubsamplingScaleImageView Git传送门

    方案4:Glide+SubsamplingScaleImageView混合加载渲染

    对于图片加载框架,Glide当然是首选,我们使用Glide进行网络图片的下载和缓存管理,FileTarget作为桥梁,SubsamplingScaleImageView进行本地资源图片的分片加载,看起来很靠谱,那么一起来实现吧。


    Glide Git传送门

    SubsamplingScaleImageView Git传送门

    fun loadLargeImage(context: Context, res: String, imageView: SubsamplingScaleImageView) {
    imageView.isQuickScaleEnabled = true
    imageView.maxScale = 15F
    imageView.isZoomEnabled = true
    imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)

    Glide.with(context).load(res).downloadOnly(object : SimpleTarget<File?>() {
    override fun onResourceReady(resource: File, glideAnimation: Transition<in File?>?) {

    val options = BitmapFactory.Options()
    options.inJustDecodeBounds = true
    BitmapFactory.decodeFile(resource.absolutePath, options)
    val sWidth = options.outWidth
    val sHeight = options.outHeight
    options.inJustDecodeBounds = false
    val wm = ContextCompat.getSystemService(context, WindowManager::class.java)
    val width = wm?.defaultDisplay?.width ?: 0
    val height = wm?.defaultDisplay?.height ?: 0
    if (sHeight >= height
    && sHeight / sWidth >= 3) {
    imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP)
    imageView.setImage(ImageSource.uri(Uri.fromFile(resource)), ImageViewState(0.5f, PointF(0f, 0f), 0))
    } else {
    imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
    imageView.setImage(ImageSource.uri(Uri.fromFile(resource)))
    imageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER_IMMEDIATE)
    }
    }
    override fun onLoadFailed(errorDrawable: Drawable?) {
    super.onLoadFailed(errorDrawable)
    }
    })

    }

    这是我封装起来的一个方法,就很简单就能理解了, 包括SubsamplingScaleImageView的缩放设置,默认展示状态、缩放、位置,计算当前图片高宽比为3倍进行长图渲染处理,否则按正常图片渲染处理。


    最后快用下面的这张完整版《清明上河图》来试一试效果吧~ 赞


    清明上河图_简书_爱吃大蒜.jpeg


    如果我帮助你成功实现了加载长图的需求,千万要记得回来点赞哦,ღ( ´・ᴗ・` )比心

    作者:rivenlee
    链接:https://juejin.cn/post/6955427322291814431
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Android自定义view,实现电子签名

    首先new一个类继承于Viewpublic class SignatureView extends View 自定义view,采用画笔绘制一张图片 定义一个画笔滑动的宽度 还需要对画笔进行跟踪,以便内容区域可以容纳笔划private static final...
    继续阅读 »

    首先new一个类继承于View

    public class SignatureView extends View

    自定义view,采用画笔绘制一张图片 定义一个画笔滑动的宽度 还需要对画笔进行跟踪,以便内容区域可以容纳笔划

    private static final float STROKE_WIDTH = 5f;
    private static final float HALF_STROKE_WIDTH = STROKE_WIDTH / 2;

    通过使最小可能区域无效来优化绘制

    private Paint paint = new Paint();
    private Path path = new Path();

    private float lastTouchX;
    private float lastTouchY;
    private final RectF dirtyRect = new RectF();

    构造方法里面初始化画笔

    public SignatureView(Context context, AttributeSet attrs) {
    super(context, attrs);

    paint.setAntiAlias(true);
    paint.setColor(Color.BLACK);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeJoin(Paint.Join.ROUND);
    paint.setStrokeWidth(STROKE_WIDTH);
    }

    处理手势,触发画笔路径

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    float eventX = event.getX();
    float eventY = event.getY();

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
    path.moveTo(eventX, eventY);
    lastTouchX = eventX;
    lastTouchY = eventY;
    // 现在还没有终点,所以不要浪费周期使其失效。
    return true;

    case MotionEvent.ACTION_MOVE:
    case MotionEvent.ACTION_UP:
    //开始跟画笔区域。
    resetDirtyRect(eventX, eventY);

    // 当硬件跟踪事件的速度快于事件的交付速度时
    // 事件将包含这些跳过点的历史记录。
    int historySize = event.getHistorySize();
    for (int i = 0; i < historySize; i++) {
    float historicalX = event.getHistoricalX(i);
    float historicalY = event.getHistoricalY(i);
    expandDirtyRect(historicalX, historicalY);
    path.lineTo(historicalX, historicalY);
    }

    // 回放历史记录后,将线路连接到触点。
    path.lineTo(eventX, eventY);
    break;

    default:

    return false;
    }

    // 包括一半笔划宽度以避免剪裁
    invalidate(
    (int) (dirtyRect.left - HALF_STROKE_WIDTH),
    (int) (dirtyRect.top - HALF_STROKE_WIDTH),
    (int) (dirtyRect.right + HALF_STROKE_WIDTH),
    (int) (dirtyRect.bottom + HALF_STROKE_WIDTH));

    lastTouchX = eventX;
    lastTouchY = eventY;

    return true;
    }
    @Override
    protected void onDraw(Canvas canvas) {
    canvas.drawPath(path, paint);
    }

    抬起时调用,以确包含所有路径

    private void expandDirtyRect(float historicalX, float historicalY) {
    if (historicalX < dirtyRect.left) {
    dirtyRect.left = historicalX;
    } else if (historicalX > dirtyRect.right) {
    dirtyRect.right = historicalX;
    }
    if (historicalY < dirtyRect.top) {
    dirtyRect.top = historicalY;
    } else if (historicalY > dirtyRect.bottom) {
    dirtyRect.bottom = historicalY;
    }
    }

    到这里,手写功能就已经能使用了,接下来是做一些处理

    在运动事件发生时重置
    lastTouchX和lastTouchY是在动作结束时设置的

    private void resetDirtyRect(float eventX, float eventY) {
    dirtyRect.left = Math.min(lastTouchX, eventX);
    dirtyRect.right = Math.max(lastTouchX, eventX);
    dirtyRect.top = Math.min(lastTouchY, eventY);
    dirtyRect.bottom = Math.max(lastTouchY, eventY);
    }

    清除签名

    那写错了怎么办,肯定要有清除签名啦

    public void clear() {
    path.reset();
    // 重新绘制整个视图
    invalidate();
    }

    获取图片缓存

    public Bitmap getBitmapFromView(){
    this.setDrawingCacheEnabled(true); //开启图片缓存
    buildDrawingCache(); //构建图片缓存
    Bitmap bitmap = Bitmap.createBitmap(getDrawingCache());
    setDrawingCacheEnabled(false); //关闭图片缓存
    return bitmap;
    }

    效果图

    微信图片_20211115231808.jpg


    收起阅读 »

    滴滴DoKit For Flutter正式开源,还是熟悉的"配方"

    社区的小伙伴们,大家好啊,艰难的2020已经过去,随着2021的到来,在新的一年里希望大家都能够需求不变更、准点上下班、代码0 Bug。同时我们DoKit团队也给大家准备了一份特殊的“新年礼物”,没错它就是DoKit For Flutter,它来啦,它带着熟悉...
    继续阅读 »

    社区的小伙伴们,大家好啊,艰难的2020已经过去,随着2021的到来,在新的一年里希望大家都能够需求不变更、准点上下班、代码0 Bug。同时我们DoKit团队也给大家准备了一份特殊的“新年礼物”,没错它就是DoKit For Flutter,它来啦,它带着熟悉的配方来啦。接下来就让我们揭开它神秘的面纱吧。


    背景


    Flutter是Google开源的跨端技术框架。凭借其区别于RN/Weex的自渲染模式,在社区里引起了广泛关注,不管是终端还是前端的小伙伴都趋之若鹜,大有一统大前端江湖的气势。而国内大厂如闲鱼、字节、美团等,也都在其核心业务上完成了落地。


    滴滴做为国内最大的出行平台,早在两年前就有多个内部团队开始在Flutter领域进行尝试。但是在开发过程中,我们遇到了很多调试性问题,如日志、帧率、抓包等。为了解决这些开发测试过程中遇到的各类问题,我们DoKit团队联合滴滴代驾和货运团队,把平时工作过程中沉淀下来的效率工具进行业务剥离和脱敏,并最终打造出DoKit For Flutter,在服务内部业务的同时,也为社区贡献一份力量,这也是滴滴的开源精神。


    介绍


    DoKit For Flutter是一个DoKit针对Flutter环境的产研工具包,内部集成了各种丰富的小工具,UI、网络、内存、监控等等。DoKit始终站在用户的角度,为用户提供最便利的产研工具。


    Github地址


    Pub仓库地址


    操作文档



    图片名称

    那么接下来就让我来列举一下DoKit For Flutter的功能以及核心实现。


    工具详解


    基本信息


    基本信息模块会展示当前dart虚拟机进程、CPU、Flutter版本信息、当前App包名和dart工程构建版本信息;



    图片名称

    VM信息通过VMService获取。Flutter版本实际上是通过Devtools服务注入的"flutterVersion"方法获取到的,在flutter attach后,本地会起一个websocket服务,连接VMService并注入flutterVersion和其余方法(HotReload、HotRestart等),通过VMService调用flutterVersion方法,会从本地flutter sdk目录下解析version文件返回版本号。


    路由信息



    图片名称

    在Flutter中,每个页面对应一个Route,通过Navigator管理Route。Navigator内部会包含一个Overlay Widget,每个Route最终都转化成一个_OverlayEntryWidget添加到Overlay上。这个地方可以把Overlay理解为Android中的FrameLayout,内部子View上下叠加。每打开一个新的Route,都相当于往FrameLayout添加一个新的子View。Navigator会存在嵌套的情况,即Route所创建的页面本身也包含一个Navigator,比如App的根Widget是MaterialApp(自带Navigator),Route页面也用MaterialApp包裹,就会形成Navigator嵌套的情况。还是以FrameLayout来理解,这也就相当于嵌套的FrameLayout。
    路由信息功能会打印出当前栈顶页面所处的Route信息,如果存在Navigator嵌套的情况,也会向上遍历打印出每层Navigator的信息。具体的实现方式是,先获取当前根app根Element,可以使用WidgetsBinding.instance.renderViewElement作为根Element,再通过递归调用element的visitChildElements方法,向下遍历整棵树找到最后一个RenderObejctElement,该RenderObejctElement即为当前显示的页面上的元素。然后使用ModalRoute.of(element)方法即可获取到当前页面的路由信息。


    至于嵌套的路由信息,则可以通过找到的RenderObejctElement的findAncestorStateOfType方法,反向向上递归遍历,获得所处的Navigator的NavigatorState,再调用ModalRoute.of(navigatorState.context),如果返回不为空则表示存在嵌套。


    方法通道



    图片名称

    Flutter的Method Channel调用最终都会经过ServiceBinding.instance._defaultBinaryMessenger这个对象,类型为BinaryMessenger,由于这个对象是个私有对象,无法动态进行修改。不过查看ServiceBinding的源码可以发现这个对象是通过ServiceBinding.createBinaryMessenger方法创建的,通过使用flutter的mixins,可以实现对该方法的重写。
    我们知道,ServiceBinding实际也是通过mixins在WidgetsFlutterBinding.ensureInitialized方法中一起被初始化的,所以只要在WidgetsFlutterBinding这个类额外mixin一个继承于ServiceBinding并且重写了createBinaryMessenger方法的类,就能实现对ServiceBinding中createBinaryMessenger的覆盖,代码如下:


    class DoKitWidgetsFlutterBinding extends WidgetsFlutterBinding
    with DoKitServicesBinding {
    static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null) DoKitWidgetsFlutterBinding();
    return WidgetsBinding.instance;
    }
    }

    mixin DoKitServicesBinding on BindingBase, ServicesBinding {
    @override
    BinaryMessenger createBinaryMessenger() {
    return DoKitBinaryMessenger(super.createBinaryMessenger());
    }
    }

    接下去把runApp的入口调用改成如下,就能实现BinaryMessenger的替换
    static void _runWrapperApp(DoKitApp wrapper) {
    DoKitWidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(wrapper)
    ..scheduleWarmUpFrame();
    }
    至于Method Channel具体信息的捕获,只要hook住BinaryMessenger.handlePlatformMessage和BinaryMessenger.send两个方法就行了,具体可看DoKitBinaryMessenger这个类


    控件检查



    图片名称

    和路由功能类似,通过从根element向下遍历,在遍历过程中记录和选中的View有交集的所有RendereObjectElement,并且记录用以标志当前页面的RendereObjectElement,获取它的Route信息。遍历完成后,遍历记录下来的RendereObjectElement,过滤掉Route信息和当前页面不一致的,这些Element属于被遮盖住的页面。然后通过比对RendereObjectElement和选中View的交叉区域面积占RendereObjectElement面积的比例,占比最大的为当前选中的组件。
    在Debug模式下可以获取选中组件在工程中的代码位置,将WidgetInspectorService.instance.selection.current赋值为选中element的renderObject,再调用WidgetInspectorService.instance.getSelectedSummaryWidget方法,会返回一个json字符串,解析这个字符串就能获取源码文件名、行列信息等。


    日志查看



    图片名称

    日志查看功能比较简单,只要使用runZoned方法替代runApp,传入zoneSpecification,就能为日志输出设置一个代理函数,在这个代理函数内进行日志捕获,同时,还可以为onError设置一个代理函数,在这里将捕获的异常也会传入到日志当中。


    帧率



    图片名称

    使用WidgetsBinding.instance.addTimingsCallback可以统计帧率信息,在每帧渲染完成时会触发回调,包含该帧渲染的信息。


    内存



    图片名称

    同VM信息,使用VMService可以获取到内存详细使用信息。


    网络请求



    图片名称

    Flutter自带的网络请求通过HttpClient类发送,只要hook住HttpClient的创建就可以hook整个网络请求的过程。查看HttpClient的构造函数可以发现,如果存在HttpOverrides,就会使用HttpOverrids来创建HttpClient


    factory HttpClient({SecurityContext? context}) {
    HttpOverrides? overrides = HttpOverrides.current;
    if (overrides == null) {
    return new _HttpClient(context);
    }
    return overrides.createHttpClient(context);
    }
    所以这里重写了一个HttpOverrids
    class DoKitHttpOverrides extends HttpOverrides {
    final HttpOverrides origin;

    DoKitHttpOverrides(this.origin);

    @override
    HttpClient createHttpClient(SecurityContext context) {
    if (origin != null) {
    return DoKitHttpClient(origin.createHttpClient(context));
    }
    // 置空,防止递归调用,使得_HttpClient可以被初始化
    HttpOverrides.global = null;
    HttpClient client = DoKitHttpClient(new HttpClient(context: context));
    // 创建完成后继续置回DoKitHttpOverrides
    HttpOverrides.global = this;
    return client;
    }
    }

    替换HttpOverrides


    HttpOverrides origin = HttpOverrides.current;
    HttpOverrides.global = new DoKitHttpOverrides(origin);

    hook住HttpClient方法后,对于请求和返回结果的hook过程就和Android中的HttpUrlConnection类似了,具体可以看DoKitHttpClient、DoKitHttpClientRequest、DoKitHttpClientResponse三个类。


    版本API兼容


    Flutter版本更新还是比较快的,每一个大版本更新都会带来一些API的变更,目前DoKit的方案需要重写一些framework层的类,在兼容多版本时就会有一些问题。以上面的BinaryMessager为例,1.17版本只有四个方法,用来hook的DoKitBinaryMessager是这么写的


    class DoKitBinaryMessenger extends BinaryMessenger {
    final MethodCodec codec = const StandardMethodCodec();
    final BinaryMessenger origin;

    DoKitBinaryMessenger(this.origin);

    @override
    Future<void> handlePlatformMessage(String channel, ByteData data, callback) {
    ChannelInfo info = saveMessage(channel, data, false);
    PlatformMessageResponseCallback wrapper = (ByteData data) {
    resolveResult(info, data);
    callback(data);
    };
    return origin.handlePlatformMessage(channel, data, wrapper);
    }

    @override
    Future<ByteData> send(String channel, ByteData message) async {
    ChannelInfo info = saveMessage(channel, message, true);
    ByteData result = await origin.send(channel, message);
    resolveResult(info, result);
    return result;
    }

    @override
    void setMessageHandler(
    String channel, Future<ByteData> Function(ByteData message) handler) {
    origin.setMessageHandler(channel, handler);
    }

    @override
    void setMockMessageHandler(
    String channel, Future<ByteData> Function(ByteData message) handler) {
    origin.setMockMessageHandler(channel, handler);
    }
    }

    用来hook的wrapper类需要调用oring对象的同名方法。但在1.20版本BinaryMessager增加了两个新方法checkMessageHandler和checkMockMessageHandler,如果使用1.17.5版本的flutter sdk去编译,就无法调用origin.checkMessageHandler方法,因为不存在;如果使用1.20.4版本的flutter sdk去编译,编译和发布没问题,但编出来的sdk在1.17.5的工程被引用后,也会因为checkMessageHandler方法不存在导致编译失败。
    针对这种多个Flutter版本API不同导致的兼容性问题,可以使用扩展方法extension关键字来解决。
    建立一个_BinaryMessengerExt类如下:


    extension _BinaryMessengerExt on BinaryMessenger {
    bool checkMessageHandler(String channel, MessageHandler handler) {
    return this.checkMessageHandler(channel, handler);
    }

    bool checkMockMessageHandler(String channel, MessageHandler handler) {
    return this.checkMockMessageHandler(channel, handler);
    }
    }

    在1.17.5版本,调用origin.checkMessageHandler会走到扩展方法的checkMessageHandler中,编译能通过,由于这个方法在1.17.5中是绝对不会被调用到的,虽然会形成递归调用,但没影响。而在1.20版本,BinaryMessenger本身实现了checkMessageHandler方法,所以调用checkMessageHandler方法会走到BinaryMessenger的checkMessageHandler方法中,也能正常使用。
    通过extentsion,只要以最低兼容版本的类作为基础,在扩展类中定义新版本中新增的API,就能解决多版本API兼容的问题。


    总结


    以上就是DoKit For Flutter的现有功能以及工具的基本原理介绍。 我们知道当前它的功能还不是完善,后续我们会继续不断深入的挖掘业务中的痛点并持续输出各种提高用户效率的工具,努力让DoKit For Flutter变得更加优秀,符合大家的期望。


    DoKit一直追求给开发者提供最便捷和最直观的开发体验,同时我们也十分欢迎社区中能有更多的人参与到DoKit的建设中来并给我们提出宝贵的意见或PR。 DoKit的未来需要大家共同的努力。


    作者:DoKit
    链接:https://juejin.cn/post/6914111076099915784
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    什么?你还不会用位运算来操作状态?

    回顾首先来回顾一下位运算,什么是位运算呢?位运算就是直接对整数在内存中的二进制位进行操作。在 Java 语言中,位运算有如下这些:左移(<<)。右移(>>)。无符号右移(>>>)。与(&)。或(|)。非(~)。...
    继续阅读 »

    回顾

    首先来回顾一下位运算,什么是位运算呢?

    位运算就是直接对整数在内存中的二进制位进行操作。

    在 Java 语言中,位运算有如下这些:

    • 左移(<<)。

    • 右移(>>)。

    • 无符号右移(>>>)。

    • 与(&)。

    • 或(|)。

    • 非(~)。

    • 异或(^)。

    在本篇文章中,我们所需要用到的有如下几个(其他的后续文章再讲):

    • &(与运算):只有当两方都为 true 时,结果才是 true,否则为 false。

    • |(或运算):只要当一方为 true 时,结果就是 true,否则为 false。

    • ^(异或运算):只要两方不同,结果就是 true,否则为 false。

    以 true、false 为例:


    true & true = true

    true & false = false



    true | false = true;

    false | false = false;



    true ^ true = false;

    true ^ false = true;

    以数字运算为例:


    6 & 4 = ?

    6 | 4 = ?

    6 ^ 4 = ?

    当以数字运算时,我们首先需要知道这些数字的二进制,假设 6 是 int 类型,那么其二进制如下:

    00000000 00000000 00000000 00000110

    在 Java 中,int 占了 4 个字节(Byte),一个字节呢又等于 8 个 Bit 位。所以 int 类型的二进制表现形式如上。

    在这里为方便讲解,直接取后 8 位:00000110。

    4 的二进制码如下:


    00000100

    在二进制码中,1 为 true,0 为 false,根据这个,我们再来看看 6 & 4 的运算过程:


    00000110

    00000100

    -----------

    00000100

    对每位的数进行运算后,结果为 4。

    再来看看 | 运算:


    6 | 4 = ?

    6 和 4 的二进制上面已经说了:


    00000110

    00000100

    -----------

    00000110

    可以发现最后的结果是 6。

    最后再来看看 ^ 运算:


    6 ^ 4 = ?


    00000110

    00000100

    -----------

    00000010

    结果是 2。

    应用

    通过上面的例子,我们已经回顾了 & 、 | 以及 ^ 运算。现在来将它应用到实际的应用中。

    假如我们现在要定义一个人的模型,这个人可能会包含有多种性格,比如说什么乐观型、内向型啦...

    要是想要知道他包含了哪种性格,那么我们该如何判断呢?

    可能在第一时间会想到:


    if(这个人是乐观性){

    ....

    }else if(这个人是内向型){

    ...

    }

    那么如果有很多种性格呢?一堆判断写起来真的是很要命..

    下面就来介绍一种更简单的方式。首先来定义一组数:


    public static final int STATUS_NORMAL = 0;
    public static final int STATUS_OPTIMISTIC = 1;
    public static final int STATUS_OPEN = 2;
    public static final int STATUS_CLOSE = 4;

    把它们转换为二进制:


    0000 0000 0000 0000
    0000 0000 0000 0001
    0000 0000 0000 0010
    0000 0000 0000 0100

    发现其中二进制的规律没有?都是 2 的次幂,并且二进制都只有一个为 1 位,其他都是 0 !

    然后再来定义一个变量,用于存储状态(默认值是 0):


    private static int mStatus = STATUS_NORMAL;

    当我们要保存状态时,直接用 | 运算即可:


    mStatus |= STATUS_OPTIMISTIC;

    保存的运算过程如下:


    00000000

    执行 | 运算(只要有 1 则为 1)

    00000001

    -----------

    00000001 = 1

    相当于就把这个 1 存储到 0 的二进制当中了。

    那么如果要判断 mStatus 中是否有某个状态呢?使用 & 运算:


    System.out.println((mStatus & STATUS_OPTIMISTIC) != 0);// true,代表有它

    计算过程如下:


    00000001

    执行 & 运算(都为 1 才为 1)

    00000001

    -----------

    00000001 = 1

    再来判断一个不存在的状态 mStatus & STATUS_OPEN


    System.out.println((mStatus & STATUS_OPEN) != 0);// false,代表没有它

    计算过程如下:


    00000001

    00000010

    -----------

    00000000 = 0

    可以发现,因为 STATUS_OPEN 这个状态的二进制位,1 的位置处,mStatus 的二进制并没有对于的 1,而又因为其他位都是 0,导致全部归 0,计算出来的结果自然也就是 0 了。

    这也就是为什么定义状态的数字中,是 1、2、4 这几位数了,因为他们的特定就是二进制只有一个为 1 的位,其他位都是 0,并同其他数位 1 的位不冲突。

    如果换成其他的数,就会有问题了。比如说 3:


    mStatus |= 3

    计算过程:


    00000000

    00000011

    -----------

    00000011 = 3

    运算完毕,这时候 mStatus 中已经存储了 3 这个值了,我们再来判断下是否存在 2:


    System.out.println((mStatus & 2) != 0);// true,代表有它,但是其实是没有的

    00000011

    00000010

    -----------

    00000010 = 2

    结果是 true,但是其实我们只存储了 3 到 mStatus 中,结果肯定是错误的。

    所以我们在定义的时候,一定不要手滑定义错了数字。

    存储和判断已经说了,那么如何取出呢?这时候就要用到 ^ 运算了。

    假如现在 mStatus 中已经存储了 STATUS_OPTIMISTIC 状态了,要把它给取出来,这样写即可:


    mStatus ^= STATUS_OPTIMISTIC

    其中的运算过程:


    00000001

    执行 ^ 运算,两边不相同,则为 true

    00000001

    -----------

    00000000

    可以看到状态又回到了最初没有存储 STATUS_OPTIMISTIC 状态的时候了。

    最后再来看一个取出的例子,这次是先存储两个状态,然后再取出其中一个:


    mStatus |= STATUS_OPTIMISTIC

    mStatus |= STATUS_OPEN

    存储完后,mStatus 的二进制为:


    00000011

    再来取出 STATUS_OPEN 这个状态:


    mStatus ^= STATUS_OPEN

    运算过程:


    00000011

    00000010

    -----------

    00000001

    mStatus 现在就只有 STATUS_OPTIMISTIC 的状态了。

    总结

    通过 |、^、& 运算,我们可以很方便快捷的对状态值进行操作。当然,位运算的应用不仅限于状态值,知道了其中的二进制运算原理后,还有更多的其他应用场景,等着你去发现。


    作者:Airsaid
    链接:https://juejin.cn/post/6844903909333401607
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    Android 多渠道打包看这一篇就够了

    Android 多渠道打包看这一篇就够了 本文三个流程 一、多渠道配置 1、多渠道配置 2、不同渠道不同签名配置 3、不同渠道不同资源文件配置 4、不同渠道不同依赖配置 二、注意事项 三、打包 1、命令行打包 2、IDE 打包 多渠道配置(2 种方式) 1、可...
    继续阅读 »

    Android 多渠道打包看这一篇就够了


    本文三个流程


    一、多渠道配置


    1、多渠道配置

    2、不同渠道不同签名配置

    3、不同渠道不同资源文件配置

    4、不同渠道不同依赖配置

    二、注意事项


    三、打包


    1、命令行打包


    2、IDE 打包


    多渠道配置(2 种方式)


    1、可写在主模块(app)的 build.gradle 下


    android {  
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
    applicationId "com.test.moduledemo"
    minSdkVersion 21
    targetSdkVersion 29
    versionCode 1
    versionName "1.0"
    }

    flavorDimensions "versionCode"

    productFlavors {
    xiaomi{
    applicationId = “com.test.xiaomi"
    //不同渠道配置不同参数
    buildConfigField "int", "TEST_VALUE", "1"
    buildConfigField "String", "TEST_NAME", "\"xiaomi\""
    }
    huawei{
    applicationId = "com.test.huawei"
    //不同渠道配置不同参数
    buildConfigField "int", "TEST_VALUE", "2"
    buildConfigField "String", "TEST_NAME", "\"huawei\""
    }
    productFlavors.all {//遍历productFlavors多渠道,设置渠道号(xiaomi 、huawei)
    flavor -> flavor.manifestPlaceholders.put("CHANNEL", name)
    }
    }
    applicationVariants.all { variant ->
    // 打包完成后输出路径
    def name = ((project.name != "app") ? project.name : rootProject.name.replace(" ", "")) +
    "_" + variant.flavorName +
    "_" + variant.buildType.name +
    "_" + variant.versionName +
    "_" + new Date().format('yyyyMMddhhmm') + ".apk"
    //相对路径app/build/outputs/apk/huawei/release/
    def path = "../../../../../apk/" //相当于路径 app/apk/
    variant.outputs.each { output ->
    def outputFile = output.outputFile
    if (outputFile != null && outputFile.name.endsWith('.apk')) {
    //指定路径输出
    output.outputFileName = new File(path, name)
    }
    }
    // 在打包完成后还可以做一些别的操作,可以复制到指定目录,或者移动文件到指定目录
    variant.assemble.doLast {
    File out = new File(“${project.rootDir}/apk”)
    variant.outputs.forEach { file ->
    //复制apk到指定文件夹
    //copy {
    // from file.outputFile
    // into out
    //}
    //把文件移动到指定文件夹
    ant.move file: file.outputFile,
    todir: "${project.rootDir}/apk"
    }
    }
    }
    //多渠道签名的配置
    signingConfigs {
    test {
    storeFile file("../test.keystore")
    storePassword 'test'
    keyAlias 'test'
    keyPassword 'test'
    v1SigningEnabled true
    v2SigningEnabled true
    }
    xiaomi {
    storeFile file("../xiaomi.keystore")
    storePassword 'xiaomi'
    keyAlias 'xiaomi'
    keyPassword 'xiaomi'
    v1SigningEnabled true
    v2SigningEnabled true
    }
    huawei {
    storeFile file("../huawei.keystore")
    storePassword 'huawei'
    keyAlias 'huawei'
    keyPassword 'huawei'
    v1SigningEnabled true
    v2SigningEnabled true
    }
    }
    buildTypes {
    debug {
    // debug这里设置不起作用,可能是编译器的问题?
    // productFlavors.xiaomi.signingConfig signingConfigs.test
    // productFlavors.huawei.signingConfig signingConfigs.test
    }
    release {
    productFlavors.xiaomi.signingConfig signingConfigs.xiaomi
    productFlavors.huawei.signingConfig signingConfigs.huawei
    }
    }
    //不同渠道不同资源文件配置
    sourceSets{
    xiaomi.res.srcDirs 'src/main/res-xiaomi'
    huawei.res.srcDirs 'src/main/res-huawei'
    }
    //不同渠道不同的依赖文件
    dependencies {
    xiaomiApi('xxxxxxx')
    huaweiImplementation('xxxxxxxx')
    }
    }

    2、在项目根目录下(与settings.gradle同目录)新建 flavors.gradle 文件


     android {  
    flavorDimensions "versionCode"

    productFlavors {
    xiaomi{
    applicationId = "com.test.xiaomi"
    //不同渠道配置不同参数
    buildConfigField "int", "TEST_VALUE", "1"
    buildConfigField "String", "TEST_NAME", "\"xiaomi\""
    }
    huawei{
    applicationId = "com.test.huawei"
    //不同渠道配置不同参数
    buildConfigField "int", "TEST_VALUE", "2"
    buildConfigField "String", "TEST_NAME", "\"huawei\""
    }
    productFlavors.all {//遍历productFlavors多渠道,设置渠道号(xiaomi 、huawei)
    flavor -> flavor.manifestPlaceholders.put("CHANNEL", name)
    }
    }
    // ............ 更多配置
    }

    在主模块(app)的 build.gradle 下引用该 flavors.gradle 文件即可
    apply from: rootProject.file('flavors.gradle')


    注意


    如果项目较为复杂,有可能通过 buildConfigField 设置不同的渠道包,不同的信息字段有可能失效,则把
    buildConfigField "int", "TEST_VALUE", "1"
    换成
    manifestPlaceholders.put("TEST_VALUE", 1)
    然后再 AndroidManifest.xml 里添加


    <application>
    <meta-data
    android:name="TEST_VALUE"
    android:value="${TEST_VALUE}" />
    </application>

    在 代码通过一下操作获取其值:


    ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(getPackageName(),  
    PackageManager.GET_META_DATA);
    int testValue = applicationInfo.metaData.getInt("TEST_VALUE");

    打包


    命令行打包:


    Windows下: gradlew assembleRelease
    Mac 下:./gradlew assembleRelease
    assembleRelease 是打所有渠道的 Release 包
    assembleDebug 是打所有渠道的 Debug 包
    还可以打指定渠道的包:
    gradlew assembleXiaoMiRelease assembleHuaWeiRelease
    (空格隔开要打的渠道包的任务名称即可,任务名称可以通过点击 android studio 右边的 Gradle 根据图中目录查看)



    编译器打包





    当渠道很多的时候,不同渠道不同配置就会变得相当繁琐了,欢迎查看我的下一篇推文多渠道打包-进阶


    作者:ASongSenior
    链接:https://juejin.cn/post/6948613840858775588
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    如何优雅的在java中统计代码块耗时

    在我们的实际开发中,多多少少会遇到统计一段代码片段的耗时的情况,我们一般的写法如下 long start = System.currentTimeMillis(); try { // .... 具体的代码段 } finally { System...
    继续阅读 »

    在我们的实际开发中,多多少少会遇到统计一段代码片段的耗时的情况,我们一般的写法如下


    long start = System.currentTimeMillis();
    try {
    // .... 具体的代码段
    } finally {
    System.out.println("cost: " + (System.currentTimeMillis() - start));
    }

    上面的写法没有什么毛病,但是看起来就不太美观了,那么有没有什么更优雅的写法呢?



    1. 代理方式


    了解 Spring AOP 的同学可能立马会想到一个解决方法,如果想要统计某个方法耗时,使用切面可以无侵入的实现,如


    // 定义切点,拦截所有满足条件的方法
    @Pointcut("execution(public * com.git.hui.boot.aop.demo.*.*(*))")
    public void point() {
    }

    @Around("point()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();
    try{
    return joinPoint.proceed();
    } finally {
    System.out.println("cost: " + (System.currentTimeMillis() - start));
    }
    }

    Spring AOP 的底层支持原理为代理模式,为目标对象提供增强功能;在 Spring 的生态体系下,使用 aop 的方式来统计方法耗时,可以说少侵入且实现简单,但是有以下几个问题



    2. AutoCloseable


    在 JDK1.7 引入了一个新的接口AutoCloseable, 通常它的实现类配合try{}使用,可在 IO 流的使用上,经常可以看到下面这种写法


    // 读取文件内容并输出
    try (Reader stream = new BufferedReader(new InputStreamReader(new FileInputStream("/tmp")))) {
    List<String> list = ((BufferedReader) stream).lines().collect(Collectors.toList());
    System.out.println(list);
    } catch (IOException e) {
    e.printStackTrace();
    }

    注意上面的写法中,最值得关注一点是,不需要再主动的写stream.close了,主要原因就是在try(){}执行完毕之后,会调用方法AutoCloseable#close方法;


    基于此,我们就会有一个大单的想法,下一个Cost类实现AutoCloseable接口,创建时记录一个时间,close 方法中记录一个时间,并输出时间差值;将需要统计耗时的逻辑放入try(){}代码块


    下面是一个具体的实现:


    public static class Cost implements AutoCloseable {
    private long start;

    public Cost() {
    this.start = System.currentTimeMillis();
    }

    @Override
    public void close() {
    System.out.println("cost: " + (System.currentTimeMillis() - start));
    }
    }

    public static void testPrint() {
    for (int i = 0; i < 5; i++) {
    System.out.println("now " + i);
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

    public static void main(String[] args) {
    try (Cost c = new Cost()) {
    testPrint();
    }
    System.out.println("------over-------");
    }

    执行后输出如下:


    now 0
    now 1
    now 2
    now 3
    now 4
    cost: 55
    ------over-------

    如果代码块抛异常,也会正常输出耗时么?


    public static void testPrint() {
    for (int i = 0; i < 5; i++) {
    System.out.println("now " + i);
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    if (i == 3) {
    throw new RuntimeException("some exception!");
    }
    }
    }

    再次输出如下,并没有问题


    now 0
    now 1
    now 2
    now 3
    cost: 46
    Exception in thread "main" java.lang.RuntimeException: some exception!
    at com.git.hui.boot.order.Application.testPrint(Application.java:43)
    at com.git.hui.boot.order.Application.main(Application.java:50)

    3. 小结


    除了上面介绍的两种方式,还有一种在业务开发中不太常见,但是在中间件、偏基础服务的功能组件中可以看到,利用 Java Agent 探针技术来实现,比如阿里的 arthas 就是在 JavaAgent 的基础上做了各种上天的功能,后续介绍 java 探针技术时会专门介绍


    下面小结一下三种统计耗时的方式


    基本写法


    long start = System.currentTimeMillis();
    try {
    // .... 具体的代码段
    } finally {
    System.out.println("cost: " + (System.currentTimeMillis() - start));
    }

    优点是简单,适用范围广泛;缺点是侵入性强,大量的重复代码


    Spring AOP


    在 Spring 生态下,可以借助 AOP 来拦截目标方法,统计耗时


    @Around("...")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();
    try{
    return joinPoint.proceed();
    } finally {
    System.out.println("cost: " + (System.currentTimeMillis() - start));
    }
    }

    优点:无侵入,适合统一管理(比如测试环境输出统计耗时,生产环境不输出);缺点是适用范围小,且粒度为方法级别,并受限于 AOP 的使用范围


    AutoCloseable


    这种方式可以看做是第一种写法的进阶版


    // 定义类
    public static class Cost implements AutoCloseable {
    private long start;

    public Cost() {
    this.start = System.currentTimeMillis();
    }

    @Override
    public void close() {
    System.out.println("cost: " + (System.currentTimeMillis() - start));
    }
    }

    // 使用姿势
    try (Cost c = new Cost()) {
    ...
    }

    优点是:简单,适用范围广泛,且适合统一管理;缺点是依然有代码侵入


    说明


    上面第二种方法看着属于最优雅的方式,但是限制性强;如果有更灵活的需求,建议考虑第三种写法,在代码的简洁性和统一管理上都要优雅很多,相比较第一种可以减少大量冗余代码


    作者:一灰灰
    链接:https://juejin.cn/post/6844904080645554189
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Flutter -项目架构篇

    介绍 多姿的青春,迷茫的青春,懵懂的青春,落泪的青春,责任的青春,青春的婀娜,青春的美妙全部撒播在了沿途的风景之中,迷茫,酸楚,欢声笑语在记忆的天空中承载着梦想而飞翔,青春才成了心中的永恒 本文带你一步一步搭建flutter项目架构,方便你项目直接集成使用...
    继续阅读 »

    介绍



    多姿的青春,迷茫的青春,懵懂的青春,落泪的青春,责任的青春,青春的婀娜,青春的美妙全部撒播在了沿途的风景之中,迷茫,酸楚,欢声笑语在记忆的天空中承载着梦想而飞翔,青春才成了心中的永恒



    本文带你一步一步搭建flutter项目架构,方便你项目直接集成使用。项目主要用到以下技术栈,小编秉着分享的宗旨,为你讲解


    1.全局捕获异常


    2.路由(Route)


    3.Dio(网络)


    4.OverlayEntry


    5.网络dio抓包工具配置(ALice)


    6.状态管理(Provider)


    7.通知(这个是小编自己写的, 很方便,类似EventBus)


    全局捕获异常


    在Flutter中 ,有些异常是可以捕获到的,有些则是捕获不到的。那么,我们要做到错误日志上报给服务器,方便线上跟踪问题,怎么办呢?有个东西了解一下,捕获不到的用runZoned。代码如下,代码中有详细的注释,这里就不一一解释了。


    void main() {
    /// 捕获flutter能try catch 捕获的异常
    /// 还有一些异常是try catch 捕获不到的 用runZoned
    FlutterError.onError = (FlutterErrorDetails errorDetails) {
    if (Application.debug) {
    /// 测试环境 日志直接打印子啊控制台
    FlutterError.dumpErrorToConsole(errorDetails);
    } else {
    /// 在生产环境上 重定向到runZone 处理
    Zone.current
    .handleUncaughtError(errorDetails.exception, errorDetails.stack);
    }
    reportErrorAndLog(errorDetails);
    };
    WidgetsFlutterBinding.ensureInitialized();
    GlobalKey<NavigatorState> globalKey = new GlobalKey<NavigatorState>();
    Application.globalKey = globalKey;

    /// dio 网络抓包工具配置
    Alice alice = Alice(
    showInspectorOnShake: true,
    showNotification: true,
    navigatorKey: globalKey);
    Application.alice = alice;

    /// 初始化网络配置
    HttpManager.initNet();

    /// 捕获try catch 捕获不到的异常
    runZoned(
    () => runApp(MultiProvider(
    providers: [
    ///注册通知
    /// 这个是相当于通知 的作用 用于 这个类中的属性改变 然后通知到用到的页面 进行刷新
    ChangeNotifierProvider(create: (_) => CounterProvider()),
    ],
    child: MyApp(),
    )), zoneSpecification: ZoneSpecification(
    print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
    /// 这里捕获所有print 日志
    },
    ), onError: (Object obj, StackTrace stack) {
    var detail = makeDetails(obj, stack);
    reportErrorAndLog(detail);
    });
    }

    void reportErrorAndLog(FlutterErrorDetails errorDetails) {
    /// 错误日志上报 服务器
    }

    /// 构建错误信息
    FlutterErrorDetails makeDetails(Object obj, StackTrace stack) {
    FlutterErrorDetails details =
    FlutterErrorDetails(stack: stack, exception: obj);
    return details;
    }

    路由相关


    路由跳转配置


    跳转有2种方式。一种是直接用Widget, 另一种是用routeName。 这里小编为你讲解routeName跳转


    先附上路由跳转封装类



    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';

    /// Created by zhengxiangke
    /// des:
    class NavigatorUtil {
    ///直接跳转
    static void push(BuildContext context, Widget widget) {
    Navigator.push(context, MaterialPageRoute(builder: (context) => widget));
    }

    ///根据路径跳转 可传参数
    static void pushName(BuildContext context, String name, {Object arguments}) {
    Navigator.pushNamed(context, name, arguments: arguments);
    }

    ///销毁页面
    static void pop(BuildContext context) {
    Navigator.of(context).pop(context);
    }

    /// 推到 指定路由页面 这指定路由页面上的页面全部销毁
    /// 注意: 若果没有指定路由 会报错
    static void popUntil(BuildContext context, String routeName) {
    Navigator.popUntil(context, ModalRoute.withName(routeName));
    }


    /// 把当前页面在栈中的位置替换为跳转的页面, 当新的页面进入后,之前的页面将执行dispose方法
    static void pushReplacementNamed(BuildContext context, String routeName,
    {Object arguments}) {

    Navigator.of(context).pushReplacementNamed(routeName, arguments: arguments);
    }
    }


    我们可以看到代码中的routeName, routeName这个是我们自己可以配置的 ,简单而言,就是根据路径去跳到指定的页面。路由配置如下


    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    localeResolutionCallback:
    (Locale locale, Iterable<Locale> supportedLocales) {
    //print("change language");
    return locale;
    },
    navigatorKey: Application.globalKey,

    /// 这个routes 不能写 如果写了的话 就不能传递参数
    // routes: routes,
    /// 这个既可以传递参数 也可以不传递参数 用这一个就够了 无须用这个routes
    onGenerateRoute: onGenerateRoute,
    navigatorObservers: [
    /// 路由监听 作用:对用户行为流的埋点监测
    GLObserver()
    ],
    home: MyHomePage(),
    );
    }
    }

    final routes = {
    '/second' : (context) => SecondPage(),
    '/NestedScrollViewDemo' : (context, {arguments}) => NestedScrollViewDemo(value: arguments['value'] as String),
    '/ProviderDemo': (context) => ProviderDemo()
    };

    // ignore: missing_return, top_level_function_literal_block
    final onGenerateRoute = (settings) {
    Function pageContentBuilder = routes[settings.name];
    Route route;
    if (pageContentBuilder != null) {
    if (settings.arguments != null) {
    /// 传递参数
    route = MaterialPageRoute(
    settings: settings,
    builder: (context) => pageContentBuilder(context, arguments: settings.arguments));
    return route;
    } else {
    /// 不传递参数 只管跳
    route = MaterialPageRoute(
    settings: settings,
    builder: (context) => pageContentBuilder(context));
    return route;
    }

    }
    };

    观察者


    页面跳转添加观察者,能获取用户行为数据(GLObserver)


      @override
    Widget build(BuildContext context) {
    return MaterialApp(
    localeResolutionCallback:
    (Locale locale, Iterable<Locale> supportedLocales) {
    //print("change language");
    return locale;
    },
    navigatorKey: Application.globalKey,

    /// 这个routes 不能写 如果写了的话 就不能传递参数
    // routes: routes,
    /// 这个既可以传递参数 也可以不传递参数 用这一个就够了 无须用这个routes
    onGenerateRoute: onGenerateRoute,
    navigatorObservers: [
    /// 路由监听 作用:对用户行为流的埋点监测
    GLObserver()
    ],
    home: MyHomePage(),
    );
    }

    Dio相关


    dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等...


    dependencies:
    dio: ^3.0.9 // 请使用pub上3.0.0分支的最新版本

    基本配置


    小编带你写个Dio单例, 在这个单例中配置Dio基本配置


    /// 这个是域名  请书写自己项目中的域名
    const String BASEURL = '';
    class HttpConfig {
    static const baseUrl = BASEURL;
    static const timeout = 5000;

    static const codeSuccess = 10000;

    }

    class HttpManager {
    factory HttpManager() => getInstance();
    static HttpManager get install => getInstance();
    static HttpManager _install;
    static Dio dio;
    HttpManager._internal() {
    // 初始化
    }
    static HttpManager getInstance() {
    if (_install == null) {
    _install = HttpManager._internal();
    }
    return _install;
    }
    /// 初始化网络配置
    static void initNet() {
    dio = Dio(BaseOptions(
    baseUrl: HttpConfig.baseUrl,
    contentType: 'application/x-www-form-urlencoded',
    connectTimeout: HttpConfig.timeout,
    receiveTimeout: HttpConfig.timeout
    ));
    }

    }

    设置代理


    有这么个需求背景, 有一天,测试来问,怎么抓网络信息。Dio 为我们提供了代理, 测试可以根据chanles等抓包工具进行查看网络信息


        if (Application.proxy) {
    /// 用于代理 抓包
    (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
    client.findProxy = (uri) {
    //// PROXY 是固定 后面的localhost:8888 指的是别人的机器ip
    return 'PROXY localhost:8888';
    };
    };
    }

    拦截器


    我们可以在拦截器中添加一些公共的参数,如用户信息,手机信息,App版本信息等等, 也可以打印请求的url, 请求头,请求体信息。也可以进行参数签名。这里签名就不一一说了,


      /// 添加拦截器
    dio.interceptors.add(CustomInterceptors());

    ///  des:  这里的api 规范是 200成功
    class CustomInterceptors extends InterceptorsWrapper {
    @override
    Future onRequest(RequestOptions options) {
    /// 在拦截里设置公共请求头
    options.headers = {HttpHeaders.authorizationHeader: '这是token'};
    if (Application.debug) {
    try {
    print("请求url:${options.path}");
    print('请求头: ' + options.headers.toString());
    /// 可以在这里个性定制 如签名 key value 从小到大排序 options.data 再次赋值即可
    print('请求体: ' + options.data);
    } catch (e) {
    print(e);
    }
    }
    return super.onRequest(options);
    }
    @override
    Future onResponse(Response response) async{
    LoadingUtil.closeLoading();
    if (Application.debug) {
    print('code=${response.statusCode.toString()} ==data=${response.data
    .toString()}');
    }
    return super.onResponse(response);
    }
    @override
    Future onError(DioError err) {
    // TODO: implement onError
    LoadingUtil.closeLoading();
    if (err.type == DioErrorType.CONNECT_TIMEOUT
    || err.type == DioErrorType.RECEIVE_TIMEOUT
    || err.type == DioErrorType.SEND_TIMEOUT) {
    Fluttertoast.showToast(msg: '请求超时');
    } else {
    Fluttertoast.showToast(msg: '服务异常');
    }
    return super.onError(err);
    }
    }

    ALice


    这是一个网络请求查看库,有了这个就不需要指定代理了,很方便。下面为dio 进行Alice 拦截,以便查看Dio 发出的请求


    dependencies:
    alice: 0.1.4
    dio.interceptors.add(Application.alice.getDioInterceptor());

    注意:ALice 一定要配置navigatorKey


      GlobalKey<NavigatorState> globalKey = new GlobalKey<NavigatorState>();
    Application.globalKey = globalKey;
    /// dio 网络抓包工具配置
    Alice alice = Alice(
    showInspectorOnShake: true,
    showNotification: true,
    navigatorKey: globalKey);
    Application.alice = alice;

      class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return MaterialApp( navigatorKey:Application.globalKey,
    ],
    home: MyHomePage(),
    );
    }
    }

    Provider


    Flutter 状态管理,实际来说就是数据和视图的绑定和刷新; 这块对应到 H5,就比较好理解,这个概念也是从前端来到; 对应到 客户端,就是监听回调,类似事件总线(EventBus)


    简而言之,就是监听的类中的变量属性发生变化就会刷新用到这个变量的Widget 页面


    dependencies:
    provider: ^4.3.2+2

    说说这个库的中心思想


    1.注册


    2.定义类


    3.赋值


    4.取值


    注册


    runApp(MultiProvider(
    providers: [
    ///注册通知
    /// 这个是相当于通知 的作用 用于 这个类中的属性改变 然后通知到用到的页面 进行刷新
    ChangeNotifierProvider(create: (_) => CounterProvider()),
    ],
    child: MyApp(),
    ))

    定义类


    class CounterProvider with ChangeNotifier {
    int count = 0;
    void addCount() {
    count ++;
    notifyListeners();
    }
    }

    赋值


     Provider.of<CounterProvider>(context, listen: false).addCount()

    取值


    context.watch<CounterProvider>().count

    通知


    小编之前在做Android开发,就用到了这个通知。后来做了Flutter开发一年来头了, 借鉴其思想,创下了这个通知


    先说说通知原理:


    我们知道EventBus有一个action事件和一个可以传递的数据对象。在页面初始化生命周期中注册通知,在页面销毁生命周期中销毁该通知。在需要发送通知 刷新数据地方, 调用发送通知 ,一个Action 对应发送到哪个通知,通知数据是一个泛型的Object, 可以发送字符串,对象,数组等任何数据


    通知管理类


    在这个类中提供几个方法


    1.注册通知


    2.销毁通知


    3.发送通知


    ///这是一个例子
    ///这个在initState() 注册 <> 里的类型 要跟 发送的数据类型对应 可以是String int bool 还有model list
    /// IUpdateViewManager.instance.registerIUPdateView(UpdateView(NoticeAction.action1,
    /// IUpdateView<List<String>>(
    /// callback: (List<String> msg) {
    /// print(msg);
    /// }
    /// )));、
    ///发送通知 注意 这里可以发送任何类型的数据 因为范型
    ///IUpdateViewManager.instance.notifyIUpdateView(NoticeAction.action1, ['xx', 'vvv']);
    ///在dispose中 取消注册 注意: 有注册就有取消 这是对应的 否则会出现功能不正常请客
    ///IUpdateViewManager.instance.unRegistIUpdateView(NoticeAction.action1);
    class IUpdateViewManager{
    List<UpdateView> updateViews = [];

    // 工厂模式
    factory IUpdateViewManager() =>_getInstance();
    static IUpdateViewManager get instance => _getInstance();
    static IUpdateViewManager _instance;
    IUpdateViewManager._internal() {
    // 初始化
    }
    static IUpdateViewManager _getInstance() {
    _instance ??= IUpdateViewManager._internal();
    return _instance;
    }
    ///注册通知 在initstatus 注册
    void registerIUPdateView(UpdateView updateView) {
    ///在数组中不能存在多个相同的action
    updateViews.insert(0, updateView);
    }
    ///发送通知 在业务场景需要的地方 调用这个方法
    void notifyIUpdateView <T>(String action, T t) {
    if (updateViews != null && updateViews.isNotEmpty) {
    for (var item in updateViews) {
    if (item.action == action) {
    item.iUpdateView.updateView(t);
    break;
    }
    }
    }
    }
    ///通知解绑 在dispose方法中解绑 注意 有注册 就有解绑 这是一定必须的
    void unRegistIUpdateView(String action) {
    if (updateViews != null && updateViews.isNotEmpty) {
    updateViews.remove(UpdateView(action, null));
    }
    }
    }
    ///这个类是时间action 用到这个类的通知的action 在这里定义常量
    class NoticeAction {
    static const String action1 = 'action1';
    static const String action2 = 'action2';
    }

    其次,通知类如下


    class UpdateView {
    String action;
    IUpdateView iUpdateView;


    UpdateView(this.action, this.iUpdateView);

    @override
    bool operator ==(Object other) =>
    identical(this, other) ||
    other is UpdateView &&
    runtimeType == other.runtimeType &&
    action == other.action;

    @override
    int get hashCode => action.hashCode;
    }

    class IUpdateView <T>{
    Function(T msg) callback;
    void updateView (T t) {
    if (callback != null) {
    callback(t);
    }
    }

    IUpdateView({@required this.callback});
    }

    注册通知


    这个在initState() 注册 <> 里的类型 要跟 发送的数据类型对应 可以是String int bool 还有model list


       IUpdateViewManager.instance.registerIUPdateView(UpdateView(NoticeAction.action1,
    IUpdateView<List<String>>(
    callback: (List<String> msg) {
    print(msg);
    }
    )));

    发送通知


    IUpdateViewManager.instance.notifyIUpdateView(NoticeAction.action1, ['xx', 'vvv']);

    销毁通知


    在dispose中 取消注册 注意: 有注册就有取消 这是对应的 否则会出现功能不正常


    IUpdateViewManager.instance.unRegistIUpdateView(NoticeAction.action1);

    架构篇


    小编先说说搭建项目的总体思想,




    1. 我们知道每一个页面刚进去的时候都会有一个loading,因此小编用一个widget的基类,所有的页面都会继承这个基类。在这个基类中提供了Appbar的方法,加载试图显示和隐藏,加载失败重试,网络请求的方法,另外还有个buildBody方法,所有继承该基类的widget都必须重写这个方法,详见BasePage




    2. 网络: 本文采用Dio,并添加了拦截器,可在拦截器中打印请求信息,有个HttpManager管理单例的dio实例,并添加了Alice网络查看器,方便测试人员查看请求信息。HttpRequest里有个请求方法, 可定义请求方式,传递方式,失败回调,成功回调。并在回调中返回ResultData(这是一个返回的数据结构封装类)




    3. 对于埋点上报,新增了一个GLObserver路由观察者,在这里可以进行简单的用户行为进行捕获




    4. 错误日志上报 详见main.dart




    5. 由于复杂的页面交互,那么通知也是少不了的,一个页面的某个行为会影响上个页面的展现内容或者刷新数据,那么 这里小编定义了2中方式:1.Provider 2.IupdateViewManager 大家可以任选其一即可




    6. 基于SmartRefresher刷新封装的CustomerSmartRefresh


      最后,代码已上传github , 欢迎下载阅读
      如有疑问 加QQ群 883130953


      github.com/zhengxiangk…


    作者:爱运动的年轻人
    链接:https://juejin.cn/post/6895566224193749006
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    关于Android架构,你是否还在生搬硬套?

    前言 关于Android架构,可能在很多人心里一直都是虚无缥缈的存在,似懂非懂、为了用而用、处处生搬硬套,这种情况使用的意义真的很有限。本人有多个项目重构的经验,恰好对设计领域较为感兴趣,今天我将毫无保留的将自己对架构、设计的理解分享给大家。 本文不会具体去讲...
    继续阅读 »

    前言


    关于Android架构,可能在很多人心里一直都是虚无缥缈的存在,似懂非懂、为了用而用、处处生搬硬套,这种情况使用的意义真的很有限。本人有多个项目重构的经验,恰好对设计领域较为感兴趣,今天我将毫无保留的将自己对架构、设计的理解分享给大家。


    本文不会具体去讲什么是MVC、MVP、MVVM,但我描述的点应该都是这些模式的基石,从本质上讲明白为什么这样做,这样做的好处是什么,有了这些底层思想的支持再去看对应的架构模式,相信会让你有一种焕然一新的感觉。


    知识储备:需掌握Java面向对象、六大设计原则,如果不理解也无妨,我尽量将用到的设计原则加以详细描述


    目录



    • 1. 模块化的意义何在?

      • 1.1 基本概念以及底层思想

      • 1.2 我们要基于哪些特性去做模块化划分?

      • 1.3 Android如何做分层处理?

      • 1.4 Data Mapper或许是解药

      • 1.5 无处安放的业务逻辑



    • 2. 合理分层是给 数据驱动UI 做铺垫

      • 2.1 什么是 控制反转?

      • 2.2 什么是数据驱动UI?

      • 2.3 为什么说数据驱动UI底层思想是控制反转?

      • 2.4 为什么引入Diff?



    • 3. 为什么我建议使用 函数式编程

      • 3.1 什么是 函数式编程?

      • 3.2 Android视图开发可以借鉴函数式编程思想




    1. 模块化的意义何在?


    1.1 基本概念以及底层思想



    所有的模块化都是为了满足单一设计原则 (字面意思理解即可),一个函数或者一个类再或者一个模块,职责越单一复用性就越强,同时能够间接降低耦合性



    在软件工程的背景下,改动就会有出错的可能,不要说"我注意一点就不会出错"这种话,因为人不是机器。我们能做的就是尽可能让模块更加单一,职责越单一影响到外层模块的可能性就越小,这样出错的概率也就越低。


    所以模块化核心思想即:单一设计原则


    1.2 我们要基于哪些特性去做模块化划分?


    做模块化处理的时候尽量基于两种特性进行功能特性业务特性


    功能特性



    网络、图片加载等等都可称之为功能特性。比如网络:我们可以将网络框架的集成、封装等等写到同一个模块(module、package等)当中,这样可以增强可读性(同一目录一目了然)、降低误操作概率,方便于维护也更加安全。同时也可将模块托管至远程如maven库,可供多个项目使用,进一步提升复用性



    业务特性



    业务特性字面意思理解即可,就是我们常常编写的业务,需要以业务的特性进行模块划分



    为什么说业务特性优先级要高于功能特性


    举个例子如下图:


    image.png


    相信很多人见过或者正在使用这种分包方式,在业务层把所有的AdapterPresenterActivity等等都放在对应的包中,这种方式合理吗?先说答案不合理,首先这已经是在业务层,我们做的所有事情其实都在为业务层服务,所以业务的优先级应该是最高的,我们应当优先根据业务特性将对应的类放入到同一个包中。


    功能模块核心是功能,应当以功能进行模块划分。业务模块核心是业务,应当优先以业务进行模块划分,其次再以功能进行模块划分。


    1.3 Android如何做分层处理?


    前端开发其实就是做数据搬运,再展示到视图中。数据视图是两个不同的概念,为了提高复用性以及可维护性,我们应当根据单一设计原则我们应当将二者进行分层处理,所以无论是MVCMVP还是MVVM最核心的点都是将数据视图进行分层。


    绊脚石:



    通常来讲,我们通过网络请求拿到数据结构都是后端定义的,这也就意味着视图层不得不直接使用后端定义的字段,一旦后端进行业务调整会迫使我们前端从数据层-->视图层都会进行对应的改动,如下伪代码所示:



    //原始逻辑
    数据层
    Model{
    title
    }
    UI层
    View{
    textView = model.title
    }

    //后端调整后
    数据层
    Model{
    title
    prefix
    }
    UI层
    View{
    textView = model.prefix + model.title
    }

    起初我们的textView显示的是model中的title,但后端调整后我们需要在model中加一个prefix字段,同时textView显示内容也要做一次字符串拼接。视图层因为数据层的改动而被动做了修改。既然做了分层我们想要的肯定是视图、数据互不干扰,如何解决?往下看...


    1.4 Data Mapper或许是解药


    Data Mapper是后端常用的一个概念,一般情况下他们是不会直接使用数据库里面的字段,而是加一个Data Mapper(数据映射)将数据库表转按需换成Java Bean,这样做的好处也很明显,表结构甭管怎么折腾都不会影响到业务层代码。


    对于前端我觉得可以适当引入Data Mapper,将后端数据转换成本地模型,本地模型只与设计图对应,将后端业务视图完全隔离。这也就解决了 1.3 面临的问题,具体方式如下:


    数据层
    Model{
    title
    prefix
    }
    本地模型(与设计图一一对应)
    LocalModel{
    //将后端模型转换为本地模型
    title = model.prefix + model.title
    }
    UI层
    View{
    textView = localModel.title
    }

    LocalModel相当于一个中间层,通过适配器模式将数据层与视图层做隔离。


    前端引入Data Mapper后可以脱离后端进行开发,只要需求明确就可以做视图层的开发,完全不需要担心后端返回什么结构字段。并且这种做法是一劳永逸的,比如后端需要对某些字段做调整,我们可以不暇思索直奔数据层,涉及到的调整100%不会影响到视图层


    注意点:



    当下有一部分公司为了将前后端分离更彻底,由前端开发人员提供Java Bean(相当于LocalModel)的结构,好处也很明显,更多的业务内聚到后端,很大程度提升了业务的灵活性,毕竟App发一次版成本还是比较大的。面对这种情况我们其实没必要再编写Data Mapper。所以任何架构设计都要结合实际情况,适合自己的才是最好的。



    1.5 无处安放的业务逻辑


    关于业务逻辑其实是一个很笼统的概念,甚至可以将任意一行代码称之为业务逻辑,如此宽泛的概念我们该如何去理解?我先大致将它分为两个方面:




    • 界面交互逻辑:视图层的交互逻辑,比如手势控制、吸顶悬浮等等都是根据业务需要实现的,所以严格来说这部分也属于业务逻辑。但这部分业务逻辑一般在视图层实现。

    • 数据逻辑:这部分是大家常说的业务逻辑,属于强业务逻辑,比如根据不同用户类型获取不同数据、展示不同界面,加上Data Mapper一系列操作其实就是给后端兜底,帮他们补全剩余逻辑而已。为了方便大家理解下文我将数据逻辑统称为业务逻辑



    前面我们说到,Android开发应该具备数据层视图层,那业务逻辑放在哪一层比较合适呢?比如MVVM模式下大家都说将业务逻辑放到ViewModel处理,这么说也没有太大的问题,但如果一个界面足够复杂那对应的ViewModel代码可能会有成百上千行,看起来会很臃肿可读性也非常差。最重要的一点这些业务很难编写单元测试用例


    关于业务逻辑我建议单独写一个use case处理。


    use case通常放在ViewModel/Presenter数据层之间,业务逻辑以及Data Mapper都应该放在use case中,每一个行为对应一个use case。这样就解决了ViewModel/Presenter臃肿的问题,同时更方便编写测试用例。


    注意点:



    好的设计都是特定场景解决特定问题,过度设计不仅解决不了任何问题反而会增加开发成本。以我目前经验来看Android开发至少一半的场景都很简单:请求-->拿数据-->渲染视图最多再加个Data Mapper,流程很单一并且后期改动的可能也不太大,这种情况就没必要写一个use case,Data Mapper扔到数据层即可。



    2. 合理分层是给 数据驱动UI 做铺垫


    先说结论:数据驱动UI的本质是控制反转


    2.1 什么是 控制反转?


    控制即对程序流程的控制,一般由我们开发者承担,此过程为控制。但开发者是人所以不可避免出现错误,此时可以将角色做一个反转由成熟的框架负责整个流程,程序员只需要在框架预留的扩展点上,添加跟自己的业务代码,就可以利用框架来驱动整个程序流程的执行,此过程为反转


    控制反转概念和设计原则中的依赖倒置很相似,只是少了一个依赖抽象


    打个比方:



    现有一个HTTP请求的需求,如果想自己维护HTTT链接、自己管理TCP Socket、自己处理HTTP缓存.....就是整个HTTP协议全部自己封装,先不说这个工程能不能靠个人实现,就算实现也是漏洞百出,此时可以换个思路:通过OkHttp去实现,OkHttp是一个成熟的框架用它基本上不会出错。个人封装HTTP协议到使用OkHttp框架,这个过程在控制HTTP的角色上发生了一个反转个人--->成熟的框架OkHttp即控制反转,好处也很明显,框架出错的概率远低于个人。



    2.2 什么是数据驱动UI?


    通俗一点说就是当数据改变时对应的UI也要跟着变,反过来说当需要改变UI只需要改变对应的数据即可。现在比较流行的UI框架如FlutterComposeVue其本质都是基于函数式编程实现数据驱动UI,它们共同的目的都是为了解决数据,UI一致性问题。


    在当前的Android中可以使用DataBinding实现同样的效果,以Jetpack MVVM为例:ViewModelRepository拿到数据暂存到ViewModel对应的ObservableFiled即可实现数据驱动UI,但前提是从Repository拿到的数据可以直接用,如果在Activity或者Adapter做数据二次处理再notify UI,已经违背数据驱动UI核心思想。所以想实现数据驱动UI必须要有合理的分层(UI层拿到的数据无需处理,可以直接用)Data Mapper恰好解决这一问题,同时也可规避大量编写BindAdapter的现状。



    DataBinding并非函数式编程,它只是通过AbstractProcessor生成中间代码,将数据映射到XML中



    2.3 为什么说数据驱动UI底层思想是控制反转?


    当前Android生态能实现数据绑定UI的框架只有两个:DataBinding、Compose(暂不讨论)


    在引入DataBinding之前渲染一条数据通常需要两步,如下:


    var title = "iOS"
    fun setTitle(){
    //第一步更改数据源
    title = "Android"
    //第二个更改UI
    textView = title
    }

    共需要两步更改数据源、更改UI,数据源UI有一个忘记修改便会出现BUG,千万不要说:“两个我都不会忘记修改”,当面临复杂的逻辑以及十几个甚至几十个的数据源很难保证不出错。这种问题可以通过DataBinding解决,只需更改对应的ObservableFiledUI便会同步修改,控制UI状态也从个人反转到的DataBinding,个人疏忽的事情DataBinding可不会。


    所以说数据驱动UI底层思想是控制反转


    2.4 为什么引入Diff?


    引入diff之前:



    RecyclerView想要实现动态删除、添加、更新需要分别手动更新数据和UI,这样在中间插了一道并且分别更新数据和UI已经违背了前面所说的数据驱动UI,而我们想要的是不管删除、添加或者更新只有一个入口,只要改变数据源就会驱动UI做更新,想要满足这一原则只能改变数据源后对RecyclerView做全部刷新,但这样会造成性能问题,复杂的界面会感到明显的卡顿。



    引入diff之后:



    Diff算法通过对oldItemnewItem做差异化比对,会自动更新改变的item,同时支持删除、添加的动画效果,这一特性解决了RecyclerView需要实现数据驱动UI的性能问题



    3 为什么我建议使用 函数式编程


    3.1 什么是 函数式编程?



    • 一个入口,一个出口。

    • 不在函数链内部执行与运算本身无关的操作

    • 不在函数链内部使用外部变量(实际上这一条很难遵守,可以适当突破)


    说的通俗点就是给定一个初始值,经过函数链的运行会得到一个目标值,运算的过程中外部没有插手的权限,同时不做与本身无关的操作,从根本上解决了不可预期错误的产生。


    举个例子:


    //Kotlin代码

    listOf(10, 20).map {
    it + 1
    }.forEach {
    Log.i("list", "$it")
    }

    上面这种链式编程就是标准的函数式编程,输入到输出之间开发者根本没有插手的机会(即Log.i(..)之前开发者没有权限处理list),所以整个流程是100%安全的,RxJavaFlow链式高阶函数都是标准的函数式编程,它们从规范层面解决数据安全问题。所以我建议在Kotlin中 碰到数据处理尽量使用链式高阶函数(RxJava、Kotlin Flow亦然)


    其实函数式编程的核心思想就是 门面模式 以及 迪米特法则


    3.2 Android视图开发可以借鉴函数式编程思想


    Android视图开发大都遵循如下流程:请求-->处理数据-->渲染UI,这一流程可以借鉴函数式编程,将请求作为入口,渲染做为出口,在这个流程中尽量不做与当前行为无关的事(这也要求ViewModel,Repository中的函数要符合单一原则)。这样说有点笼统,下面举个反例:


        View{
    //刷新
    fun refresh(){
    ViewModel.load(true)
    }
    //加载更多
    fun loadMore(){
    ViewModel.load(false)
    }
    }

    ViewModel{
    //加载数据
    load(isRefresh){
    if (isRefresh){
    //刷新
    }else{
    //加载更多
    }
    }
    }

    View层有刷新、加载更多两种行为,load(isRefresh)一个入口,两个出口。面临的问题很明显,修改刷新加载更多都会对对方产生影响,违反开闭原则中的闭(对修改关闭:行为没变不准修改源代码),导致存在不可预期的问题产生。可以借鉴函数式编程思想对其进行改进,将ViewModelload函数拆分成refreshloadMore,这样刷新加载更多两种行为、两个入口、两个出口互不干涉,通过函数的衔接形成两条独立的业务链条。


    函数式编程可以约束我们写出规范的代码,面对不能使用函数式编程的场景,我们可以尝试自我约束往函数式编程方向靠拢,大致也能实现相同的效果。


    综上所述



    • 合理的分层可以提升复用性、降低模块间耦合性

    • Data Mapper 可以让视图层脱离于后端进行开发

    • 复杂的业务逻辑应该写到use case中

    • 数据驱动UI的本质是控制反转

    • 通过函数式编程可以写出更加安全的代码

    作者:Bezier
    链接:https://juejin.cn/post/6942464122273398820
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »