精华内容
下载资源
问答
  • Android Fragment完全解析,关于碎片你所需知道的一切

    万次阅读 多人点赞 2013-05-07 09:33:45
    我们都知道,Android上的界面展示都是通过Activity实现的,Activity实在是太常用了,我相信大家都已经非常熟悉了,这里就不再赘述。 但是Activity也有它的局限性,同样的界面在手机上显示可能很好看,在平板上就未必...

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/8881711


    我们都知道,Android上的界面展示都是通过Activity实现的,Activity实在是太常用了,我相信大家都已经非常熟悉了,这里就不再赘述。


    但是Activity也有它的局限性,同样的界面在手机上显示可能很好看,在平板上就未必了,因为平板的屏幕非常大,手机的界面放在平板上可能会有过分被拉长、控件间距过大等情况。这个时候更好的体验效果是在Activity中嵌入"小Activity",然后每个"小Activity"又可以拥有自己的布局。因此,我们今天的主角Fragment登场了。


    Fragment初探


    为了让界面可以在平板上更好地展示,Android在3.0版本引入了Fragment(碎片)功能,它非常类似于Activity,可以像Activity一样包含布局。Fragment通常是嵌套在Activity中使用的,现在想象这种场景:有两个Fragment,Fragment 1包含了一个ListView,每行显示一本书的标题。Fragment 2包含了TextView和ImageView,来显示书的详细内容和图片。


    如果现在程序运行竖屏模式的平板或手机上,Fragment 1可能嵌入在一个Activity中,而Fragment 2可能嵌入在另一个Activity中,如下图所示:




    而如果现在程序运行在横屏模式的平板上,两个Fragment就可以嵌入在同一个Activity中了,如下图所示:




    由此可以看出,使用Fragment可以让我们更加充分地利用平板的屏幕空间,下面我们一起来探究下如何使用Fragment。


    首先需要注意,Fragment是在3.0版本引入的,如果你使用的是3.0之前的系统,需要先导入android-support-v4的jar包才能使用Fragment功能。


    新建一个项目叫做Fragments,然后在layout文件夹下新建一个名为fragment1.xml的布局文件:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#00ff00" >
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="This is fragment 1"
            android:textColor="#000000"
            android:textSize="25sp" />
    
    </LinearLayout>

    可以看到,这个布局文件非常简单,只有一个LinearLayout,里面加入了一个TextView。我们如法炮制再新建一个fragment2.xml :

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffff00" >
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="This is fragment 2"
            android:textColor="#000000"
            android:textSize="25sp" />
    
    </LinearLayout>

    然后新建一个类Fragment1,这个类是继承自Fragment的:

    public class Fragment1 extends Fragment {
    
    	@Override
    	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    		return inflater.inflate(R.layout.fragment1, container, false);
    	}
    
    }
    我们可以看到,这个类也非常简单,主要就是加载了我们刚刚写好的fragment1.xml布局文件并返回。同样的方法,我们再写好Fragment2 :
    public class Fragment2 extends Fragment {
    
    	@Override
    	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    		return inflater.inflate(R.layout.fragment2, container, false);
    	}
    
    }
    
    然后打开或新建activity_main.xml作为主Activity的布局文件,在里面加入两个Fragment的引用,使用android:name前缀来引用具体的Fragment:
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:baselineAligned="false" >
    
        <fragment
            android:id="@+id/fragment1"
            android:name="com.example.fragmentdemo.Fragment1"
            android:layout_width="0dip"
            android:layout_height="match_parent"
            android:layout_weight="1" />
    
        <fragment
            android:id="@+id/fragment2"
            android:name="com.example.fragmentdemo.Fragment2"
            android:layout_width="0dip"
            android:layout_height="match_parent"
            android:layout_weight="1" />
    
    </LinearLayout>
    最后打开或新建MainActivity作为程序的主Activity,里面的代码非常简单,都是自动生成的:
    public class MainActivity extends Activity {
    
    	@Override
    	protected void onCreate(Bundle savedInstanceState) {
    		super.onCreate(savedInstanceState);
    		setContentView(R.layout.activity_main);
    	}
    
    }

    现在我们来运行一次程序,就会看到,一个Activity很融洽地包含了两个Fragment,这两个Fragment平分了整个屏幕,效果图如下:




    动态添加Fragment


    你已经学会了如何在XML中使用Fragment,但是这仅仅是Fragment最简单的功能而已。Fragment真正的强大之处在于可以动态地添加到Activity当中,因此这也是你必须要掌握的东西。当你学会了在程序运行时向Activity添加Fragment,程序的界面就可以定制的更加多样化。下面我们立刻来看看,如何动态添加Fragment。


    还是在上一节代码的基础上修改,打开activity_main.xml,将其中对Fragment的引用都删除,只保留最外层的LinearLayout,并给它添加一个id,因为我们要动态添加Fragment,不用在XML里添加了,删除后代码如下:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/main_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:baselineAligned="false" >
    
    </LinearLayout>
    然后打开MainActivity,修改其中的代码如下所示:
    public class MainActivity extends Activity {
    
    	@Override
    	protected void onCreate(Bundle savedInstanceState) {
    		super.onCreate(savedInstanceState);
    		setContentView(R.layout.activity_main);
    		Display display = getWindowManager().getDefaultDisplay();
    		if (display.getWidth() > display.getHeight()) {
    			Fragment1 fragment1 = new Fragment1();
    			getFragmentManager().beginTransaction().replace(R.id.main_layout, fragment1).commit();
    		} else {
    			Fragment2 fragment2 = new Fragment2();
    			getFragmentManager().beginTransaction().replace(R.id.main_layout, fragment2).commit();
    		}
    	}
    
    }

    首先,我们要获取屏幕的宽度和高度,然后进行判断,如果屏幕宽度大于高度就添加fragment1,如果高度大于宽度就添加fragment2。动态添加Fragment主要分为4步:


    1.获取到FragmentManager,在Activity中可以直接通过getFragmentManager得到。

    2.开启一个事务,通过调用beginTransaction方法开启。

    3.向容器内加入Fragment,一般使用replace方法实现,需要传入容器的id和Fragment的实例。

    4.提交事务,调用commit方法提交。


    现在运行一下程序,效果如下图所示:




    如果你是在使用模拟器运行,按下ctrl + F11切换到竖屏模式。效果如下图所示:




    Fragment的生命周期


    和Activity一样,Fragment也有自己的生命周期,理解Fragment的生命周期非常重要,我们通过代码的方式来瞧一瞧Fragment的生命周期是什么样的:

    public class Fragment1 extends Fragment {
    	public static final String TAG = "Fragment1";
    
    	@Override
    	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    		Log.d(TAG, "onCreateView");
    		return inflater.inflate(R.layout.fragment1, container, false);
    	}
    
    	@Override
    	public void onAttach(Activity activity) {
    		super.onAttach(activity);
    		Log.d(TAG, "onAttach");
    	}
    
    	@Override
    	public void onCreate(Bundle savedInstanceState) {
    		super.onCreate(savedInstanceState);
    		Log.d(TAG, "onCreate");
    	}
    
    	@Override
    	public void onActivityCreated(Bundle savedInstanceState) {
    		super.onActivityCreated(savedInstanceState);
    		Log.d(TAG, "onActivityCreated");
    	}
    
    	@Override
    	public void onStart() {
    		super.onStart();
    		Log.d(TAG, "onStart");
    	}
    
    	@Override
    	public void onResume() {
    		super.onResume();
    		Log.d(TAG, "onResume");
    	}
    
    	@Override
    	public void onPause() {
    		super.onPause();
    		Log.d(TAG, "onPause");
    	}
    
    	@Override
    	public void onStop() {
    		super.onStop();
    		Log.d(TAG, "onStop");
    	}
    
    	@Override
    	public void onDestroyView() {
    		super.onDestroyView();
    		Log.d(TAG, "onDestroyView");
    	}
    
    	@Override
    	public void onDestroy() {
    		super.onDestroy();
    		Log.d(TAG, "onDestroy");
    	}
    
    	@Override
    	public void onDetach() {
    		super.onDetach();
    		Log.d(TAG, "onDetach");
    	}
    
    }

    可以看到,上面的代码在每个生命周期的方法里都打印了日志,然后我们来运行一下程序,可以看到打印日志如下:




    这时点击一下home键,打印日志如下:




    如果你再重新进入进入程序,打印日志如下:




    然后点击back键退出程序,打印日志如下:




    看到这里,我相信大多数朋友已经非常明白了,因为这和Activity的生命周期太相似了。只是有几个Activity中没有的新方法,这里需要重点介绍一下:

    • onAttach方法:Fragment和Activity建立关联的时候调用。
    • onCreateView方法:为Fragment加载布局时调用。
    • onActivityCreated方法:当Activity中的onCreate方法执行完后调用。
    • onDestroyView方法:Fragment中的布局被移除时调用。
    • onDetach方法:Fragment和Activity解除关联的时候调用。


    Fragment之间进行通信


    通常情况下,Activity都会包含多个Fragment,这时多个Fragment之间如何进行通信就是个非常重要的问题了。我们通过一个例子来看一下,如何在一个Fragment中去访问另一个Fragment的视图。


    还是在第一节代码的基础上修改,首先打开fragment2.xml,在这个布局里面添加一个按钮:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:background="#ffff00" >
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="This is fragment 2"
            android:textColor="#000000"
            android:textSize="25sp" />
        
        <Button 
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Get fragment1 text"
            />
    
    </LinearLayout>
    然后打开fragment1.xml,为TextView添加一个id:
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#00ff00" >
    
        <TextView
            android:id="@+id/fragment1_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="This is fragment 1"
            android:textColor="#000000"
            android:textSize="25sp" />
    
    </LinearLayout>
    接着打开Fragment2.java,添加onActivityCreated方法,并处理按钮的点击事件:
    public class Fragment2 extends Fragment {
    
    	@Override
    	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    		return inflater.inflate(R.layout.fragment2, container, false);
    	}
    
    	@Override
    	public void onActivityCreated(Bundle savedInstanceState) {
    		super.onActivityCreated(savedInstanceState);
    		Button button = (Button) getActivity().findViewById(R.id.button);
    		button.setOnClickListener(new OnClickListener() {
    			@Override
    			public void onClick(View v) {
    				TextView textView = (TextView) getActivity().findViewById(R.id.fragment1_text);
    				Toast.makeText(getActivity(), textView.getText(), Toast.LENGTH_LONG).show();
    			}
    		});
    	}
    
    }
    

    现在运行一下程序,并点击一下fragment2上的按钮,效果如下图所示:



    我们可以看到,在fragment2中成功获取到了fragment1中的视图,并弹出Toast。这是怎么实现的呢?主要都是通过getActivity这个方法实现的。getActivity方法可以让Fragment获取到关联的Activity,然后再调用Activity的findViewById方法,就可以获取到和这个Activity关联的其它Fragment的视图了。


    好了,以上就是关于Fragment你所须知道的一切。如果想要切身体验一下Fragment的实战,请继续阅读 Android手机平板两不误,使用Fragment实现兼容手机和平板的程序 以及 Android Fragment应用实战,使用碎片向ActivityGroup说再见 。


    关注我的技术公众号,每天都有优质技术文章推送。关注我的娱乐公众号,工作、学习累了的时候放松一下自己。

    微信扫一扫下方二维码即可关注:

            

    展开全文
  • Service作为Android四大组件之一,在每一个应用程序中都扮演着非常重要的角色。它主要用于在后台处理一些耗时的逻辑,或者去执行某些需要长期运行的任务。必要的时候我们甚至可以在程序退出的情况下,让Service继续...

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/11952435


    相信大多数朋友对Service这个名词都不会陌生,没错,一个老练的Android程序员如果连Service都没听说过的话,那确实也太逊了。Service作为Android四大组件之一,在每一个应用程序中都扮演着非常重要的角色。它主要用于在后台处理一些耗时的逻辑,或者去执行某些需要长期运行的任务。必要的时候我们甚至可以在程序退出的情况下,让Service在后台继续保持运行状态。


    不过,虽然Service几乎被每一个Android程序员所熟知,但并不是每个人都已经将Service的各个知识点都掌握得非常透彻。那么今天我就将带着大家对Service进行一次全面、深入的探究,希望每个人在读完本篇文章后都能对Service有更深一层的理解。


    Service的基本用法


    关于Service最基本的用法自然就是如何启动一个Service了,启动Service的方法和启动Activity很类似,都需要借助Intent来实现,下面我们就通过一个具体的例子来看一下。

    新建一个Android项目,项目名就叫ServiceTest,这里我选择使用4.0的API。


    然后新建一个MyService继承自Service,并重写父类的onCreate()、onStartCommand()和onDestroy()方法,如下所示:

    public class MyService extends Service {
    
    	public static final String TAG = "MyService";
    
    	@Override
    	public void onCreate() {
    		super.onCreate();
    		Log.d(TAG, "onCreate() executed");
    	}
    
    	@Override
    	public int onStartCommand(Intent intent, int flags, int startId) {
    		Log.d(TAG, "onStartCommand() executed");
    		return super.onStartCommand(intent, flags, startId);
    	}
    	
    	@Override
    	public void onDestroy() {
    		super.onDestroy();
    		Log.d(TAG, "onDestroy() executed");
    	}
    
    	@Override
    	public IBinder onBind(Intent intent) {
    		return null;
    	}
    
    }

    可以看到,我们只是在onCreate()、onStartCommand()和onDestroy()方法中分别打印了一句话,并没有进行其它任何的操作。


    然后打开或新建activity_main.xml作为程序的主布局文件,代码如下所示:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
    
        <Button
            android:id="@+id/start_service"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Start Service" />
    
        <Button
            android:id="@+id/stop_service"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Stop Service" />
    
    </LinearLayout>

    我们在布局文件中加入了两个按钮,一个用于启动Service,一个用于停止Service。


    然后打开或新建MainActivity作为程序的主Activity,在里面加入启动Service和停止Service的逻辑,代码如下所示:

    public class MainActivity extends Activity implements OnClickListener {
    
    	private Button startService;
    
    	private Button stopService;
    
    	@Override
    	protected void onCreate(Bundle savedInstanceState) {
    		super.onCreate(savedInstanceState);
    		setContentView(R.layout.activity_main);
    		startService = (Button) findViewById(R.id.start_service);
    		stopService = (Button) findViewById(R.id.stop_service);
    		startService.setOnClickListener(this);
    		stopService.setOnClickListener(this);
    	}
    
    	@Override
    	public void onClick(View v) {
    		switch (v.getId()) {
    		case R.id.start_service:
    			Intent startIntent = new Intent(this, MyService.class);
    			startService(startIntent);
    			break;
    		case R.id.stop_service:
    			Intent stopIntent = new Intent(this, MyService.class);
    			stopService(stopIntent);
    			break;
    		default:
    			break;
    		}
    	}
    
    }
    可以看到,在Start Service按钮的点击事件里,我们构建出了一个Intent对象,并调用startService()方法来启动MyService。然后在Stop Serivce按钮的点击事件里,我们同样构建出了一个Intent对象,并调用stopService()方法来停止MyService。代码的逻辑非常简单,相信不需要我再多做解释了吧。


    另外需要注意,项目中的每一个Service都必须在AndroidManifest.xml中注册才行,所以还需要编辑AndroidManifest.xml文件,代码如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.servicetest"
        android:versionCode="1"
        android:versionName="1.0" >
    
        <uses-sdk
            android:minSdkVersion="14"
            android:targetSdkVersion="17" />
    
        <application
            android:allowBackup="true"
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name"
            android:theme="@style/AppTheme" >
            
    	……
    
            <service android:name="com.example.servicetest.MyService" >
            </service>
        </application>
    
    </manifest>

    这样的话,一个简单的带有Service功能的程序就写好了,现在我们将程序运行起来,并点击一下Start Service按钮,可以看到LogCat的打印日志如下:



    也就是说,当启动一个Service的时候,会调用该Service中的onCreate()和onStartCommand()方法。


    那么如果我再点击一次Start Service按钮呢?这个时候的打印日志如下:




    可以看到,这次只有onStartCommand()方法执行了,onCreate()方法并没有执行,为什么会这样呢?这是由于onCreate()方法只会在Service第一次被创建的时候调用,如果当前Service已经被创建过了,不管怎样调用startService()方法,onCreate()方法都不会再执行。因此你可以再多点击几次Start Service按钮试一次,每次都只会有onStartCommand()方法中的打印日志。


    我们还可以到手机的应用程序管理界面来检查一下MyService是不是正在运行,如下图所示:



    恩,MyService确实是正在运行的,即使它的内部并没有执行任何的逻辑。


    回到ServiceTest程序,然后点击一下Stop Service按钮就可以将MyService停止掉了。


    Service和Activity通信


    上面我们学习了Service的基本用法,启动Service之后,就可以在onCreate()或onStartCommand()方法里去执行一些具体的逻辑了。不过这样的话Service和Activity的关系并不大,只是Activity通知了Service一下:“你可以启动了。”然后Service就去忙自己的事情了。那么有没有什么办法能让它们俩的关联更多一些呢?比如说在Activity中可以指定让Service去执行什么任务。当然可以,只需要让Activity和Service建立关联就好了。


    观察MyService中的代码,你会发现一直有一个onBind()方法我们都没有使用到,这个方法其实就是用于和Activity建立关联的,修改MyService中的代码,如下所示:

    public class MyService extends Service {
    
    	public static final String TAG = "MyService";
    
    	private MyBinder mBinder = new MyBinder();
    
    	@Override
    	public void onCreate() {
    		super.onCreate();
    		Log.d(TAG, "onCreate() executed");
    	}
    
    	@Override
    	public int onStartCommand(Intent intent, int flags, int startId) {
    		Log.d(TAG, "onStartCommand() executed");
    		return super.onStartCommand(intent, flags, startId);
    	}
    
    	@Override
    	public void onDestroy() {
    		super.onDestroy();
    		Log.d(TAG, "onDestroy() executed");
    	}
    
    	@Override
    	public IBinder onBind(Intent intent) {
    		return mBinder;
    	}
    
    	class MyBinder extends Binder {
    
    		public void startDownload() {
    			Log.d("TAG", "startDownload() executed");
    			// 执行具体的下载任务
    		}
    
    	}
    
    }

    这里我们新增了一个MyBinder类继承自Binder类,然后在MyBinder中添加了一个startDownload()方法用于在后台执行下载任务,当然这里并不是真正地去下载某个东西,只是做个测试,所以startDownload()方法只是打印了一行日志。


    然后修改activity_main.xml中的代码,在布局文件中添加用于绑定Service和取消绑定Service的按钮:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
    
        <Button
            android:id="@+id/start_service"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Start Service" />
    
        <Button
            android:id="@+id/stop_service"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Stop Service" />
    
        <Button
            android:id="@+id/bind_service"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Bind Service" />
        
        <Button 
            android:id="@+id/unbind_service"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Unbind Service"
            />
        
    </LinearLayout>
    接下来再修改MainActivity中的代码,让MainActivity和MyService之间建立关联,代码如下所示:
    public class MainActivity extends Activity implements OnClickListener {
    
    	private Button startService;
    
    	private Button stopService;
    
    	private Button bindService;
    
    	private Button unbindService;
    
    	private MyService.MyBinder myBinder;
    
    	private ServiceConnection connection = new ServiceConnection() {
    
    		@Override
    		public void onServiceDisconnected(ComponentName name) {
    		}
    
    		@Override
    		public void onServiceConnected(ComponentName name, IBinder service) {
    			myBinder = (MyService.MyBinder) service;
    			myBinder.startDownload();
    		}
    	};
    
    	@Override
    	protected void onCreate(Bundle savedInstanceState) {
    		super.onCreate(savedInstanceState);
    		setContentView(R.layout.activity_main);
    		startService = (Button) findViewById(R.id.start_service);
    		stopService = (Button) findViewById(R.id.stop_service);
    		bindService = (Button) findViewById(R.id.bind_service);
    		unbindService = (Button) findViewById(R.id.unbind_service);
    		startService.setOnClickListener(this);
    		stopService.setOnClickListener(this);
    		bindService.setOnClickListener(this);
    		unbindService.setOnClickListener(this);
    	}
    
    	@Override
    	public void onClick(View v) {
    		switch (v.getId()) {
    		case R.id.start_service:
    			Intent startIntent = new Intent(this, MyService.class);
    			startService(startIntent);
    			break;
    		case R.id.stop_service:
    			Intent stopIntent = new Intent(this, MyService.class);
    			stopService(stopIntent);
    			break;
    		case R.id.bind_service:
    			Intent bindIntent = new Intent(this, MyService.class);
    			bindService(bindIntent, connection, BIND_AUTO_CREATE);
    			break;
    		case R.id.unbind_service:
    			unbindService(connection);
    			break;
    		default:
    			break;
    		}
    	}
    
    }

    可以看到,这里我们首先创建了一个ServiceConnection的匿名类,在里面重写了onServiceConnected()方法和onServiceDisconnected()方法,这两个方法分别会在Activity与Service建立关联和解除关联的时候调用。在onServiceConnected()方法中,我们又通过向下转型得到了MyBinder的实例,有了这个实例,Activity和Service之间的关系就变得非常紧密了。现在我们可以在Activity中根据具体的场景来调用MyBinder中的任何public方法,即实现了Activity指挥Service干什么Service就去干什么的功能。


    当然,现在Activity和Service其实还没关联起来了呢,这个功能是在Bind Service按钮的点击事件里完成的。可以看到,这里我们仍然是构建出了一个Intent对象,然后调用bindService()方法将Activity和Service进行绑定。bindService()方法接收三个参数,第一个参数就是刚刚构建出的Intent对象,第二个参数是前面创建出的ServiceConnection的实例,第三个参数是一个标志位,这里传入BIND_AUTO_CREATE表示在Activity和Service建立关联后自动创建Service,这会使得MyService中的onCreate()方法得到执行,但onStartCommand()方法不会执行。


    然后如何我们想解除Activity和Service之间的关联怎么办呢?调用一下unbindService()方法就可以了,这也是Unbind Service按钮的点击事件里实现的逻辑。


    现在让我们重新运行一下程序吧,在MainActivity中点击一下Bind Service按钮,LogCat里的打印日志如下图所示:




    另外需要注意,任何一个Service在整个应用程序范围内都是通用的,即MyService不仅可以和MainActivity建立关联,还可以和任何一个Activity建立关联,而且在建立关联时它们都可以获取到相同的MyBinder实例。


    如何销毁Service


    在Service的基本用法这一部分,我们介绍了销毁Service最简单的一种情况,点击Start Service按钮启动Service,再点击Stop Service按钮停止Service,这样MyService就被销毁了,可以看到打印日志如下所示:



    那么如果我们是点击的Bind Service按钮呢?由于在绑定Service的时候指定的标志位是BIND_AUTO_CREATE,说明点击Bind Service按钮的时候Service也会被创建,这时应该怎么销毁Service呢?其实也很简单,点击一下Unbind Service按钮,将Activity和Service的关联解除就可以了。


    先点击一下Bind Service按钮,再点击一下Unbind Service按钮,打印日志如下所示:



    以上这两种销毁的方式都很好理解。那么如果我们既点击了Start Service按钮,又点击了Bind Service按钮会怎么样呢?这个时候你会发现,不管你是单独点击Stop Service按钮还是Unbind Service按钮,Service都不会被销毁,必要将两个按钮都点击一下,Service才会被销毁。也就是说,点击Stop Service按钮只会让Service停止,点击Unbind Service按钮只会让Service和Activity解除关联,一个Service必须要在既没有和任何Activity关联又处理停止状态的时候才会被销毁。


    为了证实一下,我们在Stop Service和Unbind Service按钮的点击事件里面加入一行打印日志:

    public void onClick(View v) {
    	switch (v.getId()) {
    	case R.id.start_service:
    		Intent startIntent = new Intent(this, MyService.class);
    		startService(startIntent);
    		break;
    	case R.id.stop_service:
    		Log.d("MyService", "click Stop Service button");
    		Intent stopIntent = new Intent(this, MyService.class);
    		stopService(stopIntent);
    		break;
    	case R.id.bind_service:
    		Intent bindIntent = new Intent(this, MyService.class);
    		bindService(bindIntent, connection, BIND_AUTO_CREATE);
    		break;
    	case R.id.unbind_service:
    		Log.d("MyService", "click Unbind Service button");
    		unbindService(connection);
    		break;
    	default:
    		break;
    	}
    }

    然后重新运行程序,先点击一下Start Service按钮,再点击一下Bind Service按钮,这样就将Service启动起来,并和Activity建立了关联。然后点击Stop Service按钮后Service并不会销毁,再点击一下Unbind Service按钮,Service就会销毁了,打印日志如下所示:



    我们应该始终记得在Service的onDestroy()方法里去清理掉那些不再使用的资源,防止在Service被销毁后还会有一些不再使用的对象仍占用着内存。


    Service和Thread的关系


    不少Android初学者都可能会有这样的疑惑,Service和Thread到底有什么关系呢?什么时候应该用Service,什么时候又应该用Thread?答案可能会有点让你吃惊,因为Service和Thread之间没有任何关系!


    之所以有不少人会把它们联系起来,主要就是因为Service的后台概念。Thread我们大家都知道,是用于开启一个子线程,在这里去执行一些耗时操作就不会阻塞主线程的运行。而Service我们最初理解的时候,总会觉得它是用来处理一些后台任务的,一些比较耗时的操作也可以放在这里运行,这就会让人产生混淆了。但是,如果我告诉你Service其实是运行在主线程里的,你还会觉得它和Thread有什么关系吗?让我们看一下这个残酷的事实吧。


    在MainActivity的onCreate()方法里加入一行打印当前线程id的语句:

    Log.d("MyService", "MainActivity thread id is " + Thread.currentThread().getId());
    然后在MyService的onCreate()方法里也加入一行打印当前线程id的语句:
    Log.d("MyService", "MyService thread id is " + Thread.currentThread().getId());

    现在重新运行一下程序,并点击Start Service按钮,会看到如下打印日志:



    可以看到,它们的线程id完全是一样的,由此证实了Service确实是运行在主线程里的,也就是说如果你在Service里编写了非常耗时的代码,程序必定会出现ANR的。


    你可能会惊呼,这不是坑爹么!?那我要Service又有何用呢?其实大家不要把后台和子线程联系在一起就行了,这是两个完全不同的概念。Android的后台就是指,它的运行是完全不依赖UI的。即使Activity被销毁,或者程序被关闭,只要进程还在,Service就可以继续运行。比如说一些应用程序,始终需要与服务器之间始终保持着心跳连接,就可以使用Service来实现。你可能又会问,前面不是刚刚验证过Service是运行在主线程里的么?在这里一直执行着心跳连接,难道就不会阻塞主线程的运行吗?当然会,但是我们可以在Service中再创建一个子线程,然后在这里去处理耗时逻辑就没问题了。


    额,既然在Service里也要创建一个子线程,那为什么不直接在Activity里创建呢?这是因为Activity很难对Thread进行控制,当Activity被销毁之后,就没有任何其它的办法可以再重新获取到之前创建的子线程的实例。而且在一个Activity中创建的子线程,另一个Activity无法对其进行操作。但是Service就不同了,所有的Activity都可以与Service进行关联,然后可以很方便地操作其中的方法,即使Activity被销毁了,之后只要重新与Service建立关联,就又能够获取到原有的Service中Binder的实例。因此,使用Service来处理后台任务,Activity就可以放心地finish,完全不需要担心无法对后台任务进行控制的情况。


    一个比较标准的Service就可以写成:

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
    	new Thread(new Runnable() {
    		@Override
    		public void run() {
    			// 开始执行后台任务
    		}
    	}).start();
    	return super.onStartCommand(intent, flags, startId);
    }
    
    class MyBinder extends Binder {
    
    	public void startDownload() {
    		new Thread(new Runnable() {
    			@Override
    			public void run() {
    				// 执行具体的下载任务
    			}
    		}).start();
    	}
    
    }

    创建前台Service


    Service几乎都是在后台运行的,一直以来它都是默默地做着辛苦的工作。但是Service的系统优先级还是比较低的,当系统出现内存不足情况时,就有可能会回收掉正在后台运行的Service。如果你希望Service可以一直保持运行状态,而不会由于系统内存不足的原因导致被回收,就可以考虑使用前台Service。前台Service和普通Service最大的区别就在于,它会一直有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。当然有时候你也可能不仅仅是为了防止Service被回收才使用前台Service,有些项目由于特殊的需求会要求必须使用前台Service,比如说墨迹天气,它的Service在后台更新天气数据的同时,还会在系统状态栏一直显示当前天气的信息,如下图所示:



    那么我们就来看一下如何才能创建一个前台Service吧,其实并不复杂,修改MyService中的代码,如下所示:

    public class MyService extends Service {
    
    	public static final String TAG = "MyService";
    
    	private MyBinder mBinder = new MyBinder();
    
    	@Override
    	public void onCreate() {
    		super.onCreate();
    		Notification notification = new Notification(R.drawable.ic_launcher,
    				"有通知到来", System.currentTimeMillis());
    		Intent notificationIntent = new Intent(this, MainActivity.class);
    		PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
    				notificationIntent, 0);
    		notification.setLatestEventInfo(this, "这是通知的标题", "这是通知的内容",
    				pendingIntent);
    		startForeground(1, notification);
    		Log.d(TAG, "onCreate() executed");
    	}
    
    	.........
    
    }

    这里只是修改了MyService中onCreate()方法的代码。可以看到,我们首先创建了一个Notification对象,然后调用了它的setLatestEventInfo()方法来为通知初始化布局和数据,并在这里设置了点击通知后就打开MainActivity。然后调用startForeground()方法就可以让MyService变成一个前台Service,并会将通知的图片显示出来。


    现在重新运行一下程序,并点击Start Service或Bind Service按钮,MyService就会以前台Service的模式启动了,并且在系统状态栏会弹出一个通栏图标,下拉状态栏后可以看到通知的详细内容,如下图所示。



    好了,由于篇幅的原因,本篇文章就先写到这里。目前我们已经把关于Service的很多重要知识点都梳理完了,下一篇文章会承接这篇文章,介绍Android Service中剩下的一个非常重要且复杂的知识点 —— 远程Service的使用,感兴趣的朋友请继续阅读 Android Service完全解析,关于服务你所需知道的一切(下) 。


    关注我的技术公众号,每天都有优质技术文章推送。关注我的娱乐公众号,工作、学习累了的时候放松一下自己。

    微信扫一扫下方二维码即可关注:

            

    展开全文
  • 这篇文章中讲解的很清楚了,这里就不再赘述。 好了,现在你已经成功入门Glide 4了,那么接下来就让我们学习一下Glide 4的更多用法吧。 占位图 观察刚才加载网络图片的效果,你会发现,点击了Load Image...

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/78582548

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每天都有文章更新。

    本篇将是我们这个Glide系列的最后一篇文章。

    其实在写这个系列第一篇文章的时候,Glide就推出4.0.0的RC版了。那个时候因为我一直研究的都是Glide 3.7.0版本,再加上RC版本还不太稳定,因此整个系列也都是基于3.7.0版本来写的。

    而现在,Glide的最新版本已经出到了4.4.0,可以说Glide 4已经是相当成熟和稳定了。而且也不断有朋友一直在留言,想让我讲一讲Glide 4的用法,因为Glide 4相对于Glide 3改动貌似还是挺大的,学完了Glide 3再去使用Glide 4,发现根本就无法使用。

    OK,那么今天就让我们用《带你全面了解Glide 4的用法》这样一篇文章,给这个Glide系列画上一个圆满的句号。

    Glide 4概述

    刚才有说到,有些朋友觉得Glide 4相对于Glide 3改动非常大,其实不然。之所以大家会有这种错觉,是因为你将Glide 3的用法直接搬到Glide 4中去使用,结果IDE全面报错,然后大家可能就觉得Glide 4的用法完全变掉了。

    其实Glide 4相对于Glide 3的变动并不大,只是你还没有了解它的变动规则而已。一旦你掌握了Glide 4的变动规则之后,你会发现大多数Glide 3的用法放到Glide 4上都还是通用的。

    我对Glide 4进行了一个大概的研究之后,发现Glide 4并不能算是有什么突破性的升级,而更多是一些API工整方面的优化。相比于Glide 3的API,Glide 4进行了更加科学合理地调整,使得易读性、易写性、可扩展性等方面都有了不错的提升。但如果你已经对Glide 3非常熟悉的话,并不是就必须要切换到Glide 4上来,因为Glide 4上能实现的功能Glide 3也都能实现,而且Glide 4在性能方面也并没有什么提升。

    但是对于新接触Glide的朋友而言,那就没必要再去学习Glide 3了,直接上手Glide 4就是最佳的选择了。

    好了,对Glide 4进行一个基本的概述之后,接下来我们就要正式开始学习它的用法了。刚才我已经说了,Glide 4的用法相对于Glide 3其实改动并不大。在前面的七篇文章中,我们已经学习了Glide 3的基本用法、缓存机制、回调与监听、图片变换、自定义模块等用法,那么今天这篇文章的目标就很简单了,就是要掌握如何在Glide 4上实现之前所学习过的所有功能,那么我们现在就开始吧。

    开始

    要想使用Glide,首先需要将这个库引入到我们的项目当中。新建一个Glide4Test项目,然后在app/build.gradle文件当中添加如下依赖:

    dependencies {
        implementation 'com.github.bumptech.glide:glide:4.4.0'
        annotationProcessor 'com.github.bumptech.glide:compiler:4.4.0'
    }

    注意,相比于Glide 3,这里要多添加一个compiler的库,这个库是用于生成Generated API的,待会我们会讲到它。

    另外,Glide中需要用到网络功能,因此你还得在AndroidManifest.xml中声明一下网络权限才行:

    <uses-permission android:name="android.permission.INTERNET" />

    就是这么简单,然后我们就可以自由地使用Glide中的任意功能了。

    加载图片

    现在我们就来尝试一下如何使用Glide来加载图片吧。比如这是一张图片的地址:

    http://guolin.tech/book.png

    然后我们想要在程序当中去加载这张图片。

    那么首先打开项目的布局文件,在布局当中加入一个Button和一个ImageView,如下所示:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Load Image"
            android:onClick="loadImage"
            />
    
        <ImageView
            android:id="@+id/image_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    </LinearLayout>

    为了让用户点击Button的时候能够将刚才的图片显示在ImageView上,我们需要修改MainActivity中的代码,如下所示:

    public class MainActivity extends AppCompatActivity {
    
        ImageView imageView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            imageView = (ImageView) findViewById(R.id.image_view);
        }
    
        public void loadImage(View view) {
            String url = "http://guolin.tech/book.png";
            Glide.with(this).load(url).into(imageView);
        }
    
    }

    没错,就是这么简单。现在我们来运行一下程序,效果如下图所示:

    可以看到,一张网络上的图片已经被成功下载,并且展示到ImageView上了。

    你会发现,到目前为止,Glide 4的用法和Glide 3是完全一样的,实际上核心的代码就只有这一行而已:

    Glide.with(this).load(url).into(imageView);

    仍然还是传统的三步走:先with(),再load(),最后into()。对这行代码的解读,我在 Android图片加载框架最全解析(一),Glide的基本用法 这篇文章中讲解的很清楚了,这里就不再赘述。

    好了,现在你已经成功入门Glide 4了,那么接下来就让我们学习一下Glide 4的更多用法吧。

    占位图

    观察刚才加载网络图片的效果,你会发现,点击了Load Image按钮之后,要稍微等一会图片才会显示出来。这其实很容易理解,因为从网络上下载图片本来就是需要时间的。那么我们有没有办法再优化一下用户体验呢?当然可以,Glide提供了各种各样非常丰富的API支持,其中就包括了占位图功能。

    顾名思义,占位图就是指在图片的加载过程中,我们先显示一张临时的图片,等图片加载出来了再替换成要加载的图片。

    下面我们就来学习一下Glide占位图功能的使用方法,首先我事先准备好了一张loading.jpg图片,用来作为占位图显示。然后修改Glide加载部分的代码,如下所示:

    RequestOptions options = new RequestOptions()
            .placeholder(R.drawable.loading);
    Glide.with(this)
         .load(url)
         .apply(options)
         .into(imageView);

    没错,就是这么简单。这里我们先创建了一个RequestOptions对象,然后调用它的placeholder()方法来指定占位图,再将占位图片的资源id传入到这个方法中。最后,在Glide的三步走之间加入一个apply()方法,来应用我们刚才创建的RequestOptions对象。

    不过如果你现在重新运行一下代码并点击Load Image,很可能是根本看不到占位图效果的。因为Glide有非常强大的缓存机制,我们刚才加载图片的时候Glide自动就已经将它缓存下来了,下次加载的时候将会直接从缓存中读取,不会再去网络下载了,因而加载的速度非常快,所以占位图可能根本来不及显示。

    因此这里我们还需要稍微做一点修改,来让占位图能有机会显示出来,修改代码如下所示:

    RequestOptions options = new RequestOptions()
            .placeholder(R.drawable.loading)
            .diskCacheStrategy(DiskCacheStrategy.NONE);
    Glide.with(this)
         .load(url)
         .apply(options)
         .into(imageView);

    可以看到,这里在RequestOptions对象中又串接了一个diskCacheStrategy()方法,并传入DiskCacheStrategy.NONE参数,这样就可以禁用掉Glide的缓存功能。

    关于Glide缓存方面的内容我们待会儿会进行更详细的讲解,这里只是为了测试占位图功能而加的一个额外配置,暂时你只需要知道禁用缓存必须这么写就可以了。

    现在重新运行一下代码,效果如下图所示:

    可以看到,当点击Load Image按钮之后会立即显示一张占位图,然后等真正的图片加载完成之后会将占位图替换掉。

    除了这种加载占位图之外,还有一种异常占位图。异常占位图就是指,如果因为某些异常情况导致图片加载失败,比如说手机网络信号不好,这个时候就显示这张异常占位图。

    异常占位图的用法相信你已经可以猜到了,首先准备一张error.jpg图片,然后修改Glide加载部分的代码,如下所示:

    RequestOptions options = new RequestOptions()
            .placeholder(R.drawable.ic_launcher_background)
            .error(R.drawable.error)
            .diskCacheStrategy(DiskCacheStrategy.NONE);
    Glide.with(this)
         .load(url)
         .apply(options)
         .into(imageView);

    很简单,这里又串接了一个error()方法就可以指定异常占位图了。

    其实看到这里,如果你熟悉Glide 3的话,相信你已经掌握Glide 4的变化规律了。在Glide 3当中,像placeholder()、error()、diskCacheStrategy()等等一系列的API,都是直接串联在Glide三步走方法中使用的。

    而Glide 4中引入了一个RequestOptions对象,将这一系列的API都移动到了RequestOptions当中。这样做的好处是可以使我们摆脱冗长的Glide加载语句,而且还能进行自己的API封装,因为RequestOptions是可以作为参数传入到方法中的。

    比如你就可以写出这样的Glide加载工具类:

    public class GlideUtil {
    
        public static void load(Context context,
                                String url,
                                ImageView imageView,
                                RequestOptions options) {
            Glide.with(context)
                 .load(url)
                 .apply(options)
                 .into(imageView);
        }
    
    }

    指定图片大小

    实际上,使用Glide在大多数情况下我们都是不需要指定图片大小的,因为Glide会自动根据ImageView的大小来决定图片的大小,以此保证图片不会占用过多的内存从而引发OOM。

    不过,如果你真的有这样的需求,必须给图片指定一个固定的大小,Glide仍然是支持这个功能的。修改Glide加载部分的代码,如下所示:

    RequestOptions options = new RequestOptions()
            .override(200, 100);
    Glide.with(this)
         .load(url)
         .apply(options)
         .into(imageView);

    仍然非常简单,这里使用override()方法指定了一个图片的尺寸。也就是说,Glide现在只会将图片加载成200*100像素的尺寸,而不会管你的ImageView的大小是多少了。

    如果你想加载一张图片的原始尺寸的话,可以使用Target.SIZE_ORIGINAL关键字,如下所示:

    RequestOptions options = new RequestOptions()
            .override(Target.SIZE_ORIGINAL);
    Glide.with(this)
         .load(url)
         .apply(options)
         .into(imageView);

    这样的话,Glide就不会再去自动压缩图片,而是会去加载图片的原始尺寸。当然,这种写法也会面临着更高的OOM风险。

    缓存机制

    Glide的缓存设计可以说是非常先进的,考虑的场景也很周全。在缓存这一功能上,Glide又将它分成了两个模块,一个是内存缓存,一个是硬盘缓存。

    这两个缓存模块的作用各不相同,内存缓存的主要作用是防止应用重复将图片数据读取到内存当中,而硬盘缓存的主要作用是防止应用重复从网络或其他地方重复下载和读取数据。

    内存缓存和硬盘缓存的相互结合才构成了Glide极佳的图片缓存效果,那么接下来我们就来分别学习一下这两种缓存的使用方法。

    首先来看内存缓存。

    你要知道,默认情况下,Glide自动就是开启内存缓存的。也就是说,当我们使用Glide加载了一张图片之后,这张图片就会被缓存到内存当中,只要在它还没从内存中被清除之前,下次使用Glide再加载这张图片都会直接从内存当中读取,而不用重新从网络或硬盘上读取了,这样无疑就可以大幅度提升图片的加载效率。比方说你在一个RecyclerView当中反复上下滑动,RecyclerView中只要是Glide加载过的图片都可以直接从内存当中迅速读取并展示出来,从而大大提升了用户体验。

    而Glide最为人性化的是,你甚至不需要编写任何额外的代码就能自动享受到这个极为便利的内存缓存功能,因为Glide默认就已经将它开启了。

    那么既然已经默认开启了这个功能,还有什么可讲的用法呢?只有一点,如果你有什么特殊的原因需要禁用内存缓存功能,Glide对此提供了接口:

    RequestOptions options = new RequestOptions()
            .skipMemoryCache(true);
    Glide.with(this)
         .load(url)
         .apply(options)
         .into(imageView);

    可以看到,只需要调用skipMemoryCache()方法并传入true,就表示禁用掉Glide的内存缓存功能。

    接下来我们开始学习硬盘缓存方面的内容。

    其实在刚刚学习占位图功能的时候,我们就使用过硬盘缓存的功能了。当时为了禁止Glide对图片进行硬盘缓存而使用了如下代码:

    RequestOptions options = new RequestOptions()
            .diskCacheStrategy(DiskCacheStrategy.NONE);
    Glide.with(this)
         .load(url)
         .apply(options)
         .into(imageView);

    调用diskCacheStrategy()方法并传入DiskCacheStrategy.NONE,就可以禁用掉Glide的硬盘缓存功能了。

    这个diskCacheStrategy()方法基本上就是Glide硬盘缓存功能的一切,它可以接收五种参数:

    • DiskCacheStrategy.NONE: 表示不缓存任何内容。
    • DiskCacheStrategy.DATA: 表示只缓存原始图片。
    • DiskCacheStrategy.RESOURCE: 表示只缓存转换过后的图片。
    • DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。
    • DiskCacheStrategy.AUTOMATIC: 表示让Glide根据图片资源智能地选择使用哪一种缓存策略(默认选项)。

    其中,DiskCacheStrategy.DATA对应Glide 3中的DiskCacheStrategy.SOURCE,DiskCacheStrategy.RESOURCE对应Glide 3中的DiskCacheStrategy.RESULT。而DiskCacheStrategy.AUTOMATIC是Glide 4中新增的一种缓存策略,并且在不指定diskCacheStrategy的情况下默认使用就是的这种缓存策略。

    上面五种参数的解释本身并没有什么难理解的地方,但是关于转换过后的图片这个概念大家可能需要了解一下。就是当我们使用Glide去加载一张图片的时候,Glide默认并不会将原始图片展示出来,而是会对图片进行压缩和转换(我们会在稍后学习这方面的内容)。总之就是经过种种一系列操作之后得到的图片,就叫转换过后的图片。

    好的,关于Glide 4硬盘缓存的内容就讲到这里。想要了解更多Glide缓存方面的知识,可以参考 Android图片加载框架最全解析(三),深入探究Glide的缓存机制 这篇文章。

    指定加载格式

    我们都知道,Glide其中一个非常亮眼的功能就是可以加载GIF图片,而同样作为非常出色的图片加载框架的Picasso是不支持这个功能的。

    而且使用Glide加载GIF图并不需要编写什么额外的代码,Glide内部会自动判断图片格式。比如我们将加载图片的URL地址改成一张GIF图,如下所示:

    Glide.with(this)
         .load("http://guolin.tech/test.gif")
         .into(imageView);

    现在重新运行一下代码,效果如下图所示:

    也就是说,不管我们传入的是一张普通图片,还是一张GIF图片,Glide都会自动进行判断,并且可以正确地把它解析并展示出来。

    但是如果我想指定加载格式该怎么办呢?就比如说,我希望加载的这张图必须是一张静态图片,我不需要Glide自动帮我判断它到底是静图还是GIF图。

    想实现这个功能仍然非常简单,我们只需要再串接一个新的方法就可以了,如下所示:

    Glide.with(this)
         .asBitmap()
         .load("http://guolin.tech/test.gif")
         .into(imageView);

    可以看到,这里在with()方法的后面加入了一个asBitmap()方法,这个方法的意思就是说这里只允许加载静态图片,不需要Glide去帮我们自动进行图片格式的判断了。如果你传入的还是一张GIF图的话,Glide会展示这张GIF图的第一帧,而不会去播放它。

    熟悉Glide 3的朋友对asBitmap()方法肯定不会陌生对吧?但是千万不要觉得这里就没有陷阱了,在Glide 3中的语法是先load()再asBitmap()的,而在Glide 4中是先asBitmap()再load()的。乍一看可能分辨不出来有什么区别,但如果你写错了顺序就肯定会报错了。

    那么类似地,既然我们能强制指定加载静态图片,就也能强制指定加载动态图片,对应的方法是asGif()。而Glide 4中又新增了asFile()方法和asDrawable()方法,分别用于强制指定文件格式的加载和Drawable格式的加载,用法都比较简单,就不再进行演示了。

    回调与监听

    回调与监听这部分的内容稍微有点多,我们分成四部分来学习一下。

    1. into()方法

    我们都知道Glide的into()方法中是可以传入ImageView的。那么into()方法还可以传入别的参数吗?我们可以让Glide加载出来的图片不显示到ImageView上吗?答案是肯定的,这就需要用到自定义Target功能。

    Glide中的Target功能多样且复杂,下面我就先简单演示一种SimpleTarget的用法吧,代码如下所示:

    SimpleTarget<Drawable> simpleTarget = new SimpleTarget<Drawable>() {
        @Override
        public void onResourceReady(Drawable resource, Transition<? super Drawable> transition) {
            imageView.setImageDrawable(resource);
        }
    };
    
    public void loadImage(View view) {
        Glide.with(this)
             .load("http://guolin.tech/book.png")
             .into(simpleTarget);
    }

    这里我们创建了一个SimpleTarget的实例,并且指定它的泛型是Drawable,然后重写了onResourceReady()方法。在onResourceReady()方法中,我们就可以获取到Glide加载出来的图片对象了,也就是方法参数中传过来的Drawable对象。有了这个对象之后你可以使用它进行任意的逻辑操作,这里我只是简单地把它显示到了ImageView上。

    SimpleTarget的实现创建好了,那么只需要在加载图片的时候将它传入到into()方法中就可以了。

    这里限于篇幅原因我只演示了自定义Target的简单用法,想学习更多相关的内容可以去阅读 Android图片加载框架最全解析(四),玩转Glide的回调与监听

    2. preload()方法

    Glide加载图片虽说非常智能,它会自动判断该图片是否已经有缓存了,如果有的话就直接从缓存中读取,没有的话再从网络去下载。但是如果我希望提前对图片进行一个预加载,等真正需要加载图片的时候就直接从缓存中读取,不想再等待慢长的网络加载时间了,这该怎么办呢?

    不用担心,Glide专门给我们提供了预加载的接口,也就是preload()方法,我们只需要直接使用就可以了。

    preload()方法有两个方法重载,一个不带参数,表示将会加载图片的原始尺寸,另一个可以通过参数指定加载图片的宽和高。

    preload()方法的用法也非常简单,直接使用它来替换into()方法即可,如下所示:

    Glide.with(this)
         .load("http://guolin.tech/book.png")
         .preload();

    调用了预加载之后,我们以后想再去加载这张图片就会非常快了,因为Glide会直接从缓存当中去读取图片并显示出来,代码如下所示:

    Glide.with(this)
         .load("http://guolin.tech/book.png")
         .into(imageView);

    3. submit()方法

    一直以来,我们使用Glide都是为了将图片显示到界面上。虽然我们知道Glide会在图片的加载过程中对图片进行缓存,但是缓存文件到底是存在哪里的,以及如何去直接访问这些缓存文件?我们都还不知道。

    其实Glide将图片加载接口设计成这样也是希望我们使用起来更加的方便,不用过多去考虑底层的实现细节。但如果我现在就是想要去访问图片的缓存文件该怎么办呢?这就需要用到submit()方法了。

    submit()方法其实就是对应的Glide 3中的downloadOnly()方法,和preload()方法类似,submit()方法也是可以替换into()方法的,不过submit()方法的用法明显要比preload()方法复杂不少。这个方法只会下载图片,而不会对图片进行加载。当图片下载完成之后,我们可以得到图片的存储路径,以便后续进行操作。

    那么首先我们还是先来看下基本用法。submit()方法有两个方法重载:

    • submit()
    • submit(int width, int height)

    其中submit()方法是用于下载原始尺寸的图片,而submit(int width, int height)则可以指定下载图片的尺寸。

    这里就以submit()方法来举例。当调用了submit()方法后会立即返回一个FutureTarget对象,然后Glide会在后台开始下载图片文件。接下来我们调用FutureTarget的get()方法就可以去获取下载好的图片文件了,如果此时图片还没有下载完,那么get()方法就会阻塞住,一直等到图片下载完成才会有值返回。

    下面我们通过一个例子来演示一下吧,代码如下所示:

    public void downloadImage() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    String url = "http://www.guolin.tech/book.png";
                    final Context context = getApplicationContext();
                    FutureTarget<File> target = Glide.with(context)
                            .asFile()
                            .load(url)
                            .submit();
                    final File imageFile = target.get();
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(context, imageFile.getPath(), Toast.LENGTH_LONG).show();
                        }
                    });
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    这段代码稍微有一点点长,我带着大家解读一下。首先,submit()方法必须要用在子线程当中,因为刚才说了FutureTarget的get()方法是会阻塞线程的,因此这里的第一步就是new了一个Thread。在子线程当中,我们先获取了一个Application Context,这个时候不能再用Activity作为Context了,因为会有Activity销毁了但子线程还没执行完这种可能出现。

    接下来就是Glide的基本用法,只不过将into()方法替换成了submit()方法,并且还使用了一个asFile()方法来指定加载格式。submit()方法会返回一个FutureTarget对象,这个时候其实Glide已经开始在后台下载图片了,我们随时都可以调用FutureTarget的get()方法来获取下载的图片文件,只不过如果图片还没下载好线程会暂时阻塞住,等下载完成了才会把图片的File对象返回。

    最后,我们使用runOnUiThread()切回到主线程,然后使用Toast将下载好的图片文件路径显示出来。

    现在重新运行一下代码,效果如下图所示。

    这样我们就能清晰地看出来图片完整的缓存路径是什么了。

    4. listener()方法

    其实listener()方法的作用非常普遍,它可以用来监听Glide加载图片的状态。举个例子,比如说我们刚才使用了preload()方法来对图片进行预加载,但是我怎样确定预加载有没有完成呢?还有如果Glide加载图片失败了,我该怎样调试错误的原因呢?答案都在listener()方法当中。

    下面来看下listener()方法的基本用法吧,不同于刚才几个方法都是要替换into()方法的,listener()是结合into()方法一起使用的,当然也可以结合preload()方法一起使用。最基本的用法如下所示:

    Glide.with(this)
         .load("http://www.guolin.tech/book.png")
         .listener(new RequestListener<Drawable>() {
             @Override
             public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
                 return false;
             }
    
             @Override
             public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
                 return false;
             }
         })
         .into(imageView);

    这里我们在into()方法之前串接了一个listener()方法,然后实现了一个RequestListener的实例。其中RequestListener需要实现两个方法,一个onResourceReady()方法,一个onLoadFailed()方法。从方法名上就可以看出来了,当图片加载完成的时候就会回调onResourceReady()方法,而当图片加载失败的时候就会回调onLoadFailed()方法,onLoadFailed()方法中会将失败的GlideException参数传进来,这样我们就可以定位具体失败的原因了。

    没错,listener()方法就是这么简单。不过还有一点需要处理,onResourceReady()方法和onLoadFailed()方法都有一个布尔值的返回值,返回false就表示这个事件没有被处理,还会继续向下传递,返回true就表示这个事件已经被处理掉了,从而不会再继续向下传递。举个简单点的例子,如果我们在RequestListener的onResourceReady()方法中返回了true,那么就不会再回调Target的onResourceReady()方法了。

    关于回调与监听的内容就讲这么多吧,如果想要学习更多深入的内容以及源码解析,还是请参考这篇文章 Android图片加载框架最全解析(四),玩转Glide的回调与监听

    图片变换

    图片变换的意思就是说,Glide从加载了原始图片到最终展示给用户之前,又进行了一些变换处理,从而能够实现一些更加丰富的图片效果,如图片圆角化、圆形化、模糊化等等。

    添加图片变换的用法非常简单,我们只需要在RequestOptions中串接transforms()方法,并将想要执行的图片变换操作作为参数传入transforms()方法即可,如下所示:

    RequestOptions options = new RequestOptions()
            .transforms(...);
    Glide.with(this)
         .load(url)
         .apply(options)
         .into(imageView);

    至于具体要进行什么样的图片变换操作,这个通常都是需要我们自己来写的。不过Glide已经内置了几种图片变换操作,我们可以直接拿来使用,比如CenterCrop、FitCenter、CircleCrop等。

    但所有的内置图片变换操作其实都不需要使用transform()方法,Glide为了方便我们使用直接提供了现成的API:

    RequestOptions options = new RequestOptions()
            .centerCrop();
    
    RequestOptions options = new RequestOptions()
            .fitCenter();
    
    RequestOptions options = new RequestOptions()
            .circleCrop();

    当然,这些内置的图片变换API其实也只是对transform()方法进行了一层封装而已,它们背后的源码仍然还是借助transform()方法来实现的。

    这里我们就选择其中一种内置的图片变换操作来演示一下吧,circleCrop()方法是用来对图片进行圆形化裁剪的,我们动手试一下,代码如下所示:

    String url = "http://guolin.tech/book.png";
    RequestOptions options = new RequestOptions()
            .circleCrop();
    Glide.with(this)
         .load(url)
         .apply(options)
         .into(imageView);

    重新运行一下程序并点击加载图片按钮,效果如下图所示。

    可以看到,现在展示的图片是对原图进行圆形化裁剪后得到的图片。

    当然,除了使用内置的图片变换操作之外,我们完全可以自定义自己的图片变换操作。理论上,在对图片进行变换这个步骤中我们可以进行任何的操作,你想对图片怎么样都可以。包括圆角化、圆形化、黑白化、模糊化等等,甚至你将原图片完全替换成另外一张图都是可以的。

    不过由于这部分内容相对于Glide 3没有任何的变化,因此就不再重复进行讲解了。想学习自定义图片变换操作的朋友们可以参考这篇文章 Android图片加载框架最全解析(五),Glide强大的图片变换功能

    关于图片变换,最后我们再来看一个非常优秀的开源库,glide-transformations。它实现了很多通用的图片变换效果,如裁剪变换、颜色变换、模糊变换等等,使得我们可以非常轻松地进行各种各样的图片变换。

    glide-transformations的项目主页地址是 https://github.com/wasabeef/glide-transformations

    下面我们就来体验一下这个库的强大功能吧。首先需要将这个库引入到我们的项目当中,在app/build.gradle文件当中添加如下依赖:

    dependencies {
        implementation 'jp.wasabeef:glide-transformations:3.0.1'
    }

    我们可以对图片进行单个变换处理,也可以将多种图片变换叠加在一起使用。比如我想同时对图片进行模糊化和黑白化处理,就可以这么写:

    String url = "http://guolin.tech/book.png";
    RequestOptions options = new RequestOptions()
            .transforms(new BlurTransformation(), new GrayscaleTransformation());
    Glide.with(this)
         .load(url)
         .apply(options)
         .into(imageView);

    可以看到,同时执行多种图片变换的时候,只需要将它们都传入到transforms()方法中即可。现在重新运行一下程序,效果如下图所示。

    当然,这只是glide-transformations库的一小部分功能而已,更多的图片变换效果你可以到它的GitHub项目主页去学习。

    自定义模块

    自定义模块属于Glide中的高级功能,同时也是难度比较高的一部分内容。

    这里我不可能在这一篇文章中将自定义模块的内容全讲一遍,限于篇幅的限制我只能讲一讲Glide 4中变化的这部分内容。关于Glide自定义模块的全部内容,请大家去参考 Android图片加载框架最全解析(六),探究Glide的自定义模块功能 这篇文章。

    自定义模块功能可以将更改Glide配置,替换Glide组件等操作独立出来,使得我们能轻松地对Glide的各种配置进行自定义,并且又和Glide的图片加载逻辑没有任何交集,这也是一种低耦合编程方式的体现。下面我们就来学习一下自定义模块要如何实现。

    首先定义一个我们自己的模块类,并让它继承自AppGlideModule,如下所示:

    @GlideModule
    public class MyAppGlideModule extends AppGlideModule {
    
        @Override
        public void applyOptions(Context context, GlideBuilder builder) {
    
        }
    
        @Override
        public void registerComponents(Context context, Glide glide, Registry registry) {
    
        }
    
    }

    可以看到,在MyAppGlideModule类当中,我们重写了applyOptions()和registerComponents()方法,这两个方法分别就是用来更改Glide配置以及替换Glide组件的。

    注意在MyAppGlideModule类在上面,我们加入了一个@GlideModule的注解,这是Gilde 4和Glide 3最大的一个不同之处。在Glide 3中,我们定义了自定义模块之后,还必须在AndroidManifest.xml文件中去注册它才能生效,而在Glide 4中是不需要的,因为@GlideModule这个注解已经能够让Glide识别到这个自定义模块了。

    这样的话,我们就将Glide自定义模块的功能完成了。后面只需要在applyOptions()和registerComponents()这两个方法中加入具体的逻辑,就能实现更改Glide配置或者替换Glide组件的功能了。详情还是请参考 Android图片加载框架最全解析(六),探究Glide的自定义模块功能 这篇文章,这里就不再展开讨论了。

    使用Generated API

    Generated API是Glide 4中全新引入的一个功能,它的工作原理是使用注解处理器 (Annotation Processor) 来生成出一个API,在Application模块中可使用该流式API一次性调用到RequestBuilder,RequestOptions和集成库中所有的选项。

    这么解释有点拗口,简单点说,就是Glide 4仍然给我们提供了一套和Glide 3一模一样的流式API接口。毕竟有些人还是觉得Glide 3的API更好用一些,比如说我。

    Generated API对于熟悉Glide 3的朋友来说那是再简单不过了,基本上就是和Glide 3一模一样的用法,只不过需要把Glide关键字替换成GlideApp关键字,如下所示:

    GlideApp.with(this)
            .load(url)
            .placeholder(R.drawable.loading)
            .error(R.drawable.error)
            .skipMemoryCache(true)
            .diskCacheStrategy(DiskCacheStrategy.NONE)
            .override(Target.SIZE_ORIGINAL)
            .circleCrop()
            .into(imageView);

    不过,有可能你的IDE中会提示找不到GlideApp这个类。这个类是通过编译时注解自动生成的,首先确保你的代码中有一个自定义的模块,并且给它加上了@GlideModule注解,也就是我们在上一节所讲的内容。然后在Android Studio中点击菜单栏Build -> Rebuild Project,GlideApp这个类就会自动生成了。

    当然,Generated API所能做到的并不只是这些而已,它还可以对现有的API进行扩展,定制出任何属于你自己的API。

    下面我来具体举个例子,比如说我们要求项目中所有图片的缓存策略全部都要缓存原始图片,那么每次在使用Glide加载图片的时候,都去指定diskCacheStrategy(DiskCacheStrategy.DATA)这么长长的一串代码,确实是让人比较心烦。这种情况我们就可以去定制一个自己的API了。

    定制自己的API需要借助@GlideExtension和@GlideOption这两个注解。创建一个我们自定义的扩展类,代码如下所示:

    @GlideExtension
    public class MyGlideExtension {
    
        private MyGlideExtension() {
    
        }
    
        @GlideOption
        public static void cacheSource(RequestOptions options) {
            options.diskCacheStrategy(DiskCacheStrategy.DATA);
        }
    
    }

    这里我们定义了一个MyGlideExtension类,并且给加上了一个@GlideExtension注解,然后要将这个类的构造函数声明成private,这都是必须要求的写法。

    接下来就可以开始自定义API了,这里我们定义了一个cacheSource()方法,表示只缓存原始图片,并给这个方法加上了@GlideOption注解。注意自定义API的方法都必须是静态方法,而且第一个参数必须是RequestOptions,后面你可以加入任意多个你想自定义的参数。

    在cacheSource()方法中,我们仍然还是调用的diskCacheStrategy(DiskCacheStrategy.DATA)方法,所以说cacheSource()就是一层简化API的封装而已。

    然后在Android Studio中点击菜单栏Build -> Rebuild Project,神奇的事情就会发生了,你会发现你已经可以使用这样的语句来加载图片了:

    GlideApp.with(this)
            .load(url)
            .cacheSource()
            .into(imageView);

    有了这个强大的功能之后,我们使用Glide就能变得更加灵活了。

    结束语

    这样我们基本上就将Glide 4的所有重要内容都介绍完了,如果你以前非常熟悉Glide 3的话,看完这篇文章之后相信你已经能够熟练使用Glide 4了。而如果你以前并未接触过Glide,仅仅只看这一篇文章可能了解得还不够深入,建议最好还是把前面的七篇文章也去通读一下,这样你才能成为一名Glide好手。

    我翻了一下历史记录,在今年的3月21号发了这个系列的第一篇文章,用了10个月的时间终于把这个系列全部更新完了。当时承诺的是写八篇文章,如今兑现了承诺,也算是有始有终吧。未来我希望能继续给大家带来更好的技术文章,不过这个系列就到此为止了。也感谢有耐心的朋友能够看到最后,能坚持看完的人,你们都和我一样棒。

    关注我的技术公众号,每天都有优质技术文章推送。关注我的娱乐公众号,工作、学习累了的时候放松一下自己。

    微信扫一扫下方二维码即可关注:

            

    展开全文
  • FaceBook推出的Android图片加载库-Fresco

    千次阅读 2015-04-08 17:32:43
    在Android设备上面,快速高效的显示图片是极为重要的。过去的几年里,我们在如何高效的存储图像这方面遇到了很多问题。图片太大,但是手机的内存却很小。每一个像素的R、G、B和alpha通道总共要占用4byte的空间。

    FaceBook推出的Android图片加载库-Fresco

    在Android设备上面,快速高效的显示图片是极为重要的。过去的几年里,我们在如何高效的存储图像这方面遇到了很多问题。图片太大,但是手机的内存却很小。每一个像素的R、G、B和alpha通道总共要占用4byte的空间。如果手机的屏幕是480*800,那么一张屏幕大小的图片就要占用1.5M的内存。手机的内存通常很小,特别是Android设备还要给各个应用分配内存。在某些设备上,分给Facebook App的内存仅仅有16MB。一张图片就要占据其内存的十分之一。

    当你的App内存溢出会发生什么呢?它当然会崩溃!我们开发了一个库来解决这个问题,我们叫它Fresco。它可以管理使用到的图片和内存,从此App不再崩溃。

    内存区

    为了理解Facebook到底做了什么工作,在此之前我们需要了解在Android可以使用的堆内存之间的区别。Android中每个App的Java堆内存大小都是被严格的限制的。每个对象都是使用Java的new在堆内存实例化,这是内存中相对安全的一块区域。内存有垃圾回收机制,所以当App不在使用内存的时候,系统就会自动把这块内存回收。

    不幸的是,内存进行垃圾回收的过程正是问题所在。当内存进行垃圾回收时,内存不仅仅进行了垃圾回收,还把 Android 应用完全终止了。这也是用户在使用 App 时最常见的卡顿或短暂假死的原因之一。这会让正在使用 App 的用户非常郁闷,然后他们可能会焦躁地滑动屏幕或者点击按钮,但 App 唯一的响应就是:在 App 恢复正常之前,请求用户耐心等待

    相比之下,Native堆是由C++程序的new进行分配的。在Native堆里面有更多可用内存,App只被设备的物理可用内存限制,而且没有垃圾回收机制或其他东西拖后腿。但是c++程序员必须自己回收所分配的每一块内存,否则就会造成内存泄露,最终导致程序崩溃。

    Android有另外一种内存区域,叫做Ashmem。它操作起来更像Native堆,但是也有额外的系统调用。Android 在操作 Ashmem 堆时,会把该堆中存有数据的内存区域从 Ashmem 堆中抽取出来,而不是把它释放掉,这是一种弱内存释放模式;被抽取出来的这部分内存只有当系统真正需要更多的内存时(系统内存不够用)才会被释放。当 Android 把被抽取出来的这部分内存放回 Ashmem 堆,只要被抽取的内存空间没有被释放,之前的数据就会恢复到相应的位置。

    可消除的Bitmap

    Ashmem不能被Java应用直接处理,但是也有一些例外,图片就是其中之一。当你创建一张没有经过压缩的Bitmap的时候,Android的API允许你指定是否是可清除的。

    BitmapFactory.Options = new BitmapFactory.Options();
    options.inPurgeable = true;
    Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);

    经过上面的代码处理后,可清除的Bitmap会驻留在 Ashmem 堆中。不管发生什么,垃圾回收器都不会自动回收这些 Bitmap。当 Android 绘制系统在渲染这些图片,Android 的系统库就会把这些 Bitmap 从 Ashmem 堆中抽取出来,而当渲染结束后,这些 Bitmap 又会被放回到原来的位置。如果一个被抽取的图片需要再绘制一次,系统仅仅需要把它再解码一次,这个操作非常迅速。

    这听起来像一个完美的解决方案,但是问题是Bitmap解码的操作是运行在UI线程的。Bitmap解码是非常消耗CPU资源的,当消耗过大时会引起UI阻塞。因为这个原因,所以Google不推荐使用这个特性。现在它们推荐使用另外一个特性——inBitmap。但是这个特性直到Android3.0之后才被支持。即使是这样,这个特性也不是非常有用,除非 App 里的所有图片大小都相同,这对Fackbook来说显然是不适用的。一直到4.4版本,这个限制才被移除了。但我们需要的是能够运行在 Android 2.3 - 最新版本中的通用解决方案。

    自力更生

    对于上面提到的“解码操作致使 UI 假死”的问题,我们找到了一种同时使 UI 显示和内存管理都表现良好的解决方法。如果我们在 UI 线程进行渲染之前把被抽取的内存区域放回到原来的位置,并确保它再也不会被抽取,那我们就可以把这些图片放在 Ashmem 里,同时不会出现 UI 假死的问题。幸运的是,Android 的 NDK 中有一个函数可以完美地实现这个需求,名字叫做 AndroidBitmap_lockPixels。这个函数最初的目的就是:在调用 unlockPixels 再次抽取内存区域后被执行。

    当我们意识到我们没有必要这样做的时候,我们取得了突破。如果我们只调用lockPixels而不调用对应的unlockPixels,那么我们就可以在Java的堆内存里面创建一个内存安全的图像,并且不会导致UI线程加载缓慢。只需要几行c++代码,我们就完美的解决了这个问题。

    用C++的思想写Java代码

    就像《蜘蛛侠》里面说的:“能力越强,责任越大。”可清除的 Bitmap 既不会被垃圾回收器回收,也不会被 Ashmem 内置的清除机制处理,这使得使用它们可能会造成内存泄露。所以我们只能靠自己啦。

    在c++中,通常的解决方案是建立智能指针类,实现引用计数。这些需要利用到c++的语言特性——拷贝构造函数、赋值操作符和确定的析构函数。这种语法在Java之中不存在,因为垃圾回收器能够处理这一切。所以我们必须以某种方式在Java中实现C++的这些保证机制。

    我们创建了两个类去完成这件事。其中一个叫做“SharedReference”,它有addReference和deleteReference两个方法,调用者调用时必须采取基类对象或让它在范围之外。一旦引用计数器归零,资源处理(Bitmap.recycle)就会发生。

    然而,很显然,让Java开发者去调用这些方法是很容易出错的。Java语言就是为了避免做这样的事情的!所以SharedReference之上,我们构建了CloseableReference类。它不仅实现了Java的Closeable接口,而且也实现了Cloneable接口。它的构造器和clone()方法会调用addReference(),而close()方法会调用deleteReference()。所以Java开发者需要遵守下面两条简单的的规则:

    1. 在分配CloseableReference新对象的时候,调用.clone()。
    2. 在超出作用域范围的时候,调用.close(),这通常是在finally代码块中。

    这些规则可以有效地防止内存泄漏,并让我们在像Fackbook的Android客户端这种大型的Java程序中享受Native内存管理和通信。

    不仅仅是加载程序,它是一个管道

    在移动设备上显示图片需要很多的步骤:

    几个优秀的开源库都是按照这个顺序执行的,比如 Picasso,Universal Image Loader,Glide和 Volley等等。上面这些开源库为Android的发展做出了非常重要的贡献。我们相信Fresco在几个重要方面会表现的更好。

    我们的不同之处在于把上面的这些步骤看作是管道,而不仅仅是加载器。每一个步骤和其他方面应该是尽可能独立的,把数据和参数传递进去,然后产生一个输出,就这么简单。它应该可以做一些操作,不管是并行还是串行。一些操作只能在特性条件下才能执行。一些有特殊要求的在线程上执行。除此之外,当我们考虑改进图像的时候,所有的图片就会变得非常复杂。很多人在低网速情况下使用Facebook,我们想要这些人能够尽快的看到图片,甚至经常是在图片没有完全下载完之前。

    不要烦恼,拥抱stream

    在Java中,异步代码历来都是通过Future机制来执行的。在另外的线程里面代码被提交执行,然后一个类似Future的对象可以检查执行的结果是不是已经完成了。但是,这只在假设只有一种结果的情况下行得通。在处理渐进的图像的时候,我们希望可以完整而且连续的显示结果。

    我们的解决方式是定义一个更广义的Future版本,叫做DataSource。它提供了一个订阅方法,调用者必须传入一个DataSubscriber和Executor。DataSubscriber可以从DataSource获取到处理中和处理完毕的结果,并且提供了很简单的方法来区分。因为我们需要非常频繁的处理这些对象,所以必须有一个明确的close调用,幸运的是,DataSource本身就是Closeable。

    在后台,每一个箱子上面都实现了一个叫做“生产者/消费者”的新框架。在这个问题是,我们是从ReactiveX获取的灵感。我们的系统拥有和RxJava相似的接口,但是更加适合移动设备,并且有内置的对Closeables的支持。

    保持简单的接口。Producer只有一个叫做produceResults的方法,这个方法需要一个Consumer对象。反过来,Consumer有一个onNewResult方法。

    我们使用像这样的系统把Producer联系起来。假设我们有一个producer的工作是把类型I转化为类型O,那么它看起来应该是这个样子:

    public class OutputProducer<I, O> implements Producer<O> {
    
      private final Producer<I> mInputProducer;
    
      public OutputProducer(Producer<I> inputProducer) {
        this.mInputProducer = inputProducer;
      }
    
      public void produceResults(Consumer<O> outputConsumer, ProducerContext context) {
        Consumer<I> inputConsumer = new InputConsumer(outputConsumer);
        mInputProducer.produceResults(inputConsumer, context);
      }
    
      private static class InputConsumer implements Consumer<I> {
        private final Consumer<O> mOutputConsumer;
    
        public InputConsumer(Consumer<O> outputConsumer) {
          mOutputConsumer = outputConsumer;
        }
    
        public void onNewResult(I newResult, boolean isLast) {
          O output = doActualWork(newResult);
          mOutputConsumer.onNewResult(output, isLast);      
        }
      }
    }

    这可以使我们把非常复杂的步骤串起来,同时也可以保持他们逻辑的独立性。

    动画全覆盖

    使用Facebook的人都非常喜欢Stickers,因为它可以以动画形式存储GIF和Web格式。如果支持这些格式,就需要面临新的挑战。因为每一个动画都是由不止一张图片组成的,你需要解码每一张图片,存储在内存里,然后显示出来。对于大一点的动画,把每一帧图片放在内存是不可行的。

    我们建立了AnimatedDrawable,一个强大的可以呈现动画的Drawable,同时支持GIF和WebP格式。AnimatedDrawable实现标准的Android Animatable接口,所以调用者可以随意的启动或者停止动画。为了优化内存使用,如果图片足够小的时候,我们就在内存里面缓存这些图片,但是如果太大,我们可以迅速的解码这些图片。这些行为调用者是完全可控的。

    所有的后台都用c++代码实现。我们保持一份解码数据和元数据解析,如宽度和高度。我们引用技术数据,它允许多个Java端的Drawables同时访问一个WebP图像。

    如何去爱你?我来告诉你…

    当一张图片从网络上下载下来之后,我们想显示一张占位图。如果下载失败了,我们就会显示一个错误标志。当图片加载完之后,我们有一个渐变动画。通过使用硬件加速,我们可以按比例放缩,或者是矩阵变换成我们想要的大小然后渲染。我们不总是按照图片的中心进行放缩,那么我们可以自己定义放缩的聚焦点。有些时候,我们想显示圆角甚至是圆形的图片。所有的这些操作都应该是迅速而平滑的。

    我们之前的实现是使用Android的View对象——时机到了,可以使用ImageView替换出占位的View。这个操作是非常慢的。改变View会让Android强制刷新整个布局,当用户滑动的时候,这绝对不是你想看到的效果。比较明智的做法是使用Android的Drawables,它可以迅速的被替换。

    所以我们创建了Drawee。这是一个像MVC架构的图片显示框架。该模型被称为DraweeHierarchy。它被实现为Drawables的一个层,对于底层的图像而言,每一个曾都有特定的功能——成像、层叠、渐变或者是放缩。

    DraweeControllers通过管道的方式连接到图像上——或者是其他的图片加载库——并且处理后台的图片操作。他们从管道接收事件并决定如何处理他们。他们控制DraweeHierarchy实际上的操作——无论是占位图片,错误条件或是完成的图片。

    DraweeViews 的功能不多,但都是至关重要的。他们监听Android的View不再显示在屏幕上的系统事件。当图片离开屏幕的时候,DraweeView可以告诉DraweeController关闭使用的图像资源。这可以避免内存泄露。此外,如果它已经不在屏幕范围内的话,控制器会告诉图片管道取消网络请求。因此,像Fackbook那样滚动一长串的图片的时候,不会频繁的网络请求。

    通过这些努力,显示图片的辛苦操作一去不复返了。调用代码只需要实例化一个DraweeView,然后指定一个URI和其他可选的参数就可以了。剩下的一切都会自动完成。开发人员不需要担心管理图像内存,或更新图像流。Fresco为他们把一切都做了。

    Fresco

    完成这个图像显示和操作复杂的工具库之后,我们想要把它分享到Android开发者社区。我们很高兴的宣布,从今天起,这个项目已经作为开源代码了!

    壁画是绘画技术,几个世纪以来一直受到世界各地人们的欢迎。我们许多伟大的艺术家使用这种名字,从意大利文艺复兴时期的大师拉斐尔到壁画艺术家斯里兰卡。我们并不是假装达到这个伟大的水平,我们真的希望Android开发者能像我们当初享受创建这个开源库的过程一样,非常享受的使用它。

    更多

    Fresco中文文档

    展开全文
  • Fresco-FaceBook推出的Android图片加载库

    千次阅读 2016-06-23 14:02:22
    在Android设备上面,快速高效的显示图片是极为重要的。过去的几年里,我们在如何高效的存储图像这方面遇到了很多问题。图片太大,但是手机的内存却很小。每一个像素的R、G、B和alpha通道总共要占用4byte的空间。如果...
  • 【Android开发经验】FaceBook推出的Android图片加载库-Fresco   欢迎关注ndroid-tech-frontier开源项目,定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章 原文链接:...
  • 在Android设备上面,快速高效的显示图片是极为重要的。过去的几年里,我们在如何高效的存储图像这方面遇到了很多问题。图片太大,但是手机的内存却很小。每一个像素的R、G、B和alpha通道总共要占用4byte的空间。如果...
  • 考虑到模板样式的多端一致性、多样性、可配置性等特点,所以生成分享图片的功能有java服务端进行实现。刚开始接到任务的时候,想着这么简单的东西,应该很快就能完成,于是保守的估计了一天用来开发此功能;2.二维码...
  • Android N开发 你需要知道的一切

    万次阅读 2016-05-06 13:58:14
    Android N开发,你需要知道的一切......
  • python识别图片验证码

    千次阅读 2017-07-07 10:39:29
    验证码是目前互联网上非常常见也是非常重要的一个事物,充当着很多系统的 防火墙 功能,但是随时OCR技术的发展,验证码暴露出来的安全问题也越来越严峻。本文介绍了一套字符验证码识别的完整流程,对于验证码安全...
  • Glide 的流式接口让这个变得非常容易的去做到!只需要调用 .placeHolder() 用一个 drawable(resource) 引用,Glide 将会显示它作为一个占位符,直到你的实际图片准备好
  • 上一篇博文写的是Android 图片加载框架Glide4.0源码完全解析(一),主要分析了Glide4.0源码中的with方法和load方法。这篇是讲Glide源码中into方法的实现原理,可以说with和load方法只是做了前期的初始化配置工作,...
  • 在你的iPad上调整图片尺寸

    千次阅读 2019-10-05 02:44:05
    在我的上一篇文章中,我描述了试图用图片平铺的方式来解决在ipad上展示“大型图片”的问题的第一次尝试。在这种方式中,你把图片拉伸成不同的尺寸,然后把每个图片分割成一张张正方形的片段。通过使用Cocoa框架提供...
  • 关于Activity你应该知道的一切

    千次阅读 2016-11-27 21:22:24
    可以表示图片,文本,视频等不同的媒体格式 * URI包含的数据就比较多了,下面是URI的结构 <scheme>://<host>:<port>/[||] eg: http://www.baidu.com:80/search/info content://...
  • CEdit的一切

    万次阅读 2013-07-17 19:52:50
     CEdit从CWnd继承了重要的功能,要在CEdit对象中设置或获取文本,使用CWnd成员函数SetWindowText和GetWindowText,可以设置和得到编辑控件的全部内容,即使它是一个多行控件。如果编辑控件是多行的,使用CEdit成员...
  • Android绘制动态文字和图片

    千次阅读 2014-11-01 21:25:45
    这一讲我将带着大家来实现文字和图片的绘制,然后试着让文字和图片在屏幕里动起来。虽然,离真正的游戏还有一段距离,但是,这些都是游戏的基础,所以,大家都是需要掌握的。好的,不多说了,一起进入正题吧!  ...
  • 而随着各种高清视网膜屏幕的出现,现在web设计也需要考虑各种高清屏幕的显示效果,同样前端在代码实现的时候也需要根据屏幕的不同来输出不同分辨率的图片。为了使我们的网页能够适配视网膜屏幕上的高
  • java重要基础知识汇总

    万次阅读 多人点赞 2019-06-16 16:09:55
    [java] 关于main方法的一切 。 JVM,JDK,JRE三者的关系是什么 JVM指的是java virtual machine, 即java虚拟机。 是运行java代码的引擎。在其他主要的编程语言中,代码的编译器都只会为某个特定系统生成编译...
  • 如此一来,如果竖着拿手机拍摄时,就相当于对手机顺时针旋转了90度,也即上面相机图片中的最后一幅,那么它的 Orientation 值为6。 验证EXIF 在经过上面的分析之后,我们来看看实际情况...
  • 【译者预读】面对海量小文件的存储和检索,Google发表了GFS,淘宝开源了TFS,而Facebook又是如何应对千亿级别的图片存储、每秒百万级别的图片查询?Facebook与同样提供了海量图片服务的淘宝,解决方案有何异同?本篇...
  • Android中图片的三级缓存策略

    千次阅读 2016-07-26 22:25:05
    最近最少使用算法体现在,当有一个值被访问的时候,这个值就会被移动到队列的对头,而当一个值添加的时候恰好达到LruCache申请的缓存空间,那么处于队尾的值就会被踢出队列,由于该值不再是缓存cache持有的对象,...
  •  "勇气者"号登上了火星,它此行的一个重要任务是寻找水存在的迹象,找到水,这颗神秘的红色星球上才有生命存在的可能,未来移民火星才不是一个天方夜谭的梦想。西方科学家说,水是生命的源头,最早的生命来自海洋,...
  • 慢慢来,一切都来得及

    千次阅读 2012-02-26 21:54:54
    慢慢来,一切都来得及 2012-01-16 15:02:22 1、人人都怕来不及 上周我的情绪一直很低落,除了情绪低落还伴随着持续地焦虑,因为我感觉到很多事情来不及去做,焦虑自己工作上的项目大半个月都没有进展...
  • Android Service完全解析,关于服务你所需知道的一切

    万次阅读 多人点赞 2014-01-25 11:02:22
    Android Service完全解析,关于服务你所需知道的一切(上) 分类: Android疑难解析2013-10-31 08:10 6451人阅读 评论(39) 收藏 举报 AndroidService服务后台前台 目录(?)[+] 转载请...
  • 关于Faster R-CNN的一切——笔记3:Faster R-CNN

    千次阅读 热门讨论 2016-12-20 09:15:46
    博主课题需要研究Faster RCNN,先后看了RCNN、Fast RCNN和...关于Faster R-CNN的一切——笔记1:R-CNN   关于Faster R-CNN的一切——笔记2:Fast R-CNN   三、Faster R-CNN【Faster R-CNN--Towards Real-Ti...
  • 谷歌更是没什么人注意的小角色,从图片中那俩创始人的个头就可见一斑;而比尔盖茨头像的肌肉男就代表着当时如日中 天的微软,当时的苹果对他们真的没什么威胁,只是盖茨不明白乔布斯为什么知道要失败还是会去做。 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 29,288
精华内容 11,715
关键字:

一切不再重要的图片