注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

android媲美微信扫码库

之前使用的是zxing封装的库,但是识别率和识别速度没法和微信比较,现在使用的Google开源识别库完全可以和微信媲美github:github.com/DyncKathlin…强烈推荐MIKit Barcode Scanning识别速度超快,基本上camer...
继续阅读 »



之前使用的是zxing封装的库,但是识别率和识别速度没法和微信比较,现在使用的Google开源识别库完全可以和微信媲美

github:github.com/DyncKathlin…

强烈推荐MIKit Barcode Scanning

识别速度超快,基本上camera抓取到二维码就能识别到其内容(这是重点)。
基于MIKit Barcode Scanning的识别库进行封装,操作简单。
支持识别多个二维码,条形码。
支持任意比例展示,可以1:2,1.5:2等,不会发生像拉伸变形。
使用camera,不是cameraX哦。

效果图


第一个是Google开源的,第二个是zxing开源的

使用方式

build.gradle引用

implementation 'com.github.dynckathline:barcode:2.5'

初始化和监听结果回调

        //构造出扫描管理器
      configViewFinderView(viewfinderView);
      mlKit = new MLKit(this, preview, graphicOverlay);
      //是否扫描成功后播放提示音和震动
      mlKit.setPlayBeepAndVibrate(true, true);
      //仅识别二维码
      BarcodeScannerOptions options =
              new BarcodeScannerOptions.Builder()
                      .setBarcodeFormats(
                              Barcode.FORMAT_QR_CODE,
                              Barcode.FORMAT_AZTEC)
                      .build();
      mlKit.setBarcodeFormats(null);
      mlKit.setOnScanListener(new MLKit.OnScanListener() {
          @Override
          public void onSuccess(List<Barcode> barcodes, @NonNull GraphicOverlay graphicOverlay, InputImage image) {
              showScanResult(barcodes, graphicOverlay, image);
          }

          @Override
          public void onFail(int code, Exception e) {

          }
      });

展示结果

private void showScanResult(List<Barcode> barcodes, @NonNull GraphicOverlay graphicOverlay, InputImage image) {
      if (barcodes.isEmpty()) {
          return;
      }

      mlKit.setAnalyze(false);
      CustomDialog.Builder builder = new CustomDialog.Builder(context);
      CustomDialog dialog = builder
              .setContentView(R.layout.barcode_result_dialog)
              .setLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
              .setOnInitListener(new CustomDialog.Builder.OnInitListener() {
                  @Override
                  public void init(CustomDialog customDialog) {
                      Button btnDialogCancel = customDialog.findViewById(R.id.btnDialogCancel);
                      Button btnDialogOK = customDialog.findViewById(R.id.btnDialogOK);
                      TextView tvDialogContent = customDialog.findViewById(R.id.tvDialogContent);
                      ImageView ivDialogContent = customDialog.findViewById(R.id.ivDialogContent);

                      Bitmap bitmap = null;
                      ByteBuffer byteBuffer = image.getByteBuffer();
                      if (byteBuffer != null) {
                          FrameMetadata.Builder builder = new FrameMetadata.Builder();
                          builder.setWidth(image.getWidth())
                                  .setHeight(image.getHeight())
                                  .setRotation(image.getRotationDegrees());
                          bitmap = BitmapUtils.getBitmap(byteBuffer, builder.build());
                      } else {
                          bitmap = image.getBitmapInternal();
                      }
                      if (bitmap != null) {
                          graphicOverlay.add(new CameraImageGraphic(graphicOverlay, bitmap));
                      } else {
                          ivDialogContent.setVisibility(View.GONE);
                      }
                      SpanUtils spanUtils = SpanUtils.with(tvDialogContent);
                      for (int i = 0; i < barcodes.size(); ++i) {
                          Barcode barcode = barcodes.get(i);
                          BarcodeGraphic graphic = new BarcodeGraphic(graphicOverlay, barcode);
                          graphicOverlay.add(graphic);
                          Rect boundingBox = barcode.getBoundingBox();
                          spanUtils.append(String.format("(%d,%d)", boundingBox.left, boundingBox.top))
                                  .append(barcode.getRawValue())
                                  .setClickSpan(i % 2 == 0 ? getResources().getColor(R.color.colorPrimary) : getResources().getColor(R.color.colorAccent), false, new View.OnClickListener() {
                              @Override
                              public void onClick(View v) {
                                  Toast.makeText(getApplicationContext(), barcode.getRawValue(), Toast.LENGTH_SHORT).show();
                              }
                          })
                                  .setBackgroundColor(i % 2 == 0 ? getResources().getColor(R.color.colorAccent) : getResources().getColor(R.color.colorPrimary))
                                  .appendLine()
                                  .appendLine();
                      }
                      spanUtils.create();
                      Bitmap bitmapFromView = loadBitmapFromView(graphicOverlay);
                      ivDialogContent.setImageBitmap(bitmapFromView);

                      btnDialogCancel.setOnClickListener(new View.OnClickListener() {
                          @Override
                          public void onClick(View v) {
                              customDialog.dismiss();
                              finish();
                          }
                      });
                      btnDialogOK.setOnClickListener(new View.OnClickListener() {
                          @Override
                          public void onClick(View v) {
                              customDialog.dismiss();
                              mlKit.setAnalyze(true);
                          }
                      });
                  }
              })
              .build();
  }

作者:KathLine
来源:https://juejin.cn/post/6972476138203381790

收起阅读 »

Android:这是一个让你心动的日期&时间选择组件

预览引入添加 JitPack repositoryallprojects { repositories { ... maven { url "https://jitpack.io" } }}添加 Gradle依赖depe...
继续阅读 »



预览




imgimgimg

引入

添加 JitPack repository

allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}

添加 Gradle依赖

dependencies {
  ...
  implementation 'com.google.android.material:material:1.1.0' //为了防止不必要的依赖冲突,0.0.3开始需要自行依赖google material库
  implementation 'com.github.loperSeven:DateTimePicker:0.3.0'//此处不保证最新版本,最新版需前往文末github查看
}

开始使用

内置弹窗CardDatePickerDialog

最简单的使用方式

//kotlin
    CardDatePickerDialog.builder(this)
              .setTitle("SET MAX DATE")
              .setOnChoose {millisecond->
                 
              }.build().show()
//java
new CardDatePickerDialog.Builder(this)
              .setTitle("SET MAX DATE")
              .setOnChoose("确定", aLong -> {
                   //aLong = millisecond
                   return null;
              }).build().show();

所有可配置属性

  CardDatePickerDialog.builder(context)
              .setTitle("CARD DATE PICKER DIALOG")
              .setDisplayType(displayList)
              .setBackGroundModel(model)
              .showBackNow(true)
              .setPickerLayout(layout)
              .setDefaultTime(defaultDate)
              .setMaxTime(maxDate)
              .setMinTime(minDate)
              .setWrapSelectorWheel(false)
              .setThemeColor(color)
              .showDateLabel(true)
              .showFocusDateInfo(true)
              .setLabelText("年","月","日","时","分")
              .setOnChoose("选择"){millisecond->}
              .setOnCancel("关闭") {}
              .build().show()

可配置属性说明

  • 设置标题

fun setTitle(value: String)
  • 是否显示回到当前按钮

fun showBackNow(b: Boolean)
  • 是否显示选中日期信息

fun showFocusDateInfo(b: Boolean)
  • 设置自定义选择器

//自定义选择器Layout注意事详见 【定制 DateTimePicker】
fun setPickerLayout(@NotNull layoutResId: Int)
  • 显示模式

// model 分为:CardDatePickerDialog.CARD//卡片,CardDatePickerDialog.CUBE//方形,CardDatePickerDialog.STACK//顶部圆角
// model 允许直接传入drawable资源文件id作为弹窗的背景,如示例内custom
fun setBackGroundModel(model: Int)
  • 设置主题颜色

fun setThemeColor(@ColorInt themeColor: Int)
  • 设置显示值

fun setDisplayType(vararg types: Int)
fun setDisplayType(types: MutableList<Int>)
  • 设置默认时间

fun setDefaultTime(millisecond: Long)
  • 设置范围最小值

fun setMinTime(millisecond: Long)
  • 设置范围最大值

fun setMaxTime(millisecond: Long)
  • 是否显示单位标签

fun showDateLabel(b: Boolean)
  • 设置标签文字

/**
*示例
*setLabelText("年","月","日","时","分")
*setLabelText("年","月","日","时")
*setLabelText(month="月",hour="时")
*/
fun setLabelText(year:String=yearLabel,month:String=monthLabel,day:String=dayLabel,hour:String=hourLabel,min:String=minLabel)
  • 设置是否循环滚动

/**
*示例(默认为true)
*setWrapSelectorWheel(false)
*setWrapSelectorWheel(DateTimeConfig.YEAR,DateTimeConfig.MONTH,wrapSelector = false)
*setWrapSelectorWheel(arrayListOf(DateTimeConfig.YEAR,DateTimeConfig.MONTH),false)
*/
fun setWrapSelectorWheel()
  • 绑定选择监听

/**
*示例
*setOnChoose("确定")
*setOnChoose{millisecond->}
*setOnChoose("确定"){millisecond->}
*/
fun setOnChoose(text: String = "确定", listener: ((Long) -> Unit)? = null)
  • 绑定取消监听

/**
*示例
*setOnCancel("取消")
*setOnCancel{}
*setOnCancel("取消"){}
*/
fun setOnCancel(text: String = "取消", listener: (() -> Unit)? = null)

选择器 DateTimePicker

xml中

app:layout 为自定义选择器布局 可参考 定制 DateTimePicker

        <com.loper7.date_time_picker.DateTimePicker
           android:id="@+id/dateTimePicker"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           app:layout="@layout/layout_date_picker_segmentation"
           app:showLabel="true"
           app:textSize="16sp"
           app:themeColor="#FF8080" />

代码中

  • 设置监听

    dateTimePicker.setOnDateTimeChangedListener { millisecond ->  }

更多设置

  • 设置自定义选择器布局(注意:需要在dateTimePicker其他方法之前调用,否则其他方法将会失效)

 dateTimePicker.setLayout(R.layout.layout_date_picker_segmentation)//自定义layout resId
  • 设置显示状态

DateTimePicker支持显示 年月日时分 五个选项的任意组合,显示顺序以此为年、月、日、时、分,setDisplayType中可无序设置。

     dateTimePicker.setDisplayType(intArrayOf(
           DateTimeConfig.YEAR,//显示年
           DateTimeConfig.MONTH,//显示月
           DateTimeConfig.DAY,//显示日
           DateTimeConfig.HOUR,//显示时
           DateTimeConfig.MIN))//显示分
  • 设置默认选中时间

 dateTimePicker.setDefaultMillisecond(defaultMillisecond)//defaultMillisecond 为毫秒时间戳
  • 设置允许选择的最小时间

  dateTimePicker.setMinMillisecond(minMillisecond)
  • 设置允许选择的最大时间

  dateTimePicker.setMaxMillisecond(maxMillisecond)
  • 是否显示label标签(选中栏 年月日时分汉字)

  dateTimePicker.showLabel(true)
  • 设置主题颜色

  dateTimePicker.setThemeColor(ContextCompat.getColor(context,R.color.colorPrimary))
  • 设置字体大小

设置的字体大小为选中栏的字体大小,预览字体会根据字体大小等比缩放

  dateTimePicker.setTextSize(15)//单位为sp
  • 设置标签文字

  //全部
 dateTimePicker.setLabelText(" Y"," M"," D"," Hr"," Min")
 //指定
 dateTimePicker.setLabelText(min = "M")

定制 DateTimePicker

说明

DateTimePicker 主要由至多6个 NumberPicker 组成,所以在自定义布局时,根据自己所需的样式摆放 NumberPicker 即可。以下为注意事项

开始定制

  • DateTimePicker 至多支持6个 NumberPicker ,你可以在xml中按需摆放1-6个 NumberPicker

  • 为了让 DateTimePicker 找到 NumberPicker ,需要在xml中为 NumberPicker 指定 idtag,规则如下

/**
* year:np_datetime_year
* month:np_datetime_month
* day:np_datetime_day
* hour:np_datetime_hour
* minute:np_datetime_minute
* second:np_datetime_second
*/
android:id="@+id/np_datetime_year"  or  android:tag="np_datetime_year"
  • 使用定制UI

CardDatePickerDialog 中使用

fun setPickerLayout(@NotNull layoutResId: Int)

DateTimePicker 中使用

<com.loper7.date_time_picker.DateTimePicker
           android:id="@+id/dateTimePicker"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           app:layout="@layout/layout_date_picker_segmentation"
           />

或者

 dateTimePicker.setLayout(R.layout.layout_date_picker_segmentation)//自定义layout resId

XML示例

示例图片

imgimg

更高的拓展性

如果以上自定义并不能满足你的需求,你还可以定制你自己的 DateTimePicker , 可参照 DateTimePicker.kt 定义你想要属性以及在代码内编写你的UI逻辑。选择器的各种逻辑约束抽离在 DateTimeController.kt ,你的 DateTimePicker 只需让 DateTimeController.kt 绑定 NumberPicker 即可。比如:

DateTimeController().bindPicker(YEAR, mYearSpinner)
          .bindPicker(MONTH, mMonthSpinner)
          .bindPicker(DAY, mDaySpinner).bindPicker(HOUR, mHourSpinner)
          .bindPicker(MIN, mMinuteSpinner).bindPicker(SECOND, mSecondSpinner).build()

作者:LOPER7
来源:https://juejin.cn/post/6917909994985750535

收起阅读 »

实现一套自己的WebKit

速度快:相比系统webview的网页打开速度有30+%的提升;省流量:使用云端优化技术使流量节省20+%;更安全:安全问题可以在24小时内修复;更稳定:经过亿级用户的使用考验,CRASH率低于0.15%;。。。打包chromium,生成官方的apk安装打开ch...
继续阅读 »

大家应该都听过腾讯的X5浏览器,据官方介绍其有以下优势:

  • 速度快:相比系统webview的网页打开速度有30+%的提升;

  • 省流量:使用云端优化技术使流量节省20+%;

  • 更安全:安全问题可以在24小时内修复;

  • 更稳定:经过亿级用户的使用考验,CRASH率低于0.15%;

  • 。。。

