APP 端接入firebase实现消息推送功能

2,612 阅读8分钟

一、背景

消息推送基本是每个app 都会实现的功能了,为了能及时接收消息展示给用户,提供服务器主动与用户相互的功能, 本文将对 ios 和 android 端接入 firebase 实现消息推送进行记录

二、接入 Firebase

firebase 链接

对于出海应用,首当其冲的就是接入 Firebase, 因为他的远程消息推送是免费的, 可以同时满足ios 和 android 接入的需要, 同时又是谷歌的亲儿子, 对于上架谷歌市场也是非常友好的。

三、消息推送的分类

3-1、本地推送

  • 由本地应用出发(闹铃/待办事项)
  • 无需网络数据,提供和远程推送统一的交互

3-2、远程推送

  • 通过工具、api 接入,通过发送消息的方式,通知到具体设备展示消息内容,以下的消息推送,主要是讲的远程消息推送

三、ios 接入消息推送能力

3-1、远程消息推送 APNS

ios 的远程消息推送,主要又以下部分完成, 下面是各部分对应的一些功能

image.png

  • UNNoticationContent 消息内容

    • 推送内容
    • 标题/副标题/提醒/声音。。。
  • UNNotification Trigger 触发时机

    • UNPishNotification Trigger
    • UNTimeIntercalNotification Trigger
    • UNCalendarNotification Trigger
    • UNLocation Notification Trigger
  • UNNotificationRequest

    • 封装 Content 和 Trigger 为统一格式
    • 交给 NotificationCenter 处理
  • UNUserNotificationCenter

    • 处理推送权限
    • 接收和移除NotificationRequest
  • UNUserNotificationCenerDelegate 消息推送一些 delegate 触发方法

    • 即将展示推送的内容
    • 用户进行交互行为的处理

3-2、实现远程消息推送能力

3-2-1、添加消息推送能力

image.png

3-2-2、新建 GTNotification 消息推送内容的管理

1、 通过单例模式实现 notificationManager 类

GTNotification.m

+ (GTNotification *)notificationManager{
   static GTNotification *manager;
   static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
            manager = [[GTNotification alloc] init];
    });
    return manager;
}

2、暴露checkNotificationAuthorization方法,检查当前是否有推送消息权限 GTNotification.h

/**
 APP 推送管理
 */
+ (GTNotification *)notificationManager;
- (void)checkNotificationAuthorization;

3、实现权限检查

- (void)checkNotificationAuthorization{
    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
    center.delegate = self;
    
    [center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error) {
            if(granted){
                NSLog(@"Notification permission granted.");
                // 模拟本地消息推送
                [self _pushLocalNotification];
            }else{
                NSLog(@"Notification permission denied.");
            }
    }];
};

当用户同意授权时,测试、实现一个本地消息推送

4、实现向本地发送消息 _pushLocalNotification

- (void)_pushLocalNotification{
    UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
    content.badge = @(1);
    content.title = @"极客时间";
    content.body = @"从0开发一款IOS APP";
    UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:10.f repeats:NO];
    UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"_pushLocalNotification" content:content trigger: trigger];
    [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
        NSLog(@"");
    }];
}

6、通过 delegate 设置收到的消息情况, 这里将会处理消息展示的类型, 以及点击消息,打开消息后接收的参数处理

@interface GTNotification()<UNUserNotificationCenterDelegate>

- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
    // completionHandler(UNNotificationPresentationOptionAlert);
    // 使用 .banner 和 .sound 代替 .alert
//    completionHandler(UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionSound);
    // 检查当前系统版本是否支持 UNNotificationPresentationOptionBanner
     if (@available(iOS 14.0, *)) {
         // 在 iOS 14.0 及以上版本中,使用 UNNotificationPresentationOptionBanner
         completionHandler(UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionSound);
     } else {
         // 在 iOS 14.0 以下的版本中,使用 UNNotificationPresentationOptionAlert
         completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound);
     }
};

