注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

ConstraintLayout 中的 Barrier 和 Chains

1. Barrier是一个准则,可以说是对其的规则,这样说还不够名义,我们可以列表一些比较常见的场景; 官网 Barrier。具体看图 “第二行的label”和“第二行value”是一个整体,他们距离上面是 100dp ...
继续阅读 »

1. Barrier

是一个准则,可以说是对其的规则,这样说还不够名义,我们可以列表一些比较常见的场景; 官网 Barrier

  1. 具体看图

1883633-62653bd01cb70813.webp “第二行的label”和“第二行value”是一个整体,他们距离上面是 100dp ,但是有可能“第二行value”的值为空或者是空,也需要“第二行label”距离上面的距离是 100dp ,由于我们知道“第二行value”的高度高于第一个,所以采用的是“第二行label”跟“第二行value”对其,“第二行value”距离上边 100dp 的距离,但是由于“第二行value”有可能为空,所以当“第二行value”为空的时候就会出现下面的效果:

1883633-043d00e43ff22557.webp 我们发现达不到预期,现在能想到的办法有,首先在代码控制的时候随便把“第二行label”的 marginTop 也添加进去;还有就是换布局,将“第二行label”和“第二行value”放到一个布局中,比如 LinearLayout ,这样上边的 marginTop 由 LinearLayout 控制;这样的话即便“第二行value”消失了也会保持上边的效果。

除了上边的方法还能使用其他的嘛,比如我们不使用代码控制,我们不使用其他的布局,因为我们知道布局嵌套太多性能也会相应的下降,所以在编写的时候能减少嵌套的情况下尽可能的减少,当然也不能为了减少嵌套让代码变得格外的复杂。

为了满足上面的需求, Barrier 出现了,它能做到隐藏的也能依靠它,并且与它的距离保持不变对于隐藏的“第二行value”来说,虽然消失了,但保留了 marginTop 的数值。下面看看布局:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">

<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="头部"
android:textSize="36sp"
app:layout_constraintBottom_toTopOf="@id/barrier3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
android:layout_marginTop="100dp"
app:constraint_referenced_ids="textView2,textView3" />

<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="第二行label"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="@+id/textView3"
app:layout_constraintStart_toStartOf="@+id/textView4"
app:layout_constraintTop_toTopOf="@+id/textView3"
app:layout_constraintVertical_bias="0.538" />

<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_marginStart="12dp"
android:layout_marginTop="100dp"
android:text="第二行value"
android:textSize="36sp"
app:layout_constraintStart_toEndOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/barrier3" />

</androidx.constraintlayout.widget.ConstraintLayout>

这样即便将“第二行value”消失,那么总体的布局仍然达到预期,并且也没有添加很多布局内容。在代码中:

<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
android:layout_marginTop="100dp"
app:constraint_referenced_ids="textView2,textView3" />

这里主要有两个属性 app:barrierDirection 和 app:constraint_referenced_ids :

  • app:barrierDirection 是代表位置,也就是在包含内容的哪一个位置,我这里写的是 top ,是在顶部,还有其他的属性 top,bottom,left,right,start 和 end 这几个属性,看意思就很明白了。
  • app:constraint_referenced_ids 上面说的内容就是包含在这里面的,这里面填写的是 id 的名称,如果有多个,那么使用逗号隔开;这里面的到 Barrier 的距离不会改变,即便隐藏了也不会变。

这里可能会有疑惑,为啥我写的 id 为 textView4 的也依赖于 Barrier ,这是因为本身 Barrier 只是规则不是实体,它的存在只能依附于实体,不能单独存在于具体的位置,如果我们只有“第二行value”依赖于它,但是本身“第二行value”没有上依赖,也相当于没有依赖,这样只会导致“第二行label”和“第二行value”都消失,如果 textView4 依赖于 Barrier ,由于 textView4 的位置是确定的,所以 Barrier 的位置也就确定了。

  1. 类似表格的效果。看布局效果:

1883633-c4b862a2df57fb96.webp 我要做成上面的样子。也就是右边永远与左边最长的保持距离。下面是我的代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">


<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="头部"
android:textSize="36sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="第二行"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="@+id/textView3"
app:layout_constraintStart_toStartOf="@+id/textView4"
app:layout_constraintTop_toTopOf="@+id/textView3" />

<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:text="第二行value"
android:textSize="36sp"
app:layout_constraintStart_toEndOf="@+id/barrier4"
app:layout_constraintTop_toBottomOf="@+id/textView4" />


<TextView
android:id="@+id/textView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="第三次测试"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="@+id/textView6"
app:layout_constraintStart_toStartOf="@+id/textView4"
app:layout_constraintTop_toTopOf="@+id/textView6" />

<TextView
android:id="@+id/textView6"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:text="第三行value"
android:textSize="36sp"
app:layout_constraintStart_toEndOf="@+id/barrier4"
app:layout_constraintTop_toBottomOf="@+id/textView3" />

<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="textView2,textView5"
tools:layout_editor_absoluteX="411dp" />


</androidx.constraintlayout.widget.ConstraintLayout>

添加好了,记得让右边的约束指向 Barrier 。这里的 Barrier ,我们看到包含 textView2 和 textView5 ,这个时候就能做到谁长听谁的,如果此时 textView2 变长了,那么就会将就 textView2 。

2. Chains

我们特别喜欢使用线性布局,因为我们发现 UI 图上的效果使用线性布局都可以使用,当然也有可能跟大部分人的思维方式有关系。比如我们非常喜欢的,水平居中,每部分的空间分布等等都非常的顺手。既然线性布局这么好用,那为啥还有约束布局呢,因为线性布局很容易写出嵌套很深的布局,但约束布局不会,甚至大部分情况都可以不需要嵌套就能实现,那是不是代表线性布局有的约束布局也有,答案是肯定的。

使用普通的约束关系就很容易实现水平居中等常用效果,其他的如水平方向平均分布空间,使用一般的约束是实现不了的,于是就要使用 Chains ,这个就很容易实现下面的效果:

1883633-78aa31c23dcb4c4f.webp 其实上一篇中我已经把官网的教程贴上去了,这里主要写双向约束怎么做,一旦双向约束形成,那么就自然进入到 Chains 模式。

1)在视图模式中操作

1883633-618f9b2eb563a637.webp

如果直接操作,那么只能单向约束,如果要形成这样的约束,需要选择相关的的节点,比如我这里就是同时选择 A 和 B ,然后点击鼠标右键,就可以看到 Chains → Create Horizontal Chain 。

对应的操作

选择图中的选项即可完成从 A 指向 B ,修改的示意图为:

1883633-cf3984e22df83c7c.webp

我们发现已经实现了水平方向的排列效果了。至于怎么实现上面的效果,主要是改变 layout_constraintVertical_chainStyle 和 layout_constraintHorizontal_chainStyle 属性。至于权重则是属性 layout_constraintHorizontal_weight 。

layout_constraintHorizontal_chainStyle 属性说明:

  • spread 默认选项,效果就是上面的那种,也就是平均分配剩余空间;
  • spread_inside 两边的紧挨着非 Chains 的视图,中间的平均分配;

1883633-49c52026c6797e51.webp

  • packed 所有的都在中间

1883633-714e58d28eaab99c.webp 注意了, layout_constraintHorizontal_weight 这个属性只有在 A 身上设置才可以,也就是首节点上设置才可行,同时 layout_constraintHorizontal_weight 是代表水平方向,只能在水平方向才发生作用,如果水平的设置了垂直则不生效。

layout_constraintHorizontal_weight 这个属性只有在当前视图的宽或者高是 0dp 。至于这个的取值跟线性布局相同。

1883633-072f1f968528ef1a.webp

2)代码的方式 跟上面的差别就是在做双向绑定,用代码就很容易实现双向绑定,可平时添加约束相同。


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

收起阅读 »

❤️Android 12 高斯模糊-RenderEffect❤️

 Android 12 高斯模糊 新功能:更易用的模糊、彩色滤镜等特效 。 新的 API 让你能更轻松地将常见图形效果应用到视图和渲染结构上。 使用 RenderEffect 将模糊、色彩滤镜等效果应用于 RenderNode 或 View。 ...
继续阅读 »

 Android 12 高斯模糊


新功能:更易用的模糊、彩色滤镜等特效 。


新的 API 让你能更轻松地将常见图形效果应用到视图和渲染结构上。




  • 使用 RenderEffect 将模糊、色彩滤镜等效果应用于 RenderNode 或 View




  • 使用新的 Window.setBackgroundBlurRadius() API 为窗口背景创建雾面玻璃效果,




  • 使用 blurBehindRadius 来模糊窗口后面的所有内容。




咱们一个一个玩。


🔥 RenderEffect


💥 实现效果


    private void setBlur(){
View.setRenderEffect(RenderEffect.createBlurEffect(3, 3, Shader.TileMode.REPEAT));
...
}

使用特别简单,走你。


🌀 X 轴的模糊效果图



咱再看看代码


    private void setBlur(){
agb.iv1.setRenderEffect(RenderEffect.createBlurEffect(3, 0, Shader.TileMode.CLAMP));
agb.iv2.setRenderEffect(RenderEffect.createBlurEffect(8, 0, Shader.TileMode.REPEAT));
agb.iv3.setRenderEffect(RenderEffect.createBlurEffect(18, 0 ,Shader.TileMode.MIRROR));
agb.iv4.setRenderEffect(RenderEffect.createBlurEffect(36, 0,Shader.TileMode.DECAL));
}

RenderEffect.createBlurEffect()的四个参数:




  • radiusX 沿 X 轴的模糊半径




  • radiusY 沿 Y 轴的模糊半径




  • inputEffect 模糊一次(传入 RenderEffect)




  • edgeTreatment 用于如何模糊模糊内核边缘附近的内容




下面两种仅看效果图。就不做代码设置了。


🌀 Y 轴的模糊效果图



🌀 XY同时模糊效果图



第四个参数对边缘模糊,效果图如下:



Shader.TileMode 提供了四个选项恕我没看出来。。


这里还有一堆方法等你玩。




注意:注意如此完美的画面只能在 Android 12(SDK31)及以上的设备上使用,其他版本的设备使用会导致崩溃,谨记谨记。
效果有了,下面咱们一起看看源码。



💥 源码


🌀 View.setRenderEffect()


    public void setRenderEffect(@Nullable RenderEffect renderEffect) {
...
}

这个方法就是:renderEffect 应用于 View。 传入 null清除之前配置的RenderEffect 。这里咱们先看传入的 RenderEffect。


🌀 RenderEffect.createBlurEffect()


    public static RenderEffect createBlurEffect(
float radiusX,
float radiusY,
@NonNull RenderEffect inputEffect,
@NonNull TileMode edgeTreatment
) {
long nativeInputEffect = inputEffect != null ? inputEffect.mNativeRenderEffect : 0;
return new RenderEffect(
nativeCreateBlurEffect(
radiusX,
radiusY,
nativeInputEffect,
edgeTreatment.nativeInt
)
);
}

两个 createBlurEffect() 方法,分别为三参(模糊一次)和四参(模糊两次)。inputEffect 先进行了一次模糊。


看效果图:



模糊程度一样,但是实现方式不同:


    private void setBlur() {
RenderEffect radiusXRenderEffect = RenderEffect.createBlurEffect(10, 0, Shader.TileMode.MIRROR);
RenderEffect radiusYRenderEffect = RenderEffect.createBlurEffect(0, 10, Shader.TileMode.MIRROR);
agb.iv1.setRenderEffect(RenderEffect.createBlurEffect(10, 10, Shader.TileMode.CLAMP));
agb.iv2.setRenderEffect(RenderEffect.createBlurEffect(10, 10, Shader.TileMode.REPEAT));
//自身radiusY 为 0 ,传入的radiusYRenderEffect设置的radiusY为10;
agb.iv3.setRenderEffect(RenderEffect.createBlurEffect(10, 0, radiusYRenderEffect, Shader.TileMode.MIRROR));
//自身radiusX 为 0 ,传入的radiusXRenderEffect设置的radiusX为10;
agb.iv4.setRenderEffect(RenderEffect.createBlurEffect(0, 10, radiusXRenderEffect, Shader.TileMode.DECAL));
}

这个方法返回一个 new RenderEffect(nativeCreateBlurEffect(...)。


那咱们去看看 nativeCreateBlurEffect()


🌀 nativeCreateBlurEffect()


frameworks/base/libs/hwui/jni/RenderEffect.cpp


static const JNINativeMethod gRenderEffectMethods[] = {
...
{"nativeCreateBlurEffect", "(FFJI)J", (void*)createBlurEffect},
...
};

static jlong createBlurEffect(JNIEnv* env , jobject, jfloat radiusX,
jfloat radiusY, jlong inputFilterHandle, jint edgeTreatment) {
auto* inputImageFilter = reinterpret_cast<SkImageFilter*>(inputFilterHandle);
sk_sp<SkImageFilter> blurFilter =
SkImageFilters::Blur(
Blur::convertRadiusToSigma(radiusX),
Blur::convertRadiusToSigma(radiusY),
static_cast<SkTileMode>(edgeTreatment),
sk_ref_sp(inputImageFilter),
nullptr);
return reinterpret_cast<jlong>(blurFilter.release());
}

这里有两个函数来处理我们传过来的模糊的值,咱进去看看。


🌀 convertRadiusToSigma(convertSigmaToRadius)


//该常数近似于在SkBlurMask::Blur()(1/sqrt(3)中,在软件路径的"高质量"模式下进行的缩放。
static const float BLUR_SIGMA_SCALE = 0.57735f;

float Blur::convertRadiusToSigma(float radius) {
return radius > 0 ? BLUR_SIGMA_SCALE * radius + 0.5f : 0.0f;
}

float Blur::convertSigmaToRadius(float sigma) {
return sigma > 0.5f ? (sigma - 0.5f) / BLUR_SIGMA_SCALE : 0.0f;
}

🌀 sk_ref_sp(inputImageFilter)


external/skia/include/core/SkRefCnt.h


/*
* 返回包装提供的 ptr 的 sk_sp 并对其调用 ref (如果不为空)
*/
template <typename T> sk_sp<T> sk_ref_sp(T* obj) {
//sk_sp<SkImageFilter> :
return sk_sp<T>(SkSafeRef(obj));
}

//SkSafeRef:检查参数是否为非空,如果是,则调用 obj->ref() 并返回 obj。
template <typename T> static inline T* SkSafeRef(T* obj) {
if (obj) {
obj->ref();
}
return obj;
}

再往下走


🌀 SkImageFilters::Blur()



#define SK_Scalar1 1.0f
#define SK_ScalarNearlyZero (SK_Scalar1 / (1 << 12))

sk_sp<SkImageFilter> SkImageFilters::Blur(
SkScalar sigmaX, SkScalar sigmaY, SkTileMode tileMode, sk_sp<SkImageFilter> input,
const CropRect& cropRect) {
if (sigmaX < SK_ScalarNearlyZero && sigmaY < SK_ScalarNearlyZero && !cropRect) {
return input;
}
return sk_sp<SkImageFilter>(
new SkBlurImageFilter(sigmaX, sigmaY, tileMode, input, cropRect));
}

附上最后的倔强


    constexpr sk_sp() : fPtr(nullptr) {}
constexpr sk_sp(std::nullptr_t) : fPtr(nullptr) {}

/**
* Shares the underlying object by calling ref(), so that both the argument and the newly
* created sk_sp both have a reference to it.
*/
sk_sp(const sk_sp<T>& that) : fPtr(SkSafeRef(that.get())) {}
template <typename U,
typename = typename std::enable_if<std::is_convertible<U*, T*>::value>::type>
sk_sp(const sk_sp<U>& that) : fPtr(SkSafeRef(that.get())) {}

/**
* Move the underlying object from the argument to the newly created sk_sp. Afterwards only
* the new sk_sp will have a reference to the object, and the argument will point to null.
* No call to ref() or unref() will be made.
*/
sk_sp(sk_sp<T>&& that) : fPtr(that.release()) {}
template <typename U,
typename = typename std::enable_if<std::is_convertible<U*, T*>::value>::type>
sk_sp(sk_sp<U>&& that) : fPtr(that.release()) {}

/**
* Adopt the bare pointer into the newly created sk_sp.
* No call to ref() or unref() will be made.
*/
explicit sk_sp(T* obj) : fPtr(obj) {}

createBlurEffect() 得到 long 类型的 native 分配的的非零地址, 传入 new RenderEffect()


🌀 new RenderEffect()


    /* 构造方法:仅从静态工厂方法构造 */
private RenderEffect(long nativeRenderEffect) {
mNativeRenderEffect = nativeRenderEffect;
RenderEffectHolder.RENDER_EFFECT_REGISTRY.registerNativeAllocation(
this, mNativeRenderEffect);
}

继续



/**
* @param classLoader ClassLoader 类加载器。
* @param freeFunction 类型为 nativePtr 的本机函数的地址,用于释放这种本机分配
* @return 由系统内存分配器分配的本机内存的 NativeAllocationRegistry。此版本更适合较小的对象(通常小于几百 KB)。
*/
private static class RenderEffectHolder {
public static final NativeAllocationRegistry RENDER_EFFECT_REGISTRY =
NativeAllocationRegistry.createMalloced(
RenderEffect.class.getClassLoader(), nativeGetFinalizer());
}

🌀 NativeAllocationRegistry.createMalloced()


libcore/luni/src/main/java/libcore/util/NativeAllocationRegistry.java


    @SystemApi(client = MODULE_LIBRARIES)
public static NativeAllocationRegistry createMalloced(
@NonNull ClassLoader classLoader, long freeFunction, long size) {
return new NativeAllocationRegistry(classLoader, freeFunction, size, true);
}

🌀 NativeAllocationRegistry()


libcore/luni/src/main/java/libcore/util/NativeAllocationRegistry.java


    private NativeAllocationRegistry(ClassLoader classLoader, long freeFunction, long size,
boolean mallocAllocation) {
if (size < 0) {
throw new IllegalArgumentException("Invalid native allocation size: " + size);
}
this.classLoader = classLoader;
this.freeFunction = freeFunction;
this.size = mallocAllocation ? (size | IS_MALLOCED) : (size & ~IS_MALLOCED);
}

既然拿到 NativeAllocationRegistry 那就继续调用其
registerNativeAllocation() 方法。


🌀 registerNativeAllocation ()


    @SystemApi(client = MODULE_LIBRARIES)
@libcore.api.IntraCoreApi
public @NonNull Runnable registerNativeAllocation(@NonNull Object referent, long nativePtr) {
//当 referent 或nativePtr 为空
...
CleanerThunk thunk;
CleanerRunner result;
try {
thunk = new CleanerThunk();
Cleaner cleaner = Cleaner.create(referent, thunk);
result = new CleanerRunner(cleaner);
registerNativeAllocation(this.size);
} catch (VirtualMachineError vme /* probably OutOfMemoryError */) {
applyFreeFunction(freeFunction, nativePtr);
throw vme;
}
// Enable the cleaner only after we can no longer throw anything, including OOME.
thunk.setNativePtr(nativePtr);
// Ensure that cleaner doesn't get invoked before we enable it.
Reference.reachabilityFence(referent);
return result;
}

向 ART 注册新的 NativePtr 和关联的 Java 对象(也就是咱们设置的模糊类)。


返回的 Runnable 可用于在引用变得无法访问之前释放本机分配。如果运行时或使用 runnable 已经释放了本机分配,则 runnable 将不起作用。


RenderEffect 算是搞完了,咱们回到View.setRenderEffect()


🌀 View.setRenderEffect()


    public void setRenderEffect(@Nullable RenderEffect renderEffect) {
if (mRenderNode.setRenderEffect(renderEffect)) {
//视图属性更改(alpha、translationXY 等)的快速失效。
invalidateViewProperty(true, true);
}
}

这里有个 mRenderNode.setRenderEffect(renderEffect)。咱们近距离观望一番。


🌀 mRenderNode 的创建


咱们先找找他是在什么地方创建的。


    public View(Context context) {
...
//在View的构造方法中创建
mRenderNode = RenderNode.create(getClass().getName(), new ViewAnimationHostBridge(this));
...
}

🌀 RenderNode.create()


    /** @hide */
public static RenderNode create(String name, @Nullable AnimationHost animationHost) {
return new RenderNode(name, animationHost);
}

private RenderNode(String name, AnimationHost animationHost) {
mNativeRenderNode = nCreate(name);
//注册 Native Allocation。
NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativeRenderNode);
mAnimationHost = animationHost;
}

再往下感觉也看不到啥了 跟上面类似,看.cpp动态分配类的地址还是有点懵。让我缓缓~以后补充。


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

不要把公司当成家,被通知裁员时会变得不幸...

公司对于员工来说,究竟是个什么地方?不要把公司当家,公司只是你出卖劳动力,换取报酬的地方。人在社会上混,不要讲感情,也不要被别人洗脑,去傻乎乎地讲感情,跟你讲感情的老板,基本上都是没更多钱给你的老板。老板开公司就是来盈利的,不是为了什么社会责任感,也不是为了造...
继续阅读 »


公司对于员工来说,究竟是个什么地方?

不要把公司当家,公司只是你出卖劳动力,换取报酬的地方。

人在社会上混,不要讲感情,也不要被别人洗脑,去傻乎乎地讲感情,跟你讲感情的老板,基本上都是没更多钱给你的老板。

老板开公司就是来盈利的,不是为了什么社会责任感,也不是为了造福社会,更不是为了降低社会失业率开的。

老板只会去找一些能够适合他公司发展,能够帮他赚钱的员工过来上班。

员工对于公司来说,只是一个可有可无的螺丝钉,随时都可以替代,你有时会觉得自己在公司好像挺忙,为公司做了很大贡献,在公司不可或缺。但是都是你的错觉,要么就是老板或者公司强加给你的意识,实际上公司除了老板,哪个员工走了,公司都可以照样转。

员工跟老板的本质,就是员工出卖自己的劳动力和时间,然后跟老板换取报酬,大家都是交易而已。

如果哪天员工觉得他可以在别的公司拿到更高的薪水,他就会选择跳槽。老板觉得员工不能给公司带来与其薪水相匹配的利益,老板就会让这个员工卷铺盖走人。

对于一个员工来说,最好的老板不是每天讲情怀,讲社会责任感,也不是每天对他态度很和蔼,笑眯眯地关心他的生活过得好不好。老板的素质再高也没有什么鸟用,一个合格的老板,就是能够按时发工资

并且这个老板有本事能够让公司越做越大,让他在公司里面打工的员工,能拿到的薪水越来越多,并且能够提升的空间也越来越大。哪怕这个老板每天板着个脸,不近人情,也是一个很好的老板。

对于一个老板来说,最好的员工就是拿最低的薪水干最多的事情,能够为他带来最大的利益,这就是一个好的员工。

所以老板不会看你是什么211、双一流、清北毕业的,也不会看你考了多少证书,这些东西虽然有时候是个敲门砖,只是因为能够为老板寻找合适的人节省时间,但是真正入职之后,老板还是要看你是否能够为他创造相应的价值。

对于绝大部分员工来说,老板跟他的关系的重要性,远远超过了他跟他所谓的亲戚朋友,同事同学等等之间的关系。

因为那些人虽然看起来有血缘关系,还沾亲带故的,但是这些人跟他都没有利益上的往来,但是老板却是他的衣食父母。

说白了,老板就是相当于是他的客户,他提供劳动,并出卖自己的时间,老板是购买他劳动和时间的人,跟他是有经济利益关系的。

很多时候,员工应该多琢磨自己跟老板之间的关系,而不要去琢磨跟自己没有经济利益往来的一些人的关系。哪怕这些人跟你很熟,还是亲戚。

所以不要把公司当家,放任自己在里面任性、软弱、懒惰,那对公司不是损失。因为它可以随时换人,而自己损失的时间和机会永远回不来了。

要把公司当成球队或船,抓住在里面的一切机会锻炼自己增加技能,让自己强大,强大到离开它也不会怕,它离开你需要掂量掂量...。

-

小编我:趁着中午吃饭,“偷拍”公司同事工位,第一张是我的...,有财神那张是财务的,嘿嘿。










作者:思齐大神

来源丨蚂蚁大喇叭

收起阅读 »

多行文本下的文字渐隐消失术

web
本文将探讨一下,在多行文本情形下的一些有意思的文字动效。多行文本,相对于单行文本,场景会复杂一些,但是在实际业务中,多行文本也是非常之多的,但是其效果处理比起单行文本会更困难。单行与多行文本的渐隐首先,我们来看这样一个例子,我们要实现这样一个单行文本的渐隐:使...
继续阅读 »

本文将探讨一下,在多行文本情形下的一些有意思的文字动效。

多行文本,相对于单行文本,场景会复杂一些,但是在实际业务中,多行文本也是非常之多的,但是其效果处理比起单行文本会更困难。

单行与多行文本的渐隐

首先,我们来看这样一个例子,我们要实现这样一个单行文本的渐隐:


使用 mask,可以轻松实现这样的效果,只需要:

<p>Lorem ipsum dolor sit amet consectetur.</p>
p {
  mask: linear-gradient(90deg, #fff, transparent);
}

但是,如果,场景变成了多行呢?我们需要将多行文本最后一行,实现渐隐消失,并且适配不同的多行场景:


这个就会稍微复杂一点点,但是也是有多种方式可以实现的。

首先我们来看一下使用 background 的方式。

使用 background 实现

这里会运用到一个技巧,就是 display: inline 内联元素的 background 展现形式与 display: block 块级元素(或者 inline-blockflexgrid)不一致。

简单看个例子:

<p>Lorem .....</p>
<a>Lorem .....</a>

这里需要注意,<p> 元素是块级元素,而 <a>内联元素

我们给它们统一添加上一个从绿色到蓝色的渐变背景色:

p, a {
background: linear-gradient(90deg, blue, green);
}

看看效果:


什么意思呢?区别很明显,块级元素的背景整体是一个渐变整体,而内联元素的每一行都是会有不一样的效果,整体连起来串联成一个整体。

基于这个特性,我们可以构造这样一种布局:

<p><a>Mollitia nostrum placeat consequatur deserunt velit ducimus possimus commodi temporibus debitis quam</a></p>
p {
  position: relative;
  width: 400px;
}

a {
  background: linear-gradient(90deg, transparent, transparent 70%, #fff);
  background-repeat: no-repeat;
  cursor: pointer;
  color: transparent;
   
  &::before {
      content: "Mollitia nostrum placeat consequatur deserunt velit ducimus possimus commodi temporibus debitis quam";
      position: absolute;
      top: 0;
      left: 0;
      color: #000;
      z-index: -1;
  }
}

这里需要解释一下:

  1. 为了利用到实际的内联元素的 background 的特性,我们需要将实际的文本包裹在内联元素 <a>

  2. 实际的文本,利用了 opacity: 0 进行隐藏,实际展示的文本使用了 <a> 元素的伪元素,并且将它的层级设置为 -1,目的是让父元素的背景可以盖过它

  3. <a> 元素的渐变为从透明到白色,利用它去遮住下面的实际用伪元素展示的文字,实现文字的渐隐

这样,我们就能得到这样一种效果:


这里,<a> 元素的渐变为从透明到白色,利用后面的白色逐渐遮住文字。

如果我将渐变改为从黑色到白色(为了方便理解,渐变的黑色和白色都带上了一些透明),你能很快的明白这是怎么回事:

a {
  background: linear-gradient(90deg, rgba(0,0,0, .8), rgba(0,0,0, .9) 70%, rgba(255, 255, 255, .9));
}


完整的代码,你可以戳这里:CodePen Demo -- Text fades away[1]

当然,这个方案有很多问题,譬如利用了 z-index: -1,如果父容器设置了背景色,则会失效,同时不容易准确定位最后一行。因此,更好的方式是使用 mask 来解决。

使用 mask 实现

那么,如果使用 mask 的话,问题,就会变得简单一些,我们只需要在一个 mask 中,实现两块 mask 区域,一块用于准确控制最后一行,一块用于控制剩余部分的透明。

也不需要特殊构造 HTML:

<p>Lorem ipsum dolor sit amet ....</p>
p {
  width: 300px;
  padding: 10px;
  line-height: 36px;
  mask:
      linear-gradient(270deg, transparent, transparent 30%, #000),
      linear-gradient(270deg, #000, #000);
  mask-size: 100% 46px, 100% calc(100% - 46px);
  mask-position: bottom, top;
  mask-repeat: no-repeat;
}

效果如下:


核心在于整个 mask 相关的代码,正如上面而言的,mask 将整个区域分成了两块进行控制:


在下部分这块,我们利用 mask 做了从右向左的渐隐效果。并且利用了 mask-position 定位,以及 calc 的计算,无论文本都多少行,都是适用的!需要说明的是,这里的 46px 的意思是单行文本的行高加上 padding-bottom 的距离。可以适配任意行数的文本:


完整的代码,你可以戳这里:CodePen Demo -- Text fades away 2[2]

添加动画效果

好,看完静态的,我们再来实现一种**动态的文字渐隐消失。

整体的效果是当鼠标 Hover 到文字的时候,整个文本逐行逐渐消失。像是这样:

图片

这里的核心在于,需要去适配不同的行数,不同的宽度,而且文字是一行一行的进行消失。

这里核心还是会运用上内联元素 background 的特性。在 妙用 background 实现花式文字效果[3] 这篇文章中,我们介绍了这样一种技巧。

实现整段文字的渐现,从一种颜色到另外一种颜色

<div>Button</div>
<p><a>Lorem ipsum dolor sit amet consectetur adipisicing elit. Mollitia nostrum placeat consequatur deserunt velit ducimus possimus commodi temporibus debitis quam, molestiae laboriosam sit repellendus sed sapiente quidem quod accusantium vero.</a></p>
a {    
  background:
      linear-gradient(90deg, #999, #999),
      linear-gradient(90deg, #fc0, #fc0);
  background-size: 100% 100%, 0 100px;
  background-repeat: no-repeat;
  background-position: 100% 100%, 0 100%;
  color: transparent;
  background-clip: text;
}
.button:hover ~ p a {
  transition: .8s all linear;
  background-size: 0 100px, 100% 100%;
}

这里需要解释一下,虽然设置了 color: transparent,但是文字默认还是有颜色的,默认的文字颜色,是由第一层渐变赋予的 background: linear-gradient(90deg, #999, #999), linear-gradient(90deg, #fc0, #fc0),也就是这一层:linear-gradient(90deg, #999, #999)

图片

当 hover 触发时,linear-gradient(90deg, #999, #999) 这一层渐变逐渐消失,而另外一层 linear-gradient(90deg, #fc0, #fc0)` 逐渐出现,借此实现上述效果。

CodePen -- background-clip 文字渐现效果[4]

好,我们可以借鉴这个技巧,去实现文字的渐隐消失。一层为实际的文本,而另外一层是进行动画的遮罩,进行动画的这一层,本身的文字设置为 color: transparent,这样,我们就只能看到背景颜色的变化。

大致的代码如下:

<p>
  <a>Mollitia nostrum placeat consequatur deserunt.</a>
  <a>Mollitia nostrum placeat consequatur deserunt.</a>
</p>
p {
  width: 500px;
}
.word {
  position: absolute;
  top: 0;
  left: 0;
  color: transparent;
  color: #000;
}
.pesudo {    
  position: relative;
  background: linear-gradient(90deg, transparent, #fff 20%, #fff);
  background-size: 0 100%;
  background-repeat: no-repeat;
  background-position: 100% 100%;
  transition: all 3s linear;
  color: transparent;
}
p:hover .pesudo,
p:active .pesudo{
  background-size: 500% 100%;
}

其中,.word 为实际在底部,展示的文字层,而 pesudo 为叠在上方的背景层,hover 的时候,触发上方元素的背景变化,逐渐遮挡住下方的文字,并且,能适用于不同长度的文本。

图片

当然,上述方案会有一点瑕疵,我们无法让不同长度的文本整体的动画时间一致。当文案数量相差不大时,整体可以接受,文案相差数量较大时,需要分别设定下 transition-duration 的时长。

完整的 DEMO,你可以戳:CodePen -- Text fades away Animation[5]

最后

好了,本文到此结束,希望对你有帮助 :)

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

参考资料

[1]CodePen Demo -- Text fades away: https://codepen.io/Chokcoco/pen/xxWPZmz

[2]CodePen Demo -- Text fades away 2: https://codepen.io/Chokcoco/pen/MWVvoyW

[3]妙用 background 实现花式文字效果: https://github.com/chokcoco/iCSS/issues/138

[4]CodePen -- background-clip 文字渐现效果: https://codepen.io/Chokcoco/pen/XWgpyqz

[5]CodePen -- Text fades away Animation: https://codepen.io/Chokcoco/pen/wvmqqWa

[6]Github -- iCSS: https://github.com/chokcoco/iCSS

来源:mp.weixin.qq.com/s/qADnUx3G2tKyMT7iv6qFwg


收起阅读 »

Flutter 中使用Chip 小部件

概述 典型的chip是一个圆角的小盒子。它有一个文本标签,并以一种有意义且紧凑的方式显示信息。chip可以在同一区域同时显示多个交互元素。一些流行的chip用例是: 发布标签(您可以在许多 WordPress ,VuePress,知乎,掘金,公众号或 Git...
继续阅读 »

概述


典型的chip是一个圆角的小盒子。它有一个文本标签,并以一种有意义且紧凑的方式显示信息。chip可以在同一区域同时显示多个交互元素。一些流行的chip用例是:



  • 发布标签(您可以在许多 WordPress ,VuePress,知乎,掘金,公众号或 GitHub等大型平台上看到它们)。

  • 可删除的内容列表(一系列电子邮件联系人、最喜欢的音乐类型列表等)。


img


在 Flutter 中,您可以使用以下构造函数来实现 Chip 小部件:


Chip({
 Key? key,
 Widget? avatar,
 required Widget label,
 TextStyle? labelStyle,
 EdgeInsetsGeometry? labelPadding,
 Widget? deleteIcon,
 VoidCallback? onDeleted,
 Color? deleteIconColor,
 bool useDeleteButtonTooltip = true,
 String? deleteButtonTooltipMessage,
 BorderSide? side,
 OutlinedBorder? shape,
 Clip clipBehavior = Clip.none,
 FocusNode? focusNode,
 bool autofocus = false,
 Color? backgroundColor,
 EdgeInsetsGeometry? padding,
 VisualDensity? visualDensity,
 MaterialTapTargetSize? materialTapTargetSize,
 double? elevation,
 Color? shadowColor
})

只有label属性是必需的,其他是可选的。一些常用的有:



  • avatar:在标签前显示一个图标或小图像。

  • backgroundColor : chip的背景颜色。

  • padding:chip内容周围的填充。

  • deleteIcon:让用户删除chip的小部件。

  • onDeleted:点击deleteIcon时调用的函数。


您可以在官方文档中找到有关其他属性的更多详细信息。但是,对于大多数应用程序,我们不需要超过一半。


简单示例


这个小例子向您展示了一种同时显示多个chip的简单使用的方法。我们将使用Wrap小部件作为chip列表的父级。当当前行的可用空间用完时,筹码会自动下行。由于Wrap 小部件的间距属性,我们还可以方便地设置chip之间的距离。


截屏:


image-20220125100331474


代码:


Scaffold(
     appBar: AppBar(
       title: const Text('大前端之旅'),
    ),
     body: Padding(
       padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 10),
       child: Wrap(
           // space between chips
           spacing: 10,
           // list of chips
           children: const [
             Chip(
               label: Text('Working'),
               avatar: Icon(
                 Icons.work,
                 color: Colors.red,
              ),
               backgroundColor: Colors.amberAccent,
               padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
            ),
             Chip(
               label: Text('Music'),
               avatar: Icon(Icons.headphones),
               backgroundColor: Colors.lightBlueAccent,
               padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
            ),
             Chip(
               label: Text('Gaming'),
               avatar: Icon(
                 Icons.gamepad,
                 color: Colors.white,
              ),
               backgroundColor: Colors.pinkAccent,
               padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
            ),
             Chip(
               label: Text('Cooking & Eating'),
               avatar: Icon(
                 Icons.restaurant,
                 color: Colors.pink,
              ),
               backgroundColor: Colors.greenAccent,
               padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
            )
          ]),
    ),
  );

在这个例子中,chip只呈现信息。在下一个示例中,chip是可交互的。


复杂示例:动态添加和移除筹码


应用预览


chip


我们要构建的应用程序包含一个浮动操作按钮。按下此按钮时,将显示一个对话框,让我们添加一个新chip。可以通过点击与其关联的删除图标来删除每个chip。


以下是应用程序的工作方式:


完整代码


main.dart中的最终代码和解释:


// main.dart
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
  return MaterialApp(
    debugShowCheckedModeBanner: false,
    title: '大前端之旅',
    theme: ThemeData(
      primarySwatch: Colors.green,
    ),
    home: const HomePage(),
  );
}
}

// Data model for a chip
class ChipData {
// an id is useful when deleting chip
final String id;
final String name;
ChipData({required this.id, required this.name});
}

class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);

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

class _HomePageState extends State<HomePage> {
// list of chips
final List<ChipData> _allChips = [];

// Text controller (that will be used for the TextField shown in the dialog)
final TextEditingController _textController = TextEditingController();
// This function will be triggered when the floating actiong button gets pressed
void _addNewChip() async {
  await showDialog(
      context: context,
      builder: (_) {
        return AlertDialog(
          title: const Text('添加'),
          content: TextField(
            controller: _textController,
          ),
          actions: [
            ElevatedButton(
                onPressed: () {
                  setState(() {
                    _allChips.add(ChipData(
                        id: DateTime.now().toString(),
                        name: _textController.text));
                  });

                  // reset the TextField
                  _textController.text = '';

                  // Close the dialog
                  Navigator.of(context).pop();
                },
                child: const Text('提交'))
          ],
        );
      });
}

// This function will be called when a delete icon associated with a chip is tapped
void _deleteChip(String id) {
  setState(() {
    _allChips.removeWhere((element) => element.id == id);
  });
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('大前端之旅'),
    ),
    body: Padding(
      padding: const EdgeInsets.all(15),
      child: Wrap(
        spacing: 10,
        children: _allChips
            .map((chip) => Chip(
                  key: ValueKey(chip.id),
                  label: Text(chip.name),
                  backgroundColor: Colors.amber.shade200,
                  padding:
                      const EdgeInsets.symmetric(vertical: 7, horizontal: 10),
                  deleteIconColor: Colors.red,
                  onDeleted: () => _deleteChip(chip.id),
                ))
            .toList(),
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: _addNewChip,
      child: const Icon(Icons.add),
    ),
  );
}
}


结论


我们已经探索了 Chip 小部件的许多方面,并经历了不止一个使用该小部件的示例。


大家喜欢的话,点赞支持一下坚果


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

DeepLink在转转的实践

1. DeepLink 简介 DeepLink:“深度链接”技术,这个名词初看比较抽象,不过在我们身边却有不少应用,比如以下场景: 刷抖音看到转转的广告,点击视频下方的下载链接,如果没有安装转转则下载转转,并在打开转转后跳转到相应活动页面 在微信看到朋友分享...
继续阅读 »

1. DeepLink 简介


DeepLink:“深度链接”技术,这个名词初看比较抽象,不过在我们身边却有不少应用,比如以下场景:



  • 刷抖音看到转转的广告,点击视频下方的下载链接,如果没有安装转转则下载转转,并在打开转转后跳转到相应活动页面

  • 在微信看到朋友分享的转转商品,点击后如果没有安装转转则下载转转,并在打开转转跳转到相应商品的详情页

  • 看到转转发送的订单提醒短信,点击链接后如果没有安装转转则下载转转, 并在打开转转跳转到相应订单详情页


DeepLink 使用户能够在目标 APP 之外,比如广告(抖音)/社交媒体(微信)/短信中通过点击链接,直接跳转到目标 APP 特定的页面(对于已经安装了 APP 会直接进行跳转,未安装 APP 会引导下载,下载安装完成之后跳转)。DeepLink 技术可以实现场景的快速还原,缩短用户使用路径,更重要的是能够用于 APP 拉新推广场景,降低用户流失率。


随着短视频的风靡,通过短视频投放广告获客的方式也流行起来, 本文主要介绍在新媒体拉新推广场景中 DeepLink 的应用以及服务端的搭建。


2 .应用场景


我在刷抖音时刷到一个转转回收的广告视频,而家里刚好有闲置的手机,我就抱着试一试的态度点击视频下方的下载链接下载转转,看看能不能在这个平台上处理掉手中的闲置,当下载安装成功之后打开跳转到了回收页面,我可能会眉头一挑,嗯~这个体验还挺好,然后测了下闲置手机值多少钱,回收价又刚好满足我的心理预期,而且还能为碳中和贡献一份自己的力量,何乐而不为之。


新媒体获客场景


以上是比较常见的一种场景,通过在抖音、快手或者其他渠道来投放广告来吸引一些有需求的用户来到转转,并通过 DeepLink 技术在下载完成打开转转后直接跳转到用户感兴趣的页面。


对于上述场景安卓和 IOS 的实现是有所区别的,包括下载策略以及 APP 内部跳转到用户感兴趣页面的策略。


2.1 IOS 应用场景


由于 IOS 下载APP只能通过 AppStore,所以 DeepLink 服务针对 IOS 会重定向到一个 H5 中间页,在 H5 中间页将服务端返回的 DeepLink 跳转链接复制到剪切板中,并拉起 AppStore 引导用户下载转转 APP,安装打开后 IOS 从剪切板中获取跳转链接进行跳转,到达用户感兴趣的页面。


IOS下载


2.2 安卓应用场景


安卓可以直接通过 DeepLink 服务下载转转 APP,而 DeepLink 跳转链接以 APK Signature Scheme v2 方式打入 apk 包中,安装打开后解析跳转链接跳转到用户感兴趣的页面。


安卓下载


APK Signature Scheme v2 是 Android 7.0 引入的一项新的应用签名方案 ,它能提供更快的应用安装时间和更多针对未授权 APK 文件更改的保护。


下图是新的签名方案和旧的签名方案的一个对比:


新旧签名方案对比


APK Signing Block 结构
































偏移字节数描述
@+08这个 Block 的长度(本字段的长度不计算在内)
@+8n一组 ID-value
@-248这个 Block 的长度(和第一个字段一样值)
@-1616魔数 “APK Sig Block 42”

APK Signing Block 中的 ID-value 是可扩展的,由于 APK Signing Block 的数据块不会参与签名校验,也就是说我们可以自定义一组 ID-value,用于存储额外的信息,我们通过自定义ID-value将跳转信息打入APK包中。


3. DeepLink 服务


在 IOS DeepLink 方案中服务端只是负责重定向到一个 H5 中间页,因此不再赘述,下面我们主要介绍下安卓的 DeepLink 方案。


概要设计


3.1 投放链接设计


投放链接是投放到各个渠道的下载链接,需要考虑以下几点:




  1. 各个渠道链接规则不一样,保证我们链接规则能够覆盖所有渠道


    通过我们的调研有些渠道只支持 Get 请求,有些渠道不允许带参数,有些渠道必须以.apk 进行结尾




  2. 投放方便,链接投放出去之后不需要再改动


    由于投放链接是给到一些自媒体创作者,在给出链接之后能够保证从始至终都能下到最新的APP




  3. 充分利用 CDN


    转转 APP、找靓机 APP 的包百兆左右,为了保证服务的稳定性同样为了节约带宽,尽量发挥 CDN 的作用把绝大多数请求让 CDN 服务器来进行处理返回




3.1.1 兼容版本1.0


考虑到兼容各个渠道,某些渠道必须以 apk 结尾、某些渠道不支持Get请求带参数,采用什么方式?


既然不能带参数,那我们的参数信息可以直接拼到path中,参数以某种规则组装,服务端解析,需要的信息包括 APP 类型、渠道信息、DeepLink 链接信息、版本号等,简要设计出的投放链接 1.0 大致如下:


apk.zhuanstatic.com/deeplink/**…



  • appType: APP 类型,目前支持转转和找靓机,可扩展,如 zhuanzhuan

  • channel:渠道类型,根据每个投放渠道单独设置渠道 id,如 douyin666

  • version:APP 版本号,如 9.0.0

  • deepLink:deepLink 信息,目前传输 deepLinkId,deepLinkId 和端内跳转链接的映射关系由后台维护,服务端通过映射关系拿到跳转链接打入 apk 包中,如 huishou


3.1.2 升级版本2.0


1.0的版本号是直接写到 path 中的,这会造成很多隐患



  1. 可以通过修改版本号恶意下载 APP 的任意版本

  2. 保证用户一直下到最新的包需要版本更新之后更新所有投放链接


这显然是不合理的,针对以上两点我们必然需要删掉 version,替代方案可以让服务端在处理下载请求的时候通过其他方式拿到版本信息,修正后的投放链接 2.0 如下:


apk.zhuanstatic.com/deeplink/**…


3.1.3 最终版本3.0


2.0中没有了版本信息进而导致相同的渠道投放链接是一致的,只要 CDN 中有老版本APP的缓存,下载的是缓存的老版本APP,无法获取最新APP


因此我们考虑中间做一次重定向,通过一个不接入 CDN 的固定链接去重定向到一个接入 CDN 的带版本号的链接,这样问题就迎刃而解了,因此投放链接 3.0 应运而生:


apk.zhuanzhuan.com/deeplink/**…

apk.zhuanstatic.com/deeplink/**…


apk.zhuanzhuan.com 不走 CDN,只是将链接中的版本号补全并重定向到走 CDN 的 apk.zhuanstatic.com ,这样在投放链接不变的情况下能保证用户下载到最新的包。


3.2 打包&下载


投放链接设计好之后,通过投放链接可以解析到一些参数信息,比如:
apk.zhuanstatic.com/deeplink/zh…


我们知道用户下载的是douyin666渠道转转9.0.0版本的包,并且APP打开后需要跳转回收的页面。


下载渠道包服务端逻辑主要分为两大块,第一部分是拿到相应版本的原始包,然后通过 APK Signature Scheme v2 方式将渠道号和 DeepLink 跳转链接打入原始包中获得渠道包,将渠道包提供给用户进行下载。


为了能应对 APP 升级和渠道投放带来的流量,尤其是 CDN 中还没有缓存的时候,避免大量请求将我们服务打垮,所以需要引入本地缓存,如何引入?


首先我们分析下服务端的主要逻辑找出不可变的数据,第一原始包肯定是不变的,第二在原始包相同的情况下如果 channel 和 deepLink 跳转链接是一致的,那我们打包出来的渠道包也相应是不可变的,因此我们可以针对这两部分来进行缓存。


接下来我们分析缓存选型以及缓存策略,本地缓存的组件有好多可选的,比如 Caffeine Cache、Guava Cache 等,网上关于他们的测评如下:


读场景性能对比


可以看到在读场景下,caffeine cache 是当之无愧的王者,而且我们的场景基本是接近 100%的读,所以我们优先选择了 Caffeine Cache。


以下是两个本地缓存策略介绍:


3.2.1 一级缓存(渠道包)


     /**
* 缓存高频渠道包文件
*/
private static final Cache<String, byte[]> channelFinalAppCache = CacheBuilder
.newBuilder()
.expireAfterAccess(1, TimeUnit.DAYS)
.maximumSize(15)
.build();

渠道包的缓存 key 是 appType+version+channel+deepLink,由于 channel 和 deepLink 组合的众多,通过分析之前的下载数据缓存最高频的 15 个渠道包就基本满足 90%以上的请求而且不至于占用太多的内存,而为了获取最高频的 15 个渠道,我们通过大数据平台以 T+1 的方式将渠道数据更新到数据库中,DeepLink服务通过定时任务读取数据库中的渠道数据刷新缓存。


3.2.2 二级缓存(原始包)


    /**
* 缓存原始包文件
*/
private static final Cache<String, byte[]> channelAppCache = CacheBuilder
.newBuilder()
.expireAfterAccess(2, TimeUnit.DAYS)
.maximumSize(10)
.build();

原始包的缓存 key 是 appType+version,由于我们只下载最新版本的包, APP 类型暂时只有转转和找靓机,所有我们设置最大数量 10 是足够的,在我们应用启动的时候会对这个缓存进行初始化,以避免第一次用户下载速度过慢,并在之后监听APP的发版信息,新版本更新后刷新缓存。


4. 总结


DeepLink 服务支撑了新媒体投放以及 APP 内置更新的下载能力,为了保证服务稳定性和性能,除上述缓存策略外,还有其他策略来协同,比如 APP 发新版本时会进行 CDN 预热,将下载量高的渠道包缓存到 CDN 中,以使大部分流量能够在 CDN 服务器被消化,即使有突发流量打过来也会有限流规则过滤流量以保证服务的稳定性。


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

Retrofit解密:接口请求是如何适配suspend协程?

最初的retrofit请求 我们先看下原来如何通过retrofit发起一个网络请求的,这里我们直接以官网的例子举例: 动态代理创建请求服务 interface GitHubService { //创建get请求方法 @GET("users/{u...
继续阅读 »

最初的retrofit请求


我们先看下原来如何通过retrofit发起一个网络请求的,这里我们直接以官网的例子举例:


动态代理创建请求服务


interface GitHubService {
//创建get请求方法
@GET("users/{user}/repos")
fun listRepos(@Path("user") user: String?): Call<Response>
}

//动态代理创建GitHubService
fun createService(): GitHubService {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build()

return retrofit.create(GitHubService::class.java)
}



  • retrofit.create底层是通过动态代理创建的GitHubService的一个子实现类;




  • 创建的这个GitHubService一般作为单例进行使用,这里只是简单举例没有实现单例;




发起网络请求


fun main() {
//异步执行网络请求
createService().listRepos("").enqueue(object : Callback<Response> {
override fun onResponse(call: Call<Response>, response: retrofit2.Response<Response>) {
//主线程网络请求成功回调
}

override fun onFailure(call: Call<Response>, t: Throwable) {
//主线程网络请求失败回调
}
})
}

这种调用enqueue()异步方法并执行callback的方式是不是感觉很麻烦,如果有下一个请求依赖上一个请求的执行结果,那就将会形成回调地狱这种可怕场景。


协程suspend本就有着以同步代码编写执行异步操作的能力,所以天然是解决回调地狱好帮手。接下来我们看下如何使用协程suspend


借助suspend发起网络请求


suspend声明接口方法


interface GitHubService {
@GET("users/{user}/repos")
suspend fun listRepos(@Path("user") user: String?): Response<String>
}

可以看到就是在listRepos方法声明前加了个suspend关键字就完了。


创建协程执行网络请求


fun main() {
//1.创建协程作用域,需要保证协程的调度器是分发到主线程执行
val scope = MainScope()
scope.launch(CoroutineExceptionHandler { _, _ ->
//2.捕捉请求异常
}) {
//3.异步执行网络请求
val result = createService().listRepos("")

val content = result.body()?
}
}



  1. 首先创建一个协程作用域,需要保证协程调度器类型为Dispatchers.Main,这样整个协程的代码块都会默认在主线程中执行,我们就可以直接在里面执行UI相关操作




  2. 创建一个CoroutineExceptionHandler捕捉协程执行过程中出现的异常,这个捕捉异常的粒度比较大,是捕捉整个协程块的异常,可以考虑使用try-catch专门捕获网络请求执行的异常:


    //异步执行网络请求
    try {
    val result = createService().listRepos("")
    } catch (e: Exception) {
    //可以考虑执行重连等逻辑或者释放资源
    }



  3. 直接调用listRepos()方法即可,不需要传入任何回调,并直接返回方法结果。这样我们就实现了以同步的代码实现了异步网络请求。




接下来我们就看下如何retrofit源码是如何实现这一效果的。


retrofit如何适配suspend


直接定位到HttpServiceMethod.parseAnnotations()方法:


static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {
//1.判断是否为suspend挂起方法
boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;

//省略一堆和当前分析主题不想关的代码

if (!isKotlinSuspendFunction) {
return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
} else if (continuationWantsResponse) {
//挂起执行
return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForResponse<>();
} else {
//挂起执行
return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForBody<>();
}
}

1.判断是否为suspend挂起方法


看下requestFactory.isKotlinSuspendFunction赋值的地方,经过一番查找(省略...),最终方法在RequestFactoryparseParameter间接赋值:


private @Nullable ParameterHandler<?> parseParameter() {
//...
//1.是否是方法最后一个参数
if (allowContinuation) {
try {
if (Utils.getRawType(parameterType) == Continuation.class) {
//2.标识为suspend挂起方法
isKotlinSuspendFunction = true;
return null;
}
} catch (NoClassDefFoundError ignored) {
}
}
}

如果一个方法被声明为suspend,该方法翻译成java代码就会给该方法添加一个Continuation类型的参数,并且放到方法参数的最后一个位置,比如:


private suspend fun test66(name: String) {  
}

会被翻译成:


private final Object test66(String name, Continuation $completion) {
return Unit.INSTANCE;
}

所以上面的代码就可以判断出请求的接口方法是否被suspend声明,是isKotlinSuspendFunction将会被置为true。


2.挂起则创建SuspendForResponseSuspendForBody


这个地方我们以SuspendForBody进行分析,最终会执行到其adapt()方法:


@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call);
//1.获取参数
Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];

try {
return isNullable
? KotlinExtensions.awaitNullable(call, continuation)
//2.调用真正的挂起方法
: KotlinExtensions.await(call, continuation);
} catch (Exception e) {
return KotlinExtensions.suspendAndThrow(e, continuation);
}
}



  1. 获取调用的suspend声明的接口方法中获取最后一个Continuation类型参数




  2. 调用await方法,由于这是一个kotlin定义的接收者为Call的挂起方法,如果在java中调用,首先第一个参数要传入接收者,也就是call,其实await()是一个挂起方法,翻译成java还会增加一个Continuation类型参数,所以调用await()还要传入第一步获取的Continuation类型参数。




3.核心调用await()方法探究


await()就是retrofit适配suspend实现同步代码写异步请求的关键,也是消除回调地狱的关键:


suspend fun <T : Any> Call<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
//关键
continuation.resumeWithException(KotlinNullPointerException())
} else {
//关键
continuation.resume(body)
}
} else {
//关键
continuation.resumeWithException(HttpException(response))
}
}

override fun onFailure(call: Call<T>, t: Throwable) {
//关键
continuation.resumeWithException(t)
}
})
}
}

使用到了协程的一个非常关键的方法suspendCancellableCoroutine{},该方法就是用来捕获传入的Continuation并决定什么恢复挂起的协程执行的,比如官方的delay()方法也是借助该方法实现的。


所以当我们执行调用enqueue()方法时在网络请求没有响应(成功或失败)前,协程一直处于挂起的状态,之后收到网络响应后,才会调用resume()resumeWithException()恢复挂起协程的执行,这样我们就实现了同步代码实现异步请求的操作,而不需要任何的callback嵌套地狱


总结


本篇文章详细分析retrofit如何适配suspend协程的,并且不用编写任何的callback回调,直接以同步代码编写实现异步请求的操作。


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

【Android】一键登录 - 三大运营商

业务背景: 在条件允许的情况下(无 SIM 卡的手机,无法触发一键登录),通过运行商提供的服务,进行【一键登录】。简化用户的登录操作,提高 App 的登录注册率以及使用率。 本方案采用的是阿里云中【一键登录】方案。 效果图: 前提知识: 整个流程如图所 ...
继续阅读 »

业务背景:


在条件允许的情况下(无 SIM 卡的手机,无法触发一键登录),通过运行商提供的服务,进行【一键登录】。简化用户的登录操作,提高 App 的登录注册率以及使用率。


本方案采用的是阿里云中【一键登录】方案。


效果图:



前提知识:



  • 整个流程如图所



(图源自网络[掘金大佬-NanBox],侵删)



  • 该方案下,不允许使用完全自定义的授权页。但是可以通过属性配置,进行一定的修改。可修改的属性如下图所示



Android 接入流程:


1.浅析 Demo


通常第一步都是下载官方 Demo 后,进行一番调试,盘点功能列表,是否符合自身需求。


链接:pan.baidu.com/s/1RX5yGp06… 提取码:qbx0


接下来,简单分析 Demo 项目架构,帮助大家尽快上手这个项目。


首先,我们要知道这个 Demo,是包括【一键登录】和【本机号码校验】两个功能。根据自己的需求分析对应的代码即可。这次我们只使用到前者,所以后者内容不在这里讲述。



主要看到下列三个模块:


Config - 就是上面预告知识中说到的配置项,主要是授权页的一些配置项


OneKeyLoginActivity - 登录页面


MessageActivity - 模拟【其他登录方式】页面


那具体的实现,就可以直接看对应模块的内容即可。可以在原 Demo,进行调试。


2.接入思路分析


基于判断是否支持【一键登录】的时机 提供两种接入思路


第一种:启动登录功能前判断



判断的方式可以通过


mPhoneNumberAuthHelper.checkEnvAvailable(PhoneNumberAuthHelper.SERVICE_TYPE_LOGIN)
复制代码

是否支持【一键登录】。该流程未经检验,大家可以执行验证。


第二种:直接唤起【一键登录】,失败后再唤起【其他登录方式】



Demo 也是第二种方式。这种方式需要用到一个壳 Activity 。但这个壳主要的作用是初始化SDK,以及做逻辑判断和处理(即并无实际内容展示)。


这里引发一个思考:


既然用不到 Activity 的内容,那能不能换种方式呢呢?对于单例,我思考后,一开始觉得是没问题的,但是等写完后,发现我写成了一个 OneKeyLoginHelper 的单例,发现相应逻辑处理需要传入 activity 或者 fragment 的引用。那么我们知道单例中是不能持有这样的引用的(这里可以考虑使用弱引用),这会导致内容泄漏。不知道是否还有其他的方法?


3.代码接入流程


//STEP 1.初始化监听器(这里根据业务自己做处理)


//STEP 2.初始化SDK实例


//STEP 3.设置SDK秘钥


//STEP 4.唤起一键登录页


4.避坑


接着,讲一下接入过程中,遇到的一些问题。帮大家避免无效劳动,可以有更多的时间学(hua )习(shui)。


问题描述: 因为选择了第二种思路,那么会有个壳 Activity 的问题。这个壳,我们不处理的话,是不透明的,这样当我们进到这个壳的时候,再跳转到别的页面就会有个空白页。


解决方案: 将壳的主题改为透明色,经过实验,下述代码可以实现。(壳Activity 需要继承 AppCompatActivity)


<style name="Theme.Transparent" parent="@style/Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:backgroundDimEnabled">false</item>
</style>

问题描述: 发现从【授权页】跳到【其他方式登录】的时候,授权页会逐渐变透明,会看到下一层页面的内容。如动图中,粉红色的箭头所示。



解决方案: 可以直接忽略,这个是 SDK 本身的问题。因为阿里那边给的回复是:(是否有最新解决方案,会及时更新,或者可以以你们当时咨询的为准)


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

记录 Kotlin 实践的一些好建议

目录 注释 函数式接口 高阶函数 扩展函数 注释 Java:    /**     * @see AdVideoUserInfoContainerData#type     */ &nbs...
继续阅读 »

目录



  1. 注释

  2. 函数式接口

  3. 高阶函数

  4. 扩展函数


注释


Java:


    /**
    * @see AdVideoUserInfoContainerData#type
    */
   public Builder type(int type) {
       userInfoData.type = type;
       return this;
  }
   /** 事件配置, 对应于 {@link FeedAdLottieRepoInfo#name 属性} */
   public String lottieConfig;

Kotlin:


/**
* 由[CountDownType.type] mapTo [CountDownType]
* 避免了使用 when(type) 写 else 了
*/
private fun type2Enum(type: () -> String): CountDownType {
   return CountDownType.values().firstOrNull {
       it.type == type()
  } ?: CountDownType.CIRCLE
}

Kotlin 可以使用内联标记来引用类、方法、属性等,这比 Java 中的 @see、@link 更加易用。


文档:kotlinlang.org/docs/kotlin…


函数式接口


非函数式接口:


internal interface ICountDownCallback {
   /**
    * 倒计时完成时回调
    */
   fun finish()
}

internal fun setCountDownCallback(callback: ICountDownCallback) {
   // ignore
}

internal fun show() {
   setCountDownCallback(object : ICountDownCallback {
       override fun finish() {
           TODO("Not yet implemented")
      }
  })
}

函数式接口:


internal fun interface ICountDownCallback {
   /**
    * 倒计时完成时回调
    */
   fun finish()
}

internal fun setCountDownCallback(callback: ICountDownCallback) {
   // ignore
}

internal fun show() {
   setCountDownCallback {
       TODO("Not yet implemented")
  }
}

函数式接口也被称为单一抽象方法(SAM)接口,使用函数式接口可以使代码更加简洁,富有表现力。


对于 Java 的接口,比如 View.OnClickListener,它在使用的时候可以直接转 lambda 使用的,只有是 kotlin 的单一抽象方法,需要加 fun 关键字标示它为函数式接口。


文档:kotlinlang.org/docs/fun-in…


高阶函数


如果对象的初始化比较麻烦,可以使用高阶函数,让代码更加流畅:


    // 定义
open fun attachToViewGroup(
       viewGroup: ViewGroup,
       index: Int = -1,
       lp: () -> MarginLayoutParams = {
           MarginLayoutParams(
               LayoutParams.WRAP_CONTENT,
               LayoutParams.WRAP_CONTENT
          )
      }
  ) {
      (this.parent as? ViewGroup)?.removeView(this)
       viewGroup.addView(this, lp.invoke())
  }

// 使用
   override fun attachToViewGroup(viewGroup: ViewGroup, index: Int, lp: () -> MarginLayoutParams) {
       super.attachToViewGroup(viewGroup, index) {
           MarginLayoutParams(
               ViewGroup.LayoutParams.WRAP_CONTENT,
               ViewGroup.LayoutParams.WRAP_CONTENT
          ).apply {
               leftMargin = 14.px(context)
               topMargin = 44.px(context)
          }
      }
  }

如果参数的获取比较复杂,代码比较长,有不少判断逻辑,也可以使用高阶函数:


// 定义
fun getCountDownViewByType(context: Context, type: () -> String = { "0" }) {
// ignore
}
// 使用
countDownView = CountDownType.getCountDownViewByType(this) {
rewardVideoCmdData.cmdPolicyData?.countDownType ?: ""
}

如果方法的返回值是一个状态值,然后根据状态值去做相关逻辑处理。这种情况下,其实我们想要的是一个行为,比如代码中充斥着大量的数据解析、校验等逻辑,我们也可以是使用高阶函数重构:


// 重构之前
/**
* 校验数据有效(校验标题和按钮有一个不为空,就可以展示 Dialog)
*/
fun checkValid(): Boolean {
   return !dialogTitle.isNullOrEmpty() || !buttonList.isNullOrEmpty()
}

private fun bindData() {
   rewardData = RewardDialogData(arguments?.getString(EXTRA_REWARD_DATA) ?: "")
   // 弹窗数据不合法,就不需要展示 dialog 了
   if (rewardData == null || !rewardData!!.checkValid()) {
       dismiss()
       return
  }
   // 更新字体颜色等
   updateSkin()
}


// 重构之后
/**
* 数据校验失败,执行 [fail] 函数
*/
internal inline fun RewardDialogData?.checkFailed(fail: () -> Unit) {
   this?.let {
       if (dialogTitle.isNullOrEmpty() && buttonList.isNullOrEmpty()) {
           fail()
      }
  } ?: fail()
}


private fun bindData() {
   rewardData = RewardDialogData(arguments?.getString(EXTRA_REWARD_DATA) ?: "")
   // 弹窗数据不合法,就不需要展示 dialog 了
   rewardData?.checkFailed {
       dismiss()
       return
  }
   // 更新字体颜色等
   updateSkin()
}

kotlin 标准库里面也是有非常多的高阶函数的,比如作用域函数(let、apply、run等等),除此之外,还有一些集合类的标准库函数:


// filter
fun showCharge() {
   adMonitorUrl?.filter {
       !it.showUrl.isNullOrEmpty()
  }?.forEach {
       ParallelCharge.charge(it.showUrl)
  }
}
// forEachIndexed
list.forEachIndexed { index, i ->
// ignore
}

文档:kotlinlang.org/docs/lambda…


扩展函数


// 比较不流畅的写法
val topImgUrl = rewardData?.topImg
if (topImgUrl.isNullOrBlank()) {
   topImg.visibility = View.GONE
} else {
   topImg.hierarchy?.useGlobalColorFilter = false
   topImg.visibility = View.VISIBLE
   topImg.setImageURI(topImgUrl)
}

// 使用局部返回标签
topImg.apply {
   if (topImgUrl.isNullOrEmpty()) {
       visibility = View.GONE
       return@apply
  }
   hierarchy?.useGlobalColorFilter = false
   setImageURI(topImgUrl)
   visibility = View.VISIBLE
}

/**
* 校验 View 可见性
*
* @return [predicate] false: GONE;true: VISIBLE
*/
internal inline fun <reified T : View> T.checkVisible(predicate: () -> Boolean): T? {
   return if (predicate()) {
       visibility = View.VISIBLE
       this
  } else {
       visibility = View.GONE
       null
  }
}

// 使用扩展函数
topImg.checkVisible {
   !topImgUrl.isNullOrEmpty()
}?.run {
   hierarchy?.useGlobalColorFilter = false
   setImageURI(topImgUrl)
}

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

我们需要从单体转到微服务吗?

起源martinfowler.com/articles/microservices.html。和微服务相对应的是单体架构,先来看看单体架构是怎样的。大多数人做软件开发应该都是从单体架构开始的,以 .NET 程序员来说,从最早的 WebForm、到后来的 MVC...
继续阅读 »

微服务或许你没有真正实践过,但一定听说过,虽然已经到了 2022 年,这个词依然很热,可以通过搜索 google 指数看得到。

起源

“微服务”一词源于 2011 年 5 月在威尼斯附近的一次软件架构师研讨会上进行的架构风格的讨论。2012 年 5 月 讨论小组决定将这种架构风格命名为“微服务”。Fred George 同年在一次技术大会上进行自己的微服务实践分享,并说微服务是一种细粒度的 SOA ,但最终将其发扬光大的是 Martin Fowler 2014 年写的博文《 Microservices 》,原文链接如下:

martinfowler.com/articles/microservices.html

自此以后,微服务就家喻户晓了,Microservices 的 Google 指数也是在 2014 年后就一路飙升。

和微服务相对应的是单体架构,先来看看单体架构是怎样的。

单体架构

大多数人做软件开发应该都是从单体架构开始的,以 .NET 程序员来说,从最早的 WebForm、到后来的 MVC、再到现在的前后端分离,后端使用 .NET 的 WebAPI ,都是整个项目的代码放到一个解决方案中,发布要么直接整个目录进行替换,或者更新有变更的 dll 文件。

包括到现在,这种单体架构的模该还占着很大的比重,凡是存在,必有道理,单体架构有着他的可取之处:

  • 开发方便,.NET 程序员只需只需使用宇宙最强 IDE VS 就可以。

  • 调试方便,在开发阶段,所有的项目都在一个解决方案下,项目之间是可以直接引用,断点可以到达你想要的任何地方。

  • 运行方便,编码完成,只需一个 F5 搞定。

  • 部署方便,无论是之前部署 IIS ,还是现在的容器部署,都只涉及到一个发布目录。

不过,随着产品的功能越来越复杂,代码也会变得越来越复杂,团队的人数也会越来越多,这时单体架构就会带来一些问题:

  • 因为代码库非常的臃肿,从编译、构建、运行到测试这个时间会越来越长。

  • 技术栈几乎是受限的,比如一个 .NET 的工程,基本就是 C# 来开发了,不太可能混杂其他的语言。

  • 不方便横向扩展,只能整套程序进行扩,满足所有模块的需求,对资源的利用率非常差。

  • 不够敏捷,团队成员越来越多多时,都在同一个代码上进行修改、提交、合并,容易引发冲突和其他问题。

  • 一个很小的改动点,容易引发全身问题,导致系统崩溃,因为影响点多,测试成本也会很高。

  • 缺乏可靠性,我们就碰到过因为一个序列化的问题导致 CPU 占用很高,结果整个系统瘫痪了。

微服务架构

上面提到的单体架构存在的问题,采用微服务架构可以很好地解决。微服务的核心是为了解耦,构建成一个松耦合的分布式系统。

一个庞大的单体系统拆分成若干个小的服务,每个服务可以由一个小的团队来维护,团队会更加敏捷,构建发布的时间更短,代码也容易维护。

不同的微服务团队可以采用不同的技术栈,比如工作流引擎使用 .NET ,规则引擎可以使用 Java ,一些全新的模块更容易采用新的技术,人员流动和补充上也更加灵活。

每个服务通常采用独立的数据库,代码或者数据库层面的问题不会导致整个系统的崩溃。

扩展方便,这个很重要,如果监控发现流程引擎的压力很大,可以只针对这个服务进行横向扩展,服务器资源可以得到更好的利用。

上面说的都是好处,但没有任何一种技术是银弹,微服务解决问题的同时,也会带来更多的问题。

1、开发调试变得困难了,需要通过日志的方式或者借助一些远程调试工具。

2、单体架构中,模块之间的调用都是进程内,添加类库的引用后,就是本地方法的调用,微服务各自独立部署,就会涉及到进程间的通信。

3、线上问题往往需要多个服务团队一起来协作解决,会存在互相甩锅的问题。

4、在分布式系统中,事务、数据一致性、联合查询等相比较单体更加复杂。

5、持续集成、部署、运维的复杂度也显著提升。

6、随着服务越来越多,客户端怎样去找这些服务呢?

7、进程内的访问不存在网络的问题,拆分后的服务可能在同个机器的不同进程,更多的时候是不同机器的不同进程,网络问题导致服务不稳定怎么办?

为了解决这些问题,各种中间件和框架就应运而生,又会带来更多的学习成本。

在 .NET 技术栈中,会用到下面这些中间件:

  • 服务注册与发现:Consul。

  • 网关:Ocelot。

  • 熔断降级:Polly。

  • 服务链路追踪:SkyWalking 或 Twitter 的 Zipkin。

  • 配置中心:Apllo。

  • 鉴权中心:IdentityServer4。

在 Java 中也有 Spring Cloud 和 Spring Cloud Alibaba 这种全家桶套件可以使用。

要不要转微服务呢?

从单体到微服务是一个权衡和取舍的问题,切记不要跟风。以我的经验来看,可以分为两类:

  1. 做企业级系统。

  2. 做互联网系统。

做企业级应用大多都是项目交付型的,客户关系维系的好,后面可以做二期、三期,当然也有一锤子买卖的。这其中一个关键点是要快,单从快速来看,采用单体架构,开发、调试、部署都是最快的。

从客户角度来说,只要能满足业务,是单体还是微服务其实不太关心。

做互联网应用,也就是我们常说的 SaaS,也分为两种情况:

1、将现有的私有化部署的系统(单体架构)改造成支持 SaaS 的模式。

这种我也不建议一上来就大刀阔斧地进行微服务改造,可以在代码的结构上做一些调整,比如按照领域去拆分目录,不同领域之间的调用可以再进行一层抽象,目的是为了未来向微服务架构转化。

当团队的技术栈变得丰富了,比如原先只有 .NET ,现在有些模块采用的是 Java ,这时已然是朝着微服务架构发展了,只是粒度比较大而已,相应的一些中间件也需要引入,比如服务网关、服务发现、服务间通信等。

2、从零开始做一个 SaaS 系统。

互联网系统和企业级系统有很大的差别,如果说企业级系统更多关注功能性需求,那么互联网系统除了功能性需求,还需要关注非功能性需求,比如:横向扩展、限流降级、日志追踪、预警、灰度发布等。

即便因为时间关系,一开始是单体架构,我觉得也应该是微服务架构的单体,随着持续迭代和发展,根据实际情况逐步进行拆分。

如果时间上比较充裕,可以一开始就按照微服务架构进行分离,但粒度不要太小。

总结

  1. 解决常说的的三高问题(高并发、高性能、高可用),一个核心的思路就是拆,分而治之,所以说微服务肯定是能解决掉我们的很多问题,也是发展方向。

  2. 实践微服务需要根据当前的实际情况,如果单体运行的很好,也没什么问题,也不要为了炫技进行微服务改造。

  3. 如果决定要实践微服务,先做好单体架构的设计,让代码遵循面向对象的设计原则,否则即便形式上变成了微服务,也不能尝到微服务的甜头。

作者:不止dotNET

收起阅读 »

牛逼,一款 996 代码分析工具

程序员是一个创作型的职业,频繁的加班并不能增加产出,而国内 996 的公司文化,真的一言难尽。但是如果你进到一家公司,你能从哪些观察来判断这家公司的工作强度的(加班文化)?是看大家走得早不早吗?有一定的参考意义,但是如果走得晚呢,可能是大家不敢提前走而在公司耗...
继续阅读 »

程序员是一个创作型的职业,频繁的加班并不能增加产出,而国内 996 的公司文化,真的一言难尽。但是如果你进到一家公司,你能从哪些观察来判断这家公司的工作强度的(加班文化)?是看大家走得早不早吗?有一定的参考意义,但是如果走得晚呢,可能是大家不敢提前走而在公司耗时间。

今天要推荐一个代码分析工具 code996,它可以统计 Git 项目的 commit 时间分布,进而推导出这个项目的编码工作强度。这算是一种对项目更了解的方式,杜绝 996 从了解数据开始。

我们先来看 code996 分析出来的结果示例,以下是分析项目的基本情况:


通过图表查看 commit 提交分布:


对比项目工作时间类型:


如果你对 code996 是如何工作的,以下是作者的说明:


因为代码是公司的很重要的资产,泄露是肯定不行的,为了解决大家的后顾之忧,该项目是完全安全的。


code996 除了能够分析项目的实际工作强度,也能用来分析我们代码编写的情况,对自身了解自己代码编写效率的时段、最近的工作强度等都是非常好的一个输入。

更多项目详情请查看如下链接。

开源项目地址:https://github.com/hellodigua/code996

开源项目作者:hellodigua

收起阅读 »

七夕,程序员到底该送什么礼物给女朋友?参与讨论有奖励哦

据统计,程序员只有30%是有女朋友的,还有15%是有男朋友的,而已经结婚的占50%。我想,你总不至于是剩下的5%吧!马上情人节了,想好送什么礼物没?柳天明、美国队长、Jiayun、李全喜、conanma柳天明、美国队长、Jiayun、李全喜、conanma
据统计,程序员只有30%是有女朋友的,还有15%是有男朋友的,而已经结婚的占50%。
我想,你总不至于是剩下的5%吧!
马上情人节了,想好送什么礼物没?

获奖名单:

柳天明、美国队长、Jiayun、李全喜、conanma

请以上获奖同学8月13日24:00前私信我邮寄信息!

马上就要情人节了又到了着急买什么送给对象的时候了!

作为一名程序员,如何快乐简单不掉头发的为各种节日安排好女朋友的礼物可能是个难题。

当网络上在讨论程序员的时候,脸谱化的礼物往往就是 鼠标/键盘/降噪耳机,但我知道,每一个程序员,除了程序人生之外,都有自己的精彩而美丽的生活。对你的女朋友来说,也是同样。

先看看各路大牛脑洞大开:

  • 高德地图实现“爱心”轨迹


(来源:juejin.cn/post/7126576400441540621)

  • 浪漫邂逅小动画


(源码:https://github.com/alexwjj/qixi)

  • 情诗表白墙


(来源:juejin.cn/post/7127210046840111117,预览

  • 无法拒绝的表白


(源码:https://github.com/andyngojs/crush-love)

  • 浪漫专属chrome插件


(作者:蜡笔小心_)

  • 代码+谜语系列




所有的相遇,都是命中注定:

致橡树(舒婷):





不得不说,这些礼物都很有特色,

但是,别人需要的是这些吗?

别人需要的是口红,是包包,是各种首饰

你整那些,就好比看到她打扫卫生辛苦了,

自己吃个冰镇瓜、自己打把游戏、自己找朋友K歌,

把自己的享受作为对她的奖励,这合理吗?

5%的群体正在向你招手欢迎!


各位情场得意的码农届高质量群体请分享下,你在七夕送了或得到了什么礼物?

我们这些直男最擅长复制粘贴和clone了。

参与回复的5人有机会获得imgeek准备的小礼物~


获奖名单:

柳天明、美国队长、Jiayun、李全喜、conanma

请以上获奖同学8月13日24:00前私信我邮寄信息!

一天高中的女同桌突然问我是不是程序猿

背景 昨天一个我高中的女同桌突然发微信问我“你是不是程序猿 我有问题求助”, 先是激动后是茫然再是冷静,毕业多年不见联系,突然发个信息求助,感觉大脑有点反应不过来... 再说我一个搞Android的也不咋会python啊(不是说Java不能实现,大家懂的,人...
继续阅读 »

背景


昨天一个我高中的女同桌突然发微信问我“你是不是程序猿 我有问题求助”,


image-20211015101843733.png


先是激动后是茫然再是冷静,毕业多年不见联系,突然发个信息求助,感觉大脑有点反应不过来... 再说我一个搞Android的也不咋会python啊(不是说Java不能实现,大家懂的,人生苦短,我用python),即使如此,
为了大家的面子,为了程序猿们的脸,不就简单的小Python嘛,必须答应!


梳理需求


现有excel表格记录着 有效图片的名字,如:


image-20211015103418631.png


要从一个文件夹里把excel表格里记录名字的图片筛选出来;


需求也不是很难,代码思路就有了:



  1. 读取Excel表格第一列的信息并放入A集合

  2. 遍历文件夹下所有的文件,判断文件名字是否存在A集合

  3. 存在A集合则拷贝到目标文件夹


实现(Python 2.7)


读取Excel表格

加载Excel表格的方法有很多种,例如pandasxlrdopenpyxl,我这里选择openpyxl库,
先安装库



pip install openpyxl



代码如下:


from openpyxl import load_workbook

def handler_excel(filename=r'C:/Users/xxx/Desktop/haha.xlsx'):
   # 根据文件路径加载一个excel表格,这里包含所有的sheet
   excel = load_workbook(filename)
   # 根据sheet名称加载对应的table
   table = excel.get_sheet_by_name('Sheet1')
   imgnames = []
   # 读取所有列
   for column in table.columns:
       for cell in column:
           imgnames.append(cell.value+".png")
# 选择图片
   pickImg(imgnames)

遍历文件夹读取文件名,找到target并拷贝

使用os.listdir 方法遍历文件,这里注意windows环境下拿到的unicode编码,需要GBK重新解码


def pickImg(pickImageNames):
   # 遍历所有图片集的文件名
   for image in os.listdir(
           r"C:\Users\xxx\Desktop\work\img"):
       # 使用gbk解码,不然中文乱码
       u_file = image.decode('gbk')
       print(u_file)
       if u_file in pickImageNames:
           oldname = r"C:\Users\xxx\Desktop\work\img/" + image
           newname = r"C:\Users\xxx\Desktop\work\target/" + image
           # 文件拷贝
           shutil.copyfile(oldname, newname)

简单搞定!没有砸程序猿的招牌,豪横的把成果发给女同桌,结果:


image-20211015112550343.png


换来有机会请你吃饭,微信都不带回的,哎 ,xdm,小丑竟是我自己!
小丑竟是我自己什么梗-小丑竟是我自己是什么意思出自什么-55手游网


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

Kotlin协程之Dispatchers原理

Kotlin协程不是什么空中阁楼,Kotlin源代码会被编译成class字节码文件,最终会运行到虚拟机中。所以从本质上讲,Kotlin和Java是类似的,都是可以编译产生class的语言,但最终还是会受到虚拟机的限制,它们的代码最终会在虚拟机上的某个线程上被执...
继续阅读 »

Kotlin协程不是什么空中阁楼,Kotlin源代码会被编译成class字节码文件,最终会运行到虚拟机中。所以从本质上讲,Kotlin和Java是类似的,都是可以编译产生class的语言,但最终还是会受到虚拟机的限制,它们的代码最终会在虚拟机上的某个线程上被执行。


之前我们分析了launch的原理,但当时我们没有去分析协程创建出来后是如何与线程产生关联的,怎么被分发到具体的线程上执行的,本篇文章就带大家分析一下。


前置知识


要想搞懂Dispatchers,我们先来看一下Dispatchers、CoroutineDispatcher、ContinuationInterceptor、CoroutineContext之间的关系


public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = DefaultScheduler

@JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher

@JvmStatic
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined

@JvmStatic
public val IO: CoroutineDispatcher = DefaultIoScheduler
}

public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
}

public interface ContinuationInterceptor : CoroutineContext.Element {}

public interface Element : CoroutineContext {}

Dispatchers中存放的是协程调度器(它本身是一个单例),有我们平时常用的IO、Default、Main等。这些协程调度器都是CoroutineDispatcher的子类,这些协程调度器其实都是CoroutineContext


demo


我们先来看一个关于launch的demo:


fun main() {
val coroutineScope = CoroutineScope(Job())
coroutineScope.launch {
println("Thread : ${Thread.currentThread().name}")
}
Thread.sleep(5000L)
}

在生成CoroutineScope时,demo中没有传入相关的协程调度器,也就是Dispatchers。那这个launch会运行到哪个线程之上?


运行试一下:


Thread : DefaultDispatcher-worker-1

居然运行到了DefaultDispatcher-worker-1线程上,这看起来明显是Dispatchers.Default协程调度器里面的线程。我明明没传Dispatchers相关的context,居然会运行到子线程上。说明运行到default线程是launch默认的。


它是怎么与default线程产生关联的?打开源码一探究竟:


public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
//代码1
val newContext = newCoroutineContext(context)

//代码2
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)

//代码3
coroutine.start(start, coroutine, block)
return coroutine
}


  1. 将传入的CoroutineContext构造出新的context

  2. 启动模式,判断是否为懒加载,如果是懒加载则构建懒加载协程对象,否则就是标准的

  3. 启动协程


我们重点关注代码1,这是与CoroutineContext相关的。


public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
//从父协程那里继承过来的context+这次的context
val combined = coroutineContext.foldCopiesForChildCoroutine() + context
val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
//combined可以简单的把它看成是一个map,它是CoroutineContext类型的
//如果当前context不等于Dispatchers.Default,而且从map里面取ContinuationInterceptor(用于拦截之后分发线程的)值为空,说明没有传入协程应该在哪个线程上运行的相关参数
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
debug + Dispatchers.Default else debug
}

调用launch的时候,我们没有传入context,默认参数是EmptyCoroutineContext。这里的combined,它其实是CoroutineContext类型的,可以简单的看成是map(其实不是,只是类似)。通过combined[ContinuationInterceptor]可以将传入的线程调度相关的参数给取出来,这里如果取出来为空,是给该context添加了一个Dispatchers.Default,然后把新的context返回出去了。所以launch默认情况下,会走到default线程去执行。


补充一点:CoroutineContext能够通过+连接是因为它内部有个public operator fun plus函数。能够通过combined[ContinuationInterceptor]这种方式访问元素是因为有个public operator fun get函数。


public interface CoroutineContext {
/**
* Returns the element with the given [key] from this context or `null`.
*/
public operator fun <E : Element> get(key: Key<E>): E?

/**
* Returns a context containing elements from this context and elements from other [context].
* The elements from this context with the same key as in the other one are dropped.
*/
public operator fun plus(context: CoroutineContext): CoroutineContext {
......
}
}

startCoroutineCancellable


上面我们分析了launch默认情况下,context中会增加Dispatchers.Default的这个协程调度器,到时launch的Lambda会在default线程上执行,其中具体流程是怎么样的,我们分析一下。


在之前的文章 Kotlin协程之launch原理 中我们分析过,launch默认情况下会最终执行到startCoroutineCancellable函数。


public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>): Unit = runSafely(completion) {
//构建ContinuationImpl
createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit))
}

public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
completion: Continuation<T>
): Continuation<Unit> {
val probeCompletion = probeCoroutineCreated(completion)
return if (this is BaseContinuationImpl)
//走这里
create(probeCompletion)
else
createCoroutineFromSuspendFunction(probeCompletion) {
(this as Function1<Continuation<T>, Any?>).invoke(it)
}
}

Kotlin协程之launch原理 文章中,咱们分析过create(probeCompletion)这里创建出来的是launch的那个Lambda,编译器会产生一个匿名内部类,它继承自SuspendLambda,而SuspendLambda是继承自ContinuationImpl。所以 createCoroutineUnintercepted(completion)一开始构建出来的是一个ContinuationImpl,接下来需要去看它的intercepted()函数。


internal abstract class ContinuationImpl(
completion: Continuation<Any?>?,
private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {
constructor(completion: Continuation<Any?>?) : this(completion, completion?.context)

public override val context: CoroutineContext
get() = _context!!

@Transient
private var intercepted: Continuation<Any?>? = null

public fun intercepted(): Continuation<Any?> =
intercepted
?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
.also { intercepted = it }
}

第一次走到intercepted()函数时,intercepted肯定是为null的,还没初始化。此时会通过context[ContinuationInterceptor]取出Dispatcher对象,然后调用该Dispatcher对象的interceptContinuation()函数。这个Dispatcher对象在demo这里其实就是Dispatchers.Default。


public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = DefaultScheduler
}

可以看到,Dispatchers.Default是一个CoroutineDispatcher对象,interceptContinuation()函数就在CoroutineDispatcher中。


public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
DispatchedContinuation(this, continuation)
}

public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>): Unit = runSafely(completion) {
createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit))
}

这个方法非常简单,就是新建并且返回了一个DispatchedContinuation对象,将this和continuation给传入进去。这里的this是Dispatchers.Default。


所以,最终我们发现走完startCoroutineCancellable的前2步之后,也就是走完intercepted()之后,创建的是DispatchedContinuation对象,最后是调用的DispatchedContinuation的resumeCancellableWith函数。最后这步比较关键,这是真正将协程的具体执行逻辑放到线程上执行的部分。


internal class DispatchedContinuation<in T>(
//这里传入的dispatcher在demo中是Dispatchers.Default
@JvmField val dispatcher: CoroutineDispatcher,
@JvmField val continuation: Continuation<T>
) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation {

inline fun resumeCancellableWith(
result: Result<T>,
noinline onCancellation: ((cause: Throwable) -> Unit)?
) {
val state = result.toState(onCancellation)
//代码1
if (dispatcher.isDispatchNeeded(context)) {
_state = state
resumeMode = MODE_CANCELLABLE
//代码2
dispatcher.dispatch(context, this)
} else {
//代码3
executeUnconfined(state, MODE_CANCELLABLE) {
if (!resumeCancelled(state)) {
resumeUndispatchedWith(result)
}
}
}
}
}

internal abstract class DispatchedTask<in T>(
@JvmField public var resumeMode: Int
) : SchedulerTask() {
......
}

internal actual typealias SchedulerTask = Task

internal abstract class Task(
@JvmField var submissionTime: Long,
@JvmField var taskContext: TaskContext
) : Runnable {
......
}

public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {

public abstract fun dispatch(context: CoroutineContext, block: Runnable)

public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true

}

从DispatchedContinuation的继承结构来看,它既是一个Continuation(通过委托给传入的continuation参数),也是一个Runnable。



  • 首先看代码1:这个dispatcher在demo中其实是Dispatchers.Default ,然后调用它的isDispatchNeeded(),这个函数定义在CoroutineDispatcher中,默认就是返回true,只有Dispatchers.Unconfined返回false

  • 代码2:调用Dispatchers.Default的dispatch函数,将context和自己(DispatchedContinuation,也就是Runnable)传过去了

  • 代码3:对应Dispatchers.Unconfined的情况,它的isDispatchNeeded()返回false


现在我们要分析代码2之后的执行逻辑,也就是将context和Runnable传入到dispatch函数之后是怎么执行的。按道理,看到Runnable,那可能这个与线程执行相关,应该离我们想要的答案不远了。回到Dispatchers,我们发现Dispatchers.Default是DefaultScheduler类型的,那我们就去DefaultScheduler中或者其父类中去找dispatch函数。


public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = DefaultScheduler
}

internal object DefaultScheduler : SchedulerCoroutineDispatcher(
CORE_POOL_SIZE, MAX_POOL_SIZE,
IDLE_WORKER_KEEP_ALIVE_NS, DEFAULT_SCHEDULER_NAME
) {
......
}

internal open class SchedulerCoroutineDispatcher(
private val corePoolSize: Int = CORE_POOL_SIZE,
private val maxPoolSize: Int = MAX_POOL_SIZE,
private val idleWorkerKeepAliveNs: Long = IDLE_WORKER_KEEP_ALIVE_NS,
private val schedulerName: String = "CoroutineScheduler",
) : ExecutorCoroutineDispatcher() {

private var coroutineScheduler = createScheduler()

private fun createScheduler() =
CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)

override fun dispatch(context: CoroutineContext, block: Runnable): Unit = coroutineScheduler.dispatch(block)
}

最后发现dispatch函数在其父类SchedulerCoroutineDispatcher中,在这里构建了一个CoroutineScheduler,直接调用了CoroutineScheduler对象的dispatch,然后将Runnable(也就是上面的DispatchedContinuation对象)传入。


internal class CoroutineScheduler(
@JvmField val corePoolSize: Int,
@JvmField val maxPoolSize: Int,
@JvmField val idleWorkerKeepAliveNs: Long = IDLE_WORKER_KEEP_ALIVE_NS,
@JvmField val schedulerName: String = DEFAULT_SCHEDULER_NAME
) : Executor, Closeable {
override fun execute(command: Runnable) = dispatch(command)

fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, tailDispatch: Boolean = false) {
trackTask() // this is needed for virtual time support
//代码1:构建Task,Task实现了Runnable接口
val task = createTask(block, taskContext)
//代码2:取当前线程转为Worker对象,Worker是一个继承自Thread的类
val currentWorker = currentWorker()
//代码3:尝试将Task提交到本地队列并根据结果执行相应的操作
val notAdded = currentWorker.submitToLocalQueue(task, tailDispatch)
if (notAdded != null) {
//代码4:notAdded不为null,则再将notAdded(Task)添加到全局队列中
if (!addToGlobalQueue(notAdded)) {
throw RejectedExecutionException("$schedulerName was terminated")
}
}
val skipUnpark = tailDispatch && currentWorker != null
// Checking 'task' instead of 'notAdded' is completely okay
if (task.mode == TASK_NON_BLOCKING) {
if (skipUnpark) return
//代码5: 创建Worker并开始执行该线程
signalCpuWork()
} else {
// Increment blocking tasks anyway
signalBlockingWork(skipUnpark = skipUnpark)
}
}

private fun currentWorker(): Worker? = (Thread.currentThread() as? Worker)?.takeIf { it.scheduler == this }

internal inner class Worker private constructor() : Thread() {
.....
}
}

观察发现,原来CoroutineScheduler类实现了java.util.concurrent.Executor接口,同时实现了它的execute方法,这个方法也会调用dispatch()。



  • 代码1:首先是通过Runnable构建了一个Task,这个Task其实也是实现了Runnable接口,只是把传入的Runnable包装了一下

  • 代码2:将当前线程取出来转换成Worker,当然第一次时,这个转换不会成功,这个Worker是继承自Thread的一个类

  • 代码3:将task提交到本地队列中,这个本地队列待会儿会在Worker这个线程执行时取出Task,并执行Task

  • 代码4:如果task提交到本地队列的过程中没有成功,那么会添加到全局队列中,待会儿也会被Worker取出来Task并执行

  • 代码5:创建Worker线程,并开始执行


开始执行Worker线程之后,我们需要看一下这个线程的run方法执行的是啥,也就是它的具体执行逻辑。


internal inner class Worker private constructor() : Thread() {
override fun run() = runWorker()
private fun runWorker() {
var rescanned = false
while (!isTerminated && state != WorkerState.TERMINATED) {
//代码1
val task = findTask(mayHaveLocalTasks)
if (task != null) {
rescanned = false
minDelayUntilStealableTaskNs = 0L
//代码2
executeTask(task)
continue
} else {
mayHaveLocalTasks = false
}
if (minDelayUntilStealableTaskNs != 0L) {
if (!rescanned) {
rescanned = true
} else {
rescanned = false
tryReleaseCpu(WorkerState.PARKING)
interrupted()
LockSupport.parkNanos(minDelayUntilStealableTaskNs)
minDelayUntilStealableTaskNs = 0L
}
continue
}
tryPark()
}
tryReleaseCpu(WorkerState.TERMINATED)
}

fun findTask(scanLocalQueue: Boolean): Task? {
if (tryAcquireCpuPermit()) return findAnyTask(scanLocalQueue)
// If we can't acquire a CPU permit -- attempt to find blocking task
val task = if (scanLocalQueue) {
localQueue.poll() ?: globalBlockingQueue.removeFirstOrNull()
} else {
globalBlockingQueue.removeFirstOrNull()
}
return task ?: trySteal(blockingOnly = true)
}

private fun executeTask(task: Task) {
val taskMode = task.mode
idleReset(taskMode)
beforeTask(taskMode)
runSafely(task)
afterTask(taskMode)
}

fun runSafely(task: Task) {
try {
task.run()
} catch (e: Throwable) {
val thread = Thread.currentThread()
thread.uncaughtExceptionHandler.uncaughtException(thread, e)
} finally {
unTrackTask()
}
}

}

run方法直接调用的runWorker(),在里面是一个while循环,不断从队列中取Task来执行。



  • 代码1:从本地队列或者全局队列中取出Task

  • 代码2:执行这个task,最终其实就是调用这个Runnable的run方法。


也就是说,在Worker这个线程中,执行了这个Runnable的run方法。还记得这个Runnable是谁么?它就是上面我们看过的DispatchedContinuation,这里的run方法执行的就是协程任务,那这块具体的run方法的实现逻辑,我们应该到DispatchedContinuation中去找。



internal class DispatchedContinuation<in T>(
@JvmField val dispatcher: CoroutineDispatcher,
@JvmField val continuation: Continuation<T>
) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation {
......
}

internal abstract class DispatchedTask<in T>(
@JvmField public var resumeMode: Int
) : SchedulerTask() {
public final override fun run() {
assert { resumeMode != MODE_UNINITIALIZED } // should have been set before dispatching
val taskContext = this.taskContext
var fatalException: Throwable? = null
try {
val delegate = delegate as DispatchedContinuation<T>
val continuation = delegate.continuation
withContinuationContext(continuation, delegate.countOrElement) {
val context = continuation.context
val state = takeState() // NOTE: Must take state in any case, even if cancelled
val exception = getExceptionalResult(state)
/*
* Check whether continuation was originally resumed with an exception.
* If so, it dominates cancellation, otherwise the original exception
* will be silently lost.
*/
val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null

//非空,且未处于active状态
if (job != null && !job.isActive) {
//开始之前,协程已经被取消,将具体的Exception传出去
val cause = job.getCancellationException()
cancelCompletedResult(state, cause)
continuation.resumeWithStackTrace(cause)
} else {
//有异常,传递异常
if (exception != null) {
continuation.resumeWithException(exception)
} else {
//代码1
continuation.resume(getSuccessfulResult(state))
}
}
}
} catch (e: Throwable) {
// This instead of runCatching to have nicer stacktrace and debug experience
fatalException = e
} finally {
val result = runCatching { taskContext.afterTask() }
handleFatalException(fatalException, result.exceptionOrNull())
}
}
}

我们主要看一下代码1处,调用了resume开启协程。前面没有异常,才开始启动协程,这里才是真正的开始启动协程,开始执行launch传入的Lambda表达式。这个时候,协程的逻辑是在Worker这个线程上执行的了,切到某个线程上执行的逻辑已经完成了。



ps: rusume会走到BaseContinuationImpl的rusumeWith,然后走到launch传入的Lambda匿名内部类的invokeSuspend方法,开始执行状态机逻辑。前面的文章 Kotlin协程createCoroutine和startCoroutine原理 我们分析过这里,这里就只是简单提一下。



到这里,Dispatchers的执行流程就算完了,前后都串起来了。


小结


Dispatchers是协程框架中与线程交互的关键。底层会有不同的线程池,Dispatchers.Default、IO,协程任务来了的时候会封装成一个个的Runnable,丢到线程中执行,这些Runnable的run方法中执行的其实就是continuation.resume,也就是launch的Lambda生成的SuspendLambda匿名内部类,也就是开启协程状态机,开始协程的真正执行。


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

Flutter 使用 json_serializable 解析 JSON 支持泛型

一般情况下,服务端接口都会有一套数据结构规范,比如 { "items": [], "success": true, "msg": "" } 不同的接口,items 中返回的数据结构一般都是不一样的,这时使用泛型,可以简化代码 本文将以 ...
继续阅读 »

一般情况下,服务端接口都会有一套数据结构规范,比如


{
"items": [],
"success": true,
"msg": ""
}

不同的接口,items 中返回的数据结构一般都是不一样的,这时使用泛型,可以简化代码


本文将以 wanAndroid 提供的开放 API 为例,介绍如何通过泛型类接解析 JSON 数据,简化代码。另外,对 wanAndroid 提供开放 API 的行为表示感谢。


本文解析 JSON 使用的方案,是官方推荐的 json_serializable,至于为什么选择 json_serializable,可以参考我之前写的一篇文章:Flutter 使用 json_serializable 解析 JSON 最佳方案


下面开始进入正文


使用 json_serializable 支持泛型


json_serializable 在大概两年前发布的 v3.5.0 版本开始支持泛型,只需要在 @JsonSerializable() 注解中设置 genericArgumentFactories 为 true,同时需要对 fromJson 和 toJson 方法进行调整,即可支持泛型解析,如下所示:


@JsonSerializable(genericArgumentFactories: true)
class Response<T> {
int status;
T value;

factory Response.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) =>
_$ResponseFromJson<T>(json, fromJsonT);

Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$ResponseToJson<T>(this, toJsonT);
}

和正常实体类相比,fromJson 方法多了一个函数参数 T Function(dynamic json) fromJsonT;toJson 方法也多了一个函数参数:Object? Function(T value) toJsonT


分析数据结构


下面使用 wanAndroid 开放 API 接口数据,进行代码实践,我们先看一下服务端接口返回的数据结构


一般接口返回数据结构如下:


{
"data": [
{
"desc": "一起来做个App吧",
"id": 10,
"imagePath": "https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png",
"isVisible": 1,
"order": 1,
"title": "一起来做个App吧",
"type": 1,
"url": "https://www.wanandroid.com/blog/show/2"
}
],
"errorCode": 0,
"errorMsg": ""
}

带分页信息的列表接口,返回数据结构如下:


{
"data": {
"curPage": 1,
"datas": [
{
"id": 23300,
"link": "https://juejin.cn/post/7114142706557075487",
"niceDate": "2022-06-28 15:30",
"niceShareDate": "2022-06-28 15:30",
"publishTime": 1656401449000,
"realSuperChapterId": 493,
"shareDate": 1656401449000,
"shareUser": "灰尘",
"superChapterId": 494,
"superChapterName": "广场Tab",
"title": "Flutter 使用 json_serializable 解析 JSON 最佳方案"
}
],
"offset": 0,
"over": false,
"pageCount": 3,
"size": 20,
"total": 46
},
"errorCode": 0,
"errorMsg": ""
}

通过上面的接口示例,我们可以发现,返回的数据结构有以下两种情况:


在一般情况下 data 是一个数组


{
"data": [],
"errorCode": 0,
"errorMsg": ""
}

在分页相关接口,data 是一个对象


{
"data": {},
"errorCode": 0,
"errorMsg": ""
}

复杂方案


如果想定义一个模型类,同时处理上述两种情况,可以把整个 data 都定义为泛型,代码如下:


import 'package:json_annotation/json_annotation.dart';

part 'base_response.g.dart';

@JsonSerializable(genericArgumentFactories: true)
class BaseResponse<T> {
T data;
int errorCode;
String errorMsg;

BaseResponse({
required this.data,
required this.errorCode,
required this.errorMsg,
});

factory BaseResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) =>
_$BaseResponseFromJson<T>(json, fromJsonT);

Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$BaseResponseToJson<T>(this, toJsonT);
}



@JsonSerializable(genericArgumentFactories: true)
class ListData<T> {
int? curPage;
List<T> datas;
int? offset;
bool? over;
int? pageCount;
int? size;
int? total;

ListData({
this.curPage,
required this.datas,
this.offset,
this.over,
this.pageCount,
this.size,
this.total,
});

factory ListData.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) =>
_$ListDataFromJson<T>(json, fromJsonT);

Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$ListDataToJson<T>(this, toJsonT);
}

测试代码如下:


void main() {
test("json", () {
String str =
'{"data": [{"category": "设计","icon": "","id": 31,"link": "https://tool.gifhome.com/compress/","name": "gif压缩","order": 4444,"visible": 1}],"errorCode": 0,"errorMsg": ""}';

Map<String, dynamic> json = jsonDecode(str);

BaseResponse<List<CategoryModel>> result =
BaseResponse.fromJson(json, (json) {
return (json as List<dynamic>)
.map((e) => CategoryModel.fromJson(e as Map<String, dynamic>))
.toList();
});

List<CategoryModel> list = result.data;

CategoryModel model = list[0];

print(model.toJson());

expect("category:设计", "category:${model.category}");
});

test("json list", () {
String str =
'{"data": {"curPage": 1,"datas": [{"id": 23300,"link": "https://juejin.cn/post/7114142706557075487","niceDate": "2022-06-28 15:30","niceShareDate": "2022-06-28 15:30","publishTime": 1656401449000,"realSuperChapterId": 493,"shareDate": 1656401449000,"shareUser": "灰尘","superChapterId": 494,"superChapterName": "广场Tab","title": "Flutter 使用 json_serializable 解析 JSON 最佳方案"}],"offset": 0,"over": false,"pageCount": 3,"size": 20,"total": 46},"errorCode": 0,"errorMsg": ""}';

Map<String, dynamic> json = jsonDecode(str);

BaseResponse<ListData<ArticleModel>> result =
BaseResponse.fromJson(json, (json) {
return ListData.fromJson(json, (json) => ArticleModel.fromJson(json));
});

ListData<ArticleModel> listData = result.data;
List<ArticleModel> datas = listData.datas;
ArticleModel model = datas[0];

print(model.toJson());

expect("id:23300", "id:${model.id}");
});
}

虽然一个 BaseResponse 解决了两种数据结构,但使用时的代码会有些复杂,很容易出错。


一般接口:


    BaseResponse<List<CategoryModel>> result =
BaseResponse.fromJson(json, (json) {
return (json as List<dynamic>)
.map((e) => CategoryModel.fromJson(e as Map<String, dynamic>))
.toList();
});

分页接口:


    BaseResponse<ListData<ArticleModel>> result =
BaseResponse.fromJson(json, (json) {
return ListData.fromJson(json, (json) => ArticleModel.fromJson(json));
});

简化方案


可以对一般接口和列表分页接口进行单独处理,


处理一般接口的泛型类,命名为 BaseCommonResponse,代码如下:


import 'package:json_annotation/json_annotation.dart';

part 'base_common_response.g.dart';

@JsonSerializable(genericArgumentFactories: true)
class BaseCommonResponse<T> {
List<T> data;
int errorCode;
String errorMsg;

BaseCommonResponse({
required this.data,
required this.errorCode,
required this.errorMsg,
});

factory BaseCommonResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) =>
_$BaseCommonResponseFromJson<T>(json, fromJsonT);

Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$BaseCommonResponseToJson<T>(this, toJsonT);
}

处理分页列表接口的泛型类,命令为 BaseListResponse


import 'package:json_annotation/json_annotation.dart';

part 'base_list_response.g.dart';

@JsonSerializable(genericArgumentFactories: true)
class BaseListResponse<T> {
ListData<T> data;
int errorCode;
String errorMsg;

BaseListResponse({
required this.data,
required this.errorCode,
required this.errorMsg,
});

factory BaseListResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) =>
_$BaseListResponseFromJson<T>(json, fromJsonT);

Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$BaseListResponseToJson<T>(this, toJsonT);
}



@JsonSerializable(genericArgumentFactories: true)
class ListData<T> {
int? curPage;
List<T> datas;
int? offset;
bool? over;
int? pageCount;
int? size;
int? total;

ListData({
this.curPage,
required this.datas,
this.offset,
this.over,
this.pageCount,
this.size,
this.total,
});

factory ListData.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) =>
_$ListDataFromJson<T>(json, fromJsonT);

Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$ListDataToJson<T>(this, toJsonT);
}

测试代码如下:


void main() {
test("json", () {
String str =
'{"data": [{"category": "设计","icon": "","id": 31,"link": "https://tool.gifhome.com/compress/","name": "gif压缩","order": 4444,"visible": 1}],"errorCode": 0,"errorMsg": ""}';

Map<String, dynamic> json = jsonDecode(str);

BaseCommonResponse<CategoryModel> result = BaseCommonResponse.fromJson(
json, (json) => CategoryModel.fromJson(json));

List<CategoryModel> list = result.data;

CategoryModel model = list[0];

print(model.toJson());

expect("category:设计", "category:${model.category}");
});

test("json list", () {
String str =
'{"data": {"curPage": 1,"datas": [{"id": 23300,"link": "https://juejin.cn/post/7114142706557075487","niceDate": "2022-06-28 15:30","niceShareDate": "2022-06-28 15:30","publishTime": 1656401449000,"realSuperChapterId": 493,"shareDate": 1656401449000,"shareUser": "灰尘","superChapterId": 494,"superChapterName": "广场Tab","title": "Flutter 使用 json_serializable 解析 JSON 最佳方案"}],"offset": 0,"over": false,"pageCount": 3,"size": 20,"total": 46},"errorCode": 0,"errorMsg": ""}';

Map<String, dynamic> json = jsonDecode(str);

BaseListResponse<ArticleModel> result =
BaseListResponse.fromJson(json, (json) => ArticleModel.fromJson(json));

ListData<ArticleModel> listData = result.data;
List<ArticleModel> datas = listData.datas;
ArticleModel model = datas[0];

print(model.toJson());

expect("id:23300", "id:${model.id}");
});
}

这时使用时的代码,就比较简单了,代码如下:


一般接口,使用 BaseCommonResponse


    BaseCommonResponse<CategoryModel> result = BaseCommonResponse.fromJson(
json, (json) => CategoryModel.fromJson(json));

列表分页接口,使用 BaseListResponse


    BaseListResponse<ArticleModel> result = BaseListResponse.fromJson(
json, (json) => ArticleModel.fromJson(json));

以上就是我在 Flutter 中解析 JSON 数据时处理泛型的实践经验,如果对你有所帮助,欢迎一键三连,👍👍👍


如果大家有相关问题,欢迎评论留言。


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

研发同学应该如何负责好一个项目

引言时间回到6年前,我在京东第一次独立负责项目的前端开发。当时的我可谓雄心壮志,希望表现和证明自己,结果第一次上线就造成了生产事故。后来半年里,我几乎把所有可能的错误都犯了个遍:老板让我搞一个技术方案,我闷头搞了两周也没有给出任何结论;本来安排好的开发计划,被...
继续阅读 »

引言

时间回到6年前,我在京东第一次独立负责项目的前端开发。当时的我可谓雄心壮志,希望表现和证明自己,结果第一次上线就造成了生产事故。

后来半年里,我几乎把所有可能的错误都犯了个遍:老板让我搞一个技术方案,我闷头搞了两周也没有给出任何结论;本来安排好的开发计划,被各种插进来的需求搞得手忙脚乱;参加需求评审没有充分的准备全程提不出问题……那段时间我很苦恼,明明很累很辛苦,但依然拿不到想要的结果。

一晃6年,自己已经从一个小白成长为技术Leader。当我站在更高的视角,我发现身边很多同学不停地重复犯着自己当年类似的错误。经过一段时间的观察与思考,我得出的结论是:他们在工作中缺少方法论的沉淀和指导

“方法论”这个看似虚无缥缈的东西,却犹如指引行动的灯塔,连接着我们的价值观与行动。对于很多研发同学来说,“ 如何负责好一个项目” 是一门只可意会不可言传的玄学,其实不然。

我将我个人经历以及对身边同事的观察,做了一些总结,希望能够给大家带来一些启发,更好地在项目推进过程中指导我们的行动。

一 负责人的定位

这里的负责人不是指的项目经理,而是研发侧牵头和统一对外的那个人,往往是虚线Lead一个项目。

初阶的负责人通常只是做一些需求拆解、任务分配、问题收集和反馈这样一些基础的工作。因为缺乏方法论的指引和对业务的理解,过程中毫无章法、顾此失彼,表现就是像一个救火队长,永远都被事情推着走,很忙很累却拿不到结果。

在我看来,优秀的负责人是这样的:把自己负责的项目当作是一次创业,自己是这个初创团队的CTO,职责是带领这些同学打胜仗(打胜仗是指在产品和技术方案上少走弯路、做出来的东西真的有人用、帮助团队提高效率降低成本、帮助业务带来更多收入和利润)。

难点在于,上面这个表述虽然有了画面感,但具体应该怎么做呢?

二 技术负责人的三大能力

我常和团队的同学讲:技术人的三大支柱是专业技术、项目管理和业务理解。对于负责人而言,则一定是这三项均衡发展。

1、专业技术能力

负责人不见得技术很强,但有深度、有广度、有影响力会走更远。深度很容易理解,广度是指什么呢?


技术负责人需要有广度。广度一方面是 “跨领域” 的部分;比如:大多数情况负责人由后端同学担任,那么最好对前端、质量、算法也了解些,至少跟在讨论方案的时候要有得聊;另一方面是抬高自身视野:在方案评估和决策的时候,能不能看到公司内部其他团队的方案或者行业内部的方案;在做一个决策之前,都了解了哪些方案、不同方案间的对比维度和选择逻辑是什么,现成方案不满足需求时如何处理等。


技术负责人需要有影响力。影响力会让你有足够的自信去把控技术方案:如果自己提出的方案频频被挑战,或者面对别人方案的时候提不出问题和建议,则是削弱技术影响力的行为。在项目中,技术影响力主要体现在三个方面:

(1)内容和技术方案输出的专业性。我见过很多同学在沟通技术方案时都是“口述”,这样的方式既低效又不专业;

(2)工作中要有技术沉淀的意识。比如:效率和质量的提升、稳定性建设等;力争量化结果,拿数据说话;

(3)对技术的场景转化能力。专业性不在于用了哪些高大上的技术,而是用合适的工具解决实际问题,同时能反向推导这一类技术其他的适用场景。

2、项目管理能力

研发项目管理是个很专业的事,我对此的理解是:在有限资源限定的条件下,协同好上下游(包括运营、产品、设计、研发、质量),综合运用专业技能、方法和工具达成项目目标。整个研发项目管理的内容很多,通过观察,我总结出一些负责人在工作中常见问题及注意事项:

(1) 要不卑不亢,对结果负责。既不是高高在上的存在,又不是老好人,在项目中团结好大家,倾听并尊重每一位成员的意见和建议。

负责人在这个过程中要保持空杯心态。初入职场的小白,可能会非常谦虚,但是工作几年之后,专业技能逐步提升,可能还取得了一些小成就,人就会越来越自信。如果不能始终保持“空杯心态”,这种自信就会逐步演变为自满,往往表现为:工作中把别人的建议当成是批评、不喜欢听反对的声音。这样一来,团队里的声音就少了,缺少了交流碰撞,负责人就会成为团队的瓶颈。

(2) 要有敏锐的问题意识以及全局观。可以识别出项目过程中存在的问题和风险、看问题的视角要更高、也要更客观。当遇到问题,不是简单指责哪一方的问题,而是把事情经过还原,弄明白真实原因是什么。


是流程问题、依赖的问题、还是个人能力和态度的问题?在问题归因上干系人是否也这么认为、在解决问题的同时,思考这类问题今后如何规避。当项目遇到风险,不是简单的报备有风险存在,而是如何管理这个风险、过程中做什么努力。

研发面对的绝大多数都是项目延期风险,作为负责人不能单一维度的思考问题。 “上线要delay了通过加班赶工” 这种事情没有任何技术含量,即使刚入职场的实习生都知道。问题和风险不分家,作为负责人不要只看表面因,要多思考过渡原因、根本因是什么,在项目中对症下药。

(3) 要懂“外交”,要学会沟通。自己搞不定的事情,要学会向上沟通和对等沟通,跟什么样的人,说什么样的话。


对等沟通不是职级的对等,而是角色的对等,比如:负责前端的同学在沟通跟测试相关的问题时,最高效的方式是找测试中负责的同学,而不是负责执行的那个同学。

受限于客观条件,项目团队的人员配比不见得是合理的。比如:有的项目或阶段重前端、有的阶段重质量,当某一方相对弱势,除了申请追加资源外,要有补位意识,或者通过技术手段寻找出路。当有一方长期弱势,严重拖累项目进度,要及时识别出来并上升给管理者。

3、业务理解能力

作为负责人,就是要想办法让业务能更好,能让技术的价值有更大的体现。对业务有深刻理解,才知道业务更需要什么,也会更有使命感去推进。

需要特别说明的一点是,理解需求并不意味着理解业务,需求是业务经过产品消化后的产物,可能已经经过演绎,或者是其中某个拆解环节,因此需求并不是业务本身**。当然了解的需求越多,可以让你更清楚业务的全貌。


要理解业务,先要理解用户:他们在干什么、为何而来、到何处去、获得何种收益;然后,了解这里面的商业模式:流量如何来的、内容如何来的、生态情况怎么样、如何商业化的;再站在宏观角度去了解:行业情况怎么样、竞争对手怎么样;最后回到产品和技术:这个业务什么产品在承载、主要对技术的依赖和诉求又是什么。

以上信息了解过后,接下来最好还能够有一些洞察和思考,比如:现在业务发展遇到了什么瓶颈?打算如何破局?基本上把这些摸清楚了,你对这个业务就有个比较清楚的脉络了。

负责人要达到这种程度是需要下苦功夫的,搞清楚上面的问题,接下来要指导自己的行动,比如:要在过程中识别真伪需求、并控制好节奏,要判断哪些功能是一定要做的、哪些是现在这个阶段没必要做但将来可以做的、哪些是完全没必要做的。这里面第二个情况最难识别的。

有了前面这些认知,负责人要做好技术上的规划。优秀的负责人会抬高视野,从思考眼前的事,变成思考未来的事,预判业务未来发展对技术的挑战在哪里。

规划不是空想,是基于对业务理解,预测业务未来发展对技术的挑战,比如:可以通过一系列技术储备,做一些业务方原以为技术不能干、干不好的那些能够直接促进业务发展的事情;还可以发掘业务痛点或机会,然后用技术力量去改善。这里的技术可以是有很大厚度的,比如算法与机器学习、区块链,也可以是不需要技术厚度,但是需要产品设计和链接的,哪怕是简单的技术解决了业务问题。

作为负责人,要有经营意识:通过对数据的洞察寻找问题的答案。在项目中资源永远都是有限的,重要的不是做了多少功能,而是做的东西有多少人用。所以要学会用数据说话,大到交易规模、小到UV/PV等行为埋点,要定期的看,用它来指导你的行为。

一个需求评审前,你是否了解过这个功能的用户量是什么规模?上线后你是否有分析过是否符合最初的假设?这次产品功能上线给业务带来的实际结果是什么?当真实情况和假设间存在偏差,你是否有跟产品了解过背后的原因?当这种问题频频出现,你是否会质疑当前的产品路线和建设节奏有问题?有了这些思考和行动,才会保证项目往正确的方向推进。

三 总结

大家做业务,都有很大的业务压力,但对于技术人的要求,是除了完成技术实现外最大化的体现业务价值,这就需要我们做事情之前有充分的思考,在做事的过程中有正确的章法。

在负责一个项目的时候,要想清楚3个问题:

· 业务的目标是什么;

· 技术团队的策略是什么;

· 我们在里面的价值是什么。

如果3个问题都想明白了,前后的衔接也对了,这事情才靠谱。

回顾自己这7年的成长历程,我总结出技术人的成长诀窍,那便是:不混日子,有自驱;不求安逸,爱折腾

最后,希望大家还是能像最初的时候一样,能多折腾,保留这种折腾劲,甚至是孩子气,如果你还有的话。

来源:李志阳-京东云

原文:mp.weixin.qq.com/s/83qFIDTNCAGxzRzmcm4m_Q

收起阅读 »

抖音 Android 性能优化系列:Java 锁优化

背景Java 多线程开发中为了保证数据的一致性,引入了同步锁(synchronized)。但是,对锁的过度使用,可能导致卡顿问题,甚至 ANR:Systrace 中的主线程因为等锁阻塞了绘制,导致卡顿 Slardar 平台(字节跳动内部 APM 平台,以下简称...
继续阅读 »

背景

Java 多线程开发中为了保证数据的一致性,引入了同步锁(synchronized)。但是,对锁的过度使用,可能导致卡顿问题,甚至 ANR:

  • Systrace 中的主线程因为等锁阻塞了绘制,导致卡顿

  • Slardar 平台(字节跳动内部 APM 平台,以下简称 Slardar)中搜索 waiting to lock 关键字发现很多锁导致的 ANR,仅 Java 锁异常占到总 ANR 的 3.9%


本文将着重向大家介绍 Slardar 线上锁监控方案的原理与使用方法,以及我们在抖音上发现的锁的经典案例与优化实践。

监控方案

获取运行时锁信息的方法有以下几种

方案应用范围特点
systrace线下可以发现锁导致的耗时没有调用栈
定制 ROM线下可以支持调用栈修改 ROM 门槛较高,仅支持特定机型
JVMTI线下只支持 Android8+ 设备不支持 release 包,且性能开销较大

考虑到,很多锁问题需要一定规模的线上用户才能暴露出来,另外没有调用栈难以从根本上定位和解决线上用户的锁问题。最终我们自研了一套线上锁监控系统,它需要满足以下要求:

  • 线上监控方案

  • 丰富的锁信息,包括 Java 调用栈

  • 数据分析平台,包括聚合能力,设备和版本信息等

  • 可纳入开发和合码流程,防止不良代码上线

这样的锁监控系统,能够帮助我们高效定位和解决线上问题,并实现防劣化。

锁监控原理

我们先从 Systrace 入手,有一类常见的耗时叫做 monitor contention,其实是 Android ART 虚拟机输出的锁信息。


简单介绍一下里面的信息

monitor contention with owner work_thread (27176) at android.content.res.Resources android.app.ResourcesManager.getOrCreateResources(android.os.IBinder, android.content.res.ResourcesKey, java.lang.ClassLoader)(ResourcesManager.java:901) waiters=1 blocking from java.util.ArrayList android.app.ActivityThread.collectComponentCallbacks(boolean, android.content.res.Configuration)(ActivityThread.java:5836)
  • 持锁线程:work_thread

  • 持锁线程方法:android.app.ResourcesManager.getOrCreateResources(…)

  • 等待线程 1 个

  • 等锁方法:android.app.ActivityThread.collectComponentCallbacks(…)

Java 锁,无论是同步方法还是同步块,虚拟机最终都会到 MonitorEnter。我们关注的 trace 是 Android 6 引入的, 在锁的开始和结束时分别调用ATRACE_BEGIN(...)ATRACE_END()

线上方案

默认情况下 atrace 是关闭的,开关在 ATRACE_ENABLED() 中。我们通过设置 atrace_enabled_tags 为 ATRACE_TAG_DALVIK 可以开启当前进程的 ART 虚拟机的 atrace。

再看 ATRACE_BEGIN(...)ATRACE_END() 的实现,其实是用 write 将字符串写入一个特殊的 atrace_marker_fd (/sys/kernel/debug/tracing/trace_marker)。

因此通过 hook libcutils.so 的 write 方法,并按 atrace_marker_fd 过滤,就实现了对 ATRACE_BEGIN(...)ATRACE_END() 的拦截。有了 BEGIN 和 END 后可以计算出阻塞时长,解析 monitor contention with owner... 日志可以得到我们关注的 Java 锁信息。

获取堆栈

到目前为止,我们已经可以监控到线上用户的锁问题。但是还不够,为了能够优化锁的性能,我们想需要知道等锁的具体原因,也就是 Java 调用栈。

获取 Java 调用栈,可以使用Thread.getStackTrace()方法。由于我们 hook 住了虚拟机的等锁线程,此时线程处于一种特殊状态,不可以直接通过 JNI 调用 Java 方法,否则导致线上 crash 问题。


解决方案是异步获取堆栈,在 MonitorBegin 的时候通知子线程 5ms 之后抓取堆栈,MonitorEnd 计算阻塞时长,并结合堆栈数据一起放入队列,等待上报 Slardar。如果 MonitorEnd 时不满足 5ms 则取消抓栈和上报


数据平台

由于方案本身有一定性能开销,我们仅对灰度测试中的部分用户开启了锁监控。配置线上采样后,命中的用户将自动开启锁监控,数据上报 Slardar 平台后就可以消费了。


具体 case 可以看到设备信息、阻塞时长、调用堆栈


根据调用栈查找源码,可以定位到是哪一个锁,说明上报数据是准确的。


稳定性方面,10 万灰度用户开启锁监控后,无新增稳定性问题。

优化实践

经过多轮锁收集和治理,我们取得了一些不错的收益,这里简单介绍下锁治理的几个典型案例。

典型案例

inflate 锁:

先解析一下什么是 inflate:Android 中解析 xml 生成 View 树的过程就叫做 inflate 过程。inflate 是一个耗时过程,常规的手段就是通过异步来减少其在主线程的耗时,这样大大的减少了卡顿、页面打开和启动时长;但此方式也会带来新的问题,比如 LayoutInflater 的 inflate 方法中有加锁保护的代码块,并行构建会造成锁等待,可能反而增加主线程耗时,针对这个问题有三种解决方案:

  • 克隆 LayoutInflater

    • 把线程分为三类别:Main、工作线程和其它线程(野线程),Context(Activity 和 App)为每个类别提供专有 LayoutInflater,这样能有效的规避 inflate 锁。

    • 优点:实现简单、兼容性好

    • 缺点:LayoutInflater 中非安全的静态属性在并发情况下有概率产生稳定性问题

  • code 构造替代 xml 构造

    • 这种方式完美的绕开了 inflate 操作,极大提高了 View 构造速度。

    • 优点:复杂度高、性能好

    • 缺点:影响编译速度、View 自定义属性需要做转换、存在兼容性问题(比如厂商改属性)

  • 定制 LayoutInflater

    • 自定义 FastInflater(继承自 LayoutInflater)替换系统的 PhoneLayoutInflater,重写 inflate 操作,去掉锁保护;从统计数据看,在并发时快了约 4%。

    • 优点:复杂度高、性能好

    • 缺点:存在兼容性,比如华为的 Inflater 为 HwPhoneLayoutInflater,无法直接替换。

文件目录锁:

ContextImpl 中获取目录(cache、files、DB 和 preferenceDir)的实现有两个关键耗时点:1. 存在 IPC(IStorageManager.mkdir)和文件 check;2. 加锁“nSync”保护;所以 ipc 变长和并发存在,都可能导致 App 卡顿,如图为 Anr 数据:

相关的常用 Api 有 getExternalCacheDir、getCacheDir、getFilesDir、getTheme 等,考虑到系统的部分目录一般不会发生变化,所以我们可以对一些不会变化的目录进行 cache 处理,减少带 锁方法块的执行,从而有效的绕过锁等待。

MessageQueue:

Android 子线程与主线程通讯的通用方式是向主线程 MessageQueue 中插入一个任务(message),等此任务(message)被主线程 Looper 调度执行;所以 MessageQueue 中会对消息链表的修改加锁保护,主要实现在 enqueueMessage 和 next 两个方法中。

利用 Slardar 采集线上锁信息,根据这些信息,我们可以轻松追踪锁的执有线程和 owner,最后根据情况将请求(message)移到子线程,这样就可以极大的减轻主线程压力和等锁的可能性。此问题的修改方式并不复杂,重点在于如何监控到这些执锁线程。


序列化和反序列化:

抖音中有一些常用数据对象使用 Json 格式存储。为保证这些数据的完整性,在读取和存储时加了锁保护,从而导致锁等待比较常见,这种情况在启动场景特别明显;所以要想减少锁等待,就必段加快序列化和反序列化,针对这个问题,我们做了三个优化方案:

  • Gson 反序列化的耗时集中在 TypeAdapter 的构建,此过程利用反射创建 Filed 和 name(key)的映射表;所以我们在编译时针对数据类创建对应的 TypeAdapter,大大减少反序列化的时耗。

  • 部分类使用 parcel 序列化和反序列化,大大提高了速度,约减少 90%的时耗。

  • 大对像根据情况拆分成多个小对像,这样可以减少锁粒度,也就减少了锁等待。以上方案在抖音项目中都有使用,取得了很不错的收益。

AssetManager 锁:

获取 string、size、color 或 xml 等资源的最终实现基本都封装在 AssertManager 中,为了保证数据的正确性,加了锁(对象 AssetManager)保护,大致的调用关系如图:

常用的调用点有:

  • View 构造方法中调用 context.obtainStyledAttributes(…)获取 TypedArray,最后都会调用 AssetManager 的带锁方法。

  • View 的 toString 也调用了 AssetManager 的带锁方法。

随着 xml 异步 inflate 的增加,这些方法并发调用也增加,造成主线程的锁等待也日渐突出,最终导致卡顿,针对这个问题,目前我们的优化方案主要有:

  • 去掉多余的调用,比如 View 的 toString,这个常见于日志打印。

  • 一个 Context 根据线程名提供不同的 AssetManager,绕过 AssetManager 对象锁;此方法可能带来一些内存消耗。

So 加载锁优化:

Android 提供的加载 so 的接口实现都在封装在 Runtime 中,比如常用的 loadLibrary0 和 load0,如图 1 和 l 图 2 所示,此方法是加了锁的,如果并发加载 so 就会造成锁等待。通过 Slardar 的监控数据,我们验证了这个问题,同时也有一些意外收获,比如平台可能有自己的 so 需要加:

我们根据 so 的不同情况,主要有以下优化思路:

  • 对于 cinit 加载的 so,我们可以提前在子线程中加载一下 cinit 的宿主类。

  • 业务层面的 so, 可以统一在子线程中进行提前加载。

  • 使用 load0 替代 loadLibrary0,可以减少锁中拼接 so 路径的时耗。

  • so 文件加载优化,比如 JNI_OnLoad。

ActivityThread:

在收集的的数据中我们也发现了一些系统层的框架锁,比如下图这个:


这个问题主要集中在启动阶段,ams 会发 trim 通知给 ActivityThread 中的 ApplicationThread,收到通知后会向 Choreographer 的 commit 列表(此任务列表不作展开)中添加一个 trim 任务,也就是在下个 vsync 到达时被执行;

trim 过程主要包括收集 Applicatioin、Activity、Service、Provider 和向它们发送 trim 消息,也是系统提供给业务清理自身内存的一个时机;收集过程是加锁(ResourcesManager)保护的,如图:

考虑到启动阶段并不太关心内存的释放,所以可以尝试在启动阶段,比如 40 秒内,不执行 trim 操作;具体的实现是这样,首先替换 Choreographer 的 FrameHandler, 这样就能接管 vsync 的 doFrame 操作,在启动 40 秒内的每次 vsync 主动 check 或删除 commint 任务列表中的 trim 操作。


收益

在抖音中我们除了优化前面列出的这些典型锁外,还优化了一些业务本身的锁,部分已经通过线上实验验证了收益,也有一些还在尝试实验中;通过对实验中各指标的分析,也证实了锁优化能带来启动和流畅度等技术收益,间接带来了不错的业务收益,这也坚定了我们在这个方向上的继续探索和深化。

小结

前面列出的只是有代表性的一些通用 Java 锁,在实际开发中遇到的远比这多,但不管什么样的锁,都可以根据进程和代码归属分为以下四类:业务锁、依赖库锁、框架锁和系统锁;

不同类型的锁优化思路也会不一样,部分方案可以复用,部分只能 case-by-case 解决,具体的优化方案有:减少调用、绕过调用、使用读写锁和无锁等。


分类描述进程代码优化方案
业务锁源码可见,可以直接修改;比如前面的序列化优化。App 进程包含直接优化;静态 aop
依赖库锁包含编译产物,可以修改产物App 进程包含直接优化;静态 aop
框架锁运行时加载,同时存在兼容性;比如前面提到的 inflate 锁、AssetManager 锁和 MessageQueue 锁App 进程不包含减少调用;动态 aop
系统锁系统为 App 提供的服务和资源,App 间存在竞争,所以服务层需要加锁保护,比如 IPC、文件系统和数据库等服务进程不包含减少调用

总结

经过了长达半年的探索和优化,此方案已在线上使用,作为我们日常防劣化和主动优化的输入工具,我们评判的点主要有以下四个:

  • 稳定性:线上开启后,ANR、Crash 和 OOM 和大盘一致。

  • 准确性:从目前线上的消费数据来看,这个值达到了 99%。

  • 扩展性:业务可以根据场景开启和关闭采集功能,也可以收集指定时间内的锁,比如启动阶段可以收集 32ms 的锁,其它阶段收集 16ms 的锁。

  • 劣化影响:从线上实验数据看,一定量(UV)的情况下,业务和性能(丢帧和启动)无显著劣化。

此方案虽然只能监控 synchronized 锁,像 CAS、Native 锁、sleep 和 wait 都无法监控,但在我们日常开发中synchronized 锁占比非常大, 所以基本满足了我们绝大部分的需求,当然,我们也在持续探索其它锁的监控和验证其价值。

————————————————
来源: 字节跳动技术团队
原文:blog.csdn.net/ByteDanceTech/article/details/125863436

收起阅读 »

倍投模型模拟:1w块搏10w,靠谱吗?

前两天刷影视解说看到一个短片,甲乙两个人打赌,甲每次输了后,都会加倍。一群人围观,甲连输8次,在最后一次赌上全部身家后,一把梭哈赢了,直接走上人生巅峰。 当然,这只是爽剧,让我们用代码模拟下真实情况是怎么的,超刺激哦! 让我们先看下代码(用JS简单写的): /...
继续阅读 »

前两天刷影视解说看到一个短片,甲乙两个人打赌,甲每次输了后,都会加倍。一群人围观,甲连输8次,在最后一次赌上全部身家后,一把梭哈赢了,直接走上人生巅峰。


当然,这只是爽剧,让我们用代码模拟下真实情况是怎么的,超刺激哦!


让我们先看下代码(用JS简单写的):


// 家底
var all = 10000;
// 第一次投注
var first = 1000;
// 假设玩1000次
for(var i=0;i<1000;i++){
// 输赢概率都是50%
var check = Math.random() >= 0.5;
console.log("第"+(i+1)+"次");
if(check){
// 赢了初始化投注
console.log("赚了"+first);
all+= first;
first=1000;
}else{
all -= first;
if(first*2>all){
// 输光了,梭哈
first=all
}else{
// 还有家底,加倍投注。
first=first*2;
}
console.log("输了"+first);
}
if(all<=0){
console.log("输光了,拜拜");
break;
}
console.log("现在有:"+all);
}

第一次模拟:


家底:10000,初始投注:1000。拿出十分之一去搏一搏,合理。


image.png


image.png


顶峰时第46轮:36000,在59轮时被一波带走。


再来一次:


image.png


image.png


这次就比较惨了,第27次顶峰:21000,在32轮时被一波带走。


上面这个有点离谱,我们在保守点,拿出家底的1%去搏一搏,把初始值设为100。


第二次模拟


家底:10000,初始投注:100,稳重求胜。


果然稳健才是硬道理,这一把运气绝对爆棚。


image.png
在第843轮,家底来到了45800,这收益率逆天啊,然而天道有轮回,仅仅到第852轮,我就输光了全部家底。果然人生得意莫嘚瑟。
image.png


再来一次:


image.png
image.png
这次运气一般,在第276轮,才14800。在340轮时被一波带走。


总结


投资有风险,入市需谨慎啊。


在资金有限的情况的下,倍投绝对不是一个好的选项,你们可以试下在当家底是100000时,初始值设为1,有意想不到的惊喜,虽然赚的少,但家底只要够厚,就不会赔。


如果你的资产是有限的,玩下去一定会输,毕竟运气总会用尽,50%概率赢在现实中也几乎不存在。


挺有意思的,欢迎大家试试,调整家底和初始值即可。



逆天了逆天了,我必须分享给大家。


初始我设定的家底1000,初始投注200,在1000轮后,家底来到了惊人的100200,简直运气爆棚,要上天。


image.png


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

Kotlin函数声明与闭包【Kotlin从拒绝到真香】

前言本文介绍闭包。闭包其实不算是新东西了。 其实 Kotlin 就基本没有多少新东西,甚至可以说新型编程语言基本都没有新东西。是把先前编程语言好用的特性组装起来,再加一部分拓展。本文大纲1. 闭包介绍首次接触 闭包 应该...
继续阅读 »

前言

本文介绍闭包。闭包其实不算是新东西了。 其实 Kotlin 就基本没有多少新东西,甚至可以说新型编程语言基本都没有新东西。是把先前编程语言好用的特性组装起来,再加一部分拓展。

本文大纲

Kotlin 函数声明与闭包.png

1. 闭包介绍

首次接触 闭包 应该是在 JavaScript 上,有函数为“一等公民”特性的编程语言都有这个概念。 函数是“一等公民”的意思是,函数跟变量一样,是某种类型的实例,可以被赋值,可以被引用。函数还可以被调用。变量类型是某个声明的类,函数类型就是规定了入参个数,类型和返回值类型(不规定名字。函数名就和变量名一样,任意定义但要符合规则)。

如要声明 Kotlin 一个函数类型,入参是两个整数,出参是一个整数,那应该这样写: val add: (Int, Int) -> Int。箭头左边括号内表示入参,括号不可省略。箭头右边表示返回值。

wiki上闭包的概念是:引用了自由变量的函数,这个被引用的自由变量将和这个函数一同存在。从定义来说,对闭包的理解,是基于普通函数之上的。一般的函数,能处理的只有入参和全局变量,然后返回一个结果。闭包比普通函数功能更强,可以获取当前上下文的局部变量。当然了,捕获局部变量的前提是可以在局部环境里声明一个函数,这只有把函数当作“一等公民”才可以做到。

2. 闭包与匿名类比较

在 Java 中,匿名类其实就是代替闭包而存在的。不过 Java 严格要求所有函数都需要在类里面,所以巧妙的把“声明一个函数”这样的行为变成了“声明一个接口”或“重写一个方法”。匿名类也可以获取当前上下文的 final 局部变量。和闭包不一样的是,匿名类无法修改获取的局部变量final 不可修改

匿名类能引用 final 局部变量,是因为在编译阶段,会把该局部变量作为匿名类的构造参数传入。

Java8 lambda 是进一步接近闭包的特性,lambda 的 JVM 实现是类似函数指针的东西。 但 Java7 中的 lambda 语法糖兼容不算是真正的 lambda,只是简化了匿名类的书写。

3. 闭包使用

来看一个闭包的例子:

fun returnFun(): () -> Int {
var count = 0
return { count++ }
}

fun main() {
val function = returnFun()
val function2 = returnFun()
println(function()) // 0
println(function()) // 1
println(function()) // 2

println(function2()) // 0
println(function2()) // 1
println(function2()) // 2
}

分析上面的代码,returnFun返回了一个函数,这个函数没有入参,返回值是Int。可以用变量接收它,还可以调用它。functionfunction2分别是创建的两个函数实例。

可以看到,每调用一次function()count都会加一,说明count 被function持有了而且可以被修改。而function2functioncount是独立的,不是共享的。

而通过 jadx 反编译可以看到:

public final class ClosureKt {
@NotNull
public static final Function0<Integer> returnFun() {
IntRef intRef = new IntRef();
intRef.element = 0;
return (Function0) new 1<>(intRef);
}

public static final void main() {
Function0 function = returnFun();
Function0 function2 = returnFun();
System.out.println(((Number) function.invoke()).intValue());
System.out.println(((Number) function.invoke()).intValue());
System.out.println(((Number) function2.invoke()).intValue());
System.out.println(((Number) function2.invoke()).intValue());
}
}

被闭包引用的 int 局部变量,会被封装成 IntRef 这个类。 IntRef 里面保存着 int 变量,原函数和闭包都可以通过 intRef 来读写 int 变量。Kotlin 正是通过这种办法使得局部变量可修改。除了 IntRef,还有 LongRefFloatRef 等,如果是非基础类型,就统一用 ObjectRef 即可。Ref 家族源码:github.com/JetBrains/k…

在 Java 中,如果想要匿名类来操作外部变量,一般做法是把这个变量放入一个 final 数组中。这和 Kotlin 的做法本质上是一样的,即通过持有该变量的引用来使得两个类可以修改同一个变量。

4. 总结

根据示例上面分析,可以总结出:

  • 闭包不算是新东西,是把函数作为“一等公民”的编程语言的特性;
  • 匿名类是 Java 世界里的闭包,但有局限性,即只能读 final 变量,不能写任何变量;
  • Kotlin 的闭包可以获取上下文的局部变量,并可以修改它。实现办法是 Kotlin 编译器给引用的局部变量封装了一层引用。


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

收起阅读 »

二次元恋爱社交开源项目---mua【附客户端、服务端源码】

Mua是由环信MVP开发者精心打造的开源项目,提供Demo体验和示例源码,支持开发者结合业务需要灵活自定义产品形态。Mua是一个二次元恋爱互动社交APP,有类似项目需求的创业者或想拥有甜蜜恋爱过程的开发者们都可以来44,下面介绍下这个恋爱升温神器的功能 打开A...
继续阅读 »

Mua是由环信MVP开发者精心打造的开源项目,提供Demo体验和示例源码,支持开发者结合业务需要灵活自定义产品形态。

Mua是一个二次元恋爱互动社交APP,有类似项目需求的创业者或想拥有甜蜜恋爱过程的开发者们都可以来44,下面介绍下这个恋爱升温神器的功能

APP----码,码   ,


Mua 

线

Mua

1MuaIM

 

 

 



2IM
Mua使IM


3
5Appdddd



4

tips.




5

AppApp+100




6





7

& 便&&





8Mua

Mua3

饿



9

cmd

 




MuaAPP,


Mua

⬇️Demo  


Android

mua端、服务端
https://github.com/easemob/mua


收起阅读 »

Kotlin-Flow常用封装类StateFlow的使用

Kotlin中StateFlow的使用 StateFlow 是 Flow 的实现,是一个特殊的流,默认的 Flow 是冷流,而StateFlow 是热流,和 LiveData 比较类似。关于冷热流后面一期 SharedFlow 会详细说明。 使用 StateF...
继续阅读 »

Kotlin中StateFlow的使用


StateFlow 是 Flow 的实现,是一个特殊的流,默认的 Flow 是冷流,而StateFlow 是热流,和 LiveData 比较类似。关于冷热流后面一期 SharedFlow 会详细说明。


使用 StateFlow 替代 LiveData 应该是目前很多开发者的呼吁了,确实 LiveData 的功能 StateFlow 都能实现,可以说是 LiveData 的升级版。


StateFlow的特点



  • 它始终是有值的。

  • 它的值是唯一的。

  • 它允许被多个观察者共用 (因此是共享的数据流)。

  • 它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的。


官方推荐当暴露 UI 的状态给视图时,应该使用 StateFlow。这是一种安全和高效的观察者,专门用于容纳 UI 状态。


一、StateFlow的使用


方式一,我们自己 new 出来


一般我们再ViewModel中定义读写分类的StateFlow


@HiltViewModel
class Demo4ViewModel @Inject constructor(
val savedState: SavedStateHandle
) : BaseViewModel() {

private val _searchFlow = MutableStateFlow("")
val searchFlow: StateFlow<String> = _searchFlow

fun changeSearch(keyword: String) {
_searchFlow.value = keyword
}
}

在Activity中我们就可以像类似 LiveData 一样的使用 StateFlow



private fun testflow() {
mViewModel.changeSearch("key")
}

override fun startObserve() {
lifecycleScope.launchWhenCreated {
mViewModel.searchFlow.collect {
YYLogUtils.w("value $it")
}
}
}

方式二,通过一个 冷流 Flow 转换为 StateFlow


    val stateFlow = flowOf(1, 2, 3).stateIn(
scope = lifecycleScope,
// started = WhileSubscribed(5000, 1000),
// started = Eagerly,
started = Lazily,
initialValue = 1
)

lifecycleScope.launch {
stateFlow.collect {

}
}

几个重要参数的说明如下



  • scope 共享开始时所在的协程作用域范围

  • started 控制共享的开始和结束的策略

  • Lazily: 当首个订阅者出现时开始,在 scope 指定的作用域被结束时终止。

  • Eagerly: 立即开始,而在 scope 指定的作用域被结束时终止。

  • WhileSubscribed能够指定当前不有订阅者后,多少时间取消上游数据和能够指定多少时间后,缓存中的数据被丢失,回复称initialValue的值。

  • initialValue 初始值


二、替代LiveData


不管是普通的 ViewModel 观察订阅模式,在Activity中订阅,还是DataBinding的模式,我们都可以使用StateFlow来代替ViewModel


    val withdrawMethod = MutableStateFlow(0)

<ImageView
android:id="@+id/iv_giro_checked"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="@dimen/d_15dp"
android:src="@drawable/pay_method_checked"
android:visibility="gone"
binding:isVisibleGone="@{viewModel.withdrawMethod == 1}" />

为什么我们需要用StateFlow来代替LiveData,或者说LiveData有什么缺点?


LiveData vs Flow


先上代码,看看它们的用法与差异


ViewModel的代码


@HiltViewModel
class Demo4ViewModel @Inject constructor(
val savedState: SavedStateHandle
) : BaseViewModel() {

private val _searchLD = MutableLiveData<String>()
val searchLD: LiveData<String> = _searchLD

private val _searchFlow = MutableStateFlow("")
val searchFlow: StateFlow<String> = _searchFlow

fun changeSearch(keyword: String) {
_searchFlow.value = keyword
_searchLD.value = keyword
}
}

Activity中触发与接收事件



private fun testflow() {
mViewModel.changeSearch("key")
}

override fun startObserve() {
mViewModel.searchLD.observe(this){
YYLogUtils.w("value $it")
}

lifecycleScope.launchWhenCreated {
mViewModel.searchFlow.collect {
YYLogUtils.w("value $it")
}
}
}

可以看到基本的使用几乎是没有差异,在DataBinding中同样的是都能使用。那么它们有哪些差异呢?


它们相同的地方:



  1. 仅持有单个且最新的数据

  2. 自动取消订阅

  3. 提供「可读可写」和「仅可读」两个版本收缩权限

  4. 配合 DataBinding 实现「双向绑定」


相比StateFlow ,LiveData的确定:



  1. LiveData在某些特定的场景下会丢失数据

  2. LiveData 只能在主线程不能方便地支持异步化

  3. LiveData 的数据变换能力远远不如 Flow

  4. LiveData 粘性问题解决需要额外扩展

  5. LiveData 多数据源的合流能力远远不如 Flow

  6. LiveData 默认不支持防抖,值没有变化也会通知


这么惨,那我们开发是不是要放弃LiveData了?



恰恰不是!


如果大家全部是Koltin代码开发,那么是可以用Flow,这是基于Kotlin代码,基于协程实现的,但是现在很多项目还是 Java 语言开发的。那么LiveData还是很香的。


其二是LiveData的学习成本与 协程、Flow 的学习成本不可同日而语,开发项目是整个团队的事情,不能说你一个人会一个人用,目前LiveData的简单学习成本是很有优势的。


只是我们需要在一些特定的场景慎重使用postValue,比如数据比较秘籍的场景,我们尽量使用setValue方法。


总结


如果大家的项目的语言是 Kotlin ,并且小组成员都会 Flow 。那么我推荐你们使用StateFlow 替代LiveData 。如果不是,那么 LiveData 是你最好的选择。


谷歌也只是推荐使用Flow替代LiveData。但是并没有说打算放弃 LiveData 。并且 LiveData 与 StateFlow 都有各自的使用场景,不需要担心 LiveData的 使用。


本文我们只是简单的对比,关于StateFlow 与 SharedFlow 和LiveData 三者的差异与选择,后面等SharedFlow那一期详细的讲解。


为什么很多东西都要等SharedFlow,是因为 SharedFlow 是 StateFlow 的基础,StateFlow 像是 SharedFlow 的‘青春版’。很多东西需要讲完 SharedFlow 才能把知识点串起来,期待一下。


好了,本期内容如讲的不到位或错漏的地方,希望同学们可以指出交流。


如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。


Ok,这一期就此完结。



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

如何从0到1构建一个稳定、高性能的Redis集群

这篇文章我想和你聊一聊 Redis 的架构演化之路。现如今 Redis 变得越来越流行,几乎在很多项目中都要被用到,不知道你在使用 Redis 时,有没有思考过,Redis 到底是如何稳定、高性能地提供服务的?你也可以尝试回答一下以下这些问题:我使用 Redi...
继续阅读 »

这篇文章我想和你聊一聊 Redis 的架构演化之路。

现如今 Redis 变得越来越流行,几乎在很多项目中都要被用到,不知道你在使用 Redis 时,有没有思考过,Redis 到底是如何稳定、高性能地提供服务的?

你也可以尝试回答一下以下这些问题:

  • 我使用 Redis 的场景很简单,只使用单机版 Redis 会有什么问题吗?
  • 我的 Redis 故障宕机了,数据丢失了怎么办?如何能保证我的业务应用不受影响?
  • 为什么需要主从集群?它有什么优势?
  • 什么是分片集群?我真的需要分片集群吗?

如果你对 Redis 已经有些了解,肯定也听说过数据持久化、主从复制、哨兵这些概念,它们之间又有什么区别和联系呢?

如果你存在这样的疑惑,这篇文章,我会从 0 到 1,再从 1 到 N,带你一步步构建出一个稳定、高性能的 Redis 集群。

在这个过程中,你可以了解到 Redis 为了做到稳定、高性能,都采取了哪些优化方案,以及为什么要这么做?

掌握了这些原理,这样平时你在使用 Redis 时,就能够做到「游刃有余」。

这篇文章干货很多,希望你可以耐心读完。

从最简单的开始:单机版 Redis

首先,我们从最简单的场景开始。

假设现在你有一个业务应用,需要引入 Redis 来提高应用的性能,此时你可以选择部署一个单机版的 Redis 来使用,就像这样:

这个架构非常简单,你的业务应用可以把 Redis 当做缓存来使用,从 MySQL 中查询数据,然后写入到 Redis 中,之后业务应用再从 Redis 中读取这些数据,由于 Redis 的数据都存储在内存中,所以这个速度飞快。

如果你的业务体量并不大,那这样的架构模型基本可以满足你的需求。是不是很简单?

随着时间的推移,你的业务体量逐渐发展起来了,Redis 中存储的数据也越来越多,此时你的业务应用对 Redis 的依赖也越来越重。

但是,突然有一天,你的 Redis 因为某些原因宕机了,这时你的所有业务流量,都会打到后端 MySQL 上,这会导致你的 MySQL 压力剧增,严重的话甚至会压垮 MySQL。

这时你应该怎么办?

我猜你的方案肯定是,赶紧重启 Redis,让它可以继续提供服务。

但是,因为之前 Redis 中的数据都在内存中,尽管你现在把 Redis 重启了,之前的数据也都丢失了。重启后的 Redis 虽然可以正常工作,但是由于 Redis 中没有任何数据,业务流量还是都会打到后端 MySQL 上,MySQL 的压力还是很大。

这可怎么办?你陷入了沉思。

有没有什么好的办法解决这个问题?

既然 Redis 只把数据存储在内存中,那是否可以把这些数据也写一份到磁盘上呢?

如果采用这种方式,当 Redis 重启时,我们把磁盘中的数据快速恢复到内存中,这样它就可以继续正常提供服务了。

是的,这是一个很好的解决方案,这个把内存数据写到磁盘上的过程,就是「数据持久化」。

数据持久化:有备无患

现在,你设想的 Redis 数据持久化是这样的:

但是,数据持久化具体应该怎么做呢?

我猜你最容易想到的一个方案是,Redis 每一次执行写操作,除了写内存之外,同时也写一份到磁盘上,就像这样:

没错,这是最简单直接的方案。

但仔细想一下,这个方案有个问题:客户端的每次写操作,既需要写内存,又需要写磁盘,而写磁盘的耗时相比于写内存来说,肯定要慢很多!这势必会影响到 Redis 的性能。

如何规避这个问题?

我们可以这样优化:Redis 写内存由主线程来做,写内存完成后就给客户端返回结果,然后 Redis 用另一个线程去写磁盘,这样就可以避免主线程写磁盘对性能的影响。

这确实是一个好方案。除此之外,我们可以换个角度,思考一下还有什么方式可以持久化数据?

这时你就要结合 Redis 的使用场景来考虑了。

回忆一下,我们在使用 Redis 时,通常把它用作什么场景?

是的,缓存。

把 Redis 当做缓存来用,意味着尽管 Redis 中没有保存全量数据,对于不在缓存中的数据,我们的业务应用依旧可以通过查询后端数据库得到结果,只不过查询后端数据的速度会慢一点而已,但对业务结果其实是没有影响的。

基于这个特点,我们的 Redis 数据持久化还可以用「数据快照」的方式来做。

那什么是数据快照呢?

简单来讲,你可以这么理解:

  1. 你把 Redis 想象成一个水杯,向 Redis 写入数据,就相当于往这个杯子里倒水
  2. 此时你拿一个相机给这个水杯拍一张照片,拍照的这一瞬间,照片中记录到这个水杯中水的容量,就是水杯的数据快照

也就是说,Redis 的数据快照,是记录某一时刻下 Redis 中的数据,然后只需要把这个数据快照写到磁盘上就可以了。

它的优势在于,只在需要持久化时,把数据「一次性」写入磁盘,其它时间都不需要操作磁盘。

基于这个方案,我们可以定时给 Redis 做数据快照,把数据持久化到磁盘上。

其实,上面说的这些持久化方案,就是 Redis 的「RDB」和「AOF」:

  • RDB:只持久化某一时刻的数据快照到磁盘上(创建一个子进程来做)
  • AOF:每一次写操作都持久到磁盘(主线程写内存,根据策略可以配置由主线程还是子线程进行数据持久化)

它们的区别除了上面讲到的,还有以下特点:

  1. RDB 采用二进制 + 数据压缩的方式写磁盘,这样文件体积小,数据恢复速度也快
  2. AOF 记录的是每一次写命令,数据最全,但文件体积大,数据恢复速度慢

如果让你来选择持久化方案,你可以这样选择:

  1. 如果你的业务对于数据丢失不敏感,采用 RDB 方案持久化数据
  2. 如果你的业务对数据完整性要求比较高,采用 AOF 方案持久化数据

假设你的业务对 Redis 数据完整性要求比较高,选择了 AOF 方案,那此时你又会遇到这些问题:

  1. AOF 记录每一次写操作,随着时间增长,AOF 文件体积会越来越大
  2. 这么大的 AOF 文件,在数据恢复时变得非常慢

这怎么办?数据完整性要求变高了,恢复数据也变困难了?有没有什么方法,可以缩小文件体积?提升恢复速度呢?

我们继续来分析 AOF 的特点。

由于 AOF 文件中记录的都是每一次写操作,但对于同一个 key 可能会发生多次修改,我们只保留最后一次被修改的值,是不是也可以?

是的,这就是我们经常听到的「AOF rewrite」,你也可以把它理解为 AOF 「瘦身」。

我们可以对 AOF 文件定时 rewrite,避免这个文件体积持续膨胀,这样在恢复时就可以缩短恢复时间了。

再进一步思考一下,还有没有办法继续缩小 AOF 文件?

回顾一下我们前面讲到的,RDB 和 AOF 各自的特点:

  1. RDB 以二进制 + 数据压缩方式存储,文件体积小
  2. AOF 记录每一次写命令,数据最全

我们可否利用它们各自的优势呢?

当然可以,这就是 Redis 的「混合持久化」。

具体来说,当 AOF rewrite 时,Redis 先以 RDB 格式在 AOF 文件中写入一个数据快照,再把在这期间产生的每一个写命令,追加到 AOF 文件中。因为 RDB 是二进制压缩写入的,这样 AOF 文件体积就变得更小了。

此时,你在使用 AOF 文件恢复数据时,这个恢复时间就会更短了!

Redis 4.0 以上版本才支持混合持久化。

这么一番优化,你的 Redis 再也不用担心实例宕机了,当发生宕机时,你就可以用持久化文件快速恢复 Redis 中的数据。

但这样就没问题了吗?

仔细想一下,虽然我们已经把持久化的文件优化到最小了,但在恢复数据时依旧是需要时间的,在这期间你的业务应用还是会受到影响,这怎么办?

我们来分析有没有更好的方案。

一个实例宕机,只能用恢复数据来解决,那我们是否可以部署多个 Redis 实例,然后让这些实例数据保持实时同步,这样当一个实例宕机时,我们在剩下的实例中选择一个继续提供服务就好了。

没错,这个方案就是接下来要讲的「主从复制:多副本」。

主从复制:多副本

此时,你可以部署多个 Redis 实例,架构模型就变成了这样:

我们这里把实时读写的节点叫做 master,另一个实时同步数据的节点叫做 slave。

采用多副本的方案,它的优势是:

  1. 缩短不可用时间:master 发生宕机,我们可以手动把 slave 提升为 master 继续提供服务
  2. 提升读性能:让 slave 分担一部分读请求,提升应用的整体性能

这个方案不错,不仅节省了数据恢复的时间,还能提升性能,那它有什么问题吗?

你可以思考一下。

其实,它的问题在于:当 master 宕机时,我们需要「手动」把 slave 提升为 master,这个过程也是需要花费时间的。

虽然比恢复数据要快得多,但还是需要人工介入处理。一旦需要人工介入,就必须要算上人的反应时间、操作时间,所以,在这期间你的业务应用依旧会受到影响。

怎么解决这个问题?我们是否可以把这个切换的过程,变成自动化呢?

对于这种情况,我们需要一个「故障自动切换」机制,这就是我们经常听到的「哨兵」所具备的能力。

哨兵:故障自动切换

现在,我们可以引入一个「观察者」,让这个观察者去实时监测 master 的健康状态,这个观察者就是「哨兵」。

具体如何做?

  1. 哨兵每间隔一段时间,询问 master 是否正常
  2. master 正常回复,表示状态正常,回复超时表示异常
  3. 哨兵发现异常,发起主从切换

有了这个方案,就不需要人去介入处理了,一切就变得自动化了,是不是很爽?

但这里还有一个问题,如果 master 状态正常,但这个哨兵在询问 master 时,它们之间的网络发生了问题,那这个哨兵可能会误判。

这个问题怎么解决?

答案是,我们可以部署多个哨兵,让它们分布在不同的机器上,它们一起监测 master 的状态,流程就变成了这样:

  1. 多个哨兵每间隔一段时间,询问 master 是否正常
  2. master 正常回复,表示状态正常,回复超时表示异常
  3. 一旦有一个哨兵判定 master 异常(不管是否是网络问题),就询问其它哨兵,如果多个哨兵(设置一个阈值)都认为 master 异常了,这才判定 master 确实发生了故障
  4. 多个哨兵经过协商后,判定 master 故障,则发起主从切换

所以,我们用多个哨兵互相协商来判定 master 的状态,这样一来,就可以大大降低误判的概率。

哨兵协商判定 master 异常后,这里还有一个问题:由哪个哨兵来发起主从切换呢?

答案是,选出一个哨兵「领导者」,由这个领导者进行主从切换。

问题又来了,这个领导者怎么选?

想象一下,在现实生活中,选举是怎么做的?

是的,投票。

在选举哨兵领导者时,我们可以制定这样一个选举规则:

  1. 每个哨兵都询问其它哨兵,请求对方为自己投票
  2. 每个哨兵只投票给第一个请求投票的哨兵,且只能投票一次
  3. 首先拿到超过半数投票的哨兵,当选为领导者,发起主从切换

其实,这个选举的过程就是我们经常听到的:分布式系统领域中的「共识算法」。

什么是共识算法?

我们在多个机器部署哨兵,它们需要共同协作完成一项任务,所以它们就组成了一个「分布式系统」。

在分布式系统领域,多个节点如何就一个问题达成共识的算法,就叫共识算法。

在这个场景下,多个哨兵共同协商,选举出一个都认可的领导者,就是使用共识算法完成的。

这个算法还规定节点的数量必须是奇数个,这样可以保证系统中即使有节点发生了故障,剩余超过「半数」的节点状态正常,依旧可以提供正确的结果,也就是说,这个算法还兼容了存在故障节点的情况。

共识算法在分布式系统领域有很多,例如 Paxos、Raft,哨兵选举领导者这个场景,使用的是 Raft 共识算法,因为它足够简单,且易于实现。

现在,我们用多个哨兵共同监测 Redis 的状态,这样一来,就可以避免误判的问题了,架构模型就变成了这样:

好了,到这里我们先小结一下。

你的 Redis 从最简单的单机版,经过数据持久化、主从多副本、哨兵集群,这一路优化下来,你的 Redis 不管是性能还是稳定性,都越来越高,就算节点发生故障,也不用担心了。

你的 Redis 以这样的架构模式部署,基本上就可以稳定运行很长时间了。

随着时间的发展,你的业务体量开始迎来了爆炸性增长,此时你的架构模型,还能够承担这么大的流量吗?

我们一起来分析一下:

  1. 稳定性:Redis 故障宕机,我们有哨兵 + 副本,可以自动完成主从切换
  2. 性能:读请求量增长,我们可以再部署多个 slave,读写分离,分担读压力
  3. 性能:写请求量增长,但我们只有一个 master 实例,这个实例达到瓶颈怎么办?

看到了么,当你的写请求量越来越大时,一个 master 实例可能就无法承担这么大的写流量了。

要想完美解决这个问题,此时你就需要考虑使用「分片集群」了。

分片集群:横向扩展

什么是「分片集群」?

简单来讲,一个实例扛不住写压力,那我们是否可以部署多个实例,然后把这些实例按照一定规则组织起来,把它们当成一个整体,对外提供服务,这样不就可以解决集中写一个实例的瓶颈问题吗?

所以,现在的架构模型就变成了这样:

现在问题又来了,这么多实例如何组织呢?

我们制定规则如下:

  1. 每个节点各自存储一部分数据,所有节点数据之和才是全量数据
  2. 制定一个路由规则,对于不同的 key,把它路由到固定一个实例上进行读写

而分片集群根据路由规则所在位置的不同,还可以分为两大类:

  1. 客户端分片
  2. 服务端分片

客户端分片指的是,key 的路由规则放在客户端来做,就是下面这样:

这个方案的缺点是,客户端需要维护这个路由规则,也就是说,你需要把路由规则写到你的业务代码中。

如何做到不把路由规则耦合在业务代码中呢?

你可以这样优化,把这个路由规则封装成一个模块,当需要使用时,集成这个模块就可以了。

这就是 Redis Cluster 的采用的方案。

Redis Cluster 内置了哨兵逻辑,无需再部署哨兵。

当你使用 Redis Cluster 时,你的业务应用需要使用配套的 Redis SDK,这个 SDK 内就集成好了路由规则,不需要你自己编写了。

再来看服务端分片。

这种方案指的是,路由规则不放在客户端来做,而是在客户端和服务端之间增加一个「中间代理层」,这个代理就是我们经常听到的 Proxy。

而数据的路由规则,就放在这个 Proxy 层来维护。

这样一来,你就无需关心服务端有多少个 Redis 节点了,只需要和这个 Proxy 交互即可。

Proxy 会把你的请求根据路由规则,转发到对应的 Redis 节点上,而且,当集群实例不足以支撑更大的流量请求时,还可以横向扩容,添加新的 Redis 实例提升性能,这一切对于你的客户端来说,都是透明无感知的。

业界开源的 Redis 分片集群方案,例如 Twemproxy、Codis 就是采用的这种方案。

分片集群在数据扩容时,还涉及到了很多细节,这块内容不是本文章重点,所以暂不详述。

至此,当你使用分片集群后,对于未来更大的流量压力,都可以从容面对了!

总结

好了,我们来总结一下,我们是如何一步步构建一个稳定、高性能的 Redis 集群的。

首先,在使用最简单的单机版 Redis 时,我们发现当 Redis 故障宕机后,数据无法恢复的问题,因此我们想到了「数据持久化」,把内存中的数据也持久化到磁盘上一份,这样 Redis 重启后就可以从磁盘上快速恢复数据。

在进行数据持久化时,我们又面临如何更高效地将数据持久化到磁盘的问题。之后我们发现 Redis 提供了 RDB 和 AOF 两种方案,分别对应了数据快照和实时的命令记录。当我们对数据完整性要求不高时,可以选择 RDB 持久化方案。如果对于数据完整性要求较高,那么可以选择 AOF 持久化方案。

但是我们又发现,AOF 文件体积会随着时间增长变得越来越大,此时我们想到的优化方案是,使用 AOF rewrite 的方式对其进行瘦身,减小文件体积,再后来,我们发现可以结合 RDB 和 AOF 各自的优势,在 AOF rewrite 时使用两者结合的「混合持久化」方式,又进一步减小了 AOF 文件体积。

之后,我们发现尽管可以通过数据恢复的方式还原数据,但恢复数据也是需要花费时间的,这意味着业务应用还是会受到影响。我们进一步优化,采用「多副本」的方案,让多个实例保持实时同步,当一个实例故障时,可以手动把其它实例提升上来继续提供服务。

但是这样也有问题,手动提升实例上来,需要人工介入,人工介入操作也需要时间,我们开始想办法把这个流程变得自动化,所以我们又引入了「哨兵」集群,哨兵集群通过互相协商的方式,发现故障节点,并可以自动完成切换,这样就大幅降低了对业务应用的影响。

最后,我们把关注点聚焦在如何支撑更大的写流量上,所以,我们又引入了「分片集群」来解决这个问题,让多个 Redis 实例分摊写压力,未来面对更大的流量,我们还可以添加新的实例,横向扩展,进一步提升集群的性能。

至此,我们的 Redis 集群才得以长期稳定、高性能的为我们的业务提供服务。

这里我画了一个思维导图,方便你更好地去理解它们之间的关系,以及演化的过程。

后记

看到这里,我想你对如何构建一个稳定、高性能的 Redis 集群问题时,应该会有自己的见解了。

其实,这篇文章所讲的优化思路,围绕的主题就是「架构设计」的核心思想:

  • 高性能:读写分离、分片集群
  • 高可用:数据持久化、多副本、故障自动切换
  • 易扩展:分片集群、横向扩展

当我们讲到哨兵集群、分片集群时,这还涉及到了「分布式系统」相关的知识:

  • 分布式共识:哨兵领导者选举
  • 负载均衡:分片集群数据分片、数据路由

当然,除了 Redis 之外,对于构建任何一个数据集群,你都可以沿用这个思路去思考、去优化,看看它们到底是如何做的。

例如当你在使用 MySQL 时,你可以思考一下 MySQL 与 Redis 有哪些不同?MySQL 为了做到高性能、高可用,又是如何做的?其实思路都是类似的。

我们现在到处可见分布式系统、数据集群,我希望通过这篇文章,你可以理解这些软件是如何一步步演化过来的,在演化过程中,它们遇到了哪些问题,为了解决这些问题,这些软件的设计者设计了怎样的方案,做了哪些取舍?

你只有了解了其中的原理,掌握了分析问题、解决问题的能力,这样在以后的开发过程中,或是学习其它优秀软件时,就能快速地找到「重点」,在最短的时间掌握它,并能在实际应用中发挥它们的优势。

其实这个思考过程,也是做「架构设计」的思路。在做软件架构设计时,你面临的场景就是发现问题、分析问题、解决问题,一步步去演化、升级你的架构,最后在性能、可靠性方面达到一个平衡。虽然各种软件层出不穷,但架构设计的思想不会变,我希望你真正吸收的是这些思想,这样才可以做到以不变应万变。

来源:mp.weixin.qq.com/s/q79ji-cgfUMo7H0p254QRg

收起阅读 »

浅谈程序的数字签名

理论基础数字签名它是基于非对称密钥加密技术与数字摘要算法技术的应用,它是一个包含电子文件信息以及发送者身份,并能够鉴别发送者身份以及发送信息是否被篡改的一段数字串。一段数字签名数字串,它包含电子文件经过Hash编码后产生的数字摘要,即一个Hash函数值以及发送...
继续阅读 »

理论基础

数字签名它是基于非对称密钥加密技术与数字摘要算法技术的应用,它是一个包含电子文件信息以及发送者身份,并能够鉴别发送者身份以及发送信息是否被篡改的一段数字串。

一段数字签名数字串,它包含电子文件经过Hash编码后产生的数字摘要,即一个Hash函数值以及发送者的公钥和私钥三部分内容。发送方通过私钥加密后发送给接收方,接收方使用公钥解密,通过对比解密后的Hash函数值确定数据电文是否被篡改。

数字签名(又称公钥数字签名)是只有信息的发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对信息的发送者发送信息真实性的一个有效证明。

它是一种类似写在纸上的普通的物理签名,但是在使用了公钥加密领域的技术来实现的,用于鉴别数字信息的方法。


数字签名方案是一种以电子形式存储消息签名的方法。一个完整的数字签名方案应该由两部分组成:签名算法和验证算法。


android数字签名

在android的APP应用程序安装过程中,系统首先会检验APP的签名信息,如果发现签名文件不存在或者校验签名失败,系统则会拒绝安装,所以APP应用程序在发布到市场之前一定要进行签名。

OTA升级中也必须使用到数字签名进行校验,在应用版本迭代必须使用相同的证书签名,不然会生成一个新的应用,导致更新失败。在更新过程中使用相同的证书签名的应用可以共享代码和功能

App安装过程中签名检验的流程:

1、检查 APP中包含的所有文件,对应的摘要值与 MANIFEST.MF 文件中记录的值一致。

2、使用证书文件(RSA 文件)检验签名文件(SF文件)是否被修改过。

3、使用签名文件(SF 文件)检验 MF 文件没有被修改过。


CERT.RSA包含数字签名以及开发者的数字证书。CERT.RSA里的数字签名是指对CERT.SF的摘要采用私钥加密后的数据;

MANIFEST.MF文件中是APP中每个文件名称和摘要SHA256;

CERT.SF则是对MANIFEST.MF的摘要

android中的数字签名有2个主要作用:

1、能定位消息确实是由发送方签名并发出来的,其他假冒不了发送方的签名。

2、确定消息的完整性,签名它代表文件的特征,文件发生变化,数字签名的数值也会发送变化。

Anroid中的签名证书不需要权威机构认证,一般是开发者的自签名证书。所以签名信息中会包含有开发者信息,在一定程度上可以防止应用被破解二次打包成山寨的APP应用,所以签名信息也是用于对APP包防二次打包的一个校验功能点。


(上图是android studio中自创建签名的界面)

在 Android Studio中通过上图创建签名信息后,最终会生成一个 .jks 的文件,它是用作证书和私钥的二进制文件。


(上图是反编译工具直接查看app的签名信息),也可以通过jarsigner,jadx,jeb等工具查看app的签名信息。

从上图中可以看到这个APP采用了V1和V2签名信息,Android中的签名目前主要由V1、V2、V3、V4组成的。

v1签名方案:基于 JAR 签名,签名完之后是META-INF 目录下的三个文件:MANIFEST.MF、CERT.SF、CERT.RSA。通过这三个文件校验来确保APP中的每个文件都不被改动。

APK v1的缺点就是META-INF目录下的文件并不在校验范围内,所以之前多渠道打包等都是通过在这个目录下添加文件来实现的。

V2签名方案:它是在Android 7.0系统中引入,为了使 APP可以在 Android 6.0 (Marshmallow) 及更低版本的设备上安装,应先使用 JAR 签名功能对 APP 进行签名,然后再使用 v2 方案对其进行签名。它是一个全文件的签名方案,它能够发现对 APP的受保护部分进行的所有更改,从而有助于加快验证速度并增强完整性保证。

V2签名,它会在 APP文件中插入一个APP签名分块,该分块位于“ZIP 中央目录”部分之前并紧邻该部分。在“APP签名分块”内,v2 签名和签名者身份信息会存储在 APK 签名方案 v2 分块中。


V3签名方案:它是Android 9.0系统中引入,基于 v2签名的升级,Android 9 支持 APK密钥轮替,这使应用能够在 APK 更新过程中更改其签名密钥。为了实现轮替,APK 必须指示新旧签名密钥之间的信任级别。v3 在 APK 签名分块中添加了有关受支持的 SDK 版本和 proof-of-rotation 结构的信息。

下面链接官方对V3签名相关的说明

https://source.android.google.cn/security/apksigning/v3

APK 密钥轮替功能可以参考:

https://developer.android.google.cn/about/versions/pie/android-9.0

V4签名方案:它是在Android 11.0 引入,用来支持 ADB 增量 APK 安装。通过 APK 签名方案 v4 支持与流式传输兼容的签名方案。v4 签名基于根据 APK 的所有字节计算得出的 Merkle 哈希树。

Android 11 将签名存储在单独的 .apk.idsig 文件中。

下面2个链接是官方对V4签名的相关说明

https://source.android.google.cn/security/apksigning/v4

https://developer.android.google.cn/about/versions/11/features

从上面的签名信息截图中,也可以看到android的签名采用的是X.509V3国际标准。

这个标准下约定了签名证书必须包含以下的内容。

1、证书的序列号

2、证书所使用的签名算法

3、证书的发行机构名称,命名规则一般采用X.500格式

4、证书的有效期

5、证书的所有人的名称

6、证书所有人的公开密钥

7、证书发行者对证书的签名

从上图APP的签名信息中数字签名要包含摘要加密算法:MD5、SHA-1、SHA-256

MD5是一种不可逆的加密算法。

SHA1:它是由NISTNSA设计为同DSA一起使用的,它对长度小于264的输入,产生长度为160bit的散列值,因此抗穷举(brute-force)性更好。

SHA-256 是 SHA-1 的升级版,现在 Android 签名使用的默认算法都已经升级到 SHA-256 了。

摘要算法中又涉及到对称加密和非对加密

对称加密就是在加密和解密过程中需要使用同一个密钥

非对称加密使用公钥/私钥中的公钥来加密明文,然后使用对应的私钥来解密密文。

APP中如果没采用加固保护,容易出现二次打包重新签名的山寨APP。

APP中二次打包流程:破解者需要对APK文件做反编译分析,反编译为smali代码,并对某些关键函数或者资源进行修改,再回编译为apk文件并重签名。

常见的对抗二次打包的方案:

1、签名校验

原理:二次打包会篡改签名,通过签名前后的变化可以检测是否被二次打包;但是这种很容易被hook掉。

2、文件校验

原理:二次打包前后apk关键文件hash值比较,判断是否被修改;但是这种很容易被hook掉。

3、核心函数转为jni层实现

原理:java层代码转为jni层实现,jni层代码相对而言篡改难度更大;写大量反射代码降低了开发效率。

window数字签名

Window的数字签名是微软的一种安全保障机制。

Window数字签名中的签名证书用于验证开发者身份真实性、保护代码的完整性。用户下载软件时,能通过数字签名验证软件来源可信,确认软件、代码没有被非法篡改或植入病毒。所以,软件开发者会在软件发行前使用代码签名证书为软件代码添加数字签名。

对于一个Windows的可执行应用程序,签发数字签名的时候需要计算的数据摘要并不会是程序文件的全部数据,而是要排除一些特定区域的数据。而这些区域当然和PE文件结构有关,具体地,不管是签发时还是校验时计算的hash都会排除一个checksum字段、一个Security数据目录字段以及数字签名证书部分的数据。


Window签名的RSA算法:通过公钥与私钥来判断私钥的合法。

公钥与私钥具有对称性,既可以通过私钥加密,公钥解密,以此来论证私钥持有者的合法身份。也可以通过公钥加密,私钥解密,来对私钥持有者发信息而不被泄露。

由于在交换公钥时免不了遭遇中间人劫持,因此window程序的签名证书,都需要第三方权威机构的认证,并不像android程序一样开发者可以对自己程序签发证书。


(查看某程序的数字签名信息)

从上面截图中看到了摘要算法用到sha1和sha256。

由于SHA-256更强的安全性,现在SHA-256已经作为代码签名证书的行业标准签名算法。

从上图中看到程序拥有2个签名信息,也就是双签名机制。

双签名就是对一个软件做两次签名,先进行SHA1签名,之后再进行SHA2签名的做法就叫做双签名。双签名需要一张支持SHA1和SHA2算法的代码签名证书,利用具备双签名功能的工具导入申请的代码签名证书对软件或应用程序进行双签名,签发后的软件或应用程序就支持SHA1和SHA2签名算法。

Windows10要求使用SHA2算法签名,而Windows7(未更新补丁的)因其兼容性只能使用SHA1算法签名,那么使用一张支持双签SHA1和SHA2算法的代码签名证书就可以实现。

软件签名校验的流程图


Windows系统验证签名流程

1、系统UAC功能开启(用户账户控制功能,默认开启);

2、程序启动时,进行CA校验程序签名信息;

2.1、使用同样算法对软件产生Hash表

2.2、使用公钥产生一个Hash表认证摘要

2.3、比较程序的Hash表认证摘要 与 自己生成的Hash表认证摘要是否一致。

3、程序在window系统执行功能。

数字签名的验证过程本质:

1、通过对要验证的软件创建hash数据;

2、使用发布者的公共密匙来解密被加密的hash数据;

3、最后比较解密的hash和新获得的hash,如果匹配说明签名是正确的,软件没有被修改过。

代码实现校验程序是否有签名,它本质上就是被加密的hash和发布者的数字证书被插入到要签名的软件,最后在进行校验签名信息。


(实现判断程序是否有签名功能)

代码实现可以通过映射文件方式,然后去安装PE文件结构去读取,读取到可选头中的数据目录表,通过判断数据目录表中

IMAGE_DIRECTORY_ENTRY_SECURITY的虚拟地址和大小不为空,那么就表示改应用程序有签名,因为数据签名都是存在在这个字段中。



同样如果要将某个应用程序的签名信息给抹除了,也是一样的思路,将数据目录表中的IMAGE_DIRECTORY_ENTRY_SECURITY的大小和地址都设置为0即可。


下图通过PE工具,可以查看这个字段Security的虚拟地址和大小不为空那么表示应用程序经过签名的。


小结

数字签名不管是在android端还是window端,它都是一种应用程序的身份标志,在安全领域中对应用程序的数字签名校验是一个很常见的鉴别真伪的一个手段。

现在很多杀毒的厂商也都是通过这个数字签名维度,作为一个该应用程序是否可信程序的校验,虽然一些安全杀毒厂商签完名后还是误报毒,那这只能找厂商开白名单了。

来源:mp.weixin.qq.com/s/gC1sqVlLdPQcJg6OkgwzZg

收起阅读 »

从val跟var了解虚拟机世界

val 跟 var val本意就是一个不可变的变量,即赋初始值后不可改变,想较于val,var其实就简单的多,就是可变变量。为什么说val是不可变的变量呢?这不就是矛盾了嘛,其实不矛盾,我们在字节码的角度出发,比如有 val a = Test() var b...
继续阅读 »

val 跟 var


val本意就是一个不可变的变量,即赋初始值后不可改变,想较于val,var其实就简单的多,就是可变变量。为什么说val是不可变的变量呢?这不就是矛盾了嘛,其实不矛盾,我们在字节码的角度出发,比如有


val a  = Test()
var b = Test()

变成的字节码是


  private final Lcom/example/newtestproject/Test; a

private Lcom/example/newtestproject/Test; b

其实val 本质就是用final修饰的变量罢了,而var,就是一个很普通的变量。两者默认都赋予private作用域,这个其实是kotlin世界赋予的额外操作,并不影响我们的理解。从这里出发,我们再继续深入进去!


一个有趣的实验


companion object{
val c = Test()
const val d = "1"
const val e = "1"
val r = "1"
val v = d
}

如果我们把val变量放在companion object里面,这个时候就会被赋予静态的特性,我们看下上面这段代码生成后的字节码



private final static Lcom/example/newtestproject/Test; c


public final static Ljava/lang/String; d = "1"


public final static Ljava/lang/String; e = "1"


private final static Ljava/lang/String; r


private final static Ljava/lang/String; v

我们可以看到,无论是普通对象还是基本数据类型,都被赋予了static的前缀,但是又有稍微不同??我们再来仔细观察一下。


对于String类型,可以用const关键字进行修饰,表示当前的String可用于字符串常量进行替换,这个就是完全的替换,直接进行了初始化!而没有const修饰的字符串r,可以看到,只是生成了一个r变量,并没有直接初始化。而r被初始化的阶段,是在clinit阶段


static void <clinit>() {
ldc "1"
putstatic 'com/example/newtestproject/ValClass.r','Ljava/lang/String;'
...

假如说我们用java代码去写的话,比如


public class JavaStaticClass {
static final String s = "123";
...
}

所生成的字节码是


  final static Ljava/lang/String; s = "123"

跟我们kotlin用const修饰的string变量一致,都是直接初始化的!(留到后面解释)我们继续深入一点,为什么有的变量直接就初始化了,有的却在clinit阶段被初始化?那就要从我们的类加载过程说起了!


类加载过程


虽然类加载有很多细分版本,但是这里笔者引用以下细分版本


image.png
由于类加载过程不是本篇的重点,这里我们稍微解释一下各阶段的主要任务即可



  1. 加载:载入类的过程 :主要是把类的二进制文件,转化为运行时内存的数据,包括静态的存储结构转为方法区等操作,在内存中生成一个代表这个类的java.lang.Class对象

  2. 验证:验证class文件等是否合法:确保Class文件的字节流中包含的信息符合《Java虚 拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

  3. 准备:准备初始数据 :准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段

  4. 解析:解析常量池,函数符号等 :解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,这个阶段就把我们普通的符号转化为对内存运行数据地址。

  5. 初始化:真正的初始化,调用clinit:在初始化阶段,则会根据代码去初始化类变量和其他资源,这个时候,就走到了我们clinit阶段了,上面的阶段都是由虚拟机操控,这个阶段过去后就正在把控制权给我们程序了


准备阶段对static数据的影响


我们主要看到准备阶段:准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,即在这个阶段过后,所有的static数据被赋予“零值”,以下是零值表


image.png
但是也有例外,就是如果类的属性表中存在ConstantValue这个特殊的属性值时,就会在准备阶段把真正的常量直接替换给当前的static变量,比如上述代码中的


省略companion object
const val d = "1"
public final static Ljava/lang/String; d = "1"

此时,只要对d的操作,就会被转化为以下字节码,比如


val v = d

字节码是
ldc "1"
putstatic 'com/example/newtestproject/ValClass.v','Ljava/lang/String;'

变成了ldc指令,即押入了一个字符串“1”进了操作数栈上,而原本的d变量盒子,已经彻底被虚拟机抛弃了。对于属性表中没有ConstantValue的变量,就会在初始化阶段,即调用clinti时,就会把数值赋给相关的变量,以替换“零值”(ps:这里就是各大字节码精简方案的核心,即删除把零值赋予零值的相关操作,比如static int xx = 0这种,就可以在Clint阶段把相关的赋值字节码删除掉也不影响其原本数值,参考框架bytex)。


当然,我们看到上面的对象c,也是在clinit阶段被赋值的,这其实就是ConstantValue生成机制的限制,ConstantValue只会对String跟基本数据类型进行生成,因为我们要替换的常量在常量池里面!对象肯定是不存在的对不对!


回归主题


看到这里,我们再回来看上面的问题,我们就知道了,kotlin中companion object里面的字符串变量,如果不用const修饰的话,其实对应的字符串String类型是不会以ConstantValue生成的,而是以静态对象相同的方式,在clinit进行!


说了半天!那么这个又有什么用呢!?其实这里主要是为了说明虚拟机背后生成的原理,同时也是为了提醒!如果以后有做指令优化的需求的时候,就要非常小心kotlin companion object里面的非const 修饰的String变量,我们就不能在Clinit的时候把这个赋值指令给清除掉!或者说不能跳过Clinit阶段就去用这个数值,因为它还是处于未初始化的状态!


最后


我们从val跟var的角度出发,分析了其背后隐含的故事,当然,看完之后你肯定就彻底懂得了这部分知识啦!无论是以后字节码插桩还是面试,相信可以很从容面对啦!


笔者说:如果你看过这篇文章 黑科技!让Native Crash 与ANR无处发泄!,就会了解到Signal的今生前世,同时我们也发布了beta版本到maven啦!快来用起来!


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

Kotlin协程:协程上下文与上下文元素

一.EmptyCoroutineContext    EmptyCoroutineContext代表空上下文,由于自身为空,因此get方法的返回值是空的,fold方法直接返回传入的初始值,plus方法也是直接返回传入的c...
继续阅读 »

一.EmptyCoroutineContext

    EmptyCoroutineContext代表空上下文,由于自身为空,因此get方法的返回值是空的,fold方法直接返回传入的初始值,plus方法也是直接返回传入的context,minusKey方法返回自身,代码如下:

public object EmptyCoroutineContext : CoroutineContext, Serializable {
private const val serialVersionUID: Long = 0
private fun readResolve(): Any = EmptyCoroutineContext

public override fun <E : Element> get(key: Key<E>): E? = null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R = initial
public override fun plus(context: CoroutineContext): CoroutineContext = context
public override fun minusKey(key: Key<*>): CoroutineContext = this
public override fun hashCode(): Int = 0
public override fun toString(): String = "EmptyCoroutineContext"
}

二.CombinedContext

    CombinedContext是组合上下文,是存储Element的重要的数据结构。内部存储的组织结构如下图所示:
image.png

    可以看出CombinedContext是一种左偏(从左向右计算)的列表,这么设计的目的是为了让CoroutineContext中的plus方法工作起来更加自然。

    由于采用这种数据结构,CombinedContext类中的很多方法都是通过循环实现的,代码如下:

internal class CombinedContext(
// 数据结构左边可能为一个Element对象或者还是一个CombinedContext对象
private val left: CoroutineContext,
// 数据结构右边只能为一个Element对象
private val element: Element
) : CoroutineContext, Serializable {

override fun <E : Element> get(key: Key<E>): E? {
var cur = this
while (true) {
// 进行get操作,如果当前CombinedContext对象中存在,则返回
cur.element[key]?.let { return it }
// 获取左边的上下文对象
val next = cur.left
// 如果是CombinedContext对象
if (next is CombinedContext) {
// 赋值,继续循环
cur = next
} else { // 如果不是CombinedContext对象
// 进行get操作,返回
return next[key]
}
}
}
// 数据结构左右分开操作,从左到右进行fold运算
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(left.fold(initial, operation), element)

public override fun minusKey(key: Key<*>): CoroutineContext {
// 如果右边是指定的Element对象,则返回左边
element[key]?.let { return left }
// 调用左边的minusKey方法
val newLeft = left.minusKey(key)
return when {
// 这种情况,说明左边部分已经是去掉指定的Element对象的,右边也是如此,因此返回当前对象,不需要在进行包裹
newLeft === left -> this
// 这种情况,说明左边部分包含指定的Element对象,因此返回只右边
newLeft === EmptyCoroutineContext -> element
// 这种情况,返回的左边部分是新的,因此需要和右边部分一起包裹后,再返回
else -> CombinedContext(newLeft, element)
}
}

private fun size(): Int {
var cur = this
//左右各一个
var size = 2
while (true) {
cur = cur.left as? CombinedContext ?: return size
size++
}
}

// 通过get方法实现
private fun contains(element: Element): Boolean =
get(element.key) == element

private fun containsAll(context: CombinedContext): Boolean {
var cur = context
// 循环展开每一个CombinedContext对象,每个CombinedContext对象中的Element对象都要包含
while (true) {
if (!contains(cur.element)) return false
val next = cur.left
if (next is CombinedContext) {
cur = next
} else {
return contains(next as Element)
}
}
}
...
}

三.Key与Element

    Key接口与Element接口定义在CoroutineContext接口中,代码如下:

public interface Key<E : Element>

public interface Element : CoroutineContext {
// 一个Key对应着一个Element对象
public val key: Key<*>
// 相等则强制转换并返回,否则则返回空
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
// 自身与初始值进行fold操作
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
// 如果要去除的是当前的Element对象,则返回空的上下文,否则返回自身
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}

四.CoroutineContext

    CoroutineContext接口定义了协程上下文的基本行为以及Key和Element接口。同时,重载了"+"操作,相关代码如下:

public interface CoroutineContext {

public operator fun <E : Element> get(key: Key<E>): E?

public fun <R> fold(initial: R, operation: (R, Element) -> R): R

public operator fun plus(context: CoroutineContext): CoroutineContext =
// 如果要与空上下文相加,则直接但会当前对象,
if (context === EmptyCoroutineContext) this else
// 当前Element作为初始值
context.fold(this) { acc, element ->
// acc:已经加完的CoroutineContext对象
// element:当前要加的CoroutineContext对象

// 获取从acc中去掉element后的上下文removed,这步是为了确保添加重复的Element时,移动到最右侧
val removed = acc.minusKey(element.key)
// 去除掉element后为空上下文(说明acc中只有一个Element对象),则返回element
if (removed === EmptyCoroutineContext) element else {
// ContinuationInterceptor代表拦截器,也是一个Element对象
// 下面的操作是为了把拦截器移动到上下文的最右端,为了方便快速获取
// 从removed中获取拦截器
val interceptor = removed[ContinuationInterceptor]
// 若上下文中没有拦截器,则进行累加(包裹成CombinedContext对象),返回
if (interceptor == null) CombinedContext(removed, element) else {
// 若上下文中有拦截器
// 获取上下文中移除到掉拦截器后的上下文left
val left = removed.minusKey(ContinuationInterceptor)
// 若移除到掉拦截器后的上下文为空上下文,说明上下文left中只有一个拦截器,
// 则进行累加(包裹成CombinedContext对象),返回
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
// 否则,现对当前要加的element和left进行累加,然后在和拦截器进行累加
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}

public fun minusKey(key: Key<*>): CoroutineContext

... // (Key和Element接口)
}

1.plus方法图解

    假设我们有一个上下文顺序为A、B、C,现在要按顺序加上D、C、A。

1)初始值A、B、C
27ee3db5-ba83-4f8b-b155-de7974e76e4a.png
2)加上D
335ec6b6-b12f-4367-a274-5f65b4330517.png
3)加上C
6c36e62f-f050-47ca-b769-c29a91ef6f07.png
4)加上A
de380c56-5377-4fcc-a8c3-e6a579bf6609.png

2.为什么要将ContinuationInterceptor放到协程上下文的最右端?

    在协程中有大量的场景需要获取ContinuationInterceptor。根据之前分析的CombinedContext的minusKey方法,ContinuationInterceptor放在上下文的最右端,可以直接获取,不需要经过多次的循环。

五.AbstractCoroutineContextKey与AbstractCoroutineContextElement

    AbstractCoroutineContextElement实现了Element接口,将Key对象作为构造方法必要的参数。

public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element

    AbstractCoroutineContextKey用于实现Element的多态。什么是Element的多态呢?假设类A实现了Element接口,Key为A。类B继承自类A,Key为B。这时将类B的对象添加到上下文中,通过指定不同的Key(A或B),可以得到不同类型对象。具体代码如下:

// baseKey为衍生类的基类的Key
// safeCast用于对基类进行转换
// B为基类,E为衍生类
public abstract class AbstractCoroutineContextKey<B : Element, E : B>(
baseKey: Key<B>,
private val safeCast: (element: Element) -> E?
) : Key<E> {
// 顶置Key,如果baseKey是AbstractCoroutineContextKey,则获取baseKey的顶置Key
private val topmostKey: Key<*> = if (baseKey is AbstractCoroutineContextKey<*, *>) baseKey.topmostKey else baseKey

// 用于类型转换
internal fun tryCast(element: Element): E? = safeCast(element)
// 用于判断当前key是否是指定key的子key
// 逻辑为与当前key相同,或者与当前key的顶置key相同
internal fun isSubKey(key: Key<*>): Boolean = key === this || topmostKey === key
}

1.getPolymorphicElement方法与minusPolymorphicKey方法

    如果衍生类使用了AbstractCoroutineContextKey,那么基类在实现Element接口中的get方法时,就需要通过getPolymorphicElement方法,实现minusKey方法时,就需要通过minusPolymorphicKey方法,代码如下:

public fun <E : Element> Element.getPolymorphicElement(key: Key<E>): E? {
// 如果key是AbstractCoroutineContextKey
if (key is AbstractCoroutineContextKey<*, *>) {
// 如果key是当前key的子key,则基类强制转换成衍生类,并返回
@Suppress("UNCHECKED_CAST")
return if (key.isSubKey(this.key)) key.tryCast(this) as? E else null
}
// 如果key不是AbstractCoroutineContextKey
// 如果key相等,则强制转换,并返回
@Suppress("UNCHECKED_CAST")
return if (this.key === key) this as E else null
}
public fun Element.minusPolymorphicKey(key: Key<*>): CoroutineContext {
// 如果key是AbstractCoroutineContextKey
if (key is AbstractCoroutineContextKey<*, *>) {
// 如果key是当前key的子key,基类强制转换后不为空,说明当前Element需要去掉,因此返回空上下文,否则返回自身
return if (key.isSubKey(this.key) && key.tryCast(this) != null) EmptyCoroutineContext else this
}
// 如果key不是AbstractCoroutineContextKey
// 如果key相等,说明当前Element需要去掉,因此返回空上下文,否则返回自身
return if (this.key === key) EmptyCoroutineContext else this
}


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

收起阅读 »

有趣的 Kotlin 0x0D: IntArray vs Array<Int>

介绍 IntArray 整数数组。在 JVM 平台上,对应 int[]。 Array Array<T> 表示 T 类型数组。在 JVM 平台上,Array<Int> 对应 Integer[]。 验证 fun main() { &nbs...
继续阅读 »

介绍


IntArray


整数数组。在 JVM 平台上,对应 int[]


Array


Array<T> 表示 T 类型数组。在 JVM 平台上,Array<Int> 对应 Integer[]


验证


fun main() {
   val one = IntArray(10) { it }
   val two = Array<Int>(10) { it }
}

Decompile


Java Code


综上,JVM 平台上,IntArrayArray<Int> 的区别在于对应的类型不同,一个是基础类型 int 数组,另外一个是封装类型 Integer 数组,有装箱开销


开销差距



一般情况下,看不出差距,只能用放大镜看一下了。



@OptIn(ExperimentalTime::class)
fun main() {

   val duration1 = measureTime {
       case1()
  }
   println(duration1)

   val duration2 = measureTime {
       case2()
  }
   println(duration2)
}

private fun case1() {
   val t = IntArray(10_000_000)
}

private fun case2() {
   val t = Array<Int>(10_000_000) { it }
}

运行结果


使用场景



  • 默认使用 IntArray,基础类型因无装箱开销而性能好,且每个元素都有默认值 0

  • 如果数组需要使用 null 值,使用 Array<Int>


StackOverflow



高赞回答,一言以蔽之。



StackOverflow Issues


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

Flutter实现微信朋友圈高斯模糊效果

1. 背景 最近一个需求改版UI视觉觉得微信朋友圈的边缘高斯模糊挺好看,然后就苦逼吭哧的尝试在Flutter实现了,来看微信朋友圈点击展开的大图效果图: 微信朋友圈高斯模糊效果大概分4部分区域实现,如下图: 居中图片为原始图,然后背景模糊全图是原始图放大c...
继续阅读 »

1. 背景


最近一个需求改版UI视觉觉得微信朋友圈的边缘高斯模糊挺好看,然后就苦逼吭哧的尝试在Flutter实现了,来看微信朋友圈点击展开的大图效果图:


image.png|400


微信朋友圈高斯模糊效果大概分4部分区域实现,如下图:
image.png


居中图片为原始图,然后背景模糊全图是原始图放大cover模式的高斯模糊,在上下两个区域分别是两层单独处理边界的高斯模糊效果特殊处理,因此有时候可以看到微信朋友圈在上下两侧有明显分界线;


2. 实践


在Flutter侧实现高斯模糊比较简单,可以直接使用系统的BackdropFilter函数实现,需要传入一个filter方式,然后对child区域进行模糊过滤;


  const BackdropFilter({
Key? key,
required this.filter,
Widget? child,
this.blendMode = BlendMode.srcOver,
}) : assert(filter != null),
super(key: key, child: child);

Flutter提供了简化ImageFiltered实现高斯模糊,代码如下:


ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Image.network(url,fit: BoxFit.cover, height: expandedHeight, width: width),
),

通过此方式,可以非常简约实现全屏高斯模糊~,现在难点是上下边界区域的边界模糊处理,这里需要使用一个ShaderMask组件,在Flutter侧ShaderMask主要是实现渐变过渡能力的;


  const ShaderMask({
Key? key,
required this.shaderCallback,
this.blendMode = BlendMode.modulate,
Widget? child,
}) : assert(shaderCallback != null),
assert(blendMode != null),
super(key: key, child: child);

其需要shaderCallback回调渐变Shader,共提供3种渐变模式:



  • RadialGradient:放射状渐变

  • LinearGradient:线性渐变

  • SweepGradient:扇形渐变


这里我们需要使用线性渐变LinearGradient从上到下的渐变过渡,代码如下:


             ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.white,
Colors.transparent
],
).createShader(bounds);
},
child: Image.network(url,
fit: BoxFit.cover, height: closeHeight, width: width),
)


就这样实现了?当我运行时候出现如下效果,效果还挺好的:


image.png


但是当我把封面图url替换了一个浅色图片,却出现如下效果,中间区域变成了黑色的,看来是我想的简单了:


image.png


分析了下Flutter线性过度源码,其将颜色进行过渡,
Color transparent = Color(0x00000000) , 而
Color white = Color(0xFFFFFFFF),可以看到除了透明度之外,需要保证颜色不要发生大变化,其实我们诉求只是需要将透明度发生渐变即可,因此将Colors.white改为Colors.black,


             ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black,
Colors.transparent
],
).createShader(bounds);
},
child: Image.network(url,
fit: BoxFit.cover, height: closeHeight, width: width),
)


出现如下效果:


image.png


这里颜色貌似符合预期,但是混合模式出现了问题,学过Android开发的一定属性如下这张BlendMode混合模式图片:


image.png


ShaderMaster默认的混合模式是BlendMode.modulate,这个我也解释不清楚:这里有一篇相关文章juejin.cn/post/684490…


这里我们将混合模式替换为BlendMode.dstIn:只显示src和dst重合部分,且src的重合部分只有不透明度有用,经过这些操作后,整体效果最后如下所示:


image.png


最后奉上完整demo的相关代码:


  Widget buildCover(BuildContext context) {
double width = MediaQuery.of(context).size.width;
double expandedHeight = 600;
double closeHeight = 300;
const String url =
'https://img.alicdn.com/imgextra/i2/O1CN01YWcPh81fbUvpcjUXp_!!6000000004025-2-tps-842-350.png';
return Container(
height: expandedHeight,
alignment: Alignment.center,
child: Stack(
children: [
ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Image.network(url,
fit: BoxFit.cover, height: expandedHeight, width: width),
),
Container(
height: expandedHeight,
alignment: Alignment.center,
child: ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black,
Colors.transparent
],
).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: Image.network(url,
fit: BoxFit.cover, height: closeHeight, width: width),
),
)
],
),
);
}

3. 总结


通过实践,发现Flutter实现高斯模糊BackdropFilter/ImageFiltered组件,渐变实现方式ShaderMask,此外还需要掌握图形学的BlendMode混合模式,以后在碰到类似需求时候建议直接砍了UI视觉吧~~费劲~~~~


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

年薪达到多少才适合留在北京?

07年大学毕业,最初没敢来北京,在三线城市哈尔滨找了份工作,也和我的小学同学在2008年5月12日之前确定了恋爱关系,她当时还在读本科,09年9月女朋友考研来到了北京,我们面临第一次异地,10年5月我辞掉哈尔滨的工作来到了北京,12年4月在媳妇毕业前在北京把证...
继续阅读 »

07年大学毕业,最初没敢来北京,在三线城市哈尔滨找了份工作,也和我的小学同学在2008年5月12日之前确定了恋爱关系,她当时还在读本科,09年9月女朋友考研来到了北京,我们面临第一次异地,10年5月我辞掉哈尔滨的工作来到了北京,12年4月在媳妇毕业前在北京把证领了,12年7月她研究生毕业,顺利的解决了北京的户口,2012年7月21日我们从北京回东北办婚礼。

2012年底我们攒了20万,结婚父母给了小10万,都拼西凑借了十几万,凑够首付44万,在南城的价格洼地买了套86平两居现房,13年10月5日搬进新居,10月26日摇号中标,10月29日喜提二手宝来,10月31日大宝降生。

2014年4月换了份待遇不错还不出差的工作,就是加班太狠,抓紧还债。15年想着换把房换到媳妇单位附近,媳妇上班方便一些,15年12月敲定为海淀某学区房,媳妇单位对面,一举多得。16年元旦后房子签约,发现钱不够,然后把南城的房子卖掉付了首付,手头还有点结余。此时出现了问题老人还想继续住在郊区不想进城,媳妇说不想租房,只想住自己的房子,几天几夜没怎么睡觉后我决定在南城原小区又贷款买了一套而且更大一点的。2017年初换了辆30多万的车,同年点电标又排到了,买了辆300公里的电车。2020年9月疫情后首批孩子开学,我们也搬进了海淀,2022年2月22日二宝也来了。

收入嘛,10年刚来北京时1万多点,每年都在涨,14年年薪30万+,16年套了点期权,19年离职,现在薪资又回到每月1万多点。轻轻松松的活着,媳妇工资一直都是1万多。

我想说的是,一定要在年轻的时候拼一拼,学到自己吃饭的本领,不要拿着6千的工资干着6千的活,那真是在浪费生命,因为再出来找工作可能也就能到8千。不论拿着多少钱都要全力投入工作,老板不给涨你也有跳槽的资本。还有我想说的就是车子是消耗品,代步工具,别追太高,够用就行,最好不要贷款买车,压力会变大。

作者:神的小屋
来源:http://www.zhihu.com/question/430567574/answer/2479008231


说说我本人的情况:老家河南农村,2005年本科毕业,在三线城市工作五年,2010年到北京读研,在学校认识了来自山东农村的老婆。2013年毕业后留京工作,2014年老婆博士毕业也留京工作。2014年底领证,2015年初结婚。2015年底买了一套小两居(当时北京出台了公积金可以最高贷120万的政策,我和老婆工作两年左右攒了30多万,又借了30多万,在南三环这个房价洼地买了套55平的两居室)。2016年7月儿子出生。2017年初获得新能源车指标,买了辆占号车。2019年底换了一辆续航里程更长的。2021年,孩子转年要上小学了,两居室满五年了,就换了套学校稍好一点的三居室(五年多时间,买第一套房借的钱还完了,又攒了三十多万。又借了几十万,还是在南三环,买了套68平的三居室)。

我的月工资收入,最初是6000多,陆续涨到1万多点。老婆的工资,最初是1万多,现在加上公积金有3万左右。

因此,年薪多少不重要。找到另一半最重要,即使家庭经济条件很差,只要工作稳定,人品好就可以,两个人一起慢慢奋斗 ,也挺好的。不要被太多焦虑误导。买不起大房子,可以买小房子;买不起海淀朝阳的,可以买丰台。良好的心态最重要。夫妻两个携手并肩最重要。

作者:坐看北二环
来源:http://www.zhihu.com/question/430567574/answer/2377968907

收起阅读 »

巧用摩斯密码作为调试工具的入口|vConsole 在线上的2种使用方式

web
前言在做手机端项目的时候,我们经常在测试环境使用 vConsole 作为调试工具,它大概可以做这么多事情:查看 console 日志查看网络请求查看页面 element 结构查看 Cookies、localStorage 和 SessionStorage手动执...
继续阅读 »

前言

在做手机端项目的时候,我们经常在测试环境使用 vConsole 作为调试工具,它大概可以做这么多事情:

  • 查看 console 日志

  • 查看网络请求

  • 查看页面 element 结构

  • 查看 Cookies、localStorage 和 SessionStorage

  • 手动执行 JS 命令

  • 自定义插件

除了开发人员,vConsole 对于,测试人员也很有用,测试 bug 的时候,如果测试人员能拿到 console 信息和网络请求,无疑对于帮助开发快速定位问题是很有帮助的。

那问题来了,这么好用的工具,貌似大家都是在测试环境使用的,线上就没有引入,是不想让这个大大的调试按钮影响用户的使用体验么?这个理由显然站不住脚啊,谁能保证线上不出问题呢,如果线上可以用 vConsole,也许就能帮助我们快速定位问题,鉴于此,我给大家提供 2 种比较好的方式来解决这个问题。

速点触发

防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时

这种方法的原理是利用了 函数防抖的概念,我们设置每次 600 ms 的间隔,在此间隔内的重复点击将计数总和,当达到 10或者10的倍数时,启用 vconsole 显示状态的改变;

若某次点击间隔超过 600 ms,则计数归零,从新开始;

实现代码如下:

import VConsole from "vconsole";

function handleVconsole() {
 new VConsole()
 let count = 0
 let lastClickTime = 0
 const VconsoleDom = document.getElementById("__vconsole")
 VconsoleDom.style.display = "none"

 window.addEventListener("click", function () {
   console.log(`连续点击数:${count}`)
   const nowTime = new Date().getTime()
   nowTime - lastClickTime < 600 ? count++ : (count = 0);
   lastClickTime = nowTime

   if (count > 0 && count % 10 === 0) {
     if (!VconsoleDom) return false
     const currentStatus = VconsoleDom.style.display
     VconsoleDom.style.display = currentStatus === "block" ? "none" : "block";
     count = 0
  }
});
}

实际效果


使用摩斯密码

摩尔斯电码(英語:Morse code)是一种时通时断的信号代码,通过不同的排列顺序来表达不同的英文字母数字标点符号。是由美國發明家萨缪尔·摩尔斯及其助手艾爾菲德·維爾在1836年发明。--维基百科

第一种方法虽然好用,不过貌似太简单了,可能会误触,有没有一种可以通过 click 模拟实现的复杂指令呢?没错,我想到了摩斯密码; 简单来说,我们可以通过两种「符号」用来表示字符:点(·)和划(-),或叫「滴」(dit)和「嗒」(dah),下面是常见字符、数字、标点符号的摩斯密码公式标识:


假设,我们用 SOS 这个单词来表示 vconsole 启用的指令,那么通过查询其标识映射表,可以得出 SOS 的 摩斯密码表示为 ...---...,只要执行这个指令我么就改变 vconsole 按钮的显示状态就好了;那么问题又来了,怎么表示点(·)和划(-)呢,本来我想还是用点击间隔的长短来表示,比如 600ms 内属于短间隔,表示点(·),600ms - 2000ms 内属于长间隔,表示划(-);

但是实现后发现效果不太好,实际操作这个间隔不太好控制,容易输错; 后来我想到可以了双击 dblclick 事件,我们用 click 表示点(·),dblclick表示划(-),让我们实现下看看。

function handleVconsole() {
 new VConsole();
 let sos = [];
 let lastClickTime = 0;
 let timeId;
 const VconsoleDom = document.getElementById("__vconsole");
 VconsoleDom.style.display = "none";

 window.addEventListener("click", function () {
   clearTimeout(timeId);
   const nowTime = new Date().getTime();
   const interval = nowTime - lastClickTime;
   timeId = setTimeout(() => {
     console.log("click");
     
     if (interval < 3000) {
       sos.push(".");
    }

     if (interval > 3000) {
       sos = [];
       lastClickTime = 0;
    }

     console.log(sos);
     lastClickTime = nowTime;

     if (sos.join("") === "...---...") {
       if (!VconsoleDom) return;
       const currentStatus = VconsoleDom.style.display;
       VconsoleDom.style.display =
         currentStatus === "block" ? "none" : "block";
       sos = [];
    }
  }, 300);
});

 window.addEventListener("dblclick", function () {
   console.log("dbclick");
   clearTimeout(timeId);
   const nowTime = new Date().getTime();
   const interval = nowTime - lastClickTime;

   if (interval < 3000) {
     sos.push("-");
  }

   if (interval > 3000) {
     sos = [];
     lastClickTime = 0;
  }

   console.log(sos);
   lastClickTime = nowTime;

   if (sos.join("") === "...---...") {
     if (!VconsoleDom) return;
     const currentStatus = VconsoleDom.style.display;
     VconsoleDom.style.display = currentStatus === "block" ? "none" : "block";
     sos = [];
  }
});
}

实际效果如下所示,感觉还不错,除了 SOS, 还可以用其他的单词或者数字什么的,这就大大增加了误触的难度,实现了完全的定制化。


总结

本文针对移动端线上调试问题,提出了 2 种解决方案,特别是通过摩斯密码这种方式,据我所知,实为首创,如果各位觉得有帮助和启发,请不要吝啬给个一件三连哦,这次一定~~~。

作者:Ethan_Zhou
来源:juejin.cn/post/7126434333442703367

收起阅读 »

Android抓包从未如此简单

一、情景再现: 有一天你正在王者团战里杀的热火朝天,忽然公司测试人员打你电话问为什么某个功能数据展示不出来了,昨天还好好的纳,是不是你又偷偷写bug了。。。WTF!,你会说:把你手机给我,我连上电脑看看打印的请求日志是不是接口有问题。然后吭哧吭哧搞半天看到接...
继续阅读 »

一、情景再现:



有一天你正在王者团战里杀的热火朝天,忽然公司测试人员打你电话问为什么某个功能数据展示不出来了,昨天还好好的纳,是不是你又偷偷写bug了。。。WTF!,你会说:把你手机给我,我连上电脑看看打印的请求日志是不是接口有问题。然后吭哧吭哧搞半天看到接口数据返回的格式确实不对,然后去群里丢了几句服务端人员看一下这个接口,数据有问题。然后有回去打游戏,可惜游戏早已结束,以失败告终,自己还被无情的举报禁赛了。。。人生最痛苦的事莫过于此。假如你的项目已经集成了抓包助手,并且也给其他人员介绍过如何使用,那么像这类问题根本就不需要你再来处理了,遇到数据问题他们第一时间会自己看请求数据,而你就可以安心上王者了。



二、Android抓包现状


目前常见的抓包工具有Charles、Fiddler、Wireshark等,这些或多或少都需要一些配置,略显麻烦,只适合开发及测试人员玩,如果产品也想看数据怎么办纳,别急,本文的主角登场了,你可以在项目中集成AndroidMonitor,只需两步简单配置即可实现抓包数据可视化功能,随时随地,人人都可以方便快捷的查看请求数据了。


三、效果展示



俗话说无图无真相



111.jpg


222.jpg


333.jpg


抓包pc.png


四、如何使用



抓包工具有两个依赖需要添加:monito和monitor-plugin



Demo下载体验


源码地址


1、monitor接入


添加依赖


   debugImplementation 'io.github.lygttpod:monitor:0.0.4'
复制代码

-备注: 使用debugImplementation是为了只在测试环境中引入


2、monitor-plugin接入



  1. 根目录build.gradle下添加如下依赖


    buildscript {
dependencies {
......
//monitor-plugin需要
classpath 'io.github.lygttpod:monitor-plugin:0.0.1'
}
}

复制代码

2.添加插件


    在APP的build.gradle中添加:

//插件内部会自动判断debug模式下hook到okhttp
apply plugin: 'monitor-plugin'

复制代码


原则上完成以上两步你的APP就成功集成了抓包工具,很简单有没有,如需定制化服务请看下边的个性化配置



3、 个性化配置


1、修改桌面抓包工具入口名字:在主项目string.xml中添加 monitor_app_name即可,例如:
```
<string name="monitor_app_name">XXX-抓包</string>
```
2、定制抓包入口logo图标:
```
添加 monitor_logo.png 即可
```
3、单个项目使用的话,添加依赖后可直接使用,无需初始化,库里会通过ContentProvider方式自动初始化

默认端口8080(端口号要唯一)

4、多个项目都集成抓包工具,需要对不同项目设置不同的端口和数据库名字,用来做区分

在主项目assets目录下新建 monitor.properties 文件,文件内如如下:对需要变更的参数修改即可
```
# 抓包助手参数配置
# Default port = 8080
# Default dbName = monitor_db
# ContentTypes白名单,默认application/json,application/xml,text/html,text/plain,text/xml
# Default whiteContentTypes = application/json,application/xml,text/html,text/plain,text/xml
# Host白名单,默认全部是白名单
# Default whiteHosts =
# Host黑名单,默认没有黑名单
# Default blackHosts =
# 如何多个项目都集成抓包工具,可以设置不同的端口进行访问
monitor.port=8080
monitor.dbName=app_name_monitor_db
```
复制代码

4、 proguard(默认已经添加混淆,如遇到问题可以添加如下混淆代码)


```
# monitor
-keep class com.lygttpod.monitor.** { *; }
```
复制代码

5、 温馨提示


    虽然monitor-plugin只会在debug环境hook代码,
但是release版编译的时候还是会走一遍Transform操作(空操作),
为了保险起见建议生产包禁掉此插件。

在jenkins打包机器的《生产环境》的local.properties中添加monitor.enablePlugin=false,全面禁用monitor插件
复制代码

6、如何使用



  • 集成之后编译运行项目即可在手机上自动生成一个抓包入口的图标,点击即可打开可视化页面查看网络请求数据,这样就可以随时随地的查看我们的请求数据了。

  • 虽然可以很方便的查看请求数据了但是手机屏幕太小,看起来不方便怎么办呐,那就去寻找在PC上展示的方法,首先想到的是能不能直接在浏览器里边直接看呐,这样不用安装任何程序在浏览输入一个地址就可以直接查看数据

  • PC和手机在同一局域网的前提下:直接在任意浏览器输入 手机ip地址+抓包工具设置的端口号即可(地址可以在抓包app首页TitleBar上可以看到)


五、原理介绍


①、 拦截APP的OKHTTP请求(添加拦截器处理抓包请求,使用ASM字节码插装技术实现)



  • 写一个Interceptor拦截器,获取请求及响应的数据,转化为需要的数据结构


override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (!MonitorHelper.isOpenMonitor) {
return chain.proceed(request)
}
val monitorData = MonitorData()
monitorData.method = request.method
val url = request.url.toString()
monitorData.url = url
if (url.isNotBlank()) {
val uri = Uri.parse(url)
monitorData.host = uri.host
monitorData.path = uri.path + if (uri.query != null) "?" + uri.query else ""
monitorData.scheme = uri.scheme
}
......以上为部分代码展示
}
复制代码


  • 有了拦截器就可以通过字节码插桩技术在编译期自动为OKHTTP添加拦截器了,避免了使用者自己添加拦截器的操作


        mv?.let {
it.visitVarInsn(ALOAD, 0)
it.visitFieldInsn(GETFIELD, "okhttp3/OkHttpClient\$Builder", "interceptors", "Ljava/util/List;")
it.visitFieldInsn(GETSTATIC, "com/lygttpod/monitor/MonitorHelper", "INSTANCE", "Lcom/lygttpod/monitor/MonitorHelper;")
it.visitMethodInsn(INVOKEVIRTUAL, "com/lygttpod/monitor/MonitorHelper", "getHookInterceptors", "()Ljava/util/List;", false)
it.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "addAll", "(Ljava/util/Collection;)Z", true)
it.visitInsn(POP)
}
复制代码

②、 数据保存到本地数据库(room)



  • 数据库选择官方推荐Room进行数据操作


@Dao
interface MonitorDao {
@Query("SELECT * FROM monitor WHERE id > :lastId ORDER BY id DESC")
fun queryByLastIdForAndroid(lastId: Long): LiveData<MutableList<MonitorData>>

@Query("SELECT * FROM monitor ORDER BY id DESC LIMIT :limit OFFSET :offset")
fun queryByOffsetForAndroid(limit: Int, offset: Int): LiveData<MutableList<MonitorData>>

@Query("SELECT * FROM monitor")
fun queryAllForAndroid(): LiveData<MutableList<MonitorData>>

@Query("SELECT * FROM monitor WHERE id > :lastId ORDER BY id DESC")
fun queryByLastId(lastId: Long): MutableList<MonitorData>

@Query("SELECT * FROM monitor ORDER BY id DESC LIMIT :limit OFFSET :offset")
fun queryByOffset(limit: Int, offset: Int): MutableList<MonitorData>

@Query("SELECT * FROM monitor")
fun queryAll(): MutableList<MonitorData>

@Insert
fun insert(data: MonitorData)

@Update
fun update(data: MonitorData)

@Query("DELETE FROM monitor")
fun deleteAll()
}
复制代码

③、 APP本地开启一个socket服务AndroidLocalService



  • AndroidLocalService基于NanoHttpd实现的一个本地微服务库,底层是通过socket实现,同时使用注解加上javapoet框架自动生成模版代码,这样就可以很方便的创建服务了,下边是创建服务并启动服务示例代码


   //@Service标记这是一个服务,端口号是服务器的端口号,注意端口号唯一
@Service(port = 9527)
abstract class AndroidService {

//@Page标注页面类,打开指定h5页面
@Page("index")
fun getIndexFileName() = "test_page.html"

//@Get注解在方法上边
@Get("query")
fun query(aaa: Boolean, bbb: Double, ccc: Float, ddd: String, eee: Int,): List<String> {
return listOf("$aaa", "$bbb", "$ccc", "$ddd", "$eee")
}

@Get("saveData")
fun saveData(content: String) {
LiveDataHelper.saveDataLiveData.postValue(content + UUID.randomUUID());
}

@Get("queryAppInfo")
fun getAppInfo(): HashMap<String, Any> {
return hashMapOf(
"applicationId" to BuildConfig.APPLICATION_ID,
"versionName" to BuildConfig.VERSION_NAME,
"versionCode" to BuildConfig.VERSION_CODE,
"uuid" to UUID.randomUUID(),
)
}
}

//初始化
ALSHelper.init(this)
//启动服务
ALSHelper.startService(ServiceConfig(AndroidService::class.java))


然后就可以通过 ip地址 + 端口号 访问了,例如:http://172.18.41.157:9527/index

复制代码


使用AndroidLocalService之后创建和启动服务就是这么简单有没有,具体用法及细节请查看其说明文档



④、 与本地socket服务通信



  • 剩下的就是与服务器的通信了,无论使用前端使用aJax还是客户端使用okhttp都可以正常请求数据了


⑤、 UI展示数据(手机端和PC端)



  • 有了接口和数据具体展示就看可以随意定制了,如果你不喜欢默认的UI风格,那就拉源码自己定制UI哦



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

Python办公软件自动化,5分钟掌握openpyxl操作

今天给大家分享一篇用openpyxl操作Excel的文章。各种数据需要导入Excel?多个Excel要合并?目前,Python处理Excel文件有很多库,openpyxl算是其中功能和性能做的比较好的一个。接下来我将为大家介绍各种Excel操作。打开Excel...
继续阅读 »

今天给大家分享一篇用openpyxl操作Excel的文章。

各种数据需要导入Excel?多个Excel要合并?目前,Python处理Excel文件有很多库,openpyxl算是其中功能和性能做的比较好的一个。接下来我将为大家介绍各种Excel操作。

打开Excel文件

新建一个Excel文件


打开现有Excel文件


打开大文件时,根据需求使用只读或只写模式减少内存消耗。


获取、创建工作表

获取当前活动工作表:


创建新的工作表:


使用工作表名字获取工作表:


获取所有的工作表名称:


保存

保存到流中在网络中使用:



单元格
单元格位置作为工作表的键直接读取:


为单元格赋值:


多个单元格 可以使用切片访问单元格区域:


使用数值格式:


使用公式:


合并单元格时,除左上角单元格外,所有单元格都将从工作表中删除:


行、列
可以单独指定行、列、或者行列的范围:


可以使用Worksheet.iter_rows()方法遍历行:


同样的Worksheet.iter_cols()方法将遍历列:


遍历文件的所有行或列,可以使用Worksheet.rows属性:


Worksheet.columns属性:


使用Worksheet.append()或者迭代使用Worksheet.cell()新增一行数据:


插入操作比较麻烦。可以使用Worksheet.insert_rows()插入一行或几行:


Worksheet.insert_cols()操作类似。Worksheet.delete_rows()Worksheet.delete_cols()用来批量删除行和列。

只读取值
使用Worksheet.values属性遍历工作表中的所有行,但只返回单元格值:


Worksheet.iter_rows()Worksheet.iter_cols()可以设置values_only参数来仅返回单元格的值:



作者:Sinchard | 来源:python中文社区

收起阅读 »

Android ViewModelScope 如何自动取消协程

先看一下 ViewModel 中的 ViewModelScope 是何方神圣 val ViewModel.viewModelScope: CoroutineScope get() { val scope: Corouti...
继续阅读 »

先看一下 ViewModel 中的 ViewModelScope 是何方神圣


val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
}

可以看到这个是一个扩展方法,


再点击 setTagIfAbsent 方法进去


 <T> T setTagIfAbsent(String key, T newValue) {
T previous;
synchronized (mBagOfTags) {
previous = (T) mBagOfTags.get(key);//第一次肯定为null
if (previous == null) {
mBagOfTags.put(key, newValue);//null 存储
}
}
T result = previous == null ? newValue : previous;
if (mCleared) {//判断是否已经clear了
// It is possible that we'll call close() multiple times on the same object, but
// Closeable interface requires close method to be idempotent:
// "if the stream is already closed then invoking this method has no effect." (c)
closeWithRuntimeException(result);
}
return result;
}

可以看到 这边 会把 我们的 ViewModel 存储到 ViewModel 内的 mBagOfTags 中


这个 mBagOfTags 是


    private final Map<String, Object> mBagOfTags = new HashMap<>();

这个时候 我们 viewModel 就会持有 我们 viewModelScope 的协程 作用域了。


那..这也只是 表述了 我们 viewModelScope 存在哪里而已,


什么时候清除呢?


先看一下 ViewModel 的生命周期



可以看到 ViewModel 的生命周期 会在 Activity onDestory 之后会被调用。


那...具体哪里调的?


翻看源码可以追溯到 ComponentActivity 的默认构造器内


 public ComponentActivity() {
/*省略一些*/
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});
}

可以看到内部会通对 Lifecycle 添加一个观察者,观察当前 Activity 的生命周期变更事件,如果走到了 Destory ,并且 本次 Destory 并非由于配置变更引起的,才会真正调用 ViewModelStore 的 clear 方法。


跟进 clear 方法看看


public class ViewModelStore {

private final HashMap<String, ViewModel> mMap = new HashMap<>();

/**
* Clears internal storage and notifies ViewModels that they are no longer used.
*/
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}
}

可以看到这个 ViewModelStore 内部实现 用 HashMap 存储 ViewModel


于是在 clear 的时候,会逐个遍历调用 clear方法


再次跟进 ViewModel 的 clear 方法


 @MainThread
final void clear() {
mCleared = true;
// Since clear() is final, this method is still called on mock objects
// and in those cases, mBagOfTags is null. It'll always be empty though
// because setTagIfAbsent and getTag are not final so we can skip
// clearing it
if (mBagOfTags != null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
}
}
onCleared();
}

可以发现我们最初 存放 viewmodelScope 的 mBagOfTags


这里面的逻辑 就是对 mBagOfTags 存储的数据 挨个提取出来并且调用 closeWithRuntimeException


跟进 closeWithRuntimeException


 private static void closeWithRuntimeException(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

该方法内会逐个判断 对象是否实现 Closeable 如果实现就会调用这个接口的 close 方法,


再回到最初 我们 viewModel 的扩展方法那边,看看我们 viewModelScope 的真正面目


internal class CloseableCoroutineScope(context: CoroutineContext) 
: Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context

override fun close() {
coroutineContext.cancel()
}
}

可以明确的看到 我们的 ViewModelScope 实现了 Closeable 并且充写了 close 方法,


close 方法内的实现 会对 协程上下文进行 cancel。


至此我们 可以大致整理一下



  1. viewModelScope 是 ViewModel 的扩展成员,该对象是 CloseableCoroutineScope,并且实现了 Closeable 接口

  2. ViewModelScope 存储在 ViewModel 的 名叫 mBagOfTags 的HashMap中 啊

  3. ViewModel 存储在 Activity 的 ViewModelStore 中,并且会监听 Activity 的 Lifecycle 的状态变更,在ON_DESTROY 且 非配置变更引起的事件中 对 viewModelStore 进行清空

  4. ViewModelStore 清空会对 ViewModelStore 内的所有 ViewModel 逐个调用 clear 方法。

  5. ViewModel的clear方法会对 ViewModel的 mBagOfTags 内存储的对象进行调用 close 方法(该对象需实现Closeable 接口)

  6. 最终会会调用 我们 ViewModelScope 的实现类 CloseableCoroutineScope 的 close 方法中。close 方法会对协程进行 cancel。

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

Android 12新功能:使用SplashScreen优化启动体验

前言由于很多应用在启动时需要进行一些初始化事务,导致在启动应用时有一定的空白延迟,在之前我们一般的做法是通过替换 android:windowBackground 的自定义主题,使应用启动时及时显示一张默认图片来改善启动体验。在Androi...
继续阅读 »

前言

由于很多应用在启动时需要进行一些初始化事务,导致在启动应用时有一定的空白延迟,在之前我们一般的做法是通过替换 android:windowBackground 的自定义主题,使应用启动时及时显示一张默认图片来改善启动体验。

在Android 12中,官方添加了SplashScreen API,它可为所有应用启用新的应用启动界面。新的启动界面是瞬时显示的,所以就不必再自定义android:windowBackground 了。新启动页面的样式默认是正中显示应用图标,但是允许我们自定义,以便应用能够保持其独特的品牌。下面我们来看看如何使用它。

启动画面实现

其实在Android 12上已经默认使用了SplashScreen,如果没有任何配置,会自动使用App图标。

当然也允许自定义启动画面,在value-v31中的style.xml中,可以在App的主Theme中通过如下属性来进行配置:

<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
<item name="android:windowSplashScreenBackground">@android:color/white</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/anim_ai_loading</item>
<item name="android:windowSplashScreenAnimationDuration">1000</item>
<item name="android:windowSplashScreenBrandingImage">@mipmap/brand</item>
</style>
  • windowSplashScreenBackground设置启动画面的背景色

  • windowSplashScreenAnimatedIcon启动图标。就是显示在启动界面中间的图片,也可以是动画

  • windowSplashScreenAnimationDuration设置动画的长度。注意这里最大只能1000ms,如果需要动画时间更长,则需要通过代码的手段让启动画面在屏幕上显示更长时间(下面会讲到)

  • windowSplashScreenIconBackground设置启动图标的背景色

  • windowSplashScreenBrandingImage设置要显示在启动画面底部的图片。官方设计准则建议不要使用品牌图片。

运行启动应用就可以看到新的启动画面了,如下: 屏幕录制2022-01-19 上午10.gif

动画的元素

在Android 12上,显示在启动界面中间的图片会有一个圆形遮罩,所以在设计图片或动画的时候一定要注意,比如上面我的例子,动画其实就没有显示完整。对此官方给了详细的设计指导,如下:

image.png

  • 应用图标 (1) 应该是矢量可绘制对象,它可以是静态或动画形式。虽然动画的时长可以不受限制,但我们建议让其不超过 1000 毫秒。默认情况下,使用启动器图标。
  • 图标背景 (2) 是可选的,在图标与窗口背景之间需要更高的对比度时很有用。如果您使用一个自适应图标,当该图标与窗口背景之间的对比度足够高时,就会显示其背景。
  • 与自适应图标一样,前景的 ⅓ 被遮盖 (3)。
  • 窗口背景 (4) 由不透明的单色组成。如果窗口背景已设置且为纯色,则未设置相应的属性时默认使用该背景。

启动时长

默认当应用绘制第一帧后,启动画面会立即关闭。但是在我们实际使用中,一般在启动时进行一些初始化操作,另外大部分应用会请求启动广告,这样其实需要一些耗时的。通常情况下,这些耗时操作我们会进行异步处理,那么是否可以让启动画面等待这些初始化完成后才关闭?

我们可以使用 ViewTreeObserver.OnPreDrawListener让应用暂停绘制第一帧,直到一切准备就绪才开始,这样就会让启动画面停留更长的时间,如下:

...
var isReady = false
...

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
...

val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
return if (isReady) {
content.viewTreeObserver.removeOnPreDrawListener(this)
true
} else {
false
}
}
}
)
}

这样当初始化等耗时操作完成后,将isReady置为true即可关闭启动画面进入应用。

上面我们提到配置启动动画的时长最多只能是1000ms,但是通过上面的代码可以让启动画面停留更长时间,所以动画的展示时间也就更长了。

关闭动画

启动画面关闭时默认直接消失,当然我们也可以对其进行自定义。

在Activity中可以通过getSplashScreen来获取(注意判断版本,低版本中没有这个函数,会crash),然后通过它的setOnExitAnimationListener来定义关闭动画,如下:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
splashScreen.setOnExitAnimationListener { splashScreenView ->
val slideUp = ObjectAnimator.ofFloat(
splashScreenView,
View.TRANSLATION_Y,
0f,
-splashScreenView.height.toFloat()
)
slideUp.interpolator = AnticipateInterpolator()
slideUp.duration = 200L
//这里doOnEnd需要Android KTX库,即androidx.core:core-ktx:1.7.0
slideUp.doOnEnd { splashScreenView.remove() }
slideUp.start()
}
}

加上如上代码后,本来直接消失的启动画面就变成了向上退出了。

这里可以通过splashScreenView可以获取到启动动画的时长和开始时间,如下:

val animationDuration = splashScreenView.iconAnimationDurationMillis
val animationStart = splashScreenView.getIconAnimationStartMillis

这样就可以计算出启动动画的剩余时长。

顺便吐槽一下官网这里代码错了,开始时间也用了iconAnimationDurationMillis来获取,实际上应该是getIconAnimationStartMillis

低版本使用SplashScreen

只能在Android 12上体验官方的启动动画,显然不能够啊!官方提供了Androidx SplashScreen compat库,能够向后兼容,并可在所有 Android 版本上显示外观和风格一致的启动画面(这点我保留意见)。

首先要升级compileSdkVersion,并依赖SplashScreen库,如下:

android {
compileSdkVersion 31
...
}
dependencies {
...
implementation 'androidx.core:core-splashscreen:1.0.0-alpha01'
}

然后在style.xml添加代码如下:

<style name="Theme.App.Starting" parent="Theme.SplashScreen">
// Set the splash screen background, animated icon, and animation duration.
<item name="windowSplashScreenBackground">@android:color/white</item>

// Use windowSplashScreenAnimatedIcon to add either a drawable or an
// animated drawable. One of these is required.
<item name="windowSplashScreenAnimatedIcon">@drawable/anim_ai_loading</item>
<item name="windowSplashScreenAnimationDuration">1000</item> # Required for
# animated icons

// Set the theme of the Activity that directly follows your splash screen.
<item name="postSplashScreenTheme">@style/AppTheme</item> # Required.
</style>

前三个我们上面都介绍过了,这里新增了一个postSplashScreenTheme,它应该设置为应用的原主题,这样会将这个主题设置给启动画面之后的Activity,这样就可以保持样式的不变。

注意上面提到的windowSplashScreenIconBackgroundwindowSplashScreenBrandingImage没有,这是与Android12的不同之一。

然后我们将这个style设置给Application或Activity即可:

<manifest>
<application android:theme="@style/Theme.App.Starting">
<!-- or -->
<activity android:theme="@style/Theme.App.Starting">
...

最后需要在启动activity中,先调用installSplashScreen,然后才能调用setContentView,如下

class MainActivity : ComponentActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       val splashScreen = installSplashScreen()
       setContentView(R.layout.activity_main)
...

然后在低版本系统上启动应用就可以看到启动画面了。

installSplashScreen这一步很重要,如果没有这一行代码,postSplashScreenTheme就无法生效,这样启动画面后Activity就无法使用之前的样式,严重的会造成崩溃。比如在Activity中存在AppCompat组件,这就需要使用AppCompat样式,否则就会Crash。

最后注意在Android 12上依然有圆形遮罩,所以需要遵循官方的设计准则;但是在低版本系统上则没发现有这个遮罩,而且在低版本上动画无效,只会显示第一帧的画面,所以我对官方说的风格一致保留意见。

现有启动画面迁移

目前市场上的App基本都自己实现了启动页面,如果直接添加SplashScreen,就会造成重复,所以我们需要对原有启动页面进行处理。具体处理还要根据每个App自己的启动页面的实现逻辑来定,这里官方给出了一些意见,大家可以参考一下:将现有的启动画面实现迁移到 Android 12 及更高版本

总结

官方的SplashScreen有点姗姗来迟,不过效果还是不错的,使用起来也非常简单,但是一定要注意版本。虽然Androidx SplashScreen compat库可以向后兼容,但是与Android 12上还是有一些不同。


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

收起阅读 »

WebView初体验【Android】

每天认真洗脸,多读书,按时睡,少食多餐。变得温柔,大度,继续善良,保持爱心。不在人前矫情,四处诉说以求宽慰,而是学会一个人静静面对,自己把道理想通。这样的你,单身也所谓啊,你在那么虔诚地做更好的自己,一定会遇到最好的,而那个人也一定值得你所有等待。 在We...
继续阅读 »

每天认真洗脸,多读书,按时睡,少食多餐。变得温柔,大度,继续善良,保持爱心。不在人前矫情,四处诉说以求宽慰,而是学会一个人静静面对,自己把道理想通。这样的你,单身也所谓啊,你在那么虔诚地做更好的自己,一定会遇到最好的,而那个人也一定值得你所有等待。



书客创作


在WebView没有出现之前,如果要访问一个网页只能通过打开手机内的浏览器,通过浏览器来加载网页,但是打开浏览器的同时,也脱离了当前的应用软件,这样就大大的降低了网页与应用软件的交互。随着Android SDK的不断升级,官方提供一个WebView控件,专门用于加载网页并实现交互。那么到底WebView是什么?又该如何使用呢?


什么是WebView?

简单来说WebView是移动端用于加载Web页面的控件。


怎么使用WebView?

1、移动端加载网页方式


A、通过打开浏览器访问网页


String weburl ="http://www.baidu.com/";
Uri uri = Uri.parse(weburl);// weburl网址
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);

B、通过WebView打开本地网页


WebView.loadUrl("file:///android_asset/baidu.html");

注意1:本地文件放在assets文件中,assets文件是main的子文件,与res文件同级。
注意2:设置WebView支持加载本地文件。


WebSettings webSettings = webView.getSettings();
// 允许加载Assets和resources文件
webSettings.setAllowFileAccess(true);

本地baidu.html代码


C、通过WebView加载网址


webView.loadUrl("http://www.baidu.com/");

加载网址,需要在清单文件中加上网络请求权限


<uses-permission android:name="android.permission.INTERNET"/>

当WebView加载失败时,可以使用webView.reload();来重新加载。
注意:当加载完网页之后,如果发现网页无法点击,这很可能是WebView没有获取焦点。


webView.requestFocus();// 使页面获取焦点,防止点击无响应

2、WebView基本属性设置


WebView提供很多属性,需要通过WebSettings来进行设置,下面是对一些常用属性进行设置。


// 设置WebView相关属性
WebSettings webSettings = webView.getSettings();
// 是否缓存表单数据
webSettings.setSaveFormData(false);
// 设置WebView 可以加载更多格式页面
webSettings.setLoadWithOverviewMode(true);
// 设置WebView使用广泛的视窗
webSettings.setUseWideViewPort(true);
// 支持2.2以上所有版本
webSettings.setPluginState(WebSettings.PluginState.ON);
// 允许加载Assets和resources文件
webSettings.setAllowFileAccess(true);
// 告诉webview启用应用程序缓存api
webSettings.setAppCacheEnabled(true);
// 排版适应屏幕
webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
// 支持插件
webSettings.setPluginState(WebSettings.PluginState.ON);
// 设置是否启用了DOM storage AP搜索I
webSettings.setDomStorageEnabled(true);
// 设置缓存,默认不使用缓存-有缓存,使用缓存
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
// 是否允许缩放
webSettings.setSupportZoom(false);
// 是否支持通过js打开新的窗口
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
// 允许加载JS
webSettings.setJavaScriptEnabled(true);

// 隐藏滚动条
webView.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY);

3、WebView默认是通过浏览器打开网页,如何使用WebView打开网页?


WebViewClient是WebView的一个重要属性,它不仅仅能够实现WebView打开网页,而且还能够实现URL重构等功能。


// WebView默认是通过浏览器打开url,使用url在WebView中打开
webView.setWebViewClient(new WebViewClient() {
// // 旧版本
// @Override
// public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 使url在WebView中打开,在这里可以进行重构url
// webView.loadUrl(url);
// return true;
// }

// 新版本
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
// 返回false,意味着请求过程中,不管有多少次的跳转请求(即新的请求地址),均交给webView自己处理,这也是此方法的默认处理
// 返回true,说明你自己想根据url,做新的跳转,比如在判断url符合条件的情况下,我想让webView加载http://baidu.com/
// 加载Url,使网页在WebView中打开,在这里可以进行重构url
if(Build.VERSION.SDK_INT>= Build.VERSION_CODES.LOLLIPOP) {
webView.loadUrl(request.getUrl().toString());
}
return true;
}

// WebViewClient帮助WebView去处理页面控制和请求通知
@Override
public void onLoadResource(WebView view, String url) {
super.onLoadResource(view, url);
}

// 错误代码处理,一般是加载本地Html页面,或者使用TextView显示错误
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
// 当网页加载出错时,加载本地错误文件
// webView.loadUrl("file:///android_asset/error.html");
}

// 页面开始加载-例如在这里开启进度条
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
}

// 页面加载结束,一般用来加载或者执行javaScript脚本
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
}
});

4、设置WebView的WebChromeClient属性


WebChromeClient是WebView中一个非常重要的属性,使用它可以监听网页加载的进度,获取网页主题等信息。


// 监听网页加载进度
webView.setWebChromeClient(new WebChromeClient() {
// 网页Title信息
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
}

// 监听网页alert方法
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super(view, url, message, result);
}

// 显示网页加载进度
@Override
public void onProgressChanged(WebView view, int newProgress) {
// newProgress 1-100
}
});

5、WebView中使用JavaScript


WebView与网页的交互大多数是使用JavaScript来实现


//设置WebView支持JavaScript
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);

6、下载文件监听


// 下载文件
webView.setDownloadListener(new DownloadListener() {
@Override
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
// url下载文件地址
// 处理下载文件逻辑
}
});

7、后退与前进


// 返回键监听
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (webView.canGoBack())
// 判断WebView是否能够返回,能-返回
webView.canGoBack();
else
finish();
return true;
}
return super.onKeyDown(keyCode, event);
}

8、WebView优化-缓存


//设置缓存,默认不使用缓存
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);//有缓存,使用缓存
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);//不使用缓存

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

普通的加载千篇一律,有趣的 loading 万里挑一

前言在网络速度较慢的场景,一个有趣的加载会提高用户的耐心和对 App 的好感,有些 loading 动效甚至会让用户有想弄清楚整个动效过程到底是怎么样的冲动。然而,大部分的 App的 loading 就是下面这种千篇一律...
继续阅读 »

前言

在网络速度较慢的场景,一个有趣的加载会提高用户的耐心和对 App 的好感,有些 loading 动效甚至会让用户有想弄清楚整个动效过程到底是怎么样的冲动。然而,大部分的 App的 loading 就是下面这种千篇一律的效果 —— 俗称“转圈”。

loading-ios.gif

loading-android.gif

本篇我们利用Flutter 的 PathMetric来玩几个有趣的 loading 效果。

效果1:圆环内滚动的球

加载圆形球动画.gif

如上图所示,一个红色的小球在蓝色的圆环内滚动,而且在往上滚动的时候速度慢,往下滚动的时候有个明显的加速过程。这个效果实现的思路如下:

  • 绘制一个蓝色的圆环,在蓝色的圆环内构建一个半径更小一号的圆环路径(Path)。
  • 让红色小球在动画控制下沿着内部的圆环定义的路径运动。
  • 选择一个中间减速(上坡)两边加速的动画曲线。

下面是实现代码:

// 动画控制设置
controller =
AnimationController(duration: const Duration(seconds: 3), vsync: this);
animation = Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
parent: controller,
curve: Curves.slowMiddle,
))
..addListener(() {
setState(() {});
});

// 绘制和动画控制方法
_drawLoadingCircle(Canvas canvas, Size size) {
var paint = Paint()..style = PaintingStyle.stroke
..color = Colors.blue[400]!
..strokeWidth = 2.0;
var path = Path();
final radius = 40.0;
var center = Offset(size.width / 2, size.height / 2);
path.addOval(Rect.fromCircle(center: center, radius: radius));
canvas.drawPath(path, paint);

var innerPath = Path();
final ballRadius = 4.0;
innerPath.addOval(Rect.fromCircle(center: center, radius: radius - ballRadius));
var metrics = innerPath.computeMetrics();
paint.color = Colors.red;
paint.style = PaintingStyle.fill;
for (var pathMetric in metrics) {
var tangent = pathMetric.getTangentForOffset(pathMetric.length * animationValue);
canvas.drawCircle(tangent!.position, ballRadius, paint);
}
}

效果2:双轨运动

双轨运动.gif

上面的实现效果其实比较简单,就是绘制了一个圆和一个椭圆,然后让两个实心圆沿着路径运动。因为有了这个组合效果,趣味性增加不少,外面的椭圆看起来就像是一条卫星轨道一样。实现的逻辑如下:

  • 绘制一个圆和一个椭圆,二者的中心点重合;
  • 在圆和椭圆的路径上分别绘制一个小的实心圆;
  • 通过动画控制实心圆沿着大圆和椭圆的路径上运动。

具体实现的代码如下所示。

controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOutSine,
))
..addListener(() {
setState(() {});
});

_drawTwinsCircle(Canvas canvas, Size size) {
var paint = Paint()
..style = PaintingStyle.stroke
..color = Colors.blue[400]!
..strokeWidth = 2.0;

final radius = 50.0;
final ballRadius = 6.0;
var center = Offset(size.width / 2, size.height / 2);
var circlePath = Path()
..addOval(Rect.fromCircle(center: center, radius: radius));
paint.style = PaintingStyle.stroke;
paint.color = Colors.blue[400]!;
canvas.drawPath(circlePath, paint);

var circleMetrics = circlePath.computeMetrics();
for (var pathMetric in circleMetrics) {
var tangent = pathMetric
.getTangentForOffset(pathMetric.length * animationValue);

paint.style = PaintingStyle.fill;
paint.color = Colors.blue;
canvas.drawCircle(tangent!.position, ballRadius, paint);
}

paint.style = PaintingStyle.stroke;
paint.color = Colors.green[600]!;
var ovalPath = Path()
..addOval(Rect.fromCenter(center: center, width: 3 * radius, height: 40));
canvas.drawPath(ovalPath, paint);
var ovalMetrics = ovalPath.computeMetrics();

for (var pathMetric in ovalMetrics) {
var tangent =
pathMetric.getTangentForOffset(pathMetric.length * animationValue);

paint.style = PaintingStyle.fill;
canvas.drawCircle(tangent!.position, ballRadius, paint);
}
}

效果3:钟摆运动

钟摆球动画.gif 钟摆运动的示意图如下所示,一条绳子系着一个球悬挂某处,把球拉起一定的角度释放后,球就会带动绳子沿着一条圆弧来回运动,这条圆弧的半径就是绳子的长度。 钟摆示意图.png 这个效果通过代码来实现的话,需要做下面的事情:

  • 绘制顶部的横线,代表悬挂的顶点;
  • 绘制运动的圆弧路径,以便让球沿着圆弧运动;
  • 绘制实心圆代表球,并通过动画控制沿着一条圆弧运动;
  • 用一条顶端固定,末端指向球心的直线代表绳子;
  • 当球运动到弧线的终点后,通过动画反转(reverse)控制球 返回;到起点后再正向(forward) 运动就可以实现来回运动的效果了。

具体实现的代码如下,这里在绘制球的时候给 Paint 对象增加了一个 maskFilter 属性,以便让球看起来发光,更加好看点。

controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOutQuart,
))
..addListener(() {
setState(() {});
}
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});

_drawPendulum(Canvas canvas, Size size) {
var paint = Paint()
..style = PaintingStyle.stroke
..color = Colors.blue[400]!
..strokeWidth = 2.0;

final ceilWidth = 60.0;
final pendulumHeight = 200.0;
var ceilCenter =
Offset(size.width / 2, size.height / 2 - pendulumHeight / 2);
var ceilPath = Path()
..moveTo(ceilCenter.dx - ceilWidth / 2, ceilCenter.dy)
..lineTo(ceilCenter.dx + ceilWidth / 2, ceilCenter.dy);
canvas.drawPath(ceilPath, paint);

var pendulumArcPath = Path()
..addArc(Rect.fromCircle(center: ceilCenter, radius: pendulumHeight),
3 * pi / 4, -pi / 2);

paint.color = Colors.white70;
var metrics = pendulumArcPath.computeMetrics();

for (var pathMetric in metrics) {
var tangent =
pathMetric.getTangentForOffset(pathMetric.length * animationValue);

canvas.drawLine(ceilCenter, tangent!.position, paint);
paint.style = PaintingStyle.fill;
paint.color = Colors.blue;
paint.maskFilter = MaskFilter.blur(BlurStyle.solid, 4.0);
canvas.drawCircle(tangent.position, 16.0, paint);
}
}

总结

本篇介绍了三种 Loading 动效的绘制逻辑和实现代码,可以看到利用路径属性进行绘图以及动画控制可以实现很多有趣的动画效果。


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

收起阅读 »

女学霸考 692 分想当“程序媛”,网友:快劝劝孩子

当记者问道这个专业男孩子比较多时,女孩女王式发言:也没见男生考得比我好,一时间引起大家热议,下面就来一起了解一下吧。据了解,今年四川理科一本线为 521 分,这名女学霸超出 171 分之多。首先我们从各大厂校招网申数量上看(盘了字节跳动、网易、快手、三七互娱、...
继续阅读 »

近日四川成都一女学霸高考分数 692 分,直言想当程序员。

当记者问道这个专业男孩子比较多时,女孩女王式发言:也没见男生考得比我好,一时间引起大家热议,下面就来一起了解一下吧。

女孩考 692 分想当程序员


6月26日,四川成都。女学霸高考考了 692 分,其中数学成绩为 149 分,最后一道大题一个小细节扣了一分。


坦言想报考复旦大学,学电子信息类工科专业,未来要做一名“程序猿”。





当记者开玩笑提到“掉头发”和“行业里男生较多”时,女孩更是霸气发言,“家里遗传的头发比较好。程序员行业女生没有什么问题,我们学校也没有男生考得比我好”。

据了解,今年四川理科一本线为 521 分,这名女学霸超出 171 分之多。

有网友评论羡慕的同时也想劝劝孩子。


引人注意的是,记者的提问也引起了不少网友反感。毕竟程序员一开始从业者都是女性,而且当今世界上最伟大程序员排名第一位的也是女性。


那么计算机技术哪家高校强呢?

首先我们从各大厂校招网申数量上看(盘了字节跳动、网易、快手、三七互娱、金山云、浪潮集团),以下学校均排在投递数量前列:

华中科技大学、北京邮电大学、西安电子科技大学、电子科技大学、哈尔滨工业大学、东北大学、武汉大学、上海交通大学、南京大学。


当程序员包括的专业类型可以有计算机专业、软件开发专业、电子信息专业、通信专业、软件工程等,程序员的范围很广,主要包括软件设计/开发和程序编码两大类。

来源:mp.weixin.qq.com/s/vxb3c_5C-Ap_9NGRMeGltA

收起阅读 »

uniapp项目优化方式及建议

1.复杂页面数据区域封装成组件例如项目里包含类似论坛页面:点击一个点赞图标,赞数要立即+1,会引发页面级所有的数据从js层向视图层的同步,造成整个页面的数据更新,造成点击延迟卡顿对于复杂页面,更新某个区域的数据时,需要把这个区域做成组件,这样更新数据时就只更新...
继续阅读 »

介绍:性能优化自古以来就是重中之重,关于uniapp项目优化方式最全整理,会根据开发情况进行补充

1.复杂页面数据区域封装成组件

场景

例如项目里包含类似论坛页面:点击一个点赞图标,赞数要立即+1,会引发页面级所有的数据从js层向视图层的同步,造成整个页面的数据更新,造成点击延迟卡顿

优化方案

对于复杂页面,更新某个区域的数据时,需要把这个区域做成组件,这样更新数据时就只更新这个组件

注:app-nvue和h5不存在此问题;造成差异的原因是小程序目前只提供了组件差量更新的机制,不能自动计算所有页面差量

2.避免使用大图

场景

页面中若大量使用大图资源,会造成页面切换的卡顿,导致系统内存升高,甚至白屏崩溃;对大体积的二进制文件进行 base64 ,也非常耗费资源

优化方案

图片请压缩后使用,避免大图,必要时可以考虑雪碧图或svg,简单代码能实现的就不要图片

3.小程序、APP分包处理pages过多

前往官网手册查看配置

4.图片懒加载

功能描述

此功能只对微信小程序、App、百度小程序、字节跳动小程序有效,默认开启

前往uView手册查看配置

5.禁止滥用本地存储

不要滥用本地存储,局部页面之间的传参用url,如果用本地存储传递数据要命名规范和按需销毁

6.可在外部定义变量

在 uni-app 中,定义在 data 里面的数据每次变化时都会通知视图层重新渲染页面;所以如果不是视图所需要的变量,可以不定义在 data 中,可在外部定义变量或直接挂载在 vue实例 上,以避免造成资源浪费

7.分批加载数据优化页面渲染

场景

页面初始化时,逻辑层一次性向视图层传递很大的数据,使视图层一次性渲染大量节点,可能造成通讯变慢、页面切换卡顿

优化方案

以局部更新页面的方式渲染页面;如:服务端返回 100条数据 ,可进行分批加载,一次加载 50条 , 500ms 后进行下一次加载

8.避免视图层和逻辑层频繁进行通讯

  1. 减少 scroll-view 组件的 scroll 事件监听,当监听 scroll-view 的滚动事件时,视图层会频繁的向逻辑层发送数据

  2. 监听 scroll-view 组件的滚动事件时,不要实时的改变 scroll-top / scroll-left 属性,因为监听滚动时,视图层向逻辑层通讯,改变 scroll-top / scroll-left 时,逻辑层又向视图层通讯,这样就可能造成通讯卡顿

  3. 注意 onPageScroll 的使用, onPageScroll 进行监听时,视图层会频繁的向逻辑层发送数据

  4. 多使用 css动画 ,而不是通过js的定时器操作界面做动画

  5. 如需在 canvas 里做跟手操作, app端 建议使用 renderjs ,小程序端建议使用 web-view 组件; web-view 里的页面没有逻辑层和视图层分离的概念,自然也不会有通信折损

9.CSS优化

要知道哪些属性是有继承效果的,像字体、字体颜色、文字大小都是继承的,禁止没有意义的重复代码

10.善用节流和防抖

防抖

等待n秒后执行某函数,若等待期间再次被触发,则等待时间重新初始化

节流

触发事件n秒内只执行一次,n秒未过,再次触发无效

11.优化页面切换动画

场景

页面初始化时存在大量图片或原生组件渲染和大量数据通讯,会发生新页面渲染和窗体进入动画抢资源,造成页面切换卡顿、掉帧

优化方案

  1. 建议延时 100ms~300ms 渲染图片或复杂原生组件,分批进行数据通讯,以减少一次性渲染的节点数量

  2. App 端动画效果可以自定义; popin/popout 的双窗体联动挤压动画效果对资源的消耗更大,如果动画期间页面里在执行耗时的js,可能会造成动画掉帧;此时可以使用消耗资源更小的动画效果,比如 slide-in-right / slide-out-right

  3. App-nvue 和 H5 ,还支持页面预载,uni.preloadPage,可以提供更好的使用体验

12.优化背景色闪白

场景

进入新页面时背景闪白,如果页面背景是深色,在vue页面中可能会发生新窗体刚开始动画时是灰白色背景,动画结束时才变为深色背景,造成闪屏

优化方案

  1. 将样式写在 App.vue 里,可以加速页面样式渲染速度; App.vue 里面的样式是全局样式,每次新开页面会优先加载 App.vue 里面的样式,然后加载普通 vue 页面的样式

  2. app端 还可以在 pages.json 的页面的 style 里单独配置页面原生背景色,比如在 globalStyle->style->app-plus->background 下配置全局背景色

"style": { "app-plus": { "background":"#000000" } }
  1. nvue页面不存在此问题,也可以更改为nvue页面

13.优化启动速度

  1. 工程代码越多,包括背景图和本地字体文件越大,对小程序启动速度有影响,应注意控制体积

  2. App端的 splash 关闭有白屏检测机制,如果首页一直白屏或首页本身就是一个空的中转页面,可能会造成 splash 10秒才关闭

  3. App端使用v3编译器,首页为 nvue页面 时,并设置为fast启动模式,此时App启动速度最快

  4. App设置为纯 nvue项目 (manifest里设置app-plus下的renderer:"native"),这种项目的启动速度更快,2秒即可完成启动;因为它整个应用都使用原生渲染,不加载基于webview的那套框架

14.优化包体积

  1. uni-app 发行到小程序时,如果使用了 es6 转 es5 、css 对齐的功能,可能会增大代码体积,可以配置这些编译功能是否开启

  2. uni-app 的 H5端,uni-app 提供了摇树优化机制,未摇树优化前的 uni-app 整体包体积约 500k,服务器部署 gzip 后162k。开启摇树优化需在manifest配置

  3. uni-app 的 App端,Android 基础引擎约 9M ,App 还提供了扩展模块,比如地图、蓝牙等,打包时如不需要这些模块,可以裁剪掉,以缩小发行包;体积在 manifest.json-App 模块权限里可以选择

  4. App端支持如果选择纯nvue项目 (manifest里设置app-plus下的renderer:"native"),包体积可以进一步减少2M左右

  5. App端在 HBuilderX 2.7 后,App 端下掉了 非v3 的编译模式,包体积下降了3M

15.禁止滥用外部js插件

描述

有官方API的就不要额外引用js插件增加项目体积

例如

url传参加密直接用 encodeURIComponent() 和 decodeURIComponent()

作者:Panda_HYC
来源:juejin.cn/post/6997224351346982942

收起阅读 »

回村三天,二舅治好了我的精神内耗

这是我的二舅,村子里曾经的天才少年。这是我的姥姥,一个每天都在跳 poping的老太太。他们在这个老屋生活。建它的时候还没美国。老师们三次登门相劝,二舅闭着眼睛横躺在床上,一言不发,像一位断了腿的卧龙先生。第一年,二舅拒绝下床,他不知道从哪找到了一本赤脚医生手...
继续阅读 »

这是我的二舅,村子里曾经的天才少年。这是我的姥姥,一个每天都在跳 poping的老太太。他们在这个老屋生活。建它的时候还没美国。

二舅上小学是全校第一,上了初中还是全校第一,全市统考。从农村一共收上去三份试卷,其中一份就是二舅的。有一天,二舅发高烧请假回家,隔壁村的医生一天在他屁股上打了四针,二舅就成了残疾。十几岁的二舅躺在床上,再也不想回到学校。

老师们三次登门相劝,二舅闭着眼睛横躺在床上,一言不发,像一位断了腿的卧龙先生。第一年,二舅拒绝下床,他不知道从哪找到了一本赤脚医生手册,疯狂地看了一年。但二舅的腿不是伤了,而是废了,所以久病并不能成医。于是第二年,二舅扔掉了手册,从床上爬了下来,呆坐在天井里望天,像一只大号的青蛙。第三年,二舅不看天了,看家里来的一个木匠干活。木匠干了三天走了,二舅跟姥爷说他看会了,求姥爷去铁匠铺给自己打做木工的工具。三年来,二舅第一次走出了院门,去生产队给人做板凳,一天做两个,一个一毛钱,可以养活自己了。

如此几年,有一天,二舅照常拄着拐来到生产队,队长告诉二舅以后不用来了,生产队没了。二舅问为什么?队长说改革开放了,于是二舅就开始改革开放,游走在镇上的各个村子给人做木工。

有天在路上遇到了当年的那个医生,他跟二舅说要是在今天我早被告倒了,得承包你一辈子。二舅笑着骂他一句,一瘸一拐的又给人干活去了。

后来不知道什么手续上的原因,二舅的残疾证怎么都办不下来,他很失望,居然拄着拐辗转去了北京。他想去天安门广场的纪念堂说要去看看他,他就说改革开放很好,他也好。为什么呢?二舅说他公平。

很快,二舅的兜里就没剩几个钱了。他的一个堂弟在北京当兵,二舅作为军人家属住进了部队,没想到居然混得风生水起。因为二舅不爱搭讪交际,只爱干活,他不知道从哪借到了木工工具。在那个部队条件还很艰苦的年代,给士兵们默默地做了很多的柜子和桌子。

哪个士兵会不喜欢这样的homie呢?

有一天,二舅的堂弟去澡堂,看见一个老头和二舅正坐在一块泡澡,二舅的堂弟吓得一句话都说不出来,因为那个老头是他只见过几次的一位首长,此刻正蹲在池子里给二舅搓背。

后来二舅回到村里,大家都问北京怎么样?二舅说北京人搓背搓得很好。

到了两个妹妹出嫁的年纪,二舅心里很不舍。二舅有自己的表达,大姨和我妈结婚时的所有家具,每一张图纸、每一块木板、每一块玻璃、每一根装饰条、每一个螺丝、每一遍漆,都是二舅一个人完成的。

你能想象在 80 年代在一个山村的女孩子结婚的时候,能拥有这样的一套家具,是多么梦幻的事情吗?

姥姥家这么穷,妹妹出嫁有这么一套家具,婆家也会高看一眼,也许就会更好地对待自己的妹妹。你可能说我在吹牛,因为这是上海牌的家具,但你忘了这是我的二舅。二舅总有办法。什么牌子他都能给你贴上,你还要什么牌子,他还有天津牌、北京牌、香港牌,超豪华OK。

再后来,年轻的二舅领养了刚出生的宁宁,二舅拼命地在周边做工赚钱,大部分时间都把宁宁寄养在了大姨家里,很少陪伴他。宁宁小时候经常被人在背后议论,不懂礼貌。

一个被抛弃了两次的小孩,对这个世界还能有什么礼貌呢?十年前,宁宁和男朋友结婚了,20万出头的县城房子啊就出了十几万,真不敢想象他是怎么攒下来的,他就掏光了半辈子积蓄给宁宁买了房子,却开心得要死。这就是中国式的家长,中国式的可敬又可怜的家长卑微地伟大着。

二舅在30岁出头的时候迎来了说媒的高峰期。但二舅跟我说,他一时觉得他这辈子只能顾得住自己,顾不住别人了,所以从来没有动过这方面的心思。

二舅说谎了,当时有一个隔壁村的女人,有老公还有两个孩子,不知道是什么样的契机,二人的关系突然变得非常的熟络,并很快变得过于熟络。她经常来二舅家串门,二舅也经常去找他。即便是她老公在的时候,两个孩子也很喜欢二舅。

再后来他开始作为二舅家的正式一员,出席家族的一切红白喜事,并对二舅体贴入微,把他乱糟糟的小屋收拾得井井有条。二舅做工回来能吃上一碗热饭,顺手把今天结的钱递给他。就这样好多年过去了,她却并没有离婚。

二舅的四个兄妹从一开始的全力支持,转而怀疑这个女人只是图二舅的那一点钱而强烈反对。而还在上小学的宁宁则喊那个女人老狐狸,喊自己班里的她的女儿小狐狸。老实的二舅进退失据,不知所措。再后来这个女人和她的丈夫死在了外地的一个工棚,煤气中毒,二舅也终生未婚。

这段感情的细节我理解不了,大姨也都记不清了,二舅则是不愿意讲,这到底算怎么一回事呢?

既不是今日实行的仙人跳,也不是那个年月的拉帮套。那时候爱情来过没有呢?

几十年过去了,故人故事无疾而终,到现在什么也没剩下,只剩了一笔烂账,烂在了二舅一个人的心里。流了血,又长了痂,不能撕,一撕就会带下皮肉。

就这样又过去了三十年,乏善可陈。是的,普通人的生活就是这样,普通到不快进 1 万倍都没法看。

转眼姥姥已经88岁了,现在农村的人工成本也越来越高。二舅正是挣钱的好时候,他很想为自己多挣一点养老钱,将来就不用拖累宁宁。但是姥姥现在的生活已经不能自理,也不是很想活了,有一次甚至已经把绳子挂到了门框上。

中国人老说生老病死,生死之间何苦还要再隔上个老病呢,这可不是上天的不仁,而是怜悯。不然我们每个人都在七八十岁却还康健力壮之年去世,对这个世界该有多么的留恋呢?那不是更加的痛苦吗?从这个意义上来讲,老病是生死之间的必要演习。所以在几年前二舅出门的时候就开始把姥姥放到车上。去别人家做木工活的时候,就把姥姥放到身边的小板凳上。

66岁老汉随身携带88岁老母,这个6688组合简直是酷得要死。这几年二舅木工活也不做了,全职照顾姥姥,早上给姥姥洗脸,晚上给姥姥洗脚,下午给姥姥锻炼。

每走二十步就是坐下歇10秒,二舅每走20步就会落后姥姥3米,赶上这3米正好需要10秒。接着走。

这么默契的走位配合,我上一次见到还是在乔丹和皮蓬身上。乔丹喜欢给皮蓬送超跑,二舅喜欢给姥姥蒸面条,再浇上点西红柿炒鸡蛋。嗯好吃的。

二舅从小对宁宁没有什么教育可言,今天的宁宁却成为了村里最孝顺的孩子。可见让小孩将来孝顺自己的最好方法就是默默地孝顺自己的父母,小孩是小不是瞎。

其实很难把二舅定义为一个木匠。我在家这三天的时间里,他给村里人修好了一个插线板、一个燃气灶、一盏床头灯、一辆玩具车、一个掘头、一个洗衣机、一个水龙头,回来的路上被另一个婶子拦住,修好了他家的门锁。还没进家门,又被另一个老头叫到家里,说电磁炉坏了。

二舅到他家发现是他插线板的电源忘了打开。

可怜的老头。

回到家,又修好了一个买来的老人机和收音机。

姥姥有胃病,他就给姥姥针灸,人家嫌门楼上光秃秃的,木头不好看,二舅自己设计好了给人画上去,山顶修了座庙,所有的龙都是二舅雕的。村里没有神婆,二舅就成了算命师。

当然了,签子是自己做的,竹筒是自己做的,本子是自己做的,挂是自己抄来的。

他甚至有一天突发奇想,要做一把二胡。木头做弧身,电话线铜芯做弦,竹子做弓杆、钓鱼线做弓卯。我们这没有蟒蛇,他就上山抓了几条双斑锦拼成一张琴皮。

你看二舅总有办法。

很想给你们看看那把有模有样的二胡。可惜十几年前,姥姥让我的傻子弟弟拿二胡当锄头娃给玩坏了。

这个村子里有的一切农具、家具、电器、车辆。二舅不会修的,只有三样,智能手机、汽车和电脑。因为这些东西二舅也没有。不过现在智能手机也有了,宁宁买的,等他拆上几次也就会修了。

夜深了,二舅家的灯还亮着,又给谁家修东西呢?听见锣声和鞭炮声了吗?不是村里有人结婚,而是年轻人都走了之后,野猪回来了。吓唬野猪呢。

村里就剩下几百个老头老太太了,如果有什么东西坏了,送维修店去修,先别说得花钱,如果到镇上是三十里山路,如果坐客车去县城下了车,他们是连北都招不到的。

二舅就总说他能顾得住自己就不错了。他其实顾住了整个村子。村里人开玩笑叫他歪子。但我们每个人都很清楚,我们爱这个歪子,我们离不开这个歪子。

一九七七年恢复高考的时候,二舅正是十八九岁。如果不是当年发烧后轮的 4 针,二舅可能已经考上了大学,成为了一名工程师。单位分的房子,国家发的退休金,悠游自适,颐养天年。隔壁村一个老头就是这样,当年学习还没二舅学习好呢。

如果是这样,那该有多好。二舅一定会成为汪曾祺笔下父亲汪居生那样充满闲情野趣的老顽童。

看着眼前的二舅,总让我想起电影棋王里的台词:他这种奇才啊只不过是生不逢时,他应该受国家的栽培,名扬天下才对,不应该弄得这么落魄。太遗憾了,真的是太遗憾了。

我问二舅有没有这么想过?

他说从来没有。

这样的心态让二舅成为了村里第二快乐的人。第一快乐的人是刚刚——我们村的树先生。

所以你看,这个世界上第一快乐的人是不需要对别人负责的人,第二快乐的人就是从不回头看的人。

遗憾谁没有呢?人往往都是快死的时候才发现,人生最大的遗憾就是一直在遗憾过去的遗憾。遗憾在电影里是主角崛起的前戏,在生活里是让人沉沦的毒药。

我北漂九年,也曾有幸相识过几位人中龙凤,反倒是从二舅这里让我看到了我们这个民族身上所有的平凡美好与强悍。

都说人生最重要的不是胡一把好牌,而是打好一把烂牌。二舅这把烂牌,打的是真好。

他在挣扎与困顿中表现出来的庄敬自强,令我心生敬意。

我四肢健全,上过大学,又生在一个充满机遇的时代,我理应度过一个比二舅更为饱满的人生。今天二舅还在走在自己的人生路,这条长长的路最终会通往何处呢?

二舅的床下有一个几十年前的笔记本。笔记本的第一页是他摘抄的一句话:

下定决心,不怕牺牲,排除万难,去争取胜利。

是的,这条人生路最后通向的一定是胜利。

作者:衣戈猜想 https://www.bilibili.com/video/BV1MN4y177PB

收起阅读 »

作为一名前端工程师,我浪费了时间学习了这些技术

作为一名前端工程师我浪费时间学习了这些技术 不要犯我曾经犯过的错误! 我2015年刚刚开始学习前端开发的时候,我在文档和在线教程上了解到了许多技术,我浪费大量时间去学习这些技术。 在一个技术、库和框架数量不断增长的行业中,高效地学习才是关键。不管你是新的Web...
继续阅读 »

作为一名前端工程师我浪费时间学习了这些技术


不要犯我曾经犯过的错误!


我2015年刚刚开始学习前端开发的时候,我在文档和在线教程上了解到了许多技术,我浪费大量时间去学习这些技术。


在一个技术、库和框架数量不断增长的行业中,高效地学习才是关键。不管你是新的Web开发人员,还是你已经入门前端并有了一些开发经验,都可以了解一下,以下列出的技术,要么是我花费时间学习但从未在我的职业生涯中实际使用过的,要么是2021年不再重要的事情(也就是说,你可以不知道)。



Ruby / Ruby-on-rails


Ruby-on-Rails在本世纪早期非常流行。我花了几个月的时间尝试用Ruby-on-Rails构建应用程序。虽然一些大型科技公司的代码库中仍然会有一些Rails代码,但近年来我很少遇到使用Rails代码的公司。事实上,在我六年的职业生涯中,我一次也没有使用过Rails。更重要的是,我不想这么做。


AngularJS


不要把AngularJS和Angular混淆。AngularJS从版本2开始就被Angular取代了。不要因为这个原因而浪费时间学习AngularJS,你会发现现在很少有公司在使用它。


jQuery


jQuery仍然是最流行的JavaScript库,但这是一个技术上的历史遗留问题,而非真的很流行(只是很多10-15年前的老网站仍然使用它)。近年来,许多大型科技公司的代码都不再使用jQuery,而是使用常规的JavaScript。jQuery过去提供的许多好处已经不像以前那么关键了(比如能编写在所有类型的浏览器上都能工作的代码,在浏览器有非常不同的规范的年代,这是一个大的问题)。


Ember


学习Ember的热火很久以前就熄灭了。如果你需要一个JavaScript库,那就去学习React(或者Vue.js)。


React class components


如果你在工作中使用React,你可能仍然会发现一些React类组件。因此,理解它们是如何工作的以及它们的生命周期方法可能仍然是很好的。但如果你正在编写新的React组件,你应该使用带有React hook的功能性组件。


PHP


坦诚的说,PHP并没有那么糟糕。在我的第一份网页开发工作中(和Laravel一起),我确实需要经常使用它。但是现在,web开发者应该着眼于更有效地学习 Node.js。如果你已经在学习JavaScript,为什么还要在服务器端添加PHP之类的服务器端语言呢?现在你可以在服务器端使用JavaScript了。


Deno


Deno是一家新公司,在未来几年可能会成为一家大公司。然而,不要轻信炒作。现在很少有公司在使用Deno。因此,如果你是Web开发新手,那就继续学习Node.js(又名服务器端JavaScript)。不过,Deno可能是你在未来几年选择学习的东西。


Conclusion


这就是我今天想说的技术。我相信还有很多东西可以添加到技术列表中——请在评论中留下你的想法。我相信对于这里列出的技术也会有一些争论——Ruby开发者更容易破防。你也可以在评论中进行讨论,这些都是宝贵的意见。


链接:https://juejin.cn/post/7086019601372282888

收起阅读 »

API 请求慢?这次锅真不在后端

问题 我们在开发过程中,发现后端 API 请求特别慢,于是跟后端抱怨。 “怎么 API 这么慢啊,请求一个接口要十几秒”。 而且这种情况是偶现的,前端开发同学表示有时候会出现,非必现。 但是后端同学通过一顿操作后发现,接口没有问题,他们是通过 postman ...
继续阅读 »

问题


我们在开发过程中,发现后端 API 请求特别慢,于是跟后端抱怨。


“怎么 API 这么慢啊,请求一个接口要十几秒”。


而且这种情况是偶现的,前端开发同学表示有时候会出现,非必现。


但是后端同学通过一顿操作后发现,接口没有问题,他们是通过 postman 工具以及 test 环境尝试,都发现接口请求速度是没有问题的。


“那感觉是前端问题”?


我们来梳理一下问题,如下:



  • 后端 API 请求特别慢,而且是偶现的。

  • 在 test 环境没有复现。

  • postman 工具请求没有复现。


问题解决过程


时间都去哪了?


第一个问题,API 耗费的时间都用来做什么了?


我们打开 Chrome 调试工具。在 network 中可以看到每个接口的耗时。



hover 到你的耗时接口的 Waterful,就可以看到该接口的具体耗时。



可以看到,其耗时主要是在 Stalled,代表浏览器得到要发出这个请求的指令到请求可以发出的等待时间,一般是代理协商、以及等待可复用的 TCP 连接释放的时间,不包括 DNS 查询、建立 TCP 连接等时间等。


所以 API 一直在等待浏览器给它发出去的指令,以上面截图的为例,整整等待了 23.84S,它请求和响应的时间很快(最多也就几百毫秒,也就是后端所说的接口并不慢)。


所以 API 到底在等待浏览器的什么处理?


什么阻塞了请求?


经过定位,我们发现,我们项目中使用 Server-Sent Events(以下简称 SSE)。它跟 WebSocket 一样,都是服务器向浏览器推送信息。但不同的是,它使用的是 HTTP 协议。


当不通过 HTTP / 2 使用时,SSE 会受到最大连接数的限制,限制为 6 次。此限制是针对每个浏览器 + 域的,因此这意味着您可以跨所有选项卡打开 6 个 SSE 连接到 http://www.example1.com,并打开 6 个 SSE 连接到 http://www.example2.com。这一点可以通过以下这个 demo 复现。


复制问题的步骤:



结果是,第 6 次之后,SSE 请求一直无法响应,打开新的标签到同一个地址的时候,浏览器也无法访问。


效果图如下:



该问题在 ChromeFirefox 中被标记为“无法解决”。


至于偶现,是因为前端开发者有时候用 Chrome 会打开了多个选项卡,每个选项卡都是同一个本地开发地址,就会导致达到 SSE 的最大连接数的限制,而它的执行时间会很长,也就会阻塞其他的请求,一致在等待 SSE 执行完。


所以解决的方法是什么?


解决方案


简单粗暴的两个方法



  • 不要打开太多个选项卡。这样就不会达到它的限制数。(因为我们一个选项卡只请求一个 SSE)。

  • 开发环境下,关闭该功能。


使用 HTTP / 2


使用 HTTP / 2 时,HTTP 同一时间内的最大连接数由服务器和客户端之间协商(默认为 100)


这解释了为什么我们 test 环境没有问题,因为 test 环境用的是 HTTP / 2。而在开发环境中,我们使用的是 HTTP 1.1 就会出现这个问题。


那如何在开发环境中使用 HTTP / 2 呢?


我们现在在开发环境,大部分还是使用 webpack-dev-server 起一个本地服务,快速开发应用程序。在文档中,我们找到 server 选项,允许设置服务器和配置项(默认为 'http')。


只需要加上这一行代码即可。


devServer: {
+ server: 'spdy',
port: PORT,
}

看看效果,是成功了的。



原理使用 spdy 使用自签名证书通过 HTTP/2 提供服务。需要注意的一点是:



该配置项在 Node 15.0.0 及以上的版本会被忽略,因为 spdy 在这些版本中不会正常工作。一旦 Express 支持 Node 内建 HTTP/2,dev server 会进行迁移。



总结归纳


原本这个问题认为跟前端无关,没想到最后吃瓜吃到自己头上。提升相关技能的知识储备以及思考问题的方式,可能会方便我们定位到此类问题。


充分利用好浏览器的调试工具,对一个问题可以从多个角度出发进行思考。比如一开始,没想到本地也可以开启 HTTP / 2。后来偶然间想搜下是否有此类方案,结果还真有!




链接:https://juejin.cn/post/7119074496610304031

收起阅读 »

搞不定移动端性能,全球爆火的 Notion 从 Hybrid 转向了 Native

7 月 20 日,Notion 笔记程序发布了版本更新,并表示更改了移动设备上的技术栈,将从 webview 逐步切换到本机应用程序,以获得更快更流畅的性能。该团队声称该应用程序现在在 iOS 上的启动速度提高了 2 倍,在 Android 上的启动速度提高了...
继续阅读 »

7 月 20 日,Notion 笔记程序发布了版本更新,并表示更改了移动设备上的技术栈,将从 webview 逐步切换到本机应用程序,以获得更快更流畅的性能。

该团队声称该应用程序现在在 iOS 上的启动速度提高了 2 倍,在 Android 上的启动速度提高了 3 倍。


Notion 发布的这条 Twitter 也得到了广泛的关注,几天之内就有了上千条转发。由于前几年 Notion 的技术栈一直没有公开,开发者对此充满了各种猜测,很多人认为 Notion 使用的是 React Native 或 Electron,因此这次 Notion 宣称切换为原生 iOS 和原生 Android,再一次引发了“框架之争”。

其中有不少人发表了“贬低”跨平台开发的看法,对 React Native 等框架产生了质疑,毕竟现在向跨平台过渡是不可避免的,这些框架是对原生工具包的一个“威胁”,而 Notion 恰恰又切换到了“原生”开发模式。

实际上,在 2020 年之前 Notion 使用的是 React Native,随后切换到了 Hybrid 混合开发模式:使用 Kotlin/Swift + 运行网络应用程序的 Web 视图。但移动端的性能一直是一个问题,2 年之后,Notion 再次切换到了原生开发模式。

有网友认为,像 Notion 这样重 UI 和交互的产品,如果不知道如何掌握 Web 技术,那么对他们的产出速度表示担忧。面对这种吵翻天的状况,Notion 的前端工程师也因此再度出面回应这次切换的原因和一些思考。

Notion 的发展和理念

Notion 是一款将笔记、知识库和任务管理无缝衔接整合的多人协作平台。Notion 打破了传统的笔记软件对于内容的组合方式,将文档的内容分成一个个 Block,并且能够以乐高式的方式拖动组合这些 Block,让它使用起来十分灵活。

Notion 由 Ivan Zhao、Simon Last 于 2013 年在旧金山创立。去年底,Notion 获得了 2.75 亿美元的 C 轮融资。截至 2021 年 10 月,Notion 估值 103 亿美元,在全球拥有超 2000 万用户。Notion 的创始人和 CEO Ivan Zhao 是一位 80 后华人。他出生于中国新疆,曾就读于清华附中,中学时随家人移居加拿大,现在被很多人认为将成为硅谷的下一个袁征(Zoom 的创始人)。Ivan 在大学时期主修认知科学,学习的是人的大脑怎么运作,外加对计算机也很感兴趣。



Ivan 也曾表示“我的很多朋友都是艺术家。我是他们中唯一会编码的人。我想开发一款软件,它不仅可以为人们提供文档或网页。” 因此,在 2012 年大学毕业后,在文档共享初创公司 Inkling 工作期间,他创办了 Notion。原本的目标是构建一个无代码应用构建工具,不过项目很快失败了。随后 Ivan 与 Simon 迁往了日本京都,待了一年左右,小而安静的地方能“让我们专注在写代码”,在相对无压力和与世隔绝的环境下,构思并设计出了现在的 Notion 原型。用 Reddit 论坛上的一条获得高赞的网友总结就是:一个 Notion = Google docs + Evernote + Trello + Confluence + Github + Wiki +……

“工具应该模仿人脑的工作方式。但由于每个人的思维和工作方式都不同,这意味着工具需要非常灵活。”Ivan 解释道。而 Notion 创建的目的,就是将用户从一堆各式各样的生产力工具之中解放出来,给予一个干净清爽、简便易行的 All in One 工作平台。企业用户也可以在 Notion 上基本实现公司的内部管理所需要涉及到的所有功能。包括公司知识库和资料库的创建与管理、项目进度管理、信息共享、工作日志、内部社交、协作办公等等。


有人甚至说,Notion 堪比办公软件届的苹果。在 2016 年发布 1.0 版本后,因其独特的设计、专注于将事情做得更好、对投资人的冷淡态度,外加疫情远程办公潮,多方面因素让 Notion 迅速火遍全球。作为一款 All in one 的概念型工具,Notion 一直被众多企业抄作业,但它目前几乎未逢敌手。

Notion 为什么要两次更换技术栈?

Notion 在 2017 年、2018 年分别发布了 iOS 客户端和 Android 客户端。在发布 2.0 版本之后,该公司于 2019 年以 8 亿美元的估值筹集了 1000 万美元的资金。但也许和创始人的发展理念相关,Notion 的员工数量一直不多。

2019 年 3 月的时候,工程团队总共才 4 个人,当时 Notion 用 React Native 来渲染 web 视图。Notion 在 Twitter 上解释说,这是为了更快地部署新功能和进行一些其他修复。

但如果这个系统适合开发者,那么它对用户来说远非最佳:许多人抱怨移动版本非常缓慢。“即使是新 iPhone 也非常慢 - 大约 6-7 秒后我才能开始输入笔记。到那时我都快忘记了我之前想写什么。它基本上是一个非常重的 web 应用程序视图。”“如果 Notion 不选择改变,那么它将迅速被其它同类产品取代。”......



2020 年,Notion 第一次因这个问题,更改了技术栈,放弃 React Native,切换到了 Hybrid 开发环境。

Notion 前端负责人 Jake Teton‑Landis 表示,“React Native 的优势在于允许 Web 开发人员构建手机应用程序。如果我们已经有了 webview,那么 React Native 不会增加价值。对我们来说,它让一切变得更加困难:性能、代码复杂性、招聘等等。用 React Native 快速完成任务的同时,也在跟复杂性战斗,这让我们感觉束手束脚。”

虽然这次移动端的性能有了一些提升,但也没有根本解决问题,更新之后,Android 端依然是一个相当大的痛点。


Notion 也曾在 2019 年的时候表示不会很快发布本机应用程序,但他们同时强调“原生开发也是一个选择”。

7 月 20 日,Notion 发布了版本更新,并表示将从主页选项卡开始,从 webview 逐步一个个地切换到本机应用程序。

此时 Notion 工程团队也大约只有 100 人, 总共包含 3 位 iOS 工程师、4 位 android 工程师,除主页使用 SwiftUI/Jetpack Compose 进行渲染,其他部分仍然是 webview 进行绘制。

“似乎这还是招聘不足产生的人员问题。”Jake 解释说,“我们的策略是随着团队的壮大逐步本地化我们应用程序的更多部分。我们这个程序必须使用本机性能,如果它是原生的,则更容易达到这个性能要求。

凭借我们拥有的经验,以及对问题的了解,我们因此选择了原生 iOS 和原生 Android 开发。虽然出于复杂性的权衡,在可预见的未来,编辑器可能仍然是一个 webview,毕竟 Google Docs、Quip、Dropbox Paper、Coda 都使用原生 shell、webview 编辑器。”

原生开发才是王道?!

虽然无论是原生开发还是 Hybrid 都可以完成工作,但原生应用程序是按照操作系统技术和用户体验准则开发的,因此具有更快的性能优势,并能轻松访问和利用用户设备的内置功能(例如,GPS、地址簿、相机等)。

Hybrid 开发方式,通常是在面对市场竞争需要尽快构建并发布应用程序时候的一种选择。如果期望的发布时间少于六个月,那么混合可能是一个更好的选择,因为可以构建一套源代码,跨平台发布,与原生开发相比,其开发时间和工作量要少得多,但这也意味着需要做出许多性能和功能上的妥协。

如果有足够时间,那么原生方法最有意义,可以让应用程序具有最佳性能、最高安全性和最佳用户体验。毕竟,用户体验是应用程序成功的关键。互联网正在放缓,人们使用手机的时间越来越长,缓慢的应用程序意味着糟糕的业务。在这种情况下,对 Notion 来说,拥有一个快速应用程序比以往任何时候都更加重要。

参考链接:

https://www.notion.so/releases/2022-07-20

https://twitter.com/jitl/status/1530326516013342723?s=20&t=xT0gfWhFvs0yNvc1GQ3sTQ

收起阅读 »

写出优雅的Kotlin代码:聊聊我认为的 "Kotlinic"

"Kotlinic" 一词属于捏造的,参考的是著名的"Pythonic",后者可以译为“很Python”,意思是写的代码一看就很有Python味。照这个意思,"Kotlinic"就是“很Kotlin”,很有Kotlin味。 Kotlin程序员们不少是从Java...
继续阅读 »

"Kotlinic" 一词属于捏造的,参考的是著名的"Pythonic",后者可以译为“很Python”,意思是写的代码一看就很有Python味。照这个意思,"Kotlinic"就是“很Kotlin”,很有Kotlin味。


Kotlin程序员们不少是从Java转过来的,包括我;大部分时候,大家也都把它当大号的Java语法糖在用。但Kotlin总归是一门新语言,而且,在我眼里还是门挺优雅的语言。所以,或许我们可以把Kotlin写得更Kotlin些。我想简单粗浅的聊聊。



本文希望:聊聊一些好用的、简洁的但又不失语义的Kotlin代码


本文不希望:鼓励无脑追求高超技巧,完全放弃了可读性、可维护性,全篇奇技淫巧的操作



受限于本人水平,可能有错误或不严谨之处。如有此类问题,欢迎指出。也欢迎在评论区探讨交流~


善用with、apply、also、let


with和apply


with和apply,除了能帮忙少打一些代码外,重要的是能让代码区分更明确。比如


val textView = TextView(context)
textView.text = "fish"
textView.setTextColor(Color.BLUE)
textView.setOnClickListener { }
val imageView = ImageView(context)
// ...
复制代码

这就是典型的Java写法,自然,没什么问题。但要是类似的代码多起来,总感觉不知道哪里是哪里。如果换用apply呢?


val textView = TextView(context).apply {
text = "fish"
setTextColor(Color.BLUE)
setOnClickListener { }
}
val imageView = ImageView(context).apply {

}
复制代码

apply的大括号轻松划清了边界:我这里的代码和TextView相关。看着更整齐。


如果后面不需要这个变量,赋值还能省了


 // 设置某个view下的各个控件
with(view) {
findViewById<TextView>(R.id.some_id).apply {
text = "fish"
setTextColor(Color.BLUE)
setOnClickListener { }
}

findViewById<ImageView>(R.id.some_id).apply {

}
}
复制代码

apply的另一个常见场景是用于那些返回自己的函数,比如常见的Builder类的方法


fun setName(name: String): Builder{
this.name = name
return this
}
复制代码

改成apply就简洁得多


fun setName(name: String) = apply{ this.name = name }
复制代码

also


also的常见场景有很多,它的语义就是干完上一件事后附带干点什么事。 举个例子,给个函数


fun someFunc() : Model{
// ...
return Model(name = "model", value = "value")
}
复制代码

如果我们突然想加个Log,打印一下返回值,按Java的写法,要这么干:


fun someFunc(): Model{
// ...
val tempModel = Model(name = "model", value = "value")
print(tempModel)
return tempModel
}
复制代码

改的不少。但是按Kotlin的写法呢?


fun someFunc() : Model{
return Model(name = "model", value = "value").also {
print(it)
}
}
复制代码

不需要额外整个变量出来。


类似的,比如上面apply的例子,在没有声明变量的情况下,也可以这样用这个值


findViewById<ImageView>(R.id.some_id).apply {
// ...
}.also{ println(it) }
复制代码

整在一起


这几个函数结合起来,在针对一些比较复杂的场景时,对提高代码的可读性还是挺有帮助的。如【唐子玄】在这篇文章里所举的例子:



假设需求如下:“缩放 textView 的同时平移 button ,然后拉长 imageView,动画结束后 toast 提示”。



“Java”式写法


PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f, 1.3f);
PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f, 1.3f);
ObjectAnimator tvAnimator = ObjectAnimator.ofPropertyValuesHolder(textView, scaleX, scaleY);
tvAnimator.setDuration(300);
tvAnimator.setInterpolator(new LinearInterpolator());

PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", 0f, 100f);
ObjectAnimator btnAnimator = ObjectAnimator.ofPropertyValuesHolder(button, translationX);
btnAnimator.setDuration(300);
btnAnimator.setInterpolator(new LinearInterpolator());

ValueAnimator rightAnimator = ValueAnimator.ofInt(ivRight, screenWidth);
rightAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int right = ((int) animation.getAnimatedValue());
imageView.setRight(right);
}
});
rightAnimator.setDuration(400);
rightAnimator.setInterpolator(new LinearInterpolator());

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(tvAnimator).with(btnAnimator);
animatorSet.play(tvAnimator).before(rightAnimator);
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@Override
public void onAnimationEnd(Animator animation) {
Toast.makeText(activity,"animation end" ,Toast.LENGTH_SHORT).show();
}
@Override
public void onAnimationCancel(Animator animation) {}
@Override
public void onAnimationRepeat(Animator animation) {}
});
animatorSet.start();
复制代码

乱糟糟的。改成“Kotlin式”写法呢?


AnimatorSet().apply {
ObjectAnimator.ofPropertyValuesHolder(
textView,
PropertyValuesHolder.ofFloat("scaleX", 1.0f, 1.3f),
PropertyValuesHolder.ofFloat("scaleY", 1.0f, 1.3f)
).apply {
duration = 300L
interpolator = LinearInterpolator()
}.let {
play(it).with(
ObjectAnimator.ofPropertyValuesHolder(
button,
PropertyValuesHolder.ofFloat("translationX", 0f, 100f)
).apply {
duration = 300L
interpolator = LinearInterpolator()
}
)
play(it).before(
ValueAnimator.ofInt(ivRight,screenWidth).apply {
addUpdateListener { animation -> imageView.right= animation.animatedValue as Int }
duration = 400L
interpolator = LinearInterpolator()
}
)
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
Toast.makeText(activity,"animation end",Toast.LENGTH_SHORT).show()
}
override fun onAnimationCancel(animation: Animator?) {}
override fun onAnimationStart(animation: Animator?) {}
})
start()
}
复制代码

从上往下读,层次分明。读起来可以感觉到:


构建动画集,它包含{
动画1
将动画1和动画2一起播放
将动画3在动画1之后播放
。。。
}
复制代码

(上面的代码均来自所引文章)


用好拓展函数


继续上面动画的例子接着说,可以看到,最后的Listener实际上我们只用了onAnimationEnd这一部分,但却写出了一大堆。这时候,拓展函数就起作用了。


幸运的是,Google官方的androidx.core:core-ktx已经有了对应的拓展函数:


public inline fun Animator.doOnEnd(
crossinline action: (animator: Animator) -> Unit
): Animator.AnimatorListener =
addListener(onEnd = action)


public inline fun Animator.addListener(
crossinline onEnd: (animator: Animator) -> Unit = {} ,
crossinline onStart: (animator: Animator) -> Unit = {} ,
crossinline onCancel: (animator: Animator) -> Unit = {} ,
crossinline onRepeat: (animator: Animator) -> Unit = {}
): Animator.AnimatorListener {
val listener = object : Animator.AnimatorListener {
override fun onAnimationRepeat(animator: Animator) = onRepeat(animator)
override fun onAnimationEnd(animator: Animator) = onEnd(animator)
override fun onAnimationCancel(animator: Animator) = onCancel(animator)
override fun onAnimationStart(animator: Animator) = onStart(animator)
}
addListener(listener)
return listener
}
复制代码

所以上面的最后几行addListener可以改成


doOnEnd { Toast.makeText(activity,"animation end", Toast.LENGTH_SHORT).show() } 
复制代码

是不是简单得多?


当然,弹出Toast似乎也很常用,所以再搞个拓展函数


inline fun Activity.toast(text: String, duration: Int = Toast.LENGTH_SHORT) 
= Toast.makeText(this, text, duration).show()
复制代码

上面的代码又可以改成这样


 (animation.) doOnEnd  { activity.toast("animation end") } 
复制代码

再比较下原来的


 (animation.) addListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
Toast.makeText(activity,"animation end",Toast.LENGTH_SHORT).show()
}
override fun onAnimationCancel(animation: Animator?) {}
override fun onAnimationStart(animation: Animator?) {}
})
复制代码

是不是简洁得多?


上面提到androidx.core:core-ktx,其实它包含了大量有用的拓展函数。如果花点时间了解了解,或许能优化不少地方。最近掘金上也有不少类似的文章,可以参考参考


juejin.cn/post/711504…


juejin.cn/post/711692…


juejin.cn/post/712171…


用好运算符重载


Kotlin的运算符重载其实很有用,举个栗子


给List添加值


我见过这种代码


val list = listOf(1)
val newList = listOf(1, 2, 3)

val mutableList = list.toMutableList() // 转成可变的
mutableList.addAll(newList) // 添加新的
return mutableList.toList() // 返回,改成不可变的
复制代码

但是换成运算符重载呢?


val list = listOf(1)
val newList = listOf(1, 2, 3)
return list + newList
复制代码

一个"+"号,简明扼要。


又比如,想判断


某个View是否在ViewGroup中


最简单的看看索引呗


val group = LinearLayout(this)
val isContain = group.indexOfChild(view) != -1
复制代码

不过,借助core-ktx提供的运算符,我们可以写出这样的代码


val group = LinearLayout(this)
val isContain = view in group
复制代码

语义上更直接


想添加(删除)一个View?除了addView(removeView),也可以直接"+="(-=)


val group = LinearLayout(activity)
group += view // 添加子View

group -= view // 移除子View
复制代码

想遍历?重载下iterator()运算符(core-ktx也写好了),就可以直接for了


val group = LinearLayout(this)
for (child in group) {
//执行操作
}
复制代码

(这几个View的例子基本也来自上面的文章)


此外,良好设计的拓展属性和拓展函数也能帮助写出更符合语意的代码,形如


// 设置view的大小
view.setSize(width = 50.dp, height = 100.dp)
// 设置文字大小
textView.setFontSize(18.sp)
复制代码

// 获取三天后的时间
val dueTime = today + 3.days
复制代码

// 获取文本的md5编码
val md5 = "FunnySaltyFish".md5
复制代码

上面的代码很容易能看出是要干嘛,而且也非常容易实现,此处就不再赘述了。


DSL


关于DSL,大家可能都知道有这么个东西,但可能用的都不多。但DSL若用得好,确实能达到化繁为简的功效。关于DSL的基本原理和实现,fundroid大佬在Kotlin DSL 实战:像 Compose 一样写代码 - 掘金中已经写得非常清晰了,本人就不再画蛇添足,接下来仅谈谈可能的使用吧。


构建UI


DSL的一个广泛应用应该就是构建UI了。


Anko(已过时)


较早的时候,一个比较广泛的应用可能就是之前的anko库了。JetBrains推出的这个库允许我们能够不用xml写布局。放一个来自博客Kotlin之小试Anko(Anko库的导入及使用) - SoClear - 博客园的例子


private fun showCustomerLayout() {
verticalLayout {
padding = dip(30)
editText {
hint = "Name"
textSize = 24f
}.textChangedListener {
onTextChanged { str, _, _, _ ->
println(str)
}
}
editText {
hint = "Password"
textSize = 24f
}.textChangedListener {
onTextChanged { str, _, _, _ ->
println(str)
}
}
button("跳转到其它界面") {
textSize = 26f
id = BTN_ID
onClick {
// 界面跳转并携带参数
startActivity<IntentActivity>("name" to "小明", "age" to 12)
}
}

button("显示对话框") {
onClick {
makeAndShowDialog()
}
}
button("列表selector") {
onClick {
makeAndShowListSelector()
}
}
}
}

private fun makeAndShowListSelector() {
val countries = listOf("Russia", "USA", "England", "Australia")
selector("Where are you from", countries) { ds, i ->
toast("So you're living in ${countries[i]},right?")
}
}

private fun makeAndShowDialog() {
alert("this is the msg") {
customTitle {
verticalLayout {
imageView(R.mipmap.ic_launcher)
editText {
hint = "hint_title"
}
}
}

okButton {
toast("button-ok")
// 会自行关闭不需要我们手动调用
}
cancelButton {
toast("button-cancel")
}
}.show()
}
复制代码

简洁优雅,而且由于是Kotlin代码生成的,还省去了解析xml的消耗。不过,由于“现在有更好的选择”,Anko官方已经停止维护此库;而被推荐的、用于取而代之的两个库分别是:Views DSLJetpack Compose


Views DSL


关于这个库,Anko官方在推荐时说,它是“An extensible View DSL which resembles Anko.”。二者也确实很相像,但Views DSL在Anko之上提供了更高的拓展性、对AppCompat的支持、对Material的支持,甚至提供了直接预览kt布局的能力!



基本的使用可以看看上图,额外的感兴趣的大家可以去官网查看,此处就不多赘述。


\


Jetpack Compose


作为一个用Compose超过一年的萌新,我自己是十分喜欢这个框架的。但同时,目前(2022-07-25)Compose的基建确实还尚不完善,所以对企业项目来说还,是应该充分评估后再考虑。但我仍然推荐你尝试一下,因为它简单、易用。即使是在现有的View项目中,也能无缝嵌入部分Compose代码;反之亦然。


Talk is cheap, show me your code. 比如要实现一个列表,View项目(使用RecyclerView)需要xml+Adapter+ViewHolder。而Compose就简洁得多:


LazyColumn(Modifier.fillMaxSize()) {
items(10) { i ->
Text(text = "Item $i", modifier = Modifier
.fillMaxWidth()
.clickable {
context.toast("点击事件")
}
.padding(8.dp), style = MaterialTheme.typography.h4)
}
}
复制代码

上面的代码创造了一个全屏的列表,并且添加了10个子项。每个item是一个文本,并且简单设置了其样式和点击事件。即使是完全不懂Compose,阅读代码也不难猜到各项的含义。运行起来,效果如下:



构建复杂的“字符串”


拼接字符串是一项常见的工作,不过,当它复杂起来但又有一定结构时,简单的"+"或者模板字符串看起来就有些杂乱了。这时,DSL就能很优雅的解决这个任务。


举几个常见的例子吧:


Html


使用DSL,能够写出类似这样的代码


val htmlText = buildHtml{
html{
body{
div("id" to "wrapper"){
p{ +"这是一个段落" }
repeat(3){ i ->
li{ +"Item ${i+1}" }
}
img("src" to "https://www.xxx.xxx/", "width" to "100px")
}
}
}
}
复制代码

上述代码会生成类似这样的html


<!DOCTYPE html>
<html lang="zh-CN">
<body>
<div id="wrapper">
<p>这是一个段落</p>
<ul>Item 1</ul>
<ul>Item 2</ul>
<ul>Item 3</ul>
<img src="https://www.xxx.xxx/" width="100px">
</div>
</body>
</html>
复制代码

简洁直接,而且不容易出错。


你可能比较疑惑上面的+"xxx"是个啥,其实这是用了运算符重载把String转成了纯文本Tag。代码可能类似于


open class Tag()
open class TextTag(val value: String) : Tag()
operator fun String.unaryPlus() = TextTag(this)
复制代码

Markdown


类似的,也可以用这种方式生成markdown。代码可能类似于


val markDownText = buildMarkdown {
text("我是")
link("FunnyFaltyFish", "https://github.com/FunnySaltyFish")
newline()
bold("很高兴见到你~")
}
复制代码

生成的文本类似于


我是 [FunnySaltyFish](https://github.com/FunnySaltyFish)  
** 很高兴见到你~ **
复制代码

SpannableString


对Android开发者来说,这个东西估计更常见。但传统的构造方式可以说够复杂的,所以DSL也能用。好的是,Google已经在core-ktx里写好了更简便的方法


使用例子如下:


val build = buildSpannedString {
backgroundColor(Color.YELLOW) {
append("我叫")
bold {
append("FunnySaltyFish")
}
append(",是一名学生")
}
}
复制代码

渲染出的效果如下


image.png


待续


本文应该还没有完,不过貌似写着写着也不短了,所以就先发了吧(主要是再晚些就赶不上征稿了 (笑))。后面我还想聊聊kotlin的代理、协程、Collection……争取下次见!


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

Android 平台 Native Crash 问题分析与定位

一 Native Crash 简介 Native Crash 是发生在 Android 系统中 C/C++ 层面的 Crash,具体可参考: # Android 平台 Native Crash 捕获原理详解 二 Native C/C++ Libraries 简...
继续阅读 »

一 Native Crash 简介


Native Crash 是发生在 Android 系统中 C/C++ 层面的 Crash,具体可参考: # Android 平台 Native Crash 捕获原理详解


二 Native C/C++ Libraries 简介


Android 开发中通常是将 Native 层代码打包为.so格式的动态库文件,然后供 Java 层调用,.so库文件通常有以下三种来源:



  • Android 系统自带的核心组件和服务,如多媒体库、OpenGL ES 图形库等

  • 引入的第三方库

  • 开发者自行编译生成的动态库


2.1 .so文件组成


一个完整的 .so 文件由 C/C++代码和一些 debug 信息组成,这些 debug 信息会记录 .so中所有方法的对照表,就是方法名和其偏移地址的对应表,也叫做符号表 symbolic 信息,这种 .so被称为未 strip 的,通常体积会比较大。



通常 release 的.so都是需要经过 strip 操作,strip 之后的.so中的 debug 信息会被剥离,整个 so 的体积也会缩小许多。


可以简单将这个 debug 信息理解为 Java 代码混淆中的 mapping 文件,只有拥有这个 mapping 文件才能进行堆栈分析。如果堆栈信息丢了,基本上堆栈无法还原,问题也无法解决。


所以,这些 debug 信息尤为重要,是我们分析 Native Crash 问题的关键信息,那么我们在编译 .so 时 候务必保留一份未被 strip 的.so或者剥离后的符号表信息,以供后面问题分析。


2.2 查看 so 状态


也可以通过命令行来查看.so的状态,Linux 下使用 file 命令即可,在命令返回值里面可以查看到.so的一 些基本信息。


如下代码所示,stripped 代表是没有 debug 信息的.so,with debug_info, not stripped 代表携带 debug 信息的.so


file libbreakpad-core-s.so
libbreakpad-core-s.so: *******, BuildID[sha1]=54ad86d708f4dc0926ad220b098d2a9e71da235a, stripped
file libbreakpad-core.so
libbreakpad-core.so: ******, BuildID[sha1]=54ad86d708f4dc0926ad220b098d2a9e71da235a, with debug_info, not stripped
复制代码

2.3 获取 strip 和未被 strip 的 so


目前 Android Studio 无论是使用 mk 或者 Cmake 编译的方式都会同时输出 strip 和未 strip 的 so,如下图是 Cmake 编译 so 产生的两个对应的 so。




strip 之前的 so 路径:{project}/app/build/intermediates/merged_native_libs


strip 之后的 so 路径:{project}/app/build/intermediates/stripped_native_libs


三 Native Crash 捕获与解析


3.1 通过 DropBox 日志解析


Android Dropbox 是 Android 在 Froyo(API level 8) 引入的用来持续化存储系统数据的机制。主要用于记录 Android 运行过程中, 内核, 系统进程, 用户进程等出现严重问题时的 log, 可以认为这是一个可持续存储的系统级别的 logcat。


相关文件记录存储目录:/data/system/dropbox


只需要将 DropBox 的日志获取到即可进行分析解决,下面贴上一份 Log 示例。


DropBox 中的 Tombstone 文件显示,Native Crash 发生在动态库 libnativedemo.so 中,具体的方法和行数可以用 Android/SDK/NDK 提供的工具 linux-android-addr2line 来进一步定位。


addr2line 工具通常在 ndk 目录下,例如:


${SDK Path}/ndk/21.4.7075529/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line
复制代码

然后使用命令行,既可将偏移地址转换为 crash 方法和行数


arm-linux-androideabi-addr2line [option(s)] [addr(s)]
复制代码

简单来说就是 arm-linux-androideabi-addr2line + 可选项 + 异常地址



























































[option(s)]介绍
@从文件中读取 options
-a在结果中显示地址 addr
-b设置二进制文件的格式
-e设置输入文件(常用:选项后面需要跟报错的共享库,用于 addr2line 程序分析)
-iunwind inline function
-jRead section-relative offsets instead of addresses
-p让输出更易读
-s在输出中,剥离文件夹名称
-f显示函数名称
-C(大写的) 将输出的函数名 demangle
-h输出帮助
-v输出版本信息

使用 addr2line 进行解析,结果可以看到,Native Crash 发生在文件 native-lib.cpp17 行的 Crash() 方法


结合代码分析,在 Crash() 中,对空指针 *a 进行了赋值操作,所以造成了 crash。


#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_elijah_nativedemo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

/**
* 引起 crash
*/
void Crash() {
volatile int *a = (int *) (NULL);
*a = 1;
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_elijah_nativedemo_MainActivity_nativeCrash(JNIEnv *env, jobject thiz) {
Crash();
}
复制代码

通过读取 DropBox 获得 crash log -> addr2line 解析偏移地址的方法确实可以定位到 native crash 发生的现场,但是 DropBox 只有系统应用能访问,非系统应用拿不到日志。对于非系统应用,可以使用 google 提供的开源工具 BreakPad 进行监测分析。


3.2 通过 BreakPad 捕获解析


3.2.1 breakpad 简介


BreakPad 是 Google 开发的一个跨平台 C/C++ dump捕获开源库,崩溃文件使用微软的 minidump格式存储,也支持发送这个 dump 文件到你的服务器,breakpad 可以在程序崩溃时触发 dump 写入操作,也可以在没有触发 dump 时主动写 dump 文件。breakpad 支持 windows、linux、macos、android、ios 等。目前已有 Google Chrome, Firefox, Google Picasa, Camino, Google Earth 等项目使用。


3.2.2 实现原理


在不同平台下使用平台特有的函数以及方式实现异常捕获:


Windows:通过 SetUnhandledExceptionFilter()设置崩溃回掉函数


Max OS:监听 Mach Exception Port 获取崩溃事件


Linux:监听 SIGILL SIGSEGV 等异常信号 获取崩溃事件


工作原理示意图


图片右上角是一个完整的应用程序,它包含了三部分即程序代码、Breakpad Client(即 brekapad 提供出来的静态库),调式信息




  • Build System中 breakpad 的 symbol 生成工具借助应用层序中的 Debugging Information 这一部分生成一个 Google 自己的符号文件,最终在发布应用层序的时候使用 strip 将调式信息去除




  • User's System中运行的应用程序是通过 strip 去除了调式信息的,若应用程序发生 Crash,Breakpad client 就会写 minidump 文件到指定目录,也可以将产生的 minidump 文件发送到远端服务器即 Crash Colletcor。




  • Crash Collector就可以利用 Build System 中产生的 symol 文件和 User's System 中上报的 minidump 文件生成用户可读的 stack trace




3.2.3 使用示例


获取 breakpad 源码


github.com/google/brea…


执行安装 breakpad


1. cd breakpad 目录
2. 直接命令窗口输入:

./configure && make
复制代码

移植 Breakpad 到客户端程序


breakpad 源码导入应用程序 cpp 目录下



然后在 breakpad 中创建 CMakeLists.txt


cmake_minimum_required(VERSION 3.18.1)
 
#导入头文件
include_directories(src src/common/android/include)
#支持汇编文件的编译
enable_language(ASM)
#源文件编译为静态库
add_library(breakpad STATIC
        src/client/linux/crash_generation/crash_generation_client.cc
        src/client/linux/dump_writer_common/thread_info.cc
        src/client/linux/dump_writer_common/ucontext_reader.cc
        src/client/linux/handler/exception_handler.cc
        src/client/linux/handler/minidump_descriptor.cc
        src/client/linux/log/log.cc
        src/client/linux/microdump_writer/microdump_writer.cc
        src/client/linux/minidump_writer/linux_dumper.cc
        src/client/linux/minidump_writer/linux_ptrace_dumper.cc
        src/client/linux/minidump_writer/minidump_writer.cc
        src/client/linux/minidump_writer/pe_file.cc
        src/client/minidump_file_writer.cc
        src/common/convert_UTF.cc
        src/common/md5.cc
        src/common/string_conversion.cc
        src/common/linux/breakpad_getcontext.S
        src/common/linux/elfutils.cc
        src/common/linux/file_id.cc
        src/common/linux/guid_creator.cc
        src/common/linux/linux_libc_support.cc
        src/common/linux/memory_mapped_file.cc
        src/common/linux/safe_readlink.cc)
#导入相关的库
target_link_libraries(breakpad log)
复制代码

breakpad 中的 CMakeLists.txt 创建完成后,还需要在 cpp 目录下的 CMakeLists.txt 中进行配置,将刚刚创建的 CMakeLists.txt 引入进去


cmake_minimum_required(VERSION 3.18.1)
 
#引入头文件
include_directories(breakpad/src breakpad/src/common/android/include)
 
add_library(nativecrash SHARED nativecrashlib.cpp)
 
#添加子目录,会自动查找这个目录下的 CMakeList
add_subdirectory(breakpad)
 
target_link_libraries(nativecrash log breakpad)
复制代码

breakpad 初始化


然后在自己项目的 native 文件中对 breakpad 进行初始化,如下


#include <jni.h>
#include <string>
#include "breakpad/src/client/linux/handler/exception_handler.h"
#include "breakpad/src/client/linux/handler/minidump_descriptor.h"

/**
* 引起 crash
*/
void Crash() {
volatile int *a = (int *) (NULL);
*a = 1;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_elijah_nativedemo_MainActivity_nativeCrash(JNIEnv *env, jobject thiz) {
Crash();
}

//回调函数
bool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor,
void* context,
bool succeeded) {
printf("Dump path: %s\n", descriptor.path());
return false;
}

//breakpad 初始化
extern "C"
JNIEXPORT void JNICALL
Java_com_elijah_nativedemo_MainActivity_initNative(JNIEnv *env, jclass clazz, jstring path_) {
const char *path = env->GetStringUTFChars(path_, 0);
google_breakpad::MinidumpDescriptor descriptor(path);
static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback,
NULL, true, -1);
env->ReleaseStringUTFChars(path_, path);
}
复制代码

Java 层代码


Java 层传入 Crash dump 文件的保存路径,用于崩溃时文件的生成


package com.elijah.nativedemo;

import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import java.io.File;

public class MainActivity extends AppCompatActivity {

static {
System.loadLibrary("nativedemo");
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init(this);
findViewById(R.id.crash)
.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
nativeCrash();
}
});
}

public static void init(Context context){
Context applicationContext = context.getApplicationContext();
File file = new File(applicationContext.getExternalCacheDir(),"native_crash");
if(!file.exists()){
file.mkdirs();
}
initNative(file.getAbsolutePath());
}

/**
* 模拟崩溃
*/
public static native void nativeCrash();

/**
* 初始化 breakpad
* @param path
*/
private static native void initNative(String path);
}
复制代码

捕获 Crash,解析 dump


Native Crash 产生后,breakpad 会捕获 crash 信息,生成后缀为.dmp的 dump 文件到指定目录下。


.dmp 格式的文件通常无法查看,需要解析工具对这个文件进行解析。解析工具在步骤“执行安装 breakpad”中就已经生成在 breakpad/src/processor目录下,名为 minidump_stackwalk


输入如下指令即可解析 dump 文件


./minidump_stackwalk my.dump > crash.txt
复制代码

生成的 crash.txt 如下图所示,关键代码是红框的部分,Thread 0 后面有一个 crashed 标识,说明这里是发生崩溃的线程,而下面就是崩溃的文件以及内存地址,使用 3.1 中介绍的 addr2line 工具进行解析即可得到问题方法与行号


参考文献


Android NativeCrash 捕获与解析


Android---Native层崩溃的监听工具BreakPad


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

关于标准 MVVM 设计模式在 Android 中应用的思考

本来这篇文章很早就应该写的,一直没(比)有(较)时(懒)间 今天决定把它写完咯 首先表明态度, I think: 网上流传的 ViewModel + LiveData + XXX 的号称 MVVM 的代码设计基本都是假的(fake news :P) MV...
继续阅读 »

本来这篇文章很早就应该写的,一直没(比)有(较)时(懒)间

今天决定把它写完咯



首先表明态度, I think:

网上流传的 ViewModel + LiveData + XXX 的号称 MVVM 的代码设计基本都是假的(fake news :P)




MVVM


首先说一下 MVVM 这种架构模式(或者说设计模式),我对它的认识源于维基百科


mvvm_pattern


我们来看看关于 MVVM 各组件的标准定义



  • model: 没啥好说的,跟 MVP MVC 之流大差不差,定义了业务(数据)逻辑和数据的模型

  • view: 还是没啥好说的,就是视图层,用户界面

  • view-model: 关键在这里,我们所谓的 view-model 其实是从 presenter 胶水演化出来的,暴露视图的公开属性和指令,本质上就是 view 视图对应的一个 model

    • 首先,它是一个 view model,对 model 层暴露来自 view 层的一些公开属性,以及来自 view 层的一些指令(presenter?)

    • And,binder:
      view-model 区别于 presentercontroller,它有一个称之 binder 的东西,作用是处理 view-model 中暴露的视图属性(状态)与视图 UI 的自动同步




所以 MVVM 模式下的流转路径应该是这样的:



  • view: user input event -> view-model: view property or command

  • view-model: handle input, biz model -> model: biz data/logic processing

  • model: state chagne events(data) -> view-model: handle biz state

  • view-model: ui state update -> binder: handle ui state synching


MVP


ok,我们再来看一下 MVP 是怎么流转的


mvp_mode



  • view: User input event -> presenter: function (command)

  • presenter: biz model -> model: biz data/logic processing

  • model: state change events(data) -> presenter: convert biz state

  • presenter: biz state/data -> view: refresh ui


仔细对比一下往上流传的基于 Jetpack ViewModel + LiveData 的伪 MVVM(MVP)的流转情况:



  1. Activity/Fragemnt -> ViewModel, 这是 view -> presenter

  2. ViewModel 调用 model 处理网络请求、数据逻辑、文件 IO 等业务逻辑,这是 presenter -> model

  3. 这里我们看一下在 ViewModel 中完成了业务逻辑通知 UI 刷新,通过 LiveDatasetValue/postValue 更新状态,

    view 层通过 viewModel.xxxLiveData.observe(lifecycleOwner) { data -> … }


在最后一个环节,我们对比一下经典的 MVP 模式的写法:presenter 通过持有的 view 接口通知视图变更,view 层在对应的接口实现中完成对 UI 组件的更新


看 ~ 发现了什么

即使是通过 LiveData 观察者模式在 view 层实现对数据的观察,省去了经典 MVP 写法的 view 接口定义和耦合,但是在事件(数据)流转的路径上,依然是走的 MVP 的模式


对比 MVVM 定义的工作流程,不难发现,其中最大的差异在于 binder 这个角色的存在

binder 作为实现数据和 UI 同步的重要组件,同时按照 MVVM 模式的定义,属于 view-model 的内部成员


因此可以得出结论:MVVM 的关键在于,用户事件的流转是单向,从 view 层开始,到 view-model 结束;而这其中的关键在于 binder


Jetpack data-binding


Databinding 就是 Google 爸爸为我们提供的一个官方 binder 实现方案


即:



  • MVVM 中的 binder 可以直接使用官方 data-binding 组件来实现

  • 不使用 data-binding 组件,自定义处理数据与 UI 的同步的 binder 并在 view-model 中维护,也是规范的 MVVM 写法


DataBinding 分为两个部分


ViewDataBinding:每个被 <layout> 标签包裹的布局都会对应生成一个 ViewDataBinding 的子类作为视图与数据绑定的管理者
Observable/BaseObservable: 实际的 binder 开发接口,需要绑定与 view 层建立绑定关系的数据通过实现此接口并注册成员,即可自动完成监听与同步(实际代码在生成的 XxxLayoutBindingImpl 类中)


关于 DataBinding 的使用及原理,此处不予赘述


MVVM 在实际项目中的落地


sample Activity


class SampleBindingActivity : AppCompatActivity(), ActivityBindingHolder<SampleActivityBinding> by ActivityBinding(R.layout.sample_activity) {

private val viewModel: SampleViewModel by viewModel()

override fun onCreate(savedInstanceState: Bundle?) {
// replace setContentView(), and hold binding instance
inflateBinding { binding ->
// init with binding
binding.initView()

viewModel.bind(binding)
}

}

private fun SampleActivityBinding.initView() {
val random = Random()
btnTest.onClick {
viewModel.random = random.nextInt(100)
}
}
}

sample layout


<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="binder"
type="package.SampleBinder" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="@{binder.nickname}"
android:textSize="12sp"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintEnd_toEndOf="@id/channel_name"
app:layout_constraintStart_toStartOf="@id/program_vip" />

<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_test"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_nickname" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

sample view-model


class SampleViewModel : ViewModel() {

var random: Int by ObservableProperty(0) { binder.nickname = "Nickname_$it" }

private val model = SampleModel() // biz handler model, network/data/io etc.
private val binder = SampleBinder() // binder for sync data and view state

fun bind(binding: ViewDataBinding) {
binding.setVariable(BR.binder, binder)
}


}

sample binder


class SampleBinder : BaseObservable() {

@get:Bindable
var nickname: String by observableField(BR.nickname, "Nickname")


}

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

Flutter【手势&绘制】模拟纸质书籍翻页

前言 今天继续探索绘制与手势的组合实践,之前在看电子书切换页面时会有一个模拟纸质书籍翻页效果,这是典型的绘制和手势的结合实现的效果,那么今天我们就用Flutter也实现这样的一个效果吧。 原理 大家可以找本书翻页到一半看下效果,从右下角翻到一半时,我们可以将可...
继续阅读 »

前言


今天继续探索绘制与手势的组合实践,之前在看电子书切换页面时会有一个模拟纸质书籍翻页效果,这是典型的绘制和手势的结合实现的效果,那么今天我们就用Flutter也实现这样的一个效果吧。


原理


大家可以找本书翻页到一半看下效果,从右下角翻到一半时,我们可以将可视区域分为下图ABC三部分区域。


image.png

A:下一页可视区域。

B:当前页不可视区域,翻的页不可见的区域。

C:当前页可视区域,也就是需要翻的页的可视区域。


原理分解:


我们可以先将A区域和B区域合为一个区域计算,那么根据路径联合C区域自然就可以得到,至于A、B区域区分后面再讲,看下图:

image.png

a为手指触摸点,表示翻页右下角位置。【已知】

f为固定书籍右下角位置。【已知】


a点和f已知,连接af,我们令g点为af的中点,过g点连接eh垂直af,为af中垂线, 可得 g = Point((a.x + f.x) / 2, (a.y + f.y) / 2);


并且知道△egf△emg△mfg为三个直角三角形,由直角三角形相似原理可知这三个三角型两两相似,所以,△emg相似△mfg,可知:

em/gm = gm/mf;

em = gm*gm/mf;

因为:gm = f.y-a.y; mf=f.x-g.x;

可得 e = Point(g.x - (pow(f.y - g.y, 2) / (f.x - g.x)), f.y);


同理过g点做fh垂直线可得h点坐标。略...


从上方理论图可知,cdb是一条二阶贝塞尔曲线,控制点为e点, abak为直线线段,接下来我们令nag的中点,同理过n点垂直于af连接cj,可知ce等于ef的一半;(可以画辅助线过gf中点垂直af得出)。

所以可得 c = Point(e.x - (f.x - e.x) / 2, f.y);

j点坐标同理。略...


接下来我们看下b点,目前我们已知 aecj点坐标,现在b点就是aecj的相交点。


那么问题来了:

用我们九年义务教育学的数学知识解决以下两个问题。


1、在坐标系中,已知两点(x1,y1)、(x2,y2)坐标,求过这两点直线函数?


2、已知两条直线函数求两条直线的相交点?


我们知道直线函数表达式为:y=kx+b;,假设k为正常值,我们可求得kb的值,


/// 两点求直线方程
static double towPointKb(Point p1, Point p2,
{bool isK = true})
{
/// 求得两点斜率
double k = 0;
double b = 0;
// 防止除数 = 0 出现的计算错误 a e x轴重合
if (p1.x == p2.x) {
// k 为无穷大 函数表达式变为 x= 常量。
k = (p1.y - p2.y) / (p1.x - p2.x-1);
} else {
k = (p1.y - p2.y) / (p1.x - p2.x);
}
b = p1.y - k * p1.x;
if (isK)
return k;
else
return b;
}

通过两条直线表达式的k值和b值,我们就可以求出两条直线是否平行、相交、重合等情况,若相交则可求出。


k相同b不同:平行无交点。

k相同b相同:重合。

k不同无论b相不相同,相交必有一交点。


那么就可得出b点坐标:(假设k永不相等)


b = Point((b2 - b1) / (k1 - k2), (b2 - b1) / (k1 - k2) * k1 + b1);

k点坐标同理。略...


绘制


以上AB区域的关键点已经全部得到了,我们将辅助线去掉将这些点连接起来看下效果。


image.png


得到AB区域的同时,我们间接的就得到了C区域,


// mPath 为书籍矩形区域
Path mPathC = Path.combine(PathOperation.reverseDifference, mPathAB, mPath);

接下来将AB区域进行区分,再回到上方,坐标图黄色线条部分,我们可以看到d点和i点坐标。

通过原理解析我们可知d点为pe的中点,而p点为cb的中点,那么就可以得出:

p.x = (e.x -c.x)/2; ,d.x = (e.x-p.x)/2;

p.y = (e.y -b.y)/2; ,d.y = (e.y-p.y)/2;


所以可得 d = Point(((c.x + b.x) / 2 + e.x) / 2, ((c.y + b.y) / 2 + e.y) / 2);

i点坐标同理。略...
接下来我们连接dai三角形区域,得到以下图形,
image.png


同理通过路径联合我们就可以将AB区域进行分开,


Path mPath1 = Path();
mPath1.moveTo(p.value.d.x, p.value.d.y);
mPath1.lineTo(p.value.a.x, p.value.a.y);
mPath1.lineTo(p.value.i.x, p.value.i.y);
mPath1.close();
Path mPathB = Path.combine(PathOperation.intersect, mPathAB, mPath1);

得到以下图形,


image.png


到这里梳理一下,目前我们A、B、C三个path路径区域已经全部得到,剩下的就是填充书籍颜色,接下来我们将画笔设置为填充不同颜色,通过手势不断变化a点坐标看下效果。


Jul-26-2022 14-47-57.gif


是不是有点翻书的意思了,这里有一个问题,书籍的左下角也就是c点坐标在我们翻页的过程中会跑到页面之外,一般书籍都是左侧装订,这里我们希望达到一个真实的翻页效果就需要将c点的x轴最小值设置为书籍最左侧0


image.png

这里涉及到相似图形的数学知识,手指触摸点是在不断变化的,当cx轴达到临界值固定的时候,我们需要重新计算a点坐标,
见下图,

image.png

a是我们真实的手指触碰的坐标,a1则为我们需要计算出来的触碰坐标,从上图可知,△acb相似△a1b1c1,并且acfd区域相似a1c1d1f,那么通过相似原理我们可以得到fb1/fc1 = fb/fc;


从而得到,fb1= fb * fc1/fc;,


已知:

fb = f.x - a.x;

fc1 = size.width;

fc = f.x-c.x;


同理 fd1/fd = fb1/fb; 得到,fd1 = fb1 * fd/fb; 即可得到a1点坐标。


计算代码:


double fc = f.x - cx;
double fa = f.x - a.x;

double bb1 = size.width * fa / fc;

double fd1 = f.y - a.y;
double fd = bb1 * fd1 / fa;

a1 = Point(f.x - bb1, f.y - fd);

这时候我们再来看下效果,


Jul-26-2022 14-45-21.gif


c点坐标被我们设定最小值为书籍最左侧,所以左侧不会被翻出区域,看起来更像真实的翻页效果。


添加阴影


我们可以在灯光下找本书翻页看下阴影效果,差不多是这个样子,这里我将阴影分为三个部分,A区域两个和C区域一个。


image.png


我们先添加A左区域的阴影,A左区域的阴影可以认为是从ha方向由h向a进行色值渐变,所以这里我们需要得到A左阴影区域左上角坐标点,也就是ha直线向外延伸固定数值的坐标。


image.png

可以理解为数学题表达:


已知ha直线方程式和a点坐标, 以a为圆心,画半径为r(r>0)的圆,


image.png


求:此圆和ha直线的相交的坐标。


设交点为坐标xy,可得 x²+y² =r²; y = kx+b;(k、b 、r)已知,最终我们得到一个一元二次方程。会解出两个坐标点,这里我们只需要往外延伸的坐标点就行,具体可以跟a点坐标判断得出,之后我们令double m1 = a.x-p1.x;double n1 = a.y-p1.y;


image.png


那么阴影外部曲线就可以用下方代码表示。


pyy1.moveTo(p.value.c.x - m1, p.value.c.y);
pyy1.quadraticBezierTo(p.value.e.x - m1, p.value.e.y - n1,
p.value.b.x - m1, p.value.b.y - n1);
pyy1.lineTo(p.value.p.x, p.value.p.y);
pyy1.lineTo(p.value.k.x, p.value.k.y);
pyy1.lineTo(p.value.f.x, p.value.f.y);
pyy1.close();

绘制出来看下效果

image.png

同理路径联合下:


Path startYY =
Path.combine(PathOperation.reverseDifference, mPathA, pyy1);

得到:

image.png

接下来通过设置画笔属性由a点向p1点进行渐变。


..shader = ui.Gradient.linear(
Offset(p.value.a.x, p.value.a.y),
Offset(p.value.p.x, p.value.p.y),
[Colors.black26, Colors.transparent]

效果:

image.png


这里我设置了由 black26,向透明渐变。延伸长度为10的效果,这里可以根据半径和色值调整影深。


A右同理,略...


效果:

image.png


接下来我们绘制C区域的阴影,C区域可以看到他是跟eh是平行的,那么我们连接c、j、h、e点,


// 右下
Path pr = Path();
pr.moveTo(p.value.c.x, p.value.c.y);
pr.lineTo(p.value.j.x, p.value.j.y);
pr.lineTo(p.value.h.x, p.value.h.y);
pr.lineTo(p.value.e.x, p.value.e.y);
pr.close();

得到下面效果:

image.png


继续与AB区域进行路径联合,


Path p1 = Path.combine(PathOperation.intersect, pr, mPathAB);

得到下面效果:


image.png


继续与B区域再次联合,


Path p2 = Path.combine(PathOperation.difference, p1, mPathB);

最终得到我们想要的阴影区域。


image.png


接下来就是跟A区域操作一样了,设置线性渐变色和渐变方向,这里渐变方向的坐标点我们为u点和g点,g点已知,主要求u点坐标,u点坐标为afdi直线的相交点。


image.png


通过两条直线方程求相交点,得到u点以后,设置渐变色和渐变方向。


核心代码:


// 右下
Path pc = Path();
pc.moveTo(p.value.c.x, p.value.c.y);
pc.lineTo(p.value.j.x, p.value.j.y);
pc.lineTo(p.value.h.x, p.value.h.y);
pc.lineTo(p.value.e.x, p.value.e.y);
pc.close();

Path p1 = Path.combine(PathOperation.intersect, pc, mPathA);
Path p2 = Path.combine(PathOperation.difference, p1, mPathB);

Offset u = Offset(
PaperPoint.toTwoPoint(p.value.a, p.value.f, p.value.d, p.value.i)
.x,
PaperPoint.toTwoPoint(p.value.a, p.value.f, p.value.d, p.value.i)
.y);
canvas.drawPath(
p2,
paint
..style = PaintingStyle.fill
..shader = ui.Gradient.linear(
u, Offset(p.value.g.x,p.value.g.y), [Colors.black26, Colors
.transparent]));

最后得到我们最终的效果。


image.png


这里阴影部分可能有些瑕疵,尤其上方a点坐标的处理有点生硬,但是没找到好的方式。以后有时间再优化。


翻页动画、回弹动画


目的: 我们希望可以滑动过程中页码可以自动翻过去,并且误触的情况下不要翻页。


这里我简单的判断当翻过去书籍宽度的3/1就理解为用户想翻页,当手势松开时自动翻过去;

当翻过去书籍宽度小于1/3,理解为用户误触并不想翻页,当手势松开自动回弹回去。


这里判断还可以根据用户滑动的速度进行判断,比如按下和松开之间的时间很快并且有想左滑动的距离,我们就可以判定用户想要翻页,不过这里就需要不断的调试优化达到一个比较理想的交互。


初始化动画


回弹动画,我们希望松开手指时,a点坐标回到和f点重合,这里我们需要在点击或移动的过程中保存当前手指触摸的坐标a


var move = d.localPosition;
// 临界值书籍以外区域 取消更新
if (move.dx >= size.width ||
move.dx < 0 ||
move.dy >= size.height ||
move.dy < 0) {
return;
}
currentA = Point(move.dx, move.dy);
...
if ((size.width - move.dx) / size.width > 1 / 3) {
isNext = false;
} else {
isNext = true;
}

然后通过动画将a点坐标置位f点;


Point currentA = Point(0, 0);
late AnimationController _controller = AnimationController(
vsync: this, duration: Duration(milliseconds: 800))
..addListener(() {
if (isNext) {
/// 不翻页 回到原始位置
_p.value = PaperPoint(
Point(
currentA.x + (size.width - currentA.x) * _controller.value,
currentA.y + (size.height - currentA.y) * _controller.value,
),
size);
} else {
/// 翻页
_p.value = PaperPoint(
Point(currentA.x - (currentA.x + size.width) * _controller.value,
currentA.y + (size.height - currentA.y) * _controller.value),
size);
}
});

翻页,我们希望a点坐标和(-f.x,f.y)重合,也就是f.x为负值,相当也我们书籍彻底翻过去,


这里需要注意的是当a.x<0时,也就是书籍左侧外面区域,这里需要将我们之前设定c值的最小值放开,否则无法彻底翻过去。


只有a.x>0才限制cx坐标点
if (a.x > 0) {
if (cx <= 0) {
// // 临界点
double fc = f.x - cx;
double fa = f.x - a.x;
double bb1 = size.width * fa / fc;
double fd1 = f.y - a.y;
double fd = bb1 * fd1 / fa;
a = Point(f.x - bb1, f.y - fd);
g = Point((a.x + f.x) / 2, (a.y + f.y) / 2);
e = Point(g.x - (pow((f - g).y, 2) / (f - g).x), f.y);
cx = 0;
}
}

ok,有了这些数据以后,我们看下效果。


Jul-26-2022 14-57-29.gif


填充内容


最后一步,填充内容,模拟书籍嘛,当然不能是这些纯色翻页了,上面我们有了A B C三个路径的区域,接下来就需要对书籍内容Widget进行裁剪,这里我们需要路径裁剪类ClipPath类,


// 裁剪的路径区域 默认组件的矩形区域
final CustomClipper? clipper;

const ClipPath({
Key? key,
this.clipper,
this.clipBehavior = Clip.antiAlias,
Widget? child,
}) : assert(clipBehavior != null),
super(key: key, child: child);


可以看到构造里有三个参数,除了子组件,clipBehavior是裁剪方式,可以设置抗锯齿等,clipper则是我们的核心裁剪方法,需要实现CustomClipper类里的Path getClip(Size size);方法。

通过它返回一个Path路径,即可将child进行自定义裁剪。


ok, 有了方法,接下来我们开始实现,首先我们将之前A区域的Path路径拿出来,裁剪当前页,通过Stack帧布局加载当前页和下一页内容,下一页内容永远在第一页内容下面,当翻过去动画结束时将下方页置位当前页,刷新第二页数据。


翻页动画结束当前页index+1;


if (status == AnimationStatus.completed) {
if (!isNext) {
setState(() {
currentIndex++;
});
}
}

填充内容布局代码:


// 定义电子书数据
List dataList = [
"第一页数据",
"第二页数据",
"第三页数据",
];

GestureDetector(
child: Stack(
children: [
currentIndex == dataList.length - 1
? SizedBox()
// 下一页
: ClipPath(
child: Container(
alignment: Alignment.center,
color: Colors.blue,
width: size.width,
height: size.height,
child: Text(
dataList[currentIndex + 1],
style: TextStyle(fontSize: 20),
),
),
),
// // 当前页
ClipPath(
child: Container(
alignment: Alignment.center,
width: size.width,
height: size.height,
color: Colors.blue,
child: Text(
dataList[currentIndex],
style: TextStyle(fontSize: 20),
),
),
clipper: CurrentPaperClipPath(_p),
),

// 最上面只绘制B区域和阴影
CustomPaint(
size: size,
painter: _BookPainter(
_p,
),
),
],
),
onPanDown: (d) {
if (currentIndex == dataList.length - 1) {
ToastUtil.show("最后一页了");
return;
}
isNext = false;
var down = d.localPosition;
_p.value = PaperPoint(Point(down.dx, down.dy), size);
currentA = Point(down.dx, down.dy);
},
onPanUpdate: currentIndex == dataList.length - 1
? null
: (d) {
var move = d.localPosition;

// 临界值取消更新
if (move.dx >= size.width ||
move.dx < 0 ||
move.dy >= size.height ||
move.dy < 0) {
return;
}
currentA = Point(move.dx, move.dy);
_p.value = PaperPoint(Point(move.dx, move.dy), size);

if ((size.width - move.dx) / size.width > 1 / 3) {
isNext = false;
} else {
isNext = true;
}
},
onPanEnd: currentIndex == dataList.length - 1
? null
: (d) {
_controller.forward(
from: 0,
);
},
),



/// 当前页区域
class CurrentPaperClipPath extends CustomClipper {
ValueNotifier p;

CurrentPaperClipPath(
this.p,
) : super(reclip: p);

@override
Path getClip(Size size)
{
///书籍区域
Path mPath = Path();
mPath.addRect(Rect.fromCenter(
center: Offset(size.width / 2, size.height / 2),
width: size.width,
height: size.height));

Path mPathA = Path();
if (p.value.a != p.value.f && p.value.a.x > -size.width) {
print("当前页 ${p.value.a} ${p.value.f}");
mPathA.moveTo(p.value.c.x, p.value.c.y);
mPathA.quadraticBezierTo(
p.value.e.x, p.value.e.y, p.value.b.x, p.value.b.y);
mPathA.lineTo(p.value.a.x, p.value.a.y);
mPathA.lineTo(p.value.k.x, p.value.k.y);
mPathA.quadraticBezierTo(
p.value.h.x, p.value.h.y, p.value.j.x, p.value.j.y);
mPathA.lineTo(p.value.f.x, p.value.f.y);
mPathA.close();
Path mPathC =
Path.combine(PathOperation.reverseDifference, mPathA, mPath);
return mPathC;
}

return mPath;
}

@override
bool shouldReclip(covariant CurrentPaperClipPath oldClipper)
{
return p != oldClipper.p;
}
}

最终看下效果.


Jul-26-2022 15-05-31.gif


返回上一页


上面只有翻页,没有返回上一页,其实返回上一页也很简单,上面我们实现了回弹动画,这里只需要修改当前a点坐标为为书籍左侧外面,之后调用回弹动画,当前页面-1即可。非常简单。


ElevatedButton(
onPressed: () {
setState(() {
// 表示从页面左侧外面开始回弹
currentA = Point(-100, size.height - 100);
currentIndex--;
// 回弹动画
isNext = false;
});
// _p.value = PaperPoint(currentA, size);
_controller.forward(
from: 0,
);
},
child: Text("上一页"))

下面再看下最终效果:


Jul-26-2022 15-14-19.gif


这里示例只是简单的填充了一个Text文本,更多内容也是可以的,毕竟裁剪的是个Widget。


总结


翻页示例可以说是手势和绘制的典型结合,实现过程中也是踩了许多的坑,网上找了很多资料,并且实现原理上也用到了一些初中数学知识,总的来说,过程还是比较曲折的,本篇文章主要讲了我在实现的过程中的一个详细过程及思路,代码目前先不传了,毕竟现在还是有些小问题,后续有时间再优化吧,后续有时间也许会将他优化下,做成一个开源组件,ok,那本篇文章到这里就结束了,希望对你有所帮助~


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

Android | ViewModel源码分析

前言ViewMode 是我们日常开发中最常用的组件之一,也是实现 MVVM 模式不可缺少的一环,这篇文章将从使用到源码分析以及常见的一些知识点来分析一下 ViewModel了解 ViewModelViewModel 旨在注重生命周期的方式存储和管理界面的相关数...
继续阅读 »

前言

ViewMode 是我们日常开发中最常用的组件之一,也是实现 MVVM 模式不可缺少的一环,这篇文章将从使用到源码分析以及常见的一些知识点来分析一下 ViewModel

了解 ViewModel

ViewModel 旨在注重生命周期的方式存储和管理界面的相关数据,ViewModel 类可以再发生旋转等配置更改后继续留存。

一般 ViewModel 配合 LiveData / Flow 实现数据驱动,由于 Activity 存在因配置改变而重建的机制,就会造成页面的数据丢失,例如网络数据已经其他数据等,而 ViewModel 可以应对 Activity 应配置而改变的场景,再重建的过程中恢复数据,从而降低用户体验受损。

ViewModel 生命周期如下:

 ViewModel 随着 Activity 状态的改变而经历的生命周期。

上图说明了 Activity 经历屏幕旋转而后结束的各种生命周期状态,旁边显示的就是 ViewModel 的生命周期了。

ViewModel 的使用

class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers()
}
}

fun getUsers(): LiveData<List<User>> {
return users
}

private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
val model: MyViewModel by viewModels()
model.getUsers().observe(this, Observer<List<User>>{ users ->
// update UI
})

ViewModel 的创建方式

  • 方式1:通过 ViewModelProvider 创建

    ViewModelProvider(this).get(WorkViewModel::class.java)

    也可以使用带工厂的创建方式

    ViewModelProvider(this, WorkViewModelFactory()).get(WorkViewModel::class.java)

    class WorkViewModelFactory() : ViewModelProvider.Factory {

    private val repository = WorkRepository()

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
    return WorkViewModel(repository) as T
    }
    }
  • 方式2:使用 Kotlin by 委托属性,实际上也是使用了 ViewModelProvider

    private val viewModel by viewModels<UserViewModel>()
  • 方式3:使用 Hilt 进行注入

ViewModel 源码分析

Viewmodel 创建的方法最终都是通过 ViewModelProvider 来完成的,他可以理解为创建 ViewModel 的工具类,在创建的时候需要两个参数:

  • ViewModelStoreOwner

    对应着 Activity / Fragment 等持有 Viewmode 的宿主,他们内部通过 ViewModelStore 维持一个 ViewModel 的映射表,ViewModelStore 是实现 ViewModel 作用域和数据恢复的关键。

  • Factory

    对于于创建 ViewModel 的工厂,如果没有传采用默认的 NewInstanceFactory 工厂反射创建 VIewModel 的实例。

创建完 ViewModelProvider 工具类后,就可以调用 get 方法来创建 ViewModel 的实例。get 方法会先从映射表 ViewModelStore 中读取缓存,若没有命中,则通过 VIewModel 的工厂创建实例在缓存到映射表中。

ViewModelProvider

//使用默认的工厂创建 ViewModel
public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
this(owner.getViewModelStore(), ...NewInstanceFactory.getInstance());
}

//指定工厂
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
this(owner.getViewModelStore(), factory);
}
//记录宿主的 viewmodelStore 和 factory
public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
mFactory = factory;
mViewModelStore = store;
}
private static final String DEFAULT_KEY =
"androidx.lifecycle.ViewModelProvider.DefaultKey";

@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
String canonicalName = modelClass.getCanonicalName();
if (canonicalName == null) {
throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
}
//使用 Default_key + 类名作为缓存的 key
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}

/** 通常是 fragment 使用*/
@SuppressWarnings("unchecked")
@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
//先从 viewModelStore 中获取缓存
ViewModel viewModel = mViewModelStore.get(key);

if (modelClass.isInstance(viewModel)) {
if (mFactory instanceof OnRequeryFactory) {
((OnRequeryFactory) mFactory).onRequery(viewModel);
}
return (T) viewModel;
}
//使用 factory 创建 ViewModel
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
} else {
viewModel = mFactory.create(modelClass);
}
//存储到 viewModelStore 中
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}

NewInstanceFactory

public static class NewInstanceFactory implements Factory {
private static NewInstanceFactory sInstance;

@NonNull
static NewInstanceFactory getInstance() {
if (sInstance == null) {
sInstance = new NewInstanceFactory();
}
return sInstance;
}


@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//反射创建 ViewModel
try {
return modelClass.newInstance();
}....
}
}

by viewModels

ActivityViewModelLazy

@MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}

return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}

ViewModelLazy

public class ViewModelLazy<VM : ViewModel> (
private val viewModelClass: KClass<VM>,
private val storeProducer: () -> ViewModelStore,
private val factoryProducer: () -> ViewModelProvider.Factory
) : Lazy<VM> {
private var cached: VM? = null

override val value: VM
get() {
val viewModel = cached
//如果第一次调用 by viewModels,则先初始化再返回
return if (viewModel == null) {
val factory = factoryProducer()
val store = storeProducer()
//最终是通过 ViewModelProvider 来创建
ViewModelProvider(store, factory).get(viewModelClass.java).also {
cached = it
}
} else {
//否则直接返回
viewModel
}
}

override fun isInitialized(): Boolean = cached != null
}

ViewModelStoreOwner

ViewModel 的宿主是 ViewModelStoreOwner 接口的实现类,例如 ComponentActivity,Fragment 等

public interface ViewModelStoreOwner {
@NonNull
ViewModelStore getViewModelStore();
}

该接口的实现的责任就是在配置期间保留拥有的 ViewModelStore,并在销毁的时候

此接口实现的责任是在配置更改期间保留拥有的 ViewModelStore 并在此范围将被销毁的时候调用 ViewModelStore.clear()

ComponentActivity
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
ContextAware,
LifecycleOwner,
ViewModelStoreOwner.... {

static final class NonConfigurationInstances {
Object custom;
ViewModelStore viewModelStore;
}

//viewmodel 的存储容器
private ViewModelStore mViewModelStore;
//创建 viewmodel 的工厂
private ViewModelProvider.Factory mDefaultFactory;

@NonNull
@Override
public ViewModelStore getViewModelStore() {
//.....
ensureViewModelStore();
return mViewModelStore;
}

void ensureViewModelStore() {
if (mViewModelStore == null) {
//先从配置文件中获取,看能不能获取到
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
mViewModelStore = nc.viewModelStore;
}
//如果没有获取到则重新创建 ViewModelStore
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
}
//重建时保存 viewModelStore
// ViweModelStore 会被封装为 NonConfigurationInstances 类,然后保存在 NonConfigurationInstances 类的 Object activity 属性中。
//前一个 NonConfigurationInstances 是 ComponentActivity 中定义的,后一个是 Activity 类中定义的,不是同一个类,不要搞混了哟!
public final Object onRetainNonConfigurationInstance() {
Object custom = onRetainCustomNonConfigurationInstance();
ViewModelStore viewModelStore = mViewModelStore;
if (viewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
viewModelStore = nc.viewModelStore;
}
}
if (viewModelStore == null && custom == null) {
return null;
}

NonConfigurationInstances nci = new NonConfigurationInstances();
nci.custom = custom;
nci.viewModelStore = viewModelStore;
return nci;
}
}

上面代码中的 NonConfigurationInstances 是一个配置文件的实例,当 activity 重建时,最终会调用到 onRetainNonConfigurationInstance() 方法中对 viewModelStore 进行缓存。所以上面才是先尝试从配置文件中获取,最后再创建新的 ViewModelStore。

Fragment
@NonNull
@Override
public ViewModelStore getViewModelStore() {
return mFragmentManager.getViewModelStore(this);
}
//fragment 中 ViewModel 的映射
private final HashMap<String, ViewModelStore> mViewModelStores = new HashMap<>();

@NonNull
ViewModelStore getViewModelStore(@NonNull Fragment f) {
return mNonConfig.getViewModelStore(f);
}

@NonNull
ViewModelStore getViewModelStore(@NonNull Fragment f) {
ViewModelStore viewModelStore = mViewModelStores.get(f.mWho);
if (viewModelStore == null) {
viewModelStore = new ViewModelStore();
mViewModelStores.put(f.mWho, viewModelStore);
}
return viewModelStore;
}

关于 ViewModel 的一些问题

  1. ViewModel 如何实现不同的作用域

    在使用 ViewModelProvider 时,需要传入一个 ViewModelStoreOwner 接口,这个接口的 getViewModelStore 会返回对应的 ViewModelStore 实例。

    对于 Activity 来说,ViewModelStore 是直接保存在成员变量中的。

    对于 Fragment 来说, ViewModelstore 是间接的存储在 FragmentManagerViewModel 中的 map 中。

    这样就实现了不同的 activity 或者 fragment 分别对应不同的 ViewModelStore 实例,进而区分不同的作用域

  2. 为什么 Activity 可以再重建后恢复 viewMdoel

    当 Activity 因为配置而发生重建时,我们可以将页面上的数据分为两类:

    1. 配置数据,例如窗口大小,主题资源等,当配置发生改变后,需要重新读取这些配置,因此这些数据在配置改变后就失去了意义,也就没有存在的价值

    2. 非配置数据,这些数据就是一些用户自己的信息,以及页面上显示的数据,这些数据和配置没有关系,如果丢失掉就会造成比较大的用户体验。

    说以,Activity 再重建时支持恢复非配置的数据,整个过程如下:

    1. 重建时保存数据

      Activity 再重建时会调用 retainNonConfigurationInstances 方法,在里面会获取需要保存的数据,例如 fragment ,activity 等数据,最后打包为 NonConfigurationInstances 类,保存在 ActivityClientRecord 中。

      NonConfigurationInstances retainNonConfigurationInstances() {
      //activity 中的非配置数据,例如 viewmodelStore
      //该方法需要子类实现 ,例如 ComponentActivity
      Object activity = onRetainNonConfigurationInstance();
      //fragment 中的非配置数据
      FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();
      // .....
      //构建 NonConfigurationInstances
      NonConfigurationInstances nci = new NonConfigurationInstances();
      nci.activity = activity;
      nci.fragments = fragments;
      return nci;
      }
    2. 恢复数据

      Activity 重新启动的最后,会通过 ActivityThread 中的 performLaunchActivity() 来完成整个启动过程,再这个方法中会通过类加载器来创建 Activity 对象,并调用 attach 方法为其关联所需要的一些信息。

      我们需要关注的就是 attach 方法:

      final void attach(NonConfigurationInstances lastNonConfigurationInstances //.. ) {
      //.....
      mLastNonConfigurationInstances = lastNonConfigurationInstances;
      }

      最后,数据被保存在了 Activity . mLastNonConfigurationInstances 成员变量中。

    3. 获取数据

      这个我们之前已经分析过了,我们简单回顾一下

      void ensureViewModelStore() {
      if (mViewModelStore == null) {
      //获取之前保存的数据
      NonConfigurationInstances nc =
      (NonConfigurationInstances) getLastNonConfigurationInstance();
      if (nc != null) {
      // Restore the ViewModelStore from NonConfigurationInstances
      mViewModelStore = nc.viewModelStore;
      }
      if (mViewModelStore == null) {
      mViewModelStore = new ViewModelStore();
      }
      }
      }

      @Nullable
      public Object getLastNonConfigurationInstance() {
      //mLastNonConfigurationInstances 就是 attach 中保存的
      return mLastNonConfigurationInstances != null
      ? mLastNonConfigurationInstances.activity : null;
      }

    至此,就完成了 ViewModel 的数据恢复了。

  3. Activity 重建的过程

    再 Activity 重建时,系统会执行 Relaunch 重建过程。在这个过程中通过 ActivityClientRecord 来完成信息传递,并销毁 Activity,紧接着马上重建同一个 Activity。

    这些操作都是在 ActivityThread 中完成的:

    private void handleRelaunchActivityInner(ActivityClientRecord r //...) {
    final Intent customIntent = r.activity.mIntent;
    //处理 onPause
    performPauseActivity(r, false, reason, null /* pendingActions */);
    //处理 onStop
    callActivityOnStop(r, true /* saveState */, reason);
    //1
    handleDestroyActivity(r.token, false, configChanges, true, reason);
    //2
    handleLaunchActivity(r, pendingActions, customIntent);
    }
    //1
    public void handleDestroyActivity(IBinder token, boolean finishing,//...) {
    ActivityClientRecord r = performDestroyActivity(token, finishing,
    configChanges, getNonConfigInstance, reason);
    }
    ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
    int configChanges, boolean getNonConfigInstance, String reason) {
    ActivityClientRecord r = mActivities.get(token);
    if (r != null) {
    if (getNonConfigInstance) {
    //调用 activity 的 retainNonConfigurationInstances 方法
    r.lastNonConfigurationInstances
    = r.activity.retainNonConfigurationInstances();
    }
    }
    return r;
    }
    //2
    public Activity handleLaunchActivity(ActivityClientRecord r,
    PendingTransactionActions pendingActions, Intent customIntent) {
    final Activity a = performLaunchActivity(r, customIntent);
    return a;
    }
    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

    activity = mInstrumentation.newActivity(
    cl, component.getClassName(), r.intent);
    //...
    //传递缓存数据以及其他数据
    activity.attach(appContext, this, getInstrumentation(), r.token,
    r.ident, app, r.intent, r.activityInfo, title, r.parent,
    r.embeddedID, r.lastNonConfigurationInstances, config,
    r.referrer, r.voiceInteractor, window, r.configCallback,
    r.assistToken);
    //....
    return activity;
    }

    上面的代码主要可以分为两部分:

    第一处:再处理 onDestory 逻辑时,调用 retainNonConfigurationInstances() 方法获取非配置数据,并临时保存在 ActivityClientRecord 上。

    第二处:再 Launch 新 activity 的时候通过 attach 方法将数据传到新 activity 中即可

    至此旧的 Activity 数据已经被传递到新的 Activity 中了。

  4. ViewModel 的数据在什么时候才会清除

    ViewModel 的数据会在 Activity 非配置变化销毁时清除,具体分为三种情况

    1. 直接调用 finish 或者按返回键退出
    2. 异常退出 Activity,例如内存不足
    3. 强制退出应用

    前两种都属于非配置变更触发的,再 Activity中存在一个 Lifecycle 的监听,当 Activity 进入 Destory 状态时,如果 Activity 不处于配置重建阶段,将调用 viewModelStore.clear() 清除 viewmodel 数据。

    public ComponentActivity() {
    Lifecycle lifecycle = getLifecycle();
    getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
    @NonNull Lifecycle.Event event) {
    if (event == Lifecycle.Event.ON_DESTROY) {
    // Clear out the available context
    mContextAwareHelper.clearAvailableContext();
    //是否处于配置变更引起的重建
    if (!isChangingConfigurations()) {
    getViewModelStore().clear();
    }
    }
    }
    });
    }
  5. ViewModel 和 onSaveInstanceState 对比

    这两种都是对数据恢复的机制,但是他们针对的场景不同,导致他们的实现原理也不同,进而优缺点也不同

    viewModel:使用常见针对于配置变更中的非配置数据恢复,由于数据是直接存储在内存中的,所以他的读取速度非常快,并且支持存储大数据,但是会收到内存空间的限制

    onSaveInstanceState:针对于应用被系统回收后重建时的数据恢复,由于应用进程坑会在这个过程中消亡,所以不能存在内存中,只能进行持久化存储,并且这种方式的数据传递是通过 Bundle 传递的,会受到 Binder 事务缓冲区的大小限制,只能存储小规模数据。

    这里借用一张大佬的图,来看一下具体的优缺点:

    https://juejin.cn/post/7121998366103306254#heading-12

总结

到这里,ViewModel 就整个分析完了,如果有任何问题可直接留言评论,谢谢!


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

收起阅读 »

PermissionX 1.5发布,支持申请Android特殊权限啦

前言 Hello 大家早上好,说起 PermissionX,其实我已经有段时间没有更新这个框架了。一是因为现在工作确实比较忙,没有过去那么多的闲暇时间来写开源项目,二是因为,PermissionX 的主体功能已经相当稳定,并不需要频繁对其进行变更。 不过之前一...
继续阅读 »

前言


Hello 大家早上好,说起 PermissionX,其实我已经有段时间没有更新这个框架了。一是因为现在工作确实比较忙,没有过去那么多的闲暇时间来写开源项目,二是因为,PermissionX 的主体功能已经相当稳定,并不需要频繁对其进行变更。


不过之前一直有朋友在反映,对于 Android 中的一些特殊权限申请,PermissionX 并不支持。是的,PermissionX 本质上只是对 Android 运行时权限 API 进行了一层封装,用于简化运行时权限申请的。而这些特殊权限并不属于 Android 运行时权限的一部分,所以 PermissionX 自然也是不支持的。


但是特殊权限却是我们这些开发者们可能经常要与之打交道的一部分,它们并不难写,但是每次去写都感觉很繁琐。因此经慎重考虑之后,我决定将几个比较常用的特殊权限纳入 PermissionX 的支持范围。那么本篇文章我们就来看一看,对于这几个常见的特殊权限,使用 PermissionX 和不使用 PermissionX 的写法有什么不同之处。


事实上,Android 的权限机制也是经历过长久的迭代的。在 6.0 系统之前,Google 将权限机制设计的比较简单,你的应用程序需要用到什么权限,只需要在 AndroidManifest.xml 文件中声明一下就可以了。


但是从 6.0 系统开始,Android 引入了运行时权限机制。Android 将常用的权限大致归成了几类,一类是普通权限,一类是危险权限,一类是特殊权限。


普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,这种权限和过去一样,只需要在 AndroidManifest.xml 文件中声明一下就可以了,不需要做任何特殊处理。


危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等。这部分权限需要通过代码进行申请,并要用户手动同意才可获得授权。PermissionX 库主要就是处理的这种权限的申请。


而特殊权限则更加少见,Google 认为这种权限比危险权限还要敏感,因此不能仅仅让用户手动同意就可以获得授权,而是需要让用户到专门的设置页面去手动对某一个应用程序授权,该程序才能使用这个权限。


不过相比于危险权限,特殊权限没有非常固定的申请方式,每个特殊权限可能都要使用不同的写法才行,这也导致申请特殊权限比申请危险权限还要繁琐。


从 1.5.0 版本开始,PermissionX 对最常用的几个特殊权限进行了支持。正如刚才所说,特殊权限没有固定的申请方式,因此 PermissionX 也是针对于这几个特殊权限一个一个去适配并支持的。如果你发现你需要申请的某个特殊权限还没有被 PermissionX 支持,也可以向我提出需求,我会考虑在接下来的版本中加入。


在过去,我们发布开源库通常都是发布到 jcenter 上的,但是相信大家现在都已经知道了,jcenter 即将停止服务,具体可以参考我的这篇文章 浅谈 JCenter 即将被停止服务的事件


目前的 jcenter 处在一个半废弃的边缘,虽然还可以正常从 jcenter 下载开源库,但是已经不能再向 jcenter 发布新的开源库了。而在明年 2 月 1 号之后,下载服务也会被关停。


所以,以后要想再发布开源库我们只能选择发布到其他仓库,比如现在 Google 推荐我们使用 Maven Central。


于是,从 1.5.0 版本开始,PermissionX 也会将库发布到 Maven Center 上,之前的老版本由于迁移价值并不大,所以我也不想再耗费经历做迁移了。1.5.0 之前的版本仍然保留在 jcenter 上,提供下载服务直到明年的 2 月 1 号。


而关于如何将库发布到 Maven Central,请参考 再见 JCenter,将你的开源库发布到 MavenCentral 上吧


Android的特殊权限


Android 里具体有哪些特殊权限呢?


说实话,这个我也不太清楚。我所了解的特殊权限基本都是因为需要用到了,然后发现这个权限即不属于普通权限,也不属于危险权限,要用一种更加特殊的方式去申请,才知道原来这是一个特殊权限。


因此,PermissionX 1.5.0 版本中对特殊权限的支持,也就仅限于我知道的,以及从网友反馈得来的几个最为常用的特殊权限。


一共是以下 3 个:



  1. 悬浮窗

  2. 修改设置

  3. 管理外部存储


接下来我就分别针对这 3 个特殊权限做一下更加详细的介绍。


悬浮窗


悬浮窗功能在不少应用程序中使用得非常频繁,因为你可能总有一些内容是要置顶于其他内容之上显示的,这个时候用悬浮窗来实现就会非常方便。


当然,如果你只是在自己的应用内部实现悬浮窗功能是不需要申请权限的,但如果你的悬浮窗希望也能置顶于其他应用程序的上方,这就必须得要申请权限了。


悬浮窗的权限名叫做 SYSTEM_ALERT_WINDOW,如果你去查一下这个权限的文档,会发现这个权限的申请方式比较特殊:



按照文档上的说法,从 Android 6.0 系统开始,我们在使用 SYSTEM_ALERT_WINDOW 权限前需要发出一个 action 为 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 的 Intent,引导用户手动授权。另外我们还可以通过 Settings.canDrawOverlays() 这个 API 来判断用户是否已经授权。


因此,想要申请悬浮窗权限,自然而然就可以写出以下代码:


if (Build.VERSION.SDK_INT >= 23) {
if (Settings.canDrawOverlays(context)) {
showFloatView()
} else {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
startActivity(intent)
}
} else {
showFloatView()
}

看上去也不复杂嘛。


确实,但是它麻烦的点主要在于,它的请求方式是脱离于一般运行时权限的请求方式的,因此得要为它额外编写独立的权限请求逻辑才行。


而 PermissionX 的目标就是要弱化这种独立的权限请求逻辑,减少差异化代码编写,争取使用同一套 API 来实现对特殊权限的请求。


如果你已经比较熟悉 PermissionX 的用法了,那么以下代码你一定不会陌生:


PermissionX.init(activity)
.permissions(Manifest.permission.SYSTEM_ALERT_WINDOW)
.onExplainRequestReason { scope, deniedList ->
val message = "PermissionX需要您同意以下权限才能正常使用"
scope.showRequestReasonDialog(deniedList, message, "Allow", "Deny")
}
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
Toast.makeText(activity, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(activity, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
}
}

可以看到,这就是最标准的 PermissionX 的正常用法,但是我们在这里却用来请求了悬浮窗权限。也就是说,即使是特殊权限,在 PermissionX 中也可以用普通的方式去处理。


另外不要忘记,所有申请的权限都必须在 AndroidManifest.xml 进行注册才行:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.permissionx.app">

<uses-permission android: />

</manifest>

那么运行效果是什么样的呢?我们来看看吧:



可以看到,PermissionX 还自带了一个权限提示框,友好地告知用户我们需要悬浮窗权限,引导用户去手动开启。


修改设置


了解了悬浮窗权限的请求方式之后,接下来我们就可以快速过一下修改设置权限的请求方式了,因为它们的用法是完全一样的。


修改设置的权限名叫 WRITE_SETTINGS,如果我们去查看一下它的文档,你会发现它和刚才悬浮窗权限的文档简直如出一辙:



同样是从 Android 6.0 系统开始,在使用 WRITE_SETTINGS 权限前需要先发出一个 action 为 Settings.ACTION_MANAGE_WRITE_SETTINGS 的 Intent,引导用户手动授权。然后我们还可以通过 Settings.System.canWrite() 这个 API 来判断用户是否已经授权。


所以,如果是自己手动申请这个权限,相信你已经知道要怎么写了。


那么用 PermissionX 申请的话应该要怎么写呢?这个当然就更简单了,只需要把要申请的权限替换一下即可,其他部分都不用作修改:


PermissionX.init(activity)
.permissions(Manifest.permission.WRITE_SETTINGS)
...

当然,不要忘记在 AndroidManifest.xml 中注册权限:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.permissionx.app">

<uses-permission android: />

</manifest>

运行一下,效果如下图所示:



管理外部存储


管理外部存储权限也是一种特殊权限,它可以允许你的 App 拥有对整个 SD 卡进行读写的权限。


有些朋友可能会问,SD 卡本来不就是可以全局读写的吗?为什么还要再申请这个权限?


那你一定是没有了解 Android 11 上的 Scoped Storage 功能。从 Android 11 开始,Android 系统强制启用了 Scoped Storage,所有 App 都不再拥有对 SD 卡进行全局读写的权限了。


关于 Scoped Storage 的更多内容,可以参考我的这篇文章 Android 11 新特性,Scoped Storage 又有了新花样


但是如果有的应用就是要对 SD 卡进行全局读写该怎么办呢(比如说文件浏览器)?


不用担心,Google 仍然还是给了我们一种解决方案,那就是请求管理外部存储权限。


这个权限是 Android 11 中新增的,为的就是应对这种特殊场景。


那么这个权限要怎么申请呢?我们还是先来看一看文档:



大致可以分为几步吧:


第一,在 AndroidManifest.xml 中声明 MANAGE_EXTERNAL_STORAGE 权限。


第二,发出一个 action 为 Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 的 Intent,引导用户手动授权。


第三,调用 Environment.isExternalStorageManager() 来判断用户是否已授权。


传统请求权限的写法我就不再演示了,使用 PermissionX 来请求的写法仍然也还是差不多的。只不过要注意,因为 MANAGE_EXTERNAL_STORAGE 权限是 Android 11 系统新加入的,所以我们也只应该在 Android 11 以上系统去请求这个权限,代码如下所示:


if (Build.VERSION.SDK_INT >= 30) {
PermissionX.init(this)
.permissions(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
...
}

AndroidManifest.xml 中的权限如下:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.permissionx.app">

<uses-permission android: />

</manifest>

运行一下程序,效果如下图所示:



这样我们就拥有全局读写 SD 卡的权限了。


另外 PermissionX 还有一个特别方便的地方,就是它可以一次性申请多个权限。假如我们想要同时申请悬浮窗权限和修改设置权限,只需要这样写就可以了:


PermissionX.init(activity)
.permissions(Manifest.permission.SYSTEM_ALERT_WINDOW, Manifest.permission.WRITE_SETTINGS)
...

运行效果如下图所示:



当然你也可以将特殊权限与普通运行时权限放在一起申请,PermissionX 对此也是支持的。只有当所有权限都请求结束时,PermissionX 才会将所有权限的请求结果一次性回调给开发者。


关于 PermissionX 新版本的内容变化就介绍到这里,升级的方式非常简单,修改一下 dependencies 当中的版本号即可:


repositories {
google()
mavenCentral()
}


dependencies {
implementation 'com.guolindev.permissionx:permissionx:1.5.0'
}

注意现在一定要使用 mavenCentral 仓库,而不能再使用 jcenter 了。


如果你对 PermissionX 的源码感兴趣,可以访问 PermissionX 的项目主页:


github.com/guolindev/P…


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

IDEA 版防沉迷插件,有点意思!

分享一个 IDEA 编码防沉迷插件,特别适合沉迷编码无法自拔的朋友使用。这个插件诞生已经有一年多了,目前在 IDEA 官方有 10.3k 的下载量,算是一个不错的成绩了。 前言当初年少懵懂,那年夏天填志愿选专业,父母听其他长辈说选择计算机专业好。从那以后,我的...
继续阅读 »

分享一个 IDEA 编码防沉迷插件,特别适合沉迷编码无法自拔的朋友使用。这个插件诞生已经有一年多了,目前在 IDEA 官方有 10.3k 的下载量,算是一个不错的成绩了。


前言

当初年少懵懂,那年夏天填志愿选专业,父母听其他长辈说选择计算机专业好。从那以后,我的身上就有了计院深深的烙印。从寝室到机房,从机房到图书馆,C、C++、Java、只要是想写点自己感兴趣的东西,一坐就是几个小时,但那时年轻,起身,收拾,一路小跑会女神,轻轻松松。现在工作了,毫无意外的做着开发的工作,长时间久坐。写代码一忙起来就忘了起来活动一下,也不怎么喝水。经常等到忙完了就感觉腰和腿不舒服。直到今年的体检报告一下来,才幡然醒悟:没有一个好身体,就不能好好打工,让老板过上他自己想要的生活了。


试过用手机提醒自己,但是没用。小米手环的久坐提醒功能也开着,有时候写代码正入神的,时间到了也就点一下就关了,还是没什么作用。所以我想究竟是我太赖了,还是用 IDEA 写代码容易沉迷,总之不可能是改需求有意思。所以元旦节打算为自己开发一款小小的 IDEA 防沉迷插件,我叫她【StopCoding】。她应该可以设置每隔多少分钟,就弹出一个提醒对话框,一旦对话框弹出来,IDEA 的代码编辑框就自动失去了焦点,什么都不能操作,到这还不算完,关键是这个对话框得关不了,并且还显示着休息倒计时,还有即使我修改了系统时间,这个倒计时也依然有效,除非我打开任务管理器,关闭 IDEA 的进程,然后再重新启动 IDEA。但是想一下想,IDEA 都都关了,还是休息一下吧。

下面就介绍一下她简单的使用教程和开发教程。

安装使用教程

安装

  1. 在 IDEA 中直接搜索安装 StopCoding 插件(官方已经审核通过)

2. 内网开发的小伙伴 可以下载之后进行本地安装 下载地址

  • 本地安装:

使用

  • Step1. 然后在菜单栏中 tools->StopCoding

  • Step2. 设置适合你的参数然后保存。


  • Step3. 然后快乐的 Coding 吧,再不用担心自己会沉迷了。工作时间结束,她会弹出下框进行提醒,当然,


这个框是关不掉的.只有你休息了足够的时间它才会自动关闭。


开发教程

这个插件非常的简约,界面操作也很简单。所使用的技术基本上都是 Java 的基础编程知识。所以小伙伴感兴趣的话,一起看看吧。

技术范围

  • 插件工程的基本结构

  • Swing 主要负责两个对话框的交互

  • Timer 作为最基本的定时器选择

插件工程结构


  • plugin.xml

这是插件工程的核心配置文件,里面每一项的解释,可以参考第一篇的介绍核心配置文件说明。

  • data

    • SettingData :配置信息对应 model

    • DataCenter :作为运行时的数据中心,都是些静态的全局变量

  • service

    • TimerService :这个定时计算的核心代码

  • task

    • RestTask :休息时的定时任务

    • WorkTask :工作时的定时任务

  • ui

    • SettingDialog :设置信息的对话框

    • TipsDialog : 休息时提醒的对话框

  • StopCodingSettingAction :启动入口的 action

Swing

其实在 IDEA 中开发 Swing 项目的界面非常简单。因为 IDEA 提供了一系列可视化的操作,以及控件布局的拖拽。接下来就简单的介绍一下对话框的创建过程和添加事件。

创建对话框

  • Step1


  • Step2


  • Step3


  • 注:这里并没有详细的展开 Swing 的讲解,因为界面的这个东西,需要大家多去自己实践。这里就不做手册式的赘述了。

添加事件

其实,刚才创建的这个对话框里的两个按钮都是默认已经创建好了点击事件的。

public class TestDialog extends JDialog {
  private JPanel contentPane;
  private JButton buttonOK;
  private JButton buttonCancel;

  public TestDialog() {
      setContentPane(contentPane);
      setModal(true);
      getRootPane().setDefaultButton(buttonOK);

      buttonOK.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
              onOK();
          }
      }); //这是给OK按钮绑定点击事件的监听器

      buttonCancel.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
              onCancel();
          }
      });//这是给取消按钮绑定点击事件的监听器
  //其他代码
  }

当然我们也可以其它任何控件去创建不同的事件监听器。这里可以通过界面操作创建很多种监听器,只要你需要,就可以使用。

  • step1


  • step2


Timer 定时器

在这个插件里面,需要用到定时的功能,同时去计算公国和休息的时间。所以使用 JDK 自带的 Timer,非常的方便。下面我 Timer 的常用的 api 放在这里,就清楚它的使用了。

  • 构造方法

img

  • 成员防范

img

  • 主要是 schedule 去添加一个定时任务,和使用 cancel 去取消任务停止定时器。

最后

相信有了这些基本介绍,感谢兴趣的小伙伴想去看看源码和尝试自己写一个小插件就没什么大问题了。不说了,我得休息了。希望这个插件能帮到作为程序员得你,和这篇文章对你有一点点启发。当然麻烦小伙伴点个赞,鼓励一下打工人。

源码地址:https://github.com/jogeen/StopCoding


来源:https://sourl.cn/z8UiUv

收起阅读 »

请不要再下载这些vscode插件了

vscode好多插件都已经内置了,但是还是有很多憨批不知道,还在傻傻的推荐这些插件来坑萌新。 Auto Rename Tag 这个插件是在写html标签的时候可以重命名标签名的,但是现在vscode已经内置了,就不需要再下载这个插件了。只不过默认是关闭的...
继续阅读 »

vscode好多插件都已经内置了,但是还是有很多憨批不知道,还在傻傻的推荐这些插件来坑萌新。



  1. Auto Rename Tag


image.png


这个插件是在写html标签的时候可以重命名标签名的,但是现在vscode已经内置了,就不需要再下载这个插件了。只不过默认是关闭的,需要开启。


点击设置,搜索link,把这个勾选上,就可以左右重命名标签了。


在html和vue中可以自动重命名,而jsx中不行,如果有react开发的,那还是继续装上把。


image.png



  1. Auto Close Tag


image.png
这个插件是用来自动闭合html标签的,但是目前vscode已经内置了这个自动闭合标签的功能了,就不需要再下载了,默认是开启的。



  1. Bracket Pair Colorizer


image.png


这个标签是用来显示多个彩色括号的,但是目前vscode也内置了,所以也不用再下载了,默认是开启的。


如果没有开启,点击设置,搜索Bracket Pair,并勾选上。


image.png



  1. Guides


image.png


这个插件是用来显示代码层级的,但是vscode也已经内置了,默认是关闭的,在上面的配置中,把是否启用括号对指南改成true即可。



  1. CSS Peek


image.png
这个插件只是用于查找html的外部css样式,对于vue、react等文件是不起作用的,并且目前处于失效中。


6.HTML Snippets


image.png
该插件目前已不再维护。


未提到的,欢迎大家补充。



链接:https://juejin.cn/post/7110626790560759845

收起阅读 »

SDK无侵入初始化并获取Application

1.SDK无侵入初始化并获取Application 无侵入初始化SDK并获取Application的意思是不需要业务方手动调用SDK的初始化函数。 这个就得利用Android四大基本组件之一ContentProvider了,其执行的时机是位于Applicati...
继续阅读 »

1.SDK无侵入初始化并获取Application


无侵入初始化SDK并获取Application的意思是不需要业务方手动调用SDK的初始化函数。


这个就得利用Android四大基本组件之一ContentProvider了,其执行的时机是位于ApplicationattchBaseContext()之后,ApplicationonCreate()之前,无需程序手动调用。


所以我们就可以自定义个ContentProvider完成SDK的自动初始化并获取应用的Application。


class CPDemo : ContentProvider() {
override fun attachInfo(context: Context?, info: ProviderInfo?) {
super.attachInfo(context, info)
//编写SDK初始化逻辑,并获取Application
val application = context?.applicationContext
}

override fun onCreate(): Boolean = true
}

直接重写ContentProvider并在attachInfo执行SDK的初始化逻辑即可。


比较出名的内存泄漏检测库 LeakCanary、Google官方的ProcessLifecycleOwner就使用这个原理。


不过如果每个第三方库都借用ContentProvider来完成无侵入式的初始化,势必造成自定义的ContentProvider过多,直接增加了启动耗时:


image.png


为了避免ContentProvider过多的问题,Google官方提供了App Startup库,这个库主要是给SDK提供方实现无侵入初始化使用的:


    implementation("androidx.startup:startup-runtime:1.1.1")

该官方库会将所有用于初始化的ContentProvider合并成一个,减少启动的耗时


基本使用如下:


class CPDemo2 : Initializer<Unit> {
override fun create(context: Context) {
//执行初始化逻辑
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> = mutableListOf()
}

然后再AndroidManifest中注册:


<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.gitlinux.CPDemo2"
android:value="androidx.startup" />
</provider>

更多用法可以参考郭神的文章:Jetpack新成员,App Startup一篇就懂


2.kotlin函数省略返回值类型真的好吗?


经常使用kotlin的程序都知道,kotlin的函数再某些场景下是可以不用显示声明返回值类型,这是为了提高开发效率,比如:


fun test() = ""

对于简单的函数来说,虽然省略了方法的返回类型,但是我们还是能够直接看出这个方法的返回值类型为String,但是方法中调用了其他方法呢,比如:


fun test() =  request()

//随便一个函数,这个函数体中还会调用其他的函数
fun request() = otherFun()

fun otherFun() = "hahaha"

这种情况下,如果我们要知道test()方法的返回值类型必须先通过request()函数再跳转到otherFun才能知道test()方法的返回值类型,这对于程序而言反而降低了开发效率。


所以我认为使用kotlin函数省略返回值类型的场景应该有一个前提:该函数的返回值类型程序能够很容易推断出来(尽量不依赖其他函数)


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