就不一一列举了,详细见(x5.tencent.com/docs/index.…
既然自定义webkit有能力做到这些,那我们为什么不自己试着整一个?

打包chromium,生成官方的apk

具体实现见Chromium网络请求

安装打开chrome_public_apk,它其实就是chrome浏览器app,那么我们怎么实现一套webkit呢?

webkit解构

webkit的包结构(从快手这看,容易理解)

这是快手app解开后webkit下的内容,整体上看,其实就是拷贝一套系统的webkit的api
上图1 mProvider就是webview的实现类,2 则为系统实现,既然是自定义webkit为什么要有2呢

主要应该是两个原因

  • 是chromium内核包太大了

  • 实现快速修复问题的目的,这可能就是X5提到的安全这点能在24小时之内修复

所以为了处理以上这两个问题,chromium内核一般都是插件化的实现,因此在插件还没有加载到的时候,我们只能去展示系统的webview,这里回到第一张图片,webkit adapter包下的就包含了所有webview参数和返回对象的包装,意在实现两套webkit对应的转换,比如

这里我们使用的地方调用的是com.kuaishou.webkit.CookieManager.getInstance(),在自己的内核没有加载完成的时候,所有调用都中转到了系统的android.webkit.CookieManager

难点攻坚

  • 内核模块调整,我们需要调整内核代码,把所有指向系统webkit包名全部改成我们自定义webkit的包名,为了使内核编译能通过,我们需要拷贝我们自定义的webkit这个模块到内核里去,在编译的时候把其剔除,这里我们需要去稍微了解一下gn构建配置相关的内容。

  • 插件化的实现,插件化现在应该都烂大街了,省略。

// 这里稍微提一下,加载内核apk的Classloader的实现
public static class DexClassLoaderOptimize extends DexClassLoader {
  @Override // java.lang.ClassLoader
  public Class loadClass(String str, boolean z) throws ClassNotFoundException {
      // 所有系统相关的类,或者我们的webkit层全部在我们app层找

      if (str.startsWith("java.") || ((str.startsWith("android.")
&& !str.startsWith("android.support.")) ||
str.startsWith("com.kuaishou.webkit"))) {

          return super.loadClass(str, z);
      }
      // 否则直接在插件中找
      try {
          return findClass(str);
      } catch (Exception unused) {
          return super.loadClass(str, z);
      }
  }
}
  • 整个webkit层,可以参照着快手的实现。

  • 在内核的加载过程中,主要涉及两个动态库一个是webview.so,另一个是webview_plat_support.so,其中webview_plat_support.so是Android系统内部的一个so,内核渲染的一个支持模块,我们需要使用到这个模块去做渲染的事情。(实际上Android10版本以上其实并不依赖该so去实现渲染能力)

webview_plat_support问题攻坚

webview_plat_support.so(以下简称plat_support)

一开始想过直接拷贝一份对应的so到我们的内核里,但是plat_support依赖了一些系统的动态库,api23以后ndk的动态库(见public),有些我们是引用不到的, 另外不同版本可能不同机型的plat_support的实现也有些差异,内核里需要维护太多的plat_support,增加复杂度

采用反射回调系统实现(所有内核里的native方法转到系统webview的实现),一开始在android10上测试ok,以为绕过了该问题,然而当我们在Android10以下的版本上,同时开启系统WebView和自定义webview就出现问题了,运气好的话要么系统WebView黑屏,要么自定义的WebView黑屏,多数情况下会崩溃,主要是plat_support其中一个方法nativeSetChromiumAwDrawGLFunction,保存chromium 内部渲染出口对象是一个单例实现,所以会出现两边同时都调用到这个方法,要么系统WebView黑屏,要么系统自定义Webview黑屏,所以我们需要自己实现一个plat_support,一开始参考快手的实现,因为从第三个图中,我们看到它有对应的plat_support

同时还看到了,它对系统库libskia.so的调用

实现上主要是GraphicsUtils对应的两个native方法比较复杂涉及到GraphicBuffer类 GraphicBuffer这个类位于libui.so,然而快手的plat_support中并没有找到libui.so的调用,既然不引用libui.so,我们就要自己去创建对应的对象,这里我们可以把它改成结构体

匹配所有的成员变量,就可以强转成目标对象(这和java完全不一样,一般没有关联的类的实例不能强转成目标类,不能调用的目标类的方法),只是我们需要该类原本实现的方法在本地重新实现一次,然而目标类的方法里又有别的类的调用,延展开来,最终处理的内容就很多了,还有一大堆版本适配问题,到这里暂时hold。

好了那么我们再来看看X5的实现,

好家伙,这直接反回0,然后我立马测试了一下,通过~~~
也就是说前面海量的代码实现,其实是可以删除的,chromium内部有自己默认的实现(这里我们还是可以反射转回到系统webview的实现,包括nativeGetFunctionTable)。

剩下的我们只需处理以下几个native方法即可 这个几个方法处理就非常简单了,注意kAwDrawGLInfoVersion的版本号适配就Ok了。

结语

当我们整完这一套,就可以开始我们的内核定制了,包括小程序领域所谓的同城渲染能力,JS ServerWorker的定制等等。

作者:北纬34点8度
来源:https: //juejin.cn/post/7037807249103798285

收起阅读 »

Android静态代码扫描效率优化与实践(下)

前面分析了如何获取差异文件以及增量扫描的原理,分析的重点还是侧重在Lint工具本身的实现机制上。接下来分析,在Gradle中如何实现一个增量扫描任务。大家知道,通过执行./gradlew lint命令来执行Lint静态代码检测任务。创建一个新的Android工...
继续阅读 »


Android静态代码扫描效率优化与实践(下)
Lint增量扫描Gradle任务实现

前面分析了如何获取差异文件以及增量扫描的原理,分析的重点还是侧重在Lint工具本身的实现机制上。接下来分析,在Gradle中如何实现一个增量扫描任务。大家知道,通过执行./gradlew lint命令来执行Lint静态代码检测任务。创建一个新的Android工程,在Gradle任务列表中可以在Verification这个组下面找到几个Lint任务,如下所示:

Android静态代码扫描效率优化与实践_美团_12

这几个任务就是 Android Gradle插件在加载的时候默认创建的。分别对应于以下几个Task:

  • lint->LintGlobalTask:由TaskManager创建;

  • lintDebug、lintRelease、lintVitalRelease->LintPerVariantTask:由ApplicationTaskManager或者LibraryTaskManager创建,其中lintVitalRelease只在release下生成。

所以,在Android Gradle 插件中,应用于Lint的任务分别为LintGlobalTask和LintPerVariantTask。他们的区别是前者执行的是扫描所有Variant,后者执行只针对单独的Variant。而我们的增量扫描任务其实是跟Variant无关的,因为我们会把所有差异文件都收集到。无论是LintGlobalTask或者是LintPerVariantTask,都继承自LintBaseTask。最终的扫描任务在LintGradleExecution的runLint方法中执行,这个类位于lint-gradle-26.1.1中,前面提到这个库是基于Lint的API针对Gradle任务做的一些封装。

/** Runs lint on the given variant and returns the set of warnings */
  private Pair, LintBaseline> runLint(
          @Nullable Variant variant,
          @NonNull VariantInputs variantInputs,
          boolean report, boolean isAndroid) {
      IssueRegistry registry = createIssueRegistry(isAndroid);
      LintCliFlags flags = new LintCliFlags();
      LintGradleClient client =
              new LintGradleClient(
                      descriptor.getGradlePluginVersion(),
                      registry,
                      flags,
                      descriptor.getProject(),
                      descriptor.getSdkHome(),
                      variant,
                      variantInputs,
                      descriptor.getBuildTools(),
                      isAndroid);
      boolean fatalOnly = descriptor.isFatalOnly();
      if (fatalOnly) {
          flags.setFatalOnly(true);
      }
      LintOptions lintOptions = descriptor.getLintOptions();
      if (lintOptions != null) {
          syncOptions(
                  lintOptions,
                  client,
                  flags,
                  variant,
                  descriptor.getProject(),
                  descriptor.getReportsDir(),
                  report,
                  fatalOnly);
      } else {
          // Set up some default reporters
          flags.getReporters().add(Reporter.createTextReporter(client, flags, null,
                  new PrintWriter(System.out, true), false));
          File html = validateOutputFile(createOutputPath(descriptor.getProject(), null, ".html",
                  null, flags.isFatalOnly()));
          File xml = validateOutputFile(createOutputPath(descriptor.getProject(), null, DOT_XML,
                  null, flags.isFatalOnly()));
          try {
              flags.getReporters().add(Reporter.createHtmlReporter(client, html, flags));
              flags.getReporters().add(Reporter.createXmlReporter(client, xml, false));
          } catch (IOException e) {
              throw new GradleException(e.getMessage(), e);
          }
      }
      if (!report || fatalOnly) {
          flags.setQuiet(true);
      }
      flags.setWriteBaselineIfMissing(report && !fatalOnly);

      Pair, LintBaseline> warnings;
      try {
          warnings = client.run(registry);
      } catch (IOException e) {
          throw new GradleException("Invalid arguments.", e);
      }

      if (report && client.haveErrors() && flags.isSetExitCode()) {
          abort(client, warnings.getFirst(), isAndroid);
      }

      return warnings;
  }

我们在这个方法中看到了warnings = client.run(registry),这就是Lint扫描得到的结果集。总结一下这个方法中做了哪些准备工作用于Lint扫描:

  1. 创建IssueRegistry,包含了Lint内建的BuiltinIssueRegistry;

  2. 创建LintCliFlags;

  3. 创建LintGradleClient,这里面传入了一大堆参数,都是从Gradle Android 插件的运行环境中获得;

  4. 同步LintOptions,这一步是将我们在build.gralde中配置的一些Lint相关的DSL属性,同步设置给LintCliFlags,给真正的Lint 扫描核心库使用;

  5. 执行Client的Run方法,开始扫描。

扫描的过程上面的原理部分已经分析了,现在我们思考一下如何构造增量扫描的任务。我们已经分析到扫描的关键点是client.run(registry),所以我们需要构造一个Client来执行扫描。一个想法是通过反射来获取Client的各个参数,当然这个思路是可行的,我们也验证过实现了一个用反射方式构造的Client。但是反射这种方式有个问题是丢失了从Gradle任务执行到调用Lint API开始扫描这一过程中做的其他事情,侵入性比较高,所以我们最终采用继承LintBaseTask自行实现增量扫描任务的方式。

FindBugs扫描简介

FindBugs是一个静态分析工具,它检查类或者JAR 文件,通过Apache的 BCEL 库来分析Class,将字节码与一组缺陷模式进行对比以发现问题。FindBugs自身定义了一套缺陷模式,目前的版本3.0.1内置了总计300多种缺陷,详细可参考 官方文档 。FindBugs作为一个扫描的工具集,可以非常灵活的集成在各种编译工具中。接下来,我们主要分析在Gradle中FindBugs的相关内容。

Gradle FindBugs任务属性分析

在Gradle的内置任务中,有一个FindBugs的Task,我们看一下 官方文档 对Gradle属性的描述。

选几个比较重要的属性介绍:

  • Classes

该属性表示我们要分析的Class文件集合,通常我们会把编译结果的Class目录用于扫描。
  • Classpath

分析目标集合中的Class需要用到的所有相关的Classes路径,但是并不会分析它们自身,只用于扫描。
  • Effort

包含MIN,Default,MAX,级别越高,分析得越严谨越耗时。
  • findbugsClasspath

Finbugs库相关的依赖路径,用于配置扫描的引擎库。
  • reportLevel

报告级别,分为Low,Medium,High。如果为Low,所有Bug都报告,如果为High,仅报告High优先级。
  • Reports

扫描结果存放路径。

通过以上属性解释,不难发现要FindBugs增量扫描,只需要指定Classes的文件集合就可以了。

FindBugs任务增量扫描分析

在做增量扫描任务之前,我们先来看一下FindBugs IDEA插件是如何进行单个文件扫描的。

Android静态代码扫描效率优化与实践_Android教程_13

我们选择Analyze Current File对当前文件进行扫描,扫描结果如下所示:

Android静态代码扫描效率优化与实践_Android教程_14

可以看到确实只扫描了一个文件。那么扫描到底使用了哪些输入数据呢,我们可以通过扫描结果的提示清楚看到:

Android静态代码扫描效率优化与实践_美团_15

这里我们能看到很多有用的信息:

  • 源码目录列表,包含了工程中的Java目录,res目录,以及编译过程中生成的一些类目录;

  • 需要分析的目标Class集合,为编译后的Build目录下的当前Java文件对应的Class文件;

  • Aux Classpath Entries,表示分析上面的目标文件需要用到的类路径。

所以,根据IDEA的扫描结果来看,我们在做增量扫描的时候需要解决上面这几个属性的获取。在前面我们分析的属性是Gradle在FindBugs lib的基础上,定义的一套对应的Task属性。真正的Finbugs属性我们可以通过 官方文档 或者源码中查到。

配置AuxClasspath

前文提到,ClassPath是用来分析目标文件需要用到的相关依赖Class,但本身并不会被分析,所以我们需要尽可能全的找到所有的依赖库,否则在扫描的时候会报依赖的类库找不到。

FileCollection buildClasses = project.fileTree(dir: "${project.buildDir}/intermediates/classes/${variant.flavorName}/${variant.buildType.name}",includes: classIncludes)

FileCollection targetClasspath = project.files()
GradleUtils.collectDepProject(project, variant).each { targetProject ->
  GradleUtils.getAndroidVariants(targetProject).each { targetVariant ->
      if (targetVariant.name.capitalize().equalsIgnoreCase(variant.name.capitalize())) {
          targetClasspath += targetVariant.javaCompile.classpath
      }
  }
}

classpath = variant.javaCompile.classpath + targetClasspath + buildClasses
FindBugs增量扫描误报优化

对于增量文件扫描,参与的少数文件扫描在某些模式规则上可能会出现误判,但是全量扫描不会有问题,因为参与分析的目标文件是全集。举一个例子:

class A {
public static String buildTime = "";
....
}

静态变量buildTime会被认为应该加上Final,但是其实其他类会对这个变量赋值。如果单独扫描类A文件,就会报缺陷BUG_TYPE_MS_SHOULD_BE_FINAL。我们通过FindBugs-IDEA插件来扫描验证,也同样会有一样的问题。要解决此类问题,需要找到谁依赖了类A,并且一同参与扫描,同时也需要找出类A依赖了哪些文件,简单来说:需要找出与类A有直接关联的类。为了解决这个问题,我们通过ASM来找出相关的依赖,具体如下:

void findAllScanClasses(ConfigurableFileTree allClass) {
  allScanFiles = [] as HashSet
  String buildClassDir = "${project.buildDir}/$FINDBUGS_ANALYSIS_DIR/$FINDBUGS_ANALYSIS_DIR_ORIGIN"

  Set moduleClassFiles = allClass.files
  for (File file : moduleClassFiles) {
      String[] splitPath = file.absolutePath.split("$FINDBUGS_ANALYSIS_DIR/$FINDBUGS_ANALYSIS_DIR_ORIGIN/")
      if (splitPath.length > 1) {
          String className = getFileNameNoFlag(splitPath[1],'.')
          String innerClassPrefix = ""
          if (className.contains('$')) {
              innerClassPrefix = className.split('\\$')[0]
          }
          if (diffClassNamePath.contains(className) || diffClassNamePath.contains(innerClassPrefix)) {
              allScanFiles.add(file)
          } else {
              Iterable classToResolve = new ArrayList()
              classToResolve.add(file.absolutePath)
              Set dependencyClasses = Dependencies.findClassDependencies(project, new ClassAcceptor(), buildClassDir, classToResolve)
              for (File dependencyClass : dependencyClasses) {
                  if (diffClassNamePath.contains(getPackagePathName(dependencyClass))) {
                      allScanFiles.add(file)
                      break
                  }
              }
          }
      }
  }
}

通过以上方式,我们可以解决一些增量扫描时出现的误报情况,相比IDEA工具,我们更进一步降低了扫描部分文件的误报率。

CheckStyle增量扫描

相比而言,CheckStyle的增量扫描就比较简单了。CheckStyle对源码扫描,根据[ 官方文档]各个属性的描述,我们发现只要指定Source属性的值就可以指定扫描的目标文件。

void configureIncrementScanSource() {
  boolean isCheckPR = false
  DiffFileFinder diffFileFinder

  if (project.hasProperty(CodeDetectorExtension.CHECK_PR)) {
      isCheckPR = project.getProperties().get(CodeDetectorExtension.CHECK_PR)
  }

  if (isCheckPR) {
      diffFileFinder = new DiffFileFinderHelper.PRDiffFileFinder()
  } else {
      diffFileFinder = new DiffFileFinderHelper.LocalDiffFileFinder()
  }

  source diffFileFinder.findDiffFiles(project)

  if (getSource().isEmpty()) {
      println '没有找到差异java文件,跳过checkStyle检测'
  }
}

优化结果数据

经过全量扫描和增量扫描的优化,我们整个扫描效率得到了很大提升,一次PR构建扫描效率整体提升50%+。优化数据如下:

Android静态代码扫描效率优化与实践_Android教程_16

落地与沉淀

扫描工具通用性

解决了扫描效率问题,我们想怎么让更多的工程能低成本的使用这个扫描插件。对于一个已经存在的工程,如果没有使用过静态代码扫描,我们希望在接入扫描插件后续新增的代码能够保证其经过增量扫描没有问题。而老的存量代码,由于代码量过大增量扫描并没有效率上的优势,我们希望可以使用全量扫描逐步解决存量代码存在的问题。同时,为了配置工具的灵活,也提供配置来让接入方自己决定选择接入哪些工具。这样可以让扫描工具同时覆盖到新老项目,保证其通用。所以,要同时支持配置使用增量或者全量扫描任务,并且提供灵活的选择接入哪些扫描工具

扫描完整性保证

前面提到过,在FindBugs增量扫描可能会出现因为参与分析的目标文件集不全导致的某类匹配规则误报,所以在保证扫描效率的同时,也要保证扫描的完整性和准确性。我们的策略是以增量扫描为主,全量扫描为辅,PR提交使用增量扫描提高效率,在CI配置Daily Build使用全量扫描保证扫描完整和不遗漏

我们在自己的项目中实践配置如下:

apply plugin: 'code-detector'

codeDetector {
  // 配置静态代码检测报告的存放位置
  reportRelativePath = rootProject.file('reports')

  /**
    * 远程仓库地址,用于配置提交pr时增量检测
    */
  upstreamGitUrl = "ssh://git@xxxxxxxx.git"

  checkStyleConfig {
      /**
        * 开启或关闭 CheckStyle 检测
        * 开启:true
        * 关闭:false
        */
      enable = true
      /**
        * 出错后是否要终止检查
        * 终止:false
        * 不终止:true。配置成不终止的话 CheckStyleTask 不会失败,也不会拷贝错误报告
        */
      ignoreFailures = false
      /**
        * 是否在日志中展示违规信息
        * 显示:true
        * 不显示:false
        */
      showViolations = true
      /**
        * 统一配置自定义的 checkstyle.xml 和 checkstyle.xsl 的 uri
        * 配置路径为:
        *     "${checkStyleUri}/checkstyle.xml"
        *     "${checkStyleUri}/checkstyle.xsl"
        *
        * 默认为 null,使用 CodeDetector 中的默认配置
        */
      checkStyleUri = rootProject.file('codequality/checkstyle')
  }

  findBugsConfig {
      /**
        * 开启或关闭 Findbugs 检测
        * 开启:true
        * 关闭:false
        */
      enable = true
      /**
        * 可选项,设置分析工作的等级,默认值为 max
        * min, default, or max. max 分析更严谨,报告的 bug 更多. min 略微少些
        */
      effort = "max"
      /**
        * 可选项,默认值为 high
        * low, medium, high. 如果是 low 的话,那么报告所有的 bug
        */
      reportLevel = "high"
      /**
        * 统一配置自定义的 findbugs_include.xml 和 findbugs_exclude.xml 的 uri
        * 配置路径为:
        *     "${findBugsUri}/findbugs_include.xml"
        *     "${findBugsUri}/findbugs_exclude.xml"
        * 默认为 null,使用 CodeDetector 中的默认配置
        */
      findBugsUri = rootProject.file('codequality/findbugs')
  }

  lintConfig {

      /**
        * 开启或关闭 lint 检测
        * 开启:true
        * 关闭:false
        */
      enable = true

      /**
        * 统一配置自定义的 lint.xml 和 retrolambda_lint.xml 的 uri
        * 配置路径为:
        *     "${lintConfigUri}/lint.xml"
        *     "${lintConfigUri}/retrolambda_lint.xml"
        * 默认为 null,使用 CodeDetector 中的默认配置
        */
      lintConfigUri = rootProject.file('codequality/lint')
  }
}

我们希望扫描插件可以灵活指定增量扫描还是全量扫描以应对不同的使用场景,比如已存在项目的接入、新项目的接入、打包时的检测等。

执行脚本示例:

./gradlew ":${appModuleName}:assemble${ultimateVariantName}" -PdetectorEnable=true -PcheckStyleIncrement=true -PlintIncrement=true -PfindBugsIncrement=true -PcheckPR=${checkPR} -PsourceCommitHash=${sourceCommitHash} -PtargetBranch=${targetBranch} --stacktrace

希望一次任务可以暴露所有扫描工具发现的问题,当某一个工具扫描到问题后不终止任务,如果是本地运行在发现问题后可以自动打开浏览器方便查看问题原因。

def finalizedTaskArray = [lintTask,checkStyleTask,findbugsTask]
checkCodeTask.finalizedBy finalizedTaskArray

"open ${reportPath}".execute()

为了保证提交的PR不会引起打包问题影响包的交付,在PR时触发的任务实际为打包任务,我们将静态代码扫描任务挂接在打包任务中。由于我们的项目是多Flavor构建,在CI上我们将触发多个Job同时执行对应Flavor的增量扫描和打包任务。同时为了保证代码扫描的完整性,我们在真正的打包Job上执行全量扫描。

总结与展望

本文主要介绍了在静态代码扫描优化方面的一些思路与实践,并重点探讨了对Lint、FindBugs、CheckStyle增量扫描的一些尝试。通过对扫描插件的优化,我们在代码扫描的效率上得到了提升,同时在实践过程中我们也积累了自定义Lint检测规则的方案,未来我们将配合基础设施标准化建设,结合静态扫描插件制定一些标准化检测规则来更好的保证我们的代码规范以及质量。

参考资料

作者简介

鸿耀,美团餐饮生态技术团队研发工程师。

作者:美团技术团队 · 鸿耀
来源:https://blog.51cto.com/u_15197658/2768467

收起阅读 »

Android静态代码扫描效率优化与实践(上)

背景与问题思考与策略思考一:现有插件包含的扫描工具是否都是必需的?为了验证扫描工具的必要性,我们关心以下一些维度:经过以上的对比分析我们发现,工具的诞生都能针对性解决某一领域问题。CheckStyle的扫描速度快效率高,对代码风格和圈复杂度支持友好;FindB...
继续阅读 »



小伙伴们,美美又来推荐干货文章啦~本文为美团研发同学实战经验,主要介绍Android静态扫描工具Lint、CheckStyle、FindBugs在扫描效率优化上的一些探索和实践,希望大家喜欢鸭。

背景与问题

DevOps实践中,我们在CI(Continuous Integration)持续集成过程主要包含了代码提交、静态检测、单元测试、编译打包环节。其中静态代码检测可以在编码规范,代码缺陷,性能等问题上提前预知,从而保证项目的交付质量。Android项目常用的静态扫描工具包括CheckStyle、Lint、FindBugs等,为降低接入成本,美团内部孵化了静态代码扫描插件,集合了以上常用的扫描工具。项目初期引入集团内部基建时我们接入了代码扫描插件,在PR(Pull Request)流程中借助Jenkins插件来触发自动化构建,从而达到监控代码质量的目的。初期单次构建耗时平均在1~2min左右,对研发效率影响甚少。但是随着时间推移,代码量随业务倍增,项目也开始使用Flavor来满足复杂的需求,这使得我们的单次PR构建达到了8~9min左右,其中静态代码扫描的时长约占50%,持续集成效率不高,对我们的研发效率带来了挑战。

思考与策略

针对以上的背景和问题,我们思考以下几个问题:

思考一:现有插件包含的扫描工具是否都是必需的?

扫描工具对比

为了验证扫描工具的必要性,我们关心以下一些维度:

  • 扫码侧重点,对比各个工具分别能针对解决什么类型的问题;

  • 内置规则种类,列举各个工具提供的能力覆盖范围;

  • 扫描对象,对比各个工具针对什么样的文件类型扫描;

  • 原理简介,简单介绍各个工具的扫描原理;

  • 优缺点,简单对比各个工具扫描效率、扩展性、定制性、全面性上的表现。

Android静态代码扫描效率优化与实践_美团_03

注:FindBugs只支持Java1.0~1.8,已经被SpotBugs替代。鉴于部分老项目并没有迁移到Java8,目前我们并没有使用SpotBugs代替FindBugs的原因如下,详情参考 官方文档
Android静态代码扫描效率优化与实践_美团_04
同时,SpotBugs的作者也在 讨论是否让SpotBugs支持老的Java版本,结论是不提供支持。

经过以上的对比分析我们发现,工具的诞生都能针对性解决某一领域问题。CheckStyle的扫描速度快效率高,对代码风格和圈复杂度支持友好;FindBugs针对Java代码潜在问题,能帮助我们发现编码上的一些错误实践以及部分安全问题和性能问题;Lint是官方深度定制,功能极其强大,且可定制性和扩展性以及全面性都表现良好。所以综合考虑,针对思考一,我们的结论是整合三种扫描工具,充分利用每一个工具的领域特性。

思考二:是否可以优化扫描过程?

既然选择了整合这几种工具,我们面临的挑战是整合工具后扫描效率的问题,首先来分析目前的插件到底耗时在哪里。

静态代码扫描耗时分析

Android项目的构建依赖Gradle工具,一次构建过程实际上是执行所有的Gradle Task。由于Gradle的特性,在构建时各个Module都需要执行CheckStyle、FindBugs、Lint相关的Task。对于Android来说,Task的数量还与其构建变体Variant有关,其中Variant = Flavor * BuildType。所以一个Module执行的相关任务可以由以下公式来描述:Flavor * BuildType (Lint,CheckStyle,Findbugs),其中为笛卡尔积。如下图所示:

Android静态代码扫描效率优化与实践_美团_05

可以看到,一次构建全量扫描执行的Task跟Varint个数正相关。对于现有工程的任务,我们可以看一下目前各个任务的耗时情况:(以实际开发中某一次扫描为例)

Android静态代码扫描效率优化与实践_Android开发_06

通过对Task耗时排序,主要的耗时体现在FindBugs和Lint对每一个Module的扫描任务上,CheckStyle任务并不占主要影响。整体来看,除了工具本身的扫描时间外,耗时主要分为多Module多Variant带来的任务数量耗时。

优化思路分析

对于工具本身的扫描时间,一方面受工具自身扫描算法和检测规则的影响,另一方面也跟扫描的文件数量相关。针对源码类型的工具比如CheckStyle和Lint,需要经过词法分析、语法分析生成抽象语法树,再遍历抽象语法树跟定义的检测规则去匹配;而针对字节码文件的工具FindBugs,需要先编译源码成Class文件,再通过BCEL分析字节码指令并与探测器规则匹配。如果要在工具本身算法上去寻找优化点,代价比较大也不一定能找到有效思路,投入产出比不高,所以我们把精力放在减少Module和Variant带来的影响上。

从上面的耗时分析可以知道,Module和Variant数直接影响任务数量, 一次PR提交的场景是多样的,比如多Module多Variant都有修改,所以要考虑这些都修改的场景。先分析一个Module多Variant的场景,考虑到不同的Variant下源代码有一定差异,并且FindBugs扫描针对的是Class文件,不同的Variant都需要编译后才能扫描,直接对多Variant做处理比较复杂。我们可以简化问题,用以空间换时间的方式,在提交PR的时候根据Variant用不同的Jenkins Job来执行每一个Variant的扫描任务。所以接下来的问题就转变为如何优化在扫描单个Variant的时候多Module任务带来的耗时。

对于Module数而言,我们可以将其抽取成组件,拆分到独立仓库,将扫描任务拆分到各自仓库的变动时期,以aar的形式集成到主项目来减少Module带来的任务数。那对于剩下的Module如何优化呢?无论是哪一种工具,都是对其输入文件进行处理,CheckStyle对Java源代码文件处理,FindBugs对Java字节码文件处理,如果我们可以通过一次任务收集到所有Module的源码文件和编译后的字节码文件,我们就可以减少多Module的任务了。所以对于全量扫描,我们的主要目标是来解决如何一次性收集所有Module的目标文件

思考三:是否支持增量扫描?

上面的优化思路都是基于全量扫描的,解决的是多Module多Variant带来的任务数量耗时。前面提到,工具本身的扫描时间也跟扫描的文件数量有关,那么是否可以从扫描的文件数量来入手呢?考虑平时的开发场景,提交PR时只是部分文件修改,我们没必要把那些没修改过的存量文件再参与扫描,而只针对修改的增量文件扫描,这样能很大程度降低无效扫描带来的效率问题。有了思路,那么我们考虑以下几个问题:

  • 如何收集增量文件,包括源码文件和Class文件?

  • 现在业界是否有增量扫描的方案,可行性如何,是否适用我们现状?

  • 各个扫描工具如何来支持增量文件的扫描?

根据上面的分析与思考路径,接下来我们详细介绍如何解决上述问题。

优化探索与实践

全量扫描优化

搜集所有Module目标文件集

获取所有Module目标文件集,首先要找出哪些Module参与了扫描。一个Module工程在Gradle构建系统中被描述为一个“Project”,那么我们只需要找出主工程依赖的所有Project即可。由于依赖配置的多样性,我们可以选择在某些Variant下依赖不同的Module,所以获取参与一次构建时与当前Variant相关的Project对象,我们可以用如下方式:

static Set collectDepProject(Project project, BaseVariant variant, Set result = null) {
if (result == null) {
  result = new HashSet<>()
}
Set taskSet = variant.javaCompiler.taskDependencies.getDependencies(variant.javaCompiler)
taskSet.each { Task task ->
  if (task.project != project && hasAndroidPlugin(task.project)) {
    result.add(task.project)
    BaseVariant childVariant = getVariant(task.project)
    if (childVariant.name == variant.name || "${variant.flavorName}${childVariant.buildType.name}".toLowerCase() == variant.name.toLowerCase()) {
      collectDepProject(task.project, childVariant, result)
    }
  }
}
return result
}

目前文件集分为两类,一类是源码文件,另一类是字节码文件,分别可以如下处理:

projectSet.each { targetProject ->
if (targetProject.plugins.hasPlugin(CodeDetectorPlugin) && GradleUtils.hasAndroidPlugin(targetProject)) {
  GradleUtils.getAndroidExtension(targetProject).sourceSets.all { AndroidSourceSet sourceSet ->
    if (!sourceSet.name.startsWith("test") && !sourceSet.name.startsWith(SdkConstants.FD_TEST)) {
      source sourceSet.java.srcDirs
    }
  }
}
}

注:上面的Source是CheckStyle Task的属性,用其来指定扫描的文件集合;

// 排除掉一些模板代码class文件
static final Collection defaultExcludes = (androidDataBindingExcludes + androidExcludes + butterKnifeExcludes + dagger2Excludes).asImmutable()

List allClassesFileTree = new ArrayList<>()
ConfigurableFileTree currentProjectClassesDir = project.fileTree(dir: variant.javaCompile.destinationDir, excludes: defaultExcludes)
allClassesFileTree.add(currentProjectClassesDir)
GradleUtils.collectDepProject(project, variant).each { targetProject ->
if (targetProject.plugins.hasPlugin(CodeDetectorPlugin) && GradleUtils.hasAndroidPlugin(targetProject)) {
  // 可能有的工程没有Flavor只有buildType
    GradleUtils.getAndroidVariants(targetProject).each { BaseVariant targetProjectVariant ->
    if (targetProjectVariant.name == variant.name || "${targetProjectVariant.name}".toLowerCase() == variant.buildType.name.toLowerCase()) {
        allClassesFileTree.add(targetProject.fileTree(dir: targetProjectVariant.javaCompile.destinationDir, excludes: defaultExcludes))
    }
  }
}
}

注:收集到字节码文件集后,可以用通过FindBugsTask 的 Class 属性指定扫描,后文会详细介绍FindBugs Task相关属性。

对于Lint工具而言,相应的Lint Task并没有相关属性可以指定扫描文件,所以在全量扫描上,我们暂时没有针对Lint做优化。

全量扫描优化数据

通过对CheckStyle和FindBugs全量扫描的优化,我们将整体扫描时间由原来的9min降低到了5min左右。

Android静态代码扫描效率优化与实践_美团_07

增量扫描优化

由前面的思考分析我们知道,并不是所有的文件每次都需要参与扫描,所以我们可以通过增量扫描的方式来提高扫描效率。

增量扫描技术调研

在做具体技术方案之前,我们先调研一下业界的现有方案,调研如下:

Android静态代码扫描效率优化与实践_Android教程_08

针对Lint,我们可以借鉴现有实现思路,同时深入分析扫描原理,在3.x版本上寻找出增量扫描的解决方案。对于CheckStyle和FindBugs,我们需要了解工具的相关配置参数,为其指定特定的差异文件集合。

注:业界有一些增量扫描的案例,例如 diff_cover,此工具主要是对单元测试整体覆盖率的检测,以增量代码覆盖率作为一个指标来衡量项目的质量,但是这跟我们的静态代码分析的需求不太符合。它有一个比较好的思路是找出差异的代码行来分析覆盖率,粒度比较细。但是对于静态代码扫描,仅仅的差异行不足以完成上下文的语义分析,尤其是针对FindBugs这类需要分析字节码的工具,获取的差异行还需要经过编译成Class文件才能进行分析,方案并不可取。

寻找增量修改文件

增量扫描的第一步是获取待扫描的目标文件。我们可以通过git diff命令来获取差异文件,值得注意的是对于删除的文件和重命名的文件需要忽略,我们更关心新增和修改的文件,并且只需要获取差异文件的路径就好了。举个例子:git diff --name-only --diff-filter=dr commitHash1 commitHash2,以上命令意思是对比两次提交记录的差异文件并获取路径,过滤删除和重命名的文件。对于寻找本地仓库的差异文件上面的命令已经足够了,但是对于PR的情况还有一些复杂,需要对比本地代码与远程仓库目标分支的差异。集团的代码管理工具在Jenkins上有相应的插件,该插件默认提供了几个参数,我们需要用到以下两个:

  • ${targetBranch}:需要合入代码的目标分支地址;

  • ${sourceCommitHash}:需要提交的代码hash值。

通过这两个参数执行以下一系列命令来获取与远程目标分支的差异文件。

git remote add upstream ${upstreamGitUrl}
git fetch upstream ${targetBranch}
git diff --name-only --diff-filter=dr $sourceCommitHash upstream/$targetBranch
  1. 配置远程分支别名为UpStream,其中upstreamGitUrl可以在插件提供的配置属性中设置;

  2. 获取远程目标分支的更新;

  3. 比较分支差异获取文件路径。

通过以上方式,我们找到了增量修改文件集。

Lint扫描原理分析

在分析Lint增量扫描原理之前,先介绍一下Lint扫描的工作流程:

Android静态代码扫描效率优化与实践_Android教程_09

App Source Files

项目中的源文件,包括Java、XML、资源文件、proGuard等。

lint.xml

用于配置希望排除的任何 Lint 检查以及自定义问题严重级别,一般各个项目都会根据自身项目情况自定义的lint.xml来排除一些检查项。

lint Tool

一套完整的扫描工具用于对Android的代码结构进行分析,可以通过命令行、IDEA、Gradle命令三种方式运行lint工具。

lint Output

Lint扫描的输出结果。

从上面可以看出,Lint Tool就像一个加工厂,对投入进来的原料(源代码)进行加工处理(各种检测器分析),得到最终的产品(扫描结果)。Lint Tool作为一个扫描工具集,有多种使用方式。Android为我们提供了三种运行方式,分别是命令行、IDEA、Gradle任务。这三种方式最终都殊途同归,通过LintDriver来实现扫描。如下图所示:

Android静态代码扫描效率优化与实践_美团_10

为了方便查看源码,新建一个工程,在build.gradle脚本中,添加如下依赖:

compile 'com.android.tools.build:gradle:3.1.1'
compile 'com.android.tools.lint:lint-gradle:26.1.1'

我们可以得到如下所示的依赖:

Android静态代码扫描效率优化与实践_Android教程_11

lint-api-26.1.1

Lint工具集的一个封装,实现了一组API接口,用于启动Lint。

lint-checks-26.1.1

一组内建的检测器,用于对这种描述好Issue进行分析处理。

lint-26.1.1

可以看做是依赖上面两个jar形成的一个基于命令行的封装接口形成的脚手架工程,我们的命令行、Gradle任务都是继承自这个jar包中相关类来做的实现。

lint-gradle-26.1.1

可以看做是针对Gradle任务这种运行方式,基于lint-26.1.1做了一些封装类。

lint-gradle-api-26.1.1

真正Gradle Lint任务在执行时调用的入口。

在理解清楚了以上几个jar的关系和作用之后,我们可以发现Lint的核心库其实是前三个依赖。后面两个其实是基于脚手架,对Gradle这种运行方式做的封装。最核心的逻辑在LintDriver的Analyze方法中。

fun analyze() {

  ...省略部分代码...

   for (project in projects) {
       fireEvent(EventType.REGISTERED_PROJECT, project = project)
  }
   registerCustomDetectors(projects)

  ...省略部分代码...

   try {
       for (project in projects) {
           phase = 1

           val main = request.getMainProject(project)

           // The set of available detectors varies between projects
           computeDetectors(project)

           if (applicableDetectors.isEmpty()) {
               // No detectors enabled in this project: skip it
               continue
          }

           checkProject(project, main)
           if (isCanceled) {
               break
          }

           runExtraPhases(project, main)
      }
  } catch (throwable: Throwable) {
       // Process canceled etc
       if (!handleDetectorError(null, this, throwable)) {
           cancel()
      }
  }
  ...省略部分代码...
}

主要是以下三个重要步骤:

registerCustomDetectors(projects)

Lint为我们提供了许多内建的检测器,除此之外我们还可以自定义一些检测器,这些都需要注册进Lint工具用于对目标文件进行扫描。这个方法主要做以下几件事情:

  1. 遍历每一个Project和它的依赖Library工程,通过client.findRuleJars来找出自定义的jar包;

  2. 通过client.findGlobalRuleJars找出全局的自定义jar包,可以作用于每一个Android工程;

  3. 从找到的jarFiles列表中,解析出自定义的规则,并与内建的Registry一起合并为CompositeIssueRegistry;需要注意的是,自定义的Lint的jar包存放位置是build/intermediaters/lint目录,如果是需要每一个工程都生效,则存放位置为~/.android/lint/

computeDetectors(project)

这一步主要用来收集当前工程所有可用的检测器。

checkProject(project, main)

接下来这一步是最为关键的一步。在此方法中,调用runFileDetectors来进行文件扫描。Lint支持的扫描文件类型很多,因为是官方支持,所以针对Android工程支持的比较友好。一次Lint任务运行时,Lint的扫描范围主要由Scope来描述。具体表现在:

fun infer(projects: Collection?): EnumSet {
          if (projects == null || projects.isEmpty()) {
              return Scope.ALL
          }

          // Infer the scope
          var scope = EnumSet.noneOf(Scope::class.java)
          for (project in projects) {
              val subset = project.subset
              if (subset != null) {
                  for (file in subset) {
                      val name = file.name
                      if (name == ANDROID_MANIFEST_XML) {
                          scope.add(MANIFEST)
                      } else if (name.endsWith(DOT_XML)) {
                          scope.add(RESOURCE_FILE)
                      } else if (name.endsWith(DOT_JAVA) || name.endsWith(DOT_KT)) {
                          scope.add(JAVA_FILE)
                      } else if (name.endsWith(DOT_CLASS)) {
                          scope.add(CLASS_FILE)
                      } else if (name.endsWith(DOT_GRADLE)) {
                          scope.add(GRADLE_FILE)
                      } else if (name == OLD_PROGUARD_FILE || name == FN_PROJECT_PROGUARD_FILE) {
                          scope.add(PROGUARD_FILE)
                      } else if (name.endsWith(DOT_PROPERTIES)) {
                          scope.add(PROPERTY_FILE)
                      } else if (name.endsWith(DOT_PNG)) {
                          scope.add(BINARY_RESOURCE_FILE)
                      } else if (name == RES_FOLDER || file.parent == RES_FOLDER) {
                          scope.add(ALL_RESOURCE_FILES)
                          scope.add(RESOURCE_FILE)
                          scope.add(BINARY_RESOURCE_FILE)
                          scope.add(RESOURCE_FOLDER)
                      }
                  }
              } else {
                  // Specified a full project: just use the full project scope
                  scope = Scope.ALL
                  break
              }
          }
}

可以看到,如果Project的Subset为Null,Scope就为Scope.ALL,表示本次扫描会针对能检测的所有范围,相应地在扫描时也会用到所有全部的Detector来扫描文件;

如果Project的Subset不为Null,就遍历Subset的集合,找出Subset中的文件分别对应哪些范围。其实到这里我们已经可以知道,Subset就是我们增量扫描的突破点。接下来我们看一下runFileDetectors:

if(scope.contains(Scope.JAVA_FILE)||scope.contains(Scope.ALL_JAVA_FILES)){
val checks = union(scopeDetectors[Scope.JAVA_FILE],scopeDetectors[Scope.ALL_JAVA_FILES])
if (checks != null && !checks.isEmpty()) {
  val files = project.subset
  if (files != null) {
    checkIndividualJavaFiles(project, main, checks, files)
  } else {
    val sourceFolders = project.javaSourceFolders
    val testFolders = if (scope.contains(Scope.TEST_SOURCES))
    project.testSourceFolders
    else
    emptyList ()
    val generatedFolders = if (isCheckGeneratedSources)
    project.generatedSourceFolders
    else
    emptyList ()
    checkJava(project, main, sourceFolders, testFolders, generatedFolders, checks)
  }
}
}

这里更加明确,如果project.subset不为空,就对单独的Java文件扫描,否则,就对源码文件和测试目录以及自动生成的代码目录进行扫描。整个runFileDetectors的扫描顺序入下:

  1. Scope.MANIFEST

  2. Scope.ALL_RESOURCE_FILES)|| scope.contains(Scope.RESOURCE_FILE) ||

    scope.contains(Scope.RESOURCE_FOLDER) || scope.contains(Scope.BINARY_RESOURCE_FILE)

  3. scope.contains(Scope.JAVA_FILE) || scope.contains(Scope.ALL_JAVA_FILES)

  4. scope.contains(Scope.CLASS_FILE) || scope.contains(Scope.ALL_CLASS_FILES) ||

    scope.contains(Scope.JAVA_LIBRARIES)

  5. scope.contains(Scope.GRADLE_FILE)

  6. scope.contains(Scope.OTHER)

  7. scope.contains(Scope.PROGUARD_FILE)

  8. scope.contains(Scope.PROPERTY_FILE)

