android10.0(Q) AOSP 增加应用锁功能

简介: android10.0(Q) AOSP 增加应用锁功能

前言


应用锁的功能可以说是很普遍了,大致就是在 startActivity 对应代码处进行拦截就行。

最开始在网上找了点资料,没有能合适直接用的,就自己搞了下,这里简单做个笔记。

Android应用锁实现

那就给大伙先来个效果图先康康


image.png


思路分析


由于我们的目标应用是系统 Settings ,这家伙的入口不唯一,一开始是想着在 Launcher3 中进行拦截就行,


最终效果不太完美,后来改到 ActivityStarter 中 startActivity()


是怎么找到这个地方的呢?因为之前改过一个 Q 版本以上不能后台拉起 Activity 的问题,当时报错的地方


就在这里,后来想了下报错不就是被拦截了么,这样我们也可以在此处定制密码进行拦截。


1、找到拦截启动位置,startActivity()


2、读取要拉起 Activity 对应包名和需要加锁 APP 包名判断


3、在加锁清单中,弹出验证界面,验证成功,继续执行 startActivity


4、无需加锁,直接放行


上代码


1、Launcher3 中可拦截的地方


packages\apps\Launcher3\src\com\android\launcher3\Launcher.java

public boolean startActivitySafely(View v, Intent intent, ItemInfo item,
            @Nullable String sourceContainer) {
        if (TestProtocol.sDebugTracing) {
            android.util.Log.d(TestProtocol.NO_START_TAG,
                    "startActivitySafely outer");
        }
         //cczheng add testcode
        final String packageName = intent.getComponent().getPackageName();
        android.util.Log.i("ActivityStarter111","packageName="+packageName);
        if ("com.android.settings".equals(packageName)) {
          //android.widget.Toast.makeText(this, "foo", 1000).show();
          //return true;
        }//end
        if (!hasBeenResumed()) {
            // Workaround an issue where the WM launch animation is clobbered when finishing the
            // recents animation into launcher. Defer launching the activity until Launcher is
            // next resumed.
            addOnResumeCallback(() -> startActivitySafely(v, intent, item, sourceContainer));
            UiFactory.clearSwipeSharedState(true /* finishAnimation */);
            return true;
        }
        boolean success = super.startActivitySafely(v, intent, item, sourceContainer);
        if (success && v instanceof BubbleTextView) {
            // This is set to the view that launched the activity that navigated the user away
            // from launcher. Since there is no callback for when the activity has finished
            // launching, enable the press state and keep this reference to reset the press
            // state when we return to launcher.
            BubbleTextView btv = (BubbleTextView) v;
            btv.setStayPressed(true);
            addOnResumeCallback(btv);
        }
        return success;
    }


在此处判断包名后可直接 new 对话框进行交互,优点方便简单,缺点不能完全加锁。


2、ActivityStarter 中可拦截的地方


我这里偷懒了,界面没啥美化可言,你们自己按需调整。这里面引入 R 资源估计会有问题,所以这里直接


java 代码写布局了,通过 window 方式展示。


要拦截的包名清单可以通过 ContentProvider 查询,毕竟有 Context,自己按需增加。


frameworks\base\services\core\java\com\android\server\wm\ActivityStarter.java

