2024 Flutter面試題一

Dart部分

String擴展一個方法
  1. 使用關(guān)鍵字extension ... on為String定義一個擴展類
  2. 在為擴展類添加一個 新的方法
  3. String類型對象調(diào)用這個擴展的方法
extension StringExt on String{
  // 2. 擴展方法
  int add(int x, int y){
    return x + y;
  }
}

void mian(){
  String str = 'hello';
  // 3. 使用擴展類方法
  int result = str.add(3, 7);
  debugPrint(result.toString());
}

// 單元測試
import 'package:flutter_start/extTest.dart';
import 'package:flutter_test/flutter_test.dart';
void main(){
  test('StringExt', (){
    String ext = 'ext';
    expect(ext.add(3, 7), 10);
  });
}
image.png
dart是單繼承還是多繼承?

單繼承

dart如何達(dá)到多繼承的效果?

Dart中使用Mixins,可以達(dá)到多繼承的效果

mixin混入有什么特點
  1. 作為mixins的類只能繼承自O(shè)bject,不能繼承其他類
  2. 作為mixins的類不能有構(gòu)造函數(shù)
  3. 一個類可以mixins多個mixins類
  4. mixins絕不是繼承,也不是接口,而是一種全新的特性
// 類D 繼承A和B   關(guān)鍵字 with
class D  extends A with B{

}

// mixin 的使用
class A {
  String info="this is A";
  void printA(){
    print("A");
  }
  void run(){
    print("A Run");
  }
}
class B {  
  void printB(){
    print("B");
  }
  void run(){
    print("B Run");
  }
}
class C extends Person with B,A{
  C(String name, num age) : super(name, age);
}
混入相同方法的多個混入,最終會執(zhí)行哪一個?

后面的類中的方法將前面的類中相同的方法覆蓋

dart運行機制是什么樣的?

消息循環(huán)機制

  1. 兩個隊列,微任務(wù)隊列和事件隊列。
  2. microtask queue 的優(yōu)先級高于event queue。
  3. 在每一次事件循環(huán)中,Dart總是先去第一個microtask queue中查詢是否有可執(zhí)行的任務(wù),如果沒有,才會處理后續(xù)的event queue的流程。
如何向事件隊列插入任務(wù)?

Future就是將任務(wù)插入到事件隊列

向微任務(wù)隊列插入任務(wù)

Future.microtask()
scheduleMicrotask()
Stream中的執(zhí)行異步的模式就是scheduleMicrotask。因為microtask的優(yōu)先級又高于event。所以,如果 microtask 太多就可能會對觸摸、繪制等外部事件造成阻塞卡頓

Future和Stream有什么區(qū)別?
  1. Future中的任務(wù)會加入下一輪事件循環(huán),而Stream中的任務(wù)則是加入微任務(wù)隊列
  2. Future 用于表示單個運算的結(jié)果,而 Stream 則表示多個結(jié)果的序列。