官方文档的描述顺序一致。

现在我们已经知道,增量扫描的突破点其实是需要构造project.subset对象。

    /**
    * Adds the given file to the list of files which should be checked in this
    * project. If no files are added, the whole project will be checked.
    *
    * @param file the file to be checked
    */
  public void addFile(@NonNull File file) {
      if (files == null) {
          files = new ArrayList<>();
      }
      files.add(file);
  }

  /**
    * The list of files to be checked in this project. If null, the whole
    * project should be checked.
    *
    * @return the subset of files to be checked, or null for the whole project
    */
  @Nullable
  public List getSubset() {
      return files;
  }

注释也很明确的说明了只要Files不为Null,就会扫描指定文件,否则扫描整个工程。

Android静态代码扫描效率优化与实践(下)

作者:美团技术团队 · 鸿耀
来源:https://blog.51cto.com/u_15197658/2768467

收起阅读 »

安卓客服云集成机器人欢迎语

1.会话分配 给APP渠道指定全天机器人2.机器人欢迎语打开,并指定一个菜单3.代码部分/** * 保存欢迎语到本地 */ public void saveMessage(){ Message message = Messa...
继续阅读 »
1.会话分配 给APP渠道指定全天机器人


2.机器人欢迎语打开,并指定一个菜单


3.代码部分

/**
* 保存欢迎语到本地
*/
public void saveMessage(){
Message message = Message.createReceiveMessage(Message.Type.TXT);
String str = Preferences.getInstance().getRobotWelcome();
EMTextMessageBody body = null;
if(!isRobotMenu(str)){
body = new EMTextMessageBody(str);
}else{
try{
body = new EMTextMessageBody("");
JSONObject msgtype = new JSONObject(str);
message.setAttribute("msgtype",msgtype);
}catch (Exception e){
Log.e("RobotMenu","onError:"+e.getMessage());
}
}
message.setFrom(toChatUsername);
message.addBody(body);
message.setMsgTime(System.currentTimeMillis());
message.setStatus(Message.Status.SUCCESS);
message.setMsgId(UUID.randomUUID().toString());

ChatClient.getInstance().chatManager().saveMessage(message);
messageList.refresh();
}

/**
* 判断机器人欢迎语是否是菜单类型
*
* @return
*/
private boolean isRobotMenu(String str) {
try {
JSONObject json = new JSONObject(str);
JSONObject obj = json.getJSONObject("choice");
} catch (Exception e) {
return false;
}
return true;
}

public void getNewRobotWelcome(String toChatUsername, MessageList messageList) {
this.toChatUsername=toChatUsername;
new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient okHttpClient = new OkHttpClient();
//需要替换为自己的参数
String tenantid = "67386";//需要替换为自己的tenantid(管理员模式->账户->账户信息->租户ID一栏)
String orgname = "1473190314068186";//appkey # 前半部分
String appname = "kefuchannelapp67386";//appkey # 后半部分
String username = toChatUsername;//IM 服务号
String token = ChatClient.getInstance().accessToken();//用户token
// String url = "http://kefu.easemob.com/v1/webimplugin/tenants/robots/welcome?channelType=easemob&originType=app&tenantId=" + tenantid + "&orgName=" + orgname + "&appName=" + appname + "&userName=" + username + "&token=" + token;
String url = "https://kefu.easemob.com/v1/webimplugin/tenants/robots/welcome?channelType=easemob&originType=app&tenantId=95739&orgName=1404210708092119&appName=kefuchannelapp95739&userName=kefuchannelimid_548067&token=YWMtamWiyuByEeuml5FFEs_ewo740PDfnhHrjuLfDWx-sxgBU8F64G8R65kl_RFfGcMJAwMAAAF6iaJgxwBPGgDFrFY27hqYUwtUP5mDC0wRg1jcOkfkyEVs38cgDdmEQw";
Request request = new Request.Builder().url(url).get().build();
try {
Response response = okHttpClient.newCall(request).execute();
String result = response.body().string();
JSONObject obj = new JSONObject(result);
Log.e("newwelcome----", obj.getJSONObject("entity").getString("greetingText"));
int type = obj.getJSONObject("entity").getInt("greetingTextType");
final String rob_welcome = obj.getJSONObject("entity").getString("greetingText");
//type0代表是文字消息的机器人欢迎语
//type1代表是菜单消息的机器人欢迎语
if (type == 0) {
//把解析拿到的string保存在本地
Preferences.getInstance().setRobotWelcome(rob_welcome);

} else if (type == 1) {
final String str = rob_welcome.replaceAll("&amp;quot;", "\"");
JSONObject json = new JSONObject(str);
JSONObject ext = json.getJSONObject("ext");
final JSONObject msgtype = ext.getJSONObject("msgtype");
//把解析拿到的string保存在本地
Preferences.getInstance().setRobotWelcome(msgtype.toString());
}
} catch (JSONException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();

ChatClient.getInstance().chatManager().getCurrentSessionId(toChatUsername, new ValueCallBack() {
@Override
public void onSuccess(String value) {
Log.e("TAG value:", value);
//当返回value不为空时,则返回的当前会话的会话ID,也就是说会话正在咨询中,不需要发送欢迎语
if (value.isEmpty()) {//
saveMessage();
}
}

@Override
public void onError(int error, String errorMsg) {

}
});
}





收起阅读 »

女儿拿着小天才电话手表问我App启动流程(下)

接 女儿拿着小天才电话手表问我App启动流程(上) 第四关:ActivityThread闪亮登场刚才说到由Zygote进行fork进程,并返回新进程的pid。其实这过程中也实例化ActivityThread对象。一起看看是怎么实现的: //RuntimeIni...
继续阅读 »

女儿拿着小天才电话手表问我App启动流程(上)


第四关:ActivityThread闪亮登场

刚才说到由Zygote进行fork进程,并返回新进程的pid。其实这过程中也实例化ActivityThread对象。一起看看是怎么实现的:


//RuntimeInit.java
protected static Runnable findStaticMain(String className, String[] argv,
ClassLoader classLoader) {
Class<?> cl;

try {
cl = Class.forName(className, true, classLoader);
} catch (ClassNotFoundException ex) {
throw new RuntimeException(
"Missing class when invoking static main " + className,
ex);
}

Method m;
try {
m = cl.getMethod("main", new Class[] { String[].class });
} catch (NoSuchMethodException ex) {
throw new RuntimeException(
"Missing static main on " + className, ex);
} catch (SecurityException ex) {
throw new RuntimeException(
"Problem getting static main on " + className, ex);
}
//...
return new MethodAndArgsCaller(m, argv);
}

原来是反射!通过反射调用了ActivityThread 的 main 方法。ActivityThread大家应该都很熟悉了,代表了Android的主线程,而main方法也是app的主入口。这不对上了!新建进程的时候就调用了,可不是主入口嘛。来看看这个主入口。


public static void main(String[] args) {
//...
Looper.prepareMainLooper();

ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);

//...

if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
//...
Looper.loop();

throw new RuntimeException("Main thread loop unexpectedly exited");
}

main方法主要创建了ActivityThread,创建了主线程的Looper对象,并开始loop循环。除了这些,还要告诉AMS,我醒啦,进程创建好了!也就是上述代码中的attach方法,最后会转到AMSattachApplicationLocked方法,一起看看这个方法干了啥:


//ActivitymanagerService.java
private final boolean attachApplicationLocked(IApplicationThread thread,
int pid, int callingUid, long startSeq) {
//...
ProcessRecord app;
//...
thread.bindApplication(processName, appInfo, providers, null, profilerInfo,
null, null, null, testMode,
mBinderTransactionTrackingEnabled, enableTrackAllocation,
isRestrictedBackupMode || !normalMode, app.isPersistent(),
new Configuration(app.getWindowProcessController().getConfiguration()),
app.compat, getCommonServicesLocked(app.isolated),
mCoreSettingsObserver.getCoreSettingsLocked(),
buildSerial, autofillOptions, contentCaptureOptions);
//...
app.makeActive(thread, mProcessStats);

//...
// See if the top visible activity is waiting to run in this process...
if (normalMode) {
try {
didSomething = mAtmInternal.attachApplication(app.getWindowProcessController());
} catch (Exception e) {
Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
badApp = true;
}
}
//...
}

//ProcessRecord.java
public void makeActive(IApplicationThread _thread, ProcessStatsService tracker) {
//...
thread = _thread;
mWindowProcessController.setThread(thread);
}

这里主要做了三件事:



  • bindApplication方法,主要用来启动Application。
  • makeActive方法,设定WindowProcessController里面的线程,也就是上文中说过判断进程是否存在所用到的。
  • attachApplication方法,启动根Activity。

第五关:创建Application

接着上面看,按照我们所熟知的,应用启动后,应该就是启动Applicaiton,启动Activity。看看是不是怎么回事:


    //ActivityThread#ApplicationThread
public final void bindApplication(String processName, ApplicationInfo appInfo,
List<ProviderInfo> providers, ComponentName instrumentationName,
ProfilerInfo profilerInfo, Bundle instrumentationArgs,
IInstrumentationWatcher instrumentationWatcher,
IUiAutomationConnection instrumentationUiConnection, int debugMode,
boolean enableBinderTracking, boolean trackAllocation,
boolean isRestrictedBackupMode, boolean persistent, Configuration config,
CompatibilityInfo compatInfo, Map services, Bundle coreSettings,
String buildSerial, AutofillOptions autofillOptions,
ContentCaptureOptions contentCaptureOptions) {
AppBindData data = new AppBindData();
data.processName = processName;
data.appInfo = appInfo;
data.providers = providers;
data.instrumentationName = instrumentationName;
data.instrumentationArgs = instrumentationArgs;
data.instrumentationWatcher = instrumentationWatcher;
data.instrumentationUiAutomationConnection = instrumentationUiConnection;
data.debugMode = debugMode;
data.enableBinderTracking = enableBinderTracking;
data.trackAllocation = trackAllocation;
data.restrictedBackupMode = isRestrictedBackupMode;
data.persistent = persistent;
data.config = config;
data.compatInfo = compatInfo;
data.initProfilerInfo = profilerInfo;
data.buildSerial = buildSerial;
data.autofillOptions = autofillOptions;
data.contentCaptureOptions = contentCaptureOptions;
sendMessage(H.BIND_APPLICATION, data);
}

public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case BIND_APPLICATION:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
AppBindData data = (AppBindData)msg.obj;
handleBindApplication(data);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
}
}