import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.text.InputType;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
    //cczheng add for appLockPass S
    boolean isLocked;
    boolean isPassCheck;
    private void showAppLockPasswordWindow(Context mContext, IApplicationThread caller, Intent intent, Intent ephemeralIntent,
            String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo,
            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
            IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid,
            String callingPackage, int realCallingPid, int realCallingUid, int startFlags,
            SafeActivityOptions options,
            boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity,
            TaskRecord inTask, boolean allowPendingRemoteAnimationRegistryLookup,
            PendingIntentRecord originatingPendingIntent, boolean allowBackgroundActivityStart) {
        final WindowManager.LayoutParams params =  new WindowManager.LayoutParams();
        params.width = 380;
        params.height = 230;
        params.format = Color.parseColor("#ADADAD");
        params.type = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
        params.setTitle("AppLock");
        WindowManager mWM = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        final LinearLayout parentLayout = new LinearLayout(mContext);
        parentLayout.setOrientation(LinearLayout.VERTICAL);
        parentLayout.setBackgroundColor(Color.WHITE);
        LinearLayout.LayoutParams layoutParams
                = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT);
        parentLayout.setLayoutParams(layoutParams);
        TextView titleText = new TextView(mContext);
        LinearLayout.LayoutParams contentParams
                = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        titleText.setLayoutParams(contentParams);
        titleText.setText("Please enter app password");
        titleText.setTextColor(Color.BLACK);
        titleText.setTypeface(Typeface.create(titleText.getTypeface(), Typeface.NORMAL), Typeface.BOLD);
        titleText.setPadding(10, 10, 0, 0);
        parentLayout.addView(titleText);
        EditText passText = new EditText(mContext);
        passText.setLayoutParams(contentParams);
        passText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
        passText.setTextColor(Color.BLACK);
        parentLayout.addView(passText);
        Button okBtn = new Button(mContext);
        LinearLayout.LayoutParams btnParams
                = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        btnParams.gravity = Gravity.RIGHT;
        okBtn.setLayoutParams(btnParams);
        okBtn.setBackgroundColor(Color.TRANSPARENT);
        okBtn.setText("Confirm");
        okBtn.setTextColor(Color.parseColor("#3996E8"));
        okBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String password = passText.getText().toString();
                //android.widget.Toast.makeText(mContext, password, 1000).show();
                if (parentLayout!=null){
                    mWM.removeViewImmediate(parentLayout);
                    //parentLayout = null;
                }
                if ("123".equals(password)) {
                    isPassCheck = true;
                    startActivity(caller, intent, ephemeralIntent,
                             resolvedType, aInfo, rInfo,
                             voiceSession,  voiceInteractor,
                             resultTo,  resultWho,  requestCode,  callingPid,  callingUid,
                             callingPackage,  realCallingPid,  realCallingUid,  startFlags,
                             options,
                             ignoreTargetSecurity,  componentSpecified,  outActivity,
                             inTask,  allowPendingRemoteAnimationRegistryLookup,
                             originatingPendingIntent,  allowBackgroundActivityStart);
                }else {
                    isPassCheck = false;
                }
            }
        });
        parentLayout.addView(okBtn);
        try {
            mWM.addView(parentLayout, params);
        } catch (WindowManager.BadTokenException e) {
            e.printStackTrace();
        }
    }
    IApplicationThread mscaller; 
    Intent msintent, msephemeralIntent;
    String msresolvedType, msresultWho, mscallingPackage;
    ActivityInfo msaInfo;
    ResolveInfo msrInfo;
    int msrequestCode, mscallingPid, mscallingUid, msrealCallingPid, msrealCallingUid, msstartFlags;
    boolean msignoreTargetSecurity, mscomponentSpecified, msallowPendingRemoteAnimationRegistryLookup, msallowBackgroundActivityStart;
    IVoiceInteractionSession msvoiceSession;
    IVoiceInteractor msvoiceInteractor;
    IBinder msresultTo;
    SafeActivityOptions msoptions;
    ActivityRecord[] msoutActivity;
    TaskRecord msinTask;
    PendingIntentRecord msoriginatingPendingIntent;
    //E
    private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent,
            String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo,
            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
            IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid,
            String callingPackage, int realCallingPid, int realCallingUid, int startFlags,
            SafeActivityOptions options,
            boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity,
            TaskRecord inTask, boolean allowPendingRemoteAnimationRegistryLookup,
            PendingIntentRecord originatingPendingIntent, boolean allowBackgroundActivityStart) {
        mSupervisor.getActivityMetricsLogger().notifyActivityLaunching(intent);
        int err = ActivityManager.START_SUCCESS;
        // Pull the optional Ephemeral Installer-only bundle out of the options early.
        final Bundle verificationBundle
                = options != null ? options.popAppVerificationBundle() : null;
      ................................
    checkedOptions = mInterceptor.mActivityOptions;
        }
        //cczheng add for appLockPass S
        android.util.Log.e("ActivityStarter","callingPackage="+callingPackage);
        final String packageName = intent.getComponent().getPackageName();
        android.util.Log.i("ActivityStarter","packageName="+packageName+" abort="+abort);
    if (("com.android.launcher3".equals(callingPackage) || "com.android.systemui".equals(callingPackage)) 
        && "com.android.settings".equals(packageName)) {
      isLocked = true;
    }else{
      isLocked = false;
    }
    if (isPassCheck) {
      android.util.Log.i("ActivityStarter","isPassCheck pass goto activity");
      isLocked = false;
      isPassCheck = false;
    }
      mscaller = caller;
      msintent = intent;
      msephemeralIntent = ephemeralIntent;
      msresolvedType = resolvedType;
      msaInfo = aInfo;
      msvoiceSession = voiceSession;
      msvoiceInteractor = voiceInteractor;
      msresultTo = resultTo;
      msresultWho = resultWho;
      msrequestCode = requestCode;
      mscallingPid = callingPid;
      mscallingUid = callingUid;
      mscallingPackage = callingPackage;
      msrealCallingPid = realCallingPid;
      msrealCallingUid = realCallingUid;
      msstartFlags = startFlags;
      msoptions = options;
      msignoreTargetSecurity = ignoreTargetSecurity;
      mscomponentSpecified = componentSpecified;
      msoutActivity = outActivity;
      msinTask = inTask;
      msallowPendingRemoteAnimationRegistryLookup = allowPendingRemoteAnimationRegistryLookup;
      msoriginatingPendingIntent = originatingPendingIntent;
      msallowBackgroundActivityStart = allowBackgroundActivityStart;
      new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
        @Override
        public void run() {
                if (isLocked) {
                    showAppLockPasswordWindow(mService.mContext, mscaller, msintent, msephemeralIntent,
                             msresolvedType, msaInfo, msrInfo,
                             msvoiceSession,  msvoiceInteractor,
                             msresultTo,  msresultWho,  msrequestCode,  mscallingPid,  mscallingUid,
                             mscallingPackage,  msrealCallingPid,  msrealCallingUid,  msstartFlags,
                             msoptions,
                             msignoreTargetSecurity,  mscomponentSpecified,  msoutActivity,
                             msinTask,  msallowPendingRemoteAnimationRegistryLookup,
                             msoriginatingPendingIntent,  msallowBackgroundActivityStart);
                }
            }
        });
       if (isLocked) {
           android.util.Log.d("ActivityStarter","START_SWITCHES_CANCELED=");
          return ActivityManager.START_SWITCHES_CANCELED;
       }///add EE
        if (abort) {
            if (resultRecord != null) {
                resultStack.sendActivityResultLocked(-1, resultRecord, resultWho, requestCode,
                        RESULT_CANCELED, null);
            }
            // We pretend to the caller that it was really started, but
            // they will just get a cancel result.
            ActivityOptions.abort(checkedOptions);
            return START_ABORTED;
        }
    ........................