Stream 有同步流和異步流之分。它們的區(qū)別在于同步流會在執(zhí)行 add,addError 或 close 方法時立即向流的監(jiān)聽器 StreamSubscription 發(fā)送事件,而異步流總是在事件隊列中的代碼執(zhí)行完成后在發(fā)送事件。`

Stream訂閱模式有哪幾種?

Stream分為Single Subscription和Broadcast兩種類型, 前者只允許訂閱(listen)一次,后者允許多次訂閱。

單訂閱在訂閱者出現(xiàn)之前會持有數(shù)據(jù),在訂閱者出現(xiàn)之后就才轉(zhuǎn)交給它。
廣播訂閱可以同時有多個訂閱者,當(dāng)有數(shù)據(jù)時就會傳遞給所有的訂閱者,而不管當(dāng)前是否已有訂閱者存在。

Stream單訂閱,多次訂閱會出現(xiàn)什么結(jié)果?

會報錯,單訂閱只能有一次訂閱.
即使取消了第一個監(jiān)聽器,也不允許在單訂閱流上設(shè)置其他的監(jiān)聽器。

Stream 可以通過 transform() 方法(返回另一個 Stream)進(jìn)行連續(xù)調(diào)用。
通過 Stream.asBroadcastStream() 可以將一個單訂閱模式的 Stream 轉(zhuǎn)換成一個多訂閱模式的 Stream,isBroadcast 屬性可以判斷當(dāng)前 Stream 所處的模式。

dart是單線程還是多線程?

Dart是單線程模型

Dart是如何實現(xiàn)多任務(wù)并行的

主要依賴dart的并發(fā)編程(Isolate)、異步和事件驅(qū)動機制

Completer
// 獲取圖片的寬高
  Future<Size> getImageSize(String path) {
    Completer<Size> completer =  Completer<Size>();
    Image image = Image.file(File.fromUri(Uri.parse(path)));
    // 預(yù)先獲取圖片信息
    image.image.resolve(const ImageConfiguration()).addListener(
        ImageStreamListener((ImageInfo info, bool _) {
          Size size = Size(info.image.width.toDouble(), info.image.height.toDouble());
          completer.complete(size);
        }));
    return completer.future;
  } 
Isolate
  • 使用場景:視頻的編碼轉(zhuǎn)碼,需要非常高的CPU計算
  • 在dart中,一個Isolate對象其實就是一個Isolate執(zhí)行環(huán)境的引用,一般來說我們都是通過當(dāng)前的Isolate去控制其他的Isolate完成彼此之間的交互,而當(dāng)我們想要創(chuàng)建一個新的Isolate可以使用Isolate.spawn方法獲取一個新的Isolate對象,兩個Isolate之間使用SendPort相互發(fā)送消息,而Isolate中也存在了一個與之對應(yīng)ReceivePort接收消息用來處理,但是我們需要注意的是SendPortReceivePort在每一個Isolate都有一對,只有同一個Isolate中的ReceivePort才能接受當(dāng)前類的SendPort發(fā)送的消息并且處理。
  • Isolate可以把它理解為Dart中的線程。但它又不同于線程,更恰當(dāng)?shù)恼f應(yīng)該是微線程。它與線程最大的區(qū)別就是不能共享內(nèi)存,因此也不存在鎖競爭問題,兩個Isolate完全是兩條獨立的執(zhí)行線,且每個Isolate都有自己的事件循環(huán),它們之間只能通過發(fā)送消息通信,所以它的資源開銷低于線程。
    創(chuàng)建Isolate的兩種方式:Isolate.spawn() 和 compute()
    compute的使用還是有些限制,它沒有辦法多次返回結(jié)果,也沒有辦法持續(xù)性的傳值計算,每次調(diào)用,相當(dāng)于新建一個隔離,如果調(diào)用過多的話反而會適得其反。
@override
  void initState() async {
    super.initState();
    // 主Isolate的ReceivePort
    ReceivePort receivePort = ReceivePort();
    SendPort? otherSendPort;
    // 主Isolate接收到子Isolate中由主Isolate的SendPort發(fā)送過來的消息
    receivePort.listen((message) {
      if(message is SendPort){
        otherSendPort = message;
        otherSendPort.send(receivePort.sendPort());
      }else{
        // 處理消息
        // ......
        
        // 子Isolate的SendPort在主Isolate中向子Isolate發(fā)送消息
       otherSendPort?.send('我是來自主Isolate的消息');
      }
    });

    // 創(chuàng)建子Isolate
    Isolate isolate = await Isolate.spawn((message) {
      // message 是主Isolate的SendPort

      // 在子Isolate中創(chuàng)建一個新的ReceivePort
      ReceivePort recPort = ReceivePort();
      // 主Isolate的SendPort
      SendPort? mainSendPort;
      
      // 運用主Isolate的SendPort將子Isolate的SendPort發(fā)送給主Isolate
      message.send(recPort.sendPort);
      // 子Isolate監(jiān)聽接收到主Isolate那邊發(fā)送的消息 誰發(fā)送?子Isolate的SendPort
      recPort.listen((msg) {
        if(msg is SendPort){
          mainSendPort = msg;
        }else{
          // 主Isolate的SendPort向主Isolate發(fā)送消息
            mainSendPort?.send('我是來自子Isolate的消息');
        }
      });
    }, receivePort.sendPort);// 參數(shù)二 將主Isolate的SendPort傳遞給子Isolate
  }


// 方式二
// 注冊主 isolate 的 SendPort
  ReceivePort mainReceivePort = ReceivePort();
  mainReceivePort.listen((state){
    // 獲取上次執(zhí)行任務(wù)的時間戳
    final lastExecutedTime = SpUtil().getStandLastTime(); // 從本地存儲或數(shù)據(jù)庫獲取
    final now = DateTime.now().millisecondsSinceEpoch;
    final interval = now - lastExecutedTime;
    if (kDebugMode) {
      print("Native called background interval: $interval");
    }
    // 判斷是否達(dá)到 1 小時
    if (interval >= 3600000) {
      // 執(zhí)行你的任務(wù)
      /// 檢查步數(shù)是否在變化  沒有變化需要站立提醒
      GlobalEvent().emit(GlobalName.wearStandCheck);
      // 更新上次執(zhí)行時間
      SpUtil().setStandLastTime(now); // 將 now 保存到本地存儲或數(shù)據(jù)庫
    }

  });
  IsolateNameServer.registerPortWithName(mainReceivePort.sendPort, 'main_isolate');

  Workmanager().initialize(
      callbackDispatcher, // The top level function, aka callbackDispatcher
      isInDebugMode: false // If enabled it will post a notification whenever the task is running. Handy for debugging tasks
  );
  /// Android系統(tǒng)限制 15分鐘執(zhí)行一次
  Workmanager().registerPeriodicTask(
    "periodic-task-identifier",
    "simplePeriodicTask",
    inputData: <String, dynamic>{
      'key': 'value',
    },
    // When no frequency is provided the default 15 minutes is set.
    // Minimum frequency is 15 min. Android will automatically change your frequency to 15 min if you have configured a lower frequency.
    frequency: const Duration(minutes: 15),
  );
await for如何使用?

await for是用來不斷獲取stream流中的數(shù)據(jù),然后執(zhí)行循環(huán)體中的操作。它一般用在直到stream什么時候完成,并且必須等待傳遞完成后才能使用,不然會阻塞。

Stream<String> stream = new Stream<String>.fromIterable(['1', '2', '3','4']);
main() async{
    await for(String s in stream){
    print(s);
  }
}

使用場景:網(wǎng)絡(luò)請求流式返回

// http庫流式返回
Future<void> postStream({required String token,required String url, required Map<String, dynamic> body,required Function(String value) onMessage}) async {
    try {
      final request = http.Request('POST', Uri.parse(url));
      request.headers['Content-Type'] = 'application/json; charset=UTF-8';
      request.headers['Authorization'] = token;
      request.body = jsonEncode(body);

      final response = await request.send();
      if (response.statusCode == 200) {
        await for (var chunk in response.stream.transform(utf8.decoder)) {
          onMessage(chunk);
        }
      } else {
        throw Exception('服務(wù)器錯誤');
      }
    }catch (e) {
      Logger.debugLog('Error: $e');
      throw Exception('服務(wù)器錯誤');
    }
  }

// dio流式返回
Future<void> postStream({required String url, required Map<String, dynamic> body,required Function(String value) onMessage,CancelToken? cancelToken,}) async {
    try {
      Options requestOptions = Options(receiveTimeout: 60000,sendTimeout: 30000);
      requestOptions.headers = {
        ..._dio!.options.headers,
        'Authorization': Ability().getLoginToken(), 
        'Content-Type': 'application/json'
      };
      requestOptions.responseType = ResponseType.stream;
      Response<dynamic> response = await _dio!.post(
        url,
        data: jsonEncode(body),
        options: requestOptions,
        cancelToken: cancelToken,
      );
      if(response.statusCode == 200){
        await for (var chunk in response.data.stream) {
          final jsonData = utf8.decode(chunk);
          onMessage(jsonData);
        }
      }
    } catch (e) {
      // 處理請求失敗的情況
      print("postStream Request failed: $e");
      rethrow;
    }
  }
如何實現(xiàn)websocket穩(wěn)定連接?
  • 定期發(fā)送心跳包,一般1秒一次
  • 捕獲關(guān)閉連接事件并重連websocket
  • 實現(xiàn)斷線重連

Flutter部分

A、B兩個組件在setState前修改背景顏色,是否會修改成功?

Flutter 渲染流程是什么?(GPU)

將dart語言的UI代碼轉(zhuǎn)換成skia能識別的數(shù)據(jù),進(jìn)行渲染。
Flutter向GPU提供視圖數(shù)據(jù)的過程。
Flutter只關(guān)心向 GPU提供視圖數(shù)據(jù),GPU的 VSync信號同步到 UI線程,UI線程使用 Dart來構(gòu)建抽象的視圖結(jié)構(gòu),這份數(shù)據(jù)結(jié)構(gòu)在 GPU線程進(jìn)行圖層合成,視圖數(shù)據(jù)提供給 Skia引擎渲染為 GPU數(shù)據(jù),這些數(shù)據(jù)通過 OpenGL或者 Vulkan提供給 GPU。


20190227094157995.png

image.png
Widget、Element、RenderObject三者關(guān)系是什么?

Widget包含業(yè)務(wù)代碼,widget樹更龐大;Element是對widget的抽取,只包含build函數(shù),去除業(yè)務(wù)代碼。

簡述

widget是用于描述Element配置信息的,flutter中一切都是widget,尺寸、顏色、組件等都是widget
element是widget樹上特定位置的實例
renderobject是渲染樹上的一個對象

依賴關(guān)系:Element樹依賴Widget樹,渲染樹依賴Element樹,最終的UI樹是由獨立的Element節(jié)點構(gòu)成。

一個widget會創(chuàng)建一個element
一個element持有一個widget和render object,element
會對比widget的變化,將那寫需要更新和重建的widget,同步到render object樹,以最小的開銷來渲染

一、它們是什么?
  • Widget:對一個Element配置的描述,刷新的過程中隨時會重建。(不參與真正的渲染,widget的屬性是不可以改變的,要想改變只能重新創(chuàng)建一個widget對象)
  • Element:表示一個Widget樹中特定位置的實例,用于對比widget,找出需要更新和重建的widget,更新Element樹和RenderObject樹。
  • RenderObject:渲染樹上的一個對象,用于界面的布局和繪制,負(fù)責(zé)真正的渲染,實例化一個 RenderObject 是非常耗能。
二、關(guān)系
  • 一個Widget會創(chuàng)建一個Element對象,是通過createElement()創(chuàng)建的。
  • 一個Element持有一個RenderObject和一個Widget。Element樹與Widget樹一一對應(yīng),每個Element負(fù)責(zé)管理一個Widget的配置和生命周期。
  • Widget 具有不可變性,但 Element 卻是可變的。Element 樹將 Widget 樹的變化做了抽象,可以只將真正需要修改的部分同步到真實的 RenderObject 樹中,最大程度降低對真實渲染視圖的修改,提高渲染效率,而不是銷毀整個渲染視圖樹重建。
flutter生命周期,setstate會執(zhí)行哪些生命周期?
image.png
image.png
  • setState()執(zhí)行后,會執(zhí)行build()
  • 父Widget使用了InheritedWidget管理狀態(tài),子Widget使用了狀態(tài)數(shù)據(jù),set State時,子組件的didChangeDependencies會調(diào)用。

https://juejin.cn/post/7348680291935862818?searchId=20240410133635350846A909709A89034C

  • app的狀態(tài):AppLifecycleState
    inactive:活躍可見
    paused:關(guān)閉或者切換到后臺時,不可見的狀態(tài)
    hidden:后臺運行狀態(tài)
    resumed:切回到前臺可見狀態(tài)
    detached:關(guān)閉狀態(tài)
  • Flutter SDK 3.13 之前的方式: with WidgetsBindingObserver
    initState()中注冊 WidgetsBinding.instance.addObserver(this);
    dispose()移除 WidgetsBinding.instance.removeObserver(this);
    didChangeAppLifecycleState()回調(diào)中,檢測app的狀態(tài)

  • Flutter SDK 3.13 之后的方式:AppLifecycleListener

late final AppLifecycleListener _listener;
@override
  void initState() {
    super.initState();
    // Initialize the AppLifecycleListener class and pass callbacks
    _listener = AppLifecycleListener(
      onStateChange: _onStateChanged,
    );
  }
  @override
  void dispose() {
    // Do not forget to dispose the listener
    _listener.dispose();
    super.dispose();
  }
  // Listen to the app lifecycle state changes
  void _onStateChanged(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.detached:
        _onDetached();
      case AppLifecycleState.resumed:
        _onResumed();
      case AppLifecycleState.inactive:
        _onInactive();
      case AppLifecycleState.hidden:
        _onHidden();
      case AppLifecycleState.paused:
        _onPaused();
    }
  }
StatefulWidget 的生命周期方法

createState:可以調(diào)用多次

State 對象的生命周期方法
class _MyWidgetState extends State<MyWidget> {
  @override
  void initState() {
    super.initState();
    // 在 State 對象被插入樹中時調(diào)用,這個方法只會被調(diào)用一次。
  }
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // 在 initState 之后調(diào)用,表示 State 對象的依賴關(guān)系發(fā)生變化。
    // state對象依賴發(fā)生改變會調(diào)用
  }
  @override
  Widget build(BuildContext context) {
    // 在此構(gòu)建 Widget 樹
    return Container();
  }
  @override
  void didUpdateWidget(MyWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 當(dāng)父 Widget 重建時,canUpdate 返回true時調(diào)用。
    // 如果父 Widget 重建時需要重新配置子 Widget,則會調(diào)用此方法。
  }
  @override
  void deactivate() {
    // 在此處理 State 對象從樹中被移除的操作
    super.deactivate();
  }

  @override
  void dispose() {
    // 當(dāng) State 對象被永久從樹中移除時調(diào)用
    super.dispose();
  }
}

// 打開頁面執(zhí)行:initState、didChangeDependencies
// 關(guān)閉頁面執(zhí)行:deactivate、dispose

StatelessWidget 的生命周期方法
class MyWidget extends StatefulWidget {
  @override
  Widget build(BuildContext context) {
    return Widget;
  }
}

StatelessWidget組件如何監(jiān)聽數(shù)據(jù)變化?

  1. context.watch<Counter>
  2. ChangeNotifierProvider + Consumer

Element的生命周期

initial 初始化
active 激活狀態(tài)
inactive 未激活狀態(tài)
defunct 失效狀態(tài)

如何監(jiān)聽頁面paused和resume狀態(tài)?

利用RouteObserverMaterialApp中注冊navigatorObservers,然后在頁面中注冊監(jiān)聽routeObserver.subscribe(this, ModalRoute.of(context)!),同時混入RouteAware,重寫didPopNext 和 didPushNext,可實現(xiàn)對頁面的監(jiān)聽。

setState()執(zhí)行做了什么事?

setState()過程主要工作是記錄所有的臟元素,會引起build函數(shù)執(zhí)行,更新widget樹、更新Element樹和RenderObject樹,最后重新渲染。

flutter中的key?
  • 作用:比較兩個Widget是不是同一個Widget
  • 分類:LocaleKey、GlobalKey
  • LocaleKey:ValueKey、ObjectKey、UniqueKey。
statelesswidget和statefullwidget有什么區(qū)別?

StatelessWidget 沒有要管理的內(nèi)部狀態(tài).
無狀態(tài)widget的build方法通常只會在以下三種情況調(diào)用:

  • 將widget插入樹中時
  • 當(dāng)widget的父級更改其配置時
  • 當(dāng)它依賴的InheritedWidget發(fā)生變化時

StatefullWidget是可變狀態(tài)的widget。 使用setState方法管理StatefulWidget的狀態(tài)的改變。調(diào)用setState告訴Flutter框架,某個狀態(tài)發(fā)生了變化,F(xiàn)lutter會重新運行build方法,以便應(yīng)用程序可以應(yīng)用最新狀態(tài)。

在有狀態(tài)類中編寫一個按鈕調(diào)用初始化生命周期(initState)方法,會發(fā)生什么?

報錯,但不影響布局;會報生命周期創(chuàng)建錯誤;

如何獲取控件的大小和位置?
  1. 使用Key拿到上下文取得findRenderObject拿內(nèi)容的尺寸數(shù)據(jù);
  2. 使用context取得findRenderObject拿內(nèi)容的尺寸數(shù)據(jù);
Flutter 是如何與原生Android、iOS進(jìn)行通信的?

PlatformChannel
BasicMessageChannel :用于傳遞字符串和半結(jié)構(gòu)化的信息。
MethodChannel :用于傳遞方法調(diào)用(method invocation)。
EventChannel : 用于數(shù)據(jù)流(event streams)的通信。

// 1. 創(chuàng)建java類。  實現(xiàn)FlutterPlugin和MethodCallHandler
 public class MsaOaidPlugin implements FlutterPlugin, MethodCallHandler{
   @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    Log.e("---------","==========onAttachedToEngine");
    channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "msa_oaid");
    channel.setMethodCallHandler(this);
    this.context = flutterPluginBinding.getApplicationContext();
    System.loadLibrary("msaoaidsec");

  }
  @Override
  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
    Log.e("---------","==========onMethodCall");
    if(call.method.equals("isSupport")){
      new DemoHelper().getDeviceIds(context, new IIdentifierListener() {
        @Override
        public void onSupport(IdSupplier idSupplier) {
          result.success(idSupplier.isSupported());
        }
      });
    }else if(call.method.equals("getOaid")){
      new DemoHelper().getDeviceIds(context, new IIdentifierListener() {
        @Override
        public void onSupport(IdSupplier idSupplier) {
          result.success(idSupplier.getOAID());
        }
      });

    }else{
      result.notImplemented();
    }

  }

  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
    channel.setMethodCallHandler(null);
    Log.e("---------","==========onDetachedFromEngine");
  }
 }


// 2. flutter lib包下創(chuàng)建dart類

class MsaOaid {
  static const MethodChannel _channel = MethodChannel('msa_oaid');

  static Future<bool>  isSupport() async {
    final bool support = await _channel.invokeMethod('isSupport');
    return support;
  }

  static Future<String?> getOaid() async {
    final String? oaid = await _channel.invokeMethod('getOaid');
    return oaid;
  }
}
Flutter中Widget的分類有哪些?Widget狀態(tài)有哪些?

Widget的分類有三類。

  1. 組合類Widget,通過繼承StatelessWidget和StatefulWidget的類。
  2. 代理類Widget,如功能組件InheritedWidget。Theme、MediaQuery正是基于InheritedWidget實現(xiàn)的。
  3. 繪制類Widget,通過RenderObjectWidget實現(xiàn)的Widget,如Align、Padding、ConstrainedBox等。
    Widget狀態(tài)有: StatelessWidget 和 StatefulWidget

Android部分

實現(xiàn)app?;钣心男┓绞??
計步器在app進(jìn)程被殺這段時間如何計步?

補充

https://zhuanlan.zhihu.com/p/102193331

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容