android编译插桩

背景:这一次分享一下关于android编译插桩这个话题,在正常编写代码实现程序的逻辑外,还要使用一点点黑科技,拿起操作代码无所不能的武器。


Android常用的能动态改变代码逻辑的方法有两种

  1. Java hook(反射,动态代理)
    android是基于java,同时也就有了反射的概念,动态去加载代码,同时动态代理也可以实现,但比较难理解,网络上讲得也很多了,这里就不讲。

  2. 插桩(编译插桩,插件化插桩)
    如果是按照插桩时机,在编译时候的就是编译插桩,然而,有些时候需要在程序运行的时候动态加载一些东西。我理解的差异性是插桩的代码是跟原来程序是分开的,在一个特定的时机与原有程序合二为一。

从xposed框架到编译插桩

记得去年曾经分享过xposed框架的使用,那是在系统层面去hook住方法,不足之处是系统要先刷入框架包,优点是可以对该系统里面所有的app进行hook

是否可以对我们自己的应用进行代码的修改?比如上面提到的有java hook,还有编译插桩:下面是比较流行的框架

  1. aspectJ (面向切面框架) 跟普通代码一样,理解容易
  2. Asm (操作字节码框架)需要使用asm字节码,相对复杂

而插桩的应用场景:apm,无埋点

比较一下aspectJ 与asm分别插桩后的代码

aspectJ

asm

通过上面两个图可以看出插桩后的代码还是有所区别,aspectJ采用插入方法的方式,这种代码混淆的时候一定要注意,不然会出现找不到方法名。而asm采用直接把差异代码嵌入原有的方法里,显然执行起来更加高效。

asm实现的插桩

原代码

字节码

asm代码

可以看出代码相对来说是比较复杂的,可能需要工具才能正确去编写asm代码,比如idea插件bytecode outline

asm在编译打包的哪个步骤插桩呢

如图所示:在编译成.class文件后,执行asm步骤,对class文件进行处理。而后就把各种编译后的文件打包进dex文件的过程。

demo-采用asm为某个方法插入代码

一个简单的demo,对一个方法进行asm插桩

https://github.com/ydpzg/TestPlugin


附录

自定义gradle plugin

https://guides.gradle.org/writing-gradle-plugins/

https://docs.gradle.org/current/userguide/custom_plugins.html

Asm 字节码插件:

Bytecode Outline```
1
2
3

java字节码:
```javac Apple.java javap -verbose Apple.class

#
vue里面的component css冲突问题

添加一个scoped

1
<style lang="less" scoped="Terms">

关于一个对话框component在vue中的的实现

component的文件形态
xxx 文件夹
index.js (封装了createDialog方法,操作了dom树)
index.vue

1、用法:
import xxx from ‘xxx/index.js’
let dialog = xxx({ YYY: yyy})

场景:
不需要view层的公共类,只需要script层

注意:
控制对话框显示与隐藏的字段是visible,通过值为true or false来代表对话框的显示与隐藏,因为要操作dom树,dom元素的显示隐藏就是visible

2、用法:
import xxx from ‘xxx/index.vue’

场景:
普通单页业务,既有view层,又有script层

注意:
控制对话框显示与隐藏的字段是value,通过值为true or false来代表对话框的显示与隐藏,因为使用了v-model

vue的动画应用

动画可以使用css动画,也可以用钩子加载js动画

css动画有个缺点,如果某些动画效果的值需要动态去改变,那只能使用js动画

如果是多个dom都要连着动画怎么办呢
比如需求是一个进度条+小火箭,进度条需要动态的属性是width,小火箭需要动态的属性是margin-left

那么可以使用transition标签,也可以使用transition-group标签

