首页 > 系统 > Android > 正文

Android Service服务详细介绍及使用总结

2019-12-12 04:27:02
字体:
来源:转载
供稿:网友

Android Service服务详解

一.Service简介

       Service是android 系统中的四大组件之一(Activity、Service、BroadcastReceiver、 ContentProvider),它跟Activity的级别差不多,但不能页面显示只能后台运行,并且可以和其他组件进行交互。service可以在很多场合的应用中使用,比如播放多媒体的时候用户启动了其他Activity这个时候程序要在后台继续播放,比如检测SD卡上文件的变化,再或者在后台记录你地理信息位置的改变等等,总之服务总是藏在后台的,例如,一个service可能处理网络 事物,播放音乐,执行文件I/O,或与一个内容提供者交互,所有这些都在后台进行。
       我们一定要知道的是这里Service的后台运行并不是子线程。Service的运行是在主线程中进行的,只是它没有界面显示而已,它的耗时操作同样需要开启子线程,否者会跟Activity一样出现ANR(application not response程序没有响应)。
       我们要知道的是主线程的内容包括UI和后台。只要程序中的UI或后台其中一个在跑,程序都算是在运行状态。

(一)Service的创建和注册1.Service服务的创建

必须要实现重写其中的onBind方法,可以在里面做各种操作,也可以接收传递过来的Intent的数据做处理。

public class MyService extends Service {  @Nullable  @Override  public IBinder onBind(Intent intent) {    System.out.println("MyService.onBind");    return null;  } }

2.Service服务的注册,在AndroidManifest中注册

<service android:name=".MyService" />

       服务的注册是四大组件中最简单的一个,一般只要设置name属性就可以了。但是如果有其他需求还是要设置其他的属性值的。

       对Service服务做好创建和注册后,就可以操作服务了。

(二)Service两种启动模式

Service的启动有两种方式:Context.startService() 和 Context.bindService()。这里的Context是上下文的意思。

1.startService()方式启动时的生命周期回调方法

(1)启动服务startService : >onCreate()> onStart()
(2)停止服务stopService : >onDestroy()
       如果调用者直接退出而没有停止Service,则Service 会一直在后台运行。这里的退走只是关闭了UI界面。
       startService()方法启动服务,在服务未被创建时,系统会先调用服务的onCreate()方 法,接着调用onStart()方法。如果调用startService()方法前服务已经被创建,多次调用 startService()方法并不会导致多次创建服务,但会导致多次调用onStart()方法。采用 startService()方法启动的服务,只能调用stopService()方法结束服务,服务结束时 会调用生命周期的onDestroy()方法。

2.bindService()方式启动时的生命周期回调方法

(1)绑定bindService : > onCreate() > onBind()
(2)解绑unbindService: >onUnbind()
(3)正常停止程序服务的方法是先解绑unbindService,再停止服务stopService。
(4)如果绑定后调用stopService 方法,这时是不能停止服务的,如果这时再调用解绑unbindService,程序后先解绑,后停止服务。
       用bindService()方法启动服务,在服务未被创建时,系统会先调用服务的onCreate() 方法,接着调用onBind()方法。这个时候调用者和服务绑定在一起,调用者退出了,系统就会
先调用服务的onUnbind()方法,接着调用onDestroy()方法。如果调用       bindService()方法前服务已经被绑定,多次调用bindService()方法并不会导致多次创建服务及绑定(也就是说 onCreate()和onBind()方法并不会被多次调用)。如果调用者希望与正在绑定的服务解除绑 定,可以调用unbindService()方法,调用该方法也会导致系统调用服务的onUnbind()->onDestroy()方法。

绑定Service方法:bindService(intent, conn, Service.BIND_AUTO_CREATE);
三个参数的说明:
第一个:Intent对象
第二个:ServiceConnection对象,创建该对象要实现它的onServiceConnected()和 on ServiceDisconnected()来判断连接成功或者是断开连接
第三个:创建Service的模式,一般指定绑定的时候自动创建

(三)Service的五个生命周期的回调方法

1.周期命名

(1)onCreate()
(2)onStart()
(3)onBind()
(4)onUnBind()
(5)onDestroy()

2.生命周期图解