以上几个坑点说一下


callingPackage 是调用 startActivity 方法的初始者,可以看到我这里加了过滤,必须是 SystemUI 和 Launcher3


调用的情况下才进行拦截,系统启动时我们看到的安卓正在启动中就是 Settings 中的 FallbackHome,此时 callingPackage 为 null


这样会卡住进不去系统,所以需要过滤。


showAppLockPasswordWindow() 需要在 Handler 执行,ActivityStarter 中不能直接进行 UI 线程操作,不然会报错。


符合拦截条件时需要先将此次 startActivity return,异步去显示 UI 操作,根据密码结果再递归一次 startActivity


所以 showAppLockPasswordWindow() 放到了内部类中,传递参数需要为 final 或者全局变量,所以上面加了一大堆全局赋值的,参数也太多了点。

目录
相关文章
|
14天前
|
Dart 前端开发 Android开发
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
37 4
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
16天前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
120 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
2月前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
92 14
|
2月前
|
搜索推荐 前端开发 测试技术
打造个性化安卓应用:从设计到开发的全面指南
在这个数字时代,拥有一个定制的移动应用不仅是一种趋势,更是个人或企业品牌的重要延伸。本文将引导你通过一系列简单易懂的步骤,从构思你的应用理念开始,直至实现一个功能齐全的安卓应用。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你提供必要的工具和知识,帮助你将创意转化为现实。
|
2月前
|
Java Android开发 开发者
探索安卓开发:构建你的第一个“Hello World”应用
在安卓开发的浩瀚海洋中,每个新手都渴望扬帆起航。本文将作为你的指南针,引领你通过创建一个简单的“Hello World”应用,迈出安卓开发的第一步。我们将一起搭建开发环境、了解基本概念,并编写第一行代码。就像印度圣雄甘地所说:“你必须成为你希望在世界上看到的改变。”让我们一起开始这段旅程,成为我们想要见到的开发者吧!
88 0
|
2月前
|
Java Linux 数据库
探索安卓开发:打造你的第一款应用
在数字时代的浪潮中,每个人都有机会成为创意的实现者。本文将带你走进安卓开发的奇妙世界,通过浅显易懂的语言和实际代码示例,引导你从零开始构建自己的第一款安卓应用。无论你是编程新手还是希望拓展技术的开发者,这篇文章都将为你打开一扇门,让你的创意和技术一起飞扬。
|
3月前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
351 4
|
3月前
|
JSON Java Android开发
探索安卓开发之旅:打造你的第一个天气应用
【10月更文挑战第30天】在这个数字时代,掌握移动应用开发技能无疑是进入IT行业的敲门砖。本文将引导你开启安卓开发的奇妙之旅,通过构建一个简易的天气应用来实践你的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都将成为你宝贵的学习资源。我们将一步步地深入到安卓开发的世界中,从搭建开发环境到实现核心功能,每个环节都充满了发现和创造的乐趣。让我们开始吧,一起在代码的海洋中航行!
|
3月前
|
存储 搜索推荐 Java
打造个性化安卓应用:从设计到实现
【10月更文挑战第30天】在数字化时代,拥有一个个性化的安卓应用不仅能够提升用户体验,还能加强品牌识别度。本文将引导您了解如何从零开始设计和实现一个安卓应用,涵盖用户界面设计、功能开发和性能优化等关键环节。我们将以一个简单的记事本应用为例,展示如何通过Android Studio工具和Java语言实现基本功能,同时确保应用流畅运行。无论您是初学者还是希望提升现有技能的开发者,这篇文章都将为您提供宝贵的见解和实用的技巧。
|
3月前
|
搜索推荐 开发工具 Android开发
打造个性化Android应用:从设计到实现的旅程
【10月更文挑战第26天】在这个数字时代,拥有一个能够脱颖而出的移动应用是成功的关键。本文将引导您了解如何从概念化阶段出发,通过设计、开发直至发布,一步步构建一个既美观又实用的Android应用。我们将探讨用户体验(UX)设计的重要性,介绍Android开发的核心组件,并通过实际案例展示如何克服开发中的挑战。无论您是初学者还是有经验的开发者,这篇文章都将为您提供宝贵的见解和实用的技巧,帮助您在竞争激烈的应用市场中脱颖而出。

热门文章

最新文章

  • 1
    如何修复 Android 和 Windows 不支持视频编解码器的问题?
  • 2
    Android历史版本与APK文件结构
  • 3
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 4
    【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
  • 5
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
  • 6
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
  • 7
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 8
    Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
  • 9
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
  • 10
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
  • 1
    Cellebrite UFED 4PC 7.71 (Windows) - Android 和 iOS 移动设备取证软件
    26
  • 2
    【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    34
  • 3
    Android历史版本与APK文件结构
    121
  • 4
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    29
  • 5
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
    23
  • 6
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
    57
  • 7
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    37
  • 8
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
    73
  • 9
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    120
  • 10
    Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
    29