复制代码

可以看到这里有个H,H是主线程的一个Handler类,用于处理需要主线程处理的各类消息,包括BIND_SERVICE,LOW_MEMORY,DUMP_HEAP等等。接着看handleBindApplication:


private void handleBindApplication(AppBindData data) {
//...
try {
final ClassLoader cl = instrContext.getClassLoader();
mInstrumentation = (Instrumentation)
cl.loadClass(data.instrumentationName.getClassName()).newInstance();
}
//...
Application app;
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();
try {
// If the app is being launched for full backup or restore, bring it up in
// a restricted environment with the base application class.
app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;
// don't bring up providers in restricted mode; they may depend on the
// app's custom Application class
if (!data.restrictedBackupMode) {
if (!ArrayUtils.isEmpty(data.providers)) {
installContentProviders(app, data.providers);
}
}

// Do this after providers, since instrumentation tests generally start their
// test thread at this point, and we don't want that racing.
try {
mInstrumentation.onCreate(data.instrumentationArgs);
}
//...
try {
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
}
//...
}

这里信息量就多了,一点点的看:



  • 首先,创建了Instrumentation,也就是上文一开始startActivity的第一步。每个应用程序都有一个Instrumentation,用于管理这个进程,比如要创建Activity的时候,首先就会执行到这个类里面。
  • makeApplication方法,创建了Application,终于到这一步了。最终会走到newApplication方法,执行Application的attach方法。

public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Application app = getFactory(context.getPackageName())
.instantiateApplication(cl, className);
app.attach(context);
return app;
}

