1 安装入口PackageInstallerActivity,这个类只是在安装前做准备。通过各种校验,然后弹出被安装应用的权限框,等待用户安装。具体的流程如下

1.1  求mSessionId 如果是已经存在的则判断对应的SessinInfo是否存在,否则默认一个-1

  

 if (PackageInstaller.ACTION_CONFIRM_PERMISSIONS.equals(intent.getAction())) {
			//可能是系统级别的应用安装时,需要授权走这个流程
            final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
            final PackageInstaller.SessionInfo info = mInstaller.getSessionInfo(sessionId);
            if (info == null || !info.sealed || info.resolvedBaseCodePath == null) {
                Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
                finish();
                return;
            }
			//如果有SessInfo则证明传过来的sessionId是有效的,并且获取packageUri
            mSessionId = sessionId;
            packageUri = Uri.fromFile(new File(info.resolvedBaseCodePath));
            mOriginatingURI = null;
            mReferrerURI = null;
        } else {
			//如果是用户自己拉起来的安装,则默认sessionId为-1 病且获取 packageUri
            mSessionId = -1;
            packageUri = intent.getData();
            mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
            mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER);
        }

1.2 开始做校验

	// 返回URI解析错误 -3
        if (packageUri == null) {
            Log.w(TAG, "Unspecified source");
            setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);
            finish();
            return;
        }
        //如果是手表就不支持
        if (DeviceUtils.isWear(this)) {
            showDialogInner(DLG_NOT_SUPPORTED_ON_WEAR);
            return;
        }

1. 3 如果是系统应用拉起安装则直接进入包分析阶段

 final boolean requestFromUnknownSource = isInstallRequestFromUnknownSource(intent);
        if (!requestFromUnknownSource) {
			//进入packageUri处理阶段
            processPackageUri(packageUri);
            return;
        }
		 //安装请求是否来自于一个未知的源。
		private boolean isInstallRequestFromUnknownSource(Intent intent) {
			String callerPackage = getCallingPackage();
			if (callerPackage != null && intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false)) {
				try {
					mSourceInfo = mPm.getApplicationInfo(callerPackage, 0);
					if (mSourceInfo != null) {
						if ((mSourceInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
							// Privileged apps are not considered an unknown source.
							//如果安装请求是来自于一个系统应用,则可以明确源是已知的
							return false;
						}
					}
				} catch (NameNotFoundException e) { }
			}
			return true;//否则源是未知
		}

1. 4 针对管理员账户做不同的处理

	// If the admin prohibits it, or we're running in a managed profile, just show error
        // and exit. Otherwise show an option to take the user to Settings to change the setting.
        // 是否是管理员用户
        final boolean isManagedProfile = mUserManager.isManagedProfile();
        if (isUnknownSourcesDisallowed()) {
            //如果有用户限制了未知来源应用的安装
            if ((mUserManager.getUserRestrictionSource(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
                    Process.myUserHandle()) & UserManager.RESTRICTION_SOURCE_SYSTEM) != 0) {
                //如果不是system用户限制当前用户安装未知来源app,启动设置,使用户在设置里面修改
                showDialogInner(DLG_UNKNOWN_SOURCES);
            } else {
				//如果是system用户限制的,则直接退出
                startActivity(new Intent(Settings.ACTION_SHOW_ADMIN_SUPPORT_DETAILS));
                clearCachedApkIfNeededAndFinish();
            }
        } else if (!isUnknownSourcesEnabled() && isManagedProfile) {
            //如果不允许安装未知市场的应用,并且当前是管理员用户,则弹出"您的管理员不允许安装来源不明的应用"的对话框
            showDialogInner(DLG_ADMIN_RESTRICTS_UNKNOWN_SOURCES);
        } else if (!isUnknownSourcesEnabled()) {
            // Ask user to enable setting first
            //如果不允许安装未知市场的应用,则弹出这个对话框修改设置
            showDialogInner(DLG_UNKNOWN_SOURCES);
        } else {
			//进入packageUri处理阶段
            processPackageUri(packageUri);
        }

