Flutter Boost 混合開發(fā)框架
一、Flutter Boost簡(jiǎn)介
眾所周知,F(xiàn)lutter是一個(gè)由C++實(shí)現(xiàn)的Flutter Engine和由Dart實(shí)現(xiàn)的Framework組成的跨平臺(tái)技術(shù)框架。其中,F(xiàn)lutter Engine負(fù)責(zé)線程管理、Dart VM狀態(tài)管理以及Dart代碼加載等工作,而Dart代碼所實(shí)現(xiàn)的Framework則負(fù)責(zé)上層業(yè)務(wù)開發(fā),如Flutter提供的組件等概念就是Framework的范疇。
隨著Flutter的發(fā)展,國(guó)內(nèi)越來越多的App開始接入Flutter。為了降低風(fēng)險(xiǎn),大部分App采用漸進(jìn)式方式引入Flutter,在App里選幾個(gè)頁(yè)面用Flutter來編寫,但都碰到了相同的問題,在原生頁(yè)面和Flutter頁(yè)面共存的情況下,如何管理路由,以及原生頁(yè)面與Flutter頁(yè)面之間的切換和通信都是混合開發(fā)中需要解決的問題。然而,官方?jīng)]有提供明確的解決方案,只是在混合開發(fā)時(shí),官方建議開發(fā)者,應(yīng)該使用同一個(gè)引擎支持多窗口繪制的能力,至少在邏輯上做到FlutterViewController是共享同一個(gè)引擎里面的資源。換句話說,官方希望所有的繪制窗口共享同一個(gè)主Isolate,而不是出現(xiàn)多個(gè)主Isolate的情況。不過,對(duì)于現(xiàn)在已經(jīng)出現(xiàn)的多引擎模式問題,F(xiàn)lutter官方也沒有提供好的解決方案。除了內(nèi)存消耗嚴(yán)重外,多引擎模式還會(huì)帶來如下一些問題。
- 冗余資源問題。多引擎模式下每個(gè)引擎的Isolate是相互獨(dú)立的,雖然在邏輯上這并沒有什么壞處,但是每個(gè)引擎底層都維護(hù)了一套圖片緩存等比較消耗內(nèi)存的對(duì)象,因此設(shè)備的內(nèi)存消耗是非常嚴(yán)重的。
- 插件注冊(cè)問題。在Flutter插件中,消息傳遞需要依賴Messenger,而Messenger是由FlutterViewController去實(shí)現(xiàn)的。如果一個(gè)應(yīng)用中同時(shí)存在多個(gè)FlutterViewController,那么插件的注冊(cè)和通信將會(huì)變得混亂且難以維護(hù)。
- Flutter組件和原生頁(yè)面的差異化問題。通常,F(xiàn)lutter頁(yè)面是由組件構(gòu)成的,原生頁(yè)面則是由ViewController或者Activity構(gòu)成的。邏輯上來說,我們希望消除Flutter頁(yè)面與原生頁(yè)面的差異,否則在進(jìn)行頁(yè)面埋點(diǎn)和其它一些操作時(shí)增加一些額外的工作量。
- 增加頁(yè)面通信的復(fù)雜度。如果所有的Dart代碼都運(yùn)行在同一個(gè)引擎實(shí)例中,那么它們會(huì)共享同一個(gè)Isolate,可以用統(tǒng)一的框架完成組件之間的通信,但是如果存在多個(gè)引擎實(shí)例會(huì)讓Isolate的管理變得更加復(fù)雜。
如果不解決多引擎問題,那么混合項(xiàng)目的導(dǎo)航棧如下圖所示。