attach方法有了,onCreate方法又是何时调用的呢?马上来了:


instrumentation.callApplicationOnCreate(app);

public void callApplicationOnCreate(Application app) {
app.onCreate();
}

也就是创建Application->attach->onCreate调用顺序。


等等,在onCreate之前还有一句重要的代码:


installContentProviders

这里就是启动Provider的相关代码了,具体逻辑就不分析了。


第六关:启动Activity

说完bindApplication,该说说后续了,上文第五关说到,bindApplication方法之后执行的是attachApplication方法,最终会执行到ActivityThread的handleLaunchActivity方法:


public Activity handleLaunchActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions, Intent customIntent) {
//...
WindowManagerGlobal.initialize();
//...
final Activity a = performLaunchActivity(r, customIntent);
//...
return a;
}

首先,初始化了WindowManagerGlobal,这是个啥呢? 没错,就是WindowManagerService了,也为后续窗口显示等作了准备。


继续看performLaunchActivity:


private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
//创建ContextImpl
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
//创建Activity
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
}

try {
if (activity != null) {
//完成activity的一些重要数据的初始化
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken);

if (customIntent != null) {
activity.mIntent = customIntent;
}

//设置activity的主题
int theme = r.activityInfo.getThemeResource();
if (theme != 0) {
activity.setTheme(theme);
}

//调用activity的onCreate方法
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
}
}

return activity;
}

哇,终于看到onCreate方法了。稳住,还是一步步看看这段代码。


首先,创建了ContextImpl对象,ContextImpl可能有的朋友不知道是啥,ContextImpl继承自Context,其实就是我们平时用的上下文。有的同学可能表示,这不对啊,获取上下文明明获取的是Context对象。来一起跟随源码看看。


//Activity.java
Context mBase;

@Override
public Executor getMainExecutor() {
return mBase.getMainExecutor();
}

@Override
public Context getApplicationContext() {
return mBase.getApplicationContext();
}

这里可以看到,我们平时用的上下文就是这个mBase,那么找到这个mBase是啥就行了:


protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}

//一层层往上找

final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) {

attachBaseContext(context);

mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}


}

这不就是,,,刚才一开始performLaunchActivity方法里面的attach吗?太巧了,所以这个ContextImpl就是我们平时所用的上下文。


顺便看看attach还干了啥?新建了PhoneWindow,建立自己和Window的关联,并设置了setSoftInputMode等等。


ContextImpl创建完之后,会通过类加载器创建Activity的对象,然后设置好activity的主题,最后调用了activity的onCreate方法。


总结

再一起捋一遍App的启动流程:



  • Launcher被调用点击事件,转到Instrumentation类的startActivity方法。
  • Instrumentation通过跨进程通信告诉AMS要启动应用的需求。
  • AMS反馈Launcher,让Launcher进入Paused状态
  • Launcher进入Paused状态,AMS转到ZygoteProcess类,并通过socket与Zygote通信,告知Zygote需要新建进程。
  • Zygote fork进程,并调用ActivityThread的main方法,也就是app的入口。
  • ActivityThread的main方法新建了ActivityThread实例,并新建了Looper实例,开始loop循环。
  • 同时ActivityThread也告知AMS,进程创建完毕,开始创建Application,Provider,并调用Applicaiton的attach,onCreate方法。
  • 最后就是创建上下文,通过类加载器加载Activity,调用Activity的onCreate方法。

至此,应用启动完毕。


当然,分析源码的目的一直都不是为了学知识而学,而是理解了这些基础,我们才能更好的解决问题。 学习了App的启动流程,我们可以再思考下一些之前没理解透的问题,比如启动优化


分析启动过程,其实可以优化启动速度的地方有三个地方:



  • Application的attach方法,MultiDexApplication会在方法里面会去执行MultiDex逻辑。所以这里可以进行MultiDex优化,比如今日头条方案就是单独启动一个进程的activity去加载MultiDex。
  • Application的onCreate方法,大量三方库的初始化都在这里进行,所以我们可以开启线程池,懒加载等等。把每个启动任务进行区分,哪些可以子线程运行,哪些有先后顺序。
  • Activity的onCreate方法,同样进行线程处理,懒加载。或者预创建Activity,提前类加载等等。

最后希望各位老铁都能有一个乖巧可爱漂亮的女儿/儿子。😊


附件

fork使用多线程
今日头条启动优化
app启动流程分析


作者:积木zz
来源:https://juejin.cn/post/6867744083809419277

收起阅读 »

女儿拿着小天才电话手表问我App启动流程(上)

首先,new一个女儿,var mDdaughter = new 女儿("6岁",“漂亮可爱”,“健康乖巧”,“最喜欢玩小天才电话手表和她的爸爸”)“爸爸爸爸,你说我玩的这个小天才电话手表怎么这么厉害,随便点一下这个小图片,这个应用就冒出来了,就可以听儿歌了。好...
继续阅读 »



前言

首先,new一个女儿,

var mDdaughter = new 女儿("6岁",“漂亮可爱”,“健康乖巧”,“最喜欢玩小天才电话手表和她的爸爸”)

好了,女儿有了,有一天,女儿问我:

“爸爸爸爸,你说我玩的这个小天才电话手表怎么这么厉害,随便点一下这个小图片,这个应用就冒出来了,就可以听儿歌了。好神奇啊。”

我心里一惊: img

小天才电话手表的系统就是Android,所以这不就是。。面试官常考的应用启动流程嘛!
女儿也要来面试我了吗!😭
好了,既然女儿问了,那就答吧。
但是,对付这个小小的0经验面试官,我该咋说呢?

解答小小面试官

女儿,你可以把手表里面想象成一个幼儿园,里面有一个老师,一个班长,一个班干部,以及一大堆小朋友。

  • 一个老师:Z老师(Zygote进程)

  • 一个班长:小A(ActivityManagerService)

  • 一个班干部:小L(Launcher桌面应用)

  • 一大堆小朋友:所有应用,包括音乐小朋友,聊天小朋友,日历小朋友等等。

img

应用启动过程就像一个小朋友被叫醒一样,开机之后呢,Z老师会依次叫醒班长和班干部(SystemServer#ActivityManagerService,Launcher),小L醒了之后就会去了解手表里有哪些小朋友,长什么样(icon,name),家庭信息(包名,androidmanifest)等等,然后一个个把小朋友的照片(icon)贴到自己的身上。比如有音乐小朋友,聊天小朋友,日历小朋友,其实也就是你手表上这个桌面啦。

这时候你要点开一个音乐小朋友呢(startActivity),小L就会通知班长小A(Binder),小A知道了之后,让小L自己休息下(Paused),然后就去找Z老师了。Z老师就负责叫音乐小朋友起床了(fork进程,启动ActivityThread),音乐小朋友起来后就又找小A带她去洗脸刷牙(启动ApplicationThread,Activity),都弄完了就可以进行各种表演了,唱歌啊,跳舞啊。

不是很明白啊?我们一起聊个天你就懂了,假如我是Launcher

img

img

女儿似懂非懂的给我点了一个赞👍,爸爸你真棒。

十五年后

mDdaughter.grow(15)
mDdaughter.study("Android")

过了十五年,女儿已经21岁了,正在学习Android,考虑要不要女从父业。

这天,她一脸疑惑的来找我: “爸,这个app启动到底是怎么个流程啊,我看了好久还是不大明白,要不你再跟我详细讲一遍吧?” “好嘞,别担心,我这次详细跟你说说”

解答Android程序媛

还记得我小时候跟你说过的故事吗,Android系统就像一个幼儿园,有一个大朋友叫Launcher,身上会贴很多其他小朋友的名片。这个Launcher就是我们的桌面了,它通过PackageManagerService获知了系统里所有应用的信息,并展示了出来,当然它本身也是一个应用。

通过点击一个应用图标,也就是触发了点击事件,最后会执行到startActivity方法。这里也就和启动Activity步骤重合上了。

那么这个startActivity干了啥?是怎么通过重重关卡唤醒这个应用的?

首先,介绍下系统中那些重要的成员,他们在app启动流程中都担任了重要的角色.

系统成员介绍

  • init进程,Android系统启动后,Zygote并不是第一个进程,而是linux的根进程init进程,然后init进程才会启动Zygote进程。

  • Zygote进程,所有android进程的父进程,当然也包括SystemServer进程

  • SystemServer进程,正如名字一样,系统服务进程,负责系统中大大小小的事物,为此也是启动了三员大将(ActivityManagerService,PackageManagerService,WindowManagerService)以及binder线程池。

  • ActivityManagerService,主要负责系统中四大组件的启动、切换、调度及应用进程的管理和调度等工作,对于一些进程的启动,都会通过Binder通信机制传递给AMS,再处理给Zygote。

  • PackageManagerService,主要负责应用包的一些操作,比如安装,卸载,解析AndroidManifest.xml,扫描文件信息等等。

  • WindowManagerService,主要负责窗口相关的一些服务,比如窗口的启动,添加,删除等。

  • Launcher,桌面应用,也是属于应用,也有自己的Activity,一开机就会默认启动,通过设置Intent.CATEGORY_HOME的Category隐式启动。

搞清楚这些成员,就跟随我一起看看怎么过五关斩六将,最终启动了一个App。

第一关:跨进程通信,告诉系统我的需求

首先,要告诉系统,我Launcher要启动一个应用了,调用Activity.startActivityForResult方法,最终会转到mInstrumentation.execStartActivity方法。 由于Launcher自己处在一个单独的进程,所以它需要跨进程告诉系统服务我要启动App的需求。 找到要通知的Service,名叫ActivityTaskManagerService,然后使用AIDL,通过Binder与他进行通信。

这里的简单说下ActivityTaskManagerService(简称ATMS)。原来这些通信工作都是属于ActivityManagerService,现在分了一部分工作给到ATMS,主要包括四大组件的调度工作。也是由SystemServer进程直接启动的,相关源码可见ActivityManagerService.Lifecycle.startService方法,感兴趣朋友可以自己看看。

接着说跨进程通信,相关代码如下:

//Instrumentation.java
int result = ActivityTaskManager.getService()
  .startActivity(whoThread, who.getBasePackageName(), intent,
                  intent.resolveTypeIfNeeded(who.getContentResolver()),
                  token, target != null ? target.mEmbeddedID : null,
                  requestCode, 0, null, options);


//ActivityTaskManager.java            
public static IActivityTaskManager getService() {
   return IActivityTaskManagerSingleton.get();
}
private static final Singleton<IActivityTaskManager> IActivityTaskManagerSingleton =
   new Singleton<IActivityTaskManager>() {
   @Override
   protected IActivityTaskManager create() {
       final IBinder b = ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE);
       return IActivityTaskManager.Stub.asInterface(b);
  }
};

//ActivityTaskManagerService.java
public class ActivityTaskManagerService extends IActivityTaskManager.Stub

   public static final class Lifecycle extends SystemService {
       private final ActivityTaskManagerService mService;

       public Lifecycle(Context context) {
           super(context);
           mService = new ActivityTaskManagerService(context);
      }

       @Override
       public void onStart() {
           publishBinderService(Context.ACTIVITY_TASK_SERVICE, mService);
           mService.start();
      }
  }

startActivity我们都很熟悉,平时启动Activity都会使用,启动应用也是从这个方法开始的,也会同样带上intent信息,表示要启动的是哪个Activity。

另外要注意的一点是,startActivity之后有个checkStartActivityResult方法,这个方法是用作检查启动Activity的结果。当启动Activity失败的时候,就会通过这个方法抛出异常,比如有我们常见的问题:未在AndroidManifest.xml注册。

public static void checkStartActivityResult(int res, Object intent) {
   switch (res) {
       case ActivityManager.START_INTENT_NOT_RESOLVED:
       case ActivityManager.START_CLASS_NOT_FOUND:
           if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
               throw new ActivityNotFoundException(
               "Unable to find explicit activity class "
               + ((Intent)intent).getComponent().toShortString()
               + "; have you declared this activity in your AndroidManifest.xml?");
           throw new ActivityNotFoundException(
               "No Activity found to handle " + intent);
       case ActivityManager.START_PERMISSION_DENIED:
           throw new SecurityException("Not allowed to start activity "
                                       + intent);
       case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
           throw new AndroidRuntimeException(
               "FORWARD_RESULT_FLAG used while also requesting a result");
       case ActivityManager.START_NOT_ACTIVITY:
           throw new IllegalArgumentException(
               "PendingIntent is not an activity");
           //...
  }
}

第二关:通知Launcher可以休息了

ATMS收到要启动的消息后,就会通知上一个应用,也就是Launcher可以休息会了,进入Paused状态。

//ActivityStack.java

private boolean resumeTopActivityInnerLocked(ActivityRecord prev, ActivityOptions options) {
   //...
   ActivityRecord next = topRunningActivityLocked(true /* focusableOnly */);
   //...
   boolean pausing = getDisplay().pauseBackStacks(userLeaving, next, false);
   if (mResumedActivity != null) {
       if (DEBUG_STATES) Slog.d(TAG_STATES,
                                "resumeTopActivityLocked: Pausing " + mResumedActivity);
       pausing |= startPausingLocked(userLeaving, false, next, false);
  }
   //...

   if (next.attachedToProcess()) {
       //应用已经启动
       try {
           //...
           transaction.setLifecycleStateRequest(
               ResumeActivityItem.obtain(next.app.getReportedProcState(),
                                         getDisplay().mDisplayContent.isNextTransitionForward()));
           mService.getLifecycleManager().scheduleTransaction(transaction);
           //...
      } catch (Exception e) {
           //...
           mStackSupervisor.startSpecificActivityLocked(next, true, false);
           return true;
      }
       //...
       // From this point on, if something goes wrong there is no way
       // to recover the activity.
       try {
           next.completeResumeLocked();
      } catch (Exception e) {
           // If any exception gets thrown, toss away this
           // activity and try the next one.
           Slog.w(TAG, "Exception thrown during resume of " + next, e);
           requestFinishActivityLocked(next.appToken, Activity.RESULT_CANCELED, null,
                                       "resume-exception", true);
           return true;
      }
  } else {
       //冷启动流程
       mStackSupervisor.startSpecificActivityLocked(next, true, true);
  }        
}

这里有两个类没有见过:

  • ActivityStack,是Activity的栈管理,相当于我们平时项目里面自己写的Activity管理类,用于管理Activity的状态啊,如栈出栈顺序等等。

  • ActivityRecord,代表具体的某一个Activity,存放了该Activity的各种信息。

startPausingLocked方法就是让上一个应用,这里也就是Launcher进入Paused状态。 然后就会判断应用是否启动,如果已经启动了,就会走ResumeActivityItem的方法,看这个名字,结合应用已经启动的前提,是不是已经猜到了它是干吗的?没错,这个就是用来控制Activity的onResume生命周期方法的,不仅是onResume还有onStart方法,具体可见ActivityThread的handleResumeActivity方法源码。

如果应用没启动就会接着走到startSpecificActivityLocked方法,接着看。

第三关:是否已启动进程,否则创建进程

Launcher进入Paused之后,ActivityTaskManagerService就会判断要打开的这个应用进程是否已经启动,如果已经启动,则直接启动Activity即可,这也就是应用内的启动Activity流程。如果进程没有启动,则需要创建进程。

这里有两个问题:

  • 怎么判断应用进程是否存在呢?如果一个应用已经启动了,会在ATMS里面保存一个WindowProcessController信息,这个信息包括processName和uid,uid则是应用程序的id,可以通过applicationInfo.uid获取。processName则是进程名,一般为程序包名。所以判断是否存在应用进程,则是根据processName和uid去判断是否有对应的WindowProcessController,并且WindowProcessController里面的线程不为空。代码如下:

//ActivityStackSupervisor.java
void startSpecificActivityLocked(ActivityRecord r, boolean andResume, boolean checkConfig) {
   // Is this activity's application already running?
   final WindowProcessController wpc =
       mService.getProcessController(r.processName, r.info.applicationInfo.uid);

   boolean knownToBeDead = false;
   if (wpc != null && wpc.hasThread()) {
       //应用进程存在
       try {
           realStartActivityLocked(r, wpc, andResume, checkConfig);
           return;
      }
  }
}

//WindowProcessController.java
IApplicationThread getThread() {
   return mThread;
}

boolean hasThread() {
   return mThread != null;
}
  • 还有个问题就是怎么创建进程?还记得Z老师吗?对,就是Zygote进程。之前说了他是所有进程的父进程,所以就要通知Zygote去fork一个新的进程,服务于这个应用。

//ZygoteProcess.java
private Process.ProcessStartResult attemptUsapSendArgsAndGetResult(
   ZygoteState zygoteState, String msgStr)
   throws ZygoteStartFailedEx, IOException {
   try (LocalSocket usapSessionSocket = zygoteState.getUsapSessionSocket()) {
       final BufferedWriter usapWriter =
           new BufferedWriter(
           new OutputStreamWriter(usapSessionSocket.getOutputStream()),
           Zygote.SOCKET_BUFFER_SIZE);
       final DataInputStream usapReader =
           new DataInputStream(usapSessionSocket.getInputStream());

       usapWriter.write(msgStr);
       usapWriter.flush();

       Process.ProcessStartResult result = new Process.ProcessStartResult();
       result.pid = usapReader.readInt();
       // USAPs can't be used to spawn processes that need wrappers.
       result.usingWrapper = false;

       if (result.pid >= 0) {
           return result;
      } else {
           throw new ZygoteStartFailedEx("USAP specialization failed");
      }
  }
}