1. 5 处理文件的uri

	    //处理包的uri
		private void processPackageUri(final Uri packageUri) {
			mPackageURI = packageUri;
			final String scheme = packageUri.getScheme();
			final PackageUtil.AppSnippet as;
			switch (scheme) {
				case SCHEME_PACKAGE:
					try {
						mPkgInfo = mPm.getPackageInfo(packageUri.getSchemeSpecificPart(), PackageManager.GET_PERMISSIONS | PackageManager.GET_UNINSTALLED_PACKAGES);
					} catch (NameNotFoundException e) { }
					if (mPkgInfo == null) {
						Log.w(TAG, "Requested package " + packageUri.getScheme() + " not available. Discontinuing installation");
						showDialogInner(DLG_PACKAGE_ERROR);
						setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
						return;
					}
					as = new PackageUtil.AppSnippet(mPm.getApplicationLabel(mPkgInfo.applicationInfo), mPm.getApplicationIcon(mPkgInfo.applicationInfo));
					break;
				case SCHEME_FILE:
					File sourceFile = new File(packageUri.getPath());
					PackageParser.Package parsed = PackageUtil.getPackageInfo(sourceFile);
					// Check for parse errors
					if (parsed == null) {
						Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation");
						showDialogInner(DLG_PACKAGE_ERROR);
						setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
						return;
					}
					mPkgInfo = PackageParser.generatePackageInfo(parsed, null,  PackageManager.GET_PERMISSIONS, 0, 0, null, new PackageUserState());
					as = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile);
					break;
				case SCHEME_CONTENT:
					//重新复制一个安装包在解析,返回一个SCHEME_FILE类型的uri重新解析包uri
					mStagingAsynTask = new StagingAsyncTask();
					mStagingAsynTask.execute(packageUri);
					return;
				default:
					Log.w(TAG, "Unsupported scheme " + scheme);
					setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);
					clearCachedApkIfNeededAndFinish();
					return;
			}
			PackageUtil.initSnippetForNewApp(this, as, R.id.app_snippet);
			//启动安装
			initiateInstall();
		}

1.6 启动安装

//启动安装
		private void initiateInstall() {
			String pkgName = mPkgInfo.packageName;
			// Check if there is already a package on the device with this name
			// but it has been renamed to something else. 是否有同名应用已经安装上去了。在此安装则被认为是替换安装
			String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] { pkgName });
			if (oldName != null && oldName.length > 0 && oldName[0] != null) {
				pkgName = oldName[0];
				mPkgInfo.packageName = pkgName;
				mPkgInfo.applicationInfo.packageName = pkgName;
			}
			// Check if package is already installed. display confirmation dialog if replacing pkg
			// 检查这个包是否真的被安装,如果要替换,则显示替换对话框
			try {
				// This is a little convoluted because we want to get all uninstalled
				// apps, but this may include apps with just data, and if it is just
				// data we still want to count it as "installed".
				// 获取设备上有残存数据,并且标记为“installed”的,实际上已经被卸载的应用。
				mAppInfo = mPm.getApplicationInfo(pkgName, PackageManager.GET_UNINSTALLED_PACKAGES);
				if ((mAppInfo.flags&ApplicationInfo.FLAG_INSTALLED) == 0) {
					////如果应用是被卸载的,但是又是被标识成安装过的,则认为是新安装
					mAppInfo = null;
				}
			} catch (NameNotFoundException e) {
				mAppInfo = null;
			}
			//列出权限列表,等待用户确认安装
			startInstallConfirm();
		}

1.7 确认安装权限

