【Android Jetpack高手日志】DataBinding 从入门到精通
前言
DataBinding 数据绑定库是 Android Jetpack 的一部分,借助该库可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。我个人觉得,使用 DataBinding 时不要在 xml 布局文件中写复杂的逻辑,只负责绑定数据。只是负责将最终的数据和 UI 直接绑定,只是一个末端赋值而已,不涉及复杂的 UI 逻辑,而且避免了代码中大量冗余代码的判空处理,同时避免了那些常见的 setVisible 等样板方法的调用,简化开发流程,统一 UI 的数据来源。
基本使用
简单使用入门
xml
布局如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.jackie.jetpackdemo.data.TestInfo"/>
<variable
name="userInfo"
type="TestInfo" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btnGetUserInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="获取用户信息"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/txtUserName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@{userInfo.age}"
app:layout_constraintTop_toBottomOf="@+id/btnGetUserInfo"
android:layout_marginTop="30dp"
android:textSize="30dp"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="text"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@+id/txtUserName"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Activity 中调用代码如下:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activityBinding: ActivityMainBinding = DataBindingUtil.setContentView(this,R.layout.activity_main)
activityBinding.lifecycleOwner = this
activityBinding.userInfo = TestInfo("lsm","lsj")
}
TestInfo 的定义如下:
public class TestInfo extends BaseObservable { //继承 BaseObservable
private String age;
private String name;
public TestInfo(String age,String name){
this.name = name;
this.age = age;
}
public void setAge(String age) {
this.age = age;
notifyPropertyChanged(BR.age); //需要变更的变量的 set 方法中加上 notifyPropertyChanged
}
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name); //需要变更的变量的 set 方法中加上 notifyPropertyChanged
}
@Bindable //需要变更的变量还要加上 @Bindable 注解
public String getAge() {
return age;
}
@Bindable //需要变更的变量还要加上 @Bindable 注解
public String getName() {
return name;
}
}
TestInfo
需要继承BaseObservable
,同时对于需要监听变化的变量加上@Bindable
注解,同时该变量的set
方法还要加上notifyPropertyChanged
,BR.xxx
是注解生成的。
数据的双向绑定
使用单向数据绑定时,您可以为特性设置值,并设置对该特性的变化作出反应的监听器:
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
/>
双向数据绑定为此过程提供了一种快捷方式:
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@={viewmodel.rememberMe}"
/>
@={}
表示法(其中重要的是包含“=”符号)可接收属性的数据更改并同时监听用户更新。其他的设置和前面的单向数据绑定一致。
结合 LiveData 使用
内容参考自这里,我们上面在使用 DataBinding 时,TestInfo 还要继承 BaseObserble
,使用注解、notifyPropertyChanged(),使用起来其实挺复杂,而且还有侵入性。LiveData 结合 DataBinding 的使用步骤如下:
- 使用 LiveData 对象作为数据绑定来源,需要设置 LifecycleOwner。
- xml 中定义变量 ViewModel,并使用 ViewModel。
- binding 设置变量 ViewModel。
//结合DataBinding使用的ViewModel
//1\. 要使用LiveData对象作为数据绑定来源,需要设置LifecycleOwner
binding.setLifecycleOwner(this);
ViewModelProvider viewModelProvider = new ViewModelProvider(this);
mUserViewModel = viewModelProvider.get(UserViewModel.class);
//3\. 设置变量ViewModel
binding.setVm(mUserViewModel);
xml 文件的定义如下:
<!-- 2\. 定义ViewModel 并绑定-->
<variable
name="vm"
type="com.hfy.demo01.module.jetpack.databinding.UserViewModel" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{vm.userLiveData.name}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{vm.userLiveData.level}"/>
这样就ok了,你会发现 我们不需要在 Activity 中拿到 LivaData 去 observe(owner,observer)了,DataBinding 自动生成的代码,会帮我们去做这操作,所以需要设置LifecycleOwner。
使用自定义特性的双向数据绑定
例如,如果要在名为 MyView
的自定义视图中对 "time"
特性启用双向数据绑定,请完成以下步骤:
- 使用
@BindingAdapter
,对用来设置初始值并在值更改时进行更新的方法进行注释:
@BindingAdapter("time")
@JvmStatic fun setTime(view: MyView, newValue: Time) {
// Important to break potential infinite loops.
if (view.time != newValue) {
view.time = newValue
}
}
- 使用
@InverseBindingAdapter
对从视图中读取值的方法进行注释:
@InverseBindingAdapter("time")
@JvmStatic fun getTime(view: MyView) : Time {
return view.getTime()
}
更多内容请参考这里。
源码分析
我们在路径app/build/intermediates/data_binding_layout_info_type_merge/debug/out/activity_main-layout.xml
查看文件
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout directory="layout" filePath="app/src/main/res/layout/activity_main.xml"
isBindingData="true" isMerge="false" layout="activity_main"
modulePackage="com.jackie.jetpackdemo" rootNodeType="androidx.constraintlayout.widget.ConstraintLayout">
<Variables name="userInfo" declared="true" type="TestInfo">
<location endLine="9" endOffset="29" startLine="7" startOffset="8" />
</Variables>
<Imports name="TestInfo" type="com.jackie.jetpackdemo.data.TestInfo">
<location endLine="6" endOffset="60" startLine="6" startOffset="8" />
</Imports>
<Targets>
<Target tag="layout/activity_main_0"
view="androidx.constraintlayout.widget.ConstraintLayout">
<Expressions />
<location endLine="47" endOffset="55" startLine="12" startOffset="4" />
</Target>
<Target id="@+id/txtUserName" tag="binding_1" view="TextView">
<Expressions>
<Expression attribute="android:text" text="userInfo.age">
<Location endLine="32" endOffset="41" startLine="32" startOffset="12" />
<TwoWay>false</TwoWay>
<ValueLocation endLine="32" endOffset="39" startLine="32" startOffset="28" />
</Expression>
</Expressions>
<location endLine="37" endOffset="13" startLine="27" startOffset="8" />
</Target>
<Target id="@+id/btnGetUserInfo" view="Button">
<Expressions />
<location endLine="25" endOffset="55" startLine="17" startOffset="8" />
</Target>
</Targets>
</Layout>
可以看到<Targets>
标签下面的就是我们布局,分成具体的子<Target>
标签对应具体的 ConstraintLayout、TextView等,activity_main_0
对应我们的ConstraintLayout
,再来路径app/build/intermediates/incremental/mergeDebugResources/stripped.dir/layout/activity_main.xml
下
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" android:tag="layout/activity_main_0" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools">
<Button
android:id="@+id/btnGetUserInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="获取用户信息"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/txtUserName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:tag="binding_1"
app:layout_constraintTop_toBottomOf="@+id/btnGetUserInfo"
android:layout_marginTop="30dp"
android:textSize="30dp"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="text"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@+id/txtUserName"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
这个文件其实就是移除了<layout>、<data>
标签的布局文件,里面的 tag 就是我们上面对应<Target>
标签中的tag
,Expression attribute="android:text" text="userInfo.age"
<Expression>
中的具体属性对应具体的值。
初始化
再来从val activityBinding: ActivityMainBinding = DataBindingUtil.setContentView(this,R.layout.activity_main)
来分析源码,
//DataBindingUtil.java
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
int layoutId, @Nullable DataBindingComponent bindingComponent) {
activity.setContentView(layoutId); //还是需要setContentView
View decorView = activity.getWindow().getDecorView();
ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}
我们设置的activity_xxx.xml
其实是在android.R.id.content
下面的,继续来看bindToAddedViews
方法
//DataBindingUtil.java
private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
ViewGroup parent, int startChildren, int layoutId) {
final int endChildren = parent.getChildCount();
final int childrenAdded = endChildren - startChildren;
if (childrenAdded == 1) {
final View childView = parent.getChildAt(endChildren - 1);
return bind(component, childView, layoutId); //调用bind
} else {
final View[] children = new View[childrenAdded];
for (int i = 0; i < childrenAdded; i++) {
children[i] = parent.getChildAt(i + startChildren);
}
return bind(component, children, layoutId); //调用bind
}
}
}
最终会调到bind
方法
//DataBindingUtil.java
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
int layoutId) {
return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
}
sMapper
是DataBinderMapper
,其真正实现类是通过 APT 生成的DataBinderMapperImpl(app/build/generated/ap_generated_sources/debug/out/com/jackie/jetpackdemo/DataBinderMapperImpl.java)
public class DataBinderMapperImpl extends DataBinderMapper {
···
@Override
public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
if(localizedLayoutId > 0) {
final Object tag = view.getTag();
if(tag == null) {
throw new RuntimeException("view must have a tag");
}
switch(localizedLayoutId) {
case LAYOUT_ACTIVITYMAIN: {
if ("layout/activity_main_0".equals(tag)) {
return new ActivityMainBindingImpl(component, view); //关键代码,new ActivityMainBindingImpl
}
···
接下来我们来分析ActivityMainBindingImpl(app/build/generated/ap_generated_sources/debug/out/com/jackie/jetpackdemo/databinding/ActivityMainBindingImpl.java)
这个类,它也是 APT 生成的,
//ActivityMainBindingImpl.java
public ActivityMainBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
this(bindingComponent, root, mapBindings(bindingComponent, root, 3, sIncludes, sViewsWithIds));
}
private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
super(bindingComponent, root, 1
, (android.widget.Button) bindings[2]
, (android.widget.TextView) bindings[1]
);
this.mboundView0 = (androidx.constraintlayout.widget.ConstraintLayout) bindings[0];
this.mboundView0.setTag(null);
this.txtUserName.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}
我们调用了第一个方法,里面的这个 3 代表着我们布局文件中有三个节点(ConstraintLayout,Button,TextView),但是我们前面的布局中明明还有一个TextView
,为什么没有呢?因为我们这个TextView
我们并没有设置它的 Id,所以没有生成,如果设置后重新 build 下 3 就会变成 4 了。
继续来看mapBindings
方法:
protected static Object[] mapBindings(DataBindingComponent bindingComponent, View root,
int numBindings, IncludedLayouts includes, SparseIntArray viewsWithIds) {
Object[] bindings = new Object[numBindings];
mapBindings(bindingComponent, root, bindings, includes, viewsWithIds, true);
return bindings;
}
它首先是 new 一个大小为 3 的对象数组,然后把这三个标签解析完放到该数组中。上面的ActivityMainBindingImpl
公有构造器会调用私有构造器,再回过头来看
val activityBinding: ActivityMainBinding = DataBindingUtil.setContentView(this,R.layout.activity_main)
执行完这个代码,activityBinding
中已经有这 3 个对象了,所以可以进行这样的调用
activityBinding.txtUserName
activityBinding.btnGetUserInfo
到目前为止,初始化已经完成了。
调用流程
接下来我们从这个调用开始分析
activityBinding.userInfo = TestInfo("lsm","lsj")
这个activityBinding.userInfo
调用的实际上是ActivityMainBinding
中的setUserInfo
方法
//ActivityMainBinding.java
public void setUserInfo(@Nullable com.jackie.jetpackdemo.data.TestInfo UserInfo) {
updateRegistration(0, UserInfo);
this.mUserInfo = UserInfo;
synchronized(this) {
mDirtyFlags |= 0x1L;
}
notifyPropertyChanged(BR.userInfo);
super.requestRebind();
}
这里的updateRegistration
方法如下
//localFieldId 为 BR 文件中的Id,observable 就是观察者
protected boolean updateRegistration(int localFieldId, Observable observable) {
return updateRegistration(localFieldId, observable, CREATE_PROPERTY_LISTENER);
}
/**
* Method object extracted out to attach a listener to a bound Observable object.
*/
private static final CreateWeakListener CREATE_PROPERTY_LISTENER = new CreateWeakListener() {
@Override
public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) {
return new WeakPropertyListener(viewDataBinding, localFieldId).getListener();
}
};
CREATE_PROPERTY_LISTENER
名字也很直白,表示创建一个属性的监听器,也就是说属性发生变化的时候WeakPropertyListener
监听器会被回调。
localFieldId
为 BR 文件中的 Id,BR 文件是什么呢?
public class BR {
public static final int _all = 0;
public static final int age = 1;
public static final int name = 2;
public static final int userInfo = 3;
}
因为我们的 xml 文件中导入了TestInfo(userInfo)
,我们也在age
和name
属性上加上了@Bindable
注解,所以生成了上面的BR文件。因为我们是上面调用的是setUserInfo
方法,所以传入的是 0。
用该方式设置name
也可以
activityBinding.setVariable(BR.name,"Jackie")
而updateRegistration
中的observable
就是我们传入的(TestInfo)UserInfo
,再来看看updateRegistration
方法
//ViewDataBinding
private boolean updateRegistration(int localFieldId, Object observable,
CreateWeakListener listenerCreator) {
if (observable == null) {
return unregisterFrom(localFieldId);
}
WeakListener listener = mLocalFieldObservers[localFieldId];
if (listener == null) {
registerTo(localFieldId, observable, listenerCreator);
return true;
}
if (listener.getTarget() == observable) {
return false;//nothing to do, same object
}
unregisterFrom(localFieldId);
registerTo(localFieldId, observable, listenerCreator);
return true;
}
mLocalFieldObservers
数组中绑定了每一个属性对应的监听器,比如我们上面的 BR 中的四个值。
如果监听器为空,调用registerTo
创建监听器并注册
protected void registerTo(int localFieldId, Object observable,
CreateWeakListener listenerCreator) {
if (observable == null) {
return;
}
WeakListener listener = mLocalFieldObservers[localFieldId];
if (listener == null) {
listener = listenerCreator.create(this, localFieldId);
mLocalFieldObservers[localFieldId] = listener;
if (mLifecycleOwner != null) {
listener.setLifecycleOwner(mLifecycleOwner);
}
}
listener.setTarget(observable);
}
setTarget
就是给被观察者添加监听器
public void setTarget(T object) {
unregister();
mTarget = object;
if (mTarget != null) {
mObservable.addListener(mTarget);
}
}
这里的mObservable
的实现类是WeakPropertyListener
,也就是每个属性发生变化后都会进行回调
@Override
public void addListener(Observable target) {
target.addOnPropertyChangedCallback(this);
}
而target
的实现类是BaseObservable
,这也就是我们的TestInfo
为什么要继承BaseObservable
了。
public class BaseObservable implements Observable {
private transient PropertyChangeRegistry mCallbacks;
public BaseObservable() {
}
@Override
public void addOnPropertyChangedCallback(@NonNull OnPropertyChangedCallback callback) {
synchronized (this) {
if (mCallbacks == null) {
mCallbacks = new PropertyChangeRegistry();
}
}
mCallbacks.add(callback);
}
总的关系图如下:
PropertryChangeRegistry
中的add(ViewDataBinding)
是将观察者和被观察者绑定起来,ViewDataBinding
中的WeakListener[] mLocalFieldObservers
中的每个变量都有一个WeakListener
,BaseObservable
中的addOnPropertyChangedCallback(WeakPropertyListener)
就是添加属性变化回调。
MainActivity 调用 setUserInfo 流程图
如果上面的关系图不清楚的话,我也把流程图画出来,你可以查看一下
ActivityMainBindingImpl
继承于ActivityMainBinding
,而ActivityMainBinding
继承于ViewDataBinding
最终是在TextViewBindingAdapter
中setText
来实现。
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don't set anything.
}
view.setText(text);
}
没调用 setUser 之前的数据绑定是怎么做的?
前面我们讲了初始化过程,因为ActivityMainBindingImpl
继承于ActivityMainBinding
,而ActivityMainBinding
继承于ViewDataBinding
,在ViewDataBinding
中静态初始化块如下
static {
if (VERSION.SDK_INT < VERSION_CODES.KITKAT) {
ROOT_REATTACHED_LISTENER = null;
} else {
ROOT_REATTACHED_LISTENER = new OnAttachStateChangeListener() {
@TargetApi(VERSION_CODES.KITKAT)
@Override
public void onViewAttachedToWindow(View v) {
// execute the pending bindings.
final ViewDataBinding binding = getBinding(v);
binding.mRebindRunnable.run();
v.removeOnAttachStateChangeListener(this);
}
@Override
public void onViewDetachedFromWindow(View v) {
}
};
}
}
会执行mRebindRunnable
的run
方法
private final Runnable mRebindRunnable = new Runnable() {
@Override
public void run() {
synchronized (this) {
mPendingRebind = false;
}
processReferenceQueue();
if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
// Nested so that we don't get a lint warning in IntelliJ
if (!mRoot.isAttachedToWindow()) {
// Don't execute the pending bindings until the View
// is attached again.
mRoot.removeOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
mRoot.addOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
return;
}
}
executePendingBindings();
}
};
然后执行executePendingBindings
方法,后面就和我们前面分析过的的流程一样了。最终会会调到
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don't set anything.
}
view.setText(text);
}
我们一开始text
为空,所以就直接返回了。
为什么设计如此复杂,以前我们用的话观察者和被观察者都是单独的类,现在因为可能有多个ViewModel(TestInfo、xxxInfo等)
,同时也可能有多个Activity
(分别进行绑定),使用原来的方式消耗很大。BR 文件会生成多个字段,用一个mLocalFieldObservers
进行处理,每个字段有其具体的监听,每个xxxInfo 有其具体的监听。设计的还是挺合理的。
总结
本文先从 DataBinding 的简单使用,单向/双向绑定,结合LiveData 使用,同时也可以使用一些自定义特性,最后是进行源码分析。最后一定要注意的是不要在 xml 中做复杂的逻辑判断,只是将其看做是一个方便末端 UI 展示的支持库即可,同时也避免了大量的判空处理,同时也统一了数据的来源。
献给读者
随着互联网企业的不断发展,产品项目中的模块越来越多,用户体验要求也越来越高,想实现小步快跑、快速迭代的目的越来越难,还有65535,应用之间的互相调用等等问题,插件化技术应用而生。如果没有插件化技术,美团、淘宝这些集成了大量“app”的应用,可能会有几个g那么大。
所以,当今的Android移动开发,不会热修复、插件化、组件化,80%以上的面试都过不了。
Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android开源框架。系统教程知识笔记已整理成PDF电子书上传在【GitHub】
1042页完整版PDF点击我就可以白嫖啦,记得给文章点个赞哦。
文末
感谢大家关注我,分享Android干货,交流Android技术。
对文章有何见解,或者有何技术问题,都可以在评论区一起留言讨论,我会虔诚为你解答。
也欢迎大家来我的B站找我玩,有各类Android架构师进阶技术难点的视频讲解,助你早日升职加薪。
B站直通车:https://space.bilibili.com/544650554
【Android Jetpack高手日志】DataBinding 从入门到精通的更多相关文章
- Android开发书籍推荐:从入门到精通系列学习路线书籍介绍
Android开发书籍推荐:从入门到精通系列学习路线书籍介绍 很多时候我们都会不断收到新手的提问"Android开发的经典入门教材和学习路线?"."Android 开发入 ...
- (转)Android开发书籍推荐:从入门到精通系列学习路线书籍介绍
Android开发书籍推荐:从入门到精通系列学习路线书籍介绍 转自:http://blog.csdn.net/findsafety/article/details/52317506 很多时候我们都会不 ...
- Android OpenGL ES 开发教程 从入门到精通
感谢,摘自:http://blog.csdn.net/mapdigit/article/details/7526556 Android OpenGL ES 简明开发教程 Android OpenGL ...
- 学习Android Jetpack? 入门教程和进阶实战这里全都有!
前言 2018年谷歌I/O,Jetpack横空出世,官方介绍如下: Jetpack 是一套库.工具和指南,可帮助开发者更轻松地编写优质应用.这些组件可帮助您遵循最佳做法.让您摆脱编写样板代码的工作并简 ...
- Android Jetpack从入门到精通(深度好文,值得收藏)
前言 即学即用Android Jetpack系列Blog的目的是通过学习Android Jetpack完成一个简单的Demo,本文是即学即用Android Jetpack系列Blog的第一篇. 记得去 ...
- Android官方数据绑定框架DataBinding
数据绑定框架给我们带来了更大的方便性,以前我们可能需要在Activity里写很多的findViewById,烦人的代码也增加了我们代码的耦合性,现在我们马上就可以抛弃那么多的findViewById. ...
- Android数据库高手秘籍(一)——SQLite命令
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/38461239 要想熟练地操作不论什么一个数据库.最最主要的要求就是要懂SQL语言, ...
- Android Studio2.0 教程从入门到精通Windows版 - 入门篇
http://www.open-open.com/lib/view/open1468121363300.html 本文转自:深度开源(open-open.com)原文标题:Android Studio ...
- Android开发高手课NOTE
最近学习了极客时间的<Android开发高手课>很有收获,记录总结一下. 欢迎学习老师的专栏:Android开发高手课 内存优化 卡顿的原因 频繁 GC 造成卡顿.物理内存不足时系统会触发 ...
随机推荐
- 七种方案!探讨Redis分布式锁的正确使用姿势
前言 日常开发中,秒杀下单.抢红包等等业务场景,都需要用到分布式锁.而Redis非常适合作为分布式锁使用.本文将分七个方案展开,跟大家探讨Redis分布式锁的正确使用方式.如果有不正确的地方,欢迎大家 ...
- AtCoder Beginner Contest 190
A Very Very Primitive Game int main() { int a, b, c; cin >> a >> b >> c; if(c == 0 ...
- 修改 Hosts 解决 Github 访问缓慢问题
背景 最近访问 Github 经常出现访问速度慢的问题,甚至会出现无法连接的情况.有一天,在一次家常聊天中提到了这个事情,有一位热心的 Gitee 朋友就说:你改一下 Hosts 文件就可以了.修改了 ...
- Linux 文件和目录管理
绝对路径:路径的写法一定由根目录/写起的,例如 /usr/local/mysql 相对路径:和绝对路径相反 不是由根目录/写起的,例如用户首先进入到/home,然后进入test 执行命令:cd /ho ...
- Distributed Cache(分布式缓存)-SqlServer
分布式缓存是由多个应用服务器共享的缓存,通常作为外部服务存储在单个应用服务器上,常用的有SqlServer,Redis,NCache. 分布式缓存可以提高ASP.NET Core应用程序的性能和可伸缩 ...
- arcgis10.2 全套安装教程
一.安装LicenseManager 1.运行在licensemanager目录下的Setup.exe 2.安装完成后点击stop停止服务,然后点击"OK" 3.选择ArcGIS1 ...
- Python中树的遍历-堆排序
1.二叉树的遍历 遍历:迭代所有元素一遍. 树的遍历:对树中所有的元素不重复的访问一遍,也成扫描 广度优先遍历:层序遍历 深度优先遍历:前序.中序.后续遍历. 遍历序列:将树中所有元素遍历一遍后,得到 ...
- 初见pyecharts
pyecharts(可互动的可视化) 模块准备 在Anaconda Prompt中下载pyecharts v1版本(>=python3.6) pip install pyecharts 可视化最 ...
- FHRP - 网关冗余协议
通常情况下,在终端设备进入网络前,都会有一个 Router 充当网络,作为第一跳的网络地址.但假设路由器发生故障,此时终端设备就无法再接入互联网. 为了防止这样的问题,一般会再加入一台路由器充当备份. ...
- swing实现QQ登录界面1.0( 实现了同一张图片只加载一次)、(以及实现简单的布局面板添加背景图片控件的标签控件和添加一个关闭按钮控件)
swing实现QQ登录界面1.0( 实现了同一张图片只加载一次).(以及实现简单的布局面板添加背景图片控件的标签控件和添加一个关闭按钮控件) 代码思路分析: 1.(同一张图片仅仅需要加载一次就够了,下 ...