可以看到,这里其实是通过socket和Zygote进行通信,BufferedWriter用于读取和接收消息。这里将要新建进程的消息传递给Zygote,由Zygote进行fork进程,并返回新进程的pid。

可能又会有人问了?fork是啥?为啥这里又变成socket进行IPC通信,而不是Bindler了?

  • 首先,fork()是一个方法,是类Unix操作系统上创建进程的主要方法。用于创建子进程(等同于当前进程的副本)。

  • 那为什么fork的时候不用Binder而用socket了呢?主要是因为fork不允许存在多线程,Binder通讯偏偏就是多线程。

问题总是在不断产生,总有好奇的朋友会接着问,为什么fork不允许存在多线程?

收起阅读 »

Android 手把手带你搭建一个组件化项目架构

🔥 一、组件化作为一个单工程撸到底的开发人员,想试着将项目进行组件化改造,说动就动。毕竟技术都是写出来的,看着文章感觉懂了,但是实际开发中还是能遇到各种各样的问题,开始搞起来。💥 1.1 为什么使用组件化一直使用单工程撸到底,项目越来越大导致出现了不少的问题:...
继续阅读 »



🔥 一、组件化

作为一个单工程撸到底的开发人员,想试着将项目进行组件化改造,说动就动。毕竟技术都是写出来的,看着文章感觉懂了,但是实际开发中还是能遇到各种各样的问题,开始搞起来。

💥 1.1 为什么使用组件化

一直使用单工程撸到底,项目越来越大导致出现了不少的问题:

  • 查找问题慢:定位问题,需要在多个代码混合的模块中寻找和跳转。

  • 开发维护成本增加:避免代码的改动影响其它业务的功能,导致开发和维护成本不断增加。

  • 编译时间长:项目工程越大,编译完整代码所花费的时间越长。

  • 开发效率低:多人协作开发时,开发风格不一,又很难将业务完全分割,大家互相影响,导致开发效率低下。

  • 代码复用性差:写过的代码很难抽离出来再次利用。

💥 1.2 模块化与组件化

🌀 1.2.1 模块

一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,比如登录模块首页模块等等。

🌀 1.2.2 组件

组件指的是单一的功能组件,如登录组件视频组件支付组件 等,每个组件都可以以一个单独的 module 开发,并且可以单独抽出来作为 SDK 对外发布使用。可以说往往一个模块包含了一个或多个组件。

💥 1.3 组件化的优势

组件化基于可重用的目的,将应用拆分成多个独立组件,以减少耦合:

  • 加快编译速度:每个业务功能都是一个单独的工程,可独立编译运行,拆分后代码量较少,编译自然变快。

  • 解耦:通过关注点分离的形式,将App分离成多个模块,每个模块都是一个组件。

  • 提高开发效率:多人开发中,每个组件模块由单人负责,降低了开发之间沟通的成本,减少因代码风格不一而产生的相互影响。

  • 代码复用:类似我们引用的第三方库,可以将基础组件或功能组件剥离。在新项目微调或直接使用。

💥 1.4 组件化需要解决的问题

  • 组件分层:怎么将一个项目分成多个组件、组件间的依赖关系是怎么样的?

  • 组件单独运行和集成调试:组件是如何独立运行和集成调试的?

  • 组件间通信:主项目与组件、组件与组件之间如何通信就变成关键?

🔥 二、组件分层

组件依赖关系是上层依赖下层,修改频率是上层高于下层。先上一张图:

img

💥 2.1 基础组件

基础公共模块,最底层的库:

  • 封装公用的基础组件;

  • 网络访问框架、图片加载框架等主流的第三方库;

  • 各种第三方SDK。

💥 2.2 common组件(lib_common)

  • 支撑业务组件、功能组件的基础(BaseActivity/BaseFragment等基础能力;

  • 依赖基础组件层;

  • 业务组件、功能组件所需的基础能力只需要依赖common组件即可获得。

💥 2.3 功能组件

  • 依赖基础组件层;

  • 对一些公用的功能业务进行封装与实现;

  • 业务组件可以在library和application之间切换,但是最后打包时必须是library ;

💥 2.4 业务组件

  • 可直接依赖基础组件层;同时也能依赖公用的一些功能组件;

  • 各组件之间不存在依赖关系,通过路由进行通信;

  • 业务组件可以在library和application之间切换,但是最后打包时必须是library ;

💥 2.5 主工程(app)

  • 只依赖各业务组件;

  • 除了一些全局的配置和主Activity之外,不包含任何业务代码,是应用的入口;

💥 2.6 完成后项目

img

这只是个大概,并不是说必须这样,可以按照自己的方式来。比如:你觉得基础组件比较多导致project里面的项目太多,那么你可以创建一个lib_base,然在lib_base里面再创建其他基础组件即可。

🔥 三、组件单独调试

💥 3.1 创建组件(收藏)

img

  • library和application之间切换:选择第一项。

  • 始终是library:选择第二项

这样尽可能的减少变动项,当然这仅仅是个建议,看个人习惯吧。

因为咱们创建的是一个module,所以在AndridManifest中添加android:exported="true"属性可直接构建一个APK。下面咱们看看如何生成不同的工程类型。

💥 3.2 动态配置组件的工程类型

在 AndroidStudio 开发 Android 项目时,使用的是 Gradle 来构建,具体来说使用的是 Android Gradle 插件来构建,Android Gradle 中提供了三种插件,在开发中可以通过配置不同的插件来构建不同的工程。

🌀 3.2.1 build.gradle(module)

//构建后输出一个 APK 安装包
apply plugin: 'com.android.application'
//构建后输出 ARR 包
apply plugin: 'com.android.library'
//配置一个 Android Test 工程
apply plugin: 'com.android.test'

独立调试:设置为 Application 插件。

集成调试:设置为 Library 插件。

🌀 3.2.2 设置gradle.properties

img

isDebug = true 独立调试

🌀 3.2.3 动态配制插件(build.gradle)

//注意gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换
if(isDebug.toBoolean()){
   //构建后输出一个 APK 安装包
   apply plugin: 'com.android.application'
}else{
   //构建后输出 ARR 包
   apply plugin: 'com.android.library'
}

💥 3.3 动态配置组件的 ApplicationId 和 AndroidManifest 文件

  • 一个 APP 是只有一个 ApplicationId ,所以在单独调试集成调试组件的 ApplicationId 应该是不同的。

  • 单独调试时也是需要有一个启动页,当集成调试时主工程和组件的AndroidManifest文件合并会产生多个启动页。

根据上面动态配制插件的经验,我们也需要在build.gradle中动态配制ApplicationId 和 AndroidManifest 文件。

🌀 3.3.1 准备两个不同路径的 AndroidManifest 文件

img

有什么不同?咱们一起看看具体内容。

🌀 3.3.2 src/main/debug/AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.scc.module.collect">

   <application
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.SccMall">
       <activity android:name=".CollectActivity"
           android:exported="true">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>
       </activity>
   </application>

</manifest>

🌀 3.3.3 src/main/debug/AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.scc.module.collect">
   <application
       android:allowBackup="true"
       android:supportsRtl="true"
       >
       <activity android:name=".CollectActivity"/>
   </application>

</manifest>

🌀 3.3.4 动态配制(build.gradle)

defaultConfig {
   if(isDebug.toBoolean()){
       //独立调试的时候才能设置applicationId
       applicationId "com.scc.module.collect"
  }
}
sourceSets {
   main {
       if (isDebug.toBoolean()) {
           //独立调试
           manifest.srcFile 'src/main/debug/AndroidManifest.xml'
      } else {
           //集成调试
           manifest.srcFile 'src/main/AndroidManifest.xml'
      }
  }
}

💥 3.4 实现效果

🌀 3.4.1 独立调试

isDebug = true

img

🌀 3.4.2 集成调试

isDebug = false

img

🔥 四、Gradle配置统一管理

💥 4.1 config.gradle

当我们需要进行插件版本、依赖库版本升级时,项目多的话改起来很麻烦,这时就需要我们对Gradle配置统一管理。如下:

img

具体内容

ext{
   //组件独立调试开关, 每次更改值后要同步工程
   isDebug = true
   android = [
           // 编译 SDK 版本
           compileSdkVersion: 31,
           // 最低兼容 Android 版本
           minSdkVersion   : 21,
           // 最高兼容 Android 版本
           targetSdkVersion : 31,
           // 当前版本编号
           versionCode     : 1,
           // 当前版本信息
           versionName     : "1.0.0"
  ]
   applicationid = [
           app:"com.scc.sccmall",
           main:"com.scc.module.main",
           webview:"com.scc.module.webview",
           login:"com.scc.module.login",
           collect:"com.scc.module.collect"
  ]
   dependencies = [
           "appcompat"         :'androidx.appcompat:appcompat:1.2.0',
           "material"         :'com.google.android.material:material:1.3.0',
           "constraintlayout" :'androidx.constraintlayout:constraintlayout:2.0.1',
           "livedata"         :'androidx.lifecycle:lifecycle-livedata:2.4.0',
           "viewmodel"         :'androidx.lifecycle:lifecycle-viewmodel:2.4.0',
           "legacyv4"         :'androidx.legacy:legacy-support-v4:1.0.0',
           "splashscreen"     :'androidx.core:core-splashscreen:1.0.0-alpha01'
  ]
   libARouter= 'com.alibaba:arouter-api:1.5.2'
   libARouterCompiler = 'com.alibaba:arouter-compiler:1.5.2'
   libGson = 'com.google.code.gson:gson:2.8.9'
}

💥 4.2 添加配制文件build.gradle(project)

apply from:"config.gradle"

💥 4.3 其他组件使用

//build.gradle
//注意gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换
if(isDebug.toBoolean()){
   //构建后输出一个 APK 安装包
   apply plugin: 'com.android.application'
}else{
   //构建后输出 ARR 包
   apply plugin: 'com.android.library'
}
android {
   compileSdkVersion 31

   defaultConfig {
       if(isDebug.toBoolean()){
           //独立调试的时候才能设置applicationId
           applicationId "com.scc.module.collect"
      }
       minSdkVersion 21
       targetSdkVersion 31
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
  }

   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
      }
  }
   sourceSets {
       main {
           if (isDebug.toBoolean()) {
               //独立调试
               manifest.srcFile 'src/main/debug/AndroidManifest.xml'
          } else {
               //集成调试
               manifest.srcFile 'src/main/AndroidManifest.xml'
          }
      }
  }
   compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_8
       targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {
//   implementation root.dependencies.appcompat
//   implementation root.dependencies.material
//   implementation root.dependencies.constraintlayout
//   implementation root.dependencies.livedata
//   implementation root.dependencies.viewmodel
//   implementation root.dependencies.legacyv4
//   implementation root.dependencies.splashscreen
//   implementation root.libARouter
   //上面内容在lib_common中已经添加咱们直接依赖lib_common
   implementation project(':lib_common')

   testImplementation 'junit:junit:4.+'
   androidTestImplementation 'androidx.test.ext:junit:1.1.2'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

🔥 五、组件间界面跳转(ARouter)

💥 5.1 介绍

Android 中的界面跳转那是相当简单,但是在组件化开发中,由于不同组件式没有相互依赖的,所以不可以直接访问彼此的类,这时候就没办法通过显式的方式实现了。

所以在这里咱们采取更加灵活的一种方式,使用 Alibaba 开源的 ARouter 来实现。

一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦

文档介绍的蛮详细的,感兴趣的可以自己实践一下。这里做个简单的使用。

💥 5.2 使用

🌀 5.2.1 添加依赖

先在统一的config.gradle添加版本等信息

ext{
  ...
   libARouter= 'com.alibaba:arouter-api:1.5.2'
   libARouterCompiler = 'com.alibaba:arouter-compiler:1.5.2'
}

因为所有的功能组件和业务组件都依赖lib_common,那么咱们先从lib_common开始配制

lib_common

dependencies {
   api root.libARouter
  ...
}

其他组件(如collect)

android {
   defaultConfig {
      ...
       javaCompileOptions {
           annotationProcessorOptions {
               arguments = [AROUTER_MODULE_NAME: project.getName()]
               //如果项目内有多个annotationProcessor,则修改为以下设置
               //arguments += [AROUTER_MODULE_NAME: project.getName()]
          }
      }
  }
}

dependencies {
   //arouter-compiler的注解依赖需要所有使用 ARouter 的 module 都添加依赖
   annotationProcessor root.libARouterCompiler
  ...
}

🌀 5.2.2 添加注解

你要跳转的Activity

// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/collect/CollectActivity")
public class CollectActivity extends AppCompatActivity {
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_collect);
  }
}

🌀 5.2.3 初始化SDK(主项目Application)

public class App extends BaseApplication {
   @Override
   public void onCreate() {
       super.onCreate();
       if (isDebug()) {           // 这两行必须写在init之前,否则这些配置在init过程中将无效
           ARouter.openLog();     // 打印日志
           ARouter.openDebug();   // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
      }
       ARouter.init(this); // 尽可能早,推荐在Application中初始化
  }
   private boolean isDebug() {
       return BuildConfig.DEBUG;
  }
}

💥 5.3 发起路由操作

🌀 5.3.1 应用内简单的跳转

ARouter.getInstance().build("/collect/CollectActivity").navigation();

这里是用module_main的HomeFragment跳转至module_collect的CollectActivity界面,两个module中不存在依赖关系。"/collect/CollectActivity"在上面已注册就不多描述了。

效果如下:

img

🌀 5.3.2 跳转并携带参数

这里是用module_main的MineFragment的Adapter跳转至module_webview的WebViewActivity界面,两个module中同样不存在依赖关系。

启动方

ARouter.getInstance().build("/webview/WebViewActivity")
  .withString("url", bean.getUrl())
  .withString("content",bean.getName())
  .navigation();

这里传了两个参数urlname到WebViewActivity,下面咱们看看WebViewActivity怎么接收。

接收方

//为每一个参数声明一个字段,并使用 @Autowired 标注
//URL中不能传递Parcelable类型数据,通过ARouter api可以传递Parcelable对象
//添加注解(必选)
@Route(path = "/webview/WebViewActivity")
public class WebViewActivity extends BaseActivity<ActivityWebviewBinding, WebViewViewModel> {
   //发送方和接收方定义的key名称相同则无需处理
   @Autowired
   public String url;
   //通过name来映射URL中的不同参数
   //发送方定义key为content,我们用title来接收
   @Autowired(name = "content")
   public String title;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       //注入参数和服务(这里用到@Autowired所以要设置)
       //不使用自动注入,可不写,如CollectActivity没接收参数就没有设置
       ARouter.getInstance().inject(this);
       binding.btnBoom.setText(String.format("%s,你来啦", title));
       //加载链接
       initWebView(binding.wbAbout, url);
  }
}

上效果图:

搞定,更多高级玩法可自行探索。

🌀 5.3.3 小记(ARouter目标不存在)

W/ARouter::: ARouter::There is no route match the path

这里出现个小问题,配置注释都好好的,但是发送发无论如何都找不到设置好的Activity。尝试方案:

  • Clean Project

  • Rebuild Project

  • 在下图也能找到ARouter内容。

后来修改Activity名称好了。

img

🔥 六、组件间通信(数据传递)

界面跳转搞定了,那么数据传递怎么办,我在module_main中使用悬浮窗,但是需要判断这个用户是否已登录,再执行后续逻辑,这个要怎么办?这里我们可以采用 接口 + ARouter 的方式来解决。

在这里可以添加一个 componentbase 模块,这个模块被所有的组件依赖

这里我们通过 module_main组件 中调用 module_login组件 中的方法来获取登录状态这个场景来演示。

💥 6.1 通过依赖注入解耦:服务管理(一) 暴露服务

🌀 6.1.1 创建 componentbase 模块(lib)

img

🌀 6.1.2 创建接口并继承IProvider

注意:接口必须继承IProvider,是为了使用ARouter的实现注入。

img

🌀 6.1.3 在module_login组件中实现接口

lib_common

所有业务组件和功能组件都依赖lib_common,所以咱们直接在lib_common添加依赖即可

dependencies {
  ...
   api project(":lib_componentbase")
}

module_login

dependencies {
  ...
   implementation project(':lib_common')
}

实现接口

//实现接口
@Route(path = "/login/AccountServiceImpl")
public class AccountServiceImpl implements IAccountService {
   @Override
   public boolean isLogin() {
       MLog.e("AccountServiceImpl.isLogin");
       return true;
  }

   @Override
   public String getAccountId() {
       MLog.e("AccountServiceImpl.getAccountId");
       return "1000";
  }

   @Override
   public void init(Context context) {

  }
}

img

💥 6.2 通过依赖注入解耦:服务管理(二) 发现服务

🌀 6.2.1 在module_main中调用调用是否已登入

