HarmonyOS开发:留存分析——用户留存

举报
Jack20 发表于 2026/06/25 20:49:57 2026/06/25
【摘要】 HarmonyOS开发:留存分析——用户留存📌 核心要点:留存率是衡量产品健康度的"体温计"——次日留存看产品第一印象,7日留存看功能价值,30日留存看长期粘性,留存曲线告诉你用户是在留下来还是在悄悄跑路。 背景与动机你花了10万推广费,拉来10000个新用户。一周后还剩多少?2000?1000?还是500?如果7日留存只有5%,意味着你花10万拉来的用户,一周后只剩500个。这500个...

HarmonyOS开发:留存分析——用户留存

📌 核心要点:留存率是衡量产品健康度的"体温计"——次日留存看产品第一印象,7日留存看功能价值,30日留存看长期粘性,留存曲线告诉你用户是在留下来还是在悄悄跑路。

背景与动机

你花了10万推广费,拉来10000个新用户。一周后还剩多少?2000?1000?还是500?

如果7日留存只有5%,意味着你花10万拉来的用户,一周后只剩500个。这500个用户能给你带来多少收入?够覆盖推广成本吗?

留存率低,说明你的产品留不住人。用户来了又走,你永远在"拉新→流失→再拉新"的死循环里打转。获客成本越来越高,利润越来越薄,最后就是慢性死亡。

留存分析要解决的核心问题:

  • 用户留没留:次日/7日/30日留存率是多少,趋势是上升还是下降
  • 谁留谁走:不同用户群体的留存差异,新用户vs老用户、付费vs免费
  • 为什么走:流失用户有什么共同特征,他们在流失前做了什么
  • 怎么留住:针对流失原因制定留存策略,提升用户粘性

鸿蒙应用的留存分析有其特殊性——用户可能在手机、平板、手表等多个设备上使用同一应用,留存率的计算需要考虑跨设备场景。

核心原理

留存分析的核心:定义留存 → 采集数据 → 计算留存率 → 分析留存曲线 → 制定留存策略

flowchart TB
    subgraph 定义留存
        A[确定留存定义<br/>登录/活跃/核心行为] --> B[选择留存周期<br/>次日/7/30]
        B --> C[划分用户群组<br/>按注册日期分组]
    end
    
    subgraph 数据采集
        C --> D[记录用户注册日]
        D --> E[记录每日活跃]
        E --> F[记录核心行为]
    end
    
    subgraph 计算分析
        F --> G[计算留存率]
        G --> H[绘制留存曲线]
        H --> I{曲线类型判断}
    end
    
    subgraph 策略输出
        I -->|平滑下降| J[正常衰减<br/>优化核心体验]
        I -->|骤降后平稳| K[找到Aha时刻<br/>引导用户到达]
        I -->|持续下降| L[产品价值不足<br/>重新审视功能]
        J --> M[留存优化策略]
        K --> M
        L --> M
    end
    
    classDef define fill:#6C5CE7,stroke:#5B4BC9,color:#fff
    classDef collect fill:#00B894,stroke:#00A383,color:#fff
    classDef analyze fill:#FDCB6E,stroke:#F0B429,color:#333
    classDef decision fill:#FF7675,stroke:#D63031,color:#fff
    classDef strategy fill:#74B9FF,stroke:#0984E3,color:#fff
    
    class A,B,C define
    class D,E,F collect
    class G,H,I analyze
    class J,K,L decision
    class M strategy

留存率计算方法:

留存类型 计算公式 说明
次日留存 第2天回访用户数 / 新增用户数 × 100% 衡量产品第一印象
7日留存 第7天回访用户数 / 新增用户数 × 100% 衡量功能价值
30日留存 第30天回访用户数 / 新增用户数 × 100% 衡量长期粘性
N日留存 第N天回访用户数 / 新增用户数 × 100% 通用公式
滚动留存 连续N天活跃用户数 / 新增用户数 × 100% 更严格的留存定义

代码实战

基础用法:留存数据采集

先采集留存计算所需的基础数据——用户注册时间和每日活跃记录。

// RetentionCollector.ets - 留存数据采集
import { analytics } from '@kit.AnalyticsKit';
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

// 用户留存记录
interface UserRetentionRecord {
  userId: string;
  registerDate: string;     // 注册日期 YYYY-MM-DD
  lastActiveDate: string;   // 最后活跃日期
  activeDates: string[];    // 所有活跃日期
  isPaidUser: boolean;      // 是否付费用户
  registerChannel: string;  // 注册渠道
}

