作者:刘天宇(谦风)

系列文章回顾《向工程腐化开炮 | proguard治理》《向工程腐化开炮 | manifest治理》。本文为系列文章第三篇,虽然标题是java代码,但准确来讲,本文主要聚焦的是jvm字节码,因此相关工具和治理,对于kotlin也同样适用,如无特殊情况,不再单独说明。此外,还会涉及到java资源。

java代码腐化和失控,主要体现在不合理代码使用不断累积。这里“不合理”的定义,由上层场景决定,例如在当前隐私合规监管态势下,我们不允许非预期的,代码直接调用系统敏感API,那么“对系统敏感API的直接调用”就是不合理。java代码治理,正是围绕这种“不合理“代码使用,逐步展开。

基础知识

本章不会介绍java语言本身,相信大家对此已有足够熟悉。相对的,会从工程应用角度,讲解几个有意思的技术点。

1.1 由源码到apk

源代码是如何经历多重处理,最终呈现在apk中,了解这个过程,有助于我们认清java代码腐化的一些原因。从apk构建视角来看,java(kotlin)代码完整处理过程如下:

上述流程中,无论是java还是kotlin代码,都会首先编译为jvm字节码。这里需要注意,app/子工程中的local jar、flat aar,以及通过外部依赖方式引入的jar、aar,都是直接包含编译好的jvm字节码,这会带来如下优劣势:

  • 【优势】无需再进行由源码到字节码的编译,在代码完全相同情况下,工程的模块化(jar/aar)程度越高,越能够缩短整体apk构建耗时;
  • 【劣势】提前编译好的jvm字节码,不会再进行源码编译期各项检查,容易出现代码间引用关系不匹配情况,具体后文「不兼容引用」部分会详述。

此外,关于jvm和Dalvik字节码,一个最核心的区别是:前者的指令,基于栈,后者基于寄存器。基于寄存器的优势,主要是运行时指令执行性能的提升。此外,jvm字节码,每个类位于独立.class文件,而dalvik字节码,所有类均位于同一(几)个dex文件,能够更好的复用代码数据,因此存储占用更低。

1.2 使用java8

java8是一个比较有代表性的java语言版本,在Android中使用java8,这个话题本身会比较复杂。首先,从java8新内容来看,主要分为新语言特性和新API;其次,从编码到运行的整个链路来看,涉及编译工具链、设备预装jdk、设备vm(Dalvik/Art)三个部分的支持;此外,Android本身使用的jdk并不是标准的oracle jdk或openjdk,而是进行了一些定制后的子集。

java8新语言特性,有一些涉及到新的jvm指令集,这些需要运行时vm能够支持。否则,就需要在编译工具链中,能够使用兼容的指令集来替换这些新指令集的功能,这个过程就是大家熟悉的“脱糖”,AndroidGradlePlugin3.0及以上版本,已经对此实现了较好的支持。由于Android系统中Art虚拟机,直到8.0版本,才完全实现对java8新指令集的支持,因此当apk构建的minSdkVersion设置为26(8.0)以下时,会触发脱糖处理。java8的主要新语言特性如下:

对于java8新API,两组具有代表性的是「流式编程streams」和「函数式编程functional」,直到os7.0才提供了较完整的支持。具体如何在更低minSdkVersion时使用这些java8新API,可以参考文末官方文档。

本节对在Android中使用java8的一些基础知识,进行了讲解,关于在工程中如何具体配置,官方文档已经给出了清晰的说明,在此不赘述。

1.3 DX vs D8

由jvm字节码“转换到”Dalvik字节码,需要编译工具来完成,这一任务由DX或D8承担。DX是第一代工具,D8是第二代工具,相对于DX,D8在编译速度、产物大小、代码性能方面,全面超越DX,官方blog中,给出的编译速度收益约25%,产物大小收益约5%:

在优酷这边,由DX升级到D8后,apk包大小降低约9%(额外对dex合并进行了优化,dex数量降低,对包大小收益较大),由此冷启动阶段dex加载耗时也降低了50ms。编译耗时无法做拆解性统计,但是肯定有正向收益。

此外,对于上一小节讲到的脱糖处理,DX并不包含,因此需要单独的前置脱糖处理,而D8则可以在转换过程中,直接进行脱糖,在速度和脱糖支持的全面性上,均有提升。

1.4 java资源

java资源如何定义,如何使用,在这里不做讲解。在Android开发领域,有一点需要关注的是,Java资源会原封不动放置到最终apk内。这就意味着,如果java资源相对路径,与apk内其它类型元素一致,java资源会“伪装”为其它类型元素,如果和对应类型元素有同路径文件,还会发生覆盖,引发运行时风险,提高问题排查难度。

其中,res目录下资源“伪装”不完整,对应资源在R类,以及资源符号表resources.arsc中,并没有记录,因此,在运行时无法当作正常资源使用。此外,google也注意到java资源对Android特有元素的这种干扰,在AndroidGradlePlugin7.0及以上版本,以java资源形式“混”入到最终apk内的so,会被剔除。

治理实践

随着工程模块/代码增加,腐化逐步积累,对app负面影响也逐步加深:定位一个类、java资源的成本越来越高;线程随意使用,导致线上卡顿、崩溃(华为某些机型对线程总数有限制);敏感API调用缺乏整体管控,存量隐私合规整改,成本极高,新增敏感API调用,对监管机构复测埋下隐患;不兼容引用,必定产生运行时异常,如果涉及功能定投或被非预期catch住,很容易产生严重故障。在此不逐一列举,后文会详细介绍。

优酷在与java代码“腐化”斗争中,从上层实际需求(例如阻塞集成、线上故障、隐私合规、线程/Phenix图片库使用管控)出发,通过相关工具建立有效的检测能力,并基于此形成日常研发卡口机制。在确保问题零新增前提下,逐步消化已有存量问题。

在问题定位、排查过程中,快速获取java代码、java资源来自哪个模块,以及代码逻辑,是最基础的诉求。二、三方模块大量引入,以及app工程模块化程度提高,都让上述信息获取的成本变得越来越高。为此,在工具层面,开发了以下几项辅助分析功能。

  • 模块包含java类列表。可以快速查看,目标类位于哪个模块(外部依赖模块、app工程、subproject工程、local jar、flat aar):
com.android.support:support-annotations:26.0.0
|-- android.support.annotation.AnyThread
|-- android.support.annotation.ColorResproject:library-aar-2:1.0
|-- com.example.libraryaar2.LibraryAar2ClassOne
|-- com.example.libraryaar2.CodeUsage2project:app:1.0
|-- com.example.myapplication.proguard.TestProguardParcel$1
  • 模块包含java资源列表。用于查看目标java资源,来自哪个模块:
com.youku.support:moduleOne:1.6.3.9
|-- com/abc/security/readme.txtcom.youku.arch:moduleTwo:1.6.3.9
|-- com/tencent/mm/sdk/platformtools/rep5402863540997075488.tmp
|-- com/abc/svc.manifest
  • class代码打印。将所有.class字节码,反编译为可读文件(与javap反编译后格式一致),为全局排查相关问题提供便利:
# 目录结构示例:
build/outputs
└── op-code├── com.youku.android-moduleOne-1.0.2.23│   └── com│       └── youku│           └── android│               ├── AnimEndEvent.txt│               ├── AnimIntervalEvent.txt│               ├── AnimKeyFrameData.txt│               ├── AnimMotionCaptureNode.txt│               ├── AnimNodeBase.txt│               ├── AnimSequenceBase.txt# 内容示例:
// class version 52.0 (52)
// access flags 0x21
public class com/example/libraryaar1/LibraryAarClassOne {// access flags 0x1public Ljava/lang/String; fieldOne// access flags 0x1public <init>()VALOAD 0INVOKESPECIAL java/lang/Object.<init> ()VALOAD 0LDC "library_aar_class_one_field_one"PUTFIELD com/example/libraryaar1/LibraryAarClassOne.fieldOne : Ljava/lang/String;RETURNMAXSTACK = 2MAXLOCALS = 1// access flags 0x9public static test()VLDC "LibraryAarClassOne"LDC "just test"INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)IPOPRETURNMAXSTACK = 2MAXLOCALS = 0
}

