Android App 授权机制简析_通过packagemanagerservices给应用赋权-程序员宅基地

技术标签: android  PMS权限  Android  

一、授予方式

1.动态授权

从Android M开始,虽然之后的版本会有部分调整,但对危险权限的处理方式是一致的。无论是亲自编码实现,还是引用第三方框架,技术都已经很成熟了,在此不做过多的论述。

2.特定shareUID属性

这里以android.uid.system属性为例。不同于通过install方式安装的App,将该属性添加到AndroidManifest.xml中后,需要先用System Signature进行签名,再像System App一样集成到Rom之中,这样便可以直接使用在AndroidManifest.xml中声明的所有权限。

3.default permission配置

对于那些既不想申请运行时权限,也不想添加android.uid.system属性,即使放置在system/priv-app目录下,依然不能使用某些在AndroidManifest.xml中声明的权限的System App。在system/etc/permissions目录下,做好default permission配置后,便可直接声明、使用那些配置过的权限。


二、查看授权

adb shell dumpsys package com.xxx.xxx

未授权:

已授权:

 

三、原理简析

1.动态授权

ActivityCompat.requestPermissions(...):实际是通过PMS的grantRuntimePermission(...)方法,来完成运行时权限的动态分配;最终存放到data/system/users/0/runtime-permissions.xml配置文件中,而安装时权限都是存放在data/system/packages.xml配置文件中的。

不同的Android系统版本,代码结构有一定的差异。下面是Android Q中的部分源码:

//frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

@Override
public void grantRuntimePermission(String packageName, String permName, final int userId) {
    boolean overridePolicy = (checkUidPermission(
            Manifest.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY, Binder.getCallingUid())
            == PackageManager.PERMISSION_GRANTED);

    mPermissionManager.grantRuntimePermission(permName, packageName, overridePolicy,
            getCallingUid(), userId, mPermissionCallback);
}
//frameworks/base/services/core/java/com/android/server/pm/permission/PermissionManagerService.java