public class HomeFragment extends BaseFragment<FragmentHomeBinding> {
   @Autowired
   IAccountService accountService;
   @Override
   public void onViewCreated(@NonNull @NotNull View view, @Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
       super.onViewCreated(view, savedInstanceState);
       ARouter.getInstance().inject(this);
       binding.frgmentHomeFab.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               MLog.e("Login:"+accountService.isLogin());
               MLog.e("AccountId:"+accountService.getAccountId());

          }
      });
  }
}

img 运行结果:

E/-SCC-: AccountServiceImpl.isLogin
E/-SCC-: Login:true
E/-SCC-: AccountServiceImpl.getAccountId
E/-SCC-: AccountId:1000

🔥 七、总结

本文介绍了组件化、组件分层、解决了组件的独立调试、集成调试、页面跳转、组件通信等。

其实会了这些后你基本可以搭建自己的组件化项目了。其实最大的问题还是分组分层、组件划分。这个就需要根据你的实际情况来设置。

本项目比较糙,后面会慢慢完善。比如添加Gilde、添加MMVK、添加Room等。

项目传送门

💥 相关推荐

Android OkHttp+Retrofit+Rxjava+Hilt实现网络请求框架

💥 参考与感谢

“终于懂了” 系列:Android组件化,全面掌握!

Android 组件化最佳实践

手把手带你搭建一个优秀的Android项目架构


作者:Android帅次
来源:https://juejin.cn/post/7033954652315975688


收起阅读 »

Unable to extract the trust manager on Android10Platform 完美解决

Unable to extract the trust manager on Android10Platform网上有大致有两种解决方案,但都不靠谱。产生这个异常的根本原因是:builder.sslSocketFactory(sslContext.getSoc...
继续阅读 »

Unable to extract the trust manager on Android10Platform

网上有大致有两种解决方案,但都不靠谱。产生这个异常的根本原因是:

builder.sslSocketFactory(sslContext.getSocketFactory());
这个方式已经过时了,需要新的方式,如下:



final X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {

}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new X509TrustManager[]{trustManager}, new SecureRandom());

OkHttpClient.Builder builder = new OkHttpClient().newBuilder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15,TimeUnit.SECONDS)
.addInterceptor(logInterceptor)
.sslSocketFactory(sslContext.getSocketFactory(),trustManager)
.hostnameVerifier(new HostnameVerifier() {

@Override
public boolean verify(String hostname, SSLSession session) {

return true;
}
});

收起阅读 »

国内知名Wchat团队荣誉出品顶级IM通讯聊天系统

iOS
国内知名Wchat团队荣誉出品顶级IM通讯聊天系统团队言语在先:想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)。想购买由类似ope...
继续阅读 »



国内知名Wchat团队荣誉出品顶级IM通讯聊天系统



团队言语在先:

想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)

。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)

。想购买由类似openfire第三方开源改造而来的所谓第三方通信server者勿扰

。想购买没有做任何安全加密场景者勿扰(随便一句api 一个接口就构成了红包收发/转账/密码设置等没有任何安全系数可言的低质产品)

。想购买非运营级别通信系统勿扰(到处呼喊:最稳定/真正可靠/大并发/真正安全!所有一切都需要实际架构支撑以及理论数值测验)

。想购买无保障/无支撑者勿扰(1W/4W/10W低质产品不可谓没有,必须做到:大并发支持合同保障/合作支持运维保障/在线人数支持架构保障)

。想购买消息丢包者勿扰(满天飞的所谓消息确认机制,最简单的测验既是前端支持消息收发demo测试环境,低质产品一秒收发百条消息必丢必崩,

别提秒发千条/万条,更低质产品可测验:同时发九张图片/根据数字12345678910发送出去,必丢!android vs ios)

。想购买大容量群uer者勿扰(随便宣传既是万人大群/几千大群/群组无限,小团队产品群组上线用户超过4000群消息体量不用很大手机前端必卡)

。最重要一点:口口声声说要运营很大的系统 却想出十几个money的人群勿扰,买产品做系统一要稳定二要长久用三要抛开运维烦恼,预算有限那就干脆

别买,买了几万的系统你一样后面用不起来会烂掉!

。产品体系包括:android ios server adminweb maintenance httpapi h5 webpc (支持server压测/前端消息收发压测/httpapi压测)

。。支持源码,但需要您拿去做一个伟大的系统出来!

。。团队产品目前国内没有同质化,客户集中在国外,有求高质量产品的个人或团队可通过以下方式联系到我们(低价者勿扰!)

。。。球球:383189941 q 513275129

。。。。产品不多介绍直接加我 测试产品更直接

。。。。。创新从未停止 更新不会终止 大陆唯一一家支持大并发保障/支持合同费用包含运维支撑的团队 

收起阅读 »

【Gradle7.0】依赖统一管理的全新方式,了解一下~

前言 随着项目的不断发展,项目中的依赖也越来越多,有时可能会有几百个,这个时候对项目依赖做一个统一的管理很有必要,我们一般会有以下需求: 项目依赖统一管理,在单独文件中配置 不同Module中的依赖版本号统一 不同项目中的依赖版本号统一 ...
继续阅读 »

前言


随着项目的不断发展,项目中的依赖也越来越多,有时可能会有几百个,这个时候对项目依赖做一个统一的管理很有必要,我们一般会有以下需求:



  1. 项目依赖统一管理,在单独文件中配置

  2. 不同Module中的依赖版本号统一

  3. 不同项目中的依赖版本号统一


针对这些需求,目前其实已经有了一些方案:



  1. 使用循环优化Gradle依赖管理

  2. 使用buildSrc管理Gradle依赖

  3. 使用includeBuild统一配置依赖版本


上面的方案支持在不同Module间统一版本号,同时如果需要在项目间共享,也可以做成Gradle插件发布到远端,已经基本可以满足我们的需求
不过Gradle7.0推出了一个新的特性,使用Catalog统一依赖版本,它支持以下特性:



  1. 对所有module可见,可统一管理所有module的依赖

  2. 支持声明依赖bundles,即总是一起使用的依赖可以组合在一起

  3. 支持版本号与依赖名分离,可以在多个依赖间共享版本号

  4. 支持在单独的libs.versions.toml文件中配置依赖

  5. 支持在项目间共享依赖


使用Version Catalog


注意,Catalog仍然是一个孵化中的特性,如需使用,需要在settings.gradle中添加以下内容:


enableFeaturePreview('VERSION_CATALOGS')

从命名上也可以看出,Version Catalog其实就是一个版本的目录,我们可以从目录中选出我们需要的依赖使用
我们可以通过如下方式使用Catalog中声明的依赖


dependencies {
implementation(libs.retrofit)
implementation(libs.groovy.core)
}

在这种情况下,libs是一个目录,retrofit表示该目录中可用的依赖项。 与直接在构建脚本中声明依赖项相比,Version Catalog具有许多优点:



  • 对于每个catalog,Gradle都会生成类型安全的访问器,以便你在IDE中可以自动补全.(注:目前在build.gradle中还不能自动补全,可能是指kts或者开发中?)

  • 声明在catalog中的依赖对所有module可见,当修改版本号时,可以统一管理统一修改

  • catalog支持声明一个依赖bundles,即一些总是一起使用的依赖的组合

  • catalog支持版本号与依赖名分离,可以在多个依赖间共享版本号


声明Version Catalog


Version Catalog可以在settings.gradle(.kts)文件中声明。


dependencyResolutionManagement {
versionCatalogs {
libs {
alias('retrofit').to('com.squareup.retrofit2:retrofit:2.9.0')
alias('groovy-core').to('org.codehaus.groovy:groovy:3.0.5')
alias('groovy-json').to('org.codehaus.groovy:groovy-json:3.0.5')
alias('groovy-nio').to('org.codehaus.groovy:groovy-nio:3.0.5')
alias('commons-lang3').to('org.apache.commons', 'commons-lang3').version {
strictly '[3.8, 4.0['
prefer '3.9'
}
}
}
}

别名必须由一系列以破折号(-,推荐)、下划线 (_) 或点 (.) 分隔的标识符组成。
标识符本身必须由ascii字符组成,最好是小写,最后是数字。


值得注意的是,groovy-core会被映射成libs.groovy.core
如果你想避免映射可以使用大小写来区分,比如groovyCore会被处理成libs.groovyCore


具有相同版本号的依赖


在上面的示例中,我们可以看到三个groovy依赖具有相同的版本号,我们可以把它们统一起来


dependencyResolutionManagement {
versionCatalogs {
libs {
version('groovy', '3.0.5')
version('compilesdk', '30')
version('targetsdk', '30')
alias('groovy-core').to('org.codehaus.groovy', 'groovy').versionRef('groovy')
alias('groovy-json').to('org.codehaus.groovy', 'groovy-json').versionRef('groovy')
alias('groovy-nio').to('org.codehaus.groovy', 'groovy-nio').versionRef('groovy')
alias('commons-lang3').to('org.apache.commons', 'commons-lang3').version {
strictly '[3.8, 4.0['
prefer '3.9'
}
}
}
}

除了在依赖中,我们同样可以在build.gradle中获取版本,比如可以用来指定compileSdk


android {
compileSdk libs.versions.compilesdk.get().toInteger()


defaultConfig {
applicationId "com.zj.gradlecatalog"
minSdk 21
targetSdk libs.versions.targetsdk.get().toInteger()
}
}

如上,可以使用catalog统一compileSdk,targetSdk,minSdk的版本号


依赖bundles


因为在不同的项目中经常系统地一起使用某些依赖项,所以Catalog提供了bundle(依赖包)的概念。依赖包基本上是几个依赖项打包的别名。
例如,你可以这样使用一个依赖包,而不是像上面那样声明 3 个单独的依赖项:


dependencies {
implementation libs.bundles.groovy
}

groovy依赖包声明如下:


dependencyResolutionManagement {
versionCatalogs {
libs {
version('groovy', '3.0.5')
version('checkstyle', '8.37')
alias('groovy-core').to('org.codehaus.groovy', 'groovy').versionRef('groovy')
alias('groovy-json').to('org.codehaus.groovy', 'groovy-json').versionRef('groovy')
alias('groovy-nio').to('org.codehaus.groovy', 'groovy-nio').versionRef('groovy')
alias('commons-lang3').to('org.apache.commons', 'commons-lang3').version {
strictly '[3.8, 4.0['
prefer '3.9'
}
bundle('groovy', ['groovy-core', 'groovy-json', 'groovy-nio'])
}
}
}

如上所示:添加groovy依赖包等同于添加依赖包下的所有依赖项


插件版本


除了Library之外,Catalog还支持声明插件版本。
因为library由它们的groupartifactversion表示,但Gradle插件仅由它们的idversion标识。
因此,插件需要单独声明:


dependencyResolutionManagement {
versionCatalogs {
libs {
alias('jmh').toPluginId('me.champeau.jmh').version('0.6.5')
}
}
}

然后可以在plugins块下面使用


plugins {
id 'java-library'
id 'checkstyle'
// 使用声明的插件
alias(libs.plugins.jmh)
}

在单独文件中配置Catalog


除了在settings.gradle中声明Catalog外,也可以通过一个单独的文件来配置Catalog
如果在根构建的gradle目录中找到了libs.versions.toml文件,则将使用该文件的内容自动声明一个Catalog


TOML文件主要由4个部分组成:



  • [versions] 部分用于声明可以被依赖项引用的版本

  • [libraries] 部分用于声明Library的别名

  • [bundles] 部分用于声明依赖包

  • [plugins] 部分用于声明插件


如下所示:


[versions]
groovy = "3.0.5"
checkstyle = "8.37"
compilesdk = "30"
targetsdk = "30"

[libraries]
retrofit = "com.squareup.retrofit2:retrofit:2.9.0"
groovy-core = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" }
groovy-json = { module = "org.codehaus.groovy:groovy-json", version.ref = "groovy" }
groovy-nio = { module = "org.codehaus.groovy:groovy-nio", version.ref = "groovy" }
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version = { strictly = "[3.8, 4.0[", prefer="3.9" } }

[bundles]
groovy = ["groovy-core", "groovy-json", "groovy-nio"]

[plugins]
jmh = { id = "me.champeau.jmh", version = "0.6.5" }

如上所示,依赖可以定义成一个字符串,也可以将moduleversion分离开来
其中versions可以定义成一个字符串,也可以定义成一个范围,详情可参见rich-version


[versions]
my-lib = { strictly = "[1.0, 2.0[", prefer = "1.2" }

在项目间共享Catalog


Catalog不仅可以在项目内统一管理依赖,同样可以实现在项目间共享
例如我们需要在团队内制定一个依赖规范,不同组的不同项目需要共享这些依赖,这是个很常见的需求


通过文件共享


Catalog支持通过从Toml文件引入依赖,这就让我们可以通过指定文件路径来实现共享依赖
如下所示,我们在settins.gradle中配置如下:


dependencyResolutionManagement {
versionCatalogs {
libs {
from(files("../gradle/libs.versions.toml"))
}
}
}

此技术可用于声明来自不同文件的多个目录:


dependencyResolutionManagement {
versionCatalogs {
// 声明一个'testLibs'目录, 从'test-libs.versions.toml'文件中
testLibs {
from(files('gradle/test-libs.versions.toml'))
}
}
}

发布插件实现共享


虽然从本地文件导入Catalog很方便,但它并没有解决在组织或外部消费者中共享Catalog的问题。
我们还可能通过Catalog插件来发布目录,这样用户直接引入这个插件即可


Gradle提供了一个Catalog插件,它提供了声明然后发布Catalog的能力。


1. 首先引入两个插件


plugins {
id 'version-catalog'
id 'maven-publish'
}

然后,此插件将公开可用于声明目录的catalog扩展


2. 定义目录


上面引入插件后,即可使用catalog扩展定义目录


catalog {
// 定义目录
versionCatalog {
from files('../libs.versions.toml')
}
}

然后可以通过maven-publish插件来发布目录


3. 发布目录


publishing {
publications {
maven(MavenPublication) {
groupId = 'com.zj.catalog'
artifactId = 'catalog'
version = '1.0.0'
from components.versionCatalog
}
}
}

我们定义好groupId,artifactId,version,from就可以发布了
我们这里发布到mavenLocal,你也可以根据需要配置发布到自己的maven
以上发布的所有代码可见:Catalog发布相关代码


4. 使用目录


因为我们已经发布到了mavenLocal,在仓库中引入mavenLocal就可以使用插件了


# settings.gradle
dependencyResolutionManagement {
//...
repositories {
mavenLocal()
//...
}
}

enableFeaturePreview('VERSION_CATALOGS')
dependencyResolutionManagement {
versionCatalogs {
libs {
from("com.zj.catalog:catalog:1.0.0")
// 我们也可以重写覆盖catalog中的groovy版本
version("groovy", "3.0.6")
}
}
}

如上就成功引入了插件,就可以使用catalog中的依赖了
这样就完成了依赖的项目间共享,以上使用的所有代码可见:Catalog使用相关代码


总结


项目间共享依赖是比较常见的需求,虽然我们也可以通过自定义插件实现,但还是不够方便
Gradle官方终于推出了Catalog,让我们可以方便地实现依赖的共享,Catalog主要具有以下特性:



  1. 对所有module可见,可统一管理所有module的依赖

  2. 支持声明依赖bundles,即总是一起使用的依赖可以组合在一起

  3. 支持版本号与依赖名分离,可以在多个依赖间共享版本号

  4. 支持在单独的libs.versions.toml文件中配置依赖

  5. 支持在项目间共享依赖


本文所有相关代码


Catalog发布相关代码
Catalog使用相关代码


参考资料



Sharing dependency versions between projects

收起阅读 »

国内知名Wchat团队荣誉出品顶级IM通讯聊天系统

iOS
国内知名Wchat团队荣誉出品顶级IM通讯聊天系统团队言语在先:想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)。想购买由类似ope...
继续阅读 »



国内知名Wchat团队荣誉出品顶级IM通讯聊天系统



团队言语在先:

想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)

。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)

。想购买由类似openfire第三方开源改造而来的所谓第三方通信server者勿扰

。想购买没有做任何安全加密场景者勿扰(随便一句api 一个接口就构成了红包收发/转账/密码设置等没有任何安全系数可言的低质产品)

。想购买非运营级别通信系统勿扰(到处呼喊:最稳定/真正可靠/大并发/真正安全!所有一切都需要实际架构支撑以及理论数值测验)

。想购买无保障/无支撑者勿扰(1W/4W/10W低质产品不可谓没有,必须做到:大并发支持合同保障/合作支持运维保障/在线人数支持架构保障)

。想购买消息丢包者勿扰(满天飞的所谓消息确认机制,最简单的测验既是前端支持消息收发demo测试环境,低质产品一秒收发百条消息必丢必崩,

别提秒发千条/万条,更低质产品可测验:同时发九张图片/根据数字12345678910发送出去,必丢!android vs ios)

。想购买大容量群uer者勿扰(随便宣传既是万人大群/几千大群/群组无限,小团队产品群组上线用户超过4000群消息体量不用很大手机前端必卡)

。最重要一点:口口声声说要运营很大的系统 却想出十几个money的人群勿扰,买产品做系统一要稳定二要长久用三要抛开运维烦恼,预算有限那就干脆

别买,买了几万的系统你一样后面用不起来会烂掉!

。产品体系包括:android ios server adminweb maintenance httpapi h5 webpc (支持server压测/前端消息收发压测/httpapi压测)

。。支持源码,但需要您拿去做一个伟大的系统出来!