目前,對(duì)于原生工程混編Flutter工程出現(xiàn)的多引擎模式問題,國(guó)內(nèi)主要有兩種解決方案,一種是字節(jié)跳動(dòng)的修改Flutter Engine源碼方案,另一種是閑魚開源的FlutterBoost。由于字節(jié)跳動(dòng)的混合開發(fā)的方案沒有開源,所以現(xiàn)在能使用的就剩下FlutterBoost方案。
FlutterBoost是閑魚技術(shù)團(tuán)隊(duì)開發(fā)的一個(gè)可復(fù)用頁(yè)面的插件,旨在把Flutter容器做成類似于瀏覽器的加載方案。為此,閑魚技術(shù)團(tuán)隊(duì)為希望FlutterBoost能完成如下的基本功能:
- 可復(fù)用的通用型混合開發(fā)方案。
- 支持更加復(fù)雜的混合模式,比如支持Tab切換的場(chǎng)景。
- 無侵入性方案,使用時(shí)不再依賴修改Flutter的方案。
- 支持對(duì)頁(yè)面生命周期進(jìn)行統(tǒng)一的管理。
- 具有統(tǒng)一明確的設(shè)計(jì)概念。
并且,最近Flutter Boost升級(jí)了3.0版本,并帶來了如下的一些更新:
- 不侵入引擎,兼容Flutter的各種版本,F(xiàn)lutter sdk的升級(jí)不需要再升級(jí)FlutterBoost,極大降低升級(jí)成本。
- 不區(qū)分Androidx和Support分支。
- 簡(jiǎn)化架構(gòu)和接口,和FlutterBoost2.0比,代碼減少了一半。
- 雙端統(tǒng)一,包括接口和設(shè)計(jì)上的統(tǒng)一。
- 支持打開Flutter頁(yè)面,不再打開容器場(chǎng)景。
- 頁(yè)面生命周期變化通知更方便業(yè)務(wù)使用。
- 解決了2.0中的遺留問題,例如,F(xiàn)ragment接入困難、頁(yè)面關(guān)閉后不能傳遞數(shù)據(jù)、dispose不執(zhí)行,內(nèi)存占用過高等。
二、Flutter Boost集成
在原生項(xiàng)目中集成Flutter Boost只需要將Flutter Boost看成是一個(gè)插件工程即可。和其他Flutter插件的集成方式一樣,使用FlutterBoost之前需要先添加依賴。使用Android Studio打開混合工程的Flutter工程,在pubspec.yaml中添加FlutterBoost依賴插件,如下所示。
flutter_boost:
git:
url: 'https://github.com/alibaba/flutter_boost.git'
ref: 'v3.0-hotfixes'
需要說明的是,此處的所依賴的FlutterBoost的版本與Flutter的版本是對(duì)應(yīng)的,如果不對(duì)應(yīng)使用過程中會(huì)出現(xiàn)版本不匹配的錯(cuò)誤。然后,使用flutter packages get命令將FlutterBoost插件拉取到本地。
2.1 Android集成
使用Android Studio打開新建的原生Android工程,在原生Android工程的settings.gradle文件中添加如下代碼。
setBinding(new Binding([gradle: this])) evaluate(new File( settingsDir.parentFile, 'flutter_library/.android/include_flutter.groovy'))
然后,打開原生Android工程app目錄下的build.gradle文件,繼續(xù)添加如下依賴腳本。
dependencies {
implementation project(':flutter_boost')
implementation project(':flutter')
}
重新編譯構(gòu)建原生Android工程,如果沒有任何錯(cuò)誤則說明Android成功了集成FlutterBoost。使用Flutter Boost 之前,需要先執(zhí)行初始化。打開原生Android工程,新建一個(gè)繼承FlutterApplication的Application,然后在onCreate()方法中初始化FlutterBoost,代碼如下。
public class MyApplication extends FlutterApplication {
@Override
public void onCreate() {
super.onCreate();
FlutterBoost.instance().setup(this, new FlutterBoostDelegate() {
@Override
public void pushNativeRoute(String pageName, HashMap<String, String> arguments) {
Intent intent = new Intent(FlutterBoost.instance().currentActivity(), NativePageActivity.class);
FlutterBoost.instance().currentActivity().startActivity(intent);
}
@Override
public void pushFlutterRoute(String pageName, HashMap<String, String> arguments) {
Intent intent = new FlutterBoostActivity.CachedEngineIntentBuilder(FlutterBoostActivity.class, FlutterBoost.ENGINE_ID)
.backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.opaque)
.destroyEngineWithActivity(false)
.url(pageName)
.urlParams(arguments)
.build(FlutterBoost.instance().currentActivity());
FlutterBoost.instance().currentActivity().startActivity(intent);
}
},engine->{
engine.getPlugins();
} );
}
}
然后,打開原生Android工程下的AndroidManifest.xml文件,將Application替換成自定義的MyApplication,如下所示。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.idlefish.flutterboost.example">
<application
android:name="com.idlefish.flutterboost.example.MyApplication"
android:label="flutter_boost_example"
android:icon="@mipmap/ic_launcher">
<activity
android:name="com.idlefish.flutterboost.containers.FlutterBoostActivity"
android:theme="@style/Theme.AppCompat"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize" >
<meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/launch_background"/>
</activity>
<meta-data android:name="flutterEmbedding"
android:value="2">
</meta-data>
</application>
</manifest>
由于Flutter Boost 是以插件的方式集成到原生Android項(xiàng)目的,所以我們可以在Native 打開和關(guān)閉Flutter模塊的頁(yè)面。
FlutterBoost.instance().open("flutterPage",params);
FlutterBoost.instance().close("uniqueId");
而Flutter Dart的使用如下。首先,我們可以在main.dart文件的程序入口main()方法中進(jìn)行初始化。
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
static Map<String, FlutterBoostRouteFactory>
routerMap = {
'/': (settings, uniqueId) {
return PageRouteBuilder<dynamic>(
settings: settings, pageBuilder: (_, __, ___)
=> Container());
},
'embedded': (settings, uniqueId) {
return PageRouteBuilder<dynamic>(
settings: settings,
pageBuilder: (_, __, ___) =>
EmbeddedFirstRouteWidget());
},
'presentFlutterPage': (settings, uniqueId) {
return PageRouteBuilder<dynamic>(
settings: settings,
pageBuilder: (_, __, ___) =>
FlutterRouteWidget(
params: settings.arguments,
uniqueId: uniqueId,
));
}};
Route<dynamic> routeFactory(RouteSettings settings, String uniqueId) {
FlutterBoostRouteFactory func =routerMap[settings.name];
if (func == null) {
return null;
}
return func(settings, uniqueId);
}
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return FlutterBoostApp(
routeFactory
);
}
當(dāng)然,還可以監(jiān)聽頁(yè)面的生命周期,如下所示。
class SimpleWidget extends StatefulWidget {
final Map params;
final String messages;
final String uniqueId;
const SimpleWidget(this.uniqueId, this.params, this.messages);
@override
_SimpleWidgetState createState() => _SimpleWidgetState();
}
class _SimpleWidgetState extends State<SimpleWidget>
with PageVisibilityObserver {
static const String _kTag = 'xlog';
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('$_kTag#didChangeDependencies, ${widget.uniqueId}, $this');
}
@override
void initState() {
super.initState();
PageVisibilityBinding.instance.addObserver(this, ModalRoute.of(context));
print('$_kTag#initState, ${widget.uniqueId}, $this');
}
@override
void dispose() {
PageVisibilityBinding.instance.removeObserver(this);
print('$_kTag#dispose, ${widget.uniqueId}, $this');
super.dispose();
}
@override
void onForeground() {
print('$_kTag#onForeground, ${widget.uniqueId}, $this');
}
@override
void onBackground() {
print('$_kTag#onBackground, ${widget.uniqueId}, $this');
}
@override
void onAppear(ChangeReason reason) {
print('$_kTag#onAppear, ${widget.uniqueId}, $reason, $this');
}
void onDisappear(ChangeReason reason) {
print('$_kTag#onDisappear, ${widget.uniqueId}, $reason, $this');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('tab_example'),
),
body: SingleChildScrollView(
physics: BouncingScrollPhysics(),
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
margin: const EdgeInsets.only(top: 80.0),
child: Text(
widget.messages,
style: TextStyle(fontSize: 28.0, color: Colors.blue),
),
alignment: AlignmentDirectional.center,
),
Container(
margin: const EdgeInsets.only(top: 32.0),
child: Text(
widget.uniqueId,
style: TextStyle(fontSize: 22.0, color: Colors.red),
),
alignment: AlignmentDirectional.center,
),
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(30.0),
color: Colors.yellow,
child: Text(
'open flutter page',
style: TextStyle(fontSize: 22.0, color: Colors.black),
)),
onTap: () => BoostNavigator.of().push("flutterPage",
arguments: <String, String>{'from': widget.uniqueId}),
)
Container(
height: 300,
width: 200,
child: Text(
'',
style: TextStyle(fontSize: 22.0, color: Colors.black),
),
)
],
))),
);
}
}
然后,運(yùn)行項(xiàng)目,就可以從原生頁(yè)面跳轉(zhuǎn)到Flutter頁(yè)面,如下圖所示效果。

