layout的基本流程:

  1. parent将约束信息传递给child,来限制child的最大宽高;
  2. child根据根据约束信息确认自己的大小(如果儿子还有孙子,则儿子继续执行所有1~4的布局流程);
  3. parent根据child返回的大小来确定自己的大小;
  4. parent确定child的位置偏移,并将位置偏移记录在child.parentData.offset中;

Center的布局

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
//RenderPositionedBox
void performLayout() {
final BoxConstraints constraints = this.constraints;
//当需要缩放,或者约束的宽度为无限大时 shrinkWrapWidth=true
final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
//同上
final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;

if (child != null) {
//child根据parent的约束进行布局,layout最终还是会调用performLayout来计算布局
child!.layout(constraints.loosen(), parentUsesSize: true);
//
size = constraints.constrain(Size(
shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
));
alignChild();
} else {
//如果没有child,则返回一个无限大的布局大小
size = constraints.constrain(Size(
shrinkWrapWidth ? 0.0 : double.infinity,
shrinkWrapHeight ? 0.0 : double.infinity,
));
}
}
阅读全文 »

Flutter启动

main函数就是Flutter app的启动入口,在main函数中调用runApp就可以启动我们的界面了:

1
2
3
void main() {
runApp(const MyApp());
}

下面是runApp的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void runApp(Widget app) {
//binding的初始化
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
//启动绘制流程
binding
..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
..scheduleWarmUpFrame();
}

class WidgetsFlutterBinding extends BindingBase
with GestureBinding,
SchedulerBinding,
ServicesBinding,
PaintingBinding,
SemanticsBinding,
RendererBinding,
WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding._instance == null) {
WidgetsFlutterBinding();
}
return WidgetsBinding.instance;
}
}

WidgetsFlutterBinding继承自BindingBase,但通过with语句混入了各种Binding,我们都知道mixin的类是没有构造函数的。ensureInitialized()会调用WidgetsFlutterBinding构造函数,主要实现在BindingBase构造函数中。BindingBase在构造函数中会调用initInstances(),这个函数在各个mixin的Binding类中都有实现,首先调用的是WidgetsBinding的实现,然后一层一层调用super.initInstances(),这样所以Binding类的initInstances()都会被执行。下面是initInstances的调用时序:


zenuml
WidgetsBinding.initInstances {
    RendererBinding.initInstances {
        SemanticsBinding.initInstances {
            PaintingBinding.initInstances {
                ServicesBinding.initInstances {
                    SchedulerBinding.initInstances {
                        GestureBinding.initInstances {
                            return
                        }
                        return
                    }
                    return
                }
                createImageCache()
                return
            }
            return
        }
        createRootPipelineOwner
        return
    }
    "_buildOwner = BuildOwner();"
    "buildOwner!.onBuildScheduled = _handleBuildScheduled;"
}
  • GestureBinding.initInstances:会向PlatformDispatcher设置手势事件的回调,会把从flutter engine接收的数据包解析成PointerEvent,包括PointerDownEvent、PointerUpEvent等;

  • SchedulerBinding提供了scheduleFrame()接口来主动触发一次绘制刷新。并且还会向PlatformDispatcher注册屏幕刷新事件的回调,如下:

    1
    2
    3
    4
    void ensureFrameCallbacksRegistered() {
    platformDispatcher.onBeginFrame ??= _handleBeginFrame;
    platformDispatcher.onDrawFrame ??= _handleDrawFrame;
    }

启动渲染流程

packages/flutter/lib/src/widgets/binding.dart中的WidgetsBinding.drawFrame()的注释中介绍了渲染流程。

阅读全文 »

下面是Flutter官网提供的关于三棵树的介绍,比较形象,这里直接摘录了,可以参考原文

1
2
3
4
5
6
7
8
9
Container(
color: Colors.blue,
child: Row(
children: [
Image.network('https://www.example.com/1.png'),
const Text('A'),
],
),
);

上面这段代码构建的三棵树如下所示:

Flutter三棵树示意图

在开发调试阶段使用Flutter inspector可以看出实际的Widget树要比代码中描述的层级更深。

Widget树

先看下Widget的家族:

WidgetRenderObjectWidgetSingleChildRenderObjectWidgetMultiChildRenderObjectWidgetFlexColumnRowStatefulWidgetStatelessWidgetInheritedWidgetProxyWidgetContainer

两外还有如下经常使用的Wiget:

  • PreferredSizeWidget继承自Widget,相关子类有AppBar和TabBar;
  • LayoutBuilder可以根据parent的布局约束来创建合理的布局;