export class RetentionCollector {
  private static instance: RetentionCollector;
  private pref: preferences.Preferences | null = null;
  private currentUserId: string = '';
  
  static getInstance(): RetentionCollector {
    if (!RetentionCollector.instance) {
      RetentionCollector.instance = new RetentionCollector();
    }
    return RetentionCollector.instance;
  }
  
  async init(context: common.UIAbilityContext): Promise<void> {
    this.pref = await preferences.getPreferences(context, 'retention_data');
  }
  
  // 记录用户注册
  async onUserRegister(userId: string, channel: string): Promise<void> {
    if (!this.pref) return;
    
    this.currentUserId = userId;
    const today = this.getToday();
    
    const record: UserRetentionRecord = {
      userId: userId,
      registerDate: today,
      lastActiveDate: today,
      activeDates: [today],
      isPaidUser: false,
      registerChannel: channel,
    };
    
    // 保存用户留存记录
    await this.pref.put(`user_${userId}`, JSON.stringify(record));
    
    // 更新注册日期索引
    const dateUsersStr = await this.pref.get(`date_${today}`, '[]') as string;
    const dateUsers: string[] = JSON.parse(dateUsersStr);
    dateUsers.push(userId);
    await this.pref.put(`date_${today}`, JSON.stringify(dateUsers));
    
    await this.pref.flush();
    
    // 上报注册事件
    analytics.reportEvent('user_register', {
      'user_id': userId,
      'register_date': today,
      'register_channel': channel,
    });
    
    console.info(`[Retention] 用户注册: ${userId}, 渠道: ${channel}`);
  }
  
  // 记录用户活跃——每次启动调用
  async onUserActive(userId: string): Promise<void> {
    if (!this.pref) return;
    
    this.currentUserId = userId;
    const today = this.getToday();
    
    // 获取用户记录
    const recordStr = await this.pref.get(`user_${userId}`, '') as string;
    if (!recordStr) {
      // 老用户没有记录,创建一条
      await this.onUserRegister(userId, 'unknown');
      return;
    }
    
    const record: UserRetentionRecord = JSON.parse(recordStr);
    
    // 去重添加今日活跃
    if (!record.activeDates.includes(today)) {
      record.activeDates.push(today);
    }
    record.lastActiveDate = today;
    
    // 保存
    await this.pref.put(`user_${userId}`, JSON.stringify(record));
    
    // 更新活跃日期索引
    const dateUsersStr = await this.pref.get(`date_${today}`, '[]') as string;
    const dateUsers: string[] = JSON.parse(dateUsersStr);
    if (!dateUsers.includes(userId)) {
      dateUsers.push(userId);
      await this.pref.put(`date_${today}`, JSON.stringify(dateUsers));
    }
    
    await this.pref.flush();
    
    // 上报活跃事件
    analytics.reportEvent('user_active', {
      'user_id': userId,
      'active_date': today,
      'days_since_register': this.daysBetween(record.registerDate, today),
    });
  }
  
  // 更新付费状态
  async updatePaidStatus(userId: string, isPaid: boolean): Promise<void> {
    if (!this.pref) return;
    
    const recordStr = await this.pref.get(`user_${userId}`, '') as string;
    if (!recordStr) return;
    
    const record: UserRetentionRecord = JSON.parse(recordStr);
    record.isPaidUser = isPaid;
    
    await this.pref.put(`user_${userId}`, JSON.stringify(record));
    await this.pref.flush();
  }
  
  // ========== 工具方法 ==========
  
  private getToday(): string {
    return new Date().toISOString().split('T')[0];
  }
  
  private daysBetween(date1: string, date2: string): number {
    const d1 = new Date(date1);
    const d2 = new Date(date2);
    return Math.floor((d2.getTime() - d1.getTime()) / (1000 * 60 * 60 * 24));
  }
}

进阶用法:留存率计算

有了数据,就可以计算留存率了。

// RetentionCalculator.ets - 留存率计算
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { UserRetentionRecord } from './RetentionCollector';

// 留存率结果
export interface RetentionResult {
  cohortDate: string;           // 群组日期
  newUsers: number;             // 新增用户数
  retentionData: Map<number, {  // key=第N天
    retainedUsers: number;
    retentionRate: number;
  }>;
}

// 留存曲线类型
export enum RetentionCurveType {
  SMOOTH_DECAY = 'smooth_decay',     // 平滑衰减——正常
  SHARP_DROP_FLAT = 'sharp_drop_flat', // 骤降后平稳——有Aha时刻
  CONTINUOUS_DECLINE = 'continuous_decline', // 持续下降——产品价值不足
}

