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. C语言中的指针

    请先看C++中的指针概述,这里只是扩充 数组指针 其实这里主要说的就是c++中的指针运算 /* 数组元素指针: 一个变量有地址,一个数组包含若干个元素,每个数组元素都有相应的地址, 指针变量可以指向数 ...

  2. 用PHP删除ftp下载导致的文件空行

    使用FTP上传下载文件,如果没有设置传输方式为二进制,可能会导致文件出现空行的情况,例如: 使用PHP对文件的空行进行删除,使用正则: <?php $file = './abc.inc.php' ...

  3. 学习MVC之租房网站(七)-房源管理和配图上传

    在上一篇<学习MVC之租房网站(六)-用户登录和权限控制>完成了后台用户登录和权限控制功能的开发,接下来要完成的是房源的管理,用户在后台新增.编辑房源信息,供前台用户操作. 一 房源管理 ...

  4. 关于java和c++中的i++

    看到一个题目,大概是i=i++之类. 乍一看很简单,都知道i++的先参与计算再自增, 所以i不变. 但是仔细一想, 如果i不变, 那么在赋值之后, 自增的i, 增加的是谁呢? 逻辑顺序是怎样的呢? / ...

  5. Maven学习-简介、安装

    Maven是一个项目管理工具,它包含了一个项目对象模型,一组标准集合,一个项目声明周期,一个依赖管理系统和用来运行定义在生命周期阶段中插件目标的逻辑.Maven采用了约定优于配置这一基本原则.在没有自 ...

  6. FileInputStream和FileOutputStream详解

    一.引子 文件,作为常见的数据源.关于操作文件的字节流就是 FileInputStream & FileOutputStream.它们是Basic IO字节流中重要的实现类.二.FileInp ...

  7. Git版本控制,rsync同步文件,完成线上部署

    之前项目开发完成,测试阶段,借着此时,由于公司暂时用两台aliyun  ecs  做业务层,所以每次都需要同步线上文件,进而想着搞一搞服务器端(小公司,新项目,先小搞一把),搭建一套小的版本控制上线的 ...

  8. vue的双向绑定原理及实现

    前言 使用vue也好有一段时间了,虽然对其双向绑定原理也有了解个大概,但也没好好探究下其原理实现,所以这次特意花了几晚时间查阅资料和阅读相关源码,自己也实现一个简单版vue的双向绑定版本,先上个成果图 ...

  9. 【注意事项】APP左右横滑设计

    移动端屏幕越来越大,但用户对内容量的要求也水涨船高.如何在有限的屏幕内透出更多的内容,是设计师们研究的重点. 常用的内容拓展设计有:Y 方向 List 滑动.Z 方向 3D Touch .入口式内容折 ...

  10. 多线程异步编程示例和实践-Thread和ThreadPool

    说到多线程异步编程,总会说起Thread.ThreadPool.Task.TPL这一系列的技术.总结整理了一版编程示例和实践,分享给大家. 先从Thread和ThreadPool说起: 1. 创建并启 ...