有了以上几项辅助分析工具作为基础,接下来逐一对各项腐化治理实践进行讲解。

2.1 代码使用检测

当app功能越来越复杂时,想要统一对某个基础能力的正确使用,会变得十分困难,腐化由此变得一发不可收拾。在优酷中就遇到了不少这类问题,迫切需要一种有效的方式,拦截不规范的代码使用。

为此,开发了基于规则的,代码间静态引用检测。以线程为例,假设我们规定所有对线程的使用,必须统一到指定线程池实现,那么我们就需要禁止所有其它的线程使用方式:'new java.lang.Thread', 'new java.util.concurrent.ThreadPoolExecutor', 'new java.util.concurrent.Executors', 'new java.util.Timer', 'new android.os.AsyncTask'],示例检测结果,列出了哪些模块中的哪些类,存在对上述代码的使用:

com.youku.arch:Hd:2.8.15
|-- com.youku.arch.hd.HChk$2 [method@android.os.AsyncTask|<init>, method@java.util.concurrent.Executors|<init>]project:library-aar-1:1.0
|-- com.example.libraryaar1.desugar.LambdaUsage [method@java.lang.Thread|<init>]

自定义检测,支持以下四种元规则:

  • 类: <class_full_name>。例如java.lang.Thread;
  • 方法: method@<class_full_name>|<method_name>。例如method@android.location.Location|getLongitude;
  • 变量: field@<class_full_name>|<field_name>。例如field@com.onepiece.demo.Util|fieldOne;
  • 字符常量: cst_string@<string>。例如cst_string@i am string。

上述四种元规则,可以进行“逻辑与”组合,形成复合规则:在同一个方法体内,这些元规则必须都命中,这条规则才算命中。例如:method@java.lang.Class|forName&cst_string@com.android.internal.R$dimen&cst_string@status_bar_height,可以用来检测反射获取status_bar高度的代码:

public class CodeUsage {public static void getStatusBarHeight(Context context) {try {Class<?> c = Class.forName("com.android.internal.R$dimen");Object obj = c.newInstance();Field field = c.getField("status_bar_height");int x = Integer.parseInt(field.get(obj).toString());context.getResources().getDimensionPixelSize(x);} catch (Exception e) {Log.d("ViewUtils", "get status bar height fail");e.printStackTrace();}}
}

与此同时,提供白名单配置,暂时排除一些二、三方库。更进一步,提供选项,当检测结果不通过时,终止构建过程,形成卡口机制。目前,优酷将这个检测能力,应用到了三个具体场景,下面逐一道来。

1、线程管控

对线程的使用,非常容易“失控”:线程数过多,增加os线程调度耗时,以及内存占用;创建不销毁,长时间占用等情况,定位排查困难;某些厂商定制os对线程数有限制,超限后再创建线程,会直接抛出OOM异常。

诸如此类问题,都需要收敛到统一的线程池实现解决,但是如何能够做到这种收敛,前述的「代码使用检测」能力,正好可以发挥作用。通过配置所有可能的非统一线程池调用规则,进行检测和拦截,有效实现了对线程使用的管控:

优酷2021年1月,上线此线程管控卡口。对于存量问题类,降低了1/8左右,后续会持续对自研代码进行清理,二、三方SDK则通过线程池注入的方式完成收敛。对于新增非统一线程池使用,累计拦截19次,防控效果明显。

在实际效果上,线程数超限导致的Java崩溃,已经大幅减少,从此“淡出”top crash行列。此外,收敛到的自研统一线程池,包含详细的各阶段耗时统计,线程不销毁和长时间占用等问题,都能够得到有效监控、定位和解决。