使用transition标签,思路是取其中一个dom作为基准,执行js动画,其他dom在基准dom的时间线回调里做属性值的变化,当然,相对于使用transition-group标签,同步的时候那些边界值可能要维护得更多。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<div ref="progressRed" class="head-panel-progress-red" v-if="show" :style="{'width': `${marginLeftRocket}`}"  ></div>
<transition v-on:enter="enter"
v-on:before-enter="beforeEnter"
v-on:after-enter="afterEnter"
v-on:before-leave="beforeLeave"
:css="false"
name="animate"
mode="in-out">
<img class="head-panel-progress-rocket" src="https://qiniu-koala.kaolalicai.cn/app/cash_flow_record/img_progress_rocket.png" v-if="show" :class="flagClassRocket">
</transition>


beforeEnter(el) {
el.style.marginLeft = '0%'
},
enter(el, done) {
Velocity(el, {
marginLeft: `${this.beatPercent}%`
}, {
duration: 1000,
progress: () => {
this.marginLeftRocket = el.style.marginLeft
},
complete: () => {
done()
}
})
},
afterEnter(el) {
this.marginLeftRocket = el.style.marginLeft
},
beforeLeave() {
this.marginLeftRocket = '0%'
},

使用transition-group标签,比较简单,n个dom同时执行,动态改变属性值,由系统去维护时间线,完美,可能也有缺点,group的子dom需要统一的标签(这点没验证)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<transition-group v-on:enter="enter"
v-on:before-enter="beforeEnter"
v-on:after-enter="afterEnter"
v-on:before-leave="beforeLeave"
:css="false"
tag="div"
name="animate"
mode="in-out">
<div :key="'progressRed'"
:data-name="'red'"
class="head-panel-progress-red"
v-if="show" ></div>
<div :key="'progressRocket'"
:data-name="'rocket'"
class="head-panel-progress-rocket"
v-if="show"></div>
</transition-group>

enter(el, done) {
let properties = el.dataset.name === 'rocket' ? { marginLeft: `${this.beatPercent}%` } : { width: `${this.beatPercent}%` }
Velocity(el, properties, {
duration: 1000,
complete: () => {
done()
}
})
},

解决weex或者vue在dev使用ip和localhost

尝试weex
在dev构建的时候的时候默认是使用ip进行访问,,

查到在webpack.dev.conf.js的一句代码

1
const ip = require('ip').address();

修改成 ip = ‘127.0.0.1’ 后发现dev的时候可以使用localhost访问了,但ip却不行了

后来网上找了解决方案,发现在配置server的时候,需要配个host,即0.0.0.0

1
devWebpackConfig.devServer.host = '0.0.0.0'

自定义plugin给方法统计时间(插桩)

####1、创建新project

创建一个新的project,默认创建就行,名字叫testPlugin

先大致看一下工程目录如下:

####2、使用buildSrc的方式创建自定义plugin

为什么使用buildSrc,因为理解简单,也可以使用其他方法,比如上传到本地repo
官网说明了如何自定义plugin
https://guides.gradle.org/writing-gradle-plugins/
https://docs.gradle.org/current/userguide/custom_plugins.html

没看懂的看同行的解释,自定义Android Gradle插件的3种方式
https://blog.csdn.net/brycegao321/article/details/82754014

另外:自定义plugin可以groovy,java,kotlin语言编写
需要在build.gradle引入
app plugin: ‘java’
app plugin: ‘groovy’
app plugin: ‘kotlin’

试了一下创建一个androidlib,然后在main后面创建一个groovy文件夹,再写一个build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
*****build.gradle*****
buildscript {
ext.android_gradle_plugin_version = '3.1.3'
repositories {
jcenter()
google()
}
dependencies {
}
}

repositories {
jcenter()
google()
}

apply plugin: 'java'
apply plugin: 'groovy'

dependencies {
implementation gradleApi()
implementation localGroovy()
implementation "com.android.tools.build:gradle:$android_gradle_plugin_version"
implementation "com.android.tools.build:gradle-api:$android_gradle_plugin_version"
}

但是结束报错了,一堆红色的log

1
2
3
4
5
6
7
8
9
10
11
block1
buildSrc会出现Plugin with id 'android-library' not found
需要 apply plugin: 'groovy'

block2
buildSrc也出现Duplicate root element
直接创建文件夹,不要创建android library

tip: id写法
buildSrc/src/main/resources/META-INF/gradle-plugins/org.example.greeting.properties
放这个映射文件后就可以用id的写法