。。团队产品目前国内没有同质化,客户集中在国外,有求高质量产品的个人或团队可通过以下方式联系到我们(低价者勿扰!)

。。。球球:383189941 q 513275129

。。。。产品不多介绍直接加我 测试产品更直接

。。。。。创新从未停止 更新不会终止 大陆唯一一家支持大并发保障/支持合同费用包含运维支撑的团队 

收起阅读 »

快来为你的照片添加个性标签吧!

搜索问题、话题或人… 问题 文章 代码 视频 活动· · ·ydhjhs发起Android快来为你的照片添加个性标签吧! 前言 需求图.png PS:最近在项目执行过程中有这样一个需求,要求拍完照的图片必须达到以上的效果。需求分析: 使用用预览布局Surfa...
继续阅读 »

搜索问题、话题或人…
问题
文章
代码
视频
活动
· · ·
ydhjhs
发起
Android
快来为你的照片添加个性标签吧!


  1. 前言

需求图.png


PS:最近在项目执行过程中有这样一个需求,要求拍完照的图片必须达到以上的效果。需求分析:


使用用预览布局SurfaceView,在不局上方使用控件的方式来进行设计,最后通过截图的方式将画面进行保存。


使用图片添加水印的方式来完成。



  1. 方法1 使用SurfaceView

我心想这不简单吗?于是开始一顿balabala的操作,结果到最后一步时发现,SurfaceView居然不能进行截图,截图下来的图片居然是一张黑色的。简单地说这是因为SurfaceView的特性决定的,我们知道安卓中唯一可以在子线程中进行绘制的view就只有Surfaceview了。他可以独立于子线程中绘制,不会导致主线程的卡顿,至于造成surfaceView黑屏的原因,可以移步这里
Android视图SurfaceView的实现原理分析。如果非要使用此方式时还是有三种思路来进行解决:
采用三种思路:


  1. 获取源头视频的截图作为SurfaceView的截图


  2. 获取SurfaceView的画布canvas,将canvas保存成Bitmap


  3. 直接截取整个屏幕,然后在截图SurfaceView位置的图



复制代码


但是我觉得这种方式太过繁琐,所以选择用添加水印的式来完成。



  1. 方法2 给拍照下来的图片添加水印

第一步:获取拍照权限








复制代码


这里使用到郭霖大佬的开源库PermissionX获取权限:


PermissionX.init(this)


.permissions(Manifest.permission.CAMERA,  Manifest.permission.RECORD_AUDIO)

.onExplainRequestReason { scope, deniedList ->

val message = "需要您同意以下权限才能正常使用"

scope.showRequestReasonDialog(deniedList, message, "确定", "取消")

}

.request { allGranted, grantedList, deniedList ->

if (allGranted) {

openCamera()

} else {

Toast.makeText(activity, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()

}

}

复制代码


第二步:拍照


android 6.0以后,相机权限需要动态申请。


// 申请相机权限的requestCode


private static final int PERMISSION_CAMERA_REQUEST_CODE = 0x00000012;


/**


* 检查权限并拍照。

* 调用相机前先检查权限。

*/

private void checkPermissionAndCamera() {


   int hasCameraPermission = ContextCompat.checkSelfPermission(getApplication(),

Manifest.permission.CAMERA);

if (hasCameraPermission == PackageManager.PERMISSION_GRANTED) {

//有调起相机拍照。

openCamera();

} else {

//没有权限,申请权限。

ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.CAMERA},

PERMISSION_CAMERA_REQUEST_CODE);

}

}


/**


* 处理权限申请的回调。

*/

@Override


public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {


   if (requestCode == PERMISSION_CAMERA_REQUEST_CODE) {

if (grantResults.length > 0

&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {

//允许权限,有调起相机拍照。

openCamera();

} else {

//拒绝权限,弹出提示框。

Toast.makeText(this,"拍照权限被拒绝",Toast.LENGTH_LONG).show();

}

}

}


复制代码


调用相机进行拍照


申请权限后,就可以调起相机拍照了。调用相机只需要调用startActivityForResult传一个Intent就可以了,但是这个Intent需要传递一个uri,用于保存拍出来的图片,创建这个uri时,各个Android版本有所不同,需要进行版本兼容。


//用于保存拍照图片的uri


private Uri mCameraUri;



// 用于保存图片的文件路径,Android 10以下使用图片路径访问图片

private String mCameraImagePath;



// 是否是Android 10以上手机

private boolean isAndroidQ = Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q;



/**

* 调起相机拍照

*/

private void openCamera() {

Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

// 判断是否有相机

if (captureIntent.resolveActivity(getPackageManager()) != null) {

File photoFile = null;

Uri photoUri = null;



if (isAndroidQ) {

// 适配android 10

photoUri = createImageUri();

} else {

try {

photoFile = createImageFile();

} catch (IOException e) {

e.printStackTrace();

}



if (photoFile != null) {

mCameraImagePath = photoFile.getAbsolutePath();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

//适配Android 7.0文件权限,通过FileProvider创建一个content类型的Uri

photoUri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", photoFile);

} else {

photoUri = Uri.fromFile(photoFile);

}

}

}



mCameraUri = photoUri;

if (photoUri != null) {

captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);

captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

startActivityForResult(captureIntent, CAMERA_REQUEST_CODE);

}

}

}



/**

* 创建图片地址uri,用于保存拍照后的照片 Android 10以后使用这种方法

*/

private Uri createImageUri() {

String status = Environment.getExternalStorageState();

// 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储

if (status.equals(Environment.MEDIA_MOUNTED)) {

return getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new ContentValues());

} else {

return getContentResolver().insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, new ContentValues());

}

}



/**

* 创建保存图片的文件

*/

private File createImageFile() throws IOException {

String imageName = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());

File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);

if (!storageDir.exists()) {

storageDir.mkdir();

}

File tempFile = new File(storageDir, imageName);

if (!Environment.MEDIA_MOUNTED.equals(EnvironmentCompat.getStorageState(tempFile))) {

return null;

}

return tempFile;

}

复制代码


接收拍照结果


@Override


protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {

super.onActivityResult(requestCode, resultCode, data);

if (requestCode == CAMERA_REQUEST_CODE) {

if (resultCode == RESULT_OK) {

if (isAndroidQ) {

// Android 10 使用图片uri加载

ivPhoto.setImageURI(mCameraUri);

} else {

// 使用图片路径加载

ivPhoto.setImageBitmap(BitmapFactory.decodeFile(mCameraImagePath));

}

} else {

Toast.makeText(this,"取消",Toast.LENGTH_LONG).show();

}

}

}

复制代码


注意:


这两需要说明一下,Android 10由于文件权限的关系,显示手机储存卡里的图片不能直接使用图片路径,需要使用图片uri加载。


另外虽然我在这里对Android 10和10以下的手机使用了不同的方式创建uri 和加载图片,但其实Android 10创建uri的方式和使用uri加载图片的方式在10以下的手机是同样适用的。
android 7.0需要配置文件共享。

android:name="androidx.core.content.FileProvider"

android:authorities="${applicationId}.fileprovider"

android:exported="false"

android:grantUriPermissions="true">


android:name="android.support.FILE_PROVIDER_PATHS"

android:resource="@xml/file_paths" />



复制代码


在res目录下创建文件夹xml ,放置一个文件file_paths.xml(文件名可以随便取),配置需要共享的文件目录,也就是拍照图片保存的目录。


<?xml version=”1.0” encoding=”utf-8”?>









name="images"

path="Pictures" />





复制代码


第三步:给拍照后得到的图片添加水印


@Override


protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {

super.onActivityResult(requestCode, resultCode, data);

if (requestCode == CAMERA_REQUEST_CODE) {

if (resultCode == RESULT_OK) {

Bitmap mp;

if (isAndroidQ) {

// Android 10 使用图片uri加载

mp = MediaStore.Images.Media.getBitmap(this.contentResolver, t.uri);

} else {

// Android 10 以下使用图片路径加载

mp = BitmapFactory.decodeFile(uri);

}

//对图片添加水印 这里添加一张图片为示例:

ImageUtil.drawTextToLeftTop(this,mp,"示例文字",30,R.color.black,20,30)

} else {

Toast.makeText(this,"取消",Toast.LENGTH_LONG).show();

}

}

}

复制代码


这里使用到一个ImageUtil工具类,我在这里贴上。如果需要使用可以直接拿走~


public class ImageUtil {


/**

* 设置水印图片在左上角

*

* @param context 上下文

* @param src

* @param watermark

* @param paddingLeft

* @param paddingTop

* @return

*/

public static Bitmap createWaterMaskLeftTop(Context context, Bitmap src, Bitmap watermark, int paddingLeft, int paddingTop) {

return createWaterMaskBitmap(src, watermark,

dp2px(context, paddingLeft), dp2px(context, paddingTop));

}



private static Bitmap createWaterMaskBitmap(Bitmap src, Bitmap watermark, int paddingLeft, int paddingTop) {

if (src == null) {

return null;

}

int width = src.getWidth();

int height = src.getHeight();

//创建一个bitmap

Bitmap newb = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);// 创建一个新的和SRC长度宽度一样的位图

//将该图片作为画布

Canvas canvas = new Canvas(newb);

//在画布 0,0坐标上开始绘制原始图片

canvas.drawBitmap(src, 0, 0, null);

//在画布上绘制水印图片

canvas.drawBitmap(watermark, paddingLeft, paddingTop, null);

// 保存

canvas.save(Canvas.ALL_SAVE_FLAG);

// 存储

canvas.restore();

return newb;

}



/**

* 设置水印图片在右下角

*

* @param context 上下文

* @param src

* @param watermark

* @param paddingRight

* @param paddingBottom

* @return

*/

public static Bitmap createWaterMaskRightBottom(Context context, Bitmap src, Bitmap watermark, int paddingRight, int paddingBottom) {

return createWaterMaskBitmap(src, watermark,

src.getWidth() - watermark.getWidth() - dp2px(context, paddingRight),

src.getHeight() - watermark.getHeight() - dp2px(context, paddingBottom));

}



/**

* 设置水印图片到右上角

*

* @param context

* @param src

* @param watermark

* @param paddingRight

* @param paddingTop

* @return

*/

public static Bitmap createWaterMaskRightTop(Context context, Bitmap src, Bitmap watermark, int paddingRight, int paddingTop) {

return createWaterMaskBitmap(src, watermark,

src.getWidth() - watermark.getWidth() - dp2px(context, paddingRight),

dp2px(context, paddingTop));

}



/**

* 设置水印图片到左下角

*

* @param context

* @param src

* @param watermark

* @param paddingLeft

* @param paddingBottom

* @return

*/

public static Bitmap createWaterMaskLeftBottom(Context context, Bitmap src, Bitmap watermark, int paddingLeft, int paddingBottom) {

return createWaterMaskBitmap(src, watermark, dp2px(context, paddingLeft),

src.getHeight() - watermark.getHeight() - dp2px(context, paddingBottom));

}



/**

* 设置水印图片到中间

*

* @param src

* @param watermark

* @return

*/

public static Bitmap createWaterMaskCenter(Bitmap src, Bitmap watermark) {

return createWaterMaskBitmap(src, watermark,

(src.getWidth() - watermark.getWidth()) / 2,

(src.getHeight() - watermark.getHeight()) / 2);

}



/**

* 给图片添加文字到左上角

*

* @param context

* @param bitmap

* @param text

* @return

*/

public static Bitmap drawTextToLeftTop(Context context, Bitmap bitmap, String text, int size, int color, int paddingLeft, int paddingTop) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

dp2px(context, paddingLeft),

dp2px(context, paddingTop) + bounds.height());

}



/**

* 绘制文字到右下角

*

* @param context

* @param bitmap

* @param text

* @param size

* @param color

* @return

*/

public static Bitmap drawTextToRightBottom(Context context, Bitmap bitmap, String text, int size, int color, int paddingRight, int paddingBottom) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

bitmap.getWidth() - bounds.width() - dp2px(context, paddingRight),

bitmap.getHeight() - dp2px(context, paddingBottom));

}



/**

* 绘制文字到右上方

*

* @param context

* @param bitmap

* @param text

* @param size

* @param color

* @param paddingRight

* @param paddingTop

* @return

*/

public static Bitmap drawTextToRightTop(Context context, Bitmap bitmap, String text, int size, int color, int paddingRight, int paddingTop) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

bitmap.getWidth() - bounds.width() - dp2px(context, paddingRight),

dp2px(context, paddingTop) + bounds.height());

}



/**

* 绘制文字到左下方

*

* @param context

* @param bitmap

* @param text

* @param size

* @param color

* @param paddingLeft

* @param paddingBottom

* @return

*/

public static Bitmap drawTextToLeftBottom(Context context, Bitmap bitmap, String text, int size, int color, int paddingLeft, int paddingBottom) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

dp2px(context, paddingLeft),

bitmap.getHeight() - dp2px(context, paddingBottom));

}



/**

* 绘制文字到中间

*

* @param context

* @param bitmap

* @param text

* @param size

* @param color

* @return

*/

public static Bitmap drawTextToCenter(Context context, Bitmap bitmap, String text, int size, int color) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

(bitmap.getWidth() - bounds.width()) / 2,

(bitmap.getHeight() + bounds.height()) / 2);

}



//图片上绘制文字

private static Bitmap drawTextToBitmap(Context context, Bitmap bitmap, String text, Paint paint, Rect bounds, int paddingLeft, int paddingTop) {

android.graphics.Bitmap.Config bitmapConfig = bitmap.getConfig();



paint.setDither(true); // 获取跟清晰的图像采样

paint.setFilterBitmap(true);// 过滤一些

if (bitmapConfig == null) {

bitmapConfig = android.graphics.Bitmap.Config.ARGB_8888;

}

bitmap = bitmap.copy(bitmapConfig, true);

Canvas canvas = new Canvas(bitmap);



canvas.drawText(text, paddingLeft, paddingTop, paint);

return bitmap;

}



/**

* 缩放图片

*

* @param src

* @param w

* @param h

* @return

*/

public static Bitmap scaleWithWH(Bitmap src, double w, double h) {

if (w == 0 || h == 0 || src == null) {

return src;

} else {

// 记录src的宽高

int width = src.getWidth();

int height = src.getHeight();

// 创建一个matrix容器

Matrix matrix = new Matrix();

// 计算缩放比例

float scaleWidth = (float) (w / width);

float scaleHeight = (float) (h / height);

// 开始缩放

matrix.postScale(scaleWidth, scaleHeight);

// 创建缩放后的图片

return Bitmap.createBitmap(src, 0, 0, width, height, matrix, true);

}

}



/**

* dip转pix

*

* @param context

* @param dp

* @return

*/

public static int dp2px(Context context, float dp) {

final float scale = context.getResources().getDisplayMetrics().density;

return (int) (dp * scale + 0.5f);

}

}


复制代码



  1. 最终实现的效果如下:

效果.jpg


5.总结


整体来说没有什么太大的问题,添加水印的原理就是通过Canvas绘制的方式将文字/图片添加到图片上。最后再将修改之后的图片呈现给用户。同时也记录下SurfaceView截图黑屏的问题。


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

0 收藏 分享 举报 2021-05-11
0 个评论
ydhjhs
写下你的评论…
发起人
柳天明柳天明
推荐内容
java设计模式:原型模式
算法与数据结构之数组
算法与数据结构之算法复杂度
算法与数据结构之链表
java设计模式:抽象工厂模式
java 设计模式:责任链模式与Android事件传递
java 设计模式:观察者
java 设计模式:模版方法
java 设计模式:策略模式
java 设计模式:工厂方法模式
一个开放、互助、协作、创意的社区
关于imGeek关于专职工程师值守社区财富榜赞助商友情链接热门标签
京ICP备14026002号-3
收起阅读 »

安卓 客服云 企业欢迎语集成

1.后台配置企业欢迎语   管理员模式---设置--功能设置  企业欢迎语     2.代码   ChatClient.getInstance().chatManager().getEnterpriseWelcome(new ValueCallBack...
继续阅读 »
1.后台配置企业欢迎语
  管理员模式---设置--功能设置  企业欢迎语 
  



2.代码
 

ChatClient.getInstance().chatManager().getEnterpriseWelcome(new ValueCallBack() {
@Override
public void onSuccess(String value) {
Log.i("TAG value", value);
String enterpriseWelcome=value;
if (!TextUtils.isEmpty(value)) {
ChatClient.getInstance().chatManager().getCurrentSessionId(toChatUsername, new ValueCallBack() {//toChatUsername替换为自己的IM服务号
@Override
public void onSuccess(String value) {
Log.e("TAG value:", value + " 当返回value不为空时,则返回的当前会话的会话ID,也就是说会话正在咨询中,不需要发送欢迎语");
if (value.isEmpty()) {//
Message message = Message.createReceiveMessage(Message.Type.TXT);

EMTextMessageBody body = null;

body = new EMTextMessageBody(enterpriseWelcome);

message.setFrom(toChatUsername);//toChatUsername替换为自己的IM服务号

message.addBody(body);

message.setMsgTime(System.currentTimeMillis());

message.setStatus(Message.Status.SUCCESS);

message.setMsgId(UUID.randomUUID().toString());

ChatClient.getInstance().chatManager().saveMessage(message);

messageList.refresh();
}
}

@Override
public void onError(int error, String errorMsg) {

}
});
}

}

@Override
public void onError(int error, String errorMsg) {

}
});












收起阅读 »