// The method will be called on the delegate when the user responded to the notification by opening the  application, dismissing the notification or choosing a UNNotificationAction. The delegate must be set before the application returns from application:didFinishLaunchingWithOptions:.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler{
    // 处理业务逻辑
    // 获取通知的内容
       UNNotificationContent *content = response.notification.request.content;
    NSLog(@"custom-path: %@", content.title);
    NSLog(@"custom-badge: %@", content.badge);
    NSLog(@"custom-sound: %@", content.sound);
    NSLog(@"custom-description: %@", content.description);
    // 根据用户当前的badge数量减一
    if(content.badge>0){
        [UIApplication sharedApplication].applicationIconBadgeNumber -=1;
    }
    
    // 获取自定义参数,例如 "custom"
    if (content.userInfo[@"path"]) {
        [[[ViewController alloc] init] reloadPage: content.userInfo[@"path"]];
    }
    completionHandler();
};

7、实现远程消息推送

实现远程消息推送, 在ios端, 可以用apple 提供的平台, icloud.developer.apple.com/dashboard/n…

消息格式为下面这样

{
    "aps": {
        "alert": {
            "title": "title121212",
            "subtitle": "subtitle1212",
            "body": "body121212"
        },
        "badge": 1
    },
    "custom-path": "/login",
    "custom-params": "tc22222"
}

虽然用apple 提供的远程消息推送也能实现,但是可以看出来, 不是很灵活,而且不是全平台的, 我们接下来接入 firebase 通过第三方的方式实现

8、接入 firebase 实现消息通知

链接

在控制台新建项目, 将apple 相关appid,然后证书正的开发者证书,和生产环境推送证书分别添加进来即可。 然后将 GoogleService-info.plist 文件下载到项目中

添加firebase消息库

  pod 'Firebase/Core'
  pod 'Firebase/Messaging'

pod install

9、计入firebase 初始化

在 入口的 AppDelegate.m 中实现初始化 firebase 和实现初始化成功和失败的方法

#import "GTNotification.h"
#import <Firebase.h>
#import <FirebaseMessaging/FirebaseMessaging.h>

// 当应用成功注册远程通知时调用
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    NSLog(@"Device Token1: %@", deviceToken);
    // 将 APNs 设备令牌传递给 Firebase
    [FIRMessaging messaging].APNSToken = deviceToken;
    
    // 获取fcp代码
    [[FIRMessaging messaging] tokenWithCompletion:^(NSString *token, NSError *error) {
      if (error != nil) {
        NSLog(@"Error getting FCM registration token: %@", error);
      } else {
        NSLog(@"FCM registration token: %@", token);
       [[NSUserDefaults standardUserDefaults] setObject:token forKey:@"fcmToken"];
      }
    }];
};

// 当应用注册远程通知失败时调用
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    // 处理注册失败的情况
    NSLog(@"Failed to register for remote notifications: %@", error);
    
}

