注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android-apk动态加载研究

前言 近期工作中遇到两个问题。 换应用皮肤 加载插件apk中的view Android 换肤技术一文中已经详细说明了如何进行应用换肤。而加载插件apk中的view,利用前文提到的换肤技术,居然无法实现!仔细重新研究Android apk动态加载机制,有了新...
继续阅读 »

前言


近期工作中遇到两个问题。



  • 换应用皮肤

  • 加载插件apk中的view


Android 换肤技术一文中已经详细说明了如何进行应用换肤。而加载插件apk中的view,利用前文提到的换肤技术,居然无法实现!仔细重新研究Android apk动态加载机制,有了新的发现,而且还可以提高插件加载效率。


布局加载


Android 换肤技术文中提到的加载插件图片方法,无法加载插件的布局文件。怎么尝试都是失败。布局文件是资源中最复杂的,需要解析xml中的其它元素,虽然布局文件的id可以获取,但xml中其它元素的id或者其它关联性的东西仍然无法获取,这应该就是加载插件布局文件失败的原因。


宿主应用无法直播加载插件中的xml布局文件,换一个思路,插件将xml解析成view,将view传递给宿主应用使用。


插件apk需要使用插件context才能正确加载view,宿主如何生成插件context呢?

  public abstract Context createPackageContext(String packageName,
@CreatePackageOptions int flags)

context.createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY |
Context.CONTEXT_INCLUDE_CODE);

使用上述方法可正确创建插件context。


除此之外还有一种方法(本人没有验证过),activity的工作主要是由ContextImpl来完成的, 它在activity中是一个叫做mBase的成员变量。注意到Context中有如下两个抽象方法,看起来是和资源有关的,实际上context就是通过它们来获取资源的,这两个抽象方法的真正实现在ContextImpl中。也即是说,只要我们自己实现这两个方法,就可以解决资源问题了。我们在代码中可以创建activity继承类,重写对应方法即可。具体可参考 下文

/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();

动态加载方式


目前动态加载方式均是使用DexClassLoader方式获取对应的class实例,再使用反射调用对应接口,代码如下:

  DexClassLoader loader = new DexClassLoader(mPluginDir, getActivity().getApplicationInfo().dataDir, null, getClass().getClassLoader());
String dex = getActivity().getDir("dex", 0).getAbsolutePath();
String data = getActivity().getApplicationInfo().dataDir;
Class<?> clazz = loader.loadClass("com.okunu.demoplugin.TailImpl");
Constructor<?> constructor = clazz.getConstructor(new Class[] {});

这种方式存在一个问题,较为耗时,如果宿主管理着许多插件,这种加载方式就有问题,使用下面这种方式可加快插件的加载。

public void getTail2(Context pluginContext){
try {
Class clazz = pluginContext.getClassLoader().loadClass("com.okunu.demoplugin.TailImpl");
Constructor<?> localConstructor = clazz.getConstructor(new Class[] {});
Object obj = localConstructor.newInstance(new Object[] {});
mTail = new IPluginProxy(clazz, obj);
} catch (Exception e) {
Log.i("okunu", "ee", e);
e.printStackTrace();
}
}

注意,一定要使用插件的context为参数,它和插件的其它类使用同一个classloader,既然能获取插件classloader,则可以获取插件中的其它类。如果不使用插件context为参数,则上述方法一定会报错。


总结


针对插件资源加载,其实分为两种形式。



  • 宿主直接使用插件资源,比如使用插件图片、字符串等

  • 宿主间接使用插件资源,比如在宿主中启动插件activity或者显示插件的view


第1种形式,可以在宿主应用中构造AssetManager,添加插件的资源路径。


第2种形式,宿主创建插件context并传递给插件,插件使用自己的context则可自由调用自己的资源了,如何创建插件context前文详述了两种方法。


注意一点,宿主中肯定无法直接调用插件的R文件的。


动态加载apk,也有两种方式。



  • 使用DexClassLoader加载插件路径,获取插件的classLoader。

  • 使用已经创建好的插件context,获取插件的classLoader,效果和第1种一样,但速度要更快


动态加载apk机制还有很多东西可以深入研究,比如说插件activity的生命周期等等,这些内容后续补充。


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

Android屏幕刷新机制

基础知识 CPU、GPU CPU:中央处理器,主要负责计算数据,在Android中主要用于三大绘制流程中Surface的计算过程。 GPU:图像处理器,主要负责对图形数据进行渲染,在Android中主要用于将CPU计算好的Surface数据合成后放到buff...
继续阅读 »

基础知识


CPU、GPU



  • CPU:中央处理器,主要负责计算数据,在Android中主要用于三大绘制流程中Surface的计算过程。

  • GPU:图像处理器,主要负责对图形数据进行渲染,在Android中主要用于将CPU计算好的Surface数据合成后放到buffer中,让显示器进行读取呈现到屏幕上。


逐行扫描


屏幕在刷新buffer的时候,并不是一次性扫描完成,而是从左到右,从上到下的一个读取过程,顺序显示一屏的每个像素点,按60HZ的屏幕刷新率来算,这个过程只有16.66666...ms。



  • 从初始位置(第一行左上角)开始扫描,从左到右,进行水平扫描(Horizontal Scanning)

  • 每一行扫描完成,扫描线会切换到下一行起点,这个切换过程叫做水平消隐,简称 hblank(horizontal blank interval),并发送水平同步信号(horizontal synchronization,又称行同步)

  • 依次类推,整个屏幕(一个垂直周期)扫描完成后,显示器就可以呈现一帧的画面

  • 屏幕最后一行(一个垂直周期)扫描完成后,需要重返左上角初始位置,这个过程叫垂直消隐,简称 vblank(vertical blank interval)

  • 扫描线回到初始位置之后,准备扫描下一帧,同时发出垂直同步信号(vertical synchronization,又称场同步)。


image.png


显卡帧率


表示GPU在1s中内可以渲染多少帧到buffer中,单位是fps,这里要理解的是帧率是一个动态的,比如我们平时说的60fps,只是1s内最多可以渲染60帧,假如我们屏幕是静止的,则GPU此时就没有任何操作,帧率就为0.


屏幕刷新频率


屏幕刷新频率:屏幕在1s内去buffer中取数据的次数,单位为HZ,常见屏幕刷新率为60HZ。屏幕刷新率是一个固定值和硬件参数有关。也就是以这个频率发出 垂直同步信号,告诉 GPU 可以往 buffer 里写数据了,即渲染下一帧。


屏幕刷新机制演变过程


单buffer


GPU和显示器共用一块buffer


screen tearing 屏幕撕裂、画面撕裂


当只有一个buffer时,GPU 向 buffer 中写入数据,屏幕从 buffer 中取图像数据、刷新后显示,理想的情况是显卡帧率和屏幕刷新频率相等,每绘制一帧,屏幕显示一帧。而实际情况是,二者之间没有必然的大小关系,如果没有同步机制,很容易出现问题。

当显卡帧率大于屏幕刷新频率,屏幕准备刷新第2帧的时候,GPU 已经在生成第3帧了,就会覆盖第2帧的部分数据。

当屏幕开始刷新第2帧的时候,缓冲区中的数据一部分是第3帧数据,一部分是第2帧的数据,显示出来的图像就会出现明显的偏差,称为屏幕撕裂,其本质是显卡帧率和屏幕刷新频率不一致所导致。


双buffer


安卓4.1之前

基本原理就是采用两块buffer。

GPU写入的缓存为:Back Buffer

屏幕刷新使用的缓存为:Frame Buffer

因为使用双buffer,屏幕刷新时,frame buffer不会发生变化,通过交换buffer来实现帧数据切换。
什么时候就行buffer交换呢,当设备屏幕刷新完毕后到下一帧刷新前,因为没有屏幕刷新,所以这段时间就是缓存交换的最佳时间。

此时硬件屏幕会发出一个脉冲信号,告知GPU和CPU可以交换了,这个就是Vsync信号,垂直同步信号。
不可否认,双缓冲可以在很大程度上降低screen tearing错误,但是呢,还是会出现一些其他问题。


Jank 掉帧


如果在Vsync到来时back buffer并没有准备好,就不会进行缓存的交换,屏幕显示的还是前一帧画面,即两个刷新周期显示的是同一帧数据,称为Jank掉帧。


image.png
发生jank的原因是:在第2帧CPU处理数据的时候太晚了,GPU没有及时将数据写入到buffer中,导致jank的发生。

CPU处理数据和GPU写入buffer的时机比较随意。


Project Butter 黄油工程


安卓4.1
系统在收到VSync信号之后,马上进行CPU的绘制以及GPU的buffer写入。最大限度的减少jank的发生。


image.png
如果显卡帧率大于屏幕刷新频率,也就是屏幕在刷新一帧的时间内,CPU和GPU可以充分利用刷新一帧的时间处理完数据并写入buffer中,那么这个方案是完美的,显示效果将很好。


image.png
由于主线程做了一些相对复杂耗时逻辑,导致CPU和GPU的处理时间超过屏幕刷新一帧的时间,由于此时back buffer写入的是B帧数据,在交换buffer前不能被覆盖,而frame buffer被Display用来做刷新用,所以在B帧写入back buffer完成到下一个VSync信号到来之前两个buffer都被占用了,CPU无法继续绘制,这段时间就会被空着,于是又出现了三缓存。


三buffer


image.png
最大程度避免CPU空闲的情况。


Choreographer


系统在收到VSync信号之后,会马上进行CPU的绘制以及GPU的buffer写入。在安卓系统中由Choreographer实现。



  • 在Choreographer的构造函数中会创建一个FrameDisplayEventReceiver类对象,这个对象实现了onVSync方法,用于VSync信号回调。

  • FrameDisplayEventReceiver这个对象的父类构造方法中会调用nativeInit方法将当前FrameDisplayEventReceiver对象传递给native层,native层返回一个地址mReceiverPtr给上层。

  • 主线程在scheduleVsync方法中调用nativeScheduleVsync,并传入2中返回的mReceiverPtr,这样就在native层就正式注册了一个FrameDisplayEventReceiver对象。

  • native层在GPU的驱使下会定时回调FrameDisplayEventReceiver的onVSync方法,从而实现了:在VSync信号到来时,立即执行doFrame方法。

  • doFrame方法中会执行输入事件,动画事件,layout/measure/draw流程并提交数据给GPU。

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

Android边框裁切的正确姿势

前言 今天写什么呢,没有太好的思路,就随便写一些细节的点吧。 平时我们都会接触到的一个东西就是设置view的边缘为圆角,因为默认的直角比较难看,这个是涉及比较多的场景,其它当然也有一些场景需用到非正常边框的情况,也需要裁切。 1. 设置圆角边框 一般我们怎么设...
继续阅读 »

前言


今天写什么呢,没有太好的思路,就随便写一些细节的点吧。

平时我们都会接触到的一个东西就是设置view的边缘为圆角,因为默认的直角比较难看,这个是涉及比较多的场景,其它当然也有一些场景需用到非正常边框的情况,也需要裁切。


1. 设置圆角边框


一般我们怎么设置圆角边框的

<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#ffffff" />
<stroke
android:width="0.8dp"
android:color="#ffffff" />

<corners android:radius="10dp" />
</shape>

这是我们比较常做的设置边框圆角的操作,有没有过这样去设置会不会出问题?其实这样的操作只不过是改变背景而已,它可能会出现内部内容穿透的效果。


2. 使用ClipToOutline进行裁切


这个是android 5.0之后提出的方法,具体的操作是这样

public static void setRoundRect(View view) {
try {
view.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), 10);
}
});
view.setClipToOutline(true);
} catch (Exception e) {
e.printStackTrace();
}
}

可以看出就是调用了view的setOutlineProvider方法和setClipToOutline方法。看这个ViewOutlineProvider,它的注释是

Interface by which a View builds its Outline, used for shadow casting and clipping.


能明显看出它就是为了处理阴影和裁切的。其中我们要设置的话,主要是设置Outline outline这个对象,我们可以看看它所提供的方法


setRect


先随便拿一张图片表示原本的显示效果来做对比


lQDPJxak951EiLzNArPNBBuwlGgcKIdKCsUD55urdoAVAA_1051_691.jpg_720x720q90g.jpg


调用setRect给原图进行边缘裁切

outline.setRect(view.getWidth()/4, view.getWidth()/4, view.getWidth()/4 *3, view.getHeight()/4 * 3);

得到这样的效果,注意,我的原效果是贴边的,这些裁切之后发现是不贴边的


lQDPJxbScR-Lx7zNAtzNBDiwSuSMvAe6JokD55uq3UAVAA_1080_732.jpg_720x720q90g.jpg


setRoundRect的效果和setRect一样,就是多了一个参数用来设置圆角。这里就不演示了


setOval
调用setOval,它的传参和setRect一样

outline.setOval(view.getWidth()/4, view.getWidth()/4, view.getWidth()/4 *3, view.getHeight()/4 * 3);

可以看到效果


lQDPJw5Lp5oLqLzNAqnNBDiwjzj9J14HHxID55ur-8AVAA_1080_681.jpg_720x720q90g.jpg


发现再裁切尺寸的同时并且把图片切成圆形,我记得很早之前,还没毕业时做圆形头像的时候还需要引用别人的第三方,现在5.0之后直接调这个就行,多方便。当然现在头像都是用Glide来做。


setAlpha和setConvexPath也一样,etAlpha是设置透明度,setConvexPath是设置路径,路径和自定义view一样用Path,我这边就不演示了


3.总结


Outline相对于shape来说,是真正的实现边缘裁切的,shape其实只是设置背景而已,它的view的范围还是那个正方形的范围。最明显的表现于,shape如果内容填满布局,会看到内容超出圆角,而Outline不会。当然如果你shape配合padding的话肯定也不会出现这种情况。


使用Outline也需要注意,一般的机子会在当范围超过圆之后,会一直显示圆。比如你设置radius为50是圆角的效果,但是甚至成100已经是整个边是半圆,这时你设200会发现还是半圆,但是在某些机子上200会变成圆锥,所以如果要做半圆的效果也需要去计算好radius


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

Android与JavaScript通信(相互回调)

简述      在移动应用开发中,Android和JavaScript是两个常用的技术栈。Android作为主流的移动操作系统,而JavaScript则是用于网页前端和跨平台开发的脚本语言。为了实现更好的用户体...
继续阅读 »

简述


     在移动应用开发中,Android和JavaScript是两个常用的技术栈。Android作为主流的移动操作系统,而JavaScript则是用于网页前端和跨平台开发的脚本语言。为了实现更好的用户体验和功能扩展,Android与JavaScript之间的通信变得至关重要。本文将介绍Android与JavaScript之间的回调通信技巧


通信基础




  1. 通过 WebView 进行通信


    Android 的 WebView 组件提供了 evaluateJavascript 方法,该方法可以执行 JavaScript 代码并获取返回结果。我们可以利用这一特性实现 Android 和 JavaScript 之间的通信。具体实现步骤如下:



    1. 在 Android 代码中,通过 evaluateJavascript 方法执行 JavaScript 代码。




  2. 使用 JavaScriptInterface 实现通信


    我们可以使用 JavascriptInterface 接口实现JavaScript 与 Android 的通信。具体实现步骤如下:



    1. 在 Android 代码中创建一个类,实现 JavascriptInterface 接口。

    2. 在该类中定义需要供 JavaScript 调用的方法,并添加 @JavascriptInterface 注解。

    3. 在 JavaScript 中通过 window.AndroidFunction 对象调用 Android 代码中的方法,实现通信,其中AndroidFunction是注册JavascriptInterface时指定的, 如下所示:
      webView.addJavascriptInterface(new Object() {
      @JavascriptInterface
      public void jsCallback(String message) {
      // ...
      }
      }, "AndroidFunction");





Android调用JavaScript函数




  1. 忽略返回值

        val webView = findViewById<WebView>(R.id.webView)

    webView.evaluateJavascript("jsFunction('message')", null)




  2. 获取返回值

        val webView = findViewById<WebView>(R.id.webView)

    webView.evaluateJavascript("jsFunction('message')") { result ->
    Log.e("TAG", result)
    }




JavaScript调用Android函数

// 在JavaScript中调用Android函数,并传递参数
function callAndroidFunctionWithParameter() {
var message = "Hello from JavaScript!";
AndroidFunction.jsCallback(message);
}

在上述示例中,JavaScript函数callAndroidFunctionWithParameter()将参数message传递给Android函数jsCallback()


双向回调通信




  1. Javascript传递回调给Android


    上述的 AndroidFunction.jsCallback(message)方式目前只能传递字符串,如果不做特殊处理,是无法执行回调函数的, 执行 AndroidFunction.jsCallback(message)时,在Android中获取到的是字符串,这时将回调函数转换成回调令牌,然后通过令牌执行相应的回调函数,步骤如下:




    1. 在js中将回调函数转换成唯一令牌,然后使用map以令牌为key存储回调函数,以便让Android端根据令牌来执行回调函数

      const recordCallMap = new Map<string, Function>();

      // Android端调用
      window.jsbridgeApiCallBack = function (callUUID: string, callbackParamsData: any) {
      // 通过ID获取对应的回调函数
      const fun = recordCallMap.get(callUUID);
      // 执行回调 `callbackParamsData`回调函数的参数 可有可无
      fun && fun(callbackParamsData);
      // 执行完毕后释放资源
      recordCallMap.delete(callUUID);

      }

      function getUuid() {
      // 生成唯一ID
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/\[xy]/g, function (c) {
      var r = (Math.random() \* 16) | 0,
      v = c == 'x' ? r : (r & 0x3) | 0x8;
      return v.toString(16);
      });
      }

      // 统一处理调用Android的方法
      export function androidCall(funname: string, funData: string, fun: Function) {

      if (!AndroidFunction) {
      return;
      }

      const dataObj = {
      funName: funname,
      funData: funData
      }

      if (typeof fun === "function") {
      const funId = getUuid();
      Object.assign(dataObj, { funId });
      recordCallMap.set(funId, fun);
      }

      AndroidFunction.jsCall(JSON.stringify(dataObj))

      }



    2. 在Android端注册JavascriptInterface统一让js调用

      class JsCallbackModel {
      lateinit var funData: String

      lateinit var funId: String

      lateinit var funName: String
      }

      abstract class JsFunctionCallBack(var funId: String) {
      abstract fun callback(respData: String?)
      abstract fun callback()
      abstract fun callback(respData: Boolean)
      }

      class JavaScriptCall(private val webView: WebView) {
      private fun jsCall(funName: String, funData: String, jsFunction: JsFunctionCallBack) {
      when(funName) {
      "screenCapture" -> {
      screenshot(funData.toInt(), jsFunction)
      }
      }
      }

      @JavascriptInterface
      fun jsCall(data: String) {
      // 将json字符串解析成kotlin对象
      val gson = GsonBuilder().create()
      val model = gson.fromJson(data, JsCallbackModel::class.java)

      // 如果不存在函数名称 则忽略
      if (model.funName == "") {
      return
      }

      val jsFunction: JsFunctionCallBack = object : JsFunctionCallBack(model.funId) {
      override fun callback(respData: String?) {
      if (webView == null) {
      return
      }
      if (funId.isEmpty()) {
      return
      }

      content.webView.post {
      webView.evaluateJavascript("jsBridgeApiCallBack('$funId', "$respData")", null)
      }
      }

      override fun callback() {
      if (funId.isEmpty()) {
      return
      }

      content.webView.post {
      webView.evaluateJavascript("jsBridgeApiCallBack('$funId')", null)
      }
      }

      override fun callback(respData: Boolean) {
      if (funId.isEmpty()) {
      return
      }

      content.webView.post {
      webView.evaluateJavascript("jsBridgeApiCallBack('$funId', '$respData')", null)
      }
      }
      }

      jsCall(model.funName, model.funData, jsFunction);
      }

      private fun screenshot(quality: Int, jsFunction: JsFunctionCallBack) {
      // 执行逻辑...
      // 执行js回调
      jsFunction("base64...")
      }
      }




    3. 在js中传递回调函数调用Android截图

      function screenshot(): Promise<string> {
      return new Promise(resolve => {
      androidCall("screenshot", (base64: string) => resolve(base64))
      })
      }
      screenshot().then(base64 => {
      })





  2. Android传递回调给Javascript


    原理跟Javascript传递回调给Android是一样的,具体实现如下:




    1. Android端

      class JavaScriptCall(private val webView: WebView) {
      companion object {
      val dataF: MutableMap<String, (callData: String) -> Unit> = mutableMapOf()

      fun jsCall(webView: WebView, funName: String, funData: String, funHandler: (callData: String) -> Unit) {
      val ctx: MutableMap<String, String> = mutableMapOf()

      ctx["funName"] = funName
      ctx["funData"] = funData
      val uuid = UUID.randomUUID().toString()
      ctx["funId"] = uuid
      dataF[uuid] = funHandler

      webView.post {
      val gson = GsonBuilder().create()
      val json = gson.toJson(ctx)
      webView.evaluateJavascript("jsCall('$json')", null)
      }
      }
      }

      @JavascriptInterface
      fun androidsBridgeApiCallBack(callUUID: String, callData: String) {
      val funHandler = dataF[callUUID];

      if (funHandler != null) {
      funHandler(callData)
      dataF.remove(callUUID)
      }
      }
      }



    2. Js端

      function doSome(funId, data, callback) {
      // 执行逻辑...
      // 执行Android的回调函数
      callback(funId, data)
      }

      function androidFunction(funId, respData: any?) {
      AndroidFunction.androidsBridgeApiCallBack(funId, respData)
      }

      function androidCallback(funId, funName, funData, fun) {
      switch (funName) {
      case 'doSome': {
      doSome(funId, funData, fun)
      }
      }
      }

      window.jsCall = function (json: string) {
      const obj = JSON.parse(json)
      const funName = obj['funName']
      const funData = obj['funData']
      const funId = obj['funId']

      if (!funName) {
      return
      }

      androidCallback(funId, funName, funData, androidFunction)
      }



    3. 在Android中传递回调函数调用js的doSome

      JavaScriptCall.jsCall(webView, "doSome", "data") { callParam ->
      Log.e("tAG", "回调参数: $callParam")
      }

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

Gradle 浅入浅出

环境介绍 OpenJDK 17.0.5 Gradle 7.6 示例代码 fly-gradle Gradle 项目下文件介绍 如果你的电脑安装了 gradle,可以使用 gradle init 去初始化一个新的 gradle 工程,然后使用电脑安装的 gra...
继续阅读 »

环境介绍



Gradle 项目下文件介绍


如果你的电脑安装了 gradle,可以使用 gradle init 去初始化一个新的 gradle 工程,然后使用电脑安装的 gradle 去执行构建命令。


但是每个开发电脑上的 gradle 版本不一样,为了统一构建环境,我们可以使用 gradle wrapper 限定项目依赖 gradle 的版本。

# 会生成 gradle/wrapper/* gradlew gradlew.bat
gradle wrapper --gradle-version 7.5.1 --distribution-type bin
.
├── build.gradle
├── gradle
│   └── wrapper
│   ├── gradle-wrapper.jar
│   └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle

gradlew 和 gradlew.bat


运行项目 wrapper 下定义的 gradle 去构建项目。


gradlew 是 macos 和 linux 系统下。


gradlew.bat 是 windows 系统下使用的。


wrapper


wrapper-workflow.png


wrapper 定义项目依赖那个版本的 gradle,如果本地 distributionPath 没有对应版本的 gradle,会自动下载对应版本的 gradle。

# gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
# 如果是国内项目,只需要修改这个url 就可以提高下载速度
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

GRADLE_USER_HOME 没有配置的话,默认是 ~/.gradle


zipStoreBasezipStorePath 定义了下载的 gradle (gradle-7.6-bin.zip) 存储的本地路径。


distributionBasedistributionPath 定义下载的 gradle 解压的本地目录。


gradlew 实际是运行 gradle-wrapper.jar 中的 main 方法,传递给 gradlew 的参数实际上也会传递给这个 main 方法。


gradle-wrapper.jar 会判断是否下载 wrapper 配置的 gradle,并且将传递参数给下载的 gradle,并运行下载的 gralde 进行构建项目。


升级 wrapper 定义的 gradle 版本

./gradlew wrapper --gradle-version 7.6

settings.gradle

pluginManagement {
repositories {
maven {
url="file://${rootDir}/maven/plugin"
}
gradlePluginPortal()
}
plugins {
// spring_boot_version 可以在 gradle.properties 配置
id 'org.springframework.boot' version "${spring_boot_version}"
}
}
rootProject.name = 'fly-gradle'
include 'user-manage-service','user-manage-sdk'
include 'lib-a'
include 'lib-b'

settings.gradle 主要用于配置项目名称,和包含哪些子项目。


也可以用于配置插件的依赖版本(不会应用到项目中去,除非项目应用这个插件)和插件下载的


build.gradle


build.gradle 是对某个项目的配置。配置 jar 依赖关系,定义或者引入 task 去完成项目构建。


gradle.properties


主要用于配置构建过程中用到的变量值。也可以配置一些 gradle 内置变量的值,用于修改默认构建行为。

org.gradle.logging.level=quiet
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xms512m -Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

org.gradle.jvmargs 用来配置 Daemon 的 JVM 参数,默认值是 -Xmx512m "-XX:MaxMetaspaceSize=384m"


当我们的项目比较大的时候,可能会由于 JVM 堆内存不足导致构建失败,就需要修改此配置。


org.gradle.logging.level 调整 gradle 的日志级别。参考 gradle logging 选择想要的日志级别。


Gradle Daemon


为加快项目构建,gralde 会启动一个常驻 JVM 后台进程去处理构建,Daemon 进程默认三小时过期且当内存压力大的时候也会关闭。


Gradle 默认会启用 Daemon 进程去构建项目。

# 查看 daemon 运行状态
./gradlew --status
# stop daemon 进程
./gradlew --stop
# 重启 daemon 进程
./gradlew --daemon

构建生命周期


Gradle 是基于 task 依赖关系来构建项目的,我们只需要定义 task 和 task 之间的依赖关系,Gradle 会保证 task 的执行顺序。


Gradle 在执行 task 之前会建立 Task Graphs,我们引入的插件和自己构建脚本会往这个 task graph 中添加 task。


task-dag-examples.png


Gradle 的构建过程分为三部分:初始化阶段、配置阶段和执行阶段。


初始化阶段



  • 找到 settings.gradle,执行其中代码

  • 确定有哪些项目需要构建,然后对每个项目创建 Project 对象,build.gradle 主要就是配置这个 Project 对象
// settings.gradle
rootProject.name = 'basic'
println '在初始化阶段执行'

配置阶段



  • 执行 build.gradle 中的配置代码,对 Project 进行配置

  • 执行 Task 中的配置段语句

  • 根据请求执行的 task,建立 task graph
println '在配置阶段执行 Task 中的配置段语句'

tasks.register('configured') {
println '在配置阶段执行 Task 中的配置段语句'
doFirst {
println '在执行阶段执行'
}
doLast {
println '在执行阶段执行'
}
}

执行阶段


根据 task graph 执行 task 代码。


依赖管理


Maven 私服配置


我们一般都是多项目构建,因此只需要在父项目 build.gradle 配置 repositories。

allprojects {
repositories {
maven {
url "${mavenPublicUrl}"
credentials {
username "${mavenUsername}"
password "${mavenPassword}"
}
}
mavenLocal()
mavenCentral()
}
}

credentials 配置账号密码,当私服不需要权限下载的时候可以不配置。


Gradle 会按照配置的仓库顺序查询依赖下载。


配置依赖来自某个目录

dependencies {
compile files('lib/hacked-vendor-module.jar')
}
dependencies {
compile fileTree('lib')
}

有的时候第三方库没有 maven 供我们使用,可以使用这个。


依赖冲突


默认依赖冲突


::: tip
当出现依赖冲突的时候,gradle 优先选择版本较高的,因为较高版本会兼容低版本。
:::

dependencies {
implementation 'com.google.guava:guava:31.1-jre'
implementation 'com.google.code.findbugs:jsr305:3.0.0'

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}

我们可以执行下面命令查看项目依赖的版本:

./gradlew dependency-management:dependencies --configuration compileClasspath

------------------------------------------------------------
Project ':dependency-management'
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- org.springframework.boot:spring-boot-dependencies:3.0.2
+--- com.google.guava:guava:31.1-jre
| +--- com.google.guava:failureaccess:1.0.1
| +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
| +--- com.google.code.findbugs:jsr305:3.0.2
| +--- org.checkerframework:checker-qual:3.12.0
| +--- com.google.errorprone:error_prone_annotations:2.11.0
| \--- com.google.j2objc:j2objc-annotations:1.3
\--- com.google.code.findbugs:jsr305:3.0.0 -> 3.0.2

我们可以看到,gradle 选择了 com.google.code.findbugs:jsr305:3.0.2 这个版本。


强制使用某个版本


如果我们想使用 com.google.code.findbugs:jsr305:3.0.0 版本

dependencies {
implementation 'com.google.guava:guava:31.1-jre'
implementation 'com.google.code.findbugs:jsr305:3.0.0', {
force = true
}
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
./gradlew -q dependency-management:dependencyInsight --dependency jsr305 --configuration compileClasspath
com.google.code.findbugs:jsr305:3.0.0 (forced)
Variant compile:
| Attribute Name | Provided | Requested |
|--------------------------------|----------|--------------|
| org.gradle.status | release | |
| org.gradle.category | library | library |
| org.gradle.libraryelements | jar | classes |
| org.gradle.usage | java-api | java-api |
| org.gradle.dependency.bundling | | external |
| org.gradle.jvm.environment | | standard-jvm |
| org.gradle.jvm.version | | 17 |

com.google.code.findbugs:jsr305:3.0.0
\--- compileClasspath

com.google.code.findbugs:jsr305:3.0.2 -> 3.0.0
\--- com.google.guava:guava:31.1-jre
\--- compileClasspath

禁用依赖传递


guava 不会传递依赖它依赖的库到当前库,可以看到

dependencies { 
implementation 'com.google.guava:guava:31.1-jre', {
transitive = false
}
implementation 'com.google.code.findbugs:jsr305:3.0.0'
}
./gradlew dependency-management:dependencies --configuration compileClasspath

------------------------------------------------------------
Project ':dependency-management'
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- org.springframework.boot:spring-boot-dependencies:3.0.2
+--- com.google.guava:guava:31.1-jre
\--- com.google.code.findbugs:jsr305:3.0.0

可以看到 guava 依赖的 jar 没有传递到当前项目中来。


排除某个依赖


Guava 依赖的别的 jar 可以传递进来,而且排除了 findbugs, 项目依赖的版本为 3.0.0

dependencies { 
implementation 'com.google.guava:guava:31.1-jre', {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
}
implementation 'com.google.code.findbugs:jsr305:3.0.0'
}
./gradlew dependency-management:dependencies --configuration compileClasspath

------------------------------------------------------------
Project ':dependency-management'
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- org.springframework.boot:spring-boot-dependencies:3.0.2
+--- com.google.guava:guava:31.1-jre
| +--- com.google.guava:failureaccess:1.0.1
| +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
| +--- org.checkerframework:checker-qual:3.12.0
| +--- com.google.errorprone:error_prone_annotations:2.11.0
| \--- com.google.j2objc:j2objc-annotations:1.3
\--- com.google.code.findbugs:jsr305:3.0.0

可以看到 guava 传递到当前项目的依赖少了 findbugs


配置依赖之间继承

configurations {
integrationTestImplementation.extendsFrom testImplementation
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}

configurations.all {
resolutionStrategy {
force 'org.apache.tomcat.embed:tomcat-embed-core:9.0.43'
}
exclude group: 'org.slf4j', module: 'slf4j-simple'
}

api 和 implementation 区别


jar b 包含一下依赖

dependencies {
api 'org.apache.commons:commons-lang3:3.12.0'
implementation 'com.google.guava:guava:31.1-jre'
}

项目 a 引入 jar b ,commons-lang3 和 guava 都可以被工程 a 使用,只是二者 scope 不一样。


api 对应 compile,在 工程 a 可以直接使用,编译可以通过。


implementation 对应 runtime,编译找不到 guava 中的类。


Task


我们引用的插件实际是添加 task 到 task graph 中去。


我们知道 build.gradle 实际是用来配置对应项目的 org.gradle.api.Project。


因此我们可以在 build.gradle 中引用 org.gradle.api.Project 暴露的属性。


我们可以在 gradle dsl 和 Project 接口中可以知道可以访问哪些属性。


tasks 实际就是 Project 暴露的一个属性,因此我们可以使用 tasks 往当前项目中注册 task。

tasks.register('hello') {
doLast {
println 'hello'
}
}

推荐使用 tasks.register 去注册 task,而不是 tasks.create 去直接创建。


tasks.register 注册的 task 只会在用到的时候才会初始化。


Groovy 闭包


Groovy Closures

{ [closureParameters -> ] statements }

闭包的示例

{ item++ }                                          

{ -> item++ }

{ println it }

{ it -> println it }

:::tip


当方法的最后一个参数是闭包时,可以将闭包放在方法调用之后。


:::


比如注册一个 task 的接口是

register(String var1, Action<? super Task> var2)

tasks.register("task55"){
doFirst {
println "task55"
}
}

tasks.register("task66",{
doFirst {
println "task66"
}
})

Task Type


gradle 已经定义好一些 task type,我们可以使用这些 task type 帮助我们完成特定的事情。比如我们想要执行某个 shell 命令。


Exec - Gradle DSL Version 7.6

tasks.register("task3", Exec) {
workingDir "$rootDir"
commandLine "ls"
}

Plugin


插件分类


Gradle 有两种类型的插件 binary plugins and script plugins


二进制插件就是封装好的构建逻辑打成 jar 发布到线上,供别的项目使用。


脚本插件就是一个 *.gradle 文件。


buildSrc


一般我们的项目是多项目构建,各个子项目会共享一些配置,比如 java 版本,repository 还有 jar publish 到哪里等等。


我们可以将这些统一配置分组抽象为单独的插件,子项目引用这个插件即可。便于维护,不用在各个子项目都重复配置相同的东西。


buildSrc 这个目录必须在根目录下,它会被 gradle 自动识别为一个 composite build,并将其编译之后放到项目构建脚本的 classpath 下。


buildSrc 也可以写插件,我们可以直接在子项目中使用插件 id 引入。

buildSrc/
├── build.gradle
├── settings.gradle
└── src
├── main
│   ├── groovy
│   │   └── mflyyou.hello2.gradle
│   ├── java
│   │   └── com
│   │   └── mflyyou
│   │   └── plugin
│   │   ├── BinaryRepositoryExtension.java
│   │   ├── BinaryRepositoryVersionPlugin.java
│   │   └── LatestArtifactVersion.java

buildSrc/build.gradle


groovy-gradle-plugin 对应的是使用 groovy 写插件。


java-gradle-plugin 对应 java

plugins {
id 'groovy-gradle-plugin'
id 'java-gradle-plugin'
}
gradlePlugin {
plugins {
helloPlugin {
id = 'com.mflyyou.hello'
implementationClass = 'com.mflyyou.plugin.BinaryRepositoryVersionPlugin'
}
}
}

我们就可以在子项目使用插件

plugins {
id 'com.mflyyou.hello'
id 'mflyyou.hello2'
}

插件使用



  • Applying plugins with the plugins DSL
plugins {
id 'java'
}


  • Applying a plugin with the buildscript block
buildscript {
repositories {
gradlePluginPortal()
}
dependencies {
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5'
}
}

apply plugin: 'com.jfrog.bintray'


  • Applying a script plugin
apply from: 'other.gradle'

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

[Git废弃提交]需求做一半,项目停了,我该怎么废弃commit

Git
在实际开发中我们要拥抱变化。我们都知道需求它是很不稳定的,在实际开发过程中会经常改变。经常会遇到一个需求,已经开始进入开发阶段了,开发到一半的时候说这个功能不需要了。甚至会出现我们已经实现某一个功能,然后被告知,这个功能被砍掉了。 那么针对这种代码已经写了,现...
继续阅读 »

在实际开发中我们要拥抱变化。我们都知道需求它是很不稳定的,在实际开发过程中会经常改变。经常会遇到一个需求,已经开始进入开发阶段了,开发到一半的时候说这个功能不需要了。甚至会出现我们已经实现某一个功能,然后被告知,这个功能被砍掉了。


那么针对这种代码已经写了,现在要废弃的情况我们应该怎么操作呢?


当然,如果这个功能都在一个单独的分支上,且这个分支只有这个功能的代码,那么可以直接废弃这个分支。(这也是为什么会有多种Git工作流的原因,不同的软件需求场景,需要配合不同的工作流。)


代码还没有提交


如果代码还在本地,并没有提交到仓库上。可以用reset来重置代码。
git reset是将当前的分支重设(reset)到指定的commit或者HEAD(默认),并且根据参数确定是否更新索引和工作目录。

# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
git reset [file]
# 重置暂存区与工作区,与上一次commit保持一致
git reset --hard
# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
git reset [commit]
# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
git reset --hard [commit]
# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
git reset --keep [commit]

其实reset也可以指定某个commit,这样就会重置到对应的commit,该commit之后的commit都会丢失。如果你没法确定这些commit中是否有需要保留的commit,就不要这样操作。


如果代码已经提交


如果代码已经提交了,且提交的分支上还有其他的代码。那么操作起来就比较麻烦。我们需要用revert来删除。


revert


git revert命令用来「撤销某个已经提交的快照(和reset重置到某个指定版本不一样)」。它是在提交记录最后面加上一个撤销了更改的新提交,而不是从项目历史中移除这个提交,这避免了Git丢失项目历史。

# 生成一个撤销最近的一次提交的新提交
git revert HEAD
# 生成一个撤销最近一次提交的上n次提交的新提交
git revert HEAD~num
# 生成一个撤销指定提交版本的新提交
git revert <commit_id>
# 生成一个撤销指定提交版本的新提交,执行时不打开默认编辑器,直接使用 Git 自动生成的提交信息
git revert <commit_id> --no-edit

比如我现在的dev分支,最近3次提交是"10","11","12"。



我现在要11这个提交去掉,我就直接revert "11" 这个commit



运行后,他会出现一个冲突对比,要求我修改完成后重新提交。(revert是添加一个撤销了更改的新提交)


这个提交,会出现"10"和"12"的对比。



修改完对比后,我们commit这次修改。



看下日志,我们可以看出,revert是新做了一个撤销代码的提交。



撤销(revert)被设计为撤销公共提交的安全方式,重设(reset)被设计为重设本地更改。
因为两个命令的目的不同,它们的实现也不一样:重设完全地移除了一堆更改,而撤销保留了原来的更改,用一个新的提交来实现撤销。「千万不要用 git reset 回退已经被推送到公共仓库上的提交,它只适用于回退本地修改(从未提交到公共仓库中)。如果你需要修复一个公共提交,最好使用 git revert」。


rebase


前面课程说过,rebase是对已有commit的重演,rebase命令也可以删除某个commit的。他和reset一样,删除掉的commit就彻底消失了,无法还原。
具体用法,这里不在再次介绍,可以去看前面的课程。


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

事件传递(android)常见面试题

一.事件分发传递流程 Android 事件分发传递流程主要分为三个阶段:事件分发阶段、事件捕获阶段和事件处理阶段。下面我将进一步解释这三个阶段的具体内容: 事件分发阶段 事件分发阶段是从 Activity 的 dispatchTouchEvent() 方法...
继续阅读 »

一.事件分发传递流程


Android 事件分发传递流程主要分为三个阶段:事件分发阶段、事件捕获阶段和事件处理阶段。下面我将进一步解释这三个阶段的具体内容:



  1. 事件分发阶段


事件分发阶段是从 Activity 的 dispatchTouchEvent() 方法开始,在这一阶段中,系统会先将事件传递给 ViewRootImpl,再由 ViewRootImpl 分发到 Activity 的 decorView,最后通过 View 的 onTouchEvent() 方法交给子 view。


具体流程如下:




  1. Activity 从 ViewRootImpl 中获取 MotionEvent 事件。




  2. ViewRootImpl 调用 ViewGroup 中的 dispatchTouchEvent() 方法,将事件分发给对应的子 View 进行处理。




  3. 子 View 依次递归处理事件,如果有子 View 拦截了事件,那么事件就不会再继续传递下去。




  4. 如果根据 onTouchEvent() 返回结果判断当前 View 处理了事件,那么事件就不会向子 View 再次传递。




  5. 如果事件都没有被拦截,那么这个事件将传递到 Activity 中的 onTouchEvent() 方法中。




  6. 事件捕获阶段




事件捕获阶段是指事件从根 View 开始,依次由父 View 捕获,直到当前 View,同时将事件自下向上传递给其祖先 View 的一种机制。主要是为了方便更改事件的属性,例如事件的坐标等。


具体流程如下:




  1. 系统会先将事件传递给该事件的目标 View,并将事件从根节点向下传递。




  2. 在向下子 View 传递事件时,父级 View 可以拦截事件并记录事件的坐标。




  3. 当事件传递到目标 View 的时候,事件的坐标信息会被用来进行对 View 执行操作的计算和判定。




  4. 如果当前 View 拦截了事件,那么此后的再次触摸事件都会被该 View 标记为 “拦截状态”,直到事件处理完毕,被放手传递给上级 View。




  5. 事件处理阶段




事件处理阶段是指最终处理 MotionEvent 的对象(除系统级别的)或者被设置成 Action.Cancel 的最后一个 View。在处理阶段中,会根据 View 的事件类型判断该事件是否可以被处理,如果可以则会继续进行处理,否则会将事件传递给父 View 或者其他 View 进行处理,直至事件被处理或者被放弃。


具体流程如下:




  1. 当一个 View 接收到一个 MotionEvent 事件时,它会首先检查自己的 onTouchEvent() 方法是否可以处理该事件。




  2. 如果当前 View 无法处理该事件,那么事件会被传递给上一级父 View 进行处理。




  3. 如果事件一直没有被处理,那么最终会将事件交给系统完成处理。




总的来说,Android 事件分发传递流程的机制是基于 View 树的节点结构,并且支持通过拦截事件、处理事件和返回事件等手段实现对事件的监测、干涉和响应。对于 Android 开发人员而言,深刻理解这个事件流程,有助于更完善地开发自己的 Android 应用程序。


二. Android常见的事件类型以及监听方式


Android 事件驱动机制是指通过一系列的事件监听器和回调函数来实现对用户交互操作的监听和相应的机制。Android 系统支持多种类型的事件监听器,包括触摸事件监听器、键盘事件监听器、手势事件监听器等,并且通过回调函数来实时监听用户的操作,从而实现对应用程序的控制和交互。下面我将分别介绍一下 Android 事件驱动机制中的各种监听器和回调函数:



  1. 触摸事件监听器


触摸事件监听器是 Android 事件驱动机制中的核心部分,主要用于对触摸事件的监听和处理。在 Android 中,触摸事件可以通过 MotionEvent 类来描述,主要包括三种基本的事件类型:ACTION_DOWN、ACTION_MOVE 和 ACTION_UP。可以通过实现 OnTouchListener 接口来监听触摸事件,并重写 onTouch() 方法来处理事件。例如:

view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 处理按下事件
break;
case MotionEvent.ACTION_MOVE:
// 处理移动事件
break;
case MotionEvent.ACTION_UP:
// 处理抬起事件
break;
}
return false;
}
});


  1. 键盘事件监听器


键盘事件监听器与触摸事件监听器类似,主要用于对键盘事件的监听和处理。在 Android 中,键盘事件可以通过 KeyEvent 类来描述,主要包括 ACTION_DOWN 和 ACTION_UP 两种基本的事件类型。可以通过实现 OnKeyListener 接口来监听键盘事件,并重写 onKey() 方法来处理事件。例如:

view.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
// 处理音量键加事件
break;
case KeyEvent.KEYCODE_VOLUME_DOWN:
// 处理音量键减事件
break;
}
}
return false;
}
});


  1. 手势事件监听器


手势事件监听器主要用于对手势事件的监听和处理。在 Android 中,手势事件需要使用 GestureDetector 类来进行监听,并实现 OnGestureListener 和 OnDoubleTapListener 接口来处理手势事件。例如:

// 在 Activity 中实例化 GestureDetector,并将它绑定到 View 上
GestureDetector gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
// 处理单击事件
return super.onSingleTapConfirmed(e);
}

@Override
public boolean onDoubleTap(MotionEvent e) {
// 处理双击事件
return super.onDoubleTap(e);
}
});

// 绑定 OnTouchListener 和 OnGestureListener
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
});

除了上述的三种事件监听器,Android 还提供了多种其他类型的事件监听器,例如文本变化监听器、列表滚动监听器、视图绘制监听器等,这些监听器通过实现相应的接口和重写对应的回调函数来实现对应用程序的控制和响应。


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

Android 中将多个子模块合并成一个 aar

1. 项目的结构 目标:将模块A打包成 aar,提供给其他工程师使用。 模块之间的关系:模块A引用模块B,模块B引用模块C。 2. 使用 fat-aar-android 三方库进行实现 fat-aar-android 中文使用文档 添加以下代码到工程...
继续阅读 »

1. 项目的结构




image.png


目标:将模块A打包成 aar,提供给其他工程师使用。


模块之间的关系:模块A引用模块B,模块B引用模块C。


2. 使用 fat-aar-android 三方库进行实现




  1. 添加以下代码到工程根目录的build.gradle文件中:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.github.kezong:fat-aar:1.3.8'
}
}


  1. 添加以下代码到主模块的build.gradle中(Module A):
apply plugin: 'com.kezong.fat-aar'


  1. 添加以下代码到主模块的build.gradle中(Module A):
embed project(path: ':ModuleB', configuration: 'default')
embed project(path: ':ModuleC', configuration: 'default')


Module B 引用 Module C,则需要在 Module B 的 build.gradle中进行引用


eg: implementation project(path: ':Module B')




  1. 执行 assemble 命令
# assemble all 
./gradlew :ModuleA:assemble

到这里的话就顺利完成了,遇到其它问题的话,我暂时不会。


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

228欢乐马事件,希望大家都能平安健

我这个人体质很奇怪,总能遇到一些奇怪的事。 比如六年前半夜打车回家,差点被出租车司机拉到深山老林。 比如两年前去福州出差,差点永远回不了家。 比如十点从实验室下班,被人半路拦住。 比如这次,被人冒充 (在我心里这事和前几件同样恶劣) 不过幸好每次都是化险为...
继续阅读 »

我这个人体质很奇怪,总能遇到一些奇怪的事。



  • 比如六年前半夜打车回家,差点被出租车司机拉到深山老林。

  • 比如两年前去福州出差,差点永远回不了家。

  • 比如十点从实验室下班,被人半路拦住。

  • 比如这次,被人冒充 (在我心里这事和前几件同样恶劣)


不过幸好每次都是化险为夷,所以我顺顺利利活到现在。




事情起因是这样的:


去年朋友B突然告诉我:发现了你的闲鱼账号。


:我没有闲鱼啊?


他给我截图,那个人卖掘金的周边,名字叫LolitaAnn


image.png


因为我遇到太多离谱的事,再加上看的一堆被冒充的新闻,所以我第一反应是:这人也在冒充我


当时朋友F 说我太敏感了,他觉得只是巧合。


但我觉得不是巧合,因为LolitaAnn是我自己造的词。




把我的沸点搬到小红书


又是某一天, 朋友H告诉我:你的小红书上热门了。


:?我没有小红书啊?


然后他们给我看,有个人的小红书完全照搬我的沸点。


为此我又下载小红书和他对线。起初正常交涉,但是让他删掉,他直接不回复我了,最后还是投诉他,被小红书官方删掉的。


image.png


现在想了想,ip一样,极有可能是一个人干的。




闲鱼再次被挖出来


今年,有人在掘金群里说我卖周边。


我跑到那个群解释,说我被人冒充了。


群友很热心,都去举报那个人的昵称。昵称被举报下去了。


但是几天之后:


image.png


看到有人提醒我,它名字又改回来了。


当时以为是热心群友,后来知道就是它冒充我,现在回想起来一阵恶寒。


把名字改回去之后还在群里跟炫耀一样,心里想着:我改回来了,你们不知道是我吧。




冒充我的人被揪出来了


2.28的时候, 朋友C突然给我发了一段聊天记录。


是它在群里说 它的咸鱼什么掘金周边都有。结果打开一看,闲鱼是我的名字和头像。


image.png


事情到今天终于知道是谁冒充我了


虽然Smile只是它的微信小号之一,都没实名认证。但是我还是知道了一些强哥的信息。


发现是谁冒充我,我第一反应必然是喷一顿。


刚在群里被我骂完,它脑子也不太好使,马上跑去改了自己掘金和闲鱼的名字。这不就是自爆了? 证明咸鱼和掘金都是他的号。


奔跑姐妹兄弟(原名一只小小黄鸭) ←点击链接即可鞭尸。


image.png




牵扯出一堆小号


本来我以为事情已经结束了,我就去群里吐槽他。结果一堆认识他的掘友们给我说它还有别的掘金号。因为它和不同掘友用不同掘金号认识的。所以掘友们给我提供了一堆。我就挨个搜。


直到我看到了这两条:


image.png


因为我搜欢乐马出来上万个同名账号, 再加上他说自己有脚本,当时我以为都是他的小号。


后来掘友们提醒我,欢乐马是微信默认生成的,所以有一堆欢乐马,不一定是他的小号。


但是我确信他有很多小号,比如:


image.png


比如咸鱼卖了六七个掘金鼠标垫,卖了掘金的国行switch……




  • 你们有没有想过为什么fixbug不许助攻了




  • 你们有没有想过为什么矿石贬值了,兑换商店越来越贵了?




  • 你们有没有想过为什么掘金活动必得奖励越来越少了?




有这种操作在,普通用户根本没办法玩吧。


所以最后,我就把这个事交给官方了。




处理结果


所幸官方很给力,都处理了,感谢各位官方大大。



本次事件,共涉及主要近期活跃UserID 4个,相关小号570个。 具体可以看
2023 年 2 月社区治理数据公布





我再叨叨几句




  • 卖周边可以, 你别冒充别人卖吧,又不是见不得光,做你自己不丢人。




  • 开小号可以,你别一开开一堆,逼得普通玩家拿不到福利吧。




  • 先成人后成才,不指望能为国家做多大贡献,起码别做蛀虫吧。




  • 又不是没生活,专注点自己的东西,别老偷别人沸点。




  • 我以后改名了嗷。叫Ann的八百万个,别碰瓷你爹了。






最后祝大家生活愉快,正常的掘友们身体健康,工作顺利。


不正常的也收敛点,别把自己搞进去。


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

2023年35大龄程序员最后的挣扎

一、自身情况 我非科班出身,年龄到了35岁。然后剧本都差不多,2022年12月各种裁员,失业像龙卷风一样席卷社会各个角落。 其实30岁的时候已经开始焦虑了,并且努力想找出路。 提升技术,努力争增加自己的能力。 努力争取进入管理层,可是卷无处不在,没有人离开这...
继续阅读 »

一、自身情况


我非科班出身,年龄到了35岁。然后剧本都差不多,2022年12月各种裁员,失业像龙卷风一样席卷社会各个角落。



  1. 其实30岁的时候已经开始焦虑了,并且努力想找出路。

  2. 提升技术,努力争增加自己的能力。

  3. 努力争取进入管理层,可是卷无处不在,没有人离开这个坑位,你努力的成效很低。

  4. 大环境我们普通人根本改变不了。

  5. 自己大龄性价比不高,中年危机就是客观情况。

  6. 无非就是在本赛道继续卷,还是换赛道卷的选择了。


啊Q精神:我还不是最惨的那一批,最惨的是19年借钱买了恒大的烂尾楼,并且在2021年就失业的那拨人。简直不敢想象,那真是绝望啊。心里不够坚强的,想不开轻生的念头都会有。我至少拿了点赔偿,手里还有些余粮,暂时饿不死。


image.png


二、大环境情况




  1. 大环境不好已经不是秘密了,整个经济走弱。大家不敢消费,对未来信心不足已经是板上钉钉的事了。




  2. 这剧本就是30年前日本的剧本,不敢说一摸一样。可以说大差不差了,互联网行业的薪资会慢慢的回归平均水平,或者技术要求在提升一个等级。




  3. 大部分普通人,还是做应用层拧螺丝,少部分框架师能造轮子也就是2:8理论。




  4. 能卷进这20%里,就能在上一层楼。也不是说这行就不行了,只不过变成了存量市场,而且坑位变少,人并没有变少还增加了。




  5. 不要怀疑自己的能力,这也不是你的问题了,是外部环境导致的市场萎缩。我们能做的就是,脱下孔乙己的长衫,先保证生活。努力干活,不违法乱纪做什么都是光荣了,不要带有色眼镜看待任何人。




三、未来出路


未来的出路在哪里?


这个我也很迷惑,因为大佬走的路,并不是我们这些普通的不能在普通的人能够走的通的。当然也有例外的情况,这些就是幸存者偏差了。


我先把chartGPT给的答应贴出来:


image.png


可以看到chartGPT还是给出,相对可行有效的方案。当然这些并不是每个人都适用。


我提几个普通人能做的建议(普通人还是围绕生存在做决策):



  1. 有存款的,并且了解一些行业的可以开店,比如餐饮店,花店,水果店等。

  2. 摆摊,国家也都改变政策了。

  3. 超市,配送员,外卖员。

  4. 开滴滴网约车。

  5. 有能力的,可以润出G。可以吸一吸GW“free的air”,反正都是要被ZBJ榨取的。


以上都是个人不成熟的观点,jym多多包涵。


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

flutter有哪些架构的框架?该怎么选择

flutter有哪些架构的框架? Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架: BLoC (Business Logic Co...
继续阅读 »

flutter有哪些架构的框架?


Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架:



  1. BLoC (Business Logic Component):BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。在Flutter中,可以使用flutter_bloc库来实现BLoC架构。
    Provider:Provider是Flutter中的一个轻量级状态管理库,它使用InheritedWidget实现状态共享,可以有效地解决Flutter应用中的状态管理问题。

  2. MobX:MobX是一种基于响应式编程的状态管理库,它使用可观察对象来管理应用程序的状态,并自动更新与之相关的UI组件。在Flutter中,可以使用mobx库来实现MobX架构。

  3. Redux:Redux是一种流行的状态管理模式,在Flutter中也有相应的实现库redux_flutter。Redux通过单一数据源管理应用程序的状态,并使用纯函数来处理状态的更新,可以有效地解决Flutter应用中的状态管理问题。
    以上是常用的Flutter架构框架,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。


除了上面提到的框架之外,还有以下几个Flutter架构框架:



  1. GetX:GetX是一种轻量级的Flutter架构框架,它提供了路由管理、状态管理和依赖注入等功能,可以大大简化Flutter应用的开发。

  2. MVC:MVC是一种经典的软件架构模式,它将应用程序分为模型、视图和控制器三个部分,可以有效地分离关注点,使得应用程序更易于维护和扩展。

  3. MVP:MVP是一种衍生自MVC的架构模式,它将应用程序分为模型、视图和Presenter三个部分,Presenter负责处理业务逻辑,将模型数据展示到视图上。

  4. MVVM:MVVM是一种流行的架构模式,它将应用程序分为模型、视图和视图模型三个部分,视图模型负责处理业务逻辑,将模型数据展示到视图上。


总之,Flutter中有很多架构框架可供选择,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。


Flutter BLoC


Flutter BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。BLoC这个缩写代表 Business Logic Component,即业务逻辑组件。
BLoC的核心思想是将UI层和业务逻辑层分离,通过Stream或者Sink等异步编程方式,将UI层和业务逻辑层连接起来。具体来说,BLoC模式包含以下三个部分:
Events:事件,即UI层的用户操作或其他触发条件,例如按钮点击,网络请求完成等等。
Bloc:业务逻辑层,用于处理Events,处理业务逻辑,并向UI层提供状态更新。
State:状态,即UI层的显示状态,例如页面的loading状态,数据请求成功或失败状态等等。


BLoC的核心是Bloc和State之间的联系,Bloc接收Events,并根据业务逻辑处理后,通过Stream向UI层提供状态更新。UI层监听Bloc的Stream,获取最新的State,并根据State更新UI状态。
在Flutter中,可以使用StreamController来创建BLoC。StreamController是一个异步数据流控制器,可以创建一个Stream用于事件流,创建一个Sink用于事件的注入。
Flutter框架提供了一个非常好的BLoC框架flutter_bloc,它封装了BLoC的核心逻辑,使得开发者可以更加方便地使用BLoC模式进行状态管理。使用flutter_bloc框架,只需要定义好Events、Bloc和State,然后将它们组合起来,就可以实现状态管理,从而将UI层和业务逻辑层分离。


总之,Flutter BLoC是一种状态管理模式,它通过将UI层和业务逻辑层分离,使用Stream或Sink等异步编程方式将它们连接起来,实现了应用程序的业务逻辑和UI分离。


如何选择使用


选择使用哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。以下是一些常见的应用场景和推荐的架构框架:
对于小型应用程序,可以考虑使用轻量级的状态管理库,如Provider或GetX。
对于需要处理复杂业务逻辑的应用程序,可以使用BLoC、MobX或Redux等状态管理框架。
对于需要实现高度可测试性的应用程序,可以考虑使用MVC、MVP或MVVM等经典的软件架构模式。
总之,选择哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。开发者应该根据自己的需求和技能水平选择最适合的架构框架,以提高开发效率和代码质量。


GetX和BLoC的优缺点


GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点,下面是它们的主要特点和优缺点的比较:


GetX


优点:


简单易用:GetX是一种轻量级的Flutter框架,提供了简单易用的依赖注入、路由管理和状态管理等功能,可以大大简化Flutter应用的开发。
性能优秀:GetX使用原生的Dart语言构建,不需要任何代码生成,因此运行速度非常快,同时也具有很好的内存管理和性能优化能力。
功能完备:GetX提供了路由管理、依赖注入、状态管理、国际化、主题管理等功能,可以满足大多数应用程序的需求。


缺点:


社区相对较小:相比其他流行的Flutter框架,GetX的社区相对较小,相关文档和教程相对较少,需要一定的自学能力。
不适合大型应用:由于GetX是一种轻量级框架,不适合处理大型应用程序的复杂业务逻辑和状态管理,需要使用其他更加强大的框架。


BLoC


优点:


灵活可扩展:BLoC提供了灵活的状态管理和业务逻辑处理能力,可以适应各种应用程序的需求,同时也具有良好的扩展性。
可测试性强:BLoC将UI和业务逻辑分离,提高了代码的可测试性,可以更容易地编写和运行测试代码。
社区活跃:BLoC是一种流行的Flutter框架,拥有较大的社区和用户群体,相关文档和教程比较丰富,容易入手。


缺点:


学习曲线较陡峭:BLoC是一种相对复杂的框架,需要一定的学习曲线和编程经验,初学者可能需要花费较多的时间和精力。
代码量较大:由于BLoC需要处理UI和业务逻辑的分离,因此需要编写更多的代码来实现相同的功能,可能会增加开发成本和维护难度。
总之,GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点。选择哪种框架取决于具体的应用程序需求和开发团队的技术水平。


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

现代化 Android 开发:基础架构 古哥E下

Android 开发经过 10 多年的发展,技术在不断更迭,软件复杂度也在不断提升。到目前为止,虽然核心需求越来越少,但是对开发速度的要求越来越高。高可用、流畅的 UI、完善的监控体系等都是现在的必备要求了。国内卷的方向又还包括了跨平台、动态化、模块化。 目前...
继续阅读 »

Android 开发经过 10 多年的发展,技术在不断更迭,软件复杂度也在不断提升。到目前为止,虽然核心需求越来越少,但是对开发速度的要求越来越高。高可用、流畅的 UI、完善的监控体系等都是现在的必备要求了。国内卷的方向又还包括了跨平台、动态化、模块化。


目前的整体感觉就是,移动开发基本是奄奄一息了。不过也不用过于悲观:一是依旧有很多存量的 App 堪称屎山,是需要有维护人员的,就跟现在很多人去卷 framework 层一样,千万行代码中找 bug。 二是 AI 日益成熟,那么应用层的创新也会出现,在没有更简洁的设备出现前,手机还是主要载体,总归是需要移动开发去接入的,如果硬件层越来越好,模型直接跑在手机上也不是不可能,所以对跨平台技术也会是新一层的考验,有可能直接去跨平台化了。毕竟去中台化也成了历史的选择。


因而,在这个存量市场,虽然竞争压力很大,但是如果技术过硬,还是能寻求一席之地的。因而我决定用几篇文章来介绍下,当前我认为的现代化 Android 开发是怎样的。其目录为:



  • 现代化 Android 开发:基础架构(本文)

  • 现代化 Android 开发:数据类

  • 现代化 Android 开发:逻辑层

  • 现代化 Android 开发:组件化与模块化的抉择

  • 现代化 Android 开发:多 Activity 多 Page 的 UI 架构

  • 现代化 Android 开发:Jetpack Compose 最佳实践

  • 现代化 Android 开发:性能监控


Scope


提到 Android 基础架构,大家可能首先想到的是 MVCMVPMVVMMVI 等分层架构。但针对现代化的 Android 开发,我们首要有的是 scope 的概念。其可以分两个方面:



  • 结构化并发之 CoroutineScope:目前协程基本已经是最推荐的并发工具了,CoroutineScope 的就是对并发任务的管理,例如 viewModelScope 启动的任务的生命周期就小于 viewModel 的存活周期。

  • 依赖注入之 KoinScope:虽然官方推荐的是 hilt,但其实它并没有 koin 好用与简洁,所以我还是推荐 koinKoinScope 是对实例对象的管理,如果 scope 结束, 那么 scope 管理的所有实例都被销毁。


一般应用总会有登录,所以大体的 scope 管理流程图是这样的:


scope



  • 我们启动 app, 创建 AppScope,对于 koin 而言就是用于存放单例,对于协程来说就是全局任务

  • 当我们登录后,创建 AuthSessionScope, 对于 koin 而言,就是存放用户相关的单例,对于协程而言就是用户执行相关的任务。当退出登录时,销毁当前的 AuthSessionScope,那么其对应的对象实例、任务全部都会被销毁。用户再次登录,就再次重新创建 AuthSessionScope。目前很多 App 对于用户域内的实例,基本上还是用单例来实现,退出登录时,没得办法,就只能杀死整个进程再重启, 所以会有黑屏现象,实现不算优雅。而用 scope 管理后,就是一件很自然而实现的事情了。所以尽量用依赖注入,而不要用单例模式

  • 当我们进入界面后,一般都是从逻辑层获取数据进行渲染,所以依赖注入没多大用了。而协程的 lifecycleScopeviewModelScope 就比较有用,管理界面相关的异步任务。


所以我们在做架构、做某些业务时,首要考虑 scope 的问题。我们可以把 CoroutineScope 也作为实例存放到 KoinScope 里,也可以把 KoinScope 作为 Context 存放到 CorutineScope 里。


岐黄小筑是将 CoroutineScope 放到 koin 里去以便依赖查找

val sessionCoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + coroutineLogExceptionHandler(TAG))
val sessionKoinScope = GlobalContext.get().createScope(...)
sessionKoinScope.declare(sessionCoroutineScope)


其实我们也完全可以用 CoroutineScopeContext 来做实例管理,而移除 koin 的使用。但是 Context 的使用并没有那么便捷,或许以后它可以进化为完全取代 koin



架构分层


随着软件复杂度的提升,MVCMVPMVVMMVI 等先后被提出,但我觉得目前所有的开发,都大体遵循某一模式而又不完全遵循,很容易因为业务的节奏,很容易打破,变成怎么方便怎么来。所以使用简单的分层 + 足够优秀的组件化,才是保证开发模式不被打破的最佳实践。下图是岐黄小筑的整体架构图:



整体架构不算复杂,其实重点是在于组件库,emo 已经有 20 个子库了,然后岐黄小筑有一些对于通用逻辑的抽象与封装,使得逻辑层虽然都集中在 logic 层,但整体都是写模板式的代码,可以面向 copy-paste 编程。


BookLogic 为例:


// 通过依赖注入传参, 拿到 db 层、网络层、以及用户态信息的应用
class BookLogic(
val authSession: AuthSession,
val kv: EmoKV,
val db: AccountDataBase,
private val bookApi: BookApi
) {
// 并发请求复用管理
private val concurrencyShare = ConcurrencyShare(successResultKeepTime = 10 * 1000L)

// 加载书籍信息,使用封装好的通用请求组件
fun logicBookInfo(bookId: Int, mode: Int = 0) = logic(
scope = authSession.coroutineScope, // 使用用户 session 协程 scope,因为有请求复用,所以退出界面,再进入,会复用之前的网络请求
mode = mode,
dbAction = { // 从 db 读取本地数据
db.bookDao().bookInfo(bookId)
},
syncAction = { // 从网络同步数据
concurrencyShare.joinPreviousOrRun("syncBookInfo-$bookId") {
bookApi.bookInfo(bookId).syncThen { _, data ->
db.runInTransaction {
db.userDao().insert(data.author)
db.bookDao().insert(data.info)
}
SyncRet.Full
}
}
}
)
// 类似的模板代码
suspend fun logicBookClassicContent(bookId: Int, mode: Int = 0) = logic(...)
suspend fun logicBookExpoundContent(bookId: Int, mode: Int = 0) = logic(...)
...
}

//将其注册到 `module` 中去,目前好像也可以通过注解的方式来做,不过我还没采用那种方式:
scopedOf(::BookLogic)

ViewModel 层浮层从 Logic 层读取数据,并可以进行特殊化处理:

class BookInfoViewModel(navBackStackEntry: NavBackStackEntry) : ViewModel() {
val bookId = navBackStackEntry.arguments?.getInt(SchemeConst.ARG_BOOK_ID) ?: throw RuntimeException("book_id is required!.")

val bookInfoFlow = MutableStateFlow(logicResultLoading<BookInfoPojo>())

init {
viewModelScope.launch {
runInBookLogic {
logicBookInfo(bookId, mode).collectLatest {
bookInfoFlow.emit(it)
}
}
}
}
}

Compose 界面再使用 ViewModel

@ComposeScheme(
action = SchemeConst.ACTION_BOOK_INFO,
alternativeHosts = [BookActivity::class]
)
@SchemeIntArg(name = SchemeConst.ARG_BOOK_ID)
@Composable
fun BookInfoPage(navBackStackEntry: NavBackStackEntry) {
LogicPage(navBackStackEntry = navBackStackEntry) {
val infoVm = schemeActivityViewModel<BookInfoViewModel>(navBackStackEntry)
val detailVm = schemeViewModel<BookDetailViewModel>(navBackStackEntry)
val bookInfo by infoVm.bookInfoFlow.collectAsStateWithLifecycle()
//...
}
}

这样整个数据流从网络加载、到存储到数据库、到传递给 UI 进行渲染的整个流程就结束了。


对于其中更多的细节,例如逻辑层具体是怎么封装的?UI 层具体是怎么使用多 ActivityPage?可以期待下之后的文章。


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

大专前端,三轮面试,终与阿里无缘

因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会 先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科 就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题...
继续阅读 »

因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会



先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科



就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题,基本上大厂简历都不会通过初筛,但还是抱着破罐子破摔的心态投递给了对方,出乎意料的是简历评估通过了,可能是因为有两个开源项目和一个协同文档加分吧。


进入到面试环节,首先是两道笔试题,算是前置面试:


第一道题目是算法题:


提供了一个数组结构的 data,要求实现一个 query 方法,返回一个新的数组,query 方法内部有 过滤排序分组 等操作,并且支持链式调用,调用最终的 execute 方法返回结果:

const result = query(list)
.where(item => item.age > 18)
.sortBy('id')
.groupBy('name')
.execute();

console.log(result);

具体实现这里就不贴了,过滤用原生的数组 filter 方法,排序用原生的数组 sort 方法,分组需要手写一下,类似 lodash/groupBy 方法。


过滤和排序实现都比较顺利,在实现分组方法的时候不是很顺利,有点忘记思路了,不过最后还是写出来了,关于链式调用,核心是只需要在每一步的操作最后返回 this 即可。


第二道题目是场景题:


要求用 vue 或者 react 实现一个倒计时抢券组件,页面加载时从 10s 开始倒计时,倒计时结束之后点击按钮请求接口进行抢券,同时更新文案等等功能。因为我对 react 比较熟悉一点,所以这里就选择了 react。


涉及到的知识点有 hook 中对 setTimeout 的封装、异步请求处理、状态更新CSS基本功 的考察等等……


具体实现这里也不贴了,写了一堆自定义 hook,因为平时也在参与 ahooks 的维护工作,ahooks 源码背的滚瓜烂熟,所以直接搬过来了,这道题整体感觉没啥难度,算是比较顺利的。


笔试题整个过程中唯一不顺利的是在线编辑器没有类似 vscode 这样的 自动补全 功能,不管是变量还是保留字,很多单词想不起来怎么拼写,就很尴尬,英文太差是硬伤 :(


笔试过程最后中出现了一点小插曲,因为笔试有时间限制,需要在规定的时间内完成,但是倒计时还没结束,不知道为什么就自动交卷了,不过那个时候已经写的差不多了,功能全部实现了,还剩下卡片的样式没完成,css 还需要完善一下,于是就在 Boss 上跟对方解释了一下,说明了情况。


过了几分钟,对面直接回复笔试过了,然后约了面试。


一面:




  • 自我介绍


    这里大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。




  • 七层网络模型、和 DNS 啥的


    计网这方面属于知识盲区了,听到这个问题两眼一黑,思索了一会儿,直接说回答不上来。




  • 然后问了一些 host 相关的东西



    • 很遗憾也没回答上来,尴尬。对方问我是不是计算机专业的,我坦诚的告诉对方是建筑工程。




  • React 代码层的优化可以说一下么?



    • 大概说了 class 组件和 function 组件两种情况,核心是通过减少渲染次数达到优化目的,具体的优化手段有 PureComponentshouldComponentUpdateReact.memoReact.useMemoReact.useCallbackReact.useRef 等等。




  • 说一下 useMemouseCallback 有什么区别



    • 很基础的问题,这里就不展开说了。




  • 说一下 useEffectuseLayoutEffect 有什么区别



    • 很基础的问题,这里就不展开说了。




  • 问了一下 useEffect 对应在 class 中都生命周期怎么写?



    • 很基础的问题,这里就不展开说了。




  • 如果在 if 里面写 useEffect 会有什么表现?



    • 开始没听清楚,误解对方的意思了,以为他说的是在 useEffect 里面写 if 语句,所以胡扯了一堆,后面对方纠正了一下,我才意识到对方在问什么,然后回答了在条件语句里面写 useEffect 控制台会出现报错,因为 hook 的规则就是不能在条件语句或者循环语句里面写,这点在 react 官方文档里面也有提到。




  • 说一下 React 的 Fiber 架构是什么




    • 这里说了一下 Fiber 本质上就是一个对象,是 React 16.8 出现的东西,主要有三层含义:




      1. 作为架构来说,在旧的架构中,Reconciler(协调器)采用递归的方式执行,无法中断,节点数据保存在递归的调用栈中,被称为 Stack Reconciler,stack 就是调用栈;在新的架构中,Reconciler(协调器)是基于 fiber 实现的,节点数据保存在 fiber 中,所以被称为 fiber Reconciler。




      2. 作为静态数据结构来说,每个 fiber 对应一个组件,保存了这个组件的类型对应的 dom 节点信息,这个时候,fiber 节点就是我们所说的虚拟 DOM。




      3. 作为动态工作单元来说,fiber 节点保存了该节点需要更新的状态,以及需要执行的副作用。




      (这里可以参考卡颂老师的《自顶向下学 React 源码》课程)






  • 前面提到,在 if 语句里面写 hook 会报错,你可以用 fiber 架构来解释一下吗?



    • 这里说了一下,因为 fiber 是一个对象,多个 fiber 之间是用链表连接起来的,有一个固定的顺序…… 其实后面还有一些没说完,然后对方听到这里直接打断了,告诉我 OK,这个问题直接过了。




  • 个人方面有什么规划吗?



    • 主要有两个方面,一个是计算机基础需要补补,前面也提到,我不是科班毕业的,计算机底层这方面比起其他人还是比较欠缺的,尤其是计网,另一方面就是英文水平有待提高,也会在将来持续学习。




  • 对未来的技术上有什么规划呢?



    • 主要从业务转型工程化,比如做一些工具链什么的,构建、打包、部署、监控几个大的方向,node 相关的,这些都是我感兴趣的方向,未来都可以去探索,当然了现在也慢慢的在做这些事情,这里顺便提了一嘴,antd 的 script 文件夹里面的文件是我迁移到 esm + ts 的,其中一些逻辑也有重构过,比如收集 css token、生成 contributors 列表、预发布前的一些检查等等…… 所以对 node 这块也有一些了解。




  • 能不能从技术的角度讲一下你工作中负责业务的复杂度?




    • 因为前两份工作中做的是传统的 B 端项目和 C 端项目,并没有什么可以深挖的技术难点,所以这里只说了第三份工作负责的项目,这是一个协同文档,既不算 B 端,也不算 C 端,这是一款企业级的多人协作数据平台,竞品有腾讯文档、飞书文档、语雀、WPS、维卡表格等等。


      协同文档在前端的难点主要有两个方面:




      1. 实时协同编辑的处理:当两个人同时进入一个单元格编辑内容,如果保证两个人看到的视图是同步的?那么这个时候就要提到冲突处理了,冲突处理的解决方案其实已经相对成熟,包括:




        • 编辑锁:当有人在编辑某个文档时,系统会将这个单元格锁定,避免其他人同时编辑,这种方法实现方式最简单,但也会直接影响用户体验。




        • diff-patch:基于 Git 等版本管理类似的思想,对内容进行差异对比、合并等操作,也可以像 Git 那样,在冲突出现时交给用户处理。




        • 最终一致性实现:包括 Operational Transformation(OT)、 Conflict-free replicated data type(CRDT,称为无冲突可复制数据类型)。






      2. 性能问题




        • 众所周知,互联网一线大厂的协同文档工具都是基于 canvas 实现,并且有一套自己的 canvas 渲染引擎,但是我们没有,毕竟团队规模没法跟大厂比,这个项目前端就 2 个人,所以只能用 dom 堆起来(另一个同事已经跑路,现在就剩下我一个人了)。这导致页面卡顿问题非常严重,即使做了虚拟滚动,但是也没有达到很好的优化效果。老板的要求是做到十万量级的数据,但是实际上几千行就非常卡了,根本原因是数据量太大(相当于一张很大的 Excel 表格,里面的每一个单元格都是一个富文本编辑器),渲染任务多,导致内存开销太大。目前没有很好的解决方案,如果需要彻底解决性能问题,那么就需要考虑用 canvas 重写,但是这个基本上不太现实。




        • 因为卡顿的问题,暴露出来另一个问题,状态更新时,视图同步缓慢,所以这时候不得不提到另一个优化策略:乐观更新。乐观更新的思想是,当用户进行交互的时候,先更新视图,然后再向服务端发送请求,如果请求成功,那么什么都不用管,如果请求失败,那么就回滚视图。这样做的好处是,用户体验会好很多,在一些强交互的场景,不会阻塞用户操作,比如抖音的点赞就是这样做的。但是也会带来一些问题,比如:如果用户在编辑某个单元格时,另一个用户也在编辑这个单元格,那么就会出现冲突,这个时候就需要用到前面提到的冲突处理方案了。










  • 可以讲一下你在工作中技术上的建设吗?



    • 这里讲了一下对 hooks 仓库的建设,封装了 100 多个功能 hook业务 hook,把不变的部分隐藏起来,把变化的部分暴露出去,在业务中无脑传参即可,让业务开发更加简单,同时也提高了代码的复用性。然后讲了一下数据流重构之类的 balabala……




  • 你有什么想问我的吗?



    • 问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。





结束之后不到 20 分钟,对方就在 Boss 上回复我说面试过了,然后约了二面。



二面:




  • 自我介绍



    • 跟上一轮一样,大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。




  • 在 js 中原型链是一个很重要的概念,你能介绍一下它吗?



    • 要介绍原型链,首先要介绍一下原型,原型是什么…… 这块是纯八股,懒得打字了,直接省略吧。




  • object 的原型指向谁?



    • 回答了 null。(我也不知道对不对,瞎说的)




  • 能说一下原型链的查找过程吗?



    • 磕磕绊绊背了一段八股文,这里省略吧。




  • node 的内存管理跟垃圾回收机制有了解过吗?




    • 暗暗窃喜,这个问题问到点子上了,因为两年前被问到过,所以当时专门写了一篇文章,虽然已经过去两年了,但还是背的滚瓜烂熟:




    • 首先分两种情况:V8 将内存分成 新生代空间老生代空间




      • 新生代空间: 用于存活较短的对象




        • 又分成两个空间: from 空间 与 to 空间




        • Scavenge GC 算法: 当 from 空间被占满时,启动 GC 算法



          • 存活的对象从 from space 转移到 to space

          • 清空 from space

          • from space 与 to space 互换

          • 完成一次新生代 GC






      • 老生代空间: 用于存活时间较长的对象




        • 新生代空间 转移到 老生代空间 的条件(这个过程称为对象晋升



          • 经历过一次以上 Scavenge GC 的对象

          • 当 to space 体积超过 25%




        • 标记清除算法:标记存活的对象,未被标记的则被释放



          • 增量标记:小模块标记,在代码执行间隙执,GC 会影响性能

          • 并发标记:不阻塞 js 执行










  • js 中的基础类型和对象类型有什么不一样?



    • 基础类型存储在栈中,对象类型存储在堆中。




  • 看你简历上是用 React,你能简单的介绍一下 hooks 吗?



    • 本质上就是一个纯函数,大概介绍了一下 hooks 的优点,以及 hooks 的使用规则等等。




  • 简单说一下 useEffect 的用法:



    • useEffect 可以代替 class 中的一些生命周期,讲了一下大概用法,然后讲了一下 useEffect 的执行时机,以及 deps 的作用。




  • 说一下 useEffect 的返回值用来做什么?



    • 返回一个函数,用来做清除副作用的工作,比如:清除定时器清除事件监听等等。




  • 你知道 useEffect 第二个参数内部是怎么比较的吗?



    • 说了一下内部是浅比较,源码中用 for 循环配合 Object.is 实现。(感觉这个问题就是在考察有没有读过 React 源码)




  • 前端的话可能跟网络打交道比较多,网络你了解多少呢?



    • 这里直接坦诚的说了一下,网络是我的弱项,前面一面也问到了网络七层模型,没回答出来。




  • 那你回去了解过七层模型吗?我现在再问你一遍,你能回答出来吗?



    • 磕磕绊绊回答出来了。




  • 追问:http 是在哪一层实现的?



    • 应用层。




  • 说一下 getpost 有什么区别?



    • 两眼一黑,脑子一片空白,突然不知道说什么了,挤了半天挤出来一句:get 大多数情况下用来查询,post 大多数情况下用来提交数据。get 的入参拼在 url 上,post 请求的入参在 body 里面。面试官问我还有其它吗?我说想不起来了……




  • 说一下浏览器输入 url 到页面加载的过程:




    • 输入网址发生以下步骤:



      1. 通过 DNS 解析域名的实际 IP 地址

      2. 检查浏览器是否有缓存,命中则直接取本地磁盘的 html,如果没有命中强缓存,则会向服务器发起请求(先进行下一步的 TCP 连接)

      3. 强缓存协商缓存都没有命中,则返回请求结果

      4. 然后与 WEB 服务器通过三次握手建立 TCP 连接。期间会判断一下,若协议是 https 则会做加密,如果不是,则会跳过这一步

      5. 加密完成之后,浏览器发送请求获取页面 html,服务器响应 html,这里的服务器可能是 server、也可能是 cdn

      6. 接下来是浏览器解析 HTML,开始渲染页面




    • 顺便说了渲染页面的过程:



      1. 浏览器会将 HTML 解析成一个 DOM 树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

      2. 将 CSS 解析成 CSS Rule Tree(css 规则树)。

      3. 解析完成后,浏览器引擎会根据 DOM 树CSS 规则树来构造 Render Tree。(注意:Render Tree 渲染树并不等同于 DOM 树,因为一些像 Headerdisplay:none 的东西就没必要放在渲染树中了。)

      4. 有了 Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的 CSS 定义以及他们的从属关系。下一步进行 layout,进入布局处理阶段,即计算出每个节点在屏幕中的位置。

      5. 最后一个步骤就是绘制,即遍历 RenderTree,层绘制每个节点。根据计算好的信息绘制整个页面。




    • 渲染完成之后,开始执行其它任务:



      1. dom 操作

      2. ajax 发起的 http 网络请求等等……

      3. 浏览器处理事件循环等异步逻辑等等……






  • 菜单左中右布局,两边定宽,中间自适应,说一下有几种实现方式



    • 比较经典的面试题,说了 flexfloat 两种方式。




  • 项目难点



    • 和一面一样,说了协同文档的两大难点,这里就不重复了。




  • 你有什么想问我的吗?



    • 和一面一样,问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。




  • 最后问了期望薪资什么的,然后就结束了。




二面结束之后,大概过了几个小时,在 Boss 上跟对方说了一声,如果没过的话也麻烦跟我说一下,然后这时候,对方在 Boss 上问我,第一学历是不是专科?我说是的,感觉到不太妙的样子,


然后又过了一会儿,对方说定级应该不会高,他后续看一下面试官的反馈如何……


然后又追问我,换工作的核心诉求是涨薪还是能力的提升,这里我回答的比较委婉,其实两个都想要 QAQ

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

TypeScript中的枚举,点燃你的代码创意!

前言 枚举是 TypeScript 中一个非常有趣且实用的特性,它可以让我们更好地组织和管理代码。 什么是枚举? 在 TypeScript 中,枚举(Enum)是一种用于定义命名常量集合的数据类型。它允许我们为一组相关的值赋予一个友好的名字,从而使代码更加可读...
继续阅读 »

前言


枚举是 TypeScript 中一个非常有趣且实用的特性,它可以让我们更好地组织和管理代码。


什么是枚举?


在 TypeScript 中,枚举(Enum)是一种用于定义命名常量集合的数据类型。它允许我们为一组相关的值赋予一个友好的名字,从而使代码更加可读和易于理解。枚举可以帮助我们避免使用魔法数值,提高代码的可维护性和可读性。


枚举的基本用法


让我们从枚举的基本用法开始,以一个简单的例子来说明。

enum Direction {
Up,
Down,
Left,
Right,
}

在这个例子中,我们定义了一个名为 Direction 的枚举,它包含了四个值:Up、Down、Left 和 Right。这些值默认情况下是从0开始自增的索引值。


我们可以使用枚举中的值来进行变量的赋值和比较。

let myDirection: Direction = Direction.Up;

if (myDirection === Direction.Up) {
console.log("向上");
} else if (myDirection === Direction.Down) {
console.log("向下");
}

在这个例子中,我们声明了一个名为 myDirection 的变量,并将其赋值为 Direction.Up。然后,我们使用 if 语句对 myDirection 进行比较,并输出相应的信息。


枚举的进阶用法


除了基本的用法外,枚举还有一些进阶的用法,让我们一起来看看。


1. 指定枚举成员的值


我们可以手动为枚举成员指定具体的值,而不是默认的自增索引值。

enum Direction {
Up = 1,
Down = 2,
Left = 3,
Right = 4,
}

在这个例子中,我们手动指定了每个枚举成员的值。这样,Up 的值为1,Down 的值为2,依此类推。


2. 使用枚举成员的名称


我们可以使用枚举成员的名称来访问其值。

enum Direction {
Up,
Down,
Left,
Right,
}

console.log(Direction.Up); // 输出

: 0
console.log(Direction[0]); // 输出: "Up"

在这个例子中,我们分别通过成员的名称和索引值来访问枚举成员的值。


3. 枚举的反向映射


枚举还具有反向映射的特性,可以通过值找到对应的名称。

enum Direction {
Up,
Down,
Left,
Right,
}

console.log(Direction.Up); // 输出: 0
console.log(Direction[0]); // 输出: "Up"

在这个例子中,我们通过 Direction.Up 输出了 0,通过 Direction[0] 输出了 "Up"。这种反向映射可以在某些场景下非常有用。


总结


枚举是一种用于定义命名常量集合的数据类型,可以帮助我们更好地组织和管理代码。我们了解了枚举的基本用法,以及一些进阶的技巧,如指定枚举成员的值、使用枚举成员的名称和枚举的反向映射。


希望能够帮助到大家更好地掌握 TypeScript 中的枚举,并在实际开发中灵活运用。


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

年后被吊打的第一面

背景 base重庆,面试中高级,目标先检验一下自己的水平和能力顺便看看薪资,好直接开始把。 自我介绍 讲了一下自己的技术栈:掌握vue全家桶,底层及上层框架、掌握react底层原理、熟悉js、熟悉工程化、熟悉微信小程序、使用过node、关注前端趋势有开源经历、...
继续阅读 »

背景


base重庆,面试中高级,目标先检验一下自己的水平和能力顺便看看薪资,好直接开始把。


自我介绍


讲了一下自己的技术栈:掌握vue全家桶,底层及上层框架、掌握react底层原理、熟悉js、熟悉工程化、熟悉微信小程序、使用过node、关注前端趋势有开源经历、主方向工程化等。


大概说了1分钟把,可能是我一边自我介绍一边在笑,面试官就问了一下:“你看起来心态很好啊!你是不是要写面经啊?”


我:“我是紧张才笑,应该是可以吗?要不多问点把~”


面试官:“可以,行”


记住这句话,我现在很后悔


步入正题


面试官:"那我们先来点基础把",下面都是我原话,小伙伴们可以纠正一下。


JS浏览器基础篇


1、dom树是怎么生成的


"浏览器是多进程架构,而其中有一个渲染进程,负责页面的渲染和js脚本的执行,而在渲染进程中有一个HTML解析器,oh对还有一个网络进程,网络进程负责根据content-type创建渲染进程,然后渲染进程用类似stream流管道那种接字节流将它解析为dom"


“而解析时,我觉得可以对标现在的各种转移编译工具,都有一个词法分析、语法分析、transfrom、genoretor的流程”


“你能具体说说这个过程吗?”


“(心理活动:当时脑子就蒙了、有点超纲啊、我要不猜一下)稍等我想一下、跟babel应该很像吧、会对一些声明命令赋值注释分词,这块我不是很了解,但应该对于html分词就是分的标签和文本内容,然后再通过算法去转换成dom”


“行,你上面提到了分析器,那如果当分析器遇到了script标签那”


“(心理活动:......这八股文味不对劲啊),我不知道对不对,但表现的是当遇到了scrpit会暂停html转换dom,去解析jascript,而async和defer会异步加载不会阻塞html转换”。


2、渲染进程


还可以你可以自信点虽然有些地方不是很对,但已经够用了。刚听你说了渲染进程,你说说它下面的几个线程把。


“emmm,下面的主线程吗,有主线程、GUI渲染线程、事件触发线程、定时器触发线程(后面发现漏了一个http线程),em,主线程和GUI是互斥的、js执行太长会造成页面渲染卡顿,但现在有很多解决方案,比如:在react中的调度器预留的5ms空闲时间、web worker之类的。然后是事件触发线程和定时器线程都是在任务队列去做Loop”


“行,那事件循坏我就不问你了,问问你V8的垃圾回收把”


“(你问啊!你问啊!)”


3、v8垃圾回收


“首先js因为是单线程,垃圾回收会占用主线程,导致页面卡顿,所以需要一个算法或者说策略,而v8采用的是分代式回收,而垃圾回收在堆中分成了很多部分用作不同的作用(我在说什么啊!当时),回收主要表现在新老生代上,新生代就活得短一点的对象,老生代就活得长一点的对象。


“在新生代里有一个算法,将新生代分成了两个区,一个FORM,一个TO,每次经过Scavenge会将FORM区中的没引用的销毁,然后活着的TO区调换位置,反复如此,当经过一次acavange后就会晋升的老生代还有个条件就是TO区的内存超过多少了也会晋升。”


“而老生代,采用标记清除和标记整理,但标记清除会造成内存不连续,所以会有标记整理取解决掉内存碎片,就是清理掉边界碎片”


“为什么TO超过25%要晋升老生代?标记清除是怎么清除的?”


“不知道~”


“第一个问题是为了不影响后续FORM空间的分配,第二个问题你应该看过有关这方面的文章把,垃圾回收会构建一个根列表,从根节点去访问那些变量,可访问到位活动,不可就是垃圾”


4、浏览器缓存


就强制缓存,协商缓存,浏览器内存那些,有兴趣看看文章,讲细点就行。写着太累了,当时讲了一大滩,直接说面试官问题把。


“因为提到了这些缓存,你觉得他们对于我们实际的业务场景下怎么运用”


“(蒙蔽),很大一部分是浏览器优化,一些http缓存我们可以做一些控制,本质上我感觉这些都属于性能优化的部分。”


“行”


5、JS上下文执行栈和闭包


“几个概念把,esc、上下文:作用域链,AO/VO,this。esc存储执行的上下文”


“算了我以一个函数来说把,主要是创建和执行。假设有一个A函数,过程是这样的创建全局执行上下文、压入esc、全局上下文初始化、执行A函数、创建A函数执行上下文,压入esc,A函数上下文初始化,这个初始化过程是这样的:创建作用域链、emm我上面提漏了一个A函数被创建全局上下文被保存到scope中的过程,是复制scpoe创建作用域链,用arguments创建活动对象,初始化活动对于,将活动对象压入链顶,执行完毕,上下文弹出。”


“但是全局上下文一直在栈底,而VO和AO的确认,我感觉是取决是是否可访问的。”


“而闭包就是上下文链中上下文scope被销毁了,但因为保持了对scope中某个变量的引用,这应该就是你上面说的回收原理的根节点实现的这个东西把,导致没销毁干净,留存在了内存中,完成了闭包”


“你怎么看待闭包的副作用”


“emmm,其实我觉得闭包是语言特性,虽然有副作用但我觉得其实挺好的,但只要管理好它就好了。况且又不是只有闭包会造成这些问题,就比如:react里面还有专门去清理一些链表和难回收的东西,去帮助v8回收。我觉得这得取决于写代码的人。”


“可以的,我感觉你的基础还是挺好的,你说下es6的东西把,控制下时间”


“你想听哪方面的那?因为东西太多了”


“工程化把,因为我前面听你介绍主方向是工程化”


“(...我怎么感觉工程化相关的只有一个esm模块化啊,这个怎么分类啊)esm:异步加载、引入抛出,编译时,值的引用。大概就这些东西把,其他的不知道了”


“行”


“那您觉得还有哪些那”


“就比如:Promise和async之类的啊”


“(..................)”


“来手写几道题把”


6、bind pipe compose 深拷贝


这个网上太多了,大伙自己去看。


CSS基础篇


“我可能CSS有点烂,但基础的应该都知道”


“先说说BFC把”


1、BFC


“BFC给我的感觉就像是个封闭盒子,不会在布局上影响到外面的元素。平常会触发BFC比较多的就是body,浮动元素、绝对定位、flex、overflow之类的。在BFC可以清除浮动、阻止元素被浮动环绕之类的。(然后我一边说一边尴尬的笑)”


“大概知道你CSS是个啥水平了,简单问点把,你说BFC可以清除浮动吗?为什么?”


“不知道”


“其实准确的说不是清除,是因为浮动元素也是BFC,两个BFC互不影响。你提到了BODY触发BFC?”


“emmm,可能是也许不是BODY,是HTML根元素”


“是的,不是BODY”


2、居中


"flex布局,positon,flex margin:auto,position transform,table-cell"


"行了,层叠上下文和层叠顺序"


3、层叠上下文


“(.......这时候感觉每多问我一个CSS都是煎熬啊),em我其实一般对于会遇到有层叠上下文不清晰的情况都是指定z-index.”


“行”


4、flex布局


“这样吧,你平时用得比较多的是什么布局?”


“flex布局把”


“那我们来聊一下flex布局把”


“(拜托我真的会哭,我感觉面试官他好兴奋),emmmm,我觉得布局无非是控制内部元素和控制容器,而flex布局对于容器的控制是基于轴这个概念的,而flex中的轴分为:主轴、垂直轴、换行轴。”


“主轴指的就是元素排列的方向轴,而flex-direction是最重要的属性在主轴中,row和col控制角度,reverse控制方向,但我们其实平时用得比较多的就默认的row和column,column会把元素垂直去排列。而主轴的另一个属性justify-content是控制元素在轴上的排列,然后我说了一下常用的几种就start end center between around”


“垂直轴就是垂直于主轴的方向轴~然后我停了大概有20秒(又开始笑了)”


“没了?”


“(啊啊啊啊啊!)可能就我只知道align-items控制垂直轴上的位置,然后说了下,start end center。”


“还有换行轴那”


“嗷对,就是刚才提漏了,垂直轴是针对于当前行,但换行轴是针对于整个容器。”


“这个针对怎么说?你继续说换行轴属性。”


就是高度嘛,布局换行后,垂直轴的高度只会是当前行高度。flex-wrap,但我只用过wrap,emm对于控制内部容器我了解得很粗浅。


"你可以了解一下wrap-reverse,下来可以去看一下正负剩余空间和flex-grow flex-shrink这些"


“抢答!就是flex:1这种写法的哈”


“对的,你知道吗”


“不知道”


我们两个同时沉默了(啊!!!!!!!!)。


"没事其实比我想象得稍微好一点,至少你在你不擅长的东西上也是花了时间去学的,css就不问了,下面问点框架把"


“谢谢!谢谢!谢谢!”


框架基础篇


“你简历里是React和Vue都会,那先说说你是怎么看这两个框架的把”


1、对React和Vue的看法


“在开发模式上,React发明了JSX这种开发模式,并且用了很多年时间让社区去接受,而Vue则是采用现成的模版语法的开发模式。但感觉就这两年这两个框架都在往一个函数式组件的方向靠,不应该说靠是已经实现了,Vue3中的函数式组件甚至在某种层面上说比react更自由。当然洛现在声明式的编程是主流嘛”


“在实现层面上说的话,就那一套,单向数据流,双向绑定,数据不可变性,更智能的依赖收集,优化的方式不同等”


“听到你说了更自由和智能的依赖收集,具体指的是?”


"比如react useEffect要手动加依赖,但vue3的wachEffect和computed就不用"。


“自由,em就比如,hook得顶层,不要条件循坏用,而且react重心智负担:就闭包陷阱那一套过时变量嘛,依赖的选择,还有重复渲染这些问题,我一直不理解为什么不把这些心智负担收到框架里,我觉得react是有这个能力的。vue3的话,你想咋用咋样api咋样,setup也会只用一次,也不会像新旧树对比hook多次调用”


“哈哈感觉你对react怨气好大,因为我看你文章写了很多react源码的嘛,如果是你你会怎么去收敛这个心智负担到框架内部?”


(.......绷不住了啊,吹过头了),稍等想1分钟,就是hook顶层那个和条件循坏应该不好动,因为react不能去破坏hooks链表的结构,对于过时变量react18已经将usestate全改为异步了,依赖的选择的心智问题我觉得是否说可以更明确一点再未来加入配置项的话,将依赖的收集在updateEffect和mountedEffect前去提前收集,就做一个依赖系统专门去处理这个事情,感觉可以从编译器自动生成依赖数组,现在react只是一层浅比较。但其实这么想,大部分问题的根源,是React函数组件机制所限:每次组件渲染,组件里的所有代码都会被重新调用一次,在这上面其实也可以动下手(自己说了感觉当没说,感觉好尴尬啊硬吹)。就长话短说就是,react的心智模型让我要花很多精力去处理边界情况。


“其实你最后句话说得挺好的,因为react要求你用声明式的方式去写组件,你就不该去纠结引用会不会变,多添加个依赖很不舒服,重新渲染这种事情,你需要保证的是无论重新渲染多少次这个组件都不会变。假设你useEffect依赖于AB,但你的B就可能只在首次创建后永远不变,它确实显得很“多余”但你不用纠结这个。真正的问题可能就在于你觉得的永远不会变只是你觉得,我们平时出现问题很多都是这种以为的边界问题导致B变造成的”


2、为什么react需要合成事件


“兼容性的考虑把,可以抹平不同浏览器事件对象差异,还有个就是避免了垃圾回收。”


“我们公司主要是Vue,你的简历里Vue也更擅长一些,我们谈一下Vue把”


3、生命周期


随便了一下,每个生命周期,父子生命周期,每个生命周期的定义和写法。


4、路由


5、指令


6、响应式原理


7、数组处理


8、key,diff算法


9、V3组合式API


10、一些TS系统


11、V3编译原理


“上面也问了挺多的了,你讲讲Vue3里面的模版编译和函数式组件编译把。”


“(我崩不住了,妈妈我想回家)先巴拉巴拉扯了一下pnpm和menorepo和整体v3源码,然后讲到complier,同样vue模版的编译也是通俗流程就是parse transform gen,我没具体研究过,但parse应该也是对标签属性文本指令等一系列做处理去干成AST,然后transform做转换最后生成。”


“行上午先面到这,二面通知你,有什么要问的吗?”


“em,你觉得我咋样(我好直白~)”


“挺不错的,2年经验,感觉你知识的掌握程度大于你的年限,有点好奇平时你怎么安排学习时间的”


“就可能目前这家公司比较清闲把,再加上学习写代码对我比较快乐,就能投入很多时间和精力,哎,就二面能不能快点!可以加个微信吗~”


“行”


总结


然后二面就是项目面了,啊好累啊,现在前端真卷啊,其实感觉可以多问点工程化,虽然准备了很多但是没被问上。


emmm,因为看评论区嘛,就想说,乐观点,其实也没这么卷,只要自己多写写代码和看看八股文都可以的,当然最重要的还是思考。emm想加群一起卷的可以看沸点


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

抓包技术的应用示例:薅瑞幸羊毛 🦙

前言 最近瑞幸在搞活动,每天免费送10000份咖啡,我是个不喝咖啡的人儿,所以没咋关注,今早我们的团宠小妹,拉着我 10点整拼手速,想着帮她抢一杯,于是点开瑞幸咖啡小程序主页,banner 栏轮播图中有一张海报入口,操作一通下来,果然,没抢到。 手速不够快不是...
继续阅读 »

前言


最近瑞幸在搞活动,每天免费送10000份咖啡,我是个不喝咖啡的人儿,所以没咋关注,今早我们的团宠小妹,拉着我 10点整拼手速,想着帮她抢一杯,于是点开瑞幸咖啡小程序主页,banner 栏轮播图中有一张海报入口,操作一通下来,果然,没抢到。


手速不够快不是主要原因,手指操作延迟 + 系统页面跳转耗时加起来到 http 发出就已经耽误了1 -2 秒钟了,这个时间才是关键,本文从技术角度探讨下怎么在最小成本比如几分钟内,实现一个小工具,来解决这个问题。


抓包工具


首先需要一个抓包工具,iphone 手机可以用 stream, 有几个注意点:


1、默认安装后是无法抓取 https 类型的,需要在设置里进行相关配置:



如果您要抓取 HTTPS 的请求,需要先启动抓包,然后安装 CA 证书后,去设置-通用-关于-证书信任设置 里信任 CA,才可以查看请求的内容。



Pasted image 20230601122258.png


2、注意小程序里面哦(原生的可能抓不到),拿到的接口名如下:


https://mkt.lkcoffee.com/ladder/capi/resource/m/promo/activity/send


stream 提供了 curl 的拷贝,将其复制并导入到 postman 中。


WechatIMG247.png


postman 导入&复现


点击 import 按钮,在弹窗中选择 raw text 将复制的 curl 字符串粘贴进去,点击确认,就成功的将 这个 http 接口导入到了 postman 中,尝试点击 send 按钮,发现拿到了正确的响应,验证了该接口已经可以正常使用。


截屏2023-06-01 12.43.31.png


Pasted image 20230601122933.png


自动化脚本?


其实到这一步,已经实现了目标,点击 send 直接发送请求,大大提升了抢到的概率,如果你还想更进一步,那么可以尝试将其封装成 自动化脚本,来实现定时、自动、重复发送;


点开右侧代码块,选择语言,假设选择 python(也可以任意选择你擅长的语言),然后就自动生成 python 版本的可执行代码片段,我们就在这个基础上拓展功能;


截屏2023-06-01 12.48.19.png


示例代码如下:

import requests
import time

url = "http://example.com" # 将此处的 URL 替换为你要请求的地址
payload = {}
headers = {
#将 postman 中的headers 复制过来
}

start_time = "09:59:55" # 设置开始请求的时间
end_time = "10:00:30" # 设置结束请求的时间

def make_request():
response = requests.get(url, headers=headers, data=payload)
if "成功" in response.text:
print("响应内容:", response.text)
raise SystemExit # 中断程序

while True:
current_time = time.strftime("%H:%M:%S", time.localtime())
if current_time >= start_time and current_time <= end_time:
make_request()
time.sleep(1) # 每秒检查一次当前时间


将其保存到本地并通过 python 指令来执行,就可以运行了。


总结


用今天的午睡时间,写了这篇文,以瑞幸的营销活动为例子,带你感受了下技术的魅力,其中涉及到了抓包、自动化脚本、定时任务、请求策略、stream 和 postman 等知识;


然后我想问下大家,对于其带来的潜在公平问题,你们怎么看呢?欢迎讨论。


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

程序员增强自控力的方法

作为一名程序员,我们经常会面临工作压力和时间紧迫的情况,因此有一个好的自控力对于我们的工作和生活都是至关重要的。以下是一些可以帮助程序员增强自控力的方法: 1. 设定明确的目标和计划 制定明确的目标和计划可以帮助我们更好地管理时间和精力。我们可以使用日程表、任...
继续阅读 »

作为一名程序员,我们经常会面临工作压力和时间紧迫的情况,因此有一个好的自控力对于我们的工作和生活都是至关重要的。以下是一些可以帮助程序员增强自控力的方法:


1. 设定明确的目标和计划


制定明确的目标和计划可以帮助我们更好地管理时间和精力。我们可以使用日程表、任务清单、时间追踪工具等,来帮助我们控制时间并更有效地完成任务。


2. 掌控情绪


作为程序员,我们需要面对很多挑战和压力,容易受到情绪的影响。因此,掌握情绪是一个非常重要的技能。可以通过冥想、呼吸练习、运动等方法,来帮助我们保持冷静、积极和乐观的心态。


3. 管理焦虑和压力


焦虑和压力是我们常常遇到的问题之一,所以我们需要学会如何管理它们。我们可以使用放松技巧、适度锻炼、交流沟通等方法,来减轻我们的焦虑和压力。


4. 培养自律习惯


自律是一个非常重要的品质。我们可以通过设定目标、建立规律和强化自我控制等方式,来培养自律习惯。


5. 自我反思和反馈


经常进行自我反思和反馈可以帮助我们更好地了解自己的优缺点和行为模式。我们可以使用反馈工具或与他人交流,来帮助我们成长和改进。


6. 持续学习和自我发展


程序员需要不断学习和自我发展,以保持竞争力和提升自己的技能。通过阅读书籍、参加培训、探究新技术等方式,可以帮助我们持续成长,增强自我控制力。


结论


自控力是我们工作和生活中重要的的品质之一,可以帮助我们更好地应对各种挑战和压力。通过设定目标、掌控情绪、管理焦虑和压力、培养自律习惯、自我反思和反馈、持续学习和自我发展等方法,我们可以帮助自己增强自我控制能力并提高工作效率。


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

Android抓包环境搭建

文章不会从零开始教你搭建抓包环境,需要你具备一定的环境基础,满足以下条件才能继续看文章: 电脑已经安装好Fiddler或则Charles; 手机已经root且具备Xposed环境; 如果已经具备上诉环境,那么可以开始继续搭建环境了。 搭建入门抓包环境 An...
继续阅读 »

文章不会从零开始教你搭建抓包环境,需要你具备一定的环境基础,满足以下条件才能继续看文章:



  • 电脑已经安装好Fiddler或则Charles;

  • 手机已经root且具备Xposed环境;


如果已经具备上诉环境,那么可以开始继续搭建环境了。


搭建入门抓包环境


Android手机抓包


手机有Xposed环境,抓包就很简单了,按照下面的步骤操作即可:



  1. 安装Xposed模块—TrustMeAlready_1.11.apk,软件可强制跳过证书验证。

  2. TrustMeAlready.apk内勾选需要抓包的app。

  3. 安装抓包工具-小黄鸟(HttpCanary.apk)

  4. 开始抓包


这种抓包方式有个缺点:有的请求会抓不到。


电脑抓包


电脑抓包的话需要安装根证书,可以参考这篇文章


抓包环境进阶


上面入门环境有时候会出现抓不到某些接口的情况,这时需要将手机端所有的请求进行拦截转发,然后用电脑端抓包工具查看。具体的步骤如下



  1. 安装拦截转发工具(VProxid_1.2.0.apk),可将流量劫持到PC端。

  2. 配置VProxid


这里详细的说下如何配置VProxid,它的界面如下


image20230103172419726.png




  1. 点击右下角的➕,界面如下


    image20230103204810203.png




  2. 设置完成后,回到主界面,点击绿色三角就可以劫持应用的网络通讯




  3. 电脑端抓包工具就可以看到应用的请求记录了。




这种抓包方式,可以抓到应用的所有网络请求。


总结


抓包可以说是逆向的第一步,本篇文章介绍了手机抓包和电脑抓包的方式,进一步的介绍了如何劫持应用的流量到PC端,通过劫持应用的流量到PC端,可以抓到app的所有网络请求,建议使用这种方式,不然可能找不到想要的内容。


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

为什么要使用Kotlin 对比 Java,Kotlin简介

什么是Kotlin 打开Kotlin编程语言的官网,里面大大的写着, A modern programming languagethat makes developers happier. 是一门让程序员写代码时更有幸福感的现代语言 Kotlin语法...
继续阅读 »

什么是Kotlin


打开Kotlin编程语言的官网,里面大大的写着,



A modern programming languagethat makes developers happier.




是一门让程序员写代码时更有幸福感的现代语言




  • Kotlin语法糖非常多,可以写出更为简洁的代码,便于阅读。

  • Kotlin提供了空安全的支持,可以的让程序更为稳定。

  • Kotlin提供了协程支持,让异步任务处理起来更为方便。

  • Google:Kotlin-first,优先支持kotlin,使用kotlin可以使用更多轮子


接下来对比Java举一些例子。


简洁


当定义一个网络请求的数据类时


Java

public class JPerson {
private String name;
private int age;
//getter
//setter
//hashcode
//copy
//equals
//toString
}

Kotlin

data class KPerson(val name: String,val age: Int)

这里用的是Kotlin 的data class 在class 前面加上data 修饰后,kotlin会自动为我们生成上述Java类注释掉的部分


当我们想从List中筛掉某些我们不想要的元素时


Java

List<Integer> list = new ArrayList<>();  

List<Integer> result = new ArrayList<>();
for (Integer integer : list) {
if (integer > 0) { //只要值>0的
result.add(integer);
}
}

System.out.println(result);

Kotlin

val list: List<Int> = ArrayList()

println(list.filter { it > 0 })

如上代码,都能达到筛选List中 值>0 的元素的效果。


这里的filter是Kotlin提供的一个拓展函数,拓展函数顾名思义就是拓展原来类中没有的函数,当然我们也可以自定义自己的拓展函数。


当我们想写一个单例类时


Java

public class PersonInJava {
public static String name = "Jayce";
public static int age = 10;

private PersonInJava() {
}
private static PersonInJava instance;
static {
instance = new PersonInJava();
}
public static PersonInJava getInstance() {
return instance;
}
}

Kotlin

object PersonInKotlin {
val name: String = "Jayce"
val age: Int = 10
}

是的,只需要把class换成object就可以了,两者的效果一样。


还有很多很多,就不一一举例了,接下来看看安全。


安全


空安全

var name: String = "Jayce" //name的定义是一个非空的String
name = null //将name赋值为null,IDE会报错,编译不能通过,因为name是非空的String

var name: String? = "Jayce" //String后面接"?"说明是一个可空的String
name.length //直接使用会报错,需要提前判空
//(当然,Kotlin为我们提供了很多语法糖,我们可以很方便的进行判空)

类型转换安全

fun gotoSleep(obj: Any) {
if (obj is PersonInKotlin) {//判断obj是不是PersonInKotlin
obj.sleep() // 在if的obj已经被认为是PersonInKotlin类型,所以可以直接调用他的函数,调用前不需要类型转换
}
}

协程


这里只是简单的举个例子


Kotlin的协程不是传统意义上那个可以提高并发性能的协程序


官方的对其定义是这样的



  • 协程是一种并发设计模式,您可以在Android平台上使用它来简化异步执行的代码

  • 程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。


当我们用Java请求网络数据时,一般是这么写的。

 getPerson(new Callback<Person>() {//这里有一个回调            
@Override
public void success(Person person) {
runOnUiThread(new Runnable() { //切换线程
@Override
public void run() {
updateUi(person)
}
})
}

@Override
public void failure(Exception e) {
...
}
});

Kotlin协程后我们只需要这么写

 CoroutineScope(Dispatchers.Main).launch { //启动一个协程
val person = withContext(Dispatchers.IO) {//切换IO线程
getPerson() //请求网络
}
updateUi(person)//主线程更新UI
}

他们两个都干的同一件事,最明显的区别就是,代码更为简洁了,如果在回调里面套回调的话回更加明显,用Java的传统写法就会造成人们所说的CallBack Hell。


除此之外协程还有如下优点



  • 轻量

  • 更少的内存泄漏

  • 内置取消操作

  • 集成了Jatpack


这里就不继续深入了,有兴趣的同学可以参考其他文章。


Kotlin-first


在Google I/O 2019的时候,谷歌已经宣布Kotlin-first,建议Android开发将Kotlin作为第一开发语言。


为什么呢,总结就是因为Kotlin简洁、安全、兼容Java、还有协程。


至于有没有其他原因,我也不知道。(手动狗头)


Google将为更多的投入到Kotlin中来,比如




  • 为Kotlin提供特定的APIs (KTX, 携程, 等)




  • 提供Kotlin的线上练习




  • 示例代码优先支持Kotlin




  • Jetpack Compose,这个是用Kotlin开发的,没得选。。。。。




  • 跨平台开发,用Kotlin实现跨平台开发。






好的Kotlin就先介绍到这里,感兴趣的同学就快学起来吧~
接下来在其他文章会对Kotlin和携程进行详细的介绍。


df7da83ea7648fdeb9963e9552ba2425.gif


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

Android 文件上传(包括大文件上传)

1.简介: android 文件上传可以分为两类:一个是小文件,直接上传文件;一个是大文件,这个需要分块上传。Okhttp+Retrofit实现文件上传。 2. 需要的依赖和权限: implementation 'com.squareup.retrofi...
继续阅读 »

1.简介:


android 文件上传可以分为两类:一个是小文件,直接上传文件;一个是大文件,这个需要分块上传。Okhttp+Retrofit实现文件上传。


2. 需要的依赖和权限:

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.5.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'io.reactivex.rxjava2:rxjava:2.2.5'
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'
    <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

3.示例:


3.1.小文件上传:直接上传文件(图片上传为例)

public class UpLoadImageUtils {
private static final String TAG = "UpLoadImageUtils";
//需要上传的图片数量
private static int imgSum;
//上传成功的图片数量
private static int uploadSuccessNum;
private static String enRttId;
//失败数量
private static int errorNum;

private static TestService apiService;


public static void getService(){
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
apiService = retrofit.create(TestService.class);
}

@SuppressLint("CheckResult")
public static void uploadImage(String url, File file) {
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);//表单类型
Map<String, RequestBody> map = new HashMap<>();

//"image/png" 是内容类型,后台设置的类型

RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);

builder.addFormDataPart("name", file.getName());
builder.addFormDataPart("size", "" + file.length());
/*
* 这里重点注意:
* com_img[]里面的值为服务器端需要key 只有这个key 才可以得到对应的文件
* filename是文件的名字,包含后缀名的 比如:abc.png
*/

builder.addFormDataPart("file", file.getName(), requestBody);
MultipartBody body = builder.build();
Observable<BaseResponse> meSetIconObservable = apiService.imgUpload(url, body);

meSetIconObservable.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(baseResponse -> {
Log.i(TAG, JsonUtil.jsonToString(baseResponse));
if ("000000".equalsIgnoreCase(baseResponse.getCode())) {
Log.i(TAG, "onComplete: ---" + uploadSuccessNum);
errorNum = 0;
uploadSuccessNum++;
if (imgSum == uploadSuccessNum) {
finishUpload(true);
uploadSuccessNum = 0;
}
} else {
if (errorNum < 4) {
uploadImage(url, file);
errorNum++;
}else {
finishUpload(false);
}
}
}, throwable -> {
Log.i(TAG, "onComplete: ---" + throwable.getMessage());
});
}


/**
* 上传
*
* @param compressFile 需要上传的文件
* @param urls 需要上传的文件地址
*/
public static void uploadList(List<String> urls, List<File> compressFile) {
getService();
//多张图片
imgSum = urls.size();
for (int i = 0; i < compressFile.size(); i++) {
uploadImage(urls.get(i), compressFile.get(i));
}
}


public interface TestService {
@POST()
Observable<BaseResponse> imgUpload(@Url String url, @Body MultipartBody multipartBody);
}
}

public class UpLoadImageUtils {
private static final String TAG = "UpLoadImageUtils";
//需要上传的图片数量
private static int imgSum;
//上传成功的图片数量
private static int uploadSuccessNum;
private static String enRttId;
//失败数量
private static int errorNum;

private static TestService apiService;


public static void getService(){
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
apiService = retrofit.create(TestService.class);
}

@SuppressLint("CheckResult")
public static void uploadImage(String url, File file) {
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);//表单类型
Map<String, RequestBody> map = new HashMap<>();

//"image/png" 是内容类型,后台设置的类型

RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);

builder.addFormDataPart("name", file.getName());
builder.addFormDataPart("size", "" + file.length());
/*
* 这里重点注意:
* com_img[]里面的值为服务器端需要key 只有这个key 才可以得到对应的文件
* filename是文件的名字,包含后缀名的 比如:abc.png
*/

builder.addFormDataPart("file", file.getName(), requestBody);
MultipartBody body = builder.build();
Observable<BaseResponse> meSetIconObservable = apiService.imgUpload(url, body);

meSetIconObservable.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(baseResponse -> {
Log.i(TAG, JsonUtil.jsonToString(baseResponse));
if ("000000".equalsIgnoreCase(baseResponse.getCode())) {
Log.i(TAG, "onComplete: ---" + uploadSuccessNum);
errorNum = 0;
uploadSuccessNum++;
if (imgSum == uploadSuccessNum) {
finishUpload(true);
uploadSuccessNum = 0;
}
} else {
if (errorNum < 4) {
uploadImage(url, file);
errorNum++;
}else {
finishUpload(false);
}
}
}, throwable -> {
Log.i(TAG, "onComplete: ---" + throwable.getMessage());
});
}


/**
* 上传
*
* @param compressFile 需要上传的文件
* @param urls 需要上传的文件地址
*/
public static void uploadList(List<String> urls, List<File> compressFile) {
getService();
//多张图片
imgSum = urls.size();
for (int i = 0; i < compressFile.size(); i++) {
uploadImage(urls.get(i), compressFile.get(i));
}
}


public interface TestService {
@POST()
Observable<BaseResponse> imgUpload(@Url String url, @Body MultipartBody multipartBody);
}
}


3.2.大文件分块上传(视频上传为例)同步

public class UploadMediaFileUtils {

private static final String TAG = "UploadMediaFileUtils";
private static UploadService uploadService;
//基础的裁剪大小20m
private static final long baseCuttingSize = 20 * 1024 * 1024;
//总的块数
private static int sumBlock;
//取消上传
private static boolean isCancel = false;
//是否在上传中
private static boolean isUploadCenter=false;

public static void uploadMediaFile(String url, String uploadName, File file, String appInfo, IOUploadAudioListener ioResultListener) {
if (file.exists()) {
getService();
//总的分块数
sumBlock = (int) (file.length() / baseCuttingSize);
if (file.length() % baseCuttingSize != 0) {
sumBlock = sumBlock + 1;
}
isCancel = false;
isUploadCenter = true;
uploadMedia(url, uploadName, file, appInfo, 1, ioResultListener);
} else {
Log.i(TAG, "文件不存在");
ioResultListener.errorResult("-1", "文件不存在");
}
}

@SuppressLint("CheckResult")
public static void uploadMedia(String url, String uploadName, File file, String appInfo, int currentBlock, IOUploadAudioListener ioResultListener) {
if (isCancel){
Log.i(TAG, "取消上传");
return;
}

byte[] fileStream = cutFile(file, currentBlock - 1, ioResultListener);
if (fileStream == null) {
Log.i(TAG, "uploadMedia: getBlock error");
ioResultListener.errorResult("-1", "fileStream为空");
return;
}

MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);//表单类型
RequestBody requestBody = RequestBody.create(MultipartBody.FORM, fileStream);
builder.addFormDataPart("name", uploadName);
builder.addFormDataPart("size", "" + fileStream.length);
Log.i(TAG, "size" + fileStream.length);
builder.addFormDataPart("num", "" + currentBlock);
/*
* 这里重点注意:
* com_img[]里面的值为服务器端需要key 只有这个key 才可以得到对应的文件
* filename是文件的名字,包含后缀名的 比如:abc.png
*/

builder.addFormDataPart("file", file.getName(), requestBody);
MultipartBody body = builder.build();
Observable<BaseResponse> meSetIconObservable = uploadService.mediaUpload("huizhan", appInfo, url, body);

meSetIconObservable.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(baseResponse -> {
if ("000000".equalsIgnoreCase(baseResponse.getCode())) {
double progress = 100 * div(currentBlock, sumBlock, 2);
ioResultListener.progress("" + progress);
if (currentBlock < sumBlock) {
uploadMedia(url, uploadName, file, appInfo, currentBlock + 1, ioResultListener);
return;
}
ioResultListener.successResult(baseResponse);
} else {
ioResultListener.errorResult(baseResponse.getCode(), baseResponse.getDesc());
}
}, throwable -> {
ioResultListener.errorResult("-1", "上传失败");
});
}

public static void getService() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
uploadService = retrofit.create(UploadService.class);
}


public interface UploadService {
@POST()
Observable<BaseResponse> mediaUpload(@Header("X-Biz-Id") String bizId,
@Header("X-App-Info") String AppInfo,
@Url String url,
@Body MultipartBody multipartBody);
}



/**
* 写入本地(测试用)
*
* @param list
*/
public static void writeFile(List<byte[]> list) {
FileWriter file1 = new FileWriter();
String path = Environment.getExternalStorageDirectory() + File.separator + "12345.wav";
try {
file1.open(path);
for (int i = 0; i < list.size(); i++) {
Log.i(TAG, "writeFile: " + list.get(i).length);
file1.writeBytes(list.get(i), 0, list.get(i).length);
}
file1.close();
LogUtils.i(TAG, "writeFile: ");
} catch (Exception e) {
e.printStackTrace();
}
}


public static byte[] cutFile(File file, int currentBlock, IOUploadAudioListener ioResultListener) {
Log.i(TAG, "getBlockThree:000000---" + currentBlock);
int size = 20 * 1024 * 1024;
byte[] endResult = null;
byte[] result = new byte[size];
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
RandomAccessFile accessFile = new RandomAccessFile(file, "rw");
accessFile.seek(currentBlock * size);
//判断是否整除
if (file.length() % baseCuttingSize != 0) {
//当前的位数和总数是否相等(是不是最后一段)
if ((currentBlock + 1) != sumBlock) {
int len = accessFile.read(result);
out.write(result, 0, len);
endResult = out.toByteArray();
} else {
//当有余数时
//当前位置2147483647-20971520
byte[] bytes = new byte[(int) (file.length() % baseCuttingSize)];
int len = accessFile.read(bytes);
out.write(bytes, 0, len);
endResult = out.toByteArray();
}
} else {
int len = accessFile.read(result);
out.write(result, 0, len);
endResult = out.toByteArray();
}
accessFile.close();
out.close();
} catch (IOException e) {
// e.printStackTrace();
ioResultListener.errorResult("-1", "cutFile失败");
}
return endResult;
}

/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入。
*
* @param v1 被除数
* @param v2 除数
* @param scale 表示表示需要精确到小数点以后几位。
* @return 两个参数的商
*/
public static double div(double v1, double v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}

/**
* 取消上传
*/
public static void cancelUpload() {
if (isUploadCenter) {
isCancel = true;
}
}

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

关于 Android 稳定性优化你应该了解的知识点

前言 Android 稳定性优化是一个需要长期投入,持续运营和维护的一个过程,不仅深入探讨了 Java Crash、Native Crash 和 ANR 的解决流程及方案,还分析了其内部实现原理和监控流程。本文对稳定性优化方面的知识做了一个全面总结,主要内容如...
继续阅读 »

前言


Android 稳定性优化是一个需要长期投入,持续运营和维护的一个过程,不仅深入探讨了 Java Crash、Native Crash 和 ANR 的解决流程及方案,还分析了其内部实现原理和监控流程。本文对稳定性优化方面的知识做了一个全面总结,主要内容如下:


image.png


如何提升App的稳定性


一般性的App能接触到稳定性的需求其实并不多,只有大型的处于稳定运营期的App才会重视App的稳定性,稳定性实际上是一个大问题,一个稳定的产品才能够保证用户的留存率,所以稳定性是质量体系中最基本也是最关键的一环:



  • 稳定性是大问题,Crash是P0优先级:对于用户来说很难容忍你的应用发生崩溃

  • 稳定性可优化的面很广:不仅仅是指崩溃,像卡顿、耗电等也属于稳定性优化的范畴,对于移动端高可用这个标准来说,性能优化只是高可用的一部分,还有一部分就是应用业务流程功能上的可用


稳定性维度



  • Crash维度:一般会将Crash单独作为一项重要指标进行突破,最常见的统计指标就是Crash率,后面会说到

  • 性能维度:启动速度、内存、卡顿、流量、电量等等,在解决应用的Crash之后,就应该着手保障性能体系的稳定

  • 业务高可用维度:业务层面的高可用是相当关键的一步,需要使用多种手段去保障App业务的主流程及核心路径的可用性


稳定性优化概述


如果App到了线上才发现异常,其实已经造成了损失,所以稳定性优化重点在于预防



  • 重在预防、监控必不可少:从开发到测试到发布上线运维这些各个阶段都需要预防异常的发生,或者说要将发生异常造成的损失降到最低,用最小的代价暴露最多的问题,同时监控也是必不可少的一步,需要拥有一定的监控手段来更加灵敏的发现问题

  • 思考更深一层、重视隐含信息:比如你发现了一个崩溃,但是你不能简单的只看这一个崩溃,要考虑这个崩溃是不是在其他地方也有同样或者类似的,如果有就考虑是否统一处理,今后该如何预防,总结经验

  • 长效保持需要科学流程:在项目的每一个阶段建立完善的相关规范,保证长效的优化效果


如何有效降低应用崩溃率


Crash相关指标


1.UV、PV Crash率



  • UV Crash率:等于Crash UV/DAU:主要针对于用户使用量的统计,它统计一段时间内所有用户中发生过崩溃的用户占比,和UV强相关,UV是指Unique Visitor一天内访问网站的人数(是以cookie为依据),一天内同一访客的多次访问只计算为1,一台电脑不同的浏览器的cookie值不同。

  • PV Crash率:针对用户使用频率的统计,统计一段时间内所有用户启动次数中发生崩溃的占比,和PV强相关,PV是指PageView也就是页面点击量,每次刷新就算一次浏览,多次打开同一页面会累加。

  • UV Crash方便评估用户影响范围,PV Crash方便评估相关Crash的影响严重程度

  • 注意:沿用同一种衡量方式:不管你是使用UVCrash还是PVCrash作为主要指标,你应该一直使用它,因为和Crash率相关的会有一些经验值,这些经验值需要对应一个衡量指标


2.Java、Native Crash率



  • Java Crash:在Java代码中,出现了未捕获的异常,导致程序异常退出

  • Native Crash:一般都是因为在Native代码中访问非法地址,也可能是地址对齐出现了问题,或者发生了程序主动abort,这些都会产生相应的signal信号,导致程序异常退出。目前Native崩溃中最成熟的方案是BreakPad


3.启动、重点流程Crash率



  • 启动Crash率:在启动阶段用户还没有完全打开就发生了崩溃的占比,只要是用户打开App10s之内发生的崩溃都被视为启动Crash,它是Crash分类中最严重的一类,我们需要重点关注这个指标,而且降低的越低越好,并且我们应该结合客户端的容灾策略进行自主修复


4.增量、存量Crash率



  • 增量Crash:指新增Crash,它是新版本Crash率变动的原因,如果没有新增的Crash,那么新版本的Crash率应该是和老版本Crash率保持一致,所以增量Crash是新版本中需要重点解决的问题

  • 存量Crash:指老版本中已经存在的Crash,这些Crash一般都是难以解决或者是需要在特定场景下才会出现的难以复现的问题,这类问题需要长期投入精力持续解决

  • 优先解决增量、持续跟进存量


5.Crash率评价指标



  • 务必在千分之二以下:Java和Native的崩溃率加起来需要在千分之二以下才能算是合格的

  • Crash率处于万分位视为优秀的标准


Crash关键问题


1.尽可能还原Crash现场


一旦发生崩溃,我们需要尽可能保留崩溃现场信息,这样有利于还原崩溃发生时的各种场景信息,从而推断出可能导致崩溃的原因,对于采集环节可以参考以下采集点:



  • 采集堆栈、用户设备、OS版本、发生崩溃的进程、线程名、崩溃前后的Logcat

  • 前后台、使用时长、App版本、小版本、渠道

  • CPU架构、内存信息、线程数、资源包信息、行为日志


image.png


上面是一张Bugly后台的截图,对于成熟的性能监控平台不仅有Crash的单独信息,同时会对各种Crash进行聚合以及报警。


2.APM后台聚合展示



  • Crash现场信息:包括Crash具体堆栈信息及其它额外信息

  • Crash Top机型、OS版本、分布版本、发生地域:有了这些Top Crash信息之后就能够知道哪些Crash的影响范围比较大需要重点关注

  • Crash起始版本、上报趋势、是否新增、持续版本、发生量级等等:可以从多个视角判断Crash发生的可能原因以及决定是否需要修复,在哪些版本上进行修复


3.Crash相关的整体架构


image.png


4.非技术相关的关键问题


建立规范的流程保证开发人员能够及时处理线上发生的问题:



  • 专项小组轮值:成立专门小组来跟踪每个版本周期之内线上产生的Crash,保证一定有人跟进处理

  • 自动分配匹配:可以自定义某些业务模块名称,自动分配给相应人员处理

  • 处理流程全纪录:记录相应人员处理Crash的每一步,后期出现问题追究相关责任人也是有据可查的


单个Crash处理方案



  • 根据堆栈及现场信息找答案:一般来说堆栈信息可以帮助我们解决90%的问题

  • 找共性比如机型、OS、实验开关、资源包等:有些Crash信息通过堆栈找不到有用的帮助,不能直接解决,这种情况下可以通过Crash发生时的各种现场信息作辅助判断,分析这些Crash用户拥有哪些共性

  • 线下复现、远程调试:有了共性之后尝试在线下进行复现,或者尝试能否进行远程调试


Crash率治理方案



  • 解决线上常规Crash:抽出一定时间来专门解决所有常规的Crash,这些Crash一般相对来说比较容易解决

  • 系统级Crash尝试Hook绕过:当然Android系统版本一直在不断的升级,随着新系统的覆盖率越来越高,老版本的系统Bug可能会逐渐减少

  • 疑难Crash重点突破、更换方案:做到长期跟踪,团队合作重点突破,实在不行可以考虑更换实现方案


通过以上几点应该可以解决大部分的存量Crash,同时再控制好新增Crash,这样一来整体的Crash率一般都能够得到有效降低。


这一部分的内容有点杂而多,一般也是需要多端配合,所以不太好做具体演示,大家可以在网上多查找相关资料进行巩固学习。


如何选择合适的崩溃服务



  1. 腾讯Bugly: 除了有crash数据还有运营数据

  2. UC 啄木鸟:可以捕获Java、Native异常,被系统强杀的异常,ANR,Low Memory Killer、killProcess。技术深度以及捕获能力强

  3. 网易云捕:继承便捷,访问快,捕获以及上报速度及时,支持实时报警,提供多种报警选项,可以自定义参数。

  4. Google的Firebase

  5. crashlytics:服务器在国外,访问速度慢,会丢掉数据

  6. 友盟:crash之后会在再次启动的时候上报数据,所以不能立即获得这部分信息


移动端业务高可用方案


移动端高可用方案不仅仅是指性能方面的高可用,业务方面的高可用也是尤为重要的,如果业务都走不通,试问你性能做的再好又有何用呢?



  • 高可用:性能+业务

  • 业务高可用侧重于用户功能完整可用

  • 业务高可用影响公司实际收入:比如支付流程不通


对于业务是否可用不像Crash一样,如果发生Crash我们可以收到系统的回调,业务不可用实际上我们是无从知道的,所以针对建设移动端业务高可用的方案总结以下几点:


1.数据采集



  • 梳理项目主流程、核心路径、关键节点:一般需要对项目主流程和核心路径做埋点监控,比如用户下单需要从列表页到详情页再到下单页,这就是一个核心路径,我们可以监控具体每个页面的到达率和下单成功率

  • AOP自动采集、统一上报:数据采集的时候可以采用AOP的方式,减少接入成本,上报的时候可以采取统一的上报减少流量和电量消耗,上传到后台之后再做详细的分析,得出所有业务流程的转化率,以及相应界面的异常率


2.报警策略



  • 阈值报警:比如某个业务失败的次数超过了阈值就报警通知

  • 趋势报警:对比前一天的异常情况,如果增加的趋势超过了一定的比例即便是未达阈值也要触发报警

  • 特定指标报警、直接上报:比如支付SDK调用失败,这种错误无需跟着统一的数据上报,出现立即上报


3.异常监控



  • Catch代码块:实际开发中我们为了避免程序崩溃,经常会写一些try{}catch(){}来捕获相关异常,但是这样操作完成之后,程序确实不崩溃了,相应的功能也是无法使用的,所以这些被Catch住的异常也要上报,有利于分析功能不可用的原因

  • 异常逻辑:比如我们需要对结果为true的调用方法进行处理,结果为false时不执行任务,但是我们也需要上报异常,统计一下出现这种情况的用户的占比情况,以便针对性的优化


这里简单的举个栗子,表明意思:

        try {
//业务处理
LogUtils.i("...");
}catch (Exception e){
//如果未加上统计,就无法知道具体是什么原因导致的功能不可用
ExceptionMonitor.monitor(Log.getStackTraceString(e));
}

boolean flag = true;
if (flag){
//正常,继续执行相关任务
}else {
//异常,不执行任务,这种情况产生的异常也应该进行上报
ExceptionMonitor.monitor("自定义业务失败标识");
}

4.单点追查



  • 需要针对性分析的特定问题:这些问题相对小众,很可能是和特定用户的操作习惯、账户体系相关,所以要尽可能获取多的数据重点分析

  • 全量日志回捞,专项分析:很多日志信息默认都是只记录不上传,比如用户全部的行为日志,这些日志只有在需要的时候才有用,平时没必要上传,没啥用还浪费流量,如果需要排查特定用户的详细信息,也可以通过服务端下发指令客户端接收指令后上传


5.兜底策略


当你通过监控了解到业务不正常之后,请问该如何修复?这里就要用到兜底策略了,就是到了最后一步各种措施都做了,用户还是出现了异常,这种情况仍然还是要有相关联的配置手段来达到高可用。对于业务上的异常除了热修复的手段之外,还可以通过建立配置中心,将功能开关关闭。



  • 配置中心,功能开关:实际项目中很多数据都是通过服务端动态下发配置的,将这些功能集合起来的处理平台就是配置中心。举个栗子:比如新版本上线了一个新功能,加了一个入口,上线之后发现功能不稳定,此时就可以通过服务端配置的方式将此功能开关关闭,这样即使用户无法使用新功能,但是至少不会发现业务的异常

  • 跳转分发中心:熟悉组件化开发的朋友都知道做组件化module的拆分必不可少的就是要有一个路由,它的作用就是跳转分发中心,所有的跳转都是通过路由来做,如果匹配到需要跳转到有Bug的功能界面时可以统一跳转到一个异常处理的页面


移动端容灾方案


移动端容灾必要性


说到容灾,首先来看一下需要防范的灾是什么?主要分为两部分:性能异常和业务异常,只要是对用户的实际体验产生了很大的影响,都是需要防范的App线上灾害。



  • 灾:性能、业务异常


传统的流程是如何处理线上出现的紧急问题的呢?传统的处理流程首先需要用户反馈出现的不正常情况,接着开发人员进行紧急的BUG修复,然后重新打包上传渠道进行更新,可见传统的流程比较繁琐,灵敏度较低,如果日活量较高,随着Bug在线上存活的时间延长对用户基数的影响是巨大的,势必是无法接受的



  • 传统流程:用户反馈、重新打包、渠道更新,不可接受


移动端容灾最佳实践


1.功能开关



  • 配置中心,服务端下发配置控制:首先对任何新上线的功能加上功能开关,可以通过配置中心的方式下发开关决定是否显示新功能的入口,出现异常情况可以随时关闭入口,这样可以保证上线的新功能处于可控状态

  • 针对场景,功能新加或代码改动:一是新增了功能,二是出现了代码改动,比如重构代码,最好保留之前的老方案,在新方案上线之后如果有问题,可以切回之前的老方案


这里简单的做个演示:

public class ConfigManager {

public static boolean mOpenClick = true; //默认值为true

}

mAdapter.setOnItemClickListener((view, position) -> {
//控制点击事件是否开启
if (ConfigManager.mOpenClick){ //mOpenClick的值从接口获取,由服务端控制
//处理具体业务
}
});

2.统跳中心


组件化之后的项目的页面跳转都是通过路由来做的,如果发现线上产生了异常,可以在路由跳转这一步拦截有Bug的跳转,重定向到备用方案,或者统一的错误处理中界面,更多的情况是为了对线上用户产生的影响降到最低,如果有Bug不能进行热修复,也没有合适的开关可用,会做一个临时的H5页面,让用户点击之后跳转到临时的H5页面,这样用户还是可以操作,只是在体验上稍微差一点,总归来说比不能用强的多



  • 界面切换通过路由,路由决定是否重定向

  • Native Bug不能热修则跳转到临时H5


3.动态化修复


目前为止,国内市场安卓的热修复方案已经比较成熟了,对于大型项目来说,一般都会支持热修复的能力,热修复技术就是用户不需要重新安装一个Apk,就可以实现比原有Apk有较大更新的能力,比如微信的Tinker和美团的Robust都是非常好的热修复实现方案。需要注意的是,热修复也只是一个功能,对于热修复也需要加上各种完善的统计,需要知道热修方案是否真正有效果,没有用造成更大的损失



  • 热修复能力,可监控、灰度、回滚、清除

  • 推拉结合、多场景调用保证到达率

  • Weex、RN增量更新


4.安全模式


安全模式侧重于移动端发生严重Crash时的自动恢复,做的好的安全模式往往会有几级不同的策略,比如App多次启动失败,那就重置整个App到安装的状态,避免因为一些脏数据导致的App持续闪退,同时如果有Bug并且非常严重到了最严重的等级,可以采用阻塞性热修来解决,即:必须等热修成功之后才可进入主页面。需要注意的是,安全模式不仅仅可以针对App Crash,也可以针对一些组件,比如网络请求多次失败后也可以进入安全模式,暂时拒绝用户的网络请求,避免给服务端造成的额外压力



  • 根据Crash信息自动恢复,多次启动失败重置App

  • 严重Bug可阻塞性热修复

  • 异常熔断:多次请求失败则主动拒绝


容灾方案总结:


image.png


这几种方式是由简单到复杂的递进,为了保障线上的稳定性,最好在应用中多加入几个稳定性保障方案。


稳定性长效治理


对于稳定性优化来说是一个细活,需要打持久战,不能一个版本优化了,后面又恶化了,因此需要在项目开发的整个周期内的不同阶段都加上相应的方案。


1.开发阶段


在开发阶段组内每个开发人员的编码实力都是不一样的,因此需要统一编码规范,然后结合一些手段增强人员的编码功底,尽可能的将问题消灭在编码阶段,尽可能的写出高质量的代码,同时要结合开发阶段的技术评审,以及每天的互相CodeReview机制,坚持几个月编码水平肯定会有明显的提升,开发阶段明显的问题应该就不会再有了,而且代码风格结构也会大体一致。同时开发阶段还需要做的事情就是架构优化,项目的架构应该根据项目的不同发展阶段来不断优化,这里说两点,第一能力收敛比如界面切换的能力用路由来实现,对网络请求要统一网络库统一使用方式,这样可以避免不正当的使用带来的Bug,第二统一容错,比如对于网络请求来说可以在网络请求回来的时候加上预先校验,判断回来的数据是否合法,如果不合法就不需要再把数据转给上层业务了



  • 统一编码规范、增强编码功底、技术评审、CodeReview机制

  • 架构优化:能力收敛、统一容错


2.测试阶段



  • 功能测试、自动化测试、回归测试、覆盖安装

  • 特殊场景、机型等边界测试

  • 云测平台:辅助测试,满足对特殊机型的测试需求


3.合码阶段


开发时肯定是在自己的分支进行开发,测试通过之后才会往主干分支合入,合入之前首先需要进行代码的编译检查和静态扫描发现可能存在的问题,经过校验之后也不能直接合入,应该将自己的分支首先合入到一个和主干分支一样的分支中进行预编译,编译通过之后最好加上主流程的回归测试



  • 编译检测,静态扫描

  • 预编译流程、主流程自动回归


4.发布阶段


到了发布阶段一般来说App都是经过了开发自测、QA测试、内部测试等测试环节,相对来说比较稳定了,但是需要注意的是,很多问题你不可能全部测出来,所以必须谨慎对待



  • 多轮灰度:灰度的量级要由小变多,争取以最小的代价暴露最多的问题

  • 分场景、维度全面覆盖


5.运维阶段


任何一个小问题在海量用户面前都会影响巨大,因此这个阶段必须要依靠APM的灵敏监控



  • APM灵敏监控

  • 回滚、降级策略

  • 热修复、容灾方案

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

SharedPreferences的一种极简优雅且安全的用法

针对Android平台键值对的持久化存储,虽然Jetpack出了新的DataStore,但实际项目中SharedPreferences还是有大量使用,本文结合以前的使用经验给出一种极简且优雅且安全的实践。(示例项目见 gitee.com/spectre1225...
继续阅读 »

针对Android平台键值对的持久化存储,虽然Jetpack出了新的DataStore,但实际项目中SharedPreferences还是有大量使用,本文结合以前的使用经验给出一种极简且优雅且安全的实践。(示例项目见 gitee.com/spectre1225…


1. SharedPreferences的使用与改进


SharedPreferences的基本读写代码如下:

preferences.edit().putInt("intKey", 1).apply();//写
preferences.getInt("intKey", 0);//读,0为默认值

代码中直接这么用的话,键会很不好管理,不清楚一个键值对到底有多少地方使用,当键发生改变需要修改的时候,也容易遗漏。于是就有了以下改进:

public interface XXXConfig{
String KEY_PROPERTY_AA = "key_aa";
String KEY_PROPERTY_BB = "key_bb";
String KEY_PROPERTY_CC = "key_cc";
//more key.......
}

//使用的地方
preferences.edit().putInt(XXXConfig.KEY_PROPERTY_AA, 1).apply();//写
preferences.getInt(XXXConfig.KEY_PROPERTY_AA, 0);//读,0为默认值

但这样写仍然有问题,就是缺少值类型的约束:一个key对应的value,可能有很多种类型。这种情况下,需要额外的注释或文档来记录每一个key对应的value的类型信息。于是,有人想到可以像JavaBean一样,采用getter和setter方法的形式:

public class XXXConfig {
private SharedPreferences preferences;
private String KEY_PROPERTY_AA = "key_aa";
private String KEY_PROPERTY_BB = "key_bb";

//中间省略初始化......

public int getPropertyAA() {
return preferences.getInt(KEY_PROPERTY_AA, 0);
}

public void setPropertyAA(int value) {
preferences.edit().putInt(KEY_PROPERTY_AA, value);
}

public int getPropertyBB() {
return preferences.getInt(KEY_PROPERTY_BB, 0);
}

public void setPropertyBB(int value) {
preferences.edit().putInt(KEY_PROPERTY_BB, value);
}
}

这种写法改进了类型安全,但每次新增就需要写一个属性和两个方法,过程比较繁琐。理想情况,我还是希望像写文档一样只需要写下面这样的信息:

属性1:类型 int
属性2:类型 String

然后使用的地方可以直接取值。因此,就有了下面介绍的新的封装方法:暂且称为NeoPreference。


2. NeoPreference简单使用


首先,我们需要需要创建一个inferface来继承Config接口,这个新的接口对应一个SharedPreferences,默认接口名即为SharedPreferences的名称,例如:

public interface DemoConfig extends Config {

}

这里的DemoConfig即为SharedPreferences的名称。有时候我们想要自己另外指定名称,则可以使用Config.Name注解:

@Config.Name("my_demo_config")
public interface DemoConfig extends Config {

}

这个时候SharedPreferences名称就是my_demo_config


然后我们就可以通过ConfigManager来获取DemoConfig的实例:

DemoConfig config = ConfigManager.getInstance().getConfig(DemoConfig.class);

到目前为止还没有什么新鲜的,接下来我们往里面添加新的配置项/属性:

@Config.Name("my_demo_config")
public interface DemoConfig extends Config {
Property<Integer> versionCode();
}

在上述基础上,只需要添加一行代码,就添加了新的键值对:key的值为versionCode,value的类型为Integer。然后我们的读写代码可以这么写:

DemoConfig config = ConfigManager.getInstance().getConfig(DemoConfig.class);
Integer versionCode = config.versionCode().get();//读
config.versionCode().set(versionCode + 1);//写

如果我们想要单独定key的名字,我们可以使用对应属性的注解:

@Config.Name("my_demo_config")
public interface DemoConfig extends Config {
@IntItem(key = "my_version_code")
Property<Integer> versionCode();
}

我们还可以指定值的范围和默认值:


@Config.Name("my_demo_config")
public interface DemoConfig extends Config {
@IntItem(key = "my_version_code", start = 1, to = 10000, defaultValue = 1)
Property<Integer> versionCode();
}

这样,在值不符合规范的时候会抛出异常:

DemoConfig config = ConfigManager.getInstance().getConfig(DemoConfig.class);
config.versionCode().set(-1);//throw exeception

3. NeoPreference API说明


这个工具的API除了ConfigManager类以外主要分两部分:Property类以及类型对应的注解。


3.1 ConfigManager接口说明


ConfigManager是单例实现,维护一个SharedPreferencesConfig的注册表,提供getConfigaddListener两个方法。


以下是getConfig方法签名:

public <P extends Config> P getConfig(Class<P> pClass);
public <P extends Config> P getConfig(Class<P> pClass, int mode);

参数pClass是继承Config类的接口class,可选参数mode对应SharedPreferences的mode。


addListener的方法监听指定preferenceName中内容的变化,签名如下:

public void addListener(String preferenceName, WeakReference<Listener> listenerRef);
public void addListener(LifecycleOwner lifecycleOwner, String preferenceName, Listener listener);

第一个方法接受一个Listener的弱引用,需要调用者自己持有监听器的引用,自己管理生命周期,否则可能被回收。第二个方法不采用弱引用参数,而是额外添加LifecycleOwner,这个监听器的声明周期采用LifecycleOwner对应的生命周期。


3.2 Property类接口说明


Property接口包括:

public final String getKey();//获取属性对应的key
public T get(T defValue); //获取属性值,defValue为默认值
public T get(); //获取属性值,采用缺省默认值
public void set(T value); //设置属性值
public Optional<T> opt(); //以Optional的形式返回属性值
public final void addListener(WeakReference<Listener<T>> listenerRef) //类似ConfigManager,不过只监听该属性的值变化
public final void addListener(LifecycleOwner owner, Listener<T> listener)//类似ConfigManager,不过只监听该属性的值变化

泛型参数支持LongIntegerFloatBooleanStringSet<String>SharedPreferences支持的几种类型,以及额外的Serializable


3.3 类型相关注解介绍


这些注解对应SharedPreferences支持的几种类型(其中description字段暂时不用)。

@interface StringItem {
String key() default "";
boolean supportEmpty() default true;
String[] valueOf() default {};
String defaultValue() default "";
String description() default "";
}

@interface BooleanItem {
String key() default "";
boolean defaultValue() default false;
String description() default "";
}

@interface IntItem {
String key() default "";
int defaultValue() default 0;
int start() default Integer.MIN_VALUE;
int to() default Integer.MAX_VALUE;
int[] valueOf() default {};
String description() default "";
}

@interface LongItem {
String key() default "";
long defaultValue() default 0;
long start() default Long.MIN_VALUE;
long to() default Long.MAX_VALUE;
long[] valueOf() default {};
String description() default "";
}

@interface FloatItem {
String key() default "";
float defaultValue() default 0;
float start() default -Float.MIN_VALUE;
float to() default Float.MAX_VALUE;
float[] valueOf() default {};
String description() default "";
}

@interface StringSetItem {
String key() default "";
String[] valueOf() default {};
String description() default "";
}

@interface SerializableItem {
String key() default "";
Class<?> type() default Object.class;
String description() default "";
}

4. 完整实现


见:gitee.com/spectre1225…


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

安卓与串口通信-数据分包的处理

前言 本文是安卓串口通信的第 5 篇文章。本来这篇文章不在计划内,但是最近在项目中遇到了这个问题,正好借此机会写一篇文章,在加深自己理解的同时也让大伙对串口通信时接收数据可能会出现分包的情况有所了解。 其实关于串口通信会可能会出现分包早有耳闻,但是我自己实际使...
继续阅读 »

前言


本文是安卓串口通信的第 5 篇文章。本来这篇文章不在计划内,但是最近在项目中遇到了这个问题,正好借此机会写一篇文章,在加深自己理解的同时也让大伙对串口通信时接收数据可能会出现分包的情况有所了解。


其实关于串口通信会可能会出现分包早有耳闻,但是我自己实际使用时一直没有遇到过,或者准确的说,虽然遇到过,但是并没有特意的去处理:


分包?不就是传过来的数据不完整嘛,那我把这个数据丢了,等一个完整的数据不就得了。


亦或者,之前使用的都是极少量的数据,一次读取的数据只有 1 byte ,所以很少出现数据包不完整的情况。


何为分包?


严格意义上来说,其实并不存在分包的概念。


因为由于串口通信的特性,它并不知道不知道也无法知道所谓的 “包” 是什么,它只知道你给了数据给它,他就尽可能的把数据发出去。


因为串口通信时使用的是流式传输,也就是说,所有数据都是以流的形式进行发送、读取,也不存在所谓的“包”的概念。


所谓的“包”只是我们在应用层人为的规定了多少长度的数据或者满足什么样格式的数据为一个“包”。


而为了最大程度的减少通信时的请求次数,在处理数据流时,通常会尽可能多的读取数据,然后缓存起来(即所谓的缓冲数据),直至达到设置的某个大小或超过某个时间没有读取到新的数据。


例如,我们人为的规定了一个数据包为 10 字节,PLC 或 其他串口设备发送时将这 10 个字节的数据连续的发送出来。但是安卓设备或其他主机在接收时,由于上面所说的原因,可能会先读到 4 字节的数据,再读到 6 字节的数据。也就是说,我们需要的完整数据不会在一次读取中读到,而是被拆分成了不同的“数据包”,此即所谓的 “分包”:


1.gif


怎么处理分包?


其实谜底就在谜面上,通过上面对分包出现的原因进行简单的解释之后,相信大伙对于怎么解决分包问题已经有了自己的答案。


解决分包的核心原理说起来非常简单,无非就是把我们需要的完整的数据包从多次读取到的数据中取出来,再拼成我们需要的完整数据包即可。


问题在于,我们应该怎么才能知晓读取到数据属于哪个数据包呢?我们又该怎么知道数据包是否已经完整了呢?


这就取决于我们在使用串口通信时定义的协议了。


一般来说,为了解决分包问题,我们常用的定义协议的方法有以下几种:



  1. 规定所有数据为固定长度。

  2. 为一个完整的数据规定一个终止字符,读到这个字符表示本次数据包已完整。

  3. 在每个数据包之前增加一个字符,用于表示后续发送的数据包长度。


固定数据包长度


固定数据长度指我们规定每次通信时发送的数据包长度都是固定的长度,如果实际长度不足规定的长度则使用某些特殊字符如 \0 填充剩余的长度。


对于这种情况,非常好处理,只要我们每次读取数据时都判断读取到的数据长度,如果数据长度没有达到符合的固定长度,则认为读取数据不完整,就接着读取,直至数据长度符合:

val resultByte = mutableListOf<Byte>()
private fun getFullData(count: Int = 0, dataSize: Int = 20): ByteArray {
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, 2000)
for (i in 0 until readLen) {
resultByte.add(buffer[i])
}

// 判断数据长度是否符合
return if (resultByte.size == dataSize) {
resultByte.toByteArray()
} else {
if (count < 10) {
getFullData(count + 1, dataSize)
}
else {
// 超时
return ByteArray(0)
}
}
}

但是这种方式也有一个明显的缺点,那就是使用场景局限性特别强,只适合于主机发送请求,从机器回应的这种场景,因为如果是在从机不停的发送数据,而主机可能在某个时间段读取,也可能一直轮询读取的情况下,光靠数据长度判断是不可靠的,因为我们无法确保我们读到的指定长度的数据一定就是同一个完整数据,有可能参杂了上一次的数据或者下一次的数据,而一旦读取错一次,就意味着以后每次读取的数据都是错的。


增加结束符


为了解决上述方式导致的局限性,我们可以给每一帧数据增加一个结束符号,通常来说我们会规定 \r\n 即 CRLF (0x0D 0x0A)为结束符号。


所以,我们在读取数据时会循环读取,直至读取到结束符号,则我们认为本次读取结束,已经获得了一个完整的数据包:

val resultByte = mutableListOf<Byte>()
private fun getFullData(): ByteArray {
var isFindEnd = false

while (!isFindEnd) {
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, 2000)
if (readLen != 0) {
for (i in 0 until readLen) {
resultByte.add(buffer[i])
}
if (buffer[readLen - 1] == 0x0A.toByte() && buffer.getOrNull(readLen - 2) == 0x0D.toByte()) {
isFindEnd = true
}
}
}

return resultByte.toByteArray()
}

但是这个方法显然也有一个缺陷,那就是如果是单次间隔读取或者轮询时第一次读取数据有可能也是不完整的数据。


因为我们虽然读取到了结束符号,但是并不意味着这次读取的就是完整的数据,或许前面还有数据我们并没有读到。


不过这种方式可以确保轮询时只有第一次读取数据有可能不完整,但是后续的数据都是完整的。


只是单次间隔读取的话就无法保证读取到的是完整数据了。


在开头增加数据包长度


和增加结束符类似,我们也可以在数据包开头增加一个特殊字符,然后在后面紧跟着一个指定长度(1byte)字符指定接下来的数据包长度有多长。


这样,我们就可以在解析时首先查找这个开始符号,查找到之后则认为一个新的数据包开始了,然后读取之后 1byte 的字符,获取到这个数据包的长度,接下里按照这个这个指定长度,循环读取直到长度符合即可。


具体读取方式其实就是上面两种方式的结合,所以这里我就不贴代码了。


最好的情况


最方便的解决数据分包的方法当然是在数据中既包括固定数据头、固定数据尾、甚至连数据长度都是固定的。


例如某款温度传感器,发送的是数据格包为固定 10 位长度,且有结束符 CRLF,并且数据包开头有且只有 -+ (0x2B 0x2D 0x20)三种情况,那么我们在接收数据时就可以这么写:

val resultByte = mutableListOf<Byte>()
val READ_WAIT_MILLIS = 2000
private fun getFullData(count: Int = 0, dataSize: Int = 14): ByteArray {
var isFindStar = false
var isFindEnd = false
while (!isFindStar) { // 查找帧头
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, READ_WAIT_MILLIS)
if (readLen != 0) {
if (buffer.first() == 0x2B.toByte() || buffer.first() == 0x2D.toByte() || buffer.first() == 0x20.toByte()) {
isFindStar = true
for (i in 0 until readLen) { // 有帧头,把这次结果存入
resultByte.add(buffer[i])
}
}
}
}

while (!isFindEnd) { // 查找帧尾
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, READ_WAIT_MILLIS)
if (readLen != 0) {
for (i in 0 until readLen) { // 先把结果存入
resultByte.add(buffer[i])
}
if (buffer[readLen - 1] == 0x0A.toByte() && buffer.getOrNull(readLen - 2) == 0x0D.toByte()) { // 是帧尾, 结束查找
isFindEnd = true
}
}
}


// 判断数据长度是否符合
return if (resultByte.size == dataSize) {
resultByte.toByteArray()
} else {
if (count < 10) {
getFullData(count + 1, dataSize)
}
else {
return ByteArray(0)
}
}

粘包呢?


上面我们只说了分包情况,但是在实际使用过程中,还有可能会出现粘包的现象。


粘包,顾名思义就是不同的数据包在一次读取中混合到了一块。


如果想要解决粘包的问题也很简单,类似于解决分包,也是需要我们在定义协议时给出能够区分不同数据包的方式,这样我们按照协议解析即可。


总结


其实串口通信中的分包或者粘包解决起来并不难,问题主要在于串口通信一般都是每个硬件设备厂商或者传感器厂商自己定义一套通信协议,而有的厂商定义的协议比较“不考虑”实际,没有给出任何能够区分不同数据包的标志,这就会导致我们在接入这些设备时无法正常的解析出数据包。


但是也并不是说就没有办法去解析,而是需要我们具体情况具体分析,比如温度传感器,虽然通信协议中没有给出数据头、数据尾、数据长度等信息,但是其实它返回的数据格式几乎都是固定的,我们只要按照这个固定格式去解析即可。


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

Android 如何统一处理登录后携带数据跳转到目标页面

需求场景 我们在开发应用的时候经常会遇到先登录,登录成功后再跳转到目标页面。比如商品详情页面我们点击购买必须要先登录,登录完成才能去下单支付。针对这种场景,我们一般有两种做法: 点击购买跳转到登录,登录完成需要用户再次点击购买才能去下单支付页面,这种用户体验...
继续阅读 »

需求场景


我们在开发应用的时候经常会遇到先登录,登录成功后再跳转到目标页面。比如商品详情页面我们点击购买必须要先登录,登录完成才能去下单支付。针对这种场景,我们一般有两种做法:



  1. 点击购买跳转到登录,登录完成需要用户再次点击购买才能去下单支付页面,这种用户体验不是很好。

  2. 点击购买跳转到登录,登录完成直接跳转到下单支付页面。


第一种我们就不谈了产品经理不同意🐶。第二种我们一般是在 onActivityResult 里面获取到登录成功,然后根据 code 跳转到目标页面。这种方式缺点就是我们要在每个页面都处理相同的逻辑还有定义各种 code,如果应用里面很多这种场景也太繁琐了。那有没有统一的方式去处理这种场景就是我们今天的主题了。


封装方式


我们的应用是组件化的,APP 的页面跳转使用了 Arouter。所以我们统一处理使用 Arouter 封装。直接上代码

fun checkLoginToTarget(postcard: Postcard) {//Postcard 是 Arouter 的类
if (User.isLogin()) {
postcard.navigation()
} else {
//不能使用 postcard 切换 path 直接跳转,因为 group 可能不同,所以重新 build
ARouter.getInstance().build(Constant.LOGIN)
.with(postcard.extras)//获取携带的参数重新转入
.withString(Constant.TAGACTIVIFY, postcard.path)//添加目标路由
.navigation()
}
}
//登录成功后在登录页面执行这个方法
fun loginSuccess() {
val intent= intent
val target = intent.getStringExtra(Constant.TAGACTIVIFY)//获取目标路由
target?.apply {
if (isNotEmpty()){
val build = ARouter.getInstance().build(this)
val extras = intent.extras//获取携带的参数
if (extras != null) {
build.with(extras)
}
build.navigation()
}
}
finish()
}

代码加了注释,使用 Kotlin 封装了顶层函数,登录页面在登录成功后跳转到目标页面,针对上面的场景直接调用 checkLoginToTarget 方法。

checkLoginToTarget(ARouter.getInstance().build(Constant.PAY_PAGE).withInt(Constant.GOOD_ID,id))

通过 Arouter 传入下单支付的路由地址,并且携带了商品的 ID,生成了 Postcard 参数。登录成功后能带着商品 ID
直接下单支付了。


最后


如果项目里没有使用路由库可以使用 Intent 封装实现,或者别的路由库也可以用上面的方式去做统一处理。


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

揭开Android视图绘制的神秘面纱

揭开Android视图绘制的神秘面纱 在Android的UI中,View是至关重要的一个组件,它是用户界面的基本构建块。在View的绘制过程中,涉及到很多重要的概念和技术。本文将详细介绍Android View的绘制过程,让你能够更好地理解和掌握Android...
继续阅读 »

揭开Android视图绘制的神秘面纱


在Android的UI中,View是至关重要的一个组件,它是用户界面的基本构建块。在View的绘制过程中,涉及到很多重要的概念和技术。本文将详细介绍Android View的绘制过程,让你能够更好地理解和掌握Android的UI开发。


什么是View?


View是Android系统中的一个基本组件,它是用户界面上的一个矩形区域,可以用来展示文本、图片、按钮等等。View可以响应用户的交互事件,比如点击、滑动等等。在Android中,所有的UI组件都是继承自View类。


View的绘制过程


View的绘制过程可以分为三个阶段:测量、布局和绘制。下面我们将逐一介绍这三个阶段。


测量阶段(Measure)


测量阶段是View绘制过程的第一个重要阶段。在测量阶段,系统会调用View的onMeasure方法,测量View的宽度和高度。在这个过程中,系统会根据View的LayoutParams和父容器的大小来计算出View的大小。


例:下面代码是一个自定义View的onMeasure方法例程。在测量过程中,我们设定了View的大小。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 获取宽度的Size和Mode
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 如果Mode是精确的,直接返回
if (widthMode == MeasureSpec.EXACTLY) {
setMeasuredDimension(widthSize, heightMeasureSpec);
return;
}

// 计算View的宽度
int desiredWidth = getPaddingLeft() + getPaddingRight() + defaultWidth;
int measuredWidth;
if (desiredWidth < widthSize) {
measuredWidth = desiredWidth;
} else {
measuredWidth = widthSize;
}

// 设置宽度和高度的Size和Mode
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredHeight = defaultHeight;
if (heightMode == MeasureSpec.EXACTLY) {
measuredHeight = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
measuredHeight = Math.min(defaultHeight, heightSize);
}
setMeasuredDimension(measuredWidth, measuredHeight);
}

在测量阶段结束后,系统会将计算好的宽度和高度传递给布局阶段。


布局阶段(Layout)


布局阶段是View绘制过程的第二个重要阶段。在布局阶段,系统会调用View的onLayout方法,将View放置在父容器中的正确位置。在这个过程中,系统会根据View的LayoutParams和父容器的位置来确定View的位置。


例:下面代码是一个自定义ViewGroup的onLayout方法例程。在布局过程中,我们遍历子View,并根据LayoutParams确定子View的位置和大小。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int left = getPaddingLeft();
int top = getPaddingTop();
int right = getMeasuredWidth() - getPaddingRight();
int bottom = getMeasuredHeight() - getPaddingBottom();

for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}

LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childLeft = left + lp.leftMargin;
int childTop = top + lp.topMargin;
int childRight = right - lp.rightMargin;
int childBottom = bottom - lp.bottomMargin;
child.layout(childLeft, childTop, childRight, childBottom);
}
}

绘制阶段(Draw)


绘制阶段是View绘制过程的最后一个重要阶段。在绘制阶段,系统会调用View的onDraw方法,绘制View的内容。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


例:下面代码是一个自定义View的onDraw方法例程。在绘制过程中,我们使用Paint对象绘制了一段文本。

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

//绘制文本
String text = "Hello World";
Paint paint = new Paint();
paint.setTextSize(50);
paint.setColor(Color.RED);
paint.setAntiAlias(true);
canvas.drawText(text, 0, getHeight() / 2, paint);
}

除了绘制内容,我们还可以在绘制阶段绘制View的背景和前景。系统会调用drawBackgrounddrawForeground方法来绘制背景和前景。值得注意的是,View的绘制顺序是:先绘制背景,再绘制内容,最后绘制前景。


View的绘制流程


View的绘制流程可以看作是一个递归调用的过程,下面我们将具体介绍这个过程。


Step 1:创建View


在View绘制过程的开始阶段,我们需要创建一个View对象,并将它添加到父容器中。在这个过程中,系统会调用View的构造函数,并将View的LayoutParams传递给它。


Step 2:测量View


接下来,系统会调用View的measure方法,测量View的宽度和高度。在这个过程中,View会根据自身的LayoutParams和父容器的大小来计算出自己的宽度和高度。


Step 3:布局View


在测量完成后,系统会调用View的layout方法,将View放置在父容器中的正确位置。在这个过程中,View会根据自身的LayoutParams和父容器的位置来确定自己的位置。


Step 4:绘制背景


在布局完成后,系统会调用View的drawBackground方法,绘制View的背景。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 5:绘制内容


接下来,系统会调用View的onDraw方法,绘制View的内容。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 6:绘制前景


在绘制内容完成后,系统会调用View的drawForeground方法,绘制View的前景。在这个过程中,我们同样可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 7:绘制子View


接着,系统会递归调用ViewGroup的dispatchDraw方法,绘制所有子View的内容。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 8:完成绘制


最后,所有的View绘制完成,整个View树也就绘制完成。


例:下面代码是一个自定义ViewGroup的绘制流程例程。在绘制过程中,我们先画背景,再绘制每个子View的内容。

public class MyViewGroup extends ViewGroup {

public MyViewGroup(Context context) {
super(context);
}

public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 测量子View的宽高
measureChildren(widthMeasureSpec, heightMeasureSpec);

// 获取ViewGroup的宽高大小
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

// 设置ViewGroup的宽高
setMeasuredDimension(widthSize, heightSize);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 遍历所有子View,设置它们的位置和大小
int childCount = getChildCount();
int left, top, right, bottom;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
left = childView.getLeft();
top = childView.getTop();
right = childView.getRight();
bottom = childView.getBottom();
childView.layout(left, top, right, bottom);
}
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 画背景
canvas.drawColor(Color.WHITE);
}

@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
// 绘制每个子View的内容
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
childView.draw(canvas);
}
}
}

在ViewGroup的绘制流程中,系统会先调用ViewGroup的draw方法,然后依次调用dispatchDraw方法和绘制每个子View的draw方法。ViewGroup的绘制顺序是先绘制自己的背景,再绘制每个子View的内容和背景,最后绘制自己的前景。


总结


本文详细介绍了Android View的绘制过程,包括测量阶段、布局阶段和绘制阶段。同时,我们还在代码实现的角度,详细说明了Android ViewGroup的绘制流程,帮助你更好地理解和掌握Android的UI开发。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


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

协程和协程作用域

理清子协程,父协程,协程作用域,协程生命周期,协程作用域的生命周期等的关系。 1、协程是在协程作用域内执行的轻量级并发单元。当协程的代码块执行完成时,它会挂起并返回到其父协程或顶层协程中。 2、父协程通过调用协程构建器(如 launch、async)来启动子...
继续阅读 »

理清子协程,父协程,协程作用域,协程生命周期,协程作用域的生命周期等的关系。


1、协程是在协程作用域内执行的轻量级并发单元。当协程的代码块执行完成时,它会挂起并返回到其父协程或顶层协程中。




2、父协程通过调用协程构建器(如 launchasync)来启动子协程。在启动子协程时,子协程会继承父协程的上下文(包括调度器、异常处理器等)。这意味着子协程会以与父协程相同的上下文执行。父协程可以通过 join() 方法等待子协程执行完成,以确保子协程的结果可用。


父协程可以通过取消操作来取消子协程。当父协程被取消时,它会递归地取消所有的子协程。子协程会接收到取消事件,并根据取消策略来决定如何处理取消。


父协程和子协程之间的关系可以帮助管理协程的层次结构和生命周期。通过父协程启动和取消子协程,可以有效地组织和控制协程的执行流程,实现更灵活和可靠的协程编程。

fun main() {
runBlocking {
val parentJob = launch {

val childJob = launch {
printMsg("childJob start")
delay(500)
printMsg("childJob complete")
}

childJob.join()
printMsg("parentJob complete")

}

parentJob.join()
parentJob.cancel()
printMsg("parentJob cancel")
}
}

//日志
main @coroutine#3 childJob start
main @coroutine#3 childJob complete
main @coroutine#2 parentJob complete
main @coroutine#1 parentJob cancel
Process finished with exit code 0
fun main() {
runBlocking {
val parentJob = launch {

val childJob = launch {
printMsg("childJob start")
delay(500)
printMsg("childJob complete")
}

childJob.join()
printMsg("parentJob complete")

}

//parentJob.join() <----------变化在这里
parentJob.cancel()
printMsg("parentJob cancel")
}
}

//日志
main @coroutine#1 parentJob cancel
Process finished with exit code 0



3、协程作用域(CoroutineScope)是用于协程的上下文环境,它提供了协程的启动和取消操作的上下文。协程作用域定义了协程的生命周期,并决定了协程在何时启动、在何时取消。


协程作用域是一个接口,定义了两个主要方法:




  • launch:用于启动一个新的协程。launch 方法会创建一个新的协程,并将其添加到当前协程作用域中。启动的协程将继承父协程的上下文,并在协程作用域内执行。




  • cancel:用于取消协程作用域中的所有协程。cancel 方法会发送一个取消事件到协程作用域中的所有协程,使它们退出执行。




协程作用域与协程之间的关系是协程在协程作用域内执行的。协程作用域为协程提供了上下文环境,使得协程可以访问到必要的上下文信息,例如调度器(Dispatcher)和异常处理器(ExceptionHandler)。通过在协程作用域中启动协程,可以确保协程的生命周期受到协程作用域的管理,并且在协程作用域取消时,所有协程都会被取消。

fun main() = runBlocking {
coroutineScope {
launch {
delay(1000)
printMsg("Coroutine 1 completed")
}

launch(Job()) { <---------协程2不使用协程作用域的上下文,会脱离协程作用域的控制
delay(2000)
printMsg("Coroutine 2 completed")
}
}
printMsg("Coroutine scope completed")
}

//日志
main @coroutine#2 Coroutine 1 completed 1685615335423
main @coroutine#1 Coroutine scope completed 1685615335424 <-------协程1执行完,协程作用域就执行完
Process finished with exit code 0 <---------程序退出



4、如果使用 GlobalScope.launch 创建协程,则协程会成为全局协程,它的生命周期独立于程序的其他部分。当协程的代码执行完毕后,全局协程不会自动退出,除非应用程序本身退出。因此,全局协程可以在整个应用程序的生命周期内持续执行,直到应用程序终止。


如果使用协程作用域(例如 runBlockingcoroutineScope 等)创建协程,则协程的生命周期受协程作用域的限制。当协程的代码执行完毕后,它会返回到作用域的父协程或顶级协程中,而不会自动退出。




5、当协程作用域内的所有协程执行完成后,协程作用域仍然存在,但其中的协程会被标记为完成状态。这意味着协程作用域仍然可以用于启动新的协程,但之前的协程不会再执行。


协程作用域的生命周期不仅仅依赖于其中的协程执行状态,还取决于其父协程或顶级协程的生命周期。如果协程作用域的父协程或顶级协程被取消或完成,那么协程作用域也将被取消

fun main() = runBlocking {
coroutineScope {
launch {
delay(1000)
printMsg("Coroutine 1 completed")
}

launch {
delay(2000)
printMsg("Coroutine 2 completed")
}
}
printMsg("Coroutine scope completed")
}

//日志
main @coroutine#2 Coroutine 1 completed
main @coroutine#3 Coroutine 2 completed
main @coroutine#1 Coroutine scope completed
Process finished with exit code 0 <---------程序退出

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

如何让安卓应用有两个入口

在使用鼎鼎大名的 leakcanary 检测内存泄漏时,我们发现,添加了 leakcanary 依赖后,再次运行 app 时,桌面上会多一个应用图标。 打开这个 Leaks 应用就能看到自己的 app 中存在的内存泄漏。 让桌面上多一个应用图标,这是怎么做到...
继续阅读 »

在使用鼎鼎大名的 leakcanary 检测内存泄漏时,我们发现,添加了 leakcanary 依赖后,再次运行 app 时,桌面上会多一个应用图标。


leakcanary


打开这个 Leaks 应用就能看到自己的 app 中存在的内存泄漏。


让桌面上多一个应用图标,这是怎么做到的呢?


答案是使用 activity-alias


一、为应用设置两个入口,分别启动两个 Activity


举个例子,通过 activity-alias 为应用程序指定另一个入口:

<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SecondActivity"
android:exported="false"
android:taskAffinity="second.affinity"/>
<activity-alias
android:name="SecondActivity"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="SecondActivity"
android:targetActivity=".SecondActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

可以看到,应用入口是 MainActivity,但我们通过 activity-alias 给 SecondActivity 也设置了应用入口的 intent-filter,安装后,桌面就会有两个入口:


activity-alias


点击两个图标就会启动两个不同的 Activity。这里还给 SecondActivity 设置了 taskAffinity,目的是让 SecondActivity 启动时,被放在一个新的栈中。


二、为应用设置两个入口,启动同一个 Activity


activity-alias 添加入口时,是不是一定要启动不同的 Activity 呢?


答案是不一定。activity-alias 也允许我们为同一个 Activity 定义多个别名,从而实现一个应用程序拥有多个图标或多个启动入口的效果。

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity-alias
android:name=".Alias"
android:icon="@mipmap/ic_chrome"
android:label="Fake Chrome"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

可以看到,应用入口是 MainActivity,但我们通过 activity-alias 给 MainActivity 又设置了一个别名,安装后,桌面就会有两个入口:


activity-alias


点击两个图标都会启动同一个 Activity。


三、activity-alias 还能做什么?


如果我们需要设置一个 Activity 支持打开网页,通常会采用这样的做法:

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
</intent-filter>
</activity>

这里给 MainActivity 添加了支持打开网页的 intent-filter。运行后,当遇到打开链接的请求时,就会弹出这样的对话框:


open with


除了这种方式,activity-alias 也可以实现同样的功能。

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity-alias
android:name=".browser"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
</intent-filter>
</activity-alias>

另外,activity-alias 还可以给我们的应用再加一个 label 说明。

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity-alias
android:name=".browser"
android:label="My Browser"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
</intent-filter>
</activity-alias>

此时再打开链接,就会在 My Application 底部展示我们新增的 label: My Browser:


My Browser


四、总结


activity-alias 为应用程序提供了更多的灵活性和可定制性。使用activity-alias,我们可以为一个Activity定义多个入口,从而增强应用程序的用户体验。


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

深入学习 Kotlin 枚举的进阶用法:简洁又高效~

Kotlin 作为现代的、强大的编程语言,可以给开发者提供诸多特性和工具,得以帮助我们编写更加高效、更具可读性的代码。 其中一个重要的特性便是 Enum 枚举,其本质上是一种数据类型:允许你定义一组用名称区分的常量。 本篇文章将通过代码案例带你探索 Kotli...
继续阅读 »

Kotlin 作为现代的、强大的编程语言,可以给开发者提供诸多特性和工具,得以帮助我们编写更加高效、更具可读性的代码。


其中一个重要的特性便是 Enum 枚举,其本质上是一种数据类型:允许你定义一组用名称区分的常量


本篇文章将通过代码案例带你探索 Kotlin 枚举的进阶用法,进而帮助大家理解如何将 Enum 更好地应用到项目当中。


1. 枚举类


可以说 Enum Classes 是 Kotlin 中展示一组常量的绝佳方式。


具体来说,它允许你定义一组有限数量的成员来限定数据类型,并且你可以在代码的各处便捷使用这些枚举类型。


如下,我们用 enum 关键字定义一周内各天的枚举类型。

 enum class DayOfWeek {
     MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
 }

然后在代码中自由使用该枚举,比如:

 fun getWeekendDays(): List<DayOfWeek> {
     return listOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)
 }

2. 枚举属性


除了展示类型,Kotlin Enum 还可以拥有属性 property,这意味着开发者可以给枚举成员添加额外的信息。


比如下面,我们给 DayOfWeek 枚举增加各天在周内的序号属性。

 enum class DayOfWeek(val number: Int) {
     MONDAY(1),
     TUESDAY(2),
     WEDNESDAY(3),
     THURSDAY(4),
     FRIDAY(5),
     SATURDAY(6),
     SUNDAY(7)
 }

然后便可以获得该天的序号信息。

 fun getDayNumber(day: DayOfWeek): Int {
     return day.number
 }

3. 枚举函数


Kotlin Enum 也支持定义函数,所以可以在枚举内部定义功能性方法、供外部使用。


如下在 DayOfWeek 枚举里增加一个用来判断该天是否属于周末的 isWeekend() 函数。

 enum class DayOfWeek(val number: Int) {
     MONDAY(1),
     TUESDAY(2),
     WEDNESDAY(3),
     THURSDAY(4),
     FRIDAY(5),
     SATURDAY(6),
     SUNDAY(7);
 
     fun isWeekend(): Boolean {
         return this == SATURDAY || this == SUNDAY
    }
 }

在使用该枚举的地方,便可以直接使用该函数进行判断。

 fun printDayType(day: DayOfWeek) {
     if (day.isWeekend()) {
         println("$day is a weekend day.")
    } else {
         println("$day is a weekday.")
    }
 }

4. 枚举构造函数


既然 Enum 可以拥有属性,那么自然支持构造函数,所以开发者可以在实例构造的时候,增加充分多的信息。


比如,我们在 DayOfWeek 枚举的构造函数里,在序号以外增加该天的名称信息。

 enum class DayOfWeek(val number: Int, val displayName: String) {
     MONDAY(1, "Monday"),
     TUESDAY(2, "Tuesday"),
     WEDNESDAY(3, "Wednesday"),
     THURSDAY(4, "Thursday"),
     FRIDAY(5, "Friday"),
     SATURDAY(6, "Saturday"),
     SUNDAY(7, "Sunday");
 
     override fun toString(): String {
         return displayName
    }
 }

这样便可以获得该枚举携带的名称数据。

 fun printDayName(day: DayOfWeek) { 
     println("The day of the week is ${day.displayName}")
 }

5. 枚举扩展函数


和普通类一样,也可以针对 Enum Class 添加扩展函数。我们可以在枚举类外部,按需添加额外的功能函数。


比如这里给 DayOfWeek 枚举扩展一个获取下一天的函数。

 fun DayOfWeek.nextDay(): DayOfWeek {
     return when (this) {
         MONDAY -> TUESDAY
         TUESDAY -> WEDNESDAY
         WEDNESDAY -> THURSDAY
         THURSDAY -> FRIDAY
         FRIDAY -> SATURDAY
         SATURDAY -> SUNDAY
         SUNDAY -> MONDAY
    }
 }

像调用枚举本身定义的函数一样,自由使用该扩展函数。

 fun printNextDay(day: DayOfWeek) {
     println("The next day is ${day.nextDay()}")
 }

结语


可以看到 Kotlin Enum 可以帮助开发者定义好一组类型的常量:大大简化代码、具备更好的可读性以及提供额外的功能函数。


通过上述的进阶用法,相信大家可以使用 Enum 创造出更加健壮和高效的代码,同时也更容易理解和维护。


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

【Android】Kotlin 中特别的关键字

前言 Kotlin 是一种现代化的静态类型编程语言,被广泛应用于 Android 开发、Web 开发等领域。与其他编程语言相比,Kotlin 中具有一些独特的关键字,本文将介绍这些关键字及其使用。 1. data data 是 Kotlin 中的一个关键字,用...
继续阅读 »

前言


Kotlin 是一种现代化的静态类型编程语言,被广泛应用于 Android 开发、Web 开发等领域。与其他编程语言相比,Kotlin 中具有一些独特的关键字,本文将介绍这些关键字及其使用。


1. data


data 是 Kotlin 中的一个关键字,用于定义数据类。数据类是一种特殊的类,用于封装数据,通常不包含任何业务逻辑。定义数据类时,只需要列出需要存储的属性即可,Kotlin 会自动生成一些常用的方法,如 toString()equals()hashCode() 等。

kotlinCopy code
data class User(val id: Int, val name: String, val age: Int)

2. companion object


companion object 是 Kotlin 中的一个关键字,用于定义伴生对象。伴生对象是一个类内部的单例对象,可以访问该类的私有成员。与 Java 中的静态方法类似,Kotlin 中的伴生对象可以定义静态方法和静态属性。

class Utils {
companion object {
fun add(a: Int, b: Int): Int {
return a + b
}
}
}

val result = Utils.add(1, 2)

3. lateinit


lateinit 是 Kotlin 中的一个关键字,用于定义延迟初始化的变量。延迟初始化的变量必须是非空类型的,并且不能使用 val 关键字定义。在变量被访问之前,它必须被初始化,否则会抛出 UninitializedPropertyAccessException 异常。

class Person {
lateinit var name: String

fun initName() {
name = "John"
}

fun printName() {
if (::name.isInitialized) {
println(name)
} else {
println("Name has not been initialized yet")
}
}
}

val person = Person()
person.initName()
person.printName()

4. by


by 是 Kotlin 中的一个关键字,用于实现委托。委托是一种将对象的某些职责交给另一个对象处理的机制,可以简化代码的编写。Kotlin 中的委托分为接口委托、类委托和属性委托三种,通过 by 关键字实现。

interface Car {
fun drive()
}

class RealCar : Car {
override fun drive() {
println("Real car is driving")
}
}

class FakeCar(private val realCar: RealCar) : Car by realCar {
override fun drive() {
println("Fake car is driving")
}
}

val realCar = RealCar()
val fakeCar = FakeCar(realCar)
fakeCar.drive()

5. when


when 是 Kotlin 中的一个关键字,用于实现类似于 switch-case 的语句。与 switch-case 不同的是,when 可以匹配更加复杂的条件表达式,并且支持任意类型的值作为条件。另外,when 的分支可以是表达式,可以方便地处理多种情况。

fun getScore(level: String): Int = when (level) {
"A" -> 90
"B" -> 80
"C" -> 70
else -> 0
}

val scoreA = getScore("A")
val scoreB = getScore("B")

6. object


object 是 Kotlin 中的一个关键字,用于定义匿名对象或单例对象。匿名对象是一种不需要定义类名的对象,可以在需要的时候创建并使用。单例对象是一种只有一个实例的对象,可以在整个应用程序中共享使用。

fun main() {
val person = object {
val name = "John"
val age = 20
}
println("${person.name} is ${person.age} years old")
}

object Config {
val host = "localhost"
val port = 8080
}

val host = Config.host
val port = Config.port

7. reified


reified 是 Kotlin 中的一个关键字,用于实现泛型类型的具体化。在 Java 中,泛型类型在运行时会被擦除,导致无法获取泛型类型的具体信息。而在 Kotlin 中,通过 reified 关键字可以将泛型类型具体化,可以在运行时获取泛型类型的信息。

inline fun <reified T> getType(): String = T::class.java.simpleName

val typeName = getType<Int>()

总结


Kotlin 中的关键字具有一些独特的特性,如数据类、伴生对象、延迟初始化、委托、when、对象表达式和具体化的泛型类型等。这些特性可以让开发者更加方便地编写代码,提高代码的可读性和可维护性。


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

2023移动端技术探索

1. 行业背景 过去的2022年对大家来说都是困难的一年,难在疫情影响,难在宏观环境的增长放缓。没有增长带来的就是痛苦的体验,对于互联网行业,21年的主题是“反996”,到了22年风向就变成了“降本增效”、“业务搜索”以及“裁员”。再细化动移动端,经过十年的发...
继续阅读 »

1. 行业背景


过去的2022年对大家来说都是困难的一年,难在疫情影响,难在宏观环境的增长放缓。没有增长带来的就是痛苦的体验,对于互联网行业,21年的主题是“反996”,到了22年风向就变成了“降本增效”、“业务搜索”以及“裁员”。再细化动移动端,经过十年的发展,它已经步入“成熟期”,各行各业都被改造差不多了,技术上该有的轮子都有了,基础的服务也搭建差不多了,似乎真正到达瓶颈了,存量时代的小修小补对人力的需求已经是对半砍了。脉脉《抢滩数字时代·人才迁徙报告2023》报告显示:2022年企业招聘总职位数量同比减少21.67%,纯互联网职位量同比减少50.4%。


2023-01-30-23-04-03-image.png
又到了制定新一年OKR的时候了,大家都在发愁技术项目的规划,不知道在技术上去做哪些探索和突破。InfoQ发布的《中国软件技术发展洞察和趋势预测研究报告2023》第三条核心结论显示:2022年技术服务理念转变,从技术先进到业务赋能,IT部门公司定位逐渐由成本部门转向业务赋能部门,技术也更被边缘化了,个人职业发展屏障出现,这个时候我们不禁对前途迷茫甚至产生质疑。再细化到移动端,《中国软件技术发展洞察和趋势预测研究报告2023》展示的“中国技术成熟度评估曲线”中前沿和早期推广项目貌似都与移动端没有太大关系。


2023-01-31-15-59-41-image.png


本文尝试从各个方面探索移动端可以发展的方向,最大程度的“压榨”可能的技术方向(有些只是抛出问题,而不是最终答案)。


2. 近两年大厂探索方向与成果


在挖掘之前先看看大厂(可能是某个领域有所建树)这些年在做什么,看看有没有直接可以抄的作业。


2.1 21年出调研结果


21年初写OKR时对几个大厂做了调研,下面分别看看阿里、美团、京东做了什么,准备做什么:


阿里移动端技术全景图


2023-01-30-15-31-22-image.png


阿里移动端发展趋势


2023-01-30-15-31-56-image.png


美团移动端技术全景图


2023-01-30-15-45-52-image.png


京东移动端技术全景图


2023-01-30-15-32-53-image.png


京东移动端未来远景图


2023-01-30-15-43-34-image.png


看起来都大同小异,可能各个规模的公司都在建设或者建设完成。


2.2 22年产出


在看看22年大厂的输出,这里主要来自于企业技术公众号输出内容。


2.2.1 阿里


阿里推出的《2022技术人的百宝黑皮书》总结了2022年阿里年度精选终端技术栈的内容:


2023-01-31-17-21-38-image.png


2.2.2 美团


下面内容摘自美团技术发布的《2022年美团技术年货-合辑》:


2023-01-31-17-35-46-image.png


2.2.3 百度


百度App技术公众号发布2022精选文章:


2023-01-31-17-39-09-image.png


2.2.4 分析


从上面三家企业对外输出的文章看,在移动端的动作不外乎几个方向:



  1. 跨端/低代码

  2. 性能优化

  3. 自动化测试

  4. 开发平台/平台化能力

  5. AI


3. 移动端主要方向分析


结合上面整理出来的,我们看看移动端“可以”有哪些方向。


3.1 业务开发


业务开发还是主流的市场需求,这块会占大部分的比例,IT部门从成本部门转为赋能部门后,主要的工作量就是支持业务。


3.2 跨端/低代码


在降本增效的背景下,跨端还会持续搞,但是也不是新东西了。H5、React、Flutter、小程序,这些都各有利弊,不同场景用不同技术,像小程序这种更适合平台化的超级APP,规模不够大的话,性价比不高。


3.3 性能优化


同样的,性能优化也是需要持续做的事情,但是也不是新东西了,一些技术手段都比较成熟了,没有太多可挖掘的空间了。


3.4 架构方向


架构管理方向随着规模的收缩,很难出现机会了。


3.5 开发平台建设


在公司内部,类似于蚂蚁的mPaas开发平台在业务快速成长期对提效会有很大的帮助,这个时候随着业务的裂变,推出各种APP,开发平台可以避免很多重复的工作,助力应用快速上线和运营,但是在收缩期再去建设就有点不划算了。


单点的平台能力,比如监控、埋点之类的或者用第三方或者也自建完成了,对缺失的个别能力,可以根据业务需求点滴建设。


3.6 系统应用/Framework/驱动开发


随着AI、Iot、新能源的发展与兴起,释放出一些系统开发的诉求,相对于之前,嵌入式驱动开发的薪资也有所增长,也算是一个方向,但是也要记住,比起手机,电视、汽车毕竟是少数,如果纯转嵌入式的话可能沾Iot的光规模更大些,不过比起芯片,这也是比较成熟的技术,可挖掘方向不大,只是多了个写业务的战场。


3.7 XR


目前比较成熟的是VR,但是VR在端上展示主要基于H5,采集会有单独硬件,有些也支持了手机采集,但是还是那句话,市场需求不大。至于AR、元宇宙更多的是AI的综合应用了,我们也不讨论了。


3.8 音视频


音视频一直是移动端比较大和前沿的一块方向,但是现在也已趋于成熟,下面看看主要的几个方向:



  • 点播:播放器的事情,主要涉及多解码期、预加载秒开,剩下的交给系统播放器都可以完成的很好了;

  • 录制:系统录制工具,或者基于系统采集、编码、封装封装一套;

  • 视频编辑和特效处理:编辑主要是解复用--->解码--->逐帧处理--->编码--->复用的过程,逐帧处理用到视频上主要设计合成、滤镜等;音频主要是变声、声音融合,都是通用的技术,稍微体现差异的就是特效处理中与AI的结合,比如美颜、带眼镜等会用到图像检测,但是都不是门槛,也谈不上前沿探讨;

  • 直播:直播也同样有成熟的结局方案:采集推流,开源服务端以及成熟CDN,播放ijk,秒开之类的都是参数优化了;

  • 实时音视频:实时音视频开发成本比较高,主要的挑战是弱网对抗,3A处理等,由于不是通用协议,没有CDN,自己搭建机房成本高,而且不见得效果比第三方好,所以也是一件性价比比较低的事情。

  • 编解码:目前主流的还是H264,VP8,H264甚至都没有推开,限制编解码算法的主要是推广和兼容性,所以编解码器都是一些组织去搞,一个公司贸然去开发,风险很大。


3.9 AI


人类一直在追求更智能的机器,AI是未来,所以即使现在不够好,并且没有找到太多的落地场景,但是很多公司还在搞,尤其是ChatGPT的能力让大家惊讶,但是它仍然不是真正的“像人类一样”的智能。目前通用的AI主要有一下几个方向:




  1. 语音方向



    1. 前端信号处理

    2. 唤醒

    3. 语音识别

    4. 声纹

    5. TTS

    6. 作曲:抖音之前分享有过这方面实践和应用

    7. 基于特征的语音编码:比如谷歌推出的的lyra和SoundStream,Lyra的设计工作速率为3kbps,听力测试表明,在该比特率下,Lyra的性能优于任何其他编解码器,并优于Opus的8kbps,因此实现了60%以上的带宽削减。但是正如上面说的,编解码器的瓶颈主要还是在于标准的推广。




  2. 图像方向



    1. 检测

    2. 识别

    3. 图像比较(应用于UI自动化测试)




  3. 自然语言处理



    1. 智能问答

    2. 意图识别

    3. 文档纠错




  4. 风控




  5. 推荐




  6. 用户画像




  7. 元宇宙/数字人:数字人更像是一个AI的综合应用。




还有些特殊的特殊业务场景的特殊用户,比如房产领域:



  1. 户型解读(基于图像的特殊特征)

  2. 训练场


对于AI,移动端可以做哪些探索?回答这个问题先要搞明白哪些场景适配放在端上来做。Android官方给了一个决策的标准:


2023-01-31-23-57-36-image.png


上面提到的特别需要在端上应用的主要有:



  1. 唤醒

  2. 图像检测

  3. 语音编解码


可以放在端上应用的:



  1. ASR

  2. TTS

  3. 图像标签


基于这些场景端上的主要工作量是什么呢?模型训练大部分还是放在云端,端上就是加载模型,输入数据,展示输出结果,还有可能就是对引擎,框架做些优化:


2023-01-30-15-44-47-image.png


4. 总结


整体来看,整个移动端技术的发展可以说到了“山穷水尽”的地步,可挖掘的创新型内容不是很多了,大部分都是在现有体系维护和迭代。整体来看业务支撑还是主要的需求来源,车机、Iot也释放出一些机会,跨端、开发平台、性能优化、VR已趋于成熟,端智能落地的还是语音、图像这些通用的方向,深度结合业务的还有待挖掘。目前的AI解决的还是“决策”问题,从现在生成式到未来创造式通用的、”人类水平“的“智能”还有很长的路要走,谁也不能打包票说雷·库兹韦尔提出的奇点理论的“奇点”能不能到来,什么时候到来,智能的进化不止是算法层面的,还会收到算力的影响,像《流浪地球》系列中的MOSS机器人是因为量子计算机算力快的加成。


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

Android深思如何防止快速点击

前言 其实快速点击是个很好解决的问题,但是如何优雅的去解决确是一个难题,本文主要是记录一些本人通过解决快速点击的过程中脑海里浮现的一些对这个问题的深思。 1. AOP 可以通过AOP来解决这个问题,而且AOP解决的方法也很优雅,在开源上也应该是能找到对应的成熟...
继续阅读 »

前言


其实快速点击是个很好解决的问题,但是如何优雅的去解决确是一个难题,本文主要是记录一些本人通过解决快速点击的过程中脑海里浮现的一些对这个问题的深思。


1. AOP


可以通过AOP来解决这个问题,而且AOP解决的方法也很优雅,在开源上也应该是能找到对应的成熟框架。


AOP来解决这类问题其实是近些年一个比较好的思路,包括比如像数据打点,通过AOP去处理,也能得到一个比较优雅的效果。牛逼的人甚至可以不用别人写的框架,自己去封装就行,我因为对这个技术栈不熟,这里就不献丑了。

总之,如果你想快速又简单的处理这种问题,AOP是一个很好的方案


2. kotlin


使用kotlin的朋友有福了,kotlin中有个概念是扩展函数,使用扩展函数去封装放快速点击的操作逻辑,也能很快的实现这个效果。它的好处就是突出两个字“方便”


那是不是我用java,不用kotlin就实现不了kotlin这个扩展函数的效果?当然不是了。这让我想到一件事,我也有去看这类问题的文章,看看有没有哪个大神有比较好的思路,然后我注意到有人就说用扩展函数就行,不用这么麻烦。


OK,那扩展函数是什么?它的原理是什么?不就是静态类去套一层吗?那用java当然能实现,为什么别人用java去封装这套逻辑就是麻烦呢?代码不都是一样,只不过kotlin帮你做了而已。所以我觉得kotlin的扩展函数效果是方便,但从整体的解决思路上看,缺少点优雅。


3. 流


简单来说也有很多人用了Rxjava或者kotlin的flow去实现,像这种实现也就是能方便而已,在底层上并没有什么实质性的突破,所以就不多说了,说白了就是和上面一样。


4. 通过拦截


因为上面已经说了kt的情况,所以接下来的相关代码都会用java来实现。

通过拦截来达到防止快速点击的效果,而拦截我想到有2种方式,第一种是拦截事件,就是基于事件分发机制去实现,第二种是拦截方法。

相对而言,其实我觉得拦截方法会更加安全,举个场景,假如你有个页面,然后页面正在到计算,到计算完之后会显示一个按钮,点击后弹出一个对话框。然后过了许久,改需求了,改成到计算完之后自动弹出对话框。但是你之前的点击按钮弹出对话框的操作还需要保留。那就会有可能因为某些操作导致到计算完的一瞬间先显示按钮,这时你以迅雷不及掩耳的速度点它,那就弹出两次对话框。


(1)拦截事件


其实就是给事件加个判断,判断两次点击的时间如果在某个范围就不触发,这可能是大部分人会用的方式。


正常情况下我们是无法去入侵事件分发机制的,只能使用它提供的方法去操作,比如我们没办法在外部影响dispatchTouchEvent这些方法。当然不正常的情况下也许可以,你可以尝试往hook的方向去思考能不能实现,我这边就不思考这种情况了。

public class FastClickHelper {

private static long beforeTime = 0;
private static Map<View, View.OnClickListener> map = new HashMap<>();

public static void setOnClickListener(View view, View.OnClickListener onClickListener) {
map.put(view, onClickListener);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
long clickTime = SystemClock.elapsedRealtime();
if (beforeTime != 0 && clickTime - beforeTime < 1000) {
return;
}
beforeTime = clickTime;

View.OnClickListener relListener = map.get(v);
if (relListener != null) {
relListener.onClick(v);
}
}
});
}

}

简单来写就是这样,其实这个就和上面说的kt的扩展函数差不多。调用的时候就

FastClickHelper.setOnClickListener(view, this);

但是能看出这个只是针对单个view去配置,如果我们想其实页面所有view都要放快速点击,只不过某个view需要快速点击,比如抢东西类型的,那肯定不能防。所以给每个view单独去配置就很麻烦,没关系,我们可以优化一下

public class FastClickHelper {

private Map<View, Integer> map;
private HandlerThread mThread;

public void init(ViewGroup viewGroup) {
map = new ConcurrentHashMap<>();
initThread();
loopAddView(viewGroup);

for (View v : map.keySet()) {
v.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
int state = map.get(v);
if (state == 1) {
return true;
} else {
map.put(v, 1);
block(v);
}
}
return false;
}
});
}
}

private void initThread() {
mThread = new HandlerThread("LAZY_CLOCK");
mThread.start();
}

private void block(View v) {
// 切条线程处理
Handler handler = new Handler(mThread.getLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (map != null) {
map.put(v, 0);
}
}
}, 1000);
}

private void exclude(View... views) {
for (View view : views) {
map.remove(view);
}
}

private void loopAddView(ViewGroup viewGroup) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
if (viewGroup.getChildAt(i) instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) viewGroup.getChildAt(i);
map.put(vg, 0);
loopAddView(vg);
} else {
map.put(viewGroup.getChildAt(i), 0);
}
}
}

public void onDestroy() {
try {
map.clear();
map = null;
mThread.interrupt();
} catch (Exception e) {
e.printStackTrace();
}
}

}

我把viewgroup当成入参,然后给它的所有子view都设置,因为onclicklistener比较常用,所以改成了设置setOnTouchListener,当然外部如果给view设置了setOnTouchListener去覆盖我这的set,那就只能自己做特殊处理了。


在外部直接调用

FastClickHelper fastClickHelper = new FastClickHelper();
fastClickHelper.init((ViewGroup) getWindow().getDecorView());

如果要想让某个view不要限制快速点击的话,就调用exclude方法。这里要注意使用完之后释放资源,要调用onDestroy方法释放资源。


关于这个部分的思考,其实上面的大家都会,也基本是这样去限制,但是就是即便我用第二种代码,也要每个页面都调用一次,而且看起来,多少差点优雅。


首先我想的办法是在事件分发下发的过程去做处理,就是在viewgroup的dispatchTouchEvent或者onInterceptTouchEvent这类方法里面,但是我简单看了源码是没有提供方法出来的,也没有比较好去hook的地方,所以只能暂时放弃思考在这个下发流程去做手脚。


补充一下,如果你是自定义view,那肯定不会烦恼这个问题,但是你总不能所有的view都做成自定义的吧。


其次我想怎么能通过不写逻辑代码能实现这个效果,但总觉得这个方向不就是AOP吗,或者不是通过开发层面,在开发结束后想办法去注入字节码等操作,我觉得要往这个方向思考的话,最终的实现肯定不是代码层面去实现的。


(2)拦截方法


上面也说了,相对于拦截事件,假设如果都能实现的情况下,我更倾向于去拦截方法。


因为从这层面上来说,如果实现拦截方法,或者说能实现中断方法,那就不只是能做到防快速点击,而是能给方法去定制相对应的规则,比如某个方法在1秒的间隔内只能调用一次,这个就是防快速点击的效果嘛,比如某个方法我限制只能调一次,如果能实现,我就不用再额外写判断这个方法调用一次过后我设置一个布尔类型,然后下次调用再判断这个布尔类型来决定是否调用,


那现在是没办法实现拦截方法吗?当然有办法,只不过会十分的不优雅,比如一个方法是这样的。

public void fun(){
// todo 第1步
// todo 第2步
// todo ......
// todo 第n步
}

那我可以封装一个类,里面去封装一些策略,然后根据策略再去决定方法要不要执行这些步骤,那可能就会写成

public void fun(){
new FunctionStrategy(FunctionStrategy.ONLY_ONE, new CallBack{
@Override
public void onAction() {
// todo 第1步
// todo 第2步
// todo ......
// todo 第n步
}
})
}

这样就实现了,比如只调用一次,具体的只调用一次的逻辑就写在FunctionStrategy里面,然后第2次,第n次就不会回调。当然我这是随便乱下来表达这个思路,现实肯定不能这样写。首先这样写就很不优雅,其次也会存在很多问题,扩展性也很差。


那在代码层面还有其它办法拦截或者中断方法吗,在代码层还真有办法中断方法,没错,那就是抛异常,但是话说回来,你也不可能在每个地方都try-catch吧,不切实际。


目前对拦截方法或者中断方法,我是没想到什么好的思路了,但是我觉得如果能实现,对防止快速点击来说,肯定会是一个很好的方案。


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

Android-Deeplink跳转失败问题修复

Android Deeplink实现 在Android中,Deeplnk通过声明Activity的intent-filter来实现对自定义url访问事件的捕捉。在有道背单词的项目中,我们需要通过前端分享词单的方式,将词单分享给别人,并通过点击前端页面收藏按钮,...
继续阅读 »

Android Deeplink实现


在Android中,Deeplnk通过声明Activity的intent-filter来实现对自定义url访问事件的捕捉。在有道背单词的项目中,我们需要通过前端分享词单的方式,将词单分享给别人,并通过点击前端页面收藏按钮,实现调起客户端收藏词单的功能。

从前端通过自定义url的方式调起客户端这个功能原来一直都没有什么问题,直到最近有部分用户反馈在某些浏览器下无法调起。下面我们来看一下分析查找问题的方法以及如何解决。
转载请注明来源「Bug总柴」


检查客户端deeplink配置


在AndroidManifest.xml文件中,对路由Activity配置如下:

<activity
android:name=".deeplink.RouterActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTask"
android:theme="@style/Theme.Translucent">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="youdao.com"
android:scheme="recite"
android:pathPattern=".*"/>
</intent-filter>

<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".home.ui.MainActivity" />
</activity>

里面比较重要的部分是intent-filter中的data配置,检查后发现配置正常,可以正常拦截到 recite://youdao.com/.*的所有请求。

转到RouterActivity通过断点调试,发现并没有到达。从而可以确认是浏览器调起的时候发生了异常。


tips: adb 命令同样可以启动deeplink进行测试
adb_test.png


分析浏览器对deeplink处理


通过用户反馈,主要集中是在UC和华为自带的浏览器点击前端页面的【收藏词单】无法调起有道背单词

同时我们在chrome上面发现通过deeplink只有第一次会跳转到应用,往后几次都是没有任何相应,确实有点百思不得其解。

经过查找资料,发现了chrome的一个对Android Intent处理的介绍

Android Intents with Chrome

里面提到



One scenario is launching an app when the user lands on a page, which you can achieve by embedding an iframe in the page with a custom URI-scheme set as the src, as follows: . This works in the Chrome for Android browser, version 18 and earlier. It also works in the Android browser, of course.




The functionality has changed slightly in Chrome for Android, versions 25 and later. It is no longer possible to launch an Android app by setting an iframe's src attribute. For example, navigating an iframe to a URI with a custom scheme such as paulsawesomeapp:// will not work even if the user has the appropriate app installed. Instead, you should implement a user gesture to launch the app via a custom scheme, or use the “intent:” syntax described in this article.



翻译一下,大概的意思就是之前通过没有用户主动操作就打开app的行为在chrome25版本及之后会被禁止。开发者必须通过用户操作来触发跳转应用的行为。目前chrome的版本都已经68了,证明这个规则已经由来已久。抱着试试看的姿态,开始查找是否是前端的代码有问题。
通过chrome inspect,捕捉到前端代码果然有一处疑似iframe的使用
ebc8daf14130474bbd69103bf4e6ff5d_ac466235c97c6e9fb45c8820addbce1d.jpg


018ff33c02e047c59196534a3a28ef8b_e15f0d6e314113b2260119e917400191.jpg


随后经过对前端代码debug,果然有走了这段逻辑
ca286076d8594b55b7db5847e6d031b6_c0169d9d3acbb13b8835c5a0a8aa028f.jpg


证据确凿,可以找前端大神反馈了。经过了解,确实是之前有改动过这部分的代码,使用了iframe来处理deeplink的打开。处理的办法也相对简单,将iframe换成href来做跳转处理就可以了。


测试


最后我们对国内的浏览器试了一下deeplink是否生效


UC浏览器


会弹出一个应用打开提醒,如果用户本次没有【允许】操作,则浏览器下次会拦截打开应用行为,没有任何提醒,不知道这是一个bug还是故意为之。点击【允许】后可以跳转应用
Screenshot_20180818-112921.jpg


QQ浏览器


同样会弹出应用打开题型,如果用户本次没有【打开】,下次用户操作还是会继续提醒。点击【打开】后可以跳转应用
Screenshot_20180818-113231.jpg


360浏览器


行为与QQ浏览器类似,每次都会提醒
Screenshot_20180818-113459.jpg


猎豹浏览器


行为与QQ浏览器类似,每次都会提醒
Screenshot_20180818-113718.jpg


一加系统默认浏览器


行为与QQ浏览器类似,每次都会提醒
Screenshot_20180818-113921.jpg


搜狗浏览器


没有提醒,直接跳转到app


chrome


行为与搜狗浏览器类似,没有提醒,直接跳转app


测试结果除了UC浏览器第一次不点击跳转之后会跳转不了之外,
其他浏览器跳转app问题得到解决。


结语


通过这次查deeplink跳转的问题,收获了两点知识。



  • 一个是前端使用iframe来处理deeplink跳转会有问题

  • 二个是除了采用
"scheme://host/path"

这种deeplink方式之外,还可以采用

"intent://about/#Intent;action=[string];scheme=[string];package=[string];S.browser_fallback_url=[encoded_full_url];end"

的方式来触发应用intent的请求访问。


同时,在处理deeplink的规则里面,体会到了一条原则:



  • 最短路径处理原则


意思就是刚开始的时候,deeplink处理的逻辑要从根目录开始进行。比如有一个收藏词单的需求,没有使用最短路径原则可能会设计成这样



recite://youdao.com/bookId?&action=collect



对应的处理是如果action为collect就收藏词单。这个时候需求如果改成默认进来不需要收藏就非常尴尬了。因为对于旧版本而已,只认有action=collect才会处理,那就意味这如果想对默认的recite://youdao.com/bookId只是查看不收藏的需求,对于旧版本就没办法实现,会出现兼容性问题。

而最短路径处理原则,意思就是在开始的时候,尽量对最短的路径行为进行处理,具体到上面的例子,对于收藏某个词单的需求,我们可以设计deeplink为



recite://youdao.com/bookId?&action=collect



然后我们对 recite://youdao.com/bookId以及recite://youdao.com/bookId?&action=collect 都处理成收藏词单。上线之后,如果想修改默认参数行为,就可以直接改对 recite://youdao.com/bookId 的处理,这样对于旧版本仍然是可以执行的收藏行为,对于新版本就可以对应新的逻辑


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

一点点编译优化

正文 经过一段时间的工作(摸鱼划水),从几个很小的地方给大家介绍下我是如何提升编译构建速度的,但是本次分享内容还是主要针对当前阿逼的工程架构,不一定对你们有帮助哦。 FileWalker 剪枝 + 多线程优化 我们工程内会在编译和同步阶段首先获取到整个工程的模...
继续阅读 »

正文


经过一段时间的工作(摸鱼划水),从几个很小的地方给大家介绍下我是如何提升编译构建速度的,但是本次分享内容还是主要针对当前阿逼的工程架构,不一定对你们有帮助哦。


FileWalker 剪枝 + 多线程优化


我们工程内会在编译和同步阶段首先获取到整个工程的模型,之后计算出每个模块的version版本。之前我们通过java.nio.file.FileVisitor来进行工程文件遍历的操作。同样文件展开的api也是可以进行剪枝的,但是由于是用groovy写的,我还是不太喜欢。


本次优化我们采用了kotlin的file相关的walkTopDown(可以快速的从上到下的遍历一个文件树)语法糖。然后通过其中的onEnterdsl进行剪枝,然后我们可以通过filter进行第二波过滤,筛选出我们实际要访问的目录。最后再进行一次foreach。

fun File.walkFileTree(action: (File) -> Unit) {
walkTopDown().onEnter {
# 对于不需要的目录进行剪枝
val value = if (!(it.isDirectory && (it.name == "build" || it.name == "src"))) {
!it.isHidden
} else {
false
}
value
}.filter {
val value = if (this == it) {
false
} else {
if (it.isDirectory) {
val build = File(it, "build.gradle")
build.exists()
} else {
false
}
}
value
}.forEachIndexed { _, file ->
action.invoke(file)
}
}


优化效果如下,原本没有剪枝的版本,我们本机进行一次FileWalker需要1分钟左右的时间。使用剪枝的版本之后,我们可以把时间优化到2s左右的时间。我们跳过了些什么?































格式是否跳过
隐藏文件夹
build
src
文件
其他文件夹

通过上述的剪枝,我们可以减少非常非常多的文件遍历操作,在完成同样的能力的情况下,可以大大的加快文件的遍历操作。


另外,我们会获取对应文件下的git commit sha值,然后作为该模块的version版本,而这个操作也是有几百毫秒的耗时,而我们工程大概有800+这样的模块,所以如果按照同步的方式去执行,就会变得耗时了。


而优化方案就比较简单了,我们通过线程池提供的invokeAll方法,并发执行完之后再继续向下执行就可以完成该优化了。

        Executors.newCachedThreadPool().invokeAll(callableList)

整体优化下来,原来在CI上一次FileWalker需要1mins,因为CI的机器足够牛逼,所以优化完仅仅只需要3.5s就可以完成整个工程的遍历以及获取对应git commit sha值的操作。


修改前:


企业微信截图_9431b7d7-1599-4f3f-a916-276cde61f71d.png


修改后:


企业微信截图_9127d691-a792-41e2-bc38-62f08697ea64.png


skip 一些非必须task


在AGP的打包流程中,会插入很多预检查的任务,比如类似kotlin版本检查, compileSdk版本检查等等任务。而这些任务即时不执行,也并不会影响本次打包任务,还是可以打出对应的apk产物的。当然前提是编译没有啥问题的情况下。


我们仔细观察了下apk打包下的所有的task,并观察了下task任务耗时情况,找到了几个看起来并没有什么实际用处的任务。


企业微信截图_912eebd8-2104-4a10-bb5a-6d60637e922f.png


接下来就是如何去关闭这个任务了,其实方式还是比较简单的。我们主要用字符串的形式去获取到这个Task,然后把enable设置成false就可以了。但是前提是这个Task的输出并不会影响到后续的Task就行了。另外这几个应该是高apg新增的task,7.x才出现的低版本的是没有的。

afterEvaluate {
def metaDebugTask = tasks.findByName("checkApinkDebugAarMetadata")
def debugCheck = System.getenv().containsKey("ENABLE_APINK_DEBUG_CHECK")
if( metaDebugTask != null && !debugCheck) {
metaDebugTask.setEnabled(false)
}
def preCheck = tasks.findByName("checkApinkDebugDuplicateClasses")
if( preCheck != null && !debugCheck) {
preCheck.setEnabled(false)
}
}

企业微信截图_99e7c774-4e22-40be-bbac-73080faa80c0.png


这样我们就可以在一个构建中优化掉大概1min30s的时间了。这些手段相对来说都比较简单,另外我们还可以考虑把一些不互相依赖的任务从线性执行变成并行执行。可以参考gradle的Worker相关api。



worker_api



二进制产物发布


原始的baseversion是基于文件内容的md5 生成的一个全局统一版本号,然后再结合仓库内的gitsha生成二进制缓存。但是由于大仓内的代码量越来越大,所以一旦变更baseversion,需要消耗大概80min左右的时间重新生成所有的二进制缓存。


虽然但是,其实并没有这个必要全部模块都进行一次发布。方法签名出现问题,我们只需要让对应的模块重编即可。所以这种全局性的baseversion 就需要进行迭代了。需要让baseversion被拆解成多个,然后进行混合生成一个新值,底层改动可以影响到上层,而最上层模块也可以具备独立更新的能力。

# 后续该文件改动不会导致整个baseVersion变更
# 基于文件路径匹配的规则,可以给每个文件路径设置一个version,但是由于工程之间存在依赖,所以可以合并多个baseVersion到一起
# 测试结果如下,framework变更80min comm 变更 50min app上变更10min
# 后续会配合a8检查,直接通知各位那些模块的方法签名检查不通过,之后直接修改局部version版本就好了


# 全局默认混入所有 如果非必要情况下 别改这个版本号 !!!!!!!
- name: global
path:
version: 1
# app目录缓存

- name: app
path: app
version: 1
mixin: [ framework,common ]
# framework基础,该层目录变更之后所有向上的全部需要变更 非必要也最好不要改
- name: framework
path: framework
version: 2
mixin:
# comm 模块,该层目录变更之后所有向上的全部需要变更 非必要也最好不要改
- name: common
path: common
version: 2
mixin: [ framework ]

通过定义出一个新的yaml文件,我们可以定义出namepath代表相对路径,version表示版本号,mixin代表混合入别的name,而相对底层改动情况下会影响到上层模块重编。


这样,我们就可以在app下进行独立的版本号增加,让app目录下的模块进行一次重编,从而解决一部分方法签名问题导致的上层库缓存刷新。


测试结果大概如下:























目录耗时情况
global80min
common50min
app10min

结尾


以下仅代表个人看法哦,我觉得编译优化还是要从本工程实际情况出发,你的工具箱内的工具要足够多,要先知道哪些东西是慢的你才会有思考的去进行一些优化,而不是很盲目的进行尝试。


另外优化不应该破坏整个工程的情况,我们不应该魔改整个编译流程,最好就是通过最小的手段去进行一些微量的优化,小步慢跑的进行一些对应的优化。


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

我就问Zygote进程,你到底都干了啥

ZYGOTE 前言 OK,这是Android系统启动的第二篇文章。第二篇我们讲解一个我们一直都在用,但是却很少提起的进程---Zygote。 提到Zygote可能了解一些的小伙伴会说,它是分裂进程用的。没错它最大的作用的确是分裂进程,但是它除了分裂进程外还做了...
继续阅读 »

ZYGOTE


前言


OK,这是Android系统启动的第二篇文章。第二篇我们讲解一个我们一直都在用,但是却很少提起的进程---Zygote
提到Zygote可能了解一些的小伙伴会说,它是分裂进程用的。没错它最大的作用的确是分裂进程,但是它除了分裂进程外还做了什么呢。
还是老规矩,让我们抱着几个问题来看文章。最后在结尾,再对问题进行思考回复。



  1. 你能大概描述一遍Zygote的启动流程吗

  2. 我们为什么可以执行java代码,和zygote有什么关系

  3. Zygote到底都做了哪些事情


另外,我最近也看了一些写底层的文章。要么就是言简意赅到只知道Zygote的作用,但是完全不知道如何实现的,就像是背作文一样。要么是全篇都是代码,又臭又长,让人完全没有看下去的动力。


不过作为一个读者,太长的我可能完全不想看,太短的又真的完全就是被课文一点都不明白原理。


因此,本篇文章,仍旧会引入一些代码,方便大家通过代码方便记忆。但是又会将代码进行精简,以免给大家造成过度疲劳。我们的目的都是希望用最少的时间,能掌握更多的知识。


读源码时:不要过于纠结细节,不要过于纠结细节,不要过于纠结细节!重要的事情说三遍!!!


OK,让我们进入正题瞅瞅Zygote到底是个什么东西。




1.C++还是Java


选这个当标题当然是有原因的。我们知道的是Android系统启动的时候,运行的是Linux内核,执行的是C++代码。这是一个很有趣的事情。


因为我们在写App的时候,AndroidStudio默认给我们创建的都是Activity.java,而选择的语言,要么是Java要么就是Kotlin


启动时候运行的是C++代码,应用层却可以使用Java代码,这到底是为什么呢?其实这就是Zygote的功劳之一


2.Native层


Init进程创建Zygote时,会调用app_main.cpp的main() 方法。此时它依旧运行的是C++代码。我们通过下面的代码,来看下它到底做了什么。


这里它做的最关键的一件事就是启动AndroidRuntime(Android运行时)。另外这里需要注意的是 start方法 中的 "com.android.internal.os.ZygoteInit" 这个类似于全类名。他到底是做什么的?别着急,大概2分钟后,你可能就会得到答案。


Zygote的新手村:app_main.cpp

int main(int argc, char* const argv[])
{
// 创建Android运行时对象
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
// 代码省略...

// 调用AppRuntime.start方法,
// 而AppRuntime是AndroidRuntime的子类,并且没有重写start方法
// 因此调用的是AndroidRuntime的start方法
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
}

精简后的代码告诉我们,这里一共就做了两件事,第一件创建AppRuntime,第二件调用start方法


不过,AppRuntimeAndroidRuntime的子类,他没有重写start方法,因此这里调用的是 AndroidRuntime的start() 方法。


奇迹的诞生地:AndroidRuntime:


这里我依旧只保留关键代码,源码关键注释也进行了保留。由下方代码看出这里做了三件改变命运的事情。



  1. startVM -- 启动Java虚拟机

  2. startReg -- 注册JNI

  3. 通过JNI调用Java方法,执行com.android.internal.os.ZygoteInit 的 main 方法
/*
* Start the Android runtime.
*/
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
/* start the virtual machine */
JNIEnv* env;
if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {
return;
}

/*
* Register android functions.
*/
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}
/*
* Start VM. This thread becomes the main thread of the VM, and will
* not return until the VM exits.
*/
jmethodID startMeth = env->GetStaticMethodID(startClass, "main","([Ljava/lang/String;)V");
env->CallStaticVoidMethod(startClass, startMeth, strArray);
}

有了JVM,注册了JNI,我们就可以执行Java代码了。


这里我个人建议不要过于纠结细节,比如JVM是如何创建的,JNI是如何注册的。如果感兴趣的童鞋,可以下载源码,到app_main.cpp里进行查看。这里就不进行赘述了。否则代码量太大,适得其反。




3.Java层


命运的十字路口:ZygoteInit.java:


之所以说是命运的十字路口,因为Zygote会在这里创建SystemServer,但是二者却走向了截然不同的道路。


从这里就开始执行Java代码了,当然这些Java代码是运行在JVM中的。


让我们通过代码来看一下,到底都做了些什么。

class ZygoteInit{

/**
* This is the entry point for a Zygote process. It creates the Zygote server, loads resources,
* and handles other tasks related to preparing the process for forking into applications.
* This process is started with a nice value of -20 (highest priority).
*/
// 上面是源码中的注释,小伙伴们可以自行翻译一下。
// 创建了ZygoteServer,加载资源,并且介绍了这个进程的优先级是最高的-20.
public static void main(String argv[]) {
ZygoteServer zygoteServer = null;

// 1. 预加载资源,常用的:resource,class,library等在此处进行加载
preload(bootTimingsTraceLog);

// 2. 创建ZygoteServer,实际是一个Socket用来进行跨进程间通信用的。
zygoteServer = new ZygoteServer(isPrimaryZygote);

// 3. fork出SystemServer进程,这个进程会创建AMS,ATMS,WMS,电池服务等一切只有你想不到没有它做不到的服务
forkSystemServer(abiList, zygoteSocketName, zygoteServer);

// 4.里面是一个while(true)循环,等待接收AMS创建进程的消息,类似于handler中的Looper.loop()
zygoteServer.runSelectLoop(abiList);
}
}

上面代码里的注释基本说明了ZygoteInit都干了啥。下面会再稍微总结下:



  1. 创建了ZygoteServer:这是一个Socket相关的服务,目的是进行跨进程通信。

  2. 预加载preload:预加载相关的资源。

  3. 创建SystemServer进程:通过forkSystemServer分裂出了两个进程,一个Zygote进程,一个SystemServer进程。而且由于是分裂的,所以新分裂出来的进程也拥有虚拟机,也能调用JNI,也拥有预加载的资源,也会执行后续的代码。

  4. 执行runSelectLoop():内部是一个while(true)循环,等待AMS创建新的进程的消息。(想想Looper.loop())




4. 戛然而止


没错就是这么突然,Zygote的故事到这就结束了。至于它是如何创建SystemServer,如何去创建App那就是后面的故事了。所以你还记得我的问题嘛?我替你总结一下Zygote到底做了什么:



  1. 创建虚拟机

  2. 注册JNI

  3. 回调Java方法ZygoteInit.java 的main方法,从这儿开始运行Java代码

  4. 创建ZygoteServer,内部包含Socket用于跨进程通信

  5. 预加载class,resource,library等相关资源

  6. fork 出了 SystemServer进程。他俩除了返回的pid不同,剩下一模一样。通过返回值不同来决定剩下的代码如何运行。这个留待后续进行讲解。

  7. 进入while(true)循环等待,等待AMS创建进程的消息(类似于Looper.loop()




5. 多说一句


这个系列才刚刚进行到第二章,我尽量让文章不是特别长的情况下,既有代码整理思路,又有总结方便记忆。代码是源码中摘抄的省略了很大一部分,只能保证执行顺序。有兴趣的小伙伴可以下载源码进行查看。

其实最近我看了很多文章和视频,最后却选择写文章来对知识进行总结。归根结底就是因为一句话,好记性不如烂笔头。要是真的想快速记住,真的需要亲自查看一下源码。翻源码的同时,也是在不知不觉中锻炼你阅读源码的能力。 万一以后让你学习一个新的库,你也知道大概该怎么看。


都已经到这儿了,就稍微厚个脸皮,希望各位看官,能够点个赞加个关注。毕竟一篇文章可能就要耗费几个小时的时间,上一篇文章就几个赞,感谢这几个小伙伴,让我有继续写下去的动力。 真心感谢你们。


另外,文章中有哪里出错,请及时指出。毕竟各位才是真正的大佬。希望各位Androider,可以一起取暖,度过这个互联网寒冬!加油各位!!!




6. Zygote功能图


zygote.png


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

代码优雅之道——如何干掉过多的if else

1、前言 注意标题是过多的,所以三四个就没必要干掉了。实际开发中我们经常遇到判断条件很多的情况,比如下图有20多种情况,不用想肯定是要优化代码的,需要思考的是如何去优化? 网上很多说用switch case啊,首先不比较if else与switch case...
继续阅读 »

1、前言


注意标题是过多的,所以三四个就没必要干掉了。实际开发中我们经常遇到判断条件很多的情况,比如下图有20多种情况,不用想肯定是要优化代码的,需要思考的是如何去优化?



网上很多说用switch case啊,首先不比较if else与switch case效率问题的,只从代码整洁度来看二者没啥区别啊!我们这里更重要的是代码整洁度问题,为什么呢?来看下文的比较。


2、If else与switch case效率真的差距很大么?


网上有两种见解:


第一种是说switch…case会生成一个跳转表来指示实际的case分支的地址,而这个跳转表的索引号与switch变量的值是相等的。从而,switch…case不用像if…else那样遍历条件分支直到命中条件,而只需访问对应索引号的表项从而到达定位分支的目的。简单来说就是以空间换时间


第二种是说二者效率上差距并不大



于是我们自己去体验一下,不存在复杂业务逻辑,仅仅比较两种方式的效率:

    @Test
void contextLoads() {
testIf(100000);
System.gc();
testSwitch(100000);
}

private void testIf(Integer param) {
long start = System.currentTimeMillis();
for (int i = 0; i < param; i++) {
if (i == param-1){
System.out.println("if判断100000次");
}
}
long end = System.currentTimeMillis();
long total = end - start;
System.out.println("Test消耗时间:" + total);
}

private void testSwitch(Integer param){
long start = System.currentTimeMillis();
for (int i = 0; i < param; i++) {
switch (i){
case 99999:
System.out.println("switch判断100000次");
break;
}
}
long end = System.currentTimeMillis();
long total = end - start;
System.out.println("Test消耗时间:" + total);
}


可见差距并不大。而情况太多的时候谁还会去用if else和switch case呢?下面还是对两种方式的使用场景做简单的分析:


if else能够把复杂的逻辑关系表达得清晰、易懂,包容了程序执行的各种情况。


switch不适合业务系统的实际复杂需求,业务不断的变更迭代,一更改需求,条件的复杂度高了,switch无力处理。switch经常忘记写break,估计很多人一不小心就忘记写了。switch…case只能处理case为常量的情况。当情况不大于5种并且单一变量的值(如枚举),此时我们就可以使用switch,它的可读性比if条件更清晰。


除了上述说到枚举的这种场景,建议使用switch,其他个人愚见:只要情况不大于5种就直接使用if else


3、策略+工厂模式


上述说到情况较少时并且业务逻辑不复杂的使用if else可以让代码清晰明了。当每种情况对应的业务逻辑复杂时,建议使用策略+工厂模式。这里我们举个栗子:厂家每个季度要举行不同的活动,我们使用策略工厂模式来实现


策略接口

public interface Strategy {

/**
* 处理各种活动
* @return
*/
String dealActivity();
}

然后春夏秋冬四季活动类实现该接口


@Service
public class SpringActivity implements Strategy{
@Override
public String dealActivity() {
return "春季活动逻辑";
}
}

策略类工厂

public class StrategyFactory {
public static Strategy execute(Integer levelCode){
Strategy strategy = null;
switch (levelCode){
case 1:
strategy = new SpringActivity();
break;
case 2:
strategy = new SummerActivity();
break;
case 3:
strategy = new AutumnActivity();
break;
case 4:
strategy = new WinterActivity();
break;
default:
throw new IllegalArgumentException("活动编号错误");
}
return strategy;
}
}

然后在service层中传入对应的编码即可 ,我这里省略了service

@RestController
public class TestController {

@PostMapping("/dealActivity")
public String dealActivity(Integer code){
Strategy strategy = StrategyFactory.execute(1);
return strategy.dealActivity();
}
}


上述已经干掉了if else ,后续季度活动调整去修改对应活动策略类中逻辑即可。缺点:如果情况比这多,那么策略类会越来越多,也就是所谓的策略类膨胀,并且没有****没有一个地方可以俯视整个业务逻辑。


4、Map+函数式接口


将上述策略类全部作为方法

@Service
public class ActivityStrategyService {

public String dealSpringActivity(){
return "春季活动逻辑";
}

public String dealSummerActivity() {
return "夏季活动逻辑";
}

public String dealAutumnActivity() {
return "秋季活动逻辑";
}

public String dealWinterActivity() {
return "冬季活动逻辑";
}
}

再写个活动Service

@Service
public class ActivityService {

@Autowired
private ActivityStrategyService activityStrategyService;

@FunctionalInterface
interface ActivityFunction<A>{
//这里可以传参啊,我这里举例用不上参数
//String dealActivity(A a);
String dealActivity();
}

private final Map<Integer, ActivityFunction> strategyMap = new HashMap<>();

/**
* 初始化策略
*/
@PostConstruct
public void initDispatcher(){
strategyMap.put(1,()->activityStrategyService.dealSpringActivity());
strategyMap.put(2, ()-> activityStrategyService.dealSummerActivity());
strategyMap.put(3, ()-> activityStrategyService.dealAutumnActivity());
strategyMap.put(4, ()-> activityStrategyService.dealWinterActivity());
}

public String dealActivity(Integer code){
ActivityFunction<Integer> function = strategyMap.get(code);
//这里防止活动编号没匹配上,可以使用断言来判断从而抛出统一异常
return function.dealActivity();
}

}

改变Controller

@RestController
public class TestController {

@Autowired
private ActivityService activityService;

@PostMapping("/dealActivity")
public String dealActivity(Integer code){
// Strategy strategy = StrategyFactory.execute(1);
// return strategy.dealActivity();
return activityService.dealActivity(code);
}
}

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

Android渠道包自动更新

一、背景 转转集团旗下有多款APP产品,随着业务发展,各APP发版频率变高。在持续交付的背景下,渠道包更新存在以下几个效率问题: (1)Android渠道包提交应用市场审核,工作重复&人工成本高   (2)公司目前存在多个APP、需...
继续阅读 »

一、背景


转转集团旗下有多款APP产品,随着业务发展,各APP发版频率变高。在持续交付的背景下,渠道包更新存在以下几个效率问题:


(1)Android渠道包提交应用市场审核,工作重复&人工成本高  


(2)公司目前存在多个APP、需更多人支持,有培训成本


(3)每次发版需要人工通知项目成员渠道包审核进度 


  针对以上问题,我们设计开发了渠道包自动更新后台,用来解决渠道更新的效率问题。


二、方案调研


1、基于业务现状,做了技术调研和逻辑抽象


  不同APP支持的渠道不同,不同渠道更包api不同,如下图:


图片


针对以上调研结果,我们将通用的逻辑统一封装开发,将差异点进行配置,做到灵活配置可扩展。


2、整体的实现方案演变


初期方案,每个应用市场单独提审(需要先选择物料,选好物料后上传包文件,文件上传成功后再点击提交审核),多个应用市场需要重复该操作。


图片


上线运行了一段时间后,发现存在一些问题:单个市场提交步骤繁琐、多个应用市场需要分开多次提交。这些步骤是重复且可简化的,因此我们又对提审的过程做了封装,提供批量上传的入口,简化交互过程,做到一键提审。以下是当前运行的第二版方案:


图片


第二版方案上线后,提审同学只需要在入口处选择要更新的应用市场,然后一键上传全部物料,再点击提审按钮即可提审成功。代码内部会处理具体的逻辑,比如:根据配置规则将物料匹配到对应市场、自动匹配包文件进行提审。


三、方案设计


自动上传包含以下核心模块:



  • APP管理:支持配置多个APP信息,包括转转、找靓机、采货侠等

  • 包管理:支持下载不同渠道,不同版本的包

  • 物料管理:包括历史物料的选择,和新增物料的存储(icon、市场截图)

  • 提交审核:包括包下载、物料下载,支持按照APP配置账号密码提交审核

  • 消息提醒:对提交的结果和审核的结果进行消息通知


图片


实现效果:


提审前信息确认,选择APP,可选择单个或者多个渠道,系统自动选择包地址,用户选择物料后可一键提审多应用市场。操作简单便捷,使用成本低


图片


提审后发送消息通知,便于各方了解渠道的审核结果,对审核异常信息进行及时干预。同时自动存储不同版本的审核记录,方便后续分析。


图片


四、总结


渠道包自动更新功能,节省了大量的提交审核人力成本,打通了Android整体的持续交付过程,降低了人工学习成本。之后我们也会针对各种体验问题进行不断的改进和更新~


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

理解Kotlin中的reified关键字

标题:理解Kotlin中的reified关键字 摘要:本文介绍了Kotlin中的reified关键字的用途,特点以及如何在实际项目中应用。我们将通过实例详细了解reified的功能以及如何在内联函数中使用它。 正文: 什么是reified关键字? 在Kotli...
继续阅读 »

标题:理解Kotlin中的reified关键字


摘要:本文介绍了Kotlin中的reified关键字的用途,特点以及如何在实际项目中应用。我们将通过实例详细了解reified的功能以及如何在内联函数中使用它。


正文:


什么是reified关键字?


在Kotlin中,reified是一个特殊的关键字,用于修饰内联函数中的类型参数。这使得在函数内部可以访问类型参数的具体类型。通常情况下,由于类型擦除(type erasure),在运行时是无法直接获取泛型类型参数的具体类型的。reified关键字解决了这个问题。


使用reified关键字的条件


要使用reified关键字,需要遵循以下几点:



  1. 函数必须是内联的(使用inline关键字修饰)。

  2. 类型参数前需要加上reified关键字。


示例:reified关键字的用法


下面是一个使用reified关键字的简单示例:

inline fun <reified T> checkType(value: Any) {
if (value is T) {
println("Value is of type T.")
} else {
println("Value is NOT of type T.")
}
}

fun main() {
val stringValue = "Hello, Kotlin!"
val intValue = 42

checkType<String>(stringValue) // 输出 "Value is of type T."
checkType<String>(intValue) // 输出 "Value is NOT of type T."
}

在这个示例中,我们定义了一个内联函数checkType,它接受一个reified类型参数T。然后,我们使用is关键字检查传入的value变量是否为类型T。在main函数中,我们用不同的类型参数调用checkType函数来验证它的功能。


获取类型参数的Java类


当你使用reified关键字修饰一个内联函数的类型参数时,你可以通过T::class.java获取类型参数对应的Java类。这在需要访问泛型类型参数的具体类型时非常有用,比如在反射操作中。


下面是一个简单的例子:

import kotlin.reflect.KClass

inline fun <reified T : Any> getClass(): KClass<T> {
return T::class
}

inline fun <reified T : Any> getJavaClass(): Class<T> {
return T::class.java
}

fun main() {
val stringKClass = getClass<String>()
println("KClass for String: $stringKClass") // 输出 "KClass for String: class kotlin.String"

val stringJavaClass = getJavaClass<String>()
println("Java class for String: $stringJavaClass") // 输出 "Java class for String: class java.lang.String"
}

在这个示例中,我们定义了两个内联函数,getClassgetJavaClass,它们都接受一个reified类型参数TgetClass函数返回类型参数对应的KClass对象,而getJavaClass函数返回类型参数对应的Java类。在main函数中,我们用String类型参数调用这两个函数,并输出结果。


注意事项


需要注意的是,reified关键字不能用于非内联函数,因为它们的类型参数在运行时会被擦除。此外,reified类型参数不能用于普通类和接口,只能用于内联函数。


总结


Kotlin中的reified关键字允许我们在内联函数中访问类型参数的具体类型。它在需要访问泛型类型参数的场景中非常有用,例如在反射操作中。本文通过实例介绍了如何使用reified关键字,并讨论了相关注意事项。希望这些示例能够帮助您更好地理解和应用reified关键字。


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

既然有Map了,为什么还要有Redis?

一、同样是缓存,用map不行吗? Redis可以存储几十个G的数据,Map行吗? Redis的缓存可以进行本地持久化,Map行吗? Redis可以作为分布式缓存,Map只能在同一个JVM中进行缓存; Redis支持每秒百万级的并发,Map行吗? Redis有...
继续阅读 »

一、同样是缓存,用map不行吗?



  1. Redis可以存储几十个G的数据,Map行吗?

  2. Redis的缓存可以进行本地持久化,Map行吗?

  3. Redis可以作为分布式缓存,Map只能在同一个JVM中进行缓存;

  4. Redis支持每秒百万级的并发,Map行吗?

  5. Redis有过期机制,Map有吗?

  6. Redis有丰富的API,支持非常多的应用场景,Map行吗?



二、Redis为什么是单线程的?



  1. 代码更清晰,处理逻辑更简单;

  2. 不用考虑各种锁的问题,不存在加锁和释放锁的操作,没有因为可能出现死锁而导致的性能问题;

  3. 不存在多线程切换而消耗CPU;

  4. 无法发挥多核CPU的优势,但可以采用多开几个Redis实例来完善;


三、Redis真的是单线程的吗?



  1. Redis6.0之前是单线程的,Redis6.0之后开始支持多线程;

  2. Redis内部使用了基于epoll的多路服用,也可以多部署几个Redis服务器解决单线程的问题;

  3. Redis主要的性能瓶颈是内存和网络;

  4. 内存好说,加内存条就行了,而网络才是大麻烦,所以Redis6内存好说,加内存条就行了;

  5. 而网络才是大麻烦,所以Redis6.0引入了多线程的概念,

  6. Redis6.0在网络IO处理方面引入了多线程,如网络数据的读写和协议解析等,需要注意的是,执行命令的核心模块还是单线程的。


四、Redis优缺点


1、优点



  1. Redis是KV数据库,MySQL是关系型数据库,Redis速度更快;

  2. Redis数据操作主要在内存中,MySQL主要将数据存储在硬盘,Redis速度更快;

  3. Redis同样支持持久化(RDB+AOF),Redis支持将数据异步将内存的数据持久化到硬盘上,避免Redis宕机出现数据丢失的问题;

  4. Redis性能极高,读的速度是110000次/秒,写的速度是81000次/秒;

  5. Redis数据类型丰富,不仅支持KV键值对,还支持list、set、zset、hash等数据结构的存储;

  6. Redis支持数据的备份,即master-slave模式的数据备份;

  7. Redis支持简单的事务,操作满足原子性;

  8. Redis支持读写分离,分担读的压力;

  9. Redis支持哨兵模式,实现故障的自动转移;

  10. 单线程操作,避免了频繁的上下文切换;

  11. 采用了非阻塞I/O多路复用机制,性能卓越;


2、缺点



  1. 数据存储在内存,容易造成数据丢失;

  2. 存储容量受内存的限制,只能存储少量的常用数据;

  3. 缓存和数据库双写一致性问题;

  4. 用于缓存时,容易出现内存穿透、缓存击穿、缓存雪崩的问题;

  5. 修改配置文件后,需要进行重启,将硬盘中的数据同步到内存中,消耗的时间较长,而且数据同步的时间里Redis不能提供服务;


五、Redis常见业务场景



  1. Redis是基于内存的nosql数据库,可以通过新建线程的形式进行持久化,不影响Redis单线程的读写操作

  2. 通过list取最新的N条数据

  3. 模拟类似于token这种需要设置过期时间的场景

  4. 发布订阅消息系统

  5. 定时器、计数器

  6. 缓存加速、分布式会话、排行榜、分布式计数器、分布式锁;

  7. Redis支持事务、持久化、LUA脚本、发布/订阅、缓存淘汰、流技术等特性;


六、Redis常见数据类型



1、String


(1)String简介


String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M。


(2)应用场景


① 作为缓存数据库


在Java管理系统体系中,大多数都是用MySQL存储数据,redis作为缓存,因为Redis具有支撑高并发的特性,通常能起到加速读写和降低数据库服务器压力的作用,大多数请求都会先请求Redis,如果Redis中没有数据,再请求MySQL数据库,然后再缓存到Redis中,以备下次使用。



② 计数器


Redis字符串中有一个命令INCR key,incr命令会对值进行自增操作,比如CSDN的文章阅读,视频的播放量,都可以通过Redis来计数,每阅读一次就+1,同时将这些数据异步存储到MySQL数据库中,降低MySQL服务器的写入压力。


③ 共享session


在分布式系统中,用户每次请求一般会访问不同的服务器 ,这就会导致session不同步的问题,这时,一般会使用Redis来解决这个问题,将session存入Redis,使用的时候从Redis中取出就可以了。


④ 分布式锁



  1. setnx key value,加锁

  2. del key,释放锁


(3)key操作命令



(4)set key value


SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]



  1. EX seconds,设置过期时间,单位秒

  2. PX milliseconds,设置过期时间,单位毫秒

  3. EXAT timestamp-seconds,设置过期时间,以秒为单位的UNIX时间戳

  4. PXAT timestamp-milliseconds,设置过期时间,以毫秒为单位的UNIX时间戳

  5. NX,键不存在的时候设置键值

  6. XX,键存在的时候设置键值

  7. KEEPTTL,保留设置前指定键的生存时间

  8. GET,返回指定键原本的值,若键不存在返回nil


备注:


命令不区分大小写,而key是区分大小写的。


help @类型:查看当前类型相关的操作命令。


Since the SET command options can replace SETNX, SETEX, PSETEX, GETSET, it is possible that in future versions of Redis these commands will be deprecated and finally removed。


(5)同时设置多个键值


(6)获取指定区间范围内的值


getrange、setrange。


(7)数值增减



  1. INCR key,递增数字

  2. INCRBY key increment,增加指定的数值递增

  3. DECR key,递减数值

  4. DECRBY key decrement,指定指定的数值递减


(8)获取字符串的长度,内容追加



  1. STRLEN key,获取值的长度

  2. APPEND key value,内容追加


2、List


(1)List 列表简介


List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。


列表的最大长度为 2^32 - 1,也即每个列表支持超过 40 亿个元素。


主要功能有push/pop,一般用在栈、队列、消息队列等场景。



  1. left、right都可以插入添加;

  2. 如果键不存在,创建新的链表;

  3. 如果键存在,新增内容;

  4. 如果值全部移除,对应的键也会消失;


它的底层是双向链表,对两端的操作性能很高,通过索引下标操作中间的节点,性能会较差。


(2)应用场景


① 消息队列


使用 lpush + rpop或者 rpush + lpop实现消息队列,Redis还支持阻塞操作,在弹出元素的时候使用阻塞命令来实现阻塞队列。



② 作为栈使用


使用 lpush+lpop或者 rpush+rpop实现栈。



③ 文章列表


(3)常用命令



3、Hash


(1)hash简介


Hash 是一个键值对(key - value)集合,value也是一个hash,相当于 Map<String,Map<Object,Object>>


(2)常用场景


由于特殊的数据结构,hash一般作为存储bean使用,String+JSON的数据结构存储特定的应用场景。



(3)常用命令




4、Set


(1)Set类型简介


Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。


一个集合最多可以存储 2^32-1 个元素。概念和数学中个的集合基本类似,可以交集,并集,差集等等,所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。


(2)应用场景


① 相同好友可见


在朋友圈场景中,对于点赞、评论的功能,通过交集实现相同还有可见的功能。


② 共同关注、共同喜好


③ 抽奖功能


(3)常用命令



5、Zset


(1)Zset 类型简介


Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。


有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。


zset k1 score1 v1 score2 v2


(2)应用场景


① 排行榜


通过score来记录点赞数,然后根据score进行排序,实现排行榜的功能。


② 延迟消息队列


订单系统,下单后需要在15分钟内进行支付操作,否则自动取消订单。


将下单后15分钟后的时间作为score,订单作为value存入Redis,消费者轮询去消费,如果消费的大于等于score,则取消该订单。


(3)Zset常用命令



6、BitMap


(1)Bitmap简介


Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。


(2)应用场景


由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。


① 签到统计


② 判断用户是否登录


③ 统计连续学习打卡的人


(3)BitMap常用命令



7、BitField


通过bitfield命令可以一次性操作多个比特位,它会执行一系列操作并返回一个响应数组,这个数组中的元素对参数列表中的相应操作的执行结果。


8、HyperLogLog


(1)HyperLogLog简介


Redis HyperLogLog 是 Redis 2.8.9 版本新增的数据类型,是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。但要注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。


所以,简单来说 HyperLogLog 提供不精确的去重计数。


HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的内存空间总是固定的、并且是很小的。


在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。


(2)应用场景


百万级网页 UV 计数


(3)常用命令



  1. pfadd key element,添加元素

  2. pfcount key,返回指定HyperLogLog的基数的估算值;

  3. pfmerge destkey sourcekey,将多个HyperLogLog合并成一个HyperLogLog;


9、GEO


(1)GEO简介


Redis GEO 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。


在日常生活中,我们越来越依赖搜索“附近的餐馆”、在打车软件上叫车,这些都离不开基于位置信息服务(Location-Based Service,LBS)的应用。LBS 应用访问的数据是和人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围,GEO 就非常适合应用在 LBS 服务的场景中。


(2)应用场景


高德地图、滴滴打车等定位软件。


(3)常用命令



10、Stream


(1)Stream简介


Redis Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。



在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:



  • 发布订阅模式,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;

  • List 实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一 ID。


基于以上问题,Redis 5.0 便推出了 Stream 类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。


(2)应用场景


消息队列


(3)常用命令



七、总结


Redis是一个key-value存储系统,支持10种数据类型,总结了为何要用Redis替代map作为程序缓存、Redis为什么是单线程的、Redis的优缺点、Redis的常用场景,做了一次Redis的快速入门。


最后说一句(求关注,别白嫖我)


如果这篇文章对您有所帮助,或者有所启发的话,您的关注和点赞是我坚持写作最大的动力。


关注公众号:【哪吒编程】,在公众号中回复【掘金】,获取Java学习资料、电子书;回复【星球】加入Java学习星球,陪伴学习,共同优秀。


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

大哥,这是并发不是并行,Are You Ok?

多线程概述 基础概念 进程和线程 进程是程序运行资源分配的最小单位 进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘IO等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。进程是具有一定独立功能...
继续阅读 »

多线程概述


file


基础概念


进程和线程



进程是程序运行资源分配的最小单位



进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘IO等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。


进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的、静态的,进程是活的、动态的。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。



线程是CPU调度的最小单位,必须依赖于进程而存在



线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。



线程无处不在



任何一个程序都必须要创建线程,特别是Java不管任何程序都必须启动一个main函数的主线程; Java Web开发里面的定时任务、定时器、JSP和 Servlet、异步消息处理机制,远程访问接口RM等,任何一个监听事件, onclick的触发事件等都离不开线程和并发的知识。


CPU核心数和线程数的关系


多核心:也指单芯片多处理器( Chip Multiprocessors,简称CMP),CMP是由美国斯坦福大学提出的,其思想是将大规模并行处理器中的SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个CPU同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理


多线程: Simultaneous Multithreading.简称SMT.让同一个处理器上的多个线程同步执行并共享处理器的执行资源。


核心数、线程数:目前主流CPU都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是1:1对应关系,也就是说四核CPU一般拥有四个线程。但 Intel引入超线程技术后,使核心数与线程数形成1:2的关系


file


CPU时间片轮转机制


file


为什么感受不到CPU线程数的限制


我们平时在开发的时候,感觉并没有受cpu核心数的限制,想启动线程就启动线程,哪怕是在单核CPU上,为什么?这是因为操作系统提供了一种CPU时间片轮转机制。


时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。


什么是CPU轮转机制


百度百科对CPU时间片轮转机制原理解释如下:


如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结来,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾


时间片长度


时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切( processwitch),有时称为上下文切换( context switch),需要5ms,再假设时间片设为20ms,则在做完20ms有用的工作之后,CPU将花费5ms来进行进程切换。CPU时间的20%被浪费在了管理开销上了。


为了提高CPU效率,我们可以将时间片设为5000ms。这时浪费的时间只有0.1%。但考虑到在一个分时系统中,如果有10个交互用户几乎同时按下回车键,将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的进程不得不等待5s才获得运行机会。多数用户无法忍受一条简短命令要5才能做出响应,同样的问题在一台支持多道程序的个人计算机上也会发


结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了CPU效率:而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100ms通常是一个比较合理的折衷。


在CPU死机的情况下,其实大家不难发现当运行一个程序的时候把CPU给弄到了100%再不重启电脑的情况下,其实我们还是有机会把它KILL掉的,我想也正是因为这种机制的缘故。


澄清并行和并发


我们举个例子,如果有条高速公路A上面并排有8条车道,那么最大的并行车辆就是8辆此条高速公路A同时并排行走的车辆小于等于8辆的时候,车辆就可以并行运行。CPU也是这个原理,一个CPU相当于一个高速公路A,核心数或者线程数就相当于并排可以通行的车道;而多个CPU就相当于并排有多条高速公路,而每个高速公路并排有多个车道。


当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少?离开了单位时间其实是没有意义的。


俗话说,一心不能二用,这对计算机也一样,原则上一个CPU只能分配给一个进程,以便运行这个进程。我们通常使用的计算机中只有一个CPU,也就是说只有一颗心,要让它一心多用同时运行多个进程,就必须使用并发技术。实现并发技术相当复杂,最容易理解的是“时间片轮转进程调度算法”。


综合来说:


并发:指应用能够交替执行不同的任务,比如单CPU核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已.


并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行


两者区别:一个是交替执行,一个是同时执行.


file
感觉上是同时发生的,但是微观上还是有区别的,并行是同意时刻发生的,并发是同一时刻交替执行


file


高并发的意义



由于多核多线程的CPU的诞生,多线程、高并发的编程越来越受重视和关注。多线程可以给程序带来如下好处。



1. 充分利用CPU的资源


从上面的CPU的介绍,可以看的出来,现在市面上没有CPU的内核不使用多线程并发机制的,特别是服务器还不止一个CPU,如果还是使用单线程的技术做思路,明显就out了。因为程序的基本调度单元是线程,并且一个线程也只能在一个CPU的一个核的一个线程跑,如果你是个i3的CPU的话,最差也是双核心4线程的运算能力:如果是一个线程的程序的话,那是要浪费3/4的CPU性能:如果设计一个多线程的程序的话,那它就可以同时在多个CPU的多个核的多个线程上跑,可以充分地利用CPU,减少CPU的空闲时间,发挥它的运算能力,提高并发量。


就像我们平时坐地铁一样,很多人坐长线地铁的时候都在认真看书,而不是为了坐地铁而坐地铁,到家了再去看书,这样你的时间就相当于有了两倍。这就是为什么有些人时间很充裕,而有些人老是说没时间的一个原因,工作也是这样,有的时候可以并发地去做几件事情,充分利用我们的时间,CPU也是一样,也要充分利用。


2. 加快响应用户的时间


比如我们经常用的迅雷下载,都喜欢多开几个线程去下载,谁都不愿意用一个线程去下载,为什么呢?答案很简单,就是多个线程下载快啊。


我们在做程序开发的时候更应该如此,特别是我们做互联网项目,网页的响应时间若提升1s,如果流量大的话,就能增加不少转换量。做过高性能web前端调优的都知道,要将静态资源地址用两三个子域名去加载,为什么?因为每多一个子域名,浏览器在加载你的页面的时候就会多开几个线程去加载你的页面资源,提升网站的响应速度。多线程,高并发真的是无处不在。


3. 可以使你的代码模块化,异步化,简单化


例如我们实现电商系统,下订单和给用户发送短信、邮件就可以进行拆分,将给用户发送短信、邮件这两个步骤独立为单独的模块,并交给其他线程去执行。这样既增加了异步的操作,提升了系统性能,又使程序模块化,清晰化和简单化。


多线程应用开发的好处还有很多,大家在日后的代码编写过程中可以慢慢体会它的魅力。


多线程程序需要注意事项


1. 线程之间的安全性


从前面的章节中我们都知道,在同一个进程里面的多线程是资源共享的,也就是都可以访问同一个内存地址当中的一个变量。例如:若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的:若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。


2. 线程之间的死锁


为了解决线程之间的安全性引入了Java的锁机制,而一不小心就会产生Java线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。


假如线程A获得了刀,而线程B获得了叉。线程A就会进入阻塞状态来等待获得叉,而线程B则阻塞来等待线程A所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生


3. 线程太多了会将服务器资源耗尽形成死机当机


线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及CPU的“过渡切换”,造成系统的死机,那么我们该如何解决这类问题呢?


某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,因为每个线程都可能希望有一个这样的资源。如果线程数相当大,或者某个资源的侯选线程数远远超过了可用的资源数则最好使用资源池。一个最好的示例是数据库连接池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返回池中。资源池也称为资源库。


多线程应用开发的注意事项很多,希望大家在日后的工作中可以慢慢体会它的危险所在。


作者:博学谷_狂野架构师
链接:https://juejin.cn/post/7197622529599324215
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

电视剧里的代码真能运行吗?

大家好,欢迎来到 Crossin的编程教室 ! 前几天,后台老有小伙伴留言“爱心代码”。这不是Crossin很早之前发过的内容嘛,怎么最近突然又被人翻出来了?后来才知道 ,原来是一部有关程序员的青春偶像剧《点燃我,温暖你》在热播,而剧中有一段关于期中考试要用程...
继续阅读 »

大家好,欢迎来到 Crossin的编程教室 !


前几天,后台老有小伙伴留言“爱心代码”。这不是Crossin很早之前发过的内容嘛,怎么最近突然又被人翻出来了?后来才知道


,原来是一部有关程序员的青春偶像剧《点燃我,温暖你》在热播,而剧中有一段关于期中考试要用程序画一个爱心的桥段。


于是出于好奇,Crossin就去看了这一集(第5集,不用谢)。这一看不要紧,差点把刚吃的鸡腿给喷出来--槽点实在太多了!


忍不住做了个欢乐吐槽向的代码解读视频,在某平台上被顶到了20个w的浏览,也算蹭了一波人家电视剧的热度吧……


下面是图文版,给大家分析下剧中出现的“爱心”代码,并且来复刻一下最后男主完成的酷炫跳动爱心。


剧中代码赏析


1. 首先是路人同学的代码:



虽然剧中说是“C语言期中考试”,但这位同学的代码名叫 draw2.py,一个典型的 Python 文件,再结合截图中的 pen.forward、pen.setpos 等方法来看,应该是用 turtle 海龟作图库来画爱心。那效果通常是这样的:

import turtle as t
t.color('red')
t.setheading(50)
t.begin_fill()
t.circle(-100, 170)
t.circle(-300, 40)
t.right(38)
t.circle(-300, 40)
t.circle(-100, 170)
t.end_fill()
t.done()



而不是剧中那个命令行下用1组成的不规则的图形。


2. 然后是课代表向路人同学展示的优秀代码:



及所谓的效果:



这确实是C语言代码了,但文件依然是以 .py 为后缀,并且 include 前面没有加上 #,这显然是没法运行的。


里面的内容是可以画出爱心的,用是这个爱心曲线公式:



然后遍历一个15*17的方阵,计算每个坐标是在曲线内还是曲线外,在内部就输出#或*,外部就是-


用python改写一下是这样的:

for y in range(9, -6, -1):
for x in range(-8, 9):
print('*##*'[(x+10)%4] if (x*x+y*y-25)**3 < 25*x*x*y*y*y else '-', end=' ')
print()

效果:



稍微改一下输出,还能做出前面那个全是1的效果:

for y in range(9, -6, -1):
for x in range(-8, 9):
print('1' if (x*x+y*y-25)**3 < 25*x*x*y*y*y else ' ', end=' ')
print()


但跟剧中所谓的效果相去甚远。


3. 最后是主角狂拽酷炫D炸天的跳动爱心:



代码有两个片段:




但这两个片段也不C语言,而是C++,且两段并不是同一个程序,用的方法也完全不一样。


第一段代码跟前面一种思路差不多,只不过没有直接用一条曲线,而是上半部用两个圆形,下半部用两条直线,围出一个爱心。



改写成 Python 代码:

size = 10
for x in range(size):
for y in range(4*size+1):
dist1 = ((x-size)**2 + (y-size)**2) ** 0.5
dist2 = ((x-size)**2 + (y-3*size)**2) ** 0.5
if dist1 < size + 0.5 or dist2 < size + 0.5:
print('V', end=' ')
else:
print(' ', end=' ')
print()

for x in range(1, 2*size):
for y in range(x):
print(' ', end=' ')
for y in range(4*size+1-2*x):
print('V', end=' ')
print()

运行效果:



第二段代码用的是基于极坐标的爱心曲线,是遍历角度来计算点的位置。公式是:



计算出不同角度对应的点坐标,然后把它们连起来,就是一个爱心。

from math import pi, sin, cos
import matplotlib.pyplot as plt
no_pieces = 100
dt = 2*pi/no_pieces
t = 0
vx = []
vy = []
while t <= 2*pi:
vx.append(16*sin(t)**3)
vy.append(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t))
t += dt
plt.plot(vx, vy)
plt.show()

效果:



代码中循环时用到的2π是为了保证曲线长度足够绕一个圈,但其实长一点也无所谓,即使 π=100 也不影响显示效果,只是相当于同一条曲线画了很多遍。所以剧中代码里写下35位小数的π,还被女主用纸笔一字不落地抄写下来,实在是让程序员无法理解的迷惑行为。



但不管写再多位的π,上述两段代码都和最终那个跳动的效果差了五百只羊了个羊。


跳动爱心实现


作为一个总是在写一些没什么乱用的代码的编程博主,Crossin当然也不会放过这个机会,下面就来挑战一下用 Python 实现最终的那个效果。


1. 想要绘制动态的效果,必定要借助一些库的帮助,不然代码量肯定会让你感动得想哭。这里我们将使用之前 羊了个羊游戏 里用过的 pgzero 库。然后结合最后那个极坐标爱心曲线代码,先绘制出曲线上离散的点。

import pgzrun
from math import pi, sin, cos

no_p = 100
dt = 2*3/no_p
t = 0
x = []
y = []
while t <= 2*3:
x.append(16*sin(t)**3)
y.append(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t))
t += dt

def draw():
screen.clear()
for i in range(len(x)):
screen.draw.filled_rect(Rect((x[i]*10+400, -y[i]*10+300), (4, 4)), 'pink')

pgzrun.go()


2. 把点的数量增加,同时沿着原点到每个点的径向加一个随机数,并且这个随机数是按照正态分布来的(半个正态分布),大概率分布在曲线上,向曲线内部递减。这样,就得到这样一个随机分布的爱心效果。

...
no_p = 20000
...
while t <= 2*pi:
l = 10 - abs(random.gauss(10, 2) - 10)
x.append(l*16*sin(t)**3)
y.append(l*(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)))
t += dt
...


3. 下面就是让点动起来,这步是关键,也有一点点复杂。为了方便对于每个点进行控制,这里将每个点自定义成了一个Particle类的实例。


从原理上来说,就是给每个点加一个缩放系数,这个系数是根据时间变化的正弦函数,看起来就会像呼吸的节律一样。

class Particle():
def __init__(self, pos, size, f):
self.pos = pos
self.pos0 = pos
self.size = size
self.f = f

def draw(self):
screen.draw.filled_rect(Rect((10*self.f*self.pos[0] + 400, -10*self.f*self.pos[1] + 300), self.size), 'hot pink')

def update(self, t):
df = 1 + (2 - 1.5) * sin(t * 3) / 8
self.pos = self.pos0[0] * df, self.pos0[1] * df

...

t = 0
def draw():
screen.clear()
for p in particles:
p.draw()

def update(dt):
global t
t += dt
for p in particles:
p.update(t)


4. 剧中爱心跳动时,靠中间的点波动的幅度更大,有一种扩张的效果。所以再根据每个点距离原点的远近,再加上一个系数,离得越近,系数越大。

class Particle():
...
def update(self, t):
df = 1 + (2 - 1.5 * self.f) * sin(t * 3) / 8
self.pos = self.pos0[0] * df, self.pos0[1] * df


5. 最后再用同样的方法画一个更大一点的爱心,这个爱心不需要跳动,只要每一帧随机绘制就可以了。

def draw():
...
t = 0
while t < 2*pi:
f = random.gauss(1.1, 0.1)
x = 16*sin(t)**3
y = 13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)
size = (random.uniform(0.5,2.5), random.uniform(0.5,2.5))
screen.draw.filled_rect(Rect((10*f*x + 400, -10*f*y + 300), size), 'hot pink')
t += dt * 3


合在一起,搞定!



总结一下,就是在原本的基础爱心曲线上加上一个正态分布的随机量、一个随时间变化的正弦函数和一个跟距离成反比的系数,外面再套一层更大的随机爱心,就得到类似剧中的跳动爱心效果。


但话说回来,真有人会在考场上这么干吗?


除非真的是超级大学霸,不然就是食堂伙食太好--


吃太饱撑的……


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

Android如何设计一个全局可调用的ViewModel对象?

很多时候我们需要维护一个全局可用的ViewModel,因为这样可以维护全局同一份数据源,且方便使用协程绑定App的生命周期。那如何设计全局可用的ViewModel对象? 一、思路 viewModel对象是存储在ViewModelStore中的,那么如果我们创建...
继续阅读 »

很多时候我们需要维护一个全局可用的ViewModel,因为这样可以维护全局同一份数据源,且方便使用协程绑定App的生命周期。那如何设计全局可用的ViewModel对象?


一、思路


viewModel对象是存储在ViewModelStore中的,那么如果我们创建一个全局使用的ViewModelStore并且在获取viewModel对象的时候从它里面获取就可以了。


viewModel是通过ViewModelProviderget方法获取的,一般是ViewModelProvider(owner: ViewModelStoreOwner, factory: Factory).get(ViewModel::class.java)


如何将ViewModelProviderViewModelStore关联起来? 纽带就是ViewModelStoreOwner, ViewModelStoreOwner是一个接口,需要实现getViewModelStore()方法,而该方法返回的就是ViewModelStore:

public interface ViewModelStoreOwner {
/**
* Returns owned {@link ViewModelStore}
*
* @return a {@code ViewModelStore}
*/
@NonNull
ViewModelStore getViewModelStore(); //返回一个ViewModelStore
}

让某个类实现这个接口,重写方法返回我们定义的ViewModelStore就可以了。


至于上面ViewModelProvider构造方法的第二个参数Factory是什么呢?


源码中提供了二种Factory,一种是NewInstanceFactory,一种是AndroidViewModelFactory,它们的主要区别是:




  • NewInstanceFactory创建ViewModel时,会为每个Activity或Fragment创建一个新的ViewModel实例,这会导致ViewModel无法在应用程序的不同部分共享数据。(ComponentActivity源码getDefaultViewModelProviderFactory方法)




  • AndroidViewModelFactory可以访问应用程序的全局状态,并且ViewModel实例可以在整个应用程序中是共享的。




根据我们的需求,需要用的是AndroidViewModelFactory。


二、具体实现


1、方式一:可以全局添加和获取任意ViewModel


定义Application,Ktx.kt文件

import android.app.Application

lateinit var appContext: Application

fun setApplicationContext(context: Application) {
appContext = context
}

定义全局可用的ViewModelOwner实现类

object ApplicationScopeViewModelProvider : ViewModelStoreOwner {

private val eventViewModelStore: ViewModelStore = ViewModelStore()

override fun getViewModelStore(): ViewModelStore {
return eventViewModelStore
}

private val mApplicationProvider: ViewModelProvider by lazy {
ViewModelProvider(
ApplicationScopeViewModelProvider,
ViewModelProvider.AndroidViewModelFactory.getInstance(appContext)
)
}

fun <T : ViewModel> getApplicationScopeViewModel(modelClass: Class<T>): T {
return mApplicationProvider.get(modelClass)
}
}

定义一个ViewModel通过StateFlow定义发送和订阅事件的方法

class EventViewModel : ViewModel() {

private val mutableStateFlow = MutableStateFlow(0)

fun postEvent(state: Int) {
mutableStateFlow.value = state
}

fun observeEvent(scope: CoroutineScope? = null, method: (Int) -> Unit = { _ -> }) {
val eventScope = scope ?: viewModelScope
eventScope.launch {
mutableStateFlow.collect {
method.invoke(it)
}
}
}
}

定义一个调用的类



object FlowEvent {

//发送事件
fun postEvent(state: Int) {
ApplicationScopeViewModelProvider.getApplicationScopeViewModel(EventViewModel::class.java)
.postEvent(state)
}

//订阅事件
fun observeEvent(scope: CoroutineScope? = null, method: (Int) -> Unit = { _ -> }) {
ApplicationScopeViewModelProvider.getApplicationScopeViewModel(EventViewModel::class.java)
.observeEvent(scope, method)
}
}

测试代码如下:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//打印协程名称
System.setProperty("kotlinx.coroutines.debug", "on")

FlowEvent.observeEvent {
printMsg("MainActivity observeEvent before :$it")
}
//修改值
FlowEvent.postEvent(1)


FlowEvent.observeEvent {
printMsg("MainActivity observeEvent after :$it")
}

}

}

//日志
内容:MainActivity observeEvent before :0 线程:main @coroutine#1
内容:MainActivity observeEvent before :1 线程:main @coroutine#1
内容:MainActivity observeEvent after :1 线程:main @coroutine#2

2、方式二:更方便在Activity和Fragment中调用


定义Application,让BaseApplication实现ViewModelStoreOwner

//BaseApplication实现ViewModelStoreOwner接口
class BaseApplication : Application(), ViewModelStoreOwner {

private lateinit var mAppViewModelStore: ViewModelStore
private var mFactory: ViewModelProvider.Factory? = null

override fun onCreate() {
super.onCreate()
//设置全局的上下文
setApplicationContext(this)
//创建ViewModelStore
mAppViewModelStore = ViewModelStore()

}

override fun getViewModelStore(): ViewModelStore = mAppViewModelStore

/**
* 获取一个全局的ViewModel
*/
fun getAppViewModelProvider(): ViewModelProvider {
return ViewModelProvider(this, this.getAppFactory())
}

private fun getAppFactory(): ViewModelProvider.Factory {
if (mFactory == null) {
mFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(this)
}
return mFactory as ViewModelProvider.Factory
}
}

Ktx.kt文件也有变化,如下

lateinit var appContext: Application

fun setApplicationContext(context: Application) {
appContext = context
}

//定义扩展方法
inline fun <reified VM : ViewModel> Fragment.getAppViewModel(): VM {
(this.requireActivity().application as? BaseApplication).let {
if (it == null) {
throw NullPointerException("Application does not inherit from BaseApplication")
} else {
return it.getAppViewModelProvider().get(VM::class.java)
}
}
}

//定义扩展方法
inline fun <reified VM : ViewModel> AppCompatActivity.getAppViewModel(): VM {
(this.application as? BaseApplication).let {
if (it == null) {
throw NullPointerException("Application does not inherit from BaseApplication")
} else {
return it.getAppViewModelProvider().get(VM::class.java)
}
}
}

BaseActivityBaseFragment中调用上述扩展方法

abstract class BaseActivity: AppCompatActivity() {

//创建ViewModel对象
val eventViewModel: EventViewModel by lazy { getAppViewModel() }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

}
}
abstract class BaseFragment: Fragment() {

//创建ViewModel对象
val eventViewModel: EventViewModel by lazy { getAppViewModel() }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

}
}

测试代码

class MainActivity : BaseActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//打印协程名称
System.setProperty("kotlinx.coroutines.debug", "on")

eventViewModel.observeEvent {
printMsg("MainActivity observeEvent :$it")
}

findViewById<AppCompatButton>(R.id.bt).setOnClickListener {
//点击按钮修改值
eventViewModel.postEvent(1)
//跳转到其他Activity
Intent(this, TwoActivity::class.java).also { startActivity(it) }
}
}

}
class TwoActivity : BaseActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_two)

eventViewModel.observeEvent {
printMsg("TwoActivity observeEvent :$it")
}
}
}

日志

内容:MainActivity observeEvent :0 线程:main @coroutine#1
内容:MainActivity observeEvent :1 线程:main @coroutine#1
内容:TwoActivity observeEvent :1 线程:main @coroutine#2

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

2023年的现代安卓开发

免责声明这是一篇来自我的观点和专业经验的文章, 考虑到了安卓开发者社区的不同意见, 也不断回顾了谷歌为安卓提供的指南.我必须明确指出, 有一些非常有趣的工具, 模式和架构, 我可能没有提到, 但这并不意味着它们不能成为开发Android应用程序的其他有趣的选择...
继续阅读 »


免责声明

这是一篇来自我的观点和专业经验的文章, 考虑到了安卓开发者社区的不同意见, 也不断回顾了谷歌为安卓提供的指南.

我必须明确指出, 有一些非常有趣的工具, 模式和架构, 我可能没有提到, 但这并不意味着它们不能成为开发Android应用程序的其他有趣的选择.

什么是Android?

Android是一个基于Linux内核的开源操作系统, 由谷歌开发.它被广泛用于各种设备, 包括智能手机, 平板电脑, 电视和智能手表.

目前, 安卓是世界上移动设备使用最多的操作系统;根据statcounter的报告, 以过去12个月为样本, 安卓的市场份额为71.96%.

接下来, 我将提到一个工具, 库, 架构, 指南和其他实用工具的清单, 我认为这些工具对在Android上构建现代应用程序非常重要.

Kotlin ❤️

0_piQN_I004o_ugTCN.webp

Kotlin是由JetBrains开发的一种编程语言.由谷歌推荐, 谷歌在2017年5月正式宣布了它(见这里的出版物).它是一种现代编程语言, 具有与Java的兼容性, 可以在JVM上运行, 这使得它在Android应用开发中的采用速度非常快.

无论你是否是安卓新手, 你都应该考虑将Kotlin作为你的首选, 不要逆水行舟🏊🏻 😎, 谷歌在2019年谷歌I/O大会上宣布了这一做法.使用Kotlin, 你将能够使用现代语言的所有功能, 包括Coroutines的强大实力和使用为Android生态系统开发的现代库.

官方kotlin文档在这里

Jetpack Compose 😍

0_kG-9BQIyUm8MblpZ.webp

Jetpack Compose是Android推荐的用于构建本地UI的现代工具包.它简化并加速了Android上的UI开发.

Jetpack Compose是Android Jetpack库的一部分, 使用Kotlin编程语言来轻松创建本地用户界面.同时, 它还与其他Android Jetpack库(如LiveData和ViewModel)集成, 使其更容易建立反应性和可维护的Android应用程序.

Jetpack Compose的一些主要特点包括:

  1. 声明式UI.
  2. 可定制的小工具.
  3. 易于与现有代码集成.
  4. 实时预览.
  5. 改进性能.

资源:

Jetpack Compose文档

Android Jetpack

0_3LHozcwxQYiKVhPG.webp

Jetpack是一套库, 帮助开发人员遵循最佳实践, 减少模板代码, 并编写在不同的Android版本和设备上一致运行的代码, 以便开发人员可以专注于他们关心的代码.

它的一些最常用的工具是:

Material Design

1_D3MK4AocfnktSnVGe4rg0g.webp

Material Design是一个由指导方针, 组件和工具组成的适应性系统, 支持用户界面设计的最佳实践.在开源代码的支持下, Material Design简化了设计师和开发人员之间的合作, 并帮助团队快速建立漂亮的产品.

Material Design网站

Material Design得到了来自谷歌的设计师和开发人员的支持, 它将使我们有一个指南来为我们的Android, Flutter和Web的UI/UX工作.

目前, Material Design的最后一个版本是3, 你可以看到更多这里.

Clean Architecture

0_KgFh38gn_lDEuoB9.webp

Clean Architecture的概念是由Robert C. Martin提出的.它的基础是通过将软件划分为不同的层来分离责任.

特点:

  1. 独立于框架.
  2. 可测试.
  3. 独立于用户界面.
  4. 独立于数据库.
  5. 独立于任何外部代理.

依赖性规则

博文Clean Architecture对依赖性规则做了很好的描述.

使得这个架构发挥作用的首要规则是依赖性规则.这个规则说, 源代码的依赖关系只能指向内部.内圈的任何东西都不能知道外圈的任何东西.特别是, 外圈中声明的东西的名字不能被内圈中的代码所提及.这包括, 函数, 类, 变量或任何其他命名的软件实体.

博文Clean Architecture

安卓系统中的Clean Architecture

  • 表示层: Activities, Fragments, ViewModels, 其他视图组件.
  • 领域层: 用例, 实体, 仓库, 其他的域组件.
  • 数据层: 存储库的实现, 映射器, DTO等.

Presentation层的架构模式

架构模式是一种更高层次的策略, 旨在帮助设计一个软件架构, 其特点是在一个可重复使用的框架内为常见的架构问题提供解决方案.架构模式类似于设计模式, 但它们的规模更大, 解决的是更多的全局性问题, 如系统的整体结构, 组件之间的关系以及数据的管理方式.

在Presentation层中, 我们有一些架构模式, 其中我想强调以下几点:

  • MVVM
  • MVI

我不想逐一解释, 因为在互联网上你可以找到太多的相关信息.

此外, 你还可以看看应用架构指南

0_QJ56TjhdXPcQweAk.webp

依赖注入

依赖注入是一种软件设计模式, 它允许客户端从外部来源获得其依赖, 而不是自己创建.它是一种在对象和其依赖关系之间实现反转控制(IoC)的技术.

模块化

模块化是一种软件设计技术, 它允许你将一个应用程序划分为独立的模块, 每个模块都有自己的功能和责任.

0_NNUw83lZ228t5yLD.webp

模块化的好处

可重复使用: 通过拥有独立的模块, 它们可以在应用程序的不同部分甚至在其他应用程序中重复使用.

严格的可见性控制: 模块使你能够轻松地控制你向你的代码库的其他部分暴露的内容.

可定制的交付Google Play的特性交付使用应用程序捆绑的高级功能, 允许你有条件地或按需交付你的应用程序的某些功能.

可扩展性: 通过独立的模块, 功能可以被添加或删除而不影响应用程序的其他部分.

易于维护: 通过将应用程序分为独立的模块, 每个模块都有自己的功能和责任, 更容易理解和维护代码.

易于测试: 通过拥有独立的模块, 它们可以被隔离测试, 这使得检测和修复错误变得容易.

架构的改进: 模块化有助于改善应用程序的架构, 使代码有更好的组织和结构.

改进协作: 通过独立的模块, 开发人员可以同时工作在应用程序的不同部分, 不受干扰.

构建时间: 一些Gradle功能, 如增量构建, 构建缓存或并行构建, 可以利用模块化来提高构建性能.

更多内容请见官方文档.

网络

序列化

在本节中, 我想提及我认为的两个重要工具: MoshiRetrofit一起广泛使用, 以及Kotlin Serialization, 这是Jetbrain的Kotlin团队的赌注.

MoshiKotlin Serialization是Kotlin和Java的两个序列化/反序列化库, 允许你将对象转换成JSON或其他序列化格式, 反之亦然.两者都提供了一个用户友好的界面, 为在移动和桌面应用程序中使用而优化.Moshi主要专注于JSON序列化, 而Kotlin Serialization则支持各种序列化格式, 包括JSON.

图像加载

要从互联网上加载图片, 有几个第三方库可以帮助你处理这个过程.图片加载库为你做了很多繁重的工作;它们既能处理缓存(这样你就不会多次下载图片), 也能处理网络逻辑以下载图片并在屏幕上显示.

Reactivity / Thread Management反应性/线程管理

1_jm3wnFbTBvURFtLlcQAYRg.webp

当我们谈论反应式编程和异步进程时, 我们的第一选择是Kotlin Coroutines;由于suspend函数Flow, 我们可以满足所有这些需求.然而, 我认为在这一节中值得强调的是RxJava的重要性, 即使在Android应用程序的开发中.对于我们这些已经在Android上工作了几年的人来说, 我们知道RxJava是一个非常强大的工具, 它有非常多的功能来处理数据流.今天我仍然认为RxJava是一个值得考虑的替代方案.

本地存储

在构建移动应用程序时, 很重要的一点是要有在本地持久化数据的能力, 比如一些会话数据或缓存数据等等.根据你的应用程序的需要, 选择合适的存储方式是很重要的.我们可以存储非结构化的数据, 如键值或结构化的数据, 如数据库.请记住, 这一点并没有提到我们可用的所有本地存储类型(如文件存储), 只是提到了允许我们保存数据的工具.

1_rILOhf6I_dtR-ircBkKvtQ.webp

建议:

测试

R8优化

R8是默认的编译器, 它将你项目的Java字节码转换为在Android平台上运行的DEX格式.它是一个帮助我们混淆和减少应用程序代码的工具, 通过缩短类和其属性的名称, 消除项目内未使用的代码和资源.想了解更多, 请查看Android文档中关于缩减, 混淆和优化你的应用程序.

1_KzoahZDnZ25lv5ydi39JSw.webp

  • 代码缩减
  • 资源缩减
  • 混淆
  • 优化

Play特性交付

Google Play的应用服务模式, 称为动态交付, 使用Android App Bundles为每个用户的设备配置生成和提供优化的APK, 因此用户只下载运行你的应用所需的代码和资源.

Android文档

0_FitxQQeB7XC7MVUq.webp

自适应布局

0_MHJwbEuvl8cXDjeq.webp

随着具有不同外形尺寸的移动设备使用的增长, 我们需要有一些工具, 使我们的Android应用程序能够适应不同类型的屏幕.这就是为什么Android为我们提供了Window Size类, 简单地说, 它是三个大的屏幕格式组, 为我们开发设计标记了关键点.这样我们就避免了考虑许多屏幕设计的复杂性, 将我们的可能性减少到三组, 即: CompatMedium 和 Expanded..

Windows Size类

1_5Tm17OKlC5n0oy6L641A5g.webp

1_Qv1nt0JJzQPzFfr2G78ulg.webp

支持不同的屏幕尺寸

我们拥有的另一个重要资源是经典布局, 这是预定义的屏幕设计, 可以用于我们的安卓应用中的大多数场景, 还向我们展示了如何将其适应大屏幕的指南.

1_XASUz4kVTK4I0dH8F5slYQ.gif

其他相关资源

Form-Factor培训

Google I/O 2022上的Form Factors

性能

0_QcvMmljmmcvCuqfN.webp

当我们为Android开发应用程序时, 我们必须确保用户体验更好, 不仅是在应用程序的开始, 而且在整个执行过程中.出于这个原因, 重要的是要有一些工具, 使我们能够对可能影响应用程序性能的情况进行预防性分析和持续监测, 因此, 这里有一个工具清单, 可以帮助你达到这个目的:

应用内更新

当你的用户在他们的设备上保持你的应用程序的更新时, 他们可以尝试新的功能, 以及从性能改进和错误修复中获益.虽然有些用户在他们的设备连接到无计量的连接时启用后台更新, 但其他用户可能需要被提醒安装更新.应用内更新是Google Play核心库的一项功能, 提示活跃用户更新你的应用.

应用内更新功能在运行Android 5.0(API级别21)或更高的设备上得到支持.此外, 应用内更新仅支持Android移动设备, Android平板电脑和Chrome OS设备.

0_m8wEQzEW1M1fwwKC.webp

应用内评论

Google Play应用内评论API让你可以提示用户提交Play Store的评分和评论, 而不需要离开你的应用或游戏, 这很方便.

一般来说, 应用内评论流程可以在你的应用的整个用户旅程中的任何时候被触发.在流程中, 用户可以使用1至5星系统对你的应用程序进行评分, 并添加一个可选的评论.一旦提交, 评论将被发送到Play Store并最终显示出来.

为了保护用户隐私和避免API被滥用, 您的应用程序应遵循关于何时请求应用内评论评论提示的设计的严格准则.

应用内评论文档

0_--T1rkTL7DEGJT9B.webp

辅助功能

0_fO3BnqLh8b-H_zLo.webp

辅助功能是软件设计和建造的一个重要特征, 除了改善他们的用户体验外, 还为有可访问性需求的人提供了使用应用程序的能力.这个概念旨在改善的一些残疾是:有视力问题的人, 色盲, 听力问题, 灵巧问题和认知障碍等等.

考虑的因素:

  • 增加文本的可见性(颜色对比, 可调整文本).
  • 使用大型, 简单的控件
  • 描述每个用户界面元素

查看辅助功能--Android文档

安全性

0_Fk42FqLrujNE0O1Z.png

安全性是我们在开发保护设备的完整性, 数据的安全性和用户的信任的应用程序时必须考虑的一个方面, 甚至是最重要的方面, 这就是为什么我在下面列出了一系列的提示, 将帮助你实现这一目的.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<permission android:name="my_custom_permission_name"
android:protectionLevel="signature" />
  • 不要将应用程序配置所需的密钥, 令牌或敏感数据直接放在项目库内的文件或类中.使用local.properties代替.

版本目录

Gradle提供了一种集中管理项目依赖关系的标准方式, 称为版本目录;它在7.0版本中试验性地引入, 并在7.4版本中正式发布.

优点是:

  • 对于每个目录, Gradle都会生成类型安全的访问器, 这样你就可以在IDE中用自动完成的方式轻松添加依赖关系.
  • 每个目录对一个构建的所有项目都是可见的.它是一个集中的地方, 可以声明一个依赖的版本, 并确保对该版本的改变适用于每个子项目.
  • 目录可以声明依赖包, 这是通常一起使用的"依赖包组".
  • 目录可以将依赖的组和名称与它的实际版本分开, 并使用版本参考来代替, 这样就可以在多个依赖之间共享一个版本声明.

更多请查看

Logger

Logger是一种软件工具, 用于登记有关程序执行的信息;重要事件, 错误调试信息和其他可能对诊断问题或了解程序如何工作有用的信息.记录器可以被配置为将信息写入不同的位置, 如日志文件, 控制台, 数据库, 或通过将信息发送到日志服务器.

Linter

0_T3lk9cUYryUAo6G1.webp

Linter是一种编程工具, 用于分析程序源代码, 以发现代码中的潜在问题或漏洞.这些问题可能是语法问题, 不恰当的代码风格, 缺乏文档, 安全问题等等, 它们会对代码的质量和可维护性产生影响.


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

深入理解Android-Runtime

上图是Android整体的架构,Android Runtime之于Android而言相当于心脏之于人体,是Android程序加载和运行的环境。这篇文章主要针对Android Runtime部分进行展开,探讨Android Runtime的发展以及目前现状,并介...
继续阅读 »
image.png

上图是Android整体的架构,Android Runtime之于Android而言相当于心脏之于人体,是Android程序加载和运行的环境。这篇文章主要针对Android Runtime部分进行展开,探讨Android Runtime的发展以及目前现状,并介绍应用Profile-Guided Optimization(PGO)技术对应用启动速度进行优化的可行性。转载请注明来源「申国骏」

App运行时演进

JVM

Android原生代码使用Java或者Kotlin编写,这些代码会通过javac或者kotlinc编译成.class文件,在Android之前,这些.class文件会被输入到JVM中执行。JVM可以简单分为三个子系统,分别是Class Loader、Runtime Data Area以及Execution Engine。其中Class Loader主要负责加载类、校验字节码、符号引用链接及对静态变量和静态方法分配内存并初始化。Runtime Data负责存储数据,分为方法区、堆区、栈区、程序计数器以及本地方法栈。Execution Engine负责二进制代码的执行以及垃圾回收。

image.png

Execution Engine中,会采用Interpreter或者JIT执行。其中Interpreter表示在运行的过程中对二进制代码进行解释,每次执行相同的二进制代码都进行解释比较浪费资源,因此对于热区的二进制代码会进行JIT即时编译,对二进制代码编译成机器码,这样相同的二进制代码执行时,就不用再次进行解释。

image.png

DVM(Android 2.1/2.2)

JVM是stack-based的运行环境,在移动设备中对性能和存储空间要求较高,因此Android使用了register-based的Dalvik VM。从JVM转换到DVM我们需要将.class文件转换为.dex文件,从.class转换到.dex的过程需要经过 desugar -> proguard -> dex compiler三个过程,这三个过程后来逐步变成 proguard -> D8(Desugar) 直到演变到今天只需要一步R8(D8(Desugar))。

image.png

我们主要关注Android中Runtime Engine与JVM的区别。在Android早期的版本里面,只存在Interpreter解释器,到了Android2.2版本将JIT引入,这个版本Dalvik与JVM的Runtime Engine区别不大。

image.png

ART-AOT(Android 4.4/5.0)

为了加快应用的启动速度和体验,到了Android4.4,Google提供了一个新的运行时环境ART(Android Runtime),到了Android5.0,ART替换Dalvik成为唯一的运行时环境。

image.png

ART运行时环境中,采用了AOT(Ahead-of-time)编译方式,即在应用安装的时候就将.dex提前编译成机器码,经过AOT编译之后.dex文件会生成.oat文件。这样在应用启动执行的时候,因为不需要进行解释编译,大大加快了启动速度。

image.png

然而AOT带来了以下两个问题:

  1. 应用安装时间大幅增加,由于在安装的过程中同时需要编译成机器码,应用安装时间会比较长,特别在系统升级的时候,需要对所有应用进行重新编译,出现了经典的升级等待噩梦。

image.png

  1. 应用占用过多的存储空间,由于所有应用都被编译成.oat机器码,应用所占的存储空间大大增加,使得本来并不充裕的存储空间变得雪上加霜。

进一步思考对应用全量进行编译可能是没有必要的,因为用户可能只会用到一个应用的部分常用功能,并且全量编译之后更大的机器码加载会占用IO资源。

ART-PGO(Android 7.0)

从Android7.0开始,Google重新引入了JIT的编译方式,不再对应用进行全量编译,结合AOT、JIT、Interpreter三者的优势提出了PGO(Profile-guided optimization)的编译方式。

在应用执行的过程中,先使用Interpreter直接解释,当某些二进制代码被调用次数较多时,会生成一个Profile文件记录这些方法存储起来,当二进制代码被频繁调用时,则直接进行JIT即时编译并缓存起来。

当应用处于空闲(屏幕关闭且充电)的状态时,编译守护进程会根据Profile文件进行AOT编译。

当应用重新打开时,进行过JIT和AOT编译的代码可以直接执行。

这样就可以在应用安装速度以及应用打开速度之间取得平衡。

image.png

image.png

JIT 工作流程:

image.png

ART-Cloud Profile(Android 9.0)

不过这里还是有一个问题,就是当用户第一次安装应用的时候并没有进行任何的AOT优化,通常会经过用户多次的使用才能使得启动速度得到优化。

image.png

考虑到一个应用通常会有一些用户经常使用执行的代码(例如启动部分以及用户常用功能)并且大多数时候会有先行版本用于收集Profile数据,因此Google考虑将用户生成的Profile文件上传到Google Play中,并在应用安装时同时带上这个Profile文件,在安装的过程中,会根据这个Profile对应用进行部分的AOT编译。这样当用户安装完第一次打开的时候,就能达到较快的启动速度。

image.png

image.png

Profile in cloude 需要系统应用市场支持,在国内市场使用Google Play的占比非常低,因此cloud profile的优化在国内几乎是没有作用的,不过Profile的机制提供了一个可以做启动优化的思路。早在2019年,支付宝就在秒开技术的回应的里面提到过profile-based compile的技术,参考:如何看待今日头条自媒体发布谣言称「支付宝几乎秒开是因为采用华为方舟编译器」?,这也是我们一直研究Profile技术的原因。困扰着我们的一直有两个问题,第一个问题是如何生成Profile文件,第二个问题是怎么使用生成的Profile文件。对于第一个问题的解决相对还是有思路的,因为app运行就会生成profile文件,因此我们手动运行几次app就能在文件系统中收集到这个文件,不过如何以一种较为自动化的手段收集仍然是个问题。第二个问题我们知道Profile文件最终生成的位置,因此我们可以把生成的文件放到相应的系统目录,不过大多数手机和应用都没有权限直接放置这个文件。因此Profile优化技术一直都没有落地,直到Baseline Proflie让我们看到了希望。

Baseline Profile

Baseline Profile是一套生成和使用Profile文件的工具,在2022年一月份开始进入视野,随后在Google I/O 2022随着Jetpack新变化得到广泛关注。其背景是Google Map加快了发版速度,Cloud Profle还没完全收集好就上新版,导致Cloud Proflie失效。还有一个背景是Jetpack Compose 不是系统代码,因此没有完全编译成机器码,而且Jetpack Compose库比较大,因此在Profile生成之前使用了Jetpack Compose的应用启动会产生性能问题。最后Google为了解决这些问题,创造了收集Profile的BaselineProfileRule Macrobenchmark以及使用Profile的ProfileInstaller。

使用Baseline Profile的机制可以在Android7及以上的手机上得到应用的启动加速,因为从上述知道Android7就已经开始有PGO(Profile-guided optimization)的编译方式。生成的Profile文件会打包到apk里面,并且会结合Google Play的Cloud Profile来引导AOT编译。虽然在国内基本上用不了Cloud Profile,不过Baseline Profile是可以独立于Google Play单独使用的。

image.png

在使用了Baseline Proflie之后,有道词典的启动速度从线上统计上看,冷启动时间有15%的提升。

这篇文章主要介绍了Android Runtime的演进以及对于应用启动的影响,下一篇文章我会详细介绍关于Profile&dex文件优化、Baseline Profile工具库原理,以及在实际操作上如何使用的问题,敬请大家期待一下!

收起阅读 »

什么情况下Activity会被杀掉呢?

首先一个报错来作为开篇:Caused by androidx.fragment.app.Fragment$InstantiationException Unable to instantiate fragment xxx: could not find Fra...
继续阅读 »

首先一个报错来作为开篇:

Caused by androidx.fragment.app.Fragment$InstantiationException
Unable to instantiate fragment xxx: could not find Fragment constructor

这个报错原因就是Fragment如果重载了有参的构造方法,没有实现默认无参构造方法。Activity被回收又回来尝试重新恢复Fragment的时候报错的。


那如何模拟Activity被回收呢?

可能有人知道,一个方便快捷的方法就是:打开 开发者选项 - 不保留活动,这样每次Activity回到后台都会被回收,也就可以很方便的测试这种case。


但抛开这种方式我怎么来复现这种情况呢?

这里我提出一种方式:我是不是可以打开我的App,按Home回到后台,然后疯狂的打开手机里其他的大型应用或者游戏这类的能占用大量手机内存的App,等手机内存占用大的时候是不是可以复现这种情况呢?


结论是不可以,不要混淆两个概念,系统内存不足App内存不足,两者能引起的后果也是不同的



  • 系统内存不足 -> 杀掉应用进程

  • App内存不足 -> 杀掉后台Activity


首先明确一点,Android框架对进程创建与管理进行了封装,对于APP开发者只需知道Android四大组件的使用。当Activity, Service, ContentProvider, BroadcastReceiver任一组件启动时,当其所承载的进程存在则直接使用,不存在则由框架代码自动调用startProcessLocked创建进程。所以说对APP来说进程几乎是透明的,但了解进程对于深刻理解Android系统是至关关键的。


1. 系统内存不够 -> 杀掉应用进程


1.1. LKM简介

Android底层还是基于Linux,在Linux中低内存是会有oom killer去杀掉一些进程去释放内存,而Android中的lowmemorykiller就是在此基础上做了一些调整来的。因为手机上的内存毕竟比较有限,而Android中APP在不使用之后并不是马上被杀掉,虽然上层ActivityManagerService中也有很多关于进程的调度以及杀进程的手段,但是毕竟还需要考虑手机剩余内存的实际情况,lowmemorykiller的作用就是当内存比较紧张的时候去及时杀掉一些ActivityManagerService还没来得及杀掉但是对用户来说不那么重要的进程,回收一些内存,保证手机的正常运行。


lowmemkiller中会涉及到几个重要的概念:

/sys/module/lowmemorykiller/parameters/minfree:里面是以”,”分割的一组数,每个数字代表一个内存级别

/sys/module/lowmemorykiller/parameters/adj: 对应上面的一组数,每个数组代表一个进程优先级级别


比如:

/sys/module/lowmemorykiller/parameters/minfree:18432, 23040, 27648, 32256, 55296, 80640

/sys/module/lowmemorykiller/parameters/adj: 0, 100, 200, 300, 900, 906


代表的意思是两组数一一对应:



  • 当手机内存低于80640时,就去杀掉优先级906以及以上级别的进程

  • 当内存低于55296时,就去杀掉优先级900以及以上的进程


可能每个手机的配置是不一样的,可以查看一下手头的手机,需要root。


1.2. 如何查看ADJ

如何查看进程的ADJ呢?比如我们想看QQ的adj

-> adb shell ps | grep "qq" 
UID PID PPID C STIME TTY TIME CMD
u0_a140 9456 959 2 10:03:07 ? 00:00:22 com.tencent.mobileqq
u0_a140 9987 959 1 10:03:13 ? 00:00:07 com.tencent.mobileqq:mini3
u0_a140 16347 959 0 01:32:48 ? 00:01:12 com.tencent.mobileqq:MSF
u0_a140 21475 959 0 19:47:33 ? 00:01:25 com.tencent.mobileqq:qzone

# 看到QQ的PID为 9456,这个时候打开QQ,让QQ来到前台
-> adb shell cat /proc/9456/oom_score_adj
0

# 随便打开一个其他的App
-> adb shell cat /proc/9456/oom_score_adj
700

# 再随便打开另外一个其他的App
-> adb shell cat /proc/9456/oom_score_adj
900

我们可以看到adj是在根据用户的行为不断变化的,前台的时候是0,到后台是700,回到后台后再打开其他App后是900

常见ADJ级别如下:























































































ADJ级别取值含义
NATIVE_ADJ-1000native进程
SYSTEM_ADJ-900仅指system_server进程
PERSISTENT_PROC_ADJ-800系统persistent进程
PERSISTENT_SERVICE_ADJ-700关联着系统或persistent进程
FOREGROUND_APP_ADJ0前台进程
VISIBLE_APP_ADJ100可见进程
PERCEPTIBLE_APP_ADJ200可感知进程,比如后台音乐播放
BACKUP_APP_ADJ300备份进程
HEAVY_WEIGHT_APP_ADJ400重量级进程
SERVICE_ADJ500服务进程
HOME_APP_ADJ600Home进程
PREVIOUS_APP_ADJ700上一个进程
SERVICE_B_ADJ800B List中的Service
CACHED_APP_MIN_ADJ900不可见进程的adj最小值
CACHED_APP_MAX_ADJ906不可见进程的adj最大值

So,当系统内存不足的时候会kill掉整个进程,皮之不存毛将焉附,Activity也就不在了,当然也不是开头说的那个case。


2. App内存不足 -> 杀掉后台Activity


上面分析了是直接kill掉进程的情况,一旦出现进程被kill掉,说明内存情况已经到了万劫不复的情况了,抛开内存泄漏的情况下,framework也需要一些策略来避免无内存可用的情况。下面我们来找一找fw里面回收Activity的逻辑(代码Base Android-30)。



Android Studio查看源码无法查看com.android.internal包名下的代码,双击Shift,勾选右上角Include non-prject Items.



入口定位到ActivityThreadattach方法,ActivityThread是App的入口程序,main方法中创建并调用atttach

// ActivityThread.java
private void attach(boolean system, long startSeq) {
...
// Watch for getting close to heap limit.
BinderInternal.addGcWatcher(new Runnable() {
@Override public void run() {
// mSomeActivitiesChanged在生命周期变化的时候会修改为true
if (!mSomeActivitiesChanged) {
return;
}
Runtime runtime = Runtime.getRuntime();
long dalvikMax = runtime.maxMemory();
long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
if (dalvikUsed > ((3*dalvikMax)/4)) {
mSomeActivitiesChanged = false;
try {
ActivityTaskManager.getService().releaseSomeActivities(mAppThread);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
});
...
}

这里关注BinderInternal.addGcWatcher, 下面有几个点需要理清:



  1. addGcWatcher是干嘛的,这个Runnable什么时候会被执行。

  2. 这里的maxMemory() / totalMemory() / freeMemory()都怎么理解,值有什么意义

  3. releaseSomeActivities()做了什么事情,回收Activity的逻辑是什么。


还有一个小的点是这里还用了mSomeActivitiesChanged这个标记位来标记让检测工作不会过于频繁的执行,检测到需要releaseSomeActivities后会有一个mSomeActivitiesChanged = false;赋值。而所有的mSomeActivitiesChanged = true操作都在handleStartActivity/handleResumeActivity...等等这些操作Activity声明周期的地方。控制了只有Activity声明周期变化了之后才会继续去检测是否需要回收。


2.1. GcWatcher

BinderInternal.addGcWatcher是个静态方法,相关代码如下:

public class BinderInternal {
private static final String TAG = "BinderInternal";
static WeakReference<GcWatcher> sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
static ArrayList<Runnable> sGcWatchers = new ArrayList<>();
static Runnable[] sTmpWatchers = new Runnable[1];

static final class GcWatcher {
@Override
protected void finalize() throws Throwable {
handleGc();
sLastGcTime = SystemClock.uptimeMillis();
synchronized (sGcWatchers) {
sTmpWatchers = sGcWatchers.toArray(sTmpWatchers);
}
for (int i=0; i<sTmpWatchers.length; i++) {
if (sTmpWatchers[i] != null) {
sTmpWatchers[i].run();
}
}
sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
}
}

public static void addGcWatcher(Runnable watcher) {
synchronized (sGcWatchers) {
sGcWatchers.add(watcher);
}
}
...
}

两个重要的角色:sGcWatcherssGcWatcher



  • sGcWatchers保存了调用BinderInternal.addGcWatcher后需要执行的Runnable(也就是检测是否需要kill Activity的Runnable)。

  • sGcWatcher是个装了new GcWatcher()的弱引用。


弱引用的规则是如果一个对象只有一个弱引用来引用它,那GC的时候就会回收这个对象。那很明显new出来的这个GcWatcher()只会有sGcWatcher这一个弱引用来引用它,所以每次GC都会回收这个GcWatcher对象,而回收的时候会调用这个对象的finalize()方法,finalize()方法中会将之前注册的Runnable来执行掉。
注意哈,这里并没有移除sGcWatcher中的Runnable,也就是一开始通过addGcWatcher(Runnable watcher)进来的runnable一直都在,不管执行多少次run的都是它。


为什么整个系统中addGcWatcher只有一个调用的地方,但是sGcWatchers确实一个List呢?我在自己写了这么一段代码并且想着怎么能反射搞到系统当前的BinderInternal一探究竟的时候明白了一点点,我觉着他们就是怕有人主动调用了addGcWatcher给弄了好多个GcWatcher导致系统的失效了才搞了个List吧。。


2.2. App可用的内存

上面的Runnable是如何检测当前的系统内存不足的呢?通过以下的代码

        Runtime runtime = Runtime.getRuntime();
long dalvikMax = runtime.maxMemory();
long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
if (dalvikUsed > ((3*dalvikMax)/4)) { ... }

看变量名字就知道,在使用的内存到达总内存的3/4的时候去做一些事情,这几个方法的注释如下:

    /**
* Returns the amount of free memory in the Java Virtual Machine.
* Calling the gc method may result in increasing the value returned by freeMemory.
* @return an approximation to the total amount of memory currently available for future allocated objects, measured in bytes.
*/
public native long freeMemory();

/**
* Returns the total amount of memory in the Java virtual machine.
* The value returned by this method may vary over time, depending on the host environment.
* @return the total amount of memory currently available for current and future objects, measured in bytes.
*/
public native long totalMemory();

/**
* Returns the maximum amount of memory that the Java virtual machine will attempt to use.
* If there is no inherent limit then the value java.lang.Long#MAX_VALUE will be returned.
* @return the maximum amount of memory that the virtual machine will attempt to use, measured in bytes
*/
public native long maxMemory();

首先确认每个App到底有多少内存可以用,这些Runtime的值都是谁来控制的呢?


可以使用adb shell getprop | grep "dalvik.vm.heap"命令来查看手机给每个虚拟机进程所分配的堆配置信息:

yocn@yocn ~ % adb shell getprop | grep "dalvik.vm.heap"
[dalvik.vm.heapgrowthlimit]: [256m]
[dalvik.vm.heapmaxfree]: [8m]
[dalvik.vm.heapminfree]: [512k]
[dalvik.vm.heapsize]: [512m]
[dalvik.vm.heapstartsize]: [8m]
[dalvik.vm.heaptargetutilization]: [0.75]

这些值分别是什么意思呢?



  • [dalvik.vm.heapgrowthlimit]和[dalvik.vm.heapsize]都是当前应用进程可分配内存的最大限制,一般heapgrowthlimit < heapsize,如果在Manifest中的application标签中声明android:largeHeap=“true”,APP直到heapsize才OOM,否则达到heapgrowthlimit就OOM

  • [dalvik.vm.heapstartsize] Java堆的起始大小,指定了Davlik虚拟机在启动的时候向系统申请的物理内存的大小,后面再根据需要逐渐向系统申请更多的物理内存,直到达到MAX

  • [dalvik.vm.heapminfree] 堆最小空闲值,GC后

  • [dalvik.vm.heapmaxfree] 堆最大空闲值

  • [dalvik.vm.heaptargetutilization] 堆目标利用率


比较难理解的就是heapminfree、heapmaxfree和heaptargetutilization了,按照上面的方法来说:
在满足 heapminfree < freeMemory() < heapmaxfree的情况下使得(totalMemory() - freeMemory()) / totalMemory()接近heaptargetutilization


所以一开始的代码就是当前使用的内存到达分配的内存的3/4的时候会调用releaseSomeActivities去kill掉某些Activity.


2.3. releaseSomeActivities

releaseSomeActivities在API 29前后差别很大,我们来分别看一下。


2.3.1. 基于API 28的版本的releaseSomeActivities实现如下:
// step①:ActivityManagerService.java
@Override
public void releaseSomeActivities(IApplicationThread appInt) {
synchronized(this) {
final long origId = Binder.clearCallingIdentity();
try {
ProcessRecord app = getRecordForAppLocked(appInt);
mStackSupervisor.releaseSomeActivitiesLocked(app, "low-mem");
} finally {
Binder.restoreCallingIdentity(origId);
}
}
}

// step②:ActivityStackSupervisor.java
void releaseSomeActivitiesLocked(ProcessRecord app, String reason) {
TaskRecord firstTask = null;
ArraySet<TaskRecord> tasks = null;
for (int i = 0; i < app.activities.size(); i++) {
ActivityRecord r = app.activities.get(i);
// 如果当前有正在销毁状态的Activity,Do Nothing
if (r.finishing || r.state == DESTROYING || r.state == DESTROYED) {
return;
}
// 只有Activity在可以销毁状态的时候才继续往下走
if (r.visible || !r.stopped || !r.haveState || r.state == RESUMED || r.state == PAUSING
|| r.state == PAUSED || r.state == STOPPING) {
continue;
}
if (r.task != null) {
if (firstTask == null) {
firstTask = r.task;
} else if (firstTask != r.task) {
// 2.1 只有存在两个以上的Task的时候才会到这里
if (tasks == null) {
tasks = new ArraySet<>();
tasks.add(firstTask);
}
tasks.add(r.task);
}
}
}
// 2.2 只有存在两个以上的Task的时候才不为空
if (tasks == null) {
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Didn't find two or more tasks to release");
return;
}
// If we have activities in multiple tasks that are in a position to be destroyed,
// let's iterate through the tasks and release the oldest one.
// 2.3 遍历找到ActivityStack释放最旧的那个
final int numDisplays = mActivityDisplays.size();
for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
final ArrayList<ActivityStack> stacks = mActivityDisplays.valueAt(displayNdx).mStacks;
// Step through all stacks starting from behind, to hit the oldest things first.
// 从后面开始遍历,从最旧的开始匹配
for (int stackNdx = 0; stackNdx < stacks.size(); stackNdx++) {
final ActivityStack stack = stacks.get(stackNdx);
// Try to release activities in this stack; if we manage to, we are done.
// 尝试在这个stack里面销毁这些Activities,如果成功就返回。
if (stack.releaseSomeActivitiesLocked(app, tasks, reason) > 0) {
return;
}
}
}
}

上面代码都加了注释,我们来理一理重点需要关注的点。整个流程可以观察tasks的走向



  • 2.1 & 2.2: 第一次循环会给firstTask赋值,当firstTask != r.task的时候才会给tasks赋值,后续会继续对tasks操作。所以单栈的应用不会回收,如果tasks为null,就直接return了,什么都不做

  • 2.3: 这一大段的双重for循环其实都没有第一步遍历出来的tasks参与,真正释放Activity的操作在ActivityStack中,所以尝试找到这些tasks对应的ActivityStack,让ActivityStack去销毁tasks,直到成功销毁。


继续查看releaseSomeActivitiesLocked:

// step③ ActivityStack.java
final int releaseSomeActivitiesLocked(ProcessRecord app, ArraySet<TaskRecord> tasks, String reason) {
// Iterate over tasks starting at the back (oldest) first.
int maxTasks = tasks.size() / 4;
if (maxTasks < 1) {
maxTasks = 1;
}
// 3.1 maxTasks至少为1,至少清理一个
int numReleased = 0;
for (int taskNdx = 0; taskNdx < mTaskHistory.size() && maxTasks > 0; taskNdx++) {
final TaskRecord task = mTaskHistory.get(taskNdx);
if (!tasks.contains(task)) {
continue;
}
int curNum = 0;
final ArrayList<ActivityRecord> activities = task.mActivities;
for (int actNdx = 0; actNdx < activities.size(); actNdx++) {
final ActivityRecord activity = activities.get(actNdx);
if (activity.app == app && activity.isDestroyable()) {
destroyActivityLocked(activity, true, reason);
if (activities.get(actNdx) != activity) {
// Was removed from list, back up so we don't miss the next one.
// 3.2 destroyActivityLocked后续会调用TaskRecord.removeActivity(),所以这里需要将index--
actNdx--;
}
curNum++;
}
}
if (curNum > 0) {
numReleased += curNum;
// 移除一个,继续循环需要判断 maxTasks > 0
maxTasks--;
if (mTaskHistory.get(taskNdx) != task) {
// The entire task got removed, back up so we don't miss the next one.
// 3.3 如果整个task都被移除了,这里同样需要将获取Task的index--。移除操作在上面3.1的destroyActivityLocked,移除Activity过程中,如果task为空了,会将task移除
taskNdx--;
}
}
}
return numReleased;
}



  • 3.1: ActivityStack利用maxTasks 保证,最多清理tasks.size() / 4,最少清理1个TaskRecord,同时,至少要保证保留一个前台可见TaskRecord,比如如果有两个TaskRecord,则清理先前的一个,保留前台显示的这个,如果三个,则还要看看最老的是否被有效清理,也就是是否有Activity被清理,如果有则只清理一个,保留两个,如果没有,则继续清理次老的,保留一个前台展示的,如果有四个,类似,如果有5个,则至少两个清理。一般APP中,很少有超过两个TaskRecord的。




  • 3.2: 这里清理的逻辑很清楚,for循环,如果定位到了期望的activity就清理掉,但这里这个actNdx--是为什么呢?注释说activity从list中移除了,为了能继续往下走,需要index--,但在这个方法中并没有将activity从lsit中移除的操作,那肯定是在destroyActivityLocked方法中。继续追进去可以一直追到TaskRecord.java#removeActivity(),从当前的TaskRecord的mActivities中移除了,所以需要index--。




  • 3.3: 我们弄懂了上面的actNdx--之后也就知道这里为什么要index--了,在ActivityStack.java#removeActivityFromHistoryLocked()中有

	if (lastActivity) {
removeTask(task, reason, REMOVE_TASK_MODE_DESTROYING);
}

如果task中没有activity了,需要将这个task移除掉。


以上就是基于API 28的releaseSomeActivities分析。


2.3.2. 基于29+的版本的releaseSomeActivities实现如下:
// ActivityTaskManagerService.java
@Override
public void releaseSomeActivities(IApplicationThread appInt) {
synchronized (mGlobalLock) {
final long origId = Binder.clearCallingIdentity();
try {
final WindowProcessController app = getProcessController(appInt);
app.releaseSomeActivities("low-mem");
} finally {
Binder.restoreCallingIdentity(origId);
}
}
}

// WindowProcessController.java
void releaseSomeActivities(String reason) {
// Examine all activities currently running in the process. Candidate activities that can be destroyed.
// 检查进程里所有的activity,看哪些可以被关掉
ArrayList<ActivityRecord> candidates = null;
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Trying to release some activities in " + this);
for (int i = 0; i < mActivities.size(); i++) {
final ActivityRecord r = mActivities.get(i);
// First, if we find an activity that is in the process of being destroyed,
// then we just aren't going to do anything for now; we want things to settle
// down before we try to prune more activities.
// 首先,如果我们发现一个activity正在执行关闭中,在关掉这个activity之前什么都不做
if (r.finishing || r.isState(DESTROYING, DESTROYED)) {
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Abort release; already destroying: " + r);
return;
}
// Don't consider any activities that are currently not in a state where they can be destroyed.
// 如果当前activity不在可关闭的state的时候,不做处理
if (r.mVisibleRequested || !r.stopped || !r.hasSavedState() || !r.isDestroyable()
|| r.isState(STARTED, RESUMED, PAUSING, PAUSED, STOPPING)) {
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Not releasing in-use activity: " + r);
continue;
}

if (r.getParent() != null) {
if (candidates == null) {
candidates = new ArrayList<>();
}
candidates.add(r);
}
}

if (candidates != null) {
// Sort based on z-order in hierarchy.
candidates.sort(WindowContainer::compareTo);
// Release some older activities
int maxRelease = Math.max(candidates.size(), 1);
do {
final ActivityRecord r = candidates.remove(0);
r.destroyImmediately(true /*removeFromApp*/, reason);
--maxRelease;
} while (maxRelease > 0);
}
}

新版本的releaseSomeActivities放到了ActivityTaskManagerService.java这个类中,这个类是API 29新添加的,承载部分AMS的工作。
相比API 28基于Task栈的回收Activity策略,新版本策略简单清晰, 也激进了很多。


遍历所有Activity,刨掉那些不在可销毁状态的Activity,按照Activity堆叠的顺序,也就是Z轴的顺序,从老到新销毁activity。


有兴趣的读者可以自行编写测试代码,分别在API 28和API 28+的手机上测试看一下回收策略是否跟上面分析的一致。

也可以参考我写的TestKillActivity,单栈和多栈的情况下在高于API 28和低于API 28的手机上的表现。


总结:



  1. 系统内存不足时LMK会根据内存配置项来kill掉进程释放内存

  2. kill时会按照进程的ADJ规则来kill

  3. App内存不足时由GcWatcher来决定回收Activity的时机

  4. 可以使用getprop命令来查看当前手机的JVM内存分配和OOM配置

  5. releaseSomeActivities在API 28和API 28+的差别很大,低版本会根据Task数量来决定清理哪个task的。高版本简单粗暴,遍历activity,按照z order排序,优先release掉更老的activity。

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

快速使用MQTT Flutter版 SDK实现消息收发

1. 前提条件1.1 部署Flutter开发环境配置好Flutter开发环境。1.2 导入项目依赖在yaml文件里配置mqtt_client: ^9.8.1 在iOS开发中需要增加一下代码到位于ios/Runner/Info.plist中的Info.plist...
继续阅读 »

1. 前提条件

1.1 部署Flutter开发环境

配置好Flutter开发环境。

1.2 导入项目依赖

在yaml文件里配置

mqtt_client: ^9.8.1

在iOS开发中需要增加一下代码到位于ios/Runner/Info.plist中的Info.plist*文件中:


<key>NSLocalNetworkUsageDescription</key>
<string>Looking for local tcp Bonjour service</string>
<key>NSBonjourServices</key>
<array>
<string>mqtt.tcp</string>
</array>

Android 

Android AndroidManifest.xml 增加如下代码


<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

2. 实现流程

2.1 获取初始化信息

登录console后台
1.点击菜单栏【MQTT】→【服务概览】→【服务配置】,获取「连接地址」、「连接端口」、「AppID」以「及REST API地址」等信息。
注:clientID由两部分组成,组织形式为“deviceID@AppID”,deviceID由用户自定义,AppID见【服务配置】。
示例:正确的clientID格式为:“device001@aitbj0”;
2.点击菜单栏【应用概览】→【应用详情】→【开发者ID】,获取「Client ID」与「ClientSecret」信息。
3.初始化代码


static const String restapi= "https://api.cn1.mqtt.chat/app/$appID/"; //环信MQTT REST API地址 通过console后台[MQTT]->[服务概览]->[服务配置]下[REST API地址]获取

static const String endpoint= "**"; //环信MQTT服务器地址 通过console后台[MQTT]->[服务概览]->[服务配置]下[连接地址]获取
static const int port = **; // 协议服务端口 通过console后台[MQTT]->[服务概览]->[服务配置]下[连接端口]获取
static const String appID= "**"; // appID 通过console后台[MQTT]->[服务概览]->[服务配置]下[AppID]获取
static late String deviceId ;// 自定义deviceID
static late String clientID ;// deviceId + '@' + appID
static const String appClientId= "**"; //开发者ID 通过console后台[应用概览]->[应用详情]->[开发者ID]下[ Client ID]获取
static const String appClientSecret= "**"; // 开发者密钥 通过console后台[应用概览]->[应用详情]->[开发者ID]下[ ClientSecret]获取

static void init() async{
deviceId = "deviceId";
clientID = "$deviceId@$appID";
}

 

2.2 获取token

  • 首先获取App Token
Dio dio = Dio();
Response<Map<String,dynamic>> data = await dio.post("${restapi}openapi/rm/app/token",data: {"appClientId": appClientId, "appClientSecret": appClientSecret});
var token = (data.data!["body"] as Map<String, dynamic> )["access_token"];
  • 然后根据App Token获取User Token,User Token作为连接服务的密码
Response<Map<String,dynamic>> data2 = await dio.post("${restapi}openapi/rm/user/token",data: {"username": "username", "cid": clientID},options: Options(headers:  <String, dynamic>{r'Authorization': token}));
var mqtttoken = (data2.data!["body"] as Map<String, dynamic> )["access_token"];

2.3 连接服务器

创建MqttAndroidClient对象,并配置连接密码、cleansession标志、心跳间隔、超时时间等信息,调用connect()函数连接至环信MQTT消息云。

var client = MqttServerClient.withPort(endpoint, clientID, port);
/// 是否打印mqtt日志信息
client.logging(on: true);
/// 设置协议版本,默认是3.1,根据服务器需要的版本来设置
/// _client.setProtocolV31();
client.setProtocolV311();
/// 保持连接ping-pong周期。默认不设置时关闭。
client.keepAlivePeriod = 60;
/// 设置自动重连
client.doAutoReconnect();
/// 设置超时时间,单位:毫秒
client.connectTimeoutPeriod = 60000;
/// 连接成功回调
client.onConnected = _onConnected;
/// 连接断开回调
client.onDisconnected = _onDisconnected;
/// 取消订阅回调
client.onUnsubscribed = _onUnsubscribed;
/// 订阅成功回调
client.onSubscribed = _onSubscribed;
/// 订阅失败回调
client.onSubscribeFail = _onSubscribeFail;
/// ping pong响应回调
client.pongCallback = _pong;
client.connect(username,mqtt_token);



static void _onConnected() {
LogManager.log.d("连接成功....");
_initTopic();
}

static void _onDisconnected() {
LogManager.log.d("连接断开");
}

static void _onUnsubscribed(String? topic) {
LogManager.log.d("取消订阅 $topic");
}

static void _onSubscribed(String topic) {
LogManager.log.d("订阅 $topic 成功");
}

static void _onSubscribeFail(String topic) {
LogManager.log.e("订阅主题: $topic 失败");
}

static void _pong() {
LogManager.log.d("Ping的响应");
}


2.4 订阅(subscribe)

2.4.1 订阅主题
当客户端成功连接环信MQTT消息云后,需尽快向服务器发送订阅主题消息。在连接成功后调用

client.subscribe(topic, MqttQos.atLeastOnce);

2.4.2 取消订阅

_client?.unsubscribe(topic)

2.5 收发消息

2.5.1 发送消息
配置发送消息回调方法,向环信MQTT消息云中指定topic发送消息。

var builder = MqttClientPayloadBuilder();
builder.addUTF8String("This is a message");
client.publishMessage("topic", MqttQos.atLeastOnce, builder.payload!);

2.5.2 接收消息
配置接收消息回调方法,从环信MQTT消息云接收订阅消息。

_client?.updates?.listen((event) {
var recvMessage = event[0].payload as MqttPublishMessage;

LogManager.log.d("原始数据-----:${recvMessage.payload.message}");
/// 转换成字符串
LogManager.log.d(
"接收到了主题${event[0].topic}的消息: ${const Utf8Decoder().convert(recvMessage.payload.message)}");
});
收起阅读 »

flutter有哪些架构的框架?该怎么选择

flutter有哪些架构的框架? Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架: BLoC (Business Logic Co...
继续阅读 »

flutter有哪些架构的框架?


Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架:



  1. BLoC (Business Logic Component):BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。在Flutter中,可以使用flutter_bloc库来实现BLoC架构。
    Provider:Provider是Flutter中的一个轻量级状态管理库,它使用InheritedWidget实现状态共享,可以有效地解决Flutter应用中的状态管理问题。

  2. MobX:MobX是一种基于响应式编程的状态管理库,它使用可观察对象来管理应用程序的状态,并自动更新与之相关的UI组件。在Flutter中,可以使用mobx库来实现MobX架构。

  3. Redux:Redux是一种流行的状态管理模式,在Flutter中也有相应的实现库redux_flutter。Redux通过单一数据源管理应用程序的状态,并使用纯函数来处理状态的更新,可以有效地解决Flutter应用中的状态管理问题。
    以上是常用的Flutter架构框架,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。


除了上面提到的框架之外,还有以下几个Flutter架构框架:



  1. GetX:GetX是一种轻量级的Flutter架构框架,它提供了路由管理、状态管理和依赖注入等功能,可以大大简化Flutter应用的开发。

  2. MVC:MVC是一种经典的软件架构模式,它将应用程序分为模型、视图和控制器三个部分,可以有效地分离关注点,使得应用程序更易于维护和扩展。

  3. MVP:MVP是一种衍生自MVC的架构模式,它将应用程序分为模型、视图和Presenter三个部分,Presenter负责处理业务逻辑,将模型数据展示到视图上。

  4. MVVM:MVVM是一种流行的架构模式,它将应用程序分为模型、视图和视图模型三个部分,视图模型负责处理业务逻辑,将模型数据展示到视图上。


总之,Flutter中有很多架构框架可供选择,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。


Flutter BLoC


Flutter BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。BLoC这个缩写代表 Business Logic Component,即业务逻辑组件。
BLoC的核心思想是将UI层和业务逻辑层分离,通过Stream或者Sink等异步编程方式,将UI层和业务逻辑层连接起来。具体来说,BLoC模式包含以下三个部分:
Events:事件,即UI层的用户操作或其他触发条件,例如按钮点击,网络请求完成等等。
Bloc:业务逻辑层,用于处理Events,处理业务逻辑,并向UI层提供状态更新。
State:状态,即UI层的显示状态,例如页面的loading状态,数据请求成功或失败状态等等。


BLoC的核心是Bloc和State之间的联系,Bloc接收Events,并根据业务逻辑处理后,通过Stream向UI层提供状态更新。UI层监听Bloc的Stream,获取最新的State,并根据State更新UI状态。
在Flutter中,可以使用StreamController来创建BLoC。StreamController是一个异步数据流控制器,可以创建一个Stream用于事件流,创建一个Sink用于事件的注入。
Flutter框架提供了一个非常好的BLoC框架flutter_bloc,它封装了BLoC的核心逻辑,使得开发者可以更加方便地使用BLoC模式进行状态管理。使用flutter_bloc框架,只需要定义好Events、Bloc和State,然后将它们组合起来,就可以实现状态管理,从而将UI层和业务逻辑层分离。


总之,Flutter BLoC是一种状态管理模式,它通过将UI层和业务逻辑层分离,使用Stream或Sink等异步编程方式将它们连接起来,实现了应用程序的业务逻辑和UI分离。


如何选择使用


选择使用哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。以下是一些常见的应用场景和推荐的架构框架:
对于小型应用程序,可以考虑使用轻量级的状态管理库,如Provider或GetX。
对于需要处理复杂业务逻辑的应用程序,可以使用BLoC、MobX或Redux等状态管理框架。
对于需要实现高度可测试性的应用程序,可以考虑使用MVC、MVP或MVVM等经典的软件架构模式。
总之,选择哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。开发者应该根据自己的需求和技能水平选择最适合的架构框架,以提高开发效率和代码质量。


GetX和BLoC的优缺点


GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点,下面是它们的主要特点和优缺点的比较:


GetX


优点:


简单易用:GetX是一种轻量级的Flutter框架,提供了简单易用的依赖注入、路由管理和状态管理等功能,可以大大简化Flutter应用的开发。
性能优秀:GetX使用原生的Dart语言构建,不需要任何代码生成,因此运行速度非常快,同时也具有很好的内存管理和性能优化能力。
功能完备:GetX提供了路由管理、依赖注入、状态管理、国际化、主题管理等功能,可以满足大多数应用程序的需求。


缺点:


社区相对较小:相比其他流行的Flutter框架,GetX的社区相对较小,相关文档和教程相对较少,需要一定的自学能力。
不适合大型应用:由于GetX是一种轻量级框架,不适合处理大型应用程序的复杂业务逻辑和状态管理,需要使用其他更加强大的框架。


BLoC


优点:


灵活可扩展:BLoC提供了灵活的状态管理和业务逻辑处理能力,可以适应各种应用程序的需求,同时也具有良好的扩展性。
可测试性强:BLoC将UI和业务逻辑分离,提高了代码的可测试性,可以更容易地编写和运行测试代码。
社区活跃:BLoC是一种流行的Flutter框架,拥有较大的社区和用户群体,相关文档和教程比较丰富,容易入手。


缺点:


学习曲线较陡峭:BLoC是一种相对复杂的框架,需要一定的学习曲线和编程经验,初学者可能需要花费较多的时间和精力。
代码量较大:由于BLoC需要处理UI和业务逻辑的分离,因此需要编写更多的代码来实现相同的功能,可能会增加开发成本和维护难度。
总之,GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点。选择哪种框架取决于具体的应用程序需求和开发团队的技术水平。


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

当我再次用Kotlin完成五年前已经通过Kotlin完成的项目后

关于贝塞尔曲线, 这里就不多赘述了. 简单来说, 针对每一个线段, 某个点到两端的比例都是一样的, 而贝塞尔曲线就是这个过程的中线段两端都在同一位置的线段(点)过程的集合. 如图, AD和AB的比例, BE和BC的比例还有DF和DE的比例都是一样的.这个比例从...
继续阅读 »

关于贝塞尔曲线, 这里就不多赘述了. 简单来说, 针对每一个线段, 某个点到两端的比例都是一样的, 而贝塞尔曲线就是这个过程的中线段两端都在同一位置的线段(点)过程的集合.


如图, AD和AB的比例, BE和BC的比例还有DF和DE的比例都是一样的.这个比例从0到1, F点的位置连成线, 就是ABC这三个点的贝塞尔曲线.


Bezier


两次完成的感受


虽然时隔五年, 但是对这个项目的印象还是比较深刻的(毕竟当时找啥资料都不好找).


当时的项目还用的是Kotlin Synthetic来进行数据绑定(虽然现在已经被弃用了), 对于当时还一直用findViewById和@BindView的我来说, 这是对我最大的惊喜. 是的, 当时用Kotlin最大惊喜就是这个. 其它的感觉就是这个"语法糖"看起来还挺好用的. 而现在, 我可以通过Compose来完成页面的布局. 最直观的结果是代码量的减少, 初版功能代码(带xml)大概有800行, 而这次完成整个功能大概只需要450行.


在使用过程中对"Compose is function"理念的理解更深了一步, 数据就是数据. 将数据作为一个参数放到Compose这个function中, 在数据变化的时候重新调用function, 达到更新UI的效果. 显而易见的事情是我们不需要的额外的持有UI的对象了, 我们不必考虑UI中某个元素和另一个元素直接的关联, 不必考虑某个元素响应什么样的操作. 我们只需要考虑某个Compose(function) 在什么样的情况下(入参)需要表现成什么样子.


比如Change Point按钮点下时, 会更改mInChange的内容, 从而影响许多其它元素的效果, 如果通过View来实现, 我需要监听Change Point的点击事件, 然后依次修改影响到的元素(这个过程中需要持有大量其它View的对象). 不过当使用Compose后, 虽然我们仍要监听Change Point的点击事件, 但是对对应Change Point的监听动作来说, 它只需要修改mInChange的内容就行了, 修改这个值会发生什么变化它不需要处理也不要知道. 真正需要变化的Compose来处理就可以了(可以理解为参数变化了, 重新调用了这个function)


特性的部分使用的并不多, 比较项目还是比较小, 很多特性并没有体现出来.


最令我感到开心的是, 再一次完成同样的功能所花费的时间仅仅只有半天多, 而5年前完成类似的功能大概用了一个多星期的时间. 也不知道我和Kotlin这5年来哪一方变化的更大😆.


贝塞尔曲线工具


先来看一下具有的功能, 主要的功能就是绘制贝塞尔曲线(可绘制任意阶数), 显示计算过程(辅助线的绘制), 关键点的调整, 以及新增的绘制进度手动调整. 为了更本质的显示绘制的结果, 此次并没有对最终结果点进行显示优化, 所以在短时间变化位置大的情况下, 可能出现不连续的现象.


3_point_bezier


more_point_bezier


bizier_change


bezier_progress


代码的比较


既然是同样的功能, 不同的代码, 即使是由不同时期所完成的, 将其相互比较一下还是有一定意义的. 当然比较的内容都尽量提供相同实现的部分.


屏幕触摸事件监测层


主要在于对屏幕的触碰事件的监测


初版代码:

override fun onTouchEvent(event: MotionEvent): Boolean {


touchX = event.x
touchY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
toFindChageCounts = true
findPointChangeIndex = -1
//增加点前点击的点到屏幕中
if (controlIndex < maxPoint || isMore == true) {
addPoints(BezierCurveView.Point(touchX, touchY))
}
invalidate()
}
MotionEvent.ACTION_MOVE ->{
checkLevel++
//判断当前是否需要检测更换点坐标
if (inChangePoint){
//判断当前是否长按 用于开始查找附件的点
if (touchX == lastPoint.x && touchY == lastPoint.y){
changePoint = true
lastPoint.x = -1F
lastPoint.y = -1F
}else{
lastPoint.x = touchX
lastPoint.y = touchY
}
//开始查找附近的点
if (changePoint){
if (toFindChageCounts){
findPointChangeIndex = findNearlyPoint(touchX , touchY)
}
}

//判断是否存在附近的点
if (findPointChangeIndex == -1){
if (checkLevel > 1){
changePoint = false
}

}else{
//更新附近的点的坐标 并重新绘制页面内容
points[findPointChangeIndex].x = touchX
points[findPointChangeIndex].y = touchY
toFindChageCounts = false
invalidate()
}
}

}
MotionEvent.ACTION_UP ->{
checkLevel = -1
changePoint = false
toFindChageCounts = false
}

}
return true
}

二次代码:

 Canvas(
...
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
model.pointDragStart(it)
},
onDragEnd = {
model.pointDragEnd()
}
) { _, dragAmount ->
model.pointDragProgress(dragAmount)
}
}
.pointerInput(Unit) {
detectTapGestures {
model.addPoint(it.x, it.y)
}
}
)
...

/**
* change point position start, check if have point in range
*/
fun pointDragStart(position: Offset) {
if (!mInChange.value) {
return
}
if (mBezierPoints.isEmpty()) {
return
}
mBezierPoints.firstOrNull() {
position.x > it.x.value - 50 && position.x < it.x.value + 50 &&
position.y > it.y.value - 50 && position.y < it.y.value + 50
}.let {
bezierPoint = it
}
}

/**
* change point position end
*/
fun pointDragEnd() {
bezierPoint = null
}

/**
* change point position progress
*/
fun pointDragProgress(drag: Offset) {
if (!mInChange.value || bezierPoint == null) {
return
} else {
bezierPoint!!.x.value += drag.x
bezierPoint!!.y.value += drag.y
calculate()
}
}

可以看到由于Compose提供了Tap和Drag的详细事件, 从而导致新的代码少许多的标记位变量.


而我之前一度认为是语法糖的特性来给我带来了不小的惊喜.


譬如这里查找点击位置最近的有效的点的方法,


初版代码:

//判断当前触碰的点附近是否有绘制过的点
private fun findNearlyPoint(touchX: Float, touchY: Float): Int {
Log.d("bsr" , "touchX: ${touchX} , touchY: ${touchY}")
var index = -1
var tempLength = 100000F
for (i in 0..points.size - 1){
val lengthX = Math.abs(touchX - points[i].x)
val lengthY = Math.abs(touchY - points[i].y)
val length = Math.sqrt((lengthX * lengthX + lengthY * lengthY).toDouble()).toFloat()
if (length < tempLength){
tempLength = length

if (tempLength < minLength){
toFindChageCounts = false
index = i
}
}
}

return index
}


而二次代码:

        mBezierPoints.firstOrNull() {
position.x > it.x.value - 50 && position.x < it.x.value + 50 &&
position.y > it.y.value - 50 && position.y < it.y.value + 50
}.let {
bezierPoint = it
}

和Java的Stream类似, 链式结构看起来更加的易于理解.


贝塞尔曲线绘制层


主要的贝塞尔曲线是通过递归实现的


初版代码:

//通过递归方法绘制贝塞尔曲线
private fun drawBezier(canvas: Canvas, per: Float, points: MutableList<Point>) {

val inBase: Boolean

//判断当前层级是否需要绘制线段
if (level == 0 || drawControl){
inBase = true
}else{
inBase = false
}


//根据当前层级和是否为无限制模式选择线段及文字的颜色
if (isMore){
linePaint.color = 0x3F000000
textPaint.color = 0x3F000000
}else {
linePaint.color = colorSequence[level].toInt()
textPaint.color = colorSequence[level].toInt()
}

//移动到开始的位置
path.moveTo(points[0].x , points[0].y)

//如果当前只有一个点
//根据贝塞尔曲线定义可以得知此点在贝塞尔曲线上
//将此点添加到贝塞尔曲线点集中(页面重新绘制后之前绘制的数据会丢失 需要重新回去前段的曲线路径)
//将当前点绘制到页面中
if (points.size == 1){
bezierPoints.add(Point(points[0].x , points[0].y))
drawBezierPoint(bezierPoints , canvas)
val paint = Paint()
paint.strokeWidth = 10F
paint.style = Paint.Style.FILL
canvas.drawPoint(points[0].x , points[0].y , paint)
return
}


val nextPoints: MutableList<Point> = ArrayList()

//更新路径信息
//计算下一级控制点的坐标
for (index in 1..points.size - 1){
path.lineTo(points[index].x , points[index].y)

val nextPointX = points[index - 1].x -(points[index - 1].x - points[index].x) * per
val nextPointY = points[index - 1].y -(points[index - 1].y - points[index].y) * per

nextPoints.add(Point(nextPointX , nextPointY))
}

//绘制控制点的文本信息
if (!(level !=0 && (per==0F || per == 1F) )) {
if (inBase) {
if (isMore && level != 0){
canvas.drawText("0:0", points[0].x, points[0].y, textPaint)
}else {
canvas.drawText("${charSequence[level]}0", points[0].x, points[0].y, textPaint)
}
for (index in 1..points.size - 1){
if (isMore && level != 0){
canvas.drawText( "${index}:${index}" ,points[index].x , points[index].y , textPaint)
}else {
canvas.drawText( "${charSequence[level]}${index}" ,points[index].x , points[index].y , textPaint)
}
}
}
}

//绘制当前层级
if (!(level !=0 && (per==0F || per == 1F) )) {
if (inBase) {
canvas.drawPath(path, linePaint)
}
}
path.reset()

//更新层级信息
level++

//绘制下一层
drawBezier(canvas, per, nextPoints)

}



二次代码:

{
lateinit var preBezierPoint: BezierPoint
val paint = Paint()
paint.textSize = mTextSize.toPx()

for (pointList in model.mBezierDrawPoints) {
if (pointList == model.mBezierDrawPoints.first() ||
(model.mInAuxiliary.value && !model.mInChange.value)
) {
for (point in pointList) {
if (point != pointList.first()) {
drawLine(
color = Color(point.color),
start = Offset(point.x.value, point.y.value),
end = Offset(preBezierPoint.x.value, preBezierPoint.y.value),
strokeWidth = mLineWidth.value
)
}
preBezierPoint = point

drawCircle(
color = Color(point.color),
radius = mPointRadius.value,
center = Offset(point.x.value, point.y.value)
)
paint.color = Color(point.color).toArgb()
drawIntoCanvas {
it.nativeCanvas.drawText(
point.name,
point.x.value - mPointRadius.value,
point.y.value - mPointRadius.value * 1.5f,
paint
)
}
}
}
}

...
}


/**
* calculate Bezier line points
*/
private fun calculateBezierPoint(deep: Int, parentList: List<BezierPoint>) {
if (parentList.size > 1) {
val childList = mutableListOf<BezierPoint>()
for (i in 0 until parentList.size - 1) {
val point1 = parentList[i]
val point2 = parentList[i + 1]
val x = point1.x.value + (point2.x.value - point1.x.value) * mProgress.value
val y = point1.y.value + (point2.y.value - point1.y.value) * mProgress.value
if (parentList.size == 2) {
mBezierLinePoints[mProgress.value] = Pair(x, y)
return
} else {
val point = BezierPoint(
mutableStateOf(x),
mutableStateOf(y),
deep + 1,
"${mCharSequence.getOrElse(deep + 1){"Z"}}$i",
mColorSequence.getOrElse(deep + 1) { 0xff000000 }
)
childList.add(point)
}
}
mBezierDrawPoints.add(childList)
calculateBezierPoint(deep + 1, childList)
} else {
return
}
}

初版开发的时候受个人能力限制, 递归方法中既包含了绘制的功能也包含了计算下一层的功能. 而二次编码的时候受Compose的设计影响, 尝试将所有的点状态变为Canvas的入参信息. 代码的编写过程就变得更加的流程.


当然, 现在的我和五年前的我, 开发的能力一定是不一样的. 即便如此, 随着Kotlin的不断发展, 即使是同样用Kotlin完成的项目, 随着新的概念的提出, 更多更适合新的开发技术的出现, 我们仍然从Kotlin和Compose收获更多.


我和Kotlin的小故事


初次认识Kotlin是在2017的5月, 当时Kotlin还不是Google所推荐的Android开发语言. 对我来说, Kotlin更多的是个新的技术, 在实际的工作中也无法进行使用.


即使如此, 我也尝试开始用Kotlin去完成更多的内容, 所幸如此, 不然这篇文章就无法完成了, 我也错过了一个更深层次了解Kotlin的机会.


但是即便2018年Google将Kotlin作为Android的推荐语言, 但Kotlin在当时仍不是一个主流的选择. 对我来说以下的一些问题导致了我在当时对Kotlin的使用性质不高. 一是新语言, 社区构建不完善, 有许多的内容需要大家填充, 带来就是在实际的使用情况中会遇到各种的问题, 这些问题在网站中没有找到可行的解决方案. 二是可以和Java十分便捷互相使用的特性, 这个特性是把双刃剑,
虽然可以让我更加无负担的使用Kotlin(不行再用Java写呗.). 但也使得我认为Kotlin是个Java++或者Java--. 三是无特殊性, Kotlin并没有带来什么新的内容, Kotlin能完成的事情Java都能做完成, (空值和data class之类的在我看来更多的是一个语法糖.) 那么我为什么要用一种新的不熟悉的技术来完成我都需求?


所幸的是, 还是有更多的人在不断的推进和建设Kotlin. 也吸引了越来越多的人加入. 近年来越来越多的项目中都开始有着Kotlin的踪迹, 我将Kotlin添加到现有的项目中也变得越来越能被大家所接受. 也期待可以帮助到更多的人.


相关代码地址:


初次代码


二次代码


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

Android自定义一个省份简称键盘

我正在参加「掘金·启航计划」hello啊各位老铁,这篇文章我们重新回到Android当中的自定义View,其实最近一直在搞Flutter,初步想法是,把Flutter当中的基础组件先封装一遍,然后接着各个工具类,列表,网络,统统由浅入深的搞一遍,弄完Flutt...
继续阅读 »

我正在参加「掘金·启航计划」

hello啊各位老铁,这篇文章我们重新回到Android当中的自定义View,其实最近一直在搞Flutter,初步想法是,把Flutter当中的基础组件先封装一遍,然后接着各个工具类,列表,网络,统统由浅入深的搞一遍,弄完Flutter之后,再逐步的更新Android当中的技术点,回头一想,还是穿插着来吧,再系统的规划,难免也有变化,想到啥就写啥吧,能够坚持输出就行。

今天的这个知识点,是一个自定义View,一个省份的简称键盘,主要用到的地方,比如车牌输入等地方,相对来说还是比较的简单,我们先看下最终的实现效果:

实现方式呢有很多种,我相信大家也有自己的一套实现机制,这里,我采用的是组合View,用的是LinearLayout的方式。

今天的内容大致如下:

1、分析UI,如何布局

2、设置属性和方法,制定可扩展效果

3、部分源码剖析

4、开源地址及实用总结

一、分析UI,如何布局

拿到UI效果图后,其实也没什么好分析的,无非就是两块,顶部的完成按钮和底部的省份简称格子,一开始,打算用RecyclerView网格布局来实现,但是最后的删除按钮如何摆放就成了问题,直接悬浮在网格上边,动态计算位置,显然不太合适,也没有这样去搞的,索性直接抛弃这个方案,多布局的想法也实验过,但最终还是选择了最简单的LinearLayout组合View形式。

所谓简单,就是在省份简称数组的遍历中,不断的给LinearLayout进行追加子View,需要注意的是,本身的View,也就是我们自定义View,继承LinearLayout后,默认的是垂直方向的,往本身View追加的是横向属性的LinearLayout,这也是换行的效果,也就是,一行一个横向的LinearLayout,记住,横向属性的LinearLayout,才是最终添加View的直接父类。

换行的条件就是基于UI效果,当模于设置length等于0时,我们就重新创建一个水平的LinearLayout,这就可以了,是不是非常的简单。

至于最后的删除按钮,使其靠右,占据两个格子的权重设置即可。

二、设置属性和方法,制定可扩展效果

当我们绘制完这个身份简称键盘后,肯定是要给他人用的,基于灵活多变的需求,那么相对应的我们也需要动态的进行配置,比如背景颜色,文字的颜色,大小,还有边距,以及点击效果等等,这些都是需要外露,让使用者选择性使用的,目前所有的属性如下,大家在使用的时候,也可以对照设置。

设置属性

属性类型概述
lp_backgroundcolor整体的背景颜色
lp_rect_spacingdimension格子的边距
lp_rect_heightdimension格子的高度
lp_rect_margin_topdimension格子的距离上边
lp_margin_left_rightdimension左右距离
lp_margin_topdimension上边距离
lp_margin_bottomdimension下边距离
lp_rect_backgroundreference格子的背景
lp_rect_select_backgroundreference格子选择后的背景
lp_rect_text_sizedimension格子的文字大小
lp_rect_text_colorcolor格子的文字颜色
lp_rect_select_text_colorcolor格子的文字选中颜色
lp_is_show_completeboolean是否显示完成按钮
lp_complete_text_sizedimension完成按钮文字大小
lp_complete_text_colorcolor完成按钮文字颜色
lp_complete_textstring完成按钮文字内容
lp_complete_margin_topdimension完成按钮距离上边
lp_complete_margin_bottomdimension完成按钮距离下边
lp_complete_margin_rightdimension完成按钮距离右边
lp_text_click_effectboolean是否触发点击效果,true点击后背景消失,false不消失

定义方法

方法参数概述
keyboardContent回调函数获取点击的省份简称简称信息
keyboardDelete函数删除省份简称简称信息
keyboardComplete回调函数键盘点击完成
openProhibit函数打开禁止(使领学港澳),使其可以点击

三、关键源码剖析

这里只贴出部分的关键性代码,整体的代码,大家滑到底部查看源码地址即可。

定义身份简称数组

    //省份简称数据
private val mLicensePlateList = arrayListOf(
"京", "津", "渝", "沪", "冀", "晋", "辽", "吉", "黑", "苏",
"浙", "皖", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "琼",
"川", "贵", "云", "陕", "甘", "青", "蒙", "桂", "宁", "新",
"藏", "使", "领", "学", "港", "澳",
)

遍历省份简称

mLength为一行展示多少个,当取模为0时,就需要换行,也就是再次创建一个水平的LinearLayout,添加至外层的垂直LinearLayout中,每个水平的LinearLayout中,则是一个一个的TextView。

  //每行对应的省份简称
var layout: LinearLayout? = null
//遍历车牌号
mLicensePlateList.forEachIndexed { index, s ->
if (index % mLength == 0) {
//重新创建,并添加View
layout = createLinearLayout()
layout?.weightSum = 1f
addView(layout)
val params = layout?.layoutParams as LayoutParams
params.apply {
topMargin = mRectMarginTop.toInt()
height = mRectHeight.toInt()
leftMargin = mMarginLeftRight.toInt()
rightMargin = mMarginLeftRight.toInt() - mSpacing.toInt()
layout?.layoutParams = this
}
}

//创建文字视图
val textView = TextView(context).apply {
text = s
//设置文字的属性
textSize = px2sp(mRectTextSize)
//最后五个是否禁止
if (mNumProhibit && index > (mLicensePlateList.size - 6)) {
setTextColor(mNumProhibitColor)
mTempTextViewList.add(this)
} else {
setTextColor(mRectTextColor)
}

setBackgroundResource(mRectBackGround)
gravity = Gravity.CENTER
setOnClickListener {
if (mNumProhibit && index > (mLicensePlateList.size - 6)) {
return@setOnClickListener
}
//每个格子的点击事件
changeTextViewState(this)
}
}

addRectView(textView, layout, 0.1f)
}

追加最后一个View

由于最后一个视图是一个图片,占据了两个格子的大小,所以需要特殊处理,需要做的就是,单独设置权重weight和单独设置宽度width,如下所示:

  /**
* AUTHOR:AbnerMing
* INTRODUCE:追加最后一个View
*/
private fun addEndView(layout: LinearLayout?) {
val endViewLayout = LinearLayout(context)
endViewLayout.gravity = Gravity.RIGHT
//删除按钮
val endView = RelativeLayout(context)
//添加删除按钮
val deleteImage = ImageView(context)
deleteImage.setImageResource(R.drawable.view_ic_key_delete)
endView.addView(deleteImage)

val imageParams = deleteImage.layoutParams as RelativeLayout.LayoutParams
imageParams.addRule(RelativeLayout.CENTER_IN_PARENT)
deleteImage.layoutParams = imageParams
endView.setOnClickListener {
//删除
mKeyboardDelete?.invoke()
invalidate()
}
endView.setBackgroundResource(mRectBackGround)
endViewLayout.addView(endView)
val params = endView.layoutParams as LayoutParams
params.width = (getScreenWidth() / mLength) * 2 - mMarginLeftRight.toInt()
params.height = LayoutParams.MATCH_PARENT

endView.layoutParams = params

layout?.addView(endViewLayout)
val endParams = endViewLayout.layoutParams as LayoutParams
endParams.apply {
width = (mSpacing * 3).toInt()
height = LayoutParams.MATCH_PARENT
weight = 0.4f
rightMargin = mSpacing.toInt()
endViewLayout.layoutParams = this
}


}

四、开源地址及使用总结

开源地址:github.com/AbnerMing88…

关于使用,其实就是一个类,大家可以下载源码,直接复制即可使用,还可以进行修改里面的代码,非常的方便,如果懒得下载源码,没关系,我也上传到了远程Maven,大家可以按照下面的方式进行使用。

Maven具体调用

1、在你的根项目下的build.gradle文件下,引入maven。

 allprojects {
repositories {
maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
}
}

2、在你需要使用的Module中build.gradle文件下,引入依赖。

 dependencies {
implementation 'com.vip:plate:1.0.0'
}

代码使用

   <com.vip.plate.LicensePlateView
android:id="@+id/lp_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:lp_complete_text_size="14sp"
app:lp_margin_left_right="10dp"
app:lp_rect_spacing="6dp"
app:lp_rect_text_size="19sp"
app:lp_text_click_effect="false" />

总结

大家在使用的时候,一定对照属性表进行选择性使用;关于这个省份简称自定义View,实现方式有很多种,我目前的这种也不是最优的实现方式,只是自己的一个实现方案,给大家一个作为参考的依据,好了,铁子们,本篇文章就先到这里,希望可以帮助到大家。


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

Android 官方架构中的 UseCase 该怎么写?

1. UseCase 的用途 Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。 Android 最新架构:developer.android.com/topi...
继续阅读 »

1. UseCase 的用途


Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。



Android 最新架构:developer.android.com/topic/archi…



传统的 MVVM 架构中,我们习惯用 ViewModel 来承载业务逻辑,随着业务规模的扩大,ViewModel 变得越来越肥大,职责不清。



Clean Architecture 提出的关注点分离和单一职责(SRP)的设计原则被广泛认可,因此 Android 在最新架构中引入了 Clean Architecture 中 UseCase 的概念。ViewModel 归属 UI Layer,更加聚焦 UiState 的管理,UI 无关的业务逻辑下沉 UseCase,UseCase 与 ViewModel 解耦后,也可以跨 ViewModel 提供公共逻辑。



Android 架构早期的示例代码 todo-app 中曾经引入过 UseCase 的概念,最新架构中只不过是将 UseCase 的思想更明确了,最新的 UseCase 示例可以从官方的 NIA 中学习。




2. UseCase 的特点


官方文档认为 UseCase 应该具有以下几个特点:


2.1 不持有状态


可以定义自己的数据结构类型,但是不能持有状态实例,像一个纯函数一样工作。甚至直接推荐大家将逻辑重写到 invoke 方法中,像调用函数一样调用实例。


下面是 NIA 中的一个示例:GetRecentSearchQueriesUseCase



2.2 单一职责


严格遵守单一职责,一个 UseCase 只做一件事情,甚至其命名就是一个具体行为。扫一眼 UseCase 的文件目录大概就知道 App 的大概功能了。


下面 NIA 中所有 UseCases:



2.3 可有可无


官方文档中将 UseCase 定义为可选的角色,按需定义。简单的业务场景中允许 UI 直接访问 Repository。如果我们将 UseCase 作为 UI 与 Data 隔离的角色,那么工程中会出现很多没有太大价值的 UseCase ,可能就只有一行调用 Repoitory 的代码。


3. 如何定义 UseCase


如上所述,官方文档虽然对 UseCase 给出了一些基本定义,但是毕竟是一个新新生概念,很多人在真正去写代码的时候仍然会感觉不清晰,缺少有效指引。在究竟如何定义 UseCase 这个问题上,还有待大家更广泛的讨论,形成可参考的共识。本文也是带着这个目的而生,算是抛砖引玉吧。


3.1 Optional or Mandatory?


首先,官方文档认为 UseCase 是可选的,虽然其初衷是好的,大家都不希望出现太多 One-Liner 的 UseCase,但是作为一个架构规范切忌模棱两可,这种“可有可无”的规则其结局往往就是“无”。


业务刚起步时由于比较简单往往定义在 Repository 中,随着业务规模的扩大,应该适当得增加 UseCase 封装一些复杂的业务逻辑,但是实际项目中此时的重构成本会让开发者变得“懒惰”,UseCase 最终难产。


那放弃 UseCase 呢?这可能会造成 Repository 的职责不清和无限膨胀,而且 Repository 往往不止有一个方法, ViewModel 直接依赖 Repository 也违反了 SOLID 中的另一个重要原则 ISP ,ViewModel 会因为不相关的 Repository 改动导致重新编译。



ISP(Interface Segregation Principle,接口隔离原则) 要求将接口分离成更小的和更具体的接口,以便调用方只需知道其需要使用的方法。这可以提高代码的灵活性和可重用性,并减少代码的依赖性和耦合性。



为了降低前期判断成本和后续重构成本,如果我们有业务持续壮大的预期,那不妨考虑将 UseCase 作为强制选项。当然,最好这需要研究如何降低 UseCase 带来的模板代码。


3.2 Class or Object?


官方建议使用 Class 定义 UseCase,每次使用都实例化一个新对象,这会做成一些重复开销,那么可否用 object 定义 UseCase 呢?


UseCase 理论上可以作为单例存在,但 Class 相对于 Object 有以下两个优势:



  • UseCase 希望像纯函数一样工作,普通 Class 可以确保每次使用时都会创建一个新的实例,从而避免状态共享和副作用等问题。

  • 普通类可以通过构造参数注入不同的 Repository,UseCase 更利于复用和单元测试


如果我们强烈希望 UseCase 有更长的生命周期,那借助 DI 框架,普通类也可以简单的支持。例如 Dagger 中只要添加 @Singleton 注解即可

@Singleton
class GetRecentSearchQueriesUseCase @Inject constructor(
private val recentSearchRepository: RecentSearchRepository,
) {
operator fun invoke(limit: Int = 10): Flow<List<RecentSearchQuery>> =
recentSearchRepository.getRecentSearchQueries(limit)
}

3.3 Class or Function?


既然我们想像函数一样使用 UseCase ,那为什么不直接定义成 Function 呢?比如像下面这样

fun GetRecentSearchQueriesUseCase : Flow<List<RecentSearchQuery>> 

这确实遵循了 FP 的原则,但又丧失了 OOP 封装性的优势:



  • UseCase 往往需要依赖 Repository 对象,一个 UseCase Class 可以将 Repository 封装为成员存储。而一个 UseCase Function 则需要调用方通过参数传入,使用成本高不说,如果 UseCase 依赖的 Repository 的类型或者数量发生变化了,调用方需要跟着修改

  • 函数起不到隔离 UI 和 Data 的作用,ViewModel 仍然需要直接依赖 Repository,为 UseCase 传参

  • UseCase Class 可以定义一些 private 的方法,相对于 Function 更能胜任一些复杂逻辑的实现


可见,在 UseCase 的定义上 Function 没法取代 Class。当然 Class 也带来一些弊端:



  • 暴露多个方法,破坏 SRP 原则。所以官方推荐用 verb in present tense + noun/what (optional) + UseCase 动词命名,也是想让职责更清晰。

  • 携带可变状态,这是大家写 OOP 的惯性思维

  • 样板代码多


3.4 Function interface ?


通过前面的分析我们知道:UseCase 的定义需要兼具 FP 和 OOP 的优势。这让我想到了 Function(SAM) Interface 。Function Interface 是一个单方法的接口,可以低成本创建一个匿名类对象,确保对象只能有一个方法,同时具有一定封装性,可以通过“闭包”依赖 Repository。此外,Kotlin 对 SAM 提供了简化写法,一定程度也减少了样板代码。



Functional (SAM) interfaces:
kotlinlang.org/docs/fun-in…



改用 Function interface 定义 GetRecentSearchQueriesUseCase 的代码如下:

fun interface GetRecentSearchQueriesUseCase : () -> Flow<List<RecentSearchQuery>>

用它创建 UseCase 实例的同时,实现函数中的逻辑

val recentSearchQueriesUseCase = GetRecentSearchQueriesUseCase {
//...
}

我在函数实现中如何 Repository 呢?这要靠 DI 容器获取。官方示例代码中都使用 Hilt 来解耦 ViewModel 与 UseCase 的,ViewModel 不关心 UseCase 的创建细节。下面是 NIA 的代码, GetRecentSearchQueriesUseCase 被自动注入到 SearchViewModel 中。

@HiltViewModel
class SearchViewModel @Inject constructor(
recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase // UseCase 注入 VM
//...
) : ViewModel() {
//...
}

Function interface 的 GetRecentSearchQueriesUseCase 没有构造函数,需要通过 Dagger 的 @Module 安装到 DI 容器中,provideGetRecentSearchQueriesUseCase 参数中的 RecentSearchRepository 可以从容器中自动获取使用。

@Module
@InstallIn(ActivityComponent::class)
object UseCaseModule {
@Provides
fun provideGetRecentSearchQueriesUseCase(recentSearchRepository: RecentSearchRepository) =
GetRecentSearchQueriesUseCase { limit ->
recentSearchRepository.getRecentSearchQueries(limit)
}
}

当时用 Koin 作为 DI 容器时也没问题,代码如下:

single<GetRecentSearchQueriesUseCase> {
GetRecentSearchQueriesUseCase { limit ->
recentSearchRepository.getRecentSearchQueries(limit)
}
}

4. 总结


UseCase 作为官方架构中的新概念,尚没有完全深入人心,需要不断探索合理的使用方式,本文给出一些基本思考:




  • 考虑到架构的扩展性,推荐在 ViewModel 与 Repository 之间强制引入 UseCase,即使眼下的业务逻辑并不复杂




  • UseCase 不持有可变状态但依赖 Repository,需要兼具 FP 与 OOP 的特性,更适合用 Class 定义而非 Function




  • 在引入 UseCase 之前应该先引入 DI 框架,确保 ViewModel 与 UseCase 的耦合。




  • Function Interface 是 Class 之外的另一种定义 UseCase 的方式,有利于代码更加函数式


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

Android 在一个APP里打开另一个APP

前言 不知道你们有没有注意过,每次打开一些软件的时候都会有广告引导页,有时候手滑点到了,会有进入手机上的另一个APP,这有没有引起你的注意呢? 运行效果图 # 正文 为了测试这个功能,首先要创建两个项目,然后运行起来都安装在你的手机上,这里为了方便了解,取名...
继续阅读 »

前言


不知道你们有没有注意过,每次打开一些软件的时候都会有广告引导页,有时候手滑点到了,会有进入手机上的另一个APP,这有没有引起你的注意呢?


运行效果图


在这里插入图片描述


# 正文
为了测试这个功能,首先要创建两个项目,然后运行起来都安装在你的手机上,这里为了方便了解,取名就是应用A和应用B,流程就是A应用里面打开B应用。

首先当然是创建项目了


DemoA


在这里插入图片描述


DemoB


在这里插入图片描述


创建好之后,别的先不管,都在手机上安装一下再说


在这里插入图片描述


① 打开另一个APP


接下来在DemoA的MainActivity里面写一个按钮,用于点击之后打开DemoB应用

	<Button
android:id="@+id/btn_open_b"
android:text="打开DemoB"
android:textAllCaps="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

在这里插入图片描述


也在DemoB的布局文件改一下显示内容

<TextView
android:textSize="18sp"
android:textColor="#000"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="DemoB" />

运行一下


在这里插入图片描述


这样就打开了。那假如我要传递数据到DemoB呢?


② 数据传递


传数据其实就跟平时单个APP内部不同页面传数据类似,也是用Intent


在这里插入图片描述


然后在另一个APP里面接收并显示出来。现在先修改一下DemoB的布局,增加一个TextView用来显示接收的内容。

<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textColor="#000"
android:textSize="16sp" />

DemoB的MainActivity里


在这里插入图片描述


一旦两个应用程序里面改动了代码你就要在手机上运行一下,否则你改动的代码就不会生效


然后运行一下:


在这里插入图片描述


传值的问题就解决了。


③ 打开指定页面


通过包名跳转APP是进入默认的启动页面,你可以打开你的AndroidManifest.xml文件查看


在这里插入图片描述


那个Activity下面有这个默认启动就是那个

            <intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

至于要打开指定的页面有两个方法


1.通过包名、类名


首先在DemoB的下面再创建一个TestActivity,简单加一个TextView


在这里插入图片描述


在这里插入图片描述


因为是要DemoB的TestActivity页面,所以这个activity在AndroidManifest.xml中需要配置


android:exported 属性,布尔类型,是否支持其他应用访问目标 Activity,默认值为 true;android:exported="true"


否则你跳转会报错的,现在运行DemoB,使改动的代码生效
然后修改DemoA里面MainActivity的代码


在这里插入图片描述


运行效果


在这里插入图片描述


这样就可以了。


2.通过Action


修改DemoB的AndroidManifest.xml


在这里插入图片描述


然后运行在手机上,再修改DemoA的MainActivity


在这里插入图片描述


运行效果


在这里插入图片描述


其实还有一种方式是通过URL打开另一个APP,但是我不推荐这样做,为什么?没有原因...


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