sheng
上面展示的是没有绑定服务和有绑定服务的生命周期的不同情况的过程。

3.关于几个方法的说明

(1)onCreate()说明服务第一次被创建
(2)onStartComand()说明服务开始工作
(3)onBind()说明服务已经绑定
(4)onUnBind()说明服务已经解绑
(5)onDestroy()说明服务已经停止
       正如上面说的启动服务有两种方式,一个是使用startService,另一个方法是使用bindService方法;使用bindService方法没有回调到startCommand方法;
       也可以先启动服务用startService,再绑定服务用bindService,这时的Service的回调方法的顺序是:
       >onCreate()>onStartCommand()>onBind()

二.IntentService

       普通的Service要创建一个线程去完成耗时操作,因为其运行在主线程,且要手停止IntentService是继承于Service并处理异步请求的一个类,在IntentService内有一个工作线程 来处理耗时操作,启动IntentService的方式和启动传统Service一样,同时,当任务执行完 后,IntentService会自动停止,而不需要我们去手动控制。
       另外,可以启动IntentService多次,而每一个耗时操作会以工作队列的方式在IntentService的onHandleIntent回调方法中执行,并且,每次只会执行一个工作线程,执行完第一个再执行第二个,以此类推。 而且,所有请求都在一个单线程中,不会阻塞应用程序的主线程(UI Thread),同一时间只处理一个请求。
       那么,用IntentService有什么好处呢?
       首先,我们省去了在Service中手动开线程的麻烦,
       第二,当操作完成时,我们不用手动停止Service IntentService,一个方便我们处理业务流程的类,它是一个Service,但是比Service更智能

三.查看Service的生命周期回调方法的简单示例

本示例只是用来看看Service在服务开启时,停止时,绑定时,解绑时,生命周期方法的回调情况加深对Service生命周期的印象。

(一)创建MyService类(继承Service)

package com.lwz.service;import android.app.Service;import android.content.Intent;import android.os.IBinder;import android.support.annotation.Nullable;  /**   * 服务的创建,   * 测试生命周期的过程和先后   * 五个生命周期:   * onCreate   * onStartCommand   * onDestroy   * onBind   * onUnBind   */public class MyService extends Service {  @Nullable  @Override  public IBinder onBind(Intent intent) {    System.out.println("MyService.onBind");    return null;  }  @Override  public void onCreate() {    System.out.println("MyService.onCreate");    super.onCreate();  }  @Override  public int onStartCommand(Intent intent, int flags, int startId) {    System.out.println("MyService.onStartCommand");    return super.onStartCommand(intent, flags, startId);  }  @Override  public void onDestroy() {    System.out.println("MyService.onDestroy");    super.onDestroy();  }  @Override  public boolean onUnbind(Intent intent) {    System.out.println("MyService.onUnbind");    return super.onUnbind(intent);  }}

(二)在AndroidManifest中注册服务

<service android:name=".MyService" />

(三)设计控制服务开启、停止、绑定、解绑状态的代码

package com.lwz.service;import android.app.Service;import android.content.ComponentName;import android.content.Intent;import android.content.ServiceConnection;import android.os.Bundle;import android.os.IBinder;import android.support.v7.app.AppCompatActivity;import android.view.View;/** * 服务的创建和使用 * 注意这里的服务不依赖于Activity页面,即使页面关闭了,服务没有主动去停止,是不会关闭的 * Service也是在主线程中执行任务的,但是为什么不会造成主线程阻塞?? * 因为做的不是耗时操作,如果做耗时操作一样会造成ANR。。。 * 这里点击绑定服务后,点击停止服务按钮是无效的,要先解绑后,才能停止服务。 * 正常情况下,从绑定状态到解绑状态是不会停止服务的。只是一种状态改变而已。 * 这里点击绑定服务后,点击停止服务按钮是无效的,但是解绑后,会马上停止服务。 */public class MainActivity extends AppCompatActivity {  @Override  protected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);  }  //开启服务  public void startService(View view) {    //开启服务需要Intent对象,和Activity跳转类似    startService(new Intent(this, MyService.class));  }  //停止服务  public void stopService(View view) {    //停止服务的方法    stopService(new Intent(this, MyService.class));  }  //绑定服务  public void bindService(View view) {    //绑定服务    bindService(new Intent(this, MyService.class), conn, flags);  }  //解绑服务  public void unBindService(View view) {    //防止在没有绑定的情况下,去解除绑定,抛出异常    try {      //解除绑定      unbindService(conn);    } catch (Exception e) {      System.out.println("MainActivity.unBindService" + e);    }  }  //服务绑定的连接对象  private ServiceConnection conn = new ServiceConnection() {    @Override    public void onServiceConnected(ComponentName name, IBinder service) {    }    @Override    public void onServiceDisconnected(ComponentName name) {    }  };  //服务绑定的标识  //BIND_AUTO_CREATE 绑定的同时,启动Service  private int flags = Service.BIND_AUTO_CREATE;}

