一、背景
技术栈前提——原生 Webview + h5 实现方案, 启动页的开发是APP区分h5很重要的东西,也是很有必要的, 能有效减少用户的等待时间, 因为套壳Webview的APP,不仅要花时间在壳的初始化启动上,还有一部分时间花在加载网页上,所以不可避免的会打开比较慢, 容易产生白屏, 对于用户来说是非常不友好的,下面将对安卓和ios上的启动页开发以及其中遇到的问题进行梳理整理,主要解决的问题有端的启动页开发, webview加载完成取消启动页的,国行ios在首次打开时需要用户网络授权问题,ios 长时间后台打开产生白屏的问题
初版启动页开发及问题
第一版开发主要是实现功能, 启动页的出现时间设置死了,问题有两个, 无法在启动页展示的同时加载内容也就是webview的内容, 当启动页关闭后, 还要花些时间渲染,这段时间容易产生白屏问题, 尤其是ios 第一次用户下载时, 第二个问题是,时间设置死无法动态调节, 有时候白屏,有时候浪费时间等待
二、ios 启动页开发
1、新建启动页视图
创建和初始化一个新的视图对象,加载一张启动页图片
SplashView.m 文件
#import "SplashView.h"
@interface SplashView()
@property(nonatomic, strong, readwrite)UIButton *button;
@end
@implementation SplashView
-(instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if(self) {
self.image = [UIImage imageNamed:@"icon.bundle/splash.png"];
// 视图进行交互
self.userInteractionEnabled = YES;
}
return self;
}
#pragma mark -
-(void)_removeSplashView{
[self removeFromSuperview];
}
@end
2、管理启动页的展示销毁逻辑
新版本的OC项目已经将AppDelegate中的一部分视图功能分离到了SceneDelegate中了,所以一些查到的资料可能已经不适用了
SceneDelegate.m 文件
// 引入SplashView.h 文件
#import "SplashView.h"
// 添加 SplashView 属性
@property (strong, nonatomic) SplashView *splashView;
在 willConnectToSession 生命周期中加载ViewController主视图(其中处理加载webview的逻辑), 然后同步添加 SplashView 启动页,在增加一个消息监听,当webview加载完成,通知关闭启动页
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
[self initWindowScreen: scene];
};
-(void)initWindowScreen :(UIScene *)scene {
UIWindowScene *windowScene = (UIWindowScene *)scene;
self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
ViewController *launchVC = [[ViewController alloc] init];
self.window.rootViewController = launchVC;
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
// 添加一个闪屏页面
[self showSplashView];
// 添加消息 注意在 dealloc 销毁时移除掉消息的监听
[[NSNotificationCenter defaultCenter] addObserver:self selector: @selector(handleRemoveSplashView) name:@"RemoveSplashViewNotification" object:nil];
}
处理showSplashView 闪屏页方法,初始化执行代码添加到 window 上, 设置 10s 后关闭(防止通知异常导致启动页无法关闭的情况)
- (void)showSplashView {
self.splashView = [[SplashView alloc] initWithFrame:self.window.bounds];
[self.window addSubview:self.splashView];
// 10秒后关闭这个页面(如果需要)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self handleRemoveSplashView];
});
}
移除启动页视图的方法, sceneDidDisconnect 断开链接的场景也移除启动页
- (void)handleRemoveSplashView {
// 从父视图中移除闪屏页面
[self.splashView removeFromSuperview];
self.splashView = nil; // 避免野指针,将引用置为 nil
};
- (void)sceneDidDisconnect:(UIScene *)scene {
[self handleRemoveSplashView];
}
在webView 的加载完成的方法中,通知移除启动页
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{
[[NSNotificationCenter defaultCenter] postNotificationName:@"RemoveSplashViewNotification" object:nil];
}
以上便可实现加载启动页,和webview加载完成取消启动页的逻辑,用户也不会感知白屏,下面是开发中遇到的一些问题的修复与记录
3、国行iphone 首次安装需要授权网络,否则无法加载webview,出现白屏
解决思路: 1、判断是否是首次启动 2、首次启动检测当前网络状态 3、有网络并加载webview 4、无网络,等待2秒后继续请求网络,等待用户同意授权
- (void)viewDidLoad {
[super viewDidLoad];
// 检查是否是首次安装
if ([self isFirstLaunch]) {
// 检查网络状态
[self checkNetworkStatus];
return;
}
[self setNotFirstLaunch];
}
3-1、 isFirstLaunch 方法判断是否是首次安装
通过检查用户默认设置中的一个标记来确定是否是第一次启动应用,并确保这个检查只在应用的生命周期中执行一次
- (BOOL)isFirstLaunch {
static BOOL hasLaunchedBefore = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
hasLaunchedBefore = [[NSUserDefaults standardUserDefaults] boolForKey:@"hasLaunchedBefore"];
});
return !hasLaunchedBefore;
}
3-2、checkNetworkStatus 方法检查当前网络状态
创建SCNetworkReachabilityRef 对象,通过baidu域名检查当前网络可达性,网络可达并且不需要链接则加载 webview 否则,每两秒检测网络状态
// 判断网络状态
- (void)checkNetworkStatus {
SCNetworkReachabilityRef reachabilityRef = SCNetworkReachabilityCreateWithName(NULL, "www.baidu.com");
if (reachabilityRef != NULL) {
SCNetworkReachabilityFlags flags;
// 变量用于表示是否成功获取了网络状态标志
BOOL didReach = SCNetworkReachabilityGetFlags(reachabilityRef, &flags);
// 释放 reachabilityRef 对象
CFRelease(reachabilityRef);
reachabilityRef = NULL;
if (didReach) {
// 网络是否可达
BOOL isReachable = ((flags & kSCNetworkFlagsReachable) != 0);
// 网络需要链接
BOOL needsConnection = ((flags & kSCNetworkFlagsConnectionRequired) != 0);
// 这行代码检查网络是否可达且不需要建立连接,如果是,则表示设备有有效的网络连接
if (isReachable && !needsConnection) {
// 有网络,加载WebView
[self loadWebView];
} else {
// 无网络,等待2秒后继续请求网络
[NSTimer scheduledTimerWithTimeInterval:2.0
target:self
selector: @selector(checkNetworkStatus)
userInfo:nil
repeats:NO];
}
} else {
NSLog(@"无法判断网络状态,可能是权限问题或其他原因");
// 无法判断网络状态,可能是权限问题或其他原因
exit(0);
}
}
}
3-3、setNotFirstLaunch 当不是首次时更新标志加载webview
- (void)setNotFirstLaunch {
[self loadWebView];
// 用户偏好设置 hasLaunchedBefore 为true 表示已经加载过
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"hasLaunchedBefore"];
// 调用 synchronize 方法来同步用户偏好设置的存储 将状态同步到磁盘
[[NSUserDefaults standardUserDefaults] synchronize];
}
3-4、当webview加载出现异常,继续进入网络状态的检查中,重新加载webview
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
// 页面加载失败时调用
NSLog(@"页面加载失败: %@", error.localizedDescription);
if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == -1009) {
[self checkNetworkStatus];
}
}
4、部分机型长时间后台唤起白屏
4-1、由于内存回收,webview 崩溃
// 内存占用过大,页面即将白屏
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0)){
[self.webView reload];
};
4-2、当webview加载出现异常,继续进入网络状态的检查中,重新加载webview
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
// 页面加载失败时调用
NSLog(@"页面加载失败: %@", error.localizedDescription);
if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == -1009) {
[self checkNetworkStatus];
}
}
4-3、 通过后台时间手动重新重启启动页和webview
1、即将从前台进入后台时记录当前时间
- (void)sceneWillResignActive:(UIScene *)scene {
// 记录应用即将进入后台的时间
self.backgroundTime = [NSDate date];
}
2、即将从后台进入前台时检查是否超过了规定的时间
- (void)sceneWillEnterForeground:(UIScene *)scene {
NSTimeInterval timeInBackground = [[NSDate date] timeIntervalSinceDate:self.backgroundTime];
BOOL shouldShowSplash = timeInBackground > 600.0; // 10分钟
if (shouldShowSplash && !self.splashView.superview) {
// 重新初始化视图
[self initWindowScreen: scene];
}
}
三、android 启动页开发
实现思路, android 的启动页逻辑和 ios 的设计思路有些不一样, android 上新建了启动页的视图 SpashActivity, 然后在启动页视图上加载了一个activity_splash 的 layout 布局设置了当前的闪屏页的图片和文字, 开始由于是写死的时间在打开 MainActivity ,于是加载webview的时候会出现很长时间白屏, 并且不是同时加载的, 为了解决这个问题,于是在 SpashActivity 上就开始加载 webview, 当webview 执行完成,导航到 MainActivity 上
1、启动页视图中加载webview成功后跳转到 MainActivity中
SplashActivity.kt 文件
// 创建单例
import com.kalodata.kalodata_android.utils.WebViewSingleton
//...
@SuppressLint("CustomSplashScreen")
class SplashActivity : ComponentActivity() {
private lateinit var webView: WebView
private var hasFinished = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
webView = WebViewSingleton.getInstance(this).webView
webView.loadUrl("https://www.baidu.com/")
webView?.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
Handler().postDelayed( {
// 检查标志位,如果已经执行过跳转,则不执行 保证执行一次
if (!hasFinished) {
hasFinished = true // 设置标志位为已执行
startActivity(Intent(this@SplashActivity, MainActivity::class.java))
}
finish()
} ,1000)
}
}
}
}
通过单例模式 设计 WebViewSingleton.java
package com.kalodata.kalodata_android.utils;
import android.content.Context;
import android.webkit.WebView;
public class WebViewSingleton {
private static WebViewSingleton instance;
private WebView webView;
// 存储Context,这里使用Application Context作为示例
private Context context;
// 私有构造函数,接收一个Context参数
private WebViewSingleton(Context ctx) {
this.context = ctx; // 保存Context供后续使用
initializeWebView();
}
// 初始化WebView的方法
private void initializeWebView() {
webView = new WebView(context); // 使用提供的Context创建WebView
webView.getSettings().setJavaScriptEnabled(true); // 根据需要设置WebView
webView.getSettings().setDomStorageEnabled(true); // 支持 JavaScript
}
// 静态方法,用于获取单例对象
public static synchronized WebViewSingleton getInstance(Context ctx) {
if (instance == null) {
instance = new WebViewSingleton(ctx); // 使用传入的Context创建实例
}
return instance;
}
// 获取WebView的公共方法
public WebView getWebView() {
return webView;
}
}
AndroidManifest.xml
<!-- 添加网络权限-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
// ...
<application
android:networkSecurityConfig="@xml/network_security_config"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.xxx_android"
tools:targetApi="31" >
<activity android:name=".SplashActivity"
android:exported="true"
android:launchMode="standard"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".MainActivity"
android:exported="true"
android:launchMode="standard">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="kalodata"/>
</intent-filter>
</activity>
</application>
2、MainActivity中加载要真正要展示的webview
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// webview
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webview)
webView.settings?.javaScriptEnabled = true // 支持 JavaScript
// 设置是否启用 DOM 存储
// DOM 存储是一种在 Web 应用程序中存储数据的机制,它使用 JavaScript 对象和属性来存储和检索数据
webView.settings?.domStorageEnabled = true
// 设置 WebView 是否可以获取焦点 ( 自选 非必要 )
webView?.isFocusable = true
// 设置 WebView 中的滚动条样式 ( 自选 非必要 )
// SCROLLBARS_INSIDE_OVERLAY - 在内容上覆盖滚动条 ( 默认 )
webView?.scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
// 设置页面自适应
// Viewport 元标记是指在 HTML 页面中的 <meta> 标签 , 可以设置网页在移动端设备上的显示方式和缩放比例
// 设置是否支持 Viewport 元标记的宽度
webView.settings?.useWideViewPort = true
// WebViewClient 是一个用于处理 WebView 页面加载事件的类
webView?.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
// 检查链接是否是 mailto 类型
if (url.startsWith("mailto:")) {
return true; // 返回 true 以拦截链接,不让它打开浏览器
}
// 其他链接可以正常处理
return super.shouldOverrideUrlLoading(view, url);
}
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
// 页面加载完成后可以在这里执行一些操作,例如调用JavaScript函数
}
}
}
Webview 视图容器
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
启动页视图编辑
activity_splash.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/splash_image"
android:scaleType="centerCrop" />
</LinearLayout>