阅读全文 »

using用法

  1. 导入命名空间

    1
    2
    3
    4
    // 导入整个命名空间到当前作用域,这样std::cout就可以写成cout了。
    using namespace std;
    // 只导入某个变量到当前作用域
    using std::cout;

    一般头文件中要避免使用using导入命名空间。否则引入这个头文件的也相当于导入了命名空间,容易带来命名冲突。

  2. 指定别名,C++11中可以使用using来指定别名,作用与typedef相同

    1
    2
    typedef int T; // 用 T 代替 int
    using T = int; // 用 T 代替 int
  3. 派生类引入基类成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Base {
    protected: //此处可以改成public,但不能改成private
    int value;
    int name;
    };

    class Derived : private Base { //Base是私有继承
    public:
    //通过Derived的实例可以访问父类的value,但是无法访问父类的name变量
    using Base::value;
    };

类型转换

static_cast

1
2
3
4
5
6
class B {};
class D : public B {};

D d;
B& b = d;
D& newD = static_cast<D&>(b);

代码中D和B的继承关系不是虚继承(没有virtual修饰继承关系),如果是虚继承,就无法使用static_cast了。

dynamic_cast

1
2
3
4
5
6
7
8
9
10
11
class B {
public:
virtual void test() {}
};
class D : virtual public B {};

D d;
B& b = d;
D& newD = dynamic_cast<D&>(b); // downcast

B& newB = dynamic_cast<B&>(d); //可以成功,但无必要,可以直接使用B& newB = d;效率更高

上述代码有两个关键点:1)D与B的继承关系通过virtual修饰;2)B包含虚函数。只有这两个条件同时满足才能使用dynamic_cast。另外与static_cast区别是:static_cast必然成功,而dynamic_cast失败的情况会返回target-type类型的空指针。

值分类

阅读全文 »

std::shared_ptr

std::shared_ptr共享智能指针,可以支持多个智能指针共享一个对象实例。shared_ptr持有的实例会在下面场景下被销毁:

  • 持有实例的最后一个shared_ptr被销毁;
  • 持有实例的最后一个shared_ptr被另一个智能指针赋值,或者shared_ptr调用了reset();

shared_ptr

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
shared_ptr<User> sp1 = make_shared<User>(1, "sp1", "123456");
shared_ptr<User> sp2(new User(2, "sp2", "123456"));

//指针和赋值动作可以分开
shared_ptr<User> sp3(nullptr);
sp3 = make_shared<User>(3, "sp3", "123456");

// Control Block的ref将会加一
audo p4 = sp1;

//Control Block和Object都会被交换
sp1.swap(sp2);

参考:创建和使用 shared_ptr 实例

注意:不要使用裸指针来初始化智能指针,会导致多次释放内存造成崩溃:

1
2
3
4
5
void test() {
User* user = new User(0, "Jone", "12345");
shared_ptr<User> p1(user);
shared_ptr<User> p2(user);
}

当test()函数退出时,p2会先调用User析构,然后p1会再次调用User析构函数,同时两次释放同一块内存,就会导致程序出错。下面是正确的写法:

阅读全文 »

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
class Base {
public:
int baseValue;
virtual void baseFun() {}
};

class B {
public:
int bValue;
virtual void fb1() {}
virtual void fb2() {}
virtual void test() { cout << "B::test()" << endl; }
};

class C : public Base {
public:
int cValue;
virtual void fc1() {}
virtual void fc2() {}
virtual void test() { cout << "C::test()" << endl; }
};

class A : public B, public C
{
public:
int aPublicValue;
virtual void fb2() {}
//virtual void test() { cout << "A::test()" << endl; }
virtual void fc2() {}
virtual void funcA1() {}
virtual void funcA2() {}
void funcA3() {}
private:
short aPrivateValue;
};

C++对象内存布局

通过VisualStudio来查看A类的内存布局:

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
class A	size(28):
+---
0 | +--- (base class B)
0 | | {vfptr}
4 | | bValue
| +---
8 | +--- (base class C)
8 | | +--- (base class Base)
8 | | | {vfptr}
12 | | | baseValue
| | +---
16 | | cValue
| +---
20 | aPublicValue
24 | aPrivateValue
| <alignment member> (size=2)
+---

A::$vftable@B@:
| &A_meta
| 0
0 | &B::fb1
1 | &A::fb2
2 | &B::test
3 | &A::funcA1
4 | &A::funcA2

A::$vftable@C@:
| -8
0 | &Base::baseFun
1 | &C::fc1
2 | &A::fc2
3 | &C::test