上面是使用四个按钮来实现服务的几种状态的改变。

(四)布局文件的设计

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  android:id="@+id/activity_main"  android:layout_width="match_parent"  android:layout_height="match_parent"  android:orientation="vertical">  <TextView    android:layout_width="wrap_content"    android:layout_height="wrap_content"    android:layout_gravity="center_horizontal"    android:text="Service" />  <Button    android:layout_width="wrap_content"    android:layout_height="wrap_content"    android:onClick="startService"    android:text="启动服务" />  <Button    android:layout_width="wrap_content"    android:layout_height="wrap_content"    android:onClick="stopService"    android:text="停止服务" />  <Button    android:layout_width="wrap_content"    android:layout_height="wrap_content"    android:onClick="bindService"    android:text="绑定服务" />  <Button    android:layout_width="wrap_content"    android:layout_height="wrap_content"    android:onClick="unBindService"    android:text="解绑服务" /></LinearLayout>

上面布局文件代码比较简单的,只是用四个按钮搞定!

程序运行后显示的界面:
s1

点击“启动服务”按钮后的Log信息:
s2
这时有两个回调方法执行。onCreate和onStartCommand.

点击“停止服务”按钮后的Log信息:
s3
执行了一个回调方法:onDestroy;这时服务已经停止,相当于程序刚运行的状态!

点击“绑定服务”按钮后的Log信息:
s4
这里执行了两个回调方法,先启动服务,再绑定服务!
在绑定服务的情况下是不能停止服务的,要解绑服务才能停止服务。
在程序的服务启动/绑定了的情况下,再点击启动服务,只会回调onStartCommand方法,也就是说一个服务在一个生命周期内只会回调一次onCreate方法。

点击“解绑服务”按钮后显示的Log信息:
s5
执行了两个回调方法:onUnBind和onDestroy

       如果是用服务做简单 的事情,使用第一种方法来启动服务就可以了,但是如果需要涉及到比较复杂的数据处理和操作就用绑定服务的方法来启动服务。

四.IntentService的使用示例

程序设计:查找手机SD卡中的所有图片显示在UI界面上。

//(一)布局文件activity_main.xml文件实际<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  android:id="@+id/activity_main"  android:layout_width="match_parent"  android:layout_height="match_parent">  <ListView    android:id="@+id/main_lv"    android:layout_width="match_parent"    android:layout_height="match_parent"></ListView></RelativeLayout>

非常简单的布局设计,使用List View来显示所有的图片

(二)遍历文件的工具类的设计

package com.lwz.intentservice;import android.os.Environment;import android.os.StatFs;import java.io.File;import java.util.ArrayList;import java.util.List;/**  * SD卡的路径:Environment.getExternalStorageDirectory() */public class FileUtils {    /**   * 获得指定目录下的所有的图片   */  public static final ArrayList<File> getAllPicture(File dir) {    ArrayList<File> files = getAllFile(dir);    ArrayList<File> imgList = new ArrayList<>();    for (File file : files) {      if (file.getName().endsWith(".png") || file.getName().endsWith(".jpg"))        imgList.add(file);    }    return imgList;  }  /**   * 递归遍历文件夹的方法   */  public static final void getFileFromDir(File dir, List<File> fileList) {    File[] files = dir.listFiles();    if (files == null)      return;    for (File file : files) {      if (file.isDirectory())        getFileFromDir(file, fileList);      fileList.add(file);    }  }     /**   * 获得根目录下的所有图片   */  public static final ArrayList<File> getAllPicture() {    return getAllPicture(Environment.getExternalStorageDirectory());  }  }