private void startInstallConfirm() {
			TabHost tabHost = (TabHost)findViewById(android.R.id.tabhost);
			tabHost.setup();
			tabHost.setVisibility(View.VISIBLE);
			ViewPager viewPager = (ViewPager)findViewById(R.id.pager);
			TabsAdapter adapter = new TabsAdapter(this, tabHost, viewPager);
			// If the app supports runtime permissions the new permissions will
			// be requested at runtime, hence we do not show them at install.
			// 如果app支持运行时权限,这里会显示新的运行时权限
			// 根据版本判断app是否有可能有运行时权限
			boolean supportsRuntimePermissions = mPkgInfo.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.M;
			boolean permVisible = false;
			mScrollView = null;
			mOkCanInstall = false;
			int msg = 0;
			//perms这个对象包括了该应用的用户的uid以及相应的一些权限,以及权限组信息。
			AppSecurityPermissions perms = new AppSecurityPermissions(this, mPkgInfo);
			//所有的权限数量
			final int N = perms.getPermissionCount(AppSecurityPermissions.WHICH_ALL);
			if (mAppInfo != null) {
				//如果是替换应用
				msg = (mAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 ? R.string.install_confirm_question_update_system : R.string.install_confirm_question_update;
				mScrollView = new CaffeinatedScrollView(this);
				mScrollView.setFillViewport(true);
				boolean newPermissionsFound = false;
				if (!supportsRuntimePermissions) {
					newPermissionsFound = (perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0);
					if (newPermissionsFound) {
						permVisible = true;
						//显示新添加的权限项(这个view竟然是系统的view)
						mScrollView.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_NEW));
					}
				}
				if (!supportsRuntimePermissions && !newPermissionsFound) {
					//如果既不支持可运行权限项也没有新权限发现,则提示没有新权限
					LayoutInflater inflater = (LayoutInflater)getSystemService( Context.LAYOUT_INFLATER_SERVICE);
					TextView label = (TextView)inflater.inflate(R.layout.label, null);
					label.setText(R.string.no_new_perms);
					mScrollView.addView(label);
				}
				adapter.addTab(tabHost.newTabSpec(TAB_ID_NEW).setIndicator(getText(R.string.newPerms)), mScrollView);
			} else  {
				findViewById(R.id.tabscontainer).setVisibility(View.GONE);
				findViewById(R.id.spacer).setVisibility(View.VISIBLE);
			}
			if (!supportsRuntimePermissions && N > 0) {
				permVisible = true;
				LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
				View root = inflater.inflate(R.layout.permissions_list, null);
				if (mScrollView == null) {
					mScrollView = (CaffeinatedScrollView)root.findViewById(R.id.scrollview);
				}
				//如果没有运行时权限并且有权限,则列出所有权限
				((ViewGroup)root.findViewById(R.id.permission_list)).addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL));
				adapter.addTab(tabHost.newTabSpec(TAB_ID_ALL).setIndicator(getText(R.string.allPerms)), root);
			}
			if (!permVisible) {
				//如果不需要任何权限。更新的不需要新的权限以及运行时权限
				if (mAppInfo != null) {
					// This is an update to an application, but there are no
					// permissions at all.
					msg = (mAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 ? R.string.install_confirm_question_update_system_no_perms: R.string.install_confirm_question_update_no_perms;
					findViewById(R.id.spacer).setVisibility(View.VISIBLE);
				} else {
					// This is a new application with no permissions.
					msg = R.string.install_confirm_question_no_perms;
				}
				tabHost.setVisibility(View.INVISIBLE);
				mScrollView = null;
			}
			if (msg != 0) {
				((TextView)findViewById(R.id.install_confirm_question)).setText(msg);
			}
			mInstallConfirm.setVisibility(View.VISIBLE);
			mOk.setEnabled(true);
			if (mScrollView == null) {
				// There is nothing to scroll view, so the ok button is immediately
				// set to install.
				mOk.setText(R.string.install);
				mOkCanInstall = true;
			} else {
				mScrollView.setFullScrollAction(new Runnable() {
                @Override
                public void run() {
                    mOk.setText(R.string.install);
                    mOkCanInstall = true;
                }
            });
        }
    }

1. 8 点击安装

public void onClick(View v) {
			if (v == mOk) {
				if (mOkCanInstall || mScrollView == null) {
					if (mSessionId != -1) {
						//如果原来是确认权限请求则赋予安装权限退出
						mInstaller.setPermissionsResult(mSessionId, true);
						clearCachedApkIfNeededAndFinish();
					} else {
						//开始安装
						startInstall();
					}
				} else {mScrollView.pageScroll(View.FOCUS_DOWN);}
			} else if (v == mCancel) {
				// Cancel and finish 取消安装
				setResult(RESULT_CANCELED);
				if (mSessionId != -1) {
					mInstaller.setPermissionsResult(mSessionId, false);
				}
				clearCachedApkIfNeededAndFinish();
			}
		}

1.9 进入安装

private void startInstall() {
			// Start subactivity to actually install the application
			Intent newIntent = new Intent();
			//带走安装包的applicationInfo
			newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO, mPkgInfo.applicationInfo);
			newIntent.setData(mPackageURI); //带走安装包的applicationInfo
			newIntent.setClass(this, InstallAppProgress.class);
			String installerPackageName = getIntent().getStringExtra( Intent.EXTRA_INSTALLER_PACKAGE_NAME);
			if (mOriginatingURI != null) { //带走安装包的mOriginatingURI
				newIntent.putExtra(Intent.EXTRA_ORIGINATING_URI, mOriginatingURI);
			}
			if (mReferrerURI != null) {//带走安装包的mReferrerURI
				newIntent.putExtra(Intent.EXTRA_REFERRER, mReferrerURI);
			}
			if (mOriginatingUid != VerificationParams.NO_UID) {//带走安装包的mOriginatingUid,这个uid如果不是拉安装的应用的uid
				newIntent.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid);
			}
			if (installerPackageName != null) {//带走安装包的installerPackageName
				newIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, installerPackageName);
			}
			if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
				newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
				newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
			}
			if(localLOGV) Log.i(TAG, "downloaded app uri="+mPackageURI);
			startActivity(newIntent);
			finish();
		}