export class RetentionCalculator {
  private static instance: RetentionCalculator;
  private pref: preferences.Preferences | null = null;
  
  static getInstance(): RetentionCalculator {
    if (!RetentionCalculator.instance) {
      RetentionCalculator.instance = new RetentionCalculator();
    }
    return RetentionCalculator.instance;
  }
  
  async init(context: common.UIAbilityContext): Promise<void> {
    this.pref = await preferences.getPreferences(context, 'retention_data');
  }
  
  // 计算指定日期群组的留存率
  async calculateCohortRetention(
    cohortDate: string, 
    days: number[] = [1, 2, 3, 7, 14, 30]
  ): Promise<RetentionResult> {
    if (!this.pref) return this.emptyResult(cohortDate);
    
    // 获取该日期注册的所有用户
    const dateUsersStr = await this.pref.get(`date_${cohortDate}`, '[]') as string;
    const userIds: string[] = JSON.parse(dateUsersStr);
    
    const result: RetentionResult = {
      cohortDate: cohortDate,
      newUsers: userIds.length,
      retentionData: new Map(),
    };
    
    if (userIds.length === 0) return result;
    
    // 计算每个留存天数的留存率
    for (const day of days) {
      const targetDate = this.addDays(cohortDate, day);
      let retainedCount = 0;
      
      for (const userId of userIds) {
        const recordStr = await this.pref.get(`user_${userId}`, '') as string;
        if (!recordStr) continue;
        
        const record: UserRetentionRecord = JSON.parse(recordStr);
        if (record.activeDates.includes(targetDate)) {
          retainedCount++;
        }
      }
      
      result.retentionData.set(day, {
        retainedUsers: retainedCount,
        retentionRate: Math.round(retainedCount / userIds.length * 10000) / 100,
      });
    }
    
    return result;
  }
  
  // 计算最近N天的群组留存
  async calculateRecentRetention(
    recentDays: number = 7,
    retentionDays: number[] = [1, 7, 30]
  ): Promise<RetentionResult[]> {
    const results: RetentionResult[] = [];
    const today = new Date();
    
    for (let i = 0; i < recentDays; i++) {
      const date = new Date(today);
      date.setDate(date.getDate() - i);
      const dateStr = date.toISOString().split('T')[0];
      
      const result = await this.calculateCohortRetention(dateStr, retentionDays);
      results.push(result);
    }
    
    return results;
  }
  
  // 判断留存曲线类型
  analyzeCurveType(result: RetentionResult): RetentionCurveType {
    const day1 = result.retentionData.get(1)?.retentionRate || 0;
    const day7 = result.retentionData.get(7)?.retentionRate || 0;
    const day30 = result.retentionData.get(30)?.retentionRate || 0;
    
    // 骤降后平稳:次日到7日降幅>30%,7日到30日降幅<20%
    if (day1 > 0 && day7 > 0 && day30 > 0) {
      const earlyDrop = (day1 - day7) / day1;
      const lateDrop = (day7 - day30) / day7;
      
      if (earlyDrop > 0.3 && lateDrop < 0.2) {
        return RetentionCurveType.SHARP_DROP_FLAT;
      }
    }
    
    // 持续下降:每个阶段降幅都>20%
    if (day1 > 0 && day7 > 0 && day30 > 0) {
      const earlyDrop = (day1 - day7) / day1;
      const lateDrop = (day7 - day30) / day7;
      
      if (earlyDrop > 0.2 && lateDrop > 0.2) {
        return RetentionCurveType.CONTINUOUS_DECLINE;
      }
    }
    
    return RetentionCurveType.SMOOTH_DECAY;
  }
  
  // 打印留存结果
  printResult(result: RetentionResult): void {
    console.info(`\n========== 留存分析: ${result.cohortDate} ==========`);
    console.info(`新增用户: ${result.newUsers}`);
    
    const sortedDays = Array.from(result.retentionData.keys()).sort((a, b) => a - b);
    for (const day of sortedDays) {
      const data = result.retentionData.get(day)!;
      const bar = '█'.repeat(Math.round(data.retentionRate / 2));
      console.info(
        `${String(day).padStart(2, ' ')}日: ` +
        `${data.retainedUsers}` +
        `${data.retentionRate}% ${bar}`
      );
    }
    
    const curveType = this.analyzeCurveType(result);
    console.info(`曲线类型: ${curveType}`);
    console.info('========================================\n');
  }
  
  // ========== 工具方法 ==========
  
