注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

【辨析】Compose 完全脱离 View 系统了吗?

前言 Compose正式发布1.0已经相当一段时间了,但相信很多同学对Compose还是有很多迷惑的地方 Compose跟原生的View到底是什么关系?是跟Flutter一样完全基于Skia引擎渲染,还是说还是View的那老一套? 相信很多同学都会有下面的疑问...
继续阅读 »

前言


Compose正式发布1.0已经相当一段时间了,但相信很多同学对Compose还是有很多迷惑的地方

Compose跟原生的View到底是什么关系?是跟Flutter一样完全基于Skia引擎渲染,还是说还是View的那老一套?

相信很多同学都会有下面的疑问




下面我们就一起来看下下面这个问题


现象分析


我们先看这样一个简单布局


class TestActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setContent {
ComposeBody()
}
}
}

@Composable
fun ComposeBody() {
Column {
Text(text = "这是一行测试数据", color = Color.Black, style = MaterialTheme.typography.h6)
Row() {
Text(text = "测试数据1!", color = Color.Black, style = MaterialTheme.typography.h6)
Text(text = "测试数据2!", color = Color.Black, style = MaterialTheme.typography.h6)
}
}
}

如上所示,就是一个简单的布局,包含Column,RowText

然后我们打开开发者选项中的显示布局边界,效果如下图所示:




我们可以看到Compose的组件显示了布局边界,我们知道,FlutterWebView H5内的组件都是不会显示布局边界的,难道Compose的布局渲染其实还是View的那一套?


我们下面再在onResume时尝试遍历一下View的层级,看一下Compose到底会不会转化成View


    override fun onResume() {
super.onResume()
window.decorView.postDelayed({
(window.decorView as? ViewGroup)?.let { transverse(it, 1) }
}, 2000)
}

private fun transverse(view: View, index: Int) {
Log.e("debug", "第${index}层:" + view)
if (view is ViewGroup) {
view.children.forEach { transverse(it, index + 1) }
}
}

通过以上方式打印页面的层级,输出结果如下:


E/debug: 第1层:DecorView@c2f703f[RallyActivity]
E/debug: 第2层:android.widget.LinearLayout{4202d0c V.E...... ........ 0,0-1080,2340}
E/debug: 第3层:android.view.ViewStub{2b50655 G.E...... ......I. 0,0-0,0 #10201b1 android:id/action_mode_bar_stub}
E/debug: 第3层:android.widget.FrameLayout{9bfc86a V.E...... ........ 0,90-1080,2340 #1020002 android:id/content}
E/debug: 第4层:androidx.compose.ui.platform.ComposeView{1b4d15b V.E...... ........ 0,0-1080,2250}
E/debug: 第5层:androidx.compose.ui.platform.AndroidComposeView{a8ec543 VFED..... ........ 0,0-1080,2250}

如上所示,我们写的Column,Row,Text并没有出现在布局层级中,跟Compose相关的只有ComposeViewAndroidComposeView两个View

ComposeViewAndroidComposeView都是在setContent时添加进去的Compose的容器,我们后面再分析,这里先给出结论



Compose在渲染时并不会转化成View,而是只有一个入口View,即AndroidComposeView

我们声明的Compose布局在渲染时会转化成NodeTree,AndroidComposeView中会触发NodeTree的布局与绘制

总得来说,Compose会有一个View的入口,但它的布局与渲染还是在LayoutNode上完成的,基本脱离了View



总得来说,纯Compose页面的页面层级如下图所示:



原理分析


前置知识


我们知道,在View系统中会有一棵ViewTree,通过一个树的数据结构来描述整个UI界面

Compose中,我们写的代码在渲染时也会构建成一个NodeTree,每一个组件就是一个ComposeNode,作为NodeTree上的一个节点


ComposeNodeTree 管理涉及 ApplierCompositionComposeNode

Composition 作为起点,发起首次的 composition,通过 Compose 的执行,填充 Slot Table,并基于 Table 创建 NodeTree。渲染引擎基于 Compose Nodes 渲染 UI, 每当 recomposition 发生时,都会通过 ApplierNodeTree 进行更新。 因此



Compose 的执行过程就是创建 Node 并构建 NodeTree 的过程。





为了了解NodeTree的构建过程,我们来介绍下面几个概念


Applier:增删 NodeTree 的节点


简单来说,Applier的作用就是增删NodeTree的节点,每个NodeTree的运算都需要配套一个Applier

同时,Applier 会提供回调,基于回调我们可以对 NodeTree 进行自定义修改:


interface Applier<N> {

val current: N // 当前处理的节点

fun onBeginChanges() {}

fun onEndChanges() {}

fun down(node: N)

fun up()

fun insertTopDown(index: Int, instance: N) // 添加节点(自顶向下)

fun insertBottomUp(index: Int, instance: N)// 添加节点(自底向上)

fun remove(index: Int, count: Int) //删除节点

fun move(from: Int, to: Int, count: Int) // 移动节点

fun clear()
}

如上所示,节点增删时会回调到Applier中,我们可以在回调的方法中自定义节点添加或删除时的逻辑,后面我们可以一起看下在Android平台Compose是怎样处理的


Composition: Compose执行的起点


CompositionCompose执行的起点,我们来看下如何创建一个Composition


val composition = Composition(
applier = NodeApplier(node = Node()),
parent = Recomposer(Dispatchers.Main)
)

composition.setContent {
// Composable function calls
}

如上所示



  1. Composition中需要传入两个参数,ApplierRecomposer

  2. Applier上面已经介绍过了,Recomposer非常重要,他负责Compose的重组,当重组后,Recomposer 通过调用 Applier 完成 NodeTree 的变更

  3. Composition#setContent 为后续 Compose 的调用提供了容器


通过上面的介绍,我们了解了NodeTree构建的基本流程,下面我们一起来分析下setContent的源码


setContent过程分析


setContent入口


setContent的源码其实比较简单,我们一起来看下:


public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
//判断ComposeView是否存在,如果存在则不创建
if (existingComposeView != null) with(existingComposeView) {
setContent(content)
} else ComposeView(this).apply {
//将Compose content添加到ComposeView上
setContent(content)
// 将ComposeView添加到DecorView上
setContentView(this, DefaultActivityContentLayoutParams)
}
}

上面就是setContent的入口,主要作用就是创建了一个ComposeView并添加到DecorView


Composition的创建


下面我们来看下AndroidComposeViewComposition是怎样创建的

通过ComposeView#setContent->AbstractComposeView#createComposition->AbstractComposeView#ensureCompositionCreated->ViewGroup#setContent

最后会调用到doSetContent方法,这里就是Compose的入口:Composition创建的地方


private fun doSetContent(
owner: AndroidComposeView, //AndroidComposeView是owner
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
//..
//创建Composition,并传入Applier与Recomposer
val original = Composition(UiApplier(owner.root), parent)
val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
as? WrappedComposition
?: WrappedComposition(owner, original).also {
owner.view.setTag(R.id.wrapped_composition_tag, it)
}
//将Compose内容添加到Composition中
wrapped.setContent(content)
return wrapped
}

如上所示,主要就是创建一个Composition并传入UIApplierRecomposer,并将Compose content传入Composition


UiApplier的实现


上面已经创建了Composition并传入了UIApplier,后续添加了Node都会回调到UIApplier


internal class UiApplier(
root: LayoutNode
) : AbstractApplier<LayoutNode>(root) {
//...

override fun insertBottomUp(index: Int, instance: LayoutNode) {
current.insertAt(index, instance)
}

//...
}

如上所示,在插入节点时,会调用current.insertAt方法,那么这个current到底是什么呢?


private fun doSetContent(
owner: AndroidComposeView, //AndroidComposeView是owner
): Composition {
//UiApplier传入的参数即为AndroidComposeView.root
val original = Composition(UiApplier(owner.root), parent)
}

abstract class AbstractApplier<T>(val root: T) : Applier<T> {
private val stack = mutableListOf<T>()
override var current: T = root
}
}

可以看出,UiApplier中传入的参数其实就是AndroidComposeViewroot,即current就是AndroidComposeViewroot


    # AndroidComposeView
override val root = LayoutNode().also {
it.measurePolicy = RootMeasurePolicy
//...
}

如上所示,root其实就是一个LayoutNode,通过上面我们知道,所有的节点都会通过Applier插入到root


布局与绘制入口


上面我们已经在AndroidComposeView中拿到NodeTree的根结点了,那Compose的布局与测量到底是怎么触发的呢?


    # AndroidComposeView
override fun dispatchDraw(canvas: android.graphics.Canvas) {
//Compose测量与布局入口
measureAndLayout()

//Compose绘制入口
canvasHolder.drawInto(canvas) { root.draw(this) }
//...
}

override fun measureAndLayout() {
val rootNodeResized = measureAndLayoutDelegate.measureAndLayout()
measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
}


如上所示,AndroidComposeView会通过root,向下遍历它的子节点进行测量布局与绘制,这里就是LayoutNode绘制的入口


小结



  1. Compose在构建NodeTree的过程中主要通过Composition,Applier,Recomposer构建,Applier会将所有节点添加到AndroidComposeView中的root节点下

  2. setContent的过程中,会创建ComposeViewAndroidComposeView,其中AndroidComposeViewCompose的入口

  3. AndroidComposeViewdispatchDraw中会通过root向下遍历子节点进行测量布局与绘制,这里是LayoutNode绘制的入口

  4. Android平台上,Compose的布局与绘制已基本脱离View体系,但仍然依赖于Canvas


Compose与跨平台


上面说到,Compose的绘制仍然依赖于Canvas,但既然这样,Compose是怎么做到跨平台的呢?

这主要是通过良好的分层设计


Compose 在代码上自下而上依次分为6层:



其中compose.runtimecompose.compiler最为核心,它们是支撑声明式UI的基础。


而我们上面分析的AndroidComposeView这一部分,属于compose.ui部分,它主要负责Android设备相关的基础UI能力,例如 layoutmeasuredrawinginput

但这一部分是可以被替换的,compose.runtime 提供了 NodeTree 管理等基础能力,此部分与平台无关,在此基础上各平台只需实现UI的渲染就是一套完整的声明式UI框架


基于compose.runtime可以实现任意一套声明式UI框架,关于compose.runtime的详细介绍可参考fundroid大佬写的:Jetpack Compose Runtime : 声明式 UI 的基础


Button的特殊情况


上面我们介绍了在纯Compose项目下,AndroidComposeView不会有子View,而是遍历LayoutnNode来布局测量绘制

但如果我们在代码中加入一个Button,结果可能就不太一样了


@Composable
fun ComposeBody() {
Column {
Text(text = "这是一行测试数据", color = Color.Black, style = MaterialTheme.typography.h6)
Row() {
Text(text = "测试数据1!", color = Color.Black, style = MaterialTheme.typography.h6)
Text(text = "测试数据2!", color = Color.Black, style = MaterialTheme.typography.h6)
}

Button(onClick = {}) {
Text(text = "这是一个Button",color = Color.White)
}
}
}

然后我们再看看页面的层级结构


E/debug: 第1层:DecorView@182e858[RallyActivity]
E/debug: 第2层:android.widget.LinearLayout{397edb1 V.E...... ........ 0,0-1080,2340}
E/debug: 第3层:android.widget.FrameLayout{e2b0e17 V.E...... ........ 0,90-1080,2340 #1020002 android:id/content}
E/debug: 第4层:androidx.compose.ui.platform.ComposeView{36a3204 V.E...... ........ 0,0-1080,2250}
E/debug: 第5层:androidx.compose.ui.platform.AndroidComposeView{a8ec543 VFED..... ........ 0,0-1080,2250}
E/debug: 第6层:androidx.compose.material.ripple.RippleContainer{28cb3ed V.E...... ......I. 0,0-0,0}
E/debug: 第7层:androidx.compose.material.ripple.RippleHostView{b090222 V.ED..... ......I. 0,0-0,0}

可以看到,很明显,AndroidComposeView下多了两层子View,这是为什么呢?


我们一起来看下RippleHostView的注释



Empty View that hosts a RippleDrawable as its background. This is needed as RippleDrawables cannot currently be drawn directly to a android.graphics.RenderNode (b/184760109),
so instead we rely on View's internal implementation to draw to the background android.graphics.RenderNode.
A RippleContainer is used to manage and assign RippleHostViews when needed - see RippleContainer.getRippleHostView.



意思也很简单,Compose目前还不能直接绘制水波纹效果,因此需要将水波纹效果设置为View的背景,这里利用View做了一个中转

然后RippleHostViewRippleContainer自然会添加到AndroidComposeView中,如果我们在Compose中使用了AndroidView,效果也是一样的

但是这种情况并没有违背我们上面说的,纯Compose项目下,AndroidComposeView下没有子View,因为Button并不是纯Compose


总结


本文主要分析回答了Compose到底有没有完全脱离View系统这个问题,总结如下:



  1. Compose在渲染时并不会转化成View,而是只有一个入口View,即AndroidComposeView,纯Compose项目下,AndroidComposeView没有子View

  2. 我们声明的Compose布局在渲染时会转化成NodeTree,AndroidComposeView中会触发NodeTree的布局与绘制,AndroidComposeView#dispatchDraw是绘制的入口

  3. Android平台上,Compose的布局与绘制已基本脱离View体系,但仍然依赖于Canvas

  4. 由于良好的分层体系,Compose可通过 compose.runtimecompose.compiler实现跨平台

  5. 在使用Button时,AndroidComposeView会有两层子View,这是因为Button中使用了View来实现水波纹效果

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

Android 程序崩溃之快速锁定!

前言 从刚开始接触Android开发,第一次发版,遇到程序崩溃,那就一个慌张。好几年过去了,现在的听到程序崩溃?嗯,稍等我看看什么问题,然后该锁定该锁定该解决解决。 发版前减少bug、崩溃等,发版后遇到bug、崩溃也不要慌张,毕竟 bug不 会因为你的慌张而自...
继续阅读 »

前言


从刚开始接触Android开发,第一次发版,遇到程序崩溃,那就一个慌张。好几年过去了,现在的听到程序崩溃?嗯,稍等我看看什么问题,然后该锁定该锁定该解决解决。


发版前减少bug、崩溃等,发版后遇到bug、崩溃也不要慌张,毕竟 bug不 会因为你的慌张而自动修复对吧?要以最快的速度解决(解决问题同样是能力的体现),并说明问题轻重,看看是直接发版还是坐等下次。同时,吸取教训避免同样问题发生。


今天咱们就聊聊Android程序闪退。一个应用的崩溃率高低,决定了这个应用的质量。


为了解决崩溃问题,Android 系统会输出各种相应的 log 日志,当然还各式各样的三方库,大程度上降低了工程师锁定崩溃问题的难度。


如果要给 crash 日志进行分类,可以分成 2 大类



  • JVM 异常(Exception)堆栈信息,如下:




  • native 代码崩溃日志,如下:



JVM 异常堆栈信息


Java 中异常(Exception)分两种:



  • 检查异常 checked Exception

  • 非检查异常 unchecked Exception


检查异常:就是在代码编译时期,Android Studio 就会提示代码有错误,无法通过编译,比如 InterruptedException。如果我们没有在代码中将这些异常 catch,而是直接抛出,最终也有可能导致程序崩溃。


        try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

非检查异常:包括 error 和运行时异常(RuntimeException),Android Studio 并不会在编译时期提示这些异常信息,而是在程序运行时期因为代码错误而直接导致程序崩溃,比如 OOM 或者空指针异常(NullPointerException)。


2021-09-13 11:50:27.327 19984-19984/com.scc.demo E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.scc.demo, PID: 19984
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.scc.demo/com.scc.demo.actvitiy.HandlerActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.Button.setOnClickListener(android.view.View$OnClickListener)' on a null object reference
...
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.Button.setOnClickListener(android.view.View$OnClickListener)' on a null object reference
at com.scc.demo.actvitiy.HandlerActivity.onCreate(HandlerActivity.java:41)
at android.app.Activity.performCreate(Activity.java:8000)
...

Java 异常


对于上述两种异常我们都可以使用 UncaughtExceptionHandler 来进行捕获操作,它是 Thread 的一个内部接口,定义如下:


    public interface UncaughtExceptionHandler {
/**
* 当给定Thread由于给定的Throwable而终止时调用的方法。
* 此方法抛出的任何异常都将被 Java 虚拟机忽略。
* @param t Thread
* @param e Throwable
*/
void uncaughtException(Thread t, Throwable e);
}

从官方对其介绍能够看出,对于传入的 Thread,如果因为"未捕获"异常而导致被终止,uncaughtException 则会被调用。我们可以借助它来间接捕获程序异常,并进行异常信息的记录工作,或者给出更友好的异常提示信息。


自定义异常处理类


自定义异常处理类


自定义类实现 UncaughtExceptionHandler 接口,并实现 uncaughtException 方法:


public class SccExceptionHandler implements Thread.UncaughtExceptionHandler {
private Thread.UncaughtExceptionHandler mDefaultHandler;
private static SccExceptionHandler sccExceptionHandler;
private Context mContext;

public static SccExceptionHandler getInstence() {
if (sccExceptionHandler == null) {
synchronized (SccExceptionHandler.class) {
sccExceptionHandler = new SccExceptionHandler();
}
}
return sccExceptionHandler;
}

public void init(Context context) {
mContext = context;
//系统默认未捕获异常handler
//the default uncaught exception handler for all threads
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
//将当前Handler设为系统默认
Thread.setDefaultUncaughtExceptionHandler(this);

}

@Override
public void uncaughtException(@NonNull @NotNull Thread t, @NonNull @NotNull Throwable e) {
if (!handlerUncaughtException(e) && mDefaultHandler != null) {
//注释1:系统处理
mDefaultHandler.uncaughtException(t, e);
} else {
//注释2:自己处理
Intent intent = new Intent(mContext, ImageViewActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
mContext.startActivity(intent);
//终止进程
android.os.Process.killProcess(android.os.Process.myPid());
//终止当前运行的 Java 虚拟机。
//参数用作状态代码; 按照惯例,非零状态代码表示异常终止。
System.exit(0);
}
}

//处理程序未捕获异常
private boolean handlerUncaughtException(Throwable e) {
//1.收集 crash 现场的相关信息,如当前 App 的版本信息,设备的相关信息以及异常信息。
//2.日志的记录工作(如保存在本地),等开发人员排查问题或等下次启动APP上传至服务器。
return true;
//不想处理 return false;
}
}

注释1:在自定义异常处理类中需要持有线程默认异常处理类。这样做的目的是在自定义异常处理类无法处理或者处理异常失败时,还可以将异常交给系统做默认处理。


注释2:如果自定义异常处理类成功处理异常,需要进行页面跳转,或者将程序进程"杀死"。否则程序会一直卡死在崩溃界面,并弹出无响应对话框。



android.os.Process.myPid():返回此进程的标识符,可与 killProcess 和 sendSignal 一起使用。




android.os.Process.killProcess(android.os.Process.myPid()):使用给定的 PID 终止进程。 请注意,尽管此 API 允许我们根据其 PID 请求终止任何进程,但内核仍会对您实际能够终止的 PID 施加标准限制。 通常这意味着只有运行调用者的包/应用程序的进程和由该应用程序创建的任何其他进程; 共享一个通用 UID 的包也将能够杀死彼此的进程。



使用自定义异常处理类


SccExceptionHandler 定义好之后,就可以将其初始化,并将主线程注册到 SccExceptionHandler 中。如下:


public class SccApp extends Application {
@Override
public void onCreate() {
super.onCreate();
SccExceptionHandler.getInstence().init(this);
}
}

native 异常


当程序中的 native 代码发生崩溃时,系统会在 /data/tombstones/ 目录下保存一份详细的崩溃日志信息。由于对 native 还不是很熟悉就不误导大家,感兴趣的自己玩玩。


对于程序崩溃信号机制的介绍,可以参考腾讯的这篇文章:Android 平台 Native 代码的崩溃捕获机制及实现


线上崩溃日志获取


上面介绍的 Java 和 Native 崩溃的捕获都是基于自己能够复现 bug 的前提下。但是对于线上的用户,这种操作方式是不太现实的。


对于大多数公司来说,针对线上版本,没有必要自己实现一个抓取 log 的平台系统。最快速的实现方式就是集成第三方 SDK。目前比较成熟,采用也比较多的就是腾讯的 Bugly。


Bugly


Bugly 基本能够满足线上版本捕获 crash 的所有需求,包括 Java 层和 Native 层的 crash 都可以获取相应的日志。并且每天 Bugly 都会邮件通知上一天的崩溃日志,方便测试和开发统计 bug 的分布以及崩溃率。


接入文档



异常概括



崩溃分析



程序崩溃分析这块我没做调整,这个是bugly自动抓取的。


错误分析



具体内容




这里我用来存放去服务端请求接口时的参数和返回的数据。,下面看看具体效果。


使用起来相当方便,而且错误还提供解决方案,美滋滋。


xCrash


xCrash 能为安卓 app 提供捕获 java 崩溃,native 崩溃和 ANR 的能力。不需要 root 权限或任何系统权限。


xCrash 能在 app 进程崩溃或 ANR 时,在你指定的目录中生成一个 tombstone 文件(格式与安卓系统的 tombstone 文件类似)。


xCrash 已经在 爱奇艺 的不同平台(手机,平板,电视)的很多安卓 app(包括爱奇艺视频)中被使用了很多年。


xCrash传送门


Sentry


Sentry 是一项可帮助您实时监控和修复崩溃的服务。 服务器使用 Python,但它包含一个完整的 API,用于在任何应用程序中从任何语言发送事件。


Sentry传送门


XCrash 和 Sentry,这两者比 Bugly 好的地方就是除了自动拦截界面崩溃事件,还可以主动上报错误信息。


可以看出 XCrash 的使用更加灵活,工程师的掌控性更高。可以通过设置不同的过滤方式,针对性地上报相应的 crash 日志。并且在捕获到 crash 之后,可以加入自定义的操作,比如本地保存日志或者直接进行网络上传等。


另外:Sentry 还有一个好处就是可以通过设置过滤,来判断是否上报 crash 日志。这对于 SDK 的开发人员是很有用的。比如一些 SDK 的开发商只是想收集自身 SDK 引入的 crash,对于用户的其他操作导致的 crash 进行过滤,这种情况就可以考虑集成 Sentry。


Bugly 简单使用


感觉教程乱的可以自己去上文找Buyle文档自己集成,很简单的。


库文件导入


自动集成(推荐)


plugins {
id 'com.android.application'
}
android {
compileSdkVersion 30//项目的编译版本
defaultConfig {
applicationId "com.scc.demo"//包名
minSdkVersion 23//最低的兼容的Android系统版本
targetSdkVersion 30//目标版本,表示你在该Android系统版本已经做过充分的测试
versionCode 1//版本号
versionName "1.0.0"//版本名称
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a','x86'
//运行环境,要上传Google Play必须兼容64位,这里仅兼容ARM架构
//对于ARM架构,32 位库位于armeabi-v7a 中。64 位等效项是arm64-v8a。
//对于x86体系结构,查找x86(用于 32 位)和 x86_64(用于 64 位)。
}
}
}

dependencies {
implementation 'com.tencent.bugly:crashreport:3.4.4'
//集成Bugly NDK时,需要同时集成Bugly SDK。
implementation 'com.tencent.bugly:nativecrashreport:3.9.2'

}
复制代码

注意:自动集成时会自动包含Bugly SO库,建议在Module的build.gradle文件中使用NDK的"abiFilter"配置,设置支持的SO库架构。


如果在添加"abiFilter"之后Android Studio出现以下提示:


NDK integration is deprecated in the current plugin. Consider trying the new experimental plugin.


则在项目根目录的gradle.properties文件中添加:


android.useDeprecatedNdk=true


初始化


public class SccApp extends Application {
@Override
public void onCreate() {
super.onCreate();
//70594a1ff8 Bugly新建产品的 App ID
CrashReport.initCrashReport(getApplicationContext(), "70594a1ff8", false);
}
}

错误分析


设置


    private void setCrashReport(String url, String name, Map<String, String> params, String message) {
try {
if (params != null && !MStringUtils.isNullOrEmpty(url) && !MStringUtils.isNullOrEmpty(name) && !MStringUtils.isNullOrEmpty(params.toString()) && !MStringUtils.isNullOrEmpty(message)) {
CrashReport.putUserData(AppGlobalUtils.getApplication(), "SccParams", params.toString());
CrashReport.putUserData(AppGlobalUtils.getApplication(), "Data", "LoginName-Scc001:" + message);
CrashReport.postCatchedException(new RuntimeException(name + ":" + url + ":" + message));
}
} catch (Exception e) {
}
}

调用


        HashMap<String,String> hashMap = new HashMap<>();
hashMap.put("name","scc001");
hashMap.put("pass","111111");
String returnData = "哈哈哈哈哈";
setCrashReport("loin/register","Main",hashMap,returnData);

效果


错误列表



错误详情



出错堆栈



跟踪数据



崩溃分析


这个不用咱自己设置,Bugly自动抓取,下面提供跟错误分析类似功能这里就不多描述了。



本文内容到这里就算结束了。希望能帮你快速锁定 bug 并解决,让应用更完美,让你的老板更放心,票票来的更多一些。


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

JavaScript之彻底理解原型与原型链

前言 原型与原型链知识历来都是面试中考察的重点,说难不算太难,但要完全理解还是得下一定的功夫。先来看一道面试题开开胃口吧: function User() {} User.prototype.sayHello = function() {} var u1 = ...
继续阅读 »

前言


原型与原型链知识历来都是面试中考察的重点,说难不算太难,但要完全理解还是得下一定的功夫。先来看一道面试题开开胃口吧:


function User() {}
User.prototype.sayHello = function() {}
var u1 = new User();
var u2 = new User();
console.log(u1.sayHello === u2.sayHello);
console.log(User.prototype.constructor);
console.log(User.prototype === Function.prototype);
console.log(User.__proto__ === Function.prototype);
console.log(User.__proto__ === Function.__proto__);
console.log(u1.__proto__ === u2.__proto__);
console.log(u1.__proto__ === User.__proto__);
console.log(Function.__proto__ === Object.__proto__);
console.log(Function.prototype.__proto__ === Object.prototype.__proto__);
console.log(Function.prototype.__proto__ === Object.prototype);

基础铺垫




  1. JavaScript所有的对象本质上都是通过new 函数创建的,包括对象字面量的形式定义对象(相当于new Object()的语法糖)。


    对象定义.jfif




  2. 所有的函数本质上都是通过new Function创建的,包括ObjectArray
    函数定义.jfif




  3. 所有的函数都是对象。




prototype


每个函数都有一个属性prototype,它就是原型,默认情况下它是一个普通Object对象,这个对象是调用该构造函数所创建的实例的原型。
函数原型.jfif


contructor属性


JavaScript同样存在由原型指向构造函数的属性:constructor,即Func.prototype.constructor --> Func
constructor.jfif


__proto__


JavaScript中所有对象(除了null)都具有一个__proto__属性,该属性指向该对象的原型。


function User() {}
var u1 = new User();
// u1.__proto__ -> User.prototype
console.log(u1.__proto__ === User.prototype) // true

显而易见,实例的__proto__属性指向了构造函数的原型,那么多个实例的__proto__会指向同一个原型吗?


var u2 = new User();
console.log(u1.__proto__ === u2.__proto__) // true

如果多个实例的__proto__都指向构造函数的原型,那么实例如果能通过一种方式,访问原型上的方法,属性等,就可以在原型进行编程,实现继承的效果。


我们继续更新一下原型与原型链的关系图:
隐式原型.jfif


原型链


实例对象在查找属性时,如果查找不到,就会沿着__proto__去与对象关联的原型上查找,如果还查找不到,就去找原型的原型,直至查到最顶层,这也就是原型链的概念。


就借助面试题,举几个原型链的例子:


举例



  1. u1.sayHello()
    u1上是没有sayHello方法的,因此访问u1.__proto__(User.prototype),成功访问到sayHello方法

  2. u2.toString()
    u2,User.prototype都没有toString方法,User.prototype也是一个普通对象,因此继续寻找User.prototype.__proto__(Object.prototype),成功调用到toString方法


提高


学完上面那些,大多数面试题都可以做出来了,例如下面这种


function A() {}
function B(a) {
this.a = a;
}
function C(a) {
if (a) {
this.a = a;
}
}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;

console.log(new A().a); //1
console.log(new B().a); //undefined
console.log(new C(2).a); //2

但距离解决文章的最初的面试题还欠缺些什么,比如Function.__proto__ === Object.__proto__、Function.prototype.__proto__ === Object.prototype.__proto__等,接着我们来一一攻克它。


Objcet.__proto__Object.prototypeObject.prototype.__proto__




  • Object是构造函数,在第二部分我们讲过所有的函数都是通过new Function创建了,因此Object相当于Function的实例,即Object.__proto__ --> Function.prototype




  • Object.prototypeObject构造函数的原型,处于原型链的顶端,Object.prototype.__proto__已经没有可以指向的上层原型,因此其值为null




// 总结:
Object.__proto__ --> Function.prototype
Object.prototype.__proto__ --> null

Function.__proto__Function.prototypeFunction.prototype.__proto__



  • Function.prototypeFunction的原型,是所有函数实例的原型,例如上面讲的Object.__proto__

  • Function.prototype是一个普通对象,因此Function.prototype.__proto__ --> Object.prototype

  • Function.__proto__: __proto__指向创造它的构造函数的原型,那谁创造了Function那?


    • 猜想:函数对象也是对象,那Function.__proto__会指向Object.prototype吗?上文提到,Object.__proto__ --> Function.prototype。如果Function.__proto__ -> Object.prototype,感觉总是怪怪的,到底谁创造了谁,于是我去做了一下测试:


      Function与Object.png




    实践证明只存在Object.__proto__ --> Function.prototype


    • 苦思冥想没得出结果,难道Function函数是猴子不成,从石头缝里面蹦出来的?于是我进行了一同乱七八糟的测试,没想到找出了端倪。


      Function proto.png




    通过上面我们可以得出:Function.__proto__ --> Function.prototype

    Function函数不通过任何东西创建,JS引擎启动时,添加到内存中





总结


最后将原型与原型链方面的知识凝结成一张图:
原型链.jfif



  1. 所有函数(包括Function)的__proto__指向Function.prototype

  2. 自定义对象实例的__proto__指向构造函数的原型

  3. 函数的prototype__proto__指向Object.prototype

  4. Object.prototype.__proto__ --> null


后语


知识的海洋往往比想象中还要辽阔,原型与原型链这边也反复的学过多次,我认为应该学的比较全面,比较完善了。但遇到这个面试题后,我才发现我所学的只不过是一根枝干,JS里面真的有很多深层次的宝藏等待挖掘。学海无涯,与君共勉。


最后再附赠个简单的面试题,提高一下自信:


var F = function () {}
Object.prototype.a = function () {}
Function.prototype.b = function () {}

var f = new F();

console.log(f.a, f.b, F.a, F.b);

// 原型链
// f.__proto__ --> F.prototype --> Object.prototype
// F.__proto__ --> Function.prototype --> Object.prototype


收起阅读 »

18 个杀手级 JavaScript 单行代码

1、复制到剪贴板 使用 navigator.clipboard.writeText 轻松将任何文本复制到剪贴板。 const copyToClipboard = (text) => navigator.clipboard.writeText(text);...
继续阅读 »

1、复制到剪贴板


使用 navigator.clipboard.writeText 轻松将任何文本复制到剪贴板。


const copyToClipboard = (text) => navigator.clipboard.writeText(text);
copyToClipboard("Hello World");

2、检查日期是否有效


使用以下代码段检查给定日期是否有效。


const isDateValid = (...val) => !Number.isNaN(new Date(...val).valueOf());
isDateValid("December 17, 1995 03:24:00");
// Result: true

3、找出一年中的哪一天


查找给定日期的哪一天。


const dayOfYear = (date) =>  Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);
dayOfYear(new Date());
// Result: 272

4、将首字符串大写


Javascript 没有内置的大写函数,因此我们可以使用以下代码。


const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1)capitalize("follow for more")// Result: Follow for more

5、找出两日期之间的天数


使用以下代码段查找给定 2 个日期之间的天数。


const dayDif = (date1, date2) => Math.ceil(Math.abs(date1.getTime() - date2.getTime()) / 86400000)dayDif(new Date("2020-10-21"), new Date("2021-10-22"))// Result: 366

6、清除所有 Cookie


你可以通过使用 document.cookie 访问 cookie 并清除它来轻松清除存储在网页中的所有 cookie。


const clearCookies = document.cookie.split(';').forEach(cookie => document.cookie = cookie.replace(/^ +/, '')
.replace(/=.*/, `=;expires=${new Date(0).toUTCString()};
path=/`));

7、生成随机十六进制


你可以使用 Math.random 和 padEnd 属性生成随机十六进制颜色。


const randomHex = () => `#${Math.floor(Math.random() * 0xffffff).toString(16).padEnd(6, "0")}`
console.log(randomHex());
//Result: #92b008

8、从数组中删除重复项


你可以使用 JavaScript 中的 Set 轻松删除重复项。


const removeDuplicates = (arr) => [...new Set(arr)];
console.log(removeDuplicates([1, 2, 3, 3, 4, 4, 5, 5, 6]));
// Result: [ 1, 2, 3, 4, 5, 6 ]

9、从 URL 获取查询参数


你可以通过传递 window.location 或原始 URL goole.com?search=easy&page=3 从 url 轻松检索查询参数


const getParameters = (URL) => {
URL = JSON.parse('{"' + decodeURI(URL.split("?")[1]).replace(/"/g, '\"').replace(/&/g, '","').replace(
/=/g, '":"') + '"}');
return JSON.stringify(URL);
};
getParameters(window.location) // Result: { search : "easy", page : 3 }

10、从日期记录时间


我们可以从给定日期以小时::分钟::秒的格式记录时间。


const timeFromDate = date => date.toTimeString().slice(0, 8);
console.log(timeFromDate(new Date(2021, 0, 10, 17, 30, 0)));
// Result: "17:30:00"

11、检查数字是偶数还是奇数


const isEven = num => num % 2 === 0;console.log(isEven(2));
// Result: True

12、求数字的平均值


使用 reduce 方法找到多个数字之间的平均值。


const average = (...args) => args.reduce((a, b) => a + b) / args.length;
average(1, 2, 3, 4);
// Result: 2.5

13、反转字符串


你可以使用 split、reverse 和 join 方法轻松反转字符串。


const reverse = str => str.split('').reverse().join('');reverse('hello world'); 
// Result: 'dlrow olleh'

14、检查数组是否为空


检查数组是否为空的简单单行程序将返回 true 或 false。


const isNotEmpty = arr => Array.isArray(arr) && arr.length > 0;
isNotEmpty([1, 2, 3]);
// Result: true

15、获取选定的文本


使用内置的 getSelectionproperty 获取用户选择的文本。


const getSelectedText = () => window.getSelection().toString();
getSelectedText();

16、打乱数组


使用 sort 和 random 方法打乱数组非常容易。


const shuffleArray = (arr) => arr.sort(() => 0.5 - Math.random());console.log(shuffleArray([1, 2, 3, 4]));// Result: [ 1, 4, 3, 2 ]

17、检测暗模式


使用以下代码检查用户的设备是否处于暗模式。


const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matchesconsole.log(isDarkMode) // Result: True or False

18、将 RGB 转换为十六进制


const rgbToHex = (r, g, b) =>   "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);rgbToHex(0, 51, 255); // Result: #0033ff

作者:耀在掘金73091
链接:https://juejin.cn/post/7018106908129099807

收起阅读 »

抛弃Vue转入React的六个月,我收获了什么?

对不起,我抛弃了Vue,转入React阵营。不因为其它,就因为在我这边使用React的工资比使用Vue的工资高。 在六月前,我硬背了几百道的React面试题,入职一家使用React的公司,薪资增幅120%; 入职就马上进入开发阶段,完全是从零开始,随着时间的推...
继续阅读 »

对不起,我抛弃了Vue,转入React阵营。不因为其它,就因为在我这边使用React的工资比使用Vue的工资高。


在六月前,我硬背了几百道的React面试题,入职一家使用React的公司,薪资增幅120%;


入职就马上进入开发阶段,完全是从零开始,随着时间的推移,发现React入门也不是网传的那么难。难道是自己天生就适合吃这碗饭…………


到今天已经六个月了,在这里想把这段时间的收获跟掘友们分享一下,请掘友们多多指教,一起进步哈。


另外我是用React16.8来入门,故在开发过程中大多使用函数式组件React Hooks来开发。


重磅提醒,文末有抽奖噢。


一、关于函数式组件的收获


函数式组件可以理解为一个能返回React元素的函数,其接收一个代表组件属性的参数props


在React16.8之前,也就是没有React Hooks之前,函数式组件只作为UI组件,其输出完全由参数props控制,没有自身的状态没有业务逻辑代码,是一个纯函数。函数式组件没有实例,没有生命周期,称为无状态组件。


在React Hooks出现后,可以用Hook赋予函数式组件状态和生命周期,于是函数式组件也可以作为业务组件。


开发过程中,类组件和函数式组件都有使用,经过六个月的开发,感觉还是函数式组件比类组件好用一些,感受最深的是以下两点:



  • 不用去学习class,不用去管烦人的this指向问题;

  • 复用性高,很容易就把共同的抽取出来,写出自定义Hook,来替代高阶组件。


函数式组件和类组件之间有一个非常重要的区别:函数式组件捕获了渲染所使用的值


我是遇到一个BUG才知道有这个区别,在解决这个BUG的过程理解了这个区别的含义。


那个BUG的场景是这样的,一个输入框,输入完内容,点击按钮搜索,搜索时先请求一个接口,获取一个类型,再用类型和输入框值去请求搜索接口。用代码简单描述一下。


import React, { Component } from "react";
import * as API from 'api/list';
class SearchComponent extends Component {
constructor() {
super();
this.state = {
inpValue: ""
};
}

getType () {
const param = {
val:this.state.inpValue
}
return API.getType(param);
}

getList(type){
const param = {
val:this.state.inpValue,
type,
}
return API.getList(param);
}

async handleSearch() {
const res = await this.getType();
const type = res?.data?.type;
const res1 = await this.getList(type);
console.log(res1);
}

render() {
return (
<div>
<input
type="text"
value={this.state.inpValue}
onChange={(e) => {
this.setState({ inpValue: e.target.value });
}}
/>
<button
onClick={() => {
this.handleSearch();
}}
>
搜索
</button>
</div>
);
}
}
export default SearchComponent;

以上代码逻辑看上去都没什么毛病,但是QA给我挑了一个BUG,在输入框输入要搜索的内容后,点击搜索按钮开始搜索,然后很快在输入框中又输入内容,结果搜索接口getList报错。查一下原因,发现是获取类型接口getType和搜索接口getList接受的参数val不一致。


在排查过程中,我非常纳闷,明明两次请求中val都是读取this.state.inpValue的值。当时同事指导我改成函数式组件就可解决这个BUG。


import React, { useState } from "react";
import * as API from 'api/list';
export const SearchComponent = () =>{
const [inpValue,setInpValue] = useState('');

const getType = () =>{
const param = {
val:inpValue
}
return API.getType(param);
}

const getList = (type) =>{
const param = {
val:inpValue,
type:type,
}
return API.getList(param);
}

const handleSearch = async ()=>{
const res = await getType();
const type = res?.data?.type;
const res1 = await getList(type);
console.log(res1);
}

return (
<div>
<input
type="text"
value={inpValue}
onChange={(e) => {
setInpValue(e.target.value);
}}
/>
<button
onClick={() => {
handleSearch();
}}
>
搜索
</button>
</div>
);
}
export default SearchComponent;

改成函数式组件后,再试一下,不报错了,BUG修复了。后面我查阅资料后才知道在函数式组件中的事件的state和props所获取的值是事件触发那一刻页面渲染所用的state和props的值。当点击搜索按钮后,val的值就是那一刻输入框中的值,无论输入框后面的值在怎么改变,不会捕获最新的值。


那为啥类组件中,能获取到最新的state值呢?关键在于类组件中是通过this去获取state的,而this永远是最新的组件实例。


其实在类组件中改一下,也可以解决这个BUG,改动的地方如下所示:


getType (val) {
const param = {
val,
}
return API.getType(param);
}
getList(val,type){
const param = {
val,
type,
}
return API.getList(param);
}
async handleSearch() {
const inpValue = this.state.inpValue;
const res = await this.getType(inpValue);
const type = res?.data?.type;
const res1 = await this.getList(inpValue,type);
console.log(res1);
}

在搜索事件handleSearch触发时,就把输入框的值this.state.inpValue存在inpValue变量中,后续执行的事件用到输入框的值都去inpValue变量取,后续再往输入框输入内容也不会影响到inpValue变量的值,除非再次触发搜索事件handleSearch。这样修改也可以解决这个BUG。


二、关于受控和非受控组件的收获


在React中正确理解受控和非受控组件的概念和作用至关重要,因为太多地方用到了



  • 受控组件


在Vue中有v-model指令可以很轻松把组件和数据关联起来,而在React中没有这种指令,那怎么让组件和数据联系起来,这时候就要用到受控组件的概念。


受控组件,我理解为组件的状态由数据来控制,改变这个数据的方法却不是组件的,这里所说的组件不仅仅是组件,也可以表示一个原生DOM。比如一个input输入框。


input输入框的状态(输入框的值)由数据value控制,改变数据value的方法setValue不是input输入框自身的。


import React, { useState } from "react";
const Input = () =>{
const [value,setValue] = useState('');
return (
<div>
<input
type="text"
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
</div>
);
}
export default Input;

再比如在Ant Design UI中的Form组件自定义表单控件时,要求自定义表单控件接受属性valueonChange,其中value来作为自定义表单控件的值,onChange事件来改变属性value


import React from 'react';
import { Form, Input } from 'antd';

const MyInput = (props) => {
const { value, onChange } = props;

const onNameChange = (e) => {
onChange?.(e.target.value)
}

return (
<Input
type="text"
value={value || ''}
onChange={onNameChange}
/>
)
}
const MyForm = () => {
const onValuesChange = (values) => {
console.log(values);
};

return (
<Form
name="demoFrom"
layout="inline"
onValuesChange={onValuesChange}
>
<Form.Item name="name">
<MyInput />
</Form.Item>
</Form>
)
}

export default MyForm;

我认为受控组件最大的作用是在第三方组件的状态经常莫名奇妙的改变时,用一个父组件将其包裹起来,传入一个要控制的状态和改变状态方法的props,把组件的状态提到父组件来控制,这样当组件的状态改变时就很清晰的知道哪个地方改变了组件的状态。



  • 非受控组件


非受控组件就是组件自身状态完全由自身来控制,是相对某个组件而言,比如input输入框受Input组件控制,而Input组件不受Demo组件控制,那么Input相对Demo组件是非受控组件。


import React, { useState } from "react";
const Input = () =>{
const [value,setValue] = useState('');
return (
<div>
<input
type="text"
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
</div>
);
}
const Demo = () =>{
return (
<Input/>
)
}
export default Demo;

也可以把非受控组件理解为组件的值只能由用户设置,而不能通过代码控制。此外要记住一个非常特殊的DOM元素<input type="file" />,其无法通过代码设置所上传的文件。


三、关于useState的收获


useState可谓是使用频率最高的一个Hook,下面分享一下使用的3个心得。




  • useState可以接收一个函数作为参数,把这个函数称为state初始化函数


    一般场景下useState只要传入一个值就可以作为这个state的初始值,但是如果这个值要通过一定计算才能得出的呢?那么此时就可以传入一个函数,在函数中计算完成后,返回一个初始值。


    import React, { useState } from 'react';
    export default (props) => {
    const { a } = props;
    const [b] = useState(() => {
    return a + 1;
    })
    return (
    <div>{b}</div>
    )
    };



  • state更新如何使用旧的state


    刚开始时,我这么使用的


    const [a , setA] = useState(1);
    const changeA = () =>{
    setA(a + 1);
    }

    后来遇到一个错误,代码如下


    const [a , setA] = useState(1);
    const changeA = () =>{
    setA(a + 1);
    setA(a + 1);
    }

    在函数中连续调用两次setA会发现a还是等于2,并不是等于3。正确要这么调用


    const [a , setA] = useState(1);
    const changeA = () =>{
    setA( a => a+1 );
    setA( a => a+1 );
    }



  • 如何拆分state


    在函数式组件中,只要一个state改变都会引起组件的重新渲染。而在React中只要父组件重新渲染,其子组件都会被重新渲染。


    那么问题就来了,如果把state拆分成多个,当依次改变这些state,则会多次触发组件重新渲染。


    若不拆分state,改变state时,就只会触发一次组件重新渲染。但是要注意,函数式组件不像类组件那样,改变其中state中的一个数据,会自动更新state中对应的数据。要这么处理


    const [data,setData] = useState({a:1,b:2,c:3});
    const changeData = () =>{
    setData(data =>{
    ...data,
    a:2,
    })
    }



当然也不能不拆分state,这样代码复用性会大大降低。我的经验是:




  • 将完全不相关的state拆分成一个个




  • 如果有些state是相互关联的,或是需要一起改变,那么将其合并为一个state




四、关于useMemo和usecallback的收获



  • 对其定义的理解


useCallback(fn,[a, b]);
useMemo(fn, [a, b]);

如上所示,useMemousecallback参数中fn是一个函数,参数中[a,b]ab可以是state或props。


useMemousecallback首次执行时,执行fn后创建一个缓存并返回这个缓存,监听[a,b]中的表示state或props的ab,若值未发生变化直接返回缓存,若值发生变化则重新执行fn再创建一个缓存并返回这个缓存。


useMemousecallback都会返回一个缓存,但是这个缓存各不相同,useMemo返回一个值,这个值可以认为是执行fn返回的。usecallback返回一个函数,这个函数可以认为是fn那么注意了传给useMemofn必须返回一个值



  • 结合React.memo来使用


React.memo包裹一个组件,当组件的props发生改变时才会重新渲染。


若包裹的组件是个函数式组件,在其中拥有useStateuseReduceruseContext的 Hook,当state 或context发生变化时,它仍会重新渲染,不过这些影响不大,使用React.memo的主要目的是控制父组件更新迫使子组件也更新的问题。


props值的类型可以为String、Boolean、Null、undefined、Number、Symbol、Object、Array、Function,简单的来说就是基础类型和引用类型。


两个值相等的基础类型的数据用==比较返回true,那两个值相等的引用类型的数据用==比较返回false,不信试一试以下代码,看是不是都为false


console.log({a:1} == {a:1});
console.log([1] == [1]);
const fn1 = () =>{console.log(1)};
const fn2 = () =>{console.log(1)};
console.log(fn1 == fn2);

正因为如此,当props的值为引用类型时,且这个值是通过函数计算出来的,用useMemousecallback来处理一下,避免计算出来的值相等,但是比较却不相等,导致组件更新。我认为usecallback是专门为处理props值为函数而创建的Hook。


在下面的例子,List组件是一个渲染开销很大的组件,它有两个属性,其中data属性是渲染列表的数据源,是一个数组,onClick属性是一个函数。在Demo组件中引入List组件,用useMome处理data属性值,用useCallback处理onClick属性值,使得List组件是否重新渲染只受Demo组件的data这个state控制。


List组件:


import React from 'react';
const List = (props) => {
const { onClick, data } = props;
//...
return (
<>
{data.map(item => {
return (
<div onClick={() =>{onClick(item)}} key={item.id}>{item.content}</div>
)
})}
</>
)
}
export default React.memo(List);

Demo组件:


import
React,
{ useState, useCallback, useMemo }
from 'react';
import List from './List';

const Demo = () => {
//...
const [data, setData] = useState([]);
const listData = useMemo(() => {
return data.filter(item => {
//...
})
}, [data])

const onClick = useCallback((item) => {
console.log(item)
}, []);

return (
<div>
<List onClick={onClick} data={listData} />
</div>
)
}

export default Demo;

可见useMemousecallback作为一个性能优化手段,可以在一点程度上解决React父组件更新,其子组件也被迫更新的性能问题。



  • 单独使用


假设List组件不用React.memo包裹,还能用useMemousecallback来优化吗?先来看一段代码,也可以这么使用组件。


import React, { useState } from 'react';
import List from './List';

export default function Demo() {
//...
const [data, setData] = useState([]);
const list = () => {
const listData = data.filter(item => {
//...
})
const onClick = (item) => {
console.log(item)
}
return (
<List onClick={onClick} data={listData} />
)
}
return (
<div>
{list()}
</div>
)
}

其中list返回一个React元素,是一个值,那么是不是可以用useMemo来处理一下。


import React, { useState, useMemo } from 'react';
import List from './List';

export default function Demo() {
//...
const [data, setData] = useState([]);
const listMemo = useMemo(() => {
const listData = data.filter(item => {
//...
})
const onClick = (item) => {
console.log(item)
}
return (
<List onClick={onClick} data={listData} />
)
}, [data])
return (
<div>
{listMemo}
</div>
)
}

listMemo这个值(React元素)是否重新生成只受Demo组件的data这个state控制。这样不是相当List组件是否重新渲染只受Demo组件的data这个state控制。



  • 不能滥用



不能认为“不管什么情况,只要用useMemouseCallback处理一下,就能远离性能的问题”



要认识到useMemouseCallback也有一定的计算开销,例如useMemo会缓存一些值,在后续重新渲染,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回缓存的值。这个过程有一定的计算开销。


所以在使用useMemouseCallback前一定要考虑清楚使用场景,不能滥用。


下面总结一下要使用useMemouseCallback的场景:




  • 一个渲染开销很大的组件的属性是一个函数时,用useCallback处理一下这个属性值。




  • 得到一个值,有很高得计算开销,用useMemo处理一下。




  • 一个通过计算得到的引用类型的值,且这个值被赋值给一个渲染开销很大的组件的属性,用useMemo处理一下。




  • 自定义Hook中暴露出的引用类型的值,用useMemo处理一下。




总之一句话使用useMemouseCallback是为了保持引用相等和避免重复成本很高的计算。


五、关于useEffect和useLayoutEffect的收获


useEffect(fn);
useLayoutEffect(fn);

useEffect可谓是使用频率第二高的一个Hook,useLayoutEffect使用频率比较低。下面介绍四个使用心得:



  • 执行的时机


在使用useEffectuseLayoutEffect之前,要搞明白传入这个两个Hook的函数会在什么时候执行。


传入useEffect的函数是在React完成对DOM的更改,浏览器把更改后的DOM渲染出来后执行的


传入useLayoutEffect的函数是在React完成对DOM的更改后,浏览器把更改后的DOM渲染出来之前执行的


所以useEffect不会阻碍页面渲染,useLayoutEffect会阻碍页面渲染,但是如果要在渲染前获取DOM元素的属性做一些修改,useLayoutEffect是一个很好的选择。



  • 只想执行一次


组件初始化和每次更新时都会重新执行传入useEffectuseLayoutEffect的函数。那只想在组件初始化时执行一次呢?相当Vue中的mounted,这样实现:


useEffect(fn,[]);
useLayoutEffect(fn,[]);


  • 用来进行事件监听的坑


上面介绍在useEffect在第二参数传入一个空数组[]相当Vue中的mounted,那么在Vue的mounted中经常会用addEventListener监听事件,然后在beforeDestory中用removeEventListener移除事件监听。那用useEffect实现一下。


useEffect(() => {
window.addEventListener('keypress', handleKeypress, false);
return () => {
window.removeEventListener('keypress', handleKeypress, false);
};
},[])

上面用来监听键盘回事事件,但是你会发现一个很奇怪的现象,有些页面回车后会执行handleKeypress方法,有些页面回车后执行几次handleKeypress方法后,就不再执行了。


这是因为一个useEffect执行前会执行上一个useEffect的传入函数的返回函数,这个返回函数可以用来解除绑定,取消请求,防止引起内存泄露


此外组件卸载时,也会执行useEffect的传入函数的返回函数


示意如下代码所示:


window.addEventListener('keypress', handleKeypress, false); // 运行第一个effect
window.removeEventListener('keypress', handleKeypress, false);// 清除上一个effect
window.addEventListener('keypress', handleKeypress, false); // 运行下一个 effect
window.removeEventListener('keypress', handleKeypress, false); // 清除上一个effect
window.addEventListener('keypress', handleKeypress, false); // 运行下一个effect
window.removeEventListener('keypress', handleKeypress, false); // 清除最后一个effect

所以要解决上面的BUG,只要把useEffect的第二参数去掉即可。这点跟Vue中的mounted不一样。



  • 用来监听某个state或prop


useEffectuseLayoutEffect的第二参数是个数组,称为依赖,当依赖改变时候会执行传入的函数。


比如监听 a 这个state 和 b 这个prop,这样实现:


useEffect(fn,[a,b]);
useLayoutEffect(fn,[a,b]);

但是要注意不要一次性监听过多的state或prop,也就是useEffect的依赖过多,如果过多要去优化它,否则会导致这个useEffect难以维护。


我是这样来优化的:



  • 考虑该依赖是否必要,不必要去掉它。

  • useEffect拆分为更小的单元,每个useEffect依赖于各自的依赖数组。

  • 通过合并依赖中的相关state,将多个state聚合为一个state。


六、结语


以上五点就是我这六个月中印象最深的收获,有踩过的坑,有写被leader吐槽的代码。不过最大的收获还是薪资涨幅120%,哈哈哈,各位掘友要勇于跳出自己的舒适区,才能有更丰厚的收获。


虽然以上的收获在某些掘友们眼里会觉得比较简单,但是对于刚转入React的掘友们这些知识的使用频率非常高,要多多琢磨。如果有错误欢迎在评论中指出,或者掘友有更好的收获也在评论中分享一下。


作者:红尘炼心
链接:https://juejin.cn/post/7018328359742636039

收起阅读 »

转动的CSS“loading”,全都是技巧!

loader-1 这应该是最简单的CSS加载了。在圆圈上有一个红色的圆弧,仔细观察会发现,这个圆弧正好是1/4. 实现逻辑: 一个宽高相等容器,设定border为白色。然后给底边bottom设置红色, 当设定border-radius是50%,那他正好可以...
继续阅读 »

loader-1



这应该是最简单的CSS加载了。在圆圈上有一个红色的圆弧,仔细观察会发现,这个圆弧正好是1/4.


实现逻辑:


一个宽高相等容器,设定border为白色。然后给底边bottom设置红色,

当设定border-radius是50%,那他正好可以变成一个圆。



给这个圆加上旋转的动画。CSS中旋转角度的动画是rotate()我们只要给他设定从0旋转到360即可。(这个动画会在下面多次使用,下文不再赘述)


 @-webkit-keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

完整代码


.loader-1 {
width: 48px;
height: 48px;
border: 5px solid #FFF;
border-bottom-color: #FF3D00;
border-radius: 50%;
display: inline-block;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}

loader-2



观察:外围是一个圈,内部有一个红色的元素在转动。


实现逻辑


一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。



里面的红色是如何实现?这里有两个思路。1;新增一个小div,让他在里面,并且跟loader-1一样,设置一个红色的底边。2:使用::after,思路跟方法1 一致。



加上旋转的动画。


完整代码


.loader-2 {
width: 48px;
height: 48px;
border: 3px solid #FFF;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}

.loader-2:after {
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
border-radius: 50%;
border: 3px solid transparent;
border-bottom-color: #FF3D00;
}

loader-3



观察:内部是一个圆,外围是一个红色的圆弧。


实现逻辑


这个加载效果跟loader-2是一致的,区别就是红色圆弧在内外。


完整代码


.loader-3 {
width: 48px;
height: 48px;
border: 3px solid #FFF;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}

.loader-3:after {
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 56px;
height: 56px;
border-radius: 50%;
border: 3px solid transparent;
border-bottom-color: #FF3D00;
}

loader-4



观察:外围是一个圆,内部有两个圆,这两个圆正好是对称的。


实现逻辑


一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。



里面的红色是如何实现?这里有两个思路。1;新增两个小div,背景颜色设置为红色,然后设置50%圆角,这样看上去就像是两个小点。2:使用::after和::before,思路跟方法1 一致。



加上旋转的动画。


完整代码


.loader-4 {
width: 48px;
height: 48px;
border: 2px solid #FFF;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}

.loader-4:before {
left: auto;
top: auto;
right: 0;
bottom: 0;
content: "";
position: absolute;
background: #FF3D00;
width: 6px;
height: 6px;
transform: translate(150%, 150%);
border-radius: 50%;
}

.loader-4:after {
content: "";
position: absolute;
left: 0;
top: 0;
background: #FF3D00;
width: 6px;
height: 6px;
transform: translate(150%, 150%);
border-radius: 50%;
}

loader-5



观察:一共是三层,最外层白圈,中间红圈,里边白圈。每一圈都有一个一半圆弧的缺口,外圈和最内圈的旋转方向一致。


实现逻辑


一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。



这里的问题是,圈的缺口如何实现,其实很简单,在css中有一个属性值:transparent,利用这个值给边框设置透明,即可实现缺口。



对于内部的红色和白色圆弧,继续使用::after和::before即可。



加上动画,这里有一个反方向旋转的动画(rotationBack)。
这里设置旋转是往负角度,旋转即可反方向旋转。


  @keyframes rotationBack {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-360deg);
}
}

完整代码


.loader-5 {
width: 48px;
height: 48px;
border-radius: 50%;
display: inline-block;
position: relative;
border: 3px solid;
border-color: #FFF #FFF transparent transparent;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}

.loader-5:before {
width: 32px;
height: 32px;
border-color: #FFF #FFF transparent transparent;
-webkit-animation: rotation 1.5s linear infinite;
animation: rotation 1.5s linear infinite;
}

.loader-5:after, .loader-5:before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
border: 3px solid;
border-color: transparent transparent #FF3D00 #FF3D00;
width: 40px;
height: 40px;
border-radius: 50%;
-webkit-animation: rotationBack 0.5s linear infinite;
animation: rotationBack 0.5s linear infinite;
transform-origin: center center; *
}

loader-6



观察:看上去像是一个时钟,一个圆里面有一根指针。


实现逻辑


一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。



指针是如何实现的:从这里开始不再讨论新增div的情况
其实红色的指针就是一个单纯的宽高不一致的容器。



完整代码


.loader-6 {
width: 48px;
height: 48px;
border: 2px solid #FFF;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}


.loader-6:after {
content: "";
position: absolute;
left: 50%;
top: 0;
background: #FF3D00;
width: 3px;
height: 24px;
transform: translateX(-50%);
}

loader-7



观察:首先确定几个圈,一共两个。当第一个圈还没消失,第二个圈已经出现。最后出现了类似水波的效果。同时要注意的是,这两个两个圈是一样大的,这是因为他们最终消失的地方是一致的。


实现逻辑


首先确定,这两个圈是否在容器上。上面一直时在容器上添加边框,当然这个例子也可以,但是为了实现的简单,我们把这两个圈放在::after和::before中。



加上动画,这里的圈是逐渐放大的,在CSS中scale用来放大缩小元素。同时为了实现波纹逐渐清晰的效果,我们加上透明度。


  @keyframes animloader7 {
0% {
transform: scale(0);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}

完整代码


这里因为两个圈是先后出现的,所以需要一个圈加上delay


.loader-7 {
width: 48px;
height: 48px;
display: inline-block;
position: relative;
}

.loader-7::after, .loader--7::before {
content: "";
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid #FFF;
position: absolute;
left: 0;
top: 0;
-webkit-animation: animloader7 2s linear infinite;
animation: animloader7 2s linear infinite;
}

.loader-7::after {
-webkit-animation-delay: 1s;
animation-delay: 1s;
}
.loader-7::after, .loader-7::before {
content: "";
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid #FFF;
position: absolute;
left: 0;
top: 0;
-webkit-animation: animloader7 2s linear infinite;
animation: animloader7 2s linear infinite;
}

loader-8



观察:一段圆弧加上一个三角形。


实现逻辑


一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。



transparent,利用这个值给边框设置透明,即可实现缺口。



在:after上创建箭头。CSS中我们有多种方法实现三角形,其中最简单是使用border,不需要给元素设置宽高,只需要设置border的大小,并且只有一边设置颜色。


border: 10px solid transparent;
border-right-color: #FFF

加上旋转动画。


完整代码


.loader-8 {
width: 48px;
height: 48px;
border: 3px solid #FFF;
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}

.loader-8:after {
content: "";
position: absolute;
left: 20px;
top: 31px;
border: 10px solid transparent;
border-right-color: #FFF;
transform: rotate(-40deg);
}



收起阅读 »

Java 内存模型

运行时的数据区(Runtime Data Area)本文主要看 JVM 内存模型主要指运行时的数据区,包括 5 个部分,如下图所示。虚拟机栈:也叫方法栈,是线程私有的,线程在执行每个方法时,JVM 都会在虚拟机栈中创建一个栈帧,用来存储局部变量表、操作栈、动态...
继续阅读 »

运行时的数据区(Runtime Data Area)

本文主要看 JVM 内存模型主要指运行时的数据区,包括 5 个部分,如下图所示。

虚拟机栈:也叫方法栈,是线程私有的,线程在执行每个方法时,JVM 都会在虚拟机栈中创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。调用方法时执行入栈,方法返回时执行出栈。

本地方法栈:与栈类似,也是用来保存线程执行方法时的信息,不同的是,执行 Java 方法使用栈,而执行 native 方法使用本地方法栈。

程序计数器:保存着当前线程所执行的字节码位置,每个线程工作时都有一个独立的计数器。程序计数器为执行 Java 方法服务,执行 native 方法时,程序计数器为空(Undefined)。

栈、本地方法栈、程序计数器这三个部分都是线程独占的。

:是 JVM 管理的内存中最大的一块,堆被所有线程共享,目的是为了存放对象实例,几乎所有的对象实例都在这里分配。当堆内存没有可用的空间时,会抛出 OOM 异常。根据对象存活的周期不同,JVM 把堆内存进行分代管理,由垃圾回收器来进行对象的回收管理。

方法区:也是各个线程共享的内存区域,又叫非堆区。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,JDK 1.7 中的永久代和 JDK 1.8 中的 Metaspace 都是方法区的一种实现。

注意:面试回答这个问题时,要答出两个要点:一个是各部分的功能,另一个是哪些线程共享,哪些独占。

下面咱们详细来了解这五个部分。

1. 虚拟机栈(Stack)

虚拟机栈也是线程私有的,与线程的生命周期同步。在 Java 虚拟机规范中,对这个区域规定了两种异常状况:

  1. StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出。
  2. OutOfMemoryError:当 Java 虚拟机动态扩展到无法申请足够内存时抛出。

在我们学习 Java 虚拟机的的过程当中,经常会看到一句话:

JVM 基于栈,而 Dalvik(DVM) 基于寄存器。

这里的"基于栈"指的就是虚拟机栈。虚拟机栈的初衷是用来描述 Java 方法执行的内存模型,线程在执行每个方法时,JVM 都会在虚拟机栈中创建一个栈帧,下面咱们来看看这个栈帧?

栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,每一个线程在执行某个方法时,都会为这个方法创建一个栈帧。

我们可以这样理解:一个线程包含多个栈帧,而每个栈帧内部包含局部变量表、操作数栈、动态连接、返回地址等。

局部变量表

局部变量表是变量值的存储空间,我们调用方法时传递的参数,以及在方法内部创建的局部变量都保存在局部变量表中。在 Java 编译成 class 文件的时候,就会在方法的 Code 属性表中的 max_locals 数据项中,确定该方法需要分配的最大局部变量表的容量。

注意:系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值),也就是说不存在类变量那样的准备阶段。

操作数栈

操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。

同局部变量表一样,操作数栈的最大深度也在编译的时候写入方法的Code属性表中的max_stacks数据项中。栈中的元素可以是任意Java数据类型,包括long和double。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的。在方法执行的过程中,会有各种字节码指令被压入和弹出操作数栈(比如:iadd指令就是将操作数栈中栈顶的两个元素弹出,执行加法运算,并将结果重新压回到操作数栈中)。

动态链接

动态链接的主要目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。

在一个 class 文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其所在内存地址中的直接引用,而符号引用存在于方法区中。

Java 虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的就是为了支持方法调用过程中的动态连接(Dynamic Linking)。

返回地址

当一个方法开始执行后,只有两种方式可以退出这个方法:

  • 正常退出:指方法中的代码正常完成,或者遇到任意一个方法返回的字节码指令(如return)并退出,没有抛出任何异常。

  • 异常退出:指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。

无论当前方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行。而虚拟机栈中的“返回地址”就是用来帮助当前方法恢复它的上层方法执行状态。

一般来说,方法正常退出时,调用者的 PC 计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。

2. 本地方法栈(Native Method Stack)

本地方法栈和上面介绍的虚拟栈基本相同,只不过是针对本地(native)方法。在开发中如果涉及 JNI 可能接触本地方法栈多一些,在有些虚拟机的实现中已经将两个合二为一了(比如HotSpot)。

3. 程序计数器(Program Counter Register)

Java 程序是多线程的,CPU 可以在多个线程中分配执行时间片段。当某一个线程被 CPU 挂起时,需要记录代码已经执行到的位置,方便 CPU 重新执行此线程时,知道从哪行指令开始执行。这就是程序计数器的作用。

"程序计数器"是虚拟机中一块较小的内存空间,主要用于记录当前线程执行的位置。

实际上一些我们熟悉的恢复线程操作、分支操作、循环操作、跳转、异常处理等都需要依赖这个计数器来完成。

关于程序计数器还有几点需要格外注意:

  1. 在 Java 虚拟机规范中,对程序计数器这一区域没有规定任何 OutOfMemoryError 情况(或许是感觉没有必要吧)。

  2. 线程私有的,每条线程内部都有一个私有程序计数器。它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

  3. 当一个线程正在执行一个 Java 方法的时候,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。

4.堆(Heap)

Java 堆(Heap)是 JVM 所管理的内存中最大的一块,该区域唯一目的就是存放对象实例,几乎所有对象的实例都在堆里面分配,因此它也是 Java 垃圾收集器(GC)管理的主要区域,有时候也叫作"GC 堆"。

同时它也是所有线程共享的内存区域,因此被分配在此区域的对象如果被多个线程访问的话,需要考虑线程安全问题。

按照对象存储时间的不同,堆中的内存可以划分为新生代(Young)和老年代(Old),其中新生代又被划分为 Eden 和 Survivor 区。具体如下图所示:

  • 堆空间=新生代(1/3)+老年代(2/3)

  • 新生代= Eden(8/10)+from(1/10)+to(1/10)

图中不同的区域存放具有不同生命周期的对象。这样可以根据不同的区域使用不同的垃圾回收算法,从而更具有针对性,进而提高垃圾回收效率。

Ecen区

大多数情况下,对象会在新生代 Eden区中进行分配,当Eden区没有足够空间进行分配时,虚拟机会发起一次Minor GC,Minor GC 相比 Major GC更频繁,回收速度也更快。

通过Minor GC之后,Eden 会被清空,Eden区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到Survivor 的 From区(若 From区不够,则直接进入Old 区)。

Survivor区

Survivor区相当于是 Eden区和 Old区的一个缓冲,类似于我们交通灯中的黄灯。

Survivor 又分为2个区,一个是 From 区,一个是To 区。每次执行 Minor GC,会将Eden区和 From 存活的对象放到Survivor 的 To 区(如果To区不够,则直接进入Old 区)。

Survivor 的存在意义就是减少被送到老年代的对象,进而减少Major GC的发生。Survivor的预筛选保 证,只有经历16次 Minor GC还能在新生代中存活的对象,才会被送到老年代。

Old区

老年代占据着2/3的堆内存空间,只有在Major GC 的时候才会进行清理,每次GC都会触发"Stop-The-World"。

内存越大,STW的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记-整理算法

5. 方法区

方法区(Method Area)也是 JVM 规范里规定的一块运行时数据区。方法区主要是存储已经被 JVM 加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据。该区域同堆一样,也是被各个线程共享的内存区域。

注意:关于方法区,很多开发者会将其跟“永久区”混淆。

所以我在这里对这两个概念进行一下对比:

  • 方法区是 JVM 规范中规定的一块区域,但是并不是实际实现,切忌将规范跟实现混为一谈,不同的 JVM 厂商可以有不同版本的“方法区”的实现。

  • HotSpot 在 JDK 1.7 以前使用“永久区”(或者叫 Perm 区)来实现方法区,在 JDK 1.8 之后“永久区”就已经被移除了,取而代之的是一个叫作“元空间(metaspace)”的实现方式。

总结一下就是:

  • 方法区是规范层面的东西,规定了这一个区域要存放哪些数据。

  • 永久区或者是 metaspace 是对方法区的不同实现,是实现层面的东西。

内存溢出与内存泄漏

  • 内存泄漏:分配出去的内存回收不了

  • 内存溢出:指系统内存不够用了

堆溢出

可以分为:内存泄漏和内存溢出,这两种情况都会抛出OutOfMemoryError异常。

内存泄露

内存泄漏是指对象实例在新建和使用完毕后,仍然被引用,没能被垃圾回收释放,一直积累,直到没有剩余内存可用。如果内存泄露,我们要找出泄露的对象是怎么被GC ROOT引用起来,然后通过引用链来具体分析泄露的原因。

内存溢出

内存溢出是指当我们新建一个实力对象时,实例对象所需占用的内存空间大于堆的可用空间。如果出现了内存溢出问题,这往往是程序本生需要的内存大于了我们给虚拟机配置的内存,这种情况下,我们可以采用调大-Xmx来解决这种问题。

栈溢出

栈(JVM Stack)存放主要是栈帧( 局部变量表,操作数栈, 动态链接,方法出口信息)的地方。注意区分栈和栈帧:栈里包含栈帧。

与线程栈相关的内存异常有两个:

  • StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出(方法调用层次太深,内存不够新建栈帧) 。
  • OutOfMemoryError:当 Java 虚拟机动态扩展到无法申请足够内存时抛出(线程太多,内存不够新建线程) 。

StackOverflowError

递归调用是造成StackOverflowError的一个常见场景。因此当需要使用递归时,需要格外谨慎。

OutOfMemoryError

理论上,虚拟机栈、堆、方法区都有发生OutOfMemoryError的可能。但是实际项目中,大多发生于堆当中。

小结

千万不要将内存模型理解为虚拟机的"具体实现",虚拟机的具体实现有很多,比如 Sun 公司的 HotSpot、JRocket、IBM J9、以及我们非常熟悉的 Android Dalvik 和 ART 等。这些具体实现在符合上面 5 种运行时数据区的前提下,又各自有不同的实现方式。

JVM 的运行时内存结构中一共有两个"栈"和一个"堆",分别是:Java 虚拟机栈和本地方法栈,以及"GC堆"和方法区。除此之外还有一个程序计数器(几乎不会用到这一部分,忽略)。

JVM 内存中只有堆和方法区线程共享的数据区域,其它区域都是线程私有的。并且程序计数器是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

收起阅读 »

Java 类加载器

类加载器 ClassLoader。在Java 内存模型我们介绍了 Java 字节码文件(.class)的格式。一个完整的 Java 程序是由多个 .class 文件组成的,在程序运行过程中,需要将这些 .class 文件加载到 JVM 中才可以使用。而负责加载...
继续阅读 »

类加载器 ClassLoader。

Java 内存模型我们介绍了 Java 字节码文件(.class)的格式。一个完整的 Java 程序是由多个 .class 文件组成的,在程序运行过程中,需要将这些 .class 文件加载到 JVM 中才可以使用。而负责加载这些 .class 文件的就是类加载器(ClassLoader)。

Java 中的类何时被加载器加载

在 Java 程序启动的时候,并不会一次性加载程序中所有的 .class 文件,而是在程序的运行过程中,动态地加载相应的类到内存中。

通常情况下,Java 程序中的 .class 文件会在以下 2 种情况下被 ClassLoader 主动加载到内存中:

  • 调用类构造器

  • 调用类中的静态(static)变量或者静态方法

Java 中 ClassLoader

JVM 中自带 3 个类加载器:

  • 启动类加载器 BootstrapClassLoader

  • 扩展类加载器 ExtClassLoader (JDK 1.9 之后,改名为 PlatformClassLoader)

  • 系统加载器 APPClassLoader

以上 3 者在 JVM 中有各自分工,但是又互相有依赖。

类加载机制

类的加载指将编译好的 Class 类文件中的字节码读入内存中,将其放在方法区内并创建对应的 Class 对象。类的加载分为加载、链接、初始化,其中链接又包括验证、准备、解析三步。如下图所示。

  • 加载是文件到内存的过程。通过类的完全限定名查找此类字节码文件,并利用字节码文件创建一个 Class 对象。

  • 验证是对类文件内容验证。目的在于确保 Class 文件符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种:文件格式验证,元数据验证,字节码验证,符号引用验证。

  • 准备阶段是进行内存分配。为类变量也就是类中由 static 修饰的变量分配内存,并且设置初始值。这里要注意,初始值是 0 或者 null,而不是代码中设置的具体值,代码中设置的值是在初始化阶段完成的。另外这里也不包含用 final 修饰的静态变量,因为 final 在编译的时候就会分配。

  • 解析主要是解析字段、接口、方法。主要是将常量池中的符号引用替换为直接引用的过程。直接引用就是直接指向目标的指针、相对偏移量等。

  • 初始化,主要完成静态块执行与静态变量的赋值。这是类加载最后阶段,若被加载类的父类没有初始化,则先对父类进行初始化。

只有对类主动使用时,才会进行初始化,初始化的触发条件包括在创建类的实例时、访问类的静态方法或者静态变量时、Class.forName() 反射类时、或者某个子类被初始化时。

如上图所示,浅绿的两个部分表示类的生命周期,就是从类的加载到类实例的创建与使用,再到类对象不再被使用时可以被 GC 卸载回收。这里要注意一点,由 Java 虚拟机自带的三种类加载器加载的类在虚拟机的整个生命周期中是不会被卸载的,只有用户自定义的类加载器所加载的类才可以被卸载。

类加载器

如上图所示,Java 自带的三种类加载器分别是:BootStrap 启动类加载器、扩展类加载器和应用加载器(也叫系统加载器)。图右边的桔黄色文字表示各类加载器对应的加载目录。启动类加载器加载 java home 中 lib 目录下的类,扩展加载器负责加载 ext 目录下的类,应用加载器加载 classpath 指定目录下的类。除此之外,可以自定义类加载器。

BootstrapClassLoader 启动类加载器

它并不是使用 Java 代码实现的,而是由 C/C++ 语言编写的,它本身属于虚拟机的一部分。因此我们无法在 Java 代码中直接获取它的引用。

Bootstrap类加载器负责加载rt.jar 中的JDK类文件,它是所有类加载器的父加载器。Bootstrap类加载器没有任何父类加载器,如果尝试在 Java 层获取 BootstrapClassLoader(String.class.getClassLoader()) 的引用,系统会返回 null,任何基于此的代码会抛出NullPointerException 异常。Bootstrap加载器被称为初始类加载器。

ExtClassLoader 扩展类加载器

而Extension将加载类的请求先委托给它的父加载器,也就是Bootstrap,如果没有成功加载的话,再从 jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下加载类。Extension加载器由sun.misc.Launcher$ExtClassLoader 实现

APPClassLoader 系统类加载器

默认的加载器就是System类加载器(又叫作Application类加载器)了。它负责从classpath环境变量中加载某些应用相关的类,classpath环境变量通常由-classpath或-cp命令行选项来定义,或者是JAR中的Manifest的classpath 属性。Application类加载器是Extension类加载器的子加载器

双亲委派模式(Parents Delegation Model)

既然 JVM 中已经有了这 3 种 ClassLoader,那么 JVM 又是如何知道该使用哪一个类加载器去加载相应的类呢?答案就是:双亲委派模式

所谓双亲委派模式就是,当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说,只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程。

注意:"双亲委派"机制只是 Java 推荐的机制,并不是强制的机制。我们可以继承 java.lang.ClassLoader 类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写 findClass(name) 方法;如果想破坏双亲委派模型,可以重写 loadClass(name) 方法。

Custom ClassLoader

JVM 中预置的 3 种 ClassLoader 只能加载特定目录下的 .class 文件,如果我们想加载其他特殊位置下的 jar 包或类时(比如,我要加载网络或者磁盘上的一个 .class 文件),默认的 ClassLoader 就不能满足我们的需求了,所以需要定义自己的 Classloader 来加载特定目录下的 .class 文件。

自定义 ClassLoader 步骤:

  1. 自定义一个类继承抽象类 ClassLoader。

  2. 重写 findClass 方法。

  3. 在 findClass 中,调用 defineClass 方法将字节码转换成 Class 对象,并返回。

public class DiskClassLoader extends ClassLoader{
String filePath;
public DiskClassLoader(String filePath){
this.filePath = filePath;
}

@RequiresApi(api = Build.VERSION_CODES.O)
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = filePath+name+".class";
byte [] classBytes = null;
try {
classBytes = Files.readAllBytes(Paths.get(new URI(path)));
} catch (IOException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
}
return defineClass(name,classBytes,0,classBytes.length);
}
}

复制代码

注意:如没有特殊的要求,一般不建议重写loadClass搜索类的算法。

加载器小结

Java 的类加载使用双亲委派模式,即一个类加载器在加载类时,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器,如上图中蓝色向上的箭头。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。如图中的桔黄色向下的箭头。

这种双亲委派模式的好处,可以避免类的重复加载,另外也避免了 Java 的核心 API 被篡改。

Android 中的 ClassLoader

本质上,Android 和传统的 JVM 是一样的,也需要通过 ClassLoader 将目标类加载到内存,类加载器之间也符合双亲委派模型。但是在 Android 中, ClassLoader 的加载细节有略微的差别。

在 Android 虚拟机里是无法直接运行 .class 文件的,Android 会将所有的 .class 文件转换成一个 .dex 文件,并且 Android 将加载 .dex 文件的实现封装在 BaseDexClassLoader 中,而我们一般只使用它的两个子类:PathClassLoader 和 DexClassLoader。

DexClassLoader

先来看官方对 DexClassLoader 的描述:

A class loader that loads classes from {@code .jar} and {@code .apk} files containing a {@code classes.dex} entry. This can be used to execute code not installed as part of an application.

DexClassLoader 可以从 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的 dex 的加载。

DexClassLoader 的源码里面只有一个构造方法,代码如下:

public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
复制代码

参数说明:

  • dexPath:包含 class.dex 的 apk、jar 文件路径 ,多个路径用文件分隔符(默认是":")分隔。

  • optimizedDirectory:此参数已弃用,自 API 级别 26 起无效。

  • librarySearchPath:C/C++ native 库的路径,多个路径用文件分隔符分隔; 可能是null。

  • parent:父类加载器

PathClassLoader

PathClassLoader 用来加载系统 apk 和被安装到手机中的 apk 内的 dex 文件。它的 2 个构造函数如下:

public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}

public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}

@SystemApi(client = MODULE_LIBRARIES)
public PathClassLoader(
@NonNull String dexPath, @Nullable String librarySearchPath, @Nullable ClassLoader parent,
@Nullable ClassLoader[] sharedLibraryLoaders) {
super(dexPath, librarySearchPath, parent, sharedLibraryLoaders);
}
}

复制代码

参数说明:

  • dexPath:dex 文件路径,或者包含 dex 文件的 jar 包路径;

  • librarySearchPath:C/C++ native 库的路径,多个路径用文件分隔符分隔; 可能是null。

  • parent:父类加载器

PathClassLoader 里面除了上面这些以外就没有其他的代码了,具体的实现都是在 BaseDexClassLoader 里面,其 dexPath 比较受限制,一般是已经安装应用的 apk 文件路径。

当一个 App 被安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的,可以通过如下代码验证:

public class MainActivity extends ActivityBase {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MLog.e(this.getClass().getName(), "onCreate");
setContentView(R.layout.activity_main);
ClassLoader classLoader = MainActivity.class.getClassLoader();
MLog.e(classLoader.toString());
}
}
复制代码

打印结果如下:

2021-09-26 17:55:56.530 /com.scc.demo E/-SCC-com.scc.demo.actvitiy.MainActivity: onCreate
2021-09-26 17:55:56.770 /com.scc.demo E/-SCC-:
dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.scc.demo-0huSvtxqzKDw3GXvCu3P8g==/base.apk"],
nativeLibraryDirectories=[
/data/app/com.scc.demo-0huSvtxqzKDw3GXvCu3P8g==/lib/arm64,
/data/app/com.scc.demo-0huSvtxqzKDw3GXvCu3P8g==/base.apk!/lib/arm64-v8a,
/system/lib64,
/system/product/lib64]]]

复制代码

小结

  • ClassLoader 就是用来加载 class 文件的,不管是 jar 中还是 dex 中的 class。

  • Java 中的 ClassLoader 通过双亲委托来加载各自指定路径下的 class 文件。

  • 可以自定义 ClassLoader,一般覆盖 findClass() 方法,不建议重写 loadClass 方法。

  • Android 中常用的两种 ClassLoader 分别为:PathClassLoader 和 DexClassLoader。


收起阅读 »

Java 垃圾回收(GC)

前言垃圾回收(Garbage Collection,简写为 GC)可能是虚拟机众多知识点中最为大众所熟知的一个了,也是Java开发者最关注的一块知识点。Java 语言开发者比 C 语言开发者幸福的地方就在于,我们不需要手动释放对象的内存,JVM 中的垃圾回收器...
继续阅读 »

前言

垃圾回收(Garbage Collection,简写为 GC)可能是虚拟机众多知识点中最为大众所熟知的一个了,也是Java开发者最关注的一块知识点。Java 语言开发者比 C 语言开发者幸福的地方就在于,我们不需要手动释放对象的内存,JVM 中的垃圾回收器(Garbage Collector)会为我们自动回收。但是这种幸福是有代价的:一旦这种自动化机制出错,我们又不得不去深入理解 GC 回收机制,甚至需要对这些"自动化"的技术实施必要的监控和调节。

程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作,这几个区域内不需要过多考虑回收的问题。

而堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的就是这部分内存。

什么是垃圾

所谓垃圾就是内存中已经没有用的对象。 既然是"垃圾回收",那就必须知道哪些对象是垃圾。Java 虚拟机中使用一种叫作可达性分析的算法来决定对象是否可以被回收。

可达性分析

可达性分析算法是从离散数学中的图论引入的,JVM 把内存中所有的对象之间的引用关系看作一张图,通过一组名为”GC Root"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。如下图所示:

比如上图中,对象ObjA/ObjB/ObjC 与 GC Root 之间都存在一条直接或者间接的引用链,这也代表它们与 GC Root 之间是可达的,因此它们是不能被 GC 回收掉的。

而对象E和被对d 引用到,但是并不存在一条引用链连接它们与 GC Root,所以当 GC 进行垃圾回收时,只要遍历到 D/E/F 这 3 个对象,就会将它们回收。

注意:上图中圆形图标虽然标记的是对象,但实际上代表的是此对象在内存中的引用。包括 GC Root 也是一组引用而并非对象。

GC Root 对象

在 Java 中,有以下几种对象可以作为 GC Root:

  1. Java 虚拟机栈(局部变量表)中的引用的对象。
  2. 方法区中静态引用指向的对象。
  3. 仍处于存活状态中的线程对象。
  4. Native 方法中 JNI 引用的对象。

什么时候回收

不同的虚拟机实现有着不同的 GC 实现机制,但是一般情况下每一种 GC 实现都会在以下两种情况下触发垃圾回收。

  • Allocation Failure:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。

  • System.gc():在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。

如何回收垃圾

由于垃圾收集算法的实现涉及大量的程序细节,各家虚拟机厂商对其实现细节各不相同,因此本课时并不会过多的讨论算法的实现,只是介绍几种算法的思想以及优缺点。

标记清除算法(Mark and Sweep GC)

从"GC Roots"集合开始,将内存整个遍历一次,保留所有可以被 GC Roots 直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,过程分两步。

  1. Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
  2. Sweep 清除阶段:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清除。

如下图所示:

  • 优点:实现简单,不需要将对象进行移动。
  • 缺点:这个算法需要中断进程内其他组件的执行(stop the world),并且可能产生内存碎片,提高了垃圾回收的频率。

复制算法(Copying)

将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

  • 1.复制算法之前,内存分为 A/B 两块,并且当前只使用内存 A,内存的状况如下图。

  • 2.标记完之后,所有可达对象都被按次序复制到内存 B 中,并设置 B 为当前使用中的内存。内存状况如下图。

  • 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
  • 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

标记-压缩算法 (Mark-Compact)

需要先从根节点开始对所有可达对象做一次标记,之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。最后,清理边界外所有的空间。因此标记压缩也分两步完成:

  1. Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
  2. Compact 压缩阶段:将剩余存活对象按顺序压缩到内存的某一端。

  • 优点:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
  • 缺点:所谓压缩操作,仍需要进行局部对象移动,所以一定程度上还是降低了效率。

JVM分代回收策略

Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代,这就是 JVM 的内存分代策略。

注意: 在 HotSpot 中除了新生代和老年代,还有永久代

分代回收的中心思想就是:对于新创建的对象会在新生代中分配内存,此区域的对象生命周期一般较短。如果经过多次回收仍然存活下来,则将它们转移到老年代中。

年轻代(Young Generation)

新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间,回收效率很高。新生代中因为要进行一些复制操作,所以一般采用的 GC 回收算法是复制算法。

Ecen区

大多数情况下,对象会在新生代 Eden区中进行分配,当Eden区没有足够空间进行分配时,虚拟机会发起一次Minor GC,Minor GC 相比 Major GC更频繁,回收速度也更快。

通过Minor GC之后,Eden 会被清空,Eden区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到Survivor 的 From区(若 From区不够,则直接进入Old 区)。

Survivor区

Survivor区相当于是 Eden区和 Old区的一个缓冲,类似于我们交通灯中的黄灯。

Survivor 又分为2个区,一个是 From 区,一个是To 区。每次执行 Minor GC,会将Eden区和 From 存活的对象放到Survivor 的 To 区(如果To区不够,则直接进入Old 区)。

Survivor 的存在意义就是减少被送到老年代的对象,进而减少Major GC的发生。Survivor的预筛选保 证,只有经历16次 Minor GC还能在新生代中存活的对象,才会被送到老年代。

Old区

老年代占据着2/3的堆内存空间,只有在Major GC 的时候才会进行清理,每次GC都会触发"Stop-The-World"。

内存越大,STW的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记-整理算法

注意:对于老年代可能存在这么一种情况,老年代中的对象有时候会引用到新生代对象。这时如果要执行新生代 GC,则可能需要查询整个老年代上可能存在引用新生代的情况,这显然是低效的。所以,老年代中维护了一个 512 byte 的 card table,所有老年代对象引用新生代对象的信息都记录在这里。每当新生代发生 GC 时,只需要检查这个 card table 即可,大大提高了性能。

GC Log 分析

为了让上层应用开发人员更加方便的调试 Java 程序,JVM 提供了相应的 GC 日志。在 GC 执行垃圾回收事件的过程中,会有各种相应的 log 被打印出来。其中新生代和老年代所打印的日志是有区别的。

  • 新生代 GC:这一区域的 GC 叫作 Minor GC。因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
  • 老年代 GC:发生在这一区域的 GC 也叫作 Major GC 或者 Full GC。当出现了 Major GC,经常会伴随至少一次的 Minor GC。

注意:在有些虚拟机实现中,Major GC 和 Full GC 还是有一些区别的。Major GC 只是代表回收老年代的内存,而 Full GC 则代表回收整个堆中的内存,也就是新生代 + 老年代。

垃圾收集器

详解 CMS

基于分代回收理论,详细介绍几个典型的垃圾回收算法,先来看 CMS 回收算法。CMS 在 JDK1.7 之前可以说是最主流的垃圾回收算法。CMS 使用标记清除算法,优点是并发收集,停顿小。

从名字(包含"Mark Sweep")上就可以看出 CMS收集器是基于“标记-清除"算法实现的,它的运作过程相对于其他收集器来说要更复杂一些,整个过程分为4个步骤,包括:

  • 初始标记(CMS initial mark)
  • 并发标记(CMs concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

CMS 算法如下图所示。

 

  • 第一个阶段是初始标记,这个阶段会 stop the world,标记的对象只是从 root 集最直接可达的对象;

  • 第二个阶段是并发标记,这时 GC 线程和应用线程并发执行。主要是标记可达的对象;

  • 第三个阶段是重新标记阶段,这个阶段是第二个 stop the world 的阶段,停顿时间比并发标记要小很多,但比初始标记稍长,主要对对象进行重新扫描并标记;

  • 第四个阶段是并发清理阶段,进行并发的垃圾清理;

  • 最后一个阶段是并发重置阶段,为下一次 GC 重置相关数据结构。

G1 收集器

G1 在 1.9 版本后成为 JVM 的默认垃圾回收算法,G1 的特点是保持高回收率的同时,减少停顿。

G1 算法取消了堆中年轻代与老年代的物理划分,但它仍然属于分代收集器。G1 算法将堆划分为若干个区域,称作 Region,如下图中的小方格所示。一部分区域用作年轻代,一部分用作老年代,另外还有一种专门用来存储巨型对象的分区。

G1 也和 CMS 一样会遍历全部的对象,然后标记对象引用情况,在清除对象后会对区域进行复制移动整合碎片空间。

G1 回收过程如下。

  • G1 的年轻代回收,采用复制算法,并行进行收集,收集过程会 STW。

  • G1 的老年代回收时也同时会对年轻代进行回收。主要分为四个阶段:

    • 依然是初始标记阶段完成对根对象的标记,这个过程是STW的;

    • 并发标记阶段,这个阶段是和用户线程并行执行的;

    • 最终标记阶段,完成三色标记周期;

    • 复制/清除阶段,这个阶段会优先对可回收空间较大的 Region 进行回收,即 garbage first,这也是 G1 名称的由来。

G1 采用每次只清理一部分而不是全部的 Region 的增量式清理,由此来保证每次 GC 停顿时间不会过长。

总结如下,G1 是逻辑分代不是物理划分,需要知道回收的过程和停顿的阶段。此外还需要知道,G1 算法允许通过 JVM 参数设置 Region 的大小,范围是 1~32MB,可以设置期望的最大 GC 停顿时间等。有兴趣读者也可以对 CMS 和 G1 使用的三色标记算法做简单了解。

详解 ZGC

ZGC 特点

ZGC 是最新的 JDK1.11 版本中提供的高效垃圾回收算法,ZGC 针对大堆内存设计可以支持 TB 级别的堆,ZGC 非常高效,能够做到 10ms 以下的回收停顿时间。

这么快的响应,ZGC 是如何做到的呢?这是由于 ZGC 具有以下特点。

  • ZGC 使用了着色指针技术,我们知道 64 位平台上,一个指针的可用位是 64 位,ZGC 限制最大支持 4TB 的堆,这样寻址只需要使用 42 位,那么剩下 22 位就可以用来保存额外的信息,着色指针技术就是利用指针的额外信息位,在指针上对对象做着色标记。

  • 第二个特点是使用读屏障,ZGC 使用读屏障来解决 GC 线程和应用线程可能并发修改对象状态的问题,而不是简单粗暴的通过 STW 来进行全局的锁定。使用读屏障只会在单个对象的处理上有概率被减速。

  • 由于读屏障的作用,进行垃圾回收的大部分时候都是不需要 STW 的,因此 ZGC 的大部分时间都是并发处理,也就是 ZGC 的第三个特点。

  • 第四个特点是基于 Region,这与 G1 算法一样,不过虽然也分了 Region,但是并没有进行分代。ZGC 的 Region 不像 G1 那样是固定大小,而是动态地决定 Region 的大小,Region 可以动态创建和销毁。这样可以更好的对大对象进行分配管理。

  • 第五个特点是压缩整理。CMS 算法清理对象时原地回收,会存在内存碎片问题。ZGC 和 G1 一样,也会在回收后对 Region 中的对象进行移动合并,解决了碎片问题。

虽然 ZGC 的大部分时间是并发进行的,但是还会有短暂的停顿。来看一下 ZGC 的回收过程。

ZGC 回收过程

如下图所示,使用 ZGC 算法进行回收,从上往下看。初始状态时,整个堆空间被划分为大小不等的许多 Region,即图中绿色的方块。

开始进行回收时,ZGC 首先会进行一个短暂的 STW,来进行 roots 标记。这个步骤非常短,因为 roots 的总数通常比较小。

然后就开始进行并发标记,如上图所示,通过对对象指针进行着色来进行标记,结合读屏障解决单个对象的并发问题。其实,这个阶段在最后还是会有一个非常短的 STW 停顿,用来处理一些边缘情况,这个阶段绝大部分时间是并发进行的,所以没有明显标出这个停顿。

下一个是清理阶段,这个阶段会把标记为不在使用的对象进行回收,如上图所示,把橘色的不在使用的对象进行了回收。

最后一个阶段是重定位,重定位就是对 GC 后存活的对象进行移动,来释放大块的内存空间,解决碎片问题。

重定位最开始会有一个短暂的 STW,用来重定位集合中的 root 对象。暂停时间取决于 root 的数量、重定位集与对象的总活动集的比率。

最后是并发重定位,这个过程也是通过读屏障,与应用线程并发进行的。

总结

本课时着重讲解了 JVM 中有关垃圾回收的相关知识点,其中重点介绍了使用可达性分析来判断对象是否可以被回收,以及 3 种垃圾回收算法。最后通过分析 GC Log 验证了 Java 虚拟机中内存分配及分代策略的一些细节。

虚拟机垃圾回收机制很多时候都是影响系统性能、并发能力的主要因素之一。尤其是对于从事 Android 开发的工程师来说,有时候垃圾回收会很大程度上影响 UI 线程,并造成界面卡顿现象。因此理解垃圾回收机制并学会分析 GC Log 也是一项必不可少的技能。

收起阅读 »

Java多线程5 Callable、Future 和FutureTask

前言创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。而自从Ja...
继续阅读 »

前言

创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。
这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。
如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。
而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果

1 Callable介绍

Callable接口代表一段可以调用并返回结果的代码;Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。

Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法在线程池中执行Callable内的任务。由于Callable任务是并行的(并行就是整体看上去是并行的,其实在某个时间点只有一个线程在执行),我们必须等待它返回的结果。
java.util.concurrent.Future对象为我们解决了这个问题。在线程池提交Callable任务后返回了一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法让我们可以等待Callable结束并获取它的执行结果。

2 Future介绍

2.1 在Future接口中声明了5个方法,下面依次解释每个方法的作用:
方法作用
cance(boolean mayInterruptIfRunning)试图取消执行的任务,参数为true时直接中断正在执行的任务,否则直到当前任务执行完成,成功取消后返回true,否则返回false
isCancelled()方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
isDone()方法表示任务是否已经完成,若任务完成,则返回true;
get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
get(long timeout, TimeUnit unit)设定计算结果的返回时间,如果在规定时间内没有返回计算结果则抛出TimeOutException。
2.2 Future提供了三种功能:

1)判断任务是否完成;
2)能够中断任务;
3)能够获取任务执行结果。
因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。

3 FutureTask

我们先来看一下FutureTask的实现:


public class FutureTask<V> implements RunnableFuture<V>
复制代码

FutureTask类实现了RunnableFuture接口,我们看一下RunnableFuture接口的实现:


public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
复制代码

可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

FutureTask提供了2个构造器:


public FutureTask(Callable<V> callable) {
}
public FutureTask(Runnable runnable, V result) {
}
复制代码

事实上,FutureTask是Future接口的一个唯一实现类。

4 Future和FutureTask的使用

4.1 使用Callable+Future获取执行结果

public class CallableFutureTest {


public static void main(String[] args) {
//创建线程池
ExecutorService es = Executors.newSingleThreadExecutor();
//创建Callable对象任务
CallableDemo calTask = new CallableDemo();
//提交任务并获取执行结果
Future<Integer> future = es.submit(calTask);
//关闭线程池
es.shutdown();
try {
Thread.sleep(2000);
System.out.println("主线程在执行其他任务");

if (future.get() != null) {
//输出获取到的结果
System.out.println("future.get()-->" + future.get());
} else {
//输出获取到的结果
System.out.println("future.get()未获取到结果");
}

} catch (Exception e) {
e.printStackTrace();
}
System.out.println("主线程在执行完成");
}


}


class CallableDemo implements Callable<Integer> {

private int sum;

@Override
public Integer call() throws Exception {
System.out.println("Callable子线程开始计算啦!");
Thread.sleep(2000);

for (int i = 0; i < 100; i++) {
sum = sum + i;
}
System.out.println("Callable子线程计算结束!");
return sum;
}
}
复制代码

Callable子线程开始计算啦!
Callable子线程计算结束!
主线程在执行其他任务
future.get()-->4950
主线程在执行完成
复制代码
4.2 使用Callable+Future获取执行结果

public class CallableFutureTest {


public static void main(String[] args) {
// //创建线程池
// ExecutorService es = Executors.newSingleThreadExecutor();
// //创建Callable对象任务
// CallableDemo calTask=new CallableDemo();
// //提交任务并获取执行结果
// Future<Integer> future =es.submit(calTask);
// //关闭线程池
// es.shutdown();

//创建线程池
ExecutorService es = Executors.newSingleThreadExecutor();
//创建Callable对象任务
CallableDemo calTask = new CallableDemo();
//创建FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(calTask);
//执行任务
es.submit(futureTask);
//关闭线程池
es.shutdown();
try {
Thread.sleep(2000);
System.out.println("主线程在执行其他任务");

if (futureTask.get() != null) {
//输出获取到的结果
System.out.println("futureTask.get()-->" + futureTask.get());
} else {
//输出获取到的结果
System.out.println("futureTask.get()未获取到结果");
}

} catch (Exception e) {
e.printStackTrace();
}
System.out.println("主线程在执行完成");


}
}

class CallableDemo implements Callable<Integer> {

private int sum;

@Override
public Integer call() throws Exception {
System.out.println("Callable子线程开始计算啦!");
Thread.sleep(2000);

for (int i = 0; i < 100; i++) {
sum = sum + i;
}
System.out.println("Callable子线程计算结束!");
return sum;
}
}
复制代码

Callable子线程开始计算啦!
Callable子线程计算结束!
主线程在执行其他任务
futureTask.get()-->4950
主线程在执行完成
复制代码

但其实这两种方法最终是一样的:
第一种方式Callable+Future最终也是以Callable+FutureTask的形式实现的。
在第一种方式中调用了: Future future = executor.submit(task);
那就让我们看看executor.submit(task)的源码吧:


//java.util.concurrent.AbstractExecutorService类中
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/

public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);//可以看到源码中其实是在submit(Callable<T> task)内部创建了一个RunnableFuture<T>接口实现类
execute(ftask);
return ftask;
}
复制代码

而FutureTask又是RunnableFuture的实现类,那就再看看newTaskFor(Callable callable)里面干了什么:


 protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
复制代码

收起阅读 »

Java虚拟机系列五:android虚拟机

一.android虚拟机与hotspot虚拟机的区别android虚拟机非标准jvm实现存储和执行dex文件采用基于寄存器的指令集指令长度为2,4,6个字节,执行指令效率高,移植性差,依赖于平台hotspot虚拟机标准jvm实现存储和执行class文件采用基于...
继续阅读 »

一.android虚拟机与hotspot虚拟机的区别

android虚拟机

  • 非标准jvm实现
  • 存储和执行dex文件
  • 采用基于寄存器的指令集
  • 指令长度为2,4,6个字节,执行指令效率高,移植性差,依赖于平台

hotspot虚拟机

  • 标准jvm实现
  • 存储和执行class文件
  • 采用基于栈的指令集
  • 指令长度为1个字节,因此具备有很好的移植性。

二. dalvik和art

1.Dalvik虚拟机

是Google等厂商合作开发的Android移动设备平台的核心组成部分之一。它可以支持已转换为.dex(即DalvikExecutable)格式的Java应用程序的运行。

2.ART虚拟机(Android Runtime)

由Google公司研发,在Android5.0及后续Android版本中作为正式的运行时库取代了以往的Dalvik虚拟机。ART能够把应用程序的字节码转换为机器码,是Android所使用的一种新的虚拟机。

3.主要不同点

Dalvik采用的是JIT技术,而ART采用Ahead-of-time(AOT)技术。ART同时也改善了性能、垃圾回收、应用程序出错以及性能分析。

android 5.0之前默认采用dalvik虚拟机,android5.0之后采用art虚拟机。

4.jit 与aot

JIT通过进行连续的性能分析(记录函数的调用频率和时长,用于确定程序哪部分需要优化)来优化程序代码的执行,在程序运行的过程中,Dalvik虚拟机在不断的进行将字节码编译成机器码的工作。

AOT在应用程序安装的过程中,就已经将所有的字节码重新编译成了机器码。应用程序运行过程中无需进行实时的编译工作,只需要进行直接调用。

image

jit通过工具dexopt将dex文件转化为odex文件,odex相对于dex的优化内容包含指令修改为quick指令以及将fied_idx改为用offset来定位

aot通过工具dex2oat将dex转化为elf(ExecutableAndLinkableFormat文件,它是一种类Unix操作系统上的二进制文件格式标准。

三.android虚拟机栈

1.hotspot 虚拟机栈

jvm_stack_frame

hotspot虚拟机栈中以栈结构存储了代表运行时的方法的栈帧列表,对于虚拟机栈的详细介绍参考Java虚拟机系列三: 运行时数据区解析,这里我们重点关注与android虚拟机的区别点:它通过局部变量表和操作数栈来实现局部变量的存储与计算,并返回结果。

2.dalvik 寄存器

android虚拟机栈同样存储的是一系列的栈帧,不同的是栈帧中去除了操作比较繁琐的局部变量表和操作数栈,改为采用寄存器来存储指令,局部变量以及结果信息。

dalvik_register

基于寄存器的指令更长,同时能处理的内容也更多,能有效的减少io次数。

四.android虚拟机堆

1.hotspot虚拟机堆

在Java虚拟机系列四:堆区管理中,我们分析了标准jvm的堆区结构由新生代,老年代和元数据区组成,其中新生代又区分为eden区和suvivor区,经过gc之后在不同的区中流转。

jvm_heap

2.Art虚拟机堆

Art虚拟机充分考虑移动端的数据特性,如启动需要的公共的class文件,图片等。

具体来看,art虚拟机堆由四部分组成:

image space - zygote space - allocation space - largeObject space

image

1.ImageSpace

存放boot.oat文件,它不会执行堆的gc

2.zygoteSpace

连续地址空间,匿名共享内存,进行垃圾回收,管理Zygote进程在启动过程中预加载的类和创建的各种对象资源。

3.allocation space

与传统的hotspot虚拟机堆相似,app运行过程中的对象的分配都在这里。

4.largetObject space

离散地址空间,进行垃圾回收,用来分配一些大于12K的大对象。

五.art虚拟机的gc策略

1.三种gc策略

  • Sticky GC :只回收上一次GC到本次GC之间申请的内存。
  • Partial GC:局部垃圾回收,除了ImageSpace和ZygoteSpace空间以外的其他内存垃圾。
  • Full GC: 全局垃圾回收,除了ImageSpace之外的Space的内存垃圾

2.gc的三个阶段:

阶段一:首先会进行一次轻量级的GC,完成后尝试分配。如果分配失败,则选取下一个GC策略,再进行一次轻量级GC。每次GC完成后都尝试分配,直到三种GC策略都被轮询了一遍还是不能完成分配,则进入下一阶段。 阶段二:允许堆进行增长的情况下进行对象的分配。 阶段三:进行一次允许回收软引用的GC的情况下进行对象的分配。

3.内存泄漏分析工具

  • as memory profiler : 实时内存dump (hprof文件)
  • leak canary : 自动泄漏分析工具
  • mat : 内存分析工具
  • hprof-conv : hprof转换工具,将memoryprofiler的hprof文件转化为mat能识别的文件,工具位于android/sdk/platform-tools/
  • visual vm : 可视化的vm内存实时情况
  • visual gc : visual vm的gc插件

4.内存泄漏分析方法

1. 记录需要分析的两个状态的hprof文件

这两个状态一般是怀疑泄漏的页面,或者通过leakCanary提示的泄漏页面,假设为页面A有泄漏。

打开Android studio 的 memory profiler页面 进入需要分析的页面A之前的页面Main(假定这个状态为纯净状态s1),点击capture-heap-dump存储纯净状态的hprof文件为memory-s1.hprof,进出几次A页面,并点击gc按钮,最终退出A页面回到纯净页面main,点击capture-heap-dump存储当前状态的hprof文件为memory-s2.hprof

2.将hprof转化为mat能识别的文件

执行命令得到mat-s1.hprof , mat-s2.hprof

hprof-conv memory-s1.hprof mat-s1.hprof
hprof-conv momory-s2.hprof mat-s2.hprof

3.对比 hprof文件

用mat工具导入mat-s1.hprof ,mat-s2.hprof 在mat-s1.hprof页面点击右上角的diff按钮,选择mat-s2.hprof,得到多次进入页面A之后增加的对象列表。

mat_diff

如图可以清晰看到SecondActivity增加了7个,为泄漏的对象。

重新点开mat-s2.hprof文件,这里一定要重新点开,否则会找不到持有链接的入口,找到SecondActivity,右键点击merge-shortest-Paths-to-Gc-Roots就可以得到持有SecondeActivity的实际对象。 mat-link


收起阅读 »

iOS SwiftUI 创建和组合视图 3.1

第四节 使用可观察对象来存储数据要实现用户标记哪个地标为自己喜爱的地标这个功能,需要使用可观察对象(observalble object)存放地标数据可观察对象是一种可以绑定到具体SwifUI视图环境中的数据对象。SwiftUI可以察觉它影响视图展示的任何变化...
继续阅读 »

第四节 使用可观察对象来存储数据

要实现用户标记哪个地标为自己喜爱的地标这个功能,需要使用可观察对象(observalble object)存放地标数据

可观察对象是一种可以绑定到具体SwifUI视图环境中的数据对象。SwiftUI可以察觉它影响视图展示的任何变化,并在这种变化发生后及时更新对应视图的展示内容

observable

步骤1 创建一个名为UserData.swift的文件

步骤2 声明一个遵循ObservableObject协议的新数据模型,ObservableObject协议来自响应式框架Combine。SwiftUI可以订阅可观察对象,并在数据发生改变时更新视图的显示内容

步骤3 添加存储属性showFavoritesOnlylandmarks,并赋予初始值。可观察对象需要对外公布内部数据的任何改动,因此订阅此可观察对象的订阅者就可以获得对应的数据改动信息

步骤4 给新建的数据模型的每一个属性添加@Published属性修饰词

combine

第五节 视图中适配数据模型对象

已经创建了UserData可观察对象,现在要改造视图,让它使用这个新的数据模型来存储视图内容数据

model

步骤1 在LandmarkList.swift文件中,使用@EnvironmentObject修饰的userData属性来替换原来的showFavoritesOnly状态属性,并对预览视图调用environmentObject(_:)修改器。只要environmentObject(_:)修改器应用在视图的父视图上,userData就能够自动获取它的值。

步骤2 替换原来使用showFavoritesOnly状态属性的地方,改为使用userData中的对应属性。与@State修饰的属性一样,也可以使用$前缀访问userData对象的成员绑定引用

步骤3 创建ForEach实例时使用userData.landmarks做为数据源

envrionment object

步骤4 在SceneDelegate.swift中,对LandmarkList视图调用environmentObject修改器,这样可以把UserData的数据对象绑定到LandmarkList视图的环境变量中,子视图可以获得父视图环境中的变量。此时如果在模拟器或者真机上运行应用,也可以正常展示视图内容

scene delegate

步骤5 更新LandmarkDetail视图,让它从父视图的环境变量中取要展示的数据。之后在更新地标的用户喜爱状态时,会用到landmarkIndex这个变量

landmark detail environment

步骤6 切换到LandmarkList.swift文件,并打开实时预览视图去验证所添加的功能是否正常工作

landmark list environment

第六节 为每一个地标创建一个喜爱按钮

Landmark这个应用可以在喜欢和不喜欢的地标列表间进行切换了,但喜欢的地标列表还是硬编码形成的,为了让用户可以自己标记哪个地标是自己喜欢的,需要在地标详情页添加一个标记喜欢的按钮

favorite button

步骤1 在LandmarkDetail.swiftHStack中添加地标名称的Text

步骤2 在地标名称的Text控件旁边添加一个新的按钮控件。使用if-else条件语句设置不同的图片显示状态表示这个地标是否被用户标记为喜欢。在Button的动作闭包中,使用了landmarkIndex去修改userData中对应地标的数据。

favorite star button

步骤3 切换到landmarkList.swift,并开启实时预览模式。当从列表页导航进入详情页后,点击喜欢按钮,喜欢的状态会在返回列表页后与列表中对应的地标喜欢状态保持一致,因为列表页和详情页的地标数据使用的是同一份,所以可以在不同页面间保持状态同步。

star button completed

检查是否理解

问题1 下列选项哪个可以把数据按视图层级关系传递下去?

  •  @EnvironmentObject属性
  •  environmentObject(_:)修改器

问题2 绑定(binding)的作用是什么?

  •  绑定是值和改变值的方法
  •  是一个视图连接在一起的方法,确保连续起来的视图接收同一份数据
  •  是一个临时固化值的方式,目的是在其它视图状态变化时,保持值不改变
收起阅读 »

iOS SwiftUI 创建和组合视图 3.0

处理用户输入在Landmark应用中,标记喜爱的地方,过滤地标列表,只显示喜欢的地标。要增加这些特性,首先要在列表上添加一个开关,用来过滤用户喜欢的地标。在地标上添加一个星标按钮,用户可以点击它来标记这个地标为自己喜欢的。下载工程文件并且跟着下面的教程实践&n...
继续阅读 »

处理用户输入

Landmark应用中,标记喜爱的地方,过滤地标列表,只显示喜欢的地标。要增加这些特性,首先要在列表上添加一个开关,用来过滤用户喜欢的地标。在地标上添加一个星标按钮,用户可以点击它来标记这个地标为自己喜欢的。

下载工程文件并且跟着下面的教程实践


第一节 标记用户最喜欢的地标

mark-favorite

给地标列表的每一行添加一个星标用来表示用户是否标记该地标为自己喜欢的

步骤1 打开工程项目,在项目导航下选择LandmarkRow.swift文件

步骤2 在空白占位后面添加一个if表达式,if表达式判断是否当前地标是用户喜欢的,如果用户标记当前地标为喜欢就显示星标。可以在SwitUI的代码块中使用if语句来条件包含视图

步骤3 由于系统图片是矢量类型的,可以使用foregroundColor(_:)来改变它的颜色。当地标landmark的isFavorite属性为真时,星标显示,稍后会讲怎么修改属性值。

star

第二节 过滤列表

可以定制地标列表,让它只显示用户喜欢的地标,或者显示所有的地标。要实现这个功能,需要给LandmarkList视图类型添加一些状态变量。

状态(State)是一个值或者一个值的集合,会随着时间而改变,同时会影响视图的内容、行为或布局。在属性前面加上@State修饰词就是给视图添加了一个状态值

state

步骤1 选择LandmarkList.swift文件,并给LandmarkList添加一个名为showFavoritesOnly的状态,初始值设置为false

步骤2 点击Resume按钮或快捷键Command+Option+P刷新画布。当对视图进行添加或修改属性等结构性改变时,需要手动刷新画布

步骤3 代码中通过检查showFavoritesOnly属性和每一个地标的isFavorite属性值来过滤地标列表所展示的内容

state favorite

第三节 添加控件来切换状态

为了让用户控制地标列表的过滤器,需要添加一个可以修改showFavoritesOnly值的控件,传递一个绑定关系给toggle控件可以实现

一个绑定关系(binding)是对可变状态的引用。当用户点击toggle控件,从开到关或从关到开,toggle控件会通过绑定关系对应的更新视图的状态

toggle state

步骤1 创建一个嵌套的ForEach组来把地标数据转换成地标行视图。在一个列表中组合静态和动态视图,或者组合两个甚至多个不同的动态视图组,使用ForEach类型动态生成而不是给列表传入数据集合生成列表视图

步骤2 添加一个Toggle视图作为列表的每一个子视图,传入一个showFavoritesOnly的绑定关系。使用$前缀来获得一个状态变量或属性的绑定关系

步骤3 实时预览模式下,点击Toggle控件来验证过滤器的功能

toggle binding

live preview


收起阅读 »

iOS SwiftUI 创建和组合视图 2.2

第七节 子视图传入数据LandmarkDetail视图目前还是使用写死的数据进行展示,与LandmarkRow视图一样,LandmarkDetail视图及它内部的子视图也需要传入landmark数据,并使用它来进行实际的展示从LandmarkDetail的子视...
继续阅读 »

第七节 子视图传入数据

LandmarkDetail视图目前还是使用写死的数据进行展示,与LandmarkRow视图一样,LandmarkDetail视图及它内部的子视图也需要传入landmark数据,并使用它来进行实际的展示

LandmarkDetail的子视图(CircleImageMapView)开始,需要把它们都改造成为使用传入的数据进行展示,而不是在布局代码中写死数据展示

pass data

步骤1 在CircleImage.swift文件中,添加一个存储属性,命名为image。这是一种在构建SwiftUI视图中很常用的模式,常常会包裹或封装一些属性修改器。

circle image data

步骤2 更新CirleImage的预览结构体,并传入Turtle Rock这个图片进行预览

circle image preview

步骤3 在MapView.swift中添加一个coordinate属性,并使用这个属性来替换写死的经纬度坐标

map view data

步骤4 更新MapView的预览结构体,并传入每一个地标的经纬度数据

map view preview

步骤5 在LandmarkDetail.swift中添加landmark属性。

步骤6 更新LandmarkDetail预览结构体,并传入第一个地标的数据

步骤7 把对应子视图的数据传入

landmark detail

步骤8 最后调用navigationBarTitle(_:displayMode:)修改器为地标详情页展示时在导航条上设置一个标题

landmark detail preview

步骤9 在SceneDelegate.swift中把应用的根视图替换为LandmarkList。应用在模拟器中独立启动时使用SceneDelegate的根视图做为第一个展示的视图

scene delegate root view

步骤10 在LandmarkList.swift中,传入当前行的地标数据到地标详情页LandmarkDetail

landmark list data

步骤11 切换到实时预览模式下去查看从地标列表页对应的行跳转到对应地标详情页是否正常

landmark list preview

第八节 动态生成预览视图

dynamic preivew

接下来要在不同尺寸设备上展示不同的预览视图,默认情况下,预览视图会选择当前Scheme选中的设备尺寸进行渲染,可以使用previewDevice(_:)修改器来改变预览视图的设备

步骤1 改变当前预览列表,让它渲染在iPhone SE设备上。可以使用Xcode Scheme菜单上的设备名称来指定渲染设备。

iPhone SE Preview

步骤2 在列表的预览视图中,还可以把LandmarkList嵌套进入ForEach实例中,使用设备数组名作为数据。ForEach运算作用在集合类型的数据上,就和列表使用集合类型数据一样,可以在子视图使用的任何场景下使用ForEach,例如:stacklistgroup等。当元素数据是简单值类型时(例如字符串类型),可以使用\.self作为keypath去标识

preiview multiple device

步骤3 使用previewDisplayName(_:)修改器可以给预览视图添加设备标签

步骤4 可以在画布上多设置几个设备进行预览,比较不同设备下视图的展示情况

preivew multiple devices

检查是否理解

问题1 除了List外,下面哪种类型可以从集合数据中展示动态列表视图

  •  Group
  •  ForEach
  •  UITableView

问题2 可以从遵循了Identifiable协议的集合数据创建列表视图。但如果集合数据不遵循Identifiable协议,还有什么办法可以创建列表视图?

  •  在集合数据上调用map(_:)方法
  •  在集合数据上调用sorted(by:)方法
  •  给List(_:id:)类型传入集合数据的同时,使用keypath指定一个唯一标识符字段

问题3 使用什么类型才能让列表的行实现点击跳转到其它视图页面?

  •  NavigationLink
  •  UITableViewDelegate
  •  NavigationView

问题4 下面哪种方式不是用来设置预览设备的?

  •  改变活动scheme中选中的模拟器
  •  在画面设置中设置一个不同的预览设备
  •  使用previewDevice(_:)指定一个或多个预览设备
  •  连接开发机并点击设备预览按钮
收起阅读 »

iOS SwiftUI 创建和组合视图 2.1

第四节 创建地标列表使用SwiftUI列表类型可以展示平台相关的列表视图。列表的元素可以是静态的,类似于栈内部的子视图,也可以是动态生成的视图,也可以混合动态和静态的视图。步骤1 创建SwiftUI视图,命名为LandmarkList.swift步骤...
继续阅读 »

第四节 创建地标列表

使用SwiftUI列表类型可以展示平台相关的列表视图。列表的元素可以是静态的,类似于栈内部的子视图,也可以是动态生成的视图,也可以混合动态和静态的视图。

landmark list

步骤1 创建SwiftUI视图,命名为LandmarkList.swift

步骤2 用List替换默认创建的Text,并将前两个LandmarkRow实例做为列表的子元素,预览视图中会以列表的形式展示出两个地标

landmark list file create

landmark list landmark list tow rows

第五节 创建动态列表

除了单独列出列表中的每个元素外,列表还可以从一个集合中动态的生成。

landmark list dynamic

创建列表时可以传入一个集合数据和一个闭包,闭包会针对每一个数据元素返回一个视图,这个视图就是列表的行视图。

步骤1 从列表中移除两个静态指定的行视图,给列表初始化器传入landmarkData数据,列表要配合可辨别的数据类型使用。想让数据变成可辨别的数据类型有两种方法:

  1. 传入一个keypath指定数据中哪一个字段用来唯一标识这个数据元素。

  2. 让数据遵循Identifiable协议

步骤2 在闭包中返回一个LandmarkRow视图,List初始化器中指定数据集合landmarkData和唯一标识符**keypath:**\.id,这样列表就会动态生成,如下图所示

keypath identifier list data

步骤3 切换到文件Landmark.swfit,声明Landmark类型遵循Identifiable协议,因为Landmark类型已经定义了id属性,正好满足Identifiable协议,所以不需要添加其它代码

identifiable data

步骤4 现在切换回文件LandmarkList.swift,移除keypath\.id,因为landmarkData数据集合的元素已经遵循了Identifiable协议,所以在列表初始化器中可以直接使用,不需要手动标明数据的唯一标识符了



第六节 设置从列表页到详情页的页面导航

地标列表可以正常渲染展示,但是列表的元素点击后没有反应,跳转不到地标详情页。现在就要给列表添加导航能力,把列表视图嵌套到NavigationView视图中,然后把列表的每一个行视图嵌套进NavigationLink视图中,就可以建立起从地标列表视图到地标详情页的跳转。

landmark list to detail

步骤1 把动态生成的列表视图嵌套进一个NavigationView视图中

embed in navigation view

步骤2 调用navigationBarTitle(_:)修改器设置地标列表显示时的导航条标题

landmark list navigation view

步骤3 在列表的闭包中,将每一个行元素包裹在NavigationLink中返回,并指定LandmarkDetail视图为目标视图

navigation link

步骤4 切换到实时预览模式下可以直接点击地标列表的任意一行,现在就可以跳转到地标详情页了。

list navigation

identifiable list

收起阅读 »

有了for循环 为什么还要forEach?

js中那么多循环,for for...in for...of forEach,有些循环感觉上是大同小异今天我们讨论下for循环和forEach的差异。 我们从几个维度展开讨论:for循环和forEach的本质区别。for循环和forEach的语法区别。for循...
继续阅读 »

js中那么多循环,for for...in for...of forEach,有些循环感觉上是大同小异今天我们讨论下for循环和forEach的差异。

我们从几个维度展开讨论:

for循环和forEach的本质区别。

for循环和forEach的语法区别。

for循环和forEach的性能区别。



本质区别



for循环是js提出时就有的循环方法。forEach是ES5提出的,挂载在可迭代对象原型上的方法,例如Array Set Map

forEach是一个迭代器,负责遍历可迭代对象。那么遍历迭代可迭代对象分别是什么呢。

遍历:指的对数据结构的每一个成员进行有规律的且为一次访问的行为。

迭代:迭代是递归的一种特殊形式,是迭代器提供的一种方法,默认情况下是按照一定顺序逐个访问数据结构成员。迭代也是一种遍历行为。

可迭代对象:ES6中引入了 iterable 类型,Array Set Map String arguments NodeList 都属于 iterable,他们特点就是都拥有 [Symbol.iterator] 方法,包含他的对象被认为是可迭代的 iterable


未标题-2.png


在了解这些后就知道 forEach 其实是一个迭代器,他与 for 循环本质上的区别是 forEach 是负责遍历(Array Set Map)可迭代对象的,而 for 循环是一种循环机制,只是能通过它遍历出数组。

再来聊聊究竟什么是迭代器,还记得之前提到的 Generator 生成器,当它被调用时就会生成一个迭代器对象(Iterator Object),它有一个 .next()方法,每次调用返回一个对象{value:value,done:Boolean}value返回的是 yield 后的返回值,当 yield 结束,done 变为 true,通过不断调用并依次的迭代访问内部的值。

迭代器是一种特殊对象。ES6规范中它的标志是返回对象的 next() 方法,迭代行为判断在 done 之中。在不暴露内部表示的情况下,迭代器实现了遍历。看代码


let arr = [1, 2, 3, 4]  // 可迭代对象
let iterator = arr[Symbol.iterator]() // 调用 Symbol.iterator 后生成了迭代器对象
console.log(iterator.next()); // {value: 1, done: false} 访问迭代器对象的next方法
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 4, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

我们看到了。只要是可迭代对象,调用内部的 Symbol.iterator 都会提供一个迭代器,并根据迭代器返回的next 方法来访问内部,这也是 for...of 的实现原理。


let arr = [1, 2, 3, 4]
for (const item of arr) {
console.log(item); // 1 2 3 4
}

把调用 next 方法返回对象的 value 值并保存在 item 中,直到 valueundefined 跳出循环,所有可迭代对象可供for...of消费。 再来看看其他可迭代对象:


function num(params) {
console.log(arguments); // Arguments(6) [1, 2, 3, 4, callee: ƒ, Symbol(Symbol.iterator): ƒ]
let iterator = arguments[Symbol.iterator]()
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 4, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
}
num(1, 2, 3, 4)

let set = new Set('1234')
set.forEach(item => {
console.log(item); // 1 2 3 4
})
let iterator = set[Symbol.iterator]()
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 4, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

所以我们也能很直观的看到可迭代对象中的 Symbol.iterator 属性被调用时都能生成迭代器,而 forEach 也是生成一个迭代器,在内部的回调函数中传递出每个元素的值。

(感兴趣的同学可以搜下 forEach 源码, Array Set Map 实例上都挂载着 forEach ,但网上的答案大多数是通过 length 判断长度, 利用for循环机制实现的。但在 Set Map 上使用会报错,所以我认为是调用的迭代器,不断调用 next,传参到回调函数。由于网上没查到答案也不妄下断言了,有答案的人可以评论区给我留言)



for循环和forEach的语法区别



了解了本质区别,在应用过程中,他们到底有什么语法区别呢?



  1. forEach 的参数。

  2. forEach 的中断。

  3. forEach 删除自身元素,index不可被重置。

  4. for 循环可以控制循环起点。


forEach 的参数


我们真正了解 forEach 的完整传参内容吗?它大概是这样:


arr.forEach((self,index,arr) =>{},this)

self: 数组当前遍历的元素,默认从左往右依次获取数组元素。

index: 数组当前元素的索引,第一个元素索引为0,依次类推。

arr: 当前遍历的数组。

this: 回调函数中this指向。


let arr = [1, 2, 3, 4];
let person = {
name: '技术直男星辰'
};
arr.forEach(function (self, index, arr) {
console.log(`当前元素为${self}索引为${index},属于数组${arr}`);
console.log(this.name+='真帅');
}, person)

我们可以利用 arr 实现数组去重:


let arr1 = [1, 2, 1, 3, 1];
let arr2 = [];
arr1.forEach(function (self, index, arr) {
arr.indexOf(self) === index ? arr2.push(self) : null;
});
console.log(arr2); // [1,2,3]

image.png


forEach 的中断


在js中有break return continue 对函数进行中断或跳出循环的操作,我们在 for循环中会用到一些中断行为,对于优化数组遍历查找是很好的,但由于forEach属于迭代器,只能按序依次遍历完成,所以不支持上述的中断行为。


let arr = [1, 2, 3, 4],
i = 0,
length = arr.length;
for (; i < length; i++) {
console.log(arr[i]); //1,2
if (arr[i] === 2) {
break;
};
};

arr.forEach((self,index) => {
console.log(self);
if (self === 2) {
break; //报错
};
});

arr.forEach((self,index) => {
console.log(self);
if (self === 2) {
continue; //报错
};
});

如果我一定要在 forEach 中跳出循环呢?其实是有办法的,借助try/catch


try {
var arr = [1, 2, 3, 4];
arr.forEach(function (item, index) {
//跳出条件
if (item === 3) {
throw new Error("LoopTerminates");
}
//do something
console.log(item);
});
} catch (e) {
if (e.message !== "LoopTerminates") throw e;
};

若遇到 return 并不会报错,但是不会生效


let arr = [1, 2, 3, 4];

function find(array, num) {
array.forEach((self, index) => {
if (self === num) {
return index;
};
});
};
let index = find(arr, 2);// undefined

forEach 删除自身元素,index不可被重置


forEach 中我们无法控制 index 的值,它只会无脑的自增直至大于数组的 length 跳出循环。所以也无法删除自身进行index重置,先看一个简单例子:


let arr = [1,2,3,4]
arr.forEach((item, index) => {
console.log(item); // 1 2 3 4
index++;
});

index不会随着函数体内部对它的增减而发生变化。在实际开发中,遍历数组同时删除某项的操作十分常见,在使用forEach删除时要注意。


for 循环可以控制循环起点


如上文提到的 forEach 的循环起点只能为0不能进行人为干预,而for循环不同:


let arr = [1, 2, 3, 4],
i = 1,
length = arr.length;

for (; i < length; i++) {
console.log(arr[i]) // 2 3 4
};

那之前的数组遍历并删除滋生的操作就可以写成


let arr = [1, 2, 1],
i = 0,
length = arr.length;

for (; i < length; i++) {
// 删除数组中所有的1
if (arr[i] === 1) {
arr.splice(i, 1);
//重置i,否则i会跳一位
i--;
};
};
console.log(arr); // [2]
//等价于
var arr1 = arr.filter(index => index !== 1);
console.log(arr1) // [2]


for循环和forEach的性能区别



在性能对比方面我们加入一个 map 迭代器,它与 filter 一样都是生成新数组。我们对比 for forEach map 的性能在浏览器环境中都是什么样的:

性能比较:for > forEach > map

在chrome 62 和 Node.js v9.1.0环境下:for 循环比 forEach 快1倍,forEachmap 快20%左右。
原因分析

for:for循环没有额外的函数调用栈和上下文,所以它的实现最为简单。

forEach:对于forEach来说,它的函数签名中包含了参数和上下文,所以性能会低于 for 循环。

mapmap 最慢的原因是因为 map 会返回一个新的数组,数组的创建和赋值会导致分配内存空间,因此会带来较大的性能开销。如果将map嵌套在一个循环中,便会带来更多不必要的内存消耗。

当大家使用迭代器遍历一个数组时,如果不需要返回一个新数组却使用 map 是违背设计初衷的。在我前端合作开发时见过很多人只是为了遍历数组而用 map 的:


let data = [];
let data2 = [1,2,3];
data2.map(item=>data.push(item));

写在最后:这是我面试遇到的一个问题,当时只知道语法区别。并没有从可迭代对象迭代器生成器性能方面,多角度进一步区分两者的异同,我也希望我能把一个简单的问题从多角度展开细讲,让大家正在搞懂搞透彻。

收起阅读 »

如何在你的项目中使用新的ES规范

JavaScript 和 ECMAScript 的关系 JavaScript 是一种高级的、编译型的编程语言。而 ECMAScript 是一种规范。 JavaScript 是基于 ECMAScript 规范的脚本语言。ECMAScript(以下简称 ES)在 ...
继续阅读 »

JavaScript 和 ECMAScript 的关系


JavaScript 是一种高级的、编译型的编程语言。而 ECMAScript 是一种规范。


JavaScript 是基于 ECMAScript 规范的脚本语言ECMAScript(以下简称 ES)在 2015 年发布了 ES6(ECMAScript 2015),而且 TC39 委员会决定每年发布一个 ECMAScript 的版本,也就是我们看到的 ES6/7/8/9/11/12


ES11 中两个非常有用的特性


空值合并操作符(??)


Nullish coalescing Operator(空值处理)只有 null 和 undefined 的时候才认为真的是空。
以下为使用方式:


let user = {
u1: 0,
u2: false,
u3: null,
u4: undefined
u5: '',
}
let u1 = user.u1 || '用户1' // 用户1
let u2 = user.u2 || '用户2' // 用户2
let u3 = user.u3 || '用户3' // 用户3
let u4 = user.u4 || '用户4' // 用户4
let u5 = user.u5 || '用户5' // 用户5
// es11语法【只有 null 和 undefined 的时候才认为真的是空】
let u1 = user.u1 ?? '用户1' // 0
let u2 = user.u2 ?? '用户2' // false
let u3 = user.u3 ?? '用户3' // 用户3
let u4 = user.u4 ?? '用户4' // 用户4
let u5 = user.u5 ?? '用户5' // ''

应用的场景:后端返回的数据中 null 和 0 表示的意义可能是不一样的,null 表示为空,展示成 /。0 还是有数值,展示为 0。


let a = 0;
console.log(a ?? '/'); // 0
a = null;
console.log(a ?? '/'); // '/'

Optional chaining(可选链)



可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之处在于,在引用为空(nullish ) (null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined。



这个有点类似于 lodash 工具库中的 get 方法


let user = {
age: 18
}
let u1 = user.childer.name // TypeError: Cannot read property 'name' of undefined
// es11 语法
let u1 = user.childer?.name // undefined

浏览器兼容性问题


虽然 ES 新的特性十分好用,但需要注意的是它们的兼容性问题。
比如,可选链目前的兼容性如下:



解决方法就是讲 ES 新特性的语法转换成 ES5 的语法。


使用 Babel 进行转换


Babel 是一个 JavaScript 编译器。它的输入是下一代 JavaScript 语法书写的代码,输出浏览器兼容的 JavaScript 代码。


我们可以通过 Babel 中的转换插件来进行语法转换。比如我们上面两个语法可以通过以下两个插件进行转换。


空值合并操作符。@babel/plugin-proposal-nullish-coalescing-operator


使用:


npm install --save-dev @babel/plugin-proposal-nullish-coalescing-operator

在配置文件中:


{
"plugins": ["@babel/plugin-proposal-nullish-coalescing-operator"]
}

就可以做到以下的转换,输入:


var foo = object.foo ?? "default";

输出:


var _object$foo;

var foo =
(_object$foo = object.foo) !== null && _object$foo !== void 0
? _object$foo
: "default";

同理,可选链操作符可以看 @babel/plugin-proposal-optional-chaining,还有其他的都可以在 @babel/preset-env 目录中找到。


测试


在 Firefox 中,下载比较老的版本。



const foo = null ?? 'default string';
console.log(foo);
// expected output: "default string"

const baz = 0 ?? 42;
console.log(baz);

运行上面的代码,报错:



项目中使用,成功。说明 polyfil 成功了。



总结


JavaScript 是基于 ECMAScript 规范的脚本语言。ECMAScript 规范的发展给前端开发带来了很多的便利,但我们在使用的时候应该使用 Babel 这种 JavaScript 编译器将其转换成浏览器兼容的代码。


作者:Gopal
链接:https://juejin.cn/post/7018174628090609701

收起阅读 »

Vue 3 凉凉了吗 - 10 个灵魂拷问

vue
很多人问我,现在可以用 Vue 3 了吗,Vue 2升级成本高吗,我想借着早早聊的场子把大家经常问的问题,跟大家谈一谈我的看法,我会尽量公平公正,客观正向,但尽然是看法,难免会有一些有争议的地方,或者不认可的地方,你可以留言。我总结了 10 个问题,期望能帮助...
继续阅读 »

很多人问我,现在可以用 Vue 3 了吗,Vue 2升级成本高吗,我想借着早早聊的场子把大家经常问的问题,跟大家谈一谈我的看法,我会尽量公平公正,客观正向,但尽然是看法,难免会有一些有争议的地方,或者不认可的地方,你可以留言。我总结了 10 个问题,期望能帮助你在技术选型中起到一定的帮助:


0、升级 Vue 3 成本大吗


可大可小,如果你使用的Vue推荐的 template 语法,成本是非常小的,改个版本号就已经可以 run 起来了,但前提是它的周边库你也已经升级了,不信你试试,其实周边库的升级成本不能算做 Vue 3 的升级成本,因为就算没有 Vue 3,周边也会不断升级,只是刚好撞到了一起,然后这个锅就让 Vue 一起背了。


关于升级这方面我想也可以借鉴 Ant Design Vue 的渐进式升级姿势,Ant Design Vue 2.x 版本是改了少量的代码,让其在 Vue 3 下运行,然后慢慢的使用Composition API 迭代重构,对于简单的没有破坏性更新的继续在 2.x 下迭代小版本,然后将有大量重构代码,复杂度高的,有破坏性更新的在 3.x 版本上迭代升级。


业务代码的更新升级相较于组件库成本会更加可控,因为组件库一般都会用到一些黑科技或非文档API等等,会让升级成本变高,这也是为什么一些组件库迟迟不兼容 Vue3 的部分原因。


1、Vue 3 稳定了吗?


目前 Vue 3 已经相当稳定,除非你会用到各种黑科技,业务项目不应该有黑科技,如果用到了,千万不要写到简历里,相信我,“黑科技”不但不会加分,还会减分,因为所谓的黑科技,大概率是你写法就不对。不服来辩


2、Vue 3 生态不丰富?


你所谓的生态是指哪些?状态管理?路由管理?国际化?组件库?SSR?常用生态库都已经提供了 Vue 3 版本,而且 Vue 2 版本都在逐渐减少维护时间。


退一步讲,如果还不够,那可是造轮子,刷 KPI 的好机会,不是嘛。


3、Vue 3 的写法不习惯?


Composition API 只是可选项,你依然可以用 Option API,没有什么变化。但我们应该跳出舒适圈,拥抱未来,拥抱更好的东西。


4、Vue 3 好找工作吗?


前端技术风口已经不多了,Vue 3 算一个,遥想当年懂个生命周期、虚拟DOM就可以进大厂的时代,甚是想念。


5、Vue 3 不兼容 IE11?


是的,不兼容,如果公司业务需要兼容IE11,我给的方案是:先统计下你们有多少 IE11 用户,是否还值得投入精力兼容,推动去IE化是需要套路的,数据、成本、收益 PPT形式报告给老板,没有想想的那么难。再透漏下,react 版本的 Antd,也会在下一个大版本中不再兼容 IE 11。


6、Vue、React 如何选择?


还在纠结?工具人用哪个,它都只是工具,哪来的优越感?我用 angular,我骄傲了吗?


摸鱼小能手:哪个熟悉用哪个,哪个干活快用哪个


职场新人:公司用哪个就用哪个


KPI 高手:轮着换,使用 Vue(React) 重构 React(Vue) 项目,加载时间减少 30%,秒开率提升,转化率提升10%,带来收益 2千万/年,这TM得跳着升,没毛病吧


学生:都得学,前端框架还没复杂到二选一的地步


7、升级 Vue 3 带来的收益


性能提升,可维护性提升(主要还是看人),刷 KPI,升职加薪。


尤其是性能提升方面,我会在早早聊 Vue 专场给大家分享 Ant Design Vue 使用 Vue 3 重构,总结的一些经验。


8、何时使用 Vue 3?


别问,问就是现在


9、大厂都在用 React ?


其实并没有,我了解的百度、腾讯、京东、字节、快手、美团等等大厂都是 Vue 重度用户,阿里相对特殊些,只有少数部门在使用 Vue、Angular,之所以使用 React,不是说 Vue 不够好,只是最开始选择了React那些部门做的比较好,后来在 React 基建方面也已经做了很多工作,两套共存,有点浪费资源,仅此而已。至于那些个别团队,自带优越感式的招聘,大家可以忽略了,技术和氛围应该都不咋地。


10、硬广,Ant Design Vue 什么时候兼容 Vue 3?


Ant Design Vue 自 2.0 版本开始,已经全面兼容 Vue 3,目前文档站点默认还是 1.x 版本,是因为就像 Vue 3 一样,2.x 版本目前是 next tag,我们会在 Ant Design Vue 3.0 rc 后切回主站,没错 Ant Design Vue 已经 3.0 alpha 了。


所以 Vue 3 凉了吗,说真的,我也不知道,怎么算凉?从 Github Star、npm 下载量来看,都是呈上升趋势,我个人甚至押宝 Vue 3,已经在 6月份辞职,目前全职开源,押宝 Vue 3 了。


但是个别人有这种感觉,也是可以理解的,可以说 Vue 的成功,yyx 个人运营能力起到了至关重要的作用,react 反而低调了很多,因为 React 主要是为公司服务的,其次才是社区,他们没有运营的压力,也没有太大的动力去做运营。在 Vue 3 前期运营的过程中,或许过度强调了 Composition API,导致有部分人产生了不兼容误解,或许这部分人并不在少数,或许 Vue 应该将 Composition API 放在 3.2、3.3 的小版本上去迭代添加。


当然这都是猜测,10月23日,yyx 亲自为大家解读 Vue 3 及生态现状,这应该可以帮助你进一步做出决策。我也会为大家同步 Ant Design Vue 现状及未来规划,如果顺利,我们也会有新产品发布,但大概率要跳票了,哈哈哈,敬请期待吧。

收起阅读 »

Android Jetpack系列之ViewModel

ViewModel介绍ViewModel的定义:ViewModel旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel本质上是视图(View)与数据(Model)之间的桥梁,想想以前的MVC模式,视图和数据都会写在Activity/Fragme...
继续阅读 »

ViewModel介绍

ViewModel的定义:ViewModel旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel本质上是视图(View)与数据(Model)之间的桥梁,想想以前的MVC模式,视图和数据都会写在Activity/Fragment中,导致Activity/Fragment过重,后续难以维护,而ViewModel将视图和数据进行了分离解耦,为视图层提供数据。

ViewModel的特点:

ViewModel生命周期比Activity长

数据可在屏幕发生旋转等配置更改后继续留存。下面是ViewModel生命周期图:

viewmodel.png

可以看到即使是发生屏幕旋转,旋转之后拿到的ViewModel跟之前的是同一个实例,即发生屏幕旋转时,ViewModel并不会消失重建;而如果Activity是正常finish(),ViewModel则会调用onClear()销毁。

ViewModel中不能持有Activity引用

不要将Activity传入ViewModel中,因为ViewModel的生命周期比Activity长,所以如果ViewModel持有了Activity引用,很容易造成内存泄漏。如果想在ViewModel中使用Application,可以使用ViewModel的子类AndroidViewModel,其在构造方法中需要传入了Application实例:

public class AndroidViewModel extends ViewModel {
private Application mApplication;

public AndroidViewModel(@NonNull Application application) {
mApplication = application;
}

public <T extends Application> T getApplication() {
return (T) mApplication;
}
}

ViewModel使用举例

引入ViewModel,在介绍Jetpack系列文章Lifecycle时已经提过一次,这里再写一下:

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"

先看运行效果图:

viewmodel.gif

页面中有两个Fragment,左侧为列表,右侧为详情,当点击左侧某一个Item时,右侧会展示相应的数据,即两个Fragment可以通过ViewModel进行通信;同时可以看到,当屏幕发生旋转的时候,右侧详情页的数据并没有丢失,而是直接进行了展示。

//ViewModelActivity.kt
class ViewModelActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.e(JConsts.VIEW_MODEL, "onCreate")
setContentView(R.layout.activity_view_model)
}
}

其中的XML文件:

//activity_view_model.xml
<?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=".viewmodel.ViewModelActivity">

<fragment
android:id="@+id/fragment_item"
android:name="com.example.jetpackstudy.viewmodel.ItemFragment"
android:layout_width="150dp"
android:layout_height="match_parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/fragment_detail"
app:layout_constraintTop_toTopOf="parent" />

<fragment
android:id="@+id/fragment_detail"
android:name="com.example.jetpackstudy.viewmodel.DetailFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintLeft_toRightOf="@+id/fragment_item"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

直接将Fragment以布局方式写入我们的Activity中,继续看两个Fragment:

//左侧列表Fragment
class ItemFragment : Fragment() {
lateinit var mRvView: RecyclerView

//Fragment之间通过传入同一个Activity来共享ViewModel
private val mShareModel by lazy {
ViewModelProvider(activity!!).get(ShareViewModel::class.java)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = LayoutInflater.from(activity)
.inflate(R.layout.layout_fragment_item, container, false)
mRvView = view.findViewById(R.id.rv_view)
mRvView.layoutManager = LinearLayoutManager(activity)
mRvView.adapter = MyAdapter(mShareModel)

return view
}

//构造RecyclerView的Adapter
class MyAdapter(private val sViewModel: ShareViewModel) :
RecyclerView.Adapter<MyAdapter.MyViewHolder>() {

class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val mTvText: TextView = itemView.findViewById(R.id.tv_text)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item_fragment_left, parent, false)
return MyViewHolder(itemView)
}

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val itemStr = "item pos:$position"
holder.mTvText.text = itemStr
//点击发送数据
holder.itemView.setOnClickListener {
sViewModel.clickItem(itemStr)
}
}

override fun getItemCount(): Int {
return 50
}
}
}
//右侧详情页Fragment
class DetailFragment : Fragment() {
lateinit var mTvDetail: TextView

//Fragment之间通过传入同一个Activity来共享ViewModel
private val mShareModel by lazy {
ViewModelProvider(activity!!).get(ShareViewModel::class.java)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return LayoutInflater.from(context)
.inflate(R.layout.layout_fragment_detail, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mTvDetail = view.findViewById(R.id.tv_detail)
//注册Observer并监听数据变化
mShareModel.itemLiveData.observe(activity!!, { itemStr ->
mTvDetail.text = itemStr
})
}
}

最后贴一下我们的ViewModel:

//ShareViewModel.kt
class ShareViewModel : ViewModel() {
val itemLiveData: MutableLiveData<String> by lazy { MutableLiveData<String>() }

//点击左侧Fragment中的Item发送数据
fun clickItem(infoStr: String) {
itemLiveData.value = infoStr
}
}

这里再强调一下,两个Fragment中ViewModelProvider(activity).get()传入的是同一个Activity,从而得到的ViewModel是同一个实例,进而可以进行互相通信。

上面使用ViewModel的写法有两个好处:

屏幕切换时保存数据

  • 屏幕发生变化时,不需要重新请求数据,直接从ViewModel中再次拿数据即可。

Fragment之间共享数据

  • Activity 不需要执行任何操作,也不需要对此通信有任何了解。
  • 除了 SharedViewModel 约定之外,Fragment 不需要相互了解。如果其中一个 Fragment 消失,另一个 Fragment 将继续照常工作。
  • 每个 Fragment 都有自己的生命周期,而不受另一个 Fragment 的生命周期的影响。如果一个 Fragment 替换另一个 Fragment,界面将继续工作而没有任何问题。

ViewModel与onSaveInstance(Bundle)对比

  • ViewModel是将数据存到内存中,而onSaveInstance()是通过Bundle将序列化数据存在磁盘中
  • ViewModel可以存储任何形式的数据,且大小不限制(不超过App分配的内存即可),onSaveInstance()中只能存储可序列化的数据,且大小一般不超过1M(IPC通信数据限制)

源码解析

ViewModel的存取

我们在获取ViewModel实例时,并不是直接new出来的,而是通过ViewModelProvider.get()获取的,顾名思义,ViewModelProvider意为ViewModel提供者,那么先来看它的构造方法:

//ViewModelProvider.java
public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
: NewInstanceFactory.getInstance());
}

public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
this(owner.getViewModelStore(), factory);
}

public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
mFactory = factory;
mViewModelStore = store;
}

ViewModelProvider构造函数中的几个参数:

  • ViewModelStoreOwner:ViewModel存储器拥有者,用来提供ViewModelStore
  • ViewModelStore:ViewModel存储器,用来存储ViewModel
  • Factory:创建ViewModel的工厂
private final ViewModelStore mViewModelStore;

private static final String DEFAULT_KEY =
"androidx.lifecycle.ViewModelProvider.DefaultKey";

@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");
}
//首先构造了一个key,直接调用下面的get(key,modelClass)
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}

@MainThread
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
//尝试从ViewModelStore中获取ViewModel
ViewModel viewModel = mViewModelStore.get(key);

if (modelClass.isInstance(viewModel)) {
if (mFactory instanceof OnRequeryFactory) {
((OnRequeryFactory) mFactory).onRequery(viewModel);
}
//viewModel不为空直接返回该实例
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
} else {
//viewModel为空,通过Factory创建
viewModel = mFactory.create(modelClass);
}
//将ViewModel保存到ViewModelStore中
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}

逻辑很简单,首先尝试通过ViewModelStore.get(key)获取ViewModel,如果不为空直接返回该实例;如果为空,通过Factory.create创建ViewModel并保存到ViewModelStore中。先来看Factory是如何创建ViewModel的,ViewModelProvider构造函数中,如果没有传入Factory,那么会使用NewInstanceFactory:

//接口Factory
public interface Factory {
<T extends ViewModel> T create(@NonNull Class<T> modelClass);
}

//默认Factory的实现类NewInstanceFactory
public static class NewInstanceFactory implements Factory {
private static NewInstanceFactory sInstance;

//获取单例
static NewInstanceFactory getInstance() {
if (sInstance == null) {
sInstance = new NewInstanceFactory();
}
return sInstance;
}

@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
try {
//反射创建
return modelClass.newInstance();
} catch (InstantiationException e) {
throw new RuntimeException("Cannot create an instance of " + modelClass, e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot create an instance of " + modelClass, e);
}
}
}

可以看到NewInstanceFactory的实现很简单,直接通过传入的class反射创建实例,泛型中限制必须是ViewModel的子类,所以最终创建的是ViewModel实例。

看完Factory,接着来看ViewModelStore:

public class ViewModelStore {
private final HashMap<String, ViewModel> mMap = new HashMap<>();

final void put(String key, ViewModel viewModel) {
ViewModel oldViewModel = mMap.put(key, viewModel);
//如果oldViewModel不为空,调用oldViewModel的onCleared释放资源
if (oldViewModel != null) {
oldViewModel.onCleared();
}
}

final ViewModel get(String key) {
return mMap.get(key);
}

Set<String> keys() {
return new HashSet<>(mMap.keySet());
}

//遍历存储的ViewModel并调用其clear()方法,然后清除所有ViewModel
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}
}

ViewModelStore类也很简单,内部就是通过Map进行存储ViewModel的。到这里,我们基本看完了ViewModel的存取,流程如下:

viewmodelcreate.png

ViewModelStore的存取

上面聊了ViewModel的存取,有一个重要的点没有说到,既然ViewModel的生命周期比Activity长,而ViewModel又是通过ViewModelStore存取的,那么ViewModelStore又是如何存取的呢?在上面的流程图中我们知道ViewModelStore是通过ViewModelStoreOwner提供的:

//接口ViewModelStoreOwner.java
public interface ViewModelStoreOwner {
ViewModelStore getViewModelStore();
}

ViewModelStoreOwner中接口方法getViewModelStore()返回的既是ViewModelStore。我们上面例子获取ViewModel时是ViewModelProvider(activity).get(ShareViewModel::class.java),其中的activity其实就是ViewModelStoreOwner,也就是Activity中实现了这个接口:

//ComponentActivity.java
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
LifecycleOwner,
ViewModelStoreOwner,.... {

@Override
public ViewModelStore getViewModelStore() {
//Activity还未关联到Application时,会抛异常,此时不能使用ViewModel
if (getApplication() == null) {
throw new IllegalStateException("Your activity is not yet attached to the "
+ "Application instance. You can't request ViewModel before onCreate call.");
}
ensureViewModelStore();
return mViewModelStore;
}

void ensureViewModelStore() {
if (mViewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
//从NonConfigurationInstances中恢复ViewModelStore
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
}

@Override
//覆写了Activity的onRetainNonConfigurationInstance方法,在Activity#retainNonConfigurationInstances()方法中被调用,即配置发生变化时调用。
public final Object onRetainNonConfigurationInstance() {
Object custom = onRetainCustomNonConfigurationInstance();

ViewModelStore viewModelStore = mViewModelStore;
if (viewModelStore == null) {
//尝试从之前的存储中获取NonConfigurationInstance
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
//从NonConfigurationInstances中恢复ViewModelStore
viewModelStore = nc.viewModelStore;
}
}

if (viewModelStore == null && custom == null) {
return null;
}
//如果viewModelStore不为空,当配置变化时将ViewModelStore保存到NonConfigurationInstances中
NonConfigurationInstances nci = new NonConfigurationInstances();
nci.custom = custom;
nci.viewModelStore = viewModelStore;
return nci;
}

//内部类NonConfigurationInstances
static final class NonConfigurationInstances {
Object custom;
ViewModelStore viewModelStore;
}

}

NonConfigurationInstances是ComponentActivity的静态内部类,里面包含了ViewModelStore。getViewModelStore()内部首先尝试通过getLastNonConfigurationInstance()来获取NonConfigurationInstances,不为空直接能拿到对应的ViewModelStore;否则直接new一个新的ViewModelStore

跟进去getLastNonConfigurationInstance()这个方法:

//Activity.java
public class Activity extends ContextThemeWrapper {

NonConfigurationInstances mLastNonConfigurationInstances;

public Object getLastNonConfigurationInstance() {
return mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.activity : null;
}

NonConfigurationInstances retainNonConfigurationInstances() {
//onRetainNonConfigurationInstance实现在子类ComponentActivity中实现
Object activity = onRetainNonConfigurationInstance();
.......

if (activity == null && children == null && fragments == null && loaders == null
&& mVoiceInteractor == null) {
return null;
}
NonConfigurationInstances nci = new NonConfigurationInstances();
nci.activity = activity;
......
return nci;
}

final void attach(Context context,
.......,
NonConfigurationInstances lastNonConfigurationInstances) {
mLastNonConfigurationInstances = lastNonConfigurationInstances;
}

static final class NonConfigurationInstances {
Object activity;
......
}
}

可以看到getLastNonConfigurationInstance()中是通过mLastNonConfigurationInstances是否为空来判断的,搜索一下该变量赋值的地方,就找到了Activity#attach()方法。我们知道Activity#attach()是在创建Activity的时候调用的,顺藤摸瓜就可以找到了ActivityThread:

//ActivityThread.java
final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();

Activity.NonConfigurationInstances lastNonConfigurationInstances;

//1、将NonConfigurationInstances存储到ActivityClientRecord
ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
int configChanges, boolean getNonConfigInstance, String reason) {
ActivityClientRecord r = mActivities.get(token);
Class<? extends Activity> activityClass = null;
if (r != null) {
......
if (getNonConfigInstance) {
try {
//retainNonConfigurationInstances
r.lastNonConfigurationInstances
= r.activity.retainNonConfigurationInstances();
} catch (Exception e) {
......
}
}
}
synchronized (mResourcesManager) {
mActivities.remove(token);
}
return r;
}

//2、从ActivityClientRecord中获取NonConfigurationInstances
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
......
activity.attach(...., r.lastNonConfigurationInstances,....);
}
  • 在执行performDestroyActivity()的时候,会调用Activity#retainNonConfigurationInstances()方法将生成的NonConfigurationInstances赋值给lastNonConfigurationInstances。
  • 在performLaunchActivity()中又会通过Activity#attach()将lastNonConfigurationInstances赋值给Activity.mLastNonConfigurationInstances,进而取到ViewModelStore。

ViewModelStore的存取都是间接在ActivityThread中进行并保存在ActivityClientRecord中。在Activity配置变化时,ViewModelStore可以在Activity销毁时得以保存并在重建时重新从lastNonConfigurationInstances中获取,又因为ViewModelStore提供了ViewModel,所以ViewModel也可以在Activity配置变化时得以保存,这也是为什么ViewModel的生命周期比Activity生命周期长的原因了。


收起阅读 »

Android内存泄露检测之LeakCanary的使用(转)

LeakCanary github地址:square.github.io/leakcanary/开始使用目前为止最新的版本是2.3版本,相比于2.0之前的版本,2.0之后的版本在使用上简洁了很多,只需要在dependencies中加入LeakCanary的依赖...
继续阅读 »


LeakCanary github地址:square.github.io/leakcanary/

开始使用

目前为止最新的版本是2.3版本,相比于2.0之前的版本,2.0之后的版本在使用上简洁了很多,只需要在dependencies中加入LeakCanary的依赖即可。而且debugImplementation只在debug模式下有效,所以不用担心用户在正式环境下也会出现LeakCanary收集。

**

dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
}

在项目中加入LeakCanary之后就可以开始检测项目的内存泄露了,把项目运行起来之后, 开始随便点自己的项目,下面以一个Demo项目为例,来聊一下LeakCanary记录内存泄露的过程以及我如何解决内存泄露的。
项目运行起来之后,在控制台可以看到LeakCanary的打印信息:

**

D/LeakCanary: Check for retained object found no objects remaining
D/LeakCanary: Scheduling check for retained objects in 5000ms because app became invisible
D/LeakCanary: Check for retained object found no objects remaining
D/LeakCanary: Rescheduling check for retained objects in 2000ms because found only 3 retained objects (< 5 while app visible)

这说明LeakCanary正在不断的检测项目中是否有剩余对象。那么LeakCanary是如何工作的呢?LeakCanary的基础是一个叫做ObjectWatcher Android的library。它hook了Android的生命周期,当activity和fragment 被销毁并且应该被垃圾回收时候自动检测。这些被销毁的对象被传递给ObjectWatcher, ObjectWatcher持有这些被销毁对象的弱引用(weak references)。如果弱引用在等待5秒钟并运行垃圾收集器后仍未被清除,那么被观察的对象就被认为是保留的(retained,在生命周期结束后仍然保留),并存在潜在的泄漏。LeakCanary会在Logcat中输出这些日志。

**

D LeakCanary: Watching instance of com.example.leakcanary.MainActivity 
// 5 seconds later...
D LeakCanary: Found 1 retained object

在我一顿瞎点之后, 在手机通知栏上面出现了这样的提示:

image.png

上面的意思是已经发现了4个保留的对象,点击通知可以触发堆转储(dump heap)。在app可见的时候,会一直等到5个保留的对象才会触发堆转储。这里要补充的一点是:当应用可见的时候默认的阈值是5,应用不可见的时候阈值是1。如果你看到了保留的对象的通知然后将应用切换到后台(例如点击home键),那么阈值就会从5变到1,LeakCanary会立即进行堆转储。那么堆转储是什么一回事呢?

堆转储

在我点了上面的通知之后, 控制台打印出了下面的语句:

**

D/LeakCanary: Check for retained objects found 3 objects, dumping the heap
D/LeakCanary: WRITE_EXTERNAL_STORAGE permission not granted, ignoring
I/testapplicatio: hprof: heap dump "/data/user/0/com.example.leakcaneraytestapplication/files/leakcanary/2020-05-28_16-35-28_155.hprof" starting...
I/testapplicatio: hprof: heap dump completed (22MB) in 2.963s objects 374548 objects with stack traces 0

这里开始进行堆转储,同时生成.hprof文件,LeakCanary将java heap的信息存到该文件中。同时在应用程序中也会出现一个提示。

image.png

LeakCanary是使用shark来转换.hprof文件并定位Java堆中保留的对象。如果找不到保留的对象,那么它们很可能在堆转储的过程中回收了。

image.png

对于每个被保留的对象,LeakCanary会找出阻止该保留对象被回收的引用链:泄漏路径。泄露路径就是从GC ROOTS到保留对象的最短的强引用路径的别名。确定泄漏路径以后,LeakCanary使用它对Android框架的了解来找出在泄漏路径上是谁泄漏了。

解决内存泄露

打开生成的Leaks应用,界面就类似下面这样婶儿滴。LeakCanary会计算一个泄漏路径并在UI上展示出来。这就是LeakCanary很友好的地方,通过UI展示,可以很直接的看到内存泄漏的过程。相对于mat和android studio 自带的profiler分析工具,这个简直太直观清晰了!

image.png

同时泄漏路径也在logcat中展示了出来:

**

HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS

References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.

111729 bytes retained by leaking objects
Signature: e030ebe81011d69c7a43074e799951b65ea73a
┬───
│ GC Root: Local variable in native code

├─ android.os.HandlerThread instance
│ Leaking: NO (PathClassLoader↓ is not leaking)
│ Thread name: 'LeakCanary-Heap-Dump'
│ ↓ HandlerThread.contextClassLoader
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (ToastUtil↓ is not leaking and A ClassLoader is never leaking)
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (ToastUtil↓ is not leaking)
│ ↓ Object[].[871]
├─ com.example.leakcaneraytestapplication.ToastUtil class
│ Leaking: NO (a class is never leaking)
│ ↓ static ToastUtil.mToast
│ ~~~~~~
├─ android.widget.Toast instance
│ Leaking: YES (This toast is done showing (Toast.mTN.mWM != null && Toast.mTN.mView == null))
│ ↓ Toast.mContext
╰→ com.example.leakcaneraytestapplication.LeakActivity instance
Leaking: YES (ObjectWatcher was watching this because com.example.leakcaneraytestapplication.LeakActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
key = c1de58ad-30d8-444c-8a40-16a3813f3593
watchDurationMillis = 40541
retainedDurationMillis = 35535
====================================
0 LIBRARY LEAKS

路径中的每一个节点都对应着一个java对象。熟悉java内存回收机制的同学都应该知道”可达性分析算法“,LeakCanary就是用可达性分析算法,从GC ROOTS向下搜索,一直去找引用链,如果某一个对象跟GC Roots没有任何引用链相连时,就证明对象是”不可达“的,可以被回收。

我们从上往下看:

**

GC Root: Local variable in native code

在泄漏路径的顶部是GC Root。GC Root是一些总是可达的特殊对象。
接着是:

**

├─ android.os.HandlerThread instance
│ Leaking: NO (PathClassLoader↓ is not leaking)
│ Thread name: 'LeakCanary-Heap-Dump'
│ ↓ HandlerThread.contextClassLoader

这里先看一下Leaking的状态(YES、NO、UNKNOWN),NO表示没泄露。那我们还得接着向下看。

**

 ├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (ToastUtil↓ is not leaking and A ClassLoader is never leaking)
│ ↓ PathClassLoader.runtimeInternalObjects

上面的节点告诉我们Leaking的状态还是NO,那再往下看。

**

   ├─ android.widget.Toast instance
│ Leaking: YES (This toast is done showing (Toast.mTN.mWM != null && Toast.mTN.mView == null))
│ ↓ Toast.mContext

中间Leaking是NO状态的我就不再贴出来,我们看看Leaking是YES的这一条,这里说明发生了内存泄露。
”This toast is done showing (Toast.mTN.mWM != null && Toast.mTN.mView == null)“,这里说明Toast发生了泄露,android.widget.Toast 这是系统的Toast控件,这说明我们在使用Toast的过程中极有可能创建了Toast对象,但是该回收它的时候无法回收它,导致出现了内存泄露,这里我们再往下看:

**

╰→ com.example.leakcaneraytestapplication.LeakActivity instance
Leaking: YES (ObjectWatcher was watching this because com.example.leakcaneraytestapplication.LeakActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)

这里就很明显的指出了内存泄露是发生在了那个activity里面,我们根据上面的提示,找到对应的activity,然后发现了一段跟Toast有关的代码:

image.png

这里再进入ToastUtil这个自定义Toast类里面,看看下面的代码,有没有发现什么问题?这里定义了一个static的Toast对象类型,然后在showToast的时候创建了对象,之后就没有然后了。我们要知道static的生命周期是存在于整个应用期间的,而一般Toast对象只需要显示那么几秒钟就可以了,因为这里创建一个静态的Toast,用完之后又没有销毁掉,所以这里提示有内存泄露了。因此我们这里要么不用static修饰,要么在用完之后把Toast置为null。

**

public class ToastUtil {

private static Toast mToast;

public static void showToast(Context context, int resId) {
String text = context.getString(resId);
showToast(context, text);
}

public static void showToast(Context context, String text){
showToast(context, text, Gravity.BOTTOM);
}

public static void showToastCenter(Context context, String text){
showToast(context, text, Gravity.CENTER);
}

public static void showToast(Context context, String text, int gravity){
cancelToast();
if (context != null){
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View layout = inflater.inflate(R.layout.toast_layout, null);
((TextView) layout.findViewById(R.id.tv_toast_text)).setText(text);
mToast = new Toast(context);
mToast.setView(layout);
mToast.setGravity(gravity, 0, 20);
mToast.setDuration(Toast.LENGTH_LONG);
mToast.show();
}
}

public static void cancelToast() {
if (mToast != null){
mToast.cancel();
}
}
}

讲了这么多,其实内存泄露的本质是长周期对象持有了短周期对象的引用,导致短周期对象该被回收的时候无法被回收,从而导致内存泄露。我们只要顺着LeakCaneray的给出的引用链一个个的往下找,找到发生内存泄露的地方,切断引用链就可以释放内存了。

这里再补充一点上面的这个例子里面Leaking没有UNKNOWN的状态,一般情况下除了YES、NO还会出现UNKNOWN的状态,UNKNOWN表示这里可能出现了内存泄露,这些引用你需要花时间来调查一下,看看是哪里出了问题。一般推断内存泄露是从最后一个没有泄漏的节点(Leaking: NO )到第一个泄漏的节点(Leaking: YES)之间的引用。


收起阅读 »

❤️ Android 源码解读-startActivity(含启动新应用)❤️

开局一张图源码版本:Android 11(SDK 30)涉及到的类Instrumentation:负责 Application 和 Activity 的建立和生命周期控制。ActivityTaskManager:此类提供...
继续阅读 »

开局一张图

源码版本:Android 11(SDK 30)

涉及到的类

  • Instrumentation:负责 Application 和 Activity 的建立和生命周期控制。

  • ActivityTaskManager:此类提供有关Activity及其容器(如任务、堆栈和显示)的信息并与之交互。

  • ActivityTaskManagerService:用于管理Activity及其容器(任务、显示等)的系统服务。之前这个活是AMS的,现在从AMS中剥离出来了。

  • ActivityStartController:用于委托Activity启动的控制器。

  • ActivityStarter:专门负责一个 Activity 的启动操做。它的主要做用包括解析 Intent、建立 ActivityRecord、若是有可能还要建立 TaskRecordflex

  • ActivityStack:Activity在ATMS的栈管理,用来记录已经启动的Activity的先后关系,状态信息等。通过ActivityStack决定是否需要启动新的进程。

  • RootWindowContainer:设备的Root WindowContainer。

  • TaskRecord:ATMS抽象出来的一个"任务"的概念,是记录ActivityRecord的栈,一个"Task"包含若干个ActivityRecord。ATMS用TaskRecord确保Activity启动和退出的顺序。

  • ActivityStackSupervisor:负责所有Activity栈的管理。内部管理了mHomeStack、mFocusedStack和mLastFocusedStack三个Activity栈。其中,mHomeStack管理的是Launcher相关的Activity栈;mFocusedStack管理的是当前显示在前台Activity的Activity栈;mLastFocusedStack管理的是上一次显示在前台Activity的Activity栈。

  • ActivitySupervisor:管理 activity 任务栈。

  • ActivityThread:ActivityThread 运行在UI线程(主线程),App的真正入口。

  • ApplicationThread:用来实现ATMS和ActivityThread之间的交互。

  • ApplicationThreadProxy:ApplicationThread 在服务端的代理。ATMS就是通过该代理与ActivityThread进行通信的。

  • ClientLifecycleManager:能够组合多个客户端生命周期 transaction 请求和/或回调,并将它们作为单个 transaction 执行。

  • TransactionExecutor:已正确的顺序管理 transaction 执行的类。

1、Activity.java

1.1 startActivity()

@Override
public void startActivity(Intent intent) {
//接着往里看
this.startActivity(intent, null);
}

@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
...
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
startActivityForResult(intent, -1);
}
}

最终调用了 startActivityForResult 方法,传入的 -1 表示不需要获取 startActivity 的结果。

1.2 startActivityForResult()

public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
//mParent表示当前Activitiy的父类,
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
//ActivityThread mMainThread;
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
...
} else {
...
}
}

startActivityForResult 调用 Instrumentation.execStartActivity 方法。剩下的交给 Instrumentation 类去处理。

mMainThread 是 ActivityThread 类型,ActivityThread 可以理解为一个进程,这就是 Activity 所在的进程。

通过 mMainThread 获取一个 ApplicationThread 的引用,这个引用就是用来实现进程间通信的,具体来说就是 AMS 所在系统进程通知应用程序进程进行的一系列操作。

上面有Instrumentation、ActivityThread、ApplicationThread 等类的介绍。

2、Instrumentation.java

frameworks/base/core/java/android/app/Instrumentation.java

/**
* Base class for implementing application instrumentation code.
*/

public class Instrumentation {
...
}

2.1 execStartActivity()

 public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
...
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
int result = ActivityTaskManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
//检查启动Activity的结果
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}

在 Instrumentation 中,调用了 ActivityTaskManager.getService().startActivity() 。咱们近距离观望一下。

3、ActivityTaskManage.java

frameworks/base/core/java/android/app/ActivityTaskManager.java

@TestApi
@SystemService(Context.ACTIVITY_TASK_SERVICE)
public class ActivityTaskManager {
...
}

3.1 getService()

    /** @hide */
public static IActivityTaskManager getService() {
return IActivityTaskManagerSingleton.get();
}

3.2 IActivityTaskManagerSingleton

    @UnsupportedAppUsage(trackingBug = 129726065)
private static final Singleton<IActivityTaskManager> IActivityTaskManagerSingleton =
new Singleton<IActivityTaskManager>() {
@Override
protected IActivityTaskManager create() {
//代理对象(动态代理)
final IBinder b = ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE);
return IActivityTaskManager.Stub.asInterface(b);
}
};

这里实际上返回的是一个 ActivityTaskManagerService 这出现了跨进程 (从应用进程(app) > system_service进程) ,然后调用其 startActivity 方法。

实际上这里就是通过 AIDL 来调用 ATMS 的 startActivity 方法,返回 IActivityTaskManager.startActivity 的结果:Activity 已正常成功启动。

至此,startActivity 的工作重心成功地从 应用进程(app) 转移到了系统进程(system_service) 的 ATMS 中。

4、ActivityTaskManagerService.java

frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java

/**
* 用于管理 activities and their containers (task, stacks, displays,... )的 system_service(系统服务)
*/

public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
...
}

4.1 startActivity()

@Override
public final int startActivity(IApplicationThread caller, String callingPackage,
String callingFeatureId, Intent intent, String resolvedType, IBinder resultTo,
String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo,
Bundle bOptions) {
return startActivityAsUser(caller, callingPackage, callingFeatureId, intent, resolvedType,
resultTo, resultWho, requestCode, startFlags, profilerInfo, bOptions,
UserHandle.getCallingUserId());
}

4.2 startActivityAsUser()

@Override
public int startActivityAsUser(IApplicationThread caller, String callingPackage,
String callingFeatureId, Intent intent, String resolvedType, IBinder resultTo,
String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo,
Bundle bOptions, int userId) {
return startActivityAsUser(caller, callingPackage, callingFeatureId, intent, resolvedType,
resultTo, resultWho, requestCode, startFlags, profilerInfo, bOptions, userId,
true /*validateIncomingUser*/);
}

4.3 startActivityAsUser() 比上面多个参数

//重点来了
private int startActivityAsUser(IApplicationThread caller, String callingPackage,
@Nullable String callingFeatureId, Intent intent, String resolvedType,
IBinder resultTo, String resultWho, int requestCode, int startFlags,
ProfilerInfo profilerInfo, Bundle bOptions, int userId, boolean validateIncomingUser) {
assertPackageMatchesCallingUid(callingPackage);
enforceNotIsolatedCaller("startActivityAsUser");

userId = getActivityStartController().checkTargetUser(userId, validateIncomingUser,
Binder.getCallingPid(), Binder.getCallingUid(), "startActivityAsUser");

// 在此处切换到用户app堆栈。
return getActivityStartController().obtainStarter(intent, "startActivityAsUser")
.setCaller(caller)
.setCallingPackage(callingPackage)
.setCallingFeatureId(callingFeatureId)
.setResolvedType(resolvedType)
.setResultTo(resultTo)
.setResultWho(resultWho)
.setRequestCode(requestCode)
.setStartFlags(startFlags)
.setProfilerInfo(profilerInfo)
.setActivityOptions(bOptions)
.setUserId(userId)
.execute();

}

4.4 getActivityStartController()

    ActivityStartController getActivityStartController() {
return mActivityStartController;
}

5、ActivityStartController.java

/**
* Controller for delegating activity launches.
*/

public class ActivityStartController {
...
}

5.1 obtainStarter();

    /**
* @return A starter to configure and execute starting an activity. It is valid until after
* {@link ActivityStarter#execute} is invoked. At that point, the starter should be
* considered invalid and no longer modified or used.
*/

ActivityStarter obtainStarter(Intent intent, String reason) {
return mFactory.obtain().setIntent(intent).setReason(reason);
}

6、ActivityStarter.java

frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java

/**
* Controller for interpreting how and then launching an activity.
*
* This class collects all the logic for determining how an intent and flags should be turned into
* an activity and associated task and stack.
*/

class ActivityStarter {
...
}

6.1 execute()

    /**
* 根据前面提供的请求参数解析必要的信息,执行开始活动的请求。
* @return The starter result.
*/

int execute() {
...
res = executeRequest(mRequest);
...
}

6.2 executeRequest()

    /**
* Activity启动之前需要做那些准备?
* 通常的Activity启动流程将通过 startActivityUnchecked 到 startActivityInner。
*/

private int executeRequest(Request request) {
...
//Activity的记录
ActivityRecord sourceRecord = null;
ActivityRecord resultRecord = null;
...
//Activity stack(栈)管理
mLastStartActivityResult = startActivityUnchecked(r, sourceRecord, voiceSession,
request.voiceInteractor, startFlags, true /* doResume */, checkedOptions, inTask,
restrictedBgActivity, intentGrants);

...
return mLastStartActivityResult;
}

从上面代码可以看出,经过多个方法的调用,最终通过 obtainStarter 方法获取了 ActivityStarter 类型的对象,然后调用其 execute() 方法。

在 execute() 方法中,会再次调用其内部的 executeRequest() 方法。

咱们接着看看 startActivityUnchecked() ;

6.3 startActivityUnchecked()

    private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
int startFlags, boolean doResume, ActivityOptions options, Task inTask,
boolean restrictedBgActivity, NeededUriGrants intentGrants) {
....
try {
...
result = startActivityInner(r, sourceRecord, voiceSession, voiceInteractor,
startFlags, doResume, options, inTask, restrictedBgActivity, intentGrants);
} finally {
...
}

postStartActivityProcessing(r, result, startedActivityRootTask);

return result;
}

6.4 startActivityInner()

    int startActivityInner(final ActivityRecord r, ActivityRecord sourceRecord,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
int startFlags, boolean doResume, ActivityOptions options, Task inTask,
boolean restrictedBgActivity, NeededUriGrants intentGrants) {
setInitialState(r, options, inTask, doResume, startFlags, sourceRecord, voiceSession,
voiceInteractor, restrictedBgActivity);

//计算启动模式
computeLaunchingTaskFlags();
computeSourceRootTask();
//设置启动模式
mIntent.setFlags(mLaunchFlags);

...

// 关键点来了
mRootWindowContainer.resumeFocusedTasksTopActivities(
mTargetRootTask, mStartActivity, mOptions, mTransientLaunch);
...

return START_SUCCESS;
}

这个是 mRootWindowContainer 是 RootWindowContainer

7、RootWindowContainer.java

frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java

/** Root {@link WindowContainer} for the device. */
class RootWindowContainer extends WindowContainer<DisplayContent>
implements DisplayManager.DisplayListener {
...
}

7.1 resumeFocusedTasksTopActivities()

    boolean resumeFocusedTasksTopActivities(
Task targetRootTask, ActivityRecord target, ActivityOptions targetOptions,
boolean deferPause) {
...
boolean result = false;
if (targetRootTask != null && (targetRootTask.isTopRootTaskInDisplayArea()
|| getTopDisplayFocusedRootTask() == targetRootTask)) {
result = targetRootTask.resumeTopActivityUncheckedLocked(target, targetOptions,
deferPause);
}

...
return result;
}

当然这里看的是 targetRootTask 是 Task 对象的实例。咱们就去追踪这个方法。

8、

收起阅读 »

【奇淫技巧】解锁X5内核WebView同层渲染能力

前言WebView同层渲染,并不是一个新技术,国内一线互联网产品广泛应用,比如小程序的原生组件,电商H5嵌原生播放器等场景;如果您了解其原理,会发现这玩意在Android端,需要修改浏览器内核才能搞定,所以上手难度高;以至于读完文章,心血澎湃直呼牛逼,但是回过...
继续阅读 »

前言

WebView同层渲染,并不是一个新技术,国内一线互联网产品广泛应用,比如小程序的原生组件,电商H5嵌原生播放器等场景;

如果您了解其原理,会发现这玩意在Android端,需要修改浏览器内核才能搞定,所以上手难度高;以至于读完文章,心血澎湃直呼牛逼,但是回过头翻看原生WebView代码,并没有找到合适的API;

同层渲染简介

在Android平台,H5内容依托WebView视图渲染,它和原生的View是独立平等的关系, 从绘制层次上来看,WebView和原生必然存在相互覆盖遮挡的,更没法做到同步滚动;

在网页DOM树中,间杂一部分原生的组件,且保留原本的层次和样式,这就是同层渲染;

图片来源微信技术文章

同层渲染能解决什么问题?

使用web前端技术实现困难,或者稳定性、性能受限等情况

例如:视频播放器、地图、游戏引擎、直播推拉流、摄像头预览等场景;

领略X5内核浏览器同层渲染

准备工作:

准备一个占位标签

X5同层渲染的原理是用原生接管在H5页面里的特定标签,所以得准备一个H5页面,然后插入一个自定义的标签, 标签名可以随意定义,比如mytag,样式就按照标准的css来设置


占位的标签

强制开启X5WebView同层渲染

X5同层渲染能力默认是关闭的,通过云端开关控制,通过分析发现,可以强制修改本地SP属性,强制打开

if (mWebView.getX5WebViewExtension()!=null){
//强制设置EMBEDDED云控开关enable
SharedPreferences tbs_public_settings = getSharedPreferences("tbs_public_settings", Context.MODE_PRIVATE);
SharedPreferences.Editor edit = tbs_public_settings.edit();
edit.putInt("MTT_CORE_EMBEDDED_WIDGET_ENABLE",1);
edit.apply();
}else {
Log.d(TAG, "init: 非x5内核");
}

向浏览器注册目标占位标签的原生控件

使用registerEmbeddedWidget方法,可以想浏览器内核注册,需要原生来接管的占位的标签,第一个参数是需要接管的标签名,第二个参数是工厂创建对应原生标签对象的工厂接口

//注册dom树中占位标签,创建对应的原生组件
boolean result = mWebView.getX5WebViewExtension().registerEmbeddedWidget(new String[]{"mytag"}, new IEmbeddedWidgetClientFactory() {
@Override
public IEmbeddedWidgetClient createWidgetClient(String s, Map map, IEmbeddedWidget iEmbeddedWidget) {
Log.d(TAG, "init: createWidgetClient s"+s);
Log.d(TAG, "init: createWidgetClient map"+map.toString());
return new VideoEmbeddedWidgetClient(BrowserActivity.this);
}
});

其中createWidgetClient方法参数意义

  • s 标签名,大写
  • map 该标签的属性,在html中指定的
  • iEmbeddedWidget 提供的原生的该标签的代理接口

下面是map打印的内容

init: createWidgetClient map{src=https://vfx.mtime.cn/Video/2019/02/04/mp4/190204084208765161.mp4, style=position:absolute;width:350px; height:500px;, id=mytag}

处理原生占位控件的实现IEmbeddedWidgetClient

原生如何接管替换占位标签,主要的实现类是IEmbeddedWidgetClient

public interface IEmbeddedWidgetClient {

void onSurfaceCreated(Surface var1);

void onSurfaceDestroyed(Surface var1);

boolean onTouchEvent(MotionEvent var1);

void onRectChanged(Rect var1);

void onVisibilityChanged(boolean var1);

void onDestroy();

void onActive();

void onDeactive();

void onRequestRedraw();
}

首先,IEmbeddedWidgetClient并不是一个原生的View,而是给原生提供的该标签区域的绘制的入口,从onSurfaceCreatedonSurfaceDestroyed可以理解;

既然不是原生的View绘制,X5还是提供了类比View的属性,从API命名可以简单看出来其作用;

  • onSurfaceCreated 该标签可以绘制,请求原生API处理
  • onSurfaceDestroyed 该标签视图销毁,请求原生销毁
  • onTouchEvent 触摸事件分发
  • onRectChanged 该标签在WebView中坐标变化回调(例如滚动,改变宽高)
  • onVisibilityChanged 该标签显示隐藏
  • onDestroy 该标签被移除,或者display = none

演示个Demo

熟悉了X5的API,写个简单的Demo玩一下

Demo的设计思路是在一个垂直滚动的网页中,嵌入一个原生的相机,原生的相机要正常的采集显示,且前端代码可以控制相机标签的显示隐藏,同步滚动等基本操作;

网页




<span class="javascript">测试网页</span>






测试网页哈哈哈



1

2



相机占位标签

点击跳转哈哈哈

4

5

6






Java实现

public class CameraEmbeddedWidgetClient implements IEmbeddedWidgetClient {

private String TAG = "VideoEmbeddedWidgetClient";

private Rect rect;

private CameraHelper cameraHelper;

public CameraEmbeddedWidgetClient(Context c) {
cameraHelper = new CameraHelper(c);
}

@Override
public void onSurfaceCreated(Surface surface) {
Log.d(TAG, "onSurfaceCreated: ");
// Canvas canvas = surface.lockCanvas(rect);
// canvas.drawColor(Color.parseColor("#7f000000"));
// surface.unlockCanvasAndPost(canvas);
cameraHelper.preview(surface);
}

@Override
public void onSurfaceDestroyed(Surface surface) {
Log.d(TAG, "onSurfaceDestroyed: ");
cameraHelper.release();
}
}

snap.jpeg

前端样式改变和Native事件触发

  • 指定样式display:none 原生回调onVisibilityChanged(false)onDestroy
  • 指定样式display:block 重新创建新的Client
  • 指定样式visibility:visible 原生回调onVisibilityChanged(true)
  • 指定样式visibility:hidden 原生回调onVisibilityChanged(false)
  • 移除当前dom 等效于display:none

触摸事件的验证

必须得在js中设置改标签的事件监听

camera.addEventListener("touchStart",handlerTouch,false);
camera.addEventListener("touchend",handlerTouch,false);
camera.addEventListener("touchcancel",handlerTouch,false);
camera.addEventListener("touchleave",handlerTouch,false);
camera.addEventListener("touchmove",handlerTouch,false);

原生接受事件处理

IEmbeddedWidgetClient实现类

@Override
public boolean onTouchEvent(MotionEvent motionEvent)
{
Log.d(TAG, "onTouchEvent: "+motionEvent.toString());
float x = motionEvent.getX();
float y = motionEvent.getY();
int action = motionEvent.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
initX = x;
initY = y;
intercepted = false;
return false;
case MotionEvent.ACTION_MOVE:
float dx = x - initX;
float dy = y - initY;
if (!intercepted && Math.abs(dy)>Math.abs(dx) && Math.abs(dy)>16){
intercepted = true;
}
break;

case MotionEvent.ACTION_UP:

break;
}
return intercepted;
}
原文地址:https://juejin.cn/post/7018037732412768269
收起阅读 »

Android Activity/Window/View 的background

前言Activity/Window/View 的background,平时接触最多的就是View的background,Activity的background次之,最后用的较少的是Window的background,这三者有什么关联、区别呢?通过本篇文章,你将...
继续阅读 »

前言

Activity/Window/View 的background,平时接触最多的就是View的background,Activity的background次之,最后用的较少的是Window的background,这三者有什么关联、区别呢?
通过本篇文章,你将了解到:

1、View background 原理与使用
2、Window background 原理与使用
3、Activity background 原理与使用
4、常用背景设置

1、View background 原理与使用

先看个简单的图示:

image.png


一般来说,View展示区域分为两个部分:
1、背景
2、内容

本篇重点分析背景绘制,内容绘制请移步:Android 自定义View之Draw过程(上)

在平时的运用中,你是否思考过两个问题:

1、为什么内容区域能遮住背景区域
2、为什么View.scrollTo(xx)只能移动内容

先看第一个问题
来看看如何绘制View的背景:
熟知的View.draw(xx)方法如下:

#View.java
public void draw(Canvas canvas) {
...
//绘制背景-------------(1)
drawBackground(canvas);

...
if (!verticalEdges && !horizontalEdges) {
//--------------(2)

//绘制自身内容
onDraw(canvas);

//绘制子布局
dispatchDraw(canvas);
...
//前景、高亮等
return;
}
...
}

从上面(1)、(2)点可以看出,先绘制背景,再绘制内容,因此内容区域会遮住部分背景区域。

再看第二个问题
主要是drawBackground(canvas)方法:

#View.java
private void drawBackground(Canvas canvas) {
//背景Drawable
final Drawable background = mBackground;
if (background == null) {
//没有背景,无需绘制
return;
}

//设置背景Drawable,并设置其尺寸
setBackgroundBounds();

//支持硬件加速
if (canvas.isHardwareAccelerated() && mAttachInfo != null
&& mAttachInfo.mThreadedRenderer != null) {
//绘制背景,并返回Drawable ------------------(1)
mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

final RenderNode renderNode = mBackgroundRenderNode;
if (renderNode != null && renderNode.hasDisplayList()) {
//绘制完成
setBackgroundRenderNodeProperties(renderNode);
//和Canvas关联起来,也就是将绘制好的背景挂到Canvas上
((RecordingCanvas) canvas).drawRenderNode(renderNode);
return;
}
}

//软件绘制
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
//没有偏移,直接绘制
background.draw(canvas);
} else {
//现将canvas平移回来 ---------------(2)
canvas.translate(scrollX, scrollY);
//绘制
background.draw(canvas);
//再平移回去
canvas.translate(-scrollX, -scrollY);
}
}

上面标注了两个重点:
(1)
真正绘制背景的地方:

#View.java
private RenderNode getDrawableRenderNode(Drawable drawable, RenderNode renderNode) {
if (renderNode == null) {
//创建renderNode
renderNode = RenderNode.create(drawable.getClass().getName(),
new ViewAnimationHostBridge(this));
renderNode.setUsageHint(RenderNode.USAGE_BACKGROUND);
}

//获取尺寸,之前已经设置过
final Rect bounds = drawable.getBounds();
final int width = bounds.width();
final int height = bounds.height();
//获取专门绘制背景的Canvas
final RecordingCanvas canvas = renderNode.beginRecording(width, height);

//平移
canvas.translate(-bounds.left, -bounds.top);

try {
//绘制背景
drawable.draw(canvas);
} finally {
//结束绘制,将displayList记录到renderNode
renderNode.endRecording();
}

...
return renderNode;
}

可以看出,生成新的Canvas,用该Canvas绘制背景,并将绘制记录到背景的renderNode里。
(2)
你可能已经发现了,此处为什么对Canvas进行平移?
对于软件绘制来说,从RootView传递下来的Canvas是同一个,也就是说整个ViewTree都共用一个Canvas。对于View的绘制,其方法调用顺序如下:

draw(x)->dispatchDraw(x)->child.draw(x1,x2,x3)->child.draw(x)

在child.draw(x1,x2,x3)方法里,判断是否需要进行内容移动(mScrollX = 0 || mScrollY != 0),如果需要则移动Canvas,如下:

canvas.translate(-mScrollX, -mScrollY)

注意此处是取反了。
此时canvas已经被移动了,当调用到child.draw(xx)时候,就是上面分析的draw(xx)方法:
1、先绘制背景
2、绘制内容

绘制背景的时候将Canvas平移回来,再绘制背景,最后平移回去。再绘制内容的时候Canvas没变,依然了平移了(-mScrollX, -mScrollY),因此内容绘制的时候就会平移,而绘制背景的时候不变,这就回答了第二个问题。

上边仅仅针对软件绘制回答了第二个问题,那么硬件加速绘制的时候为啥不需要平移Canvas呢?此处简单说下结论:

硬件加速绘制的时候,每个View都有自己的Canvas,RenderNode,而相应的背景也有自己的Canvas,RenderNode,因此即使View的Canvas发生平移,也不会影响背景的Canvas,因此背景的Canvas无需针对mScrollX、mScrollY平移。

View绘制细节部分请移步:Android 自定义View之Draw过程(上)

用图表示背景绘制过程:

image.png

以上是针对View background分析,通常来说我们设置背景只要给其指定Drawable就ok了。
不论是通过动态设置:

#View.java
public void setBackground(Drawable background){...}
public void setBackgroundColor(@ColorInt int color){...}
public void setBackgroundResource(@DrawableRes int resid){...}
...

还是通过xml静态配置:

android:background="@color/colorGreen"
android:background="@drawable/test"
...

最终都是将生成的Drawable对象赋值给View的成员变量mBackground,最终在绘制的背景[drawBackground()]的时候使用该Drawable绘制。

2、Window background 原理与使用

若要设置Window背景,那么需要获取Window对象,我们常用的用到Window对象的地方有两个:Activity和Dialog。
Window是个抽象类,它的实现类是PhoneWindow,因此Activity和Dialog里Window指向实际上就是PhoneWindow对象。
获取Window引用方式如下:

Activity.getWindow()
Dialog.getWindow()

来看看Window是如何设置背景的:

    #Window.java
public abstract void setBackgroundDrawable(Drawable drawable);

#PhoneWindow.java
@Override
public final void setBackgroundDrawable(Drawable drawable) {
//mBackgroundDrawable 为记录当前Window的背景
if (drawable != mBackgroundDrawable) {
//背景改变则需要重新设置
mBackgroundDrawable = drawable;
if (mDecor != null) {
//mDecor 即为熟知的DecorView
//此处调用DecorView方法
mDecor.setWindowBackground(drawable);
...
}
}
}

PhoneWindow重写了Window里的setBackgroundDrawable(xx)方法。该方法里调用了DecorView的setWindowBackground(xx)。

#DecorView.java
public void setWindowBackground(Drawable drawable) {
if (mOriginalBackgroundDrawable != drawable) {
mOriginalBackgroundDrawable = drawable;
//赋值给View的成员变量mBackground,也就是给View设置背景
updateBackgroundDrawable();

//该处主要是判断如果Window不是半透明(windowTranslucent=true),但是drawable有透明度,强制设置透明度=255
if (drawable != null) {
mResizingBackgroundDrawable = enforceNonTranslucentBackground(drawable,
mWindow.isTranslucent() || mWindow.isShowingWallpaper());
} else {
mResizingBackgroundDrawable = getResizingBackgroundDrawable(
mWindow.mBackgroundDrawable, mWindow.mBackgroundFallbackDrawable,
mWindow.isTranslucent() || mWindow.isShowingWallpaper());
}
if (mResizingBackgroundDrawable != null) {
mResizingBackgroundDrawable.getPadding(mBackgroundPadding);
} else {
mBackgroundPadding.setEmpty();
}
drawableChanged();
}
}

可以看出给Window 设置背景最终反馈到DecorView上了。
以Activity为例,设置Activity Window的背景为绿色:

        ColorDrawable colorDrawable = new ColorDrawable();
colorDrawable.setColor(Color.GREEN);
getWindow().setBackgroundDrawable(colorDrawable);

效果如下:

image.png

方法调用流程:

image.png

3、Activity background 原理与使用

一般来说,我们会在Activity Theme里设置其背景:

    <style name="activitytheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowBackground">#0033ff</item>
</style>

而在Activity onCreate(xx)里会调用setContentView(xx),而后调用PhoneWindow的generateLayout(xx)方法:

#PhoneWindow.java
protected ViewGroup generateLayout(DecorView decor) {
...
if (getContainer() == null) {
if (mBackgroundDrawable == null) {
...
//获取theme里设置的背景
if (a.hasValue(R.styleable.Window_windowBackground)) {
mBackgroundDrawable = a.getDrawable(R.styleable.Window_windowBackground);
}
}
...
}
...

if (getContainer() == null) {
//设置DecorView背景
mDecor.setWindowBackground(mBackgroundDrawable);
...
}
...
}

在Theme里设置的背景在此处被取出来,然后设置给DecorView背景。
严格上来说,Activity没有所谓背景的说法,它的"背景"指的是Window的背景,只是为了方便没有特意区分。 Activity有默认的背景,不同的主题取值不一样,我这主题默认背景色:

@color/material_grey_50
#fffafafa

那当要设置Activity为透明,怎么做呢?直接设置其背景透明,你会发现并没有达到效果,而是一片黑色。此时需要配合另一个属性使用:

    <style name="activitytheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">#00000000</item>
</style>

4、常用背景设置

从上可知,无论是Activity还是Window,设置它们背景的时候,最终都是设置了DecorView的背景。
我们知道,想要在屏幕上显示View,实际上是需要将这个View添加到Window里。调用如下方法:

WindowManager.addView(View view, ViewGroup.LayoutParams params)

该view作为Window的RootView。
来看看一些常用的RootView:

  • Activity/Dialog 使用DecorView作为RootView
  • PopupWindow使用PopupDecorView(没设置背景的时候)/PopupBackgroundView(有背景的时候)
  • 普通悬浮窗选择任意的View作为RootView

设置背景的过程实际上就是设置RootView的背景

上面举例说明了Activity背景设置,接下来阐述常用的弹框背景设置及其注意事项。

Dialog 背景设置

先看一个简单的Demo

    private void showDialog(Context context) {
Dialog dialog = new Dialog(context);
FrameLayout frameLayout = new FrameLayout(context);
TextView textView1 = new TextView(context);
textView1.setText("hello");
frameLayout.addView(textView1, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
// frameLayout.setBackgroundColor(Color.RED);---------->(1)
dialog.setContentView(frameLayout);
dialog.getWindow().setLayout(800, 800);
ColorDrawable colorDrawable = new ColorDrawable();
colorDrawable.setColor(Color.TRANSPARENT);
// dialog.getWindow().setBackgroundDrawable(colorDrawable);--------->(2)
dialog.show();
}

将TextView添加到FrameLayout,并将FrameLayout作为Dialog ContentView添加进去,效果如下:

image.png

可以看出,Dialog默认设置了一个背景,该背景是有圆角的矩形。
现在将注释(1)打开:设置FrameLayout背景
效果如下:

image.png

发现圆角没了,先搞清楚这个圆角背景到底是哪个的背景呢?
将(1)注释掉,注释(2)打开,并设置

colorDrawable.setColor(Color.RED);

效果如下:

image.png

平白无故外层多了黑色的区域。
修改背景颜色为透明:

colorDrawable.setColor(Color.TRANSPARENT);

再看效果时,发现整个Dialog都没背景了。

image.png

在此基础上,将注释(1)打开,效果如下:

image.png

上面操作可能比较绕,实际上想表达的就是两个:

1、Dialog默认背景是DecorView的背景(DecorView默认背景会设置4个方向的Padding,当去除默认背景后会发现ContentView区域变大了)
2、一般来说,不要将Dialog背景改为ColorDrawable类型,会有黑色背景。要么将背景变为透明,然后设置contentView的背景;要么将背景改指向Shape。

设置Dialog背景两种方式:

//动态
dialog.getWindow().setBackgroundDrawable(colorDrawable);

//静态
//设置style
<style name="myDialog">
<item name="android:windowBackground">@android:color/transparent</item>
</style>

PopupWindow 背景设置

PopupWindow没有使用Window,也没有使用DecorView作为RootView。

    private void showPopupWindow(Context context, View anchor) {
TextView textView1 = new TextView(context);
textView1.setText("heloo jj");
PopupWindow popupWindow = new PopupWindow(textView1, 300, 300, true);
ColorDrawable colorDrawable = new ColorDrawable();
// colorDrawable.setColor(Color.GREEN);-------------->(1)
popupWindow.setBackgroundDrawable(colorDrawable);
popupWindow.showAsDropDown(anchor);
}

运行上面的Demo:

image.png

可以看出,PopupWindow 没有背景。
将注释(1)打开,效果如下:

image.png

背景已添加上。
想表达的意思:

PopupWindow 如果没有设置背景的话,那么背景会是透明的。

当设置PopupWindow背景时,会生成一个PopupBackgroundView 作为PopupWindow的RootView,而设置PopupWindow背景就是设置PopupBackgroundView的背景

设置PopupWindow背景两种方式:

//动态
popupWindow.setBackgroundDrawable(colorDrawable);

//静态
//设置style
<style name="myPopupWindow">
<item name="android:popupBackground">@color/green</item>
</style>

普通悬浮窗口 背景设置

如果是直接通过WindowManager.addView(View view, ViewGroup.LayoutParams params)添加弹窗。
需要设置RootView的背景,也就是上面方法的view的背景,否则背景将是黑色的。
关于悬浮窗已经分析过很多次了,更详细的内容请移步:
Window/WindowManager 不可不知之事

本文基于Android 10.0


收起阅读 »

iOS SwiftUI 创建和组合视图 2.0

创建列表和导航地标详情页视图已经创建完成,我们需要提供一种方式让用户可以查看完整的地标列表,并且可以查看每一个地标的详情下面会创建一个可以展示任何地标信息的视图,并动态生成一个可滚动列表,用户可以点击列表项去查看地标的详细信息。优化视图显示时,可以使用Xcod...
继续阅读 »

创建列表和导航

地标详情页视图已经创建完成,我们需要提供一种方式让用户可以查看完整的地标列表,并且可以查看每一个地标的详情

下面会创建一个可以展示任何地标信息的视图,并动态生成一个可滚动列表,用户可以点击列表项去查看地标的详细信息。优化视图显示时,可以使用Xcode画布来渲染多个不同设备大小下的预览视图。

下载下面的工程文件,并跟着教程一步步学习构建列表和视图间导航


第一节 了解样本数据

前面的教程中,自定义视图所展示的信息都直接被写死在代码中,这篇教程中会学习给自定义视图传入样本数据进行展示

swiftui-building-list

步骤1 打开项目导航器,选择Models->Landmark.swift文件,这个文件中声明了需要在应用中展示一个地标所需要信息的结构化名称,并通过导入landmarkData.json文件中的数据,生成一个地标信息数组。

building list model

步骤2 在项目导航器中选择Resources->landmarkData.json,在后面的教程中我们都会使用这个样本数据文件

building list sample data

步骤3 注意,之前的ContentView视图,已经被改名为LandmarkDetail了,在本教程和后面的教程中,还会创建一些其它的视图

landmark detail

第二节 创建行视图

本教程中创建的第一个视图就是用来显示每个地标的行视图,行视图把地标的相关信息存储在一个属性中,一行就可以代表一个地标,稍后就会把这些行组合成为一个列表。

swiftui-building-list-landmark-row

步骤1 创建一个名为LandmarkRow.swift的SwiftUI视图

landmark row create

步骤2 如果预览视图没有出现,可以选择菜单编辑器->画布,打开画布,并点击Resume进行预览,或者使用Command+Option+Enter快捷键调出画面,再使用Command+Option+P快捷键开始预览模式

步骤3 添加landmark属性做为LandmarkRow视图的一个存储属性。当添加landmark属性后,预览视图可能会停止工作,因为LandmarkRow视图初始化时需要有一个landmark实例。要想修复预览视图,需要修改Preview Provider

步骤4 在LandmarkRow_Previews的静态属性previews中给LandmarkRow初始化器中传入landmark参数,这个参数使用landmarkData数组的第一个元素。预览视图当前显示Hello, World

landmark row layout

步骤5 在一个HStack中嵌入一个Text

步骤6 修改这个Text,让它使用landmark属性的name字段

步骤7 在Text视图前面添加一个图片视图,在Text视图后面添加Spacer视图

landmark layout 1

第三节 自定义行预览

Xcode的画布会自动识别当前代码编辑器中遵循PreviewProvider协议的类型,并将它们渲染并展示在画面上。一个视图预览提供者(preview provider)返回一个或多个视图,这些视图可以配置不同的大小和设备型号。

可以定制从preview provider中返回的视图被渲染在何种场景下。

row preivew

步骤1 在LandmarkRow_Previews中,把landmark参数更新为landmarkData数组的第二个元素,预览视图会立即刷新反映第二个元素的渲染情况

preivew row 2

步骤2 使用previewLayout(_:)修改器设置一个行视图在列表中显示的尺寸大小。可以使用Group的方式,返回多个不同场景下的预览视图

preview layout size

步骤3 把预览的行视图包裹在Group中,把之前的第一个行视图也加进去。Group是一个容器,它可以把视图内容组织起来,Xcode会把Group内的每个子视图当作画布内一个单独的预览视图处理

preview group size

步骤4 为了简化代码,可以把previewLayout(_:)这个修改器应用到外层的Group上,Group的每一个子视图会继承自己所处环境的配置。对preivew provider的修改只会影响预览画布的表现,对实际的应用不会产生影响。

preview group coniguration


收起阅读 »

iOS SwiftUI 创建和组合视图 1.3

第六节 组合地标详情页前面我们创建了个地标详情页所需要的各种子视图元素:名称、地点、圆形图片以及位置地图,现在可以把这些视图元素组合在一起形成地标详情页的整个视图在项目工程浏览器中选择ContentView.swift文件body属性中嵌入一个VStack视图...
继续阅读 »

第六节 组合地标详情页

前面我们创建了个地标详情页所需要的各种子视图元素:名称、地点、圆形图片以及位置地图,现在可以把这些视图元素组合在一起形成地标详情页的整个视图

swiftui combine view begin

  1. 在项目工程浏览器中选择ContentView.swift文件

  2. body属性中嵌入一个VStack视图,它内部包含另一个VStack视图,内部的VStack视图又包含三个Text视图

  3. 在外层VStack的顶部添加自定义的地图视图MapView,并使用frame(width:height:)设置视图大小。当只指定高度时,宽度会自动计算为父视图的宽度,在这里就是屏幕宽度

  4. 点击Live Preview按钮进入实时预览模式,查看地图渲染情况。在实时预览模式下可以编辑视图,最新的改动也可以实时的刷新出来。

  5. MapView后面再添加一个CircleImage视图

  6. 为了让图片视图叠放在地图视图的上面,可以设置图片视图的垂直偏移量为-130,图片视图的底部内边距也为-130,这个效果就是把图片垂直上移了130,同时和下面的文字区域留出了130的空白分隔区

  7. 在外层VStack内部的最下面加上Spacer,可以让上面的视图内容顶到屏幕的上边

  8. 为了让地图的视图内容显示在状态栏的下方,可以给MapView添加edgesIgnoringSafeArea(.top)修改器,这可以让它在布局时忽略顶部的安全区域边距

swiftui combine view completed

检查是否理解

问题1 在声明自定义SwiftUI视图时,视图布局要声明的在哪里?

  •  在视图初始化器中
  •  body属性中
  •  layoutSubviews方法中

View协议中要求实现body属性,每一个SwiftUI视图都遵循View协议

问题2 代码布局的视图是以下哪个?

swiftui combine view problem2

  • swiftui combine view problem2-1
  • swiftui combine view problem2-2
  • swiftui combine view problem2-3

问题3 下面哪种方法是从body属性中返回三个视图的正确方法?

  • swiftui combine view problem3-1
  • swiftui combine view problem3-2
  • swiftui combine view problem3-3

问题4 配置视图时,下面哪种是正确使用修改器的方式?

  • swiftui combine view problem4-1
  • swiftui combine view problem4-2
  • swiftui combine view problem4-3

修改器每次都是返回一个新的对象,所以多个修改器可以通过链式调用

收起阅读 »

iOS SwiftUI 创建和组合视图 1.2

第四节 创建自定义图像视图(Image)有了地标名称、地标位置及状态视图,下一步再添加一个地标图片视图。这个图片视图将自定义遮罩(mask)、边框(border)和阴影(shadow)从控件加中拖一个Image到画布,或直接写代码到代码编辑器中步骤1 ...
继续阅读 »

第四节 创建自定义图像视图(Image)

有了地标名称、地标位置及状态视图,下一步再添加一个地标图片视图。这个图片视图将自定义遮罩(mask)、边框(border)和阴影(shadow)

从控件加中拖一个Image到画布,或直接写代码到代码编辑器中

步骤1 在项目资源文件中找到turtlerock.png图片,把它拖入资源编辑器(asset catalog editor)中,Xcode会创建一个新的图片集来存放这个图片,然后创建一个SwiftUI视图

swiftui assets catalog editor

步骤2 选择文件->新建->文件,打开模板选择器。在用户界面(User Interface)板块下,选择SwiftUI View并点击下一步,命名为CircleImage.swift,并点击创建(Create)。现在你已经准备好插入图片并修改布局来满足设计目标

swiftui create swiftui file

swiftui create circle image

步骤3 用Image替换Text,并使用turtlerock图片初始化Image视图

步骤4 添加clipShape(Circle())修改器到Image,给图片添加圆形剪切效果。Circle是一个形状,它可以被用作遮罩、也可以是圆圈,还可以是圆形填充视图。

步骤5 创建另一个灰色的圆圈并把它作为一个浮层添加到图片上,相当于给图片加了一个灰色边框

步骤6 给视图添加半径为10的阴影

swiftui turtlerock overlay

步骤7 把圆形边框的颜色改成白色,就完成了自定义图片视图的创建。

swiftui circle image completed

第五节 UIKit视图与SwiftUI视图混合使用

现在要创建一个地图视图,可以使用MapKit中的MKMapView视图类来渲染地图。要在SwiftUI中使用UIView及其子类,需要把这些UIView包裹在一个遵循UIViewRepresentable协议的SwiftUI视图中,SwiftUI中也包含适配WatchKitAppKit的类似的协议。

swiftui uikit swiftui combine

首先需要创建一个自定义视图用来容纳和显示MKMapView

步骤1 选择文件->新建->文件,选择iOS平台,选择SwiftUI View模板,并点击下一步(Next),命名文件为MapView.swift,并点击创建(Create)

步骤2 代码中导入MapKit引用,声明MapView遵循UIViewRepresentable协议。UIViewRepresentable协议要求实现两个方法UIView(context:)updateUIView(_:context:),第一个方法用来创建MKMapView,第二个方法用来配置视图响应状态变化

步骤3 替换body,用makeUIView(context:)方法来代替,创建并返回一个空的MKMapView

步骤4 创建方法updateUIView(_:context:),在方法内部设置地图视图的坐标为Turle Rock的中心。在静态模式下预览时,只会渲染swiftUI视图的部分,因为MKMapViewUIView的子类,所以需要切换到实时预览模式下才能看到地图被完全渲染出来

swiftui mapview mkmapview wrapper

步骤5 点击Live Preview(实时预览)按钮,可能需要点击Try AgainResume按钮来激活预览模式的切换。切换到实时预览模式下不久就可以看到指定地标所在的地图位置了

swiftui mkmapview live preview


收起阅读 »

iOS SwiftUI 创建和组合视图 1.1

第三节 使用栈来组合视图上一节创建了标题视图,接下来要添加一些文本视图来描述地标所在州及所在公园的名称等其它详细信息创建SwiftUI视图就是在body属性中描述视图的内容、布局及行为,但body属性只返回单个视图,这时组合多个视图时可以把它们放入一个栈中,通...
继续阅读 »

第三节 使用栈来组合视图

上一节创建了标题视图,接下来要添加一些文本视图来描述地标所在州及所在公园的名称等其它详细信息

swiftui layout stack

创建SwiftUI视图就是在body属性中描述视图的内容、布局及行为,但body属性只返回单个视图,这时组合多个视图时可以把它们放入一个栈中,通过水平、垂直、前后嵌套多个视图完成视图组合,做为一个整体在body属性中返回

这一节中,使用一个垂直栈,把标题放在包含公园详情的水平栈的上方,在水平栈中,布局公园详情相关的内容

可以使用Xcode提供的结构化布局来把视图嵌套在容器视图中

步骤1 按下Command键的同时,点击Text视图的初始化代码打开结构化编辑弹窗,然后选择把控件嵌套在垂直栈中(Embed in VStack),在栈中添加Text View控件可以从组件中直接拖进栈中完成

swiftui view embed in vertical stack

步骤2 点击Xcode右上角的+号,托动一个Text控件到指定位置,代码立即就会在编辑器中补全

步骤3 把Text视图的占位文本修改为Joshua Tree Nation Park,视图会自动调整位置布局

步骤4 设置位置控件的字体为子标题样式

swiftui inspector add text view

步骤5 设置VStack初始化参数为左对齐内部的子视图。默认情况下,栈会把内部视图在自己的主轴上居中对齐,并自动计算各子视图的间距。下一步要添加一个Text控制用来描述公园的状态,它水平排列在位置信息的右边。

swiftui vstack leadng alignment

步骤6 在画布内,command按下的同时点击位置视图,在弹出的菜单中选择嵌入到水平栈中(Embed in HStack)

步骤7 在位置控件的后面加一个公园状态的Text视图,并把占位文字改为California,字体设置为子标题样式

步骤8 为了水平布局使用整个屏幕宽度,在位置控件和公园状态控件中间添加一个Spacer控件,用来填充两个控件中间的空白部分,并把两个控件分别顶向屏幕的两侧。Spacer是一个可以伸缩的空白控件,他负责占用其它控件布局完成后剩下的所有空间。

步骤9 使用padding()修改器给地标信息内容视图整体加内边距

swiftui embed in hstack

收起阅读 »

❤️Android 安装包体积优化❤️

介绍 实际开发应用时,包体积优化是必不可少的。毕竟手机内存有限,如果包体积过大很多用户会直接放弃(以前手机内存很小的时候,这个真的很重要),现在由于手机内存大了(512G已经挡不住了),现在的用户更关注流畅度和美观作为参考,但是该有的优化还是要优化的,毕竟要尽...
继续阅读 »

介绍


实际开发应用时,包体积优化是必不可少的。毕竟手机内存有限,如果包体积过大很多用户会直接放弃(以前手机内存很小的时候,这个真的很重要),现在由于手机内存大了(512G已经挡不住了),现在的用户更关注流畅度和美观作为参考,但是该有的优化还是要优化的,毕竟要尽善尽美嘛。


本文主要分两部分:安装包监控、安装包大小优化。


安装包监控


Android Studio 的 APK Analyser


这是 Android Studio 提供的一个 APK 检测工具,通过它可以查看一个 apk 文件内部各项内容所占的大小,并且按照大小排序显示。因此我们很容易观察到 APK 中哪一部分内容占用了最大空间。APK Analyzer 的使用非常简单,只要将需要分析的 APK 文件拖入 Android Studio 中即可(直接点击项目中的apk也可以),显示内容类似下图所示:





从上图中可以看出classes.dex都代码文件,不是很好东,看图片也占用了比较大的资源空间,因此可以针对性地对其做压缩优化等操作。(我这个项目是个demo所以显得代码占比较多。实际项目中肯定是图片资源占比相对较大)



从上图看出,实际上 APK Analyzer 的作用不光是查看 APK 大小,从它的名字也能看出它是用来分析 APK 的,因此可以使用它来分析一些优秀 APK 的目录结构、代码规范,甚至是使用了哪些动态库技术等。



Matrix中 的 ApkChecker(传送)


Matrix是微信终端自研和正在使用的一套APM(Application Performance Management)系统。 Matrix-ApkChecker 作为Matrix系统的一部分,是针对android安装包的分析检测工具,根据一系列设定好的规则检测apk是否存在特定的问题,并输出较为详细的检测结果报告,用于分析排查问题以及版本追踪。


安装包优化实践


Lint查找无用文件


使用 Lint 查看未引用资源。Lint 是一个静态扫描工具,它可以识别出项目中没有被任何代码所引用到的资源文件。具体使用也很简单,只要在 Android Studio 中点击 Analyze -> Inspect Code,如下所示:



选中整个项目,如下所示



如果项目中有未被使用资源,则 Lint 会在窗口 Inspection Result 中显示,类似结果如下:



低效布局权重:提供优化方案。



上面就是未使用的资源:会使应用程序变大,并降低构建速度。


还有很多就不多介绍了,感兴趣的可以去玩玩。


启用压缩、混淆和优化功能


当你使用 Android Studio 3.4 或 Android Gradle 插件 3.4.0 及更高版本时,R8 是默认编译器,用于将项目的 Java 字节码转换为在 Android 平台上运行的 DEX 格式。不过,当您使用 Android Studio 创建新项目时,缩减、混淆处理和代码优化功能默认处于停用状态


        debug{
// 启用代码收缩、模糊处理和优化
minifyEnabled true
// 资源缩减
shrinkResources true
//包括与打包在一起的默认ProGuard规则文件
//R8配置文件。
proguardFiles getDefaultProguardFile(
'proguard-android-optimize.txt'),
'proguard-rules.pro'
}

未启用



启用后



文件优化


图片优化


降低图片bit



不需要太精致的图片可以将图中32 bit降至16 bit或者8 bit,


使用 VectorDrawable 图片


UI小姐姐能提供最好不能提供,咱们自己自己造。


Android Studio 中点击 File > New > Vector Asset



Clip Art 项为固有的矢量图,我们直接用即可,而Local file(SVG,PSD)选项,则是我们需要转换的了,剩下的提示进行就可以啦。


使用 tint 着色器


纯色图片,仅修改颜色就要再导入一张图片,比较占资源,这时我们使用tint就避免浪费资源和时间。



<ImageView
android:layout_width="200dp"
android:src="@drawable/ic_vector"
android:layout_marginTop="@dimen/dimen_20"
android:layout_gravity="center_horizontal"
android:layout_height="200dp"/>
<ImageView
...其他属性跟上面一致
android:tint="@color/color_188FFF"
tools:ignore="UseAppTint" />


tools:igore的作用就是忽略一些指定错误,或者 抑制警告。



使用 webp 格式图片


使用webp格式的图片可以在保持清晰度的情况下减小图片的磁盘大小,是一种比较优秀的,google推荐的图片格式。


选中图片>右键>选择




图片由.png转为.webp


三方库优化


在 App 中会引入各种三方的"轮子",但是在引入之前最好权衡一下是否需要将其代码全部引入,造成不必要的代码或者资源也被打包到 APK 中。


例如Facebook全家桶,你不可能全部用到仅导入部分即可,如登入和分享


dependencies {
// Facebook Core only (Analytics)
implementation 'com.facebook.android:facebook-core:11.1.0'
// Facebook Login only
implementation 'com.facebook.android:facebook-login:11.1.0'
// Facebook Share only
implementation 'com.facebook.android:facebook-share:11.1.0'
// Facebook Messenger only
implementation 'com.facebook.android:facebook-messenger:11.1.0'
// Facebook App Links only
implementation 'com.facebook.android:facebook-applinks:11.1.0'
// Facebook Android SDK (everything)
implementation 'com.facebook.android:facebook-android-sdk:11.1.0'
}

仅需导入
dependencies {
implementation 'com.facebook.android:facebook-login:11.1.0'
implementation 'com.facebook.android:facebook-share:11.1.0'
}

例如XRecyclerView一个 RecyclerView 实现了 pullrefresh 、loadingmore 和 header featrues。你可能仅用到 loadingmore,那你就可以将关于loadingmore部分截取出来。而不用导入整个包。


关于 App Bundle


这个功能就跟ios一样了,他们就是将所有资源全部打到项目中,然后App Store,根据安装设备的属性,来选取相应资源打包进行下载。


谷歌的 Dynamic Delivery 功能就天然地解决了这个问题,通过 Google Play Store 安装 APK 时,也会根据安装设备的属性,只选取相应的资源打包到 APK 文件中。


如下图,你上传的700MB大小的aab,但是你下载的话会有两套资源打在apk中,但是用户下载仅一套资源700MB。



但是 App Bundle 目前只适合在 Google Play Store 上发布的项目,国内目前还是通过各家的插件化方案来实现动态部署,一定程度上也可以算作减少安装包大小的方案。


还有一个骚操作,就是前期资源打包,后续资源用户边玩边下载,缺点可能造成卡顿和浪费流量,仅供参考。


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

重新理解为什么 Handler 可能导致内存泄露?

总说 Handler 使用不当会导致内存泄露,真正的原因到底是什么? 网上千篇一律的答案貌似没有说到点子上,本文带你重新理解个中细节! 什么是 Handler 使用不当? 先搞清楚什么叫 Handler 使用不当? 一般具备这么几个特征: Handler 采...
继续阅读 »

总说 Handler 使用不当会导致内存泄露,真正的原因到底是什么?


网上千篇一律的答案貌似没有说到点子上,本文带你重新理解个中细节!


什么是 Handler 使用不当?


先搞清楚什么叫 Handler 使用不当?


一般具备这么几个特征:



  1. Handler 采用匿名内部类内部类扩展,默认持有外部类 Activity 的引用:


// 匿名内部类
override fun onCreate(savedInstanceState: Bundle?) {
...
val innerHandler: Handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
Log.d(
"MainActivity",
"Anonymous inner handler message occurred & what:${msg.what}"
)
}
}
}

// 内部类
override fun onCreate(savedInstanceState: Bundle?) {
...
val innerHandler: Handler = MyHandler(Looper.getMainLooper())
}

inner class MyHandler(looper: Looper): Handler(looper) {
override fun handleMessage(msg: Message) {
Log.d(
"MainActivity",
"Inner handler message occurred & what:\${msg.what}"
)
}
}


  1. Activity 退出的时候 Handler 仍可达,有两种情况:

    • 退出的时候仍有 Thread 在处理中,其引用着 Handler

    • 退出的时候虽然 Thread 结束了,但 Message 尚在队列中排队处理正在处理中,间接持有 Handler




override fun onCreate(savedInstanceState: Bundle?) {
...
val elseThread: Thread = object : Thread() {
override fun run() {
Log.d(
"MainActivity",
"Thread run"
)

sleep(2000L)
innerHandler.sendEmptyMessage(1)
}
}.apply { start() }
}

为什么会内存泄露?


上述的 Thread 在执行的过程中,如果 Activity 进入了后台,后续因为内存不足触发了 destroy。虚拟机在标记 GC 对象的时候,会发生如下两种情形:




  • Thread 尚未结束,处于活跃状态


    活跃的 Thread 作为 GC Root 对象,其持有 Handler 实例,Handler 又默认持有外部类 Activity 的实例,这层引用链仍可达:





  • Thread 虽然已结束,但发送的 Message 还未处理完毕


    Thread 发送的 Message 可能还在队列中等待,又或者正好处于 handleMessage() 的回调当中。此刻 Looper 通过 MessagQueue 持有该 Message,Handler 又作为 target 属性被 Message 持有,Handler 又持有 Activity,最终导致 Looper 间接持有 Activity。


    大家可能没有注意到主线程的 Main Looper 是不同于其他线程的 Looper 的。


    为了能够让任意线程方便取得主线程的 Looper 实例,Looper 将其定义为了静态属性 sMainLooper


    public final class Looper {
    private static Looper sMainLooper; // guarded by Looper.class
    ...
    public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
    sMainLooper = myLooper();
    }
    }
    }

    静态属性也是 GC Root 对象,其通过上述的应用链导致 Activity 仍然可达。





这两种情形都将导致 Activity 实例将无法被正确地标记,直到 Thread 结束 且 Message 被处理完毕。在此之前 Activity 实例将得不到回收。


其他线程的 Looper 会导致内存泄露吗?


为了便于每个线程方便拿到自己的 Looper 实例,Looper 采用静态的 sThreadLocal 属性缓存了各 Looper 实例。


public final class Looper {
static final ThreadLocal sThreadLocal = new ThreadLocal();
...
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
}

那 sThreadLocal 作为静态属性也是 GC Root 对象,从这个角度讲会不会也间接导致 Message 无法回收呢?


答案是不会,因为 ThreadLocal 内部的 Map 采用弱引用持有 Looper 对象,不会导致 Looper 及引用链实例无法被回收。


public class ThreadLocal {
...
static class ThreadLocalMap {
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
}
}

内部类 Thread 也会导致 Activity 无法回收吧?


为了侧重阐述 Handler 导致的内存泄漏,并没有针对 Thread 直接产生的引用链作说明。


上面的代码示例中 Thread 也采用了匿名内部类形式,其当然也持有 Activity 实例。从这点上来说,尚未结束的 Thread 会直接占据 Acitvity 实例,这也是导致 Activity 内存泄露的一条引用链,需要留意!


如何正确使用 Handler?


GC 标记的时候 Thread 已结束并且 Message 已被处理的条件一旦没有满足,Activity 的生命周期就将被错误地延长,继而引发内存泄露!


那如何避免这种情况的发生呢?针对上面的特征,其实应该已经有了答案。



  1. 将 Handler 定义为静态内部类


private class MainHandler(looper: Looper?, referencedObject: MainActivity?) :
WeakReferenceHandler(looper, referencedObject) {
override fun handleMessage(msg: Message) {
val activity: MainActivity? = referencedObject
if (activity != null) {
// ...
}
}
}

另外还需要弱引用外部类的实例:


open class WeakReferenceHandler(looper: Looper?, referencedObject: T) : Handler(looper!!) {
private val mReference: WeakReference = WeakReference(referencedObject)

protected val referencedObject: T?
protected get() = mReference.get()
}



  1. onDestroy 的时候纠正生命周期




    • Activity 销毁的时候,如果异步任务尚未结束停止 Thread:


      override fun onDestroy() {
      super.onDestroy()
      thread.interrupt()
      }



    • 同时还要将 Handler 未处理的 Message 及时移除,Message 执行 recycle() 时将重置其与和 Handler 的关系:


      override fun onDestroy() {
      super.onDestroy()
      thread.interrupt()
      handler.removeCallbacksAndMessages(null)
      }





非内部类的 Handler 会内存泄露吗?


上面说过匿名内部类或内部类是 Handler 造成内存泄漏的一个特征,那如果 Handler 不采用内部类的写法,会造成泄露吗?


比如这样:


override fun onCreate(...) {
Handler(Looper.getMainLooper()).apply {
object : Thread() {
override fun run() {
sleep(2000L)
post {
// Update ui
}
}
}.apply { start() }
}
}

仍然可能造成内存泄漏。


虽然 Handler 不是内部类,但 post 的 Runnable 也是内部类,其同样会持有 Activity 的实例。另外,post 到 Handler 的 Runnable 最终会作为 callback 属性被 Message 持有。



基于这两个表现,即便 Handler 不是内部类了,但因为 Runnable 是内部类,同样会发生 Activity 被 Thread 或 Main Looper 不当持有的风险。


结语


回顾一下本文的几个要点:



  • 持有 Activity 实例的内名内部类或内部类的生命周期应当和 Activity 保持一致

  • 如果 Activity 本该销毁了,但异步任务仍然活跃或通过 Handler 发送的 Message 尚未处理完毕,将使得内部类实例的生命周期被错误地延长

  • 造成本该回收的 Activity 实例被别的 ThreadMain Looper 占据而无法及时回收

  • 记得持有 Activity 尽量采用静态内部类 + 弱引用的写法,另外在 Activity 销毁的时候及时地终止 Thread 或清空 Message

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

Kotlin开发中的一些Tips

如果你开始使用Kotlin,我个人的建议是多关注编译后字节码或是反编译后的java代码,这样你会发现更多的细节。单纯只学习语法会让你忽略一些细节,而这可能会是性能问题或bug的来源。 下面我举一些我在使用中碰到的问题,看能否给你启发,想到哪就写到哪了。本篇内容...
继续阅读 »

如果你开始使用Kotlin,我个人的建议是多关注编译后字节码或是反编译后的java代码,这样你会发现更多的细节。单纯只学习语法会让你忽略一些细节,而这可能会是性能问题或bug的来源。


下面我举一些我在使用中碰到的问题,看能否给你启发,想到哪就写到哪了。本篇内容基于Java 8 + Kotlin 1.5.21


1.字符串拼接


java 中我们通常使用StringBuilderconcat或者+等方式来拼接字符串。Kotlin中还可以使用字符串模板和plus


一个简单的例子:


val a = "Hello"
val b = "World"

val c = "$a $b"
val d = "$a $b!"
val e = a.plus(" ").plus(b)
val f = a.plus(" ").plus(b).plus("!")

然后点击Tools - > Kotlin - > Show Kotlin Bytecode -> Decompile就可以对kotlin编译后的字节码进行反编译,从而看到java版本的代码。
在这里插入图片描述
示例代码区别只是末尾多拼接了一个!,结果转换代码略有不同,但+号拼接方式在java中最终也是用 StringBuilder 来进行字符串拼接,所以其实是等价的。这里只是展示不同。


但问题是使用plus的代码,可以看到图中倒数第二行,append中直接是a + " "。感觉有点不对劲,那我们直接看一下字节码:

可以看到创建了两个StringBuilder,也就是说每plus一次,创建一个。也就是说plus内部实现是将左右两个参数传入方法中,然后用StringBuilder拼接。等价关系为:plus(plus(a, " "), b)。所以反编译为什么会那样展示,貌似也能说的通了。


"$a $b"方式字节码如下图:
在这里插入图片描述
所以在kotlin中怎么拼接字符,不用我再多说了吧。和java一样,循环中拼接字符,还是推荐用StringBuilder,如果使用字符串模板不也是每次循环时创建一个StringBuilder吗?


2.lazy


lazy 作用是属性被第一次使用的时候再进行初始化,达到懒加载的目的。


private val name: String by lazy { "weilu" }

lazy有三种初始化模式:
在这里插入图片描述
而默认的模式是 LazyThreadSafetyMode.SYNCHRONIZED ,它确保只有一个线程可以初始化实例。我们看一下具体实现代码:
在这里插入图片描述
源码中使用了@Volatilesynchronized实现了双重检查锁,这样保证了线程安全。但是这也是不小的性能开销。如果我们只是单线程中使用lazy,可以指定LazyThreadSafetyMode.NONE来避免此类问题。
在这里插入图片描述
优化后使用方法:


private val name: String by lazy(LazyThreadSafetyMode.NONE) { "weilu" }

3.companion object


Kotlin类中如果需要写静态属性或方法,需要使用伴生对象( companion object )来创建。下面我列出了几种写法:


class CompanionTest {

companion object {
val TEST_1 = "TEST_1"
const val TEST_2 = "TEST_2"

private val TEST_3 = "TEST_3"
private const val TEST_4 = "TEST_4"

fun test() {
println(TEST_1)
println(TEST_2)
println(TEST_3)
println(TEST_4)
}
}
val test5 = "TEST_5"
private val test6 = "TEST_6"
}

我们看一下生成的代码:
在这里插入图片描述


可以看到,在不加const修饰的情况下,生成了getTEST_1方法。那么调用TEST_1时,其实是调用CompanionTest.Companion.getTEST_1(),这样的代码说实话有点繁琐。


如何可以像Java那样直接读取静态属性,那就像TEST_2一样,加const修饰,这样这个变量就可以内联式的编译,也就不会生成多余的方法。


同时也需要注意一个test5、test6这两个写法的区别。


4.inline


inline 是方法的一个修饰符,用来让方法以内联的方式进行编译。什么是内联,简单说就像是复制了一份方法实现代码进来。


比如我们有一个计算方法add:


fun add(a: Int, b: Int): Int {
return a + b;
}

如果直接使用,反编译后代码如下:


UtilsKt.add(1, 4);

如果添加一个inline 修饰符,反编译后代码如下:


byte a$iv = 1;
int b$iv = 4;
int var10000 = a$iv + b$iv;

其实一般的方法我们不需要添加inline ,否则一调用方法就“复制”一遍,这生成的代码就太多了,体积也会不断变大。所以AS也会给我们警告:


Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types

翻译过来就是:内联对性能的影响是很小的,内联最适合带有函数类型参数的函数。


这里说的很明白,内联适合将函数作为参数传递的方法。如果你看过一些kotlin源码,会发现一些高阶函数let、map、run都是如此。


具体是为什么,我们可以看一个例子:


	private fun testFunction(i: Int, call: (Int) -> String) {
call.invoke(i)
}

fun test() {
testFunction(9) {
it.toString()
}
}

反编译:
在这里插入图片描述
Function1是Kotlin 提供的泛型接口,1 就是说明有一个参数。所以使用lambda表达式实际上每次都是在创建一个FunctionX对象。


然后看一下字节码:


INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
INVOKEINTERFACE kotlin/jvm/functions/Function1.invoke (Ljava/lang/Object;)Ljava/lang/Object; (itf)

里面有使用Integer.valueOf进行自动装箱。然后对于Function1.invoke(Object) : Object,入参会装箱。如果有返回值又会拆箱。


以上两点都是直接使用lambda表达式所带来的性能损耗。避免这种情况有两个方法:



1.可以将lambda表达式赋值给一个变量,然后每次引用该变量,这样既可以避免重复创建函数对象,也可以避免重复装箱拆箱开销。



2.inline内联函数可以避免高阶函数创建函数对象及装箱拆箱开销,但是要注意inline函数体不宜过大。


下图是给testFunction方法加inline后的反编译代码:
在这里插入图片描述


5. Gson解析


主要问题是gson与data class使用时产生的问题。简单说就是即使你的变量声明为不可空(不包含基础类型),且有默认值,如果json中的这个字段是null,那么解析后这个不可空变量也会被赋值为null。这时你在使用这个字段时程序自然会崩溃。


具体的问题原因以及解决方法可以看下面的几篇博客,写的都很清晰详细,这里就不多说了。





这些内容也是我在使用kotlin开发过程中学到和遇到过的,也因此返工了不少之前的代码。。。分享出来也是希望帮你少踩一些坑。


其实还有许多类似文中提到的性能开销,我这里也没法一一列举出来。所以就需要我们在学习和使用中多关注编译后的代码。


记得很早看过一部动漫叫《钢之炼金术师》,里面的核心就是说:获得某种东西,需要以同等的代价来交换。比如我们使用的许多开源框架都非常简单灵活,但是代价就是作者的封装优化。Kotlin的简洁不是没有代价的,里面包含了许多默认的行为,而代价就是一定的性能损耗。如果我们掌握这些细节,扬长避短,或许可以实现“双赢”。


扯多了,哈哈。如果本篇对你有帮助的话,多多点赞支持一下!


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

flutter 风车加载指示组件

前言Flutter 官方提供了诸如 CircularProgressIndicator和 LinearProgressIndicator两种常见的加载指示组件,但是说实话,实在太普通,比如下面这个CircularProgressIndica...
继续阅读 »

前言

Flutter 官方提供了诸如 CircularProgressIndicator和 LinearProgressIndicator两种常见的加载指示组件,但是说实话,实在太普通,比如下面这个CircularProgressIndicator

屏幕录制2021-10-10 下午9.53.09.gif

正好我们介绍到了动画环节,那我们自己来一个有趣的加载指示组件吧。创意送哪来呢,冥思苦想中脑海里突然就响起了一首歌:

大风车吱呀吱哟哟地转,这里的风景呀真好看! 天好看,地好看

没错,这就是当时风靡全中国的放学档,儿童必看节目《大风车》的主题曲。

大风车节目 嗯,我们就自己来个风车动画加载组件吧,最终完成效果如下,支持尺寸和旋转速度的设定。

风车加载组件

接口定义

遵循接口先行的习惯,我们先设计对外的接口。对于一个动画加载组件,我们需要支持两个属性:

  • 尺寸:可以由调用者决定尺寸的大小,以便应用在不同的场合。由于风车加载是个正方形,我们定义参数名为 size,类型为 double
  • 速度:风车是旋转的,需要支持旋转速度调节,以便满足应用用户群体的偏好。我们定义参数名为 speed,单位是转/秒,即一秒旋转多少圈,类型也是 double
  • 旋转方向:可以控制顺时针还是逆时针方向旋转。参数名为 direction,为枚举。枚举名称为 RotationDirection,有两个枚举值,分别是 clockwise 和 antiClockwise(感谢评论区脖子不太长的建议)。

其实还可以支持颜色设置,不过看了一下,大部分风车是4个叶片,颜色为蓝黄红绿组合,这里我们就直接在组件内部固定颜色了,这样也可以简化调用者的使用。 然后是定义组件名称,我们依据英文意思,将组件命名为 WindmillIndicator

实现思路

风车绘制

关键是绘制风车,根据给定的尺寸绘制完风车后,再让它按设定的速度旋转起来就好了。绘制风车的关键点又在于绘制叶片,绘制一个叶片后,其他三个叶片依次旋转90度就可以了。我们来看一下叶片的绘制。叶片示意图如下: 叶片绘制示意 叶片整体在一个给定尺寸的正方形框内,由三条线组成:

  • 红色线:弧线,我们设定起点在底边X 轴方向1/3宽度处,终点是左侧边 Y 轴方向1/3高度处,圆弧半径为边长的一半。
  • 绿色线:弧线,起点为红色线的终点,终点为右上角顶点,圆弧半径为边长。
  • 蓝色线,连接绿色线的终点和红色线的起点,以达到闭合。

有了叶片,其他的就是依次旋转90度了,绘制完后的示意图如下所示: 风车绘制示意

旋转效果

我们把每一个叶片作为独立的组件,按照设定的速度,更改旋转角度即可,只要4个叶片的旋转增量角度同时保持一致,风车的形状就能够一致保持,这样就有风车旋转的效果了。

代码实现

WindmillIndicator定义

WindmillIndicator 需要使用 Animation 和 AnimationController 来控制动画,因此是一个 StatefulWidget。根据我们上面的接口定义,得到WindmillIndicator的定义如下:

class WindmillIndicator extends StatefulWidget {
final size;
// 旋转速度,默认:1转/秒
final double speed;
final direction;
WindmillIndicator({Key? key,
this.size = 50.0,
this.speed = 1.0,
this.direction = RotationDirection.clockwise,
})
: assert(speed > 0),
assert(size > 0),
super(key: key);

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

这里使用了 assert 来防止参数错误,比如 speed 不能是负数和0(因为后面计算旋转速度需要将 speed 当除数来计算动画周期),同时 size 不可以小于0

旋转速度设定

我们使用 Tween<double>设定Animation 的值的范围,begin和 end 为0和1.0,然后每个叶片在构建的时候旋转角度都加上2π 弧度乘以 Animation 对象的值,这样一个周期下来就是旋转了一圈。然后是 AnimationController 来控制具体的选择速度,实际的时间使用毫秒数,用1000 / speed 得到的就是旋转一圈需要的毫秒数。这样即能够设定旋转速度为 speed。代码如下所示:

class _WindmillIndicatorState extends State<WindmillIndicator>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;

@override
void initState() {
super.initState();
int milliseconds = 1000 ~/ widget.speed;
controller = AnimationController(
duration: Duration(milliseconds: milliseconds), vsync: this);
animation = Tween<double>(begin: 0, end: 1.0).animate(controller)
..addListener(() {
setState(() {});
});

controller.repeat();
}

@override
Widget build(BuildContext context) {
return AnimatedWindmill(
animation: animation,
size: widget.size,
direction: widget.direction,
);
}

@override
void dispose() {
if (controller.status != AnimationStatus.completed &&
controller.status != AnimationStatus.dismissed) {
controller.stop();
}

controller.dispose();
super.dispose();
}

这里在initState 里设置好参数之后就调用了controller.repeat(),以使得动画重复进行。在 build 方法里,我们构建了一个AnimatedWindmill对象,将 Animation 对象和 size 传给了它。AnimatedWindmill是风车的绘制和动画组件承载类。

风车叶片绘制

风车叶片代码定义如下:

class WindmillWing extends StatelessWidget {
final double size;
final Color color;
final double angle;

const WindmillWing(
{Key? key, required this.size, required this.color, required this.angle});

@override
Widget build(BuildContext context) {
return Container(
transformAlignment: Alignment.bottomCenter,
transform: Matrix4.translationValues(0, -size / 2, 0)..rotateZ(angle),
child: ClipPath(
child: Container(
width: size,
height: size,
alignment: Alignment.center,
color: color,
),
clipper: WindwillClipPath(),
),
);
}
}

共接收三个参数:

  • size:即矩形框的边长;
  • color:叶片填充颜色;
  • angle:叶片旋转角度。

实际叶片旋转时参照底部中心位置(bottomCenter)旋转(不同位置的效果不一样,感兴趣的可以拉取代码修改试试)。这里有两个额外的注意点:

  • transform参数我们首先往 Y 轴做了 size / 2的平移,这是因为旋转后风车整体位置会偏下size / 2,因此上移补偿,保证风车的位置在中心。
  • 实际叶片的形状是对 Container 进行裁剪得来的,这里使用了 ClipPath 类。ClipPath 支持使用自定义的CustomClipper<Path>裁剪类最子元素的边界进行裁剪。我们定义了WindwillClipPath类来实现我们说的风车叶片外观裁剪,也就是把正方形裁剪为风车叶片形状。WindwillClipPath的代码如下,在重载的 getClip方法中将我们所说的叶片绘制路径返回即可。
class WindwillClipPath extends CustomClipper<Path> {
@override
Path getClip(Size size) {
var path = Path()
..moveTo(size.width / 3, size.height)
..arcToPoint(
Offset(0, size.height * 2 / 3),
radius: Radius.circular(size.width / 2),
)
..arcToPoint(
Offset(size.width, 0),
radius: Radius.circular(size.width),
)
..lineTo(size.width / 3, size.height);

return path;
}

@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
return false;
}
}

风车组件

有了风车叶片组件,风车组件构建就简单多了(这也是拆分子组件的好处之一)。我们将风车组件继承 AnimatedWidget,然后使用 Stack 组件将4个叶片组合起来,每个叶片给定不同的颜色和旋转角度即可。而旋转角度是由叶片的初始角度加上Animation对象控制的旋转角度共同确定的。然后控制顺时针还是逆时针根据枚举值控制角度是增加还是减少就可以了,风车组件的代码如下:

class AnimatedWindmill extends AnimatedWidget {
final size;
final direction;
AnimatedWindmill(
{Key? key,
required Animation<double> animation,
required this.direction,
this.size = 50.0,
}) : super(key: key, listenable: animation);

@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
final rotationAngle = direction == RotationDirection.clockwise
? 2 * pi * animation.value
: -2 * pi * animation.value;
return Stack(
alignment: Alignment.topCenter,
children: [
WindmillWing(
size: size,
color: Colors.blue,
angle: 0 + rotationAngle,
),
WindmillWing(
size: size,
color: Colors.yellow,
angle: pi / 2 + rotationAngle,
),
WindmillWing(
size: size,
color: Colors.green,
angle: pi + rotationAngle,
),
WindmillWing(
size: size,
color: Colors.red,
angle: -pi / 2 + rotationAngle,
),
],
);
}

运行效果

我们分别看运行速度为0.5和1的效果,实测感觉速度太快或太慢体验都一般,比较舒适的速度在0.3-0.8之间,当然你想晃晕用户的可以更快些😂😂😂。

速度0.5

速度1

源码已提交至:动画相关源码,想用在项目的可以直接把WindmillIndicator的实现源文件windmill_indicator.dart拷贝到自己的项目里使用。

总结

本篇实现了风车旋转的加载指示动画效果,通过这样的效果可以提升用户体验,尤其是儿童类的应用,绝对是体验加分的动效。从 Flutter学习方面来说,重点是三个知识:

  • AnimationAnimationController 和 AnimatedWidget的应用;
  • Matrix4控制Container 的平移和旋转的使用;
  • 使用 ClipPath 和自定义CustomClipper<Path> 对组件形状进行裁剪,这个在很多场景会用到,比如那些特殊形状的组件。


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

收起阅读 »

kt协程 | suspend非阻塞挂起魔术解密

一 前言 kotin协程,一种轻量级用户态线程,能通过suspend函数避免回调地狱以及快速实现线程的切换等,已经普及到大量实际项目中。这里将解析协程核心功能suspend的「非阻塞式挂起」实现原理,从而避免管中窥豹,使得在后续使用kotlin协程开发能更加得...
继续阅读 »

一 前言


kotin协程,一种轻量级用户态线程,能通过suspend函数避免回调地狱以及快速实现线程的切换等,已经普及到大量实际项目中。这里将解析协程核心功能suspend的「非阻塞式挂起」实现原理,从而避免管中窥豹,使得在后续使用kotlin协程开发能更加得心应手。


二 callback代码同步化



kotlin官方宣传的非阻塞式挂起,即用同步代码写异步操作。其实可以理解,就是通过suspend将代码“用阻塞式的写法实现非阻塞式线程调度”,就是内部帮我们切线程。 先简述同步化写法,下面会逐步分析,kotlin协程是如何通过suspend来实现非阻塞式的代码同步化



2.1 callback代码同步化


假设一个简单的需求,先从网络获取最新的消息,再与本地数据库做增量操作,获取完整消息。 在原生中,回调代码如下:


fun fetch(){
fetchRemote { msg->
fetchLocal(msg) { result ->
//实际业务操作
println("result:$result")
}
}
}

fun fetchRemote(onNext:(Int)->Unit){
Thread.sleep(300)
val value = 1

onNext(value)
}
fun fetchLocal(id:Int,onNext:(Int)->Unit){
Thread.sleep(300)
val value = 2

onNext(id + value)

利用了kotlin协程,可以直接以同步方式:


suspend fun fetch():Int{//用正常同步写法,消除回调
val msg = fetchRemote()
val result = fetchLocal(msg)
println("result:$result")
return result
}

//简单起见,采用suspendCoroutine
suspend fun fetchRemote() = suspendCoroutine {
it.resume(1)
}

suspend fun fetchLocal(id:Int) = suspendCoroutine {
it.resume(id + 2)
}

ok,上面的 suspendFetch函数写法,就是传说中的 “同步代码实现异步操作” 了,简称「代码同步化」


三 suspend解密



备注:为方便理解,下面展示的是伪代码,与实际字节码翻译可能存在不同;



3.1 suspend函数解体


这里先讲解一个声明为suspend的函数,如suspend fun fetch():Int,会如何被kotlin编译器解体,再讲述执行过程。


先总结:kotlin编译器会使用状态机实现回调功能,每一个suspend函数都是状态机的一个状态,suspend就是声明一个状态而已,再往简单地说,编译器会注入代码,内部帮我们实现了回调!



  1. 编译器首先会在suspend函数,加入一个额外的参数 completion: Continuation,比如会将上述的suspend fun fetch():Int变成fun fetch(completion: Continuation):Any,这也额外解释了为何suspend只能被suspend函数或协程内调用。


注意,这里的返回值变成any,是因为除了我们定义的返回值以外,还可能返回挂起标识CoroutineSingletons.COROUTINE_SUSPENDED,也就是用于实现挂起的逻辑



  1. kotlin编译器用状态机机制判断当前会执行哪个代码块,每一个挂起函数都会被当成一个状态点,用label来表示,如fetchRemote是一个label,一个可能会存在挂起的状态,伪代码:


 
fun fetch(completion: Continuation):Any{
...
when(label){
0 -> {//label 为默认值0 ,即fetch函数被第一次调用运行,函数代码此时正常运行,还没接触到任何其他挂起函数
...
label = 1 //下面会运行一个挂起函数,所以状态label立即加1,也就是说label==1,表示代码运行到了第一个挂起函数,此处是fetchRemote()
val state = fetchRemote()
...
return COROUTINE_SUSPENDED
}
1 -> { //label 1 ,表示在遇到第一个挂起函数fetchRemote() 之后,调用resume等方式恢复了调度
...
label = 2 //下面会运行另一个挂起函数,所以状态label立即加1,也就是说label==2,表示代码运行到了第二个挂起函数,此处是fetchLocal()
val state = fetchLocal(id)
...
return COROUTINE_SUSPENDED
}
2 -> {//label 2 ,表示在遇到第二个挂起函数fetchLocal() 之后,调用resume等方式恢复了调度
...
println("result:$result")
return result
}
}
}

再次提下总结:每一个suspend函数都是状态机的一个状态,suspend就是声明一个状态,体现到代码层次就是一个label值来表示。



  1. 到这里,还需要在状态之间分发上一个状态机的执行结果「即,上一个suspend的返回值」。kotlin通过生成一个状态机管理类,存储label和结果值,解决这个问题:



这里的类命名只是为了方便理解



class FetchStateMachine(
completion: Continuation
) : ContinuationImpl(completion) {
var result: Result? = null
var label: Int = 0

override fun invokeSuspend(result: Any?) {
this.result = result
fetch(this)
}
}

先注意这里的invokeSuspend包裹了真实的要执行的协程体,并保存了传进来的执行结果result,负责存储每个suspend函数执行结果以共享。


4.一个小点,就是如何判断它是第一次执行这个suspend函数,也就是初始状态label==0。这里比较简单,直接通过判断completion是不是生成的状态机类就知道了,不是状态机类就代表第一次执行,包裹起来:


val continuation = completion as? FetchStateMachine ?: FetchStateMachine(completion)


  1. 再接上最开始提到的挂起逻辑。是否特别好奇过,究竟协程是如何知道该挂起,该怎么做了?答案很简单,当某个挂起函数,如fetchRemote(),没有调resume时,编译器会让它返回一个CoroutineSingletons.COROUTINE_SUSPENDED结果,这也是为什么返回值会变成Any,然后只要判断result == 挂起标志,代码直接return,就实现挂起了!!是不是很朴实??


val result = fetchRemote(continuation)
if (result == CoroutineSingletons.COROUTINE_SUSPENDED){
return result
}

到了这里,就可以看到编译器对fetch()解体的代码的模样了:


fun fetch(completion: Continuation): Any {
class FetchStateMachine(
completion: Continuation
) : ContinuationImpl(completion) {
var result: Result? = null //执行结果的共享
var label: Int = 0 //判断执行到哪个代码快,挂起函数

override fun invokeSuspend(result: Any?) {//触发状态机运行,调用resumeWith时会触发
this.result = result
suspendFetch(this)
}
}

//第一次执行,包裹成状态机类
val continuation = completion as? FetchStateMachine ?: FetchStateMachine(completion)

val result = continuation.result
val suspended = COROUTINE_SUSPENDED

when (continuation.label) {
0 -> {
//检查是否异常
result.throwOnFailure()
//立即修改label+1
continuation.label = 1
val var0 = fetchRemote(continuation)
if (var0 == suspended){ //表示suspendRemote挂起
return var0
}

//再次触发状态机跑下一个 label1,正常情况不会跑这里。只有当suspendRemote实现是普通函数 suspend fun suspendRemote() = 1,才会触发
fetch(continuation)
}
1 -> {
result.throwOnFailure()
continuation.label = 2
val var0 = fetchLocal(result.value,continuation)
if (var0 == suspended){//这里就相当于一次挂起了
return var0
}
fetch(continuation)
}
2 -> {
result.throwOnFailure()
return result.value
}
else -> throw IllegalStateException("call to 'resume' before 'invoke' with coroutine")
}
}

3.2 执行流程


ok,这里针对编译器解体的代码,讲一下状态机执行过程;



  1. launch协程后,会触发协程体执行,从而第一次调用到fetch()方法,开始执行状态机;

  2. 第一次进来,将completion包装成状态机类,此时label为0,执行到第一个挂起函数fetchRemote()

  3. fetchRemote() 是个普通函数,类似suspend fun a()=1这种只是简单声明suspend的函数,会直接返回函数结果值,递归调度fetch(continuation)


//Decompilerkotlin to java by cfr
public static final Object a(@NotNull Continuation $completion) {
return Boxing.boxInt((int)1);
}


  1. fetchRemote() 是实现了suspendCoroutine/suspendCoroutine的正经挂起函数时,函数会返回一个挂起标志CoroutineSingletons.COROUTINE_SUSPENDED,这也是会什么suspend函数返回值是Any类型,到这里会发生一次挂起;


image.png



  1. 对于fetchRemote,当调用resumeWith恢复调度时,会递归循环调用我们一开始生成的状态机包裹类的invokeSuspend方法,而invokeSuspend方法就是会再次触发自身函数,即fetch()


image.png



  1. 此时触发状态机接着跑此时的label为1,会跑到fetchLocal挂起方法。然后循环递归步骤3 4,直到结束。


这里的执行流程核心就是一个循环递归,从而帮我们内部实现回调。


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

React 中 setState 是一个宏任务还是微任务?

最近有个朋友面试,面试官问了个奇葩的问题,也就是我写在标题上的这个问题。 能问出这个问题,面试官应该对 React 不是很了解,也是可能是看到面试者简历里面有写过自己熟悉 React,面试官想通过这个问题来判断面试者是不是真的熟悉 React 🤣。 面试官的...
继续阅读 »

最近有个朋友面试,面试官问了个奇葩的问题,也就是我写在标题上的这个问题。



能问出这个问题,面试官应该对 React 不是很了解,也是可能是看到面试者简历里面有写过自己熟悉 React,面试官想通过这个问题来判断面试者是不是真的熟悉 React 🤣。


面试官的问法是否正确?


面试官的问题是,setState 是一个宏任务还是微任务,那么在他的认知里,setState 肯定是一个异步操作。为了判断 setState 到底是不是异步操作,可以先做一个实验,通过 CRA 新建一个 React 项目,在项目中,编辑如下代码:


import React from 'react';
import logo from './logo.svg';
import './App.css';

class App extends React.Component {
state = {
count: 1000
}
render() {
return (
<div className="App">
<img
src={logo} alt="logo"
className="App-logo"
onClick={this.handleClick}
/>
<p>我的关注人数:{this.state.count}</p>
</div>
);
}
}

export default App;

页面大概长这样:



上面的 React Logo 绑定了一个点击事件,现在需要实现这个点击事件,在点击 Logo 之后,进行一次 setState 操作,在 set 操作完成时打印一个 log,并且在 set 操作之前,分别添加一个宏任务和微任务。代码如下:


handleClick = () => {
const fans = Math.floor(Math.random() * 10)
setTimeout(() => {
console.log('宏任务触发')
})
Promise.resolve().then(() => {
console.log('微任务触发')
})
this.setState({
count: this.state.count + fans
}, () => {
console.log('新增粉丝数:', fans)
})
}


很明显,在点击 Logo 之后,先完成了 setState 操作,然后再是微任务的触发和宏任务的触发。所以,setState 的执行时机是早于微任务与宏任务的,即使这样也只能说它的执行时机早于 Promise.then,还不能证明它就是同步任务。


handleClick = () => {
const fans = Math.floor(Math.random() * 10)
console.log('开始运行')
this.setState({
count: this.state.count + fans
}, () => {
console.log('新增粉丝数:', fans)
})
console.log('结束运行')
}


这么看,似乎 setState 又是一个异步的操作。主要原因是,在 React 的生命周期以及绑定的事件流中,所有的 setState 操作会先缓存到一个队列中,在整个事件结束后或者 mount 流程结束后,才会取出之前缓存的 setState 队列进行一次计算,触发 state 更新。只要我们跳出 React 的事件流或者生命周期,就能打破 React 对 setState 的掌控。最简单的方法,就是把 setState 放到 setTimeout 的匿名函数中。


handleClick = () => {
setTimeout(() => {
const fans = Math.floor(Math.random() * 10)
console.log('开始运行')
this.setState({
count: this.state.count + fans
}, () => {
console.log('新增粉丝数:', fans)
})
console.log('结束运行')
})
}


由此可见,setState 本质上还是在一个事件循环中,并没有切换到另外宏任务或者微任务中,在运行上是基于同步代码实现,只是行为上看起来像异步。所以,根本不存在面试官的问题。


React 是如何控制 setState 的 ?


前面的案例中,setState 只有在 setTimeout 中才会变得像一个同步方法,这是怎么做到的?


handleClick = () => {
// 正常的操作
this.setState({
count: this.state.count + 1
})
}
handleClick = () => {
// 脱离 React 控制的操作
setTimeout(() => {
this.setState({
count: this.state.count + fans
})
})
}

先回顾之前的代码,在这两个操作中,我们分别在 Performance 中记录一次调用栈,看看两者的调用栈有何区别。


正常操作


脱离 React 控制的操作


在调用栈中,可以看到 Component.setState 方法最终会调用enqueueSetState 方法 ,而 enqueueSetState 方法内部会调用 scheduleUpdateOnFiber 方法,区别就在于正常调用的时候,scheduleUpdateOnFiber 方法内只会调用 ensureRootIsScheduled ,在事件方法结束后,才会调用 flushSyncCallbackQueue 方法。而脱离 React 事件流的时候,scheduleUpdateOnFiberensureRootIsScheduled 调用结束后,会直接调用 flushSyncCallbackQueue 方法,这个方法就是用来更新 state 并重新进行 render。




function scheduleUpdateOnFiber(fiber, lane, eventTime) {
if (lane === SyncLane) {
// 同步操作
ensureRootIsScheduled(root, eventTime);
// 判断当前是否还在 React 事件流中
// 如果不在,直接调用 flushSyncCallbackQueue 更新
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
} else {
// 异步操作
}
}

上述代码可以简单描述这个过程,主要是判断了 executionContext 是否等于 NoContext 来确定当前更新流程是否在 React 事件流中。


众所周知,React 在绑定事件时,会对事件进行合成,统一绑定到 document 上( react@17 有所改变,变成了绑定事件到 render 时指定的那个 DOM 元素),最后由 React 来派发。


所有的事件在触发的时候,都会先调用 batchedEventUpdates$1 这个方法,在这里就会修改 executionContext 的值,React 就知道此时的 setState 在自己的掌控中。


// executionContext 的默认状态
var executionContext = NoContext;
function batchedEventUpdates$1(fn, a) {
var prevExecutionContext = executionContext;
executionContext |= EventContext; // 修改状态
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
// 调用结束后,调用 flushSyncCallbackQueue
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
}
}


所以,不管是直接调用 flushSyncCallbackQueue ,还是推迟调用,这里本质上都是同步的,只是有个先后顺序的问题。


未来会有异步的 setState


如果你有认真看上面的代码,你会发现在 scheduleUpdateOnFiber 方法内,会判断 lane 是否为同步,那么是不是存在异步的情况?


function scheduleUpdateOnFiber(fiber, lane, eventTime) {
if (lane === SyncLane) {
// 同步操作
ensureRootIsScheduled(root, eventTime);
// 判断当前是否还在 React 事件流中
// 如果不在,直接调用 flushSyncCallbackQueue 更新
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
} else {
// 异步操作
}
}

React 在两年前,升级 fiber 架构的时候,就是为其异步化做准备的。在 React 18 将会正式发布 Concurrent 模式,关于 Concurrent 模式,官方的介绍如下。




什么是 Concurrent 模式?


Concurrent 模式是一组 React 的新功能,可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整。在 Concurrent 模式中,渲染不是阻塞的。它是可中断的。这改善了用户体验。它同时解锁了以前不可能的新功能。




作者:Shenfq
链接:https://juejin.cn/post/6992006476558499853

收起阅读 »

正确介绍自己的项目经验 再也不为面试发愁了

在面试时,经过简单寒暄后,面试官一般先从让候选人自我介绍开始,紧接着就是问候选人简历中所列的项目,让介绍下项目经验。常见的问法是,说下你最近的(或感觉不错的)一个项目。面试中很多人忽视对这一个环节的准备,不仅回答不了面试官的追问,甚至连自己的项目都讲不清楚,说...
继续阅读 »

在面试时,经过简单寒暄后,面试官一般先从让候选人自我介绍开始,紧接着就是问候选人简历中所列的项目,让介绍下项目经验。常见的问法是,说下你最近的(或感觉不错的)一个项目。面试中很多人忽视对这一个环节的准备,不仅回答不了面试官的追问,甚至连自己的项目都讲不清楚,说起来磕磕巴巴,甚至有的人说出的项目经验从时间段或技术等方面和简历上的不匹配,这样无疑会让面试官对面试者的能力产生怀疑。


面试时7份靠能力,3份靠技能,本文将从“前期准备”和“面试技巧”两大层面告诉大家如何准备面试时的项目介绍,当然,这只是一家之言,没有最好的方式,只有更适合的方法,仁者见仁智者见智。


前期分析




  1. 知己知彼百战不殆。如果想打动面试官,那么你就必须得了解他到底想要从你口中了解到什么,哪些信息是他所想要的。


    在面试前准备项目描述时,别害怕,因为面试官什么都不知道,最了解你项目的还是你自己。


    面试官是人,不是神,拿到你的简历的时候,只能根据你所描述的项目去推测你的工作经历,是没法核实你的项目细节的(一般公司会到录用后,通过背景调查的方式来核实)。


    更何况,你做的项目是以月或以年为单位算的,而面试官最多用30分钟来从你的简历上了解你的项目经验,所以你对项目的熟悉程度要远远超过面试官,所以你一点也不用紧张。而面试官想了解更多他想知道的你的工作方式及项目中所负责的内容、所用到的技术栈,就不得不从你的介绍中去深挖技术点,以期了解你对项目及技术的了解的深度。


    首先从气势上就要告诉面试官,这项目就是你参与的,有你所负责的功能模块,让面试官不容置疑。


    心态上准备好了,那么就要分析面试官想要考察什么呢?



    • 表达能力。考察表达及逻辑思维能力,看面试者能不能在几分钟就跟一个完全没有参与过项目的人讲清楚这个项目。

    • 实际工作经验。你在项目中中承担了什么角色,做了什么工作。这些工作中的技术含量及跟同事合作情况如何。另外可能会针对某个项目,不断深入问一些技术上的问题,或者是从侧面问技术类实现,这是为了深入核实你做项目的细节及对技术的理解运用。

    • 解决问题能力。一般都会问项目难点,其实就是想知道当你遇到技术或业务难点,是如何思考并解决问题的。

    • 项目复盘及经验总结能力。哪里是自己觉得做的成功的,哪里觉得没做好的,是否还有继续优化的空间。自己所做的项目对个人能力有哪些提升。




  2. 熟能生巧,对答自如


    首先是需要有个充足的准备,写项目经验一定要写自己熟悉的,因为面试官就会根据你写的项目经验提问。在面试前,就要在脑子里过一遍这个项目,准备好说辞,面试的时候自信点。讲清楚这个项目是满足什么需求的,开发过程中遇到哪些困难,自己怎么解决这些困难的。如果你经过充分准备,面试中也能答的很好,那面试官好感度就会增加,相反,如果面试中说的磕磕绊绊,那么可信度就会低了。




  3. 明确目标,控盘引导


    在面试前,你需要明确自己的面试目的,就是通过面试,争取拿到 Offer 。


    最保守的方式就是在自己介绍项目的时候要么就是以面试官为主导,回答很简单,就是面试官问你一句你答一句。这会让面试官失去想了解你的信心,其次也会让自己错失表现自己,凸显重点思想的机会。做好防守虽然也是一种取胜的方式,但并非上策,很容易丢分。


    讲自己简历中所列的项目一定要很清晰明了有逻辑,埋下后续可能会提问到的技术点,也给面试官留下一个好印象。如果项目经验介绍的好,有逻辑和引导力,那么就会给自己带来以下两点明显的好处:



    1. 给面试官带来的第一印象好,会让面试官感觉该候选人表述能力较强。

    2. 一般来说,面试官会根据候选人介绍的项目背景来提问题,假设面试时会问10个问题,那么至少有5个问题会根据候选人所介绍的项目背景来问,候选人如果说的好,那么就可以很好地引导面试官去问后继问题,这就相当于把提问权完全交由自己控制了。


    如果你的工作经验比面试官还丰富的话,甚至还可以控制整个面试流程,甚至遇到 Nice 的面试官的话会以讨论的方式进行沟通。


    既然面试官无法了解你的底细,那么他们怎么来验证你的项目经验和技术?下面总结了一些常用的提问方式。




面试技巧


内容上要对项目进行以下拆分,思考并进行总结,并试着口语化讲出来。



  1. 项目描述。用通俗易懂且简洁的方式简述项目,阐述整个项目及其背景、规模,不要有太多的技术词汇。

  2. 项目模块。2-3分钟的流程介绍,详细的列出项目实现的功能,各个模块,整个过程,大概思路。

  3. 项目技术栈。说出项目实现的技术栈和架构,能说出项目的不寻常之处,比如采用了某项新技术,采用了某个架框等,简要说明技术选型。

  4. 候选人的角色及责任。说出你在项目中的责任,所涉及的功能模块,运用的技术,碰到的难题、代码细节,技术点、应对措施。

  5. 项目总结,待优化点


方法上可以使用万能的STAR原则


Situation(背景):做这个项目的背景是什么,比如这是个人项目还是团队项目,为什么需要做这个
项目,你的角色是什么,等等。

Target(目标):该项目要达成的目标是什么?为了完成这个目标有哪些困难?

Action(行动):针对所要完成目标,你做了哪些工作?如何克服了其中一些困难?

Result(结果):项目最终结果如何?有哪些成就?有哪些不足之处可以改进?


除了项目所取得的成绩之外,候选人还可以说明自己做完项目的感受,包括项目中哪些环节做的不错,哪些环节有提高的空间,自己在做这个项目中有何收获等。


无论是介绍自己的IT产品开发经历,还是在其他公司的实习项目经历,候选人都可以运用STAR法则来具体说明,轻松表现出自己分析阐述问题的清晰性、条理性和逻辑性。


但面试前如下的一些情况还是需要多加注意的。




  1. 回答很简单。问什么答什么,往往就一句话回答。如果你日常回答别人的问题或者之前面试中出现过类似情况就要有所改善了。这里应该将你知道的说出来,重点突出跟问题相关的思想、框架或技术点等。




  2. 扯闲篇,大忌。说少了太过于简短没有互动不好,自来熟,回答问题没有重点,没有逻辑,乱说一通也是大忌。会让面试官感觉你思路混乱,抓不到重点,只是拿其他方面的东西东拼西凑。




  3. 说的太过流利,也未必就是好事。虽然面试有所准备在面试官看来是好事,但是机械的准备好答案去背诵,主观上给人一种你并没有理解这个问题,只是靠记忆知道答案,后续面试官的问题也会相应的加大难度。这方面改善建议是适当停顿,做思考状,边思考边说,过程中同面试官有个眼神上的互动。




  4. 有的放矢的介绍技术细节。不要一次性过多的介绍技术细节,技术面点到为止,等面试官来问。因为面试官通常都有自己的面试节奏。所以技术点等着问的时候再多聊,可以先事先埋下技术点引导着面试官继续追问。




  5. 主动介绍项目亮点。因为面试官没有义务挖掘你的亮点,所以这就需要自己主动提。遇到不会的问题,就如实说这个技术点不会。或者半懂也可以直接说。甚至可以谈谈自己的见解。把自己了解的说说。




项目准备


一般来说,在面试前,大家应当准备项目描述的说辞,自信些,因为这部分你说了算,流利些,因为你经过充分准备后,可以知道你要说些什么。而且这些是你实际的项目经验(不是学习经验,也不是培训经验),那么一旦让面试官感觉你都说不上来,那么可信度就很低了。


不少人是拘泥于“项目里做了什么业务,以及代码实现的细节”,这就相当于把后继提问权直接交给面试官。下表列出了一些不好的回答方式。


不露痕迹地说出面试官爱听的话


在项目介绍的时候(当然包括后继的面试),面试官其实很想要听一些关键点,只要你说出来,而且回答相关问题比较好,这绝对是加分项。我在面试别人的时候,一旦这些关键点得到确认,我是绝对会在评语上加上一笔的。


下面列些面试官爱听的关键点和对应的说辞。



一旦有低级错误,可能会直接出局


面试过程中有些方面你是绝对不能出错,所以你在准备过程中需要尤其注意如下的因素。下面列了些会导致你直接出局的错误回答。



面试场景题


举一个例子,比如考察候选人是否聪明,star 法则会这样询问:


1.在刚才的项目中,你提到了公司业务发展很快,人手不够,你是如何应对的呢?
2.在你的项目里面解决了什么样的难题
3.在你的项目里面如何做的登录
4.前端的项目如何进行优化,移动端呢?
5.图片加载失败要做啥
6.让你带领一个小团队完成一个项目,你会怎么做?
7.项目的同源处理,跨域相关
8.如果再做这个项目,你会在哪些方面进行改善?
面试中,如果面试官让你描述一个自己比较得意的项目的时候,一定记得要遵循 STAR 法则进行回答。比如


为了整合 xxx 业务(S),我承担 xxx 角色,具体负责 xxx (T)。做了 xxx 事情(A),最后产生了 xxx 结果


然后在描述项目亮点的时候也一样,比如


由于项目 xxx 原因(S),我需要进行 xxx 改进(T),然后进行了 xxx 处理(A),最后产出了 xxx 结果,数据对比为 xxx


整体这样下来,会显得你很有思考力,且具有行动力,可以给企业创造出价值,这也是面试官评定候选人最关键的指标之一。


面试官的套路

面试时所问的问题基本分为两种:具象的问题和开放性的问题。


具象的问题基本都会参考工作经验按照 STAR 法则来进行,主要是了解基本的素养,技术深度和潜力。


开放性的问题基本是考察思维发散能力,考察在某个领域的深度和广度,基本上会结合技术问题来问,或者是结合工作内容来问。


比如:实现某种技术的 n 种方法?某种技术的实现原理?和什么什么相比有哪些优缺点?你对这项技术的思考是什么?


面试者的应对

1.就实际情况做回答,提前准备的时候多发散,多思考,多总结。这一块是可以自己准备的加分项。


2.发散性问题主要是看自己平时积累。首先基础知识要牢固,同时也要了解最新技术动态。面对这类问题切记也不能答非所问而跑题了。


注意:


1.避免拿别人的项目直接用


很多初级阶段的同学们,可能并没有实际的商业项目,或者所做过的项目类型有限,就直接从网上找项目当做自己的项目,直接使用是断不可取的,但是如果你仿造别人的项目自己去尝试着将功能实现,有自己的新得体验,这样在做的过程中也可以对项目中的功能点和技术栈有进一步的了解,不至于在面试的时候,磕磕巴巴,甚至将项目时间都搞错。


2.避免低级错误


很多基础相关的低级错误一定要杜绝,如果被问到熟悉知识点就多答,不熟悉就直接说不熟悉。每个人都有自己擅长的点也有不擅长的。


另外就是可以引导一些话题,不要自说自话。很多人会一直很激进的表达自己,反而显得强势。有的面试者被问到数据库相关内容,他不仅回答数据库,还会把大数据处理技术全部都说出来。其实点到为止最好,面试官感兴趣会继续问,但是你一直主导话题,会减分。


这里要说的是,不要把不是自己做的项目说成是自己做的,自己不是核心负责人说成是负责人,即使你对项目很熟悉了解,像我们一线起来的面试官,问几个问题就很清楚你实际参与了多少了,只是大部分不会明说而已,反而起到反效果。


总结


首先我要劝大家,认真对待每一次面试。既然知道自己要参加面试,就在家自己模拟一下面试。自己提前准备一下自己的项目描述,不要到了面试的时候去打磕巴。但是如果你参加面试的时候实在紧张了,磕巴了不要慌。深呼吸尝试让自己放松,一般面试官也会给些提示帮助你回答的。


两句话,第一,面试前一定要准备,第二,本文给出是的方法,不是教条,大家可以按本文给出的方向结合自己的项目背景做准备,而不是死记硬背本文给出的一些说辞。



作者:Gaby
链接:https://juejin.cn/post/7017732278509453348

收起阅读 »

就因为JSON.stringify,我的年终奖差点打水漂了

产品同学在诉苦:线上用户不能提交表单了,带来了好多客诉,估计会是p0故障,希望尽快解决。 测试同学在纳闷:这个场景测试和预发环境明明验过的,怎么线上就不行了。 后端同学在讲原因:接口缺少了value字段,导致出错了。 就是木有人说问题怎么解决!!! 就是木有人...
继续阅读 »

产品同学在诉苦:线上用户不能提交表单了,带来了好多客诉,估计会是p0故障,希望尽快解决。


测试同学在纳闷:这个场景测试和预发环境明明验过的,怎么线上就不行了。


后端同学在讲原因:接口缺少了value字段,导致出错了。


就是木有人说问题怎么解决!!!


就是木有人说问题怎么解决!!!


就是木有人说问题怎么解决!!!


这样的场景不知道你是不是也似曾相识呢?o(╥﹏╥)o,不管咋说第一要务还是先把线上问题解决掉,减少持续影响,赶紧把交接的代码翻出来,开始了排查过程。


问题原因



如下图:有这样一个动态表单搜集页面,用户选择或者填写了信息之后(各字段非必填情况下也可以直接提交),接着前端把数据发送给后端,结束,看起来没有多复杂的逻辑。



image.png


直接错误原因



非必填情况下,signInfo字段中经过JSON.stringify后的字符串对象缺少value key,导致后端parse之后无法正确读取value值,进而报接口系统异常,用户无法进行下一步动作。



// 异常入参数据,数组字符串中没有value key
{
signInfo: '[{"fieldId":539},{"fieldId":540},{"fieldId":546,"value":"10:30"}]'
}

// 正常入参数据
{
signInfo: '[{"fieldId":539,"value":"银卡"},{"fieldId":540,"value":"2021-03-01"},{"fieldId":546,"value":"10:30"}]'
}



异常数据是如何产生的


// 默认情况下数据是这样的
let signInfo = [
{
fieldId: 539,
value: undefined
},
{
fieldId: 540,
value: undefined
},
{
fieldId: 546,
value: undefined
},
]
// 经过JSON.stringify之后的数据,少了value key,导致后端无法读取value值进行报错
// 具体原因是`undefined`、`任意的函数`以及`symbol值`,出现在`非数组对象`的属性值中时在序列化过程中会被忽略
console.log(JSON.stringify(signInfo))
// '[{"fieldId":539},{"fieldId":540},{"fieldId":546}]'


解决方案



问题的原因找到了,解决方式 (这里只讲前端的解决方案,当然也可以由后端解决) 也很简单,将value值为undefined的项转化为空字符串再提交即可。



方案一:新开一个对象处理


let signInfo = [
{
fieldId: 539,
value: undefined
},
{
fieldId: 540,
value: undefined
},
{
fieldId: 546,
value: undefined
},
]

let newSignInfo = signInfo.map((it) => {
const value = typeof it.value === 'undefined' ? '' : it.value
return {
...it,
value
}
})

console.log(JSON.stringify(newSignInfo))
// '[{"fieldId":539,"value":""},{"fieldId":540,"value":""},{"fieldId":546,"value":""}]'


方案二:利用JSON.stringify第二个参数,直接处理



方案一的缺陷是需要新开一个对象进行一顿操作才能解决,不够优雅



let signInfo = [
{
fieldId: 539,
value: undefined
},
{
fieldId: 540,
value: undefined
},
{
fieldId: 546,
value: undefined
},
]

// 判断到value为undefined,返回空字符串即可
JSON.stringify(signInfo, (key, value) => typeof value === 'undefined' ? '' : value)
// '[{"fieldId":539,"value":""},{"fieldId":540,"value":""},{"fieldId":546,"value":""}]'


故事后续



原本这是一个已经上线有一段时间的页面,为何会突然出现这个问题,之前却没有呢?仔细询问下,原来是中途产品同学提了一个小的优化点,离职的小伙伴感觉点比较小直接就改了代码上线了,未曾想出现了线上问题。



后面针对这件事从产品到测试、到后端、到前端单独做了一个完整的复盘,细节就不再展开说了。


因为从发现问题到解决问题速度较快、影响用户数较少,还未达到问责程度,俺的年终奖可算是保住了o(╥﹏╥)o。


重学JSON.stringify



经过这件事情,我觉得有必要重新审视一下JSON.stringify这个方法,彻底搞清楚转换规则,并尝试手写实现一个JSON.stringify



如果你曾遇到和我一样的问题,欢迎一起来重新学习一次,一定会有不一样的收获噢!


学透JSON.stringify



JSON.stringify()  方法将一个 JavaScript 对象转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。



以下信息来自MDN


语法


JSON.stringify(value[, replacer [, space]])

参数




  • value


    将要序列化成 一个 JSON 字符串的值。




  • replacer 可选



    1. 如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;

    2. 如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;

    3. 如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。




  • space 可选



    1. 指定缩进用的空白字符串,用于美化输出(pretty-print);

    2. 如果参数是个数字,它代表有多少的空格;上限为10。

    3. 该值若小于1,则意味着没有空格;

    4. 如果该参数为字符串(当字符串长度超过10个字母,取其前10个字母),该字符串将被作为空格;

    5. 如果该参数没有提供(或者为 null),将没有空格。




返回值


一个表示给定值的JSON字符串。

异常



  • 当在循环引用时会抛出异常TypeError ("cyclic object value")(循环对象值)

  • 当尝试去转换 BigInt 类型的值会抛出TypeError ("BigInt value can't be serialized in JSON")(BigInt值不能JSON序列化).


基本使用


注意



  1. JSON.stringify可以转换对象或者值(平常用的更多的是转换对象)

  2. 可以指定replacer为函数选择性的地替换

  3. 也可以指定replacer为数组,可转换指定的属性


这里仅仅是NDN上关于JSON.stringify其中最基础的说明,咱们先打个码试试这几个特性


// 1. 转换对象
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy' })) // '{"name":"前端胖头鱼","sex":"boy"}'

// 2. 转换普通值
console.log(JSON.stringify('前端胖头鱼')) // "前端胖头鱼"
console.log(JSON.stringify(1)) // "1"
console.log(JSON.stringify(true)) // "true"
console.log(JSON.stringify(null)) // "null"

// 3. 指定replacer函数
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }, (key, value) => {
return typeof value === 'number' ? undefined : value
}))
// '{"name":"前端胖头鱼","sex":"boy"}'

// 4. 指定数组
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }, [ 'name' ]))
// '{"name":"前端胖头鱼"}'

// 5. 指定space(美化输出)
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }))
// '{"name":"前端胖头鱼","sex":"boy","age":100}'
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }, null , 2))
/*
{
"name": "前端胖头鱼",
"sex": "boy",
"age": 100
}
*/

9大特性要记住



以前仅仅是使用了这个方法,却没有详细了解他的转换规则,居然有9个之多。



特性一



  1. undefined任意的函数以及symbol值,出现在非数组对象的属性值中时在序列化过程中会被忽略

  2. undefined任意的函数以及symbol值出现在数组中时会被转换成 null

  3. undefined任意的函数以及symbol值单独转换时,会返回 undefined


// 1. 对象中存在这三种值会被忽略
console.log(JSON.stringify({
name: '前端胖头鱼',
sex: 'boy',
// 函数会被忽略
showName () {
console.log('前端胖头鱼')
},
// undefined会被忽略
age: undefined,
// Symbol会被忽略
symbolName: Symbol('前端胖头鱼')
}))
// '{"name":"前端胖头鱼","sex":"boy"}'

// 2. 数组中存在着三种值会被转化为null
console.log(JSON.stringify([
'前端胖头鱼',
'boy',
// 函数会被转化为null
function showName () {
console.log('前端胖头鱼')
},
//undefined会被转化为null
undefined,
//Symbol会被转化为null
Symbol('前端胖头鱼')
]))
// '["前端胖头鱼","boy",null,null,null]'

// 3.单独转换会返回undefined
console.log(JSON.stringify(
function showName () {
console.log('前端胖头鱼')
}
)) // undefined
console.log(JSON.stringify(undefined)) // undefined
console.log(JSON.stringify(Symbol('前端胖头鱼'))) // undefined

特性二



布尔值数字字符串的包装对象在序列化过程中会自动转换成对应的原始值。



console.log(JSON.stringify([new Number(1), new String("前端胖头鱼"), new Boolean(false)]))
// '[1,"前端胖头鱼",false]'

特性三



所有以symbol为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。



console.log(JSON.stringify({
name: Symbol('前端胖头鱼'),
}))
// '{}'
console.log(JSON.stringify({
[ Symbol('前端胖头鱼') ]: '前端胖头鱼',
}, (key, value) => {
if (typeof key === 'symbol') {
return value
}
}))
// undefined

特性四



NaN 和 Infinity 格式的数值及 null 都会被当做 null。



console.log(JSON.stringify({
age: NaN,
age2: Infinity,
name: null
}))
// '{"age":null,"age2":null,"name":null}'

特性五



转换值如果有 toJSON() 方法,该方法定义什么值将被序列化。



const toJSONObj = {
name: '前端胖头鱼',
toJSON () {
return 'JSON.stringify'
}
}

console.log(JSON.stringify(toJSONObj))
// "JSON.stringify"

特性六



Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。



const d = new Date()

console.log(d.toJSON()) // 2021-10-05T14:01:23.932Z
console.log(JSON.stringify(d)) // "2021-10-05T14:01:23.932Z"

特性七



对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。



let cyclicObj = {
name: '前端胖头鱼',
}

cyclicObj.obj = cyclicObj

console.log(JSON.stringify(cyclicObj))
// Converting circular structure to JSON

特性八



其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性



let enumerableObj = {}

Object.defineProperties(enumerableObj, {
name: {
value: '前端胖头鱼',
enumerable: true
},
sex: {
value: 'boy',
enumerable: false
},
})

console.log(JSON.stringify(enumerableObj))
// '{"name":"前端胖头鱼"}'

特性九



当尝试去转换 BigInt 类型的值会抛出错误



const alsoHuge = BigInt(9007199254740991)

console.log(JSON.stringify(alsoHuge))
// TypeError: Do not know how to serialize a BigInt

手写一个JSON.stringify



终于重新学完JSON.stringify的众多特性啦!咱们根据这些特性来手写一个简单版本的吧(无replacer函数和space



源码实现


const jsonstringify = (data) => {
// 确认一个对象是否存在循环引用
const isCyclic = (obj) => {
// 使用Set数据类型来存储已经检测过的对象
let stackSet = new Set()
let detected = false

const detect = (obj) => {
// 不是对象类型的话,可以直接跳过
if (obj && typeof obj != 'object') {
return
}
// 当要检查的对象已经存在于stackSet中时,表示存在循环引用
if (stackSet.has(obj)) {
return detected = true
}
// 将当前obj存如stackSet
stackSet.add(obj)

for (let key in obj) {
// 对obj下的属性进行挨个检测
if (obj.hasOwnProperty(key)) {
detect(obj[key])
}
}
// 平级检测完成之后,将当前对象删除,防止误判
/*
例如:对象的属性指向同一引用,如果不删除的话,会被认为是循环引用
let tempObj = {
name: '前端胖头鱼'
}
let obj4 = {
obj1: tempObj,
obj2: tempObj
}
*/
stackSet.delete(obj)
}

detect(obj)

return detected
}

// 特性七:
// 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
if (isCyclic(data)) {
throw new TypeError('Converting circular structure to JSON')
}

// 特性九:
// 当尝试去转换 BigInt 类型的值会抛出错误
if (typeof data === 'bigint') {
throw new TypeError('Do not know how to serialize a BigInt')
}

const type = typeof data
const commonKeys1 = ['undefined', 'function', 'symbol']
const getType = (s) => {
return Object.prototype.toString.call(s).replace(/\[object (.*?)\]/, '$1').toLowerCase()
}

// 非对象
if (type !== 'object' || data === null) {
let result = data
// 特性四:
// NaN 和 Infinity 格式的数值及 null 都会被当做 null。
if ([NaN, Infinity, null].includes(data)) {
result = 'null'
// 特性一:
// `undefined`、`任意的函数`以及`symbol值`被`单独转换`时,会返回 undefined
} else if (commonKeys1.includes(type)) {
// 直接得到undefined,并不是一个字符串'undefined'
return undefined
} else if (type === 'string') {
result = '"' + data + '"'
}

return String(result)
} else if (type === 'object') {
// 特性五:
// 转换值如果有 toJSON() 方法,该方法定义什么值将被序列化
// 特性六:
// Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。
if (typeof data.toJSON === 'function') {
return jsonstringify(data.toJSON())
} else if (Array.isArray(data)) {
let result = data.map((it) => {
// 特性一:
// `undefined`、`任意的函数`以及`symbol值`出现在`数组`中时会被转换成 `null`
return commonKeys1.includes(typeof it) ? 'null' : jsonstringify(it)
})

return `[${result}]`.replace(/'/g, '"')
} else {
// 特性二:
// 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
if (['boolean', 'number'].includes(getType(data))) {
return String(data)
} else if (getType(data) === 'string') {
return '"' + data + '"'
} else {
let result = []
// 特性八
// 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性
Object.keys(data).forEach((key) => {
// 特性三:
// 所有以symbol为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
if (typeof key !== 'symbol') {
const value = data[key]
// 特性一
// `undefined`、`任意的函数`以及`symbol值`,出现在`非数组对象`的属性值中时在序列化过程中会被忽略
if (!commonKeys1.includes(typeof value)) {
result.push(`"${key}":${jsonstringify(value)}`)
}
}
})

return `{${result}}`.replace(/'/, '"')
}
}
}
}

测试一把


// 1. 测试一下基本输出
console.log(jsonstringify(undefined)) // undefined
console.log(jsonstringify(() => { })) // undefined
console.log(jsonstringify(Symbol('前端胖头鱼'))) // undefined
console.log(jsonstringify((NaN))) // null
console.log(jsonstringify((Infinity))) // null
console.log(jsonstringify((null))) // null
console.log(jsonstringify({
name: '前端胖头鱼',
toJSON() {
return {
name: '前端胖头鱼2',
sex: 'boy'
}
}
}))
// {"name":"前端胖头鱼2","sex":"boy"}

// 2. 和原生的JSON.stringify转换进行比较
console.log(jsonstringify(null) === JSON.stringify(null));
// true
console.log(jsonstringify(undefined) === JSON.stringify(undefined));
// true
console.log(jsonstringify(false) === JSON.stringify(false));
// true
console.log(jsonstringify(NaN) === JSON.stringify(NaN));
// true
console.log(jsonstringify(Infinity) === JSON.stringify(Infinity));
// true
let str = "前端胖头鱼";
console.log(jsonstringify(str) === JSON.stringify(str));
// true
let reg = new RegExp("\w");
console.log(jsonstringify(reg) === JSON.stringify(reg));
// true
let date = new Date();
console.log(jsonstringify(date) === JSON.stringify(date));
// true
let sym = Symbol('前端胖头鱼');
console.log(jsonstringify(sym) === JSON.stringify(sym));
// true
let array = [1, 2, 3];
console.log(jsonstringify(array) === JSON.stringify(array));
// true
let obj = {
name: '前端胖头鱼',
age: 18,
attr: ['coding', 123],
date: new Date(),
uni: Symbol(2),
sayHi: function () {
console.log("hello world")
},
info: {
age: 16,
intro: {
money: undefined,
job: null
}
},
pakingObj: {
boolean: new Boolean(false),
string: new String('前端胖头鱼'),
number: new Number(1),
}
}
console.log(jsonstringify(obj) === JSON.stringify(obj))
// true
console.log((jsonstringify(obj)))
// {"name":"前端胖头鱼","age":18,"attr":["coding",123],"date":"2021-10-06T14:59:58.306Z","info":{"age":16,"intro":{"job":null}},"pakingObj":{"boolean":false,"string":"前端胖头鱼","number":1}}
console.log(JSON.stringify(obj))
// {"name":"前端胖头鱼","age":18,"attr":["coding",123],"date":"2021-10-06T14:59:58.306Z","info":{"age":16,"intro":{"job":null}},"pakingObj":{"boolean":false,"string":"前端胖头鱼","number":1}}

// 3. 测试可遍历对象
let enumerableObj = {}

Object.defineProperties(enumerableObj, {
name: {
value: '前端胖头鱼',
enumerable: true
},
sex: {
value: 'boy',
enumerable: false
},
})

console.log(jsonstringify(enumerableObj))
// {"name":"前端胖头鱼"}

// 4. 测试循环引用和Bigint

let obj1 = { a: 'aa' }
let obj2 = { name: '前端胖头鱼', a: obj1, b: obj1 }
obj2.obj = obj2

console.log(jsonstringify(obj2))
// TypeError: Converting circular structure to JSON
console.log(jsonStringify(BigInt(1)))
// TypeError: Do not know how to serialize a BigInt

复制代码

通过上面测试可以看出,jsonstringify基本和JSON.stringify表现一致,(也有可能测试用例不够全面,欢迎提出一起学习)



作者:前端胖头鱼
链接:https://juejin.cn/post/7017588385615200270

收起阅读 »

java中的IO、NIO、Okio

java IO写这种写方法只能一个字节一个字节的写;注意把要关闭的流写在try括号中,省去了代码中finally关闭的过程,以下例子均是。private static void ioWrite() { try (OutputStream outputS...
继续阅读 »

java IO

这种写方法只能一个字节一个字节的写;

注意把要关闭的流写在try括号中,省去了代码中finally关闭的过程,以下例子均是。

private static void ioWrite() {
try (OutputStream outputStream = new FileOutputStream("./demo.txt")) {
outputStream.write('a');
outputStream.write('b');
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

这种读方法只能一个字节一个字节的写;

private static void ioRead() {
try (InputStream inputStream = new FileInputStream("./demo.txt")) {
System.out.println((char)inputStream.read());
System.out.println((char)inputStream.read());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

带缓存的读

private static void ioBufferedRead() {
try (InputStream inputStream = new FileInputStream("./demo.txt");
Reader reader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(reader)) {
System.out.println(bufferedReader.readLine());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

带缓存的写

注意需要

bufferedOutputStream.flush()需要写流数据,但是该方法会在流关闭前被自动调用,因此在try中写了流对象后,可以省去这一步。 此外注意,该方法会覆盖原来文件的内容而不是追加。

private static void ioBufferedWrite() {
try (OutputStream outputStream = new FileOutputStream("./demo.txt");
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
bufferedOutputStream.write('a');
bufferedOutputStream.write('q');
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

带缓存的读写

 private static void ioWriteRead() {
try (
InputStream inputStream = new BufferedInputStream(new FileInputStream("./demo.txt"));
OutputStream outputStream = new BufferedOutputStream(new FileOutputStream("./demoNew.txt"))) {
byte[] data = new byte[1024];
int read;
while ((read = inputStream.read(data)) != -1) {
outputStream.write(data, 0, read);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

IO在网络中实现通信

private static void ioNetDemo() {
try (Socket socket = new Socket("yanfriends.com", 80);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
writer.write("GET / HTTP/1.1\n" +
"Host: http://www.yanfriends.com\n\n");
writer.flush();
String message;
while ((message = reader.readLine()) != null) {
System.out.println(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}

NIO

NIO(New IO)库于JDK1.4引入,目的和IO一致但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。

对比IO:

IONIO
面向流面向缓冲
阻塞IO非阻塞IO
选择器

流和缓存

Java IO是面向流的,这意味着是一次性从流中读取一批数据,这些数据并不会缓存在任何地方,并且对于在流中的数据是不支持在数据中前后移动。如果需要在这些数据中移动(为什么要移动,可以多次读取),则还是需要将这部分数据先缓存在缓冲区中。

NIO采用的是面向缓冲区的方式,有些不同,数据会先读取到缓冲区中以供稍后处理。在buffer中是可以方便地前移和后移,这使得在处理数据时可以有更大的灵活性。但是需要检查buffer是否包含需要的所有数据以便能够将其完整地处理,并且需要确保在通过channel往buffer读数据的时候不能够覆盖还未处理的数据。

阻塞非阻塞

IO流是阻塞式的,当线程调用其read()或write()方法时线程会阻塞,直到完成了数据的读写,在读写的过程中线程是什么都做不了的。

NIO提供了一种非阻塞模式,使得线程向channel请求读数据时,只会获取已经就绪的数据,并不会阻塞以等待所有数据都准备好(IO就是这样做),这样在数据准备的阶段线程就能够去处理别的事情。对于非阻塞式写数据是一样的。线程往channel中写数据时,并不会阻塞以等待数据写完,而是可以处理别的事情,等到数据已经写好了,线程再处理这部分事情。当线程在进行IO调用并且不会进入阻塞的情况下,这部分的空余时间就可以花在和其他channel进行IO交互上。也就是说,这样单个线程就能够管理多个channel的输入和输出了。

Selector

Java NIO中的Selector允许单个线程监控多个channel,可以将多个channel注册到一个Selector中,然后可以"select"出已经准备好数据的channel,或者准备好写入的channel。这个selector机制使得单个线程同时管理多个channel变得更容易。

采用NIO的API调用方式和IO是不一样的,与直接从InputStream中读取字节数据不同,在NIO中,数据必须要先被读到buffer中,然后再从那里进行后续的处理。

读例子

  当线程在进行IO调用并且不会进入阻塞的情况下,这部分的空余时间就可以花在和其他channel进行IO交互上。也就是说,这样单个线程就能够管理多个channel的输入和输出了。

  private static void nioRead() {
try {
RandomAccessFile file = new RandomAccessFile("./demo.txt", "r");
FileChannel channel = file.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
channel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().decode(byteBuffer));
byteBuffer.clear();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

NIO在网络中实现通信

private static void nioNetDemo() {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(80));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
for (SelectionKey key : selector.selectedKeys()) {
if (key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (socketChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}

Okio

Okio的优势

Java IO的读写,缓冲区的存在必然涉及copy的过程,而如果涉及双流操作,比如从一个输入流读入,再写入到一个输出流,在缓冲存在的情况下,数据走向是:

  1. 从输入流读出到缓冲区
  2. 从输入流缓冲区copy到 b[]
  3. 将 b[] copy 到输出流缓冲区
  4. 输出流缓冲区读出数据到输出流

这种操作存在着冗余copy操作,Okio应运而生。除此之外,Okio还简化出了一套对开发者更加友好的API,弥补了IO/NIO使用不方便的缺点。

Segment

Okio使用Segment来作为数据存储手段。Segment 实际上也是对 byte[] 进行封装,再通过各种属性来记录各种状态。在交换时,如果可以,将Segment整体作为数据传授媒介,这样就没有具体数据的copy过程,而是交换了对应的Segment引用。Segment通过Buffer进行缓冲管理,在Buffer.write()里,通过移动引用而不是真实数据,是减少数据copy进而交换数据的关键。

Segment的数据结构如下:

final class Segment {
// 默认容量
static final int SIZE = 8192;
// 最小分享数据量
static final int SHARE_MINIMUM = 1024;
// 存储具体数据的数组
final byte[] data;
// 有效数据索引起始位置
int pos;
// 有效数据索引结束位置
int limit;
// 指示Segment是否为共享状态
boolean shared;
// 指示当前Segment是否为数据拥有者,与shared互斥
// 默认构造函数的Segment owner为true,当把数据分享
// 出去时,被分享的Segment的owner标记为false
boolean owner;
// 指向下一个Segment
Segment next;
// 指向前一个Segment
Segment prev;
}

Okio的依赖链接

private static void okioRead() {
try (BufferedSource source = Okio.buffer(Okio.source(new File("./demo.txt")))) {
System.out.println(source.readUtf8Line());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) { // AIO Asynchronous I/O
e.printStackTrace();
}
}

Android+Okio实例

下面利用OkHttp和Okio实现一个下载网络图片的简单例子:

File file = new File(getCacheDir() + "/demoImg.jpg");
OkHttpClient client = new OkHttpClient();
final Request request = new Request.Builder()
.url("https://avatar.csdnimg.cn/7/E/5/1_lucasxu01.jpg")
.build();
client.newCall(request)
.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
v.post(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "下载出错", Toast.LENGTH_SHORT).show();
}
});
}

@Override
public void onResponse(@NotNull Call call, @NotNull Response response) {
try (BufferedSink sink = Okio.buffer(Okio.sink(apk))) {
sink.write(response.body().bytes());
} catch (IOException e) {
e.printStackTrace();
}
v.post(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "下载成功", Toast.LENGTH_SHORT).show();
}
});
}
});

小结

Okio核心竞争力为,增强了流于流之间的互动,使得当数据从一个缓冲区移动到另一个缓冲区时,可以不经过copy能达到:

  1. 以Segment作为存储结构,真实数据以类型为byte[]的成员变量data存在,并用其它变量标记数据状态,在需要时,如果可以,移动Segment引用,而非copy data数据
  2. Segment在Segment线程池中以单链表存在以便复用,在Buffer中以双向链表存在存储数据,head指向头部,是最老的数据
  3. Segment能通过slipt()进行分割,可实现数据共享,能通过compact()进行合并。由Buffer来进行数据调度,基本遵守 “大块数据移动引用,小块数据进行copy” 的思想
  4. Source 对应输入流,Sink 对应输出流
  5. TimeOut 以达到在期望时间内完成IO操作的目的,同步超时在每次IO操作中检查耗时,异步超时开启另一线程间隔时间检查耗时

Okio并没有打算优化底层IO方式以及替代原生IO方式,Okio优化了缓冲策略以减轻内存压力和性能消耗,并且对于部分IO场景,提供了更友好的API,而更多的IO场景,该记的还得记。


收起阅读 »

ARouter 拦截器之多 module 独立运行

本文说明上篇文章 已分享了路由配置、跳转、原理、完整的效果演示gif以及源码,而且是多 module 项目演示的,算是路由 ARouter 的入门,还没配置使用的可以先去看看。本文的内容主要涉及如下两个:路由拦截器使用module 独立运行前者在我们...
继续阅读 »

本文说明

上篇文章 已分享了路由配置、跳转、原理、完整的效果演示gif以及源码,而且是多 module 项目演示的,算是路由 ARouter 的入门,还没配置使用的可以先去看看。

本文的内容主要涉及如下两个:

  • 路由拦截器使用
  • module 独立运行

前者在我们开发中有这样一种应用场景,默认用户不登录可以浏览一部分页面,当点击部分页面的时候就需要先去登录,也就是跳转到登录页面,普通的做法是根据需求挨个去做点击事件,这就很麻烦,如果需要跳转登录的时候传递参数啥的,那就改动超级大了;而路由ARouter的拦截器功能就很好的解决了这个问题,还支持自定义拦截器,使用起来很灵活。

后者的使用场景适合项目大,多人开发的情景,这样可以各自负责一个模块,独立调试运行,利于项目管理以及代码的维护。这块在上一篇文章的前提下还需要额外配置,本文会讲。

module 独立运行

先来看看module独立运行,然后我们在各个模块做一个模拟的跳转页面需要验证登录的示例,这样比较清晰。

第一步:配置 gradle.properties

gradle.properties 文件中添加如下代码

#是否需要单独运行某个模块 true:表示某个模块不作为依赖库使用
isSingleCircleModule=true
#isSingleCircleModule=false
isSingleHomeModule=true
#isSingleHomeModule=false

第二步:配置app下的build.gradle

在app下的build.gradle文件配置

if (!isSingleCircleModule.toBoolean()) {
implementation project(path: ':circle')
}
if (!isSingleHomeModule.toBoolean()){
implementation project(path: ':home')
}

并注释掉原来的依赖

//    implementation project(path: ':circle')
// implementation project(path: ':home')

第三步:配置各独立模块下的build.gradle

circle模块下build.gradle文件最顶部改动如下:

//plugins {
// id 'com.android.library'
//}

if (isSingleCircleModule.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}

home模块下build.gradle文件最顶部改动如下:

//plugins {
// id 'com.android.library'
//}

if (isSingleHomeModule.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}

第四步:看效果

上面的配置完成后,点击Sync Project with Gradle Files 等待编译完成,可看到如下状态:

Select Run弹窗

这个时候我们选择其中一个module运行,会发现报错如下:

Could not identify launch activity: Default Activity not found
Error while Launching activity

很明显,我们都知道Android程序的主入口是从清单文件配置的,但我们的各module都还没有做这个工作。

circle模块下的清单文件中,配置如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gs.circle">


<application
android:allowBackup="true"
android:icon="@mipmap/app_icon"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">

<activity android:name=".CircleActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

其中的iconlabel以及theme都可以定义在baselib中,这样我们任何 module 配置的时候就可以直接引用,而无需各自复制一份了;除此之外,values文件夹下的东西都可以移动到baselib下,方便其他模块引用,这也就是baselib模块的作用,如果你要细分,还可以j将公共资源放在一个独立的模块里,这个模块通常叫做:commonlib,具体情况而定。

配置完清单文件,运行后发现桌面会多出来一个 APP icon,打开只有一个页面,就是我们的circlemodule的主页面。home模块的清单配置就不展示了,下面看下效果:

module独立运行

这个时候再切回去运行app模块,如果发现有问题,先卸载再运行就ok了。但是会有一个问题,原来可以跳转其他模块的功能,现在跳转不了了,这其实很正常,因为在组件化开发模式下,每个 module 都是独立的app,所以肯定不能直接跳转过去。

那如何实现正常跳转呢?

需要两步,将gradle.properties中的代码修改为如下:

#isSingleCircleModule=true
isSingleCircleModule=false
#isSingleHomeModule=true
isSingleHomeModule=false

接着将circlehome模块的清单文件中的 application属性和默认启动配置项删掉,然后再运行就 ok 了。

如果想将其中一个作为依赖库使用,那么就指匠情挑设置为false即可。

关于组件之间 AndroidManifest 合并问题

其实这个可以在正式打包的时候,注释掉module中的相关代码即可,毕竟是在组件模式。那有没有办法解决每次都要注释的问题呢?答案是yes.

大致思路如下:

在可独立运行的module的res->main文件夹下新建一个文件夹(命名自定义),然后将对应的清单文件复制一份,名称不需要修改,内容的差别就是前面提到的,去掉application属性和默认启动配置项。

接着在对应 module 的 build.gradle 中指定表单的路径,代码如下:

sourceSets {
main {
if (isSingleCircleModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}

这样在不同的开发模式下就会读取到不同的 AndroidManifest.xml ,然后我们需要修改这两个表单的内容以为我们不同的开发模式服务。

单模块独立运行小结

优点:

  • 项目耦合度低,开发效率高,出现问题易排查
  • 利于项目进度管理,分工明确
  • 适合多人大项目

缺点:

  • 前期配置比较复杂,开发过程中需要修改部分配置
  • 稳定性不好把握,毕竟不是google官方出的框架,后期出问题不好处理

其实还有很多问题,实践过的应该明白,每个项目都有自己的独特之处,会有各种各样的奇怪问题,但一般网上我们都可以找到解决方案。

路由拦截器使用

首先还是需要添加几个配置,在工程下的build.gradle文件中添加下面这行代码:

classpath 'com.alibaba:arouter-register:1.0.2'

app模块的build.gradle文件下,配置改动如下:

plugins {
id 'com.android.application'
id 'com.alibaba.arouter' // 拦截器必须配置
}

配置完这两步,按照惯例,该是编译了。

为了演示,我这里在app下新建一个名为LoginActivity的页面,作业登录拦截后跳转的页面,页面内容只有一个提示文本,这里补贴代码。

然后分别在宿主模块app、功能模块circlehome中去做跳转登录页面,看看我们的拦截器是否起到了拦截作用,下面开始定义拦截器。

要独立运行某个模块,这里就不再赘述了,大家自行修改配置即可。

拦截器完整代码如下:

/**
* Description: 登录拦截器
* Date: 2021/10/9 10:42
* <p>
* 拦截器会在跳转之间执行,多个拦截器会按优先级顺序依次执行
* * <p>
* * priority 数值越小权限越高
*/

@Interceptor(priority = 2, name = "登录ARouter拦截器")
public class LoginInterceptor implements IInterceptor {

private Context mContext;

@Override
public void process(Postcard postcard, InterceptorCallback callback) {
boolean isLogin = mContext.getSharedPreferences("arouterdata", mContext.MODE_PRIVATE).getBoolean("isLogin", false);
if (isLogin) {
callback.onContinue(postcard);
} else {
switch (postcard.getPath()) {
// 需要登录的拦截下来
case ARouterPath.APP_MY_INFO:
ARouter.getInstance().build(ARouterPath.LOGIN_PAGE).with(postcard.getExtras()).navigation();
break;
default:
callback.onContinue(postcard);
break;
}
}
}

/**
* 拦截器的初始化,会在sdk初始化的时候调用该方法,仅会调用一次
*
* @param context
*/

@Override
public void init(Context context) {
mContext = context;
}

}

拦截器初始化需要重新安装才会生效,这点要注意。拦截器是不需要我们手动显示调用的,而是框架通过注解来使用的,所以我们只需要写好逻辑代码即可。

以上代码可以实现模块内和跨模块跳转拦截,本地的登录状态我这里没有处理逻辑,所以每次都会被拦截到。下面看效果:

拦截器效果

演示效果模拟进入MyInfoActivity页面时需要先登录,分别从三个模块做了跳转演示。

总结

组件化module独立运行与合并操作起来相对繁琐一点,但优点也很明显。路由框架ARouter的拦截器使用起来就很简单了,其实拦截器完全可以在学完上一篇之后,直接使用,如果组件化多模块独立运行实际项目使用不到,可以先跳过,简单了解流程即可。

Android的框架演变也很快,“三化技术”在两年前特别火,几乎大家都在讨论,但并没有持续多长时间就被新出的技术替代了,而作为一个开发者,自己需要掌握一个基本技能:从零开始搭建一个项目框架,并且这个框架尽可能的要跟上项目的持续发展

收起阅读 »

Android Jetpack系列之Lifecycle

Lifecycle介绍Lifecycle可以让某一个类变成Activity、Fragment的生命周期观察者类,监听其生命周期的变化并可以做出响应。Lifecycle使得代码更有条理性、精简、易于维护。Lifecycle中主要有两个角色:LifecycleOw...
继续阅读 »

Lifecycle介绍

Lifecycle可以让某一个类变成ActivityFragment的生命周期观察者类,监听其生命周期的变化并可以做出响应。Lifecycle使得代码更有条理性、精简、易于维护。

Lifecycle中主要有两个角色:

  • LifecycleOwner: 生命周期拥有者,如Activity/Fragment等类都实现了该接口并通过getLifecycle()获得Lifecycle,进而可通过addObserver()添加观察者。
  • LifecycleObserver: 生命周期观察者,实现该接口后就可以添加到Lifecycle中,从而在被观察者类生命周期发生改变时能马上收到通知。

实现LifecycleOwner的生命周期拥有者可与实现LifecycleObserver的观察者完美配合。

场景case

假设我们有一个在屏幕上显示设备位置的 Activity,我们可能会像下面这样实现:

internal class MyLocationListener(
private val context: Context,
private val callback: (Location) -> Unit) {

fun start() {
// connect to system location service
}

fun stop() {
// disconnect from system location service
}
}

class MyActivity : AppCompatActivity() {
private lateinit var myLocationListener: MyLocationListener

override fun onCreate(...) {
myLocationListener = MyLocationListener(this) { location ->
// update UI
}
}

public override fun onStart() {
super.onStart()
myLocationListener.start()
// manage other components that need to respond
// to the activity lifecycle
}

public override fun onStop() {
super.onStop()
myLocationListener.stop()
// manage other components that need to respond
// to the activity lifecycle
}
}

注:上面代码来自官方示例~

我们可以在Activity 或 Fragment 的生命周期方法(示例中的onStart/onStop)中直接对依赖组件进行操作。但是,这样会导致代码条理性很差且不易扩展。那么有了Lifecycle,可以将依赖组件的代码从Activity/Fragment生命周期方法中移入组件本身中。

Lifecycle使用

根目录下build.gradle:

allprojects {
repositories {
google()

// Gradle小于4.1时,使用下面的声明替换:
// maven {
// url 'https://maven.google.com'
// }
// An alternative URL is 'https://dl.google.com/dl/android/maven2/'
}
}

app下的build.gradle:

    dependencies {
def lifecycle_version = "2.3.1"
def arch_version = "2.1.0"

// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"

// Saved state module for ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"

// Annotation processor
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
// 可选 - 如果使用Java8,使用下面这个代替lifecycle-compiler
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"

// 可选 - 在Service中使用Lifecycle
implementation "androidx.lifecycle:lifecycle-service:$lifecycle_version"

// 可选 - Application中使用Lifecycle
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"

// 可选 - ReactiveStreams support for LiveData
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version"

// 可选 - Test helpers for LiveData
testImplementation "androidx.arch.core:core-testing:$arch_version"
}

Activity/Fragment中使用Lifecycle

首先先来实现LifecycleObserver观察者:

open class MyLifeCycleObserver : LifecycleObserver {

@OnLifecycleEvent(value = Lifecycle.Event.ON_START)
fun connect(owner: LifecycleOwner) {
Log.e(JConsts.LIFE_TAG, "Lifecycle.Event.ON_CREATE:connect")
}

@OnLifecycleEvent(value = Lifecycle.Event.ON_STOP)
fun disConnect() {
Log.e(JConsts.LIFE_TAG, "Lifecycle.Event.ON_DESTROY:disConnect")
}
}

在方法上,我们使用了@OnLifecycleEvent注解,并传入了一种生命周期事件,其类型可以为ON_CREATEON_STARTON_RESUMEON_PAUSEON_STOPON_DESTROYON_ANY中的一种。其中前6个对应Activity中对应生命周期的回调,最后一个ON_ANY可以匹配任何生命周期回调。 所以,上述代码中的connect()、disConnect()方法分别应该在ActivityonStart()、onStop()中触发时执行。接着来实现我们的Activity:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.e(JConsts.LIFE_TAG, "$ACTIVITY:onCreate")

//添加LifecycleObserver观察者
lifecycle.addObserver(MyLifeCycleObserver())
}

override fun onStart() {
Log.e(JConsts.LIFE_TAG, "$ACTIVITY:onStart")
super.onStart()
}

override fun onResume() {
Log.e(JConsts.LIFE_TAG, "$ACTIVITY:onResume")
super.onResume()
}

override fun onPause() {
Log.e(JConsts.LIFE_TAG, "$ACTIVITY:onPause")
super.onPause()
}

override fun onStop() {
Log.e(JConsts.LIFE_TAG, "$ACTIVITY:onStop")
super.onStop()
}

override fun onDestroy() {
Log.e(JConsts.LIFE_TAG, "$ACTIVITY:onDestroy")
super.onDestroy()
}
}

可以看到在Activity中我们只是在onCreate()中添加了这么一行代码:

lifecycle.addObserver(MyLifeCycleObserver())

其中getLifecycle()LifecycleOwner中的方法,返回的是Lifecycle对象,并通过addObserver()的方式添加了我们的生命周期观察者。接下来看执行结果,启动Activity:

2021-06-30 20:57:58.038 11257-11257/ E/Lifecycle_Study: ACTIVITY:onCreate

//onStart() 传递到 MyLifeCycleObserver: connect()
2021-06-30 20:57:58.048 11257-11257/ E/Lifecycle_Study: ACTIVITY:onStart
2021-06-30 20:57:58.049 11257-11257/ E/Lifecycle_Study: Lifecycle.Event.ON_START:connect

2021-06-30 20:57:58.057 11257-11257/ E/Lifecycle_Study: ACTIVITY:onResume

关闭Activity:

2021-06-30 20:58:02.646 11257-11257/ E/Lifecycle_Study: ACTIVITY:onPause

//onStop() 传递到 MyLifeCycleObserver: disConnect()
2021-06-30 20:58:03.149 11257-11257/ E/Lifecycle_Study: ACTIVITY:onStop
2021-06-30 20:58:03.161 11257-11257/ E/Lifecycle_Study: Lifecycle.Event.ON_STOP:disConnect

2021-06-30 20:58:03.169 11257-11257/ E/Lifecycle_Study: ACTIVITY:onDestroy

可以看到我们的MyLifeCycleObserver中的connect()/disconnect()方法的确是分别在ActivityonStart()/onStop()回调时执行的。

自定义LifecycleOwner

AndroidX中的Activity、Fragmen实现了LifecycleOwner,通过getLifecycle()能获取到Lifecycle实例(Lifecycle是抽象类,实例化的是子类LifecycleRegistry)。

public interface LifecycleOwner {
@NonNull
Lifecycle getLifecycle();
}

public class LifecycleRegistry extends Lifecycle {

}

如果我们想让一个自定义类成为LifecycleOwner,可以直接实现LifecycleOwner

class CustomLifeCycleOwner : LifecycleOwner {
private lateinit var registry: LifecycleRegistry

fun init() {
registry = LifecycleRegistry(this)
//通过setCurrentState来完成生命周期的传递
registry.currentState = Lifecycle.State.CREATED
}

fun onStart() {
registry.currentState = Lifecycle.State.STARTED
}

fun onResume() {
registry.currentState = Lifecycle.State.RESUMED
}

fun onPause() {
registry.currentState = Lifecycle.State.STARTED
}

fun onStop() {
registry.currentState = Lifecycle.State.CREATED
}

fun onDestroy() {
registry.currentState = Lifecycle.State.DESTROYED
}

override fun getLifecycle(): Lifecycle {
//返回LifecycleRegistry实例
return registry
}
}

首先我们的自定义类实现了接口LifecycleOwner,并在getLifecycle()返回LifecycleRegistry实例,接下来就可以通过LifecycleRegistry#setCurrentState来传递生命周期状态了。到目前为止,已经完成了大部分工作,最后也是需要去添加LifecycleObserver:

可以看到getLifecycle()返回的是LifecycleRegistry实例,并且在onSaveInstanceState()中分发了Lifecycle.State.CREATED状态,但是其他生命周期回调中并没有写了呀,嗯哼?再细看一下,onCreate()中有个ReportFragment.injectIfNeededIn(this),直接进去看看:

ObserverWithState#dispatchEvent()中调用了mLifecycleObserver.onStateChanged(),这个mLifecycleObserverLifecycleEventObserver类型(父类是接口LifecycleObserver ),在构造方法中通过Lifecycling.lifecycleEventObserver()创建的,最终返回的是ReflectiveGenericLifecycleObserver

//ReflectiveGenericLifecycleObserver.java
class ReflectiveGenericLifecycleObserver implements LifecycleEventObserver {
private final Object mWrapped;
private final CallbackInfo mInfo;

ReflectiveGenericLifecycleObserver(Object wrapped) {
mWrapped = wrapped;
mInfo = ClassesInfoCache.sInstance.getInfo(mWrapped.getClass());
}

@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Event event) {
mInfo.invokeCallbacks(source, event, mWrapped);
}
}

ClassesInfoCache内部存了所有观察者的回调信息,CallbackInfo是当前观察者的回调信息。getInfo()中如果从内存mCallbackMap中有对应回调信息,直接返回;否则通过createInfo()内部解析注解OnLifecycleEvent对应的方法并最终生成CallbackInfo返回。

//ClassesInfoCache.java
CallbackInfo getInfo(Class<?> klass) {

CallbackInfo existing = mCallbackMap.get(klass);
if (existing != null) {
return existing;
}
existing = createInfo(klass, null);
return existing;
}

private void verifyAndPutHandler(Map<MethodReference, Lifecycle.Event> handlers,
MethodReference newHandler, Lifecycle.Event newEvent, Class<?> klass) {
Lifecycle.Event event = handlers.get(newHandler);
if (event == null) {
handlers.put(newHandler, newEvent);
}
}

private CallbackInfo createInfo(Class<?> klass, @Nullable Method[] declaredMethods) {
Class<?> superclass = klass.getSuperclass();
Map<MethodReference, Lifecycle.Event> handlerToEvent = new HashMap<>();
if (superclass != null) {
CallbackInfo superInfo = getInfo(superclass);
if (superInfo != null) {
handlerToEvent.putAll(superInfo.mHandlerToEvent);
}
}

Class<?>[] interfaces = klass.getInterfaces();
for (Class<?> intrfc : interfaces) {
for (Map.Entry<MethodReference, Lifecycle.Event> entry : getInfo(
intrfc).mHandlerToEvent.entrySet()) {
verifyAndPutHandler(handlerToEvent, entry.getKey(), entry.getValue(), klass);
}
}

Method[] methods = declaredMethods != null ? declaredMethods : getDeclaredMethods(klass);
boolean hasLifecycleMethods = false;
//遍历寻找OnLifecycleEvent注解对应的方法
for (Method method : methods) {
OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class);
if (annotation == null) {
continue;
}
hasLifecycleMethods = true;
Class<?>[] params = method.getParameterTypes();
int callType = CALL_TYPE_NO_ARG;
if (params.length > 0) {
callType = CALL_TYPE_PROVIDER;
//第一个方法参数必须是LifecycleOwner
if (!params[0].isAssignableFrom(LifecycleOwner.class)) {
throw new IllegalArgumentException(
"invalid parameter type. Must be one and instanceof LifecycleOwner");
}
}
Lifecycle.Event event = annotation.value();

if (params.length > 1) {
callType = CALL_TYPE_PROVIDER_WITH_EVENT;
//第2个参数必须是Lifecycle.Event
if (!params[1].isAssignableFrom(Lifecycle.Event.class)) {
throw new IllegalArgumentException(
"invalid parameter type. second arg must be an event");
}
//当有2个参数时,注解必须是Lifecycle.Event.ON_ANY
if (event != Lifecycle.Event.ON_ANY) {
throw new IllegalArgumentException(
"Second arg is supported only for ON_ANY value");
}
}
if (params.length > 2) {
throw new IllegalArgumentException("cannot have more than 2 params");
}
MethodReference methodReference = new MethodReference(callType, method);
verifyAndPutHandler(handlerToEvent, methodReference, event, klass);
}
CallbackInfo info = new CallbackInfo(handlerToEvent);
mCallbackMap.put(klass, info);
mHasLifecycleMethods.put(klass, hasLifecycleMethods);
return info;
}

//CallbackInfo.java
static class CallbackInfo {
final Map<Lifecycle.Event, List<MethodReference>> mEventToHandlers;
final Map<MethodReference, Lifecycle.Event> mHandlerToEvent;

CallbackInfo(Map<MethodReference, Lifecycle.Event> handlerToEvent) {
mHandlerToEvent = handlerToEvent;
mEventToHandlers = new HashMap<>();
for (Map.Entry<MethodReference, Lifecycle.Event> entry : handlerToEvent.entrySet()) {
Lifecycle.Event event = entry.getValue();
List<MethodReference> methodReferences = mEventToHandlers.get(event);
if (methodReferences == null) {
methodReferences = new ArrayList<>();
mEventToHandlers.put(event, methodReferences);
}
methodReferences.add(entry.getKey());
}
}

void invokeCallbacks(LifecycleOwner source, Lifecycle.Event event, Object target) {
invokeMethodsForEvent(mEventToHandlers.get(event), source, event, target);
invokeMethodsForEvent(mEventToHandlers.get(Lifecycle.Event.ON_ANY), source, event,
target);
}

private static void invokeMethodsForEvent(List<MethodReference> handlers,
LifecycleOwner source, Lifecycle.Event event, Object mWrapped) {
if (handlers != null) {
for (int i = handlers.size() - 1; i >= 0; i--) {
handlers.get(i).invokeCallback(source, event, mWrapped);
}
}
}

最终调用到了MethodReference#invokeCallback()

//MethodReference.java
static class MethodReference {
final int mCallType;
final Method mMethod;

MethodReference(int callType, Method method) {
mCallType = callType;
mMethod = method;
mMethod.setAccessible(true);
}

void invokeCallback(LifecycleOwner source, Lifecycle.Event event, Object target) {
//noinspection TryWithIdenticalCatches
try {
//OnLifecycleEvent注解对应的方法入参
switch (mCallType) {
//没有参数
case CALL_TYPE_NO_ARG:
mMethod.invoke(target);
break;
//一个参数:LifecycleOwner
case CALL_TYPE_PROVIDER:
mMethod.invoke(target, source);
break;
//两个参数:LifecycleOwner,Event
case CALL_TYPE_PROVIDER_WITH_EVENT:
mMethod.invoke(target, source, event);
break;
}
} catch (InvocationTargetException e) {
throw new RuntimeException("Failed to call observer method", e.getCause());
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}

MethodReference that = (MethodReference) o;
return mCallType == that.mCallType && mMethod.getName().equals(that.mMethod.getName());
}

@Override
public int hashCode() {
return 31 * mCallType + mMethod.getName().hashCode();
}
}

根据不同入参个数通过反射来初始化并执行观察者相应方法,整个流程就从LifecycleOwner中的生命周期Event传到了LifecycleObserver中对应的方法。到这里整个流程就差不多结束了,最后是LifecycleOwner的子类LifecycleRegistry添加观察者的过程:

//LifecycleRegistry.java
@Override
public void addObserver(@NonNull LifecycleObserver observer) {
State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
//key是LifecycleObserver,value是ObserverWithState
ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);
//如果已经存在,直接返回
if (previous != null) {
return;
}
LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
if (lifecycleOwner == null) {
// it is null we should be destroyed. Fallback quickly
return;
}

boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
//目标State
State targetState = calculateTargetState(observer);
mAddingObserverCounter++;
//循环遍历,将目标State连续同步到Observer中
while ((statefulObserver.mState.compareTo(targetState) < 0
&& mObserverMap.contains(observer))) {
pushParentState(statefulObserver.mState);
statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
popParentState();
// mState / subling may have been changed recalculate
targetState = calculateTargetState(observer);
}

if (!isReentrance) {
// we do sync only on the top level.
sync();
}
mAddingObserverCounter--;
}

private State calculateTargetState(LifecycleObserver observer) {
Entry<LifecycleObserver, ObserverWithState> previous = mObserverMap.ceil(observer);

State siblingState = previous != null ? previous.getValue().mState : null;
State parentState = !mParentStates.isEmpty() ? mParentStates.get(mParentStates.size() - 1)
: null;
return min(min(mState, siblingState), parentState);
}

添加观察者,并通过while循环,将最新的State状态连续同步到Observer中,虽然可能添加ObserverLifecyleOwner分发事件晚,但是依然能收到所有事件,类似于事件总线的粘性事件。最后画一下整体的类图关系: Lifecycle.png


收起阅读 »

做一个透明的Dialog Activity

做一个透明的Dialog Activity平时在很多软件中,肯定见到过从底部的弹窗,比如分享某个文件,从底部弹出的分享平台,大部分是通过PopupWindow 底部弹出实现,这次来讲一个不一样的。1. 什么是 Dialog Activity让Acti...
继续阅读 »

做一个透明的Dialog Activity

平时在很多软件中,肯定见到过从底部的弹窗,比如分享某个文件,从底部弹出的分享平台,大部分是通过PopupWindow 底部弹出实现,这次来讲一个不一样的。

1.png

1. 什么是 Dialog Activity

让Activity拥有和Dialog一样的效果,背景虚化,悬浮效果。

2. 为什么要使用 Dialog Activity

有时候我们需要在弹窗中去做复杂的逻辑,这就导致Dialog很臃肿,而拥有Dialog样式的Activity可以像我们写UI一样,使用架构去对复杂的逻辑进行层次划分,这样在逻辑上会清洗一些,在页面的声明周期上也更方便管理一些。

3. 怎么实现 Dialog Activity

3.1 写一个样式

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

其中的含义
windowIsTranslucent: 是否半透明
windowBackground: 设置dialog的背景
backgroundDimEnabled:背景是否模糊显示
windowContentOverlay:设置窗口内容不覆盖
windowCloseOnTouchOutside:
windowIsFloating:是否浮现在activity之上,这个属性很重要,设置为true之后,Activty的状态栏才会消失。

3.2 引用样式

 <activity
android:name=".MainActivity2"
android:theme="@style/Theme.ActivityDialogStyle" />

注意:activity必须要继承 AppCompatActivity。

3.3 可配置选项

如果需要设置圆角背景

在onCreate添加

getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));

如果想让该页面填满屏幕,大家知道Dialog默认是不填满的。 在onCreate添加

 Window window = getWindow();
// 把 DecorView 的默认 padding 取消,同时 DecorView 的默认大小也会取消
window.getDecorView().setPadding(0, 0, 0, 0);
WindowManager.LayoutParams layoutParams = window.getAttributes();
// 设置宽度
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
window.setAttributes(layoutParams);
// 给 DecorView 设置背景颜色,很重要,不然导致 Dialog 内容显示不全,有一部分内容会充当 padding,上面例子有举出
// window.getDecorView().setBackgroundColor(Color.GREEN);

3.4 踩坑

实际上整个的过程十分简单,但是诡异的事情发生了。我写了一个布局,是线性布局,它并没有什么问题,却一直无法正常显示。去掉 windowIsFloating 属性就好了,但windowIsFloating会造成dialog Activity存在状态栏。 最终通过修改布局解决,先看看第一个布局和效果。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity2">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:background="@color/white" />

</LinearLayout>

可以看到这个Activity是显示了,因为背景有虚化,但layout_weight 为2的白色却没有显示。

2.png

修改后的布局(使用相对布局作为根布局):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity2">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:background="@color/white" />
</LinearLayout>
</RelativeLayout>

3.png

这样才正常显示,虽然解决了,但是原因真的好迷。

收起阅读 »

Java多线程2 多个线程之间共享数据

线程范围的共享变量多个业务模块针对同一个static变量的操作 要保证在不同线程中 各模块操作的是自身对应的变量对象public class ThreadScopeSharaData { private static int data = 0 ; ...
继续阅读 »

线程范围的共享变量

多个业务模块针对同一个static变量的操作 要保证在不同线程中 各模块操作的是自身对应的变量对象


public class ThreadScopeSharaData {

private static int data = 0 ;

public static void main(String[] args) {
for(int i = 0 ;i<2 ;i++){
new Thread(new Runnable(){

@Override
public void run() {
data = new Random().nextInt();
System.out.println(Thread.currentThread().getName()+ " put random data:"+data);
new A().get() ;
new B().get() ;
}

}).start() ;
}

}

static class A {
public int get(){
System.out.println("A from " + Thread.currentThread().getName()
+ " get data :" + data);
return data ;
}
}

static class B{
public int get(){
System.out.println("B from " + Thread.currentThread().getName()
+ " get data :" + data);
return data ;
}
}
}

模块A ,B都需要访问static的变量data 在线程0中会随机生成一个data值 假设为10 那么此时模块A和模块B在线程0中得到的data的值为10 ;在线程1中 假设会为data赋值为20 那么在当前线程下

模块A和模块B得到data的值应该为20

看程序执行的结果:


Thread-0 put random data:-2009009251
Thread-1 put random data:-2009009251
A from Thread-0 get data :-2009009251
A from Thread-1 get data :-2009009251
B from Thread-0 get data :-2009009251
B from Thread-1 get data :-2009009251

Thread-0 put random data:-2045829602
Thread-1 put random data:-1842611697
A from Thread-0 get data :-1842611697
A from Thread-1 get data :-1842611697
B from Thread-0 get data :-1842611697
B from Thread-1 get data :-1842611697

会出现两种情况

1.由于线程执行速度,新的随机值将就的随机值覆盖 data 值一样
2.data 值不一样,但 A、B线程都

1.使用Map实现线程范围内数据的共享

可是将data数据和当前允许的线程绑定在一块,在模块A和模块B去获取数据data的时候 是通过当前所属的线程去取得data的结果就行了。
声明一个Map集合 集合的Key为Thread 存储当前所属线程 Value 保存data的值,代码如下:


public class ThreadScopeSharaData {


private static Map<Thread, Integer> threadData = new HashMap<>();

public static void main(String[] args) {

for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
int data = new Random().nextInt();
System.out.println(Thread.currentThread().getName() + " put random data:" + data);
threadData.put(Thread.currentThread(), data);
new A().get();
new B().get();

}
}).start();

}

}

static class A {
public void get() {
int data = threadData.get(Thread.currentThread());

System.out.println("A from " + Thread.currentThread().getName() + " get data:" + data);

}
}

static class B {
public void get() {
int data = threadData.get(Thread.currentThread());
System.out.println("B from " + Thread.currentThread().getName() + " get data:" + data);

}
}
}

Thread-0 put random data:-123490895
Thread-1 put random data:-1060992440
A from Thread-0 get data:-123490895
A from Thread-1 get data:-1060992440
B from Thread-0 get data:-123490895
B from Thread-1 get data:-1060992440
2.ThreadLocal实现线程范围内数据的共享

(1)订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。\

(2)银行转账包含一系列操作: 把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的帐户对象的方法。\

(3)例如Strut2的ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在哪个模块中getContext方法,拿到的都是同一个。\

4.实验案例:定义一个全局共享的ThreadLocal变量,然后启动多个线程向该ThreadLocal变量中存储一个随机值,接着各个线程调用另外其他多个类的方法,这多个类的方法中读取这个ThreadLocal变量的值,就可以看到多个类在同一个线程中共享同一份数据。\

5.实现对ThreadLocal变量的封装,让外界不要直接操作ThreadLocal变量。
(1)对基本类型的数据的封装,这种应用相对很少见。
(2)对对象类型的数据的封装,比较常见,即让某个类针对不同线程分别创建一个独立的实例对象。


public class ThreadLocalTest {

private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

public static void main(String[] args) {

for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
int data = new Random().nextInt();
System.out.println(Thread.currentThread().getName() + " put random data:" + data);
threadLocal.set(data);
new A().get();
new B().get();

}
}).start();

}

}

static class A {
public void get() {
int data = threadLocal.get();

System.out.println("A from " + Thread.currentThread().getName() + " get data:" + data);

}
}

static class B {
public void get() {
int data = threadLocal.get();
System.out.println("B from " + Thread.currentThread().getName() + " get data:" + data);

}
}
}

Thread-0 put random data:-2015900409
Thread-1 put random data:-645411160
A from Thread-0 get data:-2015900409
A from Thread-1 get data:-645411160
B from Thread-0 get data:-2015900409
B from Thread-1 get data:-645411160
优化

public class ThreadLocalTest {

private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

//private static ThreadLocal<MyThreadScopeData> myThreadScopeDataThreadLocal = new ThreadLocal<>();


public static void main(String[] args) {

for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
int data = new Random().nextInt();
System.out.println(Thread.currentThread().getName() + " put random data:" + data);
threadLocal.set(data);

// MyThreadScopeData myThreadScopeData = new MyThreadScopeData();
// myThreadScopeData.setName("name" + data);
// myThreadScopeData.setAge(data);
// myThreadScopeDataThreadLocal.set(myThreadScopeData);

//获取与当前线程绑定的实例并设置值
MyThreadScopeData.getThreadInstance().setName("name" + data);
MyThreadScopeData.getThreadInstance().setAge(data);
new A().get();
new B().get();

}
}).start();

}

}

static class A {
public void get() {
int data = threadLocal.get();


// MyThreadScopeData myData = myThreadScopeDataThreadLocal.get();
//
//
// System.out.println("A from " + Thread.currentThread().getName()
// + " getMyData: " + myData.getName() + "," + myData.getAge());

MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
System.out.println("A from " + Thread.currentThread().getName()
+ " getMyData: " + myData.getName() + "," + myData.getAge());
}
}

static class B {
public void get() {
int data = threadLocal.get();
//System.out.println("B from " + Thread.currentThread().getName() + " get data:" + data);

MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
System.out.println("B from " + Thread.currentThread().getName()
+ " getMyData: " + myData.getName() + "," + myData.getAge());
}
}
}

//一个绑定当前线程的类
class MyThreadScopeData {

private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal<>();
private String name;
private int age;

private MyThreadScopeData() {
}

//定义一个静态方法,返回各线程自己的实例
//这里不必用同步,因为每个线程都要创建自己的实例,所以没有线程安全问题。
public static MyThreadScopeData getThreadInstance() {
//获取当前线程绑定的实例
MyThreadScopeData instance = map.get();
if (instance == null) {
instance = new MyThreadScopeData();
map.set(instance);
}
return instance;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}


}

Thread-1 put random data:-1041517189
Thread-0 put random data:-98835751
A from Thread-1 getMyData: name-1041517189,-1041517189
A from Thread-0 getMyData: name-98835751,-98835751
B from Thread-1 getMyData: name-1041517189,-1041517189
B from Thread-0 getMyData: name-98835751,-98835751
实例:

设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1,写出程序。

1、如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,卖票系统就可以这么做。


public class SellTicket {
//卖票系统,多个窗口的处理逻辑是相同的
public static void main(String[] args) {
Ticket t = new Ticket();
new Thread(t).start();
new Thread(t).start();
}
}

/**
* 将属性和处理逻辑,封装在一个类中
*
* @author yang
*/

class Ticket implements Runnable {

private int ticket = 10;

public synchronized void run() {
while (ticket > 0) {
ticket--;
System.out.println("当前票数为:" + ticket);
}
}
}

2、如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,例如,设计2个线程。一个线程对j增加1,另外一个线程对j减1,银行存取款系统。


public class MultiThreadShareData {
private int j;
public static void main(String[] args) {
MultiThreadShareData multiThreadShareData = new MultiThreadShareData();
for(int i=0;i<2;i++){
new Thread(multiThreadShareData.new ShareData1()).start();//增加
new Thread(multiThreadShareData.new ShareData2()).start();//减少
}
}
//自增
private synchronized void Inc(){
j++;
System.out.println(Thread.currentThread().getName()+" inc "+j);
}
//自减
private synchronized void Dec(){
j--;
System.out.println(Thread.currentThread().getName()+" dec "+j);
}

class ShareData1 implements Runnable {
public void run() {
for(int i=0;i<5;i++){
Inc();
}
}
}
class ShareData2 implements Runnable {
public void run() {
for(int i=0;i<5;i++){
Dec();
}
}
}
}

Thread-0 inc 1
Thread-0 inc 2
Thread-0 inc 3
Thread-0 inc 4
Thread-0 inc 5
Thread-1 dec 4
Thread-1 dec 3
Thread-2 inc 4
Thread-2 inc 5
Thread-2 inc 6
Thread-2 inc 7
Thread-2 inc 8
Thread-1 dec 7
Thread-1 dec 6
Thread-1 dec 5
Thread-3 dec 4
Thread-3 dec 3
Thread-3 dec 2
Thread-3 dec 1
Thread-3 dec 0
收起阅读 »

Kotlin是如何帮助你避免内存泄漏的?

本文的代码位置在github.com/marcosholga…中的kotlin-mem-leak分支上。 我是通过创建一个会导致内存泄漏的Activity,然后观察其使用Java和Kotlin编写时的表现来进行测试的。 其中Java代码如下: public c...
继续阅读 »

本文的代码位置在github.com/marcosholga…中的kotlin-mem-leak分支上。
我是通过创建一个会导致内存泄漏的Activity,然后观察其使用JavaKotlin编写时的表现来进行测试的。
其中Java代码如下:


public class LeakActivity extends Activity {

@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak);
View button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startAsyncWork();
}
});
}

@SuppressLint("StaticFieldLeak")
void startAsyncWork() {
Runnable work = new Runnable() {
@Override public void run() {
SystemClock.sleep(20000);
}
};
new Thread(work).start();
}
}


如上述代码所示,我们的button点击之后,执行了一个耗时任务。这样如果我们在20s之内关闭LeakActivity的话就会产生内存泄漏,因为这个新开的线程持有对LeakActivity的引用。如果我们是在20s之后再关闭这个Activity的话,就不会导致内存泄漏。
然后我们把这段代码改成Kotlin版本:


class KLeakActivity : Activity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_leak)
button.setOnClickListener { startAsyncWork() }
}

private fun startAsyncWork() {
val work = Runnable { SystemClock.sleep(20000) }
Thread(work).start()
}
}

咋一看,好像就只是在Runable中使用lambda表达式替换了原来的样板代码。然后我使用leakcanary和我自己的@LeakTest注释写了一个内存泄漏测试用例。


class LeakTest {
@get:Rule
var mainActivityActivityTestRule = ActivityTestRule(KLeakActivity::class.java)

@Test
@LeakTest
fun testLeaks() {
onView(withId(R.id.button)).perform(click())
}
}


我们使用这个用例分别对Java写的LeakActivityKotlin写的KLeakActivity进行测试。测试结果是Java写的出现内存泄漏,而Kotlin写的则没有出现内存泄漏。
这个问题困扰了我很长时间,一度接近自闭。。


image


然后某天,我突然灵光一现,感觉应该和编译后字节码有关系。


分析LeakActivity.java的字节码


Java类产生的字节码如下:


.method startAsyncWork()V
.registers 3
.annotation build Landroid/annotation/SuppressLint;
value = {
"StaticFieldLeak"
}
.end annotation

.line 29
new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;

invoke-direct {v0, p0}, Lcom/marcosholgado/performancetest/LeakActivity$2;-><init>
(Lcom/marcosholgado/performancetest/LeakActivity;)V

.line 34
.local v0, "work":Ljava/lang/Runnable;
new-instance v1, Ljava/lang/Thread;

invoke-direct {v1, v0}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V

invoke-virtual {v1}, Ljava/lang/Thread;->start()V

.line 35
return-void
.end method


我们知道匿名内部类持有对外部类的引用,正是这个引用导致了内存泄漏的产生,接下来我们就在字节码中找出这个引用。


new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;

复制代码

上述字节码的含义是:
首先我们创建了一个LeakActivity$2的实例。。


奇怪的是我们没有创建这个类啊,那这个类应该是系统自动生成的,那它的作用是什么啊?
我们打开LeakActivity$2的字节码看下


.class Lcom/marcosholgado/performancetest/LeakActivity$2;
.super Ljava/lang/Object;
.source "LeakActivity.java"

# interfaces
.implements Ljava/lang/Runnable;

# instance fields
.field final synthetic this$0:Lcom/marcosholgado/performancetest/LeakActivity;

# direct methods
.method constructor <init>(Lcom/marcosholgado/performancetest/LeakActivity;)V
.registers 2
.param p1, "this$0" # Lcom/marcosholgado/performancetest/LeakActivity;

.line 29
iput-object p1, p0, Lcom/marcosholgado/performancetest/LeakActivity$2;
->this$0:Lcom/marcosholgado/performancetest/LeakActivity;

invoke-direct {p0}, Ljava/lang/Object;-><init>()V

return-void
.end method


第一个有意思的事是这个LeakActivity$2实现了Runnable接口。


这就说明LeakActivity$2就是那个持有LeakActivity对象引用的匿名内部类的对象。


# interfaces
.implements Ljava/lang/Runnable;


就像我们前面说的,这个LeakActivity$2应该持有LeakActivity的引用,那我们继续找。


# instance fields
.field final synthetic
this$0:Lcom/marcosholgado/performancetest/LeakActivity;


果然,我们发现了外部类LeakActivity的对象的引用。
那这个引用是什么时候传入的呢?只有可能是在构造器中传入的,那我们继续找它的构造器。


.method constructor 
<init>(Lcom/marcosholgado/performancetest/LeakActivity;)V

果然,在构造器中传入了LeakActivity对象的引用。
让我们回到LeakActivity的字节码中,看看这个LeakActivity$2被初始化的时候。


new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;
invoke-direct {v0, p0},
Lcom/marcosholgado/performancetest/LeakActivity$2;-><init>
(Lcom/marcosholgado/performancetest/LeakActivity;)V


可以看到,我们使用LeakActivity对象来初始化LeakActivity$2对象,这样就解释了为什么LeakActivity.java会出现内存泄漏的现象。


分析 KLeakActivity.kt的字节码


KLeakActivity.kt中我们关注startAsyncWork这个方法的字节码,因为其他部分和Java写法是一样的,只有这部分不一样。
该方法的字节码如下所示:


.method private final startAsyncWork()V
.registers 3

.line 20
sget-object v0,
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
->INSTANCE:Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;

check-cast v0, Ljava/lang/Runnable;

.line 24
.local v0, "work":Ljava/lang/Runnable;
new-instance v1, Ljava/lang/Thread;

invoke-direct {v1, v0}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V

invoke-virtual {v1}, Ljava/lang/Thread;->start()V

.line 25
return-void
.end method

可以看出,与Java字节码中初始化一个包含Activity引用的实现Runnable接口对象不同的是,这个字节码使用了静态变量来执行静态方法。


sget-object v0,         
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1; ->
INSTANCE:Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;


我们深入KLeakActivity\$startAsyncWork\$work$1的字节码看下:


.class final Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
.super Ljava/lang/Object;
.source "KLeakActivity.kt"

# interfaces
.implements Ljava/lang/Runnable;

.method static constructor <clinit>()V
.registers 1

new-instance v0,
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;

invoke-direct {v0},
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;-><init>()V

sput-object v0,
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
->INSTANCE:Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;

return-void
.end method

.method constructor <init>()V
.registers 1

invoke-direct {p0}, Ljava/lang/Object;-><init>()V

return-void
.end method

可以看出,KLeakActivity\$startAsyncWork\$work$1实现了Runnable接口,但是其拥有的是静态方法,因此不需要外部类对象的引用。
所以Kotlin不出现内存泄漏的原因出来了,在Kotlin中,我们使用lambda(实际上是一个 SAM)来代替Java中的匿名内部类。没有Activity对象的引用就不会发生内存泄漏。
当然并不是说只有Kotlin才有这个功能,如果你使用Java8中的lambda的话,一样不会发生内存泄漏。
如果你想对这部分做更深入的了解,可以参看这篇文章Translation of Lambda Expressions
如果有需要翻译的同学可以在评论里面说就行啦。


image


现在把其中比较重要的一部分说下:



上述段落中的Lamdba表达式可以被认为是静态方法。因为它们没有使用类中的实例属性,例如使用super、this或者该类中的成员变量。
我们把这种Lambda称为Non-instance-capturing lambdas(这里我感觉还是不翻译为好)。而那些需要实例属性的Lambda则称为instance-capturing lambdas




Non-instance-capturing lambdas可以被认为是private、static方法。instance-capturing lambdas可以被认为是普通的private、instance方法。



这段话放在我们这篇文章中是什么意思呢?


因为我们Kotlin中的lambda没有使用实例属性,所以其是一个non-instance-capturing lambda,可以被当成静态方法来看待,就不会产生内存泄漏。


如果我们在其中添加一个外部类对象属性的引用的话,这个lambda就转变成instance-capturing lambdas,就会产生内存泄漏。


class KLeakActivity : Activity() {

private var test: Int = 0

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_leak)
button.setOnClickListener { startAsyncWork() }
}

private fun startAsyncWork() {
val work = Runnable {
test = 1 // comment this line to pass the test
SystemClock.sleep(20000)
}
Thread(work).start()
}
}

如上述代码所示,我们使用了test这个实例属性,就会导致内存泄漏。
startAsyncWork方法的字节码如下所示:


.method private final startAsyncWork()V
.registers 3

.line 20
new-instance v0, Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;

invoke-direct {v0, p0},
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
-><init>(Lcom/marcosholgado/performancetest/KLeakActivity;)V

check-cast v0, Ljava/lang/Runnable;

.line 24
.local v0, "work":Ljava/lang/Runnable;
new-instance v1, Ljava/lang/Thread;

invoke-direct {v1, v0}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V

invoke-virtual {v1}, Ljava/lang/Thread;->start()V

.line 25
return-void
.end method

很明显,我们传入了KLeakActivity的对象,因此就会导致内存泄漏。


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

建议收藏!!Flutter状态管理插件哪家强?请看岛上码农的排行榜!

前言一路下来,Flutter 状态管理相关的文章写了有几十篇了,这是本人也没想到的结果。Flutter 的状态管理插件实在太多,感觉要深挖,可以继续写上几十篇。只是,这样写,怕是自己不累,看客都累了!😂😂😂授人以鱼不如授人以渔,本篇就专门对 Flutter 比...
继续阅读 »

前言

一路下来,Flutter 状态管理相关的文章写了有几十篇了,这是本人也没想到的结果。Flutter 的状态管理插件实在太多,感觉要深挖,可以继续写上几十篇。只是,这样写,怕是自己不累,看客都累了!😂😂😂授人以鱼不如授人以渔,本篇就专门对 Flutter 比较流行的状态管理插件做一个合集并附上对比分析和排行榜。大家可以结合对比数据和官方文档来在实际开发中选择。大家可以在评论区晒出自己用的状态管理插件和选择的理由,互相参考一下!

横向对比参数

我们横向对比以数据说话,综合了 pub 的喜欢数(Likes)、流行度(Popularity)和得分(Pub Points),Gitbub的 Star 数、贡献者数量五个维度进行比对。各个参数说明如下:

  • 喜欢数(Likes):反映的是该插件受 Flutter 开发者的喜好程度,间接反映了插件的文档完整性、可读性和插件的易用性;
  • 流行度(Popularity):反映的是该插件受 Flutter 开发者的欢迎程度和插件应用的广泛性(白话解释:使用人的人越多,意味着有更多的人提前帮你踩坑💣)。
  • 得分(Pub Points):pub 的得分满分是130分,其实是对插件的一个比较基础的全面评测,分为如下6个部分:
    • 遵循插件规范(20分)
    • 文档完整性(20分)
    • 跨平台支持(20分)
    • 通过静态分析(30分)
    • 版本兼容性(20分)
    • 支持 null safety(20分)
  • GitHub Star 数:这个大家都懂,反映的是受开发者认可的程度,实力的象征!
  • 贡献者数量:这个其实就是插件的社区号召力和参与维护的人数,贡献者越多也意味着插件的可靠性越高,不至于说更新过慢或突然中止维护(中止维护属于天坑了💣💣💣)。

为了统一对比尺度,我们统一按与本篇列出的管理插件同维度最大值进行比对,根据比值得出星级,共设置5颗星,比值与星级对应关系如下:

  • 0.9-1.0:5星
  • 0.8-0.9:4星
  • 0.6-0.8:3星
  • 0.3-0.6:2星
  • 0.3以下:1星

状态管理插件对比分析

我们先看对比数据,再来做星级评比,状态管理插件清单的五项数据如下:

插件名称喜欢数流行度(%)得分Star 数贡献者数量
Provider52071001303.9k60
Redux2459711547514
MobX696981202k61
GetX6406991204.9k140
BLoC1215991307.8k135
Event Bus257981305973
GetIt15409913078619
FlutterCommand4372130283
Binder47571201632
StateRebuilder319951203906
Stacked8509711054362
Fish Redux52921007.2k34
flutter_meedu4685130152
Riverpod1039981302k61
flutter_hooks816981301.9k31

各项参数星级评定如下:

插件名称喜欢数流行度(%)得分Star 数贡献者数量
Provider★★★★★★★★★★★★★★★★★★★
Redux★★★★★★★★★
MobX★★★★★★★★★★★★
GetX★★★★★★★★★★★★★★★★★★★★★★★
BLoC★★★★★★★★★★★★★★★★★★★★
Event Bus★★★★★★★★★★
GetIt★★★★★★★★★★
FlutterCommand★★★★★★★★
Binder★★★★★★★
StateRebuilder★★★★★★★★★★
Stacked★★★★★★★★★★★
Fish Redux★★★★★★★★★★★★★
flutter_meedu★★★★★★★★★
Riverpod★★★★★★★★★★★★
flutter_hooks★★★★★★★★★★

排行榜

基于上面的星级评定和数据,我们把每项数据的比值求和,从大大小排序,得到的分值和排行榜如下。

插件名称排名综合评分星级
GetX14.54★★★★★
BLoC24.14★★★★
Provider33.74★★★★
Fish Redux42.86★★★
Riverpod52.83★★★
MobX62.81★★★
flutter_hooks72.57★★
GetIt82.47★★
Stacked92.46★★
Event Bus102.11★★
Redux112.05★★
StateRebuilder122.02★★
flutter_meedu131.87★★
FlutterCommand141.75★★
Binder151.53★★

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

收起阅读 »

更高级的 Android 启动任务调度库

在之前的文章中,我介绍了自研的 Android 启动任务调度工具 AndroidStartup。近期,因为在组件化项目中运用该项目的需要,我对这个库做了一番升级。在最新的 2.2 版本中,我新增了一些特性。相比于目前市面上其他的启动任务调度库,使其具备了更多的...
继续阅读 »

在之前的文章中,我介绍了自研的 Android 启动任务调度工具 AndroidStartup。近期,因为在组件化项目中运用该项目的需要,我对这个库做了一番升级。在最新的 2.2 版本中,我新增了一些特性。相比于目前市面上其他的启动任务调度库,使其具备了更多的优势。这里我只介绍下经过新的版本迭代之后该项目与其他项目的不同点。对于其基础的实现原理,可以参考我之前的文章 《异步、非阻塞式 Android 启动任务调度库》


1、支持多种线程模型


这是相对于 Jetpack 的启动任务库的优势,在指定任务的时候,你可以通过 ISchedulerJobthreadMode() 方法指定该任务执行的线程,当前支持主线程(ThreadMode.MAIN)和非主线程(ThreadMode.BACKGROUND)两种情况。前者在主线程当中执行,后者在线程池当中执行,同时,该库还允许你自定义自己的线程池。关于这块的实现原理可以参考之前的文章或者项目源码。


2、非阻塞的任务调度方式


在之前的文章中也提到了,如果说采用 CountDownLatch 等阻塞的方式来实现任务调度,虽然不会占用主线程的 CPU,但是子线程会被阻塞,一样会导致 CPU 空转,影响程序执行的性能,尤其启动的时候大量任务执行时的情况。所以,在这个库的设计中,我们使用了通知唤醒的方式进行任务调度。也就是,


首先,它会将所有的需要执行的任务收集起来;然后,它会根据任务的依赖关系指定分发和调度任务的子任务;最后,当当前任务执行完毕,该任务会通知所有的子任务按照顺序执行。大致实现逻辑如下,


override fun execute() {
val realJob = {
// 1. Run the task if match given process.
if (matcher.match(job.targetProcesses())) {
job.run(context)
}

// 2. Then sort children task.
children.sortBy { child -> -child.order() }

// 3. No matter the task invoked in current process or not,
// its children will be notified after that.
children.forEach { it.notifyJobFinished(this) }
}

try {
if (job.threadMode() == ThreadMode.MAIN) {
// Cases for main thread.
if (Thread.currentThread() == Looper.getMainLooper().thread) {
realJob.invoke()
} else {
mainThreadHandler.post { realJob.invoke() }
}
} else {
// Cases for background thread.
executor.execute { realJob.invoke() }
}
} catch (e: Throwable) {
throw SchedulerException(e)
}
}

3、非 Class 的依赖方式


之前在本项目中,以及其他的项目中可能采用了基于 Class 的形式进行任务依赖。这种使用方式存在一些问题,即在组件化开发的时候,Class 之间需要直接进行引用。这导致各个组件之间的强耦合。这显然不是我们希望的。


所以,为了更好地支持组件化,在该库的新版本中,我们允许通过 name() 方法执行任务的名称,以及通过 dependencies() 方法指定该任务依赖的其他任务的名称。name() 默认使用任务 Class 的全限定名。这样,当多个组件之间进行相互依赖的时候,只需要通过字符串指定名称而无需引用具体的类。


比如,一个任务在一个组件中定义如下,


@StartupJob class BlockingBackgroundJob : ISchedulerJob {

override fun name(): String = "blocking"

override fun threadMode(): ThreadMode = ThreadMode.BACKGROUND

override fun dependencies(): List<String> = emptyList()

override fun run(context: Context) {
Thread.sleep(5_000L) // 5 seconds
L.d("BlockingBackgroundJob done! ${Thread.currentThread()}")
toast("BlockingBackgroundJob done!")
}
}

在另一个组件中的另一个任务需要依赖上述任务的时候,定义如下,


@StartupJob class SubModuleTask : ISchedulerJob {

override fun dependencies(): List<String> = listOf("blocking")

override fun run(context: Context) {
Log.d("SubModuleTask", "runed ")
}
}

这样我们就实现组件化场景中的依赖关系了。


4、支持任务的优先级


在实际开发中,我们可能会遇到需要为所有的根任务或者一个任务的所有的子任务指定执行的先后顺序的场景。或者在组件化中,存在依赖关系,但是我们希望某个根任务优先执行,但是不想为每个子任务都执行依赖关系的时候,我们可以通过指定这个任务的优先级为最高来使其最先被执行。你可以通过 priority() 方法传递一个 0 到 100 的整数来指定任务的优先级。


@StartupJob class TopPriorityJob : ISchedulerJob{

override fun priority(): Int = 100

override fun run(context: Context) {
L.d("Top level job done!")
}
}

优先级局限于依赖关系相同的任务,所以是依赖关系的补充,不会造成歧义。


5、支持指定任务执行的进程,可自定义进程匹配策略


如果我们的项目支持多进程,而我们希望某些启动任务只在某个进程中执行而其他进程不需要执行,以此避免没必要的任务来提升任务执行的性能的时候,我们可以通过指定任务执行的进程来进行优化。你可以通过 targetProcesses() 传递一个进程的列表来指定该任务执行的所有进程。默认列表为空,表示运行在所有的进程。


对于进程的匹配,我们提供了 IProcessMatcher 这个接口,


interface IProcessMatcher {
fun match(target: List<String>): Boolean
}

你可以通过指定这个接口来自定义线程的匹配策略。


6、支持注解形式的组件化调用


在之前的版本中,通过 ContentProvider 的形式我们一样可以实现所有组件内任务的收集和调用。但是使用 ContentProvider 存在一些不便之处,比如 ContentProvider 的初始化实际在 Application 的 attachBaseContext(),如果我们的任务中一些操作需要放到 Application 的 onCreate() 中执行的时候,通过 ContentProvider 默认装载任务的调度方式就存在问题。而通过基于注解 + APT的形式,我们可以随意指定任务收集、整理和执行的时机,灵活性更好。


为了支持组件化,我们在之前的项目上做了一些拓展。之前的项目虽然也是基于注解发现机制,但是在组件化的应用中存在问题。在新的版本中,我们只是处理了组件化应用场景中的问题,但是使用方式上面完全兼容,只不过你需要为每个组件在 gradle.build 中增加一个行信息来指定组件的名称(就像 ARouter 一样),


javaCompileOptions {
annotationProcessorOptions {
arguments = [STARTUP_MODULE_NAME: project.getName()]
}
}

也就是说你还是通过 @StartupJob 注解将任务标记为启动任务,然后通过


launchStartup(this) {
scanAnnotations()
}

这行代码启动扫描并执行任务。


在新的版本中,所有生产的代码会被统一放到包 me.shouheng.startup.hunter 下面,然后通过 JobHunter$$组件名 的形式为每个组件生成自己的类,然后在扫描任务的时候通过加载这个包名之下的所有的代码来找到所有要执行的任务。如果你对组件化感兴趣可以直接阅读这块的源码实现。


总结


启动任务调度库的设计不算复杂,但是我却在之前的面试中两次被问到如何设计。这种类型的问题能很好地考察代码设计能力。相信阅读这个库的代码之后,此类的问题再也难不倒你。如果你对 APT+注解 的组件化实现方式等感兴趣一样可以阅读这个库的代码。


以上介绍了这个库的一些特性和优势,没用过多地介绍其源码实现,感兴趣的同学可以直接阅读项目的源码,相信你能够从代码中学到一些东西。对于示例项目,除了阅读这个项目的示例,还可以参考 Android-VMLib 这个项目。该项目地址:github.com/Shouheng88/…


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

Flutter 入门与实战(九十三):使用 Animation 构建爱心三连动画

前言 我们开始 Flutter 动画相关篇章之旅,在应用中通过动效能够给用户带来更愉悦的体验,比较典型的例子就是一些直播平台的动效了,比如送火箭能做出来那种火箭发射的动效——感觉倍有面子,当然这是土豪的享受,我等码农只在视频里看过😂😂😂。本篇我们来介绍基于 A...
继续阅读 »

前言


我们开始 Flutter 动画相关篇章之旅,在应用中通过动效能够给用户带来更愉悦的体验,比较典型的例子就是一些直播平台的动效了,比如送火箭能做出来那种火箭发射的动效——感觉倍有面子,当然这是土豪的享受,我等码农只在视频里看过😂😂😂。本篇我们来介绍基于 Animation 类实现的基本动画构建。


Animation 简介


Animation 是一个抽象类,它并不参与屏幕的绘制,而是在设定的事件范围内对一段区间值进行插值。插值的方式可以是线性、曲线、一个阶跃函数或其他能够想到的方式。这个类的对象能够知道当前的值和状态(完成或消失)。Animation 类提供了一个监听回调方法,当它的值改变的时候,就会调用该方法:


@override
void addListener(VoidCallback listener);

因此,在监听回调里,我们可以来刷新界面,通过Animation 对象最新的值控制 UI 组件的位置、尺寸、角度,从而实现动画的效果。Animation 类通常会和 AnimationController 一起使用。


AnimationController 简介


AnimationController 是一个特殊的 Animation 类,它继承自 Animation<double>。每当硬件准备好绘制下一帧时,AnimationController就会产生一个新的值。默认情况下 AnimationController 会在给定的时间范围内产生的值是从0到1.0的线性序列值(通常60个值/秒,以达到60 fps的效果)。例如,下面的代码构建一个时长为2秒的 AnimationController


var controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);

AnimationController 具有 forwardreverse等控制动画的方法,通常用于控制动画的开始和恢复。


连接 AnimationAnimationController 的是 Animatable类,该类也是一个抽象类, 常用的的实现类包括 Tween<T>(线性插值),CurveTween(曲线插值)。Animatable 类有一个 animate 方法,该方法接收 Animation<double>类参数(通常是 AnimationController),并返回一个 Animation 对象。


Animation<T> animate(Animation<double> parent) {
return _AnimatedEvaluation<T>(parent, this);
}

animate方法使用给定的 Animation<double>对象驱动完成动效,但使用的值的范围是自身的值,从而可以构建自定义值范围的动效。比如,要构建一个2秒内从0增长100的动效值,可以使用如下的代码。


var controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
var animation = Tween<double>(begin: 0, end: 100).animate(controller);

应用 - 爱心三连


有了上面的基础,我们就可以开始牛刀小试了,我们先来一个爱心三连放大缩小的动效,如下所示,首次点击逐渐放大,再次点击逐渐缩小恢复到原样。


爱心三连.gif
界面代码很简单,三个爱心其实就是使用Stack 将三个不同颜色的爱心 Icon 组件叠加在一起,然后通过 Animtion对象的值改变 Icon 的大小。在 Animation 值变化的监听回调李使用 setState 刷新界面就好了。完整代码如下:


import 'package:flutter/material.dart';

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

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

class _AnimtionDemoState extends State<AnimtionDemo>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;

@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 1), vsync: this);
animation = Tween<double>(begin: 40, end: 100).animate(controller)
..addListener(() {
setState(() {});
});
controller.addStatusListener((status) {
print(status);
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Animation 动画'),
),
body: Center(
child: Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.favorite,
color: Colors.red[100],
size: animation.value * 1.5,
),
Icon(
Icons.favorite,
color: Colors.red[400],
size: animation.value,
),
Icon(
Icons.favorite,
color: Colors.red[600],
size: animation.value / 2,
),
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow, color: Colors.white),
onPressed: () {
if (controller.status == AnimationStatus.completed) {
controller.reverse();
} else {
controller.forward();
}
},
),
);
}

@override
void dispose() {
controller.dispose();
super.dispose();
}
}

这里需要提的是在_AnimtionDemoState中混入了SingleTickerProviderStateMixin,这里其实是为 AnimationController 提供了一个 TickerProivder 对象。TickerProivder对象会在每一帧刷新前触发一个 onTick回调,从而实现AnimationController的值更新。


总结


本篇介绍了Flutter 动画构建类 AnimationAnimationController 的使用,通过这两个类可以实现很多基础动画效果,例如常见的进度条、缩放、旋转、移动等。接下来我们还将介绍基于 Animation 实现动画的其他方式和其他类型的动画效果。


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

RxHttp + Flow 三步搞定任意请求

1、前言 RxHttp 在之前的版本中,已提供了RxHttp + Await协程、RxHttp + RxJava两种请求方式,这一次,RxHttp 无缝适配了 Flow , RxHttp + Flow协程配合使用,使得请求更加简单,至此,RxHttp已集齐3架...
继续阅读 »

1、前言


RxHttp 在之前的版本中,已提供了RxHttp + Await协程RxHttp + RxJava两种请求方式,这一次,RxHttp 无缝适配了 Flow , RxHttp + Flow协程配合使用,使得请求更加简单,至此,RxHttp已集齐3架马车(Flow、Await、RxJava),且每架马车皆遵循请求三部曲,掌握请求三部曲,就掌握了RxHttp的精髓。


gradle依赖


1、必选


jitpack添加到项目的build.gradle文件中,如下:


allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}

//使用kapt依赖rxhttp-compiler时必须
apply plugin: 'kotlin-kapt'

android {
//必须,java 8或更高
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.github.liujingxing.rxhttp:rxhttp:2.7.0'
kapt 'com.github.liujingxing.rxhttp:rxhttp-compiler:2.7.0' //生成RxHttp类,纯Java项目,请使用annotationProcessor代替kapt
}

2、可选


//非必须,根据自己需求选择 RxHttp默认内置了GsonConverter
implementation 'com.github.liujingxing.rxhttp:converter-fastjson:2.7.0'
implementation 'com.github.liujingxing.rxhttp:converter-jackson:2.7.0'
implementation 'com.github.liujingxing.rxhttp:converter-moshi:2.7.0'
implementation 'com.github.liujingxing.rxhttp:converter-protobuf:2.7.0'
implementation 'com.github.liujingxing.rxhttp:converter-simplexml:2.7.0'

2、RxHttp + Flow 使用


2.1、请求三部曲


用过RxHttp的同学知道,RxHttp发送任意请求皆遵循请求三部曲,如下:
rxhttp_flow_chart.jpg
代码表示


 RxHttp.get("/service/...")  //第一步,确定请求方式,可以选择postForm、postJson等方法
.add("key", "value")
.toFlow<Student>() //第二步,调用toFlow方法并输入泛型类型,拿到Flow对象
.catch {
//异常回调
val throwable = it
}.collect { //第三步,调用collect方法发起请求
//成功回调
val student = it
}

协程请求三部曲详解




  • 第一步,选择get、postForm、postJson、postBody等方法来确定请求方式,随后便可通过add、addFile、addHeader等方法来添加参数、文件、请求头等信息




  • 第二步,调用toFlow/toFlowXxx系列方法,并传入泛型类型,以获取到Flow对象,toFlow有一系列重载方法,可以实现上传/下载及进度的监听,本文后续会详细介绍,在这一步后,可以调用catchonStartonCompletion等方法去监听异常、开始及结束回调,跟平时使用Flow对象没有任何区别




  • 第三步,调用collect方法就会开始发送请求,如果一些正常的话,就会收到成功回调




以上就是RxHttp在协程中最常规的操作,掌握请求三部曲,就掌握了RxHttp的精髓


2.2、BaseUrl处理


RxHttp通过@DefaultDomain、@Domain注解来配置默认域名及非默认域名,如下:


public class Url {

@DefaultDomain //通过该注解设置默认域名
public static String BASE_URL = "https://www.wanandroid.com";

// name 参数在这会生成 setDomainToGoogleIfAbsent方法,可随意指定名称
// className 参数在这会生成RxGoogleHttp类,可随意指定名称
@Domain(name = "Google", className = "Google")
public static String GOOGLE = "https://www.google.com";
}

以上配置http://www.wanandroid.com为默认域名,http://www.google.com为非默认域名


多BaseUrl处理


//1、使用默认域名,传入相对路径即可
//此时 url 为 https://www.wanandroid.com/service/...
RxHttp.get("/service/...")
...

//2、使用google域名方式一:传入绝对路径
RxHttp.get("https://wwww.google.com/service/...")
...

//3、使用google域名方式二:调用setDomainToGoogleIfAbsent方法
//该方法是通过 @Domain 注解的 name 字段生成的,命名规则为 setDomainTo{name}IfAbsent
RxHttp.get("/service/...")
.setDomainToGoogleIfAbsent()
...

//4、使用google域名方式三:直接使用RxGoogleHttp类发送请求,
//该类是通过 @Domain 注解的 className 字段生成的,命名规则为 Rx{className}http
RxGoogleHttp.get("/service/...")
...

注:以上4种配置域名的方式,优先级别为:2 > 3 > 4 > 1


动态域名处理


//直接对url重新赋值即可,改完立即生效
Url.BASE_URL = "https://www.baidu.com";
RxHttp.get("/service/...")
...
//此时 url 为 https://www.baidu.com/service/...

2.3、业务code统一判断


我想大部分人的接口返回格式都是这样的


class BaseResponse<T> {
var code = 0
var msg : String? = null
var data : T
}

拿到该对象的第一步就是对code做判断,如果code != 200(假设200代表数据正确),就会拿到msg字段给用户一些错误提示,如果等于200,就拿到data字段去更新UI,常规的操作是这样的


RxHttp.get("/service/...")
.toFlow<BaseResponse<Student>>()
.collect {
if (response.code == 200) {
//拿到data字段(Student)刷新UI
} else {
val msg = it.msg //拿到msg字段给出错误提示
}
}

试想一下,一个项目少说也有30+个这样的接口,如果每个接口读取这么判断,就显得不够优雅,也可以说是灾难,相信也没有人会这么干。而且对于UI来说,只需要data字段即可,错误提示啥的我管不着。


那有没有什么办法,能直接拿到data字段,并且对code做出统一判断呢?有的,直接上代码


RxHttp.get("/service/...")
.toFlowResponse<Student>() //调用此方法,直接拿到data字段,也就是Student对象
.catch {
// code非200时,走异常回调,在这可拿到msg及code字段
val msg = it.msg
val code = it.code
}.collect {
//直接拿到data字段,在这就是Student对象
val student = it
}

可以看到,以上调用toFlowResponse()方法,成功回调就可直接拿到data字段,也就是Student对象。


此时,相信很多人会有疑问,




  • 业务code哪里判断的?




  • 异常回调里的it是什么对象,为啥可以拿到msg、code字段?




先来回答第一个问题,业务code哪里判断的?


其实toFlowResponse()方法并不是RxHttp内部提供的,而是通过自定义解析器,并用@Parser注解标注,最后由注解处理器rxhttp-compiler自动生成的,听不懂?没关系,直接看代码


@Parser(name = "Response")
open class ResponseParser<T> : TypeParser<T> {

//以下两个构造方法是必须的
protected constructor() : super()
constructor(type: Type) : super(type)

@Throws(IOException::class)
override fun onParse(response: okhttp3.Response): T {
val data: BaseResponse<T> = response.convertTo(BaseResponse::class, *types)
val t = data.data //获取data字段
if (data.code != 200 || t == null) { //code不等于200,说明数据不正确,抛出异常
throw ParseException(data.code.toString(), data.msg, response)
}
return t //最后返回data字段
}
}

上面代码只需要关注两点即可,


第一点,我们在类开头使用了@Parser注解,并为解析器取名为Response,此时rxhttp-compiler就会生成toFlowResponse<T>()方法,命名规则为toFlow{name}


第二点,我们在if语句里,code != 200data == null时,就抛出ParseException异常,并带上了msg、code字段,所以我们在异常回调通过强转,就可以拿到这两个字段


接着回答第二个问题,异常回调里的it是什么对象,为啥可以拿到msg、code字段?


其实it就是Throwable对象,而msg、codeThrowable的扩展字段,这需要我们自己为其扩展,代码如下:


val Throwable.code: Int
get() =
when (this) {
is HttpStatusCodeException -> this.statusCode //Http状态码异常
is ParseException -> this.errorCode.toIntOrNull() ?: -1 //业务code异常
else -> -1
}

val Throwable.msg: String
get() {
return if (this is UnknownHostException) { //网络异常
"当前无网络,请检查你的网络设置"
} else if (
this is SocketTimeoutException //okhttp全局设置超时
|| this is TimeoutException //rxjava中的timeout方法超时
|| this is TimeoutCancellationException //协程超时
) {
"连接超时,请稍后再试"
} else if (this is ConnectException) {
"网络不给力,请稍候重试!"
} else if (this is HttpStatusCodeException) { //请求失败异常
"Http状态码异常"
} else if (this is JsonSyntaxException) { //请求成功,但Json语法异常,导致解析失败
"数据解析失败,请检查数据是否正确"
} else if (this is ParseException) { // ParseException异常表明请求成功,但是数据不正确
this.message ?: errorCode //msg为空,显示code
} else {
"请求失败,请稍后再试"
}
}

到这,业务code统一判断就介绍完毕,上面的代码,大部分人只需要简单修改后,就可用到自己的项目上,如ResponseParser解析器,只需要改下if语句的判断条件即可


3、上传/下载


RxHttp对文件的优雅操作是与生俱来的,配合Flow,可以说是如虎添翼,不多说,直接上代码


3.1、文件上传


RxHttp.postForm("/service/...")  
.addFile("file", File("xxx/1.png")) //添加单个文件
.addFiles("fileList", ArrayList<File>()) //添加多个文件
.toFlow<String>()
.catch { //异常回调 }
.collect { //成功回调 }

只需要通过addFile系列方法添加File对象即可,就是这么简单粗暴,想监听上传进度,toFlow方法传入进度回调即可,如下:


RxHttp.postForm("/service/...")      
.addFile("file", File("xxx/1.png"))
.addFiles("fileList", ArrayList<File>())
.toFlow<String> { //这里还可以选择自定义解析器对应的toFlowXxx方法
val process = it.progress //已上传进度 0-100
val currentSize = it.currentSize //已上传size,单位:byte
val totalSize = it.totalSize //要上传的总size 单位:byte
}.catch { //异常回调 }
.collect { //成功回调 }

3.2、文件下载


接着再来看看下载,直接贴代码


val localPath = "sdcard//android/data/..../1.apk"  
RxHttp.get("/service/...")
.toFlow(localPath) {
//it为Progress对象
val process = it.progress //已下载进度 0-100
val currentSize = it.currentSize //已下载size,单位:byte
val totalSize = it.totalSize //要下载的总size 单位:byte
}
.catch { //异常回调 }
.collect { //成功回调,这里可以拿到本地存储路径,也就是localPath }

你没看错,下载也是调用 toFlow方法,传入本地路径及进度回调即可,当然,如果不需要监听进度,进度回调也可不传,来看看用来下载的toFlow方法签名


/**
* @param destPath 本地存储路径
* @param append 是否追加下载,即是否断点下载
* @param capacity 队列size,仅监听进度回调时生效
* @param progress 进度回调
*/
fun CallFactory.toFlow(
destPath: String,
append: Boolean = false,
capacity: Int = 1,
progress: (suspend (Progress) -> Unit)? = null
): Flow<String>

以上4个参数,只有destPath是必须的,其它3个参数,根据实际需要传递,想要断点下载,append传入true,想要监听进度就传入进度回调,


至于capacity参数,这个需要额外说明一下,它是指定队列的缓存大小,什么队列?进度回调的队列,目的就是丢弃来不及消费的事件,在现实场景中,可能会存在下游消费速度 小于 上游生产速度的情况,这就会导致事件的堆积,翻译过来就是下载很快,但你处理进度回调的地方很慢,就有可能出现你还在处理进度为10的事件,但实际下载进度可能到了50甚至更高,capacity设置为1的话,10-50之间的事件就会被丢弃,接下来下游收到的可能就是进度为50的事件,这就保证了下游收到的始终的最新的事件,也就是最及时的下载进度,当然,如果你想收到全部的进度回调事件,将capacity设置为100即可。


3.3、暂停/恢复下载


很多会有暂停/恢复下载的需求,但对于下载来说,并没有真正意义的暂停及恢复,所谓的暂停,不过就是停止下载,也就是中断请求,而恢复,就是再次发起请求从上次中断的位置继续下载,也就是断点下载,所有,只需要知道如何取消请求及断点下载即可


取消请求


Flow的取消,就是外部协程的关闭


val job = lifecycleScope.launch {
val localPath = "sdcard//android/data/..../1.apk"
RxHttp.get("/service/...")
.toFlow(localPath) {
//it为Progress对象
val process = it.progress //已下载进度 0-100
val currentSize = it.currentSize //已下载size,单位:byte
val totalSize = it.totalSize //要下载的总size 单位:byte
}
.catch { //异常回调 }
.collect { //成功回调,这里可以拿到本地存储路径,也就是localPath }
}
//在需要的时候,调用job.cancel()就是取消请求
job.cancel()

断点下载


上面介绍过,想要断点下载,只需要额外将toFlow方法的第二个参数append设置为true即可,如下:


val localPath = "sdcard//android/data/..../1.apk"  
RxHttp.get("/service/...")
.toFlow(localPath, true) {
//it为Progress对象
val process = it.progress //已下载进度 0-100
val currentSize = it.currentSize //已下载size,单位:byte
val totalSize = it.totalSize //要下载的总size 单位:byte
}
.catch { //异常回调 }
.collect { //成功回调,这里可以拿到本地存储路径,也就是localPath }

注:断点下载需要服务器接口支持


对于Android 10文件上传/下载,请点击RxHttp 完美适配Android 10/11 上传/下载/进度监听


4、转LiveData 


Flow依赖于协程环境,如果不想使用协程,又想要使用Flow,那LiveData就是一个很好的选择,在官方androidx.lifecycle:lifecycle-livedata-ktx:x.x.x库中提供了asLiveData方法,可方便的将FlowLiveData对象,有了LiveData对象,就不再需要协程环境


4.1、普通请求转LiveData


//当前在FragmentActivity环境中
RxHttp.get("/service/...")
.toFlow<Student>()
.catch { }
.asLiveData()
.observe(this) {
val student = it;
//更新UI
}

由于调用了asLiveData,所以,以上代码,不需要协程环境也可执行;


4.2、带进度上传转LiveData


RxHttp.postForm("/service/...")      
.addFile("file", File("xxx/1.png"))
.addFiles("fileList", ArrayList<File>())
.toFlow<Student> { //这里还可以选择自定义解析器对应的toFlowXxx方法
val process = it.progress //已上传进度 0-100
val currentSize = it.currentSize //已上传size,单位:byte
val totalSize = it.totalSize //要上传的总size 单位:byte
}
.catch { //异常回调 }
.asLiveData()
.observe(this) {
val student = it;
//更新UI
}

上面代码中,转LiveData后,下游observe只能收到上传完成的回调,如果你想收到包括进度回调在内的所有事件,则需要使用toFlowProgress替代toFlow方法(toFlow内部是通过toFlowProgress方法实现的,有兴趣的自己查看源码),如下:


RxHttp.postForm("/service/...")      
.addFile("file", File("xxx/1.png"))
.addFiles("fileList", ArrayList<File>())
.toFlowProgress<Student>() //该方法没有进度回调参数
.catch { //异常回调 }
.asLiveData()
.observe(this) {
//此时这里将收到所有事件,这里的it为ProgressT<Student>对象
val process = it.progress //已上传进度 0-100
val currentSize = it.currentSize //已上传size,单位:byte
val totalSize = it.totalSize //要上传的总size 单位:byte
val student = it.result //接口返回的对象
if (student != null) {
//不为null,代表上传完成,接口请求结束
}
}

4.3、带进度下载转LiveData


下载也一样,RxHttp提供了一个下载对应的toFlowProgress方法,如下:


fun CallFactory.toFlowProgress(
destPath: String,
append: Boolean = false,
capacity: Int = 1
): Flow<ProgressT<String>>

跟上面介绍下载时对应的toFlow方法相比,少了一个进度回调的参数,这里悄悄告诉你,下载的toFlow方法,内部就是通过toFlowProgress方法实现的,想了解的自己去查看源码,这里不做介绍


结合asLiveData方法,使用如下:


val localPath = "sdcard//android/data/..../1.apk"  
RxHttp.get("/service/...")
.toFlowProgress(localPath)
.catch { //异常回调 }
.asLiveData()
.observe(this) {
//此时这里将收到所有事件,这里的it为ProgressT<String>对象
val process = it.progress //已下载进度 0-100
val currentSize = it.currentSize //已下载size,单位:byte
val totalSize = it.totalSize //要下载的总size 单位:byte
val path = it.result //本地存储路径
if (path != null) {
//不为null,代表下载完成,接口请求结束
}
}

5、小结


看完本文,相信你已经领悟到了RxHttp的优雅,不管上传/下载,还是进度的监听,通通三步搞懂,掌握请求三部曲,就掌握了RxHttp的精髓。


其实,RxHttp远不止这些,本文只介绍了RxHttp + Flow的配合使用,更多功能,如:公共参数/请求头的添加、请求加解密、缓存等等,请查看


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

通过命令行玩转Git,需要记住那些命令?

Git 简介 什么是 Git ? Git 是目前世界上最先进的分布式版本控制系统!!!什么?啥意思?不懂,没关系,你只要记住,它很重要,非常重要,程序猿的必备技能即可。 Git 的命令非常非常多,这里强调一下,不要傻傻的去背这些命令,没啥卵用,有些命令可能你这...
继续阅读 »

Git 简介


什么是 Git ? Git 是目前世界上最先进的分布式版本控制系统!!!什么?啥意思?不懂,没关系,你只要记住,它很重要,非常重要,程序猿的必备技能即可。


Git 的命令非常非常多,这里强调一下,不要傻傻的去背这些命令,没啥卵用,有些命令可能你这辈子你都未必使得上。


本章的目的是教你如何通过命令行完成 Git 的日常基操,并会适当介绍命令的意义,加深你的理解。


在介绍这些命令之前,我们先来看一张灰常重要的图:


image.png


图中有四个空间,是 Git 工作流程的精髓所在,分别是:



  • Remote: 远程仓库,即你在 Github 或者 Gitee 等平台上创建的项目仓库;

  • Repository: 本地仓库,你可以认为就是我们拉取项目后生成的 .git 文件夹;

  • Index: 暂存区,事实上它只是一个文件,即 .git 文件夹里面的 index 文件,它保存即将提交到本地仓库的文件列表信息;

  • workspace:  工作区,即你在 VS code 或者 WebStorm 编译器正在编写的代码。


Git 基本命令手册


1.克隆/拉取项目


拉取项目是开始搬砖的第一步,一般创建好项目远程仓库,你就能获取到一个 .git 结尾的地址,或者这个地址可能由公司同事给到你,之后你随便找个目录,通过 Git Bash 输入下面命令即可拉取到项目到本地。


git clone 你的项目地址.git

2.查看远程分支


一般拉取完项目后,我们是处于 master/main 主分支,不要着急就去编写代码,先要查看一下项目的远程分支情况,根据要求看是否需要切换到某个分支进行特定开发。


git branch -r

3.查看本地分支


一般本地创建的分支情况应该是和远程分支一一对应,当然,在未进行发布或者和远程分支建立关联,本地分支并不会影响到远程分支,你可以随意创建、删除和修改。


git branch

4.创建本地分支


git branch dev(分支名称, 可以随便取)

通过上面的命令,你会创建好一个本地分支,当然,该分支是基于你当前所在分支创建的。你可以再次敲命令去查看远程分支和本地分支各自的情况(多敲,很快命令就记住了-^〇^-)。


5.删除本地分支


对远程仓库分支并不会有任何影响


git branch -d dev

6.切换分支


创建好本地分支,我们就可以切换到该分支上。


git checkout dev


创建和切换两个操作也可以一起做: git checkout -b xxx (创建并切换到该本地分支)



7.发布本地分支到远程分支


当我们创建好一个本地分支的时候,这个时候还是不要着急去开始编码,我们要把该分支发布到远程仓库去,让远程仓库也拥有该分支,且让它和你本地分支进行关联,方便我们后续直接进行 git pull 或者 git push 操作。


git push origin dev

发布完本地分支后,你可以同样通过 git branch -r 去查看你远程仓库的分支列表是否多了新的分支。


8.建立本地分支与远程分支的关联


本地分支与远程分支关联这步不是必须,但后续就能很方便的直接使用 git pull 或者 git push 获取或提交该分支的代码,而不用去指定分支。


git push --set-upstream origin dev

如果你不关联两者分支的关系,强行去使用,你可能会遇到图中的情况,大致意思就是让你指定目标分支或者去关联分支情况再进行操作。(Git 的提示信息还是很友好的)


image.png


9.添加文件进暂存区


完成上面的几步,我们就能开始搬砖了。在对代码更改后,要把提交代码到远程仓库,我们就要先把代码添加到暂存区,之后提交到本地仓库,最后才能提交到远程仓库。



  • 把工作区某个文件添加进暂存区,比如 src/main.js 文件,则 git add src/main.js


git add xxx(文件路径)
# 多个
git add xxx(文件路径) xxx(文件路径) xxx(文件路径) ...


  • 把工作区更改的所有文件都添加进暂存区


git add .


这里你可能会在想,为什么要先添加到暂存区,再本地仓库之后才提交到远程仓库呢? 如果你是从 SVN 转过来,可能会稍微有点不了解暂存区,这里涉及暂存区的意义,网上有很多解释,这里就不做过多的解释了。

但你可以简单的怎么理解,假如你在开发中开发了用户的添加功能和文章的添加功能,两个功能都同时开发完了,因为都互不影响,完全独立,你想分成两次提交分别写上对应的 commit 信息说明,这个时候就可以使用到暂存区,先将用户添加功能添加到暂存区,然后 commit 到本地仓库,再进行文章添加功能的提交,最后在一起 push 到远程仓库即可。



10.删除文件出暂存区


当你误把文件添加进暂存区,也不要慌,有添加,就肯定有删除。



  • 把工作区某个文件删除出暂存区


git rm --cached xxx(文件路径)


  • 清空暂存区,暂存区实质是 .git 目录下的 index 文件,将此文件删除,那么就认为暂存区被清空。


rm .git/index


当然,这只是把暂存区中跟踪的文件移除而已,不会改动原文件的内容,原先更改的内容还在。



11.查看工作区与暂存区状态


这个命令用于查看工作区和暂存区的状态,能看到哪些文件被修改了,它修改后是否被暂存了,又或者还没有暂存。这个暂存的过程,专业的叫法是 Git tracked,也就是是否被跟踪。


git status

通过下图,我们能查看到所有改动过的文件,绿色的文件是已经添加进暂存区的,红色的文件则是未添加到暂存区的,而且每个文件前都有对应的操作说明,如果是新文件则是 new file,修改的文件则是 modified,删除的是 deleted, 如果是未添加进暂存区的新文件,则没有。


image.png


12.提交暂存区文件到本地仓库


git commit -m "说明信息"

通过上面的命令,我们就将暂存区的文件提交到本地仓库了,我们可以通过 git status 再次查看暂存区的情况。


image.png


Git 的提示真的是非常友好的。


13.查看提交记录


这个命令可以显示提交过的记录。


git log

image.png


进入提交记录日志列表后,可以按 q 退出。



上面的命令会显示所有的 commit 记录,如果你想要显示最近几条记录,你可以通过 git log -n(n为数字, 可以随意指定) 命令完成。



14.提交本地仓库文件到远程仓库


git push

执行这条命令的前提是进行过第7步骤才行哦。


把本地仓库文件提交到远程仓库后,我们再次查看提交日志。


image.png


15.拉取新代码


git pull

这个命令同样也是建立在第7步骤之前的。


Git 进阶命令手册


记住上面15个命令,Git 的日常基操基本也满足了,当然现在各种编辑器功能强大,基本都集成了 Git 的可视化操作,不用命令行来操作,也完全没有问题。


但是程序猿使用命令行不是一件很酷的事情吗?不为别的,只为装13也是可以搞一搞的,哈哈哈。下面再来讲一些命令,虽然使用频率不高,但是也是很重要的。


1.查看全局的Git配置


git config --global -l
# or
git config --global --list

2.查看全局用户名及邮箱


git config --global user.name
git config --global user.email

3.设置全局用户名及邮箱


git config --global user.name "你自己的用户名"
git config --global user.email "你自己的邮箱"

4.查看局部项目用户名及邮箱


要在当前项目根目录下执行。


git config user.name
git config user.email

5.设置局部项目用户名及邮箱


要在当前项目根目录下执行。


git config user.name "你自己的用户名"
git config user.email "你自己的邮箱"

6.删除远程仓库分支


这个命令对于一些未设有保护的分支来说,是挺危险的操作,要慎重执行。


git push --delete origin dev(分支名称)

7.修改远程仓库分支名称


修改远程仓库分支的名称过程是:先修改本地分支名称 - 删除远程分支 - 重新发布本地分支到远程仓库


# 修改本地分支名称
git branch -m 旧分支名称 新分支名称
# 删除远程分支
git push --delete origin 分支名称
# 发布新分支
git push origin 新分支名称
# 重新建立关联
git push --set-upstream origin 新分支名称

8.合并分支


合并分支是一个比较常见的操作了,当你在一个分支开发完新功能后,很多时候这些分支最终都要合并到 master/main 这个主分支上,这个主分支拥有所有分支的代码,并且它是稳定且在生产环境上跑的,所以在你确定要将分支合并到主分支上之前,一定要确保这个分支代码是没有问题。


# 切换到稳定的目标分支
git checkout master
# 更新最新代码,防止本地仓库对应分支代码不够新而出现问题
git pull
# 合并分支到本地仓库
git merge dev
# 发布合并后的新代码到远程分支
git push

合并后,你可以通过 git log 去查看是否有相关的 commit 记录。


9.将A分支直接合并到B分支


git merge xxx(A分支名称) xxx(B分支名称)

10.合并单个commit


有时候我们只想合并某一个 commit 来满足一些特定需要,这也是可以做到的。


# 切换到稳定的目标分支
git checkout master
# 合并某个commit到本地仓库
git cherry-pick xxx(commitId)
# 发布合并后的新代码到远程分支
git push

commitId 是一个 commit 的唯一标识,你可以通过 git log 来找到它,它是一串很长不重复的字符。


image.png


当然,你也可以去到 Github 或者 Gitee 平台对应项目里面找。



合并单个 commit 你可能会遇到冲突的情况,如:在我执行合并某一个 commit 给我报了一个错,大致意思就是代码冲突了,需要去调整后,才能提交。


image.png


代码冲突是件非常蛋疼的事情,不仅仅合并的时候会发生冲突,比较频繁发生的场景是多人共同开发的时候,因为两人负责同个项目,就有极大的可能会改到相同的文件,相同的代码,这就会造成冲突。一般这个时候 Git 会阻止你的提交,让你先去整改,这时候就需要你非常谨慎细心的去处理,因为一旦粗心,就极有可能干掉同伴的代码或者自己的代码,这会造成很严重的后果。(不要问我怎么知道的,都是血的教训︶︿︶)


解决代码冲突一般我会借助编辑器等工具来完成,就不通过命令行操作了,这样比较方便,不容易出错。我使用的是 VS code 编辑器,在编辑器内冲突的文件一般都会标红,点开文件,会发现里面会有写 <<<< 的符号,被符号包围的内容就是冲突的地方。


image.png


编辑器一般会提供四个选项帮助你快速操作,当然你也可以手动删除修改。



  • Accept Current Change:接收当前更改,也就是冲突部分以旧的为准。

  • Accept Incoming Change:接受传入的更改,也就是冲突部分以新的为准。

  • Accept Both Changes:接受两个更改,也就是新旧都存在,可能会出现重复。

  • Compare Changes:比较变化,会分成两个文件,让你更直观的查看两者的冲突内容。


当你解决完冲突后,你可以保存文件,再执行以下操作,提交代码。


# 添加更改进入暂存区
git add .
# 提交暂存区到本地仓库
git commit -m ""
# 提交本地仓库commt到远程仓库
git push


11.撤销最近的一个commit


git reset --soft HEAD^

这个命令用于撤销本地仓库最近的一个 commit ,撤销后会回到暂存区中,通过 git log 可以查看 commit 记录会减少,但不影响远程仓库。


还有另一个相似的撤销 commit 命令,但它比较危险,撤销后的内容是直接就删除的,不会回到暂存区,要慎重使用!!!


git reset --hard HEAD^

12.查看简洁提交记录


这个命令可以让你更加直观的查看你需要的信息。


git log --pretty=oneline

image.png



其实这些信息都存放在 .git 文件夹中,在 .git\logs\refs\heads 下记录了所有分支的 commit 版本号。



13.备份工作区内容stash


这是一个神奇的命令,特别对项目分支比较多的情况,是非常有用的,他的使用场景大致是:当你正在A分支中开发功能,你临时接到一个紧急的需求,需要优先切换到B分支中去开发,这个时候A分支中已经改动的代码要怎么办呢?这时就能使用 stash 来备份代码了。


git stash

执行命令后,当前工作区会回到最近的一次 commit 状态,并将更改的代码保存在 Git 栈中,这样你就能先切换到B分支中去开发,等B分支功能开发完成,再切换回A分支,通过下面的命令取回原来的更改的代码,继续进行A分支的功能开发了。


git stash pop

是不是非常的 nice!!!


14.查看备份列表或清空备份列表



  • 显示Git栈内的所有备份


git stash list


  • 清空Git栈


git stash clear

15.查看远程仓库地址


git remote -v

image.png


16.更改远程仓库地址


git remote set-url origin xxx(新仓库地址)

更改远程仓库地址有另外的两种方式


# 删除项目的远程仓库地址
git remote rm origin
# 添加项目的远程仓库地址
git remote add origin xxx(新仓库地址)

或者去改配置文件


17.比较工作区和暂存区的所有差异


git diff

这个命令是对比工作区和暂存区的所有差异,但个人感觉不太直观,也可能只是比较少用吧,关于对比我更愿意借助编辑器工具来查看。


image.png


对比工作区单个文件和暂存区的区别:


git diff xxx(文件地址)

18.将工作区中的文件还原成与暂存区的一致


git checkout xxx(文件地址)
# 批量还原
git checkout xxx(文件地址) xxx(文件地址) xxx(文件地址) ...


收起阅读 »

优雅的命名

前言 优秀的代码往往是最通俗易懂的代码,在于它的易于维护。在开发过程中,变量/方法优秀的命名往往有助于理解该变量/方法的用途,起到命名即注释的作用。而糟糕的命名往往会让人摸不着头脑。为了提高代码的可维护性,我们需要更优雅的命名方式。 一、通用规则 1. 有意义...
继续阅读 »

前言


优秀的代码往往是最通俗易懂的代码,在于它的易于维护。在开发过程中,变量/方法优秀的命名往往有助于理解该变量/方法的用途,起到命名即注释的作用。而糟糕的命名往往会让人摸不着头脑。为了提高代码的可维护性,我们需要更优雅的命名方式。


一、通用规则


1. 有意义


起一个有意义的变量名这条相信绝大多数开发者都能做到,即变量名有实际的指代意义,在此不再赘述。


2. 指代具体


命名时需要使其更加具体详尽,可以具体到所在的模块,或者能表达出其逻辑/功能。


/* bad */
.title {}
/* good */
.head-title {}

// bad
const info;
// good
const userInfo;

3. 遵循传统


无论是CSS、JavaScript、还是文件的命名,都有一些约定俗成的惯例和规范,我们只需遵循即可。


4. 约定规范


命名的规则有很多种,没有高低之分,只有相对合适,没有绝对完美的规则。通常为了维持项目的风格统一,通常在一个项目中,相同种类的规则只选取一种。毕竟规范也只是一种工具,运用规范的根本目的是为了更好的开发和维护,太过复杂的规范反而会阻碍正常开发。因之,在项目启动前,在技术栈选取后就应当进行规范的约定,这个过程是团队意见的整合,毕竟规范是要靠团队成员的相互配合。


二、CSS 中的命名


1. 划分原则


CSS中的类名根据其职责可以分为公共类名和自定义类名。其中公共类名又可以分为常见类名(一般是约定俗成)和工具类名。


2. 常见类名


下面整理了一些常见的 css类名 供大家参考:








收起阅读 »

❤️谈谈grid布局(细读必有收获)

grid布局的理念是把网页划分成一个一个网格组合成不同样式的布局,再通过对网格进行内容填充,组成一个网页。通过一下这个案例了解grid的基本概念👇👇 经典九宫格布局: 🚨关键点🚨: 容器: 需通过display:grid设置为grid容器,容器中包含所有i...
继续阅读 »

grid布局的理念是把网页划分成一个一个网格组合成不同样式的布局,再通过对网格进行内容填充,组成一个网页。通过一下这个案例了解grid的基本概念👇👇


经典九宫格布局:
image.png



🚨关键点🚨:

容器: 需通过display:grid设置为grid容器,容器中包含所有item

行: 横向为行,对应颜色块123

行距: 上下两个item的间距为行距

列: 纵向为列,对应颜色块147

列距: 左右两个item的间距为列距

item(子元素): 也就是上图对应的123456789颜色块

边: 每个itme共有 上 下 左 右 四条边



1.1 display


display属性规定是否/如何显示元素。我们需要使用grid布局,就要把容器设置为grid或者inline-grid

grid 设置为块级元素的grid布局

inline-grid 设置为行内元素的grid布局

区别如下:


image.png
代码案例
在线代码入口:👉👉(点击传送)


.grid_container {
display:grid;
/* display:inline-grid; */
grid-template-columns: 100px 100px 100px;
grid-template-rows: 100px 100px 100px;
}

1.2 grid-template-columnsgrid-template-rows


grid-template-columns属性用来定义grid布局的每一列列宽

grid-template-rows属性用来定义grid布局的每一行行高

代码案例1:在线代码入口👉👉(点击传送)

定义一个三行三列,每列列宽100px,每行行高100px


.grid_container {
display:grid;
grid-template-columns: 100px 100px 100px;
grid-template-rows: 100px 100px 100px;
}

代码案例2:在线代码入口👉👉(点击传送)

当拥有很多行和列的时候,普通的写法根本不实在,所以现在引入一个函数repeat()

repeat()函数可设置重复的值,或者重复的一个模式,还是以三行三列100px为例:


.grid_container {
display:grid;
/* 重复一个值 */
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 重复一个模式
grid-template-columns: repeat(3,100px 50px);
grid-template-rows: repeat(3,100px 50px);
*/

}

代码案例3:在线代码入口👉👉(点击传送)

这里以圣杯布局为例:左右固定,中间自适应。在这种情况下固定的列宽或行高已经不能满足实现圣杯布局了,所以这个例子引入两个关键字autofr

auto:自适应属性

fr:fraction 的缩写,意为"片段",可以看做百分比属性,通过以下例子可以帮助理解该关键字

auto为例:


.grid_container {
display:grid;
/* 左右列150px,中间列自适应*/
grid-template-columns: 150px auto 150px;
/* 一行行高 300px*/
grid-template-rows: repeat(1,300px);
}

image.png

fr为例:
左右列占比 2/10 = 20% ,中间列占比 6/10 = 60%, 注意10 = 2+6+2


#grid_container{
display: grid;
grid-template-columns: 2fr 6fr 2fr;
grid-template-rows: repeat(1,300px);
}

image.png

代码案例4:在线代码入口👉👉(点击传送)

当需求是要求每个item子元素的宽高只有100px,但是容器宽度自适应时,我们就无法得知应该设置几行几列的属性了,所以这里再引入一个关键字auto-fill

auto-fill:自动填充

⚠️注意:grid-template-rows需要使用关键字时,容器必须要有固定高度⚠️


#grid_container{
display: grid;
height:500px;
grid-template-columns: repeat(auto-fill,100px);
grid-template-rows: repeat(auto-fill,100px);
}

代码案例5:在线代码入口👉👉(点击传送)

如果grid布局的子元素设置为自适应宽度,但宽度缩小到一定程度时就会出现错误,所以避免出现这种错误,我们必须要有一个最小的宽度,所以这里引入一个函数minmax()

minmax():设置一个长度范围,参数1:最小值,参数2:最大值

例子:最小值500px,最大值6fr


.grid_container {
display:grid;
width:600px;
grid-template-columns: 2fr minmax(500px,6fr) 2fr;
/* 自行屏蔽查看区别 */
/* grid-template-columns: 2fr 6fr 2fr; */
grid-template-rows: repeat(1,300px);
}

1.3grid-template-areas


1.3 grid-template-areas


grid-template-areas:用于划分区域,通过以下案例可以帮助理解

代码案例1:在线代码入口👉👉(点击传送)

1、划分出ai九个区域

2、或者每一行划分一个区域,三行就是a b c三个区域

2、当然可以不划分部分区域,使用(.)点表示不需要划分的区域


.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 划分九个区域 */
grid-template-areas:
'a b c'
'd e f'
'g h i';

/* 划分三个区域 */
/* grid-template-areas:
'a a a'
'b b b'
'c c c'; */
/* 不划分部分区域 */
/* grid-template-areas:
'a . c'
'a . c'
'a . c'; */
}


划分区域的用途会在后面结合其他的属性进行讲解!!



1.4 grid-row-gapgrid-column-gapgrid-gap


grid-row-gap:行间距

grid-column-gap:列间距

grid-gap: 行间距 和 列间距 的简写形式,如:grid-gap: <grid-row-gap> <grid-column-gap>;


代码案例1:在线代码入口👉👉(点击传送)

这里以最简单的九宫格为例


.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
grid-row-gap:10px;
grid-column-gap:20px;
/* 下面语句和上面设置的间距效果相同,自行解除注释对比 */
/* grid-gap:10px 20px; */
}

image.png


1.5 grid-auto-flow


grid-auto-flow:设置grid布局的放置顺序,正常情况下是,从左到右放置每一个item子元素,在特殊情况下我们可以重新改变它的放置顺序,比如从上到下。可选值:从左到右 row、从上到下column、稠密从左到右row dense、稠密从上到下column dense,接下来会一一举例说明;

正常设置grid-auto-flow属性为 rowcolumn会出现以下两种效果,左边为row,右边为column


image.png
image.png


这里还是以九宫格为例,我们将 数字1数字2数字3 方块设置为各占2个单元格时,在grid-auto-flow属性默认等于row就会出现以下一幕
image.png

当我们把代码设置成 稠密型的从左到右row dense时,布局就会被尽可能的填满,不会出现上图存在的空格


image.png

代码如下:在线代码入口👉👉(点击传送)


.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 默认,从左到右 */
grid-auto-flow:row;
/* 稠密型 从左到右 请自行开启屏蔽 */
/* grid-auto-flow:row dense; */
}
.item-1 {
background-color: #B53471;
grid-column-start: 1;
grid-column-end: 3;
}
.item-2 {
background-color: #ffcccc;
grid-column-start: 1;
grid-column-end: 3;
}
.item-3 {
background-color: #ff4d4d;
grid-column-start: 1;
grid-column-end: 3;
}


通过上面的例子可以清楚稠密型其实就是,尽可能填满容器而已,所以column dense例子就不多做解析,在线代码入口👉👉(点击传送)



image.png
image.png


1.6 justify-itemsalign-itemsplace-items


属性说明

justify-items:设置item子元素内容水平位置

align-items:设置item子元素内容垂直位置

place-items:align-itemsjustify-items 两个属性的简写方式,若省略第二个值,则认为第二个值等于第一个值


place-items: <align-items> <justify-items>

属性可选值(三个属性均有以下可选值)


start案例:在线代码入口👉👉(点击传送)

对齐子元素容器的起始边框,justify-items对齐水平的起始边框,align-items对齐垂直的起始边框

image.png

end案例:在线代码入口👉👉(点击传送)

对齐子元素容器的结束边框,justify-items对齐水平的结束边框,align-items对齐垂直的结束边框

image.png

center案例:在线代码入口👉👉(点击传送)

子元素容器内部居中,justify-items水平居中,align-items垂直居中

image.png

stretch案例:在线代码入口👉👉(点击传送)

默认就是这个属性,只要不设置宽度和高度就会把宽高拉伸铺满

image.png


1.7 justify-contentalign-contentplace-content



注意这三个属性和1.6描述的区别在于, justify-itemsalign-itemsplace-items是针对子元素内容的,justify-contentalign-contentplace-content是针对grid容器内容的



属性说明

justify-content:设置grid布局容器内容水平位置

align-content:设置grid布局容器内容垂直位置

place-content:align-contentjustify-content 两个属性的简写方式,若省略第二个值,则认为第二个值等于第一个值


place-content: <align-content> <justify-content>

属性可选值(三个属性均有以下可选值)







































可选值可选值说明
start对齐grid容器的起始边框
end对齐grid容器的结束边框
centergrid容器内部居中
stretchgrid容器内容大小没有指定时,拉伸占据整个grid容器
space-around每个子元素两侧间隔相等,所以子元素之间间隔比容器边框的间隔大一倍
space-between子元素与子元素之间间隔相等,子元素与容器边框没有间隔
space-evenly子元素与子元素之间间隔相等,子元素与容器边框之间也是同样长度的间隔

start案例:在线代码入口👉👉(点击传送)

对齐容器的水平和垂直起始边框,justify-content对齐水平的起始边框,align-content对齐垂直的起始边框


  justify-content:start;
align-content:start;

image.png


end案例:在线代码入口👉👉(点击传送)

对齐容器的水平和垂直结束边框,justify-content对齐水平的结束边框,align-content对齐垂直的结束边框


  justify-content:end;
align-content:end;

image.png




center案例:在线代码入口👉👉(点击传送)

容器内容水平和垂直居中对齐,justify-content容器内容水平居中对齐,align-content容器内容垂直居中对齐


  justify-content:center;
align-content:center;

image.png




stretch案例:在线代码入口👉👉(点击传送)

自动拉伸铺满grid容器,justify-content水平铺满容器,align-content垂直铺满容器


  justify-content:stretch;
align-content:stretch;

image.png




space-around案例:在线代码入口👉👉(点击传送)

每个子元素两侧间隔相等,所以子元素之间间隔比容器边框的间隔大一倍


  justify-content:space-around;
align-content:space-around;

image.png




space-between案例:在线代码入口👉👉(点击传送)

子元素与子元素之间间隔相等,子元素与容器边框没有间隔


  justify-content:space-between;
align-content:space-between;

image.png


space-evenly案例:在线代码入口👉👉(点击传送)

子元素与子元素之间间隔相等,子元素与容器边框之间也是同样长度的间隔


  justify-content:space-evenly;
align-content:space-evenly;

image.png


1.8 grid-auto-columnsgrid-auto-rows


grid-auto-columns:设置多余列的列宽

grid-auto-rows:设置多余行的行高

在某种情况下,我们设置了9宫格布局可能会出现10个item子元素,那正常的前9个子元素都设置有合适的宽高,但是多余出现的第10个如果不进行设置,就会出现不正常的布局,通过以下案例可以帮助理解


image.png

当使用 grid-auto-flow:column;改变默认的放置顺序会出现以下情况
image.png

所以在出现以上情况时,使用grid-auto-columnsgrid-auto-rows解决问题
在线代码入口👉👉(点击传送),自行修改案例代码观察变化。


.grid_container {
grid-auto-columns:100px;
grid-auto-rows:100px;
}

image.png


1.9 grid-templategrid


grid-template属性是grid-template-columnsgrid-template-rowsgrid-template-areas这三个属性的合并简写形式。

grid属性是grid-template-rowsgrid-template-columnsgrid-template-areas、 grid-auto-rowsgrid-auto-columnsgrid-auto-flow这六个属性的合并简写形式。



这两个属性用法比较复杂,后期再考虑重新写一篇文章讲解,有需要的请在评论区留言,留言数多的话,会尽快出新文章



2.0(子元素)grid-column-startgrid-column-endgrid-row-startgrid-row-endgrid-columngrid-row


横纵向网格线始终比横纵向子元素多1,下面通过几个案例帮助理解
image.png


案例1:在线代码入口👉👉(点击传送)

🥇当方块一想占满横向两个方格时,将方块一的grid-column-startgrid-column-end分别设置成13,或者设置grid-column: 1/3

🥈当方块一想占满纵向两个方格时,将方块一的grid-row-startgrid-row-end分别设置成13,或者设置grid-row: 1/3


.item-1 {
background-color: #B53471;
/* 横向 */
/* grid-column-start: 1;
grid-column-end: 3; */
grid-column: 1/3; /*效果相同 */

/* 纵向 */
/* grid-row-start: 1;
grid-row-end: 3; */
grid-row: 1/3; /*效果相同 */
}

image.png

案例2:在线代码入口👉👉(点击传送)

🥇当遇到多个方格进行属性设置时,需要考虑网格线是否被别的元素包含,如下图所示:
image.png

所以在案例1的原有基础上,我们想把方块2的纵向占两个方块,位置放在原方块4原方块7的位置,那么我们就要考虑方块1已经包含过的网格线不能使用。所以设置上边框网格线的的时候就要避开纵向的第2条网格线,这样我们要设置上边框网格线为3,下边框网格线为5


.item-2 {
background-color: #ffcccc;
grid-column: 1/2;
grid-row: 3/5;
}

效果如下:

image.png


2.1 (子元素)justify-selfalign-selfplace-self



其实这一节没啥好讲的,属性justify-itemsalign-itemsplace-items 属性效果一样,只不过前者是统一设置grid容器中的子元素内容位置,后者则是在子元素上单独设置,并且会覆盖统一设置的效果。



justify-self:设置水平位置

align-self:设置垂直位置

place-selfalign-self属性和justify-self属性的合并简写形式。(忽略第二个值,则认为第二个值等于第一个值)

案例1:在线代码入口👉👉(点击传送)

所有子元素内容水平垂直居中,第一个子元素内容对齐垂直方向结束边框align-self: end;,对齐水平方向结束边框justify-self: end;


代码和效果如下:justify-selfalign-self 覆盖了justify-itemsalign-items 设置的居中显示


.grid_container {
justify-items: center;
align-items: center;
}
.item-1 {
justify-self:end;
align-self:end;
background-color: #B53471;
}

image.png


2.1 (子元素)grid-area















属性名属性说明
grid-area指定子元素防止在哪个区域

在上面 1.3 中已经说过如何划分区域了,接下来我们通过 grid-area 属性来了解如何使用区域

案例1:在线代码入口👉👉(点击传送)

将就九宫格中1 2 3 方块替换到 4 5 6方块


.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 划分九个区域 */
grid-template-areas:
'a b c'
'd e f'
'g h i';
}
.item-1 {
background-color: #B53471;
grid-area: d;
}
.item-2 {
background-color: #ffcccc;
grid-area: e;
}
.item-3 {
background-color: #ff4d4d;
grid-area: f;
}

image.png


案例2:在线代码入口👉👉(点击传送)

将九宫格中的方块1 2 3 纵向占满两个单元格,方块4 水平占满3个单元格


.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 划分三个区域 */
grid-template-areas:
'a b c'
'a b c'
'd d d';
}
.item-1 {
background-color: #B53471;
grid-area: a;
}
.item-2 {
background-color: #ffcccc;
grid-area: b;
}
.item-3 {
background-color: #ff4d4d;
grid-area: c;
}
.item-4 {
background-color: #ffaf40;
grid-area: d;
}

image.png


作者:是舍长
链接:https://juejin.cn/post/7017074528752762911

收起阅读 »

构建大型前端业务项目的一点经验

目前工作中接手的几个项目都是 B端 PC 项目,业务逻辑都比较复杂,并且代码历史较久,在日常的维护中经常会遇到想摊手的技术问题,发现问题、解决问题、避免再次出现同样的问题,既是项目可持续维护的因素之一,也是个人工作经验积累的一个过程 本文可当做 接手前端新项...
继续阅读 »

目前工作中接手的几个项目都是 B端 PC 项目,业务逻辑都比较复杂,并且代码历史较久,在日常的维护中经常会遇到想摊手的技术问题,发现问题、解决问题、避免再次出现同样的问题,既是项目可持续维护的因素之一,也是个人工作经验积累的一个过程



本文可当做 接手前端新项目?这里有些注意点你可能需要留意一下编写可维护的现代化前端项目 的补充



具体、连贯的变量名


在前后端分离的现代化 web开发流程下,相比于以往,前端承担了更多的业务逻辑,尽管存在着 TypeScript等约束工具,但相比于后端语言,js 仍具备相当大的灵活性,这就导致了代码一旦复杂,前端代码的排查会更加麻烦


单一变量的层层传递与到处使用是很常见的事情,变量的命名对于追踪变量有着相当大的影响


所以变量名称必须是具体且有实际意义的,不提倡为了追求变量名的精确性而使得变量名称冗长,但模糊而宽泛的变量名同样不可取,这就要求变量名称即准确又简短,在某些时候,可能很难做到这一点,个人倾向是,实在无法做好权衡的前提下,宁愿选择冗长的变量名也不要选择一个模糊宽泛的


例如,data可以当做是一个变量名,这个变量名用于临时的局部变量没啥问题,毕竟你一眼就能看到这个变量所有的使用范围,但如果变量所持有的数据的作用范围较大(例如跨组件)且具备实际业务意义,那么就不太妙了,data 可以用作任何数据的变量名,当需要追踪 data的时候,在编辑器里搜索 data,发现到处都是 data,并不是一件美好的事情


一旦确定好了变量名后,最好不要再对其进行重命名,例如想将 A组件里的 userData 传递到 B 组件中,B组件最好原模原样地接收这个变量名,而不是将其命名为其他的什么名称


有些时候可能就必须要在传递变量的时候进行重命名,例如第三方组件就接收 data,那么你无法传递 userData,这种情况下当然不得不重命名


避免对变量重命名的目的主要是,为了防止在追踪变量的时候因变量名称改变而产生思维上的重新整理,一个变量被重命名了好几次后,追踪到底再回过头来你可能就忘记了自己当初在追踪什么东西,同时对于搜索功能也不太友好,从一而终的连贯变量名可以让你一次搜索就能从追踪的起始位置跳过中间一大堆逻辑直接到终点


必要的选择器


现代化的前端项目基本都会使用 ReactVue等数据驱动的框架,UI组件一般也都是使用别人封装好的组件库,除非是要写样式,否则html元素选择器基本上都是可有可无的了,但并不是说就不需要了,最起码的一个好处是,让你想在代码里查找页面上的一个元素时,直接根据选择器名就能精准定位了


页面上有个弹窗展示得不太对,你在浏览器页面里看到这个弹窗元素名叫 ant-modal-wrap,是个第三方的组件所以你代码里根本搜不到这个选择器名;页面上有一句文案有点问题,你在浏览器页面里看到这个文案所在的元素是个没有选择器的 div标签,在目前普遍 全站div的浪潮下,光是定位这个文案到底是哪里的就够花费好一阵子了


所以,这里选择器是起到了一个索引的作用,既然是索引,那么同样应该遵守上面变量名相关的规则,即选择器名称应当 即准确又简短


优化应该从一开始就开始


不要提前优化


相信很多人都听过这句话,我曾经将这句话当做是至理名言,但经历的多了之后,目前已经开始有所质疑了


不要提前优化,那么要在什么时候优化?快要 hold 不住的时候才优化?迭代了七八十版的时候再优化?团队人员换了一茬又一茬的时候再优化?


当然是可以的,但是那个时候谁来优化,谁来做这种在业务看来毫无产出甚至是可能优化出 bug的吃力不讨好的事情?


一个函数里塞了数十层的 if...else,函数体的代码量超过千行,看着就应该要被优化的,但是这些代码在这里绵延了数年之久,经过了一批又一批不同程序员的修改,承载了不知多少明面上暗地里的业务逻辑,技术上或许好优化,但谁能保证某处优化不会对业务逻辑造成破坏?


没有提前优化,过程中也没有优化,那就完全是没有任何优化了,因而屎山就诞生了


我认为 不要提前优化 这句话是产生在一个朝九晚五不加班需求少有充足时间做技术优化的语境之下,这种语境下,这句话是没啥问题的,只是大部分情况下,现实情况根本不符合语境,所以这句话就有问题了


该拆的组件、该提取的公共方法、该规划的目录结构、该引入的代码规范……应该从一开始就形成,不要等着需要优化的时候才优化,那个时候已经来不及了


复用(组件、方法)


代码复用是为了提升工作效率,但如果只是为了复用代码而复用,就本末倒置了


通用方法、通用组件鼓励复用,但业务逻辑、业务组件,慎重复用


一个常见的例子是,移动端详情页页面和编辑页可能具有大部分重合的逻辑,但类似这种业务属性很强的组件,除非你确信这个组件将来不出现大的改动,否则不要为了贪图眼前的便利而想当然地进行复用


本来为了区分展示态和编辑态,就已经写了一些条件语句了,日后若是出现了已经复用的逻辑必须要按照业务需求进行拆分,甚至是逻辑完全南辕北辙,初期还好,或许还能抢救一下拆分出来,但到了中后期才发现这个问题很可能已经晚了,掺杂了那么多的业务逻辑,你还敢去做拆分吗?那么这个复用组件的代码量必然要被大量的 if...else 占领,修改任何一个功能点、排查任何问题都要兼顾两套逻辑,对于维护者来说,这会造成相当大的心智负担,对于项目本身来说,维护的代码将会变得更大


业务代码是千变万化的,原本多个场景下相似的逻辑,很可能随着业务的迭代变得毫无关系,在这种场景下,复用不仅不能提高工作效率,反而还会拖后腿


而对于通用方法和通用组件来说,为了更加彻底地解耦,其应当是函数式的,不应当对外部状态产生隐式地修改


通用方法最好是纯函数,相同的输入有相同的输出,入参、出参都应当是明确的,让人一眼就看出来需要哪些入参,又会有哪些出参,而不是直接传入一个大的对象,然后在方法体内去一个个查找所需的对象属性


通用组件不应当自作主张修改外部数据,而应该将产生的变化主动抛出去,让上一层组件来明确决定如何使用这个变化


依据社区而不是从心


为项目选择设计模式、UI组件库、状态管理库等基础功能的时候,应当选取社区内热度更高的而非根据个人的喜好


你所认为很牛x的设计模式、第三方库等,可能是其他人根本就没听过的,或者其他人根本就不认同的,这只会增加团队之间的协作难度


团队合作项目的代码是用来传承的而不是用来炫技的


抛弃惯性思维


待在舒适区,这是人之本能,因为熟悉,所以上一个项目使用的技术栈在下一个项目里也要继续用


但是,真的合适吗?


上一个项目用了 mobx,这个项目里也必须要用吗?上一个项目里将所有的状态数据都放到了状态管理库里,这个项目也要这样做吗?上一个项目没用 TypeScript,这一个也不用吗?


可能是不需要的,可能是需要更换的,当然,并不是说就要跟上一个项目反着来,到底怎样最起码要有一个思考的过程,而不是上一个项目就是这样的,所以这一个也要这样


考虑清楚了再写TODO


有意识做优化是个好习惯,但意识得能落到实处


以我的经验看,在多人协作的、业务敏捷迭代的项目中,大多数 todo是无法完成的


大部分 todo都是基于当时的情况做出的考量,当时这个方法可能只有几行,todo 要做的时候很简单,但是当时没有做,当过了一段时间再想起来这事的时候,发现那个方法已经变成了几百行了,你还敢动吗?


或者换句话说,你还有完成这个 todo的心思吗?人都是懒惰的,你愿意将原本可以用在打游戏刷视频的时间用在完成这个 todo上吗?看到了别人写的 todo,并且也看明白了,但是你愿意帮别人完成这个 todo吗?


该做的事情应当立即完成,或许因为某些原因无法立即完成,所以你想延后再来做,但是一般情况下,后续再来完成的成本必然大于当下,现在都完成不了,凭什么认为以后就能完成?


真的需要做的事情,哪怕会让进度延期,只要你理由充分,其他人不可能也没理由去阻止你


小结


很多时候,一些让你能够写出更好的代码建议,实际上对于业务产出是毫无帮助的,哪怕你不遵守这些建议甚至反着来,也不影响你的产出不影响你的绩效,毕竟产出和绩效跟代码写得好不好并没有直接关系,甚至这些所谓的建议有时候还会影响你快速产出,只要我能拿出一个好的产出拿到一个好的绩效,代码写得糙点烂点又有什么关系?以后的事情以后再说呗,搞不好以后维护的人根本不是我


这种心理或许才是常态,毕竟这更加符合现实的利益


但如果你是一位对技术有追求的人,你真的甘心就如此吗?我认为除了现实的考量之外,还应当为自己写下的代码负责


作者:清夜
链接:https://juejin.cn/post/7016948081321050148

收起阅读 »

npm install之后发生了什么

  下载项目后,执行的第一个命令行一般都是 npm install 。在这个过程中可能一帆风顺,也可能遇到大大小小的报错,有时候花点时间各种搜索能解决,可下次遇到还是一头雾水的上网找各种方案尝试解决报错。   那么,你清楚当你输入 npm instal ,按下...
继续阅读 »

  下载项目后,执行的第一个命令行一般都是 npm install 。在这个过程中可能一帆风顺,也可能遇到大大小小的报错,有时候花点时间各种搜索能解决,可下次遇到还是一头雾水的上网找各种方案尝试解决报错。


  那么,你清楚当你输入 npm instal ,按下 Enter 键之后,究竟发生了什么吗?


正文


一、npm install之后发生了什么


  npm install 大概会经过以下几个流程,下面我们就来简单看一下(原图地址)。


install.jpg



  1. npm install执行后,会检查并获取npm配置,优先级为



项目级别的.npmrc文件 > 用户级别的.npmrc文件 > 全局的.npmrc文件 > npm内置的.npmrc文件



.npmrc 文件就是npm的配置文件。查看npm的所有配置, 包括默认配置,可以通过下面的命令:


npm config ls -l


  1. 然后检查项目中是否有package-lock.json文件。


  从npm 5.x开始,执行npm install时会自动生成一个 package-lock.json 文件。


package-lock.json 文件精确描述了node_modules 目录下所有的包的树状依赖结构,每个包的版本号都是完全精确的。


  因此npm会先检查项目中是否有 package-lock.json 文件,分为两种情况:



  • 如果有,检查package-lock.jsonpackage.json中声明的依赖是否一致



  • 一致:直接使用 package-lock.json 中声明的依赖,从缓存或者网络中加载依赖

  • 不一致:各个版本的npm处理方式如上图



  • 如果没有,根据package.json递归构建依赖树,然后根据依赖树下载完整的依赖资源,在下载时会检查是否有相关的资源缓存



  • 存在:将缓存资源解压到 node_modules

  • 不存在:从远程仓库下载资源包,并校验完整性,并添加到缓存,同时解压到 node_modules



  1. 最终将下载资源包,存放在缓存目录中;解压资源包到当前项目的node_modules目录;并生成 package-lock.json 文件。


  构建依赖树时,不管是直接依赖还是子依赖,都会按照扁平化的原则,优先将其放置在 node_modules 根目录中(最新的npm规范), 在这个过程中,如果遇到相同的模块,会检查已放置在依赖树中的模块是否符合新模块的版本范围,如果符合,则跳过,不符合,则在当前模块的 node_modules 下放置新模块。


二、npm缓存


  在执行 npm installnpm update 命令下载依赖后,除了将依赖包安装在 node_modules 目录下外,还会在本地的缓存目录缓存一份。我们
可以通过以下命令获取缓存位置:


// 获取缓存位置
npm config get cache

// C:\Users\DB\AppData\Roaming\npm-cache
复制代码

  如我的缓存位置在C:\Users\DB\AppData\Roaming\npm-cache下面的_cacache 文件夹中。


  再次安装依赖的时候,会根据 package-lock.json 中存储的 integrity、version、name 信息生成一个唯一的 key,然后拿着key去目录中查找对应的缓存记录,如果有缓存资源,就会找到tar包的hash值,根据 hash 再去找缓存的 tar 包,并把对应的二进制文件解压到相应的项目 node_modules 下面,省去了网络下载资源的开销。


  因此,如果我们可能因为网络原因导致下载的包不完整,这就可能造成删除node_modules重新下载的依旧是问题包,假如删除 node_modules 重新下载问题依旧,此时就需借助命令行清除缓存。


// 清除缓存
npm cache clean --force

复制代码

  不过 _cacache 文件夹中不包含全局安装的包,所以想清除存在问题的包为全局安装包时,需用 npm uninstall -g 解决


三、关于yarn


yarn简介:


  yarn是由Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具 ,正如官方文档中写的,Yarn 是为了弥补 npm 的一些缺陷而出现的。


yarn特点:



  • 速度快



  • yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。



  • 安全

    • 在执行代码之前,yarn 会通过算法校验每个安装包的完整性。



  • 可靠

    • 使用详细、简洁的锁文件格式和明确的安装算法,yarn 能够保证在不同系统上无差异的工作。




四、yarn和npm部分命令对比



总结


  无论是使用npm 还是 yarn 来管理你的项目依赖,我们都应该知其然更知其所以然,这样才能在项目中跟海的定位及解决问题,不是吗?




链接:https://juejin.cn/post/7016994983186006024

收起阅读 »