正确的创建buildSrc方法

1
2
3
4
5
6
7
1、直接创建buildSrc文件夹,
2、创建src文件夹,
3、创建main文件夹,
4、创建groovy文件夹,在这里面写自定义plugin
5、创建resources文件夹,接着META-INF,接着gradle-plugins,
在文件夹gradle-plugins里面新建一个com.kalengo.customplugin.properties,里面写名称implementation-class=CustomPlugin
这两个名称在app的build.gradle引用plugin的时候是两种写法,一种是包名,一种是id

buildSrc的目录参考

####自定义plugin
自定义plugin需要implements Plugin
覆盖void apply(Project project) 方法

1
2
3
4
5
6
7
8
9
10
可以在方法里面做添加task的操作
//./gradlew -q hello
//测试build.gradle的参数获取
def extension = project.extensions.create('greeting', GreetingPluginExtension)
project.task('hello') {
doLast {
println extension.message
println extension.greeter
}
}
1
2
3
4
也可以定义transform对class进行特殊处理
//自定义transform进行处理
def android = project.extensions.getByType(AppExtension)
android.registerTransform(new CustomTransform(project))

####自定义transform

1
2
3
4
首先需要在自定义plugin的apply方法里面注册transform
//自定义transform进行处理
def android = project.extensions.getByType(AppExtension)
android.registerTransform(new CustomTransform(project))
1
2
3
4
5
6
7
自定义transform要extends Transform ,并实现方法
public abstract String getName();
public abstract Set<ContentType> getInputTypes();
Set<QualifiedContent.ContentType> getInputTypes()
Set<? super QualifiedContent.Scope> getScopes()
boolean isIncremental()
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException

此时如果transform是个空方法,build 后就会报错,
需要在transform加入复制文件的代码,作用是将class复制到dest的目录,也就是说应用自定义transform后需要自己处理复制class文件的流程。
否则的话会出现打出来的包classes.dex是0字节

1
2
3
4
5
6
7
8
9
//不管对class处理不处理,都要copy file
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)

def dest = outputProvider.getContentLocation(jarInput.name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)

####对某个注解进行asm方法代码插桩