2.2 iOS集成
和Android的集成步驟一樣,使用Xcode打開原生iOS工程,然后在iOS的AppDelegate文件中初始化Flutter Boost ,如下所示。
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
MyFlutterBoostDelegate* delegate=[[MyFlutterBoostDelegate alloc ] init];
[[FlutterBoost instance] setup:application delegate:delegate callback:^(FlutterEngine *engine) {
} ];
return YES;
}
@end
下面是自定義的FlutterBoostDelegate的代碼,如下所示。
@interface MyFlutterBoostDelegate : NSObject<FlutterBoostDelegate>
@property (nonatomic,strong) UINavigationController *navigationController;
@end
@implementation MyFlutterBoostDelegate
- (void) pushNativeRoute:(FBCommonParams*) params{
BOOL animated = [params.arguments[@"animated"] boolValue];
BOOL present= [params.arguments[@"present"] boolValue];
UIViewControllerDemo *nvc = [[UIViewControllerDemo alloc] initWithNibName:@"UIViewControllerDemo" bundle:[NSBundle mainBundle]];
if(present){
[self.navigationController presentViewController:nvc animated:animated completion:^{
}];
}else{
[self.navigationController pushViewController:nvc animated:animated];
}
}
- (void) pushFlutterRoute:(FBCommonParams*)params {
FlutterEngine* engine = [[FlutterBoost instance ] getEngine];
engine.viewController = nil;
FBFlutterViewContainer *vc = FBFlutterViewContainer.new ;
[vc setName:params.pageName params:params.arguments];
BOOL animated = [params.arguments[@"animated"] boolValue];
BOOL present= [params.arguments[@"present"] boolValue];
if(present){
[self.navigationController presentViewController:vc animated:animated completion:^{
}];
}else{
[self.navigationController pushViewController:vc animated:animated];
}
}
- (void) popRoute:(FBCommonParams*)params
result:(NSDictionary *)result{
FBFlutterViewContainer *vc = (id)self.navigationController.presentedViewController;
if([vc isKindOfClass:FBFlutterViewContainer.class] && [vc.uniqueIDString isEqual: params.uniqueId]){
[vc dismissViewControllerAnimated:YES completion:^{}];
}else{
[self.navigationController popViewControllerAnimated:YES];
}
}
@end
如果要在原生iOS代碼中打開或關(guān)閉Flutter頁(yè)面,可以使用下面的方式。
[[FlutterBoost instance] open:@"flutterPage" arguments:@{@"animated":@(YES)} ];
[[FlutterBoost instance] open:@"secondStateful" arguments:@{@"present":@(YES)}];
三、Flutter Boost架構(gòu)
對(duì)于混合工程來說,原生端和Flutter端對(duì)于頁(yè)面的定義是不一樣的。對(duì)于原生端而言,頁(yè)面通常指的是一個(gè)ViewController或者Activity,而對(duì)于Flutter來說,頁(yè)面通常指的是Flutter組件。FlutterBoost框架所要做的就是統(tǒng)一混合工程中頁(yè)面的概念,或者說弱化Flutter組件對(duì)應(yīng)容器頁(yè)面的概念。換句話說,當(dāng)有一個(gè)原生頁(yè)面存在的時(shí)候,F(xiàn)lutteBoost就能保證一定有一個(gè)對(duì)應(yīng)的Flutter的容器頁(yè)面存在。
FlutterBoost框架其實(shí)就是由原生容器通過消息驅(qū)動(dòng)Flutter頁(yè)面容器,從而達(dá)到原生容器與Flutter容器同步的目的,而Flutter渲染的內(nèi)容是由原生容器去驅(qū)動(dòng)的,下面是Flutter Boost 給的一個(gè)Flutter Boost 的架構(gòu)示意圖。