A::fb2 this adjustor: 0
A::fc2 this adjustor: 8
A::funcA1 this adjustor: 0
A::funcA2 this adjustor: 0

A::$vftable@B@可以看出虚函数表中A和B合并在一起了,并且A::fb2覆盖了B::fb2。而C类的vftable则是独立存储。

B和C两个类都定义了test()函数,而A类则没有覆盖test方法,如果A类调用test方法到底采用的是那个呢?在A::$vftable@B@指向的是B::test。而A::$vftable@C@指向的是C::test。我们通过下面代码进行测试:

1
2
3
4
5
6
7
8
9
10
int main() {
A a;
//a.test(); //此处会导致编译错误,因为无法确定test的实现
a.B::test(); //通过限定引用可以解决多义性问题
B& b = static_cast<B&>(a);
b.test(); //打印 B::test()
C* c = static_cast<C*>(&a);
c->test(); //打印 C::test()
return 0;
}

所以基于A类的实例是无法调用test函数的,但是把A类转换成B/C的引用或者指针后,就能调用test方法了,但只能分别调用自己的函数实现。

我们现在把C继承Base使用virtual进行修饰,代码改动如下:

1
class C : public virtual Base
阅读全文 »

启动优化

延迟加载,比如通过IdleHandler在系统空闲时加载模块
异步初始化

缺点:界面展示了,但是用户操控出现卡断

Android 性能优化 - 启动优化
How to Capture Heap Dump From an Android App
浏览 Systrace 报告
Dalvik 可执行文件格式
应用性能优化之VerifyClass

渲染性能优化

防止因视图层次结构导致性能下降包括两个目标:一个是实现视图层次结构扁平化,一个是减少 Double Taxation。布局耗时过长的一个常见原因是,View 对象的层次结构互相嵌套(层级过深)。每个嵌套的布局对象都会增加布局阶段的开销。层次结构越扁平,完成布局阶段所需的时间越少。

优化方案:

  • 使用 ConstraintLayout 而非 RelativeLayout 或 LinearLayout,因为这么做通常更高效,并且减少了布局的嵌套。不过,对于可通过 FrameLayout 实现的简单布局,还是建议使用 FrameLayout;
  • 如果使用 RelativeLayout 类,则可通过使用未设权重的嵌套 LinearLayout 视图,以更低的开销达到同样的效果。但是,如果您使用的是嵌套的加权 LinearLayout 视图,则布局开销会高得多,因为它需要多次布局传递;
  • 使用 RecyclerView 而不要使用 ListView,因为前者可以回收各个列表项的布局,这种方式不仅效率更高,而且可以提升滚动性能。
  • 布局中使用 <merge>来减少布局层次。
  • 使用ViewStub延迟加载视图
  • 降低透明度,比如一个字体颜色不要使用透明度的值,要使用RGB,而不是ARGB。
  • 移除布局中不必要的背景

性能和视图层次结构
Jetpack Compose 性能
检查 GPU 渲染速度和过度绘制
Android技术质量 是谷歌官方介绍的性能检查和优化措施。

频繁GC:申请大量对象、图片内存占用

阅读全文 »

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
@Preview
@Composable
fun MyDemo() {
var contents by remember {
mutableStateOf(listOf("str1", "str0"))
}
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
ElevatedButton(onClick = {
val list = mutableListOf<String>()
list.add("str${contents.size}")
list.addAll(contents)
contents = list
}) {
Text(text = "顶部插入")
}
for (txt in contents) {
key(txt) {
DemoText(text = txt)
}
}
DemoText(text = "Hello1")
DemoText(text = "Hello2")
}
}

@Composable
fun DemoText(text: String) {
Log.d("SecondActivity", "[rjy] DemoText=$text")
Text(text = text)
}

上述代码中,如果不使用key,就会导致每次顶部插入时,for循环内部的组合项都会被重新构造。加上key后,就会只构造text变化的DemoText。另外对于for循环外部的两个DemoText时不会重新构造的,因为这两个组合项的输入参数时不变的。

参考:可组合项的生命周期

阅读全文 »

sample项目调试

1
2
3
4
5
6
7
8
9
10
11
12
13
sample-host-debug.apk -> projects/sample/source/sample-host
├── sample-constant -> projects/sample/source/sample-constant
└── sample-host-lib -> projects/sample/source/sample-host-lib

pluginmanager.apk -> projects/sample/source/sample-manager
└── sample-constant -> projects/sample/source/sample-constant

