Android “退一步”的布局加载优化

组件化技巧——跨进程共享View 前言 在字节学到一招。 刚来字节,发现字节内部的技术真的很牛逼,一后会不定期更新博客,记录一下自己在头条这边的学习历程。 跨模块View通信 我们都知道,当某块A想使用模块B里面的View时,有两种方式 抽象出接口,通过依赖接口IView和管理类ServerManager,动态添加view,缺点是只能动态添加,不能在xml里面直接使用,也无法预览。(注意,这里的IView和ServerManager需要统一在common模块) 把模块B里面的View直接下沉到更底层去。 前面这两个是最通用的方法,一般第二 阅读详情

注意里面有个merger.xml,其中就包含了整个项目所有资源合并后的内容。

我们打开看一眼:

重点关注里面的type=layout的相关标签。

<file name=“activity_main1”

path=“/Users/zhanghongyang/work/TestViewOpt/app/src/main/res/layout/activity_main1.xml”

qualifiers=“” type=“layout” />

可以看到包含了我们layout文件的路劲,那么我们只要解析这个merger.xml,然后找到里面所有type=layout的标签,再解析出layout文件的实际路劲,再解析对应的layout xml就能拿到控件名了。

对了,这个任务要注入到mergeDebugResources后面执行。

怎么注入一个任务呢?

非常简单:

project.afterEvaluate {

def mergeDebugResourcesTask = project.tasks.findByName(“mergeDebugResources”)

if (mergeDebugResourcesTask != null) {

def resParseDebugTask = project.tasks.create(“ResParseDebugTask”, ResParseTask.class)

resParseDebugTask.isDebug = true

mergeDebugResourcesTask.finalizedBy(resParseDebugTask);

}

}

根目录:view_opt.gradle

我们首先找到mergeDebugResources这个task,再其之后,注入一个ResParseTask的任务。

然后在ResParseTask中完成文件解析:

class ResParseTask extends DefaultTask {

File viewNameListFile

boolean isDebug

HashSet viewSet = new HashSet<>()

// 自己根据输出几个添加

List ignoreViewNameList = Arrays.asList(“include”, “fragment”, “merge”, “view”,“DateTimeView”)

@TaskAction

void doTask() {

File distDir = new File(project.buildDir, “tmp_custom_views”)

if (!distDir.exists()) {

distDir.mkdirs()

}

viewNameListFile = new File(distDir, “custom_view_final.txt”)

if (viewNameListFile.exists()) {

viewNameListFile.delete()

}

viewNameListFile.createNewFile()

viewSet.clear()

viewSet.addAll(ignoreViewNameList)

try {

File resMergeFile = new File(project.buildDir, “/intermediates/incremental/merge” + (isDebug ? “Debug” : “Release”) + “Resources/merger.xml”)

println(“resMergeFile: ${resMergeFile.getAbsolutePath()} === ${resMergeFile.exists()}”)

if (!resMergeFile.exists()) {

return

}

XmlSlurper slurper = new XmlSlurper()

GPathResult result = slurper.parse(resMergeFile)

if (result.children() != null) {

result.childNodes().forEachRemaining({ o ->

if (o instanceof Node) {

parseNode(o)

}

})

}

} catch (Throwable e) {

e.printStackTrace()

}

}

void parseNode(Node node) {

if (node == null) {

return

}

if (node.name() == “file” && node.attributes.get(“type”) == “layout”) {

String layoutPath = node.attributes.get(“path”)

try {

XmlSlurper slurper = new XmlSlurper()

GPathResult result = slurper.parse(layoutPath)

String viewName = result.name();

if (viewSet.add(viewName)) {

viewNameListFile.append(“${viewName}\n”)

}

if (result.children() != null) {

result.childNodes().forEachRemaining({ o ->

if (o instanceof Node) {

parseLayoutNode(o)

}

})

}

} catch (Throwable e) {

e.printStackTrace();

}

} else {

node.childNodes().forEachRemaining({ o ->

if (o instanceof Node) {

parseNode(o)

}

})

}

}

void parseLayoutNode(Node node) {

if (node == null) {

return

}

String viewName = node.name()

if (viewSet.add(viewName)) {

viewNameListFile.append(“${viewName}\n”)

}

if (node.childNodes().size() <= 0) {

return

}

node.childNodes().forEachRemaining({ o ->

if (o instanceof Node) {

parseLayoutNode(o)

}

})

}

}