2、敏感API管控

当前隐私合规监管态势,日趋严格,敏感权限、以及敏感信息获取,是其中两个非常重点的监管项。在代码层面,对相关信息获取涉及到的API调用,进行统一收口管控,是一种非常有效的手段。

权限声明的治理和管控,在「向工程腐化开炮 | manifest治理」一文中,已经给出了解决方案。对于敏感信息获取,如何能够实现以上运行时的整体收口管控呢?同样,基于「代码使用检测」能力,通过对所有系统敏感信息对应的API,配置为检测规则,将这些调用收敛到统一封装的SDK中。

优酷2021年6月,上线敏感API管控卡口,随着监管力度加强,检测规则也从10几条增长到现在近50条,目前已趋于稳定。对存量敏感API调用,随着历次合规整改,也进行了大量的收敛改造;对新增问题,累计拦截11次,有效保障敏感信息获取的可控性,避免遭遇监管“回头看”式的抽查问责风险。

3、Phenix图片库管控

Phenix图片库是阿里集团内的图片加载中间件,提供了优秀的性能体验,以及丰富的功能。优酷很早就基于Phenix非管道模式进行了上层扩展,支持对url形式图片进行webp转换、尺寸自适应裁剪等处理,提高客户端图片加载性能,降低内存占用,同时也降低服务端成本。在2017年加载图片的webp占比一度达到68%,而2021年webp占比降低至24%,其中新增284个以管道方式加载图片的java类,是webp占比降低的重要因素。

为此,同样利用「代码使用检测」能力,对Phenix管道模式使用进行限制,以充分发挥非管道模式的优势。对于直接使用管道模式的存量代码,已经完成全面排查,并发起整改治理。对应卡口于21年11月底刚上线,用于有效拦截Phenix图片库的不合理使用。

2.2 不兼容引用

相信Android开发同学,对运行时NoClassDefFoundError、NoSuchMethodError、NoSuchFieldError并不陌生,这几类Java异常,正是由于不兼容的代码引用导致。前文「由源码到apk」一节,已经讲到了发生不兼容引用原因:jar包中提前编译好的jvm字节码,不会再进行源码编译期各项检查,容易出现代码间引用关系不匹配情况。下面我们来看看这个不兼容引用,到底是怎么发生的:

在上图示例中,模块B工程首先编译并发布1.0版本到maven中。然后模块A工程中以外部依赖形式,声明对模块B的引用,A类调用B类中的b方法,编译并发布1.0版本到maven仓库。此时apk1以外部依赖形式引入模块A和B,打包为apk1后,一切正常。此后,模块B将B类改名为C,然后发布2.0版本到maven,apk更新模块B版本号到2.0,打包为apk2后,不兼容引用问题发生:类B在apk中并不存在,运行时必定导致NoClassDefFoundError。

总结一下,不兼容引用,是指代码中调用了不存在(不匹配)的变量/方法,会导致运行时产生异常。有以下几种典型情况:

  • 被调用类不存在;
  • 被调用类的变量不存在,或者变量签名不匹配;
  • 被调用类的方法不存在,或者方法签名不匹配。

对此,开发了针对性的检测能力。来看一份示例结果:

# 解释:oh模块,FileUtil类copyFile方法中,调用的com.alibaba.fastjson.util.IOUtils->close,对应class不存在。一旦执行到,必定会抛出NoClassDefFoundError异常。
com.youku.android:oh:0.3.35.34
|-- com.youku.arch.util.FileUtil
|   |-- copyFile : (Ljava/lang/String;Ljava/lang/String;)V
|   |   |-- [class-no-module] com.alibaba.fastjson.util.IOUtils->close : (Ljava/io/Closeable;)V# 解释:AFManager模块,AFManagerImpl类aFCheck方法中,调用的msdk.ApiLockHelper->lock,class存在但是无此方法定义(无安全没有这个方法,或者方法签名不对)。一旦执行到,必定会抛出NoSuchMethodError异常。
com.youku.android:AFManager:1.0.1
|-- com.youku.android.af.AFManagerImpl
|   |-- aFCheck : (Ljava/lang/String;Landroid/content/Context;ILjava/util/Map;)Z
|   |   |-- msdk.ApiLockHelper->lock : (Ljava/lang/String;J)V (com.youku.android:msdk:1.0)