通过调用 [[FIRMessaging messaging] tokenWithCompletion:^(NSString *token, NSError *error) 获取到 FCM Token 后, 将其进行保存, 我是保存到了 fcmToken 中

10、在控制台进行测试发送消息

image.png

在新建宣传活动中即可进行新建消息,和进行测试

image.png

测试这里便可以将上面获取到的fcmtoken 输入, 进行测试发送,会在当前设备获取到消息,这时便可以实现通过firebase 向ios 发送消息的功能了

11、开发中遇到的问题 在ios 开发中,由于我们时 通过webview实现的套壳app, 当点击消息唤起app后, 向webview 内部的h5 传递消息,发现无法主动推送成功, 通过清理缓存,各种手段都无法解决, 后面发现只要当app 进入后台,不断开任务即可在热启动app 时,可以推送消息到 h5 端, 所以解决方式改为了, 执行webview 成功后, 立马启动一个定时器, 每隔1s 轮训当前存储状态中的参数, 轮训到了立马发送到h5端, 然后清楚存储的内容

四、 android 接入 firebase

4-1、开启权限、添加服务

AndroidManifest.xml

  <!-- 消息通知 -->
  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
  
  
   <service
        android:name=".MyFirebaseMessagingService"
        android:directBootAware="true"
        android:exported="false">
        <intent-filter>
            <action android:name="com.google.firebase.MESSAGING_EVENT" />
        </intent-filter>
    </service>

4-2、MyFirebaseMessagingService 服务代码

class MyFirebaseMessagingService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        super.onNewToken(token)
        // 处理新的 FCM 令牌
        FCMTokenSingleton.token = token
    }

    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)
        FCMTokenSingleton.fcmPath = message.data["path"]
        // 例如,显示通知
        message.notification?.let {
            generateNotification(
                message.notification?.title,
                message.notification?.body,
                message.data["path"]
            )
        }
    }

    private fun generateNotification(title: String?, body: String?, path: String?) {
        //创建通知管理
        val notificationManager =
            getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        // 将额外参数挂在到intent上
        val intent = Intent(this, MainActivity::class.java) // 指定点击通知后要启动的Activity
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK


        // 附加自定义参数
        path?.let { intent.putExtra("path", it) }

        val pendingIntent = PendingIntent.getActivity(
            this, 0, intent, PendingIntent.FLAG_IMMUTABLE
        )

        //创建通知
        val notification = NotificationCompat.Builder(this, "channel_id")
            .setContentTitle(title)
            .setContentText(body)
            //.setTicker("通知来了")
            /**通知产生的时间,会在通知信息里显示**/
            .setWhen(System.currentTimeMillis())
            /**设置该通知优先级**/
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setSmallIcon(R.drawable.logo) // 替换为您的通知图标
            .setAutoCancel(true)
            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
            // .setNumber(999)
            // 通知上的操作
            // .addAction(R.drawable.logo, "去看看", pendingIntent)
            /**设置他为一个正在进行的通知。他们通常是用来表示一个后台任务,用户积极参与(如播放音乐)或以某种方式正在等待,因此占用设备(如一个文件下载,同步操作,主动网络连接)**/
            .setOngoing(false)
            .setContentIntent(pendingIntent)
            /**向通知添加声音、闪灯和振动效果的最简单、最一致的方式是使用当前的用户默认设置,使用defaults属性,可以组合:**/
            .setDefaults(NotificationCompat.DEFAULT_ALL)
            .build()

        //发送通知
        notificationManager.notify(Random.nextInt(), notification)
    }

    object FCMTokenSingleton {
        var token: String? = null
        var fcmPath:String? = null
    }
}

收到消息通知后, 建立通知消息,设置相关参数, 将token 和 fcmPath 保存起来,供后面使用

4-3、创建消息通道

this.createFireBase();

// 确保你为目标API级别26及以上的设备创建了通知渠道
fun createFireBase(){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val channel = NotificationChannel("channel_id", "channel_name", NotificationManager.IMPORTANCE_DEFAULT)
        channel.description = "Channel description"
        val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.createNotificationChannel(channel)
    }
}

4-4、webview 加载完成请求消息权限

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
override fun onPageFinished(view: WebView, url: String) {
    super.onPageFinished(view, url)
    // 获取通知权限
    checkAndRequestNotificationPermission()

}

//初始化
FirebaseApp.initializeApp(this)
FirebaseAnalytics.getInstance(this)
// 消息通知
NotificationUtils.createNotificationChannel(this)
}

请求通知权限,初始化消息通知

// 检查通知权限
fun checkAndRequestNotificationPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        // 对于Android 12(API级别31)及以上版本,使用新的权限请求方法
        if (!NotificationManagerCompat.getEnabledListenerPackages(this).contains(packageName)) {
            requestNotificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
        }
    } else {
        // 对于Android 12以下的版本,使用旧的权限请求方法
        if (ActivityCompat.checkSelfPermission(
                this, Manifest.permission.READ_EXTERNAL_STORAGE
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            requestNotificationPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
        }
    }
}

弹窗是否允许消息推送的授权弹窗,用户允许后,获取fcmtoken, 获取 path 参数,通知到 h5 端,进行下一步处理

private val requestNotificationPermissionLauncher =
    registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {
            // 权限被授予,你可以在这里执行需要通知权限的操作
            startNotificationService()

        } else {
            // 权限被拒绝,你可以提示用户或者禁用某些功能
            showPermissionDeniedMessage()
        }
    }