根目录:view_opt.gradle

代码很简单,主要就是解析merger.xml,找到所有的layout文件,然后解析xml,最后输出到build目录中。

代码我们都写在view_opt.gradle,位于项目的根目录,在app的build.gradle中apply即可:

apply from: rootProject.file(‘view_opt.gradle’)

然后我们再次运行assembleDebug,输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7q8ZR9TX-1651210056760)(https://wanandroid.com/blogimgs/7e1d8fe2-78b1-4fe8-b475-1339b9e9553f)]

注意,上面我们还有个ignoreViewNameList对象,我们过滤了一些特殊标签,例如:“include”, “fragment”, “merge”, “view”,你可以根据输出结果自行添加。

输出结果为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7pcVSbnN-1651210056762)(https://wanandroid.com/blogimgs/f02ed649-05c3-4483-921f-5f745d1b7ed6)]

可以看到是去重后的View的名称。

这里提一下,有很多同学看到写gradle脚本就感觉恐惧,其实很简单,你就当写Java就行了,不熟悉的语法就用Java写就好了,没什么特殊的。

到这里我们就有了所有使用到的View的名称。

[](()2. apt 生成代理类

有了所有用到的View的名称,接下来我们利用apt生成一个代理类,以及代理方法。

要用到apt,那么我们需要新建3个模块:

  1. ViewOptAnnotation: 存放注解;

  2. ViewOptProcessor:放注解处理器相关代码;

  3. ViewOptApi:放相关使用API的。

关于Apt的相关基础知识就不提了哈,这块知识太杂了,大家自己查阅下,后面我把demo传到github大家自己看。

我们就直接看我们最核心的Processor类了:

@AutoService(Processor.class)

public class ViewCreatorProcessor extends AbstractProcessor {

private Messager mMessager;

@Override

public synchronized void init(ProcessingEnvironment processingEnvironment) {

super.init(processingEnvironment);

mMessager = processingEnv.getMessager();

}

@Override

public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

Set<? extends Element> classElements = roundEnvironment.getElementsAnnotatedWith(ViewOptHost.class);

for (Element element : classElements) {

TypeElement classElement = (TypeElement) element;

ViewCreatorClassGenerator viewCreatorClassGenerator = new ViewCreatorClassGenerator(processingEnv, classElement, mMessager);

viewCreatorClassGenerator.getJavaClassFile();

break;

}

return true;

}

@Override

public Set getSupportedAnnotationTypes() {

Set types = new LinkedHashSet<>();

types.add(ViewOptHost.class.getCanonicalName());

return types;

}

}

核心方法就是process了,直接交给了ViewCreatorClassGenerator去生成我们的Java类了。

看之前我们思考下我们的逻辑,其实我们这个代理类非常简单,我们只要构建好我们的类名,方法名,方法内部,根据View名称的列表去写swicth就可以了。

看代码:

定义类名:

public ViewCreatorClassGenerator(ProcessingEnvironment processingEnv, TypeElement classElement, Messager messager) {

mProcessingEnv = processingEnv;

mMessager = messager;

mTypeElement = classElement;

PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(classElement);

String packageName = packageElement.getQualifiedName().toString();

//classname

String className = ClassValidator.getClassName(classElement, packageName);

mPackageName = packageName;

mClassName = className + “__ViewCreator__Proxy”;

}

我们类名就是使用注解的类名后拼接__ViewCreator__Proxy。

生成类主体结构:

public void getJavaClassFile() {

Writer writer = null;

try {

JavaFileObject jfo = mProcessingEnv.getFiler().createSourceFile(

mClassName,

mTypeElement);

String classPath = jfo.toUri().getPath();

String buildDirStr = “/app/build/”;

String buildDirFullPath = classPath.substring(0, classPath.indexOf(buildDirStr) + buildDirStr.length());

File customViewFile = new File(buildDirFullPath + “tmp_custom_views/custom_view_final.txt”);

HashSet customViewClassNameSet = new HashSet<>();

putClassListData(customViewClassNameSet, customViewFile);

String generateClassInfoStr = generateClassInfoStr(customViewClassNameSet);

writer = jfo.openWriter();

writer.write(generateClassInfoStr);

writer.flush();

mMessager.printMessage(Diagnostic.Kind.NOTE, "generate file path : " + classPath);

} catch (Exception e) {

e.printStackTrace();

} finally {

if (writer != null) {

try {

writer.close();

} catch (IOException e) {

// ignore

}

}

}

}

这里首先我们读取了,我们刚才生成的tmp_custom_views/custom_view_final.txt,存放到了一个hashSet中。

然后交给了generateClassInfoStr方法:

private String generateClassInfoStr(HashSet customViewClassNameSet) {

StringBuilder builder = new StringBuilder();

builder.append(“// Generated code. Do not modify!\n”);

builder.append(“package “).append(mPackageName).append(”;\n\n”);

builder.append(“import com.zhy.demo.viewopt.*;\n”);

builder.append(“import android.content.Context;\n”);

builder.append(“import android.util.AttributeSet;\n”);

builder.append(“import android.view.View;\n”);

builder.append(‘\n’);

builder.append("public class “).append(mClassName).append(” implements " + sProxyInterfaceName);

builder.append(" {\n");

generateMethodStr(builder, customViewClassNameSet);

builder.append(‘\n’);

builder.append(“}\n”);

return builder.toString();

}

可以看到这里其实就是拼接了类的主体结构。

详细的方法生成逻辑:

private void generateMethodStr(StringBuilder builder, HashSet customViewClassNameSet) {

builder.append("@Override\n ");

builder.append(“public View createView(String name, Context context, AttributeSet attrs ) {\n”);

builder.append(“switch(name)”);

builder.append(“{\n”); // switch start

for (String className : customViewClassNameSet) {

if (className == null || className.trim().length() == 0) {

continue;

}

builder.append(“case “” + className + “” :\n”);

builder.append("return new " + className + “(context,attrs);\n”);

}

builder.append(“}\n”); //switch end

builder.append(“return null;\n”);

builder.append(" }\n"); // method end

}

一个for循环就搞定了。

我们现在运行下:

会在项目的如下目录生成代理类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DHv8EEVi-1651210056762)(https://wanandroid.com/blogimgs/3f395393-9407-4fa7-a253-dbe081cec0fc)]

类内容:

// Generated code. Do not modify!

package com.zhy.demo.viewopt;

import com.zhy.demo.viewopt.*;

import android.content.Context;

import android.util.AttributeSet;

import android.view.View;

public class ViewOpt__ViewCreator__Proxy implements IViewCreator {

@Override

public View createView(String name, Context context, AttributeSet attrs) {

switch (name) {

case “androidx.appcompat.widget.FitWindowsLinearLayout”:

return new androidx.appcompat.widget.FitWindowsLinearLayout(context, attrs);

case “androidx.appcompat.widget.AlertDialogLayout”:

return new androidx.appcompat.widget.AlertDialogLayout(context, attrs);

case “androidx.core.widget.NestedScrollView”:

return new androidx.core.widget.NestedScrollView(context, attrs);

case “android.widget.Space”:

return new android.widget.Space(context, attrs);

case “androidx.appcompat.widget.DialogTitle”:

return new androidx.appcompat.widget.DialogTitle(context, attrs);

case “androidx.appcompat.widget.ButtonBarLayout”:

return new androidx.appcompat.widget.ButtonBarLayout(context, attrs);

case “androidx.appcompat.widget.ActionMenuView”:

return new androidx.appcompat.widget.ActionMenuView(context, attrs);

case “androidx.appcompat.view.menu.ExpandedMenuView”:

return new androidx.appcompat.view.menu.ExpandedMenuView(context, attrs);

case “Button”:

return new Button(context, attrs);

case “androidx.appcompat.widget.ActionBarContainer”:

return new androidx.appcompat.widget.ActionBarContainer(context, attrs);

case “TextView”:

return new TextView(context, attrs);

case “ImageView”:

return new ImageView(context, attrs);

case “Space”:

return new Space(context, attrs);

case “androidx.appcompat.widget.FitWindowsFrameLayout”:

return new androidx.appcompat.widget.FitWindowsFrameLayout(context, attrs);

case “androidx.appcompat.widget.ContentFrameLayout”:

return new androidx.appcompat.widget.ContentFrameLayout(context, attrs);

case “CheckedTextView”:

return new CheckedTextView(context, attrs);

case “DateTimeView”:

return new DateTimeView(context, attrs);

case “androidx.appcompat.widget.ActionBarOverlayLayout”:

return new androidx.appcompat.widget.ActionBarOverlayLayout(context, attrs);

case “androidx.appcompat.view.menu.ListMenuItemView”:

return new androidx.appcompat.view.menu.ListMenuItemView(context, attrs);

case “androidx.appcompat.widget.ViewStubCompat”:

return new androidx.appcompat.widget.ViewStubCompat(context, attrs);

case “RadioButton”:

return new RadioButton(context, attrs);

case “com.example.testviewopt.view.MyMainView4”:

return new com.example.testviewopt.view.MyMainView4(context, attrs);

case “com.example.testviewopt.view.MyMainView3”:

return new com.example.testviewopt.view.MyMainView3(context, attrs);

case “View”:

return new View(context, attrs);

case “com.example.testviewopt.view.MyMainView2”:

return new com.example.testviewopt.view.MyMainView2(context, attrs);

case “androidx.appcompat.widget.ActionBarContextView”:

return new androidx.appcompat.widget.ActionBarContextView(context, attrs);

case “com.example.testviewopt.view.MyMainView1”:

return new com.example.testviewopt.view.MyMainView1(context, attrs);

case “ViewStub”:

return new ViewStub(context, attrs);

case “ScrollView”:

return new ScrollView(context, attrs);

case “Chronometer”:

return new Chronometer(context, attrs);

case “androidx.constraintlayout.widget.ConstraintLayout”:

return new androidx.constraintlayout.widget.ConstraintLayout(context, attrs);

case “CheckBox”:

return new CheckBox(context, attrs);

case “androidx.appcompat.view.menu.ActionMenuItemView”:

return new androidx.appcompat.view.menu.ActionMenuItemView(context, attrs);

case “FrameLayout”:

return new FrameLayout(context, attrs);

case “RelativeLayout”:

return new RelativeLayout(context, attrs);

case “androidx.appcompat.widget.Toolbar”:

return new androidx.appcompat.widget.Toolbar(context, attrs);

case “LinearLayout”:

return new LinearLayout(context, attrs);

}

return null;

}

}

看起来很完美…

不过目前是报错状态,报什么错呢?

错误: 找不到符号

return new Button(context,attrs);

^

符号: 类 Button

位置: 类 ViewOpt__ViewCreator__Proxy

我们注意到这些系统控件没有导包。

比如Button,应该是:android.widget.Button。

那么我们可以选择

import android.widget.*

不过有个问题,你会发现,android的View并不是都在android.widget下,例如View在android.view下,WebView在android.webkit下面。

所以我们要把这三个包都导入。

这个时候,你会不会有疑问,系统也只能通过xml拿到TextView,他咋知道是android.widget.LinearLayout还是android.view.LinearLayout?

难不成一个个尝试反射?

是的,你没猜错,LayoutInflater运行时的对象为:PhoneLayoutInflater,你看源码就知道了:

public class PhoneLayoutInflater extends LayoutInflater {

private static final String[] sClassPrefixList = {

“android.widget.”,

“android.webkit.”,

“android.app.”

};

public PhoneLayoutInflater(Context context) {

super(context);

}

protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {

super(original, newContext);

}

@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {

for (String prefix : sClassPrefixList) {

try {

View view = createView(name, prefix, attrs);

if (view != null) {

return view;

}

} catch (ClassNotFoundException e) {

// In this case we want to let the base class take a crack

// at it.

}

}

return super.onCreateView(name, attrs);

}

public LayoutInflater cloneInContext(Context newContext) {

return new PhoneLayoutInflater(this, newContext);

}

}

循环拼接前缀遍历…

不过怎么没看到android.view.这个前缀,嗯,在super.onCreateView里面:

#LayoutInflater

protected View onCreateView(String name, AttributeSet attrs)
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》无偿开源 徽信搜索公众号【编程进阶路】
throws ClassNotFoundException {

return createView(name, “android.view.”, attrs);

}

ok,这个时候,你可能还会遇到一些系统hide View找不到的情况,主要是因为你本地的android.jar里面没有那些hide View对应的class,所以编译不过,这种极少数,你可以选择在刚才过滤的List里面添加一下。

好了,到这里我们的代理类:

ViewOpt__ViewCreator__Proxy

生成了。

[](()3. 编写生成View的代码


@ViewOptHost

public class ViewOpt {

private static volatile IViewCreator sIViewCreator;

static {

try {

String ifsName = ViewOpt.class.getName();

String proxyClassName = String.format(“%s__ViewCreator__Proxy”, ifsName);

Class proxyClass = Class.forName(proxyClassName);

Object proxyInstance = proxyClass.newInstance();

if (proxyInstance instanceof IViewCreator) {

sIViewCreator = (IViewCreator) proxyInstance;

}

} catch (Throwable e) {

e.printStackTrace();

}

}

public static View createView(String name, Context context, AttributeSet attrs) {

try {

if (sIViewCreator != null) {

View view = sIViewCreator.createView(name, context, attrs);

if (view != null) {

Log.d(“lmj”, name + " 拦截生成");

}

return view;

}

} catch (Throwable ex) {

ex.printStackTrace();

}

return null;

}

}

其实就是反射我们刚才的生成的代理类对象,拿到它的实例。

然后强转为IViewCreator对象,这样我们后续直接 sIViewCreator.createView 调用就可以了。

这里大家有没有看到一个知识点:

就是为什么apt生成的代理类,总会让它去继承某个类或者实现每个接口?

这样在后续调用代码的时候就不需要反射了。

有了生成View的逻辑,然后注入到mPrivaryFactory就可以了,其实就是我们的Activity,找到你项目中的BaseActivity:

public class BaseActivity extends AppCompatActivity {

@Override

public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {

View view = ViewOpt.createView(name, context, attrs);

if (view != null) {

return view;

}

return super.onCreateView(parent, name, context, attrs);

}

}

流程结束。

运行下,可以看下log:

2020-05-31 18:07:26.300 31454-31454/? D/lmj: LinearLayout 拦截生成

2020-05-31 18:07:26.300 31454-31454/? D/lmj: ViewStub 拦截生成

2020-05-31 18:07:26.300 31454-31454/? D/lmj: FrameLayout 拦截生成

2020-05-31 18:07:26.305 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarOverlayLayout 拦截生成

2020-05-31 18:07:26.306 31454-31454/? D/lmj: androidx.appcompat.widget.ContentFrameLayout 拦截生成

2020-05-31 18:07:26.311 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarContainer 拦截生成

2020-05-31 18:07:26.318 31454-31454/? D/lmj: androidx.appcompat.widget.Toolbar 拦截生成

2020-05-31 18:07:26.321 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarContextView 拦截生成

2020-05-31 18:07:26.347 31454-31454/? D/lmj: androidx.constraintlayout.widget.ConstraintLayout 拦截生成

对应的布局:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=“http://schemas.android.com/apk/res/android”

xmlns:app=“http://schemas.android.com/apk/res-auto”

xmlns:tools=“http://schemas.android.com/tools”

android:layout_width=“match_parent”

android:layout_height=“match_parent”

tools:context=“.MainActivity”>

eView(name, context, attrs);

if (view != null) {

return view;

}

return super.onCreateView(parent, name, context, attrs);

}

}

流程结束。

运行下,可以看下log:

2020-05-31 18:07:26.300 31454-31454/? D/lmj: LinearLayout 拦截生成

2020-05-31 18:07:26.300 31454-31454/? D/lmj: ViewStub 拦截生成

2020-05-31 18:07:26.300 31454-31454/? D/lmj: FrameLayout 拦截生成

2020-05-31 18:07:26.305 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarOverlayLayout 拦截生成

2020-05-31 18:07:26.306 31454-31454/? D/lmj: androidx.appcompat.widget.ContentFrameLayout 拦截生成

2020-05-31 18:07:26.311 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarContainer 拦截生成

2020-05-31 18:07:26.318 31454-31454/? D/lmj: androidx.appcompat.widget.Toolbar 拦截生成

2020-05-31 18:07:26.321 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarContextView 拦截生成

2020-05-31 18:07:26.347 31454-31454/? D/lmj: androidx.constraintlayout.widget.ConstraintLayout 拦截生成

对应的布局:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=“http://schemas.android.com/apk/res/android”

xmlns:app=“http://schemas.android.com/apk/res-auto”

xmlns:tools=“http://schemas.android.com/tools”

android:layout_width=“match_parent”

android:layout_height=“match_parent”

tools:context=“.MainActivity”>

APK反编译常见错误解析与实战解决方案 在Android应用逆向工程中,反编译是理解应用内部逻辑的关键步骤。其核心原理是通过工具链将APK文件中的二进制资源与代码还原为可读格式,涉及资源解码和字节码转换两个主要环节。这一技术的价值在于支持安全审计、漏洞挖掘、遗留代码恢复以及学习优秀应用架构设计。然而,在实际操作中,开发者常会遇到资源文件解码失败、代码转换异常等各类错误,这些错误往往与APK加固、代码混淆、工具版本兼容性等因素相关。针对这些高频问题,本文系统梳理了以Apktool和dex2jar为核心的工具链在实战中的排错技巧,例如通过调整参数绕过 阅读详情

相关推荐

IEEE Access投稿全流程解析:从初次提交到最终接收的完整指南

本文详细解析了IEEE Access期刊从初次投稿到最终接收的全流程,包括ORCID注册、论文投稿七步法、回稿再投流程及最终版提交等关键步骤。特别强调了4-6周快速同行评审的特点,为研究者提供了一份实用的投稿指南,帮助高效完成IEEE Access期刊投稿。

weixin_29273849的博客 914

Android Gradle学习(七) Gradle构建生命周期

Gradle 进行构建,都要经过3个生命周期阶段: 初始化阶段 配置阶段 执行阶段 Gradle 提供了很多生命周期监听方法,可以在各个阶段 Hook 指定的任务。 1. Project Project 提供的生命周期回调方法有: //在 Project 进行配置前调用 void beforeEvaluate(Closure closure) //在 Project 配置结束后调用 void ...

假装你是大灰狼的专栏 1793

Unity中的坐标系及其转换方法

Unity中的坐标系及其转换方法 Unity中有四种坐标系: 1.全局坐标系 World Coordinate System 全局坐标系是用于描述场景内所有物体位置的方向的基准,也称为世界坐标系,Unity中创建的物体都是以全局坐标系中的坐标原点(0,0,0),来确定各自的位置的,可以使用transform.position来获取游戏对象的世界坐标。 2.局部坐标系 Local Coordinate System 局部坐标系也称为模型坐标系或物体坐标系,每个物体都有自身独立的物体坐标系。当物体移动或改变方向

Z_hongli的博客 8178

gradle系列:理解Project.afterEvaluate

备注:如果注册了多个project.afterEvaluate回调,那么执行顺序等同于注册顺序。在上面的例子中,由于buildSrc中的回调注册较早,所以它也先执行。可以看到afterEvaluate是Project的一个函数。看看gradle console里面会打印什么?这里用的Android studio版本是。如下图,项目中使用的是。继续看看官网怎么说?...

知道这个有什么用? 7837

ESP8266连接阿里云(STM32)

一、概述 之前通过串口调试助手+ESP8266已经连接上阿里云了 ESP8266连接阿里云(串口调试助手) 这次我们使用STM32+ESP8266来接入阿里云,其实本质上就是STM32和ESP8266进行串口通信,相当于使用STM32代替之前使用的串口调试助手来发送AT指令。只是发送的字符串数据可能有些细节需要注意一下。 二、保存连接的AT指令 定义几个字符串数据保存需要发送的AT指令 //注意: //末尾记得加\r\n //字符...

xyx0610的博客 4万+

ASPICE1-配置管理

SUP.8配置管理 BP1:开发配置管理策略 BP2:识别配置项 BP3:建立配置项管理系统 BP4:建立分支管理 BP5:控制变更和发布 BP6: 建立基线 BP7:报告配置项状态 BP8:验证配置项信息 BP9:管理配置项和基线的存储 1.配置管理概念 配置管理(Configuration Management,CM)是通过使用技术或行政手段对软件产品进行控制,以实现对软件开发过程和生命周期进行控制、规范的一系列措施。 完整性、一致性、可控性。 2.配置管理的主要活动: 1.

HDZQ2005的专栏 3686

Android Gradle生命周期与Gradle 自定义插件

文章目录1.Gradle生命周期2.生命周期监听方法3.Gradle Project4.Gradle Task5.自定义gradle插件的流程5.1.编写一个插件类,实现Plugin5.2编写task任务5.3.配置properties文件5.4配置插件的build.gradle5.5使用自定义的插件6.0插件的debug调试配置6.1通过Edit Configurations 点击左上角的+按钮,创建一个remote,随便取个名字6.2找到我们创建的task,鼠标右击弹出菜单,点击:create Run

风的轨迹 994

基于 FastAPI + LangGraph + LLM 大语言模型的通用 Agent 多智能体系统架构设计与开发实战、产业应用

FastAPI是一个现代、快速(高性能)的Python Web框架,专门用于构建API。它由Sebastián Ramírez(@tiangolo)创建,基于Starlette(用于Web部分)和Pydantic(用于数据部分)构建。性能优先:FastAPI是当前最快的Python Web框架之一,性能与NodeJS和Go相当。这得益于其基于Starlette的异步架构,能够充分利用Python 3.6+的async/await特性。开发效率。

AI天才研究院 229

【Android Gradle 插件】Gradle 依赖管理 ② ( build.gradle 中的 dependencies 依赖配置 | DependencyHandler#add 方法介绍 )

一、build.gradle 中的 dependencies 依赖配置、 二、DependencyHandler#add 方法介绍

让 学习 成为一种 习惯 ( 韩曙亮 の 技术博客 ) 3105

S7-200SMART 的运动控制(一)向导配置具体步骤(图文)

S7-200SMART 的运动控制(一)向导配置具体步骤(图文)

Robot_PLC_HMI_Automation 3849

【DaVinci Developer工具实战】03 -导入xml文件

本文档描述了 DaVinci Developer 导入数据的的具体功能 ,它适用于导入AUTOSAR SWC 模板【AUTOSAR XML 文件】 文件。

1990

Android依赖分析自动化工具实战

一个Android工程常常会有很多依赖,可能是组件化项目对于其他业务及中间件的依赖,也有可能是对于第三方sdk的依赖。这里我们分别看下对于这两种场景为什么需要自动化的版本依赖分析工具:

star_nwe的博客 1292

使用 MediaPipe 身体跟踪构建不良身体姿势检测和警报系统

详细介绍如何使用 MediaPipe 身体跟踪构建不良身体姿势检测和警报系统,并附实现的源码下载链接

DT程序员的博客 1813

Android “退一步”的布局加载优化,2024金九银十

题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等学习资料。【Android思维脑图(技能树)】

2401_84152109的博客 1060

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

鸿洋大神新作:“手把手”的性能优化文章来了

作者:鸿洋 原文链接:https://mp.weixin.qq.com/s/ceXsH06fUFa7y4lzi4uXzw 概述 在Android开发过程中,我们基本每天都在写各种各样的xml布局文件,然后app会在运行时,将我们的布局文件转化成View显示在界面上。 这个转化,主要就是解析xml布局文件,然后根据xml的中每个View标签,将: 标签名-> View的名称 各种属性 -> AttributeSet对象 然后反射调用View两个参数的构造方法。 这也是为什么,我们在自定义控.

Android725的博客 1200

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

面试大厂,你需要掌握这些面试技巧

前言 面试大厂,那就是考察你技术方向的一些专业技能,如果你面试Android岗位,那 Android 方向的专业技能就是考察很重要的一个方向。 但是话说回来,即便这条是所有人都知道的共识,也不是所有人都能搞得定。有些人就是觉得自己基础不好,然后就知难而退,要么就是没有决心啃下这块硬骨头,这时候就体现出每个人面对困难的方式了。 再退一步讲,即便你不想加入大厂,在IT 领域,如果你想吃技术这碗饭,长久来看,专业技能也会制约一个人的上限,等到工作时间越长,年龄越来越大,专业技能的熟练程度就会让你和别的程序员之间差

1 3843

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

提高APP冷启动速度小结

提高APP冷启动速度小结欢迎使用Markdown编辑器一 / 去掉白屏背景二 / 及时结束`onCreat()`方法三 / 假加速,‘但是肉眼可见的快了’四 / MultiDex在5.0之下的问题 欢迎使用Markdown编辑器 在系统完全结束APP的服务之后,冷启动APP仿佛像是生孩子一般难受,尤其是之前版本的支付宝。不过还是要点名表扬一下支付宝,在后续版本尤其是最新的10.0以及之后版本,大力优化了冷启动速度,虽然整个APP还是很重,主页内容还是很多,但是不得不说启动速度的优化还是很明显的。 下面是我工

一个开发 2748

手把手刷小米 TWRP:从 Bootloader 解锁到备份回滚的完整流程

这篇文章按一次真实刷机操作的时间线,带你走完小米 / Redmi 设备刷入第三方 Recovery(TWRP / OrangeFox)的全过程:先做环境体检,再解锁 Bootloader、写入 Recovery、做全量备份,然后给出回滚救砖方案和进阶 Recovery 选型,最后花一小节讲清 Recovery 的底层机制。适合"会一点命令、但怕翻车"的折腾党。 ## 动手机之前,先做三项环境体检

gitblog_00755的博客 745

Qwen生态工程落地:本地部署、量化调优与检索链路实战

本地部署大模型时,显存占用与量化格式是绕不开的工程门槛。同一模型在不同精度、上下文长度和并发条件下,资源消耗可能相差数倍,GGUF与FP8量化在节省显存的同时,也带来了精度损失与输出噪点等新问题。围绕Qwen生态,开发者不仅需要对话模型,还涉及ASR语音识别、图像编辑、代码助手与Embedding向量化等多类工具。从qwen asr的显存排查,到用qwen embedding构建Milvus向量库并通过LangChain4j接入Java服务,完整链路的关键在于分层排查与维度对齐。本文从资源边界、量化调优、工

weixin_30314813的博客 409
上一篇: Android —— 自定义View中
下一篇: Android 广播机制 详解
Java364102
博客等级 码龄5年 12粉丝 58原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值