documentsUI源码分析
documentsUI源码分析
本文基于Android 6.0的源码,来分析documentsUI模块。
原本基于7.1源码看了两天,但是Android 7.1与6.0中documentsUI模块差异很大,且更加复杂,因此重新基于6.0的源码分析。
documentsUI是什么?
documentsUI是Android系统提供的一个文件选择器,类似于Windows系统中点击“打开”按钮弹出的文件选择框,有人称documentsUI为文件管理器,这是不准确的。
documentsUI是Android系统中存储访问框架(Storage Access Framework,SAF)的一部分。
Android 4.4(API 级别 19)引入了存储访问框架 (SAF)。SAF 让用户能够在其所有首选文档存储提供程序中方便地浏览并打开文档、图像以及其他文件。 用户可以通过易用的标准UI,以统一方式在所有应用和提供程序中浏览文件和访问最近使用的文件。
documentsUI的清单文件中只有一个Activity,且没有带category.LAUNCHER的属性,因此Launcher桌面上并没有图标,但是进入documentsUI的入口很多,如桌面上的下载应用、短信中的添加附件、浏览器中上传图片等。
documentsUI清单文件中的activity如下:
<activity
android:name=".DocumentsActivity"
android:theme="@style/DocumentsTheme"
android:icon="@drawable/ic_doc_text">
<intent-==filter==>
<action android:name="android.intent.action.OPEN_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.CREATE_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter android:priority="100">
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.OPEN_DOCUMENT_TREE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.provider.action.MANAGE_ROOT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.document/root" />
</intent-filter>
<intent-filter>
<action android:name="android.provider.action.BROWSE_DOCUMENT_ROOT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.document/root" />
</intent-filter>
</activity>
存储访问框架SAF
在介绍documentUI之前,需要介绍存储访问框架,在Android 4.4(API 级别 19),Google引入了存储访问框架 (SAF),让用户能够在其所有首选文档存储提供程序中方便地浏览并打开文档、图像以及其他文件。 用户可以通过易用的标准 UI,以统一方式在所有应用和提供程序中浏览文件和访问最近使用的文件。
云存储服务或本地存储服务可以通过实现封装其服务的 DocumentsProvider 参与此生态系统。只需几行代码,便可将需要访问提供程序文档的客户端应用与 SAF 集成。
SAF 包括以下内容:
- 文档提供程序 — 一种内容提供程序,允许存储服务(如 Google Drive)显示其管理的文件。 文档提供程序作为 DocumentsProvider 类的子类实现。文档提供程序的架构基于传统文件层次结构,但其实际数据存储方式由您决定。Android 平台包括若干内置文档提供程序,如 Downloads、Images 和 Videos。
- 客户端应用 — 一种自定义应用,它调用 ACTION_OPEN_DOCUMENT 和/或 ACTION_CREATE_DOCUMENT Intent 并接收文档提供程序返回的文件;
- 选取器 — 一种系统 UI,允许用户访问所有满足客户端应用搜索条件的文档提供程序内的文档。
控制流
文档提供程序数据模型基于传统文件层次结构。 通过DocumentsProvider
API访问数据,可以按照自己喜好的任何方式存储数据。例如,可以使用基于标记的云存储来存储数据。
如上图所示,在 SAF 中,提供程序和客户端并不直接交互。
- 客户端请求与文件交互(即读取、编辑、创建或删除文件)的权限;
- 交互在应用(在本示例中为照片应用)触发 Intent ACTION_OPEN_DOCUMENT 或ACTION_CREATE_DOCUMENT 后开始。Intent 可能包括进一步细化条件的过滤器 — 例如,“为我提供所有 MIME 类型为‘图像’的可打开文件”;
- Intent 触发后,系统选取器将检索每个已注册的提供程序,并向用户显示匹配的内容根目录;
- 选取器会为用户提供一个标准的文档访问界面,但底层文档提供程序可能与其差异很大。 例如,图 2 显示了一个 Google Drive 提供程序、一个 USB 提供程序和一个云提供程序。
客户端应用
编写一个客户端应用,调用documentsUI选择文件。
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
// Filter to only show results that can be "opened", such as a file (as opposed to a list of contacts or timezones)
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Filter to show only images, using the image MIME data type.
intent.setType("image/*");
startActivityForResult(intent, READ_REQUEST_CODE);
通过Intent.ACTION_OPEN_DOCUMENT
,documentsUI将响应该意图,选择文件后,在返回结果中提取URI,解析URI后进行相应操作。
内容提供程序
如需使得的自己的应用程序通过documentsUI向用户展示文件,可编写文档提供程序,通过 SAF 提供自己的文件。
首先要在清单文件中定义相应的provider和activity属性,然后创建继承DocumentsProvider
的子类,并实现以下方法:queryRoots()、queryChildDocuments()、queryDocument()、openDocument()
。
关于存储访问框架的详细介绍可在Android开发者官网获取。
源码分析
documentsUI代码结构较为复杂,本文只分析大致流程。
1. 入口: DocumentsActivity
布局文件是DrawerLayout,左边是侧滑菜单,右边是内容显示
内容显示区域布局:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.android.documentsui.DocumentsToolBar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:background="?android:attr/colorPrimary"
android:elevation="8dp"
android:theme="?android:attr/actionBarTheme">
<Spinner
android:id="@+id/stack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:overlapAnchor="true" />
</com.android.documentsui.DocumentsToolBar>
<com.android.documentsui.DirectoryContainerView
android:id="@+id/container_directory"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<FrameLayout
android:id="@+id/container_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/material_grey_50"
android:elevation="8dp" />
</LinearLayout>
内容显示区域由一个自定义view DocumentsToolBar
和DirectoryContainerView
组成。
侧滑菜单布局:
<LinearLayout
android:id="@+id/drawer_roots"
android:layout_width="256dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:orientation="vertical"
android:elevation="16dp"
android:background="@*android:color/white">
<Toolbar
android:id="@+id/roots_toolbar"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:background="?android:attr/colorPrimary"
android:elevation="8dp"
android:theme="?android:attr/actionBarTheme" />
<FrameLayout
android:id="@+id/container_roots"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
侧滑菜单由一个Toolbar和FrameLayout组成。
在 onCreate 方法中
if (mState.action == ACTION_CREATE) {
final String mimeType = getIntent().getType();
final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);
SaveFragment.show(getFragmentManager(), mimeType, title);
} else if (mState.action == ACTION_OPEN_TREE ||
mState.action == ACTION_OPEN_COPY_DESTINATION) {
PickFragment.show(getFragmentManager());
}
if (mState.action == ACTION_GET_CONTENT) {
final Intent moreApps = new Intent(getIntent());
moreApps.setComponent(null);
moreApps.setPackage(null);
RootsFragment.show(getFragmentManager(), moreApps);
} else if (mState.action == ACTION_OPEN ||
mState.action == ACTION_CREATE ||
mState.action == ACTION_OPEN_TREE ||
mState.action == ACTION_OPEN_COPY_DESTINATION) {
RootsFragment.show(getFragmentManager(), null);
}
mState保存状态信息,在buildDefaultState初始化,假设启动的action为ACTION_GET_CONTENT,那么将调用RootsFragment
的show
方法。
2. RootsFragment
public static void show(FragmentManager fm, Intent includeApps) {
final Bundle args = new Bundle();
args.putParcelable(EXTRA_INCLUDE_APPS, includeApps);
final RootsFragment fragment = new RootsFragment();
fragment.setArguments(args);
final FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.container_roots, fragment);
ft.commitAllowingStateLoss();
}
show
方法显示出RootsFragment
自己,RootsFragment
就是侧滑菜单部分,在 RootsFragment
的 onCreateView
方法中,加载出的view就是一个listview,如下图:
listview中显示的是能响应该打开文件Itent的文档提供者
和第三方应用
,在 onActivityCreated方法中,使用Loard机制加载出listview要显示的数据
mCallbacks = new LoaderCallbacks<Collection<RootInfo>>() {
@Override
public Loader<Collection<RootInfo>> onCreateLoader(int id, Bundle args) {
return new RootsLoader(context, roots, state);
}
@Override
public void onLoadFinished(
Loader<Collection<RootInfo>> loader, Collection<RootInfo> result) {
if (!isAdded()) return;
final Intent includeApps = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
mAdapter = new RootsAdapter(context, result, includeApps);
mList.setAdapter(mAdapter);
onCurrentRootChanged();
}
@Override
public void onLoaderReset(Loader<Collection<RootInfo>> loader) {
mAdapter = null;
mList.setAdapter(null);
}
};
在onLoadFinished
中实例化RootsAdapter
RootsAdapter
private static class RootsAdapter extends ArrayAdapter<Item> {
public RootsAdapter(Context context, Collection<RootInfo> roots, Intent includeApps) {
super(context, 0);
RootItem recents = null;
RootItem images = null;
RootItem videos = null;
RootItem audio = null;
RootItem downloads = null;
final List<RootInfo> clouds = Lists.newArrayList();
final List<RootInfo> locals = Lists.newArrayList();
for (RootInfo root : roots) {
if (root.isRecents()) {
recents = new RootItem(root);
} else if (root.isExternalStorage()) {
locals.add(root);
} else if (root.isDownloads()) {
downloads = new RootItem(root);
} else if (root.isImages()) {
images = new RootItem(root);
} else if (root.isVideos()) {
videos = new RootItem(root);
} else if (root.isAudio()) {
audio = new RootItem(root);
} else {
clouds.add(root);
}
}
final RootComparator comp = new RootComparator();
Collections.sort(clouds, comp);
Collections.sort(locals, comp);
if (recents != null) add(recents);
for (RootInfo cloud : clouds) {
add(new RootItem(cloud));
}
if (images != null) add(images);
if (videos != null) add(videos);
if (audio != null) add(audio);
if (downloads != null) add(downloads);
for (RootInfo local : locals) {
add(new RootItem(local));
}
if (includeApps != null) {
final PackageManager pm = context.getPackageManager();
final List<ResolveInfo> infos = pm.queryIntentActivities(
includeApps, PackageManager.MATCH_DEFAULT_ONLY);
final List<AppItem> apps = Lists.newArrayList();
// Omit ourselves from the list
for (ResolveInfo info : infos) {
if (!context.getPackageName().equals(info.activityInfo.packageName)) {
apps.add(new AppItem(info));
}
}
if (apps.size() > 0) {
add(new SpacerItem());
for (Item item : apps) {
add(item);
}
}
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final Item item = getItem(position);
return item.getView(convertView, parent);
}
@Override
public boolean areAllItemsEnabled() {
return false;
}
@Override
public boolean isEnabled(int position) {
return getItemViewType(position) != 1;
}
@Override
public int getItemViewType(int position) {
final Item item = getItem(position);
if (item instanceof RootItem || item instanceof AppItem) {
return 0;
} else {
return 1;
}
}
@Override
public int getViewTypeCount() {
return 2;
}
}
RootsAdapter中主要包含以下几点:
- 实例化RootsAdapter时,解析传入的数据得到
recents
、images
、videos
、audio
、downloads
、locals
、clouds
,这些都可以在内容显示区展示文档 includeApps
代表可以相应该Intent的第三方APP,获取这些APP的信息(如图标、名称等)显示在listview中- 根据getItemViewType判断不同类型item,显示其布局。listview中包含两种item,分别是
RootItem
和AppItem
,它们共同继承自Item
类 - SpacerItem也是继承自
Item
类,它是一个分隔线,分隔RootItem
和AppItem
点击事件
侧滑菜单的listview设置了两个点击事件,普通点击事件和长按点击事件
private OnItemClickListener mItemListener = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Item item = mAdapter.getItem(position);
if (item instanceof RootItem) {
BaseActivity activity = BaseActivity.get(RootsFragment.this);
activity.onRootPicked(((RootItem) item).root);
} else if (item instanceof AppItem) {
DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this);
activity.onAppPicked(((AppItem) item).info);
} else {
throw new IllegalStateException("Unknown root: " + item);
}
}
};
private OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
final Item item = mAdapter.getItem(position);
if (item instanceof AppItem) {
showAppDetails(((AppItem) item).info);
return true;
} else {
return false;
}
}
};
长按点击事件只对AppItem
有效,长按AppItem
时跳转到对应APP的应用信息界面,点击AppItem
时,启动documentsUI的intent交由相应APP处理。
当点击的是RootItem
时,调用DocumentsActivity
的 onRootPicked( )
方法,该方法继承自BaseActivity
。
void onRootPicked(RootInfo root) {
State state = getDisplayState();
// Clear entire backstack and start in new root
state.stack.root = root;
state.stack.clear();
state.stackTouched = true;
mSearchManager.update(root);
// Recents is always in memory, so we just load it directly.
// Otherwise we delegate loading data from disk to a task
// to ensure a responsive ui.
if (mRoots.isRecentsRoot(root)) {
onCurrentDirectoryChanged(ANIM_SIDE);
} else {
new PickRootTask(root).executeOnExecutor(getCurrentExecutor());
}
}
这里判断是否点击的是“最近”菜单,如果是则直接加载,如果不是则执行new PickRootTask(root).executeOnExecutor(getCurrentExecutor())
加载相应item的内容,最后也是进入onCurrentDirectoryChanged
中
下面看一下onCurrentDirectoryChanged
方法
final void onCurrentDirectoryChanged(int anim) {
onDirectoryChanged(anim); //更新文档内容显示
final RootsFragment roots = RootsFragment.get(getFragmentManager());
if (roots != null) {
roots.onCurrentRootChanged();//更新侧滑菜单点击状态
}
updateActionBar();
invalidateOptionsMenu();
}
其中重点是onDirectoryChanged(anim)
方法,这个方法是在BaseActivity
类中定义的一个抽象方法
abstract void onDirectoryChanged(int anim);
其具体实现在DocumentsActivity
中:
@Override
void onDirectoryChanged(int anim) {
final FragmentManager fm = getFragmentManager();
final RootInfo root = getCurrentRoot();
final DocumentInfo cwd = getCurrentDirectory();
mDirectoryContainer.setDrawDisappearingFirst(anim == ANIM_DOWN);
if (cwd == null) {
// No directory means recents
if (mState.action == ACTION_CREATE ||
mState.action == ACTION_OPEN_TREE ||
mState.action == ACTION_OPEN_COPY_DESTINATION) {
RecentsCreateFragment.show(fm);
} else {
DirectoryFragment.showRecentsOpen(fm, anim);
// Start recents in grid when requesting visual things
final boolean visualMimes = MimePredicate.mimeMatches(
MimePredicate.VISUAL_MIMES, mState.acceptMimes);
mState.userMode = visualMimes ? State.MODE_GRID : State.MODE_LIST;
mState.derivedMode = mState.userMode;
}
} else {
if (mState.currentSearch != null) {
// Ongoing search
DirectoryFragment.showSearch(fm, root, mState.currentSearch, anim);
} else {
// Normal boring directory
DirectoryFragment.showNormal(fm, root, cwd, anim);
}
}
// Forget any replacement target
if (mState.action == ACTION_CREATE) {
final SaveFragment save = SaveFragment.get(fm);
if (save != null) {
save.setReplaceTarget(null);
}
}
if (mState.action == ACTION_OPEN_TREE ||
mState.action == ACTION_OPEN_COPY_DESTINATION) {
final PickFragment pick = PickFragment.get(fm);
if (pick != null) {
pick.setPickTarget(mState.action, cwd);
}
}
}
其中分支判断当前文档是“最近”、带搜索结果的文档内容还是普通文档内容,这里只看showNormal
方法,其他不看,showNormal
中调用的是show
方法
进入DirectoryFragment
类
private static void show(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
String query, int anim) {
final Bundle args = new Bundle();
args.putInt(EXTRA_TYPE, type);
args.putParcelable(EXTRA_ROOT, root);
args.putParcelable(EXTRA_DOC, doc);
args.putString(EXTRA_QUERY, query);
final FragmentTransaction ft = fm.beginTransaction();
......
final DirectoryFragment fragment = new DirectoryFragment();
fragment.setArguments(args);
ft.replace(R.id.container_directory, fragment);
ft.commitAllowingStateLoss();
}
show
方法显示DirectoryFragment
自己
在onCreateView
中,初始化ListView
和GridView
,在onActivityCreated
方法中:
mCallbacks = new LoaderCallbacks<DirectoryResult>() {
@Override
public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
final String query = getArguments().getString(EXTRA_QUERY);
Uri contentsUri;
switch (mType) {
case TYPE_NORMAL:
contentsUri = DocumentsContract.buildChildDocumentsUri(
doc.authority, doc.documentId);
if (state.action == ACTION_MANAGE) {
contentsUri = DocumentsContract.setManageMode(contentsUri);
}
return new DirectoryLoader(
context, mType, root, doc, contentsUri, state.userSortOrder);
case TYPE_SEARCH:
contentsUri = DocumentsContract.buildSearchDocumentsUri(
root.authority, root.rootId, query);
if (state.action == ACTION_MANAGE) {
contentsUri = DocumentsContract.setManageMode(contentsUri);
}
return new DirectoryLoader(
context, mType, root, doc, contentsUri, state.userSortOrder);
case TYPE_RECENT_OPEN:
final RootsCache roots = DocumentsApplication.getRootsCache(context);
return new RecentLoader(context, roots, state);
default:
throw new IllegalStateException("Unknown type " + mType);
}
}
@Override
public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
if (result == null || result.exception != null) {
// onBackPressed does a fragment transaction, which can't be done inside
// onLoadFinished
mHandler.post(new Runnable() {
@Override
public void run() {
final Activity activity = getActivity();
if (activity != null) {
activity.onBackPressed();
}
}
});
return;
}
if (!isAdded()) return;
mAdapter.swapResult(result);
// Push latest state up to UI
// TODO: if mode change was racing with us, don't overwrite it
if (result.mode != MODE_UNKNOWN) {
state.derivedMode = result.mode;
}
state.derivedSortOrder = result.sortOrder;
((BaseActivity) context).onStateChanged();
updateDisplayState();
// When launched into empty recents, show drawer
if (mType == TYPE_RECENT_OPEN && mAdapter.isEmpty() && !state.stackTouched &&
context instanceof DocumentsActivity) {
((DocumentsActivity) context).setRootsDrawerOpen(true);
}
// Restore any previous instance state
final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) {
getView().restoreHierarchyState(container);
} else if (mLastSortOrder != state.derivedSortOrder) {
mListView.smoothScrollToPosition(0);
mGridView.smoothScrollToPosition(0);
}
mLastSortOrder = state.derivedSortOrder;
}
@Override
public void onLoaderReset(Loader<DirectoryResult> loader) {
mAdapter.swapResult(null);
}
};
使用loader机制加载文档内容,在onCreateLoader
返回DirectoryLoader
加载文档内容内容,加载完成回调onLoadFinished
传入加载的结果,最后通过mAdapter.swapResult(result)
将数据与Adapter绑定,Adapter有了数据就去更新界面。
那么从启动documentsUI到显示出所选菜单的内容整个过程就结束了,整个过程大致经过以下步骤:
- 响应Intent启动documentsUI,转到DocumentsActivity
- 保存Intent和应用显示状态的各种信息
- 通过RootsLoader加载侧滑菜单数据
- 点击菜单选项后,通过DirectoryLoader完成异步查询,加载显示文档数据
- 显示数据
其他
还需进一步了解的
- Loader机制
- 自定义View类:
DirectoryContainerView
、DirectoryView
、DocumentsToolBar
- 缩略图显示
documentsUI源码分析的更多相关文章
- ABP源码分析一:整体项目结构及目录
ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...
- HashMap与TreeMap源码分析
1. 引言 在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...
- nginx源码分析之网络初始化
nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化. 从配置文件中读取初始化信息 与网 ...
- zookeeper源码分析之五服务端(集群leader)处理请求流程
leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...
- zookeeper源码分析之四服务端(单机)处理请求流程
上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...
- zookeeper源码分析之三客户端发送请求流程
znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的 ...
- java使用websocket,并且获取HttpSession,源码分析
转载请在页首注明作者与出处 http://www.cnblogs.com/zhuxiaojie/p/6238826.html 一:本文使用范围 此文不仅仅局限于spring boot,普通的sprin ...
- ABP源码分析二:ABP中配置的注册和初始化
一般来说,ASP.NET Web应用程序的第一个执行的方法是Global.asax下定义的Start方法.执行这个方法前HttpApplication 实例必须存在,也就是说其构造函数的执行必然是完成 ...
- ABP源码分析三:ABP Module
Abp是一种基于模块化设计的思想构建的.开发人员可以将自定义的功能以模块(module)的形式集成到ABP中.具体的功能都可以设计成一个单独的Module.Abp底层框架提供便捷的方法集成每个Modu ...
随机推荐
- C语言集成开发环境vs2017的使用技巧之修改快捷键
首先这里是说编辑C语言内容,其次开发环境是vs2017(全称:visual studio 2017).像这个开发环境体积大,但你安装的时候不要安装到C盘,然后安装的时候选择模块,比如你不开发网站,就先 ...
- 那些日常琐事(iPhone上的细小提示,大数据分析)
今天早上蹲坑玩手机的时候,无意间看到了iPhone 给我一些提醒,震惊了我.也许你们会说,没什么大惊小怪的,当然做程序的都知道苹果公司早就记载了我们日常生活中很多数据,只是苹果公司目前还没做 ...
- Spring+SpringMVC+MyBatis深入学习及搭建(九)——MyBatis和Spring整合
转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/6964162.html 前面讲到:Spring+SpringMVC+MyBatis深入学习及搭建(八)--My ...
- Java中常见的数据结构的区别
把多个数据按照一定的存储方式,存储起来,称存储方式之为数据结构. 数据的存储方式有很多,数组,队列,链表,栈,哈希表等等. 不同的数据结构,性能是不一样的,比如有的插入比较快,查询比较快,但是删除比较 ...
- Unity3d: 资源释放时存储空间不足引发的思考和遇到的问题
手机游戏第一次启动基本上都会做资源释放的操作,这个时候需要考虑存储空间是否足够,但是Unity没有自带获取设备存储空间大小的 接口,需要调用本地方法分别去android或ios获取,这样挺麻烦的.而且 ...
- win8安装sql2008及设置登陆名问题
1. .net3.5安装 使用win8系统自带的升级功能无法成功安装.其实Windows8安装文件中已经集了.Net3.5, (1)此时只需要使用虚拟光驱加载Windows8 ...
- python3实现TCP协议的简单服务器和客户端
利用python3来实现TCP协议,和UDP类似.UDP应用于及时通信,而TCP协议用来传送文件.命令等操作,因为这些数据不允许丢失,否则会造成文件错误或命令混乱.下面代码就是模拟客户端通过命令行操作 ...
- homebrew & brew cask使用技巧及Mac软件安装
homebrew 安装 /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/m ...
- 解决Ubuntu开关机动画不正常方法
联想的笔记本,显卡NVIDIA GT218M,默认使用开源的驱动,但挂起后,再唤醒就黑屏回不到桌面. 1.解决办法:安装NVIDIA专有驱动 $sudo apt-get install nvidia- ...
- [0] MVC&MVP&MVVM差异点
MVC: 用户的请求首先会到达Controller,由Controller从Model获取数据,选择合适的View,把处理结果呈现到View上: MVP: 用户的请求首先会到达View,View传递请 ...