(三)简化BaseAdapter的一个工具类

package com.lwz.intentservice;import android.content.Context;import android.widget.BaseAdapter;import java.util.ArrayList;import java.util.List;/** * 这是一个简化BaseAdapter适配器的工具类 * 这是使用的是定义一个泛型T,使用时传入什么数据,T就是什么数据 * 实际设计中除了getVIew方法外,其他的方法基本是差不多的 * 所以继承这个工具类后只要重写getView方法,就可以使用BaseAdapter了 */public abstract class ListItemAdapter<T> extends BaseAdapter {  List<T> list = new ArrayList<>();  Context context;  ListItemAdapter(Context context, List<T> list) {    this.context = context;    this.list = list;  }  ListItemAdapter(Context context, T[] list) {    this.context = context;    for (T t : list) {      this.list.add(t);    }  }  @Override  public int getCount() {    return list == null ? 0 : list.size();  }  @Override  public T getItem(int position) {    return list == null ? null : list.get(position);  }  @Override  public long getItemId(int position) {    return position;  }}

(四)MyIntentService的创建

package com.lwz.intentservice;import android.app.IntentService;import android.content.Intent;import android.os.Message;import android.util.Log;import java.io.File;import java.util.ArrayList;/** * IntentService的使用 * IntentService是Service的子类,也需要在xml中注册 * 它有自定义的子线程的方法 * 这里主要需要解决的问题是资源文件得到后怎么把数据传递给UI线程的Activity */public class MyIntentService extends IntentService {  /**   * 通过构造方法,传入子线程的名字   * 但是这里必须要创建一个无参的构造方法   */  public MyIntentService() {    super("myService");  }  /**   * 这是在子线程中的执行操作   */  @Override  protected void onHandleIntent(Intent intent) {    Log.e("TAG", "子线程开始工作");    //遍历文件夹获取图片    ArrayList<File> list = FileUtils.getAllPicture();    //使用handler发送信息    Message msg = Message.obtain();    //这里给handler对象传递一个对象    msg.obj = list;    //发送广播来传递数据    Intent intent1 = new Intent("filefinish");    intent1.putExtra("file", list);    sendBroadcast(intent1);  }  @Override  public void onCreate() {    super.onCreate();    Log.e("TAG", "onCreate");  }  @Override  public void onDestroy() {    super.onDestroy();    Log.e("TAG", "onDestroy");  }}

这里的Intent Service的注册和Intent的注册是一样的。

(五)MainActivity中的代码设计

package com.lwz.intentservice;import android.content.BroadcastReceiver;import android.content.Context;import android.content.Intent;import android.content.IntentFilter;import android.net.Uri;import android.os.Bundle;import android.support.v7.app.AppCompatActivity;import android.view.View;import android.view.ViewGroup;import android.widget.ImageView;import android.widget.ListView;import java.io.File;import java.util.ArrayList;import java.util.List;/** * 这里使用服务来IntentService来遍历文件夹 * 在程序创建的使用就要启动服务 * 在页面销毁的时候就停止服务 但是Service执行完任务后还有传递数据给MainActivity 在MainActivity中才能进行UI界面的更新 这就涉及到Service和Activity的数据传递问题了 这里使用的是用广播来传递数据 */public class MainActivity extends AppCompatActivity {  //定义布局内的控件  ListView listView;  //定义适配器的数据的集合  //一定要static???  static ArrayList<File> fileList;  static MyBaseAdapter adapter;  MyBroadcastReceiver mbcr;  @Override  protected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);    mbcr = new MyBroadcastReceiver();    //动态注册一个广播    IntentFilter filter = new IntentFilter();    filter.addAction("filefinish");    registerReceiver(mbcr, filter);// 注册    //创建适配器的对象    adapter = new MyBaseAdapter(this, fileList);    //实例化布局内的控件    listView = (ListView) findViewById(R.id.main_lv);    //给listView设置适配器    listView.setAdapter(adapter);    //启动服务    startService(new Intent(this, MyIntentService.class));  }  //创建适配器的类  class MyBaseAdapter extends ListItemAdapter<File> {    MyBaseAdapter(Context context, List<File> list) {      super(context, list);    }    @Override    public View getView(int position, View convertView, ViewGroup parent) {      ImageView image = null;      if (convertView == null) {        image = new ImageView(getBaseContext());        convertView = image;      } else {        image = (ImageView) convertView;      }      //设置图片资源和属性      image.setImageURI(Uri.fromFile(fileList.get(position)));      image.setScaleType(ImageView.ScaleType.FIT_XY);      image.setAdjustViewBounds(true);      return image;    }  }  //停止服务  public void stop() {    stopService(new Intent(MainActivity.this, MyIntentService.class));  }  @Override  protected void onDestroy() {    super.onDestroy();    //即使之前停止了服务,再次停止服务也是不会报错的    stop();    //解除广播    unregisterReceiver(mbcr);  }  //动态创建广播接收者  class MyBroadcastReceiver extends BroadcastReceiver {    @Override    public void onReceive(Context context, Intent intent) {      //对接收到的广播进行处理,intent里面包含数据      fileList = (ArrayList<File>) intent.getSerializableExtra("file");      //刷新适配器      adapter.notifyDataSetChanged();      //停止服务,它的子线程也会停止      stop();    }  }}

      程序运行前还记得加上SD卡的访问权限;
      上面程序功能还是有点问题!遍历完文件后。页面没有马上更新?退出程序再进来,页面上马上显示SD卡的图片。
      一般的说遍历文件夹也不算是耗时操作,这里只是简单示范。
      一般的耗时操作是从网络下载数据,或本地移动大文件等等。

五.同一个程序中Service和Activity通信的一些方式,这里展示主要代码。

这里组件不要忘记在AndroidManifest中注册

(一)使用Intent来传递数据1.MainActivity中的代码,发送数据

Intent intent = new Intent(this, MyService.class);

intent.putExtra("msg", "activity向service传递一个hello service");

startService(intent);

2.MyService中的代码,接收数据

      这里要使用onStartCommand的方法来接收Intent的数据,如果上面使用的是bind的方法来启动服务,这里可以在onBind方法中接收数据。

@Override  public int onStartCommand(Intent intent, int flags, int startId) {    Log.e("onStartCommand", intent.getStringExtra(msg));    return super.onStartCommand(intent, flags, startId);  }

(二)使用单例模式来传递数据

      这个方法算是有点麻烦的吧!这里要在MyService类中先做好单例,然后在Activity中调用MyService对象的方法

1.MyService中的代码

//定义一个静态的类变量,单例的使用准备  private static MyService instance;  //静态方法,返回的是一个本类对象  //为了能让另一边的类调用Myservice的方法  public static MyService getInstance() {    return instance;  }  @Override  public void onCreate() {    super.onCreate();    //单例模式变量赋值    instance = this;  }public void print(String msg) {    Log.e("service", msg);  }

      其中print方法是在Activity中调用的,可以达到传送数据给MyService,但是这里要先启动过服务后才能使用单例,因为这里是在MyService的onCreate方法中把对象赋值给instance,之后才能实现单例。

2.MainActivity中的代码:

/**   * 单例模式传参   *MyService这里通过一个静态方法,来获得MyService的对象这里通过MyService.getInstance()方法来获得MyService对象   */      //必须保证Myservice对象不能为null    //静态的变量,最后释放(不用的时候,手动将static变量=null)    if (MyService.getInstance() != null) {      MyService.getInstance().print("使用单例从activity中调用service的方法");    }

(三)广播传参传数据

      弄两个广播接收者相互传数据。 

      这里要在MyService和MyService中分别动态的创建广播接收者和动态注册广播接收者,然后在MainActivity中发送广播,在MyService中接收到广播传来递数据后,在发送广播,让MainActivity接收广播数据!

1.MyService中的代码:

@Override  public void onCreate() {    super.onCreate();    //动态注册广播接收者,要定义好接收的action属性值    IntentFilter filter = new IntentFilter("service");    registerReceiver(serviceReceiver, filter);  } //定义一个广播接收者BroadcastReceiver  BroadcastReceiver serviceReceiver = new BroadcastReceiver() {    @Override    public void onReceive(Context context, Intent intent) {      Log.e("service", "接收到了activity发送的广播:" + intent.getStringExtra("msg"));//发送广播给MainActivity      sendBroadcast(new Intent("activity").putExtra("msg", "发送给activity的消息"));    }  };

2.MainActivity中的代码:

@Override  protected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);    //注册本类内的广播,定义好action的属性值    IntentFilter filter = new IntentFilter("activity");    registerReceiver(activityReceiver, filter);  } /**   * 通过广播来传递数据   */  public void sendBroadcast(View view) {    //指明action属性值    Intent intent = new Intent("service");    intent.putExtra("msg", "activity向广播传递一个hello broadcast");    sendBroadcast(intent);  } //定义一个内部类的广播接收者,用于接收MyService传递过来的数据  BroadcastReceiver activityReceiver = new BroadcastReceiver() {    @Override    public void onReceive(Context context, Intent intent) {      Log.e("activity", intent.getStringExtra("msg"));    }  };

(四)MyService实例调用方法

      这个方法是最最麻烦的方法了!涉及到一个Binder类的使用!这里要被调用的方法其实不是MyService中的方法,而是里面的内部接口的抽象方法,需要在MainActivity中去实现这个方法!但是,实际这个方法实在MyService中执行的。

1.MyService中的代码:

//定义一个接口  interface Callback {    //定义两个要实现的方法    void call();    void start();  }  //定义一个接口对象  Callback callback;  /**   * 创建Binder类,很多很多的Service就是通过Binder机制来和客户端通讯交互的。   */  class Mybinder extends Binder {    public MyService getService() {      return MyService.this;    }    //设置回调方法    public void setCallback(Callback callback) {      MyService.this.callback = callback;    }  }  //定义一个模拟开始音乐播放的方法  //需要重写start里面的方法来开始播放音乐  public void startMusic() {    //播放    Toast.makeText(this, "音乐开始播放", Toast.LENGTH_SHORT).show();    callback.start();  } @Nullable  @Override  public IBinder onBind(Intent intent) {    //要传递一个MyBinder对象给MainActivity    return new MyBinder();  }//定义一个模拟开始音乐播放的方法  //需要重写start里面的方法来开始播放音乐  public void startMusic() {    //播放    Toast.makeText(this, "音乐开始播放", Toast.LENGTH_SHORT).show();//在MainActivity中实例化callback对象    callback.start();  }

      上面的代码中要开始播放音乐要调用startMusic方法,并且要实例化里面的callback对象,而要实例化callback对象必须要调用内部类Mybinder的set Callback方法,而实现这个方法又必须实现这个接口的方法!

2.MainActivity中的代码:

/**   * 绑定服务Service调用MyService的方法来调用内部类的接口方法   *///定义一个MyService对象MyService myService;  public void bindService(View view) {    bindService(new Intent(this, MyService.class), conn, BIND_AUTO_CREATE); myService.startMusic();  }//创建ServiceConnection对象 ServiceConnection conn = new ServiceConnection() {    @Override    public void onServiceConnected(ComponentName name, IBinder service) {      //连接服务后,操作。。。      //获取IBinder的bind对象,从MyService的onBinder中传递过来的      MyService.Mybinder bind = (MyService.Mybinder) service;      //通过bind对象获取Service对象      myService = bind.getService();      //设置监听事件的回调方法,并实现里面的两个方法//这里的回调方法不是MyService中的,而是内部类Mybinder中的      bind.setCallback(new MyService.Callback() {        @Override        public void call() {          Log.e("activity", "Service回调Activity");        }        @Override        public void start() {          //比如在后台播放音乐;开始播放音乐          Log.e("action", "正在播放音乐");          //关闭页面          finish();        }      });

      上面的关系确实是有点乱,我发现我有些注解还是有点问题的!! 

      上面就是Service中的个方面的总结。 

      Service还可以用来做进程间的数据传递,这里就涉及到AIDL(Android Interface Definition Language,安卓接口定义语言)进程通信。这个相对来说比较复杂,另作总结!

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表