// 启动通知服务
private fun startNotificationService() {
    // 启动通知服务的代码
    Log.d("FIREBASE 启动通知服务的代码", "启动通知服务的代码")
    // 页面加载完成后可以在这里执行一些操作,例如调用JavaScript函数
    val token = MyFirebaseMessagingService.FCMTokenSingleton.token
    Log.d("FIREBASE 加载完成 token1", "FCM Token: $token")
    if(token!==null){
        callJsFromAndroid("channelMessage","fcmtokenValue",token)
        Log.d("FIREBASE token", "FCM Token: $token")
        MyFirebaseMessagingService.FCMTokenSingleton.token = null;
    }
    checkFcmNotificationMessage()
}

fun checkFcmNotificationMessage(){
    val fcmPath = MyFirebaseMessagingService.FCMTokenSingleton.fcmPath
    if(fcmPath!==null){
        callJsFromAndroid("channelMessage","showLoading","true")
        Handler().postDelayed({
            try {
                callJsFromAndroid("channelMessage","reloadPage",fcmPath)
                MyFirebaseMessagingService.FCMTokenSingleton.fcmPath= null;
                // 取消通知数量
                val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
                notificationManager.cancelAll()
            }catch (e:Exception) {
                Log.d("FIREBASE catch", e.toString())
            }finally {
                callJsFromAndroid("channelMessage","showLoading","false")
            }
        },1500)
    }
}


private fun showPermissionDeniedMessage() {
    // 显示权限被拒绝的消息
    Log.d("FIREBASE 显示权限被拒绝的消息", "显示权限被拒绝的消息")
    //  val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
    //  intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
    //  startActivity(intent)
    //   Log.d("FIREBASE 通知权限未启用", "请求用户授权")
}

4-5、NotificationUtils 类

实现createNotificationChannel消息通知设置, 当由于网络原因没有获取到fcm, 执行 getfcmToken 实现再次获取fcmtoken

object NotificationUtils {
    private const val CHANNEL_ID = "channel_id"
    private const val CHANNEL_NAME = "channel_name"
    fun createNotificationChannel(context: Context) {
        val channel = NotificationChannel(
            CHANNEL_ID,
            CHANNEL_NAME,
            NotificationManager.IMPORTANCE_HIGH,
        ).apply {
            description = "Description of your channel"
            enableLights(true)
            lightColor = Color.RED
            vibrationPattern = longArrayOf(0, 1000, 1000, 1000)
        }
        val notificationManager = context.getSystemService(NotificationManager::class.java)
        notificationManager.createNotificationChannel(channel)
    }

    fun getfcmToken(callback: (String) -> Unit){
            FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
            if (!task.isSuccessful) {
                Log.d("FIREBASE", "Fetching FCM registration token failed", task.exception)
                callback("error")
                return@OnCompleteListener
            }
            val token = task.result
            callback(token)
        })
    }
}

4-6、reloadFCMTOken

当 h5 端再次请求 fcmtoken

@JavascriptInterface
fun reloadFcmToken(params:String) {
    Log.d("FIREBASE", "reloadFcmToken")
    callJsFromAndroid("channelMessage","showLoading", "true")
    Handler().postDelayed({
        try {
            NotificationUtils.getfcmToken{ token ->
                if(token == "error"){
                    callJsFromAndroid("channelMessage","showToast","获取token失败")
                    callJsFromAndroid("channelMessage","showLoading", "false")
                }else{
                    // Token 获取成功的回调
                    MyFirebaseMessagingService.FCMTokenSingleton.token = token
                    startNotificationService()
                    callJsFromAndroid("channelMessage","reloadPage","/")
                    println("获取到 FCM Token: $token")
                }
            }
        }catch (e:Exception) {
            Log.d("FIREBASE exeddd", e.toString())
            callJsFromAndroid("channelMessage","showToast","获取token失败")
        }
    },2000)
}

延迟调用获取新的fcm token 发送到 h5 端