优酷于21年6月上线此卡口,新增防控情况如下:

存量问题,进行了定向到团队的分发,有一小部分得到了及时清理,为了避免对各业务团队日常研发活动,产生较大影响,添加到了白名单,待后续适当时机再发起清理。新增问题实现100%全拦截,线上未发生代码变更导致的此类crash或业务异常。

2.3 同名类

同名类,是指类名(全限定名)相同的类。如果两个类,仅存在大小写不同,那么在大小写不敏感的文件存储系统中(macos默认就是),会被认为是同一个类,这会导致无法编译通过。在优酷历次迭代中,就发生过这样一个案例:一个二方模块工程的混淆配置问题,导致jar包中出现不同类,仅存在大小写不同,在打包平台(Linux)中可以成功构建,并进入集成,直接导致开发同学无法在本地macos机器中进行打包,影响了研发效率,两个版本(1个月)后得到修复。

对于这个问题,开发了同名类检测能力,无论在Linux还是macos中,均可以一致的检测出同名类:

com.ali.sty.ridentity.build.va
|-- com.ali.sty.ridentity.build.Va : com.ali.sty.ridentity:rpsdk:4.8.5
|-- com.ali.sty.ridentity.build.va : com.ali.sty.ridentity:rpsdk:4.8.5com.ali.sty.ridentity.build.vb
|-- com.ali.sty.ridentity.build.Vb : com.ali.sty.ridentity:rpsdk:4.8.5
|-- com.ali.sty.ridentity.build.vb : com.ali.sty.ridentity:rpsdk:4.8.5

这项检测能力,同样提供了选项,当检测结果不通过时,终止构建过程,用于形成卡口。优酷于21年7月上线对应卡口,至今未出现新增。

2.4 硬编码文本

隐私合规检测机构,会检测apk中的一些敏感文本,做为隐私合规问题的重点怀疑&验证点,例如「发票抬头」、「身份证」等。其中一部分就是来自于java代码中的硬编码文本(另外可能的来源是资源、so)。硬编码文本,存在以下缺点:

  • 易冗余。多处代码使用同一文本时,如果最终不在同一个dex,会导致不同dex常量池中存在多份此文本;
  • 不灵活。当线上版本出现问题时(例如各类运营活动),难以动态修改;
  • 低安全。一些敏感信息,如果以明文硬编码文本形式存在,非常容易被获取后,用于不正当用于。

对于这类问题,开发了对应检测能力,可以自定义正则表达式,对代码中的硬编码文本(字节码指令中的字符串常量)进行匹配。检测结果中,按照模块、类进行逐级聚合。以所有中文字符检测为例:

# 示例代码(部分):
public class TestCodeB {public static void methodA(Context context) {Toast.makeText(context, "I'am english text hardcoded with java code2.", Toast.LENGTH_SHORT).show();}public void methodB(Context context) {Toast.makeText(context, "我是java代码中硬编码的中文文本3.", Toast.LENGTH_SHORT).show();}
}# 示例检测结果:
project:app:1.0
|-- com.example.myapplication.proguard.TestProguardClass
|   |-- [text] 我是java代码中硬编码的中文文本.
|-- com.example.myapplication.code.TestCodeB
|   |-- [text] 我是java代码中硬编码的中文文本3.
|-- com.example.myapplication.code.TestCodeA
|   |-- [text] 我是java代码中硬编码的中文文本1.
|   |-- [text] 我是java代码中硬编码的中文文本2.