2  安装过程InstallAppProgress

2.1 注册安装监听

IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(BROADCAST_ACTION);
    registerReceiver(mBroadcastReceiver, intentFilter, BROADCAST_SENDER_PERMISSION, null /*scheduler*/);

2.2  正式安装

替换现存的包标示
    final int installFlags = getInstallFlags(mAppInfo.packageName);
if ("package".equals(mPackageURI.getScheme())) {
            try {
                //安装与该应用同名的应用,应该比较快,否则会抛出异常
                pm.installExistingPackage(mAppInfo.packageName);
                onPackageInstalled(PackageInstaller.STATUS_SUCCESS);
            } catch (PackageManager.NameNotFoundException e) {
                onPackageInstalled(PackageInstaller.STATUS_FAILURE_INVALID);
            }
        } else {
            final PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
                    PackageInstaller.SessionParams.MODE_FULL_INSTALL);
            params.referrerUri = getIntent().getParcelableExtra(Intent.EXTRA_REFERRER);
            params.originatingUri = getIntent().getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
            params.originatingUid = getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID, UID_UNKNOWN);
            File file = new File(mPackageURI.getPath());
            try {
				//解析安装包,设置安装位置。这个安装位置是从AndroidManifest文件获取的,至于怎么获取,最后指向native 层。没有继续跟踪
                params.setInstallLocation(PackageParser.parsePackageLite(file, 0).installLocation);
            } catch (PackageParser.PackageParserException e) {
                Log.e(TAG, "Cannot parse package " + file + ". Assuming defaults.");
            }

            mInstallHandler.post(new Runnable() {
                @Override
                public void run() {
                    doPackageStage(pm, params);
                }
            });
        }

2.3 后台安装

private void doPackageStage(PackageManager pm, PackageInstaller.SessionParams params) {
        //初始化安装器
        final PackageInstaller packageInstaller = pm.getPackageInstaller();
        PackageInstaller.Session session = null;
        try {
            final String packageLocation = mPackageURI.getPath();
            final File file = new File(packageLocation);
            //获取sessionId
            final int sessionId = packageInstaller.createSession(params);
            final byte[] buffer = new byte[65536];
            //获取session
            session = packageInstaller.openSession(sessionId);
            final InputStream in = new FileInputStream(file);
            final long sizeBytes = file.length();
            final OutputStream out = session.openWrite("PackageInstaller", 0, sizeBytes);
            try {
                int c;
                //安装中..............
                while ((c = in.read(buffer)) != -1) {
                    out.write(buffer, 0, c);
                    if (sizeBytes > 0) {
                        final float fraction = ((float) c / (float) sizeBytes);
                        session.addProgress(fraction);
                    }
                }
                session.fsync(out);
            } finally {
                IoUtils.closeQuietly(in);
                IoUtils.closeQuietly(out);
            }
            // Create a PendingIntent and use it to generate the IntentSender
            //发起安装完成提交通知
            Intent broadcastIntent = new Intent(BROADCAST_ACTION);
            PendingIntent pendingIntent = PendingIntent.getBroadcast(
                    InstallAppProgress.this /*context*/,
                    sessionId,
                    broadcastIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            session.commit(pendingIntent.getIntentSender());
        } catch (IOException e) {
            onPackageInstalled(PackageInstaller.STATUS_FAILURE);
        } finally {
            IoUtils.closeQuietly(session);
        }
    }

2.4  接受安装结果

  private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final int statusCode = intent.getIntExtra(
                    PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
            //等待安装
            if (statusCode == PackageInstaller.STATUS_PENDING_USER_ACTION) {
                context.startActivity((Intent)intent.getParcelableExtra(Intent.EXTRA_INTENT));
            } else {
				//返回安装结果
                onPackageInstalled(statusCode);
            }
        }
    };