sample-runtime-debug.apk -> projects/sample/source/sample-plugin/sample-runtime
└── activity-container -> projects/sdk/core/activity-container (maven)

sample-loader-debug.apk -> projects/sample/source/sample-plugin/sample-loader
├── sample-host-lib -> projects/sample/source/sample-host-lib (compileOnly)
└── sample-constant -> projects/sample/source/sample-constant

下载Shadow工程后,在根目录执行./gradlew build等待编译成功,然后通过adb安装:

1
adb install projects/sample/source/sample-host/build/outputs/apk/debug/sample-host-debug.apk

在sample-host-debug.apk的assets目录下包含pluginmanager.apkplugin-debug.zip,其中plugin-debug.zip包含的文件:

1
2
3
4
5
6
7
.
├── config.json
├── sample-app-plugin-debug.apk
├── sample-app-plugin-debug2.apk
├── sample-base-plugin-debug.apk
├── sample-loader-debug.apk
└── sample-runtime-debug.apk

config.json文件内容如下:

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
{
"compact_version": [
1,
2,
3
],
"pluginLoader": {
"apkName": "sample-loader-debug.apk",
"hash": "476180733ADFE69DDBE88CADAD473A4B"
},
"plugins": [
{
"partKey": "sample-plugin-app",
"apkName": "sample-app-plugin-debug.apk",
"dependsOn": [
"sample-base"
],
"businessName": "sample-plugin-app",
"hostWhiteList": [
"com.tencent.shadow.sample.host.lib"
],
"hash": "ECF851E261BF1B8D165D9AEB7C4C3699"
},
{
"partKey": "sample-plugin-app2",
"apkName": "sample-app-plugin-debug2.apk",
"dependsOn": [
"sample-base"
],
"businessName": "sample-plugin-app2",
"hostWhiteList": [
"com.tencent.shadow.sample.host.lib"
],
"hash": "ECF851E261BF1B8D165D9AEB7C4C3699"
},
{
"partKey": "sample-base",
"apkName": "sample-base-plugin-debug.apk",
"businessName": "sample-plugin-app",
"hostWhiteList": [
"com.tencent.shadow.sample.host.lib"
],
"hash": "18CA96C8EA499DC657ED6DE44B050728"
}
],
"runtime": {
"apkName": "sample-runtime-debug.apk",
"hash": "364BAB0EB1B8CD345D15D767D1FA8DC7"
},
"UUID": "F5574A0D-D937-42C0-93A6-35E190CCA955",
"version": 4,
"UUID_NickName": "1.1.5"
}

插件启动过程

在sample代码中启动插件Activity会通过一个跳板PluginLoadActivity,跳板Activity会展示插件loading view,从而避免插件启动过程中的黑屏给用户带来困惑。

阅读全文 »

Android屏幕分辨率适配一般有三种方案:

  1. 通过百分比的形式进行布局
  2. 动态适配屏幕分辨率,通过一系列算法去计算每个view在用户手机屏幕中应该的宽高,从而达到一个适配效果;
  3. 静态适配屏幕分辨率,就是通过工具生成各种屏幕尺寸的xml文件。

ConstraintLayout进行百分比布局

ConstraintLayout中,当子View尺寸设置为MATCH_CONSTRAINT(0dp)时,默认行为是占据所有可用空间,但是可以通过layout_constraintHeight_percentlayout_constraintWidth_percent来设置为占用父View的百分比,注意,这个百分比是整个父View的宽高的百分比,而不是可用空间的百分比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#00eeee"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent=".5"
app:layout_constraintWidth_percent=".5"/>
</androidx.constraintlayout.widget.ConstraintLayout>

通过bias来设置位置偏移,下面用法可以让子View左边与parent的间距占比为20%,而不是默认的50%。详细介绍可参考Centering positioning and bias

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/test_view"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#00eeee"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.2"
app:layout_constraintVertical_bias="0.2"/>
</androidx.constraintlayout.widget.ConstraintLayout>

静态屏幕适配

通过AndroidStudio的ScreenMatch插件生成不同分辨率的dimens资源文件来进行适配。该插件会基于res/values/dimens.xml生成不同分辨率的比如:

1
2
3
4
5
6
7
8
res/values-sw240dp/dimens.xml
res/values-sw320dp/dimens.xml
res/values-sw384dp/dimens.xml
res/values-sw392dp/dimens.xml
res/values-sw400dp/dimens.xml
res/values-sw410dp/dimens.xml
res/values-sw411dp/dimens.xml
res/values-sw432dp/dimens.xml
阅读全文 »
0%