  private addDays(dateStr: string, days: number): string {
    const date = new Date(dateStr);
    date.setDate(date.getDate() + days);
    return date.toISOString().split('T')[0];
  }
  
  private emptyResult(cohortDate: string): RetentionResult {
    return {
      cohortDate: cohortDate,
      newUsers: 0,
      retentionData: new Map(),
    };
  }
}

完整示例:留存策略引擎

根据留存数据自动匹配优化策略。

// RetentionStrategy.ets - 留存策略引擎
import { analytics } from '@kit.AnalyticsKit';
import { RetentionCalculator, RetentionResult, RetentionCurveType } from './RetentionCalculator';
import { common } from '@kit.AbilityKit';

// 留存问题类型
export enum RetentionIssue {
  LOW_DAY1 = 'low_day1',               // 次日留存低
  LOW_DAY7 = 'low_day7',               // 7日留存低
  LOW_DAY30 = 'low_day30',             // 30日留存低
  CONTINUOUS_DECLINE = 'continuous',    // 持续下降
  NO_AHA_MOMENT = 'no_aha',            // 没有Aha时刻
  PAID_USER_CHURN = 'paid_churn',      // 付费用户流失
}

// 留存策略
export interface RetentionAction {
  issue: RetentionIssue;
  description: string;
  strategies: string[];
  priority: number;  // 1-5,5最紧急
}

export class RetentionStrategyEngine {
  private static instance: RetentionStrategyEngine;
  private calculator: RetentionCalculator;
  
  static getInstance(): RetentionStrategyEngine {
    if (!RetentionStrategyEngine.instance) {
      RetentionStrategyEngine.instance = new RetentionStrategyEngine();
    }
    return RetentionStrategyEngine.instance;
  }
  
  constructor() {
    this.calculator = RetentionCalculator.getInstance();
  }
  
  // 分析留存问题并生成策略
  async analyze(result: RetentionResult): Promise<RetentionAction[]> {
    const actions: RetentionAction[] = [];
    const curveType = this.calculator.analyzeCurveType(result);
    
    const day1 = result.retentionData.get(1)?.retentionRate || 0;
    const day7 = result.retentionData.get(7)?.retentionRate || 0;
    const day30 = result.retentionData.get(30)?.retentionRate || 0;
    
    // 次日留存低
    if (day1 < 30) {
      actions.push({
        issue: RetentionIssue.LOW_DAY1,
        description: `次日留存仅${day1}%,低于30%警戒线`,
        strategies: [
          '优化首次启动体验,缩短到达核心功能的时间',
          '添加新手引导,帮助用户快速理解产品价值',
          '首日推送个性化内容,提高用户回访动力',
          '分析流失用户行为,找到离开前的最后操作',
        ],
        priority: 5,
      });
    }
    
    // 7日留存低
    if (day7 < 15) {
      actions.push({
        issue: RetentionIssue.LOW_DAY7,
        description: `7日留存仅${day7}%,低于15%警戒线`,
        strategies: [
          '检查核心功能是否真正解决用户需求',
          '添加签到/任务系统,增加回访理由',
          '第3天推送"你错过了什么"消息',
          '优化功能发现机制,让用户找到更多价值',
        ],
        priority: 4,
      });
    }
    
    // 30日留存低
    if (day30 < 5) {
      actions.push({
        issue: RetentionIssue.LOW_DAY30,
        description: `30日留存仅${day30}%,低于5%警戒线`,
        strategies: [
          '重新审视产品核心价值主张',
          '添加社交功能,建立用户间连接',
          '引入UGC内容,增加内容丰富度',
          '建立用户成长体系,增加离开成本',
        ],
        priority: 3,
      });
    }
    
    // 持续下降型
    if (curveType === RetentionCurveType.CONTINUOUS_DECLINE) {
      actions.push({
        issue: RetentionIssue.CONTINUOUS_DECLINE,
        description: '留存曲线持续下降,没有稳定期',
        strategies: [
          '产品缺乏持续使用价值,需要增加高频功能',
          '添加内容更新机制,给用户回访理由',
          '建立用户社区,增加社交粘性',
          '引入习惯养成机制(每日推荐/打卡)',
        ],
        priority: 5,
      });
    }
    
    // 骤降后平稳——找到Aha时刻
    if (curveType === RetentionCurveType.SHARP_DROP_FLAT) {
      actions.push({
        issue: RetentionIssue.NO_AHA_MOMENT,
        description: '留存骤降后趋于平稳,需要找到Aha时刻提前引导',
        strategies: [
          '分析留存用户的共同行为,找到Aha时刻',
          '在首次使用时引导用户到达Aha时刻',
          '缩短用户到达Aha时刻的路径',
          '对未到达Aha时刻的用户做定向召回',
        ],
        priority: 4,
      });
    }
    
    // 按优先级排序
    actions.sort((a, b) => b.priority - a.priority);
    
    // 上报分析结果
    analytics.reportEvent('retention_analysis', {
      'cohort_date': result.cohortDate,
      'new_users': result.newUsers,
      'day1_retention': day1,
      'day7_retention': day7,
      'day30_retention': day30,
      'curve_type': curveType,
      'issue_count': actions.length,
    });
    
    return actions;
  }
  