Android 7.0 安装器安装过程分析 (com.android.packageinstaller)的更多相关文章

  1. .Net 转战 Android 4.4 日常笔记(5)--新软件Android Studio 0.5.8安装与配置及问题解决

    说真心话,Eclipse跟我们.net的VS比起来就是屌丝比高富帅,一切都是那么的难用,速度慢得我无法忍受 于是想试试Google钦点的Android Studio IDE工具,这跟ADT一样也是一套 ...

  2. 在Android Studio 0.5.2中使用ArcGIS Android SDK

    环境 操作系统:Mac OSX 10.8.5Android Studio: 0.5.2ArcGIS Android SDK: 10.2.3 操作步骤 在Android Studio中新建一个Modul ...

  3. Android 7.0 调取系统相机崩溃解决android.os.FileUriExposedException

    一.写在前面 最近由于廖子尧忙于自己公司的事情和OkGo(一款专注于让网络请求更简单的网络框架) ,故让LZ 接替维护ImagePicker(一款支持单.多选.旋转和裁剪的图片选择器),也是处理了诸多 ...

  4. 【适配整理】Android 7.0 调取系统相机崩溃解决android.os.FileUriExposedException

    一.写在前面 最近由于廖子尧忙于自己公司的事情和 OkGo (一款专注于让网络请求更简单的网络框架) ,故让LZ 接替维护 ImagePicker(一款支持单.多选.旋转和裁剪的图片选择器),也是处理 ...

  5. 【Android Studio安装部署系列】三十五、从Android studio3.0.1升级到Android studio3.1.4【以及创建android p模拟器的尝试(未成功)】

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 概述 因为想要使用Android P模拟器,所以需要将Android Studio升级到3.1版本以上. Android P模拟器的最低版 ...

  6. [php-pear]如何使用 PHP-PEAR安装器,以及使用 PEAR 安装扩展库

    我们都知道 PHP PEAR,就是 PHP Extension and Application Respository,也就是 PHP 扩展和应用代码库. PHP 也可以通过 PEAR 安装器来进行 ...

  7. 谷歌安装器扫描时提示“需要root权限”,不用root也可以的!

    能FQ的用户会用谷歌服务,一般的新手机没有安装谷歌框架,但是在用谷歌安装器安装谷歌市场时会提示"需要root权限",我用的是360手机,按照下面的教程搞好了: 安装完GSM包就可以 ...

  8. android 5.0新特性

    Android Lollipop 面向开发人员的主要功能 Material Design 设计 注重性能 通知 以大屏幕呈现 以文档为中心 连接性能再上一级 高性能图形 音频处理功能更强 摄像头和视频 ...

  9. Android 8.0 功能和 API

    Android 8.0 为用户和开发者引入多种新功能.本文重点介绍面向开发者的新功能. 用户体验 通知 在 Android 8.0 中,我们已重新设计通知,以便为管理通知行为和设置提供更轻松和更统一的 ...

随机推荐

  1. Caffe2 Tutorials[0]

    本系列教程包括9个小节,对应Caffe2官网的前9个教程,第10个教程讲的是在安卓下用SqueezeNet进行物体检测,此处不再翻译.另外由于栏主不关注RNN和LSTM,所以栏主不对剩下两个教程翻译. ...

  2. 安卓模拟器tools修改

    defaults write com.apple.finder AppleShowAllFiles -bool true 这步是显示隐藏文件夹, 然后打开finder,在应用程序上右键,选择在上层文件 ...

  3. dubbo高级配置学习

    启动时检查 可以通过check="false"关闭检查,比如,测试时,有些服务不关心,或者出现了循环依赖,必须有一方先启动. 关闭某个服务的启动时检查:(没有提供者时报错) < ...

  4. css控制table的td宽度

    今天发现即使设置table的td.th宽度,仍是不管用,是根据table的td的内容来适应宽度,导致其他的th.td丢失. 下图就是浏览器渲染的table,导致缺失"端口"这一列, ...

  5. 数据结构与算法系列研究五——树、二叉树、三叉树、平衡排序二叉树AVL

    树.二叉树.三叉树.平衡排序二叉树AVL 一.树的定义 树是计算机算法最重要的非线性结构.树中每个数据元素至多有一个直接前驱,但可以有多个直接后继.树是一种以分支关系定义的层次结构.    a.树是n ...

  6. [HDU1002] A + B Problem II

    Problem Description I have a very simple problem for you. Given two integers A and B, your job is to ...

  7. setTimeout异步加载

    两道经典的面试题,直接上代码 for(var i=0; i<3; i++){ setTimeout(function(){ i+=i; console.log(i); },1000) } var ...

  8. JS 使用 splice() 对数组去重

    一 问题 有如下 js 数组 connect_clients,需要在去掉所有元素中 user_id, goods_id 这两者的值都相同的元素. [ { id: 'eff040fb-92bc-4f24 ...

  9. 模板C++ 03图论算法 2最短路之全源最短路(Floyd)

    3.2最短路之全源最短路(Floyd) 这个算法用于求所有点对的最短距离.比调用n次SPFA的优点在于代码简单,时间复杂度为O(n^3).[无法计算含有负环的图] 依次扫描每一点(k),并以该点作为中 ...

  10. a标签点击之后有个虚线边框,怎么去掉

    1.行内处理方式1 <a hidefocus="true" href="#"></a> 2.行内处理方式2,让a标签获得焦点就失去焦点, ...