可以看到,F(xiàn)lutter Boost插件分為平臺(tái)和Dart兩端,中間通過Message Channel連接。平臺(tái)側(cè)提供了Flutter引擎的配置和管理、Native容器的創(chuàng)建/銷毀、頁(yè)面可見性變化通知,以及Flutter頁(yè)面的打開/關(guān)閉接口等。而Dart側(cè)除了提供類似原生Navigator的頁(yè)面導(dǎo)航接口的能力外,還負(fù)責(zé)Flutter頁(yè)面的路由管理。
總的來說,正是基于共享同一個(gè)引擎的方案,使得FlutterBoost框架有效的解決了多引擎的問題。簡(jiǎn)單來說,F(xiàn)lutterBoost在Dart端引入了容器的概念,當(dāng)存在多個(gè)Flutter頁(yè)面時(shí),F(xiàn)lutterBoost不需要再用棧的結(jié)構(gòu)去維護(hù)現(xiàn)有頁(yè)面,而是使用扁平化鍵值對(duì)映射的形式去維護(hù)當(dāng)前所有的頁(yè)面,并且每個(gè)頁(yè)面擁有一個(gè)唯一的id
四、FlutterBoost3.0更新
4.1 不入侵引擎
為了解決官方引擎復(fù)用引起的問題,F(xiàn)lutterBoost2.0拷貝了Flutter引擎Embedding層的一些代碼進(jìn)行改造,這使得后期的升級(jí)成本極高。而FlutterBoost3.0采用繼承的方式擴(kuò)展FlutterActivity/FlutterFragment等組件的能力,并且通過在適當(dāng)時(shí)機(jī)給Dart側(cè)發(fā)送appIsResumed消息解決引擎復(fù)用時(shí)生命周期事件錯(cuò)亂導(dǎo)致的頁(yè)面卡死問題,并且,F(xiàn)lutterBoost 3.0 也兼容最新的官方發(fā)布的 Flutter 2.0。
4.2 不區(qū)分Androidx和Support分支
FlutterBoost2.0通過自己實(shí)現(xiàn)FlutterActivityAndFragmentDelegate.Host接口來擴(kuò)展FlutterActivity和FlutterFragment的能力,而getLifecycle是必須實(shí)現(xiàn)的接口,這就導(dǎo)致對(duì)androidx的依賴。這也是為什么FlutterBoostView的實(shí)現(xiàn)沒有被放入FlutterBoost3.0插件中的原因。而FlutterBoost3.0通過繼承的方式擴(kuò)展FlutterActivity/FlutterFragment的能力的額外收益就是,可以做到不依賴androidx。
4.3 雙端設(shè)計(jì)統(tǒng)一,接口統(tǒng)一
很多Flutter開發(fā)者只會(huì)一端,只會(huì)Android 或者只會(huì)IOS,但他需要接入雙端,所以雙端統(tǒng)一能降低他的 學(xué)習(xí)成本和接入成本。FlutterBoost3.0,在設(shè)計(jì)上 Android和IOS都做了對(duì)齊,特別接口上做到了參數(shù)級(jí)的對(duì)齊。
4.4 支持 【打開flutter頁(yè)面不再打開容器】 場(chǎng)景
在Flutter模塊內(nèi)部,F(xiàn)lutter 頁(yè)面跳轉(zhuǎn)Flutter 頁(yè)面是可以不需要再打開Flutter容器的,不打開容器,能節(jié)省內(nèi)存開銷。在FlutterBoost3.0上,打開容器和不打開容器的區(qū)別表現(xiàn)在用戶接口上僅僅是withContainer參數(shù)是否為true就好。
InkWell(
child: Container(
color: Colors.yellow,
child: Text(
'打開外部路由',
style: TextStyle(fontSize: 22.0, color: Colors.black),
)),
onTap: () => BoostNavigator.of().push("flutterPage",
arguments: <String, String>{'from': widget.uniqueId}),
),
InkWell(
child: Container(
color: Colors.yellow,
child: Text(
'打開內(nèi)部路由',
style: TextStyle(fontSize: 22.0, color: Colors.black),
)),
onTap: () => BoostNavigator.of().push("flutterPage",
withContainer: true,
arguments: <String, String>{'from': widget.uniqueId}),
)
4.5 生命周期的精準(zhǔn)通知
在FlutterBoost2.0上,每個(gè)頁(yè)面都會(huì)收到頁(yè)面生命周期通知,而FlutterBoost3.0只會(huì)通知頁(yè)面可見性實(shí)際發(fā)生了變化的頁(yè)面,接口也更符合flutter的設(shè)計(jì)。
4.6 其他Issue
除了上面的一些特性外,F(xiàn)lutter Boost 3.0版本還解決了如下一些問題:
- 頁(yè)面關(guān)閉后參數(shù)的傳遞,之前只有iOS支持,android不支持,目前在dart側(cè)實(shí)現(xiàn),Ios 和Android 都支持。
- 解決了Android 狀態(tài)欄字體和顏色問題。
- 解決了頁(yè)面回退willpopscope不起作用問題。
- 解決了不在棧頂?shù)捻?yè)面也收到生命周期回調(diào)的問題
- 解決了多次setState耗性能問題。
- 提供了Framgent 多種接入方式的Demo,方便tab 場(chǎng)景的接入。
- 生命周期的回調(diào)代碼,可以用戶代碼里面with的方式接入,使用更簡(jiǎn)單。
- 全面簡(jiǎn)化了,接入成本,包括 dart側(cè),android側(cè)和ios
- 豐富了demo,包含了基本場(chǎng)景,方便用戶接入 和測(cè)試回歸
到此這篇關(guān)于Flutter Boost 混合開發(fā)框架的文章就介紹到這了,更多相關(guān)Flutter Boost內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!,希望大家以后多多支持腳本之家!
相關(guān)文章
iOS 開發(fā)中 NavigationController經(jīng)常出現(xiàn)的問題原因分析
這篇文章主要介紹了iOS 開發(fā)中 NavigationController經(jīng)常出現(xiàn)的問題原因分析的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09
iOS實(shí)現(xiàn)微信/QQ顯示最近拍攝圖片的功能實(shí)例代碼
如果你剛剛拍攝了圖片,在使用微信/QQ發(fā)生消息時(shí)會(huì)顯示“你可能要發(fā)送的圖片”,這個(gè)功能非常人性化,怎么實(shí)現(xiàn)的呢?下面小編給大家分享iOS實(shí)現(xiàn)微信/QQ顯示最近拍攝圖片的功能實(shí)例代碼,一起看看吧2017-03-03
ios利用 AFN 上傳相冊(cè)或者拍照?qǐng)D片
這篇文章主要介紹了ios利用 AFN 上傳相冊(cè)或者拍照?qǐng)D片的相關(guān)資料,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-06-06
詳解iOS應(yīng)用使用Storyboard布局時(shí)的IBOutlet與IBAction
這篇文章主要介紹了iOS應(yīng)用使用Storyboard布局時(shí)的IBOutlet與IBAction,文中還附帶講解了為什么IBOutlet屬性是weak的,需要的朋友可以參考下2016-04-04
詳談iOS 位置權(quán)限彈出框閃現(xiàn)的問題
下面小編就為大家?guī)硪黄斦刬OS 位置權(quán)限彈出框閃現(xiàn)的問題。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-04-04
iOS實(shí)現(xiàn)啟動(dòng)引導(dǎo)頁(yè)與指紋解鎖的方法詳解
這篇文章主要給大家介紹了關(guān)于iOS實(shí)現(xiàn)啟動(dòng)引導(dǎo)頁(yè)與指紋解鎖的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-02-02
詳解iOS開發(fā) - 用AFNetworking實(shí)現(xiàn)https單向驗(yàn)證,雙向驗(yàn)證
這篇文章主要介紹了詳解iOS開發(fā) - 用AFNetworking實(shí)現(xiàn)https單向驗(yàn)證,雙向驗(yàn)證,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2016-12-12