目前在优酷,隐私合规相关的一些敏感文本,是一个正在进行的探索方向,由于目前没有明确规则,因此还没有实际落地使用。不过,在日常研发过程,对于需要查找特定硬编码文本的场景,已经能够起到很好的辅助提效作用。

2.5 非法java资源

如第一章「java资源」小节所述,java资源可以“伪装”为dex、Android资源、动态链接库so,一旦发生同名覆盖,会引发非预期的运行时异常。即使不发生覆盖,也会给相关元素在构建过程的处理,以及app运行时问题定位和排查,带来不必要麻烦。

针对这种非法java资源,开发了相应检测能力,可以自定义非法java资源规则,与规则匹配的java资源会被检测出来。以下示例,配置了dex、android资源(res/assets)、动态链接库so,规则集合为:['^lib/.+', '^res/.+', '^assets/.+', '^classes\\d*\\.dex'],示例内容如下:

* project:library-aar-1:1.0
|-- [ignored] classes20.dex
|   |-- [hitRule] ^classes\d*\.dex
|-- res/drawable/fake_drawable.png
|   |-- [hitRule] ^res/.+
|-- assets/java_resource_under_assets.xml
|   |-- [hitRule] ^assets/.+
|-- lib/arm64-v8a/libdwebp.so
|   |-- [hitRule] ^lib/.+.....

与此同时,提供白名单配置,排除一些特殊非法java资源的影响。更近一步,提供选项,当检测结果不通过时,终止构建过程,形成卡口机制。以上也是当前优酷在使用的规则,各app在实践中可以根据实际情况,进行自由定制。

优酷这边的存量非法java资源,包含了两个flutter的so,以及三方sdk引入的assets,整改工作量不大,后续会择机进行。对于前者,在AndroidGradlePlugin7.0及以上会直接忽略,有了这个检测结果,就可以提前整改,为AGP升级到7.0扫清障碍。非法java资源卡口,接下来很快会在优酷上线,保障后续无新增。这个例子,体现出对工程腐化问题的治理,既要面向当下,严防死守,也要防范于未然,主动出击。

2.6 治理全景

至此,对于java代码,进行了较全面有效的防腐化能力建设和治理。最后,给出一份全景图:

还能做些什么

相对于其它类型元素,java代码是大部分Android开发同学,最主要的创作产出。基于代码使用检测能力,对各种基础功能,进行统一的管控治理,除了本文讲到的这三项,还有更多的场景可以挖掘。例如,使用原生SharedPreferences,进行kv存储,不支持主线程,容易造成anr,可以统计迁移到更好的实现库。类似这样的场景,我们还会持续的进行探索。

与工程腐化的对抗,路漫修远,道阻且长,与诸君共勉。

【参考文档】

  • 【google】使用 Java 8 语言功能和 API:https://developer.android.com/studio/write/java8-support
  • 【jakewharton】Android's Java 8 Support:https://jakewharton.com/androids-java-8-support/

关注【阿里巴巴移动技术】微信公众号,每周 3 篇移动技术实践&干货给你思考!