  // 打印策略建议
  printActions(actions: RetentionAction[]): void {
    console.info('\n========== 留存优化策略 ==========');
    actions.forEach((action, index) => {
      console.info(`\n[${index + 1}] ${action.description}`);
      console.info(`  优先级: ${'⭐'.repeat(action.priority)}`);
      console.info('  策略:');
      action.strategies.forEach((s, i) => {
        console.info(`    ${i + 1}. ${s}`);
      });
    });
    console.info('\n====================================\n');
  }
}

踩坑与注意事项

1. 留存定义要明确

"留存"到底怎么定义?登录算留存?打开应用算留存?使用核心功能算留存?不同定义算出来的留存率差异巨大。

一般来说:

  • 宽泛留存:打开应用就算——数据好看但水分大
  • 活跃留存:有实际操作才算——数据真实但偏低
  • 核心留存:使用核心功能才算——最真实但最严格

选哪种取决于你的业务。社交应用用"活跃留存",工具应用用"核心留存",别用"宽泛留存"自欺欺人。

2. 群组划分要科学

按注册日期划分群组是最基础的。但有时候你需要按渠道、按版本、按设备类型划分——不同渠道来的用户留存率可能差几倍。

3. 样本量要够大

10个用户的留存率没有统计意义。至少需要100个用户以上的群组,留存数据才有参考价值。小样本的波动太大,容易误判。

4. 留存率不是越高越好

100%的留存率意味着没有新用户进来——这比低留存更可怕。健康的留存曲线是:新用户持续进来,留存率稳定在合理水平。

不同类型应用的合理留存范围:

应用类型 次日留存 7日留存 30日留存
社交应用 40-60% 20-40% 10-25%
工具应用 20-40% 10-25% 5-15%
游戏应用 30-50% 15-30% 5-15%
电商应用 25-45% 10-20% 5-10%

5. 别忽视回流用户

有些用户流失后还会回来,这叫回流用户。回流率也是重要的留存指标——如果你的30日留存低但回流率高,说明产品有吸引力,只是用户使用频率低。

HarmonyOS 6适配说明

HarmonyOS 6在留存分析方面的更新:

  1. 跨设备留存归因:基于华为账号自动识别跨设备用户,留存率计算更准确
  2. 实时留存计算:新增analytics.calculateRetention()接口,服务端实时计算留存率
  3. 留存预警:当留存率低于设定阈值时自动告警
  4. Aha时刻检测:AI自动分析留存用户的行为特征,找到Aha时刻
// HarmonyOS 6 实时留存计算
async function getRealtimeRetention(): Promise<void> {
  try {
    const result = await analytics.calculateRetention({
      cohortDate: '2024-06-24',
      retentionDays: [1, 7, 30],
      definition: 'active',  // active / core_action
    });
    
    console.info(`次日留存: ${result.day1}%`);
    console.info(`7日留存: ${result.day7}%`);
    console.info(`30日留存: ${result.day30}%`);
  } catch (error) {
    console.error('[Retention] 计算失败');
  }
}

总结

留存率是产品健康度的核心指标——用户来了能留下,产品才有未来。次日留存看第一印象,7日留存看功能价值,30日留存看长期粘性。留存曲线的形状比具体数字更重要——骤降后平稳说明有Aha时刻,持续下降说明产品价值不足。

维度 评价
学习难度 ⭐⭐⭐☆☆ 概念简单,但留存优化需要深度业务理解
使用频率 ⭐⭐⭐⭐⭐ 每日监控,每周分析
重要程度 ⭐⭐⭐⭐⭐ 决定产品生死

核心记住三点:留存定义要明确(别自欺欺人)、曲线形状比数字重要(看趋势)、优化要找到Aha时刻(引导用户到达)。留存优化不是一锤子买卖,而是持续监测、持续优化的长期工程。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。