private void grantRuntimePermission(String permName, String packageName, boolean overridePolicy,
        int callingUid, final int userId, PermissionCallback callback) {
    if (!mUserManagerInt.exists(userId)) {
        Log.e(TAG, "No such user:" + userId);
        return;
    }

    mContext.enforceCallingOrSelfPermission(
            android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS,
            "grantRuntimePermission");

    enforceCrossUserPermission(callingUid, userId,
            true,  // requireFullPermission
            true,  // checkShell
            false, // requirePermissionWhenSameUser
            "grantRuntimePermission");

    final PackageParser.Package pkg = mPackageManagerInt.getPackage(packageName);
    if (pkg == null || pkg.mExtras == null) {
        throw new IllegalArgumentException("Unknown package: " + packageName);
    }
    final BasePermission bp;
    synchronized(mLock) {
        bp = mSettings.getPermissionLocked(permName);
    }
    if (bp == null) {
        throw new IllegalArgumentException("Unknown permission: " + permName);
    }
    if (mPackageManagerInt.filterAppAccess(pkg, callingUid, userId)) {
        throw new IllegalArgumentException("Unknown package: " + packageName);
    }

    bp.enforceDeclaredUsedAndRuntimeOrDevelopment(pkg);

    // If a permission review is required for legacy apps we represent
    // their permissions as always granted runtime ones since we need
    // to keep the review required permission flag per user while an
    // install permission's state is shared across all users.
    if (pkg.applicationInfo.targetSdkVersion < Build.VERSION_CODES.M
            && bp.isRuntime()) {
        return;
    }

    final int uid = UserHandle.getUid(userId, pkg.applicationInfo.uid);

    final PackageSetting ps = (PackageSetting) pkg.mExtras;
    final PermissionsState permissionsState = ps.getPermissionsState();

    final int flags = permissionsState.getPermissionFlags(permName, userId);
    if ((flags & PackageManager.FLAG_PERMISSION_SYSTEM_FIXED) != 0) {
        Log.e(TAG, "Cannot grant system fixed permission "
                + permName + " for package " + packageName);
        return;
    }
    if (!overridePolicy && (flags & PackageManager.FLAG_PERMISSION_POLICY_FIXED) != 0) {
        Log.e(TAG, "Cannot grant policy fixed permission "
                + permName + " for package " + packageName);
        return;
    }

    if (bp.isHardRestricted()
            && (flags & PackageManager.FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) == 0) {
        Log.e(TAG, "Cannot grant hard restricted non-exempt permission "
                + permName + " for package " + packageName);
        return;
    }

    if (bp.isSoftRestricted() && !SoftRestrictedPermissionPolicy.forPermission(mContext,
            pkg.applicationInfo, UserHandle.of(userId), permName).canBeGranted()) {
        Log.e(TAG, "Cannot grant soft restricted permission " + permName + " for package "
                + packageName);
        return;
    }

    if (bp.isDevelopment()) {
        // Development permissions must be handled specially, since they are not
        // normal runtime permissions.  For now they apply to all users.
        if (permissionsState.grantInstallPermission(bp) !=
                PERMISSION_OPERATION_FAILURE) {
            if (callback != null) {
                callback.onInstallPermissionGranted();
            }
        }
        return;
    }

    if (ps.getInstantApp(userId) && !bp.isInstant()) {
        throw new SecurityException("Cannot grant non-ephemeral permission"
                + permName + " for package " + packageName);
    }

    if (pkg.applicationInfo.targetSdkVersion < Build.VERSION_CODES.M) {
        Slog.w(TAG, "Cannot grant runtime permission to a legacy app");
        return;
    }

    final int result = permissionsState.grantRuntimePermission(bp, userId);
    switch (result) {
        case PERMISSION_OPERATION_FAILURE: {
            return;
        }

        case PermissionsState.PERMISSION_OPERATION_SUCCESS_GIDS_CHANGED: {
            if (callback != null) {
                callback.onGidsChanged(UserHandle.getAppId(pkg.applicationInfo.uid), userId);
            }
        }
        break;
    }

    if (bp.isRuntime()) {
        logPermission(MetricsEvent.ACTION_PERMISSION_GRANTED, permName, packageName);
    }

    if (callback != null) {
        callback.onPermissionGranted(uid, userId);
    }

    if (bp.isRuntime()) {
        notifyRuntimePermissionStateChanged(packageName, userId);
    }

    // Only need to do this if user is initialized. Otherwise it's a new user
    // and there are no processes running as the user yet and there's no need
    // to make an expensive call to remount processes for the changed permissions.
    if (READ_EXTERNAL_STORAGE.equals(permName)
            || WRITE_EXTERNAL_STORAGE.equals(permName)) {
        final long token = Binder.clearCallingIdentity();
        try {
            if (mUserManagerInt.isUserInitialized(userId)) {
                StorageManagerInternal storageManagerInternal = LocalServices.getService(
                        StorageManagerInternal.class);
                storageManagerInternal.onExternalStoragePolicyChanged(uid, packageName);
            }
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

}

2.特定shareUID属性

在PMS的构造方法中,对sharedUserId和UID进行了映射,最终通过PMS来完成:解析应用信息、各组件信息、权限信息,分配UID、记录组件信息,更新、授予、保存权限信息。

//frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

public PackageManagerService(Context context, Installer installer,
        boolean factoryTest, boolean onlyCore) {
    // 省略部分代码

    mSettings.addSharedUserLPw("android.uid.system", Process.SYSTEM_UID,
            ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.phone", RADIO_UID,
            ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.log", LOG_UID,
            ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.nfc", NFC_UID,
            ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.bluetooth", BLUETOOTH_UID,
            ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.shell", SHELL_UID,
            ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.se", SE_UID,
            ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.networkstack", NETWORKSTACK_UID,
            ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);

    // 省略部分代码
}

3.default permission配置

DefaultPermissionGrantPolicy.grantDefaultPermissions(...):最终也是通过PMS来完成默认权限的管理,在经过一些列判断后,调用关键方法grantRuntimePermission(...)和updatePermissionFlags(...)。

//frameworks/base/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java

public void grantDefaultPermissions(int userId) {
    grantPermissionsToSysComponentsAndPrivApps(userId);
    grantDefaultSystemHandlerPermissions(userId);
    grantDefaultPermissionExceptions(userId);
    synchronized (mLock) {
        mDefaultPermissionsGrantedUsers.put(userId, userId);
    }
}


private void grantRuntimePermissions(PackageInfo pkg, Set<String> permissionsWithoutSplits,
        boolean systemFixed, boolean ignoreSystemPackage,
        boolean whitelistRestrictedPermissions, int userId) {
    UserHandle user = UserHandle.of(userId);
    if (pkg == null) {
        return;
    }

    String[] requestedPermissions = pkg.requestedPermissions;
    if (ArrayUtils.isEmpty(requestedPermissions)) {
        return;
    }

    // Intersect the requestedPermissions for a factory image with that of its current update
    // in case the latter one removed a <uses-permission>
    String[] requestedByNonSystemPackage = getPackageInfo(pkg.packageName).requestedPermissions;
    int size = requestedPermissions.length;
    for (int i = 0; i < size; i++) {
        if (!ArrayUtils.contains(requestedByNonSystemPackage, requestedPermissions[i])) {
            requestedPermissions[i] = null;
        }
    }
    requestedPermissions = ArrayUtils.filterNotNull(requestedPermissions, String[]::new);

    PackageManager pm;
    try {
        pm = mContext.createPackageContextAsUser(mContext.getPackageName(), 0,
                user).getPackageManager();
    } catch (NameNotFoundException doesNotHappen) {
        throw new IllegalStateException(doesNotHappen);
    }

    final ArraySet<String> permissions = new ArraySet<>(permissionsWithoutSplits);
    ApplicationInfo applicationInfo = pkg.applicationInfo;

    int newFlags = PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT;
    if (systemFixed) {
        newFlags |= PackageManager.FLAG_PERMISSION_SYSTEM_FIXED;
    }

    // Automatically attempt to grant split permissions to older APKs
    final List<PermissionManager.SplitPermissionInfo> splitPermissions =
            mContext.getSystemService(PermissionManager.class).getSplitPermissions();
    final int numSplitPerms = splitPermissions.size();
    for (int splitPermNum = 0; splitPermNum < numSplitPerms; splitPermNum++) {
        final PermissionManager.SplitPermissionInfo splitPerm =
                splitPermissions.get(splitPermNum);

        if (applicationInfo != null
                && applicationInfo.targetSdkVersion < splitPerm.getTargetSdk()
                && permissionsWithoutSplits.contains(splitPerm.getSplitPermission())) {
            permissions.addAll(splitPerm.getNewPermissions());
        }
    }

    Set<String> grantablePermissions = null;

    // In some cases, like for the Phone or SMS app, we grant permissions regardless
    // of if the version on the system image declares the permission as used since
    // selecting the app as the default for that function the user makes a deliberate
    // choice to grant this app the permissions needed to function. For all other
    // apps, (default grants on first boot and user creation) we don't grant default
    // permissions if the version on the system image does not declare them.
    if (!ignoreSystemPackage
            && applicationInfo != null
            && applicationInfo.isUpdatedSystemApp()) {
        final PackageInfo disabledPkg = getSystemPackageInfo(
                mServiceInternal.getDisabledSystemPackageName(pkg.packageName));
        if (disabledPkg != null) {
            if (ArrayUtils.isEmpty(disabledPkg.requestedPermissions)) {
                return;
            }
            if (!Arrays.equals(requestedPermissions, disabledPkg.requestedPermissions)) {
                grantablePermissions = new ArraySet<>(Arrays.asList(requestedPermissions));
                requestedPermissions = disabledPkg.requestedPermissions;
            }
        }
    }

    final int numRequestedPermissions = requestedPermissions.length;

    // Sort requested permissions so that all permissions that are a foreground permission (i.e.
    // permissions that have a background permission) are before their background permissions.
    final String[] sortedRequestedPermissions = new String[numRequestedPermissions];
    int numForeground = 0;
    int numOther = 0;
    for (int i = 0; i < numRequestedPermissions; i++) {
        String permission = requestedPermissions[i];
        if (getBackgroundPermission(permission) != null) {
            sortedRequestedPermissions[numForeground] = permission;
            numForeground++;
        } else {
            sortedRequestedPermissions[numRequestedPermissions - 1 - numOther] =
                    permission;
            numOther++;
        }
    }

    for (int requestedPermissionNum = 0; requestedPermissionNum < numRequestedPermissions;
            requestedPermissionNum++) {
        String permission = requestedPermissions[requestedPermissionNum];

        // If there is a disabled system app it may request a permission the updated
        // version ot the data partition doesn't, In this case skip the permission.
        if (grantablePermissions != null && !grantablePermissions.contains(permission)) {
            continue;
        }

        if (permissions.contains(permission)) {
            final int flags = mContext.getPackageManager().getPermissionFlags(
                    permission, pkg.packageName, user);

            // If we are trying to grant as system fixed and already system fixed
            // then the system can change the system fixed grant state.
            final boolean changingGrantForSystemFixed = systemFixed
                    && (flags & PackageManager.FLAG_PERMISSION_SYSTEM_FIXED) != 0;

            // Certain flags imply that the permission's current state by the system or
            // device/profile owner or the user. In these cases we do not want to clobber the
            // current state.
            //
            // Unless the caller wants to override user choices. The override is
            // to make sure we can grant the needed permission to the default
            // sms and phone apps after the user chooses this in the UI.
            if (!isFixedOrUserSet(flags) || ignoreSystemPackage
                    || changingGrantForSystemFixed) {
                // Never clobber policy fixed permissions.
                // We must allow the grant of a system-fixed permission because
                // system-fixed is sticky, but the permission itself may be revoked.
                if ((flags & PackageManager.FLAG_PERMISSION_POLICY_FIXED) != 0) {
                    continue;
                }

                // Preserve whitelisting flags.
                newFlags |= (flags & PackageManager.FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT);

                // If we are whitelisting the permission, update the exempt flag before grant.
                if (whitelistRestrictedPermissions && isPermissionRestricted(permission)) {
                    mContext.getPackageManager().updatePermissionFlags(permission,
                            pkg.packageName,
                            PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT,
                            PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT, user);
                }

                // If the system tries to change a system fixed permission from one fixed
                // state to another we need to drop the fixed flag to allow the grant.
                if (changingGrantForSystemFixed) {
                    mContext.getPackageManager().updatePermissionFlags(permission,
                            pkg.packageName, flags,
                            flags & ~PackageManager.FLAG_PERMISSION_SYSTEM_FIXED, user);
                }

                if (pm.checkPermission(permission, pkg.packageName)
                        != PackageManager.PERMISSION_GRANTED) {
                    mContext.getPackageManager()
                            .grantRuntimePermission(pkg.packageName, permission, user);
                }

                mContext.getPackageManager().updatePermissionFlags(permission, pkg.packageName,
                        newFlags, newFlags, user);

                int uid = UserHandle.getUid(userId,
                        UserHandle.getAppId(pkg.applicationInfo.uid));

                List<String> fgPerms = mPermissionManager.getBackgroundPermissions()
                        .get(permission);
                if (fgPerms != null) {
                    int numFgPerms = fgPerms.size();
                    for (int fgPermNum = 0; fgPermNum < numFgPerms; fgPermNum++) {
                        String fgPerm = fgPerms.get(fgPermNum);

                        if (pm.checkPermission(fgPerm, pkg.packageName)
                                == PackageManager.PERMISSION_GRANTED) {
                            // Upgrade the app-op state of the fg permission to allow bg access
                            // TODO: Dont' call app ops from package manager code.
                            mContext.getSystemService(AppOpsManager.class).setUidMode(
                                    AppOpsManager.permissionToOp(fgPerm), uid,
                                    AppOpsManager.MODE_ALLOWED);

                            break;
                        }
                    }
                }

                String bgPerm = getBackgroundPermission(permission);
                String op = AppOpsManager.permissionToOp(permission);
                if (bgPerm == null) {
                    if (op != null) {
                        // TODO: Dont' call app ops from package manager code.
                        mContext.getSystemService(AppOpsManager.class).setUidMode(op, uid,
                                AppOpsManager.MODE_ALLOWED);
                    }
                } else {
                    int mode;
                    if (pm.checkPermission(bgPerm, pkg.packageName)
                            == PackageManager.PERMISSION_GRANTED) {
                        mode = AppOpsManager.MODE_ALLOWED;
                    } else {
                        mode = AppOpsManager.MODE_FOREGROUND;
                    }

                    mContext.getSystemService(AppOpsManager.class).setUidMode(op, uid, mode);
                }

                if (DEBUG) {
                    Log.i(TAG, "Granted " + (systemFixed ? "fixed " : "not fixed ")
                            + permission + " to default handler " + pkg);

                    int appOp = AppOpsManager.permissionToOpCode(permission);
                    if (appOp != AppOpsManager.OP_NONE
                            && AppOpsManager.opToDefaultMode(appOp)
                                    != AppOpsManager.MODE_ALLOWED) {
                        // Permission has a corresponding appop which is not allowed by default
                        // We must allow it as well, as it's usually checked alongside the
                        // permission
                        if (DEBUG) {
                            Log.i(TAG, "Granting OP_" + AppOpsManager.opToName(appOp)
                                    + " to " + pkg.packageName);
                        }
                        mContext.getSystemService(AppOpsManager.class).setUidMode(
                                appOp, pkg.applicationInfo.uid, AppOpsManager.MODE_ALLOWED);
                    }
                }
            }

            // If a component gets a permission for being the default handler A
            // and also default handler B, we grant the weaker grant form.
            if ((flags & PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT) != 0
                    && (flags & PackageManager.FLAG_PERMISSION_SYSTEM_FIXED) != 0
                    && !systemFixed) {
                if (DEBUG) {
                    Log.i(TAG, "Granted not fixed " + permission + " to default handler "
                            + pkg);
                }
                mContext.getPackageManager().updatePermissionFlags(permission, pkg.packageName,
                        PackageManager.FLAG_PERMISSION_SYSTEM_FIXED, 0, user);
            }
        }
    }
}

 


参考:

Android 权限的一些细节

深入浅析Android动态权限的机制

Android O 默认授予预制应用运行时权限方法

PackageManagerService处理应用权限流程

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/ecjtuhq/article/details/111349986

智能推荐

while循环&CPU占用率高问题深入分析与解决方案_main函数使用while(1)循环cpu占用99-程序员宅基地

文章浏览阅读3.8k次,点赞9次,收藏28次。直接上一个工作中碰到的问题,另外一个系统开启多线程调用我这边的接口,然后我这边会开启多线程批量查询第三方接口并且返回给调用方。使用的是两三年前别人遗留下来的方法,放到线上后发现确实是可以正常取到结果,但是一旦调用,CPU占用就直接100%(部署环境是win server服务器)。因此查看了下相关的老代码并使用JProfiler查看发现是在某个while循环的时候有问题。具体项目代码就不贴了,类似于下面这段代码。​​​​​​while(flag) {//your code;}这里的flag._main函数使用while(1)循环cpu占用99

【无标题】jetbrains idea shift f6不生效_idea shift +f6快捷键不生效-程序员宅基地

文章浏览阅读347次。idea shift f6 快捷键无效_idea shift +f6快捷键不生效

node.js学习笔记之Node中的核心模块_node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是-程序员宅基地

文章浏览阅读135次。Ecmacript 中没有DOM 和 BOM核心模块Node为JavaScript提供了很多服务器级别,这些API绝大多数都被包装到了一个具名和核心模块中了,例如文件操作的 fs 核心模块 ,http服务构建的http 模块 path 路径操作模块 os 操作系统信息模块// 用来获取机器信息的var os = require('os')// 用来操作路径的var path = require('path')// 获取当前机器的 CPU 信息console.log(os.cpus._node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是

数学建模【SPSS 下载-安装、方差分析与回归分析的SPSS实现(软件概述、方差分析、回归分析)】_化工数学模型数据回归软件-程序员宅基地

文章浏览阅读10w+次,点赞435次,收藏3.4k次。SPSS 22 下载安装过程7.6 方差分析与回归分析的SPSS实现7.6.1 SPSS软件概述1 SPSS版本与安装2 SPSS界面3 SPSS特点4 SPSS数据7.6.2 SPSS与方差分析1 单因素方差分析2 双因素方差分析7.6.3 SPSS与回归分析SPSS回归分析过程牙膏价格问题的回归分析_化工数学模型数据回归软件

利用hutool实现邮件发送功能_hutool发送邮件-程序员宅基地

文章浏览阅读7.5k次。如何利用hutool工具包实现邮件发送功能呢?1、首先引入hutool依赖<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.19</version></dependency>2、编写邮件发送工具类package com.pc.c..._hutool发送邮件

docker安装elasticsearch,elasticsearch-head,kibana,ik分词器_docker安装kibana连接elasticsearch并且elasticsearch有密码-程序员宅基地

文章浏览阅读867次,点赞2次,收藏2次。docker安装elasticsearch,elasticsearch-head,kibana,ik分词器安装方式基本有两种,一种是pull的方式,一种是Dockerfile的方式,由于pull的方式pull下来后还需配置许多东西且不便于复用,个人比较喜欢使用Dockerfile的方式所有docker支持的镜像基本都在https://hub.docker.com/docker的官网上能找到合..._docker安装kibana连接elasticsearch并且elasticsearch有密码

随便推点

Python 攻克移动开发失败!_beeware-程序员宅基地

文章浏览阅读1.3w次,点赞57次,收藏92次。整理 | 郑丽媛出品 | CSDN(ID:CSDNnews)近年来,随着机器学习的兴起,有一门编程语言逐渐变得火热——Python。得益于其针对机器学习提供了大量开源框架和第三方模块,内置..._beeware

Swift4.0_Timer 的基本使用_swift timer 暂停-程序员宅基地

文章浏览阅读7.9k次。//// ViewController.swift// Day_10_Timer//// Created by dongqiangfei on 2018/10/15.// Copyright 2018年 飞飞. All rights reserved.//import UIKitclass ViewController: UIViewController { ..._swift timer 暂停

元素三大等待-程序员宅基地

文章浏览阅读986次,点赞2次,收藏2次。1.硬性等待让当前线程暂停执行,应用场景:代码执行速度太快了,但是UI元素没有立马加载出来,造成两者不同步,这时候就可以让代码等待一下,再去执行找元素的动作线程休眠,强制等待 Thread.sleep(long mills)package com.example.demo;import org.junit.jupiter.api.Test;import org.openqa.selenium.By;import org.openqa.selenium.firefox.Firefox.._元素三大等待

Java软件工程师职位分析_java岗位分析-程序员宅基地

文章浏览阅读3k次,点赞4次,收藏14次。Java软件工程师职位分析_java岗位分析

Java:Unreachable code的解决方法_java unreachable code-程序员宅基地

文章浏览阅读2k次。Java:Unreachable code的解决方法_java unreachable code

标签data-*自定义属性值和根据data属性值查找对应标签_如何根据data-*属性获取对应的标签对象-程序员宅基地

文章浏览阅读1w次。1、html中设置标签data-*的值 标题 11111 222222、点击获取当前标签的data-url的值$('dd').on('click', function() { var urlVal = $(this).data('ur_如何根据data-*属性获取对应的标签对象

推荐文章

热门文章

相关标签