在自定义transtorm方法里要这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
println('hello transform')
//遍历input
inputs.each { TransformInput input ->
//遍历文件input
input.directoryInputs.each { DirectoryInput directoryInput ->
if (directoryInput.file.isDirectory()) {
directoryInput.file.eachFileRecurse { File file ->
def name = file.name
//过滤,只处理该处理都class
if (name.endsWith(".class") && !name.startsWith("R\$") &&
"R.class" != name && "BuildConfig.class" != name) {
println(name)

ClassReader cr = new ClassReader(file.bytes)
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new MethodTimeClassVisitor(cw)

cr.accept(cv, ClassReader.EXPAND_FRAMES)

byte[] code = cw.toByteArray()

FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
}

}
}
//不管对class处理不处理,都要copy file
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
//遍历jar包input
input.jarInputs.each { JarInput jarInput ->
//不管对jar处理不处理,都要copy file
def dest = outputProvider.getContentLocation(jarInput.name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}

}

MethodTimeClassVisitor 需要extends ClassVisitor实现visitMethod方法

1
2
protected void onMethodEnter()
protected void onMethodExit(int opcode)

上面这两个方法就是分布在方法前后调用的。

那里面的asm代码如何写呢?

先在项目里新建一个TestAsm.java的文件
写一些方法,比如

1
2
3
4
5
6
7
8
9
10
11
public class TestAsm {
private void methodTimeStart() {
System.out.println("========start=========");
TimeCache.setStartTime("method", System.nanoTime());
}
private void methodTimeDnd() {
TimeCache.setEndTime("method", System.nanoTime());
System.out.println(TimeCache.getCostTime("method"));
System.out.println("========end=========");
}
}

这时要用到bytecode outline神器了,使用结果如下图

然后ide->code->show bytecode outline按钮
在右侧的asm面板选择ASMified的tab,要把生成的asm代码复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
{
mv = cw.visitMethod(ACC_PRIVATE, "methodTimeStart", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(11, l0);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("========start=========");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(12, l1);
mv.visitLdcInsn("method");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "com/kalengo/commonlib/TimeCache", "setStartTime", "(Ljava/lang/String;J)V", false);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLineNumber(13, l2);
mv.visitInsn(RETURN);
Label l3 = new Label();
mv.visitLabel(l3);
mv.visitLocalVariable("this", "Lcom/kalengo/testplugin/TestAsm;", null, l0, l3, 0);
mv.visitMaxs(3, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PRIVATE, "methodTimeDnd", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(15, l0);
mv.visitLdcInsn("method");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "com/kalengo/commonlib/TimeCache", "setEndTime", "(Ljava/lang/String;J)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(16, l1);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("newFunc");
mv.visitMethodInsn(INVOKESTATIC, "com/kalengo/commonlib/TimeCache", "getCostTime", "(Ljava/lang/String;)Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLineNumber(17, l2);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("========end=========");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l3 = new Label();
mv.visitLabel(l3);
mv.visitLineNumber(18, l3);
mv.visitInsn(RETURN);
Label l4 = new Label();
mv.visitLabel(l4);
mv.visitLocalVariable("this", "Lcom/kalengo/testplugin/TestAsm;", null, l0, l4, 0);
mv.visitMaxs(3, 1);
mv.visitEnd();
}

注意要去除一些代码,比如RETURN相关的,比如RETURN后面的,比如cw.visitMethod

整理后就变成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Override
protected void onMethodEnter() {
if (inject) {
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(11, l0);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("========start=========");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(12, l1);
mv.visitLdcInsn(name);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "com/kalengo/commonlib/TimeCache", "setStartTime", "(Ljava/lang/String;J)V", false);
}
}

@Override
protected void onMethodExit(int opcode) {
if (inject) {
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(15, l0);
mv.visitLdcInsn(name);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "com/kalengo/commonlib/TimeCache", "setEndTime", "(Ljava/lang/String;J)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(16, l1);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn(name);
mv.visitMethodInsn(INVOKESTATIC, "com/kalengo/commonlib/TimeCache", "getCostTime", "(Ljava/lang/String;)Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLineNumber(17, l2);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("========end=========");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}

然后就可以build了。在这里可以看到经过插桩后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
TestPlugin/app/build/intermediates/classes/release/com/kalengo/testplugin

public class Logic {

@MethodTime
public void test() {
for (int i = 0;i < 1000;i++) {
System.out.println();
}
}

@MethodTime
public void test2() {
for (int i = 0;i < 100;i++) {
System.out.println();
}
}
}



public class Logic {
public Logic() {
}

@MethodTime
public void test() {
System.out.println("========start=========");
TimeCache.setStartTime("test", System.nanoTime());

for(int i = 0; i < 1000; ++i) {
System.out.println();
}

TimeCache.setEndTime("test", System.nanoTime());
System.out.println(TimeCache.getCostTime("test"));
System.out.println("========end=========");
}

@MethodTime
public void test2() {
System.out.println("========start=========");
TimeCache.setStartTime("test2", System.nanoTime());

for(int i = 0; i < 100; ++i) {
System.out.println();
}

TimeCache.setEndTime("test2", System.nanoTime());
System.out.println(TimeCache.getCostTime("test2"));
System.out.println("========end=========");
}
}

然后就可以assemble了。在这里可以看到经过插桩后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
TestPlugin/app/build/intermediates/transforms/CustomTransform/debug/31/com/kalengo/testplugin


public class Logic {
public Logic() {
}

@MethodTime
public void test() {
System.out.println("========start=========");
TimeCache.setStartTime("test", System.nanoTime());

for(int i = 0; i < 1000; ++i) {
System.out.println();
}

TimeCache.setEndTime("test", System.nanoTime());
System.out.println(TimeCache.getCostTime("test"));
System.out.println("========end=========");
}

@MethodTime
public void test2() {
System.out.println("========start=========");
TimeCache.setStartTime("test2", System.nanoTime());

for(int i = 0; i < 100; ++i) {
System.out.println();
}

TimeCache.setEndTime("test2", System.nanoTime());
System.out.println(TimeCache.getCostTime("test2"));
System.out.println("========end=========");
}
}

至于混淆,那就要反编译去看了,或者可以直接在ide里面点击apk包进行查看。

最后,运行一下,结果出来了。

1
2
04-14 19:50:30.503 4162-4162/com.kalengo.testplugin I/System.out: method: test time 367.292 ms
04-14 19:50:30.503 4162-4162/com.kalengo.testplugin I/System.out: method: test2 time 26.041 ms

关于设置adjustResize后webview出现输入框弹出布局错乱问题

背景:
SimpleWebActivity设置
windowSoftInputMode="adjustResize"

并且加上了沉浸式效果
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

方案1 网上有人说是因为主题设置了这个透明状态的原因,尝试去掉,webview确实正常,但是沉浸式不见了
<item name="android:windowTranslucentStatus">true</item>

或者把沉浸式效果去掉
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

但沉浸式不见了,不是我们想要的效果

方案2 随手记的经验,在activity的oncreate里面加上这段代码,就是为了把root的layout的inses消费掉,但是我尝试了,没有效果

if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.root_container_layout), new android.support.v4.view.OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { return insets.consumeSystemWindowInsets(); } }); }

链接:https://juejin.im/post/5a25f6146fb9a0452405ad5b

方案3 在SimpleWebActivity根layout加上这一句,问题解决

android:fitsSystemWindows="true"

vue怎么处理后端返回的换行符'\n'

解决方案有三个,用第三个方案会好一点

  • 1、使用v-html的标签
1
2
3
4
5
6
7
<p class="text" v-html="requestText"></p>
data(){
return{
requestText:""
}
}
str.replace('ln', '<br>');
  • 2、使用v-for循环显示替换后的文本
1
<div v-for="line in str.split('\n')">{{line}}<br></div>
  • 3、css加上
1
2
white-space: pre-line;//多个空格会合并成一个空格
white-space: pre-wrap;//多个空格仍然不变

注意:
popupConfig.tip.replace(/\n/g, ‘\n’)
popupConfig.tip.replace(/\n/g, ‘
‘)

flutter之殇

跳转url

url_launch(url, enableJavaScript: true, forceWebView: true)

SnackBar用法
切记snackBar用的context是特snackBar自己的context,不然会报

1
Another exception was thrown: Scaffold.of() called with a context that does not contain a Scaffold.

代码如下:vl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import 'package:flutter/material.dart';

class SnackBarPage extends StatelessWidget {
BuildContext scaffoldContext;

@override
Widget build(BuildContext context) {
// TODO: implement build
Widget body = Center (
child: RaisedButton(
onPressed: () {
final snackBar = SnackBar(
content: Text('hello! A SnackBar!'),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
// Some code to undo the change!
},
),
);
Scaffold.of(scaffoldContext).showSnackBar(snackBar);
},
child: Text('test')
)
);

return
MaterialApp(
title: 'SnackBar',
home: Scaffold(
appBar: AppBar(
title: Text('SnackBar')
),
body: Builder(builder: (BuildContext context) {
scaffoldContext = context;
return body;
}),
),
);
}

}

打release包,flutter build apk
但是发现请求都失败了

找不到3des加密方法,只能从android和ios分别实现,然后桥接

如果image出现找不到的情况,不要hot运行,直接重新run

webdev could not run for this project.
The pubspec.yaml file has changed since the pubspec.lock file was generated, please run “pub get” again.

pub get

webdev could not run for this project.
You must have a dependency on build_web_compilers in pubspec.yaml.

https://cloud.tencent.com/developer/article/1430689

web
很多第三方插件都不支持

No address associated with hostname

ui适配问题(大屏幕和小屏幕手机问题)

BottomNavigationBar保留不了状态问题(切换tab的时候重新构建了页面),
1、body: widget
2、
3、PageView