向工程腐化开炮:Java代码治理相关推荐

  1. 向工程腐化开炮 | 治理思路全解

    作者:刘天宇(谦风) 系列文章回顾<向工程腐化开炮 | proguard治理><向工程腐化开炮 | manifest治理><向工程腐化开炮:Java代码治理>< ...

  2. 【Unity3D】Android Studio 工程中使用 Java 代码调用 Unity 的 C# 脚本 ( Java 中调用 UnityPlayer#UnitySendMessage 方法 )

    文章目录 一. Java 调用 C# 依赖库准备 1.依赖库位置 2.unityLibrary 依赖库位置 二. Java 调用 C# 的 UnityPlayer#UnitySendMessage 方 ...

  3. 实际工程中加快 Java 代码编写的小提示

    这里我简单谈谈 Java 语法在编程效率方面的弱势,以及如何补救. 一.集合的快速创建 C# 是少数拥有集合字面值(又叫初始化表达式)的静态语言之一. var list = new List<i ...

  4. java工程如何跑起来的_你编写的Java代码是咋跑起来的?

    如果你是一名 Java 开发人员,你肯定指定 Java 代码有很多种不同的运行方式.比如说可以在开发工具(IDEA.Eclipse等)中运行,可以双击执行 jar 文件运行,也可以在命令行中运行,甚至 ...

  5. 查看工程里有多少行java代码

    public class TestCodeNum {public static void main(String[] args) throws IOException {// java代码int ja ...

  6. java获取jndi密码_用JAVA代码获取Weblogic配置的JNDI 数据源连接

    第一步:生成与JDK版本对应的weblogicjar,利用cmd 进入到weblogic_home 路径下进入到server/lib目录,然后运行  JDK  1.6 命令 "java -j ...

  7. Hadoop HDFS文件操作的Java代码

    1.创建目录 import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.ha ...

  8. maven 主工程 java_Maven创建Java Application工程(既jar包)

    Maven在创建工程时使用的是archetype(原型)插件,而如果要创建具体的工程,比如Application这些,那么可以使用maven-archetype-quickstart(相当于一个子类型 ...

  9. 使用Eclipse可以方便的统计工程或文件的代码行数,

    使用Eclipse可以方便的统计工程或文件的代码行数,方法如下: 1.点击要统计的项目或许文件夹,在菜单栏点击Search,然后点击File... 2.选中正则表达式(Regular expressi ...

  10. 阿里巴巴Java“代码反潜机”P3C喜提首届中国优秀开源项目二等奖!

    12 月 12-14 日,由中国开源云联盟.中国电子技术标准化研究院和全国信标委云计算标准工作组主办的第八届中国云计算标准和应用大会在京举行. 聚焦企业上云和开源,大会公布了首届中国优秀开源项目的名单 ...

最新文章

  1. 【Qt】QStackedWidget:将多个窗口控件放入堆中,每次只显示一个窗口控件
  2. python中二进制以什么开头_Python二进制表示和位操作
  3. 全球自动驾驶汽车发展指数哪家强?美德领衔,中国第七
  4. VTK:Utilities之RenderScalarToFloatBuffer
  5. 宜搭功能更新:子表全面升级,高级流程可以批量审批
  6. C语言程序练习-L1-011 A-B (20分)
  7. 服务器操作系统用什么好,服务器操作系统一般用什么
  8. jsonp react 获取返回值_必须要会的 50 个React 面试题(下)
  9. Spark应用日志级别设置
  10. .net分页控件webdiyer:AspNetPager
  11. docker技术之基本命令
  12. python urllib.parse_Python3 urllib.parse 常用函数示例
  13. 学习CNDS博客写作
  14. 高级维修电工实训装置
  15. 数字化转型:中国企业数字化转型趋势
  16. cmd看控制台输出红桃、方块、黑桃、梅花乱码解决
  17. 《老路用得上的商学课21—25》消费心理学(二)
  18. CRYPTO buuctf 摩斯
  19. 汇总 | 嵌入式软硬件领域各种“黑科技”
  20. -----已搬运-------Linux的/proc/self/学习 ++ CTF例题

热门文章

  1. 自助图书馆系统-Tkinter界面和openpyxl表格综合设计案例
  2. Python爬虫实战(6)-爬取QQ空间好友说说并生成词云(超详细)
  3. timestamp和datetime的区别
  4. ThoughtWorks思特沃克2018校园招聘之春招家庭作业 - 无人机
  5. JZ2440 ping不通电脑的一种可能原因 PING 192.168.88.226 (192.168.88.226): 56 data bytes
  6. error: RPC failed; curl 56 OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 10054解决方法
  7. iOS 16横竖屏切换适配
  8. Excel——如何批量从身份证号码中提取出年龄
  9. 黑客是什么?揭开郭盛华的神秘面纱,讲解他不为人知传奇故事
  10. 2018杭州云栖大会主要演讲:新杭州故事,平头哥与新制造